From 86b81996121d9a4d9053e1153738e44936262402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Mon, 21 Sep 2026 15:00:10 -0300 Subject: [PATCH] fix: create indexes after the tables they name The 4.1 indexes were added beside the existing uploads_owner, which sits partway through schema.sql, so CREATE INDEX ... ON dtf_local.order_files ran before that table was created and bootstrap aborted with UndefinedTable. db-init then restarted on failure without ever completing, and everything waiting on it timed out. Every local run passed because those volumes already had the tables. Only a clean database exposes it, which is what CI has and my checks did not. All eleven indexes now sit at the end of the file, after every table, with an assertion in the change that each indexed table is created before its index. Verified from docker compose down -v: the stack starts, bootstrap completes, eleven indexes exist, and the full suite passes. Recorded as ROADMAP 5.11: nothing exercises the schema against an empty database, which is the only way this class of fault appears. Co-Authored-By: Claude Opus 5 --- ROADMAP.md | 8 ++++++++ local/schema.sql | 31 ++++++++++++++++--------------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 2e132a7..bba49b4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -432,6 +432,14 @@ charges. Fix as part of 1.1. table and the packer so 3.2 has somewhere to land. - `[ ]` 5.7 — Commercial rules duplicated between `FAIXAS` (JS) and `TIERS` (Python) `(F26)`. `test_pricing` guards parity; generate one from the other instead. +- `[ ]` 5.11 — Nothing tests the schema against an empty database. The 4.1 indexes + were added next to the existing one, which sits before the tables they name, so + `CREATE INDEX ... ON dtf_local.order_files` ran before that table existed. Every + local run passed because the volume already had the tables; CI caught it on its + clean volume. A migration is only really exercised from nothing, so the + integration job should run `down -v` before `up` — or a dedicated step should + apply `schema.sql` twice to a fresh database, proving both a first install and + a re-run. - `[ ]` 5.10 — The browser suites do not run in CI. Chrome runs in the runner container and can only reach the stack through ports published on the host, which is a different network namespace when the runner is itself a container. The API, diff --git a/local/schema.sql b/local/schema.sql index 6ef21c3..a4414a5 100644 --- a/local/schema.sql +++ b/local/schema.sql @@ -68,6 +68,22 @@ CREATE TABLE IF NOT EXISTS dtf_local.security_events ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, event text NOT NULL, details jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); +ALTER TABLE dtf_local.uploads ADD COLUMN IF NOT EXISTS expires_at timestamptz; +ALTER TABLE dtf_local.uploads ADD COLUMN IF NOT EXISTS purged_at timestamptz; +ALTER TABLE dtf_local.uploads ADD COLUMN IF NOT EXISTS scan_state text NOT NULL DEFAULT 'pending'; +ALTER TABLE dtf_local.uploads ADD COLUMN IF NOT EXISTS scan_reason text; +ALTER TABLE dtf_local.uploads ADD COLUMN IF NOT EXISTS scanned_at timestamptz; +ALTER TABLE dtf_local.uploads ADD COLUMN IF NOT EXISTS scan_after timestamptz NOT NULL DEFAULT now(); +UPDATE dtf_local.uploads SET expires_at=created_at + interval '30 days' WHERE expires_at IS NULL; +ALTER TABLE dtf_local.uploads ALTER COLUMN expires_at SET DEFAULT now() + interval '30 days'; +CREATE TABLE IF NOT EXISTS dtf_local.order_files ( + id uuid PRIMARY KEY, order_id uuid NOT NULL REFERENCES dtf_local.orders(id), + upload_id uuid NOT NULL REFERENCES dtf_local.uploads(id), item_index integer NOT NULL, + kind text NOT NULL CHECK(kind IN ('final','correction')), active boolean NOT NULL DEFAULT true, + note text NOT NULL, created_by text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), + UNIQUE(order_id,upload_id,kind) +); + CREATE INDEX IF NOT EXISTS uploads_owner ON dtf_local.uploads(owner); -- Indexes follow the queries the application actually issues. Only these; every @@ -102,18 +118,3 @@ CREATE INDEX IF NOT EXISTS operator_sessions_username ON dtf_local.operator_sess -- security_status reads recent events; the worker prunes old ones by age. CREATE INDEX IF NOT EXISTS security_events_created ON dtf_local.security_events(created_at); -ALTER TABLE dtf_local.uploads ADD COLUMN IF NOT EXISTS expires_at timestamptz; -ALTER TABLE dtf_local.uploads ADD COLUMN IF NOT EXISTS purged_at timestamptz; -ALTER TABLE dtf_local.uploads ADD COLUMN IF NOT EXISTS scan_state text NOT NULL DEFAULT 'pending'; -ALTER TABLE dtf_local.uploads ADD COLUMN IF NOT EXISTS scan_reason text; -ALTER TABLE dtf_local.uploads ADD COLUMN IF NOT EXISTS scanned_at timestamptz; -ALTER TABLE dtf_local.uploads ADD COLUMN IF NOT EXISTS scan_after timestamptz NOT NULL DEFAULT now(); -UPDATE dtf_local.uploads SET expires_at=created_at + interval '30 days' WHERE expires_at IS NULL; -ALTER TABLE dtf_local.uploads ALTER COLUMN expires_at SET DEFAULT now() + interval '30 days'; -CREATE TABLE IF NOT EXISTS dtf_local.order_files ( - id uuid PRIMARY KEY, order_id uuid NOT NULL REFERENCES dtf_local.orders(id), - upload_id uuid NOT NULL REFERENCES dtf_local.uploads(id), item_index integer NOT NULL, - kind text NOT NULL CHECK(kind IN ('final','correction')), active boolean NOT NULL DEFAULT true, - note text NOT NULL, created_by text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), - UNIQUE(order_id,upload_id,kind) -);