Repository navigation
Immutable
release. Only release title and notes can be modified.
Added
- A guard on every cascade child archives a child created or re-pointed under an archived parent (#91, ADR 0044). An archive locks the parent row
FOR NO KEY UPDATE, which the foreign-key check of an insert (FOR KEY SHARE) does not wait for, and the cascade only reaches rows that exist when its statement starts. So a child committed while another session archived its parent stayed live under it, in either order, atREAD COMMITTEDandREPEATABLE READ, and the same for a child moved onto an archived parent and a leaf added to a tree being archived. This was true of the rule the arms replaced too; it was measured with two sessions for the first time. Each cascade child table now carriessoft_delete_guard_on_<n>_<table>, aBEFORE INSERT OR UPDATErow trigger that reads the parentFOR SHAREwhen the key arrives or moves, waits for an uncommitted archive, and copies the parent's_deleted_at; an archive in turn waits for an uncommitted insert and takes its child. A key declared on an MTI descendant archives the ancestor's row from anAFTERtrigger. The stamp is the parent's own, so restoring the parent brings the late child back. GUITARS_CASCADE_GUARD(defaultTrue):Falseretires every recorded guard.tests/test_concurrency_arms.py: the interleavings, stepped by hand with two connections, and the limits that remain.
Changed
- A child created under an archived parent is archived with it, where it was live. Code that relied on a live child under an archived parent needs the parent restored first.
- Existing projects:
makemigrationswrites one enforcement migration per app that hosts a cascade child, creating the guards. A guard reads the parent row on every insert and on every update that moves the key, and holdsFOR SHAREon it until commit, so an insert waits for a concurrent update of that parent and a bulk load pays a lookup per row, and two transactions that each insert a child and then update the same parent (a counter, a denormalised total) now deadlock (40P01) where they serialised. If that pattern is common in your code, setGUITARS_CASCADE_GUARD = False. It runs as the invoker:FOR SHAREneedsUPDATEon the parent table, and a role without it gets a plain read, so no insert fails but the race stays open for that role.session_replication_role = replicaskips it. - Not closed, and pinned by tests: an archive in a
REPEATABLE READtransaction whose snapshot predates a child's commit still misses that child (twoSERIALIZABLEsessions are refused with40001), and a child restored by hand under an archived parent stays live. RetireEnforcementtakes the guard, with a column or a whole table: its body names the key and_deleted_at.- Tests only, for corners the suite did not reach: a self key on a model with no
_updated_at(Twig), an MTI child's redirect rule and parent trigger across a drop and a rename in the scan, and--adoptover a recorded self-cascade trigger.