# 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
`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
```rust,ignore
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:
```text
db/queries/get_post_by_id.sql
```
So the SQL file for the example above would look like:
```sql
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:
```text
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:
```shell
tusker query sync
```
Or for a specific glob:
```shell
tusker query sync 'db/queries/**/*.sql'
```
To inspect a single query without writing the sidecar metadata file:
```shell
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
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
at your option.