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    let mut out: HashMap<String, Vec<C>> = HashMap::new();
49    for child in children {
50        let fk = child.get_column_value(R::FOREIGN_KEY).ok_or_else(|| {
51            QueryError::internal(format!(
52                "relation {}: child model missing FK column {}",
53                R::NAME,
54                R::FOREIGN_KEY
55            ))
56        })?;
57        let key = filter_value_key(&fk);
58        out.entry(key).or_default().push(child);
59    }
60    Ok(out)
61}
62
63/// Map a scalar [`FilterValue`] to a stable string key, re-exported
64/// for use inside `#[derive(Model)]`-generated code.
65///
66/// Codegen emits this call inline in the `ModelRelationLoader` match
67/// arms, so it must be publicly reachable from `::prax_query::...`.
68/// Runtime callers should prefer the inner [`filter_value_key`]
69/// helper.
70#[doc(hidden)]
71pub fn filter_value_key_public(v: &FilterValue) -> String {
72    filter_value_key(v)
73}
74
75/// Map a scalar [`FilterValue`] to a stable string key for bucketing
76/// children by their parent's PK value.
77///
78/// Single-column PKs route through the scalar variants. Composite PKs
79/// arrive here as [`FilterValue::List`] and currently panic — the
80/// relation executor does not support composite keys yet because every
81/// derived model has a single-column PK, and supporting multi-column
82/// PKs would complicate the FK column lookup on the child side.
83pub(crate) fn filter_value_key(v: &FilterValue) -> String {
84    match v {
85        FilterValue::Int(i) => i.to_string(),
86        FilterValue::String(s) => s.clone(),
87        FilterValue::Bool(b) => b.to_string(),
88        FilterValue::Float(f) => f.to_string(),
89        FilterValue::Null => "<null>".into(),
90        FilterValue::Json(v) => v.to_string(),
91        FilterValue::List(_) => {
92            panic!("relation executor does not support composite keys yet (FilterValue::List)")
93        }
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn filter_value_key_int() {
103        assert_eq!(filter_value_key(&FilterValue::Int(42)), "42");
104    }
105
106    #[test]
107    fn filter_value_key_string() {
108        assert_eq!(filter_value_key(&FilterValue::String("abc".into())), "abc");
109    }
110
111    #[test]
112    fn filter_value_key_bool() {
113        assert_eq!(filter_value_key(&FilterValue::Bool(true)), "true");
114    }
115
116    #[test]
117    fn filter_value_key_null() {
118        assert_eq!(filter_value_key(&FilterValue::Null), "<null>");
119    }
120
121    #[test]
122    #[should_panic(expected = "composite keys")]
123    fn filter_value_key_list_panics() {
124        let _ = filter_value_key(&FilterValue::List(vec![FilterValue::Int(1)]));
125    }
126}