Skip to main content

icydb_core/db/query/
dynamic.rs

1//! Module: db::query::dynamic
2//! Responsibility: entity-name-driven structural read requests and results.
3//! Does not own: accepted schema resolution, planning, or execution.
4//! Boundary: public dynamic inputs are lowered once against accepted authority.
5
6use crate::db::query::{
7    builder::AggregateExpr,
8    expr::{FilterExpr, JunctionOperator, OrderTerm},
9};
10
11///
12/// DynamicQuery
13///
14/// Entity-name-driven structural read request.
15/// The session resolves fields, ordering, indexes, and projection against the
16/// accepted schema; no generated entity descriptor participates.
17///
18
19#[derive(Clone, Debug)]
20pub struct DynamicQuery {
21    entity: String,
22    filter: Option<FilterExpr>,
23    order: Vec<OrderTerm>,
24    fields: Vec<String>,
25    #[cfg(test)]
26    distinct: bool,
27    limit: Option<u32>,
28    group_fields: Vec<String>,
29    aggregates: Vec<AggregateExpr>,
30    grouped_limits: Option<(u32, u32)>,
31    cursor: Option<String>,
32}
33
34impl DynamicQuery {
35    /// Start one dynamic read for an accepted entity name.
36    #[must_use]
37    pub fn new(entity: impl Into<String>) -> Self {
38        Self {
39            entity: entity.into(),
40            filter: None,
41            order: Vec::new(),
42            fields: Vec::new(),
43            #[cfg(test)]
44            distinct: false,
45            limit: None,
46            group_fields: Vec::new(),
47            aggregates: Vec::new(),
48            grouped_limits: None,
49            cursor: None,
50        }
51    }
52
53    /// Add one filter expression, joined with prior filters by `AND`.
54    #[must_use]
55    pub fn filter(mut self, filter: impl Into<FilterExpr>) -> Self {
56        let appended = filter.into();
57        self.filter = Some(match self.filter.take() {
58            Some(FilterExpr::Junction {
59                operator: JunctionOperator::And,
60                mut filters,
61            }) => {
62                filters.push(appended);
63                FilterExpr::and(filters)
64            }
65            Some(existing) => FilterExpr::and(vec![existing, appended]),
66            None => appended,
67        });
68        self
69    }
70
71    /// Append one deterministic ordering term.
72    #[must_use]
73    pub fn order_by(mut self, order: OrderTerm) -> Self {
74        self.order.push(order);
75        self
76    }
77
78    /// Select explicit fields in scalar output order.
79    ///
80    /// Grouped execution rejects an explicit scalar selection because group
81    /// keys and aggregates define its output contract.
82    #[must_use]
83    pub fn select<I, S>(mut self, fields: I) -> Self
84    where
85        I: IntoIterator<Item = S>,
86        S: Into<String>,
87    {
88        self.fields = fields.into_iter().map(Into::into).collect();
89        self
90    }
91
92    /// Limit the number of returned rows.
93    #[must_use]
94    pub const fn limit(mut self, limit: u32) -> Self {
95        self.limit = Some(limit);
96        self
97    }
98
99    /// Enable projection DISTINCT for maintained internal execution callers.
100    ///
101    /// The public dynamic-query grammar deliberately does not expose this
102    /// builder; SQL and internal executor contracts remain the DISTINCT
103    /// frontends until a separately reviewed public API is designed.
104    #[cfg(test)]
105    #[must_use]
106    pub(in crate::db) const fn distinct_for_internal_execution(mut self) -> Self {
107        self.distinct = true;
108        self
109    }
110
111    /// Append one accepted field to the grouped key in declaration order.
112    #[must_use]
113    pub fn group_by(mut self, field: impl Into<String>) -> Self {
114        self.group_fields.push(field.into());
115        self
116    }
117
118    /// Append one grouped aggregate in declaration order.
119    #[must_use]
120    pub fn aggregate(mut self, aggregate: AggregateExpr) -> Self {
121        self.aggregates.push(aggregate);
122        self
123    }
124
125    /// Set explicit hard limits for grouped execution.
126    ///
127    /// Ordinary public reads additionally enforce their built-in admission
128    /// ceilings. Zero values are rejected before execution.
129    #[must_use]
130    pub const fn grouped_limits(mut self, max_groups: u32, max_group_bytes: u32) -> Self {
131        self.grouped_limits = Some((max_groups, max_group_bytes));
132        self
133    }
134
135    /// Continue a grouped page from one opaque cursor returned by IcyDB.
136    #[must_use]
137    pub fn cursor(mut self, cursor: impl Into<String>) -> Self {
138        self.cursor = Some(cursor.into());
139        self
140    }
141
142    pub(in crate::db) const fn entity(&self) -> &str {
143        self.entity.as_str()
144    }
145
146    pub(in crate::db) const fn filter_expr(&self) -> Option<&FilterExpr> {
147        self.filter.as_ref()
148    }
149
150    pub(in crate::db) const fn order_terms(&self) -> &[OrderTerm] {
151        self.order.as_slice()
152    }
153
154    pub(in crate::db) const fn selected_fields(&self) -> &[String] {
155        self.fields.as_slice()
156    }
157
158    pub(in crate::db) const fn row_limit(&self) -> Option<u32> {
159        self.limit
160    }
161
162    #[cfg(test)]
163    pub(in crate::db) const fn projection_is_distinct(&self) -> bool {
164        self.distinct
165    }
166
167    pub(in crate::db) const fn has_grouping(&self) -> bool {
168        !self.group_fields.is_empty() || !self.aggregates.is_empty()
169    }
170
171    pub(in crate::db) const fn group_fields(&self) -> &[String] {
172        self.group_fields.as_slice()
173    }
174
175    pub(in crate::db) const fn aggregates(&self) -> &[AggregateExpr] {
176        self.aggregates.as_slice()
177    }
178
179    pub(in crate::db) const fn grouped_execution_limits(&self) -> Option<(u32, u32)> {
180        self.grouped_limits
181    }
182
183    pub(in crate::db) fn continuation_cursor(&self) -> Option<&str> {
184        self.cursor.as_deref()
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::{DynamicQuery, FilterExpr};
191
192    #[test]
193    fn repeated_filters_accumulate_as_one_conjunction() {
194        let owner = FilterExpr::eq("owner_id", 1_u64);
195        let slot = FilterExpr::eq("slot", 2_u64);
196        let query = DynamicQuery::new("InventoryStack")
197            .filter(owner.clone())
198            .filter(slot.clone());
199
200        assert_eq!(
201            query.filter_expr(),
202            Some(&FilterExpr::and(vec![owner, slot]))
203        );
204    }
205}