KB-7721

04 · cleanup executor dry-run — fn_cut_cleanup_dry_run

6 min read Revision 1
dieu44iu_cutphase_dcleanupttl_15ddry_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 STABLE and idempotent.
  • Not a replacement for fn_iu_op_cleanup_dry_run. The two surfaces cover disjoint layers:
    • fn_iu_op_cleanup_dry_runiu_core.iu_staging_record rows
    • fn_cut_cleanup_dry_runpublic.cut_request rows
  • 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:

  1. Should "cleanup" mean DELETE FROM cut_request or UPDATE cut_request SET status='cleaned' ... RETURNING old?
  2. Does cleanup cascade to cut_request_transition (audit trail)? Recommended: NO — keep transition timeline as forever-audit; only the cut_request row itself is anonymized/deleted.
  3. Does cleanup cascade to iu_staging_record referenced by copy_staging_record_id and manifest_staging_record_id? Probably yes (these are intermediate, not durable audit). Already governed by fn_iu_staging_cleanup.
  4. Gate name: queue.cut_cleanup.apply_enabled (default false).
  5. Should apply require approval doc id like fn_cut_verify_mark does? 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.

Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/v0.6-iu-cut-operational-pipeline-runtime-hardening/04-cleanup-executor-dry-run.md