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