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) 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) 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) const fn require_index(&mut self) {
96 self.index_required = true;
97 }
98
99 pub(in crate::db) fn require_index_named(&mut self, index_name: impl Into<String>) {
100 self.index_required = true;
101 self.named_index = Some(index_name.into());
102 }
103
104 pub(in crate::db) const fn require_access_path(&mut self, path: RequiredAccessPath) {
105 self.access_path = Some(path);
106 }
107
108 pub(in crate::db) const fn require_no_residual_filter(&mut self) {
109 self.no_residual_filter = true;
110 }
111
112 pub(in crate::db) fn validate(&self, plan: &AccessPlannedQuery) -> Result<(), QueryError> {
113 if self.is_empty() {
114 return Ok(());
115 }
116
117 let explain = plan.explain();
118 let decision = explain.access_decision();
119
120 if self.index_required && !selected_access_is_secondary_index(decision.selected.kind) {
121 return Err(QueryError::from(AccessRequirementError::new(
122 AccessRequirementViolation::IndexRequired,
123 decision.clone(),
124 )));
125 }
126
127 if let Some(required_index_name) = &self.named_index
128 && decision.selected.index_name.as_deref() != Some(required_index_name.as_str())
129 {
130 return Err(QueryError::from(AccessRequirementError::new(
131 AccessRequirementViolation::NamedIndexRequired {
132 expected: required_index_name.clone(),
133 },
134 decision.clone(),
135 )));
136 }
137
138 if let Some(required_path) = self.access_path
139 && !required_path.matches(decision.selected.kind)
140 {
141 return Err(QueryError::from(AccessRequirementError::new(
142 AccessRequirementViolation::AccessPathRequired {
143 expected: required_path,
144 },
145 decision.clone(),
146 )));
147 }
148
149 if self.no_residual_filter && plan.has_any_residual_filter() {
150 return Err(QueryError::from(AccessRequirementError::new(
151 AccessRequirementViolation::ResidualFilterForbidden,
152 decision.clone(),
153 )));
154 }
155
156 Ok(())
157 }
158
159 pub(in crate::db) const fn is_empty(&self) -> bool {
160 !self.index_required
161 && self.named_index.is_none()
162 && self.access_path.is_none()
163 && !self.no_residual_filter
164 }
165}
166
167#[derive(Debug)]
169pub struct AccessRequirementError {
170 violation: AccessRequirementViolation,
171 decision: ExplainAccessDecisionV1,
172}
173
174impl AccessRequirementError {
175 pub(in crate::db) const fn new(
176 violation: AccessRequirementViolation,
177 decision: ExplainAccessDecisionV1,
178 ) -> Self {
179 Self {
180 violation,
181 decision,
182 }
183 }
184
185 #[must_use]
187 pub const fn violation(&self) -> &AccessRequirementViolation {
188 &self.violation
189 }
190
191 #[must_use]
193 pub const fn decision(&self) -> &ExplainAccessDecisionV1 {
194 &self.decision
195 }
196}
197
198#[derive(Clone, Debug, Eq, PartialEq)]
200pub enum AccessRequirementViolation {
201 IndexRequired,
203 NamedIndexRequired {
205 expected: String,
207 },
208 AccessPathRequired {
210 expected: RequiredAccessPath,
212 },
213 ResidualFilterForbidden,
215}
216
217const fn selected_access_is_secondary_index(kind: ExplainAccessDecisionKind) -> bool {
218 matches!(
219 kind,
220 ExplainAccessDecisionKind::IndexPrefix
221 | ExplainAccessDecisionKind::IndexMultiLookup
222 | ExplainAccessDecisionKind::IndexBranchSet
223 | ExplainAccessDecisionKind::IndexRange
224 )
225}