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::{
11    ast::FunctionBinding,
12    plan::{QueryPlan, RelationalPlan, SourcePlan, UnifiedPlan},
13    ColumnType, ScalarExpr,
14};
15use uqa_core::Value;
16
17impl PreparedStatementPlan {
18    /// 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.
19    #[must_use]
20    pub fn has_tracked_executable_dependencies(&self) -> bool {
21        if self.needs_analysis
22            || !self.dependencies.routines.is_empty()
23            || !self
24                .parameter_types
25                .iter()
26                .all(|ty| ty.as_ref().is_some_and(independent_type))
27        {
28            return false;
29        }
30        let Some(schema) = &self.result_schema else {
31            return false;
32        };
33        if !schema
34            .column_types()
35            .iter()
36            .all(|ty| ty.as_ref().is_some_and(independent_type))
37            || (0..schema.len()).any(|index| schema.record_fields(index).is_some())
38        {
39            return false;
40        }
41        let tracked_relations = !self.dependencies.relations.is_empty()
42            && self.dependency_snapshot.as_ref().is_some_and(|snapshot| {
43                snapshot.global_catalog.is_some()
44                    && self
45                        .dependencies
46                        .relations
47                        .iter()
48                        .all(|oid| snapshot.relations.get(oid).is_some_and(Option::is_some))
49            });
50        if !self.dependencies.relations.is_empty() && !tracked_relations {
51            return false;
52        }
53        self.plan
54            .as_ref()
55            .is_some_and(|plan| independent_plan(plan, true, tracked_relations))
56            && independent_plan(&self.source_plan, false, tracked_relations)
57            && independent_plan(&self.logical_plan, false, tracked_relations)
58    }
59}
60
61fn independent_plan(plan: &UnifiedPlan, executable: bool, tracked_relations: bool) -> bool {
62    match plan {
63        UnifiedPlan::Query(query) => independent_query(query, executable, tracked_relations),
64        UnifiedPlan::Command(_) => false,
65    }
66}
67
68fn independent_query(query: &QueryPlan, executable: bool, tracked_relations: bool) -> bool {
69    if !query.ctes.is_empty() {
70        return false;
71    }
72    let admitted = |expression: &ScalarExpr| independent_expression(expression, executable);
73    match &query.root {
74        RelationalPlan::QueryBlock(block) => {
75            block.from.as_ref().is_none_or(|source| {
76                tracked_relations && matches!(source, SourcePlan::Table { .. })
77            }) && block.subqueries.is_empty()
78                && block.windows.is_empty()
79                && block.locking.is_empty()
80                && block
81                    .expressions()
82                    .iter()
83                    .all(|expression| admitted(expression))
84        }
85        RelationalPlan::SetOp {
86            left,
87            right,
88            order_by,
89            limit,
90            offset,
91            subqueries,
92            ..
93        } => {
94            subqueries.is_empty()
95                && independent_query(left, executable, tracked_relations)
96                && independent_query(right, executable, tracked_relations)
97                && order_by
98                    .iter()
99                    .map(|order| &order.expr)
100                    .chain(limit.as_deref())
101                    .chain(offset.as_deref())
102                    .all(admitted)
103        }
104        RelationalPlan::Values { rows, subqueries } => {
105            subqueries.is_empty() && rows.iter().flatten().all(admitted)
106        }
107    }
108}
109
110fn independent_expression(expression: &ScalarExpr, executable: bool) -> bool {
111    let mut independent = true;
112    expression.visit(&mut |part| {
113        independent &= match part {
114            ScalarExpr::Literal(value) => independent_value(value),
115            ScalarExpr::Cast { ty, .. } => independent_type_name(ty),
116            ScalarExpr::TypedLiteral {
117                value,
118                ty,
119                bound_type,
120                parameter_index,
121            } => {
122                parameter_index.is_none()
123                    && independent_value(value)
124                    && bound_type
125                        .as_ref()
126                        .map_or_else(|| independent_type_name(ty), independent_type)
127            }
128            ScalarExpr::Func {
129                binding,
130                args,
131                distinct,
132                order_by,
133                filter,
134                ..
135            } => {
136                !*distinct
137                    && order_by.is_empty()
138                    && filter.is_none()
139                    && binding.as_ref().map_or(!executable, |binding| {
140                        independent_binding(binding)
141                            && (!executable
142                                || (args.len() == binding.argument_types.len()
143                                    && crate::fixed_builtin_return_type(binding)
144                                        .is_some_and(|ty| independent_type(&ty))))
145                    })
146            }
147            ScalarExpr::Column(_)
148            | ScalarExpr::QualifiedColumn { .. }
149            | ScalarExpr::Param(_)
150            | ScalarExpr::Array(_)
151            | ScalarExpr::Binary { .. }
152            | ScalarExpr::UnaryMinus(_)
153            | ScalarExpr::Not(_)
154            | ScalarExpr::And(_)
155            | ScalarExpr::Or(_)
156            | ScalarExpr::IsNull { .. }
157            | ScalarExpr::Between { .. }
158            | ScalarExpr::InList { .. }
159            | ScalarExpr::Case { .. } => true,
160            _ => false,
161        };
162    });
163    independent
164}
165
166fn independent_binding(binding: &FunctionBinding) -> bool {
167    binding.builtin
168        && binding.object_id.is_none()
169        && binding.dispatch.is_none()
170        && binding.invocation.is_none()
171        && binding.resolution_error.is_none()
172        && binding
173            .argument_types
174            .iter()
175            .all(|ty| independent_type_name(ty))
176}
177
178fn independent_value(value: &Value) -> bool {
179    match value {
180        Value::Null
181        | Value::Bool(_)
182        | Value::Int(_)
183        | Value::Float(_)
184        | Value::Str(_)
185        | Value::FixedChar(_)
186        | Value::Bytes(_)
187        | Value::Decimal(_)
188        | Value::Json(_)
189        | Value::JsonB(_) => true,
190        Value::Array(array) => array.elements().iter().all(independent_value),
191        _ => false,
192    }
193}
194
195fn independent_type_name(name: &str) -> bool {
196    ColumnType::from_sql_name(name).is_ok_and(|ty| independent_type(&ty))
197}
198
199// 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.
200fn independent_type(ty: &ColumnType) -> bool {
201    match ty {
202        ColumnType::Array(element) => independent_type(element),
203        ColumnType::SmallInteger
204        | ColumnType::Integer
205        | ColumnType::BigInteger
206        | ColumnType::Boolean
207        | ColumnType::Text
208        | ColumnType::Name
209        | ColumnType::Uuid
210        | ColumnType::Varchar(_)
211        | ColumnType::Bpchar
212        | ColumnType::Character(_)
213        | ColumnType::Real
214        | ColumnType::DoublePrecision
215        | ColumnType::Numeric { .. }
216        | ColumnType::Json
217        | ColumnType::JsonB
218        | ColumnType::Bytea
219        | ColumnType::InternalChar => true,
220        _ => false,
221    }
222}
223
224#[cfg(test)]
225mod tests;