Menu

Database SDK

microhost/sql and microhost/db (alias db) give you parameterized SQL fragments, object rows, and table objects generated from your migrations. They run on top of ctx.db: the same database, the same grant, the same broker rules (Context). The SDK adds no permission of its own. Request the modules with sdk: ["db"] (see SDK modules).

Status. Works on the Wasm, native and Linux engines. The behaviour below is taken from the shared module source (sdk-js/runtime/v1/sql.js, db.js). The cross-engine conformance suite does not yet contain database-module scenarios, and the example tests that exercised the module are stale (see Examples). Two defects and a typing mismatch are listed in Known issues.

sql: fragments

import { sql } from "microhost/sql";

const byState = sql`state = ${"open"}`;
const q = sql`SELECT id, title FROM tasks WHERE ${byState} AND id IN (${[1, 2, 3]}) ORDER BY id`;
// q.text       = "SELECT id, title FROM tasks WHERE state = ? AND id IN (?, ?, ?) ORDER BY id"
// q.parameters = ["open", 1, 2, 3]

db: running fragments

import { all, one, run, tx, id, now } from "microhost/db";
Function Returns Notes
all(fragment) Promise<object[]> Read-only (uses ctx.db.query). One object per row, keys in column order.
one(fragment) Promise<object | null> Reads; null when empty; throws db_multiple_rows for more than one row.
run(fragment) Promise<{ rowsAffected, lastInsertRowid }> For writes (uses execute). Returns counts only.
tx(fragments) Promise<{ columns, rows, rowsAffected, lastInsertRowid }[]> One all-or-nothing transaction; rows are objects.
id() string Random UUID v4 from crypto.getRandomValues.
now() string UTC ISO 8601 timestamp, for example 2026-10-25T09:30:00.000Z.

These entry points accept fragments only. A plain string throws TypeError with code db_use_sql_template. Duplicate column names in a result (for example SELECT a.id, b.id) throw db_duplicate_column: alias them (SELECT a.id AS a_id, b.id AS b_id). If ctx.db is absent or revoked, every call rejects with capability_denied.

const open = await all(sql`SELECT id, title FROM tasks WHERE state = ${"open"} ORDER BY id`);
const task = await one(sql`SELECT * FROM tasks WHERE id = ${taskId}`);
await run(sql`UPDATE tasks SET state = 'done' WHERE id = ${taskId}`);
await tx([
  sql`INSERT INTO audit (at, what) VALUES (${now()}, 'close')`,
  sql`UPDATE tasks SET state = 'done' WHERE id = ${taskId}`,
]);

all and one read through query, which the broker refuses for statements that modify data. For INSERT ... RETURNING use a table object (below) or raw ctx.db.execute.

Table objects

Every user table created by your migrations becomes a table object:

import { tasks, tables } from "microhost/db";   // tasks === tables.tasks
Method Result
get(key) The row whose primary key equals key, or null. Always null for a table without a primary key.
find(filter?) One row or null; throws db_multiple_rows if more than one row matches.
where(filter?) A lazy query (below).
count(filter?) The row count as a number (a bigint only beyond the safe integer range).
insert(values) Runs INSERT ... RETURNING * and resolves with the new row (or null).
update(keyOrFilter, values) Runs UPDATE ... RETURNING *; resolves with an array of the updated rows.
delete(keyOrFilter) Runs DELETE ... RETURNING *; resolves with an array of the deleted rows.
stmt.insert(values), stmt.insertOrIgnore(values) Insert fragments for use inside tx([...]). Known defect, see below.

Rules:

import { tasks } from "microhost/db";

const created = await tasks.insert({ title: "Write docs", state: "open" });   // note defaults to NULL
const open = await tasks.where({ state: "open" }).orderBy("id").limit(50).all();
await tasks.update(created.id, { state: "done" });
const one = await tasks.find({ title: "Write docs" });
const n = await tasks.count({ note: null });
await tasks.delete({ state: "done", note: null });

Queries and paging

table.where(filter?) returns a lazy query. Nothing runs until all() or iteration.

const rows = await posts.where({ status: "published" })
  .orderBy("published_at", "desc")
  .limit(20)
  .all();
const page1 = await posts.where().orderBy("id", "desc").limit(20).all();
const page2 = page1.hasMore ? await page1.next() : null;

A full pagination guide (cursor in the URL) is in Guides.

Error codes

SDK errors carry a stable, enumerable code. Messages are the code text. db_use_sql_template, db_duplicate_column and others are thrown as Error or TypeError; match on error.code, never the message.

Code Thrown when
db_use_sql_template A SQL entry point received a plain string or non-fragment; or a filter has an empty array.
db_multiple_rows one or find saw more than one row.
db_duplicate_column The result has duplicate column names.
db_no_such_column A filter, value or order key names a column that is not in the table snapshot; also update with no values.
db_where_required update or delete has no primary key or non-empty filter; insert with no values; next() on a table without a primary key or without a further page.
db_invalid_direction orderBy direction is not asc or desc.
sdk_module_disabled The module is not requested or is disabled for the site (thrown by the import).
capability_denied ctx.db is absent, not granted, or revoked at the time of the call.

Broker errors from the underlying calls keep their usual codes (QUOTA_EXCEEDED, INVALID_INPUT, HOST_UNAVAILABLE, ...). All codes are summarized in Limits and errors. Operators see the stable code in diagnostics; visitors see only a sanitized HTTP status.

Types

sdk/microhost.d.ts declares sql, the microhost/db functions, DbTable, DbQuery, DbRows and DbFilter generically. Column-level types need a per-release declaration file shaped like sdk/db.d.ts (column types, nullability, CHECK (col IN (...)) string unions).

Status. No tool in this repository generates db.d.ts from a release: sdk/db.d.ts is a hand-written example of the intended shape, and sdk/tests/db-types.ts is a type-check fixture for it. Write the declaration for your tables yourself if you want typed rows.

Known issues

These are defects in the current shared module source, documented so you can avoid them.

  1. stmt.insert and stmt.insertOrIgnore used to quote the table name twice. Fixed on 2026-10-25 and covered by a regression test in examples/tasks/test.mjs.
  2. Result type of all, update, delete. These return plain arrays at runtime, and microhost.d.ts now types them as T[]. Only table.where(...).all() returns the paged Rows (first, hasMore, next).
  3. sql empty-array error has no code. The TypeError for an empty array does not carry db_use_sql_template.