An interactive, browser-based SQL learning platform for students. Write real SQL queries against live SQLite databases — no server, no installation, no ongoing costs.
Built with Blazor WebAssembly (.NET 10), sql.js (SQLite compiled to WebAssembly), CodeMirror 6, and Markdig.
- Interactive SQL editor — CodeMirror 6 with SQLite syntax highlighting and autocomplete. Press
Ctrl+Enterto run. - In-browser SQLite — queries execute entirely in the browser via sql.js (no server required).
- Lessons — markdown-based lessons with live databases, rendered with full support for tables, code blocks, headings, and more.
- Problems — practice problems with a Verify button that checks student answers against expected results.
- Playground — free-form query editor against any loaded database.
- Database management — load, reset to original data, download as
.db, or upload your own.sqlscript or.dbfile. - Three starter databases — School, Northwind Lite, Chinook Lite.
- Deployable as static files — works on GitHub Pages, Azure Static Web Apps, Netlify, or any static host.
| Tool | Minimum version | Purpose |
|---|---|---|
| .NET SDK | 10.0 | Build the Blazor app |
| Node.js | 18 LTS | Bundle the CodeMirror editor (one-time step) |
git clone <your-repo-url>
cd SQL-Practicenpm installThis installs CodeMirror 6, the SQL language plugin, and esbuild. It does not affect the .NET project.
cd SQL-Practice
dotnet runOpen the URL shown in the terminal (typically https://localhost:5001).
Different types of changes require different rebuild steps.
Edit the files under SQL-Practice/wwwroot/content/. No rebuild is needed during development — dotnet run serves the files directly. For a production deployment, just rebuild and redeploy:
cd SQL-Practice
dotnet publish -c ReleaseOutput lands in bin/Release/net10.0/publish/wwwroot/ — copy that folder to your host.
cd SQL-Practice
dotnet build # check for errors
dotnet run # run with hot reloadFor deployment:
dotnet publish -c ReleaseThe editor source lives in editor-src/editor.js. After editing it, rebuild the bundle:
# From the repo root (where package.json is)
npm run build # minified production bundle
npm run build:dev # unminified with source maps (for debugging)Then run or publish the .NET project as normal.
Run this from the repo root when you want to do everything at once:
npm run build && cd SQL-Practice && dotnet publish -c ReleaseSQL-Practice/ ← repo root
├── package.json ← npm config (CodeMirror + esbuild)
├── editor-src/
│ └── editor.js ← CodeMirror 6 source (edit this to change editor behaviour)
└── SQL-Practice/ ← .NET Blazor WASM project
├── SQL-Practice.csproj
├── Program.cs ← DI service registrations
├── App.razor ← root component / router
├── _Imports.razor ← global @using directives
├── Models/ ← C# data models (Lesson, Problem, QueryResult, …)
├── Services/
│ ├── ContentService.cs ← loads JSON/Markdown from wwwroot via HttpClient
│ ├── SqliteService.cs ← IJSRuntime wrapper for sql.js
│ ├── DatabaseManager.cs ← tracks loaded databases, lazy-loads from scripts
│ └── VerificationService.cs ← compares student results to expected output
├── Components/
│ ├── SqlEditor.razor ← CodeMirror wrapper component
│ ├── QueryResults.razor ← renders a result table
│ └── MarkdownView.razor ← Markdig markdown → HTML renderer
├── Pages/
│ ├── Home.razor
│ ├── Lessons.razor ← /lessons
│ ├── LessonView.razor ← /lesson/{id}
│ ├── Problems.razor ← /problems
│ ├── ProblemSolve.razor ← /problem/{id}
│ ├── Playground.razor ← /playground
│ └── Databases.razor ← /databases
├── Layout/
│ ├── MainLayout.razor
│ └── NavMenu.razor
└── wwwroot/
├── index.html ← single HTML entry point
├── js/
│ ├── sql-wasm.js ← sql.js loader (from npm, committed to repo)
│ ├── sql-wasm.wasm ← SQLite compiled to WASM (~1.5 MB)
│ ├── sqlInterop.js ← multi-database JS interop module
│ └── editor.bundle.js ← CodeMirror bundle (output of npm run build)
└── content/ ← ALL editable content lives here
├── manifest.json ← master list of lessons, problems, and databases
├── lessons/
│ └── {id}/
│ ├── meta.json ← title, order, database, description, tags
│ └── content.md ← lesson body (full markdown)
├── problems/
│ └── {id}/
│ ├── meta.json ← title, database, hint, expectedResults
│ └── description.md ← problem statement (markdown)
└── databases/
├── school.sql
├── northwind.sql
└── chinook.sql
- Create a folder:
wwwroot/content/lessons/{your-id}/ - Create
meta.json:
{
"id": "your-id",
"title": "Your Lesson Title",
"description": "One-line description shown on the lessons list.",
"database": "school",
"order": 5,
"tags": ["your", "tags"]
}- Create
content.mdwith the full lesson body (standard markdown). - Register it in
manifest.json— add an entry to the"lessons"array with the same fields asmeta.json.
- Create a folder:
wwwroot/content/problems/{your-id}/ - Create
meta.json:
{
"id": "your-id",
"title": "Problem Title",
"lessonId": "related-lesson-id",
"database": "school",
"order": 3,
"hint": "Optional hint shown when the student asks for help.",
"expectedResults": {
"columns": ["column1", "column2"],
"rows": [
["value1a", "value2a"],
["value1b", "value2b"]
]
}
}- Create
description.mdwith the problem statement. - Register it in
manifest.jsonunder the"problems"array.
How to get
expectedResults: Run the correct query in the Playground, then copy the columns and rows into the JSON. All values are stored as strings (numbers and dates included).
- Write a
.sqlfile and place it inwwwroot/content/databases/. - Register it in
manifest.jsonunder the"databases"array:
{
"id": "my-database",
"title": "My Database",
"description": "What this database contains.",
"script": "databases/my-database.sql"
}The script should be a series of CREATE TABLE and INSERT INTO statements compatible with SQLite. Avoid BEGIN TRANSACTION / COMMIT wrappers — the service executes the script in a single call.
When a student clicks Verify, the app:
- Executes their query against the in-memory SQLite database.
- Compares the result to the
expectedResultsstored in the problem'smeta.json.
The check is exact-match (same columns in the same order, same rows in the same order, case-insensitive string comparison). If a problem requires a specific ORDER BY, the expected rows must be stored in that order.
To update the expected answer for a problem, edit expectedResults in its meta.json and redeploy.
The published output is a folder of static files that can be hosted anywhere.
dotnet publish -c Release- Deploy the
bin/Release/net10.0/publish/wwwroot/folder. - No special headers are needed (sql.js runs on the main thread, not a Web Worker).
- Publish as above.
- Add a
.nojekyllfile at the root of the deployed folder (prevents Jekyll from ignoring_framework/). - Add a
404.htmlthat is a copy ofindex.html(needed for client-side routing on direct URL access).
# After dotnet publish
$pub = "SQL-Practice\bin\Release\net10.0\publish\wwwroot"
Copy-Item "$pub\index.html" "$pub\404.html"
New-Item "$pub\.nojekyll" -ItemType FileDeploy the wwwroot publish folder. Add a redirect rule so all paths serve index.html:
Netlify — create _redirects in the publish root:
/* /index.html 200
Vercel — create vercel.json in the publish root:
{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }- All SQL runs in the browser. sql.js compiles SQLite to WebAssembly. No query ever leaves the client.
- Databases are in-memory. Data resets on page refresh. Students can click Reset on the Databases page to restore any database to its original state at any time.
- The solution query is never stored. Only the expected output (
expectedResults) is stored inmeta.json. Students cannot find the answer SQL by inspecting the source. - No authentication. This is designed as a teaching tool for a classroom setting, not a secure assessment platform.
- Page load size is roughly 3–5 MB gzipped (Blazor WASM runtime + sql.js WASM + CodeMirror bundle). A loading spinner is shown during startup.