1use std::fmt;
8
9use alopex_core::{EncodedRowKeyRange, ReadAtPoint, RowKeyRange};
10
11use crate::catalog::TableMetadata;
12
13use super::error::Result;
14use super::{KeyEncoder, SqlValue, StorageError, TableScanIterator};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct RangeReadSnapshot {
19 read_at: ReadAtPoint,
20 schema_manifest_id: String,
21}
22
23impl RangeReadSnapshot {
24 pub fn new(
26 read_at: ReadAtPoint,
27 schema_manifest_id: impl Into<String>,
28 ) -> std::result::Result<Self, StorageRangeConstraintError> {
29 let schema_manifest_id = schema_manifest_id.into();
30 if schema_manifest_id.trim().is_empty() {
31 return Err(StorageRangeConstraintError::MissingSchemaManifest);
32 }
33 Ok(Self {
34 read_at,
35 schema_manifest_id,
36 })
37 }
38
39 pub const fn read_at(&self) -> ReadAtPoint {
41 self.read_at
42 }
43
44 pub fn schema_manifest_id(&self) -> &str {
46 &self.schema_manifest_id
47 }
48
49 pub const fn catalog_version(&self) -> u64 {
51 self.read_at.metadata_version
52 }
53
54 pub const fn schema_epoch(&self) -> u64 {
56 self.read_at.schema_epoch
57 }
58
59 pub const fn index_epoch(&self) -> u64 {
61 self.read_at.index_epoch
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct StorageRangeConstraint {
72 range_id: String,
73 generation: u64,
74 row_key_range: RowKeyRange,
75 encoded_bounds: EncodedRowKeyRange,
76 snapshot: RangeReadSnapshot,
77}
78
79impl StorageRangeConstraint {
80 pub fn new(
82 range_id: impl Into<String>,
83 generation: u64,
84 row_key_range: RowKeyRange,
85 snapshot: RangeReadSnapshot,
86 ) -> std::result::Result<Self, StorageRangeConstraintError> {
87 let range_id = range_id.into();
88 if range_id.trim().is_empty() {
89 return Err(StorageRangeConstraintError::MissingRangeIdentity);
90 }
91 Ok(Self {
92 range_id,
93 generation,
94 encoded_bounds: row_key_range.encoded_bounds(),
95 row_key_range,
96 snapshot,
97 })
98 }
99
100 pub fn range_id(&self) -> &str {
102 &self.range_id
103 }
104
105 pub const fn generation(&self) -> u64 {
107 self.generation
108 }
109
110 pub const fn table_id(&self) -> u32 {
112 self.row_key_range.table_id()
113 }
114
115 pub const fn row_key_range(&self) -> RowKeyRange {
117 self.row_key_range
118 }
119
120 pub fn encoded_bounds(&self) -> (&[u8], &[u8]) {
122 (
123 &self.encoded_bounds.lower_inclusive,
124 &self.encoded_bounds.upper_exclusive,
125 )
126 }
127
128 pub fn snapshot(&self) -> &RangeReadSnapshot {
130 &self.snapshot
131 }
132
133 pub fn validate_table(&self, table_meta: &TableMetadata) -> Result<()> {
135 if table_meta.table_id != self.table_id() {
136 return Err(StorageError::CorruptedData {
137 reason: format!(
138 "range {} is fenced for table {}, not table {}",
139 self.range_id,
140 self.table_id(),
141 table_meta.table_id
142 ),
143 });
144 }
145 Ok(())
146 }
147
148 pub fn validate_read_at(&self, actual: Option<ReadAtPoint>) -> Result<()> {
150 match actual {
151 Some(actual) if actual == self.snapshot.read_at() => Ok(()),
152 Some(actual) => Err(StorageError::CorruptedData {
153 reason: format!(
154 "range {} read-at fence mismatch: expected {:?}, got {:?}",
155 self.range_id,
156 self.snapshot.read_at(),
157 actual
158 ),
159 }),
160 None => Err(StorageError::CorruptedData {
161 reason: format!(
162 "range {} requires a transaction opened at its read-at fence",
163 self.range_id
164 ),
165 }),
166 }
167 }
168
169 pub fn contains_primary_key(&self, encoded_primary_key: &[u8]) -> Result<bool> {
171 KeyEncoder::primary_key_is_in_range(encoded_primary_key, self.row_key_range)
172 }
173
174 pub fn recheck_secondary_index_hits<I>(&self, row_ids: I) -> Result<Vec<u64>>
180 where
181 I: IntoIterator<Item = u64>,
182 {
183 row_ids
184 .into_iter()
185 .filter_map(|row_id| {
186 match self.contains_primary_key(&KeyEncoder::row_key(self.table_id(), row_id)) {
187 Ok(true) => Some(Ok(row_id)),
188 Ok(false) => None,
189 Err(error) => Some(Err(error)),
190 }
191 })
192 .collect()
193 }
194}
195
196pub struct RangeBoundedScanIterator<'a> {
198 inner: TableScanIterator<'a>,
199 constraint: StorageRangeConstraint,
200}
201
202impl<'a> RangeBoundedScanIterator<'a> {
203 pub fn new(inner: TableScanIterator<'a>, constraint: StorageRangeConstraint) -> Self {
205 Self { inner, constraint }
206 }
207}
208
209impl Iterator for RangeBoundedScanIterator<'_> {
210 type Item = Result<(u64, Vec<SqlValue>)>;
211
212 fn next(&mut self) -> Option<Self::Item> {
213 self.inner.next().map(|entry| {
214 let (row_id, values) = entry?;
215 let key = KeyEncoder::row_key(self.constraint.table_id(), row_id);
216 if self.constraint.contains_primary_key(&key)? {
217 Ok((row_id, values))
218 } else {
219 Err(StorageError::CorruptedData {
220 reason: format!(
221 "storage scan returned row {} outside fenced range {}",
222 row_id,
223 self.constraint.range_id()
224 ),
225 })
226 }
227 })
228 }
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
233pub enum StorageRangeConstraintError {
234 MissingRangeIdentity,
236 MissingSchemaManifest,
238}
239
240impl fmt::Display for StorageRangeConstraintError {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 match self {
243 Self::MissingRangeIdentity => f.write_str("range constraint has no range identity"),
244 Self::MissingSchemaManifest => {
245 f.write_str("range constraint has no schema-manifest identity")
246 }
247 }
248 }
249}
250
251impl std::error::Error for StorageRangeConstraintError {}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use crate::storage::{RowCodec, TableScanIterator};
257 use alopex_core::CanonicalRowKey;
258
259 fn constraint() -> StorageRangeConstraint {
260 let snapshot =
261 RangeReadSnapshot::new(ReadAtPoint::new(7, 11, 13, 17), "schema-13").unwrap();
262 StorageRangeConstraint::new(
263 "range-a",
264 3,
265 RowKeyRange::new(7, Some(10), Some(20)).unwrap(),
266 snapshot,
267 )
268 .unwrap()
269 }
270
271 #[test]
272 fn constraint_uses_concrete_canonical_bounds_not_a_prefix_scan() {
273 let constraint = constraint();
274 let (lower, upper) = constraint.encoded_bounds();
275
276 assert_eq!(lower, KeyEncoder::row_key(7, 10));
277 assert_eq!(upper, KeyEncoder::row_key(7, 20));
278 assert_ne!(lower, KeyEncoder::table_prefix(7));
279 }
280
281 #[test]
282 fn secondary_index_candidates_are_rechecked_against_primary_bounds() {
283 let constraint = constraint();
284
285 assert_eq!(
286 constraint
287 .recheck_secondary_index_hits([9, 10, 19, 20])
288 .unwrap(),
289 vec![10, 19]
290 );
291 }
292
293 #[test]
294 fn scan_wrapper_never_emits_an_out_of_range_primary_row() {
295 let entries = vec![(
296 KeyEncoder::row_key(7, 20),
297 RowCodec::encode(&[SqlValue::Integer(20)]),
298 )];
299 let table_scan = TableScanIterator::new(Box::new(entries.into_iter()), 7);
300 let mut scan = RangeBoundedScanIterator::new(table_scan, constraint());
301
302 assert!(matches!(
303 scan.next(),
304 Some(Err(StorageError::CorruptedData { .. }))
305 ));
306 }
307
308 #[test]
309 fn constraint_requires_route_and_schema_identities() {
310 let point = ReadAtPoint::new(1, 2, 3, 4);
311 assert_eq!(
312 RangeReadSnapshot::new(point, ""),
313 Err(StorageRangeConstraintError::MissingSchemaManifest)
314 );
315 let snapshot = RangeReadSnapshot::new(point, "schema-3").unwrap();
316 assert_eq!(
317 StorageRangeConstraint::new("", 1, RowKeyRange::full_table(7), snapshot),
318 Err(StorageRangeConstraintError::MissingRangeIdentity)
319 );
320 }
321
322 #[test]
323 fn constraint_rejects_a_different_read_at_fence() {
324 let constraint = constraint();
325 assert!(
326 constraint
327 .validate_read_at(Some(ReadAtPoint::new(7, 11, 13, 18)))
328 .is_err()
329 );
330 assert!(constraint.validate_read_at(None).is_err());
331 assert!(
332 constraint
333 .validate_read_at(Some(ReadAtPoint::new(7, 11, 13, 17)))
334 .is_ok()
335 );
336 }
337
338 #[test]
339 fn primary_key_recheck_rejects_malformed_keys() {
340 let constraint = constraint();
341 assert!(matches!(
342 constraint.contains_primary_key(&[0x01]),
343 Err(StorageError::InvalidKeyFormat)
344 ));
345 assert!(
346 !constraint
347 .contains_primary_key(&CanonicalRowKey::new(8, 19).encode())
348 .unwrap()
349 );
350 }
351}