Skip to main content

uqa_sql/expr/
context.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Evaluation context, row lookup, and engine-backed type resolution.
8
9use std::borrow::Cow;
10
11use uqa_core::{ArrayValue, Value};
12
13use crate::ast::{ColumnType, InternalColumnRef};
14use crate::error::{Result, SQLError};
15use crate::params::SQLParam;
16use crate::result::ResultRow;
17
18use super::casting::{cast_value_from, parse_pg_array_literal};
19use super::conversion::{array_value_to_string, value_to_string};
20
21#[must_use]
22pub fn coercion_type_name(ty: &ColumnType) -> String {
23    match ty {
24        ColumnType::Domain { base, .. } => coercion_type_name(base),
25        ColumnType::Array(element) => format!("{}[]", coercion_type_name(element)),
26        _ => ty.sql_name(),
27    }
28}
29
30fn regrole_array_type(ty: &ColumnType) -> bool {
31    match ty {
32        ColumnType::Array(element) => {
33            matches!(element.as_ref(), ColumnType::Regrole) || regrole_array_type(element)
34        }
35        _ => false,
36    }
37}
38
39fn array_leaf_type(ty: &ColumnType) -> &ColumnType {
40    match ty {
41        ColumnType::Array(element) => array_leaf_type(element),
42        _ => ty,
43    }
44}
45
46fn cast_regrole_array_elements(
47    values: &[Value],
48    source_ty: Option<&str>,
49    engine: Option<&dyn EngineHook>,
50) -> Result<Vec<Value>> {
51    values
52        .iter()
53        .map(|value| match value {
54            Value::List(nested) => {
55                cast_regrole_array_elements(nested, source_ty, engine).map(Value::List)
56            }
57            value => cast_value_with_type_resolution(value, source_ty, "regrole", engine),
58        })
59        .collect()
60}
61
62fn cast_regrole_array(
63    value: &Value,
64    source_ty: Option<&ColumnType>,
65    engine: Option<&dyn EngineHook>,
66) -> Result<Value> {
67    let array = match value {
68        Value::Array(array) => array.clone(),
69        Value::Str(text) => parse_pg_array_literal(text)?,
70        other => {
71            return Err(SQLError::TypeMismatch(format!(
72                "CAST AS regrole[]: expected array, got {other:?}"
73            )));
74        }
75    };
76    let source_name = source_ty.map(array_leaf_type).map(ColumnType::sql_name);
77    let elements = cast_regrole_array_elements(array.elements(), source_name.as_deref(), engine)?;
78    ArrayValue::with_lower_bounds(elements, array.lower_bounds().to_vec())
79        .map(Value::Array)
80        .ok_or_else(|| SQLError::TypeMismatch("array dimensions changed during cast".into()))
81}
82
83/// Engine-side hook that scalar function evaluation calls for stateful
84/// sequence and user-defined functions. Query-valued expressions are not
85/// accepted here: lowering assigns them physical query-plan slots executed by
86/// `uqa-execution::ScalarSubqueryRunner`.
87pub trait EngineHook {
88    fn nextval(&self, name: &str) -> Result<i64>;
89    fn currval(&self, name: &str) -> Result<i64>;
90    fn lastval(&self) -> Result<i64> {
91        Err(SQLError::Unsupported(
92            "lastval requires an engine hook implementation".into(),
93        ))
94    }
95    fn setval(&self, name: &str, value: i64, is_called: bool) -> Result<i64>;
96
97    fn call_scalar_function(&self, _name: &str, _args: &[Value]) -> Option<Result<Value>> {
98        None
99    }
100
101    /// Invoke an engine-backed built-in after an exact catalog binding has
102    /// selected it. Unlike `call_scalar_function`, this path is also available
103    /// when dynamic dispatch is disabled, so runtime callbacks cannot override
104    /// the stored built-in identity.
105    fn call_bound_builtin_function(
106        &self,
107        _binding: &crate::ast::FunctionBinding,
108        _args: &[(Option<String>, Value)],
109    ) -> Option<Result<Value>> {
110        None
111    }
112
113    fn has_scalar_functions(&self) -> bool {
114        true
115    }
116
117    /// Resolve a catalog-owned SQL type name for casts evaluated with an engine context.
118    fn resolve_type_name(&self, _name: &str) -> std::result::Result<Option<ColumnType>, String> {
119        Ok(None)
120    }
121
122    /// Resolve a relation name to the OID carrier used by `regclass`.
123    fn resolve_regclass(&self, _name: &str) -> std::result::Result<Option<i64>, String> {
124        Ok(None)
125    }
126
127    /// Resolve `regclass` input while preserving typed SQL errors. Embedders that implement the historical string-error hook retain its previous behavior; engines with catalog privilege checks override this method directly.
128    fn resolve_regclass_input(&self, name: &str) -> Result<Option<i64>> {
129        self.resolve_regclass(name).map_err(SQLError::Internal)
130    }
131
132    /// Resolve an exact routine signature to the OID carrier used by `regprocedure`.
133    fn resolve_regprocedure(&self, _name: &str) -> std::result::Result<Option<i64>, String> {
134        Ok(None)
135    }
136
137    /// Resolve a `regrole` input while preserving hard input errors for direct casts.
138    fn resolve_regrole(&self, _name: &str) -> Result<Option<i64>> {
139        Ok(None)
140    }
141
142    /// Resolve a `regnamespace` input while preserving hard input errors for direct casts.
143    fn resolve_regnamespace(&self, name: &str) -> Result<Option<i64>> {
144        self.resolve_regobject(&ColumnType::Regnamespace, name)
145    }
146
147    /// Resolve the text argument of one `PostgreSQL` `to_reg*` lookup function. The engine override owns catalog visibility and the lookup function's NULL-versus-error boundary; the default preserves the two historical hooks for embedders that only implement `regclass` or `regprocedure`.
148    fn resolve_regobject(&self, ty: &ColumnType, name: &str) -> Result<Option<i64>> {
149        match ty {
150            ColumnType::Regclass => self.resolve_regclass_input(name),
151            ColumnType::Regprocedure => self.resolve_regprocedure(name).map_err(SQLError::Internal),
152            ColumnType::Regrole => self.resolve_regrole(name),
153            ColumnType::Regproc | ColumnType::Regnamespace | ColumnType::Regtype => Ok(None),
154            _ => Err(SQLError::Internal(format!(
155                "unsupported regobject lookup type `{}`",
156                ty.sql_name()
157            ))),
158        }
159    }
160
161    /// Resolve one OID-backed alias type to its `PostgreSQL` text output.
162    fn resolve_regtype_output(
163        &self,
164        _ty: &ColumnType,
165        _oid: i64,
166    ) -> std::result::Result<Option<String>, String> {
167        Ok(None)
168    }
169
170    /// Resolve the first existing schema on the logical session's search
171    /// path. `None` lets standalone expression evaluation use its `public`
172    /// compatibility default.
173    fn current_schema(&self) -> std::result::Result<Option<String>, String> {
174        Ok(None)
175    }
176
177    fn current_user(&self) -> std::result::Result<Option<String>, String> {
178        Ok(None)
179    }
180
181    fn session_user(&self) -> std::result::Result<Option<String>, String> {
182        Ok(None)
183    }
184
185    /// Resolve the existing schemas visible to the logical session.
186    fn current_schemas(
187        &self,
188        _include_implicit: bool,
189    ) -> std::result::Result<Option<Vec<String>>, String> {
190        Ok(None)
191    }
192
193    /// Draw from an engine-owned logical-session PRNG. `None` keeps pure,
194    /// engine-free expression evaluation available for library callers.
195    fn random_value(&self) -> std::result::Result<Option<f64>, String> {
196        Ok(None)
197    }
198
199    /// Draw every bit of one engine-owned logical-session PRNG word. Range
200    /// functions use this instead of a floating-point sample so `bigint` and
201    /// arbitrary-precision `numeric` bounds remain uniform.
202    fn random_u64(&self) -> std::result::Result<Option<u64>, String> {
203        Ok(None)
204    }
205
206    /// Reseed the logical-session PRNG. `false` means the hook does not own a
207    /// mutable random stream and the caller must report the unsupported call.
208    fn set_random_seed(&self, _seed: f64) -> std::result::Result<bool, String> {
209        Ok(false)
210    }
211
212    /// Invoke a user-defined SQL / `PL/pgSQL` function. Consulted
213    /// after built-in dispatch misses (and immediately for calls with
214    /// named arguments, which built-ins never accept). `None` means
215    /// no user-defined function with this name exists.
216    fn call_user_function(
217        &self,
218        _name: &str,
219        _args: &[(Option<String>, Value)],
220    ) -> Option<Result<Value>> {
221        None
222    }
223
224    fn call_bound_user_function(
225        &self,
226        _binding: &crate::ast::FunctionBinding,
227        _args: &[(Option<String>, Value)],
228    ) -> Option<Result<Value>> {
229        None
230    }
231}
232
233/// Format a scalar or array OID carrier using the catalog-aware output function of a `reg*` type. `None` means the declared type is not one of the supported alias types or the value is SQL NULL.
234pub fn format_regtype_value(
235    value: &Value,
236    ty: &ColumnType,
237    engine: Option<&dyn EngineHook>,
238) -> Result<Option<String>> {
239    if matches!(value, Value::Null) {
240        return Ok(None);
241    }
242    if let ColumnType::Array(element) = ty {
243        if !matches!(
244            element.as_ref(),
245            ColumnType::Regproc
246                | ColumnType::Regprocedure
247                | ColumnType::Regclass
248                | ColumnType::Regnamespace
249                | ColumnType::Regrole
250                | ColumnType::Regtype
251        ) {
252            return Ok(None);
253        }
254        let Value::Array(array) = value else {
255            return Ok(Some(value_to_string(value)));
256        };
257        let elements = format_regtype_array_elements(array.elements(), element, engine)?;
258        let formatted = array.with_elements(elements).ok_or_else(|| {
259            SQLError::Internal("regtype array output changed the array dimensions".into())
260        })?;
261        return Ok(Some(array_value_to_string(&formatted)));
262    }
263    if !matches!(
264        ty,
265        ColumnType::Regproc
266            | ColumnType::Regprocedure
267            | ColumnType::Regclass
268            | ColumnType::Regnamespace
269            | ColumnType::Regrole
270            | ColumnType::Regtype
271    ) {
272        return Ok(None);
273    }
274    let Value::Int(oid) = value else {
275        return Ok(Some(value_to_string(value)));
276    };
277    if *oid == 0 {
278        return Ok(Some("-".into()));
279    }
280    let resolved = engine
281        .map(|engine| engine.resolve_regtype_output(ty, *oid))
282        .transpose()
283        .map_err(SQLError::Internal)?
284        .flatten();
285    Ok(Some(resolved.unwrap_or_else(|| oid.to_string())))
286}
287
288fn format_regtype_array_elements(
289    values: &[Value],
290    element: &ColumnType,
291    engine: Option<&dyn EngineHook>,
292) -> Result<Vec<Value>> {
293    values
294        .iter()
295        .map(|value| match value {
296            Value::Null => Ok(Value::Null),
297            Value::List(nested) => {
298                format_regtype_array_elements(nested, element, engine).map(Value::List)
299            }
300            other => format_regtype_value(other, element, engine)
301                .map(|text| text.map_or_else(|| other.clone(), Value::Str)),
302        })
303        .collect()
304}
305
306/// Cast a value after resolving catalog-owned source and target types and flattening domains to their coercion types.
307pub fn cast_value_with_type_resolution(
308    value: &Value,
309    source_ty: Option<&str>,
310    target_ty: &str,
311    engine: Option<&dyn EngineHook>,
312) -> Result<Value> {
313    let resolved_source = match (engine, source_ty) {
314        (Some(engine), Some(source_ty)) => engine
315            .resolve_type_name(source_ty)
316            .map_err(SQLError::Internal)?
317            .map(|ty| coercion_type_name(&ty)),
318        _ => None,
319    };
320    let source_ty = resolved_source.as_deref().or(source_ty);
321    let resolved_target = engine
322        .map(|engine| engine.resolve_type_name(target_ty))
323        .transpose()
324        .map_err(SQLError::Internal)?
325        .flatten();
326    let target_ty = resolved_target.as_ref().map_or_else(
327        || Cow::Borrowed(target_ty),
328        |ty| Cow::Owned(coercion_type_name(ty)),
329    );
330    let target_column_type = resolved_target
331        .clone()
332        .or_else(|| ColumnType::from_sql_name(&target_ty).ok());
333    if target_column_type.as_ref().is_some_and(regrole_array_type) {
334        let source_column_type = source_ty.and_then(|name| ColumnType::from_sql_name(name).ok());
335        return cast_regrole_array(value, source_column_type.as_ref(), engine);
336    }
337    if target_ty.eq_ignore_ascii_case("text") {
338        if let Some(source_ty) = source_ty.and_then(|source| ColumnType::from_sql_name(source).ok())
339        {
340            if let Some(text) = format_regtype_value(value, &source_ty, engine)? {
341                return Ok(Value::Str(text));
342            }
343        }
344    }
345    if target_ty.eq_ignore_ascii_case("regclass") {
346        if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
347            return engine
348                .resolve_regclass_input(name)?
349                .map(Value::Int)
350                .ok_or_else(|| SQLError::Routine {
351                    sqlstate: "42P01".into(),
352                    message: format!("relation \"{name}\" does not exist"),
353                });
354        }
355    }
356    if target_ty.eq_ignore_ascii_case("regprocedure") {
357        if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
358            return engine
359                .resolve_regprocedure(name)
360                .map_err(SQLError::Internal)?
361                .map(Value::Int)
362                .ok_or_else(|| SQLError::Routine {
363                    sqlstate: "42883".into(),
364                    message: format!("function {name} does not exist"),
365                });
366        }
367    }
368    if target_ty.eq_ignore_ascii_case("regrole") {
369        if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
370            return engine
371                .resolve_regrole(name)?
372                .map(Value::Int)
373                .ok_or_else(|| SQLError::Routine {
374                    sqlstate: "42704".into(),
375                    message: format!("role \"{name}\" does not exist"),
376                });
377        }
378    }
379    if matches!(target_column_type.as_ref(), Some(ColumnType::Regnamespace)) {
380        if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
381            return engine
382                .resolve_regnamespace(name)?
383                .map(Value::Int)
384                .ok_or_else(|| SQLError::Routine {
385                    sqlstate: "3F000".into(),
386                    message: format!("schema \"{name}\" does not exist"),
387                });
388        }
389    }
390    cast_value_from(value, &target_ty, source_ty)
391}
392
393/// Read-only row interface used by the expression evaluator. Most callers
394/// use a materialised [`ResultRow`], while hot execution paths can expose a
395/// projected value slice without rebuilding a string-keyed map for every row.
396pub trait RowLookup {
397    fn column(&self, name: &str) -> Option<&Value>;
398
399    /// Whether an unqualified name identifies more than one visible input
400    /// column. Callers must report SQLSTATE 42702 instead of selecting an
401    /// arbitrary suffix match.
402    fn column_is_ambiguous(&self, _name: &str) -> bool {
403        false
404    }
405
406    fn qualified_column(&self, qualifier: &str, column: &str) -> Option<&Value>;
407
408    /// Whether a qualified identity names more than one visible input column.
409    fn qualified_column_is_ambiguous(&self, _qualifier: &str, _column: &str) -> bool {
410        false
411    }
412
413    /// Return a value by the physical schema position used to construct this
414    /// row view. Materialized named rows do not expose positional access;
415    /// projected execution sources override it so compiled hot paths can avoid
416    /// repeating string lookup for every expression and row.
417    fn positional_column(&self, _index: usize) -> Option<&Value> {
418        None
419    }
420
421    /// Resolve an executor-only relation attribute. Materialized SQL rows do
422    /// not expose these structural slots.
423    fn internal_column(&self, _column: InternalColumnRef) -> Option<&Value> {
424        None
425    }
426
427    /// Read the structurally carried retrieval score for one relation. The qualifier selects a score-bearing source without exposing an executor field in the SQL column namespace.
428    fn score_source(&self, _qualifier: Option<&str>) -> Option<&Value> {
429        None
430    }
431
432    /// Whether the requested score source resolves to more than one retrieval relation.
433    fn score_source_is_ambiguous(&self, _qualifier: Option<&str>) -> bool {
434        false
435    }
436
437    /// Visit every logical column in schema order. Named rows use their map
438    /// order; positional execution rows override this without materializing a
439    /// map. The default keeps narrow projected lookup implementations source
440    /// compatible when they deliberately do not expose whole-row semantics.
441    fn visit_columns(&self, _visitor: &mut dyn FnMut(&str, &Value)) {}
442}
443
444impl RowLookup for ResultRow {
445    fn column(&self, name: &str) -> Option<&Value> {
446        self.get(name)
447    }
448
449    fn qualified_column(&self, _qualifier: &str, _column: &str) -> Option<&Value> {
450        None
451    }
452
453    fn visit_columns(&self, visitor: &mut dyn FnMut(&str, &Value)) {
454        for (column, value) in self {
455            visitor(column, value);
456        }
457    }
458}
459
460pub struct EvalContext<'a> {
461    pub row: Option<&'a ResultRow>,
462    row_lookup: Option<&'a dyn RowLookup>,
463    pub params: &'a [SQLParam],
464    pub engine: Option<&'a dyn EngineHook>,
465}
466
467impl<'a> EvalContext<'a> {
468    pub fn new(row: Option<&'a ResultRow>, params: &'a [SQLParam]) -> Self {
469        Self {
470            row,
471            row_lookup: row.map(|row| row as &dyn RowLookup),
472            params,
473            engine: None,
474        }
475    }
476
477    pub fn from_row_lookup(row: &'a dyn RowLookup, params: &'a [SQLParam]) -> Self {
478        Self {
479            // Whole-row materialization is needed only by correlated
480            // subqueries. Ordinary scalar evaluation must remain on the
481            // lookup/slot path.
482            row: None,
483            row_lookup: Some(row),
484            params,
485            engine: None,
486        }
487    }
488
489    pub fn with_engine(mut self, engine: &'a dyn EngineHook) -> Self {
490        self.engine = Some(engine);
491        self
492    }
493
494    pub(super) fn row_lookup(&self) -> Result<&'a dyn RowLookup> {
495        self.row_lookup
496            .ok_or_else(|| SQLError::Internal("column reference without row context".into()))
497    }
498
499    /// Resolve an unqualified column through the same row semantics used by
500    /// the AST evaluator. Physical scalar IR evaluators call this instead of
501    /// reconstructing an [`Expr::Column`](crate::ast::Expr::Column) carrier.
502    pub fn column_value(&self, name: &str) -> Result<Value> {
503        if self.row_lookup()?.column_is_ambiguous(name) {
504            return Err(SQLError::AmbiguousColumn(name.to_string()));
505        }
506        Ok(self
507            .row_lookup()?
508            .column(name)
509            .cloned()
510            .unwrap_or(Value::Null))
511    }
512
513    /// Resolve a qualified column without constructing an AST expression.
514    pub fn qualified_column_value(&self, qualifier: &str, column: &str) -> Result<Value> {
515        if self
516            .row_lookup()?
517            .qualified_column_is_ambiguous(qualifier, column)
518        {
519            return Err(SQLError::AmbiguousColumn(format!("{qualifier}.{column}")));
520        }
521        Ok(self
522            .row_lookup()?
523            .qualified_column(qualifier, column)
524            .cloned()
525            .unwrap_or(Value::Null))
526    }
527}