mukan-ignite/ignite/pkg/cosmostxcollector/adapter/postgres/postgres.go
Mukan Erkin Törük 26b204bd04
Some checks are pending
Docs Deploy / build_and_deploy (push) Waiting to run
Generate Docs / cli (push) Waiting to run
Generate Config Doc / cli (push) Waiting to run
Go formatting / go-formatting (push) Waiting to run
Check links / markdown-link-check (push) Waiting to run
Integration / pre-test (push) Waiting to run
Integration / test on (push) Blocked by required conditions
Integration / status (push) Blocked by required conditions
Lint / Lint Go code (push) Waiting to run
Test / test (ubuntu-latest) (push) Waiting to run
feat: fork Ignite CLI v29 as Mukan Ignite — remove cosmos-sdk restrictions
2026-05-11 03:31:37 +03:00

424 lines
9.2 KiB
Go

package postgres
import (
"context"
"database/sql"
"embed"
"encoding/json"
"fmt"
"net/url"
"github.com/lib/pq"
ctypes "github.com/cometbft/cometbft/rpc/core/types"
"github.com/ignite/cli/v29/ignite/pkg/cosmosclient"
"github.com/ignite/cli/v29/ignite/pkg/cosmostxcollector/query"
"github.com/ignite/cli/v29/ignite/pkg/errors"
)
const (
DefaultPort = 5432
DefaultHost = "127.0.0.1"
)
const (
adapterType = "postgres"
sqlSelectBlockHeight = `
SELECT COALESCE(MAX(height), 0)
FROM tx
`
sqlSelectEventAttrs = `
SELECT event_id, name, value FROM attribute
WHERE event_id = ANY($1)
ORDER BY event_id
`
sqlInsertTX = `
INSERT INTO tx (hash, index, height, block_time)
VALUES ($1, $2, $3, $4)
`
sqlInsertEvent = `
INSERT INTO event (tx_hash, type, index)
VALUES ($1, $2, $3) RETURNING id
`
sqlInsertEventAttr = `
INSERT INTO attribute (event_id, name, value)
VALUES ($1, $2, $3)
`
sqlInsertRawTX = `
INSERT INTO raw_tx (hash, data)
VALUES ($1, $2)
`
)
//go:embed schemas/*
var fsSchemas embed.FS
// ErrClosed is returned when database connection is not open.
var ErrClosed = errors.New("no database connection")
// Option defines an option for the adapter.
type Option func(*Adapter)
// WithHost configures a database host name or IP.
func WithHost(host string) Option {
return func(a *Adapter) {
a.host = host
}
}
// WithPort configures a database port.
func WithPort(port uint) Option {
return func(a *Adapter) {
a.port = port
}
}
// WithUser configures a database user.
func WithUser(user string) Option {
return func(a *Adapter) {
a.user = user
}
}
// WithPassword configures a database password.
func WithPassword(password string) Option {
return func(a *Adapter) {
a.password = password
}
}
// WithParams configures extra database parameters.
func WithParams(params map[string]string) Option {
return func(a *Adapter) {
a.params = params
}
}
// NewAdapter creates a new PostgreSQL adapter.
func NewAdapter(database string, options ...Option) (Adapter, error) {
adapter := Adapter{
host: DefaultHost,
port: DefaultPort,
database: database,
schemas: NewSchemas(fsSchemas, ""),
}
for _, o := range options {
o(&adapter)
}
db, err := sql.Open("postgres", createPostgresURI(adapter))
if err != nil {
return Adapter{}, err
}
adapter.db = db
return adapter, nil
}
// Adapter implements a data backend adapter for PostgreSQL.
type Adapter struct {
host, user, password, database string
port uint
params map[string]string
db *sql.DB
schemas Schemas
}
// UpdateSchema updates the database schema to the latest version available.
// It applies all available schemas that were not applied already.
func (a Adapter) UpdateSchema(ctx context.Context, s Schemas) error {
db, err := a.getDB()
if err != nil {
return err
}
// Create the schema table if it doesn't exist
if _, err := db.ExecContext(ctx, s.GetTableDDL()); err != nil {
return errors.Errorf("failed to check schema table: %w", err)
}
// Get the current schema version
var v uint64
if err := db.QueryRowContext(ctx, s.GetSchemaVersionSQL()).Scan(&v); err != nil {
return errors.Errorf("failed to read current schema version: %w", err)
}
return s.WalkFrom(v+1, func(version uint64, script []byte) error {
if _, err := db.ExecContext(ctx, string(script)); err != nil {
return errors.Errorf("error applying schema version %d: %w", version, err)
}
return nil
})
}
func (a Adapter) GetType() string {
return adapterType
}
func (a Adapter) Init(ctx context.Context) error {
return a.UpdateSchema(ctx, a.schemas)
}
func (a Adapter) Save(ctx context.Context, txs []cosmosclient.TX) error {
db, err := a.getDB()
if err != nil {
return err
}
// Start a transaction
sqlTx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
// Rollback won't have any effect if the transaction is committed before
defer sqlTx.Rollback() //nolint:errcheck
// Prepare insert statements to speed up "bulk" saving times
txStmt, err := sqlTx.PrepareContext(ctx, sqlInsertTX)
if err != nil {
return err
}
defer txStmt.Close()
evtStmt, err := sqlTx.PrepareContext(ctx, sqlInsertEvent)
if err != nil {
return err
}
defer evtStmt.Close()
attrStmt, err := sqlTx.PrepareContext(ctx, sqlInsertEventAttr)
if err != nil {
return err
}
defer attrStmt.Close()
// All the transactions are saved within the context of the same database
// transactions and because of that either all block transactions are
// saved or none of them.
for _, tx := range txs {
if err := saveRawTX(ctx, sqlTx, tx.Raw); err != nil {
return err
}
if err := saveTX(ctx, txStmt, evtStmt, attrStmt, tx); err != nil {
return err
}
}
return sqlTx.Commit()
}
func (a Adapter) GetLatestHeight(ctx context.Context) (height int64, err error) {
db, err := a.getDB()
if err != nil {
return 0, err
}
row := db.QueryRowContext(ctx, sqlSelectBlockHeight)
if err = row.Scan(&height); err != nil {
return 0, err
}
return height, nil
}
func (a Adapter) QueryEvents(ctx context.Context, q query.EventQuery) ([]query.Event, error) {
db, err := a.getDB()
if err != nil {
return nil, err
}
sql := parseEventQuery(q)
args := extractEventQueryArgs(q)
rows, err := db.QueryContext(ctx, sql, args...)
if err != nil {
return nil, err
}
var (
events []query.Event
eventIDs []int64
// Keep an index of the event position within the events slice
// to find them later when updating their attributes.
eventIndexes = make(map[int64]int)
)
for i := 0; rows.Next(); i++ {
e := query.Event{}
if err := rows.Scan(&e.ID, &e.Index, &e.TXHash, &e.Type, &e.CreatedAt); err != nil {
return nil, errors.Errorf("failed to read event: %w", err)
}
events = append(events, e)
eventIDs = append(eventIDs, e.ID)
eventIndexes[e.ID] = i
}
// Don't query attributes when there are no events
if len(events) == 0 {
return events, nil
}
// Select the attributes for the events that matched the query
rows, err = db.QueryContext(ctx, sqlSelectEventAttrs, pq.Array(eventIDs))
if err != nil {
return nil, err
}
// Update the attributes of the selected events
for rows.Next() {
var (
eventID int64
name string
value []byte
)
if err := rows.Scan(&eventID, &name, &value); err != nil {
return nil, errors.Errorf("failed to read event attribute: %w", err)
}
i := eventIndexes[eventID]
events[i].Attributes = append(events[i].Attributes, query.NewAttribute(name, value))
}
return events, nil
}
func (a Adapter) Query(ctx context.Context, q query.Query) (query.Cursor, error) {
db, err := a.getDB()
if err != nil {
return nil, err
}
sql, err := parseQuery(q)
if err != nil {
return nil, err
}
args := extractQueryArgs(q)
rows, err := db.QueryContext(ctx, sql, args...)
if err != nil {
return nil, err
}
return rows, nil
}
func (a Adapter) getDB() (*sql.DB, error) {
if a.db == nil {
return nil, ErrClosed
}
return a.db, nil
}
func createPostgresURI(a Adapter) string {
uri := url.URL{
Scheme: adapterType,
Host: fmt.Sprintf("%s:%d", a.host, a.port),
Path: a.database,
}
if a.user != "" {
if a.password != "" {
uri.User = url.UserPassword(a.user, a.password)
} else {
uri.User = url.User(a.user)
}
}
// Add extra params as query arguments
if a.params != nil {
val := url.Values{}
for k, v := range a.params {
val.Set(k, v)
}
uri.RawQuery = val.Encode()
}
return uri.String()
}
func saveRawTX(ctx context.Context, sqlTx *sql.Tx, rtx *ctypes.ResultTx) error {
hash := rtx.Hash.String()
raw, err := json.Marshal(rtx)
if err != nil {
return errors.Errorf("failed to encode raw TX %s: %w", hash, err)
}
if _, err := sqlTx.ExecContext(ctx, sqlInsertRawTX, hash, raw); err != nil {
return errors.Errorf("error saving raw TX %s: %w", hash, err)
}
return nil
}
func saveTX(ctx context.Context, txStmt, evtStmt, attrStmt *sql.Stmt, tx cosmosclient.TX) error {
hash := tx.Raw.Hash.String()
if _, err := txStmt.ExecContext(ctx, hash, tx.Raw.Index, tx.Raw.Height, tx.BlockTime); err != nil {
return errors.Errorf("error saving TX %s: %w", hash, err)
}
events, err := tx.GetEvents()
if err != nil {
return err
}
for i, evt := range events {
var evtID int
row := evtStmt.QueryRowContext(ctx, hash, evt.Type, i)
if err := row.Err(); err != nil {
return errors.Errorf("error saving event '%s': %w", evt.Type, err)
}
if err := row.Scan(&evtID); err != nil {
return errors.Errorf("error reading event ID: %w", err)
}
for _, attr := range evt.Attributes {
if _, err := attrStmt.ExecContext(ctx, evtID, attr.Key, attr.Value); err != nil {
return errors.Errorf("error saving event attr '%s.%s': %w", evt.Type, attr.Key, err)
}
}
}
return nil
}
func extractQueryArgs(q query.Query) []any {
// When the query is a call to a postgres function
// add the arguments before the filter values
args := q.Args()
// Add the values from the filters
for _, f := range q.Filters() {
if a := f.Value(); a != nil {
args = append(args, a)
}
}
return args
}
func extractEventQueryArgs(q query.EventQuery) (args []any) {
for _, f := range q.Filters() {
if a := f.Value(); a != nil {
args = append(args, a)
}
}
return args
}