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