feat(t-paliad-209): mig 098 prefix submission codes + rename code → submission_code
m's 2026-05-18 call (workstream B): the paliad.deadline_rules.code field
is a SUBMISSION identifier (the filing/event within a proceeding), not
the legal-citation rule code (which lives in rule_code / legal_source).
Two cleanups land in this migration:
1. DATA — prefix every existing submission code with its proceeding
code so submission codes carry the full hierarchical shape:
inf.soc (on upc.inf.cfi) → upc.inf.cfi.soc
de_inf.klage (on de.inf.lg) → de.inf.lg.klage
de_inf_bgh.revision (on de.inf.bgh) → de.inf.bgh.revision
Idempotent: WHERE NOT LIKE pt.code || '.%' skips already-prefixed
rows so re-running is a no-op.
2. SCHEMA — rename paliad.deadline_rules.code → submission_code so
future devs don't conflate it with rule_code (legal citation) or
proceeding_types.code. The rename is guarded by a column-existence
check, idempotent on a second run.
Drops + recreates the deadline_search materialized view because its
SELECT bakes `dr.code AS rule_local_code` (mig 051 §4); the rebuild
sources from `dr.submission_code` and reproduces every index from mig
051 verbatim.
Backup snapshot table paliad.deadline_rules_pre_098 captures the rows
before the prefix step; serves as the audit anchor and the down's
source.
Hard assertions (§6) gate the migration on:
- every active+published row matches the 4+-segment proceeding-prefixed
shape regex
- no NULL submission_code on active+published rows
- the column was actually renamed
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
-- Reverses mig 098. Restores the pre-098 submission codes on
|
||||
-- paliad.deadline_rules, renames the column back to `code`, recreates
|
||||
-- the deadline_search matview against the restored column, then drops
|
||||
-- the snapshot table.
|
||||
--
|
||||
-- audit_reason wrapper required by the mig 079 audit trigger.
|
||||
|
||||
SELECT set_config(
|
||||
'paliad.audit_reason',
|
||||
'mig 098 (down): revert t-paliad-209 workstream B — restore paliad.deadline_rules.code values from deadline_rules_pre_098 snapshot and rename submission_code → code; matview deadline_search rebuilt against the restored column.',
|
||||
true);
|
||||
|
||||
-- =============================================================================
|
||||
-- 1. Drop the matview so the column rename can succeed.
|
||||
-- =============================================================================
|
||||
|
||||
DROP MATERIALIZED VIEW IF EXISTS paliad.deadline_search;
|
||||
|
||||
-- =============================================================================
|
||||
-- 2. Rename the column back. Guarded so a down run on a DB where the
|
||||
-- up never ran (or where the column is already named `code`) is a
|
||||
-- no-op rather than an error.
|
||||
-- =============================================================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'paliad'
|
||||
AND table_name = 'deadline_rules'
|
||||
AND column_name = 'submission_code'
|
||||
) THEN
|
||||
ALTER TABLE paliad.deadline_rules
|
||||
RENAME COLUMN submission_code TO code;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- =============================================================================
|
||||
-- 3. Restore code values from the pre_098 snapshot. The snapshot was
|
||||
-- captured at the first up-migration run; if the table is missing
|
||||
-- (down run before up), the restore is a no-op.
|
||||
-- =============================================================================
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
v_snap_exists boolean;
|
||||
BEGIN
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'paliad'
|
||||
AND table_name = 'deadline_rules_pre_098'
|
||||
) INTO v_snap_exists;
|
||||
|
||||
IF NOT v_snap_exists THEN
|
||||
RAISE NOTICE
|
||||
'mig 098 (down): snapshot table paliad.deadline_rules_pre_098 missing — nothing to restore';
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
UPDATE paliad.deadline_rules dr
|
||||
SET code = snap.code
|
||||
FROM paliad.deadline_rules_pre_098 snap
|
||||
WHERE dr.id = snap.id
|
||||
AND dr.code <> snap.code;
|
||||
END $$;
|
||||
|
||||
-- =============================================================================
|
||||
-- 4. Recreate the deadline_search matview against the restored column.
|
||||
-- Identical body to mig 051 §4, reproduced here so the down leaves
|
||||
-- the schema in the same shape mig 051 created.
|
||||
-- =============================================================================
|
||||
|
||||
CREATE MATERIALIZED VIEW paliad.deadline_search AS
|
||||
SELECT
|
||||
'rule'::text AS kind,
|
||||
'r:' || dr.id::text AS row_key,
|
||||
dc.id AS concept_id,
|
||||
dc.slug AS concept_slug,
|
||||
dc.name_de AS concept_name_de,
|
||||
dc.name_en AS concept_name_en,
|
||||
dc.description AS concept_description,
|
||||
dc.aliases AS concept_aliases,
|
||||
dc.party AS concept_party,
|
||||
dc.category AS concept_category,
|
||||
dc.sort_order AS concept_sort_order,
|
||||
dr.id AS rule_id,
|
||||
NULL::bigint AS trigger_event_id,
|
||||
pt.code AS proceeding_code,
|
||||
pt.name AS proceeding_name_de,
|
||||
pt.name_en AS proceeding_name_en,
|
||||
pt.jurisdiction AS jurisdiction,
|
||||
pt.display_order AS proceeding_display_order,
|
||||
dr.code AS rule_local_code,
|
||||
dr.name AS rule_name_de,
|
||||
dr.name_en AS rule_name_en,
|
||||
dr.legal_source AS legal_source,
|
||||
dr.rule_code AS rule_code,
|
||||
dr.duration_value,
|
||||
dr.duration_unit,
|
||||
dr.timing,
|
||||
COALESCE(dr.primary_party, dc.party) AS effective_party
|
||||
FROM paliad.deadline_rules dr
|
||||
JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id
|
||||
JOIN paliad.deadline_concepts dc ON dc.id = dr.concept_id
|
||||
WHERE dr.is_active
|
||||
AND pt.is_active
|
||||
AND pt.category = 'fristenrechner'
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'trigger'::text,
|
||||
't:' || te.id::text,
|
||||
dc.id,
|
||||
dc.slug,
|
||||
dc.name_de,
|
||||
dc.name_en,
|
||||
dc.description,
|
||||
dc.aliases,
|
||||
dc.party,
|
||||
dc.category,
|
||||
dc.sort_order,
|
||||
NULL::uuid,
|
||||
te.id,
|
||||
NULL::text,
|
||||
NULL::text,
|
||||
NULL::text,
|
||||
'cross-cutting'::text,
|
||||
9999::int AS proceeding_display_order,
|
||||
te.code,
|
||||
te.name_de,
|
||||
te.name,
|
||||
NULL::text,
|
||||
NULL::text,
|
||||
NULL::int,
|
||||
NULL::text,
|
||||
NULL::text,
|
||||
dc.party
|
||||
FROM paliad.trigger_events te
|
||||
JOIN paliad.deadline_concepts dc ON dc.slug = te.concept_id
|
||||
WHERE te.is_active;
|
||||
|
||||
CREATE UNIQUE INDEX deadline_search_row_key ON paliad.deadline_search (row_key);
|
||||
CREATE INDEX deadline_search_concept_id ON paliad.deadline_search (concept_id);
|
||||
CREATE INDEX deadline_search_proc_code ON paliad.deadline_search (proceeding_code);
|
||||
CREATE INDEX deadline_search_legal_source ON paliad.deadline_search (legal_source);
|
||||
CREATE INDEX deadline_search_effective_party ON paliad.deadline_search (effective_party);
|
||||
CREATE INDEX deadline_search_legal_source_trgm ON paliad.deadline_search USING gin (legal_source gin_trgm_ops);
|
||||
CREATE INDEX deadline_search_concept_de_trgm ON paliad.deadline_search USING gin (concept_name_de gin_trgm_ops);
|
||||
CREATE INDEX deadline_search_concept_en_trgm ON paliad.deadline_search USING gin (concept_name_en gin_trgm_ops);
|
||||
CREATE INDEX deadline_search_rule_de_trgm ON paliad.deadline_search USING gin (rule_name_de gin_trgm_ops);
|
||||
CREATE INDEX deadline_search_rule_en_trgm ON paliad.deadline_search USING gin (rule_name_en gin_trgm_ops);
|
||||
CREATE INDEX deadline_search_rule_code_trgm ON paliad.deadline_search USING gin (rule_code gin_trgm_ops);
|
||||
|
||||
-- =============================================================================
|
||||
-- 5. Drop the snapshot table so a re-applied up captures a fresh
|
||||
-- snapshot of the current state.
|
||||
-- =============================================================================
|
||||
|
||||
DROP TABLE IF EXISTS paliad.deadline_rules_pre_098;
|
||||
@@ -0,0 +1,268 @@
|
||||
-- t-paliad-209 / workstream B — submission-code prefix + rename.
|
||||
--
|
||||
-- m's 2026-05-18 call: the `paliad.deadline_rules.code` field is a
|
||||
-- SUBMISSION identifier (the event/filing within a proceeding), not the
|
||||
-- legal-citation rule code (which lives in `rule_code` / `legal_source`).
|
||||
-- Two cleanups land here:
|
||||
--
|
||||
-- 1. DATA — prefix every existing submission code with its proceeding
|
||||
-- code so submission codes carry the full hierarchical shape
|
||||
-- (e.g. `inf.soc` on `upc.inf.cfi` → `upc.inf.cfi.soc`,
|
||||
-- `de_inf.klage` on `de.inf.lg` → `de.inf.lg.klage`).
|
||||
-- Algorithm: keep the proceeding-code prefix as-is, strip the
|
||||
-- old single-segment prefix (everything before the first dot in
|
||||
-- `dr.code`) and replace it with the proceeding's full `code`.
|
||||
--
|
||||
-- 2. SCHEMA — rename `paliad.deadline_rules.code` → `submission_code`
|
||||
-- so future devs don't conflate it with `rule_code` (legal
|
||||
-- citation) or `proceeding_types.code`. Explicit name encodes the
|
||||
-- semantic taxonomy ratified in
|
||||
-- docs/design-proceeding-code-taxonomy-2026-05-18.md §0.1.
|
||||
--
|
||||
-- Materialized-view dependency: `paliad.deadline_search` (mig 051) has
|
||||
-- `dr.code AS rule_local_code` baked into its SELECT list. Postgres
|
||||
-- rejects RENAME COLUMN when a matview's column list still resolves
|
||||
-- via the old name — so the matview is dropped before the rename and
|
||||
-- recreated against `submission_code` afterwards, with every index
|
||||
-- reproduced. The mig 047 / 051 indexes are reproduced verbatim here.
|
||||
--
|
||||
-- IDs and FKs are untouched. `deadline_rules.proceeding_type_id` /
|
||||
-- `parent_id` / `spawn_proceeding_type_id` reference ids; no
|
||||
-- code-string FK exists on submission codes (the parent_id chain is on
|
||||
-- UUID `id`, not the code string), so the data UPDATE doesn't risk
|
||||
-- breaking joins.
|
||||
--
|
||||
-- Idempotent:
|
||||
-- * The data UPDATE is gated `WHERE dr.code NOT LIKE pt.code || '.%'`
|
||||
-- — rows already prefixed with their proceeding code (i.e. the
|
||||
-- migration ran before) are skipped.
|
||||
-- * The rename is wrapped in a DO block that checks column existence,
|
||||
-- so a second run is a no-op.
|
||||
-- * Snapshot table uses CREATE TABLE IF NOT EXISTS.
|
||||
-- * Matview drop/recreate is DROP IF EXISTS + CREATE.
|
||||
--
|
||||
-- audit_reason wrapper required by the mig 079 audit trigger.
|
||||
|
||||
SELECT set_config(
|
||||
'paliad.audit_reason',
|
||||
'mig 098: t-paliad-209 workstream B — prefix every paliad.deadline_rules.code with its proceeding code, then rename code → submission_code; matview deadline_search rebuilt against the new column. See docs/design-proceeding-code-taxonomy-2026-05-18.md and the t-paliad-209 task brief.',
|
||||
true);
|
||||
|
||||
-- =============================================================================
|
||||
-- 1. Backup snapshot of paliad.deadline_rules BEFORE the prefix + rename.
|
||||
-- Captures the rows as they are; serves as the source for the down
|
||||
-- migration and the permanent audit anchor.
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS paliad.deadline_rules_pre_098 AS
|
||||
SELECT *, now() AS snapshotted_at
|
||||
FROM paliad.deadline_rules;
|
||||
|
||||
COMMENT ON TABLE paliad.deadline_rules_pre_098 IS
|
||||
'Snapshot of paliad.deadline_rules taken before mig 098 prefixed '
|
||||
'every `code` with its proceeding code and renamed the column to '
|
||||
'`submission_code` (t-paliad-209, 2026-05-18). Source-of-truth '
|
||||
'for the down migration; persists post-rename as the permanent '
|
||||
'audit record.';
|
||||
|
||||
-- =============================================================================
|
||||
-- 2. Drop the deadline_search materialized view. It bakes `dr.code AS
|
||||
-- rule_local_code` into its SELECT list (mig 051 §4), and Postgres
|
||||
-- refuses to rename a column that a matview's column list still
|
||||
-- resolves via the old name. The matview is recreated verbatim in §5
|
||||
-- against the renamed column.
|
||||
-- =============================================================================
|
||||
|
||||
DROP MATERIALIZED VIEW IF EXISTS paliad.deadline_search;
|
||||
|
||||
-- =============================================================================
|
||||
-- 3. Data UPDATE — prefix every submission code with its proceeding
|
||||
-- code. Algorithm:
|
||||
-- * proceeding_code = pt.code
|
||||
-- * suffix = portion of dr.code after the first '.'
|
||||
-- * new code = proceeding_code || '.' || suffix
|
||||
--
|
||||
-- regexp_replace('inf.soc', '^[^.]+\.', '') = 'soc'
|
||||
-- regexp_replace('de_inf_bgh.revision', ...) = 'revision'
|
||||
--
|
||||
-- The WHERE clause skips rows that already start with `pt.code || '.'`
|
||||
-- so re-running the migration is a no-op on already-prefixed rows.
|
||||
-- Archived rows (proceeding `_archived_litigation`) get the same
|
||||
-- treatment — they end up as `_archived_litigation.<suffix>`. The
|
||||
-- shape regex in §6 only inspects active+published rows, so the
|
||||
-- archived form sits outside the constraint by design.
|
||||
-- =============================================================================
|
||||
|
||||
UPDATE paliad.deadline_rules dr
|
||||
SET code = pt.code || '.' || regexp_replace(dr.code, '^[^.]+\.', '')
|
||||
FROM paliad.proceeding_types pt
|
||||
WHERE pt.id = dr.proceeding_type_id
|
||||
AND dr.code IS NOT NULL
|
||||
AND position('.' in dr.code) > 0
|
||||
AND dr.code NOT LIKE pt.code || '.%';
|
||||
|
||||
-- =============================================================================
|
||||
-- 4. Rename the column. Guarded in a DO block so a second run (e.g. a
|
||||
-- fresh DB built up to mig 098 from an empty schema, or a manual
|
||||
-- re-apply) is a no-op rather than a hard error.
|
||||
-- =============================================================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'paliad'
|
||||
AND table_name = 'deadline_rules'
|
||||
AND column_name = 'code'
|
||||
) THEN
|
||||
ALTER TABLE paliad.deadline_rules
|
||||
RENAME COLUMN code TO submission_code;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- =============================================================================
|
||||
-- 5. Recreate the deadline_search matview against the renamed column.
|
||||
-- Column list reproduced verbatim from mig 051 §4 with the single
|
||||
-- edit: `dr.code AS rule_local_code` → `dr.submission_code AS
|
||||
-- rule_local_code`. All indexes from mig 051 are reproduced too.
|
||||
-- =============================================================================
|
||||
|
||||
CREATE MATERIALIZED VIEW paliad.deadline_search AS
|
||||
SELECT
|
||||
'rule'::text AS kind,
|
||||
'r:' || dr.id::text AS row_key,
|
||||
dc.id AS concept_id,
|
||||
dc.slug AS concept_slug,
|
||||
dc.name_de AS concept_name_de,
|
||||
dc.name_en AS concept_name_en,
|
||||
dc.description AS concept_description,
|
||||
dc.aliases AS concept_aliases,
|
||||
dc.party AS concept_party,
|
||||
dc.category AS concept_category,
|
||||
dc.sort_order AS concept_sort_order,
|
||||
dr.id AS rule_id,
|
||||
NULL::bigint AS trigger_event_id,
|
||||
pt.code AS proceeding_code,
|
||||
pt.name AS proceeding_name_de,
|
||||
pt.name_en AS proceeding_name_en,
|
||||
pt.jurisdiction AS jurisdiction,
|
||||
pt.display_order AS proceeding_display_order,
|
||||
dr.submission_code AS rule_local_code,
|
||||
dr.name AS rule_name_de,
|
||||
dr.name_en AS rule_name_en,
|
||||
dr.legal_source AS legal_source,
|
||||
dr.rule_code AS rule_code,
|
||||
dr.duration_value,
|
||||
dr.duration_unit,
|
||||
dr.timing,
|
||||
COALESCE(dr.primary_party, dc.party) AS effective_party
|
||||
FROM paliad.deadline_rules dr
|
||||
JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id
|
||||
JOIN paliad.deadline_concepts dc ON dc.id = dr.concept_id
|
||||
WHERE dr.is_active
|
||||
AND pt.is_active
|
||||
AND pt.category = 'fristenrechner'
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'trigger'::text,
|
||||
't:' || te.id::text,
|
||||
dc.id,
|
||||
dc.slug,
|
||||
dc.name_de,
|
||||
dc.name_en,
|
||||
dc.description,
|
||||
dc.aliases,
|
||||
dc.party,
|
||||
dc.category,
|
||||
dc.sort_order,
|
||||
NULL::uuid,
|
||||
te.id,
|
||||
NULL::text,
|
||||
NULL::text,
|
||||
NULL::text,
|
||||
'cross-cutting'::text,
|
||||
9999::int AS proceeding_display_order,
|
||||
te.code,
|
||||
te.name_de,
|
||||
te.name,
|
||||
NULL::text,
|
||||
NULL::text,
|
||||
NULL::int,
|
||||
NULL::text,
|
||||
NULL::text,
|
||||
dc.party
|
||||
FROM paliad.trigger_events te
|
||||
JOIN paliad.deadline_concepts dc ON dc.slug = te.concept_id
|
||||
WHERE te.is_active;
|
||||
|
||||
CREATE UNIQUE INDEX deadline_search_row_key ON paliad.deadline_search (row_key);
|
||||
CREATE INDEX deadline_search_concept_id ON paliad.deadline_search (concept_id);
|
||||
CREATE INDEX deadline_search_proc_code ON paliad.deadline_search (proceeding_code);
|
||||
CREATE INDEX deadline_search_legal_source ON paliad.deadline_search (legal_source);
|
||||
CREATE INDEX deadline_search_effective_party ON paliad.deadline_search (effective_party);
|
||||
CREATE INDEX deadline_search_legal_source_trgm ON paliad.deadline_search USING gin (legal_source gin_trgm_ops);
|
||||
CREATE INDEX deadline_search_concept_de_trgm ON paliad.deadline_search USING gin (concept_name_de gin_trgm_ops);
|
||||
CREATE INDEX deadline_search_concept_en_trgm ON paliad.deadline_search USING gin (concept_name_en gin_trgm_ops);
|
||||
CREATE INDEX deadline_search_rule_de_trgm ON paliad.deadline_search USING gin (rule_name_de gin_trgm_ops);
|
||||
CREATE INDEX deadline_search_rule_en_trgm ON paliad.deadline_search USING gin (rule_name_en gin_trgm_ops);
|
||||
CREATE INDEX deadline_search_rule_code_trgm ON paliad.deadline_search USING gin (rule_code gin_trgm_ops);
|
||||
|
||||
-- =============================================================================
|
||||
-- 6. Hard assertions. Half-applied migrations would leave the rule
|
||||
-- corpus inconsistent; gate on the shape of every active+published
|
||||
-- row and on column existence so this fails loudly rather than
|
||||
-- leaving the schema in a half-renamed state.
|
||||
-- =============================================================================
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
v_bad_shape integer;
|
||||
v_null_codes integer;
|
||||
v_col_exists boolean;
|
||||
BEGIN
|
||||
-- 6.1 Every active+published row has the proceeding-code-prefixed
|
||||
-- 4+-segment shape. Archived rows (`_archived_litigation` ones)
|
||||
-- keep their shorter shape by design — they're carved out.
|
||||
SELECT count(*) INTO v_bad_shape
|
||||
FROM paliad.deadline_rules
|
||||
WHERE is_active = true
|
||||
AND lifecycle_state = 'published'
|
||||
AND submission_code !~ '^[a-z_]+\.[a-z_]+\.[a-z_]+\.[a-z_]+(\..*)?$';
|
||||
IF v_bad_shape <> 0 THEN
|
||||
RAISE EXCEPTION
|
||||
'mig 098: expected every active+published deadline_rules row to match the 4+-segment submission_code shape, got % violators',
|
||||
v_bad_shape;
|
||||
END IF;
|
||||
|
||||
-- 6.2 No NULL submission_code on active+published rows. The column
|
||||
-- is nullable for legacy reasons, but every live row should
|
||||
-- carry a code after the prefix step.
|
||||
SELECT count(*) INTO v_null_codes
|
||||
FROM paliad.deadline_rules
|
||||
WHERE is_active = true
|
||||
AND lifecycle_state = 'published'
|
||||
AND submission_code IS NULL;
|
||||
IF v_null_codes <> 0 THEN
|
||||
RAISE EXCEPTION
|
||||
'mig 098: expected 0 NULL submission_code on active+published rows, got %',
|
||||
v_null_codes;
|
||||
END IF;
|
||||
|
||||
-- 6.3 Column was actually renamed. Catches the case where the DO
|
||||
-- guard in §4 short-circuited because the schema hadn't yet
|
||||
-- been migrated.
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'paliad'
|
||||
AND table_name = 'deadline_rules'
|
||||
AND column_name = 'submission_code'
|
||||
) INTO v_col_exists;
|
||||
IF NOT v_col_exists THEN
|
||||
RAISE EXCEPTION
|
||||
'mig 098: column paliad.deadline_rules.submission_code missing after rename — half-applied migration';
|
||||
END IF;
|
||||
END $$;
|
||||
Reference in New Issue
Block a user