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