Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .changeset/filter-logic-conformance-mongodb-wasm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
"@objectstack/driver-mongodb": patch
"@objectstack/driver-sqlite-wasm": patch
---

test(drivers): the filter-logic standard now covers the backend it was counted without (#4405)

`FILTER_LOGIC_CASES` (#3774) opens by calling itself the standard "the four
independent FilterCondition backends are each checked against". Five backends
exist. `driver-mongodb`'s `translateFilter` was missed, not excluded — an
independent implementation whose `$and`/`$or`/`$not` translation shares no line
of code with the SQL compiler or the in-memory matcher, and the only one whose
target language cannot spell the standard directly: MongoDB has no
document-level `$not` at all (the server answers `unknown top level operator:
$not`), so a negation has to leave as `$nor`, and a branch's own keys have to
stay in one document while `$and`/`$or` clauses are lifted beside them. That
route was never checked against the shared cases. Both DEBT rows the #4363 gate
recorded are now cleared, and `scripts/check-driver-conformance.mjs` reports
`ok` for every cell of the matrix.

**`driver-mongodb` runs the table twice, and the split is deliberate.**
`mongodb-filter-logic-translation.test.ts` drives every shared case through
`translateFilter` and evaluates the emitted MongoDB *document* over the shared
fixture — a pure function, no server, so it always runs. That matters here more
than anywhere: `mongodb-memory-server` downloads a ~123 MB binary from
fastdl.mongodb.org, and a defect only a downloadable binary can catch is a
defect nobody catches on a restricted network. Its in-process reader is strict
by construction — every shape it does not model throws instead of evaluating to
true, a document-level `$not` included — and its own discrimination is pinned by
cases that require a widened document to FAIL the case it widens, so "all green"
cannot mean "the reader says yes to everything".
`mongodb-filter-logic-conformance.test.ts` runs the same table against a real
mongod and answers the one question the first half cannot — does MongoDB agree?
— skipping cleanly (never silently) when the binary is unreachable.

**`driver-sqlite-wasm` runs the table through its own engine.** It inherits
`SqlDriver`'s filter compiler, so nothing is re-implemented; what the suite pins
is that a nested `(… AND …) OR (… AND …)` survives the custom sql.js dialect
that compiles, binds and marshals it — the same seam its temporal and pagination
suites cover for their clauses. Tracked as DEBT rather than EXEMPT because
"inherits, therefore fine" is the assumption those suites exist to disprove; the
suite is what disproves it.

**No divergence was found.** `translateFilter` answers all seventeen shared
cases correctly today, `$not`-inside-a-branch and nested `$and`-inside-`$or`
included, so no translation change ships here — what changes is that the next
edit to it cannot quietly widen a filter. Both suites were verified to be
discriminating rather than decorative by reintroducing the #3774 miscompile
(propagating `or` into a branch's own contents): 15 of the mongodb translation
suite's 26 tests fail, and 13 of the wasm suite's 18.

`packages/spec`'s `filter-logic-conformance.ts` header now says five and names
the fifth — a code comment; no schema, export or generated artifact moved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Filter logical-combinator conformance for the MongoDB driver, against a REAL
* mongod (#4405) — the half that answers whether MongoDB agrees.
*
* The shared cases come from `@objectstack/spec/data`, so this backend now
* stands beside `driver-sql`, `driver-memory`, `formula`'s
* `matchesFilterCondition` and `read-scope-sql` under one standard (#3774).
* `mongodb-filter.ts` reaches that standard by a completely separate route:
* MongoDB has no document-level `$not`, so a negation is emitted as `$nor`, and
* a branch's own keys have to stay inside one document while `$and`/`$or`
* clauses are lifted beside them. Whether that route arrives at the same rows
* is not a question a translator test can close — it is a question about the
* server's evaluation of the document, and this file is where it is asked.
*
* The same table is driven server-free by
* `mongodb-filter-logic-translation.test.ts`, which is the half that always
* runs. This one skips when the mongod binary cannot be fetched (the
* `createTestMongod` convention every suite in this package uses — a blocked or
* hanging download costs a skipped suite, not a stalled test job). **A skip is
* not a pass**: on a machine without the binary, the translation suite is the
* whole proof, which is exactly why it carries the priority half.
*/

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import type { MongoMemoryServer } from 'mongodb-memory-server';
import { FILTER_LOGIC_CASES, FILTER_LOGIC_ROWS } from '@objectstack/spec/data';
import { MongoDBDriver } from './mongodb-driver.js';
import { createTestMongod } from './test-mongod.js';

const sharedMongod: MongoMemoryServer | undefined = await createTestMongod('filter logic conformance');

describe.skipIf(!sharedMongod)('driver-mongodb — filter logic conformance', () => {
const mongod = sharedMongod as MongoMemoryServer;
let driver: MongoDBDriver;

beforeAll(async () => {
driver = new MongoDBDriver({ url: mongod.getUri(), database: 'filter_logic_conformance' });
await driver.connect();
// Every fixture column is a plain string — the shared table keeps its
// predicates boring on purpose, so nothing here is about coercion. The
// declaration is still made, because that is how a real object reaches the
// driver and how its field kinds are resolved (#4047).
await driver.syncSchema('conformance', {
name: 'conformance',
fields: {
a: { type: 'string' },
b: { type: 'string' },
c: { type: 'string' },
owner: { type: 'string' },
status: { type: 'string' },
parent_object: { type: 'string' },
parent_id: { type: 'string' },
},
});
for (const row of FILTER_LOGIC_ROWS) {
await driver.create('conformance', { ...row });
}
}, 90_000);

afterAll(async () => {
if (driver) await driver.disconnect();
if (sharedMongod) await sharedMongod.stop();
});

for (const c of FILTER_LOGIC_CASES) {
it(c.name, async () => {
const rows = await driver.find('conformance', { object: 'conformance', where: c.filter } as any);
const got = (rows as any[])
.map((r) => String(r.id))
.sort((x, y) => x.localeCompare(y));
expect(got, c.note).toEqual([...c.expected]);
});
}

/**
* The fixture as a whole, so a case that returns nothing because the seed
* failed cannot read as a case that correctly excluded everything.
*/
it('the fixture really is all four rows', async () => {
const rows = await driver.find('conformance', { object: 'conformance' } as any);
expect((rows as any[]).map((r) => String(r.id)).sort()).toEqual(['1', '2', '3', '4']);
});
});
Loading
Loading