Skip to content

fix(driver-memory): render the analytics echo as the query it describes (#7117) - #7852

Merged
huangyiirene merged 1 commit into
mainfrom
claude/issue-7117-analytics-like-wildcards
Aug 11, 2026
Merged

fix(driver-memory): render the analytics echo as the query it describes (#7117)#7852
huangyiirene merged 1 commit into
mainfrom
claude/issue-7117-analytics-like-wildcards

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes #7117

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 echo's only job is reproducing execution, so an author who ran it to debug the chart got a narrower row set and read the filter as broken: the #5333 / #3650 class, reached through driver-memory's analytics face.

Both axes, because #7723 opened the second one

#7723 made the $contains family case-exact on this package's execution faces (#4706 Q2 = A) — including filterSubstringPattern, which this face borrows. Measured on 69fde55 before this PR:

axis query() echoed
containment substring equality — no wildcard
case ($contains) exact LIKE folds ASCII on a SQLite consumer

The halves are one fix, exactly as #7723's body predicted: the case-exact construct on SQLite is GLOB, GLOB speaks a different pattern language from LIKE, so choosing the construct and rendering the wildcards are the same decision — half of it emits incoherent SQL.

What the echo emits now. $containscol GLOB '*v*', $notContains(col IS NULL OR col NOT GLOB '*v*'), $icontainslower(col) GLOB lower('*v*'). SQLite because that is the dialect this exit has always emitted (toSqlLiteral spells booleans 1/0; #6520's icontains row reasoned from SQLite's ASCII-only LIKE) — stated as an executed premise in the new suite so it goes red rather than stale. 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); a third hand-copy of that escape is what service-analytics' like-pattern.ts header says to refuse.

Every face of this package's analytics surface, declared

face carries this defect? disposition
query() (mingo $match) no — it is the reference untouched; its rows are what the echo is now pinned against
generateSql() yes fixed here; this is the whole diff
generateSqlFromPipeline() (AnalyticsResult.sql) no, but a different, lesser defect it is not SQL — it prints the executed pipeline as /* Stage n: $match */ <JSON>, so it cannot render a predicate execution did not run. It does lose a RegExp operand to {} in JSON.stringify (measured: {"name":{"$regex":{}}}). Visibly degenerate rather than plausibly wrong, and a separate surface — filed, not folded in
measureToSql() n/a measures only (COUNT/SUM/…); no filter operator reaches it

The || '=' fallback, and what measurement said about it

The card expected startsWith / endsWith to fall to operatorToSql's || '='. They cannot — they are not in MONGO_TO_CUBE_OPERATOR, so #5345's gate refuses them on both exits with INVALID_FILTER / 400 before any lowering. That premise is falsified, and the new suite asserts the refusal so it stays that way.

Three other operators were reaching it. A name→name map cannot hold a wildcard, a list, or a null-safe negation, so operatorToSql is now a builder table keyed by CubeOperator — the shape #5374 gave the mingo exit — and widening the vocabulary fails to compile until the SQL spelling exists. Measured against query():

where query() echoed, before echoed, now
{name: {$in: [a, b]}} both rows name = a — one row name IN (a, b)
{name: {$nin: [a]}} the other five name = a — the complement (name IS NULL OR name NOT IN (a))
{name: {$exists: true}} five rows name = 1no rows name IS NOT NULL

Three smaller divergences on the same builder went with them, each measured before it was changed:

The one cell SQL cannot translate exactly, stated rather than hidden: mingo's $exists tests KEY PRESENCE, which a relational column always has, so a row storing an explicit null satisfies $exists: true on query() and fails IS NOT NULL in the echo. IS NOT NULL is nonetheless the spelling both of this repo's other SQL lowerings use (read-scope-sql.ts's $exists arm; driver-sql's "a present field is a non-null column in SQL"), and it is a far smaller gap than the name = 1 it replaces, which matched nothing. It is pinned as an explicit inequality in the new suite so it cannot be closed in silence.

The check is the one the maintainer required

memory-analytics-echo-operator-coverage.test.ts (44 cases) pins generateSql()'s WHERE against query()'s row set, not against a string — comment 5234868334, and the shape objectql-echo-operator-coverage.test.ts uses. The echoed statement is executed on a real SQLite engine (sql.js, pure WASM, for the ABI reason native-sql-filter-logic-conformance.test.ts gives) over the same six-row fixture the pipeline runs on. A string assertion would not have caught the missing % either: WHERE name LIKE 'acme' reads as a working predicate. The closed vocabulary is enumerated in both directions — every accepted operator round-trips, every refused one refuses identically on both exits — so the two tables cannot drift apart unnoticed.

Reverse verification, predicted first then measured — all eight held, recorded in the suite's docblock. Examples: reverting GLOB to LIKE '%v%' keeps containment and breaks CASE (expected ['1','2'] to deeply equal ['2']); dropping the comparand's LIKE-escape widens {$contains: '%'} from one row to every non-null row; restoring the values.length > 0 guard makes an empty $in echo the whole table.

Prose corrected

Docblocks in this package that #7723 made false and this PR reasons directly against: MongoPredicateInput.substring / .asciiSubstring (called the family Unicode-folding and cited #6682 as open), memory-like-pattern.test.ts's $contains control (said the two faces disagree — measured, they now both answer ['1','2','3']), and memory-icontains.test.ts's coverage note (said the DEBT row stays until #6682 closes). Equivalent stale #6682 prose in packages/spec is filed separately rather than dragged into a driver-memory patch.

Gates

gate result
@objectstack/driver-memory tests 708 passed / 23 files (was 664 / 22)
@objectstack/spec tests 9970 passed / 378 files
dependents (pnpm --filter '...@objectstack/driver-memory', measured — 17 packages) all green: runtime 2057, cli 1182, driver-turso 946, service-datasource 326, client 282, cloud-connection 112, hono 73, http-conformance 72, plugin-dev 45, client-react 34, verify 23, embed-objectql 2
tsc --noEmit clean
pnpm lint clean
check:query-options-erasure holds — 67 non-test sites / 17 files, none new; baseline verified against 69fde55
check:wildcard-fallthrough 7 yielding / 0 ratcheted / 5 exempt
check:driver-conformance 40 covered / 0 DEBT / 0 exempt — unchanged

Changeset: @objectstack/driver-memory patch — the displayed SQL is user-visible. content/docs/releases/ untouched.


Generated by Claude Code

…es (#7117)

`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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPYmYr8mjAsbZ6RqwEk4TT
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 11, 2026 8:44pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/driver-memory.

8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-memory)
  • content/docs/deployment/vercel.mdx (via @objectstack/driver-memory)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-memory)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/driver-memory)
  • content/docs/permissions/authentication.mdx (via @objectstack/driver-memory)
  • content/docs/plugins/index.mdx (via @objectstack/driver-memory)
  • content/docs/plugins/packages.mdx (via @objectstack/driver-memory)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/driver-memory)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/driver-memory)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

driver-memory's analytics generateSql() renders the LIKE family with NO wildcards, so the echoed statement is an EQUALITY the pipeline never ran

2 participants