Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .changeset/21397-null-ordering-message-faces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'@objectstack/spec': patch
---

fix(spec): the null ordering-comparand refusals name only evaluation faces that exist, and say only what was measured

Clause-②: no

`FieldOperatorsSchema` and `ComparisonOperatorSchema` refuse a `null` comparand of `$gt` / `$gte` /
`$lt` / `$lte` with a pointed message. Its example of the evaluation faces disagreeing named
driver-memory's reference matcher, which has been deleted, so an author or agent reading the
refusal went looking for a face that no longer exists. The example now names two faces that exist
and were measured to disagree: driver-memory's query path reads a stored `null` as equal to the
comparand, so `{"$gte": null}` admits that row, while driver-sql compares against SQL `NULL` and
admits no row.

That refusal and its runtime twin, the `parseFilterAST` refusal for the same comparand
(`Operator "$gt" on field "…" does not accept a null comparand …`), both said "no two evaluation
faces agree" on what an ordering against `null` matches. Measured, two faces do agree (driver-sql
and formula both admit no row), so both now say "the evaluation faces do not agree".

Text only: each message's first sentence, its prescription (`{"$eq": null}` / `{"$ne": null}`), the
schema door's ruling sentence and the runtime door's "NOT applied" sentence are unchanged, and both
doors accept and refuse exactly the same filters. A client or log filter that matches the old
wording needs the new spelling.
2 changes: 1 addition & 1 deletion packages/spec/src/data/filter-comparand-shape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,7 @@ function nullOrderingComparandError(
return invalidFilterComparandError(
context,
`Operator "${op}" on field "${field}" does not accept a null comparand (at ${path}). ` +
`null is not ordered; no two evaluation faces agree on what it matches. State absence ` +
`null is not ordered; the evaluation faces do not agree on what it matches. State absence ` +
`with the null predicate: {"$eq": null} is "has no value", {"$ne": null} is "has a value". ` +
`Authoring spellings: ${spellings.join(', ')}. The filter was NOT applied, and an ` +
`unapplied filter would have returned the UNFILTERED result set.`,
Expand Down
5 changes: 3 additions & 2 deletions packages/spec/src/data/filter-operator-vocabulary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,9 @@ describe('the declaration surface and the enforcement surface', () => {
+ 'so declaring ahead of the arms is the correct staging. To stage one: declare it in '
+ 'FieldOperatorsSchema, add it to the array THIS assertion compares against, and note on '
+ 'FILTER_OPERATORS which issue implements it. To clear one: implement it on EVERY face '
+ 'in ONE PR — spec word list, driver-memory (query path, reference matcher, analytics '
+ 'face), driver-mongodb, service-analytics (3 compilers), objectql `having`, formula — '
+ 'in ONE PR — spec word list, driver-memory (query path, analytics face), driver-sql, '
+ 'driver-turso (remote transport), driver-mongodb, service-analytics (3 compilers), '
+ 'objectql `having`, formula — '
+ 'then empty this list. #6520 is the worked example of the clearing direction.',
).toEqual(STAGED_AHEAD_OF_BACKENDS);
});
Expand Down
7 changes: 4 additions & 3 deletions packages/spec/src/data/filter.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,9 +467,10 @@ const ORDERING_COMPARAND_DESCRIPTION =
*/
function nullOrderingComparandMessage(op: string): string {
return (
`null is not a valid ${op} comparand. null is not ordered, and no two evaluation faces `
+ 'agree on what an ordering against it matches (driver-memory\'s live path reads two '
+ 'absences as equal; its reference matcher compares through JS coercion). State absence '
`null is not a valid ${op} comparand. null is not ordered, and the evaluation faces do not `
+ 'agree on what an ordering against it matches (driver-memory\'s query path reads a '
+ 'stored null as equal to it, so {"$gte": null} admits that row; driver-sql compares '
+ 'against SQL NULL and admits no row). State absence '
+ 'with the null predicate instead: {"$eq": null} is "has no value", {"$ne": null} is '
+ '"has a value". Ruled 2026-09-01: a null ordering comparand is refused at the validation '
+ 'entrance.'
Expand Down
Loading