This page explains how the backup engine (Lightweight::SqlBackup::Backup / Restore)
works: its pipeline, parallelism model, memory and disk profile, fault tolerance, and the
consistency guarantees you can (and cannot) expect. For the on-disk archive layout, see
sql-backup-format.md; for the CLI, see dbtool.md
(dbtool backup / dbtool restore).
A backup is a single zip archive containing:
metadata.json— schema description (tables, columns, types, row counts),checksums.json— SHA-256 per data chunk,data/<table>/NNNN_SS.msgpack— the table data, split into self-contained msgpack chunks (NNNN= window index,SS= sub-chunk within the window).
Backup and restore work against SQLite3, Microsoft SQL Server, and PostgreSQL through ODBC; all per-DBMS SQL differences are handled internally.
-
Schema scan (single-threaded). All table schemas are read up front; on MS SQL Server this uses batched catalog queries.
-
Chunk planning (single-threaded). Each table becomes one or more chunks — units of work for the parallel phase:
- Tables with a single numeric (integer-family) primary key are split into
disjoint key windows
[lo, hi]: the planner runsSELECT MIN(pk), MAX(pk)(an index seek) and divides the span into windows of aboutrowsPerChunkkeys, capped at 1024 windows per table — sparse key spaces (e.g. snowflake-style IDs) get proportionally wider windows instead of millions of empty ones. Each window is an independent chunk, so one big table is read by many workers concurrently. - All other tables (no PK, composite PK, non-numeric PK) are read sequentially by a single worker as one chunk, ordered with OFFSET-based resumption.
- Empty PK tables are detected at plan time and skipped.
- Tables with a single numeric (integer-family) primary key are split into
disjoint key windows
-
Parallel data export.
concurrencyworker threads, each with its own pooled database connection, drain a shared chunk queue. Workers fetch rows (bulk array-fetch where possible, row-by-row otherwise), encode them to msgpack, hash, and add the chunk files to the archive. Chunk filenames are assigned at plan time, so workers never coordinate on naming, and chunks may complete in any order.Array-fetch coverage (one driver round-trip per block of rows instead of one
SQLGetDataper cell — the dominant cost on remote servers): integer family, Real, Bool, Varchar/Char, Decimal, NVarchar/NChar (UTF-16 bound), Date/DateTime/Timestamp (native structs) on all databases; Time and Guid additionally on SQL Server and PostgreSQL. Tables containing LOBs (varchar(max),text, binary) fall back to the row-by-row path. The per-cursor fetch-buffer memory is capped (~4 MB per worker); wide tables automatically read fewer rows per round-trip instead of exhausting RAM. -
Finalize. The workers' sealed archives are merged into the final archive as a raw copy (no recompression — chunk compression already happened inside the workers, overlapped with the network-bound fetch),
metadata.jsonandchecksums.jsonare written, and the archive is closed. The close only streams already-compressed bytes, so it takes seconds even for very large backups.
Restore is chunk-order-independent: it enumerates data/<table>/*.msgpack entries and
tracks completion by per-table chunk count, so parallel, out-of-order backups restore
exactly like sequential ones.
RAM usage is bounded and independent of database size. Each worker buffers at most
chunkSizeBytes (default 10 MB) of encoded rows; flushed chunks go into the worker's
private temp archive under <output>.zip.tmp/, which is sealed (compressed in the
worker thread) every workerArchiveBytes (default 256 MB) of input. The memory ceiling
is therefore about concurrency × workerArchiveBytes plus fetch buffers — and because
the temp archives hold compressed data, the extra disk needed during the run is
only about the size of the final archive itself. The temp directory is removed
automatically when the backup finishes (also on failure).
- Transient errors (connection loss, deadlocks, timeouts) are retried per chunk with
reconnect and exponential backoff (
RetrySettings). A PK-window chunk re-reads just its own window; rows already counted are not double-reported, and chunk files are rewritten in place (ZIP_FL_OVERWRITE), so retries are idempotent. - Hard errors fail the affected table (reported through the progress callback); other tables continue.
- Re-running a backup to the same output is always safe — every entry is overwritten, never appended.
- Integrity: every chunk is SHA-256-hashed into
checksums.jsonand verified on restore.
The backup runs online, without a global snapshot or table locks. If the database is being written to during the run:
- different tables (and different windows of one table) are read at different times, so cross-table consistency is not guaranteed;
- rows inserted above
MAX(pk)after a table was planned are not included; - a row updated between two windows may appear in its old or new version.
For a strictly consistent backup, run against a quiesced database, a snapshot, or a restored replica.
| Knob | Default | Effect |
|---|---|---|
concurrency (jobs) |
caller-defined | Worker threads and database connections. The export phase is mostly network/IO-bound, so more jobs ≈ more concurrent result streams. |
BackupSettings::rowsPerChunk |
100 000 | Target keys per PK window. Smaller = finer load balancing, more files; larger = fewer round-trips per table. |
BackupSettings::chunkSizeBytes |
10 MB | Byte threshold per chunk file flush; sets data-file granularity. |
BackupSettings::workerArchiveBytes |
256 MB | Uncompressed input per worker temp archive before it is sealed (compressed). Bounds worker memory at ~jobs × workerArchiveBytes; lower it on memory-constrained machines. |
BackupSettings::method / level |
Deflate / 6 | Compression method and level (applied at archive close). |
RetrySettings |
sensible defaults | Max retries and backoff for transient errors. |
- sql-backup-format.md — archive layout and msgpack chunk format.
- dbtool.md —
dbtool backup/dbtool restoreCLI usage.