Commit 32efaacb by Augusto

import advanced teams and matches, search update

parent d6fd393a
...@@ -1103,6 +1103,80 @@ const docTemplate = `{ ...@@ -1103,6 +1103,80 @@ const docTemplate = `{
} }
} }
}, },
"/import/players/advancedstats": {
"post": {
"description": "Single: provide playerWyId+competitionId+seasonId. Batch: omit playerWyId (imports all players for that competition+season). Auto: omit all IDs (imports distinct player_wy_id+competition_id+season_id combos from player_careers; resumable).",
"tags": [
"Import"
],
"summary": "Import player advanced stats from Wyscout",
"parameters": [
{
"type": "integer",
"description": "Wyscout player ID (optional; omit for batch/auto modes)",
"name": "playerWyId",
"in": "query"
},
{
"type": "integer",
"description": "Wyscout competition ID (required for single/batch; omit for auto mode)",
"name": "competitionId",
"in": "query"
},
{
"type": "integer",
"description": "Wyscout season ID (required for single/batch; omit for auto mode)",
"name": "seasonId",
"in": "query"
},
{
"type": "integer",
"description": "Optional limit on number of requests (batch: players; auto: combos)",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Auto mode only: concurrent workers (default 4)",
"name": "workers",
"in": "query"
},
{
"type": "boolean",
"description": "Auto mode only: reset checkpoint and restart from beginning",
"name": "reset",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/import/players/career": { "/import/players/career": {
"post": { "post": {
"description": "Fetches /v3/players/{playerWyId}/career for players already present in the DB (players.wy_id not null). Upserts records into player_careers.", "description": "Fetches /v3/players/{playerWyId}/career for players already present in the DB (players.wy_id not null). Upserts records into player_careers.",
...@@ -1457,6 +1531,74 @@ const docTemplate = `{ ...@@ -1457,6 +1531,74 @@ const docTemplate = `{
} }
} }
}, },
"/import/teams/career": {
"post": {
"description": "Fetches /v3/teams/{teamWyId}/career?details=competition,season for teams in the DB (teams.wy_id not null) plus teams referenced in team_children. Upserts records into team_careers.",
"tags": [
"Import"
],
"summary": "Import team career stats from Wyscout",
"parameters": [
{
"type": "integer",
"description": "Process only one Wyscout team wy_id",
"name": "teamWyId",
"in": "query"
},
{
"type": "integer",
"description": "Limit number of teams processed when teamWyId is omitted",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Concurrent workers (default 8)",
"name": "workers",
"in": "query"
},
{
"type": "integer",
"description": "DB batch size for reading team IDs (default 2000)",
"name": "batchSize",
"in": "query"
},
{
"type": "integer",
"description": "Global request rate limit (default 12)",
"name": "rps",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/import/teams/images": { "/import/teams/images": {
"post": { "post": {
"description": "Fetches /v3/teams/{wyId} for a specific team (or for teams in DB when wyId is omitted) and stores imageDataURL on the team and its children.", "description": "Fetches /v3/teams/{wyId} for a specific team (or for teams in DB when wyId is omitted) and stores imageDataURL on the team and its children.",
...@@ -1888,6 +2030,12 @@ const docTemplate = `{ ...@@ -1888,6 +2030,12 @@ const docTemplate = `{
"in": "query" "in": "query"
}, },
{ {
"type": "string",
"description": "Filter players by gender (male/female)",
"name": "gender",
"in": "query"
},
{
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
...@@ -2080,6 +2228,102 @@ const docTemplate = `{ ...@@ -2080,6 +2228,102 @@ const docTemplate = `{
} }
} }
}, },
"/players/wyscout/{wyId}/advancedpositions": {
"get": {
"description": "Returns the positions played by season for a player, plus an average distribution across the last 5 seasons.",
"tags": [
"Players"
],
"summary": "Get player advanced positions",
"parameters": [
{
"type": "integer",
"description": "Wyscout player wy_id",
"name": "wyId",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/players/wyscout/{wyId}/advancedstats": {
"get": {
"description": "Returns player advanced stats averages: anchor season average, last 2 years average, last 5 years average. If seasonId is omitted, the latest season found for the player is used as anchor.",
"tags": [
"Players"
],
"summary": "Get player advanced stats averages",
"parameters": [
{
"type": "integer",
"description": "Wyscout player wy_id",
"name": "wyId",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "Anchor season wy_id (optional; defaults to latest season available)",
"name": "seasonId",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/players/{id}": { "/players/{id}": {
"get": { "get": {
"description": "Returns a single player by its internal ID.", "description": "Returns a single player by its internal ID.",
...@@ -2646,6 +2890,42 @@ const docTemplate = `{ ...@@ -2646,6 +2890,42 @@ const docTemplate = `{
"description": "Number of items to skip before starting to collect the result set (default 0)", "description": "Number of items to skip before starting to collect the result set (default 0)",
"name": "offset", "name": "offset",
"in": "query" "in": "query"
},
{
"type": "string",
"description": "Filter teams by name",
"name": "name",
"in": "query"
},
{
"type": "string",
"description": "Filter teams by gender (male/female)",
"name": "gender",
"in": "query"
},
{
"type": "string",
"description": "Filter teams by type (club/national)",
"name": "teamType",
"in": "query"
},
{
"type": "string",
"description": "Alias for teamType (club/national)",
"name": "type",
"in": "query"
},
{
"type": "string",
"description": "If truthy, filter teams by type=club",
"name": "club",
"in": "query"
},
{
"type": "string",
"description": "If truthy, filter teams by type=national",
"name": "national",
"in": "query"
} }
], ],
"responses": { "responses": {
......
...@@ -1096,6 +1096,80 @@ ...@@ -1096,6 +1096,80 @@
} }
} }
}, },
"/import/players/advancedstats": {
"post": {
"description": "Single: provide playerWyId+competitionId+seasonId. Batch: omit playerWyId (imports all players for that competition+season). Auto: omit all IDs (imports distinct player_wy_id+competition_id+season_id combos from player_careers; resumable).",
"tags": [
"Import"
],
"summary": "Import player advanced stats from Wyscout",
"parameters": [
{
"type": "integer",
"description": "Wyscout player ID (optional; omit for batch/auto modes)",
"name": "playerWyId",
"in": "query"
},
{
"type": "integer",
"description": "Wyscout competition ID (required for single/batch; omit for auto mode)",
"name": "competitionId",
"in": "query"
},
{
"type": "integer",
"description": "Wyscout season ID (required for single/batch; omit for auto mode)",
"name": "seasonId",
"in": "query"
},
{
"type": "integer",
"description": "Optional limit on number of requests (batch: players; auto: combos)",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Auto mode only: concurrent workers (default 4)",
"name": "workers",
"in": "query"
},
{
"type": "boolean",
"description": "Auto mode only: reset checkpoint and restart from beginning",
"name": "reset",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/import/players/career": { "/import/players/career": {
"post": { "post": {
"description": "Fetches /v3/players/{playerWyId}/career for players already present in the DB (players.wy_id not null). Upserts records into player_careers.", "description": "Fetches /v3/players/{playerWyId}/career for players already present in the DB (players.wy_id not null). Upserts records into player_careers.",
...@@ -1450,6 +1524,74 @@ ...@@ -1450,6 +1524,74 @@
} }
} }
}, },
"/import/teams/career": {
"post": {
"description": "Fetches /v3/teams/{teamWyId}/career?details=competition,season for teams in the DB (teams.wy_id not null) plus teams referenced in team_children. Upserts records into team_careers.",
"tags": [
"Import"
],
"summary": "Import team career stats from Wyscout",
"parameters": [
{
"type": "integer",
"description": "Process only one Wyscout team wy_id",
"name": "teamWyId",
"in": "query"
},
{
"type": "integer",
"description": "Limit number of teams processed when teamWyId is omitted",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Concurrent workers (default 8)",
"name": "workers",
"in": "query"
},
{
"type": "integer",
"description": "DB batch size for reading team IDs (default 2000)",
"name": "batchSize",
"in": "query"
},
{
"type": "integer",
"description": "Global request rate limit (default 12)",
"name": "rps",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/import/teams/images": { "/import/teams/images": {
"post": { "post": {
"description": "Fetches /v3/teams/{wyId} for a specific team (or for teams in DB when wyId is omitted) and stores imageDataURL on the team and its children.", "description": "Fetches /v3/teams/{wyId} for a specific team (or for teams in DB when wyId is omitted) and stores imageDataURL on the team and its children.",
...@@ -1881,6 +2023,12 @@ ...@@ -1881,6 +2023,12 @@
"in": "query" "in": "query"
}, },
{ {
"type": "string",
"description": "Filter players by gender (male/female)",
"name": "gender",
"in": "query"
},
{
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
...@@ -2073,6 +2221,102 @@ ...@@ -2073,6 +2221,102 @@
} }
} }
}, },
"/players/wyscout/{wyId}/advancedpositions": {
"get": {
"description": "Returns the positions played by season for a player, plus an average distribution across the last 5 seasons.",
"tags": [
"Players"
],
"summary": "Get player advanced positions",
"parameters": [
{
"type": "integer",
"description": "Wyscout player wy_id",
"name": "wyId",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/players/wyscout/{wyId}/advancedstats": {
"get": {
"description": "Returns player advanced stats averages: anchor season average, last 2 years average, last 5 years average. If seasonId is omitted, the latest season found for the player is used as anchor.",
"tags": [
"Players"
],
"summary": "Get player advanced stats averages",
"parameters": [
{
"type": "integer",
"description": "Wyscout player wy_id",
"name": "wyId",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "Anchor season wy_id (optional; defaults to latest season available)",
"name": "seasonId",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/players/{id}": { "/players/{id}": {
"get": { "get": {
"description": "Returns a single player by its internal ID.", "description": "Returns a single player by its internal ID.",
...@@ -2639,6 +2883,42 @@ ...@@ -2639,6 +2883,42 @@
"description": "Number of items to skip before starting to collect the result set (default 0)", "description": "Number of items to skip before starting to collect the result set (default 0)",
"name": "offset", "name": "offset",
"in": "query" "in": "query"
},
{
"type": "string",
"description": "Filter teams by name",
"name": "name",
"in": "query"
},
{
"type": "string",
"description": "Filter teams by gender (male/female)",
"name": "gender",
"in": "query"
},
{
"type": "string",
"description": "Filter teams by type (club/national)",
"name": "teamType",
"in": "query"
},
{
"type": "string",
"description": "Alias for teamType (club/national)",
"name": "type",
"in": "query"
},
{
"type": "string",
"description": "If truthy, filter teams by type=club",
"name": "club",
"in": "query"
},
{
"type": "string",
"description": "If truthy, filter teams by type=national",
"name": "national",
"in": "query"
} }
], ],
"responses": { "responses": {
......
...@@ -1162,6 +1162,60 @@ paths: ...@@ -1162,6 +1162,60 @@ paths:
summary: Import players from TheSports summary: Import players from TheSports
tags: tags:
- Import - Import
/import/players/advancedstats:
post:
description: 'Single: provide playerWyId+competitionId+seasonId. Batch: omit
playerWyId (imports all players for that competition+season). Auto: omit all
IDs (imports distinct player_wy_id+competition_id+season_id combos from player_careers;
resumable).'
parameters:
- description: Wyscout player ID (optional; omit for batch/auto modes)
in: query
name: playerWyId
type: integer
- description: Wyscout competition ID (required for single/batch; omit for auto
mode)
in: query
name: competitionId
type: integer
- description: Wyscout season ID (required for single/batch; omit for auto mode)
in: query
name: seasonId
type: integer
- description: 'Optional limit on number of requests (batch: players; auto:
combos)'
in: query
name: limit
type: integer
- description: 'Auto mode only: concurrent workers (default 4)'
in: query
name: workers
type: integer
- description: 'Auto mode only: reset checkpoint and restart from beginning'
in: query
name: reset
type: boolean
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Import player advanced stats from Wyscout
tags:
- Import
/import/players/career: /import/players/career:
post: post:
description: Fetches /v3/players/{playerWyId}/career for players already present description: Fetches /v3/players/{playerWyId}/career for players already present
...@@ -1411,6 +1465,53 @@ paths: ...@@ -1411,6 +1465,53 @@ paths:
summary: Import teams from TheSports summary: Import teams from TheSports
tags: tags:
- Import - Import
/import/teams/career:
post:
description: Fetches /v3/teams/{teamWyId}/career?details=competition,season
for teams in the DB (teams.wy_id not null) plus teams referenced in team_children.
Upserts records into team_careers.
parameters:
- description: Process only one Wyscout team wy_id
in: query
name: teamWyId
type: integer
- description: Limit number of teams processed when teamWyId is omitted
in: query
name: limit
type: integer
- description: Concurrent workers (default 8)
in: query
name: workers
type: integer
- description: DB batch size for reading team IDs (default 2000)
in: query
name: batchSize
type: integer
- description: Global request rate limit (default 12)
in: query
name: rps
type: integer
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Import team career stats from Wyscout
tags:
- Import
/import/teams/images: /import/teams/images:
post: post:
description: Fetches /v3/teams/{wyId} for a specific team (or for teams in DB description: Fetches /v3/teams/{wyId} for a specific team (or for teams in DB
...@@ -1707,6 +1808,10 @@ paths: ...@@ -1707,6 +1808,10 @@ paths:
in: query in: query
name: country name: country
type: string type: string
- description: Filter players by gender (male/female)
in: query
name: gender
type: string
- collectionFormat: csv - collectionFormat: csv
description: 'Filter players by role name (supports multiple: role=Defender&role=Midfielder description: 'Filter players by role name (supports multiple: role=Defender&role=Midfielder
or role=Defender,Midfielder)' or role=Defender,Midfielder)'
...@@ -1872,6 +1977,73 @@ paths: ...@@ -1872,6 +1977,73 @@ paths:
summary: Get player by provider ID summary: Get player by provider ID
tags: tags:
- Players - Players
/players/wyscout/{wyId}/advancedpositions:
get:
description: Returns the positions played by season for a player, plus an average
distribution across the last 5 seasons.
parameters:
- description: Wyscout player wy_id
in: path
name: wyId
required: true
type: integer
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get player advanced positions
tags:
- Players
/players/wyscout/{wyId}/advancedstats:
get:
description: 'Returns player advanced stats averages: anchor season average,
last 2 years average, last 5 years average. If seasonId is omitted, the latest
season found for the player is used as anchor.'
parameters:
- description: Wyscout player wy_id
in: path
name: wyId
required: true
type: integer
- description: Anchor season wy_id (optional; defaults to latest season available)
in: query
name: seasonId
type: integer
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Get player advanced stats averages
tags:
- Players
/referees: /referees:
get: get:
description: Returns a paginated list of referees, optionally filtered by name, description: Returns a paginated list of referees, optionally filtered by name,
...@@ -2225,6 +2397,30 @@ paths: ...@@ -2225,6 +2397,30 @@ paths:
in: query in: query
name: offset name: offset
type: integer type: integer
- description: Filter teams by name
in: query
name: name
type: string
- description: Filter teams by gender (male/female)
in: query
name: gender
type: string
- description: Filter teams by type (club/national)
in: query
name: teamType
type: string
- description: Alias for teamType (club/national)
in: query
name: type
type: string
- description: If truthy, filter teams by type=club
in: query
name: club
type: string
- description: If truthy, filter teams by type=national
in: query
name: national
type: string
responses: responses:
"200": "200":
description: OK description: OK
......
...@@ -32,17 +32,23 @@ func Connect(cfg config.Config) (*gorm.DB, error) { ...@@ -32,17 +32,23 @@ func Connect(cfg config.Config) (*gorm.DB, error) {
&models.Round{}, &models.Round{},
&models.Team{}, &models.Team{},
&models.TeamChild{}, &models.TeamChild{},
&models.TeamCareer{},
&models.TeamAdvancedStats{},
&models.Coach{}, &models.Coach{},
&models.Referee{}, &models.Referee{},
&models.Player{}, &models.Player{},
&models.Match{}, &models.Match{},
&models.MatchAdvancedStats{},
&models.MatchTeam{}, &models.MatchTeam{},
&models.MatchLineupPlayer{}, &models.MatchLineupPlayer{},
&models.MatchFormation{}, &models.MatchFormation{},
&models.PlayerTransfer{}, &models.PlayerTransfer{},
&models.PlayerCareer{}, &models.PlayerCareer{},
&models.PlayerAdvancedStats{},
&models.PlayerAdvancedPosition{},
&models.TeamSquad{}, &models.TeamSquad{},
&models.Standing{}, &models.Standing{},
&models.ImportCheckpoint{},
&models.SampleRecord{}, &models.SampleRecord{},
); err != nil { ); err != nil {
return nil, err return nil, err
......
package handlers package handlers
import ( import (
"log"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
...@@ -17,5 +18,6 @@ func respondError(c *gin.Context, err error) { ...@@ -17,5 +18,6 @@ func respondError(c *gin.Context, err error) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
log.Printf("internal error: %T: %v", err, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
} }
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -3,6 +3,7 @@ package handlers ...@@ -3,6 +3,7 @@ package handlers
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"reflect"
"strconv" "strconv"
"strings" "strings"
"time" "time"
...@@ -30,12 +31,399 @@ func RegisterPlayerRoutes(rg *gin.RouterGroup, db *gorm.DB) { ...@@ -30,12 +31,399 @@ func RegisterPlayerRoutes(rg *gin.RouterGroup, db *gorm.DB) {
players := rg.Group("/players") players := rg.Group("/players")
players.GET("", h.List) players.GET("", h.List)
players.GET("/wyscout/:wyId", h.GetByProviderID) players.GET("/wyscout/:wyId", h.GetByProviderID)
players.GET("/wyscout/:wyId/advancedstats", h.GetAdvancedStatsAverages)
players.GET("/wyscout/:wyId/advancedpositions", h.GetAdvancedPositions)
players.GET("/provider/:providerId", h.GetByAnyProviderID) players.GET("/provider/:providerId", h.GetByAnyProviderID)
players.GET("/hudl/:hudlId/transfers", h.ListTransfersByHudlID) players.GET("/hudl/:hudlId/transfers", h.ListTransfersByHudlID)
players.GET("/hudl/:hudlId/career", h.ListCareerByHudlID) players.GET("/hudl/:hudlId/career", h.ListCareerByHudlID)
players.GET("/:id", h.GetByID) players.GET("/:id", h.GetByID)
} }
type numericAgg struct {
sum float64
count int64
}
func aggregateNumericPointers(rows []models.PlayerAdvancedStats) map[string]float64 {
out := map[string]float64{}
acc := map[string]*numericAgg{}
for _, row := range rows {
rv := reflect.ValueOf(row)
rt := rv.Type()
for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i)
jsonKey := strings.TrimSpace(strings.Split(f.Tag.Get("json"), ",")[0])
if jsonKey == "" || jsonKey == "-" {
continue
}
if jsonKey == "id" || jsonKey == "playerId" || jsonKey == "competitionId" || jsonKey == "seasonId" || jsonKey == "roundId" || jsonKey == "apiLastSyncedAt" || jsonKey == "createdAt" || jsonKey == "updatedAt" {
continue
}
fv := rv.Field(i)
if fv.Kind() != reflect.Pointer || fv.IsNil() {
continue
}
el := fv.Elem()
var val float64
switch el.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
val = float64(el.Int())
case reflect.Float32, reflect.Float64:
val = el.Float()
default:
continue
}
a := acc[jsonKey]
if a == nil {
a = &numericAgg{}
acc[jsonKey] = a
}
a.sum += val
a.count++
}
}
for k, a := range acc {
if a.count > 0 {
out[k] = a.sum / float64(a.count)
}
}
return out
}
func parseWyIDParam(c *gin.Context) (int, bool) {
wyIDStr := strings.TrimSpace(c.Param("wyId"))
wyID, err := strconv.Atoi(wyIDStr)
if err != nil || wyID <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid wyId"})
return 0, false
}
return wyID, true
}
func parseOptionalSeasonID(c *gin.Context) (*int, bool) {
seasonStr := strings.TrimSpace(c.Query("seasonId"))
if seasonStr == "" {
return nil, true
}
seasonID, err := strconv.Atoi(seasonStr)
if err != nil || seasonID <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid seasonId"})
return nil, false
}
return &seasonID, true
}
func (h *PlayerHandler) resolveAnchorSeason(ctx *gin.Context, playerWyID int, seasonID *int) (int, *models.Season, error) {
if seasonID != nil {
var s models.Season
if err := h.DB.WithContext(ctx.Request.Context()).First(&s, "wy_id = ?", *seasonID).Error; err == nil {
return *seasonID, &s, nil
}
return *seasonID, nil, nil
}
type row struct {
SeasonID int `gorm:"column:season_id"`
}
var r row
err := h.DB.WithContext(ctx.Request.Context()).
Model(&models.PlayerAdvancedStats{}).
Select("player_advanced_stats.season_id").
Joins("JOIN seasons ON seasons.wy_id = player_advanced_stats.season_id").
Where("player_advanced_stats.player_wy_id = ?", playerWyID).
Order("seasons.end_date DESC NULLS LAST").
Order("seasons.start_date DESC NULLS LAST").
Order("player_advanced_stats.season_id DESC").
Limit(1).
Scan(&r).Error
if err != nil {
return 0, nil, err
}
if r.SeasonID <= 0 {
return 0, nil, nil
}
var s models.Season
if err := h.DB.WithContext(ctx.Request.Context()).First(&s, "wy_id = ?", r.SeasonID).Error; err != nil {
return r.SeasonID, nil, nil
}
return r.SeasonID, &s, nil
}
func (h *PlayerHandler) seasonIDsSince(ctx *gin.Context, playerWyID int, cutoff time.Time) ([]int, error) {
type row struct {
SeasonID int `gorm:"column:season_id"`
SortDate *time.Time `gorm:"column:sort_date"`
}
var rows []row
if err := h.DB.WithContext(ctx.Request.Context()).
Model(&models.PlayerAdvancedStats{}).
Select("player_advanced_stats.season_id, MAX(COALESCE(seasons.end_date, seasons.start_date)) AS sort_date").
Joins("JOIN seasons ON seasons.wy_id = player_advanced_stats.season_id").
Where("player_advanced_stats.player_wy_id = ?", playerWyID).
Where("COALESCE(seasons.end_date, seasons.start_date) >= ?", cutoff).
Group("player_advanced_stats.season_id").
Order("sort_date DESC NULLS LAST").
Scan(&rows).Error; err != nil {
return nil, err
}
out := make([]int, 0, len(rows))
for _, r := range rows {
if r.SeasonID > 0 {
out = append(out, r.SeasonID)
}
}
return out, nil
}
// GetAdvancedStatsAverages returns aggregated advanced stats for a player.
// @Summary Get player advanced stats averages
// @Description Returns player advanced stats averages: anchor season average, last 2 years average, last 5 years average. If seasonId is omitted, the latest season found for the player is used as anchor.
// @Tags Players
// @Param wyId path int true "Wyscout player wy_id"
// @Param seasonId query int false "Anchor season wy_id (optional; defaults to latest season available)"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /players/wyscout/{wyId}/advancedstats [get]
func (h *PlayerHandler) GetAdvancedStatsAverages(c *gin.Context) {
wyID, ok := parseWyIDParam(c)
if !ok {
return
}
seasonID, ok := parseOptionalSeasonID(c)
if !ok {
return
}
anchorSeasonID, anchorSeason, err := h.resolveAnchorSeason(c, wyID, seasonID)
if err != nil {
respondError(c, err)
return
}
if anchorSeasonID <= 0 {
c.JSON(http.StatusOK, gin.H{"data": gin.H{"playerWyId": wyID, "seasonId": seasonID, "seasonAvg": map[string]float64{}, "last2YearsAvg": map[string]float64{}, "last5YearsAvg": map[string]float64{}}, "meta": gin.H{"timestamp": time.Now().UTC().Format(time.RFC3339), "endpoint": fmt.Sprintf("/players/wyscout/%d/advancedstats", wyID), "method": "GET"}})
return
}
anchorDate := time.Now().UTC()
var anchorSeasonName *string
if anchorSeason != nil {
anchorSeasonName = &anchorSeason.Name
if anchorSeason.EndDate != nil {
anchorDate = anchorSeason.EndDate.UTC()
} else if anchorSeason.StartDate != nil {
anchorDate = anchorSeason.StartDate.UTC()
}
}
var seasonRows []models.PlayerAdvancedStats
if err := h.DB.WithContext(c.Request.Context()).
Where("player_wy_id = ? AND season_id = ?", wyID, anchorSeasonID).
Find(&seasonRows).Error; err != nil {
respondError(c, err)
return
}
last2Cutoff := anchorDate.AddDate(-2, 0, 0)
last5Cutoff := anchorDate.AddDate(-5, 0, 0)
last2SeasonIDs, err := h.seasonIDsSince(c, wyID, last2Cutoff)
if err != nil {
respondError(c, err)
return
}
last5SeasonIDs, err := h.seasonIDsSince(c, wyID, last5Cutoff)
if err != nil {
respondError(c, err)
return
}
var last2Rows []models.PlayerAdvancedStats
if len(last2SeasonIDs) > 0 {
if err := h.DB.WithContext(c.Request.Context()).
Where("player_wy_id = ? AND season_id IN ?", wyID, last2SeasonIDs).
Find(&last2Rows).Error; err != nil {
respondError(c, err)
return
}
}
var last5Rows []models.PlayerAdvancedStats
if len(last5SeasonIDs) > 0 {
if err := h.DB.WithContext(c.Request.Context()).
Where("player_wy_id = ? AND season_id IN ?", wyID, last5SeasonIDs).
Find(&last5Rows).Error; err != nil {
respondError(c, err)
return
}
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/players/wyscout/%d/advancedstats", wyID)
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"playerWyId": wyID,
"anchorSeasonId": anchorSeasonID,
"anchorSeason": gin.H{
"wyId": anchorSeasonID,
"name": anchorSeasonName,
},
"seasonAvg": aggregateNumericPointers(seasonRows),
"last2YearsAvg": aggregateNumericPointers(last2Rows),
"last5YearsAvg": aggregateNumericPointers(last5Rows),
},
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetAdvancedPositions returns positions played by season and last 5 seasons averages.
// @Summary Get player advanced positions
// @Description Returns the positions played by season for a player, plus an average distribution across the last 5 seasons.
// @Tags Players
// @Param wyId path int true "Wyscout player wy_id"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /players/wyscout/{wyId}/advancedpositions [get]
func (h *PlayerHandler) GetAdvancedPositions(c *gin.Context) {
wyID, ok := parseWyIDParam(c)
if !ok {
return
}
type posRow struct {
SeasonID int `json:"seasonId" gorm:"column:season_id"`
SeasonName *string `json:"seasonName,omitempty" gorm:"column:season_name"`
PositionCode string `json:"positionCode" gorm:"column:position_code"`
PositionName *string `json:"positionName,omitempty" gorm:"column:position_name"`
Percent float64 `json:"percent" gorm:"column:percent"`
SortDate *time.Time `json:"-" gorm:"column:sort_date"`
}
var rows []posRow
if err := h.DB.WithContext(c.Request.Context()).
Model(&models.PlayerAdvancedPosition{}).
Select("player_advanced_positions.season_id, seasons.name AS season_name, player_advanced_positions.position_code, player_advanced_positions.position_name, player_advanced_positions.percent, COALESCE(seasons.end_date, seasons.start_date) AS sort_date").
Joins("JOIN seasons ON seasons.wy_id = player_advanced_positions.season_id").
Where("player_advanced_positions.player_wy_id = ?", wyID).
Order("sort_date DESC NULLS LAST").
Order("player_advanced_positions.season_id DESC").
Order("player_advanced_positions.percent DESC").
Scan(&rows).Error; err != nil {
respondError(c, err)
return
}
type pos struct {
PositionCode string `json:"positionCode"`
PositionName *string `json:"positionName,omitempty"`
Percent float64 `json:"percent"`
}
seasonOrder := make([]int, 0)
seasonSeen := map[int]struct{}{}
bySeason := map[int]struct {
SeasonID int `json:"seasonId"`
SeasonName *string `json:"seasonName,omitempty"`
Positions []pos `json:"positions"`
}{}
for _, r := range rows {
if _, ok := seasonSeen[r.SeasonID]; !ok {
seasonSeen[r.SeasonID] = struct{}{}
seasonOrder = append(seasonOrder, r.SeasonID)
bySeason[r.SeasonID] = struct {
SeasonID int `json:"seasonId"`
SeasonName *string `json:"seasonName,omitempty"`
Positions []pos `json:"positions"`
}{SeasonID: r.SeasonID, SeasonName: r.SeasonName, Positions: []pos{}}
}
tmp := bySeason[r.SeasonID]
tmp.Positions = append(tmp.Positions, pos{PositionCode: r.PositionCode, PositionName: r.PositionName, Percent: r.Percent})
bySeason[r.SeasonID] = tmp
}
seasonsOut := make([]any, 0, len(seasonOrder))
for _, sid := range seasonOrder {
seasonsOut = append(seasonsOut, bySeason[sid])
}
// last 5 seasons average by position
seasonIDsLast5 := make([]int, 0, 5)
for _, sid := range seasonOrder {
seasonIDsLast5 = append(seasonIDsLast5, sid)
if len(seasonIDsLast5) >= 5 {
break
}
}
avgByCode := map[string]*numericAgg{}
nameByCode := map[string]*string{}
if len(seasonIDsLast5) > 0 {
var lastRows []models.PlayerAdvancedPosition
if err := h.DB.WithContext(c.Request.Context()).
Where("player_wy_id = ? AND season_id IN ?", wyID, seasonIDsLast5).
Find(&lastRows).Error; err != nil {
respondError(c, err)
return
}
for _, pr := range lastRows {
code := strings.TrimSpace(pr.PositionCode)
if code == "" {
continue
}
a := avgByCode[code]
if a == nil {
a = &numericAgg{}
avgByCode[code] = a
}
a.sum += pr.Percent
a.count++
if pr.PositionName != nil && strings.TrimSpace(*pr.PositionName) != "" {
nameByCode[code] = pr.PositionName
}
}
}
last5Avg := make([]any, 0, len(avgByCode))
for code, a := range avgByCode {
if a.count <= 0 {
continue
}
last5Avg = append(last5Avg, gin.H{
"positionCode": code,
"positionName": nameByCode[code],
"percent": a.sum / float64(a.count),
})
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/players/wyscout/%d/advancedpositions", wyID)
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"playerWyId": wyID,
"seasons": seasonsOut,
"last5SeasonsAvg": last5Avg,
"last5SeasonsSeasonIds": seasonIDsLast5,
},
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
type PlayerRole struct { type PlayerRole struct {
Name string `json:"name"` Name string `json:"name"`
Code2 string `json:"code2"` Code2 string `json:"code2"`
...@@ -360,6 +748,7 @@ func valueOrDefault(s *string, def string) string { ...@@ -360,6 +748,7 @@ func valueOrDefault(s *string, def string) string {
// @Param name query string false "Filter players by name (short, first, middle, or last)" // @Param name query string false "Filter players by name (short, first, middle, or last)"
// @Param teamId query string false "Filter players by current team ID" // @Param teamId query string false "Filter players by current team ID"
// @Param country query string false "Filter players by birth country name" // @Param country query string false "Filter players by birth country name"
// @Param gender query string false "Filter players by gender (male/female)"
// @Param role query []string false "Filter players by role name (supports multiple: role=Defender&role=Midfielder or role=Defender,Midfielder)" // @Param role query []string false "Filter players by role name (supports multiple: role=Defender&role=Midfielder or role=Defender,Midfielder)"
// @Success 200 {object} map[string]interface{} // @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
...@@ -380,6 +769,7 @@ func (h *PlayerHandler) List(c *gin.Context) { ...@@ -380,6 +769,7 @@ func (h *PlayerHandler) List(c *gin.Context) {
name := c.Query("name") name := c.Query("name")
teamID := c.Query("teamId") teamID := c.Query("teamId")
country := c.Query("country") country := c.Query("country")
gender := c.Query("gender")
rolesQuery := c.QueryArray("role") rolesQuery := c.QueryArray("role")
if len(rolesQuery) == 0 { if len(rolesQuery) == 0 {
rolesQuery = c.QueryArray("role[]") rolesQuery = c.QueryArray("role[]")
...@@ -405,6 +795,8 @@ func (h *PlayerHandler) List(c *gin.Context) { ...@@ -405,6 +795,8 @@ func (h *PlayerHandler) List(c *gin.Context) {
endpoint = fmt.Sprintf("/players?teamId=%s", teamID) endpoint = fmt.Sprintf("/players?teamId=%s", teamID)
} else if country != "" { } else if country != "" {
endpoint = fmt.Sprintf("/players?country=%s", country) endpoint = fmt.Sprintf("/players?country=%s", country)
} else if gender != "" {
endpoint = fmt.Sprintf("/players?gender=%s", gender)
} }
players, total, err := h.Service.ListPlayers(c.Request.Context(), services.ListPlayersOptions{ players, total, err := h.Service.ListPlayers(c.Request.Context(), services.ListPlayersOptions{
...@@ -413,6 +805,7 @@ func (h *PlayerHandler) List(c *gin.Context) { ...@@ -413,6 +805,7 @@ func (h *PlayerHandler) List(c *gin.Context) {
Name: name, Name: name,
TeamID: teamID, TeamID: teamID,
Country: country, Country: country,
Gender: gender,
Roles: roles, Roles: roles,
}) })
if err != nil { if err != nil {
......
...@@ -576,6 +576,12 @@ func (h *TeamHandler) GetImagesByID(c *gin.Context) { ...@@ -576,6 +576,12 @@ func (h *TeamHandler) GetImagesByID(c *gin.Context) {
// @Tags Teams // @Tags Teams
// @Param limit query int false "Maximum number of items to return (default 100)" // @Param limit query int false "Maximum number of items to return (default 100)"
// @Param offset query int false "Number of items to skip before starting to collect the result set (default 0)" // @Param offset query int false "Number of items to skip before starting to collect the result set (default 0)"
// @Param name query string false "Filter teams by name"
// @Param gender query string false "Filter teams by gender (male/female)"
// @Param teamType query string false "Filter teams by type (club/national)"
// @Param type query string false "Alias for teamType (club/national)"
// @Param club query string false "If truthy, filter teams by type=club"
// @Param national query string false "If truthy, filter teams by type=national"
// @Success 200 {object} handlers.TeamListResponse // @Success 200 {object} handlers.TeamListResponse
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Router /teams [get] // @Router /teams [get]
...@@ -593,13 +599,62 @@ func (h *TeamHandler) List(c *gin.Context) { ...@@ -593,13 +599,62 @@ func (h *TeamHandler) List(c *gin.Context) {
} }
name := c.Query("name") name := c.Query("name")
gender := c.Query("gender")
teamType := strings.TrimSpace(c.Query("teamType"))
if teamType == "" {
teamType = strings.TrimSpace(c.Query("type"))
}
if teamType == "" {
q := c.Request.URL.Query()
clubV, clubPresent := q["club"]
nationalV, nationalPresent := q["national"]
isTruthy := func(present bool, vals []string) bool {
if !present {
return false
}
if len(vals) == 0 {
return true
}
v := strings.ToLower(strings.TrimSpace(vals[0]))
if v == "" {
return true
}
switch v {
case "1", "true", "t", "yes", "y", "on":
return true
default:
return false
}
}
if isTruthy(clubPresent, clubV) {
teamType = "club"
} else if isTruthy(nationalPresent, nationalV) {
teamType = "national"
}
}
if teamType != "" {
switch strings.ToLower(strings.TrimSpace(teamType)) {
case "club", "clubs":
teamType = "club"
case "national", "nation", "nt":
teamType = "national"
default:
teamType = strings.ToLower(strings.TrimSpace(teamType))
}
}
endpoint := "/teams" endpoint := "/teams"
if name != "" { if name != "" {
endpoint = fmt.Sprintf("/teams?name=%s", name) endpoint = fmt.Sprintf("/teams?name=%s", name)
} else if gender != "" {
endpoint = fmt.Sprintf("/teams?gender=%s", gender)
} else if teamType != "" {
endpoint = fmt.Sprintf("/teams?teamType=%s", teamType)
} }
teams, total, err := h.Service.ListTeams(c.Request.Context(), limit, offset, name) teams, total, err := h.Service.ListTeams(c.Request.Context(), limit, offset, name, gender, teamType)
if err != nil { if err != nil {
respondError(c, err) respondError(c, err)
return return
......
...@@ -513,6 +513,169 @@ type PlayerTransfer struct { ...@@ -513,6 +513,169 @@ type PlayerTransfer struct {
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"` UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
} }
type MatchAdvancedStats struct {
ID string `gorm:"primaryKey;size:16" json:"id"`
MatchWyID int `gorm:"column:match_wy_id;uniqueIndex:uidx_match_advanced_stats;index" json:"matchWyId"`
TeamWyID int `gorm:"column:team_wy_id;uniqueIndex:uidx_match_advanced_stats;index" json:"teamWyId"`
CompetitionID *int `gorm:"column:competition_id;index" json:"competitionId"`
SeasonID *int `gorm:"column:season_id;index" json:"seasonId"`
GeneralShots *int `gorm:"column:general_shots" json:"generalShots"`
GeneralFouls *int `gorm:"column:general_fouls" json:"generalFouls"`
GeneralCorners *int `gorm:"column:general_corners" json:"generalCorners"`
GeneralRedCards *int `gorm:"column:general_red_cards" json:"generalRedCards"`
GeneralYellowCards *int `gorm:"column:general_yellow_cards" json:"generalYellowCards"`
GeneralOffsides *int `gorm:"column:general_offsides" json:"generalOffsides"`
GeneralDribbles *int `gorm:"column:general_dribbles" json:"generalDribbles"`
GeneralGoals *int `gorm:"column:general_goals" json:"generalGoals"`
GeneralXGPerShot *float64 `gorm:"column:general_xg_per_shot" json:"generalXgPerShot"`
GeneralAvgDistance *float64 `gorm:"column:general_avg_distance" json:"generalAvgDistance"`
GeneralXG *float64 `gorm:"column:general_xg" json:"generalXg"`
GeneralProgressiveRuns *int `gorm:"column:general_progressive_runs" json:"generalProgressiveRuns"`
GeneralTouchesInBox *int `gorm:"column:general_touches_in_box" json:"generalTouchesInBox"`
GeneralFoulsSuffered *int `gorm:"column:general_fouls_suffered" json:"generalFoulsSuffered"`
GeneralShotsOnTarget *int `gorm:"column:general_shots_on_target" json:"generalShotsOnTarget"`
GeneralShotsBlocked *int `gorm:"column:general_shots_blocked" json:"generalShotsBlocked"`
GeneralShotsOutsideBox *int `gorm:"column:general_shots_outside_box" json:"generalShotsOutsideBox"`
GeneralShotsOutsideBoxOnTarget *int `gorm:"column:general_shots_outside_box_on_target" json:"generalShotsOutsideBoxOnTarget"`
GeneralShotsOnPost *int `gorm:"column:general_shots_on_post" json:"generalShotsOnPost"`
GeneralShotsWide *int `gorm:"column:general_shots_wide" json:"generalShotsWide"`
GeneralShotsFromBox *int `gorm:"column:general_shots_from_box" json:"generalShotsFromBox"`
GeneralShotsFromBoxOnTarget *int `gorm:"column:general_shots_from_box_on_target" json:"generalShotsFromBoxOnTarget"`
GeneralFreeKicks *int `gorm:"column:general_free_kicks" json:"generalFreeKicks"`
GeneralShotsFromDangerZone *int `gorm:"column:general_shots_from_danger_zone" json:"generalShotsFromDangerZone"`
GeneralTotalThrowIns *int `gorm:"column:general_total_throw_ins" json:"generalTotalThrowIns"`
GeneralLeftThrowIns *int `gorm:"column:general_left_throw_ins" json:"generalLeftThrowIns"`
GeneralRightThrowIns *int `gorm:"column:general_right_throw_ins" json:"generalRightThrowIns"`
PossessionPercent *int `gorm:"column:possession_percent" json:"possessionPercent"`
PossessionPurePossessionTime *string `gorm:"column:possession_pure_possession_time" json:"possessionPurePossessionTime"`
PossessionNumber *int `gorm:"column:possession_number" json:"possessionNumber"`
PossessionAvgPossessionDuration *string `gorm:"column:possession_avg_possession_duration" json:"possessionAvgPossessionDuration"`
PossessionReachingOpponentHalf *int `gorm:"column:possession_reaching_opponent_half" json:"possessionReachingOpponentHalf"`
PossessionReachingOpponentBox *int `gorm:"column:possession_reaching_opponent_box" json:"possessionReachingOpponentBox"`
Possession1_15 *int `gorm:"column:possession_1_15" json:"possession1_15"`
Possession16_30 *int `gorm:"column:possession_16_30" json:"possession16_30"`
Possession31_45 *int `gorm:"column:possession_31_45" json:"possession31_45"`
Possession46_60 *int `gorm:"column:possession_46_60" json:"possession46_60"`
Possession61_75 *int `gorm:"column:possession_61_75" json:"possession61_75"`
Possession76_90 *int `gorm:"column:possession_76_90" json:"possession76_90"`
Possession91_105 *int `gorm:"column:possession_91_105" json:"possession91_105"`
Possession106_120 *int `gorm:"column:possession_106_120" json:"possession106_120"`
PossessionMinutes1_15 *string `gorm:"column:possession_minutes_1_15" json:"possessionMinutes1_15"`
PossessionMinutes16_30 *string `gorm:"column:possession_minutes_16_30" json:"possessionMinutes16_30"`
PossessionMinutes31_45 *string `gorm:"column:possession_minutes_31_45" json:"possessionMinutes31_45"`
PossessionMinutes46_60 *string `gorm:"column:possession_minutes_46_60" json:"possessionMinutes46_60"`
PossessionMinutes61_75 *string `gorm:"column:possession_minutes_61_75" json:"possessionMinutes61_75"`
PossessionMinutes76_90 *string `gorm:"column:possession_minutes_76_90" json:"possessionMinutes76_90"`
PossessionMinutes91_105 *string `gorm:"column:possession_minutes_91_105" json:"possessionMinutes91_105"`
PossessionMinutes106_120 *string `gorm:"column:possession_minutes_106_120" json:"possessionMinutes106_120"`
PossessionTotalTime *string `gorm:"column:possession_total_time" json:"possessionTotalTime"`
PossessionDeadTime *string `gorm:"column:possession_dead_time" json:"possessionDeadTime"`
OpenPlayTotal *int `gorm:"column:open_play_total" json:"openPlayTotal"`
OpenPlayShort *int `gorm:"column:open_play_short" json:"openPlayShort"`
OpenPlayMedium *int `gorm:"column:open_play_medium" json:"openPlayMedium"`
OpenPlayLong *int `gorm:"column:open_play_long" json:"openPlayLong"`
OpenPlayVeryLong *int `gorm:"column:open_play_very_long" json:"openPlayVeryLong"`
AttacksTotal *int `gorm:"column:attacks_total" json:"attacksTotal"`
AttacksWithShots *int `gorm:"column:attacks_with_shots" json:"attacksWithShots"`
AttacksPositionalAttack *int `gorm:"column:attacks_positional_attack" json:"attacksPositionalAttack"`
AttacksPositionalWithShots *int `gorm:"column:attacks_positional_with_shots" json:"attacksPositionalWithShots"`
AttacksCounterAttacks *int `gorm:"column:attacks_counter_attacks" json:"attacksCounterAttacks"`
AttacksFreeKicks *int `gorm:"column:attacks_free_kicks" json:"attacksFreeKicks"`
AttacksFreeKicksWithShot *int `gorm:"column:attacks_free_kicks_with_shot" json:"attacksFreeKicksWithShot"`
AttacksCorners *int `gorm:"column:attacks_corners" json:"attacksCorners"`
AttacksCornersWithShot *int `gorm:"column:attacks_corners_with_shot" json:"attacksCornersWithShot"`
TransitionsRecoveriesHigh *int `gorm:"column:transitions_recoveries_high" json:"transitionsRecoveriesHigh"`
TransitionsRecoveriesMedium *int `gorm:"column:transitions_recoveries_medium" json:"transitionsRecoveriesMedium"`
TransitionsRecoveriesLow *int `gorm:"column:transitions_recoveries_low" json:"transitionsRecoveriesLow"`
TransitionsRecoveriesTotal *int `gorm:"column:transitions_recoveries_total" json:"transitionsRecoveriesTotal"`
TransitionsOpponentHalfRecoveries *int `gorm:"column:transitions_opponent_half_recoveries" json:"transitionsOpponentHalfRecoveries"`
TransitionsLossesHigh *int `gorm:"column:transitions_losses_high" json:"transitionsLossesHigh"`
TransitionsLossesMedium *int `gorm:"column:transitions_losses_medium" json:"transitionsLossesMedium"`
TransitionsLossesLow *int `gorm:"column:transitions_losses_low" json:"transitionsLossesLow"`
TransitionsLossesTotal *int `gorm:"column:transitions_losses_total" json:"transitionsLossesTotal"`
TransitionsOwnHalfLosses *int `gorm:"column:transitions_own_half_losses" json:"transitionsOwnHalfLosses"`
PassesCrossesTotal *int `gorm:"column:passes_crosses_total" json:"passesCrossesTotal"`
PassesCrossesSuccessful *int `gorm:"column:passes_crosses_successful" json:"passesCrossesSuccessful"`
PassesCrossesBlocked *int `gorm:"column:passes_crosses_blocked" json:"passesCrossesBlocked"`
PassesCrossesLow *int `gorm:"column:passes_crosses_low" json:"passesCrossesLow"`
PassesCrossesHigh *int `gorm:"column:passes_crosses_high" json:"passesCrossesHigh"`
PassesCrossesFromLeftFlank *int `gorm:"column:passes_crosses_from_left_flank" json:"passesCrossesFromLeftFlank"`
PassesCrossesFromRightFlank *int `gorm:"column:passes_crosses_from_right_flank" json:"passesCrossesFromRightFlank"`
PassesCrossesFromLeftFlankSuccessful *int `gorm:"column:passes_crosses_from_left_flank_successful" json:"passesCrossesFromLeftFlankSuccessful"`
PassesCrossesFromRightFlankSuccessful *int `gorm:"column:passes_crosses_from_right_flank_successful" json:"passesCrossesFromRightFlankSuccessful"`
PassesPasses *int `gorm:"column:passes_passes" json:"passesPasses"`
PassesPassesSuccessful *int `gorm:"column:passes_passes_successful" json:"passesPassesSuccessful"`
PassesForwardPasses *int `gorm:"column:passes_forward_passes" json:"passesForwardPasses"`
PassesForwardPassesSuccessful *int `gorm:"column:passes_forward_passes_successful" json:"passesForwardPassesSuccessful"`
PassesBackPasses *int `gorm:"column:passes_back_passes" json:"passesBackPasses"`
PassesBackPassesSuccessful *int `gorm:"column:passes_back_passes_successful" json:"passesBackPassesSuccessful"`
PassesProgressivePasses *int `gorm:"column:passes_progressive_passes" json:"passesProgressivePasses"`
PassesProgressivePassesSuccessful *int `gorm:"column:passes_progressive_passes_successful" json:"passesProgressivePassesSuccessful"`
PassesLongPasses *int `gorm:"column:passes_long_passes" json:"passesLongPasses"`
PassesLongPassesSuccessful *int `gorm:"column:passes_long_passes_successful" json:"passesLongPassesSuccessful"`
PassesThroughPasses *int `gorm:"column:passes_through_passes" json:"passesThroughPasses"`
PassesThroughPassesSuccessful *int `gorm:"column:passes_through_passes_successful" json:"passesThroughPassesSuccessful"`
PassesPassToFinalThirds *int `gorm:"column:passes_pass_to_final_thirds" json:"passesPassToFinalThirds"`
PassesPassToFinalThirdsSuccessful *int `gorm:"column:passes_pass_to_final_thirds_successful" json:"passesPassToFinalThirdsSuccessful"`
PassesPassToPenaltyAreas *int `gorm:"column:passes_pass_to_penalty_areas" json:"passesPassToPenaltyAreas"`
PassesPassToPenaltyAreasSuccessful *int `gorm:"column:passes_pass_to_penalty_areas_successful" json:"passesPassToPenaltyAreasSuccessful"`
PassesVerticalPasses *int `gorm:"column:passes_vertical_passes" json:"passesVerticalPasses"`
PassesVerticalPassesSuccessful *int `gorm:"column:passes_vertical_passes_successful" json:"passesVerticalPassesSuccessful"`
PassesDeepCompletedPasses *int `gorm:"column:passes_deep_completed_passes" json:"passesDeepCompletedPasses"`
PassesDeepCompletedPassesSuccessful *int `gorm:"column:passes_deep_completed_passes_successful" json:"passesDeepCompletedPassesSuccessful"`
PassesKeyPasses *int `gorm:"column:passes_key_passes" json:"passesKeyPasses"`
PassesKeyPassesSuccessful *int `gorm:"column:passes_key_passes_successful" json:"passesKeyPassesSuccessful"`
PassesAssists *int `gorm:"column:passes_assists" json:"passesAssists"`
PassesSmartPasses *int `gorm:"column:passes_smart_passes" json:"passesSmartPasses"`
PassesSmartPassesSuccessful *int `gorm:"column:passes_smart_passes_successful" json:"passesSmartPassesSuccessful"`
PassesShotAssists *int `gorm:"column:passes_shot_assists" json:"passesShotAssists"`
PassesShortMediumPasses *int `gorm:"column:passes_short_medium_passes" json:"passesShortMediumPasses"`
PassesShortMediumPassesSuccessful *int `gorm:"column:passes_short_medium_passes_successful" json:"passesShortMediumPassesSuccessful"`
PassesAvgPassLength *float64 `gorm:"column:passes_avg_pass_length" json:"passesAvgPassLength"`
PassesAvgPassToFinalThirdLength *float64 `gorm:"column:passes_avg_pass_to_final_third_length" json:"passesAvgPassToFinalThirdLength"`
PassesMatchTempo *float64 `gorm:"column:passes_match_tempo" json:"passesMatchTempo"`
PassesLateralPasses *int `gorm:"column:passes_lateral_passes" json:"passesLateralPasses"`
PassesLateralPassesSuccessful *int `gorm:"column:passes_lateral_passes_successful" json:"passesLateralPassesSuccessful"`
DefenceTackles *int `gorm:"column:defence_tackles" json:"defenceTackles"`
DefenceInterceptions *int `gorm:"column:defence_interceptions" json:"defenceInterceptions"`
DefenceClearances *int `gorm:"column:defence_clearances" json:"defenceClearances"`
DefencePPDA *float64 `gorm:"column:defence_ppda" json:"defencePpda"`
DuelsDuels *int `gorm:"column:duels_duels" json:"duelsDuels"`
DuelsDuelsSuccessful *int `gorm:"column:duels_duels_successful" json:"duelsDuelsSuccessful"`
DuelsDefensiveDuels *int `gorm:"column:duels_defensive_duels" json:"duelsDefensiveDuels"`
DuelsDefensiveDuelsSuccessful *int `gorm:"column:duels_defensive_duels_successful" json:"duelsDefensiveDuelsSuccessful"`
DuelsOffensiveDuels *int `gorm:"column:duels_offensive_duels" json:"duelsOffensiveDuels"`
DuelsOffensiveDuelsSuccessful *int `gorm:"column:duels_offensive_duels_successful" json:"duelsOffensiveDuelsSuccessful"`
DuelsLooseBallDuels *int `gorm:"column:duels_loose_ball_duels" json:"duelsLooseBallDuels"`
DuelsLooseBallDuelsSuccessful *int `gorm:"column:duels_loose_ball_duels_successful" json:"duelsLooseBallDuelsSuccessful"`
DuelsAerialDuels *int `gorm:"column:duels_aerial_duels" json:"duelsAerialDuels"`
DuelsAerialDuelsSuccessful *int `gorm:"column:duels_aerial_duels_successful" json:"duelsAerialDuelsSuccessful"`
DuelsGroundDuels *int `gorm:"column:duels_ground_duels" json:"duelsGroundDuels"`
DuelsGroundDuelsSuccessful *int `gorm:"column:duels_ground_duels_successful" json:"duelsGroundDuelsSuccessful"`
DuelsDribbles *int `gorm:"column:duels_dribbles" json:"duelsDribbles"`
DuelsDribblesSuccessful *int `gorm:"column:duels_dribbles_successful" json:"duelsDribblesSuccessful"`
DuelsChallengeIntensity *float64 `gorm:"column:duels_challenge_intensity" json:"duelsChallengeIntensity"`
FlanksLeftFlankAttacks *int `gorm:"column:flanks_left_flank_attacks" json:"flanksLeftFlankAttacks"`
FlanksLeftFlankXG *float64 `gorm:"column:flanks_left_flank_xg" json:"flanksLeftFlankXg"`
FlanksRightFlankAttacks *int `gorm:"column:flanks_right_flank_attacks" json:"flanksRightFlankAttacks"`
FlanksRightFlankXG *float64 `gorm:"column:flanks_right_flank_xg" json:"flanksRightFlankXg"`
FlanksCenterAttacks *int `gorm:"column:flanks_center_attacks" json:"flanksCenterAttacks"`
FlanksCenterXG *float64 `gorm:"column:flanks_center_xg" json:"flanksCenterXg"`
APILastSyncedAt *time.Time `gorm:"column:api_last_synced_at" json:"apiLastSyncedAt"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
}
func (pt *PlayerTransfer) BeforeCreate(tx *gorm.DB) (err error) { func (pt *PlayerTransfer) BeforeCreate(tx *gorm.DB) (err error) {
if pt.ID != "" { if pt.ID != "" {
return nil return nil
...@@ -548,6 +711,595 @@ type PlayerCareer struct { ...@@ -548,6 +711,595 @@ type PlayerCareer struct {
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"` UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
} }
type PlayerAdvancedStats struct {
ID string `gorm:"primaryKey;size:16" json:"id"`
PlayerWyID int `gorm:"column:player_wy_id;uniqueIndex:uidx_player_advanced_stats;index" json:"playerId"`
CompetitionID int `gorm:"column:competition_id;uniqueIndex:uidx_player_advanced_stats;index" json:"competitionId"`
SeasonID int `gorm:"column:season_id;uniqueIndex:uidx_player_advanced_stats;index" json:"seasonId"`
RoundID *int `gorm:"column:round_id" json:"roundId"`
TotalMatches *int `gorm:"column:total_matches" json:"totalMatches"`
TotalMatchesInStart *int `gorm:"column:total_matches_in_start" json:"totalMatchesInStart"`
TotalMatchesSubstituted *int `gorm:"column:total_matches_substituted" json:"totalMatchesSubstituted"`
TotalMatchesComingOff *int `gorm:"column:total_matches_coming_off" json:"totalMatchesComingOff"`
TotalMinutesOnField *int `gorm:"column:total_minutes_on_field" json:"totalMinutesOnField"`
TotalMinutesTagged *int `gorm:"column:total_minutes_tagged" json:"totalMinutesTagged"`
TotalGoals *int `gorm:"column:total_goals" json:"totalGoals"`
TotalAssists *int `gorm:"column:total_assists" json:"totalAssists"`
TotalShots *int `gorm:"column:total_shots" json:"totalShots"`
TotalHeadShots *int `gorm:"column:total_head_shots" json:"totalHeadShots"`
TotalYellowCards *int `gorm:"column:total_yellow_cards" json:"totalYellowCards"`
TotalRedCards *int `gorm:"column:total_red_cards" json:"totalRedCards"`
TotalDirectRedCards *int `gorm:"column:total_direct_red_cards" json:"totalDirectRedCards"`
TotalPenalties *int `gorm:"column:total_penalties" json:"totalPenalties"`
TotalLinkupPlays *int `gorm:"column:total_linkup_plays" json:"totalLinkupPlays"`
TotalDuels *int `gorm:"column:total_duels" json:"totalDuels"`
TotalDuelsWon *int `gorm:"column:total_duels_won" json:"totalDuelsWon"`
TotalDefensiveDuels *int `gorm:"column:total_defensive_duels" json:"totalDefensiveDuels"`
TotalDefensiveDuelsWon *int `gorm:"column:total_defensive_duels_won" json:"totalDefensiveDuelsWon"`
TotalOffensiveDuels *int `gorm:"column:total_offensive_duels" json:"totalOffensiveDuels"`
TotalOffensiveDuelsWon *int `gorm:"column:total_offensive_duels_won" json:"totalOffensiveDuelsWon"`
TotalAerialDuels *int `gorm:"column:total_aerial_duels" json:"totalAerialDuels"`
TotalAerialDuelsWon *int `gorm:"column:total_aerial_duels_won" json:"totalAerialDuelsWon"`
TotalFouls *int `gorm:"column:total_fouls" json:"totalFouls"`
TotalPasses *int `gorm:"column:total_passes" json:"totalPasses"`
TotalSuccessfulPasses *int `gorm:"column:total_successful_passes" json:"totalSuccessfulPasses"`
TotalSmartPasses *int `gorm:"column:total_smart_passes" json:"totalSmartPasses"`
TotalSuccessfulSmartPasses *int `gorm:"column:total_successful_smart_passes" json:"totalSuccessfulSmartPasses"`
TotalPassesToFinalThird *int `gorm:"column:total_passes_to_final_third" json:"totalPassesToFinalThird"`
TotalSuccessfulPassesToFinalThird *int `gorm:"column:total_successful_passes_to_final_third" json:"totalSuccessfulPassesToFinalThird"`
TotalCrosses *int `gorm:"column:total_crosses" json:"totalCrosses"`
TotalSuccessfulCrosses *int `gorm:"column:total_successful_crosses" json:"totalSuccessfulCrosses"`
TotalForwardPasses *int `gorm:"column:total_forward_passes" json:"totalForwardPasses"`
TotalSuccessfulForwardPasses *int `gorm:"column:total_successful_forward_passes" json:"totalSuccessfulForwardPasses"`
TotalBackPasses *int `gorm:"column:total_back_passes" json:"totalBackPasses"`
TotalSuccessfulBackPasses *int `gorm:"column:total_successful_back_passes" json:"totalSuccessfulBackPasses"`
TotalThroughPasses *int `gorm:"column:total_through_passes" json:"totalThroughPasses"`
TotalSuccessfulThroughPasses *int `gorm:"column:total_successful_through_passes" json:"totalSuccessfulThroughPasses"`
TotalKeyPasses *int `gorm:"column:total_key_passes" json:"totalKeyPasses"`
TotalSuccessfulKeyPasses *int `gorm:"column:total_successful_key_passes" json:"totalSuccessfulKeyPasses"`
TotalVerticalPasses *int `gorm:"column:total_vertical_passes" json:"totalVerticalPasses"`
TotalSuccessfulVerticalPasses *int `gorm:"column:total_successful_vertical_passes" json:"totalSuccessfulVerticalPasses"`
TotalLongPasses *int `gorm:"column:total_long_passes" json:"totalLongPasses"`
TotalSuccessfulLongPasses *int `gorm:"column:total_successful_long_passes" json:"totalSuccessfulLongPasses"`
TotalDribbles *int `gorm:"column:total_dribbles" json:"totalDribbles"`
TotalSuccessfulDribbles *int `gorm:"column:total_successful_dribbles" json:"totalSuccessfulDribbles"`
TotalInterceptions *int `gorm:"column:total_interceptions" json:"totalInterceptions"`
TotalDefensiveActions *int `gorm:"column:total_defensive_actions" json:"totalDefensiveActions"`
TotalSuccessfulDefensiveAction *int `gorm:"column:total_successful_defensive_action" json:"totalSuccessfulDefensiveAction"`
TotalAttackingActions *int `gorm:"column:total_attacking_actions" json:"totalAttackingActions"`
TotalSuccessfulAttackingActions *int `gorm:"column:total_successful_attacking_actions" json:"totalSuccessfulAttackingActions"`
TotalFreeKicks *int `gorm:"column:total_free_kicks" json:"totalFreeKicks"`
TotalFreeKicksOnTarget *int `gorm:"column:total_free_kicks_on_target" json:"totalFreeKicksOnTarget"`
TotalDirectFreeKicks *int `gorm:"column:total_direct_free_kicks" json:"totalDirectFreeKicks"`
TotalDirectFreeKicksOnTarget *int `gorm:"column:total_direct_free_kicks_on_target" json:"totalDirectFreeKicksOnTarget"`
TotalCorners *int `gorm:"column:total_corners" json:"totalCorners"`
TotalSuccessfulPenalties *int `gorm:"column:total_successful_penalties" json:"totalSuccessfulPenalties"`
TotalSuccessfulLinkupPlays *int `gorm:"column:total_successful_linkup_plays" json:"totalSuccessfulLinkupPlays"`
TotalAccelerations *int `gorm:"column:total_accelerations" json:"totalAccelerations"`
TotalPressingDuels *int `gorm:"column:total_pressing_duels" json:"totalPressingDuels"`
TotalPressingDuelsWon *int `gorm:"column:total_pressing_duels_won" json:"totalPressingDuelsWon"`
TotalLooseBallDuels *int `gorm:"column:total_loose_ball_duels" json:"totalLooseBallDuels"`
TotalLooseBallDuelsWon *int `gorm:"column:total_loose_ball_duels_won" json:"totalLooseBallDuelsWon"`
TotalMissedBalls *int `gorm:"column:total_missed_balls" json:"totalMissedBalls"`
TotalShotAssists *int `gorm:"column:total_shot_assists" json:"totalShotAssists"`
TotalShotOnTargetAssists *int `gorm:"column:total_shot_on_target_assists" json:"totalShotOnTargetAssists"`
TotalRecoveries *int `gorm:"column:total_recoveries" json:"totalRecoveries"`
TotalOpponentHalfRecoveries *int `gorm:"column:total_opponent_half_recoveries" json:"totalOpponentHalfRecoveries"`
TotalDangerousOpponentHalfRecoveries *int `gorm:"column:total_dangerous_opponent_half_recoveries" json:"totalDangerousOpponentHalfRecoveries"`
TotalLosses *int `gorm:"column:total_losses" json:"totalLosses"`
TotalOwnHalfLosses *int `gorm:"column:total_own_half_losses" json:"totalOwnHalfLosses"`
TotalDangerousOwnHalfLosses *int `gorm:"column:total_dangerous_own_half_losses" json:"totalDangerousOwnHalfLosses"`
TotalXGShot *float64 `gorm:"column:total_xg_shot" json:"totalXgShot"`
TotalXGAssist *float64 `gorm:"column:total_xg_assist" json:"totalXgAssist"`
TotalXGSave *float64 `gorm:"column:total_xg_save" json:"totalXgSave"`
TotalReceivedPass *int `gorm:"column:total_received_pass" json:"totalReceivedPass"`
TotalTouchInBox *int `gorm:"column:total_touch_in_box" json:"totalTouchInBox"`
TotalProgressiveRun *int `gorm:"column:total_progressive_run" json:"totalProgressiveRun"`
TotalOffsides *int `gorm:"column:total_offsides" json:"totalOffsides"`
TotalClearances *int `gorm:"column:total_clearances" json:"totalClearances"`
TotalSecondAssists *int `gorm:"column:total_second_assists" json:"totalSecondAssists"`
TotalThirdAssists *int `gorm:"column:total_third_assists" json:"totalThirdAssists"`
TotalShotsBlocked *int `gorm:"column:total_shots_blocked" json:"totalShotsBlocked"`
TotalFoulsSuffered *int `gorm:"column:total_fouls_suffered" json:"totalFoulsSuffered"`
TotalProgressivePasses *int `gorm:"column:total_progressive_passes" json:"totalProgressivePasses"`
TotalCounterpressingRecoveries *int `gorm:"column:total_counterpressing_recoveries" json:"totalCounterpressingRecoveries"`
TotalSlidingTackles *int `gorm:"column:total_sliding_tackles" json:"totalSlidingTackles"`
TotalGoalKicks *int `gorm:"column:total_goal_kicks" json:"totalGoalKicks"`
TotalDribblesAgainst *int `gorm:"column:total_dribbles_against" json:"totalDribblesAgainst"`
TotalDribblesAgainstWon *int `gorm:"column:total_dribbles_against_won" json:"totalDribblesAgainstWon"`
TotalGoalKicksShort *int `gorm:"column:total_goal_kicks_short" json:"totalGoalKicksShort"`
TotalGoalKicksLong *int `gorm:"column:total_goal_kicks_long" json:"totalGoalKicksLong"`
TotalShotsOnTarget *int `gorm:"column:total_shots_on_target" json:"totalShotsOnTarget"`
TotalSuccessfulProgressivePasses *int `gorm:"column:total_successful_progressive_passes" json:"totalSuccessfulProgressivePasses"`
TotalSuccessfulSlidingTackles *int `gorm:"column:total_successful_sliding_tackles" json:"totalSuccessfulSlidingTackles"`
TotalSuccessfulGoalKicks *int `gorm:"column:total_successful_goal_kicks" json:"totalSuccessfulGoalKicks"`
TotalFieldAerialDuels *int `gorm:"column:total_field_aerial_duels" json:"totalFieldAerialDuels"`
TotalFieldAerialDuelsWon *int `gorm:"column:total_field_aerial_duels_won" json:"totalFieldAerialDuelsWon"`
TotalGKCleanSheets *int `gorm:"column:total_gk_clean_sheets" json:"totalGkCleanSheets"`
TotalGKConcededGoals *int `gorm:"column:total_gk_conceded_goals" json:"totalGkConcededGoals"`
TotalGKShotsAgainst *int `gorm:"column:total_gk_shots_against" json:"totalGkShotsAgainst"`
TotalGKExits *int `gorm:"column:total_gk_exits" json:"totalGkExits"`
TotalGKSuccessfulExits *int `gorm:"column:total_gk_successful_exits" json:"totalGkSuccessfulExits"`
TotalGKAerialDuels *int `gorm:"column:total_gk_aerial_duels" json:"totalGkAerialDuels"`
TotalGKAerialDuelsWon *int `gorm:"column:total_gk_aerial_duels_won" json:"totalGkAerialDuelsWon"`
TotalGKSaves *int `gorm:"column:total_gk_saves" json:"totalGkSaves"`
TotalNewDuelsWon *int `gorm:"column:total_new_duels_won" json:"totalNewDuelsWon"`
TotalNewDefensiveDuelsWon *int `gorm:"column:total_new_defensive_duels_won" json:"totalNewDefensiveDuelsWon"`
TotalNewOffensiveDuelsWon *int `gorm:"column:total_new_offensive_duels_won" json:"totalNewOffensiveDuelsWon"`
TotalNewSuccessfulDribbles *int `gorm:"column:total_new_successful_dribbles" json:"totalNewSuccessfulDribbles"`
TotalLateralPasses *int `gorm:"column:total_lateral_passes" json:"totalLateralPasses"`
TotalSuccessfulLateralPasses *int `gorm:"column:total_successful_lateral_passes" json:"totalSuccessfulLateralPasses"`
AvgPassLength *float64 `gorm:"column:avg_pass_length" json:"avgPassLength"`
AvgLongPassLength *float64 `gorm:"column:avg_long_pass_length" json:"avgLongPassLength"`
AvgDribbleDistanceFromOpponentGoal *float64 `gorm:"column:avg_dribble_distance_from_opponent_goal" json:"avgDribbleDistanceFromOpponentGoal"`
AvgBallRecoveries *float64 `gorm:"column:avg_ball_recoveries" json:"avgBallRecoveries"`
AvgDuels *float64 `gorm:"column:avg_duels" json:"avgDuels"`
AvgDefensiveDuels *float64 `gorm:"column:avg_defensive_duels" json:"avgDefensiveDuels"`
AvgOffensiveDuels *float64 `gorm:"column:avg_offensive_duels" json:"avgOffensiveDuels"`
AvgAerialDuels *float64 `gorm:"column:avg_aerial_duels" json:"avgAerialDuels"`
AvgFouls *float64 `gorm:"column:avg_fouls" json:"avgFouls"`
AvgGoals *float64 `gorm:"column:avg_goals" json:"avgGoals"`
AvgAssists *float64 `gorm:"column:avg_assists" json:"avgAssists"`
AvgPasses *float64 `gorm:"column:avg_passes" json:"avgPasses"`
AvgSmartPasses *float64 `gorm:"column:avg_smart_passes" json:"avgSmartPasses"`
AvgPassesToFinalThird *float64 `gorm:"column:avg_passes_to_final_third" json:"avgPassesToFinalThird"`
AvgCrosses *float64 `gorm:"column:avg_crosses" json:"avgCrosses"`
AvgDribbles *float64 `gorm:"column:avg_dribbles" json:"avgDribbles"`
AvgShots *float64 `gorm:"column:avg_shots" json:"avgShots"`
AvgHeadShots *float64 `gorm:"column:avg_head_shots" json:"avgHeadShots"`
AvgInterceptions *float64 `gorm:"column:avg_interceptions" json:"avgInterceptions"`
AvgSuccessfulDefensiveAction *float64 `gorm:"column:avg_successful_defensive_action" json:"avgSuccessfulDefensiveAction"`
AvgYellowCards *float64 `gorm:"column:avg_yellow_cards" json:"avgYellowCards"`
AvgRedCards *float64 `gorm:"column:avg_red_cards" json:"avgRedCards"`
AvgDirectRedCards *float64 `gorm:"column:avg_direct_red_cards" json:"avgDirectRedCards"`
AvgSuccessfulAttackingActions *float64 `gorm:"column:avg_successful_attacking_actions" json:"avgSuccessfulAttackingActions"`
AvgFreeKicks *float64 `gorm:"column:avg_free_kicks" json:"avgFreeKicks"`
AvgDirectFreeKicks *float64 `gorm:"column:avg_direct_free_kicks" json:"avgDirectFreeKicks"`
AvgCorners *float64 `gorm:"column:avg_corners" json:"avgCorners"`
AvgPenalties *float64 `gorm:"column:avg_penalties" json:"avgPenalties"`
AvgAccelerations *float64 `gorm:"column:avg_accelerations" json:"avgAccelerations"`
AvgLooseBallDuels *float64 `gorm:"column:avg_loose_ball_duels" json:"avgLooseBallDuels"`
AvgMissedBalls *float64 `gorm:"column:avg_missed_balls" json:"avgMissedBalls"`
AvgForwardPasses *float64 `gorm:"column:avg_forward_passes" json:"avgForwardPasses"`
AvgBackPasses *float64 `gorm:"column:avg_back_passes" json:"avgBackPasses"`
AvgThroughPasses *float64 `gorm:"column:avg_through_passes" json:"avgThroughPasses"`
AvgKeyPasses *float64 `gorm:"column:avg_key_passes" json:"avgKeyPasses"`
AvgVerticalPasses *float64 `gorm:"column:avg_vertical_passes" json:"avgVerticalPasses"`
AvgLongPasses *float64 `gorm:"column:avg_long_passes" json:"avgLongPasses"`
AvgShotAssists *float64 `gorm:"column:avg_shot_assists" json:"avgShotAssists"`
AvgShotOnTargetAssists *float64 `gorm:"column:avg_shot_on_target_assists" json:"avgShotOnTargetAssists"`
AvgLinkupPlays *float64 `gorm:"column:avg_linkup_plays" json:"avgLinkupPlays"`
AvgOpponentHalfRecoveries *float64 `gorm:"column:avg_opponent_half_recoveries" json:"avgOpponentHalfRecoveries"`
AvgDangerousOpponentHalfRecoveries *float64 `gorm:"column:avg_dangerous_opponent_half_recoveries" json:"avgDangerousOpponentHalfRecoveries"`
AvgBallLosses *float64 `gorm:"column:avg_ball_losses" json:"avgBallLosses"`
AvgLosses *float64 `gorm:"column:avg_losses" json:"avgLosses"`
AvgOwnHalfLosses *float64 `gorm:"column:avg_own_half_losses" json:"avgOwnHalfLosses"`
AvgDangerousOwnHalfLosses *float64 `gorm:"column:avg_dangerous_own_half_losses" json:"avgDangerousOwnHalfLosses"`
AvgDuelsWon *float64 `gorm:"column:avg_duels_won" json:"avgDuelsWon"`
AvgDefensiveDuelsWon *float64 `gorm:"column:avg_defensive_duels_won" json:"avgDefensiveDuelsWon"`
AvgOffensiveDuelsWon *float64 `gorm:"column:avg_offensive_duels_won" json:"avgOffensiveDuelsWon"`
AvgSuccessfulPasses *float64 `gorm:"column:avg_successful_passes" json:"avgSuccessfulPasses"`
AvgSuccessfulSmartPasses *float64 `gorm:"column:avg_successful_smart_passes" json:"avgSuccessfulSmartPasses"`
AvgSuccessfulCrosses *float64 `gorm:"column:avg_successful_crosses" json:"avgSuccessfulCrosses"`
AvgSuccessfulForwardPasses *float64 `gorm:"column:avg_successful_forward_passes" json:"avgSuccessfulForwardPasses"`
AvgSuccessfulBackPasses *float64 `gorm:"column:avg_successful_back_passes" json:"avgSuccessfulBackPasses"`
AvgSuccessfulThroughPasses *float64 `gorm:"column:avg_successful_through_passes" json:"avgSuccessfulThroughPasses"`
AvgSuccessfulKeyPasses *float64 `gorm:"column:avg_successful_key_passes" json:"avgSuccessfulKeyPasses"`
AvgSuccessfulVerticalPasses *float64 `gorm:"column:avg_successful_vertical_passes" json:"avgSuccessfulVerticalPasses"`
AvgSuccessfulLongPasses *float64 `gorm:"column:avg_successful_long_passes" json:"avgSuccessfulLongPasses"`
AvgSuccessfulDribbles *float64 `gorm:"column:avg_successful_dribbles" json:"avgSuccessfulDribbles"`
AvgDefensiveActions *float64 `gorm:"column:avg_defensive_actions" json:"avgDefensiveActions"`
AvgAttackingActions *float64 `gorm:"column:avg_attacking_actions" json:"avgAttackingActions"`
AvgFreeKicksOnTarget *float64 `gorm:"column:avg_free_kicks_on_target" json:"avgFreeKicksOnTarget"`
AvgDirectFreeKicksOnTarget *float64 `gorm:"column:avg_direct_free_kicks_on_target" json:"avgDirectFreeKicksOnTarget"`
AvgSuccessfulPenalties *float64 `gorm:"column:avg_successful_penalties" json:"avgSuccessfulPenalties"`
AvgSuccessfulLinkupPlays *float64 `gorm:"column:avg_successful_linkup_plays" json:"avgSuccessfulLinkupPlays"`
AvgLooseBallDuelsWon *float64 `gorm:"column:avg_loose_ball_duels_won" json:"avgLooseBallDuelsWon"`
AvgSuccessfulPassesToFinalThird *float64 `gorm:"column:avg_successful_passes_to_final_third" json:"avgSuccessfulPassesToFinalThird"`
AvgXGShot *float64 `gorm:"column:avg_xg_shot" json:"avgXgShot"`
AvgXGAssist *float64 `gorm:"column:avg_xg_assist" json:"avgXgAssist"`
AvgXGSave *float64 `gorm:"column:avg_xg_save" json:"avgXgSave"`
AvgReceivedPass *float64 `gorm:"column:avg_received_pass" json:"avgReceivedPass"`
AvgTouchInBox *float64 `gorm:"column:avg_touch_in_box" json:"avgTouchInBox"`
AvgProgressiveRun *float64 `gorm:"column:avg_progressive_run" json:"avgProgressiveRun"`
AvgOffsides *float64 `gorm:"column:avg_offsides" json:"avgOffsides"`
AvgClearances *float64 `gorm:"column:avg_clearances" json:"avgClearances"`
AvgSecondAssists *float64 `gorm:"column:avg_second_assists" json:"avgSecondAssists"`
AvgThirdAssists *float64 `gorm:"column:avg_third_assists" json:"avgThirdAssists"`
AvgFoulsSuffered *float64 `gorm:"column:avg_fouls_suffered" json:"avgFoulsSuffered"`
AvgProgressivePasses *float64 `gorm:"column:avg_progressive_passes" json:"avgProgressivePasses"`
AvgCounterpressingRecoveries *float64 `gorm:"column:avg_counterpressing_recoveries" json:"avgCounterpressingRecoveries"`
AvgSlidingTackles *float64 `gorm:"column:avg_sliding_tackles" json:"avgSlidingTackles"`
AvgGoalKicks *float64 `gorm:"column:avg_goal_kicks" json:"avgGoalKicks"`
AvgShotsBlocked *float64 `gorm:"column:avg_shots_blocked" json:"avgShotsBlocked"`
AvgShotsOnTarget *float64 `gorm:"column:avg_shots_on_target" json:"avgShotsOnTarget"`
AvgSuccessfulProgressivePasses *float64 `gorm:"column:avg_successful_progressive_passes" json:"avgSuccessfulProgressivePasses"`
AvgSuccessfulSlidingTackles *float64 `gorm:"column:avg_successful_sliding_tackles" json:"avgSuccessfulSlidingTackles"`
AvgSuccessfulGoalKicks *float64 `gorm:"column:avg_successful_goal_kicks" json:"avgSuccessfulGoalKicks"`
AvgDribblesAgainst *float64 `gorm:"column:avg_dribbles_against" json:"avgDribblesAgainst"`
AvgDribblesAgainstWon *float64 `gorm:"column:avg_dribbles_against_won" json:"avgDribblesAgainstWon"`
AvgGoalKicksShort *float64 `gorm:"column:avg_goal_kicks_short" json:"avgGoalKicksShort"`
AvgGoalKicksLong *float64 `gorm:"column:avg_goal_kicks_long" json:"avgGoalKicksLong"`
AvgFieldAerialDuels *float64 `gorm:"column:avg_field_aerial_duels" json:"avgFieldAerialDuels"`
AvgFieldAerialDuelsWon *float64 `gorm:"column:avg_field_aerial_duels_won" json:"avgFieldAerialDuelsWon"`
AvgGKConcededGoals *float64 `gorm:"column:avg_gk_conceded_goals" json:"avgGkConcededGoals"`
AvgGKShotsAgainst *float64 `gorm:"column:avg_gk_shots_against" json:"avgGkShotsAgainst"`
AvgGKExits *float64 `gorm:"column:avg_gk_exits" json:"avgGkExits"`
AvgGKAerialDuels *float64 `gorm:"column:avg_gk_aerial_duels" json:"avgGkAerialDuels"`
AvgGKSaves *float64 `gorm:"column:avg_gk_saves" json:"avgGkSaves"`
AvgGKSuccessfulExits *float64 `gorm:"column:avg_gk_successful_exits" json:"avgGkSuccessfulExits"`
AvgGKAerialDuelsWon *float64 `gorm:"column:avg_gk_aerial_duels_won" json:"avgGkAerialDuelsWon"`
AvgNewDuelsWon *float64 `gorm:"column:avg_new_duels_won" json:"avgNewDuelsWon"`
AvgNewDefensiveDuelsWon *float64 `gorm:"column:avg_new_defensive_duels_won" json:"avgNewDefensiveDuelsWon"`
AvgNewOffensiveDuelsWon *float64 `gorm:"column:avg_new_offensive_duels_won" json:"avgNewOffensiveDuelsWon"`
AvgNewSuccessfulDribbles *float64 `gorm:"column:avg_new_successful_dribbles" json:"avgNewSuccessfulDribbles"`
AvgLateralPasses *float64 `gorm:"column:avg_lateral_passes" json:"avgLateralPasses"`
AvgSuccessfulLateralPasses *float64 `gorm:"column:avg_successful_lateral_passes" json:"avgSuccessfulLateralPasses"`
PctDuelsWon *float64 `gorm:"column:pct_duels_won" json:"pctDuelsWon"`
PctDefensiveDuelsWon *float64 `gorm:"column:pct_defensive_duels_won" json:"pctDefensiveDuelsWon"`
PctOffensiveDuelsWon *float64 `gorm:"column:pct_offensive_duels_won" json:"pctOffensiveDuelsWon"`
PctAerialDuelsWon *float64 `gorm:"column:pct_aerial_duels_won" json:"pctAerialDuelsWon"`
PctSuccessfulPasses *float64 `gorm:"column:pct_successful_passes" json:"pctSuccessfulPasses"`
PctSuccessfulSmartPasses *float64 `gorm:"column:pct_successful_smart_passes" json:"pctSuccessfulSmartPasses"`
PctSuccessfulPassesToFinalThird *float64 `gorm:"column:pct_successful_passes_to_final_third" json:"pctSuccessfulPassesToFinalThird"`
PctSuccessfulCrosses *float64 `gorm:"column:pct_successful_crosses" json:"pctSuccessfulCrosses"`
PctSuccessfulDribbles *float64 `gorm:"column:pct_successful_dribbles" json:"pctSuccessfulDribbles"`
PctShotsOnTarget *float64 `gorm:"column:pct_shots_on_target" json:"pctShotsOnTarget"`
PctHeadShotsOnTarget *float64 `gorm:"column:pct_head_shots_on_target" json:"pctHeadShotsOnTarget"`
PctGoalConversion *float64 `gorm:"column:pct_goal_conversion" json:"pctGoalConversion"`
PctDirectFreeKicksOnTarget *float64 `gorm:"column:pct_direct_free_kicks_on_target" json:"pctDirectFreeKicksOnTarget"`
PctPenaltiesConversion *float64 `gorm:"column:pct_penalties_conversion" json:"pctPenaltiesConversion"`
PctWin *float64 `gorm:"column:pct_win" json:"pctWin"`
PctSuccessfulForwardPasses *float64 `gorm:"column:pct_successful_forward_passes" json:"pctSuccessfulForwardPasses"`
PctSuccessfulBackPasses *float64 `gorm:"column:pct_successful_back_passes" json:"pctSuccessfulBackPasses"`
PctSuccessfulThroughPasses *float64 `gorm:"column:pct_successful_through_passes" json:"pctSuccessfulThroughPasses"`
PctSuccessfulKeyPasses *float64 `gorm:"column:pct_successful_key_passes" json:"pctSuccessfulKeyPasses"`
PctSuccessfulVerticalPasses *float64 `gorm:"column:pct_successful_vertical_passes" json:"pctSuccessfulVerticalPasses"`
PctSuccessfulLongPasses *float64 `gorm:"column:pct_successful_long_passes" json:"pctSuccessfulLongPasses"`
PctSuccessfulShotAssists *float64 `gorm:"column:pct_successful_shot_assists" json:"pctSuccessfulShotAssists"`
PctSuccessfulLinkupPlays *float64 `gorm:"column:pct_successful_linkup_plays" json:"pctSuccessfulLinkupPlays"`
PctYellowCardsPerFoul *float64 `gorm:"column:pct_yellow_cards_per_foul" json:"pctYellowCardsPerFoul"`
PctSuccessfulProgressivePasses *float64 `gorm:"column:pct_successful_progressive_passes" json:"pctSuccessfulProgressivePasses"`
PctSuccessfulSlidingTackles *float64 `gorm:"column:pct_successful_sliding_tackles" json:"pctSuccessfulSlidingTackles"`
PctSuccessfulGoalKicks *float64 `gorm:"column:pct_successful_goal_kicks" json:"pctSuccessfulGoalKicks"`
PctDribblesAgainstWon *float64 `gorm:"column:pct_dribbles_against_won" json:"pctDribblesAgainstWon"`
PctFieldAerialDuelsWon *float64 `gorm:"column:pct_field_aerial_duels_won" json:"pctFieldAerialDuelsWon"`
PctGKSaves *float64 `gorm:"column:pct_gk_saves" json:"pctGkSaves"`
PctGKSuccessfulExits *float64 `gorm:"column:pct_gk_successful_exits" json:"pctGkSuccessfulExits"`
PctGKAerialDuelsWon *float64 `gorm:"column:pct_gk_aerial_duels_won" json:"pctGkAerialDuelsWon"`
PctNewDuelsWon *float64 `gorm:"column:pct_new_duels_won" json:"pctNewDuelsWon"`
PctNewDefensiveDuelsWon *float64 `gorm:"column:pct_new_defensive_duels_won" json:"pctNewDefensiveDuelsWon"`
PctNewOffensiveDuelsWon *float64 `gorm:"column:pct_new_offensive_duels_won" json:"pctNewOffensiveDuelsWon"`
PctNewSuccessfulDribbles *float64 `gorm:"column:pct_new_successful_dribbles" json:"pctNewSuccessfulDribbles"`
PctSuccessfulLateralPasses *float64 `gorm:"column:pct_successful_lateral_passes" json:"pctSuccessfulLateralPasses"`
APILastSyncedAt *time.Time `gorm:"column:api_last_synced_at" json:"apiLastSyncedAt"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
}
type PlayerAdvancedPosition struct {
ID string `gorm:"primaryKey;size:16" json:"id"`
PlayerWyID int `gorm:"column:player_wy_id;uniqueIndex:uidx_player_advanced_positions;index" json:"playerId"`
CompetitionID int `gorm:"column:competition_id;uniqueIndex:uidx_player_advanced_positions;index" json:"competitionId"`
SeasonID int `gorm:"column:season_id;uniqueIndex:uidx_player_advanced_positions;index" json:"seasonId"`
PositionName *string `gorm:"column:position_name" json:"positionName"`
PositionCode string `gorm:"column:position_code;uniqueIndex:uidx_player_advanced_positions" json:"positionCode"`
Percent float64 `gorm:"column:percent" json:"percent"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
}
type TeamAdvancedStats struct {
ID string `gorm:"primaryKey;size:16" json:"id"`
TeamWyID int `gorm:"column:team_wy_id;uniqueIndex:uidx_team_advanced_stats;index" json:"teamId"`
CompetitionID int `gorm:"column:competition_id;uniqueIndex:uidx_team_advanced_stats;index" json:"competitionId"`
SeasonID int `gorm:"column:season_id;uniqueIndex:uidx_team_advanced_stats;index" json:"seasonId"`
RoundID *int `gorm:"column:round_id" json:"roundId"`
TotalMatches *int `gorm:"column:total_matches" json:"totalMatches"`
TotalGoals *int `gorm:"column:total_goals" json:"totalGoals"`
TotalAssists *int `gorm:"column:total_assists" json:"totalAssists"`
TotalShots *int `gorm:"column:total_shots" json:"totalShots"`
TotalHeadShots *int `gorm:"column:total_head_shots" json:"totalHeadShots"`
TotalYellowCards *int `gorm:"column:total_yellow_cards" json:"totalYellowCards"`
TotalRedCards *int `gorm:"column:total_red_cards" json:"totalRedCards"`
TotalDirectRedCards *int `gorm:"column:total_direct_red_cards" json:"totalDirectRedCards"`
TotalPenalties *int `gorm:"column:total_penalties" json:"totalPenalties"`
TotalLinkupPlays *int `gorm:"column:total_linkup_plays" json:"totalLinkupPlays"`
TotalCleanSheets *int `gorm:"column:total_clean_sheets" json:"totalCleanSheets"`
TotalDuels *int `gorm:"column:total_duels" json:"totalDuels"`
TotalDuelsWon *int `gorm:"column:total_duels_won" json:"totalDuelsWon"`
TotalDefensiveDuels *int `gorm:"column:total_defensive_duels" json:"totalDefensiveDuels"`
TotalDefensiveDuelsWon *int `gorm:"column:total_defensive_duels_won" json:"totalDefensiveDuelsWon"`
TotalOffensiveDuels *int `gorm:"column:total_offensive_duels" json:"totalOffensiveDuels"`
TotalOffensiveDuelsWon *int `gorm:"column:total_offensive_duels_won" json:"totalOffensiveDuelsWon"`
TotalAerialDuels *int `gorm:"column:total_aerial_duels" json:"totalAerialDuels"`
TotalAerialDuelsWon *int `gorm:"column:total_aerial_duels_won" json:"totalAerialDuelsWon"`
TotalFouls *int `gorm:"column:total_fouls" json:"totalFouls"`
TotalOffsides *int `gorm:"column:total_offsides" json:"totalOffsides"`
TotalPasses *int `gorm:"column:total_passes" json:"totalPasses"`
TotalSuccessfulPasses *int `gorm:"column:total_successful_passes" json:"totalSuccessfulPasses"`
TotalSmartPasses *int `gorm:"column:total_smart_passes" json:"totalSmartPasses"`
TotalSuccessfulSmartPasses *int `gorm:"column:total_successful_smart_passes" json:"totalSuccessfulSmartPasses"`
TotalPassesToFinalThird *int `gorm:"column:total_passes_to_final_third" json:"totalPassesToFinalThird"`
TotalSuccessfulPassesToFinalThird *int `gorm:"column:total_successful_passes_to_final_third" json:"totalSuccessfulPassesToFinalThird"`
TotalCrosses *int `gorm:"column:total_crosses" json:"totalCrosses"`
TotalSuccessfulCrosses *int `gorm:"column:total_successful_crosses" json:"totalSuccessfulCrosses"`
TotalForwardPasses *int `gorm:"column:total_forward_passes" json:"totalForwardPasses"`
TotalSuccessfulForwardPasses *int `gorm:"column:total_successful_forward_passes" json:"totalSuccessfulForwardPasses"`
TotalBackPasses *int `gorm:"column:total_back_passes" json:"totalBackPasses"`
TotalSuccessfulBackPasses *int `gorm:"column:total_successful_back_passes" json:"totalSuccessfulBackPasses"`
TotalThroughPasses *int `gorm:"column:total_through_passes" json:"totalThroughPasses"`
TotalSuccessfulThroughPasses *int `gorm:"column:total_successful_through_passes" json:"totalSuccessfulThroughPasses"`
TotalKeyPasses *int `gorm:"column:total_key_passes" json:"totalKeyPasses"`
TotalSuccessfulKeyPasses *int `gorm:"column:total_successful_key_passes" json:"totalSuccessfulKeyPasses"`
TotalVerticalPasses *int `gorm:"column:total_vertical_passes" json:"totalVerticalPasses"`
TotalSuccessfulVerticalPasses *int `gorm:"column:total_successful_vertical_passes" json:"totalSuccessfulVerticalPasses"`
TotalLongPasses *int `gorm:"column:total_long_passes" json:"totalLongPasses"`
TotalSuccessfulLongPasses *int `gorm:"column:total_successful_long_passes" json:"totalSuccessfulLongPasses"`
TotalDribbles *int `gorm:"column:total_dribbles" json:"totalDribbles"`
TotalSuccessfulDribbles *int `gorm:"column:total_successful_dribbles" json:"totalSuccessfulDribbles"`
TotalInterceptions *int `gorm:"column:total_interceptions" json:"totalInterceptions"`
TotalDefensiveActions *int `gorm:"column:total_defensive_actions" json:"totalDefensiveActions"`
TotalSuccessfulDefensiveActions *int `gorm:"column:total_successful_defensive_actions" json:"totalSuccessfulDefensiveActions"`
TotalAttackingActions *int `gorm:"column:total_attacking_actions" json:"totalAttackingActions"`
TotalSuccessfulAttackingActions *int `gorm:"column:total_successful_attacking_actions" json:"totalSuccessfulAttackingActions"`
TotalFreeKicks *int `gorm:"column:total_free_kicks" json:"totalFreeKicks"`
TotalFreeKicksOnTarget *int `gorm:"column:total_free_kicks_on_target" json:"totalFreeKicksOnTarget"`
TotalDirectFreeKicks *int `gorm:"column:total_direct_free_kicks" json:"totalDirectFreeKicks"`
TotalDirectFreeKicksOnTarget *int `gorm:"column:total_direct_free_kicks_on_target" json:"totalDirectFreeKicksOnTarget"`
TotalCorners *int `gorm:"column:total_corners" json:"totalCorners"`
TotalSuccessfulPenalties *int `gorm:"column:total_successful_penalties" json:"totalSuccessfulPenalties"`
TotalSuccessfulLinkupPlays *int `gorm:"column:total_successful_linkup_plays" json:"totalSuccessfulLinkupPlays"`
TotalAccelerations *int `gorm:"column:total_accelerations" json:"totalAccelerations"`
TotalPressingDuels *int `gorm:"column:total_pressing_duels" json:"totalPressingDuels"`
TotalPressingDuelsWon *int `gorm:"column:total_pressing_duels_won" json:"totalPressingDuelsWon"`
TotalLooseBallDuels *int `gorm:"column:total_loose_ball_duels" json:"totalLooseBallDuels"`
TotalLooseBallDuelsWon *int `gorm:"column:total_loose_ball_duels_won" json:"totalLooseBallDuelsWon"`
TotalMissedBalls *int `gorm:"column:total_missed_balls" json:"totalMissedBalls"`
TotalShotAssists *int `gorm:"column:total_shot_assists" json:"totalShotAssists"`
TotalShotOnTargetAssists *int `gorm:"column:total_shot_on_target_assists" json:"totalShotOnTargetAssists"`
TotalRecoveries *int `gorm:"column:total_recoveries" json:"totalRecoveries"`
TotalOpponentHalfRecoveries *int `gorm:"column:total_opponent_half_recoveries" json:"totalOpponentHalfRecoveries"`
TotalDangerousOpponentHalfRecoveries *int `gorm:"column:total_dangerous_opponent_half_recoveries" json:"totalDangerousOpponentHalfRecoveries"`
TotalLosses *int `gorm:"column:total_losses" json:"totalLosses"`
TotalOwnHalfLosses *int `gorm:"column:total_own_half_losses" json:"totalOwnHalfLosses"`
TotalDangerousOwnHalfLosses *int `gorm:"column:total_dangerous_own_half_losses" json:"totalDangerousOwnHalfLosses"`
TotalFieldAerialDuels *int `gorm:"column:total_field_aerial_duels" json:"totalFieldAerialDuels"`
TotalFieldAerialDuelsWon *int `gorm:"column:total_field_aerial_duels_won" json:"totalFieldAerialDuelsWon"`
TotalGKExits *int `gorm:"column:total_gk_exits" json:"totalGkExits"`
TotalGKSuccessfulExits *int `gorm:"column:total_gk_successful_exits" json:"totalGkSuccessfulExits"`
TotalGKAerialDuels *int `gorm:"column:total_gk_aerial_duels" json:"totalGkAerialDuels"`
TotalGKAerialDuelsWon *int `gorm:"column:total_gk_aerial_duels_won" json:"totalGkAerialDuelsWon"`
TotalGKSaves *int `gorm:"column:total_gk_saves" json:"totalGkSaves"`
TotalXGShot *float64 `gorm:"column:total_xg_shot" json:"totalXgShot"`
TotalXGShotAgainst *float64 `gorm:"column:total_xg_shot_against" json:"totalXgShotAgainst"`
TotalPPDA *float64 `gorm:"column:total_ppda" json:"totalPpda"`
TotalReceivedPass *int `gorm:"column:total_received_pass" json:"totalReceivedPass"`
TotalTouchInBox *int `gorm:"column:total_touch_in_box" json:"totalTouchInBox"`
TotalProgressiveRun *int `gorm:"column:total_progressive_run" json:"totalProgressiveRun"`
TotalConcededGoals *int `gorm:"column:total_conceded_goals" json:"totalConcededGoals"`
TotalOpponentOffsides *int `gorm:"column:total_opponent_offsides" json:"totalOpponentOffsides"`
TotalShotsAgainst *int `gorm:"column:total_shots_against" json:"totalShotsAgainst"`
TotalGKGoalKicks *int `gorm:"column:total_gk_goal_kicks" json:"totalGkGoalKicks"`
TotalGKGoalKicksSuccess *int `gorm:"column:total_gk_goal_kicks_success" json:"totalGkGoalKicksSuccess"`
TotalShortGoalKicks *int `gorm:"column:total_short_goal_kicks" json:"totalShortGoalKicks"`
TotalLongGoalKicks *int `gorm:"column:total_long_goal_kicks" json:"totalLongGoalKicks"`
TotalMatchesTagged *int `gorm:"column:total_matches_tagged" json:"totalMatchesTagged"`
TotalNewDuelsWon *int `gorm:"column:total_new_duels_won" json:"totalNewDuelsWon"`
TotalNewDefensiveDuelsWon *int `gorm:"column:total_new_defensive_duels_won" json:"totalNewDefensiveDuelsWon"`
TotalNewOffensiveDuelsWon *int `gorm:"column:total_new_offensive_duels_won" json:"totalNewOffensiveDuelsWon"`
TotalNewSuccessfulDribbles *int `gorm:"column:total_new_successful_dribbles" json:"totalNewSuccessfulDribbles"`
TotalLateralPasses *int `gorm:"column:total_lateral_passes" json:"totalLateralPasses"`
TotalSuccessfulLateralPasses *int `gorm:"column:total_successful_lateral_passes" json:"totalSuccessfulLateralPasses"`
AvgPossessionPercent *float64 `gorm:"column:avg_possession_percent" json:"avgPossessionPercent"`
AvgDuels *float64 `gorm:"column:avg_duels" json:"avgDuels"`
AvgDefensiveDuels *float64 `gorm:"column:avg_defensive_duels" json:"avgDefensiveDuels"`
AvgOffensiveDuels *float64 `gorm:"column:avg_offensive_duels" json:"avgOffensiveDuels"`
AvgAerialDuels *float64 `gorm:"column:avg_aerial_duels" json:"avgAerialDuels"`
AvgFouls *float64 `gorm:"column:avg_fouls" json:"avgFouls"`
AvgGoals *float64 `gorm:"column:avg_goals" json:"avgGoals"`
AvgAssists *float64 `gorm:"column:avg_assists" json:"avgAssists"`
AvgPasses *float64 `gorm:"column:avg_passes" json:"avgPasses"`
AvgSmartPasses *float64 `gorm:"column:avg_smart_passes" json:"avgSmartPasses"`
AvgPassesToFinalThird *float64 `gorm:"column:avg_passes_to_final_third" json:"avgPassesToFinalThird"`
AvgCrosses *float64 `gorm:"column:avg_crosses" json:"avgCrosses"`
AvgDribbles *float64 `gorm:"column:avg_dribbles" json:"avgDribbles"`
AvgShots *float64 `gorm:"column:avg_shots" json:"avgShots"`
AvgHeadShots *float64 `gorm:"column:avg_head_shots" json:"avgHeadShots"`
AvgInterceptions *float64 `gorm:"column:avg_interceptions" json:"avgInterceptions"`
AvgSuccessfulDefensiveAction *float64 `gorm:"column:avg_successful_defensive_action" json:"avgSuccessfulDefensiveAction"`
AvgYellowCards *float64 `gorm:"column:avg_yellow_cards" json:"avgYellowCards"`
AvgRedCards *float64 `gorm:"column:avg_red_cards" json:"avgRedCards"`
AvgDirectRedCards *float64 `gorm:"column:avg_direct_red_cards" json:"avgDirectRedCards"`
AvgSuccessfulAttackingActions *float64 `gorm:"column:avg_successful_attacking_actions" json:"avgSuccessfulAttackingActions"`
AvgFreeKicks *float64 `gorm:"column:avg_free_kicks" json:"avgFreeKicks"`
AvgDirectFreeKicks *float64 `gorm:"column:avg_direct_free_kicks" json:"avgDirectFreeKicks"`
AvgCorners *float64 `gorm:"column:avg_corners" json:"avgCorners"`
AvgPenalties *float64 `gorm:"column:avg_penalties" json:"avgPenalties"`
AvgPassLength *float64 `gorm:"column:avg_pass_length" json:"avgPassLength"`
AvgLongPassLength *float64 `gorm:"column:avg_long_pass_length" json:"avgLongPassLength"`
AvgDribbleDistanceFromOpponentGoal *float64 `gorm:"column:avg_dribble_distance_from_opponent_goal" json:"avgDribbleDistanceFromOpponentGoal"`
AvgAccelerations *float64 `gorm:"column:avg_accelerations" json:"avgAccelerations"`
AvgLooseBallDuels *float64 `gorm:"column:avg_loose_ball_duels" json:"avgLooseBallDuels"`
AvgMissedBalls *float64 `gorm:"column:avg_missed_balls" json:"avgMissedBalls"`
AvgForwardPasses *float64 `gorm:"column:avg_forward_passes" json:"avgForwardPasses"`
AvgBackPasses *float64 `gorm:"column:avg_back_passes" json:"avgBackPasses"`
AvgThroughPasses *float64 `gorm:"column:avg_through_passes" json:"avgThroughPasses"`
AvgKeyPasses *float64 `gorm:"column:avg_key_passes" json:"avgKeyPasses"`
AvgVerticalPasses *float64 `gorm:"column:avg_vertical_passes" json:"avgVerticalPasses"`
AvgLongPasses *float64 `gorm:"column:avg_long_passes" json:"avgLongPasses"`
AvgShotAssists *float64 `gorm:"column:avg_shot_assists" json:"avgShotAssists"`
AvgShotOnTargetAssists *float64 `gorm:"column:avg_shot_on_target_assists" json:"avgShotOnTargetAssists"`
AvgLinkupPlays *float64 `gorm:"column:avg_linkup_plays" json:"avgLinkupPlays"`
AvgBallRecoveries *float64 `gorm:"column:avg_ball_recoveries" json:"avgBallRecoveries"`
AvgOpponentHalfRecoveries *float64 `gorm:"column:avg_opponent_half_recoveries" json:"avgOpponentHalfRecoveries"`
AvgDangerousOpponentHalfRecoveries *float64 `gorm:"column:avg_dangerous_opponent_half_recoveries" json:"avgDangerousOpponentHalfRecoveries"`
AvgBallLosses *float64 `gorm:"column:avg_ball_losses" json:"avgBallLosses"`
AvgOwnHalfLosses *float64 `gorm:"column:avg_own_half_losses" json:"avgOwnHalfLosses"`
AvgDangerousOwnHalfLosses *float64 `gorm:"column:avg_dangerous_own_half_losses" json:"avgDangerousOwnHalfLosses"`
AvgFieldAerialDuels *float64 `gorm:"column:avg_field_aerial_duels" json:"avgFieldAerialDuels"`
AvgConcededGoals *float64 `gorm:"column:avg_conceded_goals" json:"avgConcededGoals"`
AvgGKExits *float64 `gorm:"column:avg_gk_exits" json:"avgGkExits"`
AvgGKAerialDuels *float64 `gorm:"column:avg_gk_aerial_duels" json:"avgGkAerialDuels"`
AvgDuelsWon *float64 `gorm:"column:avg_duels_won" json:"avgDuelsWon"`
AvgDefensiveDuelsWon *float64 `gorm:"column:avg_defensive_duels_won" json:"avgDefensiveDuelsWon"`
AvgOffensiveDuelsWon *float64 `gorm:"column:avg_offensive_duels_won" json:"avgOffensiveDuelsWon"`
AvgSuccessfulPasses *float64 `gorm:"column:avg_successful_passes" json:"avgSuccessfulPasses"`
AvgSuccessfulSmartPasses *float64 `gorm:"column:avg_successful_smart_passes" json:"avgSuccessfulSmartPasses"`
AvgSuccessfulPassesToFinalThird *float64 `gorm:"column:avg_successful_passes_to_final_third" json:"avgSuccessfulPassesToFinalThird"`
AvgSuccessfulCrosses *float64 `gorm:"column:avg_successful_crosses" json:"avgSuccessfulCrosses"`
AvgSuccessfulForwardPasses *float64 `gorm:"column:avg_successful_forward_passes" json:"avgSuccessfulForwardPasses"`
AvgSuccessfulBackPasses *float64 `gorm:"column:avg_successful_back_passes" json:"avgSuccessfulBackPasses"`
AvgSuccessfulThroughPasses *float64 `gorm:"column:avg_successful_through_passes" json:"avgSuccessfulThroughPasses"`
AvgSuccessfulKeyPasses *float64 `gorm:"column:avg_successful_key_passes" json:"avgSuccessfulKeyPasses"`
AvgSuccessfulVerticalPasses *float64 `gorm:"column:avg_successful_vertical_passes" json:"avgSuccessfulVerticalPasses"`
AvgSuccessfulLongPasses *float64 `gorm:"column:avg_successful_long_passes" json:"avgSuccessfulLongPasses"`
AvgSuccessfulDribbles *float64 `gorm:"column:avg_successful_dribbles" json:"avgSuccessfulDribbles"`
AvgDefensiveActions *float64 `gorm:"column:avg_defensive_actions" json:"avgDefensiveActions"`
AvgAttackingActions *float64 `gorm:"column:avg_attacking_actions" json:"avgAttackingActions"`
AvgFreeKicksOnTarget *float64 `gorm:"column:avg_free_kicks_on_target" json:"avgFreeKicksOnTarget"`
AvgDirectFreeKicksOnTarget *float64 `gorm:"column:avg_direct_free_kicks_on_target" json:"avgDirectFreeKicksOnTarget"`
AvgSuccessfulPenalties *float64 `gorm:"column:avg_successful_penalties" json:"avgSuccessfulPenalties"`
AvgSuccessfulLinkupPlays *float64 `gorm:"column:avg_successful_linkup_plays" json:"avgSuccessfulLinkupPlays"`
AvgLooseBallDuelsWon *float64 `gorm:"column:avg_loose_ball_duels_won" json:"avgLooseBallDuelsWon"`
AvgFieldAerialDuelsWon *float64 `gorm:"column:avg_field_aerial_duels_won" json:"avgFieldAerialDuelsWon"`
AvgGKSuccessfulExits *float64 `gorm:"column:avg_gk_successful_exits" json:"avgGkSuccessfulExits"`
AvgGKAerialDuelsWon *float64 `gorm:"column:avg_gk_aerial_duels_won" json:"avgGkAerialDuelsWon"`
AvgGKSaves *float64 `gorm:"column:avg_gk_saves" json:"avgGkSaves"`
AvgXGShot *float64 `gorm:"column:avg_xg_shot" json:"avgXgShot"`
AvgXGShotAgainst *float64 `gorm:"column:avg_xg_shot_against" json:"avgXgShotAgainst"`
AvgPPDA *float64 `gorm:"column:avg_ppda" json:"avgPpda"`
AvgReceivedPass *float64 `gorm:"column:avg_received_pass" json:"avgReceivedPass"`
AvgTouchInBox *float64 `gorm:"column:avg_touch_in_box" json:"avgTouchInBox"`
AvgProgressiveRun *float64 `gorm:"column:avg_progressive_run" json:"avgProgressiveRun"`
AvgOffsides *float64 `gorm:"column:avg_offsides" json:"avgOffsides"`
AvgOpponentOffsides *float64 `gorm:"column:avg_opponent_offsides" json:"avgOpponentOffsides"`
AvgShotsAgainst *float64 `gorm:"column:avg_shots_against" json:"avgShotsAgainst"`
AvgGKGoalKicks *float64 `gorm:"column:avg_gk_goal_kicks" json:"avgGkGoalKicks"`
AvgGKGoalKicksSuccess *float64 `gorm:"column:avg_gk_goal_kicks_success" json:"avgGkGoalKicksSuccess"`
AvgShortGoalKicks *float64 `gorm:"column:avg_short_goal_kicks" json:"avgShortGoalKicks"`
AvgLongGoalKicks *float64 `gorm:"column:avg_long_goal_kicks" json:"avgLongGoalKicks"`
AvgNewDuelsWon *float64 `gorm:"column:avg_new_duels_won" json:"avgNewDuelsWon"`
AvgNewDefensiveDuelsWon *float64 `gorm:"column:avg_new_defensive_duels_won" json:"avgNewDefensiveDuelsWon"`
AvgNewOffensiveDuelsWon *float64 `gorm:"column:avg_new_offensive_duels_won" json:"avgNewOffensiveDuelsWon"`
AvgNewSuccessfulDribbles *float64 `gorm:"column:avg_new_successful_dribbles" json:"avgNewSuccessfulDribbles"`
AvgLateralPasses *float64 `gorm:"column:avg_lateral_passes" json:"avgLateralPasses"`
AvgSuccessfulLateralPasses *float64 `gorm:"column:avg_successful_lateral_passes" json:"avgSuccessfulLateralPasses"`
PctDuelsWon *float64 `gorm:"column:pct_duels_won" json:"pctDuelsWon"`
PctDefensiveDuelsWon *float64 `gorm:"column:pct_defensive_duels_won" json:"pctDefensiveDuelsWon"`
PctOffensiveDuelsWon *float64 `gorm:"column:pct_offensive_duels_won" json:"pctOffensiveDuelsWon"`
PctAerialDuelsWon *float64 `gorm:"column:pct_aerial_duels_won" json:"pctAerialDuelsWon"`
PctSuccessfulPasses *float64 `gorm:"column:pct_successful_passes" json:"pctSuccessfulPasses"`
PctSuccessfulSmartPasses *float64 `gorm:"column:pct_successful_smart_passes" json:"pctSuccessfulSmartPasses"`
PctSuccessfulPassesToFinalThird *float64 `gorm:"column:pct_successful_passes_to_final_third" json:"pctSuccessfulPassesToFinalThird"`
PctSuccessfulCrosses *float64 `gorm:"column:pct_successful_crosses" json:"pctSuccessfulCrosses"`
PctSuccessfulDribbles *float64 `gorm:"column:pct_successful_dribbles" json:"pctSuccessfulDribbles"`
PctShotsOnTarget *float64 `gorm:"column:pct_shots_on_target" json:"pctShotsOnTarget"`
PctHeadShotsOnTarget *float64 `gorm:"column:pct_head_shots_on_target" json:"pctHeadShotsOnTarget"`
PctGoalConversion *float64 `gorm:"column:pct_goal_conversion" json:"pctGoalConversion"`
PctYellowCardsPerFoul *float64 `gorm:"column:pct_yellow_cards_per_foul" json:"pctYellowCardsPerFoul"`
PctDirectFreeKicksOnTarget *float64 `gorm:"column:pct_direct_free_kicks_on_target" json:"pctDirectFreeKicksOnTarget"`
PctPenaltiesConversion *float64 `gorm:"column:pct_penalties_conversion" json:"pctPenaltiesConversion"`
PctWin *float64 `gorm:"column:pct_win" json:"pctWin"`
PctSuccessfulForwardPasses *float64 `gorm:"column:pct_successful_forward_passes" json:"pctSuccessfulForwardPasses"`
PctSuccessfulBackPasses *float64 `gorm:"column:pct_successful_back_passes" json:"pctSuccessfulBackPasses"`
PctSuccessfulThroughPasses *float64 `gorm:"column:pct_successful_through_passes" json:"pctSuccessfulThroughPasses"`
PctSuccessfulKeyPasses *float64 `gorm:"column:pct_successful_key_passes" json:"pctSuccessfulKeyPasses"`
PctSuccessfulVerticalPasses *float64 `gorm:"column:pct_successful_vertical_passes" json:"pctSuccessfulVerticalPasses"`
PctSuccessfulLongPasses *float64 `gorm:"column:pct_successful_long_passes" json:"pctSuccessfulLongPasses"`
PctSuccessfulShotAssists *float64 `gorm:"column:pct_successful_shot_assists" json:"pctSuccessfulShotAssists"`
PctSuccessfulLinkupPlays *float64 `gorm:"column:pct_successful_linkup_plays" json:"pctSuccessfulLinkupPlays"`
PctFieldAerialDuelsWon *float64 `gorm:"column:pct_field_aerial_duels_won" json:"pctFieldAerialDuelsWon"`
PctGKSaves *float64 `gorm:"column:pct_gk_saves" json:"pctGkSaves"`
PctGKSuccessfulExits *float64 `gorm:"column:pct_gk_successful_exits" json:"pctGkSuccessfulExits"`
PctGKAerialDuelsWon *float64 `gorm:"column:pct_gk_aerial_duels_won" json:"pctGkAerialDuelsWon"`
PctSuccessfulTouchInBox *float64 `gorm:"column:pct_successful_touch_in_box" json:"pctSuccessfulTouchInBox"`
PctNewDuelsWon *float64 `gorm:"column:pct_new_duels_won" json:"pctNewDuelsWon"`
PctNewDefensiveDuelsWon *float64 `gorm:"column:pct_new_defensive_duels_won" json:"pctNewDefensiveDuelsWon"`
PctNewOffensiveDuelsWon *float64 `gorm:"column:pct_new_offensive_duels_won" json:"pctNewOffensiveDuelsWon"`
PctNewSuccessfulDribbles *float64 `gorm:"column:pct_new_successful_dribbles" json:"pctNewSuccessfulDribbles"`
PctSuccessfulLateralPasses *float64 `gorm:"column:pct_successful_lateral_passes" json:"pctSuccessfulLateralPasses"`
APILastSyncedAt *time.Time `gorm:"column:api_last_synced_at" json:"apiLastSyncedAt"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
}
type ImportCheckpoint struct {
Key string `gorm:"primaryKey;column:key" json:"key"`
LastPlayerWyID int `gorm:"column:last_player_wy_id" json:"lastPlayerWyId"`
LastTeamWyID int `gorm:"column:last_team_wy_id" json:"lastTeamWyId"`
LastMatchWyID int `gorm:"column:last_match_wy_id" json:"lastMatchWyId"`
LastCompetitionID int `gorm:"column:last_competition_id" json:"lastCompetitionId"`
LastSeasonID int `gorm:"column:last_season_id" json:"lastSeasonId"`
Processed int64 `gorm:"column:processed" json:"processed"`
Errors int64 `gorm:"column:errors" json:"errors"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
}
func (pas *PlayerAdvancedStats) BeforeCreate(tx *gorm.DB) (err error) {
if pas.ID != "" {
return nil
}
id, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 15)
if err != nil {
return err
}
pas.ID = id
return nil
}
func (pap *PlayerAdvancedPosition) BeforeCreate(tx *gorm.DB) (err error) {
if pap.ID != "" {
return nil
}
id, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 15)
if err != nil {
return err
}
pap.ID = id
return nil
}
func (tas *TeamAdvancedStats) BeforeCreate(tx *gorm.DB) (err error) {
if tas.ID != "" {
return nil
}
id, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 15)
if err != nil {
return err
}
tas.ID = id
return nil
}
func (mas *MatchAdvancedStats) BeforeCreate(tx *gorm.DB) (err error) {
if mas.ID != "" {
return nil
}
id, err := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 15)
if err != nil {
return err
}
mas.ID = id
return nil
}
type TeamCareer struct { type TeamCareer struct {
ID string `gorm:"primaryKey;size:16" json:"id"` ID string `gorm:"primaryKey;size:16" json:"id"`
TeamWyID int `gorm:"column:team_wy_id;uniqueIndex:uidx_team_careers;index" json:"teamWyId"` TeamWyID int `gorm:"column:team_wy_id;uniqueIndex:uidx_team_careers;index" json:"teamWyId"`
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"strings" "strings"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
"ScoutingSystemScoreData/internal/errors" "ScoutingSystemScoreData/internal/errors"
"ScoutingSystemScoreData/internal/models" "ScoutingSystemScoreData/internal/models"
...@@ -41,51 +42,48 @@ func (s *coachService) ListCoaches(ctx context.Context, opts ListCoachesOptions) ...@@ -41,51 +42,48 @@ func (s *coachService) ListCoaches(ctx context.Context, opts ListCoachesOptions)
query := s.db.WithContext(ctx).Model(&models.Coach{}) query := s.db.WithContext(ctx).Model(&models.Coach{})
if opts.Name != "" { if opts.Name != "" {
// Normalize the search term for better matching searchLower := strings.ToLower(strings.TrimSpace(opts.Name))
normalizedSearch := utils.NormalizeText(opts.Name) likePattern := "%" + searchLower + "%"
searchTokens := utils.TokenizeSearchTerm(opts.Name) searchTokens := utils.TokenizeSearchTerm(opts.Name)
// Build flexible search conditions
likePattern := "%" + normalizedSearch + "%"
// Create search conditions for normalized text
searchConditions := s.db.Where( searchConditions := s.db.Where(
"LOWER(REGEXP_REPLACE(coaches.short_name, '[^a-zA-Z0-9 ]', '', 'g')) ILIKE ? OR "+ "unaccent(LOWER(coaches.short_name)) ILIKE unaccent(?) OR "+
"LOWER(REGEXP_REPLACE(coaches.first_name, '[^a-zA-Z0-9 ]', '', 'g')) ILIKE ? OR "+ "unaccent(LOWER(coaches.first_name)) ILIKE unaccent(?) OR "+
"LOWER(REGEXP_REPLACE(coaches.middle_name, '[^a-zA-Z0-9 ]', '', 'g')) ILIKE ? OR "+ "unaccent(LOWER(coaches.middle_name)) ILIKE unaccent(?) OR "+
"LOWER(REGEXP_REPLACE(coaches.last_name, '[^a-zA-Z0-9 ]', '', 'g')) ILIKE ?", "unaccent(LOWER(coaches.last_name)) ILIKE unaccent(?)",
likePattern, likePattern, likePattern, likePattern, likePattern, likePattern, likePattern, likePattern,
) )
// Also search with original pattern for exact matches
originalPattern := "%" + strings.ToLower(opts.Name) + "%"
searchConditions = searchConditions.Or(
"coaches.short_name ILIKE ? OR coaches.first_name ILIKE ? OR coaches.middle_name ILIKE ? OR coaches.last_name ILIKE ?",
originalPattern, originalPattern, originalPattern, originalPattern,
)
// Add token-based search for multi-word queries
if len(searchTokens) > 1 {
// Intentionally do NOT OR tokens together.
// For multi-token input (e.g. "pablo rosario"), we require ALL tokens to match
// somewhere across the name fields.
}
query = query.Where(searchConditions)
if len(searchTokens) > 1 { if len(searchTokens) > 1 {
tokenFilter := s.db
for _, token := range searchTokens { for _, token := range searchTokens {
tokenPattern := "%" + token + "%" tokenPattern := "%" + token + "%"
query = query.Where( tokenFilter = tokenFilter.Where(
"LOWER(coaches.short_name) ILIKE ? OR LOWER(coaches.first_name) ILIKE ? OR LOWER(coaches.middle_name) ILIKE ? OR LOWER(coaches.last_name) ILIKE ?", "unaccent(LOWER(coaches.short_name)) ILIKE unaccent(?) OR unaccent(LOWER(coaches.first_name)) ILIKE unaccent(?) OR "+
"unaccent(LOWER(coaches.middle_name)) ILIKE unaccent(?) OR unaccent(LOWER(coaches.last_name)) ILIKE unaccent(?)",
tokenPattern, tokenPattern, tokenPattern, tokenPattern, tokenPattern, tokenPattern, tokenPattern, tokenPattern,
) )
} }
searchConditions = searchConditions.Or(tokenFilter)
} }
query = query.Where(searchConditions)
// Join with teams to check for big competitions // Join with teams to check for big competitions
query = query.Joins("LEFT JOIN teams ON teams.wy_id = coaches.current_team_wy_id") query = query.Joins("LEFT JOIN teams ON teams.wy_id = coaches.current_team_wy_id")
query = query.Joins("LEFT JOIN competitions ON competitions.wy_id = teams.competition_wy_id") query = query.Joins("LEFT JOIN competitions ON competitions.wy_id = teams.competition_wy_id")
query = query.Order(clause.Expr{SQL: "CASE " +
"WHEN unaccent(LOWER(coaches.short_name)) = unaccent(?) THEN 0 " +
"WHEN unaccent(LOWER(coaches.short_name)) ILIKE unaccent(?) THEN 1 " +
"WHEN unaccent(LOWER(coaches.last_name)) = unaccent(?) THEN 2 " +
"WHEN unaccent(LOWER(coaches.first_name)) = unaccent(?) THEN 3 " +
"WHEN unaccent(LOWER(coaches.last_name)) ILIKE unaccent(?) THEN 4 " +
"WHEN unaccent(LOWER(coaches.first_name)) ILIKE unaccent(?) THEN 5 " +
"ELSE 6 END",
Vars: []interface{}{searchLower, likePattern, searchLower, searchLower, likePattern, likePattern},
})
// Prioritization logic for coaches // Prioritization logic for coaches
query = query.Order("coaches.is_active DESC") query = query.Order("coaches.is_active DESC")
query = query.Order("CASE WHEN teams.competition_wy_id IN (364, 795, 426, 524, 412, 102, 103) THEN 0 ELSE 1 END") query = query.Order("CASE WHEN teams.competition_wy_id IN (364, 795, 426, 524, 412, 102, 103) THEN 0 ELSE 1 END")
......
...@@ -29,6 +29,7 @@ type ListPlayersOptions struct { ...@@ -29,6 +29,7 @@ type ListPlayersOptions struct {
Name string Name string
TeamID string TeamID string
Country string Country string
Gender string
Roles []string Roles []string
} }
...@@ -44,6 +45,10 @@ func (s *playerService) ListPlayers(ctx context.Context, opts ListPlayersOptions ...@@ -44,6 +45,10 @@ func (s *playerService) ListPlayers(ctx context.Context, opts ListPlayersOptions
var players []models.Player var players []models.Player
baseQuery := s.db.WithContext(ctx).Model(&models.Player{}).Where("players.is_active = ?", true) baseQuery := s.db.WithContext(ctx).Model(&models.Player{}).Where("players.is_active = ?", true)
if strings.TrimSpace(opts.Gender) != "" {
baseQuery = baseQuery.Where("players.gender = ?", strings.ToLower(strings.TrimSpace(opts.Gender)))
}
if len(opts.Roles) > 0 { if len(opts.Roles) > 0 {
roles := make([]string, 0, len(opts.Roles)) roles := make([]string, 0, len(opts.Roles))
seen := make(map[string]struct{}, len(opts.Roles)) seen := make(map[string]struct{}, len(opts.Roles))
...@@ -64,87 +69,66 @@ func (s *playerService) ListPlayers(ctx context.Context, opts ListPlayersOptions ...@@ -64,87 +69,66 @@ func (s *playerService) ListPlayers(ctx context.Context, opts ListPlayersOptions
} }
if opts.Name != "" { if opts.Name != "" {
// Normalize the search term for better matching // Normalize search term to lowercase for index-friendly queries
normalizedSearch := utils.NormalizeText(opts.Name) searchLower := strings.ToLower(strings.TrimSpace(opts.Name))
likePattern := "%" + searchLower + "%"
searchTokens := utils.TokenizeSearchTerm(opts.Name) searchTokens := utils.TokenizeSearchTerm(opts.Name)
// Build flexible search conditions // Join with teams table to enable team name search
likePattern := "%" + normalizedSearch + "%" baseQuery = baseQuery.Joins("LEFT JOIN teams ON teams.wy_id = players.current_team_id")
// Create search conditions for normalized text // Build search conditions using LOWER() which can use GIN trigram indexes
// Using LOWER() and unaccent-like matching through normalized comparison // Search in: player names AND team names
searchConditions := s.db.Where( searchConditions := s.db.Where(
"LOWER(REGEXP_REPLACE(players.short_name, '[^a-zA-Z0-9 ]', '', 'g')) ILIKE ? OR "+ "unaccent(LOWER(players.short_name)) ILIKE unaccent(?) OR "+
"LOWER(REGEXP_REPLACE(players.first_name, '[^a-zA-Z0-9 ]', '', 'g')) ILIKE ? OR "+ "unaccent(LOWER(players.first_name)) ILIKE unaccent(?) OR "+
"LOWER(REGEXP_REPLACE(players.middle_name, '[^a-zA-Z0-9 ]', '', 'g')) ILIKE ? OR "+ "unaccent(LOWER(players.middle_name)) ILIKE unaccent(?) OR "+
"LOWER(REGEXP_REPLACE(players.last_name, '[^a-zA-Z0-9 ]', '', 'g')) ILIKE ?", "unaccent(LOWER(players.last_name)) ILIKE unaccent(?) OR "+
likePattern, likePattern, likePattern, likePattern, "unaccent(LOWER(teams.name)) ILIKE unaccent(?) OR "+
) "unaccent(LOWER(teams.short_name)) ILIKE unaccent(?)",
likePattern, likePattern, likePattern, likePattern, likePattern, likePattern,
// Also search with original pattern for exact matches
originalPattern := "%" + strings.ToLower(opts.Name) + "%"
searchConditions = searchConditions.Or(
"players.short_name ILIKE ? OR players.first_name ILIKE ? OR players.middle_name ILIKE ? OR players.last_name ILIKE ?",
originalPattern, originalPattern, originalPattern, originalPattern,
) )
// Build multi-token filter as: // For multi-word searches (e.g., "ronaldo alnassr", "samu porto"):
// (phrase-like match across fields) OR (ALL tokens match somewhere across fields) // Match if tokens appear across player name + team name fields
nameFilter := s.db.Where(searchConditions)
if len(searchTokens) > 1 { if len(searchTokens) > 1 {
// Strategy: each token must match somewhere (player name OR team name)
tokenFilter := s.db tokenFilter := s.db
for _, token := range searchTokens { for _, token := range searchTokens {
tokenPattern := "%" + token + "%" tokenPattern := "%" + token + "%"
tokenFilter = tokenFilter.Where( tokenFilter = tokenFilter.Where(
"LOWER(players.short_name) ILIKE ? OR LOWER(players.first_name) ILIKE ? OR LOWER(players.middle_name) ILIKE ? OR LOWER(players.last_name) ILIKE ?", "unaccent(LOWER(players.short_name)) ILIKE unaccent(?) OR unaccent(LOWER(players.first_name)) ILIKE unaccent(?) OR "+
tokenPattern, tokenPattern, tokenPattern, tokenPattern, "unaccent(LOWER(players.middle_name)) ILIKE unaccent(?) OR unaccent(LOWER(players.last_name)) ILIKE unaccent(?) OR "+
"unaccent(LOWER(teams.name)) ILIKE unaccent(?) OR unaccent(LOWER(teams.short_name)) ILIKE unaccent(?)",
tokenPattern, tokenPattern, tokenPattern, tokenPattern, tokenPattern, tokenPattern,
) )
} }
nameFilter = nameFilter.Or(tokenFilter) searchConditions = searchConditions.Or(tokenFilter)
} }
baseQuery = baseQuery.Where(nameFilter) baseQuery = baseQuery.Where(searchConditions)
// Always rank best name match first. // Optimized ranking: prioritize ShortName matches first
// This prevents a "big competition" result from outranking an exact name hit. // Using simple LOWER() comparisons that can leverage indexes
fetchQuery := baseQuery fetchQuery := baseQuery
fetchQuery = fetchQuery.Order(clause.Expr{SQL: "CASE " + fetchQuery = fetchQuery.Order(clause.Expr{SQL: "CASE " +
"WHEN LOWER(REGEXP_REPLACE(players.short_name, '[^a-zA-Z0-9 ]', '', 'g')) = ? THEN 0 " + "WHEN unaccent(LOWER(players.short_name)) = unaccent(?) THEN 0 " +
"WHEN TRIM(LOWER(REGEXP_REPLACE(players.first_name, '[^a-zA-Z0-9 ]', '', 'g')) || ' ' || LOWER(REGEXP_REPLACE(players.last_name, '[^a-zA-Z0-9 ]', '', 'g'))) = ? THEN 1 " + "WHEN unaccent(LOWER(players.short_name)) ILIKE unaccent(?) THEN 1 " +
"WHEN TRIM(LOWER(REGEXP_REPLACE(players.first_name, '[^a-zA-Z0-9 ]', '', 'g')) || ' ' || COALESCE(LOWER(REGEXP_REPLACE(players.middle_name, '[^a-zA-Z0-9 ]', '', 'g')) || ' ', '') || LOWER(REGEXP_REPLACE(players.last_name, '[^a-zA-Z0-9 ]', '', 'g'))) = ? THEN 2 " + "WHEN unaccent(LOWER(players.last_name)) = unaccent(?) THEN 2 " +
"WHEN LOWER(REGEXP_REPLACE(players.last_name, '[^a-zA-Z0-9 ]', '', 'g')) = ? THEN 3 " + "WHEN unaccent(LOWER(players.first_name)) = unaccent(?) THEN 3 " +
"WHEN LOWER(REGEXP_REPLACE(players.first_name, '[^a-zA-Z0-9 ]', '', 'g')) = ? THEN 4 " + "WHEN unaccent(LOWER(players.last_name)) ILIKE unaccent(?) THEN 4 " +
"ELSE 5 END", "WHEN unaccent(LOWER(players.first_name)) ILIKE unaccent(?) THEN 5 " +
Vars: []interface{}{normalizedSearch, normalizedSearch, normalizedSearch, normalizedSearch, normalizedSearch}, "ELSE 6 END",
Vars: []interface{}{searchLower, likePattern, searchLower, searchLower, likePattern, likePattern},
}) })
// Prioritization logic: // Secondary sort: prioritize players with market value (professional/active players)
// 1. Players with current team (especially from big competitions) fetchQuery = fetchQuery.Order("CASE WHEN players.market_value IS NOT NULL AND players.market_value > 0 THEN 0 ELSE 1 END")
// 2. Players with market value (indicates active/professional status) fetchQuery = fetchQuery.Order("players.market_value DESC NULLS LAST")
// 3. Players with national team
// 4. Alphabetical by name
// Join with teams to check for big competitions // Tertiary sort: players with current team
fetchQuery = fetchQuery.Joins("LEFT JOIN teams ON teams.wy_id = players.current_team_id")
fetchQuery = fetchQuery.Joins("LEFT JOIN competitions ON competitions.wy_id = teams.competition_wy_id")
// Tie-breakers after name match rank.
// Priority 1: Big competitions (top 5 leagues + Champions League, etc.)
// Competition WyIDs for major leagues:
// Premier League: 364, La Liga: 795, Bundesliga: 426, Serie A: 524, Ligue 1: 412
// Champions League: 102, Europa League: 103, etc.
fetchQuery = fetchQuery.Order("CASE WHEN teams.competition_wy_id IN (364, 795, 426, 524, 412, 102, 103) THEN 0 ELSE 1 END")
// Priority 2: Has current team
fetchQuery = fetchQuery.Order("CASE WHEN players.current_team_id IS NOT NULL THEN 0 ELSE 1 END") fetchQuery = fetchQuery.Order("CASE WHEN players.current_team_id IS NOT NULL THEN 0 ELSE 1 END")
// Priority 3: Market value (higher is better) // Final sort: alphabetical
fetchQuery = fetchQuery.Order("CASE WHEN players.market_value IS NULL THEN 1 ELSE 0 END")
fetchQuery = fetchQuery.Order("players.market_value DESC")
// Priority 4: Has national team
fetchQuery = fetchQuery.Order("CASE WHEN players.current_national_team_id IS NULL THEN 1 ELSE 0 END")
// Priority 5: Alphabetical
fetchQuery = fetchQuery.Order("players.last_name ASC") fetchQuery = fetchQuery.Order("players.last_name ASC")
fetchQuery = fetchQuery.Order("players.first_name ASC") fetchQuery = fetchQuery.Order("players.first_name ASC")
......
...@@ -3,6 +3,7 @@ package services ...@@ -3,6 +3,7 @@ package services
import ( import (
"context" "context"
"strconv" "strconv"
"strings"
"gorm.io/gorm" "gorm.io/gorm"
...@@ -11,7 +12,7 @@ import ( ...@@ -11,7 +12,7 @@ import (
) )
type TeamService interface { type TeamService interface {
ListTeams(ctx context.Context, limit, offset int, name string) ([]models.Team, int64, error) ListTeams(ctx context.Context, limit, offset int, name, gender, teamType string) ([]models.Team, int64, error)
GetByID(ctx context.Context, id string) (models.Team, error) GetByID(ctx context.Context, id string) (models.Team, error)
GetByWyID(ctx context.Context, wyID int) (models.Team, error) GetByWyID(ctx context.Context, wyID int) (models.Team, error)
GetByAnyProviderID(ctx context.Context, providerID string) (models.Team, error) GetByAnyProviderID(ctx context.Context, providerID string) (models.Team, error)
...@@ -28,13 +29,20 @@ func NewTeamService(db *gorm.DB) TeamService { ...@@ -28,13 +29,20 @@ func NewTeamService(db *gorm.DB) TeamService {
return &teamService{db: db} return &teamService{db: db}
} }
func (s *teamService) ListTeams(ctx context.Context, limit, offset int, name string) ([]models.Team, int64, error) { func (s *teamService) ListTeams(ctx context.Context, limit, offset int, name, gender, teamType string) ([]models.Team, int64, error) {
var teams []models.Team var teams []models.Team
query := s.db.WithContext(ctx).Model(&models.Team{}) query := s.db.WithContext(ctx).Model(&models.Team{})
if name != "" { if name != "" {
query = query.Where("name ILIKE ?", "%"+name+"%") query = query.Where("name ILIKE ?", "%"+name+"%")
} }
if strings.TrimSpace(gender) != "" {
query = query.Where("gender = ?", strings.ToLower(strings.TrimSpace(gender)))
}
if strings.TrimSpace(teamType) != "" {
query = query.Where("type = ?", strings.ToLower(strings.TrimSpace(teamType)))
}
query = query.Order("CASE WHEN market_value IS NULL THEN 1 ELSE 0 END") query = query.Order("CASE WHEN market_value IS NULL THEN 1 ELSE 0 END")
query = query.Order("market_value DESC") query = query.Order("market_value DESC")
query = query.Order("is_active DESC") query = query.Order("is_active DESC")
......
-- Migration 0014: create player_advanced_stats table for Wyscout player advanced stats
CREATE TABLE IF NOT EXISTS player_advanced_stats (
id varchar(16) PRIMARY KEY,
player_wy_id integer NOT NULL,
competition_id integer NOT NULL,
season_id integer NOT NULL,
round_id integer,
positions_json jsonb,
total_json jsonb,
average_json jsonb,
percent_json jsonb,
api_last_synced_at timestamp with time zone,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT uidx_player_advanced_stats UNIQUE (player_wy_id, competition_id, season_id)
);
CREATE INDEX IF NOT EXISTS idx_player_advanced_stats_player_wy_id ON player_advanced_stats (player_wy_id);
CREATE INDEX IF NOT EXISTS idx_player_advanced_stats_competition_id ON player_advanced_stats (competition_id);
CREATE INDEX IF NOT EXISTS idx_player_advanced_stats_season_id ON player_advanced_stats (season_id);
-- Migration 0015: expand player_advanced_stats into explicit columns + positions table
-- Drop previous JSON columns (if present)
ALTER TABLE player_advanced_stats
DROP COLUMN IF EXISTS positions_json,
DROP COLUMN IF EXISTS total_json,
DROP COLUMN IF EXISTS average_json,
DROP COLUMN IF EXISTS percent_json;
-- Core metadata
ALTER TABLE player_advanced_stats
ADD COLUMN IF NOT EXISTS round_id integer;
-- TOTAL metrics
ALTER TABLE player_advanced_stats
ADD COLUMN IF NOT EXISTS total_matches integer,
ADD COLUMN IF NOT EXISTS total_matches_in_start integer,
ADD COLUMN IF NOT EXISTS total_matches_substituted integer,
ADD COLUMN IF NOT EXISTS total_matches_coming_off integer,
ADD COLUMN IF NOT EXISTS total_minutes_on_field integer,
ADD COLUMN IF NOT EXISTS total_minutes_tagged integer,
ADD COLUMN IF NOT EXISTS total_goals integer,
ADD COLUMN IF NOT EXISTS total_assists integer,
ADD COLUMN IF NOT EXISTS total_shots integer,
ADD COLUMN IF NOT EXISTS total_head_shots integer,
ADD COLUMN IF NOT EXISTS total_yellow_cards integer,
ADD COLUMN IF NOT EXISTS total_red_cards integer,
ADD COLUMN IF NOT EXISTS total_direct_red_cards integer,
ADD COLUMN IF NOT EXISTS total_penalties integer,
ADD COLUMN IF NOT EXISTS total_linkup_plays integer,
ADD COLUMN IF NOT EXISTS total_duels integer,
ADD COLUMN IF NOT EXISTS total_duels_won integer,
ADD COLUMN IF NOT EXISTS total_defensive_duels integer,
ADD COLUMN IF NOT EXISTS total_defensive_duels_won integer,
ADD COLUMN IF NOT EXISTS total_offensive_duels integer,
ADD COLUMN IF NOT EXISTS total_offensive_duels_won integer,
ADD COLUMN IF NOT EXISTS total_aerial_duels integer,
ADD COLUMN IF NOT EXISTS total_aerial_duels_won integer,
ADD COLUMN IF NOT EXISTS total_fouls integer,
ADD COLUMN IF NOT EXISTS total_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_passes integer,
ADD COLUMN IF NOT EXISTS total_smart_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_smart_passes integer,
ADD COLUMN IF NOT EXISTS total_passes_to_final_third integer,
ADD COLUMN IF NOT EXISTS total_successful_passes_to_final_third integer,
ADD COLUMN IF NOT EXISTS total_crosses integer,
ADD COLUMN IF NOT EXISTS total_successful_crosses integer,
ADD COLUMN IF NOT EXISTS total_forward_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_forward_passes integer,
ADD COLUMN IF NOT EXISTS total_back_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_back_passes integer,
ADD COLUMN IF NOT EXISTS total_through_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_through_passes integer,
ADD COLUMN IF NOT EXISTS total_key_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_key_passes integer,
ADD COLUMN IF NOT EXISTS total_vertical_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_vertical_passes integer,
ADD COLUMN IF NOT EXISTS total_long_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_long_passes integer,
ADD COLUMN IF NOT EXISTS total_dribbles integer,
ADD COLUMN IF NOT EXISTS total_successful_dribbles integer,
ADD COLUMN IF NOT EXISTS total_interceptions integer,
ADD COLUMN IF NOT EXISTS total_defensive_actions integer,
ADD COLUMN IF NOT EXISTS total_successful_defensive_action integer,
ADD COLUMN IF NOT EXISTS total_attacking_actions integer,
ADD COLUMN IF NOT EXISTS total_successful_attacking_actions integer,
ADD COLUMN IF NOT EXISTS total_free_kicks integer,
ADD COLUMN IF NOT EXISTS total_free_kicks_on_target integer,
ADD COLUMN IF NOT EXISTS total_direct_free_kicks integer,
ADD COLUMN IF NOT EXISTS total_direct_free_kicks_on_target integer,
ADD COLUMN IF NOT EXISTS total_corners integer,
ADD COLUMN IF NOT EXISTS total_successful_penalties integer,
ADD COLUMN IF NOT EXISTS total_successful_linkup_plays integer,
ADD COLUMN IF NOT EXISTS total_accelerations integer,
ADD COLUMN IF NOT EXISTS total_pressing_duels integer,
ADD COLUMN IF NOT EXISTS total_pressing_duels_won integer,
ADD COLUMN IF NOT EXISTS total_loose_ball_duels integer,
ADD COLUMN IF NOT EXISTS total_loose_ball_duels_won integer,
ADD COLUMN IF NOT EXISTS total_missed_balls integer,
ADD COLUMN IF NOT EXISTS total_shot_assists integer,
ADD COLUMN IF NOT EXISTS total_shot_on_target_assists integer,
ADD COLUMN IF NOT EXISTS total_recoveries integer,
ADD COLUMN IF NOT EXISTS total_opponent_half_recoveries integer,
ADD COLUMN IF NOT EXISTS total_dangerous_opponent_half_recoveries integer,
ADD COLUMN IF NOT EXISTS total_losses integer,
ADD COLUMN IF NOT EXISTS total_own_half_losses integer,
ADD COLUMN IF NOT EXISTS total_dangerous_own_half_losses integer,
ADD COLUMN IF NOT EXISTS total_xg_shot double precision,
ADD COLUMN IF NOT EXISTS total_xg_assist double precision,
ADD COLUMN IF NOT EXISTS total_xg_save double precision,
ADD COLUMN IF NOT EXISTS total_received_pass integer,
ADD COLUMN IF NOT EXISTS total_touch_in_box integer,
ADD COLUMN IF NOT EXISTS total_progressive_run integer,
ADD COLUMN IF NOT EXISTS total_offsides integer,
ADD COLUMN IF NOT EXISTS total_clearances integer,
ADD COLUMN IF NOT EXISTS total_second_assists integer,
ADD COLUMN IF NOT EXISTS total_third_assists integer,
ADD COLUMN IF NOT EXISTS total_shots_blocked integer,
ADD COLUMN IF NOT EXISTS total_fouls_suffered integer,
ADD COLUMN IF NOT EXISTS total_progressive_passes integer,
ADD COLUMN IF NOT EXISTS total_counterpressing_recoveries integer,
ADD COLUMN IF NOT EXISTS total_sliding_tackles integer,
ADD COLUMN IF NOT EXISTS total_goal_kicks integer,
ADD COLUMN IF NOT EXISTS total_dribbles_against integer,
ADD COLUMN IF NOT EXISTS total_dribbles_against_won integer,
ADD COLUMN IF NOT EXISTS total_goal_kicks_short integer,
ADD COLUMN IF NOT EXISTS total_goal_kicks_long integer,
ADD COLUMN IF NOT EXISTS total_shots_on_target integer,
ADD COLUMN IF NOT EXISTS total_successful_progressive_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_sliding_tackles integer,
ADD COLUMN IF NOT EXISTS total_successful_goal_kicks integer,
ADD COLUMN IF NOT EXISTS total_field_aerial_duels integer,
ADD COLUMN IF NOT EXISTS total_field_aerial_duels_won integer,
ADD COLUMN IF NOT EXISTS total_gk_clean_sheets integer,
ADD COLUMN IF NOT EXISTS total_gk_conceded_goals integer,
ADD COLUMN IF NOT EXISTS total_gk_shots_against integer,
ADD COLUMN IF NOT EXISTS total_gk_exits integer,
ADD COLUMN IF NOT EXISTS total_gk_successful_exits integer,
ADD COLUMN IF NOT EXISTS total_gk_aerial_duels integer,
ADD COLUMN IF NOT EXISTS total_gk_aerial_duels_won integer,
ADD COLUMN IF NOT EXISTS total_gk_saves integer,
ADD COLUMN IF NOT EXISTS total_new_duels_won integer,
ADD COLUMN IF NOT EXISTS total_new_defensive_duels_won integer,
ADD COLUMN IF NOT EXISTS total_new_offensive_duels_won integer,
ADD COLUMN IF NOT EXISTS total_new_successful_dribbles integer,
ADD COLUMN IF NOT EXISTS total_lateral_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_lateral_passes integer;
-- AVERAGE metrics
ALTER TABLE player_advanced_stats
ADD COLUMN IF NOT EXISTS avg_pass_length double precision,
ADD COLUMN IF NOT EXISTS avg_long_pass_length double precision,
ADD COLUMN IF NOT EXISTS avg_dribble_distance_from_opponent_goal double precision,
ADD COLUMN IF NOT EXISTS avg_ball_recoveries double precision,
ADD COLUMN IF NOT EXISTS avg_duels double precision,
ADD COLUMN IF NOT EXISTS avg_defensive_duels double precision,
ADD COLUMN IF NOT EXISTS avg_offensive_duels double precision,
ADD COLUMN IF NOT EXISTS avg_aerial_duels double precision,
ADD COLUMN IF NOT EXISTS avg_fouls double precision,
ADD COLUMN IF NOT EXISTS avg_goals double precision,
ADD COLUMN IF NOT EXISTS avg_assists double precision,
ADD COLUMN IF NOT EXISTS avg_passes double precision,
ADD COLUMN IF NOT EXISTS avg_smart_passes double precision,
ADD COLUMN IF NOT EXISTS avg_passes_to_final_third double precision,
ADD COLUMN IF NOT EXISTS avg_crosses double precision,
ADD COLUMN IF NOT EXISTS avg_dribbles double precision,
ADD COLUMN IF NOT EXISTS avg_shots double precision,
ADD COLUMN IF NOT EXISTS avg_head_shots double precision,
ADD COLUMN IF NOT EXISTS avg_interceptions double precision,
ADD COLUMN IF NOT EXISTS avg_successful_defensive_action double precision,
ADD COLUMN IF NOT EXISTS avg_yellow_cards double precision,
ADD COLUMN IF NOT EXISTS avg_red_cards double precision,
ADD COLUMN IF NOT EXISTS avg_direct_red_cards double precision,
ADD COLUMN IF NOT EXISTS avg_successful_attacking_actions double precision,
ADD COLUMN IF NOT EXISTS avg_free_kicks double precision,
ADD COLUMN IF NOT EXISTS avg_direct_free_kicks double precision,
ADD COLUMN IF NOT EXISTS avg_corners double precision,
ADD COLUMN IF NOT EXISTS avg_penalties double precision,
ADD COLUMN IF NOT EXISTS avg_accelerations double precision,
ADD COLUMN IF NOT EXISTS avg_loose_ball_duels double precision,
ADD COLUMN IF NOT EXISTS avg_missed_balls double precision,
ADD COLUMN IF NOT EXISTS avg_forward_passes double precision,
ADD COLUMN IF NOT EXISTS avg_back_passes double precision,
ADD COLUMN IF NOT EXISTS avg_through_passes double precision,
ADD COLUMN IF NOT EXISTS avg_key_passes double precision,
ADD COLUMN IF NOT EXISTS avg_vertical_passes double precision,
ADD COLUMN IF NOT EXISTS avg_long_passes double precision,
ADD COLUMN IF NOT EXISTS avg_shot_assists double precision,
ADD COLUMN IF NOT EXISTS avg_shot_on_target_assists double precision,
ADD COLUMN IF NOT EXISTS avg_linkup_plays double precision,
ADD COLUMN IF NOT EXISTS avg_opponent_half_recoveries double precision,
ADD COLUMN IF NOT EXISTS avg_dangerous_opponent_half_recoveries double precision,
ADD COLUMN IF NOT EXISTS avg_ball_losses double precision,
ADD COLUMN IF NOT EXISTS avg_losses double precision,
ADD COLUMN IF NOT EXISTS avg_own_half_losses double precision,
ADD COLUMN IF NOT EXISTS avg_dangerous_own_half_losses double precision,
ADD COLUMN IF NOT EXISTS avg_duels_won double precision,
ADD COLUMN IF NOT EXISTS avg_defensive_duels_won double precision,
ADD COLUMN IF NOT EXISTS avg_offensive_duels_won double precision,
ADD COLUMN IF NOT EXISTS avg_successful_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_smart_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_crosses double precision,
ADD COLUMN IF NOT EXISTS avg_successful_forward_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_back_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_through_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_key_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_vertical_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_long_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_dribbles double precision,
ADD COLUMN IF NOT EXISTS avg_defensive_actions double precision,
ADD COLUMN IF NOT EXISTS avg_attacking_actions double precision,
ADD COLUMN IF NOT EXISTS avg_free_kicks_on_target double precision,
ADD COLUMN IF NOT EXISTS avg_direct_free_kicks_on_target double precision,
ADD COLUMN IF NOT EXISTS avg_successful_penalties double precision,
ADD COLUMN IF NOT EXISTS avg_successful_linkup_plays double precision,
ADD COLUMN IF NOT EXISTS avg_loose_ball_duels_won double precision,
ADD COLUMN IF NOT EXISTS avg_successful_passes_to_final_third double precision,
ADD COLUMN IF NOT EXISTS avg_xg_shot double precision,
ADD COLUMN IF NOT EXISTS avg_xg_assist double precision,
ADD COLUMN IF NOT EXISTS avg_xg_save double precision,
ADD COLUMN IF NOT EXISTS avg_received_pass double precision,
ADD COLUMN IF NOT EXISTS avg_touch_in_box double precision,
ADD COLUMN IF NOT EXISTS avg_progressive_run double precision,
ADD COLUMN IF NOT EXISTS avg_offsides double precision,
ADD COLUMN IF NOT EXISTS avg_clearances double precision,
ADD COLUMN IF NOT EXISTS avg_second_assists double precision,
ADD COLUMN IF NOT EXISTS avg_third_assists double precision,
ADD COLUMN IF NOT EXISTS avg_fouls_suffered double precision,
ADD COLUMN IF NOT EXISTS avg_progressive_passes double precision,
ADD COLUMN IF NOT EXISTS avg_counterpressing_recoveries double precision,
ADD COLUMN IF NOT EXISTS avg_sliding_tackles double precision,
ADD COLUMN IF NOT EXISTS avg_goal_kicks double precision,
ADD COLUMN IF NOT EXISTS avg_shots_blocked double precision,
ADD COLUMN IF NOT EXISTS avg_shots_on_target double precision,
ADD COLUMN IF NOT EXISTS avg_successful_progressive_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_sliding_tackles double precision,
ADD COLUMN IF NOT EXISTS avg_successful_goal_kicks double precision,
ADD COLUMN IF NOT EXISTS avg_dribbles_against double precision,
ADD COLUMN IF NOT EXISTS avg_dribbles_against_won double precision,
ADD COLUMN IF NOT EXISTS avg_goal_kicks_short double precision,
ADD COLUMN IF NOT EXISTS avg_goal_kicks_long double precision,
ADD COLUMN IF NOT EXISTS avg_field_aerial_duels double precision,
ADD COLUMN IF NOT EXISTS avg_field_aerial_duels_won double precision,
ADD COLUMN IF NOT EXISTS avg_gk_conceded_goals double precision,
ADD COLUMN IF NOT EXISTS avg_gk_shots_against double precision,
ADD COLUMN IF NOT EXISTS avg_gk_exits double precision,
ADD COLUMN IF NOT EXISTS avg_gk_aerial_duels double precision,
ADD COLUMN IF NOT EXISTS avg_gk_saves double precision,
ADD COLUMN IF NOT EXISTS avg_gk_successful_exits double precision,
ADD COLUMN IF NOT EXISTS avg_gk_aerial_duels_won double precision,
ADD COLUMN IF NOT EXISTS avg_new_duels_won double precision,
ADD COLUMN IF NOT EXISTS avg_new_defensive_duels_won double precision,
ADD COLUMN IF NOT EXISTS avg_new_offensive_duels_won double precision,
ADD COLUMN IF NOT EXISTS avg_new_successful_dribbles double precision,
ADD COLUMN IF NOT EXISTS avg_lateral_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_lateral_passes double precision;
-- PERCENT metrics
ALTER TABLE player_advanced_stats
ADD COLUMN IF NOT EXISTS pct_duels_won double precision,
ADD COLUMN IF NOT EXISTS pct_defensive_duels_won double precision,
ADD COLUMN IF NOT EXISTS pct_offensive_duels_won double precision,
ADD COLUMN IF NOT EXISTS pct_aerial_duels_won double precision,
ADD COLUMN IF NOT EXISTS pct_successful_passes double precision,
ADD COLUMN IF NOT EXISTS pct_successful_smart_passes double precision,
ADD COLUMN IF NOT EXISTS pct_successful_passes_to_final_third double precision,
ADD COLUMN IF NOT EXISTS pct_successful_crosses double precision,
ADD COLUMN IF NOT EXISTS pct_successful_dribbles double precision,
ADD COLUMN IF NOT EXISTS pct_shots_on_target double precision,
ADD COLUMN IF NOT EXISTS pct_head_shots_on_target double precision,
ADD COLUMN IF NOT EXISTS pct_goal_conversion double precision,
ADD COLUMN IF NOT EXISTS pct_direct_free_kicks_on_target double precision,
ADD COLUMN IF NOT EXISTS pct_penalties_conversion double precision,
ADD COLUMN IF NOT EXISTS pct_win double precision,
ADD COLUMN IF NOT EXISTS pct_successful_forward_passes double precision,
ADD COLUMN IF NOT EXISTS pct_successful_back_passes double precision,
ADD COLUMN IF NOT EXISTS pct_successful_through_passes double precision,
ADD COLUMN IF NOT EXISTS pct_successful_key_passes double precision,
ADD COLUMN IF NOT EXISTS pct_successful_vertical_passes double precision,
ADD COLUMN IF NOT EXISTS pct_successful_long_passes double precision,
ADD COLUMN IF NOT EXISTS pct_successful_shot_assists double precision,
ADD COLUMN IF NOT EXISTS pct_successful_linkup_plays double precision,
ADD COLUMN IF NOT EXISTS pct_yellow_cards_per_foul double precision,
ADD COLUMN IF NOT EXISTS pct_successful_progressive_passes double precision,
ADD COLUMN IF NOT EXISTS pct_successful_sliding_tackles double precision,
ADD COLUMN IF NOT EXISTS pct_successful_goal_kicks double precision,
ADD COLUMN IF NOT EXISTS pct_dribbles_against_won double precision,
ADD COLUMN IF NOT EXISTS pct_field_aerial_duels_won double precision,
ADD COLUMN IF NOT EXISTS pct_gk_saves double precision,
ADD COLUMN IF NOT EXISTS pct_gk_successful_exits double precision,
ADD COLUMN IF NOT EXISTS pct_gk_aerial_duels_won double precision,
ADD COLUMN IF NOT EXISTS pct_new_duels_won double precision,
ADD COLUMN IF NOT EXISTS pct_new_defensive_duels_won double precision,
ADD COLUMN IF NOT EXISTS pct_new_offensive_duels_won double precision,
ADD COLUMN IF NOT EXISTS pct_new_successful_dribbles double precision,
ADD COLUMN IF NOT EXISTS pct_successful_lateral_passes double precision;
-- Positions table (one row per position code)
CREATE TABLE IF NOT EXISTS player_advanced_positions (
id varchar(16) PRIMARY KEY,
player_wy_id integer NOT NULL,
competition_id integer NOT NULL,
season_id integer NOT NULL,
position_name text,
position_code text NOT NULL,
percent double precision NOT NULL DEFAULT 0,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT uidx_player_advanced_positions UNIQUE (player_wy_id, competition_id, season_id, position_code)
);
CREATE INDEX IF NOT EXISTS idx_player_advanced_positions_player_wy_id ON player_advanced_positions (player_wy_id);
CREATE INDEX IF NOT EXISTS idx_player_advanced_positions_competition_id ON player_advanced_positions (competition_id);
CREATE INDEX IF NOT EXISTS idx_player_advanced_positions_season_id ON player_advanced_positions (season_id);
-- Migration 0016: create import_checkpoints table for resumable imports
CREATE TABLE IF NOT EXISTS import_checkpoints (
key text PRIMARY KEY,
last_player_wy_id integer NOT NULL DEFAULT 0,
last_competition_id integer NOT NULL DEFAULT 0,
last_season_id integer NOT NULL DEFAULT 0,
processed bigint NOT NULL DEFAULT 0,
errors bigint NOT NULL DEFAULT 0,
updated_at timestamp with time zone NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_import_checkpoints_updated_at ON import_checkpoints (updated_at);
-- Migration 0017: Add indexes for player name search optimization
-- This migration adds trigram indexes for fast fuzzy text search on player names
-- Enable pg_trgm extension for trigram-based text search (if not already enabled)
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- Enable unaccent extension for accent-insensitive search (if not already enabled)
CREATE EXTENSION IF NOT EXISTS unaccent;
-- Add GIN trigram indexes for fast ILIKE searches on name columns
-- These indexes dramatically speed up pattern matching queries like ILIKE '%search%'
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_players_short_name_trgm
ON players USING GIN (LOWER(short_name) gin_trgm_ops);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_players_first_name_trgm
ON players USING GIN (LOWER(first_name) gin_trgm_ops);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_players_last_name_trgm
ON players USING GIN (LOWER(last_name) gin_trgm_ops);
-- Add B-tree indexes for exact match and prefix searches (faster for short_name = 'X' or short_name LIKE 'X%')
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_players_short_name_lower
ON players (LOWER(short_name));
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_players_last_name_lower
ON players (LOWER(last_name));
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_players_first_name_lower
ON players (LOWER(first_name));
-- Composite index for active players with market value (common sort criteria)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_players_active_market_value
ON players (is_active, market_value DESC NULLS LAST)
WHERE is_active = true;
-- Index for role filtering (commonly used with name search)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_players_role_name_lower
ON players (LOWER(role_name))
WHERE is_active = true;
-- Teams: trigram indexes for fast ILIKE searches (used by player/coach search joins)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_teams_name_trgm
ON teams USING GIN (LOWER(name) gin_trgm_ops);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_teams_short_name_trgm
ON teams USING GIN (LOWER(short_name) gin_trgm_ops);
-- Coaches: trigram indexes for fast ILIKE searches on coach name fields
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_coaches_first_name_trgm
ON coaches USING GIN (LOWER(first_name) gin_trgm_ops);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_coaches_middle_name_trgm
ON coaches USING GIN (LOWER(middle_name) gin_trgm_ops);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_coaches_last_name_trgm
ON coaches USING GIN (LOWER(last_name) gin_trgm_ops);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_coaches_short_name_trgm
ON coaches USING GIN (LOWER(short_name) gin_trgm_ops);
-- Accent-insensitive trigram indexes (support searches where user input may omit accents)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_players_short_name_unaccent_trgm
ON players USING GIN (unaccent(LOWER(short_name)) gin_trgm_ops);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_players_first_name_unaccent_trgm
ON players USING GIN (unaccent(LOWER(first_name)) gin_trgm_ops);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_players_last_name_unaccent_trgm
ON players USING GIN (unaccent(LOWER(last_name)) gin_trgm_ops);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_teams_name_unaccent_trgm
ON teams USING GIN (unaccent(LOWER(name)) gin_trgm_ops);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_teams_short_name_unaccent_trgm
ON teams USING GIN (unaccent(LOWER(short_name)) gin_trgm_ops);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_coaches_first_name_unaccent_trgm
ON coaches USING GIN (unaccent(LOWER(first_name)) gin_trgm_ops);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_coaches_middle_name_unaccent_trgm
ON coaches USING GIN (unaccent(LOWER(middle_name)) gin_trgm_ops);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_coaches_last_name_unaccent_trgm
ON coaches USING GIN (unaccent(LOWER(last_name)) gin_trgm_ops);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_coaches_short_name_unaccent_trgm
ON coaches USING GIN (unaccent(LOWER(short_name)) gin_trgm_ops);
-- Migration 0018: create team_advanced_stats table for Wyscout team advanced stats
CREATE TABLE IF NOT EXISTS team_advanced_stats (
id varchar(16) PRIMARY KEY,
team_wy_id integer NOT NULL,
competition_id integer NOT NULL,
season_id integer NOT NULL,
round_id integer,
api_last_synced_at timestamp with time zone,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT uidx_team_advanced_stats UNIQUE (team_wy_id, competition_id, season_id)
);
CREATE INDEX IF NOT EXISTS idx_team_advanced_stats_team_wy_id ON team_advanced_stats (team_wy_id);
CREATE INDEX IF NOT EXISTS idx_team_advanced_stats_competition_id ON team_advanced_stats (competition_id);
CREATE INDEX IF NOT EXISTS idx_team_advanced_stats_season_id ON team_advanced_stats (season_id);
-- Migration 0019: expand team_advanced_stats into explicit columns
-- TOTAL metrics
ALTER TABLE team_advanced_stats
ADD COLUMN IF NOT EXISTS total_matches integer,
ADD COLUMN IF NOT EXISTS total_goals integer,
ADD COLUMN IF NOT EXISTS total_assists integer,
ADD COLUMN IF NOT EXISTS total_shots integer,
ADD COLUMN IF NOT EXISTS total_head_shots integer,
ADD COLUMN IF NOT EXISTS total_yellow_cards integer,
ADD COLUMN IF NOT EXISTS total_red_cards integer,
ADD COLUMN IF NOT EXISTS total_direct_red_cards integer,
ADD COLUMN IF NOT EXISTS total_penalties integer,
ADD COLUMN IF NOT EXISTS total_linkup_plays integer,
ADD COLUMN IF NOT EXISTS total_clean_sheets integer,
ADD COLUMN IF NOT EXISTS total_duels integer,
ADD COLUMN IF NOT EXISTS total_duels_won integer,
ADD COLUMN IF NOT EXISTS total_defensive_duels integer,
ADD COLUMN IF NOT EXISTS total_defensive_duels_won integer,
ADD COLUMN IF NOT EXISTS total_offensive_duels integer,
ADD COLUMN IF NOT EXISTS total_offensive_duels_won integer,
ADD COLUMN IF NOT EXISTS total_aerial_duels integer,
ADD COLUMN IF NOT EXISTS total_aerial_duels_won integer,
ADD COLUMN IF NOT EXISTS total_fouls integer,
ADD COLUMN IF NOT EXISTS total_offsides integer,
ADD COLUMN IF NOT EXISTS total_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_passes integer,
ADD COLUMN IF NOT EXISTS total_smart_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_smart_passes integer,
ADD COLUMN IF NOT EXISTS total_passes_to_final_third integer,
ADD COLUMN IF NOT EXISTS total_successful_passes_to_final_third integer,
ADD COLUMN IF NOT EXISTS total_crosses integer,
ADD COLUMN IF NOT EXISTS total_successful_crosses integer,
ADD COLUMN IF NOT EXISTS total_forward_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_forward_passes integer,
ADD COLUMN IF NOT EXISTS total_back_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_back_passes integer,
ADD COLUMN IF NOT EXISTS total_through_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_through_passes integer,
ADD COLUMN IF NOT EXISTS total_key_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_key_passes integer,
ADD COLUMN IF NOT EXISTS total_vertical_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_vertical_passes integer,
ADD COLUMN IF NOT EXISTS total_long_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_long_passes integer,
ADD COLUMN IF NOT EXISTS total_dribbles integer,
ADD COLUMN IF NOT EXISTS total_successful_dribbles integer,
ADD COLUMN IF NOT EXISTS total_interceptions integer,
ADD COLUMN IF NOT EXISTS total_defensive_actions integer,
ADD COLUMN IF NOT EXISTS total_successful_defensive_actions integer,
ADD COLUMN IF NOT EXISTS total_attacking_actions integer,
ADD COLUMN IF NOT EXISTS total_successful_attacking_actions integer,
ADD COLUMN IF NOT EXISTS total_free_kicks integer,
ADD COLUMN IF NOT EXISTS total_free_kicks_on_target integer,
ADD COLUMN IF NOT EXISTS total_direct_free_kicks integer,
ADD COLUMN IF NOT EXISTS total_direct_free_kicks_on_target integer,
ADD COLUMN IF NOT EXISTS total_corners integer,
ADD COLUMN IF NOT EXISTS total_successful_penalties integer,
ADD COLUMN IF NOT EXISTS total_successful_linkup_plays integer,
ADD COLUMN IF NOT EXISTS total_accelerations integer,
ADD COLUMN IF NOT EXISTS total_pressing_duels integer,
ADD COLUMN IF NOT EXISTS total_pressing_duels_won integer,
ADD COLUMN IF NOT EXISTS total_loose_ball_duels integer,
ADD COLUMN IF NOT EXISTS total_loose_ball_duels_won integer,
ADD COLUMN IF NOT EXISTS total_missed_balls integer,
ADD COLUMN IF NOT EXISTS total_shot_assists integer,
ADD COLUMN IF NOT EXISTS total_shot_on_target_assists integer,
ADD COLUMN IF NOT EXISTS total_recoveries integer,
ADD COLUMN IF NOT EXISTS total_opponent_half_recoveries integer,
ADD COLUMN IF NOT EXISTS total_dangerous_opponent_half_recoveries integer,
ADD COLUMN IF NOT EXISTS total_losses integer,
ADD COLUMN IF NOT EXISTS total_own_half_losses integer,
ADD COLUMN IF NOT EXISTS total_dangerous_own_half_losses integer,
ADD COLUMN IF NOT EXISTS total_field_aerial_duels integer,
ADD COLUMN IF NOT EXISTS total_field_aerial_duels_won integer,
ADD COLUMN IF NOT EXISTS total_gk_exits integer,
ADD COLUMN IF NOT EXISTS total_gk_successful_exits integer,
ADD COLUMN IF NOT EXISTS total_gk_aerial_duels integer,
ADD COLUMN IF NOT EXISTS total_gk_aerial_duels_won integer,
ADD COLUMN IF NOT EXISTS total_gk_saves integer,
ADD COLUMN IF NOT EXISTS total_xg_shot double precision,
ADD COLUMN IF NOT EXISTS total_xg_shot_against double precision,
ADD COLUMN IF NOT EXISTS total_ppda double precision,
ADD COLUMN IF NOT EXISTS total_received_pass integer,
ADD COLUMN IF NOT EXISTS total_touch_in_box integer,
ADD COLUMN IF NOT EXISTS total_progressive_run integer,
ADD COLUMN IF NOT EXISTS total_conceded_goals integer,
ADD COLUMN IF NOT EXISTS total_opponent_offsides integer,
ADD COLUMN IF NOT EXISTS total_shots_against integer,
ADD COLUMN IF NOT EXISTS total_gk_goal_kicks integer,
ADD COLUMN IF NOT EXISTS total_gk_goal_kicks_success integer,
ADD COLUMN IF NOT EXISTS total_short_goal_kicks integer,
ADD COLUMN IF NOT EXISTS total_long_goal_kicks integer,
ADD COLUMN IF NOT EXISTS total_matches_tagged integer,
ADD COLUMN IF NOT EXISTS total_new_duels_won integer,
ADD COLUMN IF NOT EXISTS total_new_defensive_duels_won integer,
ADD COLUMN IF NOT EXISTS total_new_offensive_duels_won integer,
ADD COLUMN IF NOT EXISTS total_new_successful_dribbles integer,
ADD COLUMN IF NOT EXISTS total_lateral_passes integer,
ADD COLUMN IF NOT EXISTS total_successful_lateral_passes integer;
-- AVERAGE metrics
ALTER TABLE team_advanced_stats
ADD COLUMN IF NOT EXISTS avg_possession_percent double precision,
ADD COLUMN IF NOT EXISTS avg_duels double precision,
ADD COLUMN IF NOT EXISTS avg_defensive_duels double precision,
ADD COLUMN IF NOT EXISTS avg_offensive_duels double precision,
ADD COLUMN IF NOT EXISTS avg_aerial_duels double precision,
ADD COLUMN IF NOT EXISTS avg_fouls double precision,
ADD COLUMN IF NOT EXISTS avg_goals double precision,
ADD COLUMN IF NOT EXISTS avg_assists double precision,
ADD COLUMN IF NOT EXISTS avg_passes double precision,
ADD COLUMN IF NOT EXISTS avg_smart_passes double precision,
ADD COLUMN IF NOT EXISTS avg_passes_to_final_third double precision,
ADD COLUMN IF NOT EXISTS avg_crosses double precision,
ADD COLUMN IF NOT EXISTS avg_dribbles double precision,
ADD COLUMN IF NOT EXISTS avg_shots double precision,
ADD COLUMN IF NOT EXISTS avg_head_shots double precision,
ADD COLUMN IF NOT EXISTS avg_interceptions double precision,
ADD COLUMN IF NOT EXISTS avg_successful_defensive_action double precision,
ADD COLUMN IF NOT EXISTS avg_yellow_cards double precision,
ADD COLUMN IF NOT EXISTS avg_red_cards double precision,
ADD COLUMN IF NOT EXISTS avg_direct_red_cards double precision,
ADD COLUMN IF NOT EXISTS avg_successful_attacking_actions double precision,
ADD COLUMN IF NOT EXISTS avg_free_kicks double precision,
ADD COLUMN IF NOT EXISTS avg_direct_free_kicks double precision,
ADD COLUMN IF NOT EXISTS avg_corners double precision,
ADD COLUMN IF NOT EXISTS avg_penalties double precision,
ADD COLUMN IF NOT EXISTS avg_pass_length double precision,
ADD COLUMN IF NOT EXISTS avg_long_pass_length double precision,
ADD COLUMN IF NOT EXISTS avg_dribble_distance_from_opponent_goal double precision,
ADD COLUMN IF NOT EXISTS avg_accelerations double precision,
ADD COLUMN IF NOT EXISTS avg_loose_ball_duels double precision,
ADD COLUMN IF NOT EXISTS avg_missed_balls double precision,
ADD COLUMN IF NOT EXISTS avg_forward_passes double precision,
ADD COLUMN IF NOT EXISTS avg_back_passes double precision,
ADD COLUMN IF NOT EXISTS avg_through_passes double precision,
ADD COLUMN IF NOT EXISTS avg_key_passes double precision,
ADD COLUMN IF NOT EXISTS avg_vertical_passes double precision,
ADD COLUMN IF NOT EXISTS avg_long_passes double precision,
ADD COLUMN IF NOT EXISTS avg_shot_assists double precision,
ADD COLUMN IF NOT EXISTS avg_shot_on_target_assists double precision,
ADD COLUMN IF NOT EXISTS avg_linkup_plays double precision,
ADD COLUMN IF NOT EXISTS avg_ball_recoveries double precision,
ADD COLUMN IF NOT EXISTS avg_opponent_half_recoveries double precision,
ADD COLUMN IF NOT EXISTS avg_dangerous_opponent_half_recoveries double precision,
ADD COLUMN IF NOT EXISTS avg_ball_losses double precision,
ADD COLUMN IF NOT EXISTS avg_own_half_losses double precision,
ADD COLUMN IF NOT EXISTS avg_dangerous_own_half_losses double precision,
ADD COLUMN IF NOT EXISTS avg_field_aerial_duels double precision,
ADD COLUMN IF NOT EXISTS avg_conceded_goals double precision,
ADD COLUMN IF NOT EXISTS avg_gk_exits double precision,
ADD COLUMN IF NOT EXISTS avg_gk_aerial_duels double precision,
ADD COLUMN IF NOT EXISTS avg_duels_won double precision,
ADD COLUMN IF NOT EXISTS avg_defensive_duels_won double precision,
ADD COLUMN IF NOT EXISTS avg_offensive_duels_won double precision,
ADD COLUMN IF NOT EXISTS avg_successful_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_smart_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_passes_to_final_third double precision,
ADD COLUMN IF NOT EXISTS avg_successful_crosses double precision,
ADD COLUMN IF NOT EXISTS avg_successful_forward_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_back_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_through_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_key_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_vertical_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_long_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_dribbles double precision,
ADD COLUMN IF NOT EXISTS avg_defensive_actions double precision,
ADD COLUMN IF NOT EXISTS avg_attacking_actions double precision,
ADD COLUMN IF NOT EXISTS avg_free_kicks_on_target double precision,
ADD COLUMN IF NOT EXISTS avg_direct_free_kicks_on_target double precision,
ADD COLUMN IF NOT EXISTS avg_successful_penalties double precision,
ADD COLUMN IF NOT EXISTS avg_successful_linkup_plays double precision,
ADD COLUMN IF NOT EXISTS avg_loose_ball_duels_won double precision,
ADD COLUMN IF NOT EXISTS avg_field_aerial_duels_won double precision,
ADD COLUMN IF NOT EXISTS avg_gk_successful_exits double precision,
ADD COLUMN IF NOT EXISTS avg_gk_aerial_duels_won double precision,
ADD COLUMN IF NOT EXISTS avg_gk_saves double precision,
ADD COLUMN IF NOT EXISTS avg_xg_shot double precision,
ADD COLUMN IF NOT EXISTS avg_xg_shot_against double precision,
ADD COLUMN IF NOT EXISTS avg_ppda double precision,
ADD COLUMN IF NOT EXISTS avg_received_pass double precision,
ADD COLUMN IF NOT EXISTS avg_touch_in_box double precision,
ADD COLUMN IF NOT EXISTS avg_progressive_run double precision,
ADD COLUMN IF NOT EXISTS avg_offsides double precision,
ADD COLUMN IF NOT EXISTS avg_opponent_offsides double precision,
ADD COLUMN IF NOT EXISTS avg_shots_against double precision,
ADD COLUMN IF NOT EXISTS avg_gk_goal_kicks double precision,
ADD COLUMN IF NOT EXISTS avg_gk_goal_kicks_success double precision,
ADD COLUMN IF NOT EXISTS avg_short_goal_kicks double precision,
ADD COLUMN IF NOT EXISTS avg_long_goal_kicks double precision,
ADD COLUMN IF NOT EXISTS avg_new_duels_won double precision,
ADD COLUMN IF NOT EXISTS avg_new_defensive_duels_won double precision,
ADD COLUMN IF NOT EXISTS avg_new_offensive_duels_won double precision,
ADD COLUMN IF NOT EXISTS avg_new_successful_dribbles double precision,
ADD COLUMN IF NOT EXISTS avg_lateral_passes double precision,
ADD COLUMN IF NOT EXISTS avg_successful_lateral_passes double precision;
-- PERCENT metrics
ALTER TABLE team_advanced_stats
ADD COLUMN IF NOT EXISTS pct_duels_won double precision,
ADD COLUMN IF NOT EXISTS pct_defensive_duels_won double precision,
ADD COLUMN IF NOT EXISTS pct_offensive_duels_won double precision,
ADD COLUMN IF NOT EXISTS pct_aerial_duels_won double precision,
ADD COLUMN IF NOT EXISTS pct_successful_passes double precision,
ADD COLUMN IF NOT EXISTS pct_successful_smart_passes double precision,
ADD COLUMN IF NOT EXISTS pct_successful_passes_to_final_third double precision,
ADD COLUMN IF NOT EXISTS pct_successful_crosses double precision,
ADD COLUMN IF NOT EXISTS pct_successful_dribbles double precision,
ADD COLUMN IF NOT EXISTS pct_shots_on_target double precision,
ADD COLUMN IF NOT EXISTS pct_head_shots_on_target double precision,
ADD COLUMN IF NOT EXISTS pct_goal_conversion double precision,
ADD COLUMN IF NOT EXISTS pct_yellow_cards_per_foul double precision,
ADD COLUMN IF NOT EXISTS pct_direct_free_kicks_on_target double precision,
ADD COLUMN IF NOT EXISTS pct_penalties_conversion double precision,
ADD COLUMN IF NOT EXISTS pct_win double precision,
ADD COLUMN IF NOT EXISTS pct_successful_forward_passes double precision,
ADD COLUMN IF NOT EXISTS pct_successful_back_passes double precision,
ADD COLUMN IF NOT EXISTS pct_successful_through_passes double precision,
ADD COLUMN IF NOT EXISTS pct_successful_key_passes double precision,
ADD COLUMN IF NOT EXISTS pct_successful_vertical_passes double precision,
ADD COLUMN IF NOT EXISTS pct_successful_long_passes double precision,
ADD COLUMN IF NOT EXISTS pct_successful_shot_assists double precision,
ADD COLUMN IF NOT EXISTS pct_successful_linkup_plays double precision,
ADD COLUMN IF NOT EXISTS pct_field_aerial_duels_won double precision,
ADD COLUMN IF NOT EXISTS pct_gk_saves double precision,
ADD COLUMN IF NOT EXISTS pct_gk_successful_exits double precision,
ADD COLUMN IF NOT EXISTS pct_gk_aerial_duels_won double precision,
ADD COLUMN IF NOT EXISTS pct_successful_touch_in_box double precision,
ADD COLUMN IF NOT EXISTS pct_new_duels_won double precision,
ADD COLUMN IF NOT EXISTS pct_new_defensive_duels_won double precision,
ADD COLUMN IF NOT EXISTS pct_new_offensive_duels_won double precision,
ADD COLUMN IF NOT EXISTS pct_new_successful_dribbles double precision,
ADD COLUMN IF NOT EXISTS pct_successful_lateral_passes double precision;
-- Migration 0020: extend import_checkpoints to support team advanced stats auto import
ALTER TABLE import_checkpoints
ADD COLUMN IF NOT EXISTS last_team_wy_id integer NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS idx_import_checkpoints_last_team_wy_id ON import_checkpoints (last_team_wy_id);
-- Migration 0021: create match_advanced_stats table
CREATE TABLE IF NOT EXISTS match_advanced_stats (
id text PRIMARY KEY,
match_wy_id integer NOT NULL,
team_wy_id integer NOT NULL,
competition_id integer,
season_id integer,
api_last_synced_at timestamp with time zone,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS uidx_match_advanced_stats ON match_advanced_stats (match_wy_id, team_wy_id);
CREATE INDEX IF NOT EXISTS idx_match_advanced_stats_match_wy_id ON match_advanced_stats (match_wy_id);
CREATE INDEX IF NOT EXISTS idx_match_advanced_stats_team_wy_id ON match_advanced_stats (team_wy_id);
CREATE INDEX IF NOT EXISTS idx_match_advanced_stats_comp_season ON match_advanced_stats (competition_id, season_id);
-- Migration 0022: add explicit columns to match_advanced_stats
ALTER TABLE match_advanced_stats
ADD COLUMN IF NOT EXISTS general_shots integer,
ADD COLUMN IF NOT EXISTS general_fouls integer,
ADD COLUMN IF NOT EXISTS general_corners integer,
ADD COLUMN IF NOT EXISTS general_red_cards integer,
ADD COLUMN IF NOT EXISTS general_yellow_cards integer,
ADD COLUMN IF NOT EXISTS general_offsides integer,
ADD COLUMN IF NOT EXISTS general_dribbles integer,
ADD COLUMN IF NOT EXISTS general_goals integer,
ADD COLUMN IF NOT EXISTS general_xg_per_shot double precision,
ADD COLUMN IF NOT EXISTS general_avg_distance double precision,
ADD COLUMN IF NOT EXISTS general_xg double precision,
ADD COLUMN IF NOT EXISTS general_progressive_runs integer,
ADD COLUMN IF NOT EXISTS general_touches_in_box integer,
ADD COLUMN IF NOT EXISTS general_fouls_suffered integer,
ADD COLUMN IF NOT EXISTS general_shots_on_target integer,
ADD COLUMN IF NOT EXISTS general_shots_blocked integer,
ADD COLUMN IF NOT EXISTS general_shots_outside_box integer,
ADD COLUMN IF NOT EXISTS general_shots_outside_box_on_target integer,
ADD COLUMN IF NOT EXISTS general_shots_on_post integer,
ADD COLUMN IF NOT EXISTS general_shots_wide integer,
ADD COLUMN IF NOT EXISTS general_shots_from_box integer,
ADD COLUMN IF NOT EXISTS general_shots_from_box_on_target integer,
ADD COLUMN IF NOT EXISTS general_free_kicks integer,
ADD COLUMN IF NOT EXISTS general_shots_from_danger_zone integer,
ADD COLUMN IF NOT EXISTS general_total_throw_ins integer,
ADD COLUMN IF NOT EXISTS general_left_throw_ins integer,
ADD COLUMN IF NOT EXISTS general_right_throw_ins integer,
ADD COLUMN IF NOT EXISTS possession_percent integer,
ADD COLUMN IF NOT EXISTS possession_pure_possession_time text,
ADD COLUMN IF NOT EXISTS possession_number integer,
ADD COLUMN IF NOT EXISTS possession_avg_possession_duration text,
ADD COLUMN IF NOT EXISTS possession_reaching_opponent_half integer,
ADD COLUMN IF NOT EXISTS possession_reaching_opponent_box integer,
ADD COLUMN IF NOT EXISTS possession_1_15 integer,
ADD COLUMN IF NOT EXISTS possession_16_30 integer,
ADD COLUMN IF NOT EXISTS possession_31_45 integer,
ADD COLUMN IF NOT EXISTS possession_46_60 integer,
ADD COLUMN IF NOT EXISTS possession_61_75 integer,
ADD COLUMN IF NOT EXISTS possession_76_90 integer,
ADD COLUMN IF NOT EXISTS possession_91_105 integer,
ADD COLUMN IF NOT EXISTS possession_106_120 integer,
ADD COLUMN IF NOT EXISTS possession_minutes_1_15 text,
ADD COLUMN IF NOT EXISTS possession_minutes_16_30 text,
ADD COLUMN IF NOT EXISTS possession_minutes_31_45 text,
ADD COLUMN IF NOT EXISTS possession_minutes_46_60 text,
ADD COLUMN IF NOT EXISTS possession_minutes_61_75 text,
ADD COLUMN IF NOT EXISTS possession_minutes_76_90 text,
ADD COLUMN IF NOT EXISTS possession_minutes_91_105 text,
ADD COLUMN IF NOT EXISTS possession_minutes_106_120 text,
ADD COLUMN IF NOT EXISTS possession_total_time text,
ADD COLUMN IF NOT EXISTS possession_dead_time text,
ADD COLUMN IF NOT EXISTS open_play_total integer,
ADD COLUMN IF NOT EXISTS open_play_short integer,
ADD COLUMN IF NOT EXISTS open_play_medium integer,
ADD COLUMN IF NOT EXISTS open_play_long integer,
ADD COLUMN IF NOT EXISTS open_play_very_long integer,
ADD COLUMN IF NOT EXISTS attacks_total integer,
ADD COLUMN IF NOT EXISTS attacks_with_shots integer,
ADD COLUMN IF NOT EXISTS attacks_positional_attack integer,
ADD COLUMN IF NOT EXISTS attacks_positional_with_shots integer,
ADD COLUMN IF NOT EXISTS attacks_counter_attacks integer,
ADD COLUMN IF NOT EXISTS attacks_free_kicks integer,
ADD COLUMN IF NOT EXISTS attacks_free_kicks_with_shot integer,
ADD COLUMN IF NOT EXISTS attacks_corners integer,
ADD COLUMN IF NOT EXISTS attacks_corners_with_shot integer,
ADD COLUMN IF NOT EXISTS transitions_recoveries_high integer,
ADD COLUMN IF NOT EXISTS transitions_recoveries_medium integer,
ADD COLUMN IF NOT EXISTS transitions_recoveries_low integer,
ADD COLUMN IF NOT EXISTS transitions_recoveries_total integer,
ADD COLUMN IF NOT EXISTS transitions_opponent_half_recoveries integer,
ADD COLUMN IF NOT EXISTS transitions_losses_high integer,
ADD COLUMN IF NOT EXISTS transitions_losses_medium integer,
ADD COLUMN IF NOT EXISTS transitions_losses_low integer,
ADD COLUMN IF NOT EXISTS transitions_losses_total integer,
ADD COLUMN IF NOT EXISTS transitions_own_half_losses integer,
ADD COLUMN IF NOT EXISTS passes_crosses_total integer,
ADD COLUMN IF NOT EXISTS passes_crosses_successful integer,
ADD COLUMN IF NOT EXISTS passes_crosses_blocked integer,
ADD COLUMN IF NOT EXISTS passes_crosses_low integer,
ADD COLUMN IF NOT EXISTS passes_crosses_high integer,
ADD COLUMN IF NOT EXISTS passes_crosses_from_left_flank integer,
ADD COLUMN IF NOT EXISTS passes_crosses_from_right_flank integer,
ADD COLUMN IF NOT EXISTS passes_crosses_from_left_flank_successful integer,
ADD COLUMN IF NOT EXISTS passes_crosses_from_right_flank_successful integer,
ADD COLUMN IF NOT EXISTS passes_passes integer,
ADD COLUMN IF NOT EXISTS passes_passes_successful integer,
ADD COLUMN IF NOT EXISTS passes_forward_passes integer,
ADD COLUMN IF NOT EXISTS passes_forward_passes_successful integer,
ADD COLUMN IF NOT EXISTS passes_back_passes integer,
ADD COLUMN IF NOT EXISTS passes_back_passes_successful integer,
ADD COLUMN IF NOT EXISTS passes_progressive_passes integer,
ADD COLUMN IF NOT EXISTS passes_progressive_passes_successful integer,
ADD COLUMN IF NOT EXISTS passes_long_passes integer,
ADD COLUMN IF NOT EXISTS passes_long_passes_successful integer,
ADD COLUMN IF NOT EXISTS passes_through_passes integer,
ADD COLUMN IF NOT EXISTS passes_through_passes_successful integer,
ADD COLUMN IF NOT EXISTS passes_pass_to_final_thirds integer,
ADD COLUMN IF NOT EXISTS passes_pass_to_final_thirds_successful integer,
ADD COLUMN IF NOT EXISTS passes_pass_to_penalty_areas integer,
ADD COLUMN IF NOT EXISTS passes_pass_to_penalty_areas_successful integer,
ADD COLUMN IF NOT EXISTS passes_vertical_passes integer,
ADD COLUMN IF NOT EXISTS passes_vertical_passes_successful integer,
ADD COLUMN IF NOT EXISTS passes_deep_completed_passes integer,
ADD COLUMN IF NOT EXISTS passes_deep_completed_passes_successful integer,
ADD COLUMN IF NOT EXISTS passes_key_passes integer,
ADD COLUMN IF NOT EXISTS passes_key_passes_successful integer,
ADD COLUMN IF NOT EXISTS passes_assists integer,
ADD COLUMN IF NOT EXISTS passes_smart_passes integer,
ADD COLUMN IF NOT EXISTS passes_smart_passes_successful integer,
ADD COLUMN IF NOT EXISTS passes_shot_assists integer,
ADD COLUMN IF NOT EXISTS passes_short_medium_passes integer,
ADD COLUMN IF NOT EXISTS passes_short_medium_passes_successful integer,
ADD COLUMN IF NOT EXISTS passes_avg_pass_length double precision,
ADD COLUMN IF NOT EXISTS passes_avg_pass_to_final_third_length double precision,
ADD COLUMN IF NOT EXISTS passes_match_tempo double precision,
ADD COLUMN IF NOT EXISTS passes_lateral_passes integer,
ADD COLUMN IF NOT EXISTS passes_lateral_passes_successful integer,
ADD COLUMN IF NOT EXISTS defence_tackles integer,
ADD COLUMN IF NOT EXISTS defence_interceptions integer,
ADD COLUMN IF NOT EXISTS defence_clearances integer,
ADD COLUMN IF NOT EXISTS defence_ppda double precision,
ADD COLUMN IF NOT EXISTS duels_duels integer,
ADD COLUMN IF NOT EXISTS duels_duels_successful integer,
ADD COLUMN IF NOT EXISTS duels_defensive_duels integer,
ADD COLUMN IF NOT EXISTS duels_defensive_duels_successful integer,
ADD COLUMN IF NOT EXISTS duels_offensive_duels integer,
ADD COLUMN IF NOT EXISTS duels_offensive_duels_successful integer,
ADD COLUMN IF NOT EXISTS duels_loose_ball_duels integer,
ADD COLUMN IF NOT EXISTS duels_loose_ball_duels_successful integer,
ADD COLUMN IF NOT EXISTS duels_aerial_duels integer,
ADD COLUMN IF NOT EXISTS duels_aerial_duels_successful integer,
ADD COLUMN IF NOT EXISTS duels_ground_duels integer,
ADD COLUMN IF NOT EXISTS duels_ground_duels_successful integer,
ADD COLUMN IF NOT EXISTS duels_dribbles integer,
ADD COLUMN IF NOT EXISTS duels_dribbles_successful integer,
ADD COLUMN IF NOT EXISTS duels_challenge_intensity double precision,
ADD COLUMN IF NOT EXISTS flanks_left_flank_attacks integer,
ADD COLUMN IF NOT EXISTS flanks_left_flank_xg double precision,
ADD COLUMN IF NOT EXISTS flanks_right_flank_attacks integer,
ADD COLUMN IF NOT EXISTS flanks_right_flank_xg double precision,
ADD COLUMN IF NOT EXISTS flanks_center_attacks integer,
ADD COLUMN IF NOT EXISTS flanks_center_xg double precision;
-- Migration 0023: extend import_checkpoints for match advanced stats auto-import
ALTER TABLE import_checkpoints
ADD COLUMN IF NOT EXISTS last_match_wy_id integer NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS idx_import_checkpoints_last_match_wy_id ON import_checkpoints (last_match_wy_id);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment