Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,11 +517,11 @@ let users = db.query(users)
.find_many()?;

for user in &users {
println!("{}: {} posts", user.name, user.posts().len());
println!("{}: {} posts", user.name, user.posts.len());
}
```

`.find_first()` returns `Option<QueryRow<...>>` instead of `Vec`:
`.find_first()` returns `Option<...>` instead of `Vec`:

```rust
let user = db.query(users)
Expand All @@ -537,8 +537,8 @@ let users = db.query(users)
.with(users.posts().with(posts.comments()))
.find_many()?;

let first_post = &users[0].posts()[0];
println!("{} comments", first_post.comments().len());
let first_post = &users[0].posts[0];
println!("{} comments", first_post.comments.len());
```

Supports `where`, `order_by`, `limit`, and `offset` on the root query:
Expand Down Expand Up @@ -572,15 +572,15 @@ for u in &users {
Each table generates convenient type aliases for use in function signatures:

```rust
fn print_user_posts(user: &UsersQueryRow<UsersWithPosts>) {
println!("{} has {} posts", user.name, user.posts().len());
fn print_user_posts(user: &UsersWithPosts) {
println!("{} has {} posts", user.name, user.posts.len());
}
```

`UsersQueryRow<R>` is the row type returned by `db.query(users)`, parameterized over the `with(...)` shape (`UsersWithPosts` here means "include `posts`").
`UsersWithPosts` is the row type returned by `db.query(users).with(users.posts())`. Nest `*With*` types when composing multiple relations.

> [!NOTE]
> Accessing a relation on a returned row (`user.posts()`, `post.comments()`) requires the generated `Query{Table}{Relation}` trait to be in scope — import it from your schema module (e.g. `use crate::schema::{QueryUsersPosts, QueryPostsComments};`).
> Relation data is exposed as fields on the generated `*With*` row type (e.g. `user.posts`, `post.comments`). Base columns remain available via deref.

## Transactions

Expand Down
2 changes: 1 addition & 1 deletion benches/postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ fn bench_sync(c: &mut Criterion) {
.order_by([asc(user.id)])
.find_many()
.expect("relations");
let post_count = out.iter().map(|row| row.posts().len()).sum::<usize>();
let post_count = out.iter().map(|row| row.posts.len()).sum::<usize>();
black_box(post_count);
black_box(out);
},
Expand Down
2 changes: 1 addition & 1 deletion benches/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ fn bench_rusqlite(c: &mut Criterion) {
.order_by([asc(user.id)])
.find_many()
.expect("relations");
let post_count = out.iter().map(|row| row.posts().len()).sum::<usize>();
let post_count = out.iter().map(|row| row.posts.len()).sum::<usize>();
black_box(post_count);
black_box(out);
},
Expand Down
2 changes: 1 addition & 1 deletion core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ pub use pagination::PaginationArg;
pub use param::{OwnedParam, Param, ParamBind, ParamSet};
pub use placeholder::*;
#[cfg(feature = "query")]
pub use relation::{CardWrap, Many, One, OptionalOne, RelationDef};
pub use relation::{AssembleRel, CardWrap, Many, One, OptionalOne, RelationDef};
pub use relation::{Joinable, Relation, SchemaHasTable};
pub use row::{
AfterFullJoin, AfterJoin, AfterLeftJoin, AfterRightJoin, DecodeSelectedRef, ExprValueType,
Expand Down
58 changes: 55 additions & 3 deletions core/src/query/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,12 +307,12 @@ impl<'a, V: SQLParam, T: QueryTable, Rels, Cl> QueryBuilder<'a, V, T, Rels, AllC
}

// =============================================================================
// BuildStore
// BuildStore / BuildRow
// =============================================================================

/// Maps a builder's type-level relation list to a `RelEntry` storage chain.
/// Maps a builder's relation list to the JSON-decode store type.
pub trait BuildStore {
/// The concrete storage type for this relation configuration.
/// Storage type for the configured relations.
type Store;
}

Expand All @@ -338,6 +338,58 @@ where
>;
}

/// Assembles a decoded relation store into the public query row type.
///
/// With no relations, `Row` is the base select / partial-select model. Each
/// `.with(...)` wraps that row in a generated `*With*` struct that exposes the
/// relation as a named field.
pub trait BuildRow<Base>: BuildStore {
/// Row type returned by `find_many` / `find_first`.
type Row;

/// Builds the public row from a base model and decoded relation store.
fn assemble(base: Base, store: Self::Store) -> Self::Row;
}

impl<Base> BuildRow<Base> for () {
type Row = Base;

fn assemble(base: Base, (): ()) -> Self::Row {
base
}
}

impl<'a, V: SQLParam, R, Nested, Rest, Cols, Cl, Base> BuildRow<Base>
for (RelationHandle<'a, V, R, Nested, Cols, Cl>, Rest)
where
R: RelationDef + crate::relation::AssembleRel,
R::Target: QueryTable,
Cols: ResolveSelect<R::Target>,
Nested: BuildRow<<Cols as ResolveSelect<R::Target>>::Model>,
Rest: BuildRow<Base>,
Self: BuildStore<
Store = RelEntry<
R,
<R::Card as CardWrap>::Wrap<
QueryRow<<Cols as ResolveSelect<R::Target>>::Model, Nested::Store>,
>,
Rest::Store,
>,
>,
{
type Row = <R as crate::relation::AssembleRel>::Row<Rest::Row, Nested::Row>;

fn assemble(base: Base, store: Self::Store) -> Self::Row {
let (data, rest) = store.into_parts();
let inner = Rest::assemble(base, rest);
let children = R::Card::map_wrap(data, |child_row| {
let (child_base, child_store) = child_row.into_parts();
Nested::assemble(child_base, child_store)
});
R::assemble_row(inner, children)
}
}

// =============================================================================
// QueryTable
// =============================================================================
Expand Down
43 changes: 0 additions & 43 deletions core/src/query/find.rs

This file was deleted.

14 changes: 6 additions & 8 deletions core/src/query/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,29 @@
//!
//! Provides type-safe relational queries with nested relation loading.
//!
//! The pipeline: [`QueryBuilder`] configures filtering/pagination and collects
//! [`RelationHandle`]s via `.with()`. At execution time, handles are rendered
//! into SQL via [`RenderRelations`], the query is built by [`build_query_sql`],
//! and results are deserialized via [`DeserializeStore`] + [`FromJsonObject`]
//! into [`QueryRow<Base, Store>`](QueryRow) values.
//! [`QueryBuilder`] collects [`RelationHandle`]s via `.with()`. At execution
//! time, handles are rendered into SQL via [`RenderRelations`] and
//! [`build_query_sql`]. Results decode through [`DeserializeStore`] /
//! [`FromJsonObject`] and are assembled into concrete `*With*` row types by
//! [`BuildRow`].

mod builder;
mod deser;
mod find;
#[doc(hidden)]
pub mod handle;
mod row;
mod sql;
mod store;

pub use builder::{
AllColumns, BuildStore, Clauses, HasLimit, HasOffset, HasOrderBy, HasWhere,
AllColumns, BuildRow, BuildStore, Clauses, HasLimit, HasOffset, HasOrderBy, HasWhere,
IntoColumnSelection, NoLimit, NoOrderBy, NoWhere, PartialColumns, QueryBuilder, QueryTable,
ResolveSelect,
};
pub use deser::{
DeserializeStore, FromJsonColumn, FromJsonField, FromJsonObject, JsonBool, JsonObjectDecoder,
JsonOptionalBool,
};
pub use find::{FindRel, Here, There};
pub use handle::RelationHandle;
pub use row::QueryRow;
pub use sql::{RelCardinality, RenderRelations, RenderedRelation, build_query_sql};
Expand Down
8 changes: 4 additions & 4 deletions core/src/query/row.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
//! `QueryRow<Base, Store>` — wrapper for a row with relation data.
//! `QueryRow<Base, Store>` — nested relation JSON decode target.

use core::ops::Deref;

/// A query result row that wraps a base select model with optional relation data.
/// A base select model paired with a decoded relation store.
///
/// Users access base fields via `Deref` (transparent) and relation data
/// via extension trait methods generated by the table macro.
/// Used while deserializing nested relation JSON columns. Public query APIs
/// assemble this into generated `*With*` row structs via [`super::BuildRow`].
#[derive(Debug, Clone)]
pub struct QueryRow<Base, Store = ()> {
base: Base,
Expand Down
14 changes: 7 additions & 7 deletions core/src/query/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@

use core::marker::PhantomData;

/// Stores one relation's data plus the rest of the chain.
/// One loaded relation's data plus the remaining store chain.
///
/// A `QueryRow` with two included relations has store type:
/// ```rust
/// # let _ = r####"
/// RelEntry<RelPosts, Vec<SelectPost>, RelEntry<RelInvitedBy, Option<SelectUser>, ()>>
/// # "####;
/// ```
/// Built during JSON column decode and consumed by [`super::BuildRow`].
#[derive(Debug, Clone)]
pub struct RelEntry<Rel, Data, Rest> {
pub(crate) data: Data,
Expand All @@ -26,4 +21,9 @@ impl<Rel, Data, Rest> RelEntry<Rel, Data, Rest> {
_rel: PhantomData,
}
}

/// Splits this entry into its relation data and the remaining chain.
pub(crate) fn into_parts(self) -> (Data, Rest) {
(self.data, self.rest)
}
}
31 changes: 31 additions & 0 deletions core/src/relation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,25 @@ pub trait CardWrap: private::Sealed {
/// The cardinality for runtime SQL generation decisions.
const CARDINALITY: crate::query::RelCardinality;
type Wrap<T>;

/// Maps the wrapped value from `A` to `B`.
fn map_wrap<A, B>(data: Self::Wrap<A>, f: impl FnMut(A) -> B) -> Self::Wrap<B>;
}

/// Constructs the public with-row type for a loaded relation.
///
/// Implemented by table macros for each relation definition. `Row<Inner, Child>`
/// is the generated struct (for example `UsersWithPosts<Inner, Child>`).
#[cfg(feature = "query")]
pub trait AssembleRel: RelationDef {
/// Row type produced when this relation is included in a query.
type Row<Inner, Child>;

/// Builds a with-row from the composed inner row and this relation's data.
fn assemble_row<Inner, Child>(
inner: Inner,
data: <Self::Card as CardWrap>::Wrap<Child>,
) -> Self::Row<Inner, Child>;
}

/// Many-cardinality: wraps data as `Vec<T>`.
Expand Down Expand Up @@ -97,18 +116,30 @@ impl private::Sealed for OptionalOne {}
impl CardWrap for Many {
const CARDINALITY: crate::query::RelCardinality = crate::query::RelCardinality::Many;
type Wrap<T> = crate::prelude::Vec<T>;

fn map_wrap<A, B>(data: Self::Wrap<A>, f: impl FnMut(A) -> B) -> Self::Wrap<B> {
data.into_iter().map(f).collect()
}
}

#[cfg(feature = "query")]
impl CardWrap for One {
const CARDINALITY: crate::query::RelCardinality = crate::query::RelCardinality::One;
type Wrap<T> = T;

fn map_wrap<A, B>(data: Self::Wrap<A>, mut f: impl FnMut(A) -> B) -> Self::Wrap<B> {
f(data)
}
}

#[cfg(feature = "query")]
impl CardWrap for OptionalOne {
const CARDINALITY: crate::query::RelCardinality = crate::query::RelCardinality::OptionalOne;
type Wrap<T> = Option<T>;

fn map_wrap<A, B>(data: Self::Wrap<A>, f: impl FnMut(A) -> B) -> Self::Wrap<B> {
data.map(f)
}
}

#[cfg(feature = "query")]
Expand Down
5 changes: 2 additions & 3 deletions examples/orm-comparison/drizzle/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ use drizzle::sqlite::prelude::*;
use drizzle::sqlite::rusqlite::Drizzle;

use schema::{
InsertComments, InsertPosts, InsertUsers, Posts, QueryUsersPosts, Schema, SelectUsers,
UpdateUsers, Users,
InsertComments, InsertPosts, InsertUsers, Posts, Schema, SelectUsers, UpdateUsers, Users,
};

fn main() -> drizzle::Result<()> {
Expand Down Expand Up @@ -72,7 +71,7 @@ fn main() -> drizzle::Result<()> {
println!("--- relations ---");
let loaded = db.query(users).with(users.posts()).find_many()?;
for u in &loaded {
println!("{}: {} posts", u.name, u.posts().len());
println!("{}: {} posts", u.name, u.posts.len());
}

Ok(())
Expand Down
9 changes: 4 additions & 5 deletions examples/readme_smoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
use drizzle::sqlite::rusqlite::Drizzle;

use readme::{
InsertComments, InsertPosts, InsertUsers, Posts, QueryPostsComments, QueryUsersPosts,
Schema, SelectUsers, UpdateUsers, Users,
InsertComments, InsertPosts, InsertUsers, Posts, Schema, SelectUsers, UpdateUsers, Users,
};

// -------- 4. Connect & Query --------
Expand Down Expand Up @@ -254,7 +253,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// -------- Relational Queries --------
let user_rows = db.query(users).with(users.posts()).find_many()?;
for u in &user_rows {
let _ = u.posts().len();
let _ = u.posts.len();
}

let _found = db
Expand All @@ -268,7 +267,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.with(users.posts().with(posts.comments()))
.find_many()?;
if let Some(first) = nested.first() {
let _ = first.posts().first().map(|p| p.comments().len());
let _ = first.posts.first().map(|p| p.comments.len());
}

let _paged = db
Expand All @@ -290,7 +289,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}

// Type aliases used in function signatures (no body needed — just verify they exist).
fn _consume(_: &readme::UsersQueryRow<readme::UsersWithPosts>) {}
fn _consume(_: &readme::UsersWithPosts) {}

// -------- Transactions --------
use drizzle::sqlite::connection::SQLiteTransactionType;
Expand Down
Loading
Loading