Skip to main content

teaql_core/
query.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use crate::{Expr, Value};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum SortDirection {
8    Asc,
9    Desc,
10}
11
12#[cfg(test)]
13mod hard_limit_tests {
14    use super::*;
15
16    #[test]
17    fn list_limit_defaults_rejects_and_allows_explicit_override() {
18        assert_eq!(
19            SelectQuery::new("Order")
20                .prepare_for_list()
21                .unwrap()
22                .slice
23                .unwrap()
24                .limit,
25            Some(10_000)
26        );
27        assert!(
28            SelectQuery::new("Order")
29                .limit(10_001)
30                .prepare_for_list()
31                .is_err()
32        );
33        assert!(
34            SelectQuery::new("Order")
35                .limit(10_001)
36                .hard_limit(20_000)
37                .prepare_for_list()
38                .is_ok()
39        );
40    }
41
42    #[test]
43    fn continuous_page_fetch_is_explicit_and_validated() {
44        assert!(SelectQuery::new("Order").continuous_page_fetch.is_none());
45        let query =
46            SelectQuery::new("Order").optimize_for_continuous_page_fetch_with("recent-orders", 30);
47        let options = query.continuous_page_fetch.unwrap();
48        assert_eq!(options.namespace, "recent-orders");
49        assert_eq!(options.ttl_seconds, 30);
50    }
51
52    #[test]
53    #[should_panic(expected = "continuous page namespace must not be empty")]
54    fn continuous_page_fetch_rejects_empty_namespace() {
55        let _ = SelectQuery::new("Order").optimize_for_continuous_page_fetch_with(" ", 30);
56    }
57
58    #[test]
59    fn id_set_pagination_is_explicit_and_validated() {
60        assert!(SelectQuery::new("Order").id_set_pagination.is_none());
61        let query = SelectQuery::new("Order").optimize_pagination_with_id_set_config(
62            "recent-orders",
63            30,
64            5_000,
65        );
66        let options = query.id_set_pagination.expect("ID set options");
67        assert_eq!(options.namespace, "recent-orders");
68        assert_eq!(options.ttl_seconds, 30);
69        assert_eq!(options.max_ids, 5_000);
70    }
71
72    #[test]
73    #[should_panic(expected = "ID set pagination max_ids must be positive")]
74    fn id_set_pagination_rejects_zero_limit() {
75        let _ = SelectQuery::new("Order").optimize_pagination_with_id_set_config("orders", 30, 0);
76    }
77
78    #[test]
79    fn topn_001_002_003_probe_threshold_is_explicit_and_bounded() {
80        assert!(
81            SelectQuery::new("Trip")
82                .top_n_probe_parent_threshold
83                .is_none()
84        );
85        assert_eq!(
86            SelectQuery::new("Trip")
87                .top_n_probe_parent_threshold(32)
88                .top_n_probe_parent_threshold,
89            Some(32)
90        );
91        assert_eq!(
92            SelectQuery::new("Trip")
93                .top_n_probe_parent_threshold(0)
94                .top_n_probe_parent_threshold,
95            Some(0)
96        );
97    }
98}
99
100#[derive(Debug, Clone, PartialEq)]
101pub struct NamedExpr {
102    pub alias: String,
103    pub expr: Expr,
104}
105
106impl NamedExpr {
107    pub fn new(alias: impl Into<String>, expr: Expr) -> Self {
108        Self {
109            alias: alias.into(),
110            expr,
111        }
112    }
113}
114
115#[derive(Debug, Clone, PartialEq)]
116pub struct OrderBy {
117    pub field: String,
118    pub expr: Option<Expr>,
119    pub direction: SortDirection,
120}
121
122impl OrderBy {
123    pub fn new(field: impl Into<String>, direction: SortDirection) -> Self {
124        Self {
125            field: field.into(),
126            expr: None,
127            direction,
128        }
129    }
130
131    pub fn expr(expr: Expr, direction: SortDirection) -> Self {
132        Self {
133            field: String::new(),
134            expr: Some(expr),
135            direction,
136        }
137    }
138
139    pub fn asc(field: impl Into<String>) -> Self {
140        Self::new(field, SortDirection::Asc)
141    }
142
143    pub fn desc(field: impl Into<String>) -> Self {
144        Self::new(field, SortDirection::Desc)
145    }
146
147    pub fn asc_expr(expr: Expr) -> Self {
148        Self::expr(expr, SortDirection::Asc)
149    }
150
151    pub fn desc_expr(expr: Expr) -> Self {
152        Self::expr(expr, SortDirection::Desc)
153    }
154
155    pub fn asc_gbk(field: impl Into<String>) -> Self {
156        Self::asc_expr(Expr::gbk(Expr::column(field)))
157    }
158
159    pub fn desc_gbk(field: impl Into<String>) -> Self {
160        Self::desc_expr(Expr::gbk(Expr::column(field)))
161    }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum AggregateFunction {
166    Count,
167    Sum,
168    Avg,
169    Min,
170    Max,
171    Stddev,
172    StddevPop,
173    VarSamp,
174    VarPop,
175    BitAnd,
176    BitOr,
177    BitXor,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct Aggregate {
182    pub function: AggregateFunction,
183    pub field: String,
184    pub alias: String,
185}
186
187impl Aggregate {
188    pub fn new(
189        function: AggregateFunction,
190        field: impl Into<String>,
191        alias: impl Into<String>,
192    ) -> Self {
193        Self {
194            function,
195            field: field.into(),
196            alias: alias.into(),
197        }
198    }
199
200    pub fn count(alias: impl Into<String>) -> Self {
201        Self::new(AggregateFunction::Count, "*", alias)
202    }
203
204    pub fn count_field(field: impl Into<String>, alias: impl Into<String>) -> Self {
205        Self::new(AggregateFunction::Count, field, alias)
206    }
207
208    pub fn sum(field: impl Into<String>, alias: impl Into<String>) -> Self {
209        Self::new(AggregateFunction::Sum, field, alias)
210    }
211
212    pub fn avg(field: impl Into<String>, alias: impl Into<String>) -> Self {
213        Self::new(AggregateFunction::Avg, field, alias)
214    }
215
216    pub fn min(field: impl Into<String>, alias: impl Into<String>) -> Self {
217        Self::new(AggregateFunction::Min, field, alias)
218    }
219
220    pub fn max(field: impl Into<String>, alias: impl Into<String>) -> Self {
221        Self::new(AggregateFunction::Max, field, alias)
222    }
223
224    pub fn stddev(field: impl Into<String>, alias: impl Into<String>) -> Self {
225        Self::new(AggregateFunction::Stddev, field, alias)
226    }
227
228    pub fn stddev_pop(field: impl Into<String>, alias: impl Into<String>) -> Self {
229        Self::new(AggregateFunction::StddevPop, field, alias)
230    }
231
232    pub fn var_samp(field: impl Into<String>, alias: impl Into<String>) -> Self {
233        Self::new(AggregateFunction::VarSamp, field, alias)
234    }
235
236    pub fn var_pop(field: impl Into<String>, alias: impl Into<String>) -> Self {
237        Self::new(AggregateFunction::VarPop, field, alias)
238    }
239
240    pub fn bit_and(field: impl Into<String>, alias: impl Into<String>) -> Self {
241        Self::new(AggregateFunction::BitAnd, field, alias)
242    }
243
244    pub fn bit_or(field: impl Into<String>, alias: impl Into<String>) -> Self {
245        Self::new(AggregateFunction::BitOr, field, alias)
246    }
247
248    pub fn bit_xor(field: impl Into<String>, alias: impl Into<String>) -> Self {
249        Self::new(AggregateFunction::BitXor, field, alias)
250    }
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub struct Slice {
255    pub limit: Option<u64>,
256    pub offset: u64,
257}
258
259#[derive(Debug, Clone, PartialEq)]
260pub struct RelationLoad {
261    pub name: String,
262    pub query: Option<Box<SelectQuery>>,
263}
264
265impl RelationLoad {
266    pub fn new(name: impl Into<String>) -> Self {
267        Self {
268            name: name.into(),
269            query: None,
270        }
271    }
272
273    pub fn with_query(name: impl Into<String>, query: SelectQuery) -> Self {
274        Self {
275            name: name.into(),
276            query: Some(Box::new(query)),
277        }
278    }
279}
280
281#[derive(Debug, Clone, PartialEq)]
282pub struct RelationAggregate {
283    pub relation_name: String,
284    pub alias: String,
285    pub query: SelectQuery,
286    pub single_result: bool,
287}
288
289impl RelationAggregate {
290    pub fn new(
291        relation_name: impl Into<String>,
292        alias: impl Into<String>,
293        query: SelectQuery,
294        single_result: bool,
295    ) -> Self {
296        Self {
297            relation_name: relation_name.into(),
298            alias: alias.into(),
299            query,
300            single_result,
301        }
302    }
303}
304
305#[derive(Debug, Clone, PartialEq, Eq)]
306pub struct RawSqlProjection {
307    pub property_name: String,
308    pub raw_sql_segment: String,
309}
310
311impl RawSqlProjection {
312    pub fn new(property_name: impl Into<String>, raw_sql_segment: impl Into<String>) -> Self {
313        Self {
314            property_name: property_name.into(),
315            raw_sql_segment: raw_sql_segment.into(),
316        }
317    }
318}
319
320#[derive(Debug, Clone, PartialEq)]
321pub struct ObjectGroupBy {
322    pub property_name: String,
323    pub storage_field: String,
324    pub query: SelectQuery,
325}
326
327impl ObjectGroupBy {
328    pub fn new(
329        property_name: impl Into<String>,
330        storage_field: impl Into<String>,
331        query: SelectQuery,
332    ) -> Self {
333        Self {
334            property_name: property_name.into(),
335            storage_field: storage_field.into(),
336            query,
337        }
338    }
339}
340
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342pub struct AggregationCacheOptions {
343    pub enabled: bool,
344    pub cache_expired_millis: u64,
345    pub propagate: bool,
346    pub propagate_cache_expired_millis: u64,
347}
348
349impl AggregationCacheOptions {
350    pub fn enabled(cache_expired_millis: u64) -> Self {
351        Self {
352            enabled: true,
353            cache_expired_millis,
354            propagate: false,
355            propagate_cache_expired_millis: 0,
356        }
357    }
358
359    pub fn propagate(mut self, cache_expired_millis: u64) -> Self {
360        self.propagate = true;
361        self.propagate_cache_expired_millis = cache_expired_millis;
362        self
363    }
364}
365
366#[derive(Debug, Clone, PartialEq)]
367pub struct StreamConfig {
368    pub chunk_size: usize,
369}
370
371#[derive(Debug, Clone, PartialEq, Eq)]
372pub struct ContinuousPageFetchOptions {
373    pub namespace: String,
374    pub ttl_seconds: u64,
375}
376
377#[derive(Debug, Clone, PartialEq, Eq)]
378pub struct IdSetPaginationOptions {
379    pub namespace: String,
380    pub ttl_seconds: u64,
381    pub max_ids: u64,
382}
383
384impl IdSetPaginationOptions {
385    pub const DEFAULT_TTL_SECONDS: u64 = 600;
386    pub const DEFAULT_MAX_IDS: u64 = 3_000_000;
387
388    pub fn new(namespace: impl Into<String>, ttl_seconds: u64, max_ids: u64) -> Self {
389        let namespace = namespace.into();
390        assert!(
391            !namespace.trim().is_empty(),
392            "ID set pagination namespace must not be empty"
393        );
394        assert!(
395            ttl_seconds > 0,
396            "ID set pagination ttl_seconds must be positive"
397        );
398        assert!(max_ids > 0, "ID set pagination max_ids must be positive");
399        Self {
400            namespace,
401            ttl_seconds,
402            max_ids,
403        }
404    }
405}
406
407impl ContinuousPageFetchOptions {
408    pub const DEFAULT_TTL_SECONDS: u64 = 600;
409
410    pub fn new(namespace: impl Into<String>, ttl_seconds: u64) -> Self {
411        let namespace = namespace.into();
412        assert!(
413            !namespace.trim().is_empty(),
414            "continuous page namespace must not be empty"
415        );
416        assert!(
417            ttl_seconds > 0,
418            "continuous page ttl_seconds must be positive"
419        );
420        Self {
421            namespace,
422            ttl_seconds,
423        }
424    }
425}
426
427impl Default for StreamConfig {
428    fn default() -> Self {
429        Self { chunk_size: 1000 }
430    }
431}
432
433#[derive(Debug, Clone, PartialEq)]
434pub struct SelectQuery {
435    /// Safety ceiling for a fully materialized outer query.
436    pub hard_limit: u64,
437    pub entity: String,
438    pub projection: Vec<String>,
439    pub expr_projection: Vec<NamedExpr>,
440    pub search_with_text: Option<String>,
441    pub filter: Option<Expr>,
442    pub having: Option<Expr>,
443    pub order_by: Vec<OrderBy>,
444    pub slice: Option<Slice>,
445    /// Apply `slice` independently inside each value of this property.
446    pub partition_by: Option<String>,
447    /// Optional bounded-probe override for per-parent Top-N relation loading.
448    /// Zero forces the provider's window plan; a positive value permits probes
449    /// only when the already-loaded parent count does not exceed the value.
450    pub top_n_probe_parent_threshold: Option<usize>,
451    pub aggregates: Vec<Aggregate>,
452    pub group_by: Vec<String>,
453    pub relations: Vec<RelationLoad>,
454    /// Query-only Facet selections retained across ordinary relation planning.
455    /// The relation loader executes these, never the physical SQL compiler.
456    pub facets: Vec<crate::request::FacetRequest>,
457    /// Runtime-assembled related metrics retained by nested typed selections.
458    /// These are not physical SQL aggregates or relation projection requests.
459    pub relation_aggregates: Vec<RelationAggregate>,
460    pub aggregation_cache: Option<AggregationCacheOptions>,
461    pub comment: Option<String>,
462    /// Explicit request purpose; trace nodes are diagnostic lineage, not intent.
463    pub purpose: Option<String>,
464    pub trace_chain: Vec<crate::TraceNode>,
465    pub raw_sql: Option<String>,
466    pub raw_sql_search_criteria: Vec<String>,
467    pub dynamic_properties: Vec<RawSqlProjection>,
468    /// Optional readonly result schema; no SQL fields, presence claims or persistence authority.
469    pub dynamic_property_definitions:
470        Option<Arc<crate::dynamic_properties::DynamicPropertyDefinitions>>,
471    /// Persistent extensions loaded by the context-owned provider, never compiled as physical SQL fields.
472    pub dynamic_field_selection:
473        Option<std::sync::Arc<crate::dynamic_fields::DynamicFieldSelection>>,
474    pub raw_projections: Vec<RawSqlProjection>,
475    pub object_group_bys: Vec<ObjectGroupBy>,
476    pub child_enhancements: Vec<SelectQuery>,
477    pub stream_config: Option<StreamConfig>,
478    /// Explicit, process-local hint for transparent seek pagination of outer list queries.
479    pub continuous_page_fetch: Option<ContinuousPageFetchOptions>,
480    /// Explicit hint to retain the complete ordered ID sequence for pagination.
481    pub id_set_pagination: Option<IdSetPaginationOptions>,
482}
483
484impl SelectQuery {
485    pub fn new(entity: impl Into<String>) -> Self {
486        Self {
487            hard_limit: 10_000,
488            entity: entity.into(),
489            projection: Vec::new(),
490            expr_projection: Vec::new(),
491            search_with_text: None,
492            filter: None,
493            having: None,
494            order_by: Vec::new(),
495            slice: None,
496            partition_by: None,
497            top_n_probe_parent_threshold: None,
498            aggregates: Vec::new(),
499            group_by: Vec::new(),
500            relations: Vec::new(),
501            facets: Vec::new(),
502            relation_aggregates: Vec::new(),
503            aggregation_cache: None,
504            comment: None,
505            purpose: None,
506            trace_chain: Vec::new(),
507            raw_sql: None,
508            raw_sql_search_criteria: Vec::new(),
509            dynamic_properties: Vec::new(),
510            dynamic_property_definitions: None,
511            dynamic_field_selection: None,
512            raw_projections: Vec::new(),
513            object_group_bys: Vec::new(),
514            child_enhancements: Vec::new(),
515            stream_config: None,
516            continuous_page_fetch: None,
517            id_set_pagination: None,
518        }
519    }
520
521    pub fn project(mut self, field: impl Into<String>) -> Self {
522        self.projection.push(field.into());
523        self
524    }
525
526    pub fn select_dynamic_fields(
527        mut self,
528        selection: crate::dynamic_fields::DynamicFieldSelection,
529    ) -> Self {
530        self.dynamic_field_selection = Some(std::sync::Arc::new(selection));
531        self
532    }
533
534    pub fn projects(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
535        self.projection.extend(fields.into_iter().map(Into::into));
536        self
537    }
538
539    pub fn project_expr(mut self, alias: impl Into<String>, expr: Expr) -> Self {
540        self.expr_projection.push(NamedExpr::new(alias, expr));
541        self
542    }
543
544    pub fn project_raw(
545        mut self,
546        alias: impl Into<String>,
547        raw_sql_segment: impl Into<String>,
548    ) -> Self {
549        self.raw_projections
550            .push(RawSqlProjection::new(alias, raw_sql_segment));
551        self
552    }
553
554    pub fn dynamic_property_raw(
555        mut self,
556        alias: impl Into<String>,
557        raw_sql_segment: impl Into<String>,
558    ) -> Self {
559        self.dynamic_properties
560            .push(RawSqlProjection::new(alias, raw_sql_segment));
561        self
562    }
563
564    pub fn with_dynamic_property_definitions(
565        mut self,
566        definitions: Arc<crate::dynamic_properties::DynamicPropertyDefinitions>,
567    ) -> Self {
568        self.dynamic_property_definitions = Some(definitions);
569        self
570    }
571
572    pub fn search_with_text(mut self, text: impl Into<String>) -> Self {
573        self.search_with_text = Some(text.into());
574        self
575    }
576
577    pub fn filter(mut self, filter: Expr) -> Self {
578        self.filter = Some(filter);
579        self
580    }
581
582    pub fn and_filter(mut self, filter: Expr) -> Self {
583        self.filter = Some(match self.filter.take() {
584            Some(existing) => existing.and_expr(filter),
585            None => filter,
586        });
587        self
588    }
589
590    pub fn or_filter(mut self, filter: Expr) -> Self {
591        self.filter = Some(match self.filter.take() {
592            Some(existing) => existing.or_expr(filter),
593            None => filter,
594        });
595        self
596    }
597
598    pub fn having(mut self, having: Expr) -> Self {
599        self.having = Some(having);
600        self
601    }
602
603    pub fn and_having(mut self, having: Expr) -> Self {
604        self.having = Some(match self.having.take() {
605            Some(existing) => existing.and_expr(having),
606            None => having,
607        });
608        self
609    }
610
611    pub fn or_having(mut self, having: Expr) -> Self {
612        self.having = Some(match self.having.take() {
613            Some(existing) => existing.or_expr(having),
614            None => having,
615        });
616        self
617    }
618
619    pub fn order_by(mut self, order: OrderBy) -> Self {
620        self.order_by.push(order);
621        self
622    }
623
624    pub fn order_asc(self, field: impl Into<String>) -> Self {
625        self.order_by(OrderBy::asc(field))
626    }
627
628    pub fn order_desc(self, field: impl Into<String>) -> Self {
629        self.order_by(OrderBy::desc(field))
630    }
631
632    pub fn order_expr_asc(self, expr: Expr) -> Self {
633        self.order_by(OrderBy::asc_expr(expr))
634    }
635
636    pub fn order_expr_desc(self, expr: Expr) -> Self {
637        self.order_by(OrderBy::desc_expr(expr))
638    }
639
640    pub fn order_gbk_asc(self, field: impl Into<String>) -> Self {
641        self.order_by(OrderBy::asc_gbk(field))
642    }
643
644    pub fn order_gbk_desc(self, field: impl Into<String>) -> Self {
645        self.order_by(OrderBy::desc_gbk(field))
646    }
647
648    pub fn group_by(mut self, field: impl Into<String>) -> Self {
649        self.group_by.push(field.into());
650        self
651    }
652
653    pub fn aggregate(mut self, aggregate: Aggregate) -> Self {
654        self.aggregates.push(aggregate);
655        self
656    }
657
658    pub fn count(self, alias: impl Into<String>) -> Self {
659        self.aggregate(Aggregate::count(alias))
660    }
661
662    pub fn count_field(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
663        self.aggregate(Aggregate::count_field(field, alias))
664    }
665
666    pub fn sum(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
667        self.aggregate(Aggregate::sum(field, alias))
668    }
669
670    pub fn avg(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
671        self.aggregate(Aggregate::avg(field, alias))
672    }
673
674    pub fn min(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
675        self.aggregate(Aggregate::min(field, alias))
676    }
677
678    pub fn max(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
679        self.aggregate(Aggregate::max(field, alias))
680    }
681
682    pub fn stddev(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
683        self.aggregate(Aggregate::stddev(field, alias))
684    }
685
686    pub fn stddev_pop(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
687        self.aggregate(Aggregate::stddev_pop(field, alias))
688    }
689
690    pub fn var_samp(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
691        self.aggregate(Aggregate::var_samp(field, alias))
692    }
693
694    pub fn var_pop(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
695        self.aggregate(Aggregate::var_pop(field, alias))
696    }
697
698    pub fn bit_and(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
699        self.aggregate(Aggregate::bit_and(field, alias))
700    }
701
702    pub fn bit_or(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
703        self.aggregate(Aggregate::bit_or(field, alias))
704    }
705
706    pub fn bit_xor(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
707        self.aggregate(Aggregate::bit_xor(field, alias))
708    }
709
710    pub fn enable_aggregation_cache(self) -> Self {
711        self.enable_aggregation_cache_for(0)
712    }
713
714    pub fn enable_aggregation_cache_for(mut self, cache_expired_millis: u64) -> Self {
715        self.aggregation_cache = Some(AggregationCacheOptions::enabled(cache_expired_millis));
716        self
717    }
718
719    pub fn propagate_aggregation_cache(mut self, cache_expired_millis: u64) -> Self {
720        self.aggregation_cache = Some(
721            self.aggregation_cache
722                .unwrap_or_else(|| AggregationCacheOptions::enabled(0))
723                .propagate(cache_expired_millis),
724        );
725        self
726    }
727
728    pub fn comment(mut self, comment: impl Into<String>) -> Self {
729        let comment_str = comment.into();
730        self.comment = Some(comment_str.clone());
731        self.trace_chain.push(crate::TraceNode {
732            kind: crate::TraceKind::Comment,
733            entity_type: self.entity.clone(),
734            entity_id: None,
735            comment: comment_str,
736        });
737        self
738    }
739
740    pub fn purpose(mut self, purpose: impl Into<String>) -> Self {
741        self.purpose = Some(purpose.into());
742        self
743    }
744
745    pub fn raw_sql(mut self, raw_sql: impl Into<String>) -> Self {
746        self.raw_sql = Some(raw_sql.into());
747        self
748    }
749
750    pub fn raw_sql_search_criteria(mut self, raw_sql: impl Into<String>) -> Self {
751        self.raw_sql_search_criteria.push(raw_sql.into());
752        self
753    }
754
755    pub fn object_group_by(
756        mut self,
757        property_name: impl Into<String>,
758        storage_field: impl Into<String>,
759        query: SelectQuery,
760    ) -> Self {
761        self.object_group_bys
762            .push(ObjectGroupBy::new(property_name, storage_field, query));
763        self
764    }
765
766    pub fn child_enhancement(mut self, query: SelectQuery) -> Self {
767        self.child_enhancements.push(query);
768        self
769    }
770
771    pub fn relation(mut self, name: impl Into<String>) -> Self {
772        self.relations.push(RelationLoad::new(name));
773        self
774    }
775
776    pub fn relation_query(mut self, name: impl Into<String>, query: SelectQuery) -> Self {
777        self.relations.push(RelationLoad::with_query(name, query));
778        self
779    }
780
781    pub fn limit(mut self, limit: u64) -> Self {
782        let slice = self.slice.get_or_insert(Slice {
783            limit: None,
784            offset: 0,
785        });
786        slice.limit = Some(limit);
787        self
788    }
789
790    /// Override the outer materialized-list ceiling. Most callers should keep 10,000.
791    pub fn hard_limit(mut self, hard_limit: u64) -> Self {
792        assert!(hard_limit > 0, "hard_limit must be positive");
793        self.hard_limit = hard_limit;
794        self
795    }
796
797    /// Apply and validate list-materialization limits. This is intentionally not
798    /// used by streaming execution.
799    pub fn prepare_for_list(mut self) -> Result<Self, String> {
800        self.apply_list_limit(self.hard_limit, true)?;
801        Ok(self)
802    }
803
804    fn apply_list_limit(&mut self, ceiling: u64, outer: bool) -> Result<(), String> {
805        let slice = self.slice.get_or_insert(Slice {
806            limit: None,
807            offset: 0,
808        });
809        match slice.limit {
810            Some(limit) if limit > ceiling => {
811                return Err(format!(
812                    "QUERY_HARD_LIMIT_EXCEEDED: requested limit {limit} exceeds hard limit {ceiling}"
813                ));
814            }
815            None => slice.limit = Some(ceiling),
816            _ => {}
817        }
818        for relation in &mut self.relations {
819            if let Some(query) = relation.query.as_mut() {
820                query.apply_list_limit(10_000, false)?;
821            }
822        }
823        for query in &mut self.child_enhancements {
824            query.apply_list_limit(10_000, false)?;
825        }
826        let _ = outer;
827        Ok(())
828    }
829
830    pub fn offset(mut self, offset: u64) -> Self {
831        let slice = self.slice.get_or_insert(Slice {
832            limit: None,
833            offset: 0,
834        });
835        slice.offset = offset;
836        self
837    }
838
839    pub fn page(self, offset: u64, limit: u64) -> Self {
840        self.offset(offset).limit(limit)
841    }
842
843    pub fn optimize_for_continuous_page_fetch(mut self) -> Self {
844        self.continuous_page_fetch = Some(ContinuousPageFetchOptions::new(
845            "default",
846            ContinuousPageFetchOptions::DEFAULT_TTL_SECONDS,
847        ));
848        self
849    }
850
851    pub fn optimize_for_continuous_page_fetch_with(
852        mut self,
853        namespace: impl Into<String>,
854        ttl_seconds: u64,
855    ) -> Self {
856        self.continuous_page_fetch = Some(ContinuousPageFetchOptions::new(namespace, ttl_seconds));
857        self
858    }
859
860    pub fn optimize_pagination_with_id_set(mut self) -> Self {
861        self.id_set_pagination = Some(IdSetPaginationOptions::new(
862            "default",
863            IdSetPaginationOptions::DEFAULT_TTL_SECONDS,
864            IdSetPaginationOptions::DEFAULT_MAX_IDS,
865        ));
866        self
867    }
868
869    pub fn optimize_pagination_with_id_set_config(
870        mut self,
871        namespace: impl Into<String>,
872        ttl_seconds: u64,
873        max_ids: u64,
874    ) -> Self {
875        self.id_set_pagination = Some(IdSetPaginationOptions::new(namespace, ttl_seconds, max_ids));
876        self
877    }
878
879    /// Scope pagination to each distinct value of `field`.
880    ///
881    /// Relation loading sets this automatically. Most application queries
882    /// should use a generated relation selector instead of calling this
883    /// method directly.
884    pub fn partition_by(mut self, field: impl Into<String>) -> Self {
885        self.partition_by = Some(field.into());
886        self
887    }
888
889    /// Override the provider's per-parent Top-N execution policy.
890    ///
891    /// `0` selects the window plan. A positive threshold selects bounded
892    /// probes only when the parent rows already in memory are at or below it.
893    pub fn top_n_probe_parent_threshold(mut self, threshold: usize) -> Self {
894        self.top_n_probe_parent_threshold = Some(threshold);
895        self
896    }
897
898    /// Enable streaming mode with the given chunk size.
899    /// When streaming, rows are fetched and enhanced in batches rather than all at once.
900    pub fn stream(mut self, chunk_size: usize) -> Self {
901        self.stream_config = Some(StreamConfig { chunk_size });
902        self
903    }
904
905    /// Enable streaming mode with default chunk size (1000).
906    pub fn stream_default(mut self) -> Self {
907        self.stream_config = Some(StreamConfig::default());
908        self
909    }
910}
911
912pub type Record = BTreeMap<String, Value>;
913
914// Kept behind one optional pointer so rows without query-result metadata stay
915// compact. The result carrier is separate from projected/persistent values.
916#[derive(Debug, Clone, Default, PartialEq)]
917struct LoadedRelationResults {
918    lists: BTreeMap<String, crate::SmartList<CompactRow>>,
919    dynamic_fields: Option<crate::dynamic_fields::DynamicFieldValues>,
920}
921
922/// A database result row whose column names are shared by the whole result set.
923///
924/// Providers use this representation for typed decoding so a 100-row result does
925/// not allocate 100 copies of every projected column name (or 100 B-trees).
926/// Result-shape metadata shared by rows. Cached availability never contains values or ledgers.
927#[derive(Debug)]
928pub struct CompactRowLayout {
929    names: Arc<[String]>,
930    relation_names: Arc<[String]>,
931    dynamic_properties: Option<Arc<crate::dynamic_properties::DynamicPropertyDefinitions>>,
932    primary: std::sync::OnceLock<Arc<crate::LoadedSnapshot>>,
933    other_types:
934        std::sync::Mutex<std::collections::HashMap<usize, std::sync::Weak<crate::LoadedSnapshot>>>,
935}
936
937impl CompactRowLayout {
938    pub fn new(names: Arc<[String]>) -> Arc<Self> {
939        Self::with_relations(names, Arc::from([]))
940    }
941
942    fn with_relations(names: Arc<[String]>, relation_names: Arc<[String]>) -> Arc<Self> {
943        Self::with_metadata(names, relation_names, None)
944    }
945
946    fn with_metadata(
947        names: Arc<[String]>,
948        relation_names: Arc<[String]>,
949        dynamic_properties: Option<Arc<crate::dynamic_properties::DynamicPropertyDefinitions>>,
950    ) -> Arc<Self> {
951        Arc::new(Self {
952            names,
953            relation_names,
954            dynamic_properties,
955            primary: Default::default(),
956            other_types: Default::default(),
957        })
958    }
959
960    pub fn with_dynamic_property_definitions(
961        self: &Arc<Self>,
962        definitions: Arc<crate::dynamic_properties::DynamicPropertyDefinitions>,
963    ) -> Arc<Self> {
964        if self.dynamic_properties.as_ref() == Some(&definitions) {
965            return self.clone();
966        }
967        Self::with_metadata(
968            self.names.clone(),
969            self.relation_names.clone(),
970            Some(definitions),
971        )
972    }
973
974    fn load_state(&self, layout: Arc<crate::FieldLayout>) -> crate::eval::LoadState {
975        let primary = self.primary.get_or_init(|| {
976            crate::LoadedSnapshot::projection(
977                layout.clone(),
978                self.names
979                    .iter()
980                    .chain(self.relation_names.iter())
981                    .map(String::as_str),
982            )
983            .with_property_schema(self.dynamic_properties.clone())
984            .into_shared()
985        });
986        if Arc::ptr_eq(primary.layout(), &layout) {
987            return crate::eval::LoadState::Indexed(primary.clone());
988        }
989        // Unusual case: more than one typed decoder consumes one result shape.
990        let key = Arc::as_ptr(&layout) as usize;
991        let mut states = self
992            .other_types
993            .lock()
994            .unwrap_or_else(std::sync::PoisonError::into_inner);
995        states.retain(|_, state| state.strong_count() > 0);
996        if let Some(state) = states.get(&key).and_then(std::sync::Weak::upgrade) {
997            return crate::eval::LoadState::Indexed(state);
998        }
999        let state = crate::LoadedSnapshot::projection(
1000            layout,
1001            self.names
1002                .iter()
1003                .chain(self.relation_names.iter())
1004                .map(String::as_str),
1005        )
1006        .with_property_schema(self.dynamic_properties.clone())
1007        .into_shared();
1008        states.insert(key, Arc::downgrade(&state));
1009        crate::eval::LoadState::Indexed(state)
1010    }
1011}
1012
1013impl std::ops::Deref for CompactRowLayout {
1014    type Target = [String];
1015    fn deref(&self) -> &Self::Target {
1016        &self.names
1017    }
1018}
1019
1020impl PartialEq for CompactRowLayout {
1021    fn eq(&self, other: &Self) -> bool {
1022        self.names == other.names
1023            && self.relation_names == other.relation_names
1024            && self.dynamic_properties == other.dynamic_properties
1025    }
1026}
1027
1028#[derive(Debug, Clone, PartialEq)]
1029pub struct CompactRow {
1030    columns: Arc<CompactRowLayout>,
1031    values: Vec<Value>,
1032    // Read-result metadata only. Not a projected field, serialized Value, or
1033    // mutation snapshot. Ordinary provider rows allocate no sidecar.
1034    loaded_relations: Option<Box<LoadedRelationResults>>,
1035}
1036
1037/// Bounded to one hydration pass. Retaining the source shape prevents address
1038/// reuse from confusing projections while no entity values are retained.
1039#[doc(hidden)]
1040#[derive(Default)]
1041pub struct RelationShapeCache {
1042    relation: Option<String>,
1043    shapes: std::collections::HashMap<usize, (Arc<CompactRowLayout>, Arc<CompactRowLayout>)>,
1044}
1045
1046impl CompactRow {
1047    pub fn new(columns: Arc<[String]>, values: Vec<Value>) -> Self {
1048        Self::with_layout(CompactRowLayout::new(columns), values)
1049    }
1050
1051    pub fn with_layout(columns: Arc<CompactRowLayout>, values: Vec<Value>) -> Self {
1052        assert_eq!(
1053            columns.len(),
1054            values.len(),
1055            "row values must match the actual projection layout"
1056        );
1057        Self {
1058            columns,
1059            values,
1060            loaded_relations: None,
1061        }
1062    }
1063
1064    pub fn loaded_relation(&self, name: &str) -> Option<&crate::SmartList<CompactRow>> {
1065        self.loaded_relations.as_ref()?.lists.get(name)
1066    }
1067
1068    pub fn set_loaded_relation(&mut self, name: String, list: crate::SmartList<CompactRow>) {
1069        self.loaded_relations
1070            .get_or_insert_with(Default::default)
1071            .lists
1072            .insert(name, list);
1073    }
1074
1075    pub fn take_loaded_relation(&mut self, name: &str) -> Option<crate::SmartList<CompactRow>> {
1076        self.loaded_relations.as_mut()?.lists.remove(name)
1077    }
1078
1079    /// Framework read metadata, never an SQL column or mutation payload.
1080    #[doc(hidden)]
1081    pub fn set_loaded_dynamic_fields(&mut self, values: crate::dynamic_fields::DynamicFieldValues) {
1082        self.loaded_relations
1083            .get_or_insert_with(Default::default)
1084            .dynamic_fields = Some(values);
1085    }
1086
1087    #[doc(hidden)]
1088    pub fn merge_loaded_dynamic_fields(
1089        &mut self,
1090        values: crate::dynamic_fields::DynamicFieldValues,
1091        shapes: &mut crate::dynamic_fields::DynamicFieldMergeShapes,
1092    ) -> Result<(), crate::dynamic_fields::DynamicFieldError> {
1093        let metadata = self.loaded_relations.get_or_insert_with(Default::default);
1094        if let Some(current) = metadata.dynamic_fields.as_mut() {
1095            current.merge_loaded(values, shapes)?;
1096        } else {
1097            metadata.dynamic_fields = Some(values);
1098        }
1099        Ok(())
1100    }
1101
1102    #[doc(hidden)]
1103    pub fn take_loaded_dynamic_fields(
1104        &mut self,
1105    ) -> Option<crate::dynamic_fields::DynamicFieldValues> {
1106        self.loaded_relations.as_mut()?.dynamic_fields.take()
1107    }
1108
1109    pub fn has_loaded_relations(&self) -> bool {
1110        self.loaded_relations
1111            .as_ref()
1112            .is_some_and(|relations| !relations.lists.is_empty())
1113    }
1114
1115    /// Drop read-only relation metadata before retaining a persistence snapshot.
1116    pub fn clear_loaded_relations(&mut self) {
1117        self.loaded_relations = None;
1118    }
1119
1120    pub fn get(&self, name: &str) -> Option<&Value> {
1121        self.columns
1122            .iter()
1123            .position(|column| column == name)
1124            .and_then(|index| self.values.get(index))
1125    }
1126
1127    pub fn shared_columns(&self) -> Arc<[String]> {
1128        self.columns.names.clone()
1129    }
1130
1131    pub fn shared_layout(&self) -> Arc<CompactRowLayout> {
1132        self.columns.clone()
1133    }
1134
1135    pub fn indexed_load_state(&self, layout: Arc<crate::FieldLayout>) -> crate::eval::LoadState {
1136        self.columns.load_state(layout)
1137    }
1138
1139    pub fn validate_dynamic_properties(&self) -> Result<(), String> {
1140        if let Some(definitions) = &self.columns.dynamic_properties {
1141            for (name, value) in self.iter() {
1142                definitions.validate_value(name, value)?;
1143            }
1144        }
1145        Ok(())
1146    }
1147
1148    /// One schema binding per actual result layout, never a dictionary copy per row.
1149    pub fn attach_dynamic_property_definitions(
1150        rows: &mut [Self],
1151        definitions: Arc<crate::dynamic_properties::DynamicPropertyDefinitions>,
1152    ) {
1153        let mut layouts = std::collections::HashMap::new();
1154        for row in rows {
1155            let key = Arc::as_ptr(&row.columns) as usize;
1156            row.columns = layouts
1157                .entry(key)
1158                .or_insert_with(|| {
1159                    (
1160                        row.columns.clone(),
1161                        row.columns
1162                            .with_dynamic_property_definitions(definitions.clone()),
1163                    )
1164                })
1165                .1
1166                .clone();
1167        }
1168    }
1169
1170    /// Query-only edge availability lives in the shared shape, never a row value.
1171    #[doc(hidden)]
1172    pub fn mark_relation_loaded(&mut self, name: &str, shapes: &mut RelationShapeCache) {
1173        if self.is_loaded_relation(name) {
1174            return;
1175        }
1176        let key = Arc::as_ptr(&self.columns) as usize;
1177        if shapes.relation.as_deref() != Some(name) {
1178            shapes.shapes.clear();
1179            shapes.relation = Some(name.to_owned());
1180        }
1181        self.columns = shapes
1182            .shapes
1183            .entry(key)
1184            .or_insert_with(|| {
1185                let mut relations = self.columns.relation_names.to_vec();
1186                relations.push(name.to_owned());
1187                (
1188                    self.columns.clone(),
1189                    CompactRowLayout::with_metadata(
1190                        self.columns.names.clone(),
1191                        relations.into(),
1192                        self.columns.dynamic_properties.clone(),
1193                    ),
1194                )
1195            })
1196            .1
1197            .clone();
1198    }
1199
1200    #[doc(hidden)]
1201    pub fn is_loaded_relation(&self, name: &str) -> bool {
1202        self.columns
1203            .relation_names
1204            .iter()
1205            .any(|relation| relation == name)
1206    }
1207
1208    /// Share actual shapes after relation hydration. The temporary pool cannot
1209    /// retain historical projections, entity values or mutation ownership.
1210    pub fn share_layouts(rows: &mut [Self]) {
1211        let mut shapes = std::collections::HashMap::<
1212            (
1213                Arc<[String]>,
1214                Arc<[String]>,
1215                Option<Arc<crate::dynamic_properties::DynamicPropertyDefinitions>>,
1216            ),
1217            Arc<CompactRowLayout>,
1218        >::new();
1219        let mut previous: Option<Arc<CompactRowLayout>> = None;
1220        for row in rows {
1221            if previous
1222                .as_ref()
1223                .is_some_and(|layout| Arc::ptr_eq(layout, &row.columns))
1224            {
1225                continue;
1226            }
1227            let layout = shapes
1228                .entry((
1229                    row.shared_columns(),
1230                    row.columns.relation_names.clone(),
1231                    row.columns.dynamic_properties.clone(),
1232                ))
1233                .or_insert_with(|| row.columns.clone())
1234                .clone();
1235            row.columns = layout.clone();
1236            previous = Some(layout);
1237        }
1238    }
1239
1240    pub fn get_mut(&mut self, name: &str) -> Option<&mut Value> {
1241        self.columns
1242            .iter()
1243            .position(|column| column == name)
1244            .and_then(|index| self.values.get_mut(index))
1245    }
1246
1247    /// Adds or replaces a projected value. Column layouts remain shared until
1248    /// an enhancement actually changes the shape of this row.
1249    pub fn insert(&mut self, name: String, value: Value) -> Option<Value> {
1250        if let Some(index) = self.columns.iter().position(|column| column == &name) {
1251            return Some(std::mem::replace(&mut self.values[index], value));
1252        }
1253        let mut columns = self.columns.to_vec();
1254        columns.push(name);
1255        self.columns = CompactRowLayout::with_metadata(
1256            columns.into(),
1257            self.columns.relation_names.clone(),
1258            self.columns.dynamic_properties.clone(),
1259        );
1260        self.values.push(value);
1261        None
1262    }
1263
1264    pub fn remove(&mut self, name: &str) -> Option<Value> {
1265        self.take_loaded_relation(name);
1266        let index = self.columns.iter().position(|column| column == name)?;
1267        let mut columns = self.columns.to_vec();
1268        columns.remove(index);
1269        self.columns = CompactRowLayout::with_metadata(
1270            columns.into(),
1271            self.columns.relation_names.clone(),
1272            self.columns.dynamic_properties.clone(),
1273        );
1274        Some(self.values.remove(index))
1275    }
1276
1277    pub fn extend(&mut self, other: CompactRow) {
1278        self.try_extend(other, &mut Default::default())
1279            .expect("incompatible CompactRow read metadata");
1280    }
1281
1282    /// Checked enhancement merge. Validate provenance before changing any
1283    /// native value, relation or read payload; callers retain a failed row.
1284    #[doc(hidden)]
1285    pub fn try_extend(
1286        &mut self,
1287        other: CompactRow,
1288        shapes: &mut crate::dynamic_fields::DynamicFieldMergeShapes,
1289    ) -> Result<(), crate::dynamic_fields::DynamicFieldError> {
1290        let property_definitions = match (
1291            &self.columns.dynamic_properties,
1292            &other.columns.dynamic_properties,
1293        ) {
1294            (Some(left), Some(right)) => Some(
1295                crate::dynamic_properties::DynamicPropertyDefinitions::merge(left, right).map_err(
1296                    |field| crate::dynamic_fields::DynamicFieldError {
1297                        code: "DYNAMIC_PROPERTY_TYPE_MISMATCH",
1298                        field,
1299                    },
1300                )?,
1301            ),
1302            (left, right) => left.clone().or_else(|| right.clone()),
1303        };
1304        if let Some(definitions) = &property_definitions {
1305            // Check the final values before changing either payload or schema.
1306            for (name, value) in self
1307                .iter()
1308                .filter(|(name, _)| !other.contains_key(name))
1309                .chain(other.iter())
1310            {
1311                definitions.validate_value(name, value).map_err(|_| {
1312                    crate::dynamic_fields::DynamicFieldError {
1313                        code: "DYNAMIC_PROPERTY_TYPE_MISMATCH",
1314                        field: name.clone(),
1315                    }
1316                })?;
1317            }
1318        }
1319        if let (Some(left), Some(right)) = (
1320            self.loaded_relations
1321                .as_ref()
1322                .and_then(|metadata| metadata.dynamic_fields.as_ref()),
1323            other
1324                .loaded_relations
1325                .as_ref()
1326                .and_then(|metadata| metadata.dynamic_fields.as_ref()),
1327        ) {
1328            left.validate_merge(right)?;
1329        }
1330        let mut relation_names: Option<Vec<String>> = None;
1331        for name in other.columns.relation_names.iter() {
1332            let existing = relation_names
1333                .as_deref()
1334                .unwrap_or(&self.columns.relation_names);
1335            if !existing.contains(name) {
1336                relation_names
1337                    .get_or_insert_with(|| self.columns.relation_names.to_vec())
1338                    .push(name.clone());
1339            }
1340        }
1341        if let Some(relations) = other.loaded_relations {
1342            if let Some(values) = relations.dynamic_fields {
1343                self.merge_loaded_dynamic_fields(values, shapes)?;
1344            }
1345            self.loaded_relations
1346                .get_or_insert_with(Default::default)
1347                .lists
1348                .extend(relations.lists);
1349        }
1350        let mut columns: Option<Vec<String>> = None;
1351        for (name, value) in other.columns.iter().zip(other.values) {
1352            let names = columns.as_deref().unwrap_or(&self.columns.names);
1353            if let Some(index) = names.iter().position(|column| column == name) {
1354                self.values[index] = value;
1355            } else {
1356                columns
1357                    .get_or_insert_with(|| self.columns.names.to_vec())
1358                    .push(name.clone());
1359                self.values.push(value);
1360            }
1361        }
1362        if columns.is_some()
1363            || relation_names.is_some()
1364            || property_definitions != self.columns.dynamic_properties
1365        {
1366            self.columns = CompactRowLayout::with_metadata(
1367                columns
1368                    .map(Arc::from)
1369                    .unwrap_or_else(|| self.columns.names.clone()),
1370                relation_names
1371                    .map(Arc::from)
1372                    .unwrap_or_else(|| self.columns.relation_names.clone()),
1373                property_definitions,
1374            );
1375        }
1376        Ok(())
1377    }
1378
1379    pub fn contains_key(&self, name: &str) -> bool {
1380        self.columns.iter().any(|column| column == name)
1381    }
1382
1383    pub fn len(&self) -> usize {
1384        self.values.len()
1385    }
1386
1387    pub fn is_empty(&self) -> bool {
1388        self.values.is_empty()
1389    }
1390
1391    pub fn iter(&self) -> impl Iterator<Item = (&String, &Value)> {
1392        self.columns.iter().zip(self.values.iter())
1393    }
1394
1395    pub fn keys(&self) -> impl Iterator<Item = &String> {
1396        self.columns.iter()
1397    }
1398
1399    pub fn values(&self) -> impl Iterator<Item = &Value> {
1400        self.values.iter()
1401    }
1402
1403    pub fn into_map(self) -> BTreeMap<String, Value> {
1404        self.columns.iter().cloned().zip(self.values).collect()
1405    }
1406
1407    /// Transitional boundary adapter. Core query providers should construct
1408    /// compact rows directly instead of routing through this function.
1409    pub fn from_map(values_by_name: BTreeMap<String, Value>) -> Self {
1410        let (columns, values): (Vec<_>, Vec<_>) = values_by_name.into_iter().unzip();
1411        Self::new(columns.into(), values)
1412    }
1413}
1414
1415impl From<BTreeMap<String, Value>> for CompactRow {
1416    fn from(values: BTreeMap<String, Value>) -> Self {
1417        Self::from_map(values)
1418    }
1419}
1420
1421/// Internal projection used to implement per-parent pagination for relation
1422/// loads. Runtime relation attachment removes it before exposing child rows.
1423pub const PARTITION_RANK_PROPERTY: &str = "__teaql_partition_rank";
1424
1425pub fn record_to_json_value(record: &Record) -> serde_json::Value {
1426    serde_json::Value::Object(
1427        record
1428            .iter()
1429            .map(|(key, value)| (key.clone(), value.to_json_value()))
1430            .collect(),
1431    )
1432}
1433
1434pub fn compact_row_to_json_value(row: &CompactRow) -> serde_json::Value {
1435    serde_json::Value::Object(
1436        row.iter()
1437            .map(|(key, value)| (key.clone(), value.to_json_value()))
1438            .collect(),
1439    )
1440}