CI / test (push) Successful in 3m11s
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>
57 lines
2.6 KiB
SQL
57 lines
2.6 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.
|
|
|
|
-- Accounts, so a person can sign up, be approved, and then issue their own keys
|
|
-- without an operator running a command for each one.
|
|
CREATE TABLE users (
|
|
id TEXT PRIMARY KEY,
|
|
-- Lowercased at the boundary. Two accounts differing only in case would be
|
|
-- two accounts to the database and one to every human who looks at them.
|
|
email TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
-- argon2id, encoded with its own parameters so the cost can be raised later
|
|
-- without invalidating existing hashes. Slow hashing is right here and wrong
|
|
-- for API keys: this input is chosen by a human and has a dictionary.
|
|
password_hash TEXT NOT NULL,
|
|
-- 'pending' until an operator approves, then 'active'. 'suspended' keeps the
|
|
-- account and stops it working.
|
|
state TEXT NOT NULL,
|
|
-- Assigned at approval: which project this person's keys belong to, and
|
|
-- therefore which limits they inherit.
|
|
project_id TEXT REFERENCES projects(id),
|
|
created_at BIGINT NOT NULL,
|
|
approved_at BIGINT,
|
|
approved_by TEXT,
|
|
last_login_at BIGINT
|
|
);
|
|
CREATE INDEX users_by_state ON users(state);
|
|
|
|
-- Server-side sessions rather than signed cookies, so signing out and
|
|
-- suspending an account take effect immediately instead of whenever a token
|
|
-- happens to expire.
|
|
CREATE TABLE sessions (
|
|
token_hash BYTEA PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(id),
|
|
-- Per session, submitted with every form. A cookie alone authenticates the
|
|
-- browser, not the page that asked, which is what makes a cross-site POST
|
|
-- work.
|
|
csrf_hash BYTEA NOT NULL,
|
|
created_at BIGINT NOT NULL,
|
|
expires_at BIGINT NOT NULL,
|
|
last_seen BIGINT NOT NULL
|
|
);
|
|
CREATE INDEX sessions_by_user ON sessions(user_id);
|
|
CREATE INDEX sessions_by_expiry ON sessions(expires_at);
|
|
|
|
-- A key issued through the profile page belongs to the person who made it, so
|
|
-- they can see and revoke their own and nobody else's. NULL means a key an
|
|
-- operator issued directly.
|
|
ALTER TABLE api_keys ADD COLUMN user_id TEXT REFERENCES users(id);
|
|
CREATE INDEX api_keys_by_user ON api_keys(user_id);
|