Commit f7df82c
`MemoryAnalyticsService` lowered `AnalyticsQuery.where` into a cube-style
`{member, operator, values}` list whose `values` was `string[]`, so every
comparand made a JS value → string → JS value round trip. That round trip is
lossy for anything not already a string, and both of the issue's symptoms are
the one root cause:
- `stringifyForCube(true)` → `'1'` → `coerceFilterValue('1')` → the NUMBER 1
(the `/^-?\d+$/` arm wins), compared against a stored `true`. mingo compares
cross-type as never-equal, so `{is_active: true}` matched zero rows.
- `flattenFilterCondition` opened with `if (raw == null) continue`, so
`{closed_at: null}` produced no cube entry at all and the predicate vanished.
Fewer constraints means MORE rows: the query widened to the full table. This
is the #3948 direction and the more dangerous half — a widened chart looks
exactly like a working chart.
Route B of the issue's three: stop round-tripping. `values` is `unknown[]`;
stringification happens only at the `generateSql` exit, where a SQL literal is
genuinely needed. A/C were rejected because A layers a tag on an encoding the
issue already calls suspect, and C would refuse
`{is_active: true, stage: {$nin: ['lost']}}` — `AnalyticsQuerySchema.where`'s
own docstring example.
B is affordable because the triple is a purely INTERNAL intermediate. Verified
rather than assumed before building on it: `normalizeFilters`,
`flattenFilterCondition`, `stringifyForCube`, `coerceFilterValue` and
`toSqlLiteral` are all private and referenced only in this file; `index.ts`
exports only the class; `IAnalyticsService` exposes no such shape; and the API
layer actively REJECTS a `{member, operator, values}` array on the wire
(`spec/src/api/analytics.test.ts`, `runtime/src/http-dispatcher.test.ts` both
assert the rejection). Zero spec bytes touched.
The encoding could not simply be made lossless: its own justification for
`'1'`/`'0'` ("downstream consumers expecting SQLite-style numeric booleans") is
true for the SQL exit and false for the in-memory one, and both exits shared it.
Both exits are fixed, because a fix that satisfied mingo while emitting SQL
meaning something else would only move the loss. `toSqlLiteral` now takes the
real value instead of guessing a type back out of text — a TEXT `'100'` is
quoted where it used to emit `code = 100` — and a null comparand becomes
`IS NULL` / `IS NOT NULL` rather than SQL's never-true `= NULL`.
Temporal comparands still convert, now via the driver's own storage-form rule
(new narrow `filterComparandStorageForm`, keyed on the declared field kind,
#4047) instead of an ad-hoc `toISOString()`, so a `Date` still meets a declared
`datetime` column and no second derivation of that rule appears in this face
(#5240).
The issue's unverified third symptom is REAL and fixed by the same change:
`{code: '100'}` against a TEXT column storing `'100'` round-tripped to the
number 100 and matched zero rows. Measured, along with two more of the same
root cause the issue did not list — `{is_active: {$ne: true}}` and
`{closed_at: {$ne: null}}` each returned the whole table.
Measured on the issue's 3-row fixture, analytics vs `find()`:
| where | before | after | find() |
|---|---|---|---|
| `{is_active: true}` | 0 | 2 | 2 |
| `{is_active: false}` | 0 | 1 | 1 |
| `{closed_at: null}` | 3 | 2 | 2 |
| `{code: '100'}` | 0 | 2 | 2 |
| `{is_active: {$ne: true}}` | 3 | 1 | 1 |
| `{closed_at: {$ne: null}}` | 3 | 1 | 1 |
Tests go in the shared conformance file beside the #5324/#5345 shape table
rather than a suite of their own. `FILTER_LOGIC_CASES` varies filter SHAPE over
an all-string fixture — deliberately, so nothing in it is about coercion —
which is exactly why every case stayed green through this defect. The new block
varies comparand TYPE and holds the same invariant: agree with `find()`, or
refuse. Reverting only the source change fails 11 of the new assertions, across
both exits.
Out of scope, not fixed here: #5374 (`$notContains` → bare mingo `{$not: 'x'}`),
held to follow serially since its call site is the value-compilation point this
PR changes.
Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0c52202 commit f7df82c
4 files changed
Lines changed: 489 additions & 65 deletions
File tree
- .changeset
- packages/plugins/driver-memory/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
0 commit comments