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 composite_inputs: super::composites::CompositeInputs,
29    pub logical_plan: UnifiedPlan,
30    pub parameter_types: Vec<Option<ColumnType>>,
31    pub result_schema: Option<RowSchema>,
32    pub effective_search_path: Option<EffectiveSearchPath>,
33    pub dependencies: PreparedAnalysisDependencies,
34    pub dependency_snapshot: Option<PreparedDependencySnapshot>,
35}
36
37pub fn analyze_definition(
38    context: &PreparedDefinitionContext<'_>,
39    mut logical_plan: UnifiedPlan,
40    declared: &[ColumnType],
41) -> Result<PreparedDefinition, SQLError> {
42    let parameter_types =
43        super::declared_parameter_types(context.types, &mut logical_plan, declared)?;
44    let (input, effective_search_path, dependency_snapshot) =
45        with_scope_result(context.scopes, |scope| {
46            let binding = scope.binding_context()?;
47            let mut input = crate::binding::read_prepared_inputs(
48                context.routines,
49                &mut logical_plan,
50                &parameter_types,
51                &binding,
52                Some(context.aliases),
53            )?;
54            crate::schema::dependencies::oid_alias::read_prepared_oid_alias_constants(
55                context.aliases,
56                &mut logical_plan,
57            )?;
58            logical_plan.visit_scalar_expressions(&mut |expression| {
59                input.dependencies.include_expression(expression);
60            });
61            let effective_search_path =
62                binding.catalog.effective_search_path(&binding.resolution)?;
63            let dependency_snapshot = binding
64                .catalog
65                .prepared_dependency_snapshot(&input.dependencies)?;
66            Ok((input, effective_search_path, dependency_snapshot))
67        })?;
68    let result_schema = analyze_result_schema(context, &logical_plan, &input.parameter_types)?;
69    Ok(PreparedDefinition {
70        composite_inputs: super::composites::CompositeInputs::capture(
71            &logical_plan,
72            context.types,
73        )?,
74        logical_plan,
75        parameter_types: input.parameter_types,
76        result_schema,
77        effective_search_path,
78        dependencies: input.dependencies,
79        dependency_snapshot,
80    })
81}
82
83/// 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.
84pub fn analysis_is_current(
85    context: &PreparedDefinitionContext<'_>,
86    retained_path: Option<&EffectiveSearchPath>,
87    dependencies: &PreparedAnalysisDependencies,
88    retained_snapshot: Option<&PreparedDependencySnapshot>,
89) -> Result<bool, SQLError> {
90    if retained_path.is_none() && retained_snapshot.is_none() {
91        return Ok(true);
92    }
93    with_scope_result(context.scopes, |scope| {
94        let binding = scope.binding_context()?;
95        if let Some(path) = retained_path {
96            if binding
97                .catalog
98                .effective_search_path(&binding.resolution)?
99                .as_ref()
100                != Some(path)
101            {
102                return Ok(false);
103            }
104        }
105        match retained_snapshot {
106            Some(snapshot) => Ok(binding
107                .catalog
108                .prepared_dependency_snapshot(dependencies)?
109                .as_ref()
110                == Some(snapshot)),
111            None => Ok(true),
112        }
113    })
114}
115
116/// Read the live namespace through one retained binding scope before selecting a previously analyzed prepared statement.
117pub fn effective_search_path(
118    context: &PreparedDefinitionContext<'_>,
119) -> Result<Option<EffectiveSearchPath>, SQLError> {
120    with_scope_result(context.scopes, |scope| {
121        let binding = scope.binding_context()?;
122        binding.catalog.effective_search_path(&binding.resolution)
123    })
124}
125
126pub fn analyze_result_schema(
127    context: &PreparedDefinitionContext<'_>,
128    logical_plan: &UnifiedPlan,
129    parameter_types: &[Option<ColumnType>],
130) -> Result<Option<RowSchema>, SQLError> {
131    with_scope_result(context.scopes, |scope| {
132        super::analyze_prepared_plan(
133            context.routines,
134            logical_plan,
135            parameter_types,
136            &scope.binding_context()?,
137        )
138    })
139}
140
141fn with_scope_result<T>(
142    scopes: &dyn StatementAnalysisScopes,
143    mut analyze: impl FnMut(&dyn StatementBindingScope) -> Result<T, SQLError>,
144) -> Result<T, SQLError> {
145    let mut result = None;
146    scopes.with_scope(&mut |scope| {
147        result = Some(analyze(scope)?);
148        Ok(())
149    })?;
150    result.ok_or_else(|| {
151        SQLError::Internal("prepared analysis scope did not invoke its operation".into())
152    })
153}
154
155#[cfg(test)]
156mod tests;