Skip to content

History-free pre_pack: delete an object's old refs rows before inserting (fixes #482) - #533

Open
djay wants to merge 1 commit into
zodb:masterfrom
djay:fix-482-prepack-refs-added-order
Open

djay wants to merge 1 commit into
zodb:masterfrom
djay:fix-482-prepack-refs-added-order

Conversation

@djay

@djay djay commented Oct 12, 2026

Copy link
Copy Markdown

Fixes #482 (and its duplicates #448, #493): history-free zodbpack failing in pre-pack with

psycopg2.errors.UniqueViolation: duplicate key value violates unique constraint "object_refs_added_pkey"
DETAIL:  Key (zoid)=(949915) already exists.

Cause

HistoryFreePackUndo._add_refs_for_oids queues, per object, the object_refs_added INSERT first and then the DELETEs of that object's old object_refs_added / object_ref rows. RowBatcher.flush() runs deletes before inserts, but only within one flush, and insert_into() calls _flush_if_needed() straight away. When that insert is the row that reaches row_limit (or the size or bind limit), the batch is flushed with the insert but without the delete, which hasn't been queued yet. The insert then hits the object's existing row.

An object has an existing row whenever it changed since its references were last read, because object_refs_added is kept between packs. That explains the pattern reported on #482: truncating the table makes packs work again, then the failure returns a few days (or hours) later, at whichever object happens to land on a batch boundary.

The history-preserving _add_refs_for_tid already queues its delete first.

Fix

Queue the two deletes before the object_refs_added insert, so a flush triggered by the insert always carries the deletes for that object.

Tests

src/relstorage/adapters/tests/test_packundo.py drives _add_refs_for_oids through a real RowBatcher with row_limit=1 (every queued row flushes) over MockCursor, and asserts each table's DELETE executes before its INSERT. Before the fix:

AssertionError: 1 not less than 0 : ['INSERT INTO object_refs_added', 'DELETE FROM object_refs_added', 'DELETE FROM object_ref', 'INSERT INTO object_ref']

After the fix, both tests pass, along with relstorage.adapters.tests.test_batch and relstorage.tests.test_zodbpack (38 tests) and the SQLite pack tests (--package relstorage.adapters.sqlite -t Pack: 80 tests, 0 failures, 14 skipped). I ran them against the 4.3.0 wheel with the patched packundo.py; master's packundo.py is identical to 4.3.0.

Found on a production history-free PostgreSQL database (RelStorage 4.1.1, ~855k objects), whose object_refs_added still held 2.67M rows from an earlier interrupted pack.

_add_refs_for_oids queued the object_refs_added INSERT before the DELETEs
of the object's old rows. The batcher runs deletes before inserts only
within a flush, and insert_into can flush immediately, so the insert could
reach the database while the old row was still there: duplicate key value
violates unique constraint "object_refs_added_pkey". Any object changed
since the previous pack can hit it (the table is kept between packs).

Fixes zodb#482.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

Problem packing RelStorage

1 participant