reinhardt_db/orm/
set_operations.rs1use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
6pub enum SetOperation {
7 Union,
9 UnionAll,
11 Intersect,
13 IntersectAll,
15 Except,
17 ExceptAll,
19}
20
21impl SetOperation {
22 pub fn to_sql(&self) -> &'static str {
25 match self {
26 SetOperation::Union => "UNION",
27 SetOperation::UnionAll => "UNION ALL",
28 SetOperation::Intersect => "INTERSECT",
29 SetOperation::IntersectAll => "INTERSECT ALL",
30 SetOperation::Except => "EXCEPT",
31 SetOperation::ExceptAll => "EXCEPT ALL",
32 }
33 }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct CombinedQuery {
39 pub queries: Vec<String>,
41 pub operations: Vec<SetOperation>,
43 pub order_by: Vec<String>,
45 pub limit: Option<usize>,
47 pub offset: Option<usize>,
49}
50
51impl CombinedQuery {
52 pub fn new(first_query: impl Into<String>) -> Self {
64 Self {
65 queries: vec![first_query.into()],
66 operations: Vec::new(),
67 order_by: Vec::new(),
68 limit: None,
69 offset: None,
70 }
71 }
72 pub fn union(mut self, query: impl Into<String>) -> Self {
75 self.queries.push(query.into());
76 self.operations.push(SetOperation::Union);
77 self
78 }
79 pub fn union_all(mut self, query: impl Into<String>) -> Self {
82 self.queries.push(query.into());
83 self.operations.push(SetOperation::UnionAll);
84 self
85 }
86 pub fn intersect(mut self, query: impl Into<String>) -> Self {
89 self.queries.push(query.into());
90 self.operations.push(SetOperation::Intersect);
91 self
92 }
93 pub fn intersect_all(mut self, query: impl Into<String>) -> Self {
96 self.queries.push(query.into());
97 self.operations.push(SetOperation::IntersectAll);
98 self
99 }
100 pub fn except(mut self, query: impl Into<String>) -> Self {
103 self.queries.push(query.into());
104 self.operations.push(SetOperation::Except);
105 self
106 }
107 pub fn except_all(mut self, query: impl Into<String>) -> Self {
110 self.queries.push(query.into());
111 self.operations.push(SetOperation::ExceptAll);
112 self
113 }
114 pub fn order_by(mut self, field: impl Into<String>) -> Self {
117 self.order_by.push(field.into());
118 self
119 }
120 pub fn limit(mut self, limit: usize) -> Self {
123 self.limit = Some(limit);
124 self
125 }
126 pub fn offset(mut self, offset: usize) -> Self {
129 self.offset = Some(offset);
130 self
131 }
132 pub fn to_sql(&self) -> String {
135 if self.queries.is_empty() {
136 return String::new();
137 }
138
139 if self.queries.len() == 1 {
140 return self.queries[0].clone();
141 }
142
143 let mut sql = String::new();
144
145 sql.push('(');
147 sql.push_str(&self.queries[0]);
148 sql.push(')');
149
150 for (i, query) in self.queries.iter().enumerate().skip(1) {
152 if let Some(operation) = self.operations.get(i - 1) {
153 sql.push_str(&format!("\n{}\n", operation.to_sql()));
154 }
155 sql.push('(');
156 sql.push_str(query);
157 sql.push(')');
158 }
159
160 if !self.order_by.is_empty() {
162 sql.push_str(&format!("\nORDER BY {}", self.order_by.join(", ")));
163 }
164
165 if let Some(limit) = self.limit {
167 sql.push_str(&format!("\nLIMIT {}", limit));
168 }
169
170 if let Some(offset) = self.offset {
172 sql.push_str(&format!("\nOFFSET {}", offset));
173 }
174
175 sql
176 }
177}
178
179pub struct SetOperationBuilder {
181 base_query: String,
182}
183
184impl SetOperationBuilder {
185 pub fn new(base_query: impl Into<String>) -> Self {
196 Self {
197 base_query: base_query.into(),
198 }
199 }
200 pub fn union(self, other_query: impl Into<String>) -> CombinedQuery {
203 CombinedQuery::new(self.base_query).union(other_query)
204 }
205 pub fn union_all(self, other_query: impl Into<String>) -> CombinedQuery {
208 CombinedQuery::new(self.base_query).union_all(other_query)
209 }
210 pub fn intersect(self, other_query: impl Into<String>) -> CombinedQuery {
213 CombinedQuery::new(self.base_query).intersect(other_query)
214 }
215 pub fn except(self, other_query: impl Into<String>) -> CombinedQuery {
218 CombinedQuery::new(self.base_query).except(other_query)
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn test_union() {
228 let combined = CombinedQuery::new("SELECT * FROM users WHERE active = true")
229 .union("SELECT * FROM users WHERE admin = true");
230
231 let sql = combined.to_sql();
232 assert!(sql.contains("SELECT * FROM users WHERE active = true"));
233 assert!(sql.contains("UNION"));
234 assert!(sql.contains("SELECT * FROM users WHERE admin = true"));
235 }
236
237 #[test]
238 fn test_union_all() {
239 let combined = CombinedQuery::new("SELECT name FROM employees")
240 .union_all("SELECT name FROM contractors");
241
242 let sql = combined.to_sql();
243 assert!(sql.contains("UNION ALL"));
244 }
245
246 #[test]
247 fn test_intersect() {
248 let combined = CombinedQuery::new("SELECT id FROM customers")
249 .intersect("SELECT customer_id FROM orders");
250
251 let sql = combined.to_sql();
252 assert!(sql.contains("INTERSECT"));
253 }
254
255 #[test]
256 fn test_except() {
257 let combined =
258 CombinedQuery::new("SELECT id FROM all_users").except("SELECT id FROM deleted_users");
259
260 let sql = combined.to_sql();
261 assert!(sql.contains("EXCEPT"));
262 }
263
264 #[test]
265 fn test_multiple_operations() {
266 let combined = CombinedQuery::new("SELECT * FROM table1")
267 .union("SELECT * FROM table2")
268 .union("SELECT * FROM table3");
269
270 let sql = combined.to_sql();
271 assert_eq!(sql.matches("UNION").count(), 2);
272 }
273
274 #[test]
275 fn test_with_order_by() {
276 let combined = CombinedQuery::new("SELECT name FROM users WHERE role = 'admin'")
277 .union("SELECT name FROM users WHERE role = 'moderator'")
278 .order_by("name ASC");
279
280 let sql = combined.to_sql();
281 assert!(sql.contains("ORDER BY name ASC"));
282 }
283
284 #[test]
285 fn test_with_limit() {
286 let combined = CombinedQuery::new("SELECT * FROM table1")
287 .union("SELECT * FROM table2")
288 .limit(10);
289
290 let sql = combined.to_sql();
291 assert!(sql.contains("LIMIT 10"));
292 }
293
294 #[test]
295 fn test_with_offset() {
296 let combined = CombinedQuery::new("SELECT * FROM table1")
297 .union("SELECT * FROM table2")
298 .offset(5);
299
300 let sql = combined.to_sql();
301 assert!(sql.contains("OFFSET 5"));
302 }
303
304 #[test]
305 fn test_with_limit_and_offset() {
306 let combined = CombinedQuery::new("SELECT * FROM table1")
307 .union("SELECT * FROM table2")
308 .order_by("created_at DESC")
309 .limit(20)
310 .offset(10);
311
312 let sql = combined.to_sql();
313 assert!(sql.contains("ORDER BY created_at DESC"));
314 assert!(sql.contains("LIMIT 20"));
315 assert!(sql.contains("OFFSET 10"));
316 }
317
318 #[test]
319 fn test_mixed_operations() {
320 let combined = CombinedQuery::new("SELECT id FROM table1")
321 .union("SELECT id FROM table2")
322 .intersect("SELECT id FROM table3");
323
324 let sql = combined.to_sql();
325 assert!(sql.contains("UNION"));
326 assert!(sql.contains("INTERSECT"));
327 }
328
329 #[test]
330 fn test_parenthesized_queries() {
331 let combined = CombinedQuery::new("SELECT * FROM users").union("SELECT * FROM admins");
332
333 let sql = combined.to_sql();
334 assert!(sql.starts_with("("));
336 assert!(sql.contains(")\nUNION\n("));
337 assert!(sql.ends_with(")"));
338 }
339
340 #[test]
341 fn test_set_operation_builder() {
342 let builder = SetOperationBuilder::new("SELECT * FROM users");
343 let combined = builder.union("SELECT * FROM admins");
344
345 let sql = combined.to_sql();
346 assert!(sql.contains("UNION"));
347 }
348}