use crate::types::query::EngineType;
use super::SqlPlan;
use super::expr_scan::projection_is_cp_computed;
use super::variants::{
CtePlan, DocumentIndexLookupPlan, HybridSearchPlan, HybridSearchTriplePlan, InsertPlan,
KvInsertPlan, LateralLoopPlan, LateralTopKPlan, MergePlan, RangeScanPlan, RecursiveScanPlan,
TextSearchPlan, TimeseriesIngestPlan, TimeseriesScanPlan, UpsertPlan, VectorPrimaryDeletePlan,
VectorPrimaryInsertPlan, VectorPrimaryUpdatePlan,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlanCacheEligibility {
Cacheable,
DataDependent,
}
impl PlanCacheEligibility {
pub fn is_cacheable(self) -> bool {
self == Self::Cacheable
}
fn combine(self, other: Self) -> Self {
if self == Self::DataDependent || other == Self::DataDependent {
Self::DataDependent
} else {
Self::Cacheable
}
}
}
impl SqlPlan {
pub fn cache_eligibility(&self) -> PlanCacheEligibility {
use PlanCacheEligibility::{Cacheable, DataDependent};
match self {
Self::ConstantResult { volatile: true, .. } => DataDependent,
Self::Scan { projection, .. }
| Self::PointGet { projection, .. }
| Self::DocumentIndexLookup(DocumentIndexLookupPlan { projection, .. })
| Self::RangeScan(RangeScanPlan { projection, .. })
| Self::Join { projection, .. }
| Self::TimeseriesScan(TimeseriesScanPlan { projection, .. })
| Self::VectorSearch { projection, .. }
| Self::MultiVectorSearch { projection, .. }
| Self::SparseSearch { projection, .. }
| Self::TextSearch(TextSearchPlan { projection, .. })
| Self::HybridSearch(HybridSearchPlan { projection, .. })
| Self::HybridSearchTriple(HybridSearchTriplePlan { projection, .. })
| Self::SpatialScan { projection, .. }
| Self::RecursiveScan(RecursiveScanPlan { projection, .. })
| Self::Subquery { projection, .. }
| Self::LateralTopK(LateralTopKPlan { projection, .. })
| Self::LateralLoop(LateralLoopPlan { projection, .. })
if projection_is_cp_computed(projection) =>
{
DataDependent
}
Self::Insert(InsertPlan {
volatile_defaults: true,
..
})
| Self::Upsert(UpsertPlan {
volatile_defaults: true,
..
})
| Self::TimeseriesIngest(TimeseriesIngestPlan {
volatile_defaults: true,
..
})
| Self::KvInsert(KvInsertPlan {
volatile_defaults: true,
..
})
| Self::VectorPrimaryInsert(VectorPrimaryInsertPlan {
volatile_defaults: true,
..
}) => DataDependent,
Self::PointGet {
engine: EngineType::DocumentSchemaless | EngineType::DocumentStrict,
..
} => DataDependent,
Self::Update {
engine,
target_keys,
..
}
| Self::Delete {
engine,
target_keys,
..
} if !target_keys.is_empty()
&& matches!(
engine,
EngineType::DocumentSchemaless | EngineType::DocumentStrict
) =>
{
DataDependent
}
Self::VectorPrimaryDelete(VectorPrimaryDeletePlan { target_keys, .. })
| Self::VectorPrimaryUpdate(VectorPrimaryUpdatePlan { target_keys, .. })
if !target_keys.is_empty() =>
{
DataDependent
}
Self::InsertSelect { source, .. }
| Self::UpdateFrom { source, .. }
| Self::Aggregate { input: source, .. }
| Self::Merge(MergePlan { source, .. }) => source.cache_eligibility(),
Self::Join { left, right, .. }
| Self::Intersect { left, right, .. }
| Self::Except { left, right, .. } => {
left.cache_eligibility().combine(right.cache_eligibility())
}
Self::Union { inputs, .. } => inputs.iter().fold(Cacheable, |eligibility, input| {
eligibility.combine(input.cache_eligibility())
}),
Self::Cte(CtePlan { definitions, outer }) => definitions
.iter()
.fold(outer.cache_eligibility(), |eligibility, (_, plan)| {
eligibility.combine(plan.cache_eligibility())
}),
Self::Subquery { input, .. } => input.cache_eligibility(),
Self::LateralTopK(LateralTopKPlan { outer, .. }) => outer.cache_eligibility(),
Self::LateralLoop(LateralLoopPlan { outer, inner, .. }) => {
outer.cache_eligibility().combine(inner.cache_eligibility())
}
Self::ConstantResult { .. }
| Self::Scan { .. }
| Self::PointGet { .. }
| Self::DocumentIndexLookup { .. }
| Self::RangeScan { .. }
| Self::Insert { .. }
| Self::KvInsert { .. }
| Self::Upsert { .. }
| Self::Update { .. }
| Self::Delete { .. }
| Self::Truncate { .. }
| Self::TimeseriesScan { .. }
| Self::TimeseriesIngest { .. }
| Self::VectorSearch { .. }
| Self::MultiVectorSearch { .. }
| Self::SparseSearch { .. }
| Self::TextSearch { .. }
| Self::HybridSearch { .. }
| Self::HybridSearchTriple { .. }
| Self::SpatialScan { .. }
| Self::RecursiveScan { .. }
| Self::RecursiveValue { .. }
| Self::CreateArray { .. }
| Self::DropArray { .. }
| Self::AlterArray { .. }
| Self::InsertArray { .. }
| Self::DeleteArray { .. }
| Self::ArraySlice { .. }
| Self::ArrayProject { .. }
| Self::ArrayAgg { .. }
| Self::ArrayElementwise { .. }
| Self::ArrayFlush { .. }
| Self::ArrayCompact { .. }
| Self::VectorPrimaryInsert { .. }
| Self::VectorPrimaryDelete { .. }
| Self::VectorPrimaryTruncate { .. }
| Self::VectorPrimaryUpdate { .. }
| Self::CreateIndex { .. }
| Self::DropIndex { .. } => Cacheable,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types_expr::SqlValue;
fn point_get(engine: EngineType) -> SqlPlan {
SqlPlan::PointGet {
collection: "docs".into(),
alias: None,
engine,
key_column: "id".into(),
key_value: SqlValue::String("k".into()),
projection: Vec::new(),
}
}
fn update(engine: EngineType, target_keys: Vec<SqlValue>) -> SqlPlan {
SqlPlan::Update {
collection: "docs".into(),
engine,
assignments: Vec::new(),
filters: Vec::new(),
target_keys,
returning: false,
}
}
fn delete(engine: EngineType, target_keys: Vec<SqlValue>) -> SqlPlan {
SqlPlan::Delete {
collection: "docs".into(),
engine,
filters: Vec::new(),
target_keys,
}
}
#[test]
fn document_point_get_is_data_dependent() {
assert_eq!(
point_get(EngineType::DocumentStrict).cache_eligibility(),
PlanCacheEligibility::DataDependent
);
}
#[test]
fn key_value_point_get_is_cacheable() {
assert_eq!(
point_get(EngineType::KeyValue).cache_eligibility(),
PlanCacheEligibility::Cacheable
);
}
#[test]
fn document_point_update_is_data_dependent() {
assert_eq!(
update(
EngineType::DocumentSchemaless,
vec![SqlValue::String("k".into())]
)
.cache_eligibility(),
PlanCacheEligibility::DataDependent
);
}
#[test]
fn document_predicate_update_is_cacheable() {
assert_eq!(
update(EngineType::DocumentStrict, Vec::new()).cache_eligibility(),
PlanCacheEligibility::Cacheable
);
}
#[test]
fn document_point_delete_is_data_dependent() {
assert_eq!(
delete(
EngineType::DocumentStrict,
vec![SqlValue::String("k".into())]
)
.cache_eligibility(),
PlanCacheEligibility::DataDependent
);
}
#[test]
fn cp_computed_projection_is_data_dependent() {
use crate::types::query::Projection;
use crate::types_expr::SqlExpr;
let cp = Projection::CpComputed {
expr: SqlExpr::Function {
name: "nextval".into(),
args: vec![SqlExpr::Literal(SqlValue::String("s".into()))],
distinct: false,
},
alias: "nextval".into(),
};
let scan = |projection: Vec<Projection>| SqlPlan::Scan {
collection: "docs".into(),
alias: None,
engine: EngineType::KeyValue,
filters: Vec::new(),
projection,
sort_keys: Vec::new(),
limit: None,
offset: 0,
distinct: false,
window_functions: Vec::new(),
temporal: crate::temporal::TemporalScope::default(),
};
assert_eq!(
scan(vec![Projection::Column("id".into()), cp.clone()]).cache_eligibility(),
PlanCacheEligibility::DataDependent
);
assert_eq!(
scan(vec![Projection::Column("id".into())]).cache_eligibility(),
PlanCacheEligibility::Cacheable
);
let wrapped = SqlPlan::Subquery {
input: Box::new(scan(Vec::new())),
filters: Vec::new(),
projection: vec![cp],
window_functions: Vec::new(),
sort_keys: Vec::new(),
offset: 0,
distinct: false,
limit: None,
};
assert_eq!(
wrapped.cache_eligibility(),
PlanCacheEligibility::DataDependent
);
}
#[test]
fn nested_point_dependency_propagates() {
let plan = SqlPlan::Cte(CtePlan {
definitions: vec![("selected".into(), point_get(EngineType::DocumentStrict))],
outer: Box::new(point_get(EngineType::KeyValue)),
});
assert_eq!(
plan.cache_eligibility(),
PlanCacheEligibility::DataDependent
);
}
}