Skip to main content

code_moniker_workspace/snapshot/
records.rs

1//! Slot-aligned record storage for the code index.
2//!
3//! Records live in one immutable shard per catalog file slot, shared via
4//! `Arc`. Cloning the table bumps refcounts; refreshing a file replaces one
5//! shard. The API mirrors slices (`len`, `get`, `iter`, `Index`) so
6//! consumers keep positional record indexes; positions are resolved through
7//! the shard offset table.
8
9use std::ops::Index;
10use std::sync::Arc;
11
12#[derive(Clone, Debug)]
13pub struct RecordTable<T> {
14	shards: Vec<Arc<[T]>>,
15	offsets: Vec<usize>,
16}
17
18impl<T> RecordTable<T> {
19	pub fn from_shards(shards: Vec<Arc<[T]>>) -> Self {
20		let mut table = Self {
21			shards,
22			offsets: Vec::new(),
23		};
24		table.rebuild_offsets();
25		table
26	}
27
28	pub fn from_records(records: Vec<T>) -> Self {
29		Self::from_shards(vec![Arc::from(records)])
30	}
31
32	pub fn len(&self) -> usize {
33		self.offsets.last().copied().unwrap_or(0)
34	}
35
36	pub fn is_empty(&self) -> bool {
37		self.len() == 0
38	}
39
40	pub fn get(&self, index: usize) -> Option<&T> {
41		if index >= self.len() {
42			return None;
43		}
44		let slot = self.offsets.partition_point(|offset| *offset <= index) - 1;
45		self.shards[slot].get(index - self.offsets[slot])
46	}
47
48	pub fn iter(&self) -> impl Iterator<Item = &T> + '_ {
49		self.shards.iter().flat_map(|shard| shard.iter())
50	}
51
52	pub fn file_records(&self, slot: usize) -> &[T] {
53		self.shards.get(slot).map(Arc::as_ref).unwrap_or(&[])
54	}
55
56	pub(crate) fn estimated_heap_bytes(&self) -> usize {
57		self.shards.capacity() * std::mem::size_of::<Arc<[T]>>()
58			+ self.offsets.capacity() * std::mem::size_of::<usize>()
59			+ self.len() * std::mem::size_of::<T>()
60	}
61
62	pub(crate) fn replace(&mut self, slot: usize, records: Arc<[T]>) {
63		if let Some(shard) = self.shards.get_mut(slot) {
64			*shard = records;
65		} else if slot == self.shards.len() {
66			self.shards.push(records);
67		}
68		self.rebuild_offsets();
69	}
70
71	fn rebuild_offsets(&mut self) {
72		self.offsets.clear();
73		self.offsets.reserve(self.shards.len() + 1);
74		let mut total = 0usize;
75		self.offsets.push(0);
76		for shard in &self.shards {
77			total += shard.len();
78			self.offsets.push(total);
79		}
80	}
81}
82
83impl<T> Index<usize> for RecordTable<T> {
84	type Output = T;
85
86	fn index(&self, index: usize) -> &T {
87		self.get(index)
88			.unwrap_or_else(|| panic!("record index {index} out of bounds"))
89	}
90}
91
92impl<T: PartialEq> PartialEq for RecordTable<T> {
93	fn eq(&self, other: &Self) -> bool {
94		self.len() == other.len() && self.iter().eq(other.iter())
95	}
96}
97
98impl<T: Eq> Eq for RecordTable<T> {}
99
100#[cfg(test)]
101mod tests {
102	use super::*;
103
104	fn table() -> RecordTable<u32> {
105		RecordTable::from_shards(vec![
106			Arc::from(vec![1u32, 2]),
107			Arc::from(Vec::<u32>::new()),
108			Arc::from(vec![3u32, 4, 5]),
109		])
110	}
111
112	#[test]
113	fn positions_span_shards_with_empty_slots() {
114		let table = table();
115		assert_eq!(table.len(), 5);
116		assert_eq!(table.get(0), Some(&1));
117		assert_eq!(table.get(1), Some(&2));
118		assert_eq!(table.get(2), Some(&3));
119		assert_eq!(table.get(4), Some(&5));
120		assert_eq!(table.get(5), None);
121		assert_eq!(
122			table.iter().copied().collect::<Vec<_>>(),
123			vec![1, 2, 3, 4, 5]
124		);
125		assert_eq!(table.file_records(1), &[] as &[u32]);
126		assert_eq!(table.file_records(9), &[] as &[u32]);
127	}
128
129	#[test]
130	fn replace_swaps_one_shard_and_reindexes() {
131		let mut table = table();
132		table.replace(0, Arc::from(vec![9u32]));
133		assert_eq!(table.iter().copied().collect::<Vec<_>>(), vec![9, 3, 4, 5]);
134		assert_eq!(table[1], 3);
135	}
136
137	#[test]
138	fn equality_ignores_shard_boundaries() {
139		let flat = RecordTable::from_records(vec![1u32, 2, 3, 4, 5]);
140		assert_eq!(table(), flat);
141	}
142}