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