package db import ( "context" "crypto/sha256" "encoding/hex" "fmt" "log/slog" "github.com/dtoro/oikos/internal/migrate" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "gopkg.in/yaml.v3" ) // Pool wraps a pgx connection pool. type Pool struct { *pgxpool.Pool } // New creates a new connection pool. func New(ctx context.Context, databaseURL string) (*Pool, error) { cfg, err := pgxpool.ParseConfig(databaseURL) if err != nil { return nil, fmt.Errorf("parse database url: %w", err) } cfg.MaxConns = 15 pool, err := pgxpool.NewWithConfig(ctx, cfg) if err != nil { return nil, fmt.Errorf("create pool: %w", err) } if err := pool.Ping(ctx); err != nil { return nil, fmt.Errorf("ping db: %w", err) } return &Pool{pool}, nil } // Migrate applies all embedded forward migrations in order (delegates to // the shared runner in internal/migrate — the same one nomos's session // tests use; ADR 0016 rule 3 keeps nomos off the adapters). func (p *Pool) Migrate(ctx context.Context) error { return migrate.Apply(ctx, p.Pool) } // SeedIngest ingests a YAML seed file into the database. // Idempotent: if the file's content hash matches seed_versions, it's a no-op (A4). func (p *Pool) SeedIngest(ctx context.Context, filename string, content []byte, ingestFn func(ctx context.Context, tx pgx.Tx, data map[string]any) error) error { hash := contentHash(content) // Check if already applied with same hash var existing string err := p.QueryRow(ctx, "SELECT content_hash FROM seed_versions WHERE file = $1", filename).Scan(&existing) if err == nil && existing == hash { return nil // no-op, same content } // Parse YAML var data map[string]any if err := yaml.Unmarshal(content, &data); err != nil { return fmt.Errorf("parse %s: %w", filename, err) } // Apply in a single transaction tx, err := p.Begin(ctx) if err != nil { return fmt.Errorf("begin tx: %w", err) } defer func() { if err := tx.Rollback(ctx); err != nil { slog.Debug("postgres: rollback after failed ingest", "error", err) } }() if err := ingestFn(ctx, tx, data); err != nil { return fmt.Errorf("ingest %s: %w", filename, err) } // Record the seed version _, err = tx.Exec(ctx, `INSERT INTO seed_versions (file, content_hash) VALUES ($1, $2) ON CONFLICT (file) DO UPDATE SET content_hash = $2, applied_at = now()`, filename, hash) if err != nil { return fmt.Errorf("record seed version: %w", err) } if err := tx.Commit(ctx); err != nil { return fmt.Errorf("commit seed: %w", err) } return nil } // contentHash returns a SHA-256 hex digest of the content. func contentHash(content []byte) string { h := sha256.Sum256(content) return hex.EncodeToString(h[:]) }