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;
6
7/// Whether a logical plan may be lowered once and reused from the physical-plan cache.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum PlanCacheEligibility {
10    /// Lowering depends only on schema/catalog descriptors tracked by the cache.
11    Cacheable,
12    /// Lowering consults mutable row identity and must run for every execution.
13    DataDependent,
14}
15
16impl PlanCacheEligibility {
17    /// Whether the lowered physical tasks may be admitted to the plan cache.
18    pub fn is_cacheable(self) -> bool {
19        self == Self::Cacheable
20    }
21
22    fn combine(self, other: Self) -> Self {
23        if self == Self::DataDependent || other == Self::DataDependent {
24            Self::DataDependent
25        } else {
26            Self::Cacheable
27        }
28    }
29}
30
31impl SqlPlan {
32    /// Classify dependencies that are not represented by descriptor versions.
33    ///
34    /// Document point operations resolve primary-key bytes to a surrogate while
35    /// lowering. That binding can appear after an earlier miss without any
36    /// schema-version change, so those physical tasks cannot be cached.
37    pub fn cache_eligibility(&self) -> PlanCacheEligibility {
38        use PlanCacheEligibility::{Cacheable, DataDependent};
39
40        match self {
41            Self::PointGet {
42                engine: EngineType::DocumentSchemaless | EngineType::DocumentStrict,
43                ..
44            } => DataDependent,
45            Self::Update {
46                engine,
47                target_keys,
48                ..
49            }
50            | Self::Delete {
51                engine,
52                target_keys,
53                ..
54            } if !target_keys.is_empty()
55                && matches!(
56                    engine,
57                    EngineType::DocumentSchemaless | EngineType::DocumentStrict
58                ) =>
59            {
60                DataDependent
61            }
62            Self::InsertSelect { source, .. }
63            | Self::UpdateFrom { source, .. }
64            | Self::Aggregate { input: source, .. }
65            | Self::Merge { source, .. } => source.cache_eligibility(),
66            Self::Join { left, right, .. }
67            | Self::Intersect { left, right, .. }
68            | Self::Except { left, right, .. } => {
69                left.cache_eligibility().combine(right.cache_eligibility())
70            }
71            Self::Union { inputs, .. } => inputs.iter().fold(Cacheable, |eligibility, input| {
72                eligibility.combine(input.cache_eligibility())
73            }),
74            Self::Cte { definitions, outer } => definitions
75                .iter()
76                .fold(outer.cache_eligibility(), |eligibility, (_, plan)| {
77                    eligibility.combine(plan.cache_eligibility())
78                }),
79            Self::LateralTopK { outer, .. } => outer.cache_eligibility(),
80            Self::LateralLoop { outer, inner, .. } => {
81                outer.cache_eligibility().combine(inner.cache_eligibility())
82            }
83            Self::ConstantResult { .. }
84            | Self::Scan { .. }
85            | Self::PointGet { .. }
86            | Self::DocumentIndexLookup { .. }
87            | Self::RangeScan { .. }
88            | Self::Insert { .. }
89            | Self::KvInsert { .. }
90            | Self::Upsert { .. }
91            | Self::Update { .. }
92            | Self::Delete { .. }
93            | Self::Truncate { .. }
94            | Self::TimeseriesScan { .. }
95            | Self::TimeseriesIngest { .. }
96            | Self::VectorSearch { .. }
97            | Self::MultiVectorSearch { .. }
98            | Self::SparseSearch { .. }
99            | Self::TextSearch { .. }
100            | Self::HybridSearch { .. }
101            | Self::HybridSearchTriple { .. }
102            | Self::SpatialScan { .. }
103            | Self::RecursiveScan { .. }
104            | Self::RecursiveValue { .. }
105            | Self::CreateArray { .. }
106            | Self::DropArray { .. }
107            | Self::AlterArray { .. }
108            | Self::InsertArray { .. }
109            | Self::DeleteArray { .. }
110            | Self::ArraySlice { .. }
111            | Self::ArrayProject { .. }
112            | Self::ArrayAgg { .. }
113            | Self::ArrayElementwise { .. }
114            | Self::ArrayFlush { .. }
115            | Self::ArrayCompact { .. }
116            | Self::VectorPrimaryInsert { .. }
117            | Self::CreateIndex { .. }
118            | Self::DropIndex { .. } => Cacheable,
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::types_expr::SqlValue;
127
128    fn point_get(engine: EngineType) -> SqlPlan {
129        SqlPlan::PointGet {
130            collection: "docs".into(),
131            alias: None,
132            engine,
133            key_column: "id".into(),
134            key_value: SqlValue::String("k".into()),
135            projection: Vec::new(),
136        }
137    }
138
139    fn update(engine: EngineType, target_keys: Vec<SqlValue>) -> SqlPlan {
140        SqlPlan::Update {
141            collection: "docs".into(),
142            engine,
143            assignments: Vec::new(),
144            filters: Vec::new(),
145            target_keys,
146            returning: false,
147        }
148    }
149
150    fn delete(engine: EngineType, target_keys: Vec<SqlValue>) -> SqlPlan {
151        SqlPlan::Delete {
152            collection: "docs".into(),
153            engine,
154            filters: Vec::new(),
155            target_keys,
156        }
157    }
158
159    #[test]
160    fn document_point_get_is_data_dependent() {
161        assert_eq!(
162            point_get(EngineType::DocumentStrict).cache_eligibility(),
163            PlanCacheEligibility::DataDependent
164        );
165    }
166
167    #[test]
168    fn key_value_point_get_is_cacheable() {
169        assert_eq!(
170            point_get(EngineType::KeyValue).cache_eligibility(),
171            PlanCacheEligibility::Cacheable
172        );
173    }
174
175    #[test]
176    fn document_point_update_is_data_dependent() {
177        assert_eq!(
178            update(
179                EngineType::DocumentSchemaless,
180                vec![SqlValue::String("k".into())]
181            )
182            .cache_eligibility(),
183            PlanCacheEligibility::DataDependent
184        );
185    }
186
187    #[test]
188    fn document_predicate_update_is_cacheable() {
189        assert_eq!(
190            update(EngineType::DocumentStrict, Vec::new()).cache_eligibility(),
191            PlanCacheEligibility::Cacheable
192        );
193    }
194
195    #[test]
196    fn document_point_delete_is_data_dependent() {
197        assert_eq!(
198            delete(
199                EngineType::DocumentStrict,
200                vec![SqlValue::String("k".into())]
201            )
202            .cache_eligibility(),
203            PlanCacheEligibility::DataDependent
204        );
205    }
206
207    #[test]
208    fn nested_point_dependency_propagates() {
209        let plan = SqlPlan::Cte {
210            definitions: vec![("selected".into(), point_get(EngineType::DocumentStrict))],
211            outer: Box::new(point_get(EngineType::KeyValue)),
212        };
213        assert_eq!(
214            plan.cache_eligibility(),
215            PlanCacheEligibility::DataDependent
216        );
217    }
218}