Skip to content

Fix $not where clauses matching no rows - #365

Merged
rsslldnphy merged 1 commit into
mainfrom
russell/ck-108-orm-not-where-clauses-silently-match-nothing
Oct 4, 2026
Merged

rsslldnphy merged 1 commit into
mainfrom
russell/ck-108-orm-not-where-clauses-silently-match-nothing

Conversation

@rsslldnphy

@rsslldnphy rsslldnphy commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

A [$not] clause in a where came out of the ORM as SQL that cannot match any row, so queries using it silently did nothing. Fixes CK-108.

Two bugs compounded in buildWhere.

Middleware was re-applied on every recursion. The recursive call ran applyWhereMiddleware again, so middleware injected its conditions inside the NOT as well as at the top level. Negating them there contradicts the top-level copy:

WHERE "a"."deleted_at" IS NULL
  AND ...
  AND NOT "a"."deleted_at" IS NULL   -- never true

Middleware now applies once, to the top-level clause, with a private buildWhereClause handling the recursion. This also stops $and and $or repeating the conditions in every branch, where they were redundant rather than wrong.

NOT was unbracketed. sql`NOT ${subclause}` let NOT bind to the first conjunct only, so what was asked for as NOT (field_id IN (...)) came out as NOT (deleted_at IS NULL) AND field_id IN (...). $and and $or already bracket their subclauses; $not now does too. This one mis-negates a $not over several plain clauses with no middleware involved at all.

Tests

buildWhere.test.ts gets a where middleware with logical operators block asserting the generated SQL for $not, $and, $or and a nested combination under an injecting middleware — the combination the ticket called out as untested — plus a $not over multiple conditions for the bracketing bug on its own. The existing builds NOT clause expectation becomes NOT ("u"."active" IS TRUE).

orm.middleware.where.test.ts gets an end-to-end deleteMany test using [$not]: { id: { [$in]: [...] } } under soft-delete middleware, asserting the right rows actually go.

Verified by stashing the fix: 7 assertions fail, the end-to-end one with expected +0 to be 2 — the reported symptom. With the fix the full suite, typecheck, lint and format are clean.

Notes for reviewers

  • Behaviour change beyond the bug fix: an empty subclause inside $and/$or now throws "must not be empty" where middleware could previously have filled it in. That follows from applying middleware once, but it is a visible change if anyone relied on it. Nothing in the suite did.
  • No changeset: .changeset/ is configured but no recent commit includes one, so I matched current practice. Happy to add one.
  • Follow-up in casekit-v2 once this is released: savePersonType.procedure.test.ts > leaves the dropped field's formField row behind should be flipped to assert the row is gone and its comment removed, and the other four affected call sites want coverage. Tracked on the ticket.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Corrected how middleware filters interact with nested AND, OR, and NOT conditions, preventing filters from being applied more than once.
    • Ensured NOT conditions are grouped correctly in generated queries, including when negating multiple conditions.
    • Fixed soft-delete filtering in bulk deletion so already deleted records remain excluded.

@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: bf06cdab-3596-4f59-86fc-8ba925674eb8

📥 Commits

Reviewing files that changed from the base of the PR and between ea3d923 and d6ae947.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 11f09b43-3577-408c-94f3-bd79fc717513

📥 Commits

Reviewing files that changed from the base of the PR and between 6334e95 and ea3d923.

📒 Files selected for processing (3)
  • packages/orm/src/builders/buildWhere.test.ts
  • packages/orm/src/builders/buildWhere.ts
  • packages/orm/src/tests/orm.middleware.where.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The where builder now applies middleware to the top-level clause and builds nested clauses without applying middleware again. $not subclauses are enclosed in parentheses. Tests cover SQL grouping, middleware conditions, parameter values, and soft-delete behavior.

Changes

Where Clause Handling

Layer / File(s) Summary
Top-level middleware and clause grouping
packages/orm/src/builders/buildWhere.ts, packages/orm/src/builders/buildWhere.test.ts, packages/orm/src/tests/orm.middleware.where.test.ts
buildWhere applies middleware once before building nested clauses. $and and $or use the recursive builder, and $not groups its subclause in parentheses. Tests verify generated SQL, parameter values, middleware placement, and soft-delete behavior.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to ea3d9

The change corrects negated predicates and prevents repeated middleware application. No concrete merge-blocking risk remains after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to ea3d9

The corrected predicates preserve the demonstrated soft-delete filter and restore intended negation behavior. Custom middleware now runs only on the complete predicate, rather than on each logical branch. Production integrations were not established, leaving limited uncertainty about policy compatibility.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The affected surface includes record visibility, counts, updates, and deletions for models using the shared builder. Actual tenant, service, and database exposure depends on application callers and middleware policies that were not established by the inspected evidence.

Trust Boundaries and Controls

  • observed — The added deleteMany regression asserts that the caller-excluded row and an already soft-deleted row survive, while two eligible rows are deleted. This supports preservation of the demonstrated soft-delete boundary under NOT, not proof of production tenant authorization.
  • observed — The soft-delete fixture allows an explicit caller-supplied deletedAt field to override its default through object spread. The same callback exists on the target branch; it is not a new enforcement weakness introduced by this PR and does not establish production authorization behavior.

Resilience and Maintainability Implications

  • observed — deleteMany constructs SQL before opening its transaction, commits after successful execution, and attempts rollback on exceptions. Builder validation failures therefore precede mutation. Interrupted commits, concurrent execution, and retry behavior were not directly tested in this review.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: fixing $not where clauses that incorrectly match no rows. It is concise and specific.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 3…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

A `[$not]` clause came out as SQL that could not match any row, so
queries using it silently did nothing. Two bugs compounded in
buildWhere:

The recursive call re-ran applyWhereMiddleware, so middleware injected
its conditions inside the NOT as well as at the top level. Negating them
there contradicts the top-level copy — `deleted_at IS NULL AND NOT
(deleted_at IS NULL AND ...)` is never true. Middleware now applies once,
to the top-level clause, with an inner buildWhereClause handling the
recursion. This also stops $and and $or repeating the conditions in
every branch, where they were redundant rather than wrong.

`NOT ${subclause}` was also unbracketed, so NOT bound to the first
conjunct only: a $not over several plain clauses was mis-negated with no
middleware involved at all.

Covers both with unit tests on the generated SQL, and an end-to-end
deleteMany regression test under soft-delete middleware.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rsslldnphy
rsslldnphy force-pushed the russell/ck-108-orm-not-where-clauses-silently-match-nothing branch from ea3d923 to d6ae947 Compare October 2, 2026 08:32
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

  • orm-cli: 96.85%
  • orm-migrate: 94.02%
  • orm: 98.42%
  • sql: 97.03%
  • toolbox: 100%

@rsslldnphy
rsslldnphy merged commit 20b1cec into main Oct 4, 2026
15 checks passed
@rsslldnphy
rsslldnphy deleted the russell/ck-108-orm-not-where-clauses-silently-match-nothing branch October 4, 2026 13:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant