Skip to content

Commit e3f9295

Browse files
committed
docs(service-analytics): record the comparand-TYPE face in the where door's docblocks
The module header gains the #20035 section; the #6386, #5234 and toSqlBindValue notes say which positions the type face now answers first and which the door's own gates keep. Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF Co-authored-by: Claude <noreply@anthropic.com>
1 parent e48c570 commit e3f9295

1 file changed

Lines changed: 57 additions & 11 deletions

File tree

‎packages/services/service-analytics/src/strategies/filter-normalizer.ts‎

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,10 @@
280280
* The refusal is {@link undefinedComparandError}, in this module's existing
281281
* envelope (`INVALID_FILTER` / 400) — the opposite attribution from
282282
* `read-scope-sql`'s 500, and deliberately so: that door compiles a platform
283-
* artifact, this one receives what the CALLER wrote.
283+
* artifact, this one receives what the CALLER wrote. [#20035] In every
284+
* position the shared comparand-TYPE face judges, that face now refuses the
285+
* `undefined` first, in the same envelope and in its own sentence (see the
286+
* #20035 section below); this gate answers the positions it does not judge.
284287
*
285288
* ⛔ `null` does not move, and that is the way this change could do harm: the two
286289
* live one `===` apart in every polarity table here. `{d: null}`, `{$eq: null}`,
@@ -361,9 +364,27 @@
361364
* `INVALID_FILTER` / 400. {@link assertWhereComparandShapes} now hands every
362365
* field entry to the face after the #19888 equality pass, so both spellings
363366
* get the face's own refusal byte for byte, on every face of this door and on
364-
* the draft preview. The comparand-TYPE face is not run here; `undefined`
365-
* keeps {@link assertDefinedComparands}' refusal except as a `$between`
366-
* endpoint, which the face now answers.
367+
* the draft preview. [#20035] The comparand-TYPE face now runs right after
368+
* it — the next section.
369+
*
370+
* # …and so does the shared comparand-TYPE face (#20035)
371+
*
372+
* The maintainer's ruling on #7872 (2026-08-12) puts the accepted comparand
373+
* types — `string | number | bigint | boolean | null | Date` — on the shared
374+
* type face, `normalizeFilterComparandTypes`, which 「refuses everything else
375+
* loudly at the compile face」 and narrows a `bigint` within 2^53 to its
376+
* number. `parseFilterAST` runs it on the `FilterArray` spelling and the
377+
* engine seam on every object-form `where`; the object spelling of this door
378+
* never met it, so a plain object under `$ne` bound as JSON text and served
379+
* every row while the other spelling was refused 400. {@link normalizeWhereComparands}
380+
* now runs it after the shape face and before any node is built, in
381+
* `parseFilterAST`'s order, and the condition this door lowers is the face's
382+
* RETURN value. Refusals this door gave in its own words for a position the
383+
* type face judges (#6386's `undefined`, #5234's unbindable member and
384+
* LIKE-family comparand) now read in the face's words; the positions it does
385+
* not judge keep theirs. Binary is reconciled to the face's refusal rather
386+
* than kept as a declared local extra — the evidence is on
387+
* {@link normalizeWhereComparands}.
367388
*
368389
* Row-result cover: `filter-operator-coverage.test.ts` for the operator
369390
* vocabulary, `native-sql-filter-logic-conformance.test.ts`, which runs the
@@ -381,7 +402,8 @@
381402
* `where-equality-slot-list-refusal.test.ts` for the equality-slot list refusal
382403
* on every analytics face and its neighbouring shapes (#19888), and
383404
* `where-face-arms-refusal.test.ts` for the face's other arms, both spellings,
384-
* every face (#20010).
405+
* every face (#20010), and `where-type-face-refusal.test.ts` for the
406+
* comparand-TYPE face, both spellings, every face, and its narrowing (#20035).
385407
*/
386408

387409
import {
@@ -500,7 +522,8 @@ const MONGO_TO_CUBE_OP: Record<string, string> = {
500522
* ## Addendum (#6386): the `undefined` arm is now UNREACHABLE from this door
501523
*
502524
* {@link assertDefinedComparands} refuses an `undefined` before any comparand is
503-
* read, and it covers every call site of this function — the `$between` bounds,
525+
* read — and since #20035 the shared comparand-TYPE face refuses it before that,
526+
* in every position it judges — and it covers every call site of this function — the `$between` bounds,
504527
* the operator value and its array members, and the implicit `=` (the bare-array
505528
* `$in` that used to be a fifth call site is refused whole since #19888) — so
506529
* nothing can arrive here holding `undefined` any more. The refusal
@@ -619,7 +642,11 @@ function andOf(children: NormalizedFilterNode[]): NormalizedFilterNode | null {
619642
*
620643
* Two shapes are refused, the two #5234 measured. `$eq` and friends keep
621644
* binding any OTHER object as JSON (`toSqlBindValue`), which remains a separate
622-
* account.
645+
* account. [#20035] That account is closed: the shared comparand-TYPE face
646+
* refuses a plain object, a `Map`, a binary or a class instance in every
647+
* comparand position before this function runs (the #7872 ruling). From the
648+
* `where` door this gate now answers only what that face steps around — an
649+
* ARRAY and a `{ $field }` reference, as a list member or a LIKE comparand.
623650
*
624651
* ⚠️ [#7598, maintainer ruling 2026-08-12 Q1 = B] A THIRD arm briefly lived
625652
* here — a `{$field}` reference in the comparand of the six scalar comparison
@@ -793,14 +820,29 @@ function undefinedComparandError(field: string, path: string): Error {
793820
* that face before any leaf is built, so this gate's sentence is reached
794821
* only in the other positions.
795822
*
823+
* [#20035] From the `where` door, the shared comparand-TYPE face now answers
824+
* first in every position it judges — the implicit comparand, each declared
825+
* operator's comparand, each `$in` / `$nin` member — in its own sentence and at
826+
* its own path (the #7872 ruling). This gate's sentence is reached only where
827+
* that face steps around: a member of an ARRAY comparand outside the list
828+
* operators (`{d: {$contains: ['a', undefined]}}`) and the comparand of an
829+
* operator outside the vocabulary (`{d: {$wat: undefined}}`). It stays as
830+
* {@link fieldLeaves}' invariant, the same stance {@link assertCompilableComparand}
831+
* takes, and `where-type-face-refusal.test.ts` pins those two positions.
832+
*
796833
* `$null` / `$exists` are deliberately NOT swept, exactly as on the twin: their
797834
* comparand is a declared BOOLEAN — a flag, not a value to compare against — so
798835
* `undefined` there is not a comparand at all. ⚠️ This module reads that flag by
799836
* IDENTITY (`=== true` / `=== false`, see {@link fieldLeaves}) where the twin
800-
* reads it by truthiness, so `{$null: undefined}` lowers here to `set`
837+
* reads it by truthiness, so `{$null: undefined}` used to lower here to `set`
801838
* (`IS NOT NULL`). That is the boolean-DOMAIN question #5347 / #5369 opened and
802-
* #6387 is measuring on the sibling door; it is a different cell and is not
803-
* decided as a rider on this one.
839+
* #6387 measured on the sibling door; it is a different cell and is not
840+
* decided as a rider on this one. [#20035] The `undefined` half of it is
841+
* answered upstream now, and not by this gate: the shared comparand-TYPE face
842+
* judges the `$null` / `$exists` comparand as a literal (its operator split),
843+
* so from the `where` door `{$null: undefined}` is refused before any leaf
844+
* exists. An ACCEPTED non-boolean flag (`{$null: 'false'}`) still reaches the
845+
* identity read unchanged.
804846
*
805847
* ## Why the gate sits HERE, and what that decides for `{$not: {d: undefined}}`
806848
*
@@ -2086,7 +2128,11 @@ export function collectFilterLeaves(
20862128
* unbindable object would otherwise reach the driver.
20872129
* - any other object / array → JSON text. Not a meaningful comparison on any
20882130
* column, but the shape `filter.zod.ts` cannot exclude, and a driver-level
2089-
* bind error tells the author nothing about their filter.
2131+
* bind error tells the author nothing about their filter. [#20035] From the
2132+
* `where` door no plain object, `Map`, binary or class instance reaches
2133+
* this arm any more: the shared comparand-TYPE face refuses each before a
2134+
* leaf exists, where it used to bind here as JSON text (a plain object
2135+
* under `$ne` served every row that way).
20902136
*
20912137
* `number`, `bigint`, `null` and `string` pass through — `null` included, and
20922138
* that is deliberate: `col > NULL` is UNKNOWN, so the widget draws nothing. It is

0 commit comments

Comments
 (0)