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 indb/queries/#[derive(FromRow)]for decoding rows into Rust structsquery()andquery_one()helpers on top oftokio-postgres- metadata-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 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.sqlSo 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.jsonthen #[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 syncOr 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->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 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
.jsonsidecar 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-postgresstill handles connections, prepared statements, and decodingtusker-queryadds 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
- 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.
Modules§
- types
- Marker traits and PostgreSQL type markers used by checked query validation.
Traits§
- FromRow
- Converts a
tokio-postgresrow 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.