Upgrading

Timelake has one version number for everything: the Rust crates, the CLI, the Python package, and the TypeScript client. Versions follow semantic versioning as Cargo applies it: before 1.0, a minor release (0.3 → 0.4) may change behaviour or APIs, and the changelog says what to do; a patch release (0.3.0 → 0.3.1) never does. Every version is a tag on GitHub.

Table format

Tables have a version of their own, the table format, stored as the table property timelake.format-version. It is a semantic version too, separate from Timelake’s: it changes only when the files a table writes change, and most changes are compatible.

FormatWritten byWhat it adds
0.1.0Timelake 0.1, 0.2 (no property)Iceberg tables with Timelake’s layout and last-value index
0.2.0Timelake 0.3, 0.4The head object, metadata/timelake-head: first queries in one round trip
0.3.0Timelake 0.5 and laterAn insertion-time column, _ingest_ts, that writers fill (timelake.ingest-time-column)

Timelake 0.3.0 wrote the format as 2; it reads as 0.2.0, so those tables need no upgrade. Upgrading a table to 0.3.0 doesn't add _ingest_ts: a table's columns are fixed at creation for now, so only tables created by Timelake 0.5 or later have it.

What works across versions

  • Any Timelake reads and writes older formats. A table written by an older version, or by another Iceberg engine, is used as it is, only slower if it lacks a head or a last-value index.
  • A newer patch format (0.2.0 → 0.2.1) is compatible: older Timelakes still read and write the table.
  • A newer minor format (0.2 → 0.3) is not: an older Timelake refuses to read or write the table, with a message to upgrade Timelake. It could otherwise give wrong answers, or break what the newer format relies on (for example, by not updating the head).
  • From 1.0, a minor bump only stops older writers, and a major bump also stops older readers.
  • Other engines (DuckDB, Spark, PyIceberg) read every format: the Iceberg metadata is standard.

So upgrade every reader and writer of a table before you upgrade the table.

Upgrading tables

timelake upgrade --dry-run          # what would change, for every table
timelake upgrade                    # do it
timelake upgrade iot.readings       # one table
report = table.upgrade(dry_run=True)   # {"steps": [...], ...}
report = table.upgrade()

An upgrade brings a table to the current format and layout:

  1. Records the table format and keeps every snapshot for 7 days (--keep-snapshots).
  2. Builds the last-value index, if a time-series table has none (written by another engine).
  3. Rewrites unsorted or small files into sorted ones, as maintain does (--no-compact skips it).
  4. Writes the head object.

A table that is already current is left alone. An upgrade reads all of a table’s data only when it builds a last-value index or rewrites files.

Nothing is lost

Every step is an ordinary commit: new files are written, and the old snapshots, with all the files they use, stay readable. The upgrade sets timelake.keep-snapshots-until so that no snapshot expires for the next 7 days. Within that time, go back to the table as it was:

timelake rollback iot.readings 7210694381829421937   # the upgrade prints this command
table.rollback(report["snapshot_before"])

A rollback is a commit too: it makes the earlier snapshot current again, and the later ones stay in the history until they expire, so it can be undone the same way. Snapshot ids are listed by timelake info <table>.