Skip to main content

Crate tusker_query

Crate tusker_query 

Source
Expand description

§tusker-query

tusker-query is a small query layer for tokio-postgres with derive-based query definitions and optional compile-time validation from checked .json sidecar metadata files.

This crate provides:

  • #[derive(Query)] for binding Rust structs to SQL files in db/queries/
  • #[derive(FromRow)] for decoding rows into Rust structs
  • query() and query_one() helpers on top of tokio-postgres
  • metadata-driven query checks similar in spirit to SQLx offline metadata

§Features

FeatureDescriptionExtra dependenciesDefault
deadpoolEnable deadpool-postgres client support with cached prepared statements in query() and query_one()deadpool-postgresno
with-time-0_3Enable typed query checks for time 0.3 date/time typestimeno
with-uuid-1Enable typed query checks for uuid 1 typesuuidno
with-serde_json-1Enable typed query checks for serde_json::Value and Json<T> wrappersserde_jsonno

These feature flags only affect the Rust types accepted by the compile-time query checker. If query metadata references a PostgreSQL type that maps to an optional feature, the corresponding feature must be enabled in the crate that uses tusker-query.

§Example

use tusker_query::{query_one, FromRow, Query};

#[derive(Query)]
#[query(sql = "get_post_by_id", row = Post)]
struct GetPostById {
    pub id: i32,
}

#[derive(FromRow)]
struct Post {
    pub id: i32,
    pub author: String,
    pub text: String,
}

async fn load_post(
    client: &tokio_postgres::Client,
    id: i32,
) -> Result<Post, tokio_postgres::Error> {
    query_one(client, GetPostById { id }).await
}

The Query derive loads SQL from:

db/queries/get_post_by_id.sql

So the SQL file for the example above would look like:

SELECT id, author, text
FROM post
WHERE id = $1

§How it works

#[derive(Query)] implements the tusker_query::Query trait for a named struct. Each struct field becomes a bind parameter in declaration order.

#[derive(FromRow)] implements tusker_query::FromRow for a named struct. Each struct field is decoded from the row by index in declaration order.

At runtime, query() and query_one() prepare the SQL, bind the values from the query struct, execute the statement, and map the result rows through the generated FromRow implementation.

When the deadpool feature is enabled and you pass a deadpool-postgres client or transaction directly, these helpers use prepare_cached() instead of prepare().

For example, query_one(&db, ...) uses the deadpool statement cache, while query_one(db.client(), ...) bypasses it and uses the raw tokio-postgres client path.

§Checked query metadata

If a matching sidecar metadata file exists next to the SQL file:

db/queries/get_post_by_id.json

then #[derive(Query)] uses it at compile time to validate:

  • parameter count
  • parameter types
  • result column count
  • result column types
  • basic nullability expectations
  • SQL checksum freshness

If the sidecar checksum does not match the SQL file, the derive emits a compile error asking you to refresh the metadata.

Queries without sidecar metadata still compile; they just skip this extra validation.

§Generating sidecars

Use the tusker CLI to refresh checked query metadata:

tusker query sync

Or for a specific glob:

tusker query sync 'db/queries/**/*.sql'

To inspect a single query without writing the sidecar metadata file:

tusker query inspect db/queries/get_post_by_id.sql

§Supported PostgreSQL type mappings

The checked query metadata currently maps common PostgreSQL types to Rust types through marker traits in tusker_query::types.

Examples:

  • int4 -> i32
  • text, varchar -> String, &str
  • bytea -> Vec<u8>, &[u8]
  • timestamptz -> time::OffsetDateTime with with-time-0_3
  • uuid -> uuid::Uuid with with-uuid-1
  • json / jsonb -> serde_json::Value or tusker_query::types::Json<T> with with-serde_json-1

This mapping is intentionally conservative. If query metadata references a type that is not supported yet, the derive fails with a compile error instead of quietly accepting a potentially wrong mapping.

§Limitations

  • SQL files are resolved relative to db/queries/
  • bind parameters are matched by Rust field order
  • row decoding is matched by Rust field order
  • compile-time checking only runs when a .json sidecar metadata file exists
  • the nullability signal comes from query metadata and is currently best-effort

§Relationship to tokio-postgres

tusker-query is not a replacement for tokio-postgres. It is a thin layer on top of it:

  • tokio-postgres still handles connections, prepared statements, and decoding
  • tusker-query adds query definitions, row mapping derives, and checked query metadata

If you already use tokio-postgres directly, this crate is meant to give you a lighter-weight, file-based alternative to handwritten SQL wrappers.

If you use deadpool-postgres, enable the deadpool feature and pass the pool client itself to query() / query_one() to reuse the statement cache. If you intentionally extract the raw client with db.client(), the helpers fall back to the uncached tokio-postgres path.

§License

Licensed under either of

at your option.

Modules§

types
Marker traits and PostgreSQL type markers used by checked query validation.

Traits§

FromRow
Converts a tokio-postgres row into a strongly typed Rust value.
Query
A typed SQL query that can be executed through tokio-postgres.

Functions§

query
Executes a query and collects all returned rows.
query_one
Executes a query that must return exactly one row.

Derive Macros§

FromRow
Derives tusker_query::FromRow for a named struct.
Query
Derives tusker_query::Query for a named struct.