Skip to main content

hawdb_cascades/
properties.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
16pub struct RequiredProperties {
17    pub distribution: Distribution,
18    pub ordering: Vec<String>,
19    pub covering_fields: Vec<String>,
20    pub requires_scan_pruning: bool,
21    pub requires_raw_vector_rerank: bool,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
25pub struct PhysicalProperties {
26    pub distribution: Distribution,
27    pub ordering: Vec<String>,
28    pub covering_fields: Vec<String>,
29    pub scan_pruning: ScanPruningSupport,
30    pub vector_precision: VectorPrecision,
31    pub memory_budget: MemoryBudgetClass,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
35pub enum Distribution {
36    #[default]
37    Any,
38    Single,
39    Hash(Vec<String>),
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
43pub enum ScanPruningSupport {
44    #[default]
45    Unknown,
46    None,
47    Label,
48    Segment,
49    Index,
50    ExactEmpty,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
54pub enum VectorPrecision {
55    #[default]
56    NotVector,
57    ApproximateCandidate,
58    RawReranked,
59    Exact,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
63pub enum MemoryBudgetClass {
64    #[default]
65    Unknown,
66    Constant,
67    RowLinear,
68    Blocking,
69}
70
71impl PhysicalProperties {
72    pub fn satisfies(&self, required: &RequiredProperties) -> bool {
73        distribution_satisfies(&self.distribution, &required.distribution)
74            && ordering_satisfies(&self.ordering, &required.ordering)
75            && covering_satisfies(&self.covering_fields, &required.covering_fields)
76            && scan_pruning_satisfies(self.scan_pruning, required.requires_scan_pruning)
77            && vector_rerank_satisfies(self.vector_precision, required.requires_raw_vector_rerank)
78    }
79}
80
81impl Distribution {
82    pub fn as_str(&self) -> &'static str {
83        match self {
84            Distribution::Any => "any",
85            Distribution::Single => "single",
86            Distribution::Hash(_) => "hash",
87        }
88    }
89}
90
91impl ScanPruningSupport {
92    pub fn as_str(self) -> &'static str {
93        match self {
94            ScanPruningSupport::Unknown => "unknown",
95            ScanPruningSupport::None => "none",
96            ScanPruningSupport::Label => "label",
97            ScanPruningSupport::Segment => "segment",
98            ScanPruningSupport::Index => "index",
99            ScanPruningSupport::ExactEmpty => "exact_empty",
100        }
101    }
102
103    pub fn is_prunable(self) -> bool {
104        matches!(
105            self,
106            ScanPruningSupport::Label
107                | ScanPruningSupport::Segment
108                | ScanPruningSupport::Index
109                | ScanPruningSupport::ExactEmpty
110        )
111    }
112}
113
114impl VectorPrecision {
115    pub fn as_str(self) -> &'static str {
116        match self {
117            VectorPrecision::NotVector => "not_vector",
118            VectorPrecision::ApproximateCandidate => "approximate_candidate",
119            VectorPrecision::RawReranked => "raw_reranked",
120            VectorPrecision::Exact => "exact",
121        }
122    }
123
124    pub fn includes_raw_rerank(self) -> bool {
125        matches!(self, VectorPrecision::RawReranked | VectorPrecision::Exact)
126    }
127}
128
129impl MemoryBudgetClass {
130    pub fn as_str(self) -> &'static str {
131        match self {
132            MemoryBudgetClass::Unknown => "unknown",
133            MemoryBudgetClass::Constant => "constant",
134            MemoryBudgetClass::RowLinear => "row_linear",
135            MemoryBudgetClass::Blocking => "blocking",
136        }
137    }
138}
139
140fn distribution_satisfies(actual: &Distribution, required: &Distribution) -> bool {
141    match required {
142        Distribution::Any => true,
143        Distribution::Single => actual == required,
144        Distribution::Hash(keys) => {
145            matches!(actual, Distribution::Hash(actual_keys) if actual_keys == keys)
146        }
147    }
148}
149
150fn covering_satisfies(actual: &[String], required: &[String]) -> bool {
151    required.iter().all(|field| actual.contains(field))
152}
153
154fn scan_pruning_satisfies(actual: ScanPruningSupport, required: bool) -> bool {
155    !required || actual.is_prunable()
156}
157
158fn vector_rerank_satisfies(actual: VectorPrecision, required: bool) -> bool {
159    !required || actual.includes_raw_rerank()
160}
161
162fn ordering_satisfies(actual: &[String], required: &[String]) -> bool {
163    required.is_empty()
164        || (actual.len() >= required.len()
165            && actual
166                .iter()
167                .zip(required.iter())
168                .all(|(actual, required)| actual == required))
169}
170
171#[cfg(test)]
172mod tests {
173    use super::{
174        Distribution, MemoryBudgetClass, PhysicalProperties, RequiredProperties,
175        ScanPruningSupport, VectorPrecision,
176    };
177
178    #[test]
179    fn empty_required_properties_accept_any_plan() {
180        let actual = PhysicalProperties {
181            distribution: Distribution::Hash(vec!["space_id".to_string()]),
182            ordering: vec!["updated_at".to_string()],
183            scan_pruning: ScanPruningSupport::Index,
184            ..PhysicalProperties::default()
185        };
186
187        assert!(actual.satisfies(&RequiredProperties::default()));
188    }
189
190    #[test]
191    fn ordering_requirement_accepts_prefix_match() {
192        let actual = PhysicalProperties {
193            distribution: Distribution::Single,
194            ordering: vec!["space_id".to_string(), "updated_at".to_string()],
195            ..PhysicalProperties::default()
196        };
197        let required = RequiredProperties {
198            distribution: Distribution::Single,
199            ordering: vec!["space_id".to_string()],
200            ..RequiredProperties::default()
201        };
202
203        assert!(actual.satisfies(&required));
204    }
205
206    #[test]
207    fn embedded_requirements_check_covering_pruning_and_vector_rerank() {
208        let actual = PhysicalProperties {
209            distribution: Distribution::Single,
210            covering_fields: vec!["Memory.id".to_string(), "Memory.title".to_string()],
211            scan_pruning: ScanPruningSupport::Index,
212            vector_precision: VectorPrecision::RawReranked,
213            memory_budget: MemoryBudgetClass::RowLinear,
214            ..PhysicalProperties::default()
215        };
216
217        assert!(actual.satisfies(&RequiredProperties {
218            distribution: Distribution::Single,
219            covering_fields: vec!["Memory.id".to_string()],
220            requires_scan_pruning: true,
221            requires_raw_vector_rerank: true,
222            ..RequiredProperties::default()
223        }));
224        assert!(!actual.satisfies(&RequiredProperties {
225            covering_fields: vec!["Memory.embedding".to_string()],
226            ..RequiredProperties::default()
227        }));
228        assert!(
229            !PhysicalProperties::default().satisfies(&RequiredProperties {
230                requires_scan_pruning: true,
231                ..RequiredProperties::default()
232            })
233        );
234    }
235
236    #[test]
237    fn distribution_exposes_stable_names() {
238        assert_eq!(Distribution::Any.as_str(), "any");
239        assert_eq!(Distribution::Single.as_str(), "single");
240        assert_eq!(
241            Distribution::Hash(vec!["space_id".to_string()]).as_str(),
242            "hash"
243        );
244        assert_eq!(ScanPruningSupport::Index.as_str(), "index");
245        assert_eq!(ScanPruningSupport::Segment.as_str(), "segment");
246        assert_eq!(VectorPrecision::RawReranked.as_str(), "raw_reranked");
247        assert_eq!(MemoryBudgetClass::Blocking.as_str(), "blocking");
248    }
249}