Skip to main content

uqa_sql/prepared/entry/
reuse.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Recognize executables whose mutable dependencies are covered by their retained analysis snapshot.
8
9use super::PreparedStatementPlan;
10use crate::prepared::composites::CompositeInputs;
11use crate::{
12    ast::FunctionBinding,
13    plan::{QueryPlan, RelationalPlan, SourcePlan, UnifiedPlan},
14    ColumnType, ScalarExpr,
15};
16use uqa_core::Value;
17
18impl PreparedStatementPlan {
19    /// Primitive expressions and selected fixed builtins can retain their executable across an unrelated registry refresh. Relations require exact analysis revisions, checked before every plan selection; surviving calls still check current execution privileges.
20    #[must_use]
21    pub fn has_tracked_executable_dependencies(&self) -> bool {
22        let composites = &self.composite_inputs;
23        let tracked_type = |ty: &ColumnType| independent_type(ty) || composites.tracks_type(ty);
24        if self.needs_analysis
25            || !self.dependencies.routines.is_empty()
26            || !self
27                .parameter_types
28                .iter()
29                .all(|ty| ty.as_ref().is_some_and(tracked_type))
30        {
31            return false;
32        }
33        let Some(schema) = &self.result_schema else {
34            return false;
35        };
36        if !schema
37            .column_types()
38            .iter()
39            .all(|ty| ty.as_ref().is_some_and(tracked_type))
40            || (0..schema.len()).any(|index| {
41                schema.record_fields(index).is_some()
42                    && schema.column_types()[index]
43                        .as_ref()
44                        .is_none_or(|ty| !composites.tracks_type(ty))
45            })
46        {
47            return false;
48        }
49        let tracked_relations = !self.dependencies.relations.is_empty()
50            && self.dependency_snapshot.as_ref().is_some_and(|snapshot| {
51                snapshot.global_catalog.is_some()
52                    && self
53                        .dependencies
54                        .relations
55                        .iter()
56                        .all(|oid| snapshot.relations.get(oid).is_some_and(Option::is_some))
57            });
58        if !self.dependencies.relations.is_empty() && !tracked_relations {
59            return false;
60        }
61        self.plan
62            .as_ref()
63            .is_some_and(|plan| independent_plan(plan, true, tracked_relations, composites))
64            // Composite analysis has replaced written type names and ROW casts with retained identities; its logical tree carries every remaining executable input.
65            && (composites.has_inputs()
66                || independent_plan(&self.source_plan, false, tracked_relations, composites))
67            && independent_plan(&self.logical_plan, false, tracked_relations, composites)
68    }
69}
70
71fn independent_plan(
72    plan: &UnifiedPlan,
73    executable: bool,
74    tracked_relations: bool,
75    composites: &CompositeInputs,
76) -> bool {
77    match plan {
78        UnifiedPlan::Query(query) => {
79            independent_query(query, executable, tracked_relations, composites)
80        }
81        UnifiedPlan::Command(_) => false,
82    }
83}
84
85fn independent_query(
86    query: &QueryPlan,
87    executable: bool,
88    tracked_relations: bool,
89    composites: &CompositeInputs,
90) -> bool {
91    if !query.ctes.is_empty() {
92        return false;
93    }
94    let admitted =
95        |expression: &ScalarExpr| independent_expression(expression, executable, composites);
96    match &query.root {
97        RelationalPlan::QueryBlock(block) => {
98            block.from.as_ref().is_none_or(|source| {
99                tracked_relations && matches!(source, SourcePlan::Table { .. })
100            }) && block.subqueries.is_empty()
101                && block.windows.is_empty()
102                && block.locking.is_empty()
103                && block
104                    .expressions()
105                    .iter()
106                    .all(|expression| admitted(expression))
107        }
108        RelationalPlan::SetOp {
109            left,
110            right,
111            order_by,
112            limit,
113            offset,
114            subqueries,
115            ..
116        } => {
117            subqueries.is_empty()
118                && independent_query(left, executable, tracked_relations, composites)
119                && independent_query(right, executable, tracked_relations, composites)
120                && order_by
121                    .iter()
122                    .map(|order| &order.expr)
123                    .chain(limit.as_deref())
124                    .chain(offset.as_deref())
125                    .all(admitted)
126        }
127        RelationalPlan::Values { rows, subqueries } => {
128            subqueries.is_empty() && rows.iter().flatten().all(admitted)
129        }
130    }
131}
132
133fn independent_expression(
134    expression: &ScalarExpr,
135    executable: bool,
136    composites: &CompositeInputs,
137) -> bool {
138    let mut independent = true;
139    expression.visit(&mut |part| {
140        independent &= match part {
141            ScalarExpr::Literal(value) => independent_value(value),
142            ScalarExpr::Collated { ty, .. } => independent_type(ty) || composites.tracks_type(ty),
143            ScalarExpr::Cast { ty, .. } => independent_type_name(ty) || composites.tracks_name(ty),
144            ScalarExpr::TypedLiteral {
145                value,
146                ty,
147                bound_type,
148                parameter_index,
149                ..
150            } => {
151                parameter_index.is_none()
152                    && ((bound_type
153                        .as_ref()
154                        .is_some_and(|ty| composites.tracks_type(ty))
155                        || composites.tracks_name(ty))
156                        || (independent_value(value)
157                            && bound_type
158                                .as_ref()
159                                .map_or_else(|| independent_type_name(ty), independent_type)))
160            }
161            ScalarExpr::CompositeRow {
162                bound_type,
163                binding,
164                ..
165            } => {
166                bound_type
167                    .as_ref()
168                    .is_some_and(|ty| composites.tracks_type(ty))
169                    || composites.tracks_name(&binding.ty)
170            }
171            ScalarExpr::Func {
172                binding,
173                args,
174                distinct,
175                order_by,
176                filter,
177                ..
178            } => {
179                !*distinct
180                    && order_by.is_empty()
181                    && filter.is_none()
182                    && binding.as_ref().map_or(!executable, |binding| {
183                        tracked_field(binding, composites)
184                            || (independent_binding(binding)
185                                && (!executable
186                                    || (args.len() == binding.argument_types.len()
187                                        && crate::fixed_builtin_return_type(binding)
188                                            .is_some_and(|ty| independent_type(&ty)))))
189                    })
190            }
191            ScalarExpr::Column(_)
192            | ScalarExpr::QualifiedColumn { .. }
193            | ScalarExpr::Param(_)
194            | ScalarExpr::Array(_)
195            | ScalarExpr::Binary { .. }
196            | ScalarExpr::UnaryMinus(_)
197            | ScalarExpr::Not(_)
198            | ScalarExpr::And(_)
199            | ScalarExpr::Or(_)
200            | ScalarExpr::IsNull { .. }
201            | ScalarExpr::Between { .. }
202            | ScalarExpr::InList { .. }
203            | ScalarExpr::Case { .. } => true,
204            _ => false,
205        };
206    });
207    independent
208}
209
210fn tracked_field(binding: &FunctionBinding, composites: &CompositeInputs) -> bool {
211    binding.builtin
212        && binding.object_id.is_none()
213        && binding.dispatch == Some(crate::ast::FunctionDispatch::FieldSelect)
214        && binding.invocation.is_none()
215        && binding.resolution_error.is_none()
216        && binding.composite_field.as_ref().is_some_and(|field| {
217            composites.tracks_oid(field.type_oid)
218                && (independent_type(&field.result_type)
219                    || composites.tracks_type(&field.result_type))
220        })
221}
222
223fn independent_binding(binding: &FunctionBinding) -> bool {
224    binding.builtin
225        && binding.object_id.is_none()
226        && binding.dispatch.is_none()
227        && binding.invocation.is_none()
228        && binding.resolution_error.is_none()
229        && binding
230            .argument_types
231            .iter()
232            .all(|ty| independent_type_name(ty))
233}
234
235fn independent_value(value: &Value) -> bool {
236    match value {
237        Value::Null
238        | Value::Bool(_)
239        | Value::Int(_)
240        | Value::Float(_)
241        | Value::Str(_)
242        | Value::FixedChar(_)
243        | Value::Bytes(_)
244        | Value::Decimal(_)
245        | Value::Json(_)
246        | Value::JsonB(_) => true,
247        Value::Array(array) => array.elements().iter().all(independent_value),
248        _ => false,
249    }
250}
251
252fn independent_type_name(name: &str) -> bool {
253    ColumnType::from_sql_name(name).is_ok_and(|ty| independent_type(&ty))
254}
255
256// The admitted set has catalog-free input and result identities. Keep pseudo, reg*, temporal, domain, enum and composite types on the existing conservative invalidation path.
257fn independent_type(ty: &ColumnType) -> bool {
258    match ty {
259        ColumnType::Array(element) => independent_type(element),
260        ColumnType::SmallInteger
261        | ColumnType::Integer
262        | ColumnType::BigInteger
263        | ColumnType::Boolean
264        | ColumnType::Text
265        | ColumnType::Name
266        | ColumnType::Uuid
267        | ColumnType::Varchar(_)
268        | ColumnType::Bpchar
269        | ColumnType::Character(_)
270        | ColumnType::Real
271        | ColumnType::DoublePrecision
272        | ColumnType::Numeric { .. }
273        | ColumnType::Json
274        | ColumnType::JsonB
275        | ColumnType::Bytea
276        | ColumnType::InternalChar => true,
277        _ => false,
278    }
279}
280
281#[cfg(test)]
282mod tests;