Engine

The database API

MinnowDatabase, batch writes, and the options that shape a database.

MinnowDatabase is the engine. It takes a block store and exposes everything else — SQL, batch writes, the catalog, and maintenance.

import { MinnowDatabase } from "@minnowdb/core";
import { IndexedDbBlockStore } from "@minnowdb/core/storage";

const db = new MinnowDatabase(await IndexedDbBlockStore.open({ name: "shop" }), {
  compression: "gzip",
  bufferPoolBytes: 64 * 1024 * 1024,
});

The same surface is available from the main thread when the engine runs in a workerMinnowDatabaseClient mirrors it call for call.

Catalog

await db.createTable({
  name: "orders",
  uniqueKey: "order_id",
  columns: [
    { name: "order_id", type: "number" },
    { name: "total", type: "number" },
    { name: "note", type: "string", nullable: true },
  ],
});

await db.listTables();

createTable is the programmatic form of CREATE TABLE and the only place column defaults can be declared.

Bulk writes

Parsing an INSERT per row is the wrong shape for loading data. The batch APIs take rows directly:

await db.insertBatch("orders", [
  { order_id: 1, total: 24.5, note: null },
  { order_id: 2, total: 88.0, note: "gift wrap" },
]);

Or columns, when you already hold them that way — which skips the pivot the engine would otherwise do:

await db.insertBatch("orders", {
  columns: {
    order_id: [1, 2],
    total: [24.5, 88.0],
    note: [null, "gift wrap"],
  },
});

The full set:

CallEffect
insertBatch(table, input)Append rows. A duplicate key throws.
upsertBatch(table, input)Insert, replacing any row with the same key.
updateBatch(table, { keys, changes })Change named columns on rows addressed by key.
deleteBatch(table, { keys })Remove rows by key.
insert / upsert / updateSingle-row convenience wrappers over the same paths.

Each returns what it did — rowCount, blockCount, storedBytes, and the published version — which is enough to drive a progress bar over a large load without a second query.

One batch is one commit. To make several land together, wrap them in a write scope.

Buffered writing

For a stream of small writes — telemetry, edits as a user types — a buffered writer coalesces them into blocks worth committing:

const writer = db.bufferedWriter("events", { maxRows: 5_000, maxDelayMs: 250 });
writer.add({ event_id: id, kind: "click", at: new Date() });
await writer.flush();

attachLifecycleFlush wires a writer's flush to page hide and freeze events, so a buffer does not follow the tab into the grave.

Options

OptionDefaultWhat it does
compression"gzip"Block codec. "raw" ingests about twice as fast; gzip halves stored bytes and reads faster cold, because reading half the bytes out of IndexedDB more than pays for decompressing them.
rowsPerBlock65536The scan's row group and the buffer pool's residency unit. Measured flat above ~16k; small blocks cost up to 66% on top-N.
bufferPoolBytes64 MiBRetained decoded blocks and their vectorized forms. 0 disables it. Every entry is keyed by an immutable identity, so a cached entry can never be stale.
ftsAutoIndexRows4096Rows above which a MATCH on an unindexed append-only column schedules a background index build.
maxCommitRetries8How many times a losing writer rebases and retries before giving up.
autoCompacttrueWhether small segments are merged in the background.

Errors

Errors are classes, so they can be caught by kind rather than by matching a message:

import { SqlCompileError, UniqueConstraintError, WriteConflictError } from "@minnowdb/core";

SqlCompileError carries the offset and length of the offending span, which is what the devtools editor underlines. WriteConflictError means another writer committed first; the engine retries these itself up to maxCommitRetries before surfacing one.

Closing

db.close();

Closes the underlying store. Any in-flight query rejects rather than hanging.

On this page