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
58 changes: 58 additions & 0 deletions .changeset/objectql-test-doubles-conjoin-or.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
---
"@objectstack/objectql": patch
---

test(objectql): six in-memory driver doubles conjoin `$or`/`$and` with their sibling filters instead of short-circuiting (part of #7620)

Six test files in `packages/objectql/src` build an in-memory driver whose `WHERE`
matcher **returned early** on `$and`/`$or`, discarding every sibling equality key
in the same object:

```ts
if (Array.isArray(where.$and)) return where.$and.every((w) => matchesWhere(row, w));
if (Array.isArray(where.$or)) return where.$or.some((w) => matchesWhere(row, w));
for (const [k, v] of Object.entries(where)) { /* siblings, never reached */ }
```

A real driver ANDs them. So a query shaped like `SysMetadataRepository.listDrafts`'s —
`{ state:'draft', package_id:'app.x', $or:[{organization_id:ORG},{organization_id:null}] }`
— would have been answered on the `$or` alone, handing back rows matching neither
`state` nor `package_id`. That is not a stricter or looser edge case; it is a
different query, and the suite stays **green** while testing it.

The corrected form is the one `protocol-revert-org-scope.test.ts` already carries
from #7619: fold `$and`/`$or` into the entries loop so they compose with their
siblings rather than replacing them.

Files corrected: `protocol-recorded-by-null.test.ts`,
`save-meta-response-conformance.test.ts`, `plugin.authoring-channel.test.ts`,
`publish-meta-response-conformance.test.ts`,
`protocol-save-meta-repo-path-real-engine.test.ts`,
`protocol-registry-shadow.test.ts`.

**All six are dormant today — measured, not assumed.** A probe installed in each
matcher, logging every `where` it was handed across the six suites, recorded
**132 matcher calls and not one `$or` or `$and`** (44 / 60 / 21 / 7 plain-equality
calls in four of the files; the matchers in `save-meta-response-conformance` and
`plugin.authoring-channel` were never invoked at all — those suites drive writes,
not reads). The control that makes that silence evidence rather than a dead probe
is the 132 plain calls it did record through the same instrumentation. So no
existing test outcome changes, and none should: `packages/objectql` is
**185 files / 3274 tests, all passing**, before and after.

Dormant is not harmless, which is the point of closing it: nothing distinguished
"this double is faithful here" from "this double quietly changed the fixture",
and the next test to add an `$or` would have inherited a matcher that lies.

No product code changed, and no test assertion changed. Each matcher keeps
exactly the operator surface it already had — `$eq` unwrapping, the
`undefined`→`null` comparison normalisation, and the skip for any other
`$`-prefixed key — and a non-array `$and`/`$or` still falls through to that skip,
as before. **Deliberately not extracted into a shared helper**: the six are
identical, but `publish-meta-response-conformance.test.ts` carries the repo's own
rationale for keeping these harnesses self-contained ("a gate that imports its own
substrate from another gate's file couples two tripwires that must be able to fail
independently"), #7619's reference correction is inline for the same reason, and an
objectql-local helper could not serve the ten remaining files in `plugin-sharing`,
`plugin-security` and `runtime` anyway — it would add a second convention rather
than consolidate to one.
17 changes: 15 additions & 2 deletions packages/objectql/src/plugin.authoring-channel.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,11 +69,24 @@ function makeMemoryDriver() {
return s;
};
let nextId = 0;
// `$and` / `$or` are conjoined WITH their sibling keys, the way a real
// driver ANDs them. The short-circuiting shape this stub used to carry
// (`if ($or) return $or.some(...)`) discarded every sibling equality key in
// the same object, so a query like
// `{ state:'draft', package_id, $or:[{organization_id:ORG},{organization_id:null}] }`
// was silently answered on the `$or` alone — a different query than the one
// written, with the suite still green. See #7620.
const matchesWhere = (row: Record<string, unknown>, where: any): boolean => {
if (!where || typeof where !== 'object') return true;
if (Array.isArray(where.$and)) return where.$and.every((w: any) => matchesWhere(row, w));
if (Array.isArray(where.$or)) return where.$or.some((w: any) => matchesWhere(row, w));
for (const [k, v] of Object.entries(where)) {
if (k === '$and' && Array.isArray(v)) {
if (!v.every((w: any) => matchesWhere(row, w))) return false;
continue;
}
if (k === '$or' && Array.isArray(v)) {
if (!v.some((w: any) => matchesWhere(row, w))) return false;
continue;
}
if (k.startsWith('$')) continue;
const expected = (v && typeof v === 'object' && '$eq' in (v as any)) ? (v as any).$eq : v;
const a = row[k] === undefined ? null : row[k];
Expand Down
17 changes: 15 additions & 2 deletions packages/objectql/src/protocol-recorded-by-null.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,11 +86,24 @@ function makeStubDriver() {
};
let nextId = 0;

// `$and` / `$or` are conjoined WITH their sibling keys, the way a real
// driver ANDs them. The short-circuiting shape this stub used to carry
// (`if ($or) return $or.some(...)`) discarded every sibling equality key in
// the same object, so a query like
// `{ state:'draft', package_id, $or:[{organization_id:ORG},{organization_id:null}] }`
// was silently answered on the `$or` alone — a different query than the one
// written, with the suite still green. See #7620.
const matchesWhere = (row: Record<string, unknown>, where: any): boolean => {
if (!where || typeof where !== 'object') return true;
if (Array.isArray(where.$and)) return where.$and.every((w: any) => matchesWhere(row, w));
if (Array.isArray(where.$or)) return where.$or.some((w: any) => matchesWhere(row, w));
for (const [k, v] of Object.entries(where)) {
if (k === '$and' && Array.isArray(v)) {
if (!v.every((w: any) => matchesWhere(row, w))) return false;
continue;
}
if (k === '$or' && Array.isArray(v)) {
if (!v.some((w: any) => matchesWhere(row, w))) return false;
continue;
}
if (k.startsWith('$')) continue;
const expected = (v && typeof v === 'object' && '$eq' in (v as any)) ? (v as any).$eq : v;
const a = row[k] === undefined ? null : row[k];
Expand Down
17 changes: 15 additions & 2 deletions packages/objectql/src/protocol-registry-shadow.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -78,11 +78,24 @@ function makeStubDriver() {
};
let nextId = 0;

// `$and` / `$or` are conjoined WITH their sibling keys, the way a real
// driver ANDs them. The short-circuiting shape this stub used to carry
// (`if ($or) return $or.some(...)`) discarded every sibling equality key in
// the same object, so a query like
// `{ state:'draft', package_id, $or:[{organization_id:ORG},{organization_id:null}] }`
// was silently answered on the `$or` alone — a different query than the one
// written, with the suite still green. See #7620.
const matchesWhere = (row: Record<string, unknown>, where: any): boolean => {
if (!where || typeof where !== 'object') return true;
if (Array.isArray(where.$and)) return where.$and.every((w: any) => matchesWhere(row, w));
if (Array.isArray(where.$or)) return where.$or.some((w: any) => matchesWhere(row, w));
for (const [k, v] of Object.entries(where)) {
if (k === '$and' && Array.isArray(v)) {
if (!v.every((w: any) => matchesWhere(row, w))) return false;
continue;
}
if (k === '$or' && Array.isArray(v)) {
if (!v.some((w: any) => matchesWhere(row, w))) return false;
continue;
}
if (k.startsWith('$')) continue;
const expected = (v && typeof v === 'object' && '$eq' in (v as any)) ? (v as any).$eq : v;
const a = row[k] === undefined ? null : row[k];
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,11 +44,24 @@ function makeStubDriver() {
};
let nextId = 0;

// `$and` / `$or` are conjoined WITH their sibling keys, the way a real
// driver ANDs them. The short-circuiting shape this stub used to carry
// (`if ($or) return $or.some(...)`) discarded every sibling equality key in
// the same object, so a query like
// `{ state:'draft', package_id, $or:[{organization_id:ORG},{organization_id:null}] }`
// was silently answered on the `$or` alone — a different query than the one
// written, with the suite still green. See #7620.
const matchesWhere = (row: Record<string, unknown>, where: any): boolean => {
if (!where || typeof where !== 'object') return true;
if (Array.isArray(where.$and)) return where.$and.every((w: any) => matchesWhere(row, w));
if (Array.isArray(where.$or)) return where.$or.some((w: any) => matchesWhere(row, w));
for (const [k, v] of Object.entries(where)) {
if (k === '$and' && Array.isArray(v)) {
if (!v.every((w: any) => matchesWhere(row, w))) return false;
continue;
}
if (k === '$or' && Array.isArray(v)) {
if (!v.some((w: any) => matchesWhere(row, w))) return false;
continue;
}
if (k.startsWith('$')) continue;
const rowVal = row[k];
const expected = (v && typeof v === 'object' && '$eq' in (v as any))
Expand Down
17 changes: 15 additions & 2 deletions packages/objectql/src/publish-meta-response-conformance.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,11 +68,24 @@ function makeMemoryDriver() {
return s;
};
let nextId = 0;
// `$and` / `$or` are conjoined WITH their sibling keys, the way a real
// driver ANDs them. The short-circuiting shape this stub used to carry
// (`if ($or) return $or.some(...)`) discarded every sibling equality key in
// the same object, so a query like
// `{ state:'draft', package_id, $or:[{organization_id:ORG},{organization_id:null}] }`
// was silently answered on the `$or` alone — a different query than the one
// written, with the suite still green. See #7620.
const matchesWhere = (row: Record<string, unknown>, where: any): boolean => {
if (!where || typeof where !== 'object') return true;
if (Array.isArray(where.$and)) return where.$and.every((w: any) => matchesWhere(row, w));
if (Array.isArray(where.$or)) return where.$or.some((w: any) => matchesWhere(row, w));
for (const [k, v] of Object.entries(where)) {
if (k === '$and' && Array.isArray(v)) {
if (!v.every((w: any) => matchesWhere(row, w))) return false;
continue;
}
if (k === '$or' && Array.isArray(v)) {
if (!v.some((w: any) => matchesWhere(row, w))) return false;
continue;
}
if (k.startsWith('$')) continue;
const rowVal = row[k];
const expected = (v && typeof v === 'object' && '$eq' in (v as any)) ? (v as any).$eq : v;
Expand Down
17 changes: 15 additions & 2 deletions packages/objectql/src/save-meta-response-conformance.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -73,11 +73,24 @@ function makeMemoryDriver() {
return s;
};
let nextId = 0;
// `$and` / `$or` are conjoined WITH their sibling keys, the way a real
// driver ANDs them. The short-circuiting shape this stub used to carry
// (`if ($or) return $or.some(...)`) discarded every sibling equality key in
// the same object, so a query like
// `{ state:'draft', package_id, $or:[{organization_id:ORG},{organization_id:null}] }`
// was silently answered on the `$or` alone — a different query than the one
// written, with the suite still green. See #7620.
const matchesWhere = (row: Record<string, unknown>, where: any): boolean => {
if (!where || typeof where !== 'object') return true;
if (Array.isArray(where.$and)) return where.$and.every((w: any) => matchesWhere(row, w));
if (Array.isArray(where.$or)) return where.$or.some((w: any) => matchesWhere(row, w));
for (const [k, v] of Object.entries(where)) {
if (k === '$and' && Array.isArray(v)) {
if (!v.every((w: any) => matchesWhere(row, w))) return false;
continue;
}
if (k === '$or' && Array.isArray(v)) {
if (!v.some((w: any) => matchesWhere(row, w))) return false;
continue;
}
if (k.startsWith('$')) continue;
const rowVal = row[k];
const expected = (v && typeof v === 'object' && '$eq' in (v as any)) ? (v as any).$eq : v;
Expand Down
Loading