sql-workbench GitHub

A SQL practice bench, in one script tag.

sql-workbench is a web component: a full SQLite engine, a query editor, a schema browser, and a run journal compiled into a single JavaScript file. Drop it into any page — a lesson, a docs site, a course — and your users can practice real SQL. Nothing to install, nothing leaves the browser.

Try it right here

This is the real component running on this page — not a screenshot. Run a query, open the schema, check the diagram.

Add it to your page

One script, one element. The file is ~1.5 MB (614 KB gzipped) and makes zero side requests — the SQLite engine, worker and all, is inside it.

<script type="module"
        src="https://cdn.jsdelivr.net/gh/udit-001/sql-workbench@v0.7/dist/sql-workbench.js"></script>

<sql-workbench namespace="my-app" style="display:block;height:560px"></sql-workbench>

Prefer hosting it yourself? Download sql-workbench.js from the releases and serve it from your own origin.

Card mode: a drill, not a workbench

Add mode="card" and the bench collapses to just the editor and Run/Reset — for embedding inside lesson content where the surrounding page provides the context. This one below is the same component, same script:

Graded problems: verify what learners write

Give an embed a test — JSON rows — and it becomes a graded problem: the child text is the prompt, and every attempt gets a verdict. The learner is judged on their result, not their query text — any correct query passes. Row order doesn't matter unless the lesson is about ordering. This one is live — solve it:

Count the orders per customer region.

Omit sql for a recall problem — the learner writes from memory and the same test grades the result. Navigation between problems stays out of the component: hosts swap the slot with setProblem() (agent-driven teaching) or render more embeds. Every graded attempt also journals a step event — title, concept, outcome — so a host can see what was practiced, and who ran it (actor: "learner" | "agent").

Mistakes become explanations

SQL errors arrive in the engine's own words — never paraphrased — with a plain-language explanation layered on top, and did-you-mean suggestions when the loaded schema can offer one. The verbatim message is what you learn from; the hint is what you read first.

Bring your own data

Drag a CSV onto the bench (or use the Import button): pick a table name, confirm the delimiter and header row, and query it like any other table. Imports persist per namespace and show up in the schema panel and the diagram.

It matches your app, not the other way round

The bench follows your page's html[data-theme] and the OS color scheme — try the ◐ button in the header: both benches above and below flip with the page. For a full rebrand, override design tokens on the element; they win even against dark mode:

sql-workbench {
  --wb-bg: #24283b; --wb-surface: #1f2335; --wb-accent: #bb9af7;
  --wb-text: #c0caf5; --wb-heading: #7aa2f7; --wb-border: #414868;
}
sql-workbench::part(run-button) { border-radius: 999px; }

Tokens exist for every surface color, the SQL syntax palette, and fonts (--wb-bg, --wb-surface, --wb-accent, --wb-kw, --wb-str, --wb-num, --wb-mono, …). Styleable parts: editor, run-button, reset-button, results, schema, diagram, history, statusbar.

Each instance keeps its own data

Two benches on one page don't share anything. The namespace attribute gives each one a private journal and private imported tables — one per lesson, per user task, or per tab:

<sql-workbench namespace="lesson-1"></sql-workbench>
<sql-workbench namespace="lesson-2"></sql-workbench>

Start learners mid-thought

The sql attribute pre-fills the editor, so a lesson can pose a half-written query and let the learner finish it. The card demo above uses it. You can also pass ?sql=… in the URL on a standalone page, and dataset loads a named dataset instead of the built-in demo.

Pre-built datasets with fixtures

A fixture is a JSON file that seeds the bench with tables and data. Create one at fixtures/<id>.json, then load it with dataset="<id>".

Schema — the built-in ecommerce fixture:

TableColumns
customersid, name, region, signup_date
productsid, name, list_price
ordersid, customer_id → customers, order_date, status, total_amount, shipped_at
order_itemsid, order_id → orders, product_id → products, quantity, unit_price

6 customers (6 regions), 4 products, 10 orders (shipped/pending/cancelled), 12 line items.

{
  "id": "ecommerce",
  "kind": "sqlite-dataset",
  "version": 1,
  "title": "E-commerce sample",
  "description": "A tiny web shop: customers place orders.",
  "reset": {
    "sql": "CREATE TABLE ... ; INSERT INTO ..."
  }
}

The reset.sql contains the full seed script. See ecommerce.json for the complete example. The id must match the filename stem. Keep seed SQL plain and readable; no base64.

Empty state: start from scratch

Omit dataset and the bench starts empty — no tables, no data. Users see a prompt to create a table or load a sample. Try it below:

Hosts can override the empty state via slot="empty" or react to state-change events. Open standalone — bookmark it for quick SQL practice. Seed the bench with your own tables using a fixture — a JSON file with the CREATE/INSERT script that runs on load and reset.

<sql-workbench namespace="my-app"></sql-workbench>

<script>
  const bench = document.querySelector("sql-workbench");
  bench.addEventListener("state-change", (e) => {
    console.log("tables:", e.detail.tableCount);
  });
</script>

Watch what learners run

Every query, import, reset, and graded attempt is journaled. Get them live as DOM events, read the whole journal on demand, or export it as Markdown:

bench.addEventListener("workbench-event", (e) => {
  // e.detail: { type: "query" | "step" | "dataset-reset" | "csv-import" | "csv-import-removed", sql, ok, rows, ms, error … }
});

const events = await bench.events();      // newest first, same shapes
const md = await bench.exportMarkdown();  // paste-ready practice log

Reference

Attributes — mode and theme react live; namespace, dataset, sql are read at connect.

AttributeEffect
mode="card"editor + Run/Reset only
themelight / dark override; else follows the page
namespaceprivate journal + imported tables
datasetnamed dataset instead of the built-in demo
sqlboot-time editor prefill
testJSON rows — grades every attempt (pass/miss verdict)
conceptfree grouping tag, carried on journal events
labelproblem title for journals and history

Methods & events

MemberWhat it does
run(sql)fills the editor, executes, journals
setProblem(p)swap the problem slot (agent navigation)
runGraded(sql, opts)run + verdict in one call, attributed
loadedProblemthe loaded problem, or null
reset()restores the dataset's seed data
events()journal entries, newest first
exportMarkdown()journal as a Markdown practice log
setTheme(t)"light" or "dark"
tableCountcurrent number of tables
workbench-eventfired for every journaled event
state-changefired when table count changes

Where the data lives

Everything runs in the learner's browser: SQLite in memory inside the tab, the journal and imported CSVs in IndexedDB under your namespace. No server, no telemetry.