Standalone, client-agnostic SQLite dump and restore library for Node.js.
Produces the same plain-SQL dump the sqlite3 shell's .dump command writes, and
restores it back — entirely through a database handle. No sqlite3 shell, no
external process is ever invoked. Framework-independent: it does not depend on
DbGate internals and works outside DbGate.
- Node.js >= 20, ESM and CJS builds, full TypeScript types
better-sqlite3is an optional peer dependency, reachable only through the separatedbgate-sqlite-dumper/better-sqlite3entry point — the core never imports a driver- Streaming both ways: a multi-gigabyte database dumps, and a multi-gigabyte
.sqlfile restores, in constant memory
Both directions are proven by automated tests against the real sqlite3 shell,
not assumed:
- Dumps produced by this library restore with the native shell.
sqlite3 copy.db < dump.sql - Dumps produced by the native
.dumprestore with this library.await restoreSqlDump({ connection, source: createReadStream('native-dump.sql') });
There is no custom format, no archive wrapper, and no metadata sidecar. A .sql file
this package writes is byte-identical to sqlite3 database.db .dump — and to its
--data-only, --preserve-rowids, --newlines and --nosys variants — with one
exception that is a property of SQLite itself: the digits past the 17th in a REAL
literal come from SQLite's own printf("%!.20g"), which changed between releases, so a
driver bundling a different SQLite than the shell may spell the same double differently
(0.100000000000000005 vs 0.1000000000000000056). Both parse to the identical value;
the tests compare such lines as doubles and every other byte exactly. See
docs/native-compatibility.md.
Every path in the matrix ends by reading back the restored database and deep-comparing its schema and every table's rows — text compared as bytes — against the source.
| Path | Tested |
|---|---|
this library → native sqlite3 restore |
✅ |
native .dump → this library's restore |
✅ |
| this library → this library | ✅ |
native .dump → native restore (baseline) |
✅ |
npm install dbgate-sqlite-dumper
# optional, for the bundled better-sqlite3 adapter:
npm install better-sqlite3import { createWriteStream } from 'node:fs';
import { dumpSqlite } from 'dbgate-sqlite-dumper';
import { connectBetterSqlite3 } from 'dbgate-sqlite-dumper/better-sqlite3';
const { connection, close } = await connectBetterSqlite3('shop.db', { readonly: true });
try {
const result = await dumpSqlite(
connection,
{ mode: 'full' },
createWriteStream('shop.sql'),
event => console.log(event.phase, event.objectName ?? '', event.bytesWritten ?? ''),
);
console.log(`${result.rowsExported} rows in ${result.statementsWritten} statements`);
for (const warning of result.warnings) {
console.warn(`[${warning.severity}] ${warning.code}: ${warning.message}`);
}
} finally {
await close();
}The result is restorable by sqlite3 shop_copy.db < shop.sql.
import { createReadStream } from 'node:fs';
import { restoreSqlDump } from 'dbgate-sqlite-dumper';
import { connectBetterSqlite3 } from 'dbgate-sqlite-dumper/better-sqlite3';
const { connection, close } = await connectBetterSqlite3('shop_copy.db');
const result = await restoreSqlDump({
connection,
source: createReadStream('shop.sql'),
progress: event => console.log(event.phase, event.currentObject, event.rowsRestored),
});
console.log(`${result.statementsExecuted} statements, ${result.rowsRestored} rows`);
for (const error of result.errors) {
console.error(
`statement ${error.statementIndex} (line ${error.location.startLine}): ${error.message}`,
);
console.error(` ${error.sqlPreview}`); // truncated, key-redacted
console.error(` code=${error.sqliteError?.code}`);
}
await close();source accepts a string, a Buffer, a Readable, or any AsyncIterable of text or
Buffer chunks. Input is parsed incrementally, so restoring a multi-gigabyte dump does not
read it into memory.
import Database from 'better-sqlite3';
import { fromBetterSqlite3 } from 'dbgate-sqlite-dumper/better-sqlite3';
const connection = fromBetterSqlite3(new Database('shop.db'));A database you supply is borrowed and never closed. See
docs/better-sqlite3-adapter.md; any other driver works by
implementing the small SqliteConnection interface.
| Function | Purpose |
|---|---|
dumpSqlite(connection, options, output, onProgress?, signal?) |
Full pipeline: snapshot → introspect → plan → render → stream rows |
restoreSqlDump({ connection, source, options?, progress?, signal? }) |
Streaming parser → statements → database |
introspectSqlite(connection, options?, signal?) |
Normalized SqliteDatabase + version/capabilities/diagnostics |
inspectDumpArchive(database, options?) |
Pure planning → ordered, verified ArchiveEntry[] |
renderPlainSql(request) |
Pure model → plain SQL text (never touches the database) |
exportTableDataAsInserts(request) |
Stream one table's rows as INSERT statements |
preflightRestore(request) |
Target version, modules, conflicts, and what the dump needs |
isSqliteDump(sample) |
Recognizes native and this package's dumps |
parseSqlStatements(sql) / streamSqlStatements(source) |
The shell-compatible statement splitter, usable on its own |
isCompleteStatement(sql) |
A port of sqlite3_complete() |
beginSqliteDumpSession(connection, options?) |
The read snapshot, on its own |
checkTargetCompatibility(database, target) |
Which features a target cannot accept |
fromBetterSqlite3(database) |
Adapter (from dbgate-sqlite-dumper/better-sqlite3) |
connectBetterSqlite3(filename, options?) |
Convenience opener (from dbgate-sqlite-dumper/better-sqlite3) |
Each stage is independently usable: inspectDumpArchive and renderPlainSql are pure
functions of the model and need no connection at all.
Splitting a SQLite script on semicolons breaks on the first trigger in any dump:
CREATE TRIGGER audit_books AFTER INSERT ON books BEGIN
INSERT INTO audit VALUES ('book ' || new.title || '; END');
UPDATE authors SET books = books + 1 WHERE id = new.author_id;
END;This package splits a script exactly where the sqlite3 shell does, by porting the two
pieces of SQLite that decide it: sqlite3_complete() — the state machine that keeps a
trigger body's inner statements together — and the shell's own line rules:
GOand/lines end a statement, as in the shell (SQL Server and Oracle habits).- Dot-commands are the shell's, not SQL. Presentational ones (
.mode,.headers,.print) are skipped with a warning; ones that change the database (.read,.import,.open) are refused with a precise error rather than silently ignored. - A
\rbefore a newline is dropped, even inside a string literal — the shell's line reader does that, so a CRLF file restores identically. - Bytes that are not valid UTF-8 survive. The native
.dumpwrites such text raw; a JavaScript driver cannot pass it through a string, so the literal is rewritten to the exact-byteCAST(X'...' AS TEXT).
Boundary correctness is not assumed: the parser's output is asserted identical at every chunk size and every single split point, and a suite of hand-written scripts is run through both the shell and this package, which must leave identical databases.
| Document | Contents |
|---|---|
| docs/native-compatibility.md | The two-way promise, what is reproduced and why, native quirks |
| docs/dump-api.md | dumpSqlite options, modes, consistency, progress, batching |
| docs/restore-api.md | restoreSqlDump, the parser, session cleanup, errors, preflight |
| docs/better-sqlite3-adapter.md | Ownership, defensive mode, the SqliteConnection contract |
| docs/supported-objects.md | Object matrix: dumped / restored / round-trip tested |
| docs/supported-data-types.md | Per-storage-class fidelity and how each value is written |
| docs/known-limitations.md | What this package does not do, and why |
| docs/round-trip-testing.md | Running the interop suite; the fixture |
| docs/architecture.md | Layer-by-layer design and the reasoning behind it |
- 64-bit integers are exact. Every value is rendered to text by SQLite itself, so
9223372036854775807never passes through a JavaScript number. REALdigits are SQLite's own, from the sameprintfthe shell uses.- Virtual tables come back byte-identical, FTS5 indexes and R-trees included: like the
native
.dump, the schema row is written directly and the shadow tables' contents are copied, instead of rebuilding the index. - Text is bytes. Invalid UTF-8 and embedded
NULs survive a round trip; the native shell silently truncates text at the firstNUL, this package does not. - Connections are left as they were found. A native dump turns
foreign_keysoff and never turns it back on; a restore that stops early leaves a transaction open. Both are put back before the handle is returned. - Consistent under concurrent writes. A dump reads every table inside one snapshot.
npm install
npm run typecheck
npm run lint
npm test # unit tests; better-sqlite3 in-process, nothing else needed
npm run test:integration # needs the sqlite3 shell on PATH (apt install sqlite3)
npm run test:package # builds, then smoke-tests dist/ as ESM and CJSIntegration tests skip themselves with a clear message when the sqlite3 shell is not
installed; set SQLITE_TEST_REQUIRED=1 (as CI does) to make that a hard error.
SQLITE3_BIN selects a specific shell binary.
GPL-3.0-only. See LICENSE.