Commit 32efaacb by Augusto

import advanced teams and matches, search update

parent d6fd393a
......@@ -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": {
"post": {
"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 = `{
}
}
},
"/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": {
"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.",
......@@ -1888,6 +2030,12 @@ const docTemplate = `{
"in": "query"
},
{
"type": "string",
"description": "Filter players by gender (male/female)",
"name": "gender",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
......@@ -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}": {
"get": {
"description": "Returns a single player by its internal ID.",
......@@ -2646,6 +2890,42 @@ const docTemplate = `{
"description": "Number of items to skip before starting to collect the result set (default 0)",
"name": "offset",
"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": {
......
......@@ -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": {
"post": {
"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 @@
}
}
},
"/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": {
"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.",
......@@ -1881,6 +2023,12 @@
"in": "query"
},
{
"type": "string",
"description": "Filter players by gender (male/female)",
"name": "gender",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
......@@ -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}": {
"get": {
"description": "Returns a single player by its internal ID.",
......@@ -2639,6 +2883,42 @@
"description": "Number of items to skip before starting to collect the result set (default 0)",
"name": "offset",
"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": {
......
......@@ -1162,6 +1162,60 @@ paths:
summary: Import players from TheSports
tags:
- 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:
post:
description: Fetches /v3/players/{playerWyId}/career for players already present
......@@ -1411,6 +1465,53 @@ paths:
summary: Import teams from TheSports
tags:
- 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:
post:
description: Fetches /v3/teams/{wyId} for a specific team (or for teams in DB
......@@ -1707,6 +1808,10 @@ paths:
in: query
name: country
type: string
- description: Filter players by gender (male/female)
in: query
name: gender
type: string
- collectionFormat: csv
description: 'Filter players by role name (supports multiple: role=Defender&role=Midfielder
or role=Defender,Midfielder)'
......@@ -1872,6 +1977,73 @@ paths:
summary: Get player by provider ID
tags:
- 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:
get:
description: Returns a paginated list of referees, optionally filtered by name,
......@@ -2225,6 +2397,30 @@ paths:
in: query
name: offset
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:
"200":
description: OK
......
......@@ -32,17 +32,23 @@ func Connect(cfg config.Config) (*gorm.DB, error) {
&models.Round{},
&models.Team{},
&models.TeamChild{},
&models.TeamCareer{},
&models.TeamAdvancedStats{},
&models.Coach{},
&models.Referee{},
&models.Player{},
&models.Match{},
&models.MatchAdvancedStats{},
&models.MatchTeam{},
&models.MatchLineupPlayer{},
&models.MatchFormation{},
&models.PlayerTransfer{},
&models.PlayerCareer{},
&models.PlayerAdvancedStats{},
&models.PlayerAdvancedPosition{},
&models.TeamSquad{},
&models.Standing{},
&models.ImportCheckpoint{},
&models.SampleRecord{},
); err != nil {
return nil, err
......
package handlers
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
......@@ -17,5 +18,6 @@ func respondError(c *gin.Context, err error) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
log.Printf("internal error: %T: %v", err, err)
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
import (
"fmt"
"net/http"
"reflect"
"strconv"
"strings"
"time"
......@@ -30,12 +31,399 @@ func RegisterPlayerRoutes(rg *gin.RouterGroup, db *gorm.DB) {
players := rg.Group("/players")
players.GET("", h.List)
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("/hudl/:hudlId/transfers", h.ListTransfersByHudlID)
players.GET("/hudl/:hudlId/career", h.ListCareerByHudlID)
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 {
Name string `json:"name"`
Code2 string `json:"code2"`
......@@ -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 teamId query string false "Filter players by current team ID"
// @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)"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]string
......@@ -380,6 +769,7 @@ func (h *PlayerHandler) List(c *gin.Context) {
name := c.Query("name")
teamID := c.Query("teamId")
country := c.Query("country")
gender := c.Query("gender")
rolesQuery := c.QueryArray("role")
if len(rolesQuery) == 0 {
rolesQuery = c.QueryArray("role[]")
......@@ -405,6 +795,8 @@ func (h *PlayerHandler) List(c *gin.Context) {
endpoint = fmt.Sprintf("/players?teamId=%s", teamID)
} else if 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{
......@@ -413,6 +805,7 @@ func (h *PlayerHandler) List(c *gin.Context) {
Name: name,
TeamID: teamID,
Country: country,
Gender: gender,
Roles: roles,
})
if err != nil {
......
......@@ -576,6 +576,12 @@ func (h *TeamHandler) GetImagesByID(c *gin.Context) {
// @Tags Teams
// @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 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
// @Failure 500 {object} map[string]string
// @Router /teams [get]
......@@ -593,13 +599,62 @@ func (h *TeamHandler) List(c *gin.Context) {
}
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"
if 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 {
respondError(c, err)
return
......
......@@ -513,6 +513,169 @@ type PlayerTransfer struct {
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) {
if pt.ID != "" {
return nil
......@@ -548,6 +711,595 @@ type PlayerCareer struct {
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 {
ID string `gorm:"primaryKey;size:16" json:"id"`
TeamWyID int `gorm:"column:team_wy_id;uniqueIndex:uidx_team_careers;index" json:"teamWyId"`
......
......@@ -6,6 +6,7 @@ import (
"strings"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"ScoutingSystemScoreData/internal/errors"
"ScoutingSystemScoreData/internal/models"
......@@ -41,51 +42,48 @@ func (s *coachService) ListCoaches(ctx context.Context, opts ListCoachesOptions)
query := s.db.WithContext(ctx).Model(&models.Coach{})
if opts.Name != "" {
// Normalize the search term for better matching
normalizedSearch := utils.NormalizeText(opts.Name)
searchLower := strings.ToLower(strings.TrimSpace(opts.Name))
likePattern := "%" + searchLower + "%"
searchTokens := utils.TokenizeSearchTerm(opts.Name)
// Build flexible search conditions
likePattern := "%" + normalizedSearch + "%"
// Create search conditions for normalized text
searchConditions := s.db.Where(
"LOWER(REGEXP_REPLACE(coaches.short_name, '[^a-zA-Z0-9 ]', '', 'g')) ILIKE ? OR "+
"LOWER(REGEXP_REPLACE(coaches.first_name, '[^a-zA-Z0-9 ]', '', 'g')) ILIKE ? OR "+
"LOWER(REGEXP_REPLACE(coaches.middle_name, '[^a-zA-Z0-9 ]', '', 'g')) ILIKE ? OR "+
"LOWER(REGEXP_REPLACE(coaches.last_name, '[^a-zA-Z0-9 ]', '', 'g')) 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(?)",
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 {
tokenFilter := s.db
for _, token := range searchTokens {
tokenPattern := "%" + token + "%"
query = query.Where(
"LOWER(coaches.short_name) ILIKE ? OR LOWER(coaches.first_name) ILIKE ? OR LOWER(coaches.middle_name) ILIKE ? OR LOWER(coaches.last_name) ILIKE ?",
tokenFilter = tokenFilter.Where(
"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,
)
}
searchConditions = searchConditions.Or(tokenFilter)
}
query = query.Where(searchConditions)
// 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 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
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")
......
......@@ -29,6 +29,7 @@ type ListPlayersOptions struct {
Name string
TeamID string
Country string
Gender string
Roles []string
}
......@@ -44,6 +45,10 @@ func (s *playerService) ListPlayers(ctx context.Context, opts ListPlayersOptions
var players []models.Player
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 {
roles := make([]string, 0, len(opts.Roles))
seen := make(map[string]struct{}, len(opts.Roles))
......@@ -64,87 +69,66 @@ func (s *playerService) ListPlayers(ctx context.Context, opts ListPlayersOptions
}
if opts.Name != "" {
// Normalize the search term for better matching
normalizedSearch := utils.NormalizeText(opts.Name)
// Normalize search term to lowercase for index-friendly queries
searchLower := strings.ToLower(strings.TrimSpace(opts.Name))
likePattern := "%" + searchLower + "%"
searchTokens := utils.TokenizeSearchTerm(opts.Name)
// Build flexible search conditions
likePattern := "%" + normalizedSearch + "%"
// Join with teams table to enable team name search
baseQuery = baseQuery.Joins("LEFT JOIN teams ON teams.wy_id = players.current_team_id")
// Create search conditions for normalized text
// Using LOWER() and unaccent-like matching through normalized comparison
// Build search conditions using LOWER() which can use GIN trigram indexes
// Search in: player names AND team names
searchConditions := s.db.Where(
"LOWER(REGEXP_REPLACE(players.short_name, '[^a-zA-Z0-9 ]', '', 'g')) ILIKE ? OR "+
"LOWER(REGEXP_REPLACE(players.first_name, '[^a-zA-Z0-9 ]', '', 'g')) ILIKE ? OR "+
"LOWER(REGEXP_REPLACE(players.middle_name, '[^a-zA-Z0-9 ]', '', 'g')) ILIKE ? OR "+
"LOWER(REGEXP_REPLACE(players.last_name, '[^a-zA-Z0-9 ]', '', 'g')) ILIKE ?",
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,
"unaccent(LOWER(players.short_name)) ILIKE unaccent(?) OR "+
"unaccent(LOWER(players.first_name)) ILIKE unaccent(?) OR "+
"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(?)",
likePattern, likePattern, likePattern, likePattern, likePattern, likePattern,
)
// Build multi-token filter as:
// (phrase-like match across fields) OR (ALL tokens match somewhere across fields)
nameFilter := s.db.Where(searchConditions)
// For multi-word searches (e.g., "ronaldo alnassr", "samu porto"):
// Match if tokens appear across player name + team name fields
if len(searchTokens) > 1 {
// Strategy: each token must match somewhere (player name OR team name)
tokenFilter := s.db
for _, token := range searchTokens {
tokenPattern := "%" + token + "%"
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 ?",
tokenPattern, tokenPattern, tokenPattern, tokenPattern,
"unaccent(LOWER(players.short_name)) ILIKE unaccent(?) OR unaccent(LOWER(players.first_name)) ILIKE unaccent(?) OR "+
"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.
// This prevents a "big competition" result from outranking an exact name hit.
// Optimized ranking: prioritize ShortName matches first
// Using simple LOWER() comparisons that can leverage indexes
fetchQuery := baseQuery
fetchQuery = fetchQuery.Order(clause.Expr{SQL: "CASE " +
"WHEN LOWER(REGEXP_REPLACE(players.short_name, '[^a-zA-Z0-9 ]', '', 'g')) = ? 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 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 LOWER(REGEXP_REPLACE(players.last_name, '[^a-zA-Z0-9 ]', '', 'g')) = ? THEN 3 " +
"WHEN LOWER(REGEXP_REPLACE(players.first_name, '[^a-zA-Z0-9 ]', '', 'g')) = ? THEN 4 " +
"ELSE 5 END",
Vars: []interface{}{normalizedSearch, normalizedSearch, normalizedSearch, normalizedSearch, normalizedSearch},
"WHEN unaccent(LOWER(players.short_name)) = unaccent(?) THEN 0 " +
"WHEN unaccent(LOWER(players.short_name)) ILIKE unaccent(?) THEN 1 " +
"WHEN unaccent(LOWER(players.last_name)) = unaccent(?) THEN 2 " +
"WHEN unaccent(LOWER(players.first_name)) = unaccent(?) THEN 3 " +
"WHEN unaccent(LOWER(players.last_name)) ILIKE unaccent(?) THEN 4 " +
"WHEN unaccent(LOWER(players.first_name)) ILIKE unaccent(?) THEN 5 " +
"ELSE 6 END",
Vars: []interface{}{searchLower, likePattern, searchLower, searchLower, likePattern, likePattern},
})
// Prioritization logic:
// 1. Players with current team (especially from big competitions)
// 2. Players with market value (indicates active/professional status)
// 3. Players with national team
// 4. Alphabetical by name
// Secondary sort: prioritize players with market value (professional/active players)
fetchQuery = fetchQuery.Order("CASE WHEN players.market_value IS NOT NULL AND players.market_value > 0 THEN 0 ELSE 1 END")
fetchQuery = fetchQuery.Order("players.market_value DESC NULLS LAST")
// Join with teams to check for big competitions
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
// Tertiary sort: players with current team
fetchQuery = fetchQuery.Order("CASE WHEN players.current_team_id IS NOT NULL THEN 0 ELSE 1 END")
// Priority 3: Market value (higher is better)
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
// Final sort: alphabetical
fetchQuery = fetchQuery.Order("players.last_name ASC")
fetchQuery = fetchQuery.Order("players.first_name ASC")
......
......@@ -3,6 +3,7 @@ package services
import (
"context"
"strconv"
"strings"
"gorm.io/gorm"
......@@ -11,7 +12,7 @@ import (
)
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)
GetByWyID(ctx context.Context, wyID int) (models.Team, error)
GetByAnyProviderID(ctx context.Context, providerID string) (models.Team, error)
......@@ -28,13 +29,20 @@ func NewTeamService(db *gorm.DB) TeamService {
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
query := s.db.WithContext(ctx).Model(&models.Team{})
if 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("market_value 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