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::EntityRoot,
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::EntityRoot,
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::EntityRoot,
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::EntityRoot,
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    async fn hydrate_compact_flat_leaf(
707        &self,
708        parent_rows: &[CompactRow],
709        plan: &RelationLoadPlan,
710        root: &crate::EntityRoot,
711        graph: &mut crate::EntityGraphBuilder,
712    ) -> Result<(), DataServiceError<E::Error>> {
713        let child_repo = self.scoped_data_service_internal(plan.target_entity.clone());
714        let child_rows = self
715            .fetch_relation_rows(&child_repo, plan, parent_rows, true)
716            .await?;
717        self.install_compact_flat_relation(parent_rows, plan, child_rows, root, graph)
718    }
719
720    fn install_compact_flat_relation(
721        &self,
722        parent_rows: &[CompactRow],
723        plan: &RelationLoadPlan,
724        child_rows: Vec<CompactRow>,
725        root: &crate::EntityRoot,
726        graph: &mut crate::EntityGraphBuilder,
727    ) -> Result<(), DataServiceError<E::Error>> {
728        // A forward to-one relation only needs its fetched targets installed in the shared
729        // identity table. Building owner buckets and then removing them one parent at a time
730        // creates a map and one Vec per distinct target without adding information.
731        if !plan.many && plan.local_key != "id" {
732            return self
733                .data_service
734                .metadata
735                .context
736                .decode_compact_entity_batch_into_graph(
737                    &plan.target_entity,
738                    child_rows,
739                    root,
740                    graph,
741                )
742                .map_err(DataServiceError::Entity);
743        }
744
745        let mut buckets: BTreeMap<FlatIdentityKey, Vec<CompactRow>> = BTreeMap::new();
746        for child in child_rows {
747            if let Some(key) = child.get(&plan.foreign_key) {
748                buckets
749                    .entry(FlatIdentityKey::from_value(key))
750                    .or_default()
751                    .push(child);
752            }
753        }
754
755        let context = self.data_service.metadata.context;
756        for parent in parent_rows {
757            let Some(local_value) = parent.get(&plan.local_key) else {
758                continue;
759            };
760            let related = buckets
761                .remove(&FlatIdentityKey::from_value(local_value))
762                .unwrap_or_default();
763
764            if plan.many || plan.local_key == "id" {
765                let owner_id = parent.get("id").and_then(Value::try_u64).ok_or_else(|| {
766                    DataServiceError::Entity(teaql_core::EntityError::new(
767                        &plan.parent_entity,
768                        "loaded reverse relation owner is missing its u64 id",
769                    ))
770                })?;
771                if plan.many {
772                    context
773                        .decode_compact_entity_list_into_graph(
774                            &plan.target_entity,
775                            related,
776                            root,
777                            graph,
778                            &plan.parent_entity,
779                            owner_id,
780                            &plan.relation_name,
781                        )
782                        .map_err(DataServiceError::Entity)?;
783                } else {
784                    context
785                        .decode_compact_entity_option_into_graph(
786                            &plan.target_entity,
787                            related,
788                            root,
789                            graph,
790                            &plan.parent_entity,
791                            owner_id,
792                            &plan.relation_name,
793                        )
794                        .map_err(DataServiceError::Entity)?;
795                }
796            } else {
797                for child in related {
798                    context
799                        .decode_compact_entity_into_graph(&plan.target_entity, child, root, graph)
800                        .map_err(DataServiceError::Entity)?;
801                }
802            }
803        }
804        Ok(())
805    }
806
807    fn enhance_child_record<'b>(
808        &'b self,
809        child: &'b mut std::collections::BTreeMap<String, Value>,
810        plans: &'b [RelationLoadPlan],
811    ) -> std::pin::Pin<
812        Box<dyn std::future::Future<Output = Result<(), DataServiceError<E::Error>>> + Send + 'b>,
813    > {
814        Box::pin(async move {
815            for plan in plans {
816                let mut row = CompactRow::from_map(std::mem::take(child));
817                self.enhance_plan(slice::from_mut(&mut row), plan).await?;
818                *child = row.into_map();
819            }
820            Ok(())
821        })
822    }
823
824    fn query_for_plan(&self, plan: &RelationLoadPlan, parent_rows: &[CompactRow]) -> SelectQuery {
825        // Relation identities are a set. Keeping one value per normalized identity avoids
826        // compiling and binding the same foreign key once for every parent row (a common shape
827        // for pages containing many rows that share a small reference table).
828        let ids = unique_relation_values(parent_rows, &plan.local_key);
829
830        let mut query = plan
831            .query
832            .clone()
833            .unwrap_or_else(|| SelectQuery::new(plan.target_entity.clone()));
834        query.entity = plan.target_entity.clone();
835        ensure_projection(&mut query, &plan.foreign_key);
836        for child in &plan.children {
837            ensure_projection(&mut query, &child.local_key);
838        }
839        if !ids.is_empty() {
840            query = query.and_filter(Expr::in_list(plan.foreign_key.clone(), ids));
841        }
842        if query.slice.is_some() {
843            query.partition_by = Some(plan.foreign_key.clone());
844        }
845        query
846    }
847
848    async fn fetch_relation_rows(
849        &self,
850        child_repo: &EntityDataService<'a, E>,
851        plan: &RelationLoadPlan,
852        parent_rows: &[CompactRow],
853        compact: bool,
854    ) -> Result<Vec<CompactRow>, DataServiceError<E::Error>> {
855        let ids = unique_relation_values(parent_rows, &plan.local_key);
856        if ids.is_empty() {
857            return Ok(Vec::new());
858        }
859
860        let capabilities =
861            teaql_data_service::DataServiceExecutor::capabilities(self.data_service.executor);
862        let probe = should_use_small_parent_relation_probes(&capabilities, plan, ids.len());
863
864        if !probe {
865            let query = if compact {
866                self.query_for_compact_plan(plan, parent_rows)
867            } else {
868                self.query_for_plan(plan, parent_rows)
869            };
870            return child_repo.fetch_compact_all_internal(query).await;
871        }
872
873        // With execution metadata disabled, retain one semantic partition query and let an
874        // embedded provider execute its indexed parent probes behind one executor boundary.
875        // When metadata is enabled we intentionally keep one observable result per SQL probe.
876        if !self.data_service.metadata.capture_execution_metadata() {
877            let query = if compact {
878                self.query_for_compact_plan(plan, parent_rows)
879            } else {
880                self.query_for_plan(plan, parent_rows)
881            };
882            return child_repo.fetch_compact_all_internal(query).await;
883        }
884
885        let mut rows = Vec::new();
886        for id in ids {
887            let mut query = self.base_relation_query(plan);
888            query = query.and_filter(Expr::eq(plan.foreign_key.clone(), id));
889            // The slice now belongs to one parent, so no window partition is needed.
890            query.partition_by = None;
891            rows.extend(child_repo.fetch_compact_all_internal(query).await?);
892        }
893        Ok(rows)
894    }
895
896    fn base_relation_query(&self, plan: &RelationLoadPlan) -> SelectQuery {
897        let mut query = plan
898            .query
899            .clone()
900            .unwrap_or_else(|| SelectQuery::new(plan.target_entity.clone()));
901        query.entity = plan.target_entity.clone();
902        ensure_projection(&mut query, &plan.foreign_key);
903        for child in &plan.children {
904            ensure_projection(&mut query, &child.local_key);
905        }
906        query
907    }
908
909    fn query_for_compact_plan(
910        &self,
911        plan: &RelationLoadPlan,
912        parent_rows: &[CompactRow],
913    ) -> SelectQuery {
914        let ids = unique_relation_values(parent_rows, &plan.local_key);
915        let mut query = plan
916            .query
917            .clone()
918            .unwrap_or_else(|| SelectQuery::new(plan.target_entity.clone()));
919        query.entity = plan.target_entity.clone();
920        // The flat hydrator owns the relation tree and recursively loads each child plan into
921        // one shared identity graph. Leaving nested relations on this single-layer query makes
922        // fetch_compact_all_internal hydrate the same subtree first; the flat hydrator then
923        // hydrates it again, causing an exponential number of duplicate relation queries.
924        query.relations.clear();
925        ensure_projection(&mut query, &plan.foreign_key);
926        for child in &plan.children {
927            ensure_projection(&mut query, &child.local_key);
928        }
929        if !ids.is_empty() {
930            query = query.and_filter(Expr::in_list(plan.foreign_key.clone(), ids));
931        }
932        if query.slice.is_some() {
933            query.partition_by = Some(plan.foreign_key.clone());
934        }
935        query
936    }
937
938    fn attach_relation_rows(
939        &self,
940        parent_rows: &mut [CompactRow],
941        plan: &RelationLoadPlan,
942        child_rows: Vec<CompactRow>,
943    ) {
944        let inverse_relation = self
945            .data_service
946            .metadata
947            .context
948            .entity(&plan.target_entity)
949            .and_then(|descriptor| {
950                descriptor.relations.iter().find(|relation| {
951                    relation.target_entity == plan.parent_entity
952                        && relation.local_key == plan.foreign_key
953                        && relation.foreign_key == plan.local_key
954                })
955            })
956            .map(|relation| (relation.name.clone(), relation.many));
957
958        let mut buckets: BTreeMap<String, Vec<CompactRow>> = BTreeMap::new();
959        for child in child_rows.clone() {
960            if let Some(key) = child.get(&plan.foreign_key) {
961                buckets
962                    .entry(graph_identity_key(key))
963                    .or_default()
964                    .push(child);
965            }
966        }
967
968        for parent in parent_rows.iter_mut() {
969            let Some(local_value) = parent.get(&plan.local_key) else {
970                continue;
971            };
972            let bucket_key = graph_identity_key(local_value);
973            let related = buckets.get(&bucket_key).cloned().unwrap_or_default();
974            let related = match &inverse_relation {
975                Some((inverse_relation, inverse_many)) => {
976                    let mut parent_object = parent.clone();
977                    parent_object.remove(&plan.relation_name);
978                    related
979                        .into_iter()
980                        .map(|mut child| {
981                            match *inverse_many {
982                                true => {
983                                    if !child.contains_key(inverse_relation) {
984                                        child.insert(
985                                            inverse_relation.clone(),
986                                            Value::List(Vec::new()),
987                                        );
988                                    }
989                                    let entry = child
990                                        .get_mut(inverse_relation)
991                                        .expect("inverse relation was inserted immediately above");
992                                    if let Value::List(list) = entry {
993                                        list.push(Value::object(parent_object.clone().into_map()));
994                                    }
995                                }
996                                false => {
997                                    child.insert(
998                                        inverse_relation.clone(),
999                                        Value::object(parent_object.clone().into_map()),
1000                                    );
1001                                }
1002                            }
1003                            child
1004                        })
1005                        .collect::<Vec<_>>()
1006                }
1007                None => related,
1008            };
1009            match plan.many {
1010                true => {
1011                    parent.insert(
1012                        plan.relation_name.clone(),
1013                        Value::List(
1014                            related
1015                                .into_iter()
1016                                .map(|row| Value::object(row.into_map()))
1017                                .collect(),
1018                        ),
1019                    );
1020                }
1021                false => {
1022                    let value = related
1023                        .into_iter()
1024                        .next()
1025                        .map(|row| Value::object(row.into_map()))
1026                        .unwrap_or(Value::Null);
1027                    parent.insert(plan.relation_name.clone(), value);
1028                }
1029            }
1030        }
1031    }
1032}
1033
1034#[cfg(test)]
1035mod planner_tests {
1036    use super::*;
1037
1038    fn limited_many_plan() -> RelationLoadPlan {
1039        RelationLoadPlan {
1040            parent_entity: "Vendor".to_owned(),
1041            relation_name: "trips".to_owned(),
1042            path: "trips".to_owned(),
1043            target_entity: "Trip".to_owned(),
1044            local_key: "id".to_owned(),
1045            foreign_key: "vendor_id".to_owned(),
1046            many: true,
1047            query: Some(SelectQuery::new("Trip").order_desc("id").limit(10)),
1048            children: Vec::new(),
1049        }
1050    }
1051
1052    #[test]
1053    fn small_parent_probe_requires_provider_opt_in_and_bounded_parent_set() {
1054        let plan = limited_many_plan();
1055        let mut capabilities = teaql_data_service::DataServiceCapabilities::default();
1056        assert!(!should_use_small_parent_relation_probes(
1057            &capabilities,
1058            &plan,
1059            6
1060        ));
1061
1062        capabilities.small_parent_relation_probes = true;
1063        assert!(should_use_small_parent_relation_probes(
1064            &capabilities,
1065            &plan,
1066            6
1067        ));
1068        assert!(!should_use_small_parent_relation_probes(
1069            &capabilities,
1070            &plan,
1071            SMALL_PARENT_RELATION_PROBE_LIMIT + 1
1072        ));
1073    }
1074}