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