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            args,
40            distinct,
41            order_by,
42            filter,
43            ..
44        } => {
45            let mut rendered_args = args
46                .iter()
47                .map(schema_expr_text)
48                .collect::<Vec<_>>()
49                .join(", ");
50            if *distinct {
51                rendered_args = format!("DISTINCT {rendered_args}");
52            }
53            if !order_by.is_empty() {
54                let order = order_by
55                    .iter()
56                    .map(|order| {
57                        let direction = if order.descending { " DESC" } else { "" };
58                        let nulls = match order.nulls {
59                            Some(crate::ast::NullsOrder::First) => " NULLS FIRST",
60                            Some(crate::ast::NullsOrder::Last) => " NULLS LAST",
61                            None => "",
62                        };
63                        format!("{}{direction}{nulls}", schema_expr_text(&order.expr))
64                    })
65                    .collect::<Vec<_>>()
66                    .join(", ");
67                if !rendered_args.is_empty() {
68                    rendered_args.push(' ');
69                }
70                rendered_args.push_str("ORDER BY ");
71                rendered_args.push_str(&order);
72            }
73            let mut rendered = format!("{name}({rendered_args})");
74            if let Some(filter) = filter {
75                write!(
76                    &mut rendered,
77                    " FILTER (WHERE {})",
78                    schema_expr_text(filter)
79                )
80                .expect("writing to a String cannot fail");
81            }
82            rendered
83        }
84        Expr::Array(items) => format!(
85            "ARRAY[{}]",
86            items
87                .iter()
88                .map(schema_expr_text)
89                .collect::<Vec<_>>()
90                .join(", ")
91        ),
92        Expr::Row(items) => format!(
93            "ROW({})",
94            items
95                .iter()
96                .map(schema_expr_text)
97                .collect::<Vec<_>>()
98                .join(", ")
99        ),
100        Expr::Binary { op, lhs, rhs } => format!(
101            "({} {} {})",
102            schema_expr_text(lhs),
103            match op {
104                crate::ast::BinaryOp::Equal => "=",
105                crate::ast::BinaryOp::NotEqual => "<>",
106                crate::ast::BinaryOp::Less => "<",
107                crate::ast::BinaryOp::LessEqual => "<=",
108                crate::ast::BinaryOp::Greater => ">",
109                crate::ast::BinaryOp::GreaterEqual => ">=",
110                crate::ast::BinaryOp::Add => "+",
111                crate::ast::BinaryOp::Subtract => "-",
112                crate::ast::BinaryOp::Multiply => "*",
113                crate::ast::BinaryOp::Divide => "/",
114            },
115            schema_expr_text(rhs)
116        ),
117        Expr::Not(inner) => format!("(NOT {})", schema_expr_text(inner)),
118        Expr::UnaryMinus(inner) => format!("(-{})", schema_expr_text(inner)),
119        Expr::And(items) => format!(
120            "({})",
121            items
122                .iter()
123                .map(schema_expr_text)
124                .collect::<Vec<_>>()
125                .join(" AND ")
126        ),
127        Expr::Or(items) => format!(
128            "({})",
129            items
130                .iter()
131                .map(schema_expr_text)
132                .collect::<Vec<_>>()
133                .join(" OR ")
134        ),
135        Expr::IsNull { expr, negated } => format!(
136            "({} IS {}NULL)",
137            schema_expr_text(expr),
138            if *negated { "NOT " } else { "" }
139        ),
140        Expr::Between { expr, low, high } => format!(
141            "({} BETWEEN {} AND {})",
142            schema_expr_text(expr),
143            schema_expr_text(low),
144            schema_expr_text(high)
145        ),
146        Expr::InList {
147            expr,
148            list,
149            negated,
150        } => format!(
151            "({} {}IN ({}))",
152            schema_expr_text(expr),
153            if *negated { "NOT " } else { "" },
154            list.iter()
155                .map(schema_expr_text)
156                .collect::<Vec<_>>()
157                .join(", ")
158        ),
159        Expr::WindowCall { name, args, .. } => format!(
160            "{}({}) OVER (...)",
161            name,
162            args.iter()
163                .map(schema_expr_text)
164                .collect::<Vec<_>>()
165                .join(", ")
166        ),
167        Expr::Case {
168            base,
169            when,
170            else_branch,
171        } => {
172            let mut rendered = "CASE".to_string();
173            if let Some(base) = base {
174                rendered.push(' ');
175                rendered.push_str(&schema_expr_text(base));
176            }
177            for (condition, result) in when {
178                write!(
179                    &mut rendered,
180                    " WHEN {} THEN {}",
181                    schema_expr_text(condition),
182                    schema_expr_text(result)
183                )
184                .expect("writing to a String cannot fail");
185            }
186            if let Some(else_branch) = else_branch {
187                write!(&mut rendered, " ELSE {}", schema_expr_text(else_branch))
188                    .expect("writing to a String cannot fail");
189            }
190            rendered.push_str(" END");
191            rendered
192        }
193        Expr::Cast { expr, ty } => format!("({})::{ty}", schema_expr_text(expr)),
194        Expr::ScalarSubquery(body) => format!("({body:?})"),
195        Expr::Exists { body, negated } => {
196            format!("{}EXISTS ({body:?})", if *negated { "NOT " } else { "" })
197        }
198        Expr::InSubquery {
199            expr,
200            body,
201            negated,
202        } => format!(
203            "({} {}IN ({body:?}))",
204            schema_expr_text(expr),
205            if *negated { "NOT " } else { "" }
206        ),
207    }
208}
209
210fn schema_literal_text(value: &Value) -> String {
211    match value {
212        Value::Null => "NULL".into(),
213        Value::Void => "''::void".into(),
214        Value::Bool(value) => if *value { "true" } else { "false" }.into(),
215        Value::Int(value) => value.to_string(),
216        Value::Float(value) if value.is_finite() => value.to_string(),
217        Value::Float(value) => format!("'{value}'::double precision"),
218        Value::Str(value) | Value::FixedChar(value) => {
219            format!("'{}'", value.replace('\'', "''"))
220        }
221        Value::Bytes(value) => {
222            let mut hex = String::new();
223            for byte in value {
224                write!(&mut hex, "{byte:02x}").expect("writing to a String cannot fail");
225            }
226            format!("'\\x{hex}'::bytea")
227        }
228        Value::Temporal(value) => format!("'{value:?}'"),
229        Value::Decimal(value) => format!("{value:?}"),
230        Value::Json(value) => format!("'{}'::json", value.replace('\'', "''")),
231        Value::JsonB(value) => format!("'{}'::jsonb", value.replace('\'', "''")),
232        Value::Array(array)
233            if array
234                .lower_bounds()
235                .iter()
236                .any(|lower_bound| *lower_bound != 1) =>
237        {
238            format!(
239                "'{}'",
240                crate::expr::array_value_to_string(array).replace('\'', "''")
241            )
242        }
243        Value::Array(array) => format!(
244            "ARRAY[{}]",
245            array
246                .elements()
247                .iter()
248                .map(schema_literal_text)
249                .collect::<Vec<_>>()
250                .join(", ")
251        ),
252        Value::List(values) => format!(
253            "ARRAY[{}]",
254            values
255                .iter()
256                .map(schema_literal_text)
257                .collect::<Vec<_>>()
258                .join(", ")
259        ),
260        Value::Row(values) => format!(
261            "ROW({})",
262            values
263                .iter()
264                .map(schema_literal_text)
265                .collect::<Vec<_>>()
266                .join(", ")
267        ),
268        Value::Record(fields) => format!(
269            "ROW({})",
270            fields
271                .iter()
272                .map(|(_, value)| schema_literal_text(value))
273                .collect::<Vec<_>>()
274                .join(", ")
275        ),
276        Value::Map(value) => format!(
277            "'{}'::jsonb",
278            serde_json::to_string(value)
279                .expect("serializing an in-memory Value map cannot fail")
280                .replace('\'', "''")
281        ),
282    }
283}