1use crate::columnar;
10use crate::encryption::{setup_run_encryption, Cipher, Kek, RunEncryption};
11use crate::epoch::Epoch;
12use crate::error::{MongrelError, Result};
13use crate::index::pgm::PgmIndex;
14use crate::memtable::{Row, Value};
15use crate::page::{Encoding, PageStat};
16use crate::row_id_set::RowIdSet;
17use crate::rowid::RowId;
18use crate::schema::{Schema, TypeId};
19use serde::{Deserialize, Serialize};
20use sha2::{Digest, Sha256};
21use std::collections::HashMap;
22use std::fs::{File, OpenOptions};
23use std::io::{Read, Seek, SeekFrom, Write};
24use std::path::Path;
25use std::sync::Arc;
26
27pub const RUN_MAGIC: [u8; 8] = *b"MONGRRUN";
28pub const RUN_FORMAT_VERSION: u16 = 1;
29pub const RUN_HEADER_VERSION: u16 = 2;
35pub const RUN_HEADER_PAD: usize = 256;
36
37const ENC_STATS_NONCE_COLUMN: u16 = u16::MAX;
42const ENC_STATS_NONCE_SEQ: u32 = u16::MAX as u32;
43
44type PageMinMax = (Option<Vec<u8>>, Option<Vec<u8>>);
46
47type EncryptedColumnStats = Vec<(u16, Vec<PageMinMax>)>;
54
55const GCM_TAG_LEN: usize = 16;
60
61fn on_disk_len(page_len: usize, encrypted: bool) -> usize {
64 if encrypted {
65 page_len + GCM_TAG_LEN
66 } else {
67 page_len
68 }
69}
70
71pub const RUN_FLAG_ENCRYPTED: u8 = 1 << 0;
72pub const RUN_FLAG_TOMBSTONE_ONLY: u8 = 1 << 1;
73pub const RUN_FLAG_CLEAN: u8 = 1 << 2;
82pub const RUN_FLAG_UNIFORM_EPOCH: u8 = 1 << 3;
90pub const SORT_KEY_ROW_ID: u16 = 0xFFFF;
91
92pub const SYS_ROW_ID: u16 = 0xFFFE;
94pub const SYS_EPOCH: u16 = 0xFFFD;
95pub const SYS_DELETED: u16 = 0xFFFC;
96
97const LEARNED_EPSILON: usize = 64;
100
101fn build_learned_trailer(row_ids: &[Value]) -> Vec<u8> {
105 let points: Vec<(u64, usize)> = row_ids
106 .iter()
107 .enumerate()
108 .filter_map(|(i, v)| match v {
109 Value::Int64(r) => Some((*r as u64, i)),
110 _ => None,
111 })
112 .collect();
113 let pgm = PgmIndex::build(&points, LEARNED_EPSILON);
114 bincode::serialize(&pgm).expect("pgm serialize")
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct RunHeader {
119 pub magic: [u8; 8],
120 pub format_version: u16,
121 pub header_layout_version: u16,
122 pub run_id: u128,
123 pub content_hash: [u8; 32],
124 pub schema_id: u64,
125 pub epoch_created: u64,
126 pub level: u8,
127 pub flags: u8,
128 pub sort_key_column_id: u16,
129 pub row_count: u64,
130 pub min_row_id: u64,
131 pub max_row_id: u64,
132 pub column_count: u64,
133 pub column_dir_offset: u64,
134 pub index_trailer_offset: u64,
135 pub encryption_descriptor_offset: u64,
136 pub footer_offset: u64,
137 pub encrypted_stats_offset: u64,
141 pub encrypted_stats_len: u64,
142}
143
144impl RunHeader {
145 pub fn is_encrypted(&self) -> bool {
146 self.flags & RUN_FLAG_ENCRYPTED != 0
147 }
148 pub fn is_clean(&self) -> bool {
149 self.flags & RUN_FLAG_CLEAN != 0
150 }
151 pub fn is_uniform_epoch(&self) -> bool {
152 self.flags & RUN_FLAG_UNIFORM_EPOCH != 0
153 }
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct ColumnPageHeader {
158 pub column_id: u16,
159 pub type_id_tag: u16,
160 pub encoding: u8,
161 pub flags: u8,
162 pub page_count: u32,
163 pub page_region_offset: u64,
164 pub page_region_len: u64,
165 pub page_stats: Vec<PageStat>,
166}
167
168impl ColumnPageHeader {
169 const PAGE_ENCRYPTED: u8 = 1 << 0;
170}
171
172const RUN_MAC_LEN: usize = 32;
175
176fn compute_run_mac(
180 enc: Option<&RunEncryption>,
181 header_bytes: &[u8],
182 dir_bytes: &[u8],
183) -> Option<[u8; RUN_MAC_LEN]> {
184 #[cfg(feature = "encryption")]
185 {
186 if let Some(e) = enc {
187 if let Some(mac_key) = &e.mac_key {
188 return Some(crate::encryption::run_metadata_mac(
189 mac_key,
190 header_bytes,
191 dir_bytes,
192 &e.descriptor_bytes,
193 ));
194 }
195 }
196 }
197 #[cfg(not(feature = "encryption"))]
198 {
199 let _ = (enc, header_bytes, dir_bytes);
200 }
201 None
202}
203
204pub struct ColumnPayload {
206 pub column_id: u16,
207 pub type_id_tag: u16,
208 pub encoding: Encoding,
209 pub pages: Vec<Vec<u8>>,
210 pub page_stats: Vec<PageStat>,
214}
215
216pub struct RunSpec<'a> {
218 pub run_id: u128,
219 pub schema_id: u64,
220 pub epoch_created: u64,
221 pub level: u8,
222 pub flags: u8,
223 pub sort_key_column_id: u16,
224 pub row_count: u64,
225 pub min_row_id: u64,
226 pub max_row_id: u64,
227 pub columns: &'a [ColumnPayload],
228}
229
230pub fn write_run(path: impl AsRef<Path>, spec: &RunSpec) -> Result<RunHeader> {
232 write_run_with(path, spec, None, &[], None)
233}
234
235pub fn write_run_with(
245 path: impl AsRef<Path>,
246 spec: &RunSpec,
247 kek: Option<&Kek>,
248 indexable_columns: &[(u16, u8)],
249 index_trailer: Option<&[u8]>,
250) -> Result<RunHeader> {
251 let enc: Option<RunEncryption> = match kek {
255 Some(k) => Some(setup_run_encryption(k, indexable_columns)?),
256 None => None,
257 };
258 match write_run_mmap(path.as_ref(), spec, enc.as_ref(), index_trailer) {
259 Ok(h) => Ok(h),
260 Err(e) if is_mmap_unavailable(&e) => write_run_vec(path, spec, enc, index_trailer),
261 Err(e) => Err(e),
262 }
263}
264
265fn is_mmap_unavailable(e: &MongrelError) -> bool {
269 matches!(e, MongrelError::InvalidArgument(m) if m.starts_with("__mmap_unavailable__:"))
270}
271
272fn write_run_mmap(
293 path: &Path,
294 spec: &RunSpec,
295 enc: Option<&RunEncryption>,
296 index_trailer: Option<&[u8]>,
297) -> Result<RunHeader> {
298 let plan = plan_run(spec, enc, index_trailer)?;
299 let file = OpenOptions::new()
300 .create(true)
301 .write(true)
302 .truncate(true)
303 .open(path)?;
304 file.set_len(plan.total as u64)?;
305 let mut mmap = match unsafe { memmap2::MmapMut::map_mut(&file) } {
306 Ok(m) => m,
307 Err(e) => {
308 return Err(MongrelError::InvalidArgument(format!(
309 "__mmap_unavailable__: {e}"
310 )))
311 }
312 };
313 let header = place_run(spec, enc, index_trailer, &plan, &mut mmap[..])?;
314 mmap.flush()?;
315 file.sync_all()?;
316 Ok(header)
317}
318
319struct RunPlan {
325 jobs: Vec<(usize, usize, u64, usize)>, dir_bytes: Vec<u8>,
327 encrypted: bool,
328 column_dir_offset: u64,
329 index_trailer_offset: u64,
330 encryption_descriptor_offset: u64,
331 footer_offset: u64,
332 total: usize,
333 encrypted_stats_plain: Option<Vec<u8>>,
337 encrypted_stats_offset: u64,
338 encrypted_stats_len: u64,
339}
340
341fn plan_run(
345 spec: &RunSpec,
346 enc: Option<&RunEncryption>,
347 index_trailer: Option<&[u8]>,
348) -> Result<RunPlan> {
349 let encrypted = enc.is_some();
350 let columns = spec.columns;
351 let mut jobs: Vec<(usize, usize, u64, usize)> = Vec::new();
352 let mut dir: Vec<ColumnPageHeader> = Vec::with_capacity(columns.len());
353 let mut enc_stats: EncryptedColumnStats = Vec::new();
354 let mut cursor: u64 = RUN_HEADER_PAD as u64;
355 for (ci, col) in columns.iter().enumerate() {
356 let region_offset = cursor;
357 let mut region_len = 0u64;
358 let mut stats = Vec::with_capacity(col.pages.len());
359 let mut col_minmax: Vec<PageMinMax> = Vec::new();
360 for (ps, page) in col.pages.iter().enumerate() {
361 if encrypted && ps >= u16::MAX as usize {
366 return Err(MongrelError::Full(format!(
367 "column {:#x} exceeds 65534 pages; encrypted-run page-seq nonce space exhausted",
368 col.column_id
369 )));
370 }
371 let odl = on_disk_len(page.len(), encrypted);
372 jobs.push((ci, ps, cursor, odl));
373 let mut stat = col.page_stats.get(ps).cloned().unwrap_or(PageStat {
374 first_row_id: 0,
375 last_row_id: 0,
376 null_count: 0,
377 row_count: 0,
378 min: None,
379 max: None,
380 offset: 0,
381 compressed_len: 0,
382 uncompressed_len: 0,
383 });
384 stat.offset = cursor;
385 stat.compressed_len = odl as u32;
386 stat.uncompressed_len = page.len() as u32;
387 if encrypted {
394 col_minmax.push((stat.min.take(), stat.max.take()));
395 }
396 stats.push(stat);
397 cursor += odl as u64;
398 region_len += odl as u64;
399 }
400 if col_minmax
401 .iter()
402 .any(|(mn, mx)| mn.is_some() || mx.is_some())
403 {
404 enc_stats.push((col.column_id, col_minmax));
405 }
406 let page_flags = if encrypted {
407 ColumnPageHeader::PAGE_ENCRYPTED
408 } else {
409 0
410 };
411 dir.push(ColumnPageHeader {
412 column_id: col.column_id,
413 type_id_tag: col.type_id_tag,
414 encoding: col.encoding as u8,
415 flags: page_flags,
416 page_count: col.pages.len() as u32,
417 page_region_offset: region_offset,
418 page_region_len: region_len,
419 page_stats: stats,
420 });
421 }
422 let dir_bytes = bincode::serialize(&dir)?;
423 let column_dir_offset = cursor;
424 cursor += dir_bytes.len() as u64;
425 let index_trailer_offset = match index_trailer {
426 Some(t) => {
427 let off = cursor;
428 cursor += t.len() as u64;
429 off
430 }
431 None => 0,
432 };
433 let encryption_descriptor_offset = match enc {
434 Some(e) => {
435 let off = cursor;
436 cursor += 4 + e.descriptor_bytes.len() as u64;
437 off
438 }
439 None => 0,
440 };
441 let (encrypted_stats_plain, encrypted_stats_offset, encrypted_stats_len) =
442 if encrypted && !enc_stats.is_empty() {
443 let plain = bincode::serialize(&enc_stats)?;
444 let ct_len = on_disk_len(plain.len(), true) as u64;
445 let off = cursor;
446 cursor += ct_len;
447 (Some(plain), off, ct_len)
448 } else {
449 (None, 0, 0)
450 };
451 let footer_offset = cursor;
452 let total = footer_offset as usize + 8 + 8 + 32 + if encrypted { RUN_MAC_LEN } else { 0 };
455 Ok(RunPlan {
456 jobs,
457 dir_bytes,
458 encrypted,
459 column_dir_offset,
460 index_trailer_offset,
461 encryption_descriptor_offset,
462 footer_offset,
463 total,
464 encrypted_stats_plain,
465 encrypted_stats_offset,
466 encrypted_stats_len,
467 })
468}
469
470fn place_run(
477 spec: &RunSpec,
478 enc: Option<&RunEncryption>,
479 index_trailer: Option<&[u8]>,
480 plan: &RunPlan,
481 buf: &mut [u8],
482) -> Result<RunHeader> {
483 use rayon::prelude::*;
484 use std::borrow::Cow;
485 debug_assert_eq!(
486 buf.len(),
487 plan.total,
488 "place_run: buffer must be exactly plan.total bytes"
489 );
490
491 let cipher: Option<&dyn Cipher> = enc.map(|e| e.cipher.as_ref());
492 let nonce_prefix = enc.map(|e| e.nonce_prefix);
493 let columns = spec.columns;
494
495 struct SyncPtr(*mut u8);
500 unsafe impl Send for SyncPtr {}
501 unsafe impl Sync for SyncPtr {}
502 impl SyncPtr {
503 fn get(&self) -> *mut u8 {
504 self.0
505 }
506 }
507 let base = SyncPtr(buf.as_mut_ptr());
508 plan.jobs
509 .par_iter()
510 .map(
511 |&(ci, ps, offset, odl): &(usize, usize, u64, usize)| -> Result<()> {
512 let page = &columns[ci].pages[ps];
513 let dst =
514 unsafe { std::slice::from_raw_parts_mut(base.get().add(offset as usize), odl) };
515 let bytes: Cow<[u8]> = match cipher {
516 Some(c) => {
517 let nonce =
518 page_nonce(nonce_prefix.unwrap(), columns[ci].column_id, ps as u32);
519 let ct = c.encrypt_page(&nonce, page)?;
520 assert_eq!(
523 ct.len(), odl,
524 "ciphertext length {} != predicted {}; GCM tag size assumption is stale",
525 ct.len(), odl
526 );
527 Cow::Owned(ct)
528 }
529 None => {
530 debug_assert_eq!(page.len(), odl);
531 Cow::Borrowed(page.as_slice())
532 }
533 };
534 dst.copy_from_slice(&bytes);
535 Ok(())
536 },
537 )
538 .collect::<Result<()>>()?;
539
540 let content_hash = {
542 let mut h = Sha256::new();
543 h.update(&buf[RUN_HEADER_PAD..plan.column_dir_offset as usize]);
544 h.finalize()
545 };
546
547 let doff = plan.column_dir_offset as usize;
549 buf[doff..doff + plan.dir_bytes.len()].copy_from_slice(&plan.dir_bytes);
550 if let Some(t) = index_trailer {
551 let off = plan.index_trailer_offset as usize;
552 buf[off..off + t.len()].copy_from_slice(t);
553 }
554 if let Some(e) = enc {
555 let off = plan.encryption_descriptor_offset as usize;
556 buf[off..off + 4].copy_from_slice(&(e.descriptor_bytes.len() as u32).to_le_bytes());
557 buf[off + 4..off + 4 + e.descriptor_bytes.len()].copy_from_slice(&e.descriptor_bytes);
558 }
559 if let (Some(e), Some(plain)) = (enc, plan.encrypted_stats_plain.as_ref()) {
560 let nonce = page_nonce(e.nonce_prefix, ENC_STATS_NONCE_COLUMN, ENC_STATS_NONCE_SEQ);
561 let ct = e.cipher.encrypt_page(&nonce, plain)?;
562 debug_assert_eq!(ct.len() as u64, plan.encrypted_stats_len);
563 let off = plan.encrypted_stats_offset as usize;
564 buf[off..off + ct.len()].copy_from_slice(&ct);
565 }
566
567 let header_flags = if plan.encrypted {
569 spec.flags | RUN_FLAG_ENCRYPTED
570 } else {
571 spec.flags
572 };
573 let header = RunHeader {
574 magic: RUN_MAGIC,
575 format_version: RUN_FORMAT_VERSION,
576 header_layout_version: RUN_HEADER_VERSION,
577 run_id: spec.run_id,
578 content_hash: content_hash.into(),
579 schema_id: spec.schema_id,
580 epoch_created: spec.epoch_created,
581 level: spec.level,
582 flags: header_flags,
583 sort_key_column_id: spec.sort_key_column_id,
584 row_count: spec.row_count,
585 min_row_id: spec.min_row_id,
586 max_row_id: spec.max_row_id,
587 column_count: columns.len() as u64,
588 column_dir_offset: plan.column_dir_offset,
589 index_trailer_offset: plan.index_trailer_offset,
590 encryption_descriptor_offset: plan.encryption_descriptor_offset,
591 footer_offset: plan.footer_offset,
592 encrypted_stats_offset: plan.encrypted_stats_offset,
593 encrypted_stats_len: plan.encrypted_stats_len,
594 };
595 let header_bytes = bincode::serialize(&header)?;
596 if header_bytes.len() > RUN_HEADER_PAD {
597 return Err(MongrelError::InvalidArgument(format!(
598 "run header too large: {} > {RUN_HEADER_PAD}",
599 header_bytes.len()
600 )));
601 }
602 buf[..header_bytes.len()].copy_from_slice(&header_bytes);
603
604 let checksum = Sha256::digest(&buf[..plan.footer_offset as usize]);
605 let foot = plan.footer_offset as usize;
606 buf[foot..foot + 8].copy_from_slice(&RUN_MAGIC);
607 buf[foot + 8..foot + 16].copy_from_slice(&plan.footer_offset.to_le_bytes());
608 buf[foot + 16..foot + 48].copy_from_slice(&checksum);
609 if let Some(tag) = compute_run_mac(enc, &header_bytes, &plan.dir_bytes) {
611 buf[foot + 48..foot + 48 + RUN_MAC_LEN].copy_from_slice(&tag);
612 }
613 Ok(header)
614}
615
616fn write_run_vec(
619 path: impl AsRef<Path>,
620 spec: &RunSpec,
621 enc: Option<RunEncryption>,
622 index_trailer: Option<&[u8]>,
623) -> Result<RunHeader> {
624 let mut buf: Vec<u8> = vec![0; RUN_HEADER_PAD]; let mut content_hasher = Sha256::new();
626 let mut dir: Vec<ColumnPageHeader> = Vec::with_capacity(spec.columns.len());
627 let mut enc_stats: EncryptedColumnStats = Vec::new();
628
629 for col in spec.columns {
630 let region_offset = buf.len() as u64;
631 let mut region_len = 0u64;
632 let mut stats = Vec::with_capacity(col.pages.len());
633 let mut col_minmax: Vec<PageMinMax> = Vec::new();
634 for (page_seq, page) in col.pages.iter().enumerate() {
635 if enc.is_some() && page_seq >= u16::MAX as usize {
636 return Err(MongrelError::Full(format!(
637 "column {:#x} exceeds 65534 pages; encrypted-run page-seq nonce space exhausted",
638 col.column_id
639 )));
640 }
641 let on_disk: Vec<u8> = match &enc {
642 Some(e) => e.cipher.encrypt_page(
643 &page_nonce(e.nonce_prefix, col.column_id, page_seq as u32),
644 page,
645 )?,
646 None => page.clone(),
647 };
648 let offset = buf.len() as u64;
649 buf.write_all(&on_disk)?;
650 content_hasher.update(&on_disk);
651 region_len += on_disk.len() as u64;
652 let mut stat = if let Some(s) = col.page_stats.get(page_seq) {
653 s.clone()
654 } else {
655 PageStat {
656 first_row_id: 0,
657 last_row_id: 0,
658 null_count: 0,
659 row_count: 0,
660 min: None,
661 max: None,
662 offset: 0,
663 compressed_len: 0,
664 uncompressed_len: 0,
665 }
666 };
667 stat.offset = offset;
668 stat.compressed_len = on_disk.len() as u32;
669 stat.uncompressed_len = page.len() as u32;
670 if enc.is_some() {
675 col_minmax.push((stat.min.take(), stat.max.take()));
676 }
677 stats.push(stat);
678 }
679 if col_minmax
680 .iter()
681 .any(|(mn, mx)| mn.is_some() || mx.is_some())
682 {
683 enc_stats.push((col.column_id, col_minmax));
684 }
685 let page_flags = if enc.is_some() {
686 ColumnPageHeader::PAGE_ENCRYPTED
687 } else {
688 0
689 };
690 dir.push(ColumnPageHeader {
691 column_id: col.column_id,
692 type_id_tag: col.type_id_tag,
693 encoding: col.encoding as u8,
694 flags: page_flags,
695 page_count: col.pages.len() as u32,
696 page_region_offset: region_offset,
697 page_region_len: region_len,
698 page_stats: stats,
699 });
700 }
701
702 let column_dir_offset = buf.len() as u64;
703 let dir_bytes = bincode::serialize(&dir)?;
704 buf.write_all(&dir_bytes)?;
705
706 let index_trailer_offset = match index_trailer {
707 Some(trailer) => {
708 let off = buf.len() as u64;
709 buf.write_all(trailer)?;
710 off
711 }
712 None => 0,
713 };
714 let encryption_descriptor_offset = match &enc {
715 Some(e) => {
716 let off = buf.len() as u64;
717 buf.write_all(&(e.descriptor_bytes.len() as u32).to_le_bytes())?;
718 buf.write_all(&e.descriptor_bytes)?;
719 off
720 }
721 None => 0,
722 };
723 let (encrypted_stats_offset, encrypted_stats_len) = match &enc {
724 Some(e) if !enc_stats.is_empty() => {
725 let plain = bincode::serialize(&enc_stats)?;
726 let nonce = page_nonce(e.nonce_prefix, ENC_STATS_NONCE_COLUMN, ENC_STATS_NONCE_SEQ);
727 let ct = e.cipher.encrypt_page(&nonce, &plain)?;
728 let off = buf.len() as u64;
729 let len = ct.len() as u64;
730 buf.write_all(&ct)?;
731 (off, len)
732 }
733 _ => (0, 0),
734 };
735 let footer_offset = buf.len() as u64;
736
737 let header_flags = if enc.is_some() {
738 spec.flags | RUN_FLAG_ENCRYPTED
739 } else {
740 spec.flags
741 };
742 let header = RunHeader {
743 magic: RUN_MAGIC,
744 format_version: RUN_FORMAT_VERSION,
745 header_layout_version: RUN_HEADER_VERSION,
746 run_id: spec.run_id,
747 content_hash: content_hasher.finalize().into(),
748 schema_id: spec.schema_id,
749 epoch_created: spec.epoch_created,
750 level: spec.level,
751 flags: header_flags,
752 sort_key_column_id: spec.sort_key_column_id,
753 row_count: spec.row_count,
754 min_row_id: spec.min_row_id,
755 max_row_id: spec.max_row_id,
756 column_count: spec.columns.len() as u64,
757 column_dir_offset,
758 index_trailer_offset,
759 encryption_descriptor_offset,
760 footer_offset,
761 encrypted_stats_offset,
762 encrypted_stats_len,
763 };
764 let header_bytes = bincode::serialize(&header)?;
765 if header_bytes.len() > RUN_HEADER_PAD {
766 return Err(MongrelError::InvalidArgument(format!(
767 "run header too large: {} > {RUN_HEADER_PAD}",
768 header_bytes.len()
769 )));
770 }
771 buf[..header_bytes.len()].copy_from_slice(&header_bytes);
772
773 let checksum = Sha256::digest(&buf[..footer_offset as usize]);
774 buf.write_all(&RUN_MAGIC)?;
775 buf.write_all(&footer_offset.to_le_bytes())?;
776 buf.write_all(&checksum)?;
777 if let Some(tag) = compute_run_mac(enc.as_ref(), &header_bytes, &dir_bytes) {
780 buf.write_all(&tag)?;
781 }
782
783 let mut file = OpenOptions::new()
784 .create(true)
785 .write(true)
786 .truncate(true)
787 .open(path)?;
788 file.write_all(&buf)?;
789 file.sync_all()?;
790 Ok(header)
791}
792
793fn page_nonce(nonce_prefix: [u8; 12], column_id: u16, page_seq: u32) -> [u8; 12] {
794 let mut n = nonce_prefix;
795 n[8..10].copy_from_slice(&column_id.to_le_bytes());
796 n[10..12].copy_from_slice(&(page_seq as u16).to_le_bytes());
797 n
798}
799
800fn overlay_encrypted_stats(
807 path: &Path,
808 header: &RunHeader,
809 cipher: &dyn Cipher,
810 nonce_prefix: [u8; 12],
811 dir: &mut [ColumnPageHeader],
812) -> Result<()> {
813 let mut file = File::open(path)?;
814 file.seek(SeekFrom::Start(header.encrypted_stats_offset))?;
815 let mut ct = vec![0u8; header.encrypted_stats_len as usize];
816 file.read_exact(&mut ct)?;
817 let nonce = page_nonce(nonce_prefix, ENC_STATS_NONCE_COLUMN, ENC_STATS_NONCE_SEQ);
818 let plain = cipher.decrypt_page(&nonce, &ct)?;
819 let stats: EncryptedColumnStats = bincode::deserialize(&plain)
820 .map_err(|e| MongrelError::Encryption(format!("bad encrypted page-stats envelope: {e}")))?;
821 for (cid, minmax) in stats {
822 let Some(col) = dir.iter_mut().find(|c| c.column_id == cid) else {
823 continue;
824 };
825 for (stat, (mn, mx)) in col.page_stats.iter_mut().zip(minmax) {
826 stat.min = mn;
827 stat.max = mx;
828 }
829 }
830 Ok(())
831}
832
833pub(crate) fn page_cache_key(
840 table_id: u64,
841 run_id: u128,
842 column_id: u16,
843 page_seq: usize,
844) -> [u8; 32] {
845 let mut h = Sha256::new();
846 h.update(table_id.to_be_bytes());
847 h.update(run_id.to_be_bytes());
848 h.update(column_id.to_be_bytes());
849 h.update((page_seq as u64).to_be_bytes());
850 let out = h.finalize();
851 let mut k = [0u8; 32];
852 k.copy_from_slice(&out);
853 k
854}
855
856fn decrypt_or_passthrough(
860 cipher: Option<&dyn Cipher>,
861 nonce_prefix: [u8; 12],
862 column_id: u16,
863 page_seq: usize,
864 encrypted: bool,
865 buf: &[u8],
866) -> Result<Vec<u8>> {
867 if encrypted {
868 match cipher {
869 Some(c) => {
870 Ok(c.decrypt_page(&page_nonce(nonce_prefix, column_id, page_seq as u32), buf)?)
871 }
872 None => Err(MongrelError::Decryption(
873 "encrypted page but no cipher".into(),
874 )),
875 }
876 } else {
877 Ok(buf.to_vec())
878 }
879}
880
881fn read_header_fast(path: impl AsRef<Path>) -> Result<RunHeader> {
891 let mut file = File::open(path)?;
892 let mut header_buf = vec![0u8; RUN_HEADER_PAD];
893 file.read_exact(&mut header_buf)?;
894 let header: RunHeader = bincode::deserialize(&header_buf)
895 .map_err(|e| MongrelError::InvalidArgument(format!("bad run header: {e}")))?;
896 if header.magic != RUN_MAGIC {
897 return Err(MongrelError::MagicMismatch {
898 what: "sorted run",
899 expected: RUN_MAGIC,
900 got: header.magic,
901 });
902 }
903 file.seek(SeekFrom::Start(header.footer_offset))?;
904 let mut footer_magic = [0u8; 8];
905 file.read_exact(&mut footer_magic)?;
906 if footer_magic != RUN_MAGIC {
907 return Err(MongrelError::MagicMismatch {
908 what: "sorted run footer",
909 expected: RUN_MAGIC,
910 got: footer_magic,
911 });
912 }
913 Ok(header)
914}
915
916fn read_header_cached(
926 path: impl AsRef<Path>,
927 verified_runs: &parking_lot::Mutex<std::collections::HashSet<u128>>,
928) -> Result<RunHeader> {
929 let header = read_header_fast(path.as_ref())?;
930 if verified_runs.lock().contains(&header.run_id) {
931 return Ok(header);
932 }
933 let verified = read_header(path)?;
934 verified_runs.lock().insert(verified.run_id);
935 Ok(verified)
936}
937
938pub fn read_header(path: impl AsRef<Path>) -> Result<RunHeader> {
940 let mut file = File::open(path)?;
941 let mut header_buf = vec![0u8; RUN_HEADER_PAD];
942 file.read_exact(&mut header_buf)?;
943 let header: RunHeader = bincode::deserialize(&header_buf)
944 .map_err(|e| MongrelError::InvalidArgument(format!("bad run header: {e}")))?;
945 if header.magic != RUN_MAGIC {
946 return Err(MongrelError::MagicMismatch {
947 what: "sorted run",
948 expected: RUN_MAGIC,
949 got: header.magic,
950 });
951 }
952
953 file.seek(SeekFrom::Start(header.footer_offset))?;
954 let mut footer = [0u8; 8 + 8 + 32];
955 file.read_exact(&mut footer)?;
956 if footer[..8] != RUN_MAGIC {
957 return Err(MongrelError::MagicMismatch {
958 what: "sorted run footer",
959 expected: RUN_MAGIC,
960 got: footer[..8].try_into().unwrap(),
961 });
962 }
963 let mut hasher = Sha256::new();
964 hasher.update(&header_buf);
965 let body_len = header.footer_offset.saturating_sub(RUN_HEADER_PAD as u64);
966 file.seek(SeekFrom::Start(RUN_HEADER_PAD as u64))?;
967 let mut body = vec![0u8; body_len as usize];
968 file.read_exact(&mut body)?;
969 hasher.update(&body);
970 let computed: [u8; 32] = hasher.finalize().into();
971 let stored: [u8; 32] = footer[16..].try_into().unwrap();
972 if computed != stored {
973 return Err(MongrelError::ChecksumMismatch {
974 expected: u64::from_be_bytes(stored[..8].try_into().unwrap()),
975 actual: u64::from_be_bytes(computed[..8].try_into().unwrap()),
976 context: "sorted run footer".into(),
977 });
978 }
979 Ok(header)
980}
981
982pub fn read_column_dir(
984 path: impl AsRef<Path>,
985 header: &RunHeader,
986) -> Result<Vec<ColumnPageHeader>> {
987 let mut file = File::open(path)?;
988 file.seek(SeekFrom::Start(header.column_dir_offset))?;
989 let end = if header.index_trailer_offset != 0 {
990 header.index_trailer_offset
991 } else {
992 header.footer_offset
993 };
994 let len = end.saturating_sub(header.column_dir_offset);
995 let mut buf = vec![0u8; len as usize];
996 file.read_exact(&mut buf)?;
997 let dir: Vec<ColumnPageHeader> = bincode::deserialize(&buf)
998 .map_err(|e| MongrelError::InvalidArgument(format!("bad column dir: {e}")))?;
999 Ok(dir)
1000}
1001
1002pub(crate) fn read_encryption_descriptor_bytes(
1005 path: impl AsRef<Path>,
1006 header: &RunHeader,
1007) -> Result<Vec<u8>> {
1008 let mut file = File::open(path)?;
1009 file.seek(SeekFrom::Start(header.encryption_descriptor_offset))?;
1010 let mut len_buf = [0u8; 4];
1011 file.read_exact(&mut len_buf)?;
1012 let len = u32::from_le_bytes(len_buf) as usize;
1013 const MAX_DESCRIPTOR_BYTES: usize = 65_536;
1018 if len > MAX_DESCRIPTOR_BYTES {
1019 return Err(MongrelError::InvalidArgument(format!(
1020 "encryption descriptor length {len} exceeds {MAX_DESCRIPTOR_BYTES}"
1021 )));
1022 }
1023 let mut buf = vec![0u8; len];
1024 file.read_exact(&mut buf)?;
1025 Ok(buf)
1026}
1027
1028#[cfg(feature = "encryption")]
1035fn verify_run_mac(
1036 path: &Path,
1037 header: &RunHeader,
1038 dir: &[ColumnPageHeader],
1039 kek: &Kek,
1040 desc_bytes: &[u8],
1041) -> Result<()> {
1042 let header_bytes = bincode::serialize(header)?;
1043 let dir_bytes = bincode::serialize(dir)?;
1044 let mac_key = kek.derive_run_mac_key();
1045 let expected =
1046 crate::encryption::run_metadata_mac(&mac_key, &header_bytes, &dir_bytes, desc_bytes);
1047 let mut file = File::open(path)?;
1048 file.seek(SeekFrom::Start(header.footer_offset + 8 + 8 + 32))?;
1049 let mut tag = [0u8; RUN_MAC_LEN];
1050 file.read_exact(&mut tag).map_err(|_| {
1051 MongrelError::Decryption(
1052 "encrypted run is missing or truncated its metadata MAC; cannot \
1053 authenticate metadata"
1054 .into(),
1055 )
1056 })?;
1057 let mut diff = 0u8;
1059 for (x, y) in tag.iter().zip(expected.iter()) {
1060 diff |= x ^ y;
1061 }
1062 if diff != 0 {
1063 return Err(MongrelError::Decryption(
1064 "run metadata authentication failed — tampered run or wrong key".into(),
1065 ));
1066 }
1067 Ok(())
1068}
1069
1070#[cfg(not(feature = "encryption"))]
1071fn verify_run_mac(
1072 _path: &Path,
1073 _header: &RunHeader,
1074 _dir: &[ColumnPageHeader],
1075 _kek: &Kek,
1076 _desc_bytes: &[u8],
1077) -> Result<()> {
1078 Ok(())
1079}
1080
1081pub struct RunWriter<'a> {
1090 schema: &'a Schema,
1091 run_id: u128,
1092 epoch_created: Epoch,
1093 level: u8,
1094 kek: Option<&'a Kek>,
1095 indexable_columns: Vec<(u16, u8)>,
1098 compress: columnar::Compress,
1102 clean: bool,
1107 uniform_epoch: bool,
1112 le: bool,
1119}
1120
1121impl<'a> RunWriter<'a> {
1122 pub fn new(schema: &'a Schema, run_id: u128, epoch_created: Epoch, level: u8) -> Self {
1123 Self {
1124 schema,
1125 run_id,
1126 epoch_created,
1127 level,
1128 kek: None,
1129 indexable_columns: Vec::new(),
1130 compress: columnar::Compress::Zstd(3),
1131 clean: false,
1132 uniform_epoch: false,
1133 le: false,
1134 }
1135 }
1136
1137 pub fn uniform_epoch(mut self, uniform: bool) -> Self {
1143 self.uniform_epoch = uniform;
1144 self
1145 }
1146
1147 pub fn with_encryption(mut self, kek: &'a Kek, indexable_columns: Vec<(u16, u8)>) -> Self {
1151 self.kek = Some(kek);
1152 self.indexable_columns = indexable_columns;
1153 self
1154 }
1155
1156 pub fn with_zstd_level(mut self, level: i32) -> Self {
1159 self.compress = columnar::Compress::Zstd(level);
1160 self
1161 }
1162
1163 pub fn with_lz4(mut self) -> Self {
1167 self.compress = columnar::Compress::Lz4;
1168 self
1169 }
1170
1171 pub fn with_plain(mut self) -> Self {
1174 self.compress = columnar::Compress::Plain;
1175 self
1176 }
1177
1178 pub fn clean(mut self, clean: bool) -> Self {
1184 self.clean = clean;
1185 self
1186 }
1187
1188 pub fn with_native_endian(mut self) -> Self {
1193 if cfg!(target_endian = "little") {
1194 self.le = true;
1195 }
1196 self
1197 }
1198
1199 pub fn write_native(
1206 self,
1207 path: impl AsRef<Path>,
1208 user_columns: &[(u16, columnar::NativeColumn)],
1209 n: usize,
1210 first_row_id: u64,
1211 ) -> Result<RunHeader> {
1212 use columnar::NativeColumn;
1213 let row_id_col = NativeColumn::int64_sequence(first_row_id as i64, n);
1214 let epoch_col = NativeColumn::int64_constant(self.epoch_created.0 as i64, n);
1215 let deleted_col = NativeColumn::bool_constant(false, n);
1216
1217 let learned_trailer = build_learned_trailer_native(&row_id_col);
1218
1219 let row_ids = match &row_id_col {
1222 NativeColumn::Int64 { data, .. } => data.as_slice(),
1223 _ => &[],
1224 };
1225 let bounds = page_bounds(row_ids);
1226 let compress = self.compress;
1227 let le = self.le;
1228 let plain = matches!(compress, columnar::Compress::Plain);
1229 let row_id_enc = if plain {
1232 Encoding::Plain
1233 } else {
1234 Encoding::Delta
1235 };
1236 let sys_enc = if plain {
1237 Encoding::Plain
1238 } else {
1239 Encoding::Zstd
1240 };
1241
1242 let mut columns: Vec<ColumnPayload> = Vec::with_capacity(3 + self.schema.columns.len());
1243 let (pages, stats) = native_column_pages(
1244 TypeId::Int64,
1245 &row_id_col,
1246 row_id_enc,
1247 compress,
1248 le,
1249 &bounds,
1250 )?;
1251 columns.push(ColumnPayload {
1252 column_id: SYS_ROW_ID,
1253 type_id_tag: type_tag(&TypeId::Int64),
1254 encoding: row_id_enc,
1255 pages,
1256 page_stats: stats,
1257 });
1258 let (pages, stats) =
1259 native_column_pages(TypeId::Int64, &epoch_col, sys_enc, compress, le, &bounds)?;
1260 columns.push(ColumnPayload {
1261 column_id: SYS_EPOCH,
1262 type_id_tag: type_tag(&TypeId::Int64),
1263 encoding: sys_enc,
1264 pages,
1265 page_stats: stats,
1266 });
1267 let (pages, stats) =
1268 native_column_pages(TypeId::Bool, &deleted_col, sys_enc, compress, le, &bounds)?;
1269 columns.push(ColumnPayload {
1270 column_id: SYS_DELETED,
1271 type_id_tag: type_tag(&TypeId::Bool),
1272 encoding: sys_enc,
1273 pages,
1274 page_stats: stats,
1275 });
1276 use rayon::prelude::*;
1282 let user_cols: Vec<ColumnPayload> = self
1283 .schema
1284 .columns
1285 .par_iter()
1286 .map(|cdef| -> Result<ColumnPayload> {
1287 let col = user_columns
1288 .iter()
1289 .find(|(id, _)| *id == cdef.id)
1290 .map(|(_, c)| c)
1291 .ok_or_else(|| {
1292 MongrelError::ColumnNotFound(format!("user column {}", cdef.id))
1293 })?;
1294 let encoding = if plain {
1295 Encoding::Plain
1296 } else {
1297 choose_encoding_native(&cdef.ty, col)
1298 };
1299 let (pages, stats) =
1300 native_column_pages(cdef.ty, col, encoding, compress, le, &bounds)?;
1301 Ok(ColumnPayload {
1302 column_id: cdef.id,
1303 type_id_tag: type_tag(&cdef.ty),
1304 encoding,
1305 pages,
1306 page_stats: stats,
1307 })
1308 })
1309 .collect::<Result<Vec<_>>>()?;
1310 columns.extend(user_cols);
1311
1312 let flags = if self.uniform_epoch {
1318 RUN_FLAG_UNIFORM_EPOCH
1319 } else {
1320 RUN_FLAG_CLEAN
1321 };
1322 let spec = RunSpec {
1323 run_id: self.run_id,
1324 schema_id: self.schema.schema_id,
1325 epoch_created: self.epoch_created.0,
1326 level: self.level,
1327 flags,
1328 sort_key_column_id: SYS_ROW_ID,
1329 row_count: n as u64,
1330 min_row_id: first_row_id,
1331 max_row_id: first_row_id + n as u64 - 1,
1332 columns: &columns,
1333 };
1334 write_run_with(
1335 path,
1336 &spec,
1337 self.kek,
1338 &self.indexable_columns,
1339 Some(&learned_trailer),
1340 )
1341 }
1342
1343 pub fn write(self, path: impl AsRef<Path>, rows: &[Row]) -> Result<RunHeader> {
1344 let n = rows.len();
1345 let mut row_ids = Vec::with_capacity(n);
1347 let mut epochs = Vec::with_capacity(n);
1348 let mut deleted = Vec::with_capacity(n);
1349 for r in rows {
1350 row_ids.push(Value::Int64(r.row_id.0 as i64));
1351 epochs.push(Value::Int64(r.committed_epoch.0 as i64));
1352 deleted.push(Value::Bool(r.deleted));
1353 }
1354 let learned_trailer = build_learned_trailer(&row_ids);
1355 let (min_rid, max_rid) = row_id_bounds(rows);
1356 let row_id_i64: Vec<i64> = rows.iter().map(|r| r.row_id.0 as i64).collect();
1357 let bounds = page_bounds(&row_id_i64);
1358
1359 let mut columns: Vec<ColumnPayload> = Vec::with_capacity(3 + self.schema.columns.len());
1360 let (pages, stats, enc) = value_column_pages(TypeId::Int64, &row_ids, &bounds)?;
1361 columns.push(ColumnPayload {
1362 column_id: SYS_ROW_ID,
1363 type_id_tag: type_tag(&TypeId::Int64),
1364 encoding: enc,
1365 pages,
1366 page_stats: stats,
1367 });
1368 let (pages, stats, enc) = value_column_pages(TypeId::Int64, &epochs, &bounds)?;
1369 columns.push(ColumnPayload {
1370 column_id: SYS_EPOCH,
1371 type_id_tag: type_tag(&TypeId::Int64),
1372 encoding: enc,
1373 pages,
1374 page_stats: stats,
1375 });
1376 let (pages, stats, enc) = value_column_pages(TypeId::Bool, &deleted, &bounds)?;
1377 columns.push(ColumnPayload {
1378 column_id: SYS_DELETED,
1379 type_id_tag: type_tag(&TypeId::Bool),
1380 encoding: enc,
1381 pages,
1382 page_stats: stats,
1383 });
1384 for cdef in &self.schema.columns {
1386 let vals: Vec<Value> = rows
1387 .iter()
1388 .map(|r| r.columns.get(&cdef.id).cloned().unwrap_or(Value::Null))
1389 .collect();
1390 let (pages, stats, encoding) = value_column_pages(cdef.ty, &vals, &bounds)?;
1391 columns.push(ColumnPayload {
1392 column_id: cdef.id,
1393 type_id_tag: type_tag(&cdef.ty),
1394 encoding,
1395 pages,
1396 page_stats: stats,
1397 });
1398 }
1399
1400 let spec = RunSpec {
1401 run_id: self.run_id,
1402 schema_id: self.schema.schema_id,
1403 epoch_created: self.epoch_created.0,
1404 level: self.level,
1405 flags: {
1406 let mut f = if self.clean { RUN_FLAG_CLEAN } else { 0 };
1407 if self.uniform_epoch {
1408 f |= RUN_FLAG_UNIFORM_EPOCH;
1409 }
1410 f
1411 },
1412 sort_key_column_id: SYS_ROW_ID,
1413 row_count: n as u64,
1414 min_row_id: min_rid,
1415 max_row_id: max_rid,
1416 columns: &columns,
1417 };
1418 write_run_with(
1419 path,
1420 &spec,
1421 self.kek,
1422 &self.indexable_columns,
1423 Some(&learned_trailer),
1424 )
1425 }
1426}
1427
1428fn type_tag(ty: &TypeId) -> u16 {
1429 match ty {
1431 TypeId::Bool => 1,
1432 TypeId::Int64 => 8,
1433 TypeId::Float64 => 9,
1434 TypeId::Bytes => 12,
1435 TypeId::Embedding { .. } => 13,
1436 _ => 0,
1437 }
1438}
1439
1440pub(crate) fn be_i64(b: Option<&[u8]>) -> Option<i64> {
1443 let b = b?;
1444 (b.len() == 8).then(|| i64::from_be_bytes(b.try_into().unwrap()))
1445}
1446
1447pub(crate) fn be_f64(b: Option<&[u8]>) -> Option<f64> {
1449 let b = b?;
1450 (b.len() == 8).then(|| f64::from_bits(u64::from_be_bytes(b.try_into().unwrap())))
1451}
1452
1453const PAGE_ROWS: usize = 65_536;
1456
1457fn page_bounds(row_ids: &[i64]) -> Vec<(usize, usize, u64, u64)> {
1460 let n = row_ids.len();
1461 if n == 0 {
1462 return vec![(0, 0, 0, 0)];
1463 }
1464 let mut out = Vec::new();
1465 let mut start = 0;
1466 while start < n {
1467 let end = (start + PAGE_ROWS).min(n);
1468 out.push((start, end, row_ids[start] as u64, row_ids[end - 1] as u64));
1469 start = end;
1470 }
1471 out
1472}
1473
1474fn native_column_pages(
1480 ty: TypeId,
1481 col: &columnar::NativeColumn,
1482 encoding: Encoding,
1483 compress: columnar::Compress,
1484 le: bool,
1485 bounds: &[(usize, usize, u64, u64)],
1486) -> Result<(Vec<Vec<u8>>, Vec<PageStat>)> {
1487 use rayon::prelude::*;
1488 let encode_one =
1489 |&(s, e, frid, lrid): &(usize, usize, u64, u64)| -> Result<(Vec<u8>, PageStat)> {
1490 let chunk = col.slice_range(s, e);
1491 let stat = columnar::page_stat_for(ty, &chunk, frid, lrid);
1492 let page = columnar::encode_page_native(ty, &chunk, encoding, compress, le)?;
1493 Ok((page, stat))
1494 };
1495 let pairs: Vec<(Vec<u8>, PageStat)> = if bounds.len() > 1 {
1497 bounds
1498 .par_iter()
1499 .map(encode_one)
1500 .collect::<Result<Vec<_>>>()?
1501 } else {
1502 bounds.iter().map(encode_one).collect::<Result<Vec<_>>>()?
1503 };
1504 let (pages, stats) = pairs.into_iter().unzip();
1505 Ok((pages, stats))
1506}
1507
1508fn value_column_pages(
1510 ty: TypeId,
1511 vals: &[Value],
1512 bounds: &[(usize, usize, u64, u64)],
1513) -> Result<(Vec<Vec<u8>>, Vec<PageStat>, Encoding)> {
1514 let encoding = choose_encoding(&ty, vals);
1515 let mut pages = Vec::with_capacity(bounds.len());
1516 let mut stats = Vec::with_capacity(bounds.len());
1517 for &(s, e, frid, lrid) in bounds {
1518 let chunk = &vals[s..e];
1519 pages.push(columnar::encode_page(ty, chunk, encoding)?);
1520 let native = columnar::values_to_native(ty, chunk);
1521 stats.push(columnar::page_stat_for(ty, &native, frid, lrid));
1522 }
1523 Ok((pages, stats, encoding))
1524}
1525
1526fn choose_encoding(ty: &TypeId, values: &[Value]) -> Encoding {
1529 use std::collections::HashSet;
1530 if matches!(ty, TypeId::Bytes) {
1531 let n = values.len();
1532 if n > 0 {
1533 let distinct = values
1534 .iter()
1535 .filter(|v| !matches!(v, Value::Null))
1536 .map(|v| v.encode_key())
1537 .collect::<HashSet<_>>()
1538 .len();
1539 if (distinct as f64 / n as f64) < 0.5 {
1540 return Encoding::Dictionary;
1541 }
1542 }
1543 }
1544 Encoding::Zstd
1545}
1546
1547fn choose_encoding_native(ty: &TypeId, col: &columnar::NativeColumn) -> Encoding {
1550 use std::collections::HashSet;
1551 match (ty, col) {
1552 (TypeId::Int64 | TypeId::TimestampNanos, columnar::NativeColumn::Int64 { data, .. }) => {
1553 if data.windows(2).all(|w| w[0] <= w[1]) {
1554 Encoding::Delta
1555 } else {
1556 Encoding::Zstd
1557 }
1558 }
1559 (
1560 TypeId::Bytes,
1561 columnar::NativeColumn::Bytes {
1562 offsets, values, ..
1563 },
1564 ) => {
1565 let n = offsets.len().saturating_sub(1);
1566 if n > 0 {
1567 let distinct: HashSet<&[u8]> = (0..n)
1568 .map(|i| &values[offsets[i] as usize..offsets[i + 1] as usize])
1569 .collect();
1570 if (distinct.len() as f64 / n as f64) < 0.5 {
1571 return Encoding::Dictionary;
1572 }
1573 }
1574 Encoding::Zstd
1575 }
1576 _ => Encoding::Zstd,
1577 }
1578}
1579
1580fn build_learned_trailer_native(col: &columnar::NativeColumn) -> Vec<u8> {
1582 let points: Vec<(u64, usize)> = match col {
1583 columnar::NativeColumn::Int64 { data, .. } => data
1584 .iter()
1585 .enumerate()
1586 .map(|(i, v)| (*v as u64, i))
1587 .collect(),
1588 _ => Vec::new(),
1589 };
1590 let pgm = PgmIndex::build(&points, LEARNED_EPSILON);
1591 bincode::serialize(&pgm).expect("pgm serialize")
1592}
1593
1594fn row_id_bounds(rows: &[Row]) -> (u64, u64) {
1595 match (rows.first(), rows.last()) {
1596 (Some(f), Some(l)) => (f.row_id.0, l.row_id.0),
1597 _ => (0, 0),
1598 }
1599}
1600
1601pub struct RunReader {
1607 file: File,
1608 mmap: Option<memmap2::Mmap>,
1609 header: RunHeader,
1610 dir: Vec<ColumnPageHeader>,
1611 schema: Schema,
1612 table_id: u64,
1614 cipher: Option<Box<dyn Cipher>>,
1616 nonce_prefix: [u8; 12],
1618 col_cache: HashMap<u16, Vec<Value>>,
1619 page_cache: Option<Arc<crate::cache::Sharded<crate::cache::PageCache>>>,
1623 decoded_cache: Option<Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>>,
1627 epoch_override: Option<Epoch>,
1631}
1632
1633impl RunReader {
1634 pub fn open(path: impl AsRef<Path>, schema: Schema, kek: Option<Arc<Kek>>) -> Result<Self> {
1635 Self::open_with_cache(path, schema, kek, None, None, 0, None)
1636 }
1637
1638 #[allow(clippy::too_many_arguments)]
1639 pub(crate) fn open_with_cache(
1640 path: impl AsRef<Path>,
1641 schema: Schema,
1642 kek: Option<Arc<Kek>>,
1643 page_cache: Option<Arc<crate::cache::Sharded<crate::cache::PageCache>>>,
1644 decoded_cache: Option<Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>>,
1645 table_id: u64,
1646 verified_runs: Option<&parking_lot::Mutex<std::collections::HashSet<u128>>>,
1647 ) -> Result<Self> {
1648 let path = path.as_ref().to_path_buf();
1649 let header = match verified_runs {
1650 Some(cache) => read_header_cached(&path, cache)?,
1651 None => read_header(&path)?,
1652 };
1653 let mut dir = read_column_dir(&path, &header)?;
1654 let (cipher, nonce_prefix): (Option<Box<dyn Cipher>>, [u8; 12]) = if header.is_encrypted() {
1657 let kek = kek.as_ref().ok_or_else(|| {
1658 MongrelError::Encryption(
1659 "run is encrypted but no key-encryption key was provided".into(),
1660 )
1661 })?;
1662 if header.encryption_descriptor_offset == 0 {
1663 return Err(MongrelError::Encryption(
1664 "encrypted run has no encryption descriptor".into(),
1665 ));
1666 }
1667 let desc_bytes = read_encryption_descriptor_bytes(&path, &header)?;
1668 verify_run_mac(&path, &header, &dir, kek, &desc_bytes)?;
1674 let enc = crate::encryption::build_run_cipher(kek, &desc_bytes)?;
1675 if header.encrypted_stats_offset != 0 {
1679 overlay_encrypted_stats(
1680 &path,
1681 &header,
1682 enc.cipher.as_ref(),
1683 enc.nonce_prefix,
1684 &mut dir,
1685 )?;
1686 }
1687 (Some(enc.cipher), enc.nonce_prefix)
1688 } else {
1689 (None, [0u8; 12])
1690 };
1691 let file = File::open(&path)?;
1694 let mmap = unsafe { memmap2::MmapOptions::new().map(&file) }.ok();
1701 let _ = path;
1702 Ok(Self {
1703 file,
1704 mmap,
1705 header,
1706 dir,
1707 schema,
1708 table_id,
1709 cipher,
1710 nonce_prefix,
1711 col_cache: HashMap::new(),
1712 epoch_override: None,
1713 page_cache,
1714 decoded_cache,
1715 })
1716 }
1717
1718 pub fn header(&self) -> &RunHeader {
1719 &self.header
1720 }
1721
1722 pub(crate) fn set_uniform_epoch(&mut self, epoch: Epoch) {
1726 if self.header.is_uniform_epoch() {
1727 self.epoch_override = Some(epoch);
1728 self.col_cache.remove(&SYS_EPOCH);
1730 }
1731 }
1732
1733 pub fn is_clean(&self) -> bool {
1736 self.header.is_clean()
1737 }
1738
1739 pub fn row_count(&self) -> usize {
1740 self.header.row_count as usize
1741 }
1742
1743 pub(crate) fn clean_contiguous_row_ids(&self) -> bool {
1744 let n = self.row_count();
1745 n > 0
1746 && self.is_clean()
1747 && self.epoch_override.is_none()
1748 && self.header.max_row_id >= self.header.min_row_id
1749 && self
1750 .header
1751 .max_row_id
1752 .checked_sub(self.header.min_row_id)
1753 .and_then(|span| span.checked_add(1))
1754 == Some(n as u64)
1755 }
1756
1757 pub(crate) fn position_for_row_id_fast(&self, row_id: u64) -> Option<usize> {
1758 if !self.clean_contiguous_row_ids()
1759 || row_id < self.header.min_row_id
1760 || row_id > self.header.max_row_id
1761 {
1762 return None;
1763 }
1764 Some((row_id - self.header.min_row_id) as usize)
1765 }
1766
1767 pub(crate) fn positions_for_row_ids_fast(&self, row_ids: &[u64]) -> Option<Vec<usize>> {
1768 if !self.clean_contiguous_row_ids() {
1769 return None;
1770 }
1771 let mut positions = Vec::with_capacity(row_ids.len());
1772 for &row_id in row_ids {
1773 if let Some(pos) = self.position_for_row_id_fast(row_id) {
1774 positions.push(pos);
1775 }
1776 }
1777 positions.sort_unstable();
1778 Some(positions)
1779 }
1780
1781 fn resolve_type(&self, column_id: u16) -> TypeId {
1782 match column_id {
1783 SYS_ROW_ID | SYS_EPOCH => TypeId::Int64,
1784 SYS_DELETED => TypeId::Bool,
1785 _ => self
1786 .schema
1787 .columns
1788 .iter()
1789 .find(|c| c.id == column_id)
1790 .map(|c| c.ty)
1791 .unwrap_or(TypeId::Bytes),
1792 }
1793 }
1794
1795 fn find_header(&self, column_id: u16) -> Result<&ColumnPageHeader> {
1796 self.dir
1797 .iter()
1798 .find(|h| h.column_id == column_id)
1799 .ok_or_else(|| MongrelError::ColumnNotFound(format!("column {column_id}")))
1800 }
1801
1802 fn col_encrypted(&self, column_id: u16) -> bool {
1808 self.dir
1809 .iter()
1810 .find(|h| h.column_id == column_id)
1811 .map(|h| h.flags & ColumnPageHeader::PAGE_ENCRYPTED != 0)
1812 .unwrap_or(false)
1813 }
1814
1815 pub(crate) fn read_page(&mut self, column_id: u16, page_seq: usize) -> Result<Vec<u8>> {
1816 let (offset, compressed_len, encrypted) = {
1817 let ch = self.find_header(column_id)?;
1818 let stat = ch
1819 .page_stats
1820 .get(page_seq)
1821 .ok_or_else(|| MongrelError::InvalidArgument("page seq out of range".into()))?;
1822 (
1823 stat.offset,
1824 stat.compressed_len,
1825 ch.flags & ColumnPageHeader::PAGE_ENCRYPTED != 0,
1826 )
1827 };
1828 let key = page_cache_key(self.table_id, self.header.run_id, column_id, page_seq);
1831 if let Some(cache) = &self.page_cache {
1832 if let Some(bytes) = cache.lock(&key).get(
1833 &key,
1834 crate::epoch::Snapshot::at(crate::epoch::Epoch(u64::MAX)),
1835 ) {
1836 return decrypt_or_passthrough(
1837 self.cipher.as_deref(),
1838 self.nonce_prefix,
1839 column_id,
1840 page_seq,
1841 encrypted,
1842 &bytes,
1843 );
1844 }
1845 }
1846 let buf = match &self.mmap {
1847 Some(m) => m[offset as usize..(offset + compressed_len as u64) as usize].to_vec(),
1850 None => {
1851 self.file.seek(SeekFrom::Start(offset))?;
1852 let mut buf = vec![0u8; compressed_len as usize];
1853 self.file.read_exact(&mut buf)?;
1854 buf
1855 }
1856 };
1857 if let Some(cache) = &self.page_cache {
1859 cache.lock(&key).insert(crate::page::CachedPage {
1860 committed_epoch: crate::epoch::Epoch(self.header.epoch_created),
1861 content_hash: key,
1862 bytes: bytes::Bytes::copy_from_slice(&buf),
1863 });
1864 }
1865 decrypt_or_passthrough(
1866 self.cipher.as_deref(),
1867 self.nonce_prefix,
1868 column_id,
1869 page_seq,
1870 encrypted,
1871 &buf,
1872 )
1873 }
1874
1875 fn read_page_shared(&self, column_id: u16, page_seq: usize) -> Result<Vec<u8>> {
1880 let ch = self.find_header(column_id)?;
1881 let stat = ch
1882 .page_stats
1883 .get(page_seq)
1884 .ok_or_else(|| MongrelError::InvalidArgument("page seq out of range".into()))?;
1885 let encrypted = ch.flags & ColumnPageHeader::PAGE_ENCRYPTED != 0;
1886 let key = page_cache_key(self.table_id, self.header.run_id, column_id, page_seq);
1889 if let Some(cache) = &self.page_cache {
1890 if let Some(guard) = cache.try_lock(&key) {
1891 if let Some(bytes) = guard.try_get(
1892 &key,
1893 crate::epoch::Snapshot::at(crate::epoch::Epoch(u64::MAX)),
1894 ) {
1895 return decrypt_or_passthrough(
1896 self.cipher.as_deref(),
1897 self.nonce_prefix,
1898 column_id,
1899 page_seq,
1900 encrypted,
1901 &bytes,
1902 );
1903 }
1904 }
1905 }
1906 let mmap = self.mmap.as_ref().ok_or_else(|| {
1907 MongrelError::InvalidArgument("parallel page decode requires an mmap-backed run".into())
1908 })?;
1909 let start = stat.offset as usize;
1910 let end = (stat.offset + stat.compressed_len as u64) as usize;
1911 let buf = mmap[start..end].to_vec();
1912 if let Some(cache) = &self.page_cache {
1916 if let Some(mut guard) = cache.try_lock(&key) {
1917 guard.insert(crate::page::CachedPage {
1918 committed_epoch: crate::epoch::Epoch(self.header.epoch_created),
1919 content_hash: key,
1920 bytes: bytes::Bytes::copy_from_slice(&buf),
1921 });
1922 }
1923 }
1924 decrypt_or_passthrough(
1925 self.cipher.as_deref(),
1926 self.nonce_prefix,
1927 column_id,
1928 page_seq,
1929 encrypted,
1930 &buf,
1931 )
1932 }
1933
1934 fn column(&mut self, column_id: u16) -> Result<&[Value]> {
1936 if column_id == SYS_EPOCH {
1939 if let Some(ov) = self.epoch_override {
1940 if !self.col_cache.contains_key(&column_id) {
1941 let n = self.row_count();
1942 self.col_cache
1943 .insert(column_id, vec![Value::Int64(ov.0 as i64); n]);
1944 }
1945 return Ok(self.col_cache.get(&column_id).unwrap().as_slice());
1946 }
1947 }
1948 if !self.col_cache.contains_key(&column_id) {
1949 let ty = self.resolve_type(column_id);
1950 let page_rows: Vec<usize> = {
1951 let ch = self.find_header(column_id)?;
1952 ch.page_stats.iter().map(|s| s.row_count as usize).collect()
1953 };
1954 let mut decoded: Vec<Value> = Vec::with_capacity(self.row_count());
1955 for (seq, &pr) in page_rows.iter().enumerate() {
1956 let page = self.read_page(column_id, seq)?;
1957 decoded.extend(columnar::decode_page(ty, &page, pr)?);
1958 }
1959 self.col_cache.insert(column_id, decoded);
1960 }
1961 Ok(self.col_cache.get(&column_id).unwrap().as_slice())
1962 }
1963
1964 pub fn get_version(&mut self, row_id: RowId, snapshot: Epoch) -> Result<Option<(Epoch, Row)>> {
1978 match self.find_version_page(row_id, snapshot)? {
1979 None => Ok(None),
1980 Some((epoch, seq, local_index)) => Ok(Some((
1981 Epoch(epoch),
1982 self.materialize_in_page(seq, local_index)?,
1983 ))),
1984 }
1985 }
1986
1987 fn find_version_page(
1993 &mut self,
1994 row_id: RowId,
1995 snapshot: Epoch,
1996 ) -> Result<Option<(u64, usize, usize)>> {
1997 let n = self.row_count();
1998 if n == 0 {
1999 return Ok(None);
2000 }
2001 let target = row_id.0 as i64;
2002 let ch = self.find_header(SYS_ROW_ID)?;
2003 let mut page_start = 0u64;
2004 let candidate_pages: Vec<(usize, u64)> = ch .page_stats
2006 .iter()
2007 .enumerate()
2008 .filter_map(|(seq, s)| {
2009 let start = page_start;
2010 page_start += s.row_count as u64;
2011 (s.first_row_id <= row_id.0 && row_id.0 <= s.last_row_id).then_some((seq, start))
2012 })
2013 .collect();
2014 if candidate_pages.is_empty() {
2015 return Ok(None);
2016 }
2017 let ty = self.resolve_type(SYS_ROW_ID);
2018 let mut best: Option<(u64, usize, usize)> = None; for (seq, _page_row_start) in candidate_pages {
2020 let page_rows = self.find_header(SYS_ROW_ID)?.page_stats[seq].row_count as usize;
2021 let row_ids = match self.decode_page_native_cached(ty, SYS_ROW_ID, seq, page_rows)? {
2022 columnar::NativeColumn::Int64 { data, .. } => data,
2023 _ => return Err(MongrelError::InvalidArgument("sys row_id not int64".into())),
2024 };
2025 let local = match row_ids.binary_search(&target) {
2026 Ok(i) => i,
2027 Err(_) => continue,
2028 };
2029 let epoch_ty = self.resolve_type(SYS_EPOCH);
2030 let epochs =
2031 match self.decode_page_native_cached(epoch_ty, SYS_EPOCH, seq, page_rows)? {
2032 columnar::NativeColumn::Int64 { data, .. } => data,
2033 _ => return Err(MongrelError::InvalidArgument("sys epoch not int64".into())),
2034 };
2035 let mut lo = local;
2038 while lo > 0 && row_ids[lo - 1] == target {
2039 lo -= 1;
2040 }
2041 let mut hi = local;
2042 while hi + 1 < row_ids.len() && row_ids[hi + 1] == target {
2043 hi += 1;
2044 }
2045 for (i, &epoch) in epochs[lo..=hi].iter().enumerate() {
2046 let epoch = epoch as u64;
2047 if epoch <= snapshot.0 && best.map(|(be, ..)| epoch > be).unwrap_or(true) {
2048 best = Some((epoch, seq, lo + i));
2049 }
2050 }
2051 }
2052 Ok(best)
2053 }
2054
2055 pub fn get_version_column(
2063 &mut self,
2064 row_id: RowId,
2065 snapshot: Epoch,
2066 column_id: u16,
2067 ) -> Result<Option<(Epoch, bool, Option<Value>)>> {
2068 let Some((epoch, seq, local_index)) = self.find_version_page(row_id, snapshot)? else {
2069 return Ok(None);
2070 };
2071 let page_rows = self.find_header(SYS_ROW_ID)?.page_stats[seq].row_count as usize;
2072 let page_start: usize = self.find_header(SYS_ROW_ID)?.page_stats[..seq]
2073 .iter()
2074 .map(|s| s.row_count as usize)
2075 .sum();
2076 let global_index = page_start + local_index;
2077 let native_at = |slf: &mut Self, cid: u16| -> Result<Option<Value>> {
2078 if !slf.dir.iter().any(|h| h.column_id == cid) {
2079 return Ok(None);
2080 }
2081 let ty = slf.resolve_type(cid);
2082 if !matches!(
2083 ty,
2084 TypeId::Bool
2085 | TypeId::Int8
2086 | TypeId::Int16
2087 | TypeId::Int32
2088 | TypeId::Int64
2089 | TypeId::UInt8
2090 | TypeId::UInt16
2091 | TypeId::UInt32
2092 | TypeId::UInt64
2093 | TypeId::Float32
2094 | TypeId::Float64
2095 | TypeId::TimestampNanos
2096 | TypeId::Date32
2097 | TypeId::Bytes
2098 ) {
2099 return Ok(slf.column(cid)?.get(global_index).cloned());
2100 }
2101 Ok(slf
2102 .decode_page_native_cached(ty, cid, seq, page_rows)?
2103 .value_at(local_index))
2104 };
2105 let deleted = matches!(native_at(self, SYS_DELETED)?, Some(Value::Bool(true)));
2106 let value = native_at(self, column_id)?;
2107 Ok(Some((Epoch(epoch), deleted, value)))
2108 }
2109
2110 fn materialize_in_page(&mut self, seq: usize, local_index: usize) -> Result<Row> {
2119 let page_rows = self.find_header(SYS_ROW_ID)?.page_stats[seq].row_count as usize;
2120 let page_start: usize = self.find_header(SYS_ROW_ID)?.page_stats[..seq]
2121 .iter()
2122 .map(|s| s.row_count as usize)
2123 .sum();
2124 let global_index = page_start + local_index;
2125 let native_at = |slf: &mut Self, column_id: u16| -> Result<Option<Value>> {
2126 if !slf.dir.iter().any(|h| h.column_id == column_id) {
2129 return Ok(None);
2130 }
2131 let ty = slf.resolve_type(column_id);
2132 if !matches!(
2133 ty,
2134 TypeId::Bool
2135 | TypeId::Int8
2136 | TypeId::Int16
2137 | TypeId::Int32
2138 | TypeId::Int64
2139 | TypeId::UInt8
2140 | TypeId::UInt16
2141 | TypeId::UInt32
2142 | TypeId::UInt64
2143 | TypeId::Float32
2144 | TypeId::Float64
2145 | TypeId::TimestampNanos
2146 | TypeId::Date32
2147 | TypeId::Bytes
2148 ) {
2149 return Ok(slf.column(column_id)?.get(global_index).cloned());
2152 }
2153 Ok(slf
2154 .decode_page_native_cached(ty, column_id, seq, page_rows)?
2155 .value_at(local_index))
2156 };
2157 let row_id = RowId(match native_at(self, SYS_ROW_ID)? {
2158 Some(Value::Int64(x)) => x as u64,
2159 _ => 0,
2160 });
2161 let committed_epoch = Epoch(match native_at(self, SYS_EPOCH)? {
2162 Some(Value::Int64(x)) => x as u64,
2163 _ => 0,
2164 });
2165 let deleted = matches!(native_at(self, SYS_DELETED)?, Some(Value::Bool(true)));
2166 let col_ids: Vec<u16> = self.schema.columns.iter().map(|c| c.id).collect();
2167 let mut columns = HashMap::new();
2168 for id in col_ids {
2169 columns.insert(id, native_at(self, id)?.unwrap_or(Value::Null));
2170 }
2171 Ok(Row {
2172 row_id,
2173 committed_epoch,
2174 columns,
2175 deleted,
2176 })
2177 }
2178
2179 pub fn all_rows(&mut self) -> Result<Vec<Row>> {
2182 let n = self.row_count();
2183 let mut out = Vec::with_capacity(n);
2184 for i in 0..n {
2185 out.push(self.materialize(i)?);
2186 }
2187 Ok(out)
2188 }
2189
2190 pub fn visible_indices(&mut self, snapshot: Epoch) -> Result<Vec<usize>> {
2196 let n = self.row_count();
2197 if n == 0 {
2198 return Ok(Vec::new());
2199 }
2200 let row_ids = self.column(SYS_ROW_ID)?.to_vec();
2201 let epochs = self.column(SYS_EPOCH)?.to_vec();
2202 let deleted = self.column(SYS_DELETED)?.to_vec();
2203 let mut best: HashMap<u64, (u64, usize)> = HashMap::new();
2204 for i in 0..n {
2205 let rid = int_at(&row_ids, i);
2206 let e = int_at(&epochs, i);
2207 if e > snapshot.0 {
2208 continue;
2209 }
2210 best.entry(rid)
2211 .and_modify(|(be, bi)| {
2212 if e > *be {
2213 *be = e;
2214 *bi = i;
2215 }
2216 })
2217 .or_insert((e, i));
2218 }
2219 let mut idxs: Vec<usize> = best.into_values().map(|(_, i)| i).collect();
2220 idxs.retain(|&i| !bool_at(&deleted, i));
2221 idxs.sort_unstable();
2222 Ok(idxs)
2223 }
2224
2225 pub fn gather_column(&mut self, column_id: u16, indices: &[usize]) -> Result<Vec<Value>> {
2230 if !self.dir.iter().any(|h| h.column_id == column_id) {
2231 return Ok(vec![Value::Null; indices.len()]);
2232 }
2233 let col = self.column(column_id)?;
2234 Ok(indices
2235 .iter()
2236 .map(|&i| col.get(i).cloned().unwrap_or(Value::Null))
2237 .collect())
2238 }
2239
2240 pub fn column_native(&mut self, column_id: u16) -> Result<columnar::NativeColumn> {
2245 use rayon::prelude::*;
2246 if column_id == SYS_EPOCH {
2247 if let Some(ov) = self.epoch_override {
2248 return Ok(columnar::NativeColumn::int64_constant(
2249 ov.0 as i64,
2250 self.row_count(),
2251 ));
2252 }
2253 }
2254 let ty = self.resolve_type(column_id);
2255 let n = self.row_count();
2256 let Some(ch) = self.dir.iter().find(|h| h.column_id == column_id) else {
2257 return Ok(columnar::null_native(ty, n));
2258 };
2259 let page_count = ch.page_count as usize;
2260 let page_rows: Vec<usize> = ch.page_stats.iter().map(|s| s.row_count as usize).collect();
2261 if page_count == 0 {
2262 return Ok(columnar::null_native(ty, n));
2263 }
2264 let parts: Vec<columnar::NativeColumn> = if self.mmap.is_some() && page_count > 1 {
2265 let reader: &RunReader = self;
2269 (0..page_count)
2270 .into_par_iter()
2271 .map(|seq| {
2272 let raw = reader.read_page_shared(column_id, seq)?;
2273 columnar::decode_page_native(ty, &raw, page_rows[seq])
2274 })
2275 .collect::<Result<Vec<_>>>()?
2276 } else {
2277 let mut out = Vec::with_capacity(page_count);
2278 for (seq, &pr) in page_rows.iter().enumerate() {
2279 let page = self.read_page(column_id, seq)?;
2280 out.push(columnar::decode_page_native(ty, &page, pr)?);
2281 }
2282 out
2283 };
2284 Ok(columnar::NativeColumn::concat(&parts))
2285 }
2286
2287 pub fn has_mmap(&self) -> bool {
2291 self.mmap.is_some()
2292 }
2293
2294 pub fn column_native_shared(&self, column_id: u16) -> Result<columnar::NativeColumn> {
2301 use rayon::prelude::*;
2302 if column_id == SYS_EPOCH {
2303 if let Some(ov) = self.epoch_override {
2304 return Ok(columnar::NativeColumn::int64_constant(
2305 ov.0 as i64,
2306 self.row_count(),
2307 ));
2308 }
2309 }
2310 let ty = self.resolve_type(column_id);
2311 let n = self.row_count();
2312 let Some(ch) = self.dir.iter().find(|h| h.column_id == column_id) else {
2313 return Ok(columnar::null_native(ty, n));
2314 };
2315 let page_count = ch.page_count as usize;
2316 let page_rows: Vec<usize> = ch.page_stats.iter().map(|s| s.row_count as usize).collect();
2317 if page_count == 0 {
2318 return Ok(columnar::null_native(ty, n));
2319 }
2320 #[cfg(unix)]
2324 {
2325 if let (Some(m), Some(first)) = (&self.mmap, ch.page_stats.first()) {
2326 let start = first.offset as usize;
2327 let end = (ch.page_region_offset as usize) + (ch.page_region_len as usize);
2328 if end > start {
2329 let _ = m.advise_range(memmap2::Advice::WillNeed, start, end - start);
2330 }
2331 }
2332 }
2333 let run_id = self.header.run_id;
2334 let mut parts_keys: Vec<(columnar::NativeColumn, Option<[u8; 32]>)> = if page_count > 1 {
2338 (0..page_count)
2339 .into_par_iter()
2340 .map(|seq| self.decode_page_cached(ty, column_id, seq, page_rows[seq], run_id))
2341 .collect::<Result<Vec<_>>>()?
2342 } else {
2343 vec![self.decode_page_cached(ty, column_id, 0, page_rows[0], run_id)?]
2344 };
2345 if let Some(cache) = &self.decoded_cache {
2348 for (col, key) in parts_keys.iter_mut() {
2349 if let Some(k) = key.take() {
2350 cache.lock(&k).insert(k, Arc::new(col.clone()));
2351 }
2352 }
2353 }
2354 let parts: Vec<columnar::NativeColumn> = parts_keys.into_iter().map(|(c, _)| c).collect();
2355 Ok(columnar::NativeColumn::concat(&parts))
2356 }
2357
2358 fn decode_page_cached(
2363 &self,
2364 ty: TypeId,
2365 column_id: u16,
2366 seq: usize,
2367 nrows: usize,
2368 run_id: u128,
2369 ) -> Result<(columnar::NativeColumn, Option<[u8; 32]>)> {
2370 let key = page_cache_key(self.table_id, run_id, column_id, seq);
2371 if let Some(cache) = &self.decoded_cache {
2372 if let Some(g) = cache.try_lock(&key) {
2373 if let Some(hit) = g.try_get(&key) {
2374 return Ok(((*hit).clone(), None));
2375 }
2376 }
2377 }
2378 let raw = self.read_page_shared(column_id, seq)?;
2379 let col = columnar::decode_page_native(ty, &raw, nrows)?;
2380 Ok((col, Some(key)))
2381 }
2382
2383 fn decode_page_native_cached(
2394 &mut self,
2395 ty: TypeId,
2396 column_id: u16,
2397 seq: usize,
2398 nrows: usize,
2399 ) -> Result<columnar::NativeColumn> {
2400 let key = page_cache_key(self.table_id, self.header.run_id, column_id, seq);
2401 if let Some(cache) = &self.decoded_cache {
2402 if let Some(hit) = cache.lock(&key).try_get(&key) {
2403 return Ok((*hit).clone());
2404 }
2405 }
2406 let raw = self.read_page(column_id, seq)?;
2407 let col = columnar::decode_page_native(ty, &raw, nrows)?;
2408 if let Some(cache) = &self.decoded_cache {
2409 cache
2410 .lock(&key)
2411 .insert(key, std::sync::Arc::new(col.clone()));
2412 }
2413 Ok(col)
2414 }
2415
2416 pub fn range_row_ids_i64(
2421 &mut self,
2422 column_id: u16,
2423 lo: i64,
2424 hi: i64,
2425 ) -> Result<std::collections::HashSet<u64>> {
2426 Ok(self
2427 .range_row_id_set_i64(column_id, lo, hi)?
2428 .into_sorted_vec()
2429 .into_iter()
2430 .collect())
2431 }
2432
2433 pub(crate) fn range_row_id_set_i64(
2434 &mut self,
2435 column_id: u16,
2436 lo: i64,
2437 hi: i64,
2438 ) -> Result<RowIdSet> {
2439 let info: Vec<(Option<i64>, Option<i64>, usize)> =
2440 match self.dir.iter().find(|h| h.column_id == column_id) {
2441 Some(ch) => ch
2442 .page_stats
2443 .iter()
2444 .map(|s| {
2445 (
2446 be_i64(s.min.as_deref()),
2447 be_i64(s.max.as_deref()),
2448 s.row_count as usize,
2449 )
2450 })
2451 .collect(),
2452 None => return Ok(RowIdSet::empty()),
2453 };
2454 let stats_pruneable =
2460 !self.col_encrypted(column_id) || self.header.encrypted_stats_offset != 0;
2461 let clean_contiguous = self.clean_contiguous_row_ids();
2462 let mut out = Vec::new();
2463 let mut page_start = 0usize;
2464 for (seq, (mn, mx, nrows)) in info.into_iter().enumerate() {
2465 let current_page_start = page_start;
2466 page_start += nrows;
2467 let skip = stats_pruneable
2469 && match (mn, mx) {
2470 (Some(mn), Some(mx)) => mx < lo || mn > hi,
2471 _ => true,
2472 };
2473 if skip {
2474 continue;
2475 }
2476 let val_page = self.read_page(column_id, seq)?;
2477 let vals =
2478 columnar::decode_page_native(self.resolve_type(column_id), &val_page, nrows)?;
2479 if let columnar::NativeColumn::Int64 { data: v, validity } = vals {
2480 if clean_contiguous {
2481 for (i, val) in v.iter().enumerate() {
2482 if columnar::validity_bit(&validity, i) && *val >= lo && *val <= hi {
2483 out.push(self.header.min_row_id + current_page_start as u64 + i as u64);
2484 }
2485 }
2486 } else {
2487 let rid_page = self.read_page(SYS_ROW_ID, seq)?;
2488 let rids = columnar::decode_page_native(TypeId::Int64, &rid_page, nrows)?;
2489 if let columnar::NativeColumn::Int64 { data: r, .. } = rids {
2490 for (i, val) in v.iter().enumerate() {
2491 if columnar::validity_bit(&validity, i) && *val >= lo && *val <= hi {
2492 out.push(r[i] as u64);
2493 }
2494 }
2495 }
2496 }
2497 }
2498 }
2499 Ok(RowIdSet::from_unsorted(out))
2500 }
2501
2502 pub fn range_row_ids_f64(
2505 &mut self,
2506 column_id: u16,
2507 lo: f64,
2508 lo_inclusive: bool,
2509 hi: f64,
2510 hi_inclusive: bool,
2511 ) -> Result<std::collections::HashSet<u64>> {
2512 Ok(self
2513 .range_row_id_set_f64(column_id, lo, lo_inclusive, hi, hi_inclusive)?
2514 .into_sorted_vec()
2515 .into_iter()
2516 .collect())
2517 }
2518
2519 pub(crate) fn range_row_id_set_f64(
2520 &mut self,
2521 column_id: u16,
2522 lo: f64,
2523 lo_inclusive: bool,
2524 hi: f64,
2525 hi_inclusive: bool,
2526 ) -> Result<RowIdSet> {
2527 let info: Vec<(Option<f64>, Option<f64>, usize)> =
2528 match self.dir.iter().find(|h| h.column_id == column_id) {
2529 Some(ch) => ch
2530 .page_stats
2531 .iter()
2532 .map(|s| {
2533 (
2534 be_f64(s.min.as_deref()),
2535 be_f64(s.max.as_deref()),
2536 s.row_count as usize,
2537 )
2538 })
2539 .collect(),
2540 None => return Ok(RowIdSet::empty()),
2541 };
2542 let stats_pruneable =
2548 !self.col_encrypted(column_id) || self.header.encrypted_stats_offset != 0;
2549 let clean_contiguous = self.clean_contiguous_row_ids();
2550 let mut out = Vec::new();
2551 let mut page_start = 0usize;
2552 for (seq, (mn, mx, nrows)) in info.into_iter().enumerate() {
2553 let current_page_start = page_start;
2554 page_start += nrows;
2555 let skip = stats_pruneable
2558 && match (mn, mx) {
2559 (Some(mn), Some(mx)) => {
2560 let skip_lo = mx < lo || (!lo_inclusive && mx == lo);
2561 let skip_hi = mn > hi || (!hi_inclusive && mn == hi);
2562 skip_lo || skip_hi
2563 }
2564 _ => true,
2565 };
2566 if skip {
2567 continue;
2568 }
2569 let val_page = self.read_page(column_id, seq)?;
2570 let vals = columnar::decode_page_native(TypeId::Float64, &val_page, nrows)?;
2571 if let columnar::NativeColumn::Float64 { data: v, validity } = vals {
2572 if clean_contiguous {
2573 for (i, val) in v.iter().enumerate() {
2574 if !columnar::validity_bit(&validity, i) || val.is_nan() {
2575 continue;
2576 }
2577 let ok_lo = if lo_inclusive { *val >= lo } else { *val > lo };
2578 let ok_hi = if hi_inclusive { *val <= hi } else { *val < hi };
2579 if ok_lo && ok_hi {
2580 out.push(self.header.min_row_id + current_page_start as u64 + i as u64);
2581 }
2582 }
2583 } else {
2584 let rid_page = self.read_page(SYS_ROW_ID, seq)?;
2585 let rids = columnar::decode_page_native(TypeId::Int64, &rid_page, nrows)?;
2586 if let columnar::NativeColumn::Int64 { data: r, .. } = rids {
2587 for (i, val) in v.iter().enumerate() {
2588 if !columnar::validity_bit(&validity, i) || val.is_nan() {
2589 continue;
2590 }
2591 let ok_lo = if lo_inclusive { *val >= lo } else { *val > lo };
2592 let ok_hi = if hi_inclusive { *val <= hi } else { *val < hi };
2593 if ok_lo && ok_hi {
2594 out.push(r[i] as u64);
2595 }
2596 }
2597 }
2598 }
2599 }
2600 }
2601 Ok(RowIdSet::from_unsorted(out))
2602 }
2603
2604 pub(crate) fn null_row_id_set(&mut self, column_id: u16, want_nulls: bool) -> Result<RowIdSet> {
2609 let stats: Vec<(usize, usize)> = match self.dir.iter().find(|h| h.column_id == column_id) {
2610 Some(ch) => ch
2611 .page_stats
2612 .iter()
2613 .map(|s| (s.null_count as usize, s.row_count as usize))
2614 .collect(),
2615 None => return Ok(RowIdSet::empty()),
2616 };
2617 let ty = self.resolve_type(column_id);
2618 let clean_contiguous = self.clean_contiguous_row_ids();
2619 let mut out = Vec::new();
2620 let mut page_start = 0usize;
2621 for (seq, (null_count, nrows)) in stats.into_iter().enumerate() {
2622 let current_page_start = page_start;
2623 page_start += nrows;
2624 if want_nulls && null_count == 0 {
2626 continue;
2627 }
2628 if !want_nulls && null_count == nrows {
2629 continue;
2630 }
2631 let val_page = self.read_page(column_id, seq)?;
2632 let col = columnar::decode_page_native(ty, &val_page, nrows)?;
2633 let validity = col.validity();
2634 if clean_contiguous {
2635 for i in 0..nrows {
2636 let is_null = !columnar::validity_bit(validity, i);
2637 if is_null == want_nulls {
2638 out.push(self.header.min_row_id + current_page_start as u64 + i as u64);
2639 }
2640 }
2641 } else {
2642 let rid_page = self.read_page(SYS_ROW_ID, seq)?;
2643 let rids = columnar::decode_page_native(TypeId::Int64, &rid_page, nrows)?;
2644 if let columnar::NativeColumn::Int64 { data: r, .. } = rids {
2645 for (i, &rid) in r.iter().enumerate().take(nrows) {
2646 let is_null = !columnar::validity_bit(validity, i);
2647 if is_null == want_nulls {
2648 out.push(rid as u64);
2649 }
2650 }
2651 }
2652 }
2653 }
2654 Ok(RowIdSet::from_unsorted(out))
2655 }
2656
2657 pub fn visible_indices_native(&mut self, snapshot: Epoch) -> Result<Vec<usize>> {
2658 let n = self.row_count();
2659 if n == 0 {
2660 return Ok(Vec::new());
2661 }
2662 let (row_ids, epochs, deleted) = self.system_columns_native()?;
2663 let mut best: HashMap<u64, (u64, usize)> = HashMap::new();
2664 for i in 0..n {
2665 let rid = row_ids[i] as u64;
2666 let e = epochs[i] as u64;
2667 if e > snapshot.0 {
2668 continue;
2669 }
2670 best.entry(rid)
2671 .and_modify(|(be, bi)| {
2672 if e > *be {
2673 *be = e;
2674 *bi = i;
2675 }
2676 })
2677 .or_insert((e, i));
2678 }
2679 let mut idxs: Vec<usize> = best.into_values().map(|(_, i)| i).collect();
2680 idxs.retain(|&i| deleted[i] == 0);
2681 idxs.sort_unstable();
2682 Ok(idxs)
2683 }
2684
2685 pub fn range_row_ids_visible_i64(
2694 &mut self,
2695 column_id: u16,
2696 lo: i64,
2697 hi: i64,
2698 snapshot: Epoch,
2699 ) -> Result<Vec<u64>> {
2700 let stats: Vec<(Option<i64>, Option<i64>, usize)> = match self.column_page_stats(column_id)
2701 {
2702 Some(s) => s
2703 .iter()
2704 .map(|st| {
2705 (
2706 be_i64(st.min.as_deref()),
2707 be_i64(st.max.as_deref()),
2708 st.row_count as usize,
2709 )
2710 })
2711 .collect(),
2712 None => return Ok(Vec::new()),
2713 };
2714 let stats_pruneable =
2720 !self.col_encrypted(column_id) || self.header.encrypted_stats_offset != 0;
2721 let (positions, rids) = self.visible_positions_with_rids(snapshot)?;
2722 let mut out: Vec<u64> = Vec::new();
2723 let mut vis = 0usize;
2724 let mut page_start = 0usize;
2725 for (seq, &(mn, mx, nrows)) in stats.iter().enumerate() {
2726 let page_end = page_start + nrows;
2727 let skip = stats_pruneable
2729 && match (mn, mx) {
2730 (Some(mn), Some(mx)) => mx < lo || mn > hi,
2731 _ => true, };
2733 if !skip {
2734 let val_page = self.read_page(column_id, seq)?;
2735 let vals = columnar::decode_page_native(TypeId::Int64, &val_page, nrows)?;
2736 if let columnar::NativeColumn::Int64 { data: v, validity } = vals {
2737 while vis < positions.len() && positions[vis] < page_end {
2738 let local = positions[vis] - page_start;
2739 if columnar::validity_bit(&validity, local)
2740 && v[local] >= lo
2741 && v[local] <= hi
2742 {
2743 out.push(rids[vis] as u64);
2744 }
2745 vis += 1;
2746 }
2747 }
2748 } else {
2749 while vis < positions.len() && positions[vis] < page_end {
2750 vis += 1;
2751 }
2752 }
2753 page_start = page_end;
2754 }
2755 Ok(out)
2756 }
2757
2758 pub fn range_row_ids_visible_f64(
2761 &mut self,
2762 column_id: u16,
2763 lo: f64,
2764 lo_inclusive: bool,
2765 hi: f64,
2766 hi_inclusive: bool,
2767 snapshot: Epoch,
2768 ) -> Result<Vec<u64>> {
2769 let stats: Vec<(Option<f64>, Option<f64>, usize)> = match self.column_page_stats(column_id)
2770 {
2771 Some(s) => s
2772 .iter()
2773 .map(|st| {
2774 (
2775 be_f64(st.min.as_deref()),
2776 be_f64(st.max.as_deref()),
2777 st.row_count as usize,
2778 )
2779 })
2780 .collect(),
2781 None => return Ok(Vec::new()),
2782 };
2783 let stats_pruneable =
2789 !self.col_encrypted(column_id) || self.header.encrypted_stats_offset != 0;
2790 let (positions, rids) = self.visible_positions_with_rids(snapshot)?;
2791 let mut out: Vec<u64> = Vec::new();
2792 let mut vis = 0usize;
2793 let mut page_start = 0usize;
2794 for (seq, &(mn, mx, nrows)) in stats.iter().enumerate() {
2795 let page_end = page_start + nrows;
2796 let skip = stats_pruneable
2797 && match (mn, mx) {
2798 (Some(mn), Some(mx)) => {
2799 let skip_lo = mx < lo || (!lo_inclusive && mx == lo);
2800 let skip_hi = mn > hi || (!hi_inclusive && mn == hi);
2801 skip_lo || skip_hi
2802 }
2803 _ => true,
2804 };
2805 if !skip {
2806 let val_page = self.read_page(column_id, seq)?;
2807 let vals = columnar::decode_page_native(TypeId::Float64, &val_page, nrows)?;
2808 if let columnar::NativeColumn::Float64 { data: v, validity } = vals {
2809 while vis < positions.len() && positions[vis] < page_end {
2810 let local = positions[vis] - page_start;
2811 if columnar::validity_bit(&validity, local) && !v[local].is_nan() {
2812 let val = v[local];
2813 let ok_lo = if lo_inclusive { val >= lo } else { val > lo };
2814 let ok_hi = if hi_inclusive { val <= hi } else { val < hi };
2815 if ok_lo && ok_hi {
2816 out.push(rids[vis] as u64);
2817 }
2818 }
2819 vis += 1;
2820 }
2821 }
2822 } else {
2823 while vis < positions.len() && positions[vis] < page_end {
2824 vis += 1;
2825 }
2826 }
2827 page_start = page_end;
2828 }
2829 Ok(out)
2830 }
2831
2832 pub fn null_row_ids_visible(
2838 &mut self,
2839 column_id: u16,
2840 want_nulls: bool,
2841 snapshot: Epoch,
2842 ) -> Result<Vec<u64>> {
2843 let stats: Vec<(usize, usize)> = match self.column_page_stats(column_id) {
2844 Some(s) => s
2845 .iter()
2846 .map(|st| (st.null_count as usize, st.row_count as usize))
2847 .collect(),
2848 None => return Ok(Vec::new()),
2849 };
2850 let ty = self.resolve_type(column_id);
2851 let (positions, rids) = self.visible_positions_with_rids(snapshot)?;
2852 let mut out: Vec<u64> = Vec::new();
2853 let mut vis = 0usize;
2854 let mut page_start = 0usize;
2855 for (seq, &(null_count, nrows)) in stats.iter().enumerate() {
2856 let page_end = page_start + nrows;
2857 let skip = (want_nulls && null_count == 0) || (!want_nulls && null_count == nrows);
2858 if !skip {
2859 let val_page = self.read_page(column_id, seq)?;
2860 let col = columnar::decode_page_native(ty, &val_page, nrows)?;
2861 let validity = col.validity();
2862 while vis < positions.len() && positions[vis] < page_end {
2863 let local = positions[vis] - page_start;
2864 let is_null = !columnar::validity_bit(validity, local);
2865 if is_null == want_nulls {
2866 out.push(rids[vis] as u64);
2867 }
2868 vis += 1;
2869 }
2870 } else {
2871 while vis < positions.len() && positions[vis] < page_end {
2872 vis += 1;
2873 }
2874 }
2875 page_start = page_end;
2876 }
2877 Ok(out)
2878 }
2879
2880 pub fn visible_positions_with_rids(
2884 &mut self,
2885 snapshot: Epoch,
2886 ) -> Result<(Vec<usize>, Vec<i64>)> {
2887 let n = self.row_count();
2888 if n == 0 {
2889 return Ok((Vec::new(), Vec::new()));
2890 }
2891 if self.is_clean() && self.epoch_override.is_none() {
2900 let row_ids = match self.column_native_shared(SYS_ROW_ID)? {
2901 columnar::NativeColumn::Int64 { data, .. } => data,
2902 _ => return Err(MongrelError::InvalidArgument("sys row_id not int64".into())),
2903 };
2904 let positions: Vec<usize> = (0..n).collect();
2905 return Ok((positions, row_ids));
2906 }
2907 let (row_ids, epochs, deleted) = self.system_columns_native()?;
2908 let mut idxs: Vec<usize> = Vec::new();
2919 let mut i = 0;
2920 while i < n {
2921 let rid = row_ids[i] as u64;
2922 let mut best: Option<usize> = None;
2925 let mut j = i;
2926 while j < n && row_ids[j] as u64 == rid {
2927 if epochs[j] as u64 <= snapshot.0 {
2928 best = Some(j);
2929 }
2930 j += 1;
2931 }
2932 if let Some(b) = best {
2933 if deleted[b] == 0 {
2934 idxs.push(b);
2935 }
2936 }
2937 i = j;
2938 }
2939 let rids: Vec<i64> = idxs.iter().map(|&k| row_ids[k]).collect();
2941 Ok((idxs, rids))
2942 }
2943
2944 pub fn page_row_counts(&self, column_id: u16) -> Result<Vec<usize>> {
2948 Ok(self
2949 .find_header(column_id)?
2950 .page_stats
2951 .iter()
2952 .map(|s| s.row_count as usize)
2953 .collect())
2954 }
2955
2956 pub fn has_column(&self, column_id: u16) -> bool {
2959 self.dir.iter().any(|h| h.column_id == column_id)
2960 }
2961
2962 pub fn column_page_stats(&self, column_id: u16) -> Option<&[crate::page::PageStat]> {
2966 self.dir
2967 .iter()
2968 .find(|h| h.column_id == column_id)
2969 .map(|ch| ch.page_stats.as_slice())
2970 }
2971
2972 pub(crate) fn system_columns_native(&mut self) -> Result<(Vec<i64>, Vec<i64>, Vec<u8>)> {
2977 let row_ids = match self.column_native_shared(SYS_ROW_ID)? {
2978 columnar::NativeColumn::Int64 { data, .. } => data,
2979 _ => return Err(MongrelError::InvalidArgument("sys row_id not int64".into())),
2980 };
2981 let epochs = match self.column_native_shared(SYS_EPOCH)? {
2982 columnar::NativeColumn::Int64 { data, .. } => data,
2983 _ => return Err(MongrelError::InvalidArgument("sys epoch not int64".into())),
2984 };
2985 let deleted = match self.column_native_shared(SYS_DELETED)? {
2986 columnar::NativeColumn::Bool { data, .. } => data,
2987 _ => return Err(MongrelError::InvalidArgument("sys deleted not bool".into())),
2988 };
2989 Ok((row_ids, epochs, deleted))
2990 }
2991
2992 pub fn visible_versions(&mut self, snapshot: Epoch) -> Result<Vec<Row>> {
2996 let n = self.row_count();
2997 if n == 0 {
2998 return Ok(Vec::new());
2999 }
3000 let row_ids = self.column(SYS_ROW_ID)?.to_vec();
3001 let epochs = self.column(SYS_EPOCH)?.to_vec();
3002 let mut best: HashMap<u64, (u64, usize)> = HashMap::new();
3003 for i in 0..n {
3004 let rid = int_at(&row_ids, i);
3005 let epoch = int_at(&epochs, i);
3006 if epoch > snapshot.0 {
3007 continue;
3008 }
3009 best.entry(rid)
3010 .and_modify(|e| {
3011 if epoch > e.0 {
3012 *e = (epoch, i);
3013 }
3014 })
3015 .or_insert((epoch, i));
3016 }
3017 let mut picks: Vec<usize> = best.into_values().map(|(_, i)| i).collect();
3018 picks.sort();
3019 let mut out = Vec::with_capacity(picks.len());
3020 for i in picks {
3021 out.push(self.materialize(i)?);
3022 }
3023 Ok(out)
3024 }
3025
3026 pub fn visible_rows(&mut self, snapshot: Epoch) -> Result<Vec<Row>> {
3028 Ok(self
3029 .visible_versions(snapshot)?
3030 .into_iter()
3031 .filter(|r| !r.deleted)
3032 .collect())
3033 }
3034
3035 pub(crate) fn materialize(&mut self, index: usize) -> Result<Row> {
3036 let row_id = RowId(int_at(self.column(SYS_ROW_ID)?, index));
3037 let epoch = Epoch(int_at(self.column(SYS_EPOCH)?, index));
3038 let deleted = bool_at(self.column(SYS_DELETED)?, index);
3039 let col_ids: Vec<u16> = self.schema.columns.iter().map(|c| c.id).collect();
3040 let mut columns = HashMap::new();
3041 for id in col_ids {
3042 let val = if self.dir.iter().any(|h| h.column_id == id) {
3043 self.column(id)?.get(index).cloned().unwrap_or(Value::Null)
3044 } else {
3045 Value::Null
3047 };
3048 columns.insert(id, val);
3049 }
3050 Ok(Row {
3051 row_id,
3052 committed_epoch: epoch,
3053 columns,
3054 deleted,
3055 })
3056 }
3057
3058 pub(crate) fn materialize_batch(&mut self, indices: &[usize]) -> Result<Vec<Row>> {
3067 if indices.is_empty() {
3068 return Ok(Vec::new());
3069 }
3070 use std::collections::HashMap;
3071 let rid_col = self.column_native_shared(SYS_ROW_ID)?;
3072 let epoch_col = self.column_native_shared(SYS_EPOCH)?;
3073 let del_col = self.column_native_shared(SYS_DELETED)?;
3074 let mut user: HashMap<u16, columnar::NativeColumn> = HashMap::new();
3075 let present: Vec<u16> = self
3076 .schema
3077 .columns
3078 .iter()
3079 .map(|c| c.id)
3080 .filter(|id| self.dir.iter().any(|h| h.column_id == *id))
3081 .collect();
3082 for id in present {
3083 user.insert(id, self.column_native(id)?);
3084 }
3085 let i64_at = |col: &columnar::NativeColumn, i: usize| -> i64 {
3086 match col {
3087 columnar::NativeColumn::Int64 { data, .. } => data.get(i).copied().unwrap_or(0),
3088 _ => 0,
3089 }
3090 };
3091 let bool_at_native = |col: &columnar::NativeColumn, i: usize| -> bool {
3092 match col {
3093 columnar::NativeColumn::Bool { data, .. } => {
3094 data.get(i).copied().map(|b| b != 0).unwrap_or(false)
3095 }
3096 _ => false,
3097 }
3098 };
3099 let mut rows = Vec::with_capacity(indices.len());
3100 for &idx in indices {
3101 let row_id = RowId(i64_at(&rid_col, idx) as u64);
3102 let epoch = Epoch(i64_at(&epoch_col, idx) as u64);
3103 let deleted = bool_at_native(&del_col, idx);
3104 let mut columns = HashMap::with_capacity(self.schema.columns.len());
3105 for cdef in self.schema.columns.iter() {
3106 let val = match user.get(&cdef.id) {
3107 Some(col) => col.value_at(idx).unwrap_or(Value::Null),
3108 None => Value::Null,
3109 };
3110 columns.insert(cdef.id, val);
3111 }
3112 rows.push(Row {
3113 row_id,
3114 committed_epoch: epoch,
3115 columns,
3116 deleted,
3117 });
3118 }
3119 Ok(rows)
3120 }
3121}
3122
3123fn int_at(vals: &[Value], i: usize) -> u64 {
3124 match vals.get(i) {
3125 Some(Value::Int64(x)) => *x as u64,
3126 _ => 0,
3127 }
3128}
3129
3130fn bool_at(vals: &[Value], i: usize) -> bool {
3131 matches!(vals.get(i), Some(Value::Bool(true)))
3132}
3133
3134#[cfg(test)]
3135mod tests {
3136 use super::*;
3137 use crate::columnar::NativeColumn;
3138 use crate::memtable::Value;
3139 use crate::rowid::RowId;
3140 use crate::schema::{ColumnDef, ColumnFlags};
3141 use tempfile::tempdir;
3142
3143 fn schema() -> Schema {
3144 Schema {
3145 schema_id: 1,
3146 columns: vec![
3147 ColumnDef {
3148 id: 1,
3149 name: "id".into(),
3150 ty: TypeId::Int64,
3151 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
3152 },
3153 ColumnDef {
3154 id: 2,
3155 name: "name".into(),
3156 ty: TypeId::Bytes,
3157 flags: ColumnFlags::empty(),
3158 },
3159 ],
3160 indexes: Vec::new(),
3161 colocation: vec![],
3162 constraints: Default::default(),
3163 clustered: false,
3164 }
3165 }
3166
3167 fn rows() -> Vec<Row> {
3168 vec![
3169 Row::new(RowId(1), Epoch(10))
3170 .with_column(1, Value::Int64(1))
3171 .with_column(2, Value::Bytes(b"alice".to_vec())),
3172 Row::new(RowId(2), Epoch(10))
3173 .with_column(1, Value::Int64(2))
3174 .with_column(2, Value::Bytes(b"bob".to_vec())),
3175 Row::new(RowId(3), Epoch(10))
3176 .with_column(1, Value::Int64(3))
3177 .with_column(2, Value::Bytes(b"carol".to_vec())),
3178 ]
3179 }
3180
3181 #[test]
3182 fn flush_then_read_mvcc() {
3183 let dir = tempdir().unwrap();
3184 let path = dir.path().join("r-1.sr");
3185 let header = RunWriter::new(&schema(), 1, Epoch(10), 0)
3186 .write(&path, &rows())
3187 .unwrap();
3188 assert_eq!(header.row_count, 3);
3189
3190 let mut r = RunReader::open(&path, schema(), None).unwrap();
3191 let (e, row) = r.get_version(RowId(2), Epoch(20)).unwrap().unwrap();
3193 assert_eq!(e, Epoch(10));
3194 assert_eq!(row.row_id, RowId(2));
3195 assert!(matches!(row.columns.get(&2), Some(Value::Bytes(_))));
3196 assert!(r.get_version(RowId(99), Epoch(20)).unwrap().is_none());
3198 let all = r.visible_rows(Epoch(20)).unwrap();
3200 assert_eq!(all.len(), 3);
3201 }
3202
3203 #[test]
3204 fn learned_index_was_stored() {
3205 let dir = tempdir().unwrap();
3206 let path = dir.path().join("r-2.sr");
3207 RunWriter::new(&schema(), 2, Epoch(1), 0)
3208 .write(&path, &rows())
3209 .unwrap();
3210 let header = read_header(&path).unwrap();
3211 assert!(
3212 header.index_trailer_offset != 0,
3213 "trailer should be present"
3214 );
3215 }
3216
3217 #[test]
3218 fn low_level_container_still_round_trips() {
3219 let dir = tempdir().unwrap();
3220 let path = dir.path().join("r-3.sr");
3221 let cols = vec![ColumnPayload {
3222 column_id: 1,
3223 type_id_tag: 8,
3224 encoding: Encoding::Plain,
3225 pages: vec![vec![1, 2, 3, 4]],
3226 page_stats: Vec::new(),
3227 }];
3228 let header = write_run(
3229 &path,
3230 &RunSpec {
3231 run_id: 1,
3232 schema_id: 1,
3233 epoch_created: 1,
3234 level: 0,
3235 flags: 0,
3236 sort_key_column_id: SORT_KEY_ROW_ID,
3237 row_count: 1,
3238 min_row_id: 0,
3239 max_row_id: 0,
3240 columns: &cols,
3241 },
3242 )
3243 .unwrap();
3244 let back = read_header(&path).unwrap();
3245 assert_eq!(back.content_hash, header.content_hash);
3246 assert_eq!(read_column_dir(&path, &back).unwrap().len(), 1);
3247 }
3248
3249 #[test]
3250 fn detects_corruption() {
3251 let dir = tempdir().unwrap();
3252 let path = dir.path().join("r-4.sr");
3253 write_run(
3254 &path,
3255 &RunSpec {
3256 run_id: 1,
3257 schema_id: 1,
3258 epoch_created: 1,
3259 level: 0,
3260 flags: 0,
3261 sort_key_column_id: SORT_KEY_ROW_ID,
3262 row_count: 1,
3263 min_row_id: 0,
3264 max_row_id: 0,
3265 columns: &[ColumnPayload {
3266 column_id: 1,
3267 type_id_tag: 8,
3268 encoding: Encoding::Plain,
3269 pages: vec![vec![1, 2, 3]],
3270 page_stats: Vec::new(),
3271 }],
3272 },
3273 )
3274 .unwrap();
3275 let mut bytes = std::fs::read(&path).unwrap();
3276 bytes[300] ^= 0xFF;
3277 std::fs::write(&path, bytes).unwrap();
3278 let err = read_header(&path).unwrap_err();
3279 assert!(
3280 matches!(err, MongrelError::ChecksumMismatch { .. }),
3281 "got {err:?}"
3282 );
3283 }
3284
3285 #[test]
3291 fn mmap_and_vec_writers_are_byte_identical() {
3292 let dir = tempdir().unwrap();
3293 let stats = vec![PageStat {
3294 first_row_id: 0,
3295 last_row_id: 1,
3296 null_count: 0,
3297 row_count: 2,
3298 min: Some(vec![0]),
3299 max: Some(vec![9]),
3300 offset: 0,
3301 compressed_len: 0,
3302 uncompressed_len: 0,
3303 }];
3304 let spec = RunSpec {
3305 run_id: 7,
3306 schema_id: 1,
3307 epoch_created: 10,
3308 level: 0,
3309 flags: 0,
3310 sort_key_column_id: SORT_KEY_ROW_ID,
3311 row_count: 2,
3312 min_row_id: 0,
3313 max_row_id: 1,
3314 columns: &[
3315 ColumnPayload {
3316 column_id: 1,
3317 type_id_tag: 8,
3318 encoding: Encoding::Plain,
3319 pages: vec![vec![1, 2, 3, 4], vec![5, 6, 7, 8]],
3320 page_stats: stats.clone(),
3321 },
3322 ColumnPayload {
3323 column_id: 2,
3324 type_id_tag: 8,
3325 encoding: Encoding::Plain,
3326 pages: vec![vec![10, 20], vec![30, 40]],
3327 page_stats: stats.clone(),
3328 },
3329 ],
3330 };
3331 let trailer = b"learned-trailer-bytes";
3332
3333 let plan = plan_run(&spec, None, Some(trailer)).expect("plan");
3335 let mut buf = vec![0u8; plan.total];
3336 let h_place = place_run(&spec, None, Some(trailer), &plan, &mut buf)
3337 .expect("place_run into Vec succeeds");
3338
3339 let path_vec = dir.path().join("vec.sr");
3341 let h_vec = write_run_vec(&path_vec, &spec, None, Some(trailer)).unwrap();
3342 let bv = std::fs::read(&path_vec).unwrap();
3343
3344 assert_eq!(h_place.content_hash, h_vec.content_hash);
3345 assert_eq!(h_place.footer_offset, h_vec.footer_offset);
3346 assert_eq!(
3347 buf, bv,
3348 "place_run and write_run_vec must be byte-identical"
3349 );
3350 assert!(read_header(&path_vec).is_ok());
3351
3352 let path_mmap = dir.path().join("mmap.sr");
3356 match write_run_mmap(&path_mmap, &spec, None, Some(trailer)) {
3357 Ok(h_mmap) => {
3358 let bm = std::fs::read(&path_mmap).unwrap();
3359 assert_eq!(bm, bv, "mmap run must be byte-identical to vec run");
3360 assert_eq!(h_mmap.content_hash, h_vec.content_hash);
3361 }
3362 Err(e) if is_mmap_unavailable(&e) => {
3363 eprintln!("note: file mmap unavailable here; vec path covered (1)/(2)");
3364 }
3365 Err(e) => panic!("unexpected mmap error: {e:?}"),
3366 }
3367 }
3368
3369 #[test]
3374 fn column_native_shared_matches_column_native() {
3375 let dir = tempdir().unwrap();
3376 let path = dir.path().join("r.sr");
3377 let n = 65_536 * 2 + 9;
3380 let id_col = NativeColumn::int64_sequence(1, n);
3381 let mut offsets = vec![0u32];
3382 let mut values = Vec::new();
3383 for i in 0..n {
3384 values.extend_from_slice(format!("v{}", i % 17).as_bytes());
3385 offsets.push(values.len() as u32);
3386 }
3387 let name_col = NativeColumn::Bytes {
3388 offsets,
3389 values,
3390 validity: vec![0xFF; n.div_ceil(8)],
3391 };
3392 let header = RunWriter::new(&schema(), 9, Epoch(5), 0)
3393 .write_native(&path, &[(1, id_col), (2, name_col)], n, 1)
3394 .unwrap();
3395
3396 let mut reader = RunReader::open_with_cache(&path, schema(), None, None, None, 0, None)
3397 .expect("open reader");
3398 assert!(reader.has_mmap(), "test env must support read-only mmap");
3399 assert_eq!(reader.row_count(), header.row_count as usize);
3400
3401 for cid in [1u16, 2] {
3402 let a = reader.column_native(cid).expect("column_native");
3403 let b = reader
3404 .column_native_shared(cid)
3405 .expect("column_native_shared");
3406 assert_eq!(a.len(), b.len(), "len mismatch col {cid}");
3407 match (&a, &b) {
3409 (
3410 NativeColumn::Int64 {
3411 data: da,
3412 validity: va,
3413 },
3414 NativeColumn::Int64 {
3415 data: db,
3416 validity: vb,
3417 },
3418 ) => {
3419 assert_eq!(da, db, "Int64 data col {cid}");
3420 assert_eq!(va, vb, "Int64 validity col {cid}");
3421 }
3422 (
3423 NativeColumn::Bytes {
3424 offsets: oa,
3425 values: ua,
3426 validity: va,
3427 },
3428 NativeColumn::Bytes {
3429 offsets: ob,
3430 values: ub,
3431 validity: vb,
3432 },
3433 ) => {
3434 assert_eq!(oa, ob, "Bytes offsets col {cid}");
3435 assert_eq!(ua, ub, "Bytes values col {cid}");
3436 assert_eq!(va, vb, "Bytes validity col {cid}");
3437 }
3438 _ => panic!("type mismatch col {cid}: {a:?} vs {b:?}"),
3439 }
3440 }
3441 }
3442
3443 #[test]
3444 fn page_cache_key_distinguishes_tables() {
3445 let a = page_cache_key(1, 5, 2, 3);
3446 let b = page_cache_key(2, 5, 2, 3);
3447 assert_ne!(a, b, "same run/col/page, different table must differ");
3448 assert_ne!(page_cache_key(1, 5, 2, 3), page_cache_key(1, 6, 2, 3));
3450 assert_ne!(page_cache_key(1, 5, 2, 3), page_cache_key(1, 5, 2, 4));
3451 }
3452}