Skip to content
Merged
23 changes: 23 additions & 0 deletions .changeset/20822-retired-matcher-pointers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@objectstack/spec': patch
'@objectstack/service-analytics': patch
'@objectstack/formula': patch
'@objectstack/objectql': patch
---

Published comments that named `driver-memory`'s retired reference matcher as a live filter backend now name what replaced it

Clause-②: no

`driver-memory`'s reference matcher (`memory-matcher.ts`) was retired in commit `8fec76a2b`. Four published packages still described it as a live surface in text that ships:

- `@objectstack/spec`:
- The backend table in the filter-logic conformance docblock, which ships in `data/index.d.ts` and `data/index.d.mts`, now lists the in-memory backend as `driver-memory`'s query path (`normalizeFilterCondition`, then mingo) where it listed `memory-matcher`, and says the matcher held that row until commit `8fec76a2b` retired it.
- `src/data/filter.zod.ts` ships as source. In it, the `$icontains` implementation table lists `driver-memory`'s query path and analytics face, both on `asciiCaseInsensitiveRegexSource`. The `$like` / `$ilike` and `$empty` tables keep the matcher only in a note that commit `8fec76a2b` retired it. The `foldAsciiCase` docblock counts five JS evaluation faces where it counted six. The `asciiCaseInsensitiveContains` docblock names objectql's `having` and `formula` as its callers. The string-ordering note says `driver-memory`'s query path hands the comparison to mingo. Of these, the `foldAsciiCase`, `asciiCaseInsensitiveContains` and `FILTER_OPERATORS` docblocks also ship in the filter declaration chunk (`filter.zod-*.d.ts` / `.d.mts`).
- `src/ui/view.zod.ts` ships as source. It now says that `driver-memory`'s query path runs `assertFilterConditionShape` through `convertToMongoQuery`, where it said `match()` did.
- A comment inside `FILTER_TEXT_CASES` ships in `data/index.js` / `.mjs` and `browser/data/index.js` / `.mjs`. It now says the reference matcher measured case-exact until commit `8fec76a2b` retired it.
- `@objectstack/service-analytics`: two comments in `ObjectQLStrategy`, which ship in the JavaScript output (the first also in `index.d.ts` / `index.d.cts`), changed. The first names `driver-memory`'s query path, not its matcher, as a face that pins `{$not: {}}` as the zero-row filter. The second says in the past tense that `memory-matcher.ts` read `$regex` as a real regex, until `$regex` was retired and commit `8fec76a2b` retired the matcher too.
- `@objectstack/formula`: the comment over the `$icontains` arm in `matches-filter.ts` ships in `index.js` / `index.mjs`. It now names objectql's `having` as the other caller of `asciiCaseInsensitiveContains`. It says `driver-memory`'s reference matcher called it until commit `8fec76a2b` retired it, and that `driver-memory`'s query path folds through `asciiCaseInsensitiveRegexSource`.
- `@objectstack/objectql`: the comment over the `having` walker's `$notContains` arm in `having-filter.ts` ships in `index.js` / `index.mjs` and `core.js` / `core.mjs`. It now says the record-at-a-time faces (`formula` and this walker) answer the predicate on a stored value that is not a string, as `driver-memory`'s reference matcher did until commit `8fec76a2b` retired it.

Comment only: no export, type, error code, status, message text or runtime behaviour changes.
6 changes: 3 additions & 3 deletions docs/design/predicate-compilation-convergence.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Vocabulary is counted against the 19 field operators an author can write. That i
| F1 | `driver-sql` | `SqlDriver.applyFilters` → `compileFilters` → `applyFilterCondition` (`sql-driver.ts:16350` / `16462` / `16789`) | `find`/`findOne`, `count`, `aggregate`, `distinct`, `updateMany`, `deleteMany`, `findWithWindowFunctions`, `analyzeQuery` on PostgreSQL, MySQL and SQLite. `driver-sqlite-wasm` and `driver-turso` local/replica mode inherit it (`extends SqlDriver`). | 19/19, plus `$and`/`$or`/`$not` and `$field` | none (relies on the engine seam); calls `reduceFilterVerdict`, `isAcceptedFilterComparand`, `expandEmptyOperator` |
| F2 | `driver-turso` remote | `TursoDriver.toRemoteFilter` (`turso-driver.ts:2566`, a lowering pass) → `RemoteTransport.buildWhereSQL` / `compileWhereSQL` (`remote-transport.ts:2852` / `2896`) | remote-mode `find`, `count`, `aggregate`, `distinct`, `updateMany`, `deleteMany` | 18/19 (`$between` is lowered before the transport) | none; calls `isAcceptedFilterComparand`, `expandEmptyOperator` |
| F3 | `driver-memory` query path | `InMemoryDriver.convertToMongoQuery` (`memory-driver.ts:1334`) → mingo `Query`, behind `assertFilterConditionShape` (`filter-refusal.ts`) | `find`/`findOne`, `count`, `distinct`, `aggregate`, `updateMany`, `deleteMany` | 19/19 | none (relies on the engine seam) |
| F4 | `driver-memory` reference matcher | `match()` (`memory-matcher.ts:43`) | **no production caller.** It is not exported from the package index, and 20 test files import it. | 19/19 | none |
| F4 | `driver-memory` reference matcher | `match()` (`memory-matcher.ts:43`) | **no production caller.** It was not exported from the package index, and 20 test files imported it. **Retired** under D6 by commit `8fec76a2b`: `memory-matcher.ts` is deleted, and the tests that imported it assert on F3, the shared gate or the spec predicate. | 19/19 | none |
| F5 | `driver-memory` cube face | `MemoryAnalyticsService.query` / `generateSql` → `normalizeFilters` (`memory-analytics.ts:910` / `1389` / `1501`) → mingo `$match` and echo SQL | the published `@objectstack/driver-memory` export. No in-repo door constructs it (recorded on #20661). | 12/19 (no `$between`, `$startsWith`, `$endsWith`, `$null`, `$empty`, `$like`, `$ilike`); `$and` is its only combinator | **none** — see §2.5 |
| F6 | `driver-mongodb` | `translateFilter` (`mongodb-filter.ts:805`); the aggregation `$match` reuses it (`mongodb-aggregation.ts:556`) | all CRUD verbs and `aggregate` | 17/19 (no `$like`/`$ilike`) | none (relies on the engine seam) |
| F7 | `formula` | `matchesFilterCondition` (`matches-filter.ts:322`) | the RLS `check` on a write's post-image (`security-plugin.ts:3215`), the tenant check (`:3519`), the explain engine (`explain-engine.ts:964`), and F8's scalar comparisons (`having-filter.ts:1155`) | 19/19 | none (the RLS compile seam runs the doors first) |
Expand All @@ -53,7 +53,7 @@ Vocabulary is counted against the 19 field operators an author can write. That i
| F10c | ↳ engine hand-off | `filterNodeToCondition` (`objectql-strategy.ts:1602`) | a `FilterCondition` handed back to the engine, which F1, F3 or F6 then compile a second time | (the tree's) | — |
| F11 | `service-analytics` draft preview | `evaluateAnalyticsQueryOverRows` → `matchesWhere` (`preview-evaluator.ts:640` / `325`) | the draft-preview branch of `queryDataset` (`analytics-service.ts:1819`), reached through REST `?preview=` (`rest-server.ts:5872`) | 10/19 (`$eq $ne $gt $gte $lt $lte $between $in $nin $contains`); the rest are refused | both, through `normalizeWhereComparands` (`preview-evaluator.ts:665`) |

The 15 source files are `sql-driver.ts`, `turso-driver.ts`, `remote-transport.ts`, `memory-driver.ts`, `filter-refusal.ts`, `memory-matcher.ts`, `memory-analytics.ts`, `mongodb-filter.ts`, `matches-filter.ts`, `having-filter.ts`, `read-scope-sql.ts`, `filter-normalizer.ts`, `native-sql-strategy.ts`, `objectql-strategy.ts` and `preview-evaluator.ts`. §2 uses this list as its "face files".
The 15 source files are `sql-driver.ts`, `turso-driver.ts`, `remote-transport.ts`, `memory-driver.ts`, `filter-refusal.ts`, `memory-matcher.ts` (deleted since, by commit `8fec76a2b`), `memory-analytics.ts`, `mongodb-filter.ts`, `matches-filter.ts`, `having-filter.ts`, `read-scope-sql.ts`, `filter-normalizer.ts`, `native-sql-strategy.ts`, `objectql-strategy.ts` and `preview-evaluator.ts`. §2 uses this list as its "face files".

### 1.2 Against the card's table

Expand Down Expand Up @@ -355,7 +355,7 @@ A ruling that adds a new predicate *kind* or a dialect construct still costs one
| F1 `driver-sql` | the engine seam | no | polarity quartet (67 lines), `assertDefinedComparands`, the `calendarDay*Rewrite` calls (5 sites) — only under D4 (b) | `FILTER_LOGIC`, `FILTER_TEXT`, `TEMPORAL`, `FILTER_COMPARAND_TYPE` on SQLite, plus the PostgreSQL/MySQL live matrix | direct callers (D4); a 21,103-line file |
| F2 `driver-turso` remote | the engine seam, then `toRemoteFilter` | no | `toRemoteFilter`'s `$between` / whole-day arms (3 sites), the transport's polarity copy (69 lines) | turso filter-logic (local and remote), local/remote NULL parity | a live remote server was NOT MEASURED here |
| F3 `driver-memory` query | the engine seam | no | whole-day calls (8 sites) | memory filter-logic, temporal, text | — |
| F4 reference matcher | — (no production caller) | no | keep as test oracle, or retire (D6) | 20 test files | — |
| F4 reference matcher | — (no production caller) | no | **retired** (D6, commit `8fec76a2b`): `memory-matcher.ts` is deleted, and the tests that imported it keep their assertions on F3, the shared gate or the spec predicate | 20 test files | — |
| F5 cube face | the new `normalizeFilters` door | **yes**: doors + lowering, and widen `$or` / `$not` / `$null` | whole-day calls (5 sites) | its own suites; not in `check:driver-conformance` | an accept-set widening, so a changeset with its Clause-② line |
| F6 `driver-mongodb` | the engine seam | no | whole-day calls (4 sites) | mongodb filter-logic, text, temporal, comparand-type | the server answer was NOT MEASURED here |
| F7 `formula` | the RLS compile seam (policies); the engine (via F8) | no | **retired** (#21242): `lteBound` and its 2 sites are deleted; a bound that reaches F7 unlowered is compared as written (D-D1 item 5) | matches-filter not-null-safe, or-semantics, temporal | — |
Expand Down
6 changes: 4 additions & 2 deletions packages/drivers/driver-mongodb/src/mongodb-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1148,8 +1148,10 @@ interface LoweredWrite {
* write becomes its own `$and` branch on the same field, where both constraints
* survive. That is exactly the guard #13195 landed for `$exists` alone,
* generalised to every writer rather than restated once per operator.
* `driver-memory`'s reference matcher loops the operators and therefore cannot
* express this defect at all; it is the oracle both drivers agree with.
* `driver-memory`'s reference matcher looped the operators and therefore could
* not express this defect at all; it was the oracle both drivers agreed with
* until commit `8fec76a2b` retired it, and `driver-memory`'s
* `memory-operator-key-clobber.test.ts` keeps its answers as literal row sets.
*
* ## Why rank, and not author order
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ import { markFilterSubtreeProvenance } from '@objectstack/spec/data';
* them correctly rather than refuse them. Framework's `matchesFilterCondition`
* (`packages/formula/src/matches-filter.ts`) already evaluates them this way and
* pins it — `expect(m(rec, { $or: [] })).toBe(false) // empty OR matches
* nothing` — as does `driver-memory`'s matcher (`.some()` over an empty array).
* These tests hold the remote transport to the same table.
* nothing` — as does `driver-memory`'s query path, which runs
* `FILTER_LOGIC_CASES`' "empty $or is FALSE" case
* (`memory-driver-filter-logic-conformance.test.ts`); its reference matcher
* (`.some()` over an empty array) answered the same until commit `8fec76a2b`
* retired it. These tests hold the remote transport to the same table.
*
* The other half of the fix is that "compiles to nothing" now has exactly ONE
* cause. An element that is not a filter NODE (null, a scalar, an array, a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ import { lowerFilterCondition, markFilterSubtreeProvenance } from '@objectstack/
* — declared in the same object literal as the two that WERE implemented. And
* it is a shape real rules produce: `SqlDriver.applyFilterCondition` compiles it
* with `whereNot`/`orWhereNot` (framework#2704, added to close this same
* silent-filter-bypass family), `driver-memory`'s matcher and
* silent-filter-bypass family), `driver-memory`'s query path
* (`memory-driver-document-not.test.ts`; its reference matcher did too until
* commit `8fec76a2b` retired it) and
* `matchesFilterCondition` both evaluate it, and CEL `!expr` in a permission /
* RLS read scope lowers to `{ $not: {…} }` (`formula/src/cel-to-filter.ts`). So
* one RLS scope answered correctly on a local SqlDriver and broke on Turso
Expand Down
12 changes: 8 additions & 4 deletions packages/formula/src/matches-filter-not-null-safe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
*
* These cases are therefore a PIN on the reference behaviour, mirrored id-for-id
* by `driver-sql`'s `sql-driver-not-null-safe.test.ts` and `driver-memory`'s
* `memory-matcher-not-null-safe.test.ts`. Moving an expectation here silently
* re-opens the divergence.
* `memory-driver-document-not.test.ts` (its query path; it holds the cells of
* `memory-matcher-not-null-safe.test.ts`, deleted with the reference matcher in
* commit `8fec76a2b`). Moving an expectation here silently re-opens the
* divergence.
*
* `cel-to-filter.ts` is why this matters in practice: a CEL `!expr` in a
* permission rule lowers to exactly these `$not` shapes.
Expand Down Expand Up @@ -116,8 +118,10 @@ describe('[#5146] matchesFilterCondition — $not over records with no value', (

it('$not of $notContains does NOT match them — the mirror case', () => {
// A value-less field satisfies `$notContains` here, so the negation
// rejects it. `driver-sql` follows this answer; `driver-memory`'s
// REFERENCE matcher answers the opposite for a null-valued field.
// rejects it. `driver-sql` follows this answer, and so does
// `driver-memory`'s query path (`memory-driver-document-not.test.ts`).
// Its REFERENCE matcher answered the opposite for a null-valued field
// until PR #13356, and commit `8fec76a2b` has since retired it.
//
// ⚠️ [#5299, 2026-08-10] A ruling that morning would have reversed this
// direction; it was WITHDRAWN the same day and include re-affirmed. See
Expand Down
4 changes: 3 additions & 1 deletion packages/formula/src/matches-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,9 @@ function evalOp(
/**
* [#6520] `$contains`' case-INSENSITIVE twin, folding ASCII case and nothing
* else — `asciiCaseInsensitiveContains` is the spec's shared definition, the
* same one `driver-memory`'s matcher and objectql's `having` call.
* same one objectql's `having` calls. `driver-memory`'s reference matcher
* called it too until commit `8fec76a2b` retired it; `driver-memory`'s
* query path folds through its pattern twin, `asciiCaseInsensitiveRegexSource`.
*
* NOT `actual.toLowerCase().includes(v.toLowerCase())`, which is the obvious
* line and the wrong one: it folds the whole Unicode range, so an RLS
Expand Down
5 changes: 3 additions & 2 deletions packages/objectql/src/having-filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
*
* The namespace is the aggregated row's own columns (aggregation aliases +
* groupBy projections); operator semantics follow the Filter Protocol, with two
* deliberate divergences from driver-memory's matcher: an unknown operator
* deliberate divergences from driver-memory's reference matcher (retired since by
* commit `8fec76a2b`): an unknown operator
* throws — ignoring one would silently return unfiltered aggregates, the exact
* silently-inert failure (#4286, ADR-0078) enforcement exists to end — and the
* negation-carrying operators are NULL-safe per #5298 (see the grid at the
Expand Down Expand Up @@ -57,7 +58,7 @@ describe('applyHaving', () => {
expect(applyHaving(ROWS, { total: { $between: [600, 1300] } }).map((r) => r.customer_id))
.toEqual(['c2', 'c3']);
expect(applyHaving(ROWS, { region: { $null: true } }).map((r) => r.customer_id))
.toEqual(['c1', 'c2', 'c3']); // absent folds into null, like the memory matcher
.toEqual(['c1', 'c2', 'c3']); // absent folds into null, as driver-memory's query path reads `$null`
});

it('multiple keys on one condition AND together, like `where`', () => {
Expand Down
12 changes: 7 additions & 5 deletions packages/objectql/src/having-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
// ordinary FilterCondition over those columns: implicit equality, the
// comparison / set / null / existence / string operators, and `$and` / `$or` /
// `$not` composition. Operator semantics follow the Filter Protocol, with TWO
// deliberate divergences from driver-memory's matcher — the face this module
// was originally written against:
// deliberate divergences from driver-memory's reference matcher — the face this
// module was originally written against, which commit `8fec76a2b` retired:
//
// 1. AN UNKNOWN OPERATOR THROWS. The memory matcher ignores operators it does
// not know; here an ignored operator would silently return UNFILTERED
// 1. AN UNKNOWN OPERATOR THROWS. The memory matcher ignored operators it did
// not know when this module was written; here an ignored operator would silently return UNFILTERED
// aggregates — the precise failure mode (#4286, ADR-0078) this module exists
// to end. The rejection names the operator and the supported set.
//
Expand Down Expand Up @@ -2061,7 +2061,9 @@ function checkCondition(
// at query time (SQLSTATE 42883), and `driver-memory`'s reference
// matcher failed both polarities. The maintainer ruled the cell on
// 2026-09-05 (option A, type-gate) and `FILTER_TEXT_CASES`' `score` rows
// pin it on every face: the reference matcher answers the predicate, and
// pin it on every face: the record-at-a-time faces (`formula`, this
// walker) answer the predicate, as `driver-memory`'s reference matcher did
// until commit `8fec76a2b` retired it, and
// the SQL compilers emit a type-gated constant for a column whose
// declared type is in `NON_TEXT_STORED_VALUE_TYPES`. This arm was already
// on the ruled side; nothing here moved.
Expand Down
5 changes: 3 additions & 2 deletions packages/objectql/src/number-comparand-declared-type-door.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@
* | `having` on `sum(amount)`: `$gt "abc"` | 200, no group | 200, no group | 200, no group |
*
* One client mistake, three answers, one of them a server fault; and a numeric
* string read two ways (the memory matcher compares `12 > "12"` without
* coercing it, the SQL backends bind it with numeric affinity or input).
* string read two ways (`InMemoryDriver`'s query path hands the comparison to
* mingo, which compares `12 > "12"` without coercing it; the SQL backends bind
* it with numeric affinity or input).
*
* ## The door's two answers
*
Expand Down
9 changes: 5 additions & 4 deletions packages/objectql/src/validation/record-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,10 +529,11 @@ function isMultiValueField(def: FieldDef): boolean {
*
* ## Why derived and not `key.startsWith('$')`
*
* The repo already carries five hand-rolled `keys.some(k => k.startsWith('$'))`
* shape tests (`having-filter.ts`, `driver-memory`'s matcher and
* `filter-refusal.ts`, `driver-mongodb`'s `mongodb-filter.ts`, `driver-turso`'s
* `remote-transport.ts`). None of them is exported, and none is reachable from
* The repo already carries hand-rolled `keys.some(k => k.startsWith('$'))`
* shape tests: five when this was written (`having-filter.ts`, `driver-memory`'s
* matcher and `filter-refusal.ts`, `driver-mongodb`'s `mongodb-filter.ts`,
* `driver-turso`'s `remote-transport.ts`), four since commit `8fec76a2b` retired
* the matcher. None of them is exported, and none is reachable from
* this package without inverting the layering — `@objectstack/objectql` depends
* on no driver. Writing a sixth `startsWith('$')` here is the accident #5659
* names: one question, N private answers, and the day one of them changes only
Expand Down
Loading
Loading