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
ProducesA .sql scriptA zip of NDJSON files
ForRecreating the databaseHanding the data to another program
Read back bysqlite3, or any SQLite toolAny language with a zip and a JSON library
Can go to a pipeYes, when --out is omittedNo. 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

FlagRequiredDescription
--in <dbfilename>YesPath to an existing .sqlite database to read.
--out <archive>YesWhere 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 SQLiteIn the archiveIn Python
INTEGERA JSON numberint
REALA JSON number, always with a decimal point or an exponentfloat
TEXTA JSON stringstr
BLOB{"$blob": "<base64>"} dict
NULLnullNone

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" } ]
}
FieldWhat it tells you
format_versionThe archive layout's own version. A program that doesn't recognize the number should refuse the archive rather than guess at it.
kent, cordeliaWhich releases produced the archive. Cordelia's is the one that identifies the schema. See Exporting a Database with sqldump for why.
schema.user_versionThe source database's schema version, readable without opening any table data.
sourceThe 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_schemaCounted in the source database, not in this build of Cordelia, so an archive of an older database reports what that database actually had.
columnsEach 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.
skippedTables 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.

See also