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  }