Skip to content

fix(cascade): restrict only the rows a cascade reaches - #1592

Open
dimitri-yatsenko wants to merge 1 commit into
masterfrom
fix/cascade-overdelete
Open

dimitri-yatsenko wants to merge 1 commit into
masterfrom
fix/cascade-overdelete

Conversation

@dimitri-yatsenko

Copy link
Copy Markdown
Member

Closes #1589, closes #1590, closes #1591.

A cascade delete reached rows outside the cascade in three cases:

  • dj.Top seed (delete() ignores a dj.Top restriction and deletes every matching row #1589). Diagram.cascade and Diagram.trace built the seed from the WHERE restriction only, and delete_quick used where_clause() only. A seed restricted by Top is now replaced by the primary keys it selects.
  • Copy through a foreign key (Cascade delete removes child rows whose nullable foreign key references nothing #1590). A parent's restriction was copied to the child whenever the restricted attributes were in the child's primary key. Through a nullable foreign key that shares a primary-key column, that also selected child rows whose foreign key is NULL. In a diamond, where the child inherits the attribute through another foreign key, it missed rows. The copy now applies only when the restricted attributes are among the foreign key's attributes and the foreign key lies in the child's primary key. Otherwise the child is restricted by the projected parent.
  • Part-to-master lift (part_integrity="cascade" deletes masters that have no matching part rows #1591). With part_integrity="cascade", the lift copied the part's condition to the master, which selected masters with no matching part rows. The master is now restricted by the restricted part, so a master joins the cascade only if one of its part rows did.

Two follow-on changes:

  • The part-to-master walk restricts intermediate parts only to carry the restriction up to the master. Those restrictions reference the descendant part, which is deleted first (and on MySQL they raise error 1093). They are now restored once the master is materialized; the master then cascades back to all of its parts.
  • The Diagram copy constructor copies each node's restriction list instead of deep-copying it. Projected restrictions are query expressions bound to a live connection and cannot be deep-copied. This affected prune() and the diagram operators after restrict().

Not changed: Diagram.trace keeps its upward copy for now. self.upstream traces from a row that make() has not inserted yet and relies on it. The traversal redesign for 2.4 defines that case explicitly.

Tests: tests/integration/test_cascade_overdelete.py adds four tests on MySQL and PostgreSQL. All fail on v2.3.4 and pass here. The full suite passes locally (1166 passed, 14 skipped).

The same commit is cherry-picked onto maint/2.3 for a v2.3.5 patch release.

Three cases deleted rows outside the cascade:

- `(T & dj.Top(n)).delete()` and `delete_quick()` ignored the Top and
  deleted every row matching the other conditions. A Top seed is now
  replaced by the primary keys it selects.
- A parent restriction was copied to a child whenever the restricted
  attributes were in the child's primary key. Through a nullable foreign
  key that shares a primary-key column, the copy also deleted child rows
  whose foreign key is NULL; in a diamond it missed rows. The copy now
  applies only when the restricted attributes are among the foreign key's
  attributes and the foreign key lies in the child's primary key.
  Otherwise the child is restricted by the projected parent.
- With part_integrity="cascade", the part-to-master lift copied the
  part's condition to the master, which also deleted masters with no
  matching part rows. The master is now restricted by the restricted
  part, so a master joins the cascade only if one of its part rows did.
  The walk's restrictions on intermediate parts are restored once the
  master is materialized; the master cascades back to all of its parts.

The Diagram copy constructor now copies each node's restriction list
instead of deep-copying projected query expressions, which hold a live
connection.
@dimitri-yatsenko dimitri-yatsenko added this to the v2.3.5 milestone Oct 17, 2026
@dimitri-yatsenko dimitri-yatsenko added the bug Indicates an unexpected problem or unintended behavior label Oct 17, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Indicates an unexpected problem or unintended behavior

Projects

None yet

1 participant