Timeline unified-audit partition cutover

This runbook governs the forward conversion of an already-converged timeline.unified_audit_events heap to monthly range partitions. The source-side implementation belongs to SPRINT_20260722_020 W3-01. It is not authorization to deploy, merge historical corpora, or drop a retained relation.

The Timeline host applies one embedded startup chain from StellaOps.Timeline.Persistence.Consolidated, in this order:

  1. 000_v1_partition_existing_unified_audit_events.sql converts an existing legacy heap and is a no-op on a fresh database or an already-partitioned parent.
  2. 001_v1_timeline_consolidated_baseline.sql converges the full Timeline schema.
  3. 002_v1_timeline_partition_maintenance.sql installs the recurring rolling-window maintenance functions.

The migration runner executes each file transactionally and records it in timeline.schema_migrations. A failed 000 leaves the legacy heap canonical and does not record the bridge.

Preconditions and read-only preflight

Run the preflight against the exact Timeline owner connection. Confirm the database name is stellaops_timeline, the caller is the Timeline owner role, and the deployed image digest is the approved post-W3-01 artifact. Never substitute a generic or Platform connection.

Capture these facts in the window evidence before starting:

The forward bridge refuses unsupported dependencies, duplicate sequence positions, an unexpected relation kind, an existing rollback name, and target-name collisions. Do not weaken those checks during a window. The migration takes an ACCESS EXCLUSIVE lock, renames the heap, creates the partitioned parent, copies the rows, and proves count plus bidirectional EXCEPT ALL parity before committing.

Window and postflight

  1. Stop or drain Timeline writers and prove that the gateway is no longer accepting audit ingest for this instance.
  2. Take the approved database snapshot or restore point required by ADR-004.
  3. Start the approved Timeline image with only STELLAOPS_POSTGRES_TIMELINE_CONNECTION resolving the owner database.
  4. Wait for startup migration completion. Do not run the embedded SQL manually.
  5. Prove the ledger order is 000, 001, 002 and a second convergence reports no work.
  6. Prove timeline.unified_audit_events is relkind='p', the DEFAULT partition is attached, monthly partitions cover the recorded corpus and rolling window, and row/count/hash-chain parity matches preflight.
  7. Resume ingest and execute the W3-01 forcing action. Prove the new event landed in the expected monthly child, its tenant sequence advanced once, and the event is returned through the Timeline API/gateway.
  8. Execute and record at least two real retention cycles before the rollback heap is eligible for disposal. A dry-run is evidence about purge candidates only and does not count as an executed cycle.

AuditRetentionPurge:DryRun=true is observably read-only: it may query candidate rows, but it performs no partition creation, DEFAULT-row rehoming, pruning, or other schema DDL. An executing cycle maintains three previous months plus the current month and four months ahead, rehomes matching DEFAULT rows transactionally, performs per-row retention while honoring legal holds, and then drops only empty monthly partitions older than the rolling keep window.

Rollback heap retention and disposal gate

timeline.unified_audit_events_heap_rollback is a bounded, temporary rollback surface. It is never a reader, writer, reconciliation source, or second audit corpus. The Timeline doctor reports its presence, bytes, and bridge age.

The approved disposal command must be taken from the window record after the exact relation has been re-verified. This runbook deliberately does not provide a copy-paste DROP command outside that approval context.

Failed conversion recovery

A normal bridge failure rolls its transaction back: the canonical relation remains the original heap, the rollback name is absent, and 000 has no ledger row. Record the exact exception, resolve the named dependency or collision, and rerun startup.

If both canonical and rollback relations exist after an external interruption, or if 000 is recorded while the canonical relation is not a partitioned parent, stop. Do not drop or rename either relation. Capture relation OIDs, kinds, inheritance, counts, bounds, checksums, and the migration ledger, then escalate under the ADR-004 snapshot-restore procedure. The state is outside the transactionally proven shape.

Partition name and bound collision

The maintenance function treats an existing monthly name as valid only when it is attached directly to timeline.unified_audit_events and PostgreSQL renders the exact expected half-open monthly bound. A detached table, a child of another parent, or a differently bounded child fails closed. Record its OID, parent OID, and pg_get_expr(relpartbound, oid) result; disposition it explicitly before retrying. Never infer compatibility from the relation name.

During the initial heap bridge, the new partitioned parent does not yet exist, so any pre-existing target monthly/default name is necessarily ambiguous and the whole transaction fails. Resolve it through the failed-conversion procedure; do not adopt it by name.

Historical corpus two-root decision

The current stellaops_timeline corpus and retained stellaops_platform.timeline corpus both begin the default-tenant sequence at 1. A blind append would produce two roots and duplicate sequence positions while still looking row-complete. W3-01 therefore does not reconcile the historical corpus.

Before any merge, Product/Legal and the evidence owner must choose and document one deterministic protocol:

Until that decision lands, keep the last online legacy corpus read-only and do not copy it into the active heap/parent. Duplicate (tenant_id, sequence_number) causes the bridge to fail closed by design.

Audit event identity across partitions

PostgreSQL requires the physical primary key of a range-partitioned table to include the partition key, so it is (id, tenant_id, timestamp). The public logical identity remains (tenant_id, id), and timestamp is immutable after ingest. The sole writer, PostgresUnifiedAuditEventStore, holds the per-tenant sequence-row lock and checks logical identity across all partitions before insert: an exact-timestamp replay is an idempotent no-op; the same logical ID with a shifted timestamp is rejected and the sequence transaction rolls back. Direct SQL writes bypass this contract and are not a supported ingest path.