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>
68 lines
2.5 KiB
Go
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
|
|
}
|