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 name,
336 args,
337 distinct,
338 order_by,
339 filter,
340 ..
341 } => {
342 let mut arguments = args.iter().map(expr_sql).collect::<Vec<_>>().join(", ");
343 if *distinct {
344 arguments = format!("DISTINCT {arguments}");
345 }
346 if !order_by.is_empty() {
347 if !arguments.is_empty() {
348 arguments.push(' ');
349 }
350 arguments.push_str("ORDER BY ");
351 arguments.push_str(&order_by_sql(order_by));
352 }
353 let mut rendered = format!("{name}({arguments})");
354 if let Some(filter) = filter {
355 write!(&mut rendered, " FILTER (WHERE {})", expr_sql(filter))
356 .expect("writing to a String cannot fail");
357 }
358 rendered
359 }
360 Expr::Array(items) => format!("ARRAY[{}]", expr_list(items)),
361 Expr::Row(items) => format!("ROW({})", expr_list(items)),
362 Expr::Binary { op, lhs, rhs } => format!(
363 "({} {} {})",
364 expr_sql(lhs),
365 binary_operator_sql(*op),
366 expr_sql(rhs)
367 ),
368 Expr::UnaryMinus(inner) => format!("(-{})", expr_sql(inner)),
369 Expr::Not(inner) => format!("(NOT {})", expr_sql(inner)),
370 Expr::And(items) => format!(
371 "({})",
372 items.iter().map(expr_sql).collect::<Vec<_>>().join(" AND ")
373 ),
374 Expr::Or(items) => format!(
375 "({})",
376 items.iter().map(expr_sql).collect::<Vec<_>>().join(" OR ")
377 ),
378 Expr::IsNull { expr, negated } => format!(
379 "({} IS {}NULL)",
380 expr_sql(expr),
381 if *negated { "NOT " } else { "" }
382 ),
383 Expr::Between { expr, low, high } => format!(
384 "({} BETWEEN {} AND {})",
385 expr_sql(expr),
386 expr_sql(low),
387 expr_sql(high)
388 ),
389 Expr::InList {
390 expr,
391 list,
392 negated,
393 } => format!(
394 "({} {}IN ({}))",
395 expr_sql(expr),
396 if *negated { "NOT " } else { "" },
397 expr_list(list)
398 ),
399 Expr::WindowCall { name, args, spec } => {
400 format!("{name}({}) OVER {}", expr_list(args), window_sql(spec))
401 }
402 Expr::Case {
403 base,
404 when,
405 else_branch,
406 } => {
407 let mut rendered = String::from("CASE");
408 if let Some(base) = base {
409 rendered.push(' ');
410 rendered.push_str(&expr_sql(base));
411 }
412 for (condition, result) in when {
413 write!(
414 &mut rendered,
415 " WHEN {} THEN {}",
416 expr_sql(condition),
417 expr_sql(result)
418 )
419 .expect("writing to a String cannot fail");
420 }
421 if let Some(branch) = else_branch {
422 rendered.push_str(" ELSE ");
423 rendered.push_str(&expr_sql(branch));
424 }
425 rendered.push_str(" END");
426 rendered
427 }
428 Expr::Cast { expr, ty } => format!("CAST({} AS {ty})", expr_sql(expr)),
429 Expr::ScalarSubquery(body) => format!("({})", select_sql(body)),
430 Expr::Exists { body, negated } => format!(
431 "{}EXISTS ({})",
432 if *negated { "NOT " } else { "" },
433 select_sql(body)
434 ),
435 Expr::InSubquery {
436 expr,
437 body,
438 negated,
439 } => format!(
440 "({} {}IN ({}))",
441 expr_sql(expr),
442 if *negated { "NOT " } else { "" },
443 select_sql(body)
444 ),
445 })
446}
447
448fn window_sql(spec: &WindowSpec) -> String {
449 if let Some(reference) = &spec.reference {
450 if reference.kind == WindowReferenceKind::Direct
451 && spec.partition_by.is_empty()
452 && spec.order_by.is_empty()
453 && spec.frame.is_none()
454 {
455 return ident(&reference.name);
456 }
457 }
458 let mut parts = Vec::new();
459 if let Some(reference) = &spec.reference {
460 parts.push(ident(&reference.name));
461 }
462 if !spec.partition_by.is_empty() {
463 parts.push(format!("PARTITION BY {}", expr_list(&spec.partition_by)));
464 }
465 if !spec.order_by.is_empty() {
466 parts.push(format!("ORDER BY {}", order_by_sql(&spec.order_by)));
467 }
468 if let Some(frame) = &spec.frame {
469 parts.push(format!(
470 "{} BETWEEN {} AND {}",
471 match frame.mode {
472 FrameMode::Rows => "ROWS",
473 FrameMode::Range => "RANGE",
474 FrameMode::Groups => "GROUPS",
475 },
476 frame_bound_sql(&frame.start),
477 frame_bound_sql(&frame.end)
478 ));
479 }
480 format!("({})", parts.join(" "))
481}
482
483const fn binary_operator_sql(operator: crate::ast::BinaryOp) -> &'static str {
484 match operator {
485 crate::ast::BinaryOp::Equal => "=",
486 crate::ast::BinaryOp::NotEqual => "<>",
487 crate::ast::BinaryOp::Less => "<",
488 crate::ast::BinaryOp::LessEqual => "<=",
489 crate::ast::BinaryOp::Greater => ">",
490 crate::ast::BinaryOp::GreaterEqual => ">=",
491 crate::ast::BinaryOp::Add => "+",
492 crate::ast::BinaryOp::Subtract => "-",
493 crate::ast::BinaryOp::Multiply => "*",
494 crate::ast::BinaryOp::Divide => "/",
495 }
496}
497
498fn frame_bound_sql(bound: &FrameBound) -> String {
499 match bound {
500 FrameBound::UnboundedPreceding => "UNBOUNDED PRECEDING".into(),
501 FrameBound::UnboundedFollowing => "UNBOUNDED FOLLOWING".into(),
502 FrameBound::CurrentRow => "CURRENT ROW".into(),
503 FrameBound::Preceding(expression) => format!("{} PRECEDING", expr_sql(expression)),
504 FrameBound::Following(expression) => format!("{} FOLLOWING", expr_sql(expression)),
505 }
506}
507
508fn with_sql(ctes: &[CTE]) -> String {
509 if ctes.is_empty() {
510 return String::new();
511 }
512 let recursive = ctes.iter().any(|cte| cte.recursive);
513 format!(
514 "WITH {}{} ",
515 if recursive { "RECURSIVE " } else { "" },
516 ctes.iter().map(cte_sql).collect::<Vec<_>>().join(", ")
517 )
518}
519
520fn cte_sql(cte: &CTE) -> String {
521 let mut rendered = ident(&cte.name);
522 if !cte.columns.is_empty() {
523 rendered.push_str(" (");
524 rendered.push_str(&ident_list(&cte.columns));
525 rendered.push(')');
526 }
527 rendered.push_str(" AS ");
528 rendered.push_str(match cte.materialization {
529 CteMaterialization::Default => "",
530 CteMaterialization::Materialized => "MATERIALIZED ",
531 CteMaterialization::NotMaterialized => "NOT MATERIALIZED ",
532 });
533 rendered.push('(');
534 rendered.push_str(&match &cte.body {
535 CteBody::Query(query) => select_sql(query),
536 CteBody::Insert(command) => insert_sql(command),
537 CteBody::Update(command) => update_sql(command),
538 CteBody::Delete(command) => delete_sql(command),
539 CteBody::Merge(command) => merge_sql(command),
540 });
541 rendered.push(')');
542 if let Some(search) = &cte.search {
543 write!(
544 &mut rendered,
545 " SEARCH {} FIRST BY {} SET {}",
546 if search.breadth_first {
547 "BREADTH"
548 } else {
549 "DEPTH"
550 },
551 ident_list(&search.columns),
552 ident(&search.sequence_column)
553 )
554 .expect("writing to a String cannot fail");
555 }
556 if let Some(cycle) = &cte.cycle {
557 write!(
558 &mut rendered,
559 " CYCLE {} SET {} TO {} DEFAULT {} USING {}",
560 ident_list(&cycle.columns),
561 ident(&cycle.mark_column),
562 expr_sql(&cycle.mark_value),
563 expr_sql(&cycle.mark_default),
564 ident(&cycle.path_column)
565 )
566 .expect("writing to a String cannot fail");
567 }
568 rendered
569}
570
571fn render_order_limit_offset(
572 rendered: &mut String,
573 order_by: &[OrderBy],
574 limit: Option<&Expr>,
575 with_ties: bool,
576 offset: Option<&Expr>,
577) {
578 if !order_by.is_empty() {
579 rendered.push_str(" ORDER BY ");
580 rendered.push_str(&order_by_sql(order_by));
581 }
582 if with_ties {
583 if let Some(offset) = offset {
584 rendered.push_str(" OFFSET ");
585 rendered.push_str(&expr_sql(offset));
586 }
587 if let Some(limit) = limit {
588 rendered.push_str(" FETCH FIRST ");
589 rendered.push_str(&expr_sql(limit));
590 rendered.push_str(" ROWS WITH TIES");
591 }
592 } else {
593 if let Some(limit) = limit {
594 rendered.push_str(" LIMIT ");
595 rendered.push_str(&expr_sql(limit));
596 }
597 if let Some(offset) = offset {
598 rendered.push_str(" OFFSET ");
599 rendered.push_str(&expr_sql(offset));
600 }
601 }
602}
603
604fn render_returning(rendered: &mut String, aliases: &ReturningAliases, projections: &[Projection]) {
605 if projections.is_empty() {
606 return;
607 }
608 rendered.push_str(" RETURNING ");
609 if aliases.old_explicit || aliases.new_explicit {
610 rendered.push_str("WITH (");
611 let mut names = Vec::new();
612 if aliases.old_explicit {
613 names.push(format!("OLD AS {}", ident(&aliases.old)));
614 }
615 if aliases.new_explicit {
616 names.push(format!("NEW AS {}", ident(&aliases.new)));
617 }
618 rendered.push_str(&names.join(", "));
619 rendered.push_str(") ");
620 }
621 rendered.push_str(&projections_sql(projections));
622}
623
624fn render_target_alias(rendered: &mut String, relation: &str, qualifier: &str) {
625 if relation_local_name(relation) != qualifier {
626 rendered.push_str(" AS ");
627 rendered.push_str(&ident(qualifier));
628 }
629}
630
631fn render_relation_alias(rendered: &mut String, alias: Option<&str>, columns: &[String]) {
632 if let Some(alias) = alias {
633 rendered.push_str(" AS ");
634 rendered.push_str(&ident(alias));
635 if !columns.is_empty() {
636 rendered.push('(');
637 rendered.push_str(&ident_list(columns));
638 rendered.push(')');
639 }
640 }
641}
642
643fn render_function_alias(
644 rendered: &mut String,
645 alias: Option<&str>,
646 columns: &[String],
647 types: &[String],
648) {
649 if let Some(alias) = alias {
650 rendered.push_str(" AS ");
651 rendered.push_str(&ident(alias));
652 } else if !types.is_empty() {
653 rendered.push_str(" AS");
654 }
655 if !types.is_empty() {
656 rendered.push_str(" (");
657 rendered.push_str(
658 &columns
659 .iter()
660 .zip(types)
661 .map(|(name, ty)| format!("{} {ty}", ident(name)))
662 .collect::<Vec<_>>()
663 .join(", "),
664 );
665 rendered.push(')');
666 } else if !columns.is_empty() {
667 rendered.push('(');
668 rendered.push_str(&ident_list(columns));
669 rendered.push(')');
670 }
671}
672
673fn assignments_sql(assignments: &[(String, Expr)]) -> String {
674 assignments
675 .iter()
676 .map(|(column, expression)| format!("{} = {}", ident(column), expr_sql(expression)))
677 .collect::<Vec<_>>()
678 .join(", ")
679}
680
681fn projections_sql(projections: &[Projection]) -> String {
682 projections
683 .iter()
684 .map(|projection| {
685 let mut rendered = expr_sql(&projection.expr);
686 if let Some(alias) = &projection.alias {
687 rendered.push_str(" AS ");
688 rendered.push_str(&ident(alias));
689 }
690 rendered
691 })
692 .collect::<Vec<_>>()
693 .join(", ")
694}
695
696fn order_by_sql(order_by: &[OrderBy]) -> String {
697 order_by
698 .iter()
699 .map(|order| {
700 let mut rendered = expr_sql(&order.expr);
701 if order.descending {
702 rendered.push_str(" DESC");
703 }
704 match order.nulls {
705 Some(NullsOrder::First) => rendered.push_str(" NULLS FIRST"),
706 Some(NullsOrder::Last) => rendered.push_str(" NULLS LAST"),
707 None => {}
708 }
709 rendered
710 })
711 .collect::<Vec<_>>()
712 .join(", ")
713}
714
715fn rows_sql(rows: &[Vec<Expr>]) -> String {
716 rows.iter()
717 .map(|row| format!("({})", expr_list(row)))
718 .collect::<Vec<_>>()
719 .join(", ")
720}
721
722fn expr_list(expressions: &[Expr]) -> String {
723 expressions
724 .iter()
725 .map(expr_sql)
726 .collect::<Vec<_>>()
727 .join(", ")
728}
729
730fn expr_sql(expression: &Expr) -> String {
731 render_expr(expression).expect("durable SQL AST cannot contain executor-only columns")
732}
733
734fn only_relation(name: &str, include_descendants: bool) -> String {
735 if include_descendants {
736 name.to_string()
737 } else {
738 format!("ONLY {name}")
739 }
740}
741
742fn ident_list(names: &[String]) -> String {
743 names
744 .iter()
745 .map(|name| ident(name))
746 .collect::<Vec<_>>()
747 .join(", ")
748}
749
750fn ident(name: &str) -> String {
751 crate::expr::quote_ident(name)
752}
753
754fn relation_local_name(name: &str) -> &str {
755 let mut quoted = false;
756 let mut last_dot = None;
757 let bytes = name.as_bytes();
758 let mut position = 0;
759 while position < bytes.len() {
760 match bytes[position] {
761 b'"' if quoted && bytes.get(position + 1) == Some(&b'"') => position += 2,
762 b'"' => {
763 quoted = !quoted;
764 position += 1;
765 }
766 b'.' if !quoted => {
767 last_dot = Some(position);
768 position += 1;
769 }
770 _ => position += 1,
771 }
772 }
773 let component = &name[last_dot.map_or(0, |dot| dot + 1)..];
774 component
775 .strip_prefix('"')
776 .and_then(|component| component.strip_suffix('"'))
777 .unwrap_or(component)
778}
779
780fn string_literal(value: &str) -> String {
781 format!("'{}'", value.replace('\'', "''"))
782}
783
784fn value_sql(value: &Value) -> String {
785 match value {
786 Value::Null => "NULL".into(),
787 Value::Void => "''::void".into(),
788 Value::Bool(value) => if *value { "true" } else { "false" }.into(),
789 Value::Int(value) => value.to_string(),
790 Value::Float(value) if value.is_finite() => value.to_string(),
791 Value::Float(value) => format!("{}::double precision", string_literal(&value.to_string())),
792 Value::Str(value) => string_literal(value),
793 Value::FixedChar(value) => format!("{}::character", string_literal(value)),
794 Value::Bytes(value) => {
795 let mut hex = String::new();
796 for byte in value {
797 write!(&mut hex, "{byte:02x}").expect("writing to a String cannot fail");
798 }
799 format!("{}::bytea", string_literal(&format!("\\x{hex}")))
800 }
801 Value::Temporal(value) => {
802 let ty = match value {
803 TemporalValue::Date { .. } => "date",
804 TemporalValue::Time { .. } => "time",
805 TemporalValue::TimeTz { .. } => "time with time zone",
806 TemporalValue::Timestamp { .. } => "timestamp",
807 TemporalValue::TimestampTz { .. } => "timestamp with time zone",
808 TemporalValue::Interval { .. } => "interval",
809 };
810 format!("{}::{ty}", string_literal(&value.to_sql_string()))
811 }
812 Value::Decimal(value) if value.is_nan() || value.is_infinite() => {
813 format!("{}::numeric", string_literal(&value.to_sql_string()))
814 }
815 Value::Decimal(value) => format!("{}::numeric", value.to_sql_string()),
816 Value::Json(value) => format!("{}::json", string_literal(value)),
817 Value::JsonB(value) => format!("{}::jsonb", string_literal(value)),
818 Value::Array(array) => format!(
819 "ARRAY[{}]",
820 array
821 .elements()
822 .iter()
823 .map(value_sql)
824 .collect::<Vec<_>>()
825 .join(", ")
826 ),
827 Value::List(values) => format!(
828 "ARRAY[{}]",
829 values.iter().map(value_sql).collect::<Vec<_>>().join(", ")
830 ),
831 Value::Row(values) => format!(
832 "ROW({})",
833 values.iter().map(value_sql).collect::<Vec<_>>().join(", ")
834 ),
835 Value::Record(fields) => format!(
836 "ROW({})",
837 fields
838 .iter()
839 .map(|(_, value)| value_sql(value))
840 .collect::<Vec<_>>()
841 .join(", ")
842 ),
843 Value::Map(value) => format!(
844 "{}::jsonb",
845 string_literal(
846 &serde_json::to_string(value)
847 .expect("serializing an in-memory Value map cannot fail")
848 )
849 ),
850 }
851}
852
853#[cfg(test)]
854mod tests {
855 use super::statement_sql;
856
857 #[test]
858 fn rendered_rule_action_shapes_round_trip_stably() {
859 for sql in [
860 "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",
861 "WITH source(value) AS MATERIALIZED (SELECT 1) SELECT value FROM source UNION ALL SELECT 2 ORDER BY value",
862 "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",
863 "UPDATE target_table AS target SET value = source.value FROM source_table AS source(id, value) WHERE target.id = source.id RETURNING target.id",
864 "DELETE FROM target_table AS target USING source_table AS source(id) WHERE target.id = source.id RETURNING target.id",
865 "NOTIFY rule_channel, 'payload'",
866 ] {
867 let mut statements = crate::compile(sql).unwrap_or_else(|error| panic!("{sql}: {error}"));
868 let rendered = statement_sql(&statements.remove(0))
869 .unwrap_or_else(|error| panic!("render {sql}: {error}"));
870 let mut reparsed = crate::compile(&rendered)
871 .unwrap_or_else(|error| panic!("reparse `{rendered}` from `{sql}`: {error}"));
872 let rerendered = statement_sql(&reparsed.remove(0))
873 .unwrap_or_else(|error| panic!("rerender `{rendered}`: {error}"));
874 assert_eq!(rerendered, rendered, "unstable SQL rendering for `{sql}`");
875 }
876 }
877}