11// The playground engine and its span handling, under Node's test runner: `npm run test:engine`.
2- // Needs the engine built into wasm/pkg (scripts/build_engine.sh).
2+ // Needs the engine built into wasm/pkg (scripts/build_engine.sh); the script prepares the playground first .
33import assert from 'node:assert/strict' ;
44import { readFileSync } from 'node:fs' ;
55import { createRequire } from 'node:module' ;
66import { dirname , join } from 'node:path' ;
7+ import { fileURLToPath } from 'node:url' ;
78import { test } from 'node:test' ;
89
910import { initSync , analyze , fixture_tables , run_failure } from '../wasm/pkg/sqlquack_wasm.js' ;
@@ -13,6 +14,27 @@ initSync({ module: readFileSync(new URL('../wasm/pkg/sqlquack_wasm_bg.wasm', imp
1314
1415const SCHEMA = JSON . stringify ( { ORDERS : [ [ 'ORDER_ID' , 'NUMBER(38,0)' ] , [ 'NOTE' , 'VARCHAR' ] ] } ) ;
1516
17+ // A DuckDB as the page starts one: the DuckDB-WASM build it serves, the extensions it loads from the
18+ // site, then the sample schema's tables and rows. A fresh one per case, so no earlier case has
19+ // already loaded an extension a later case needs.
20+ async function pageDuckDB ( ) {
21+ const { SCHEMA : PAGE_SCHEMA , ROWS } = await import ( '../public/playground/examples.js' ) ;
22+ const { DUCKDB_DIR , DUCKDB_EXTENSIONS } = await import ( '../public/playground/duckdb-version.js' ) ;
23+ const require = createRequire ( import . meta. url ) ;
24+ const duckdb = require ( '@duckdb/duckdb-wasm/dist/duckdb-node-blocking.cjs' ) ;
25+ const dist = dirname ( require . resolve ( '@duckdb/duckdb-wasm/dist/duckdb-node-blocking.cjs' ) ) ;
26+ const bundles = { base : { mainModule : join ( dist , 'duckdb-base.wasm' ) , mainWorker : join ( dist , 'duckdb-node-base.worker.cjs' ) } } ;
27+ const db = await duckdb . createDuckDB ( bundles , new duckdb . VoidLogger ( ) , duckdb . NODE_RUNTIME ) ;
28+ await db . instantiate ( ) ;
29+ const conn = db . connect ( ) ;
30+ conn . query ( `SET custom_extension_repository = '${ fileURLToPath ( new URL ( `../public/playground/${ DUCKDB_DIR } extensions` , import . meta. url ) ) } '` ) ;
31+ for ( const extension of DUCKDB_EXTENSIONS ) conn . query ( `LOAD ${ extension } ` ) ;
32+ for ( const table of JSON . parse ( fixture_tables ( PAGE_SCHEMA ) ) . tables ) conn . query ( table ) ;
33+ conn . query ( ROWS ) ;
34+ // Every value as text, as the page reads them.
35+ return ( sql ) => conn . query ( `SELECT COLUMNS(*)::VARCHAR FROM (${ sql } ) AS result` ) . toArray ( ) . map ( ( row ) => Object . values ( row . toJSON ( ) ) ) ;
36+ }
37+
1638const marked = ( html ) => [ ...html . matchAll ( / < m a r k [ ^ > ] * > ( .* ?) < \/ m a r k > / g) ] . map ( ( match ) => match [ 1 ] ) ;
1739
1840test ( 'spans after non-ASCII text underline the characters the diagnostic names' , ( ) => {
@@ -86,39 +108,52 @@ test('the conversion budget is timed in the browser and refuses with SQ0006 when
86108} ) ;
87109
88110test ( 'every example chip stops at the stage its label names, the Run stage included' , async ( ) => {
89- const { EXAMPLE_GROUPS , EXAMPLES , CAPTIONS , ROWS , SCHEMA : PAGE_SCHEMA } = await import ( '../public/playground/examples.js' ) ;
111+ const { EXAMPLE_GROUPS , EXAMPLES , CAPTIONS , SCHEMA : PAGE_SCHEMA } = await import ( '../public/playground/examples.js' ) ;
90112 const chips = EXAMPLE_GROUPS . flatMap ( ( group ) => group . examples ) ;
91113 assert . deepEqual ( chips . map ( ( chip ) => chip . key ) . sort ( ) , Object . keys ( EXAMPLES ) . sort ( ) ) ;
92114 assert . equal ( new Set ( chips . map ( ( chip ) => chip . slug ) ) . size , chips . length ) ;
93- // Translated examples run as the page runs them: the page's fixture tables and rows, in the
94- // DuckDB-WASM build the page serves, with every value read as text.
95- const require = createRequire ( import . meta. url ) ;
96- const duckdb = require ( '@duckdb/duckdb-wasm/dist/duckdb-node-blocking.cjs' ) ;
97- const dist = dirname ( require . resolve ( '@duckdb/duckdb-wasm/dist/duckdb-node-blocking.cjs' ) ) ;
98- const bundles = { base : { mainModule : join ( dist , 'duckdb-base.wasm' ) , mainWorker : join ( dist , 'duckdb-node-base.worker.cjs' ) } } ;
99- const db = await duckdb . createDuckDB ( bundles , new duckdb . VoidLogger ( ) , duckdb . NODE_RUNTIME ) ;
100- await db . instantiate ( ) ;
101- const conn = db . connect ( ) ;
102- for ( const table of JSON . parse ( fixture_tables ( PAGE_SCHEMA ) ) . tables ) conn . query ( table ) ;
103- conn . query ( ROWS ) ;
104- const stops = ( sql ) => {
115+ // Translated examples run as the page runs them, each in a fresh DuckDB.
116+ const stops = async ( sql ) => {
105117 const report = JSON . parse ( analyze ( sql , PAGE_SCHEMA ) ) ;
106118 const failed = [ 'parse' , 'checks' , 'translate' ] . find ( ( stage ) => report [ stage ] . status !== 'pass' ) ;
107119 if ( failed ) return failed ;
120+ const run = await pageDuckDB ( ) ;
108121 try {
109- conn . query ( `SELECT COLUMNS(*)::VARCHAR FROM ( ${ report . translate . statements [ 0 ] . sql } ) AS result` ) ;
122+ run ( report . translate . statements [ 0 ] . sql ) ;
110123 return null ;
111124 } catch {
112125 return 'run' ;
113126 }
114127 } ;
115- for ( const chip of chips ) assert . equal ( stops ( EXAMPLES [ chip . key ] ) , chip . stops , chip . name ) ;
128+ for ( const chip of chips ) assert . equal ( await stops ( EXAMPLES [ chip . key ] ) , chip . stops , chip . name ) ;
116129 // The one deliberate refusal is a known divergence, explained by its caption.
117130 const refused = JSON . parse ( analyze ( EXAMPLES . refused , PAGE_SCHEMA ) ) . translate ;
118131 assert . deepEqual ( refused . diagnostics . map ( ( d ) => d . code ) , [ 'SQ0008' ] ) ;
119132 assert . ok ( CAPTIONS . refused ) ;
120133} ) ;
121134
135+ test ( 'date arithmetic against CURRENT_DATE runs in the in-browser DuckDB, on its first statement' , async ( ) => {
136+ const { SCHEMA : PAGE_SCHEMA } = await import ( '../public/playground/examples.js' ) ;
137+ const cases = [
138+ // Last 30 days: the fixture's orders are older, and stay so.
139+ [ 'SELECT order_id FROM orders WHERE ordered_at >= DATEADD(day, -30, CURRENT_DATE)' , [ ] ] ,
140+ // Every sample customer signed up more than 30 days ago.
141+ [ 'SELECT customer_id FROM customers WHERE DATEDIFF(day, signup_date, CURRENT_DATE) > 30 ORDER BY customer_id' , [ [ '10' ] , [ '11' ] , [ '12' ] ] ] ,
142+ // A month later than a month's last day is the next month's last day when it is shorter.
143+ [
144+ "SELECT DATEADD(month, 1, DATE '2024-01-31') AS leap, DATEADD(month, -1, DATE '2026-03-31') AS feb FROM orders " +
145+ 'WHERE ordered_at < DATEADD(year, 1, CURRENT_DATE) AND order_id = 1' ,
146+ [ [ '2024-02-29' , '2026-02-28' ] ] ,
147+ ] ,
148+ ] ;
149+ for ( const [ sql , rows ] of cases ) {
150+ const report = JSON . parse ( analyze ( sql , PAGE_SCHEMA ) ) ;
151+ assert . equal ( report . translate . status , 'pass' , sql ) ;
152+ const run = await pageDuckDB ( ) ;
153+ assert . deepEqual ( run ( report . translate . statements [ 0 ] . sql ) , rows , sql ) ;
154+ }
155+ } ) ;
156+
122157test ( 'a run error from the in-browser DuckDB is told apart from an error about the rows' , ( ) => {
123158 // What the preview build of DuckDB-WASM reported for a translated DATEADD that native DuckDB 2.0 runs.
124159 assert . ok ( previewLimit ( "Binder Error: No function matches the given name and argument types '+(DATE, INTERVAL)'." ) ) ;
0 commit comments