- The Cargo workspace root is
crates/. Run allcargocommands from that directory (e.g.cd crates && cargo check). - Use
cargo checkfor quick verification, restrict further (e.g.cargo check --package tensorzero-core) if appropriate. For complex changes, you might want to runcargo check --all-targets --all-features. Test suite compilation is slow. - If you update Rust types or functions used in TypeScript, regenerate bindings with
pnpm build-bindings(from root), then rebuild the NAPI bindings withpnpm --filter=@tensorzero/tensorzero-node build. Runcargo checkfirst to catch compilation errors. - If you change a signature of a struct, function, and so on, use
grepto find all instances in the codebase. For example, search forStructName {when updating struct fields. - Place crate imports at the top of the file or module using
use crate::.... Avoid imports inside functions or tests. Avoid long inline crate paths. - Once you're done with your work, make sure to:
- Run
cargo fmt. - Run
cargo clippy --all-targets --all-features -- -D warningsto catch warnings and errors. - Run unit tests with
cargo test-unit-fastwhich usesnextestunder the hood.
- Run
- Use
#[expect(clippy::...)]instead of#[allow(clippy::...)]. - Prefer early returns over nested
match/ifblocks. For example, uselet ... else { return Err(...) };orif !condition { return Err(...) }to reduce nesting. - For internally-tagged enums (
#[serde(tag = "...")]) without lifetimes, useTensorZeroDeserializeinstead ofDeserializefor better error messages viaserde_path_to_error. - When converting between
Stored*types and core types, use explicit match-based conversions (e.g.Fromimpls or helper functions). Do not round-trip throughserde_json::to_value/serde_json::from_valuefor type conversions —serde_jsonis only appropriate when the source is already aserde_json::Value.
- Run tests with
cargo nextest. - Use
googletestfor new Rust tests. - Annotate new tests with
#[gtest](googletest crate). - Include descriptive messages: use
.expect("why")over.unwrap(), and add custom messages to key assertions. - Prefer
expect_that!to collect all failure messages; useassert_that!when subsequent code depends on the assertion. - To check a string is non-empty, use
not(eq("")). - Prefer
matches_pattern!to assert on multiple struct fields at once rather than separate assertions per field. - Use
matches_json!andmatches_json_literal!from thegoogletest_matcherscrate for JSON assertions. - Never compare serialized JSON strings directly — Postgres JSONB does not preserve key order, so parse to
serde_json::Valueand usematches_json_literal!instead.
- Use
_instead of-in API routes. - Prefer using
#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS))]for ts-rs exports. - For any
Optiontypes visible from the frontend, include#[cfg_attr(feature = "ts-bindings", ts(export, optional_fields))]and#[serde(skip_serializing_if = "Option::is_none")]soNonevalues are not returned over the wire. In very rare cases we may decide do returnnulls, but in general we want to omit them. - Some tests make HTTP requests to the gateway; to start the gateway, you can run
cargo run-e2e. (This gateway has dependencies on some docker containers, and it's appropriate to ask the user to rundocker compose -f crates/tensorzero-core/tests/e2e/docker-compose.yml up.) - We use RFC 3339 as the standard format for datetime.
- API handler will be a thin function that handles properties injected by Axum and calls a function to perform business logic.
- Business logic layer will generate all data that TensorZero is responsible for (e.g. UUIDs for new datapoints,
staled_attimestamps). - Database layer (ClickHouse and/or Postgres) will insert data as-is into the backing database, with the only exception of
updated_attimestamps which we insert by calling native functions in the database.
- Do not use
format!for SQL queries. Usesqlx::QueryBuilderfor dynamic queries.- Use
.push()for trusted SQL fragments (table names, SQL keywords). - Use
.push_bind()for user-provided values (prevents SQL injection, handles types). - Use
.build_query_scalar()for scalar results,.build()for row results.
- Use
- Prefer
sqlx::query!for static queries (queries where only values change, not structure). This provides compile-time verification and typed field access (row.field_nameinstead ofrow.get("field_name")).- Use
QueryBuilderonly when the query structure is dynamic (e.g., optional WHERE clauses, dynamic table names, conditional JOINs, pagination with optional before/after). - For columns that sqlx infers as nullable but are guaranteed non-null by your query logic, use type overrides:
SELECT column as "column!"to get a non-optional type. - For aggregates that should be non-null, use the same pattern:
SELECT COUNT(*)::BIGINT as "total!".
- Use
- After adding or modifying
sqlx::query!/sqlx::query_as!/sqlx::query_scalar!macros, runcargo sqlx prepare --workspace -- --all-features --all-targetsto regenerate the query cache. This requires a running Postgres database with up-to-date migrations. The generated.sqlxdirectory must be committed to version control. - Prefer "Postgres" instead of "PostgreSQL" in comments, error messages, docs, etc.
- Do not run
COUNT(*)or other aggregations over full inference tables (chat_inferences,json_inferences). These tables can be very large and full scans are expensive. Use pre-aggregated rollup tables (e.g.inference_by_function_statistics) or filtered partial indexes instead.
We use uv to manage Python dependencies.
We use ts-rs and n-api for TypeScript-Rust interoperability.
- To generate TypeScript type definitions from Rust types, run
pnpm build-bindings. Then, rebuildtensorzero-nodewithpnpm -r build. The generated type definitions will live incrates/tensorzero-node/lib/bindings/. - To generate implementations for
n-apifunctions to be called in TypeScript, and package types incrates/tensorzero-nodefor UI, runpnpm --filter=@tensorzero/tensorzero-node run build. - Remember to run
pnpm -r typecheckto make sure TypeScript and Rust implementations agree on types. Prefer to maintain all types in Rust.
- Most GitHub Actions workflows run on Unix only, but some also run on Windows and macOS. For workflows that run on multiple operating systems, ensure any bash scripts are compatible with all three platforms. You can check which OS a workflow uses by looking at the
runs-onfield. Settingshell: bashin the job definition is often sufficient.
CONTRIBUTING.mdhas additional context on working on this codebase.- Prefer backticks (`) instead of ticks (') to wrap technical terms in comments, error messages, READMEs, etc.