Skip to main content

uqa_sql/catalog/
expression_text.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Stable SQL text rendering for cataloged expressions.
8
9use std::fmt::Write as _;
10
11use crate::ast::Expr;
12use crate::SQLError;
13use uqa_core::Value;
14
15pub fn default_expr_text(expr: Option<&Expr>) -> Result<Value, SQLError> {
16    expr.map_or(Ok(Value::Null), |expr| {
17        schema_expr_text(expr).map(Value::Str)
18    })
19}
20
21#[expect(
22    clippy::too_many_lines,
23    reason = "preserves catalog column and OID order"
24)]
25pub fn schema_expr_text(expr: &Expr) -> Result<String, SQLError> {
26    if let Some(clause) = crate::catalog::collations::expression::ast_clause(expr)? {
27        return Ok(format!(
28            "({} COLLATE {})",
29            schema_expr_text(clause.argument)?,
30            clause.executable_name()?
31        ));
32    }
33    Ok(match expr {
34        Expr::Star => "*".into(),
35        Expr::QualifiedStar(qualifier) => format!("{qualifier}.*"),
36        Expr::Default => "DEFAULT".into(),
37        Expr::Column(name) => name.clone(),
38        Expr::QualifiedColumn {
39            qualifier, column, ..
40        } => format!("{qualifier}.{column}"),
41        Expr::InternalColumn(column) => {
42            unreachable!("executor-only column {column:?} reached catalog SQL rendering")
43        }
44        Expr::Literal(value) => schema_literal_text(value)?,
45        Expr::TypedLiteral { value, ty, .. } => format!("({})::{ty}", schema_literal_text(value)?),
46        Expr::Param(index) => format!("${index}"),
47        Expr::Func {
48            name,
49            binding,
50            args,
51            distinct,
52            order_by,
53            order_syntax,
54            filter,
55            ..
56        } => {
57            if let Some(crate::ast::FunctionDispatch::Pattern { operator, .. }) =
58                binding.as_ref().and_then(|binding| binding.dispatch)
59            {
60                return crate::render::pattern_sql(operator, args, schema_expr_text);
61            }
62            if binding
63                .as_ref()
64                .and_then(|binding| binding.dispatch)
65                .is_some_and(crate::ast::FunctionDispatch::is_concatenation)
66            {
67                let [left, right] = args.as_slice() else {
68                    return Err(SQLError::Internal("invalid concatenation operands".into()));
69                };
70                return Ok(format!(
71                    "({} || {})",
72                    schema_expr_text(left)?,
73                    schema_expr_text(right)?
74                ));
75            }
76            if *order_syntax == crate::ast::FunctionCallSyntax::Extract {
77                let (field, source) = crate::render::function_syntax::extract_fields(args)?;
78                return Ok(format!(
79                    "EXTRACT({field} FROM {})",
80                    schema_expr_text(source)?
81                ));
82            }
83            if let Some(crate::ast::FunctionDispatch::NumericOperator(operator)) =
84                binding.as_ref().and_then(|binding| binding.dispatch)
85            {
86                match args.as_slice() {
87                    [argument] if operator.arity() == 1 => {
88                        return Ok(format!(
89                            "({} {})",
90                            operator.symbol(),
91                            schema_expr_text(argument)?
92                        ))
93                    }
94                    [left, right] if operator.arity() == 2 => {
95                        return Ok(format!(
96                            "({} {} {})",
97                            schema_expr_text(left)?,
98                            operator.symbol(),
99                            schema_expr_text(right)?
100                        ))
101                    }
102                    _ => {}
103                }
104            }
105            let mut rendered_args = args
106                .iter()
107                .map(schema_expr_text)
108                .collect::<Result<Vec<_>, SQLError>>()?
109                .join(", ");
110            if *distinct {
111                rendered_args = format!("DISTINCT {rendered_args}");
112            }
113            let name = crate::render::function_syntax::ordinary_function_name(name);
114            let mut rendered = format!("{name}({rendered_args})");
115            if !order_by.is_empty() {
116                let order = order_by
117                    .iter()
118                    .map(|order| {
119                        let direction = if order.descending { " DESC" } else { "" };
120                        let nulls = match order.nulls {
121                            Some(crate::ast::NullsOrder::First) => " NULLS FIRST",
122                            Some(crate::ast::NullsOrder::Last) => " NULLS LAST",
123                            None => "",
124                        };
125                        Ok(format!(
126                            "{}{direction}{nulls}",
127                            schema_expr_text(&order.expr)?
128                        ))
129                    })
130                    .collect::<Result<Vec<_>, SQLError>>()?
131                    .join(", ");
132                if *order_syntax == crate::ast::FunctionOrderSyntax::WithinGroup {
133                    write!(&mut rendered, " WITHIN GROUP (ORDER BY {order})")
134                        .expect("writing to a String cannot fail");
135                } else {
136                    rendered.pop();
137                    if !rendered_args.is_empty() {
138                        rendered.push(' ');
139                    }
140                    write!(&mut rendered, "ORDER BY {order})")
141                        .expect("writing to a String cannot fail");
142                }
143            }
144            if let Some(filter) = filter {
145                write!(
146                    &mut rendered,
147                    " FILTER (WHERE {})",
148                    schema_expr_text(filter)?
149                )
150                .expect("writing to a String cannot fail");
151            }
152            rendered
153        }
154        Expr::Array(items) => format!(
155            "ARRAY[{}]",
156            items
157                .iter()
158                .map(schema_expr_text)
159                .collect::<Result<Vec<_>, SQLError>>()?
160                .join(", ")
161        ),
162        Expr::CompositeRow { items, binding } => format!(
163            "ROW({})::{}",
164            items
165                .iter()
166                .map(schema_expr_text)
167                .collect::<Result<Vec<_>, SQLError>>()?
168                .join(", "),
169            binding.ty
170        ),
171        Expr::Row(items) => format!(
172            "ROW({})",
173            items
174                .iter()
175                .map(schema_expr_text)
176                .collect::<Result<Vec<_>, SQLError>>()?
177                .join(", ")
178        ),
179        Expr::Binary { op, lhs, rhs } => format!(
180            "({} {} {})",
181            schema_expr_text(lhs)?,
182            match op {
183                crate::ast::BinaryOp::Equal => "=",
184                crate::ast::BinaryOp::NotEqual => "<>",
185                crate::ast::BinaryOp::Less => "<",
186                crate::ast::BinaryOp::LessEqual => "<=",
187                crate::ast::BinaryOp::Greater => ">",
188                crate::ast::BinaryOp::GreaterEqual => ">=",
189                crate::ast::BinaryOp::Add => "+",
190                crate::ast::BinaryOp::Subtract => "-",
191                crate::ast::BinaryOp::Multiply => "*",
192                crate::ast::BinaryOp::Divide => "/",
193            },
194            schema_expr_text(rhs)?
195        ),
196        Expr::Not(inner) => format!("(NOT {})", schema_expr_text(inner)?),
197        Expr::UnaryMinus(inner) => format!("(-{})", schema_expr_text(inner)?),
198        Expr::And(items) => format!(
199            "({})",
200            items
201                .iter()
202                .map(schema_expr_text)
203                .collect::<Result<Vec<_>, SQLError>>()?
204                .join(" AND ")
205        ),
206        Expr::Or(items) => format!(
207            "({})",
208            items
209                .iter()
210                .map(schema_expr_text)
211                .collect::<Result<Vec<_>, SQLError>>()?
212                .join(" OR ")
213        ),
214        Expr::IsNull { expr, negated } => format!(
215            "({} IS {}NULL)",
216            schema_expr_text(expr)?,
217            if *negated { "NOT " } else { "" }
218        ),
219        Expr::Between { expr, low, high } => format!(
220            "({} BETWEEN {} AND {})",
221            schema_expr_text(expr)?,
222            schema_expr_text(low)?,
223            schema_expr_text(high)?
224        ),
225        Expr::InList {
226            expr,
227            list,
228            negated,
229        } => format!(
230            "({} {}IN ({}))",
231            schema_expr_text(expr)?,
232            if *negated { "NOT " } else { "" },
233            list.iter()
234                .map(schema_expr_text)
235                .collect::<Result<Vec<_>, SQLError>>()?
236                .join(", ")
237        ),
238        Expr::WindowCall {
239            name, args, filter, ..
240        } => format!(
241            "{}({}){} OVER (...)",
242            name,
243            args.iter()
244                .map(schema_expr_text)
245                .collect::<Result<Vec<_>, SQLError>>()?
246                .join(", "),
247            filter
248                .as_deref()
249                .map(schema_expr_text)
250                .transpose()?
251                .map(|filter| format!(" FILTER (WHERE {filter})"))
252                .unwrap_or_default()
253        ),
254        Expr::Case {
255            base,
256            when,
257            else_branch,
258        } => {
259            let mut rendered = "CASE".to_string();
260            if let Some(base) = base {
261                rendered.push(' ');
262                rendered.push_str(&schema_expr_text(base)?);
263            }
264            for (condition, result) in when {
265                write!(
266                    &mut rendered,
267                    " WHEN {} THEN {}",
268                    schema_expr_text(condition)?,
269                    schema_expr_text(result)?
270                )
271                .expect("writing to a String cannot fail");
272            }
273            if let Some(else_branch) = else_branch {
274                write!(&mut rendered, " ELSE {}", schema_expr_text(else_branch)?)
275                    .expect("writing to a String cannot fail");
276            }
277            rendered.push_str(" END");
278            rendered
279        }
280        Expr::Cast { expr, ty, .. } => format!("({})::{ty}", schema_expr_text(expr)?),
281        Expr::ScalarSubquery(body) => format!("({body:?})"),
282        Expr::Exists { body, negated } => {
283            format!("{}EXISTS ({body:?})", if *negated { "NOT " } else { "" })
284        }
285        Expr::InSubquery {
286            expr,
287            body,
288            negated,
289        } => format!(
290            "({} {}IN ({body:?}))",
291            schema_expr_text(expr)?,
292            if *negated { "NOT " } else { "" }
293        ),
294    })
295}
296
297fn schema_literal_text(value: &Value) -> Result<String, SQLError> {
298    Ok(match value {
299        Value::Null => "NULL".into(),
300        Value::Void => "''::void".into(),
301        Value::Bool(value) => if *value { "true" } else { "false" }.into(),
302        Value::Int(value) => value.to_string(),
303        Value::Float(value) if value.is_finite() => value.to_string(),
304        Value::Float(value) => format!("'{value}'::double precision"),
305        Value::Str(value) | Value::FixedChar(value) => {
306            format!("'{}'", value.replace('\'', "''"))
307        }
308        Value::Bytes(value) => {
309            let mut hex = String::new();
310            for byte in value {
311                write!(&mut hex, "{byte:02x}").expect("writing to a String cannot fail");
312            }
313            format!("'\\x{hex}'::bytea")
314        }
315        Value::Temporal(value) => format!("'{value:?}'"),
316        Value::Decimal(value) => format!("{value:?}"),
317        Value::Json(value) => format!("'{}'::json", value.replace('\'', "''")),
318        Value::JsonB(value) => format!("'{}'::jsonb", value.replace('\'', "''")),
319        Value::Enum(value) => return Err(crate::expr::catalog_output_required(value)),
320        Value::Datum(value) => return schema_literal_text(&crate::expr::datums::read(value)?),
321        Value::LegacyVector(vector) => crate::render::legacy_vector_expression(vector)
322            .expect("stored SQL vector has SQL-produced bounds"),
323        Value::Array(array)
324            if array
325                .lower_bounds()
326                .iter()
327                .any(|lower_bound| *lower_bound != 1) =>
328        {
329            format!(
330                "'{}'",
331                crate::expr::array_value_to_string(array)?.replace('\'', "''")
332            )
333        }
334        Value::Array(array) => format!(
335            "ARRAY[{}]",
336            array
337                .elements()
338                .iter()
339                .map(schema_literal_text)
340                .collect::<Result<Vec<_>, SQLError>>()?
341                .join(", ")
342        ),
343        Value::List(values) => format!(
344            "ARRAY[{}]",
345            values
346                .iter()
347                .map(schema_literal_text)
348                .collect::<Result<Vec<_>, SQLError>>()?
349                .join(", ")
350        ),
351        Value::Row(values) => format!(
352            "ROW({})",
353            values
354                .iter()
355                .map(schema_literal_text)
356                .collect::<Result<Vec<_>, SQLError>>()?
357                .join(", ")
358        ),
359        Value::Record(fields) => format!(
360            "ROW({})",
361            fields
362                .iter()
363                .map(|(_, value)| schema_literal_text(value))
364                .collect::<Result<Vec<_>, SQLError>>()?
365                .join(", ")
366        ),
367        Value::Map(value) => format!(
368            "'{}'::jsonb",
369            serde_json::to_string(value)
370                .expect("serializing an in-memory Value map cannot fail")
371                .replace('\'', "''")
372        ),
373    })
374}