Measured on 1.5.0 against PostgreSQL 17.11: a plain events table with two million rows over the last year, swapped under a new parent as its DEFAULT partition the way guide/partition-existing-table.md says, with one writer inserting now() rows through events every 5 ms the whole time. Nothing else is running.
The first tick plans CREATE events__2026_09 / __2026_10 / __2026_11 (create_ahead). The two future months attach; the current month never does:
--- 3. the first tick: this month and two ahead, this month's rows move out of DEFAULT
Failed to attach after reconciliation retries
...
sqlalchemy.exc.IntegrityError: ... CheckViolationError: updated partition constraint for default partition "events_legacy" would be violated by some row
[SQL: ALTER TABLE "public"."events" ATTACH PARTITION "public"."events__2026_09" FOR VALUES FROM ('2026-09-01') TO ('2026-10-01')]
Attach failed after reconciliation; returned rows to DEFAULT partition
created 0, issues 0, error 'IntegrityError: ...'
live rows: 0 in this month's partition, 87 left in DEFAULT
writer: 19 inserts, p50 0.5 ms, longest 604 ms, 2 over 100 ms
The cause is the shape of _attach_with_reconcile (aio/services/execution.py, and the sync twin): the attach fails on the DEFAULT conflict, the window's rows are moved out of DEFAULT in their own transaction, the attach is retried in another. Between the move's commit and the attach's scan the writer has inserted one more row for the window, so the scan finds it and the attach fails again, DEFAULT_CONFLICT_MAX_RETRIES times in a row, and the rows are moved back. Under any steady write rate the current month cannot be attached, which is the one window every live table is writing into, and the state after the tick is the state before it: every insert keeps landing in DEFAULT. The run reports error set and issues empty; each retry costs the writer a full scan of the two-million-row DEFAULT under ACCESS EXCLUSIVE (the 604 ms above).
partition_data reaches the same window last and does worse: it fills the detached events__2026_09, fails the attach the same way, moves the rows back, and then raises the IntegrityError out of partition_data instead of reporting a move issue with complete=False, which is what the guide promises for a window it cannot handle. The eleven older months drained fine before that (lowest count 29764 of 129764 on the oldest month while it was mid-move, as the guide warns).
What I think the fix is: do the move and the attach in one transaction, under the lock the attach takes anyway. ATTACH already needs ACCESS EXCLUSIVE on the DEFAULT sibling for its scan, so BEGIN; LOCK TABLE <default> IN ACCESS EXCLUSIVE MODE; DELETE ... RETURNING / INSERT; ALTER TABLE ... ATTACH PARTITION ...; COMMIT holds the same lock for the move plus the scan and cannot lose the race; a writer waits for that transaction and is then routed to the new partition. The rows also stop needing a compensating move-back for this path, since a failed attach rolls the move back with it. For a large window the move itself can be long, so it may be worth draining most of the window in batches first (which partition_data already does) and taking the locked move-and-attach only for what is left, so the exclusive window is a batch and a scan, not the whole month. partition_data should report an attach it still cannot do as a move issue and return complete=False, never raise. The DEFAULT reconciliation section of concepts/execution.md and the guide's numbered list follow.
Lab: migrate_lab.py in https://github.com/bedrock-python/bedrock-python.github.io/tree/docs/production-python-series/docs/blog/lab/2026-09-07-partition-existing-table (the writer is Load._writer; steps 3 and 4 are the ones above).
Measured on 1.5.0 against PostgreSQL 17.11: a plain
eventstable with two million rows over the last year, swapped under a new parent as its DEFAULT partition the wayguide/partition-existing-table.mdsays, with one writer insertingnow()rows througheventsevery 5 ms the whole time. Nothing else is running.The first tick plans
CREATE events__2026_09 / __2026_10 / __2026_11 (create_ahead). The two future months attach; the current month never does:The cause is the shape of
_attach_with_reconcile(aio/services/execution.py, and the sync twin): the attach fails on the DEFAULT conflict, the window's rows are moved out of DEFAULT in their own transaction, the attach is retried in another. Between the move's commit and the attach's scan the writer has inserted one more row for the window, so the scan finds it and the attach fails again,DEFAULT_CONFLICT_MAX_RETRIEStimes in a row, and the rows are moved back. Under any steady write rate the current month cannot be attached, which is the one window every live table is writing into, and the state after the tick is the state before it: every insert keeps landing in DEFAULT. The run reportserrorset andissuesempty; each retry costs the writer a full scan of the two-million-row DEFAULT underACCESS EXCLUSIVE(the 604 ms above).partition_datareaches the same window last and does worse: it fills the detachedevents__2026_09, fails the attach the same way, moves the rows back, and then raises theIntegrityErrorout ofpartition_datainstead of reporting amoveissue withcomplete=False, which is what the guide promises for a window it cannot handle. The eleven older months drained fine before that (lowest count 29764 of 129764on the oldest month while it was mid-move, as the guide warns).What I think the fix is: do the move and the attach in one transaction, under the lock the attach takes anyway.
ATTACHalready needsACCESS EXCLUSIVEon the DEFAULT sibling for its scan, soBEGIN; LOCK TABLE <default> IN ACCESS EXCLUSIVE MODE; DELETE ... RETURNING / INSERT; ALTER TABLE ... ATTACH PARTITION ...; COMMITholds the same lock for the move plus the scan and cannot lose the race; a writer waits for that transaction and is then routed to the new partition. The rows also stop needing a compensating move-back for this path, since a failed attach rolls the move back with it. For a large window the move itself can be long, so it may be worth draining most of the window in batches first (whichpartition_dataalready does) and taking the locked move-and-attach only for what is left, so the exclusive window is a batch and a scan, not the whole month.partition_datashould report an attach it still cannot do as amoveissue and returncomplete=False, never raise. The DEFAULT reconciliation section ofconcepts/execution.mdand the guide's numbered list follow.Lab:
migrate_lab.pyin https://github.com/bedrock-python/bedrock-python.github.io/tree/docs/production-python-series/docs/blog/lab/2026-09-07-partition-existing-table (the writer isLoad._writer; steps 3 and 4 are the ones above).