icydb_core/db/query/intent/
access_requirement.rs1use crate::db::query::{
7 explain::{ExplainAccessDecisionKind, ExplainAccessDecisionV1},
8 intent::QueryError,
9 plan::AccessPlannedQuery,
10};
11use thiserror::Error as ThisError;
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub enum RequiredAccessPath {
16 ByKey,
18 ByKeys,
20 KeyRange,
22 IndexPrefix,
24 IndexMultiLookup,
26 IndexRange,
28 FullScan,
30 Union,
32 Intersection,
34}
35
36impl RequiredAccessPath {
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, ThisError)]
162#[error(
163 "query access requirement failed: {violation}; selected={selected_label}",
164 selected_label = decision.selected.label
165)]
166pub struct AccessRequirementError {
167 violation: AccessRequirementViolation,
168 decision: ExplainAccessDecisionV1,
169}
170
171impl AccessRequirementError {
172 pub(in crate::db) const fn new(
173 violation: AccessRequirementViolation,
174 decision: ExplainAccessDecisionV1,
175 ) -> Self {
176 Self {
177 violation,
178 decision,
179 }
180 }
181
182 #[must_use]
184 pub const fn violation(&self) -> &AccessRequirementViolation {
185 &self.violation
186 }
187
188 #[must_use]
190 pub const fn decision(&self) -> &ExplainAccessDecisionV1 {
191 &self.decision
192 }
193}
194
195#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
197pub enum AccessRequirementViolation {
198 #[error("secondary index access required")]
200 IndexRequired,
201 #[error("index '{expected}' required")]
203 NamedIndexRequired {
204 expected: String,
206 },
207 #[error("access path '{}' required", expected.code())]
209 AccessPathRequired {
210 expected: RequiredAccessPath,
212 },
213 #[error("residual filter forbidden")]
215 ResidualFilterForbidden,
216}
217
218const fn selected_access_is_secondary_index(kind: ExplainAccessDecisionKind) -> bool {
219 matches!(
220 kind,
221 ExplainAccessDecisionKind::IndexPrefix
222 | ExplainAccessDecisionKind::IndexMultiLookup
223 | ExplainAccessDecisionKind::IndexRange
224 )
225}