nodedb_sql/types/plan/
cacheability.rs1use crate::types::query::EngineType;
4
5use super::SqlPlan;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum PlanCacheEligibility {
10 Cacheable,
12 DataDependent,
14}
15
16impl PlanCacheEligibility {
17 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 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}