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