Skip to content

Commit 9bf4dd0

Browse files
huangyiireneclaude
andauthored
fix(driver-memory): render the analytics echo as the query it describes (#7117) (#7852)
`MemoryAnalyticsService` has two exits for one normalized filter tree and they disagreed about what the LIKE family MEANS. `query()` builds a real containment pattern; `generateSql()` emitted the comparand as a bare literal with no wildcard anywhere, so `{name: {$contains: 'acme'}}` echoed `WHERE name LIKE 'acme'` — an EQUALITY — beside a chart drawn from every row CONTAINING `acme`. `notContains` mirrored it through `NOT LIKE`. The `$contains` family now renders `GLOB '*v*'` / `NOT GLOB`, and `$icontains` `lower(col) GLOB lower('*v*')`. GLOB rather than LIKE because this exit emits SQLite-shaped SQL and SQLite's LIKE folds ASCII unconditionally, while #4706 Q2 = A rules the family case-SENSITIVE and #7723 put this package's execution faces on that answer — a LIKE echo would have contradicted execution on a second axis the moment the wildcards were added. The translation is the spec's shared `likePatternToGlobPattern`, over a LIKE-escaped comparand, so an author's own `%` / `_` / `*` / `?` / `[` stay literal instead of becoming the match-every-row bypass (#5567). `operatorToSql`'s `|| '='` fallback is gone with it: a name→name map cannot hold a wildcard, a list, or a null-safe negation, so it is now a builder table keyed by `CubeOperator` — the shape #5374 gave the mingo exit — and a widened vocabulary fails to compile until its SQL spelling exists. Three operators were reaching that fallback and are fixed with it: `{$in: [a,b]}` echoed `= a`, `{$nin: [a]}` echoed the exact COMPLEMENT of the query's rows, and `{$exists: true}` echoed `name = 1`, which selects nothing. Three smaller divergences on the same builder went too — negations are null-safe (#5146 / #5297), an empty `$in`/`$nin` renders a predicate instead of no WHERE, and a bare-day `$lte` renders half-open as the pipeline has read it since #4042. `startsWith` / `endsWith` never reached the fallback and are unchanged: this face does not lower them, so both exits refuse them with `INVALID_FILTER` / 400. `memory-analytics-echo-operator-coverage.test.ts` pins the WHERE against `query()`'s ROW SET by executing the echoed statement on a real SQLite engine (`sql.js`), enumerates the closed vocabulary on both exits, and records the eight-way reverse verification. Only the DISPLAYED SQL changes; `query()`'s rows are untouched. Claude-Session: https://claude.ai/code/session_01TPYmYr8mjAsbZ6RqwEk4TT Co-authored-by: Claude <noreply@anthropic.com>
1 parent c9c2d92 commit 9bf4dd0

7 files changed

Lines changed: 813 additions & 71 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
"@objectstack/driver-memory": patch
3+
---
4+
5+
fix(driver-memory): the analytics echo renders the query it describes (#7117)
6+
7+
`MemoryAnalyticsService` has two exits for one normalized filter tree, and they
8+
disagreed about what the LIKE family MEANS. `query()` builds a real containment
9+
pattern; `generateSql()` emitted the comparand as a bare literal with no
10+
wildcard anywhere, so `{name: {$contains: 'acme'}}` echoed
11+
12+
```sql
13+
WHERE name LIKE 'acme'
14+
```
15+
16+
— an **equality** — beside a chart drawn from every row *containing* `acme`.
17+
The echo's only job is reproducing execution, so an author who ran it to debug
18+
the chart got a **narrower** row set and read the filter as broken.
19+
`$notContains` mirrored it through `NOT LIKE`.
20+
21+
**What the echo emits now.** The `$contains` family renders `GLOB '*v*'` /
22+
`NOT GLOB`, and `$icontains` renders `lower(col) GLOB lower('*v*')`. `GLOB`
23+
rather than `LIKE` because this exit emits SQLite-shaped SQL and SQLite's `LIKE`
24+
folds ASCII case unconditionally: #4706 Q2 = A rules the `$contains` family
25+
case-**sensitive**, and #7723 put this package's execution faces on that answer,
26+
so a `LIKE` echo would have contradicted execution on a second axis the moment
27+
the missing wildcards were added. The two halves are one fix because `GLOB`
28+
speaks a different pattern language from `LIKE` — choosing the construct and
29+
rendering the wildcards are the same decision. The translation is the spec's
30+
shared `likePatternToGlobPattern`, and the comparand is escaped first, so an
31+
author's own `%` / `_` / `*` / `?` / `[` stay literal instead of becoming the
32+
match-every-row bypass (#5567).
33+
34+
**The `|| '='` fallback is gone with it.** `operatorToSql` was a
35+
name→name map, which cannot hold a wildcard, a list, or a null-safe negation;
36+
it is now a builder table keyed by `CubeOperator`, the shape #5374 gave the
37+
mingo exit, so a widened vocabulary fails to compile until its SQL spelling
38+
exists. Three operators were reaching that fallback and are fixed with it —
39+
measured against `query()` on a six-row fixture:
40+
41+
| `where` | `query()` | echoed, before | echoed, now |
42+
|---|---|---|---|
43+
| `{name: {$in: [a, b]}}` | both rows | `name = a` — one row | `name IN (a, b)` |
44+
| `{name: {$nin: [a]}}` | the other five | `name = a` — the **complement** | `(name IS NULL OR name NOT IN (a))` |
45+
| `{name: {$exists: true}}` | five rows | `name = 1`**no** rows | `name IS NOT NULL` |
46+
47+
Three smaller divergences on the same builder went with them: negations are
48+
null-safe (`$ne` / `$nin` / `$notContains` kept only rows whose column was not
49+
NULL, where the pipeline returns them — the #5146 / #5297 rule the rest of the
50+
repo already follows); an empty `$in` / `$nin` list now renders a predicate
51+
instead of no `WHERE` at all (an empty `$in` echoed the whole table while the
52+
pipeline returns nothing); and a bare-day `$lte` bound renders half-open, as the
53+
pipeline has read it since #4042.
54+
55+
`$startsWith` and `$endsWith` never reached the fallback and are unchanged: this
56+
face does not lower them, so both exits refuse them with `INVALID_FILTER` / 400
57+
(#5345).
58+
59+
Only the *displayed* SQL changes — this exit produces the statement shown for
60+
transparency, never the query that runs, and `query()`'s rows are untouched.

packages/drivers/driver-memory/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
},
2626
"devDependencies": {
2727
"@types/node": "^26.1.2",
28+
"@types/sql.js": "^1.4.11",
29+
"sql.js": "^1.14.1",
2830
"typescript": "^6.0.3",
2931
"vitest": "^4.1.10"
3032
},

0 commit comments

Comments
 (0)