uqa-sql 0.4.0

PostgreSQL-compatible SQL compiler built on libpg_query
Documentation
//
// Unified Query Algebra
//
// Copyright (c) 2023-2026 Cognica, Inc.
//

//! Evaluation context, row lookup, and engine-backed type resolution.

use uqa_core::{
    memory::{Produced, ProductionControl},
    Value,
};

use crate::ast::{ColumnType, InternalColumnRef};
use crate::error::{Result, SQLError};
use crate::params::SQLParam;
use crate::result::ResultRow;

mod casting;
mod regtype;
pub use casting::{
    cast_value_with_type_resolution, cast_value_with_type_resolution_with_control,
    coercion_type_name,
};
pub use regtype::{format_regtype_value, format_regtype_value_with_control};

/// Engine-side hook that scalar function evaluation calls for stateful
/// sequence and user-defined functions. Query-valued expressions are not
/// accepted here: lowering assigns them physical query-plan slots executed by
/// `uqa-execution::ScalarSubqueryRunner`.
pub trait EngineHook {
    /// Start of the current SQL transaction, in Unix microseconds.
    fn transaction_timestamp_micros(&self) -> Option<i64> {
        None
    }

    /// Start of the current frontend SQL message, in Unix microseconds.
    fn statement_timestamp_micros(&self) -> Option<i64> {
        None
    }

    fn nextval(&self, name: &str) -> Result<i64>;
    fn currval(&self, name: &str) -> Result<i64>;
    fn lastval(&self) -> Result<i64> {
        Err(SQLError::Unsupported(
            "lastval requires an engine hook implementation".into(),
        ))
    }
    fn setval(&self, name: &str, value: i64, is_called: bool) -> Result<i64>;

    fn call_scalar_function(&self, _name: &str, _args: &[Value]) -> Option<Result<Value>> {
        None
    }

    /// Invoke an engine-backed built-in after an exact catalog binding has
    /// selected it. Unlike `call_scalar_function`, this path is also available
    /// when dynamic dispatch is disabled, so runtime callbacks cannot override
    /// the stored built-in identity.
    fn call_bound_builtin_function(
        &self,
        _binding: &crate::ast::FunctionBinding,
        _args: &[(Option<String>, Value)],
    ) -> Option<Result<Value>> {
        None
    }

    fn has_scalar_functions(&self) -> bool {
        true
    }

    /// Resolve a catalog-owned SQL type name for casts evaluated with an engine context.
    fn resolve_type_name(&self, _name: &str) -> std::result::Result<Option<ColumnType>, String> {
        Ok(None)
    }

    /// Apply catalog-owned domain conversion and constraints. A missing implementation leaves built-in catalog domains on their base-type conversion path.
    fn cast_domain(
        &self,
        _value: &Value,
        _source: Option<&str>,
        _target: &ColumnType,
    ) -> Result<Option<Value>> {
        Ok(None)
    }

    /// Resolve a regtype cast to its OID carrier when a complete type catalog is available.
    fn resolve_regtype_input(&self, _name: &str) -> Result<Option<i64>> {
        Ok(None)
    }

    /// Resolve a relation name to the OID carrier used by `regclass`.
    fn resolve_regclass(&self, _name: &str) -> std::result::Result<Option<i64>, String> {
        Ok(None)
    }

    /// 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.
    fn resolve_regclass_input(&self, name: &str) -> Result<Option<i64>> {
        self.resolve_regclass(name).map_err(SQLError::Internal)
    }

    /// Resolve an exact routine signature to the OID carrier used by `regprocedure`.
    fn resolve_regprocedure(&self, _name: &str) -> std::result::Result<Option<i64>, String> {
        Ok(None)
    }

    /// Resolve a `regrole` input while preserving hard input errors for direct casts.
    fn resolve_regrole(&self, _name: &str) -> Result<Option<i64>> {
        Ok(None)
    }

    /// Resolve a `regnamespace` input while preserving hard input errors for direct casts.
    fn resolve_regnamespace(&self, name: &str) -> Result<Option<i64>> {
        self.resolve_regobject(&ColumnType::Regnamespace, name)
    }

    /// 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`.
    fn resolve_regobject(&self, ty: &ColumnType, name: &str) -> Result<Option<i64>> {
        match ty {
            ColumnType::Regclass => self.resolve_regclass_input(name),
            ColumnType::Regprocedure => self.resolve_regprocedure(name).map_err(SQLError::Internal),
            ColumnType::Regrole => self.resolve_regrole(name),
            ColumnType::Regproc | ColumnType::Regnamespace | ColumnType::Regtype => Ok(None),
            _ => Err(SQLError::Internal(format!(
                "unsupported regobject lookup type `{}`",
                ty.sql_name()
            ))),
        }
    }

    /// Resolve one OID-backed alias type to its `PostgreSQL` text output.
    fn resolve_regtype_output(
        &self,
        _ty: &ColumnType,
        _oid: i64,
    ) -> std::result::Result<Option<String>, String> {
        Ok(None)
    }

    /// Resolve the first existing schema on the logical session's search
    /// path. `None` lets standalone expression evaluation use its `public`
    /// compatibility default.
    fn current_schema(&self) -> std::result::Result<Option<String>, String> {
        Ok(None)
    }

    fn current_user(&self) -> std::result::Result<Option<String>, crate::SQLError> {
        Ok(None)
    }

    fn session_user(&self) -> std::result::Result<Option<String>, crate::SQLError> {
        Ok(None)
    }

    /// Read a session setting. `None` means the parameter is unknown; errors must remain visible even for `current_setting(..., true)`.
    fn runtime_parameter(&self, _name: &str) -> Result<Option<String>> {
        Err(SQLError::Unsupported(
            "engine hook does not provide session settings".into(),
        ))
    }

    /// Resolve the existing schemas visible to the logical session.
    fn current_schemas(
        &self,
        _include_implicit: bool,
    ) -> std::result::Result<Option<Vec<String>>, String> {
        Ok(None)
    }

    /// Draw from an engine-owned logical-session PRNG. `None` keeps pure,
    /// engine-free expression evaluation available for library callers.
    fn random_value(&self) -> std::result::Result<Option<f64>, String> {
        Ok(None)
    }

    /// Draw every bit of one engine-owned logical-session PRNG word. Range
    /// functions use this instead of a floating-point sample so `bigint` and
    /// arbitrary-precision `numeric` bounds remain uniform.
    fn random_u64(&self) -> std::result::Result<Option<u64>, String> {
        Ok(None)
    }

    /// Reseed the logical-session PRNG. `false` means the hook does not own a
    /// mutable random stream and the caller must report the unsupported call.
    fn set_random_seed(&self, _seed: f64) -> std::result::Result<bool, String> {
        Ok(false)
    }

    /// Invoke a user-defined SQL / `PL/pgSQL` function. Consulted
    /// after built-in dispatch misses (and immediately for calls with
    /// named arguments, which built-ins never accept). `None` means
    /// no user-defined function with this name exists.
    fn call_user_function(
        &self,
        _name: &str,
        _args: &[(Option<String>, Value)],
    ) -> Option<Result<Value>> {
        None
    }

    fn call_bound_user_function(
        &self,
        _binding: &crate::ast::FunctionBinding,
        _args: &[(Option<String>, Value)],
    ) -> Option<Result<Value>> {
        None
    }
}

/// Read-only row interface used by the expression evaluator. Most callers
/// use a materialised [`ResultRow`], while hot execution paths can expose a
/// projected value slice without rebuilding a string-keyed map for every row.
pub trait RowLookup {
    fn column(&self, name: &str) -> Option<&Value>;

    /// Whether an unqualified name identifies more than one visible input
    /// column. Callers must report SQLSTATE 42702 instead of selecting an
    /// arbitrary suffix match.
    fn column_is_ambiguous(&self, _name: &str) -> bool {
        false
    }

    fn qualified_column(&self, qualifier: &str, column: &str) -> Option<&Value>;

    /// Whether a qualified identity names more than one visible input column.
    fn qualified_column_is_ambiguous(&self, _qualifier: &str, _column: &str) -> bool {
        false
    }

    /// Return a value by the physical schema position used to construct this
    /// row view. Materialized named rows do not expose positional access;
    /// projected execution sources override it so compiled hot paths can avoid
    /// repeating string lookup for every expression and row.
    fn positional_column(&self, _index: usize) -> Option<&Value> {
        None
    }

    /// Resolve an executor-only relation attribute. Materialized SQL rows do
    /// not expose these structural slots.
    fn internal_column(&self, _column: InternalColumnRef) -> Option<&Value> {
        None
    }

    /// 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.
    fn score_source(&self, _qualifier: Option<&str>) -> Option<&Value> {
        None
    }

    /// Whether the requested score source resolves to more than one retrieval relation.
    fn score_source_is_ambiguous(&self, _qualifier: Option<&str>) -> bool {
        false
    }

    /// Visit every logical column in schema order. Named rows use their map
    /// order; positional execution rows override this without materializing a
    /// map. The default keeps narrow projected lookup implementations source
    /// compatible when they deliberately do not expose whole-row semantics.
    fn visit_columns(&self, _visitor: &mut dyn FnMut(&str, &Value)) {}
}

impl RowLookup for ResultRow {
    fn column(&self, name: &str) -> Option<&Value> {
        self.get(name)
    }

    fn qualified_column(&self, _qualifier: &str, _column: &str) -> Option<&Value> {
        None
    }

    fn visit_columns(&self, visitor: &mut dyn FnMut(&str, &Value)) {
        for (column, value) in self {
            visitor(column, value);
        }
    }
}

pub struct EvalContext<'a> {
    pub row: Option<&'a ResultRow>,
    row_lookup: Option<&'a dyn RowLookup>,
    pub params: &'a [SQLParam],
    pub engine: Option<&'a dyn EngineHook>,
}

impl<'a> EvalContext<'a> {
    pub fn new(row: Option<&'a ResultRow>, params: &'a [SQLParam]) -> Self {
        Self {
            row,
            row_lookup: row.map(|row| row as &dyn RowLookup),
            params,
            engine: None,
        }
    }

    pub fn from_row_lookup(row: &'a dyn RowLookup, params: &'a [SQLParam]) -> Self {
        Self {
            // Whole-row materialization is needed only by correlated
            // subqueries. Ordinary scalar evaluation must remain on the
            // lookup/slot path.
            row: None,
            row_lookup: Some(row),
            params,
            engine: None,
        }
    }

    pub fn with_engine(mut self, engine: &'a dyn EngineHook) -> Self {
        self.engine = Some(engine);
        self
    }

    pub(super) fn row_lookup(&self) -> Result<&'a dyn RowLookup> {
        self.row_lookup
            .ok_or_else(|| SQLError::Internal("column reference without row context".into()))
    }

    /// Resolve an unqualified column through the same row semantics used by
    /// the AST evaluator. Physical scalar IR evaluators call this instead of
    /// reconstructing an [`Expr::Column`](crate::ast::Expr::Column) carrier.
    pub fn column_value(&self, name: &str) -> Result<Value> {
        self.column_value_with_control(name, &ProductionControl::uncontrolled())
            .map(|value| value.into_uncontrolled().expect("ordinary column value"))
    }

    /// Resolve the same row slot while its copied payload retains the caller's allowance and cancellation scopes.
    pub fn column_value_with_control(
        &self,
        name: &str,
        control: &ProductionControl<'_>,
    ) -> Result<Produced<Value>> {
        control.check()?;
        let row = self.row_lookup()?;
        if row.column_is_ambiguous(name) {
            return Err(SQLError::AmbiguousColumn(name.to_string()));
        }
        Ok(control.copy_value(row.column(name).unwrap_or(&Value::Null))?)
    }

    /// Resolve a qualified column without constructing an AST expression.
    pub fn qualified_column_value(&self, qualifier: &str, column: &str) -> Result<Value> {
        self.qualified_column_value_with_control(
            qualifier,
            column,
            &ProductionControl::uncontrolled(),
        )
        .map(|value| {
            value
                .into_uncontrolled()
                .expect("ordinary qualified column value")
        })
    }

    /// Resolve a qualified slot with the same ambiguity and missing-value behavior under a retained output owner.
    pub fn qualified_column_value_with_control(
        &self,
        qualifier: &str,
        column: &str,
        control: &ProductionControl<'_>,
    ) -> Result<Produced<Value>> {
        control.check()?;
        let row = self.row_lookup()?;
        if row.qualified_column_is_ambiguous(qualifier, column) {
            return Err(SQLError::AmbiguousColumn(format!("{qualifier}.{column}")));
        }
        Ok(control.copy_value(
            row.qualified_column(qualifier, column)
                .unwrap_or(&Value::Null),
        )?)
    }
}

#[cfg(test)]
mod production_tests;