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 constructs;
11mod operators;
12
13use super::{values, Field, Node};
14use crate::ast::{BinaryOp, Expr, FunctionBinding};
15use crate::catalog::type_metadata::{pg_type_collation_oid, pg_type_modifier, pg_type_oid};
16use crate::type_resolution::{
17    binary_operator_catalog_entry, binary_operator_types, common_context_expression_type,
18    FunctionTypeResolver,
19};
20use crate::{ColumnType, RowSchema, SQLError};
21
22pub struct RoutineIdentity {
23    pub oid: i64,
24    pub argument_types: Vec<ColumnType>,
25    pub result_type: ColumnType,
26}
27
28pub trait ExpressionRoutines {
29    fn resolve(
30        &self,
31        name: &str,
32        binding: Option<&FunctionBinding>,
33        argument_types: &[Option<ColumnType>],
34    ) -> Result<RoutineIdentity, SQLError>;
35}
36
37pub struct ExpressionContext<'a> {
38    pub schema: &'a RowSchema,
39    pub domain_value: Option<&'a ColumnType>,
40    pub types: Option<&'a dyn FunctionTypeResolver>,
41    pub routines: &'a dyn ExpressionRoutines,
42}
43
44struct TypedNode {
45    node: Node,
46    ty: ColumnType,
47}
48
49impl ExpressionContext<'_> {
50    pub fn check(&self, expression: &Expr) -> Result<Node, SQLError> {
51        self.encode(expression, Some(&ColumnType::Boolean))
52            .map(|value| value.node)
53    }
54
55    fn expression_type(&self, expression: &Expr) -> Result<Option<ColumnType>, SQLError> {
56        let plan = crate::plan::ExpressionPlan::lower(expression.clone());
57        common_context_expression_type(&plan.scalar, self.schema, &[], self.types)
58    }
59
60    fn encode(
61        &self,
62        expression: &Expr,
63        expected: Option<&ColumnType>,
64    ) -> Result<TypedNode, SQLError> {
65        let value = match expression {
66            Expr::Column(column) => self.column(column, None)?,
67            Expr::QualifiedColumn { qualifier, column } => self.column(column, Some(qualifier))?,
68            Expr::Literal(value) => self.literal(expression, value, expected)?,
69            Expr::TypedLiteral { value, ty } => {
70                let ty = self.resolve_type(ty)?;
71                TypedNode {
72                    node: values::constant(value, &ty)?,
73                    ty,
74                }
75            }
76            Expr::Binary { op, lhs, rhs } => self.binary(*op, lhs, rhs)?,
77            Expr::UnaryMinus(argument) => self.unary_minus(argument)?,
78            Expr::Array(elements) => self.array(elements, expected)?,
79            Expr::InList {
80                expr,
81                list,
82                negated,
83            } => self.in_list(expr, list, *negated)?,
84            Expr::Case {
85                base,
86                when,
87                else_branch,
88            } => self.case(expression, base.as_deref(), when, else_branch.as_deref())?,
89            Expr::And(items) => self.boolean("and", items)?,
90            Expr::Or(items) => self.boolean("or", items)?,
91            Expr::Not(item) => self.boolean("not", std::slice::from_ref(item.as_ref()))?,
92            Expr::IsNull { expr, negated } => {
93                let arg = self.encode(expr, None)?;
94                TypedNode {
95                    node: Node::new(
96                        "NULLTEST",
97                        [
98                            ("arg", arg.node.into()),
99                            ("nulltesttype", i64::from(*negated).into()),
100                            ("argisrow", false.into()),
101                            ("location", (-1).into()),
102                        ],
103                    ),
104                    ty: ColumnType::Boolean,
105                }
106            }
107            Expr::Between { expr, low, high } => self.boolean(
108                "and",
109                &[
110                    Expr::Binary {
111                        op: BinaryOp::GreaterEqual,
112                        lhs: expr.clone(),
113                        rhs: low.clone(),
114                    },
115                    Expr::Binary {
116                        op: BinaryOp::LessEqual,
117                        lhs: expr.clone(),
118                        rhs: high.clone(),
119                    },
120                ],
121            )?,
122            Expr::Func {
123                name,
124                binding,
125                args,
126                distinct: false,
127                order_by,
128                filter: None,
129            } if order_by.is_empty() => {
130                if let Some(value) = self.construct(expression, name, binding.as_ref(), args)? {
131                    value
132                } else {
133                    self.function(name, binding.as_ref(), args)?
134                }
135            }
136            Expr::Cast { expr, ty } => {
137                let ty = self.resolve_type(ty)?;
138                let unknown = self.expression_type(expr)?.is_none();
139                let mut input_type = &ty;
140                while let ColumnType::Domain { base, .. } = input_type {
141                    input_type = base;
142                }
143                let input_type = input_type.without_type_modifiers();
144                let inner = self.encode(expr, unknown.then_some(&input_type))?;
145                Self::coerce(inner, &ty, 1)?
146            }
147            _ => {
148                return Err(SQLError::Unsupported(
149                    "catalog expression node encoding for this expression".into(),
150                ))
151            }
152        };
153        if let Some(ty) = expected {
154            Self::coerce(value, ty, 2)
155        } else {
156            Ok(value)
157        }
158    }
159
160    fn literal(
161        &self,
162        expression: &Expr,
163        value: &uqa_core::Value,
164        expected: Option<&ColumnType>,
165    ) -> Result<TypedNode, SQLError> {
166        let source = self.expression_type(expression)?;
167        let ty = source
168            .as_ref()
169            .or(expected)
170            .cloned()
171            .unwrap_or(ColumnType::Text);
172        let value = crate::type_resolution::coerce_common_context_value(
173            value.clone(),
174            source.as_ref(),
175            Some(&ty),
176        )?;
177        Ok(TypedNode {
178            node: values::constant(&value, &ty)?,
179            ty,
180        })
181    }
182
183    fn resolve_type(&self, name: &str) -> Result<ColumnType, SQLError> {
184        if let Some(resolver) = self.types {
185            if let Some(ty) = resolver.resolve_type_name(name)? {
186                return Ok(ty);
187            }
188        }
189        ColumnType::from_sql_name(name)
190    }
191
192    fn column(&self, name: &str, qualifier: Option<&str>) -> Result<TypedNode, SQLError> {
193        if let Some(ty) = self.domain_value {
194            if name != "value" || qualifier.is_some() {
195                return Err(SQLError::UnknownColumn(name.into()));
196            }
197            return Ok(TypedNode {
198                node: Node::new(
199                    "COERCETODOMAINVALUE",
200                    [
201                        ("typeId", pg_type_oid(ty).into()),
202                        ("typeMod", pg_type_modifier(ty).into()),
203                        ("collation", pg_type_collation_oid(ty).into()),
204                        ("location", (-1).into()),
205                    ],
206                ),
207                ty: ty.clone(),
208            });
209        }
210        let position = qualifier
211            .map_or_else(
212                || self.schema.unqualified_position(name),
213                |qualifier| self.schema.qualified_position(qualifier, name),
214            )
215            .ok_or_else(|| SQLError::UnknownColumn(name.into()))?;
216        let ty = self
217            .schema
218            .column_type(position)
219            .ok_or_else(|| SQLError::Internal("catalog column has no declared type".into()))?;
220        let ordinal = i64::try_from(position + 1)
221            .map_err(|_| SQLError::Internal("column ordinal overflow".into()))?;
222        Ok(TypedNode {
223            node: Node::new(
224                "VAR",
225                [
226                    ("varno", 1.into()),
227                    ("varattno", ordinal.into()),
228                    ("vartype", pg_type_oid(ty).into()),
229                    ("vartypmod", pg_type_modifier(ty).into()),
230                    ("varcollid", pg_type_collation_oid(ty).into()),
231                    ("varnullingrels", Field::List(vec![Field::Atom("b".into())])),
232                    ("varlevelsup", 0.into()),
233                    ("varreturningtype", 0.into()),
234                    ("varnosyn", 1.into()),
235                    ("varattnosyn", ordinal.into()),
236                    ("location", (-1).into()),
237                ],
238            ),
239            ty: ty.clone(),
240        })
241    }
242
243    fn binary(&self, op: BinaryOp, lhs: &Expr, rhs: &Expr) -> Result<TypedNode, SQLError> {
244        let types = [self.expression_type(lhs)?, self.expression_type(rhs)?];
245        let [left, right, result] =
246            binary_operator_types(op, types[0].as_ref(), types[1].as_ref())?;
247        let identity = binary_operator_catalog_entry(op, [&left, &right])?;
248        let arguments = [
249            self.encode(lhs, Some(&left))?,
250            self.encode(rhs, Some(&right))?,
251        ];
252        Ok(operator_node(
253            identity.oid,
254            identity.function_oid,
255            arguments,
256            result,
257        ))
258    }
259
260    fn boolean(&self, operator: &str, args: &[Expr]) -> Result<TypedNode, SQLError> {
261        let args = args
262            .iter()
263            .map(|arg| {
264                self.encode(arg, Some(&ColumnType::Boolean))
265                    .map(|value| value.node.into())
266            })
267            .collect::<Result<_, _>>()?;
268        Ok(TypedNode {
269            node: Node::new(
270                "BOOLEXPR",
271                [
272                    ("boolop", Field::Atom(operator.into())),
273                    ("args", Field::List(args)),
274                    ("location", (-1).into()),
275                ],
276            ),
277            ty: ColumnType::Boolean,
278        })
279    }
280
281    fn function(
282        &self,
283        name: &str,
284        binding: Option<&FunctionBinding>,
285        arguments: &[Expr],
286    ) -> Result<TypedNode, SQLError> {
287        let types = arguments
288            .iter()
289            .map(|arg| self.expression_type(arg))
290            .collect::<Result<Vec<_>, _>>()?;
291        let routine = self.routines.resolve(name, binding, &types)?;
292        if routine.argument_types.len() != arguments.len() {
293            return Err(SQLError::Internal(
294                "catalog routine arity differs from bound arguments".into(),
295            ));
296        }
297        let arguments = arguments
298            .iter()
299            .zip(&routine.argument_types)
300            .map(|(arg, ty)| self.encode(arg, Some(ty)))
301            .collect::<Result<Vec<_>, _>>()?;
302        let collation = arguments
303            .iter()
304            .map(|argument| pg_type_collation_oid(&argument.ty))
305            .find(|oid| *oid != 0)
306            .unwrap_or(0);
307        Ok(TypedNode {
308            node: Node::new(
309                "FUNCEXPR",
310                [
311                    ("funcid", routine.oid.into()),
312                    ("funcresulttype", pg_type_oid(&routine.result_type).into()),
313                    ("funcretset", false.into()),
314                    ("funcvariadic", false.into()),
315                    ("funcformat", 0.into()),
316                    (
317                        "funccollid",
318                        pg_type_collation_oid(&routine.result_type).into(),
319                    ),
320                    ("inputcollid", collation.into()),
321                    (
322                        "args",
323                        Field::List(arguments.into_iter().map(|arg| arg.node.into()).collect()),
324                    ),
325                    ("location", (-1).into()),
326                ],
327            ),
328            ty: routine.result_type,
329        })
330    }
331}
332
333fn operator_node(
334    oid: i64,
335    function_oid: i64,
336    arguments: impl IntoIterator<Item = TypedNode>,
337    result: ColumnType,
338) -> TypedNode {
339    let arguments: Vec<_> = arguments.into_iter().collect();
340    let input_collation = arguments
341        .iter()
342        .map(|argument| pg_type_collation_oid(&argument.ty))
343        .find(|oid| *oid != 0)
344        .unwrap_or(0);
345    TypedNode {
346        node: Node::new(
347            "OPEXPR",
348            [
349                ("opno", oid.into()),
350                ("opfuncid", function_oid.into()),
351                ("opresulttype", pg_type_oid(&result).into()),
352                ("opretset", false.into()),
353                ("opcollid", pg_type_collation_oid(&result).into()),
354                ("inputcollid", input_collation.into()),
355                (
356                    "args",
357                    Field::List(arguments.into_iter().map(|arg| arg.node.into()).collect()),
358                ),
359                ("location", (-1).into()),
360            ],
361        ),
362        ty: result,
363    }
364}