22// worker) and shows its stages; translated SQL runs in DuckDB-WASM, which downloads in the
33// background once the page is idle.
44
5- import { DUCKDB_DIR , DUCKDB_WASM_BYTES } from './duckdb-version.js' ;
5+ import { DUCKDB_DIR , DUCKDB_EXTENSIONS , DUCKDB_WASM_BYTES } from './duckdb-version.js' ;
66import { ENGINE_DIR , ENGINE_WASM_BYTES } from './engine-version.js' ;
7- import { CAPTIONS , EXAMPLES , ROWS , SCHEMA } from './examples.js' ;
8- import { escape , rendered , resultTable , underlined } from './spans.js' ;
7+ import { CAPTIONS , EXAMPLE_GROUPS , EXAMPLES , ROWS , SCHEMA , stopLabel } from './examples.js' ;
8+ import { escape , previewLimit , rendered , resultTable , underlined } from './spans.js' ;
99
1010const here = ( path ) => new URL ( path , import . meta. url ) . href ;
1111const ENGINE_WASM = here ( `${ ENGINE_DIR } sqlquack_wasm_bg.wasm` ) ;
@@ -328,7 +328,17 @@ async function loadDuckDB() {
328328 const module = await import ( moduleUrl ) ;
329329 const db = new module . AsyncDuckDB ( new module . VoidLogger ( ) , new Worker ( workerUrl ) ) ;
330330 await db . instantiate ( wasmUrl ) ;
331- const version = await ( await db . connect ( ) ) . query ( 'SELECT version() AS v' ) ;
331+ const conn = await db . connect ( ) ;
332+ // The extensions this build lacks, from the site, before any SQL: DuckDB autoloads ICU while
333+ // binding the first statement that needs it, which corrupts that statement's function lookups.
334+ await conn . query ( `SET custom_extension_repository = '${ here ( `${ DUCKDB_DIR } extensions` ) } '` ) ;
335+ for ( const extension of DUCKDB_EXTENSIONS ) {
336+ await conn . query ( `LOAD ${ extension } ` ) . catch ( ( error ) => {
337+ throw new Error ( `the ${ extension } extension did not load: ${ error . message ?? error } ` ) ;
338+ } ) ;
339+ }
340+ const version = await conn . query ( 'SELECT version() AS v' ) ;
341+ await conn . close ( ) ;
332342 return { db, version : String ( version . toArray ( ) [ 0 ] . v ) } ;
333343}
334344
@@ -362,6 +372,8 @@ async function execute() {
362372 if ( ! sql ) return ;
363373 const ticket = ++ running ;
364374 setStage ( 'run' , 'running' , '' ) ;
375+ // Whether the error came from the translated statement itself, after the fixture loaded.
376+ let ranStatement = false ;
365377 try {
366378 if ( ! duckdb ) await startDuckDB ( ) ;
367379 const conn = await duckdb . db . connect ( ) ;
@@ -371,6 +383,7 @@ async function execute() {
371383 for ( const table of tables . tables ) await conn . query ( table . replace ( / ^ C R E A T E T A B L E / , 'CREATE OR REPLACE TABLE' ) ) ;
372384 if ( rowsBox . value . trim ( ) ) await conn . query ( rowsBox . value ) ;
373385 const started = performance . now ( ) ;
386+ ranStatement = true ;
374387 // Every value as DuckDB's own text, so decimals and timestamps show exactly.
375388 const result = await conn . query ( `SELECT COLUMNS(*)::VARCHAR FROM (${ sql } ) AS result` ) ;
376389 const elapsed = performance . now ( ) - started ;
@@ -392,6 +405,20 @@ async function execute() {
392405 const failure = JSON . parse ( ( await engine ( 'failure' , { message } ) ) . result ) ;
393406 if ( ticket !== running ) return ;
394407 const version = duckdb ? `DuckDB ${ duckdb . version } ` : '' ;
408+ if ( ! failure . errno && ranStatement && previewLimit ( message ) ) {
409+ setStage (
410+ 'run' ,
411+ 'refused' ,
412+ '<p class="pg-conversion-note"><strong>A limit of the in-browser DuckDB, not of the translation.</strong> ' +
413+ 'This page runs a preview build of DuckDB-WASM, which could not run this translated SQL. SQLQuack translates for ' +
414+ 'native DuckDB 2.0: <code>sqlquack convert</code> binds every translated statement in native DuckDB before ' +
415+ 'returning it, so run it there to see the result.</p>' +
416+ terminal ( `<span class="dim">DuckDB-WASM:</span> ${ escape ( message ) } ` ) ,
417+ version ,
418+ 'not run here' ,
419+ ) ;
420+ return ;
421+ }
395422 if ( ! failure . errno ) {
396423 setStage ( 'run' , 'fail' , terminal ( `<span class="bad">DuckDB:</span> ${ escape ( message ) } ` ) , version ) ;
397424 return ;
@@ -426,6 +453,13 @@ document.addEventListener('click', (event) => {
426453} ) ;
427454sqlBox . addEventListener ( 'input' , ( ) => {
428455 showCaption ( null ) ;
456+ // Edited SQL is no longer the example, so neither the chip nor the link names it any more.
457+ if ( activeExample ) {
458+ setActiveExample ( null ) ;
459+ const url = new URL ( location . href ) ;
460+ url . searchParams . delete ( 'example' ) ;
461+ history . replaceState ( null , '' , url ) ;
462+ }
429463 schedule ( ) ;
430464} ) ;
431465schemaBox . addEventListener ( 'input' , schedule ) ;
@@ -436,11 +470,45 @@ $('#pg-load-policy').addEventListener('click', async () => {
436470} ) ;
437471rowsBox . addEventListener ( 'input' , ( ) => latest && duckdb && schedule ( ) ) ;
438472sqlBox . addEventListener ( 'scroll' , ( ) => ( marks . scrollTop = sqlBox . scrollTop ) ) ;
439- $ ( '#pg-example' ) . addEventListener ( 'change' , ( event ) => {
440- if ( ! EXAMPLES [ event . target . value ] ) return ;
441- sqlBox . value = EXAMPLES [ event . target . value ] ;
442- showCaption ( event . target . value ) ;
443- event . target . value = '' ;
473+
474+ // The example chips: one click loads an example and runs it, and the link names it
475+ // (`?example=<slug>`) so it can be shared.
476+ const examples = EXAMPLE_GROUPS . flatMap ( ( group ) => group . examples ) ;
477+ const examplesBox = $ ( '#pg-examples' ) ;
478+ examplesBox . innerHTML = EXAMPLE_GROUPS . map (
479+ ( group ) =>
480+ `<div class="pg-group"><p class="pg-group-head"><strong>${ escape ( group . title ) } </strong> <span>${ escape ( group . tagline ) } </span></p>` +
481+ '<div class="pg-chips">' +
482+ group . examples
483+ . map (
484+ ( example ) =>
485+ `<button class="pg-chip" type="button" aria-pressed="false" data-example="${ example . key } " data-stops="${ example . stops ?? 'none' } ">` +
486+ `<span class="pg-chip-name">${ escape ( example . name ) } </span><span class="pg-chip-stop">${ escape ( stopLabel ( example . stops ) ) } </span></button>` ,
487+ )
488+ . join ( '' ) +
489+ '</div></div>' ,
490+ ) . join ( '' ) ;
491+
492+ let activeExample = null ;
493+ function setActiveExample ( key ) {
494+ activeExample = key ;
495+ for ( const chip of examplesBox . querySelectorAll ( '.pg-chip' ) ) chip . setAttribute ( 'aria-pressed' , String ( chip . dataset . example === key ) ) ;
496+ }
497+
498+ function loadExample ( key ) {
499+ sqlBox . value = EXAMPLES [ key ] ;
500+ showCaption ( key ) ;
501+ setActiveExample ( key ) ;
502+ }
503+
504+ examplesBox . addEventListener ( 'click' , ( event ) => {
505+ const key = event . target . closest ( '.pg-chip' ) ?. dataset . example ;
506+ if ( ! key ) return ;
507+ loadExample ( key ) ;
508+ const url = new URL ( location . href ) ;
509+ url . searchParams . delete ( 'sql' ) ;
510+ url . searchParams . set ( 'example' , examples . find ( ( example ) => example . key === key ) . slug ) ;
511+ history . replaceState ( null , '' , url ) ;
444512 run ( ) ;
445513} ) ;
446514
@@ -462,8 +530,13 @@ sqlBox.addEventListener('mousemove', (event) => {
462530sqlBox . addEventListener ( 'mouseleave' , ( ) => ( tip . hidden = true ) ) ;
463531
464532const params = new URLSearchParams ( location . search ) ;
465- sqlBox . value = params . get ( 'sql' ) ?? EXAMPLES [ params . get ( 'example' ) ] ?? EXAMPLES . valid ;
466- if ( ! params . get ( 'sql' ) ) showCaption ( params . get ( 'example' ) ) ;
533+ if ( params . get ( 'sql' ) !== null ) sqlBox . value = params . get ( 'sql' ) ;
534+ else {
535+ // A slug, or an example's key as earlier links used; anything else opens the default quietly.
536+ const requested = params . get ( 'example' ) ;
537+ const key = examples . find ( ( example ) => example . slug === requested ) ?. key ?? ( Object . hasOwn ( EXAMPLES , requested ?? '' ) ? requested : 'valid' ) ;
538+ loadExample ( key ) ;
539+ }
467540schemaBox . value = SCHEMA ;
468541rowsBox . value = ROWS ;
469542setStage ( 'parse' , 'loading' , '' , 'loading the engine…' , 'loading' ) ;
0 commit comments