1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum PlanCacheEligibility {
17 Cacheable,
19 DataDependent,
22}
23
24impl PlanCacheEligibility {
25 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 pub fn cache_eligibility(&self) -> PlanCacheEligibility {
50 use PlanCacheEligibility::{Cacheable, DataDependent};
51
52 match self {
53 Self::ConstantResult { volatile: true, .. } => DataDependent,
54 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 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}