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