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).
112 lines
5.0 KiB
SQL
112 lines
5.0 KiB
SQL
-- 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 $$;
|