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