code.vegaprotocol.io/vega@v0.79.0/datanode/sqlstore/oracle_data.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  
    22  	"code.vegaprotocol.io/vega/datanode/entities"
    23  	"code.vegaprotocol.io/vega/datanode/metrics"
    24  	v2 "code.vegaprotocol.io/vega/protos/data-node/api/v2"
    25  
    26  	"github.com/georgysavva/scany/pgxscan"
    27  )
    28  
    29  type OracleData struct {
    30  	*ConnectionSource
    31  }
    32  
    33  const (
    34  	sqlOracleDataColumns = `signers, data, meta_data, broadcast_at, error, tx_hash, vega_time, seq_num`
    35  	oracleDataQuery      = `SELECT od.*, aggregated.spec_ids as matched_spec_ids
    36  	FROM
    37  		oracle_data od
    38  	LEFT JOIN LATERAL (
    39  		SELECT ARRAY_AGG(spec_id) AS spec_ids
    40  		FROM oracle_data_oracle_specs ods
    41  		WHERE od.vega_time = ods.vega_time
    42  		AND od.seq_num = ods.seq_num
    43  	) aggregated ON true
    44  	`
    45  )
    46  
    47  var oracleDataOrdering = TableOrdering{
    48  	ColumnOrdering{Name: "vega_time", Sorting: ASC},
    49  	ColumnOrdering{Name: "signers", Sorting: ASC},
    50  }
    51  
    52  func NewOracleData(connectionSource *ConnectionSource) *OracleData {
    53  	return &OracleData{
    54  		ConnectionSource: connectionSource,
    55  	}
    56  }
    57  
    58  func (od *OracleData) Add(ctx context.Context, oracleData *entities.OracleData) error {
    59  	defer metrics.StartSQLQuery("OracleData", "Add")()
    60  	query := fmt.Sprintf("insert into oracle_data(%s) values ($1, $2, $3, $4, $5, $6, $7, $8)", sqlOracleDataColumns)
    61  
    62  	if _, err := od.Exec(
    63  		ctx, query,
    64  		oracleData.ExternalData.Data.Signers, oracleData.ExternalData.Data.Data, oracleData.ExternalData.Data.MetaData,
    65  		oracleData.ExternalData.Data.BroadcastAt,
    66  		oracleData.ExternalData.Data.Error, oracleData.ExternalData.Data.TxHash,
    67  		oracleData.ExternalData.Data.VegaTime, oracleData.ExternalData.Data.SeqNum,
    68  	); err != nil {
    69  		err = fmt.Errorf("could not insert oracle data into database: %w", err)
    70  		return err
    71  	}
    72  
    73  	query2 := "insert into oracle_data_oracle_specs(vega_time, seq_num, spec_id) values ($1, $2, unnest($3::bytea[]))"
    74  	if _, err := od.Exec(
    75  		ctx, query2,
    76  		oracleData.ExternalData.Data.VegaTime, oracleData.ExternalData.Data.SeqNum, oracleData.ExternalData.Data.MatchedSpecIds,
    77  	); err != nil {
    78  		err = fmt.Errorf("could not insert oracle data join into database: %w", err)
    79  		return err
    80  	}
    81  
    82  	return nil
    83  }
    84  
    85  func (od *OracleData) ListOracleData(ctx context.Context, id string, pagination entities.Pagination) ([]entities.OracleData, entities.PageInfo, error) {
    86  	switch p := pagination.(type) {
    87  	case entities.CursorPagination:
    88  		return listOracleDataBySpecIDCursorPagination(ctx, od.ConnectionSource, id, p)
    89  	default:
    90  		panic("unsupported pagination")
    91  	}
    92  }
    93  
    94  func (od *OracleData) GetByTxHash(ctx context.Context, txHash entities.TxHash) ([]entities.OracleData, error) {
    95  	defer metrics.StartSQLQuery("OracleData", "GetByTxHash")()
    96  
    97  	var data []entities.Data
    98  	query := fmt.Sprintf(`%s WHERE tx_hash = $1`, oracleDataQuery)
    99  	err := pgxscan.Select(ctx, od.ConnectionSource, &data, query, txHash)
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  
   104  	return scannedDataToOracleData(data), nil
   105  }
   106  
   107  func scannedDataToOracleData(scanned []entities.Data) []entities.OracleData {
   108  	oracleData := []entities.OracleData{}
   109  	if len(scanned) > 0 {
   110  		for _, s := range scanned {
   111  			oracleData = append(oracleData, entities.OracleData{
   112  				ExternalData: &entities.ExternalData{
   113  					Data: &entities.Data{
   114  						Signers:        s.Signers,
   115  						Data:           s.Data,
   116  						MetaData:       s.MetaData,
   117  						MatchedSpecIds: s.MatchedSpecIds,
   118  						BroadcastAt:    s.BroadcastAt,
   119  						Error:          s.Error,
   120  						TxHash:         s.TxHash,
   121  						VegaTime:       s.VegaTime,
   122  						SeqNum:         s.SeqNum,
   123  					},
   124  				},
   125  			})
   126  		}
   127  	}
   128  
   129  	return oracleData
   130  }
   131  
   132  func listOracleDataBySpecIDCursorPagination(ctx context.Context, conn Connection, id string, pagination entities.CursorPagination) (
   133  	[]entities.OracleData, entities.PageInfo, error,
   134  ) {
   135  	var (
   136  		oracleData []entities.OracleData
   137  		data       = []entities.Data{}
   138  
   139  		pageInfo entities.PageInfo
   140  		bindVars []interface{}
   141  		err      error
   142  	)
   143  
   144  	query := oracleDataQuery
   145  	andOrWhere := "WHERE"
   146  
   147  	if len(id) > 0 {
   148  		specID := entities.SpecID(id)
   149  
   150  		query = fmt.Sprintf(`%s
   151  		WHERE EXISTS (SELECT 1 from  oracle_data_oracle_specs ods
   152  		  WHERE  od.vega_time = ods.vega_time
   153  		  AND    od.seq_num = ods.seq_num
   154  		  AND    ods.spec_id=%s)`, query, nextBindVar(&bindVars, specID))
   155  
   156  		andOrWhere = "AND"
   157  	}
   158  
   159  	// if the cursor is empty, we should restrict the query to the last day of data as otherwise, the query will scan the full hypertable
   160  	// we only do this if we are returning the newest first data because that should be kept in memory by TimescaleDB anyway.
   161  	// If we have a first N cursor traversing newest first data, without an after cursor, we should also restrict by date.
   162  	// Traversing from the oldest data to the newest data will result in table scans and take time as we don't know what the oldest data is due to retention policies.
   163  	// Anything after the first page will have a vega time in the cursor so this will not be needed.
   164  	if pagination.HasForward() && !pagination.Forward.HasCursor() && pagination.NewestFirst {
   165  		query = fmt.Sprintf("%s %s vega_time > now() - interval '1 day'", query, andOrWhere)
   166  	}
   167  
   168  	query, bindVars, err = PaginateQuery[entities.OracleDataCursor](query, bindVars, oracleDataOrdering, pagination)
   169  	if err != nil {
   170  		return oracleData, pageInfo, err
   171  	}
   172  
   173  	defer metrics.StartSQLQuery("OracleData", "ListOracleData")()
   174  	// NOTE: If any error during the scan occurred, we return empty oracle data object.
   175  	if err = pgxscan.Select(ctx, conn, &data, query, bindVars...); err != nil {
   176  		return oracleData, pageInfo, err
   177  	}
   178  
   179  	oracleData = scannedDataToOracleData(data)
   180  
   181  	oracleData, pageInfo = entities.PageEntities[*v2.OracleDataEdge](oracleData, pagination)
   182  	return oracleData, pageInfo, nil
   183  }