Skip to main content

nodedb_sql/types/plan/
cacheability.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use crate::types::query::EngineType;
4
5use super::SqlPlan;
6use super::expr_scan::projection_is_cp_computed;
7use super::variants::{
8    CtePlan, DocumentIndexLookupPlan, HybridSearchPlan, HybridSearchTriplePlan, InsertPlan,
9    KvInsertPlan, LateralLoopPlan, LateralTopKPlan, MergePlan, RangeScanPlan, RecursiveScanPlan,
10    TextSearchPlan, TimeseriesIngestPlan, TimeseriesScanPlan, UpsertPlan, VectorPrimaryDeletePlan,
11    VectorPrimaryInsertPlan, VectorPrimaryUpdatePlan,
12};
13
14/// Whether a logical plan may be lowered once and reused from the physical-plan cache.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum PlanCacheEligibility {
17    /// Lowering depends only on schema/catalog descriptors tracked by the cache.
18    Cacheable,
19    /// Lowering consults mutable row identity, or the plan holds a volatile
20    /// expression, and must run for every execution.
21    DataDependent,
22}
23
24impl PlanCacheEligibility {
25    /// Whether the lowered physical tasks may be admitted to the plan cache.
26    pub fn is_cacheable(self) -> bool {
27        self == Self::Cacheable
28    }
29
30    fn combine(self, other: Self) -> Self {
31        if self == Self::DataDependent || other == Self::DataDependent {
32            Self::DataDependent
33        } else {
34            Self::Cacheable
35        }
36    }
37}
38
39impl SqlPlan {
40    /// Classify dependencies that are not represented by descriptor versions.
41    ///
42    /// Document point operations resolve primary-key bytes to a surrogate while
43    /// lowering. That binding can appear after an earlier miss without any
44    /// schema-version change, so those physical tasks cannot be cached.
45    ///
46    /// A plan holding a volatile call is `DataDependent` for the same reason:
47    /// the call was evaluated while the plan was built, so caching it would
48    /// replay one execution's value into every later one.
49    pub fn cache_eligibility(&self) -> PlanCacheEligibility {
50        use PlanCacheEligibility::{Cacheable, DataDependent};
51
52        match self {
53            Self::ConstantResult { volatile: true, .. } => DataDependent,
54            // A Control-Plane-computed projection entry holds a sequence
55            // accessor, which allocates per execution: the lowered tasks
56            // must not replay one execution's rows for another.
57            Self::Scan { projection, .. }
58            | Self::PointGet { projection, .. }
59            | Self::DocumentIndexLookup(DocumentIndexLookupPlan { projection, .. })
60            | Self::RangeScan(RangeScanPlan { projection, .. })
61            | Self::Join { projection, .. }
62            | Self::TimeseriesScan(TimeseriesScanPlan { projection, .. })
63            | Self::VectorSearch { projection, .. }
64            | Self::MultiVectorSearch { projection, .. }
65            | Self::SparseSearch { projection, .. }
66            | Self::TextSearch(TextSearchPlan { projection, .. })
67            | Self::HybridSearch(HybridSearchPlan { projection, .. })
68            | Self::HybridSearchTriple(HybridSearchTriplePlan { projection, .. })
69            | Self::SpatialScan { projection, .. }
70            | Self::RecursiveScan(RecursiveScanPlan { projection, .. })
71            | Self::Subquery { projection, .. }
72            | Self::LateralTopK(LateralTopKPlan { projection, .. })
73            | Self::LateralLoop(LateralLoopPlan { projection, .. })
74                if projection_is_cp_computed(projection) =>
75            {
76                DataDependent
77            }
78            Self::Insert(InsertPlan {
79                volatile_defaults: true,
80                ..
81            })
82            | Self::Upsert(UpsertPlan {
83                volatile_defaults: true,
84                ..
85            })
86            | Self::TimeseriesIngest(TimeseriesIngestPlan {
87                volatile_defaults: true,
88                ..
89            })
90            | Self::KvInsert(KvInsertPlan {
91                volatile_defaults: true,
92                ..
93            })
94            | Self::VectorPrimaryInsert(VectorPrimaryInsertPlan {
95                volatile_defaults: true,
96                ..
97            }) => DataDependent,
98            Self::PointGet {
99                engine: EngineType::DocumentSchemaless | EngineType::DocumentStrict,
100                ..
101            } => DataDependent,
102            Self::Update {
103                engine,
104                target_keys,
105                ..
106            }
107            | Self::Delete {
108                engine,
109                target_keys,
110                ..
111            } if !target_keys.is_empty()
112                && matches!(
113                    engine,
114                    EngineType::DocumentSchemaless | EngineType::DocumentStrict
115                ) =>
116            {
117                DataDependent
118            }
119            // A point-key delete or update binds its surrogates while the plan
120            // is lowered, from the catalog state of that moment.
121            Self::VectorPrimaryDelete(VectorPrimaryDeletePlan { target_keys, .. })
122            | Self::VectorPrimaryUpdate(VectorPrimaryUpdatePlan { target_keys, .. })
123                if !target_keys.is_empty() =>
124            {
125                DataDependent
126            }
127            Self::InsertSelect { source, .. }
128            | Self::UpdateFrom { source, .. }
129            | Self::Aggregate { input: source, .. }
130            | Self::Merge(MergePlan { source, .. }) => source.cache_eligibility(),
131            Self::Join { left, right, .. }
132            | Self::Intersect { left, right, .. }
133            | Self::Except { left, right, .. } => {
134                left.cache_eligibility().combine(right.cache_eligibility())
135            }
136            Self::Union { inputs, .. } => inputs.iter().fold(Cacheable, |eligibility, input| {
137                eligibility.combine(input.cache_eligibility())
138            }),
139            Self::Cte(CtePlan { definitions, outer }) => definitions
140                .iter()
141                .fold(outer.cache_eligibility(), |eligibility, (_, plan)| {
142                    eligibility.combine(plan.cache_eligibility())
143                }),
144            Self::Subquery { input, .. } => input.cache_eligibility(),
145            Self::LateralTopK(LateralTopKPlan { outer, .. }) => outer.cache_eligibility(),
146            Self::LateralLoop(LateralLoopPlan { outer, inner, .. }) => {
147                outer.cache_eligibility().combine(inner.cache_eligibility())
148            }
149            Self::ConstantResult { .. }
150            | Self::Scan { .. }
151            | Self::PointGet { .. }
152            | Self::DocumentIndexLookup { .. }
153            | Self::RangeScan { .. }
154            | Self::Insert { .. }
155            | Self::KvInsert { .. }
156            | Self::Upsert { .. }
157            | Self::Update { .. }
158            | Self::Delete { .. }
159            | Self::Truncate { .. }
160            | Self::TimeseriesScan { .. }
161            | Self::TimeseriesIngest { .. }
162            | Self::VectorSearch { .. }
163            | Self::MultiVectorSearch { .. }
164            | Self::SparseSearch { .. }
165            | Self::TextSearch { .. }
166            | Self::HybridSearch { .. }
167            | Self::HybridSearchTriple { .. }
168            | Self::SpatialScan { .. }
169            | Self::RecursiveScan { .. }
170            | Self::RecursiveValue { .. }
171            | Self::CreateArray { .. }
172            | Self::DropArray { .. }
173            | Self::AlterArray { .. }
174            | Self::InsertArray { .. }
175            | Self::DeleteArray { .. }
176            | Self::ArraySlice { .. }
177            | Self::ArrayProject { .. }
178            | Self::ArrayAgg { .. }
179            | Self::ArrayElementwise { .. }
180            | Self::ArrayFlush { .. }
181            | Self::ArrayCompact { .. }
182            | Self::VectorPrimaryInsert { .. }
183            | Self::VectorPrimaryDelete { .. }
184            | Self::VectorPrimaryTruncate { .. }
185            | Self::VectorPrimaryUpdate { .. }
186            | Self::CreateIndex { .. }
187            | Self::DropIndex { .. } => Cacheable,
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use crate::types_expr::SqlValue;
196
197    fn point_get(engine: EngineType) -> SqlPlan {
198        SqlPlan::PointGet {
199            collection: "docs".into(),
200            alias: None,
201            engine,
202            key_column: "id".into(),
203            key_value: SqlValue::String("k".into()),
204            projection: Vec::new(),
205        }
206    }
207
208    fn update(engine: EngineType, target_keys: Vec<SqlValue>) -> SqlPlan {
209        SqlPlan::Update {
210            collection: "docs".into(),
211            engine,
212            assignments: Vec::new(),
213            filters: Vec::new(),
214            target_keys,
215            returning: false,
216        }
217    }
218
219    fn delete(engine: EngineType, target_keys: Vec<SqlValue>) -> SqlPlan {
220        SqlPlan::Delete {
221            collection: "docs".into(),
222            engine,
223            filters: Vec::new(),
224            target_keys,
225        }
226    }
227
228    #[test]
229    fn document_point_get_is_data_dependent() {
230        assert_eq!(
231            point_get(EngineType::DocumentStrict).cache_eligibility(),
232            PlanCacheEligibility::DataDependent
233        );
234    }
235
236    #[test]
237    fn key_value_point_get_is_cacheable() {
238        assert_eq!(
239            point_get(EngineType::KeyValue).cache_eligibility(),
240            PlanCacheEligibility::Cacheable
241        );
242    }
243
244    #[test]
245    fn document_point_update_is_data_dependent() {
246        assert_eq!(
247            update(
248                EngineType::DocumentSchemaless,
249                vec![SqlValue::String("k".into())]
250            )
251            .cache_eligibility(),
252            PlanCacheEligibility::DataDependent
253        );
254    }
255
256    #[test]
257    fn document_predicate_update_is_cacheable() {
258        assert_eq!(
259            update(EngineType::DocumentStrict, Vec::new()).cache_eligibility(),
260            PlanCacheEligibility::Cacheable
261        );
262    }
263
264    #[test]
265    fn document_point_delete_is_data_dependent() {
266        assert_eq!(
267            delete(
268                EngineType::DocumentStrict,
269                vec![SqlValue::String("k".into())]
270            )
271            .cache_eligibility(),
272            PlanCacheEligibility::DataDependent
273        );
274    }
275
276    #[test]
277    fn cp_computed_projection_is_data_dependent() {
278        use crate::types::query::Projection;
279        use crate::types_expr::SqlExpr;
280        let cp = Projection::CpComputed {
281            expr: SqlExpr::Function {
282                name: "nextval".into(),
283                args: vec![SqlExpr::Literal(SqlValue::String("s".into()))],
284                distinct: false,
285            },
286            alias: "nextval".into(),
287        };
288        let scan = |projection: Vec<Projection>| SqlPlan::Scan {
289            collection: "docs".into(),
290            alias: None,
291            engine: EngineType::KeyValue,
292            filters: Vec::new(),
293            projection,
294            sort_keys: Vec::new(),
295            limit: None,
296            offset: 0,
297            distinct: false,
298            window_functions: Vec::new(),
299            temporal: crate::temporal::TemporalScope::default(),
300        };
301        assert_eq!(
302            scan(vec![Projection::Column("id".into()), cp.clone()]).cache_eligibility(),
303            PlanCacheEligibility::DataDependent
304        );
305        assert_eq!(
306            scan(vec![Projection::Column("id".into())]).cache_eligibility(),
307            PlanCacheEligibility::Cacheable
308        );
309        let wrapped = SqlPlan::Subquery {
310            input: Box::new(scan(Vec::new())),
311            filters: Vec::new(),
312            projection: vec![cp],
313            window_functions: Vec::new(),
314            sort_keys: Vec::new(),
315            offset: 0,
316            distinct: false,
317            limit: None,
318        };
319        assert_eq!(
320            wrapped.cache_eligibility(),
321            PlanCacheEligibility::DataDependent
322        );
323    }
324
325    #[test]
326    fn nested_point_dependency_propagates() {
327        let plan = SqlPlan::Cte(CtePlan {
328            definitions: vec![("selected".into(), point_get(EngineType::DocumentStrict))],
329            outer: Box::new(point_get(EngineType::KeyValue)),
330        });
331        assert_eq!(
332            plan.cache_eligibility(),
333            PlanCacheEligibility::DataDependent
334        );
335    }
336}