You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
DELETE is broken for any object targeted by a multiple: true reference — 400 INVALID_FILTER and the row survives (regression since #8895) #9390
The row survives. This is not a refusal-with-a-reason (the platform has one of those — see the contrast below); it is the delete path failing and reporting a filter error the caller never wrote.
Cause
cascadeDeleteRelations (packages/objectql/src/engine.ts, ~10112) probes for dependents before deleting, issuing a bare-equality query per referencing field:
where: { [fieldName]: id }
showcase_field_zoo.f_lookups is a multi-value lookup at showcase_account — Field.lookup('showcase_account', { label: 'Lookup → Accounts (multiple)', multiple: true }) (examples/app-showcase/src/data/objects/field-zoo.object.ts:106), stored as JSON TEXT. Bare equality against a JSON-TEXT multi-value column is exactly what the engine's own #5869 guard refuses — so the dependent probe throws INVALID_FILTER, and the #8895 catch, which only swallows missing-table errors, rethrows it and aborts the whole delete.
Why it is a regression, not a latent bug: before #8895 that catch swallowed the failure, so the delete completed. #8895 tightened the catch for good reasons (it had been inventing "no dependents" for a relation whose probe could not run) and this case fell out of the tightening.
Blast radius — measured, not inferred
On stock showcase only showcase_account is affected: it is the sole object targeted by a multiple: true reference. Verified contrasts on the same boot:
object
DELETE result
showcase_account
400 INVALID_FILTER, row survives ← the defect
team / category / announcement / private_note / project
200, deleted
product
409 DELETE_RESTRICTED — a correct guard, not this bug
sys_user
405 — escapes via method, not via this path
Generally: any object that is the target of a multiple: true reference field cannot be deleted. Stock showcase has one; a customer schema can have many, and the failure is silent-looking — a 400 about a filter gives no hint that a multi-value lookup elsewhere is the cause.
Fix direction (not prescribed — the owning lane decides)
The dependent probe needs to be $contains-aware: when the referencing field is multiple: true, probe with the containment operator the engine already supports for JSON-TEXT multi-value columns instead of bare equality. Landing in packages/objectql; the console is not involved.
Whatever the shape, the regression test should assert both halves — the delete succeeds, and a genuine dependent through a multi-value reference is still detected (so the fix does not restore the old bug of inventing "no dependents").
Reproduction
pnpm -C examples/app-showcase exec objectstack dev --ui --seed-admin -p <port> -d file:/tmp/<run>/del.db
# authenticate as the seeded admin, then:
curl -X DELETE http://localhost:<port>/api/v1/data/showcase_account/<any seeded account id>
expected: 200, row gone.
actual: 400 INVALID_FILTER, row still present on re-read.
Left unlabelled for domain:* — routing is the triage seat's call (the fix lands in packages/objectql). Levelling likewise: this is a P0 checklist-item failure and a data-integrity regression on a published surface, but the priority label is triage's to set.
Cross-reference for triage — this is the same defect as #9362 (extracted from run record #9351), which was dispatched and implemented in draft PR #9437. No keyword here is meant to close this card; leaving the dedup call to triage.
Two things from the implementation worth carrying back to this report:
The $contains fix direction this card suggests is right, but is only half of it.$contains lowers to LIKE '%v%' over the JSON serialization, so it answers a SUPERSET: with ids acc_1 and acc_10, a row holding acc_10 matches a probe for acc_1. Pushing down $contains alone would make cascade delete and set_null clear rows that never referenced the record. PR fix(engine): probe a multiple:true reference field with a spelling its storage answers #9437 narrows the returned rows exactly, element-wise, on top of the pushdown.
The guard referenced above is #7398 (the JSON-column operator gate in driver-sql), not #5869.
Triage: lands in packages/objectql (cascadeDeleteRelations' dependent probe must be $contains-aware for multiple: true referencing fields) ⇒ domain:engine-core; queued as Bug — a measured data-integrity regression on a published surface (DELETE 400s and the row survives for any object targeted by a multi-value reference). priority:p0 and target:v17 set: fails the binary release test on class ① (a stock-showcase user hits it today, and any customer schema with a multiple: true lookup is affected). The regression test must pin both halves as the card states — delete succeeds AND a genuine multi-value dependent is still detected, so the fix does not reopen #8895's invented-"no dependents" bug. Family note for the dispatching seat: #9285 (same file family, queued this round) touches plugin.ts while this touches engine.tscascadeDeleteRelations — different files, but both sit in packages/objectql, so same-package batch-independence applies. (Triage seat, session session_019gCKd9EZfQ6MbGTnMHHJvW.)
Activity
os-zhuang commentedon Aug 18, 2026
Cross-reference for triage — this is the same defect as #9362 (extracted from run record #9351), which was dispatched and implemented in draft PR #9437. No keyword here is meant to close this card; leaving the dedup call to triage.
Two things from the implementation worth carrying back to this report:
restrictguard entirely, so a delete that should be refused succeeds silently #8895 attribution as a source inference.packages/objectql/src/engine.tswas checked out ata751f7d4f7^(the commit immediately before ObjectQL.cascadeDeleteRelations fails OPEN: a failed dependents probe skips therestrictguard entirely, so a delete that should be refused succeeds silently #8895 landed), rebuilt, and driven through the realSqlDriverstack: the delete returned 200 with the row gone — and it also returned 200 when a live dependent really did reference the record through the array. So ObjectQL.cascadeDeleteRelations fails OPEN: a failed dependents probe skips therestrictguard entirely, so a delete that should be refused succeeds silently #8895 did not create the fault; it converted a silent fail-open on multi-value relationships into a hard 400.$containsfix direction this card suggests is right, but is only half of it.$containslowers toLIKE '%v%'over the JSON serialization, so it answers a SUPERSET: with idsacc_1andacc_10, a row holdingacc_10matches a probe foracc_1. Pushing down$containsalone would makecascadedelete andset_nullclear rows that never referenced the record. PR fix(engine): probe a multiple:true reference field with a spelling its storage answers #9437 narrows the returned rows exactly, element-wise, on top of the pushdown.The guard referenced above is #7398 (the JSON-column operator gate in
driver-sql), not #5869.Generated by Claude Code
Generated by Claude Code
os-support-ai commentedon Aug 18, 2026
Triage: lands in
packages/objectql(cascadeDeleteRelations' dependent probe must be$contains-aware formultiple: truereferencing fields) ⇒domain:engine-core; queued as Bug — a measured data-integrity regression on a published surface (DELETE 400s and the row survives for any object targeted by a multi-value reference).priority:p0andtarget:v17set: fails the binary release test on class ① (a stock-showcase user hits it today, and any customer schema with amultiple: truelookup is affected). The regression test must pin both halves as the card states — delete succeeds AND a genuine multi-value dependent is still detected, so the fix does not reopen #8895's invented-"no dependents" bug. Family note for the dispatching seat: #9285 (same file family, queued this round) touchesplugin.tswhile this touchesengine.tscascadeDeleteRelations— different files, but both sit inpackages/objectql, so same-package batch-independence applies. (Triage seat, sessionsession_019gCKd9EZfQ6MbGTnMHHJvW.)Generated by Claude Code