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-wasmreimplements just the reading needed for time-series queries (latest values, time ranges, time buckets). The rules both must agree on live incrates/timelake-format. - The website uses the client package to read the demo data, and the CLI for local SQL.
Repository layout
| Path | Language | What it is |
|---|---|---|
crates/timelake | Rust | The engine: storage, catalog, writer, commits, maintenance, and the SQL reader. Everything else wraps or mirrors it. |
crates/timelake-format | Rust | Shared 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-cli | Rust | The timelake command. Parses arguments, reads CSV/Parquet, calls the engine, prints results. |
crates/timelake-py | Rust + Python | The Python package. Thin bindings over the engine (PyO3), built with maturin. |
crates/timelake-wasm | Rust | A small read-only engine for browsers and JavaScript, compiled to WebAssembly. It does no network I/O itself. |
packages/timelake-client | TypeScript | @timelake/client: fetches files over HTTPS and hands them to the WASM reader. Rows can be typed with Zod. |
dashboard | TypeScript (Next.js) | The website timelake.dev: landing page, docs, and benchmark charts. |
bench | Python, Go, shell | Benchmarks: the timebench runner with its workloads and system adapters, the run history, AWS scripts, and a request-counting S3 proxy. |
brand | JavaScript | Logo and wordmark sources and the mark editor. |
scripts | shell, JavaScript | Builds a demo warehouse to serve over HTTPS; a static file server with Range support for testing. |
spikes/iceberg-write | Markdown | Findings of an early experiment with iceberg-rust's write support (the code is in git history). |
pyproject.toml | TOML | The uv workspace: the Python package and the benchmarks share one environment (.venv). |
spec/ | Markdown | The 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
| Technology | Used for | Why |
|---|---|---|
Rust | Language of the engine, CLI, Python bindings, and WASM reader | Fast, safe, and the language of the Arrow, Parquet, Iceberg, and DataFusion libraries Timelake builds on |
Apache Arrow (arrow-rs) | In-memory columns | The common format between the writer, the query engine, Python, and DuckDB |
Apache Parquet (parquet-rs) | Data files | Column storage with compression and min/max statistics |
Apache Iceberg (iceberg-rust 0.10) | Table metadata types | Timelake uses its types to read and write metadata, but writes its own commits (iceberg-rust only supports simple appends) |
object_store | All file I/O | One interface for S3, GCS, Azure, HTTP, and local disk |
Apache DataFusion 54 | SQL engine | Embeddable and extensible: Timelake adds a table provider and a query-rewrite rule |
Tokio | Async runtime | Runs many storage requests in parallel |
PyO3 + maturin | Python bindings | Builds a native Python module from Rust; data crosses as Arrow without copies |
wasm-bindgen | WASM bindings | Exposes the Rust reader to JavaScript |
TypeScript, Zod 4 | Client package | Typed rows, validated at runtime |
Bun | JavaScript runtime and package manager | Installs, runs scripts and tests for the website and client |
Next.js 16, React 19 | Website | Server-rendered pages |
Tailwind CSS 4, shadcn/ui (Base UI), Recharts | Website styling, components, and charts | |
Vercel | Website hosting | Deploys on every push to main |
Python + uv | Python package and benchmarks | One uv workspace; uv.lock pins every version |
DuckDB, PyIceberg, Lance, Polars | Benchmark competitors and helpers | |
Go | s3proxy | A small proxy that counts S3 requests and signs them for AWS |
AWS CloudFormation, SSM | Benchmark infrastructure | Creates 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:
| File | What it does |
|---|---|
lib.rs | The public API: Lake, Table, Session, options, and results, with a short example. |
storage.rs | Opens 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.rs | The 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.rs | Table: one version of a table (name, metadata, contract, schema) and files(), the live data files (from the head, or from its manifests). |
info.rs | Table::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.rs | Parquet 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.rs | Small bounded caches for files that never change once written (manifests, manifest lists, last-value indexes) and recently loaded table versions. |
schema.rs | Arrow 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.rs | Turns 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.rs | The 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.rs | The 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.rs | Writes a call doesn't wait for (the version hint and the head), and flush, which waits for them before a process exits. |
append.rs | Table::append, with inline maintenance afterwards. |
maintain.rs | Table::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.rs | Table::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.rs | Error: 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):
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.write_partition(write.rs): encode each day's rows as Parquet underdata/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 anUploadsqueue (a large file's earlier parts are already sent).latest_of_sorted(timelake_format::rows): the newest row of each series in the new data.commit(commit.rs): aSnapshotWriterprepares 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.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, updateversion-hint.textand the head in the background (background.rs,head/).- 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):
- Parse the SQL and find every table it mentions.
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.- DataFusion plans the query. It asks the lake catalog (
query/catalog.rs) for each table and gets aTimelakeProvider(query/provider.rs). - 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): aGROUP BY time_bucketanswered from a rollup for the buckets it has right. Newest-first (query/newest.rs):ORDER BY time DESC LIMIT kreads time partitions newest first and stops early. Series order (query/series_order.rs): scans under atime_weighted_meanmerge files that could give a series out of order. - Otherwise the provider drops files whose min/max values (
query/stats.rs) can't match theWHEREclause, 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. ForINlists 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. - DataFusion runs the rest (filters, joins, aggregates) and returns Arrow batches.
The CLI and the Python package
crates/timelake-cli/src:main.rsdefines the commands withclapand prints results as a table, CSV, or JSON;load.rsreads CSV and Parquet and appends them in commits;info.rsprints the engine'sTableInfoastimelake infotext;bench.rsis the hidden benchmark mode oftimelake sql.tests/cli.rsruns the binary end to end.crates/timelake-py/src/lib.rs: classesLakeandTable. 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 toKeyError,FileExistsError,ValueErrorandtimelake.ConflictError.crates/timelake-py/python/timelake: the Python side:__init__.py, the type stub_timelake.pyi,_types.py(typed result dicts), andpy.typed. Tests are incrates/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:
- The client fetches
version-hint.textandvN.metadata.json;new Table(json)parses it (meta.rs, with the metadata reader oftimelake-format). - 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. planFiles(query)drops files whose min/max values can't match (prune.rs).- 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. Query.pushdecodes and filters each file a batch at a time (filter.rs) and collects rows (with alimit, only those that can still be returned), time buckets (aggregate.rs), or the newest row per series (latest.rs);finishreturns 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
| Path | What |
|---|---|
dashboard/app/(site) | Landing page, /docs (one folder per page), and /benchmarks. |
dashboard/lib/benchmarks.ts | Reads 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.ts | Every benchmark we run, plan, or skip: status, why, links to the original, and the methodology pages' text. |
dashboard/components | Page 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
| Path | What |
|---|---|
bench/timebench/cli.py | timebench: runs a workload (run), a set of runs (profile weekly), and prints one run (report) or the history (history). |
bench/timebench/harness.py | One 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/workloads | Workloads: data at a named scale, the order rows arrive in, and the queries. tsbs.py holds TSBS IoT and DevOps. |
bench/timebench/systems | One 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/runs | The benchmark history: one file per run, with the commit, machine, versions, and every measurement. |
bench/run.py | The Phase 0 engine comparison: one query suite on one engine over static files (Parquet, Vortex, Iceberg), records to bench/results/*.jsonl. |
bench/queries | The TSBS IoT and single-truck queries, written once per engine. |
bench/scripts | Data preparation: TSBS output to Parquet, Iceberg, and Vortex; TPC-H to Parquet. |
bench/timebench | The runner and the code the scripts share: query files and dialects, result records and reports, S3 settings for each engine, storage targets. |
bench/tools/s3proxy | Go. 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/infra | AWS: one CloudFormation stack (buckets, optional instance), scripts to create, use, and delete it, and the weekly run (weekly.sh). |
bench/run_aws.sh | Runs TSBS IoT against AWS S3 and S3 Tables from your machine. |
bench/tests | Tests for the helpers and the summary scripts (uv run pytest bench/tests). |
bench/results | Raw 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.