use crate::db::query::{
explain::{ExplainAccessDecisionKind, ExplainAccessDecisionV1},
intent::QueryError,
plan::AccessPlannedQuery,
};
use thiserror::Error as ThisError;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RequiredAccessPath {
ByKey,
ByKeys,
KeyRange,
IndexPrefix,
IndexMultiLookup,
IndexRange,
FullScan,
Union,
Intersection,
}
impl RequiredAccessPath {
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::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::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) struct AccessRequirements {
index_required: bool,
named_index: Option<String>,
access_path: Option<RequiredAccessPath>,
no_residual_filter: bool,
}
impl AccessRequirements {
pub(in crate::db) const fn new() -> Self {
Self {
index_required: false,
named_index: None,
access_path: None,
no_residual_filter: false,
}
}
pub(in crate::db) const fn require_index(&mut self) {
self.index_required = true;
}
pub(in crate::db) 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) const fn require_access_path(&mut self, path: RequiredAccessPath) {
self.access_path = Some(path);
}
pub(in crate::db) const fn require_no_residual_filter(&mut self) {
self.no_residual_filter = true;
}
pub(in crate::db) 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) 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, ThisError)]
#[error(
"query access requirement failed: {violation}; selected={selected_label}",
selected_label = decision.selected.label
)]
pub struct AccessRequirementError {
violation: AccessRequirementViolation,
decision: ExplainAccessDecisionV1,
}
impl AccessRequirementError {
pub(in crate::db) 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, ThisError)]
pub enum AccessRequirementViolation {
#[error("secondary index access required")]
IndexRequired,
#[error("index '{expected}' required")]
NamedIndexRequired {
expected: String,
},
#[error("access path '{}' required", expected.code())]
AccessPathRequired {
expected: RequiredAccessPath,
},
#[error("residual filter forbidden")]
ResidualFilterForbidden,
}
const fn selected_access_is_secondary_index(kind: ExplainAccessDecisionKind) -> bool {
matches!(
kind,
ExplainAccessDecisionKind::IndexPrefix
| ExplainAccessDecisionKind::IndexMultiLookup
| ExplainAccessDecisionKind::IndexRange
)
}