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:
000_v1_partition_existing_unified_audit_events.sqlconverts an existing legacy heap and is a no-op on a fresh database or an already-partitioned parent.001_v1_timeline_consolidated_baseline.sqlconverges the full Timeline schema.002_v1_timeline_partition_maintenance.sqlinstalls 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 intended source commit and exact image digest. The repository-owned
devops/compose/docker-compose.timeline-image-pin.override.ymlis the W3-01 opt-in selector: append it last only for an authorised service-scoped recreate, render the effectivetimeline-webimage beforeup, and never substitute a mutable tag; timeline.schema_migrationsrows and checksums;pg_class.relkindfortimeline.unified_audit_events(ris the legacy heap,pis an already-partitioned parent);- row count, minimum/maximum timestamp, total bytes, and per-tenant minimum/maximum sequence;
- duplicate
(tenant_id, sequence_number)and duplicate(tenant_id, id)checks; - dependent views/materialized views, inbound or outbound foreign keys, non-internal triggers, RLS policies, and non-owner grants;
- whether
timeline.unified_audit_events_heap_rollbackor any target monthly/default relation already exists; - chain verification for each retained tenant range.
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
- Stop or drain Timeline writers and prove that the gateway is no longer accepting audit ingest for this instance.
- Take the approved database snapshot or restore point required by ADR-004.
- Start the approved Timeline image with only
STELLAOPS_POSTGRES_TIMELINE_CONNECTIONresolving the owner database. - Wait for startup migration completion. Do not run the embedded SQL manually.
- Prove the ledger order is
000,001,002and a second convergence reports no work. - Prove
timeline.unified_audit_eventsisrelkind='p', the DEFAULT partition is attached, monthly partitions cover the recorded corpus and rolling window, and row/count/hash-chain parity matches preflight. - 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.
- 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.
- Minimum retention: 24 hours after the successful cutover and two successful executed retention cycles.
- Maximum retention: 7 days after
000was applied. Crossing this age is a doctor finding, not permission to drop automatically. - Disposal is a separate destructive operator step requiring the recorded W3-01 approval plus successful parity, chain, partition, and live forcing evidence.
- If an owner extends retention, record the reason, approving owner, follow-up sprint, and explicit expiry before day 7. The heap remains non-serving.
- No migration or retention service automatically drops the heap. Do not rename, truncate, or drop it while the historical-corpus decision below is open. On this estate that decision is closed: the owner ruling of 2026-08-24 abandoned the pre-move corpus in place and
stellaops_platform.timelinewas snapshotted and dropped that day, so the clause no longer gates disposal here. The 2026-08-24 bridge’s rollback heap was dropped 2026-09-11 under W3-01’s recorded approval.
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:
- preserve the historical rows as a separately addressed, read-only corpus with an explicit epoch/root identity; or
- define a versioned re-sequencing/re-hashing protocol whose provenance preserves both original roots and whose readers understand that epoch transition.
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.
