package psql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
sq "github.com/Masterminds/squirrel"
|
|
proto "github.com/gogo/protobuf/proto"
|
|
abci "github.com/tendermint/tendermint/abci/types"
|
|
"github.com/tendermint/tendermint/libs/pubsub/query"
|
|
"github.com/tendermint/tendermint/state/indexer"
|
|
"github.com/tendermint/tendermint/types"
|
|
)
|
|
|
|
var _ indexer.EventSink = (*EventSink)(nil)
|
|
|
|
const (
|
|
TableEventBlock = "block_events"
|
|
TableEventTx = "tx_events"
|
|
TableResultTx = "tx_results"
|
|
DriverName = "postgres"
|
|
)
|
|
|
|
// EventSink is an indexer backend providing the tx/block index services.
|
|
type EventSink struct {
|
|
store *sql.DB
|
|
chainID string
|
|
}
|
|
|
|
func NewEventSink(connStr string, chainID string) (indexer.EventSink, *sql.DB, error) {
|
|
db, err := sql.Open(DriverName, connStr)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return &EventSink{
|
|
store: db,
|
|
chainID: chainID,
|
|
}, db, nil
|
|
}
|
|
|
|
func (es *EventSink) Type() indexer.EventSinkType {
|
|
return indexer.PSQL
|
|
}
|
|
|
|
func (es *EventSink) IndexBlockEvents(h types.EventDataNewBlockHeader) error {
|
|
sqlStmt := sq.
|
|
Insert(TableEventBlock).
|
|
Columns("key", "value", "height", "type", "created_at", "chain_id").
|
|
PlaceholderFormat(sq.Dollar).
|
|
Suffix("ON CONFLICT (key,height)").
|
|
Suffix("DO NOTHING")
|
|
|
|
ts := time.Now()
|
|
// index the reserved block height index
|
|
sqlStmt = sqlStmt.
|
|
Values(types.BlockHeightKey, fmt.Sprint(h.Header.Height), h.Header.Height, "", ts, es.chainID)
|
|
|
|
// index begin_block events
|
|
sqlStmt, err := indexBlockEvents(
|
|
sqlStmt, h.ResultBeginBlock.Events, types.EventTypeBeginBlock, h.Header.Height, ts, es.chainID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// index end_block events
|
|
sqlStmt, err = indexBlockEvents(
|
|
sqlStmt, h.ResultEndBlock.Events, types.EventTypeEndBlock, h.Header.Height, ts, es.chainID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = sqlStmt.RunWith(es.store).Exec()
|
|
return err
|
|
}
|
|
|
|
func (es *EventSink) IndexTxEvents(txr []*abci.TxResult) error {
|
|
// index the tx result
|
|
var txid uint32
|
|
sqlStmtTxResult := sq.
|
|
Insert(TableResultTx).
|
|
Columns("tx_result", "created_at").
|
|
PlaceholderFormat(sq.Dollar).
|
|
RunWith(es.store).
|
|
Suffix("ON CONFLICT (tx_result)").
|
|
Suffix("DO NOTHING").
|
|
Suffix("RETURNING \"id\"")
|
|
|
|
sqlStmtEvents := sq.
|
|
Insert(TableEventTx).
|
|
Columns("key", "value", "height", "hash", "tx_result_id", "created_at", "chain_id").
|
|
PlaceholderFormat(sq.Dollar).
|
|
Suffix("ON CONFLICT (key,hash)").
|
|
Suffix("DO NOTHING")
|
|
|
|
ts := time.Now()
|
|
for _, tx := range txr {
|
|
txBz, err := proto.Marshal(tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sqlStmtTxResult = sqlStmtTxResult.Values(txBz, ts)
|
|
|
|
// execute sqlStmtTxResult db query and retrieve the txid
|
|
r, err := sqlStmtTxResult.Query()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer r.Close()
|
|
|
|
if !r.Next() {
|
|
return nil
|
|
}
|
|
|
|
if err := r.Scan(&txid); err != nil {
|
|
return err
|
|
}
|
|
|
|
// index the reserved height and hash indices
|
|
hash := fmt.Sprintf("%X", types.Tx(tx.Tx).Hash())
|
|
|
|
sqlStmtEvents = sqlStmtEvents.Values(types.TxHashKey, hash, tx.Height, hash, txid, ts, es.chainID)
|
|
sqlStmtEvents = sqlStmtEvents.Values(types.TxHeightKey, fmt.Sprint(tx.Height), tx.Height, hash, txid, ts, es.chainID)
|
|
for _, event := range tx.Result.Events {
|
|
// only index events with a non-empty type
|
|
if len(event.Type) == 0 {
|
|
continue
|
|
}
|
|
|
|
for _, attr := range event.Attributes {
|
|
if len(attr.Key) == 0 {
|
|
continue
|
|
}
|
|
|
|
// index if `index: true` is set
|
|
compositeTag := fmt.Sprintf("%s.%s", event.Type, attr.Key)
|
|
|
|
// ensure event does not conflict with a reserved prefix key
|
|
if compositeTag == types.TxHashKey || compositeTag == types.TxHeightKey {
|
|
return fmt.Errorf("event type and attribute key \"%s\" is reserved; please use a different key", compositeTag)
|
|
}
|
|
|
|
if attr.GetIndex() {
|
|
sqlStmtEvents = sqlStmtEvents.Values(compositeTag, attr.Value, tx.Height, hash, txid, ts, es.chainID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// execute sqlStmtEvents db query...
|
|
_, err := sqlStmtEvents.RunWith(es.store).Exec()
|
|
return err
|
|
}
|
|
|
|
func (es *EventSink) SearchBlockEvents(ctx context.Context, q *query.Query) ([]int64, error) {
|
|
return nil, errors.New("block search is not supported via the postgres event sink")
|
|
}
|
|
|
|
func (es *EventSink) SearchTxEvents(ctx context.Context, q *query.Query) ([]*abci.TxResult, error) {
|
|
return nil, errors.New("tx search is not supported via the postgres event sink")
|
|
}
|
|
|
|
func (es *EventSink) GetTxByHash(hash []byte) (*abci.TxResult, error) {
|
|
return nil, errors.New("getTxByHash is not supported via the postgres event sink")
|
|
}
|
|
|
|
func (es *EventSink) HasBlock(h int64) (bool, error) {
|
|
return false, errors.New("hasBlock is not supported via the postgres event sink")
|
|
}
|
|
|
|
func indexBlockEvents(
|
|
sqlStmt sq.InsertBuilder,
|
|
events []abci.Event,
|
|
ty string,
|
|
height int64,
|
|
ts time.Time,
|
|
chainID string,
|
|
) (sq.InsertBuilder, error) {
|
|
for _, event := range events {
|
|
// only index events with a non-empty type
|
|
if len(event.Type) == 0 {
|
|
continue
|
|
}
|
|
|
|
for _, attr := range event.Attributes {
|
|
if len(attr.Key) == 0 {
|
|
continue
|
|
}
|
|
|
|
// index iff the event specified index:true and it's not a reserved event
|
|
compositeKey := fmt.Sprintf("%s.%s", event.Type, attr.Key)
|
|
if compositeKey == types.BlockHeightKey {
|
|
return sqlStmt, fmt.Errorf(
|
|
"event type and attribute key \"%s\" is reserved; please use a different key", compositeKey)
|
|
}
|
|
|
|
if attr.GetIndex() {
|
|
sqlStmt = sqlStmt.Values(compositeKey, attr.Value, height, ty, ts, chainID)
|
|
}
|
|
}
|
|
}
|
|
return sqlStmt, nil
|
|
}
|
|
|
|
func (es *EventSink) Stop() error {
|
|
return es.store.Close()
|
|
}
|