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