Skip to content

Commit eaf17f6

Browse files
committed
Merge main into DATEDIFF over TIMESTAMP_LTZ
2 parents ad99a0a + 0fcc4ce commit eaf17f6

8 files changed

Lines changed: 334 additions & 46 deletions

File tree

‎website/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"dev": "node scripts/prepare_playground.mjs && astro dev",
1010
"build": "node scripts/prepare_playground.mjs && astro build",
1111
"preview": "astro preview",
12-
"test:engine": "node --test tests/playground.test.mjs",
12+
"test:engine": "node scripts/prepare_playground.mjs && node --test tests/playground.test.mjs",
1313
"check:figures": "node scripts/check_figures.mjs"
1414
},
1515
"dependencies": {

‎website/public/playground/examples.js‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,42 @@ FROM orders`,
8686
FROM orders`,
8787
};
8888

89+
// The example chips above the editor, grouped by what SQLQuack does with them. `slug` is the
90+
// example's `?example=` link and `stops` the stage where its run stops (null when all four pass);
91+
// tests/playground.test.mjs checks each `stops` against the engine and DuckDB-WASM.
92+
export const EXAMPLE_GROUPS = [
93+
{
94+
title: 'Runs it',
95+
tagline: 'verified results',
96+
examples: [
97+
{ key: 'valid', slug: 'converts-and-runs', name: 'Converts and runs', stops: null },
98+
{ key: 'decimal', slug: 'decimal-division', name: 'Decimal division', stops: null },
99+
],
100+
},
101+
{
102+
title: 'Catches the mistake',
103+
tagline: 'a clear no',
104+
examples: [
105+
{ key: 'syntax', slug: 'syntax-error', name: 'Syntax error', stops: 'parse' },
106+
{ key: 'rejected', slug: 'snowflake-rejects-it', name: 'Snowflake rejects it', stops: 'checks' },
107+
{ key: 'unknown', slug: 'unknown-column', name: 'Unknown column', stops: 'checks' },
108+
],
109+
},
110+
{
111+
title: 'Says no, clearly',
112+
tagline: 'a clear no',
113+
examples: [{ key: 'refused', slug: 'translation-refused', name: 'Translation refused', stops: 'translate' }],
114+
},
115+
];
116+
117+
export const STOP_LABELS = {
118+
parse: 'stops at Parse',
119+
checks: 'stops at Check',
120+
translate: 'stops at Translate',
121+
run: 'stops at Run',
122+
};
123+
export const stopLabel = (stops) => (stops ? STOP_LABELS[stops] : 'all four pass');
124+
89125
// Shown under the editor while an example is loaded and unedited.
90126
export const CAPTIONS = {
91127
refused:

‎website/public/playground/index.html‎

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,20 +66,10 @@ <h1>Playground</h1>
6666
your browser. Each stage passes or fails on its own, so you can see which step stopped and where.
6767
</p>
6868
</div>
69+
<div class="wrap pg-examples" id="pg-examples" role="group" aria-label="Examples"></div>
6970
<div class="wrap playground">
7071
<div class="pg-inputs">
71-
<label class="pg-label" for="pg-sql"
72-
><span>Snowflake SQL</span>
73-
<select id="pg-example" aria-label="Examples">
74-
<option value="">Examples…</option>
75-
<option value="valid">Converts and runs</option>
76-
<option value="decimal">Decimal division</option>
77-
<option value="syntax">Syntax error</option>
78-
<option value="rejected">Snowflake rejects it</option>
79-
<option value="unknown">Unknown column</option>
80-
<option value="refused">Translation refused</option>
81-
</select></label
82-
>
72+
<label class="pg-label" for="pg-sql"><span>Snowflake SQL</span></label>
8373
<div class="pg-editor">
8474
<div class="pg-marks" id="pg-marks" aria-hidden="true"></div>
8575
<textarea id="pg-sql" spellcheck="false" autocomplete="off" autocapitalize="off"></textarea>

‎website/public/playground/playground.js‎

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
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';
66
import { 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

1010
const here = (path) => new URL(path, import.meta.url).href;
1111
const 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(/^CREATE TABLE/, '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
});
427454
sqlBox.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
});
431465
schemaBox.addEventListener('input', schedule);
@@ -436,11 +470,45 @@ $('#pg-load-policy').addEventListener('click', async () => {
436470
});
437471
rowsBox.addEventListener('input', () => latest && duckdb && schedule());
438472
sqlBox.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) => {
462530
sqlBox.addEventListener('mouseleave', () => (tip.hidden = true));
463531

464532
const 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+
}
467540
schemaBox.value = SCHEMA;
468541
rowsBox.value = ROWS;
469542
setStage('parse', 'loading', '', 'loading the engine…', 'loading');

‎website/public/playground/spans.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,10 @@ export function resultTable(names, columns) {
6969
const head = names.map((name) => `<th>${escape(name)}</th>`).join('');
7070
return { html: `<div class="pg-rows"><table><thead><tr>${head}</tr></thead><tbody>${body.join('')}</tbody></table></div>`, rows };
7171
}
72+
73+
// Whether a DuckDB-WASM error on a translated statement comes from the in-browser engine rather
74+
// than from the rows: a statement it could not bind or plan, or a crash of the WebAssembly engine.
75+
// The playground runs a preview build of DuckDB-WASM; errors about values (conversion, out of
76+
// range) would be raised by native DuckDB too, so they are not counted.
77+
const PREVIEW_LIMITS = /^(Binder|Catalog|Parser|Not implemented|INTERNAL) Error|memory access out of bounds|unreachable|RuntimeError/;
78+
export const previewLimit = (message) => PREVIEW_LIMITS.test(message);

‎website/public/styles.css‎

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,13 +1135,92 @@ footer a {
11351135
font-weight: 500;
11361136
}
11371137

1138-
.pg-label select {
1139-
padding: 4px 8px;
1138+
/* Example chips above the editor, in groups by what SQLQuack does with them. */
1139+
.pg-examples {
1140+
display: flex;
1141+
flex-wrap: wrap;
1142+
gap: 16px 32px;
1143+
padding-top: 28px;
1144+
}
1145+
1146+
.pg-group-head {
1147+
margin: 0 0 8px;
1148+
color: var(--muted-foreground);
1149+
font-size: 13px;
1150+
}
1151+
1152+
.pg-group-head strong {
1153+
color: var(--foreground);
1154+
font-weight: 600;
1155+
}
1156+
1157+
.pg-group-head span::before {
1158+
content: '· ';
1159+
}
1160+
1161+
.pg-chips {
1162+
display: flex;
1163+
flex-wrap: wrap;
1164+
gap: 8px;
1165+
}
1166+
1167+
.pg-chip {
1168+
display: inline-flex;
1169+
flex-direction: column;
1170+
align-items: flex-start;
1171+
gap: 2px;
1172+
padding: 7px 12px;
11401173
border: 1px solid var(--inner-line);
11411174
border-radius: var(--radius);
11421175
background: var(--panel);
11431176
color: var(--foreground);
11441177
font: inherit;
1178+
text-align: left;
1179+
cursor: pointer;
1180+
}
1181+
1182+
.pg-chip:hover {
1183+
border-color: var(--faint);
1184+
}
1185+
1186+
.pg-chip:focus-visible {
1187+
outline: 2px solid var(--primary);
1188+
outline-offset: 2px;
1189+
}
1190+
1191+
.pg-chip[aria-pressed='true'] {
1192+
border-color: var(--primary);
1193+
background: var(--accent-soft);
1194+
box-shadow: inset 0 0 0 1px var(--primary);
1195+
}
1196+
1197+
.pg-chip-name {
1198+
font-size: 14px;
1199+
font-weight: 550;
1200+
}
1201+
1202+
.pg-chip-stop {
1203+
display: inline-flex;
1204+
align-items: center;
1205+
gap: 6px;
1206+
color: var(--muted-foreground);
1207+
font-size: 12px;
1208+
}
1209+
1210+
.pg-chip-stop::before {
1211+
content: '';
1212+
width: 7px;
1213+
height: 7px;
1214+
border-radius: 50%;
1215+
background: var(--error);
1216+
}
1217+
1218+
.pg-chip[data-stops='none'] .pg-chip-stop::before {
1219+
background: var(--success);
1220+
}
1221+
1222+
.pg-chip[data-stops='translate'] .pg-chip-stop::before {
1223+
background: var(--warning);
11451224
}
11461225

11471226
.pg-editor {
@@ -1477,3 +1556,9 @@ footer a {
14771556
grid-template-columns: minmax(0, 1fr);
14781557
}
14791558
}
1559+
1560+
@media (max-width: 620px) {
1561+
.pg-examples {
1562+
flex-direction: column;
1563+
}
1564+
}

0 commit comments

Comments
 (0)