Skip to content

Commit 8fec76a

Browse files
refactor(driver-memory): retire the reference matcher and the cube face's own whole-day bound (#5930 step 4, group 1: F4 + F5; F3 stopped) (#20895)
Part of #20822 Clause-②: no #5930 step 4, group 1 of 3 (`driver-memory`), under ruling 5902355785 (D4 (b), D6) and ADR-0053 D-D1 as amended. **Two of the three faces land here: F5 and F4. F3 is stopped and reported.** Its deletion moves answers on a production direct caller and on engine-seamed reads, measured below. The card says "If routing it changes an answer, that face stops and is reported". This PR does not touch `memory-driver.ts`'s four whole-day sites. ## What lands | Face | Change | Typed column reader | |:--|:--|:--| | **F5** `MemoryAnalyticsService` (cube face) | `lteUpperBound` (the filter copy, 2 of the design's 5 call sites) is deleted. The `lte` row on both exits (mingo `$match` and SQL echo) now compiles the comparison it is handed. The `storageForm` hand-over that existed only for that derived bound is deleted too. | **None, by design.** The step-3 door (`normalizeFilters`) calls `lowerFilterCondition` with no `isDatetimeColumn`, so rules 1 and 2 apply type-blind on every column. That is the reading the deleted copy gave, so no bound is lost. | | F5, `dateRange` window | **Kept** (the other 3 call sites). It is not a `where`. The lowering never sees it (ADR-0053 D-D1 item 8, review 5903065983 item 8), and removing it would drop the explicit-end whole day. | n/a (a window, not a filter) | | **F4** reference matcher | `memory-matcher.ts` is deleted (ruling D6). `match()` had no production caller and was never exported. The one production import from the module, `getValueByPath` in `memory-driver.ts`, moves into `memory-driver.ts` byte for byte. The 21 importing test files keep every assertion; the table below lists where each one lives now. | No `datetime` column to bound. The matcher's `$lte` compared raw and applied no whole-day rule. | The changeset is `.changeset/20822-driver-memory-face-copies.md`, `@objectstack/driver-memory` **patch**. **Measured: retiring `match` removes no export.** The built `dist/index.d.ts` export list has the same 33 names before and after. `function match(` was never in `dist`: `index.ts` does not reach it, so the bundler dropped it. The positive control is `function getValueByPath`, which is present in both builds. ## Why F3 stops — measured before and after its deletion The four sites were measured with the deletion applied (commit `6df6dcfab1`, never pushed; patch kept), against built `dist`, through `examples/embed-objectql`'s dependency graph. Rows: `at` = `2026-07-27T10Z` / `07-28T00Z` / `07-28T10Z` / `07-29T10Z` / null; `note` (text) = `2026-07-27` / `2026-07-28` / `2026-07-28 late` / `zzz` / null. **Engine (the seamed path), `engine.find`; `engine.count` agrees:** | Column the filter names | `$lte '2026-07-28'` before → after | `$lte '9999-12-31'` before → after | |:--|:--|:--| | declared `datetime` | r1,r2,r3 → r1,r2,r3 (unchanged) | r1..r4 → r1..r4 (unchanged) | | declared `date` | r1,r2,r3 → r1,r2,r3 (unchanged) | unchanged | | declared `text` holding ISO text | r1,r2,r3 → **r1,r2** | r1..r4 → **r1,r2,r3** | | column not declared on a registered object | r1,r2,r3 → **r1** | — | | object not in the registry | r1,r2,r3 → **r1** | — | The seam's reader is `declaredDatetimeLowering` in `engine.ts`. It lowers only a declared `datetime`. An object with no field map reads no column as `datetime`, and its comment says the rule is then "left to the faces, as it was". ADR-0053 D-D1 item 7 says a seam that cannot read the declared type "applies the rewrite type-blind". Deleting F3 exposes that gap. **Direct callers (no seam):** - **`@objectstack/metadata` `DatabaseLoader.queryHistory({ until })`.** It builds `recorded_at: { $lte: until }` and calls the driver directly whenever the loader holds a driver and no engine (`setDatabaseDriver`). Probe: two history rows recorded today, then `until` set to today's bare day. Before: 2 rows. After: **0 rows**. `since` and `until` both set to today: 2 → **0**. - The face's own suites call the driver directly. With the four sites deleted, **43 tests go red**: `memory-temporal-conformance` 25, `memory-driver-calendar-day-upper-bound` 5, `memory-analytics-20661-lte-whole-day-first` 5, `memory-datetime-storage` 4, `memory-temporal-storage-form` 2 and `memory-analytics-shared-lowering-door` 2. So H2 ("dead on the seamed path, suites green") is falsified as stated. - In-repo production direct callers grepped for an upper-bound filter: `packages/cli` (`secret-reference-union`, `migrate/duplicates`, `secret/orphans`, `storage-driver`), `metadata-protocol` migrations, `runtime`, `plugin-dev` and `examples/embed-objectql` have **0**. `plugin-auth`'s adapter has `$lte` but goes through the engine. `metadata`'s `queryHistory` is the one hit, measured above. Options and a recommendation are in the report on #20822 (`open_questions`). ## F4 — the 21 importing test files, and where each assertion lives | File | Disposition | The matcher's assertions now live in | |:--|:--|:--| | `memory-matcher-or-semantics.test.ts` (36) | deleted | `memory-driver-filter-logic-conformance.test.ts`, the `InMemoryDriver.find` describe: one `it` per `FILTER_LOGIC_CASES` case, same names, same `expected`, plus a whole-table sweep | | `memory-matcher-not-null-safe.test.ts` (17) | deleted | `memory-driver-document-not.test.ts`. 15 of the cells were already asserted there, live, same fixture, both readings. The `$not $nin` (both readings), `$not $exists` and `$exists` = `$null: false` cells were added, and `$not $notContains` now runs on both readings | | `memory-driver-filter-logic-conformance.test.ts` | edited | the live-vs-matcher sweep became live vs the table's `expected`, in one assertion | | `memory-20041-like-nul-pattern.test.ts` | edited | the matcher's refusals → `assertFilterConditionShape` called directly (the gate `match()` ran); the controls → live vs `formula`'s rows | | `memory-20143-like-code-point.test.ts` | edited | the matcher door → spec `matchesLikePattern` (what `match()` evaluated) | | `memory-20444-empty-operator.test.ts` | edited | the leaf by-value cells → spec `isEmptyFilterValue`. The combinator by-value half → `formula`'s `matches-filter-empty-operator.test.ts` (same rows, same answers). Non-boolean refusal → the gate, plus a live `$or` identity case | | `memory-driver-document-not.test.ts` | edited | the live-vs-reference columns → literal expectations on the live path | | `memory-empty-field-constraint.test.ts` | edited | refusals → the gate; record-independence → the live path over one-row and empty tables; row answers and the nested-object comparison → the live path | | `memory-exists-has-value-faces.test.ts` | edited | the matcher exit → its literal answers (the ruling's), held on the live path; the composed cells' oracle → the literals measured on the matcher at `9905e61ca2`; the analytics face is checked against both | | `memory-filter-text-conformance.test.ts` | edited | the 19 per-case matcher `it`s → the query path's per-case `it`s (same names, same `expected`) plus a one-sweep assertion; refusals → the gate; the #14079 cell → the live path | | `memory-filter-vocabulary-refusal.test.ts` | edited | refusals → the gate (same message as the live path); record-independence → the live path over one-row and empty tables | | `memory-icontains.test.ts` | edited | → spec `asciiCaseInsensitiveContains`; the `$contains` case-exact pin → the live path; refusal → the gate | | `memory-like-pattern.test.ts` | edited | the agreement check → spec `matchesLikePattern` for `$like` / `$ilike`; the `$contains` control → the live path | | `memory-matcher-array-and-date-comparand.test.ts` | edited, name kept | every cell → the live path. The Date cells use a declared `datetime` column (the case they were written for); the array refusals go through the live path's gate | | `memory-matcher-no-value-negated-operators.test.ts` | edited, name kept (`spec`'s `filter-logic-conformance.ts` points at it) | every cell → the live path, both readings | | `memory-matcher-null-value-and-comparand.test.ts` | edited, name kept | the matcher half of each cell dropped (the live half was already literal); the matcher-only comparisons → the live path | | `memory-matcher-scalar-comparand-array-value.test.ts` | edited, name kept | 7 per-case matcher `it`s → the live per-case `it`s plus a whole-matrix sweep; the card's three rows, OR-over-elements and `$ne`'s per-row complement → the live path, a row at a time | | `memory-null-comparand-refusal.test.ts` | edited | refusals → the gate; answers → the live path. **`$exists: 'yes'` is the one divergence found**: the matcher answered `['1']` (it read the flag by truthiness), the live path answers `['2']` (`val === true` → `$ne: null`, anything else → `$eq: null`). The live answer is pinned and the divergence reported | | `memory-null-list-member-unreachable.test.ts` | edited | the compile-then-`match` pipeline → compile-then-`find` | | `memory-null-ordering-comparand-unreachable.test.ts` | edited | the same | | `memory-operator-key-clobber.test.ts` | edited | the matcher lines dropped; each cell's literal was already asserted on the live path | Counts: the 21 files go from 560 to 483 `it`s, and the package from 1490 to 1413. The 77 are 62 duplicate per-case `it`s (36 + 19 + 7), each still asserted by case name on the live path with the same `expected`, plus the 17 not-null-safe `it`s folded into `document-not` (+2 there). ## Hypotheses (dispatch zone 2) - **H1** held for F5 (reached only through step 3's door) and was falsified for F3 (see above). - **H2** was falsified. F3's sites are dead only for declared `datetime` columns and are equivalent on `date`. They are alive for text, undeclared and unregistered columns on the engine path, for every direct caller, and for 43 of the face's own tests. - **H3** held. Each F5 filter site was ablated on its own at `9905e61ca2`: 1490 of 1490 green, twice. The window arm stays. - **H4** held, with two corrections: 21 importing test files, not 20; and the module had one production import (`getValueByPath`), which moved. Every mention outside `driver-memory` is a comment, not an import. - **H5**: stale comments are listed under Acceptance notes. Nothing outside `driver-memory` is edited. ## Tests and gates (at `94616a955f`, after merging `origin/main` `4edb61449b`) - `pnpm --filter @objectstack/driver-memory exec vitest run --maxWorkers=2`: **1413 passed / 0 failed**, 65 files. - `pnpm --filter @objectstack/driver-memory typecheck` (`tsc --noEmit && tsc --noEmit -p tsconfig.typecheck.json`): exit 0. `--listFiles` shows all 65 test files in the program. - `node scripts/pm/dispatch-gates.mjs --commands` derived 60 families, all 60 run. `--ran` with exit codes: "60 derived, 58 run, 2 NOT-MEASURED, 0 UNRUN". That includes `check:driver-conformance` (OK, 50 covered cells, 0 DEBT), `check:driver-memory-census` (OK), `check:engine-double-contract`, `check:nul-bytes`, `check:published-files`, `check:adr-0087-registration`, `check:changeset-no-major` and `check:empty-changeset`, all exit 0. - **NOT MEASURED:** `check:dual-build-cjs-loads` (exit 3, PREREQUISITE NOT MET: it reads every workspace package's `dist`). Narrowed probe: `driver-memory`'s CJS entry loads, with 23 runtime exports. **NOT MEASURED:** `check:type-check-debt` (exit 3, PREREQUISITE NOT MET: 16 unbuilt dependencies of ledgered packages). `driver-memory` has no ledger row, and its own typecheck is green. - Lint, narrowed: `eslint --no-inline-config --format json` over the 22 changed `driver-memory` files: 22 files, 0 errors, 0 warnings. All 22 are inside the config (`--print-config` reads each). The config enables no type-aware linting (`eslint.config.mjs` states it has no `parserOptions.project`), so this diff cannot move a verdict on an untouched file. The full `pnpm lint` is CI's. - Consumers: the public surface is byte-identical (33 exports), so no downstream suites are owed. ## Ablations Every leg below went through `scripts/ablation-replace.mjs` (anchor hit on disk, restore proven by blob == HEAD). - F5 mongo `lte` row replaced by a plain `$lte` at `9905e61ca2`: 1490/1490 green. The same for the F5 SQL echo `lte` row: 1490/1490 green. - Control, after the deletion: the door's `lowerFilterCondition(admitted)` removed → **14 red**, including the 20661 whole-day cells, the echo's half-open `$lte`, `9999-12-31` and the shared-lowering door. The bound now lives in the door. - F4, the moved row assertions bind the live path: the live `$exists` arm reverted to mingo key presence → **19 red** across document-not, exists-has-value, null-value-and-comparand, null-comparand-refusal, operator-key-clobber and filter-logic. - F4, the moved refusals bind the gate: the gate's `$null` non-boolean check disabled → **9 red** (the gate-direct cells in null-comparand-refusal). - F3 metadata-history leg: patch applied, `driver-memory` rebuilt, then restored and rebuilt. The `dist` preflight shows the marker back and the tree clean, and the probe reads 2 again. ## Acceptance notes - **Stale comments outside `driver-memory` that name the retired matcher** (not edited: the spec lane's or another package's; `.claude/**` is governed): - `packages/spec/src/data/filter-logic-conformance.ts:15`: the backend table's "In-memory matcher / `memory-matcher`" row. It ships in `dist/data/index.d.ts`. It should read `InMemoryDriver.find` (mingo). - `packages/formula/src/matches-filter-not-null-safe.test.ts:17`, `service-analytics` `filter-normalizer-not-null-safe.test.ts:50` and `read-scope-not-null-safe.test.ts:43` point at the deleted `memory-matcher-not-null-safe.test.ts`. The cells now live in `memory-driver-document-not.test.ts`. - `service-storage` `attachment-read-visibility.test.ts:13` and `:326` cite `memory-matcher.ts` and `memory-matcher-or-semantics.test.ts`. - `plugin-security` `claim-seed-ownership.ts:91` attributes `InMemoryDriver`'s `id IN (…)` scan to `memory-matcher.ts`. It was always mingo's, and this was already wrong before this PR. - `service-analytics` `objectql-strategy.ts:1929` and `objectql-contains-canonical-operator.test.ts` describe the matcher's `$regex` arm in the present tense (that arm was retired by #5702). - `.claude/skills/pm-dispatch/references/compile-surfaces.md:16` names `memory-matcher.ts:134` as a live surface. - `docs/design/predicate-compilation-convergence.md` F4 row. - `spec` `filter-logic-conformance.ts:492` still points at a live file, which kept its name for it. - Runtime refusal texts in `filter-refusal.ts` (`malformedBetweenError`, `nonBooleanNullComparandError`, `arrayComparandError`) mention the reference matcher in the past tense. They stay true and are untouched. - Four migrated files keep their `memory-matcher-` names so that no pointer dangles. Each header says so. --- _Generated by [Claude Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent da93a8b commit 8fec76a

26 files changed

Lines changed: 865 additions & 1642 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/driver-memory': patch
3+
---
4+
5+
refactor(driver-memory): the cube face's own whole-day bound and the in-memory reference matcher are deleted; no answer a caller gets moves (#5930 step 4, #20822)
6+
7+
Clause-②: no
8+
9+
- **`MemoryAnalyticsService` (the cube face).** Its `where` door has run the shared `lowerFilterCondition` (`@objectstack/spec/data`) since #5930 step 3, on every column. A bare-day `$lte` therefore reaches the `lte` row already lowered: as `$lt` the next day, or as `$null: false` on `9999-12-31`. The row's own copy of that rule is deleted, and the `lte` row now compiles the comparison it is handed on both exits. The rows `query()` returns and the SQL `generateSql()` echoes are unchanged. An explicit `dateRange` end still widens a bare day through its own window arm (ADR-0053 D-D1 item 8).
10+
- **The reference matcher (`memory-matcher.ts`, `match()`) is retired** (ruling D6 on #5930). No production code called it and the package never exported it: the published `dist` exports are the same 33 names before and after. `InMemoryDriver` keeps `getValueByPath`, the one helper it imported from that module. The matcher's tests now assert the live query path (`InMemoryDriver.find`), the shared filter shape gate, or the spec predicate the matcher evaluated.

‎packages/drivers/driver-memory/src/filter-refusal.ts‎

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
* The filter refusals this driver raises, in ONE place — and, since #5324/#5328,
55
* the ONE walk that decides which shapes are refused at all.
66
*
7-
* All THREE of this package's filter surfaces refuse the same shapes with the
8-
* same wire envelope: the live query path (`memory-driver.ts` → mingo), the
9-
* reference matcher (`memory-matcher.ts`, the record-at-a-time evaluator the
10-
* conformance suites hold against `driver-sql` and `@objectstack/formula`), and
11-
* since #5345 the analytics/cube face (`memory-analytics.ts`). They were
12-
* independent code paths with independent notions of what a filter may be, which
13-
* is exactly how #5240's divergence survived unnoticed in-package.
7+
* All of this package's filter surfaces refuse the same shapes with the same
8+
* wire envelope: the live query path (`memory-driver.ts` → mingo) and since
9+
* #5345 the analytics/cube face (`memory-analytics.ts`) — and, until #5930
10+
* step 4 retired it (ruling D6: no production caller), the reference matcher
11+
* (`memory-matcher.ts`, a record-at-a-time evaluator whose tests now hold the
12+
* live path and this gate). They were independent code paths with independent
13+
* notions of what a filter may be, which is exactly how #5240's divergence
14+
* survived unnoticed in-package.
1415
*
1516
* #5240 gave the first two faces one refusal by writing the same check twice.
1617
* That was still two implementations of one rule, and the shapes #5324/#5328
@@ -319,8 +320,8 @@ export function emptyFieldConstraintError(field: string, path: string): Error {
319320
* RLS read scope is a permission bypass rather than a degraded filter (#3948).
320321
*
321322
* So the arms and the word list HAD to land in one PR, and #6520 did that:
322-
* `memory-matcher.ts` and `memory-driver.ts` both carry a `$icontains` case, and
323-
* `memory-analytics.ts` lowers it too. Re-verified by deleting the matcher's arm
323+
* `memory-matcher.ts` (retired since) and `memory-driver.ts` both carried a
324+
* `$icontains` case, and `memory-analytics.ts` lowers it too. Re-verified by deleting the matcher's arm
324325
* on the #6520 branch — with the name admitted, the reference matcher answered
325326
* EVERY row, which is the measurement, not a prediction.
326327
*
@@ -350,22 +351,22 @@ export function emptyFieldConstraintError(field: string, path: string): Error {
350351
* PARSES must survive to a matched row here.
351352
*
352353
* The ordering rule from the `$icontains` paragraph applies unchanged and was
353-
* followed: both arms (`memory-driver.ts`'s query path and `memory-matcher.ts`)
354-
* landed in the same commit as this widening. A name added here with no arm
354+
* followed: both arms (`memory-driver.ts`'s query path and the since-retired
355+
* `memory-matcher.ts`) landed in the same commit as this widening. A name added here with no arm
355356
* behind it is the #5701 measurement — gate stops refusing, matcher has no
356357
* case, predicate silently DROPPED, every row matches.
357358
*
358359
* Everything else is refused. That includes the mingo operators this driver used
359360
* to hand through by accident (`$elemMatch`, `$size`, `$type`, `$mod`, `$where`,
360-
* `$expr`, field-level `$not`) — none of them is in the Filter Protocol, none is
361-
* implemented by the matcher, and `driver-sql` refuses every one.
361+
* `$expr`, field-level `$not`) — none of them is in the Filter Protocol, none was
362+
* implemented by the retired matcher, and `driver-sql` refuses every one.
362363
*/
363364
export const SUPPORTED_FIELD_OPERATORS: ReadonlySet<string> = new Set<string>([
364365
...FILTER_OPERATORS,
365366
'$like',
366367
'$ilike',
367368
// [#20444] `$empty` was admitted here BY HAND, with both its arms (the
368-
// reference matcher by value through `isEmptyFilterValue`, the live query
369+
// since-retired reference matcher by value through `isEmptyFilterValue`, the live query
369370
// path by the field's DECLARED row through `expandEmptyOperator`), while it
370371
// was staged out of `FILTER_OPERATORS`. [#20446] It arrives by DERIVATION
371372
// now, after `$exists` in the spec's order, so the hand entry is gone — the
@@ -409,10 +410,10 @@ export interface FilterFaceCapabilities {
409410
}
410411

411412
/**
412-
* [#5345] The default: the whole vocabulary this driver's query path and
413-
* reference matcher evaluate. Passing no capabilities means "this face compiles
414-
* everything the driver does", which is true of both of them and keeps every
415-
* pre-#5345 call site behaving byte-for-byte as before.
413+
* [#5345] The default: the whole vocabulary this driver's query path evaluates
414+
* (as its reference matcher did, until retired). Passing no capabilities means
415+
* "this face compiles everything the driver does", which is true of the query
416+
* path and keeps every pre-#5345 call site behaving byte-for-byte as before.
416417
*/
417418
export const DRIVER_FILTER_CAPABILITIES: FilterFaceCapabilities = Object.freeze({
418419
face: 'this driver',
@@ -638,9 +639,10 @@ export function nonBooleanEmptyComparandError(field: string, value: unknown, pat
638639
*
639640
* What counts as empty is the field's DECLARED row of the ruled table, and the
640641
* live path reads it from the declaration rather than from a value, so without
641-
* one there is no answer to give: refused, never guessed. The reference matcher
642-
* (`memory-matcher.ts`) is the face that holds NO declarations at all, and it
643-
* judges the stored value instead — the spec's reading for such a face.
642+
* one there is no answer to give: refused, never guessed. A face that holds NO
643+
* declarations at all judges the stored value instead — the spec's reading for
644+
* such a face (`@objectstack/formula`'s, and this package's reference matcher's
645+
* until it was retired).
644646
*/
645647
export function undeclaredEmptyOperatorFieldError(field: string, path: string): Error {
646648
return unsupportedFilterError(
@@ -804,7 +806,7 @@ export function filterNodeExpectedError(value: unknown, path: string): Error {
804806
}
805807

806808
/**
807-
* [#5324 / #5328] The ONE shape gate, walked before either face evaluates.
809+
* [#5324 / #5328] The ONE shape gate, walked before any face evaluates.
808810
*
809811
* ## Why up front, and why exhaustive
810812
*
@@ -845,8 +847,8 @@ export function filterNodeExpectedError(value: unknown, path: string): Error {
845847
* Shape is universal; CAPABILITY is per-face. `capabilities` narrows what this
846848
* particular caller can lower — see {@link FilterFaceCapabilities} — and the
847849
* walk refuses the difference. It defaults to
848-
* {@link DRIVER_FILTER_CAPABILITIES}, i.e. everything, so the query path and the
849-
* matcher are unaffected.
850+
* {@link DRIVER_FILTER_CAPABILITIES}, i.e. everything, so the query path (and
851+
* the matcher, while it lived) is unaffected.
850852
*
851853
* The capability check is made BEFORE the shape checks at the same key, and
852854
* deliberately: on a face that cannot compile `$or` at all, reporting that its

‎packages/drivers/driver-memory/src/memory-20041-like-nul-pattern.test.ts‎

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@
2020
* - the `$`-spelling on the live query path (`assertFieldConstraintShape`);
2121
* - the QueryAST `comparison` spelling (`convertConditionToMongo`'s `like` /
2222
* `ilike` arm), which has its own dangling-escape refusal;
23-
* - the reference matcher `match()`, through the same shape gate.
23+
* - the shape gate itself (`assertFilterConditionShape`, `filter-refusal.ts`),
24+
* called directly. It was the reference matcher's door; [#5930 step 4, ruling
25+
* D6] that matcher is retired, and its refusal assertions are held on the
26+
* gate it ran, which the live query path runs too.
2427
*/
2528

2629
import { describe, it, expect, beforeEach } from 'vitest';
2730
import { InMemoryDriver } from './memory-driver.js';
28-
import { match } from './memory-matcher.js';
31+
import { assertFilterConditionShape } from './filter-refusal.js';
2932

3033
interface WireBearingError extends Error {
3134
code?: string;
@@ -81,13 +84,14 @@ describe('[#20041] driver-memory — a $like / $ilike pattern holding U+0000 is
8184
return err;
8285
};
8386

84-
const matcherRefusalOf = (where: unknown): WireBearingError => {
87+
/** The shared shape gate, called directly — the door the retired reference matcher ran. */
88+
const gateRefusalOf = (where: unknown): WireBearingError => {
8589
try {
86-
match({ v: 'ab' }, where);
90+
assertFilterConditionShape(where, 'filter');
8791
} catch (e) {
8892
return e as WireBearingError;
8993
}
90-
throw new Error(`expected the matcher to refuse ${JSON.stringify(where)}, but it answered`);
94+
throw new Error(`expected the shape gate to refuse ${JSON.stringify(where)}, but it passed`);
9195
};
9296

9397
const expectEnvelope = (err: WireBearingError, located: string) => {
@@ -120,8 +124,8 @@ describe('[#20041] driver-memory — a $like / $ilike pattern holding U+0000 is
120124
);
121125
});
122126

123-
it(`${op} ${shown}: the reference matcher refuses it rather than answering`, () => {
124-
expectEnvelope(matcherRefusalOf({ v: { [op]: pattern } }), `Operator "${op}" on field "v" at filter.v.${op}`);
127+
it(`${op} ${shown}: the shape gate refuses it rather than passing it on (the retired matcher's door)`, () => {
128+
expectEnvelope(gateRefusalOf({ v: { [op]: pattern } }), `Operator "${op}" on field "v" at filter.v.${op}`);
125129
});
126130
}
127131

@@ -130,7 +134,7 @@ describe('[#20041] driver-memory — a $like / $ilike pattern holding U+0000 is
130134
for (const err of [
131135
await refusalOf({ v: { $like: pattern } }),
132136
await refusalOf({ type: 'comparison', field: 'v', operator: 'like', value: pattern }),
133-
matcherRefusalOf({ v: { $like: pattern } }),
137+
gateRefusalOf({ v: { $like: pattern } }),
134138
]) {
135139
expect(err.code).toBe('INVALID_FILTER');
136140
expect(err.status).toBe(400);
@@ -141,12 +145,14 @@ describe('[#20041] driver-memory — a $like / $ilike pattern holding U+0000 is
141145
});
142146

143147
for (const c of CONTROLS) {
144-
it(`control: ${JSON.stringify(c.where)} (no U+0000) answers the same rows on the query path and the matcher`, async () => {
148+
// [#5930 step 4] The retired matcher's half of this control asserted the
149+
// same `expected` rows — `@objectstack/formula`'s answer, the oracle both
150+
// halves were held to — so the control keeps that oracle on the live path.
151+
it(`control: ${JSON.stringify(c.where)} (no U+0000) answers formula's rows on the query path`, async () => {
145152
const queried = (await driver.find(TABLE, { where: c.where as never }))
146153
.map((r: Record<string, unknown>) => String(r.label))
147154
.sort();
148-
const matched = ROWS.filter((r) => match(r, c.where)).map((r) => r.label).sort();
149-
expect({ queried, matched }).toEqual({ queried: [...c.expected], matched: [...c.expected] });
155+
expect(queried).toEqual([...c.expected]);
150156
});
151157
}
152158
});

‎packages/drivers/driver-memory/src/memory-20143-like-code-point.test.ts‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@
1818
*
1919
* Each door is asserted against the literal code-point rows, not merely
2020
* against the others: parity alone is satisfied by breaking all three alike.
21+
*
22+
* [#5930 step 4, ruling D6] The reference matcher is retired. Its door held
23+
* nothing of its own for this operator — it evaluated the spec's
24+
* `matchesLikePattern` — so its third of each assertion is held on that shared
25+
* predicate directly, the one `@objectstack/formula` evaluates too.
2126
*/
2227

2328
import { describe, it, expect, beforeAll } from 'vitest';
2429
import { InMemoryDriver } from './memory-driver.js';
25-
import { match } from './memory-matcher.js';
30+
import { matchesLikePattern } from '@objectstack/spec/data';
2631

2732
const GRIN = String.fromCodePoint(0x1f600);
2833
const SCRIPT_A = String.fromCodePoint(0x1d49c);
@@ -71,14 +76,17 @@ describe('[#20143] driver-memory — `_` is one code point on every door', () =>
7176
});
7277

7378
for (const [op, pattern, expected] of FAMILY) {
74-
it(`${op} ${JSON.stringify(pattern)} answers the code-point rows on all three doors`, async () => {
79+
it(`${op} ${JSON.stringify(pattern)} answers the code-point rows on every door`, async () => {
7580
const where = { v: { [op]: pattern } };
7681
const ast = { type: 'comparison', field: 'v', operator: op === '$ilike' ? 'ilike' : 'like', value: pattern };
7782
expect({
7883
dollarQuery: labels(await driver.find(TABLE, { where: where as never })),
7984
astQuery: labels(await driver.find(TABLE, { where: ast as never })),
80-
matcher: Object.entries(ROWS).filter(([label, v]) => match({ label, v }, where)).map(([l]) => l).sort(),
81-
}).toEqual({ dollarQuery: [...expected], astQuery: [...expected], matcher: [...expected] });
85+
sharedPredicate: Object.entries(ROWS)
86+
.filter(([, v]) => matchesLikePattern(v, pattern, op === '$ilike'))
87+
.map(([l]) => l)
88+
.sort(),
89+
}).toEqual({ dollarQuery: [...expected], astQuery: [...expected], sharedPredicate: [...expected] });
8290
});
8391
}
8492
});

0 commit comments

Comments
 (0)