Archiving a Database with archive
kent archive writes a database to a single-file interchange archive: a zip
containing one NDJSON file per table that holds data,
a manifest describing them, and the source schema.
It is read-only. kent archive opens the database in SQLite's read-only mode.
Which one do you want, archive or sqldump
Kent has two ways out of a database, and they are for different jobs. Picking the wrong one costs you a conversion step later.
sqldump |
archive | |
|---|---|---|
| Produces | A .sql script | A zip of NDJSON files |
| For | Recreating the database | Handing the data to another program |
| Read back by | sqlite3, or any SQLite tool | Any language with a zip and a JSON library |
| Can go to a pipe | Yes, when --out is omitted | No. A zip cannot be written to standard output |
Back into SQLite, use sqldump. Into Python, use archive.
Synopsis
kent archive --in <dbfilename> --out <archive>
Arguments
| Flag | Required | Description |
|---|---|---|
--in <dbfilename> | Yes | Path to an existing
.sqlite database to read. |
--out <archive> | Yes | Where to write the archive. |
Both are required. Unlike sqldump, there is no
standard-output form to fall back on, because a zip cannot be written to a pipe.
$ kent archive --in mydata.sqlite
missing required flag: --out
usage: kent archive --in <dbfilename> --out <archive>
A note on the file name
.kent is the extension for these archives. It names a defined format, with the
manifest, schema, and per-table layout described below, rather than an arbitrary folder
whose contents are yours to rearrange.
Kent doesn't enforce it. --out takes whatever path you give it, and the result
is an ordinary zip that any zip tool opens. Naming one mydata.zip gives you a
perfectly valid archive. It just doesn't say what it is.
What the archive contains
mydata.kent
mimetype always first, uncompressed: application/vnd.venhola.kent
manifest.json what this archive is, and what is in it
schema.sql the source schema, in the same form kent sqldump --schema-only emits
tables/00000001.jsonl one JSON object per line, one line per row
tables/00000002.jsonl the manifest says which table each number holds
...
Table files are numbered rather than named after their table. A table name can contain
characters a file name cannot, such as a quote, a colon or a slash, so Kent writes
tables/00000001.jsonl, tables/00000002.jsonl and so on, counting in
the order the tables were written. The number tells you nothing about which table it holds:
the manifest's tables[].file is what maps one to the other, which is why the
example below reads the manifest first.
The mimetype entry is what makes a .kent recognisable as one.
Every other entry is compressed, so without it nothing identifies the file until something
decompresses part of it. Those 28 bytes are stored uncompressed at the very front, where a
tool can read them straight out of the file, the same trick EPUB uses, for the same reason.
You can ignore it when reading an archive yourself; kent restore checks it, and
refuses an archive that does not carry it.
A table with no rows produces no file. It is listed in the manifest's
skipped array with a reason instead, so a program reading the archive can tell
an empty table apart from one that was left out or that the source schema never had. A
Cordelia schema has 129 tables and a real database populates roughly 15 of them, so most of
what the manifest describes is what is not there.
Reading it
Python's standard library is enough. No third-party package, and no need to have Kent or Cordelia installed on the machine doing the reading:
import zipfile, json
with zipfile.ZipFile('mydata.kent') as z:
manifest = json.loads(z.read('manifest.json'))
files = {t['name']: t['file'] for t in manifest['tables']}
with z.open(files['Session']) as f:
for line in f:
row = json.loads(line)
print(row['start_time'], row['sport_id'])
The loop reads one line at a time, because a single table can run to tens of thousands of rows. It also opens only the table it wants, and never decompresses the rest. That is why the format is a zip of separate files rather than one large JSON document.
Rows are JSON objects, not arrays, so a column is reached by name. Repeating every column
name on every line costs nothing: that repetition is what zip compression removes, and
row['sport_id'] is easier to get right than row[47].
How values come across
SQLite's storage classes survive the trip:
| In SQLite | In the archive | In Python |
|---|---|---|
INTEGER | A JSON number | int |
REAL | A JSON number, always with a decimal point or an exponent | float |
TEXT | A JSON string | str |
BLOB | {"$blob": "<base64>"} |
dict |
NULL | null | None |
A whole-numbered REAL keeps its decimal point so that it doesn't read back
as an integer. A blob is wrapped in an object rather than written as a bare string because
base64 text is indistinguishable from ordinary text that happens to look like base64.
One value does not survive. Not-a-number and infinity are written as
0.0, and a substituted zero cannot be told apart from a measured one.
sqldump writes not-a-number as NULL
instead.
The manifest
One file answers every structural question about the archive, so a program reads it once and then knows what it is holding:
{
"format_version": 1,
"kent": "1.3.4",
"cordelia": "1.3.3",
"exported": "2026-08-29T14:02:11Z",
"schema": { "user_version": 2 },
"source": { "path": "mydata.sqlite", "size_bytes": 5492736,
"modified": "2026-08-29T00:08:26Z" },
"counts": { "tables_in_schema": 129, "tables_exported": 15,
"tables_skipped": 114, "total_rows": 53673 },
"tables": [
{ "name": "Session", "rows": 26, "file": "tables/00000001.jsonl",
"columns": [ { "name": "sport_id", "type": "INTEGER" } ] }
],
"skipped": [ { "name": "AntRx", "reason": "empty" } ]
}
| Field | What it tells you |
|---|---|
format_version | The archive layout's own version. A program that doesn't recognize the number should refuse the archive rather than guess at it. |
kent, cordelia | Which releases produced the archive. Cordelia's is the one that identifies the schema. See Exporting a Database with sqldump for why. |
schema.user_version | The source database's schema version, readable without opening any table data. |
source | The path, size, and modification time of the file this came from. Size and time together answer "is this an archive of the file I think it is", which a path alone does not. |
counts.tables_in_schema | Counted in the source database, not in this build of Cordelia, so an archive of an older database reports what that database actually had. |
columns | Each column's declared SQLite type, in order. This
is the authority on how to interpret a value. A JSON 1 on its own cannot
tell you whether the column is an integer or a whole-numbered real. |
skipped | Tables with no file in the archive, and why. Today
the only reason is empty. |
The exported and tables_exported names describe the operation
rather than the command that ran it, and they match the -- exported: line in a
kent sqldump header.
The reference tables travel with it
The archive includes Cordelia's pre-seeded lookup tables,
better_labels_definitions and better_labels_lookup, around 19,700
rows between them, rather than leaving them out as bulk.
That is what lets the archive answer its own questions. A sport_id of
5 means nothing alone; resolving it to a sport name needs those two tables.
Without them a script would need a separate Cordelia database to read what it already
holds. They compress well, being short and highly repetitive.
An old database is archived, not refused
Like sqldump, and unlike
info, details,
and files, this command does not refuse a database
whose schema version differs from the one this build expects. The manifest records whatever
version it found, so the archive states what the source actually was.
A run that fails leaves no archive behind, and no partial one. A path given to
--in that does not exist is reported as missing and is not
created:
$ kent archive --in nope.sqlite --out out.kent
kent archive: nope.sqlite does not exist
$ echo $?
1
A temporary directory appears while it runs
Tables are written to a directory named after the archive with .staging
appended, beside the archive, and zipped from there rather than held in memory, because a
single table can be very large. The directory is removed when the command
finishes, including when it fails. If you see one left behind after Kent has exited, that is
worth reporting.
It also means the destination needs room for the archive and its staged contents at the same time.
Reading an archive back into a database
kent restore rebuilds a .sqlite file
from an archive:
$ kent restore --in mydata.kent --out restored.sqlite
It reproduces the database the archive came from rather than bringing it up to date, including its schema version, so an archive of an old database restores to an old database.
See also Error Messages and Exit Codes and Exporting a Database with sqldump.