uqa-engine 0.1.11

Engine: schema-aware table store, catalog restore, transactions
//
// Unified Query Algebra
//
// Copyright (c) 2023-2026 Cognica, Inc.
//

//! Recursive scalar and subquery reference validation.

use super::super::{Engine, SQLError, SQLParam, ScalarExpr};
use super::functions::{
    is_semantic_all_argument, validate_qualified_column, validate_scalar_function,
    validate_unqualified_column, validate_window_function, ScalarFunctionValidation,
};
use uqa_execution::{FunctionTypeResolver, RowSchema, ScalarFrameBound};

pub(super) fn validate_expression(
    engine: &Engine,
    expression: &ScalarExpr,
    schema: &RowSchema,
    fallback: Option<&RowSchema>,
    params: &[SQLParam],
    resolver: &dyn FunctionTypeResolver,
) -> Result<(), SQLError> {
    match expression {
        ScalarExpr::Column(column) => validate_unqualified_column(schema, fallback, column),
        ScalarExpr::Position(position) => {
            if *position < schema.len() {
                Ok(())
            } else {
                Err(SQLError::UnknownColumn((position + 1).to_string()))
            }
        }
        ScalarExpr::InternalColumn(column) => {
            if schema.internal_slot(*column).is_some()
                || fallback.is_some_and(|fallback| fallback.internal_slot(*column).is_some())
            {
                Ok(())
            } else {
                Err(SQLError::Internal(format!(
                    "internal relation attribute {column:?} is outside the bound row scope"
                )))
            }
        }
        ScalarExpr::QualifiedColumn { qualifier, column } => {
            validate_qualified_column(schema, fallback, qualifier, column)
        }
        ScalarExpr::QualifiedStar(qualifier) => {
            if schema.has_qualifier(qualifier)
                || fallback.is_some_and(|fallback| fallback.has_qualifier(qualifier))
            {
                Ok(())
            } else {
                Err(SQLError::UnknownTable(qualifier.clone()))
            }
        }
        ScalarExpr::Func {
            name,
            binding,
            args,
            order_by,
            filter,
            ..
        } => {
            for argument in args {
                if is_semantic_all_argument(name, argument) {
                    continue;
                }
                validate_expression(engine, argument, schema, None, params, resolver)?;
            }
            for order in order_by {
                validate_expression(engine, &order.expr, schema, None, params, resolver)?;
            }
            if let Some(filter) = filter.as_deref() {
                validate_expression(engine, filter, schema, None, params, resolver)?;
            }
            validate_scalar_function(
                engine,
                ScalarFunctionValidation {
                    name,
                    binding: binding.as_ref(),
                    args,
                    order_by,
                    expression,
                    schema,
                    params,
                    resolver,
                },
            )
        }
        ScalarExpr::WindowCall { name, args, spec } => {
            for argument in args {
                validate_expression(engine, argument, schema, None, params, resolver)?;
            }
            for expression in &spec.partition_by {
                validate_expression(engine, expression, schema, None, params, resolver)?;
            }
            for order in &spec.order_by {
                validate_expression(engine, &order.expr, schema, None, params, resolver)?;
            }
            if let Some(frame) = &spec.frame {
                for bound in [&frame.start, &frame.end] {
                    if let ScalarFrameBound::Preceding(expression)
                    | ScalarFrameBound::Following(expression) = bound
                    {
                        validate_expression(engine, expression, schema, None, params, resolver)?;
                    }
                }
            }
            validate_window_function(engine, name, args, schema, params, resolver)
        }
        ScalarExpr::Array(items)
        | ScalarExpr::Row(items)
        | ScalarExpr::And(items)
        | ScalarExpr::Or(items) => validate_items(engine, items, schema, params, resolver),
        ScalarExpr::Binary { lhs, rhs, .. } => {
            validate_expression(engine, lhs, schema, None, params, resolver)?;
            validate_expression(engine, rhs, schema, None, params, resolver)
        }
        ScalarExpr::UnaryMinus(inner)
        | ScalarExpr::Not(inner)
        | ScalarExpr::IsNull { expr: inner, .. }
        | ScalarExpr::Cast { expr: inner, .. } => {
            validate_expression(engine, inner, schema, None, params, resolver)
        }
        ScalarExpr::Between { expr, low, high } => {
            for item in [expr.as_ref(), low.as_ref(), high.as_ref()] {
                validate_expression(engine, item, schema, None, params, resolver)?;
            }
            Ok(())
        }
        ScalarExpr::InList { expr, list, .. } => {
            validate_expression(engine, expr, schema, None, params, resolver)?;
            validate_items(engine, list, schema, params, resolver)
        }
        ScalarExpr::Case {
            base,
            when,
            else_branch,
        } => {
            if let Some(base) = base.as_deref() {
                validate_expression(engine, base, schema, None, params, resolver)?;
            }
            for (condition, value) in when {
                validate_expression(engine, condition, schema, None, params, resolver)?;
                validate_expression(engine, value, schema, None, params, resolver)?;
            }
            if let Some(else_branch) = else_branch.as_deref() {
                validate_expression(engine, else_branch, schema, None, params, resolver)?;
            }
            Ok(())
        }
        ScalarExpr::ScalarSubquery(index)
        | ScalarExpr::Exists {
            subquery: index, ..
        } => resolver
            .resolve_scalar_subquery_type(*index, schema, params)
            .map(drop),
        ScalarExpr::InSubquery { expr, subquery, .. } => {
            validate_expression(engine, expr, schema, None, params, resolver)?;
            resolver
                .resolve_scalar_subquery_type(*subquery, schema, params)
                .map(drop)
        }
        ScalarExpr::Star | ScalarExpr::Default | ScalarExpr::Literal(_) | ScalarExpr::Param(_) => {
            Ok(())
        }
    }
}

fn validate_items(
    engine: &Engine,
    items: &[ScalarExpr],
    schema: &RowSchema,
    params: &[SQLParam],
    resolver: &dyn FunctionTypeResolver,
) -> Result<(), SQLError> {
    for item in items {
        validate_expression(engine, item, schema, None, params, resolver)?;
    }
    Ok(())
}