1#[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}