quick_oxibooks_sql/
lib.rs

1use std::fmt::Display;
2
3// Re-export the procedural macro
4pub use quick_oxibooks_sql_macro::qb_sql;
5use quickbooks_types::QBItem;
6
7/// Struct representing a SQL-like query for `QuickBooks` entities
8#[derive(Debug, PartialEq, Clone)]
9pub struct Query<QB> {
10    condition: Vec<WhereClause>,
11    order: Vec<OrderClause>,
12    limit: Option<Limit>,
13    _phantom: std::marker::PhantomData<QB>,
14}
15
16impl<QB: QBItem> Default for Query<QB> {
17    fn default() -> Self {
18        Self::new()
19    }
20}
21
22impl<QB: QBItem> Query<QB> {
23    /// Create a new empty query
24    #[must_use] 
25    pub fn new() -> Self {
26        Query {
27            condition: Vec::new(),
28            order: Vec::new(),
29            limit: None,
30            _phantom: std::marker::PhantomData,
31        }
32    }
33
34    /// Add a condition to the query
35    ///
36    /// # Safety
37    /// This function is unsafe because it accepts a raw `WhereClause`.
38    /// The caller must ensure that the `WhereClause` is valid and corresponds to the `QuickBooks` entity.
39    #[must_use] 
40    pub unsafe fn condition(mut self, condition: WhereClause) -> Self {
41        self.condition.push(condition);
42        self
43    }
44
45    /// Add an order clause to the query
46    ///
47    /// # Safety
48    /// This function is unsafe because it accepts a raw string slice as the field name.
49    /// The caller must ensure that the field name is valid and corresponds to a field in the `QuickBooks` entity.
50    #[must_use] 
51    pub unsafe fn order(mut self, field: &'static str, order: Order) -> Self {
52        self.order.push(OrderClause { field, order });
53        self
54    }
55
56    /// Set a limit on the number of results returned by the query
57    #[must_use] 
58    pub fn limit(mut self, number: u32, offset: Option<u32>) -> Self {
59        self.limit = Some(Limit { number, offset });
60        self
61    }
62
63    /// Generate the query string
64    #[must_use] 
65    pub fn query_string(&self) -> String {
66        let mut query = format!("select * from {}", QB::name());
67
68        if !self.condition.is_empty() {
69            query.push_str(" where");
70            for (i, cond) in self.condition.iter().enumerate() {
71                if i > 0 {
72                    query.push_str(" and");
73                }
74                cond.extend_query(&mut query);
75            }
76        }
77
78        if !self.order.is_empty() {
79            query.push_str(" order by");
80            for (i, ord) in self.order.iter().enumerate() {
81                if i > 0 {
82                    query.push(',');
83                }
84                ord.extend_query(&mut query);
85            }
86        }
87
88        if let Some(limit) = &self.limit {
89            limit.extend_query(&mut query);
90        }
91
92        query
93    }
94
95    #[cfg(feature = "api")]
96    /// Execute the query against the `QuickBooks` API, returning a vector of results or an error
97    pub fn execute(
98        &self,
99        qb: &quick_oxibooks::QBContext,
100        client: &ureq::Agent,
101    ) -> Result<Vec<QB>, quick_oxibooks::error::APIError> {
102        // Safety: The query has been constructed using the provided methods,
103        // ensuring that it is valid for the QuickBooks entity QB.
104        unsafe { quick_oxibooks::functions::query::qb_query_raw::<QB>(self, qb, client) }
105    }
106}
107
108impl<QB: QBItem> std::fmt::Display for Query<QB> {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        write!(f, "{}", self.query_string())
111    }
112}
113
114#[derive(Debug, PartialEq, Clone, Copy)]
115struct Limit {
116    number: u32,
117    offset: Option<u32>,
118}
119
120impl Limit {
121    fn extend_query(&self, query: &mut String) {
122        query.push_str(&format!(" LIMIT {}", self.number));
123        if let Some(offset) = self.offset {
124            query.push_str(&format!(" OFFSET {offset}"));
125        }
126    }
127}
128
129/// Struct representing an order clause in a query
130#[derive(Debug, PartialEq, Clone)]
131struct OrderClause {
132    field: &'static str,
133    order: Order,
134}
135
136impl OrderClause {
137    fn extend_query(&self, query: &mut String) {
138        query.push_str(&format!(
139            " {} {}",
140            self.field,
141            match self.order {
142                Order::Asc => "ASC",
143                Order::Desc => "DESC",
144            }
145        ));
146    }
147}
148
149/// Enum representing the order direction in a query
150#[derive(Debug, PartialEq, Clone)]
151pub enum Order {
152    Asc,
153    Desc,
154}
155
156/// Struct representing a where clause in a query
157#[derive(Debug, PartialEq, Clone)]
158pub struct WhereClause {
159    pub field: &'static str,
160    pub operator: Operator,
161    pub values: Vec<String>,
162}
163
164impl WhereClause {
165    /// Create a new where clause
166    #[must_use] 
167    pub fn new(field: &'static str, operator: Operator) -> Self {
168        Self {
169            field,
170            operator,
171            values: Vec::new(),
172        }
173    }
174
175    /// Add a value to the where clause
176    pub fn add_value<T: Display>(mut self, value: T) -> Self {
177        self.values.push(value.to_string());
178        self
179    }
180
181    /// Add multiple values to the where clause from an iterator
182    pub fn add_values<I, T>(mut self, values: I) -> Self
183    where
184        I: Iterator<Item = T>,
185        T: Display,
186    {
187        self.values.extend(values.map(|v| v.to_string()));
188        self
189    }
190}
191
192impl WhereClause {
193    fn extend_query(&self, query: &mut String) {
194        let op_str = match self.operator {
195            Operator::In => "IN",
196            Operator::Like => "LIKE",
197            Operator::Equal => "=",
198            Operator::Less => "<",
199            Operator::Greater => ">",
200            Operator::LessEqual => "<=",
201            Operator::GreaterEqual => ">=",
202        };
203
204        if self.operator == Operator::In {
205            query.push_str(&format!(" {} IN (", self.field));
206            for (i, value) in self.values.iter().enumerate() {
207                if i > 0 {
208                    query.push_str(", ");
209                }
210                query.push_str(&format!("'{value}'"));
211            }
212            query.push(')');
213        } else {
214            query.push_str(&format!(" {} {} '{}'", self.field, op_str, self.values[0]));
215        }
216    }
217}
218
219/// Enum representing the operators used in where clauses
220#[derive(Debug, PartialEq, Clone)]
221pub enum Operator {
222    In,
223    Like,
224    Equal,
225    Less,
226    Greater,
227    LessEqual,
228    GreaterEqual,
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use quickbooks_types::Customer;
235
236    #[test]
237    fn test_empty_query() {
238        let query = qb_sql!(select * from Customer);
239        assert_eq!(query.condition.len(), 0);
240        assert_eq!(query.order.len(), 0);
241        assert!(query.limit.is_none());
242    }
243
244    #[test]
245    fn test_basic_query() {
246        let query = qb_sql!(
247            select * from Customer
248            where display_name like "John%"
249        );
250
251        assert_eq!(query.condition.len(), 1);
252        assert_eq!(query.condition[0].field, "DisplayName");
253    }
254
255    #[test]
256    fn test_multiple_conditions() {
257        let balance_min = 1000.0;
258        let query = qb_sql!(
259            select * from Customer
260            where display_name like "John%"
261            and balance >= balance_min
262        );
263
264        assert_eq!(query.condition.len(), 2);
265    }
266
267    #[test]
268    fn test_order_by() {
269        let query = qb_sql!(
270            select * from Customer
271            where display_name like "John%"
272            order by display_name asc, balance desc
273        );
274
275        assert_eq!(query.order.len(), 2);
276        assert_eq!(query.order[0].field, "DisplayName");
277        assert_eq!(query.order[0].order, Order::Asc);
278    }
279
280    #[test]
281    fn test_limit_and_offset() {
282        let offset_val = 5;
283        let query = qb_sql!(
284            select * from Customer
285            where display_name like "John%"
286            limit 10 offset offset_val
287        );
288
289        assert!(query.limit.is_some());
290        let limit = query.limit.unwrap();
291        assert_eq!(limit.number, 10);
292        assert_eq!(limit.offset, Some(5));
293    }
294
295    #[test]
296    fn test_query_string_generation() {
297        let query = qb_sql!(
298            select * from Customer
299            where display_name like "John%"
300            and id in (1, 2, 3)
301            and balance >= 1000.0
302            order by display_name asc, balance desc
303            limit 10 offset 5
304        );
305
306        let query_string = query.query_string();
307        let expected = "select * from Customer where DisplayName LIKE 'John%' and Id IN ('1', '2', '3') and Balance >= '1000' order by DisplayName ASC, Balance DESC LIMIT 10 OFFSET 5";
308        assert_eq!(query_string, expected);
309    }
310
311    #[test]
312    fn test_in_operator() {
313        let query = qb_sql!(
314            select * from Customer
315            where id in (1, 2, 3, 4, 5)
316        );
317
318        assert_eq!(query.condition.len(), 1);
319        assert_eq!(query.condition[0].field, "Id");
320        assert_eq!(query.condition[0].operator, Operator::In);
321        assert_eq!(query.condition[0].values.len(), 5);
322
323        let query_string = query.query_string();
324        assert_eq!(
325            query_string,
326            "select * from Customer where Id IN ('1', '2', '3', '4', '5')"
327        );
328    }
329
330    #[test]
331    fn test_in_operator_with_strings() {
332        let title1 = "Mr";
333        let title2 = "Mrs";
334        let query = qb_sql!(
335            select * from Customer
336            where title in (title1, title2, "Dr")
337        );
338
339        assert_eq!(query.condition.len(), 1);
340        assert_eq!(query.condition[0].values.len(), 3);
341
342        let query_string = query.query_string();
343        assert_eq!(
344            query_string,
345            "select * from Customer where Title IN ('Mr', 'Mrs', 'Dr')"
346        );
347    }
348
349    #[test]
350    fn test_in_iterator() {
351        let ids = vec![1, 2, 3, 4, 5];
352        let query = qb_sql!(
353            select * from Customer
354            where id in (ids)
355        );
356
357        assert_eq!(query.condition.len(), 1);
358        assert_eq!(query.condition[0].field, "Id");
359        assert_eq!(query.condition[0].operator, Operator::In);
360        assert_eq!(query.condition[0].values.len(), 5);
361
362        let query_string = query.query_string();
363        assert_eq!(
364            query_string,
365            "select * from Customer where Id IN ('1', '2', '3', '4', '5')"
366        );
367    }
368}