nodedb_vector/mmap_segment/
writer.rs1use std::path::Path;
6
7use super::format::{
8 DTYPE_F32, FOOTER_SIZE, FORMAT_VERSION, HEADER_SIZE, MAGIC, VectorSegmentCodec, vec_pad,
9};
10
11pub fn write_segment(
28 dir: &Path,
29 name: &str,
30 dim: usize,
31 vectors: &[&[f32]],
32 surrogate_ids: &[u64],
33) -> std::io::Result<()> {
34 debug_assert!(
35 surrogate_ids.is_empty() || surrogate_ids.len() == vectors.len(),
36 "surrogate_ids length must match vectors length or be empty"
37 );
38
39 std::fs::create_dir_all(dir)?;
40
41 let count = vectors.len() as u64;
42
43 let vec_bytes = dim
44 .checked_mul(vectors.len())
45 .and_then(|n| n.checked_mul(4))
46 .ok_or_else(|| {
47 std::io::Error::new(std::io::ErrorKind::InvalidData, "vector data size overflow")
48 })?;
49 let surrogate_bytes = vectors.len().checked_mul(8).ok_or_else(|| {
50 std::io::Error::new(
51 std::io::ErrorKind::InvalidData,
52 "surrogate block size overflow",
53 )
54 })?;
55 let pad_bytes = vec_pad(vec_bytes);
56 let body_len = HEADER_SIZE + vec_bytes + pad_bytes + surrogate_bytes;
57
58 let mut buf = Vec::with_capacity(body_len + FOOTER_SIZE);
59
60 buf.extend_from_slice(&MAGIC);
62 buf.extend_from_slice(&FORMAT_VERSION.to_le_bytes());
63 buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&(dim as u32).to_le_bytes());
65 buf.extend_from_slice(&count.to_le_bytes());
66 buf.push(DTYPE_F32);
67 buf.push(VectorSegmentCodec::None as u8);
68 buf.extend_from_slice(&[0u8; 10]); for v in vectors {
72 debug_assert_eq!(v.len(), dim, "vector dimension mismatch during write");
73 let bytes: &[u8] =
76 unsafe { std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 4) };
77 buf.extend_from_slice(bytes);
78 }
79
80 if pad_bytes > 0 {
82 buf.extend_from_slice(&[0u8; 8][..pad_bytes]);
83 }
84
85 for i in 0..vectors.len() {
87 let sid: u64 = surrogate_ids.get(i).copied().unwrap_or(0);
88 buf.extend_from_slice(&sid.to_le_bytes());
89 }
90
91 if buf.len() != body_len {
92 return Err(std::io::Error::other(format!(
93 "unexpected segment body size: {} vs {body_len}",
94 buf.len()
95 )));
96 }
97
98 let checksum = crc32c::crc32c(&buf);
100
101 let mut created_by = [0u8; 32];
102 let ver = env!("CARGO_PKG_VERSION").as_bytes();
103 let copy_len = ver.len().min(31);
104 created_by[..copy_len].copy_from_slice(&ver[..copy_len]);
105
106 buf.extend_from_slice(&FORMAT_VERSION.to_le_bytes()); buf.extend_from_slice(&created_by); buf.extend_from_slice(&checksum.to_le_bytes()); buf.extend_from_slice(&(FOOTER_SIZE as u32).to_le_bytes()); buf.extend_from_slice(&MAGIC); nodedb_wal::segment::atomic_write_fsync(dir, name, &buf).map_err(|e| {
114 std::io::Error::other(format!("publish segment {}: {e}", dir.join(name).display()))
115 })
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 fn read_footer_checksum(bytes: &[u8]) -> u32 {
123 let start = bytes.len() - FOOTER_SIZE;
124 u32::from_le_bytes([
125 bytes[start + 34],
126 bytes[start + 35],
127 bytes[start + 36],
128 bytes[start + 37],
129 ])
130 }
131
132 #[test]
135 fn publishes_a_complete_footer_terminated_segment() {
136 let dir = tempfile::tempdir().expect("tempdir");
137 let path = dir.path().join("seg.ndvs");
138 let v = [1.0f32, 2.0, 3.0];
139
140 write_segment(dir.path(), "seg.ndvs", 3, &[&v], &[7]).expect("write");
141
142 let bytes = std::fs::read(&path).expect("read");
143 assert!(bytes.len() > FOOTER_SIZE);
144 assert_eq!(&bytes[bytes.len() - 4..], &MAGIC, "trailing magic");
145 assert_eq!(
146 read_footer_checksum(&bytes),
147 crc32c::crc32c(&bytes[..bytes.len() - FOOTER_SIZE]),
148 "footer checksum must cover exactly the body"
149 );
150
151 let leftovers: Vec<String> = std::fs::read_dir(dir.path())
152 .expect("read_dir")
153 .flatten()
154 .filter_map(|e| e.file_name().to_str().map(str::to_string))
155 .collect();
156 assert_eq!(
157 leftovers,
158 vec!["seg.ndvs".to_string()],
159 "the staging file must be renamed away, not left behind"
160 );
161 }
162
163 #[test]
166 fn rewrite_replaces_the_previous_segment_wholesale() {
167 let dir = tempfile::tempdir().expect("tempdir");
168 let path = dir.path().join("seg.ndvs");
169
170 let a = [1.0f32, 2.0];
171 write_segment(dir.path(), "seg.ndvs", 2, &[&a, &a, &a], &[1, 2, 3]).expect("first write");
172 let first_len = std::fs::metadata(&path).expect("stat").len();
173
174 let b = [9.0f32, 8.0];
175 write_segment(dir.path(), "seg.ndvs", 2, &[&b], &[4]).expect("second write");
176 let bytes = std::fs::read(&path).expect("read");
177
178 assert_ne!(bytes.len() as u64, first_len, "smaller segment replaced it");
179 assert_eq!(&bytes[bytes.len() - 4..], &MAGIC);
180 assert_eq!(
181 read_footer_checksum(&bytes),
182 crc32c::crc32c(&bytes[..bytes.len() - FOOTER_SIZE])
183 );
184 assert_eq!(
185 std::fs::read_dir(dir.path()).expect("read_dir").count(),
186 1,
187 "no staging file survives a rewrite"
188 );
189 }
190
191 #[test]
193 fn empty_segment_is_valid() {
194 let dir = tempfile::tempdir().expect("tempdir");
195 let path = dir.path().join("empty.ndvs");
196
197 write_segment(dir.path(), "empty.ndvs", 4, &[], &[]).expect("write");
198
199 let bytes = std::fs::read(&path).expect("read");
200 assert_eq!(bytes.len(), HEADER_SIZE + FOOTER_SIZE);
201 assert_eq!(
202 read_footer_checksum(&bytes),
203 crc32c::crc32c(&bytes[..HEADER_SIZE])
204 );
205 }
206}