| Property | Value |
|---|---|
| Dialect name | sqlite |
| Extends | nothing — implements Dialect directly |
| Driver | better-sqlite3 |
| Cloud SDK | none |
| Default port | none — embedded |
| Local runtime | embedded — a file or :memory: |
SQLiteDialect (in src/dialects/sqlite/index.ts) is built on thebetter-sqlite3better-sqlite3's native Statement#iterate()LIMIT/OFFSET fallback.src/dialects/sqlite/index.ts):name is 'sqlite'; the underlying driver library is'better-sqlite3'.escapeId('col') → "col", with embedded" doubled ("").connect()PRAGMA foreign_keys = ON on every new connection.STRICT tables are$ npm install ts-prorm-orm better-sqlite3$ pnpm add ts-prorm-orm better-sqlite3$ yarn add ts-prorm-orm better-sqlite3$ bun add ts-prorm-orm better-sqlite3import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'sqlite',
storage: './data/app.db', // or ':memory:'
});import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'sqlite',
url: 'file:./data/app.db',
});host, port, database, username and password before the dialect is built; any of those passed alongside it wins.import { DataTypes } from 'ts-prorm-orm';
const User = db.define('User', {
id: { type: DataTypes.INTEGER(), primaryKey: true, autoIncrement: true },
email: { type: DataTypes.STRING(255), allowNull: false, unique: true },
name: { type: DataTypes.STRING(100) },
});
await db.sync(); // CREATE TABLE — see Generated SQL belowimport { Op } from 'ts-prorm-orm';
await User.create({ email: 'ada@example.com', name: 'Ada' });
const page = await User.findAll({
where: { name: { [Op.like]: 'A%' } },
order: [['id', 'ASC']],
limit: 20,
offset: 40,
});query(), captured with the driver stubbed out. The model layer inlines the values.db.sync() for the model in the Quick start:CREATE TABLEawait User.create({ email: 'ada@example.com', name: 'Ada' });
await User.update({ name: 'Grace' }, { where: { email: 'ada@example.com' } });
await User.destroy({ where: { id: 1 } });await User.upsert({ id: 1, email: 'ada@example.com', name: 'Ada Lovelace' });upsert() also reads the row by primary key before and after the write, to return the instance.await User.findAll({
where: { name: { [Op.like]: 'A%' } },
order: [['id', 'ASC']],
limit: 20,
offset: 40,
});await db.transaction(async (t) => {
await User.create({ email: 'grace@example.com', name: 'Grace' }, { transaction: t });
// A nested transaction is a SAVEPOINT; its failure undoes only its own work.
await db
.transaction(async (sp) => {
await User.destroy({ where: { id: 1 }, transaction: sp });
throw new Error('keep the row');
}, { transaction: t })
.catch(() => {});
});STRING → VARCHAR(255), auto-increment INTEGER → INTEGER, BOOLEAN → INTEGER, DATE → DATETIME, UUID → TEXT, JSON → TEXT. The type comparison lines the major engines up side by side.SQLiteDialectOptions extends DialectOptions and adds SQLite-specific fields.PrormOptions exposes dialect and storage; the remainingreadonly, fileMustExist, timeout, verbose) pass throughdialectOptions.import { Prorm } from 'ts-prorm-orm';
// File-backed database
const db = new Prorm({
dialect: 'sqlite',
storage: './data/app.db',
});
// In-memory database (the default when storage is omitted)
const mem = new Prorm({
dialect: 'sqlite',
storage: ':memory:',
});
// With extra better-sqlite3 driver options
const ro = new Prorm({
dialect: 'sqlite',
storage: './data/app.db',
dialectOptions: {
readonly: true, // open the file read-only
fileMustExist: true, // fail instead of creating a new file
timeout: 5000, // busy-handler timeout in ms (default 5000)
// verbose: console.log, // log every executed statement
},
});storage defaults to ':memory:'. An in-memory database is discarded whenstorage may also point at an attached-database path or :memory: for useattachDatabase().connect(), the constructed better-sqlite3 options are{ readonly, fileMustExist, timeout, verbose }, and PRAGMA foreign_keys =
ON is issued immediately.getDataTypeSql(dataType, columnName?) maps the ORM's type descriptors toTEXT, INTEGER, or BLOB.| ORM type descriptor | Generated SQL |
|---|---|
{ key: 'STRING' } | VARCHAR(255) |
{ key: 'STRING', length: 100 } | VARCHAR(100) |
{ key: 'CHAR', length: 4 } | CHAR(4) |
{ key: 'TEXT' } | TEXT |
{ key: 'INTEGER' } | INTEGER |
{ key: 'INTEGER', unsigned: true } | INTEGER UNSIGNED |
{ key: 'BIGINT' } | BIGINT |
{ key: 'FLOAT' } | FLOAT(10,2) |
{ key: 'DOUBLE' } | DOUBLE(10,2) |
{ key: 'DECIMAL', precision: 12, scale: 4 } | DECIMAL(12,4) |
{ key: 'BOOLEAN' } | INTEGER (SQLite has no boolean type) |
{ key: 'DATE' } | DATETIME |
{ key: 'DATEONLY' } | DATE |
{ key: 'TIME' } | TIME |
{ key: 'BLOB' } | BLOB |
{ key: 'JSON' } / { key: 'JSONB' } | TEXT (JSON stored as text) |
{ key: 'UUID' } | TEXT |
{ key: 'GEOMETRY' } | BLOB |
{ key: 'ENUM', values: ['a','b'] } | TEXT CHECK("col" IN ('a','b')) |
{ key: 'VIRTUAL' } | '' (no column emitted) |
'TEXT' (raw string) | TEXT (passed through verbatim) |
BOOLEAN maps to INTEGER, and theescape()/bind-coercion paths convert JS true/false to 1/0. DateJSON.stringify-ed; Buffer values are emitted as X'...' blob literals.ENUM. The object form{ key: 'ENUM', values: [...] } and the raw string form ENUM('a','b') bothTEXT column with a CHECK(... IN (...)) constraint thatcolumnName argument),dialect.getDataTypeSql({ key: 'ENUM', values: ['draft', 'live'] } as any, 'status');createTable() builds a standard CREATE TABLE, optionally withIF NOT EXISTS. Two SQLite-specific table modifiers are supported after the) STRICT, WITHOUT ROWID:strict: true → appends STRICT (SQLite 3.37+ rigid type checking).withoutRowid: true → appends WITHOUT ROWID (stores rows keyed by theAUTOINCREMENT is emitted only when a column is both primaryKey andautoIncrement and its resolved type is exactly INTEGER — this mirrorsAUTOINCREMENT is valid only on an INTEGER PRIMARY KEY.console.warn: engine, charset, table-level collate, rowFormat, andinitialAutoIncrement. uniqueKeys and indexes are realized as separateCREATE [UNIQUE] INDEX statements after the table is created.await dialect.createTable(
'users',
{
id: { type: { key: 'INTEGER' } as any, primaryKey: true, autoIncrement: true },
email: { type: { key: 'STRING' } as any, allowNull: false, unique: true },
role: { type: { key: 'ENUM', values: ['admin', 'user'] } as any },
},
{ strict: true },
);addIndex() emits CREATE [UNIQUE] INDEX IF NOT EXISTS, and supports partialwhere, SQLite 3.8+) and expression indexes (expression, SQLite((expr))).buildInsertQuery() produces a parameterized INSERT, inlining LiteralINSERT INTO ... DEFAULT VALUES. Upserts use SQLite's ON CONFLICT syntax:dialect.buildInsertQuery('kv', { k: 'a', v: 1 });
// { sql: 'INSERT INTO "kv" ("k", "v") VALUES (?, ?)', values: ['a', 1] }ON CONFLICT ... DO UPDATE requires an explicit conflictFields targetdialect.buildInsertQuery(
'kv',
{ k: 'a', v: 1 },
{ upsert: true, conflictFields: ['k'] },
);DO NOTHING is the one form that may omit a target:dialect.buildInsertQuery('kv', { k: 'a', v: 1 }, { upsert: true, doNothing: true });
// ... VALUES (?, ?) ON CONFLICT DO NOTHINGbuildUpsertQuery() behaves the same way, additionally honoringupdateOnDuplicate (restrict the SET list) and returning (SQLite 3.35+RETURNING * or a column list).ON CONFLICT clauses in one INSERT, each targeting aconflictTargets (aConflictTarget[]), which takes precedence over conflictFields/doNothing:dialect.buildInsertQuery(
'accounts',
{ id: 1, email: 'a@x.io', handle: 'ada' },
{
upsert: true,
conflictTargets: [
{ fields: ['id'], action: 'DO NOTHING' },
{ fields: ['email'], action: 'DO UPDATE', updateColumns: ['handle'] },
],
},
);ConflictTarget supports fields (required), action'DO NOTHING' | 'DO UPDATE', default DO UPDATE), updateColumnswhere restricting when theDO UPDATE applies.buildUpdateQuery() and buildDeleteQuery() build parameterized statementsLIMIT and an optional RETURNING clause (SQLite 3.35+).Literal values in an UPDATE set are inlined.buildWhereClause() produces parameterized SQL and supports both string keys$gt) and Symbol operators (normalized via operatorToWhereKey). Sibling$and/$or/$not are AND-ed with plain field conditions rather than{ age: { $gte,
$lte } }) are split and AND-ed.| Operator | SQL |
|---|---|
$eq / $ne | = ? / != ? |
$gt / $gte / $lt / $lte | > ? / >= ? / < ? / <= ? |
$like / $notLike | LIKE ? / NOT LIKE ? |
$iLike / $notILike | LIKE ? / NOT LIKE ? (LIKE is ASCII-case-insensitive) |
$startsWith / $endsWith / $substring | LIKE ? with % padding on the bound value |
$in / $notIn | IN (...) / NOT IN (...) |
$between / $notBetween | BETWEEN ? AND ? / NOT BETWEEN ? AND ? |
$isNull / $isNotNull / $is / $not | IS NULL / IS NOT NULL / IS ? / != ? |
$contains | json_each(...) subquery counting matched elements |
$col | "key" = "otherColumn" (column-to-column) |
IN:dialect.buildWhereClause({ id: [1, 2, 3] });
// { sql: '"id" IN (?,?,?)', values: [1, 2, 3] }TEXT and operates on it via the JSON1 extension. ThebuildJsonExtract(col, path, asText?) → json_extract("col", '$.path'),CAST(... AS TEXT) when asText (the default) is set.buildJsonContains, buildJsonHasKey, buildJsonPathQuery — containment /buildJsonGroupArray, buildJsonGroupObject — the json_group_array() /json_group_object() aggregates for rolling child rows into nested JSON.buildJsonPatch(col, patch) → json_patch("col", json(?)) (RFC 7396 mergenull are removed).BEGIN TRANSACTION / COMMIT / ROLLBACK;SAVEPOINT / RELEASE SAVEPOINT /ROLLBACK TO SAVEPOINT. Standalone savepoint SQL is available viacreateSavepointSQL, releaseSavepointSQL, and rollbackToSavepointSQL.query() supports retry options and, by default when retry is enabled,SQLITE_BUSY, SQLITE_LOCKED, database is locked, anddatabase table is locked — the transient errors that arise when multipleALTER TABLE ADD CONSTRAINT / DROP COLUMN / ALTER COLUMN. SQLite'sALTER TABLE is limited, so these are implemented via the documentedremoveColumn() and changeColumn() recreate the table without / with theaddForeignKey() runs the full 12-step rebuild: read the existingCREATE TABLE from sqlite_master, splice the FOREIGN KEY (...)
REFERENCES ... clause in before the closing paren, then (withPRAGMA foreign_keys=OFF) RENAME the old table aside, recreate it,INSERT INTO new SELECT * FROM old, drop the temp, and restorePRAGMA foreign_keys=ON in a finally.DROP TABLE ... CASCADE is unsupported. SQLite has no CASCADE onDROP TABLE; cascade: true is treated as a no-op (dropping a table simplysync({ force: true }) to work at all.main schema (plus attached databases).createSchema/dropSchema are no-ops, showAllSchemas()/listSchemas()['main'], and quoteTable(name, schema) treats the schema as a"schema"."table") for attached databases.createUser, createRole, grant, revoke, etc. all throw"... not supported by SQLite"; identity columns throw and direct you toAUTOINCREMENT. Sequences are simulated with anINTEGER PRIMARY KEY AUTOINCREMENT table.addPartition() throws; the other partitioncreatePartitionedTable, createPartition, attach/detach) are aUNION ALL view emulation, not real partitions.comment options are acceptedCREATE DATABASE / DROP DATABASE generate CREATE DATABASE "name" /DROP DATABASE "name" strings for interface parity, but the SQLite way to addattachDatabase(path, alias) (a real ATTACH DATABASE ? AS
alias), with detachDatabase(alias) to remove it.import { SQLiteDialect } from 'ts-prorm-orm';
// The dialect class itself is exported, so its SQLite-only options (`strict`)
// and helpers (`buildJsonExtract`) are typed. `new Prorm({ dialect: 'sqlite' })`
// reaches the same class through `getDialectInstance()`.
const dialect = new SQLiteDialect({ storage: ':memory:' });
await dialect.connect(); // opens the DB and runs PRAGMA foreign_keys = ON
// Create a STRICT table with an emulated ENUM
await dialect.createTable(
'todos',
{
id: { type: { key: 'INTEGER' } as any, primaryKey: true, autoIncrement: true },
title: { type: { key: 'STRING', length: 200 } as any, allowNull: false },
status: { type: { key: 'ENUM', values: ['open', 'done'] } as any, defaultValue: 'open' },
meta: { type: { key: 'JSON' } as any },
},
{ strict: true },
);
// Insert (booleans/objects are coerced automatically on bind)
const insert = dialect.buildInsertQuery('todos', {
title: 'Write docs',
meta: { priority: 3 },
});
await dialect.query(insert.sql, { replacements: insert.values as any });
// Upsert on the primary key
const up = dialect.buildUpsertQuery(
'todos',
{ id: 1, title: 'Write docs (v2)', status: 'done' },
{ conflictFields: ['id'], updateOnDuplicate: ['title', 'status'], returning: true },
);
await dialect.query(up.sql, { replacements: up.values as any });
// Query with operators + JSON extraction
const where = dialect.buildWhereClause({ status: 'done', id: { $gte: 1 } });
const jsonPri = dialect.buildJsonExtract('meta', 'priority');
const rows = await dialect.query(
`SELECT id, title, ${jsonPri} AS priority FROM "todos" WHERE ${where.sql}`,
{ replacements: where.values as any },
);
await dialect.disconnect();build* methods shown here are pure/synchronous string builders; the SQLsrc/dialects/sqlite/index.ts. Live executionSTRICT rejects a mistyped value) depends on the running SQLite versiontests/dialects/sqlite-*.test.ts).RETURNING, STRICT, expression-index, and partial-index featuresbetter-sqlite3 bundles a modern SQLite, but a system SQLitecapabilitiesOf() (src/transaction-support.ts),whereSupportOf() (src/query-builders/expression-render.ts),sqlObjectCapabilitiesOf() (src/sql-objects/capabilities.ts) and the| Capability | sqlite |
|---|---|
| Transactions atomic | yes |
| Savepoints | yes |
| Isolation | up to SERIALIZABLE |
| Transaction modes | read-only no; deferrable no; constraint deferral no; SQLite locking type |
| RETURNING | not emitted; returning is accepted and ignored |
| Upsert | INSERT … ON CONFLICT (…) DO UPDATE |
| JSON column | TEXT (JSONB → TEXT) |
| Vector column | refused — The VECTOR column type is not supported on SQLite. SQLite has no vector type; use Turso/libSQL (F32_BLOB) or the sqlite-vec extension |
| Array column | TEXT |
| WHERE builder | 32 operator keys; $and/$or/$not; $exists: yes; $in subquery: yes; expression operands: yes |
| SQL objects | family sqlite: triggers yes; procedures no; functions driverUdf; materialized views emulated (app); sequences no; enums check; RLS no |
| Streaming | queryStream() present; LISTEN absent; createExtension() present |
FindOptions becomes SQL