Files
llmgw/internal/store/postgres.go
T
Ilker ManapandClaude Opus 5 a76ebdb79a
CI / test (push) Successful in 3m11s
Run the store on PostgreSQL, chosen by what the address looks like
A path is still SQLite and a postgres:// URL is PostgreSQL, so every existing
configuration keeps working untouched and moving is one line changed rather
than a new setting to find. An address of the form @/path/to/file reads the
real one out of that file, which is how a DSN carrying a password stays out of
a configuration that gets copied, diffed and pasted into issues.

The driver is pgx, which is pure Go. That is not a preference: the whole binary
is built with CGO_ENABLED=0 so it runs on a node whose libc is older than the
builder's, and a cgo driver would take that away from every deployment,
including the ones still on SQLite.

Tested against a real PostgreSQL, which found two things nothing else could.
The schema_migrations table is created in Go rather than in a migration file,
so it escaped the port and still said INTEGER — 64 bits in SQLite, 32 in
PostgreSQL, and a millisecond timestamp does not fit, so the very first
migration failed to record that it had run. And the memory migration uses an
FTS5 virtual table with three triggers keeping it in step, which has no
counterpart: PostgreSQL derives a tsvector from the row, so there is nothing
to keep in step and no trigger to forget.

That leaves exactly one query that is not the same sentence in both dialects —
the note search, MATCH against @@ — and it is written out twice rather than
hidden behind a shared spelling that would mean something subtly different in
each. Everything else is one set of queries.

The text search uses the 'simple' configuration rather than English. These are
an agent's own notes, in whatever language it chose, and stemming English
rules over Turkish would quietly stop finding things.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 16:18:10 +02:00

64 lines
2.3 KiB
Go

package store
import (
"context"
"database/sql"
"fmt"
"time"
// The pure-Go PostgreSQL driver. Pure-Go is not a preference here: the
// whole binary is built with CGO_ENABLED=0 so that it runs on a node whose
// libc is older than the builder's, and a cgo driver would take that away
// from every deployment, including the ones still on SQLite.
_ "github.com/jackc/pgx/v5/stdlib"
)
// OpenPostgres opens the store against PostgreSQL.
//
// The same queries run here as on SQLite; only the placeholders differ, and
// the dialect handles that. What differs operationally is concurrency: SQLite
// is held to one connection because more would only produce lock contention
// that reads as a deadlock, while PostgreSQL is the reason someone moves to
// it and wants a pool.
//
// The DSN carries a password, so it is taken as a string the caller read from
// somewhere protected rather than as a path this package knows how to find —
// and it is never logged, which is why errors below name the database and not
// the DSN.
func OpenPostgres(ctx context.Context, dsn string) (*SQLite, error) {
if dsn == "" {
return nil, fmt.Errorf("store: no PostgreSQL address configured")
}
db, err := sql.Open("pgx", dsn)
if err != nil {
// Deliberately not wrapped with the DSN: it holds a password, and an
// error string ends up in logs, tickets and screenshots.
return nil, fmt.Errorf("opening the PostgreSQL store: %w", err)
}
// Sized for a control plane rather than for a fleet of application
// servers. The request path barely touches the store — usage is batched,
// budgets are counted in memory, authentication is cached — so the pool
// exists for conversation turns and reporting queries, not for inference.
db.SetMaxOpenConns(16)
db.SetMaxIdleConns(4)
// Long enough that a quiet cluster keeps its connections, short enough
// that a restarted database is not answered with dead ones for an hour.
db.SetConnMaxLifetime(30 * time.Minute)
db.SetConnMaxIdleTime(5 * time.Minute)
if err := db.PingContext(ctx); err != nil {
_ = db.Close()
return nil, fmt.Errorf("reaching the PostgreSQL store: %w", err)
}
if err := migrate(ctx, db, string(PostgresDialect)); err != nil {
_ = db.Close()
return nil, err
}
return &SQLite{
raw: db,
dialect: PostgresDialect,
db: bound{c: PostgresDialect, to: db},
}, nil
}