Summary
After investigating the codebase, we found that:
$none on a bare relation path already works for nested to-many paths (e.g. filter[home.pillows]=$none) — producing a bare NOT EXISTS correlated subquery with no inner column conditions.
$none on direct OneToMany / ManyToMany relation paths is untested — e.g. filter[toys]=$none, filter[friends]=$none.
$null on a relation path does NOT work correctly — it silently produces a bare EXISTS (same as $any), not NOT EXISTS, because the $null operator is placed into the findOperator of an empty sub-filter that never gets applied. The $null/$not:$null distinction is effectively ignored.
Actual Gaps
Gap 1: Missing tests for $none on direct OneToMany / ManyToMany
The $none quantifier on a bare relation path (no sub-column) produces an empty sub-filter, which results in a bare NOT EXISTS correlated subquery — exactly the right behavior for "no related records". This is already implemented and tested for home.pillows (nested path), but there are no tests verifying it works for:
filter[toys]=$none (direct OneToMany)
filter[friends]=$none (direct ManyToMany, owning side)
filter[friendOf]=$none (direct ManyToMany, inverse side)
Gap 2: $null / $not:$null on relation paths is misleading / broken
When filter[toys]=$null is used:
token.operator = $null, token.quantifier = $any (default)
- The sub-filter is empty (no sub-column)
addWhereCondition is never called — the IsNull() findOperator is ignored
- Quantifier
$any → bare EXISTS → "cats WITH toys" (wrong — user expected "cats with NO toys")
When filter[toys]=$not:$null:
- Same result — also bare
EXISTS — the $not suffix is also ignored
Neither maps to the expected behavior. To get "cats with no toys", users must use $none, not $null.
What Needs to Be Done
Option A (Minimal — recommended): Test + document existing behavior
- Add tests confirming
$none works on direct OneToMany (toys) and ManyToMany (friends, friendOf) bare relation paths
- If any tests fail, fix the underlying issue
- Document in README that
$none on a bare relation path means "no related records"
Option B (Extended): Also support $null as an alias for $none on relation paths
Make filter[toys]=$null behave like filter[toys]=$none when the filter key is a to-many relation (not a column). This requires:
- In
addToManySubFilters() — detect when a filter key equals the to-many path exactly and the operator is $null; override the quantifier to $none (NOT EXISTS)
- For
$not:$null → override to $any (EXISTS)
- Add tests for both syntaxes
Current Behavior Reference
| Filter |
SQL |
Correct? |
filter[home.pillows]=$none |
NOT EXISTS (pillows correlated) |
✅ works, tested |
filter[toys]=$none |
NOT EXISTS (toys correlated) |
✅ likely works, untested |
filter[friends]=$none |
NOT EXISTS (junction correlated) |
✅ likely works, untested |
filter[toys]=$null |
bare EXISTS (wrong!) |
❌ broken |
filter[toys]=$not:$null |
bare EXISTS (same as above!) |
❌ broken |
Summary
After investigating the codebase, we found that:
$noneon a bare relation path already works for nested to-many paths (e.g.filter[home.pillows]=$none) — producing a bareNOT EXISTScorrelated subquery with no inner column conditions.$noneon direct OneToMany / ManyToMany relation paths is untested — e.g.filter[toys]=$none,filter[friends]=$none.$nullon a relation path does NOT work correctly — it silently produces a bareEXISTS(same as$any), notNOT EXISTS, because the$nulloperator is placed into thefindOperatorof an empty sub-filter that never gets applied. The$null/$not:$nulldistinction is effectively ignored.Actual Gaps
Gap 1: Missing tests for
$noneon direct OneToMany / ManyToManyThe
$nonequantifier on a bare relation path (no sub-column) produces an empty sub-filter, which results in a bareNOT EXISTScorrelated subquery — exactly the right behavior for "no related records". This is already implemented and tested forhome.pillows(nested path), but there are no tests verifying it works for:filter[toys]=$none(direct OneToMany)filter[friends]=$none(direct ManyToMany, owning side)filter[friendOf]=$none(direct ManyToMany, inverse side)Gap 2:
$null/$not:$nullon relation paths is misleading / brokenWhen
filter[toys]=$nullis used:token.operator = $null,token.quantifier = $any(default)addWhereConditionis never called — theIsNull()findOperator is ignored$any→ bareEXISTS→ "cats WITH toys" (wrong — user expected "cats with NO toys")When
filter[toys]=$not:$null:EXISTS— the$notsuffix is also ignoredNeither maps to the expected behavior. To get "cats with no toys", users must use
$none, not$null.What Needs to Be Done
Option A (Minimal — recommended): Test + document existing behavior
$noneworks on direct OneToMany (toys) and ManyToMany (friends,friendOf) bare relation paths$noneon a bare relation path means "no related records"Option B (Extended): Also support
$nullas an alias for$noneon relation pathsMake
filter[toys]=$nullbehave likefilter[toys]=$nonewhen the filter key is a to-many relation (not a column). This requires:addToManySubFilters()— detect when a filter key equals the to-many path exactly and the operator is$null; override the quantifier to$none(NOT EXISTS)$not:$null→ override to$any(EXISTS)Current Behavior Reference
filter[home.pillows]=$noneNOT EXISTS (pillows correlated)filter[toys]=$noneNOT EXISTS (toys correlated)filter[friends]=$noneNOT EXISTS (junction correlated)filter[toys]=$nullEXISTS(wrong!)filter[toys]=$not:$nullEXISTS(same as above!)