1use crate::{
2 compression::Compression, ColumnResult, NippyJar, NippyJarChecker, NippyJarError,
3 NippyJarHeader,
4};
5use std::{
6 fs::{File, OpenOptions},
7 io::{BufWriter, Read, Seek, SeekFrom, Write},
8 path::Path,
9};
10
11pub(crate) const OFFSET_SIZE_BYTES: u8 = 8;
13
14#[derive(Debug)]
28pub struct NippyJarWriter<H: NippyJarHeader = ()> {
29 jar: NippyJar<H>,
32 data_file: BufWriter<File>,
34 offsets_file: BufWriter<File>,
36 tmp_buf: Vec<u8>,
38 uncompressed_row_size: usize,
40 offsets: Vec<u64>,
42 column: usize,
44 dirty: bool,
46 pending_truncation: Option<PendingTruncation>,
47}
48
49#[derive(Debug, Clone, Copy)]
50struct PendingTruncation {
51 offsets_len: u64,
52 data_len: u64,
53}
54
55impl<H: NippyJarHeader> NippyJarWriter<H> {
56 pub fn new(jar: NippyJar<H>) -> Result<Self, NippyJarError> {
61 let (data_file, offsets_file, is_created) =
62 Self::create_or_open_files(jar.data_path(), &jar.offsets_path(), &jar.config_path())?;
63
64 let (jar, data_file, offsets_file) = if is_created {
65 jar.freeze_config()?;
67
68 (jar, BufWriter::new(data_file), BufWriter::new(offsets_file))
69 } else {
70 let mut checker = NippyJarChecker::new(jar);
73 checker.ensure_consistency()?;
74
75 let NippyJarChecker { jar, data_file, offsets_file } = checker;
76
77 (jar, data_file.expect("qed"), offsets_file.expect("qed"))
79 };
80
81 let mut writer = Self {
82 jar,
83 data_file,
84 offsets_file,
85 tmp_buf: Vec::with_capacity(1_000_000),
86 uncompressed_row_size: 0,
87 offsets: Vec::with_capacity(1_000_000),
88 column: 0,
89 dirty: false,
90 pending_truncation: None,
91 };
92
93 if !is_created {
94 writer.commit()?;
96 }
97
98 Ok(writer)
99 }
100
101 pub const fn user_header(&self) -> &H {
103 &self.jar.user_header
104 }
105
106 pub const fn user_header_mut(&mut self) -> &mut H {
111 self.dirty = true;
112 &mut self.jar.user_header
113 }
114
115 pub const fn is_dirty(&self) -> bool {
117 self.dirty
118 }
119
120 pub const fn set_dirty(&mut self) {
122 self.dirty = true
123 }
124
125 pub const fn rows(&self) -> usize {
127 self.jar.rows()
128 }
129
130 pub fn into_jar(self) -> NippyJar<H> {
132 self.jar
133 }
134
135 fn create_or_open_files(
136 data: &Path,
137 offsets: &Path,
138 config: &Path,
139 ) -> Result<(File, File, bool), NippyJarError> {
140 let is_created = !data.exists() || !offsets.exists() || !config.exists();
143
144 if !data.exists() {
145 File::create(data)?;
147 }
148
149 let mut data_file = OpenOptions::new().read(true).write(true).open(data)?;
150 if is_created {
151 data_file.set_len(0)?;
153 }
154 data_file.seek(SeekFrom::End(0))?;
155
156 if !offsets.exists() {
157 File::create(offsets)?;
159 }
160
161 let mut offsets_file = OpenOptions::new().read(true).write(true).open(offsets)?;
162 if is_created {
163 data_file.set_len(0)?;
164 data_file.seek(SeekFrom::Start(0))?;
165 offsets_file.set_len(0)?;
166
167 let mut buf = Vec::with_capacity(1 + OFFSET_SIZE_BYTES as usize);
168
169 buf.write_all(&[OFFSET_SIZE_BYTES])?;
171
172 buf.write_all(&[0; OFFSET_SIZE_BYTES as usize])?;
175
176 offsets_file.write_all(&buf)?;
177 offsets_file.set_len(buf.len() as u64)?;
180 offsets_file.seek(SeekFrom::End(0))?;
181 }
182
183 Ok((data_file, offsets_file, is_created))
184 }
185
186 pub fn append_rows(
192 &mut self,
193 column_values_per_row: Vec<impl IntoIterator<Item = ColumnResult<impl AsRef<[u8]>>>>,
194 num_rows: u64,
195 ) -> Result<(), NippyJarError> {
196 let mut column_iterators = column_values_per_row
197 .into_iter()
198 .map(|v| v.into_iter())
199 .collect::<Vec<_>>()
200 .into_iter();
201
202 for _ in 0..num_rows {
203 let mut iterators = Vec::with_capacity(self.jar.columns);
204
205 for mut column_iter in column_iterators {
206 self.append_column(column_iter.next())?;
207
208 iterators.push(column_iter);
209 }
210
211 column_iterators = iterators.into_iter();
212 }
213
214 Ok(())
215 }
216
217 pub fn append_column(
220 &mut self,
221 column: Option<ColumnResult<impl AsRef<[u8]>>>,
222 ) -> Result<(), NippyJarError> {
223 self.apply_pending_truncation()?;
224 self.dirty = true;
225
226 match column {
227 Some(Ok(value)) => {
228 if self.offsets.is_empty() {
229 self.offsets.push(self.data_file.stream_position()?);
231 }
232
233 let written = self.write_column(value.as_ref())?;
234
235 self.offsets.push(self.offsets.last().expect("qed") + written as u64);
238 }
239 None => {
240 return Err(NippyJarError::UnexpectedMissingValue(
241 self.jar.rows as u64,
242 self.column as u64,
243 ))
244 }
245 Some(Err(err)) => return Err(err.into()),
246 }
247
248 Ok(())
249 }
250
251 fn write_column(&mut self, value: &[u8]) -> Result<usize, NippyJarError> {
253 self.uncompressed_row_size += value.len();
254 let len = if let Some(compression) = &self.jar.compressor {
255 let before = self.tmp_buf.len();
256 let len = compression.compress_to(value, &mut self.tmp_buf)?;
257 self.data_file.write_all(&self.tmp_buf[before..before + len])?;
258 len
259 } else {
260 self.data_file.write_all(value)?;
261 value.len()
262 };
263
264 self.column += 1;
265
266 if self.jar.columns == self.column {
267 self.finalize_row();
268 }
269
270 Ok(len)
271 }
272
273 pub fn prune_rows(&mut self, num_rows: usize) -> Result<(), NippyJarError> {
275 self.prune_rows_deferred(num_rows)?;
276 self.apply_pending_truncation()?;
277 Ok(())
278 }
279
280 pub fn prune_rows_deferred(&mut self, num_rows: usize) -> Result<(), NippyJarError> {
283 self.apply_pending_truncation()?;
284 self.dirty = true;
285
286 self.offsets_file.flush()?;
287 self.data_file.flush()?;
288
289 let num_offsets = num_rows * self.jar.columns;
291
292 let offsets_prune_count = num_offsets.min(self.offsets.len().saturating_sub(1)); let remaining_to_prune = num_offsets.saturating_sub(offsets_prune_count);
295
296 if offsets_prune_count > 0 {
298 let new_len = self.offsets[(self.offsets.len() - 1) - offsets_prune_count]; self.offsets.truncate(self.offsets.len() - offsets_prune_count);
301
302 self.data_file.get_mut().set_len(new_len)?;
304 }
305
306 if remaining_to_prune > 0 {
308 let length = self.offsets_file.get_ref().metadata()?.len();
310
311 if length > 1 {
313 let num_offsets = (length - 1) / OFFSET_SIZE_BYTES as u64;
315
316 if remaining_to_prune as u64 > num_offsets {
317 return Err(NippyJarError::InvalidPruning(
318 num_offsets,
319 remaining_to_prune as u64,
320 ))
321 }
322
323 let new_num_offsets = num_offsets.saturating_sub(remaining_to_prune as u64);
324
325 if new_num_offsets <= 1 {
327 self.pending_truncation = Some(PendingTruncation {
334 offsets_len: 1 + OFFSET_SIZE_BYTES as u64,
335 data_len: 0,
336 });
337 } else {
338 let new_len = 1 + new_num_offsets * OFFSET_SIZE_BYTES as u64;
340 self.offsets_file
342 .seek(SeekFrom::Start(new_len.saturating_sub(OFFSET_SIZE_BYTES as u64)))?;
343 let mut last_offset = [0u8; OFFSET_SIZE_BYTES as usize];
345 self.offsets_file.get_ref().read_exact(&mut last_offset)?;
346 let last_offset = u64::from_le_bytes(last_offset);
347
348 self.pending_truncation =
350 Some(PendingTruncation { offsets_len: new_len, data_len: last_offset });
351 }
352 } else {
353 return Err(NippyJarError::InvalidPruning(0, remaining_to_prune as u64))
354 }
355 }
356
357 if remaining_to_prune > 0 {
358 self.offsets.clear();
361 } else {
362 self.commit_offsets_inner()?;
367 }
368
369 self.offsets_file.get_ref().sync_all()?;
370 self.data_file.get_ref().sync_all()?;
371
372 self.offsets_file.seek(SeekFrom::End(0))?;
373 self.data_file.seek(SeekFrom::End(0))?;
374
375 self.jar.rows = self.jar.rows.saturating_sub(num_rows);
376 if self.jar.rows == 0 {
377 self.jar.max_row_size = 0;
378 }
379 self.jar.freeze_config()?;
380
381 Ok(())
382 }
383
384 pub fn apply_pending_truncation(&mut self) -> Result<bool, NippyJarError> {
388 let Some(PendingTruncation { offsets_len, data_len }) = self.pending_truncation else {
389 return Ok(false)
390 };
391
392 self.offsets_file.get_mut().set_len(offsets_len)?;
393 self.data_file.get_mut().set_len(data_len)?;
394
395 self.offsets_file.get_ref().sync_all()?;
396 self.data_file.get_ref().sync_all()?;
397
398 self.offsets_file.seek(SeekFrom::End(0))?;
399 self.data_file.seek(SeekFrom::End(0))?;
400 self.pending_truncation = None;
401
402 Ok(true)
403 }
404
405 fn finalize_row(&mut self) {
408 self.jar.max_row_size = self.jar.max_row_size.max(self.uncompressed_row_size);
409 self.jar.rows += 1;
410
411 self.tmp_buf.clear();
412 self.uncompressed_row_size = 0;
413 self.column = 0;
414 }
415
416 pub fn commit(&mut self) -> Result<(), NippyJarError> {
418 self.sync_all()?;
419 self.finalize()?;
420 Ok(())
421 }
422
423 pub fn sync_all(&mut self) -> Result<(), NippyJarError> {
428 self.data_file.flush()?;
429 self.data_file.get_ref().sync_all()?;
430
431 self.commit_offsets()?;
432 Ok(())
433 }
434
435 pub fn finalize(&mut self) -> Result<(), NippyJarError> {
439 self.jar.freeze_config()?;
441 self.dirty = false;
442
443 Ok(())
444 }
445
446 #[cfg(feature = "test-utils")]
451 pub fn commit_without_sync_all(&mut self) -> Result<(), NippyJarError> {
452 self.data_file.flush()?;
453
454 self.commit_offsets_without_sync_all()?;
455
456 self.jar.freeze_config()?;
458 self.dirty = false;
459
460 Ok(())
461 }
462
463 pub(crate) fn commit_offsets(&mut self) -> Result<(), NippyJarError> {
465 self.commit_offsets_inner()?;
466 self.offsets_file.get_ref().sync_all()?;
467
468 Ok(())
469 }
470
471 #[cfg(feature = "test-utils")]
472 fn commit_offsets_without_sync_all(&mut self) -> Result<(), NippyJarError> {
473 self.commit_offsets_inner()
474 }
475
476 fn commit_offsets_inner(&mut self) -> Result<(), NippyJarError> {
481 let mut last_offset_ondisk = if self.offsets_file.get_ref().metadata()?.len() > 1 {
484 self.offsets_file.seek(SeekFrom::End(-(OFFSET_SIZE_BYTES as i64)))?;
485 let mut buf = [0u8; OFFSET_SIZE_BYTES as usize];
486 self.offsets_file.get_ref().read_exact(&mut buf)?;
487 Some(u64::from_le_bytes(buf))
488 } else {
489 None
490 };
491
492 self.offsets_file.seek(SeekFrom::End(0))?;
493
494 for offset in self.offsets.drain(..) {
496 if let Some(last_offset_ondisk) = last_offset_ondisk.take() &&
497 last_offset_ondisk == offset
498 {
499 continue
500 }
501 self.offsets_file.write_all(&offset.to_le_bytes())?;
502 }
503 self.offsets_file.flush()?;
504
505 Ok(())
506 }
507
508 #[cfg(test)]
510 pub const fn max_row_size(&self) -> usize {
511 self.jar.max_row_size
512 }
513
514 #[cfg(test)]
516 pub const fn column(&self) -> usize {
517 self.column
518 }
519
520 #[cfg(test)]
522 pub fn offsets(&self) -> &[u64] {
523 &self.offsets
524 }
525
526 #[cfg(test)]
528 pub const fn offsets_mut(&mut self) -> &mut Vec<u64> {
529 &mut self.offsets
530 }
531
532 #[cfg(test)]
534 pub fn offsets_path(&self) -> std::path::PathBuf {
535 self.jar.offsets_path()
536 }
537
538 #[cfg(test)]
540 pub fn data_path(&self) -> &Path {
541 self.jar.data_path()
542 }
543
544 #[cfg(any(test, feature = "test-utils"))]
546 pub const fn data_file(&mut self) -> &mut BufWriter<File> {
547 &mut self.data_file
548 }
549
550 #[cfg(any(test, feature = "test-utils"))]
552 pub const fn jar(&self) -> &NippyJar<H> {
553 &self.jar
554 }
555}