Best use case

sqljs is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Teams using sqljs should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/sqljs/SKILL.md --create-dirs "https://raw.githubusercontent.com/heldernoid/agentic-build-templates/main/projects/education-learning/sql-tutorial-engine/skills/sqljs/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/sqljs/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How sqljs Compares

Feature / AgentsqljsStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

This skill provides specific capabilities for your AI agent. See the About section for full details.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Skill: sqljs

## What this skill knows

This skill covers sql.js v1: SQLite compiled to WebAssembly for use in the browser and Node.js. It covers initialization, database creation, query execution, schema inspection, and common patterns.

## Installation

```bash
pnpm add sql.js
pnpm add -D @types/sql.js
```

## Browser initialization

sql.js requires the WASM binary. The easiest setup is to serve the binary from `public/` and point `locateFile` at it:

```ts
import initSqlJs from 'sql.js';

// Fetch WASM from /sql-wasm.wasm (copied to public/)
const SQL = await initSqlJs({
  locateFile: (file) => `/${file}`,
});
```

For Vite, use `vite-plugin-wasm` or manually copy `node_modules/sql.js/dist/sql-wasm.wasm` to `public/`.

## Creating a database

```ts
// Empty in-memory database
const db = new SQL.Database();

// From an existing database file (Uint8Array)
const response = await fetch('/my-database.db');
const buffer = await response.arrayBuffer();
const db = new SQL.Database(new Uint8Array(buffer));
```

## Running queries

### exec - for SELECT (returns results)

```ts
const results = db.exec('SELECT * FROM customers');
// results: Array<{ columns: string[], values: any[][] }>

if (results.length === 0) {
  // Query returned no result sets (e.g., DDL)
}

const { columns, values } = results[0];
// columns: ['id', 'name', 'email', 'city']
// values: [[1, 'Alice', 'alice@example.com', 'Austin'], ...]
```

`exec` can run multiple statements separated by `;`. Returns one object per SELECT.

### run - for DDL/DML (no results)

```ts
db.run('CREATE TABLE foo (id INTEGER PRIMARY KEY, bar TEXT)');
db.run("INSERT INTO foo (bar) VALUES (?)", ['hello']);
db.run("UPDATE foo SET bar = ? WHERE id = ?", ['world', 1]);
```

The second argument to `run` is an array of bind parameters.

### Parameterized queries

```ts
// Positional parameters
db.run('SELECT * FROM users WHERE id = ?', [42]);

// Named parameters
db.run('SELECT * FROM users WHERE id = :id', { ':id': 42 });
```

Always use parameterized queries when inserting user input. Do not concatenate user strings into SQL.

## Prepared statements

For repeated queries, use a prepared statement:

```ts
const stmt = db.prepare('SELECT * FROM orders WHERE customer_id = ?');

stmt.bind([customerId]);
while (stmt.step()) {
  const row = stmt.getAsObject();
  console.log(row.id, row.total_amount);
}
stmt.free();
```

- `stmt.bind(params)` binds parameters.
- `stmt.step()` returns `true` while there are more rows.
- `stmt.getAsObject()` returns the current row as `{ columnName: value }`.
- `stmt.free()` releases the statement's memory. Always call this when done.

## Transaction

```ts
db.run('BEGIN TRANSACTION');
try {
  db.run("INSERT INTO orders ...");
  db.run("UPDATE inventory ...");
  db.run('COMMIT');
} catch (err) {
  db.run('ROLLBACK');
  throw err;
}
```

## Error handling

`db.exec` and `db.run` throw a JavaScript `Error` when the SQL is invalid:

```ts
try {
  db.exec('SELECT * FORM users'); // typo: FORM instead of FROM
} catch (err) {
  console.error(err.message);
  // "near "FORM": syntax error"
}
```

Always wrap `exec` in try/catch in application code.

## Schema introspection

```ts
// List all tables
const tableResult = db.exec(
  "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
);
const tableNames = tableResult[0]?.values.map(([name]) => name) ?? [];

// Get column info for a table
const infoResult = db.exec(`PRAGMA table_info(customers)`);
// columns: cid, name, type, notnull, dflt_value, pk
const columns = infoResult[0]?.values.map(([cid, name, type, notNull, dflt, pk]) => ({
  cid: Number(cid),
  name: String(name),
  type: String(type),
  notNull: Boolean(notNull),
  defaultValue: dflt,
  primaryKey: Number(pk) > 0,
})) ?? [];

// Row count
const countResult = db.exec(`SELECT COUNT(*) FROM customers`);
const count = Number(countResult[0].values[0][0]);
```

## Exporting the database

```ts
// Export as Uint8Array (can be saved as a .db file)
const data = db.export();
const blob = new Blob([data], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
```

## Importing data from SQL string

```ts
// Execute a .sql dump file
const sqlDump = await fetch('/datasets/ecommerce.sql').then(r => r.text());
db.run(sqlDump);
```

## Performance characteristics

- sql.js runs in the main thread by default. For large queries (> 100ms), move to a Web Worker.
- Each `exec` call parses and compiles the SQL. Use `prepare` for queries run many times.
- `db.exec` with multiple rows is faster than multiple individual `db.run` calls. Batch INSERTs in a transaction.
- The WASM module is ~1.5 MB (gzip: ~650 KB). Load it once on app init.

## Web Worker pattern (for heavy queries)

```ts
// worker.ts
import initSqlJs from 'sql.js';

let db: any;

self.onmessage = async ({ data: { type, sql, params } }) => {
  if (type === 'init') {
    const SQL = await initSqlJs({ locateFile: f => `/${f}` });
    db = new SQL.Database();
    self.postMessage({ type: 'ready' });
  }
  if (type === 'exec') {
    try {
      const results = db.exec(sql);
      self.postMessage({ type: 'result', results });
    } catch (err: any) {
      self.postMessage({ type: 'error', message: err.message });
    }
  }
};

// main thread
const worker = new Worker(new URL('./worker.ts', import.meta.url));
worker.postMessage({ type: 'init' });
worker.onmessage = ({ data }) => {
  if (data.type === 'ready') {
    worker.postMessage({ type: 'exec', sql: 'SELECT * FROM customers' });
  }
};
```

For the sql-tutorial-engine (max ~2400 rows), the main thread is fast enough. Use a Web Worker only if queries start blocking the UI.

## NULL handling

sql.js returns JavaScript `null` for SQL NULL values in query results. Display them as the string `"NULL"` with muted styling rather than rendering an empty cell.

## Node.js usage

In Node.js, sql.js works without a WASM file - it bundles the WASM inline:

```ts
import initSqlJs from 'sql.js';
const SQL = await initSqlJs();
const db = new SQL.Database();
```

## Type definitions

```ts
import type { Database, Statement, QueryExecResult } from 'sql.js';

// QueryExecResult shape:
interface QueryExecResult {
  columns: string[];
  values: SqlValue[][];
}

type SqlValue = string | number | null | Uint8Array;
```

## Common mistakes

- Calling `new SQL.Database()` before `await initSqlJs()` - `SQL` will be undefined.
- Using `db.exec` for DML and expecting a row count - `exec` always returns result sets. For DML, use `db.run` and `db.getRowsModified()`.
- Not calling `stmt.free()` after using a prepared statement - memory leak.
- Storing `db` in React component state - this causes React re-render issues. Store it in `useRef` or a Zustand store that is not compared by value.
- Forgetting `db.run('COMMIT')` after a `BEGIN TRANSACTION` - the transaction stays open and blocks subsequent writes.

Related Skills

Skill: Uptime Monitoring

7
from heldernoid/agentic-build-templates

## Overview

Skill: Status Page

7
from heldernoid/agentic-build-templates

## Overview

Skill: unit-conversion

7
from heldernoid/agentic-build-templates

## Overview

Skill: recipe-scaler

7
from heldernoid/agentic-build-templates

## Overview

reading-list

7
from heldernoid/agentic-build-templates

Operate the reading-list API to save, manage, tag, search, and export articles.

email-digest

7
from heldernoid/agentic-build-templates

Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.

websocket-realtime

7
from heldernoid/agentic-build-templates

Use the WebSocket connection in poll-builder to receive live vote updates. Use when you need to stream real-time poll results, monitor a poll for new votes, or build a live dashboard. Triggers include "live results", "real-time updates", "stream votes", "watch poll", or "WebSocket".

poll-builder

7
from heldernoid/agentic-build-templates

Self-hosted poll creation tool with real-time results. Use when you need to create a poll, check vote counts, close a poll, export results, or get the shareable link for a poll. Triggers include "create poll", "vote", "poll results", "survey", "collect votes", "share poll", or any task involving polling or voting.

Skill: personal-finance

7
from heldernoid/agentic-build-templates

## Overview

Skill: csv-import

7
from heldernoid/agentic-build-templates

## Overview

Skill: Syntax Highlighting

7
from heldernoid/agentic-build-templates

## Purpose

Skill: Pastebin Core

7
from heldernoid/agentic-build-templates

## Purpose