1#![doc(
8 html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
9 html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
10 issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
11)]
12#![cfg_attr(not(test), warn(unused_crate_dependencies))]
13#![cfg_attr(docsrs, feature(doc_cfg))]
14
15use memmap2::Mmap;
16use serde::{Deserialize, Serialize};
17use smallvec::SmallVec;
18use std::{
19 error::Error as StdError,
20 fs::File,
21 io::{self, Read, Write},
22 ops::Range,
23 path::{Path, PathBuf},
24};
25use tracing::*;
26
27pub mod compression;
29#[cfg(test)]
30use compression::Compression;
31use compression::Compressors;
32
33#[derive(Debug, Serialize, Deserialize)]
35#[cfg_attr(test, derive(PartialEq, Eq))]
36pub enum Functions {}
37
38#[derive(Debug, Serialize, Deserialize)]
40#[cfg_attr(test, derive(PartialEq, Eq))]
41pub enum InclusionFilters {}
42
43mod error;
44pub use error::NippyJarError;
45
46mod cursor;
47pub use cursor::NippyJarCursor;
48
49mod writer;
50pub use writer::NippyJarWriter;
51
52mod consistency;
53pub use consistency::NippyJarChecker;
54
55const NIPPY_JAR_VERSION: usize = 1;
57const INDEX_FILE_EXTENSION: &str = "idx";
59const OFFSETS_FILE_EXTENSION: &str = "off";
61pub const CONFIG_FILE_EXTENSION: &str = "conf";
63pub const CHANGESET_OFFSETS_FILE_EXTENSION: &str = "csoff";
65
66pub type RefRow<'a> = SmallVec<[&'a [u8]; 4]>;
72
73pub type ColumnResult<T> = Result<T, Box<dyn StdError + Send + Sync>>;
75
76pub trait NippyJarHeader:
78 Send + Sync + Serialize + for<'b> Deserialize<'b> + std::fmt::Debug + 'static
79{
80}
81
82impl<T> NippyJarHeader for T where
84 T: Send + Sync + Serialize + for<'b> Deserialize<'b> + std::fmt::Debug + 'static
85{
86}
87
88#[derive(Serialize, Deserialize)]
93#[cfg_attr(test, derive(PartialEq))]
94pub struct NippyJar<H = ()> {
95 version: usize,
97 user_header: H,
100 columns: usize,
102 rows: usize,
104 compressor: Option<Compressors>,
106 #[serde(skip)]
107 filter: Option<InclusionFilters>,
109 #[serde(skip)]
110 phf: Option<Functions>,
112 max_row_size: usize,
115 #[serde(skip)]
117 path: PathBuf,
118}
119
120impl<H: NippyJarHeader> std::fmt::Debug for NippyJar<H> {
121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 f.debug_struct("NippyJar")
123 .field("version", &self.version)
124 .field("user_header", &self.user_header)
125 .field("rows", &self.rows)
126 .field("columns", &self.columns)
127 .field("compressor", &self.compressor)
128 .field("filter", &self.filter)
129 .field("phf", &self.phf)
130 .field("path", &self.path)
131 .field("max_row_size", &self.max_row_size)
132 .finish_non_exhaustive()
133 }
134}
135
136impl NippyJar<()> {
137 pub fn new_without_header(columns: usize, path: &Path) -> Self {
139 Self::new(columns, path, ())
140 }
141
142 pub fn load_without_header(path: &Path) -> Result<Self, NippyJarError> {
144 Self::load(path)
145 }
146}
147
148impl<H: NippyJarHeader> NippyJar<H> {
149 pub fn new(columns: usize, path: &Path, user_header: H) -> Self {
151 Self {
152 version: NIPPY_JAR_VERSION,
153 user_header,
154 columns,
155 rows: 0,
156 max_row_size: 0,
157 compressor: None,
158 filter: None,
159 phf: None,
160 path: path.to_path_buf(),
161 }
162 }
163
164 pub fn with_zstd(mut self, use_dict: bool, max_dict_size: usize) -> Self {
166 self.compressor =
167 Some(Compressors::Zstd(compression::Zstd::new(use_dict, max_dict_size, self.columns)));
168 self
169 }
170
171 pub fn with_lz4(mut self) -> Self {
173 self.compressor = Some(Compressors::Lz4(compression::Lz4::default()));
174 self
175 }
176
177 pub const fn user_header(&self) -> &H {
179 &self.user_header
180 }
181
182 pub const fn columns(&self) -> usize {
184 self.columns
185 }
186
187 pub const fn rows(&self) -> usize {
189 self.rows
190 }
191
192 pub const fn compressor(&self) -> Option<&Compressors> {
194 self.compressor.as_ref()
195 }
196
197 pub const fn compressor_mut(&mut self) -> Option<&mut Compressors> {
199 self.compressor.as_mut()
200 }
201
202 pub fn load(path: &Path) -> Result<Self, NippyJarError> {
206 let config_path = path.with_extension(CONFIG_FILE_EXTENSION);
208 let config_file = File::open(&config_path)
209 .inspect_err(|e| {
210 warn!(?path, %e, "Failed to load static file jar");
211 })
212 .map_err(|err| reth_fs_util::FsPathError::open(err, config_path))?;
213
214 let mut obj = Self::load_from_reader(io::BufReader::new(config_file))?;
215 obj.path = path.to_path_buf();
216 Ok(obj)
217 }
218
219 pub fn load_from_reader<R: Read>(reader: R) -> Result<Self, NippyJarError> {
221 Ok(bincode::deserialize_from(reader)?)
222 }
223
224 pub fn save_to_writer<W: Write>(&self, writer: W) -> Result<(), NippyJarError> {
226 Ok(bincode::serialize_into(writer, self)?)
227 }
228
229 pub fn data_path(&self) -> &Path {
231 self.path.as_ref()
232 }
233
234 pub fn index_path(&self) -> PathBuf {
236 self.path.with_extension(INDEX_FILE_EXTENSION)
237 }
238
239 pub fn offsets_path(&self) -> PathBuf {
241 self.path.with_extension(OFFSETS_FILE_EXTENSION)
242 }
243
244 pub fn config_path(&self) -> PathBuf {
246 self.path.with_extension(CONFIG_FILE_EXTENSION)
247 }
248
249 pub fn changeset_offsets_path(&self) -> PathBuf {
251 self.path.with_extension(CHANGESET_OFFSETS_FILE_EXTENSION)
252 }
253
254 pub fn delete(self) -> Result<(), NippyJarError> {
256 for path in [
259 self.data_path().into(),
260 self.index_path(),
261 self.offsets_path(),
262 self.config_path(),
263 self.changeset_offsets_path(),
264 ] {
265 if path.exists() {
266 debug!(target: "nippy-jar", ?path, "Removing file.");
267 reth_fs_util::remove_file(path)?;
268 }
269 }
270
271 Ok(())
272 }
273
274 pub fn open_data_reader(&self) -> Result<DataReader, NippyJarError> {
276 DataReader::new(self.data_path())
277 }
278
279 fn freeze_config(&self) -> Result<(), NippyJarError> {
281 Ok(reth_fs_util::atomic_write_file(&self.config_path(), |file| self.save_to_writer(file))?)
282 }
283}
284
285#[cfg(test)]
286impl<H: NippyJarHeader> NippyJar<H> {
287 pub fn prepare_compression(
289 &mut self,
290 columns: Vec<impl IntoIterator<Item = Vec<u8>>>,
291 ) -> Result<(), NippyJarError> {
292 if let Some(compression) = &mut self.compressor {
294 debug!(target: "nippy-jar", columns=columns.len(), "Preparing compression.");
295 compression.prepare_compression(columns)?;
296 }
297 Ok(())
298 }
299
300 pub fn freeze(
302 self,
303 columns: Vec<impl IntoIterator<Item = ColumnResult<Vec<u8>>>>,
304 total_rows: u64,
305 ) -> Result<Self, NippyJarError> {
306 self.check_before_freeze(&columns)?;
307
308 debug!(target: "nippy-jar", path=?self.data_path(), "Opening data file.");
309
310 let mut writer = NippyJarWriter::new(self)?;
312
313 writer.append_rows(columns, total_rows)?;
315
316 writer.commit()?;
318
319 debug!(target: "nippy-jar", ?writer, "Finished writing data.");
320
321 Ok(writer.into_jar())
322 }
323
324 fn check_before_freeze(
326 &self,
327 columns: &[impl IntoIterator<Item = ColumnResult<Vec<u8>>>],
328 ) -> Result<(), NippyJarError> {
329 if columns.len() != self.columns {
330 return Err(NippyJarError::ColumnLenMismatch(self.columns, columns.len()))
331 }
332
333 if let Some(compression) = &self.compressor &&
334 !compression.is_ready()
335 {
336 return Err(NippyJarError::CompressorNotReady)
337 }
338
339 Ok(())
340 }
341}
342
343#[derive(Debug)]
347pub struct DataReader {
348 #[expect(dead_code)]
350 data_file: File,
351 data_mmap: Mmap,
353 offset_file: File,
355 offset_mmap: Mmap,
357 offset_size: u8,
359}
360
361impl DataReader {
362 pub fn new(path: impl AsRef<Path>) -> Result<Self, NippyJarError> {
364 let data_file = File::open(path.as_ref())?;
365 let data_mmap = unsafe { Mmap::map(&data_file)? };
367
368 let offset_file = File::open(path.as_ref().with_extension(OFFSETS_FILE_EXTENSION))?;
369 let offset_mmap = unsafe { Mmap::map(&offset_file)? };
371
372 let offset_size = offset_mmap[0];
374
375 if offset_size > 8 {
377 return Err(NippyJarError::OffsetSizeTooBig { offset_size })
378 } else if offset_size == 0 {
379 return Err(NippyJarError::OffsetSizeTooSmall { offset_size })
380 }
381
382 Ok(Self { data_file, data_mmap, offset_file, offset_size, offset_mmap })
383 }
384
385 pub fn offset(&self, index: usize) -> Result<u64, NippyJarError> {
387 let from = index * self.offset_size as usize + 1;
389
390 self.offset_at(from)
391 }
392
393 pub fn reverse_offset(&self, index: usize) -> Result<u64, NippyJarError> {
395 let offsets_file_size = self.offset_file.metadata()?.len() as usize;
396
397 if offsets_file_size > 1 {
398 let from = offsets_file_size - self.offset_size as usize * (index + 1);
399
400 self.offset_at(from)
401 } else {
402 Ok(0)
403 }
404 }
405
406 pub fn offsets_count(&self) -> Result<usize, NippyJarError> {
409 Ok((self.offset_file.metadata()?.len().saturating_sub(1) / self.offset_size as u64)
410 as usize)
411 }
412
413 fn offset_at(&self, index: usize) -> Result<u64, NippyJarError> {
415 let mut buffer: [u8; 8] = [0; 8];
416
417 let offset_end = index.saturating_add(self.offset_size as usize);
418 if offset_end > self.offset_mmap.len() {
419 return Err(NippyJarError::OffsetOutOfBounds { index })
420 }
421
422 buffer[..self.offset_size as usize].copy_from_slice(&self.offset_mmap[index..offset_end]);
423 Ok(u64::from_le_bytes(buffer))
424 }
425
426 pub const fn offset_size(&self) -> u8 {
428 self.offset_size
429 }
430
431 pub fn data(&self, range: Range<usize>) -> &[u8] {
433 &self.data_mmap[range]
434 }
435
436 pub fn size(&self) -> usize {
438 self.data_mmap.len()
439 }
440
441 pub fn offsets_size(&self) -> usize {
443 self.offset_mmap.len()
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450 use compression::Compression;
451 use rand::{rngs::SmallRng, seq::SliceRandom, RngCore, SeedableRng};
452 use std::{fs::OpenOptions, io::Read};
453
454 type ColumnResults<T> = Vec<ColumnResult<T>>;
455 type ColumnValues = Vec<Vec<u8>>;
456
457 fn test_data(seed: Option<u64>) -> (ColumnValues, ColumnValues) {
458 let value_length = 32;
459 let num_rows = 100;
460
461 let mut vec: Vec<u8> = vec![0; value_length];
462 let mut rng = seed.map(SmallRng::seed_from_u64).unwrap_or_else(SmallRng::from_os_rng);
463
464 let mut entry_gen = || {
465 (0..num_rows)
466 .map(|_| {
467 rng.fill_bytes(&mut vec[..]);
468 vec.clone()
469 })
470 .collect()
471 };
472
473 (entry_gen(), entry_gen())
474 }
475
476 fn clone_with_result(col: &ColumnValues) -> ColumnResults<Vec<u8>> {
477 col.iter().map(|v| Ok(v.clone())).collect()
478 }
479
480 #[test]
481 fn test_config_serialization() {
482 let file = tempfile::NamedTempFile::new().unwrap();
483 let jar = NippyJar::new_without_header(23, file.path()).with_lz4();
484 jar.freeze_config().unwrap();
485
486 let mut config_file = OpenOptions::new().read(true).open(jar.config_path()).unwrap();
487 let config_file_len = config_file.metadata().unwrap().len();
488 assert_eq!(config_file_len, 37);
489
490 let mut buf = Vec::with_capacity(config_file_len as usize);
491 config_file.read_to_end(&mut buf).unwrap();
492
493 assert_eq!(
494 vec![
495 1, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
496 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
497 ],
498 buf
499 );
500
501 let mut read_jar = bincode::deserialize_from::<_, NippyJar>(&buf[..]).unwrap();
502 read_jar.path = file.path().to_path_buf();
504 assert_eq!(jar, read_jar);
505 }
506
507 #[test]
508 fn test_zstd_with_dictionaries() {
509 let (col1, col2) = test_data(None);
510 let num_rows = col1.len() as u64;
511 let num_columns = 2;
512 let file_path = tempfile::NamedTempFile::new().unwrap();
513
514 let nippy = NippyJar::new_without_header(num_columns, file_path.path());
515 assert!(nippy.compressor().is_none());
516
517 let mut nippy =
518 NippyJar::new_without_header(num_columns, file_path.path()).with_zstd(true, 5000);
519 assert!(nippy.compressor().is_some());
520
521 if let Some(Compressors::Zstd(zstd)) = &mut nippy.compressor_mut() {
522 assert!(matches!(zstd.compressors(), Err(NippyJarError::CompressorNotReady)));
523
524 assert!(matches!(
526 zstd.prepare_compression(vec![col1.clone(), col2.clone(), col2.clone()]),
527 Err(NippyJarError::ColumnLenMismatch(columns, 3)) if columns == num_columns
528 ));
529 }
530
531 assert!(matches!(
534 nippy.freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows),
535 Err(NippyJarError::CompressorNotReady)
536 ));
537
538 let mut nippy =
539 NippyJar::new_without_header(num_columns, file_path.path()).with_zstd(true, 5000);
540 assert!(nippy.compressor().is_some());
541
542 nippy.prepare_compression(vec![col1.clone(), col2.clone()]).unwrap();
543
544 if let Some(Compressors::Zstd(zstd)) = &nippy.compressor() {
545 assert!(matches!(
546 (&zstd.state, zstd.dictionaries.as_ref().map(|dict| dict.len())),
547 (compression::ZstdState::Ready, Some(columns)) if columns == num_columns
548 ));
549 }
550
551 let nippy = nippy
552 .freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows)
553 .unwrap();
554
555 let loaded_nippy = NippyJar::load_without_header(file_path.path()).unwrap();
556 assert_eq!(nippy.version, loaded_nippy.version);
557 assert_eq!(nippy.columns, loaded_nippy.columns);
558 assert_eq!(nippy.filter, loaded_nippy.filter);
559 assert_eq!(nippy.phf, loaded_nippy.phf);
560 assert_eq!(nippy.max_row_size, loaded_nippy.max_row_size);
561 assert_eq!(nippy.path, loaded_nippy.path);
562
563 if let Some(Compressors::Zstd(zstd)) = loaded_nippy.compressor() {
564 assert!(zstd.use_dict);
565 let mut cursor = NippyJarCursor::new(&loaded_nippy).unwrap();
566
567 let mut row_index = 0usize;
569 while let Some(row) = cursor.next_row().unwrap() {
570 assert_eq!(
571 (row[0], row[1]),
572 (col1[row_index].as_slice(), col2[row_index].as_slice())
573 );
574 row_index += 1;
575 }
576 } else {
577 panic!("Expected Zstd compressor")
578 }
579 }
580
581 #[test]
582 fn test_lz4() {
583 let (col1, col2) = test_data(None);
584 let num_rows = col1.len() as u64;
585 let num_columns = 2;
586 let file_path = tempfile::NamedTempFile::new().unwrap();
587
588 let nippy = NippyJar::new_without_header(num_columns, file_path.path());
589 assert!(nippy.compressor().is_none());
590
591 let nippy = NippyJar::new_without_header(num_columns, file_path.path()).with_lz4();
592 assert!(nippy.compressor().is_some());
593
594 let nippy = nippy
595 .freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows)
596 .unwrap();
597
598 let loaded_nippy = NippyJar::load_without_header(file_path.path()).unwrap();
599 assert_eq!(nippy, loaded_nippy);
600
601 if let Some(Compressors::Lz4(_)) = loaded_nippy.compressor() {
602 let mut cursor = NippyJarCursor::new(&loaded_nippy).unwrap();
603
604 let mut row_index = 0usize;
606 while let Some(row) = cursor.next_row().unwrap() {
607 assert_eq!(
608 (row[0], row[1]),
609 (col1[row_index].as_slice(), col2[row_index].as_slice())
610 );
611 row_index += 1;
612 }
613 } else {
614 panic!("Expected Lz4 compressor")
615 }
616 }
617
618 #[test]
619 fn test_zstd_no_dictionaries() {
620 let (col1, col2) = test_data(None);
621 let num_rows = col1.len() as u64;
622 let num_columns = 2;
623 let file_path = tempfile::NamedTempFile::new().unwrap();
624
625 let nippy = NippyJar::new_without_header(num_columns, file_path.path());
626 assert!(nippy.compressor().is_none());
627
628 let nippy =
629 NippyJar::new_without_header(num_columns, file_path.path()).with_zstd(false, 5000);
630 assert!(nippy.compressor().is_some());
631
632 let nippy = nippy
633 .freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows)
634 .unwrap();
635
636 let loaded_nippy = NippyJar::load_without_header(file_path.path()).unwrap();
637 assert_eq!(nippy, loaded_nippy);
638
639 if let Some(Compressors::Zstd(zstd)) = loaded_nippy.compressor() {
640 assert!(!zstd.use_dict);
641
642 let mut cursor = NippyJarCursor::new(&loaded_nippy).unwrap();
643
644 let mut row_index = 0usize;
646 while let Some(row) = cursor.next_row().unwrap() {
647 assert_eq!(
648 (row[0], row[1]),
649 (col1[row_index].as_slice(), col2[row_index].as_slice())
650 );
651 row_index += 1;
652 }
653 } else {
654 panic!("Expected Zstd compressor")
655 }
656 }
657
658 #[test]
660 fn test_full_nippy_jar() {
661 let (col1, col2) = test_data(None);
662 let num_rows = col1.len() as u64;
663 let num_columns = 2;
664 let file_path = tempfile::NamedTempFile::new().unwrap();
665 let data = vec![col1.clone(), col2.clone()];
666
667 let block_start = 500;
668
669 #[derive(Serialize, Deserialize, Debug)]
670 struct BlockJarHeader {
671 block_start: usize,
672 }
673
674 {
676 let mut nippy =
677 NippyJar::new(num_columns, file_path.path(), BlockJarHeader { block_start })
678 .with_zstd(true, 5000);
679
680 nippy.prepare_compression(data.clone()).unwrap();
681 nippy
682 .freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows)
683 .unwrap();
684 }
685
686 {
688 let loaded_nippy = NippyJar::<BlockJarHeader>::load(file_path.path()).unwrap();
689
690 assert!(loaded_nippy.compressor().is_some());
691 assert_eq!(loaded_nippy.user_header().block_start, block_start);
692
693 if let Some(Compressors::Zstd(_zstd)) = loaded_nippy.compressor() {
694 let mut cursor = NippyJarCursor::new(&loaded_nippy).unwrap();
695
696 let mut row_num = 0usize;
698 while let Some(row) = cursor.next_row().unwrap() {
699 assert_eq!(
700 (row[0], row[1]),
701 (data[0][row_num].as_slice(), data[1][row_num].as_slice())
702 );
703 row_num += 1;
704 }
705
706 let mut data = col1.iter().zip(col2.iter()).enumerate().collect::<Vec<_>>();
708 data.shuffle(&mut rand::rng());
709
710 for (row_num, (v0, v1)) in data {
711 let row_by_num = cursor.row_by_number(row_num).unwrap().unwrap();
713 assert_eq!((&row_by_num[0].to_vec(), &row_by_num[1].to_vec()), (v0, v1));
714 }
715 }
716 }
717 }
718
719 #[test]
720 fn test_selectable_column_values() {
721 let (col1, col2) = test_data(None);
722 let num_rows = col1.len() as u64;
723 let num_columns = 2;
724 let file_path = tempfile::NamedTempFile::new().unwrap();
725 let data = vec![col1.clone(), col2.clone()];
726
727 {
729 let mut nippy =
730 NippyJar::new_without_header(num_columns, file_path.path()).with_zstd(true, 5000);
731 nippy.prepare_compression(data).unwrap();
732 nippy
733 .freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows)
734 .unwrap();
735 }
736
737 {
739 let loaded_nippy = NippyJar::load_without_header(file_path.path()).unwrap();
740
741 if let Some(Compressors::Zstd(_zstd)) = loaded_nippy.compressor() {
742 let mut cursor = NippyJarCursor::new(&loaded_nippy).unwrap();
743
744 let mut data = col1.iter().zip(col2.iter()).enumerate().collect::<Vec<_>>();
746 data.shuffle(&mut rand::rng());
747
748 const BLOCKS_FULL_MASK: usize = 0b11;
750
751 for (row_num, (v0, v1)) in &data {
753 let row_by_num = cursor
755 .row_by_number_with_cols(*row_num, BLOCKS_FULL_MASK)
756 .unwrap()
757 .unwrap();
758 assert_eq!((&row_by_num[0].to_vec(), &row_by_num[1].to_vec()), (*v0, *v1));
759 }
760
761 const BLOCKS_BLOCK_MASK: usize = 0b01;
763 for (row_num, (v0, _)) in &data {
764 let row_by_num = cursor
766 .row_by_number_with_cols(*row_num, BLOCKS_BLOCK_MASK)
767 .unwrap()
768 .unwrap();
769 assert_eq!(row_by_num.len(), 1);
770 assert_eq!(&row_by_num[0].to_vec(), *v0);
771 }
772
773 const BLOCKS_WITHDRAWAL_MASK: usize = 0b10;
775 for (row_num, (_, v1)) in &data {
776 let row_by_num = cursor
778 .row_by_number_with_cols(*row_num, BLOCKS_WITHDRAWAL_MASK)
779 .unwrap()
780 .unwrap();
781 assert_eq!(row_by_num.len(), 1);
782 assert_eq!(&row_by_num[0].to_vec(), *v1);
783 }
784
785 const BLOCKS_EMPTY_MASK: usize = 0b00;
787 for (row_num, _) in &data {
788 assert!(cursor
790 .row_by_number_with_cols(*row_num, BLOCKS_EMPTY_MASK)
791 .unwrap()
792 .unwrap()
793 .is_empty());
794 }
795 }
796 }
797 }
798
799 #[test]
800 fn test_writer() {
801 let (col1, col2) = test_data(None);
802 let num_columns = 2;
803 let file_path = tempfile::NamedTempFile::new().unwrap();
804
805 append_two_rows(num_columns, file_path.path(), &col1, &col2);
806
807 prune_rows(num_columns, file_path.path(), &col1, &col2);
810
811 append_two_rows(num_columns, file_path.path(), &col1, &col2);
813
814 test_append_consistency_no_commit(file_path.path(), &col1, &col2);
817
818 test_append_consistency_partial_commit(file_path.path(), &col1, &col2);
820 }
821
822 #[test]
823 fn test_pruner() {
824 let (col1, col2) = test_data(None);
825 let num_columns = 2;
826 let num_rows = 2;
827
828 let missing_offsets_scenarios = [(1, 1), (2, 1), (3, 0)];
831
832 for (missing_offsets, expected_rows) in missing_offsets_scenarios {
833 let file_path = tempfile::NamedTempFile::new().unwrap();
834
835 append_two_rows(num_columns, file_path.path(), &col1, &col2);
836
837 simulate_interrupted_prune(num_columns, file_path.path(), num_rows, missing_offsets);
838
839 let nippy = NippyJar::load_without_header(file_path.path()).unwrap();
840 assert_eq!(nippy.rows, expected_rows);
841 }
842 }
843
844 fn test_append_consistency_partial_commit(
845 file_path: &Path,
846 col1: &[Vec<u8>],
847 col2: &[Vec<u8>],
848 ) {
849 let nippy = NippyJar::load_without_header(file_path).unwrap();
850
851 let initial_rows = nippy.rows;
853 let initial_data_size =
854 File::open(nippy.data_path()).unwrap().metadata().unwrap().len() as usize;
855 let initial_offset_size =
856 File::open(nippy.offsets_path()).unwrap().metadata().unwrap().len() as usize;
857 assert!(initial_data_size > 0);
858 assert!(initial_offset_size > 0);
859
860 let mut writer = NippyJarWriter::new(nippy).unwrap();
862 writer.append_column(Some(Ok(&col1[2]))).unwrap();
863 writer.append_column(Some(Ok(&col2[2]))).unwrap();
864
865 let _ = writer.offsets_mut().pop();
867
868 writer.commit_offsets().unwrap();
871
872 drop(writer);
874
875 let nippy = NippyJar::load_without_header(file_path).unwrap();
876 assert_eq!(initial_rows, nippy.rows);
877
878 let new_data_size =
880 File::open(nippy.data_path()).unwrap().metadata().unwrap().len() as usize;
881 assert_eq!(new_data_size, initial_data_size + col1[2].len() + col2[2].len());
882
883 assert_eq!(
885 initial_offset_size + 8,
886 File::open(nippy.offsets_path()).unwrap().metadata().unwrap().len() as usize
887 );
888
889 let writer = NippyJarWriter::new(nippy).unwrap();
893 assert_eq!(initial_rows, writer.rows());
894 assert_eq!(
895 initial_offset_size,
896 File::open(writer.offsets_path()).unwrap().metadata().unwrap().len() as usize
897 );
898 assert_eq!(
899 initial_data_size,
900 File::open(writer.data_path()).unwrap().metadata().unwrap().len() as usize
901 );
902 }
903
904 fn test_append_consistency_no_commit(file_path: &Path, col1: &[Vec<u8>], col2: &[Vec<u8>]) {
905 let nippy = NippyJar::load_without_header(file_path).unwrap();
906
907 let initial_rows = nippy.rows;
909 let initial_data_size =
910 File::open(nippy.data_path()).unwrap().metadata().unwrap().len() as usize;
911 let initial_offset_size =
912 File::open(nippy.offsets_path()).unwrap().metadata().unwrap().len() as usize;
913 assert!(initial_data_size > 0);
914 assert!(initial_offset_size > 0);
915
916 let mut writer = NippyJarWriter::new(nippy).unwrap();
919 writer.append_column(Some(Ok(&col1[2]))).unwrap();
920 writer.append_column(Some(Ok(&col2[2]))).unwrap();
921
922 drop(writer);
924
925 let nippy = NippyJar::load_without_header(file_path).unwrap();
926 assert_eq!(initial_rows, nippy.rows);
927
928 let new_data_size =
930 File::open(nippy.data_path()).unwrap().metadata().unwrap().len() as usize;
931 assert_eq!(new_data_size, initial_data_size + col1[2].len() + col2[2].len());
932
933 assert_eq!(
935 initial_offset_size,
936 File::open(nippy.offsets_path()).unwrap().metadata().unwrap().len() as usize
937 );
938
939 let writer = NippyJarWriter::new(nippy).unwrap();
942 assert_eq!(initial_rows, writer.rows());
943 assert_eq!(
944 initial_data_size,
945 File::open(writer.data_path()).unwrap().metadata().unwrap().len() as usize
946 );
947 }
948
949 fn append_two_rows(num_columns: usize, file_path: &Path, col1: &[Vec<u8>], col2: &[Vec<u8>]) {
950 {
952 let nippy = NippyJar::new_without_header(num_columns, file_path);
953 nippy.freeze_config().unwrap();
954 assert_eq!(nippy.max_row_size, 0);
955 assert_eq!(nippy.rows, 0);
956
957 let mut writer = NippyJarWriter::new(nippy).unwrap();
958 assert_eq!(writer.column(), 0);
959
960 writer.append_column(Some(Ok(&col1[0]))).unwrap();
961 assert_eq!(writer.column(), 1);
962 assert!(writer.is_dirty());
963
964 writer.append_column(Some(Ok(&col2[0]))).unwrap();
965 assert!(writer.is_dirty());
966
967 assert_eq!(writer.column(), 0);
969
970 assert_eq!(writer.offsets().len(), 3);
972 let expected_data_file_size = *writer.offsets().last().unwrap();
973 writer.commit().unwrap();
974 assert!(!writer.is_dirty());
975
976 assert_eq!(writer.max_row_size(), col1[0].len() + col2[0].len());
977 assert_eq!(writer.rows(), 1);
978 assert_eq!(
979 File::open(writer.offsets_path()).unwrap().metadata().unwrap().len(),
980 1 + num_columns as u64 * 8 + 8
981 );
982 assert_eq!(
983 File::open(writer.data_path()).unwrap().metadata().unwrap().len(),
984 expected_data_file_size
985 );
986 }
987
988 {
990 let nippy = NippyJar::load_without_header(file_path).unwrap();
991 assert_eq!(nippy.max_row_size, col1[0].len() + col2[0].len());
993 assert_eq!(nippy.rows, 1);
994
995 let mut writer = NippyJarWriter::new(nippy).unwrap();
996 assert_eq!(writer.column(), 0);
997
998 writer.append_column(Some(Ok(&col1[1]))).unwrap();
999 assert_eq!(writer.column(), 1);
1000
1001 writer.append_column(Some(Ok(&col2[1]))).unwrap();
1002
1003 assert_eq!(writer.column(), 0);
1005
1006 assert_eq!(writer.offsets().len(), 3);
1008 let expected_data_file_size = *writer.offsets().last().unwrap();
1009 writer.commit().unwrap();
1010
1011 assert_eq!(writer.max_row_size(), col1[0].len() + col2[0].len());
1012 assert_eq!(writer.rows(), 2);
1013 assert_eq!(
1014 File::open(writer.offsets_path()).unwrap().metadata().unwrap().len(),
1015 1 + writer.rows() as u64 * num_columns as u64 * 8 + 8
1016 );
1017 assert_eq!(
1018 File::open(writer.data_path()).unwrap().metadata().unwrap().len(),
1019 expected_data_file_size
1020 );
1021 }
1022 }
1023
1024 fn prune_rows(num_columns: usize, file_path: &Path, col1: &[Vec<u8>], col2: &[Vec<u8>]) {
1025 let nippy = NippyJar::load_without_header(file_path).unwrap();
1026 let mut writer = NippyJarWriter::new(nippy).unwrap();
1027
1028 writer.append_column(Some(Ok(&col1[2]))).unwrap();
1030 writer.append_column(Some(Ok(&col2[2]))).unwrap();
1031 assert!(writer.is_dirty());
1032
1033 writer.prune_rows(2).unwrap();
1035 assert_eq!(writer.rows(), 1);
1036
1037 assert_eq!(
1038 File::open(writer.offsets_path()).unwrap().metadata().unwrap().len(),
1039 1 + writer.rows() as u64 * num_columns as u64 * 8 + 8
1040 );
1041
1042 let expected_data_size = col1[0].len() + col2[0].len();
1043 assert_eq!(
1044 File::open(writer.data_path()).unwrap().metadata().unwrap().len() as usize,
1045 expected_data_size
1046 );
1047
1048 let nippy = NippyJar::load_without_header(file_path).unwrap();
1049 {
1050 let data_reader = nippy.open_data_reader().unwrap();
1051 assert_eq!(data_reader.offset(2).unwrap(), expected_data_size as u64);
1054 }
1055
1056 let mut writer = NippyJarWriter::new(nippy).unwrap();
1058 writer.prune_rows(1).unwrap();
1059 assert!(writer.is_dirty());
1060
1061 assert_eq!(writer.rows(), 0);
1062 assert_eq!(writer.max_row_size(), 0);
1063 assert_eq!(File::open(writer.data_path()).unwrap().metadata().unwrap().len() as usize, 0);
1064 assert_eq!(
1066 File::open(writer.offsets_path()).unwrap().metadata().unwrap().len() as usize,
1067 9
1068 );
1069 writer.commit().unwrap();
1070 assert!(!writer.is_dirty());
1071 }
1072
1073 fn simulate_interrupted_prune(
1074 num_columns: usize,
1075 file_path: &Path,
1076 num_rows: u64,
1077 missing_offsets: u64,
1078 ) {
1079 let nippy = NippyJar::load_without_header(file_path).unwrap();
1080 let reader = nippy.open_data_reader().unwrap();
1081 let offsets_file =
1082 OpenOptions::new().read(true).write(true).open(nippy.offsets_path()).unwrap();
1083 let offsets_len = 1 + num_rows * num_columns as u64 * 8 + 8;
1084 assert_eq!(offsets_len, offsets_file.metadata().unwrap().len());
1085
1086 let data_file = OpenOptions::new().read(true).write(true).open(nippy.data_path()).unwrap();
1087 let data_len = reader.reverse_offset(0).unwrap();
1088 assert_eq!(data_len, data_file.metadata().unwrap().len());
1089
1090 data_file.set_len(data_len - 32 * missing_offsets).unwrap();
1096
1097 let _ = NippyJarWriter::new(nippy).unwrap();
1099 }
1100}