Files
paliad/internal/db/migrations/084_backfill_condition_expr.up.sql
mAi 9d73b91e05 feat(t-paliad-183): mig 084 — backfill condition_expr per design §2.4
Phase 3 Slice 2 Step B-3. Convert condition_flag text[] →
condition_expr jsonb per DESIGN §2.4 long form (NOT msg 1746's
short {"and":[...]} form — head clarified in msg 1750 that
design §2.4 wins because long form parses uniformly across
and/or/not, matching what the Slice-4 calculator + Slice-11 rule
editor will emit).

Mapping:
  ['with_ccr']                  →  {"flag":"with_ccr"}              (5 rows)
  ['with_amend']                →  {"flag":"with_amend"}            (4 rows)
  ['with_cci']                  →  {"flag":"with_cci"}              (4 rows)
  ['with_ccr', 'with_amend']    →  {"op":"and","args":[
                                       {"flag":"with_ccr"},
                                       {"flag":"with_amend"}
                                   ]}                                (4 rows)
  NULL or {}                    →  NULL                             (155 rows)

Total translated: 17 rows.

Single-flag is unwrapped (no AND wrapper) per design §2.4 — a
shortcut equivalent to a 1-arg AND that saves a layer of nesting
without losing semantics. The calculator's parser treats
{"flag":"<name>"} as the leaf and {"op":"<and|or|not>","args":[…]}
as the canonical boolean node.

jsonb construction uses jsonb_build_object + a LATERAL unnest…WITH
ORDINALITY over the flag array so args[] order matches the source
array exactly (load-bearing if a future migration adds order-
sensitive ops).

Idempotent via WHERE condition_expr IS NULL — re-running doesn't
double-write audit rows for already-translated rules. Migration
ends with a DO block that RAISE EXCEPTION if any non-empty
condition_flag row still has NULL condition_expr (catches a
broken translation path before it reaches Slice 4).
2026-05-15 00:29:00 +02:00

112 lines
5.0 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- t-paliad-183 / Fristen Phase 3 Slice 2 Step B-3 — backfill
-- paliad.deadline_rules.condition_expr from the legacy
-- condition_flag text[] column per DESIGN §2.4 long form (NOT the
-- short {"and":[...]} form sketched in head's msg 1746 — head's
-- clarification msg 1750 rules in favour of the design doc).
--
-- Mapping (design §2.4):
--
-- condition_flag IS NULL OR array_length(_, 1) = 0
-- → condition_expr stays NULL (unconditional, every rule renders)
--
-- array_length = 1, e.g. ['with_ccr']
-- → condition_expr = jsonb '{"flag": "with_ccr"}'
-- (single flag unwrapped — saves a layer of nesting that
-- parses as the same boolean expression)
--
-- array_length >= 2, e.g. ['with_ccr', 'with_amend']
-- → condition_expr = jsonb '{"op":"and","args":[
-- {"flag":"with_ccr"},
-- {"flag":"with_amend"}
-- ]}'
-- (long form — same shape the rule editor will emit for OR /
-- NOT in future rules so the calculator's parser is uniform)
--
-- Why long form on >=2: the calculator (Slice 4) reads
-- {"op":"<and|or|not>","args":[...]} as the canonical boolean node and
-- {"flag":"<name>"} as the leaf. Single-flag unwrap is a parse-time
-- shortcut equivalent to a 1-arg AND. The short {"and":[...]} form in
-- msg 1746 would require a per-key parser that doesn't generalise to
-- OR / NOT. Design §2.4 long form is the load-bearing decision.
--
-- Live-data expected delta (172 rules total):
--
-- ['with_ccr'] × 5 rows → {"flag":"with_ccr"}
-- ['with_amend'] × 4 rows → {"flag":"with_amend"}
-- ['with_cci'] × 4 rows → {"flag":"with_cci"}
-- ['with_ccr', 'with_amend'] × 4 rows → {"op":"and","args":[
-- {"flag":"with_ccr"},
-- {"flag":"with_amend"}
-- ]}
-- NULL or {} × 155 rows → stays NULL
--
-- Total touched: 17 rows.
--
-- Idempotent: WHERE condition_expr IS NULL guards re-runs against
-- double-writing audit rows for already-translated rules.
--
-- jsonb construction: jsonb_build_object + jsonb_agg + a CASE on
-- array_length keeps the long-form / unwrapped-flag split inline in
-- one UPDATE. Per-flag jsonb leaf is built by a LATERAL unnest over
-- the flag array so the args[] order matches the source array.
SELECT set_config(
'paliad.audit_reason',
'backfill 084: condition_expr from condition_flag text[] per design §2.4 — '
|| 'single flag unwrapped, multi flag long form {op:and, args:[...]}',
true);
UPDATE paliad.deadline_rules dr
SET condition_expr = sub.expr
FROM (
SELECT dr_inner.id AS rule_id,
CASE
-- Single flag: unwrapped leaf.
WHEN array_length(dr_inner.condition_flag, 1) = 1
THEN jsonb_build_object('flag', dr_inner.condition_flag[1])
-- >=2 flags: long-form AND with args[] preserving order.
WHEN array_length(dr_inner.condition_flag, 1) >= 2
THEN jsonb_build_object(
'op', 'and',
'args', (
SELECT jsonb_agg(jsonb_build_object('flag', f) ORDER BY ord)
FROM unnest(dr_inner.condition_flag) WITH ORDINALITY AS u(f, ord)
)
)
-- Empty array (array_length=0) or NULL: leave NULL.
ELSE NULL
END AS expr
FROM paliad.deadline_rules dr_inner
WHERE dr_inner.condition_flag IS NOT NULL
AND array_length(dr_inner.condition_flag, 1) > 0
) AS sub
WHERE dr.id = sub.rule_id
AND dr.condition_expr IS NULL;
DO $$
DECLARE
n_total int;
n_with_flag int;
n_with_expr int;
n_with_both int;
BEGIN
SELECT count(*),
count(*) FILTER (WHERE condition_flag IS NOT NULL AND array_length(condition_flag, 1) > 0),
count(*) FILTER (WHERE condition_expr IS NOT NULL),
count(*) FILTER (WHERE condition_flag IS NOT NULL AND array_length(condition_flag, 1) > 0
AND condition_expr IS NOT NULL)
INTO n_total, n_with_flag, n_with_expr, n_with_both
FROM paliad.deadline_rules;
RAISE NOTICE 'backfill 084: total=%, with_condition_flag=%, with_condition_expr=%, both=%',
n_total, n_with_flag, n_with_expr, n_with_both;
-- Hard assertion: every rule with a non-empty condition_flag now
-- has a non-NULL condition_expr (the inverse of the legacy column).
IF n_with_flag <> n_with_both THEN
RAISE EXCEPTION 'backfill 084: % rules carry condition_flag but no condition_expr — '
'translation incomplete',
n_with_flag - n_with_both;
END IF;
END $$;