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};
76mod cache;
77pub use cache::{analyze_for_statement_reuse, AnalyzedStatement};
78
79/// Analyze once and report whether the converted inputs can be reused by a later
80/// ordinary message. Prepared definitions use their separate creation lifetime.
81pub fn analyze_executable_plan_for_cache(
82    context: &StatementAnalysisContext<'_>,
83    plan: &mut UnifiedPlan,
84    params: &[SQLParam],
85) -> Result<(AnalyzedResult, bool), SQLError> {
86    analyze_reusable_inputs(context, plan, params)
87        .map(|(result, reusable)| (result, reusable && params.is_empty()))
88}
89
90fn analyze_reusable_inputs(
91    context: &StatementAnalysisContext<'_>,
92    plan: &mut UnifiedPlan,
93    params: &[SQLParam],
94) -> Result<(AnalyzedResult, bool), SQLError> {
95    let mut result = None;
96    let mut reusable = true;
97    context.scopes.with_scope(&mut |scope| {
98        result = Some(match plan {
99            UnifiedPlan::Command(command) => match command.as_mut() {
100                // The explained statement retains a scope of its own.
101                CommandPlan::Explain { body, .. } => {
102                    let (_, body_reusable) = analyze_reusable_inputs(context, body, params)?;
103                    reusable &= body_reusable;
104                    AnalyzedResult::Command
105                }
106                _ => analyze_plan_result_inner(
107                    context.routines,
108                    context.aliases,
109                    plan,
110                    params,
111                    scope,
112                    &mut reusable,
113                )?,
114            },
115            UnifiedPlan::Query(_) => analyze_plan_result_inner(
116                context.routines,
117                context.aliases,
118                plan,
119                params,
120                scope,
121                &mut reusable,
122            )?,
123        });
124        Ok(())
125    })?;
126    result
127        .map(|result| (result, reusable))
128        .ok_or_else(|| SQLError::Internal("statement analysis scope did not run".into()))
129}
130
131/// Analyze a statement in one binding scope, retain input-function results in its
132/// executable tree and derive its result without running statement expressions.
133pub fn analyze_plan_result(
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) -> Result<AnalyzedResult, SQLError> {
140    analyze_plan_result_inner(routines, aliases, plan, params, scope, &mut true)
141}
142
143fn analyze_plan_result_inner(
144    routines: &dyn RoutineResolution,
145    aliases: &dyn crate::schema::dependencies::oid_alias::OidAliasInput,
146    plan: &mut UnifiedPlan,
147    params: &[SQLParam],
148    scope: &dyn StatementBindingScope,
149    reusable: &mut bool,
150) -> Result<AnalyzedResult, SQLError> {
151    let binding = scope.binding_context()?;
152    *reusable &=
153        super::preparation::read_executable_inputs(routines, plan, params, &binding, aliases)?;
154    if let UnifiedPlan::Command(command) = plan {
155        if let CommandPlan::Explain { body, .. } = command.as_mut() {
156            analyze_plan_result_inner(routines, aliases, body, params, scope, reusable)?;
157            return Ok(AnalyzedResult::Command);
158        }
159    }
160    analyze_bound_result(routines, plan, params, &binding)
161}
162
163fn analyze_bound_result(
164    routines: &dyn RoutineResolution,
165    plan: &UnifiedPlan,
166    params: &[SQLParam],
167    binding: &BindingContext<'_>,
168) -> Result<AnalyzedResult, SQLError> {
169    let rows = AnalyzedResult::Schema;
170    match plan {
171        UnifiedPlan::Query(query) => {
172            super::analyze_query_plan_schema(routines, query, params, binding, None).map(rows)
173        }
174        UnifiedPlan::Command(command) => match command.as_ref() {
175            CommandPlan::CreateView { query, .. }
176            | CommandPlan::CreateTableAs { query, .. }
177            | CommandPlan::CreateMaterializedView { query, .. }
178            | CommandPlan::DeclareCursor { query, .. } => {
179                super::analyze_query_plan_schema(routines, query, params, binding, None)?;
180                Ok(AnalyzedResult::Command)
181            }
182            _ => Ok(
183                super::analyze_prepared_command_schema(routines, command, params, binding)?
184                    .map_or(AnalyzedResult::Command, rows),
185            ),
186        },
187    }
188}
189
190pub fn analyze_command_parameters(
191    routines: &dyn RoutineResolution,
192    command: &CommandPlan,
193    params: &[SQLParam],
194    scope: &dyn StatementBindingScope,
195) -> Result<(), SQLError> {
196    let declared = parameter_input_types(params)?;
197    super::infer_prepared_parameter_types(
198        routines,
199        &UnifiedPlan::Command(Box::new(command.clone())),
200        &declared,
201        &scope.binding_context()?,
202    )?;
203    Ok(())
204}
205
206pub(super) fn parameter_input_types(
207    params: &[SQLParam],
208) -> Result<Vec<Option<crate::ColumnType>>, SQLError> {
209    let schema = RowSchema::default();
210    (1..=params.len())
211        .map(|index| match &params[index - 1] {
212            SQLParam::Scalar(Value::Str(_) | Value::Null) => Ok(None),
213            _ => crate::scalar_type(&ScalarExpr::Param(index), &schema, params),
214        })
215        .collect()
216}
217
218#[cfg(test)]
219mod tests;