1use std::fmt::Display;
2
3pub use quick_oxibooks_sql_macro::qb_sql;
5use quickbooks_types::QBItem;
6
7#[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 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 pub unsafe fn field(mut self, field: &'static str) -> Self {
35 self.fields.push(field);
36 self
37 }
38
39 pub unsafe fn condition(mut self, condition: WhereClause) -> Self {
45 self.condition.push(condition);
46 self
47 }
48
49 pub unsafe fn order(mut self, field: &'static str, order: Order) -> Self {
55 self.order.push(OrderClause { field, order });
56 self
57 }
58
59 pub fn limit(mut self, number: u32, offset: Option<u32>) -> Self {
61 self.limit = Some(Limit { number, offset });
62 self
63 }
64
65 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 pub fn execute(
114 &self,
115 qb: &quick_oxibooks::QBContext,
116 client: &ureq::Agent,
117 ) -> Result<Vec<QB>, quick_oxibooks::error::APIError> {
118 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#[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#[derive(Debug, PartialEq, Clone)]
167pub enum Order {
168 Asc,
169 Desc,
170}
171
172#[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 pub fn new(field: &'static str, operator: Operator) -> Self {
183 Self {
184 field,
185 operator,
186 values: Vec::new(),
187 }
188 }
189
190 pub fn add_value<T: Display>(mut self, value: T) -> Self {
192 self.values.push(value.to_string());
193 self
194 }
195
196 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#[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}