Skip to content

Commit 9b43ee2

Browse files
os-zhuangclaude
andauthored
test(drivers): enroll driver-mongodb and driver-sqlite-wasm in the filter-logic standard (#4405) (#4596)
`FILTER_LOGIC_CASES` (#3774) called itself the standard "the four independent FilterCondition backends" are checked against. There are five: `driver-mongodb`'s `translateFilter` is an independent implementation whose $and/$or/$not translation shares no code with the SQL compiler or the in-memory matcher, and it was missed rather than excluded. `driver-sqlite-wasm` inherits SqlDriver's compiler but had never run the table through its own sql.js engine. - `mongodb-filter-logic-translation.test.ts` — server-free: drives every shared case through `translateFilter` and evaluates the emitted MongoDB document over the shared fixture with a deliberately strict in-process reader (unmodelled shapes throw; a document-level `$not` throws by name, because the server rejects it). Wire-shape pins cover #4405's named risk areas ($not → $nor, nested $and inside $or), and negative controls keep the reader discriminating. - `mongodb-filter-logic-conformance.test.ts` — the same table against a real mongod via `createTestMongod`, skipping cleanly when the binary is unreachable. - `sqlite-wasm-filter-logic-conformance.test.ts` — the same table through the wasm driver's own engine. No translation change: `translateFilter` answers all seventeen cases correctly today. Both DEBT entries are deleted from `scripts/check-driver-conformance.mjs` in the same commit, so the matrix is ok/ok for both cells and the ledger is empty. The spec's "four backends" prose now says five and names the fifth. Claude-Session: https://claude.ai/code/session_012C2cd7tL8QDoZ2QKN3djJ5 Co-authored-by: Claude <noreply@anthropic.com>
1 parent a52e2ef commit 9b43ee2

7 files changed

Lines changed: 519 additions & 27 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
"@objectstack/driver-mongodb": patch
3+
"@objectstack/driver-sqlite-wasm": patch
4+
---
5+
6+
test(drivers): the filter-logic standard now covers the backend it was counted without (#4405)
7+
8+
`FILTER_LOGIC_CASES` (#3774) opens by calling itself the standard "the four
9+
independent FilterCondition backends are each checked against". Five backends
10+
exist. `driver-mongodb`'s `translateFilter` was missed, not excluded — an
11+
independent implementation whose `$and`/`$or`/`$not` translation shares no line
12+
of code with the SQL compiler or the in-memory matcher, and the only one whose
13+
target language cannot spell the standard directly: MongoDB has no
14+
document-level `$not` at all (the server answers `unknown top level operator:
15+
$not`), so a negation has to leave as `$nor`, and a branch's own keys have to
16+
stay in one document while `$and`/`$or` clauses are lifted beside them. That
17+
route was never checked against the shared cases. Both DEBT rows the #4363 gate
18+
recorded are now cleared, and `scripts/check-driver-conformance.mjs` reports
19+
`ok` for every cell of the matrix.
20+
21+
**`driver-mongodb` runs the table twice, and the split is deliberate.**
22+
`mongodb-filter-logic-translation.test.ts` drives every shared case through
23+
`translateFilter` and evaluates the emitted MongoDB *document* over the shared
24+
fixture — a pure function, no server, so it always runs. That matters here more
25+
than anywhere: `mongodb-memory-server` downloads a ~123 MB binary from
26+
fastdl.mongodb.org, and a defect only a downloadable binary can catch is a
27+
defect nobody catches on a restricted network. Its in-process reader is strict
28+
by construction — every shape it does not model throws instead of evaluating to
29+
true, a document-level `$not` included — and its own discrimination is pinned by
30+
cases that require a widened document to FAIL the case it widens, so "all green"
31+
cannot mean "the reader says yes to everything".
32+
`mongodb-filter-logic-conformance.test.ts` runs the same table against a real
33+
mongod and answers the one question the first half cannot — does MongoDB agree?
34+
— skipping cleanly (never silently) when the binary is unreachable.
35+
36+
**`driver-sqlite-wasm` runs the table through its own engine.** It inherits
37+
`SqlDriver`'s filter compiler, so nothing is re-implemented; what the suite pins
38+
is that a nested `(… AND …) OR (… AND …)` survives the custom sql.js dialect
39+
that compiles, binds and marshals it — the same seam its temporal and pagination
40+
suites cover for their clauses. Tracked as DEBT rather than EXEMPT because
41+
"inherits, therefore fine" is the assumption those suites exist to disprove; the
42+
suite is what disproves it.
43+
44+
**No divergence was found.** `translateFilter` answers all seventeen shared
45+
cases correctly today, `$not`-inside-a-branch and nested `$and`-inside-`$or`
46+
included, so no translation change ships here — what changes is that the next
47+
edit to it cannot quietly widen a filter. Both suites were verified to be
48+
discriminating rather than decorative by reintroducing the #3774 miscompile
49+
(propagating `or` into a branch's own contents): 15 of the mongodb translation
50+
suite's 26 tests fail, and 13 of the wasm suite's 18.
51+
52+
`packages/spec`'s `filter-logic-conformance.ts` header now says five and names
53+
the fifth — a code comment; no schema, export or generated artifact moved.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Filter logical-combinator conformance for the MongoDB driver, against a REAL
5+
* mongod (#4405) — the half that answers whether MongoDB agrees.
6+
*
7+
* The shared cases come from `@objectstack/spec/data`, so this backend now
8+
* stands beside `driver-sql`, `driver-memory`, `formula`'s
9+
* `matchesFilterCondition` and `read-scope-sql` under one standard (#3774).
10+
* `mongodb-filter.ts` reaches that standard by a completely separate route:
11+
* MongoDB has no document-level `$not`, so a negation is emitted as `$nor`, and
12+
* a branch's own keys have to stay inside one document while `$and`/`$or`
13+
* clauses are lifted beside them. Whether that route arrives at the same rows
14+
* is not a question a translator test can close — it is a question about the
15+
* server's evaluation of the document, and this file is where it is asked.
16+
*
17+
* The same table is driven server-free by
18+
* `mongodb-filter-logic-translation.test.ts`, which is the half that always
19+
* runs. This one skips when the mongod binary cannot be fetched (the
20+
* `createTestMongod` convention every suite in this package uses — a blocked or
21+
* hanging download costs a skipped suite, not a stalled test job). **A skip is
22+
* not a pass**: on a machine without the binary, the translation suite is the
23+
* whole proof, which is exactly why it carries the priority half.
24+
*/
25+
26+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
27+
import type { MongoMemoryServer } from 'mongodb-memory-server';
28+
import { FILTER_LOGIC_CASES, FILTER_LOGIC_ROWS } from '@objectstack/spec/data';
29+
import { MongoDBDriver } from './mongodb-driver.js';
30+
import { createTestMongod } from './test-mongod.js';
31+
32+
const sharedMongod: MongoMemoryServer | undefined = await createTestMongod('filter logic conformance');
33+
34+
describe.skipIf(!sharedMongod)('driver-mongodb — filter logic conformance', () => {
35+
const mongod = sharedMongod as MongoMemoryServer;
36+
let driver: MongoDBDriver;
37+
38+
beforeAll(async () => {
39+
driver = new MongoDBDriver({ url: mongod.getUri(), database: 'filter_logic_conformance' });
40+
await driver.connect();
41+
// Every fixture column is a plain string — the shared table keeps its
42+
// predicates boring on purpose, so nothing here is about coercion. The
43+
// declaration is still made, because that is how a real object reaches the
44+
// driver and how its field kinds are resolved (#4047).
45+
await driver.syncSchema('conformance', {
46+
name: 'conformance',
47+
fields: {
48+
a: { type: 'string' },
49+
b: { type: 'string' },
50+
c: { type: 'string' },
51+
owner: { type: 'string' },
52+
status: { type: 'string' },
53+
parent_object: { type: 'string' },
54+
parent_id: { type: 'string' },
55+
},
56+
});
57+
for (const row of FILTER_LOGIC_ROWS) {
58+
await driver.create('conformance', { ...row });
59+
}
60+
}, 90_000);
61+
62+
afterAll(async () => {
63+
if (driver) await driver.disconnect();
64+
if (sharedMongod) await sharedMongod.stop();
65+
});
66+
67+
for (const c of FILTER_LOGIC_CASES) {
68+
it(c.name, async () => {
69+
const rows = await driver.find('conformance', { object: 'conformance', where: c.filter } as any);
70+
const got = (rows as any[])
71+
.map((r) => String(r.id))
72+
.sort((x, y) => x.localeCompare(y));
73+
expect(got, c.note).toEqual([...c.expected]);
74+
});
75+
}
76+
77+
/**
78+
* The fixture as a whole, so a case that returns nothing because the seed
79+
* failed cannot read as a case that correctly excluded everything.
80+
*/
81+
it('the fixture really is all four rows', async () => {
82+
const rows = await driver.find('conformance', { object: 'conformance' } as any);
83+
expect((rows as any[]).map((r) => String(r.id)).sort()).toEqual(['1', '2', '3', '4']);
84+
});
85+
});

0 commit comments

Comments
 (0)