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>
82 lines
3.0 KiB
SQL
82 lines
3.0 KiB
SQL
-- Ported from the SQLite migration of the same number. Same version, same
|
|
-- meaning, different spelling: SQLite's storage classes and PostgreSQL's types
|
|
-- do not line up, and one shared file would be a promise neither could keep.
|
|
--
|
|
-- Times are milliseconds since the epoch in BIGINT, exactly as in SQLite,
|
|
-- rather than TIMESTAMPTZ. The Go code reads and writes int64 milliseconds
|
|
-- everywhere; converting here would mean two time representations in one
|
|
-- codebase, and the conversion would be the thing that is wrong at 2am.
|
|
|
|
-- The tenancy tree. Three levels, each owning the next.
|
|
CREATE TABLE organizations (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE teams (
|
|
id TEXT PRIMARY KEY,
|
|
org_id TEXT NOT NULL REFERENCES organizations(id),
|
|
name TEXT NOT NULL
|
|
);
|
|
CREATE INDEX teams_by_org ON teams(org_id);
|
|
|
|
CREATE TABLE projects (
|
|
id TEXT PRIMARY KEY,
|
|
team_id TEXT NOT NULL REFERENCES teams(id),
|
|
name TEXT NOT NULL
|
|
);
|
|
CREATE INDEX projects_by_team ON projects(team_id);
|
|
|
|
-- Only sha256(key) is stored, so a database leak hands over no working
|
|
-- credential. SHA-256 rather than a slow password hash on purpose: the input is
|
|
-- 256 random bits with no dictionary to attack, so the cost of argon2 would buy
|
|
-- nothing and charge tens of milliseconds on every request (plan §7.9.1).
|
|
CREATE TABLE api_keys (
|
|
id TEXT PRIMARY KEY,
|
|
key_hash BYTEA NOT NULL UNIQUE,
|
|
key_prefix TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
project_id TEXT NOT NULL REFERENCES projects(id),
|
|
band TEXT,
|
|
routing_profile TEXT,
|
|
created_at BIGINT NOT NULL,
|
|
expires_at BIGINT,
|
|
-- Revocation is a soft delete. Usage rows reference the key, and an audit
|
|
-- trail that loses its subject is not an audit trail.
|
|
revoked_at BIGINT,
|
|
last_used_at BIGINT
|
|
);
|
|
CREATE INDEX api_keys_by_project ON api_keys(project_id);
|
|
|
|
-- Limits hang off any scope rather than being duplicated onto each one. A NULL
|
|
-- column means "no limit at this level", which is not the same as 0.
|
|
CREATE TABLE limits (
|
|
scope_type TEXT NOT NULL,
|
|
scope_id TEXT NOT NULL,
|
|
rpm BIGINT,
|
|
tpm BIGINT,
|
|
-- Micro-units, not a float: a budget is accumulated by repeated addition, and
|
|
-- repeated addition of floats drifts.
|
|
budget_micros BIGINT,
|
|
budget_period TEXT,
|
|
allowed_models TEXT,
|
|
PRIMARY KEY (scope_type, scope_id)
|
|
);
|
|
|
|
-- One row per request, written in batches off the request path.
|
|
CREATE TABLE usage (
|
|
ts BIGINT NOT NULL,
|
|
key_id TEXT NOT NULL,
|
|
project_id TEXT NOT NULL,
|
|
model TEXT NOT NULL,
|
|
served_model TEXT NOT NULL,
|
|
prompt_tokens BIGINT NOT NULL,
|
|
completion_tokens BIGINT NOT NULL,
|
|
cost_micros BIGINT NOT NULL,
|
|
status BIGINT NOT NULL,
|
|
latency_ms BIGINT NOT NULL
|
|
);
|
|
-- Budget restore reads by key within a period, which is this index exactly.
|
|
CREATE INDEX usage_by_key_ts ON usage(key_id, ts);
|
|
CREATE INDEX usage_by_ts ON usage(ts);
|