Skip to main content

teaql_runtime/data_service/
relation.rs

1use std::collections::BTreeMap;
2use std::slice;
3
4use teaql_core::{
5    Aggregate, CompactRow, Expr, ObjectGroupBy, RelationAggregate, RelationLoad, SelectQuery, Value,
6};
7
8use crate::{DataServiceError, MetadataStore, RuntimeError};
9
10use super::{EntityDataService, RelationLoadPlan, helpers::*};
11
12#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
13enum FlatIdentityKey {
14    U64(u64),
15    Other(String),
16}
17
18impl FlatIdentityKey {
19    fn from_value(value: &Value) -> Self {
20        match value {
21            Value::U64(value) => Self::U64(*value),
22            Value::I64(value) if *value >= 0 => Self::U64(*value as u64),
23            _ => Self::Other(graph_identity_key(value)),
24        }
25    }
26}
27
28fn unique_relation_values(rows: &[CompactRow], field: &str) -> Vec<Value> {
29    let mut values = rows
30        .iter()
31        .filter_map(|row| row.get(field).cloned())
32        .map(|value| (FlatIdentityKey::from_value(&value), value))
33        .collect::<Vec<_>>();
34    values.sort_unstable_by(|left, right| left.0.cmp(&right.0));
35    values.dedup_by(|left, right| left.0 == right.0);
36    values.into_iter().map(|(_, value)| value).collect()
37}
38
39const SMALL_PARENT_RELATION_PROBE_LIMIT: usize = 16;
40
41fn should_use_small_parent_relation_probes(
42    capabilities: &teaql_data_service::DataServiceCapabilities,
43    plan: &RelationLoadPlan,
44    parent_count: usize,
45) -> bool {
46    capabilities.small_parent_relation_probes
47        && plan.many
48        && parent_count <= SMALL_PARENT_RELATION_PROBE_LIMIT
49        && plan
50            .query
51            .as_ref()
52            .is_some_and(|query| query.slice.is_some())
53}
54
55impl<'a, E> EntityDataService<'a, E>
56where
57    E: teaql_data_service::QueryExecutor
58        + teaql_data_service::MutationExecutor
59        + Send
60        + Sync
61        + 'static,
62{
63    pub fn relation_loads(&self) -> Vec<String> {
64        self.behavior()
65            .map(|behavior| behavior.relation_loads(self.data_service.metadata.context))
66            .unwrap_or_default()
67    }
68
69    pub fn relation_plans(&self) -> Result<Vec<RelationLoadPlan>, RuntimeError> {
70        self.build_relation_plans(&self.entity, &self.relation_loads())
71    }
72
73    pub fn relation_query(
74        &self,
75        relation_name: &str,
76        parent_rows: &[CompactRow],
77    ) -> Result<SelectQuery, RuntimeError> {
78        let plan = self
79            .relation_plans()?
80            .into_iter()
81            .find(|plan| plan.relation_name == relation_name)
82            .ok_or_else(|| RuntimeError::MissingRelation {
83                entity: self.entity.clone(),
84                relation: relation_name.to_owned(),
85            })?;
86        Ok(self.query_for_plan(&plan, parent_rows))
87    }
88
89    pub(crate) async fn enhance_relations_internal(
90        &self,
91        parent_rows: &mut [CompactRow],
92    ) -> Result<(), DataServiceError<E::Error>> {
93        let plans = self.relation_plans().map_err(DataServiceError::Runtime)?;
94        for plan in plans {
95            self.enhance_plan(parent_rows, &plan).await?;
96        }
97        Ok(())
98    }
99
100    pub(crate) async fn enhance_query_relations_internal(
101        &self,
102        parent_rows: &mut [CompactRow],
103        query: &SelectQuery,
104    ) -> Result<(), DataServiceError<E::Error>> {
105        let plans = self
106            .build_relation_plans_from_loads(&query.entity, &query.relations)
107            .map_err(DataServiceError::Runtime)?;
108        for plan in plans {
109            self.enhance_plan(parent_rows, &plan).await?;
110        }
111        Ok(())
112    }
113
114    pub(crate) async fn hydrate_flat_plans_internal(
115        &self,
116        parent_rows: &mut [CompactRow],
117        plans: &[RelationLoadPlan],
118        root: &crate::EntityRuntimeState,
119        graph: &mut crate::EntityGraphBuilder,
120    ) -> Result<(), DataServiceError<E::Error>> {
121        for plan in plans {
122            self.hydrate_flat_plan(parent_rows, plan, root, graph)
123                .await?;
124        }
125        Ok(())
126    }
127
128    pub(crate) async fn hydrate_compact_flat_plans_internal(
129        &self,
130        parent_rows: &[CompactRow],
131        plans: &[RelationLoadPlan],
132        root: &crate::EntityRuntimeState,
133        graph: &mut crate::EntityGraphBuilder,
134    ) -> Result<(), DataServiceError<E::Error>> {
135        for plan in plans {
136            if plan.children.is_empty() {
137                self.hydrate_compact_flat_leaf(parent_rows, plan, root, graph)
138                    .await?;
139            } else {
140                self.hydrate_compact_flat_plan(parent_rows, plan, root, graph)
141                    .await?;
142            }
143        }
144        Ok(())
145    }
146
147    pub(crate) fn flat_relation_plans(
148        &self,
149        query: &SelectQuery,
150    ) -> Result<Option<(Vec<RelationLoadPlan>, Vec<RelationLoadPlan>)>, RuntimeError> {
151        let context = self.data_service.metadata.context;
152        let query_plans = self.build_relation_plans_from_loads(&query.entity, &query.relations)?;
153        let behavior_plans = self.relation_plans()?;
154
155        fn supported(context: &crate::UserContext, plan: &RelationLoadPlan) -> bool {
156            context.has_entity_graph_decoder(&plan.target_entity)
157                && plan.children.iter().all(|child| supported(context, child))
158        }
159
160        let all_supported = query_plans
161            .iter()
162            .chain(behavior_plans.iter())
163            .all(|plan| supported(context, plan));
164        Ok(all_supported.then_some((query_plans, behavior_plans)))
165    }
166
167    pub(crate) fn enhance_relation_aggregates_internal<'b>(
168        &'b self,
169        parent_rows: &'b mut [CompactRow],
170        relation_aggregates: &'b [RelationAggregate],
171        parent_cache_options: Option<teaql_core::AggregationCacheOptions>,
172        parent_trace_chain: &'b [teaql_core::TraceNode],
173    ) -> std::pin::Pin<
174        Box<dyn std::future::Future<Output = Result<(), DataServiceError<E::Error>>> + Send + 'b>,
175    > {
176        Box::pin(async move {
177            for aggregate in relation_aggregates {
178                self.enhance_relation_aggregate(
179                    parent_rows,
180                    aggregate,
181                    parent_cache_options,
182                    parent_trace_chain,
183                )
184                .await?;
185            }
186            Ok(())
187        })
188    }
189
190    pub(crate) fn enhance_object_group_bys_internal<'b>(
191        &'b self,
192        rows: &'b mut [CompactRow],
193        object_group_bys: &'b [ObjectGroupBy],
194        parent_trace_chain: &'b [teaql_core::TraceNode],
195    ) -> std::pin::Pin<
196        Box<dyn std::future::Future<Output = Result<(), DataServiceError<E::Error>>> + Send + 'b>,
197    > {
198        Box::pin(async move {
199            for group_by in object_group_bys {
200                let ids = rows
201                    .iter()
202                    .filter_map(|row| row.get(&group_by.storage_field).cloned())
203                    .collect::<Vec<_>>();
204                if ids.is_empty() {
205                    continue;
206                }
207                let mut query = group_by.query.clone();
208                ensure_projection(&mut query, "id");
209                query = query.and_filter(Expr::in_list("id", ids));
210                let object_rows = self
211                    .scoped_data_service_internal(query.entity.clone())
212                    .with_trace_context(parent_trace_chain.to_vec())
213                    .fetch_compact_all_internal(query)
214                    .await?
215                    .into_iter()
216                    .filter_map(|row| {
217                        row.get("id")
218                            .cloned()
219                            .map(|id| (graph_identity_key(&id), row))
220                    })
221                    .collect::<BTreeMap<_, _>>();
222                for row in rows.iter_mut() {
223                    if let Some(key) = row.get(&group_by.storage_field).map(graph_identity_key) {
224                        let value = object_rows
225                            .get(&key)
226                            .cloned()
227                            .map(|row| Value::object(row.into_map()))
228                            .unwrap_or(Value::Null);
229                        row.insert(group_by.property_name.clone(), value);
230                    }
231                }
232            }
233            Ok(())
234        })
235    }
236
237    pub(crate) fn enhance_child_queries_internal<'b>(
238        &'b self,
239        rows: &'b mut [CompactRow],
240        child_queries: &'b [SelectQuery],
241        parent_trace_chain: &'b [teaql_core::TraceNode],
242    ) -> std::pin::Pin<
243        Box<dyn std::future::Future<Output = Result<(), DataServiceError<E::Error>>> + Send + 'b>,
244    > {
245        Box::pin(async move {
246            for child_query in child_queries {
247                let ids = rows
248                    .iter()
249                    .filter_map(|row| row.get("id").cloned())
250                    .collect::<Vec<_>>();
251                if ids.is_empty() {
252                    continue;
253                }
254                let mut query = child_query.clone();
255                ensure_projection(&mut query, "id");
256                query = query.and_filter(Expr::in_list("id", ids));
257                let child_rows = self
258                    .scoped_data_service_internal(query.entity.clone())
259                    .with_trace_context(parent_trace_chain.to_vec())
260                    .fetch_compact_all_internal(query)
261                    .await?
262                    .into_iter()
263                    .filter_map(|row| {
264                        row.get("id")
265                            .cloned()
266                            .map(|id| (graph_identity_key(&id), row))
267                    })
268                    .collect::<BTreeMap<_, _>>();
269                for row in rows.iter_mut() {
270                    if let Some(key) = row.get("id").map(graph_identity_key) {
271                        if let Some(child) = child_rows.get(&key) {
272                            row.extend(child.clone());
273                        }
274                    }
275                }
276            }
277            Ok(())
278        })
279    }
280
281    async fn enhance_relation_aggregate(
282        &self,
283        parent_rows: &mut [CompactRow],
284        aggregate: &RelationAggregate,
285        parent_cache_options: Option<teaql_core::AggregationCacheOptions>,
286        parent_trace_chain: &[teaql_core::TraceNode],
287    ) -> Result<(), DataServiceError<E::Error>> {
288        let plan = self
289            .build_relation_plans_from_loads(
290                &self.entity,
291                &[RelationLoad::with_query(
292                    aggregate.relation_name.clone(),
293                    aggregate.query.clone(),
294                )],
295            )
296            .map_err(DataServiceError::Runtime)?
297            .into_iter()
298            .next()
299            .ok_or_else(|| {
300                DataServiceError::Runtime(RuntimeError::MissingRelation {
301                    entity: self.entity.clone(),
302                    relation: aggregate.relation_name.clone(),
303                })
304            })?;
305
306        let ids = parent_rows
307            .iter()
308            .filter_map(|row| row.get(&plan.local_key).cloned())
309            .collect::<Vec<_>>();
310        if ids.is_empty() {
311            attach_empty_relation_aggregate(parent_rows, &aggregate.alias, aggregate.single_result);
312            return Ok(());
313        }
314
315        let child_repo = self.scoped_data_service_internal(plan.target_entity.clone());
316        let mut query = aggregate.query.clone();
317        query.entity = plan.target_entity.clone();
318        if query.aggregation_cache.is_none() {
319            if let Some(options) = parent_cache_options.filter(|options| options.propagate) {
320                query.aggregation_cache = Some(teaql_core::AggregationCacheOptions::enabled(
321                    options.propagate_cache_expired_millis,
322                ));
323            }
324        }
325        query.projection.clear();
326        query.expr_projection.clear();
327        query.order_by.clear();
328        query.slice = None;
329        query.relations.clear();
330        if query.aggregates.is_empty() {
331            let alias = aggregate_alias(aggregate.single_result, &aggregate.alias);
332            query = query.aggregate(Aggregate::count(alias));
333        }
334        if !query
335            .group_by
336            .iter()
337            .any(|field| field == &plan.foreign_key)
338        {
339            query = query.group_by(plan.foreign_key.clone());
340        }
341        query = query.and_filter(Expr::in_list(plan.foreign_key.clone(), ids));
342
343        let mut chain = parent_trace_chain.to_vec();
344        chain.push(teaql_core::TraceNode {
345            entity_type: query.entity.clone(),
346            entity_id: None,
347            comment: aggregate.alias.clone(),
348        });
349
350        let mut aggregate_rows = child_repo
351            .with_trace_context(chain)
352            .fetch_compact_all_internal(query)
353            .await?;
354        let foreign_key_column = self
355            .data_service
356            .metadata
357            .context
358            .entity(&plan.target_entity)
359            .and_then(|descriptor| {
360                descriptor
361                    .properties
362                    .iter()
363                    .find(|property| property.name == plan.foreign_key)
364                    .map(|property| property.column_name.clone())
365            });
366        if let Some(foreign_key_column) =
367            foreign_key_column.filter(|column| column != &plan.foreign_key)
368        {
369            for row in &mut aggregate_rows {
370                if !row.contains_key(&plan.foreign_key) {
371                    if let Some(value) = row.remove(&foreign_key_column) {
372                        row.insert(plan.foreign_key.clone(), value);
373                    }
374                }
375            }
376        }
377        attach_relation_aggregate_rows(parent_rows, &plan, aggregate, aggregate_rows);
378        Ok(())
379    }
380
381    fn build_relation_plans(
382        &self,
383        entity: &str,
384        loads: &[String],
385    ) -> Result<Vec<RelationLoadPlan>, RuntimeError> {
386        let descriptor = self.data_service.metadata.context.require_entity(entity)?;
387        let mut grouped: BTreeMap<String, Vec<String>> = BTreeMap::new();
388        for load in loads {
389            match load.split_once('.') {
390                Some((head, tail)) => {
391                    grouped
392                        .entry(head.to_owned())
393                        .or_default()
394                        .push(tail.to_owned());
395                }
396                None => {
397                    grouped.entry(load.clone()).or_default();
398                }
399            }
400        }
401
402        grouped
403            .into_iter()
404            .map(|(name, child_loads)| {
405                let relation = descriptor.relation_by_name(&name).ok_or_else(|| {
406                    RuntimeError::MissingRelation {
407                        entity: entity.to_owned(),
408                        relation: name.clone(),
409                    }
410                })?;
411                let child_repo = self.scoped_data_service_internal(relation.target_entity.clone());
412                let children =
413                    child_repo.build_relation_plans(&relation.target_entity, &child_loads)?;
414                Ok(RelationLoadPlan {
415                    parent_entity: entity.to_owned(),
416                    relation_name: relation.name.clone(),
417                    path: relation.name.clone(),
418                    target_entity: relation.target_entity.clone(),
419                    local_key: relation.local_key.clone(),
420                    foreign_key: relation.foreign_key.clone(),
421                    many: relation.many,
422                    query: None,
423                    children,
424                })
425            })
426            .collect()
427    }
428
429    fn build_relation_plans_from_loads(
430        &self,
431        entity: &str,
432        loads: &[RelationLoad],
433    ) -> Result<Vec<RelationLoadPlan>, RuntimeError> {
434        let descriptor = self.data_service.metadata.context.require_entity(entity)?;
435        loads
436            .iter()
437            .map(|load| {
438                let relation = descriptor.relation_by_name(&load.name).ok_or_else(|| {
439                    RuntimeError::MissingRelation {
440                        entity: entity.to_owned(),
441                        relation: load.name.clone(),
442                    }
443                })?;
444                let relation_query = load.query.as_deref().cloned();
445                let child_loads = relation_query
446                    .as_ref()
447                    .map(|query| query.relations.as_slice())
448                    .unwrap_or_default();
449                let child_repo = self.scoped_data_service_internal(relation.target_entity.clone());
450                let children = child_repo
451                    .build_relation_plans_from_loads(&relation.target_entity, child_loads)?;
452                Ok(RelationLoadPlan {
453                    parent_entity: entity.to_owned(),
454                    relation_name: relation.name.clone(),
455                    path: relation.name.clone(),
456                    target_entity: relation.target_entity.clone(),
457                    local_key: relation.local_key.clone(),
458                    foreign_key: relation.foreign_key.clone(),
459                    many: relation.many,
460                    query: relation_query,
461                    children,
462                })
463            })
464            .collect()
465    }
466    fn enhance_plan<'b>(
467        &'b self,
468        parent_rows: &'b mut [CompactRow],
469        plan: &'b RelationLoadPlan,
470    ) -> std::pin::Pin<
471        Box<dyn std::future::Future<Output = Result<(), DataServiceError<E::Error>>> + Send + 'b>,
472    > {
473        Box::pin(async move {
474            let scope = self.data_service.metadata.context.start_runtime_operation(
475                crate::RuntimeOperation::new(
476                    "relation_load",
477                    format!("{}.{}", plan.parent_entity, plan.path),
478                )
479                .attribute("teaql.entity.type", plan.parent_entity.clone())
480                .attribute("teaql.relation.name", plan.path.clone()),
481            );
482            let result = scope
483                .run(async {
484                    let child_repo = self.scoped_data_service_internal(plan.target_entity.clone());
485                    let mut child_rows = self
486                        .fetch_relation_rows(&child_repo, plan, parent_rows, false)
487                        .await?;
488                    for child in &mut child_rows {
489                        child.remove(teaql_core::PARTITION_RANK_PROPERTY);
490                    }
491                    self.attach_relation_rows(parent_rows, plan, child_rows);
492
493                    if !plan.children.is_empty() {
494                        for parent in parent_rows.iter_mut() {
495                            match parent.get_mut(&plan.relation_name) {
496                                Some(Value::Object(child)) => {
497                                    child_repo
498                                        .enhance_child_record(child, &plan.children)
499                                        .await?;
500                                }
501                                Some(Value::List(values)) => {
502                                    for value in values.iter_mut() {
503                                        if let Value::Object(child) = value {
504                                            child_repo
505                                                .enhance_child_record(child, &plan.children)
506                                                .await?;
507                                        }
508                                    }
509                                }
510                                _ => {}
511                            }
512                        }
513                    }
514                    Ok(())
515                })
516                .await;
517            match &result {
518                Ok(_) => scope.success(BTreeMap::from([(
519                    "teaql.result.cardinality".to_owned(),
520                    crate::RuntimeAttributeValue::Integer(parent_rows.len() as i64),
521                )])),
522                Err(_) => scope.failure("relation_load_error"),
523            }
524            result
525        })
526    }
527
528    fn hydrate_flat_plan<'b>(
529        &'b self,
530        parent_rows: &'b mut [CompactRow],
531        plan: &'b RelationLoadPlan,
532        root: &'b crate::EntityRuntimeState,
533        graph: &'b mut crate::EntityGraphBuilder,
534    ) -> std::pin::Pin<
535        Box<dyn std::future::Future<Output = Result<(), DataServiceError<E::Error>>> + Send + 'b>,
536    > {
537        Box::pin(async move {
538            let child_repo = self.scoped_data_service_internal(plan.target_entity.clone());
539            let mut child_rows = self
540                .fetch_relation_rows(&child_repo, plan, parent_rows, false)
541                .await?;
542            for child in &mut child_rows {
543                child.remove(teaql_core::PARTITION_RANK_PROPERTY);
544            }
545
546            // Hydrate descendants while the rows are still owned by this level. Nothing is
547            // embedded into a parent row: every relation is published directly into the
548            // shared, immutable identity graph.
549            for child_plan in &plan.children {
550                child_repo
551                    .hydrate_flat_plan(&mut child_rows, child_plan, root, graph)
552                    .await?;
553            }
554
555            let inverse_relation = self
556                .data_service
557                .metadata
558                .context
559                .entity(&plan.target_entity)
560                .and_then(|descriptor| {
561                    descriptor.relations.iter().find(|relation| {
562                        relation.target_entity == plan.parent_entity
563                            && relation.local_key == plan.foreign_key
564                            && relation.foreign_key == plan.local_key
565                    })
566                })
567                .map(|relation| (relation.name.clone(), relation.many));
568
569            let mut buckets: BTreeMap<FlatIdentityKey, Vec<CompactRow>> = BTreeMap::new();
570            for child in child_rows {
571                if let Some(key) = child.get(&plan.foreign_key) {
572                    buckets
573                        .entry(FlatIdentityKey::from_value(key))
574                        .or_default()
575                        .push(child);
576                }
577            }
578
579            let context = self.data_service.metadata.context;
580            for parent in parent_rows {
581                let Some(local_value) = parent.get(&plan.local_key) else {
582                    continue;
583                };
584                let related = buckets
585                    .remove(&FlatIdentityKey::from_value(local_value))
586                    .unwrap_or_default();
587
588                if let Some((inverse_name, inverse_many)) = &inverse_relation {
589                    let parent_record = parent.clone();
590                    for child in &related {
591                        let Some(child_id) = child.get("id").and_then(Value::try_u64) else {
592                            continue;
593                        };
594                        if *inverse_many {
595                            context
596                                .decode_compact_entity_list_into_graph(
597                                    &plan.parent_entity,
598                                    vec![parent_record.clone()],
599                                    root,
600                                    graph,
601                                    &plan.target_entity,
602                                    child_id,
603                                    inverse_name,
604                                )
605                                .map_err(DataServiceError::Entity)?;
606                        } else {
607                            context
608                                .decode_compact_entity_option_into_graph(
609                                    &plan.parent_entity,
610                                    vec![parent_record.clone()],
611                                    root,
612                                    graph,
613                                    &plan.target_entity,
614                                    child_id,
615                                    inverse_name,
616                                )
617                                .map_err(DataServiceError::Entity)?;
618                        }
619                    }
620                }
621
622                if plan.many || plan.local_key == "id" {
623                    let owner_id = parent.get("id").and_then(Value::try_u64).ok_or_else(|| {
624                        DataServiceError::Entity(teaql_core::EntityError::new(
625                            &plan.parent_entity,
626                            "loaded reverse relation owner is missing its u64 id",
627                        ))
628                    })?;
629                    if plan.many {
630                        context
631                            .decode_compact_entity_list_into_graph(
632                                &plan.target_entity,
633                                related,
634                                root,
635                                graph,
636                                &plan.parent_entity,
637                                owner_id,
638                                &plan.relation_name,
639                            )
640                            .map_err(DataServiceError::Entity)?;
641                    } else {
642                        context
643                            .decode_compact_entity_option_into_graph(
644                                &plan.target_entity,
645                                related,
646                                root,
647                                graph,
648                                &plan.parent_entity,
649                                owner_id,
650                                &plan.relation_name,
651                            )
652                            .map_err(DataServiceError::Entity)?;
653                    }
654                } else if related.is_empty() {
655                    // Forward optional relations use the scalar loaded marker to distinguish a
656                    // loaded null from a relation that was never requested.
657                    parent.insert(plan.relation_name.clone(), Value::Null);
658                } else {
659                    for child in related {
660                        context
661                            .decode_compact_entity_into_graph(
662                                &plan.target_entity,
663                                child,
664                                root,
665                                graph,
666                            )
667                            .map_err(DataServiceError::Entity)?;
668                    }
669                }
670            }
671            Ok(())
672        })
673    }
674
675    fn hydrate_compact_flat_plan<'b>(
676        &'b self,
677        parent_rows: &'b [CompactRow],
678        plan: &'b RelationLoadPlan,
679        root: &'b crate::EntityRuntimeState,
680        graph: &'b mut crate::EntityGraphBuilder,
681    ) -> std::pin::Pin<
682        Box<dyn std::future::Future<Output = Result<(), DataServiceError<E::Error>>> + Send + 'b>,
683    > {
684        Box::pin(async move {
685            let child_repo = self.scoped_data_service_internal(plan.target_entity.clone());
686            let child_rows = self
687                .fetch_relation_rows(&child_repo, plan, parent_rows, true)
688                .await?;
689
690            for child_plan in &plan.children {
691                if child_plan.children.is_empty() {
692                    child_repo
693                        .hydrate_compact_flat_leaf(&child_rows, child_plan, root, graph)
694                        .await?;
695                } else {
696                    child_repo
697                        .hydrate_compact_flat_plan(&child_rows, child_plan, root, graph)
698                        .await?;
699                }
700            }
701
702            self.install_compact_flat_relation(parent_rows, plan, child_rows, root, graph)
703        })
704    }
705
706    fn hydrate_compact_flat_leaf<'b>(
707        &'b self,
708        parent_rows: &'b [CompactRow],
709        plan: &'b RelationLoadPlan,
710        root: &'b crate::EntityRuntimeState,
711        graph: &'b mut crate::EntityGraphBuilder,
712    ) -> std::pin::Pin<
713        Box<dyn std::future::Future<Output = Result<(), DataServiceError<E::Error>>> + Send + 'b>,
714    > {
715        Box::pin(async move {
716            let child_repo = self.scoped_data_service_internal(plan.target_entity.clone());
717            let child_rows = self
718                .fetch_relation_rows(&child_repo, plan, parent_rows, true)
719                .await?;
720            self.install_compact_flat_relation(parent_rows, plan, child_rows, root, graph)
721        })
722    }
723
724    fn install_compact_flat_relation(
725        &self,
726        parent_rows: &[CompactRow],
727        plan: &RelationLoadPlan,
728        child_rows: Vec<CompactRow>,
729        root: &crate::EntityRuntimeState,
730        graph: &mut crate::EntityGraphBuilder,
731    ) -> Result<(), DataServiceError<E::Error>> {
732        // A forward to-one relation only needs its fetched targets installed in the shared
733        // identity table. Building owner buckets and then removing them one parent at a time
734        // creates a map and one Vec per distinct target without adding information.
735        if !plan.many && plan.local_key != "id" {
736            return self
737                .data_service
738                .metadata
739                .context
740                .decode_compact_entity_batch_into_graph(
741                    &plan.target_entity,
742                    child_rows,
743                    root,
744                    graph,
745                )
746                .map_err(DataServiceError::Entity);
747        }
748
749        let mut buckets: BTreeMap<FlatIdentityKey, Vec<CompactRow>> = BTreeMap::new();
750        for child in child_rows {
751            if let Some(key) = child.get(&plan.foreign_key) {
752                buckets
753                    .entry(FlatIdentityKey::from_value(key))
754                    .or_default()
755                    .push(child);
756            }
757        }
758
759        let context = self.data_service.metadata.context;
760        for parent in parent_rows {
761            let Some(local_value) = parent.get(&plan.local_key) else {
762                continue;
763            };
764            let related = buckets
765                .remove(&FlatIdentityKey::from_value(local_value))
766                .unwrap_or_default();
767
768            if plan.many || plan.local_key == "id" {
769                let owner_id = parent.get("id").and_then(Value::try_u64).ok_or_else(|| {
770                    DataServiceError::Entity(teaql_core::EntityError::new(
771                        &plan.parent_entity,
772                        "loaded reverse relation owner is missing its u64 id",
773                    ))
774                })?;
775                if plan.many {
776                    context
777                        .decode_compact_entity_list_into_graph(
778                            &plan.target_entity,
779                            related,
780                            root,
781                            graph,
782                            &plan.parent_entity,
783                            owner_id,
784                            &plan.relation_name,
785                        )
786                        .map_err(DataServiceError::Entity)?;
787                } else {
788                    context
789                        .decode_compact_entity_option_into_graph(
790                            &plan.target_entity,
791                            related,
792                            root,
793                            graph,
794                            &plan.parent_entity,
795                            owner_id,
796                            &plan.relation_name,
797                        )
798                        .map_err(DataServiceError::Entity)?;
799                }
800            } else {
801                for child in related {
802                    context
803                        .decode_compact_entity_into_graph(&plan.target_entity, child, root, graph)
804                        .map_err(DataServiceError::Entity)?;
805                }
806            }
807        }
808        Ok(())
809    }
810
811    fn enhance_child_record<'b>(
812        &'b self,
813        child: &'b mut std::collections::BTreeMap<String, Value>,
814        plans: &'b [RelationLoadPlan],
815    ) -> std::pin::Pin<
816        Box<dyn std::future::Future<Output = Result<(), DataServiceError<E::Error>>> + Send + 'b>,
817    > {
818        Box::pin(async move {
819            for plan in plans {
820                let mut row = CompactRow::from_map(std::mem::take(child));
821                self.enhance_plan(slice::from_mut(&mut row), plan).await?;
822                *child = row.into_map();
823            }
824            Ok(())
825        })
826    }
827
828    fn query_for_plan(&self, plan: &RelationLoadPlan, parent_rows: &[CompactRow]) -> SelectQuery {
829        // Relation identities are a set. Keeping one value per normalized identity avoids
830        // compiling and binding the same foreign key once for every parent row (a common shape
831        // for pages containing many rows that share a small reference table).
832        let ids = unique_relation_values(parent_rows, &plan.local_key);
833
834        let mut query = plan
835            .query
836            .clone()
837            .unwrap_or_else(|| SelectQuery::new(plan.target_entity.clone()));
838        query.entity = plan.target_entity.clone();
839        ensure_projection(&mut query, &plan.foreign_key);
840        for child in &plan.children {
841            ensure_projection(&mut query, &child.local_key);
842        }
843        if !ids.is_empty() {
844            query = query.and_filter(Expr::in_list(plan.foreign_key.clone(), ids));
845        }
846        if query.slice.is_some() {
847            query.partition_by = Some(plan.foreign_key.clone());
848        }
849        query
850    }
851
852    async fn fetch_relation_rows(
853        &self,
854        child_repo: &EntityDataService<'a, E>,
855        plan: &RelationLoadPlan,
856        parent_rows: &[CompactRow],
857        compact: bool,
858    ) -> Result<Vec<CompactRow>, DataServiceError<E::Error>> {
859        let ids = unique_relation_values(parent_rows, &plan.local_key);
860        if ids.is_empty() {
861            return Ok(Vec::new());
862        }
863
864        let capabilities =
865            teaql_data_service::DataServiceExecutor::capabilities(self.data_service.executor);
866        let probe = should_use_small_parent_relation_probes(&capabilities, plan, ids.len());
867
868        if !probe {
869            let query = if compact {
870                self.query_for_compact_plan(plan, parent_rows)
871            } else {
872                self.query_for_plan(plan, parent_rows)
873            };
874            return child_repo.fetch_compact_all_internal(query).await;
875        }
876
877        // With execution metadata disabled, retain one semantic partition query and let an
878        // embedded provider execute its indexed parent probes behind one executor boundary.
879        // When metadata is enabled we intentionally keep one observable result per SQL probe.
880        if !self.data_service.metadata.capture_execution_metadata() {
881            let query = if compact {
882                self.query_for_compact_plan(plan, parent_rows)
883            } else {
884                self.query_for_plan(plan, parent_rows)
885            };
886            return child_repo.fetch_compact_all_internal(query).await;
887        }
888
889        let mut rows = Vec::new();
890        for id in ids {
891            let mut query = self.base_relation_query(plan);
892            query = query.and_filter(Expr::eq(plan.foreign_key.clone(), id));
893            // The slice now belongs to one parent, so no window partition is needed.
894            query.partition_by = None;
895            rows.extend(child_repo.fetch_compact_all_internal(query).await?);
896        }
897        Ok(rows)
898    }
899
900    fn base_relation_query(&self, plan: &RelationLoadPlan) -> SelectQuery {
901        let mut query = plan
902            .query
903            .clone()
904            .unwrap_or_else(|| SelectQuery::new(plan.target_entity.clone()));
905        query.entity = plan.target_entity.clone();
906        ensure_projection(&mut query, &plan.foreign_key);
907        for child in &plan.children {
908            ensure_projection(&mut query, &child.local_key);
909        }
910        query
911    }
912
913    fn query_for_compact_plan(
914        &self,
915        plan: &RelationLoadPlan,
916        parent_rows: &[CompactRow],
917    ) -> SelectQuery {
918        let ids = unique_relation_values(parent_rows, &plan.local_key);
919        let mut query = plan
920            .query
921            .clone()
922            .unwrap_or_else(|| SelectQuery::new(plan.target_entity.clone()));
923        query.entity = plan.target_entity.clone();
924        // The flat hydrator owns the relation tree and recursively loads each child plan into
925        // one shared identity graph. Leaving nested relations on this single-layer query makes
926        // fetch_compact_all_internal hydrate the same subtree first; the flat hydrator then
927        // hydrates it again, causing an exponential number of duplicate relation queries.
928        query.relations.clear();
929        ensure_projection(&mut query, &plan.foreign_key);
930        for child in &plan.children {
931            ensure_projection(&mut query, &child.local_key);
932        }
933        if !ids.is_empty() {
934            query = query.and_filter(Expr::in_list(plan.foreign_key.clone(), ids));
935        }
936        if query.slice.is_some() {
937            query.partition_by = Some(plan.foreign_key.clone());
938        }
939        query
940    }
941
942    fn attach_relation_rows(
943        &self,
944        parent_rows: &mut [CompactRow],
945        plan: &RelationLoadPlan,
946        child_rows: Vec<CompactRow>,
947    ) {
948        let inverse_relation = self
949            .data_service
950            .metadata
951            .context
952            .entity(&plan.target_entity)
953            .and_then(|descriptor| {
954                descriptor.relations.iter().find(|relation| {
955                    relation.target_entity == plan.parent_entity
956                        && relation.local_key == plan.foreign_key
957                        && relation.foreign_key == plan.local_key
958                })
959            })
960            .map(|relation| (relation.name.clone(), relation.many));
961
962        let mut buckets: BTreeMap<String, Vec<CompactRow>> = BTreeMap::new();
963        for child in child_rows.clone() {
964            if let Some(key) = child.get(&plan.foreign_key) {
965                buckets
966                    .entry(graph_identity_key(key))
967                    .or_default()
968                    .push(child);
969            }
970        }
971
972        for parent in parent_rows.iter_mut() {
973            let Some(local_value) = parent.get(&plan.local_key) else {
974                continue;
975            };
976            let bucket_key = graph_identity_key(local_value);
977            let related = buckets.get(&bucket_key).cloned().unwrap_or_default();
978            let related = match &inverse_relation {
979                Some((inverse_relation, inverse_many)) => {
980                    let mut parent_object = parent.clone();
981                    parent_object.remove(&plan.relation_name);
982                    related
983                        .into_iter()
984                        .map(|mut child| {
985                            match *inverse_many {
986                                true => {
987                                    if !child.contains_key(inverse_relation) {
988                                        child.insert(
989                                            inverse_relation.clone(),
990                                            Value::List(Vec::new()),
991                                        );
992                                    }
993                                    let entry = child
994                                        .get_mut(inverse_relation)
995                                        .expect("inverse relation was inserted immediately above");
996                                    if let Value::List(list) = entry {
997                                        list.push(Value::object(parent_object.clone().into_map()));
998                                    }
999                                }
1000                                false => {
1001                                    child.insert(
1002                                        inverse_relation.clone(),
1003                                        Value::object(parent_object.clone().into_map()),
1004                                    );
1005                                }
1006                            }
1007                            child
1008                        })
1009                        .collect::<Vec<_>>()
1010                }
1011                None => related,
1012            };
1013            match plan.many {
1014                true => {
1015                    parent.insert(
1016                        plan.relation_name.clone(),
1017                        Value::List(
1018                            related
1019                                .into_iter()
1020                                .map(|row| Value::object(row.into_map()))
1021                                .collect(),
1022                        ),
1023                    );
1024                }
1025                false => {
1026                    let value = related
1027                        .into_iter()
1028                        .next()
1029                        .map(|row| Value::object(row.into_map()))
1030                        .unwrap_or(Value::Null);
1031                    parent.insert(plan.relation_name.clone(), value);
1032                }
1033            }
1034        }
1035    }
1036}
1037
1038#[cfg(test)]
1039mod planner_tests {
1040    use super::*;
1041
1042    fn limited_many_plan() -> RelationLoadPlan {
1043        RelationLoadPlan {
1044            parent_entity: "Vendor".to_owned(),
1045            relation_name: "trips".to_owned(),
1046            path: "trips".to_owned(),
1047            target_entity: "Trip".to_owned(),
1048            local_key: "id".to_owned(),
1049            foreign_key: "vendor_id".to_owned(),
1050            many: true,
1051            query: Some(SelectQuery::new("Trip").order_desc("id").limit(10)),
1052            children: Vec::new(),
1053        }
1054    }
1055
1056    #[test]
1057    fn small_parent_probe_requires_provider_opt_in_and_bounded_parent_set() {
1058        let plan = limited_many_plan();
1059        let mut capabilities = teaql_data_service::DataServiceCapabilities::default();
1060        assert!(!should_use_small_parent_relation_probes(
1061            &capabilities,
1062            &plan,
1063            6
1064        ));
1065
1066        capabilities.small_parent_relation_probes = true;
1067        assert!(should_use_small_parent_relation_probes(
1068            &capabilities,
1069            &plan,
1070            6
1071        ));
1072        assert!(!should_use_small_parent_relation_probes(
1073            &capabilities,
1074            &plan,
1075            SMALL_PARENT_RELATION_PROBE_LIMIT + 1
1076        ));
1077    }
1078}