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 an ascending order by clause for the specified field.
78    pub fn order_by_asc(mut self, field: &str) -> Self {
79        self.query
80            .order_by
81            .push((field.to_string(), OrderDirection::Ascending));
82        self
83    }
84
85    /// Adds a descending order by clause for the specified field.
86    pub fn order_by_desc(mut self, field: &str) -> Self {
87        self.query
88            .order_by
89            .push((field.to_string(), OrderDirection::Descending));
90        self
91    }
92
93    /// Sets a limit on the number of records to return.
94    pub fn limit(mut self, limit: usize) -> Self {
95        self.query.limit = Some(limit);
96        self
97    }
98
99    /// Sets an offset for pagination.
100    pub fn offset(mut self, offset: usize) -> Self {
101        self.query.offset = Some(offset);
102        self
103    }
104
105    /// Sets a filter for the query, replacing any existing filter.
106    pub fn filter(mut self, filter: Option<Filter>) -> Self {
107        self.query.filter = filter;
108        self
109    }
110
111    /// Adds a filter to the query, combining with existing filters using AND.
112    pub fn and_where(mut self, filter: Filter) -> Self {
113        self.query.filter = match self.query.filter {
114            Some(existing_filter) => Some(existing_filter.and(filter)),
115            None => Some(filter),
116        };
117        self
118    }
119
120    /// Adds a filter to the query, combining with existing filters using OR.
121    pub fn or_where(mut self, filter: Filter) -> Self {
122        self.query.filter = match self.query.filter {
123            Some(existing_filter) => Some(existing_filter.or(filter)),
124            None => Some(filter),
125        };
126        self
127    }
128
129    /// Add a [`Join`] to the current query from the given parameters.
130    fn join(mut self, join_type: JoinType, table: &str, left_col: &str, right_col: &str) -> Self {
131        self.query.joins.push(Join {
132            join_type,
133            table: table.to_string(),
134            left_column: left_col.to_string(),
135            right_column: right_col.to_string(),
136        });
137        self
138    }
139}
140
141#[cfg(test)]
142mod tests {
143
144    use super::*;
145    use crate::dbms::value::Value;
146    use crate::tests::User;
147
148    #[test]
149    fn test_default_query_builder() {
150        let query_builder = QueryBuilder::default();
151        let query = query_builder.build();
152        assert!(matches!(query.columns, crate::dbms::query::Select::All));
153        assert!(query.eager_relations.is_empty());
154        assert!(query.filter.is_none());
155        assert!(query.order_by.is_empty());
156        assert!(query.limit.is_none());
157        assert!(query.offset.is_none());
158    }
159
160    #[test]
161    fn test_should_add_field_to_query_builder() {
162        let query_builder = QueryBuilder::default().field("id").field("name");
163
164        let query = query_builder.build();
165        assert_eq!(query.columns::<User>(), vec!["id", "name"]);
166    }
167
168    #[test]
169    fn test_should_set_fields() {
170        let query_builder = QueryBuilder::default().fields(["id", "email"]);
171
172        let query = query_builder.build();
173        assert_eq!(query.columns::<User>(), vec!["id", "email"]);
174    }
175
176    #[test]
177    fn test_should_set_all_fields() {
178        let query_builder = QueryBuilder::default().field("id").all();
179
180        let query = query_builder.build();
181        assert!(matches!(query.columns, crate::dbms::query::Select::All));
182    }
183
184    #[test]
185    fn test_should_add_eager_relation() {
186        let query_builder = QueryBuilder::default().with("posts");
187        let query = query_builder.build();
188        assert_eq!(query.eager_relations, vec!["posts"]);
189    }
190
191    #[test]
192    fn test_should_not_duplicate_eager_relation() {
193        let query_builder = QueryBuilder::default().with("posts").with("posts");
194        let query = query_builder.build();
195        assert_eq!(query.eager_relations, vec!["posts"]);
196    }
197
198    #[test]
199    fn test_should_add_order_by_clauses() {
200        let query_builder = QueryBuilder::default()
201            .order_by_asc("name")
202            .order_by_desc("created_at");
203        let query = query_builder.build();
204        assert_eq!(
205            query.order_by,
206            vec![
207                ("name".to_string(), OrderDirection::Ascending),
208                ("created_at".to_string(), OrderDirection::Descending)
209            ]
210        );
211    }
212
213    #[test]
214    fn test_should_set_limit_and_offset() {
215        let query_builder = QueryBuilder::default().limit(10).offset(5);
216        let query = query_builder.build();
217        assert_eq!(query.limit, Some(10));
218        assert_eq!(query.offset, Some(5));
219    }
220
221    #[test]
222    fn test_should_create_filters() {
223        let query = QueryBuilder::default()
224            .all()
225            .and_where(Filter::eq("id", Value::Uint32(1u32.into())))
226            .or_where(Filter::like("name", "John%"))
227            .build();
228
229        let filter = query.filter.expect("should have filter");
230        if let Filter::Or(left, right) = filter {
231            assert!(matches!(*left, Filter::Eq(id, Value::Uint32(_)) if id == "id"));
232            assert!(matches!(*right, Filter::Like(name, _) if name == "name"));
233        } else {
234            panic!("Expected OR filter at the top level");
235        }
236    }
237
238    #[test]
239    fn test_should_add_inner_join() {
240        let query = QueryBuilder::default()
241            .all()
242            .inner_join("posts", "id", "user")
243            .build();
244        assert_eq!(query.joins.len(), 1);
245        assert_eq!(
246            query.joins[0].join_type,
247            crate::dbms::query::JoinType::Inner
248        );
249        assert_eq!(query.joins[0].table, "posts");
250        assert_eq!(query.joins[0].left_column, "id");
251        assert_eq!(query.joins[0].right_column, "user");
252    }
253
254    #[test]
255    fn test_should_add_left_join() {
256        let query = QueryBuilder::default()
257            .all()
258            .left_join("posts", "id", "user")
259            .build();
260        assert_eq!(query.joins[0].join_type, crate::dbms::query::JoinType::Left);
261    }
262
263    #[test]
264    fn test_should_add_right_join() {
265        let query = QueryBuilder::default()
266            .all()
267            .right_join("posts", "id", "user")
268            .build();
269        assert_eq!(
270            query.joins[0].join_type,
271            crate::dbms::query::JoinType::Right
272        );
273    }
274
275    #[test]
276    fn test_should_add_full_join() {
277        let query = QueryBuilder::default()
278            .all()
279            .full_join("posts", "id", "user")
280            .build();
281        assert_eq!(query.joins[0].join_type, crate::dbms::query::JoinType::Full);
282    }
283
284    #[test]
285    fn test_should_chain_multiple_joins() {
286        let query = QueryBuilder::default()
287            .all()
288            .inner_join("posts", "id", "user")
289            .left_join("comments", "posts.id", "post_id")
290            .build();
291        assert_eq!(query.joins.len(), 2);
292    }
293}