1use std::fmt::Write as _;
10
11use uqa_core::{TemporalValue, Value};
12
13use crate::ast::{
14 CteBody, CteMaterialization, Expr, FrameBound, FrameMode, FromClause, JoinKind, LockWait,
15 NullsOrder, OrderBy, Projection, ReturningAliases, SelectStmt, SetOpKind, Statement,
16 TableFunction, WindowReferenceKind, WindowSpec, CTE,
17};
18use crate::SQLError;
19
20mod commands;
21use commands::{delete_sql, insert_sql, merge_sql, update_sql};
22
23pub fn statement_sql(statement: &Statement) -> Result<String, SQLError> {
25 match statement {
26 Statement::Select(select) => Ok(select_sql(select)),
27 Statement::Insert(insert) => Ok(insert_sql(insert)),
28 Statement::Update(update) => Ok(update_sql(update)),
29 Statement::Delete(delete) => Ok(delete_sql(delete)),
30 Statement::Merge(merge) => Ok(merge_sql(merge)),
31 Statement::Notify { channel, payload } => {
32 let payload = if payload.is_empty() {
33 String::new()
34 } else {
35 format!(", {}", string_literal(payload))
36 };
37 Ok(format!("NOTIFY {}{payload}", ident(channel)))
38 }
39 _ => Err(SQLError::Internal(
40 "durable rewrite-rule action has an unsupported statement kind".into(),
41 )),
42 }
43}
44
45pub fn expression_sql(expression: &Expr) -> Result<String, SQLError> {
47 render_expr(expression)
48}
49
50fn select_sql(statement: &SelectStmt) -> String {
51 let mut rendered = with_sql(&statement.with);
52 if let Some(set) = statement.set_op.as_deref() {
53 let left = set
54 .left
55 .as_deref()
56 .map_or_else(|| select_body_sql(statement), select_sql);
57 rendered.push('(');
58 rendered.push_str(&left);
59 rendered.push_str(") ");
60 rendered.push_str(match set.kind {
61 SetOpKind::Union => "UNION",
62 SetOpKind::Intersect => "INTERSECT",
63 SetOpKind::Except => "EXCEPT",
64 });
65 if set.all {
66 rendered.push_str(" ALL");
67 }
68 rendered.push_str(" (");
69 rendered.push_str(&select_sql(&set.right));
70 rendered.push(')');
71 render_order_limit_offset(
72 &mut rendered,
73 &set.combined_order_by,
74 set.combined_limit.as_ref(),
75 set.combined_with_ties,
76 set.combined_offset.as_ref(),
77 );
78 return rendered;
79 }
80 rendered.push_str(&select_body_sql(statement));
81 rendered
82}
83
84fn select_body_sql(statement: &SelectStmt) -> String {
85 let mut rendered = String::new();
86 if statement.values.is_empty() {
87 rendered.push_str("SELECT");
88 if !statement.distinct_on.is_empty() {
89 rendered.push_str(" DISTINCT ON (");
90 rendered.push_str(&expr_list(&statement.distinct_on));
91 rendered.push(')');
92 } else if statement.distinct {
93 rendered.push_str(" DISTINCT");
94 }
95 rendered.push(' ');
96 rendered.push_str(&projections_sql(&statement.projections));
97 if let Some(source) = &statement.from {
98 rendered.push_str(" FROM ");
99 rendered.push_str(&from_sql(source));
100 }
101 if let Some(predicate) = &statement.r#where {
102 rendered.push_str(" WHERE ");
103 rendered.push_str(&expr_sql(predicate));
104 }
105 if !statement.grouping_sets.is_empty() {
106 rendered.push_str(" GROUP BY ");
107 if statement.group_distinct {
108 rendered.push_str("DISTINCT ");
109 }
110 rendered.push_str("GROUPING SETS (");
111 rendered.push_str(
112 &statement
113 .grouping_sets
114 .iter()
115 .map(|set| format!("({})", expr_list(set)))
116 .collect::<Vec<_>>()
117 .join(", "),
118 );
119 rendered.push(')');
120 } else if !statement.group_by.is_empty() {
121 rendered.push_str(" GROUP BY ");
122 if statement.group_distinct {
123 rendered.push_str("DISTINCT ");
124 }
125 rendered.push_str(&expr_list(&statement.group_by));
126 }
127 if let Some(predicate) = &statement.having {
128 rendered.push_str(" HAVING ");
129 rendered.push_str(&expr_sql(predicate));
130 }
131 } else {
132 rendered.push_str("VALUES ");
133 rendered.push_str(&rows_sql(&statement.values));
134 }
135 render_order_limit_offset(
136 &mut rendered,
137 &statement.order_by,
138 statement.limit.as_ref(),
139 statement.with_ties,
140 statement.offset.as_ref(),
141 );
142 for locking in &statement.locking {
143 rendered.push(' ');
144 rendered.push_str(locking.strength.sql_name());
145 if !locking.relations.is_empty() {
146 rendered.push_str(" OF ");
147 rendered.push_str(&ident_list(&locking.relations));
148 }
149 rendered.push_str(match locking.wait {
150 LockWait::Block => "",
151 LockWait::SkipLocked => " SKIP LOCKED",
152 LockWait::NoWait => " NOWAIT",
153 });
154 }
155 rendered
156}
157
158#[expect(
159 clippy::too_many_lines,
160 reason = "exhaustive FROM rendering keeps each AST variant visibly complete"
161)]
162fn from_sql(source: &FromClause) -> String {
163 match source {
164 FromClause::Table {
165 name,
166 alias,
167 column_aliases,
168 include_descendants,
169 ..
170 } => {
171 let mut rendered = only_relation(name, *include_descendants);
172 render_relation_alias(&mut rendered, alias.as_deref(), column_aliases);
173 rendered
174 }
175 FromClause::Join {
176 left,
177 right,
178 kind,
179 on,
180 using,
181 natural,
182 alias,
183 column_aliases,
184 lateral,
185 } => {
186 let mut rendered = String::from("(");
187 rendered.push_str(&from_sql(left));
188 rendered.push(' ');
189 if *natural {
190 rendered.push_str("NATURAL ");
191 }
192 rendered.push_str(match kind {
193 JoinKind::Inner => "JOIN",
194 JoinKind::Left => "LEFT JOIN",
195 JoinKind::Right => "RIGHT JOIN",
196 JoinKind::Full => "FULL JOIN",
197 JoinKind::Cross => "CROSS JOIN",
198 });
199 rendered.push(' ');
200 if *lateral {
201 rendered.push_str("LATERAL ");
202 }
203 rendered.push_str(&from_sql(right));
204 if let Some(predicate) = on {
205 rendered.push_str(" ON ");
206 rendered.push_str(&expr_sql(predicate));
207 } else if let Some(using) = using {
208 rendered.push_str(" USING (");
209 rendered.push_str(&ident_list(&using.columns));
210 rendered.push(')');
211 if let Some(alias) = &using.alias {
212 rendered.push_str(" AS ");
213 rendered.push_str(&ident(alias));
214 }
215 }
216 rendered.push(')');
217 render_relation_alias(&mut rendered, alias.as_deref(), column_aliases);
218 rendered
219 }
220 FromClause::Values {
221 rows,
222 alias,
223 column_aliases,
224 ..
225 } => {
226 let mut rendered = format!("(VALUES {})", rows_sql(rows));
227 render_relation_alias(&mut rendered, alias.as_deref(), column_aliases);
228 rendered
229 }
230 FromClause::Function {
231 name,
232 output_name: _,
233 relations,
234 args,
235 alias,
236 column_aliases,
237 ordinality,
238 column_types,
239 ..
240 } => {
241 let mut arguments = args.iter().map(expr_sql).collect::<Vec<_>>();
242 if let Some(relations) = relations {
243 arguments.insert(0, relations.left.clone());
244 arguments.insert(2, relations.right.clone());
245 }
246 let mut rendered = format!("{name}({})", arguments.join(", "));
247 if *ordinality {
248 rendered.push_str(" WITH ORDINALITY");
249 }
250 render_function_alias(
251 &mut rendered,
252 alias.as_deref(),
253 column_aliases,
254 column_types,
255 );
256 rendered
257 }
258 FromClause::FunctionGroup {
259 functions,
260 alias,
261 column_aliases,
262 ordinality,
263 } => {
264 let mut rendered = format!(
265 "ROWS FROM ({})",
266 functions
267 .iter()
268 .map(table_function_sql)
269 .collect::<Vec<_>>()
270 .join(", ")
271 );
272 if *ordinality {
273 rendered.push_str(" WITH ORDINALITY");
274 }
275 render_relation_alias(&mut rendered, alias.as_deref(), column_aliases);
276 rendered
277 }
278 FromClause::Subquery {
279 body,
280 alias,
281 column_aliases,
282 } => {
283 let mut rendered = format!("({})", select_sql(body));
284 render_relation_alias(&mut rendered, alias.as_deref(), column_aliases);
285 rendered
286 }
287 }
288}
289
290fn table_function_sql(function: &TableFunction) -> String {
291 let mut arguments = function.args.iter().map(expr_sql).collect::<Vec<_>>();
292 if let Some(relations) = &function.relations {
293 arguments.insert(0, relations.left.clone());
294 arguments.insert(2, relations.right.clone());
295 }
296 let mut rendered = format!("{}({})", function.name, arguments.join(", "));
297 if !function.column_types.is_empty() {
298 rendered.push_str(" AS (");
299 rendered.push_str(
300 &function
301 .column_aliases
302 .iter()
303 .zip(&function.column_types)
304 .map(|(name, ty)| format!("{} {ty}", ident(name)))
305 .collect::<Vec<_>>()
306 .join(", "),
307 );
308 rendered.push(')');
309 }
310 rendered
311}
312
313#[expect(
314 clippy::too_many_lines,
315 reason = "exhaustive scalar rendering keeps every durable AST variant explicit"
316)]
317fn render_expr(expression: &Expr) -> Result<String, SQLError> {
318 Ok(match expression {
319 Expr::Star => "*".into(),
320 Expr::QualifiedStar(qualifier) => format!("{}.*", ident(qualifier)),
321 Expr::Default => "DEFAULT".into(),
322 Expr::Column(name) => ident(name),
323 Expr::QualifiedColumn { qualifier, column } => {
324 format!("{}.{}", ident(qualifier), ident(column))
325 }
326 Expr::InternalColumn(column) => {
327 return Err(SQLError::Internal(format!(
328 "executor-only column {column:?} reached durable SQL rendering"
329 )))
330 }
331 Expr::Literal(value) => value_sql(value),
332 Expr::TypedLiteral { value, ty } => format!("({})::{ty}", value_sql(value)),
333 Expr::Param(index) => format!("${index}"),
334 Expr::Func {
335 binding: Some(binding),
336 args,
337 ..
338 } if matches!(
339 binding.dispatch,
340 Some(crate::ast::FunctionDispatch::JsonExtract { .. })
341 ) =>
342 {
343 let Some(crate::ast::FunctionDispatch::JsonExtract { as_text, path }) =
344 binding.dispatch
345 else {
346 unreachable!()
347 };
348 let [lhs, rhs] = args.as_slice() else {
349 return Err(SQLError::Internal(
350 "JSON extraction requires two operands".into(),
351 ));
352 };
353 let operator = match (path, as_text) {
354 (false, false) => "->",
355 (false, true) => "->>",
356 (true, false) => "#>",
357 (true, true) => "#>>",
358 };
359 format!("({} {operator} {})", render_expr(lhs)?, render_expr(rhs)?)
360 }
361 Expr::Func {
362 name,
363 args,
364 distinct,
365 order_by,
366 filter,
367 ..
368 } => {
369 let mut arguments = args.iter().map(expr_sql).collect::<Vec<_>>().join(", ");
370 if *distinct {
371 arguments = format!("DISTINCT {arguments}");
372 }
373 if !order_by.is_empty() {
374 if !arguments.is_empty() {
375 arguments.push(' ');
376 }
377 arguments.push_str("ORDER BY ");
378 arguments.push_str(&order_by_sql(order_by));
379 }
380 let mut rendered = format!("{name}({arguments})");
381 if let Some(filter) = filter {
382 write!(&mut rendered, " FILTER (WHERE {})", expr_sql(filter))
383 .expect("writing to a String cannot fail");
384 }
385 rendered
386 }
387 Expr::Array(items) => format!("ARRAY[{}]", expr_list(items)),
388 Expr::Row(items) => format!("ROW({})", expr_list(items)),
389 Expr::Binary { op, lhs, rhs } => format!(
390 "({} {} {})",
391 expr_sql(lhs),
392 binary_operator_sql(*op),
393 expr_sql(rhs)
394 ),
395 Expr::UnaryMinus(inner) => format!("(-{})", expr_sql(inner)),
396 Expr::Not(inner) => format!("(NOT {})", expr_sql(inner)),
397 Expr::And(items) => format!(
398 "({})",
399 items.iter().map(expr_sql).collect::<Vec<_>>().join(" AND ")
400 ),
401 Expr::Or(items) => format!(
402 "({})",
403 items.iter().map(expr_sql).collect::<Vec<_>>().join(" OR ")
404 ),
405 Expr::IsNull { expr, negated } => format!(
406 "({} IS {}NULL)",
407 expr_sql(expr),
408 if *negated { "NOT " } else { "" }
409 ),
410 Expr::Between { expr, low, high } => format!(
411 "({} BETWEEN {} AND {})",
412 expr_sql(expr),
413 expr_sql(low),
414 expr_sql(high)
415 ),
416 Expr::InList {
417 expr,
418 list,
419 negated,
420 } => format!(
421 "({} {}IN ({}))",
422 expr_sql(expr),
423 if *negated { "NOT " } else { "" },
424 expr_list(list)
425 ),
426 Expr::WindowCall { name, args, spec } => {
427 format!("{name}({}) OVER {}", expr_list(args), window_sql(spec))
428 }
429 Expr::Case {
430 base,
431 when,
432 else_branch,
433 } => {
434 let mut rendered = String::from("CASE");
435 if let Some(base) = base {
436 rendered.push(' ');
437 rendered.push_str(&expr_sql(base));
438 }
439 for (condition, result) in when {
440 write!(
441 &mut rendered,
442 " WHEN {} THEN {}",
443 expr_sql(condition),
444 expr_sql(result)
445 )
446 .expect("writing to a String cannot fail");
447 }
448 if let Some(branch) = else_branch {
449 rendered.push_str(" ELSE ");
450 rendered.push_str(&expr_sql(branch));
451 }
452 rendered.push_str(" END");
453 rendered
454 }
455 Expr::Cast { expr, ty } => format!("CAST({} AS {ty})", expr_sql(expr)),
456 Expr::ScalarSubquery(body) => format!("({})", select_sql(body)),
457 Expr::Exists { body, negated } => format!(
458 "{}EXISTS ({})",
459 if *negated { "NOT " } else { "" },
460 select_sql(body)
461 ),
462 Expr::InSubquery {
463 expr,
464 body,
465 negated,
466 } => format!(
467 "({} {}IN ({}))",
468 expr_sql(expr),
469 if *negated { "NOT " } else { "" },
470 select_sql(body)
471 ),
472 })
473}
474
475fn window_sql(spec: &WindowSpec) -> String {
476 if let Some(reference) = &spec.reference {
477 if reference.kind == WindowReferenceKind::Direct
478 && spec.partition_by.is_empty()
479 && spec.order_by.is_empty()
480 && spec.frame.is_none()
481 {
482 return ident(&reference.name);
483 }
484 }
485 let mut parts = Vec::new();
486 if let Some(reference) = &spec.reference {
487 parts.push(ident(&reference.name));
488 }
489 if !spec.partition_by.is_empty() {
490 parts.push(format!("PARTITION BY {}", expr_list(&spec.partition_by)));
491 }
492 if !spec.order_by.is_empty() {
493 parts.push(format!("ORDER BY {}", order_by_sql(&spec.order_by)));
494 }
495 if let Some(frame) = &spec.frame {
496 parts.push(format!(
497 "{} BETWEEN {} AND {}",
498 match frame.mode {
499 FrameMode::Rows => "ROWS",
500 FrameMode::Range => "RANGE",
501 FrameMode::Groups => "GROUPS",
502 },
503 frame_bound_sql(&frame.start),
504 frame_bound_sql(&frame.end)
505 ));
506 }
507 format!("({})", parts.join(" "))
508}
509
510const fn binary_operator_sql(operator: crate::ast::BinaryOp) -> &'static str {
511 match operator {
512 crate::ast::BinaryOp::Equal => "=",
513 crate::ast::BinaryOp::NotEqual => "<>",
514 crate::ast::BinaryOp::Less => "<",
515 crate::ast::BinaryOp::LessEqual => "<=",
516 crate::ast::BinaryOp::Greater => ">",
517 crate::ast::BinaryOp::GreaterEqual => ">=",
518 crate::ast::BinaryOp::Add => "+",
519 crate::ast::BinaryOp::Subtract => "-",
520 crate::ast::BinaryOp::Multiply => "*",
521 crate::ast::BinaryOp::Divide => "/",
522 }
523}
524
525fn frame_bound_sql(bound: &FrameBound) -> String {
526 match bound {
527 FrameBound::UnboundedPreceding => "UNBOUNDED PRECEDING".into(),
528 FrameBound::UnboundedFollowing => "UNBOUNDED FOLLOWING".into(),
529 FrameBound::CurrentRow => "CURRENT ROW".into(),
530 FrameBound::Preceding(expression) => format!("{} PRECEDING", expr_sql(expression)),
531 FrameBound::Following(expression) => format!("{} FOLLOWING", expr_sql(expression)),
532 }
533}
534
535fn with_sql(ctes: &[CTE]) -> String {
536 if ctes.is_empty() {
537 return String::new();
538 }
539 let recursive = ctes.iter().any(|cte| cte.recursive);
540 format!(
541 "WITH {}{} ",
542 if recursive { "RECURSIVE " } else { "" },
543 ctes.iter().map(cte_sql).collect::<Vec<_>>().join(", ")
544 )
545}
546
547fn cte_sql(cte: &CTE) -> String {
548 let mut rendered = ident(&cte.name);
549 if !cte.columns.is_empty() {
550 rendered.push_str(" (");
551 rendered.push_str(&ident_list(&cte.columns));
552 rendered.push(')');
553 }
554 rendered.push_str(" AS ");
555 rendered.push_str(match cte.materialization {
556 CteMaterialization::Default => "",
557 CteMaterialization::Materialized => "MATERIALIZED ",
558 CteMaterialization::NotMaterialized => "NOT MATERIALIZED ",
559 });
560 rendered.push('(');
561 rendered.push_str(&match &cte.body {
562 CteBody::Query(query) => select_sql(query),
563 CteBody::Insert(command) => insert_sql(command),
564 CteBody::Update(command) => update_sql(command),
565 CteBody::Delete(command) => delete_sql(command),
566 CteBody::Merge(command) => merge_sql(command),
567 });
568 rendered.push(')');
569 if let Some(search) = &cte.search {
570 write!(
571 &mut rendered,
572 " SEARCH {} FIRST BY {} SET {}",
573 if search.breadth_first {
574 "BREADTH"
575 } else {
576 "DEPTH"
577 },
578 ident_list(&search.columns),
579 ident(&search.sequence_column)
580 )
581 .expect("writing to a String cannot fail");
582 }
583 if let Some(cycle) = &cte.cycle {
584 write!(
585 &mut rendered,
586 " CYCLE {} SET {} TO {} DEFAULT {} USING {}",
587 ident_list(&cycle.columns),
588 ident(&cycle.mark_column),
589 expr_sql(&cycle.mark_value),
590 expr_sql(&cycle.mark_default),
591 ident(&cycle.path_column)
592 )
593 .expect("writing to a String cannot fail");
594 }
595 rendered
596}
597
598fn render_order_limit_offset(
599 rendered: &mut String,
600 order_by: &[OrderBy],
601 limit: Option<&Expr>,
602 with_ties: bool,
603 offset: Option<&Expr>,
604) {
605 if !order_by.is_empty() {
606 rendered.push_str(" ORDER BY ");
607 rendered.push_str(&order_by_sql(order_by));
608 }
609 if with_ties {
610 if let Some(offset) = offset {
611 rendered.push_str(" OFFSET ");
612 rendered.push_str(&expr_sql(offset));
613 }
614 if let Some(limit) = limit {
615 rendered.push_str(" FETCH FIRST ");
616 rendered.push_str(&expr_sql(limit));
617 rendered.push_str(" ROWS WITH TIES");
618 }
619 } else {
620 if let Some(limit) = limit {
621 rendered.push_str(" LIMIT ");
622 rendered.push_str(&expr_sql(limit));
623 }
624 if let Some(offset) = offset {
625 rendered.push_str(" OFFSET ");
626 rendered.push_str(&expr_sql(offset));
627 }
628 }
629}
630
631fn render_returning(rendered: &mut String, aliases: &ReturningAliases, projections: &[Projection]) {
632 if projections.is_empty() {
633 return;
634 }
635 rendered.push_str(" RETURNING ");
636 if aliases.old_explicit || aliases.new_explicit {
637 rendered.push_str("WITH (");
638 let mut names = Vec::new();
639 if aliases.old_explicit {
640 names.push(format!("OLD AS {}", ident(&aliases.old)));
641 }
642 if aliases.new_explicit {
643 names.push(format!("NEW AS {}", ident(&aliases.new)));
644 }
645 rendered.push_str(&names.join(", "));
646 rendered.push_str(") ");
647 }
648 rendered.push_str(&projections_sql(projections));
649}
650
651fn render_target_alias(rendered: &mut String, relation: &str, qualifier: &str) {
652 if relation_local_name(relation) != qualifier {
653 rendered.push_str(" AS ");
654 rendered.push_str(&ident(qualifier));
655 }
656}
657
658fn render_relation_alias(rendered: &mut String, alias: Option<&str>, columns: &[String]) {
659 if let Some(alias) = alias {
660 rendered.push_str(" AS ");
661 rendered.push_str(&ident(alias));
662 if !columns.is_empty() {
663 rendered.push('(');
664 rendered.push_str(&ident_list(columns));
665 rendered.push(')');
666 }
667 }
668}
669
670fn render_function_alias(
671 rendered: &mut String,
672 alias: Option<&str>,
673 columns: &[String],
674 types: &[String],
675) {
676 if let Some(alias) = alias {
677 rendered.push_str(" AS ");
678 rendered.push_str(&ident(alias));
679 } else if !types.is_empty() {
680 rendered.push_str(" AS");
681 }
682 if !types.is_empty() {
683 rendered.push_str(" (");
684 rendered.push_str(
685 &columns
686 .iter()
687 .zip(types)
688 .map(|(name, ty)| format!("{} {ty}", ident(name)))
689 .collect::<Vec<_>>()
690 .join(", "),
691 );
692 rendered.push(')');
693 } else if !columns.is_empty() {
694 rendered.push('(');
695 rendered.push_str(&ident_list(columns));
696 rendered.push(')');
697 }
698}
699
700fn assignments_sql(assignments: &[(String, Expr)]) -> String {
701 assignments
702 .iter()
703 .map(|(column, expression)| format!("{} = {}", ident(column), expr_sql(expression)))
704 .collect::<Vec<_>>()
705 .join(", ")
706}
707
708fn projections_sql(projections: &[Projection]) -> String {
709 projections
710 .iter()
711 .map(|projection| {
712 let mut rendered = expr_sql(&projection.expr);
713 if let Some(alias) = &projection.alias {
714 rendered.push_str(" AS ");
715 rendered.push_str(&ident(alias));
716 }
717 rendered
718 })
719 .collect::<Vec<_>>()
720 .join(", ")
721}
722
723fn order_by_sql(order_by: &[OrderBy]) -> String {
724 order_by
725 .iter()
726 .map(|order| {
727 let mut rendered = expr_sql(&order.expr);
728 if order.descending {
729 rendered.push_str(" DESC");
730 }
731 match order.nulls {
732 Some(NullsOrder::First) => rendered.push_str(" NULLS FIRST"),
733 Some(NullsOrder::Last) => rendered.push_str(" NULLS LAST"),
734 None => {}
735 }
736 rendered
737 })
738 .collect::<Vec<_>>()
739 .join(", ")
740}
741
742fn rows_sql(rows: &[Vec<Expr>]) -> String {
743 rows.iter()
744 .map(|row| format!("({})", expr_list(row)))
745 .collect::<Vec<_>>()
746 .join(", ")
747}
748
749fn expr_list(expressions: &[Expr]) -> String {
750 expressions
751 .iter()
752 .map(expr_sql)
753 .collect::<Vec<_>>()
754 .join(", ")
755}
756
757fn expr_sql(expression: &Expr) -> String {
758 render_expr(expression).expect("durable SQL AST cannot contain executor-only columns")
759}
760
761fn only_relation(name: &str, include_descendants: bool) -> String {
762 if include_descendants {
763 name.to_string()
764 } else {
765 format!("ONLY {name}")
766 }
767}
768
769fn ident_list(names: &[String]) -> String {
770 names
771 .iter()
772 .map(|name| ident(name))
773 .collect::<Vec<_>>()
774 .join(", ")
775}
776
777fn ident(name: &str) -> String {
778 crate::expr::quote_ident(name)
779}
780
781fn relation_local_name(name: &str) -> &str {
782 let mut quoted = false;
783 let mut last_dot = None;
784 let bytes = name.as_bytes();
785 let mut position = 0;
786 while position < bytes.len() {
787 match bytes[position] {
788 b'"' if quoted && bytes.get(position + 1) == Some(&b'"') => position += 2,
789 b'"' => {
790 quoted = !quoted;
791 position += 1;
792 }
793 b'.' if !quoted => {
794 last_dot = Some(position);
795 position += 1;
796 }
797 _ => position += 1,
798 }
799 }
800 let component = &name[last_dot.map_or(0, |dot| dot + 1)..];
801 component
802 .strip_prefix('"')
803 .and_then(|component| component.strip_suffix('"'))
804 .unwrap_or(component)
805}
806
807fn string_literal(value: &str) -> String {
808 format!("'{}'", value.replace('\'', "''"))
809}
810
811fn value_sql(value: &Value) -> String {
812 match value {
813 Value::Null => "NULL".into(),
814 Value::Void => "''::void".into(),
815 Value::Bool(value) => if *value { "true" } else { "false" }.into(),
816 Value::Int(value) => value.to_string(),
817 Value::Float(value) if value.is_finite() => value.to_string(),
818 Value::Float(value) => format!("{}::double precision", string_literal(&value.to_string())),
819 Value::Str(value) => string_literal(value),
820 Value::FixedChar(value) => format!("{}::character", string_literal(value)),
821 Value::Bytes(value) => {
822 let mut hex = String::new();
823 for byte in value {
824 write!(&mut hex, "{byte:02x}").expect("writing to a String cannot fail");
825 }
826 format!("{}::bytea", string_literal(&format!("\\x{hex}")))
827 }
828 Value::Temporal(value) => {
829 let ty = match value {
830 TemporalValue::Date { .. } => "date",
831 TemporalValue::Time { .. } => "time",
832 TemporalValue::TimeTz { .. } => "time with time zone",
833 TemporalValue::Timestamp { .. } => "timestamp",
834 TemporalValue::TimestampTz { .. } => "timestamp with time zone",
835 TemporalValue::Interval { .. } => "interval",
836 };
837 format!("{}::{ty}", string_literal(&value.to_sql_string()))
838 }
839 Value::Decimal(value) if value.is_nan() || value.is_infinite() => {
840 format!("{}::numeric", string_literal(&value.to_sql_string()))
841 }
842 Value::Decimal(value) => format!("{}::numeric", value.to_sql_string()),
843 Value::Json(value) => format!("{}::json", string_literal(value)),
844 Value::JsonB(value) => format!("{}::jsonb", string_literal(value)),
845 Value::Array(array) => format!(
846 "ARRAY[{}]",
847 array
848 .elements()
849 .iter()
850 .map(value_sql)
851 .collect::<Vec<_>>()
852 .join(", ")
853 ),
854 Value::List(values) => format!(
855 "ARRAY[{}]",
856 values.iter().map(value_sql).collect::<Vec<_>>().join(", ")
857 ),
858 Value::Row(values) => format!(
859 "ROW({})",
860 values.iter().map(value_sql).collect::<Vec<_>>().join(", ")
861 ),
862 Value::Record(fields) => format!(
863 "ROW({})",
864 fields
865 .iter()
866 .map(|(_, value)| value_sql(value))
867 .collect::<Vec<_>>()
868 .join(", ")
869 ),
870 Value::Map(value) => format!(
871 "{}::jsonb",
872 string_literal(
873 &serde_json::to_string(value)
874 .expect("serializing an in-memory Value map cannot fail")
875 )
876 ),
877 }
878}
879
880#[cfg(test)]
881mod tests {
882 use super::statement_sql;
883
884 #[test]
885 fn rendered_rule_action_shapes_round_trip_stably() {
886 for sql in [
887 "SELECT source.key_value, row_number() OVER (ORDER BY source.key_value ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS sequence FROM left_table AS source(key_value, payload) JOIN right_table AS other USING (key_value) WHERE source.payload IS NOT NULL ORDER BY sequence LIMIT 2 OFFSET 1",
888 "WITH source(value) AS MATERIALIZED (SELECT 1) SELECT value FROM source UNION ALL SELECT 2 ORDER BY value",
889 "INSERT INTO target_table AS target(id, value) VALUES (1, 'one') ON CONFLICT (id) DO UPDATE SET value = excluded.value WHERE target.id = 1 RETURNING WITH (OLD AS before, NEW AS after) after.id",
890 "UPDATE target_table AS target SET value = source.value FROM source_table AS source(id, value) WHERE target.id = source.id RETURNING target.id",
891 "DELETE FROM target_table AS target USING source_table AS source(id) WHERE target.id = source.id RETURNING target.id",
892 "NOTIFY rule_channel, 'payload'",
893 ] {
894 let mut statements = crate::compile(sql).unwrap_or_else(|error| panic!("{sql}: {error}"));
895 let rendered = statement_sql(&statements.remove(0))
896 .unwrap_or_else(|error| panic!("render {sql}: {error}"));
897 let mut reparsed = crate::compile(&rendered)
898 .unwrap_or_else(|error| panic!("reparse `{rendered}` from `{sql}`: {error}"));
899 let rerendered = statement_sql(&reparsed.remove(0))
900 .unwrap_or_else(|error| panic!("rerender `{rendered}`: {error}"));
901 assert_eq!(rerendered, rendered, "unstable SQL rendering for `{sql}`");
902 }
903 }
904}