code.vegaprotocol.io/vega@v0.79.0/datanode/sqlstore/game_scores.go (about) 1 // Copyright (C) 2023 Gobalsky Labs Limited 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 package sqlstore 17 18 import ( 19 "context" 20 "encoding/hex" 21 "fmt" 22 "strings" 23 "time" 24 25 "code.vegaprotocol.io/vega/datanode/entities" 26 "code.vegaprotocol.io/vega/datanode/metrics" 27 "code.vegaprotocol.io/vega/libs/ptr" 28 v2 "code.vegaprotocol.io/vega/protos/data-node/api/v2" 29 30 "github.com/georgysavva/scany/pgxscan" 31 "github.com/shopspring/decimal" 32 ) 33 34 type GameScores struct { 35 *ConnectionSource 36 } 37 38 var gamesTeamOrderding = TableOrdering{ 39 ColumnOrdering{Name: "game_id", Sorting: ASC}, 40 ColumnOrdering{Name: "epoch_id", Sorting: DESC}, 41 ColumnOrdering{Name: "team_id", Sorting: ASC}, 42 } 43 44 var gamesPartyOrderding = TableOrdering{ 45 ColumnOrdering{Name: "game_id", Sorting: ASC}, 46 ColumnOrdering{Name: "epoch_id", Sorting: DESC}, 47 ColumnOrdering{Name: "party_id", Sorting: ASC}, 48 } 49 50 func NewGameScores(connectionSource *ConnectionSource) *GameScores { 51 r := &GameScores{ 52 ConnectionSource: connectionSource, 53 } 54 return r 55 } 56 57 func (gs *GameScores) AddPartyScore(ctx context.Context, r entities.GamePartyScore) error { 58 defer metrics.StartSQLQuery("GameScores", "AddPartyScores")() 59 _, err := gs.Exec(ctx, 60 `INSERT INTO game_party_scores( 61 game_id, 62 team_id, 63 epoch_id, 64 party_id, 65 score, 66 staking_balance, 67 open_volume, 68 total_fees_paid, 69 is_eligible, 70 rank, 71 vega_time 72 ) 73 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11);`, 74 r.GameID, r.TeamID, r.EpochID, r.PartyID, r.Score, r.StakingBalance, r.OpenVolume, r.TotalFeesPaid, r.IsEligible, 75 r.Rank, r.VegaTime) 76 return err 77 } 78 79 func (gs *GameScores) AddTeamScore(ctx context.Context, r entities.GameTeamScore) error { 80 defer metrics.StartSQLQuery("GameScores", "AddPartyScores")() 81 _, err := gs.Exec(ctx, 82 `INSERT INTO game_team_scores( 83 game_id, 84 team_id, 85 epoch_id, 86 score, 87 vega_time 88 ) 89 VALUES ($1, $2, $3, $4, $5);`, 90 r.GameID, r.TeamID, r.EpochID, r.Score, r.VegaTime) 91 return err 92 } 93 94 // scany does not like deserializing byte arrays to strings so if an ID 95 // needs to be nillable, we need to scan it into a temporary struct that will 96 // define the ID field as a byte array and then parse the value accordingly. 97 type scannedPartyGameScore struct { 98 GameID entities.GameID 99 TeamID []byte 100 EpochID int64 101 PartyID entities.PartyID 102 Score decimal.Decimal 103 StakingBalance decimal.Decimal 104 OpenVolume decimal.Decimal 105 TotalFeesPaid decimal.Decimal 106 IsEligible bool 107 Rank *uint64 108 VegaTime time.Time 109 TxHash entities.TxHash 110 SeqNum uint64 111 } 112 113 func (gs *GameScores) ListPartyScores( 114 ctx context.Context, 115 gameIDs []entities.GameID, 116 partyIDs []entities.PartyID, 117 teamIDs []entities.TeamID, 118 epochFromID *uint64, 119 epochToID *uint64, 120 pagination entities.CursorPagination, 121 ) ([]entities.GamePartyScore, entities.PageInfo, error) { 122 var pageInfo entities.PageInfo 123 where, args, err := filterPartyQuery(gameIDs, partyIDs, teamIDs, epochFromID, epochToID) 124 if err != nil { 125 return nil, pageInfo, err 126 } 127 128 query := `SELECT t1.* FROM game_party_scores_current t1` 129 if epochFromID != nil || epochToID != nil { 130 var epochWhere string 131 if epochFromID != nil && epochToID == nil { 132 epochWhere = fmt.Sprintf("epoch_id >= %d", *epochFromID) 133 } else if epochFromID == nil && epochToID != nil { 134 epochWhere = fmt.Sprintf("epoch_id <= %d", *epochToID) 135 } else { 136 epochWhere = fmt.Sprintf("epoch_id >= %d and epoch_id <= %d", *epochFromID, *epochToID) 137 } 138 query = `SELECT t1.* FROM game_party_scores t1 139 JOIN ( 140 SELECT 141 party_id, 142 epoch_id, 143 MAX(vega_time) AS latest_time 144 FROM 145 game_party_scores 146 WHERE ` + epochWhere + ` 147 GROUP BY 148 party_id, 149 epoch_id 150 ) t2 ON t1.party_id = t2.party_id AND t1.epoch_id = t2.epoch_id AND t1.vega_time = t2.latest_time 151 ` 152 } 153 query = fmt.Sprintf("%s %s", query, where) 154 query, args, err = PaginateQuery[entities.PartyGameScoreCursor](query, args, gamesPartyOrderding, pagination) 155 if err != nil { 156 return nil, pageInfo, err 157 } 158 159 sPgs := []scannedPartyGameScore{} 160 defer metrics.StartSQLQuery("GameScores", "ListPartyScores")() 161 162 if err = pgxscan.Select(ctx, gs.ConnectionSource, &sPgs, query, args...); err != nil { 163 return nil, pageInfo, fmt.Errorf("querying game party scores: %w", err) 164 } 165 166 pgs := parseScannedPartyGameScores(sPgs) 167 ret, pageInfo := entities.PageEntities[*v2.GamePartyScoresEdge](pgs, pagination) 168 return ret, pageInfo, nil 169 } 170 171 func filterPartyQuery(gameIDs []entities.GameID, partyIDs []entities.PartyID, teamIDs []entities.TeamID, epochFromID, epochToID *uint64) (string, []any, error) { 172 var ( 173 args []any 174 conditions []string 175 ) 176 177 if len(gameIDs) > 0 { 178 gids := make([][]byte, len(gameIDs)) 179 for i, gid := range gameIDs { 180 bytes, err := gid.Bytes() 181 if err != nil { 182 return "", nil, fmt.Errorf("could not decode game ID: %w", err) 183 } 184 gids[i] = bytes 185 } 186 conditions = append(conditions, fmt.Sprintf("t1.game_id = ANY(%s)", nextBindVar(&args, gids))) 187 } 188 189 if epochFromID != nil { 190 conditions = append(conditions, fmt.Sprintf("t1.epoch_id >= %s", nextBindVar(&args, epochFromID))) 191 } 192 193 if epochToID != nil { 194 conditions = append(conditions, fmt.Sprintf("t1.epoch_id <= %s", nextBindVar(&args, epochToID))) 195 } 196 197 if len(partyIDs) > 0 { 198 pids := make([][]byte, len(partyIDs)) 199 for i, pid := range partyIDs { 200 bytes, err := pid.Bytes() 201 if err != nil { 202 return "", nil, fmt.Errorf("could not decode party ID: %w", err) 203 } 204 pids[i] = bytes 205 } 206 conditions = append(conditions, fmt.Sprintf("t1.party_id = ANY(%s)", nextBindVar(&args, pids))) 207 } 208 209 if len(teamIDs) > 0 { 210 tids := make([][]byte, len(teamIDs)) 211 for i, tid := range teamIDs { 212 bytes, err := tid.Bytes() 213 if err != nil { 214 return "", nil, fmt.Errorf("could not decode team ID: %w", err) 215 } 216 tids[i] = bytes 217 } 218 conditions = append(conditions, fmt.Sprintf("t1.team_id = ANY(%s)", nextBindVar(&args, tids))) 219 } 220 221 whereClause := strings.Join(conditions, " AND ") 222 if len(whereClause) > 0 { 223 return " WHERE " + whereClause, args, nil 224 } 225 return "", args, nil 226 } 227 228 func filterTeamQuery(gameIDs []entities.GameID, teamIDs []entities.TeamID, epochFromID, epochToID *uint64) (string, []any, error) { 229 var ( 230 args []any 231 conditions []string 232 ) 233 234 if epochFromID != nil { 235 conditions = append(conditions, fmt.Sprintf("t1.epoch_id >= %s", nextBindVar(&args, epochFromID))) 236 } 237 238 if epochToID != nil { 239 conditions = append(conditions, fmt.Sprintf("t1.epoch_id <= %s", nextBindVar(&args, epochToID))) 240 } 241 242 if len(gameIDs) > 0 { 243 gids := make([][]byte, len(gameIDs)) 244 for i, gid := range gameIDs { 245 bytes, err := gid.Bytes() 246 if err != nil { 247 return "", nil, fmt.Errorf("could not decode game ID: %w", err) 248 } 249 gids[i] = bytes 250 } 251 conditions = append(conditions, fmt.Sprintf("t1.game_id = ANY(%s)", nextBindVar(&args, gids))) 252 } 253 if len(teamIDs) > 0 { 254 tids := make([][]byte, len(teamIDs)) 255 for i, tid := range teamIDs { 256 bytes, err := tid.Bytes() 257 if err != nil { 258 return "", nil, fmt.Errorf("could not decode team ID: %w", err) 259 } 260 tids[i] = bytes 261 } 262 conditions = append(conditions, fmt.Sprintf("t1.team_id = ANY(%s)", nextBindVar(&args, tids))) 263 } 264 if len(conditions) > 0 { 265 return " WHERE " + strings.Join(conditions, " AND "), args, nil 266 } 267 return "", args, nil 268 } 269 270 func (gs *GameScores) ListTeamScores( 271 ctx context.Context, 272 gameIDs []entities.GameID, 273 teamIDs []entities.TeamID, 274 epochFromID *uint64, 275 epochToID *uint64, 276 pagination entities.CursorPagination, 277 ) ([]entities.GameTeamScore, entities.PageInfo, error) { 278 var pageInfo entities.PageInfo 279 where, args, err := filterTeamQuery(gameIDs, teamIDs, epochFromID, epochToID) 280 if err != nil { 281 return nil, pageInfo, err 282 } 283 284 query := `select t1.* from game_team_scores_current t1` 285 if epochFromID != nil || epochToID != nil { 286 var epochWhere string 287 if epochFromID != nil && epochToID == nil { 288 epochWhere = fmt.Sprintf("epoch_id >= %d", *epochFromID) 289 } else if epochFromID == nil && epochToID != nil { 290 epochWhere = fmt.Sprintf("epoch_id <= %d", *epochToID) 291 } else { 292 epochWhere = fmt.Sprintf("epoch_id >= %d and epoch_id <= %d", *epochFromID, *epochToID) 293 } 294 query = `SELECT t1.* FROM game_team_scores t1 295 JOIN ( 296 SELECT 297 team_id, 298 epoch_id, 299 MAX(vega_time) AS latest_time 300 FROM 301 game_team_scores 302 WHERE ` + epochWhere + ` 303 GROUP BY 304 team_id, 305 epoch_id 306 ) t2 ON t1.team_id = t2.team_id AND t1.epoch_id = t2.epoch_id AND t1.vega_time = t2.latest_time 307 ` 308 } 309 query = fmt.Sprintf("%s %s", query, where) 310 query, args, err = PaginateQuery[entities.TeamGameScoreCursor](query, args, gamesTeamOrderding, pagination) 311 if err != nil { 312 return nil, pageInfo, err 313 } 314 315 tgs := []entities.GameTeamScore{} 316 defer metrics.StartSQLQuery("GameScores", "ListTeamScores")() 317 318 if err = pgxscan.Select(ctx, gs.ConnectionSource, &tgs, query, args...); err != nil { 319 return nil, pageInfo, fmt.Errorf("querying game team scores: %w", err) 320 } 321 322 ret, pageInfo := entities.PageEntities[*v2.GameTeamScoresEdge](tgs, pagination) 323 return ret, pageInfo, nil 324 } 325 326 func parseScannedPartyGameScores(scanned []scannedPartyGameScore) []entities.GamePartyScore { 327 pgs := make([]entities.GamePartyScore, 0, len(scanned)) 328 for _, s := range scanned { 329 var teamID *entities.TeamID 330 if s.TeamID != nil { 331 id := hex.EncodeToString(s.TeamID) 332 if id != "" { 333 teamID = ptr.From(entities.TeamID(id)) 334 } 335 } 336 337 pgs = append(pgs, entities.GamePartyScore{ 338 GameID: s.GameID, 339 TeamID: teamID, 340 EpochID: s.EpochID, 341 PartyID: s.PartyID, 342 Score: s.Score, 343 StakingBalance: s.StakingBalance, 344 OpenVolume: s.OpenVolume, 345 TotalFeesPaid: s.TotalFeesPaid, 346 IsEligible: s.IsEligible, 347 Rank: s.Rank, 348 VegaTime: s.VegaTime, 349 }) 350 } 351 return pgs 352 }