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