1use std::borrow::Cow;
2
3use crate::error::Error;
4use crate::expr::{Expr, IntoExpr};
5use crate::writer::{Expression, SqlWriter};
6
7use super::{MaybeAbsent, write_quoted_list};
8
9#[derive(Debug, Clone, Default)]
24pub struct Cte {
25 pub name: Cow<'static, str>,
27 pub columns: Vec<Cow<'static, str>>,
29 pub query: Option<Expr>,
31 pub materialized: Option<bool>,
35 pub search: CteSearch,
37 pub cycle: CteCycle,
39}
40
41impl Cte {
42 pub fn new(name: impl Into<Cow<'static, str>>, query: impl IntoExpr) -> Self {
44 Cte {
45 name: name.into(),
46 query: Some(query.into_expr()),
47 ..Cte::default()
48 }
49 }
50
51 pub fn is_empty(&self) -> bool {
53 self.name.is_empty() && self.query.is_none()
54 }
55}
56
57impl Expression for Cte {
58 fn write_sql(&self, w: &mut SqlWriter<'_>) {
59 if self.is_empty() {
60 return;
61 }
62
63 let Some(query) = &self.query else {
64 w.record_error(Error::Incomplete("the query of a CTE"));
67 return;
68 };
69
70 w.push_quoted(&[&self.name]);
71 write_quoted_list(w, &self.columns, " (", ", ", ")");
72 w.push_str(" AS ");
73
74 match self.materialized {
75 None => {}
76 Some(true) => w.push_str("MATERIALIZED "),
77 Some(false) => w.push_str("NOT MATERIALIZED "),
78 }
79
80 w.push_str("(");
81 w.write_expr(query);
82 w.push_str(")");
83
84 w.write_if(!self.search.is_empty(), " ", &self.search, "");
85 w.write_if(!self.cycle.is_empty(), " ", &self.cycle, "");
86 }
87}
88
89#[derive(Debug, Clone, Default)]
94pub struct CteSearch {
95 pub order: SearchOrder,
97 pub columns: Vec<Cow<'static, str>>,
99 pub set: Cow<'static, str>,
101}
102
103impl CteSearch {
104 pub fn new(
106 order: SearchOrder,
107 columns: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
108 set: impl Into<Cow<'static, str>>,
109 ) -> Self {
110 CteSearch {
111 order,
112 columns: columns.into_iter().map(Into::into).collect(),
113 set: set.into(),
114 }
115 }
116
117 pub fn is_empty(&self) -> bool {
119 self.columns.is_empty()
120 }
121}
122
123impl Expression for CteSearch {
124 fn write_sql(&self, w: &mut SqlWriter<'_>) {
125 if self.is_empty() {
126 return;
127 }
128 if self.set.is_empty() {
129 w.record_error(Error::Incomplete("the SET column of a CTE SEARCH clause"));
132 return;
133 }
134
135 w.push_str("SEARCH ");
136 w.push_str(self.order.as_str());
137 w.push_str(" FIRST BY ");
138 write_quoted_list(w, &self.columns, "", ", ", "");
139 w.push_str(" SET ");
140 w.push_quoted(&[&self.set]);
141 }
142}
143
144#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
146pub enum SearchOrder {
147 #[default]
149 Breadth,
150 Depth,
152}
153
154impl SearchOrder {
155 pub fn as_str(self) -> &'static str {
157 match self {
158 SearchOrder::Breadth => "BREADTH",
159 SearchOrder::Depth => "DEPTH",
160 }
161 }
162}
163
164#[derive(Debug, Clone, Default)]
179pub struct CteCycle {
180 pub columns: Vec<Cow<'static, str>>,
182 pub set: Cow<'static, str>,
184 pub using: Cow<'static, str>,
186 pub to: Option<Expr>,
189 pub default_val: Option<Expr>,
191}
192
193impl CteCycle {
194 pub fn new(
197 columns: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
198 set: impl Into<Cow<'static, str>>,
199 using: impl Into<Cow<'static, str>>,
200 ) -> Self {
201 CteCycle {
202 columns: columns.into_iter().map(Into::into).collect(),
203 set: set.into(),
204 using: using.into(),
205 ..CteCycle::default()
206 }
207 }
208
209 pub fn is_empty(&self) -> bool {
211 self.columns.is_empty()
212 }
213}
214
215impl Expression for CteCycle {
216 fn write_sql(&self, w: &mut SqlWriter<'_>) {
217 if self.is_empty() {
218 return;
219 }
220 if self.set.is_empty() || self.using.is_empty() {
221 w.record_error(Error::Incomplete(
222 "the SET and USING columns of a CTE CYCLE clause",
223 ));
224 return;
225 }
226 if self.to.is_some() != self.default_val.is_some() {
229 w.record_error(Error::Incomplete(
230 "both TO and DEFAULT of a CTE CYCLE clause",
231 ));
232 return;
233 }
234
235 w.push_str("CYCLE ");
236 write_quoted_list(w, &self.columns, "", ", ", "");
237 w.push_str(" SET ");
238 w.push_quoted(&[&self.set]);
239
240 if let (Some(to), Some(default_val)) = (&self.to, &self.default_val) {
241 w.push_str(" TO ");
242 w.write_expr(to);
243 w.push_str(" DEFAULT ");
244 w.write_expr(default_val);
245 }
246
247 w.push_str(" USING ");
248 w.push_quoted(&[&self.using]);
249 }
250}
251
252impl MaybeAbsent for Cte {
253 fn is_absent(&self) -> bool {
254 self.is_empty()
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use keelson_sqlcheck::testing::assert_frag_sql;
261
262 use super::*;
263 use crate::dialect::testing::Numbered;
264 use crate::expr::arg;
265 use crate::value::Value;
266 use crate::writer::build;
267
268 const FRAME: &str = r#"WITH {} SELECT * FROM "c""#;
271 const RECURSIVE_FRAME: &str = r#"WITH RECURSIVE {} SELECT * FROM "c""#;
272 const AFTER_RECURSIVE_CTE: &str = concat!(
275 r#"WITH RECURSIVE "c" AS ("#,
276 r#"SELECT 1 AS "id" UNION ALL SELECT "id" + 1 FROM "c" WHERE "id" < 5"#,
277 r#") {} SELECT * FROM "c""#
278 );
279
280 fn sub() -> Expr {
283 Expr::join((
284 Expr::raw(r#"SELECT "id" FROM posts WHERE "id" ="#),
285 arg(1i32),
286 ))
287 }
288
289 const SUB_SQL: &str = r#"SELECT "id" FROM posts WHERE "id" = $1"#;
290
291 fn recursive_sub() -> Expr {
293 Expr::raw(r#"SELECT 1 AS "id" UNION ALL SELECT "id" + 1 FROM "c" WHERE "id" < 5"#)
294 }
295
296 const RECURSIVE_SUB_SQL: &str =
297 r#"SELECT 1 AS "id" UNION ALL SELECT "id" + 1 FROM "c" WHERE "id" < 5"#;
298
299 fn sql(e: &impl Expression) -> String {
300 build(&Numbered, e).expect("render").0
301 }
302
303 #[test]
304 fn an_untouched_cte_writes_nothing() {
305 assert_eq!(build(&Numbered, &Cte::default()).unwrap().0, "");
308 assert!(Cte::default().is_empty());
309 }
310
311 #[test]
312 fn a_bare_cte_is_name_as_query() {
313 let (rendered, args) = build(&Numbered, &Cte::new("c", sub())).unwrap();
314 assert_frag_sql(FRAME, &rendered, &format!(r#""c" AS ({SUB_SQL})"#));
315 assert_eq!(args, vec![Value::I32(1)]);
316 }
317
318 #[test]
319 fn column_names_follow_the_cte_name() {
320 let two_cols = Expr::raw(r#"SELECT "id", "title" FROM posts"#);
323 let cte = Cte {
324 columns: vec!["id".into(), "data".into()],
325 ..Cte::new("c", two_cols)
326 };
327 assert_frag_sql(
328 FRAME,
329 &sql(&cte),
330 r#""c" ("id", "data") AS (SELECT "id", "title" FROM posts)"#,
331 );
332 }
333
334 #[test]
335 fn materialisation_is_three_valued() {
336 let base = Cte::new("c", sub());
340 assert_frag_sql(FRAME, &sql(&base), &format!(r#""c" AS ({SUB_SQL})"#));
341
342 let yes = Cte {
343 materialized: Some(true),
344 ..base.clone()
345 };
346 assert_frag_sql(
347 FRAME,
348 &sql(&yes),
349 &format!(r#""c" AS MATERIALIZED ({SUB_SQL})"#),
350 );
351
352 let no = Cte {
353 materialized: Some(false),
354 ..base
355 };
356 assert_frag_sql(
357 FRAME,
358 &sql(&no),
359 &format!(r#""c" AS NOT MATERIALIZED ({SUB_SQL})"#),
360 );
361 }
362
363 #[test]
364 fn a_named_cte_with_no_query_is_a_recorded_failure() {
365 let cte = Cte {
366 name: "c".into(),
367 ..Cte::default()
368 };
369 let err = build(&Numbered, &cte).unwrap_err();
370 assert!(
373 matches!(&err, Error::Incomplete(what) if what.contains("CTE")),
374 "got: {err}"
375 );
376 }
377
378 #[test]
379 fn search_and_cycle_follow_the_query_and_hinge_on_their_columns() {
380 let mut cte = Cte::new("c", recursive_sub());
381 cte.search = CteSearch::new(SearchOrder::Depth, ["id"], "ordercol");
382 cte.cycle = CteCycle {
385 set: "is_cycle".into(),
386 using: "path".into(),
387 ..CteCycle::default()
388 };
389
390 assert_frag_sql(
391 RECURSIVE_FRAME,
392 &sql(&cte),
393 &format!(r#""c" AS ({RECURSIVE_SUB_SQL}) SEARCH DEPTH FIRST BY "id" SET "ordercol""#),
394 );
395
396 cte.cycle.columns = vec!["id".into()];
397 assert_frag_sql(
398 RECURSIVE_FRAME,
399 &sql(&cte),
400 &format!(
401 concat!(
402 r#""c" AS ({}) SEARCH DEPTH FIRST BY "id" SET "ordercol""#,
403 r#" CYCLE "id" SET "is_cycle" USING "path""#
404 ),
405 RECURSIVE_SUB_SQL
406 ),
407 );
408 }
409
410 #[test]
411 fn breadth_is_the_default_search_order() {
412 let search = CteSearch::new(SearchOrder::default(), ["id"], "seq");
413 assert_frag_sql(
414 AFTER_RECURSIVE_CTE,
415 &sql(&search),
416 r#"SEARCH BREADTH FIRST BY "id" SET "seq""#,
417 );
418 }
419
420 #[test]
421 fn a_search_clause_without_its_set_column_is_a_recorded_failure() {
422 let search = CteSearch {
423 columns: vec!["id".into()],
424 ..CteSearch::default()
425 };
426 let err = build(&Numbered, &search).unwrap_err();
427 assert!(
430 matches!(&err, Error::Incomplete(what)
431 if what.contains("SET") && what.contains("SEARCH")),
432 "got: {err}"
433 );
434 }
435
436 #[test]
437 fn the_cycle_mark_values_are_written_as_one_optional_group() {
438 let mut cycle = CteCycle::new(["id"], "is_cycle", "path");
443 cycle.to = Some(Expr::literal("Y"));
444 cycle.default_val = Some(Expr::literal("N"));
445
446 let (rendered, args) = build(&Numbered, &cycle).unwrap();
447 assert_frag_sql(
448 AFTER_RECURSIVE_CTE,
449 &rendered,
450 r#"CYCLE "id" SET "is_cycle" TO 'Y' DEFAULT 'N' USING "path""#,
451 );
452 assert!(args.is_empty(), "a constant binds nothing");
453
454 cycle.default_val = None;
455 let err = build(&Numbered, &cycle).unwrap_err();
456 assert!(
459 matches!(&err, Error::Incomplete(what)
460 if what.contains("TO") && what.contains("DEFAULT") && what.contains("CYCLE")),
461 "got: {err}"
462 );
463 }
464
465 #[test]
466 fn a_cycle_clause_without_its_added_columns_is_a_recorded_failure() {
467 let cycle = CteCycle {
468 columns: vec!["id".into()],
469 ..CteCycle::default()
470 };
471 let err = build(&Numbered, &cycle).unwrap_err();
472 assert!(
475 matches!(&err, Error::Incomplete(what)
476 if what.contains("SET") && what.contains("USING") && what.contains("CYCLE")),
477 "got: {err}"
478 );
479 }
480}