Skip to main content

luci/storage/
directory.rs

1use crate::core::{FieldId, LuciError, Result, SegmentId};
2
3use crate::storage::block::{BLOCK_SIZE, BlockId, Extent};
4
5/// An entry in the segment directory mapping a segment to its block location.
6///
7/// Each live segment is tracked in the metadata root block. The generation
8/// counter supports snapshot isolation — readers see a consistent view of
9/// which segments existed at their open time.
10///
11/// See [[architecture-storage-format#Root Metadata]].
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub struct SegmentEntry {
14    /// Unique identifier for this segment.
15    pub segment_id: SegmentId,
16    /// Contiguous block range where the segment data is stored.
17    pub extent: Extent,
18    /// Monotonically increasing version counter. Incremented on each commit
19    /// that modifies the segment directory.
20    pub generation: u64,
21    /// Actual byte length of the segment data. May be less than
22    /// `extent.byte_len()` since the last block can be partially filled.
23    pub data_len: u64,
24}
25
26impl SegmentEntry {
27    pub const fn new(
28        segment_id: SegmentId,
29        extent: Extent,
30        generation: u64,
31        data_len: u64,
32    ) -> Self {
33        Self {
34            segment_id,
35            extent,
36            generation,
37            data_len,
38        }
39    }
40}
41
42/// An entry in the vector-index directory mapping a `dense_vector` field
43/// to the file extent holding its serialized search index (HNSW graph
44/// today; other algorithms in the future).
45///
46/// Vector indexes are index-wide (one per field, not per segment) per
47/// [[global-vector-indices]] Alternative B. They live in their
48/// own block extents, managed by the same `BlockAllocator` as segments,
49/// but tracked separately so segment iteration stays clean.
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub struct VectorIndexEntry {
52    /// Schema field id this index belongs to.
53    pub field_id: FieldId,
54    /// Contiguous block range where the serialized index lives.
55    pub extent: Extent,
56    /// Actual byte length of the serialized index.
57    pub data_len: u64,
58}
59
60impl VectorIndexEntry {
61    pub const fn new(field_id: FieldId, extent: Extent, data_len: u64) -> Self {
62        Self {
63            field_id,
64            extent,
65            data_len,
66        }
67    }
68}
69
70/// Point-in-time snapshot of all index metadata, stored in the metadata root
71/// block referenced by a root pointer.
72///
73/// Contains the segment directory, vector-index directory, allocator state,
74/// and free list — everything needed to reconstruct the index state after a
75/// crash.
76///
77/// See [[architecture-storage-format#Root Metadata]].
78#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct MetadataSnapshot {
80    /// Live segments and their block locations.
81    pub segments: Vec<SegmentEntry>,
82    /// Live per-field vector indexes (HNSW today; algorithm-agnostic).
83    /// Index-wide artifact, decoupled from segments per
84    /// [[global-vector-indices]].
85    pub vector_indexes: Vec<VectorIndexEntry>,
86    /// Total number of data blocks the file spans.
87    pub total_blocks: u64,
88    /// Free block extents available for reuse.
89    pub free_list: Vec<Extent>,
90    /// Opaque application metadata (e.g., serialized field mappings,
91    /// deletion bitmaps). The storage layer does not interpret this —
92    /// it just persists and returns the bytes.
93    pub user_metadata: Vec<u8>,
94}
95
96// --- Binary format ---
97//
98// Header (20 bytes):
99//   total_blocks:        u64
100//   segment_count:       u32
101//   free_extent_count:   u32
102//   vector_index_count:  u32
103//
104// Segment entries (36 bytes each):
105//   segment_id:        u64
106//   extent_start:      u64
107//   extent_count:      u32
108//   generation:        u64
109//   data_len:          u64
110//
111// Free list entries (12 bytes each):
112//   start_block:       u64
113//   count:             u32
114//
115// Vector index entries (22 bytes each):
116//   field_id:          u16
117//   extent_start:      u64
118//   extent_count:      u32
119//   data_len:          u64
120
121const HEADER_BYTES: usize = 20;
122const SEGMENT_ENTRY_BYTES: usize = 36;
123const FREE_EXTENT_BYTES: usize = 12;
124const VECTOR_INDEX_ENTRY_BYTES: usize = 22;
125
126impl MetadataSnapshot {
127    /// Create an empty snapshot for a fresh index.
128    pub fn empty() -> Self {
129        Self {
130            segments: Vec::new(),
131            vector_indexes: Vec::new(),
132            total_blocks: 0,
133            free_list: Vec::new(),
134            user_metadata: Vec::new(),
135        }
136    }
137
138    /// Serialized size in bytes.
139    pub fn serialized_size(&self) -> usize {
140        HEADER_BYTES
141            + self.segments.len() * SEGMENT_ENTRY_BYTES
142            + self.free_list.len() * FREE_EXTENT_BYTES
143            + self.vector_indexes.len() * VECTOR_INDEX_ENTRY_BYTES
144            + 4 // user_metadata length prefix
145            + self.user_metadata.len()
146    }
147
148    /// Serialize to bytes for storage in a metadata block.
149    ///
150    /// The output is checksummed by the caller (via [`xxh3_checksum`]) and
151    /// stored in a block referenced by a root pointer.
152    ///
153    /// [`xxh3_checksum`]: crate::storage::xxh3_checksum
154    pub fn to_bytes(&self) -> Vec<u8> {
155        let size = self.serialized_size();
156        let mut buf = Vec::with_capacity(size);
157
158        // Header
159        buf.extend_from_slice(&self.total_blocks.to_le_bytes());
160        buf.extend_from_slice(&(self.segments.len() as u32).to_le_bytes());
161        buf.extend_from_slice(&(self.free_list.len() as u32).to_le_bytes());
162        buf.extend_from_slice(&(self.vector_indexes.len() as u32).to_le_bytes());
163
164        // Segment entries
165        for entry in &self.segments {
166            buf.extend_from_slice(&entry.segment_id.as_u64().to_le_bytes());
167            buf.extend_from_slice(&entry.extent.start.as_u64().to_le_bytes());
168            buf.extend_from_slice(&entry.extent.count.to_le_bytes());
169            buf.extend_from_slice(&entry.generation.to_le_bytes());
170            buf.extend_from_slice(&entry.data_len.to_le_bytes());
171        }
172
173        // Free list
174        for extent in &self.free_list {
175            buf.extend_from_slice(&extent.start.as_u64().to_le_bytes());
176            buf.extend_from_slice(&extent.count.to_le_bytes());
177        }
178
179        // Vector index entries
180        for entry in &self.vector_indexes {
181            buf.extend_from_slice(&entry.field_id.as_u16().to_le_bytes());
182            buf.extend_from_slice(&entry.extent.start.as_u64().to_le_bytes());
183            buf.extend_from_slice(&entry.extent.count.to_le_bytes());
184            buf.extend_from_slice(&entry.data_len.to_le_bytes());
185        }
186
187        // User metadata (opaque blob)
188        buf.extend_from_slice(&(self.user_metadata.len() as u32).to_le_bytes());
189        buf.extend_from_slice(&self.user_metadata);
190
191        debug_assert_eq!(buf.len(), size);
192        buf
193    }
194
195    /// Deserialize from bytes read from a metadata block.
196    ///
197    /// # Errors
198    ///
199    /// Returns `LuciError::IndexCorrupted` if the data is truncated or
200    /// contains invalid values.
201    pub fn from_bytes(data: &[u8]) -> Result<Self> {
202        if data.len() < HEADER_BYTES {
203            return Err(LuciError::IndexCorrupted(
204                "metadata block too small for header".into(),
205            ));
206        }
207
208        let total_blocks = u64::from_le_bytes(data[0..8].try_into().unwrap());
209        let segment_count = u32::from_le_bytes(data[8..12].try_into().unwrap()) as usize;
210        let free_extent_count = u32::from_le_bytes(data[12..16].try_into().unwrap()) as usize;
211        let vector_index_count = u32::from_le_bytes(data[16..20].try_into().unwrap()) as usize;
212
213        let expected_size = HEADER_BYTES
214            + segment_count * SEGMENT_ENTRY_BYTES
215            + free_extent_count * FREE_EXTENT_BYTES
216            + vector_index_count * VECTOR_INDEX_ENTRY_BYTES;
217        if data.len() < expected_size {
218            return Err(LuciError::IndexCorrupted(format!(
219                "metadata block truncated: need {expected_size} bytes, got {}",
220                data.len()
221            )));
222        }
223
224        let mut offset = HEADER_BYTES;
225
226        let mut segments = Vec::with_capacity(segment_count);
227        for _ in 0..segment_count {
228            let segment_id = SegmentId::new(u64::from_le_bytes(
229                data[offset..offset + 8].try_into().unwrap(),
230            ));
231            offset += 8;
232            let extent_start = BlockId::new(u64::from_le_bytes(
233                data[offset..offset + 8].try_into().unwrap(),
234            ));
235            offset += 8;
236            let extent_count = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
237            offset += 4;
238            let generation = u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
239            offset += 8;
240            let data_len = u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
241            offset += 8;
242
243            segments.push(SegmentEntry::new(
244                segment_id,
245                Extent::new(extent_start, extent_count),
246                generation,
247                data_len,
248            ));
249        }
250
251        let mut free_list = Vec::with_capacity(free_extent_count);
252        for _ in 0..free_extent_count {
253            let start = BlockId::new(u64::from_le_bytes(
254                data[offset..offset + 8].try_into().unwrap(),
255            ));
256            offset += 8;
257            let count = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
258            offset += 4;
259
260            free_list.push(Extent::new(start, count));
261        }
262
263        let mut vector_indexes = Vec::with_capacity(vector_index_count);
264        for _ in 0..vector_index_count {
265            let field_id = FieldId::new(u16::from_le_bytes(
266                data[offset..offset + 2].try_into().unwrap(),
267            ));
268            offset += 2;
269            let extent_start = BlockId::new(u64::from_le_bytes(
270                data[offset..offset + 8].try_into().unwrap(),
271            ));
272            offset += 8;
273            let extent_count = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
274            offset += 4;
275            let data_len = u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
276            offset += 8;
277
278            vector_indexes.push(VectorIndexEntry::new(
279                field_id,
280                Extent::new(extent_start, extent_count),
281                data_len,
282            ));
283        }
284
285        let user_metadata = if offset + 4 <= data.len() {
286            let meta_len =
287                u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
288            offset += 4;
289            if offset + meta_len <= data.len() {
290                data[offset..offset + meta_len].to_vec()
291            } else {
292                Vec::new()
293            }
294        } else {
295            Vec::new()
296        };
297
298        Ok(Self {
299            segments,
300            vector_indexes,
301            total_blocks,
302            free_list,
303            user_metadata,
304        })
305    }
306
307    /// Check that the serialized metadata fits within a single block.
308    ///
309    /// Returns `false` if overflow chaining would be needed (not yet
310    /// implemented).
311    pub fn fits_in_single_block(&self) -> bool {
312        self.serialized_size() <= BLOCK_SIZE as usize
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    fn sample_snapshot() -> MetadataSnapshot {
321        MetadataSnapshot {
322            segments: vec![
323                SegmentEntry::new(SegmentId::new(1), Extent::new(BlockId(0), 4), 1, 900_000),
324                SegmentEntry::new(SegmentId::new(2), Extent::new(BlockId(4), 2), 1, 400_000),
325                SegmentEntry::new(SegmentId::new(3), Extent::new(BlockId(8), 6), 2, 1_500_000),
326            ],
327            vector_indexes: Vec::new(),
328            total_blocks: 14,
329            free_list: vec![Extent::new(BlockId(6), 2)],
330            user_metadata: Vec::new(),
331        }
332    }
333
334    #[test]
335    fn empty_snapshot() {
336        let snap = MetadataSnapshot::empty();
337        assert!(snap.segments.is_empty());
338        assert_eq!(snap.total_blocks, 0);
339        assert!(snap.free_list.is_empty());
340    }
341
342    #[test]
343    fn round_trip_empty() {
344        let snap = MetadataSnapshot::empty();
345        let bytes = snap.to_bytes();
346        let snap2 = MetadataSnapshot::from_bytes(&bytes).unwrap();
347        assert_eq!(snap, snap2);
348    }
349
350    #[test]
351    fn round_trip_populated() {
352        let snap = sample_snapshot();
353        let bytes = snap.to_bytes();
354        let snap2 = MetadataSnapshot::from_bytes(&bytes).unwrap();
355        assert_eq!(snap, snap2);
356    }
357
358    #[test]
359    fn serialized_size_matches_output() {
360        let snap = sample_snapshot();
361        let bytes = snap.to_bytes();
362        assert_eq!(bytes.len(), snap.serialized_size());
363        // 20 header + 3*36 segments + 1*12 free + 0 vector_indexes
364        //   + 4 user_metadata_len = 20 + 108 + 12 + 0 + 4 = 144
365        assert_eq!(bytes.len(), 144);
366    }
367
368    #[test]
369    fn round_trip_with_vector_indexes() {
370        let mut snap = sample_snapshot();
371        snap.vector_indexes = vec![
372            VectorIndexEntry::new(FieldId::new(7), Extent::new(BlockId(100), 3), 600_000),
373            VectorIndexEntry::new(FieldId::new(11), Extent::new(BlockId(103), 5), 1_200_000),
374        ];
375        let bytes = snap.to_bytes();
376        let snap2 = MetadataSnapshot::from_bytes(&bytes).unwrap();
377        assert_eq!(snap, snap2);
378    }
379
380    #[test]
381    fn truncated_header_is_rejected() {
382        let err = MetadataSnapshot::from_bytes(&[0u8; 10]).unwrap_err();
383        assert!(format!("{err}").contains("too small"));
384    }
385
386    #[test]
387    fn truncated_body_is_rejected() {
388        let snap = sample_snapshot();
389        let bytes = snap.to_bytes();
390        let err = MetadataSnapshot::from_bytes(&bytes[..bytes.len() - 5]).unwrap_err();
391        assert!(format!("{err}").contains("truncated"));
392    }
393
394    #[test]
395    fn fits_in_single_block() {
396        let snap = sample_snapshot();
397        assert!(snap.fits_in_single_block());
398    }
399
400    #[test]
401    fn from_bytes_tolerates_trailing_data() {
402        let snap = sample_snapshot();
403        let mut bytes = snap.to_bytes();
404        bytes.extend_from_slice(&[0u8; 1024]);
405        let snap2 = MetadataSnapshot::from_bytes(&bytes).unwrap();
406        assert_eq!(snap, snap2);
407    }
408
409    #[test]
410    fn segment_entry_fields() {
411        let entry = SegmentEntry::new(SegmentId::new(42), Extent::new(BlockId(10), 3), 7, 768_000);
412        assert_eq!(entry.segment_id, SegmentId::new(42));
413        assert_eq!(entry.extent, Extent::new(BlockId(10), 3));
414        assert_eq!(entry.generation, 7);
415        assert_eq!(entry.data_len, 768_000);
416    }
417
418    #[test]
419    fn round_trip_large_snapshot() {
420        let segments: Vec<_> = (0..500)
421            .map(|i| {
422                SegmentEntry::new(
423                    SegmentId::new(i),
424                    Extent::new(BlockId(i * 10), 5),
425                    i / 10,
426                    256_000,
427                )
428            })
429            .collect();
430        let free_list: Vec<_> = (0..200)
431            .map(|i| Extent::new(BlockId(5000 + i * 3), 2))
432            .collect();
433        let snap = MetadataSnapshot {
434            segments,
435            vector_indexes: Vec::new(),
436            total_blocks: 10000,
437            free_list,
438            user_metadata: Vec::new(),
439        };
440
441        assert!(snap.fits_in_single_block());
442        let bytes = snap.to_bytes();
443        let snap2 = MetadataSnapshot::from_bytes(&bytes).unwrap();
444        assert_eq!(snap, snap2);
445    }
446
447    #[test]
448    fn checksum_integration() {
449        let snap = sample_snapshot();
450        let bytes = snap.to_bytes();
451        let checksum = crate::storage::xxh3_checksum(&bytes);
452        assert_eq!(checksum, crate::storage::xxh3_checksum(&bytes));
453        let snap2 = MetadataSnapshot::empty();
454        let bytes2 = snap2.to_bytes();
455        assert_ne!(checksum, crate::storage::xxh3_checksum(&bytes2));
456    }
457}