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, read_catalog_array_input, read_catalog_input, CatalogInputFunctions,
24};
25pub(crate) use casting::{requires_catalog_constant_input, requires_domain_array_input};
26pub use regtype::{
27    format_regtype_elements_with_control, format_regtype_value, format_regtype_value_with_control,
28};
29
30/// Engine-side hook that scalar function evaluation calls for stateful
31/// sequence and user-defined functions. Query-valued expressions are not
32/// accepted here: lowering assigns them physical query-plan slots executed by
33/// `uqa-execution::ScalarSubqueryRunner`.
34pub trait EngineHook {
35    /// Enforce EXECUTE on a selected built-in at an execution boundary. Catalog-free embedders have no mutable routine ACLs.
36    fn require_builtin_execute(&self, _binding: &crate::ast::FunctionBinding) -> Result<()> {
37        Ok(())
38    }
39
40    /// Start of the current SQL transaction, in Unix microseconds.
41    fn transaction_timestamp_micros(&self) -> Option<i64> {
42        None
43    }
44
45    /// Start of the current frontend SQL message, in Unix microseconds.
46    fn statement_timestamp_micros(&self) -> Option<i64> {
47        None
48    }
49
50    fn nextval(&self, name: &str) -> Result<i64>;
51    fn currval(&self, name: &str) -> Result<i64>;
52    fn lastval(&self) -> Result<i64> {
53        Err(SQLError::Unsupported(
54            "lastval requires an engine hook implementation".into(),
55        ))
56    }
57    fn setval(&self, name: &str, value: i64, is_called: bool) -> Result<i64>;
58
59    fn call_scalar_function(&self, _name: &str, _args: &[Value]) -> Option<Result<Value>> {
60        None
61    }
62
63    /// Invoke an engine-backed built-in after an exact catalog binding has
64    /// selected it. Unlike `call_scalar_function`, this path is also available
65    /// when dynamic dispatch is disabled, so runtime callbacks cannot override
66    /// the stored built-in identity.
67    fn call_bound_builtin_function(
68        &self,
69        _binding: &crate::ast::FunctionBinding,
70        _args: &[(Option<String>, Value)],
71    ) -> Option<Result<Value>> {
72        None
73    }
74
75    fn has_scalar_functions(&self) -> bool {
76        true
77    }
78
79    /// Enum labels of the statement's catalog. Embedders without catalog enum types keep the default, so enum input and output fail as an unavailable type.
80    fn enum_labels(&self) -> Option<&dyn super::enums::EnumLabelCatalog> {
81        None
82    }
83
84    /// Composite type attributes of the statement's catalog. Embedders without catalog composite types keep the default, so composite input and coercion fail as an unavailable type.
85    fn composite_types(&self) -> Option<&dyn super::composites::CompositeTypeCatalog> {
86        None
87    }
88
89    /// Resolve a catalog-owned SQL type name for casts evaluated with an engine context.
90    fn resolve_type_name(&self, _name: &str) -> std::result::Result<Option<ColumnType>, String> {
91        Ok(None)
92    }
93
94    /// Resolve an already admitted catalog type identity without name lookup or input privileges.
95    fn resolve_type_oid(&self, _oid: u32) -> std::result::Result<Option<ColumnType>, String> {
96        Ok(None)
97    }
98
99    /// Apply catalog-owned domain conversion and constraints. A missing implementation leaves built-in catalog domains on their base-type conversion path.
100    fn cast_domain(
101        &self,
102        _value: &Value,
103        _source: Option<&str>,
104        _target: &ColumnType,
105    ) -> Result<Option<Value>> {
106        Ok(None)
107    }
108
109    /// Resolve a regtype cast to its OID carrier when a complete type catalog is available.
110    fn resolve_regtype_input(&self, _name: &str) -> Result<Option<i64>> {
111        Ok(None)
112    }
113
114    /// Resolve a relation name to the OID carrier used by `regclass`.
115    fn resolve_regclass(&self, _name: &str) -> std::result::Result<Option<i64>, String> {
116        Ok(None)
117    }
118
119    /// 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.
120    fn resolve_regclass_input(&self, name: &str) -> Result<Option<i64>> {
121        self.resolve_regclass(name).map_err(SQLError::Internal)
122    }
123
124    /// Resolve a routine name that must name exactly one routine, as `regproc` input does.
125    fn resolve_regproc(&self, _name: &str) -> Result<Option<i64>> {
126        Ok(None)
127    }
128
129    /// Resolve an exact routine signature to the OID carrier used by `regprocedure`.
130    fn resolve_regprocedure(&self, _name: &str) -> std::result::Result<Option<i64>, String> {
131        Ok(None)
132    }
133
134    /// Resolve `regprocedure` input while preserving the typed errors `regprocedurein` reports. Embedders that implement only the historical lookup hook keep its behavior.
135    fn resolve_regprocedure_input(&self, name: &str) -> Result<Option<i64>> {
136        self.resolve_regprocedure(name).map_err(SQLError::Internal)
137    }
138
139    /// Resolve a `regrole` input while preserving hard input errors for direct casts.
140    fn resolve_regrole(&self, _name: &str) -> Result<Option<i64>> {
141        Ok(None)
142    }
143
144    /// Resolve a `regnamespace` input while preserving hard input errors for direct casts.
145    fn resolve_regnamespace(&self, name: &str) -> Result<Option<i64>> {
146        self.resolve_regobject(&ColumnType::Regnamespace, name)
147    }
148
149    /// Resolve `regcollation` input with the direct input function's diagnostics.
150    fn resolve_regcollation(&self, name: &str) -> Result<Option<i64>> {
151        self.resolve_regobject(&ColumnType::Regcollation, name)
152    }
153
154    /// 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`.
155    fn resolve_regobject(&self, ty: &ColumnType, name: &str) -> Result<Option<i64>> {
156        match ty {
157            ColumnType::Regclass => self.resolve_regclass_input(name),
158            ColumnType::Regprocedure => self.resolve_regprocedure(name).map_err(SQLError::Internal),
159            ColumnType::Regrole => self.resolve_regrole(name),
160            ColumnType::Regproc
161            | ColumnType::Regcollation
162            | ColumnType::Regnamespace
163            | ColumnType::Regtype => Ok(None),
164            _ => Err(SQLError::Internal(format!(
165                "unsupported regobject lookup type `{}`",
166                ty.sql_name()
167            ))),
168        }
169    }
170
171    /// Resolve one OID-backed alias type to its `PostgreSQL` text output.
172    fn resolve_regtype_output(
173        &self,
174        _ty: &ColumnType,
175        _oid: i64,
176    ) -> std::result::Result<Option<String>, String> {
177        Ok(None)
178    }
179
180    /// The first schema of the session's search path that exists and that the role may use, or `None` when there is none, which `current_schema()` reports as NULL. A hook without a session reports `public`, the first schema of the default search path.
181    fn current_schema(&self) -> std::result::Result<Option<String>, String> {
182        Ok(Some("public".into()))
183    }
184
185    fn current_user(&self) -> std::result::Result<Option<String>, crate::SQLError> {
186        Ok(None)
187    }
188
189    fn session_user(&self) -> std::result::Result<Option<String>, crate::SQLError> {
190        Ok(None)
191    }
192
193    /// Read a session setting. `None` means the parameter is unknown; errors must remain visible even for `current_setting(..., true)`.
194    fn runtime_parameter(&self, _name: &str) -> Result<Option<String>> {
195        Err(SQLError::Unsupported(
196            "engine hook does not provide session settings".into(),
197        ))
198    }
199
200    /// Sleep in the session for `duration`, ending at once when the statement is canceled or times out (`pg_sleep`).
201    fn sleep(&self, _duration: std::time::Duration) -> Result<()> {
202        Err(SQLError::Unsupported(
203            "engine hook does not provide session sleeps".into(),
204        ))
205    }
206
207    /// Assign a session setting as `set_config(name, value, is_local)` does, restoring the reset setting for `None`, and return the new value as `SHOW` reports it.
208    fn set_runtime_parameter(
209        &self,
210        _name: &str,
211        _value: Option<&str>,
212        _local: bool,
213    ) -> Result<String> {
214        Err(SQLError::Unsupported(
215            "engine hook does not provide session settings".into(),
216        ))
217    }
218
219    /// Resolve the existing schemas visible to the logical session.
220    fn current_schemas(
221        &self,
222        _include_implicit: bool,
223    ) -> std::result::Result<Option<Vec<String>>, String> {
224        Ok(None)
225    }
226
227    /// Draw from an engine-owned logical-session PRNG. `None` keeps pure,
228    /// engine-free expression evaluation available for library callers.
229    fn random_value(&self) -> std::result::Result<Option<f64>, String> {
230        Ok(None)
231    }
232
233    /// Draw every bit of one engine-owned logical-session PRNG word. Range
234    /// functions use this instead of a floating-point sample so `bigint` and
235    /// arbitrary-precision `numeric` bounds remain uniform.
236    fn random_u64(&self) -> std::result::Result<Option<u64>, String> {
237        Ok(None)
238    }
239
240    /// Reseed the logical-session PRNG. `false` means the hook does not own a
241    /// mutable random stream and the caller must report the unsupported call.
242    fn set_random_seed(&self, _seed: f64) -> std::result::Result<bool, String> {
243        Ok(false)
244    }
245
246    /// Invoke a user-defined SQL / `PL/pgSQL` function. Consulted
247    /// after built-in dispatch misses (and immediately for calls with
248    /// named arguments, which built-ins never accept). `None` means
249    /// no user-defined function with this name exists.
250    fn call_user_function(
251        &self,
252        _name: &str,
253        _args: &[(Option<String>, Value)],
254    ) -> Option<Result<Value>> {
255        None
256    }
257
258    fn call_bound_user_function(
259        &self,
260        _binding: &crate::ast::FunctionBinding,
261        _args: &[(Option<String>, Value)],
262    ) -> Option<Result<Value>> {
263        None
264    }
265}
266
267/// Read-only row interface used by the expression evaluator. Most callers
268/// use a materialised [`ResultRow`], while hot execution paths can expose a
269/// projected value slice without rebuilding a string-keyed map for every row.
270pub trait RowLookup {
271    fn column(&self, name: &str) -> Option<&Value>;
272
273    /// Whether an unqualified name identifies more than one visible input
274    /// column. Callers must report SQLSTATE 42702 instead of selecting an
275    /// arbitrary suffix match.
276    fn column_is_ambiguous(&self, _name: &str) -> bool {
277        false
278    }
279
280    fn qualified_column(&self, qualifier: &str, column: &str) -> Option<&Value>;
281
282    /// Whether a qualified identity names more than one visible input column.
283    fn qualified_column_is_ambiguous(&self, _qualifier: &str, _column: &str) -> bool {
284        false
285    }
286
287    /// Return a value by the physical schema position used to construct this
288    /// row view. Materialized named rows do not expose positional access;
289    /// projected execution sources override it so compiled hot paths can avoid
290    /// repeating string lookup for every expression and row.
291    fn positional_column(&self, _index: usize) -> Option<&Value> {
292        None
293    }
294
295    /// Resolve an executor-only relation attribute. Materialized SQL rows do
296    /// not expose these structural slots.
297    fn internal_column(&self, _column: InternalColumnRef) -> Option<&Value> {
298        None
299    }
300
301    /// 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.
302    fn score_source(&self, _qualifier: Option<&str>) -> Option<&Value> {
303        None
304    }
305
306    /// Whether the requested score source resolves to more than one retrieval relation.
307    fn score_source_is_ambiguous(&self, _qualifier: Option<&str>) -> bool {
308        false
309    }
310
311    /// Visit every logical column in schema order. Named rows use their map
312    /// order; positional execution rows override this without materializing a
313    /// map. The default keeps narrow projected lookup implementations source
314    /// compatible when they deliberately do not expose whole-row semantics.
315    fn visit_columns(&self, _visitor: &mut dyn FnMut(&str, &Value)) {}
316}
317
318impl RowLookup for ResultRow {
319    fn column(&self, name: &str) -> Option<&Value> {
320        self.get(name)
321    }
322
323    fn qualified_column(&self, _qualifier: &str, _column: &str) -> Option<&Value> {
324        None
325    }
326
327    fn visit_columns(&self, visitor: &mut dyn FnMut(&str, &Value)) {
328        for (column, value) in self {
329            visitor(column, value);
330        }
331    }
332}
333
334pub struct EvalContext<'a> {
335    pub row: Option<&'a ResultRow>,
336    row_lookup: Option<&'a dyn RowLookup>,
337    pub params: &'a [SQLParam],
338    pub engine: Option<&'a dyn EngineHook>,
339    enum_comparison_states: [Option<&'a super::enums::EnumComparisonState>; 4],
340}
341
342impl<'a> EvalContext<'a> {
343    pub fn new(row: Option<&'a ResultRow>, params: &'a [SQLParam]) -> Self {
344        Self {
345            row,
346            row_lookup: row.map(|row| row as &dyn RowLookup),
347            params,
348            engine: None,
349            enum_comparison_states: [None; 4],
350        }
351    }
352
353    pub fn from_row_lookup(row: &'a dyn RowLookup, params: &'a [SQLParam]) -> Self {
354        Self {
355            // Whole-row materialization is needed only by correlated
356            // subqueries. Ordinary scalar evaluation must remain on the
357            // lookup/slot path.
358            row: None,
359            row_lookup: Some(row),
360            params,
361            engine: None,
362            enum_comparison_states: [None; 4],
363        }
364    }
365
366    pub fn with_engine(mut self, engine: &'a dyn EngineHook) -> Self {
367        self.engine = Some(engine);
368        self
369    }
370
371    /// The state of the current bound enum call, supplied by its prepared execution owner.
372    pub fn with_enum_comparison_state(
373        mut self,
374        state: Option<&'a super::enums::EnumComparisonState>,
375    ) -> Self {
376        self.enum_comparison_states[0] = state;
377        self
378    }
379
380    /// Independent comparisons within one prepared expression, including the
381    /// four operators of BETWEEN SYMMETRIC, retain separate type caches.
382    pub fn with_enum_comparison_states(
383        mut self,
384        states: [Option<&'a super::enums::EnumComparisonState>; 4],
385    ) -> Self {
386        self.enum_comparison_states = states;
387        self
388    }
389
390    pub(super) fn enum_comparison_state(&self) -> Option<&super::enums::EnumComparisonState> {
391        self.enum_comparison_states[0]
392    }
393
394    pub(super) fn enum_comparison_state_at(
395        &self,
396        slot: usize,
397    ) -> Option<&super::enums::EnumComparisonState> {
398        self.enum_comparison_states[slot]
399    }
400
401    pub(super) fn row_lookup(&self) -> Result<&'a dyn RowLookup> {
402        self.row_lookup
403            .ok_or_else(|| SQLError::Internal("column reference without row context".into()))
404    }
405
406    /// Resolve an unqualified column through the same row semantics used by
407    /// the AST evaluator. Physical scalar IR evaluators call this instead of
408    /// reconstructing an [`Expr::Column`](crate::ast::Expr::Column) carrier.
409    pub fn column_value(&self, name: &str) -> Result<Value> {
410        self.column_value_with_control(name, &ProductionControl::uncontrolled())
411            .map(|value| value.into_uncontrolled().expect("ordinary column value"))
412    }
413
414    /// Resolve the same row slot while its copied payload retains the caller's allowance and cancellation scopes.
415    pub fn column_value_with_control(
416        &self,
417        name: &str,
418        control: &ProductionControl<'_>,
419    ) -> Result<Produced<Value>> {
420        control.check()?;
421        let row = self.row_lookup()?;
422        if row.column_is_ambiguous(name) {
423            return Err(SQLError::AmbiguousColumn(name.to_string()));
424        }
425        Ok(control.copy_value(row.column(name).unwrap_or(&Value::Null))?)
426    }
427
428    /// Resolve a qualified column without constructing an AST expression.
429    pub fn qualified_column_value(&self, qualifier: &str, column: &str) -> Result<Value> {
430        self.qualified_column_value_with_control(
431            qualifier,
432            column,
433            &ProductionControl::uncontrolled(),
434        )
435        .map(|value| {
436            value
437                .into_uncontrolled()
438                .expect("ordinary qualified column value")
439        })
440    }
441
442    /// Resolve a qualified slot with the same ambiguity and missing-value behavior under a retained output owner.
443    pub fn qualified_column_value_with_control(
444        &self,
445        qualifier: &str,
446        column: &str,
447        control: &ProductionControl<'_>,
448    ) -> Result<Produced<Value>> {
449        control.check()?;
450        let row = self.row_lookup()?;
451        if row.qualified_column_is_ambiguous(qualifier, column) {
452            return Err(SQLError::AmbiguousColumn(format!("{qualifier}.{column}")));
453        }
454        Ok(control.copy_value(
455            row.qualified_column(qualifier, column)
456                .unwrap_or(&Value::Null),
457        )?)
458    }
459}
460
461#[cfg(test)]
462mod production_tests;