Skip to content

Commit 9268aec

Browse files
claude[bot]claude
andauthored
test(drivers): pin the measured $exists has-value divergence on both no-value readings (#13420)
Executes the cell the recorded table states as reading rather than measurement, and pins what it found. Two files, no source change, no conformance enrolment. driver-memory has FOUR exits and they do not agree: the reference matcher reads has-value (the ruling, shipped by #5962), the live mingo path and the analytics face's executed rows read key-presence, and the analytics face's echoed SQL renders `IS NOT NULL` / `IS NULL` — so that face disagrees with itself. driver-mongodb passes `$exists` through under its own name. Executing widened the divergence threefold against the reading: `$exists: false` and `$not { $exists: true }` diverge too, and in the opposite, worse direction — the no-value row is dropped from the query that asks for no-value rows. Every divergent cell is on the `name: null` reading; the key-absent reading already answers the ruling everywhere, so a fixture that spells "no value" as an absent key measures none of it. The direction stays undecided here. Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5f0a9c4 commit 9268aec

2 files changed

Lines changed: 404 additions & 0 deletions

File tree

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#13195] `$exists` = HAS A VALUE, and the four exits of this package that do
5+
* not all say so — measured, not read.
6+
*
7+
* ## The ruling
8+
*
9+
* `$exists` means "the field has a value" (`!= null`), never key-presence:
10+
* #5298 leg 3 / #5369, landed in PR #5962. It is settled and shipped on the
11+
* surfaces that ruling named — `@objectstack/formula`'s `matchesFilterCondition`
12+
* and this package's reference matcher (`memory-matcher.ts`, pinned by
13+
* `memory-matcher-not-null-safe.test.ts`).
14+
*
15+
* ## What this file is, and what it is NOT
16+
*
17+
* It is a PIN OF THE MEASURED PRESENT, taken because the card that records this
18+
* divergence records it as reading rather than execution and its own header
19+
* says so ("Read, not re-measured"). Executing it found the reading OPTIMISTIC
20+
* by a factor of three, exactly as `aggregation-conformance.ts`'s DEBT note
21+
* predicts a read row will be.
22+
*
23+
* It is NOT the ruling's enforcement, and it is NOT a decision. The direction
24+
* for `driver-mongodb` is unsettled (its `$exists` is key-presence at the wire
25+
* level), and the `FILTER_LOGIC_CASES` enrolment that would enforce the ruling
26+
* cannot land before the backends move — the DEBT ledger in
27+
* `scripts/check-driver-conformance.mjs` is per (driver x case-set), so a row
28+
* added ahead of a backend is only "a gate that reports a known red" (#5903).
29+
*
30+
* ⚠️ WHEN THE DIRECTION IS DECIDED, INVERT THESE IN PLACE. Do not delete them
31+
* and do not re-baseline them to whatever the new output happens to be: each
32+
* divergent expectation below names the ruling's answer beside the measured
33+
* one, so flipping it is a one-line edit that stays reviewable.
34+
*
35+
* ## Why the fixture has TWO readings and why that is the load-bearing part
36+
*
37+
* "No value" has two shapes, and they reach different code:
38+
*
39+
* - `name: null` — how a SQL NULL round-trips into a record;
40+
* - the key ABSENT — what a partial write leaves.
41+
*
42+
* Measured: EVERY divergent cell in this file is on the `name: null` reading,
43+
* and the key-absent reading agrees with the ruling on all four exits. That is
44+
* the opposite shape from the neighbouring #13166 cell, where `$notContains`
45+
* diverged on both readings and `$nin` on the absent one only. Consequence
46+
* worth stating loudly: a fixture that spells "no value" as an ABSENT KEY
47+
* measures ZERO of this divergence. Keep both columns.
48+
*
49+
* ## Why `$exists: false` is here and not only `$exists: true`
50+
*
51+
* The recorded table carries one column, `$exists: true` on a null value, whose
52+
* divergence is SURPLUS — a row the author can see and narrow. `$exists: false`
53+
* diverges in the opposite direction, and it is the worse one: the row with no
54+
* value is DROPPED from the query that asks for rows with no value, so the
55+
* caller sees an empty result and nothing to narrow. `filter-logic-conformance.ts`
56+
* makes exactly that trade its reason for keeping the include direction on
57+
* `$ne` / `$nin` ("silent absence for visible surplus"); this cell sits on the
58+
* wrong side of it and was invisible while only `$exists: true` was recorded.
59+
*/
60+
61+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
62+
63+
import { InMemoryDriver } from './memory-driver.js';
64+
import { match } from './memory-matcher.js';
65+
import { MemoryAnalyticsService } from './memory-analytics.js';
66+
67+
/** `name` present but NULL — how a SQL NULL round-trips into a record. */
68+
const NULLED: Array<Record<string, unknown>> = [
69+
{ id: '1', name: 'alpha-one' },
70+
{ id: '2', name: 'beta' },
71+
{ id: '3', name: null },
72+
];
73+
74+
/** The same rows with `name` ABSENT — the shape a partial write leaves. */
75+
const MISSING: Array<Record<string, unknown>> = [
76+
{ id: '1', name: 'alpha-one' },
77+
{ id: '2', name: 'beta' },
78+
{ id: '3' },
79+
];
80+
81+
const sorted = (ids: string[]): string[] => [...ids].sort();
82+
83+
/** Exit 1 — the LIVE query path: `find()` → `convertToMongoQuery` → mingo. */
84+
async function liveIds(rows: Array<Record<string, unknown>>, where: unknown): Promise<string[]> {
85+
const driver = new InMemoryDriver({ persistence: false });
86+
await driver.connect();
87+
for (const row of rows) await driver.create('t', { ...row });
88+
try {
89+
const out = await driver.find('t', { where } as never);
90+
return sorted((out as Array<Record<string, unknown>>).map((r) => String(r.id)));
91+
} finally {
92+
await driver.disconnect();
93+
}
94+
}
95+
96+
/** Exit 2 — the REFERENCE matcher, the exit #5962 already aligned. */
97+
const matcherIds = (rows: Array<Record<string, unknown>>, where: unknown): string[] =>
98+
sorted(rows.filter((r) => match(r, where)).map((r) => String(r.id)));
99+
100+
const CUBE = {
101+
name: 'deals',
102+
title: 'Deals',
103+
sql: 't',
104+
measures: { total: { name: 'total', label: 'Total', type: 'count', sql: 'id' } },
105+
dimensions: {
106+
id: { name: 'id', label: 'Id', type: 'string', sql: 'id' },
107+
name: { name: 'name', label: 'Name', type: 'string', sql: 'name' },
108+
},
109+
public: true,
110+
} as never;
111+
112+
/** Exits 3 and 4 — the analytics face's EXECUTED rows and its ECHOED statement. */
113+
async function analytics(
114+
rows: Array<Record<string, unknown>>,
115+
where: unknown,
116+
): Promise<{ executed: string[]; sql: string }> {
117+
const driver = new InMemoryDriver({ persistence: false });
118+
await driver.connect();
119+
for (const row of rows) await driver.create('t', { ...row });
120+
const service = new MemoryAnalyticsService({ driver, cubes: [CUBE] } as never);
121+
const query = { cube: 'deals', measures: ['total'], dimensions: ['id'], where } as never;
122+
try {
123+
const executed = sorted(
124+
((await service.query(query)).rows as Array<Record<string, unknown>>).map((r) => String(r.id)),
125+
);
126+
const { sql } = await service.generateSql(query);
127+
return { executed, sql: sql.replace(/\s+/g, ' ') };
128+
} finally {
129+
await driver.disconnect();
130+
}
131+
}
132+
133+
describe('[#13195] `$exists` on a row with NO VALUE — the two readings, the four exits', () => {
134+
describe('the key-absent reading: every exit already answers the ruling', () => {
135+
it('`$exists: true` excludes the no-key row everywhere', async () => {
136+
expect(await liveIds(MISSING, { name: { $exists: true } })).toEqual(['1', '2']);
137+
expect(matcherIds(MISSING, { name: { $exists: true } })).toEqual(['1', '2']);
138+
expect((await analytics(MISSING, { name: { $exists: true } })).executed).toEqual(['1', '2']);
139+
});
140+
141+
it('`$exists: false` returns the no-key row everywhere', async () => {
142+
expect(await liveIds(MISSING, { name: { $exists: false } })).toEqual(['3']);
143+
expect(matcherIds(MISSING, { name: { $exists: false } })).toEqual(['3']);
144+
expect((await analytics(MISSING, { name: { $exists: false } })).executed).toEqual(['3']);
145+
});
146+
147+
it('`$not` around it agrees too, on the exits that accept `$not`', async () => {
148+
expect(await liveIds(MISSING, { $not: { name: { $exists: true } } })).toEqual(['3']);
149+
expect(matcherIds(MISSING, { $not: { name: { $exists: true } } })).toEqual(['3']);
150+
});
151+
});
152+
153+
describe('the `name: null` reading: the reference matcher answers the ruling, the other exits do not', () => {
154+
it('the reference matcher reads HAS-VALUE — the ruling, shipped by #5962', () => {
155+
expect(matcherIds(NULLED, { name: { $exists: true } })).toEqual(['1', '2']);
156+
expect(matcherIds(NULLED, { name: { $exists: false } })).toEqual(['3']);
157+
expect(matcherIds(NULLED, { $not: { name: { $exists: true } } })).toEqual(['3']);
158+
});
159+
160+
it('DIVERGENT — the live mingo path reads KEY-PRESENCE: `$exists: true` keeps the null row', async () => {
161+
// Ruling: ['1','2']. Measured: ['1','2','3'] — mingo tests key presence.
162+
expect(await liveIds(NULLED, { name: { $exists: true } })).toEqual(['1', '2', '3']);
163+
});
164+
165+
it('DIVERGENT and WORSE — `$exists: false` on the live path returns NOTHING', async () => {
166+
// Ruling: ['3']. Measured: [] — the row with no value is dropped from the
167+
// query that asks for rows with no value. Silent absence, not surplus.
168+
expect(await liveIds(NULLED, { name: { $exists: false } })).toEqual([]);
169+
expect(await liveIds(NULLED, { $not: { name: { $exists: true } } })).toEqual([]);
170+
});
171+
172+
it('DIVERGENT — the analytics face EXECUTES the live mingo key-presence answer', async () => {
173+
expect((await analytics(NULLED, { name: { $exists: true } })).executed).toEqual(['1', '2', '3']);
174+
expect((await analytics(NULLED, { name: { $exists: false } })).executed).toEqual([]);
175+
});
176+
177+
it('the analytics face ECHOES the has-value answer — so it disagrees with ITSELF', async () => {
178+
// The statement drawn beside the chart says `IS NOT NULL` / `IS NULL`,
179+
// which is the ruling; the rows the chart is drawn FROM say key-presence.
180+
// Asserted as an INEQUALITY as well, so it cannot be closed in silence.
181+
const t = await analytics(NULLED, { name: { $exists: true } });
182+
const f = await analytics(NULLED, { name: { $exists: false } });
183+
expect(t.sql).toContain('name IS NOT NULL');
184+
expect(f.sql).toContain('name IS NULL');
185+
expect(t.executed).not.toEqual(matcherIds(NULLED, { name: { $exists: true } }));
186+
expect(f.executed).not.toEqual(matcherIds(NULLED, { name: { $exists: false } }));
187+
});
188+
});
189+
190+
describe('controls — the fixture really carries the two readings, and the exits still discriminate', () => {
191+
it('the driver stores a null VALUE for one fixture and NO KEY for the other', async () => {
192+
const store = async (rows: Array<Record<string, unknown>>) => {
193+
const driver = new InMemoryDriver({ persistence: false });
194+
await driver.connect();
195+
for (const row of rows) await driver.create('t', { ...row });
196+
try {
197+
const all = (await driver.find('t', {} as never)) as Array<Record<string, unknown>>;
198+
const three = all.find((r) => r.id === '3')!;
199+
return { hasKey: Object.prototype.hasOwnProperty.call(three, 'name'), value: three.name };
200+
} finally {
201+
await driver.disconnect();
202+
}
203+
};
204+
expect(await store(NULLED)).toEqual({ hasKey: true, value: null });
205+
expect(await store(MISSING)).toEqual({ hasKey: false, value: undefined });
206+
});
207+
208+
it('a predicate that should narrow still narrows on every exit', async () => {
209+
expect(await liveIds(NULLED, { name: { $eq: 'beta' } })).toEqual(['2']);
210+
expect(matcherIds(NULLED, { name: { $eq: 'beta' } })).toEqual(['2']);
211+
expect((await analytics(NULLED, { name: { $eq: 'beta' } })).executed).toEqual(['2']);
212+
});
213+
});
214+
});

0 commit comments

Comments
 (0)