$sloprun.dev

SQL Playground

demo sandbox: allow-scripts · csp: default-src 'none'

sandboxed and isolated in your browser · never enter a real password in a demo

▶ 6 ran · ✓ 0 worked
i ran it — no login needed:
share: preview embed ↗
post it anywhere:
email the card:

0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously

SQLite itself — the C library, compiled to WebAssembly — boots in your tab and builds a small hardware shop's books from scratch: orders, customers, products, 64 rows. The page opens already run, on a three-table join ranking customers by revenue, with the row count and the wall-clock time of that run on the readout rail under the editor. The five example buttons are the tour: Monthly running total accumulates SUM(...) OVER (ORDER BY month), Best in each category ranks inside partitions with RANK(), and Nothing ever shipped is a LEFT JOIN … HAVING 0 anti-join that names the three customers who never took delivery of anything. Then write your own — it is real SQLite, so CTEs, subqueries, INSERT and UPDATE all work, the schema panel's row counts follow your edits, Reset data rebuilds the sample from the seed, and none of it leaves the tab.

Embed this demo — put a runnable demo in your blog post

Paste this where you write. It renders a live, runnable demo inline anywhere raw HTML / iframes are allowed — Ghost, WordPress, Notion, Discourse, your own site.

<iframe src="https://sloprun.dev/embed/sql-playground" width="100%" height="640" loading="lazy" allow="" style="border:1px solid #E3E2DC;border-radius:10px" title="SQL Playground — a runnable demo on sloprun.dev"></iframe>
preview ↗

On Medium and dev.to the plain link becomes a rich preview card that links back here — they don't run third-party iframes, so paste the URL there and the reader clicks through to run it. https://sloprun.dev/p/sql-playground

Source

the code you see is the code that just ran raw ↗
Source — the code you see is the code that just ran 1026 lines 49.1 KB index.html
demos/sql-playground/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SQL Playground — a real SQLite database in your browser</title>
<style>
/* ── sloprun design tokens (verbatim from demos/tokens.css) ─────────────── */
/* sloprun design tokens — inline this block into every demo (self-contained rule).
   Identity: instrument-panel. Machine facts in mono; human words in sans.
   Green is EARNED: only for "it ran / it worked" states, never decoration. */
:root {
  --bg: #F6F6F3;          /* neutral paper, not cream */
  --surface: #FFFFFF;
  --ink: #1A1D21;
  --muted: #5A6068;
  --line: #E3E2DC;
  --accent: #C05600;      /* phosphor amber, darkened for light bg */
  --accent-soft: #FFF3E6;
  --run: #1A7F37;         /* earned green */
  --run-soft: #E7F4EA;
  --danger: #C93C37;
  --radius: 10px;
  --font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
    --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
    --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
  }
}
:root[data-theme="dark"] {
  --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
  --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
  --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }
/* ── end verbatim token block ───────────────────────────────────────────── */

/* demo-local derived tokens (defined for every theme state) */
:root {
  color-scheme: light;
  --zebra: rgba(20,22,26,.022);
  --on-accent: #FFFFFF;
  --shadow: 0 1px 2px rgba(20,22,26,.06);
  /* small text sitting ON --accent-soft: plain --accent is only 4.2:1 there */
  --accent-ink: #9A4500;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --zebra: rgba(232,230,225,.03); --on-accent: #14161A;
    --shadow: 0 1px 2px rgba(0,0,0,.35); --accent-ink: #FFB454;
  }
}
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] {
  color-scheme: dark;
  --zebra: rgba(232,230,225,.03); --on-accent: #14161A;
  --shadow: 0 1px 2px rgba(0,0,0,.35); --accent-ink: #FFB454;
}

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body { margin: 0; line-height: 1.5; overflow-x: hidden; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
button { font: inherit; color: inherit; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important; animation-iteration-count: 1 !important;
    transition-duration: .01ms !important; scroll-behavior: auto !important;
  }
}

.wrap { max-width: 1120px; margin: 0 auto; padding: clamp(16px, 3vw, 34px) clamp(14px, 3vw, 28px) 28px; }

/* ── header ─────────────────────────────────────────────────────────────── */
.eyebrow {
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--muted); margin: 0 0 10px;
}
.titlebar { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px 14px; }
h1 {
  font-size: clamp(1.35rem, 4.4vw, 1.85rem); font-weight: 640; letter-spacing: -.021em;
  margin: 0; line-height: 1.15;
}
.chip {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .02em;
  color: var(--muted); border: 1px solid var(--line); background: var(--surface);
  border-radius: 999px; padding: 3px 10px; white-space: nowrap;
}
.lede { margin: 10px 0 0; color: var(--muted); max-width: 62ch; font-size: 15px; }
.lede b { color: var(--ink); font-weight: 600; }

/* ── standalone notice ──────────────────────────────────────────────────── */
.notice {
  margin: 18px 0 0; padding: 12px 14px; border: 1px solid var(--accent);
  border-left-width: 3px; background: var(--accent-soft); border-radius: var(--radius);
  font-size: 14px;
}
.notice[hidden] { display: none; }
.notice h2 {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--accent-ink); margin: 0 0 5px; font-weight: 600;
}
.notice p { margin: 0; color: var(--ink); }
.notice code {
  font-family: var(--font-mono); font-size: 12.5px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 4px; padding: 1px 5px; word-break: break-all;
}

/* ── examples ───────────────────────────────────────────────────────────── */
.examples { margin: 22px 0 0; }
.legend {
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--muted); margin: 0 0 9px;
}
.chips { display: flex; flex-wrap: wrap; gap: 8px; padding: 0; margin: 0; }
.ex {
  display: flex; flex-direction: column; gap: 2px; align-items: flex-start;
  text-align: left; cursor: pointer; padding: 7px 12px 8px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  transition: border-color .12s ease, background-color .12s ease, transform .12s ease;
}
.ex:hover { border-color: var(--accent); transform: translateY(-1px); }
.ex .nm { font-size: 13.5px; font-weight: 550; letter-spacing: -.005em; }
.ex .tag {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .07em;
  text-transform: uppercase; color: var(--muted);
}
.ex[aria-pressed="true"] {
  background: var(--accent-soft); border-color: var(--accent);
}
.ex[aria-pressed="true"] .nm { color: var(--accent-ink); }
.ex[aria-pressed="true"] .tag { color: var(--accent-ink); }

/* ── layout ─────────────────────────────────────────────────────────────── */
.panels { display: grid; gap: 20px; grid-template-columns: minmax(0, 1fr); margin-top: 20px; }
@media (min-width: 900px) { .panels { grid-template-columns: minmax(0, 1fr) 268px; align-items: start; } }
.console { min-width: 0; }
.aside { min-width: 0; display: grid; gap: 14px; }

/* ── editor ─────────────────────────────────────────────────────────────── */
.editor-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 8px; }
.hint { font-family: var(--font-mono); font-size: 11px; color: var(--muted); }
kbd {
  font-family: var(--font-mono); font-size: 10.5px; border: 1px solid var(--line);
  border-bottom-width: 2px; border-radius: 4px; padding: 1px 5px; background: var(--surface);
}
textarea {
  display: block; width: 100%; min-height: 178px; resize: vertical;
  font-family: var(--font-mono); font-size: 13px; line-height: 1.6; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius) var(--radius) 0 0; border-bottom: 0;
  padding: 13px 15px; tab-size: 2;
}
textarea:focus-visible { outline-offset: -2px; border-radius: var(--radius) var(--radius) 0 0; }
textarea[readonly] { color: var(--muted); }

/* ── readout rail (the instrument bezel) ────────────────────────────────── */
.rail {
  --lamp: var(--muted);
  display: flex; flex-wrap: wrap; align-items: center; gap: 5px 14px;
  background: var(--surface); border: 1px solid var(--line);
  border-left: 3px solid var(--lamp); border-radius: 0 0 var(--radius) var(--radius);
  padding: 9px 13px;
}
.rail.is-ok { --lamp: var(--run); }
.rail.is-error { --lamp: var(--danger); }
.lamp {
  width: 8px; height: 8px; border-radius: 50%; background: var(--lamp);
  flex: none; transition: background-color .15s ease, box-shadow .15s ease;
}
.rail.is-ok .lamp { box-shadow: 0 0 0 3px var(--run-soft); }
.state {
  font-family: var(--font-mono); font-size: 11.5px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted);
}
.rail.is-ok .state { color: var(--run); }
.rail.is-error .state { color: var(--danger); }
.readouts { display: flex; flex-wrap: wrap; align-items: center; gap: 5px 14px; margin-left: auto; }
.ro { display: flex; align-items: baseline; gap: 6px; padding-left: 14px; border-left: 1px solid var(--line); }
.ro:first-child { padding-left: 0; border-left: 0; }
.ro .k {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted);
}
.ro .v {
  font-family: var(--font-mono); font-size: 13px; font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.run-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-top: 12px; }
.btn {
  cursor: pointer; border-radius: var(--radius); padding: 9px 16px;
  border: 1px solid var(--line); background: var(--surface);
  font-size: 14px; font-weight: 550; letter-spacing: -.005em;
  transition: border-color .12s ease, background-color .12s ease;
  box-shadow: var(--shadow);
}
.btn:hover:not(:disabled) { border-color: var(--accent); }
.btn-run {
  background: var(--accent); border-color: var(--accent); color: var(--on-accent);
}
.btn-run:hover:not(:disabled) { filter: brightness(1.06); }
.btn:disabled { opacity: .45; cursor: not-allowed; box-shadow: none; }

/* ── error ──────────────────────────────────────────────────────────────── */
.error {
  margin-top: 16px; border: 1px solid var(--danger); border-left-width: 3px;
  border-radius: var(--radius); background: var(--surface); padding: 12px 14px;
}
.error[hidden] { display: none; }
.error h2 {
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .11em;
  text-transform: uppercase; color: var(--danger); margin: 0 0 6px; font-weight: 600;
}
.error pre {
  font-family: var(--font-mono); font-size: 12.5px; color: var(--ink);
  margin: 0 0 7px; white-space: pre-wrap; word-break: break-word;
}
.error p { margin: 0; font-size: 13px; color: var(--muted); }

/* ── results ────────────────────────────────────────────────────────────── */
.results { margin-top: 16px; }
.results.stale .tablewrap, .results.stale .cap { opacity: .5; }
.stale-note {
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted); margin: 0 0 7px;
}
.stale-note[hidden] { display: none; }
.tablewrap {
  overflow: auto; max-height: 62vh;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface);
  -webkit-overflow-scrolling: touch;
}
table { border-collapse: separate; border-spacing: 0; width: 100%; font-family: var(--font-mono); font-size: 12.5px; }
thead th {
  position: sticky; top: 0; z-index: 2; background: var(--surface);
  text-align: left; font-weight: 600; color: var(--muted); white-space: nowrap;
  font-size: 10.5px; letter-spacing: .09em; text-transform: uppercase;
  padding: 10px 13px; box-shadow: inset 0 -1px 0 var(--line);
}
tbody td { padding: 7px 13px; white-space: nowrap; box-shadow: inset 0 -1px 0 var(--line); }
tbody tr:last-child td { box-shadow: none; }
tbody tr:nth-child(even) td { background: var(--zebra); }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
.fill { width: 100%; padding: 0 !important; }
.gut {
  position: sticky; left: 0; text-align: right; color: var(--muted);
  background: var(--surface); width: 1%; padding-right: 12px !important;
  box-shadow: inset -1px 0 0 var(--line), inset 0 -1px 0 var(--line);
}
thead th.gut { z-index: 3; }
tbody tr:nth-child(even) td.gut { background: var(--surface); }
.nul { color: var(--muted); font-style: italic; }
td.norows {
  padding: 22px 13px !important; text-align: center; color: var(--muted);
  font-family: var(--font-mono); font-size: 11.5px; letter-spacing: .06em;
  text-transform: uppercase;
}
.empty {
  padding: 26px 16px; text-align: center; color: var(--muted); font-size: 14px;
}
.empty .big { font-family: var(--font-mono); font-size: 12px; letter-spacing: .08em; text-transform: uppercase; }
.cap {
  margin: 8px 2px 0; font-family: var(--font-mono); font-size: 11px; color: var(--muted);
}

/* ── schema aside ───────────────────────────────────────────────────────── */
.tbl {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); padding: 11px 13px 12px;
}
.tbl.gone { opacity: .55; }
.tbl.gone .n { color: var(--danger); }
.tbl-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
.tbl-head h3 { font-family: var(--font-mono); font-size: 13px; margin: 0; color: var(--accent); font-weight: 600; }
.tbl-head .n { font-family: var(--font-mono); font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
.cols { list-style: none; margin: 0; padding: 0; font-family: var(--font-mono); font-size: 11.5px; }
.cols li { display: flex; justify-content: space-between; gap: 12px; padding: 2.5px 0; }
.cols .ty { color: var(--muted); font-size: 10px; letter-spacing: .06em; text-transform: uppercase; }
.cols .pk { color: var(--accent); }
.joins {
  border: 1px dashed var(--line); border-radius: var(--radius); padding: 10px 13px;
  font-family: var(--font-mono); font-size: 11px; color: var(--muted); line-height: 1.75;
}
.joins .lbl {
  display: block; font-size: 10px; letter-spacing: .11em; text-transform: uppercase;
  margin-bottom: 5px; color: var(--muted);
}
.joins b { color: var(--ink); font-weight: 500; }
.reset-row { display: flex; justify-content: flex-start; }
.btn-sm { padding: 7px 12px; font-size: 12.5px; font-weight: 500; }

footer {
  margin-top: 30px; padding-top: 14px; border-top: 1px solid var(--line);
  font-family: var(--font-mono); font-size: 11px; color: var(--muted);
  display: flex; flex-wrap: wrap; gap: 6px 14px; justify-content: space-between;
}
footer a { color: var(--muted); }
</style>
</head>
<body>
<div class="wrap">

  <header>
    <p class="eyebrow" id="eyebrow">SQLite · WebAssembly · in-memory · 64 rows</p>
    <div class="titlebar">
      <h1>SQL Playground</h1>
      <span class="chip" id="engine-chip">starting engine…</span>
    </div>
    <p class="lede">
      A real SQLite database, built fresh in this tab. Three tables of a small
      hardware shop&rsquo;s sales — <b>orders</b>, <b>customers</b>, <b>products</b>.
      Pick a question below or write your own SQL. Nothing leaves your browser.
    </p>
  </header>

  <div class="notice" id="notice" hidden>
    <h2>Engine not loaded</h2>
    <p>
      This demo runs SQLite through <code>/vendor/sqljs/sql-wasm.js</code>, and that
      file did not load here — it is served by sloprun.dev, so a standalone copy of
      this page will not find it. The example questions below still show
      <b>saved results from a real run</b>, so you can read the SQL and the answers;
      the editor turns on wherever the engine does load.
    </p>
  </div>

  <section class="examples" aria-labelledby="ex-legend">
    <p class="legend" id="ex-legend">Ask the database</p>
    <div class="chips" id="chips" role="group" aria-labelledby="ex-legend"></div>
  </section>

  <div class="panels">
    <main class="console">
      <div class="editor-head">
        <label class="legend" for="editor" style="margin:0">Query</label>
        <span class="hint"><kbd id="kbdhint">Ctrl</kbd> + <kbd>Enter</kbd> to run</span>
      </div>
      <textarea id="editor" spellcheck="false" autocomplete="off" autocapitalize="off"
        autocorrect="off" wrap="off" aria-describedby="rail"></textarea>

      <div class="rail" id="rail" role="status" aria-live="polite">
        <span class="lamp" aria-hidden="true"></span>
        <span class="state" id="state">booting</span>
        <div class="readouts">
          <span class="ro"><span class="k">rows</span><span class="v" id="ro-rows">—</span></span>
          <span class="ro"><span class="k">time</span><span class="v" id="ro-time">—</span></span>
        </div>
      </div>

      <div class="run-row">
        <button class="btn btn-run" id="run" type="button">Run query</button>
        <button class="btn btn-sm" id="reset" type="button">Reset data</button>
      </div>

      <div class="error" id="error" role="alert" hidden>
        <h2>SQLite rejected that</h2>
        <pre id="error-msg"></pre>
        <p id="error-hint">Fix the statement and run it again — Reset data puts the sample back any time.</p>
      </div>

      <section class="results" id="results" aria-labelledby="res-legend">
        <p class="sr-only" id="res-legend">Query results</p>
        <p class="stale-note" id="stale-note" hidden>previous result</p>
        <div class="tablewrap" id="tablewrap" tabindex="0" role="group" aria-labelledby="res-legend"></div>
        <p class="cap" id="cap"></p>
      </section>
    </main>

    <aside class="aside" aria-label="Database schema">
      <div id="schema"></div>
      <div class="joins">
        <span class="lbl">How they join</span>
        <b>orders.customer_id</b> → customers.id<br>
        <b>orders.product_id</b> → products.id
      </div>
      <div class="reset-row"><span class="hint" id="reset-note">Edits you make with INSERT or UPDATE stick until you reset.</span></div>
    </aside>
  </div>

  <footer>
    <span>demo · sloprun.dev</span>
    <span id="foot-engine">sql.js · SQLite compiled to WebAssembly</span>
  </footer>
</div>

<script>
(function () {
  "use strict";

  /* ── the database ────────────────────────────────────────────────────── */
  var SEED_SQL = "CREATE TABLE customers (\n  id         INTEGER PRIMARY KEY,\n  name       TEXT    NOT NULL,\n  country    TEXT    NOT NULL,\n  segment    TEXT    NOT NULL,\n  signup_date TEXT   NOT NULL\n);\n\nCREATE TABLE products (\n  id       INTEGER PRIMARY KEY,\n  name     TEXT    NOT NULL,\n  category TEXT    NOT NULL,\n  price    REAL    NOT NULL,\n  cost     REAL    NOT NULL\n);\n\nCREATE TABLE orders (\n  id          INTEGER PRIMARY KEY,\n  customer_id INTEGER NOT NULL REFERENCES customers(id),\n  product_id  INTEGER NOT NULL REFERENCES products(id),\n  qty         INTEGER NOT NULL,\n  order_date  TEXT    NOT NULL,\n  status      TEXT    NOT NULL\n);\n\nINSERT INTO customers (id, name, country, segment, signup_date) VALUES\n  (1,  'Ada Okonkwo',    'NG', 'pro',        '2023-11-02'),\n  (2,  'Bruno Salgado',  'BR', 'starter',    '2024-01-14'),\n  (3,  'Chen Wei',       'SG', 'enterprise', '2023-08-21'),\n  (4,  'Dalia Haddad',   'JO', 'pro',        '2024-02-09'),\n  (5,  'Eero Lahtinen',  'FI', 'starter',    '2024-03-30'),\n  (6,  'Farida Nasser',  'EG', 'enterprise', '2023-06-17'),\n  (7,  'Grace Mbeki',    'KE', 'pro',        '2024-04-05'),\n  (8,  'Hugo Marchand',  'FR', 'starter',    '2024-05-19'),\n  (9,  'Ines Ferreira',  'PT', 'pro',        '2023-12-28'),\n  (10, 'Jonas Weber',    'DE', 'enterprise', '2024-01-03'),\n  (11, 'Kavya Raman',    'IN', 'starter',    '2024-06-11'),\n  (12, 'Luca Bianchi',   'IT', 'pro',        '2024-02-24'),\n  (13, 'Mei Tanaka',     'JP', 'starter',    '2024-05-02'),\n  (14, 'Noor Rahman',    'BD', 'pro',        '2024-07-08');\n\nINSERT INTO products (id, name, category, price, cost) VALUES\n  (1,  'Thermal Receipt Printer', 'hardware',  189.00,  96.40),\n  (2,  'Barcode Scanner',         'hardware',  129.50,  61.20),\n  (3,  'Cash Drawer',             'hardware',  219.00, 118.75),\n  (4,  'Label Roll (500ct)',      'supplies',   24.00,   9.10),\n  (5,  'Receipt Paper (12pk)',    'supplies',   18.50,   6.40),\n  (6,  'Card Reader Dock',        'hardware',  259.00, 140.00),\n  (7,  'Inventory Sync',          'software',   49.00,   4.50),\n  (8,  'Analytics Add-on',        'software',   99.00,   7.25),\n  (9,  'Priority Support',        'services',  149.00,  62.00),\n  (10, 'On-site Setup',           'services',  399.00, 210.00);\n\nINSERT INTO orders (id, customer_id, product_id, qty, order_date, status) VALUES\n  (1001,  1,  1, 2, '2024-01-08', 'shipped'),\n  (1002,  1,  4, 6, '2024-01-08', 'shipped'),\n  (1003,  3,  6, 4, '2024-01-15', 'shipped'),\n  (1004,  2,  5, 3, '2024-01-22', 'shipped'),\n  (1005,  6, 10, 1, '2024-01-27', 'shipped'),\n  (1006,  3,  8, 5, '2024-02-03', 'shipped'),\n  (1007,  4,  2, 1, '2024-02-11', 'shipped'),\n  (1008,  9,  7, 2, '2024-02-14', 'shipped'),\n  (1009, 10,  3, 3, '2024-02-19', 'shipped'),\n  (1010,  1,  5, 8, '2024-02-25', 'shipped'),\n  (1011,  6,  1, 5, '2024-03-02', 'shipped'),\n  (1012, 12,  4, 4, '2024-03-06', 'refunded'),\n  (1013,  3, 10, 2, '2024-03-12', 'shipped'),\n  (1014,  5,  5, 2, '2024-03-18', 'shipped'),\n  (1015,  9,  8, 1, '2024-03-21', 'shipped'),\n  (1016, 10,  6, 2, '2024-03-29', 'shipped'),\n  (1017,  4,  9, 1, '2024-04-04', 'shipped'),\n  (1018,  1,  7, 3, '2024-04-09', 'shipped'),\n  (1019,  7,  2, 2, '2024-04-15', 'shipped'),\n  (1020,  6,  3, 4, '2024-04-18', 'shipped'),\n  (1021,  2,  4, 5, '2024-04-23', 'cancelled'),\n  (1022, 12,  1, 1, '2024-04-30', 'cancelled'),\n  (1023,  3,  9, 6, '2024-05-06', 'shipped'),\n  (1024, 10,  8, 4, '2024-05-11', 'shipped'),\n  (1025,  8,  5, 2, '2024-05-16', 'shipped'),\n  (1026,  9,  1, 2, '2024-05-21', 'shipped'),\n  (1027,  6,  6, 3, '2024-05-27', 'shipped'),\n  (1028,  7, 10, 1, '2024-06-02', 'shipped'),\n  (1029,  1,  8, 2, '2024-06-07', 'shipped'),\n  (1030,  4,  4, 9, '2024-06-12', 'shipped'),\n  (1031,  3,  1, 6, '2024-06-15', 'shipped'),\n  (1032, 11,  7, 1, '2024-06-20', 'shipped'),\n  (1033, 10, 10, 2, '2024-06-24', 'shipped'),\n  (1034, 12,  2, 3, '2024-06-28', 'refunded'),\n  (1035,  6,  8, 7, '2024-07-03', 'shipped'),\n  (1036,  9,  3, 1, '2024-07-09', 'shipped'),\n  (1037,  5,  4, 3, '2024-07-14', 'shipped'),\n  (1038,  3,  2, 5, '2024-07-19', 'shipped'),\n  (1039,  1,  9, 2, '2024-07-25', 'shipped'),\n  (1040, 10,  1, 4, '2024-07-30', 'shipped');\n";

  var SCHEMA = [
    { name: "customers", n: 14, cols: [
      ["id", "int pk"], ["name", "text"], ["country", "text"],
      ["segment", "text"], ["signup_date", "text"] ] },
    { name: "products", n: 10, cols: [
      ["id", "int pk"], ["name", "text"], ["category", "text"],
      ["price", "real"], ["cost", "real"] ] },
    { name: "orders", n: 40, cols: [
      ["id", "int pk"], ["customer_id", "int fk"], ["product_id", "int fk"],
      ["qty", "int"], ["order_date", "text"], ["status", "text"] ] }
  ];

  var EXAMPLES = [
      {
          "id": "q1",
          "name": "Top customers",
          "tag": "JOIN",
          "sql": "-- Who are our best customers? Join all three tables, sum the money.\nSELECT\n  c.name,\n  c.country,\n  COUNT(*)                          AS orders,\n  ROUND(SUM(o.qty * p.price), 2)    AS revenue\nFROM orders o\nJOIN customers c ON c.id = o.customer_id\nJOIN products  p ON p.id = o.product_id\nWHERE o.status = 'shipped'\nGROUP BY c.id\nORDER BY revenue DESC\nLIMIT 8;"
      },
      {
          "id": "q2",
          "name": "Revenue by category",
          "tag": "GROUP BY",
          "sql": "-- Revenue and margin per category, plus each category's share of the total.\nSELECT\n  p.category,\n  SUM(o.qty)                                     AS units,\n  ROUND(SUM(o.qty * p.price), 2)                 AS revenue,\n  ROUND(SUM(o.qty * (p.price - p.cost)), 2)      AS margin,\n  ROUND(100.0 * SUM(o.qty * p.price)\n        / SUM(SUM(o.qty * p.price)) OVER (), 1)  AS pct_of_total\nFROM orders o\nJOIN products p ON p.id = o.product_id\nWHERE o.status = 'shipped'\nGROUP BY p.category\nORDER BY revenue DESC;"
      },
      {
          "id": "q3",
          "name": "Monthly running total",
          "tag": "WINDOW",
          "sql": "-- Monthly revenue with a running total -- a window function over the grouped rows.\nSELECT\n  strftime('%Y-%m', o.order_date)                AS month,\n  ROUND(SUM(o.qty * p.price), 2)                 AS revenue,\n  ROUND(SUM(SUM(o.qty * p.price)) OVER (\n          ORDER BY strftime('%Y-%m', o.order_date)\n        ), 2)                                    AS running_total\nFROM orders o\nJOIN products p ON p.id = o.product_id\nWHERE o.status = 'shipped'\nGROUP BY month\nORDER BY month;"
      },
      {
          "id": "q4",
          "name": "Best in each category",
          "tag": "RANK()",
          "sql": "-- Rank products inside their own category with RANK() and PARTITION BY.\nWITH sales AS (\n  SELECT p.category, p.name, SUM(o.qty * p.price) AS revenue\n  FROM orders o\n  JOIN products p ON p.id = o.product_id\n  WHERE o.status = 'shipped'\n  GROUP BY p.id\n)\nSELECT\n  category,\n  RANK() OVER (PARTITION BY category ORDER BY revenue DESC) AS rank_in_cat,\n  name,\n  ROUND(revenue, 2) AS revenue\nFROM sales\nORDER BY category, rank_in_cat;"
      },
      {
          "id": "q5",
          "name": "Nothing ever shipped",
          "tag": "LEFT JOIN",
          "sql": "-- Anti-join: signed up, but nothing ever shipped. LEFT JOIN + HAVING 0.\nSELECT\n  c.name,\n  c.country,\n  c.segment,\n  c.signup_date,\n  COUNT(o.id) AS orders_ever\nFROM customers c\nLEFT JOIN orders o\n  ON o.customer_id = c.id AND o.status = 'shipped'\nGROUP BY c.id\nHAVING orders_ever = 0\nORDER BY c.signup_date;"
      }
  ];
  var CACHE = {"q1":{"columns":["name","country","orders","revenue"],"values":[["Chen Wei","SG",6,5004.5],["Farida Nasser","EG",5,3690.0],["Jonas Weber","DE",5,3125.0],["Ada Okonkwo","NG",6,1313.0],["Ines Ferreira","PT",4,794.0],["Grace Mbeki","KE",2,658.0],["Dalia Haddad","JO",3,494.5],["Eero Lahtinen","FI",2,109.0]]},"q2":{"columns":["category","units","revenue","margin","pct_of_total"],"values":[["hardware",44,8710.0,4178.8,56.8],["services",15,3735.0,1917.0,24.4],["software",25,2175.0,2010.25,14.2],["supplies",33,709.5,449.7,4.6]]},"q3":{"columns":["month","revenue","running_total"],"values":[["2024-01",2012.5,2012.5],["2024-02",1527.5,3540.0],["2024-03",2397.0,5937.0],["2024-04",1431.0,7368.0],["2024-05",2482.0,9850.0],["2024-06",2794.0,12644.0],["2024-07",2685.5,15329.5]]},"q4":{"columns":["category","rank_in_cat","name","revenue"],"values":[["hardware",1,"Thermal Receipt Printer",3591.0],["hardware",2,"Card Reader Dock",2331.0],["hardware",3,"Cash Drawer",1752.0],["hardware",4,"Barcode Scanner",1036.0],["services",1,"On-site Setup",2394.0],["services",2,"Priority Support",1341.0],["software",1,"Analytics Add-on",1881.0],["software",2,"Inventory Sync",294.0],["supplies",1,"Label Roll (500ct)",432.0],["supplies",2,"Receipt Paper (12pk)",277.5]]},"q5":{"columns":["name","country","segment","signup_date","orders_ever"],"values":[["Luca Bianchi","IT","pro","2024-02-24",0],["Mei Tanaka","JP","starter","2024-05-02",0],["Noor Rahman","BD","pro","2024-07-08",0]]}};

  /* ── elements ────────────────────────────────────────────────────────── */
  var $ = function (id) { return document.getElementById(id); };
  var editor = $("editor"), chipsEl = $("chips"), rail = $("rail"),
      stateEl = $("state"), roRows = $("ro-rows"), roTime = $("ro-time"),
      runBtn = $("run"), resetBtn = $("reset"), errBox = $("error"),
      errMsg = $("error-msg"), errHint = $("error-hint"), results = $("results"),
      staleNote = $("stale-note"), tablewrap = $("tablewrap"), cap = $("cap"),
      notice = $("notice"), engineChip = $("engine-chip"), schemaEl = $("schema");

  var db = null;           // sql.js Database, or null when the engine is absent
  var booting = true;      // true from first paint until the engine resolves
  var activeId = null;
  var totalRows = 0;
  var lastMutatingSql = null;  // so Reset never silently replays an INSERT
  var hasRendered = false; // have we ever painted a result table?

  /* ── schema panel ────────────────────────────────────────────────────── */
  function renderSchema() {
    schemaEl.innerHTML = "";
    SCHEMA.forEach(function (t) {
      var card = document.createElement("div");
      card.className = "tbl";
      var head = document.createElement("div");
      head.className = "tbl-head";
      var h = document.createElement("h3");
      h.textContent = t.name;
      var n = document.createElement("span");
      n.className = "n";
      // t.n is null once a table stops answering COUNT(*) — say so instead of
      // leaving the last good number sitting there as if it were still true.
      if (typeof t.n === "number") { n.textContent = t.n + " rows"; }
      else { n.textContent = "no table"; card.className = "tbl gone"; }
      head.appendChild(h); head.appendChild(n);
      var ul = document.createElement("ul");
      ul.className = "cols";
      t.cols.forEach(function (c) {
        var li = document.createElement("li");
        var a = document.createElement("span");
        a.textContent = c[0];
        if (c[1].indexOf("pk") > -1 || c[1].indexOf("fk") > -1) a.className = "pk";
        var b = document.createElement("span");
        b.className = "ty";
        b.textContent = c[1];
        li.appendChild(a); li.appendChild(b);
        ul.appendChild(li);
      });
      card.appendChild(head); card.appendChild(ul);
      schemaEl.appendChild(card);
    });
  }

  // SQLite bumps this cookie on every DDL change (CREATE / DROP / ALTER), which
  // is the only cheap signal that a statement touched the shape of the database.
  function schemaVersion() {
    if (!db) return null;
    try {
      var r = db.exec("PRAGMA schema_version");
      return (r.length && r[0].values.length) ? r[0].values[0][0] : null;
    } catch (e) { return null; }
  }

  function refreshCounts() {
    if (!db) return;
    SCHEMA.forEach(function (t) {
      try {
        var r = db.exec("SELECT COUNT(*) FROM " + t.name);
        t.n = (r.length && r[0].values.length) ? r[0].values[0][0] : null;
      } catch (e) { t.n = null; /* dropped or renamed by the user */ }
    });
    renderSchema();
    totalRows = SCHEMA.reduce(function (s, t) { return s + (typeof t.n === "number" ? t.n : 0); }, 0);
    $("eyebrow").textContent = "SQLite · WebAssembly · in-memory · " + totalRows + " rows";
  }

  /* ── example chips ───────────────────────────────────────────────────── */
  function renderChips() {
    EXAMPLES.forEach(function (ex) {
      var b = document.createElement("button");
      b.type = "button";
      b.className = "ex";
      b.setAttribute("aria-pressed", "false");
      b.dataset.id = ex.id;
      var nm = document.createElement("span");
      nm.className = "nm";
      nm.textContent = ex.name;
      var tg = document.createElement("span");
      tg.className = "tag";
      tg.textContent = ex.tag;
      b.appendChild(nm); b.appendChild(tg);
      b.addEventListener("click", function () { pick(ex.id); });
      chipsEl.appendChild(b);
    });
  }

  function markActive(id) {
    activeId = id;
    Array.prototype.forEach.call(chipsEl.children, function (b) {
      b.setAttribute("aria-pressed", String(b.dataset.id === id));
    });
  }

  // Grow the editor to the query we just put in it, so a picked example is
  // readable end-to-end without scrolling. Only ever on a programmatic set —
  // typing never yanks the box out from under you.
  function fitEditor() {
    editor.style.height = "auto";
    var h = Math.max(178, Math.min(editor.scrollHeight + 2, 300));
    editor.style.height = h + "px";
  }

  function pick(id) {
    var ex = EXAMPLES.filter(function (e) { return e.id === id; })[0];
    if (!ex) return;
    markActive(id);
    editor.value = ex.sql;
    fitEditor();
    if (db) run(); else showCached(id);
  }

  /* ── readout rail ────────────────────────────────────────────────────── */
  function setRail(state, label, rows, time) {
    rail.classList.remove("is-ok", "is-error");
    if (state) rail.classList.add(state);
    stateEl.textContent = label;
    roRows.textContent = rows;
    roTime.textContent = time;
  }

  function fmtMs(ms) {
    // performance.now() is clamped to ~0.1 ms in a normal (non cross-origin
    // isolated) page, so a reading of 0 means "faster than the clock can see" —
    // claiming "< 0.01 ms" would be a precision this timer does not have.
    if (ms <= 0) return "< 0.1 ms";
    if (ms < 10) return ms.toFixed(2) + " ms";
    if (ms < 1000) return ms.toFixed(1) + " ms";
    return (ms / 1000).toFixed(2) + " s";
  }

  function plural(n, word) { return n + " " + word + (n === 1 ? "" : "s"); }

  /* ── rendering a result set ──────────────────────────────────────────── */
  var ROW_CAP = 500;

  function isNum(v) { return typeof v === "number"; }

  // A zero-content trailing column that soaks up leftover width, so the real
  // columns stay packed together and stay scannable across a wide viewport.
  function mkFill(tag) {
    var el = document.createElement(tag);
    el.className = "fill";
    el.setAttribute("aria-hidden", "true");
    return el;
  }

  function renderTable(res) {
    var cols = res.columns, vals = res.values;
    var numeric = cols.map(function (_, i) {
      var seen = false;
      for (var r = 0; r < vals.length; r++) {
        var v = vals[r][i];
        if (v === null || v === undefined) continue;
        if (!isNum(v)) return false;
        seen = true;
      }
      return seen;
    });

    var shown = Math.min(vals.length, ROW_CAP);
    var table = document.createElement("table");

    var cp = document.createElement("caption");
    cp.className = "sr-only";
    cp.textContent = plural(vals.length, "row") + ", " + plural(cols.length, "column");
    table.appendChild(cp);

    var thead = document.createElement("thead");
    var hr = document.createElement("tr");
    var gh = document.createElement("th");
    gh.className = "gut";
    gh.scope = "col";
    gh.textContent = "#";
    hr.appendChild(gh);
    cols.forEach(function (c, i) {
      var th = document.createElement("th");
      th.scope = "col";
      if (numeric[i]) th.className = "num";
      th.textContent = c;
      hr.appendChild(th);
    });
    hr.appendChild(mkFill("th"));
    thead.appendChild(hr);
    table.appendChild(thead);

    var tb = document.createElement("tbody");
    for (var r = 0; r < shown; r++) {
      var tr = document.createElement("tr");
      var g = document.createElement("td");
      g.className = "gut";
      g.textContent = String(r + 1);
      tr.appendChild(g);
      for (var i = 0; i < cols.length; i++) {
        var td = document.createElement("td");
        var v = vals[r][i];
        if (numeric[i]) td.className = "num";
        if (v === null || v === undefined) {
          var s = document.createElement("span");
          s.className = "nul";
          s.textContent = "NULL";
          td.appendChild(s);
        } else if (v && v.byteLength !== undefined) {
          td.textContent = "⟨blob " + v.byteLength + " B⟩";
        } else {
          td.textContent = String(v);
        }
        tr.appendChild(td);
      }
      tr.appendChild(mkFill("td"));
      tb.appendChild(tr);
    }
    // A result set with real columns and no rows is an answer, not a failure —
    // keep the header so you can see what the query would have returned.
    if (!shown) {
      var er = document.createElement("tr");
      var ec = document.createElement("td");
      ec.className = "norows";
      ec.colSpan = cols.length + 2;
      ec.textContent = "0 rows — the query ran, nothing matched";
      er.appendChild(ec);
      tb.appendChild(er);
    }
    table.appendChild(tb);

    tablewrap.innerHTML = "";
    tablewrap.appendChild(table);
    cap.textContent = plural(cols.length, "column") +
      (vals.length > shown ? " · showing first " + ROW_CAP + " of " + vals.length + " rows" : "");
    hasRendered = true;
  }

  function renderEmpty(title, sub) {
    tablewrap.innerHTML = "";
    var d = document.createElement("div");
    d.className = "empty";
    var b = document.createElement("p");
    b.className = "big";
    b.style.margin = "0 0 6px";
    b.textContent = title;
    var p = document.createElement("p");
    p.style.margin = "0";
    p.textContent = sub;
    d.appendChild(b); d.appendChild(p);
    tablewrap.appendChild(d);
    cap.textContent = "";
    hasRendered = true;
  }

  function clearError() {
    errBox.hidden = true;
    results.classList.remove("stale");
    staleNote.hidden = true;
  }

  function showError(msg) {
    // Unhide first, then write: role="alert" announces the text that lands in a
    // region that is already visible, not text that was there before it appeared.
    errBox.hidden = false;
    errMsg.textContent = msg;
    errHint.textContent = hasRendered
      ? "Fix the statement and run it again \u2014 Reset data puts the sample back any time."
      : "Check the table and column names against the schema, then run it again.";
    if (hasRendered) {
      results.classList.add("stale");
      staleNote.hidden = false;
    }
    setRail("is-error", "error", "—", "—");
  }

  /* ── running ─────────────────────────────────────────────────────────── */

  // Column names of the last statement that has any, without running anything:
  // iterateStatements compiles one statement at a time and we never step it.
  function columnsFor(sql) {
    if (!db || typeof db.iterateStatements !== "function") return null;
    var names = null;
    try {
      var it = db.iterateStatements(sql), step;
      while (!(step = it.next()).done) {
        var cn = step.value.getColumnNames();
        if (cn && cn.length) names = cn;
      }
    } catch (e) { return null; }
    return names && names.length ? names : null;
  }

  function run() {
    if (!db) {
      showError(booting
        ? "The SQLite engine is still starting \u2014 give it a moment and run it again."
        : "The SQLite engine isn't loaded, so there's nothing to run against.");
      return;
    }
    var sql = editor.value;
    if (!sql.trim()) {
      showError("No statement to run.");
      return;
    }
    clearError();

    // SQLite's change counter is not reset by SELECT, so zero it against a
    // scratch temp table first — otherwise a SELECT inherits the last INSERT's count.
    try {
      db.run("CREATE TEMP TABLE IF NOT EXISTS _sloprun_probe (x); DELETE FROM _sloprun_probe;");
    } catch (e) { /* worst case the count below is stale; not fatal */ }

    var rowsBefore = totalRows;
    var svBefore = schemaVersion();

    var res, t0, t1;
    try {
      t0 = performance.now();
      res = db.exec(sql);
      t1 = performance.now();
    } catch (e) {
      showError(e && e.message ? e.message : String(e));
      noteIfMutating(sql, 0, svBefore, rowsBefore);
      return;
    }

    var ms = t1 - t0;
    var last = null;
    for (var i = res.length - 1; i >= 0; i--) {
      if (res[i] && res[i].columns && res[i].columns.length) { last = res[i]; break; }
    }

    var changed = 0;
    try { changed = db.getRowsModified(); } catch (e) { changed = 0; }
    noteIfMutating(sql, changed, svBefore, rowsBefore);

    // exec() only hands back a result set when at least one row came out, so a
    // SELECT that matched nothing looks identical to an INSERT. Re-compile (no
    // stepping, no side effects) just to recover the column names.
    var emptyCols = last ? null : columnsFor(sql);

    if (last) {
      renderTable(last);
      setRail("is-ok", "ok", String(last.values.length), fmtMs(ms));
    } else if (emptyCols) {
      renderTable({ columns: emptyCols, values: [] });
      if (changed > 0) cap.textContent += " · " + plural(changed, "row") + " changed";
      setRail("is-ok", "ok", "0", fmtMs(ms));
    } else {
      renderEmpty("no rows returned",
        changed > 0
          ? "That statement changed " + plural(changed, "row") + ". Query the table to see it."
          : "The statement ran without error \u2014 either nothing matched, or it isn't the kind of statement that returns rows.");
      setRail("is-ok", "ok", changed > 0 ? String(changed) + " changed" : "0", fmtMs(ms));
    }
  }

  // Did that statement change the database? Reset needs the answer, and the row
  // counter alone gets it wrong: DROP / CREATE / ALTER move no rows at all, and
  // a bare DELETE FROM t can report none either. Cross-check the schema cookie
  // and the table counts, and refresh the schema panel while we are here.
  function noteIfMutating(sql, changed, svBefore, rowsBefore) {
    var svAfter = schemaVersion();
    refreshCounts();
    var mutated = changed > 0
      || (svBefore !== null && svAfter !== null && svBefore !== svAfter)
      || totalRows !== rowsBefore;
    lastMutatingSql = mutated ? sql.trim() : null;
  }

  /* ── cached (engine-absent) mode ─────────────────────────────────────── */
  function showCached(id) {
    clearError();
    var c = CACHE[id];
    if (!c) { renderEmpty("no saved result", "Load the page on sloprun.dev to run this live."); return; }
    renderTable(c);
    cap.textContent += " · saved result";
    setRail(null, "cached", String(c.values.length), "not run");
  }

  function enterOfflineMode() {
    booting = false;
    notice.hidden = false;
    engineChip.textContent = "engine offline";
    $("foot-engine").textContent = "sql.js unavailable at /vendor/sqljs/";

    // The editor was writable while the engine was starting, so it may hold a
    // query we have no saved result for. Put back the example the saved result
    // actually belongs to rather than freezing a mismatched pair on screen.
    var id = activeId || EXAMPLES[0].id;
    var ex = EXAMPLES.filter(function (e) { return e.id === id; })[0];
    if (ex && editor.value !== ex.sql) { editor.value = ex.sql; fitEditor(); }
    markActive(id);

    editor.readOnly = true;
    editor.setAttribute("aria-readonly", "true");
    runBtn.disabled = true;
    runBtn.title = "The SQLite engine isn't loaded, so there's nothing to run.";
    resetBtn.disabled = true;
    resetBtn.title = "The SQLite engine isn't loaded.";
    $("reset-note").textContent = "Saved results only — the editor is read-only.";
    document.querySelector(".editor-head .hint").hidden = true;
    showCached(id);
  }

  /* ── engine boot ─────────────────────────────────────────────────────── */
  function loadScript(src) {
    return new Promise(function (resolve, reject) {
      var el = document.createElement("script");
      el.src = src;
      el.async = true;
      el.onload = function () { resolve(); };
      el.onerror = function () { reject(new Error("could not load " + src)); };
      document.head.appendChild(el);
    });
  }

  // Fetch the wasm ourselves and hand it to sql.js as `wasmBinary`.
  // Left to itself sql.js fetches sql-wasm.wasm on a promise chain with no
  // rejection handler, so a request the browser refuses — file missing, an
  // offline copy of this page, a sandboxed frame the server has not opened up —
  // never settles: no error, no fallback, just "booting" forever. Owning the
  // request means every failure is a rejection we can actually act on.
  function loadEngine() {
    return loadScript("/vendor/sqljs/sql-wasm.js")
      .then(function () {
        if (typeof window.initSqlJs !== "function") {
          throw new Error("sql-wasm.js loaded but defined no initSqlJs");
        }
        if (typeof fetch !== "function") return null;   // ancient browser: let sql.js try
        return fetch("/vendor/sqljs/sql-wasm.wasm").then(function (r) {
          if (!r.ok) throw new Error("sql-wasm.wasm \u2192 HTTP " + r.status);
          return r.arrayBuffer();
        });
      })
      .then(function (buf) {
        var cfg = { locateFile: function (f) { return "/vendor/sqljs/" + f; } };
        if (buf) cfg.wasmBinary = buf;
        return window.initSqlJs(cfg);
      })
      .then(function (SQL) { return SQL || null; }, function () { return null; });
  }

  // Last-resort net, in case something above hangs rather than fails.
  function withTimeout(promise, ms) {
    return new Promise(function (resolve) {
      var t = setTimeout(function () { resolve(null); }, ms);
      var settle = function (v) { clearTimeout(t); resolve(v); };
      promise.then(settle, function () { settle(null); });
    });
  }

  function build(SQL) {
    if (db) { try { db.close(); } catch (e) {} }
    db = null;                       // never leave a half-built handle in place
    var fresh = new SQL.Database();
    fresh.run(SEED_SQL);
    fresh.run("CREATE TEMP TABLE _sloprun_probe (x)");
    db = fresh;
    lastMutatingSql = null;
    return db;
  }

  var SQLmod = null;

  function boot() {
    renderSchema();
    renderChips();
    markActive(EXAMPLES[0].id);
    editor.value = EXAMPLES[0].sql;
    fitEditor();
    setRail(null, "booting", "\u2014", "\u2014");

    // Nothing can run until the wasm engine resolves, so the controls are
    // visibly off (with a reason on hover) rather than silently dead.
    runBtn.disabled = true;
    resetBtn.disabled = true;
    runBtn.title = "Starting the SQLite engine\u2026";
    resetBtn.title = "Starting the SQLite engine\u2026";

    // Open pre-loaded. The saved result for the first example is painted before
    // the wasm engine has even been fetched, so the first frame already shows
    // the demo doing its thing; the live run replaces it a moment later.
    var preview = CACHE[EXAMPLES[0].id];
    if (preview) {
      renderTable(preview);
      cap.textContent += " · saved preview, engine starting";
      setRail(null, "booting", String(preview.values.length), "—");
      hasRendered = false;   // nothing has actually run in this tab yet
    }

    var isMac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
    if (isMac) { $("kbdhint").textContent = "⌘"; }

    withTimeout(loadEngine(), 8000).then(function (SQL) {
      if (!SQL) { enterOfflineMode(); return; }
      SQLmod = SQL;
      try {
        build(SQL);
      } catch (e) {
        enterOfflineMode();
        return;
      }
      var ver = "";
      try { ver = db.exec("SELECT sqlite_version()")[0].values[0][0]; } catch (e) {}
      engineChip.textContent = ver ? "sql.js · SQLite " + ver : "sql.js · ready";
      booting = false;
      runBtn.disabled = false;
      resetBtn.disabled = false;
      runBtn.removeAttribute("title");
      resetBtn.removeAttribute("title");
      refreshCounts();
      run();
    });
  }

  /* ── wiring ──────────────────────────────────────────────────────────── */
  runBtn.addEventListener("click", function () { run(); });

  resetBtn.addEventListener("click", function () {
    if (!SQLmod) return;
    var was = lastMutatingSql;
    try {
      build(SQLmod);
    } catch (e) {
      showError("Could not rebuild the database: " + (e && e.message ? e.message : e));
      return;
    }
    refreshCounts();
    clearError();
    if (was && editor.value.trim() === was) {
      // The editor still holds the statement that changed the data. Re-running it
      // would undo the reset, so show the restored state instead.
      renderEmpty("data restored",
        "The sample data is back to " + totalRows + " rows. Run a query to see it.");
      setRail(null, "reset", String(totalRows), "not run");
    } else {
      run();
    }
  });

  editor.addEventListener("input", function () {
    if (!activeId) return;
    var ex = EXAMPLES.filter(function (e) { return e.id === activeId; })[0];
    if (ex && editor.value !== ex.sql) markActive(null);
  });

  editor.addEventListener("keydown", function (e) {
    if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
      e.preventDefault();
      if (!runBtn.disabled) run();
    }
  });

  /* platform theme control */
  window.addEventListener("message", function (e) {
    var d = e.data;
    if (!d || d.type !== "sloprun:theme") return;
    if (d.theme === "light" || d.theme === "dark") {
      document.documentElement.setAttribute("data-theme", d.theme);
    } else {
      document.documentElement.removeAttribute("data-theme");
    }
  });

  try {
    if (window.parent !== window) window.parent.postMessage({ type: "sloprun:ready" }, "*");
  } catch (e) { /* cross-origin parent, or no parent — prefers-color-scheme covers us */ }

  boot();
})();
</script>
</body>
</html>

source-visible by construction · nothing is published here without its code

Comments

0 total

No comments yet. If you ran it, say what happened.

Report this post

Goes straight to the moderation queue. Enough independent reports and the post is suspended automatically until a human looks.

what is wrong