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.
This is the real component running on this page — not a screenshot. Run a query, open the schema, check the diagram.
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.
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:
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:
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").
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.
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.
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.
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>
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.
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:
| Table | Columns |
|---|---|
customers | id, name, region, signup_date |
products | id, name, list_price |
orders | id, customer_id → customers, order_date, status, total_amount, shipped_at |
order_items | id, 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.
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>
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
Attributes — mode and theme react live;
namespace, dataset, sql are read at connect.
| Attribute | Effect |
|---|---|
mode="card" | editor + Run/Reset only |
theme | light / dark override; else follows the page |
namespace | private journal + imported tables |
dataset | named dataset instead of the built-in demo |
sql | boot-time editor prefill |
test | JSON rows — grades every attempt (pass/miss verdict) |
concept | free grouping tag, carried on journal events |
label | problem title for journals and history |
Methods & events
| Member | What 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 |
loadedProblem | the 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" |
tableCount | current number of tables |
workbench-event | fired for every journaled event |
state-change | fired when table count changes |
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.