|
280 | 280 | * The refusal is {@link undefinedComparandError}, in this module's existing |
281 | 281 | * envelope (`INVALID_FILTER` / 400) — the opposite attribution from |
282 | 282 | * `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. |
284 | 287 | * |
285 | 288 | * ⛔ `null` does not move, and that is the way this change could do harm: the two |
286 | 289 | * live one `===` apart in every polarity table here. `{d: null}`, `{$eq: null}`, |
|
361 | 364 | * `INVALID_FILTER` / 400. {@link assertWhereComparandShapes} now hands every |
362 | 365 | * field entry to the face after the #19888 equality pass, so both spellings |
363 | 366 | * 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}. |
367 | 388 | * |
368 | 389 | * Row-result cover: `filter-operator-coverage.test.ts` for the operator |
369 | 390 | * vocabulary, `native-sql-filter-logic-conformance.test.ts`, which runs the |
|
381 | 402 | * `where-equality-slot-list-refusal.test.ts` for the equality-slot list refusal |
382 | 403 | * on every analytics face and its neighbouring shapes (#19888), and |
383 | 404 | * `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). |
385 | 407 | */ |
386 | 408 |
|
387 | 409 | import { |
@@ -500,7 +522,8 @@ const MONGO_TO_CUBE_OP: Record<string, string> = { |
500 | 522 | * ## Addendum (#6386): the `undefined` arm is now UNREACHABLE from this door |
501 | 523 | * |
502 | 524 | * {@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, |
504 | 527 | * the operator value and its array members, and the implicit `=` (the bare-array |
505 | 528 | * `$in` that used to be a fifth call site is refused whole since #19888) — so |
506 | 529 | * nothing can arrive here holding `undefined` any more. The refusal |
@@ -619,7 +642,11 @@ function andOf(children: NormalizedFilterNode[]): NormalizedFilterNode | null { |
619 | 642 | * |
620 | 643 | * Two shapes are refused, the two #5234 measured. `$eq` and friends keep |
621 | 644 | * 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. |
623 | 650 | * |
624 | 651 | * ⚠️ [#7598, maintainer ruling 2026-08-12 Q1 = B] A THIRD arm briefly lived |
625 | 652 | * here — a `{$field}` reference in the comparand of the six scalar comparison |
@@ -793,14 +820,29 @@ function undefinedComparandError(field: string, path: string): Error { |
793 | 820 | * that face before any leaf is built, so this gate's sentence is reached |
794 | 821 | * only in the other positions. |
795 | 822 | * |
| 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 | + * |
796 | 833 | * `$null` / `$exists` are deliberately NOT swept, exactly as on the twin: their |
797 | 834 | * comparand is a declared BOOLEAN — a flag, not a value to compare against — so |
798 | 835 | * `undefined` there is not a comparand at all. ⚠️ This module reads that flag by |
799 | 836 | * 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` |
801 | 838 | * (`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. |
804 | 846 | * |
805 | 847 | * ## Why the gate sits HERE, and what that decides for `{$not: {d: undefined}}` |
806 | 848 | * |
@@ -2086,7 +2128,11 @@ export function collectFilterLeaves( |
2086 | 2128 | * unbindable object would otherwise reach the driver. |
2087 | 2129 | * - any other object / array → JSON text. Not a meaningful comparison on any |
2088 | 2130 | * 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). |
2090 | 2136 | * |
2091 | 2137 | * `number`, `bigint`, `null` and `string` pass through — `null` included, and |
2092 | 2138 | * that is deliberate: `col > NULL` is UNKNOWN, so the widget draws nothing. It is |
|
0 commit comments