hayson_engine/query/
builder.rs1use 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
9pub 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
50pub 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 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 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}