Skip to content

fix(cloud-backups): audit-rotate rename-aside collides across coexisting archives - #6

Merged
logicflakes merged 1 commit into
mainfrom
2026-07-audit-rotate-rename-collision
Jul 20, 2026
Merged

fix(cloud-backups): audit-rotate rename-aside collides across coexisting archives#6
logicflakes merged 1 commit into
mainfrom
2026-07-audit-rotate-rename-collision

Conversation

@logicflakes

Copy link
Copy Markdown
Collaborator

The bug

rotateSQL renames the archived table's constraints/indexes aside so the fresh CREATE ... LIKE can reclaim the canonical names. The rename target derived its suffix from the original (constant) constraint/index name:

RENAME CONSTRAINT audit_pkey TO left('audit_pkey',55) || '_' || substr(md5('audit_pkey'),1,7)  -- always audit_pkey_eb1c437

Every rotation starts from a fresh table whose PK is again audit_pkey, so the suffix is identical each run. When two archives coexist — a --no-drop staging re-run, or a leftover that couldn't be dropped / was quarantined — the second rotation tries to rename its audit_pkey to a name the first archive's constraint already holds:

ERROR: relation "audit_pkey_eb1c437" already exists
CONTEXT: ALTER TABLE rearm.audit_archive_...d2337e60 RENAME CONSTRAINT audit_pkey TO audit_pkey_eb1c437

Rotation then fails every run until the backlog is manually dropped. This defeats the recovery mechanism, whose whole job is to tolerate leftover archives. Hit live on the demo instance during a --no-drop re-run.

Not data-affecting: rotateSQL is a single BEGIN…COMMIT, so the failed rotation rolls back cleanly — the live audit table is untouched (fail-safe worked).

The fix

Derive the rename suffix from the unique archive name (substr(md5(archive),1,8)) instead of the constant original name. Each archive's renamed constraints/indexes are then schema-unique. The renamed names are throwaway (the archive is dropped later); only uniqueness matters. left(name,54)+'_'+8 stays within the 63-byte identifier limit.

Validation

Two consecutive --no-drop runs against Postgres + MinIO now coexist:

audit                                   -> audit_pkey, audit_entity_name_entity_uuid_revision_key   (canonical, reclaimed)
audit_archive_...99060364               -> audit_pkey_3120682c, ..._key_3120682c
audit_archive_...f8c4bc5a               -> audit_pkey_55579c74, ..._key_55579c74

The old code failed the second rotation with the collision above. Unit test asserts the suffix derives from the archive name and differs across archives. build / test / gofmt clean.

🤖 Generated with Claude Code

…ing archives

The rotate step renames the archived table's constraints/indexes aside so the fresh
CREATE ... LIKE can reclaim the canonical names (audit_pkey, ..._key). The rename
target used a suffix derived from the ORIGINAL (constant) name --
`left(conname,55) || '_' || substr(md5(conname),1,7)` -- which is identical on every
rotation (each fresh table's PK is again `audit_pkey`). When two archives coexist
(a --no-drop staging re-run, or a leftover that couldn't be dropped / was
quarantined), the second rotation tries to rename its `audit_pkey` to a name the
first archive's constraint already holds -> `relation "audit_pkey_eb1c437" already
exists`, and rotation fails every run until the backlog is dropped.

Derive the suffix from the UNIQUE archive name instead (`substr(md5(archive),1,8)`),
so each archive's renamed constraints/indexes are schema-unique. The renamed names
are throwaway (the archive is dropped later); only uniqueness matters. left(name,54)
+'_'+8 stays within the 63-byte identifier limit.

Validated: two consecutive --no-drop runs against Postgres+MinIO now coexist as
audit + two archives (audit_pkey_3120682c / audit_pkey_55579c74), where the old
code failed the second rotation. Unit test asserts the suffix derives from the
archive name and differs across archives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ReARM-Agent: 1420896f-adf5-4843-896f-d863cfcc6528
ReARM-Agentic-Session: 1403ccf5-4b2a-4718-850a-3a42d188532d
@logicflakes
logicflakes merged commit 5f36508 into main Jul 20, 2026
9 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant