Skip to main content

rust_ef/
navigation_loader.rs

1//! Eager-loading of navigation properties via secondary queries.
2
3use crate::entity::{IEntitySnapshot, IEntityType, IFromRow, IGetKeyValues, INavigationSetter};
4use crate::error::{EFError, EFResult};
5use crate::metadata::{EntityTypeMeta, NavigationKind, NavigationMeta};
6use crate::provider::{DbValue, DbValueKey, IDatabaseProvider, ISqlGenerator};
7use crate::query::{compile_bool_expr, CompiledFilter, IncludePath};
8use std::collections::{HashMap, HashSet};
9
10/// Appends a query filter (e.g. tenant_id = ?) to a navigation SQL statement.
11///
12/// Uses the pre-collected `params` from `CompiledFilter` (collected once at
13/// registration time) instead of traversing the `BoolExpr` tree per query.
14/// The SQL fragment is still compiled per query because placeholder numbering
15/// is dialect-specific and depends on the current parameter count.
16fn apply_filter_to_sql(
17    sql: &mut String,
18    params: &mut Vec<DbValue>,
19    related_table: &str,
20    filter_map: Option<&HashMap<String, CompiledFilter>>,
21    gen: &dyn ISqlGenerator,
22) {
23    if let Some(filter) = filter_map.and_then(|m| m.get(related_table)) {
24        let mut idx = params.len() + 1;
25        let filter_sql = compile_bool_expr(&filter.expr, gen, &mut idx);
26        params.extend(filter.params.iter().cloned());
27        *sql = format!("{} AND ({})", sql, filter_sql);
28    }
29}
30
31/// Builds include paths from navigation field names using entity metadata.
32pub fn include_paths_for<T: IEntityType, S: AsRef<str>>(
33    navigation_names: &[S],
34) -> Vec<IncludePath> {
35    let meta = T::entity_meta();
36    navigation_names
37        .iter()
38        .filter_map(|name| include_path_from_meta(&meta, name.as_ref()))
39        .collect()
40}
41
42fn include_path_from_meta(meta: &EntityTypeMeta, navigation: &str) -> Option<IncludePath> {
43    let nav_meta = meta.find_navigation(navigation)?;
44    Some(IncludePath {
45        navigation: navigation.to_string(),
46        nested: Vec::new(),
47        related_table: nav_meta.related_table.as_ref().map(|s| s.to_string()),
48        foreign_key_column: nav_meta.fk_column.as_ref().map(|s| s.to_string()),
49        referenced_key_column: nav_meta
50            .referenced_key_column
51            .as_ref()
52            .map(|s| s.to_string()),
53    })
54}
55
56/// Loads related entities and fills navigation properties on `entities`.
57///
58/// `filter_map` carries per-table query filters (e.g. tenant isolation). When
59/// present, the filter for each related table is appended (AND'd) to the
60/// secondary SELECT so navigation data is scoped consistently with primary
61/// queries.
62pub async fn load_includes<T>(
63    entities: &mut [T],
64    includes: &[IncludePath],
65    provider: &dyn IDatabaseProvider,
66    filter_map: Option<&HashMap<String, CompiledFilter>>,
67) -> EFResult<()>
68where
69    T: IEntityType + IFromRow + INavigationSetter + IGetKeyValues + IEntitySnapshot,
70{
71    if entities.is_empty() || includes.is_empty() {
72        return Ok(());
73    }
74
75    let meta = T::entity_meta();
76    for include in includes {
77        let nav = match meta.find_navigation(&include.navigation) {
78            Some(n) => n,
79            None => continue,
80        };
81
82        if nav.kind == NavigationKind::ManyToMany {
83            load_many_to_many(entities, &include.navigation, nav, provider, filter_map).await?;
84        } else {
85            load_scalar_navigation(entities, include, nav, &meta, provider, filter_map).await?;
86        }
87
88        if !include.nested.is_empty() {
89            T::load_nested_includes(
90                entities,
91                &include.navigation,
92                &include.nested,
93                provider,
94                filter_map,
95            )
96            .await?;
97        }
98    }
99    Ok(())
100}
101
102async fn load_scalar_navigation<T>(
103    entities: &mut [T],
104    include: &IncludePath,
105    nav: &NavigationMeta,
106    meta: &EntityTypeMeta,
107    provider: &dyn IDatabaseProvider,
108    filter_map: Option<&HashMap<String, CompiledFilter>>,
109) -> EFResult<()>
110where
111    T: IEntitySnapshot + INavigationSetter + IGetKeyValues,
112{
113    let related_table = include
114        .related_table
115        .as_deref()
116        .or(nav.related_table.as_deref());
117    let fk_column = include
118        .foreign_key_column
119        .as_deref()
120        .or(nav.fk_column.as_deref());
121    let ref_column = include
122        .referenced_key_column
123        .as_deref()
124        .or(nav.referenced_key_column.as_deref())
125        .or_else(|| meta.primary_keys.first().map(|k| k.as_ref()));
126
127    let (Some(related_table), Some(fk_column), Some(ref_column)) =
128        (related_table, fk_column, ref_column)
129    else {
130        return Ok(());
131    };
132
133    match nav.kind {
134        NavigationKind::HasMany => {
135            let parent_ids: Vec<DbValue> = entities
136                .iter()
137                .filter_map(|e| e.key_values().get(ref_column).cloned())
138                .collect();
139            if parent_ids.is_empty() {
140                return Ok(());
141            }
142
143            let gen = provider.sql_generator();
144            let placeholders: Vec<String> = (0..parent_ids.len())
145                .map(|i| gen.parameter_placeholder(i + 1))
146                .collect();
147            let mut sql = format!(
148                "SELECT * FROM {} WHERE {} IN ({})",
149                related_table,
150                fk_column,
151                placeholders.join(", ")
152            );
153            let mut params = parent_ids;
154            apply_filter_to_sql(&mut sql, &mut params, related_table, filter_map, gen);
155
156            let mut conn = provider.get_connection().await?;
157            let rows = conn.query(&sql, &params).await?;
158            let grouped = group_rows(&rows, nav.fk_row_index);
159
160            for entity in entities.iter_mut() {
161                if let Some(pk) = entity.key_values().get(ref_column) {
162                    let key = db_value_key(pk);
163                    if let Some(child_rows) = grouped.get(&key) {
164                        entity.apply_has_many(&include.navigation, child_rows)?;
165                    }
166                }
167            }
168        }
169        NavigationKind::BelongsTo | NavigationKind::HasOne => {
170            let fk_values: Vec<DbValue> = entities
171                .iter()
172                .filter_map(|e| e.snapshot().get(fk_column).cloned())
173                .collect();
174            if fk_values.is_empty() {
175                return Ok(());
176            }
177
178            let gen = provider.sql_generator();
179            let placeholders: Vec<String> = (0..fk_values.len())
180                .map(|i| gen.parameter_placeholder(i + 1))
181                .collect();
182            let mut sql = format!(
183                "SELECT * FROM {} WHERE {} IN ({})",
184                related_table,
185                ref_column,
186                placeholders.join(", ")
187            );
188            let mut params = fk_values;
189            apply_filter_to_sql(&mut sql, &mut params, related_table, filter_map, gen);
190
191            let mut conn = provider.get_connection().await?;
192            let rows = conn.query(&sql, &params).await?;
193            let lookup = index_rows(&rows, nav.pk_row_index);
194
195            for entity in entities.iter_mut() {
196                if let Some(fk) = entity.snapshot().get(fk_column) {
197                    let key = db_value_key(fk);
198                    if let Some(related_row) = lookup.get(&key) {
199                        entity.apply_reference(&include.navigation, related_row)?;
200                    }
201                }
202            }
203        }
204        NavigationKind::ManyToMany => {
205            return Err(EFError::configuration(
206                "many-to-many navigations are loaded via load_many_to_many",
207            ));
208        }
209    }
210    Ok(())
211}
212
213async fn load_many_to_many<T>(
214    entities: &mut [T],
215    navigation: &str,
216    nav: &NavigationMeta,
217    provider: &dyn IDatabaseProvider,
218    filter_map: Option<&HashMap<String, CompiledFilter>>,
219) -> EFResult<()>
220where
221    T: IEntityType + INavigationSetter + IGetKeyValues,
222{
223    let meta = T::entity_meta();
224    let ref_column = meta
225        .primary_keys
226        .first()
227        .map(|k| k.as_ref())
228        .unwrap_or("id");
229
230    let (Some(join_table), Some(parent_fk), Some(_related_fk), Some(related_table)) = (
231        nav.through_table.as_deref(),
232        nav.through_parent_fk.as_deref(),
233        nav.through_related_fk.as_deref(),
234        nav.related_table.as_deref(),
235    ) else {
236        return Ok(());
237    };
238
239    let parent_ids: Vec<DbValue> = entities
240        .iter()
241        .filter_map(|e| e.key_values().get(ref_column).cloned())
242        .collect();
243    if parent_ids.is_empty() {
244        return Ok(());
245    }
246
247    let gen = provider.sql_generator();
248    let placeholders: Vec<String> = (0..parent_ids.len())
249        .map(|i| gen.parameter_placeholder(i + 1))
250        .collect();
251    let join_sql = format!(
252        "SELECT * FROM {} WHERE {} IN ({})",
253        join_table,
254        parent_fk,
255        placeholders.join(", ")
256    );
257
258    let mut conn = provider.get_connection().await?;
259    let join_rows = conn.query(&join_sql, &parent_ids).await?;
260    if join_rows.is_empty() {
261        return Ok(());
262    }
263
264    let parent_to_related = group_join_rows(
265        &join_rows,
266        nav.through_parent_fk_index,
267        nav.through_related_fk_index,
268    );
269
270    // Collect unique related keys using a HashSet for O(1) dedup
271    // (previously O(N²) linear scan per join row).
272    let mut seen: HashSet<DbValueKey> = HashSet::new();
273    let related_ids: Vec<DbValue> = join_rows
274        .iter()
275        .filter_map(|row| row.get(nav.through_related_fk_index).cloned())
276        .filter(|v| seen.insert(db_value_key(v)))
277        .collect();
278
279    if related_ids.is_empty() {
280        return Ok(());
281    }
282
283    let related_pk = nav.referenced_key_column.as_deref().unwrap_or("id");
284    let rel_placeholders: Vec<String> = (0..related_ids.len())
285        .map(|i| gen.parameter_placeholder(i + 1))
286        .collect();
287    let mut related_sql = format!(
288        "SELECT * FROM {} WHERE {} IN ({})",
289        related_table,
290        related_pk,
291        rel_placeholders.join(", ")
292    );
293    let mut related_params = related_ids;
294    apply_filter_to_sql(
295        &mut related_sql,
296        &mut related_params,
297        related_table,
298        filter_map,
299        gen,
300    );
301    let related_rows = conn.query(&related_sql, &related_params).await?;
302    let related_by_pk = index_rows(&related_rows, nav.pk_row_index);
303
304    for entity in entities.iter_mut() {
305        if let Some(pk) = entity.key_values().get(ref_column) {
306            let parent_key = db_value_key(pk);
307            if let Some(related_keys) = parent_to_related.get(&parent_key) {
308                let mut child_rows = Vec::new();
309                for rk in related_keys {
310                    if let Some(row) = related_by_pk.get(&db_value_key(rk)) {
311                        child_rows.push(row.clone());
312                    }
313                }
314                if !child_rows.is_empty() {
315                    entity.apply_has_many(navigation, &child_rows)?;
316                }
317            }
318        }
319    }
320
321    Ok(())
322}
323
324fn db_value_key(v: &DbValue) -> DbValueKey {
325    DbValueKey::from(v)
326}
327
328fn group_rows(rows: &[Vec<DbValue>], fk_index: usize) -> HashMap<DbValueKey, Vec<Vec<DbValue>>> {
329    let mut map: HashMap<DbValueKey, Vec<Vec<DbValue>>> = HashMap::new();
330    for row in rows {
331        if let Some(fk_val) = row.get(fk_index) {
332            map.entry(db_value_key(fk_val))
333                .or_default()
334                .push(row.clone());
335        }
336    }
337    map
338}
339
340fn group_join_rows(
341    rows: &[Vec<DbValue>],
342    parent_index: usize,
343    related_index: usize,
344) -> HashMap<DbValueKey, Vec<DbValue>> {
345    let mut map: HashMap<DbValueKey, Vec<DbValue>> = HashMap::new();
346    for row in rows {
347        if let (Some(parent), Some(related)) = (row.get(parent_index), row.get(related_index)) {
348            map.entry(db_value_key(parent))
349                .or_default()
350                .push(related.clone());
351        }
352    }
353    map
354}
355
356fn index_rows(rows: &[Vec<DbValue>], pk_index: usize) -> HashMap<DbValueKey, Vec<DbValue>> {
357    let mut map = HashMap::new();
358    for row in rows {
359        if let Some(pk) = row.get(pk_index) {
360            map.insert(db_value_key(pk), row.clone());
361        }
362    }
363    map
364}