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    /// Start of the current SQL transaction, in Unix microseconds.
89    fn transaction_timestamp_micros(&self) -> Option<i64> {
90        None
91    }
92
93    /// Start of the current frontend SQL message, in Unix microseconds.
94    fn statement_timestamp_micros(&self) -> Option<i64> {
95        None
96    }
97
98    fn nextval(&self, name: &str) -> Result<i64>;
99    fn currval(&self, name: &str) -> Result<i64>;
100    fn lastval(&self) -> Result<i64> {
101        Err(SQLError::Unsupported(
102            "lastval requires an engine hook implementation".into(),
103        ))
104    }
105    fn setval(&self, name: &str, value: i64, is_called: bool) -> Result<i64>;
106
107    fn call_scalar_function(&self, _name: &str, _args: &[Value]) -> Option<Result<Value>> {
108        None
109    }
110
111    /// Invoke an engine-backed built-in after an exact catalog binding has
112    /// selected it. Unlike `call_scalar_function`, this path is also available
113    /// when dynamic dispatch is disabled, so runtime callbacks cannot override
114    /// the stored built-in identity.
115    fn call_bound_builtin_function(
116        &self,
117        _binding: &crate::ast::FunctionBinding,
118        _args: &[(Option<String>, Value)],
119    ) -> Option<Result<Value>> {
120        None
121    }
122
123    fn has_scalar_functions(&self) -> bool {
124        true
125    }
126
127    /// Resolve a catalog-owned SQL type name for casts evaluated with an engine context.
128    fn resolve_type_name(&self, _name: &str) -> std::result::Result<Option<ColumnType>, String> {
129        Ok(None)
130    }
131
132    /// Apply catalog-owned domain conversion and constraints. A missing implementation leaves built-in catalog domains on their base-type conversion path.
133    fn cast_domain(
134        &self,
135        _value: &Value,
136        _source: Option<&str>,
137        _target: &ColumnType,
138    ) -> Result<Option<Value>> {
139        Ok(None)
140    }
141
142    /// Resolve a regtype cast to its OID carrier when a complete type catalog is available.
143    fn resolve_regtype_input(&self, _name: &str) -> Result<Option<i64>> {
144        Ok(None)
145    }
146
147    /// Resolve a relation name to the OID carrier used by `regclass`.
148    fn resolve_regclass(&self, _name: &str) -> std::result::Result<Option<i64>, String> {
149        Ok(None)
150    }
151
152    /// 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.
153    fn resolve_regclass_input(&self, name: &str) -> Result<Option<i64>> {
154        self.resolve_regclass(name).map_err(SQLError::Internal)
155    }
156
157    /// Resolve an exact routine signature to the OID carrier used by `regprocedure`.
158    fn resolve_regprocedure(&self, _name: &str) -> std::result::Result<Option<i64>, String> {
159        Ok(None)
160    }
161
162    /// Resolve a `regrole` input while preserving hard input errors for direct casts.
163    fn resolve_regrole(&self, _name: &str) -> Result<Option<i64>> {
164        Ok(None)
165    }
166
167    /// Resolve a `regnamespace` input while preserving hard input errors for direct casts.
168    fn resolve_regnamespace(&self, name: &str) -> Result<Option<i64>> {
169        self.resolve_regobject(&ColumnType::Regnamespace, name)
170    }
171
172    /// 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`.
173    fn resolve_regobject(&self, ty: &ColumnType, name: &str) -> Result<Option<i64>> {
174        match ty {
175            ColumnType::Regclass => self.resolve_regclass_input(name),
176            ColumnType::Regprocedure => self.resolve_regprocedure(name).map_err(SQLError::Internal),
177            ColumnType::Regrole => self.resolve_regrole(name),
178            ColumnType::Regproc | ColumnType::Regnamespace | ColumnType::Regtype => Ok(None),
179            _ => Err(SQLError::Internal(format!(
180                "unsupported regobject lookup type `{}`",
181                ty.sql_name()
182            ))),
183        }
184    }
185
186    /// Resolve one OID-backed alias type to its `PostgreSQL` text output.
187    fn resolve_regtype_output(
188        &self,
189        _ty: &ColumnType,
190        _oid: i64,
191    ) -> std::result::Result<Option<String>, String> {
192        Ok(None)
193    }
194
195    /// Resolve the first existing schema on the logical session's search
196    /// path. `None` lets standalone expression evaluation use its `public`
197    /// compatibility default.
198    fn current_schema(&self) -> std::result::Result<Option<String>, String> {
199        Ok(None)
200    }
201
202    fn current_user(&self) -> std::result::Result<Option<String>, String> {
203        Ok(None)
204    }
205
206    fn session_user(&self) -> std::result::Result<Option<String>, String> {
207        Ok(None)
208    }
209
210    /// Resolve the existing schemas visible to the logical session.
211    fn current_schemas(
212        &self,
213        _include_implicit: bool,
214    ) -> std::result::Result<Option<Vec<String>>, String> {
215        Ok(None)
216    }
217
218    /// Draw from an engine-owned logical-session PRNG. `None` keeps pure,
219    /// engine-free expression evaluation available for library callers.
220    fn random_value(&self) -> std::result::Result<Option<f64>, String> {
221        Ok(None)
222    }
223
224    /// Draw every bit of one engine-owned logical-session PRNG word. Range
225    /// functions use this instead of a floating-point sample so `bigint` and
226    /// arbitrary-precision `numeric` bounds remain uniform.
227    fn random_u64(&self) -> std::result::Result<Option<u64>, String> {
228        Ok(None)
229    }
230
231    /// Reseed the logical-session PRNG. `false` means the hook does not own a
232    /// mutable random stream and the caller must report the unsupported call.
233    fn set_random_seed(&self, _seed: f64) -> std::result::Result<bool, String> {
234        Ok(false)
235    }
236
237    /// Invoke a user-defined SQL / `PL/pgSQL` function. Consulted
238    /// after built-in dispatch misses (and immediately for calls with
239    /// named arguments, which built-ins never accept). `None` means
240    /// no user-defined function with this name exists.
241    fn call_user_function(
242        &self,
243        _name: &str,
244        _args: &[(Option<String>, Value)],
245    ) -> Option<Result<Value>> {
246        None
247    }
248
249    fn call_bound_user_function(
250        &self,
251        _binding: &crate::ast::FunctionBinding,
252        _args: &[(Option<String>, Value)],
253    ) -> Option<Result<Value>> {
254        None
255    }
256}
257
258/// 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.
259pub fn format_regtype_value(
260    value: &Value,
261    ty: &ColumnType,
262    engine: Option<&dyn EngineHook>,
263) -> Result<Option<String>> {
264    if matches!(value, Value::Null) {
265        return Ok(None);
266    }
267    if let ColumnType::Array(element) = ty {
268        if !matches!(
269            element.as_ref(),
270            ColumnType::Regproc
271                | ColumnType::Regprocedure
272                | ColumnType::Regclass
273                | ColumnType::Regnamespace
274                | ColumnType::Regrole
275                | ColumnType::Regtype
276        ) {
277            return Ok(None);
278        }
279        let Value::Array(array) = value else {
280            return Ok(Some(value_to_string(value)));
281        };
282        let elements = format_regtype_array_elements(array.elements(), element, engine)?;
283        let formatted = array.with_elements(elements).ok_or_else(|| {
284            SQLError::Internal("regtype array output changed the array dimensions".into())
285        })?;
286        return Ok(Some(array_value_to_string(&formatted)));
287    }
288    if !matches!(
289        ty,
290        ColumnType::Regproc
291            | ColumnType::Regprocedure
292            | ColumnType::Regclass
293            | ColumnType::Regnamespace
294            | ColumnType::Regrole
295            | ColumnType::Regtype
296    ) {
297        return Ok(None);
298    }
299    let Value::Int(oid) = value else {
300        return Ok(Some(value_to_string(value)));
301    };
302    if *oid == 0 {
303        return Ok(Some("-".into()));
304    }
305    let resolved = engine
306        .map(|engine| engine.resolve_regtype_output(ty, *oid))
307        .transpose()
308        .map_err(SQLError::Internal)?
309        .flatten();
310    Ok(Some(resolved.unwrap_or_else(|| oid.to_string())))
311}
312
313fn format_regtype_array_elements(
314    values: &[Value],
315    element: &ColumnType,
316    engine: Option<&dyn EngineHook>,
317) -> Result<Vec<Value>> {
318    values
319        .iter()
320        .map(|value| match value {
321            Value::Null => Ok(Value::Null),
322            Value::List(nested) => {
323                format_regtype_array_elements(nested, element, engine).map(Value::List)
324            }
325            other => format_regtype_value(other, element, engine)
326                .map(|text| text.map_or_else(|| other.clone(), Value::Str)),
327        })
328        .collect()
329}
330
331/// Cast a value after resolving catalog-owned types and enforcing domain constraints before exposing a base-type carrier.
332pub fn cast_value_with_type_resolution(
333    value: &Value,
334    source_ty: Option<&str>,
335    target_ty: &str,
336    engine: Option<&dyn EngineHook>,
337) -> Result<Value> {
338    let resolved_target = engine
339        .map(|engine| engine.resolve_type_name(target_ty))
340        .transpose()
341        .map_err(SQLError::Internal)?
342        .flatten();
343    if let (Some(engine), Some(target)) = (engine, resolved_target.as_ref()) {
344        if let Some(value) = engine.cast_domain(value, source_ty, target)? {
345            return Ok(value);
346        }
347        if matches!(target, ColumnType::Array(_)) && requires_catalog_array_cast(target) {
348            return cast_catalog_array(value, source_ty, target, engine);
349        }
350    }
351    let resolved_source = match (engine, source_ty) {
352        (Some(engine), Some(source_ty)) => engine
353            .resolve_type_name(source_ty)
354            .map_err(SQLError::Internal)?
355            .map(|ty| coercion_type_name(&ty)),
356        _ => None,
357    };
358    let source_ty = resolved_source.as_deref().or(source_ty);
359    let target_ty = resolved_target.as_ref().map_or_else(
360        || Cow::Borrowed(target_ty),
361        |ty| Cow::Owned(coercion_type_name(ty)),
362    );
363    let target_column_type = resolved_target
364        .clone()
365        .or_else(|| ColumnType::from_sql_name(&target_ty).ok());
366    if target_column_type.as_ref().is_some_and(regrole_array_type) {
367        let source_column_type = source_ty.and_then(|name| ColumnType::from_sql_name(name).ok());
368        return cast_regrole_array(value, source_column_type.as_ref(), engine);
369    }
370    if target_ty.eq_ignore_ascii_case("text") {
371        if let Some(source_ty) = source_ty.and_then(|source| ColumnType::from_sql_name(source).ok())
372        {
373            if let Some(text) = format_regtype_value(value, &source_ty, engine)? {
374                return Ok(Value::Str(text));
375            }
376        }
377    }
378    if target_ty.eq_ignore_ascii_case("regclass") {
379        if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
380            return engine
381                .resolve_regclass_input(name)?
382                .map(Value::Int)
383                .ok_or_else(|| SQLError::Routine {
384                    sqlstate: "42P01".into(),
385                    message: format!("relation \"{name}\" does not exist"),
386                });
387        }
388    }
389    if target_ty.eq_ignore_ascii_case("regprocedure") {
390        if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
391            return engine
392                .resolve_regprocedure(name)
393                .map_err(SQLError::Internal)?
394                .map(Value::Int)
395                .ok_or_else(|| SQLError::Routine {
396                    sqlstate: "42883".into(),
397                    message: format!("function {name} does not exist"),
398                });
399        }
400    }
401    if target_ty.eq_ignore_ascii_case("regrole") {
402        if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
403            return engine
404                .resolve_regrole(name)?
405                .map(Value::Int)
406                .ok_or_else(|| SQLError::Routine {
407                    sqlstate: "42704".into(),
408                    message: format!("role \"{name}\" does not exist"),
409                });
410        }
411    }
412    if matches!(target_column_type.as_ref(), Some(ColumnType::Regnamespace)) {
413        if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
414            return engine
415                .resolve_regnamespace(name)?
416                .map(Value::Int)
417                .ok_or_else(|| SQLError::Routine {
418                    sqlstate: "3F000".into(),
419                    message: format!("schema \"{name}\" does not exist"),
420                });
421        }
422    }
423    if matches!(target_column_type.as_ref(), Some(ColumnType::Regtype)) {
424        if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
425            if let Some(oid) = engine.resolve_regtype_input(name)? {
426                return Ok(Value::Int(oid));
427            }
428        }
429    }
430    cast_value_from(value, &target_ty, source_ty)
431}
432
433fn requires_catalog_array_cast(ty: &ColumnType) -> bool {
434    match ty {
435        ColumnType::Domain { .. } | ColumnType::Regtype => true,
436        ColumnType::Array(element) => requires_catalog_array_cast(element),
437        _ => false,
438    }
439}
440
441fn cast_catalog_array(
442    value: &Value,
443    source: Option<&str>,
444    target: &ColumnType,
445    engine: &dyn EngineHook,
446) -> Result<Value> {
447    if matches!(value, Value::Null) {
448        return Ok(Value::Null);
449    }
450    let array = match value {
451        Value::Array(array) => array.clone(),
452        Value::Str(text) => parse_pg_array_literal(text)?,
453        other => {
454            return Err(SQLError::TypeMismatch(format!(
455                "CAST AS {}: expected array, got {other:?}",
456                target.sql_name()
457            )))
458        }
459    };
460    let source_element = source.map(|name| name.trim_end_matches("[]"));
461    let target_element = array_leaf_type(target).sql_name();
462    let values =
463        cast_catalog_array_elements(array.elements(), source_element, &target_element, engine)?;
464    ArrayValue::with_lower_bounds(values, array.lower_bounds().to_vec())
465        .map(Value::Array)
466        .ok_or_else(|| SQLError::TypeMismatch("array dimensions changed during cast".into()))
467}
468
469fn cast_catalog_array_elements(
470    values: &[Value],
471    source: Option<&str>,
472    target: &str,
473    engine: &dyn EngineHook,
474) -> Result<Vec<Value>> {
475    values
476        .iter()
477        .map(|value| match value {
478            Value::List(values) => {
479                cast_catalog_array_elements(values, source, target, engine).map(Value::List)
480            }
481            value => cast_value_with_type_resolution(value, source, target, Some(engine)),
482        })
483        .collect()
484}
485
486/// Read-only row interface used by the expression evaluator. Most callers
487/// use a materialised [`ResultRow`], while hot execution paths can expose a
488/// projected value slice without rebuilding a string-keyed map for every row.
489pub trait RowLookup {
490    fn column(&self, name: &str) -> Option<&Value>;
491
492    /// Whether an unqualified name identifies more than one visible input
493    /// column. Callers must report SQLSTATE 42702 instead of selecting an
494    /// arbitrary suffix match.
495    fn column_is_ambiguous(&self, _name: &str) -> bool {
496        false
497    }
498
499    fn qualified_column(&self, qualifier: &str, column: &str) -> Option<&Value>;
500
501    /// Whether a qualified identity names more than one visible input column.
502    fn qualified_column_is_ambiguous(&self, _qualifier: &str, _column: &str) -> bool {
503        false
504    }
505
506    /// Return a value by the physical schema position used to construct this
507    /// row view. Materialized named rows do not expose positional access;
508    /// projected execution sources override it so compiled hot paths can avoid
509    /// repeating string lookup for every expression and row.
510    fn positional_column(&self, _index: usize) -> Option<&Value> {
511        None
512    }
513
514    /// Resolve an executor-only relation attribute. Materialized SQL rows do
515    /// not expose these structural slots.
516    fn internal_column(&self, _column: InternalColumnRef) -> Option<&Value> {
517        None
518    }
519
520    /// 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.
521    fn score_source(&self, _qualifier: Option<&str>) -> Option<&Value> {
522        None
523    }
524
525    /// Whether the requested score source resolves to more than one retrieval relation.
526    fn score_source_is_ambiguous(&self, _qualifier: Option<&str>) -> bool {
527        false
528    }
529
530    /// Visit every logical column in schema order. Named rows use their map
531    /// order; positional execution rows override this without materializing a
532    /// map. The default keeps narrow projected lookup implementations source
533    /// compatible when they deliberately do not expose whole-row semantics.
534    fn visit_columns(&self, _visitor: &mut dyn FnMut(&str, &Value)) {}
535}
536
537impl RowLookup for ResultRow {
538    fn column(&self, name: &str) -> Option<&Value> {
539        self.get(name)
540    }
541
542    fn qualified_column(&self, _qualifier: &str, _column: &str) -> Option<&Value> {
543        None
544    }
545
546    fn visit_columns(&self, visitor: &mut dyn FnMut(&str, &Value)) {
547        for (column, value) in self {
548            visitor(column, value);
549        }
550    }
551}
552
553pub struct EvalContext<'a> {
554    pub row: Option<&'a ResultRow>,
555    row_lookup: Option<&'a dyn RowLookup>,
556    pub params: &'a [SQLParam],
557    pub engine: Option<&'a dyn EngineHook>,
558}
559
560impl<'a> EvalContext<'a> {
561    pub fn new(row: Option<&'a ResultRow>, params: &'a [SQLParam]) -> Self {
562        Self {
563            row,
564            row_lookup: row.map(|row| row as &dyn RowLookup),
565            params,
566            engine: None,
567        }
568    }
569
570    pub fn from_row_lookup(row: &'a dyn RowLookup, params: &'a [SQLParam]) -> Self {
571        Self {
572            // Whole-row materialization is needed only by correlated
573            // subqueries. Ordinary scalar evaluation must remain on the
574            // lookup/slot path.
575            row: None,
576            row_lookup: Some(row),
577            params,
578            engine: None,
579        }
580    }
581
582    pub fn with_engine(mut self, engine: &'a dyn EngineHook) -> Self {
583        self.engine = Some(engine);
584        self
585    }
586
587    pub(super) fn row_lookup(&self) -> Result<&'a dyn RowLookup> {
588        self.row_lookup
589            .ok_or_else(|| SQLError::Internal("column reference without row context".into()))
590    }
591
592    /// Resolve an unqualified column through the same row semantics used by
593    /// the AST evaluator. Physical scalar IR evaluators call this instead of
594    /// reconstructing an [`Expr::Column`](crate::ast::Expr::Column) carrier.
595    pub fn column_value(&self, name: &str) -> Result<Value> {
596        if self.row_lookup()?.column_is_ambiguous(name) {
597            return Err(SQLError::AmbiguousColumn(name.to_string()));
598        }
599        Ok(self
600            .row_lookup()?
601            .column(name)
602            .cloned()
603            .unwrap_or(Value::Null))
604    }
605
606    /// Resolve a qualified column without constructing an AST expression.
607    pub fn qualified_column_value(&self, qualifier: &str, column: &str) -> Result<Value> {
608        if self
609            .row_lookup()?
610            .qualified_column_is_ambiguous(qualifier, column)
611        {
612            return Err(SQLError::AmbiguousColumn(format!("{qualifier}.{column}")));
613        }
614        Ok(self
615            .row_lookup()?
616            .qualified_column(qualifier, column)
617            .cloned()
618            .unwrap_or(Value::Null))
619    }
620}