icydb_core/db/query/intent/
access_requirement.rs1use crate::db::query::{
7 explain::{ExplainAccessDecisionKind, ExplainAccessDecisionV1},
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 #[cfg(test)]
39 pub(in crate::db) const fn code(self) -> &'static str {
40 match self {
41 Self::ByKey => "ByKey",
42 Self::ByKeys => "ByKeys",
43 Self::KeyRange => "KeyRange",
44 Self::IndexPrefix => "IndexPrefix",
45 Self::IndexMultiLookup => "IndexMultiLookup",
46 Self::IndexBranchSet => "IndexBranchSet",
47 Self::IndexRange => "IndexRange",
48 Self::FullScan => "FullScan",
49 Self::Union => "Union",
50 Self::Intersection => "Intersection",
51 }
52 }
53
54 const fn matches(self, actual: ExplainAccessDecisionKind) -> bool {
55 matches!(
56 (self, actual),
57 (Self::ByKey, ExplainAccessDecisionKind::ByKey)
58 | (Self::ByKeys, ExplainAccessDecisionKind::ByKeys)
59 | (Self::KeyRange, ExplainAccessDecisionKind::KeyRange)
60 | (Self::IndexPrefix, ExplainAccessDecisionKind::IndexPrefix)
61 | (
62 Self::IndexMultiLookup,
63 ExplainAccessDecisionKind::IndexMultiLookup
64 )
65 | (
66 Self::IndexBranchSet,
67 ExplainAccessDecisionKind::IndexBranchSet
68 )
69 | (Self::IndexRange, ExplainAccessDecisionKind::IndexRange)
70 | (Self::FullScan, ExplainAccessDecisionKind::FullScan)
71 | (Self::Union, ExplainAccessDecisionKind::Union)
72 | (Self::Intersection, ExplainAccessDecisionKind::Intersection)
73 )
74 }
75}
76
77#[derive(Clone, Debug, Default, Eq, PartialEq)]
78pub(in crate::db::query::intent) struct AccessRequirements {
79 index_required: bool,
80 named_index: Option<String>,
81 access_path: Option<RequiredAccessPath>,
82 no_residual_filter: bool,
83}
84
85impl AccessRequirements {
86 pub(in crate::db::query::intent) const fn new() -> Self {
87 Self {
88 index_required: false,
89 named_index: None,
90 access_path: None,
91 no_residual_filter: false,
92 }
93 }
94
95 pub(in crate::db::query::intent) const fn require_index(&mut self) {
96 self.index_required = true;
97 }
98
99 pub(in crate::db::query::intent) fn require_index_named(
100 &mut self,
101 index_name: impl Into<String>,
102 ) {
103 self.index_required = true;
104 self.named_index = Some(index_name.into());
105 }
106
107 pub(in crate::db::query::intent) const fn require_access_path(
108 &mut self,
109 path: RequiredAccessPath,
110 ) {
111 self.access_path = Some(path);
112 }
113
114 pub(in crate::db::query::intent) const fn require_no_residual_filter(&mut self) {
115 self.no_residual_filter = true;
116 }
117
118 pub(in crate::db::query::intent) fn validate(
119 &self,
120 plan: &AccessPlannedQuery,
121 ) -> Result<(), QueryError> {
122 if self.is_empty() {
123 return Ok(());
124 }
125
126 let explain = plan.explain();
127 let decision = explain.access_decision();
128
129 if self.index_required && !selected_access_is_secondary_index(decision.selected.kind) {
130 return Err(QueryError::from(AccessRequirementError::new(
131 AccessRequirementViolation::IndexRequired,
132 decision.clone(),
133 )));
134 }
135
136 if let Some(required_index_name) = &self.named_index
137 && decision.selected.index_name.as_deref() != Some(required_index_name.as_str())
138 {
139 return Err(QueryError::from(AccessRequirementError::new(
140 AccessRequirementViolation::NamedIndexRequired {
141 expected: required_index_name.clone(),
142 },
143 decision.clone(),
144 )));
145 }
146
147 if let Some(required_path) = self.access_path
148 && !required_path.matches(decision.selected.kind)
149 {
150 return Err(QueryError::from(AccessRequirementError::new(
151 AccessRequirementViolation::AccessPathRequired {
152 expected: required_path,
153 },
154 decision.clone(),
155 )));
156 }
157
158 if self.no_residual_filter && plan.has_any_residual_filter() {
159 return Err(QueryError::from(AccessRequirementError::new(
160 AccessRequirementViolation::ResidualFilterForbidden,
161 decision.clone(),
162 )));
163 }
164
165 Ok(())
166 }
167
168 pub(in crate::db::query::intent) const fn is_empty(&self) -> bool {
169 !self.index_required
170 && self.named_index.is_none()
171 && self.access_path.is_none()
172 && !self.no_residual_filter
173 }
174}
175
176#[derive(Debug)]
178pub struct AccessRequirementError {
179 violation: AccessRequirementViolation,
180 decision: ExplainAccessDecisionV1,
181}
182
183impl AccessRequirementError {
184 const fn new(violation: AccessRequirementViolation, decision: ExplainAccessDecisionV1) -> Self {
185 Self {
186 violation,
187 decision,
188 }
189 }
190
191 #[must_use]
193 pub const fn violation(&self) -> &AccessRequirementViolation {
194 &self.violation
195 }
196
197 #[must_use]
199 pub const fn decision(&self) -> &ExplainAccessDecisionV1 {
200 &self.decision
201 }
202}
203
204#[derive(Clone, Debug, Eq, PartialEq)]
206pub enum AccessRequirementViolation {
207 IndexRequired,
209 NamedIndexRequired {
211 expected: String,
213 },
214 AccessPathRequired {
216 expected: RequiredAccessPath,
218 },
219 ResidualFilterForbidden,
221}
222
223const fn selected_access_is_secondary_index(kind: ExplainAccessDecisionKind) -> bool {
224 matches!(
225 kind,
226 ExplainAccessDecisionKind::IndexPrefix
227 | ExplainAccessDecisionKind::IndexMultiLookup
228 | ExplainAccessDecisionKind::IndexBranchSet
229 | ExplainAccessDecisionKind::IndexRange
230 )
231}