package main import ( "context" "fmt" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) // newPool creates a pgx pool against the dereth TimescaleDB. // // Phase 1 is strictly read-only. As defense-in-depth we force every pooled // connection into read-only transaction mode, so even a buggy or future write // statement cannot mutate the live production data the Python service owns. func newPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) { cfg, err := pgxpool.ParseConfig(dsn) if err != nil { return nil, fmt.Errorf("parse DATABASE_URL: %w", err) } cfg.MaxConns = 10 cfg.MaxConnIdleTime = 5 * time.Minute cfg.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error { if _, err := conn.Exec(ctx, "SET default_transaction_read_only = on"); err != nil { return fmt.Errorf("set read-only: %w", err) } return nil } pool, err := pgxpool.NewWithConfig(ctx, cfg) if err != nil { return nil, fmt.Errorf("create pool: %w", err) } return pool, nil } // queryRowsAsMaps runs a query and returns each row as a column-name->value map, // mirroring how the Python service builds response dicts directly from rows. // A nil result is coerced to an empty (non-nil) slice so JSON encodes "[]". func queryRowsAsMaps(ctx context.Context, pool *pgxpool.Pool, sql string, args ...any) ([]map[string]any, error) { rows, err := pool.Query(ctx, sql, args...) if err != nil { return nil, err } out, err := pgx.CollectRows(rows, pgx.RowToMap) if err != nil { return nil, err } if out == nil { out = []map[string]any{} } return out, nil } // pyISO formats a timestamp the way Python's datetime.isoformat() does for a // UTC tz-aware value, so output matches FastAPI's jsonable_encoder: // - no fractional part when microseconds are zero // - otherwise exactly 6 fractional digits // - "+00:00" offset (not "Z") // Postgres timestamptz has microsecond resolution, so ns is always a multiple // of 1000. func pyISO(t time.Time) string { t = t.UTC() if t.Nanosecond() == 0 { return t.Format("2006-01-02T15:04:05+00:00") } return t.Format("2006-01-02T15:04:05") + fmt.Sprintf(".%06d+00:00", t.Nanosecond()/1000) } // formatTimes rewrites the named time.Time columns in-place to pyISO strings. // Missing or NULL (nil) values are left untouched, so they encode as JSON null. func formatTimes(rows []map[string]any, keys ...string) { for _, m := range rows { for _, k := range keys { if t, ok := m[k].(time.Time); ok { m[k] = pyISO(t) } } } }