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::Cast { ty, .. } => independent_type_name(ty) || composites.tracks_name(ty),
143            ScalarExpr::TypedLiteral {
144                value,
145                ty,
146                bound_type,
147                parameter_index,
148                ..
149            } => {
150                parameter_index.is_none()
151                    && ((bound_type
152                        .as_ref()
153                        .is_some_and(|ty| composites.tracks_type(ty))
154                        || composites.tracks_name(ty))
155                        || (independent_value(value)
156                            && bound_type
157                                .as_ref()
158                                .map_or_else(|| independent_type_name(ty), independent_type)))
159            }
160            ScalarExpr::CompositeRow {
161                bound_type,
162                binding,
163                ..
164            } => {
165                bound_type
166                    .as_ref()
167                    .is_some_and(|ty| composites.tracks_type(ty))
168                    || composites.tracks_name(&binding.ty)
169            }
170            ScalarExpr::Func {
171                binding,
172                args,
173                distinct,
174                order_by,
175                filter,
176                ..
177            } => {
178                !*distinct
179                    && order_by.is_empty()
180                    && filter.is_none()
181                    && binding.as_ref().map_or(!executable, |binding| {
182                        tracked_field(binding, composites)
183                            || (independent_binding(binding)
184                                && (!executable
185                                    || (args.len() == binding.argument_types.len()
186                                        && crate::fixed_builtin_return_type(binding)
187                                            .is_some_and(|ty| independent_type(&ty)))))
188                    })
189            }
190            ScalarExpr::Column(_)
191            | ScalarExpr::QualifiedColumn { .. }
192            | ScalarExpr::Param(_)
193            | ScalarExpr::Array(_)
194            | ScalarExpr::Binary { .. }
195            | ScalarExpr::UnaryMinus(_)
196            | ScalarExpr::Not(_)
197            | ScalarExpr::And(_)
198            | ScalarExpr::Or(_)
199            | ScalarExpr::IsNull { .. }
200            | ScalarExpr::Between { .. }
201            | ScalarExpr::InList { .. }
202            | ScalarExpr::Case { .. } => true,
203            _ => false,
204        };
205    });
206    independent
207}
208
209fn tracked_field(binding: &FunctionBinding, composites: &CompositeInputs) -> bool {
210    binding.builtin
211        && binding.object_id.is_none()
212        && binding.dispatch == Some(crate::ast::FunctionDispatch::FieldSelect)
213        && binding.invocation.is_none()
214        && binding.resolution_error.is_none()
215        && binding.composite_field.as_ref().is_some_and(|field| {
216            composites.tracks_oid(field.type_oid)
217                && (independent_type(&field.result_type)
218                    || composites.tracks_type(&field.result_type))
219        })
220}
221
222fn independent_binding(binding: &FunctionBinding) -> bool {
223    binding.builtin
224        && binding.object_id.is_none()
225        && binding.dispatch.is_none()
226        && binding.invocation.is_none()
227        && binding.resolution_error.is_none()
228        && binding
229            .argument_types
230            .iter()
231            .all(|ty| independent_type_name(ty))
232}
233
234fn independent_value(value: &Value) -> bool {
235    match value {
236        Value::Null
237        | Value::Bool(_)
238        | Value::Int(_)
239        | Value::Float(_)
240        | Value::Str(_)
241        | Value::FixedChar(_)
242        | Value::Bytes(_)
243        | Value::Decimal(_)
244        | Value::Json(_)
245        | Value::JsonB(_) => true,
246        Value::Array(array) => array.elements().iter().all(independent_value),
247        _ => false,
248    }
249}
250
251fn independent_type_name(name: &str) -> bool {
252    ColumnType::from_sql_name(name).is_ok_and(|ty| independent_type(&ty))
253}
254
255// 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.
256fn independent_type(ty: &ColumnType) -> bool {
257    match ty {
258        ColumnType::Array(element) => independent_type(element),
259        ColumnType::SmallInteger
260        | ColumnType::Integer
261        | ColumnType::BigInteger
262        | ColumnType::Boolean
263        | ColumnType::Text
264        | ColumnType::Name
265        | ColumnType::Uuid
266        | ColumnType::Varchar(_)
267        | ColumnType::Bpchar
268        | ColumnType::Character(_)
269        | ColumnType::Real
270        | ColumnType::DoublePrecision
271        | ColumnType::Numeric { .. }
272        | ColumnType::Json
273        | ColumnType::JsonB
274        | ColumnType::Bytea
275        | ColumnType::InternalChar => true,
276        _ => false,
277    }
278}
279
280#[cfg(test)]
281mod tests;