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