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
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
94113import { describe , it , expect , beforeAll , afterAll } from 'vitest' ;
@@ -123,6 +142,33 @@ const CUBES = [
123142
124143const 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+
126172const quiet = { debug ( ) { } , info ( ) { } , warn ( ) { } , error ( ) { } , child ( ) { return quiet ; } } ;
127173
128174type 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) => ({
256304async 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
303376describe ( '[#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