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, PathBuf};
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;
36const MAX_RUN_PAGE_BYTES: u64 = 64 * 1024 * 1024 + 32;
37
38const ENC_STATS_NONCE_COLUMN: u16 = u16::MAX;
43const ENC_STATS_NONCE_SEQ: u32 = u16::MAX as u32;
44
45type PageMinMax = (Option<Vec<u8>>, Option<Vec<u8>>);
47
48type EncryptedColumnStats = Vec<(u16, Vec<PageMinMax>)>;
55
56const GCM_TAG_LEN: usize = 16;
61
62fn on_disk_len(page_len: usize, encrypted: bool) -> usize {
65 if encrypted {
66 page_len + GCM_TAG_LEN
67 } else {
68 page_len
69 }
70}
71
72pub const RUN_FLAG_ENCRYPTED: u8 = 1 << 0;
73pub const RUN_FLAG_TOMBSTONE_ONLY: u8 = 1 << 1;
74pub const RUN_FLAG_CLEAN: u8 = 1 << 2;
83pub const RUN_FLAG_UNIFORM_EPOCH: u8 = 1 << 3;
91pub const SORT_KEY_ROW_ID: u16 = 0xFFFF;
92
93pub const SYS_ROW_ID: u16 = 0xFFFE;
95pub const SYS_EPOCH: u16 = 0xFFFD;
96pub const SYS_DELETED: u16 = 0xFFFC;
97
98const LEARNED_EPSILON: usize = 64;
101
102fn build_learned_trailer(row_ids: &[Value]) -> Vec<u8> {
106 let points: Vec<(u64, usize)> = row_ids
107 .iter()
108 .enumerate()
109 .filter_map(|(i, v)| match v {
110 Value::Int64(r) => Some((*r as u64, i)),
111 _ => None,
112 })
113 .collect();
114 let pgm = PgmIndex::build(&points, LEARNED_EPSILON);
115 bincode::serialize(&pgm).expect("pgm serialize")
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct RunHeader {
120 pub magic: [u8; 8],
121 pub format_version: u16,
122 pub header_layout_version: u16,
123 pub run_id: u128,
124 pub content_hash: [u8; 32],
125 pub schema_id: u64,
126 pub epoch_created: u64,
127 pub level: u8,
128 pub flags: u8,
129 pub sort_key_column_id: u16,
130 pub row_count: u64,
131 pub min_row_id: u64,
132 pub max_row_id: u64,
133 pub column_count: u64,
134 pub column_dir_offset: u64,
135 pub index_trailer_offset: u64,
136 pub encryption_descriptor_offset: u64,
137 pub footer_offset: u64,
138 pub encrypted_stats_offset: u64,
142 pub encrypted_stats_len: u64,
143}
144
145impl RunHeader {
146 pub fn is_encrypted(&self) -> bool {
147 self.flags & RUN_FLAG_ENCRYPTED != 0
148 }
149 pub fn is_clean(&self) -> bool {
150 self.flags & RUN_FLAG_CLEAN != 0
151 }
152 pub fn is_uniform_epoch(&self) -> bool {
153 self.flags & RUN_FLAG_UNIFORM_EPOCH != 0
154 }
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct ColumnPageHeader {
159 pub column_id: u16,
160 pub type_id_tag: u16,
161 pub encoding: u8,
162 pub flags: u8,
163 pub page_count: u32,
164 pub page_region_offset: u64,
165 pub page_region_len: u64,
166 pub page_stats: Vec<PageStat>,
167}
168
169impl ColumnPageHeader {
170 const PAGE_ENCRYPTED: u8 = 1 << 0;
171}
172
173const RUN_MAC_LEN: usize = 32;
176
177fn compute_run_mac(
181 enc: Option<&RunEncryption>,
182 header_bytes: &[u8],
183 dir_bytes: &[u8],
184) -> Option<[u8; RUN_MAC_LEN]> {
185 #[cfg(feature = "encryption")]
186 {
187 if let Some(e) = enc {
188 if let Some(mac_key) = &e.mac_key {
189 return Some(crate::encryption::run_metadata_mac(
190 mac_key,
191 header_bytes,
192 dir_bytes,
193 &e.descriptor_bytes,
194 ));
195 }
196 }
197 }
198 #[cfg(not(feature = "encryption"))]
199 {
200 let _ = (enc, header_bytes, dir_bytes);
201 }
202 None
203}
204
205pub struct ColumnPayload {
207 pub column_id: u16,
208 pub type_id_tag: u16,
209 pub encoding: Encoding,
210 pub pages: Vec<Vec<u8>>,
211 pub page_stats: Vec<PageStat>,
215}
216
217pub struct RunSpec<'a> {
219 pub run_id: u128,
220 pub schema_id: u64,
221 pub epoch_created: u64,
222 pub level: u8,
223 pub flags: u8,
224 pub sort_key_column_id: u16,
225 pub row_count: u64,
226 pub min_row_id: u64,
227 pub max_row_id: u64,
228 pub columns: &'a [ColumnPayload],
229}
230
231pub fn write_run(path: impl AsRef<Path>, spec: &RunSpec) -> Result<RunHeader> {
233 write_run_with(path, spec, None, &[], None)
234}
235
236pub fn write_run_with(
246 path: impl AsRef<Path>,
247 spec: &RunSpec,
248 kek: Option<&Kek>,
249 indexable_columns: &[(u16, u8)],
250 index_trailer: Option<&[u8]>,
251) -> Result<RunHeader> {
252 let enc: Option<RunEncryption> = match kek {
256 Some(k) => Some(setup_run_encryption(k, indexable_columns)?),
257 None => None,
258 };
259 match write_run_mmap(path.as_ref(), spec, enc.as_ref(), index_trailer) {
260 Ok(h) => Ok(h),
261 Err(e) if is_mmap_unavailable(&e) => write_run_vec(path, spec, enc, index_trailer),
262 Err(e) => Err(e),
263 }
264}
265
266fn write_run_with_file(
267 mut file: File,
268 spec: &RunSpec,
269 kek: Option<&Kek>,
270 indexable_columns: &[(u16, u8)],
271 index_trailer: Option<&[u8]>,
272) -> Result<RunHeader> {
273 let enc = match kek {
274 Some(key) => Some(setup_run_encryption(key, indexable_columns)?),
275 None => None,
276 };
277 match write_run_mmap_file(&file, spec, enc.as_ref(), index_trailer) {
278 Ok(header) => Ok(header),
279 Err(error) if is_mmap_unavailable(&error) => {
280 file.set_len(0)?;
281 file.seek(SeekFrom::Start(0))?;
282 let (bytes, header) = encode_run_vec(spec, enc, index_trailer)?;
283 file.write_all(&bytes)?;
284 file.sync_all()?;
285 Ok(header)
286 }
287 Err(error) => Err(error),
288 }
289}
290
291fn is_mmap_unavailable(e: &MongrelError) -> bool {
295 matches!(e, MongrelError::InvalidArgument(m) if m.starts_with("__mmap_unavailable__:"))
296}
297
298fn write_run_mmap(
319 path: &Path,
320 spec: &RunSpec,
321 enc: Option<&RunEncryption>,
322 index_trailer: Option<&[u8]>,
323) -> Result<RunHeader> {
324 let file = OpenOptions::new().create_new(true).write(true).open(path)?;
325 let result = write_run_mmap_file(&file, spec, enc, index_trailer);
326 if result.as_ref().is_err_and(is_mmap_unavailable) {
327 drop(file);
328 let _ = std::fs::remove_file(path);
329 }
330 result
331}
332
333fn write_run_mmap_file(
334 file: &File,
335 spec: &RunSpec,
336 enc: Option<&RunEncryption>,
337 index_trailer: Option<&[u8]>,
338) -> Result<RunHeader> {
339 let plan = plan_run(spec, enc, index_trailer)?;
340 file.set_len(plan.total as u64)?;
341 let mut mmap = match unsafe { memmap2::MmapMut::map_mut(file) } {
342 Ok(m) => m,
343 Err(e) => {
344 return Err(MongrelError::InvalidArgument(format!(
345 "__mmap_unavailable__: {e}"
346 )));
347 }
348 };
349 let header = place_run(spec, enc, index_trailer, &plan, &mut mmap[..])?;
350 mmap.flush()?;
351 file.sync_all()?;
352 Ok(header)
353}
354
355struct RunPlan {
361 jobs: Vec<(usize, usize, u64, usize)>, dir_bytes: Vec<u8>,
363 encrypted: bool,
364 column_dir_offset: u64,
365 index_trailer_offset: u64,
366 encryption_descriptor_offset: u64,
367 footer_offset: u64,
368 total: usize,
369 encrypted_stats_plain: Option<Vec<u8>>,
373 encrypted_stats_offset: u64,
374 encrypted_stats_len: u64,
375}
376
377fn plan_run(
381 spec: &RunSpec,
382 enc: Option<&RunEncryption>,
383 index_trailer: Option<&[u8]>,
384) -> Result<RunPlan> {
385 let encrypted = enc.is_some();
386 let columns = spec.columns;
387 let mut jobs: Vec<(usize, usize, u64, usize)> = Vec::new();
388 let mut dir: Vec<ColumnPageHeader> = Vec::with_capacity(columns.len());
389 let mut enc_stats: EncryptedColumnStats = Vec::new();
390 let mut cursor: u64 = RUN_HEADER_PAD as u64;
391 for (ci, col) in columns.iter().enumerate() {
392 let region_offset = cursor;
393 let mut region_len = 0u64;
394 let mut stats = Vec::with_capacity(col.pages.len());
395 let mut col_minmax: Vec<PageMinMax> = Vec::new();
396 for (ps, page) in col.pages.iter().enumerate() {
397 if encrypted && ps >= u16::MAX as usize {
402 return Err(MongrelError::Full(format!(
403 "column {:#x} exceeds 65534 pages; encrypted-run page-seq nonce space exhausted",
404 col.column_id
405 )));
406 }
407 let odl = on_disk_len(page.len(), encrypted);
408 jobs.push((ci, ps, cursor, odl));
409 let mut stat = col.page_stats.get(ps).cloned().unwrap_or(PageStat {
410 first_row_id: 0,
411 last_row_id: 0,
412 null_count: 0,
413 row_count: 0,
414 min: None,
415 max: None,
416 offset: 0,
417 compressed_len: 0,
418 uncompressed_len: 0,
419 });
420 stat.offset = cursor;
421 stat.compressed_len = odl as u32;
422 stat.uncompressed_len = page.len() as u32;
423 if encrypted {
430 col_minmax.push((stat.min.take(), stat.max.take()));
431 }
432 stats.push(stat);
433 cursor += odl as u64;
434 region_len += odl as u64;
435 }
436 if col_minmax
437 .iter()
438 .any(|(mn, mx)| mn.is_some() || mx.is_some())
439 {
440 enc_stats.push((col.column_id, col_minmax));
441 }
442 let page_flags = if encrypted {
443 ColumnPageHeader::PAGE_ENCRYPTED
444 } else {
445 0
446 };
447 dir.push(ColumnPageHeader {
448 column_id: col.column_id,
449 type_id_tag: col.type_id_tag,
450 encoding: col.encoding as u8,
451 flags: page_flags,
452 page_count: col.pages.len() as u32,
453 page_region_offset: region_offset,
454 page_region_len: region_len,
455 page_stats: stats,
456 });
457 }
458 let dir_bytes = bincode::serialize(&dir)?;
459 let column_dir_offset = cursor;
460 cursor += dir_bytes.len() as u64;
461 let index_trailer_offset = match index_trailer {
462 Some(t) => {
463 let off = cursor;
464 cursor += t.len() as u64;
465 off
466 }
467 None => 0,
468 };
469 let encryption_descriptor_offset = match enc {
470 Some(e) => {
471 let off = cursor;
472 cursor += 4 + e.descriptor_bytes.len() as u64;
473 off
474 }
475 None => 0,
476 };
477 let (encrypted_stats_plain, encrypted_stats_offset, encrypted_stats_len) =
478 if encrypted && !enc_stats.is_empty() {
479 let plain = bincode::serialize(&enc_stats)?;
480 let ct_len = on_disk_len(plain.len(), true) as u64;
481 let off = cursor;
482 cursor += ct_len;
483 (Some(plain), off, ct_len)
484 } else {
485 (None, 0, 0)
486 };
487 let footer_offset = cursor;
488 let total = footer_offset as usize + 8 + 8 + 32 + if encrypted { RUN_MAC_LEN } else { 0 };
491 Ok(RunPlan {
492 jobs,
493 dir_bytes,
494 encrypted,
495 column_dir_offset,
496 index_trailer_offset,
497 encryption_descriptor_offset,
498 footer_offset,
499 total,
500 encrypted_stats_plain,
501 encrypted_stats_offset,
502 encrypted_stats_len,
503 })
504}
505
506fn place_run(
513 spec: &RunSpec,
514 enc: Option<&RunEncryption>,
515 index_trailer: Option<&[u8]>,
516 plan: &RunPlan,
517 buf: &mut [u8],
518) -> Result<RunHeader> {
519 use rayon::prelude::*;
520 use std::borrow::Cow;
521 debug_assert_eq!(
522 buf.len(),
523 plan.total,
524 "place_run: buffer must be exactly plan.total bytes"
525 );
526
527 let cipher: Option<&dyn Cipher> = enc.map(|e| e.cipher.as_ref());
528 let nonce_prefix = enc.map(|e| e.nonce_prefix);
529 let columns = spec.columns;
530
531 struct SyncPtr(*mut u8);
536 unsafe impl Send for SyncPtr {}
537 unsafe impl Sync for SyncPtr {}
538 impl SyncPtr {
539 fn get(&self) -> *mut u8 {
540 self.0
541 }
542 }
543 let base = SyncPtr(buf.as_mut_ptr());
544 plan.jobs
545 .par_iter()
546 .map(
547 |&(ci, ps, offset, odl): &(usize, usize, u64, usize)| -> Result<()> {
548 let page = &columns[ci].pages[ps];
549 let dst =
550 unsafe { std::slice::from_raw_parts_mut(base.get().add(offset as usize), odl) };
551 let bytes: Cow<[u8]> = match cipher {
552 Some(c) => {
553 let nonce =
554 page_nonce(nonce_prefix.unwrap(), columns[ci].column_id, ps as u32);
555 let ct = c.encrypt_page(&nonce, page)?;
556 assert_eq!(
559 ct.len(), odl,
560 "ciphertext length {} != predicted {}; GCM tag size assumption is stale",
561 ct.len(), odl
562 );
563 Cow::Owned(ct)
564 }
565 None => {
566 debug_assert_eq!(page.len(), odl);
567 Cow::Borrowed(page.as_slice())
568 }
569 };
570 dst.copy_from_slice(&bytes);
571 Ok(())
572 },
573 )
574 .collect::<Result<()>>()?;
575
576 let content_hash = {
578 let mut h = Sha256::new();
579 h.update(&buf[RUN_HEADER_PAD..plan.column_dir_offset as usize]);
580 h.finalize()
581 };
582
583 let doff = plan.column_dir_offset as usize;
585 buf[doff..doff + plan.dir_bytes.len()].copy_from_slice(&plan.dir_bytes);
586 if let Some(t) = index_trailer {
587 let off = plan.index_trailer_offset as usize;
588 buf[off..off + t.len()].copy_from_slice(t);
589 }
590 if let Some(e) = enc {
591 let off = plan.encryption_descriptor_offset as usize;
592 buf[off..off + 4].copy_from_slice(&(e.descriptor_bytes.len() as u32).to_le_bytes());
593 buf[off + 4..off + 4 + e.descriptor_bytes.len()].copy_from_slice(&e.descriptor_bytes);
594 }
595 if let (Some(e), Some(plain)) = (enc, plan.encrypted_stats_plain.as_ref()) {
596 let nonce = page_nonce(e.nonce_prefix, ENC_STATS_NONCE_COLUMN, ENC_STATS_NONCE_SEQ);
597 let ct = e.cipher.encrypt_page(&nonce, plain)?;
598 debug_assert_eq!(ct.len() as u64, plan.encrypted_stats_len);
599 let off = plan.encrypted_stats_offset as usize;
600 buf[off..off + ct.len()].copy_from_slice(&ct);
601 }
602
603 let header_flags = if plan.encrypted {
605 spec.flags | RUN_FLAG_ENCRYPTED
606 } else {
607 spec.flags
608 };
609 let header = RunHeader {
610 magic: RUN_MAGIC,
611 format_version: RUN_FORMAT_VERSION,
612 header_layout_version: RUN_HEADER_VERSION,
613 run_id: spec.run_id,
614 content_hash: content_hash.into(),
615 schema_id: spec.schema_id,
616 epoch_created: spec.epoch_created,
617 level: spec.level,
618 flags: header_flags,
619 sort_key_column_id: spec.sort_key_column_id,
620 row_count: spec.row_count,
621 min_row_id: spec.min_row_id,
622 max_row_id: spec.max_row_id,
623 column_count: columns.len() as u64,
624 column_dir_offset: plan.column_dir_offset,
625 index_trailer_offset: plan.index_trailer_offset,
626 encryption_descriptor_offset: plan.encryption_descriptor_offset,
627 footer_offset: plan.footer_offset,
628 encrypted_stats_offset: plan.encrypted_stats_offset,
629 encrypted_stats_len: plan.encrypted_stats_len,
630 };
631 let header_bytes = bincode::serialize(&header)?;
632 if header_bytes.len() > RUN_HEADER_PAD {
633 return Err(MongrelError::InvalidArgument(format!(
634 "run header too large: {} > {RUN_HEADER_PAD}",
635 header_bytes.len()
636 )));
637 }
638 buf[..header_bytes.len()].copy_from_slice(&header_bytes);
639
640 let checksum = Sha256::digest(&buf[..plan.footer_offset as usize]);
641 let foot = plan.footer_offset as usize;
642 buf[foot..foot + 8].copy_from_slice(&RUN_MAGIC);
643 buf[foot + 8..foot + 16].copy_from_slice(&plan.footer_offset.to_le_bytes());
644 buf[foot + 16..foot + 48].copy_from_slice(&checksum);
645 if let Some(tag) = compute_run_mac(enc, &header_bytes, &plan.dir_bytes) {
647 buf[foot + 48..foot + 48 + RUN_MAC_LEN].copy_from_slice(&tag);
648 }
649 Ok(header)
650}
651
652fn write_run_vec(
655 path: impl AsRef<Path>,
656 spec: &RunSpec,
657 enc: Option<RunEncryption>,
658 index_trailer: Option<&[u8]>,
659) -> Result<RunHeader> {
660 let (buf, header) = encode_run_vec(spec, enc, index_trailer)?;
661 let mut file = OpenOptions::new().create_new(true).write(true).open(path)?;
662 file.write_all(&buf)?;
663 file.sync_all()?;
664 Ok(header)
665}
666
667fn encode_run_vec(
668 spec: &RunSpec,
669 enc: Option<RunEncryption>,
670 index_trailer: Option<&[u8]>,
671) -> Result<(Vec<u8>, RunHeader)> {
672 let mut buf: Vec<u8> = vec![0; RUN_HEADER_PAD]; let mut content_hasher = Sha256::new();
674 let mut dir: Vec<ColumnPageHeader> = Vec::with_capacity(spec.columns.len());
675 let mut enc_stats: EncryptedColumnStats = Vec::new();
676
677 for col in spec.columns {
678 let region_offset = buf.len() as u64;
679 let mut region_len = 0u64;
680 let mut stats = Vec::with_capacity(col.pages.len());
681 let mut col_minmax: Vec<PageMinMax> = Vec::new();
682 for (page_seq, page) in col.pages.iter().enumerate() {
683 if enc.is_some() && page_seq >= u16::MAX as usize {
684 return Err(MongrelError::Full(format!(
685 "column {:#x} exceeds 65534 pages; encrypted-run page-seq nonce space exhausted",
686 col.column_id
687 )));
688 }
689 let on_disk: Vec<u8> = match &enc {
690 Some(e) => e.cipher.encrypt_page(
691 &page_nonce(e.nonce_prefix, col.column_id, page_seq as u32),
692 page,
693 )?,
694 None => page.clone(),
695 };
696 let offset = buf.len() as u64;
697 buf.write_all(&on_disk)?;
698 content_hasher.update(&on_disk);
699 region_len += on_disk.len() as u64;
700 let mut stat = if let Some(s) = col.page_stats.get(page_seq) {
701 s.clone()
702 } else {
703 PageStat {
704 first_row_id: 0,
705 last_row_id: 0,
706 null_count: 0,
707 row_count: 0,
708 min: None,
709 max: None,
710 offset: 0,
711 compressed_len: 0,
712 uncompressed_len: 0,
713 }
714 };
715 stat.offset = offset;
716 stat.compressed_len = on_disk.len() as u32;
717 stat.uncompressed_len = page.len() as u32;
718 if enc.is_some() {
723 col_minmax.push((stat.min.take(), stat.max.take()));
724 }
725 stats.push(stat);
726 }
727 if col_minmax
728 .iter()
729 .any(|(mn, mx)| mn.is_some() || mx.is_some())
730 {
731 enc_stats.push((col.column_id, col_minmax));
732 }
733 let page_flags = if enc.is_some() {
734 ColumnPageHeader::PAGE_ENCRYPTED
735 } else {
736 0
737 };
738 dir.push(ColumnPageHeader {
739 column_id: col.column_id,
740 type_id_tag: col.type_id_tag,
741 encoding: col.encoding as u8,
742 flags: page_flags,
743 page_count: col.pages.len() as u32,
744 page_region_offset: region_offset,
745 page_region_len: region_len,
746 page_stats: stats,
747 });
748 }
749
750 let column_dir_offset = buf.len() as u64;
751 let dir_bytes = bincode::serialize(&dir)?;
752 buf.write_all(&dir_bytes)?;
753
754 let index_trailer_offset = match index_trailer {
755 Some(trailer) => {
756 let off = buf.len() as u64;
757 buf.write_all(trailer)?;
758 off
759 }
760 None => 0,
761 };
762 let encryption_descriptor_offset = match &enc {
763 Some(e) => {
764 let off = buf.len() as u64;
765 buf.write_all(&(e.descriptor_bytes.len() as u32).to_le_bytes())?;
766 buf.write_all(&e.descriptor_bytes)?;
767 off
768 }
769 None => 0,
770 };
771 let (encrypted_stats_offset, encrypted_stats_len) = match &enc {
772 Some(e) if !enc_stats.is_empty() => {
773 let plain = bincode::serialize(&enc_stats)?;
774 let nonce = page_nonce(e.nonce_prefix, ENC_STATS_NONCE_COLUMN, ENC_STATS_NONCE_SEQ);
775 let ct = e.cipher.encrypt_page(&nonce, &plain)?;
776 let off = buf.len() as u64;
777 let len = ct.len() as u64;
778 buf.write_all(&ct)?;
779 (off, len)
780 }
781 _ => (0, 0),
782 };
783 let footer_offset = buf.len() as u64;
784
785 let header_flags = if enc.is_some() {
786 spec.flags | RUN_FLAG_ENCRYPTED
787 } else {
788 spec.flags
789 };
790 let header = RunHeader {
791 magic: RUN_MAGIC,
792 format_version: RUN_FORMAT_VERSION,
793 header_layout_version: RUN_HEADER_VERSION,
794 run_id: spec.run_id,
795 content_hash: content_hasher.finalize().into(),
796 schema_id: spec.schema_id,
797 epoch_created: spec.epoch_created,
798 level: spec.level,
799 flags: header_flags,
800 sort_key_column_id: spec.sort_key_column_id,
801 row_count: spec.row_count,
802 min_row_id: spec.min_row_id,
803 max_row_id: spec.max_row_id,
804 column_count: spec.columns.len() as u64,
805 column_dir_offset,
806 index_trailer_offset,
807 encryption_descriptor_offset,
808 footer_offset,
809 encrypted_stats_offset,
810 encrypted_stats_len,
811 };
812 let header_bytes = bincode::serialize(&header)?;
813 if header_bytes.len() > RUN_HEADER_PAD {
814 return Err(MongrelError::InvalidArgument(format!(
815 "run header too large: {} > {RUN_HEADER_PAD}",
816 header_bytes.len()
817 )));
818 }
819 buf[..header_bytes.len()].copy_from_slice(&header_bytes);
820
821 let checksum = Sha256::digest(&buf[..footer_offset as usize]);
822 buf.write_all(&RUN_MAGIC)?;
823 buf.write_all(&footer_offset.to_le_bytes())?;
824 buf.write_all(&checksum)?;
825 if let Some(tag) = compute_run_mac(enc.as_ref(), &header_bytes, &dir_bytes) {
828 buf.write_all(&tag)?;
829 }
830
831 Ok((buf, header))
832}
833
834fn page_nonce(nonce_prefix: [u8; 12], column_id: u16, page_seq: u32) -> [u8; 12] {
835 let mut n = nonce_prefix;
836 n[8..10].copy_from_slice(&column_id.to_le_bytes());
837 n[10..12].copy_from_slice(&(page_seq as u16).to_le_bytes());
838 n
839}
840
841fn overlay_encrypted_stats(
848 file: &mut File,
849 header: &RunHeader,
850 cipher: &dyn Cipher,
851 nonce_prefix: [u8; 12],
852 dir: &mut [ColumnPageHeader],
853) -> Result<()> {
854 file.seek(SeekFrom::Start(header.encrypted_stats_offset))?;
855 const MAX_ENCRYPTED_STATS_BYTES: u64 = 64 * 1024 * 1024;
856 if header.encrypted_stats_len > MAX_ENCRYPTED_STATS_BYTES {
857 return Err(MongrelError::InvalidArgument(format!(
858 "encrypted run stats length {} exceeds {MAX_ENCRYPTED_STATS_BYTES}",
859 header.encrypted_stats_len
860 )));
861 }
862 let mut ct = vec![0u8; header.encrypted_stats_len as usize];
863 file.read_exact(&mut ct)?;
864 let nonce = page_nonce(nonce_prefix, ENC_STATS_NONCE_COLUMN, ENC_STATS_NONCE_SEQ);
865 let plain = cipher.decrypt_page(&nonce, &ct)?;
866 let stats: EncryptedColumnStats = bincode::deserialize(&plain)
867 .map_err(|e| MongrelError::Encryption(format!("bad encrypted page-stats envelope: {e}")))?;
868 for (cid, minmax) in stats {
869 let Some(col) = dir.iter_mut().find(|c| c.column_id == cid) else {
870 continue;
871 };
872 for (stat, (mn, mx)) in col.page_stats.iter_mut().zip(minmax) {
873 stat.min = mn;
874 stat.max = mx;
875 }
876 }
877 Ok(())
878}
879
880pub(crate) fn page_cache_key(
887 table_id: u64,
888 run_id: u128,
889 column_id: u16,
890 page_seq: usize,
891) -> [u8; 32] {
892 let mut h = Sha256::new();
893 h.update(table_id.to_be_bytes());
894 h.update(run_id.to_be_bytes());
895 h.update(column_id.to_be_bytes());
896 h.update((page_seq as u64).to_be_bytes());
897 let out = h.finalize();
898 let mut k = [0u8; 32];
899 k.copy_from_slice(&out);
900 k
901}
902
903fn decrypt_or_passthrough(
907 cipher: Option<&dyn Cipher>,
908 nonce_prefix: [u8; 12],
909 column_id: u16,
910 page_seq: usize,
911 encrypted: bool,
912 buf: &[u8],
913) -> Result<Vec<u8>> {
914 if encrypted {
915 match cipher {
916 Some(c) => {
917 Ok(c.decrypt_page(&page_nonce(nonce_prefix, column_id, page_seq as u32), buf)?)
918 }
919 None => Err(MongrelError::Decryption(
920 "encrypted page but no cipher".into(),
921 )),
922 }
923 } else {
924 Ok(buf.to_vec())
925 }
926}
927
928fn read_header_fast_from_file(file: &mut File) -> Result<RunHeader> {
938 file.seek(SeekFrom::Start(0))?;
939 let mut header_buf = [0u8; RUN_HEADER_PAD];
940 file.read_exact(&mut header_buf)?;
941 let header: RunHeader = bincode::deserialize(&header_buf)
942 .map_err(|e| MongrelError::InvalidArgument(format!("bad run header: {e}")))?;
943 validate_run_header_bytes(&header, &header_buf)?;
944 validate_run_layout(&header, file.metadata()?.len())?;
945 file.seek(SeekFrom::Start(header.footer_offset))?;
946 let mut footer_magic = [0u8; 8];
947 file.read_exact(&mut footer_magic)?;
948 if footer_magic != RUN_MAGIC {
949 return Err(MongrelError::MagicMismatch {
950 what: "sorted run footer",
951 expected: RUN_MAGIC,
952 got: footer_magic,
953 });
954 }
955 Ok(header)
956}
957
958pub fn read_header(path: impl AsRef<Path>) -> Result<RunHeader> {
960 let mut file = crate::durable_file::open_regular_nofollow(path.as_ref())?;
961 read_header_from_file(&mut file)
962}
963
964fn validate_run_layout(header: &RunHeader, file_len: u64) -> Result<()> {
965 let footer_len = 8u64
966 + 8
967 + 32
968 + if header.is_encrypted() {
969 RUN_MAC_LEN as u64
970 } else {
971 0
972 };
973 let expected_len = header
974 .footer_offset
975 .checked_add(footer_len)
976 .ok_or_else(|| MongrelError::InvalidArgument("sorted run length overflow".into()))?;
977 if header.footer_offset < RUN_HEADER_PAD as u64 || expected_len != file_len {
978 return Err(MongrelError::InvalidArgument(format!(
979 "invalid sorted run layout: footer={} file={file_len}",
980 header.footer_offset
981 )));
982 }
983 let in_body = |offset: u64| {
984 offset == 0 || (offset >= RUN_HEADER_PAD as u64 && offset <= header.footer_offset)
985 };
986 if header.column_dir_offset < RUN_HEADER_PAD as u64
987 || header.column_dir_offset > header.footer_offset
988 || !in_body(header.index_trailer_offset)
989 || !in_body(header.encryption_descriptor_offset)
990 || !in_body(header.encrypted_stats_offset)
991 || header
992 .encrypted_stats_offset
993 .checked_add(header.encrypted_stats_len)
994 .is_none_or(|end| end > header.footer_offset)
995 {
996 return Err(MongrelError::InvalidArgument(
997 "sorted run metadata offsets are outside the file".into(),
998 ));
999 }
1000 Ok(())
1001}
1002
1003fn validate_run_header_bytes(header: &RunHeader, bytes: &[u8; RUN_HEADER_PAD]) -> Result<()> {
1004 if header.magic != RUN_MAGIC {
1005 return Err(MongrelError::MagicMismatch {
1006 what: "sorted run",
1007 expected: RUN_MAGIC,
1008 got: header.magic,
1009 });
1010 }
1011 const KNOWN_FLAGS: u8 =
1012 RUN_FLAG_ENCRYPTED | RUN_FLAG_TOMBSTONE_ONLY | RUN_FLAG_CLEAN | RUN_FLAG_UNIFORM_EPOCH;
1013 if header.format_version != RUN_FORMAT_VERSION
1014 || header.header_layout_version != RUN_HEADER_VERSION
1015 || header.flags & !KNOWN_FLAGS != 0
1016 || (header.row_count == 0 && (header.min_row_id != 0 || header.max_row_id != 0))
1017 || (header.row_count != 0 && header.min_row_id > header.max_row_id)
1018 {
1019 return Err(MongrelError::InvalidArgument(
1020 "unsupported or invalid sorted run header".into(),
1021 ));
1022 }
1023 let canonical = bincode::serialize(header)?;
1024 if canonical.len() > RUN_HEADER_PAD
1025 || bytes[..canonical.len()] != canonical
1026 || bytes[canonical.len()..].iter().any(|byte| *byte != 0)
1027 {
1028 return Err(MongrelError::InvalidArgument(
1029 "sorted run header has noncanonical bytes or nonzero padding".into(),
1030 ));
1031 }
1032 Ok(())
1033}
1034
1035fn read_header_from_file(file: &mut File) -> Result<RunHeader> {
1036 file.seek(SeekFrom::Start(0))?;
1037 let mut header_buf = [0u8; RUN_HEADER_PAD];
1038 file.read_exact(&mut header_buf)?;
1039 let header: RunHeader = bincode::deserialize(&header_buf)
1040 .map_err(|e| MongrelError::InvalidArgument(format!("bad run header: {e}")))?;
1041 validate_run_header_bytes(&header, &header_buf)?;
1042
1043 validate_run_layout(&header, file.metadata()?.len())?;
1044 file.seek(SeekFrom::Start(header.footer_offset))?;
1045 let mut footer = [0u8; 8 + 8 + 32];
1046 file.read_exact(&mut footer)?;
1047 if footer[..8] != RUN_MAGIC {
1048 return Err(MongrelError::MagicMismatch {
1049 what: "sorted run footer",
1050 expected: RUN_MAGIC,
1051 got: footer[..8].try_into().unwrap(),
1052 });
1053 }
1054 let mut hasher = Sha256::new();
1055 file.seek(SeekFrom::Start(0))?;
1056 let mut remaining = header.footer_offset;
1057 let mut buffer = [0u8; 64 * 1024];
1058 while remaining != 0 {
1059 let length = usize::try_from(remaining.min(buffer.len() as u64)).unwrap();
1060 file.read_exact(&mut buffer[..length])?;
1061 hasher.update(&buffer[..length]);
1062 remaining -= length as u64;
1063 }
1064 let computed: [u8; 32] = hasher.finalize().into();
1065 let stored: [u8; 32] = footer[16..].try_into().unwrap();
1066 if computed != stored {
1067 return Err(MongrelError::ChecksumMismatch {
1068 expected: u64::from_be_bytes(stored[..8].try_into().unwrap()),
1069 actual: u64::from_be_bytes(computed[..8].try_into().unwrap()),
1070 context: "sorted run footer".into(),
1071 });
1072 }
1073 file.seek(SeekFrom::Start(RUN_HEADER_PAD as u64))?;
1074 let mut remaining = header.column_dir_offset - RUN_HEADER_PAD as u64;
1075 let mut content = Sha256::new();
1076 while remaining != 0 {
1077 let length = usize::try_from(remaining.min(buffer.len() as u64)).unwrap();
1078 file.read_exact(&mut buffer[..length])?;
1079 content.update(&buffer[..length]);
1080 remaining -= length as u64;
1081 }
1082 let content_hash: [u8; 32] = content.finalize().into();
1083 if content_hash != header.content_hash {
1084 return Err(MongrelError::ChecksumMismatch {
1085 expected: u64::from_be_bytes(header.content_hash[..8].try_into().unwrap()),
1086 actual: u64::from_be_bytes(content_hash[..8].try_into().unwrap()),
1087 context: "sorted run content hash".into(),
1088 });
1089 }
1090 Ok(header)
1091}
1092
1093pub fn read_column_dir(
1095 path: impl AsRef<Path>,
1096 header: &RunHeader,
1097) -> Result<Vec<ColumnPageHeader>> {
1098 let mut file = crate::durable_file::open_regular_nofollow(path.as_ref())?;
1099 read_column_dir_from_file(&mut file, header)
1100}
1101
1102fn read_column_dir_from_file(file: &mut File, header: &RunHeader) -> Result<Vec<ColumnPageHeader>> {
1103 file.seek(SeekFrom::Start(header.column_dir_offset))?;
1104 let end = [
1105 header.index_trailer_offset,
1106 header.encryption_descriptor_offset,
1107 header.encrypted_stats_offset,
1108 header.footer_offset,
1109 ]
1110 .into_iter()
1111 .filter(|offset| *offset > header.column_dir_offset)
1112 .min()
1113 .ok_or_else(|| {
1114 MongrelError::InvalidArgument("sorted run column directory has no end".into())
1115 })?;
1116 let len = end.checked_sub(header.column_dir_offset).ok_or_else(|| {
1117 MongrelError::InvalidArgument("sorted run column directory offsets are reversed".into())
1118 })?;
1119 const MAX_COLUMN_DIR_BYTES: u64 = 64 * 1024 * 1024;
1120 if len > MAX_COLUMN_DIR_BYTES {
1121 return Err(MongrelError::InvalidArgument(format!(
1122 "sorted run column directory length {len} exceeds {MAX_COLUMN_DIR_BYTES}"
1123 )));
1124 }
1125 let mut buf = vec![0u8; len as usize];
1126 file.read_exact(&mut buf)?;
1127 let dir: Vec<ColumnPageHeader> = bincode::deserialize(&buf)
1128 .map_err(|e| MongrelError::InvalidArgument(format!("bad column dir: {e}")))?;
1129 Ok(dir)
1130}
1131
1132fn validate_column_directory_layout(header: &RunHeader, dir: &[ColumnPageHeader]) -> Result<()> {
1133 if header.column_count != dir.len() as u64 || dir.len() > u16::MAX as usize {
1134 return Err(MongrelError::InvalidArgument(
1135 "sorted run column count is invalid".into(),
1136 ));
1137 }
1138 let mut regions = Vec::with_capacity(dir.len());
1139 let mut ids = std::collections::HashSet::new();
1140 for column in dir {
1141 if !ids.insert(column.column_id)
1142 || column.flags & !ColumnPageHeader::PAGE_ENCRYPTED != 0
1143 || column.page_count as usize != column.page_stats.len()
1144 {
1145 return Err(MongrelError::InvalidArgument(
1146 "sorted run column directory identity is invalid".into(),
1147 ));
1148 }
1149 let region_end = column
1150 .page_region_offset
1151 .checked_add(column.page_region_len)
1152 .ok_or_else(|| MongrelError::InvalidArgument("run page region overflows".into()))?;
1153 if column.page_region_offset < RUN_HEADER_PAD as u64
1154 || region_end > header.column_dir_offset
1155 {
1156 return Err(MongrelError::InvalidArgument(
1157 "sorted run page region is outside its body".into(),
1158 ));
1159 }
1160 let mut cursor = column.page_region_offset;
1161 let mut rows = 0_u64;
1162 for stat in &column.page_stats {
1163 let length = stat.compressed_len as u64;
1164 let end = stat
1165 .offset
1166 .checked_add(length)
1167 .ok_or_else(|| MongrelError::InvalidArgument("run page offset overflows".into()))?;
1168 if stat.offset != cursor
1169 || length == 0
1170 || length > MAX_RUN_PAGE_BYTES
1171 || stat.uncompressed_len as u64 > MAX_RUN_PAGE_BYTES
1172 || stat.row_count as usize > PAGE_ROWS
1173 || end > region_end
1174 {
1175 return Err(MongrelError::InvalidArgument(
1176 "sorted run page metadata is outside its region".into(),
1177 ));
1178 }
1179 rows = rows.checked_add(stat.row_count as u64).ok_or_else(|| {
1180 MongrelError::InvalidArgument("sorted run row count overflows".into())
1181 })?;
1182 cursor = end;
1183 }
1184 if cursor != region_end || rows != header.row_count {
1185 return Err(MongrelError::InvalidArgument(
1186 "sorted run page region length or row count is inconsistent".into(),
1187 ));
1188 }
1189 regions.push((column.page_region_offset, region_end));
1190 }
1191 regions.sort_unstable();
1192 if regions.windows(2).any(|pair| pair[0].1 > pair[1].0) {
1193 return Err(MongrelError::InvalidArgument(
1194 "sorted run page regions overlap".into(),
1195 ));
1196 }
1197 Ok(())
1198}
1199
1200fn read_encryption_descriptor_bytes_from_file(
1201 file: &mut File,
1202 header: &RunHeader,
1203) -> Result<Vec<u8>> {
1204 file.seek(SeekFrom::Start(header.encryption_descriptor_offset))?;
1205 let mut len_buf = [0u8; 4];
1206 file.read_exact(&mut len_buf)?;
1207 let len = u32::from_le_bytes(len_buf) as usize;
1208 const MAX_DESCRIPTOR_BYTES: usize = 65_536;
1213 if len > MAX_DESCRIPTOR_BYTES {
1214 return Err(MongrelError::InvalidArgument(format!(
1215 "encryption descriptor length {len} exceeds {MAX_DESCRIPTOR_BYTES}"
1216 )));
1217 }
1218 let mut buf = vec![0u8; len];
1219 file.read_exact(&mut buf)?;
1220 Ok(buf)
1221}
1222
1223#[cfg(feature = "encryption")]
1230fn verify_run_mac(
1231 file: &mut File,
1232 header: &RunHeader,
1233 dir: &[ColumnPageHeader],
1234 kek: &Kek,
1235 desc_bytes: &[u8],
1236) -> Result<()> {
1237 let header_bytes = bincode::serialize(header)?;
1238 let dir_bytes = bincode::serialize(dir)?;
1239 let mac_key = kek.derive_run_mac_key();
1240 let expected =
1241 crate::encryption::run_metadata_mac(&mac_key, &header_bytes, &dir_bytes, desc_bytes);
1242 file.seek(SeekFrom::Start(header.footer_offset + 8 + 8 + 32))?;
1243 let mut tag = [0u8; RUN_MAC_LEN];
1244 file.read_exact(&mut tag).map_err(|_| {
1245 MongrelError::Decryption(
1246 "encrypted run is missing or truncated its metadata MAC; cannot \
1247 authenticate metadata"
1248 .into(),
1249 )
1250 })?;
1251 let mut diff = 0u8;
1253 for (x, y) in tag.iter().zip(expected.iter()) {
1254 diff |= x ^ y;
1255 }
1256 if diff != 0 {
1257 return Err(MongrelError::Decryption(
1258 "run metadata authentication failed — tampered run or wrong key".into(),
1259 ));
1260 }
1261 Ok(())
1262}
1263
1264#[cfg(not(feature = "encryption"))]
1265fn verify_run_mac(
1266 _file: &mut File,
1267 _header: &RunHeader,
1268 _dir: &[ColumnPageHeader],
1269 _kek: &Kek,
1270 _desc_bytes: &[u8],
1271) -> Result<()> {
1272 Ok(())
1273}
1274
1275pub struct RunWriter<'a> {
1284 schema: &'a Schema,
1285 run_id: u128,
1286 epoch_created: Epoch,
1287 level: u8,
1288 kek: Option<&'a Kek>,
1289 indexable_columns: Vec<(u16, u8)>,
1292 compress: columnar::Compress,
1296 clean: bool,
1301 uniform_epoch: bool,
1306 le: bool,
1313}
1314
1315impl<'a> RunWriter<'a> {
1316 pub fn new(schema: &'a Schema, run_id: u128, epoch_created: Epoch, level: u8) -> Self {
1317 Self {
1318 schema,
1319 run_id,
1320 epoch_created,
1321 level,
1322 kek: None,
1323 indexable_columns: Vec::new(),
1324 compress: columnar::Compress::Zstd(3),
1325 clean: false,
1326 uniform_epoch: false,
1327 le: false,
1328 }
1329 }
1330
1331 pub fn uniform_epoch(mut self, uniform: bool) -> Self {
1337 self.uniform_epoch = uniform;
1338 self
1339 }
1340
1341 pub fn with_encryption(mut self, kek: &'a Kek, indexable_columns: Vec<(u16, u8)>) -> Self {
1345 self.kek = Some(kek);
1346 self.indexable_columns = indexable_columns;
1347 self
1348 }
1349
1350 pub fn with_zstd_level(mut self, level: i32) -> Self {
1353 self.compress = columnar::Compress::Zstd(level);
1354 self
1355 }
1356
1357 pub fn with_lz4(mut self) -> Self {
1361 self.compress = columnar::Compress::Lz4;
1362 self
1363 }
1364
1365 pub fn with_plain(mut self) -> Self {
1368 self.compress = columnar::Compress::Plain;
1369 self
1370 }
1371
1372 pub fn clean(mut self, clean: bool) -> Self {
1378 self.clean = clean;
1379 self
1380 }
1381
1382 pub fn with_native_endian(mut self) -> Self {
1387 if cfg!(target_endian = "little") {
1388 self.le = true;
1389 }
1390 self
1391 }
1392
1393 pub fn write_native(
1400 self,
1401 path: impl AsRef<Path>,
1402 user_columns: &[(u16, columnar::NativeColumn)],
1403 n: usize,
1404 first_row_id: u64,
1405 ) -> Result<RunHeader> {
1406 self.write_native_target(Some(path.as_ref()), None, user_columns, n, first_row_id)
1407 }
1408
1409 pub(crate) fn write_native_file(
1410 self,
1411 file: File,
1412 user_columns: &[(u16, columnar::NativeColumn)],
1413 n: usize,
1414 first_row_id: u64,
1415 ) -> Result<RunHeader> {
1416 self.write_native_target(None, Some(file), user_columns, n, first_row_id)
1417 }
1418
1419 fn write_native_target(
1420 self,
1421 path: Option<&Path>,
1422 file: Option<File>,
1423 user_columns: &[(u16, columnar::NativeColumn)],
1424 n: usize,
1425 first_row_id: u64,
1426 ) -> Result<RunHeader> {
1427 use columnar::NativeColumn;
1428 let row_id_col = NativeColumn::int64_sequence(first_row_id as i64, n);
1429 let epoch_col = NativeColumn::int64_constant(self.epoch_created.0 as i64, n);
1430 let deleted_col = NativeColumn::bool_constant(false, n);
1431
1432 let learned_trailer = build_learned_trailer_native(&row_id_col);
1433
1434 let row_ids = match &row_id_col {
1437 NativeColumn::Int64 { data, .. } => data.as_slice(),
1438 _ => &[],
1439 };
1440 let bounds = page_bounds(row_ids);
1441 let compress = self.compress;
1442 let le = self.le;
1443 let plain = matches!(compress, columnar::Compress::Plain);
1444 let row_id_enc = if plain {
1447 Encoding::Plain
1448 } else {
1449 Encoding::Delta
1450 };
1451 let sys_enc = if plain {
1452 Encoding::Plain
1453 } else {
1454 Encoding::Zstd
1455 };
1456
1457 let mut columns: Vec<ColumnPayload> = Vec::with_capacity(3 + self.schema.columns.len());
1458 let (pages, stats) = native_column_pages(
1459 TypeId::Int64,
1460 &row_id_col,
1461 row_id_enc,
1462 compress,
1463 le,
1464 &bounds,
1465 )?;
1466 columns.push(ColumnPayload {
1467 column_id: SYS_ROW_ID,
1468 type_id_tag: type_tag(&TypeId::Int64),
1469 encoding: row_id_enc,
1470 pages,
1471 page_stats: stats,
1472 });
1473 let (pages, stats) =
1474 native_column_pages(TypeId::Int64, &epoch_col, sys_enc, compress, le, &bounds)?;
1475 columns.push(ColumnPayload {
1476 column_id: SYS_EPOCH,
1477 type_id_tag: type_tag(&TypeId::Int64),
1478 encoding: sys_enc,
1479 pages,
1480 page_stats: stats,
1481 });
1482 let (pages, stats) =
1483 native_column_pages(TypeId::Bool, &deleted_col, sys_enc, compress, le, &bounds)?;
1484 columns.push(ColumnPayload {
1485 column_id: SYS_DELETED,
1486 type_id_tag: type_tag(&TypeId::Bool),
1487 encoding: sys_enc,
1488 pages,
1489 page_stats: stats,
1490 });
1491 use rayon::prelude::*;
1497 let user_cols: Vec<ColumnPayload> = self
1498 .schema
1499 .columns
1500 .par_iter()
1501 .map(|cdef| -> Result<ColumnPayload> {
1502 let col = user_columns
1503 .iter()
1504 .find(|(id, _)| *id == cdef.id)
1505 .map(|(_, c)| c)
1506 .ok_or_else(|| {
1507 MongrelError::ColumnNotFound(format!("user column {}", cdef.id))
1508 })?;
1509 let encoding = if plain {
1510 Encoding::Plain
1511 } else {
1512 choose_encoding_native(&cdef.ty, col)
1513 };
1514 let (pages, stats) =
1515 native_column_pages(cdef.ty.clone(), col, encoding, compress, le, &bounds)?;
1516 Ok(ColumnPayload {
1517 column_id: cdef.id,
1518 type_id_tag: type_tag(&cdef.ty),
1519 encoding,
1520 pages,
1521 page_stats: stats,
1522 })
1523 })
1524 .collect::<Result<Vec<_>>>()?;
1525 columns.extend(user_cols);
1526
1527 let flags = if self.uniform_epoch {
1533 RUN_FLAG_UNIFORM_EPOCH
1534 } else {
1535 RUN_FLAG_CLEAN
1536 };
1537 let spec = RunSpec {
1538 run_id: self.run_id,
1539 schema_id: self.schema.schema_id,
1540 epoch_created: self.epoch_created.0,
1541 level: self.level,
1542 flags,
1543 sort_key_column_id: SYS_ROW_ID,
1544 row_count: n as u64,
1545 min_row_id: first_row_id,
1546 max_row_id: first_row_id + n as u64 - 1,
1547 columns: &columns,
1548 };
1549 match file {
1550 Some(file) => write_run_with_file(
1551 file,
1552 &spec,
1553 self.kek,
1554 &self.indexable_columns,
1555 Some(&learned_trailer),
1556 ),
1557 None => write_run_with(
1558 path.ok_or_else(|| {
1559 MongrelError::InvalidArgument("sorted run output is missing".into())
1560 })?,
1561 &spec,
1562 self.kek,
1563 &self.indexable_columns,
1564 Some(&learned_trailer),
1565 ),
1566 }
1567 }
1568
1569 pub fn write(self, path: impl AsRef<Path>, rows: &[Row]) -> Result<RunHeader> {
1570 self.write_target(Some(path.as_ref()), None, rows)
1571 }
1572
1573 pub(crate) fn write_file(self, file: File, rows: &[Row]) -> Result<RunHeader> {
1574 self.write_target(None, Some(file), rows)
1575 }
1576
1577 fn write_target(
1578 self,
1579 path: Option<&Path>,
1580 file: Option<File>,
1581 rows: &[Row],
1582 ) -> Result<RunHeader> {
1583 let n = rows.len();
1584 let mut row_ids = Vec::with_capacity(n);
1586 let mut epochs = Vec::with_capacity(n);
1587 let mut deleted = Vec::with_capacity(n);
1588 for r in rows {
1589 row_ids.push(Value::Int64(r.row_id.0 as i64));
1590 epochs.push(Value::Int64(r.committed_epoch.0 as i64));
1591 deleted.push(Value::Bool(r.deleted));
1592 }
1593 let learned_trailer = build_learned_trailer(&row_ids);
1594 let (min_rid, max_rid) = row_id_bounds(rows);
1595 let row_id_i64: Vec<i64> = rows.iter().map(|r| r.row_id.0 as i64).collect();
1596 let bounds = page_bounds(&row_id_i64);
1597
1598 let mut columns: Vec<ColumnPayload> = Vec::with_capacity(3 + self.schema.columns.len());
1599 let (pages, stats, enc) = value_column_pages(TypeId::Int64, &row_ids, &bounds)?;
1600 columns.push(ColumnPayload {
1601 column_id: SYS_ROW_ID,
1602 type_id_tag: type_tag(&TypeId::Int64),
1603 encoding: enc,
1604 pages,
1605 page_stats: stats,
1606 });
1607 let (pages, stats, enc) = value_column_pages(TypeId::Int64, &epochs, &bounds)?;
1608 columns.push(ColumnPayload {
1609 column_id: SYS_EPOCH,
1610 type_id_tag: type_tag(&TypeId::Int64),
1611 encoding: enc,
1612 pages,
1613 page_stats: stats,
1614 });
1615 let (pages, stats, enc) = value_column_pages(TypeId::Bool, &deleted, &bounds)?;
1616 columns.push(ColumnPayload {
1617 column_id: SYS_DELETED,
1618 type_id_tag: type_tag(&TypeId::Bool),
1619 encoding: enc,
1620 pages,
1621 page_stats: stats,
1622 });
1623 for cdef in &self.schema.columns {
1625 let vals: Vec<Value> = rows
1626 .iter()
1627 .map(|r| r.columns.get(&cdef.id).cloned().unwrap_or(Value::Null))
1628 .collect();
1629 let (pages, stats, encoding) = value_column_pages(cdef.ty.clone(), &vals, &bounds)?;
1630 columns.push(ColumnPayload {
1631 column_id: cdef.id,
1632 type_id_tag: type_tag(&cdef.ty),
1633 encoding,
1634 pages,
1635 page_stats: stats,
1636 });
1637 }
1638
1639 let spec = RunSpec {
1640 run_id: self.run_id,
1641 schema_id: self.schema.schema_id,
1642 epoch_created: self.epoch_created.0,
1643 level: self.level,
1644 flags: {
1645 let mut f = if self.clean { RUN_FLAG_CLEAN } else { 0 };
1646 if self.uniform_epoch {
1647 f |= RUN_FLAG_UNIFORM_EPOCH;
1648 }
1649 f
1650 },
1651 sort_key_column_id: SYS_ROW_ID,
1652 row_count: n as u64,
1653 min_row_id: min_rid,
1654 max_row_id: max_rid,
1655 columns: &columns,
1656 };
1657 match file {
1658 Some(file) => write_run_with_file(
1659 file,
1660 &spec,
1661 self.kek,
1662 &self.indexable_columns,
1663 Some(&learned_trailer),
1664 ),
1665 None => write_run_with(
1666 path.ok_or_else(|| {
1667 MongrelError::InvalidArgument("sorted run output is missing".into())
1668 })?,
1669 &spec,
1670 self.kek,
1671 &self.indexable_columns,
1672 Some(&learned_trailer),
1673 ),
1674 }
1675 }
1676}
1677
1678fn type_tag(ty: &TypeId) -> u16 {
1679 match ty {
1681 TypeId::Bool => 1,
1682 TypeId::Int64 => 8,
1683 TypeId::Float64 => 9,
1684 TypeId::Bytes => 12,
1685 TypeId::Embedding { .. } => 13,
1686 _ => 0,
1687 }
1688}
1689
1690pub(crate) fn be_i64(b: Option<&[u8]>) -> Option<i64> {
1693 let b = b?;
1694 (b.len() == 8).then(|| i64::from_be_bytes(b.try_into().unwrap()))
1695}
1696
1697pub(crate) fn be_f64(b: Option<&[u8]>) -> Option<f64> {
1699 let b = b?;
1700 (b.len() == 8).then(|| f64::from_bits(u64::from_be_bytes(b.try_into().unwrap())))
1701}
1702
1703const PAGE_ROWS: usize = 65_536;
1706
1707fn page_bounds(row_ids: &[i64]) -> Vec<(usize, usize, u64, u64)> {
1710 let n = row_ids.len();
1711 if n == 0 {
1712 return vec![(0, 0, 0, 0)];
1713 }
1714 let mut out = Vec::new();
1715 let mut start = 0;
1716 while start < n {
1717 let end = (start + PAGE_ROWS).min(n);
1718 out.push((start, end, row_ids[start] as u64, row_ids[end - 1] as u64));
1719 start = end;
1720 }
1721 out
1722}
1723
1724fn native_column_pages(
1730 ty: TypeId,
1731 col: &columnar::NativeColumn,
1732 encoding: Encoding,
1733 compress: columnar::Compress,
1734 le: bool,
1735 bounds: &[(usize, usize, u64, u64)],
1736) -> Result<(Vec<Vec<u8>>, Vec<PageStat>)> {
1737 use rayon::prelude::*;
1738 let encode_one =
1739 |&(s, e, frid, lrid): &(usize, usize, u64, u64)| -> Result<(Vec<u8>, PageStat)> {
1740 let chunk = col.slice_range(s, e);
1741 let stat = columnar::page_stat_for(ty.clone(), &chunk, frid, lrid);
1742 let page = columnar::encode_page_native(ty.clone(), &chunk, encoding, compress, le)?;
1743 Ok((page, stat))
1744 };
1745 let pairs: Vec<(Vec<u8>, PageStat)> = if bounds.len() > 1 {
1747 bounds
1748 .par_iter()
1749 .map(encode_one)
1750 .collect::<Result<Vec<_>>>()?
1751 } else {
1752 bounds.iter().map(encode_one).collect::<Result<Vec<_>>>()?
1753 };
1754 let (pages, stats) = pairs.into_iter().unzip();
1755 Ok((pages, stats))
1756}
1757
1758fn value_column_pages(
1760 ty: TypeId,
1761 vals: &[Value],
1762 bounds: &[(usize, usize, u64, u64)],
1763) -> Result<(Vec<Vec<u8>>, Vec<PageStat>, Encoding)> {
1764 let encoding = choose_encoding(&ty, vals);
1765 let mut pages = Vec::with_capacity(bounds.len());
1766 let mut stats = Vec::with_capacity(bounds.len());
1767 for &(s, e, frid, lrid) in bounds {
1768 let chunk = &vals[s..e];
1769 pages.push(columnar::encode_page(ty.clone(), chunk, encoding)?);
1770 let native = columnar::values_to_native(ty.clone(), chunk);
1771 stats.push(columnar::page_stat_for(ty.clone(), &native, frid, lrid));
1772 }
1773 Ok((pages, stats, encoding))
1774}
1775
1776fn choose_encoding(ty: &TypeId, values: &[Value]) -> Encoding {
1779 use std::collections::HashSet;
1780 if matches!(ty, TypeId::Bytes) {
1781 let n = values.len();
1782 if n > 0 {
1783 let distinct = values
1784 .iter()
1785 .filter(|v| !matches!(v, Value::Null))
1786 .map(|v| v.encode_key())
1787 .collect::<HashSet<_>>()
1788 .len();
1789 if (distinct as f64 / n as f64) < 0.5 {
1790 return Encoding::Dictionary;
1791 }
1792 }
1793 }
1794 Encoding::Zstd
1795}
1796
1797fn choose_encoding_native(ty: &TypeId, col: &columnar::NativeColumn) -> Encoding {
1800 use std::collections::HashSet;
1801 match (ty, col) {
1802 (TypeId::Int64 | TypeId::TimestampNanos, columnar::NativeColumn::Int64 { data, .. }) => {
1803 if data.windows(2).all(|w| w[0] <= w[1]) {
1804 Encoding::Delta
1805 } else {
1806 Encoding::Zstd
1807 }
1808 }
1809 (
1810 TypeId::Bytes,
1811 columnar::NativeColumn::Bytes {
1812 offsets, values, ..
1813 },
1814 ) => {
1815 let n = offsets.len().saturating_sub(1);
1816 if n > 0 {
1817 let distinct: HashSet<&[u8]> = (0..n)
1818 .map(|i| &values[offsets[i] as usize..offsets[i + 1] as usize])
1819 .collect();
1820 if (distinct.len() as f64 / n as f64) < 0.5 {
1821 return Encoding::Dictionary;
1822 }
1823 }
1824 Encoding::Zstd
1825 }
1826 _ => Encoding::Zstd,
1827 }
1828}
1829
1830fn build_learned_trailer_native(col: &columnar::NativeColumn) -> Vec<u8> {
1832 let points: Vec<(u64, usize)> = match col {
1833 columnar::NativeColumn::Int64 { data, .. } => data
1834 .iter()
1835 .enumerate()
1836 .map(|(i, v)| (*v as u64, i))
1837 .collect(),
1838 _ => Vec::new(),
1839 };
1840 let pgm = PgmIndex::build(&points, LEARNED_EPSILON);
1841 bincode::serialize(&pgm).expect("pgm serialize")
1842}
1843
1844fn row_id_bounds(rows: &[Row]) -> (u64, u64) {
1845 match (rows.first(), rows.last()) {
1846 (Some(f), Some(l)) => (f.row_id.0, l.row_id.0),
1847 _ => (0, 0),
1848 }
1849}
1850
1851pub struct RunReader {
1857 file: File,
1858 mmap: Option<memmap2::Mmap>,
1859 header: RunHeader,
1860 dir: Vec<ColumnPageHeader>,
1861 schema: Schema,
1862 table_id: u64,
1864 cipher: Option<Box<dyn Cipher>>,
1866 nonce_prefix: [u8; 12],
1868 col_cache: HashMap<u16, Vec<Value>>,
1869 page_cache: Option<Arc<crate::cache::Sharded<crate::cache::PageCache>>>,
1873 decoded_cache: Option<Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>>,
1877 epoch_override: Option<Epoch>,
1881}
1882
1883#[derive(Debug, Clone, Copy)]
1884pub(crate) struct RunVisibleVersion {
1885 pub(crate) row_id: RowId,
1886 pub(crate) committed_epoch: Epoch,
1887 pub(crate) deleted: bool,
1888 page_seq: usize,
1889 within_page: usize,
1890}
1891
1892pub(crate) struct RunVisibleVersionCursor {
1896 reader: RunReader,
1897 snapshot: Epoch,
1898 page_row_counts: Vec<usize>,
1899 page_seq: usize,
1900 within_page: usize,
1901 row_ids: Vec<i64>,
1902 epochs: Vec<i64>,
1903 deleted: Vec<u8>,
1904 lookahead: Option<RunVisibleVersion>,
1905 materialized_page: Option<usize>,
1906 materialized_columns: Vec<(u16, columnar::NativeColumn)>,
1907}
1908
1909impl RunVisibleVersionCursor {
1910 fn new(reader: RunReader, snapshot: Epoch) -> Result<Self> {
1911 let page_row_counts = reader.page_row_counts(SYS_ROW_ID)?;
1912 Ok(Self {
1913 reader,
1914 snapshot,
1915 page_row_counts,
1916 page_seq: 0,
1917 within_page: 0,
1918 row_ids: Vec::new(),
1919 epochs: Vec::new(),
1920 deleted: Vec::new(),
1921 lookahead: None,
1922 materialized_page: None,
1923 materialized_columns: Vec::new(),
1924 })
1925 }
1926
1927 fn load_system_page(&mut self, control: &crate::ExecutionControl) -> Result<bool> {
1928 while self.page_seq < self.page_row_counts.len() {
1929 control.checkpoint()?;
1930 let rows = self.page_row_counts[self.page_seq];
1931 if rows == 0 {
1932 self.page_seq += 1;
1933 continue;
1934 }
1935 self.row_ids = match columnar::decode_page_native(
1936 TypeId::Int64,
1937 &self.reader.read_page(SYS_ROW_ID, self.page_seq)?,
1938 rows,
1939 )? {
1940 columnar::NativeColumn::Int64 { data, .. } => data,
1941 _ => return Err(MongrelError::InvalidArgument("sys row_id not int64".into())),
1942 };
1943 self.epochs = if let Some(epoch) = self.reader.epoch_override {
1944 vec![epoch.0 as i64; rows]
1945 } else {
1946 match columnar::decode_page_native(
1947 TypeId::Int64,
1948 &self.reader.read_page(SYS_EPOCH, self.page_seq)?,
1949 rows,
1950 )? {
1951 columnar::NativeColumn::Int64 { data, .. } => data,
1952 _ => return Err(MongrelError::InvalidArgument("sys epoch not int64".into())),
1953 }
1954 };
1955 self.deleted = match columnar::decode_page_native(
1956 TypeId::Bool,
1957 &self.reader.read_page(SYS_DELETED, self.page_seq)?,
1958 rows,
1959 )? {
1960 columnar::NativeColumn::Bool { data, .. } => data,
1961 _ => return Err(MongrelError::InvalidArgument("sys deleted not bool".into())),
1962 };
1963 self.within_page = 0;
1964 return Ok(true);
1965 }
1966 Ok(false)
1967 }
1968
1969 fn next_raw(&mut self, control: &crate::ExecutionControl) -> Result<Option<RunVisibleVersion>> {
1970 if self.within_page >= self.row_ids.len() {
1971 if !self.row_ids.is_empty() {
1972 self.page_seq += 1;
1973 self.row_ids.clear();
1974 self.epochs.clear();
1975 self.deleted.clear();
1976 }
1977 if !self.load_system_page(control)? {
1978 return Ok(None);
1979 }
1980 }
1981 if self.within_page.is_multiple_of(256) {
1982 control.checkpoint()?;
1983 }
1984 let position = self.within_page;
1985 self.within_page += 1;
1986 Ok(Some(RunVisibleVersion {
1987 row_id: RowId(self.row_ids[position] as u64),
1988 committed_epoch: Epoch(self.epochs[position] as u64),
1989 deleted: self.deleted[position] != 0,
1990 page_seq: self.page_seq,
1991 within_page: position,
1992 }))
1993 }
1994
1995 pub(crate) fn next_visible_version(
1996 &mut self,
1997 control: &crate::ExecutionControl,
1998 ) -> Result<Option<RunVisibleVersion>> {
1999 loop {
2000 let first = match self.lookahead.take() {
2001 Some(version) => version,
2002 None => match self.next_raw(control)? {
2003 Some(version) => version,
2004 None => return Ok(None),
2005 },
2006 };
2007 let row_id = first.row_id;
2008 let mut best = (first.committed_epoch <= self.snapshot).then_some(first);
2009 while let Some(candidate) = self.next_raw(control)? {
2010 if candidate.row_id != row_id {
2011 self.lookahead = Some(candidate);
2012 break;
2013 }
2014 if candidate.committed_epoch <= self.snapshot
2015 && best
2016 .is_none_or(|current| candidate.committed_epoch > current.committed_epoch)
2017 {
2018 best = Some(candidate);
2019 }
2020 }
2021 if best.is_some() {
2022 return Ok(best);
2023 }
2024 }
2025 }
2026
2027 pub(crate) fn materialize(
2028 &mut self,
2029 version: RunVisibleVersion,
2030 control: &crate::ExecutionControl,
2031 ) -> Result<Row> {
2032 if self.materialized_page != Some(version.page_seq) {
2033 let rows = self.page_row_counts[version.page_seq];
2034 let columns = self.reader.schema.columns.clone();
2035 let mut materialized = Vec::with_capacity(columns.len());
2036 for (index, column) in columns.into_iter().enumerate() {
2037 if index % 16 == 0 {
2038 control.checkpoint()?;
2039 }
2040 let native = if self.reader.has_column(column.id) {
2041 columnar::decode_page_native(
2042 column.ty,
2043 &self.reader.read_page(column.id, version.page_seq)?,
2044 rows,
2045 )?
2046 } else {
2047 columnar::null_native(column.ty, rows)
2048 };
2049 materialized.push((column.id, native));
2050 }
2051 self.materialized_columns = materialized;
2052 self.materialized_page = Some(version.page_seq);
2053 }
2054 let columns = self
2055 .materialized_columns
2056 .iter()
2057 .map(|(column_id, column)| {
2058 (
2059 *column_id,
2060 column.value_at(version.within_page).unwrap_or(Value::Null),
2061 )
2062 })
2063 .collect();
2064 Ok(Row {
2065 row_id: version.row_id,
2066 committed_epoch: version.committed_epoch,
2067 columns,
2068 deleted: version.deleted,
2069 })
2070 }
2071}
2072
2073impl RunReader {
2074 pub fn open(path: impl AsRef<Path>, schema: Schema, kek: Option<Arc<Kek>>) -> Result<Self> {
2075 Self::open_with_cache(path, schema, kek, None, None, 0, None)
2076 }
2077
2078 pub(crate) fn open_file(file: File, schema: Schema, kek: Option<Arc<Kek>>) -> Result<Self> {
2079 Self::open_file_with_cache(file, schema, kek, None, None, 0, None, None)
2080 }
2081
2082 #[allow(clippy::too_many_arguments)]
2083 pub(crate) fn open_with_cache(
2084 path: impl AsRef<Path>,
2085 schema: Schema,
2086 kek: Option<Arc<Kek>>,
2087 page_cache: Option<Arc<crate::cache::Sharded<crate::cache::PageCache>>>,
2088 decoded_cache: Option<Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>>,
2089 table_id: u64,
2090 verified_runs: Option<&parking_lot::Mutex<std::collections::HashSet<u128>>>,
2091 ) -> Result<Self> {
2092 let path = path.as_ref().to_path_buf();
2093 let file = crate::durable_file::open_regular_nofollow(&path)?;
2094 Self::open_file_with_cache(
2095 file,
2096 schema,
2097 kek,
2098 page_cache,
2099 decoded_cache,
2100 table_id,
2101 verified_runs,
2102 Some(path),
2103 )
2104 }
2105
2106 #[allow(clippy::too_many_arguments)]
2107 pub(crate) fn open_file_with_cache(
2108 mut file: File,
2109 schema: Schema,
2110 kek: Option<Arc<Kek>>,
2111 page_cache: Option<Arc<crate::cache::Sharded<crate::cache::PageCache>>>,
2112 decoded_cache: Option<Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>>,
2113 table_id: u64,
2114 verified_runs: Option<&parking_lot::Mutex<std::collections::HashSet<u128>>>,
2115 path: Option<PathBuf>,
2116 ) -> Result<Self> {
2117 let header = match verified_runs {
2118 Some(cache) => {
2119 let header = read_header_fast_from_file(&mut file)?;
2120 if cache.lock().contains(&header.run_id) {
2121 header
2122 } else {
2123 let verified = read_header_from_file(&mut file)?;
2124 cache.lock().insert(verified.run_id);
2125 verified
2126 }
2127 }
2128 None => read_header_from_file(&mut file)?,
2129 };
2130 let mut dir = read_column_dir_from_file(&mut file, &header)?;
2131 validate_column_directory_layout(&header, &dir)?;
2132 if header.is_encrypted() != kek.is_some() {
2133 return Err(MongrelError::Encryption(
2134 "sorted-run encryption mode differs from the database".into(),
2135 ));
2136 }
2137 let (cipher, nonce_prefix): (Option<Box<dyn Cipher>>, [u8; 12]) = if header.is_encrypted() {
2140 let kek = kek.as_ref().ok_or_else(|| {
2141 MongrelError::Encryption(
2142 "run is encrypted but no key-encryption key was provided".into(),
2143 )
2144 })?;
2145 if header.encryption_descriptor_offset == 0 {
2146 return Err(MongrelError::Encryption(
2147 "encrypted run has no encryption descriptor".into(),
2148 ));
2149 }
2150 let desc_bytes = read_encryption_descriptor_bytes_from_file(&mut file, &header)?;
2151 verify_run_mac(&mut file, &header, &dir, kek, &desc_bytes)?;
2157 let enc = crate::encryption::build_run_cipher(kek, &desc_bytes)?;
2158 if header.encrypted_stats_offset != 0 {
2162 overlay_encrypted_stats(
2163 &mut file,
2164 &header,
2165 enc.cipher.as_ref(),
2166 enc.nonce_prefix,
2167 &mut dir,
2168 )?;
2169 }
2170 (Some(enc.cipher), enc.nonce_prefix)
2171 } else {
2172 (None, [0u8; 12])
2173 };
2174 let mmap = unsafe { memmap2::MmapOptions::new().map(&file) }.ok();
2181 let _ = path;
2182 Ok(Self {
2183 file,
2184 mmap,
2185 header,
2186 dir,
2187 schema,
2188 table_id,
2189 cipher,
2190 nonce_prefix,
2191 col_cache: HashMap::new(),
2192 epoch_override: None,
2193 page_cache,
2194 decoded_cache,
2195 })
2196 }
2197
2198 pub fn header(&self) -> &RunHeader {
2199 &self.header
2200 }
2201
2202 pub(crate) fn validate_all_pages(&mut self) -> Result<()> {
2203 let mut columns = std::collections::HashSet::new();
2204 let row_header = self.find_header(SYS_ROW_ID)?.clone();
2205 let expected_rows = row_header
2206 .page_stats
2207 .iter()
2208 .map(|stat| stat.row_count)
2209 .collect::<Vec<_>>();
2210 let mut row_bounds = Vec::with_capacity(row_header.page_stats.len());
2211 let mut previous = None::<u64>;
2212 let mut first = None::<u64>;
2213 let mut last = None::<u64>;
2214 let mut counted = 0_u64;
2215 for (page, stat) in row_header.page_stats.iter().enumerate() {
2216 let bytes = self.read_page(SYS_ROW_ID, page)?;
2217 let native =
2218 columnar::decode_page_native(TypeId::Int64, &bytes, stat.row_count as usize)?;
2219 if columnar::page_stat_for(TypeId::Int64, &native, 0, 0).null_count != 0 {
2220 return Err(MongrelError::InvalidArgument(
2221 "sorted run row-id page contains nulls".into(),
2222 ));
2223 }
2224 let columnar::NativeColumn::Int64 { data, validity } = native else {
2225 return Err(MongrelError::InvalidArgument(
2226 "sorted run row-id page has the wrong type".into(),
2227 ));
2228 };
2229 let _ = validity;
2230 if data.is_empty() {
2231 if self.header.row_count != 0 || stat.row_count != 0 {
2232 return Err(MongrelError::InvalidArgument(
2233 "sorted run row-id page is unexpectedly empty".into(),
2234 ));
2235 }
2236 row_bounds.push((0, 0));
2237 continue;
2238 }
2239 if data.iter().any(|value| *value < 0) {
2240 return Err(MongrelError::InvalidArgument(
2241 "sorted run contains a negative row id".into(),
2242 ));
2243 }
2244 let page_first = data[0] as u64;
2245 let page_last = data[data.len() - 1] as u64;
2246 for value in data {
2247 let value = value as u64;
2248 if previous.is_some_and(|prior| {
2249 value < prior || (self.header.is_clean() && value == prior)
2250 }) {
2251 return Err(MongrelError::InvalidArgument(
2252 "sorted run row ids are not ordered".into(),
2253 ));
2254 }
2255 first.get_or_insert(value);
2256 previous = Some(value);
2257 last = Some(value);
2258 counted += 1;
2259 }
2260 if stat.first_row_id != page_first || stat.last_row_id != page_last {
2261 return Err(MongrelError::InvalidArgument(
2262 "sorted run row-id page bounds are stale".into(),
2263 ));
2264 }
2265 row_bounds.push((page_first, page_last));
2266 }
2267 if counted != self.header.row_count
2268 || first.unwrap_or(0) != self.header.min_row_id
2269 || last.unwrap_or(0) != self.header.max_row_id
2270 {
2271 return Err(MongrelError::InvalidArgument(
2272 "sorted run header row bounds differ from its pages".into(),
2273 ));
2274 }
2275 for column in self.dir.clone() {
2276 if !columns.insert(column.column_id) {
2277 return Err(MongrelError::InvalidArgument(format!(
2278 "sorted run contains duplicate column {}",
2279 column.column_id
2280 )));
2281 }
2282 let rows = column
2283 .page_stats
2284 .iter()
2285 .map(|stat| stat.row_count)
2286 .collect::<Vec<_>>();
2287 if rows != expected_rows {
2288 return Err(MongrelError::InvalidArgument(
2289 "sorted run columns have inconsistent page row counts".into(),
2290 ));
2291 }
2292 let ty = self.resolve_type(column.column_id);
2293 if column.type_id_tag != type_tag(&ty)
2294 || column.encoding > Encoding::Zstd as u8
2295 || (!matches!(column.column_id, SYS_ROW_ID | SYS_EPOCH | SYS_DELETED)
2296 && self
2297 .schema
2298 .columns
2299 .iter()
2300 .all(|item| item.id != column.column_id))
2301 {
2302 return Err(MongrelError::InvalidArgument(
2303 "sorted run column type or encoding is invalid".into(),
2304 ));
2305 }
2306 for (page, stat) in column.page_stats.iter().enumerate() {
2307 let bytes = self.read_page(column.column_id, page)?;
2308 if bytes.len() != stat.uncompressed_len as usize {
2309 return Err(MongrelError::InvalidArgument(
2310 "sorted run page length differs from its metadata".into(),
2311 ));
2312 }
2313 let native =
2314 match columnar::decode_page_native(ty.clone(), &bytes, stat.row_count as usize)
2315 {
2316 Ok(native) => native,
2317 Err(MongrelError::InvalidArgument(message))
2318 if message.starts_with("decode_page_native: unsupported ty") =>
2319 {
2320 let values =
2321 columnar::decode_page(ty.clone(), &bytes, stat.row_count as usize)?;
2322 columnar::values_to_native(ty.clone(), &values)
2323 }
2324 Err(error) => return Err(error),
2325 };
2326 let (first_row_id, last_row_id) =
2327 row_bounds.get(page).copied().ok_or_else(|| {
2328 MongrelError::InvalidArgument(
2329 "sorted run column has more pages than its row-id column".into(),
2330 )
2331 })?;
2332 let expected =
2333 columnar::page_stat_for(ty.clone(), &native, first_row_id, last_row_id);
2334 if stat.first_row_id != expected.first_row_id
2335 || stat.last_row_id != expected.last_row_id
2336 || stat.null_count != expected.null_count
2337 || stat.min != expected.min
2338 || stat.max != expected.max
2339 {
2340 return Err(MongrelError::InvalidArgument(
2341 "sorted run page statistics differ from decoded values".into(),
2342 ));
2343 }
2344 }
2345 }
2346 if !columns.contains(&SYS_ROW_ID)
2347 || !columns.contains(&SYS_EPOCH)
2348 || !columns.contains(&SYS_DELETED)
2349 || expected_rows.into_iter().map(u64::from).sum::<u64>() != self.header.row_count
2350 {
2351 return Err(MongrelError::InvalidArgument(
2352 "sorted run system columns or row count are invalid".into(),
2353 ));
2354 }
2355 if self.header.schema_id == self.schema.schema_id
2356 && self
2357 .schema
2358 .columns
2359 .iter()
2360 .any(|column| !columns.contains(&column.id))
2361 {
2362 return Err(MongrelError::InvalidArgument(
2363 "sorted run is missing a column from its declared schema".into(),
2364 ));
2365 }
2366
2367 for row_index in 0..usize::try_from(self.header.row_count).map_err(|_| {
2371 MongrelError::InvalidArgument("sorted run row count exceeds this platform".into())
2372 })? {
2373 let epoch = match self.column(SYS_EPOCH)?.get(row_index) {
2374 Some(Value::Int64(value)) if *value >= 0 => *value as u64,
2375 _ => {
2376 return Err(MongrelError::InvalidArgument(
2377 "sorted run contains an invalid commit epoch".into(),
2378 ))
2379 }
2380 };
2381 if epoch > self.header.epoch_created || (self.header.is_uniform_epoch() && epoch != 0) {
2382 return Err(MongrelError::InvalidArgument(
2383 "sorted run commit epoch exceeds its creation epoch".into(),
2384 ));
2385 }
2386 let deleted = match self.column(SYS_DELETED)?.get(row_index) {
2387 Some(Value::Bool(value)) => *value,
2388 _ => {
2389 return Err(MongrelError::InvalidArgument(
2390 "sorted run contains an invalid tombstone marker".into(),
2391 ))
2392 }
2393 };
2394 let mut values = Vec::with_capacity(self.schema.columns.len());
2395 for column in self.schema.columns.clone() {
2396 let value = if columns.contains(&column.id) {
2397 self.column(column.id)?
2398 .get(row_index)
2399 .cloned()
2400 .unwrap_or(Value::Null)
2401 } else {
2402 Value::Null
2403 };
2404 values.push((column.id, value));
2405 }
2406 if !deleted {
2407 self.schema
2408 .validate_persisted_values(&values)
2409 .map_err(|error| {
2410 MongrelError::InvalidArgument(format!(
2411 "sorted run row violates its schema: {error}"
2412 ))
2413 })?;
2414 }
2415 }
2416 Ok(())
2417 }
2418
2419 pub(crate) fn set_uniform_epoch(&mut self, epoch: Epoch) {
2423 if self.header.is_uniform_epoch() {
2424 self.epoch_override = Some(epoch);
2425 self.col_cache.remove(&SYS_EPOCH);
2427 }
2428 }
2429
2430 pub(crate) fn into_visible_version_cursor(
2431 self,
2432 snapshot: Epoch,
2433 ) -> Result<RunVisibleVersionCursor> {
2434 RunVisibleVersionCursor::new(self, snapshot)
2435 }
2436
2437 pub fn is_clean(&self) -> bool {
2440 self.header.is_clean()
2441 }
2442
2443 pub fn row_count(&self) -> usize {
2444 self.header.row_count as usize
2445 }
2446
2447 pub(crate) fn clean_contiguous_row_ids(&self) -> bool {
2448 let n = self.row_count();
2449 n > 0
2450 && self.is_clean()
2451 && self.epoch_override.is_none()
2452 && self.header.max_row_id >= self.header.min_row_id
2453 && self
2454 .header
2455 .max_row_id
2456 .checked_sub(self.header.min_row_id)
2457 .and_then(|span| span.checked_add(1))
2458 == Some(n as u64)
2459 }
2460
2461 pub(crate) fn position_for_row_id_fast(&self, row_id: u64) -> Option<usize> {
2462 if !self.clean_contiguous_row_ids()
2463 || row_id < self.header.min_row_id
2464 || row_id > self.header.max_row_id
2465 {
2466 return None;
2467 }
2468 Some((row_id - self.header.min_row_id) as usize)
2469 }
2470
2471 pub(crate) fn positions_for_row_ids_fast(&self, row_ids: &[u64]) -> Option<Vec<usize>> {
2472 if !self.clean_contiguous_row_ids() {
2473 return None;
2474 }
2475 let mut positions = Vec::with_capacity(row_ids.len());
2476 for &row_id in row_ids {
2477 if let Some(pos) = self.position_for_row_id_fast(row_id) {
2478 positions.push(pos);
2479 }
2480 }
2481 positions.sort_unstable();
2482 Some(positions)
2483 }
2484
2485 fn resolve_type(&self, column_id: u16) -> TypeId {
2486 match column_id {
2487 SYS_ROW_ID | SYS_EPOCH => TypeId::Int64,
2488 SYS_DELETED => TypeId::Bool,
2489 _ => self
2490 .schema
2491 .columns
2492 .iter()
2493 .find(|c| c.id == column_id)
2494 .map(|c| c.ty.clone())
2495 .unwrap_or(TypeId::Bytes),
2496 }
2497 }
2498
2499 fn find_header(&self, column_id: u16) -> Result<&ColumnPageHeader> {
2500 self.dir
2501 .iter()
2502 .find(|h| h.column_id == column_id)
2503 .ok_or_else(|| MongrelError::ColumnNotFound(format!("column {column_id}")))
2504 }
2505
2506 fn col_encrypted(&self, column_id: u16) -> bool {
2512 self.dir
2513 .iter()
2514 .find(|h| h.column_id == column_id)
2515 .map(|h| h.flags & ColumnPageHeader::PAGE_ENCRYPTED != 0)
2516 .unwrap_or(false)
2517 }
2518
2519 pub(crate) fn read_page(&mut self, column_id: u16, page_seq: usize) -> Result<Vec<u8>> {
2520 let (offset, compressed_len, encrypted) = {
2521 let ch = self.find_header(column_id)?;
2522 let stat = ch
2523 .page_stats
2524 .get(page_seq)
2525 .ok_or_else(|| MongrelError::InvalidArgument("page seq out of range".into()))?;
2526 (
2527 stat.offset,
2528 stat.compressed_len,
2529 ch.flags & ColumnPageHeader::PAGE_ENCRYPTED != 0,
2530 )
2531 };
2532 let end = offset
2533 .checked_add(compressed_len as u64)
2534 .ok_or_else(|| MongrelError::InvalidArgument("run page offset overflows".into()))?;
2535 let start = usize::try_from(offset)
2536 .map_err(|_| MongrelError::InvalidArgument("run page offset is too large".into()))?;
2537 let end = usize::try_from(end)
2538 .map_err(|_| MongrelError::InvalidArgument("run page end is too large".into()))?;
2539 let key = page_cache_key(self.table_id, self.header.run_id, column_id, page_seq);
2542 if let Some(cache) = &self.page_cache {
2543 if let Some(bytes) = cache.lock(&key).get(
2544 &key,
2545 crate::epoch::Snapshot::at(crate::epoch::Epoch(u64::MAX)),
2546 ) {
2547 return decrypt_or_passthrough(
2548 self.cipher.as_deref(),
2549 self.nonce_prefix,
2550 column_id,
2551 page_seq,
2552 encrypted,
2553 &bytes,
2554 );
2555 }
2556 }
2557 let buf = match &self.mmap {
2558 Some(m) => m
2561 .get(start..end)
2562 .ok_or_else(|| {
2563 MongrelError::InvalidArgument("run page is outside the mapped file".into())
2564 })?
2565 .to_vec(),
2566 None => {
2567 self.file.seek(SeekFrom::Start(offset))?;
2568 let mut buf = vec![0u8; compressed_len as usize];
2569 self.file.read_exact(&mut buf)?;
2570 buf
2571 }
2572 };
2573 if let Some(cache) = &self.page_cache {
2575 cache.lock(&key).insert(crate::page::CachedPage {
2576 committed_epoch: crate::epoch::Epoch(self.header.epoch_created),
2577 content_hash: key,
2578 bytes: bytes::Bytes::copy_from_slice(&buf),
2579 });
2580 }
2581 decrypt_or_passthrough(
2582 self.cipher.as_deref(),
2583 self.nonce_prefix,
2584 column_id,
2585 page_seq,
2586 encrypted,
2587 &buf,
2588 )
2589 }
2590
2591 fn read_page_shared(&self, column_id: u16, page_seq: usize) -> Result<Vec<u8>> {
2596 let ch = self.find_header(column_id)?;
2597 let stat = ch
2598 .page_stats
2599 .get(page_seq)
2600 .ok_or_else(|| MongrelError::InvalidArgument("page seq out of range".into()))?;
2601 let encrypted = ch.flags & ColumnPageHeader::PAGE_ENCRYPTED != 0;
2602 let key = page_cache_key(self.table_id, self.header.run_id, column_id, page_seq);
2605 if let Some(cache) = &self.page_cache {
2606 if let Some(guard) = cache.try_lock(&key) {
2607 if let Some(bytes) = guard.try_get(
2608 &key,
2609 crate::epoch::Snapshot::at(crate::epoch::Epoch(u64::MAX)),
2610 ) {
2611 return decrypt_or_passthrough(
2612 self.cipher.as_deref(),
2613 self.nonce_prefix,
2614 column_id,
2615 page_seq,
2616 encrypted,
2617 &bytes,
2618 );
2619 }
2620 }
2621 }
2622 let mmap = self.mmap.as_ref().ok_or_else(|| {
2623 MongrelError::InvalidArgument("parallel page decode requires an mmap-backed run".into())
2624 })?;
2625 let end = stat
2626 .offset
2627 .checked_add(stat.compressed_len as u64)
2628 .ok_or_else(|| MongrelError::InvalidArgument("run page offset overflows".into()))?;
2629 let start = usize::try_from(stat.offset)
2630 .map_err(|_| MongrelError::InvalidArgument("run page offset is too large".into()))?;
2631 let end = usize::try_from(end)
2632 .map_err(|_| MongrelError::InvalidArgument("run page end is too large".into()))?;
2633 let buf = mmap
2634 .get(start..end)
2635 .ok_or_else(|| {
2636 MongrelError::InvalidArgument("run page is outside the mapped file".into())
2637 })?
2638 .to_vec();
2639 if let Some(cache) = &self.page_cache {
2643 if let Some(mut guard) = cache.try_lock(&key) {
2644 guard.insert(crate::page::CachedPage {
2645 committed_epoch: crate::epoch::Epoch(self.header.epoch_created),
2646 content_hash: key,
2647 bytes: bytes::Bytes::copy_from_slice(&buf),
2648 });
2649 }
2650 }
2651 decrypt_or_passthrough(
2652 self.cipher.as_deref(),
2653 self.nonce_prefix,
2654 column_id,
2655 page_seq,
2656 encrypted,
2657 &buf,
2658 )
2659 }
2660
2661 fn column(&mut self, column_id: u16) -> Result<&[Value]> {
2663 if column_id == SYS_EPOCH {
2666 if let Some(ov) = self.epoch_override {
2667 if !self.col_cache.contains_key(&column_id) {
2668 let n = self.row_count();
2669 self.col_cache
2670 .insert(column_id, vec![Value::Int64(ov.0 as i64); n]);
2671 }
2672 return Ok(self.col_cache.get(&column_id).unwrap().as_slice());
2673 }
2674 }
2675 if !self.col_cache.contains_key(&column_id) {
2676 let ty = self.resolve_type(column_id);
2677 let page_rows: Vec<usize> = {
2678 let ch = self.find_header(column_id)?;
2679 ch.page_stats.iter().map(|s| s.row_count as usize).collect()
2680 };
2681 let mut decoded: Vec<Value> = Vec::with_capacity(self.row_count());
2682 for (seq, &pr) in page_rows.iter().enumerate() {
2683 let page = self.read_page(column_id, seq)?;
2684 decoded.extend(columnar::decode_page(ty.clone(), &page, pr)?);
2685 }
2686 self.col_cache.insert(column_id, decoded);
2687 }
2688 Ok(self.col_cache.get(&column_id).unwrap().as_slice())
2689 }
2690
2691 pub fn get_version(&mut self, row_id: RowId, snapshot: Epoch) -> Result<Option<(Epoch, Row)>> {
2705 match self.find_version_page(row_id, snapshot)? {
2706 None => Ok(None),
2707 Some((epoch, seq, local_index)) => Ok(Some((
2708 Epoch(epoch),
2709 self.materialize_in_page(seq, local_index)?,
2710 ))),
2711 }
2712 }
2713
2714 fn find_version_page(
2720 &mut self,
2721 row_id: RowId,
2722 snapshot: Epoch,
2723 ) -> Result<Option<(u64, usize, usize)>> {
2724 let n = self.row_count();
2725 if n == 0 {
2726 return Ok(None);
2727 }
2728 let target = row_id.0 as i64;
2729 let ch = self.find_header(SYS_ROW_ID)?;
2730 let mut page_start = 0u64;
2731 let candidate_pages: Vec<(usize, u64)> = ch .page_stats
2733 .iter()
2734 .enumerate()
2735 .filter_map(|(seq, s)| {
2736 let start = page_start;
2737 page_start += s.row_count as u64;
2738 (s.first_row_id <= row_id.0 && row_id.0 <= s.last_row_id).then_some((seq, start))
2739 })
2740 .collect();
2741 if candidate_pages.is_empty() {
2742 return Ok(None);
2743 }
2744 let ty = self.resolve_type(SYS_ROW_ID);
2745 let mut best: Option<(u64, usize, usize)> = None; for (seq, _page_row_start) in candidate_pages {
2747 let page_rows = self.find_header(SYS_ROW_ID)?.page_stats[seq].row_count as usize;
2748 let row_ids =
2749 match self.decode_page_native_cached(ty.clone(), SYS_ROW_ID, seq, page_rows)? {
2750 columnar::NativeColumn::Int64 { data, .. } => data,
2751 _ => return Err(MongrelError::InvalidArgument("sys row_id not int64".into())),
2752 };
2753 let local = match row_ids.binary_search(&target) {
2754 Ok(i) => i,
2755 Err(_) => continue,
2756 };
2757 let epoch_ty = self.resolve_type(SYS_EPOCH);
2758 let epochs =
2759 match self.decode_page_native_cached(epoch_ty, SYS_EPOCH, seq, page_rows)? {
2760 columnar::NativeColumn::Int64 { data, .. } => data,
2761 _ => return Err(MongrelError::InvalidArgument("sys epoch not int64".into())),
2762 };
2763 let mut lo = local;
2766 while lo > 0 && row_ids[lo - 1] == target {
2767 lo -= 1;
2768 }
2769 let mut hi = local;
2770 while hi + 1 < row_ids.len() && row_ids[hi + 1] == target {
2771 hi += 1;
2772 }
2773 for (i, &epoch) in epochs[lo..=hi].iter().enumerate() {
2774 let epoch = epoch as u64;
2775 if epoch <= snapshot.0 && best.map(|(be, ..)| epoch > be).unwrap_or(true) {
2776 best = Some((epoch, seq, lo + i));
2777 }
2778 }
2779 }
2780 Ok(best)
2781 }
2782
2783 pub fn get_version_column(
2791 &mut self,
2792 row_id: RowId,
2793 snapshot: Epoch,
2794 column_id: u16,
2795 ) -> Result<Option<(Epoch, bool, Option<Value>)>> {
2796 let Some((epoch, seq, local_index)) = self.find_version_page(row_id, snapshot)? else {
2797 return Ok(None);
2798 };
2799 let page_rows = self.find_header(SYS_ROW_ID)?.page_stats[seq].row_count as usize;
2800 let page_start: usize = self.find_header(SYS_ROW_ID)?.page_stats[..seq]
2801 .iter()
2802 .map(|s| s.row_count as usize)
2803 .sum();
2804 let global_index = page_start + local_index;
2805 let native_at = |slf: &mut Self, cid: u16| -> Result<Option<Value>> {
2806 if !slf.dir.iter().any(|h| h.column_id == cid) {
2807 return Ok(None);
2808 }
2809 let ty = slf.resolve_type(cid);
2810 if !matches!(
2811 ty,
2812 TypeId::Bool
2813 | TypeId::Int8
2814 | TypeId::Int16
2815 | TypeId::Int32
2816 | TypeId::Int64
2817 | TypeId::UInt8
2818 | TypeId::UInt16
2819 | TypeId::UInt32
2820 | TypeId::UInt64
2821 | TypeId::Float32
2822 | TypeId::Float64
2823 | TypeId::TimestampNanos
2824 | TypeId::Date32
2825 | TypeId::Bytes
2826 ) {
2827 return Ok(slf.column(cid)?.get(global_index).cloned());
2828 }
2829 Ok(slf
2830 .decode_page_native_cached(ty, cid, seq, page_rows)?
2831 .value_at(local_index))
2832 };
2833 let deleted = matches!(native_at(self, SYS_DELETED)?, Some(Value::Bool(true)));
2834 let value = native_at(self, column_id)?;
2835 Ok(Some((Epoch(epoch), deleted, value)))
2836 }
2837
2838 pub fn get_version_visibility(
2840 &mut self,
2841 row_id: RowId,
2842 snapshot: Epoch,
2843 ) -> Result<Option<(Epoch, bool)>> {
2844 let Some((epoch, seq, local_index)) = self.find_version_page(row_id, snapshot)? else {
2845 return Ok(None);
2846 };
2847 let page_rows = self.find_header(SYS_ROW_ID)?.page_stats[seq].row_count as usize;
2848 let deleted = match self.decode_page_native_cached(
2849 self.resolve_type(SYS_DELETED),
2850 SYS_DELETED,
2851 seq,
2852 page_rows,
2853 )? {
2854 columnar::NativeColumn::Bool { data, .. } => data[local_index] != 0,
2855 _ => return Err(MongrelError::InvalidArgument("sys deleted not bool".into())),
2856 };
2857 Ok(Some((Epoch(epoch), deleted)))
2858 }
2859
2860 fn materialize_in_page(&mut self, seq: usize, local_index: usize) -> Result<Row> {
2869 let page_rows = self.find_header(SYS_ROW_ID)?.page_stats[seq].row_count as usize;
2870 let page_start: usize = self.find_header(SYS_ROW_ID)?.page_stats[..seq]
2871 .iter()
2872 .map(|s| s.row_count as usize)
2873 .sum();
2874 let global_index = page_start + local_index;
2875 let native_at = |slf: &mut Self, column_id: u16| -> Result<Option<Value>> {
2876 if !slf.dir.iter().any(|h| h.column_id == column_id) {
2879 return Ok(None);
2880 }
2881 let ty = slf.resolve_type(column_id);
2882 if !matches!(
2883 ty,
2884 TypeId::Bool
2885 | TypeId::Int8
2886 | TypeId::Int16
2887 | TypeId::Int32
2888 | TypeId::Int64
2889 | TypeId::UInt8
2890 | TypeId::UInt16
2891 | TypeId::UInt32
2892 | TypeId::UInt64
2893 | TypeId::Float32
2894 | TypeId::Float64
2895 | TypeId::TimestampNanos
2896 | TypeId::Date32
2897 | TypeId::Bytes
2898 ) {
2899 return Ok(slf.column(column_id)?.get(global_index).cloned());
2902 }
2903 Ok(slf
2904 .decode_page_native_cached(ty, column_id, seq, page_rows)?
2905 .value_at(local_index))
2906 };
2907 let row_id = RowId(match native_at(self, SYS_ROW_ID)? {
2908 Some(Value::Int64(x)) => x as u64,
2909 _ => 0,
2910 });
2911 let committed_epoch = Epoch(match native_at(self, SYS_EPOCH)? {
2912 Some(Value::Int64(x)) => x as u64,
2913 _ => 0,
2914 });
2915 let deleted = matches!(native_at(self, SYS_DELETED)?, Some(Value::Bool(true)));
2916 let col_ids: Vec<u16> = self.schema.columns.iter().map(|c| c.id).collect();
2917 let mut columns = HashMap::new();
2918 for id in col_ids {
2919 columns.insert(id, native_at(self, id)?.unwrap_or(Value::Null));
2920 }
2921 Ok(Row {
2922 row_id,
2923 committed_epoch,
2924 columns,
2925 deleted,
2926 })
2927 }
2928
2929 pub fn all_rows(&mut self) -> Result<Vec<Row>> {
2932 let n = self.row_count();
2933 let mut out = Vec::with_capacity(n);
2934 for i in 0..n {
2935 out.push(self.materialize(i)?);
2936 }
2937 Ok(out)
2938 }
2939
2940 pub fn all_rows_controlled(
2942 &mut self,
2943 control: &crate::ExecutionControl,
2944 max_rows: usize,
2945 ) -> Result<Vec<Row>> {
2946 let n = self.row_count();
2947 if n > max_rows {
2948 return Err(MongrelError::ResourceLimitExceeded {
2949 resource: "controlled run row materialization",
2950 requested: n,
2951 limit: max_rows,
2952 });
2953 }
2954 let mut out = Vec::with_capacity(n);
2955 for i in 0..n {
2956 if i % 256 == 0 {
2957 control.checkpoint()?;
2958 }
2959 out.push(self.materialize(i)?);
2960 }
2961 control.checkpoint()?;
2962 Ok(out)
2963 }
2964
2965 pub fn visible_indices(&mut self, snapshot: Epoch) -> Result<Vec<usize>> {
2971 let n = self.row_count();
2972 if n == 0 {
2973 return Ok(Vec::new());
2974 }
2975 let row_ids = self.column(SYS_ROW_ID)?.to_vec();
2976 let epochs = self.column(SYS_EPOCH)?.to_vec();
2977 let deleted = self.column(SYS_DELETED)?.to_vec();
2978 let mut best: HashMap<u64, (u64, usize)> = HashMap::new();
2979 for i in 0..n {
2980 let rid = int_at(&row_ids, i);
2981 let e = int_at(&epochs, i);
2982 if e > snapshot.0 {
2983 continue;
2984 }
2985 best.entry(rid)
2986 .and_modify(|(be, bi)| {
2987 if e > *be {
2988 *be = e;
2989 *bi = i;
2990 }
2991 })
2992 .or_insert((e, i));
2993 }
2994 let mut idxs: Vec<usize> = best.into_values().map(|(_, i)| i).collect();
2995 idxs.retain(|&i| !bool_at(&deleted, i));
2996 idxs.sort_unstable();
2997 Ok(idxs)
2998 }
2999
3000 pub fn gather_column(&mut self, column_id: u16, indices: &[usize]) -> Result<Vec<Value>> {
3005 if !self.dir.iter().any(|h| h.column_id == column_id) {
3006 return Ok(vec![Value::Null; indices.len()]);
3007 }
3008 let col = self.column(column_id)?;
3009 Ok(indices
3010 .iter()
3011 .map(|&i| col.get(i).cloned().unwrap_or(Value::Null))
3012 .collect())
3013 }
3014
3015 pub fn column_native(&mut self, column_id: u16) -> Result<columnar::NativeColumn> {
3020 use rayon::prelude::*;
3021 if column_id == SYS_EPOCH {
3022 if let Some(ov) = self.epoch_override {
3023 return Ok(columnar::NativeColumn::int64_constant(
3024 ov.0 as i64,
3025 self.row_count(),
3026 ));
3027 }
3028 }
3029 let ty = self.resolve_type(column_id);
3030 let n = self.row_count();
3031 let Some(ch) = self.dir.iter().find(|h| h.column_id == column_id) else {
3032 return Ok(columnar::null_native(ty, n));
3033 };
3034 let page_count = ch.page_count as usize;
3035 let page_rows: Vec<usize> = ch.page_stats.iter().map(|s| s.row_count as usize).collect();
3036 if page_count == 0 {
3037 return Ok(columnar::null_native(ty, n));
3038 }
3039 let parts: Vec<columnar::NativeColumn> = if self.mmap.is_some() && page_count > 1 {
3040 let reader: &RunReader = self;
3044 (0..page_count)
3045 .into_par_iter()
3046 .map(|seq| {
3047 let raw = reader.read_page_shared(column_id, seq)?;
3048 columnar::decode_page_native(ty.clone(), &raw, page_rows[seq])
3049 })
3050 .collect::<Result<Vec<_>>>()?
3051 } else {
3052 let mut out = Vec::with_capacity(page_count);
3053 for (seq, &pr) in page_rows.iter().enumerate() {
3054 let page = self.read_page(column_id, seq)?;
3055 out.push(columnar::decode_page_native(ty.clone(), &page, pr)?);
3056 }
3057 out
3058 };
3059 Ok(columnar::NativeColumn::concat(&parts))
3060 }
3061
3062 pub fn has_mmap(&self) -> bool {
3066 self.mmap.is_some()
3067 }
3068
3069 pub fn column_native_shared(&self, column_id: u16) -> Result<columnar::NativeColumn> {
3076 use rayon::prelude::*;
3077 if column_id == SYS_EPOCH {
3078 if let Some(ov) = self.epoch_override {
3079 return Ok(columnar::NativeColumn::int64_constant(
3080 ov.0 as i64,
3081 self.row_count(),
3082 ));
3083 }
3084 }
3085 let ty = self.resolve_type(column_id);
3086 let n = self.row_count();
3087 let Some(ch) = self.dir.iter().find(|h| h.column_id == column_id) else {
3088 return Ok(columnar::null_native(ty, n));
3089 };
3090 let page_count = ch.page_count as usize;
3091 let page_rows: Vec<usize> = ch.page_stats.iter().map(|s| s.row_count as usize).collect();
3092 if page_count == 0 {
3093 return Ok(columnar::null_native(ty, n));
3094 }
3095 #[cfg(unix)]
3099 {
3100 if let (Some(m), Some(first)) = (&self.mmap, ch.page_stats.first()) {
3101 let start = first.offset as usize;
3102 let end = (ch.page_region_offset as usize) + (ch.page_region_len as usize);
3103 if end > start {
3104 let _ = m.advise_range(memmap2::Advice::WillNeed, start, end - start);
3105 }
3106 }
3107 }
3108 let run_id = self.header.run_id;
3109 let mut parts_keys: Vec<(columnar::NativeColumn, Option<[u8; 32]>)> = if page_count > 1 {
3113 (0..page_count)
3114 .into_par_iter()
3115 .map(|seq| {
3116 self.decode_page_cached(ty.clone(), column_id, seq, page_rows[seq], run_id)
3117 })
3118 .collect::<Result<Vec<_>>>()?
3119 } else {
3120 vec![self.decode_page_cached(ty, column_id, 0, page_rows[0], run_id)?]
3121 };
3122 if let Some(cache) = &self.decoded_cache {
3125 for (col, key) in parts_keys.iter_mut() {
3126 if let Some(k) = key.take() {
3127 cache.lock(&k).insert(k, Arc::new(col.clone()));
3128 }
3129 }
3130 }
3131 let parts: Vec<columnar::NativeColumn> = parts_keys.into_iter().map(|(c, _)| c).collect();
3132 Ok(columnar::NativeColumn::concat(&parts))
3133 }
3134
3135 fn decode_page_cached(
3140 &self,
3141 ty: TypeId,
3142 column_id: u16,
3143 seq: usize,
3144 nrows: usize,
3145 run_id: u128,
3146 ) -> Result<(columnar::NativeColumn, Option<[u8; 32]>)> {
3147 let key = page_cache_key(self.table_id, run_id, column_id, seq);
3148 if let Some(cache) = &self.decoded_cache {
3149 if let Some(g) = cache.try_lock(&key) {
3150 if let Some(hit) = g.try_get(&key) {
3151 return Ok(((*hit).clone(), None));
3152 }
3153 }
3154 }
3155 let raw = self.read_page_shared(column_id, seq)?;
3156 let col = columnar::decode_page_native(ty, &raw, nrows)?;
3157 Ok((col, Some(key)))
3158 }
3159
3160 fn decode_page_native_cached(
3171 &mut self,
3172 ty: TypeId,
3173 column_id: u16,
3174 seq: usize,
3175 nrows: usize,
3176 ) -> Result<columnar::NativeColumn> {
3177 let key = page_cache_key(self.table_id, self.header.run_id, column_id, seq);
3178 if let Some(cache) = &self.decoded_cache {
3179 if let Some(hit) = cache.lock(&key).try_get(&key) {
3180 return Ok((*hit).clone());
3181 }
3182 }
3183 let raw = self.read_page(column_id, seq)?;
3184 let col = columnar::decode_page_native(ty, &raw, nrows)?;
3185 if let Some(cache) = &self.decoded_cache {
3186 cache
3187 .lock(&key)
3188 .insert(key, std::sync::Arc::new(col.clone()));
3189 }
3190 Ok(col)
3191 }
3192
3193 pub fn range_row_ids_i64(
3198 &mut self,
3199 column_id: u16,
3200 lo: i64,
3201 hi: i64,
3202 ) -> Result<std::collections::HashSet<u64>> {
3203 Ok(self
3204 .range_row_id_set_i64(column_id, lo, hi)?
3205 .into_sorted_vec()
3206 .into_iter()
3207 .collect())
3208 }
3209
3210 pub(crate) fn range_row_id_set_i64(
3211 &mut self,
3212 column_id: u16,
3213 lo: i64,
3214 hi: i64,
3215 ) -> Result<RowIdSet> {
3216 let info: Vec<(Option<i64>, Option<i64>, usize)> =
3217 match self.dir.iter().find(|h| h.column_id == column_id) {
3218 Some(ch) => ch
3219 .page_stats
3220 .iter()
3221 .map(|s| {
3222 (
3223 be_i64(s.min.as_deref()),
3224 be_i64(s.max.as_deref()),
3225 s.row_count as usize,
3226 )
3227 })
3228 .collect(),
3229 None => return Ok(RowIdSet::empty()),
3230 };
3231 let stats_pruneable =
3237 !self.col_encrypted(column_id) || self.header.encrypted_stats_offset != 0;
3238 let clean_contiguous = self.clean_contiguous_row_ids();
3239 let mut out = Vec::new();
3240 let mut page_start = 0usize;
3241 for (seq, (mn, mx, nrows)) in info.into_iter().enumerate() {
3242 let current_page_start = page_start;
3243 page_start += nrows;
3244 let skip = stats_pruneable
3246 && match (mn, mx) {
3247 (Some(mn), Some(mx)) => mx < lo || mn > hi,
3248 _ => true,
3249 };
3250 if skip {
3251 continue;
3252 }
3253 let val_page = self.read_page(column_id, seq)?;
3254 let vals =
3255 columnar::decode_page_native(self.resolve_type(column_id), &val_page, nrows)?;
3256 if let columnar::NativeColumn::Int64 { data: v, validity } = vals {
3257 if clean_contiguous {
3258 for (i, val) in v.iter().enumerate() {
3259 if columnar::validity_bit(&validity, i) && *val >= lo && *val <= hi {
3260 out.push(self.header.min_row_id + current_page_start as u64 + i as u64);
3261 }
3262 }
3263 } else {
3264 let rid_page = self.read_page(SYS_ROW_ID, seq)?;
3265 let rids = columnar::decode_page_native(TypeId::Int64, &rid_page, nrows)?;
3266 if let columnar::NativeColumn::Int64 { data: r, .. } = rids {
3267 for (i, val) in v.iter().enumerate() {
3268 if columnar::validity_bit(&validity, i) && *val >= lo && *val <= hi {
3269 out.push(r[i] as u64);
3270 }
3271 }
3272 }
3273 }
3274 }
3275 }
3276 Ok(RowIdSet::from_unsorted(out))
3277 }
3278
3279 pub fn range_row_ids_f64(
3282 &mut self,
3283 column_id: u16,
3284 lo: f64,
3285 lo_inclusive: bool,
3286 hi: f64,
3287 hi_inclusive: bool,
3288 ) -> Result<std::collections::HashSet<u64>> {
3289 Ok(self
3290 .range_row_id_set_f64(column_id, lo, lo_inclusive, hi, hi_inclusive)?
3291 .into_sorted_vec()
3292 .into_iter()
3293 .collect())
3294 }
3295
3296 pub(crate) fn range_row_id_set_f64(
3297 &mut self,
3298 column_id: u16,
3299 lo: f64,
3300 lo_inclusive: bool,
3301 hi: f64,
3302 hi_inclusive: bool,
3303 ) -> Result<RowIdSet> {
3304 let info: Vec<(Option<f64>, Option<f64>, usize)> =
3305 match self.dir.iter().find(|h| h.column_id == column_id) {
3306 Some(ch) => ch
3307 .page_stats
3308 .iter()
3309 .map(|s| {
3310 (
3311 be_f64(s.min.as_deref()),
3312 be_f64(s.max.as_deref()),
3313 s.row_count as usize,
3314 )
3315 })
3316 .collect(),
3317 None => return Ok(RowIdSet::empty()),
3318 };
3319 let stats_pruneable =
3325 !self.col_encrypted(column_id) || self.header.encrypted_stats_offset != 0;
3326 let clean_contiguous = self.clean_contiguous_row_ids();
3327 let mut out = Vec::new();
3328 let mut page_start = 0usize;
3329 for (seq, (mn, mx, nrows)) in info.into_iter().enumerate() {
3330 let current_page_start = page_start;
3331 page_start += nrows;
3332 let skip = stats_pruneable
3335 && match (mn, mx) {
3336 (Some(mn), Some(mx)) => {
3337 let skip_lo = mx < lo || (!lo_inclusive && mx == lo);
3338 let skip_hi = mn > hi || (!hi_inclusive && mn == hi);
3339 skip_lo || skip_hi
3340 }
3341 _ => true,
3342 };
3343 if skip {
3344 continue;
3345 }
3346 let val_page = self.read_page(column_id, seq)?;
3347 let vals = columnar::decode_page_native(TypeId::Float64, &val_page, nrows)?;
3348 if let columnar::NativeColumn::Float64 { data: v, validity } = vals {
3349 if clean_contiguous {
3350 for (i, val) in v.iter().enumerate() {
3351 if !columnar::validity_bit(&validity, i) || val.is_nan() {
3352 continue;
3353 }
3354 let ok_lo = if lo_inclusive { *val >= lo } else { *val > lo };
3355 let ok_hi = if hi_inclusive { *val <= hi } else { *val < hi };
3356 if ok_lo && ok_hi {
3357 out.push(self.header.min_row_id + current_page_start as u64 + i as u64);
3358 }
3359 }
3360 } else {
3361 let rid_page = self.read_page(SYS_ROW_ID, seq)?;
3362 let rids = columnar::decode_page_native(TypeId::Int64, &rid_page, nrows)?;
3363 if let columnar::NativeColumn::Int64 { data: r, .. } = rids {
3364 for (i, val) in v.iter().enumerate() {
3365 if !columnar::validity_bit(&validity, i) || val.is_nan() {
3366 continue;
3367 }
3368 let ok_lo = if lo_inclusive { *val >= lo } else { *val > lo };
3369 let ok_hi = if hi_inclusive { *val <= hi } else { *val < hi };
3370 if ok_lo && ok_hi {
3371 out.push(r[i] as u64);
3372 }
3373 }
3374 }
3375 }
3376 }
3377 }
3378 Ok(RowIdSet::from_unsorted(out))
3379 }
3380
3381 pub(crate) fn null_row_id_set(&mut self, column_id: u16, want_nulls: bool) -> Result<RowIdSet> {
3386 let stats: Vec<(usize, usize)> = match self.dir.iter().find(|h| h.column_id == column_id) {
3387 Some(ch) => ch
3388 .page_stats
3389 .iter()
3390 .map(|s| (s.null_count as usize, s.row_count as usize))
3391 .collect(),
3392 None => return Ok(RowIdSet::empty()),
3393 };
3394 let ty = self.resolve_type(column_id);
3395 let clean_contiguous = self.clean_contiguous_row_ids();
3396 let mut out = Vec::new();
3397 let mut page_start = 0usize;
3398 for (seq, (null_count, nrows)) in stats.into_iter().enumerate() {
3399 let current_page_start = page_start;
3400 page_start += nrows;
3401 if want_nulls && null_count == 0 {
3403 continue;
3404 }
3405 if !want_nulls && null_count == nrows {
3406 continue;
3407 }
3408 let val_page = self.read_page(column_id, seq)?;
3409 let col = columnar::decode_page_native(ty.clone(), &val_page, nrows)?;
3410 let validity = col.validity();
3411 if clean_contiguous {
3412 for i in 0..nrows {
3413 let is_null = !columnar::validity_bit(validity, i);
3414 if is_null == want_nulls {
3415 out.push(self.header.min_row_id + current_page_start as u64 + i as u64);
3416 }
3417 }
3418 } else {
3419 let rid_page = self.read_page(SYS_ROW_ID, seq)?;
3420 let rids = columnar::decode_page_native(TypeId::Int64, &rid_page, nrows)?;
3421 if let columnar::NativeColumn::Int64 { data: r, .. } = rids {
3422 for (i, &rid) in r.iter().enumerate().take(nrows) {
3423 let is_null = !columnar::validity_bit(validity, i);
3424 if is_null == want_nulls {
3425 out.push(rid as u64);
3426 }
3427 }
3428 }
3429 }
3430 }
3431 Ok(RowIdSet::from_unsorted(out))
3432 }
3433
3434 pub fn visible_indices_native(&mut self, snapshot: Epoch) -> Result<Vec<usize>> {
3435 let n = self.row_count();
3436 if n == 0 {
3437 return Ok(Vec::new());
3438 }
3439 let (row_ids, epochs, deleted) = self.system_columns_native()?;
3440 let mut best: HashMap<u64, (u64, usize)> = HashMap::new();
3441 for i in 0..n {
3442 let rid = row_ids[i] as u64;
3443 let e = epochs[i] as u64;
3444 if e > snapshot.0 {
3445 continue;
3446 }
3447 best.entry(rid)
3448 .and_modify(|(be, bi)| {
3449 if e > *be {
3450 *be = e;
3451 *bi = i;
3452 }
3453 })
3454 .or_insert((e, i));
3455 }
3456 let mut idxs: Vec<usize> = best.into_values().map(|(_, i)| i).collect();
3457 idxs.retain(|&i| deleted[i] == 0);
3458 idxs.sort_unstable();
3459 Ok(idxs)
3460 }
3461
3462 pub fn range_row_ids_visible_i64(
3471 &mut self,
3472 column_id: u16,
3473 lo: i64,
3474 hi: i64,
3475 snapshot: Epoch,
3476 ) -> Result<Vec<u64>> {
3477 let stats: Vec<(Option<i64>, Option<i64>, usize)> = match self.column_page_stats(column_id)
3478 {
3479 Some(s) => s
3480 .iter()
3481 .map(|st| {
3482 (
3483 be_i64(st.min.as_deref()),
3484 be_i64(st.max.as_deref()),
3485 st.row_count as usize,
3486 )
3487 })
3488 .collect(),
3489 None => return Ok(Vec::new()),
3490 };
3491 let stats_pruneable =
3497 !self.col_encrypted(column_id) || self.header.encrypted_stats_offset != 0;
3498 let (positions, rids) = self.visible_positions_with_rids(snapshot)?;
3499 let mut out: Vec<u64> = Vec::new();
3500 let mut vis = 0usize;
3501 let mut page_start = 0usize;
3502 for (seq, &(mn, mx, nrows)) in stats.iter().enumerate() {
3503 let page_end = page_start + nrows;
3504 let skip = stats_pruneable
3506 && match (mn, mx) {
3507 (Some(mn), Some(mx)) => mx < lo || mn > hi,
3508 _ => true, };
3510 if !skip {
3511 let val_page = self.read_page(column_id, seq)?;
3512 let vals = columnar::decode_page_native(TypeId::Int64, &val_page, nrows)?;
3513 if let columnar::NativeColumn::Int64 { data: v, validity } = vals {
3514 while vis < positions.len() && positions[vis] < page_end {
3515 let local = positions[vis] - page_start;
3516 if columnar::validity_bit(&validity, local)
3517 && v[local] >= lo
3518 && v[local] <= hi
3519 {
3520 out.push(rids[vis] as u64);
3521 }
3522 vis += 1;
3523 }
3524 }
3525 } else {
3526 while vis < positions.len() && positions[vis] < page_end {
3527 vis += 1;
3528 }
3529 }
3530 page_start = page_end;
3531 }
3532 Ok(out)
3533 }
3534
3535 pub fn range_row_ids_visible_f64(
3538 &mut self,
3539 column_id: u16,
3540 lo: f64,
3541 lo_inclusive: bool,
3542 hi: f64,
3543 hi_inclusive: bool,
3544 snapshot: Epoch,
3545 ) -> Result<Vec<u64>> {
3546 let stats: Vec<(Option<f64>, Option<f64>, usize)> = match self.column_page_stats(column_id)
3547 {
3548 Some(s) => s
3549 .iter()
3550 .map(|st| {
3551 (
3552 be_f64(st.min.as_deref()),
3553 be_f64(st.max.as_deref()),
3554 st.row_count as usize,
3555 )
3556 })
3557 .collect(),
3558 None => return Ok(Vec::new()),
3559 };
3560 let stats_pruneable =
3566 !self.col_encrypted(column_id) || self.header.encrypted_stats_offset != 0;
3567 let (positions, rids) = self.visible_positions_with_rids(snapshot)?;
3568 let mut out: Vec<u64> = Vec::new();
3569 let mut vis = 0usize;
3570 let mut page_start = 0usize;
3571 for (seq, &(mn, mx, nrows)) in stats.iter().enumerate() {
3572 let page_end = page_start + nrows;
3573 let skip = stats_pruneable
3574 && match (mn, mx) {
3575 (Some(mn), Some(mx)) => {
3576 let skip_lo = mx < lo || (!lo_inclusive && mx == lo);
3577 let skip_hi = mn > hi || (!hi_inclusive && mn == hi);
3578 skip_lo || skip_hi
3579 }
3580 _ => true,
3581 };
3582 if !skip {
3583 let val_page = self.read_page(column_id, seq)?;
3584 let vals = columnar::decode_page_native(TypeId::Float64, &val_page, nrows)?;
3585 if let columnar::NativeColumn::Float64 { data: v, validity } = vals {
3586 while vis < positions.len() && positions[vis] < page_end {
3587 let local = positions[vis] - page_start;
3588 if columnar::validity_bit(&validity, local) && !v[local].is_nan() {
3589 let val = v[local];
3590 let ok_lo = if lo_inclusive { val >= lo } else { val > lo };
3591 let ok_hi = if hi_inclusive { val <= hi } else { val < hi };
3592 if ok_lo && ok_hi {
3593 out.push(rids[vis] as u64);
3594 }
3595 }
3596 vis += 1;
3597 }
3598 }
3599 } else {
3600 while vis < positions.len() && positions[vis] < page_end {
3601 vis += 1;
3602 }
3603 }
3604 page_start = page_end;
3605 }
3606 Ok(out)
3607 }
3608
3609 pub fn null_row_ids_visible(
3615 &mut self,
3616 column_id: u16,
3617 want_nulls: bool,
3618 snapshot: Epoch,
3619 ) -> Result<Vec<u64>> {
3620 let stats: Vec<(usize, usize)> = match self.column_page_stats(column_id) {
3621 Some(s) => s
3622 .iter()
3623 .map(|st| (st.null_count as usize, st.row_count as usize))
3624 .collect(),
3625 None => return Ok(Vec::new()),
3626 };
3627 let ty = self.resolve_type(column_id);
3628 let (positions, rids) = self.visible_positions_with_rids(snapshot)?;
3629 let mut out: Vec<u64> = Vec::new();
3630 let mut vis = 0usize;
3631 let mut page_start = 0usize;
3632 for (seq, &(null_count, nrows)) in stats.iter().enumerate() {
3633 let page_end = page_start + nrows;
3634 let skip = (want_nulls && null_count == 0) || (!want_nulls && null_count == nrows);
3635 if !skip {
3636 let val_page = self.read_page(column_id, seq)?;
3637 let col = columnar::decode_page_native(ty.clone(), &val_page, nrows)?;
3638 let validity = col.validity();
3639 while vis < positions.len() && positions[vis] < page_end {
3640 let local = positions[vis] - page_start;
3641 let is_null = !columnar::validity_bit(validity, local);
3642 if is_null == want_nulls {
3643 out.push(rids[vis] as u64);
3644 }
3645 vis += 1;
3646 }
3647 } else {
3648 while vis < positions.len() && positions[vis] < page_end {
3649 vis += 1;
3650 }
3651 }
3652 page_start = page_end;
3653 }
3654 Ok(out)
3655 }
3656
3657 pub fn visible_positions_with_rids(
3661 &mut self,
3662 snapshot: Epoch,
3663 ) -> Result<(Vec<usize>, Vec<i64>)> {
3664 let n = self.row_count();
3665 if n == 0 {
3666 return Ok((Vec::new(), Vec::new()));
3667 }
3668 if self.is_clean()
3677 && self.epoch_override.is_none()
3678 && self.header.epoch_created <= snapshot.0
3679 {
3680 let row_ids = match self.column_native_shared(SYS_ROW_ID)? {
3681 columnar::NativeColumn::Int64 { data, .. } => data,
3682 _ => return Err(MongrelError::InvalidArgument("sys row_id not int64".into())),
3683 };
3684 let positions: Vec<usize> = (0..n).collect();
3685 return Ok((positions, row_ids));
3686 }
3687 let (row_ids, epochs, deleted) = self.system_columns_native()?;
3688 let mut idxs: Vec<usize> = Vec::new();
3699 let mut i = 0;
3700 while i < n {
3701 let rid = row_ids[i] as u64;
3702 let mut best: Option<usize> = None;
3705 let mut j = i;
3706 while j < n && row_ids[j] as u64 == rid {
3707 if epochs[j] as u64 <= snapshot.0 {
3708 best = Some(j);
3709 }
3710 j += 1;
3711 }
3712 if let Some(b) = best {
3713 if deleted[b] == 0 {
3714 idxs.push(b);
3715 }
3716 }
3717 i = j;
3718 }
3719 let rids: Vec<i64> = idxs.iter().map(|&k| row_ids[k]).collect();
3721 Ok((idxs, rids))
3722 }
3723
3724 pub fn page_row_counts(&self, column_id: u16) -> Result<Vec<usize>> {
3728 Ok(self
3729 .find_header(column_id)?
3730 .page_stats
3731 .iter()
3732 .map(|s| s.row_count as usize)
3733 .collect())
3734 }
3735
3736 pub fn has_column(&self, column_id: u16) -> bool {
3739 self.dir.iter().any(|h| h.column_id == column_id)
3740 }
3741
3742 pub fn column_page_stats(&self, column_id: u16) -> Option<&[crate::page::PageStat]> {
3746 self.dir
3747 .iter()
3748 .find(|h| h.column_id == column_id)
3749 .map(|ch| ch.page_stats.as_slice())
3750 }
3751
3752 pub(crate) fn system_columns_native(&mut self) -> Result<(Vec<i64>, Vec<i64>, Vec<u8>)> {
3757 let row_ids = match self.column_native_shared(SYS_ROW_ID)? {
3758 columnar::NativeColumn::Int64 { data, .. } => data,
3759 _ => return Err(MongrelError::InvalidArgument("sys row_id not int64".into())),
3760 };
3761 let epochs = match self.column_native_shared(SYS_EPOCH)? {
3762 columnar::NativeColumn::Int64 { data, .. } => data,
3763 _ => return Err(MongrelError::InvalidArgument("sys epoch not int64".into())),
3764 };
3765 let deleted = match self.column_native_shared(SYS_DELETED)? {
3766 columnar::NativeColumn::Bool { data, .. } => data,
3767 _ => return Err(MongrelError::InvalidArgument("sys deleted not bool".into())),
3768 };
3769 Ok((row_ids, epochs, deleted))
3770 }
3771
3772 pub fn visible_versions(&mut self, snapshot: Epoch) -> Result<Vec<Row>> {
3776 let n = self.row_count();
3777 if n == 0 {
3778 return Ok(Vec::new());
3779 }
3780 let row_ids = self.column(SYS_ROW_ID)?.to_vec();
3781 let epochs = self.column(SYS_EPOCH)?.to_vec();
3782 let mut best: HashMap<u64, (u64, usize)> = HashMap::new();
3783 for i in 0..n {
3784 let rid = int_at(&row_ids, i);
3785 let epoch = int_at(&epochs, i);
3786 if epoch > snapshot.0 {
3787 continue;
3788 }
3789 best.entry(rid)
3790 .and_modify(|e| {
3791 if epoch > e.0 {
3792 *e = (epoch, i);
3793 }
3794 })
3795 .or_insert((epoch, i));
3796 }
3797 let mut picks: Vec<usize> = best.into_values().map(|(_, i)| i).collect();
3798 picks.sort();
3799 let mut out = Vec::with_capacity(picks.len());
3800 for i in picks {
3801 out.push(self.materialize(i)?);
3802 }
3803 Ok(out)
3804 }
3805
3806 pub fn visible_rows(&mut self, snapshot: Epoch) -> Result<Vec<Row>> {
3808 Ok(self
3809 .visible_versions(snapshot)?
3810 .into_iter()
3811 .filter(|r| !r.deleted)
3812 .collect())
3813 }
3814
3815 pub(crate) fn materialize(&mut self, index: usize) -> Result<Row> {
3816 let row_id = RowId(int_at(self.column(SYS_ROW_ID)?, index));
3817 let epoch = Epoch(int_at(self.column(SYS_EPOCH)?, index));
3818 let deleted = bool_at(self.column(SYS_DELETED)?, index);
3819 let col_ids: Vec<u16> = self.schema.columns.iter().map(|c| c.id).collect();
3820 let mut columns = HashMap::new();
3821 for id in col_ids {
3822 let val = if self.dir.iter().any(|h| h.column_id == id) {
3823 self.column(id)?.get(index).cloned().unwrap_or(Value::Null)
3824 } else {
3825 Value::Null
3827 };
3828 columns.insert(id, val);
3829 }
3830 Ok(Row {
3831 row_id,
3832 committed_epoch: epoch,
3833 columns,
3834 deleted,
3835 })
3836 }
3837
3838 pub(crate) fn materialize_batch(&mut self, indices: &[usize]) -> Result<Vec<Row>> {
3847 if indices.is_empty() {
3848 return Ok(Vec::new());
3849 }
3850 use std::collections::HashMap;
3851 let rid_col = self.column_native_shared(SYS_ROW_ID)?;
3852 let epoch_col = self.column_native_shared(SYS_EPOCH)?;
3853 let del_col = self.column_native_shared(SYS_DELETED)?;
3854 let mut user: HashMap<u16, columnar::NativeColumn> = HashMap::new();
3855 let present: Vec<u16> = self
3856 .schema
3857 .columns
3858 .iter()
3859 .map(|c| c.id)
3860 .filter(|id| self.dir.iter().any(|h| h.column_id == *id))
3861 .collect();
3862 for id in present {
3863 user.insert(id, self.column_native(id)?);
3864 }
3865 let i64_at = |col: &columnar::NativeColumn, i: usize| -> i64 {
3866 match col {
3867 columnar::NativeColumn::Int64 { data, .. } => data.get(i).copied().unwrap_or(0),
3868 _ => 0,
3869 }
3870 };
3871 let bool_at_native = |col: &columnar::NativeColumn, i: usize| -> bool {
3872 match col {
3873 columnar::NativeColumn::Bool { data, .. } => {
3874 data.get(i).copied().map(|b| b != 0).unwrap_or(false)
3875 }
3876 _ => false,
3877 }
3878 };
3879 let mut rows = Vec::with_capacity(indices.len());
3880 for &idx in indices {
3881 let row_id = RowId(i64_at(&rid_col, idx) as u64);
3882 let epoch = Epoch(i64_at(&epoch_col, idx) as u64);
3883 let deleted = bool_at_native(&del_col, idx);
3884 let mut columns = HashMap::with_capacity(self.schema.columns.len());
3885 for cdef in self.schema.columns.iter() {
3886 let val = match user.get(&cdef.id) {
3887 Some(col) => col.value_at(idx).unwrap_or(Value::Null),
3888 None => Value::Null,
3889 };
3890 columns.insert(cdef.id, val);
3891 }
3892 rows.push(Row {
3893 row_id,
3894 committed_epoch: epoch,
3895 columns,
3896 deleted,
3897 });
3898 }
3899 Ok(rows)
3900 }
3901}
3902
3903fn int_at(vals: &[Value], i: usize) -> u64 {
3904 match vals.get(i) {
3905 Some(Value::Int64(x)) => *x as u64,
3906 _ => 0,
3907 }
3908}
3909
3910fn bool_at(vals: &[Value], i: usize) -> bool {
3911 matches!(vals.get(i), Some(Value::Bool(true)))
3912}
3913
3914#[cfg(test)]
3915mod tests {
3916 use super::*;
3917 use crate::columnar::NativeColumn;
3918 use crate::memtable::Value;
3919 use crate::rowid::RowId;
3920 use crate::schema::{ColumnDef, ColumnFlags};
3921 use tempfile::tempdir;
3922
3923 fn schema() -> Schema {
3924 Schema {
3925 schema_id: 1,
3926 columns: vec![
3927 ColumnDef {
3928 id: 1,
3929 name: "id".into(),
3930 ty: TypeId::Int64,
3931 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
3932 default_value: None,
3933 },
3934 ColumnDef {
3935 id: 2,
3936 name: "name".into(),
3937 ty: TypeId::Bytes,
3938 flags: ColumnFlags::empty(),
3939 default_value: None,
3940 },
3941 ],
3942 indexes: Vec::new(),
3943 colocation: vec![],
3944 constraints: Default::default(),
3945 clustered: false,
3946 }
3947 }
3948
3949 fn rows() -> Vec<Row> {
3950 vec![
3951 Row::new(RowId(1), Epoch(10))
3952 .with_column(1, Value::Int64(1))
3953 .with_column(2, Value::Bytes(b"alice".to_vec())),
3954 Row::new(RowId(2), Epoch(10))
3955 .with_column(1, Value::Int64(2))
3956 .with_column(2, Value::Bytes(b"bob".to_vec())),
3957 Row::new(RowId(3), Epoch(10))
3958 .with_column(1, Value::Int64(3))
3959 .with_column(2, Value::Bytes(b"carol".to_vec())),
3960 ]
3961 }
3962
3963 #[test]
3964 fn flush_then_read_mvcc() {
3965 let dir = tempdir().unwrap();
3966 let path = dir.path().join("r-1.sr");
3967 let header = RunWriter::new(&schema(), 1, Epoch(10), 0)
3968 .write(&path, &rows())
3969 .unwrap();
3970 assert_eq!(header.row_count, 3);
3971
3972 let mut r = RunReader::open(&path, schema(), None).unwrap();
3973 let (e, row) = r.get_version(RowId(2), Epoch(20)).unwrap().unwrap();
3975 assert_eq!(e, Epoch(10));
3976 assert_eq!(row.row_id, RowId(2));
3977 assert!(matches!(row.columns.get(&2), Some(Value::Bytes(_))));
3978 assert!(r.get_version(RowId(99), Epoch(20)).unwrap().is_none());
3980 let all = r.visible_rows(Epoch(20)).unwrap();
3982 assert_eq!(all.len(), 3);
3983 }
3984
3985 #[test]
3986 fn visible_version_cursor_preserves_order_snapshot_and_tombstones() {
3987 let dir = tempdir().unwrap();
3988 let path = dir.path().join("r-cursor.sr");
3989 let mut tombstone = Row::new(RowId(2), Epoch(2));
3990 tombstone.deleted = true;
3991 let versions = vec![
3992 Row::new(RowId(1), Epoch(4)).with_column(1, Value::Int64(10)),
3993 Row::new(RowId(2), Epoch(1)).with_column(1, Value::Int64(2)),
3994 tombstone,
3995 Row::new(RowId(3), Epoch(1)).with_column(1, Value::Int64(20)),
3996 Row::new(RowId(3), Epoch(3)).with_column(1, Value::Int64(23)),
3997 Row::new(RowId(4), Epoch(4)).with_column(1, Value::Int64(30)),
3998 ];
3999 RunWriter::new(&schema(), 7, Epoch(4), 0)
4000 .write(&path, &versions)
4001 .unwrap();
4002
4003 let control = crate::ExecutionControl::new(None);
4004 let mut cursor = RunReader::open(&path, schema(), None)
4005 .unwrap()
4006 .into_visible_version_cursor(Epoch(2))
4007 .unwrap();
4008 let first = cursor.next_visible_version(&control).unwrap().unwrap();
4009 assert_eq!(first.row_id, RowId(2));
4010 assert!(first.deleted);
4011 assert_eq!(first.committed_epoch, Epoch(2));
4012 let second = cursor.next_visible_version(&control).unwrap().unwrap();
4013 assert_eq!(second.row_id, RowId(3));
4014 assert_eq!(second.committed_epoch, Epoch(1));
4015 let row = cursor.materialize(second, &control).unwrap();
4016 assert_eq!(row.columns.get(&1), Some(&Value::Int64(20)));
4017 assert!(cursor.next_visible_version(&control).unwrap().is_none());
4018 }
4019
4020 #[test]
4021 fn learned_index_was_stored() {
4022 let dir = tempdir().unwrap();
4023 let path = dir.path().join("r-2.sr");
4024 RunWriter::new(&schema(), 2, Epoch(1), 0)
4025 .write(&path, &rows())
4026 .unwrap();
4027 let header = read_header(&path).unwrap();
4028 assert!(
4029 header.index_trailer_offset != 0,
4030 "trailer should be present"
4031 );
4032 }
4033
4034 #[test]
4035 fn low_level_container_still_round_trips() {
4036 let dir = tempdir().unwrap();
4037 let path = dir.path().join("r-3.sr");
4038 let cols = vec![ColumnPayload {
4039 column_id: 1,
4040 type_id_tag: 8,
4041 encoding: Encoding::Plain,
4042 pages: vec![vec![1, 2, 3, 4]],
4043 page_stats: Vec::new(),
4044 }];
4045 let header = write_run(
4046 &path,
4047 &RunSpec {
4048 run_id: 1,
4049 schema_id: 1,
4050 epoch_created: 1,
4051 level: 0,
4052 flags: 0,
4053 sort_key_column_id: SORT_KEY_ROW_ID,
4054 row_count: 1,
4055 min_row_id: 0,
4056 max_row_id: 0,
4057 columns: &cols,
4058 },
4059 )
4060 .unwrap();
4061 let back = read_header(&path).unwrap();
4062 assert_eq!(back.content_hash, header.content_hash);
4063 assert_eq!(read_column_dir(&path, &back).unwrap().len(), 1);
4064 }
4065
4066 #[test]
4067 fn detects_corruption() {
4068 let dir = tempdir().unwrap();
4069 let path = dir.path().join("r-4.sr");
4070 write_run(
4071 &path,
4072 &RunSpec {
4073 run_id: 1,
4074 schema_id: 1,
4075 epoch_created: 1,
4076 level: 0,
4077 flags: 0,
4078 sort_key_column_id: SORT_KEY_ROW_ID,
4079 row_count: 1,
4080 min_row_id: 0,
4081 max_row_id: 0,
4082 columns: &[ColumnPayload {
4083 column_id: 1,
4084 type_id_tag: 8,
4085 encoding: Encoding::Plain,
4086 pages: vec![vec![1, 2, 3]],
4087 page_stats: Vec::new(),
4088 }],
4089 },
4090 )
4091 .unwrap();
4092 let mut bytes = std::fs::read(&path).unwrap();
4093 bytes[300] ^= 0xFF;
4094 std::fs::write(&path, bytes).unwrap();
4095 let err = read_header(&path).unwrap_err();
4096 assert!(
4097 matches!(err, MongrelError::ChecksumMismatch { .. }),
4098 "got {err:?}"
4099 );
4100 }
4101
4102 #[test]
4108 fn mmap_and_vec_writers_are_byte_identical() {
4109 let dir = tempdir().unwrap();
4110 let stats = vec![PageStat {
4111 first_row_id: 0,
4112 last_row_id: 1,
4113 null_count: 0,
4114 row_count: 2,
4115 min: Some(vec![0]),
4116 max: Some(vec![9]),
4117 offset: 0,
4118 compressed_len: 0,
4119 uncompressed_len: 0,
4120 }];
4121 let spec = RunSpec {
4122 run_id: 7,
4123 schema_id: 1,
4124 epoch_created: 10,
4125 level: 0,
4126 flags: 0,
4127 sort_key_column_id: SORT_KEY_ROW_ID,
4128 row_count: 2,
4129 min_row_id: 0,
4130 max_row_id: 1,
4131 columns: &[
4132 ColumnPayload {
4133 column_id: 1,
4134 type_id_tag: 8,
4135 encoding: Encoding::Plain,
4136 pages: vec![vec![1, 2, 3, 4], vec![5, 6, 7, 8]],
4137 page_stats: stats.clone(),
4138 },
4139 ColumnPayload {
4140 column_id: 2,
4141 type_id_tag: 8,
4142 encoding: Encoding::Plain,
4143 pages: vec![vec![10, 20], vec![30, 40]],
4144 page_stats: stats.clone(),
4145 },
4146 ],
4147 };
4148 let trailer = b"learned-trailer-bytes";
4149
4150 let plan = plan_run(&spec, None, Some(trailer)).expect("plan");
4152 let mut buf = vec![0u8; plan.total];
4153 let h_place = place_run(&spec, None, Some(trailer), &plan, &mut buf)
4154 .expect("place_run into Vec succeeds");
4155
4156 let path_vec = dir.path().join("vec.sr");
4158 let h_vec = write_run_vec(&path_vec, &spec, None, Some(trailer)).unwrap();
4159 let bv = std::fs::read(&path_vec).unwrap();
4160
4161 assert_eq!(h_place.content_hash, h_vec.content_hash);
4162 assert_eq!(h_place.footer_offset, h_vec.footer_offset);
4163 assert_eq!(
4164 buf, bv,
4165 "place_run and write_run_vec must be byte-identical"
4166 );
4167 assert!(read_header(&path_vec).is_ok());
4168
4169 let path_mmap = dir.path().join("mmap.sr");
4173 match write_run_mmap(&path_mmap, &spec, None, Some(trailer)) {
4174 Ok(h_mmap) => {
4175 let bm = std::fs::read(&path_mmap).unwrap();
4176 assert_eq!(bm, bv, "mmap run must be byte-identical to vec run");
4177 assert_eq!(h_mmap.content_hash, h_vec.content_hash);
4178 }
4179 Err(e) if is_mmap_unavailable(&e) => {
4180 eprintln!("note: file mmap unavailable here; vec path covered (1)/(2)");
4181 }
4182 Err(e) => panic!("unexpected mmap error: {e:?}"),
4183 }
4184 }
4185
4186 #[test]
4191 fn column_native_shared_matches_column_native() {
4192 let dir = tempdir().unwrap();
4193 let path = dir.path().join("r.sr");
4194 let n = 65_536 * 2 + 9;
4197 let id_col = NativeColumn::int64_sequence(1, n);
4198 let mut offsets = vec![0u32];
4199 let mut values = Vec::new();
4200 for i in 0..n {
4201 values.extend_from_slice(format!("v{}", i % 17).as_bytes());
4202 offsets.push(values.len() as u32);
4203 }
4204 let name_col = NativeColumn::Bytes {
4205 offsets,
4206 values,
4207 validity: vec![0xFF; n.div_ceil(8)],
4208 };
4209 let header = RunWriter::new(&schema(), 9, Epoch(5), 0)
4210 .write_native(&path, &[(1, id_col), (2, name_col)], n, 1)
4211 .unwrap();
4212
4213 let mut reader = RunReader::open_with_cache(&path, schema(), None, None, None, 0, None)
4214 .expect("open reader");
4215 assert!(reader.has_mmap(), "test env must support read-only mmap");
4216 assert_eq!(reader.row_count(), header.row_count as usize);
4217
4218 for cid in [1u16, 2] {
4219 let a = reader.column_native(cid).expect("column_native");
4220 let b = reader
4221 .column_native_shared(cid)
4222 .expect("column_native_shared");
4223 assert_eq!(a.len(), b.len(), "len mismatch col {cid}");
4224 match (&a, &b) {
4226 (
4227 NativeColumn::Int64 {
4228 data: da,
4229 validity: va,
4230 },
4231 NativeColumn::Int64 {
4232 data: db,
4233 validity: vb,
4234 },
4235 ) => {
4236 assert_eq!(da, db, "Int64 data col {cid}");
4237 assert_eq!(va, vb, "Int64 validity col {cid}");
4238 }
4239 (
4240 NativeColumn::Bytes {
4241 offsets: oa,
4242 values: ua,
4243 validity: va,
4244 },
4245 NativeColumn::Bytes {
4246 offsets: ob,
4247 values: ub,
4248 validity: vb,
4249 },
4250 ) => {
4251 assert_eq!(oa, ob, "Bytes offsets col {cid}");
4252 assert_eq!(ua, ub, "Bytes values col {cid}");
4253 assert_eq!(va, vb, "Bytes validity col {cid}");
4254 }
4255 _ => panic!("type mismatch col {cid}: {a:?} vs {b:?}"),
4256 }
4257 }
4258 }
4259
4260 #[test]
4261 fn page_cache_key_distinguishes_tables() {
4262 let a = page_cache_key(1, 5, 2, 3);
4263 let b = page_cache_key(2, 5, 2, 3);
4264 assert_ne!(a, b, "same run/col/page, different table must differ");
4265 assert_ne!(page_cache_key(1, 5, 2, 3), page_cache_key(1, 6, 2, 3));
4267 assert_ne!(page_cache_key(1, 5, 2, 3), page_cache_key(1, 5, 2, 4));
4268 }
4269}