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
Original file line number Diff line number Diff line change
Expand Up @@ -203,22 +203,30 @@ describe('[#5324] InMemoryDriver.find compiles a document-level $not', () => {
* where this measurement is recorded. Pinned as measured so the fix that lands
* there has to move these lines deliberately.
*
* ⚠️ [#5299, ruled 2026-08-10] The ruling is in, and it says the REFERENCE
* column below is the target on all three rows: SQL three-valued logic is the
* common denominator, so **negative operators never match no-value rows; the
* only ways to select "no value" are `$exists: false` / `$null: true`.** The
* `live` column is therefore the side that is wrong on every row here —
* mingo's `$exists` is key-presence, and its `$nin` / `$notContains` match a
* value that is not there.
* ⚠️ [#5299, settled 2026-08-10] The semantics are settled, and neither column
* below is wholly right — they are correct on complementary rows:
*
* ⛔ Still not flipped, and by decision rather than by difficulty: this package
* is inside the #5499 investment freeze. Note also what the ruling assumed and
* this file disproves — it says "driver-memory already reads has-value" and
* "driver-memory and SQL already agree", which is true of the reference
* matcher and FALSE of the live query path users actually reach. That is the
* reason this pin exists.
* `$exists` REFERENCE is correct. `$exists` means "has a value"
* (#5298 ③ / #5369, PR #5962), so mingo's key-presence
* reading is the divergent one.
* `$nin` LIVE is correct. Negative operators MATCH no-value rows —
* #5146, extended by #5298, re-affirmed 2026-08-10 — so a
* missing key satisfying `$nin` is the affirmed answer, and
* the reference matcher's early-exit guard is the divergence.
* `$notContains` LIVE is correct, for the same reason.
*
* A ruling that morning (07:33Z) would have made the REFERENCE column the
* target on all three rows. Cells 1 and 3 of it were WITHDRAWN the same day,
* once the reversal's cross-backend cost had been measured, and the include
* direction was re-affirmed — which leaves the split above.
*
* ⛔ Nothing is flipped in either direction: this package is inside the #5499
* investment freeze. What the round trip confirmed is exactly why this pin
* exists — this package answers with two different faces, so a statement like
* "driver-memory already reads has-value" is true of the reference matcher and
* FALSE of the live query path users actually reach.
*/
describe('[#5299] the ruled cells, live vs reference — behaviour frozen (#5499)', () => {
describe('[#5299] the settled cells, live vs reference — behaviour frozen (#5499)', () => {
const liveVsReference = async (where: unknown) => ({
live: await idsFrom(nulled, where),
reference: NULLED.filter((r) => match(r, where)).map((r) => r.id),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,25 +128,32 @@ describe('[#5146] memory-matcher — $not over records with no value', () => {
});
});

// ── The three cells #5299 ruled on — behaviour FROZEN, annotation current ──
// ── The three #5299 cells — behaviour FROZEN, annotation current ───────────

/**
* [#5299, ruled 2026-08-10] These three cells were filed as "known
* disagreements with `formula`, not ruled on by #5146". They are ruled now:
* SQL's native three-valued logic is the common denominator, so **negative
* operators never match no-value rows; the only ways to select "no value" are
* `$exists: false` / `$null: true`.**
* [#5299, settled 2026-08-10] These three cells were filed as "known
* disagreements with `formula`, not ruled on by #5146". They are settled now,
* and the settled direction is INCLUDE: `$ne` / `$nin` / `$notContains` MATCH
* a no-value row (#5146, extended by #5298, shipped across eleven surfaces),
* and `$exists` means "has a value" (#5298 ③ / #5369, PR #5962).
*
* ⛔ Nothing below is flipped, and the reason is not inertia. This package is
* inside the #5499 investment freeze, and the ruling itself says
* `checkCondition`'s early-exit guard STAYS AS IT IS. What the ruling changed
* is the annotation: the section is no longer "a divergence nobody has ruled
* on", it is "a ruled target, with this matcher's distance from it measured".
* A ruling on 2026-08-10 07:33Z briefly went the other way — SQL's native
* three-valued logic as the common denominator, negative operators never
* matching no-value rows — which would have made this matcher's answers below
* the canonical ones. Cells 1 and 3 of it were WITHDRAWN the same day, once
* the reversal's cross-backend cost had been measured, and include was
* re-affirmed. `$exists` = has-value is the leg that stands.
*
* ⛔ Nothing below is flipped, and the reason is not inertia: this package is
* inside the #5499 investment freeze, and both rulings leave `checkCondition`'s
* early-exit guard exactly as it is. So this section is what it always was — a
* measured divergence from the platform answer, pinned rather than harmonised
* — with its wording brought current.
*
* Re-measured on `60f0dd8`, because the old wording had gone stale in a way
* that mattered — it named `formula` as the key-presence reader on `$exists`,
* and `formula` stopped being that in PR #5962 (#5298 ③ / #5369). Where each
* cell actually stands:
* and `formula` stopped being that in PR #5962. Where each cell stands against
* the affirmed include direction:
*
* `$exists` CONVERGED, and this matcher was already right. Both
* evaluators read "has a value"; the formula-side assertion
Expand All @@ -155,29 +162,30 @@ describe('[#5146] memory-matcher — $not over records with no value', () => {
* mingo query path and the analytics face — still read
* key-presence, so the package disagrees with itself and this
* test is the face that is correct.
* `$notContains` This matcher ALREADY answers the ruled semantics; every
* other surface in the repo (including all four SQL
* compilers, deliberately, via #5298's `nullSafeNegative`)
* answers the opposite. Here the gap is theirs, not ours.
* `$nin` HALF right: a missing key already does not match, a
* present-but-null value still does. The ruled answer is "no"
* for both. Frozen at this state.
* `$notContains` DIVERGES: a value-less field does not satisfy it here,
* while `formula` and all four SQL compilers say it does —
* deliberately, via #5298's `nullSafeNegative`. This matcher
* is the odd one out among the eleven surfaces, and frozen.
* `$nin` HALF converged: a present-but-null value already matches,
* as the affirmed direction requires; a MISSING key still
* does not, because the early-exit guard does not exempt
* `$nin`. Frozen at this state.
*/
describe('[#5299] the ruled no-value cells — target recorded, behaviour frozen (#5499)', () => {
describe('[#5299] the settled no-value cells — divergence measured, behaviour frozen (#5499)', () => {
it('$nin: an ABSENT field is treated differently from a null one', () => {
// The early `value === undefined` guard in `checkCondition` exempts only
// `$exists` / `$ne` / `$null`, so an absent field fails `$nin` outright
// while a null field passes it. The ruling keeps this guard; the NULL half
// is the part still short of the ruled answer, and it is frozen.
// while a null field passes it. The guard stays; the MISSING half is the
// part still short of the affirmed include answer, and it is frozen.
expect(ids(NULLED, { $not: { stage: { $nin: ['won'] } } })).toEqual(['1']);
expect(ids(MISSING, { $not: { stage: { $nin: ['won'] } } })).toEqual(['1', '3', '4']);
});

it('$notContains: a value-less field does NOT satisfy it here — the RULED answer', () => {
// `typeof null !== 'string'` → false, so the negation matches. This is
// what #5299 ruled canonical. `formula` and all four SQL compilers answer
// the opposite today; moving them is a cross-backend programme, not a
// change to this file.
it('$notContains: a value-less field does NOT satisfy it here — DIVERGENT', () => {
// `typeof null !== 'string'` → false, so the negation matches. The
// affirmed direction is the opposite: `formula` and all four SQL compilers
// say a value-less field DOES satisfy `$notContains`, and #5299 re-affirmed
// that on 2026-08-10. This matcher stays where it is under #5499.
expect(matched({ $not: { stage: { $notContains: 'w' } } })).toEqual(['1', '3', '4']);
});

Expand Down
67 changes: 36 additions & 31 deletions packages/formula/src/matches-filter-not-null-safe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,10 @@ describe('[#5146] matchesFilterCondition — $not over records with no value', (
// rejects it. `driver-sql` follows this answer; `driver-memory`'s
// REFERENCE matcher answers the opposite for a null-valued field.
//
// ⚠️ [#5299, ruled 2026-08-10] This is the SUPERSEDED direction — see the
// block at the bottom of this file for the ruled target and the measured
// reason nothing has moved yet.
// ⚠️ [#5299, 2026-08-10] A ruling that morning would have reversed this
// direction; it was WITHDRAWN the same day and include re-affirmed. See
// the block at the bottom of this file for the affirmed direction and the
// measurement that settled it.
expect(matched({ $not: { stage: { $notContains: 'w' } } })).toEqual(['1']);
});

Expand Down Expand Up @@ -187,41 +188,44 @@ describe('[#5146] matchesFilterCondition — $not over records with no value', (
});
});

// ── The NON-negated negatives — pinned against a ruling that has not landed
// ── The NON-negated negatives — the affirmed direction, pinned ────────────

/**
* [#5299, ruled 2026-08-10] The maintainer took SQL's native three-valued
* logic as the common denominator: **negative operators never match no-value
* rows; the only ways to select "no value" are `$exists: false` /
* `$null: true`.** Under that rule this evaluator answers `['2']` below.
* [#5299, re-affirmed 2026-08-10] `$ne` / `$nin` / `$notContains` MATCH a
* no-value row. That is the platform semantics: ruled by #5146 for `$not`,
* extended to this operator family by #5298, and shipped across all eleven
* filter surfaces. This evaluator answers `['2','3','4']` below — the affirmed
* answer, not a lag behind a target.
*
* It answers `['2','3','4']`, and that is pinned here rather than fixed,
* because flipping it ALONE would re-open the exact hole PR #5962 closed. That
* PR converged `formula` (the RLS write-side `check`) and `read-scope-sql`
* (the read-side lowering) in ONE change precisely because they are
* security-coupled: one policy string must not admit two row sets. Every SQL
* face still emits `nullSafeNegative` for these two operators
* It was challenged and it held. A ruling on 2026-08-10 07:33Z would have
* taken SQL's native three-valued logic as the common denominator — negative
* operators never matching no-value rows, `['2']` below — and cells 1 and 3 of
* it were WITHDRAWN the same day once the reversal's cost had been measured
* across every surface.
*
* These assertions are load-bearing under either direction, and the coupling
* that makes them so is a large part of why the reversal was declined: this
* evaluator is the RLS write-side `check` and `read-scope-sql` is the
* read-side lowering, and PR #5962 converged them in ONE change precisely
* because they are security-coupled — one policy string must not admit two row
* sets. Every SQL face emits `nullSafeNegative` for these two operators
* (`col IS NULL OR col NOT IN (…)`), so a formula-only flip would make an RLS
* `check` DENY a write on a null field that the read scope still RETURNS —
* #5962's defect with the sign reversed.
*
* So these assertions are load-bearing in both directions. They say what this
* evaluator does today, and they are the tripwire the cross-backend PR must
* step on: whoever lands the ruled semantics changes these lines DELIBERATELY,
* in the same PR that moves `driver-sql`, `read-scope-sql`, `filter-normalizer`
* and `driver-turso`'s remote transport — not one evaluator at a time.
* #5962's defect with the sign reversed. Anyone re-proposing a reversal steps
* on these lines DELIBERATELY, in the same PR that moves `driver-sql`,
* `read-scope-sql`, `filter-normalizer` and `driver-turso`'s remote transport
* — not one evaluator at a time.
*
* The full eleven-surface measurement and the enrolment blocker (the
* conformance ledger has no per-row DEBT, and two of the five scored drivers
* are inside the #5499 freeze) are recorded on family 4 in
* `@objectstack/spec`'s `filter-logic-conformance.ts` header.
* The full eleven-surface measurement, and why the reversal was declined, are
* recorded on family 4 in `@objectstack/spec`'s
* `filter-logic-conformance.ts` header.
*/
describe('[#5299] $notContains / $nin over a value-less field — the pre-ruling answer', () => {
it('$notContains MATCHES a value-less field — ruled target is that it must NOT', () => {
describe('[#5299] $notContains / $nin over a value-less field — the affirmed direction', () => {
it('$notContains MATCHES a value-less field — re-affirmed 2026-08-10', () => {
expect(matched({ stage: { $notContains: 'w' } })).toEqual(['2', '3', '4']);
});

it('$nin MATCHES a value-less field — ruled target is that it must NOT', () => {
it('$nin MATCHES a value-less field — re-affirmed 2026-08-10', () => {
expect(matched({ stage: { $nin: ['won'] } })).toEqual(['2', '3', '4']);
});

Expand All @@ -233,9 +237,10 @@ describe('[#5146] matchesFilterCondition — $not over records with no value', (
expect(matched({ stage: { $nin: ['won'] } })).toEqual(matched({ stage: { $ne: 'won' } }));
});

it('the ruled ESCAPE HATCH already works, in both directions', () => {
// Whatever happens to the three cells above, the rule's second half is
// already true here: "no value" is selectable, precisely, today.
it('the ESCAPE HATCH works, in both directions', () => {
// The half of the 07:33Z rule that was never in dispute, and that its
// withdrawal did not touch: "no value" is selectable, precisely, today —
// and `$exists` reads has-value (cell 2, shipped in PR #5962, stands).
expect(matched({ stage: { $exists: false } })).toEqual(['3', '4']);
expect(matched({ stage: { $null: true } })).toEqual(['3', '4']);
expect(matched({ stage: { $exists: true } })).toEqual(['1', '2']);
Expand Down
Loading
Loading