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 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
52pub 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
316fn 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;