Recovery during the prod outage uncovered a second mig 098 bug: §6.2 assertion '0 NULL submission_code on active+published rows' counted the 77 orphan rules (proceeding_type_id IS NULL, cross-cutting Wiedereinsetzung / Schriftsatznachreichung pattern) and rejected the migration. Patch: gate the NULL count on `proceeding_type_id IS NOT NULL` so orphans pass through. Migration already applied to prod via manual recovery with the same patched assertion; this commit aligns the in-repo file with the deployed state.
276 lines
12 KiB
SQL
276 lines
12 KiB
SQL
-- 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.
|
|
-- Suffix segments may include digits (existing data — e.g. EPA rule
|
|
-- codes like `epa.opp.boa.r106` / `epa.grant.exa.r71_3` carry the
|
|
-- statutory rule number in the suffix). Allow [a-z_0-9] per segment.
|
|
SELECT count(*) INTO v_bad_shape
|
|
FROM paliad.deadline_rules
|
|
WHERE is_active = true
|
|
AND lifecycle_state = 'published'
|
|
AND submission_code !~ '^[a-z_0-9]+\.[a-z_0-9]+\.[a-z_0-9]+\.[a-z_0-9]+(\..*)?$';
|
|
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 that BELONG
|
|
-- to a proceeding. Orphan rows (`proceeding_type_id IS NULL`)
|
|
-- are cross-cutting rules without a fixed proceeding home
|
|
-- (Wiedereinsetzung, Schriftsatznachreichung, etc.) — they
|
|
-- legitimately carry NULL submission_code because there's no
|
|
-- proceeding to prefix with. Exempt them.
|
|
SELECT count(*) INTO v_null_codes
|
|
FROM paliad.deadline_rules
|
|
WHERE is_active = true
|
|
AND lifecycle_state = 'published'
|
|
AND proceeding_type_id IS NOT NULL
|
|
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 $$;
|