use super::{BindingContext, QueryPlan, RowSchema, SQLError, SQLParam, ScalarExpr, SchemaScope};
use crate::ast::{ColumnType, InternalRelationId};
use crate::catalog::resolution::RelationLookupMode;
use crate::plan::{CommandPlan, ExpressionPlan, ProjectionPlan, UnifiedPlan};
use crate::routines::RoutineResolution;
use std::sync::LazyLock;
static ROUTINE_PARAMETERS: LazyLock<InternalRelationId> =
LazyLock::new(InternalRelationId::allocate);
#[derive(Clone)]
pub struct RoutineParameterScope {
function: String,
names: Vec<String>,
schema: RowSchema,
}
impl RoutineParameterScope {
#[must_use]
pub fn new(function: &str, names: Vec<String>, types: Vec<Option<ColumnType>>) -> Self {
let visible = RowSchema::with_qualified_types(function, names.clone(), types.clone());
let marked = RowSchema::with_internal_relation_types(*ROUTINE_PARAMETERS, types);
Self {
function: function.to_string(),
names,
schema: RowSchema::with_trailing_internal_aliases(&visible, &marked),
}
}
#[must_use]
pub const fn schema(&self) -> &RowSchema {
&self.schema
}
fn position_at(schema: &RowSchema, slot: usize) -> Option<usize> {
schema
.unique_internal_column_for_slot(slot)
.filter(|column| column.relation() == *ROUTINE_PARAMETERS)
.map(crate::ast::InternalColumnRef::attribute)
}
fn reaches(schema: &RowSchema) -> bool {
schema.internal_slot(ROUTINE_PARAMETERS.column(0)).is_some()
}
fn parameter(&self, expression: &ScalarExpr, schema: &RowSchema) -> Option<usize> {
match expression {
ScalarExpr::Column(name) => {
let position = Self::position_at(schema, schema.column_slot(name)?)?;
(!self.names_relation(schema, name)).then_some(position)
}
ScalarExpr::QualifiedColumn { qualifier, column } => {
match schema.qualified_slot(qualifier, column) {
Some(slot) => Self::position_at(schema, slot),
None if *qualifier == self.function && Self::reaches(schema) => self
.names
.iter()
.position(|name| !name.is_empty() && name == column),
None => None,
}
}
_ => None,
}
}
fn names_relation(&self, schema: &RowSchema, name: &str) -> bool {
schema.has_qualifier(name)
&& (name != self.function
|| !self.names.iter().any(|parameter| {
schema
.qualified_slot(name, parameter)
.and_then(|slot| Self::position_at(schema, slot))
.is_some()
}))
}
}
pub(super) fn column_labels(projections: &[ProjectionPlan]) -> Vec<Option<String>> {
projections
.iter()
.map(|projection| match &projection.expr {
ScalarExpr::Column(name) | ScalarExpr::QualifiedColumn { column: name, .. }
if projection.alias.is_none() =>
{
Some(name.clone())
}
_ => None,
})
.collect()
}
pub(super) fn keep_column_labels(projections: &mut [ProjectionPlan], labels: Vec<Option<String>>) {
for (projection, label) in projections.iter_mut().zip(labels) {
if let Some(label) = label {
if matches!(projection.expr, ScalarExpr::Param(_)) {
projection.alias = Some(label);
}
}
}
}
impl SchemaScope {
pub(super) fn routine_parameter_reference(
&self,
expression: &ScalarExpr,
schema: &RowSchema,
) -> Option<usize> {
self.routine_parameters
.as_ref()
.and_then(|parameters| parameters.parameter(expression, schema))
.map(|position| position + 1)
}
pub(super) fn canonicalize_routine_parameters(
&self,
expression: &mut ScalarExpr,
schema: &RowSchema,
) {
let Some(parameters) = self.routine_parameters.as_ref() else {
return;
};
crate::plan::rewrite_scalar_expression(expression, &mut |node| {
if let Some(position) = parameters.parameter(node, schema) {
*node = ScalarExpr::Param(position + 1);
}
});
}
pub(super) fn bind_statement_parameters(
&mut self,
routines: &dyn RoutineResolution,
plan: &mut UnifiedPlan,
params: &[SQLParam],
outer: Option<&RowSchema>,
) -> Result<(), SQLError> {
let command = match plan {
UnifiedPlan::Query(query) => {
return self.bind_query_parameters(routines, query, params, outer);
}
UnifiedPlan::Command(command) => command.as_mut(),
};
match command {
CommandPlan::Explain { body, .. } => {
self.bind_statement_parameters(routines, body, params, outer)
}
CommandPlan::CreateTableAs { query, .. }
| CommandPlan::CreateMaterializedView { query, .. }
| CommandPlan::DeclareCursor { query, .. } => {
self.bind_query_parameters(routines, query, params, outer)
}
CommandPlan::Call { args, .. } => {
for argument in args {
self.bind_expression_parameters(routines, argument, params, outer)?;
}
Ok(())
}
command if command.mutation_target().is_some() => {
self.set_command_lookup_mode(command);
self.bind_command_routines_for_storage(routines, command, params, outer)
}
_ => Ok(()),
}
}
fn bind_query_parameters(
&mut self,
routines: &dyn RoutineResolution,
query: &mut QueryPlan,
params: &[SQLParam],
outer: Option<&RowSchema>,
) -> Result<(), SQLError> {
let previous = self.resolution.set_lookup_mode(if query.relations_bound {
RelationLookupMode::Bound
} else {
RelationLookupMode::Dynamic
});
let result = self
.bind_query_routines_for_storage(routines, query, params, outer)
.map(|_| ());
self.resolution.set_lookup_mode(previous);
result
}
fn bind_expression_parameters(
&mut self,
routines: &dyn RoutineResolution,
expression: &mut ExpressionPlan,
params: &[SQLParam],
outer: Option<&RowSchema>,
) -> Result<(), SQLError> {
for subquery in &mut expression.subqueries {
self.bind_query_parameters(routines, subquery, params, outer)?;
}
let schema = outer.cloned().unwrap_or_default();
self.canonicalize_routine_parameters(&mut expression.scalar, &schema);
self.resolve_variable_sites(&mut expression.scalar, &schema);
Ok(())
}
}
pub fn bind_routine_parameter_references(
routines: &dyn RoutineResolution,
plan: &mut UnifiedPlan,
params: &[SQLParam],
ctes: &BindingContext,
parameters: &RoutineParameterScope,
) -> Result<(), SQLError> {
let mut scope = SchemaScope::for_analysis(ctes)?;
scope.routine_parameters = Some(parameters.clone());
scope.binds_routine_identities = false;
scope.preserve_syntax_shape = true;
scope.bind_statement_parameters(routines, plan, params, Some(parameters.schema()))
}