1use gonzalo_core::{ContentHash, CoreError, RecordKey, Result};
8use std::num::NonZeroU16;
9
10pub const DEFAULT_SHARDS: NonZeroU16 = match NonZeroU16::new(256) {
13 Some(n) => n,
14 None => panic!("256 is nonzero"),
15};
16
17const MAGIC: &[u8; 4] = b"GZVS";
18const VERSION: u8 = 1;
19
20pub fn shard_of(key: &RecordKey, shards: NonZeroU16) -> u16 {
26 let s = format!("{}/{}/{}", key.namespace, key.collection, key.id);
27 let hex = ContentHash::of(s.as_bytes()).0;
28 let bits = u16::from_str_radix(&hex[..4], 16).expect("blake3 hex is 64 hex digits");
29 bits % shards.get()
30}
31
32pub(crate) fn encode_shard(dim: usize, entries: &[(RecordKey, Vec<f32>)]) -> Vec<u8> {
39 let mut sorted: Vec<&(RecordKey, Vec<f32>)> = entries.iter().collect();
40 sorted.sort_by(|a, b| a.0.cmp(&b.0));
41
42 let mut out = Vec::new();
43 out.extend_from_slice(MAGIC);
44 out.push(VERSION);
45 out.extend_from_slice(&(dim as u32).to_le_bytes());
46 out.extend_from_slice(&(sorted.len() as u32).to_le_bytes());
47 for (key, vector) in sorted {
48 put_str16(&mut out, &key.namespace);
49 put_str16(&mut out, &key.collection);
50 out.extend_from_slice(&(key.id.len() as u32).to_le_bytes());
51 out.extend_from_slice(key.id.as_bytes());
52 for f in vector {
53 out.extend_from_slice(&f.to_le_bytes());
54 }
55 }
56 out
57}
58
59#[allow(clippy::type_complexity)]
68pub(crate) fn decode_shard(bytes: &[u8]) -> Result<(usize, Vec<(RecordKey, Vec<f32>)>)> {
69 let mut r = Reader { bytes, at: 0 };
70 if r.take(4)? != MAGIC {
71 return Err(CoreError::Backend("vector shard: bad magic".into()));
72 }
73 let version = r.take(1)?[0];
74 if version != VERSION {
75 return Err(CoreError::Backend(format!(
76 "vector shard: unsupported version {version}, expected {VERSION}"
77 )));
78 }
79 let dim = r.u32()? as usize;
80 let count = r.u32()? as usize;
81
82 let remaining = bytes.len().saturating_sub(r.at);
86 let min_entry_size = 8_usize.saturating_add(dim.saturating_mul(4));
87 let max_count = remaining.checked_div(min_entry_size).unwrap_or(usize::MAX);
88 if count > max_count {
89 return Err(CoreError::Backend(
90 "vector shard: entry count exceeds remaining data".into(),
91 ));
92 }
93
94 let mut entries = Vec::new();
95 for _ in 0..count {
96 let namespace = r.str16()?;
97 let collection = r.str16()?;
98 let id_len = r.u32()? as usize;
99 let id = String::from_utf8(r.take(id_len)?.to_vec())
100 .map_err(|e| CoreError::Backend(format!("vector shard: bad utf8 in id: {e}")))?;
101 let mut vector = Vec::new();
102 for _ in 0..dim {
103 let b: [u8; 4] = r.take(4)?.try_into().expect("took exactly 4 bytes");
104 vector.push(f32::from_le_bytes(b));
105 }
106 entries.push((RecordKey::new(namespace, collection, id), vector));
107 }
108 Ok((dim, entries))
109}
110
111fn put_str16(out: &mut Vec<u8>, s: &str) {
112 out.extend_from_slice(&(s.len() as u16).to_le_bytes());
113 out.extend_from_slice(s.as_bytes());
114}
115
116struct Reader<'a> {
117 bytes: &'a [u8],
118 at: usize,
119}
120
121impl<'a> Reader<'a> {
122 fn take(&mut self, n: usize) -> Result<&'a [u8]> {
123 let end = self.at.checked_add(n).ok_or_else(overflow)?;
124 let slice = self
125 .bytes
126 .get(self.at..end)
127 .ok_or_else(|| CoreError::Backend("vector shard: truncated".into()))?;
128 self.at = end;
129 Ok(slice)
130 }
131
132 fn u32(&mut self) -> Result<u32> {
133 let b: [u8; 4] = self.take(4)?.try_into().expect("took exactly 4 bytes");
134 Ok(u32::from_le_bytes(b))
135 }
136
137 fn str16(&mut self) -> Result<String> {
138 let b: [u8; 2] = self.take(2)?.try_into().expect("took exactly 2 bytes");
139 let len = u16::from_le_bytes(b) as usize;
140 String::from_utf8(self.take(len)?.to_vec())
141 .map_err(|e| CoreError::Backend(format!("vector shard: bad utf8: {e}")))
142 }
143}
144
145fn overflow() -> CoreError {
146 CoreError::Backend("vector shard: length overflow".into())
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 fn key(id: &str) -> RecordKey {
154 RecordKey::new("ns", "coll", id)
155 }
156
157 #[test]
158 fn shard_assignment_is_stable_across_calls() {
159 let k = key("a");
160 let shards = NonZeroU16::new(256).unwrap();
161 assert_eq!(shard_of(&k, shards), shard_of(&k, shards));
162 }
163
164 #[test]
168 fn shard_assignment_is_pinned_to_a_known_value() {
169 assert_eq!(shard_of(&key("a"), NonZeroU16::new(256).unwrap()), 121);
170 }
171
172 #[test]
173 fn shard_assignment_respects_the_shard_count() {
174 let shards = NonZeroU16::new(4).unwrap();
175 for i in 0..100 {
176 assert!(shard_of(&key(&i.to_string()), shards) < 4);
177 }
178 }
179
180 #[test]
181 fn round_trips_entries_and_dim() {
182 let entries = vec![
183 (key("a"), vec![1.0, 2.0, 3.0]),
184 (key("b"), vec![-1.5, 0.0, 2.5]),
185 ];
186 let bytes = encode_shard(3, &entries);
187 let (dim, decoded) = decode_shard(&bytes).unwrap();
188 assert_eq!(dim, 3);
189 assert_eq!(decoded, entries);
190 }
191
192 #[test]
193 fn round_trips_an_empty_shard() {
194 let bytes = encode_shard(4, &[]);
195 let (dim, decoded) = decode_shard(&bytes).unwrap();
196 assert_eq!(dim, 4);
197 assert!(decoded.is_empty());
198 }
199
200 #[test]
203 fn encoding_is_deterministic_regardless_of_input_order() {
204 let a = vec![(key("b"), vec![1.0]), (key("a"), vec![2.0])];
205 let b = vec![(key("a"), vec![2.0]), (key("b"), vec![1.0])];
206 assert_eq!(encode_shard(1, &a), encode_shard(1, &b));
207 }
208
209 #[test]
210 fn decode_rejects_a_bad_magic() {
211 let mut bytes = encode_shard(1, &[(key("a"), vec![1.0])]);
212 bytes[0] = b'X';
213 assert!(matches!(decode_shard(&bytes), Err(CoreError::Backend(_))));
214 }
215
216 #[test]
217 fn decode_rejects_an_unknown_version() {
218 let mut bytes = encode_shard(1, &[(key("a"), vec![1.0])]);
219 bytes[4] = 99;
220 assert!(matches!(decode_shard(&bytes), Err(CoreError::Backend(_))));
221 }
222
223 #[test]
224 fn decode_rejects_a_truncated_shard() {
225 let bytes = encode_shard(2, &[(key("a"), vec![1.0, 2.0])]);
226 let truncated = &bytes[..bytes.len() - 3];
227 assert!(matches!(
228 decode_shard(truncated),
229 Err(CoreError::Backend(_))
230 ));
231 }
232
233 #[test]
234 fn decode_rejects_a_huge_entry_count() {
235 let mut bytes = vec![];
238 bytes.extend_from_slice(b"GZVS");
239 bytes.push(1); bytes.extend_from_slice(&(1u32).to_le_bytes()); bytes.extend_from_slice(&(u32::MAX).to_le_bytes()); assert!(matches!(decode_shard(&bytes), Err(CoreError::Backend(_))));
243 }
244
245 #[test]
246 fn decode_rejects_a_huge_dimension() {
247 let mut bytes = vec![];
250 bytes.extend_from_slice(b"GZVS");
251 bytes.push(1); bytes.extend_from_slice(&(u32::MAX).to_le_bytes()); bytes.extend_from_slice(&(1u32).to_le_bytes()); assert!(matches!(decode_shard(&bytes), Err(CoreError::Backend(_))));
255 }
256}