github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/internal/state/indexer/sink/psql/psql.go (about)

     1  // Package psql implements an event sink backed by a PostgreSQL database.
     2  package psql
     3  
     4  import (
     5  	"context"
     6  	"database/sql"
     7  	"errors"
     8  	"fmt"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/gogo/protobuf/proto"
    13  
    14  	abci "github.com/ari-anchor/sei-tendermint/abci/types"
    15  	"github.com/ari-anchor/sei-tendermint/internal/pubsub/query"
    16  	"github.com/ari-anchor/sei-tendermint/internal/state/indexer"
    17  	"github.com/ari-anchor/sei-tendermint/types"
    18  )
    19  
    20  const (
    21  	tableBlocks     = "blocks"
    22  	tableTxResults  = "tx_results"
    23  	tableEvents     = "events"
    24  	tableAttributes = "attributes"
    25  	driverName      = "postgres"
    26  )
    27  
    28  // EventSink is an indexer backend providing the tx/block index services.  This
    29  // implementation stores records in a PostgreSQL database using the schema
    30  // defined in state/indexer/sink/psql/schema.sql.
    31  type EventSink struct {
    32  	store   *sql.DB
    33  	chainID string
    34  }
    35  
    36  // NewEventSink constructs an event sink associated with the PostgreSQL
    37  // database specified by connStr. Events written to the sink are attributed to
    38  // the specified chainID.
    39  func NewEventSink(connStr, chainID string) (*EventSink, error) {
    40  	db, err := sql.Open(driverName, connStr)
    41  	if err != nil {
    42  		return nil, err
    43  	} else if err := db.Ping(); err != nil {
    44  		return nil, err
    45  	}
    46  
    47  	return &EventSink{
    48  		store:   db,
    49  		chainID: chainID,
    50  	}, nil
    51  }
    52  
    53  // DB returns the underlying Postgres connection used by the sink.
    54  // This is exported to support testing.
    55  func (es *EventSink) DB() *sql.DB { return es.store }
    56  
    57  // Type returns the structure type for this sink, which is Postgres.
    58  func (es *EventSink) Type() indexer.EventSinkType { return indexer.PSQL }
    59  
    60  // runInTransaction executes query in a fresh database transaction.
    61  // If query reports an error, the transaction is rolled back and the
    62  // error from query is reported to the caller.
    63  // Otherwise, the result of committing the transaction is returned.
    64  func runInTransaction(db *sql.DB, query func(*sql.Tx) error) error {
    65  	dbtx, err := db.Begin()
    66  	if err != nil {
    67  		return err
    68  	}
    69  	if err := query(dbtx); err != nil {
    70  		_ = dbtx.Rollback() // report the initial error, not the rollback
    71  		return err
    72  	}
    73  	return dbtx.Commit()
    74  }
    75  
    76  // queryWithID executes the specified SQL query with the given arguments,
    77  // expecting a single-row, single-column result containing an ID. If the query
    78  // succeeds, the ID from the result is returned.
    79  func queryWithID(tx *sql.Tx, query string, args ...interface{}) (uint32, error) {
    80  	var id uint32
    81  	if err := tx.QueryRow(query, args...).Scan(&id); err != nil {
    82  		return 0, err
    83  	}
    84  	return id, nil
    85  }
    86  
    87  // insertEvents inserts a slice of events and any indexed attributes of those
    88  // events into the database associated with dbtx.
    89  //
    90  // If txID > 0, the event is attributed to the Tendermint transaction with that
    91  // ID; otherwise it is recorded as a block event.
    92  func insertEvents(dbtx *sql.Tx, blockID, txID uint32, evts []abci.Event) error {
    93  	// Populate the transaction ID field iff one is defined (> 0).
    94  	var txIDArg interface{}
    95  	if txID > 0 {
    96  		txIDArg = txID
    97  	}
    98  
    99  	// Add each event to the events table, and retrieve its row ID to use when
   100  	// adding any attributes the event provides.
   101  	for _, evt := range evts {
   102  		// Skip events with an empty type.
   103  		if evt.Type == "" {
   104  			continue
   105  		}
   106  
   107  		eid, err := queryWithID(dbtx, `
   108  INSERT INTO `+tableEvents+` (block_id, tx_id, type) VALUES ($1, $2, $3)
   109    RETURNING rowid;
   110  `, blockID, txIDArg, evt.Type)
   111  		if err != nil {
   112  			return err
   113  		}
   114  
   115  		// Add any attributes flagged for indexing.
   116  		for _, attr := range evt.Attributes {
   117  			if !attr.Index {
   118  				continue
   119  			}
   120  			compositeKey := evt.Type + "." + string(attr.Key)
   121  			if _, err := dbtx.Exec(`
   122  INSERT INTO `+tableAttributes+` (event_id, key, composite_key, value)
   123    VALUES ($1, $2, $3, $4);
   124  `, eid, attr.Key, compositeKey, attr.Value); err != nil {
   125  				return err
   126  			}
   127  		}
   128  	}
   129  	return nil
   130  }
   131  
   132  // makeIndexedEvent constructs an event from the specified composite key and
   133  // value. If the key has the form "type.name", the event will have a single
   134  // attribute with that name and the value; otherwise the event will have only
   135  // a type and no attributes.
   136  func makeIndexedEvent(compositeKey, value string) abci.Event {
   137  	i := strings.Index(compositeKey, ".")
   138  	if i < 0 {
   139  		return abci.Event{Type: compositeKey}
   140  	}
   141  	return abci.Event{Type: compositeKey[:i], Attributes: []abci.EventAttribute{
   142  		{Key: []byte(compositeKey[i+1:]), Value: []byte(value), Index: true},
   143  	}}
   144  }
   145  
   146  // IndexBlockEvents indexes the specified block header, part of the
   147  // indexer.EventSink interface.
   148  func (es *EventSink) IndexBlockEvents(h types.EventDataNewBlockHeader) error {
   149  	ts := time.Now().UTC()
   150  
   151  	return runInTransaction(es.store, func(dbtx *sql.Tx) error {
   152  		// Add the block to the blocks table and report back its row ID for use
   153  		// in indexing the events for the block.
   154  		blockID, err := queryWithID(dbtx, `
   155  INSERT INTO `+tableBlocks+` (height, chain_id, created_at)
   156    VALUES ($1, $2, $3)
   157    ON CONFLICT DO NOTHING
   158    RETURNING rowid;
   159  `, h.Header.Height, es.chainID, ts)
   160  		if err == sql.ErrNoRows {
   161  			return nil // we already saw this block; quietly succeed
   162  		} else if err != nil {
   163  			return fmt.Errorf("indexing block header: %w", err)
   164  		}
   165  
   166  		// Insert the special block meta-event for height.
   167  		if err := insertEvents(dbtx, blockID, 0, []abci.Event{
   168  			makeIndexedEvent(types.BlockHeightKey, fmt.Sprint(h.Header.Height)),
   169  		}); err != nil {
   170  			return fmt.Errorf("block meta-events: %w", err)
   171  		}
   172  		// Insert all the block events. Order is important here,
   173  		if err := insertEvents(dbtx, blockID, 0, h.ResultFinalizeBlock.Events); err != nil {
   174  			return fmt.Errorf("finalize-block events: %w", err)
   175  		}
   176  		return nil
   177  	})
   178  }
   179  
   180  func (es *EventSink) IndexTxEvents(txrs []*abci.TxResult) error {
   181  	ts := time.Now().UTC()
   182  
   183  	for _, txr := range txrs {
   184  		// Encode the result message in protobuf wire format for indexing.
   185  		resultData, err := proto.Marshal(txr)
   186  		if err != nil {
   187  			return fmt.Errorf("marshaling tx_result: %w", err)
   188  		}
   189  
   190  		// Index the hash of the underlying transaction as a hex string.
   191  		txHash := fmt.Sprintf("%X", types.Tx(txr.Tx).Hash())
   192  
   193  		if err := runInTransaction(es.store, func(dbtx *sql.Tx) error {
   194  			// Find the block associated with this transaction. The block header
   195  			// must have been indexed prior to the transactions belonging to it.
   196  			blockID, err := queryWithID(dbtx, `
   197  SELECT rowid FROM `+tableBlocks+` WHERE height = $1 AND chain_id = $2;
   198  `, txr.Height, es.chainID)
   199  			if err != nil {
   200  				return fmt.Errorf("finding block ID: %w", err)
   201  			}
   202  
   203  			// Insert a record for this tx_result and capture its ID for indexing events.
   204  			txID, err := queryWithID(dbtx, `
   205  INSERT INTO `+tableTxResults+` (block_id, index, created_at, tx_hash, tx_result)
   206    VALUES ($1, $2, $3, $4, $5)
   207    ON CONFLICT DO NOTHING
   208    RETURNING rowid;
   209  `, blockID, txr.Index, ts, txHash, resultData)
   210  			if err == sql.ErrNoRows {
   211  				return nil // we already saw this transaction; quietly succeed
   212  			} else if err != nil {
   213  				return fmt.Errorf("indexing tx_result: %w", err)
   214  			}
   215  
   216  			// Insert the special transaction meta-events for hash and height.
   217  			if err := insertEvents(dbtx, blockID, txID, []abci.Event{
   218  				makeIndexedEvent(types.TxHashKey, txHash),
   219  				makeIndexedEvent(types.TxHeightKey, fmt.Sprint(txr.Height)),
   220  			}); err != nil {
   221  				return fmt.Errorf("indexing transaction meta-events: %w", err)
   222  			}
   223  			// Index any events packaged with the transaction.
   224  			if err := insertEvents(dbtx, blockID, txID, txr.Result.Events); err != nil {
   225  				return fmt.Errorf("indexing transaction events: %w", err)
   226  			}
   227  			return nil
   228  
   229  		}); err != nil {
   230  			return err
   231  		}
   232  	}
   233  	return nil
   234  }
   235  
   236  // SearchBlockEvents is not implemented by this sink, and reports an error for all queries.
   237  func (es *EventSink) SearchBlockEvents(ctx context.Context, q *query.Query) ([]int64, error) {
   238  	return nil, errors.New("block search is not supported via the postgres event sink")
   239  }
   240  
   241  // SearchTxEvents is not implemented by this sink, and reports an error for all queries.
   242  func (es *EventSink) SearchTxEvents(ctx context.Context, q *query.Query) ([]*abci.TxResult, error) {
   243  	return nil, errors.New("tx search is not supported via the postgres event sink")
   244  }
   245  
   246  // GetTxByHash is not implemented by this sink, and reports an error for all queries.
   247  func (es *EventSink) GetTxByHash(hash []byte) (*abci.TxResult, error) {
   248  	return nil, errors.New("getTxByHash is not supported via the postgres event sink")
   249  }
   250  
   251  // HasBlock is not implemented by this sink, and reports an error for all queries.
   252  func (es *EventSink) HasBlock(h int64) (bool, error) {
   253  	return false, errors.New("hasBlock is not supported via the postgres event sink")
   254  }
   255  
   256  // Stop closes the underlying PostgreSQL database.
   257  func (es *EventSink) Stop() error { return es.store.Close() }