Skip to main content

icydb_core/db/query/intent/
access_requirement.rs

1//! Module: query::intent::access_requirement
2//! Responsibility: fail-closed query access assertions evaluated after planning.
3//! Does not own: optimizer ranking or physical access selection.
4//! Boundary: query contracts inspect the selected plan without acting as hints.
5
6use crate::db::query::{
7    explain::{ExplainAccessDecision, ExplainAccessDecisionKind},
8    intent::QueryError,
9    plan::AccessPlannedQuery,
10};
11
12/// Required selected access path for fail-closed query contracts.
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum RequiredAccessPath {
15    /// Require primary-key lookup.
16    ByKey,
17    /// Require multiple primary-key lookup.
18    ByKeys,
19    /// Require primary-key range lookup.
20    KeyRange,
21    /// Require secondary-index equality-prefix access.
22    IndexPrefix,
23    /// Require secondary-index multi-lookup access.
24    IndexMultiLookup,
25    /// Require secondary-index branch-set access.
26    IndexBranchSet,
27    /// Require secondary-index range access.
28    IndexRange,
29    /// Require full scan access.
30    FullScan,
31    /// Require union access.
32    Union,
33    /// Require intersection access.
34    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/// Query access requirement failure with the selected decision preserved.
138#[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    /// Borrow the violated access requirement.
153    #[must_use]
154    pub const fn violation(&self) -> &AccessRequirementViolation {
155        &self.violation
156    }
157
158    /// Borrow the selected access decision that failed the requirement.
159    #[must_use]
160    pub const fn decision(&self) -> &ExplainAccessDecision {
161        &self.decision
162    }
163}
164
165/// Specific fail-closed access requirement that was not satisfied.
166#[derive(Clone, Debug, Eq, PartialEq)]
167pub enum AccessRequirementViolation {
168    /// A secondary-index route was required but not selected.
169    IndexRequired,
170    /// One specific semantic index name was required but not selected.
171    NamedIndexRequired {
172        /// Required semantic index name.
173        expected: String,
174    },
175    /// One selected access path kind was required but not selected.
176    AccessPathRequired {
177        /// Required selected access path.
178        expected: RequiredAccessPath,
179    },
180    /// Residual predicate or scalar filter work was forbidden.
181    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}