04 · cleanup executor dry-run — fn_cut_cleanup_dry_run
04 · Cleanup executor dry-run — fn_cut_cleanup_dry_run
Goal
cut_request.cleanup_scheduled_at is set when a cut completes
(mig 052 default: now() + interval '15 days'), but no executor exists
yet to act on that timestamp. Before batch cutting, operators need:
- visibility into which cut_request rows are past their TTL;
- a clearly-labeled dry-run-only surface (no DELETE) until a future apply gate is designed;
- adjustable lookback window for forward-looking eligibility planning.
The pre-existing public.fn_iu_op_cleanup_dry_run(int,text) operates on
iu_core.iu_staging_record via fn_iu_staging_cleanup — that's staging
cleanup, not cut_request cleanup. They serve different layers
and must be kept distinct so a staging hygiene run does not mutate the
cut_request audit timeline.
Surface
public.fn_cut_cleanup_dry_run(
p_older_than_days int DEFAULT 15,
p_actor text DEFAULT 'operator'
) RETURNS jsonb
LANGUAGE plpgsql
STABLE;
Granted to workflow_admin only. Marked STABLE so the planner knows it
performs no writes (defense-in-depth alongside the "no DELETE" promise).
Semantics
v_cutoff := now() - (p_older_than_days::text || ' days')::interval;
SELECT ... FROM public.cut_request
WHERE status = 'cleanup_scheduled'
AND cleanup_scheduled_at IS NOT NULL
AND cleanup_scheduled_at <= v_cutoff
ORDER BY cleanup_scheduled_at;
cleanup_scheduled_at <= v_cutoff means "scheduled at least
p_older_than_days days ago." For a cut_request scheduled
now() + 15d, today's call with default p_older_than_days=15 produces
cutoff = now() - 15d, which is roughly 30 days before
cleanup_scheduled_at — so today's eligible count is 0. This matches
the operator's intent: "rows whose cleanup_scheduled_at has already
passed by ≥15 days" — and is robust to clock skew on the operator side.
Return shape
{
"alias": "fn_cut_cleanup_dry_run",
"apply": false,
"older_than_days": 15,
"cutoff_at": "2026-05-11T17:18:01.508851+00:00",
"eligible_count": 0,
"candidates": [
{
"cut_request_id": "<uuid>",
"status": "cleanup_scheduled",
"source_ref": "<text>",
"cleanup_scheduled_at": "<timestamptz>",
"cut_run_id": "<uuid>",
"completed_at": "<timestamptz>",
"eligible_action": "cleanup_dry_run_only_no_delete"
}
],
"actor": "operator",
"note": "Dry-run only. No DELETE. Apply executor not implemented; future explicit apply gate required."
}
Proofs
| proof | input | expected | actual |
|---|---|---|---|
| PD.1 | (15, 'proof_pd1') against Điều 38 cut_request (scheduled 2026-06-10) |
eligible_count=0 |
match (cutoff 2026-05-11) |
| PD.2 | (1, 'proof_pd2') |
eligible_count=0 (still future) |
match (cutoff 2026-05-25) |
| PD.3 | (0, …) |
RAISE must be >= 1 |
match |
| E.5 LIVE | (15, 'phase_e_regression') post-COMMIT |
eligible_count=0 |
match |
| E.6 LIVE | (-1, …) |
RAISE | match |
| D31.7 | re-run PD.1 | eligible_count=0, no row deleted |
confirmed: cut_request_post_d31_7_count=1 (same as pre) |
Hypothetical scenario — when does Điều 38 become eligible?
cleanup_scheduled_at = 2026-06-10 16:41:34.629456+00. With default
p_older_than_days=15, the row becomes eligible when
now() >= 2026-06-10 16:41:34 + 15d = 2026-06-25 16:41:34.
So the dry-run will report 1 eligible candidate starting 2026-06-25. Operators can pre-stage the cleanup apply executor design in the intervening 4 weeks.
What this function is NOT
- Not an apply executor. It does not DELETE, does not transition
status, does not write to any table. It is
STABLEand idempotent. - Not a replacement for
fn_iu_op_cleanup_dry_run. The two surfaces cover disjoint layers:fn_iu_op_cleanup_dry_run→iu_core.iu_staging_recordrowsfn_cut_cleanup_dry_run→public.cut_requestrows
- Not a generic TTL framework. It hard-codes
status='cleanup_scheduled'to avoid conflating already-completed requests with mid-flight ones.
CF-1 (carry-forward)
The apply executor is the next design step. Open questions:
- Should "cleanup" mean
DELETE FROM cut_requestorUPDATE cut_request SET status='cleaned' ... RETURNING old? - Does cleanup cascade to
cut_request_transition(audit trail)? Recommended: NO — keep transition timeline as forever-audit; only thecut_requestrow itself is anonymized/deleted. - Does cleanup cascade to
iu_staging_recordreferenced bycopy_staging_record_idandmanifest_staging_record_id? Probably yes (these are intermediate, not durable audit). Already governed byfn_iu_staging_cleanup. - Gate name:
queue.cut_cleanup.apply_enabled(default false). - Should apply require approval doc id like
fn_cut_verify_markdoes? Recommended: yes — anonymizing a completed cut_request is irreversible and deserves a paper trail.
These are deliberately deferred. The dry-run function is the only production-safe addition this pack delivers for cut_request TTL.