- 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-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. - Run tests with
cargo nextest. - 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
- When writing tests, key assertions should include a custom message stating the expected behavior.
- Use
#[expect(clippy::...)]instead of#[allow(clippy::...)]. - For internally-tagged enums (
#[serde(tag = "...")]) without lifetimes, useTensorZeroDeserializeinstead ofDeserializefor better error messages viaserde_path_to_error.
- 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 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 --workspaceto regenerate the query cache. This requires a running Postgres database with up-to-date migrations. The generated.sqlxdirectory must be committed to version control.
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 ininternal/tensorzero-node/lib/bindings/. - To generate implementations for
n-apifunctions to be called in TypeScript, and package types ininternal/tensorzero-nodefor UI, runpnpm --filter=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.