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