1use crate::columnar;
19use crate::encryption::{setup_run_encryption, Cipher, Kek, RunEncryption};
20use crate::epoch::{Epoch, Snapshot, VersionStamp};
21use crate::error::{MongrelError, Result};
22use crate::index::pgm::PgmIndex;
23use crate::memtable::{Row, Value};
24use crate::page::{Encoding, PageStat};
25use crate::row_id_set::RowIdSet;
26use crate::rowid::RowId;
27use crate::schema::{Schema, TypeId};
28use mongreldb_types::hlc::HlcTimestamp;
29use serde::{Deserialize, Serialize};
30use sha2::{Digest, Sha256};
31use std::collections::HashMap;
32use std::fs::{File, OpenOptions};
33use std::io::{Read, Seek, SeekFrom, Write};
34use std::path::{Path, PathBuf};
35use std::sync::Arc;
36
37pub const RUN_MAGIC: [u8; 8] = *b"MONGRRUN";
38pub const RUN_FORMAT_VERSION: u16 = 1;
39pub const RUN_HEADER_VERSION: u16 = 2;
45pub const RUN_HEADER_PAD: usize = 256;
46const MAX_RUN_PAGE_BYTES: u64 = 64 * 1024 * 1024 + 32;
47
48const ENC_STATS_NONCE_COLUMN: u16 = u16::MAX;
53const ENC_STATS_NONCE_SEQ: u32 = u16::MAX as u32;
54
55type PageMinMax = (Option<Vec<u8>>, Option<Vec<u8>>);
57
58type EncryptedColumnStats = Vec<(u16, Vec<PageMinMax>)>;
65
66const GCM_TAG_LEN: usize = 16;
71
72fn on_disk_len(page_len: usize, encrypted: bool) -> usize {
75 if encrypted {
76 page_len + GCM_TAG_LEN
77 } else {
78 page_len
79 }
80}
81
82pub const RUN_FLAG_ENCRYPTED: u8 = 1 << 0;
83pub const RUN_FLAG_TOMBSTONE_ONLY: u8 = 1 << 1;
84pub const RUN_FLAG_CLEAN: u8 = 1 << 2;
93pub const RUN_FLAG_UNIFORM_EPOCH: u8 = 1 << 3;
101pub const SORT_KEY_ROW_ID: u16 = 0xFFFF;
102
103pub const SYS_ROW_ID: u16 = 0xFFFE;
105pub const SYS_EPOCH: u16 = 0xFFFD;
106pub const SYS_DELETED: u16 = 0xFFFC;
107pub const SYS_COMMIT_TS: u16 = 0xFFFB;
113
114const COMMIT_TS_BYTES: usize = 16;
116
117fn encode_commit_ts_value(ts: Option<HlcTimestamp>) -> Value {
119 match ts {
120 None => Value::Null,
121 Some(ts) => {
122 let mut buf = [0u8; COMMIT_TS_BYTES];
123 buf[0..8].copy_from_slice(&ts.physical_micros.to_le_bytes());
124 buf[8..12].copy_from_slice(&ts.logical.to_le_bytes());
125 buf[12..16].copy_from_slice(&ts.node_tiebreaker.to_le_bytes());
126 Value::Bytes(buf.to_vec())
127 }
128 }
129}
130
131pub(crate) fn decode_commit_ts_value(value: Option<&Value>) -> Option<HlcTimestamp> {
133 match value {
134 Some(Value::Bytes(bytes)) if bytes.len() == COMMIT_TS_BYTES => {
135 let physical_micros = u64::from_le_bytes(bytes[0..8].try_into().ok()?);
136 let logical = u32::from_le_bytes(bytes[8..12].try_into().ok()?);
137 let node_tiebreaker = u32::from_le_bytes(bytes[12..16].try_into().ok()?);
138 Some(HlcTimestamp {
139 physical_micros,
140 logical,
141 node_tiebreaker,
142 })
143 }
144 _ => None,
145 }
146}
147
148#[derive(Debug, Clone)]
152pub(crate) struct VersionedColumnValue {
153 pub stamp: VersionStamp,
154 pub deleted: bool,
155 pub value: Option<Value>,
156}
157
158#[derive(Debug, Clone, Copy)]
164pub(crate) struct VersionVisibility {
165 pub stamp: VersionStamp,
166 pub deleted: bool,
167}
168
169type FoundVersionPage = (u64, Option<HlcTimestamp>, usize, usize);
174
175fn is_system_column_id(column_id: u16) -> bool {
177 matches!(
178 column_id,
179 SYS_ROW_ID | SYS_EPOCH | SYS_DELETED | SYS_COMMIT_TS
180 )
181}
182
183const LEARNED_EPSILON: usize = 64;
186
187fn build_learned_trailer(row_ids: &[Value]) -> Vec<u8> {
191 let points: Vec<(u64, usize)> = row_ids
192 .iter()
193 .enumerate()
194 .filter_map(|(i, v)| match v {
195 Value::Int64(r) => Some((*r as u64, i)),
196 _ => None,
197 })
198 .collect();
199 let pgm = PgmIndex::build(&points, LEARNED_EPSILON);
200 bincode::serialize(&pgm).expect("pgm serialize")
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct RunHeader {
205 pub magic: [u8; 8],
206 pub format_version: u16,
207 pub header_layout_version: u16,
208 pub run_id: u128,
209 pub content_hash: [u8; 32],
210 pub schema_id: u64,
211 pub epoch_created: u64,
212 pub level: u8,
213 pub flags: u8,
214 pub sort_key_column_id: u16,
215 pub row_count: u64,
216 pub min_row_id: u64,
217 pub max_row_id: u64,
218 pub column_count: u64,
219 pub column_dir_offset: u64,
220 pub index_trailer_offset: u64,
221 pub encryption_descriptor_offset: u64,
222 pub footer_offset: u64,
223 pub encrypted_stats_offset: u64,
227 pub encrypted_stats_len: u64,
228}
229
230impl RunHeader {
231 pub fn is_encrypted(&self) -> bool {
232 self.flags & RUN_FLAG_ENCRYPTED != 0
233 }
234 pub fn is_clean(&self) -> bool {
235 self.flags & RUN_FLAG_CLEAN != 0
236 }
237 pub fn is_uniform_epoch(&self) -> bool {
238 self.flags & RUN_FLAG_UNIFORM_EPOCH != 0
239 }
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct ColumnPageHeader {
244 pub column_id: u16,
245 pub type_id_tag: u16,
246 pub encoding: u8,
247 pub flags: u8,
248 pub page_count: u32,
249 pub page_region_offset: u64,
250 pub page_region_len: u64,
251 pub page_stats: Vec<PageStat>,
252}
253
254impl ColumnPageHeader {
255 const PAGE_ENCRYPTED: u8 = 1 << 0;
256}
257
258const RUN_MAC_LEN: usize = 32;
261
262fn compute_run_mac(
266 enc: Option<&RunEncryption>,
267 header_bytes: &[u8],
268 dir_bytes: &[u8],
269) -> Option<[u8; RUN_MAC_LEN]> {
270 {
271 if let Some(e) = enc {
272 if let Some(mac_key) = &e.mac_key {
273 return Some(crate::encryption::run_metadata_mac(
274 mac_key,
275 header_bytes,
276 dir_bytes,
277 &e.descriptor_bytes,
278 ));
279 }
280 }
281 }
282 None
283}
284
285pub struct ColumnPayload {
287 pub column_id: u16,
288 pub type_id_tag: u16,
289 pub encoding: Encoding,
290 pub pages: Vec<Vec<u8>>,
291 pub page_stats: Vec<PageStat>,
295}
296
297pub struct RunSpec<'a> {
299 pub run_id: u128,
300 pub schema_id: u64,
301 pub epoch_created: u64,
302 pub level: u8,
303 pub flags: u8,
304 pub sort_key_column_id: u16,
305 pub row_count: u64,
306 pub min_row_id: u64,
307 pub max_row_id: u64,
308 pub columns: &'a [ColumnPayload],
309}
310
311pub fn write_run(path: impl AsRef<Path>, spec: &RunSpec) -> Result<RunHeader> {
313 write_run_with(path, spec, None, &[], None)
314}
315
316pub fn write_run_with(
326 path: impl AsRef<Path>,
327 spec: &RunSpec,
328 kek: Option<&Kek>,
329 indexable_columns: &[(u16, u8)],
330 index_trailer: Option<&[u8]>,
331) -> Result<RunHeader> {
332 let enc: Option<RunEncryption> = match kek {
336 Some(k) => Some(setup_run_encryption(k, indexable_columns)?),
337 None => None,
338 };
339 match write_run_mmap(path.as_ref(), spec, enc.as_ref(), index_trailer) {
340 Ok(h) => Ok(h),
341 Err(e) if is_mmap_unavailable(&e) => write_run_vec(path, spec, enc, index_trailer),
342 Err(e) => Err(e),
343 }
344}
345
346fn write_run_with_file(
347 mut file: File,
348 spec: &RunSpec,
349 kek: Option<&Kek>,
350 indexable_columns: &[(u16, u8)],
351 index_trailer: Option<&[u8]>,
352) -> Result<RunHeader> {
353 let enc = match kek {
354 Some(key) => Some(setup_run_encryption(key, indexable_columns)?),
355 None => None,
356 };
357 match write_run_mmap_file(&file, spec, enc.as_ref(), index_trailer) {
358 Ok(header) => Ok(header),
359 Err(error) if is_mmap_unavailable(&error) => {
360 file.set_len(0)?;
361 file.seek(SeekFrom::Start(0))?;
362 let (bytes, header) = encode_run_vec(spec, enc, index_trailer)?;
363 file.write_all(&bytes)?;
364 file.sync_all()?;
365 Ok(header)
366 }
367 Err(error) => Err(error),
368 }
369}
370
371fn is_mmap_unavailable(e: &MongrelError) -> bool {
375 matches!(e, MongrelError::InvalidArgument(m) if m.starts_with("__mmap_unavailable__:"))
376}
377
378fn write_run_mmap(
399 path: &Path,
400 spec: &RunSpec,
401 enc: Option<&RunEncryption>,
402 index_trailer: Option<&[u8]>,
403) -> Result<RunHeader> {
404 let file = OpenOptions::new().create_new(true).write(true).open(path)?;
405 let result = write_run_mmap_file(&file, spec, enc, index_trailer);
406 if result.as_ref().is_err_and(is_mmap_unavailable) {
407 drop(file);
408 let _ = std::fs::remove_file(path);
409 }
410 result
411}
412
413fn write_run_mmap_file(
414 file: &File,
415 spec: &RunSpec,
416 enc: Option<&RunEncryption>,
417 index_trailer: Option<&[u8]>,
418) -> Result<RunHeader> {
419 let plan = plan_run(spec, enc, index_trailer)?;
420 file.set_len(plan.total as u64)?;
421 let mut mmap = match unsafe { memmap2::MmapMut::map_mut(file) } {
422 Ok(m) => m,
423 Err(e) => {
424 return Err(MongrelError::InvalidArgument(format!(
425 "__mmap_unavailable__: {e}"
426 )));
427 }
428 };
429 let header = place_run(spec, enc, index_trailer, &plan, &mut mmap[..])?;
430 mmap.flush()?;
431 file.sync_all()?;
432 Ok(header)
433}
434
435struct RunPlan {
441 jobs: Vec<(usize, usize, u64, usize)>, dir_bytes: Vec<u8>,
443 encrypted: bool,
444 column_dir_offset: u64,
445 index_trailer_offset: u64,
446 encryption_descriptor_offset: u64,
447 footer_offset: u64,
448 total: usize,
449 encrypted_stats_plain: Option<Vec<u8>>,
453 encrypted_stats_offset: u64,
454 encrypted_stats_len: u64,
455}
456
457fn plan_run(
461 spec: &RunSpec,
462 enc: Option<&RunEncryption>,
463 index_trailer: Option<&[u8]>,
464) -> Result<RunPlan> {
465 let encrypted = enc.is_some();
466 let columns = spec.columns;
467 let mut jobs: Vec<(usize, usize, u64, usize)> = Vec::new();
468 let mut dir: Vec<ColumnPageHeader> = Vec::with_capacity(columns.len());
469 let mut enc_stats: EncryptedColumnStats = Vec::new();
470 let mut cursor: u64 = RUN_HEADER_PAD as u64;
471 for (ci, col) in columns.iter().enumerate() {
472 let region_offset = cursor;
473 let mut region_len = 0u64;
474 let mut stats = Vec::with_capacity(col.pages.len());
475 let mut col_minmax: Vec<PageMinMax> = Vec::new();
476 for (ps, page) in col.pages.iter().enumerate() {
477 if encrypted && ps >= u16::MAX as usize {
482 return Err(MongrelError::Full(format!(
483 "column {:#x} exceeds 65534 pages; encrypted-run page-seq nonce space exhausted",
484 col.column_id
485 )));
486 }
487 let odl = on_disk_len(page.len(), encrypted);
488 jobs.push((ci, ps, cursor, odl));
489 let mut stat = col.page_stats.get(ps).cloned().unwrap_or(PageStat {
490 first_row_id: 0,
491 last_row_id: 0,
492 null_count: 0,
493 row_count: 0,
494 min: None,
495 max: None,
496 offset: 0,
497 compressed_len: 0,
498 uncompressed_len: 0,
499 });
500 stat.offset = cursor;
501 stat.compressed_len = odl as u32;
502 stat.uncompressed_len = page.len() as u32;
503 if encrypted {
510 col_minmax.push((stat.min.take(), stat.max.take()));
511 }
512 stats.push(stat);
513 cursor += odl as u64;
514 region_len += odl as u64;
515 }
516 if col_minmax
517 .iter()
518 .any(|(mn, mx)| mn.is_some() || mx.is_some())
519 {
520 enc_stats.push((col.column_id, col_minmax));
521 }
522 let page_flags = if encrypted {
523 ColumnPageHeader::PAGE_ENCRYPTED
524 } else {
525 0
526 };
527 dir.push(ColumnPageHeader {
528 column_id: col.column_id,
529 type_id_tag: col.type_id_tag,
530 encoding: col.encoding as u8,
531 flags: page_flags,
532 page_count: col.pages.len() as u32,
533 page_region_offset: region_offset,
534 page_region_len: region_len,
535 page_stats: stats,
536 });
537 }
538 let dir_bytes = bincode::serialize(&dir)?;
539 let column_dir_offset = cursor;
540 cursor += dir_bytes.len() as u64;
541 let index_trailer_offset = match index_trailer {
542 Some(t) => {
543 let off = cursor;
544 cursor += t.len() as u64;
545 off
546 }
547 None => 0,
548 };
549 let encryption_descriptor_offset = match enc {
550 Some(e) => {
551 let off = cursor;
552 cursor += 4 + e.descriptor_bytes.len() as u64;
553 off
554 }
555 None => 0,
556 };
557 let (encrypted_stats_plain, encrypted_stats_offset, encrypted_stats_len) =
558 if encrypted && !enc_stats.is_empty() {
559 let plain = bincode::serialize(&enc_stats)?;
560 let ct_len = on_disk_len(plain.len(), true) as u64;
561 let off = cursor;
562 cursor += ct_len;
563 (Some(plain), off, ct_len)
564 } else {
565 (None, 0, 0)
566 };
567 let footer_offset = cursor;
568 let total = footer_offset as usize + 8 + 8 + 32 + if encrypted { RUN_MAC_LEN } else { 0 };
571 Ok(RunPlan {
572 jobs,
573 dir_bytes,
574 encrypted,
575 column_dir_offset,
576 index_trailer_offset,
577 encryption_descriptor_offset,
578 footer_offset,
579 total,
580 encrypted_stats_plain,
581 encrypted_stats_offset,
582 encrypted_stats_len,
583 })
584}
585
586fn place_run(
593 spec: &RunSpec,
594 enc: Option<&RunEncryption>,
595 index_trailer: Option<&[u8]>,
596 plan: &RunPlan,
597 buf: &mut [u8],
598) -> Result<RunHeader> {
599 use rayon::prelude::*;
600 use std::borrow::Cow;
601 debug_assert_eq!(
602 buf.len(),
603 plan.total,
604 "place_run: buffer must be exactly plan.total bytes"
605 );
606
607 let cipher: Option<&dyn Cipher> = enc.map(|e| e.cipher.as_ref());
608 let nonce_prefix = enc.map(|e| e.nonce_prefix);
609 let columns = spec.columns;
610
611 struct SyncPtr(*mut u8);
616 unsafe impl Send for SyncPtr {}
617 unsafe impl Sync for SyncPtr {}
618 impl SyncPtr {
619 fn get(&self) -> *mut u8 {
620 self.0
621 }
622 }
623 let base = SyncPtr(buf.as_mut_ptr());
624 plan.jobs
625 .par_iter()
626 .map(
627 |&(ci, ps, offset, odl): &(usize, usize, u64, usize)| -> Result<()> {
628 let page = &columns[ci].pages[ps];
629 let dst =
630 unsafe { std::slice::from_raw_parts_mut(base.get().add(offset as usize), odl) };
631 let bytes: Cow<[u8]> = match cipher {
632 Some(c) => {
633 let nonce =
634 page_nonce(nonce_prefix.unwrap(), columns[ci].column_id, ps as u32);
635 let ct = c.encrypt_page(&nonce, page)?;
636 assert_eq!(
639 ct.len(), odl,
640 "ciphertext length {} != predicted {}; GCM tag size assumption is stale",
641 ct.len(), odl
642 );
643 Cow::Owned(ct)
644 }
645 None => {
646 debug_assert_eq!(page.len(), odl);
647 Cow::Borrowed(page.as_slice())
648 }
649 };
650 dst.copy_from_slice(&bytes);
651 Ok(())
652 },
653 )
654 .collect::<Result<()>>()?;
655
656 let content_hash = {
658 let mut h = Sha256::new();
659 h.update(&buf[RUN_HEADER_PAD..plan.column_dir_offset as usize]);
660 h.finalize()
661 };
662
663 let doff = plan.column_dir_offset as usize;
665 buf[doff..doff + plan.dir_bytes.len()].copy_from_slice(&plan.dir_bytes);
666 if let Some(t) = index_trailer {
667 let off = plan.index_trailer_offset as usize;
668 buf[off..off + t.len()].copy_from_slice(t);
669 }
670 if let Some(e) = enc {
671 let off = plan.encryption_descriptor_offset as usize;
672 buf[off..off + 4].copy_from_slice(&(e.descriptor_bytes.len() as u32).to_le_bytes());
673 buf[off + 4..off + 4 + e.descriptor_bytes.len()].copy_from_slice(&e.descriptor_bytes);
674 }
675 if let (Some(e), Some(plain)) = (enc, plan.encrypted_stats_plain.as_ref()) {
676 let nonce = page_nonce(e.nonce_prefix, ENC_STATS_NONCE_COLUMN, ENC_STATS_NONCE_SEQ);
677 let ct = e.cipher.encrypt_page(&nonce, plain)?;
678 debug_assert_eq!(ct.len() as u64, plan.encrypted_stats_len);
679 let off = plan.encrypted_stats_offset as usize;
680 buf[off..off + ct.len()].copy_from_slice(&ct);
681 }
682
683 let header_flags = if plan.encrypted {
685 spec.flags | RUN_FLAG_ENCRYPTED
686 } else {
687 spec.flags
688 };
689 let header = RunHeader {
690 magic: RUN_MAGIC,
691 format_version: RUN_FORMAT_VERSION,
692 header_layout_version: RUN_HEADER_VERSION,
693 run_id: spec.run_id,
694 content_hash: content_hash.into(),
695 schema_id: spec.schema_id,
696 epoch_created: spec.epoch_created,
697 level: spec.level,
698 flags: header_flags,
699 sort_key_column_id: spec.sort_key_column_id,
700 row_count: spec.row_count,
701 min_row_id: spec.min_row_id,
702 max_row_id: spec.max_row_id,
703 column_count: columns.len() as u64,
704 column_dir_offset: plan.column_dir_offset,
705 index_trailer_offset: plan.index_trailer_offset,
706 encryption_descriptor_offset: plan.encryption_descriptor_offset,
707 footer_offset: plan.footer_offset,
708 encrypted_stats_offset: plan.encrypted_stats_offset,
709 encrypted_stats_len: plan.encrypted_stats_len,
710 };
711 let header_bytes = bincode::serialize(&header)?;
712 if header_bytes.len() > RUN_HEADER_PAD {
713 return Err(MongrelError::InvalidArgument(format!(
714 "run header too large: {} > {RUN_HEADER_PAD}",
715 header_bytes.len()
716 )));
717 }
718 buf[..header_bytes.len()].copy_from_slice(&header_bytes);
719
720 let checksum = Sha256::digest(&buf[..plan.footer_offset as usize]);
721 let foot = plan.footer_offset as usize;
722 buf[foot..foot + 8].copy_from_slice(&RUN_MAGIC);
723 buf[foot + 8..foot + 16].copy_from_slice(&plan.footer_offset.to_le_bytes());
724 buf[foot + 16..foot + 48].copy_from_slice(&checksum);
725 if let Some(tag) = compute_run_mac(enc, &header_bytes, &plan.dir_bytes) {
727 buf[foot + 48..foot + 48 + RUN_MAC_LEN].copy_from_slice(&tag);
728 }
729 Ok(header)
730}
731
732fn write_run_vec(
735 path: impl AsRef<Path>,
736 spec: &RunSpec,
737 enc: Option<RunEncryption>,
738 index_trailer: Option<&[u8]>,
739) -> Result<RunHeader> {
740 let (buf, header) = encode_run_vec(spec, enc, index_trailer)?;
741 let mut file = OpenOptions::new().create_new(true).write(true).open(path)?;
742 file.write_all(&buf)?;
743 file.sync_all()?;
744 Ok(header)
745}
746
747fn encode_run_vec(
748 spec: &RunSpec,
749 enc: Option<RunEncryption>,
750 index_trailer: Option<&[u8]>,
751) -> Result<(Vec<u8>, RunHeader)> {
752 let mut buf: Vec<u8> = vec![0; RUN_HEADER_PAD]; let mut content_hasher = Sha256::new();
754 let mut dir: Vec<ColumnPageHeader> = Vec::with_capacity(spec.columns.len());
755 let mut enc_stats: EncryptedColumnStats = Vec::new();
756
757 for col in spec.columns {
758 let region_offset = buf.len() as u64;
759 let mut region_len = 0u64;
760 let mut stats = Vec::with_capacity(col.pages.len());
761 let mut col_minmax: Vec<PageMinMax> = Vec::new();
762 for (page_seq, page) in col.pages.iter().enumerate() {
763 if enc.is_some() && page_seq >= u16::MAX as usize {
764 return Err(MongrelError::Full(format!(
765 "column {:#x} exceeds 65534 pages; encrypted-run page-seq nonce space exhausted",
766 col.column_id
767 )));
768 }
769 let on_disk: Vec<u8> = match &enc {
770 Some(e) => e.cipher.encrypt_page(
771 &page_nonce(e.nonce_prefix, col.column_id, page_seq as u32),
772 page,
773 )?,
774 None => page.clone(),
775 };
776 let offset = buf.len() as u64;
777 buf.write_all(&on_disk)?;
778 content_hasher.update(&on_disk);
779 region_len += on_disk.len() as u64;
780 let mut stat = if let Some(s) = col.page_stats.get(page_seq) {
781 s.clone()
782 } else {
783 PageStat {
784 first_row_id: 0,
785 last_row_id: 0,
786 null_count: 0,
787 row_count: 0,
788 min: None,
789 max: None,
790 offset: 0,
791 compressed_len: 0,
792 uncompressed_len: 0,
793 }
794 };
795 stat.offset = offset;
796 stat.compressed_len = on_disk.len() as u32;
797 stat.uncompressed_len = page.len() as u32;
798 if enc.is_some() {
803 col_minmax.push((stat.min.take(), stat.max.take()));
804 }
805 stats.push(stat);
806 }
807 if col_minmax
808 .iter()
809 .any(|(mn, mx)| mn.is_some() || mx.is_some())
810 {
811 enc_stats.push((col.column_id, col_minmax));
812 }
813 let page_flags = if enc.is_some() {
814 ColumnPageHeader::PAGE_ENCRYPTED
815 } else {
816 0
817 };
818 dir.push(ColumnPageHeader {
819 column_id: col.column_id,
820 type_id_tag: col.type_id_tag,
821 encoding: col.encoding as u8,
822 flags: page_flags,
823 page_count: col.pages.len() as u32,
824 page_region_offset: region_offset,
825 page_region_len: region_len,
826 page_stats: stats,
827 });
828 }
829
830 let column_dir_offset = buf.len() as u64;
831 let dir_bytes = bincode::serialize(&dir)?;
832 buf.write_all(&dir_bytes)?;
833
834 let index_trailer_offset = match index_trailer {
835 Some(trailer) => {
836 let off = buf.len() as u64;
837 buf.write_all(trailer)?;
838 off
839 }
840 None => 0,
841 };
842 let encryption_descriptor_offset = match &enc {
843 Some(e) => {
844 let off = buf.len() as u64;
845 buf.write_all(&(e.descriptor_bytes.len() as u32).to_le_bytes())?;
846 buf.write_all(&e.descriptor_bytes)?;
847 off
848 }
849 None => 0,
850 };
851 let (encrypted_stats_offset, encrypted_stats_len) = match &enc {
852 Some(e) if !enc_stats.is_empty() => {
853 let plain = bincode::serialize(&enc_stats)?;
854 let nonce = page_nonce(e.nonce_prefix, ENC_STATS_NONCE_COLUMN, ENC_STATS_NONCE_SEQ);
855 let ct = e.cipher.encrypt_page(&nonce, &plain)?;
856 let off = buf.len() as u64;
857 let len = ct.len() as u64;
858 buf.write_all(&ct)?;
859 (off, len)
860 }
861 _ => (0, 0),
862 };
863 let footer_offset = buf.len() as u64;
864
865 let header_flags = if enc.is_some() {
866 spec.flags | RUN_FLAG_ENCRYPTED
867 } else {
868 spec.flags
869 };
870 let header = RunHeader {
871 magic: RUN_MAGIC,
872 format_version: RUN_FORMAT_VERSION,
873 header_layout_version: RUN_HEADER_VERSION,
874 run_id: spec.run_id,
875 content_hash: content_hasher.finalize().into(),
876 schema_id: spec.schema_id,
877 epoch_created: spec.epoch_created,
878 level: spec.level,
879 flags: header_flags,
880 sort_key_column_id: spec.sort_key_column_id,
881 row_count: spec.row_count,
882 min_row_id: spec.min_row_id,
883 max_row_id: spec.max_row_id,
884 column_count: spec.columns.len() as u64,
885 column_dir_offset,
886 index_trailer_offset,
887 encryption_descriptor_offset,
888 footer_offset,
889 encrypted_stats_offset,
890 encrypted_stats_len,
891 };
892 let header_bytes = bincode::serialize(&header)?;
893 if header_bytes.len() > RUN_HEADER_PAD {
894 return Err(MongrelError::InvalidArgument(format!(
895 "run header too large: {} > {RUN_HEADER_PAD}",
896 header_bytes.len()
897 )));
898 }
899 buf[..header_bytes.len()].copy_from_slice(&header_bytes);
900
901 let checksum = Sha256::digest(&buf[..footer_offset as usize]);
902 buf.write_all(&RUN_MAGIC)?;
903 buf.write_all(&footer_offset.to_le_bytes())?;
904 buf.write_all(&checksum)?;
905 if let Some(tag) = compute_run_mac(enc.as_ref(), &header_bytes, &dir_bytes) {
908 buf.write_all(&tag)?;
909 }
910
911 Ok((buf, header))
912}
913
914fn page_nonce(nonce_prefix: [u8; 12], column_id: u16, page_seq: u32) -> [u8; 12] {
915 let mut n = nonce_prefix;
916 n[8..10].copy_from_slice(&column_id.to_le_bytes());
917 n[10..12].copy_from_slice(&(page_seq as u16).to_le_bytes());
918 n
919}
920
921fn overlay_encrypted_stats(
928 file: &mut File,
929 header: &RunHeader,
930 cipher: &dyn Cipher,
931 nonce_prefix: [u8; 12],
932 dir: &mut [ColumnPageHeader],
933) -> Result<()> {
934 file.seek(SeekFrom::Start(header.encrypted_stats_offset))?;
935 const MAX_ENCRYPTED_STATS_BYTES: u64 = 64 * 1024 * 1024;
936 if header.encrypted_stats_len > MAX_ENCRYPTED_STATS_BYTES {
937 return Err(MongrelError::InvalidArgument(format!(
938 "encrypted run stats length {} exceeds {MAX_ENCRYPTED_STATS_BYTES}",
939 header.encrypted_stats_len
940 )));
941 }
942 let mut ct = vec![0u8; header.encrypted_stats_len as usize];
943 file.read_exact(&mut ct)?;
944 let nonce = page_nonce(nonce_prefix, ENC_STATS_NONCE_COLUMN, ENC_STATS_NONCE_SEQ);
945 let plain = cipher.decrypt_page(&nonce, &ct)?;
946 let stats: EncryptedColumnStats = bincode::deserialize(&plain)
947 .map_err(|e| MongrelError::Encryption(format!("bad encrypted page-stats envelope: {e}")))?;
948 for (cid, minmax) in stats {
949 let Some(col) = dir.iter_mut().find(|c| c.column_id == cid) else {
950 continue;
951 };
952 for (stat, (mn, mx)) in col.page_stats.iter_mut().zip(minmax) {
953 stat.min = mn;
954 stat.max = mx;
955 }
956 }
957 Ok(())
958}
959
960pub(crate) fn page_cache_key(
967 table_id: u64,
968 run_id: u128,
969 column_id: u16,
970 page_seq: usize,
971) -> [u8; 32] {
972 let mut h = Sha256::new();
973 h.update(table_id.to_be_bytes());
974 h.update(run_id.to_be_bytes());
975 h.update(column_id.to_be_bytes());
976 h.update((page_seq as u64).to_be_bytes());
977 let out = h.finalize();
978 let mut k = [0u8; 32];
979 k.copy_from_slice(&out);
980 k
981}
982
983fn decrypt_or_passthrough(
987 cipher: Option<&dyn Cipher>,
988 nonce_prefix: [u8; 12],
989 column_id: u16,
990 page_seq: usize,
991 encrypted: bool,
992 buf: &[u8],
993) -> Result<Vec<u8>> {
994 if encrypted {
995 match cipher {
996 Some(c) => {
997 Ok(c.decrypt_page(&page_nonce(nonce_prefix, column_id, page_seq as u32), buf)?)
998 }
999 None => Err(MongrelError::Decryption(
1000 "encrypted page but no cipher".into(),
1001 )),
1002 }
1003 } else {
1004 Ok(buf.to_vec())
1005 }
1006}
1007
1008fn read_header_fast_from_file(file: &mut File) -> Result<RunHeader> {
1018 file.seek(SeekFrom::Start(0))?;
1019 let mut header_buf = [0u8; RUN_HEADER_PAD];
1020 file.read_exact(&mut header_buf)?;
1021 let header: RunHeader = bincode::deserialize(&header_buf)
1022 .map_err(|e| MongrelError::InvalidArgument(format!("bad run header: {e}")))?;
1023 validate_run_header_bytes(&header, &header_buf)?;
1024 validate_run_layout(&header, file.metadata()?.len())?;
1025 file.seek(SeekFrom::Start(header.footer_offset))?;
1026 let mut footer_magic = [0u8; 8];
1027 file.read_exact(&mut footer_magic)?;
1028 if footer_magic != RUN_MAGIC {
1029 return Err(MongrelError::MagicMismatch {
1030 what: "sorted run footer",
1031 expected: RUN_MAGIC,
1032 got: footer_magic,
1033 });
1034 }
1035 Ok(header)
1036}
1037
1038pub fn read_header(path: impl AsRef<Path>) -> Result<RunHeader> {
1040 let mut file = crate::durable_file::open_regular_nofollow(path.as_ref())?;
1041 read_header_from_file(&mut file)
1042}
1043
1044fn validate_run_layout(header: &RunHeader, file_len: u64) -> Result<()> {
1045 let footer_len = 8u64
1046 + 8
1047 + 32
1048 + if header.is_encrypted() {
1049 RUN_MAC_LEN as u64
1050 } else {
1051 0
1052 };
1053 let expected_len = header
1054 .footer_offset
1055 .checked_add(footer_len)
1056 .ok_or_else(|| MongrelError::InvalidArgument("sorted run length overflow".into()))?;
1057 if header.footer_offset < RUN_HEADER_PAD as u64 || expected_len != file_len {
1058 return Err(MongrelError::InvalidArgument(format!(
1059 "invalid sorted run layout: footer={} file={file_len}",
1060 header.footer_offset
1061 )));
1062 }
1063 let in_body = |offset: u64| {
1064 offset == 0 || (offset >= RUN_HEADER_PAD as u64 && offset <= header.footer_offset)
1065 };
1066 if header.column_dir_offset < RUN_HEADER_PAD as u64
1067 || header.column_dir_offset > header.footer_offset
1068 || !in_body(header.index_trailer_offset)
1069 || !in_body(header.encryption_descriptor_offset)
1070 || !in_body(header.encrypted_stats_offset)
1071 || header
1072 .encrypted_stats_offset
1073 .checked_add(header.encrypted_stats_len)
1074 .is_none_or(|end| end > header.footer_offset)
1075 {
1076 return Err(MongrelError::InvalidArgument(
1077 "sorted run metadata offsets are outside the file".into(),
1078 ));
1079 }
1080 Ok(())
1081}
1082
1083fn validate_run_header_bytes(header: &RunHeader, bytes: &[u8; RUN_HEADER_PAD]) -> Result<()> {
1084 if header.magic != RUN_MAGIC {
1085 return Err(MongrelError::MagicMismatch {
1086 what: "sorted run",
1087 expected: RUN_MAGIC,
1088 got: header.magic,
1089 });
1090 }
1091 const KNOWN_FLAGS: u8 =
1092 RUN_FLAG_ENCRYPTED | RUN_FLAG_TOMBSTONE_ONLY | RUN_FLAG_CLEAN | RUN_FLAG_UNIFORM_EPOCH;
1093 if header.format_version != RUN_FORMAT_VERSION
1094 || header.header_layout_version != RUN_HEADER_VERSION
1095 || header.flags & !KNOWN_FLAGS != 0
1096 || (header.row_count == 0 && (header.min_row_id != 0 || header.max_row_id != 0))
1097 || (header.row_count != 0 && header.min_row_id > header.max_row_id)
1098 {
1099 return Err(MongrelError::InvalidArgument(
1100 "unsupported or invalid sorted run header".into(),
1101 ));
1102 }
1103 let canonical = bincode::serialize(header)?;
1104 if canonical.len() > RUN_HEADER_PAD
1105 || bytes[..canonical.len()] != canonical
1106 || bytes[canonical.len()..].iter().any(|byte| *byte != 0)
1107 {
1108 return Err(MongrelError::InvalidArgument(
1109 "sorted run header has noncanonical bytes or nonzero padding".into(),
1110 ));
1111 }
1112 Ok(())
1113}
1114
1115fn read_header_from_file(file: &mut File) -> Result<RunHeader> {
1116 file.seek(SeekFrom::Start(0))?;
1117 let mut header_buf = [0u8; RUN_HEADER_PAD];
1118 file.read_exact(&mut header_buf)?;
1119 let header: RunHeader = bincode::deserialize(&header_buf)
1120 .map_err(|e| MongrelError::InvalidArgument(format!("bad run header: {e}")))?;
1121 validate_run_header_bytes(&header, &header_buf)?;
1122
1123 validate_run_layout(&header, file.metadata()?.len())?;
1124 file.seek(SeekFrom::Start(header.footer_offset))?;
1125 let mut footer = [0u8; 8 + 8 + 32];
1126 file.read_exact(&mut footer)?;
1127 if footer[..8] != RUN_MAGIC {
1128 return Err(MongrelError::MagicMismatch {
1129 what: "sorted run footer",
1130 expected: RUN_MAGIC,
1131 got: footer[..8].try_into().unwrap(),
1132 });
1133 }
1134 let mut hasher = Sha256::new();
1135 file.seek(SeekFrom::Start(0))?;
1136 let mut remaining = header.footer_offset;
1137 let mut buffer = [0u8; 64 * 1024];
1138 while remaining != 0 {
1139 let length = usize::try_from(remaining.min(buffer.len() as u64)).unwrap();
1140 file.read_exact(&mut buffer[..length])?;
1141 hasher.update(&buffer[..length]);
1142 remaining -= length as u64;
1143 }
1144 let computed: [u8; 32] = hasher.finalize().into();
1145 let stored: [u8; 32] = footer[16..].try_into().unwrap();
1146 if computed != stored {
1147 return Err(MongrelError::ChecksumMismatch {
1148 expected: u64::from_be_bytes(stored[..8].try_into().unwrap()),
1149 actual: u64::from_be_bytes(computed[..8].try_into().unwrap()),
1150 context: "sorted run footer".into(),
1151 });
1152 }
1153 file.seek(SeekFrom::Start(RUN_HEADER_PAD as u64))?;
1154 let mut remaining = header.column_dir_offset - RUN_HEADER_PAD as u64;
1155 let mut content = Sha256::new();
1156 while remaining != 0 {
1157 let length = usize::try_from(remaining.min(buffer.len() as u64)).unwrap();
1158 file.read_exact(&mut buffer[..length])?;
1159 content.update(&buffer[..length]);
1160 remaining -= length as u64;
1161 }
1162 let content_hash: [u8; 32] = content.finalize().into();
1163 if content_hash != header.content_hash {
1164 return Err(MongrelError::ChecksumMismatch {
1165 expected: u64::from_be_bytes(header.content_hash[..8].try_into().unwrap()),
1166 actual: u64::from_be_bytes(content_hash[..8].try_into().unwrap()),
1167 context: "sorted run content hash".into(),
1168 });
1169 }
1170 Ok(header)
1171}
1172
1173pub fn read_column_dir(
1175 path: impl AsRef<Path>,
1176 header: &RunHeader,
1177) -> Result<Vec<ColumnPageHeader>> {
1178 let mut file = crate::durable_file::open_regular_nofollow(path.as_ref())?;
1179 read_column_dir_from_file(&mut file, header)
1180}
1181
1182fn read_column_dir_from_file(file: &mut File, header: &RunHeader) -> Result<Vec<ColumnPageHeader>> {
1183 file.seek(SeekFrom::Start(header.column_dir_offset))?;
1184 let end = [
1185 header.index_trailer_offset,
1186 header.encryption_descriptor_offset,
1187 header.encrypted_stats_offset,
1188 header.footer_offset,
1189 ]
1190 .into_iter()
1191 .filter(|offset| *offset > header.column_dir_offset)
1192 .min()
1193 .ok_or_else(|| {
1194 MongrelError::InvalidArgument("sorted run column directory has no end".into())
1195 })?;
1196 let len = end.checked_sub(header.column_dir_offset).ok_or_else(|| {
1197 MongrelError::InvalidArgument("sorted run column directory offsets are reversed".into())
1198 })?;
1199 const MAX_COLUMN_DIR_BYTES: u64 = 64 * 1024 * 1024;
1200 if len > MAX_COLUMN_DIR_BYTES {
1201 return Err(MongrelError::InvalidArgument(format!(
1202 "sorted run column directory length {len} exceeds {MAX_COLUMN_DIR_BYTES}"
1203 )));
1204 }
1205 let mut buf = vec![0u8; len as usize];
1206 file.read_exact(&mut buf)?;
1207 let dir: Vec<ColumnPageHeader> = bincode::deserialize(&buf)
1208 .map_err(|e| MongrelError::InvalidArgument(format!("bad column dir: {e}")))?;
1209 Ok(dir)
1210}
1211
1212fn validate_column_directory_layout(header: &RunHeader, dir: &[ColumnPageHeader]) -> Result<()> {
1213 if header.column_count != dir.len() as u64 || dir.len() > u16::MAX as usize {
1214 return Err(MongrelError::InvalidArgument(
1215 "sorted run column count is invalid".into(),
1216 ));
1217 }
1218 let mut regions = Vec::with_capacity(dir.len());
1219 let mut ids = std::collections::HashSet::new();
1220 for column in dir {
1221 if !ids.insert(column.column_id)
1222 || column.flags & !ColumnPageHeader::PAGE_ENCRYPTED != 0
1223 || column.page_count as usize != column.page_stats.len()
1224 {
1225 return Err(MongrelError::InvalidArgument(
1226 "sorted run column directory identity is invalid".into(),
1227 ));
1228 }
1229 let region_end = column
1230 .page_region_offset
1231 .checked_add(column.page_region_len)
1232 .ok_or_else(|| MongrelError::InvalidArgument("run page region overflows".into()))?;
1233 if column.page_region_offset < RUN_HEADER_PAD as u64
1234 || region_end > header.column_dir_offset
1235 {
1236 return Err(MongrelError::InvalidArgument(
1237 "sorted run page region is outside its body".into(),
1238 ));
1239 }
1240 let mut cursor = column.page_region_offset;
1241 let mut rows = 0_u64;
1242 for stat in &column.page_stats {
1243 let length = stat.compressed_len as u64;
1244 let end = stat
1245 .offset
1246 .checked_add(length)
1247 .ok_or_else(|| MongrelError::InvalidArgument("run page offset overflows".into()))?;
1248 if stat.offset != cursor
1249 || length == 0
1250 || length > MAX_RUN_PAGE_BYTES
1251 || stat.uncompressed_len as u64 > MAX_RUN_PAGE_BYTES
1252 || stat.row_count as usize > PAGE_ROWS
1253 || end > region_end
1254 {
1255 return Err(MongrelError::InvalidArgument(
1256 "sorted run page metadata is outside its region".into(),
1257 ));
1258 }
1259 rows = rows.checked_add(stat.row_count as u64).ok_or_else(|| {
1260 MongrelError::InvalidArgument("sorted run row count overflows".into())
1261 })?;
1262 cursor = end;
1263 }
1264 if cursor != region_end || rows != header.row_count {
1265 return Err(MongrelError::InvalidArgument(
1266 "sorted run page region length or row count is inconsistent".into(),
1267 ));
1268 }
1269 regions.push((column.page_region_offset, region_end));
1270 }
1271 regions.sort_unstable();
1272 if regions.windows(2).any(|pair| pair[0].1 > pair[1].0) {
1273 return Err(MongrelError::InvalidArgument(
1274 "sorted run page regions overlap".into(),
1275 ));
1276 }
1277 Ok(())
1278}
1279
1280fn read_encryption_descriptor_bytes_from_file(
1281 file: &mut File,
1282 header: &RunHeader,
1283) -> Result<Vec<u8>> {
1284 file.seek(SeekFrom::Start(header.encryption_descriptor_offset))?;
1285 let mut len_buf = [0u8; 4];
1286 file.read_exact(&mut len_buf)?;
1287 let len = u32::from_le_bytes(len_buf) as usize;
1288 const MAX_DESCRIPTOR_BYTES: usize = 65_536;
1293 if len > MAX_DESCRIPTOR_BYTES {
1294 return Err(MongrelError::InvalidArgument(format!(
1295 "encryption descriptor length {len} exceeds {MAX_DESCRIPTOR_BYTES}"
1296 )));
1297 }
1298 let mut buf = vec![0u8; len];
1299 file.read_exact(&mut buf)?;
1300 Ok(buf)
1301}
1302
1303fn verify_run_mac(
1310 file: &mut File,
1311 header: &RunHeader,
1312 dir: &[ColumnPageHeader],
1313 kek: &Kek,
1314 desc_bytes: &[u8],
1315) -> Result<()> {
1316 let header_bytes = bincode::serialize(header)?;
1317 let dir_bytes = bincode::serialize(dir)?;
1318 let mac_key = kek.derive_run_mac_key();
1319 let expected =
1320 crate::encryption::run_metadata_mac(&mac_key, &header_bytes, &dir_bytes, desc_bytes);
1321 file.seek(SeekFrom::Start(header.footer_offset + 8 + 8 + 32))?;
1322 let mut tag = [0u8; RUN_MAC_LEN];
1323 file.read_exact(&mut tag).map_err(|_| {
1324 MongrelError::Decryption(
1325 "encrypted run is missing or truncated its metadata MAC; cannot \
1326 authenticate metadata"
1327 .into(),
1328 )
1329 })?;
1330 let mut diff = 0u8;
1332 for (x, y) in tag.iter().zip(expected.iter()) {
1333 diff |= x ^ y;
1334 }
1335 if diff != 0 {
1336 return Err(MongrelError::Decryption(
1337 "run metadata authentication failed — tampered run or wrong key".into(),
1338 ));
1339 }
1340 Ok(())
1341}
1342
1343pub struct RunWriter<'a> {
1352 schema: &'a Schema,
1353 run_id: u128,
1354 epoch_created: Epoch,
1355 level: u8,
1356 kek: Option<&'a Kek>,
1357 indexable_columns: Vec<(u16, u8)>,
1360 compress: columnar::Compress,
1364 clean: bool,
1369 uniform_epoch: bool,
1374 le: bool,
1381}
1382
1383impl<'a> RunWriter<'a> {
1384 pub fn new(schema: &'a Schema, run_id: u128, epoch_created: Epoch, level: u8) -> Self {
1385 Self {
1386 schema,
1387 run_id,
1388 epoch_created,
1389 level,
1390 kek: None,
1391 indexable_columns: Vec::new(),
1392 compress: columnar::Compress::Zstd(3),
1393 clean: false,
1394 uniform_epoch: false,
1395 le: false,
1396 }
1397 }
1398
1399 pub fn uniform_epoch(mut self, uniform: bool) -> Self {
1405 self.uniform_epoch = uniform;
1406 self
1407 }
1408
1409 pub fn with_encryption(mut self, kek: &'a Kek, indexable_columns: Vec<(u16, u8)>) -> Self {
1413 self.kek = Some(kek);
1414 self.indexable_columns = indexable_columns;
1415 self
1416 }
1417
1418 pub fn with_zstd_level(mut self, level: i32) -> Self {
1421 self.compress = columnar::Compress::Zstd(level);
1422 self
1423 }
1424
1425 pub fn with_lz4(mut self) -> Self {
1429 self.compress = columnar::Compress::Lz4;
1430 self
1431 }
1432
1433 pub fn with_plain(mut self) -> Self {
1436 self.compress = columnar::Compress::Plain;
1437 self
1438 }
1439
1440 pub fn clean(mut self, clean: bool) -> Self {
1446 self.clean = clean;
1447 self
1448 }
1449
1450 pub fn with_native_endian(mut self) -> Self {
1455 if cfg!(target_endian = "little") {
1456 self.le = true;
1457 }
1458 self
1459 }
1460
1461 pub fn write_native(
1468 self,
1469 path: impl AsRef<Path>,
1470 user_columns: &[(u16, columnar::NativeColumn)],
1471 n: usize,
1472 first_row_id: u64,
1473 ) -> Result<RunHeader> {
1474 self.write_native_target(Some(path.as_ref()), None, user_columns, n, first_row_id)
1475 }
1476
1477 pub(crate) fn write_native_file(
1478 self,
1479 file: File,
1480 user_columns: &[(u16, columnar::NativeColumn)],
1481 n: usize,
1482 first_row_id: u64,
1483 ) -> Result<RunHeader> {
1484 self.write_native_target(None, Some(file), user_columns, n, first_row_id)
1485 }
1486
1487 fn write_native_target(
1488 self,
1489 path: Option<&Path>,
1490 file: Option<File>,
1491 user_columns: &[(u16, columnar::NativeColumn)],
1492 n: usize,
1493 first_row_id: u64,
1494 ) -> Result<RunHeader> {
1495 use columnar::NativeColumn;
1496 let row_id_col = NativeColumn::int64_sequence(first_row_id as i64, n);
1497 let epoch_col = NativeColumn::int64_constant(self.epoch_created.0 as i64, n);
1498 let deleted_col = NativeColumn::bool_constant(false, n);
1499
1500 let learned_trailer = build_learned_trailer_native(&row_id_col);
1501
1502 let row_ids = match &row_id_col {
1505 NativeColumn::Int64 { data, .. } => data.as_slice(),
1506 _ => &[],
1507 };
1508 let bounds = page_bounds(row_ids);
1509 let compress = self.compress;
1510 let le = self.le;
1511 let plain = matches!(compress, columnar::Compress::Plain);
1512 let row_id_enc = if plain {
1515 Encoding::Plain
1516 } else {
1517 Encoding::Delta
1518 };
1519 let sys_enc = if plain {
1520 Encoding::Plain
1521 } else {
1522 Encoding::Zstd
1523 };
1524
1525 let mut columns: Vec<ColumnPayload> = Vec::with_capacity(3 + self.schema.columns.len());
1526 let (pages, stats) = native_column_pages(
1527 TypeId::Int64,
1528 &row_id_col,
1529 row_id_enc,
1530 compress,
1531 le,
1532 &bounds,
1533 )?;
1534 columns.push(ColumnPayload {
1535 column_id: SYS_ROW_ID,
1536 type_id_tag: type_tag(&TypeId::Int64),
1537 encoding: row_id_enc,
1538 pages,
1539 page_stats: stats,
1540 });
1541 let (pages, stats) =
1542 native_column_pages(TypeId::Int64, &epoch_col, sys_enc, compress, le, &bounds)?;
1543 columns.push(ColumnPayload {
1544 column_id: SYS_EPOCH,
1545 type_id_tag: type_tag(&TypeId::Int64),
1546 encoding: sys_enc,
1547 pages,
1548 page_stats: stats,
1549 });
1550 let (pages, stats) =
1551 native_column_pages(TypeId::Bool, &deleted_col, sys_enc, compress, le, &bounds)?;
1552 columns.push(ColumnPayload {
1553 column_id: SYS_DELETED,
1554 type_id_tag: type_tag(&TypeId::Bool),
1555 encoding: sys_enc,
1556 pages,
1557 page_stats: stats,
1558 });
1559 use rayon::prelude::*;
1565 let user_cols: Vec<ColumnPayload> = self
1566 .schema
1567 .columns
1568 .par_iter()
1569 .map(|cdef| -> Result<ColumnPayload> {
1570 let col = user_columns
1571 .iter()
1572 .find(|(id, _)| *id == cdef.id)
1573 .map(|(_, c)| c)
1574 .ok_or_else(|| {
1575 MongrelError::ColumnNotFound(format!("user column {}", cdef.id))
1576 })?;
1577 let encoding = if plain {
1578 Encoding::Plain
1579 } else {
1580 choose_encoding_native(&cdef.ty, col)
1581 };
1582 let (pages, stats) =
1583 native_column_pages(cdef.ty.clone(), col, encoding, compress, le, &bounds)?;
1584 Ok(ColumnPayload {
1585 column_id: cdef.id,
1586 type_id_tag: type_tag(&cdef.ty),
1587 encoding,
1588 pages,
1589 page_stats: stats,
1590 })
1591 })
1592 .collect::<Result<Vec<_>>>()?;
1593 columns.extend(user_cols);
1594
1595 let flags = if self.uniform_epoch {
1601 RUN_FLAG_UNIFORM_EPOCH
1602 } else {
1603 RUN_FLAG_CLEAN
1604 };
1605 let spec = RunSpec {
1606 run_id: self.run_id,
1607 schema_id: self.schema.schema_id,
1608 epoch_created: self.epoch_created.0,
1609 level: self.level,
1610 flags,
1611 sort_key_column_id: SYS_ROW_ID,
1612 row_count: n as u64,
1613 min_row_id: first_row_id,
1614 max_row_id: first_row_id + n as u64 - 1,
1615 columns: &columns,
1616 };
1617 match file {
1618 Some(file) => write_run_with_file(
1619 file,
1620 &spec,
1621 self.kek,
1622 &self.indexable_columns,
1623 Some(&learned_trailer),
1624 ),
1625 None => write_run_with(
1626 path.ok_or_else(|| {
1627 MongrelError::InvalidArgument("sorted run output is missing".into())
1628 })?,
1629 &spec,
1630 self.kek,
1631 &self.indexable_columns,
1632 Some(&learned_trailer),
1633 ),
1634 }
1635 }
1636
1637 pub fn write(self, path: impl AsRef<Path>, rows: &[Row]) -> Result<RunHeader> {
1638 self.write_target(Some(path.as_ref()), None, rows)
1639 }
1640
1641 pub(crate) fn write_file(self, file: File, rows: &[Row]) -> Result<RunHeader> {
1642 self.write_target(None, Some(file), rows)
1643 }
1644
1645 fn write_target(
1646 self,
1647 path: Option<&Path>,
1648 file: Option<File>,
1649 rows: &[Row],
1650 ) -> Result<RunHeader> {
1651 let n = rows.len();
1652 let mut row_ids = Vec::with_capacity(n);
1654 let mut epochs = Vec::with_capacity(n);
1655 let mut deleted = Vec::with_capacity(n);
1656 let mut commit_ts_vals = Vec::with_capacity(n);
1657 let has_commit_ts = rows.iter().any(|r| r.commit_ts.is_some());
1659 for r in rows {
1660 row_ids.push(Value::Int64(r.row_id.0 as i64));
1661 epochs.push(Value::Int64(r.committed_epoch.0 as i64));
1662 deleted.push(Value::Bool(r.deleted));
1663 if has_commit_ts {
1664 commit_ts_vals.push(encode_commit_ts_value(r.commit_ts));
1665 }
1666 }
1667 let learned_trailer = build_learned_trailer(&row_ids);
1668 let (min_rid, max_rid) = row_id_bounds(rows);
1669 let row_id_i64: Vec<i64> = rows.iter().map(|r| r.row_id.0 as i64).collect();
1670 let bounds = page_bounds(&row_id_i64);
1671
1672 let mut columns: Vec<ColumnPayload> =
1673 Vec::with_capacity(3 + usize::from(has_commit_ts) + self.schema.columns.len());
1674 let (pages, stats, enc) = value_column_pages(TypeId::Int64, &row_ids, &bounds)?;
1675 columns.push(ColumnPayload {
1676 column_id: SYS_ROW_ID,
1677 type_id_tag: type_tag(&TypeId::Int64),
1678 encoding: enc,
1679 pages,
1680 page_stats: stats,
1681 });
1682 let (pages, stats, enc) = value_column_pages(TypeId::Int64, &epochs, &bounds)?;
1683 columns.push(ColumnPayload {
1684 column_id: SYS_EPOCH,
1685 type_id_tag: type_tag(&TypeId::Int64),
1686 encoding: enc,
1687 pages,
1688 page_stats: stats,
1689 });
1690 let (pages, stats, enc) = value_column_pages(TypeId::Bool, &deleted, &bounds)?;
1691 columns.push(ColumnPayload {
1692 column_id: SYS_DELETED,
1693 type_id_tag: type_tag(&TypeId::Bool),
1694 encoding: enc,
1695 pages,
1696 page_stats: stats,
1697 });
1698 if has_commit_ts {
1699 let (pages, stats, enc) = value_column_pages(TypeId::Bytes, &commit_ts_vals, &bounds)?;
1700 columns.push(ColumnPayload {
1701 column_id: SYS_COMMIT_TS,
1702 type_id_tag: type_tag(&TypeId::Bytes),
1703 encoding: enc,
1704 pages,
1705 page_stats: stats,
1706 });
1707 }
1708 for cdef in &self.schema.columns {
1710 let vals: Vec<Value> = rows
1711 .iter()
1712 .map(|r| r.columns.get(&cdef.id).cloned().unwrap_or(Value::Null))
1713 .collect();
1714 let (pages, stats, encoding) = value_column_pages(cdef.ty.clone(), &vals, &bounds)?;
1715 columns.push(ColumnPayload {
1716 column_id: cdef.id,
1717 type_id_tag: type_tag(&cdef.ty),
1718 encoding,
1719 pages,
1720 page_stats: stats,
1721 });
1722 }
1723
1724 let spec = RunSpec {
1725 run_id: self.run_id,
1726 schema_id: self.schema.schema_id,
1727 epoch_created: self.epoch_created.0,
1728 level: self.level,
1729 flags: {
1730 let mut f = if self.clean { RUN_FLAG_CLEAN } else { 0 };
1731 if self.uniform_epoch {
1732 f |= RUN_FLAG_UNIFORM_EPOCH;
1733 }
1734 f
1735 },
1736 sort_key_column_id: SYS_ROW_ID,
1737 row_count: n as u64,
1738 min_row_id: min_rid,
1739 max_row_id: max_rid,
1740 columns: &columns,
1741 };
1742 match file {
1743 Some(file) => write_run_with_file(
1744 file,
1745 &spec,
1746 self.kek,
1747 &self.indexable_columns,
1748 Some(&learned_trailer),
1749 ),
1750 None => write_run_with(
1751 path.ok_or_else(|| {
1752 MongrelError::InvalidArgument("sorted run output is missing".into())
1753 })?,
1754 &spec,
1755 self.kek,
1756 &self.indexable_columns,
1757 Some(&learned_trailer),
1758 ),
1759 }
1760 }
1761}
1762
1763fn type_tag(ty: &TypeId) -> u16 {
1764 match ty {
1766 TypeId::Bool => 1,
1767 TypeId::Int64 => 8,
1768 TypeId::Float64 => 9,
1769 TypeId::Bytes => 12,
1770 TypeId::Embedding { .. } => 13,
1771 _ => 0,
1772 }
1773}
1774
1775pub(crate) fn be_i64(b: Option<&[u8]>) -> Option<i64> {
1778 let b = b?;
1779 (b.len() == 8).then(|| i64::from_be_bytes(b.try_into().unwrap()))
1780}
1781
1782pub(crate) fn be_f64(b: Option<&[u8]>) -> Option<f64> {
1784 let b = b?;
1785 (b.len() == 8).then(|| f64::from_bits(u64::from_be_bytes(b.try_into().unwrap())))
1786}
1787
1788const PAGE_ROWS: usize = 65_536;
1791
1792fn page_bounds(row_ids: &[i64]) -> Vec<(usize, usize, u64, u64)> {
1795 let n = row_ids.len();
1796 if n == 0 {
1797 return vec![(0, 0, 0, 0)];
1798 }
1799 let mut out = Vec::new();
1800 let mut start = 0;
1801 while start < n {
1802 let end = (start + PAGE_ROWS).min(n);
1803 out.push((start, end, row_ids[start] as u64, row_ids[end - 1] as u64));
1804 start = end;
1805 }
1806 out
1807}
1808
1809fn native_column_pages(
1815 ty: TypeId,
1816 col: &columnar::NativeColumn,
1817 encoding: Encoding,
1818 compress: columnar::Compress,
1819 le: bool,
1820 bounds: &[(usize, usize, u64, u64)],
1821) -> Result<(Vec<Vec<u8>>, Vec<PageStat>)> {
1822 use rayon::prelude::*;
1823 let encode_one =
1824 |&(s, e, frid, lrid): &(usize, usize, u64, u64)| -> Result<(Vec<u8>, PageStat)> {
1825 let chunk = col.slice_range(s, e);
1826 let stat = columnar::page_stat_for(ty.clone(), &chunk, frid, lrid);
1827 let page = columnar::encode_page_native(ty.clone(), &chunk, encoding, compress, le)?;
1828 Ok((page, stat))
1829 };
1830 let pairs: Vec<(Vec<u8>, PageStat)> = if bounds.len() > 1 {
1832 bounds
1833 .par_iter()
1834 .map(encode_one)
1835 .collect::<Result<Vec<_>>>()?
1836 } else {
1837 bounds.iter().map(encode_one).collect::<Result<Vec<_>>>()?
1838 };
1839 let (pages, stats) = pairs.into_iter().unzip();
1840 Ok((pages, stats))
1841}
1842
1843fn value_column_pages(
1845 ty: TypeId,
1846 vals: &[Value],
1847 bounds: &[(usize, usize, u64, u64)],
1848) -> Result<(Vec<Vec<u8>>, Vec<PageStat>, Encoding)> {
1849 let encoding = choose_encoding(&ty, vals);
1850 let mut pages = Vec::with_capacity(bounds.len());
1851 let mut stats = Vec::with_capacity(bounds.len());
1852 for &(s, e, frid, lrid) in bounds {
1853 let chunk = &vals[s..e];
1854 pages.push(columnar::encode_page(ty.clone(), chunk, encoding)?);
1855 let native = columnar::values_to_native(ty.clone(), chunk);
1856 stats.push(columnar::page_stat_for(ty.clone(), &native, frid, lrid));
1857 }
1858 Ok((pages, stats, encoding))
1859}
1860
1861fn choose_encoding(ty: &TypeId, values: &[Value]) -> Encoding {
1864 use std::collections::HashSet;
1865 if matches!(ty, TypeId::Bytes) {
1866 let n = values.len();
1867 if n > 0 {
1868 let distinct = values
1869 .iter()
1870 .filter(|v| !matches!(v, Value::Null))
1871 .map(|v| v.encode_key())
1872 .collect::<HashSet<_>>()
1873 .len();
1874 if (distinct as f64 / n as f64) < 0.5 {
1875 return Encoding::Dictionary;
1876 }
1877 }
1878 }
1879 Encoding::Zstd
1880}
1881
1882fn choose_encoding_native(ty: &TypeId, col: &columnar::NativeColumn) -> Encoding {
1885 use std::collections::HashSet;
1886 match (ty, col) {
1887 (TypeId::Int64 | TypeId::TimestampNanos, columnar::NativeColumn::Int64 { data, .. }) => {
1888 if data.windows(2).all(|w| w[0] <= w[1]) {
1889 Encoding::Delta
1890 } else {
1891 Encoding::Zstd
1892 }
1893 }
1894 (
1895 TypeId::Bytes,
1896 columnar::NativeColumn::Bytes {
1897 offsets, values, ..
1898 },
1899 ) => {
1900 let n = offsets.len().saturating_sub(1);
1901 if n > 0 {
1902 let distinct: HashSet<&[u8]> = (0..n)
1903 .map(|i| &values[offsets[i] as usize..offsets[i + 1] as usize])
1904 .collect();
1905 if (distinct.len() as f64 / n as f64) < 0.5 {
1906 return Encoding::Dictionary;
1907 }
1908 }
1909 Encoding::Zstd
1910 }
1911 _ => Encoding::Zstd,
1912 }
1913}
1914
1915fn build_learned_trailer_native(col: &columnar::NativeColumn) -> Vec<u8> {
1917 let points: Vec<(u64, usize)> = match col {
1918 columnar::NativeColumn::Int64 { data, .. } => data
1919 .iter()
1920 .enumerate()
1921 .map(|(i, v)| (*v as u64, i))
1922 .collect(),
1923 _ => Vec::new(),
1924 };
1925 let pgm = PgmIndex::build(&points, LEARNED_EPSILON);
1926 bincode::serialize(&pgm).expect("pgm serialize")
1927}
1928
1929fn row_id_bounds(rows: &[Row]) -> (u64, u64) {
1930 match (rows.first(), rows.last()) {
1931 (Some(f), Some(l)) => (f.row_id.0, l.row_id.0),
1932 _ => (0, 0),
1933 }
1934}
1935
1936pub struct RunReader {
1942 file: File,
1943 mmap: Option<memmap2::Mmap>,
1944 header: RunHeader,
1945 dir: Vec<ColumnPageHeader>,
1946 schema: Schema,
1947 table_id: u64,
1949 cipher: Option<Box<dyn Cipher>>,
1951 nonce_prefix: [u8; 12],
1953 col_cache: HashMap<u16, Vec<Value>>,
1954 page_cache: Option<Arc<crate::cache::Sharded<crate::cache::PageCache>>>,
1958 decoded_cache: Option<Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>>,
1962 epoch_override: Option<Epoch>,
1966}
1967
1968#[derive(Debug, Clone, Copy)]
1969pub struct RunVisibleVersion {
1970 pub row_id: RowId,
1971 pub committed_epoch: Epoch,
1972 pub deleted: bool,
1973 pub commit_ts: Option<HlcTimestamp>,
1977 pub(crate) page_seq: usize,
1978 pub(crate) within_page: usize,
1979}
1980
1981pub struct RunVisibleVersionCursor {
1989 reader: RunReader,
1990 snapshot: Snapshot,
1991 page_row_counts: Vec<usize>,
1992 page_seq: usize,
1993 within_page: usize,
1994 row_ids: Vec<i64>,
1995 epochs: Vec<i64>,
1996 deleted: Vec<u8>,
1997 commit_ts: Vec<Option<HlcTimestamp>>,
2000 has_commit_ts_col: bool,
2001 lookahead: Option<RunVisibleVersion>,
2002 materialized_page: Option<usize>,
2003 materialized_columns: Vec<(u16, columnar::NativeColumn)>,
2004}
2005
2006impl RunVisibleVersionCursor {
2007 pub(crate) fn new(reader: RunReader, snapshot: Snapshot) -> Result<Self> {
2008 let page_row_counts = reader.page_row_counts(SYS_ROW_ID)?;
2009 let has_commit_ts_col = reader.has_column(SYS_COMMIT_TS);
2010 Ok(Self {
2011 reader,
2012 snapshot,
2013 page_row_counts,
2014 page_seq: 0,
2015 within_page: 0,
2016 row_ids: Vec::new(),
2017 epochs: Vec::new(),
2018 deleted: Vec::new(),
2019 commit_ts: Vec::new(),
2020 has_commit_ts_col,
2021 lookahead: None,
2022 materialized_page: None,
2023 materialized_columns: Vec::new(),
2024 })
2025 }
2026
2027 fn load_system_page(&mut self, control: &crate::ExecutionControl) -> Result<bool> {
2028 while self.page_seq < self.page_row_counts.len() {
2029 control.checkpoint()?;
2030 let rows = self.page_row_counts[self.page_seq];
2031 if rows == 0 {
2032 self.page_seq += 1;
2033 continue;
2034 }
2035 self.row_ids = match columnar::decode_page_native(
2036 TypeId::Int64,
2037 &self.reader.read_page(SYS_ROW_ID, self.page_seq)?,
2038 rows,
2039 )? {
2040 columnar::NativeColumn::Int64 { data, .. } => data,
2041 _ => return Err(MongrelError::InvalidArgument("sys row_id not int64".into())),
2042 };
2043 self.epochs = if let Some(epoch) = self.reader.epoch_override {
2044 vec![epoch.0 as i64; rows]
2045 } else {
2046 match columnar::decode_page_native(
2047 TypeId::Int64,
2048 &self.reader.read_page(SYS_EPOCH, self.page_seq)?,
2049 rows,
2050 )? {
2051 columnar::NativeColumn::Int64 { data, .. } => data,
2052 _ => return Err(MongrelError::InvalidArgument("sys epoch not int64".into())),
2053 }
2054 };
2055 self.deleted = match columnar::decode_page_native(
2056 TypeId::Bool,
2057 &self.reader.read_page(SYS_DELETED, self.page_seq)?,
2058 rows,
2059 )? {
2060 columnar::NativeColumn::Bool { data, .. } => data,
2061 _ => return Err(MongrelError::InvalidArgument("sys deleted not bool".into())),
2062 };
2063 self.commit_ts.clear();
2064 if self.has_commit_ts_col {
2065 let page = self.reader.read_page(SYS_COMMIT_TS, self.page_seq)?;
2066 let native = columnar::decode_page_native(TypeId::Bytes, &page, rows)?;
2067 self.commit_ts.reserve(rows);
2068 for i in 0..rows {
2069 self.commit_ts
2070 .push(decode_commit_ts_value(native.value_at(i).as_ref()));
2071 }
2072 }
2073 self.within_page = 0;
2074 return Ok(true);
2075 }
2076 Ok(false)
2077 }
2078
2079 fn next_raw(&mut self, control: &crate::ExecutionControl) -> Result<Option<RunVisibleVersion>> {
2080 if self.within_page >= self.row_ids.len() {
2081 if !self.row_ids.is_empty() {
2082 self.page_seq += 1;
2083 self.row_ids.clear();
2084 self.epochs.clear();
2085 self.deleted.clear();
2086 self.commit_ts.clear();
2087 }
2088 if !self.load_system_page(control)? {
2089 return Ok(None);
2090 }
2091 }
2092 if self.within_page.is_multiple_of(256) {
2093 control.checkpoint()?;
2094 }
2095 let position = self.within_page;
2096 self.within_page += 1;
2097 let commit_ts = self.commit_ts.get(position).copied().flatten();
2098 Ok(Some(RunVisibleVersion {
2099 row_id: RowId(self.row_ids[position] as u64),
2100 committed_epoch: Epoch(self.epochs[position] as u64),
2101 deleted: self.deleted[position] != 0,
2102 commit_ts,
2103 page_seq: self.page_seq,
2104 within_page: position,
2105 }))
2106 }
2107
2108 pub fn next_visible_version(
2109 &mut self,
2110 control: &crate::ExecutionControl,
2111 ) -> Result<Option<RunVisibleVersion>> {
2112 loop {
2113 let first = match self.lookahead.take() {
2114 Some(version) => version,
2115 None => match self.next_raw(control)? {
2116 Some(version) => version,
2117 None => return Ok(None),
2118 },
2119 };
2120 let row_id = first.row_id;
2121 let mut best = self
2126 .snapshot
2127 .observes_row(first.committed_epoch, first.commit_ts)
2128 .then_some(first);
2129 while let Some(candidate) = self.next_raw(control)? {
2130 if candidate.row_id != row_id {
2131 self.lookahead = Some(candidate);
2132 break;
2133 }
2134 if self
2135 .snapshot
2136 .observes_row(candidate.committed_epoch, candidate.commit_ts)
2137 && best.is_none_or(|current| {
2138 crate::epoch::Snapshot::version_is_newer(
2139 candidate.committed_epoch,
2140 candidate.commit_ts,
2141 current.committed_epoch,
2142 current.commit_ts,
2143 )
2144 })
2145 {
2146 best = Some(candidate);
2147 }
2148 }
2149 if best.is_some() {
2150 return Ok(best);
2151 }
2152 }
2153 }
2154
2155 pub fn materialize(
2156 &mut self,
2157 version: RunVisibleVersion,
2158 control: &crate::ExecutionControl,
2159 ) -> Result<Row> {
2160 if self.materialized_page != Some(version.page_seq) {
2161 let rows = self.page_row_counts[version.page_seq];
2162 let columns = self.reader.schema.columns.clone();
2163 let mut materialized = Vec::with_capacity(columns.len());
2164 for (index, column) in columns.into_iter().enumerate() {
2165 if index % 16 == 0 {
2166 control.checkpoint()?;
2167 }
2168 let native = if self.reader.has_column(column.id) {
2169 columnar::decode_page_native(
2170 column.ty,
2171 &self.reader.read_page(column.id, version.page_seq)?,
2172 rows,
2173 )?
2174 } else {
2175 columnar::null_native(column.ty, rows)
2176 };
2177 materialized.push((column.id, native));
2178 }
2179 self.materialized_columns = materialized;
2180 self.materialized_page = Some(version.page_seq);
2181 }
2182 let columns = self
2183 .materialized_columns
2184 .iter()
2185 .map(|(column_id, column)| {
2186 (
2187 *column_id,
2188 column.value_at(version.within_page).unwrap_or(Value::Null),
2189 )
2190 })
2191 .collect();
2192 let commit_ts = version.commit_ts.or_else(|| {
2195 if self.reader.has_column(SYS_COMMIT_TS) {
2196 let page = self
2197 .reader
2198 .read_page(SYS_COMMIT_TS, version.page_seq)
2199 .ok()?;
2200 let native = columnar::decode_page_native(
2201 TypeId::Bytes,
2202 &page,
2203 self.page_row_counts[version.page_seq],
2204 )
2205 .ok()?;
2206 decode_commit_ts_value(native.value_at(version.within_page).as_ref())
2207 } else {
2208 None
2209 }
2210 });
2211 Ok(Row {
2212 row_id: version.row_id,
2213 committed_epoch: version.committed_epoch,
2214 columns,
2215 deleted: version.deleted,
2216 commit_ts,
2217 })
2218 }
2219}
2220
2221impl RunReader {
2222 pub fn open(path: impl AsRef<Path>, schema: Schema, kek: Option<Arc<Kek>>) -> Result<Self> {
2223 Self::open_with_cache(path, schema, kek, None, None, 0, None)
2224 }
2225
2226 pub(crate) fn open_file(file: File, schema: Schema, kek: Option<Arc<Kek>>) -> Result<Self> {
2227 Self::open_file_with_cache(file, schema, kek, None, None, 0, None, None)
2228 }
2229
2230 #[allow(clippy::too_many_arguments)]
2231 pub(crate) fn open_with_cache(
2232 path: impl AsRef<Path>,
2233 schema: Schema,
2234 kek: Option<Arc<Kek>>,
2235 page_cache: Option<Arc<crate::cache::Sharded<crate::cache::PageCache>>>,
2236 decoded_cache: Option<Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>>,
2237 table_id: u64,
2238 verified_runs: Option<&parking_lot::Mutex<std::collections::HashSet<u128>>>,
2239 ) -> Result<Self> {
2240 let path = path.as_ref().to_path_buf();
2241 let file = crate::durable_file::open_regular_nofollow(&path)?;
2242 Self::open_file_with_cache(
2243 file,
2244 schema,
2245 kek,
2246 page_cache,
2247 decoded_cache,
2248 table_id,
2249 verified_runs,
2250 Some(path),
2251 )
2252 }
2253
2254 #[allow(clippy::too_many_arguments)]
2255 pub(crate) fn open_file_with_cache(
2256 mut file: File,
2257 schema: Schema,
2258 kek: Option<Arc<Kek>>,
2259 page_cache: Option<Arc<crate::cache::Sharded<crate::cache::PageCache>>>,
2260 decoded_cache: Option<Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>>,
2261 table_id: u64,
2262 verified_runs: Option<&parking_lot::Mutex<std::collections::HashSet<u128>>>,
2263 path: Option<PathBuf>,
2264 ) -> Result<Self> {
2265 let header = match verified_runs {
2266 Some(cache) => {
2267 let header = read_header_fast_from_file(&mut file)?;
2268 if cache.lock().contains(&header.run_id) {
2269 header
2270 } else {
2271 let verified = read_header_from_file(&mut file)?;
2272 cache.lock().insert(verified.run_id);
2273 verified
2274 }
2275 }
2276 None => read_header_from_file(&mut file)?,
2277 };
2278 let mut dir = read_column_dir_from_file(&mut file, &header)?;
2279 validate_column_directory_layout(&header, &dir)?;
2280 if header.is_encrypted() != kek.is_some() {
2281 return Err(MongrelError::Encryption(
2282 "sorted-run encryption mode differs from the database".into(),
2283 ));
2284 }
2285 let (cipher, nonce_prefix): (Option<Box<dyn Cipher>>, [u8; 12]) = if header.is_encrypted() {
2288 let kek = kek.as_ref().ok_or_else(|| {
2289 MongrelError::Encryption(
2290 "run is encrypted but no key-encryption key was provided".into(),
2291 )
2292 })?;
2293 if header.encryption_descriptor_offset == 0 {
2294 return Err(MongrelError::Encryption(
2295 "encrypted run has no encryption descriptor".into(),
2296 ));
2297 }
2298 let desc_bytes = read_encryption_descriptor_bytes_from_file(&mut file, &header)?;
2299 verify_run_mac(&mut file, &header, &dir, kek, &desc_bytes)?;
2305 let enc = crate::encryption::build_run_cipher(kek, &desc_bytes)?;
2306 if header.encrypted_stats_offset != 0 {
2310 overlay_encrypted_stats(
2311 &mut file,
2312 &header,
2313 enc.cipher.as_ref(),
2314 enc.nonce_prefix,
2315 &mut dir,
2316 )?;
2317 }
2318 (Some(enc.cipher), enc.nonce_prefix)
2319 } else {
2320 (None, [0u8; 12])
2321 };
2322 let mmap = unsafe { memmap2::MmapOptions::new().map(&file) }.ok();
2329 let _ = path;
2330 Ok(Self {
2331 file,
2332 mmap,
2333 header,
2334 dir,
2335 schema,
2336 table_id,
2337 cipher,
2338 nonce_prefix,
2339 col_cache: HashMap::new(),
2340 epoch_override: None,
2341 page_cache,
2342 decoded_cache,
2343 })
2344 }
2345
2346 pub fn header(&self) -> &RunHeader {
2347 &self.header
2348 }
2349
2350 pub(crate) fn validate_all_pages(&mut self) -> Result<()> {
2351 let mut columns = std::collections::HashSet::new();
2352 let row_header = self.find_header(SYS_ROW_ID)?.clone();
2353 let expected_rows = row_header
2354 .page_stats
2355 .iter()
2356 .map(|stat| stat.row_count)
2357 .collect::<Vec<_>>();
2358 let mut row_bounds = Vec::with_capacity(row_header.page_stats.len());
2359 let mut previous = None::<u64>;
2360 let mut first = None::<u64>;
2361 let mut last = None::<u64>;
2362 let mut counted = 0_u64;
2363 for (page, stat) in row_header.page_stats.iter().enumerate() {
2364 let bytes = self.read_page(SYS_ROW_ID, page)?;
2365 let native =
2366 columnar::decode_page_native(TypeId::Int64, &bytes, stat.row_count as usize)?;
2367 if columnar::page_stat_for(TypeId::Int64, &native, 0, 0).null_count != 0 {
2368 return Err(MongrelError::InvalidArgument(
2369 "sorted run row-id page contains nulls".into(),
2370 ));
2371 }
2372 let columnar::NativeColumn::Int64 { data, validity } = native else {
2373 return Err(MongrelError::InvalidArgument(
2374 "sorted run row-id page has the wrong type".into(),
2375 ));
2376 };
2377 let _ = validity;
2378 if data.is_empty() {
2379 if self.header.row_count != 0 || stat.row_count != 0 {
2380 return Err(MongrelError::InvalidArgument(
2381 "sorted run row-id page is unexpectedly empty".into(),
2382 ));
2383 }
2384 row_bounds.push((0, 0));
2385 continue;
2386 }
2387 if data.iter().any(|value| *value < 0) {
2388 return Err(MongrelError::InvalidArgument(
2389 "sorted run contains a negative row id".into(),
2390 ));
2391 }
2392 let page_first = data[0] as u64;
2393 let page_last = data[data.len() - 1] as u64;
2394 for value in data {
2395 let value = value as u64;
2396 if previous.is_some_and(|prior| {
2397 value < prior || (self.header.is_clean() && value == prior)
2398 }) {
2399 return Err(MongrelError::InvalidArgument(
2400 "sorted run row ids are not ordered".into(),
2401 ));
2402 }
2403 first.get_or_insert(value);
2404 previous = Some(value);
2405 last = Some(value);
2406 counted += 1;
2407 }
2408 if stat.first_row_id != page_first || stat.last_row_id != page_last {
2409 return Err(MongrelError::InvalidArgument(
2410 "sorted run row-id page bounds are stale".into(),
2411 ));
2412 }
2413 row_bounds.push((page_first, page_last));
2414 }
2415 if counted != self.header.row_count
2416 || first.unwrap_or(0) != self.header.min_row_id
2417 || last.unwrap_or(0) != self.header.max_row_id
2418 {
2419 return Err(MongrelError::InvalidArgument(
2420 "sorted run header row bounds differ from its pages".into(),
2421 ));
2422 }
2423 for column in self.dir.clone() {
2424 if !columns.insert(column.column_id) {
2425 return Err(MongrelError::InvalidArgument(format!(
2426 "sorted run contains duplicate column {}",
2427 column.column_id
2428 )));
2429 }
2430 let rows = column
2431 .page_stats
2432 .iter()
2433 .map(|stat| stat.row_count)
2434 .collect::<Vec<_>>();
2435 if rows != expected_rows {
2436 return Err(MongrelError::InvalidArgument(
2437 "sorted run columns have inconsistent page row counts".into(),
2438 ));
2439 }
2440 let ty = self.resolve_type(column.column_id);
2441 if column.type_id_tag != type_tag(&ty)
2442 || column.encoding > Encoding::Zstd as u8
2443 || (!is_system_column_id(column.column_id)
2444 && self
2445 .schema
2446 .columns
2447 .iter()
2448 .all(|item| item.id != column.column_id))
2449 {
2450 return Err(MongrelError::InvalidArgument(
2451 "sorted run column type or encoding is invalid".into(),
2452 ));
2453 }
2454 for (page, stat) in column.page_stats.iter().enumerate() {
2455 let bytes = self.read_page(column.column_id, page)?;
2456 if bytes.len() != stat.uncompressed_len as usize {
2457 return Err(MongrelError::InvalidArgument(
2458 "sorted run page length differs from its metadata".into(),
2459 ));
2460 }
2461 let native =
2462 match columnar::decode_page_native(ty.clone(), &bytes, stat.row_count as usize)
2463 {
2464 Ok(native) => native,
2465 Err(MongrelError::InvalidArgument(message))
2466 if message.starts_with("decode_page_native: unsupported ty") =>
2467 {
2468 let values =
2469 columnar::decode_page(ty.clone(), &bytes, stat.row_count as usize)?;
2470 columnar::values_to_native(ty.clone(), &values)
2471 }
2472 Err(error) => return Err(error),
2473 };
2474 let (first_row_id, last_row_id) =
2475 row_bounds.get(page).copied().ok_or_else(|| {
2476 MongrelError::InvalidArgument(
2477 "sorted run column has more pages than its row-id column".into(),
2478 )
2479 })?;
2480 let expected =
2481 columnar::page_stat_for(ty.clone(), &native, first_row_id, last_row_id);
2482 if stat.first_row_id != expected.first_row_id
2483 || stat.last_row_id != expected.last_row_id
2484 || stat.null_count != expected.null_count
2485 || stat.min != expected.min
2486 || stat.max != expected.max
2487 {
2488 return Err(MongrelError::InvalidArgument(
2489 "sorted run page statistics differ from decoded values".into(),
2490 ));
2491 }
2492 }
2493 }
2494 if !columns.contains(&SYS_ROW_ID)
2495 || !columns.contains(&SYS_EPOCH)
2496 || !columns.contains(&SYS_DELETED)
2497 || expected_rows.into_iter().map(u64::from).sum::<u64>() != self.header.row_count
2498 {
2499 return Err(MongrelError::InvalidArgument(
2500 "sorted run system columns or row count are invalid".into(),
2501 ));
2502 }
2503 if self.header.schema_id == self.schema.schema_id
2504 && self
2505 .schema
2506 .columns
2507 .iter()
2508 .any(|column| !columns.contains(&column.id))
2509 {
2510 return Err(MongrelError::InvalidArgument(
2511 "sorted run is missing a column from its declared schema".into(),
2512 ));
2513 }
2514
2515 for row_index in 0..usize::try_from(self.header.row_count).map_err(|_| {
2519 MongrelError::InvalidArgument("sorted run row count exceeds this platform".into())
2520 })? {
2521 let epoch = match self.column(SYS_EPOCH)?.get(row_index) {
2522 Some(Value::Int64(value)) if *value >= 0 => *value as u64,
2523 _ => {
2524 return Err(MongrelError::InvalidArgument(
2525 "sorted run contains an invalid commit epoch".into(),
2526 ))
2527 }
2528 };
2529 if epoch > self.header.epoch_created || (self.header.is_uniform_epoch() && epoch != 0) {
2530 return Err(MongrelError::InvalidArgument(
2531 "sorted run commit epoch exceeds its creation epoch".into(),
2532 ));
2533 }
2534 let deleted = match self.column(SYS_DELETED)?.get(row_index) {
2535 Some(Value::Bool(value)) => *value,
2536 _ => {
2537 return Err(MongrelError::InvalidArgument(
2538 "sorted run contains an invalid tombstone marker".into(),
2539 ))
2540 }
2541 };
2542 let mut values = Vec::with_capacity(self.schema.columns.len());
2543 for column in self.schema.columns.clone() {
2544 let value = if columns.contains(&column.id) {
2545 self.column(column.id)?
2546 .get(row_index)
2547 .cloned()
2548 .unwrap_or(Value::Null)
2549 } else {
2550 Value::Null
2551 };
2552 values.push((column.id, value));
2553 }
2554 if !deleted {
2555 self.schema
2556 .validate_persisted_values(&values)
2557 .map_err(|error| {
2558 MongrelError::InvalidArgument(format!(
2559 "sorted run row violates its schema: {error}"
2560 ))
2561 })?;
2562 }
2563 }
2564 Ok(())
2565 }
2566
2567 pub(crate) fn set_uniform_epoch(&mut self, epoch: Epoch) {
2571 if self.header.is_uniform_epoch() {
2572 self.epoch_override = Some(epoch);
2573 self.col_cache.remove(&SYS_EPOCH);
2575 }
2576 }
2577
2578 #[allow(dead_code)] pub(crate) fn into_visible_version_cursor(
2581 self,
2582 snapshot: Epoch,
2583 ) -> Result<RunVisibleVersionCursor> {
2584 RunVisibleVersionCursor::new(self, Snapshot::at(snapshot))
2587 }
2588
2589 pub fn into_visible_version_cursor_at(
2593 self,
2594 snapshot: Snapshot,
2595 ) -> Result<RunVisibleVersionCursor> {
2596 RunVisibleVersionCursor::new(self, snapshot)
2597 }
2598
2599 pub fn is_clean(&self) -> bool {
2602 self.header.is_clean()
2603 }
2604
2605 pub fn row_count(&self) -> usize {
2606 self.header.row_count as usize
2607 }
2608
2609 pub(crate) fn clean_contiguous_row_ids(&self) -> bool {
2610 let n = self.row_count();
2611 n > 0
2612 && self.is_clean()
2613 && self.epoch_override.is_none()
2614 && self.header.max_row_id >= self.header.min_row_id
2615 && self
2616 .header
2617 .max_row_id
2618 .checked_sub(self.header.min_row_id)
2619 .and_then(|span| span.checked_add(1))
2620 == Some(n as u64)
2621 }
2622
2623 pub(crate) fn position_for_row_id_fast(&self, row_id: u64) -> Option<usize> {
2624 if !self.clean_contiguous_row_ids()
2625 || row_id < self.header.min_row_id
2626 || row_id > self.header.max_row_id
2627 {
2628 return None;
2629 }
2630 Some((row_id - self.header.min_row_id) as usize)
2631 }
2632
2633 pub(crate) fn positions_for_row_ids_fast(&self, row_ids: &[u64]) -> Option<Vec<usize>> {
2634 if !self.clean_contiguous_row_ids() {
2635 return None;
2636 }
2637 let mut positions = Vec::with_capacity(row_ids.len());
2638 for &row_id in row_ids {
2639 if let Some(pos) = self.position_for_row_id_fast(row_id) {
2640 positions.push(pos);
2641 }
2642 }
2643 positions.sort_unstable();
2644 Some(positions)
2645 }
2646
2647 fn resolve_type(&self, column_id: u16) -> TypeId {
2648 match column_id {
2649 SYS_ROW_ID | SYS_EPOCH => TypeId::Int64,
2650 SYS_DELETED => TypeId::Bool,
2651 SYS_COMMIT_TS => TypeId::Bytes,
2652 _ => self
2653 .schema
2654 .columns
2655 .iter()
2656 .find(|c| c.id == column_id)
2657 .map(|c| c.ty.clone())
2658 .unwrap_or(TypeId::Bytes),
2659 }
2660 }
2661
2662 fn commit_ts_at(&mut self, index: usize) -> Result<Option<HlcTimestamp>> {
2664 if !self.has_column(SYS_COMMIT_TS) {
2665 return Ok(None);
2666 }
2667 Ok(decode_commit_ts_value(
2668 self.column(SYS_COMMIT_TS)?.get(index),
2669 ))
2670 }
2671
2672 fn find_header(&self, column_id: u16) -> Result<&ColumnPageHeader> {
2673 self.dir
2674 .iter()
2675 .find(|h| h.column_id == column_id)
2676 .ok_or_else(|| MongrelError::ColumnNotFound(format!("column {column_id}")))
2677 }
2678
2679 fn col_encrypted(&self, column_id: u16) -> bool {
2685 self.dir
2686 .iter()
2687 .find(|h| h.column_id == column_id)
2688 .map(|h| h.flags & ColumnPageHeader::PAGE_ENCRYPTED != 0)
2689 .unwrap_or(false)
2690 }
2691
2692 pub(crate) fn read_page(&mut self, column_id: u16, page_seq: usize) -> Result<Vec<u8>> {
2693 let (offset, compressed_len, encrypted) = {
2694 let ch = self.find_header(column_id)?;
2695 let stat = ch
2696 .page_stats
2697 .get(page_seq)
2698 .ok_or_else(|| MongrelError::InvalidArgument("page seq out of range".into()))?;
2699 (
2700 stat.offset,
2701 stat.compressed_len,
2702 ch.flags & ColumnPageHeader::PAGE_ENCRYPTED != 0,
2703 )
2704 };
2705 let end = offset
2706 .checked_add(compressed_len as u64)
2707 .ok_or_else(|| MongrelError::InvalidArgument("run page offset overflows".into()))?;
2708 let start = usize::try_from(offset)
2709 .map_err(|_| MongrelError::InvalidArgument("run page offset is too large".into()))?;
2710 let end = usize::try_from(end)
2711 .map_err(|_| MongrelError::InvalidArgument("run page end is too large".into()))?;
2712 let key = page_cache_key(self.table_id, self.header.run_id, column_id, page_seq);
2715 if let Some(cache) = &self.page_cache {
2716 if let Some(bytes) = cache.lock(&key).get(
2717 &key,
2718 crate::epoch::Snapshot::at(crate::epoch::Epoch(u64::MAX)),
2719 ) {
2720 return decrypt_or_passthrough(
2721 self.cipher.as_deref(),
2722 self.nonce_prefix,
2723 column_id,
2724 page_seq,
2725 encrypted,
2726 &bytes,
2727 );
2728 }
2729 }
2730 let buf = match &self.mmap {
2731 Some(m) => m
2734 .get(start..end)
2735 .ok_or_else(|| {
2736 MongrelError::InvalidArgument("run page is outside the mapped file".into())
2737 })?
2738 .to_vec(),
2739 None => {
2740 self.file.seek(SeekFrom::Start(offset))?;
2741 let mut buf = vec![0u8; compressed_len as usize];
2742 self.file.read_exact(&mut buf)?;
2743 buf
2744 }
2745 };
2746 if let Some(cache) = &self.page_cache {
2748 cache.lock(&key).insert(crate::page::CachedPage {
2749 committed_epoch: crate::epoch::Epoch(self.header.epoch_created),
2750 content_hash: key,
2751 bytes: bytes::Bytes::copy_from_slice(&buf),
2752 });
2753 }
2754 decrypt_or_passthrough(
2755 self.cipher.as_deref(),
2756 self.nonce_prefix,
2757 column_id,
2758 page_seq,
2759 encrypted,
2760 &buf,
2761 )
2762 }
2763
2764 fn read_page_shared(&self, column_id: u16, page_seq: usize) -> Result<Vec<u8>> {
2769 let ch = self.find_header(column_id)?;
2770 let stat = ch
2771 .page_stats
2772 .get(page_seq)
2773 .ok_or_else(|| MongrelError::InvalidArgument("page seq out of range".into()))?;
2774 let encrypted = ch.flags & ColumnPageHeader::PAGE_ENCRYPTED != 0;
2775 let key = page_cache_key(self.table_id, self.header.run_id, column_id, page_seq);
2778 if let Some(cache) = &self.page_cache {
2779 if let Some(guard) = cache.try_lock(&key) {
2780 if let Some(bytes) = guard.try_get(
2781 &key,
2782 crate::epoch::Snapshot::at(crate::epoch::Epoch(u64::MAX)),
2783 ) {
2784 return decrypt_or_passthrough(
2785 self.cipher.as_deref(),
2786 self.nonce_prefix,
2787 column_id,
2788 page_seq,
2789 encrypted,
2790 &bytes,
2791 );
2792 }
2793 }
2794 }
2795 let mmap = self.mmap.as_ref().ok_or_else(|| {
2796 MongrelError::InvalidArgument("parallel page decode requires an mmap-backed run".into())
2797 })?;
2798 let end = stat
2799 .offset
2800 .checked_add(stat.compressed_len as u64)
2801 .ok_or_else(|| MongrelError::InvalidArgument("run page offset overflows".into()))?;
2802 let start = usize::try_from(stat.offset)
2803 .map_err(|_| MongrelError::InvalidArgument("run page offset is too large".into()))?;
2804 let end = usize::try_from(end)
2805 .map_err(|_| MongrelError::InvalidArgument("run page end is too large".into()))?;
2806 let buf = mmap
2807 .get(start..end)
2808 .ok_or_else(|| {
2809 MongrelError::InvalidArgument("run page is outside the mapped file".into())
2810 })?
2811 .to_vec();
2812 if let Some(cache) = &self.page_cache {
2816 if let Some(mut guard) = cache.try_lock(&key) {
2817 guard.insert(crate::page::CachedPage {
2818 committed_epoch: crate::epoch::Epoch(self.header.epoch_created),
2819 content_hash: key,
2820 bytes: bytes::Bytes::copy_from_slice(&buf),
2821 });
2822 }
2823 }
2824 decrypt_or_passthrough(
2825 self.cipher.as_deref(),
2826 self.nonce_prefix,
2827 column_id,
2828 page_seq,
2829 encrypted,
2830 &buf,
2831 )
2832 }
2833
2834 fn column(&mut self, column_id: u16) -> Result<&[Value]> {
2836 if column_id == SYS_EPOCH {
2839 if let Some(ov) = self.epoch_override {
2840 if !self.col_cache.contains_key(&column_id) {
2841 let n = self.row_count();
2842 self.col_cache
2843 .insert(column_id, vec![Value::Int64(ov.0 as i64); n]);
2844 }
2845 return Ok(self.col_cache.get(&column_id).unwrap().as_slice());
2846 }
2847 }
2848 if !self.col_cache.contains_key(&column_id) {
2849 let ty = self.resolve_type(column_id);
2850 let page_rows: Vec<usize> = {
2851 let ch = self.find_header(column_id)?;
2852 ch.page_stats.iter().map(|s| s.row_count as usize).collect()
2853 };
2854 let mut decoded: Vec<Value> = Vec::with_capacity(self.row_count());
2855 for (seq, &pr) in page_rows.iter().enumerate() {
2856 let page = self.read_page(column_id, seq)?;
2857 decoded.extend(columnar::decode_page(ty.clone(), &page, pr)?);
2858 }
2859 self.col_cache.insert(column_id, decoded);
2860 }
2861 Ok(self.col_cache.get(&column_id).unwrap().as_slice())
2862 }
2863
2864 pub fn get_version(&mut self, row_id: RowId, snapshot: Epoch) -> Result<Option<(Epoch, Row)>> {
2878 self.get_version_at(row_id, Snapshot::at(snapshot))
2879 }
2880
2881 pub fn get_version_at(
2893 &mut self,
2894 row_id: RowId,
2895 snapshot: Snapshot,
2896 ) -> Result<Option<(Epoch, Row)>> {
2897 match self.find_version_page_at(row_id, snapshot)? {
2898 None => Ok(None),
2899 Some((epoch, _commit_ts, seq, local_index)) => Ok(Some((
2900 Epoch(epoch),
2901 self.materialize_in_page(seq, local_index)?,
2902 ))),
2903 }
2904 }
2905
2906 pub(crate) fn get_version_stamp_at(
2912 &mut self,
2913 row_id: RowId,
2914 snapshot: Snapshot,
2915 ) -> Result<Option<(VersionStamp, Row)>> {
2916 match self.find_version_page_at(row_id, snapshot)? {
2917 None => Ok(None),
2918 Some((epoch, commit_ts, seq, local_index)) => Ok(Some((
2919 VersionStamp {
2920 epoch: Epoch(epoch),
2921 commit_ts,
2922 },
2923 self.materialize_in_page(seq, local_index)?,
2924 ))),
2925 }
2926 }
2927
2928 #[allow(dead_code)] fn find_version_page(
2935 &mut self,
2936 row_id: RowId,
2937 snapshot: Epoch,
2938 ) -> Result<Option<FoundVersionPage>> {
2939 self.find_version_page_at(row_id, Snapshot::at(snapshot))
2940 }
2941
2942 fn find_version_page_at(
2953 &mut self,
2954 row_id: RowId,
2955 snapshot: Snapshot,
2956 ) -> Result<Option<FoundVersionPage>> {
2957 let n = self.row_count();
2958 if n == 0 {
2959 return Ok(None);
2960 }
2961 let target = row_id.0 as i64;
2962 let ch = self.find_header(SYS_ROW_ID)?;
2963 let mut page_start = 0u64;
2964 let candidate_pages: Vec<(usize, u64)> = ch .page_stats
2966 .iter()
2967 .enumerate()
2968 .filter_map(|(seq, s)| {
2969 let start = page_start;
2970 page_start += s.row_count as u64;
2971 (s.first_row_id <= row_id.0 && row_id.0 <= s.last_row_id).then_some((seq, start))
2972 })
2973 .collect();
2974 if candidate_pages.is_empty() {
2975 return Ok(None);
2976 }
2977 let ty = self.resolve_type(SYS_ROW_ID);
2978 let has_commit_ts_col = self.has_column(SYS_COMMIT_TS);
2979 let mut best: Option<(u64, Option<HlcTimestamp>, usize, usize)> = None; for (seq, _page_row_start) in candidate_pages {
2986 let page_rows = self.find_header(SYS_ROW_ID)?.page_stats[seq].row_count as usize;
2987 let row_ids =
2988 match self.decode_page_native_cached(ty.clone(), SYS_ROW_ID, seq, page_rows)? {
2989 columnar::NativeColumn::Int64 { data, .. } => data,
2990 _ => return Err(MongrelError::InvalidArgument("sys row_id not int64".into())),
2991 };
2992 let local = match row_ids.binary_search(&target) {
2993 Ok(i) => i,
2994 Err(_) => continue,
2995 };
2996 let epoch_ty = self.resolve_type(SYS_EPOCH);
2997 let epochs =
2998 match self.decode_page_native_cached(epoch_ty, SYS_EPOCH, seq, page_rows)? {
2999 columnar::NativeColumn::Int64 { data, .. } => data,
3000 _ => return Err(MongrelError::InvalidArgument("sys epoch not int64".into())),
3001 };
3002 let mut lo = local;
3005 while lo > 0 && row_ids[lo - 1] == target {
3006 lo -= 1;
3007 }
3008 let mut hi = local;
3009 while hi + 1 < row_ids.len() && row_ids[hi + 1] == target {
3010 hi += 1;
3011 }
3012 let commit_ts_native = if has_commit_ts_col {
3016 let page = self.read_page(SYS_COMMIT_TS, seq)?;
3017 Some(columnar::decode_page_native(
3018 TypeId::Bytes,
3019 &page,
3020 page_rows,
3021 )?)
3022 } else {
3023 None
3024 };
3025 for (i, &epoch_val) in epochs[lo..=hi].iter().enumerate() {
3026 let epoch = epoch_val as u64;
3027 let commit_ts = commit_ts_native
3028 .as_ref()
3029 .and_then(|col| decode_commit_ts_value(col.value_at(lo + i).as_ref()));
3030 if !snapshot.observes_row(Epoch(epoch), commit_ts) {
3034 continue;
3035 }
3036 let candidate = (epoch, commit_ts, seq, lo + i);
3040 let is_newer = best.as_ref().is_none_or(|cur| {
3041 crate::epoch::version_supersedes(
3042 Epoch(candidate.0),
3043 candidate.1,
3044 Epoch(cur.0),
3045 cur.1,
3046 )
3047 });
3048 if is_newer {
3049 best = Some(candidate);
3050 }
3051 }
3052 }
3053 Ok(best)
3054 }
3055
3056 pub fn get_version_column(
3064 &mut self,
3065 row_id: RowId,
3066 snapshot: Epoch,
3067 column_id: u16,
3068 ) -> Result<Option<(Epoch, bool, Option<Value>)>> {
3069 self.get_version_column_at(row_id, Snapshot::at(snapshot), column_id)
3070 }
3071
3072 pub fn get_version_column_at(
3074 &mut self,
3075 row_id: RowId,
3076 snapshot: Snapshot,
3077 column_id: u16,
3078 ) -> Result<Option<(Epoch, bool, Option<Value>)>> {
3079 let Some((epoch, _commit_ts, seq, local_index)) =
3080 self.find_version_page_at(row_id, snapshot)?
3081 else {
3082 return Ok(None);
3083 };
3084 let page_rows = self.find_header(SYS_ROW_ID)?.page_stats[seq].row_count as usize;
3085 let page_start: usize = self.find_header(SYS_ROW_ID)?.page_stats[..seq]
3086 .iter()
3087 .map(|s| s.row_count as usize)
3088 .sum();
3089 let global_index = page_start + local_index;
3090 let native_at = |slf: &mut Self, cid: u16| -> Result<Option<Value>> {
3091 if !slf.dir.iter().any(|h| h.column_id == cid) {
3092 return Ok(None);
3093 }
3094 let ty = slf.resolve_type(cid);
3095 if !matches!(
3096 ty,
3097 TypeId::Bool
3098 | TypeId::Int8
3099 | TypeId::Int16
3100 | TypeId::Int32
3101 | TypeId::Int64
3102 | TypeId::UInt8
3103 | TypeId::UInt16
3104 | TypeId::UInt32
3105 | TypeId::UInt64
3106 | TypeId::Float32
3107 | TypeId::Float64
3108 | TypeId::TimestampNanos
3109 | TypeId::Date32
3110 | TypeId::Bytes
3111 ) {
3112 return Ok(slf.column(cid)?.get(global_index).cloned());
3113 }
3114 Ok(slf
3115 .decode_page_native_cached(ty, cid, seq, page_rows)?
3116 .value_at(local_index))
3117 };
3118 let deleted = matches!(native_at(self, SYS_DELETED)?, Some(Value::Bool(true)));
3119 let value = native_at(self, column_id)?;
3120 Ok(Some((Epoch(epoch), deleted, value)))
3121 }
3122
3123 pub(crate) fn get_version_column_full_at(
3128 &mut self,
3129 row_id: RowId,
3130 snapshot: Snapshot,
3131 column_id: u16,
3132 ) -> Result<Option<VersionedColumnValue>> {
3133 let Some((epoch, commit_ts, seq, local_index)) =
3134 self.find_version_page_at(row_id, snapshot)?
3135 else {
3136 return Ok(None);
3137 };
3138 let page_rows = self.find_header(SYS_ROW_ID)?.page_stats[seq].row_count as usize;
3139 let page_start: usize = self.find_header(SYS_ROW_ID)?.page_stats[..seq]
3140 .iter()
3141 .map(|s| s.row_count as usize)
3142 .sum();
3143 let global_index = page_start + local_index;
3144 let native_at = |slf: &mut Self, cid: u16| -> Result<Option<Value>> {
3145 if !slf.dir.iter().any(|h| h.column_id == cid) {
3146 return Ok(None);
3147 }
3148 let ty = slf.resolve_type(cid);
3149 if !matches!(
3150 ty,
3151 TypeId::Bool
3152 | TypeId::Int8
3153 | TypeId::Int16
3154 | TypeId::Int32
3155 | TypeId::Int64
3156 | TypeId::UInt8
3157 | TypeId::UInt16
3158 | TypeId::UInt32
3159 | TypeId::UInt64
3160 | TypeId::Float32
3161 | TypeId::Float64
3162 | TypeId::TimestampNanos
3163 | TypeId::Date32
3164 | TypeId::Bytes
3165 ) {
3166 return Ok(slf.column(cid)?.get(global_index).cloned());
3167 }
3168 Ok(slf
3169 .decode_page_native_cached(ty, cid, seq, page_rows)?
3170 .value_at(local_index))
3171 };
3172 let deleted = matches!(native_at(self, SYS_DELETED)?, Some(Value::Bool(true)));
3173 let value = native_at(self, column_id)?;
3174 Ok(Some(VersionedColumnValue {
3175 stamp: VersionStamp {
3176 epoch: Epoch(epoch),
3177 commit_ts,
3178 },
3179 deleted,
3180 value,
3181 }))
3182 }
3183
3184 pub fn get_version_visibility(
3186 &mut self,
3187 row_id: RowId,
3188 snapshot: Epoch,
3189 ) -> Result<Option<(Epoch, bool)>> {
3190 self.get_version_visibility_at(row_id, Snapshot::at(snapshot))
3191 }
3192
3193 pub fn get_version_visibility_at(
3195 &mut self,
3196 row_id: RowId,
3197 snapshot: Snapshot,
3198 ) -> Result<Option<(Epoch, bool)>> {
3199 let Some((epoch, _commit_ts, seq, local_index)) =
3200 self.find_version_page_at(row_id, snapshot)?
3201 else {
3202 return Ok(None);
3203 };
3204 let page_rows = self.find_header(SYS_ROW_ID)?.page_stats[seq].row_count as usize;
3205 let deleted = match self.decode_page_native_cached(
3206 self.resolve_type(SYS_DELETED),
3207 SYS_DELETED,
3208 seq,
3209 page_rows,
3210 )? {
3211 columnar::NativeColumn::Bool { data, .. } => data[local_index] != 0,
3212 _ => return Err(MongrelError::InvalidArgument("sys deleted not bool".into())),
3213 };
3214 Ok(Some((Epoch(epoch), deleted)))
3215 }
3216
3217 pub(crate) fn get_version_visibility_full_at(
3221 &mut self,
3222 row_id: RowId,
3223 snapshot: Snapshot,
3224 ) -> Result<Option<VersionVisibility>> {
3225 let Some((epoch, commit_ts, seq, local_index)) =
3226 self.find_version_page_at(row_id, snapshot)?
3227 else {
3228 return Ok(None);
3229 };
3230 let page_rows = self.find_header(SYS_ROW_ID)?.page_stats[seq].row_count as usize;
3231 let deleted = match self.decode_page_native_cached(
3232 self.resolve_type(SYS_DELETED),
3233 SYS_DELETED,
3234 seq,
3235 page_rows,
3236 )? {
3237 columnar::NativeColumn::Bool { data, .. } => data[local_index] != 0,
3238 _ => return Err(MongrelError::InvalidArgument("sys deleted not bool".into())),
3239 };
3240 Ok(Some(VersionVisibility {
3241 stamp: VersionStamp {
3242 epoch: Epoch(epoch),
3243 commit_ts,
3244 },
3245 deleted,
3246 }))
3247 }
3248
3249 fn materialize_in_page(&mut self, seq: usize, local_index: usize) -> Result<Row> {
3258 let page_rows = self.find_header(SYS_ROW_ID)?.page_stats[seq].row_count as usize;
3259 let page_start: usize = self.find_header(SYS_ROW_ID)?.page_stats[..seq]
3260 .iter()
3261 .map(|s| s.row_count as usize)
3262 .sum();
3263 let global_index = page_start + local_index;
3264 let native_at = |slf: &mut Self, column_id: u16| -> Result<Option<Value>> {
3265 if !slf.dir.iter().any(|h| h.column_id == column_id) {
3268 return Ok(None);
3269 }
3270 let ty = slf.resolve_type(column_id);
3271 if !matches!(
3272 ty,
3273 TypeId::Bool
3274 | TypeId::Int8
3275 | TypeId::Int16
3276 | TypeId::Int32
3277 | TypeId::Int64
3278 | TypeId::UInt8
3279 | TypeId::UInt16
3280 | TypeId::UInt32
3281 | TypeId::UInt64
3282 | TypeId::Float32
3283 | TypeId::Float64
3284 | TypeId::TimestampNanos
3285 | TypeId::Date32
3286 | TypeId::Bytes
3287 ) {
3288 return Ok(slf.column(column_id)?.get(global_index).cloned());
3291 }
3292 Ok(slf
3293 .decode_page_native_cached(ty, column_id, seq, page_rows)?
3294 .value_at(local_index))
3295 };
3296 let row_id = RowId(match native_at(self, SYS_ROW_ID)? {
3297 Some(Value::Int64(x)) => x as u64,
3298 _ => 0,
3299 });
3300 let committed_epoch = Epoch(match native_at(self, SYS_EPOCH)? {
3301 Some(Value::Int64(x)) => x as u64,
3302 _ => 0,
3303 });
3304 let deleted = matches!(native_at(self, SYS_DELETED)?, Some(Value::Bool(true)));
3305 let commit_ts = if self.has_column(SYS_COMMIT_TS) {
3306 decode_commit_ts_value(native_at(self, SYS_COMMIT_TS)?.as_ref())
3307 } else {
3308 None
3309 };
3310 let col_ids: Vec<u16> = self.schema.columns.iter().map(|c| c.id).collect();
3311 let mut columns = HashMap::new();
3312 for id in col_ids {
3313 columns.insert(id, native_at(self, id)?.unwrap_or(Value::Null));
3314 }
3315 Ok(Row {
3316 row_id,
3317 committed_epoch,
3318 columns,
3319 deleted,
3320 commit_ts,
3321 })
3322 }
3323
3324 pub fn all_rows(&mut self) -> Result<Vec<Row>> {
3327 let n = self.row_count();
3328 let mut out = Vec::with_capacity(n);
3329 for i in 0..n {
3330 out.push(self.materialize(i)?);
3331 }
3332 Ok(out)
3333 }
3334
3335 pub fn for_each_system<F>(&mut self, mut visit: F) -> Result<()>
3342 where
3343 F: FnMut(RowId, Epoch, Option<HlcTimestamp>, bool) -> Result<()>,
3344 {
3345 let row_ids = self.column(SYS_ROW_ID)?.to_vec();
3349 let epochs = self.column(SYS_EPOCH)?.to_vec();
3350 let deleted = self.column(SYS_DELETED)?.to_vec();
3351 let has_commit_ts = self.has_column(SYS_COMMIT_TS);
3352 let commit_ts: Vec<Option<HlcTimestamp>> = if has_commit_ts {
3353 let col = self.column(SYS_COMMIT_TS)?;
3354 let mut out = Vec::with_capacity(col.len());
3355 for v in col {
3356 out.push(decode_commit_ts_value(Some(v)));
3357 }
3358 out
3359 } else {
3360 Vec::new()
3361 };
3362 debug_assert_eq!(row_ids.len(), epochs.len());
3363 debug_assert_eq!(row_ids.len(), deleted.len());
3364 for (i, rid_val) in row_ids.iter().enumerate() {
3365 let rid = match rid_val {
3366 Value::Int64(n) => RowId(*n as u64),
3367 _ => continue,
3368 };
3369 let epoch = match epochs[i] {
3370 Value::Int64(n) => Epoch(n as u64),
3371 _ => Epoch(0),
3372 };
3373 let ts = commit_ts.get(i).copied().flatten();
3374 let del = match deleted[i] {
3375 Value::Bool(b) => b,
3376 _ => false,
3377 };
3378 visit(rid, epoch, ts, del)?;
3379 }
3380 Ok(())
3381 }
3382
3383 pub fn all_rows_controlled(
3385 &mut self,
3386 control: &crate::ExecutionControl,
3387 max_rows: usize,
3388 ) -> Result<Vec<Row>> {
3389 let n = self.row_count();
3390 if n > max_rows {
3391 return Err(MongrelError::ResourceLimitExceeded {
3392 resource: "controlled run row materialization",
3393 requested: n,
3394 limit: max_rows,
3395 });
3396 }
3397 let mut out = Vec::with_capacity(n);
3398 for i in 0..n {
3399 if i % 256 == 0 {
3400 control.checkpoint()?;
3401 }
3402 out.push(self.materialize(i)?);
3403 }
3404 control.checkpoint()?;
3405 Ok(out)
3406 }
3407
3408 pub fn visible_indices(&mut self, snapshot: Epoch) -> Result<Vec<usize>> {
3414 self.visible_indices_at(Snapshot::at(snapshot))
3415 }
3416
3417 pub fn visible_indices_at(&mut self, snapshot: Snapshot) -> Result<Vec<usize>> {
3421 let n = self.row_count();
3422 if n == 0 {
3423 return Ok(Vec::new());
3424 }
3425 let row_ids = self.column(SYS_ROW_ID)?.to_vec();
3426 let epochs = self.column(SYS_EPOCH)?.to_vec();
3427 let deleted = self.column(SYS_DELETED)?.to_vec();
3428 let commit_ts_native = if self.has_column(SYS_COMMIT_TS) {
3429 Some(self.column(SYS_COMMIT_TS)?.to_vec())
3430 } else {
3431 None
3432 };
3433 let mut best: HashMap<u64, (u64, Option<HlcTimestamp>, usize)> = HashMap::new();
3434 for i in 0..n {
3435 let rid = int_at(&row_ids, i);
3436 let e = int_at(&epochs, i);
3437 let commit_ts = commit_ts_native
3438 .as_ref()
3439 .and_then(|vals| decode_commit_ts_value(vals.get(i)));
3440 if !snapshot.observes_row(Epoch(e), commit_ts) {
3441 continue;
3442 }
3443 best.entry(rid)
3444 .and_modify(|(be, bts, bi)| {
3445 if Snapshot::version_is_newer(Epoch(e), commit_ts, Epoch(*be), *bts) {
3446 *be = e;
3447 *bts = commit_ts;
3448 *bi = i;
3449 }
3450 })
3451 .or_insert((e, commit_ts, i));
3452 }
3453 let mut idxs: Vec<usize> = best.into_values().map(|(_, _, i)| i).collect();
3454 idxs.retain(|&i| !bool_at(&deleted, i));
3455 idxs.sort_unstable();
3456 Ok(idxs)
3457 }
3458
3459 pub fn gather_column(&mut self, column_id: u16, indices: &[usize]) -> Result<Vec<Value>> {
3464 if !self.dir.iter().any(|h| h.column_id == column_id) {
3465 return Ok(vec![Value::Null; indices.len()]);
3466 }
3467 let col = self.column(column_id)?;
3468 Ok(indices
3469 .iter()
3470 .map(|&i| col.get(i).cloned().unwrap_or(Value::Null))
3471 .collect())
3472 }
3473
3474 pub fn column_native(&mut self, column_id: u16) -> Result<columnar::NativeColumn> {
3479 use rayon::prelude::*;
3480 if column_id == SYS_EPOCH {
3481 if let Some(ov) = self.epoch_override {
3482 return Ok(columnar::NativeColumn::int64_constant(
3483 ov.0 as i64,
3484 self.row_count(),
3485 ));
3486 }
3487 }
3488 let ty = self.resolve_type(column_id);
3489 let n = self.row_count();
3490 let Some(ch) = self.dir.iter().find(|h| h.column_id == column_id) else {
3491 return Ok(columnar::null_native(ty, n));
3492 };
3493 let page_count = ch.page_count as usize;
3494 let page_rows: Vec<usize> = ch.page_stats.iter().map(|s| s.row_count as usize).collect();
3495 if page_count == 0 {
3496 return Ok(columnar::null_native(ty, n));
3497 }
3498 let parts: Vec<columnar::NativeColumn> = if self.mmap.is_some() && page_count > 1 {
3499 let reader: &RunReader = self;
3503 (0..page_count)
3504 .into_par_iter()
3505 .map(|seq| {
3506 let raw = reader.read_page_shared(column_id, seq)?;
3507 columnar::decode_page_native(ty.clone(), &raw, page_rows[seq])
3508 })
3509 .collect::<Result<Vec<_>>>()?
3510 } else {
3511 let mut out = Vec::with_capacity(page_count);
3512 for (seq, &pr) in page_rows.iter().enumerate() {
3513 let page = self.read_page(column_id, seq)?;
3514 out.push(columnar::decode_page_native(ty.clone(), &page, pr)?);
3515 }
3516 out
3517 };
3518 Ok(columnar::NativeColumn::concat(&parts))
3519 }
3520
3521 pub fn has_mmap(&self) -> bool {
3525 self.mmap.is_some()
3526 }
3527
3528 pub fn column_native_shared(&self, column_id: u16) -> Result<columnar::NativeColumn> {
3535 use rayon::prelude::*;
3536 if column_id == SYS_EPOCH {
3537 if let Some(ov) = self.epoch_override {
3538 return Ok(columnar::NativeColumn::int64_constant(
3539 ov.0 as i64,
3540 self.row_count(),
3541 ));
3542 }
3543 }
3544 let ty = self.resolve_type(column_id);
3545 let n = self.row_count();
3546 let Some(ch) = self.dir.iter().find(|h| h.column_id == column_id) else {
3547 return Ok(columnar::null_native(ty, n));
3548 };
3549 let page_count = ch.page_count as usize;
3550 let page_rows: Vec<usize> = ch.page_stats.iter().map(|s| s.row_count as usize).collect();
3551 if page_count == 0 {
3552 return Ok(columnar::null_native(ty, n));
3553 }
3554 #[cfg(unix)]
3558 {
3559 if let (Some(m), Some(first)) = (&self.mmap, ch.page_stats.first()) {
3560 let start = first.offset as usize;
3561 let end = (ch.page_region_offset as usize) + (ch.page_region_len as usize);
3562 if end > start {
3563 let _ = m.advise_range(memmap2::Advice::WillNeed, start, end - start);
3564 }
3565 }
3566 }
3567 let run_id = self.header.run_id;
3568 let mut parts_keys: Vec<(columnar::NativeColumn, Option<[u8; 32]>)> = if page_count > 1 {
3572 (0..page_count)
3573 .into_par_iter()
3574 .map(|seq| {
3575 self.decode_page_cached(ty.clone(), column_id, seq, page_rows[seq], run_id)
3576 })
3577 .collect::<Result<Vec<_>>>()?
3578 } else {
3579 vec![self.decode_page_cached(ty, column_id, 0, page_rows[0], run_id)?]
3580 };
3581 if let Some(cache) = &self.decoded_cache {
3584 for (col, key) in parts_keys.iter_mut() {
3585 if let Some(k) = key.take() {
3586 cache.lock(&k).insert(k, Arc::new(col.clone()));
3587 }
3588 }
3589 }
3590 let parts: Vec<columnar::NativeColumn> = parts_keys.into_iter().map(|(c, _)| c).collect();
3591 Ok(columnar::NativeColumn::concat(&parts))
3592 }
3593
3594 fn decode_page_cached(
3599 &self,
3600 ty: TypeId,
3601 column_id: u16,
3602 seq: usize,
3603 nrows: usize,
3604 run_id: u128,
3605 ) -> Result<(columnar::NativeColumn, Option<[u8; 32]>)> {
3606 let key = page_cache_key(self.table_id, run_id, column_id, seq);
3607 if let Some(cache) = &self.decoded_cache {
3608 if let Some(g) = cache.try_lock(&key) {
3609 if let Some(hit) = g.try_get(&key) {
3610 return Ok(((*hit).clone(), None));
3611 }
3612 }
3613 }
3614 let raw = self.read_page_shared(column_id, seq)?;
3615 let col = columnar::decode_page_native(ty, &raw, nrows)?;
3616 Ok((col, Some(key)))
3617 }
3618
3619 fn decode_page_native_cached(
3630 &mut self,
3631 ty: TypeId,
3632 column_id: u16,
3633 seq: usize,
3634 nrows: usize,
3635 ) -> Result<columnar::NativeColumn> {
3636 let key = page_cache_key(self.table_id, self.header.run_id, column_id, seq);
3637 if let Some(cache) = &self.decoded_cache {
3638 if let Some(hit) = cache.lock(&key).try_get(&key) {
3639 return Ok((*hit).clone());
3640 }
3641 }
3642 let raw = self.read_page(column_id, seq)?;
3643 let col = columnar::decode_page_native(ty, &raw, nrows)?;
3644 if let Some(cache) = &self.decoded_cache {
3645 cache
3646 .lock(&key)
3647 .insert(key, std::sync::Arc::new(col.clone()));
3648 }
3649 Ok(col)
3650 }
3651
3652 pub fn range_row_ids_i64(
3657 &mut self,
3658 column_id: u16,
3659 lo: i64,
3660 hi: i64,
3661 ) -> Result<std::collections::HashSet<u64>> {
3662 Ok(self
3663 .range_row_id_set_i64(column_id, lo, hi)?
3664 .into_sorted_vec()
3665 .into_iter()
3666 .collect())
3667 }
3668
3669 pub(crate) fn range_row_id_set_i64(
3670 &mut self,
3671 column_id: u16,
3672 lo: i64,
3673 hi: i64,
3674 ) -> Result<RowIdSet> {
3675 let info: Vec<(Option<i64>, Option<i64>, usize)> =
3676 match self.dir.iter().find(|h| h.column_id == column_id) {
3677 Some(ch) => ch
3678 .page_stats
3679 .iter()
3680 .map(|s| {
3681 (
3682 be_i64(s.min.as_deref()),
3683 be_i64(s.max.as_deref()),
3684 s.row_count as usize,
3685 )
3686 })
3687 .collect(),
3688 None => return Ok(RowIdSet::empty()),
3689 };
3690 let stats_pruneable =
3696 !self.col_encrypted(column_id) || self.header.encrypted_stats_offset != 0;
3697 let clean_contiguous = self.clean_contiguous_row_ids();
3698 let mut out = Vec::new();
3699 let mut page_start = 0usize;
3700 for (seq, (mn, mx, nrows)) in info.into_iter().enumerate() {
3701 let current_page_start = page_start;
3702 page_start += nrows;
3703 let skip = stats_pruneable
3705 && match (mn, mx) {
3706 (Some(mn), Some(mx)) => mx < lo || mn > hi,
3707 _ => true,
3708 };
3709 if skip {
3710 continue;
3711 }
3712 let val_page = self.read_page(column_id, seq)?;
3713 let vals =
3714 columnar::decode_page_native(self.resolve_type(column_id), &val_page, nrows)?;
3715 if let columnar::NativeColumn::Int64 { data: v, validity } = vals {
3716 if clean_contiguous {
3717 for (i, val) in v.iter().enumerate() {
3718 if columnar::validity_bit(&validity, i) && *val >= lo && *val <= hi {
3719 out.push(self.header.min_row_id + current_page_start as u64 + i as u64);
3720 }
3721 }
3722 } else {
3723 let rid_page = self.read_page(SYS_ROW_ID, seq)?;
3724 let rids = columnar::decode_page_native(TypeId::Int64, &rid_page, nrows)?;
3725 if let columnar::NativeColumn::Int64 { data: r, .. } = rids {
3726 for (i, val) in v.iter().enumerate() {
3727 if columnar::validity_bit(&validity, i) && *val >= lo && *val <= hi {
3728 out.push(r[i] as u64);
3729 }
3730 }
3731 }
3732 }
3733 }
3734 }
3735 Ok(RowIdSet::from_unsorted(out))
3736 }
3737
3738 pub fn range_row_ids_f64(
3741 &mut self,
3742 column_id: u16,
3743 lo: f64,
3744 lo_inclusive: bool,
3745 hi: f64,
3746 hi_inclusive: bool,
3747 ) -> Result<std::collections::HashSet<u64>> {
3748 Ok(self
3749 .range_row_id_set_f64(column_id, lo, lo_inclusive, hi, hi_inclusive)?
3750 .into_sorted_vec()
3751 .into_iter()
3752 .collect())
3753 }
3754
3755 pub(crate) fn range_row_id_set_f64(
3756 &mut self,
3757 column_id: u16,
3758 lo: f64,
3759 lo_inclusive: bool,
3760 hi: f64,
3761 hi_inclusive: bool,
3762 ) -> Result<RowIdSet> {
3763 let info: Vec<(Option<f64>, Option<f64>, usize)> =
3764 match self.dir.iter().find(|h| h.column_id == column_id) {
3765 Some(ch) => ch
3766 .page_stats
3767 .iter()
3768 .map(|s| {
3769 (
3770 be_f64(s.min.as_deref()),
3771 be_f64(s.max.as_deref()),
3772 s.row_count as usize,
3773 )
3774 })
3775 .collect(),
3776 None => return Ok(RowIdSet::empty()),
3777 };
3778 let stats_pruneable =
3784 !self.col_encrypted(column_id) || self.header.encrypted_stats_offset != 0;
3785 let clean_contiguous = self.clean_contiguous_row_ids();
3786 let mut out = Vec::new();
3787 let mut page_start = 0usize;
3788 for (seq, (mn, mx, nrows)) in info.into_iter().enumerate() {
3789 let current_page_start = page_start;
3790 page_start += nrows;
3791 let skip = stats_pruneable
3794 && match (mn, mx) {
3795 (Some(mn), Some(mx)) => {
3796 let skip_lo = mx < lo || (!lo_inclusive && mx == lo);
3797 let skip_hi = mn > hi || (!hi_inclusive && mn == hi);
3798 skip_lo || skip_hi
3799 }
3800 _ => true,
3801 };
3802 if skip {
3803 continue;
3804 }
3805 let val_page = self.read_page(column_id, seq)?;
3806 let vals = columnar::decode_page_native(TypeId::Float64, &val_page, nrows)?;
3807 if let columnar::NativeColumn::Float64 { data: v, validity } = vals {
3808 if clean_contiguous {
3809 for (i, val) in v.iter().enumerate() {
3810 if !columnar::validity_bit(&validity, i) || val.is_nan() {
3811 continue;
3812 }
3813 let ok_lo = if lo_inclusive { *val >= lo } else { *val > lo };
3814 let ok_hi = if hi_inclusive { *val <= hi } else { *val < hi };
3815 if ok_lo && ok_hi {
3816 out.push(self.header.min_row_id + current_page_start as u64 + i as u64);
3817 }
3818 }
3819 } else {
3820 let rid_page = self.read_page(SYS_ROW_ID, seq)?;
3821 let rids = columnar::decode_page_native(TypeId::Int64, &rid_page, nrows)?;
3822 if let columnar::NativeColumn::Int64 { data: r, .. } = rids {
3823 for (i, val) in v.iter().enumerate() {
3824 if !columnar::validity_bit(&validity, i) || val.is_nan() {
3825 continue;
3826 }
3827 let ok_lo = if lo_inclusive { *val >= lo } else { *val > lo };
3828 let ok_hi = if hi_inclusive { *val <= hi } else { *val < hi };
3829 if ok_lo && ok_hi {
3830 out.push(r[i] as u64);
3831 }
3832 }
3833 }
3834 }
3835 }
3836 }
3837 Ok(RowIdSet::from_unsorted(out))
3838 }
3839
3840 pub(crate) fn null_row_id_set(&mut self, column_id: u16, want_nulls: bool) -> Result<RowIdSet> {
3845 let stats: Vec<(usize, usize)> = match self.dir.iter().find(|h| h.column_id == column_id) {
3846 Some(ch) => ch
3847 .page_stats
3848 .iter()
3849 .map(|s| (s.null_count as usize, s.row_count as usize))
3850 .collect(),
3851 None => return Ok(RowIdSet::empty()),
3852 };
3853 let ty = self.resolve_type(column_id);
3854 let clean_contiguous = self.clean_contiguous_row_ids();
3855 let mut out = Vec::new();
3856 let mut page_start = 0usize;
3857 for (seq, (null_count, nrows)) in stats.into_iter().enumerate() {
3858 let current_page_start = page_start;
3859 page_start += nrows;
3860 if want_nulls && null_count == 0 {
3862 continue;
3863 }
3864 if !want_nulls && null_count == nrows {
3865 continue;
3866 }
3867 let val_page = self.read_page(column_id, seq)?;
3868 let col = columnar::decode_page_native(ty.clone(), &val_page, nrows)?;
3869 let validity = col.validity();
3870 if clean_contiguous {
3871 for i in 0..nrows {
3872 let is_null = !columnar::validity_bit(validity, i);
3873 if is_null == want_nulls {
3874 out.push(self.header.min_row_id + current_page_start as u64 + i as u64);
3875 }
3876 }
3877 } else {
3878 let rid_page = self.read_page(SYS_ROW_ID, seq)?;
3879 let rids = columnar::decode_page_native(TypeId::Int64, &rid_page, nrows)?;
3880 if let columnar::NativeColumn::Int64 { data: r, .. } = rids {
3881 for (i, &rid) in r.iter().enumerate().take(nrows) {
3882 let is_null = !columnar::validity_bit(validity, i);
3883 if is_null == want_nulls {
3884 out.push(rid as u64);
3885 }
3886 }
3887 }
3888 }
3889 }
3890 Ok(RowIdSet::from_unsorted(out))
3891 }
3892
3893 pub fn visible_indices_native(&mut self, snapshot: Epoch) -> Result<Vec<usize>> {
3894 self.visible_indices_native_at(Snapshot::at(snapshot))
3895 }
3896
3897 pub fn visible_indices_native_at(&mut self, snapshot: Snapshot) -> Result<Vec<usize>> {
3899 let n = self.row_count();
3900 if n == 0 {
3901 return Ok(Vec::new());
3902 }
3903 let (row_ids, epochs, deleted) = self.system_columns_native()?;
3904 let commit_ts_native = if self.has_column(SYS_COMMIT_TS) {
3905 Some(self.column_native_shared(SYS_COMMIT_TS)?)
3906 } else {
3907 None
3908 };
3909 let mut best: HashMap<u64, (u64, Option<HlcTimestamp>, usize)> = HashMap::new();
3910 for i in 0..n {
3911 let rid = row_ids[i] as u64;
3912 let e = epochs[i] as u64;
3913 let commit_ts = commit_ts_native
3914 .as_ref()
3915 .and_then(|col| decode_commit_ts_value(col.value_at(i).as_ref()));
3916 if !snapshot.observes_row(Epoch(e), commit_ts) {
3917 continue;
3918 }
3919 best.entry(rid)
3920 .and_modify(|(be, bts, bi)| {
3921 if Snapshot::version_is_newer(Epoch(e), commit_ts, Epoch(*be), *bts) {
3922 *be = e;
3923 *bts = commit_ts;
3924 *bi = i;
3925 }
3926 })
3927 .or_insert((e, commit_ts, i));
3928 }
3929 let mut idxs: Vec<usize> = best.into_values().map(|(_, _, i)| i).collect();
3930 idxs.retain(|&i| deleted[i] == 0);
3931 idxs.sort_unstable();
3932 Ok(idxs)
3933 }
3934
3935 pub fn range_row_ids_visible_i64(
3944 &mut self,
3945 column_id: u16,
3946 lo: i64,
3947 hi: i64,
3948 snapshot: Epoch,
3949 ) -> Result<Vec<u64>> {
3950 self.range_row_ids_visible_i64_at(column_id, lo, hi, Snapshot::at(snapshot))
3951 }
3952
3953 pub fn range_row_ids_visible_i64_at(
3955 &mut self,
3956 column_id: u16,
3957 lo: i64,
3958 hi: i64,
3959 snapshot: Snapshot,
3960 ) -> Result<Vec<u64>> {
3961 let stats: Vec<(Option<i64>, Option<i64>, usize)> = match self.column_page_stats(column_id)
3962 {
3963 Some(s) => s
3964 .iter()
3965 .map(|st| {
3966 (
3967 be_i64(st.min.as_deref()),
3968 be_i64(st.max.as_deref()),
3969 st.row_count as usize,
3970 )
3971 })
3972 .collect(),
3973 None => return Ok(Vec::new()),
3974 };
3975 let stats_pruneable =
3981 !self.col_encrypted(column_id) || self.header.encrypted_stats_offset != 0;
3982 let (positions, rids) = self.visible_positions_with_rids_at(snapshot)?;
3983 let mut out: Vec<u64> = Vec::new();
3984 let mut vis = 0usize;
3985 let mut page_start = 0usize;
3986 for (seq, &(mn, mx, nrows)) in stats.iter().enumerate() {
3987 let page_end = page_start + nrows;
3988 let skip = stats_pruneable
3990 && match (mn, mx) {
3991 (Some(mn), Some(mx)) => mx < lo || mn > hi,
3992 _ => true, };
3994 if !skip {
3995 let val_page = self.read_page(column_id, seq)?;
3996 let vals = columnar::decode_page_native(TypeId::Int64, &val_page, nrows)?;
3997 if let columnar::NativeColumn::Int64 { data: v, validity } = vals {
3998 while vis < positions.len() && positions[vis] < page_end {
3999 let local = positions[vis] - page_start;
4000 if columnar::validity_bit(&validity, local)
4001 && v[local] >= lo
4002 && v[local] <= hi
4003 {
4004 out.push(rids[vis] as u64);
4005 }
4006 vis += 1;
4007 }
4008 }
4009 } else {
4010 while vis < positions.len() && positions[vis] < page_end {
4011 vis += 1;
4012 }
4013 }
4014 page_start = page_end;
4015 }
4016 Ok(out)
4017 }
4018
4019 pub fn range_row_ids_visible_f64(
4022 &mut self,
4023 column_id: u16,
4024 lo: f64,
4025 lo_inclusive: bool,
4026 hi: f64,
4027 hi_inclusive: bool,
4028 snapshot: Epoch,
4029 ) -> Result<Vec<u64>> {
4030 self.range_row_ids_visible_f64_at(
4031 column_id,
4032 lo,
4033 lo_inclusive,
4034 hi,
4035 hi_inclusive,
4036 Snapshot::at(snapshot),
4037 )
4038 }
4039
4040 pub fn range_row_ids_visible_f64_at(
4042 &mut self,
4043 column_id: u16,
4044 lo: f64,
4045 lo_inclusive: bool,
4046 hi: f64,
4047 hi_inclusive: bool,
4048 snapshot: Snapshot,
4049 ) -> Result<Vec<u64>> {
4050 let stats: Vec<(Option<f64>, Option<f64>, usize)> = match self.column_page_stats(column_id)
4051 {
4052 Some(s) => s
4053 .iter()
4054 .map(|st| {
4055 (
4056 be_f64(st.min.as_deref()),
4057 be_f64(st.max.as_deref()),
4058 st.row_count as usize,
4059 )
4060 })
4061 .collect(),
4062 None => return Ok(Vec::new()),
4063 };
4064 let stats_pruneable =
4070 !self.col_encrypted(column_id) || self.header.encrypted_stats_offset != 0;
4071 let (positions, rids) = self.visible_positions_with_rids_at(snapshot)?;
4072 let mut out: Vec<u64> = Vec::new();
4073 let mut vis = 0usize;
4074 let mut page_start = 0usize;
4075 for (seq, &(mn, mx, nrows)) in stats.iter().enumerate() {
4076 let page_end = page_start + nrows;
4077 let skip = stats_pruneable
4078 && match (mn, mx) {
4079 (Some(mn), Some(mx)) => {
4080 let skip_lo = mx < lo || (!lo_inclusive && mx == lo);
4081 let skip_hi = mn > hi || (!hi_inclusive && mn == hi);
4082 skip_lo || skip_hi
4083 }
4084 _ => true,
4085 };
4086 if !skip {
4087 let val_page = self.read_page(column_id, seq)?;
4088 let vals = columnar::decode_page_native(TypeId::Float64, &val_page, nrows)?;
4089 if let columnar::NativeColumn::Float64 { data: v, validity } = vals {
4090 while vis < positions.len() && positions[vis] < page_end {
4091 let local = positions[vis] - page_start;
4092 if columnar::validity_bit(&validity, local) && !v[local].is_nan() {
4093 let val = v[local];
4094 let ok_lo = if lo_inclusive { val >= lo } else { val > lo };
4095 let ok_hi = if hi_inclusive { val <= hi } else { val < hi };
4096 if ok_lo && ok_hi {
4097 out.push(rids[vis] as u64);
4098 }
4099 }
4100 vis += 1;
4101 }
4102 }
4103 } else {
4104 while vis < positions.len() && positions[vis] < page_end {
4105 vis += 1;
4106 }
4107 }
4108 page_start = page_end;
4109 }
4110 Ok(out)
4111 }
4112
4113 pub fn null_row_ids_visible(
4119 &mut self,
4120 column_id: u16,
4121 want_nulls: bool,
4122 snapshot: Epoch,
4123 ) -> Result<Vec<u64>> {
4124 self.null_row_ids_visible_at(column_id, want_nulls, Snapshot::at(snapshot))
4125 }
4126
4127 pub fn null_row_ids_visible_at(
4129 &mut self,
4130 column_id: u16,
4131 want_nulls: bool,
4132 snapshot: Snapshot,
4133 ) -> Result<Vec<u64>> {
4134 let stats: Vec<(usize, usize)> = match self.column_page_stats(column_id) {
4135 Some(s) => s
4136 .iter()
4137 .map(|st| (st.null_count as usize, st.row_count as usize))
4138 .collect(),
4139 None => return Ok(Vec::new()),
4140 };
4141 let ty = self.resolve_type(column_id);
4142 let (positions, rids) = self.visible_positions_with_rids_at(snapshot)?;
4143 let mut out: Vec<u64> = Vec::new();
4144 let mut vis = 0usize;
4145 let mut page_start = 0usize;
4146 for (seq, &(null_count, nrows)) in stats.iter().enumerate() {
4147 let page_end = page_start + nrows;
4148 let skip = (want_nulls && null_count == 0) || (!want_nulls && null_count == nrows);
4149 if !skip {
4150 let val_page = self.read_page(column_id, seq)?;
4151 let col = columnar::decode_page_native(ty.clone(), &val_page, nrows)?;
4152 let validity = col.validity();
4153 while vis < positions.len() && positions[vis] < page_end {
4154 let local = positions[vis] - page_start;
4155 let is_null = !columnar::validity_bit(validity, local);
4156 if is_null == want_nulls {
4157 out.push(rids[vis] as u64);
4158 }
4159 vis += 1;
4160 }
4161 } else {
4162 while vis < positions.len() && positions[vis] < page_end {
4163 vis += 1;
4164 }
4165 }
4166 page_start = page_end;
4167 }
4168 Ok(out)
4169 }
4170
4171 pub fn tombstoned_row_ids(&mut self, snapshot: Epoch) -> Result<Vec<u64>> {
4178 self.tombstoned_row_ids_at(Snapshot::at(snapshot))
4179 }
4180
4181 pub fn tombstoned_row_ids_at(&mut self, snapshot: Snapshot) -> Result<Vec<u64>> {
4183 let n = self.row_count();
4184 if n == 0 {
4185 return Ok(Vec::new());
4186 }
4187 if self.is_clean()
4189 && self.epoch_override.is_none()
4190 && self.header.epoch_created <= snapshot.epoch.0
4191 && (!snapshot.uses_hlc_authority() || !self.has_column(SYS_COMMIT_TS))
4192 {
4193 return Ok(Vec::new());
4194 }
4195 let (row_ids, epochs, deleted) = self.system_columns_native()?;
4196 let commit_ts_native = if self.has_column(SYS_COMMIT_TS) {
4197 Some(self.column_native_shared(SYS_COMMIT_TS)?)
4198 } else {
4199 None
4200 };
4201 let mut out = Vec::new();
4202 let mut i = 0;
4203 while i < n {
4204 let rid = row_ids[i] as u64;
4205 let mut best: Option<usize> = None;
4206 let mut j = i;
4207 while j < n && row_ids[j] as u64 == rid {
4208 let e = epochs[j] as u64;
4209 let commit_ts = commit_ts_native
4210 .as_ref()
4211 .and_then(|col| decode_commit_ts_value(col.value_at(j).as_ref()));
4212 if snapshot.observes_row(Epoch(e), commit_ts)
4213 && (best.is_none_or(|cur| {
4214 Snapshot::version_is_newer(
4215 Epoch(e),
4216 commit_ts,
4217 Epoch(epochs[cur] as u64),
4218 commit_ts_native
4219 .as_ref()
4220 .and_then(|col| decode_commit_ts_value(col.value_at(cur).as_ref())),
4221 )
4222 }))
4223 {
4224 best = Some(j);
4225 }
4226 j += 1;
4227 }
4228 if let Some(b) = best {
4229 if deleted[b] != 0 {
4230 out.push(rid);
4231 }
4232 }
4233 i = j;
4234 }
4235 Ok(out)
4236 }
4237
4238 pub fn visible_positions_with_rids(
4242 &mut self,
4243 snapshot: Epoch,
4244 ) -> Result<(Vec<usize>, Vec<i64>)> {
4245 self.visible_positions_with_rids_at(Snapshot::at(snapshot))
4246 }
4247
4248 pub fn visible_positions_with_rids_at(
4252 &mut self,
4253 snapshot: Snapshot,
4254 ) -> Result<(Vec<usize>, Vec<i64>)> {
4255 let n = self.row_count();
4256 if n == 0 {
4257 return Ok((Vec::new(), Vec::new()));
4258 }
4259 if self.is_clean()
4272 && self.epoch_override.is_none()
4273 && self.header.epoch_created <= snapshot.epoch.0
4274 {
4275 let row_ids = match self.column_native_shared(SYS_ROW_ID)? {
4276 columnar::NativeColumn::Int64 { data, .. } => data,
4277 _ => return Err(MongrelError::InvalidArgument("sys row_id not int64".into())),
4278 };
4279 if !snapshot.uses_hlc_authority() || !self.has_column(SYS_COMMIT_TS) {
4281 let positions: Vec<usize> = (0..n).collect();
4282 return Ok((positions, row_ids));
4283 }
4284 let commit_ts_native = self.column_native_shared(SYS_COMMIT_TS)?;
4286 let mut idxs = Vec::with_capacity(n);
4287 let mut rids = Vec::with_capacity(n);
4288 for (i, &rid) in row_ids.iter().enumerate().take(n) {
4289 let ts = decode_commit_ts_value(commit_ts_native.value_at(i).as_ref());
4290 if snapshot.observes_row(Epoch(self.header.epoch_created), ts) {
4292 idxs.push(i);
4293 rids.push(rid);
4294 }
4295 }
4296 return Ok((idxs, rids));
4297 }
4298 let (row_ids, epochs, deleted) = self.system_columns_native()?;
4299 let commit_ts_native = if self.has_column(SYS_COMMIT_TS) {
4300 Some(self.column_native_shared(SYS_COMMIT_TS)?)
4301 } else {
4302 None
4303 };
4304 let mut idxs: Vec<usize> = Vec::new();
4311 let mut i = 0;
4312 while i < n {
4313 let rid = row_ids[i] as u64;
4314 let mut best: Option<usize> = None;
4315 let mut j = i;
4316 while j < n && row_ids[j] as u64 == rid {
4317 let e = epochs[j] as u64;
4318 let commit_ts = commit_ts_native
4319 .as_ref()
4320 .and_then(|col| decode_commit_ts_value(col.value_at(j).as_ref()));
4321 if snapshot.observes_row(Epoch(e), commit_ts) {
4322 let candidate = j;
4323 let is_newer = best.is_none_or(|cur| {
4324 Snapshot::version_is_newer(
4325 Epoch(e),
4326 commit_ts,
4327 Epoch(epochs[cur] as u64),
4328 commit_ts_native
4329 .as_ref()
4330 .and_then(|col| decode_commit_ts_value(col.value_at(cur).as_ref())),
4331 )
4332 });
4333 if best.is_none() || is_newer {
4334 best = Some(candidate);
4335 }
4336 }
4337 j += 1;
4338 }
4339 if let Some(b) = best {
4340 if deleted[b] == 0 {
4341 idxs.push(b);
4342 }
4343 }
4344 i = j;
4345 }
4346 let rids: Vec<i64> = idxs.iter().map(|&k| row_ids[k]).collect();
4347 Ok((idxs, rids))
4348 }
4349
4350 pub fn page_row_counts(&self, column_id: u16) -> Result<Vec<usize>> {
4354 Ok(self
4355 .find_header(column_id)?
4356 .page_stats
4357 .iter()
4358 .map(|s| s.row_count as usize)
4359 .collect())
4360 }
4361
4362 pub fn has_column(&self, column_id: u16) -> bool {
4365 self.dir.iter().any(|h| h.column_id == column_id)
4366 }
4367
4368 pub fn column_page_stats(&self, column_id: u16) -> Option<&[crate::page::PageStat]> {
4372 self.dir
4373 .iter()
4374 .find(|h| h.column_id == column_id)
4375 .map(|ch| ch.page_stats.as_slice())
4376 }
4377
4378 pub(crate) fn system_columns_native(&mut self) -> Result<(Vec<i64>, Vec<i64>, Vec<u8>)> {
4383 let row_ids = match self.column_native_shared(SYS_ROW_ID)? {
4384 columnar::NativeColumn::Int64 { data, .. } => data,
4385 _ => return Err(MongrelError::InvalidArgument("sys row_id not int64".into())),
4386 };
4387 let epochs = match self.column_native_shared(SYS_EPOCH)? {
4388 columnar::NativeColumn::Int64 { data, .. } => data,
4389 _ => return Err(MongrelError::InvalidArgument("sys epoch not int64".into())),
4390 };
4391 let deleted = match self.column_native_shared(SYS_DELETED)? {
4392 columnar::NativeColumn::Bool { data, .. } => data,
4393 _ => return Err(MongrelError::InvalidArgument("sys deleted not bool".into())),
4394 };
4395 Ok((row_ids, epochs, deleted))
4396 }
4397
4398 pub fn visible_versions(&mut self, snapshot: Epoch) -> Result<Vec<Row>> {
4407 self.visible_versions_at(Snapshot::at(snapshot))
4408 }
4409
4410 pub fn visible_versions_at(&mut self, snapshot: Snapshot) -> Result<Vec<Row>> {
4414 let n = self.row_count();
4415 if n == 0 {
4416 return Ok(Vec::new());
4417 }
4418 let row_ids = self.column(SYS_ROW_ID)?.to_vec();
4419 let epochs = self.column(SYS_EPOCH)?.to_vec();
4420 let commit_ts_native = if self.has_column(SYS_COMMIT_TS) {
4421 Some(self.column(SYS_COMMIT_TS)?.to_vec())
4422 } else {
4423 None
4424 };
4425 let mut best: HashMap<u64, (u64, Option<HlcTimestamp>, usize)> = HashMap::new();
4426 for i in 0..n {
4427 let rid = int_at(&row_ids, i);
4428 let e = int_at(&epochs, i);
4429 let commit_ts = commit_ts_native
4430 .as_ref()
4431 .and_then(|vals| decode_commit_ts_value(vals.get(i)));
4432 if !snapshot.observes_row(Epoch(e), commit_ts) {
4433 continue;
4434 }
4435 best.entry(rid)
4439 .and_modify(|cur| {
4440 if crate::epoch::version_supersedes(Epoch(e), commit_ts, Epoch(cur.0), cur.1) {
4441 *cur = (e, commit_ts, i);
4442 }
4443 })
4444 .or_insert((e, commit_ts, i));
4445 }
4446 let mut picks: Vec<usize> = best.into_values().map(|(_, _, i)| i).collect();
4447 picks.sort();
4448 let mut out = Vec::with_capacity(picks.len());
4449 for i in picks {
4450 out.push(self.materialize(i)?);
4451 }
4452 Ok(out)
4453 }
4454
4455 pub fn visible_rows(&mut self, snapshot: Epoch) -> Result<Vec<Row>> {
4457 Ok(self
4458 .visible_versions_at(Snapshot::at(snapshot))?
4459 .into_iter()
4460 .filter(|r| !r.deleted)
4461 .collect())
4462 }
4463
4464 pub(crate) fn materialize(&mut self, index: usize) -> Result<Row> {
4465 let row_id = RowId(int_at(self.column(SYS_ROW_ID)?, index));
4466 let epoch = Epoch(int_at(self.column(SYS_EPOCH)?, index));
4467 let deleted = bool_at(self.column(SYS_DELETED)?, index);
4468 let commit_ts = self.commit_ts_at(index)?;
4469 let col_ids: Vec<u16> = self.schema.columns.iter().map(|c| c.id).collect();
4470 let mut columns = HashMap::new();
4471 for id in col_ids {
4472 let val = if self.dir.iter().any(|h| h.column_id == id) {
4473 self.column(id)?.get(index).cloned().unwrap_or(Value::Null)
4474 } else {
4475 Value::Null
4477 };
4478 columns.insert(id, val);
4479 }
4480 Ok(Row {
4481 row_id,
4482 committed_epoch: epoch,
4483 columns,
4484 deleted,
4485 commit_ts,
4486 })
4487 }
4488
4489 pub(crate) fn materialize_batch(&mut self, indices: &[usize]) -> Result<Vec<Row>> {
4498 if indices.is_empty() {
4499 return Ok(Vec::new());
4500 }
4501 use std::collections::HashMap;
4502 let rid_col = self.column_native_shared(SYS_ROW_ID)?;
4503 let epoch_col = self.column_native_shared(SYS_EPOCH)?;
4504 let del_col = self.column_native_shared(SYS_DELETED)?;
4505 let commit_ts_col = if self.has_column(SYS_COMMIT_TS) {
4506 Some(self.column_native_shared(SYS_COMMIT_TS)?)
4507 } else {
4508 None
4509 };
4510 let mut user: HashMap<u16, columnar::NativeColumn> = HashMap::new();
4511 let present: Vec<u16> = self
4512 .schema
4513 .columns
4514 .iter()
4515 .map(|c| c.id)
4516 .filter(|id| self.dir.iter().any(|h| h.column_id == *id))
4517 .collect();
4518 for id in present {
4519 user.insert(id, self.column_native(id)?);
4520 }
4521 let i64_at = |col: &columnar::NativeColumn, i: usize| -> i64 {
4522 match col {
4523 columnar::NativeColumn::Int64 { data, .. } => data.get(i).copied().unwrap_or(0),
4524 _ => 0,
4525 }
4526 };
4527 let bool_at_native = |col: &columnar::NativeColumn, i: usize| -> bool {
4528 match col {
4529 columnar::NativeColumn::Bool { data, .. } => {
4530 data.get(i).copied().map(|b| b != 0).unwrap_or(false)
4531 }
4532 _ => false,
4533 }
4534 };
4535 let mut rows = Vec::with_capacity(indices.len());
4536 for &idx in indices {
4537 let row_id = RowId(i64_at(&rid_col, idx) as u64);
4538 let epoch = Epoch(i64_at(&epoch_col, idx) as u64);
4539 let deleted = bool_at_native(&del_col, idx);
4540 let commit_ts = commit_ts_col
4541 .as_ref()
4542 .and_then(|col| decode_commit_ts_value(col.value_at(idx).as_ref()));
4543 let mut columns = HashMap::with_capacity(self.schema.columns.len());
4544 for cdef in self.schema.columns.iter() {
4545 let val = match user.get(&cdef.id) {
4546 Some(col) => col.value_at(idx).unwrap_or(Value::Null),
4547 None => Value::Null,
4548 };
4549 columns.insert(cdef.id, val);
4550 }
4551 rows.push(Row {
4552 row_id,
4553 committed_epoch: epoch,
4554 columns,
4555 deleted,
4556 commit_ts,
4557 });
4558 }
4559 Ok(rows)
4560 }
4561}
4562
4563fn int_at(vals: &[Value], i: usize) -> u64 {
4564 match vals.get(i) {
4565 Some(Value::Int64(x)) => *x as u64,
4566 _ => 0,
4567 }
4568}
4569
4570fn bool_at(vals: &[Value], i: usize) -> bool {
4571 matches!(vals.get(i), Some(Value::Bool(true)))
4572}
4573
4574#[cfg(test)]
4575mod tests {
4576 use super::*;
4577 use crate::columnar::NativeColumn;
4578 use crate::memtable::Value;
4579 use crate::rowid::RowId;
4580 use crate::schema::{ColumnDef, ColumnFlags};
4581 use tempfile::tempdir;
4582
4583 fn schema() -> Schema {
4584 Schema {
4585 schema_id: 1,
4586 columns: vec![
4587 ColumnDef {
4588 id: 1,
4589 name: "id".into(),
4590 ty: TypeId::Int64,
4591 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
4592 default_value: None,
4593 embedding_source: None,
4594 },
4595 ColumnDef {
4596 id: 2,
4597 name: "name".into(),
4598 ty: TypeId::Bytes,
4599 flags: ColumnFlags::empty(),
4600 default_value: None,
4601 embedding_source: None,
4602 },
4603 ],
4604 indexes: Vec::new(),
4605 colocation: vec![],
4606 constraints: Default::default(),
4607 clustered: false,
4608 }
4609 }
4610
4611 fn rows() -> Vec<Row> {
4612 vec![
4613 Row::new(RowId(1), Epoch(10))
4614 .with_column(1, Value::Int64(1))
4615 .with_column(2, Value::Bytes(b"alice".to_vec())),
4616 Row::new(RowId(2), Epoch(10))
4617 .with_column(1, Value::Int64(2))
4618 .with_column(2, Value::Bytes(b"bob".to_vec())),
4619 Row::new(RowId(3), Epoch(10))
4620 .with_column(1, Value::Int64(3))
4621 .with_column(2, Value::Bytes(b"carol".to_vec())),
4622 ]
4623 }
4624
4625 #[test]
4626 fn flush_then_read_mvcc() {
4627 let dir = tempdir().unwrap();
4628 let path = dir.path().join("r-1.sr");
4629 let header = RunWriter::new(&schema(), 1, Epoch(10), 0)
4630 .write(&path, &rows())
4631 .unwrap();
4632 assert_eq!(header.row_count, 3);
4633
4634 let mut r = RunReader::open(&path, schema(), None).unwrap();
4635 let (e, row) = r.get_version(RowId(2), Epoch(20)).unwrap().unwrap();
4637 assert_eq!(e, Epoch(10));
4638 assert_eq!(row.row_id, RowId(2));
4639 assert!(matches!(row.columns.get(&2), Some(Value::Bytes(_))));
4640 assert!(r.get_version(RowId(99), Epoch(20)).unwrap().is_none());
4642 let all = r.visible_rows(Epoch(20)).unwrap();
4644 assert_eq!(all.len(), 3);
4645 assert!(!r.has_column(SYS_COMMIT_TS));
4647 assert!(all.iter().all(|row| row.commit_ts.is_none()));
4648 }
4649
4650 #[test]
4653 fn p05_t3_sys_commit_ts_preserved_on_flush_and_legacy_is_none() {
4654 let dir = tempdir().unwrap();
4655 let stamped_path = dir.path().join("r-hlc.sr");
4656 let stamp = HlcTimestamp {
4657 physical_micros: 42_000_000,
4658 logical: 7,
4659 node_tiebreaker: 3,
4660 };
4661 let stamped = vec![
4662 Row::new_with_hlc(RowId(1), Epoch(10), stamp)
4663 .with_column(1, Value::Int64(1))
4664 .with_column(2, Value::Bytes(b"alice".to_vec())),
4665 Row::new(RowId(2), Epoch(11)) .with_column(1, Value::Int64(2))
4667 .with_column(2, Value::Bytes(b"bob".to_vec())),
4668 ];
4669 RunWriter::new(&schema(), 9, Epoch(11), 0)
4670 .write(&stamped_path, &stamped)
4671 .unwrap();
4672 let mut reader = RunReader::open(&stamped_path, schema(), None).unwrap();
4673 assert!(
4674 reader.has_column(SYS_COMMIT_TS),
4675 "stamped flush must emit optional SYS_COMMIT_TS"
4676 );
4677 let (_, row1) = reader
4678 .get_version(RowId(1), Epoch(20))
4679 .unwrap()
4680 .expect("row 1");
4681 assert_eq!(row1.commit_ts, Some(stamp));
4682 let (_, row2) = reader
4683 .get_version(RowId(2), Epoch(20))
4684 .unwrap()
4685 .expect("row 2");
4686 assert_eq!(row2.commit_ts, None, "Null stamp cell stays None");
4687 let versions = reader.visible_versions(Epoch(20)).unwrap();
4688 assert_eq!(versions.len(), 2);
4689 assert_eq!(
4690 versions
4691 .iter()
4692 .find(|r| r.row_id == RowId(1))
4693 .and_then(|r| r.commit_ts),
4694 Some(stamp)
4695 );
4696
4697 let legacy_path = dir.path().join("r-legacy.sr");
4699 RunWriter::new(&schema(), 10, Epoch(10), 0)
4700 .write(&legacy_path, &rows())
4701 .unwrap();
4702 let mut legacy = RunReader::open(&legacy_path, schema(), None).unwrap();
4703 assert!(!legacy.has_column(SYS_COMMIT_TS));
4704 let all = legacy.visible_versions(Epoch(20)).unwrap();
4705 assert!(all.iter().all(|r| r.commit_ts.is_none()));
4706 }
4707
4708 #[test]
4709 fn visible_version_cursor_preserves_order_snapshot_and_tombstones() {
4710 let dir = tempdir().unwrap();
4711 let path = dir.path().join("r-cursor.sr");
4712 let mut tombstone = Row::new(RowId(2), Epoch(2));
4713 tombstone.deleted = true;
4714 let versions = vec![
4715 Row::new(RowId(1), Epoch(4)).with_column(1, Value::Int64(10)),
4716 Row::new(RowId(2), Epoch(1)).with_column(1, Value::Int64(2)),
4717 tombstone,
4718 Row::new(RowId(3), Epoch(1)).with_column(1, Value::Int64(20)),
4719 Row::new(RowId(3), Epoch(3)).with_column(1, Value::Int64(23)),
4720 Row::new(RowId(4), Epoch(4)).with_column(1, Value::Int64(30)),
4721 ];
4722 RunWriter::new(&schema(), 7, Epoch(4), 0)
4723 .write(&path, &versions)
4724 .unwrap();
4725
4726 let control = crate::ExecutionControl::new(None);
4727 let mut cursor = RunReader::open(&path, schema(), None)
4728 .unwrap()
4729 .into_visible_version_cursor(Epoch(2))
4730 .unwrap();
4731 let first = cursor.next_visible_version(&control).unwrap().unwrap();
4732 assert_eq!(first.row_id, RowId(2));
4733 assert!(first.deleted);
4734 assert_eq!(first.committed_epoch, Epoch(2));
4735 let second = cursor.next_visible_version(&control).unwrap().unwrap();
4736 assert_eq!(second.row_id, RowId(3));
4737 assert_eq!(second.committed_epoch, Epoch(1));
4738 let row = cursor.materialize(second, &control).unwrap();
4739 assert_eq!(row.columns.get(&1), Some(&Value::Int64(20)));
4740 assert!(cursor.next_visible_version(&control).unwrap().is_none());
4741 }
4742
4743 #[test]
4744 fn learned_index_was_stored() {
4745 let dir = tempdir().unwrap();
4746 let path = dir.path().join("r-2.sr");
4747 RunWriter::new(&schema(), 2, Epoch(1), 0)
4748 .write(&path, &rows())
4749 .unwrap();
4750 let header = read_header(&path).unwrap();
4751 assert!(
4752 header.index_trailer_offset != 0,
4753 "trailer should be present"
4754 );
4755 }
4756
4757 #[test]
4758 fn low_level_container_still_round_trips() {
4759 let dir = tempdir().unwrap();
4760 let path = dir.path().join("r-3.sr");
4761 let cols = vec![ColumnPayload {
4762 column_id: 1,
4763 type_id_tag: 8,
4764 encoding: Encoding::Plain,
4765 pages: vec![vec![1, 2, 3, 4]],
4766 page_stats: Vec::new(),
4767 }];
4768 let header = write_run(
4769 &path,
4770 &RunSpec {
4771 run_id: 1,
4772 schema_id: 1,
4773 epoch_created: 1,
4774 level: 0,
4775 flags: 0,
4776 sort_key_column_id: SORT_KEY_ROW_ID,
4777 row_count: 1,
4778 min_row_id: 0,
4779 max_row_id: 0,
4780 columns: &cols,
4781 },
4782 )
4783 .unwrap();
4784 let back = read_header(&path).unwrap();
4785 assert_eq!(back.content_hash, header.content_hash);
4786 assert_eq!(read_column_dir(&path, &back).unwrap().len(), 1);
4787 }
4788
4789 #[test]
4790 fn detects_corruption() {
4791 let dir = tempdir().unwrap();
4792 let path = dir.path().join("r-4.sr");
4793 write_run(
4794 &path,
4795 &RunSpec {
4796 run_id: 1,
4797 schema_id: 1,
4798 epoch_created: 1,
4799 level: 0,
4800 flags: 0,
4801 sort_key_column_id: SORT_KEY_ROW_ID,
4802 row_count: 1,
4803 min_row_id: 0,
4804 max_row_id: 0,
4805 columns: &[ColumnPayload {
4806 column_id: 1,
4807 type_id_tag: 8,
4808 encoding: Encoding::Plain,
4809 pages: vec![vec![1, 2, 3]],
4810 page_stats: Vec::new(),
4811 }],
4812 },
4813 )
4814 .unwrap();
4815 let mut bytes = std::fs::read(&path).unwrap();
4816 bytes[300] ^= 0xFF;
4817 std::fs::write(&path, bytes).unwrap();
4818 let err = read_header(&path).unwrap_err();
4819 assert!(
4820 matches!(err, MongrelError::ChecksumMismatch { .. }),
4821 "got {err:?}"
4822 );
4823 }
4824
4825 #[test]
4831 fn mmap_and_vec_writers_are_byte_identical() {
4832 let dir = tempdir().unwrap();
4833 let stats = vec![PageStat {
4834 first_row_id: 0,
4835 last_row_id: 1,
4836 null_count: 0,
4837 row_count: 2,
4838 min: Some(vec![0]),
4839 max: Some(vec![9]),
4840 offset: 0,
4841 compressed_len: 0,
4842 uncompressed_len: 0,
4843 }];
4844 let spec = RunSpec {
4845 run_id: 7,
4846 schema_id: 1,
4847 epoch_created: 10,
4848 level: 0,
4849 flags: 0,
4850 sort_key_column_id: SORT_KEY_ROW_ID,
4851 row_count: 2,
4852 min_row_id: 0,
4853 max_row_id: 1,
4854 columns: &[
4855 ColumnPayload {
4856 column_id: 1,
4857 type_id_tag: 8,
4858 encoding: Encoding::Plain,
4859 pages: vec![vec![1, 2, 3, 4], vec![5, 6, 7, 8]],
4860 page_stats: stats.clone(),
4861 },
4862 ColumnPayload {
4863 column_id: 2,
4864 type_id_tag: 8,
4865 encoding: Encoding::Plain,
4866 pages: vec![vec![10, 20], vec![30, 40]],
4867 page_stats: stats.clone(),
4868 },
4869 ],
4870 };
4871 let trailer = b"learned-trailer-bytes";
4872
4873 let plan = plan_run(&spec, None, Some(trailer)).expect("plan");
4875 let mut buf = vec![0u8; plan.total];
4876 let h_place = place_run(&spec, None, Some(trailer), &plan, &mut buf)
4877 .expect("place_run into Vec succeeds");
4878
4879 let path_vec = dir.path().join("vec.sr");
4881 let h_vec = write_run_vec(&path_vec, &spec, None, Some(trailer)).unwrap();
4882 let bv = std::fs::read(&path_vec).unwrap();
4883
4884 assert_eq!(h_place.content_hash, h_vec.content_hash);
4885 assert_eq!(h_place.footer_offset, h_vec.footer_offset);
4886 assert_eq!(
4887 buf, bv,
4888 "place_run and write_run_vec must be byte-identical"
4889 );
4890 assert!(read_header(&path_vec).is_ok());
4891
4892 let path_mmap = dir.path().join("mmap.sr");
4896 match write_run_mmap(&path_mmap, &spec, None, Some(trailer)) {
4897 Ok(h_mmap) => {
4898 let bm = std::fs::read(&path_mmap).unwrap();
4899 assert_eq!(bm, bv, "mmap run must be byte-identical to vec run");
4900 assert_eq!(h_mmap.content_hash, h_vec.content_hash);
4901 }
4902 Err(e) if is_mmap_unavailable(&e) => {
4903 eprintln!("note: file mmap unavailable here; vec path covered (1)/(2)");
4904 }
4905 Err(e) => panic!("unexpected mmap error: {e:?}"),
4906 }
4907 }
4908
4909 #[test]
4914 fn column_native_shared_matches_column_native() {
4915 let dir = tempdir().unwrap();
4916 let path = dir.path().join("r.sr");
4917 let n = 65_536 * 2 + 9;
4920 let id_col = NativeColumn::int64_sequence(1, n);
4921 let mut offsets = vec![0u32];
4922 let mut values = Vec::new();
4923 for i in 0..n {
4924 values.extend_from_slice(format!("v{}", i % 17).as_bytes());
4925 offsets.push(values.len() as u32);
4926 }
4927 let name_col = NativeColumn::Bytes {
4928 offsets,
4929 values,
4930 validity: vec![0xFF; n.div_ceil(8)],
4931 };
4932 let header = RunWriter::new(&schema(), 9, Epoch(5), 0)
4933 .write_native(&path, &[(1, id_col), (2, name_col)], n, 1)
4934 .unwrap();
4935
4936 let mut reader = RunReader::open_with_cache(&path, schema(), None, None, None, 0, None)
4937 .expect("open reader");
4938 assert!(reader.has_mmap(), "test env must support read-only mmap");
4939 assert_eq!(reader.row_count(), header.row_count as usize);
4940
4941 for cid in [1u16, 2] {
4942 let a = reader.column_native(cid).expect("column_native");
4943 let b = reader
4944 .column_native_shared(cid)
4945 .expect("column_native_shared");
4946 assert_eq!(a.len(), b.len(), "len mismatch col {cid}");
4947 match (&a, &b) {
4949 (
4950 NativeColumn::Int64 {
4951 data: da,
4952 validity: va,
4953 },
4954 NativeColumn::Int64 {
4955 data: db,
4956 validity: vb,
4957 },
4958 ) => {
4959 assert_eq!(da, db, "Int64 data col {cid}");
4960 assert_eq!(va, vb, "Int64 validity col {cid}");
4961 }
4962 (
4963 NativeColumn::Bytes {
4964 offsets: oa,
4965 values: ua,
4966 validity: va,
4967 },
4968 NativeColumn::Bytes {
4969 offsets: ob,
4970 values: ub,
4971 validity: vb,
4972 },
4973 ) => {
4974 assert_eq!(oa, ob, "Bytes offsets col {cid}");
4975 assert_eq!(ua, ub, "Bytes values col {cid}");
4976 assert_eq!(va, vb, "Bytes validity col {cid}");
4977 }
4978 _ => panic!("type mismatch col {cid}: {a:?} vs {b:?}"),
4979 }
4980 }
4981 }
4982
4983 #[test]
4984 fn page_cache_key_distinguishes_tables() {
4985 let a = page_cache_key(1, 5, 2, 3);
4986 let b = page_cache_key(2, 5, 2, 3);
4987 assert_ne!(a, b, "same run/col/page, different table must differ");
4988 assert_ne!(page_cache_key(1, 5, 2, 3), page_cache_key(1, 6, 2, 3));
4990 assert_ne!(page_cache_key(1, 5, 2, 3), page_cache_key(1, 5, 2, 4));
4991 }
4992}