setup_sql/01_schema.sql was an idempotent bootstrap script — CREATE TABLE IF NOT EXISTS plus a tail of ALTER ... ADD COLUMN IF NOT EXISTS. It had no record of what any given database had applied, which is how a branch could declare col_meta.in_grain while the running database lacked it, with nothing able to detect the mismatch. The symptom would have been a confusing 'column "in_grain" does not exist' inside an unrelated request. Migrations-only, no hand-maintained current-state file to drift: - setup_sql/migrations/*.sql applied in filename order, recorded in pf.schema_version with a checksum. Split along the schema's actual evolution, so each column is declared exactly once — 01_schema.sql had grown to declare dim_group, dim_period_col and in_grain twice each. - lib/migrations.js holds the bookkeeping, shared by the CLI and the boot check. scripts/migrate.js provides up | status | baseline. - server.js refuses to start when the database is behind, listing what is pending. This converts silent drift into a clear boot message, which was the whole point. PF_SKIP_MIGRATION_CHECK=1 bypasses. - Four integrity guards, each verified to fire: a migration modified after being applied, one recorded as applied but missing from disk, one that would apply out of order, and a re-run when already current. - No IF NOT EXISTS on new migrations. The bookkeeping already guarantees one run each, and the guards hide ordering mistakes — that is exactly why 01_schema.sql had ALTERs sitting above the CREATE TABLE they depended on, broken for anyone installing from scratch. 0004 keeps the guard only because it was applied by hand before migrations existed. Verified: replaying all four migrations into a throwaway schema reproduces the live pf schema exactly, 41 columns, column for column. schema.generated.sql is a pg_dump snapshot for reading, refreshed by npm run schema:dump. It excludes the runtime fc_* tables and strips pg_dump's random \restrict token and version banner, so regenerating an unchanged schema yields an identical file rather than a spurious diff. pf.dim_period stays out of migrations — it is a parameterised data load (fiscal year start month), not a schema change. The dev database (ubm) has been baselined at all four migrations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
64 lines
2.3 KiB
SQL
64 lines
2.3 KiB
SQL
-- Initial pf schema: sources, column metadata, versions, audit log, generated SQL.
|
|
--
|
|
-- This is the schema as it stood before the additive columns in later migrations.
|
|
-- Columns added afterwards are declared once, in their own migration — not here.
|
|
|
|
CREATE SCHEMA IF NOT EXISTS pf;
|
|
|
|
CREATE TABLE pf.source (
|
|
id serial PRIMARY KEY,
|
|
schema text NOT NULL,
|
|
tname text NOT NULL,
|
|
label text,
|
|
status text NOT NULL DEFAULT 'active', -- active | archived
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
created_by text,
|
|
UNIQUE (schema, tname)
|
|
);
|
|
|
|
CREATE TABLE pf.col_meta (
|
|
id serial PRIMARY KEY,
|
|
source_id integer NOT NULL REFERENCES pf.source(id) ON DELETE CASCADE,
|
|
cname text NOT NULL,
|
|
label text,
|
|
role text NOT NULL DEFAULT 'ignore', -- dimension | value | units | date | filter | ignore
|
|
is_key boolean NOT NULL DEFAULT false, -- true = usable in WHERE slice
|
|
opos integer,
|
|
UNIQUE (source_id, cname)
|
|
);
|
|
|
|
CREATE TABLE pf.version (
|
|
id serial PRIMARY KEY,
|
|
source_id integer NOT NULL REFERENCES pf.source(id) ON DELETE RESTRICT,
|
|
name text NOT NULL,
|
|
description text,
|
|
status text NOT NULL DEFAULT 'open', -- open | closed
|
|
exclude_iters jsonb NOT NULL DEFAULT '["reference"]'::jsonb,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
created_by text,
|
|
closed_at timestamptz,
|
|
closed_by text,
|
|
UNIQUE (source_id, name)
|
|
);
|
|
|
|
CREATE TABLE pf.log (
|
|
id bigserial PRIMARY KEY,
|
|
version_id integer NOT NULL REFERENCES pf.version(id) ON DELETE CASCADE,
|
|
pf_user text NOT NULL,
|
|
stamp timestamptz NOT NULL DEFAULT now(),
|
|
operation text NOT NULL, -- baseline | reference | scale | recode | clone
|
|
slice jsonb,
|
|
params jsonb,
|
|
note text
|
|
);
|
|
|
|
-- generated operation SQL per source, stored after col_meta is configured
|
|
CREATE TABLE pf.sql (
|
|
id serial PRIMARY KEY,
|
|
source_id integer NOT NULL REFERENCES pf.source(id) ON DELETE CASCADE,
|
|
operation text NOT NULL, -- get_data | get_agg | baseline | reference | scale | recode | clone | undo
|
|
sql text NOT NULL,
|
|
generated_at timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE (source_id, operation)
|
|
);
|