Codebase tour

What lives where, which technologies are used and why, and how a write and a query flow through the code. Paths are relative to the repository root. For the design behind it, see the specs; for the on-disk format, see Architecture.

The big picture

            your code
   ┌───────────┼──────────────┬──────────────────┐
   │           │              │                  │
 timelake    Python         Rust             browser / Node / serverless
  (CLI)    (timelake-py)   (use the crate)       │
   └───────────┼──────────────┘           @timelake/client (TypeScript)
               │                                 │
        crates/timelake  (the engine)     crates/timelake-wasm (read-only)
               │                                 │
               └──────── crates/timelake-format ─┘   shared rules
                                 │
                 object storage: Iceberg metadata + Parquet files
  • One engine. All writing and full SQL happen in crates/timelake. The CLI and the Python package are thin layers over it.
  • A second, small reader for JavaScript. The engine depends on DataFusion and Tokio, which are too large for a browser. crates/timelake-wasm reimplements just the reading needed for time-series queries (latest values, time ranges, time buckets). The rules both must agree on live in crates/timelake-format.
  • The website uses the client package to read the demo data, and the CLI for local SQL.

Repository layout

PathLanguageWhat it is
crates/timelakeRustThe engine: storage, catalog, writer, commits, maintenance, and the SQL reader. Everything else wraps or mirrors it.
crates/timelake-formatRustShared rules with no I/O: the table contract (time column, series key, …), the settings registry and the typed values of its settings (settings, value), table names and where a table's objects live (layout), time-partition math, Iceberg column types and decoding their min/max values (types, bounds), a rollup's aggregates (rollup), the rules of commits and the plans of maintenance both writers follow, and the shared write path. Used by the engine and the WASM reader and writer.
crates/timelake-cliRustThe timelake command. Parses arguments, reads CSV/Parquet, calls the engine, prints results.
crates/timelake-pyRust + PythonThe Python package. Thin bindings over the engine (PyO3), built with maturin.
crates/timelake-wasmRustA small read-only engine for browsers and JavaScript, compiled to WebAssembly. It does no network I/O itself.
packages/timelake-clientTypeScript@timelake/client: fetches files over HTTPS and hands them to the WASM reader. Rows can be typed with Zod.
dashboardTypeScript (Next.js)The website timelake.dev: landing page, docs, and benchmark charts.
benchPython, Go, shellBenchmarks: the timebench runner with its workloads and system adapters, the run history, AWS scripts, and a request-counting S3 proxy.
brandJavaScriptLogo and wordmark sources and the mark editor.
scriptsshell, JavaScriptBuilds a demo warehouse to serve over HTTPS; a static file server with Range support for testing.
spikes/iceberg-writeMarkdownFindings of an early experiment with iceberg-rust's write support (the code is in git history).
pyproject.tomlTOMLThe uv workspace: the Python package and the benchmarks share one environment (.venv).
spec/MarkdownThe design, one numbered spec per feature with its status and releases, the milestones made of them (spec/milestones/), and background pages. Shown on the site at /spec and /roadmap.

Technologies

TechnologyUsed forWhy
RustLanguage of the engine, CLI, Python bindings, and WASM readerFast, safe, and the language of the Arrow, Parquet, Iceberg, and DataFusion libraries Timelake builds on
Apache Arrow (arrow-rs)In-memory columnsThe common format between the writer, the query engine, Python, and DuckDB
Apache Parquet (parquet-rs)Data filesColumn storage with compression and min/max statistics
Apache Iceberg (iceberg-rust 0.10)Table metadata typesTimelake uses its types to read and write metadata, but writes its own commits (iceberg-rust only supports simple appends)
object_storeAll file I/OOne interface for S3, GCS, Azure, HTTP, and local disk
Apache DataFusion 54SQL engineEmbeddable and extensible: Timelake adds a table provider and a query-rewrite rule
TokioAsync runtimeRuns many storage requests in parallel
PyO3 + maturinPython bindingsBuilds a native Python module from Rust; data crosses as Arrow without copies
wasm-bindgenWASM bindingsExposes the Rust reader to JavaScript
TypeScript, Zod 4Client packageTyped rows, validated at runtime
BunJavaScript runtime and package managerInstalls, runs scripts and tests for the website and client
Next.js 16, React 19WebsiteServer-rendered pages
Tailwind CSS 4, shadcn/ui (Base UI), RechartsWebsite styling, components, and charts
VercelWebsite hostingDeploys on every push to main
Python + uvPython package and benchmarksOne uv workspace; uv.lock pins every version
DuckDB, PyIceberg, Lance, PolarsBenchmark competitors and helpers
Gos3proxyA small proxy that counts S3 requests and signs them for AWS
AWS CloudFormation, SSMBenchmark infrastructureCreates and deletes the buckets (and an optional instance) with one command

The engine, file by file

crates/timelake/src, about 4,000 lines including unit tests. Read them in this order:

FileWhat it does
lib.rsThe public API: Lake, Table, Session, options, and results, with a short example.
storage.rsOpens the right object store for a URL (s3://, gs://, az://, https://, local). Read, write, write_if_absent (the conditional write), list, and delete; Uploads, a queue of files a commit sends all at once; also adapts the store to iceberg-rust's file interface.
lake.rsThe catalog. Lake is a warehouse: create_table writes v1.metadata.json, table() finds the current version (the head and a listing of the metadata files at once, or a check for the next version of one it already has), tables() lists them.
table.rsTable: one version of a table (name, metadata, contract, schema) and files(), the live data files (from the head, or from its manifests).
info.rsTable::info: a version's contract, size, partitions and newest snapshots, which timelake info prints and Python's Table.info() returns.
head/The head object: mod.rs writes a table's head after a commit without moving it backwards, and loads a table from it; codec.rs holds the codecs a lake reads and writes heads with. The layout (header, HeadDoc, sections) and the arrow and json codecs are timelake_format::head's, shared with the WASM reader.
footer.rsParquet footers kept apart from their files, for the head: read from the store once and cached. Their encoding is shared with the WASM writer (timelake_format::footer).
cache.rsSmall bounded caches for files that never change once written (manifests, manifest lists, last-value indexes) and recently loaded table versions.
schema.rsArrow types Iceberg can store, on create (normalize_schema). On write, batches are matched to the table by column name and cast by timelake_format::rows::align_batch.
write.rsTurns batches into Parquet: PartitionWriter writes the batches timelake_format::rows::sort_and_split sorted by partition, series and time (the sorting helpers are shared with the WASM writer) into files of the target size through the shared file writer (timelake_format::file: settings, columns encoded in parallel, and the statistics Iceberg records) and streams them out as multipart uploads.
lvi.rsThe last-value index: the newest row per series, merged with the previous index, read through the lake's cache. Its Parquet file is timelake_format::lvi's.
commit.rsThe commit layer. A SnapshotWriter prepares manifests, the last-value index, and the manifest list, uploads them with the data files in one round, then commits the new metadata with write_if_absent; a lost race cleans up and retries (up to 10 times). Three kinds of change: append, replace (compaction) and delete (retention). The rules (summary, last-value index, which manifests to rewrite, judging the conditional write, backoff) are timelake_format::commit's, which the WebAssembly writer follows too.
background.rsWrites a call doesn't wait for (the version hint and the head), and flush, which waits for them before a process exits.
append.rsTable::append, with inline maintenance afterwards.
maintain.rsTable::maintain: retention, compaction with dedup, rollup refresh, snapshot expiry, and file cleanup. What to merge, drop, expire and delete is planned by timelake_format::maintain, as for the WebAssembly writer; this file reads, writes and commits.
upgrade.rsTable::upgrade (format version, missing last-value index, compaction, head; each a commit, with snapshots kept for a rollback) and Table::rollback.
query/The SQL reader: session.rs (Session: parse, plan, run), settings.rs (session settings, and Override: a session's setting, else the table's), catalog.rs (namespaces and tables for DataFusion), provider.rs (a table as a pruned Parquet scan, planned by a Scan, or the last-value index), dedup.rs (dedup on read: which files may share keys, and the operator that keeps the latest write), stats.rs (min/max from manifests), rewrite.rs (latest-row queries onto the index), route.rs and rollup.rs (answering queries from rollups), in_list.rs (pruning with long IN lists), newest.rs (newest-first scans for ORDER BY time DESC LIMIT), gapfill.rs (time_buckets and the fill window functions), m4.rs and signal.rs (the m4 aggregate, and time_weighted_mean, integral, counter_diff, and counter_rate), shared_state.rs (one state for several of them over the same column), series_order.rs (reading each series in time order for those four), time.rs (times as microseconds, from intervals and timestamps in other units), and functions.rs, lists.rs and groups.rs (what the SQL functions share: their arguments, states as lists of structs, and accumulators for one group or all).
error.rsError: not found, already exists, invalid input, conflict, and the underlying storage, Arrow, and Parquet errors.

Tests live next to the code (#[cfg(test)] modules, with test storage in testing.rs that injects faults or latency) and in crates/timelake/tests: end to end on local disk (lifecycle.rs), concurrent commits (commits.rs), and SQL (sql.rs).

Follow an append through the code

timelake append iot.readings day1.csv, or t.append(batch) in Python, ends up in Table::append (append.rs):

  1. sort_and_split (timelake_format::rows): match columns to the table, compute each row's day, sort by day, series key, and time, and cut the batch into one piece per day.
  2. write_partition (write.rs): encode each day's rows as Parquet under data/time_day=…/, several days at a time, each row group's columns on threads of their own. Each file comes back with its path, size, row count, and min/max per column; its upload waits in an Uploads queue (a large file's earlier parts are already sent).
  3. latest_of_sorted (timelake_format::rows): the newest row of each series in the new data.
  4. commit (commit.rs): a SnapshotWriter prepares a manifest for the new files, the merged last-value index, and the manifest list, then uploads them and the data files at once. With more than eight manifests, the old ones are merged into one so reads stay quick.
  5. publish (commit.rs): write_if_absent(vN+1.metadata.json). If the file already exists, another writer won: delete every file this attempt wrote, reload, and retry. On success, update version-hint.text and the head in the background (background.rs, head/).
  6. Back in append: if inline maintenance is on, run size-tiered compaction on the days this append touched, and expire old snapshots in batches. If that fails, the append still succeeded: the error is returned in the result, not raised.

Follow a query through the code

timelake sql "…" or lake.sql("…") calls Session::sql (query/session.rs):

  1. Parse the SQL and find every table it mentions.
  2. prefetch: load all those tables in parallel. A table with a head comes from that one object (metadata, files, last-value index, footers), confirmed by a listing made at the same time. A table this process already has is checked for a newer version while planning goes on; the scan waits for that check. Others are read through the metadata chain.
  3. DataFusion plans the query. It asks the lake catalog (query/catalog.rs) for each table and gets a TimelakeProvider (query/provider.rs).
  4. The optimizer runs Timelake's rules. The latest-row rewrite (query/rewrite.rs): if the query asks for the newest row per series (or the newest row for some series), the table scan is replaced with a scan of the last-value index. Rollup routing (query/route.rs): a GROUP BY time_bucket answered from a rollup for the buckets it has right. Newest-first (query/newest.rs): ORDER BY time DESC LIMIT k reads time partitions newest first and stops early. Series order (query/series_order.rs): scans under a time_weighted_mean merge files that could give a series out of order.
  5. Otherwise the provider drops files whose min/max values (query/stats.rs) can't match the WHERE clause, spreads the remaining files over parallel scan tasks, fetches every file footer once, and hands the files to DataFusion's Parquet reader, which skips row groups and pages. For IN lists longer than DataFusion prunes with, the provider works out the row groups and pages to read from the footers itself (query/in_list.rs) and passes them as each file's access plan. On scans without filter pushdown, a predicate adapter does the same for a join's runtime filter as each file is opened. With dedup on, files of one partition whose key bounds overlap may hold the same (series, time) (query/dedup.rs): they are pruned only on the key columns, split by a hash of the series, merged in key order slice by slice, and only the latest write of each key is kept.
  6. DataFusion runs the rest (filters, joins, aggregates) and returns Arrow batches.

The CLI and the Python package

  • crates/timelake-cli/src: main.rs defines the commands with clap and prints results as a table, CSV, or JSON; load.rs reads CSV and Parquet and appends them in commits; info.rs prints the engine's TableInfo as timelake info text; bench.rs is the hidden benchmark mode of timelake sql. tests/cli.rs runs the binary end to end.
  • crates/timelake-py/src/lib.rs: classes Lake and Table. Each method runs the engine on a shared Tokio runtime and releases Python's GIL while it waits. Data comes in through the Arrow C stream interface, so pyarrow and polars objects work directly. Errors map to KeyError, FileExistsError, ValueError and timelake.ConflictError.
  • crates/timelake-py/python/timelake: the Python side: __init__.py, the type stub _timelake.pyi, _types.py (typed result dicts), and py.typed. Tests are in crates/timelake-py/tests.

The WebAssembly reader and the TypeScript client

The WASM module never makes network requests. The TypeScript client fetches bytes and hands them in, which keeps the module small and identical in every JavaScript runtime:

  1. The client fetches version-hint.text and vN.metadata.json; new Table(json) parses it (meta.rs, with the metadata reader of timelake-format).
  2. The client fetches the manifest list and manifests; the module parses them (Avro, with timelake_format::iceberg, as the writers read them) into a list of files.
  3. planFiles(query) drops files whose min/max values can't match (prune.rs).
  4. For each file, the client fetches the end of the file (the footer). ParquetFile.plan (file.rs) returns the byte ranges of the row groups and columns needed. The client fetches just those ranges.
  5. Query.push decodes and filters each file a batch at a time (filter.rs) and collects rows (with a limit, only those that can still be returned), time buckets (aggregate.rs), or the newest row per series (latest.rs); finish returns JSON rows (output.rs). The client validates them with Zod if a schema was given.

spec.rs describes the query a host sends; error.rs the errors it gets back. The crate has its own unit tests and a golden test on a small table written by the engine (crates/timelake-wasm/tests).

packages/timelake-client/scripts/build-wasm.sh compiles the crate, shrinks it with wasm-opt, and embeds it as base64 in src/wasm/inline.js, so the package loads without a separate .wasm file. The generated files are committed with a hash of their sources in the header; bun run check:wasm (and CI) fails when they are stale. The client's tests build a small warehouse with the CLI, serve it locally, and compare answers with the engine's, without network access.

The website

PathWhat
dashboard/app/(site)Landing page, /docs (one folder per page), and /benchmarks.
dashboard/lib/benchmarks.tsReads the run files in dashboard/data/runs (a copy of bench/results/runs, made by bun run sync-results) and computes the numbers for the charts and the history.
dashboard/lib/benchmark-catalog.tsEvery benchmark we run, plan, or skip: status, why, links to the original, and the methodology pages' text.
dashboard/componentsPage pieces (diagrams, charts, tables); components/ui holds shadcn/ui components.

Vercel builds and deploys the site on every push to main (project root dashboard/).

The benchmarks

PathWhat
bench/timebench/cli.pytimebench: runs a workload (run), a set of runs (profile weekly), and prints one run (report) or the history (history).
bench/timebench/harness.pyOne run: streams the workload into each system, runs its maintenance, and runs every query in a fresh process (worker.py) at each checkpoint. Writes bench/results/runs/<run>.jsonl.
bench/timebench/workloadsWorkloads: data at a named scale, the order rows arrive in, and the queries. tsbs.py holds TSBS IoT and DevOps.
bench/timebench/systemsOne adapter per system under test: Timelake, Iceberg (PyIceberg), S3 Tables, Lance, plain Parquet files, TimescaleDB. Each writes the tables and opens its readers.
bench/results/runsThe benchmark history: one file per run, with the commit, machine, versions, and every measurement.
bench/run.pyThe Phase 0 engine comparison: one query suite on one engine over static files (Parquet, Vortex, Iceberg), records to bench/results/*.jsonl.
bench/queriesThe TSBS IoT and single-truck queries, written once per engine.
bench/scriptsData preparation: TSBS output to Parquet, Iceberg, and Vortex; TPC-H to Parquet.
bench/timebenchThe runner and the code the scripts share: query files and dialects, result records and reports, S3 settings for each engine, storage targets.
bench/tools/s3proxyGo. Sits between the engines and storage, counts requests and bytes, and can add a fixed delay. In AWS mode it signs requests for the real bucket.
bench/infraAWS: one CloudFormation stack (buckets, optional instance), scripts to create, use, and delete it, and the weekly run (weekly.sh).
bench/run_aws.shRuns TSBS IoT against AWS S3 and S3 Tables from your machine.
bench/testsTests for the helpers and the summary scripts (uv run pytest bench/tests).
bench/resultsRaw results (.jsonl) and write-ups: BASELINE.md (Phase 0), V0.md, S3.md, TIMESCALE.md.

How to build and test each part is on Development.