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