You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

211 lines
5.5 KiB

  1. package psql
  2. import (
  3. "context"
  4. "database/sql"
  5. "errors"
  6. "fmt"
  7. "time"
  8. sq "github.com/Masterminds/squirrel"
  9. proto "github.com/gogo/protobuf/proto"
  10. abci "github.com/tendermint/tendermint/abci/types"
  11. "github.com/tendermint/tendermint/libs/pubsub/query"
  12. "github.com/tendermint/tendermint/state/indexer"
  13. "github.com/tendermint/tendermint/types"
  14. )
  15. var _ indexer.EventSink = (*EventSink)(nil)
  16. const (
  17. TableEventBlock = "block_events"
  18. TableEventTx = "tx_events"
  19. TableResultTx = "tx_results"
  20. DriverName = "postgres"
  21. )
  22. // EventSink is an indexer backend providing the tx/block index services.
  23. type EventSink struct {
  24. store *sql.DB
  25. chainID string
  26. }
  27. func NewEventSink(connStr string, chainID string) (indexer.EventSink, *sql.DB, error) {
  28. db, err := sql.Open(DriverName, connStr)
  29. if err != nil {
  30. return nil, nil, err
  31. }
  32. return &EventSink{
  33. store: db,
  34. chainID: chainID,
  35. }, db, nil
  36. }
  37. func (es *EventSink) Type() indexer.EventSinkType {
  38. return indexer.PSQL
  39. }
  40. func (es *EventSink) IndexBlockEvents(h types.EventDataNewBlockHeader) error {
  41. sqlStmt := sq.
  42. Insert(TableEventBlock).
  43. Columns("key", "value", "height", "type", "created_at", "chain_id").
  44. PlaceholderFormat(sq.Dollar).
  45. Suffix("ON CONFLICT (key,height)").
  46. Suffix("DO NOTHING")
  47. ts := time.Now()
  48. // index the reserved block height index
  49. sqlStmt = sqlStmt.
  50. Values(types.BlockHeightKey, fmt.Sprint(h.Header.Height), h.Header.Height, "", ts, es.chainID)
  51. // index begin_block events
  52. sqlStmt, err := indexBlockEvents(
  53. sqlStmt, h.ResultBeginBlock.Events, types.EventTypeBeginBlock, h.Header.Height, ts, es.chainID)
  54. if err != nil {
  55. return err
  56. }
  57. // index end_block events
  58. sqlStmt, err = indexBlockEvents(
  59. sqlStmt, h.ResultEndBlock.Events, types.EventTypeEndBlock, h.Header.Height, ts, es.chainID)
  60. if err != nil {
  61. return err
  62. }
  63. _, err = sqlStmt.RunWith(es.store).Exec()
  64. return err
  65. }
  66. func (es *EventSink) IndexTxEvents(txr []*abci.TxResult) error {
  67. // index the tx result
  68. var txid uint32
  69. sqlStmtTxResult := sq.
  70. Insert(TableResultTx).
  71. Columns("tx_result", "created_at").
  72. PlaceholderFormat(sq.Dollar).
  73. RunWith(es.store).
  74. Suffix("ON CONFLICT (tx_result)").
  75. Suffix("DO NOTHING").
  76. Suffix("RETURNING \"id\"")
  77. sqlStmtEvents := sq.
  78. Insert(TableEventTx).
  79. Columns("key", "value", "height", "hash", "tx_result_id", "created_at", "chain_id").
  80. PlaceholderFormat(sq.Dollar).
  81. Suffix("ON CONFLICT (key,hash)").
  82. Suffix("DO NOTHING")
  83. ts := time.Now()
  84. for _, tx := range txr {
  85. txBz, err := proto.Marshal(tx)
  86. if err != nil {
  87. return err
  88. }
  89. sqlStmtTxResult = sqlStmtTxResult.Values(txBz, ts)
  90. // execute sqlStmtTxResult db query and retrieve the txid
  91. r, err := sqlStmtTxResult.Query()
  92. if err != nil {
  93. return err
  94. }
  95. defer r.Close()
  96. if !r.Next() {
  97. return nil
  98. }
  99. if err := r.Scan(&txid); err != nil {
  100. return err
  101. }
  102. // index the reserved height and hash indices
  103. hash := fmt.Sprintf("%X", types.Tx(tx.Tx).Hash())
  104. sqlStmtEvents = sqlStmtEvents.Values(types.TxHashKey, hash, tx.Height, hash, txid, ts, es.chainID)
  105. sqlStmtEvents = sqlStmtEvents.Values(types.TxHeightKey, fmt.Sprint(tx.Height), tx.Height, hash, txid, ts, es.chainID)
  106. for _, event := range tx.Result.Events {
  107. // only index events with a non-empty type
  108. if len(event.Type) == 0 {
  109. continue
  110. }
  111. for _, attr := range event.Attributes {
  112. if len(attr.Key) == 0 {
  113. continue
  114. }
  115. // index if `index: true` is set
  116. compositeTag := fmt.Sprintf("%s.%s", event.Type, attr.Key)
  117. // ensure event does not conflict with a reserved prefix key
  118. if compositeTag == types.TxHashKey || compositeTag == types.TxHeightKey {
  119. return fmt.Errorf("event type and attribute key \"%s\" is reserved; please use a different key", compositeTag)
  120. }
  121. if attr.GetIndex() {
  122. sqlStmtEvents = sqlStmtEvents.Values(compositeTag, attr.Value, tx.Height, hash, txid, ts, es.chainID)
  123. }
  124. }
  125. }
  126. }
  127. // execute sqlStmtEvents db query...
  128. _, err := sqlStmtEvents.RunWith(es.store).Exec()
  129. return err
  130. }
  131. func (es *EventSink) SearchBlockEvents(ctx context.Context, q *query.Query) ([]int64, error) {
  132. return nil, errors.New("block search is not supported via the postgres event sink")
  133. }
  134. func (es *EventSink) SearchTxEvents(ctx context.Context, q *query.Query) ([]*abci.TxResult, error) {
  135. return nil, errors.New("tx search is not supported via the postgres event sink")
  136. }
  137. func (es *EventSink) GetTxByHash(hash []byte) (*abci.TxResult, error) {
  138. return nil, errors.New("getTxByHash is not supported via the postgres event sink")
  139. }
  140. func (es *EventSink) HasBlock(h int64) (bool, error) {
  141. return false, errors.New("hasBlock is not supported via the postgres event sink")
  142. }
  143. func indexBlockEvents(
  144. sqlStmt sq.InsertBuilder,
  145. events []abci.Event,
  146. ty string,
  147. height int64,
  148. ts time.Time,
  149. chainID string,
  150. ) (sq.InsertBuilder, error) {
  151. for _, event := range events {
  152. // only index events with a non-empty type
  153. if len(event.Type) == 0 {
  154. continue
  155. }
  156. for _, attr := range event.Attributes {
  157. if len(attr.Key) == 0 {
  158. continue
  159. }
  160. // index iff the event specified index:true and it's not a reserved event
  161. compositeKey := fmt.Sprintf("%s.%s", event.Type, attr.Key)
  162. if compositeKey == types.BlockHeightKey {
  163. return sqlStmt, fmt.Errorf(
  164. "event type and attribute key \"%s\" is reserved; please use a different key", compositeKey)
  165. }
  166. if attr.GetIndex() {
  167. sqlStmt = sqlStmt.Values(compositeKey, attr.Value, height, ty, ts, chainID)
  168. }
  169. }
  170. }
  171. return sqlStmt, nil
  172. }
  173. func (es *EventSink) Stop() error {
  174. return es.store.Close()
  175. }