use crate::db::query::{
explain::{ExplainAccessDecisionKind, ExplainAccessDecisionV1},
intent::QueryError,
plan::AccessPlannedQuery,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RequiredAccessPath {
ByKey,
ByKeys,
KeyRange,
IndexPrefix,
IndexMultiLookup,
IndexBranchSet,
IndexRange,
FullScan,
Union,
Intersection,
}
impl RequiredAccessPath {
#[cfg(test)]
pub(in crate::db) const fn code(self) -> &'static str {
match self {
Self::ByKey => "ByKey",
Self::ByKeys => "ByKeys",
Self::KeyRange => "KeyRange",
Self::IndexPrefix => "IndexPrefix",
Self::IndexMultiLookup => "IndexMultiLookup",
Self::IndexBranchSet => "IndexBranchSet",
Self::IndexRange => "IndexRange",
Self::FullScan => "FullScan",
Self::Union => "Union",
Self::Intersection => "Intersection",
}
}
const fn matches(self, actual: ExplainAccessDecisionKind) -> bool {
matches!(
(self, actual),
(Self::ByKey, ExplainAccessDecisionKind::ByKey)
| (Self::ByKeys, ExplainAccessDecisionKind::ByKeys)
| (Self::KeyRange, ExplainAccessDecisionKind::KeyRange)
| (Self::IndexPrefix, ExplainAccessDecisionKind::IndexPrefix)
| (
Self::IndexMultiLookup,
ExplainAccessDecisionKind::IndexMultiLookup
)
| (
Self::IndexBranchSet,
ExplainAccessDecisionKind::IndexBranchSet
)
| (Self::IndexRange, ExplainAccessDecisionKind::IndexRange)
| (Self::FullScan, ExplainAccessDecisionKind::FullScan)
| (Self::Union, ExplainAccessDecisionKind::Union)
| (Self::Intersection, ExplainAccessDecisionKind::Intersection)
)
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub(in crate::db::query::intent) struct AccessRequirements {
index_required: bool,
named_index: Option<String>,
access_path: Option<RequiredAccessPath>,
no_residual_filter: bool,
}
impl AccessRequirements {
pub(in crate::db::query::intent) const fn new() -> Self {
Self {
index_required: false,
named_index: None,
access_path: None,
no_residual_filter: false,
}
}
pub(in crate::db::query::intent) const fn require_index(&mut self) {
self.index_required = true;
}
pub(in crate::db::query::intent) fn require_index_named(
&mut self,
index_name: impl Into<String>,
) {
self.index_required = true;
self.named_index = Some(index_name.into());
}
pub(in crate::db::query::intent) const fn require_access_path(
&mut self,
path: RequiredAccessPath,
) {
self.access_path = Some(path);
}
pub(in crate::db::query::intent) const fn require_no_residual_filter(&mut self) {
self.no_residual_filter = true;
}
pub(in crate::db::query::intent) fn validate(
&self,
plan: &AccessPlannedQuery,
) -> Result<(), QueryError> {
if self.is_empty() {
return Ok(());
}
let explain = plan.explain();
let decision = explain.access_decision();
if self.index_required && !selected_access_is_secondary_index(decision.selected.kind) {
return Err(QueryError::from(AccessRequirementError::new(
AccessRequirementViolation::IndexRequired,
decision.clone(),
)));
}
if let Some(required_index_name) = &self.named_index
&& decision.selected.index_name.as_deref() != Some(required_index_name.as_str())
{
return Err(QueryError::from(AccessRequirementError::new(
AccessRequirementViolation::NamedIndexRequired {
expected: required_index_name.clone(),
},
decision.clone(),
)));
}
if let Some(required_path) = self.access_path
&& !required_path.matches(decision.selected.kind)
{
return Err(QueryError::from(AccessRequirementError::new(
AccessRequirementViolation::AccessPathRequired {
expected: required_path,
},
decision.clone(),
)));
}
if self.no_residual_filter && plan.has_any_residual_filter() {
return Err(QueryError::from(AccessRequirementError::new(
AccessRequirementViolation::ResidualFilterForbidden,
decision.clone(),
)));
}
Ok(())
}
pub(in crate::db::query::intent) const fn is_empty(&self) -> bool {
!self.index_required
&& self.named_index.is_none()
&& self.access_path.is_none()
&& !self.no_residual_filter
}
}
#[derive(Debug)]
pub struct AccessRequirementError {
violation: AccessRequirementViolation,
decision: ExplainAccessDecisionV1,
}
impl AccessRequirementError {
const fn new(violation: AccessRequirementViolation, decision: ExplainAccessDecisionV1) -> Self {
Self {
violation,
decision,
}
}
#[must_use]
pub const fn violation(&self) -> &AccessRequirementViolation {
&self.violation
}
#[must_use]
pub const fn decision(&self) -> &ExplainAccessDecisionV1 {
&self.decision
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AccessRequirementViolation {
IndexRequired,
NamedIndexRequired {
expected: String,
},
AccessPathRequired {
expected: RequiredAccessPath,
},
ResidualFilterForbidden,
}
const fn selected_access_is_secondary_index(kind: ExplainAccessDecisionKind) -> bool {
matches!(
kind,
ExplainAccessDecisionKind::IndexPrefix
| ExplainAccessDecisionKind::IndexMultiLookup
| ExplainAccessDecisionKind::IndexBranchSet
| ExplainAccessDecisionKind::IndexRange
)
}