Skip to main content

uqa_sql/prepared/
definition.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Analyze prepared declarations with separate retained inference and descriptor scopes.
8
9use super::dependencies::{PreparedAnalysisDependencies, PreparedDependencySnapshot};
10use crate::{
11    binding::statements::{StatementAnalysisScopes, StatementBindingScope},
12    catalog::resolution::EffectiveSearchPath,
13    plan::UnifiedPlan,
14    routines::RoutineResolution,
15    ColumnType, FunctionTypeResolver, RowSchema, SQLError,
16};
17
18#[derive(Clone, Copy)]
19pub struct PreparedDefinitionContext<'a> {
20    pub types: &'a dyn FunctionTypeResolver,
21    pub routines: &'a dyn RoutineResolution,
22    pub scopes: &'a dyn StatementAnalysisScopes,
23    /// The catalog the `reg*` input functions read object names from when the statement is analyzed.
24    pub aliases: &'a dyn crate::schema::dependencies::oid_alias::OidAliasInput,
25}
26
27pub struct PreparedDefinition {
28    pub logical_plan: UnifiedPlan,
29    pub parameter_types: Vec<Option<ColumnType>>,
30    pub result_schema: Option<RowSchema>,
31    pub effective_search_path: Option<EffectiveSearchPath>,
32    pub dependencies: PreparedAnalysisDependencies,
33    pub dependency_snapshot: Option<PreparedDependencySnapshot>,
34}
35
36pub fn analyze_definition(
37    context: &PreparedDefinitionContext<'_>,
38    mut logical_plan: UnifiedPlan,
39    declared: &[ColumnType],
40) -> Result<PreparedDefinition, SQLError> {
41    let parameter_types =
42        super::declared_parameter_types(context.types, &mut logical_plan, declared)?;
43    let (input, effective_search_path, dependency_snapshot) =
44        with_scope_result(context.scopes, |scope| {
45            let binding = scope.binding_context()?;
46            let mut input = crate::binding::read_prepared_inputs(
47                context.routines,
48                &mut logical_plan,
49                &parameter_types,
50                &binding,
51                Some(context.aliases),
52            )?;
53            crate::schema::dependencies::oid_alias::read_prepared_oid_alias_constants(
54                context.aliases,
55                &mut logical_plan,
56            )?;
57            logical_plan.visit_scalar_expressions(&mut |expression| {
58                input.dependencies.include_expression(expression);
59            });
60            let effective_search_path =
61                binding.catalog.effective_search_path(&binding.resolution)?;
62            let dependency_snapshot = binding
63                .catalog
64                .prepared_dependency_snapshot(&input.dependencies)?;
65            Ok((input, effective_search_path, dependency_snapshot))
66        })?;
67    let result_schema = analyze_result_schema(context, &logical_plan, &input.parameter_types)?;
68    Ok(PreparedDefinition {
69        logical_plan,
70        parameter_types: input.parameter_types,
71        result_schema,
72        effective_search_path,
73        dependencies: input.dependencies,
74        dependency_snapshot,
75    })
76}
77
78/// Check analysis dependencies in one retained namespace. Catalogs that supplied no path or revision capability at analysis leave that part of freshness to their owner's invalidation events.
79pub fn analysis_is_current(
80    context: &PreparedDefinitionContext<'_>,
81    retained_path: Option<&EffectiveSearchPath>,
82    dependencies: &PreparedAnalysisDependencies,
83    retained_snapshot: Option<&PreparedDependencySnapshot>,
84) -> Result<bool, SQLError> {
85    if retained_path.is_none() && retained_snapshot.is_none() {
86        return Ok(true);
87    }
88    with_scope_result(context.scopes, |scope| {
89        let binding = scope.binding_context()?;
90        if let Some(path) = retained_path {
91            if binding
92                .catalog
93                .effective_search_path(&binding.resolution)?
94                .as_ref()
95                != Some(path)
96            {
97                return Ok(false);
98            }
99        }
100        match retained_snapshot {
101            Some(snapshot) => Ok(binding
102                .catalog
103                .prepared_dependency_snapshot(dependencies)?
104                .as_ref()
105                == Some(snapshot)),
106            None => Ok(true),
107        }
108    })
109}
110
111/// Read the live namespace through one retained binding scope before selecting a previously analyzed prepared statement.
112pub fn effective_search_path(
113    context: &PreparedDefinitionContext<'_>,
114) -> Result<Option<EffectiveSearchPath>, SQLError> {
115    with_scope_result(context.scopes, |scope| {
116        let binding = scope.binding_context()?;
117        binding.catalog.effective_search_path(&binding.resolution)
118    })
119}
120
121pub fn analyze_result_schema(
122    context: &PreparedDefinitionContext<'_>,
123    logical_plan: &UnifiedPlan,
124    parameter_types: &[Option<ColumnType>],
125) -> Result<Option<RowSchema>, SQLError> {
126    with_scope_result(context.scopes, |scope| {
127        super::analyze_prepared_plan(
128            context.routines,
129            logical_plan,
130            parameter_types,
131            &scope.binding_context()?,
132        )
133    })
134}
135
136fn with_scope_result<T>(
137    scopes: &dyn StatementAnalysisScopes,
138    mut analyze: impl FnMut(&dyn StatementBindingScope) -> Result<T, SQLError>,
139) -> Result<T, SQLError> {
140    let mut result = None;
141    scopes.with_scope(&mut |scope| {
142        result = Some(analyze(scope)?);
143        Ok(())
144    })?;
145    result.ok_or_else(|| {
146        SQLError::Internal("prepared analysis scope did not invoke its operation".into())
147    })
148}
149
150#[cfg(test)]
151mod tests;