icydb_core/db/query/intent/
access_requirement.rs1use crate::db::query::{
7 explain::{ExplainAccessDecision, ExplainAccessDecisionKind},
8 intent::QueryError,
9 plan::AccessPlannedQuery,
10};
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum RequiredAccessPath {
15 ByKey,
17 ByKeys,
19 KeyRange,
21 IndexPrefix,
23 IndexMultiLookup,
25 IndexBranchSet,
27 IndexRange,
29 FullScan,
31 Union,
33 Intersection,
35}
36
37impl RequiredAccessPath {
38 const fn matches(self, actual: ExplainAccessDecisionKind) -> bool {
39 matches!(
40 (self, actual),
41 (Self::ByKey, ExplainAccessDecisionKind::ByKey)
42 | (Self::ByKeys, ExplainAccessDecisionKind::ByKeys)
43 | (Self::KeyRange, ExplainAccessDecisionKind::KeyRange)
44 | (Self::IndexPrefix, ExplainAccessDecisionKind::IndexPrefix)
45 | (
46 Self::IndexMultiLookup,
47 ExplainAccessDecisionKind::IndexMultiLookup
48 )
49 | (
50 Self::IndexBranchSet,
51 ExplainAccessDecisionKind::IndexBranchSet
52 )
53 | (Self::IndexRange, ExplainAccessDecisionKind::IndexRange)
54 | (Self::FullScan, ExplainAccessDecisionKind::FullScan)
55 | (Self::Union, ExplainAccessDecisionKind::Union)
56 | (Self::Intersection, ExplainAccessDecisionKind::Intersection)
57 )
58 }
59}
60
61#[derive(Clone, Debug, Default, Eq, PartialEq)]
62pub(in crate::db::query::intent) struct AccessRequirements {
63 index_required: bool,
64 named_index: Option<String>,
65 access_path: Option<RequiredAccessPath>,
66 no_residual_filter: bool,
67}
68
69impl AccessRequirements {
70 pub(in crate::db::query::intent) const fn new() -> Self {
71 Self {
72 index_required: false,
73 named_index: None,
74 access_path: None,
75 no_residual_filter: false,
76 }
77 }
78
79 pub(in crate::db::query::intent) fn validate(
80 &self,
81 plan: &AccessPlannedQuery,
82 ) -> Result<(), QueryError> {
83 if self.is_empty() {
84 return Ok(());
85 }
86
87 let explain = plan.explain();
88 let decision = explain.access_decision();
89
90 if self.index_required && !selected_access_is_secondary_index(decision.selected.kind) {
91 return Err(QueryError::from(AccessRequirementError::new(
92 AccessRequirementViolation::IndexRequired,
93 decision.clone(),
94 )));
95 }
96
97 if let Some(required_index_name) = &self.named_index
98 && decision.selected.index_name.as_deref() != Some(required_index_name.as_str())
99 {
100 return Err(QueryError::from(AccessRequirementError::new(
101 AccessRequirementViolation::NamedIndexRequired {
102 expected: required_index_name.clone(),
103 },
104 decision.clone(),
105 )));
106 }
107
108 if let Some(required_path) = self.access_path
109 && !required_path.matches(decision.selected.kind)
110 {
111 return Err(QueryError::from(AccessRequirementError::new(
112 AccessRequirementViolation::AccessPathRequired {
113 expected: required_path,
114 },
115 decision.clone(),
116 )));
117 }
118
119 if self.no_residual_filter && plan.has_any_residual_filter() {
120 return Err(QueryError::from(AccessRequirementError::new(
121 AccessRequirementViolation::ResidualFilterForbidden,
122 decision.clone(),
123 )));
124 }
125
126 Ok(())
127 }
128
129 pub(in crate::db::query::intent) const fn is_empty(&self) -> bool {
130 !self.index_required
131 && self.named_index.is_none()
132 && self.access_path.is_none()
133 && !self.no_residual_filter
134 }
135}
136
137#[derive(Debug)]
139pub struct AccessRequirementError {
140 violation: AccessRequirementViolation,
141 decision: ExplainAccessDecision,
142}
143
144impl AccessRequirementError {
145 const fn new(violation: AccessRequirementViolation, decision: ExplainAccessDecision) -> Self {
146 Self {
147 violation,
148 decision,
149 }
150 }
151
152 #[must_use]
154 pub const fn violation(&self) -> &AccessRequirementViolation {
155 &self.violation
156 }
157
158 #[must_use]
160 pub const fn decision(&self) -> &ExplainAccessDecision {
161 &self.decision
162 }
163}
164
165#[derive(Clone, Debug, Eq, PartialEq)]
167pub enum AccessRequirementViolation {
168 IndexRequired,
170 NamedIndexRequired {
172 expected: String,
174 },
175 AccessPathRequired {
177 expected: RequiredAccessPath,
179 },
180 ResidualFilterForbidden,
182}
183
184const fn selected_access_is_secondary_index(kind: ExplainAccessDecisionKind) -> bool {
185 matches!(
186 kind,
187 ExplainAccessDecisionKind::IndexPrefix
188 | ExplainAccessDecisionKind::IndexMultiLookup
189 | ExplainAccessDecisionKind::IndexBranchSet
190 | ExplainAccessDecisionKind::IndexRange
191 )
192}