diff --git a/.changeset/21397-null-ordering-message-faces.md b/.changeset/21397-null-ordering-message-faces.md new file mode 100644 index 00000000000..e96c007002b --- /dev/null +++ b/.changeset/21397-null-ordering-message-faces.md @@ -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. diff --git a/packages/spec/src/data/filter-comparand-shape.ts b/packages/spec/src/data/filter-comparand-shape.ts index c225b1ffede..2c903708ac5 100644 --- a/packages/spec/src/data/filter-comparand-shape.ts +++ b/packages/spec/src/data/filter-comparand-shape.ts @@ -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.`, diff --git a/packages/spec/src/data/filter-operator-vocabulary.test.ts b/packages/spec/src/data/filter-operator-vocabulary.test.ts index 65714f0887c..c68a6bcfefb 100644 --- a/packages/spec/src/data/filter-operator-vocabulary.test.ts +++ b/packages/spec/src/data/filter-operator-vocabulary.test.ts @@ -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); }); diff --git a/packages/spec/src/data/filter.zod.ts b/packages/spec/src/data/filter.zod.ts index 47cc9375123..79b39215046 100644 --- a/packages/spec/src/data/filter.zod.ts +++ b/packages/spec/src/data/filter.zod.ts @@ -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.'