keelson_models/load.rs
1//! Then-loading: the second query, and the third, and the fourth.
2//!
3//! A one-level then-load fetches a relation with one keyed query instead of
4//! one query per parent row. The level below it — the relation *of* that
5//! relation — is where the N+1 problem actually bites, so a then-load is not a
6//! closure here but a value, [`ThenLoad`], that another then-load can be
7//! attached to.
8//!
9//! # The decisions, recorded
10//!
11//! **Path syntax: chained values, not strings.** A path is written by hanging
12//! one generated relation mod off another:
13//!
14//! ```ignore
15//! posts::table().query(
16//! posts::then_load::user() // posts → author
17//! .then(users::then_load::posts()), // → the author's posts
18//! )
19//! ```
20//!
21//! There is no `"user.team"` string anywhere. `then` takes an
22//! [`IntoLoader<C::Row>`](IntoLoader) — a loader for *this* level's child
23//! model — so `posts::then_load::user().then(posts::then_load::user())` does
24//! not compile: the inner one loads onto `Post`s, and this level hands it
25//! `User`s. A misspelt or mis-rooted path is a type error at the call site,
26//! and each level is still an ordinary mod that can be used alone.
27//!
28//! Several relations at one level are several `then` calls
29//! (`.then(a).then(b)`); they run in order, each over the same child set.
30//!
31//! **One batched `IN` query per level, not a join.** Level *n* runs after
32//! level *n-1* has its rows, keyed by exactly the keys those rows carry:
33//! `SELECT … WHERE "users"."id" IN ($1, …)`. The alternative — widening the
34//! parent's `LEFT JOIN` chain — was rejected for to-many relations because a
35//! join multiplies the parent rows by every child (and by every grandchild
36//! again at the next level), so the wire cost of a three-level path grows as
37//! the product of the fan-outs while the batched form stays additive. It is
38//! also the only shape that works uniformly: a to-many level cannot be
39//! decoded out of a widened row set without a group-by pass, while a batched
40//! level is the same code for both cardinalities. The cost of the choice is
41//! one round trip per level (plus one per batch, below) — an explicit,
42//! bounded number of queries, which the specs assert exactly so a regression
43//! to N+1 fails the test.
44//!
45//! Same-query `preload` (the to-one `LEFT JOIN`) is the exception that keeps
46//! its shape: it costs no query at all. It has no `then`, deliberately — its
47//! children exist only as per-parent copies inside the parent rows, so a
48//! level below it would have to re-derive the distinct child set that the
49//! join had already dissolved. Spell that path `then_load::user().then(…)`
50//! instead; it is one query, and it is the query the level below needs
51//! anyway. The compile error is that `preload::user()` has no `then` method.
52//!
53//! **Batching: [`KEY_BATCH`] keys per query.** An unbounded `IN` list is a
54//! real failure mode — PostgreSQL's and MySQL's wire protocols both cap a
55//! statement at 65535 bind parameters, and SQLite built before 3.32 caps
56//! `SQLITE_MAX_VARIABLE_NUMBER` at 999 — so the distinct keys of a level are
57//! chunked and one query runs per chunk. [`KEY_BATCH`] is 900: under the
58//! oldest of those limits, with room left for whatever arguments the caller's
59//! own mods put in the same statement. [`ThenLoad::batch`] overrides it per
60//! level. The children of every chunk are concatenated before the next level
61//! runs, so batching costs queries at *this* level only — it does not
62//! multiply the levels below it.
63//!
64//! **Deduplication.** Keys are sorted and deduplicated before the query, so
65//! two posts by the same author put that author in the `IN` list once, fetch
66//! it once, and — because the next level runs over the fetched child set
67//! rather than over the parents — load *its* relations once. The author
68//! arrives in both posts by [`attach_to_one`]'s clone, grandchildren
69//! included. Sorting is not only for the `dedup`: it makes the argument list
70//! deterministic, so the emitted SQL of a given result set is stable enough
71//! to judge.
72//!
73//! **Cycles terminate because a path is a finite value.** Nothing here walks
74//! a relation graph; `then` builds a list, and the list has the length the
75//! caller wrote. A cyclic path is legal and terminates at the depth it was
76//! written to:
77//!
78//! ```ignore
79//! posts::then_load::user() // 1 query: the users
80//! .then(users::then_load::posts() // 1 query: their posts
81//! .then(posts::then_load::user())) // 1 query: those posts' users
82//! ```
83//!
84//! — four queries in total including the caller's own, then it stops. The
85//! same is true of a self-referential relation (`posts.parent_id → posts`):
86//! `parent().then(parent())` is two levels because it says two levels. There
87//! is no "load until it stops changing" mode to run away, and no depth
88//! counter is needed to stop one.
89//!
90//! **Ordering within a level.** The child model's `after_select` runs per
91//! batch, as part of the batch's own `all()`, and therefore *before* the
92//! deeper levels load; a hook on the child model sees its own rows with
93//! `rel` still empty. The alternative — deferring it until the whole subtree
94//! is loaded — would make a two-level query run the hook at a different point
95//! than a one-level query does, which is the worse surprise.
96
97use std::collections::HashMap;
98use std::hash::Hash;
99use std::sync::Arc;
100
101use keelson_core::Mod;
102use keelson_exec::{ExecError, Executor};
103
104use crate::select::{Loader, ModelSelect};
105use crate::{ModelTable, View};
106
107/// How many keys one level's keyed query may carry.
108///
109/// The binding constraint is the oldest SQLite default
110/// (`SQLITE_MAX_VARIABLE_NUMBER` = 999 before 3.32); PostgreSQL and MySQL cap
111/// a statement at 65535 parameters. 900 sits under all three with room for
112/// the caller's own arguments in the same statement. Override per level with
113/// [`ThenLoad::batch`].
114pub const KEY_BATCH: usize = 900;
115
116/// A re-appliable query shaper ([`ThenLoad::with`]). Not a [`Mod`]: a mod is
117/// consumed when it is applied, and a level applies its shape to every batch
118/// query, every time the parent query runs.
119type Shape<C> = Arc<dyn Fn(&mut ModelSelect<C>) + Send + Sync>;
120
121/// Anything that can act as one level of a load path over `T`.
122///
123/// Implemented by [`ThenLoad`] — which is what the generated relation mods
124/// return — and by a bare [`Loader<T>`], so a hand-written loader nests
125/// exactly like a generated one.
126pub trait IntoLoader<T> {
127 /// The loader payload this level runs.
128 fn into_loader(self) -> Loader<T>;
129}
130
131impl<T> IntoLoader<T> for Loader<T> {
132 fn into_loader(self) -> Loader<T> {
133 self
134 }
135}
136
137/// One level of a load path: fetch `C` for a set of `P`, keyed, batched and
138/// deduplicated — plus the levels hanging off it.
139///
140/// Generated `then_load::…()` functions return one of these. It is a
141/// [`Mod`] over the parent's [`ModelSelect`], so it drops into a query tuple
142/// like any other mod; [`then`](ThenLoad::then) is what makes it a path
143/// rather than a single level.
144///
145/// The three function pointers are the model-specific half — which keys to
146/// take off the parents, how to filter the child query by them, how to
147/// attach the results — and they are function pointers rather than closures
148/// because generated code has nothing to capture, which keeps this type
149/// `Send + Sync` without a bound in sight.
150pub struct ThenLoad<P: View, C: View, K> {
151 keys: fn(&[P::Row]) -> Vec<K>,
152 key_filter: fn(Vec<K>, &mut ModelSelect<C>),
153 attach: fn(&mut [P::Row], Vec<C::Row>),
154 shape: Vec<Shape<C>>,
155 nested: Vec<Loader<C::Row>>,
156 batch: usize,
157}
158
159impl<P: View, C: View, K> std::fmt::Debug for ThenLoad<P, C, K> {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 f.debug_struct("ThenLoad")
162 .field("shape", &self.shape.len())
163 .field("nested", &self.nested.len())
164 .field("batch", &self.batch)
165 .finish()
166 }
167}
168
169impl<P, C, K> ThenLoad<P, C, K>
170where
171 P: View,
172 C: View,
173 K: Ord + Clone + Send + Sync + 'static,
174{
175 /// Assemble a level. Generated code calls this; the three arguments are
176 /// the parent keys, the child-query filter and the attachment.
177 pub fn new(
178 keys: fn(&[P::Row]) -> Vec<K>,
179 key_filter: fn(Vec<K>, &mut ModelSelect<C>),
180 attach: fn(&mut [P::Row], Vec<C::Row>),
181 ) -> Self {
182 ThenLoad {
183 keys,
184 key_filter,
185 attach,
186 shape: Vec::new(),
187 nested: Vec::new(),
188 batch: KEY_BATCH,
189 }
190 }
191
192 /// Hang another level off this one: load `deeper` over the children this
193 /// level fetched, before they are attached to their parents.
194 ///
195 /// Typed by the child model — only a loader over `C::Row` fits — so the
196 /// path is checked at the call site rather than spelled in a string.
197 #[must_use]
198 pub fn then(mut self, deeper: impl IntoLoader<C::Row>) -> Self {
199 self.nested.push(deeper.into_loader());
200 self
201 }
202
203 /// Shape this level's query: the closure runs on **every batch query**,
204 /// after the key filter.
205 ///
206 /// A closure rather than a mod because a mod is consumed when it is
207 /// applied and there may be many batches — and because a query is
208 /// re-issued every time the parent query runs. Anything that applies to a
209 /// `ModelSelect` goes in it: a filter, an order, a Layer 1 mod, or a
210 /// `preload` of the child's own to-one relation.
211 ///
212 /// ```ignore
213 /// posts::then_load::user()
214 /// .with(|q| users::is_active().eq(true).apply(q))
215 /// ```
216 #[must_use]
217 pub fn with(mut self, shape: impl Fn(&mut ModelSelect<C>) + Send + Sync + 'static) -> Self {
218 self.shape.push(Arc::new(shape));
219 self
220 }
221
222 /// Override [`KEY_BATCH`] for this level.
223 ///
224 /// # Panics
225 ///
226 /// If `keys` is zero: a batch of no keys makes no progress, and silently
227 /// substituting a size would hide the caller's mistake.
228 #[must_use]
229 #[track_caller]
230 pub fn batch(mut self, keys: usize) -> Self {
231 assert!(keys > 0, "then-load batch size must be at least 1");
232 self.batch = keys;
233 self
234 }
235
236 async fn run(&self, db: &dyn Executor, parents: &mut [P::Row]) -> Result<(), ExecError> {
237 let keys = distinct((self.keys)(parents));
238 if keys.is_empty() {
239 return Ok(());
240 }
241 let mut children: Vec<C::Row> = Vec::new();
242 for chunk in keys.chunks(self.batch) {
243 let mut q = ModelTable::<C>::new().query(());
244 (self.key_filter)(chunk.to_vec(), &mut q);
245 for shape in &self.shape {
246 shape(&mut q);
247 }
248 children.extend(q.all(db).await?);
249 }
250 // The deeper levels run over the distinct children, once — not per
251 // batch and not per parent, which is what makes a shared child load
252 // its own relations exactly once.
253 for deeper in &self.nested {
254 deeper(db, &mut children).await?;
255 }
256 (self.attach)(parents, children);
257 Ok(())
258 }
259}
260
261/// The keys of one level, deduplicated and ordered: each key appears in the
262/// `IN` list once, and the list is deterministic so the emitted SQL of a
263/// given result set is stable.
264fn distinct<K: Ord>(mut keys: Vec<K>) -> Vec<K> {
265 keys.sort_unstable();
266 keys.dedup();
267 keys
268}
269
270impl<P, C, K> IntoLoader<P::Row> for ThenLoad<P, C, K>
271where
272 P: View,
273 C: View,
274 K: Ord + Clone + Send + Sync + 'static,
275{
276 fn into_loader(self) -> Loader<P::Row> {
277 let level = Arc::new(self);
278 Arc::new(move |db, rows: &mut Vec<P::Row>| {
279 let level = Arc::clone(&level);
280 Box::pin(async move { level.run(db, rows).await })
281 })
282 }
283}
284
285impl<P, C, K> Mod<ModelSelect<P>> for ThenLoad<P, C, K>
286where
287 P: View,
288 C: View,
289 K: Ord + Clone + Send + Sync + 'static,
290{
291 fn apply(self, q: &mut ModelSelect<P>) {
292 q.add_loader(self.into_loader());
293 }
294}
295
296/// Attach a to-one relation: each parent gets the child whose key matches, or
297/// `None`. Children are cloned only where several parents share one.
298///
299/// The child arrives here unboxed; the generated `attach` closure is what
300/// writes it into the row's `Option<Box<_>>` field (`= c.map(Box::new)`), so
301/// the `Box` is one move per attached parent and changes neither how many
302/// children are fetched nor how many are cloned.
303pub fn attach_to_one<P, C, K>(
304 parents: &mut [P],
305 children: Vec<C>,
306 parent_key: impl Fn(&P) -> K,
307 child_key: impl Fn(&C) -> K,
308 mut attach: impl FnMut(&mut P, Option<C>),
309) where
310 K: Eq + Hash,
311 C: Clone,
312{
313 let by_key: HashMap<K, C> = children.into_iter().map(|c| (child_key(&c), c)).collect();
314 for p in parents {
315 let child = by_key.get(&parent_key(p)).cloned();
316 attach(p, child);
317 }
318}
319
320/// Attach a to-many relation: each parent gets every child whose key matches.
321///
322/// Each child is attached exactly once — parents are assumed key-distinct,
323/// which they are when the key is the parent's primary key (the shape every
324/// generated then-load has).
325pub fn attach_to_many<P, C, K>(
326 parents: &mut [P],
327 children: Vec<C>,
328 parent_key: impl Fn(&P) -> K,
329 child_key: impl Fn(&C) -> K,
330 mut attach: impl FnMut(&mut P, Vec<C>),
331) where
332 K: Eq + Hash,
333{
334 let mut by_key: HashMap<K, Vec<C>> = HashMap::new();
335 for c in children {
336 by_key.entry(child_key(&c)).or_default().push(c);
337 }
338 for p in parents {
339 let own = by_key.remove(&parent_key(p)).unwrap_or_default();
340 attach(p, own);
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[derive(Debug, PartialEq)]
349 struct Parent {
350 id: i32,
351 children: Vec<i32>,
352 one: Option<i32>,
353 }
354
355 fn parents() -> Vec<Parent> {
356 (1..=3)
357 .map(|id| Parent {
358 id,
359 children: Vec::new(),
360 one: None,
361 })
362 .collect()
363 }
364
365 #[test]
366 fn to_many_groups_by_key_and_leaves_misses_empty() {
367 let mut ps = parents();
368 // (child id, parent id): parent 1 has two, parent 3 none.
369 let children = vec![(10, 1), (11, 1), (20, 2)];
370 attach_to_many(
371 &mut ps,
372 children,
373 |p| p.id,
374 |c| c.1,
375 |p, cs| p.children = cs.into_iter().map(|c| c.0).collect(),
376 );
377 assert_eq!(ps[0].children, vec![10, 11]);
378 assert_eq!(ps[1].children, vec![20]);
379 assert_eq!(ps[2].children, Vec::<i32>::new());
380 }
381
382 #[test]
383 fn to_one_attaches_a_match_or_none_and_shares_children() {
384 let mut ps = parents();
385 ps[1].id = 1; // two parents share the same key
386 let children = vec![(100, 1)];
387 attach_to_one(
388 &mut ps,
389 children,
390 |p| p.id,
391 |c| c.1,
392 |p, c| p.one = c.map(|c| c.0),
393 );
394 assert_eq!(ps[0].one, Some(100));
395 assert_eq!(ps[1].one, Some(100), "a shared child is cloned, not stolen");
396 assert_eq!(ps[2].one, None);
397 }
398
399 /// The deduplication contract: the `IN` list carries each key once, in a
400 /// deterministic order.
401 #[test]
402 fn keys_are_deduplicated_and_ordered() {
403 assert_eq!(distinct(vec![3, 1, 3, 2, 1, 1]), vec![1, 2, 3]);
404 assert_eq!(distinct(Vec::<i32>::new()), Vec::<i32>::new());
405 }
406
407 /// The batching boundary — `distinct(keys).chunks(batch)` is exactly what
408 /// `run` iterates, so this counts the queries one level will issue. The
409 /// live specs prove a real engine sees the same number.
410 #[test]
411 fn keys_batch_at_the_boundary() {
412 let queries = |n: usize, batch: usize| {
413 distinct((0..n as i32).collect::<Vec<_>>())
414 .chunks(batch)
415 .count()
416 };
417 assert_eq!(queries(KEY_BATCH - 1, KEY_BATCH), 1);
418 assert_eq!(
419 queries(KEY_BATCH, KEY_BATCH),
420 1,
421 "the cap itself is one query"
422 );
423 assert_eq!(
424 queries(KEY_BATCH + 1, KEY_BATCH),
425 2,
426 "one over the cap is two"
427 );
428 assert_eq!(queries(2 * KEY_BATCH, KEY_BATCH), 2);
429 assert_eq!(queries(2 * KEY_BATCH + 1, KEY_BATCH), 3);
430 // An overridden batch behaves the same way — which is what makes the
431 // boundary cheap to test against a live engine.
432 assert_eq!(queries(4, 2), 2);
433 assert_eq!(queries(5, 2), 3);
434 // Duplicates cost nothing: 900 rows sharing 3 keys are one query.
435 let mut dup = vec![1; KEY_BATCH * 2];
436 dup.extend([2, 3]);
437 assert_eq!(distinct(dup).chunks(KEY_BATCH).count(), 1);
438 }
439}