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, FromClause, JoinKind, LockWait, NullsOrder,
15    OperatorJoinRelations, OrderBy, Projection, ReturningAliases, SelectStmt, SetOpKind, Statement,
16    TableFunction, WindowDefinition, 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;
24mod window;
25pub use window::frame_clause_sql;
26mod expressions;
27use expressions::{render_expr, render_expr_in};
28pub(crate) mod function_syntax;
29
30pub(crate) fn pattern_sql<'a, T: 'a>(
31    operator: crate::ast::PatternOperator,
32    arguments: &'a [T],
33    mut render: impl FnMut(&'a T) -> Result<String, SQLError>,
34) -> Result<String, SQLError> {
35    let [left, pattern] = arguments else {
36        return Err(SQLError::Internal(
37            "invalid pattern operator operands".into(),
38        ));
39    };
40    Ok(format!(
41        "({} {} {})",
42        render(left)?,
43        operator.syntax(),
44        render(pattern)?
45    ))
46}
47
48/// Render one executable statement represented by UQA's durable SQL AST.
49pub fn statement_sql(statement: &Statement) -> Result<String, SQLError> {
50    match statement {
51        Statement::Select(select) => select_sql(select),
52        Statement::Insert(insert) => insert_sql(insert),
53        Statement::Update(update) => update_sql(update),
54        Statement::Delete(delete) => delete_sql(delete),
55        Statement::Merge(merge) => merge_sql(merge),
56        Statement::Notify { channel, payload } => {
57            let payload = if payload.is_empty() {
58                String::new()
59            } else {
60                format!(", {}", string_literal(payload))
61            };
62            Ok(format!("NOTIFY {}{payload}", ident(channel)))
63        }
64        _ => Err(SQLError::Internal(
65            "durable rewrite-rule action has an unsupported statement kind".into(),
66        )),
67    }
68}
69
70/// Render one compiler-owned scalar expression without consulting runtime state.
71pub fn expression_sql(expression: &Expr) -> Result<String, SQLError> {
72    render_expr(expression)
73}
74
75fn select_sql(statement: &SelectStmt) -> Result<String, SQLError> {
76    let mut rendered = with_sql(&statement.with)?;
77    if let Some(set) = statement.set_op.as_deref() {
78        let left = set
79            .left
80            .as_deref()
81            .map_or_else(|| select_body_sql(statement), select_sql)?;
82        rendered.push('(');
83        rendered.push_str(&left);
84        rendered.push_str(") ");
85        rendered.push_str(match set.kind {
86            SetOpKind::Union => "UNION",
87            SetOpKind::Intersect => "INTERSECT",
88            SetOpKind::Except => "EXCEPT",
89        });
90        if set.all {
91            rendered.push_str(" ALL");
92        }
93        rendered.push_str(" (");
94        rendered.push_str(&select_sql(&set.right)?);
95        rendered.push(')');
96        render_order_limit_offset(
97            &mut rendered,
98            &set.combined_order_by,
99            set.combined_limit.as_ref(),
100            set.combined_with_ties,
101            set.combined_offset.as_ref(),
102        )?;
103        return Ok(rendered);
104    }
105    rendered.push_str(&select_body_sql(statement)?);
106    Ok(rendered)
107}
108
109fn select_body_sql(statement: &SelectStmt) -> Result<String, SQLError> {
110    let mut rendered = String::new();
111    if statement.values.is_empty() {
112        rendered.push_str("SELECT");
113        if !statement.distinct_on.is_empty() {
114            rendered.push_str(" DISTINCT ON (");
115            rendered.push_str(&expr_list_in(&statement.distinct_on, &statement.windows)?);
116            rendered.push(')');
117        } else if statement.distinct {
118            rendered.push_str(" DISTINCT");
119        }
120        rendered.push(' ');
121        rendered.push_str(&projections_sql_in(
122            &statement.projections,
123            &statement.windows,
124        )?);
125        if let Some(source) = &statement.from {
126            rendered.push_str(" FROM ");
127            rendered.push_str(&from_sql(source)?);
128        }
129        if let Some(predicate) = &statement.r#where {
130            rendered.push_str(" WHERE ");
131            rendered.push_str(&render_expr(predicate)?);
132        }
133        if !statement.grouping_sets.is_empty() {
134            rendered.push_str(" GROUP BY ");
135            if statement.group_distinct {
136                rendered.push_str("DISTINCT ");
137            }
138            rendered.push_str("GROUPING SETS (");
139            rendered.push_str(
140                &statement
141                    .grouping_sets
142                    .iter()
143                    .map(|set| Ok(format!("({})", expr_list(set)?)))
144                    .collect::<Result<Vec<_>, SQLError>>()?
145                    .join(", "),
146            );
147            rendered.push(')');
148        } else if !statement.group_by.is_empty() {
149            rendered.push_str(" GROUP BY ");
150            if statement.group_distinct {
151                rendered.push_str("DISTINCT ");
152            }
153            rendered.push_str(&expr_list(&statement.group_by)?);
154        }
155        if let Some(predicate) = &statement.having {
156            rendered.push_str(" HAVING ");
157            rendered.push_str(&render_expr(predicate)?);
158        }
159    } else {
160        rendered.push_str("VALUES ");
161        rendered.push_str(&rows_sql(&statement.values)?);
162    }
163    rendered.push_str(&window::window_clause_sql(&statement.windows)?);
164    render_order_limit_offset_in(
165        &mut rendered,
166        &statement.order_by,
167        statement.limit.as_ref(),
168        statement.with_ties,
169        statement.offset.as_ref(),
170        &statement.windows,
171    )?;
172    for locking in &statement.locking {
173        rendered.push(' ');
174        rendered.push_str(locking.strength.sql_name());
175        if !locking.relations.is_empty() {
176            rendered.push_str(" OF ");
177            rendered.push_str(&ident_list(&locking.relations));
178        }
179        rendered.push_str(match locking.wait {
180            LockWait::Block => "",
181            LockWait::SkipLocked => " SKIP LOCKED",
182            LockWait::NoWait => " NOWAIT",
183        });
184    }
185    Ok(rendered)
186}
187
188#[expect(
189    clippy::too_many_lines,
190    reason = "exhaustive FROM rendering keeps each AST variant visibly complete"
191)]
192fn from_sql(source: &FromClause) -> Result<String, SQLError> {
193    Ok(match source {
194        FromClause::Table {
195            name,
196            alias,
197            column_aliases,
198            include_descendants,
199            ..
200        } => {
201            let mut rendered = only_relation(name, *include_descendants);
202            render_relation_alias(&mut rendered, alias.as_deref(), column_aliases);
203            rendered
204        }
205        FromClause::Join {
206            left,
207            right,
208            kind,
209            on,
210            using,
211            natural,
212            alias,
213            column_aliases,
214            lateral,
215        } => {
216            let mut rendered = String::from("(");
217            rendered.push_str(&from_sql(left)?);
218            rendered.push(' ');
219            if *natural {
220                rendered.push_str("NATURAL ");
221            }
222            rendered.push_str(match kind {
223                JoinKind::Inner => "JOIN",
224                JoinKind::Left => "LEFT JOIN",
225                JoinKind::Right => "RIGHT JOIN",
226                JoinKind::Full => "FULL JOIN",
227                JoinKind::Cross => "CROSS JOIN",
228            });
229            rendered.push(' ');
230            if *lateral {
231                rendered.push_str("LATERAL ");
232            }
233            rendered.push_str(&from_sql(right)?);
234            if let Some(predicate) = on {
235                rendered.push_str(" ON ");
236                rendered.push_str(&render_expr(predicate)?);
237            } else if let Some(using) = using {
238                rendered.push_str(" USING (");
239                rendered.push_str(&ident_list(&using.columns));
240                rendered.push(')');
241                if let Some(alias) = &using.alias {
242                    rendered.push_str(" AS ");
243                    rendered.push_str(&ident(alias));
244                }
245            }
246            rendered.push(')');
247            render_relation_alias(&mut rendered, alias.as_deref(), column_aliases);
248            rendered
249        }
250        FromClause::Values {
251            rows,
252            alias,
253            column_aliases,
254            ..
255        } => {
256            let mut rendered = format!("(VALUES {})", rows_sql(rows)?);
257            render_relation_alias(&mut rendered, alias.as_deref(), column_aliases);
258            rendered
259        }
260        FromClause::Function {
261            name,
262            output_name: _,
263            relations,
264            args,
265            alias,
266            column_aliases,
267            ordinality,
268            column_types,
269            ..
270        } => {
271            let mut rendered = table_function_call_sql(name, relations.as_ref(), args)?;
272            if *ordinality {
273                rendered.push_str(" WITH ORDINALITY");
274            }
275            render_function_alias(
276                &mut rendered,
277                alias.as_deref(),
278                column_aliases,
279                column_types,
280            );
281            rendered
282        }
283        FromClause::FunctionGroup {
284            functions,
285            alias,
286            column_aliases,
287            ordinality,
288        } => {
289            let mut rendered = format!(
290                "ROWS FROM ({})",
291                functions
292                    .iter()
293                    .map(table_function_sql)
294                    .collect::<Result<Vec<_>, _>>()?
295                    .join(", ")
296            );
297            if *ordinality {
298                rendered.push_str(" WITH ORDINALITY");
299            }
300            render_relation_alias(&mut rendered, alias.as_deref(), column_aliases);
301            rendered
302        }
303        FromClause::Subquery {
304            body,
305            alias,
306            column_aliases,
307        } => {
308            let mut rendered = format!("({})", select_sql(body)?);
309            render_relation_alias(&mut rendered, alias.as_deref(), column_aliases);
310            rendered
311        }
312    })
313}
314
315fn table_function_sql(function: &TableFunction) -> Result<String, SQLError> {
316    let mut rendered =
317        table_function_call_sql(&function.name, function.relations.as_ref(), &function.args)?;
318    if !function.column_types.is_empty() {
319        rendered.push_str(" AS (");
320        rendered.push_str(
321            &function
322                .column_aliases
323                .iter()
324                .zip(&function.column_types)
325                .map(|(name, ty)| format!("{} {ty}", ident(name)))
326                .collect::<Vec<_>>()
327                .join(", "),
328        );
329        rendered.push(')');
330    }
331    Ok(rendered)
332}
333
334/// A table function call whose operator-join relations precede the operands bound to them.
335fn table_function_call_sql(
336    name: &str,
337    relations: Option<&OperatorJoinRelations>,
338    args: &[Expr],
339) -> Result<String, SQLError> {
340    let mut arguments = args
341        .iter()
342        .map(render_expr)
343        .collect::<Result<Vec<_>, _>>()?;
344    if let Some(relations) = relations {
345        if arguments.is_empty() {
346            return Err(SQLError::Internal(format!(
347                "operator join table function `{name}` has no left operand"
348            )));
349        }
350        arguments.insert(0, relations.left.clone());
351        arguments.insert(2, relations.right.clone());
352    }
353    Ok(format!("{name}({})", arguments.join(", ")))
354}
355
356fn with_sql(ctes: &[CTE]) -> Result<String, SQLError> {
357    if ctes.is_empty() {
358        return Ok(String::new());
359    }
360    let recursive = ctes.iter().any(|cte| cte.recursive);
361    Ok(format!(
362        "WITH {}{} ",
363        if recursive { "RECURSIVE " } else { "" },
364        ctes.iter()
365            .map(cte_sql)
366            .collect::<Result<Vec<_>, _>>()?
367            .join(", ")
368    ))
369}
370
371fn cte_sql(cte: &CTE) -> Result<String, SQLError> {
372    let mut rendered = ident(&cte.name);
373    if !cte.columns.is_empty() {
374        rendered.push_str(" (");
375        rendered.push_str(&ident_list(&cte.columns));
376        rendered.push(')');
377    }
378    rendered.push_str(" AS ");
379    rendered.push_str(match cte.materialization {
380        CteMaterialization::Default => "",
381        CteMaterialization::Materialized => "MATERIALIZED ",
382        CteMaterialization::NotMaterialized => "NOT MATERIALIZED ",
383    });
384    rendered.push('(');
385    rendered.push_str(&match &cte.body {
386        CteBody::Query(query) => select_sql(query),
387        CteBody::Insert(command) => insert_sql(command),
388        CteBody::Update(command) => update_sql(command),
389        CteBody::Delete(command) => delete_sql(command),
390        CteBody::Merge(command) => merge_sql(command),
391    }?);
392    rendered.push(')');
393    if let Some(search) = &cte.search {
394        write!(
395            &mut rendered,
396            " SEARCH {} FIRST BY {} SET {}",
397            if search.breadth_first {
398                "BREADTH"
399            } else {
400                "DEPTH"
401            },
402            ident_list(&search.columns),
403            ident(&search.sequence_column)
404        )
405        .expect("writing to a String cannot fail");
406    }
407    if let Some(cycle) = &cte.cycle {
408        write!(
409            &mut rendered,
410            " CYCLE {} SET {} TO {} DEFAULT {} USING {}",
411            ident_list(&cycle.columns),
412            ident(&cycle.mark_column),
413            render_expr(&cycle.mark_value)?,
414            render_expr(&cycle.mark_default)?,
415            ident(&cycle.path_column)
416        )
417        .expect("writing to a String cannot fail");
418    }
419    Ok(rendered)
420}
421
422fn render_order_limit_offset(
423    rendered: &mut String,
424    order_by: &[OrderBy],
425    limit: Option<&Expr>,
426    with_ties: bool,
427    offset: Option<&Expr>,
428) -> Result<(), SQLError> {
429    render_order_limit_offset_in(rendered, order_by, limit, with_ties, offset, &[])
430}
431
432fn render_order_limit_offset_in(
433    rendered: &mut String,
434    order_by: &[OrderBy],
435    limit: Option<&Expr>,
436    with_ties: bool,
437    offset: Option<&Expr>,
438    windows: &[WindowDefinition],
439) -> Result<(), SQLError> {
440    if !order_by.is_empty() {
441        rendered.push_str(" ORDER BY ");
442        rendered.push_str(&order_by_sql_in(order_by, windows)?);
443    }
444    if with_ties {
445        if let Some(offset) = offset {
446            rendered.push_str(" OFFSET ");
447            rendered.push_str(&render_expr_in(offset, windows)?);
448        }
449        if let Some(limit) = limit {
450            rendered.push_str(" FETCH FIRST ");
451            rendered.push_str(&render_expr_in(limit, windows)?);
452            rendered.push_str(" ROWS WITH TIES");
453        }
454    } else {
455        if let Some(limit) = limit {
456            rendered.push_str(" LIMIT ");
457            rendered.push_str(&render_expr_in(limit, windows)?);
458        }
459        if let Some(offset) = offset {
460            rendered.push_str(" OFFSET ");
461            rendered.push_str(&render_expr_in(offset, windows)?);
462        }
463    }
464    Ok(())
465}
466
467fn render_returning(
468    rendered: &mut String,
469    aliases: &ReturningAliases,
470    projections: &[Projection],
471) -> Result<(), SQLError> {
472    if projections.is_empty() {
473        return Ok(());
474    }
475    rendered.push_str(" RETURNING ");
476    if aliases.old_explicit || aliases.new_explicit {
477        rendered.push_str("WITH (");
478        let mut names = Vec::new();
479        if aliases.old_explicit {
480            names.push(format!("OLD AS {}", ident(&aliases.old)));
481        }
482        if aliases.new_explicit {
483            names.push(format!("NEW AS {}", ident(&aliases.new)));
484        }
485        rendered.push_str(&names.join(", "));
486        rendered.push_str(") ");
487    }
488    rendered.push_str(&projections_sql(projections)?);
489    Ok(())
490}
491
492fn render_target_alias(rendered: &mut String, relation: &str, qualifier: &str) {
493    if relation_local_name(relation) != qualifier {
494        rendered.push_str(" AS ");
495        rendered.push_str(&ident(qualifier));
496    }
497}
498
499fn render_relation_alias(rendered: &mut String, alias: Option<&str>, columns: &[String]) {
500    if let Some(alias) = alias {
501        rendered.push_str(" AS ");
502        rendered.push_str(&ident(alias));
503        if !columns.is_empty() {
504            rendered.push('(');
505            rendered.push_str(&ident_list(columns));
506            rendered.push(')');
507        }
508    }
509}
510
511fn render_function_alias(
512    rendered: &mut String,
513    alias: Option<&str>,
514    columns: &[String],
515    types: &[String],
516) {
517    if let Some(alias) = alias {
518        rendered.push_str(" AS ");
519        rendered.push_str(&ident(alias));
520    } else if !types.is_empty() {
521        rendered.push_str(" AS");
522    }
523    if !types.is_empty() {
524        rendered.push_str(" (");
525        rendered.push_str(
526            &columns
527                .iter()
528                .zip(types)
529                .map(|(name, ty)| format!("{} {ty}", ident(name)))
530                .collect::<Vec<_>>()
531                .join(", "),
532        );
533        rendered.push(')');
534    } else if !columns.is_empty() {
535        rendered.push('(');
536        rendered.push_str(&ident_list(columns));
537        rendered.push(')');
538    }
539}
540
541fn assignment_target_sql(target: &crate::ast::AssignmentTarget) -> Result<String, SQLError> {
542    use crate::ast::AssignmentStep;
543    let mut sql = ident(&target.column);
544    for step in &target.indirection {
545        match step {
546            AssignmentStep::Field(field) => {
547                sql.push('.');
548                sql.push_str(&ident(field));
549            }
550            AssignmentStep::Index(index) => {
551                sql.push('[');
552                sql.push_str(&render_expr(index)?);
553                sql.push(']');
554            }
555            AssignmentStep::Slice { lower, upper } => {
556                sql.push('[');
557                if let Some(lower) = lower {
558                    sql.push_str(&render_expr(lower)?);
559                }
560                sql.push(':');
561                if let Some(upper) = upper {
562                    sql.push_str(&render_expr(upper)?);
563                }
564                sql.push(']');
565            }
566        }
567    }
568    Ok(sql)
569}
570
571fn assignment_targets_sql(targets: &[crate::ast::AssignmentTarget]) -> Result<String, SQLError> {
572    Ok(targets
573        .iter()
574        .map(assignment_target_sql)
575        .collect::<Result<Vec<_>, _>>()?
576        .join(", "))
577}
578
579fn assignments_sql(
580    assignments: &[(crate::ast::AssignmentTargets, Expr)],
581) -> Result<String, SQLError> {
582    Ok(assignments
583        .iter()
584        .map(|(target, expression)| {
585            Ok(format!(
586                "{} = {}",
587                match target {
588                    crate::ast::AssignmentTargets::Single(target) => assignment_target_sql(target)?,
589                    crate::ast::AssignmentTargets::Multiple(targets) =>
590                        format!("({})", assignment_targets_sql(&targets.targets)?),
591                },
592                render_expr(expression)?
593            ))
594        })
595        .collect::<Result<Vec<_>, SQLError>>()?
596        .join(", "))
597}
598
599fn projections_sql(projections: &[Projection]) -> Result<String, SQLError> {
600    projections_sql_in(projections, &[])
601}
602
603fn projections_sql_in(
604    projections: &[Projection],
605    windows: &[WindowDefinition],
606) -> Result<String, SQLError> {
607    Ok(projections
608        .iter()
609        .map(|projection| {
610            let mut rendered = render_expr_in(&projection.expr, windows)?;
611            if let Some(alias) = &projection.alias {
612                rendered.push_str(" AS ");
613                rendered.push_str(&ident(alias));
614            }
615            Ok(rendered)
616        })
617        .collect::<Result<Vec<_>, SQLError>>()?
618        .join(", "))
619}
620
621fn order_by_sql(order_by: &[OrderBy]) -> Result<String, SQLError> {
622    order_by_sql_in(order_by, &[])
623}
624
625fn order_by_sql_in(order_by: &[OrderBy], windows: &[WindowDefinition]) -> Result<String, SQLError> {
626    Ok(order_by
627        .iter()
628        .map(|order| {
629            let mut rendered = render_expr_in(&order.expr, windows)?;
630            if order.descending {
631                rendered.push_str(" DESC");
632            }
633            match order.nulls {
634                Some(NullsOrder::First) => rendered.push_str(" NULLS FIRST"),
635                Some(NullsOrder::Last) => rendered.push_str(" NULLS LAST"),
636                None => {}
637            }
638            Ok(rendered)
639        })
640        .collect::<Result<Vec<_>, SQLError>>()?
641        .join(", "))
642}
643
644fn rows_sql(rows: &[Vec<Expr>]) -> Result<String, SQLError> {
645    Ok(rows
646        .iter()
647        .map(|row| Ok(format!("({})", expr_list(row)?)))
648        .collect::<Result<Vec<_>, SQLError>>()?
649        .join(", "))
650}
651
652fn expr_list(expressions: &[Expr]) -> Result<String, SQLError> {
653    expr_list_in(expressions, &[])
654}
655
656fn expr_list_in(expressions: &[Expr], windows: &[WindowDefinition]) -> Result<String, SQLError> {
657    Ok(expressions
658        .iter()
659        .map(|expression| render_expr_in(expression, windows))
660        .collect::<Result<Vec<_>, _>>()?
661        .join(", "))
662}
663
664fn only_relation(name: &str, include_descendants: bool) -> String {
665    if include_descendants {
666        name.to_string()
667    } else {
668        format!("ONLY {name}")
669    }
670}
671
672fn ident_list(names: &[String]) -> String {
673    names
674        .iter()
675        .map(|name| ident(name))
676        .collect::<Vec<_>>()
677        .join(", ")
678}
679
680fn ident(name: &str) -> String {
681    crate::expr::quote_ident(name)
682}
683
684fn relation_local_name(name: &str) -> &str {
685    let mut quoted = false;
686    let mut last_dot = None;
687    let bytes = name.as_bytes();
688    let mut position = 0;
689    while position < bytes.len() {
690        match bytes[position] {
691            b'"' if quoted && bytes.get(position + 1) == Some(&b'"') => position += 2,
692            b'"' => {
693                quoted = !quoted;
694                position += 1;
695            }
696            b'.' if !quoted => {
697                last_dot = Some(position);
698                position += 1;
699            }
700            _ => position += 1,
701        }
702    }
703    let component = &name[last_dot.map_or(0, |dot| dot + 1)..];
704    component
705        .strip_prefix('"')
706        .and_then(|component| component.strip_suffix('"'))
707        .unwrap_or(component)
708}
709
710fn string_literal(value: &str) -> String {
711    format!("'{}'", value.replace('\'', "''"))
712}
713
714fn value_sql(value: &Value) -> Result<String, SQLError> {
715    Ok(match value {
716        Value::Datum(value) => return value_sql(&crate::expr::datums::read(value)?),
717        Value::Null => "NULL".into(),
718        Value::Void => "''::void".into(),
719        Value::Bool(value) => if *value { "true" } else { "false" }.into(),
720        Value::Int(value) => value.to_string(),
721        Value::Float(value) if value.is_finite() => value.to_string(),
722        Value::Float(value) => format!("{}::double precision", string_literal(&value.to_string())),
723        Value::Str(value) => string_literal(value),
724        Value::Enum(value) => return Err(crate::expr::catalog_output_required(value)),
725        Value::FixedChar(value) => format!("{}::character", string_literal(value)),
726        Value::Bytes(value) => {
727            let mut hex = String::new();
728            for byte in value {
729                write!(&mut hex, "{byte:02x}").expect("writing to a String cannot fail");
730            }
731            format!("{}::bytea", string_literal(&format!("\\x{hex}")))
732        }
733        Value::Temporal(value) => {
734            let ty = match value {
735                TemporalValue::Date { .. } => "date",
736                TemporalValue::Time { .. } => "time",
737                TemporalValue::TimeTz { .. } => "time with time zone",
738                TemporalValue::Timestamp { .. } => "timestamp",
739                TemporalValue::TimestampTz { .. } => "timestamp with time zone",
740                TemporalValue::Interval { .. } => "interval",
741            };
742            format!("{}::{ty}", string_literal(&value.to_sql_string()))
743        }
744        Value::Decimal(value) if value.is_nan() || value.is_infinite() => {
745            format!("{}::numeric", string_literal(&value.to_sql_string()))
746        }
747        Value::Decimal(value) => format!("{}::numeric", value.to_sql_string()),
748        Value::Json(value) => format!("{}::json", string_literal(value)),
749        Value::JsonB(value) => format!("{}::jsonb", string_literal(value)),
750        Value::LegacyVector(vector) => legacy_vector_expression(vector)?,
751        Value::Array(array) => format!(
752            "ARRAY[{}]",
753            array
754                .elements()
755                .iter()
756                .map(value_sql)
757                .collect::<Result<Vec<_>, _>>()?
758                .join(", ")
759        ),
760        Value::List(values) => format!(
761            "ARRAY[{}]",
762            values
763                .iter()
764                .map(value_sql)
765                .collect::<Result<Vec<_>, _>>()?
766                .join(", ")
767        ),
768        Value::Row(values) => format!(
769            "ROW({})",
770            values
771                .iter()
772                .map(value_sql)
773                .collect::<Result<Vec<_>, _>>()?
774                .join(", ")
775        ),
776        Value::Record(fields) => format!(
777            "ROW({})",
778            fields
779                .iter()
780                .map(|(_, value)| value_sql(value))
781                .collect::<Result<Vec<_>, _>>()?
782                .join(", ")
783        ),
784        Value::Map(value) => format!(
785            "{}::jsonb",
786            string_literal(&serde_json::to_string(value).map_err(|error| {
787                SQLError::Internal(format!("serialize map literal as jsonb: {error}"))
788            })?)
789        ),
790    })
791}
792
793#[cfg(test)]
794mod tests;