Skip to main content

quick_oxibooks_sql/
lib.rs

1extern crate self as quick_oxibooks_sql;
2
3// Re-export the procedural macro
4#[cfg(feature = "macros")]
5pub use quick_oxibooks_sql_macro::qb_sql;
6
7mod query;
8pub use query::Query;
9mod limit;
10pub(crate) use limit::Limit;
11mod order;
12pub use order::{Order, OrderClause};
13mod condition;
14pub use condition::{Operator, TypedWhereClause, WhereClause};
15
16#[cfg(feature = "macros")]
17pub use pastey::paste;
18
19pub mod traits {
20    // --- 1. Wrapping Traits (_qb_wrap) ---
21    // Normalizes values into Vec<T>
22
23    pub trait QbWrapVec {
24        type Item;
25        fn _qb_wrap(self) -> Vec<Self::Item>;
26    }
27    impl<T> QbWrapVec for Vec<T> {
28        type Item = T;
29        fn _qb_wrap(self) -> Vec<T> {
30            self
31        }
32    }
33
34    pub trait QbWrapOpt {
35        type Item;
36        fn _qb_wrap(self) -> Vec<Self::Item>;
37    }
38    impl<T> QbWrapOpt for Option<T> {
39        type Item = T;
40        fn _qb_wrap(self) -> Vec<T> {
41            self.into_iter().collect()
42        }
43    }
44
45    pub trait QbWrapScalar {
46        type Item;
47        fn _qb_wrap(&self) -> Vec<Self::Item>;
48    }
49    impl<T> QbWrapScalar for &T {
50        type Item = T;
51        fn _qb_wrap(&self) -> Vec<T> {
52            Vec::new()
53        }
54    }
55
56    // --- 2. Access pub Traits (_qb_access) ---
57    // Handles chaining and flattening
58
59    // Priority 1: Nested Vec<Vec<T>> (Matches by Value)
60    // Flattens two levels: Vec<Vec<T>> -> T
61    pub trait QbAccessNested {
62        type Inner;
63        fn _qb_access<R, F>(self, f: F) -> Vec<R>
64        where
65            F: FnMut(Self::Inner) -> Vec<R>;
66    }
67    impl<T> QbAccessNested for Vec<Vec<T>> {
68        type Inner = T;
69        fn _qb_access<R, F>(self, f: F) -> Vec<R>
70        where
71            F: FnMut(T) -> Vec<R>,
72        {
73            self.into_iter().flatten().flat_map(f).collect()
74        }
75    }
76
77    // Priority 2: Generic Vec<T> (Matches by Ref)
78    // Flattens one level: Vec<T> -> T
79    // Passes &T to closure to avoid moving out of Vec if we don't have ownership of elements (although here we do,
80    // but using Ref ensures we don't conflict with Value match on Vec<Vec>)
81    pub trait QbAccessGeneric {
82        type Inner;
83        fn _qb_access<R, F>(&self, f: F) -> Vec<R>
84        where
85            F: FnMut(&Self::Inner) -> Vec<R>;
86    }
87    impl<T> QbAccessGeneric for Vec<T> {
88        type Inner = T;
89        fn _qb_access<R, F>(&self, f: F) -> Vec<R>
90        where
91            F: FnMut(&T) -> Vec<R>,
92        {
93            self.iter().flat_map(f).collect()
94        }
95    }
96}
97pub use traits::*;
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    #[cfg(feature = "macros")]
103    use quickbooks_types::Attachable;
104    use quickbooks_types::Customer;
105
106    #[test]
107    fn test_empty_query() {
108        #[cfg(feature = "macros")]
109        let qry = qb_sql!(select * from Customer);
110        #[cfg(not(feature = "macros"))]
111        let qry: Query<Customer> = Query::new();
112        assert_eq!(qry.condition.len(), 0);
113        assert_eq!(qry.order.len(), 0);
114        assert!(qry.limit.is_none());
115    }
116
117    #[test]
118    fn test_basic_query() {
119        #[cfg(feature = "macros")]
120        let qry = qb_sql!(
121            select * from Customer
122            where display_name like "John%"
123        );
124        #[cfg(not(feature = "macros"))]
125        let qry: Query<Customer> = unsafe {
126            Query::new().condition(WhereClause {
127                field: "DisplayName",
128                operator: Operator::Like,
129                values: vec!["John%".to_string()],
130            })
131        };
132
133        assert_eq!(qry.condition.len(), 1);
134        assert_eq!(qry.condition[0].field, "DisplayName");
135    }
136
137    #[test]
138    fn test_multiple_conditions() {
139        let balance_min = 1000.0;
140        #[cfg(feature = "macros")]
141        let qry = qb_sql!(
142            select * from Customer
143            where display_name like "John%"
144            and balance >= balance_min
145        );
146        #[cfg(not(feature = "macros"))]
147        let qry: Query<Customer> = unsafe {
148            Query::new()
149                .condition(WhereClause {
150                    field: "DisplayName",
151                    operator: Operator::Like,
152                    values: vec!["John%".into()],
153                })
154                .condition(WhereClause {
155                    field: "Balance",
156                    operator: Operator::GreaterEqual,
157                    values: vec![balance_min.to_string()],
158                })
159        };
160
161        assert_eq!(qry.condition.len(), 2);
162    }
163
164    #[test]
165    fn test_order_by() {
166        #[cfg(feature = "macros")]
167        let qry = qb_sql!(
168            select * from Customer
169            where display_name like "John%"
170            order by display_name asc, balance desc
171        );
172        #[cfg(not(feature = "macros"))]
173        let qry: Query<Customer> = unsafe {
174            Query::new()
175                .condition(WhereClause {
176                    field: "DisplayName",
177                    operator: Operator::Like,
178                    values: vec!["John%".into()],
179                })
180                .order("DisplayName", Order::Asc)
181                .order("Balance", Order::Desc)
182        };
183
184        assert_eq!(qry.order.len(), 2);
185        assert_eq!(qry.order[0].field, "DisplayName");
186        assert_eq!(qry.order[0].order, Order::Asc);
187    }
188
189    #[test]
190    fn test_limit_and_offset() {
191        let offset_val = 5;
192        #[cfg(feature = "macros")]
193        let qry = qb_sql!(
194            select * from Customer
195            where display_name like "John%"
196            limit 10 offset offset_val
197        );
198        #[cfg(not(feature = "macros"))]
199        let qry: Query<Customer> = unsafe {
200            Query::new()
201                .condition(WhereClause {
202                    field: "DisplayName",
203                    operator: Operator::Like,
204                    values: vec!["John%".into()],
205                })
206                .limit(10, Some(offset_val))
207        };
208
209        assert!(qry.limit.is_some());
210        let limit = qry.limit.unwrap();
211        assert_eq!(limit.number, 10);
212        assert_eq!(limit.offset, Some(5));
213    }
214
215    #[test]
216    fn test_qry_string_generation() {
217        #[cfg(feature = "macros")]
218        let qry = qb_sql!(
219            select * from Customer
220            where display_name like "John%"
221            and id in (1, 2, 3)
222            and balance >= 1000.0
223            order by display_name asc, balance desc
224            limit 10 offset 5
225        );
226        #[cfg(not(feature = "macros"))]
227        let qry: Query<Customer> = unsafe {
228            Query::new()
229                .condition(WhereClause {
230                    field: "DisplayName",
231                    operator: Operator::Like,
232                    values: vec!["John%".into()],
233                })
234                .condition(WhereClause {
235                    field: "Id",
236                    operator: Operator::In,
237                    values: vec!["1".into(), "2".into(), "3".into()],
238                })
239                .condition(WhereClause {
240                    field: "Balance",
241                    operator: Operator::GreaterEqual,
242                    values: vec!["1000".into()],
243                })
244                .order("DisplayName", Order::Asc)
245                .order("Balance", Order::Desc)
246                .limit(10, Some(5))
247        };
248
249        let qry_string = qry.query_string();
250        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";
251        assert_eq!(qry_string, expected);
252    }
253
254    #[test]
255    fn test_in_operator() {
256        #[cfg(feature = "macros")]
257        let qry = qb_sql!(
258            select * from Customer
259            where id in (1, 2, 3, 4, 5)
260        );
261
262        #[cfg(not(feature = "macros"))]
263        let qry: Query<Customer> = unsafe {
264            Query::new().condition(WhereClause {
265                field: "Id",
266                operator: Operator::In,
267                values: vec!["1".into(), "2".into(), "3".into(), "4".into(), "5".into()],
268            })
269        };
270
271        assert_eq!(qry.condition.len(), 1);
272        assert_eq!(qry.condition[0].field, "Id");
273        assert_eq!(qry.condition[0].operator, Operator::In);
274        assert_eq!(qry.condition[0].values.len(), 5);
275
276        let qry_string = qry.query_string();
277        assert_eq!(
278            qry_string,
279            "select * from Customer where Id IN ('1', '2', '3', '4', '5')"
280        );
281    }
282
283    #[test]
284    fn test_in_operator_with_strings() {
285        let title1 = "Mr";
286        let title2 = "Mrs";
287        #[cfg(feature = "macros")]
288        let qry = qb_sql!(
289            select * from Customer
290            where title in (title1, title2, "Dr")
291        );
292
293        #[cfg(not(feature = "macros"))]
294        let qry: Query<Customer> = unsafe {
295            Query::new().condition(WhereClause {
296                field: "Title",
297                operator: Operator::In,
298                values: vec![title1.into(), title2.into(), "Dr".into()],
299            })
300        };
301
302        assert_eq!(qry.condition.len(), 1);
303        assert_eq!(qry.condition[0].values.len(), 3);
304
305        let qry_string = qry.query_string();
306        assert_eq!(
307            qry_string,
308            "select * from Customer where Title IN ('Mr', 'Mrs', 'Dr')"
309        );
310    }
311
312    #[test]
313    fn test_in_iterator() {
314        let ids = vec![1, 2, 3, 4, 5];
315        #[cfg(feature = "macros")]
316        let qry = qb_sql!(
317            select * from Customer
318            where id in (ids)
319        );
320        #[cfg(not(feature = "macros"))]
321        let qry: Query<Customer> = unsafe {
322            Query::new().condition(WhereClause {
323                field: "Id",
324                operator: Operator::In,
325                values: ids.iter().map(|id| id.to_string()).collect(),
326            })
327        };
328
329        assert_eq!(qry.condition.len(), 1);
330        assert_eq!(qry.condition[0].field, "Id");
331        assert_eq!(qry.condition[0].operator, Operator::In);
332        assert_eq!(qry.condition[0].values.len(), 5);
333
334        let qry_string = qry.query_string();
335        assert_eq!(
336            qry_string,
337            "select * from Customer where Id IN ('1', '2', '3', '4', '5')"
338        );
339    }
340
341    #[test]
342    fn test_nested_fields() {
343        #[cfg(feature = "macros")]
344        let qry = qb_sql!(
345            select * from Customer
346            where primary_email_addr.address like "%@example.com"
347        );
348
349        #[cfg(not(feature = "macros"))]
350        let qry: Query<Customer> = unsafe {
351            Query::new().condition(WhereClause {
352                field: "PrimaryEmailAddr.Address",
353                operator: Operator::Like,
354                values: vec!["%@example.com".into()],
355            })
356        };
357
358        assert_eq!(qry.condition.len(), 1);
359        assert_eq!(qry.condition[0].field, "PrimaryEmailAddr.Address");
360
361        let qry_string = qry.query_string();
362        assert_eq!(
363            qry_string,
364            "select * from Customer where PrimaryEmailAddr.Address LIKE '%@example.com'"
365        );
366    }
367
368    #[test]
369    fn test_vec_fields() {
370        let ids = vec!["1", "2", "3"];
371        #[cfg(feature = "macros")]
372        let qry = qb_sql!(
373            select * from Attachable // comments work
374            where attachable_ref.entity_ref.value in (ids)
375        );
376
377        #[cfg(not(feature = "macros"))]
378        let qry: Query<Attachable> = unsafe {
379            Query::new().condition(WhereClause {
380                field: "AttachableRef.EntityRef.Value",
381                operator: Operator::In,
382                values: ids.iter().map(|f| f.to_string()).collect(),
383            })
384        };
385
386        assert_eq!(qry.condition.len(), 1);
387        assert_eq!(qry.condition[0].field, "AttachableRef.EntityRef.Value");
388        assert_eq!(qry.condition[0].operator, Operator::In);
389        assert_eq!(qry.condition[0].values.len(), 3);
390
391        let qry_string = qry.query_string();
392        assert_eq!(
393            qry_string,
394            "select * from Attachable where AttachableRef.EntityRef.Value IN ('1', '2', '3')"
395        );
396    }
397}