Skip to main content

uqa_sql/
render.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Stable SQL rendering for compiler-owned statement trees.
8
9use 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
23/// Render one executable statement represented by UQA's durable SQL AST.
24pub 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
45/// Render one compiler-owned scalar expression without consulting runtime state.
46pub 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}