SQL Workbench

help · workflow & architecture
Open workbench →

01 What it is

One HTML file that turns five HR CSVs into a queryable SQLite database inside your browser, then lets you query it two ways: in plain English (translated to SQL by a local model) or by writing SQL yourself.

The whole idea
There is no backend. The database engine, the data, the UI, and the SQL all live in the page. The only outside calls are the sql.js library (CDN) and — for the English mode only — your local Ollama server.

02 Architecture

Two independent paths share one SQLite engine. Direct SQL reaches the engine straight; Ask goes through the model first.

Inside sql-workbench.html (browser)
UI layer
tabs · chat · editor · result grid
→
sql.js engine
SQLite compiled to WASM · in-memory
↑
embedded data
DDL + 5 CSVs as strings
NL → SQL
prompt · extraction · read-only guard
→
UI layer
shows generated SQL, runs it
CDN · jsdelivr
sql-wasm.js + .wasm
→
page loads engine
Ollama :11434
/v1/chat/completions · deepseek
⇄Ask mode only
returns one SELECT

Layers, bottom-up

  1. Data layer — the 5 CSVs + the CREATE TABLE DDL, embedded as JS strings. No file reads at runtime.
  2. Engine layer — sql.js (SQLite/WASM). One in-memory database, built once at load.
  3. Logic layer — CSV parsing, query execution, result shaping, and (for Ask) prompt building + SQL extraction + a read-only guard.
  4. UI layer — tabs, chat stream, SQL editor, schema rail, result table.

03 Components

ComponentWhat it doesTalks to
Data blobHolds DDL + every CSV row as a string. Source of truth at load time.boot()
parseCSVMinimal RFC-4180 parser. Handles quoted fields (the addresses contain commas).boot()
boot()Loads sql.js, creates the DB, runs DDL, inserts every row, records row counts.sql.js, renderSchema
renderSchemaBuilds the left rail of tables/columns; clicks insert TABLE.COLUMN into the editor.insertIntoEditor
runSql / execSQLExecutes SQL against the engine, times it, renders or reports the error.sql.js, renderResults
askLLMSends schema + question to Ollama, returns raw model text.Ollama HTTP
extractSQL / isReadOnlyPulls the SELECT out of the model reply; rejects anything non-read-only.onAsk
onAskOrchestrates the Ask path: chat echo → model → guard → run → render.askLLM, runSql

04 Boot workflow

Runs once, when the page opens. Ends with the status chip reading ok · 5 tables · 36 rows.

  1. Page parses; the browser fetches sql-wasm.js from the CDN and its .wasm companion.
  2. initSqlJs() resolves; boot() creates an empty in-memory database.
  3. DDL runs → the 5 tables exist (empty).
  4. For each table in _order: parseCSV() splits the embedded text into rows, then a prepared INSERT writes them with the fixed column list.
  5. Row counts are stored in DB_INFO; renderSchema() draws the rail; the status chip updates.
If boot fails
The chip turns red and the schema rail shows the error. Almost always a blocked CDN — the engine could not download. Check network / CDN access.

05 Ask workflow (English → SQL)

  1. You type a question and press Enter (onAsk).
  2. Your question is echoed into the chat; a “thinking…” bubble appears.
  3. askLLM POSTs to http://localhost:11434/v1/chat/completions with a system prompt = the DDL + a column glossary + few-shot examples + the rule “output only one SELECT”.
  4. The reply is passed to extractSQL, which strips fences/prose and keeps the SELECT.
  5. isReadOnly checks it: must start with SELECT/WITH/EXPLAIN and contain no destructive keyword.
  6. The SQL is shown in a copyable block, then executed via runSql; the result grid fills below.
Why the model sees the schema
The prompt embeds the exact table/column names and the join keys, so the model names things correctly instead of guessing. That glossary is what makes joins like EMPLOYEES.DEP_ID = DEPARTMENTS.DEPT_ID_DEP work.
Ollama must be reachable from the browser
Browsers block cross-origin calls by default. Start Ollama with OLLAMA_ORIGINS='*', or serve the page from http://localhost. On failure the page prints both fixes and Direct mode keeps working.

06 Direct SQL workflow

  1. Switch to Write SQL (or click a column in the schema rail to jump there).
  2. Type SQL in the editor. Ctrl/Cmd+Enter or Run query.
  3. The statement executes against the same in-memory engine — no model, no network.
  4. Results render as a grid; multiple statements return the last result set.

Direct mode is the fallback: it works with no Ollama, no keys, no server — just open the file.

07 Code map

Line ranges inside sql-workbench.html (729 lines). Sections are numbered in the source comments.

LinesSectionContents
1–9headMeta, viewport, Google Fonts (Fraunces + JetBrains Mono).
10–242CSSDesign tokens (:root), dark + light themes, layout, all component styles.
254–313markupHeader bar, schema rail, tabs + both panels, results area.
314CDN scriptLoads sql-wasm.js (pins engine version 1.13.0).
321–322configSQLJS_CDN, OLLAMA_URL.
324–385§1 dataDDL and the DATA object (5 CSVs + column lists + _order).
387–406§2 parseCSVRFC-4180 CSV parser — quote-aware, skips CR.
408–435§3 bootEngine init, DDL, per-table inserts, DB_INFO, status chip.
437–463§4 schema paneRail rendering; column click → editor insert.
465–534§5 exec/renderexecSQL, buildResultCard, buildErrorCard, runSql (target-aware).
536–552§6 editorSQL textarea wiring, Ctrl+Enter, insertIntoEditor.
554–562§7 tabsswitchTab — Ask ⇄ Write SQL.
564–639§8 NL→SQLGLOSSARY, SYSTEM_PROMPT, extractSQL, isReadOnly, askLLM.
641–714§9 chataddMsg, addSqlBlock (copy button), onAsk orchestration + inline result host.
716–720§10 themeDark/light toggle.
722–725§11 startboot() call with failure handler.

08 Data model

Five tables. CSVs are headerless, so columns are matched positionally against the lists in DATA.

TableRowsColumns
EMPLOYEES10EMP_ID, F_NAME, L_NAME, SSN, B_DATE, SEX, ADDRESS, JOB_ID, SALARY, MANAGER_ID, DEP_ID
JOBS10JOB_IDENT, JOB_TITLE, MIN_SALARY, MAX_SALARY
JOB_HISTORY10EMPL_ID, START_DATE, JOBS_ID, DEPT_ID
DEPARTMENTS3DEPT_ID_DEP, DEP_NAME, MANAGER_ID, LOC_ID
LOCATIONS3LOCT_ID, DEP_ID_LOC

Join keys

EMPLOYEES.DEP_ID
→ DEPARTMENTS.DEPT_ID_DEP  (values '2','5','7')
EMPLOYEES.JOB_ID
→ JOBS.JOB_IDENT
JOB_HISTORY.EMPL_ID
→ EMPLOYEES.EMP_ID
JOB_HISTORY.JOBS_ID
→ JOBS.JOB_IDENT
DEPARTMENTS.LOC_ID
→ LOCATIONS.LOCT_ID
Dates & types
Dates are stored as YYYY-MM-DD text; SQLite compares them as strings, which sorts correctly. Column types come from your DDL but SQLite is dynamically typed, so no coercion runs on insert.

09 Config knobs

model
Top-bar input. Must match a name from ollama list. Default deepseek-v4.1-flash:cloud.
OLLAMA_URL
Line 322. Change if Ollama runs on another host/port.
SQLJS_CDN
Line 321. Bump or mirror the engine version here.
DATA / DDL
Lines 326–385. Swap in new CSVs or a new schema; keep cols in file order.
SYSTEM_PROMPT
Lines 579–602. Tune the NL→SQL rules, glossary, and few-shot examples.

10 Limits & gotchas

  • In-memory only. Reload resets to the original CSVs. Nothing is saved back to your files.
  • Read-only for Ask. Generated DML/DDL is refused by design. Direct mode allows writes, but they vanish on reload.
  • Ollama origin. Ask mode needs OLLAMA_ORIGINS='*' or a served page. Direct mode never needs it.
  • CDN dependency at load. No sql.js download → no database. Cache it locally if you need fully offline runs.
  • Headerless CSVs. If you replace a CSV, column order must match the cols array exactly.
  • Extraction is heuristic. If the model wraps SQL in prose the parser still finds it, but a model that emits two statements may confuse the guard.