1use std::fmt::Display;
2use std::fmt::Write;
3
4pub use quick_oxibooks_sql_macro::qb_sql;
6use quickbooks_types::QBItem;
7
8#[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 #[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 #[must_use]
41 pub unsafe fn condition(mut self, condition: WhereClause) -> Self {
42 self.condition.push(condition);
43 self
44 }
45
46 #[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 #[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 #[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 pub fn execute(
102 &self,
103 qb: &quick_oxibooks::QBContext,
104 client: &ureq::Agent,
105 ) -> Result<Vec<QB>, quick_oxibooks::error::APIError> {
106 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#[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#[derive(Debug, PartialEq, Clone)]
157pub enum Order {
158 Asc,
159 Desc,
160}
161
162#[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 #[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 #[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 #[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#[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}