Skip to main content

uqa_sql/catalog/node_tree/
expressions.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Bind catalog expression nodes to declared column and domain-value types.
8
9mod coercion;
10mod collations;
11mod constructs;
12mod fields;
13mod functions;
14mod operators;
15mod routines;
16mod rows;
17mod schema;
18mod subscripts;
19pub use routines::{select_routine, RoutineDeclaration};
20pub use schema::{ColumnReference, ExpressionSchema};
21
22use super::{values, Field, Node};
23use crate::ast::{BinaryOp, Expr, FunctionBinding, RoutineInvocationBinding};
24use crate::catalog::type_metadata::{pg_type_collation_oid, pg_type_modifier, pg_type_oid};
25use crate::type_resolution::{
26    binary_operator_catalog_entry, binary_operator_types, common_context_expression_type,
27    FunctionTypeResolver, RoutineCallDescriptor,
28};
29use crate::{ColumnType, SQLError};
30
31pub struct RoutineIdentity {
32    pub oid: i64,
33    pub invocation: RoutineInvocationBinding,
34    pub result_type: ColumnType,
35}
36
37pub trait ExpressionRoutines {
38    fn resolve(
39        &self,
40        name: &str,
41        binding: Option<&FunctionBinding>,
42        call: RoutineCallDescriptor<'_>,
43    ) -> Result<RoutineIdentity, SQLError>;
44}
45
46pub struct ExpressionContext<'a> {
47    pub schema: &'a dyn ExpressionSchema,
48    pub domain_value: Option<&'a ColumnType>,
49    pub types: Option<&'a dyn FunctionTypeResolver>,
50    pub routines: &'a dyn ExpressionRoutines,
51}
52
53struct TypedNode {
54    node: Node,
55    /// `None` retains the non-literal `PostgreSQL` unknown result identity until a context selects a cast.
56    ty: Option<ColumnType>,
57}
58
59impl TypedNode {
60    fn type_ref(&self) -> Result<&ColumnType, SQLError> {
61        self.ty
62            .as_ref()
63            .ok_or_else(|| super::invalid("expected a selected expression type"))
64    }
65}
66
67impl ExpressionContext<'_> {
68    pub fn check(&self, expression: &Expr) -> Result<Node, SQLError> {
69        self.expression(expression, Some(&ColumnType::Boolean))
70    }
71
72    /// Encode a retained expression with the type selected by its declaration, if any.
73    pub fn expression(
74        &self,
75        expression: &Expr,
76        expected: Option<&ColumnType>,
77    ) -> Result<Node, SQLError> {
78        let mut value = self.encode(expression, expected)?;
79        collations::assign(&mut value.node)?;
80        Ok(value.node)
81    }
82
83    /// Defaults use assignment coercion, whose cast set is broader than implicit expression coercion.
84    pub fn assignment(&self, expression: &Expr, target: &ColumnType) -> Result<Node, SQLError> {
85        let unknown = self.expression_type(expression)?.is_none();
86        let input_type = target.without_type_modifiers();
87        let value = self.encode(expression, unknown.then_some(&input_type))?;
88        let mut value = Self::coerce_assignment(value, target)?;
89        collations::assign(&mut value.node)?;
90        Ok(value.node)
91    }
92
93    fn expression_type(&self, expression: &Expr) -> Result<Option<ColumnType>, SQLError> {
94        let plan = crate::plan::ExpressionPlan::lower(expression.clone());
95        common_context_expression_type(&plan.scalar, self.schema, &[], self.types)
96    }
97
98    fn encode(
99        &self,
100        expression: &Expr,
101        expected: Option<&ColumnType>,
102    ) -> Result<TypedNode, SQLError> {
103        if let Some(clause) = crate::catalog::collations::expression::ast_clause(expression)? {
104            return self.collated_expression(clause, expected);
105        }
106        let value = match expression {
107            Expr::Column(column) => self.column(column, None)?,
108            Expr::QualifiedColumn { qualifier, column } => self.column(column, Some(qualifier))?,
109            Expr::QualifiedStar(qualifier) => {
110                Self::variable(self.schema.whole_row_reference(qualifier)?)
111            }
112            Expr::Literal(value) => self.literal(expression, value, expected)?,
113            Expr::TypedLiteral {
114                value,
115                ty,
116                composite_source,
117                collation_oid,
118            } => self.retained_constant(
119                value,
120                self.resolve_type(ty)?,
121                composite_source.as_deref(),
122                *collation_oid,
123            )?,
124            Expr::Binary { op, lhs, rhs } => self.binary(*op, lhs, rhs)?,
125            Expr::UnaryMinus(argument) => self.unary_minus(argument)?,
126            Expr::Array(elements) => self.array(elements, expected)?,
127            Expr::Row(elements) => self.row(elements)?,
128            Expr::CompositeRow { items, binding } => self.stored_row(items, binding)?,
129            Expr::InList {
130                expr,
131                list,
132                negated,
133            } => self.in_list(expr, list, *negated)?,
134            Expr::Case {
135                base,
136                when,
137                else_branch,
138            } => self.case(expression, base.as_deref(), when, else_branch.as_deref())?,
139            Expr::And(items) => self.boolean("and", items)?,
140            Expr::Or(items) => self.boolean("or", items)?,
141            Expr::Not(item) => self.boolean("not", std::slice::from_ref(item.as_ref()))?,
142            Expr::IsNull { expr, negated } => {
143                let (arg, is_row) = self.uncoerced_input(expr)?;
144                TypedNode {
145                    node: Node::new(
146                        "NULLTEST",
147                        [
148                            ("arg", arg.into()),
149                            ("nulltesttype", i64::from(*negated).into()),
150                            ("argisrow", is_row.into()),
151                            ("location", (-1).into()),
152                        ],
153                    ),
154                    ty: Some(ColumnType::Boolean),
155                }
156            }
157            Expr::Between { expr, low, high } => self.boolean(
158                "and",
159                &[
160                    Expr::Binary {
161                        op: BinaryOp::GreaterEqual,
162                        lhs: expr.clone(),
163                        rhs: low.clone(),
164                    },
165                    Expr::Binary {
166                        op: BinaryOp::LessEqual,
167                        lhs: expr.clone(),
168                        rhs: high.clone(),
169                    },
170                ],
171            )?,
172            Expr::Func {
173                order_syntax,
174                name,
175                binding,
176                args,
177                distinct: false,
178                order_by,
179                filter: None,
180            } if order_by.is_empty() => {
181                if let Some(value) = self.construct(expression, name, binding.as_ref(), args)? {
182                    value
183                } else {
184                    self.function(name, binding.as_ref(), args, *order_syntax)?
185                }
186            }
187            Expr::Cast { expr, ty, implicit } => self.cast_expression(expr, ty, *implicit)?,
188            _ => {
189                return Err(SQLError::Unsupported(
190                    "catalog expression node encoding for this expression".into(),
191                ))
192            }
193        };
194        if let Some(ty) = expected {
195            Self::coerce(value, ty, 2)
196        } else {
197            Ok(value)
198        }
199    }
200
201    fn literal(
202        &self,
203        expression: &Expr,
204        value: &uqa_core::Value,
205        expected: Option<&ColumnType>,
206    ) -> Result<TypedNode, SQLError> {
207        let source = self.expression_type(expression)?;
208        let ty = source
209            .as_ref()
210            .or(expected)
211            .cloned()
212            .unwrap_or(ColumnType::Text);
213        let value = if source.is_none() {
214            if let Some(value) = crate::expr::enums::fold_unknown_literal(
215                self.types.and_then(FunctionTypeResolver::enum_labels),
216                value,
217                &ty,
218            )? {
219                value
220            } else if let uqa_core::Value::Str(text) = value {
221                if crate::expr::requires_catalog_constant_input(&ty) {
222                    self.types
223                        .and_then(FunctionTypeResolver::catalog_input_functions)
224                        .ok_or_else(|| {
225                            SQLError::Internal("catalog input functions are unavailable".into())
226                        })?
227                        .read_unknown_input(text, &ty)?
228                } else {
229                    crate::type_resolution::coerce_common_context_value(
230                        value.clone(),
231                        None,
232                        Some(&ty),
233                    )?
234                }
235            } else {
236                crate::type_resolution::coerce_common_context_value(value.clone(), None, Some(&ty))?
237            }
238        } else {
239            crate::type_resolution::coerce_common_context_value(
240                value.clone(),
241                source.as_ref(),
242                Some(&ty),
243            )?
244        };
245        self.constant(&value, ty, None)
246    }
247
248    fn constant(
249        &self,
250        value: &uqa_core::Value,
251        ty: ColumnType,
252        source: Option<&crate::expr::composites::CompositeConstantSource>,
253    ) -> Result<TypedNode, SQLError> {
254        Ok(TypedNode {
255            node: values::constant_with_source(
256                value,
257                &ty,
258                source,
259                self.types.and_then(FunctionTypeResolver::composite_types),
260            )?,
261            ty: Some(ty),
262        })
263    }
264
265    fn retained_constant(
266        &self,
267        value: &uqa_core::Value,
268        ty: ColumnType,
269        source: Option<&crate::expr::composites::CompositeConstantSource>,
270        collation: Option<u32>,
271    ) -> Result<TypedNode, SQLError> {
272        let mut value = self.constant(value, ty, source)?;
273        if let Some(oid) = collation {
274            crate::catalog::collations::expression::validate_literal_collation(
275                value.ty.as_ref(),
276                Some(oid),
277            )?;
278            value
279                .node
280                .fields
281                .iter_mut()
282                .find(|(name, _)| name == "constcollid")
283                .expect("constant collation")
284                .1 = i64::from(oid).into();
285        }
286        Ok(value)
287    }
288
289    fn resolve_type(&self, name: &str) -> Result<ColumnType, SQLError> {
290        if let Some(resolver) = self.types {
291            if let Some(ty) = resolver.resolve_type_name(name)? {
292                return Ok(ty);
293            }
294        }
295        ColumnType::from_sql_name(name)
296    }
297
298    fn column(&self, name: &str, qualifier: Option<&str>) -> Result<TypedNode, SQLError> {
299        if let Some(ty) = self.domain_value {
300            if name != "value" || qualifier.is_some() {
301                return Err(SQLError::UnknownColumn(name.into()));
302            }
303            return Ok(TypedNode {
304                node: Node::new(
305                    "COERCETODOMAINVALUE",
306                    [
307                        ("typeId", pg_type_oid(ty).into()),
308                        ("typeMod", pg_type_modifier(ty).into()),
309                        ("collation", pg_type_collation_oid(ty).into()),
310                        ("location", (-1).into()),
311                    ],
312                ),
313                ty: Some(ty.clone()),
314            });
315        }
316        let column = self.schema.column_reference(name, qualifier)?;
317        Ok(Self::variable(column))
318    }
319
320    fn variable(column: ColumnReference<'_>) -> TypedNode {
321        let ty = column.ty;
322        let ordinal = column.attribute_number;
323        TypedNode {
324            node: Node::new(
325                "VAR",
326                [
327                    ("varno", 1.into()),
328                    ("varattno", ordinal.into()),
329                    ("vartype", pg_type_oid(ty).into()),
330                    ("vartypmod", pg_type_modifier(ty).into()),
331                    ("varcollid", column.collation_oid.into()),
332                    ("varnullingrels", Field::List(vec![Field::Atom("b".into())])),
333                    ("varlevelsup", 0.into()),
334                    ("varreturningtype", 0.into()),
335                    ("varnosyn", 1.into()),
336                    ("varattnosyn", ordinal.into()),
337                    ("location", (-1).into()),
338                ],
339            ),
340            ty: Some(ty.clone()),
341        }
342    }
343
344    fn binary(&self, op: BinaryOp, lhs: &Expr, rhs: &Expr) -> Result<TypedNode, SQLError> {
345        if let (Expr::Row(left), Expr::Row(right)) = (lhs, rhs) {
346            return self.row_comparison(op, left, right);
347        }
348        self.scalar_binary(op, lhs, rhs)
349    }
350
351    fn scalar_binary(&self, op: BinaryOp, lhs: &Expr, rhs: &Expr) -> Result<TypedNode, SQLError> {
352        let types = [self.expression_type(lhs)?, self.expression_type(rhs)?];
353        let [left, right, result] =
354            binary_operator_types(op, types[0].as_ref(), types[1].as_ref())?;
355        let identity = binary_operator_catalog_entry(op, [&left, &right])?;
356        let arguments = [
357            self.encode(lhs, Some(&left))?,
358            self.encode(rhs, Some(&right))?,
359        ];
360        Ok(operator_node(
361            identity.oid,
362            identity.function_oid,
363            arguments,
364            result,
365        ))
366    }
367
368    fn boolean(&self, operator: &str, args: &[Expr]) -> Result<TypedNode, SQLError> {
369        let args = args
370            .iter()
371            .map(|arg| {
372                self.encode(arg, Some(&ColumnType::Boolean))
373                    .map(|value| value.node.into())
374            })
375            .collect::<Result<_, _>>()?;
376        Ok(TypedNode {
377            node: Node::new(
378                "BOOLEXPR",
379                [
380                    ("boolop", Field::Atom(operator.into())),
381                    ("args", Field::List(args)),
382                    ("location", (-1).into()),
383                ],
384            ),
385            ty: Some(ColumnType::Boolean),
386        })
387    }
388}
389
390fn operator_node(
391    oid: i64,
392    function_oid: i64,
393    arguments: impl IntoIterator<Item = TypedNode>,
394    result: ColumnType,
395) -> TypedNode {
396    let arguments: Vec<_> = arguments.into_iter().collect();
397    TypedNode {
398        node: Node::new(
399            "OPEXPR",
400            [
401                ("opno", oid.into()),
402                ("opfuncid", function_oid.into()),
403                ("opresulttype", pg_type_oid(&result).into()),
404                ("opretset", false.into()),
405                ("opcollid", pg_type_collation_oid(&result).into()),
406                ("inputcollid", 0.into()),
407                (
408                    "args",
409                    Field::List(arguments.into_iter().map(|arg| arg.node.into()).collect()),
410                ),
411                ("location", (-1).into()),
412            ],
413        ),
414        ty: Some(result),
415    }
416}