Skip to main content

reinhardt_db/orm/
cte.rs

1//! # Common Table Expressions (CTEs)
2//!
3//! SQL Common Table Expressions (WITH clauses) support.
4//!
5//! This module is inspired by SQLAlchemy's CTE implementation
6//! Copyright 2005-2025 SQLAlchemy authors and contributors
7//! Licensed under MIT License. See THIRD-PARTY-NOTICES for details.
8
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12/// Represents a Common Table Expression (WITH clause)
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct CTE {
15	/// The name.
16	pub name: String,
17	/// The query.
18	pub query: String,
19	/// The columns.
20	pub columns: Vec<String>,
21	/// The recursive.
22	pub recursive: bool,
23	/// The materialized.
24	pub materialized: Option<bool>,
25}
26
27impl CTE {
28	/// Create a Common Table Expression (WITH clause)
29	///
30	/// # Examples
31	///
32	/// ```
33	/// use reinhardt_db::orm::cte::CTE;
34	///
35	/// let cte = CTE::new("recent_users", "SELECT * FROM users WHERE created_at > NOW() - INTERVAL '7 days'");
36	/// assert_eq!(cte.name, "recent_users");
37	/// assert!(!cte.recursive); // Not recursive by default
38	/// ```
39	pub fn new(name: impl Into<String>, query: impl Into<String>) -> Self {
40		Self {
41			name: name.into(),
42			query: query.into(),
43			columns: Vec::new(),
44			recursive: false,
45			materialized: None,
46		}
47	}
48	/// Documentation for `with_columns`
49	pub fn with_columns(mut self, columns: Vec<String>) -> Self {
50		self.columns = columns;
51		self
52	}
53	/// Documentation for `recursive`
54	///
55	pub fn recursive(mut self) -> Self {
56		self.recursive = true;
57		self
58	}
59	/// Documentation for `materialized`
60	///
61	pub fn materialized(mut self, materialized: bool) -> Self {
62		self.materialized = Some(materialized);
63		self
64	}
65	/// Generate SQL for this CTE
66	///
67	pub fn to_sql(&self) -> String {
68		let mut parts = vec![self.name.clone()];
69
70		// Add column list if specified
71		if !self.columns.is_empty() {
72			parts[0] = format!("{} ({})", parts[0], self.columns.join(", "));
73		}
74
75		parts.push("AS".to_string());
76
77		// Add materialized hint if specified (PostgreSQL)
78		if let Some(mat) = self.materialized {
79			if mat {
80				parts.push("MATERIALIZED".to_string());
81			} else {
82				parts.push("NOT MATERIALIZED".to_string());
83			}
84		}
85
86		parts.push(format!("({})", self.query));
87
88		parts.join(" ")
89	}
90}
91
92impl fmt::Display for CTE {
93	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94		write!(f, "{}", self.to_sql())
95	}
96}
97
98/// Collection of CTEs for building WITH clauses
99#[derive(Debug, Clone, Default)]
100pub struct CTECollection {
101	ctes: Vec<CTE>,
102	recursive: bool,
103}
104
105impl CTECollection {
106	/// Create a new collection of CTEs for building WITH clauses
107	///
108	/// # Examples
109	///
110	/// ```
111	/// use reinhardt_db::orm::cte::CTECollection;
112	///
113	/// let collection = CTECollection::new();
114	/// assert!(collection.is_empty()); // New collection is empty
115	/// ```
116	pub fn new() -> Self {
117		Self {
118			ctes: Vec::new(),
119			recursive: false,
120		}
121	}
122	/// Documentation for `add`
123	///
124	pub fn add(&mut self, cte: CTE) {
125		if cte.recursive {
126			self.recursive = true;
127		}
128		self.ctes.push(cte);
129	}
130	/// Documentation for `get`
131	///
132	pub fn get(&self, name: &str) -> Option<&CTE> {
133		self.ctes.iter().find(|c| c.name == name)
134	}
135	/// Documentation for `is_empty`
136	///
137	pub fn is_empty(&self) -> bool {
138		self.ctes.is_empty()
139	}
140	/// Documentation for `len`
141	///
142	pub fn len(&self) -> usize {
143		self.ctes.len()
144	}
145	/// Generate complete WITH clause
146	///
147	pub fn to_sql(&self) -> Option<String> {
148		if self.ctes.is_empty() {
149			return None;
150		}
151
152		let with_keyword = if self.recursive {
153			"WITH RECURSIVE"
154		} else {
155			"WITH"
156		};
157
158		let cte_sql: Vec<String> = self.ctes.iter().map(|c| c.to_sql()).collect();
159
160		Some(format!("{} {}", with_keyword, cte_sql.join(", ")))
161	}
162}
163
164/// Builder for creating CTEs
165pub struct CTEBuilder {
166	name: String,
167	query: String,
168	columns: Vec<String>,
169	recursive: bool,
170	materialized: Option<bool>,
171}
172
173impl CTEBuilder {
174	/// Create a new CTE builder with a name
175	///
176	/// # Examples
177	///
178	/// ```
179	/// use reinhardt_db::orm::cte::CTEBuilder;
180	///
181	/// let builder = CTEBuilder::new("user_stats");
182	/// // Use builder to construct a CTE step by step
183	/// ```
184	pub fn new(name: impl Into<String>) -> Self {
185		Self {
186			name: name.into(),
187			query: String::new(),
188			columns: Vec::new(),
189			recursive: false,
190			materialized: None,
191		}
192	}
193	/// Documentation for `query`
194	///
195	pub fn query(mut self, query: impl Into<String>) -> Self {
196		self.query = query.into();
197		self
198	}
199	/// Documentation for `columns`
200	///
201	pub fn columns(mut self, columns: Vec<String>) -> Self {
202		self.columns = columns;
203		self
204	}
205	/// Documentation for `column`
206	///
207	pub fn column(mut self, column: impl Into<String>) -> Self {
208		self.columns.push(column.into());
209		self
210	}
211	/// Documentation for `recursive`
212	///
213	pub fn recursive(mut self) -> Self {
214		self.recursive = true;
215		self
216	}
217	/// Documentation for `materialized`
218	///
219	pub fn materialized(mut self, materialized: bool) -> Self {
220		self.materialized = Some(materialized);
221		self
222	}
223	/// Documentation for `build`
224	///
225	pub fn build(self) -> CTE {
226		CTE {
227			name: self.name,
228			query: self.query,
229			columns: self.columns,
230			recursive: self.recursive,
231			materialized: self.materialized,
232		}
233	}
234}
235
236/// Common CTE patterns
237pub struct CTEPatterns;
238
239impl CTEPatterns {
240	/// Hierarchical data traversal (organization tree, categories, etc.)
241	///
242	pub fn recursive_hierarchy(
243		cte_name: &str,
244		table: &str,
245		id_col: &str,
246		parent_col: &str,
247		root_condition: &str,
248	) -> CTE {
249		let query = format!(
250			r#"
251            SELECT {id}, {parent}, 1 as level, CAST({id} AS TEXT) as path
252            FROM {table}
253            WHERE {root_condition}
254
255            UNION ALL
256
257            SELECT t.{id}, t.{parent}, cte.level + 1, cte.path || '/' || CAST(t.{id} AS TEXT)
258            FROM {table} t
259            INNER JOIN {cte} cte ON t.{parent} = cte.{id}
260            "#,
261			id = id_col,
262			parent = parent_col,
263			table = table,
264			root_condition = root_condition,
265			cte = cte_name
266		);
267
268		CTE::new(cte_name, query.trim()).recursive()
269	}
270	/// Aggregation with intermediate results
271	///
272	pub fn aggregation_cte(cte_name: &str, table: &str, group_by: &str, agg_expr: &str) -> CTE {
273		let query = format!(
274			"SELECT {}, {} FROM {} GROUP BY {}",
275			group_by, agg_expr, table, group_by
276		);
277
278		CTE::new(cte_name, query)
279	}
280	/// Date series generation
281	///
282	pub fn date_series(cte_name: &str, start_date: &str, end_date: &str) -> CTE {
283		let query = format!(
284			r#"
285            SELECT DATE('{}') as date
286            UNION ALL
287            SELECT DATE(date, '+1 day')
288            FROM {}
289            WHERE date < DATE('{}')
290            "#,
291			start_date, cte_name, end_date
292		);
293
294		CTE::new(cte_name, query.trim()).recursive()
295	}
296	/// Number series generation
297	///
298	pub fn number_series(cte_name: &str, start: i64, end: i64) -> CTE {
299		let query = format!(
300			r#"
301            SELECT {} as n
302            UNION ALL
303            SELECT n + 1
304            FROM {}
305            WHERE n < {}
306            "#,
307			start, cte_name, end
308		);
309
310		CTE::new(cte_name, query.trim()).recursive()
311	}
312	/// Moving average calculation
313	///
314	pub fn moving_average(
315		cte_name: &str,
316		table: &str,
317		value_col: &str,
318		date_col: &str,
319		window_size: i32,
320	) -> CTE {
321		let query = format!(
322			r#"
323            SELECT
324                {},
325                {},
326                AVG({}) OVER (
327                    ORDER BY {}
328                    ROWS BETWEEN {} PRECEDING AND CURRENT ROW
329                ) as moving_avg
330            FROM {}
331            ORDER BY {}
332            "#,
333			date_col,
334			value_col,
335			value_col,
336			date_col,
337			window_size - 1,
338			table,
339			date_col
340		);
341
342		CTE::new(cte_name, query.trim())
343	}
344	/// Deduplication
345	///
346	pub fn deduplicate(cte_name: &str, table: &str, partition_by: &str, order_by: &str) -> CTE {
347		let query = format!(
348			r#"
349            SELECT *,
350                ROW_NUMBER() OVER (PARTITION BY {} ORDER BY {}) as rn
351            FROM {}
352            "#,
353			partition_by, order_by, table
354		);
355
356		CTE::new(cte_name, query.trim())
357	}
358	/// Graph traversal (follows relationships)
359	///
360	pub fn graph_traversal(
361		cte_name: &str,
362		table: &str,
363		id_col: &str,
364		relation_col: &str,
365		start_id: i64,
366	) -> CTE {
367		let query = format!(
368			r#"
369            SELECT {id}, {relation}, 1 as depth
370            FROM {table}
371            WHERE {id} = {start_id}
372
373            UNION ALL
374
375            SELECT t.{id}, t.{relation}, cte.depth + 1
376            FROM {table} t
377            INNER JOIN {cte} cte ON t.{id} = cte.{relation}
378            WHERE cte.depth < 100
379            "#,
380			id = id_col,
381			relation = relation_col,
382			table = table,
383			start_id = start_id,
384			cte = cte_name
385		);
386
387		CTE::new(cte_name, query.trim()).recursive()
388	}
389	/// Running total calculation
390	///
391	pub fn running_total(cte_name: &str, table: &str, amount_col: &str, date_col: &str) -> CTE {
392		let query = format!(
393			r#"
394            SELECT
395                {},
396                {},
397                SUM({}) OVER (ORDER BY {}) as running_total
398            FROM {}
399            ORDER BY {}
400            "#,
401			date_col, amount_col, amount_col, date_col, table, date_col
402		);
403
404		CTE::new(cte_name, query.trim())
405	}
406	/// Pivot table simulation
407	///
408	pub fn pivot(
409		cte_name: &str,
410		table: &str,
411		row_col: &str,
412		col_col: &str,
413		value_col: &str,
414	) -> CTE {
415		let query = format!(
416			r#"
417            SELECT
418                {},
419                SUM(CASE WHEN {} = 'A' THEN {} ELSE 0 END) as a_value,
420                SUM(CASE WHEN {} = 'B' THEN {} ELSE 0 END) as b_value,
421                SUM(CASE WHEN {} = 'C' THEN {} ELSE 0 END) as c_value
422            FROM {}
423            GROUP BY {}
424            "#,
425			row_col, col_col, value_col, col_col, value_col, col_col, value_col, table, row_col
426		);
427
428		CTE::new(cte_name, query.trim())
429	}
430}
431
432#[cfg(test)]
433mod tests {
434	use super::*;
435
436	#[test]
437	fn test_cte_creation() {
438		let cte = CTE::new(
439			"regional_sales",
440			"SELECT region, SUM(amount) as total FROM orders GROUP BY region",
441		);
442		assert_eq!(cte.name, "regional_sales");
443		assert!(!cte.recursive);
444	}
445
446	#[test]
447	fn test_cte_with_columns() {
448		let cte = CTE::new("sales", "SELECT * FROM orders")
449			.with_columns(vec!["region".to_string(), "total".to_string()]);
450
451		let sql = cte.to_sql();
452		assert!(sql.contains("sales (region, total)"));
453	}
454
455	#[test]
456	fn test_cte_recursive_unit() {
457		let cte = CTE::new("tree", "SELECT * FROM nodes").recursive();
458		assert!(cte.recursive);
459	}
460
461	#[test]
462	fn test_materialized_cte() {
463		let cte = CTE::new("expensive_query", "SELECT * FROM large_table").materialized(true);
464		let sql = cte.to_sql();
465		assert!(sql.contains("MATERIALIZED"));
466	}
467
468	#[test]
469	fn test_not_materialized_cte() {
470		let cte = CTE::new("simple_query", "SELECT * FROM small_table").materialized(false);
471		let sql = cte.to_sql();
472		assert!(sql.contains("NOT MATERIALIZED"));
473	}
474
475	#[test]
476	fn test_cte_builder() {
477		let cte = CTEBuilder::new("user_stats")
478			.query("SELECT user_id, COUNT(*) as count FROM posts GROUP BY user_id")
479			.column("user_id")
480			.column("post_count")
481			.build();
482
483		assert_eq!(cte.name, "user_stats");
484		assert_eq!(cte.columns.len(), 2);
485	}
486
487	#[test]
488	fn test_cte_collection() {
489		let mut collection = CTECollection::new();
490
491		collection.add(CTE::new("cte1", "SELECT * FROM table1"));
492		collection.add(CTE::new("cte2", "SELECT * FROM table2"));
493
494		assert_eq!(collection.len(), 2);
495		assert!(collection.get("cte1").is_some());
496
497		let sql = collection.to_sql().unwrap();
498		assert!(sql.starts_with("WITH"));
499		assert!(sql.contains("cte1"));
500		assert!(sql.contains("cte2"));
501	}
502
503	#[test]
504	fn test_recursive_collection() {
505		let mut collection = CTECollection::new();
506		collection.add(CTE::new("tree", "SELECT * FROM nodes").recursive());
507
508		let sql = collection.to_sql().unwrap();
509		assert!(sql.starts_with("WITH RECURSIVE"));
510	}
511
512	#[test]
513	fn test_recursive_hierarchy_pattern() {
514		let cte = CTEPatterns::recursive_hierarchy(
515			"org_tree",
516			"employees",
517			"id",
518			"manager_id",
519			"manager_id IS NULL",
520		);
521
522		assert!(cte.recursive);
523		assert!(cte.query.contains("UNION ALL"));
524		assert!(cte.query.contains("level"));
525		assert!(cte.query.contains("path"));
526	}
527
528	#[test]
529	fn test_date_series_pattern() {
530		let cte = CTEPatterns::date_series("dates", "2024-01-01", "2024-01-31");
531
532		assert!(cte.recursive);
533		assert!(cte.query.contains("DATE"));
534		assert!(cte.query.contains("+1 day"));
535	}
536
537	#[test]
538	fn test_number_series_pattern() {
539		let cte = CTEPatterns::number_series("numbers", 1, 100);
540
541		assert!(cte.recursive);
542		assert!(cte.query.contains("n + 1"));
543	}
544
545	#[test]
546	fn test_moving_average_pattern() {
547		let cte = CTEPatterns::moving_average("ma", "sales", "amount", "date", 7);
548
549		assert!(cte.query.contains("AVG"));
550		assert!(cte.query.contains("OVER"));
551		assert!(cte.query.contains("PRECEDING"));
552	}
553
554	#[test]
555	fn test_deduplicate_pattern() {
556		let cte = CTEPatterns::deduplicate("deduped", "users", "email", "created_at DESC");
557
558		assert!(cte.query.contains("ROW_NUMBER()"));
559		assert!(cte.query.contains("PARTITION BY"));
560	}
561
562	#[test]
563	fn test_graph_traversal_pattern() {
564		let cte = CTEPatterns::graph_traversal("graph", "relationships", "id", "related_id", 1);
565
566		assert!(cte.recursive);
567		assert!(cte.query.contains("depth"));
568		assert!(cte.query.contains("UNION ALL"));
569	}
570
571	#[test]
572	fn test_running_total_pattern() {
573		let cte = CTEPatterns::running_total("totals", "transactions", "amount", "date");
574
575		assert!(cte.query.contains("SUM"));
576		assert!(cte.query.contains("OVER"));
577		assert!(cte.query.contains("running_total"));
578	}
579
580	#[test]
581	fn test_aggregation_cte_pattern() {
582		let cte = CTEPatterns::aggregation_cte(
583			"region_totals",
584			"sales",
585			"region",
586			"SUM(amount) as total",
587		);
588
589		assert!(cte.query.contains("GROUP BY"));
590	}
591
592	#[test]
593	fn test_empty_collection_sql() {
594		let collection = CTECollection::new();
595		assert!(collection.to_sql().is_none());
596	}
597}