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 files.
This crate provides:
#[derive(Query)]for binding Rust structs to SQL files indb/queries/#[derive(FromRow)]for decoding rows into Rust structsquery()andquery_one()helpers on top oftokio-postgres- sidecar-driven query checks similar in spirit to SQLx offline metadata
Features
| Feature | Description | Extra dependencies | Default |
|---|---|---|---|
deadpool |
Enable deadpool-postgres client support with cached prepared statements in query() and query_one() |
deadpool-postgres |
no |
with-time-0_3 |
Enable typed query checks for time 0.3 date/time types |
time |
no |
with-uuid-1 |
Enable typed query checks for uuid 1 types |
uuid |
no |
with-serde_json-1 |
Enable typed query checks for serde_json::Value and Json<T> wrappers |
serde_json |
no |
These feature flags only affect the Rust types accepted by the compile-time
query checker. If a query sidecar 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 ;
async
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 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 a sidecar 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:
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->i32text,varchar->String,&strbytea->Vec<u8>,&[u8]timestamptz->time::OffsetDateTimewithwith-time-0_3uuid->uuid::Uuidwithwith-uuid-1json/jsonb->serde_json::Valueortusker_query::types::Json<T>withwith-serde_json-1
This mapping is intentionally conservative. If a sidecar 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
.jsonsidecar 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-postgresstill handles connections, prepared statements, and decodingtusker-queryadds query definitions, row mapping derives, and checked sidecar 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
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.