Skip to main content

Crate keelson_models

Crate keelson_models 

Source
Expand description

The typed model layer — the runtime the code generator will emit against.

Nothing is generated yet: this crate is the machinery (View/Table, the Set three-state setter, hooks, preload/then-load plumbing), and the hand-written users/posts model in tests/ is the generator’s specification — what it will write, written once by hand and tested end to end. The call-site shape being served:

use models::users;

let adults = users::table().query((
    users::age().gte(21),   // typed: passing &str is a compile error
    select::limit(20),      // Layer 1 mods mix in directly
)).all(&db).await?;

let u = users::table().insert(users::Setter {
    name: set("Stephen"),
    ..Default::default()
}).one(&db).await?;

§Where this sits

Layer 3 of keelson: the runtime a generated model is written against. It stands on Layer 1 (the dialect crates, through keelson-core’s clause traits) and Layer 2 (keelson-exec, through &dyn Executor), and names no dialect and no driver itself. The models that use it are written by Layer 4, keelson-gen — you can write one by hand, and the tests/ in this crate are exactly that. Test data for those models is keelson-factory. The whole map is the keelson facade crate.

§The decisions, recorded

One column entry point. bob splits a column across four generated surfaces (ColumnNames/Columns/SelectWhere/Preload); here users::age() is one Column<i32> that is the expression, the typed filter origin and the alias carrier at once. The column’s Rust type comes from docs/type-mappings.md; comparisons take impl Into<T>, so age().gte(21) compiles and age().gte("x") does not (pinned by a compile_fail doctest on Column::eq).

Setter three states by type. Set<T> is Unset | Null | Value(T) with Default = Unset, built by set/null; an unset field does not appear in the statement at all. Null stays representable on NOT NULL columns — the constraint is the engine’s to enforce, as it is for raw SQL.

Hooks are trait default methods on the model marker — static dispatch, no downcasting, the deliberate departure from bob’s runtime type-assertion opt-in. before/after insert/update/delete, after select (there is no before-select: a query mod at the same call site already is that hook). Every hook receives &dyn Executor — the caller’s own executor — so hooks run inside the caller’s transaction and cannot end it. The before-mutation hooks receive the Setter mutably.

QueryExtensions, wired shut. Core fixed the mechanism with type-parameter payloads; keelson-exec pinned Hook = ExecHook; the remaining two are pinned here, where the row-mapper lives: MapperMod = MapperMod<T> (same-query preloads decode prefixed columns into the already-mapped struct) and Loader = Loader<T> (typed over the model, not ExecLoader’s &[Row], because a then-loader’s job is to mutate decoded structs). ModelSelect implements QueryExtensions<ExecHook, Loader<_>, MapperMod<_>> and its verbs consume the extensions through that trait.

Loaders. Preload is a same-query LEFT JOIN for to-one relations: the generated mod joins, appends prefixed columns through the dialect’s preload_columns (kept apart from the caller’s projection by SelectList, which was designed for this), and registers a mapper mod that reads "user.id"-style columns back — Row’s by-name access is what makes the prefix trick work. Then-load is a second query keyed by the first’s keys, to-one and to-many, attached by attach_to_one/attach_to_many.

Nested loads are chained values, not paths in a string. A then-load is a ThenLoad, and another one hangs off it: posts::then_load::user().then(users::then_load::posts()) is posts → author → the author’s posts in three queries, checked by the compiler (the inner level must load onto this level’s child model). One batched IN query per level, KEY_BATCH keys at a time, over the deduplicated child set — the design and its alternatives are recorded in load.rs.

Relation field naming: rel, not bob’s r. The row struct carries post.rel.user / user.rel.posts. r is a Go-ism (single-letter receivers are idiomatic there; in Rust a one-letter public field reads as an accident), rel is greppable, self-describing, and still two characters shorter than related. The generated mod modules follow the design vocabulary: posts::preload::user(), posts::then_load::user().

Relation field shape: Option<Box<Row>> to-one, Vec<Row> to-many. A Rel field holds the target’s whole row, so two models whose to-one relations point at each other would be a recursive type of infinite size — post.rel.user: Option<User> next to user.rel.featured_post: Option<Post> does not compile. Every to-one field is therefore boxed, uniformly, rather than only the ones a schema’s cycles happen to need: a field’s type must not depend on the rest of the schema graph, or adding an unrelated foreign key would silently change it. A to-many field is a Vec and already carries its own indirection. Reading is unaffected (post.rel.user.as_ref().unwrap().name derefs through the Box); building one by hand is Some(Box::new(row)). The rejected alternative and the whole argument are recorded in keelson-gen/src/emit/model.rs.

View vs Table. View is SELECT-only and needs no primary key; Table adds insert/update/delete and requires one. The mutations are bounded on Table, so calling insert on a view model is a compile error, not a runtime one.

Layer 1 interop is structural, not special-cased. The wrappers (ModelSelect, ModelUpdate, ModelDelete) implement every Has* clause trait their statement implements, so the dialect’s shared mods — and any raw &str fragment those mods accept — apply to the wrapper directly, in the same tuple as typed filters. Statement-specific mods (psql’s select::distinct()) go through each wrapper’s apply; INSERT mods ride ModelInsert::with, deferred because the statement is only built after before_insert has seen the setter.

Verbs. all/one/optional on select (one means one: RowNotFound/TooManyRows, matching the execution layer); one/exec on insert; exec/all on update and delete, where all decodes whatever RETURNING the statement carries. The statement itself always goes through keelson-exec’s traced verb funnel (fetch_rows/execute), so model queries appear in telemetry like every other query.

§Per-dialect notes

The machinery is dialect-generic — it names only keelson-core’s Has* traits and keelson-exec’s Executor. What diverges lives in the generated (here: hand-written) model:

  • PostgreSQL (the demonstration dialect): RETURNING carries insert(...).one() and update/delete(...).all(); an all-unset setter renders INSERT INTO t DEFAULT VALUES.
  • SQLite: identical shapes (SQLite has RETURNING since 3.35 and DEFAULT VALUES); timestamptz columns are TEXT, and a column whose default writes the naive CURRENT_TIMESTAMP form is honestly typed NaiveDateTime by the schema-reading generator.
  • MySQL: no RETURNING anywhere. A generated MySQL model backs insert(...).one() with ExecResult::last_insert_id plus a keyed re-SELECT, and offers no update/delete(...).all(); an all-unset setter is spelled INSERT INTO t () VALUES (). The Table trait’s *_query seam is per-model precisely so these differences stay inside the generator’s output.

Structs§

Column
A model’s column: the one entry point for everything a column is used for.
Filter
A typed condition on its way to a WHERE.
ModelDelete
A pending model DELETE.
ModelInsert
A pending model INSERT: the three-state setter, held unbuilt so Table::before_insert can still rewrite it once an executor is in hand.
ModelSelect
A model SELECT: the dialect statement plus the extension payloads the query carries — hooks, preload mapper mods, then-loaders.
ModelTable
The model’s entry point — what users::table() (or, for a SELECT-only model, reports::view()) returns.
ModelUpdate
A pending model UPDATE: the statement (which filters and mods have already landed on) plus the setter, joined together only at verb time so Table::before_update sees the setter first.
ThenLoad
One level of a load path: fetch C for a set of P, keyed, batched and deduplicated — plus the levels hanging off it.

Enums§

Set
One field of a generated Setter: unset, NULL, or a value — three states, distinguished by type.

Constants§

KEY_BATCH
How many keys one level’s keyed query may carry.

Traits§

IntoLoader
Anything that can act as one level of a load path over T.
Table
A writable model: a View with a primary key and the three mutations.
View
A readable model: enough to SELECT and map rows. No primary key required — a database view, a reporting projection, a read-only slice of a table are all Views. Table adds the mutations.

Functions§

attach_to_many
Attach a to-many relation: each parent gets every child whose key matches.
attach_to_one
Attach a to-one relation: each parent gets the child whose key matches, or None. Children are cloned only where several parents share one.
hook
Wrap a closure as an ExecHook. The named-function-plus-Box::pin shape generated code uses:
loader
Wrap a closure as a Loader.
mapper_mod
Wrap a closure as a MapperMod.
null
An explicit SQL NULLSet::Null, spelled the way the sketch spells it.
set
A set field: set("Stephen") is Set::Value("Stephen".into()).

Type Aliases§

Loader
The then-load payload, pinned: runs after the rows are mapped, with the caller’s executor and the decoded models, so a second query can be keyed by the first’s keys and its results attached to the rel fields.
MapperMod
The mapper-mod payload, pinned: after the base row struct is decoded, each mapper mod reads more of the same Row into the struct — the prefixed preload columns a same-query LEFT JOIN added.