81 lines
3.0 KiB
SQL
81 lines
3.0 KiB
SQL
-- 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);
|