Files
Ilker ManapandClaude Opus 5.5 d35630defe Knowledge bases in the store: collections, grants and chunks kept apart per tenant
Collections owned by an organization, team, project or person; grants that
add up down the tree and never cross an organization; chunks in PostgreSQL
with pgvector, partitioned by organization, an HNSW index per collection, and
row-level security as a second line behind each query's filter. Search fuses
nearest-neighbour and full-text ranks. Off on SQLite and without pgvector,
saying why. server init and the HA database setup install and enable it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 12:47:57 +02:00

68 lines
2.5 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) (*DB, 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
}
s := &DB{
raw: db,
dialect: PostgresDialect,
db: bound{c: PostgresDialect, to: db},
}
// Not fatal: a store without pgvector serves everything but knowledge
// bases, and says why when somebody asks for one.
s.kbErr = s.ensureKnowledge(ctx)
return s, nil
}