Skip to main content

hayson_engine/query/
builder.rs

1use serde_json::Value;
2
3use crate::registry::fulltext_index::SearchMode;
4
5use super::aggregate::AggregateSpec;
6use super::planner::{QuerySpec, SortDirection};
7use super::predicates::{Combine, ComparisonOp, PredicateNode};
8
9/// Collects the predicates of one bracket. Everything added inside it combines by the bracket's
10/// own operator; nesting is unlimited. Mirrors the TS `QueryGroupBuilder`.
11pub struct PredicateGroupBuilder {
12    nodes: Vec<PredicateNode>,
13}
14
15impl PredicateGroupBuilder {
16    fn new() -> Self {
17        PredicateGroupBuilder { nodes: Vec::new() }
18    }
19
20    pub fn r#where(&mut self, field: impl Into<String>, op: ComparisonOp, value: Value) -> &mut Self {
21        self.nodes.push(PredicateNode::leaf(field, op, value));
22        self
23    }
24
25    pub fn where_any(&mut self, build: impl FnOnce(&mut PredicateGroupBuilder)) -> &mut Self {
26        self.nest(Combine::Or, build)
27    }
28
29    pub fn where_all(&mut self, build: impl FnOnce(&mut PredicateGroupBuilder)) -> &mut Self {
30        self.nest(Combine::And, build)
31    }
32
33    fn nest(&mut self, combine: Combine, build: impl FnOnce(&mut PredicateGroupBuilder)) -> &mut Self {
34        let mut bracket = PredicateGroupBuilder::new();
35        build(&mut bracket);
36        if !bracket.nodes.is_empty() {
37            self.nodes.push(PredicateNode::Group {
38                combine,
39                nodes: bracket.nodes,
40            });
41        }
42        self
43    }
44
45    fn into_nodes(self) -> Vec<PredicateNode> {
46        self.nodes
47    }
48}
49
50/// Fluent, programmatic construction of a `QuerySpec` — the typed counterpart of the WHERE-clause
51/// text parsed by `super::parser::parse_query`. `SELECT`/`FROM`/`JOIN`/`MATCH` from ADR-0009 are a
52/// separate, larger piece and still not covered by either front.
53// TODO: SELECT/FROM/JOIN/MATCH and bound params (ADR-0009) — out of scope.
54pub struct QueryBuilder {
55    root: PredicateGroupBuilder,
56    search: Option<(String, SearchMode)>,
57    order_by: Option<(String, SortDirection)>,
58    limit: Option<usize>,
59    offset: Option<usize>,
60    aggregate: AggregateSpec,
61}
62
63impl QueryBuilder {
64    pub fn new() -> Self {
65        QueryBuilder {
66            root: PredicateGroupBuilder::new(),
67            search: None,
68            order_by: None,
69            limit: None,
70            offset: None,
71            aggregate: AggregateSpec::default(),
72        }
73    }
74
75    pub fn r#where(mut self, field: impl Into<String>, op: ComparisonOp, value: Value) -> Self {
76        self.root.r#where(field, op, value);
77        self
78    }
79
80    pub fn where_any(mut self, build: impl FnOnce(&mut PredicateGroupBuilder)) -> Self {
81        self.root.where_any(build);
82        self
83    }
84
85    pub fn where_all(mut self, build: impl FnOnce(&mut PredicateGroupBuilder)) -> Self {
86        self.root.where_all(build);
87        self
88    }
89
90    pub fn search(mut self, text: impl Into<String>, mode: SearchMode) -> Self {
91        self.search = Some((text.into(), mode));
92        self
93    }
94
95    /// Sets the sort key. One key only, and a second call replaces the first — the TS
96    /// `QueryBuilder.orderBy` this mirrors keeps a single key too.
97    pub fn order_by(mut self, field: impl Into<String>, direction: SortDirection) -> Self {
98        self.order_by = Some((field.into(), direction));
99        self
100    }
101
102    pub fn limit(mut self, limit: usize) -> Self {
103        self.limit = Some(limit);
104        self
105    }
106
107    pub fn offset(mut self, offset: usize) -> Self {
108        self.offset = Some(offset);
109        self
110    }
111
112    pub fn count(mut self) -> Self {
113        self.aggregate.count = true;
114        self
115    }
116
117    pub fn sum(mut self, field: impl Into<String>) -> Self {
118        self.aggregate.sum.push(field.into());
119        self
120    }
121
122    pub fn avg(mut self, field: impl Into<String>) -> Self {
123        self.aggregate.avg.push(field.into());
124        self
125    }
126
127    pub fn min(mut self, field: impl Into<String>) -> Self {
128        self.aggregate.min.push(field.into());
129        self
130    }
131
132    pub fn max(mut self, field: impl Into<String>) -> Self {
133        self.aggregate.max.push(field.into());
134        self
135    }
136
137    pub fn group_by(mut self, field: impl Into<String>) -> Self {
138        self.aggregate.group_by = Some(field.into());
139        self
140    }
141
142    pub fn spec(self) -> QuerySpec {
143        let mut spec = QuerySpec::new(PredicateNode::Group {
144            combine: Combine::And,
145            nodes: self.root.into_nodes(),
146        });
147        if let Some((text, mode)) = self.search {
148            spec = spec.with_search(text, mode);
149        }
150        if let Some((field, direction)) = self.order_by {
151            spec = spec.with_order_by(field, direction);
152        }
153        if let Some(limit) = self.limit {
154            spec = spec.with_limit(limit);
155        }
156        if let Some(offset) = self.offset {
157            spec = spec.with_offset(offset);
158        }
159        spec
160    }
161
162    /// Splits the builder into its `QuerySpec` (predicates/search) and the `AggregateSpec`
163    /// accumulated by `count`/`sum`/`avg`/`min`/`max` — the two halves `Table::aggregate` expects.
164    pub fn build_aggregate(mut self) -> (QuerySpec, AggregateSpec) {
165        let aggregate = std::mem::take(&mut self.aggregate);
166        (self.spec(), aggregate)
167    }
168}
169
170impl Default for QueryBuilder {
171    fn default() -> Self {
172        Self::new()
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use serde_json::json;
179
180    use super::*;
181
182    #[test]
183    fn builds_a_flat_conjunction() {
184        let spec = QueryBuilder::new()
185            .r#where("role", ComparisonOp::Eq, json!("admin"))
186            .r#where("age", ComparisonOp::Gt, json!(30))
187            .spec();
188
189        match spec.root {
190            PredicateNode::Group {
191                combine: Combine::And,
192                nodes,
193            } => assert_eq!(nodes.len(), 2),
194            _ => panic!("expected a top-level conjunction"),
195        }
196    }
197
198    #[test]
199    fn nests_an_and_bracket_inside_an_or_bracket() {
200        let spec = QueryBuilder::new()
201            .where_any(|group| {
202                group
203                    .r#where("role", ComparisonOp::Eq, json!("admin"))
204                    .where_all(|inner| {
205                        inner.r#where("role", ComparisonOp::Eq, json!("user")).r#where(
206                            "age",
207                            ComparisonOp::Gt,
208                            json!(35),
209                        );
210                    });
211            })
212            .spec();
213
214        let PredicateNode::Group { nodes, .. } = spec.root else {
215            panic!("expected root group")
216        };
217        assert_eq!(nodes.len(), 1);
218        let PredicateNode::Group {
219            combine: Combine::Or,
220            nodes: or_nodes,
221        } = &nodes[0]
222        else {
223            panic!("expected an or bracket")
224        };
225        assert_eq!(or_nodes.len(), 2);
226        assert!(matches!(
227            or_nodes[1],
228            PredicateNode::Group {
229                combine: Combine::And,
230                ..
231            }
232        ));
233    }
234
235    #[test]
236    fn drops_an_empty_bracket_instead_of_keeping_a_hollow_node() {
237        let spec = QueryBuilder::new().where_any(|_group| {}).spec();
238
239        let PredicateNode::Group { nodes, .. } = spec.root else {
240            panic!("expected root group")
241        };
242        assert!(nodes.is_empty());
243    }
244}