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::{LearnedIndex, 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 = 1;
30pub const RUN_HEADER_PAD: usize = 256;
31
32const GCM_TAG_LEN: usize = 16;
37
38fn on_disk_len(page_len: usize, encrypted: bool) -> usize {
41 if encrypted {
42 page_len + GCM_TAG_LEN
43 } else {
44 page_len
45 }
46}
47
48pub const RUN_FLAG_ENCRYPTED: u8 = 1 << 0;
49pub const RUN_FLAG_TOMBSTONE_ONLY: u8 = 1 << 1;
50pub const RUN_FLAG_CLEAN: u8 = 1 << 2;
59pub const RUN_FLAG_UNIFORM_EPOCH: u8 = 1 << 3;
67pub const SORT_KEY_ROW_ID: u16 = 0xFFFF;
68
69pub const SYS_ROW_ID: u16 = 0xFFFE;
71pub const SYS_EPOCH: u16 = 0xFFFD;
72pub const SYS_DELETED: u16 = 0xFFFC;
73
74const LEARNED_EPSILON: usize = 64;
77
78fn build_learned_trailer(row_ids: &[Value]) -> Vec<u8> {
82 let points: Vec<(u64, usize)> = row_ids
83 .iter()
84 .enumerate()
85 .filter_map(|(i, v)| match v {
86 Value::Int64(r) => Some((*r as u64, i)),
87 _ => None,
88 })
89 .collect();
90 let pgm = PgmIndex::build(&points, LEARNED_EPSILON);
91 bincode::serialize(&pgm).expect("pgm serialize")
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct RunHeader {
96 pub magic: [u8; 8],
97 pub format_version: u16,
98 pub header_layout_version: u16,
99 pub run_id: u128,
100 pub content_hash: [u8; 32],
101 pub schema_id: u64,
102 pub epoch_created: u64,
103 pub level: u8,
104 pub flags: u8,
105 pub sort_key_column_id: u16,
106 pub row_count: u64,
107 pub min_row_id: u64,
108 pub max_row_id: u64,
109 pub column_count: u64,
110 pub column_dir_offset: u64,
111 pub index_trailer_offset: u64,
112 pub encryption_descriptor_offset: u64,
113 pub footer_offset: u64,
114}
115
116impl RunHeader {
117 pub fn is_encrypted(&self) -> bool {
118 self.flags & RUN_FLAG_ENCRYPTED != 0
119 }
120 pub fn is_clean(&self) -> bool {
121 self.flags & RUN_FLAG_CLEAN != 0
122 }
123 pub fn is_uniform_epoch(&self) -> bool {
124 self.flags & RUN_FLAG_UNIFORM_EPOCH != 0
125 }
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct ColumnPageHeader {
130 pub column_id: u16,
131 pub type_id_tag: u16,
132 pub encoding: u8,
133 pub flags: u8,
134 pub page_count: u32,
135 pub page_region_offset: u64,
136 pub page_region_len: u64,
137 pub page_stats: Vec<PageStat>,
138}
139
140impl ColumnPageHeader {
141 const PAGE_ENCRYPTED: u8 = 1 << 0;
142}
143
144const RUN_MAC_LEN: usize = 32;
147
148fn compute_run_mac(
152 enc: Option<&RunEncryption>,
153 header_bytes: &[u8],
154 dir_bytes: &[u8],
155) -> Option<[u8; RUN_MAC_LEN]> {
156 #[cfg(feature = "encryption")]
157 {
158 if let Some(e) = enc {
159 if let Some(mac_key) = &e.mac_key {
160 return Some(crate::encryption::run_metadata_mac(
161 mac_key,
162 header_bytes,
163 dir_bytes,
164 &e.descriptor_bytes,
165 ));
166 }
167 }
168 }
169 #[cfg(not(feature = "encryption"))]
170 {
171 let _ = (enc, header_bytes, dir_bytes);
172 }
173 None
174}
175
176pub struct ColumnPayload {
178 pub column_id: u16,
179 pub type_id_tag: u16,
180 pub encoding: Encoding,
181 pub pages: Vec<Vec<u8>>,
182 pub page_stats: Vec<PageStat>,
186}
187
188pub struct RunSpec<'a> {
190 pub run_id: u128,
191 pub schema_id: u64,
192 pub epoch_created: u64,
193 pub level: u8,
194 pub flags: u8,
195 pub sort_key_column_id: u16,
196 pub row_count: u64,
197 pub min_row_id: u64,
198 pub max_row_id: u64,
199 pub columns: &'a [ColumnPayload],
200}
201
202pub fn write_run(path: impl AsRef<Path>, spec: &RunSpec) -> Result<RunHeader> {
204 write_run_with(path, spec, None, &[], None)
205}
206
207pub fn write_run_with(
217 path: impl AsRef<Path>,
218 spec: &RunSpec,
219 kek: Option<&Kek>,
220 indexable_columns: &[(u16, u8)],
221 index_trailer: Option<&[u8]>,
222) -> Result<RunHeader> {
223 let enc: Option<RunEncryption> = match kek {
227 Some(k) => Some(setup_run_encryption(k, indexable_columns)?),
228 None => None,
229 };
230 match write_run_mmap(path.as_ref(), spec, enc.as_ref(), index_trailer) {
231 Ok(h) => Ok(h),
232 Err(e) if is_mmap_unavailable(&e) => write_run_vec(path, spec, enc, index_trailer),
233 Err(e) => Err(e),
234 }
235}
236
237fn is_mmap_unavailable(e: &MongrelError) -> bool {
241 matches!(e, MongrelError::InvalidArgument(m) if m.starts_with("__mmap_unavailable__:"))
242}
243
244fn write_run_mmap(
265 path: &Path,
266 spec: &RunSpec,
267 enc: Option<&RunEncryption>,
268 index_trailer: Option<&[u8]>,
269) -> Result<RunHeader> {
270 let plan = plan_run(spec, enc, index_trailer)?;
271 let file = OpenOptions::new()
272 .create(true)
273 .write(true)
274 .truncate(true)
275 .open(path)?;
276 file.set_len(plan.total as u64)?;
277 let mut mmap = match unsafe { memmap2::MmapMut::map_mut(&file) } {
278 Ok(m) => m,
279 Err(e) => {
280 return Err(MongrelError::InvalidArgument(format!(
281 "__mmap_unavailable__: {e}"
282 )))
283 }
284 };
285 let header = place_run(spec, enc, index_trailer, &plan, &mut mmap[..])?;
286 mmap.flush()?;
287 file.sync_all()?;
288 Ok(header)
289}
290
291struct RunPlan {
297 jobs: Vec<(usize, usize, u64, usize)>, dir_bytes: Vec<u8>,
299 encrypted: bool,
300 column_dir_offset: u64,
301 index_trailer_offset: u64,
302 encryption_descriptor_offset: u64,
303 footer_offset: u64,
304 total: usize,
305}
306
307fn plan_run(
311 spec: &RunSpec,
312 enc: Option<&RunEncryption>,
313 index_trailer: Option<&[u8]>,
314) -> Result<RunPlan> {
315 let encrypted = enc.is_some();
316 let columns = spec.columns;
317 let mut jobs: Vec<(usize, usize, u64, usize)> = Vec::new();
318 let mut dir: Vec<ColumnPageHeader> = Vec::with_capacity(columns.len());
319 let mut cursor: u64 = RUN_HEADER_PAD as u64;
320 for (ci, col) in columns.iter().enumerate() {
321 let region_offset = cursor;
322 let mut region_len = 0u64;
323 let mut stats = Vec::with_capacity(col.pages.len());
324 for (ps, page) in col.pages.iter().enumerate() {
325 if encrypted && ps > u16::MAX as usize {
329 return Err(MongrelError::Full(format!(
330 "column {:#x} exceeds 65535 pages; encrypted-run page-seq nonce space exhausted",
331 col.column_id
332 )));
333 }
334 let odl = on_disk_len(page.len(), encrypted);
335 jobs.push((ci, ps, cursor, odl));
336 let mut stat = col.page_stats.get(ps).cloned().unwrap_or(PageStat {
337 first_row_id: 0,
338 last_row_id: 0,
339 null_count: 0,
340 row_count: 0,
341 min: None,
342 max: None,
343 offset: 0,
344 compressed_len: 0,
345 uncompressed_len: 0,
346 });
347 stat.offset = cursor;
348 stat.compressed_len = odl as u32;
349 stat.uncompressed_len = page.len() as u32;
350 if encrypted {
357 stat.min = None;
358 stat.max = None;
359 }
360 stats.push(stat);
361 cursor += odl as u64;
362 region_len += odl as u64;
363 }
364 let page_flags = if encrypted {
365 ColumnPageHeader::PAGE_ENCRYPTED
366 } else {
367 0
368 };
369 dir.push(ColumnPageHeader {
370 column_id: col.column_id,
371 type_id_tag: col.type_id_tag,
372 encoding: col.encoding as u8,
373 flags: page_flags,
374 page_count: col.pages.len() as u32,
375 page_region_offset: region_offset,
376 page_region_len: region_len,
377 page_stats: stats,
378 });
379 }
380 let dir_bytes = bincode::serialize(&dir)?;
381 let column_dir_offset = cursor;
382 cursor += dir_bytes.len() as u64;
383 let index_trailer_offset = match index_trailer {
384 Some(t) => {
385 let off = cursor;
386 cursor += t.len() as u64;
387 off
388 }
389 None => 0,
390 };
391 let encryption_descriptor_offset = match enc {
392 Some(e) => {
393 let off = cursor;
394 cursor += 4 + e.descriptor_bytes.len() as u64;
395 off
396 }
397 None => 0,
398 };
399 let footer_offset = cursor;
400 let total = footer_offset as usize + 8 + 8 + 32 + if encrypted { RUN_MAC_LEN } else { 0 };
403 Ok(RunPlan {
404 jobs,
405 dir_bytes,
406 encrypted,
407 column_dir_offset,
408 index_trailer_offset,
409 encryption_descriptor_offset,
410 footer_offset,
411 total,
412 })
413}
414
415fn place_run(
422 spec: &RunSpec,
423 enc: Option<&RunEncryption>,
424 index_trailer: Option<&[u8]>,
425 plan: &RunPlan,
426 buf: &mut [u8],
427) -> Result<RunHeader> {
428 use rayon::prelude::*;
429 use std::borrow::Cow;
430 debug_assert_eq!(
431 buf.len(),
432 plan.total,
433 "place_run: buffer must be exactly plan.total bytes"
434 );
435
436 let cipher: Option<&dyn Cipher> = enc.map(|e| e.cipher.as_ref());
437 let nonce_prefix = enc.map(|e| e.nonce_prefix);
438 let columns = spec.columns;
439
440 struct SyncPtr(*mut u8);
445 unsafe impl Send for SyncPtr {}
446 unsafe impl Sync for SyncPtr {}
447 impl SyncPtr {
448 fn get(&self) -> *mut u8 {
449 self.0
450 }
451 }
452 let base = SyncPtr(buf.as_mut_ptr());
453 plan.jobs
454 .par_iter()
455 .map(
456 |&(ci, ps, offset, odl): &(usize, usize, u64, usize)| -> Result<()> {
457 let page = &columns[ci].pages[ps];
458 let dst =
459 unsafe { std::slice::from_raw_parts_mut(base.get().add(offset as usize), odl) };
460 let bytes: Cow<[u8]> = match cipher {
461 Some(c) => {
462 let nonce =
463 page_nonce(nonce_prefix.unwrap(), columns[ci].column_id, ps as u32);
464 let ct = c.encrypt_page(&nonce, page)?;
465 assert_eq!(
468 ct.len(), odl,
469 "ciphertext length {} != predicted {}; GCM tag size assumption is stale",
470 ct.len(), odl
471 );
472 Cow::Owned(ct)
473 }
474 None => {
475 debug_assert_eq!(page.len(), odl);
476 Cow::Borrowed(page.as_slice())
477 }
478 };
479 dst.copy_from_slice(&bytes);
480 Ok(())
481 },
482 )
483 .collect::<Result<()>>()?;
484
485 let content_hash = {
487 let mut h = Sha256::new();
488 h.update(&buf[RUN_HEADER_PAD..plan.column_dir_offset as usize]);
489 h.finalize()
490 };
491
492 let doff = plan.column_dir_offset as usize;
494 buf[doff..doff + plan.dir_bytes.len()].copy_from_slice(&plan.dir_bytes);
495 if let Some(t) = index_trailer {
496 let off = plan.index_trailer_offset as usize;
497 buf[off..off + t.len()].copy_from_slice(t);
498 }
499 if let Some(e) = enc {
500 let off = plan.encryption_descriptor_offset as usize;
501 buf[off..off + 4].copy_from_slice(&(e.descriptor_bytes.len() as u32).to_le_bytes());
502 buf[off + 4..off + 4 + e.descriptor_bytes.len()].copy_from_slice(&e.descriptor_bytes);
503 }
504
505 let header_flags = if plan.encrypted {
507 spec.flags | RUN_FLAG_ENCRYPTED
508 } else {
509 spec.flags
510 };
511 let header = RunHeader {
512 magic: RUN_MAGIC,
513 format_version: RUN_FORMAT_VERSION,
514 header_layout_version: RUN_HEADER_VERSION,
515 run_id: spec.run_id,
516 content_hash: content_hash.into(),
517 schema_id: spec.schema_id,
518 epoch_created: spec.epoch_created,
519 level: spec.level,
520 flags: header_flags,
521 sort_key_column_id: spec.sort_key_column_id,
522 row_count: spec.row_count,
523 min_row_id: spec.min_row_id,
524 max_row_id: spec.max_row_id,
525 column_count: columns.len() as u64,
526 column_dir_offset: plan.column_dir_offset,
527 index_trailer_offset: plan.index_trailer_offset,
528 encryption_descriptor_offset: plan.encryption_descriptor_offset,
529 footer_offset: plan.footer_offset,
530 };
531 let header_bytes = bincode::serialize(&header)?;
532 if header_bytes.len() > RUN_HEADER_PAD {
533 return Err(MongrelError::InvalidArgument(format!(
534 "run header too large: {} > {RUN_HEADER_PAD}",
535 header_bytes.len()
536 )));
537 }
538 buf[..header_bytes.len()].copy_from_slice(&header_bytes);
539
540 let checksum = Sha256::digest(&buf[..plan.footer_offset as usize]);
541 let foot = plan.footer_offset as usize;
542 buf[foot..foot + 8].copy_from_slice(&RUN_MAGIC);
543 buf[foot + 8..foot + 16].copy_from_slice(&plan.footer_offset.to_le_bytes());
544 buf[foot + 16..foot + 48].copy_from_slice(&checksum);
545 if let Some(tag) = compute_run_mac(enc, &header_bytes, &plan.dir_bytes) {
547 buf[foot + 48..foot + 48 + RUN_MAC_LEN].copy_from_slice(&tag);
548 }
549 Ok(header)
550}
551
552fn write_run_vec(
555 path: impl AsRef<Path>,
556 spec: &RunSpec,
557 enc: Option<RunEncryption>,
558 index_trailer: Option<&[u8]>,
559) -> Result<RunHeader> {
560 let mut buf: Vec<u8> = vec![0; RUN_HEADER_PAD]; let mut content_hasher = Sha256::new();
562 let mut dir: Vec<ColumnPageHeader> = Vec::with_capacity(spec.columns.len());
563
564 for col in spec.columns {
565 let region_offset = buf.len() as u64;
566 let mut region_len = 0u64;
567 let mut stats = Vec::with_capacity(col.pages.len());
568 for (page_seq, page) in col.pages.iter().enumerate() {
569 if enc.is_some() && page_seq > u16::MAX as usize {
570 return Err(MongrelError::Full(format!(
571 "column {:#x} exceeds 65535 pages; encrypted-run page-seq nonce space exhausted",
572 col.column_id
573 )));
574 }
575 let on_disk: Vec<u8> = match &enc {
576 Some(e) => e.cipher.encrypt_page(
577 &page_nonce(e.nonce_prefix, col.column_id, page_seq as u32),
578 page,
579 )?,
580 None => page.clone(),
581 };
582 let offset = buf.len() as u64;
583 buf.write_all(&on_disk)?;
584 content_hasher.update(&on_disk);
585 region_len += on_disk.len() as u64;
586 let mut stat = if let Some(s) = col.page_stats.get(page_seq) {
587 s.clone()
588 } else {
589 PageStat {
590 first_row_id: 0,
591 last_row_id: 0,
592 null_count: 0,
593 row_count: 0,
594 min: None,
595 max: None,
596 offset: 0,
597 compressed_len: 0,
598 uncompressed_len: 0,
599 }
600 };
601 stat.offset = offset;
602 stat.compressed_len = on_disk.len() as u32;
603 stat.uncompressed_len = page.len() as u32;
604 if enc.is_some() {
608 stat.min = None;
609 stat.max = None;
610 }
611 stats.push(stat);
612 }
613 let page_flags = if enc.is_some() {
614 ColumnPageHeader::PAGE_ENCRYPTED
615 } else {
616 0
617 };
618 dir.push(ColumnPageHeader {
619 column_id: col.column_id,
620 type_id_tag: col.type_id_tag,
621 encoding: col.encoding as u8,
622 flags: page_flags,
623 page_count: col.pages.len() as u32,
624 page_region_offset: region_offset,
625 page_region_len: region_len,
626 page_stats: stats,
627 });
628 }
629
630 let column_dir_offset = buf.len() as u64;
631 let dir_bytes = bincode::serialize(&dir)?;
632 buf.write_all(&dir_bytes)?;
633
634 let index_trailer_offset = match index_trailer {
635 Some(trailer) => {
636 let off = buf.len() as u64;
637 buf.write_all(trailer)?;
638 off
639 }
640 None => 0,
641 };
642 let encryption_descriptor_offset = match &enc {
643 Some(e) => {
644 let off = buf.len() as u64;
645 buf.write_all(&(e.descriptor_bytes.len() as u32).to_le_bytes())?;
646 buf.write_all(&e.descriptor_bytes)?;
647 off
648 }
649 None => 0,
650 };
651 let footer_offset = buf.len() as u64;
652
653 let header_flags = if enc.is_some() {
654 spec.flags | RUN_FLAG_ENCRYPTED
655 } else {
656 spec.flags
657 };
658 let header = RunHeader {
659 magic: RUN_MAGIC,
660 format_version: RUN_FORMAT_VERSION,
661 header_layout_version: RUN_HEADER_VERSION,
662 run_id: spec.run_id,
663 content_hash: content_hasher.finalize().into(),
664 schema_id: spec.schema_id,
665 epoch_created: spec.epoch_created,
666 level: spec.level,
667 flags: header_flags,
668 sort_key_column_id: spec.sort_key_column_id,
669 row_count: spec.row_count,
670 min_row_id: spec.min_row_id,
671 max_row_id: spec.max_row_id,
672 column_count: spec.columns.len() as u64,
673 column_dir_offset,
674 index_trailer_offset,
675 encryption_descriptor_offset,
676 footer_offset,
677 };
678 let header_bytes = bincode::serialize(&header)?;
679 if header_bytes.len() > RUN_HEADER_PAD {
680 return Err(MongrelError::InvalidArgument(format!(
681 "run header too large: {} > {RUN_HEADER_PAD}",
682 header_bytes.len()
683 )));
684 }
685 buf[..header_bytes.len()].copy_from_slice(&header_bytes);
686
687 let checksum = Sha256::digest(&buf[..footer_offset as usize]);
688 buf.write_all(&RUN_MAGIC)?;
689 buf.write_all(&footer_offset.to_le_bytes())?;
690 buf.write_all(&checksum)?;
691 if let Some(tag) = compute_run_mac(enc.as_ref(), &header_bytes, &dir_bytes) {
694 buf.write_all(&tag)?;
695 }
696
697 let mut file = OpenOptions::new()
698 .create(true)
699 .write(true)
700 .truncate(true)
701 .open(path)?;
702 file.write_all(&buf)?;
703 file.sync_all()?;
704 Ok(header)
705}
706
707fn page_nonce(nonce_prefix: [u8; 12], column_id: u16, page_seq: u32) -> [u8; 12] {
708 let mut n = nonce_prefix;
709 n[8..10].copy_from_slice(&column_id.to_le_bytes());
710 n[10..12].copy_from_slice(&(page_seq as u16).to_le_bytes());
711 n
712}
713
714pub(crate) fn page_cache_key(
721 table_id: u64,
722 run_id: u128,
723 column_id: u16,
724 page_seq: usize,
725) -> [u8; 32] {
726 let mut h = Sha256::new();
727 h.update(table_id.to_be_bytes());
728 h.update(run_id.to_be_bytes());
729 h.update(column_id.to_be_bytes());
730 h.update((page_seq as u64).to_be_bytes());
731 let out = h.finalize();
732 let mut k = [0u8; 32];
733 k.copy_from_slice(&out);
734 k
735}
736
737fn decrypt_or_passthrough(
741 cipher: Option<&dyn Cipher>,
742 nonce_prefix: [u8; 12],
743 column_id: u16,
744 page_seq: usize,
745 encrypted: bool,
746 buf: &[u8],
747) -> Result<Vec<u8>> {
748 if encrypted {
749 match cipher {
750 Some(c) => {
751 Ok(c.decrypt_page(&page_nonce(nonce_prefix, column_id, page_seq as u32), buf)?)
752 }
753 None => Err(MongrelError::Decryption(
754 "encrypted page but no cipher".into(),
755 )),
756 }
757 } else {
758 Ok(buf.to_vec())
759 }
760}
761
762pub fn read_header(path: impl AsRef<Path>) -> Result<RunHeader> {
764 let mut file = File::open(path)?;
765 let mut header_buf = vec![0u8; RUN_HEADER_PAD];
766 file.read_exact(&mut header_buf)?;
767 let header: RunHeader = bincode::deserialize(&header_buf)
768 .map_err(|e| MongrelError::InvalidArgument(format!("bad run header: {e}")))?;
769 if header.magic != RUN_MAGIC {
770 return Err(MongrelError::MagicMismatch {
771 what: "sorted run",
772 expected: RUN_MAGIC,
773 got: header.magic,
774 });
775 }
776
777 file.seek(SeekFrom::Start(header.footer_offset))?;
778 let mut footer = [0u8; 8 + 8 + 32];
779 file.read_exact(&mut footer)?;
780 if footer[..8] != RUN_MAGIC {
781 return Err(MongrelError::MagicMismatch {
782 what: "sorted run footer",
783 expected: RUN_MAGIC,
784 got: footer[..8].try_into().unwrap(),
785 });
786 }
787 let mut hasher = Sha256::new();
788 hasher.update(&header_buf);
789 let body_len = header.footer_offset.saturating_sub(RUN_HEADER_PAD as u64);
790 file.seek(SeekFrom::Start(RUN_HEADER_PAD as u64))?;
791 let mut body = vec![0u8; body_len as usize];
792 file.read_exact(&mut body)?;
793 hasher.update(&body);
794 let computed: [u8; 32] = hasher.finalize().into();
795 let stored: [u8; 32] = footer[16..].try_into().unwrap();
796 if computed != stored {
797 return Err(MongrelError::ChecksumMismatch {
798 expected: u64::from_be_bytes(stored[..8].try_into().unwrap()),
799 actual: u64::from_be_bytes(computed[..8].try_into().unwrap()),
800 context: "sorted run footer".into(),
801 });
802 }
803 Ok(header)
804}
805
806pub fn read_column_dir(
808 path: impl AsRef<Path>,
809 header: &RunHeader,
810) -> Result<Vec<ColumnPageHeader>> {
811 let mut file = File::open(path)?;
812 file.seek(SeekFrom::Start(header.column_dir_offset))?;
813 let end = if header.index_trailer_offset != 0 {
814 header.index_trailer_offset
815 } else {
816 header.footer_offset
817 };
818 let len = end.saturating_sub(header.column_dir_offset);
819 let mut buf = vec![0u8; len as usize];
820 file.read_exact(&mut buf)?;
821 let dir: Vec<ColumnPageHeader> = bincode::deserialize(&buf)
822 .map_err(|e| MongrelError::InvalidArgument(format!("bad column dir: {e}")))?;
823 Ok(dir)
824}
825
826pub(crate) fn read_encryption_descriptor_bytes(
829 path: impl AsRef<Path>,
830 header: &RunHeader,
831) -> Result<Vec<u8>> {
832 let mut file = File::open(path)?;
833 file.seek(SeekFrom::Start(header.encryption_descriptor_offset))?;
834 let mut len_buf = [0u8; 4];
835 file.read_exact(&mut len_buf)?;
836 let len = u32::from_le_bytes(len_buf) as usize;
837 const MAX_DESCRIPTOR_BYTES: usize = 65_536;
842 if len > MAX_DESCRIPTOR_BYTES {
843 return Err(MongrelError::InvalidArgument(format!(
844 "encryption descriptor length {len} exceeds {MAX_DESCRIPTOR_BYTES}"
845 )));
846 }
847 let mut buf = vec![0u8; len];
848 file.read_exact(&mut buf)?;
849 Ok(buf)
850}
851
852#[cfg(feature = "encryption")]
859fn verify_run_mac(
860 path: &Path,
861 header: &RunHeader,
862 dir: &[ColumnPageHeader],
863 kek: &Kek,
864 desc_bytes: &[u8],
865) -> Result<()> {
866 let header_bytes = bincode::serialize(header)?;
867 let dir_bytes = bincode::serialize(dir)?;
868 let mac_key = kek.derive_run_mac_key();
869 let expected =
870 crate::encryption::run_metadata_mac(&mac_key, &header_bytes, &dir_bytes, desc_bytes);
871 let mut file = File::open(path)?;
872 file.seek(SeekFrom::Start(header.footer_offset + 8 + 8 + 32))?;
873 let mut tag = [0u8; RUN_MAC_LEN];
874 file.read_exact(&mut tag).map_err(|_| {
875 MongrelError::Decryption(
876 "encrypted run is missing or truncated its metadata MAC; cannot \
877 authenticate metadata"
878 .into(),
879 )
880 })?;
881 let mut diff = 0u8;
883 for (x, y) in tag.iter().zip(expected.iter()) {
884 diff |= x ^ y;
885 }
886 if diff != 0 {
887 return Err(MongrelError::Decryption(
888 "run metadata authentication failed — tampered run or wrong key".into(),
889 ));
890 }
891 Ok(())
892}
893
894#[cfg(not(feature = "encryption"))]
895fn verify_run_mac(
896 _path: &Path,
897 _header: &RunHeader,
898 _dir: &[ColumnPageHeader],
899 _kek: &Kek,
900 _desc_bytes: &[u8],
901) -> Result<()> {
902 Ok(())
903}
904
905pub struct RunWriter<'a> {
914 schema: &'a Schema,
915 run_id: u128,
916 epoch_created: Epoch,
917 level: u8,
918 kek: Option<&'a Kek>,
919 indexable_columns: Vec<(u16, u8)>,
922 compress: columnar::Compress,
926 clean: bool,
931 uniform_epoch: bool,
936 le: bool,
943}
944
945impl<'a> RunWriter<'a> {
946 pub fn new(schema: &'a Schema, run_id: u128, epoch_created: Epoch, level: u8) -> Self {
947 Self {
948 schema,
949 run_id,
950 epoch_created,
951 level,
952 kek: None,
953 indexable_columns: Vec::new(),
954 compress: columnar::Compress::Zstd(3),
955 clean: false,
956 uniform_epoch: false,
957 le: false,
958 }
959 }
960
961 pub fn uniform_epoch(mut self, uniform: bool) -> Self {
967 self.uniform_epoch = uniform;
968 self
969 }
970
971 pub fn with_encryption(mut self, kek: &'a Kek, indexable_columns: Vec<(u16, u8)>) -> Self {
975 self.kek = Some(kek);
976 self.indexable_columns = indexable_columns;
977 self
978 }
979
980 pub fn with_zstd_level(mut self, level: i32) -> Self {
983 self.compress = columnar::Compress::Zstd(level);
984 self
985 }
986
987 pub fn with_lz4(mut self) -> Self {
991 self.compress = columnar::Compress::Lz4;
992 self
993 }
994
995 pub fn with_plain(mut self) -> Self {
998 self.compress = columnar::Compress::Plain;
999 self
1000 }
1001
1002 pub fn clean(mut self, clean: bool) -> Self {
1008 self.clean = clean;
1009 self
1010 }
1011
1012 pub fn with_native_endian(mut self) -> Self {
1017 if cfg!(target_endian = "little") {
1018 self.le = true;
1019 }
1020 self
1021 }
1022
1023 pub fn write_native(
1030 self,
1031 path: impl AsRef<Path>,
1032 user_columns: &[(u16, columnar::NativeColumn)],
1033 n: usize,
1034 first_row_id: u64,
1035 ) -> Result<RunHeader> {
1036 use columnar::NativeColumn;
1037 let row_id_col = NativeColumn::int64_sequence(first_row_id as i64, n);
1038 let epoch_col = NativeColumn::int64_constant(self.epoch_created.0 as i64, n);
1039 let deleted_col = NativeColumn::bool_constant(false, n);
1040
1041 let learned_trailer = build_learned_trailer_native(&row_id_col);
1042
1043 let row_ids = match &row_id_col {
1046 NativeColumn::Int64 { data, .. } => data.as_slice(),
1047 _ => &[],
1048 };
1049 let bounds = page_bounds(row_ids);
1050 let compress = self.compress;
1051 let le = self.le;
1052 let plain = matches!(compress, columnar::Compress::Plain);
1053 let row_id_enc = if plain {
1056 Encoding::Plain
1057 } else {
1058 Encoding::Delta
1059 };
1060 let sys_enc = if plain {
1061 Encoding::Plain
1062 } else {
1063 Encoding::Zstd
1064 };
1065
1066 let mut columns: Vec<ColumnPayload> = Vec::with_capacity(3 + self.schema.columns.len());
1067 let (pages, stats) = native_column_pages(
1068 TypeId::Int64,
1069 &row_id_col,
1070 row_id_enc,
1071 compress,
1072 le,
1073 &bounds,
1074 )?;
1075 columns.push(ColumnPayload {
1076 column_id: SYS_ROW_ID,
1077 type_id_tag: type_tag(&TypeId::Int64),
1078 encoding: row_id_enc,
1079 pages,
1080 page_stats: stats,
1081 });
1082 let (pages, stats) =
1083 native_column_pages(TypeId::Int64, &epoch_col, sys_enc, compress, le, &bounds)?;
1084 columns.push(ColumnPayload {
1085 column_id: SYS_EPOCH,
1086 type_id_tag: type_tag(&TypeId::Int64),
1087 encoding: sys_enc,
1088 pages,
1089 page_stats: stats,
1090 });
1091 let (pages, stats) =
1092 native_column_pages(TypeId::Bool, &deleted_col, sys_enc, compress, le, &bounds)?;
1093 columns.push(ColumnPayload {
1094 column_id: SYS_DELETED,
1095 type_id_tag: type_tag(&TypeId::Bool),
1096 encoding: sys_enc,
1097 pages,
1098 page_stats: stats,
1099 });
1100 use rayon::prelude::*;
1106 let user_cols: Vec<ColumnPayload> = self
1107 .schema
1108 .columns
1109 .par_iter()
1110 .map(|cdef| -> Result<ColumnPayload> {
1111 let col = user_columns
1112 .iter()
1113 .find(|(id, _)| *id == cdef.id)
1114 .map(|(_, c)| c)
1115 .ok_or_else(|| {
1116 MongrelError::ColumnNotFound(format!("user column {}", cdef.id))
1117 })?;
1118 let encoding = if plain {
1119 Encoding::Plain
1120 } else {
1121 choose_encoding_native(&cdef.ty, col)
1122 };
1123 let (pages, stats) =
1124 native_column_pages(cdef.ty, col, encoding, compress, le, &bounds)?;
1125 Ok(ColumnPayload {
1126 column_id: cdef.id,
1127 type_id_tag: type_tag(&cdef.ty),
1128 encoding,
1129 pages,
1130 page_stats: stats,
1131 })
1132 })
1133 .collect::<Result<Vec<_>>>()?;
1134 columns.extend(user_cols);
1135
1136 let flags = if self.uniform_epoch {
1142 RUN_FLAG_UNIFORM_EPOCH
1143 } else {
1144 RUN_FLAG_CLEAN
1145 };
1146 let spec = RunSpec {
1147 run_id: self.run_id,
1148 schema_id: self.schema.schema_id,
1149 epoch_created: self.epoch_created.0,
1150 level: self.level,
1151 flags,
1152 sort_key_column_id: SYS_ROW_ID,
1153 row_count: n as u64,
1154 min_row_id: first_row_id,
1155 max_row_id: first_row_id + n as u64 - 1,
1156 columns: &columns,
1157 };
1158 write_run_with(
1159 path,
1160 &spec,
1161 self.kek,
1162 &self.indexable_columns,
1163 Some(&learned_trailer),
1164 )
1165 }
1166
1167 pub fn write(self, path: impl AsRef<Path>, rows: &[Row]) -> Result<RunHeader> {
1168 let n = rows.len();
1169 let mut row_ids = Vec::with_capacity(n);
1171 let mut epochs = Vec::with_capacity(n);
1172 let mut deleted = Vec::with_capacity(n);
1173 for r in rows {
1174 row_ids.push(Value::Int64(r.row_id.0 as i64));
1175 epochs.push(Value::Int64(r.committed_epoch.0 as i64));
1176 deleted.push(Value::Bool(r.deleted));
1177 }
1178 let learned_trailer = build_learned_trailer(&row_ids);
1179 let (min_rid, max_rid) = row_id_bounds(rows);
1180 let row_id_i64: Vec<i64> = rows.iter().map(|r| r.row_id.0 as i64).collect();
1181 let bounds = page_bounds(&row_id_i64);
1182
1183 let mut columns: Vec<ColumnPayload> = Vec::with_capacity(3 + self.schema.columns.len());
1184 let (pages, stats, enc) = value_column_pages(TypeId::Int64, &row_ids, &bounds)?;
1185 columns.push(ColumnPayload {
1186 column_id: SYS_ROW_ID,
1187 type_id_tag: type_tag(&TypeId::Int64),
1188 encoding: enc,
1189 pages,
1190 page_stats: stats,
1191 });
1192 let (pages, stats, enc) = value_column_pages(TypeId::Int64, &epochs, &bounds)?;
1193 columns.push(ColumnPayload {
1194 column_id: SYS_EPOCH,
1195 type_id_tag: type_tag(&TypeId::Int64),
1196 encoding: enc,
1197 pages,
1198 page_stats: stats,
1199 });
1200 let (pages, stats, enc) = value_column_pages(TypeId::Bool, &deleted, &bounds)?;
1201 columns.push(ColumnPayload {
1202 column_id: SYS_DELETED,
1203 type_id_tag: type_tag(&TypeId::Bool),
1204 encoding: enc,
1205 pages,
1206 page_stats: stats,
1207 });
1208 for cdef in &self.schema.columns {
1210 let vals: Vec<Value> = rows
1211 .iter()
1212 .map(|r| r.columns.get(&cdef.id).cloned().unwrap_or(Value::Null))
1213 .collect();
1214 let (pages, stats, encoding) = value_column_pages(cdef.ty, &vals, &bounds)?;
1215 columns.push(ColumnPayload {
1216 column_id: cdef.id,
1217 type_id_tag: type_tag(&cdef.ty),
1218 encoding,
1219 pages,
1220 page_stats: stats,
1221 });
1222 }
1223
1224 let spec = RunSpec {
1225 run_id: self.run_id,
1226 schema_id: self.schema.schema_id,
1227 epoch_created: self.epoch_created.0,
1228 level: self.level,
1229 flags: {
1230 let mut f = if self.clean { RUN_FLAG_CLEAN } else { 0 };
1231 if self.uniform_epoch {
1232 f |= RUN_FLAG_UNIFORM_EPOCH;
1233 }
1234 f
1235 },
1236 sort_key_column_id: SYS_ROW_ID,
1237 row_count: n as u64,
1238 min_row_id: min_rid,
1239 max_row_id: max_rid,
1240 columns: &columns,
1241 };
1242 write_run_with(
1243 path,
1244 &spec,
1245 self.kek,
1246 &self.indexable_columns,
1247 Some(&learned_trailer),
1248 )
1249 }
1250}
1251
1252fn type_tag(ty: &TypeId) -> u16 {
1253 match ty {
1255 TypeId::Bool => 1,
1256 TypeId::Int64 => 8,
1257 TypeId::Float64 => 9,
1258 TypeId::Bytes => 12,
1259 TypeId::Embedding { .. } => 13,
1260 _ => 0,
1261 }
1262}
1263
1264pub(crate) fn be_i64(b: Option<&[u8]>) -> Option<i64> {
1267 let b = b?;
1268 (b.len() == 8).then(|| i64::from_be_bytes(b.try_into().unwrap()))
1269}
1270
1271pub(crate) fn be_f64(b: Option<&[u8]>) -> Option<f64> {
1273 let b = b?;
1274 (b.len() == 8).then(|| f64::from_bits(u64::from_be_bytes(b.try_into().unwrap())))
1275}
1276
1277const PAGE_ROWS: usize = 65_536;
1280
1281fn page_bounds(row_ids: &[i64]) -> Vec<(usize, usize, u64, u64)> {
1284 let n = row_ids.len();
1285 if n == 0 {
1286 return vec![(0, 0, 0, 0)];
1287 }
1288 let mut out = Vec::new();
1289 let mut start = 0;
1290 while start < n {
1291 let end = (start + PAGE_ROWS).min(n);
1292 out.push((start, end, row_ids[start] as u64, row_ids[end - 1] as u64));
1293 start = end;
1294 }
1295 out
1296}
1297
1298fn native_column_pages(
1304 ty: TypeId,
1305 col: &columnar::NativeColumn,
1306 encoding: Encoding,
1307 compress: columnar::Compress,
1308 le: bool,
1309 bounds: &[(usize, usize, u64, u64)],
1310) -> Result<(Vec<Vec<u8>>, Vec<PageStat>)> {
1311 use rayon::prelude::*;
1312 let encode_one =
1313 |&(s, e, frid, lrid): &(usize, usize, u64, u64)| -> Result<(Vec<u8>, PageStat)> {
1314 let chunk = col.slice_range(s, e);
1315 let stat = columnar::page_stat_for(ty, &chunk, frid, lrid);
1316 let page = columnar::encode_page_native(ty, &chunk, encoding, compress, le)?;
1317 Ok((page, stat))
1318 };
1319 let pairs: Vec<(Vec<u8>, PageStat)> = if bounds.len() > 1 {
1321 bounds
1322 .par_iter()
1323 .map(encode_one)
1324 .collect::<Result<Vec<_>>>()?
1325 } else {
1326 bounds.iter().map(encode_one).collect::<Result<Vec<_>>>()?
1327 };
1328 let (pages, stats) = pairs.into_iter().unzip();
1329 Ok((pages, stats))
1330}
1331
1332fn value_column_pages(
1334 ty: TypeId,
1335 vals: &[Value],
1336 bounds: &[(usize, usize, u64, u64)],
1337) -> Result<(Vec<Vec<u8>>, Vec<PageStat>, Encoding)> {
1338 let encoding = choose_encoding(&ty, vals);
1339 let mut pages = Vec::with_capacity(bounds.len());
1340 let mut stats = Vec::with_capacity(bounds.len());
1341 for &(s, e, frid, lrid) in bounds {
1342 let chunk = &vals[s..e];
1343 pages.push(columnar::encode_page(ty, chunk, encoding)?);
1344 let native = columnar::values_to_native(ty, chunk);
1345 stats.push(columnar::page_stat_for(ty, &native, frid, lrid));
1346 }
1347 Ok((pages, stats, encoding))
1348}
1349
1350fn choose_encoding(ty: &TypeId, values: &[Value]) -> Encoding {
1353 use std::collections::HashSet;
1354 if matches!(ty, TypeId::Bytes) {
1355 let n = values.len();
1356 if n > 0 {
1357 let distinct = values
1358 .iter()
1359 .filter(|v| !matches!(v, Value::Null))
1360 .map(|v| v.encode_key())
1361 .collect::<HashSet<_>>()
1362 .len();
1363 if (distinct as f64 / n as f64) < 0.5 {
1364 return Encoding::Dictionary;
1365 }
1366 }
1367 }
1368 Encoding::Zstd
1369}
1370
1371fn choose_encoding_native(ty: &TypeId, col: &columnar::NativeColumn) -> Encoding {
1374 use std::collections::HashSet;
1375 match (ty, col) {
1376 (TypeId::Int64 | TypeId::TimestampNanos, columnar::NativeColumn::Int64 { data, .. }) => {
1377 if data.windows(2).all(|w| w[0] <= w[1]) {
1378 Encoding::Delta
1379 } else {
1380 Encoding::Zstd
1381 }
1382 }
1383 (
1384 TypeId::Bytes,
1385 columnar::NativeColumn::Bytes {
1386 offsets, values, ..
1387 },
1388 ) => {
1389 let n = offsets.len().saturating_sub(1);
1390 if n > 0 {
1391 let distinct: HashSet<&[u8]> = (0..n)
1392 .map(|i| &values[offsets[i] as usize..offsets[i + 1] as usize])
1393 .collect();
1394 if (distinct.len() as f64 / n as f64) < 0.5 {
1395 return Encoding::Dictionary;
1396 }
1397 }
1398 Encoding::Zstd
1399 }
1400 _ => Encoding::Zstd,
1401 }
1402}
1403
1404fn build_learned_trailer_native(col: &columnar::NativeColumn) -> Vec<u8> {
1406 let points: Vec<(u64, usize)> = match col {
1407 columnar::NativeColumn::Int64 { data, .. } => data
1408 .iter()
1409 .enumerate()
1410 .map(|(i, v)| (*v as u64, i))
1411 .collect(),
1412 _ => Vec::new(),
1413 };
1414 let pgm = PgmIndex::build(&points, LEARNED_EPSILON);
1415 bincode::serialize(&pgm).expect("pgm serialize")
1416}
1417
1418fn row_id_bounds(rows: &[Row]) -> (u64, u64) {
1419 match (rows.first(), rows.last()) {
1420 (Some(f), Some(l)) => (f.row_id.0, l.row_id.0),
1421 _ => (0, 0),
1422 }
1423}
1424
1425pub struct RunReader {
1430 file: File,
1431 mmap: Option<memmap2::Mmap>,
1432 header: RunHeader,
1433 dir: Vec<ColumnPageHeader>,
1434 schema: Schema,
1435 table_id: u64,
1437 cipher: Option<Box<dyn Cipher>>,
1439 nonce_prefix: [u8; 12],
1441 col_cache: HashMap<u16, Vec<Value>>,
1442 learned: Option<PgmIndex>,
1443 page_cache: Option<Arc<parking_lot::Mutex<crate::cache::PageCache>>>,
1447 decoded_cache: Option<Arc<parking_lot::Mutex<crate::cache::DecodedPageCache>>>,
1451 epoch_override: Option<Epoch>,
1455}
1456
1457impl RunReader {
1458 pub fn open(path: impl AsRef<Path>, schema: Schema, kek: Option<Arc<Kek>>) -> Result<Self> {
1459 Self::open_with_cache(path, schema, kek, None, None, 0)
1460 }
1461
1462 pub(crate) fn open_with_cache(
1463 path: impl AsRef<Path>,
1464 schema: Schema,
1465 kek: Option<Arc<Kek>>,
1466 page_cache: Option<Arc<parking_lot::Mutex<crate::cache::PageCache>>>,
1467 decoded_cache: Option<Arc<parking_lot::Mutex<crate::cache::DecodedPageCache>>>,
1468 table_id: u64,
1469 ) -> Result<Self> {
1470 let path = path.as_ref().to_path_buf();
1471 let header = read_header(&path)?;
1472 let dir = read_column_dir(&path, &header)?;
1473 let learned = read_learned(&path, &header)?;
1474 let (cipher, nonce_prefix): (Option<Box<dyn Cipher>>, [u8; 12]) = if header.is_encrypted() {
1477 let kek = kek.as_ref().ok_or_else(|| {
1478 MongrelError::Encryption(
1479 "run is encrypted but no key-encryption key was provided".into(),
1480 )
1481 })?;
1482 if header.encryption_descriptor_offset == 0 {
1483 return Err(MongrelError::Encryption(
1484 "encrypted run has no encryption descriptor".into(),
1485 ));
1486 }
1487 let desc_bytes = read_encryption_descriptor_bytes(&path, &header)?;
1488 verify_run_mac(&path, &header, &dir, kek, &desc_bytes)?;
1494 let enc = crate::encryption::build_run_cipher(kek, &desc_bytes)?;
1495 (Some(enc.cipher), enc.nonce_prefix)
1496 } else {
1497 (None, [0u8; 12])
1498 };
1499 let file = File::open(&path)?;
1502 let mmap = unsafe { memmap2::MmapOptions::new().map(&file) }.ok();
1509 let _ = path;
1510 Ok(Self {
1511 file,
1512 mmap,
1513 header,
1514 dir,
1515 schema,
1516 table_id,
1517 cipher,
1518 nonce_prefix,
1519 col_cache: HashMap::new(),
1520 learned,
1521 epoch_override: None,
1522 page_cache,
1523 decoded_cache,
1524 })
1525 }
1526
1527 pub fn header(&self) -> &RunHeader {
1528 &self.header
1529 }
1530
1531 pub(crate) fn set_uniform_epoch(&mut self, epoch: Epoch) {
1535 if self.header.is_uniform_epoch() {
1536 self.epoch_override = Some(epoch);
1537 self.col_cache.remove(&SYS_EPOCH);
1539 }
1540 }
1541
1542 pub fn is_clean(&self) -> bool {
1545 self.header.is_clean()
1546 }
1547
1548 pub fn row_count(&self) -> usize {
1549 self.header.row_count as usize
1550 }
1551
1552 pub(crate) fn clean_contiguous_row_ids(&self) -> bool {
1553 let n = self.row_count();
1554 n > 0
1555 && self.is_clean()
1556 && self.epoch_override.is_none()
1557 && self.header.max_row_id >= self.header.min_row_id
1558 && self
1559 .header
1560 .max_row_id
1561 .checked_sub(self.header.min_row_id)
1562 .and_then(|span| span.checked_add(1))
1563 == Some(n as u64)
1564 }
1565
1566 pub(crate) fn position_for_row_id_fast(&self, row_id: u64) -> Option<usize> {
1567 if !self.clean_contiguous_row_ids()
1568 || row_id < self.header.min_row_id
1569 || row_id > self.header.max_row_id
1570 {
1571 return None;
1572 }
1573 Some((row_id - self.header.min_row_id) as usize)
1574 }
1575
1576 pub(crate) fn positions_for_row_ids_fast(&self, row_ids: &[u64]) -> Option<Vec<usize>> {
1577 if !self.clean_contiguous_row_ids() {
1578 return None;
1579 }
1580 let mut positions = Vec::with_capacity(row_ids.len());
1581 for &row_id in row_ids {
1582 if let Some(pos) = self.position_for_row_id_fast(row_id) {
1583 positions.push(pos);
1584 }
1585 }
1586 positions.sort_unstable();
1587 Some(positions)
1588 }
1589
1590 fn resolve_type(&self, column_id: u16) -> TypeId {
1591 match column_id {
1592 SYS_ROW_ID | SYS_EPOCH => TypeId::Int64,
1593 SYS_DELETED => TypeId::Bool,
1594 _ => self
1595 .schema
1596 .columns
1597 .iter()
1598 .find(|c| c.id == column_id)
1599 .map(|c| c.ty)
1600 .unwrap_or(TypeId::Bytes),
1601 }
1602 }
1603
1604 fn find_header(&self, column_id: u16) -> Result<&ColumnPageHeader> {
1605 self.dir
1606 .iter()
1607 .find(|h| h.column_id == column_id)
1608 .ok_or_else(|| MongrelError::ColumnNotFound(format!("column {column_id}")))
1609 }
1610
1611 fn col_encrypted(&self, column_id: u16) -> bool {
1617 self.dir
1618 .iter()
1619 .find(|h| h.column_id == column_id)
1620 .map(|h| h.flags & ColumnPageHeader::PAGE_ENCRYPTED != 0)
1621 .unwrap_or(false)
1622 }
1623
1624 pub(crate) fn read_page(&mut self, column_id: u16, page_seq: usize) -> Result<Vec<u8>> {
1625 let (offset, compressed_len, encrypted) = {
1626 let ch = self.find_header(column_id)?;
1627 let stat = ch
1628 .page_stats
1629 .get(page_seq)
1630 .ok_or_else(|| MongrelError::InvalidArgument("page seq out of range".into()))?;
1631 (
1632 stat.offset,
1633 stat.compressed_len,
1634 ch.flags & ColumnPageHeader::PAGE_ENCRYPTED != 0,
1635 )
1636 };
1637 let key = page_cache_key(self.table_id, self.header.run_id, column_id, page_seq);
1640 if let Some(cache) = &self.page_cache {
1641 if let Some(bytes) = cache.lock().get(
1642 &key,
1643 crate::epoch::Snapshot::at(crate::epoch::Epoch(u64::MAX)),
1644 ) {
1645 return decrypt_or_passthrough(
1646 self.cipher.as_deref(),
1647 self.nonce_prefix,
1648 column_id,
1649 page_seq,
1650 encrypted,
1651 &bytes,
1652 );
1653 }
1654 }
1655 let buf = match &self.mmap {
1656 Some(m) => m[offset as usize..(offset + compressed_len as u64) as usize].to_vec(),
1659 None => {
1660 self.file.seek(SeekFrom::Start(offset))?;
1661 let mut buf = vec![0u8; compressed_len as usize];
1662 self.file.read_exact(&mut buf)?;
1663 buf
1664 }
1665 };
1666 if let Some(cache) = &self.page_cache {
1668 cache.lock().insert(crate::page::CachedPage {
1669 committed_epoch: crate::epoch::Epoch(self.header.epoch_created),
1670 content_hash: key,
1671 bytes: bytes::Bytes::copy_from_slice(&buf),
1672 });
1673 }
1674 decrypt_or_passthrough(
1675 self.cipher.as_deref(),
1676 self.nonce_prefix,
1677 column_id,
1678 page_seq,
1679 encrypted,
1680 &buf,
1681 )
1682 }
1683
1684 fn read_page_shared(&self, column_id: u16, page_seq: usize) -> Result<Vec<u8>> {
1689 let ch = self.find_header(column_id)?;
1690 let stat = ch
1691 .page_stats
1692 .get(page_seq)
1693 .ok_or_else(|| MongrelError::InvalidArgument("page seq out of range".into()))?;
1694 let encrypted = ch.flags & ColumnPageHeader::PAGE_ENCRYPTED != 0;
1695 let key = page_cache_key(self.table_id, self.header.run_id, column_id, page_seq);
1698 if let Some(cache) = &self.page_cache {
1699 if let Some(guard) = cache.try_lock() {
1700 if let Some(bytes) = guard.try_get(
1701 &key,
1702 crate::epoch::Snapshot::at(crate::epoch::Epoch(u64::MAX)),
1703 ) {
1704 return decrypt_or_passthrough(
1705 self.cipher.as_deref(),
1706 self.nonce_prefix,
1707 column_id,
1708 page_seq,
1709 encrypted,
1710 &bytes,
1711 );
1712 }
1713 }
1714 }
1715 let mmap = self.mmap.as_ref().ok_or_else(|| {
1716 MongrelError::InvalidArgument("parallel page decode requires an mmap-backed run".into())
1717 })?;
1718 let start = stat.offset as usize;
1719 let end = (stat.offset + stat.compressed_len as u64) as usize;
1720 let buf = mmap[start..end].to_vec();
1721 if let Some(cache) = &self.page_cache {
1725 if let Some(mut guard) = cache.try_lock() {
1726 guard.insert(crate::page::CachedPage {
1727 committed_epoch: crate::epoch::Epoch(self.header.epoch_created),
1728 content_hash: key,
1729 bytes: bytes::Bytes::copy_from_slice(&buf),
1730 });
1731 }
1732 }
1733 decrypt_or_passthrough(
1734 self.cipher.as_deref(),
1735 self.nonce_prefix,
1736 column_id,
1737 page_seq,
1738 encrypted,
1739 &buf,
1740 )
1741 }
1742
1743 fn column(&mut self, column_id: u16) -> Result<&[Value]> {
1745 if column_id == SYS_EPOCH {
1748 if let Some(ov) = self.epoch_override {
1749 if !self.col_cache.contains_key(&column_id) {
1750 let n = self.row_count();
1751 self.col_cache
1752 .insert(column_id, vec![Value::Int64(ov.0 as i64); n]);
1753 }
1754 return Ok(self.col_cache.get(&column_id).unwrap().as_slice());
1755 }
1756 }
1757 if !self.col_cache.contains_key(&column_id) {
1758 let ty = self.resolve_type(column_id);
1759 let page_rows: Vec<usize> = {
1760 let ch = self.find_header(column_id)?;
1761 ch.page_stats.iter().map(|s| s.row_count as usize).collect()
1762 };
1763 let mut decoded: Vec<Value> = Vec::with_capacity(self.row_count());
1764 for (seq, &pr) in page_rows.iter().enumerate() {
1765 let page = self.read_page(column_id, seq)?;
1766 decoded.extend(columnar::decode_page(ty, &page, pr)?);
1767 }
1768 self.col_cache.insert(column_id, decoded);
1769 }
1770 Ok(self.col_cache.get(&column_id).unwrap().as_slice())
1771 }
1772
1773 pub fn get_version(&mut self, row_id: RowId, snapshot: Epoch) -> Result<Option<(Epoch, Row)>> {
1776 let n = self.row_count();
1777 if n == 0 {
1778 return Ok(None);
1779 }
1780 let target = row_id.0;
1781 let row_ids = self.column(SYS_ROW_ID)?.to_vec();
1782 let window = self.predict_window(target, n);
1783 let mut start = None;
1786 let mut probe = window;
1787 if probe.is_empty() {
1788 probe = 0..n;
1789 }
1790 for i in probe.clone() {
1791 if int_at(&row_ids, i) == target {
1792 start = Some(i);
1793 break;
1794 }
1795 }
1796 let mut idx = start;
1797 if idx.is_none() {
1798 match row_ids.binary_search_by_key(&target, value_row_id) {
1799 Ok(i) => idx = Some(i),
1800 Err(_) => return Ok(None),
1801 }
1802 }
1803 let mut best: Option<(u64, usize)> = None; let mut i = idx.unwrap();
1805 while i < n && int_at(&row_ids, i) == target {
1806 let epoch = int_at(self.column(SYS_EPOCH)?, i);
1807 if epoch <= snapshot.0 && best.map(|(be, _)| epoch > be).unwrap_or(true) {
1808 best = Some((epoch, i));
1809 }
1810 i += 1;
1811 }
1812 match best {
1813 None => Ok(None),
1814 Some((epoch, index)) => Ok(Some((Epoch(epoch), self.materialize(index)?))),
1815 }
1816 }
1817
1818 pub fn all_rows(&mut self) -> Result<Vec<Row>> {
1821 let n = self.row_count();
1822 let mut out = Vec::with_capacity(n);
1823 for i in 0..n {
1824 out.push(self.materialize(i)?);
1825 }
1826 Ok(out)
1827 }
1828
1829 pub fn visible_indices(&mut self, snapshot: Epoch) -> Result<Vec<usize>> {
1835 let n = self.row_count();
1836 if n == 0 {
1837 return Ok(Vec::new());
1838 }
1839 let row_ids = self.column(SYS_ROW_ID)?.to_vec();
1840 let epochs = self.column(SYS_EPOCH)?.to_vec();
1841 let deleted = self.column(SYS_DELETED)?.to_vec();
1842 let mut best: HashMap<u64, (u64, usize)> = HashMap::new();
1843 for i in 0..n {
1844 let rid = int_at(&row_ids, i);
1845 let e = int_at(&epochs, i);
1846 if e > snapshot.0 {
1847 continue;
1848 }
1849 best.entry(rid)
1850 .and_modify(|(be, bi)| {
1851 if e > *be {
1852 *be = e;
1853 *bi = i;
1854 }
1855 })
1856 .or_insert((e, i));
1857 }
1858 let mut idxs: Vec<usize> = best.into_values().map(|(_, i)| i).collect();
1859 idxs.retain(|&i| !bool_at(&deleted, i));
1860 idxs.sort_unstable();
1861 Ok(idxs)
1862 }
1863
1864 pub fn gather_column(&mut self, column_id: u16, indices: &[usize]) -> Result<Vec<Value>> {
1869 if !self.dir.iter().any(|h| h.column_id == column_id) {
1870 return Ok(vec![Value::Null; indices.len()]);
1871 }
1872 let col = self.column(column_id)?;
1873 Ok(indices
1874 .iter()
1875 .map(|&i| col.get(i).cloned().unwrap_or(Value::Null))
1876 .collect())
1877 }
1878
1879 pub fn column_native(&mut self, column_id: u16) -> Result<columnar::NativeColumn> {
1884 use rayon::prelude::*;
1885 if column_id == SYS_EPOCH {
1886 if let Some(ov) = self.epoch_override {
1887 return Ok(columnar::NativeColumn::int64_constant(
1888 ov.0 as i64,
1889 self.row_count(),
1890 ));
1891 }
1892 }
1893 let ty = self.resolve_type(column_id);
1894 let n = self.row_count();
1895 let Some(ch) = self.dir.iter().find(|h| h.column_id == column_id) else {
1896 return Ok(columnar::null_native(ty, n));
1897 };
1898 let page_count = ch.page_count as usize;
1899 let page_rows: Vec<usize> = ch.page_stats.iter().map(|s| s.row_count as usize).collect();
1900 if page_count == 0 {
1901 return Ok(columnar::null_native(ty, n));
1902 }
1903 let parts: Vec<columnar::NativeColumn> = if self.mmap.is_some() && page_count > 1 {
1904 let reader: &RunReader = self;
1908 (0..page_count)
1909 .into_par_iter()
1910 .map(|seq| {
1911 let raw = reader.read_page_shared(column_id, seq)?;
1912 columnar::decode_page_native(ty, &raw, page_rows[seq])
1913 })
1914 .collect::<Result<Vec<_>>>()?
1915 } else {
1916 let mut out = Vec::with_capacity(page_count);
1917 for (seq, &pr) in page_rows.iter().enumerate() {
1918 let page = self.read_page(column_id, seq)?;
1919 out.push(columnar::decode_page_native(ty, &page, pr)?);
1920 }
1921 out
1922 };
1923 Ok(columnar::NativeColumn::concat(&parts))
1924 }
1925
1926 pub fn has_mmap(&self) -> bool {
1930 self.mmap.is_some()
1931 }
1932
1933 pub fn column_native_shared(&self, column_id: u16) -> Result<columnar::NativeColumn> {
1940 use rayon::prelude::*;
1941 if column_id == SYS_EPOCH {
1942 if let Some(ov) = self.epoch_override {
1943 return Ok(columnar::NativeColumn::int64_constant(
1944 ov.0 as i64,
1945 self.row_count(),
1946 ));
1947 }
1948 }
1949 let ty = self.resolve_type(column_id);
1950 let n = self.row_count();
1951 let Some(ch) = self.dir.iter().find(|h| h.column_id == column_id) else {
1952 return Ok(columnar::null_native(ty, n));
1953 };
1954 let page_count = ch.page_count as usize;
1955 let page_rows: Vec<usize> = ch.page_stats.iter().map(|s| s.row_count as usize).collect();
1956 if page_count == 0 {
1957 return Ok(columnar::null_native(ty, n));
1958 }
1959 if let (Some(m), Some(first)) = (&self.mmap, ch.page_stats.first()) {
1963 let start = first.offset as usize;
1964 let end = (ch.page_region_offset as usize) + (ch.page_region_len as usize);
1965 if end > start {
1966 let _ = m.advise_range(memmap2::Advice::WillNeed, start, end - start);
1967 }
1968 }
1969 let run_id = self.header.run_id;
1970 let mut parts_keys: Vec<(columnar::NativeColumn, Option<[u8; 32]>)> = if page_count > 1 {
1974 (0..page_count)
1975 .into_par_iter()
1976 .map(|seq| self.decode_page_cached(ty, column_id, seq, page_rows[seq], run_id))
1977 .collect::<Result<Vec<_>>>()?
1978 } else {
1979 vec![self.decode_page_cached(ty, column_id, 0, page_rows[0], run_id)?]
1980 };
1981 if let Some(cache) = &self.decoded_cache {
1984 let mut g = cache.lock();
1985 for (col, key) in parts_keys.iter_mut() {
1986 if let Some(k) = key.take() {
1987 g.insert(k, Arc::new(col.clone()));
1988 }
1989 }
1990 }
1991 let parts: Vec<columnar::NativeColumn> = parts_keys.into_iter().map(|(c, _)| c).collect();
1992 Ok(columnar::NativeColumn::concat(&parts))
1993 }
1994
1995 fn decode_page_cached(
2000 &self,
2001 ty: TypeId,
2002 column_id: u16,
2003 seq: usize,
2004 nrows: usize,
2005 run_id: u128,
2006 ) -> Result<(columnar::NativeColumn, Option<[u8; 32]>)> {
2007 let key = page_cache_key(self.table_id, run_id, column_id, seq);
2008 if let Some(cache) = &self.decoded_cache {
2009 if let Some(g) = cache.try_lock() {
2010 if let Some(hit) = g.try_get(&key) {
2011 return Ok(((*hit).clone(), None));
2012 }
2013 }
2014 }
2015 let raw = self.read_page_shared(column_id, seq)?;
2016 let col = columnar::decode_page_native(ty, &raw, nrows)?;
2017 Ok((col, Some(key)))
2018 }
2019
2020 pub fn range_row_ids_i64(
2025 &mut self,
2026 column_id: u16,
2027 lo: i64,
2028 hi: i64,
2029 ) -> Result<std::collections::HashSet<u64>> {
2030 Ok(self
2031 .range_row_id_set_i64(column_id, lo, hi)?
2032 .into_sorted_vec()
2033 .into_iter()
2034 .collect())
2035 }
2036
2037 pub(crate) fn range_row_id_set_i64(
2038 &mut self,
2039 column_id: u16,
2040 lo: i64,
2041 hi: i64,
2042 ) -> Result<RowIdSet> {
2043 let info: Vec<(Option<i64>, Option<i64>, usize)> =
2044 match self.dir.iter().find(|h| h.column_id == column_id) {
2045 Some(ch) => ch
2046 .page_stats
2047 .iter()
2048 .map(|s| {
2049 (
2050 be_i64(s.min.as_deref()),
2051 be_i64(s.max.as_deref()),
2052 s.row_count as usize,
2053 )
2054 })
2055 .collect(),
2056 None => return Ok(RowIdSet::empty()),
2057 };
2058 let col_encrypted = self.col_encrypted(column_id);
2060 let clean_contiguous = self.clean_contiguous_row_ids();
2061 let mut out = Vec::new();
2062 let mut page_start = 0usize;
2063 for (seq, (mn, mx, nrows)) in info.into_iter().enumerate() {
2064 let current_page_start = page_start;
2065 page_start += nrows;
2066 let skip = !col_encrypted
2068 && match (mn, mx) {
2069 (Some(mn), Some(mx)) => mx < lo || mn > hi,
2070 _ => true,
2071 };
2072 if skip {
2073 continue;
2074 }
2075 let val_page = self.read_page(column_id, seq)?;
2076 let vals =
2077 columnar::decode_page_native(self.resolve_type(column_id), &val_page, nrows)?;
2078 if let columnar::NativeColumn::Int64 { data: v, validity } = vals {
2079 if clean_contiguous {
2080 for (i, val) in v.iter().enumerate() {
2081 if columnar::validity_bit(&validity, i) && *val >= lo && *val <= hi {
2082 out.push(self.header.min_row_id + current_page_start as u64 + i as u64);
2083 }
2084 }
2085 } else {
2086 let rid_page = self.read_page(SYS_ROW_ID, seq)?;
2087 let rids = columnar::decode_page_native(TypeId::Int64, &rid_page, nrows)?;
2088 if let columnar::NativeColumn::Int64 { data: r, .. } = rids {
2089 for (i, val) in v.iter().enumerate() {
2090 if columnar::validity_bit(&validity, i) && *val >= lo && *val <= hi {
2091 out.push(r[i] as u64);
2092 }
2093 }
2094 }
2095 }
2096 }
2097 }
2098 Ok(RowIdSet::from_unsorted(out))
2099 }
2100
2101 pub fn range_row_ids_f64(
2104 &mut self,
2105 column_id: u16,
2106 lo: f64,
2107 lo_inclusive: bool,
2108 hi: f64,
2109 hi_inclusive: bool,
2110 ) -> Result<std::collections::HashSet<u64>> {
2111 Ok(self
2112 .range_row_id_set_f64(column_id, lo, lo_inclusive, hi, hi_inclusive)?
2113 .into_sorted_vec()
2114 .into_iter()
2115 .collect())
2116 }
2117
2118 pub(crate) fn range_row_id_set_f64(
2119 &mut self,
2120 column_id: u16,
2121 lo: f64,
2122 lo_inclusive: bool,
2123 hi: f64,
2124 hi_inclusive: bool,
2125 ) -> Result<RowIdSet> {
2126 let info: Vec<(Option<f64>, Option<f64>, usize)> =
2127 match self.dir.iter().find(|h| h.column_id == column_id) {
2128 Some(ch) => ch
2129 .page_stats
2130 .iter()
2131 .map(|s| {
2132 (
2133 be_f64(s.min.as_deref()),
2134 be_f64(s.max.as_deref()),
2135 s.row_count as usize,
2136 )
2137 })
2138 .collect(),
2139 None => return Ok(RowIdSet::empty()),
2140 };
2141 let col_encrypted = self.col_encrypted(column_id);
2143 let clean_contiguous = self.clean_contiguous_row_ids();
2144 let mut out = Vec::new();
2145 let mut page_start = 0usize;
2146 for (seq, (mn, mx, nrows)) in info.into_iter().enumerate() {
2147 let current_page_start = page_start;
2148 page_start += nrows;
2149 let skip = !col_encrypted
2152 && match (mn, mx) {
2153 (Some(mn), Some(mx)) => {
2154 let skip_lo = mx < lo || (!lo_inclusive && mx == lo);
2155 let skip_hi = mn > hi || (!hi_inclusive && mn == hi);
2156 skip_lo || skip_hi
2157 }
2158 _ => true,
2159 };
2160 if skip {
2161 continue;
2162 }
2163 let val_page = self.read_page(column_id, seq)?;
2164 let vals = columnar::decode_page_native(TypeId::Float64, &val_page, nrows)?;
2165 if let columnar::NativeColumn::Float64 { data: v, validity } = vals {
2166 if clean_contiguous {
2167 for (i, val) in v.iter().enumerate() {
2168 if !columnar::validity_bit(&validity, i) || val.is_nan() {
2169 continue;
2170 }
2171 let ok_lo = if lo_inclusive { *val >= lo } else { *val > lo };
2172 let ok_hi = if hi_inclusive { *val <= hi } else { *val < hi };
2173 if ok_lo && ok_hi {
2174 out.push(self.header.min_row_id + current_page_start as u64 + i as u64);
2175 }
2176 }
2177 } else {
2178 let rid_page = self.read_page(SYS_ROW_ID, seq)?;
2179 let rids = columnar::decode_page_native(TypeId::Int64, &rid_page, nrows)?;
2180 if let columnar::NativeColumn::Int64 { data: r, .. } = rids {
2181 for (i, val) in v.iter().enumerate() {
2182 if !columnar::validity_bit(&validity, i) || val.is_nan() {
2183 continue;
2184 }
2185 let ok_lo = if lo_inclusive { *val >= lo } else { *val > lo };
2186 let ok_hi = if hi_inclusive { *val <= hi } else { *val < hi };
2187 if ok_lo && ok_hi {
2188 out.push(r[i] as u64);
2189 }
2190 }
2191 }
2192 }
2193 }
2194 }
2195 Ok(RowIdSet::from_unsorted(out))
2196 }
2197
2198 pub(crate) fn null_row_id_set(&mut self, column_id: u16, want_nulls: bool) -> Result<RowIdSet> {
2203 let stats: Vec<(usize, usize)> = match self.dir.iter().find(|h| h.column_id == column_id) {
2204 Some(ch) => ch
2205 .page_stats
2206 .iter()
2207 .map(|s| (s.null_count as usize, s.row_count as usize))
2208 .collect(),
2209 None => return Ok(RowIdSet::empty()),
2210 };
2211 let ty = self.resolve_type(column_id);
2212 let clean_contiguous = self.clean_contiguous_row_ids();
2213 let mut out = Vec::new();
2214 let mut page_start = 0usize;
2215 for (seq, (null_count, nrows)) in stats.into_iter().enumerate() {
2216 let current_page_start = page_start;
2217 page_start += nrows;
2218 if want_nulls && null_count == 0 {
2220 continue;
2221 }
2222 if !want_nulls && null_count == nrows {
2223 continue;
2224 }
2225 let val_page = self.read_page(column_id, seq)?;
2226 let col = columnar::decode_page_native(ty, &val_page, nrows)?;
2227 let validity = col.validity();
2228 if clean_contiguous {
2229 for i in 0..nrows {
2230 let is_null = !columnar::validity_bit(validity, i);
2231 if is_null == want_nulls {
2232 out.push(self.header.min_row_id + current_page_start as u64 + i as u64);
2233 }
2234 }
2235 } else {
2236 let rid_page = self.read_page(SYS_ROW_ID, seq)?;
2237 let rids = columnar::decode_page_native(TypeId::Int64, &rid_page, nrows)?;
2238 if let columnar::NativeColumn::Int64 { data: r, .. } = rids {
2239 for (i, &rid) in r.iter().enumerate().take(nrows) {
2240 let is_null = !columnar::validity_bit(validity, i);
2241 if is_null == want_nulls {
2242 out.push(rid as u64);
2243 }
2244 }
2245 }
2246 }
2247 }
2248 Ok(RowIdSet::from_unsorted(out))
2249 }
2250
2251 pub fn visible_indices_native(&mut self, snapshot: Epoch) -> Result<Vec<usize>> {
2252 let n = self.row_count();
2253 if n == 0 {
2254 return Ok(Vec::new());
2255 }
2256 let (row_ids, epochs, deleted) = self.system_columns_native()?;
2257 let mut best: HashMap<u64, (u64, usize)> = HashMap::new();
2258 for i in 0..n {
2259 let rid = row_ids[i] as u64;
2260 let e = epochs[i] as u64;
2261 if e > snapshot.0 {
2262 continue;
2263 }
2264 best.entry(rid)
2265 .and_modify(|(be, bi)| {
2266 if e > *be {
2267 *be = e;
2268 *bi = i;
2269 }
2270 })
2271 .or_insert((e, i));
2272 }
2273 let mut idxs: Vec<usize> = best.into_values().map(|(_, i)| i).collect();
2274 idxs.retain(|&i| deleted[i] == 0);
2275 idxs.sort_unstable();
2276 Ok(idxs)
2277 }
2278
2279 pub fn range_row_ids_visible_i64(
2288 &mut self,
2289 column_id: u16,
2290 lo: i64,
2291 hi: i64,
2292 snapshot: Epoch,
2293 ) -> Result<Vec<u64>> {
2294 let stats: Vec<(Option<i64>, Option<i64>, usize)> = match self.column_page_stats(column_id)
2295 {
2296 Some(s) => s
2297 .iter()
2298 .map(|st| {
2299 (
2300 be_i64(st.min.as_deref()),
2301 be_i64(st.max.as_deref()),
2302 st.row_count as usize,
2303 )
2304 })
2305 .collect(),
2306 None => return Ok(Vec::new()),
2307 };
2308 let col_encrypted = self.col_encrypted(column_id);
2310 let (positions, rids) = self.visible_positions_with_rids(snapshot)?;
2311 let mut out: Vec<u64> = Vec::new();
2312 let mut vis = 0usize;
2313 let mut page_start = 0usize;
2314 for (seq, &(mn, mx, nrows)) in stats.iter().enumerate() {
2315 let page_end = page_start + nrows;
2316 let skip = !col_encrypted
2318 && match (mn, mx) {
2319 (Some(mn), Some(mx)) => mx < lo || mn > hi,
2320 _ => true, };
2322 if !skip {
2323 let val_page = self.read_page(column_id, seq)?;
2324 let vals = columnar::decode_page_native(TypeId::Int64, &val_page, nrows)?;
2325 if let columnar::NativeColumn::Int64 { data: v, validity } = vals {
2326 while vis < positions.len() && positions[vis] < page_end {
2327 let local = positions[vis] - page_start;
2328 if columnar::validity_bit(&validity, local)
2329 && v[local] >= lo
2330 && v[local] <= hi
2331 {
2332 out.push(rids[vis] as u64);
2333 }
2334 vis += 1;
2335 }
2336 }
2337 } else {
2338 while vis < positions.len() && positions[vis] < page_end {
2339 vis += 1;
2340 }
2341 }
2342 page_start = page_end;
2343 }
2344 Ok(out)
2345 }
2346
2347 pub fn range_row_ids_visible_f64(
2350 &mut self,
2351 column_id: u16,
2352 lo: f64,
2353 lo_inclusive: bool,
2354 hi: f64,
2355 hi_inclusive: bool,
2356 snapshot: Epoch,
2357 ) -> Result<Vec<u64>> {
2358 let stats: Vec<(Option<f64>, Option<f64>, usize)> = match self.column_page_stats(column_id)
2359 {
2360 Some(s) => s
2361 .iter()
2362 .map(|st| {
2363 (
2364 be_f64(st.min.as_deref()),
2365 be_f64(st.max.as_deref()),
2366 st.row_count as usize,
2367 )
2368 })
2369 .collect(),
2370 None => return Ok(Vec::new()),
2371 };
2372 let col_encrypted = self.col_encrypted(column_id);
2374 let (positions, rids) = self.visible_positions_with_rids(snapshot)?;
2375 let mut out: Vec<u64> = Vec::new();
2376 let mut vis = 0usize;
2377 let mut page_start = 0usize;
2378 for (seq, &(mn, mx, nrows)) in stats.iter().enumerate() {
2379 let page_end = page_start + nrows;
2380 let skip = !col_encrypted
2381 && match (mn, mx) {
2382 (Some(mn), Some(mx)) => {
2383 let skip_lo = mx < lo || (!lo_inclusive && mx == lo);
2384 let skip_hi = mn > hi || (!hi_inclusive && mn == hi);
2385 skip_lo || skip_hi
2386 }
2387 _ => true,
2388 };
2389 if !skip {
2390 let val_page = self.read_page(column_id, seq)?;
2391 let vals = columnar::decode_page_native(TypeId::Float64, &val_page, nrows)?;
2392 if let columnar::NativeColumn::Float64 { data: v, validity } = vals {
2393 while vis < positions.len() && positions[vis] < page_end {
2394 let local = positions[vis] - page_start;
2395 if columnar::validity_bit(&validity, local) && !v[local].is_nan() {
2396 let val = v[local];
2397 let ok_lo = if lo_inclusive { val >= lo } else { val > lo };
2398 let ok_hi = if hi_inclusive { val <= hi } else { val < hi };
2399 if ok_lo && ok_hi {
2400 out.push(rids[vis] as u64);
2401 }
2402 }
2403 vis += 1;
2404 }
2405 }
2406 } else {
2407 while vis < positions.len() && positions[vis] < page_end {
2408 vis += 1;
2409 }
2410 }
2411 page_start = page_end;
2412 }
2413 Ok(out)
2414 }
2415
2416 pub fn null_row_ids_visible(
2422 &mut self,
2423 column_id: u16,
2424 want_nulls: bool,
2425 snapshot: Epoch,
2426 ) -> Result<Vec<u64>> {
2427 let stats: Vec<(usize, usize)> = match self.column_page_stats(column_id) {
2428 Some(s) => s
2429 .iter()
2430 .map(|st| (st.null_count as usize, st.row_count as usize))
2431 .collect(),
2432 None => return Ok(Vec::new()),
2433 };
2434 let ty = self.resolve_type(column_id);
2435 let (positions, rids) = self.visible_positions_with_rids(snapshot)?;
2436 let mut out: Vec<u64> = Vec::new();
2437 let mut vis = 0usize;
2438 let mut page_start = 0usize;
2439 for (seq, &(null_count, nrows)) in stats.iter().enumerate() {
2440 let page_end = page_start + nrows;
2441 let skip = (want_nulls && null_count == 0) || (!want_nulls && null_count == nrows);
2442 if !skip {
2443 let val_page = self.read_page(column_id, seq)?;
2444 let col = columnar::decode_page_native(ty, &val_page, nrows)?;
2445 let validity = col.validity();
2446 while vis < positions.len() && positions[vis] < page_end {
2447 let local = positions[vis] - page_start;
2448 let is_null = !columnar::validity_bit(validity, local);
2449 if is_null == want_nulls {
2450 out.push(rids[vis] as u64);
2451 }
2452 vis += 1;
2453 }
2454 } else {
2455 while vis < positions.len() && positions[vis] < page_end {
2456 vis += 1;
2457 }
2458 }
2459 page_start = page_end;
2460 }
2461 Ok(out)
2462 }
2463
2464 pub fn visible_positions_with_rids(
2468 &mut self,
2469 snapshot: Epoch,
2470 ) -> Result<(Vec<usize>, Vec<i64>)> {
2471 let n = self.row_count();
2472 if n == 0 {
2473 return Ok((Vec::new(), Vec::new()));
2474 }
2475 if self.is_clean() && self.epoch_override.is_none() {
2484 let row_ids = match self.column_native_shared(SYS_ROW_ID)? {
2485 columnar::NativeColumn::Int64 { data, .. } => data,
2486 _ => return Err(MongrelError::InvalidArgument("sys row_id not int64".into())),
2487 };
2488 let positions: Vec<usize> = (0..n).collect();
2489 return Ok((positions, row_ids));
2490 }
2491 let (row_ids, epochs, deleted) = self.system_columns_native()?;
2492 let mut idxs: Vec<usize> = Vec::new();
2503 let mut i = 0;
2504 while i < n {
2505 let rid = row_ids[i] as u64;
2506 let mut best: Option<usize> = None;
2509 let mut j = i;
2510 while j < n && row_ids[j] as u64 == rid {
2511 if epochs[j] as u64 <= snapshot.0 {
2512 best = Some(j);
2513 }
2514 j += 1;
2515 }
2516 if let Some(b) = best {
2517 if deleted[b] == 0 {
2518 idxs.push(b);
2519 }
2520 }
2521 i = j;
2522 }
2523 let rids: Vec<i64> = idxs.iter().map(|&k| row_ids[k]).collect();
2525 Ok((idxs, rids))
2526 }
2527
2528 pub fn page_row_counts(&self, column_id: u16) -> Result<Vec<usize>> {
2532 Ok(self
2533 .find_header(column_id)?
2534 .page_stats
2535 .iter()
2536 .map(|s| s.row_count as usize)
2537 .collect())
2538 }
2539
2540 pub fn has_column(&self, column_id: u16) -> bool {
2543 self.dir.iter().any(|h| h.column_id == column_id)
2544 }
2545
2546 pub fn column_page_stats(&self, column_id: u16) -> Option<&[crate::page::PageStat]> {
2550 self.dir
2551 .iter()
2552 .find(|h| h.column_id == column_id)
2553 .map(|ch| ch.page_stats.as_slice())
2554 }
2555
2556 pub(crate) fn system_columns_native(&mut self) -> Result<(Vec<i64>, Vec<i64>, Vec<u8>)> {
2561 let row_ids = match self.column_native_shared(SYS_ROW_ID)? {
2562 columnar::NativeColumn::Int64 { data, .. } => data,
2563 _ => return Err(MongrelError::InvalidArgument("sys row_id not int64".into())),
2564 };
2565 let epochs = match self.column_native_shared(SYS_EPOCH)? {
2566 columnar::NativeColumn::Int64 { data, .. } => data,
2567 _ => return Err(MongrelError::InvalidArgument("sys epoch not int64".into())),
2568 };
2569 let deleted = match self.column_native_shared(SYS_DELETED)? {
2570 columnar::NativeColumn::Bool { data, .. } => data,
2571 _ => return Err(MongrelError::InvalidArgument("sys deleted not bool".into())),
2572 };
2573 Ok((row_ids, epochs, deleted))
2574 }
2575
2576 pub fn visible_versions(&mut self, snapshot: Epoch) -> Result<Vec<Row>> {
2580 let n = self.row_count();
2581 if n == 0 {
2582 return Ok(Vec::new());
2583 }
2584 let row_ids = self.column(SYS_ROW_ID)?.to_vec();
2585 let epochs = self.column(SYS_EPOCH)?.to_vec();
2586 let mut best: HashMap<u64, (u64, usize)> = HashMap::new();
2587 for i in 0..n {
2588 let rid = int_at(&row_ids, i);
2589 let epoch = int_at(&epochs, i);
2590 if epoch > snapshot.0 {
2591 continue;
2592 }
2593 best.entry(rid)
2594 .and_modify(|e| {
2595 if epoch > e.0 {
2596 *e = (epoch, i);
2597 }
2598 })
2599 .or_insert((epoch, i));
2600 }
2601 let mut picks: Vec<usize> = best.into_values().map(|(_, i)| i).collect();
2602 picks.sort();
2603 let mut out = Vec::with_capacity(picks.len());
2604 for i in picks {
2605 out.push(self.materialize(i)?);
2606 }
2607 Ok(out)
2608 }
2609
2610 pub fn visible_rows(&mut self, snapshot: Epoch) -> Result<Vec<Row>> {
2612 Ok(self
2613 .visible_versions(snapshot)?
2614 .into_iter()
2615 .filter(|r| !r.deleted)
2616 .collect())
2617 }
2618
2619 fn predict_window(&self, target: u64, n: usize) -> std::ops::Range<usize> {
2620 let Some(idx) = &self.learned else {
2621 return 0..n;
2622 };
2623 let (lo, hi) = idx.predict(target);
2624 let lo = lo.min(n);
2625 let hi = if hi == usize::MAX { n } else { hi.min(n) };
2626 lo..hi
2627 }
2628
2629 pub(crate) fn materialize(&mut self, index: usize) -> Result<Row> {
2630 let row_id = RowId(int_at(self.column(SYS_ROW_ID)?, index));
2631 let epoch = Epoch(int_at(self.column(SYS_EPOCH)?, index));
2632 let deleted = bool_at(self.column(SYS_DELETED)?, index);
2633 let col_ids: Vec<u16> = self.schema.columns.iter().map(|c| c.id).collect();
2634 let mut columns = HashMap::new();
2635 for id in col_ids {
2636 let val = if self.dir.iter().any(|h| h.column_id == id) {
2637 self.column(id)?.get(index).cloned().unwrap_or(Value::Null)
2638 } else {
2639 Value::Null
2641 };
2642 columns.insert(id, val);
2643 }
2644 Ok(Row {
2645 row_id,
2646 committed_epoch: epoch,
2647 columns,
2648 deleted,
2649 })
2650 }
2651
2652 pub(crate) fn materialize_batch(&mut self, indices: &[usize]) -> Result<Vec<Row>> {
2661 if indices.is_empty() {
2662 return Ok(Vec::new());
2663 }
2664 use std::collections::HashMap;
2665 let rid_col = self.column_native_shared(SYS_ROW_ID)?;
2666 let epoch_col = self.column_native_shared(SYS_EPOCH)?;
2667 let del_col = self.column_native_shared(SYS_DELETED)?;
2668 let mut user: HashMap<u16, columnar::NativeColumn> = HashMap::new();
2669 let present: Vec<u16> = self
2670 .schema
2671 .columns
2672 .iter()
2673 .map(|c| c.id)
2674 .filter(|id| self.dir.iter().any(|h| h.column_id == *id))
2675 .collect();
2676 for id in present {
2677 user.insert(id, self.column_native(id)?);
2678 }
2679 let i64_at = |col: &columnar::NativeColumn, i: usize| -> i64 {
2680 match col {
2681 columnar::NativeColumn::Int64 { data, .. } => data.get(i).copied().unwrap_or(0),
2682 _ => 0,
2683 }
2684 };
2685 let bool_at_native = |col: &columnar::NativeColumn, i: usize| -> bool {
2686 match col {
2687 columnar::NativeColumn::Bool { data, .. } => {
2688 data.get(i).copied().map(|b| b != 0).unwrap_or(false)
2689 }
2690 _ => false,
2691 }
2692 };
2693 let mut rows = Vec::with_capacity(indices.len());
2694 for &idx in indices {
2695 let row_id = RowId(i64_at(&rid_col, idx) as u64);
2696 let epoch = Epoch(i64_at(&epoch_col, idx) as u64);
2697 let deleted = bool_at_native(&del_col, idx);
2698 let mut columns = HashMap::with_capacity(self.schema.columns.len());
2699 for cdef in self.schema.columns.iter() {
2700 let val = match user.get(&cdef.id) {
2701 Some(col) => col.value_at(idx).unwrap_or(Value::Null),
2702 None => Value::Null,
2703 };
2704 columns.insert(cdef.id, val);
2705 }
2706 rows.push(Row {
2707 row_id,
2708 committed_epoch: epoch,
2709 columns,
2710 deleted,
2711 });
2712 }
2713 Ok(rows)
2714 }
2715}
2716
2717fn read_learned(path: &Path, header: &RunHeader) -> Result<Option<PgmIndex>> {
2718 if header.index_trailer_offset == 0 {
2719 return Ok(None);
2720 }
2721 let mut file = File::open(path)?;
2722 file.seek(SeekFrom::Start(header.index_trailer_offset))?;
2723 let len = header
2724 .footer_offset
2725 .saturating_sub(header.index_trailer_offset);
2726 let mut buf = vec![0u8; len as usize];
2727 file.read_exact(&mut buf)?;
2728 let pgm: PgmIndex = bincode::deserialize(&buf)
2729 .map_err(|e| MongrelError::InvalidArgument(format!("bad learned trailer: {e}")))?;
2730 Ok(Some(pgm))
2731}
2732
2733fn int_at(vals: &[Value], i: usize) -> u64 {
2734 match vals.get(i) {
2735 Some(Value::Int64(x)) => *x as u64,
2736 _ => 0,
2737 }
2738}
2739
2740fn bool_at(vals: &[Value], i: usize) -> bool {
2741 matches!(vals.get(i), Some(Value::Bool(true)))
2742}
2743
2744fn value_row_id(v: &Value) -> u64 {
2745 match v {
2746 Value::Int64(x) => *x as u64,
2747 _ => u64::MAX,
2748 }
2749}
2750
2751#[cfg(test)]
2752mod tests {
2753 use super::*;
2754 use crate::columnar::NativeColumn;
2755 use crate::memtable::Value;
2756 use crate::rowid::RowId;
2757 use crate::schema::{ColumnDef, ColumnFlags};
2758 use tempfile::tempdir;
2759
2760 fn schema() -> Schema {
2761 Schema {
2762 schema_id: 1,
2763 columns: vec![
2764 ColumnDef {
2765 id: 1,
2766 name: "id".into(),
2767 ty: TypeId::Int64,
2768 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
2769 },
2770 ColumnDef {
2771 id: 2,
2772 name: "name".into(),
2773 ty: TypeId::Bytes,
2774 flags: ColumnFlags::empty(),
2775 },
2776 ],
2777 indexes: Vec::new(),
2778 colocation: vec![],
2779 constraints: Default::default(),
2780 }
2781 }
2782
2783 fn rows() -> Vec<Row> {
2784 vec![
2785 Row::new(RowId(1), Epoch(10))
2786 .with_column(1, Value::Int64(1))
2787 .with_column(2, Value::Bytes(b"alice".to_vec())),
2788 Row::new(RowId(2), Epoch(10))
2789 .with_column(1, Value::Int64(2))
2790 .with_column(2, Value::Bytes(b"bob".to_vec())),
2791 Row::new(RowId(3), Epoch(10))
2792 .with_column(1, Value::Int64(3))
2793 .with_column(2, Value::Bytes(b"carol".to_vec())),
2794 ]
2795 }
2796
2797 #[test]
2798 fn flush_then_read_mvcc() {
2799 let dir = tempdir().unwrap();
2800 let path = dir.path().join("r-1.sr");
2801 let header = RunWriter::new(&schema(), 1, Epoch(10), 0)
2802 .write(&path, &rows())
2803 .unwrap();
2804 assert_eq!(header.row_count, 3);
2805
2806 let mut r = RunReader::open(&path, schema(), None).unwrap();
2807 let (e, row) = r.get_version(RowId(2), Epoch(20)).unwrap().unwrap();
2809 assert_eq!(e, Epoch(10));
2810 assert_eq!(row.row_id, RowId(2));
2811 assert!(matches!(row.columns.get(&2), Some(Value::Bytes(_))));
2812 assert!(r.get_version(RowId(99), Epoch(20)).unwrap().is_none());
2814 let all = r.visible_rows(Epoch(20)).unwrap();
2816 assert_eq!(all.len(), 3);
2817 }
2818
2819 #[test]
2820 fn learned_index_was_stored() {
2821 let dir = tempdir().unwrap();
2822 let path = dir.path().join("r-2.sr");
2823 RunWriter::new(&schema(), 2, Epoch(1), 0)
2824 .write(&path, &rows())
2825 .unwrap();
2826 let header = read_header(&path).unwrap();
2827 assert!(
2828 header.index_trailer_offset != 0,
2829 "trailer should be present"
2830 );
2831 }
2832
2833 #[test]
2834 fn low_level_container_still_round_trips() {
2835 let dir = tempdir().unwrap();
2836 let path = dir.path().join("r-3.sr");
2837 let cols = vec![ColumnPayload {
2838 column_id: 1,
2839 type_id_tag: 8,
2840 encoding: Encoding::Plain,
2841 pages: vec![vec![1, 2, 3, 4]],
2842 page_stats: Vec::new(),
2843 }];
2844 let header = write_run(
2845 &path,
2846 &RunSpec {
2847 run_id: 1,
2848 schema_id: 1,
2849 epoch_created: 1,
2850 level: 0,
2851 flags: 0,
2852 sort_key_column_id: SORT_KEY_ROW_ID,
2853 row_count: 1,
2854 min_row_id: 0,
2855 max_row_id: 0,
2856 columns: &cols,
2857 },
2858 )
2859 .unwrap();
2860 let back = read_header(&path).unwrap();
2861 assert_eq!(back.content_hash, header.content_hash);
2862 assert_eq!(read_column_dir(&path, &back).unwrap().len(), 1);
2863 }
2864
2865 #[test]
2866 fn detects_corruption() {
2867 let dir = tempdir().unwrap();
2868 let path = dir.path().join("r-4.sr");
2869 write_run(
2870 &path,
2871 &RunSpec {
2872 run_id: 1,
2873 schema_id: 1,
2874 epoch_created: 1,
2875 level: 0,
2876 flags: 0,
2877 sort_key_column_id: SORT_KEY_ROW_ID,
2878 row_count: 1,
2879 min_row_id: 0,
2880 max_row_id: 0,
2881 columns: &[ColumnPayload {
2882 column_id: 1,
2883 type_id_tag: 8,
2884 encoding: Encoding::Plain,
2885 pages: vec![vec![1, 2, 3]],
2886 page_stats: Vec::new(),
2887 }],
2888 },
2889 )
2890 .unwrap();
2891 let mut bytes = std::fs::read(&path).unwrap();
2892 bytes[300] ^= 0xFF;
2893 std::fs::write(&path, bytes).unwrap();
2894 let err = read_header(&path).unwrap_err();
2895 assert!(
2896 matches!(err, MongrelError::ChecksumMismatch { .. }),
2897 "got {err:?}"
2898 );
2899 }
2900
2901 #[test]
2907 fn mmap_and_vec_writers_are_byte_identical() {
2908 let dir = tempdir().unwrap();
2909 let stats = vec![PageStat {
2910 first_row_id: 0,
2911 last_row_id: 1,
2912 null_count: 0,
2913 row_count: 2,
2914 min: Some(vec![0]),
2915 max: Some(vec![9]),
2916 offset: 0,
2917 compressed_len: 0,
2918 uncompressed_len: 0,
2919 }];
2920 let spec = RunSpec {
2921 run_id: 7,
2922 schema_id: 1,
2923 epoch_created: 10,
2924 level: 0,
2925 flags: 0,
2926 sort_key_column_id: SORT_KEY_ROW_ID,
2927 row_count: 2,
2928 min_row_id: 0,
2929 max_row_id: 1,
2930 columns: &[
2931 ColumnPayload {
2932 column_id: 1,
2933 type_id_tag: 8,
2934 encoding: Encoding::Plain,
2935 pages: vec![vec![1, 2, 3, 4], vec![5, 6, 7, 8]],
2936 page_stats: stats.clone(),
2937 },
2938 ColumnPayload {
2939 column_id: 2,
2940 type_id_tag: 8,
2941 encoding: Encoding::Plain,
2942 pages: vec![vec![10, 20], vec![30, 40]],
2943 page_stats: stats.clone(),
2944 },
2945 ],
2946 };
2947 let trailer = b"learned-trailer-bytes";
2948
2949 let plan = plan_run(&spec, None, Some(trailer)).expect("plan");
2951 let mut buf = vec![0u8; plan.total];
2952 let h_place = place_run(&spec, None, Some(trailer), &plan, &mut buf)
2953 .expect("place_run into Vec succeeds");
2954
2955 let path_vec = dir.path().join("vec.sr");
2957 let h_vec = write_run_vec(&path_vec, &spec, None, Some(trailer)).unwrap();
2958 let bv = std::fs::read(&path_vec).unwrap();
2959
2960 assert_eq!(h_place.content_hash, h_vec.content_hash);
2961 assert_eq!(h_place.footer_offset, h_vec.footer_offset);
2962 assert_eq!(
2963 buf, bv,
2964 "place_run and write_run_vec must be byte-identical"
2965 );
2966 assert!(read_header(&path_vec).is_ok());
2967
2968 let path_mmap = dir.path().join("mmap.sr");
2972 match write_run_mmap(&path_mmap, &spec, None, Some(trailer)) {
2973 Ok(h_mmap) => {
2974 let bm = std::fs::read(&path_mmap).unwrap();
2975 assert_eq!(bm, bv, "mmap run must be byte-identical to vec run");
2976 assert_eq!(h_mmap.content_hash, h_vec.content_hash);
2977 }
2978 Err(e) if is_mmap_unavailable(&e) => {
2979 eprintln!("note: file mmap unavailable here; vec path covered (1)/(2)");
2980 }
2981 Err(e) => panic!("unexpected mmap error: {e:?}"),
2982 }
2983 }
2984
2985 #[test]
2990 fn column_native_shared_matches_column_native() {
2991 let dir = tempdir().unwrap();
2992 let path = dir.path().join("r.sr");
2993 let n = 65_536 * 2 + 9;
2996 let id_col = NativeColumn::int64_sequence(1, n);
2997 let mut offsets = vec![0u32];
2998 let mut values = Vec::new();
2999 for i in 0..n {
3000 values.extend_from_slice(format!("v{}", i % 17).as_bytes());
3001 offsets.push(values.len() as u32);
3002 }
3003 let name_col = NativeColumn::Bytes {
3004 offsets,
3005 values,
3006 validity: vec![0xFF; n.div_ceil(8)],
3007 };
3008 let header = RunWriter::new(&schema(), 9, Epoch(5), 0)
3009 .write_native(&path, &[(1, id_col), (2, name_col)], n, 1)
3010 .unwrap();
3011
3012 let mut reader =
3013 RunReader::open_with_cache(&path, schema(), None, None, None, 0).expect("open reader");
3014 assert!(reader.has_mmap(), "test env must support read-only mmap");
3015 assert_eq!(reader.row_count(), header.row_count as usize);
3016
3017 for cid in [1u16, 2] {
3018 let a = reader.column_native(cid).expect("column_native");
3019 let b = reader
3020 .column_native_shared(cid)
3021 .expect("column_native_shared");
3022 assert_eq!(a.len(), b.len(), "len mismatch col {cid}");
3023 match (&a, &b) {
3025 (
3026 NativeColumn::Int64 {
3027 data: da,
3028 validity: va,
3029 },
3030 NativeColumn::Int64 {
3031 data: db,
3032 validity: vb,
3033 },
3034 ) => {
3035 assert_eq!(da, db, "Int64 data col {cid}");
3036 assert_eq!(va, vb, "Int64 validity col {cid}");
3037 }
3038 (
3039 NativeColumn::Bytes {
3040 offsets: oa,
3041 values: ua,
3042 validity: va,
3043 },
3044 NativeColumn::Bytes {
3045 offsets: ob,
3046 values: ub,
3047 validity: vb,
3048 },
3049 ) => {
3050 assert_eq!(oa, ob, "Bytes offsets col {cid}");
3051 assert_eq!(ua, ub, "Bytes values col {cid}");
3052 assert_eq!(va, vb, "Bytes validity col {cid}");
3053 }
3054 _ => panic!("type mismatch col {cid}: {a:?} vs {b:?}"),
3055 }
3056 }
3057 }
3058
3059 #[test]
3060 fn page_cache_key_distinguishes_tables() {
3061 let a = page_cache_key(1, 5, 2, 3);
3062 let b = page_cache_key(2, 5, 2, 3);
3063 assert_ne!(a, b, "same run/col/page, different table must differ");
3064 assert_ne!(page_cache_key(1, 5, 2, 3), page_cache_key(1, 6, 2, 3));
3066 assert_ne!(page_cache_key(1, 5, 2, 3), page_cache_key(1, 5, 2, 4));
3067 }
3068}