From 09d77da786b245412dfe8806799e31f8f8bc26d6 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Sat, 7 Nov 2026 09:24:57 +0000 Subject: [PATCH] playground: say when the in-browser DuckDB, not the translation, failed The preview build of DuckDB-WASM the playground runs cannot bind or crashes on some translated statements that native DuckDB 2.0 runs, such as a DATEADD over CURRENT_DATE. The Run stage showed those as a red failure, which reads as a wrong translation. When the translated statement itself fails with an error from the engine (a binder, catalog, parser, not-implemented or internal error, or a WebAssembly crash), the Run stage now says it is a limit of the in-browser DuckDB, not of the translation, that SQLQuack translates for native DuckDB 2.0, and shows DuckDB-WASM's error underneath. Errors about values, errors from the fixture rows and errors Snowflake would raise keep their existing messages. --- website/public/playground/playground.js | 19 ++++++++++++++++++- website/public/playground/spans.js | 7 +++++++ website/tests/playground.test.mjs | 11 ++++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/website/public/playground/playground.js b/website/public/playground/playground.js index 1bd592748..a6b9923e2 100644 --- a/website/public/playground/playground.js +++ b/website/public/playground/playground.js @@ -5,7 +5,7 @@ import { DUCKDB_DIR, DUCKDB_WASM_BYTES } from './duckdb-version.js'; import { ENGINE_DIR, ENGINE_WASM_BYTES } from './engine-version.js'; import { CAPTIONS, EXAMPLE_GROUPS, EXAMPLES, ROWS, SCHEMA, stopLabel } from './examples.js'; -import { escape, rendered, resultTable, underlined } from './spans.js'; +import { escape, previewLimit, rendered, resultTable, underlined } from './spans.js'; const here = (path) => new URL(path, import.meta.url).href; const ENGINE_WASM = here(`${ENGINE_DIR}sqlquack_wasm_bg.wasm`); @@ -362,6 +362,8 @@ async function execute() { if (!sql) return; const ticket = ++running; setStage('run', 'running', ''); + // Whether the error came from the translated statement itself, after the fixture loaded. + let ranStatement = false; try { if (!duckdb) await startDuckDB(); const conn = await duckdb.db.connect(); @@ -371,6 +373,7 @@ async function execute() { for (const table of tables.tables) await conn.query(table.replace(/^CREATE TABLE/, 'CREATE OR REPLACE TABLE')); if (rowsBox.value.trim()) await conn.query(rowsBox.value); const started = performance.now(); + ranStatement = true; // Every value as DuckDB's own text, so decimals and timestamps show exactly. const result = await conn.query(`SELECT COLUMNS(*)::VARCHAR FROM (${sql}) AS result`); const elapsed = performance.now() - started; @@ -392,6 +395,20 @@ async function execute() { const failure = JSON.parse((await engine('failure', { message })).result); if (ticket !== running) return; const version = duckdb ? `DuckDB ${duckdb.version}` : ''; + if (!failure.errno && ranStatement && previewLimit(message)) { + setStage( + 'run', + 'refused', + '

A limit of the in-browser DuckDB, not of the translation. ' + + 'This page runs a preview build of DuckDB-WASM, which could not run this translated SQL. SQLQuack translates for ' + + 'native DuckDB 2.0: sqlquack convert binds every translated statement in native DuckDB before ' + + 'returning it, so run it there to see the result.

' + + terminal(`DuckDB-WASM: ${escape(message)}`), + version, + 'not run here', + ); + return; + } if (!failure.errno) { setStage('run', 'fail', terminal(`DuckDB: ${escape(message)}`), version); return; diff --git a/website/public/playground/spans.js b/website/public/playground/spans.js index d0002d0fa..b85cf71bd 100644 --- a/website/public/playground/spans.js +++ b/website/public/playground/spans.js @@ -69,3 +69,10 @@ export function resultTable(names, columns) { const head = names.map((name) => `${escape(name)}`).join(''); return { html: `
${head}${body.join('')}
`, rows }; } + +// Whether a DuckDB-WASM error on a translated statement comes from the in-browser engine rather +// than from the rows: a statement it could not bind or plan, or a crash of the WebAssembly engine. +// The playground runs a preview build of DuckDB-WASM; errors about values (conversion, out of +// range) would be raised by native DuckDB too, so they are not counted. +const PREVIEW_LIMITS = /^(Binder|Catalog|Parser|Not implemented|INTERNAL) Error|memory access out of bounds|unreachable|RuntimeError/; +export const previewLimit = (message) => PREVIEW_LIMITS.test(message); diff --git a/website/tests/playground.test.mjs b/website/tests/playground.test.mjs index c0a11a151..27b0b0a61 100644 --- a/website/tests/playground.test.mjs +++ b/website/tests/playground.test.mjs @@ -7,7 +7,7 @@ import { dirname, join } from 'node:path'; import { test } from 'node:test'; import { initSync, analyze, fixture_tables, run_failure } from '../wasm/pkg/sqlquack_wasm.js'; -import { rendered, resultTable, underlined } from '../public/playground/spans.js'; +import { previewLimit, rendered, resultTable, underlined } from '../public/playground/spans.js'; initSync({ module: readFileSync(new URL('../wasm/pkg/sqlquack_wasm_bg.wasm', import.meta.url)) }); @@ -119,6 +119,15 @@ test('every example chip stops at the stage its label names, the Run stage inclu assert.ok(CAPTIONS.refused); }); +test('a run error from the in-browser DuckDB is told apart from an error about the rows', () => { + // What the preview build of DuckDB-WASM reported for a translated DATEADD that native DuckDB 2.0 runs. + assert.ok(previewLimit("Binder Error: No function matches the given name and argument types '+(DATE, INTERVAL)'.")); + assert.ok(previewLimit('memory access out of bounds')); + // Errors about values would come from native DuckDB too. + assert.ok(!previewLimit("Conversion Error: Could not convert string 'x' to INT32")); + assert.ok(!previewLimit('Invalid Input Error: Attempting to fetch from an unsuccessful query result')); +}); + test('translated SQL is laid out one clause per line unless the compact setting asks for one line', () => { const sql = "SELECT order_id, note, 'a longer literal' AS label FROM orders WHERE order_id > 1"; const translated = (settings) => JSON.parse(analyze(sql, SCHEMA, JSON.stringify(settings))).translate.statements[0].sql;