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