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!(SelectQuery::new("Order")
28            .limit(10_001)
29            .prepare_for_list()
30            .is_err());
31        assert!(SelectQuery::new("Order")
32            .limit(10_001)
33            .hard_limit(20_000)
34            .prepare_for_list()
35            .is_ok());
36    }
37
38    #[test]
39    fn continuous_page_fetch_is_explicit_and_validated() {
40        assert!(SelectQuery::new("Order").continuous_page_fetch.is_none());
41        let query =
42            SelectQuery::new("Order").optimize_for_continuous_page_fetch_with("recent-orders", 30);
43        let options = query.continuous_page_fetch.unwrap();
44        assert_eq!(options.namespace, "recent-orders");
45        assert_eq!(options.ttl_seconds, 30);
46    }
47
48    #[test]
49    #[should_panic(expected = "continuous page namespace must not be empty")]
50    fn continuous_page_fetch_rejects_empty_namespace() {
51        let _ = SelectQuery::new("Order").optimize_for_continuous_page_fetch_with(" ", 30);
52    }
53}
54
55#[derive(Debug, Clone, PartialEq)]
56pub struct NamedExpr {
57    pub alias: String,
58    pub expr: Expr,
59}
60
61impl NamedExpr {
62    pub fn new(alias: impl Into<String>, expr: Expr) -> Self {
63        Self {
64            alias: alias.into(),
65            expr,
66        }
67    }
68}
69
70#[derive(Debug, Clone, PartialEq)]
71pub struct OrderBy {
72    pub field: String,
73    pub expr: Option<Expr>,
74    pub direction: SortDirection,
75}
76
77impl OrderBy {
78    pub fn new(field: impl Into<String>, direction: SortDirection) -> Self {
79        Self {
80            field: field.into(),
81            expr: None,
82            direction,
83        }
84    }
85
86    pub fn expr(expr: Expr, direction: SortDirection) -> Self {
87        Self {
88            field: String::new(),
89            expr: Some(expr),
90            direction,
91        }
92    }
93
94    pub fn asc(field: impl Into<String>) -> Self {
95        Self::new(field, SortDirection::Asc)
96    }
97
98    pub fn desc(field: impl Into<String>) -> Self {
99        Self::new(field, SortDirection::Desc)
100    }
101
102    pub fn asc_expr(expr: Expr) -> Self {
103        Self::expr(expr, SortDirection::Asc)
104    }
105
106    pub fn desc_expr(expr: Expr) -> Self {
107        Self::expr(expr, SortDirection::Desc)
108    }
109
110    pub fn asc_gbk(field: impl Into<String>) -> Self {
111        Self::asc_expr(Expr::gbk(Expr::column(field)))
112    }
113
114    pub fn desc_gbk(field: impl Into<String>) -> Self {
115        Self::desc_expr(Expr::gbk(Expr::column(field)))
116    }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum AggregateFunction {
121    Count,
122    Sum,
123    Avg,
124    Min,
125    Max,
126    Stddev,
127    StddevPop,
128    VarSamp,
129    VarPop,
130    BitAnd,
131    BitOr,
132    BitXor,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct Aggregate {
137    pub function: AggregateFunction,
138    pub field: String,
139    pub alias: String,
140}
141
142impl Aggregate {
143    pub fn new(
144        function: AggregateFunction,
145        field: impl Into<String>,
146        alias: impl Into<String>,
147    ) -> Self {
148        Self {
149            function,
150            field: field.into(),
151            alias: alias.into(),
152        }
153    }
154
155    pub fn count(alias: impl Into<String>) -> Self {
156        Self::new(AggregateFunction::Count, "*", alias)
157    }
158
159    pub fn count_field(field: impl Into<String>, alias: impl Into<String>) -> Self {
160        Self::new(AggregateFunction::Count, field, alias)
161    }
162
163    pub fn sum(field: impl Into<String>, alias: impl Into<String>) -> Self {
164        Self::new(AggregateFunction::Sum, field, alias)
165    }
166
167    pub fn avg(field: impl Into<String>, alias: impl Into<String>) -> Self {
168        Self::new(AggregateFunction::Avg, field, alias)
169    }
170
171    pub fn min(field: impl Into<String>, alias: impl Into<String>) -> Self {
172        Self::new(AggregateFunction::Min, field, alias)
173    }
174
175    pub fn max(field: impl Into<String>, alias: impl Into<String>) -> Self {
176        Self::new(AggregateFunction::Max, field, alias)
177    }
178
179    pub fn stddev(field: impl Into<String>, alias: impl Into<String>) -> Self {
180        Self::new(AggregateFunction::Stddev, field, alias)
181    }
182
183    pub fn stddev_pop(field: impl Into<String>, alias: impl Into<String>) -> Self {
184        Self::new(AggregateFunction::StddevPop, field, alias)
185    }
186
187    pub fn var_samp(field: impl Into<String>, alias: impl Into<String>) -> Self {
188        Self::new(AggregateFunction::VarSamp, field, alias)
189    }
190
191    pub fn var_pop(field: impl Into<String>, alias: impl Into<String>) -> Self {
192        Self::new(AggregateFunction::VarPop, field, alias)
193    }
194
195    pub fn bit_and(field: impl Into<String>, alias: impl Into<String>) -> Self {
196        Self::new(AggregateFunction::BitAnd, field, alias)
197    }
198
199    pub fn bit_or(field: impl Into<String>, alias: impl Into<String>) -> Self {
200        Self::new(AggregateFunction::BitOr, field, alias)
201    }
202
203    pub fn bit_xor(field: impl Into<String>, alias: impl Into<String>) -> Self {
204        Self::new(AggregateFunction::BitXor, field, alias)
205    }
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub struct Slice {
210    pub limit: Option<u64>,
211    pub offset: u64,
212}
213
214#[derive(Debug, Clone, PartialEq)]
215pub struct RelationLoad {
216    pub name: String,
217    pub query: Option<Box<SelectQuery>>,
218}
219
220impl RelationLoad {
221    pub fn new(name: impl Into<String>) -> Self {
222        Self {
223            name: name.into(),
224            query: None,
225        }
226    }
227
228    pub fn with_query(name: impl Into<String>, query: SelectQuery) -> Self {
229        Self {
230            name: name.into(),
231            query: Some(Box::new(query)),
232        }
233    }
234}
235
236#[derive(Debug, Clone, PartialEq)]
237pub struct RelationAggregate {
238    pub relation_name: String,
239    pub alias: String,
240    pub query: SelectQuery,
241    pub single_result: bool,
242}
243
244impl RelationAggregate {
245    pub fn new(
246        relation_name: impl Into<String>,
247        alias: impl Into<String>,
248        query: SelectQuery,
249        single_result: bool,
250    ) -> Self {
251        Self {
252            relation_name: relation_name.into(),
253            alias: alias.into(),
254            query,
255            single_result,
256        }
257    }
258}
259
260#[derive(Debug, Clone, PartialEq, Eq)]
261pub struct RawSqlProjection {
262    pub property_name: String,
263    pub raw_sql_segment: String,
264}
265
266impl RawSqlProjection {
267    pub fn new(property_name: impl Into<String>, raw_sql_segment: impl Into<String>) -> Self {
268        Self {
269            property_name: property_name.into(),
270            raw_sql_segment: raw_sql_segment.into(),
271        }
272    }
273}
274
275#[derive(Debug, Clone, PartialEq)]
276pub struct ObjectGroupBy {
277    pub property_name: String,
278    pub storage_field: String,
279    pub query: SelectQuery,
280}
281
282impl ObjectGroupBy {
283    pub fn new(
284        property_name: impl Into<String>,
285        storage_field: impl Into<String>,
286        query: SelectQuery,
287    ) -> Self {
288        Self {
289            property_name: property_name.into(),
290            storage_field: storage_field.into(),
291            query,
292        }
293    }
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297pub struct AggregationCacheOptions {
298    pub enabled: bool,
299    pub cache_expired_millis: u64,
300    pub propagate: bool,
301    pub propagate_cache_expired_millis: u64,
302}
303
304impl AggregationCacheOptions {
305    pub fn enabled(cache_expired_millis: u64) -> Self {
306        Self {
307            enabled: true,
308            cache_expired_millis,
309            propagate: false,
310            propagate_cache_expired_millis: 0,
311        }
312    }
313
314    pub fn propagate(mut self, cache_expired_millis: u64) -> Self {
315        self.propagate = true;
316        self.propagate_cache_expired_millis = cache_expired_millis;
317        self
318    }
319}
320
321#[derive(Debug, Clone, PartialEq)]
322pub struct StreamConfig {
323    pub chunk_size: usize,
324}
325
326#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct ContinuousPageFetchOptions {
328    pub namespace: String,
329    pub ttl_seconds: u64,
330}
331
332impl ContinuousPageFetchOptions {
333    pub const DEFAULT_TTL_SECONDS: u64 = 600;
334
335    pub fn new(namespace: impl Into<String>, ttl_seconds: u64) -> Self {
336        let namespace = namespace.into();
337        assert!(
338            !namespace.trim().is_empty(),
339            "continuous page namespace must not be empty"
340        );
341        assert!(
342            ttl_seconds > 0,
343            "continuous page ttl_seconds must be positive"
344        );
345        Self {
346            namespace,
347            ttl_seconds,
348        }
349    }
350}
351
352impl Default for StreamConfig {
353    fn default() -> Self {
354        Self { chunk_size: 1000 }
355    }
356}
357
358#[derive(Debug, Clone, PartialEq)]
359pub struct SelectQuery {
360    /// Safety ceiling for a fully materialized outer query.
361    pub hard_limit: u64,
362    pub entity: String,
363    pub projection: Vec<String>,
364    pub expr_projection: Vec<NamedExpr>,
365    pub search_with_text: Option<String>,
366    pub filter: Option<Expr>,
367    pub having: Option<Expr>,
368    pub order_by: Vec<OrderBy>,
369    pub slice: Option<Slice>,
370    /// Apply `slice` independently inside each value of this property.
371    pub partition_by: Option<String>,
372    pub aggregates: Vec<Aggregate>,
373    pub group_by: Vec<String>,
374    pub relations: Vec<RelationLoad>,
375    pub aggregation_cache: Option<AggregationCacheOptions>,
376    pub comment: Option<String>,
377    pub trace_chain: Vec<crate::TraceNode>,
378    pub raw_sql: Option<String>,
379    pub raw_sql_search_criteria: Vec<String>,
380    pub dynamic_properties: Vec<RawSqlProjection>,
381    pub raw_projections: Vec<RawSqlProjection>,
382    pub object_group_bys: Vec<ObjectGroupBy>,
383    pub child_enhancements: Vec<SelectQuery>,
384    pub stream_config: Option<StreamConfig>,
385    /// Explicit, process-local hint for transparent seek pagination of outer list queries.
386    pub continuous_page_fetch: Option<ContinuousPageFetchOptions>,
387}
388
389impl SelectQuery {
390    pub fn new(entity: impl Into<String>) -> Self {
391        Self {
392            hard_limit: 10_000,
393            entity: entity.into(),
394            projection: Vec::new(),
395            expr_projection: Vec::new(),
396            search_with_text: None,
397            filter: None,
398            having: None,
399            order_by: Vec::new(),
400            slice: None,
401            partition_by: None,
402            aggregates: Vec::new(),
403            group_by: Vec::new(),
404            relations: Vec::new(),
405            aggregation_cache: None,
406            comment: None,
407            trace_chain: Vec::new(),
408            raw_sql: None,
409            raw_sql_search_criteria: Vec::new(),
410            dynamic_properties: Vec::new(),
411            raw_projections: Vec::new(),
412            object_group_bys: Vec::new(),
413            child_enhancements: Vec::new(),
414            stream_config: None,
415            continuous_page_fetch: None,
416        }
417    }
418
419    pub fn project(mut self, field: impl Into<String>) -> Self {
420        self.projection.push(field.into());
421        self
422    }
423
424    pub fn projects(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
425        self.projection.extend(fields.into_iter().map(Into::into));
426        self
427    }
428
429    pub fn project_expr(mut self, alias: impl Into<String>, expr: Expr) -> Self {
430        self.expr_projection.push(NamedExpr::new(alias, expr));
431        self
432    }
433
434    pub fn project_raw(
435        mut self,
436        alias: impl Into<String>,
437        raw_sql_segment: impl Into<String>,
438    ) -> Self {
439        self.raw_projections
440            .push(RawSqlProjection::new(alias, raw_sql_segment));
441        self
442    }
443
444    pub fn dynamic_property_raw(
445        mut self,
446        alias: impl Into<String>,
447        raw_sql_segment: impl Into<String>,
448    ) -> Self {
449        self.dynamic_properties
450            .push(RawSqlProjection::new(alias, raw_sql_segment));
451        self
452    }
453
454    pub fn search_with_text(mut self, text: impl Into<String>) -> Self {
455        self.search_with_text = Some(text.into());
456        self
457    }
458
459    pub fn filter(mut self, filter: Expr) -> Self {
460        self.filter = Some(filter);
461        self
462    }
463
464    pub fn and_filter(mut self, filter: Expr) -> Self {
465        self.filter = Some(match self.filter.take() {
466            Some(existing) => existing.and_expr(filter),
467            None => filter,
468        });
469        self
470    }
471
472    pub fn or_filter(mut self, filter: Expr) -> Self {
473        self.filter = Some(match self.filter.take() {
474            Some(existing) => existing.or_expr(filter),
475            None => filter,
476        });
477        self
478    }
479
480    pub fn having(mut self, having: Expr) -> Self {
481        self.having = Some(having);
482        self
483    }
484
485    pub fn and_having(mut self, having: Expr) -> Self {
486        self.having = Some(match self.having.take() {
487            Some(existing) => existing.and_expr(having),
488            None => having,
489        });
490        self
491    }
492
493    pub fn or_having(mut self, having: Expr) -> Self {
494        self.having = Some(match self.having.take() {
495            Some(existing) => existing.or_expr(having),
496            None => having,
497        });
498        self
499    }
500
501    pub fn order_by(mut self, order: OrderBy) -> Self {
502        self.order_by.push(order);
503        self
504    }
505
506    pub fn order_asc(self, field: impl Into<String>) -> Self {
507        self.order_by(OrderBy::asc(field))
508    }
509
510    pub fn order_desc(self, field: impl Into<String>) -> Self {
511        self.order_by(OrderBy::desc(field))
512    }
513
514    pub fn order_expr_asc(self, expr: Expr) -> Self {
515        self.order_by(OrderBy::asc_expr(expr))
516    }
517
518    pub fn order_expr_desc(self, expr: Expr) -> Self {
519        self.order_by(OrderBy::desc_expr(expr))
520    }
521
522    pub fn order_gbk_asc(self, field: impl Into<String>) -> Self {
523        self.order_by(OrderBy::asc_gbk(field))
524    }
525
526    pub fn order_gbk_desc(self, field: impl Into<String>) -> Self {
527        self.order_by(OrderBy::desc_gbk(field))
528    }
529
530    pub fn group_by(mut self, field: impl Into<String>) -> Self {
531        self.group_by.push(field.into());
532        self
533    }
534
535    pub fn aggregate(mut self, aggregate: Aggregate) -> Self {
536        self.aggregates.push(aggregate);
537        self
538    }
539
540    pub fn count(self, alias: impl Into<String>) -> Self {
541        self.aggregate(Aggregate::count(alias))
542    }
543
544    pub fn count_field(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
545        self.aggregate(Aggregate::count_field(field, alias))
546    }
547
548    pub fn sum(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
549        self.aggregate(Aggregate::sum(field, alias))
550    }
551
552    pub fn avg(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
553        self.aggregate(Aggregate::avg(field, alias))
554    }
555
556    pub fn min(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
557        self.aggregate(Aggregate::min(field, alias))
558    }
559
560    pub fn max(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
561        self.aggregate(Aggregate::max(field, alias))
562    }
563
564    pub fn stddev(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
565        self.aggregate(Aggregate::stddev(field, alias))
566    }
567
568    pub fn stddev_pop(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
569        self.aggregate(Aggregate::stddev_pop(field, alias))
570    }
571
572    pub fn var_samp(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
573        self.aggregate(Aggregate::var_samp(field, alias))
574    }
575
576    pub fn var_pop(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
577        self.aggregate(Aggregate::var_pop(field, alias))
578    }
579
580    pub fn bit_and(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
581        self.aggregate(Aggregate::bit_and(field, alias))
582    }
583
584    pub fn bit_or(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
585        self.aggregate(Aggregate::bit_or(field, alias))
586    }
587
588    pub fn bit_xor(self, field: impl Into<String>, alias: impl Into<String>) -> Self {
589        self.aggregate(Aggregate::bit_xor(field, alias))
590    }
591
592    pub fn enable_aggregation_cache(self) -> Self {
593        self.enable_aggregation_cache_for(0)
594    }
595
596    pub fn enable_aggregation_cache_for(mut self, cache_expired_millis: u64) -> Self {
597        self.aggregation_cache = Some(AggregationCacheOptions::enabled(cache_expired_millis));
598        self
599    }
600
601    pub fn propagate_aggregation_cache(mut self, cache_expired_millis: u64) -> Self {
602        self.aggregation_cache = Some(
603            self.aggregation_cache
604                .unwrap_or_else(|| AggregationCacheOptions::enabled(0))
605                .propagate(cache_expired_millis),
606        );
607        self
608    }
609
610    pub fn comment(mut self, comment: impl Into<String>) -> Self {
611        let comment_str = comment.into();
612        self.comment = Some(comment_str.clone());
613        self.trace_chain.push(crate::TraceNode {
614            entity_type: self.entity.clone(),
615            entity_id: None,
616            comment: comment_str,
617        });
618        self
619    }
620
621    pub fn raw_sql(mut self, raw_sql: impl Into<String>) -> Self {
622        self.raw_sql = Some(raw_sql.into());
623        self
624    }
625
626    pub fn raw_sql_search_criteria(mut self, raw_sql: impl Into<String>) -> Self {
627        self.raw_sql_search_criteria.push(raw_sql.into());
628        self
629    }
630
631    pub fn object_group_by(
632        mut self,
633        property_name: impl Into<String>,
634        storage_field: impl Into<String>,
635        query: SelectQuery,
636    ) -> Self {
637        self.object_group_bys
638            .push(ObjectGroupBy::new(property_name, storage_field, query));
639        self
640    }
641
642    pub fn child_enhancement(mut self, query: SelectQuery) -> Self {
643        self.child_enhancements.push(query);
644        self
645    }
646
647    pub fn relation(mut self, name: impl Into<String>) -> Self {
648        self.relations.push(RelationLoad::new(name));
649        self
650    }
651
652    pub fn relation_query(mut self, name: impl Into<String>, query: SelectQuery) -> Self {
653        self.relations.push(RelationLoad::with_query(name, query));
654        self
655    }
656
657    pub fn limit(mut self, limit: u64) -> Self {
658        let slice = self.slice.get_or_insert(Slice {
659            limit: None,
660            offset: 0,
661        });
662        slice.limit = Some(limit);
663        self
664    }
665
666    /// Override the outer materialized-list ceiling. Most callers should keep 10,000.
667    pub fn hard_limit(mut self, hard_limit: u64) -> Self {
668        assert!(hard_limit > 0, "hard_limit must be positive");
669        self.hard_limit = hard_limit;
670        self
671    }
672
673    /// Apply and validate list-materialization limits. This is intentionally not
674    /// used by streaming execution.
675    pub fn prepare_for_list(mut self) -> Result<Self, String> {
676        self.apply_list_limit(self.hard_limit, true)?;
677        Ok(self)
678    }
679
680    fn apply_list_limit(&mut self, ceiling: u64, outer: bool) -> Result<(), String> {
681        let slice = self.slice.get_or_insert(Slice {
682            limit: None,
683            offset: 0,
684        });
685        match slice.limit {
686            Some(limit) if limit > ceiling => {
687                return Err(format!(
688                    "QUERY_HARD_LIMIT_EXCEEDED: requested limit {limit} exceeds hard limit {ceiling}"
689                ));
690            }
691            None => slice.limit = Some(ceiling),
692            _ => {}
693        }
694        for relation in &mut self.relations {
695            if let Some(query) = relation.query.as_mut() {
696                query.apply_list_limit(10_000, false)?;
697            }
698        }
699        for query in &mut self.child_enhancements {
700            query.apply_list_limit(10_000, false)?;
701        }
702        let _ = outer;
703        Ok(())
704    }
705
706    pub fn offset(mut self, offset: u64) -> Self {
707        let slice = self.slice.get_or_insert(Slice {
708            limit: None,
709            offset: 0,
710        });
711        slice.offset = offset;
712        self
713    }
714
715    pub fn page(self, offset: u64, limit: u64) -> Self {
716        self.offset(offset).limit(limit)
717    }
718
719    pub fn optimize_for_continuous_page_fetch(mut self) -> Self {
720        self.continuous_page_fetch = Some(ContinuousPageFetchOptions::new(
721            "default",
722            ContinuousPageFetchOptions::DEFAULT_TTL_SECONDS,
723        ));
724        self
725    }
726
727    pub fn optimize_for_continuous_page_fetch_with(
728        mut self,
729        namespace: impl Into<String>,
730        ttl_seconds: u64,
731    ) -> Self {
732        self.continuous_page_fetch = Some(ContinuousPageFetchOptions::new(namespace, ttl_seconds));
733        self
734    }
735
736    /// Scope pagination to each distinct value of `field`.
737    ///
738    /// Relation loading sets this automatically. Most application queries
739    /// should use a generated relation selector instead of calling this
740    /// method directly.
741    pub fn partition_by(mut self, field: impl Into<String>) -> Self {
742        self.partition_by = Some(field.into());
743        self
744    }
745
746    /// Enable streaming mode with the given chunk size.
747    /// When streaming, rows are fetched and enhanced in batches rather than all at once.
748    pub fn stream(mut self, chunk_size: usize) -> Self {
749        self.stream_config = Some(StreamConfig { chunk_size });
750        self
751    }
752
753    /// Enable streaming mode with default chunk size (1000).
754    pub fn stream_default(mut self) -> Self {
755        self.stream_config = Some(StreamConfig::default());
756        self
757    }
758}
759
760pub type Record = BTreeMap<String, Value>;
761
762/// A database result row whose column names are shared by the whole result set.
763///
764/// Providers use this representation for typed decoding so a 100-row result does
765/// not allocate 100 copies of every projected column name (or 100 B-trees).
766#[derive(Debug, Clone, PartialEq)]
767pub struct CompactRow {
768    columns: Arc<[String]>,
769    values: Vec<Value>,
770}
771
772impl CompactRow {
773    pub fn new(columns: Arc<[String]>, values: Vec<Value>) -> Self {
774        debug_assert_eq!(columns.len(), values.len());
775        Self { columns, values }
776    }
777
778    pub fn get(&self, name: &str) -> Option<&Value> {
779        self.columns
780            .iter()
781            .position(|column| column == name)
782            .and_then(|index| self.values.get(index))
783    }
784
785    pub fn get_mut(&mut self, name: &str) -> Option<&mut Value> {
786        self.columns
787            .iter()
788            .position(|column| column == name)
789            .and_then(|index| self.values.get_mut(index))
790    }
791
792    /// Adds or replaces a projected value. Column layouts remain shared until
793    /// an enhancement actually changes the shape of this row.
794    pub fn insert(&mut self, name: String, value: Value) -> Option<Value> {
795        if let Some(index) = self.columns.iter().position(|column| column == &name) {
796            return Some(std::mem::replace(&mut self.values[index], value));
797        }
798        let mut columns = self.columns.to_vec();
799        columns.push(name);
800        self.columns = columns.into();
801        self.values.push(value);
802        None
803    }
804
805    pub fn remove(&mut self, name: &str) -> Option<Value> {
806        let index = self.columns.iter().position(|column| column == name)?;
807        let mut columns = self.columns.to_vec();
808        columns.remove(index);
809        self.columns = columns.into();
810        Some(self.values.remove(index))
811    }
812
813    pub fn extend(&mut self, other: CompactRow) {
814        for (name, value) in other.columns.iter().cloned().zip(other.values) {
815            self.insert(name, value);
816        }
817    }
818
819    pub fn contains_key(&self, name: &str) -> bool {
820        self.columns.iter().any(|column| column == name)
821    }
822
823    pub fn len(&self) -> usize {
824        self.values.len()
825    }
826
827    pub fn is_empty(&self) -> bool {
828        self.values.is_empty()
829    }
830
831    pub fn iter(&self) -> impl Iterator<Item = (&String, &Value)> {
832        self.columns.iter().zip(self.values.iter())
833    }
834
835    pub fn keys(&self) -> impl Iterator<Item = &String> {
836        self.columns.iter()
837    }
838
839    pub fn values(&self) -> impl Iterator<Item = &Value> {
840        self.values.iter()
841    }
842
843    pub fn into_map(self) -> BTreeMap<String, Value> {
844        self.columns.iter().cloned().zip(self.values).collect()
845    }
846
847    /// Transitional boundary adapter. Core query providers should construct
848    /// compact rows directly instead of routing through this function.
849    pub fn from_map(values_by_name: BTreeMap<String, Value>) -> Self {
850        let (columns, values): (Vec<_>, Vec<_>) = values_by_name.into_iter().unzip();
851        Self::new(columns.into(), values)
852    }
853}
854
855impl From<BTreeMap<String, Value>> for CompactRow {
856    fn from(values: BTreeMap<String, Value>) -> Self {
857        Self::from_map(values)
858    }
859}
860
861/// Internal projection used to implement per-parent pagination for relation
862/// loads. Runtime relation attachment removes it before exposing child rows.
863pub const PARTITION_RANK_PROPERTY: &str = "__teaql_partition_rank";
864
865pub fn record_to_json_value(record: &Record) -> serde_json::Value {
866    serde_json::Value::Object(
867        record
868            .iter()
869            .map(|(key, value)| (key.clone(), value.to_json_value()))
870            .collect(),
871    )
872}
873
874pub fn compact_row_to_json_value(row: &CompactRow) -> serde_json::Value {
875    serde_json::Value::Object(
876        row.iter()
877            .map(|(key, value)| (key.clone(), value.to_json_value()))
878            .collect(),
879    )
880}