Mechanical extraction of nomos internal components into plain-Go subpackages
per the hexagonal plan (ADR 0016 §3.1 rule 3):
turngate/ — per-session turn serialization (plan 2026-08-03 F1)
retrycap/ — per-turn run retry cap (maxRunRetries=3)
messagequeue/ — operator-message queue for busy-turn re-entry (F2)
assent/ — chat-assent detection (isAssent, isTypedConfirmation,
ExtractPendingApprovals), decoupled from agent via
[]string input instead of persistedCall
session/ — store (chat sessions, plan execution, DB persistence),
migration runner + local emitEvent to break adapter
dependency
internal/migrate/ — shared migration runner extracted from postgres pool,
used by both the oikos postgres adapter and session tests.
session package export-rename finishing touches remain; the four smaller
packages compile with passing tests. Depguard rules and ADR-0016 leaf-note
update deferred to a followup. VERSION 0.35.1.
103 lines
2.7 KiB
Go
103 lines
2.7 KiB
Go
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[:])
|
|
}
|