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 uqa_core::{
10    memory::{Produced, ProductionControl},
11    Value,
12};
13
14use crate::ast::{ColumnType, InternalColumnRef};
15use crate::error::{Result, SQLError};
16use crate::params::SQLParam;
17use crate::result::ResultRow;
18
19mod casting;
20mod regtype;
21pub use casting::{
22    cast_value_with_type_resolution, cast_value_with_type_resolution_with_control,
23    coercion_type_name,
24};
25pub use regtype::{format_regtype_value, format_regtype_value_with_control};
26
27/// Engine-side hook that scalar function evaluation calls for stateful
28/// sequence and user-defined functions. Query-valued expressions are not
29/// accepted here: lowering assigns them physical query-plan slots executed by
30/// `uqa-execution::ScalarSubqueryRunner`.
31pub trait EngineHook {
32    /// Start of the current SQL transaction, in Unix microseconds.
33    fn transaction_timestamp_micros(&self) -> Option<i64> {
34        None
35    }
36
37    /// Start of the current frontend SQL message, in Unix microseconds.
38    fn statement_timestamp_micros(&self) -> Option<i64> {
39        None
40    }
41
42    fn nextval(&self, name: &str) -> Result<i64>;
43    fn currval(&self, name: &str) -> Result<i64>;
44    fn lastval(&self) -> Result<i64> {
45        Err(SQLError::Unsupported(
46            "lastval requires an engine hook implementation".into(),
47        ))
48    }
49    fn setval(&self, name: &str, value: i64, is_called: bool) -> Result<i64>;
50
51    fn call_scalar_function(&self, _name: &str, _args: &[Value]) -> Option<Result<Value>> {
52        None
53    }
54
55    /// Invoke an engine-backed built-in after an exact catalog binding has
56    /// selected it. Unlike `call_scalar_function`, this path is also available
57    /// when dynamic dispatch is disabled, so runtime callbacks cannot override
58    /// the stored built-in identity.
59    fn call_bound_builtin_function(
60        &self,
61        _binding: &crate::ast::FunctionBinding,
62        _args: &[(Option<String>, Value)],
63    ) -> Option<Result<Value>> {
64        None
65    }
66
67    fn has_scalar_functions(&self) -> bool {
68        true
69    }
70
71    /// Resolve a catalog-owned SQL type name for casts evaluated with an engine context.
72    fn resolve_type_name(&self, _name: &str) -> std::result::Result<Option<ColumnType>, String> {
73        Ok(None)
74    }
75
76    /// Apply catalog-owned domain conversion and constraints. A missing implementation leaves built-in catalog domains on their base-type conversion path.
77    fn cast_domain(
78        &self,
79        _value: &Value,
80        _source: Option<&str>,
81        _target: &ColumnType,
82    ) -> Result<Option<Value>> {
83        Ok(None)
84    }
85
86    /// Resolve a regtype cast to its OID carrier when a complete type catalog is available.
87    fn resolve_regtype_input(&self, _name: &str) -> Result<Option<i64>> {
88        Ok(None)
89    }
90
91    /// Resolve a relation name to the OID carrier used by `regclass`.
92    fn resolve_regclass(&self, _name: &str) -> std::result::Result<Option<i64>, String> {
93        Ok(None)
94    }
95
96    /// 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.
97    fn resolve_regclass_input(&self, name: &str) -> Result<Option<i64>> {
98        self.resolve_regclass(name).map_err(SQLError::Internal)
99    }
100
101    /// Resolve an exact routine signature to the OID carrier used by `regprocedure`.
102    fn resolve_regprocedure(&self, _name: &str) -> std::result::Result<Option<i64>, String> {
103        Ok(None)
104    }
105
106    /// Resolve a `regrole` input while preserving hard input errors for direct casts.
107    fn resolve_regrole(&self, _name: &str) -> Result<Option<i64>> {
108        Ok(None)
109    }
110
111    /// Resolve a `regnamespace` input while preserving hard input errors for direct casts.
112    fn resolve_regnamespace(&self, name: &str) -> Result<Option<i64>> {
113        self.resolve_regobject(&ColumnType::Regnamespace, name)
114    }
115
116    /// 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`.
117    fn resolve_regobject(&self, ty: &ColumnType, name: &str) -> Result<Option<i64>> {
118        match ty {
119            ColumnType::Regclass => self.resolve_regclass_input(name),
120            ColumnType::Regprocedure => self.resolve_regprocedure(name).map_err(SQLError::Internal),
121            ColumnType::Regrole => self.resolve_regrole(name),
122            ColumnType::Regproc | ColumnType::Regnamespace | ColumnType::Regtype => Ok(None),
123            _ => Err(SQLError::Internal(format!(
124                "unsupported regobject lookup type `{}`",
125                ty.sql_name()
126            ))),
127        }
128    }
129
130    /// Resolve one OID-backed alias type to its `PostgreSQL` text output.
131    fn resolve_regtype_output(
132        &self,
133        _ty: &ColumnType,
134        _oid: i64,
135    ) -> std::result::Result<Option<String>, String> {
136        Ok(None)
137    }
138
139    /// Resolve the first existing schema on the logical session's search
140    /// path. `None` lets standalone expression evaluation use its `public`
141    /// compatibility default.
142    fn current_schema(&self) -> std::result::Result<Option<String>, String> {
143        Ok(None)
144    }
145
146    fn current_user(&self) -> std::result::Result<Option<String>, crate::SQLError> {
147        Ok(None)
148    }
149
150    fn session_user(&self) -> std::result::Result<Option<String>, crate::SQLError> {
151        Ok(None)
152    }
153
154    /// Read a session setting. `None` means the parameter is unknown; errors must remain visible even for `current_setting(..., true)`.
155    fn runtime_parameter(&self, _name: &str) -> Result<Option<String>> {
156        Err(SQLError::Unsupported(
157            "engine hook does not provide session settings".into(),
158        ))
159    }
160
161    /// Resolve the existing schemas visible to the logical session.
162    fn current_schemas(
163        &self,
164        _include_implicit: bool,
165    ) -> std::result::Result<Option<Vec<String>>, String> {
166        Ok(None)
167    }
168
169    /// Draw from an engine-owned logical-session PRNG. `None` keeps pure,
170    /// engine-free expression evaluation available for library callers.
171    fn random_value(&self) -> std::result::Result<Option<f64>, String> {
172        Ok(None)
173    }
174
175    /// Draw every bit of one engine-owned logical-session PRNG word. Range
176    /// functions use this instead of a floating-point sample so `bigint` and
177    /// arbitrary-precision `numeric` bounds remain uniform.
178    fn random_u64(&self) -> std::result::Result<Option<u64>, String> {
179        Ok(None)
180    }
181
182    /// Reseed the logical-session PRNG. `false` means the hook does not own a
183    /// mutable random stream and the caller must report the unsupported call.
184    fn set_random_seed(&self, _seed: f64) -> std::result::Result<bool, String> {
185        Ok(false)
186    }
187
188    /// Invoke a user-defined SQL / `PL/pgSQL` function. Consulted
189    /// after built-in dispatch misses (and immediately for calls with
190    /// named arguments, which built-ins never accept). `None` means
191    /// no user-defined function with this name exists.
192    fn call_user_function(
193        &self,
194        _name: &str,
195        _args: &[(Option<String>, Value)],
196    ) -> Option<Result<Value>> {
197        None
198    }
199
200    fn call_bound_user_function(
201        &self,
202        _binding: &crate::ast::FunctionBinding,
203        _args: &[(Option<String>, Value)],
204    ) -> Option<Result<Value>> {
205        None
206    }
207}
208
209/// Read-only row interface used by the expression evaluator. Most callers
210/// use a materialised [`ResultRow`], while hot execution paths can expose a
211/// projected value slice without rebuilding a string-keyed map for every row.
212pub trait RowLookup {
213    fn column(&self, name: &str) -> Option<&Value>;
214
215    /// Whether an unqualified name identifies more than one visible input
216    /// column. Callers must report SQLSTATE 42702 instead of selecting an
217    /// arbitrary suffix match.
218    fn column_is_ambiguous(&self, _name: &str) -> bool {
219        false
220    }
221
222    fn qualified_column(&self, qualifier: &str, column: &str) -> Option<&Value>;
223
224    /// Whether a qualified identity names more than one visible input column.
225    fn qualified_column_is_ambiguous(&self, _qualifier: &str, _column: &str) -> bool {
226        false
227    }
228
229    /// Return a value by the physical schema position used to construct this
230    /// row view. Materialized named rows do not expose positional access;
231    /// projected execution sources override it so compiled hot paths can avoid
232    /// repeating string lookup for every expression and row.
233    fn positional_column(&self, _index: usize) -> Option<&Value> {
234        None
235    }
236
237    /// Resolve an executor-only relation attribute. Materialized SQL rows do
238    /// not expose these structural slots.
239    fn internal_column(&self, _column: InternalColumnRef) -> Option<&Value> {
240        None
241    }
242
243    /// 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.
244    fn score_source(&self, _qualifier: Option<&str>) -> Option<&Value> {
245        None
246    }
247
248    /// Whether the requested score source resolves to more than one retrieval relation.
249    fn score_source_is_ambiguous(&self, _qualifier: Option<&str>) -> bool {
250        false
251    }
252
253    /// Visit every logical column in schema order. Named rows use their map
254    /// order; positional execution rows override this without materializing a
255    /// map. The default keeps narrow projected lookup implementations source
256    /// compatible when they deliberately do not expose whole-row semantics.
257    fn visit_columns(&self, _visitor: &mut dyn FnMut(&str, &Value)) {}
258}
259
260impl RowLookup for ResultRow {
261    fn column(&self, name: &str) -> Option<&Value> {
262        self.get(name)
263    }
264
265    fn qualified_column(&self, _qualifier: &str, _column: &str) -> Option<&Value> {
266        None
267    }
268
269    fn visit_columns(&self, visitor: &mut dyn FnMut(&str, &Value)) {
270        for (column, value) in self {
271            visitor(column, value);
272        }
273    }
274}
275
276pub struct EvalContext<'a> {
277    pub row: Option<&'a ResultRow>,
278    row_lookup: Option<&'a dyn RowLookup>,
279    pub params: &'a [SQLParam],
280    pub engine: Option<&'a dyn EngineHook>,
281}
282
283impl<'a> EvalContext<'a> {
284    pub fn new(row: Option<&'a ResultRow>, params: &'a [SQLParam]) -> Self {
285        Self {
286            row,
287            row_lookup: row.map(|row| row as &dyn RowLookup),
288            params,
289            engine: None,
290        }
291    }
292
293    pub fn from_row_lookup(row: &'a dyn RowLookup, params: &'a [SQLParam]) -> Self {
294        Self {
295            // Whole-row materialization is needed only by correlated
296            // subqueries. Ordinary scalar evaluation must remain on the
297            // lookup/slot path.
298            row: None,
299            row_lookup: Some(row),
300            params,
301            engine: None,
302        }
303    }
304
305    pub fn with_engine(mut self, engine: &'a dyn EngineHook) -> Self {
306        self.engine = Some(engine);
307        self
308    }
309
310    pub(super) fn row_lookup(&self) -> Result<&'a dyn RowLookup> {
311        self.row_lookup
312            .ok_or_else(|| SQLError::Internal("column reference without row context".into()))
313    }
314
315    /// Resolve an unqualified column through the same row semantics used by
316    /// the AST evaluator. Physical scalar IR evaluators call this instead of
317    /// reconstructing an [`Expr::Column`](crate::ast::Expr::Column) carrier.
318    pub fn column_value(&self, name: &str) -> Result<Value> {
319        self.column_value_with_control(name, &ProductionControl::uncontrolled())
320            .map(|value| value.into_uncontrolled().expect("ordinary column value"))
321    }
322
323    /// Resolve the same row slot while its copied payload retains the caller's allowance and cancellation scopes.
324    pub fn column_value_with_control(
325        &self,
326        name: &str,
327        control: &ProductionControl<'_>,
328    ) -> Result<Produced<Value>> {
329        control.check()?;
330        let row = self.row_lookup()?;
331        if row.column_is_ambiguous(name) {
332            return Err(SQLError::AmbiguousColumn(name.to_string()));
333        }
334        Ok(control.copy_value(row.column(name).unwrap_or(&Value::Null))?)
335    }
336
337    /// Resolve a qualified column without constructing an AST expression.
338    pub fn qualified_column_value(&self, qualifier: &str, column: &str) -> Result<Value> {
339        self.qualified_column_value_with_control(
340            qualifier,
341            column,
342            &ProductionControl::uncontrolled(),
343        )
344        .map(|value| {
345            value
346                .into_uncontrolled()
347                .expect("ordinary qualified column value")
348        })
349    }
350
351    /// Resolve a qualified slot with the same ambiguity and missing-value behavior under a retained output owner.
352    pub fn qualified_column_value_with_control(
353        &self,
354        qualifier: &str,
355        column: &str,
356        control: &ProductionControl<'_>,
357    ) -> Result<Produced<Value>> {
358        control.check()?;
359        let row = self.row_lookup()?;
360        if row.qualified_column_is_ambiguous(qualifier, column) {
361            return Err(SQLError::AmbiguousColumn(format!("{qualifier}.{column}")));
362        }
363        Ok(control.copy_value(
364            row.qualified_column(qualifier, column)
365                .unwrap_or(&Value::Null),
366        )?)
367    }
368}
369
370#[cfg(test)]
371mod production_tests;