Skip to main content

uqa_sql/binding/
statements.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Executable statement schemas and mutation parameter analysis.
8
9use super::BindingContext;
10use crate::{
11    plan::{CommandPlan, UnifiedPlan},
12    routines::RoutineResolution,
13    RowSchema, SQLError, SQLParam, ScalarExpr,
14};
15use uqa_core::Value;
16
17/// Borrow binding inputs only when the statement's semantic branch requires them.
18pub trait StatementBindingScope {
19    fn binding_context(&self) -> Result<BindingContext<'_>, SQLError>;
20}
21pub type StatementAnalysisOperation<'a> =
22    &'a mut dyn FnMut(&dyn StatementBindingScope) -> Result<(), SQLError>;
23
24/// Retain a fresh catalog, namespace and transition scope for each statement analysis.
25pub trait StatementAnalysisScopes {
26    fn with_scope(&self, analyze: StatementAnalysisOperation<'_>) -> Result<(), SQLError>;
27}
28pub struct StatementAnalysisContext<'a> {
29    pub scopes: &'a dyn StatementAnalysisScopes,
30    pub routines: &'a dyn RoutineResolution,
31    pub aliases: &'a dyn crate::schema::dependencies::oid_alias::OidAliasInput,
32}
33
34/// The result a statement's analysis derives: the column types of a query or of a data-modifying statement's `RETURNING` list, `None` where analysis derives no type, or no result for any other statement.
35#[derive(Debug, Clone, PartialEq)]
36pub enum AnalyzedResult {
37    Rows(Vec<Option<crate::ColumnType>>),
38    /// An analyzed output whose anonymous records retain their field descriptors.
39    Schema(RowSchema),
40    Command,
41}
42
43impl AnalyzedResult {
44    pub fn column_types(&self) -> Option<&[Option<crate::ColumnType>]> {
45        match self {
46            Self::Rows(types) => Some(types),
47            Self::Schema(schema) => Some(schema.column_types()),
48            Self::Command => None,
49        }
50    }
51
52    pub fn record_fields(&self, column: usize) -> Option<&crate::schema::RecordFields> {
53        match self {
54            Self::Schema(schema) => schema.record_fields(column),
55            _ => None,
56        }
57    }
58}
59
60impl StatementBindingScope for super::snapshot::BindingSnapshot {
61    fn binding_context(&self) -> Result<BindingContext<'_>, SQLError> {
62        Ok(self.context())
63    }
64}
65
66pub fn analyze_executable_plan(
67    context: &StatementAnalysisContext<'_>,
68    plan: &mut UnifiedPlan,
69    params: &[SQLParam],
70) -> Result<AnalyzedResult, SQLError> {
71    analyze_executable_plan_for_cache(context, plan, params).map(|(result, _)| result)
72}
73
74mod procedural;
75pub use procedural::{analyze_procedural_plan, ProceduralPlanAnalysis};
76
77/// Analyze once and report whether the converted inputs can be reused by a later
78/// ordinary message. Prepared definitions use their separate creation lifetime.
79pub fn analyze_executable_plan_for_cache(
80    context: &StatementAnalysisContext<'_>,
81    plan: &mut UnifiedPlan,
82    params: &[SQLParam],
83) -> Result<(AnalyzedResult, bool), SQLError> {
84    let mut result = None;
85    let mut reusable = params.is_empty();
86    context.scopes.with_scope(&mut |scope| {
87        result = Some(match plan {
88            UnifiedPlan::Command(command) => match command.as_mut() {
89                // The explained statement retains a scope of its own.
90                CommandPlan::Explain { body, .. } => {
91                    let (_, body_reusable) =
92                        analyze_executable_plan_for_cache(context, body, params)?;
93                    reusable &= body_reusable;
94                    AnalyzedResult::Command
95                }
96                _ => analyze_plan_result_inner(
97                    context.routines,
98                    context.aliases,
99                    plan,
100                    params,
101                    scope,
102                    &mut reusable,
103                )?,
104            },
105            UnifiedPlan::Query(_) => analyze_plan_result_inner(
106                context.routines,
107                context.aliases,
108                plan,
109                params,
110                scope,
111                &mut reusable,
112            )?,
113        });
114        Ok(())
115    })?;
116    result
117        .map(|result| (result, reusable))
118        .ok_or_else(|| SQLError::Internal("statement analysis scope did not run".into()))
119}
120
121/// Analyze a statement in one binding scope, retain input-function results in its
122/// executable tree and derive its result without running statement expressions.
123pub fn analyze_plan_result(
124    routines: &dyn RoutineResolution,
125    aliases: &dyn crate::schema::dependencies::oid_alias::OidAliasInput,
126    plan: &mut UnifiedPlan,
127    params: &[SQLParam],
128    scope: &dyn StatementBindingScope,
129) -> Result<AnalyzedResult, SQLError> {
130    analyze_plan_result_inner(routines, aliases, plan, params, scope, &mut true)
131}
132
133fn analyze_plan_result_inner(
134    routines: &dyn RoutineResolution,
135    aliases: &dyn crate::schema::dependencies::oid_alias::OidAliasInput,
136    plan: &mut UnifiedPlan,
137    params: &[SQLParam],
138    scope: &dyn StatementBindingScope,
139    reusable: &mut bool,
140) -> Result<AnalyzedResult, SQLError> {
141    let binding = scope.binding_context()?;
142    *reusable &=
143        super::preparation::read_executable_inputs(routines, plan, params, &binding, aliases)?;
144    if let UnifiedPlan::Command(command) = plan {
145        if let CommandPlan::Explain { body, .. } = command.as_mut() {
146            analyze_plan_result_inner(routines, aliases, body, params, scope, reusable)?;
147            return Ok(AnalyzedResult::Command);
148        }
149    }
150    analyze_bound_result(routines, plan, params, &binding)
151}
152
153fn analyze_bound_result(
154    routines: &dyn RoutineResolution,
155    plan: &UnifiedPlan,
156    params: &[SQLParam],
157    binding: &BindingContext<'_>,
158) -> Result<AnalyzedResult, SQLError> {
159    let rows = AnalyzedResult::Schema;
160    match plan {
161        UnifiedPlan::Query(query) => {
162            super::analyze_query_plan_schema(routines, query, params, binding, None).map(rows)
163        }
164        UnifiedPlan::Command(command) => match command.as_ref() {
165            CommandPlan::CreateView { query, .. }
166            | CommandPlan::CreateTableAs { query, .. }
167            | CommandPlan::CreateMaterializedView { query, .. }
168            | CommandPlan::DeclareCursor { query, .. } => {
169                super::analyze_query_plan_schema(routines, query, params, binding, None)?;
170                Ok(AnalyzedResult::Command)
171            }
172            _ => Ok(
173                super::analyze_prepared_command_schema(routines, command, params, binding)?
174                    .map_or(AnalyzedResult::Command, rows),
175            ),
176        },
177    }
178}
179
180pub fn analyze_command_parameters(
181    routines: &dyn RoutineResolution,
182    command: &CommandPlan,
183    params: &[SQLParam],
184    scope: &dyn StatementBindingScope,
185) -> Result<(), SQLError> {
186    let declared = parameter_input_types(params)?;
187    super::infer_prepared_parameter_types(
188        routines,
189        &UnifiedPlan::Command(Box::new(command.clone())),
190        &declared,
191        &scope.binding_context()?,
192    )?;
193    Ok(())
194}
195
196pub(super) fn parameter_input_types(
197    params: &[SQLParam],
198) -> Result<Vec<Option<crate::ColumnType>>, SQLError> {
199    let schema = RowSchema::default();
200    (1..=params.len())
201        .map(|index| match &params[index - 1] {
202            SQLParam::Scalar(Value::Str(_) | Value::Null) => Ok(None),
203            _ => crate::scalar_type(&ScalarExpr::Param(index), &schema, params),
204        })
205        .collect()
206}
207
208#[cfg(test)]
209mod tests;