1use 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
48pub 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
70pub 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
334fn 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;