-- Ledgerline schema. Idempotent: safe to run against an existing database. -- Index policy is deliberate -- see docs/phase-0-base-project-spec.md §5. -- Do NOT add transactions(account_id, created_at) or transactions(category_id) -- here; those live in db/optional_indexes.sql, applied only by `make fix-index`. CREATE EXTENSION IF NOT EXISTS pg_stat_statements; CREATE TABLE IF NOT EXISTS accounts ( id UUID PRIMARY KEY, holder_name TEXT NOT NULL, currency CHAR(3) NOT NULL, balance_minor BIGINT NOT NULL, is_hot BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS merchant_categories ( id SMALLINT PRIMARY KEY, code TEXT NOT NULL UNIQUE, label TEXT NOT NULL, risk_weight NUMERIC(3,2) NOT NULL ); CREATE TABLE IF NOT EXISTS transactions ( id UUID PRIMARY KEY, account_id UUID NOT NULL, kind TEXT NOT NULL, amount_minor BIGINT NOT NULL, currency CHAR(3), merchant_name TEXT, category_id SMALLINT, status TEXT NOT NULL, risk_score NUMERIC(4,3), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Parallel workers otherwise let a plain sequential scan of this table -- compete with an index scan on raw CPU parallelism for higher-selectivity -- accounts (e.g. the 5 hot ones, each ~6-8% of all rows) -- masking exactly -- the missing-index pathology this table's index policy (README §5) exists -- to demonstrate. Scoped to this one table, not a database-wide setting. ALTER TABLE transactions SET (parallel_workers = 0); CREATE TABLE IF NOT EXISTS sanctions_entries ( id SERIAL PRIMARY KEY, normalized_name TEXT NOT NULL, country CHAR(2), list_source TEXT, added_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS notifications ( id UUID PRIMARY KEY, transaction_id UUID NOT NULL, channel TEXT NOT NULL, status TEXT NOT NULL, attempts INT NOT NULL DEFAULT 0, sent_at TIMESTAMPTZ ); CREATE TABLE IF NOT EXISTS audit_events ( id BIGSERIAL PRIMARY KEY, transaction_id UUID NOT NULL, event_type TEXT NOT NULL, payload JSONB, recorded_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Not part of the base spec's domain model: a single-row table the seed -- script writes its summary manifest into, so both `scripts/smoke.py` and -- the test suite can read back what a given seed run actually produced -- (row counts, hot/sanctioned/poison account ids, the SEED value used) -- without re-deriving it via ad-hoc queries. CREATE TABLE IF NOT EXISTS seed_manifest ( id SMALLINT PRIMARY KEY DEFAULT 1, data JSONB NOT NULL ); CREATE INDEX IF NOT EXISTS accounts_holder_name_idx ON accounts (holder_name); CREATE INDEX IF NOT EXISTS sanctions_entries_normalized_name_idx ON sanctions_entries (normalized_name);