Skip to main content

uqa_sql/binding/
composite_dependencies.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Attribute identities selected from named composite values, resolved with the ordinary SQL scopes and type rules.
8
9use super::{BindingContext, SchemaScope};
10use crate::ast::{ColumnType, FunctionDispatch};
11use crate::catalog::dependencies::ObjectAddress;
12use crate::plan::QueryPlan;
13use crate::routines::RoutineResolution;
14use crate::schema::ScalarTypeSchema;
15use crate::type_resolution::{scalar_type_with_resolver, FunctionTypeResolver};
16use crate::{RowSchema, SQLError, SQLParam, ScalarExpr};
17use uqa_core::Value;
18
19/// References of field selections in one expression. Anonymous rows and whole-row relation projections do not introduce a named composite attribute dependency.
20pub fn expression_composite_dependencies(
21    expression: &ScalarExpr,
22    schema: &dyn ScalarTypeSchema,
23    params: &[SQLParam],
24    types: &dyn FunctionTypeResolver,
25) -> Result<Vec<ObjectAddress>, SQLError> {
26    let mut references = Vec::new();
27    let mut failure = None;
28    expression.visit(&mut |node| {
29        if failure.is_some() {
30            return;
31        }
32        let ScalarExpr::Func { binding, args, .. } = node else {
33            return;
34        };
35        if binding.as_ref().and_then(|binding| binding.dispatch)
36            != Some(FunctionDispatch::FieldSelect)
37        {
38            return;
39        }
40        let [base, ScalarExpr::Literal(Value::Str(field))] = args.as_slice() else {
41            return;
42        };
43        let result = (|| {
44            let Some(mut ty) = scalar_type_with_resolver(base, schema, params, types)? else {
45                return Ok(None);
46            };
47            while let ColumnType::Domain { base, .. } = ty {
48                ty = *base;
49            }
50            let ColumnType::Composite(reference) = ty else {
51                return Ok(None);
52            };
53            let descriptor =
54                crate::expr::composites::descriptor(types.composite_types(), reference.oid)?;
55            Ok(descriptor.attribute(field).map(|(_, attribute)| {
56                ObjectAddress::column(descriptor.relation_oid, i32::from(attribute.number))
57            }))
58        })();
59        match result {
60            Ok(Some(reference)) => references.push(reference),
61            Ok(None) => {}
62            Err(error) => failure = Some(error),
63        }
64    });
65    failure.map_or(Ok(references), Err)
66}
67
68/// Rebind a private query copy against the supplied catalog to collect field dependencies even for definitions written before attribute removal was supported. No persistent representation or input constant is changed.
69pub fn query_composite_dependencies(
70    routines: &dyn RoutineResolution,
71    query: &QueryPlan,
72    params: &[SQLParam],
73    context: &BindingContext<'_>,
74    outer: Option<&RowSchema>,
75) -> Result<Vec<ObjectAddress>, SQLError> {
76    let mut contains_field = false;
77    query.visit_scalar_expressions(&mut |expression| {
78        expression.visit(&mut |node| {
79            contains_field |= matches!(node, ScalarExpr::Func { binding: Some(binding), .. }
80            if binding.dispatch == Some(FunctionDispatch::FieldSelect));
81        });
82    });
83    if !contains_field {
84        return Ok(Vec::new());
85    }
86    let mut scope = SchemaScope::for_analysis(context)?;
87    scope.composite_dependencies = Some(Vec::new());
88    scope.bind_query_routines_for_storage(routines, &mut query.clone(), params, outer)?;
89    Ok(scope.composite_dependencies.unwrap_or_default())
90}
91
92/// Resolve scalar subqueries with their own source scopes and the surrounding expression's correlated row scope.
93pub fn expression_plan_composite_dependencies(
94    routines: &dyn RoutineResolution,
95    plan: &crate::plan::ExpressionPlan,
96    params: &[SQLParam],
97    context: &BindingContext<'_>,
98    schema: &RowSchema,
99) -> Result<Vec<ObjectAddress>, SQLError> {
100    let mut scope = SchemaScope::for_analysis(context)?;
101    scope.composite_dependencies = Some(Vec::new());
102    scope.preserve_syntax_shape = true;
103    scope.stored_expression_outer = Some(schema.clone());
104    let mut plan = plan.clone();
105    for subquery in &mut plan.subqueries {
106        scope.bind_query_routines_for_storage(routines, subquery, params, Some(schema))?;
107    }
108    scope.bind_scalar_routines_for_storage(
109        routines,
110        &mut plan.scalar,
111        schema,
112        &plan.subqueries,
113        params,
114    )?;
115    Ok(scope.composite_dependencies.unwrap_or_default())
116}
117
118impl SchemaScope {
119    pub(super) fn record_composite_dependencies(
120        &mut self,
121        routines: &dyn RoutineResolution,
122        expression: &ScalarExpr,
123        schema: &RowSchema,
124        subqueries: &[QueryPlan],
125        params: &[SQLParam],
126    ) -> Result<(), SQLError> {
127        if self.composite_dependencies.is_none() {
128            return Ok(());
129        }
130        let resolver = self.query_function_type_resolver_for_subqueries(
131            routines,
132            std::slice::from_ref(expression),
133            schema,
134            subqueries,
135            params,
136        )?;
137        let references = expression_composite_dependencies(expression, schema, params, &resolver)?;
138        if let Some(dependencies) = &mut self.composite_dependencies {
139            dependencies.extend(references);
140        }
141        Ok(())
142    }
143}
144
145/// Resolve the typed parameters and source scopes of one stored SQL-standard routine statement without executing its expressions.
146pub fn routine_statement_composite_dependencies(
147    routines: &dyn RoutineResolution,
148    definition: &crate::ast::CreateFunction,
149    statement: &crate::ast::Statement,
150    context: &BindingContext<'_>,
151) -> Result<Vec<ObjectAddress>, SQLError> {
152    let mut plan = crate::plan::UnifiedPlan::lower_with(statement.clone(), &|name: &str| {
153        routines.has_registered_aggregate_function(name)
154    });
155    let mut contains_field = false;
156    plan.rewrite_scalar_expressions(&mut |node| {
157        contains_field |= matches!(node, ScalarExpr::Func { binding: Some(binding), .. }
158            if binding.dispatch == Some(FunctionDispatch::FieldSelect));
159    });
160    if !contains_field {
161        return Ok(Vec::new());
162    }
163    super::stored_routines::mark_catalog_statement_relations_bound(&mut plan)?;
164    let params = crate::routines::body_parameters::sql_body_parameters(definition)
165        .into_iter()
166        .map(|parameter| {
167            routines.resolve_type_name(&parameter.type_name).map(|ty| {
168                ty.map_or_else(
169                    || SQLParam::scalar(Value::Null),
170                    |ty| SQLParam::typed_scalar(Value::Null, ty),
171                )
172            })
173        })
174        .collect::<Result<Vec<_>, _>>()?;
175    let parameters =
176        crate::routines::body_parameters::sql_body_parameter_scope(definition, &params)?;
177    let mut scope = SchemaScope::for_analysis(context)?;
178    scope.routine_parameters = Some(parameters.clone());
179    scope.composite_dependencies = Some(Vec::new());
180    scope.preserve_syntax_shape = true;
181    scope.bind_statement_parameters(routines, &mut plan, &params, Some(parameters.schema()))?;
182    Ok(scope.composite_dependencies.unwrap_or_default())
183}
184
185/// Resolve the source and transition scopes of a stored rule action.
186pub fn statement_composite_dependencies(
187    routines: &dyn RoutineResolution,
188    statement: &crate::ast::Statement,
189    context: &BindingContext<'_>,
190    outer: &RowSchema,
191) -> Result<Vec<ObjectAddress>, SQLError> {
192    let mut plan = crate::plan::UnifiedPlan::lower_with(statement.clone(), &|name: &str| {
193        routines.has_registered_aggregate_function(name)
194    });
195    let mut contains_field = false;
196    plan.rewrite_scalar_expressions(&mut |node| {
197        contains_field |= matches!(node, ScalarExpr::Func { binding: Some(binding), .. }
198            if binding.dispatch == Some(FunctionDispatch::FieldSelect));
199    });
200    if !contains_field {
201        return Ok(Vec::new());
202    }
203    super::stored_routines::mark_catalog_statement_relations_bound(&mut plan)?;
204    let mut scope = SchemaScope::for_analysis(context)?;
205    scope.composite_dependencies = Some(Vec::new());
206    scope.preserve_syntax_shape = true;
207    scope.bind_statement_parameters(routines, &mut plan, &[], Some(outer))?;
208    Ok(scope.composite_dependencies.unwrap_or_default())
209}
210
211#[cfg(test)]
212mod tests;