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    /// Apply catalog-owned domain conversion and constraints. A missing implementation leaves built-in catalog domains on their base-type conversion path.
95    fn cast_domain(
96        &self,
97        _value: &Value,
98        _source: Option<&str>,
99        _target: &ColumnType,
100    ) -> Result<Option<Value>> {
101        Ok(None)
102    }
103
104    /// Resolve a regtype cast to its OID carrier when a complete type catalog is available.
105    fn resolve_regtype_input(&self, _name: &str) -> Result<Option<i64>> {
106        Ok(None)
107    }
108
109    /// Resolve a relation name to the OID carrier used by `regclass`.
110    fn resolve_regclass(&self, _name: &str) -> std::result::Result<Option<i64>, String> {
111        Ok(None)
112    }
113
114    /// 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.
115    fn resolve_regclass_input(&self, name: &str) -> Result<Option<i64>> {
116        self.resolve_regclass(name).map_err(SQLError::Internal)
117    }
118
119    /// Resolve a routine name that must name exactly one routine, as `regproc` input does.
120    fn resolve_regproc(&self, _name: &str) -> Result<Option<i64>> {
121        Ok(None)
122    }
123
124    /// Resolve an exact routine signature to the OID carrier used by `regprocedure`.
125    fn resolve_regprocedure(&self, _name: &str) -> std::result::Result<Option<i64>, String> {
126        Ok(None)
127    }
128
129    /// Resolve `regprocedure` input while preserving the typed errors `regprocedurein` reports. Embedders that implement only the historical lookup hook keep its behavior.
130    fn resolve_regprocedure_input(&self, name: &str) -> Result<Option<i64>> {
131        self.resolve_regprocedure(name).map_err(SQLError::Internal)
132    }
133
134    /// Resolve a `regrole` input while preserving hard input errors for direct casts.
135    fn resolve_regrole(&self, _name: &str) -> Result<Option<i64>> {
136        Ok(None)
137    }
138
139    /// Resolve a `regnamespace` input while preserving hard input errors for direct casts.
140    fn resolve_regnamespace(&self, name: &str) -> Result<Option<i64>> {
141        self.resolve_regobject(&ColumnType::Regnamespace, name)
142    }
143
144    /// 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`.
145    fn resolve_regobject(&self, ty: &ColumnType, name: &str) -> Result<Option<i64>> {
146        match ty {
147            ColumnType::Regclass => self.resolve_regclass_input(name),
148            ColumnType::Regprocedure => self.resolve_regprocedure(name).map_err(SQLError::Internal),
149            ColumnType::Regrole => self.resolve_regrole(name),
150            ColumnType::Regproc | ColumnType::Regnamespace | ColumnType::Regtype => Ok(None),
151            _ => Err(SQLError::Internal(format!(
152                "unsupported regobject lookup type `{}`",
153                ty.sql_name()
154            ))),
155        }
156    }
157
158    /// Resolve one OID-backed alias type to its `PostgreSQL` text output.
159    fn resolve_regtype_output(
160        &self,
161        _ty: &ColumnType,
162        _oid: i64,
163    ) -> std::result::Result<Option<String>, String> {
164        Ok(None)
165    }
166
167    /// 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.
168    fn current_schema(&self) -> std::result::Result<Option<String>, String> {
169        Ok(Some("public".into()))
170    }
171
172    fn current_user(&self) -> std::result::Result<Option<String>, crate::SQLError> {
173        Ok(None)
174    }
175
176    fn session_user(&self) -> std::result::Result<Option<String>, crate::SQLError> {
177        Ok(None)
178    }
179
180    /// Read a session setting. `None` means the parameter is unknown; errors must remain visible even for `current_setting(..., true)`.
181    fn runtime_parameter(&self, _name: &str) -> Result<Option<String>> {
182        Err(SQLError::Unsupported(
183            "engine hook does not provide session settings".into(),
184        ))
185    }
186
187    /// Sleep in the session for `duration`, ending at once when the statement is canceled or times out (`pg_sleep`).
188    fn sleep(&self, _duration: std::time::Duration) -> Result<()> {
189        Err(SQLError::Unsupported(
190            "engine hook does not provide session sleeps".into(),
191        ))
192    }
193
194    /// 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.
195    fn set_runtime_parameter(
196        &self,
197        _name: &str,
198        _value: Option<&str>,
199        _local: bool,
200    ) -> Result<String> {
201        Err(SQLError::Unsupported(
202            "engine hook does not provide session settings".into(),
203        ))
204    }
205
206    /// Resolve the existing schemas visible to the logical session.
207    fn current_schemas(
208        &self,
209        _include_implicit: bool,
210    ) -> std::result::Result<Option<Vec<String>>, String> {
211        Ok(None)
212    }
213
214    /// Draw from an engine-owned logical-session PRNG. `None` keeps pure,
215    /// engine-free expression evaluation available for library callers.
216    fn random_value(&self) -> std::result::Result<Option<f64>, String> {
217        Ok(None)
218    }
219
220    /// Draw every bit of one engine-owned logical-session PRNG word. Range
221    /// functions use this instead of a floating-point sample so `bigint` and
222    /// arbitrary-precision `numeric` bounds remain uniform.
223    fn random_u64(&self) -> std::result::Result<Option<u64>, String> {
224        Ok(None)
225    }
226
227    /// Reseed the logical-session PRNG. `false` means the hook does not own a
228    /// mutable random stream and the caller must report the unsupported call.
229    fn set_random_seed(&self, _seed: f64) -> std::result::Result<bool, String> {
230        Ok(false)
231    }
232
233    /// Invoke a user-defined SQL / `PL/pgSQL` function. Consulted
234    /// after built-in dispatch misses (and immediately for calls with
235    /// named arguments, which built-ins never accept). `None` means
236    /// no user-defined function with this name exists.
237    fn call_user_function(
238        &self,
239        _name: &str,
240        _args: &[(Option<String>, Value)],
241    ) -> Option<Result<Value>> {
242        None
243    }
244
245    fn call_bound_user_function(
246        &self,
247        _binding: &crate::ast::FunctionBinding,
248        _args: &[(Option<String>, Value)],
249    ) -> Option<Result<Value>> {
250        None
251    }
252}
253
254/// Read-only row interface used by the expression evaluator. Most callers
255/// use a materialised [`ResultRow`], while hot execution paths can expose a
256/// projected value slice without rebuilding a string-keyed map for every row.
257pub trait RowLookup {
258    fn column(&self, name: &str) -> Option<&Value>;
259
260    /// Whether an unqualified name identifies more than one visible input
261    /// column. Callers must report SQLSTATE 42702 instead of selecting an
262    /// arbitrary suffix match.
263    fn column_is_ambiguous(&self, _name: &str) -> bool {
264        false
265    }
266
267    fn qualified_column(&self, qualifier: &str, column: &str) -> Option<&Value>;
268
269    /// Whether a qualified identity names more than one visible input column.
270    fn qualified_column_is_ambiguous(&self, _qualifier: &str, _column: &str) -> bool {
271        false
272    }
273
274    /// Return a value by the physical schema position used to construct this
275    /// row view. Materialized named rows do not expose positional access;
276    /// projected execution sources override it so compiled hot paths can avoid
277    /// repeating string lookup for every expression and row.
278    fn positional_column(&self, _index: usize) -> Option<&Value> {
279        None
280    }
281
282    /// Resolve an executor-only relation attribute. Materialized SQL rows do
283    /// not expose these structural slots.
284    fn internal_column(&self, _column: InternalColumnRef) -> Option<&Value> {
285        None
286    }
287
288    /// 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.
289    fn score_source(&self, _qualifier: Option<&str>) -> Option<&Value> {
290        None
291    }
292
293    /// Whether the requested score source resolves to more than one retrieval relation.
294    fn score_source_is_ambiguous(&self, _qualifier: Option<&str>) -> bool {
295        false
296    }
297
298    /// Visit every logical column in schema order. Named rows use their map
299    /// order; positional execution rows override this without materializing a
300    /// map. The default keeps narrow projected lookup implementations source
301    /// compatible when they deliberately do not expose whole-row semantics.
302    fn visit_columns(&self, _visitor: &mut dyn FnMut(&str, &Value)) {}
303}
304
305impl RowLookup for ResultRow {
306    fn column(&self, name: &str) -> Option<&Value> {
307        self.get(name)
308    }
309
310    fn qualified_column(&self, _qualifier: &str, _column: &str) -> Option<&Value> {
311        None
312    }
313
314    fn visit_columns(&self, visitor: &mut dyn FnMut(&str, &Value)) {
315        for (column, value) in self {
316            visitor(column, value);
317        }
318    }
319}
320
321pub struct EvalContext<'a> {
322    pub row: Option<&'a ResultRow>,
323    row_lookup: Option<&'a dyn RowLookup>,
324    pub params: &'a [SQLParam],
325    pub engine: Option<&'a dyn EngineHook>,
326}
327
328impl<'a> EvalContext<'a> {
329    pub fn new(row: Option<&'a ResultRow>, params: &'a [SQLParam]) -> Self {
330        Self {
331            row,
332            row_lookup: row.map(|row| row as &dyn RowLookup),
333            params,
334            engine: None,
335        }
336    }
337
338    pub fn from_row_lookup(row: &'a dyn RowLookup, params: &'a [SQLParam]) -> Self {
339        Self {
340            // Whole-row materialization is needed only by correlated
341            // subqueries. Ordinary scalar evaluation must remain on the
342            // lookup/slot path.
343            row: None,
344            row_lookup: Some(row),
345            params,
346            engine: None,
347        }
348    }
349
350    pub fn with_engine(mut self, engine: &'a dyn EngineHook) -> Self {
351        self.engine = Some(engine);
352        self
353    }
354
355    pub(super) fn row_lookup(&self) -> Result<&'a dyn RowLookup> {
356        self.row_lookup
357            .ok_or_else(|| SQLError::Internal("column reference without row context".into()))
358    }
359
360    /// Resolve an unqualified column through the same row semantics used by
361    /// the AST evaluator. Physical scalar IR evaluators call this instead of
362    /// reconstructing an [`Expr::Column`](crate::ast::Expr::Column) carrier.
363    pub fn column_value(&self, name: &str) -> Result<Value> {
364        self.column_value_with_control(name, &ProductionControl::uncontrolled())
365            .map(|value| value.into_uncontrolled().expect("ordinary column value"))
366    }
367
368    /// Resolve the same row slot while its copied payload retains the caller's allowance and cancellation scopes.
369    pub fn column_value_with_control(
370        &self,
371        name: &str,
372        control: &ProductionControl<'_>,
373    ) -> Result<Produced<Value>> {
374        control.check()?;
375        let row = self.row_lookup()?;
376        if row.column_is_ambiguous(name) {
377            return Err(SQLError::AmbiguousColumn(name.to_string()));
378        }
379        Ok(control.copy_value(row.column(name).unwrap_or(&Value::Null))?)
380    }
381
382    /// Resolve a qualified column without constructing an AST expression.
383    pub fn qualified_column_value(&self, qualifier: &str, column: &str) -> Result<Value> {
384        self.qualified_column_value_with_control(
385            qualifier,
386            column,
387            &ProductionControl::uncontrolled(),
388        )
389        .map(|value| {
390            value
391                .into_uncontrolled()
392                .expect("ordinary qualified column value")
393        })
394    }
395
396    /// Resolve a qualified slot with the same ambiguity and missing-value behavior under a retained output owner.
397    pub fn qualified_column_value_with_control(
398        &self,
399        qualifier: &str,
400        column: &str,
401        control: &ProductionControl<'_>,
402    ) -> Result<Produced<Value>> {
403        control.check()?;
404        let row = self.row_lookup()?;
405        if row.qualified_column_is_ambiguous(qualifier, column) {
406            return Err(SQLError::AmbiguousColumn(format!("{qualifier}.{column}")));
407        }
408        Ok(control.copy_value(
409            row.qualified_column(qualifier, column)
410                .unwrap_or(&Value::Null),
411        )?)
412    }
413}
414
415#[cfg(test)]
416mod production_tests;