Skip to main content

prax_query/relations/
executor.rs

1//! Runtime for relation loading.
2//!
3//! Fetches children keyed by parent PK, buckets by FK, and hands back
4//! a map the per-model [`crate::traits::ModelRelationLoader::load_relation`]
5//! closure uses to stitch results onto the parent slice.
6//!
7//! The executor is the only place in the pipeline that issues a
8//! secondary query on behalf of `.include()`. It deliberately avoids
9//! JOINs so the parent hydration path stays the same as a bare
10//! `find_many()` — no row-multiplication, no column-name collisions,
11//! one network round-trip per included relation.
12
13use std::collections::HashMap;
14
15use crate::error::{QueryError, QueryResult};
16use crate::filter::{Filter, FilterValue};
17use crate::relations::RelationMeta;
18use crate::row::FromRow;
19use crate::traits::{Model, ModelWithPk, QueryEngine};
20
21/// Fetch children for a `HasMany` (or `HasOne`) relation and bucket by
22/// the FK column value turned into a stable string key.
23///
24/// Returns an empty map when `parents` is empty — the caller must
25/// short-circuit before issuing the SELECT.
26pub async fn load_has_many<E, P, C, R>(
27    engine: &E,
28    parents: &[P],
29) -> QueryResult<HashMap<String, Vec<C>>>
30where
31    E: QueryEngine,
32    P: Model + ModelWithPk,
33    C: Model + ModelWithPk + FromRow + Send + 'static,
34    R: RelationMeta<Owner = P, Target = C>,
35{
36    let pk_values: Vec<FilterValue> = parents.iter().map(|p| p.pk_value()).collect();
37    if pk_values.is_empty() {
38        return Ok(HashMap::new());
39    }
40
41    let filter = Filter::In(R::FOREIGN_KEY.into(), pk_values);
42    let dialect = engine.dialect();
43    let (where_sql, params) = filter.to_sql(0, dialect);
44    let sql = format!("SELECT * FROM {} WHERE {}", C::TABLE_NAME, where_sql);
45
46    let children: Vec<C> = engine.query_many::<C>(&sql, params).await?;
47
48    // Each bucket key is a parent PK, so the map's upper bound is the
49    // number of parents, not the number of children (children typically
50    // outnumber parents in a HasMany). Pre-sizing on parents.len()
51    // avoids the rehash chain as buckets are populated.
52    let mut out: HashMap<String, Vec<C>> = HashMap::with_capacity(parents.len());
53    for child in children {
54        let fk = child.get_column_value(R::FOREIGN_KEY).ok_or_else(|| {
55            QueryError::internal(format!(
56                "relation {}: child model missing FK column {}",
57                R::NAME,
58                R::FOREIGN_KEY
59            ))
60        })?;
61        let key = filter_value_key(&fk);
62        out.entry(key).or_default().push(child);
63    }
64    Ok(out)
65}
66
67/// Map a scalar [`FilterValue`] to a stable string key, re-exported
68/// for use inside `#[derive(Model)]`-generated code.
69///
70/// Codegen emits this call inline in the `ModelRelationLoader` match
71/// arms, so it must be publicly reachable from `::prax_query::...`.
72/// Runtime callers should prefer the inner [`filter_value_key`]
73/// helper.
74#[doc(hidden)]
75pub fn filter_value_key_public(v: &FilterValue) -> String {
76    filter_value_key(v)
77}
78
79/// Map a scalar [`FilterValue`] to a stable string key for bucketing
80/// children by their parent's PK value.
81///
82/// Single-column PKs route through the scalar variants. Composite PKs
83/// arrive here as [`FilterValue::List`] and currently panic — the
84/// relation executor does not support composite keys yet because every
85/// derived model has a single-column PK, and supporting multi-column
86/// PKs would complicate the FK column lookup on the child side.
87pub(crate) fn filter_value_key(v: &FilterValue) -> String {
88    match v {
89        FilterValue::Int(i) => i.to_string(),
90        FilterValue::String(s) => s.clone(),
91        FilterValue::Bool(b) => b.to_string(),
92        FilterValue::Float(f) => f.to_string(),
93        FilterValue::Null => "<null>".into(),
94        FilterValue::Json(v) => v.to_string(),
95        FilterValue::List(_) => {
96            panic!("relation executor does not support composite keys yet (FilterValue::List)")
97        }
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn filter_value_key_int() {
107        assert_eq!(filter_value_key(&FilterValue::Int(42)), "42");
108    }
109
110    #[test]
111    fn filter_value_key_string() {
112        assert_eq!(filter_value_key(&FilterValue::String("abc".into())), "abc");
113    }
114
115    #[test]
116    fn filter_value_key_bool() {
117        assert_eq!(filter_value_key(&FilterValue::Bool(true)), "true");
118    }
119
120    #[test]
121    fn filter_value_key_null() {
122        assert_eq!(filter_value_key(&FilterValue::Null), "<null>");
123    }
124
125    #[test]
126    #[should_panic(expected = "composite keys")]
127    fn filter_value_key_list_panics() {
128        let _ = filter_value_key(&FilterValue::List(vec![FilterValue::Int(1)]));
129    }
130}