Skip to content

fix(cascade): seed a delete with its keys when its restriction is another query, and keep a % in it (2.3) - #1612

Open
dimitri-yatsenko wants to merge 2 commits into
fix/cascade-overdelete-2.3from
fix/cascade-subquery-seed-2.3
Open

dimitri-yatsenko wants to merge 2 commits into
fix/cascade-overdelete-2.3from
fix/cascade-subquery-seed-2.3

Conversation

@dimitri-yatsenko

@dimitri-yatsenko dimitri-yatsenko commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Backport of #1611 to maint/2.3 for v2.3.5, cherry-picked unchanged. Stacked on #1593, so its base is fix/cascade-overdelete-2.3; it moves to maint/2.3 when #1593 merges.

A delete restricted by another query, such as
(Subject & (Session & "quality = 0")).delete(), failed. The cascade
applies the seed's compiled conditions again to a fresh copy of the table
through restrict(), which collects column names from the whole condition
text, subquery included, and rejects every name the seed table lacks. The
delete raised Attribute ... is not found in query before deleting
anything. On MySQL the backquoted schema name in every subquery tripped the
same check, so even (Subject & Session).delete() raised.

On PostgreSQL, a seed whose subquery names only the seed's columns got past
the check but still did nothing. The cascade deletes children before
parents, so by the time it reached the seed, the rows its subquery reads
were gone. (Subject & Session).delete() matched no subject, returned 0
and rolled back.

A seed whose conditions contain a subquery is now replaced by the primary
keys it selects, as a dj.Top seed already is. The traversal and the
delete then work from the rows the restriction selected when the delete
started.

The new tests delete subjects restricted by a query on their sessions, by
its negation, and by the session table itself. They check the preview
counts and the rows left afterwards, on both backends. All six cases fail
on the previous code.

Fixes #1610.

Second commit: keep a % in a delete's restriction (#1636)

A delete restricted by a condition containing %, such as (Label & "name = '50%'").delete() or a dict {"name": "50%"}, deleted nothing and returned 0. The seed's conditions are compiled SQL, in which each % is already escaped as %%; the cascade applies them again through restrict(), which escaped every % a second time. The seed's conditions now have that first escape undone before the cascade applies them again. The new test checks a string condition, a dict and a condition holding %%, on both backends, and fails before this change.

Fixes #1636.

A delete restricted by another query, such as
`(Subject & (Session & "quality = 0")).delete()`, failed. The cascade
applies the seed's compiled conditions again to a fresh copy of the table
through `restrict()`, which collects column names from the whole condition
text, subquery included, and rejects every name the seed table lacks. The
delete raised `Attribute ... is not found in query` before deleting
anything. On MySQL the backquoted schema name in every subquery tripped the
same check, so even `(Subject & Session).delete()` raised.

On PostgreSQL, a seed whose subquery names only the seed's columns got past
the check but still did nothing. The cascade deletes children before
parents, so by the time it reached the seed, the rows its subquery reads
were gone. `(Subject & Session).delete()` matched no subject, returned 0
and rolled back.

A seed whose conditions contain a subquery is now replaced by the primary
keys it selects, as a `dj.Top` seed already is. The traversal and the
delete then work from the rows the restriction selected when the delete
started.

The new tests delete subjects restricted by a query on their sessions, by
its negation, and by the session table itself. They check the preview
counts and the rows left afterwards, on both backends. All six cases fail
on the previous code.

(cherry picked from commit 6863332)
@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
A delete restricted by a condition containing %, such as `(Label & "name = '50%'").delete()` or a dict `{"name": "50%"}`, deleted nothing and returned 0. The seed's conditions are compiled SQL, in which each % is already escaped as %% for the database driver. The cascade applies them again through `restrict()`, which escaped every % a second time, so the condition looked for the literal text `50%%` and matched no row. A LIKE pattern kept working only because `%%` and `%` match the same strings there.

The seed's conditions now have that first escape undone before the cascade applies them again, so they select the same rows as the restriction on its own.

The new test checks the preview counts for a string condition, a dict and a condition holding `%%`, and deletes with a LIKE pattern, on both backends. It fails before this change.

(cherry picked from commit e9e1d67)
@dimitri-yatsenko dimitri-yatsenko changed the title fix(cascade): seed a delete restricted by another query with its keys (2.3) fix(cascade): seed a delete with its keys when its restriction is another query, and keep a % in it (2.3) Oct 18, 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

Development

Successfully merging this pull request may close these issues.

1 participant