SQLiteAdvanced exposes the parts of SQLite that have no cross-dialectPRAGMA tuning, VACUUM/ANALYZE, savepoints, extensionON CONFLICT clause vocabulary.import { createSQLiteAdvanced } from 'ts-prorm-orm';
const sqlite = createSQLiteAdvanced(prorm.getDialectInstance() as any);SQLiteDialect.await sqlite.createFTS5Table(
'docs_fts',
[{ name: 'title' }, { name: 'body' }, { name: 'doc_id', notindexed: true }],
{ tokenizer: 'porter', content: 'docs', prefix: [2, 3] },
);| Option | Purpose |
|---|---|
tokenizer | porter, unicode61, trigram, ascii — with per-tokenizer options |
content | External-content table, so the FTS index shadows a real table |
contentRowId | Which column links back to it |
prefix | Prefix-index lengths, for term* queries |
notindexed | A column stored but not searched |
const { rows } = await sqlite.fullTextSearch('docs_fts', 'database AND index', {
columns: ['title', 'body'],
bm25: true,
highlight: { column: 'body', before: '<mark>', after: '</mark>' },
limit: 20,
});AND,OR, NEAR and * are operators to the matcher.content_rowid. Three triggers keeparticles and never touchesimport { Prorm, DataTypes, createSQLiteAdvanced } from 'ts-prorm-orm';
const prorm = new Prorm({ dialect: 'sqlite', storage: './app.db' });
await prorm.connect();
const Article = prorm.define(
'Article',
{ title: { type: DataTypes.STRING }, body: { type: DataTypes.TEXT } },
{ tableName: 'articles' },
);
await prorm.sync();
const sqlite = createSQLiteAdvanced(prorm.getDialectInstance() as any);
await sqlite.createFTS5Table('articles_fts', [{ name: 'title' }, { name: 'body' }], {
tokenizer: 'porter',
content: 'articles',
contentRowId: 'id',
prefix: [2, 3],
});'delete' command, which carries the old values so FTS5 can find the tokensawait sqlite.createAfterInsertTrigger('articles_fts_ai', 'articles',
'INSERT INTO articles_fts(rowid, title, body) VALUES (new.id, new.title, new.body);');
await sqlite.createAfterDeleteTrigger('articles_fts_ad', 'articles',
"INSERT INTO articles_fts(articles_fts, rowid, title, body) VALUES ('delete', old.id, old.title, old.body);");
await sqlite.createAfterUpdateTrigger('articles_fts_au', 'articles', `
INSERT INTO articles_fts(articles_fts, rowid, title, body) VALUES ('delete', old.id, old.title, old.body);
INSERT INTO articles_fts(rowid, title, body) VALUES (new.id, new.title, new.body);`);await sqlite.rebuildFTS5('articles_fts');await Article.create({ title: 'Indexes everywhere', body: 'Covering indexes avoid the table lookup.' });
const { rows } = await sqlite.fullTextSearch('articles_fts', 'index*', {
columns: ['title', 'body'],
bm25: true,
orderBy: [{ column: 'score' }], // bm25 is lower-is-better
highlight: { column: 'title', before: '[', after: ']' },
limit: 5,
});[
{ "score": -0.0000014, "highlighted": "[Indexes] everywhere", "title": "Indexes everywhere", "body": "Covering indexes avoid the table lookup." },
{ "score": -0.0000013, "highlighted": "[Indexing] in SQLite", "title": "Indexing in SQLite", "body": "A B-tree index speeds up lookups by key." }
]porter tokenizer matches Indexing for index*.[!WARNING]
Without the'delete'command, an external-content index keeps the old
tokens of every updated or deleted row, and searches return rowids that no
longer match their text. If the triggers were ever missing, runrebuildFTS5()to regenerate the index from the content table.
rebuildFTS5(table) and optimizeFTS5(table) issue theINSERT INTO t(t) VALUES('rebuild' | 'optimize') incantations. Rebuild after asqlite.jsonExtract('data', '$.user.name'); // json_extract("data", '$.user.name')
sqlite.jsonSet('data', '$.seen', true);
sqlite.jsonArrayAppend('data', '$.tags', 'new');
sqlite.jsonRemove('data', '$.secret');
sqlite.jsonArrayLength('data', '$.tags');
sqlite.jsonContains('data', 'red', '$.colours');
sqlite.jsonType('data', '$.count');
sqlite.jsonValid('data');where or attributes with prorm.literal(), or into raw SQL.await sqlite.jsonEachAsTable('data', '$.tags'); // json_each
await sqlite.jsonTreeAsTable('data'); // json_tree, recursivesqlite.createVirtualColumn('full_name', 'TEXT', "first_name || ' ' || last_name");
// "full_name" TEXT GENERATED ALWAYS AS (first_name || ' ' || last_name) VIRTUAL
sqlite.createStoredColumn('total', 'REAL', 'price * quantity');
// … STOREDVIRTUAL computes on read and costs no space; STORED writes the value and cancreateTableWithGeneratedColumns() builds the whole CREATE TABLE.sqlite.rowNumber({ partitionBy: ['team'], orderBy: [{ column: 'score', desc: true }] });
sqlite.rank(...); sqlite.denseRank(...); sqlite.ntile(4, ...);
sqlite.lag('score', 1, 0); sqlite.lead('score');
sqlite.firstValue('score'); sqlite.lastValue('score'); sqlite.nthValue('score', 3);
sqlite.buildCTE('ancestors', ['id', 'parent_id'], 'SELECT … UNION ALL SELECT …', true);
await sqlite.executeRecursiveCTE(/* … */);
sqlite.generateSequence(1, 100); // the recursive-CTE integer-series trickgenerateSeries() runs the same trick and returns the rows, which is theconst { rows } = await sqlite.generateSeries(1, 5);
// [{ x: 1 }, { x: 2 }, { x: 3 }, { x: 4 }, { x: 5 }]rowNumber, rank,lag, sumOver, …) compile for every dialect — prefer those unless you needawait sqlite.createTrigger(
'audit_users', // name
'AFTER', // timing: BEFORE | AFTER | INSTEAD OF
['UPDATE'], // events
'users', // table
'INSERT INTO audit …', // body
{ whenCondition: 'OLD.email <> NEW.email', forEachRow: true, replace: true },
);
await sqlite.createAfterInsertTrigger(/* … */);
await sqlite.createInsteadOfTrigger(/* … */); // the only way to write to a view
await sqlite.listTriggers('users');
await sqlite.getTriggerDefinition('audit_users');
await sqlite.dropTrigger('audit_users', { ifExists: true });
await sqlite.createView('active_users', { from: User, where: { status: 'active' } });
await sqlite.listViews();
await sqlite.getViewDefinition('active_users');CREATE OR REPLACE TRIGGER, so replace: true drops anycreateAfterInsertTrigger, createAfterUpdateTrigger,createAfterDeleteTrigger, createBeforeInsertTrigger,createBeforeUpdateTrigger, createInsteadOfTrigger — pluscreateRecursiveTrigger for a trigger that re-fires (needsPRAGMA recursive_triggers = ON).ON CONFLICT algorithms — ROLLBACK, ABORT, FAIL,IGNORE, REPLACE:sqlite.createNotNullConstraint('email', 'ABORT');
sqlite.createUniqueConstraint(['org_id', 'email']);
sqlite.addOnConflictClause('UNIQUE (email)', 'IGNORE');
sqlite.createTableWithOnConflict(/* … */);
await sqlite.upsert('users', { id: 1, email: 'a@x' }, ['id']); // conflict columns
sqlite.buildUpsertQuery('users', ['id','email'], ['id'], undefined, { ignore: true });buildUpsertQuery() produces, forbuildUpsertQuery('users', ['id', 'email', 'name'], ['id'], undefined, options):updateColumns to update only some of them.UNIQUE, NOT NULL or CHECK constraint.[!CAUTION] ON CONFLICT REPLACEdeletes the conflicting row and inserts a new one. That
fires delete triggers, can cascade throughON DELETE CASCADEforeign keys,
and resets every column you did not supply. It is rarely what you want;IGNOREor a real upsert usually is.
await sqlite.vacuum({ analyze: true });
await sqlite.analyze('users');
await sqlite.reindex();
await sqlite.getTableInfo('users'); // PRAGMA table_info
await sqlite.getIndexList('users'); // PRAGMA index_list
await sqlite.getForeignKeyList('users'); // PRAGMA foreign_key_list
await sqlite.getDatabaseInfo();
await sqlite.getTableStats('users');[!WARNING] VACUUMrewrites the whole file. It reclaims space after large deletes, but
it takes an exclusive lock for the duration and needs free disk of up to twice
the database's size while it runs, so schedule it.
await sqlite.pragma('journal_mode', 'WAL');
await sqlite.pragma('synchronous', 'NORMAL');
await sqlite.setDatabaseOptions({ /* several at once */ });journal_mode = WAL is the single highest-value setting for a concurrentimport { Prorm, createSQLiteAdvanced } from 'ts-prorm-orm';
export const prorm = new Prorm({ dialect: 'sqlite', storage: './data/app.db' });
await prorm.connect();
const sqlite = createSQLiteAdvanced(prorm.getDialectInstance() as any);
await sqlite.setDatabaseOptions({
journalingMode: 'WAL', // PRAGMA journal_mode = WAL
synchronous: 'NORMAL', // PRAGMA synchronous = NORMAL (safe with WAL)
foreignKeys: true, // PRAGMA foreign_keys = 1
cacheSize: -20000, // PRAGMA cache_size = -20000, i.e. ~20 MB
});
await sqlite.pragma('busy_timeout', 5000);
console.log(await sqlite.getDatabaseInfo());
// { schemaVersion: 9, userVersion: 0, foreignKeys: true, journalingMode: 'wal',
// synchronous: 1, cacheSize: -20000, pageSize: 4096 }[!NOTE]
An in-memory database (storage: ':memory:') cannot use WAL: SQLite keeps
answeringmemoryforjournal_mode, andgetDatabaseInfo()says so.
await sqlite.savepoint('sp1');
await sqlite.rollbackToSavepoint('sp1');
await sqlite.releaseSavepoint('sp1');withSavepoint() wraps the three: it opens the savepoint, rolls back to it iftry {
await sqlite.withSavepoint('import_batch', async () => {
await Article.create({ title: 'ok', body: '…' });
throw new Error('bad row'); // the insert above is rolled back
});
} catch (e) {
console.warn('batch skipped:', (e as Error).message);
}
sqlite.setExtensionLoading(true);
sqlite.loadExtension('/usr/lib/sqlite/spellfix1.so');
sqlite.getLoadedExtensions();prorm.createSavepoint() / rollbackToSavepoint() / releaseSavepoint() — and[!CAUTION]
Loading an extension executes native code from that path with the process's
privileges. Load only paths you control, and never a path taken from user
input.
sqlite.getColumnAffinity('VARCHAR(255)'); // 'TEXT'
sqlite.createColumnWithAffinity('price', 'DECIMAL(10,2)');
sqlite.createIntegerPrimaryKey('id', true); // INTEGER PRIMARY KEY AUTOINCREMENTVARCHAR(255) willDataTypes maps several types to TEXT