Found by the priority:P0 round of the #9296 QA wave; run record #9334. Subject sha e4e5c6e3c608b1b807c83a0d5b734f213eb1a1dd, stock showcase.
Symptom
DELETE /api/v1/data/showcase_account/<id> → 400 INVALID_FILTER
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.
Found by the
priority:P0round of the #9296 QA wave; run record #9334. Subject shae4e5c6e3c608b1b807c83a0d5b734f213eb1a1dd, stock showcase.Symptom
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:showcase_field_zoo.f_lookupsis a multi-value lookup atshowcase_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 throwsINVALID_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_accountis affected: it is the sole object targeted by amultiple: truereference. Verified contrasts on the same boot:showcase_accountDELETE_RESTRICTED— a correct guard, not this bugGenerally: any object that is the target of a
multiple: truereference 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 ismultiple: true, probe with the containment operator the engine already supports for JSON-TEXT multi-value columns instead of bare equality. Landing inpackages/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
INVALID_FILTER, row still present on re-read.Left unlabelled for
domain:*— routing is the triage seat's call (the fix lands inpackages/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.