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: ``, 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;