SQLite
SQLite made pleasant, on the engine built into Node.js (22.13+): CRUD with rich filters, JSON and boolean columns, transactions, migrations and backups. Methods are synchronous, like the engine, but await still works. Errors are SQLiteErrors. Values are always bound as parameters and names are checked, so there's no SQL injection through them.
const { SQLite } = require("@ix-xs/node-comfort");
import SQLite from "@ix-xs/node-comfort/sqlite";const db = new nc.SQLite("data/app.sqlite");
db.createTable({
name: "users",
columns: {
id: { type: "INTEGER", primaryKey: true, autoincrement: true },
email: { type: "TEXT", notNull: true, unique: true },
settings: { type: "JSON", defaultValue: {} },
},
});
db.insert("users", { email: "ada@example.com", settings: { theme: "dark" } });
db.getAll("users", { email: { like: "%@example.com" } }, { orderBy: "email", limit: 20 });SQLite.Constructor and methods
new SQLite()
new SQLite<R = Record<string, any>>(path?: string, options?: SQLiteOptions): SQLite<R>Opens a database, creating the file and its folders if needed. Use ":memory:" for a throwaway in-memory database.
Parameters
| Name | Type | Description |
|---|---|---|
pathoptional | string | Relative to the working directory.Default: "db.sqlite" |
optionsoptional | SQLiteOptions |
Throws SQLiteError If the file can't be opened.
Example
const db = new nc.SQLite(); // ./db.sqlite
const app = new nc.SQLite("data/app.sqlite", { dates: true });
const mem = new nc.SQLite(":memory:");db.exec()method
exec(sql: string, params?: string | number | bigint | boolean | unknown[] | Record<string, unknown> | Date | null): { changes: number; lastInsertRowid: number | bigint; }Runs SQL that doesn't return rows. Without parameters, you can pass several statements at once.
Parameters
| Name | Type | Description |
|---|---|---|
sql | string | |
paramsoptional | unknown[] | Record<string, unknown> | string | number | bigint | boolean | Date | null | An array, named parameters like { $id: 1 }, or a single value. |
Returns { changes: number, lastInsertRowid: number | bigint }
Throws SQLiteError
Example
db.exec("UPDATE users SET active = ? WHERE last_login < ?", [false, cutoff]);
db.exec("DELETE FROM sessions WHERE user_id = $id", { $id: 42 });
db.exec(`
CREATE TABLE tags (id INTEGER PRIMARY KEY, label TEXT UNIQUE);
CREATE INDEX idx_tags_label ON tags (label);
`);db.queryOne()method
queryOne<T = R>(sql: string, params?: string | number | bigint | boolean | unknown[] | Record<string, unknown> | Date | null): T | undefinedRuns a query and returns the first row. When it reads a single table, JSON and boolean columns are converted.
Parameters
| Name | Type |
|---|---|
sql | string |
paramsoptional | unknown[] | Record<string, unknown> | string | number | bigint | boolean | Date | null |
Returns T | undefined
Throws SQLiteError
Example
const user = db.queryOne("SELECT * FROM users WHERE email = ?", [email]);db.queryAll()method
queryAll<T = R>(sql: string, params?: string | number | bigint | boolean | unknown[] | Record<string, unknown> | Date | null): T[]Runs a query and returns every row.
Parameters
| Name | Type |
|---|---|
sql | string |
paramsoptional | unknown[] | Record<string, unknown> | string | number | bigint | boolean | Date | null |
Returns T[]
Throws SQLiteError
Example
const stats = db.queryAll("SELECT role, COUNT(*) AS n FROM users GROUP BY role");db.iterate()method
iterate<T = R>(sql: string, params?: string | number | bigint | boolean | unknown[] | Record<string, unknown> | Date | null): Generator<T, void, undefined>Goes through the rows of a query one at a time, without loading them all. For big exports.
Parameters
| Name | Type |
|---|---|
sql | string |
paramsoptional | unknown[] | Record<string, unknown> | string | number | bigint | boolean | Date | null |
Returns Generator<T, void, undefined>
Throws SQLiteError
Example
for (const row of db.iterate("SELECT * FROM events WHERE day = ?", [day])) {
stream.write(JSON.stringify(row) + "\n");
}db.createTable()method
createTable(table: TableDefinition): { ok: true; }Creates a table with its constraints and indexes, unless it already exists. To change an existing table, use migrate().
Parameters
| Name | Type |
|---|---|
table | TableDefinition |
Returns { ok: true }
Throws SQLiteError If the definition is invalid.
Example
db.createTable({
name: "posts",
columns: {
id: { type: "INTEGER", primaryKey: true, autoincrement: true },
user_id: { type: "INTEGER", notNull: true, references: { table: "users", column: "id" }, onDelete: "CASCADE" },
status: { type: "TEXT", values: ["draft", "published"], defaultValue: "draft" },
tags: { type: "JSON", defaultValue: [] },
},
indexes: [{ columns: ["user_id", "status"] }],
});db.createIndex()method
createIndex(tableName: string, index: IndexDefinition): { ok: true; }Creates an index, unless it already exists.
Parameters
| Name | Type |
|---|---|
tableName | string |
index | IndexDefinition |
Returns { ok: true }
Throws SQLiteError
Example
db.createIndex("users", { columns: ["email"], unique: true });
db.createIndex("posts", { columns: ["slug"], unique: true, where: "deleted_at IS NULL" });db.deleteTable()method
deleteTable(tableName: string): { ok: true; }Drops a table and everything in it.
Parameters
| Name | Type |
|---|---|
tableName | string |
Returns { ok: true }
Throws SQLiteError If the table doesn't exist.
db.clearTable()method
clearTable(tableName: string): { ok: true; }Deletes every row, keeping the table.
Parameters
| Name | Type |
|---|---|
tableName | string |
Returns { ok: true }
Throws SQLiteError If the table doesn't exist.
db.hasTable()method
hasTable(tableName: string): booleanDoes the table exist?
Parameters
| Name | Type |
|---|---|
tableName | string |
Returns boolean
db.tables()method
tables(): string[]The tables in the database, sorted.
Returns string[]
db.columns()method
columns(tableName: string): ColumnInfo[]The columns of a table.
Parameters
| Name | Type |
|---|---|
tableName | string |
Returns ColumnInfo[]
Throws SQLiteError If the table doesn't exist.
Example
db.columns("users");
// [{ name: "id", type: "INTEGER", notNull: false, defaultValue: null, primaryKey: true }, ...]db.get()method
get<T = R>(tableName: string, where?: Where<R>, options?: Pick<QueryOptions, "columns" | "orderBy" | "direction">): T | undefinedThe first row that matches.
Parameters
| Name | Type |
|---|---|
tableName | string |
whereoptional | Where<R> |
optionsoptional | Pick<QueryOptions, "columns" | "orderBy" | "direction"> |
Returns T | undefined
Throws SQLiteError
Example
const user = db.get("users", { email: "ada@example.com" });
const latest = db.get("posts", { status: "published" }, { orderBy: "created_at", direction: "DESC" });db.getAll()method
getAll<T = R>(tableName: string, where?: Where<R>, options?: QueryOptions): T[]The rows that match, with sorting and pagination.
Parameters
| Name | Type |
|---|---|
tableName | string |
whereoptional | Where<R> |
optionsoptional | QueryOptions |
Returns T[]
Throws SQLiteError
Example
db.getAll("users");
db.getAll("users", { role: ["admin", "editor"], age: { gte: 18 } });
db.getAll("posts", { $or: [{ pinned: true }, { views: { gt: 1000 } }] }, {
orderBy: [["views", "DESC"], "title"],
limit: 20,
offset: 40,
});db.count()method
count(tableName: string, where?: Where<R>): numberCounts the rows that match.
Parameters
| Name | Type |
|---|---|
tableName | string |
whereoptional | Where<R> |
Returns number
Throws SQLiteError
Example
db.count("users", { active: true });db.exists()method
exists(tableName: string, where?: Where<R>): booleanDoes any row match?
Parameters
| Name | Type |
|---|---|
tableName | string |
whereoptional | Where<R> |
Returns boolean
Throws SQLiteError
Example
if (db.exists("users", { email })) throw new Error("Email already registered");db.insert()method
insert(tableName: string, data: Partial<R> & Record<string, unknown>): InsertResultInserts a row. Objects and arrays are stored as JSON, booleans as 1 and 0, dates as ISO strings. undefined values are skipped, so column defaults apply.
Parameters
| Name | Type |
|---|---|
tableName | string |
data | Partial<R> & Record<string, unknown> |
Returns InsertResult
Throws SQLiteError On a constraint failure; sqliteCode tells you which, like "SQLITE_CONSTRAINT_UNIQUE".
Example
const { lastInsertRowid } = db.insert("users", { email: "ada@example.com", settings: { theme: "dark" } });db.insertMany()method
insertMany(tableName: string, rows: (Partial<R> & Record<string, unknown>)[]): ChangeResultInserts many rows in one transaction: all or nothing, and much faster than one by one.
Parameters
| Name | Type |
|---|---|
tableName | string |
rows | Array<Partial<R> & Record<string, unknown>> |
Returns ChangeResult
Throws SQLiteError If a row fails; nothing is inserted then.
Example
db.insertMany("products", rowsFromCsv);db.update()method
update(tableName: string, data: Partial<R> & Record<string, unknown>, where: Where<R>): ChangeResultUpdates the rows that match. The filter is required, so a forgotten condition can't rewrite the whole table.
Parameters
| Name | Type | Description |
|---|---|---|
tableName | string | |
data | Partial<R> & Record<string, unknown> | undefined values are skipped. |
where | Where<R> |
Returns ChangeResult
Throws SQLiteError
Example
db.update("users", { active: false }, { last_login: { lt: "2025-01-01" } });db.upsert()method
upsert(tableName: string, data: Partial<R> & Record<string, unknown>, conflict: string[]): InsertResultInserts a row, or updates it in place if it clashes with an existing one on the conflict columns.
Parameters
| Name | Type | Description |
|---|---|---|
tableName | string | |
data | Partial<R> & Record<string, unknown> | |
conflict | string[] | The unique columns that identify the row. |
Returns InsertResult
Throws SQLiteError If conflict doesn't match a unique constraint.
Example
db.upsert("settings", { user_id: 1, key: "theme", value: "dark" }, ["user_id", "key"]);db.set()method
set(tableName: string, data: Partial<R> & Record<string, unknown>, where?: Where<R> | null): { ok: true; }Updates the row matching where (or { id: data.id }), or inserts it. Kept from 1.x; upsert() does the same in one atomic statement.
Parameters
| Name | Type |
|---|---|
tableName | string |
data | Partial<R> & Record<string, unknown> |
whereoptional | Where<R> | null |
Returns { ok: true }
Throws SQLiteError
db.delete()method
delete(tableName: string, where: Where<R>): ChangeResultDeletes the rows that match. The filter is required; clearTable() deletes everything on purpose.
Parameters
| Name | Type |
|---|---|
tableName | string |
where | Where<R> |
Returns ChangeResult
Throws SQLiteError
Example
db.delete("sessions", { expires_at: { lt: new Date() } });db.table()method
table<T = R>(tableName: string): TableHandle<T>Shortcuts bound to one table, so you don't repeat its name.
Parameters
| Name | Type | Description |
|---|---|---|
tableName | string | The table can be created later. |
Returns TableHandle<T>
Example
const users = db.table("users"); // in TypeScript: db.table<User>("users")
users.insert({ email: "ada@example.com" });
users.count({ active: true });db.transaction()method
transaction<T>(callback: () => T): TRuns callback in a transaction: all its changes are saved together, or none are if it throws. The error is then thrown again. Transactions can nest; an inner failure can be caught without losing the outer work.
Parameters
| Name | Type | Description |
|---|---|---|
callback | () => T | Can be async; the commit then waits for it. |
Returns T
Throws Whatever callback threw, after rolling back.
Example
db.transaction(() => {
db.update("accounts", { balance: from.balance - amount }, { id: from.id });
db.update("accounts", { balance: to.balance + amount }, { id: to.id });
});db.inTransactionproperty
inTransaction: booleanWhether a transaction is open.
db.migrate()method
migrate(migrations: Migration[]): { from: number; to: number; }Applies migrations in order, each one once. The current version is kept in the database, and each migration runs in its own transaction. Only ever add migrations at the end of the list.
Parameters
| Name | Type | Description |
|---|---|---|
migrations | Migration[] | All of them, from the first. |
Returns { from: number, to: number }: The version before and after.
Throws SQLiteError If a migration fails. It's rolled back; earlier ones stay.
Example
db.migrate([
"CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL UNIQUE)",
"ALTER TABLE users ADD COLUMN name TEXT",
(db) => db.update("users", { name: "unknown" }, { name: null }),
]);db.versionproperty
version: numberThe migration version, stored in PRAGMA user_version.
db.backup()method
backup(path: string): stringCopies the database to a new file. Safe while the app is running.
Parameters
| Name | Type | Description |
|---|---|---|
path | string | Must not exist yet. |
Returns string: The absolute path of the copy.
Throws SQLiteError If the file exists or can't be written.
Example
db.backup(`backups/app-${nc.time.format(new Date(), "YYYY-MM-DD")}.sqlite`);db.vacuum()method
vacuum(): voidCompacts the database file to reclaim unused space.
db.fn()method
fn(name: string, implementation: (...args: any[]) => string | number | bigint | Uint8Array<ArrayBufferLike> | null, options?: { deterministic?: boolean; varargs?: boolean; } | undefined): SQLite<R>Makes a JavaScript function callable from SQL.
Parameters
| Name | Type | Description |
|---|---|---|
name | string | |
implementation | (...args: any[]) => null | number | bigint | string | Uint8Array | |
optionsoptional | { deterministic?: boolean, varargs?: boolean } | deterministic (on by default) lets indexes use it; varargs accepts any number of arguments. |
Returns this
Example
db.fn("slugify", (text) => nc.str.slugify(String(text)));
db.queryAll("SELECT slugify(title) AS slug FROM posts");db.pathproperty
path: stringThe absolute path of the database file, or ":memory:".
db.isOpenproperty
isOpen: booleanWhether the connection is open.
db.nativeproperty
native: DatabaseSyncThe underlying node:sqlite database, for anything not covered here.
db.close()method
close(): voidCloses the connection. Calling it twice is fine.
Types
Import any of them in TypeScript with import type { ChangeResult } from "@ix-xs/node-comfort", or in JavaScript with import("@ix-xs/node-comfort").ChangeResult.
ChangeResult
What an update or delete returns.
| Property | Type | Description |
|---|---|---|
ok | true | |
changes | number |
ColumnDefinition
A column.
| Property | Type | Description |
|---|---|---|
type | ColumnType | Like "INTEGER", "TEXT", "JSON" or "VARCHAR(255)". |
primaryKeyoptional | boolean | |
autoincrementoptional | boolean | With an INTEGER primary key: ids are never reused. |
notNulloptional | boolean | |
uniqueoptional | boolean | |
defaultValueoptional | unknown | Objects work for JSON columns, booleans for BOOLEAN ones. |
valuesoptional | unknown[] | Allowed values. |
checkoptional | string | A CHECK expression, like "price >= 0". |
referencesoptional | { table: string; column: string; } | Foreign key target. |
onDeleteoptional | ForeignKeyAction | When the referenced row is deleted. |
onUpdateoptional | ForeignKeyAction | When the referenced key changes. |
ColumnInfo
A column, as returned by columns().
| Property | Type | Description |
|---|---|---|
name | string | |
type | string | "" when there's none. |
notNull | boolean | |
defaultValue | unknown | As SQL text, or null. |
primaryKey | boolean |
ColumnType
A column type. JSON and BOOLEAN columns give you back objects and true/false.
type ColumnType = "INTEGER" | "TEXT" | "REAL" | "BLOB" | "NUMERIC" | "JSON" | "BOOLEAN" | "DATETIME" | (string & {})ForeignKeyAction
What happens to a row when the row it references changes.
type ForeignKeyAction = "CASCADE" | "SET NULL" | "SET DEFAULT" | "RESTRICT" | "NO ACTION"IndexDefinition
An index.
| Property | Type | Description |
|---|---|---|
columns | string[] | Several columns make a composite index. |
uniqueoptional | boolean | |
nameoptional | string | Defaults to idx_<table>_<columns>. |
whereoptional | string | Makes it a partial index, like "deleted_at IS NULL". |
InsertResult
What an insert returns.
| Property | Type | Description |
|---|---|---|
ok | true | |
changes | number | |
lastInsertRowid | number | bigint |
Migration
A migration: SQL (several statements are fine), or a function that gets the database.
type Migration = string | ((db: SQLite<any>) => void)QueryOptions
Options for getAll().
| Property | Type | Description |
|---|---|---|
orderByoptional | string | (string | [string, SortDirection])[] | Like "created_at" or [["age", "DESC"], "name"]. |
directionoptional | SortDirection | For a single orderBy column. Defaults to "ASC". |
limitoptional | number | |
offsetoptional | number | Rows to skip, for pagination. |
columnsoptional | string[] | Columns to return. Defaults to all. |
SortDirection
Sort direction.
type SortDirection = "ASC" | "DESC" | "asc" | "desc"SQLiteOptions
Options for new SQLite().
| Property | Type | Description |
|---|---|---|
jsonoptional | boolean | Parse JSON columns. Defaults to true. |
booleansoptional | boolean | Return BOOLEAN columns as true/false. Defaults to true. |
datesoptional | boolean | Return DATE, DATETIME and TIMESTAMP columns as Dates. |
waloptional | boolean | Write-ahead logging: faster, and reads don't wait for writes. Defaults to true. |
foreignKeysoptional | boolean | Enforce foreign keys. Defaults to true. |
busyTimeoutoptional | number | How long to wait for a lock held by another connection, in ms. Defaults to 5000. |
readOnlyoptional | boolean |
TableConstraint
A constraint on several columns.
type TableConstraint = { type: "unique"; columns: string[]; } | { type: "primaryKey"; columns: string[]; } | { type: "check"; expression: string; } | { type: "foreignKey"; columns: string[]; references: { table: string; columns: string[]; }; onDelete?: ForeignKeyAction; onUpdate?: ForeignKeyAction; }TableDefinition
A table for createTable().
| Property | Type | Description |
|---|---|---|
name | string | |
columnsoptional | Record<string, ColumnDefinition> | |
optionsoptional | Record<string, ColumnDefinition> | Old name for columns, still accepted. |
constraintsoptional | TableConstraint[] | |
indexesoptional | IndexDefinition[] |
TableHandle
Shortcuts bound to one table, from table().
| Property | Type | Description |
|---|---|---|
name | string | |
get | (where?: Where<R>, options?: Pick<QueryOptions, "columns" | "orderBy" | "direction">) => R | The first matching row. |
getAll | (where?: Where<R>, options?: QueryOptions) => R[] | All matching rows. |
insert | (data: Partial<R>) => InsertResult | Inserts a row. |
insertMany | (rows: Partial<R>[]) => ChangeResult | Inserts rows in one transaction. |
update | (data: Partial<R>, where: Where<R>) => ChangeResult | Updates matching rows. |
upsert | (data: Partial<R>, conflict: string[]) => InsertResult | Inserts, or updates on conflict. |
delete | (where: Where<R>) => ChangeResult | Deletes matching rows. |
count | (where?: Where<R>) => number | Counts matching rows. |
exists | (where?: Where<R>) => boolean | Does any row match? |
clear | () => { ok: true; } | Deletes every row. |
Where
A filter. Each key is a column: a value means "equals", null means IS NULL, an array means "one of", and an object uses operators. Keys combine with AND; use $or and $and for the rest.
type Where = { [K in keyof R]?: WhereValue; } & { $or?: Where<R>[]; $and?: Where<R>[]; }WhereOperators
Operators for a column in a filter.
| Property | Type | Description |
|---|---|---|
eqoptional | unknown | Equal. null means IS NULL. |
neoptional | unknown | Not equal. Rows where the column is NULL match too. |
gtoptional | string | number | bigint | Date | Greater than. |
gteoptional | string | number | bigint | Date | Greater than or equal. |
ltoptional | string | number | bigint | Date | Less than. |
lteoptional | string | number | bigint | Date | Less than or equal. |
inoptional | unknown[] | One of these values. |
notInoptional | unknown[] | None of these values. |
likeoptional | string | A LIKE pattern: % any text, _ one character. |
notLikeoptional | string | |
globoptional | string | A case-sensitive pattern: * any text, ? one character. |
betweenoptional | [unknown, unknown] | From min to max, both included. |
isNulloptional | boolean | true for IS NULL, false for IS NOT NULL. |
WhereValue
The condition for one column: a value, null, an array, or operators.
type WhereValue = null | string | number | bigint | boolean | Date | Uint8Array | readonly unknown[] | WhereOperators