-- 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":"","args":[...]} as the canonical boolean node and -- {"flag":""} 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 $$;