code.vegaprotocol.io/vega@v0.79.0/datanode/sqlstore/teams.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 "fmt" 21 "strings" 22 23 "code.vegaprotocol.io/vega/datanode/entities" 24 "code.vegaprotocol.io/vega/datanode/metrics" 25 "code.vegaprotocol.io/vega/libs/num" 26 v2 "code.vegaprotocol.io/vega/protos/data-node/api/v2" 27 eventspb "code.vegaprotocol.io/vega/protos/vega/events/v1" 28 29 "github.com/georgysavva/scany/pgxscan" 30 ) 31 32 const ( 33 listTeamsStatsQuery = `WITH 34 -- This CTE retrieves the all teams statistics reported for the last N epochs. 35 eligible_stats AS ( 36 SELECT * 37 FROM teams_stats 38 WHERE at_epoch > ( 39 SELECT MAX(id) - $1 40 FROM epochs 41 ) %s 42 ), 43 team_numbers AS ( 44 SELECT t.team_id, 45 SUM(total_quantum_reward) AS total_quantum_rewards, 46 JSONB_AGG(JSONB_BUILD_OBJECT('epoch', at_epoch, 'total', total_quantum_reward) ORDER BY at_epoch, total_quantum_reward) AS quantum_rewards, 47 SUM(total_quantum_volume) AS total_quantum_volumes, 48 JSONB_AGG(JSONB_BUILD_OBJECT('epoch', at_epoch, 'total', total_quantum_volume) ORDER BY at_epoch, total_quantum_volume) AS quantum_volumes 49 FROM eligible_stats t 50 GROUP BY 51 t.team_id 52 ), 53 team_games AS ( 54 SELECT team_id, 55 COALESCE(ARRAY_LENGTH(ARRAY_REMOVE(ARRAY_AGG(DISTINCT game_played), NULL), 1), 0) AS count, 56 COALESCE(JSONB_AGG(DISTINCT game_played::BYTEA ORDER BY game_played::BYTEA) 57 FILTER (WHERE game_played <> 'null' ), '[]'::JSONB) AS LIST 58 FROM eligible_stats stats 59 LEFT JOIN LATERAL JSONB_OBJECT_KEYS(stats.games_played) AS game_played ON TRUE 60 GROUP BY 61 team_id 62 ) 63 SELECT tn.team_id AS team_id, 64 tn.total_quantum_rewards AS total_quantum_rewards, 65 tn.quantum_rewards AS quantum_rewards, 66 tn.total_quantum_volumes AS total_quantum_volumes, 67 tn.quantum_volumes AS quantum_volumes, 68 mg.list AS games_played, 69 mg.count AS total_games_played 70 FROM team_numbers tn 71 LEFT JOIN team_games mg ON tn.team_id = mg.team_id 72 ` 73 74 listTeamMembersStatsQuery = `WITH 75 -- This CTE retrieves the all teams statistics reported for the last N epochs. 76 eligible_stats AS ( 77 SELECT * 78 FROM teams_stats 79 WHERE at_epoch > ( 80 SELECT MAX(id) - $1 81 FROM epochs 82 ) AND team_id = $2 %s 83 ), 84 members_numbers AS ( 85 SELECT team_id, 86 party_id, 87 SUM(total_quantum_reward) AS total_quantum_rewards, 88 JSONB_AGG(JSONB_BUILD_OBJECT('epoch', at_epoch, 'total', total_quantum_reward) ORDER BY at_epoch, total_quantum_reward) AS quantum_rewards, 89 SUM(total_quantum_volume) AS total_quantum_volumes, 90 JSONB_AGG(JSONB_BUILD_OBJECT('epoch', at_epoch, 'total', total_quantum_volume) ORDER BY at_epoch, total_quantum_volume) AS quantum_volumes 91 FROM eligible_stats 92 GROUP BY 93 team_id, 94 party_id 95 ), 96 members_games AS ( 97 SELECT team_id, 98 party_id, 99 COALESCE(ARRAY_LENGTH(ARRAY_REMOVE(ARRAY_AGG(DISTINCT game_played), NULL), 1), 0) AS count, 100 COALESCE(JSONB_AGG(DISTINCT game_played::BYTEA ORDER BY game_played::BYTEA) 101 FILTER ( WHERE game_played <> 'null' ), '[]'::JSONB) AS list 102 FROM eligible_stats stats 103 LEFT JOIN LATERAL JSONB_OBJECT_KEYS(stats.games_played) AS game_played ON TRUE 104 GROUP BY 105 team_id, 106 party_id 107 ) 108 SELECT mn.party_id AS party_id, 109 mn.total_quantum_rewards AS total_quantum_rewards, 110 mn.quantum_rewards AS quantum_rewards, 111 mn.total_quantum_volumes AS total_quantum_volumes, 112 mn.quantum_volumes AS quantum_volumes, 113 mg.list AS games_played, 114 mg.count AS total_games_played 115 FROM members_numbers mn 116 LEFT JOIN members_games mg ON mn.team_id = mg.team_id AND mn.party_id = mg.party_id` 117 118 upsertTeamsStats = `INSERT INTO teams_stats(team_id, party_id, at_epoch, total_quantum_volume, total_quantum_reward, games_played) 119 VALUES 120 %s 121 ON CONFLICT (team_id, party_id, at_epoch) DO UPDATE 122 SET total_quantum_volume = excluded.total_quantum_volume 123 ` 124 ) 125 126 type ( 127 Teams struct { 128 *ConnectionSource 129 } 130 131 ListTeamsStatisticsFilters struct { 132 TeamID *entities.TeamID 133 AggregationEpochs uint64 134 } 135 136 ListTeamMembersStatisticsFilters struct { 137 TeamID entities.TeamID 138 PartyID *entities.PartyID 139 AggregationEpochs uint64 140 } 141 ) 142 143 var ( 144 teamsOrdering = TableOrdering{ 145 ColumnOrdering{Name: "created_at", Sorting: ASC}, 146 } 147 teamsStatsOrdering = TableOrdering{ 148 ColumnOrdering{Name: "team_id", Sorting: ASC}, 149 } 150 teamMembersStatsOrdering = TableOrdering{ 151 ColumnOrdering{Name: "party_id", Sorting: ASC}, 152 } 153 refereesOrdering = TableOrdering{ 154 ColumnOrdering{Name: "party_id", Sorting: ASC}, 155 } 156 refereeHistoryOrdering = TableOrdering{ 157 ColumnOrdering{Name: "joined_at_epoch", Sorting: ASC}, 158 } 159 ) 160 161 func NewTeams(connectionSource *ConnectionSource) *Teams { 162 return &Teams{ 163 ConnectionSource: connectionSource, 164 } 165 } 166 167 func (t *Teams) AddTeam(ctx context.Context, team *entities.Team) error { 168 defer metrics.StartSQLQuery("Teams", "AddTeam")() 169 170 if team.AllowList == nil { 171 team.AllowList = []string{} 172 } 173 174 if _, err := t.Exec( 175 ctx, 176 "INSERT INTO teams(id, referrer, name, team_url, avatar_url, closed, allow_list, created_at, created_at_epoch, vega_time) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", 177 team.ID, 178 team.Referrer, 179 team.Name, 180 team.TeamURL, 181 team.AvatarURL, 182 team.Closed, 183 team.AllowList, 184 team.CreatedAt, 185 team.CreatedAtEpoch, 186 team.VegaTime, 187 ); err != nil { 188 return err 189 } 190 191 // in case the party already was in a team? 192 _, _ = t.Exec( 193 ctx, 194 "DELETE FROM team_members WHERE party_id = $1", 195 team.Referrer, 196 ) 197 198 if _, err := t.Exec( 199 ctx, 200 "INSERT INTO team_members(team_id, party_id, joined_at_epoch, joined_at, vega_time) VALUES ($1, $2, $3, $4, $5)", 201 team.ID, 202 team.Referrer, 203 team.CreatedAtEpoch, 204 team.CreatedAt, 205 team.VegaTime, 206 ); err != nil { 207 return err 208 } 209 210 return nil 211 } 212 213 func (t *Teams) UpdateTeam(ctx context.Context, team *entities.TeamUpdated) error { 214 defer metrics.StartSQLQuery("Teams", "UpdateTeam")() 215 216 if team.AllowList == nil { 217 team.AllowList = []string{} 218 } 219 220 ct, err := t.Exec(ctx, 221 `UPDATE teams 222 SET name = $1, 223 team_url = $2, 224 avatar_url = $3, 225 closed = $4, 226 allow_list = $5 227 WHERE id = $6`, 228 team.Name, 229 team.TeamURL, 230 team.AvatarURL, 231 team.Closed, 232 team.AllowList, 233 team.ID, 234 ) 235 236 if ct.RowsAffected() == 0 { 237 return fmt.Errorf("could not update team with id %s", team.ID) 238 } 239 return err 240 } 241 242 func (t *Teams) RefereeJoinedTeam(ctx context.Context, referee *entities.TeamMember) error { 243 defer metrics.StartSQLQuery("Teams", "RefereeJoinedTeam")() 244 _, err := t.Exec(ctx, 245 `INSERT INTO team_members(team_id, party_id, joined_at, joined_at_epoch, vega_time) VALUES ($1, $2, $3, $4, $5)`, 246 referee.TeamID, 247 referee.PartyID, 248 referee.JoinedAt, 249 referee.JoinedAtEpoch, 250 referee.VegaTime, 251 ) 252 253 return err 254 } 255 256 func (t *Teams) RefereeSwitchedTeam(ctx context.Context, referee *entities.RefereeTeamSwitch) error { 257 defer metrics.StartSQLQuery("Teams", "RefereeSwitchedTeam")() 258 259 // in case the party was removed from the team owner from a team 260 if len(referee.ToTeamID) <= 0 { 261 _, err := t.Exec( 262 ctx, 263 "DELETE FROM team_members WHERE party_id = $1", 264 referee.PartyID, 265 ) 266 267 return err 268 } 269 270 // normal path, team_members just being updated. 271 _, err := t.Exec(ctx, 272 `INSERT INTO team_members(team_id, party_id, joined_at, joined_at_epoch, vega_time) VALUES ($1, $2, $3, $4, $5)`, 273 referee.ToTeamID, 274 referee.PartyID, 275 referee.SwitchedAt, 276 referee.SwitchedAtEpoch, 277 referee.VegaTime, 278 ) 279 280 return err 281 } 282 283 func (t *Teams) TeamsStatsUpdated(ctx context.Context, evt *eventspb.TeamsStatsUpdated) error { 284 defer metrics.StartSQLQuery("Teams", "TeamsStatsUpdated")() 285 286 var args []interface{} 287 288 values := []string{} 289 for _, teamStats := range evt.Stats { 290 for _, memberStats := range teamStats.MembersStats { 291 notionalVolume, hasErr := num.UintFromString(memberStats.NotionalVolume, 10) 292 if hasErr { 293 notionalVolume = num.UintZero() 294 } 295 296 values = append(values, fmt.Sprintf("(%s, %s, %s, %s, 0, '{}'::JSONB)", 297 nextBindVar(&args, entities.TeamID(teamStats.TeamId)), 298 nextBindVar(&args, entities.PartyID(memberStats.PartyId)), 299 nextBindVar(&args, evt.AtEpoch), 300 nextBindVar(&args, notionalVolume)), 301 ) 302 } 303 } 304 305 if len(values) == 0 { 306 return nil 307 } 308 309 query := fmt.Sprintf(upsertTeamsStats, strings.Join(values, ",")) 310 _, err := t.Exec(ctx, query, args...) 311 if err != nil { 312 return fmt.Errorf("could not insert team stats update: %w", err) 313 } 314 315 return nil 316 } 317 318 func (t *Teams) GetTeam(ctx context.Context, teamID entities.TeamID, partyID entities.PartyID) (*entities.Team, error) { 319 defer metrics.StartSQLQuery("Teams", "GetTeam")() 320 321 var team entities.Team 322 323 if teamID == "" && partyID == "" { 324 return nil, fmt.Errorf("either teamID or partyID must be provided") 325 } 326 327 var args []interface{} 328 329 query := `WITH 330 members_stats AS ( 331 SELECT team_id, COUNT(DISTINCT party_id) AS total_members 332 FROM current_team_members 333 GROUP BY 334 team_id 335 ) 336 SELECT teams.*, members_stats.total_members 337 FROM teams 338 LEFT JOIN members_stats on teams.id = members_stats.team_id %s` 339 340 var where string 341 if teamID != "" { 342 where = fmt.Sprintf("WHERE teams.id = %s", nextBindVar(&args, teamID)) 343 } else if partyID != "" { 344 where = fmt.Sprintf("INNER JOIN current_team_members ON current_team_members.party_id = %s AND teams.id = current_team_members.team_id", nextBindVar(&args, partyID)) 345 } 346 347 query = fmt.Sprintf(query, where) 348 349 if err := pgxscan.Get(ctx, t.ConnectionSource, &team, query, args...); err != nil { 350 return nil, err 351 } 352 353 return &team, nil 354 } 355 356 func (t *Teams) ListTeams(ctx context.Context, pagination entities.CursorPagination) ([]entities.Team, entities.PageInfo, error) { 357 defer metrics.StartSQLQuery("Teams", "ListTeams")() 358 359 var ( 360 teams []entities.Team 361 args []interface{} 362 pageInfo entities.PageInfo 363 ) 364 365 query := `WITH 366 members_stats AS ( 367 SELECT team_id, COUNT(DISTINCT party_id) AS total_members 368 FROM current_team_members 369 GROUP BY 370 team_id 371 ) 372 SELECT teams.*, members_stats.total_members 373 FROM teams 374 LEFT JOIN members_stats on teams.id = members_stats.team_id` 375 query, args, err := PaginateQuery[entities.TeamCursor](query, args, teamsOrdering, pagination) 376 if err != nil { 377 return nil, pageInfo, err 378 } 379 380 if err := pgxscan.Select(ctx, t.ConnectionSource, &teams, query, args...); err != nil { 381 return nil, pageInfo, err 382 } 383 384 teams, pageInfo = entities.PageEntities[*v2.TeamEdge](teams, pagination) 385 386 return teams, pageInfo, nil 387 } 388 389 func (t *Teams) ListTeamsStatistics(ctx context.Context, pagination entities.CursorPagination, filters ListTeamsStatisticsFilters) ([]entities.TeamsStatistics, entities.PageInfo, error) { 390 defer metrics.StartSQLQuery("Teams", "ListTeamsStatistics")() 391 392 var ( 393 teamsStats []entities.TeamsStatistics 394 pageInfo entities.PageInfo 395 ) 396 397 query := listTeamsStatsQuery 398 args := []any{filters.AggregationEpochs} 399 400 if filters.TeamID != nil { 401 query = fmt.Sprintf(query, fmt.Sprintf(`AND team_id = %s`, nextBindVar(&args, *filters.TeamID))) 402 } else { 403 query = fmt.Sprintf(query, "") 404 } 405 406 query, args, err := PaginateQuery[entities.TeamsStatisticsCursor](query, args, teamsStatsOrdering, pagination) 407 if err != nil { 408 return nil, pageInfo, err 409 } 410 411 if err := pgxscan.Select(ctx, t.ConnectionSource, &teamsStats, query, args...); err != nil { 412 return nil, pageInfo, err 413 } 414 415 teamsStats, pageInfo = entities.PageEntities[*v2.TeamStatisticsEdge](teamsStats, pagination) 416 417 // Deserializing the GameID array as a PostgreSQL array is not correctly 418 // interpreted by the scanny library. So, we have to use the JSONB array which 419 // convert the bytea as strings. This leaves the prefix `\\x` on the game ID. 420 // As a result, we have to manually clean up of the ID. 421 for i := range teamsStats { 422 for j := range teamsStats[i].GamesPlayed { 423 teamsStats[i].GamesPlayed[j] = entities.GameID(strings.TrimLeft(teamsStats[i].GamesPlayed[j].String(), "\\x")) 424 } 425 } 426 427 return teamsStats, pageInfo, nil 428 } 429 430 func (t *Teams) ListTeamMembersStatistics(ctx context.Context, pagination entities.CursorPagination, filters ListTeamMembersStatisticsFilters) ([]entities.TeamMembersStatistics, entities.PageInfo, error) { 431 defer metrics.StartSQLQuery("Teams", "ListTeamMembersStatistics")() 432 433 var ( 434 membersStats []entities.TeamMembersStatistics 435 pageInfo entities.PageInfo 436 ) 437 438 query := listTeamMembersStatsQuery 439 args := []any{filters.AggregationEpochs, filters.TeamID} 440 441 if filters.PartyID != nil { 442 query = fmt.Sprintf(query, fmt.Sprintf(`AND party_id = %s`, nextBindVar(&args, *filters.PartyID))) 443 } else { 444 query = fmt.Sprintf(query, "") 445 } 446 447 query, args, err := PaginateQuery[entities.TeamMemberStatisticsCursor](query, args, teamMembersStatsOrdering, pagination) 448 if err != nil { 449 return nil, pageInfo, err 450 } 451 452 if err := pgxscan.Select(ctx, t.ConnectionSource, &membersStats, query, args...); err != nil { 453 return nil, pageInfo, err 454 } 455 456 membersStats, pageInfo = entities.PageEntities[*v2.TeamMemberStatisticsEdge](membersStats, pagination) 457 458 // Deserializing the GameID array as a PostgreSQL array is not correctly 459 // interpreted by the scanny library. So, we have to use the JSONB array which 460 // convert the bytea as strings. This leaves the prefix `\\x` on the game ID. 461 // As a result, we have to manually clean up of the ID. 462 for i := range membersStats { 463 for j := range membersStats[i].GamesPlayed { 464 membersStats[i].GamesPlayed[j] = entities.GameID(strings.TrimLeft(membersStats[i].GamesPlayed[j].String(), "\\x")) 465 } 466 } 467 468 return membersStats, pageInfo, nil 469 } 470 471 func (t *Teams) ListReferees(ctx context.Context, teamID entities.TeamID, pagination entities.CursorPagination) ([]entities.TeamMember, entities.PageInfo, error) { 472 defer metrics.StartSQLQuery("Teams", "ListReferees")() 473 var ( 474 referees []entities.TeamMember 475 args []interface{} 476 pageInfo entities.PageInfo 477 ) 478 479 if teamID == "" { 480 return nil, pageInfo, fmt.Errorf("teamID must be provided") 481 } 482 483 query := `SELECT ctm.* 484 FROM current_team_members ctm 485 LEFT JOIN teams t ON t.id = ctm.team_id 486 WHERE ctm.party_id != t.referrer AND ctm.team_id = %s` 487 488 query = fmt.Sprintf(query, nextBindVar(&args, teamID)) 489 490 query, args, err := PaginateQuery[entities.RefereeCursor](query, args, refereesOrdering, pagination) 491 if err != nil { 492 return nil, pageInfo, err 493 } 494 495 if err := pgxscan.Select(ctx, t.ConnectionSource, &referees, query, args...); err != nil { 496 return nil, pageInfo, err 497 } 498 499 referees, pageInfo = entities.PageEntities[*v2.TeamRefereeEdge](referees, pagination) 500 501 return referees, pageInfo, nil 502 } 503 504 func (t *Teams) ListRefereeHistory(ctx context.Context, referee entities.PartyID, pagination entities.CursorPagination) ([]entities.TeamMemberHistory, entities.PageInfo, error) { 505 defer metrics.StartSQLQuery("Teams", "ListRefereeHistory")() 506 var ( 507 referees []entities.TeamMemberHistory 508 args []interface{} 509 pageInfo entities.PageInfo 510 ) 511 512 if referee == "" { 513 return nil, pageInfo, fmt.Errorf("referee must be provided") 514 } 515 516 query := fmt.Sprintf(`SELECT team_id, joined_at_epoch, joined_at FROM team_members WHERE party_id = %s`, nextBindVar(&args, referee)) 517 518 query, args, err := PaginateQuery[entities.RefereeHistoryCursor](query, args, refereeHistoryOrdering, pagination) 519 if err != nil { 520 return nil, pageInfo, err 521 } 522 523 if err := pgxscan.Select(ctx, t.ConnectionSource, &referees, query, args...); err != nil { 524 return nil, pageInfo, err 525 } 526 527 referees, pageInfo = entities.PageEntities[*v2.TeamRefereeHistoryEdge](referees, pagination) 528 529 return referees, pageInfo, nil 530 }