Skip to main content

reinhardt_db/orm/
set_operations.rs

1/// Set operations (UNION, INTERSECT, EXCEPT) similar to Django's QuerySet combinators
2use serde::{Deserialize, Serialize};
3
4/// Set operation type
5#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
6pub enum SetOperation {
7	/// Union variant.
8	Union,
9	/// UnionAll variant.
10	UnionAll,
11	/// Intersect variant.
12	Intersect,
13	/// IntersectAll variant.
14	IntersectAll,
15	/// Except variant.
16	Except,
17	/// ExceptAll variant.
18	ExceptAll,
19}
20
21impl SetOperation {
22	/// Documentation for `to_sql`
23	///
24	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/// Combined query using set operations
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct CombinedQuery {
39	/// The queries.
40	pub queries: Vec<String>,
41	/// The operations.
42	pub operations: Vec<SetOperation>,
43	/// The order by.
44	pub order_by: Vec<String>,
45	/// The limit.
46	pub limit: Option<usize>,
47	/// The offset.
48	pub offset: Option<usize>,
49}
50
51impl CombinedQuery {
52	/// Create a new combined query for set operations (UNION, INTERSECT, EXCEPT)
53	///
54	/// # Examples
55	///
56	/// ```
57	/// use reinhardt_db::orm::set_operations::CombinedQuery;
58	///
59	/// let query = CombinedQuery::new("SELECT * FROM users WHERE active = true");
60	/// assert_eq!(query.queries.len(), 1);
61	/// assert!(query.operations.is_empty());
62	/// ```
63	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	/// Documentation for `union`
73	///
74	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	/// Documentation for `union_all`
80	///
81	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	/// Documentation for `intersect`
87	///
88	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	/// Documentation for `intersect_all`
94	///
95	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	/// Documentation for `except`
101	///
102	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	/// Documentation for `except_all`
108	///
109	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	/// Documentation for `order_by`
115	///
116	pub fn order_by(mut self, field: impl Into<String>) -> Self {
117		self.order_by.push(field.into());
118		self
119	}
120	/// Documentation for `limit`
121	///
122	pub fn limit(mut self, limit: usize) -> Self {
123		self.limit = Some(limit);
124		self
125	}
126	/// Documentation for `offset`
127	///
128	pub fn offset(mut self, offset: usize) -> Self {
129		self.offset = Some(offset);
130		self
131	}
132	/// Documentation for `to_sql`
133	///
134	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		// Add first query in parentheses
146		sql.push('(');
147		sql.push_str(&self.queries[0]);
148		sql.push(')');
149
150		// Add remaining queries with operations
151		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		// Add ORDER BY
161		if !self.order_by.is_empty() {
162			sql.push_str(&format!("\nORDER BY {}", self.order_by.join(", ")));
163		}
164
165		// Add LIMIT
166		if let Some(limit) = self.limit {
167			sql.push_str(&format!("\nLIMIT {}", limit));
168		}
169
170		// Add OFFSET
171		if let Some(offset) = self.offset {
172			sql.push_str(&format!("\nOFFSET {}", offset));
173		}
174
175		sql
176	}
177}
178
179/// Builder for set operations on QuerySets
180pub struct SetOperationBuilder {
181	base_query: String,
182}
183
184impl SetOperationBuilder {
185	/// Create a new builder for set operations starting with a base query
186	///
187	/// # Examples
188	///
189	/// ```
190	/// use reinhardt_db::orm::set_operations::SetOperationBuilder;
191	///
192	/// let builder = SetOperationBuilder::new("SELECT * FROM users");
193	/// // Can chain: .union().intersect().except()
194	/// ```
195	pub fn new(base_query: impl Into<String>) -> Self {
196		Self {
197			base_query: base_query.into(),
198		}
199	}
200	/// Documentation for `union`
201	///
202	pub fn union(self, other_query: impl Into<String>) -> CombinedQuery {
203		CombinedQuery::new(self.base_query).union(other_query)
204	}
205	/// Documentation for `union_all`
206	///
207	pub fn union_all(self, other_query: impl Into<String>) -> CombinedQuery {
208		CombinedQuery::new(self.base_query).union_all(other_query)
209	}
210	/// Documentation for `intersect`
211	///
212	pub fn intersect(self, other_query: impl Into<String>) -> CombinedQuery {
213		CombinedQuery::new(self.base_query).intersect(other_query)
214	}
215	/// Documentation for `except`
216	///
217	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		// Each query should be in parentheses
335		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}