Repository navigation
fix!: negated operators, $not and $every match NULL rows - #46
Merged
Merged
Conversation
feathers-adapter-vitest 0.4.1 adds the `$ne` / `$nin` "matches null" cases, which fail until the next commit. TypeScript stays on 6 (7 is a major). minimumReleaseAgeExclude keeps only the one version still younger than the release-age window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A NULL is not 25, yet SQL evaluates `age != 25` to UNKNOWN and drops the row. Feathers queries are Mongo-shaped, where every negation is the exact complement of its positive twin, so: - `$ne`, `$nin`, `$notLike`, `$notILike`, `$notBetween` and `$notRegex` OR an `IS NULL` next to the comparison. `$ne: null` and a `$nin` that lists `null` keep excluding NULL rows. - `$not` and the inner negation of `$every` use `(cond) IS NOT TRUE` instead of `NOT (cond)`, so a row the condition is UNKNOWN for matches the negation. An EXISTS is never UNKNOWN, so `$not` over a relation path stays a plain `NOT EXISTS`, which the planner can turn into an anti-join. BREAKING CHANGE: these operators now return rows whose column is NULL. Add `$ne: null` (or `null` to a `$nin` list) to exclude them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
commit: |
commit: |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
feathers-kysely | ab3ef80 | Commit Preview URL Branch Preview URL |
Oct 22 2026, 09:56 AM |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A NULL is not 25, yet SQL evaluates
age != 25to UNKNOWN and drops the row. Feathers queries are Mongo-shaped, where every negation is the exact complement of its positive twin, andfeathers-adapter-vitest0.4.1 now tests$ne/$ninagainst NULL rows.Unchanged:
$ne: null→is not null,$nin: [null, 2]→(not in (2) and is not null).Also a dependency update (feathers-adapter-vitest 0.4.1, kysely 0.29.7, vite 8.3.5, …; TypeScript stays on 6), and
minimumReleaseAgeExcludekeeps only the one version still inside the release-age window.Evidence
.find + $ne matches null/.find + $nin matches nullfail on all 4 service variants (2 !== 3).After: pass.
{ age: { $gt: 1 } }and{ $not: { age: { $gt: 1 } } }both miss the NULL row;$every: { assigneeId: { $gt: 0 } }accepts a user whose only todo has a NULL assignee, while$somewith the same filter rejects it.After: new tests:
$notpartitions the rows for 10 conditions, negated operators match NULL rows (query-operators.test.ts),$some/$nonewith$ne/$ninand$everyvs$someon a NULL child (relations.test.ts). All four fail with the fix reverted. 1sql-shapesnapshot updated (hasMany $every).Performance, local PostgreSQL 18, 1M users / 3M todos, 10% NULL, median of 7:
$ne/$nin/$notLike, count$ne+ indexedgroup_id = 7$notBetween, NOT NULL columnIS NULLarm is indexable)$every, count$not: { x: { $gt: 3 } }, NOT NULL indexed column, same rowsElsewhere a query only gets slower by the NULL rows it now returns (
$notBetweenon a nullable column: 3.8 → 10.1 ms for 10k → 110k rows).Known regression:
(cond) IS NOT TRUEhidescondfrom indexes, so$notover a selective indexed range loses its index. Pushing the negation down to each comparison (not (x > 3) or x is null) would stay indexable; that would be a follow-up.Merge Danger
Door: two-way
A revert restores the old SQL, but consumers will start to rely on the new results once released.
Blast Radius: query results
$ne,$nin,$notLike,$notILike,$notBetween,$notRegex,$notand$everynow return rows whose column is NULL. A hook that excludes rows with$not/$negets more rows back. Breaking, so it needs a 0.15.0 and a changelog note.Conflicts with #45 in
src/utils/build-in.ts(its Postgres<> all($1)path),docs/api/operators.mdandpnpm-lock.yaml.🤖 Generated with Claude Code