Expand description
keelson’s derive macros: Bind for newtype column types, FromRow
for row mapping.
Both close the same gap. docs/type-mappings.md promises that the Rust
type of a generated column can be replaced with your own, and that what a
replacement must satisfy is a trait bound in the generated code — so a
type that cannot bind is a compile error, not a runtime surprise. That
bound is keelson_exec::Bind:
pub trait Bind: ToValue + FromValue + Send + 'static {}
impl<T: ToValue + FromValue + Send + 'static> Bind for T {}and for every [[types.override]] in a keelson-gen configuration the
generator emits one line asserting it:
const _: () = keelson_exec::assert_bind::<crate::types::UserId>();Satisfying that used to mean hand-writing ToValue and FromValue.
#[derive(Bind)] writes them for a newtype.
§Where this sits
The derives, and nothing else. Do not depend on this crate directly:
keelson-core re-exports both behind its macros feature,
which is the path use keelson_core::Bind; takes and the one generated code
is written against. The traits they implement live in keelson-core
(ToValue/FromValue) and keelson-exec (FromRow). The
whole map is the keelson facade crate.
§#[derive(Bind)]
use keelson_core::{Bind, FromValue as _, ToValue as _, Value};
#[derive(Debug, Clone, PartialEq, Bind)]
pub struct UserId(pub i64);
#[derive(Debug, Clone, PartialEq, Bind)]
pub struct Email(String);
// The line generated code emits for an override now passes.
const _: () = keelson_exec::assert_bind::<UserId>();
assert_eq!(UserId(7).to_value(), Value::I64(7));
assert_eq!(UserId::from_value(Value::I64(7)).unwrap(), UserId(7));What it emits, and nothing else: keelson_core::ToValue and
keelson_core::FromValue, each delegating to the single inner field.
Bind itself is never implemented — it is a blanket alias, and naming the
derive after the bound it satisfies is the point. Everything the inner type
can do, the newtype now does: Option<UserId> binds as NULL-or-value
through core’s blanket impls, the widening FromValue accepts (a driver
that hands back I32 for a BIGINT) still applies, and the type is usable
anywhere arg(...) takes a value.
§What it accepts
One field, named or not — struct UserId(i64); and
struct UserId { raw: i64 } are the same thing to this derive. Generics
work (struct Tagged<T>(T)), with the inner type’s bound added to the
generated impls.
§What it refuses, and why
- Multi-field structs. A column is one value. A struct of several is a
row, which is what
FromRowis for; if the parts genuinely are one column, the encoding is a decision (separator? escaping? what does a malformed value mean?) that belongs in your ownToValue/FromValue. - Enums. Same reason, one level deeper: an enum needs a chosen
database representation — text or integer, the spelling of each variant,
and what an unrecognised value read back means. Every one of those is a
decision this derive would have to invent, and inventing it silently is
exactly the “plausible guess” keelson does not make. Write the two impls
(about ten lines; the unknown-variant case is
keelson_core::Error::type_mismatch), or deriveBindon a newtype over the representation. - Unions and unit structs. Nothing to bind.
- Types with a lifetime parameter.
FromValuebuilds an owned value out of aValue, so a borrowing newtype could never read back. This one is refused rather than left to the compiler for a specific reason: the bound goes in awhereclause, and rustc accepts an impl whosewhereclause can never hold — it just never applies. The derive would appear to work and then fail at the first call site, which is exactly the distant, inference-swamped failure this whole mechanism exists to replace. #[keelson(...)]options. There are none. A newtype has one field and one meaning. The wholekeelsonnamespace is refused here rather than only the unrecognised keys, so arenamethat drifted onto a newtype is caught instead of silently doing nothing. DerivingBindandFromRowon the same one-field struct still works — just leave its field attribute-free.
Each refusal is a compile error spanned at the offending item, naming the
restriction and what to do instead —
keelson-macros/tests/compile_fail/*.stderr pins the exact text.
§#[derive(FromRow)]
use std::sync::Arc;
use keelson_core::{FromRow, Value};
use keelson_exec::{Column, FromRow as _, Row};
#[derive(Debug, PartialEq, FromRow)]
struct Account {
id: i64,
// The column is `email_address`; the field is not.
#[keelson(rename = "email_address")]
email: String,
// A nullable column must be an Option, exactly as in a hand-written impl.
nickname: Option<String>,
// Read out of the same row, by the nested type's own FromRow.
#[keelson(flatten)]
audit: Audit,
}
#[derive(Debug, PartialEq, FromRow)]
struct Audit {
created_by: i64,
}
let columns: Arc<[Column]> = vec![
Column::new("id"),
Column::new("email_address"),
Column::new("nickname"),
Column::new("created_by"),
]
.into();
let mut row = Row::new(
columns,
vec![
Value::I64(1),
Value::Text("ada@example.com".into()),
Value::Null,
Value::I64(9),
],
);
assert_eq!(
Account::from_row(&mut row).unwrap(),
Account {
id: 1,
email: "ada@example.com".into(),
nickname: None,
audit: Audit { created_by: 9 },
}
);The emitted body is the shape keelson_exec::FromRow documents and
keelson-gen already emits by hand — one row.take("column")? per field, by
name. By name, not by position, so it survives column reordering and
SELECT * drift; take rather than get, so Strings and blobs move out
of the row instead of cloning. Errors keep their column name, because
Row::take puts it there.
§Field options
#[keelson(rename = "column")]— read that column instead of the one named after the field.#[keelson(flatten)]— read the field’s own type out of the same row, through itsFromRowimpl. Nested structs, in other words, and it costs one line of generated code becauseFromRow::from_rowalready takes the whole row.
§What it refuses, and why
- Tuple structs and unit structs. Mapping is by name, and unnamed
fields have none. Tuples up to arity 8 already implement
FromRowpositionally, so the alternative is to delete the struct, and the error says so with your own field types substituted in. - Enums. Which variant a row is depends on a discriminator column only you can name.
- Structs with a lifetime parameter. A row is decoded into owned
Values and every field is taken out of it by value, so a field borrowing from the row could not outlive the mapping — refused for the same “an unsatisfiablewhereclause compiles” reason as above. renametogether withflatten. One names a single column, the other reads many.- Two fields reading the same column.
takeconsumes: the value moves out and NULL is left behind, so the second field would silently decode NULL. That is a bug the derive can see, so it is a compile error rather than a mystery at runtime. prefix = "...". Deliberately not implemented, and the error says why rather than pretending the option does not exist. Stripping a prefix means rebuilding the row under different column names before handing it to the nestedFromRow; the nested impl then reports failures against the stripped names (“no column "id"” when the result set says “author_id”), and the available-columns list in the error is the stripped set too. An honest prefix needs a prefix-aware view insidekeelson_exec::Row, which is a change to the execution layer, not to a macro. Until then:flattenplusrenameon the nested fields is explicit, exact, and reports real column names.
§Getting at the derives
They are re-exported by keelson-core behind its macros feature, which is
how a user reaches them:
keelson-core = { version = "…", features = ["macros"] }use keelson_core::Bind; then imports the derive; keelson_exec::Bind
is the trait. Importing both is fine — they live in different namespaces
— and the trait is rarely named directly, since it is a blanket alias.
§What the generated code depends on
Nothing is imported into your scope, and nothing you write must be: every path the expansion names is absolute.
#[derive(Bind)]names only keelson-core (ToValue,FromValue,Value,Error).#[derive(FromRow)]names only keelson-exec (FromRow,Row,ExecError) — plus keelson-core in the one case where a bound must be written out: a generic struct, whose emittedwhereclause saysFieldTy: FromValue.
Which spelling those become is decided per call site. A crate that
depends on keelson-core or keelson-exec directly gets ::keelson_core and
::keelson_exec. A crate that depends only on the keelson facade gets
::keelson::core and ::keelson::exec, because a transitive dependency is
not in the extern prelude and the direct spelling would not resolve there.
proc_macro_crate reads the caller’s manifest to tell those apart, so a
renamed dependency works too.
This was wrong in 0.1.0, which always emitted the direct spelling and so
broke both derives for exactly the one-line dependency the facade
advertises. tests/facade-consumer is the package that now compiles from
that seat; every other context in the workspace has the inner crates for
reasons of its own and cannot catch it.