Skip to content

Commit ce0acc3

Browse files
committed
test(service-analytics): the Turso remote-face echo exception asserts its equivalence
The enumeration pin's named exception (driver-turso's remote face, where the engine buckets in memory and the echo still prints the inherited SQLite expression) pinned only that a statement prints. Per #21647's triage, an exception must assert the equivalence: the printed statement, run with its params on the face's datasource, answers the face's keys and rows. A face that declares printsWhileEngineBucketsInMemory is now served over a seeded table with pass-through doors. Each of its printed cells (UTC and unset, every granularity) runs dryRun.sql with dryRun.params through the engine's raw-SQL bridge and asserts the closed_at / amount_sum pairs equal the face's rows, with a non-vacuity guard. The header names both exceptions and where each asserts its equivalence. No new analytics.query call site. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7d07814 commit ce0acc3

1 file changed

Lines changed: 104 additions & 20 deletions

File tree

‎packages/services/service-analytics/src/__tests__/objectql-echo-bucket-enumeration.test.ts‎

Lines changed: 104 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
* - anything else (bucketed in memory, or grouped by a driver that runs no
3434
* SQL): the refusal on `/analytics/sql`, and no `sql` on the query.
3535
*
36+
* A face's doors answer nothing, except on a named exception (below): its
37+
* table is seeded and its doors pass through, so its rows are real.
38+
*
3639
* The engine's predicate (`engine.aggregate`'s pushdown test: the driver's
3740
* `supports.queryDateGranularity`, `tzRequiresInMemory`, per-aggregation
3841
* filters) is not reachable from this package, so the strategy reads the
@@ -81,14 +84,30 @@
8184
* SQLite's `supports`; the remote face publishes `queryDateGranularity: {}`,
8285
* so the engine buckets in memory there.
8386
*
84-
* ## The one named exception
87+
* ## The two named exceptions, and the equivalence each asserts
88+
*
89+
* Two cells print while the engine buckets in memory. The echo prints a
90+
* statement only if that statement, run on the query's own datasource,
91+
* answers the face's keys and rows; otherwise it answers the refusal
92+
* (#21647's triage). So each exception asserts that equivalence, not merely
93+
* that a statement prints. If it ever breaks, the cell is red, and the cell
94+
* falls under the refusal.
8595
*
86-
* `turso`'s remote face: the engine buckets in memory, and the echo prints the
87-
* SQLite expression the driver renders, because the hook answers. Run on
88-
* libSQL it answers the face's keys (the driver's own reason for inheriting
89-
* it). It is pinned as an exception, so moving either side is red. The other
90-
* cell of that kind, a measure carrying its own `filter`, is outside this
91-
* table's axes and is pinned in `objectql-echo-date-bucket.test.ts`.
96+
* - `turso`'s remote face, in this table. The engine buckets in memory, and
97+
* the echo prints the SQLite expression the driver renders, because the
98+
* hook answers. A face declaring `printsWhileEngineBucketsInMemory` is
99+
* served over a seeded table (`EXCEPTION_DEALS`). Each printed cell (UTC
100+
* and unset, every granularity) runs the printed statement with its
101+
* params through the engine's raw-SQL bridge, on that face's datasource.
102+
* It asserts the `closed_at` / `amount_sum` pairs equal the face's rows.
103+
* The row's datasource is better-sqlite3, by code path. That libSQL
104+
* answers the labels better-sqlite3 answers for this expression is pinned
105+
* in `driver-turso`'s `turso-remote-inherited-members.test.ts`.
106+
* - A measure carrying its own `filter`, outside this table's axes. The case
107+
* "a measure filter, which the engine aggregates in memory" in
108+
* `objectql-echo-date-bucket.test.ts` asserts the same equivalence. It
109+
* runs on SQLite on every run, and on PostgreSQL where
110+
* `OS_TEST_POSTGRES_URL` is set.
92111
*/
93112

94113
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
@@ -123,6 +142,33 @@ const CUBES = [
123142

124143
const GRANULARITIES = TimeUpdateInterval.options;
125144

145+
/**
146+
* The table a named exception is served over (see the header). Every
147+
* granularity answers several buckets, and at least one bucket sums two rows.
148+
* e1 and e2 straddle an ISO week-year boundary: Sunday 2025-12-28 is
149+
* 2025-W52, and Monday 2025-12-29 is 2026-W01, both in calendar 2025. e3 is
150+
* 20:00 UTC on 31 January, e4 and e5 share a UTC day, and e6 opens Q2.
151+
*/
152+
const EXCEPTION_DEALS = [
153+
{ id: 'e1', closed_at: '2025-12-28T10:00:00.000Z', amount: 3 },
154+
{ id: 'e2', closed_at: '2025-12-29T10:00:00.000Z', amount: 20 },
155+
{ id: 'e3', closed_at: '2026-01-31T20:00:00.000Z', amount: 7 },
156+
{ id: 'e4', closed_at: '2026-02-03T08:00:00.000Z', amount: 1 },
157+
{ id: 'e5', closed_at: '2026-02-03T23:30:00.000Z', amount: 4 },
158+
{ id: 'e6', closed_at: '2026-04-02T00:30:00.000Z', amount: 5 },
159+
] as const;
160+
const EXCEPTION_TOTAL = EXCEPTION_DEALS.reduce((sum, deal) => sum + deal.amount, 0);
161+
162+
/**
163+
* Rows as `[closed_at, amount_sum]` pairs, ordered by the bucket. The key is
164+
* compared verbatim, and the measure as a number. The query asks no `order`,
165+
* so arrival order is not part of the answer on either side.
166+
*/
167+
const bucketPairs = (rows: unknown): Array<[unknown, number]> =>
168+
(rows as Array<Record<string, unknown>>)
169+
.map((row): [unknown, number] => [row.closed_at, Number(row.amount_sum)])
170+
.sort(([a], [b]) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0));
171+
126172
const quiet = { debug() {}, info() {}, warn() {}, error() {}, child() { return quiet; } };
127173

128174
type Tier = 'pushdown' | 'in-memory';
@@ -154,7 +200,9 @@ interface DriverFace {
154200
/**
155201
* The named exception: where the zone's class lets the engine push a bucket
156202
* down, this face still buckets in memory, and the echo prints the hook's
157-
* expression. The reason, in words.
203+
* expression. The reason, in words. A face declaring it is served over
204+
* `EXCEPTION_DEALS`, and each of its printed cells asserts the equivalence
205+
* (see the header).
158206
*/
159207
readonly printsWhileEngineBucketsInMemory?: string;
160208
}
@@ -230,7 +278,7 @@ const DRIVER_ROWS = {
230278
},
231279
release: releaseKnex,
232280
printsWhileEngineBucketsInMemory:
233-
'it advertises no granularity, and inherits the SQLite expression, which libSQL runs and which answers the face\'s keys',
281+
'it advertises no granularity, and inherits the SQLite expression, which libSQL runs and which answers the face\'s keys and rows',
234282
},
235283
],
236284
} satisfies Record<BuiltinDriverId, readonly DriverFace[]>;
@@ -256,16 +304,31 @@ const bucketed = (granularity: string, timezone: string | undefined) => ({
256304
async function serve(face: DriverFace) {
257305
const driver = face.build();
258306
const doors: Tier[] = [];
259-
// Spies, not passthroughs: the engine's choice is made before either door is
260-
// called, so the rows are not asked here (the live block below asks them).
261-
driver.connect = async () => {};
262-
driver.aggregate = async () => { doors.push('pushdown'); return []; };
263-
driver.find = async () => { doors.push('in-memory'); return []; };
307+
// A named exception must show its equivalence, so its table is seeded and
308+
// its doors record the tier and pass through: its rows are real. Every other
309+
// face's doors are spies that answer nothing, because the engine's choice is
310+
// made before either door is called (the block at the end asks the rows of
311+
// `driver-memory`'s surface).
312+
const seeded = face.printsWhileEngineBucketsInMemory !== undefined;
313+
if (seeded) {
314+
const aggregate = driver.aggregate.bind(driver);
315+
const find = driver.find.bind(driver);
316+
driver.aggregate = async (...args: unknown[]) => { doors.push('pushdown'); return aggregate(...args); };
317+
driver.find = async (...args: unknown[]) => { doors.push('in-memory'); return find(...args); };
318+
} else {
319+
driver.connect = async () => {};
320+
driver.aggregate = async () => { doors.push('pushdown'); return []; };
321+
driver.find = async () => { doors.push('in-memory'); return []; };
322+
}
264323

265324
const engine = new ObjectQL({ logger: quiet } as any);
266325
engine.registerDriver(driver as any, true);
267326
await engine.init();
268327
engine.registry.registerObject(DEAL_OBJECT as any);
328+
if (seeded) {
329+
await engine.syncSchemas();
330+
for (const deal of EXCEPTION_DEALS) await engine.insert(DEAL, { ...deal } as any);
331+
}
269332

270333
const registered: Record<string, unknown> = {};
271334
await new AnalyticsServicePlugin({ cubes: CUBES, debugSql: true } as any).init({
@@ -277,17 +340,27 @@ async function serve(face: DriverFace) {
277340
} as never);
278341
const analytics = registered.analytics as AnalyticsService;
279342

280-
/** Ask one cell: where the engine put the bucket, and both faces' echoes. */
343+
/** Ask one cell: where the engine put the bucket, the face's rows, and both faces' echoes. */
281344
const ask = async (granularity: string, timezone: string | undefined) => {
282345
doors.length = 0;
283346
const query = bucketed(granularity, timezone);
284347
const res = await analytics.query(query as any);
285348
const tiers = [...doors];
286349
const dryRun = await analytics.generateSql(query as any).then(
287-
(r) => ({ sql: r.sql, refusal: undefined }),
288-
(e) => ({ sql: undefined, refusal: e as Error & { code?: string; status?: number; refusal?: unknown } }),
350+
(r) => ({ sql: r.sql, params: r.params, refusal: undefined }),
351+
(e) => ({ sql: undefined, params: undefined, refusal: e as Error & { code?: string; status?: number; refusal?: unknown } }),
289352
);
290-
return { tiers, querySql: res.sql, dryRun };
353+
return { tiers, rows: res.rows, querySql: res.sql, dryRun };
354+
};
355+
356+
/**
357+
* Run a printed statement with its params on this face's datasource,
358+
* through the engine's raw-SQL bridge, as `objectql-echo-date-bucket.test.ts`
359+
* runs its echo.
360+
*/
361+
const run = async (sql: string, params: readonly unknown[]) => {
362+
const result = await (engine as any).execute(sql.replace(/\$(\d+)/g, '?'), { args: params, object: DEAL });
363+
return Array.isArray(result) ? result : (result as { rows: unknown[] }).rows;
291364
};
292365

293366
/** The driver's own answer for this bucket, as the bridge receives it. */
@@ -297,7 +370,7 @@ async function serve(face: DriverFace) {
297370
return typeof answered === 'string' && answered !== '' ? answered : undefined;
298371
};
299372

300-
return { ask, expression, release: async () => { await face.release?.(driver); } };
373+
return { ask, expression, run, release: async () => { await face.release?.(driver); } };
301374
}
302375

303376
describe('[#21647] the echo of a date bucket: driver x timezone class x granularity', () => {
@@ -341,7 +414,7 @@ describe('[#21647] the echo of a date bucket: driver x timezone class x granular
341414
for (const zoneClass of ZONE_CLASSES) {
342415
for (const probe of zoneClass.probes) {
343416
it.each(GRANULARITIES)(`timezone ${probe ?? 'unset'} (${zoneClass.label}), %s`, async (granularity) => {
344-
const { tiers, querySql, dryRun } = await served.ask(granularity, probe);
417+
const { tiers, rows, querySql, dryRun } = await served.ask(granularity, probe);
345418
expect(tiers, 'the engine reached the driver exactly once').toHaveLength(1);
346419
const [tier] = tiers;
347420
const expression = served.expression(granularity);
@@ -360,6 +433,17 @@ describe('[#21647] the echo of a date bucket: driver x timezone class x granular
360433
expect(selectedBucket(dryRun.sql!, 'closed_at')).toBe(expression);
361434
expect(dryRun.sql).toContain(`GROUP BY ${expression}`);
362435
expect(querySql).toBe(dryRun.sql);
436+
437+
if (face.printsWhileEngineBucketsInMemory !== undefined) {
438+
// The exception's equivalence: the printed statement, run
439+
// with its params on this face's datasource, answers the
440+
// face's keys and rows. If it stops, this cell is red and
441+
// belongs to the refusal.
442+
const faceRows = bucketPairs(rows);
443+
expect(faceRows.length, 'the face answered several buckets').toBeGreaterThan(1);
444+
expect(faceRows.reduce((sum, [, amount]) => sum + amount, 0), 'the face counted every seeded row').toBe(EXCEPTION_TOTAL);
445+
expect(bucketPairs(await served.run(dryRun.sql!, dryRun.params!)), face.printsWhileEngineBucketsInMemory).toEqual(faceRows);
446+
}
363447
} else {
364448
const err = dryRun.refusal;
365449
expect(err, `expected the refusal; the dry run printed ${dryRun.sql}`).toBeDefined();

0 commit comments

Comments
 (0)