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::{
453 fs::OpenOptions,
454 io::{Read, Seek, SeekFrom},
455 };
456
457 type ColumnResults<T> = Vec<ColumnResult<T>>;
458 type ColumnValues = Vec<Vec<u8>>;
459
460 fn test_data(seed: Option<u64>) -> (ColumnValues, ColumnValues) {
461 let value_length = 32;
462 let num_rows = 100;
463
464 let mut vec: Vec<u8> = vec![0; value_length];
465 let mut rng = seed.map(SmallRng::seed_from_u64).unwrap_or_else(SmallRng::from_os_rng);
466
467 let mut entry_gen = || {
468 (0..num_rows)
469 .map(|_| {
470 rng.fill_bytes(&mut vec[..]);
471 vec.clone()
472 })
473 .collect()
474 };
475
476 (entry_gen(), entry_gen())
477 }
478
479 fn clone_with_result(col: &ColumnValues) -> ColumnResults<Vec<u8>> {
480 col.iter().map(|v| Ok(v.clone())).collect()
481 }
482
483 #[test]
484 fn test_config_serialization() {
485 let file = tempfile::NamedTempFile::new().unwrap();
486 let jar = NippyJar::new_without_header(23, file.path()).with_lz4();
487 jar.freeze_config().unwrap();
488
489 let mut config_file = OpenOptions::new().read(true).open(jar.config_path()).unwrap();
490 let config_file_len = config_file.metadata().unwrap().len();
491 assert_eq!(config_file_len, 37);
492
493 let mut buf = Vec::with_capacity(config_file_len as usize);
494 config_file.read_to_end(&mut buf).unwrap();
495
496 assert_eq!(
497 vec![
498 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,
499 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
500 ],
501 buf
502 );
503
504 let mut read_jar = bincode::deserialize_from::<_, NippyJar>(&buf[..]).unwrap();
505 read_jar.path = file.path().to_path_buf();
507 assert_eq!(jar, read_jar);
508 }
509
510 #[test]
511 fn test_zstd_with_dictionaries() {
512 let (col1, col2) = test_data(None);
513 let num_rows = col1.len() as u64;
514 let num_columns = 2;
515 let file_path = tempfile::NamedTempFile::new().unwrap();
516
517 let nippy = NippyJar::new_without_header(num_columns, file_path.path());
518 assert!(nippy.compressor().is_none());
519
520 let mut nippy =
521 NippyJar::new_without_header(num_columns, file_path.path()).with_zstd(true, 5000);
522 assert!(nippy.compressor().is_some());
523
524 if let Some(Compressors::Zstd(zstd)) = &mut nippy.compressor_mut() {
525 assert!(matches!(zstd.compressors(), Err(NippyJarError::CompressorNotReady)));
526
527 assert!(matches!(
529 zstd.prepare_compression(vec![col1.clone(), col2.clone(), col2.clone()]),
530 Err(NippyJarError::ColumnLenMismatch(columns, 3)) if columns == num_columns
531 ));
532 }
533
534 assert!(matches!(
537 nippy.freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows),
538 Err(NippyJarError::CompressorNotReady)
539 ));
540
541 let mut nippy =
542 NippyJar::new_without_header(num_columns, file_path.path()).with_zstd(true, 5000);
543 assert!(nippy.compressor().is_some());
544
545 nippy.prepare_compression(vec![col1.clone(), col2.clone()]).unwrap();
546
547 if let Some(Compressors::Zstd(zstd)) = &nippy.compressor() {
548 assert!(matches!(
549 (&zstd.state, zstd.dictionaries.as_ref().map(|dict| dict.len())),
550 (compression::ZstdState::Ready, Some(columns)) if columns == num_columns
551 ));
552 }
553
554 let nippy = nippy
555 .freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows)
556 .unwrap();
557
558 let loaded_nippy = NippyJar::load_without_header(file_path.path()).unwrap();
559 assert_eq!(nippy.version, loaded_nippy.version);
560 assert_eq!(nippy.columns, loaded_nippy.columns);
561 assert_eq!(nippy.filter, loaded_nippy.filter);
562 assert_eq!(nippy.phf, loaded_nippy.phf);
563 assert_eq!(nippy.max_row_size, loaded_nippy.max_row_size);
564 assert_eq!(nippy.path, loaded_nippy.path);
565
566 if let Some(Compressors::Zstd(zstd)) = loaded_nippy.compressor() {
567 assert!(zstd.use_dict);
568 let mut cursor = NippyJarCursor::new(&loaded_nippy).unwrap();
569
570 let mut row_index = 0usize;
572 while let Some(row) = cursor.next_row().unwrap() {
573 assert_eq!(
574 (row[0], row[1]),
575 (col1[row_index].as_slice(), col2[row_index].as_slice())
576 );
577 row_index += 1;
578 }
579 } else {
580 panic!("Expected Zstd compressor")
581 }
582 }
583
584 #[test]
585 fn test_lz4() {
586 let (col1, col2) = test_data(None);
587 let num_rows = col1.len() as u64;
588 let num_columns = 2;
589 let file_path = tempfile::NamedTempFile::new().unwrap();
590
591 let nippy = NippyJar::new_without_header(num_columns, file_path.path());
592 assert!(nippy.compressor().is_none());
593
594 let nippy = NippyJar::new_without_header(num_columns, file_path.path()).with_lz4();
595 assert!(nippy.compressor().is_some());
596
597 let nippy = nippy
598 .freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows)
599 .unwrap();
600
601 let loaded_nippy = NippyJar::load_without_header(file_path.path()).unwrap();
602 assert_eq!(nippy, loaded_nippy);
603
604 if let Some(Compressors::Lz4(_)) = loaded_nippy.compressor() {
605 let mut cursor = NippyJarCursor::new(&loaded_nippy).unwrap();
606
607 let mut row_index = 0usize;
609 while let Some(row) = cursor.next_row().unwrap() {
610 assert_eq!(
611 (row[0], row[1]),
612 (col1[row_index].as_slice(), col2[row_index].as_slice())
613 );
614 row_index += 1;
615 }
616 } else {
617 panic!("Expected Lz4 compressor")
618 }
619 }
620
621 #[test]
622 fn test_zstd_no_dictionaries() {
623 let (col1, col2) = test_data(None);
624 let num_rows = col1.len() as u64;
625 let num_columns = 2;
626 let file_path = tempfile::NamedTempFile::new().unwrap();
627
628 let nippy = NippyJar::new_without_header(num_columns, file_path.path());
629 assert!(nippy.compressor().is_none());
630
631 let nippy =
632 NippyJar::new_without_header(num_columns, file_path.path()).with_zstd(false, 5000);
633 assert!(nippy.compressor().is_some());
634
635 let nippy = nippy
636 .freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows)
637 .unwrap();
638
639 let loaded_nippy = NippyJar::load_without_header(file_path.path()).unwrap();
640 assert_eq!(nippy, loaded_nippy);
641
642 if let Some(Compressors::Zstd(zstd)) = loaded_nippy.compressor() {
643 assert!(!zstd.use_dict);
644
645 let mut cursor = NippyJarCursor::new(&loaded_nippy).unwrap();
646
647 let mut row_index = 0usize;
649 while let Some(row) = cursor.next_row().unwrap() {
650 assert_eq!(
651 (row[0], row[1]),
652 (col1[row_index].as_slice(), col2[row_index].as_slice())
653 );
654 row_index += 1;
655 }
656 } else {
657 panic!("Expected Zstd compressor")
658 }
659 }
660
661 #[test]
663 fn test_full_nippy_jar() {
664 let (col1, col2) = test_data(None);
665 let num_rows = col1.len() as u64;
666 let num_columns = 2;
667 let file_path = tempfile::NamedTempFile::new().unwrap();
668 let data = vec![col1.clone(), col2.clone()];
669
670 let block_start = 500;
671
672 #[derive(Serialize, Deserialize, Debug)]
673 struct BlockJarHeader {
674 block_start: usize,
675 }
676
677 {
679 let mut nippy =
680 NippyJar::new(num_columns, file_path.path(), BlockJarHeader { block_start })
681 .with_zstd(true, 5000);
682
683 nippy.prepare_compression(data.clone()).unwrap();
684 nippy
685 .freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows)
686 .unwrap();
687 }
688
689 {
691 let loaded_nippy = NippyJar::<BlockJarHeader>::load(file_path.path()).unwrap();
692
693 assert!(loaded_nippy.compressor().is_some());
694 assert_eq!(loaded_nippy.user_header().block_start, block_start);
695
696 if let Some(Compressors::Zstd(_zstd)) = loaded_nippy.compressor() {
697 let mut cursor = NippyJarCursor::new(&loaded_nippy).unwrap();
698
699 let mut row_num = 0usize;
701 while let Some(row) = cursor.next_row().unwrap() {
702 assert_eq!(
703 (row[0], row[1]),
704 (data[0][row_num].as_slice(), data[1][row_num].as_slice())
705 );
706 row_num += 1;
707 }
708
709 let mut data = col1.iter().zip(col2.iter()).enumerate().collect::<Vec<_>>();
711 data.shuffle(&mut rand::rng());
712
713 for (row_num, (v0, v1)) in data {
714 let row_by_num = cursor.row_by_number(row_num).unwrap().unwrap();
716 assert_eq!((&row_by_num[0].to_vec(), &row_by_num[1].to_vec()), (v0, v1));
717 }
718 }
719 }
720 }
721
722 #[test]
723 fn test_selectable_column_values() {
724 let (col1, col2) = test_data(None);
725 let num_rows = col1.len() as u64;
726 let num_columns = 2;
727 let file_path = tempfile::NamedTempFile::new().unwrap();
728 let data = vec![col1.clone(), col2.clone()];
729
730 {
732 let mut nippy =
733 NippyJar::new_without_header(num_columns, file_path.path()).with_zstd(true, 5000);
734 nippy.prepare_compression(data).unwrap();
735 nippy
736 .freeze(vec![clone_with_result(&col1), clone_with_result(&col2)], num_rows)
737 .unwrap();
738 }
739
740 {
742 let loaded_nippy = NippyJar::load_without_header(file_path.path()).unwrap();
743
744 if let Some(Compressors::Zstd(_zstd)) = loaded_nippy.compressor() {
745 let mut cursor = NippyJarCursor::new(&loaded_nippy).unwrap();
746
747 let mut data = col1.iter().zip(col2.iter()).enumerate().collect::<Vec<_>>();
749 data.shuffle(&mut rand::rng());
750
751 const BLOCKS_FULL_MASK: usize = 0b11;
753
754 for (row_num, (v0, v1)) in &data {
756 let row_by_num = cursor
758 .row_by_number_with_cols(*row_num, BLOCKS_FULL_MASK)
759 .unwrap()
760 .unwrap();
761 assert_eq!((&row_by_num[0].to_vec(), &row_by_num[1].to_vec()), (*v0, *v1));
762 }
763
764 const BLOCKS_BLOCK_MASK: usize = 0b01;
766 for (row_num, (v0, _)) in &data {
767 let row_by_num = cursor
769 .row_by_number_with_cols(*row_num, BLOCKS_BLOCK_MASK)
770 .unwrap()
771 .unwrap();
772 assert_eq!(row_by_num.len(), 1);
773 assert_eq!(&row_by_num[0].to_vec(), *v0);
774 }
775
776 const BLOCKS_WITHDRAWAL_MASK: usize = 0b10;
778 for (row_num, (_, v1)) in &data {
779 let row_by_num = cursor
781 .row_by_number_with_cols(*row_num, BLOCKS_WITHDRAWAL_MASK)
782 .unwrap()
783 .unwrap();
784 assert_eq!(row_by_num.len(), 1);
785 assert_eq!(&row_by_num[0].to_vec(), *v1);
786 }
787
788 const BLOCKS_EMPTY_MASK: usize = 0b00;
790 for (row_num, _) in &data {
791 assert!(cursor
793 .row_by_number_with_cols(*row_num, BLOCKS_EMPTY_MASK)
794 .unwrap()
795 .unwrap()
796 .is_empty());
797 }
798 }
799 }
800 }
801
802 #[test]
803 fn test_writer() {
804 let (col1, col2) = test_data(None);
805 let num_columns = 2;
806 let file_path = tempfile::NamedTempFile::new().unwrap();
807
808 append_two_rows(num_columns, file_path.path(), &col1, &col2);
809
810 prune_rows(num_columns, file_path.path(), &col1, &col2);
813
814 append_two_rows(num_columns, file_path.path(), &col1, &col2);
816
817 test_append_consistency_no_commit(file_path.path(), &col1, &col2);
820
821 test_append_consistency_partial_commit(file_path.path(), &col1, &col2);
823 }
824
825 #[test]
826 fn test_pruner() {
827 let (col1, col2) = test_data(None);
828 let num_columns = 2;
829 let num_rows = 2;
830
831 let missing_offsets_scenarios = [(1, 1), (2, 1), (3, 0)];
834
835 for (missing_offsets, expected_rows) in missing_offsets_scenarios {
836 let file_path = tempfile::NamedTempFile::new().unwrap();
837
838 append_two_rows(num_columns, file_path.path(), &col1, &col2);
839
840 simulate_interrupted_prune(num_columns, file_path.path(), num_rows, missing_offsets);
841
842 let nippy = NippyJar::load_without_header(file_path.path()).unwrap();
843 assert_eq!(nippy.rows, expected_rows);
844 }
845 }
846
847 #[test]
848 fn test_read_last_row_while_appending() {
849 let (col1, col2) = test_data(None);
850 let num_columns = 2;
851 let file_path = tempfile::NamedTempFile::new().unwrap();
852
853 append_two_rows(num_columns, file_path.path(), &col1, &col2);
854
855 let nippy = NippyJar::load_without_header(file_path.path()).unwrap();
857 let mut writer = NippyJarWriter::new(nippy).unwrap();
858 writer.append_column(Some(Ok(&col1[2]))).unwrap();
859 writer.append_column(Some(Ok(&col2[2]))).unwrap();
860 writer.data_file().flush().unwrap();
861
862 let nippy = NippyJar::load_without_header(file_path.path()).unwrap();
864 assert_eq!(nippy.rows, 2);
865 let mut cursor = NippyJarCursor::new(&nippy).unwrap();
866 assert_eq!(
867 cursor.row_by_number(1).unwrap().unwrap().as_slice(),
868 [col1[1].as_slice(), col2[1].as_slice()]
869 );
870 assert!(cursor.row_by_number(2).unwrap().is_none());
871 }
872
873 #[test]
874 fn test_read_last_row_with_invalid_final_offset() {
875 let (col1, col2) = test_data(None);
876 let num_columns = 2;
877
878 let assert_inconsistent = |final_offset: fn(u64) -> u64| {
880 let file_path = tempfile::NamedTempFile::new().unwrap();
881 append_two_rows(num_columns, file_path.path(), &col1, &col2);
882
883 let nippy = NippyJar::load_without_header(file_path.path()).unwrap();
884 let data_len = File::open(nippy.data_path()).unwrap().metadata().unwrap().len();
885 let mut offsets =
886 OpenOptions::new().read(true).write(true).open(nippy.offsets_path()).unwrap();
887 offsets.seek(SeekFrom::End(-8)).unwrap();
888 offsets.write_all(&final_offset(data_len).to_le_bytes()).unwrap();
889 offsets.sync_all().unwrap();
890
891 let nippy = NippyJar::load_without_header(file_path.path()).unwrap();
892 let mut cursor = NippyJarCursor::new(&nippy).unwrap();
893 assert!(matches!(cursor.row_by_number(1), Err(NippyJarError::InconsistentState)));
894 };
895
896 assert_inconsistent(|_| 0);
898 assert_inconsistent(|data_len| data_len + 1);
900 }
901
902 #[test]
903 fn test_prune_partially_uncommitted_rows() {
904 let (col1, col2) = test_data(None);
905 let num_columns = 2;
906 let file_path = tempfile::NamedTempFile::new().unwrap();
907
908 append_two_rows(num_columns, file_path.path(), &col1, &col2);
910
911 let nippy = NippyJar::load_without_header(file_path.path()).unwrap();
912 let mut writer = NippyJarWriter::new(nippy).unwrap();
913
914 append_row(&mut writer, &col1, &col2, 2);
916 append_row(&mut writer, &col1, &col2, 3);
917 writer.prune_rows(1).unwrap();
918 assert_eq!(writer.rows(), 3);
919
920 let expected_offsets_len = 1 + 3 * num_columns as u64 * 8 + 8;
922 assert_eq!(
923 File::open(writer.offsets_path()).unwrap().metadata().unwrap().len(),
924 expected_offsets_len
925 );
926 let expected_data_len = col1[..3].iter().chain(&col2[..3]).map(Vec::len).sum::<usize>();
927 assert_eq!(
928 File::open(writer.data_path()).unwrap().metadata().unwrap().len() as usize,
929 expected_data_len
930 );
931 assert_jar_rows(file_path.path(), &col1, &col2, 3);
932
933 writer.commit().unwrap();
935 assert_eq!(
936 File::open(writer.offsets_path()).unwrap().metadata().unwrap().len(),
937 expected_offsets_len
938 );
939 assert_jar_rows(file_path.path(), &col1, &col2, 3);
940
941 append_row(&mut writer, &col1, &col2, 3);
942 writer.commit().unwrap();
943 assert_eq!(
944 File::open(writer.offsets_path()).unwrap().metadata().unwrap().len(),
945 1 + 4 * num_columns as u64 * 8 + 8
946 );
947 assert_jar_rows(file_path.path(), &col1, &col2, 4);
948 }
949
950 #[test]
951 fn test_prune_uncommitted_and_committed_rows() {
952 let (col1, col2) = test_data(None);
953 let num_columns = 2;
954 let file_path = tempfile::NamedTempFile::new().unwrap();
955
956 append_two_rows(num_columns, file_path.path(), &col1, &col2);
958
959 let nippy = NippyJar::load_without_header(file_path.path()).unwrap();
960 let mut writer = NippyJarWriter::new(nippy).unwrap();
961
962 append_row(&mut writer, &col1, &col2, 2);
964 writer.prune_rows(2).unwrap();
965 assert_eq!(writer.rows(), 1);
966
967 writer.commit().unwrap();
969 assert_eq!(
970 File::open(writer.offsets_path()).unwrap().metadata().unwrap().len(),
971 1 + num_columns as u64 * 8 + 8
972 );
973 assert_eq!(
974 File::open(writer.data_path()).unwrap().metadata().unwrap().len() as usize,
975 col1[0].len() + col2[0].len()
976 );
977 assert_jar_rows(file_path.path(), &col1, &col2, 1);
978
979 append_row(&mut writer, &col1, &col2, 1);
981 writer.commit().unwrap();
982 assert_eq!(
983 File::open(writer.offsets_path()).unwrap().metadata().unwrap().len(),
984 1 + 2 * num_columns as u64 * 8 + 8
985 );
986 assert_jar_rows(file_path.path(), &col1, &col2, 2);
987 }
988
989 fn test_append_consistency_partial_commit(
990 file_path: &Path,
991 col1: &[Vec<u8>],
992 col2: &[Vec<u8>],
993 ) {
994 let nippy = NippyJar::load_without_header(file_path).unwrap();
995
996 let initial_rows = nippy.rows;
998 let initial_data_size =
999 File::open(nippy.data_path()).unwrap().metadata().unwrap().len() as usize;
1000 let initial_offset_size =
1001 File::open(nippy.offsets_path()).unwrap().metadata().unwrap().len() as usize;
1002 assert!(initial_data_size > 0);
1003 assert!(initial_offset_size > 0);
1004
1005 let mut writer = NippyJarWriter::new(nippy).unwrap();
1007 writer.append_column(Some(Ok(&col1[2]))).unwrap();
1008 writer.append_column(Some(Ok(&col2[2]))).unwrap();
1009
1010 let _ = writer.offsets_mut().pop();
1012
1013 writer.commit_offsets().unwrap();
1016
1017 drop(writer);
1019
1020 let nippy = NippyJar::load_without_header(file_path).unwrap();
1021 assert_eq!(initial_rows, nippy.rows);
1022
1023 let new_data_size =
1025 File::open(nippy.data_path()).unwrap().metadata().unwrap().len() as usize;
1026 assert_eq!(new_data_size, initial_data_size + col1[2].len() + col2[2].len());
1027
1028 assert_eq!(
1030 initial_offset_size + 8,
1031 File::open(nippy.offsets_path()).unwrap().metadata().unwrap().len() as usize
1032 );
1033
1034 let writer = NippyJarWriter::new(nippy).unwrap();
1038 assert_eq!(initial_rows, writer.rows());
1039 assert_eq!(
1040 initial_offset_size,
1041 File::open(writer.offsets_path()).unwrap().metadata().unwrap().len() as usize
1042 );
1043 assert_eq!(
1044 initial_data_size,
1045 File::open(writer.data_path()).unwrap().metadata().unwrap().len() as usize
1046 );
1047 }
1048
1049 fn test_append_consistency_no_commit(file_path: &Path, col1: &[Vec<u8>], col2: &[Vec<u8>]) {
1050 let nippy = NippyJar::load_without_header(file_path).unwrap();
1051
1052 let initial_rows = nippy.rows;
1054 let initial_data_size =
1055 File::open(nippy.data_path()).unwrap().metadata().unwrap().len() as usize;
1056 let initial_offset_size =
1057 File::open(nippy.offsets_path()).unwrap().metadata().unwrap().len() as usize;
1058 assert!(initial_data_size > 0);
1059 assert!(initial_offset_size > 0);
1060
1061 let mut writer = NippyJarWriter::new(nippy).unwrap();
1064 writer.append_column(Some(Ok(&col1[2]))).unwrap();
1065 writer.append_column(Some(Ok(&col2[2]))).unwrap();
1066
1067 drop(writer);
1069
1070 let nippy = NippyJar::load_without_header(file_path).unwrap();
1071 assert_eq!(initial_rows, nippy.rows);
1072
1073 let new_data_size =
1075 File::open(nippy.data_path()).unwrap().metadata().unwrap().len() as usize;
1076 assert_eq!(new_data_size, initial_data_size + col1[2].len() + col2[2].len());
1077
1078 assert_eq!(
1080 initial_offset_size,
1081 File::open(nippy.offsets_path()).unwrap().metadata().unwrap().len() as usize
1082 );
1083
1084 let writer = NippyJarWriter::new(nippy).unwrap();
1087 assert_eq!(initial_rows, writer.rows());
1088 assert_eq!(
1089 initial_data_size,
1090 File::open(writer.data_path()).unwrap().metadata().unwrap().len() as usize
1091 );
1092 }
1093
1094 fn append_two_rows(num_columns: usize, file_path: &Path, col1: &[Vec<u8>], col2: &[Vec<u8>]) {
1095 {
1097 let nippy = NippyJar::new_without_header(num_columns, file_path);
1098 nippy.freeze_config().unwrap();
1099 assert_eq!(nippy.max_row_size, 0);
1100 assert_eq!(nippy.rows, 0);
1101
1102 let mut writer = NippyJarWriter::new(nippy).unwrap();
1103 assert_eq!(writer.column(), 0);
1104
1105 writer.append_column(Some(Ok(&col1[0]))).unwrap();
1106 assert_eq!(writer.column(), 1);
1107 assert!(writer.is_dirty());
1108
1109 writer.append_column(Some(Ok(&col2[0]))).unwrap();
1110 assert!(writer.is_dirty());
1111
1112 assert_eq!(writer.column(), 0);
1114
1115 assert_eq!(writer.offsets().len(), 3);
1117 let expected_data_file_size = *writer.offsets().last().unwrap();
1118 writer.commit().unwrap();
1119 assert!(!writer.is_dirty());
1120
1121 assert_eq!(writer.max_row_size(), col1[0].len() + col2[0].len());
1122 assert_eq!(writer.rows(), 1);
1123 assert_eq!(
1124 File::open(writer.offsets_path()).unwrap().metadata().unwrap().len(),
1125 1 + num_columns as u64 * 8 + 8
1126 );
1127 assert_eq!(
1128 File::open(writer.data_path()).unwrap().metadata().unwrap().len(),
1129 expected_data_file_size
1130 );
1131 }
1132
1133 {
1135 let nippy = NippyJar::load_without_header(file_path).unwrap();
1136 assert_eq!(nippy.max_row_size, col1[0].len() + col2[0].len());
1138 assert_eq!(nippy.rows, 1);
1139
1140 let mut writer = NippyJarWriter::new(nippy).unwrap();
1141 assert_eq!(writer.column(), 0);
1142
1143 writer.append_column(Some(Ok(&col1[1]))).unwrap();
1144 assert_eq!(writer.column(), 1);
1145
1146 writer.append_column(Some(Ok(&col2[1]))).unwrap();
1147
1148 assert_eq!(writer.column(), 0);
1150
1151 assert_eq!(writer.offsets().len(), 3);
1153 let expected_data_file_size = *writer.offsets().last().unwrap();
1154 writer.commit().unwrap();
1155
1156 assert_eq!(writer.max_row_size(), col1[0].len() + col2[0].len());
1157 assert_eq!(writer.rows(), 2);
1158 assert_eq!(
1159 File::open(writer.offsets_path()).unwrap().metadata().unwrap().len(),
1160 1 + writer.rows() as u64 * num_columns as u64 * 8 + 8
1161 );
1162 assert_eq!(
1163 File::open(writer.data_path()).unwrap().metadata().unwrap().len(),
1164 expected_data_file_size
1165 );
1166 }
1167 }
1168
1169 fn prune_rows(num_columns: usize, file_path: &Path, col1: &[Vec<u8>], col2: &[Vec<u8>]) {
1170 let nippy = NippyJar::load_without_header(file_path).unwrap();
1171 let mut writer = NippyJarWriter::new(nippy).unwrap();
1172
1173 writer.append_column(Some(Ok(&col1[2]))).unwrap();
1175 writer.append_column(Some(Ok(&col2[2]))).unwrap();
1176 assert!(writer.is_dirty());
1177
1178 writer.prune_rows(2).unwrap();
1180 assert_eq!(writer.rows(), 1);
1181
1182 assert_eq!(
1183 File::open(writer.offsets_path()).unwrap().metadata().unwrap().len(),
1184 1 + writer.rows() as u64 * num_columns as u64 * 8 + 8
1185 );
1186
1187 let expected_data_size = col1[0].len() + col2[0].len();
1188 assert_eq!(
1189 File::open(writer.data_path()).unwrap().metadata().unwrap().len() as usize,
1190 expected_data_size
1191 );
1192
1193 let nippy = NippyJar::load_without_header(file_path).unwrap();
1194 {
1195 let data_reader = nippy.open_data_reader().unwrap();
1196 assert_eq!(data_reader.offset(2).unwrap(), expected_data_size as u64);
1199 }
1200
1201 let mut writer = NippyJarWriter::new(nippy).unwrap();
1203 writer.prune_rows(1).unwrap();
1204 assert!(writer.is_dirty());
1205
1206 assert_eq!(writer.rows(), 0);
1207 assert_eq!(writer.max_row_size(), 0);
1208 assert_eq!(File::open(writer.data_path()).unwrap().metadata().unwrap().len() as usize, 0);
1209 assert_eq!(
1211 File::open(writer.offsets_path()).unwrap().metadata().unwrap().len() as usize,
1212 9
1213 );
1214 writer.commit().unwrap();
1215 assert!(!writer.is_dirty());
1216 }
1217
1218 fn simulate_interrupted_prune(
1219 num_columns: usize,
1220 file_path: &Path,
1221 num_rows: u64,
1222 missing_offsets: u64,
1223 ) {
1224 let nippy = NippyJar::load_without_header(file_path).unwrap();
1225 let reader = nippy.open_data_reader().unwrap();
1226 let offsets_file =
1227 OpenOptions::new().read(true).write(true).open(nippy.offsets_path()).unwrap();
1228 let offsets_len = 1 + num_rows * num_columns as u64 * 8 + 8;
1229 assert_eq!(offsets_len, offsets_file.metadata().unwrap().len());
1230
1231 let data_file = OpenOptions::new().read(true).write(true).open(nippy.data_path()).unwrap();
1232 let data_len = reader.reverse_offset(0).unwrap();
1233 assert_eq!(data_len, data_file.metadata().unwrap().len());
1234
1235 data_file.set_len(data_len - 32 * missing_offsets).unwrap();
1241
1242 let _ = NippyJarWriter::new(nippy).unwrap();
1244 }
1245
1246 fn append_row(writer: &mut NippyJarWriter, col1: &[Vec<u8>], col2: &[Vec<u8>], row: usize) {
1247 writer.append_column(Some(Ok(&col1[row]))).unwrap();
1248 writer.append_column(Some(Ok(&col2[row]))).unwrap();
1249 }
1250
1251 fn assert_jar_rows(file_path: &Path, col1: &[Vec<u8>], col2: &[Vec<u8>], rows: usize) {
1254 let nippy = NippyJar::load_without_header(file_path).unwrap();
1255 assert_eq!(nippy.rows, rows);
1256 NippyJarChecker::new(NippyJar::load_without_header(file_path).unwrap())
1257 .check_consistency()
1258 .unwrap();
1259
1260 let mut cursor = NippyJarCursor::new(&nippy).unwrap();
1261 for row in 0..rows {
1262 let values = cursor.row_by_number(row).unwrap().unwrap();
1263 assert_eq!((values[0], values[1]), (col1[row].as_slice(), col2[row].as_slice()));
1264 }
1265 assert!(cursor.row_by_number(rows).unwrap().is_none());
1266 }
1267}