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