Development
How to build, test, and run each part of the repository. For what each part is, see the Codebase tour; for how to propose a change, Contribute.
Tools to install
| Tool | Needed for | Install |
|---|---|---|
| Rust (rustup) | Everything in crates/ | curl https://sh.rustup.rs -sSf | sh. The version is pinned in rust-toolchain.toml; rustup installs it on first use. |
| uv | Python package and benchmarks | curl -LsSf https://astral.sh/uv/install.sh | sh |
| Bun | Website and TypeScript client | curl -fsSL https://bun.sh/install | bash |
| Go | The benchmark proxy only | brew install go |
| LLVM, wasm-bindgen | Rebuilding the WASM reader only | brew install llvm (Linux: clang and llvm packages), rustup target add wasm32-unknown-unknown, cargo install wasm-bindgen-cli --version 0.2.129 (must match crates/timelake-wasm/Cargo.lock) |
| prek | Git hooks | uv tool install prek |
First setup
uv sync # Python environment in ./.venv (builds the Python package)
bun install # JavaScript dependencies
prek install # Git hooks: formatters and fast lints on commit, clippy and type checks on pushCheck everything the way CI does:
prek run --all-files --hook-stage pre-push # formatters, linters and type checkers for every language
cargo test --workspace # Rust tests
uv run pytest crates/timelake-py/tests # Python package tests
uv run pytest bench/tests # benchmark helper tests
cargo build -p timelake-cli && (cd packages/timelake-client && bun test) # TypeScript client vs the engineRust: engine and CLI
cargo build --release -p timelake-cli # → target/release/timelake
cargo test --workspace # tests
cargo clippy --workspace --all-targets # lints (pedantic set, see [workspace.lints] in Cargo.toml)
cargo fmt --all # formatting (rustfmt.toml: width 120)
cargo doc --no-deps --open # API docsThe Cargo workspace (root Cargo.toml) holds timelake, timelake-format, timelake-cli, and timelake-py, with one Cargo.lock and one target/. timelake-wasm is built separately for WebAssembly (see below).
Try the CLI on a throwaway warehouse:
export TIMELAKE_WAREHOUSE=/tmp/tl-dev
target/release/timelake import iot.readings readings.csv --time-column time --series-key device_id
target/release/timelake sql "SELECT count(*) FROM iot.readings"Python package
uv sync # at the repo root: builds the package into ./.venv
uv run pytest crates/timelake-py/tests
uv run ruff format . && uv run ruff check . # formatting and lints (config in the root pyproject.toml)
uv run ty check # type checkThe root pyproject.toml is a uv workspace with two members: the package (crates/timelake-py) and the benchmarks (bench). uv sync and uv run rebuild the native module with maturin whenever Rust sources change. The Python files live in python/timelake; the native part is imported as timelake._timelake.
WASM reader and TypeScript client
bun install # at the repo root (bun workspace)
cd packages/timelake-client
bun run build:wasm # after changing crates/timelake-wasm or timelake-format (about 4 minutes)
bun run check:wasm # is the embedded WASM current?
bun run typecheckThe client tests build a small warehouse with the CLI in a temporary folder, serve it locally, and compare the client's answers with the CLI's. They never touch the network:
cargo build -p timelake-cli # the tests use target/debug/timelake (or $TIMELAKE_CLI)
(cd packages/timelake-client && bun test)Website
bun install # at the repo root
cd dashboard
bun run dev # http://127.0.0.1:3000
bun run typecheck # Next.js route types, then tsc
bun run build # production build, as Vercel runs itLinting and formatting run from the repo root for all JavaScript and TypeScript: bun run lint (oxlint) and bun run format (oxfmt).
Docs pages are Markdown (MDX) files in dashboard/app/(site)/docs/<page>/page.mdx; the sidebar order is in dashboard/lib/docs-nav.ts. After re-running benchmarks, copy the results the site uses with bun run sync-results.
Benchmarks
uv sync # at the repo root; the same environment as above
cargo build --release -p timelake-cli # the Timelake reader
GOBIN=$PWD/bench/.bin go install github.com/timescale/tsbs/cmd/tsbs_generate_data@v0.0.0-20260527045238-8323e59c7402
uv run timebench run tsbs-iot --scale small
uv run timebench report latestThen follow bench/README.md (workloads, systems, storage targets, adding your own) or bench/infra/README.md (AWS and the weekly run). Benchmark data lives in bench/data (git-ignored, several GB). Commit a run file from bench/results/runs only when it should join the history on the website.
Code style
The code guide for people and AI agents is CONTRIBUTING.md; agents also read AGENTS.md (and CLAUDE.md, which imports it).
| Language | Format | Lint | Types |
|---|---|---|---|
| Rust | rustfmt (width 120) | clippy, pedantic set | the compiler |
| Python | ruff format (width 120) | ruff (broad rule set, Google-style docstrings) | ty; full annotations |
| TypeScript | oxfmt (width 120, no semicolons) | oxlint | tsc, strict with noUncheckedIndexedAccess |
| Go | gofmt | go vet | the compiler |
- Errors:
thiserrorin Rust libraries,anyhowin the CLI; specific exception types in Python. - Comments: explain why, not what. Public items get a short doc comment.
- Commits: short imperative subject with the area first, such as
bench: …,Site: …, orSpec: ….
Continuous integration
GitHub Actions (.github/workflows/ci.yml) runs on every push to main and every pull request, with no secrets:
| Job | Checks |
|---|---|
| Rust | cargo fmt --check, clippy with warnings as errors, cargo test --workspace |
| WASM reader | fmt, clippy for wasm32 and the host, unit and golden tests |
| Python | uv sync --locked, ruff format and lint, ty, the package and benchmark tests |
| TypeScript | oxlint, oxfmt, tsc, the embedded WASM matches its sources, website results match bench/results |
| TypeScript client | client tests against a debug build of the CLI |
| Go | gofmt, go vet |
Vercel builds and deploys the website separately, on every push to main. Other branches and pull requests get no preview deployment (git.deploymentEnabled in dashboard/vercel.json); run bun run dev to see a change.
Formatting-only commits are listed in .git-blame-ignore-revs; run git config blame.ignoreRevsFile .git-blame-ignore-revs once so git blame skips them.