Skip to main content

wasm_dbms_api/dbms/query/
builder.rs

1use crate::dbms::query::{Filter, OrderDirection, Query};
2use crate::prelude::{Join, JoinType};
3
4/// A builder for constructing database [`Query`]es.
5#[derive(Debug, Default, Clone)]
6pub struct QueryBuilder {
7    query: Query,
8}
9
10impl QueryBuilder {
11    /// Builds and returns a [`Query`] object based on the current state of the [`QueryBuilder`].
12    pub fn build(self) -> Query {
13        self.query
14    }
15
16    /// Adds a field to select in the query.
17    pub fn field(mut self, field: &str) -> Self {
18        let field = field.to_string();
19        match &mut self.query.columns {
20            crate::dbms::query::Select::All => {
21                self.query.columns = crate::dbms::query::Select::Columns(vec![field]);
22            }
23            crate::dbms::query::Select::Columns(cols) if !cols.contains(&field) => {
24                cols.push(field);
25            }
26            _ => {}
27        }
28        self
29    }
30
31    /// Adds multiple fields to select in the query.
32    pub fn fields<I>(mut self, fields: I) -> Self
33    where
34        I: IntoIterator<Item = &'static str>,
35    {
36        for field in fields {
37            self = self.field(field);
38        }
39        self
40    }
41
42    /// Sets the query to select all fields.
43    pub fn all(mut self) -> Self {
44        self.query.columns = crate::dbms::query::Select::All;
45        self
46    }
47
48    /// Adds a relation to eagerly load with the main records.
49    pub fn with(mut self, table_relation: &str) -> Self {
50        let table_relation = table_relation.to_string();
51        if !self.query.eager_relations.contains(&table_relation) {
52            self.query.eager_relations.push(table_relation);
53        }
54        self
55    }
56
57    /// Adds an INNER JOIN operation to this query
58    pub fn inner_join(self, table: &str, left_col: &str, right_col: &str) -> Self {
59        self.join(JoinType::Inner, table, left_col, right_col)
60    }
61
62    /// Adds a LEFT JOIN operation to this query
63    pub fn left_join(self, table: &str, left_col: &str, right_col: &str) -> Self {
64        self.join(JoinType::Left, table, left_col, right_col)
65    }
66
67    /// Adds a RIGHT JOIN operation to this query
68    pub fn right_join(self, table: &str, left_col: &str, right_col: &str) -> Self {
69        self.join(JoinType::Right, table, left_col, right_col)
70    }
71
72    /// Adds a FULL JOIN operation to this query
73    pub fn full_join(self, table: &str, left_col: &str, right_col: &str) -> Self {
74        self.join(JoinType::Full, table, left_col, right_col)
75    }
76
77    /// Adds a DISTINCT clause to the query for the specified fields.
78    pub fn distinct<S: ToString>(mut self, fields: &[S]) -> Self {
79        self.query.distinct_by = fields.iter().map(|s| s.to_string()).collect();
80        self
81    }
82
83    /// Adds a GROUP BY clause to the query for the specified fields.
84    pub fn group_by<S: ToString>(mut self, fields: &[S]) -> Self {
85        self.query.group_by = fields.iter().map(|s| s.to_string()).collect();
86        self
87    }
88
89    /// Adds a HAVING clause to the query with the specified filter.
90    pub fn having(mut self, filter: Filter) -> Self {
91        self.query.having = Some(filter);
92        self
93    }
94
95    /// Adds an ascending order by clause for the specified field.
96    pub fn order_by_asc<S: ToString>(mut self, field: S) -> Self {
97        self.query
98            .order_by
99            .push((field.to_string(), OrderDirection::Ascending));
100        self
101    }
102
103    /// Adds a descending order by clause for the specified field.
104    pub fn order_by_desc<S: ToString>(mut self, field: S) -> Self {
105        self.query
106            .order_by
107            .push((field.to_string(), OrderDirection::Descending));
108        self
109    }
110
111    /// Sets a limit on the number of records to return.
112    pub fn limit(mut self, limit: usize) -> Self {
113        self.query.limit = Some(limit);
114        self
115    }
116
117    /// Sets an offset for pagination.
118    pub fn offset(mut self, offset: usize) -> Self {
119        self.query.offset = Some(offset);
120        self
121    }
122
123    /// Sets a filter for the query, replacing any existing filter.
124    pub fn filter(mut self, filter: Option<Filter>) -> Self {
125        self.query.filter = filter;
126        self
127    }
128
129    /// Adds a filter to the query, combining with existing filters using AND.
130    pub fn and_where(mut self, filter: Filter) -> Self {
131        self.query.filter = match self.query.filter {
132            Some(existing_filter) => Some(existing_filter.and(filter)),
133            None => Some(filter),
134        };
135        self
136    }
137
138    /// Adds a filter to the query, combining with existing filters using OR.
139    pub fn or_where(mut self, filter: Filter) -> Self {
140        self.query.filter = match self.query.filter {
141            Some(existing_filter) => Some(existing_filter.or(filter)),
142            None => Some(filter),
143        };
144        self
145    }
146
147    /// Add a [`Join`] to the current query from the given parameters.
148    fn join(mut self, join_type: JoinType, table: &str, left_col: &str, right_col: &str) -> Self {
149        self.query.joins.push(Join {
150            join_type,
151            table: table.to_string(),
152            left_column: left_col.to_string(),
153            right_column: right_col.to_string(),
154        });
155        self
156    }
157}
158
159#[cfg(test)]
160mod tests {
161
162    use super::*;
163    use crate::dbms::value::Value;
164    use crate::tests::User;
165
166    #[test]
167    fn test_default_query_builder() {
168        let query_builder = QueryBuilder::default();
169        let query = query_builder.build();
170        assert!(matches!(query.columns, crate::dbms::query::Select::All));
171        assert!(query.eager_relations.is_empty());
172        assert!(query.filter.is_none());
173        assert!(query.order_by.is_empty());
174        assert!(query.limit.is_none());
175        assert!(query.offset.is_none());
176    }
177
178    #[test]
179    fn test_should_add_field_to_query_builder() {
180        let query_builder = QueryBuilder::default().field("id").field("name");
181
182        let query = query_builder.build();
183        assert_eq!(query.columns::<User>(), vec!["id", "name"]);
184    }
185
186    #[test]
187    fn test_should_set_fields() {
188        let query_builder = QueryBuilder::default().fields(["id", "email"]);
189
190        let query = query_builder.build();
191        assert_eq!(query.columns::<User>(), vec!["id", "email"]);
192    }
193
194    #[test]
195    fn test_should_set_all_fields() {
196        let query_builder = QueryBuilder::default().field("id").all();
197
198        let query = query_builder.build();
199        assert!(matches!(query.columns, crate::dbms::query::Select::All));
200    }
201
202    #[test]
203    fn test_should_add_eager_relation() {
204        let query_builder = QueryBuilder::default().with("posts");
205        let query = query_builder.build();
206        assert_eq!(query.eager_relations, vec!["posts"]);
207    }
208
209    #[test]
210    fn test_should_not_duplicate_eager_relation() {
211        let query_builder = QueryBuilder::default().with("posts").with("posts");
212        let query = query_builder.build();
213        assert_eq!(query.eager_relations, vec!["posts"]);
214    }
215
216    #[test]
217    fn test_should_add_order_by_clauses() {
218        let query_builder = QueryBuilder::default()
219            .order_by_asc("name")
220            .order_by_desc("created_at");
221        let query = query_builder.build();
222        assert_eq!(
223            query.order_by,
224            vec![
225                ("name".to_string(), OrderDirection::Ascending),
226                ("created_at".to_string(), OrderDirection::Descending)
227            ]
228        );
229    }
230
231    #[test]
232    fn test_should_distinct_by_fields() {
233        let query_builder = QueryBuilder::default().distinct(&["name", "email"]);
234        let query = query_builder.build();
235        assert_eq!(
236            query.distinct_by,
237            vec!["name".to_string(), "email".to_string()]
238        );
239    }
240
241    #[test]
242    fn test_should_set_group_by_fields() {
243        let query_builder = QueryBuilder::default().group_by(&["category", "status"]);
244        let query = query_builder.build();
245        assert_eq!(
246            query.group_by,
247            vec!["category".to_string(), "status".to_string()]
248        );
249    }
250
251    #[test]
252    fn test_should_set_having_filter() {
253        let filter = Filter::gt("count", Value::Uint32(10u32.into()));
254        let query_builder = QueryBuilder::default().having(filter.clone());
255        let query = query_builder.build();
256        assert_eq!(query.having, Some(filter));
257    }
258
259    #[test]
260    fn test_should_set_limit_and_offset() {
261        let query_builder = QueryBuilder::default().limit(10).offset(5);
262        let query = query_builder.build();
263        assert_eq!(query.limit, Some(10));
264        assert_eq!(query.offset, Some(5));
265    }
266
267    #[test]
268    fn test_should_create_filters() {
269        let query = QueryBuilder::default()
270            .all()
271            .and_where(Filter::eq("id", Value::Uint32(1u32.into())))
272            .or_where(Filter::like("name", "John%"))
273            .build();
274
275        let filter = query.filter.expect("should have filter");
276        if let Filter::Or(left, right) = filter {
277            assert!(matches!(*left, Filter::Eq(id, Value::Uint32(_)) if id == "id"));
278            assert!(matches!(*right, Filter::Like(name, _) if name == "name"));
279        } else {
280            panic!("Expected OR filter at the top level");
281        }
282    }
283
284    #[test]
285    fn test_should_add_inner_join() {
286        let query = QueryBuilder::default()
287            .all()
288            .inner_join("posts", "id", "user")
289            .build();
290        assert_eq!(query.joins.len(), 1);
291        assert_eq!(
292            query.joins[0].join_type,
293            crate::dbms::query::JoinType::Inner
294        );
295        assert_eq!(query.joins[0].table, "posts");
296        assert_eq!(query.joins[0].left_column, "id");
297        assert_eq!(query.joins[0].right_column, "user");
298    }
299
300    #[test]
301    fn test_should_add_left_join() {
302        let query = QueryBuilder::default()
303            .all()
304            .left_join("posts", "id", "user")
305            .build();
306        assert_eq!(query.joins[0].join_type, crate::dbms::query::JoinType::Left);
307    }
308
309    #[test]
310    fn test_should_add_right_join() {
311        let query = QueryBuilder::default()
312            .all()
313            .right_join("posts", "id", "user")
314            .build();
315        assert_eq!(
316            query.joins[0].join_type,
317            crate::dbms::query::JoinType::Right
318        );
319    }
320
321    #[test]
322    fn test_should_add_full_join() {
323        let query = QueryBuilder::default()
324            .all()
325            .full_join("posts", "id", "user")
326            .build();
327        assert_eq!(query.joins[0].join_type, crate::dbms::query::JoinType::Full);
328    }
329
330    #[test]
331    fn test_should_chain_multiple_joins() {
332        let query = QueryBuilder::default()
333            .all()
334            .inner_join("posts", "id", "user")
335            .left_join("comments", "posts.id", "post_id")
336            .build();
337        assert_eq!(query.joins.len(), 2);
338    }
339}