1use serde::{Deserialize, Serialize};
10use std::fmt;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct CTE {
15 pub name: String,
17 pub query: String,
19 pub columns: Vec<String>,
21 pub recursive: bool,
23 pub materialized: Option<bool>,
25}
26
27impl CTE {
28 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 pub fn with_columns(mut self, columns: Vec<String>) -> Self {
50 self.columns = columns;
51 self
52 }
53 pub fn recursive(mut self) -> Self {
56 self.recursive = true;
57 self
58 }
59 pub fn materialized(mut self, materialized: bool) -> Self {
62 self.materialized = Some(materialized);
63 self
64 }
65 pub fn to_sql(&self) -> String {
68 let mut parts = vec![self.name.clone()];
69
70 if !self.columns.is_empty() {
72 parts[0] = format!("{} ({})", parts[0], self.columns.join(", "));
73 }
74
75 parts.push("AS".to_string());
76
77 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#[derive(Debug, Clone, Default)]
100pub struct CTECollection {
101 ctes: Vec<CTE>,
102 recursive: bool,
103}
104
105impl CTECollection {
106 pub fn new() -> Self {
117 Self {
118 ctes: Vec::new(),
119 recursive: false,
120 }
121 }
122 pub fn add(&mut self, cte: CTE) {
125 if cte.recursive {
126 self.recursive = true;
127 }
128 self.ctes.push(cte);
129 }
130 pub fn get(&self, name: &str) -> Option<&CTE> {
133 self.ctes.iter().find(|c| c.name == name)
134 }
135 pub fn is_empty(&self) -> bool {
138 self.ctes.is_empty()
139 }
140 pub fn len(&self) -> usize {
143 self.ctes.len()
144 }
145 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
164pub struct CTEBuilder {
166 name: String,
167 query: String,
168 columns: Vec<String>,
169 recursive: bool,
170 materialized: Option<bool>,
171}
172
173impl CTEBuilder {
174 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 pub fn query(mut self, query: impl Into<String>) -> Self {
196 self.query = query.into();
197 self
198 }
199 pub fn columns(mut self, columns: Vec<String>) -> Self {
202 self.columns = columns;
203 self
204 }
205 pub fn column(mut self, column: impl Into<String>) -> Self {
208 self.columns.push(column.into());
209 self
210 }
211 pub fn recursive(mut self) -> Self {
214 self.recursive = true;
215 self
216 }
217 pub fn materialized(mut self, materialized: bool) -> Self {
220 self.materialized = Some(materialized);
221 self
222 }
223 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
236pub struct CTEPatterns;
238
239impl CTEPatterns {
240 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 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 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 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 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 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 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 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 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}