Skip to main content

teaql_runtime/data_service/
relation.rs

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