quick_oxibooks_sql/
lib.rs

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