1use crate::columnar;
19use crate::encryption::{setup_run_encryption, Cipher, Kek, RunEncryption};
20use crate::epoch::{Epoch, Snapshot};
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
148fn is_system_column_id(column_id: u16) -> bool {
150 matches!(
151 column_id,
152 SYS_ROW_ID | SYS_EPOCH | SYS_DELETED | SYS_COMMIT_TS
153 )
154}
155
156const LEARNED_EPSILON: usize = 64;
159
160fn build_learned_trailer(row_ids: &[Value]) -> Vec<u8> {
164 let points: Vec<(u64, usize)> = row_ids
165 .iter()
166 .enumerate()
167 .filter_map(|(i, v)| match v {
168 Value::Int64(r) => Some((*r as u64, i)),
169 _ => None,
170 })
171 .collect();
172 let pgm = PgmIndex::build(&points, LEARNED_EPSILON);
173 bincode::serialize(&pgm).expect("pgm serialize")
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct RunHeader {
178 pub magic: [u8; 8],
179 pub format_version: u16,
180 pub header_layout_version: u16,
181 pub run_id: u128,
182 pub content_hash: [u8; 32],
183 pub schema_id: u64,
184 pub epoch_created: u64,
185 pub level: u8,
186 pub flags: u8,
187 pub sort_key_column_id: u16,
188 pub row_count: u64,
189 pub min_row_id: u64,
190 pub max_row_id: u64,
191 pub column_count: u64,
192 pub column_dir_offset: u64,
193 pub index_trailer_offset: u64,
194 pub encryption_descriptor_offset: u64,
195 pub footer_offset: u64,
196 pub encrypted_stats_offset: u64,
200 pub encrypted_stats_len: u64,
201}
202
203impl RunHeader {
204 pub fn is_encrypted(&self) -> bool {
205 self.flags & RUN_FLAG_ENCRYPTED != 0
206 }
207 pub fn is_clean(&self) -> bool {
208 self.flags & RUN_FLAG_CLEAN != 0
209 }
210 pub fn is_uniform_epoch(&self) -> bool {
211 self.flags & RUN_FLAG_UNIFORM_EPOCH != 0
212 }
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct ColumnPageHeader {
217 pub column_id: u16,
218 pub type_id_tag: u16,
219 pub encoding: u8,
220 pub flags: u8,
221 pub page_count: u32,
222 pub page_region_offset: u64,
223 pub page_region_len: u64,
224 pub page_stats: Vec<PageStat>,
225}
226
227impl ColumnPageHeader {
228 const PAGE_ENCRYPTED: u8 = 1 << 0;
229}
230
231const RUN_MAC_LEN: usize = 32;
234
235fn compute_run_mac(
239 enc: Option<&RunEncryption>,
240 header_bytes: &[u8],
241 dir_bytes: &[u8],
242) -> Option<[u8; RUN_MAC_LEN]> {
243 {
244 if let Some(e) = enc {
245 if let Some(mac_key) = &e.mac_key {
246 return Some(crate::encryption::run_metadata_mac(
247 mac_key,
248 header_bytes,
249 dir_bytes,
250 &e.descriptor_bytes,
251 ));
252 }
253 }
254 }
255 None
256}
257
258pub struct ColumnPayload {
260 pub column_id: u16,
261 pub type_id_tag: u16,
262 pub encoding: Encoding,
263 pub pages: Vec<Vec<u8>>,
264 pub page_stats: Vec<PageStat>,
268}
269
270pub struct RunSpec<'a> {
272 pub run_id: u128,
273 pub schema_id: u64,
274 pub epoch_created: u64,
275 pub level: u8,
276 pub flags: u8,
277 pub sort_key_column_id: u16,
278 pub row_count: u64,
279 pub min_row_id: u64,
280 pub max_row_id: u64,
281 pub columns: &'a [ColumnPayload],
282}
283
284pub fn write_run(path: impl AsRef<Path>, spec: &RunSpec) -> Result<RunHeader> {
286 write_run_with(path, spec, None, &[], None)
287}
288
289pub fn write_run_with(
299 path: impl AsRef<Path>,
300 spec: &RunSpec,
301 kek: Option<&Kek>,
302 indexable_columns: &[(u16, u8)],
303 index_trailer: Option<&[u8]>,
304) -> Result<RunHeader> {
305 let enc: Option<RunEncryption> = match kek {
309 Some(k) => Some(setup_run_encryption(k, indexable_columns)?),
310 None => None,
311 };
312 match write_run_mmap(path.as_ref(), spec, enc.as_ref(), index_trailer) {
313 Ok(h) => Ok(h),
314 Err(e) if is_mmap_unavailable(&e) => write_run_vec(path, spec, enc, index_trailer),
315 Err(e) => Err(e),
316 }
317}
318
319fn write_run_with_file(
320 mut file: File,
321 spec: &RunSpec,
322 kek: Option<&Kek>,
323 indexable_columns: &[(u16, u8)],
324 index_trailer: Option<&[u8]>,
325) -> Result<RunHeader> {
326 let enc = match kek {
327 Some(key) => Some(setup_run_encryption(key, indexable_columns)?),
328 None => None,
329 };
330 match write_run_mmap_file(&file, spec, enc.as_ref(), index_trailer) {
331 Ok(header) => Ok(header),
332 Err(error) if is_mmap_unavailable(&error) => {
333 file.set_len(0)?;
334 file.seek(SeekFrom::Start(0))?;
335 let (bytes, header) = encode_run_vec(spec, enc, index_trailer)?;
336 file.write_all(&bytes)?;
337 file.sync_all()?;
338 Ok(header)
339 }
340 Err(error) => Err(error),
341 }
342}
343
344fn is_mmap_unavailable(e: &MongrelError) -> bool {
348 matches!(e, MongrelError::InvalidArgument(m) if m.starts_with("__mmap_unavailable__:"))
349}
350
351fn write_run_mmap(
372 path: &Path,
373 spec: &RunSpec,
374 enc: Option<&RunEncryption>,
375 index_trailer: Option<&[u8]>,
376) -> Result<RunHeader> {
377 let file = OpenOptions::new().create_new(true).write(true).open(path)?;
378 let result = write_run_mmap_file(&file, spec, enc, index_trailer);
379 if result.as_ref().is_err_and(is_mmap_unavailable) {
380 drop(file);
381 let _ = std::fs::remove_file(path);
382 }
383 result
384}
385
386fn write_run_mmap_file(
387 file: &File,
388 spec: &RunSpec,
389 enc: Option<&RunEncryption>,
390 index_trailer: Option<&[u8]>,
391) -> Result<RunHeader> {
392 let plan = plan_run(spec, enc, index_trailer)?;
393 file.set_len(plan.total as u64)?;
394 let mut mmap = match unsafe { memmap2::MmapMut::map_mut(file) } {
395 Ok(m) => m,
396 Err(e) => {
397 return Err(MongrelError::InvalidArgument(format!(
398 "__mmap_unavailable__: {e}"
399 )));
400 }
401 };
402 let header = place_run(spec, enc, index_trailer, &plan, &mut mmap[..])?;
403 mmap.flush()?;
404 file.sync_all()?;
405 Ok(header)
406}
407
408struct RunPlan {
414 jobs: Vec<(usize, usize, u64, usize)>, dir_bytes: Vec<u8>,
416 encrypted: bool,
417 column_dir_offset: u64,
418 index_trailer_offset: u64,
419 encryption_descriptor_offset: u64,
420 footer_offset: u64,
421 total: usize,
422 encrypted_stats_plain: Option<Vec<u8>>,
426 encrypted_stats_offset: u64,
427 encrypted_stats_len: u64,
428}
429
430fn plan_run(
434 spec: &RunSpec,
435 enc: Option<&RunEncryption>,
436 index_trailer: Option<&[u8]>,
437) -> Result<RunPlan> {
438 let encrypted = enc.is_some();
439 let columns = spec.columns;
440 let mut jobs: Vec<(usize, usize, u64, usize)> = Vec::new();
441 let mut dir: Vec<ColumnPageHeader> = Vec::with_capacity(columns.len());
442 let mut enc_stats: EncryptedColumnStats = Vec::new();
443 let mut cursor: u64 = RUN_HEADER_PAD as u64;
444 for (ci, col) in columns.iter().enumerate() {
445 let region_offset = cursor;
446 let mut region_len = 0u64;
447 let mut stats = Vec::with_capacity(col.pages.len());
448 let mut col_minmax: Vec<PageMinMax> = Vec::new();
449 for (ps, page) in col.pages.iter().enumerate() {
450 if encrypted && ps >= u16::MAX as usize {
455 return Err(MongrelError::Full(format!(
456 "column {:#x} exceeds 65534 pages; encrypted-run page-seq nonce space exhausted",
457 col.column_id
458 )));
459 }
460 let odl = on_disk_len(page.len(), encrypted);
461 jobs.push((ci, ps, cursor, odl));
462 let mut stat = col.page_stats.get(ps).cloned().unwrap_or(PageStat {
463 first_row_id: 0,
464 last_row_id: 0,
465 null_count: 0,
466 row_count: 0,
467 min: None,
468 max: None,
469 offset: 0,
470 compressed_len: 0,
471 uncompressed_len: 0,
472 });
473 stat.offset = cursor;
474 stat.compressed_len = odl as u32;
475 stat.uncompressed_len = page.len() as u32;
476 if encrypted {
483 col_minmax.push((stat.min.take(), stat.max.take()));
484 }
485 stats.push(stat);
486 cursor += odl as u64;
487 region_len += odl as u64;
488 }
489 if col_minmax
490 .iter()
491 .any(|(mn, mx)| mn.is_some() || mx.is_some())
492 {
493 enc_stats.push((col.column_id, col_minmax));
494 }
495 let page_flags = if encrypted {
496 ColumnPageHeader::PAGE_ENCRYPTED
497 } else {
498 0
499 };
500 dir.push(ColumnPageHeader {
501 column_id: col.column_id,
502 type_id_tag: col.type_id_tag,
503 encoding: col.encoding as u8,
504 flags: page_flags,
505 page_count: col.pages.len() as u32,
506 page_region_offset: region_offset,
507 page_region_len: region_len,
508 page_stats: stats,
509 });
510 }
511 let dir_bytes = bincode::serialize(&dir)?;
512 let column_dir_offset = cursor;
513 cursor += dir_bytes.len() as u64;
514 let index_trailer_offset = match index_trailer {
515 Some(t) => {
516 let off = cursor;
517 cursor += t.len() as u64;
518 off
519 }
520 None => 0,
521 };
522 let encryption_descriptor_offset = match enc {
523 Some(e) => {
524 let off = cursor;
525 cursor += 4 + e.descriptor_bytes.len() as u64;
526 off
527 }
528 None => 0,
529 };
530 let (encrypted_stats_plain, encrypted_stats_offset, encrypted_stats_len) =
531 if encrypted && !enc_stats.is_empty() {
532 let plain = bincode::serialize(&enc_stats)?;
533 let ct_len = on_disk_len(plain.len(), true) as u64;
534 let off = cursor;
535 cursor += ct_len;
536 (Some(plain), off, ct_len)
537 } else {
538 (None, 0, 0)
539 };
540 let footer_offset = cursor;
541 let total = footer_offset as usize + 8 + 8 + 32 + if encrypted { RUN_MAC_LEN } else { 0 };
544 Ok(RunPlan {
545 jobs,
546 dir_bytes,
547 encrypted,
548 column_dir_offset,
549 index_trailer_offset,
550 encryption_descriptor_offset,
551 footer_offset,
552 total,
553 encrypted_stats_plain,
554 encrypted_stats_offset,
555 encrypted_stats_len,
556 })
557}
558
559fn place_run(
566 spec: &RunSpec,
567 enc: Option<&RunEncryption>,
568 index_trailer: Option<&[u8]>,
569 plan: &RunPlan,
570 buf: &mut [u8],
571) -> Result<RunHeader> {
572 use rayon::prelude::*;
573 use std::borrow::Cow;
574 debug_assert_eq!(
575 buf.len(),
576 plan.total,
577 "place_run: buffer must be exactly plan.total bytes"
578 );
579
580 let cipher: Option<&dyn Cipher> = enc.map(|e| e.cipher.as_ref());
581 let nonce_prefix = enc.map(|e| e.nonce_prefix);
582 let columns = spec.columns;
583
584 struct SyncPtr(*mut u8);
589 unsafe impl Send for SyncPtr {}
590 unsafe impl Sync for SyncPtr {}
591 impl SyncPtr {
592 fn get(&self) -> *mut u8 {
593 self.0
594 }
595 }
596 let base = SyncPtr(buf.as_mut_ptr());
597 plan.jobs
598 .par_iter()
599 .map(
600 |&(ci, ps, offset, odl): &(usize, usize, u64, usize)| -> Result<()> {
601 let page = &columns[ci].pages[ps];
602 let dst =
603 unsafe { std::slice::from_raw_parts_mut(base.get().add(offset as usize), odl) };
604 let bytes: Cow<[u8]> = match cipher {
605 Some(c) => {
606 let nonce =
607 page_nonce(nonce_prefix.unwrap(), columns[ci].column_id, ps as u32);
608 let ct = c.encrypt_page(&nonce, page)?;
609 assert_eq!(
612 ct.len(), odl,
613 "ciphertext length {} != predicted {}; GCM tag size assumption is stale",
614 ct.len(), odl
615 );
616 Cow::Owned(ct)
617 }
618 None => {
619 debug_assert_eq!(page.len(), odl);
620 Cow::Borrowed(page.as_slice())
621 }
622 };
623 dst.copy_from_slice(&bytes);
624 Ok(())
625 },
626 )
627 .collect::<Result<()>>()?;
628
629 let content_hash = {
631 let mut h = Sha256::new();
632 h.update(&buf[RUN_HEADER_PAD..plan.column_dir_offset as usize]);
633 h.finalize()
634 };
635
636 let doff = plan.column_dir_offset as usize;
638 buf[doff..doff + plan.dir_bytes.len()].copy_from_slice(&plan.dir_bytes);
639 if let Some(t) = index_trailer {
640 let off = plan.index_trailer_offset as usize;
641 buf[off..off + t.len()].copy_from_slice(t);
642 }
643 if let Some(e) = enc {
644 let off = plan.encryption_descriptor_offset as usize;
645 buf[off..off + 4].copy_from_slice(&(e.descriptor_bytes.len() as u32).to_le_bytes());
646 buf[off + 4..off + 4 + e.descriptor_bytes.len()].copy_from_slice(&e.descriptor_bytes);
647 }
648 if let (Some(e), Some(plain)) = (enc, plan.encrypted_stats_plain.as_ref()) {
649 let nonce = page_nonce(e.nonce_prefix, ENC_STATS_NONCE_COLUMN, ENC_STATS_NONCE_SEQ);
650 let ct = e.cipher.encrypt_page(&nonce, plain)?;
651 debug_assert_eq!(ct.len() as u64, plan.encrypted_stats_len);
652 let off = plan.encrypted_stats_offset as usize;
653 buf[off..off + ct.len()].copy_from_slice(&ct);
654 }
655
656 let header_flags = if plan.encrypted {
658 spec.flags | RUN_FLAG_ENCRYPTED
659 } else {
660 spec.flags
661 };
662 let header = RunHeader {
663 magic: RUN_MAGIC,
664 format_version: RUN_FORMAT_VERSION,
665 header_layout_version: RUN_HEADER_VERSION,
666 run_id: spec.run_id,
667 content_hash: content_hash.into(),
668 schema_id: spec.schema_id,
669 epoch_created: spec.epoch_created,
670 level: spec.level,
671 flags: header_flags,
672 sort_key_column_id: spec.sort_key_column_id,
673 row_count: spec.row_count,
674 min_row_id: spec.min_row_id,
675 max_row_id: spec.max_row_id,
676 column_count: columns.len() as u64,
677 column_dir_offset: plan.column_dir_offset,
678 index_trailer_offset: plan.index_trailer_offset,
679 encryption_descriptor_offset: plan.encryption_descriptor_offset,
680 footer_offset: plan.footer_offset,
681 encrypted_stats_offset: plan.encrypted_stats_offset,
682 encrypted_stats_len: plan.encrypted_stats_len,
683 };
684 let header_bytes = bincode::serialize(&header)?;
685 if header_bytes.len() > RUN_HEADER_PAD {
686 return Err(MongrelError::InvalidArgument(format!(
687 "run header too large: {} > {RUN_HEADER_PAD}",
688 header_bytes.len()
689 )));
690 }
691 buf[..header_bytes.len()].copy_from_slice(&header_bytes);
692
693 let checksum = Sha256::digest(&buf[..plan.footer_offset as usize]);
694 let foot = plan.footer_offset as usize;
695 buf[foot..foot + 8].copy_from_slice(&RUN_MAGIC);
696 buf[foot + 8..foot + 16].copy_from_slice(&plan.footer_offset.to_le_bytes());
697 buf[foot + 16..foot + 48].copy_from_slice(&checksum);
698 if let Some(tag) = compute_run_mac(enc, &header_bytes, &plan.dir_bytes) {
700 buf[foot + 48..foot + 48 + RUN_MAC_LEN].copy_from_slice(&tag);
701 }
702 Ok(header)
703}
704
705fn write_run_vec(
708 path: impl AsRef<Path>,
709 spec: &RunSpec,
710 enc: Option<RunEncryption>,
711 index_trailer: Option<&[u8]>,
712) -> Result<RunHeader> {
713 let (buf, header) = encode_run_vec(spec, enc, index_trailer)?;
714 let mut file = OpenOptions::new().create_new(true).write(true).open(path)?;
715 file.write_all(&buf)?;
716 file.sync_all()?;
717 Ok(header)
718}
719
720fn encode_run_vec(
721 spec: &RunSpec,
722 enc: Option<RunEncryption>,
723 index_trailer: Option<&[u8]>,
724) -> Result<(Vec<u8>, RunHeader)> {
725 let mut buf: Vec<u8> = vec![0; RUN_HEADER_PAD]; let mut content_hasher = Sha256::new();
727 let mut dir: Vec<ColumnPageHeader> = Vec::with_capacity(spec.columns.len());
728 let mut enc_stats: EncryptedColumnStats = Vec::new();
729
730 for col in spec.columns {
731 let region_offset = buf.len() as u64;
732 let mut region_len = 0u64;
733 let mut stats = Vec::with_capacity(col.pages.len());
734 let mut col_minmax: Vec<PageMinMax> = Vec::new();
735 for (page_seq, page) in col.pages.iter().enumerate() {
736 if enc.is_some() && page_seq >= u16::MAX as usize {
737 return Err(MongrelError::Full(format!(
738 "column {:#x} exceeds 65534 pages; encrypted-run page-seq nonce space exhausted",
739 col.column_id
740 )));
741 }
742 let on_disk: Vec<u8> = match &enc {
743 Some(e) => e.cipher.encrypt_page(
744 &page_nonce(e.nonce_prefix, col.column_id, page_seq as u32),
745 page,
746 )?,
747 None => page.clone(),
748 };
749 let offset = buf.len() as u64;
750 buf.write_all(&on_disk)?;
751 content_hasher.update(&on_disk);
752 region_len += on_disk.len() as u64;
753 let mut stat = if let Some(s) = col.page_stats.get(page_seq) {
754 s.clone()
755 } else {
756 PageStat {
757 first_row_id: 0,
758 last_row_id: 0,
759 null_count: 0,
760 row_count: 0,
761 min: None,
762 max: None,
763 offset: 0,
764 compressed_len: 0,
765 uncompressed_len: 0,
766 }
767 };
768 stat.offset = offset;
769 stat.compressed_len = on_disk.len() as u32;
770 stat.uncompressed_len = page.len() as u32;
771 if enc.is_some() {
776 col_minmax.push((stat.min.take(), stat.max.take()));
777 }
778 stats.push(stat);
779 }
780 if col_minmax
781 .iter()
782 .any(|(mn, mx)| mn.is_some() || mx.is_some())
783 {
784 enc_stats.push((col.column_id, col_minmax));
785 }
786 let page_flags = if enc.is_some() {
787 ColumnPageHeader::PAGE_ENCRYPTED
788 } else {
789 0
790 };
791 dir.push(ColumnPageHeader {
792 column_id: col.column_id,
793 type_id_tag: col.type_id_tag,
794 encoding: col.encoding as u8,
795 flags: page_flags,
796 page_count: col.pages.len() as u32,
797 page_region_offset: region_offset,
798 page_region_len: region_len,
799 page_stats: stats,
800 });
801 }
802
803 let column_dir_offset = buf.len() as u64;
804 let dir_bytes = bincode::serialize(&dir)?;
805 buf.write_all(&dir_bytes)?;
806
807 let index_trailer_offset = match index_trailer {
808 Some(trailer) => {
809 let off = buf.len() as u64;
810 buf.write_all(trailer)?;
811 off
812 }
813 None => 0,
814 };
815 let encryption_descriptor_offset = match &enc {
816 Some(e) => {
817 let off = buf.len() as u64;
818 buf.write_all(&(e.descriptor_bytes.len() as u32).to_le_bytes())?;
819 buf.write_all(&e.descriptor_bytes)?;
820 off
821 }
822 None => 0,
823 };
824 let (encrypted_stats_offset, encrypted_stats_len) = match &enc {
825 Some(e) if !enc_stats.is_empty() => {
826 let plain = bincode::serialize(&enc_stats)?;
827 let nonce = page_nonce(e.nonce_prefix, ENC_STATS_NONCE_COLUMN, ENC_STATS_NONCE_SEQ);
828 let ct = e.cipher.encrypt_page(&nonce, &plain)?;
829 let off = buf.len() as u64;
830 let len = ct.len() as u64;
831 buf.write_all(&ct)?;
832 (off, len)
833 }
834 _ => (0, 0),
835 };
836 let footer_offset = buf.len() as u64;
837
838 let header_flags = if enc.is_some() {
839 spec.flags | RUN_FLAG_ENCRYPTED
840 } else {
841 spec.flags
842 };
843 let header = RunHeader {
844 magic: RUN_MAGIC,
845 format_version: RUN_FORMAT_VERSION,
846 header_layout_version: RUN_HEADER_VERSION,
847 run_id: spec.run_id,
848 content_hash: content_hasher.finalize().into(),
849 schema_id: spec.schema_id,
850 epoch_created: spec.epoch_created,
851 level: spec.level,
852 flags: header_flags,
853 sort_key_column_id: spec.sort_key_column_id,
854 row_count: spec.row_count,
855 min_row_id: spec.min_row_id,
856 max_row_id: spec.max_row_id,
857 column_count: spec.columns.len() as u64,
858 column_dir_offset,
859 index_trailer_offset,
860 encryption_descriptor_offset,
861 footer_offset,
862 encrypted_stats_offset,
863 encrypted_stats_len,
864 };
865 let header_bytes = bincode::serialize(&header)?;
866 if header_bytes.len() > RUN_HEADER_PAD {
867 return Err(MongrelError::InvalidArgument(format!(
868 "run header too large: {} > {RUN_HEADER_PAD}",
869 header_bytes.len()
870 )));
871 }
872 buf[..header_bytes.len()].copy_from_slice(&header_bytes);
873
874 let checksum = Sha256::digest(&buf[..footer_offset as usize]);
875 buf.write_all(&RUN_MAGIC)?;
876 buf.write_all(&footer_offset.to_le_bytes())?;
877 buf.write_all(&checksum)?;
878 if let Some(tag) = compute_run_mac(enc.as_ref(), &header_bytes, &dir_bytes) {
881 buf.write_all(&tag)?;
882 }
883
884 Ok((buf, header))
885}
886
887fn page_nonce(nonce_prefix: [u8; 12], column_id: u16, page_seq: u32) -> [u8; 12] {
888 let mut n = nonce_prefix;
889 n[8..10].copy_from_slice(&column_id.to_le_bytes());
890 n[10..12].copy_from_slice(&(page_seq as u16).to_le_bytes());
891 n
892}
893
894fn overlay_encrypted_stats(
901 file: &mut File,
902 header: &RunHeader,
903 cipher: &dyn Cipher,
904 nonce_prefix: [u8; 12],
905 dir: &mut [ColumnPageHeader],
906) -> Result<()> {
907 file.seek(SeekFrom::Start(header.encrypted_stats_offset))?;
908 const MAX_ENCRYPTED_STATS_BYTES: u64 = 64 * 1024 * 1024;
909 if header.encrypted_stats_len > MAX_ENCRYPTED_STATS_BYTES {
910 return Err(MongrelError::InvalidArgument(format!(
911 "encrypted run stats length {} exceeds {MAX_ENCRYPTED_STATS_BYTES}",
912 header.encrypted_stats_len
913 )));
914 }
915 let mut ct = vec![0u8; header.encrypted_stats_len as usize];
916 file.read_exact(&mut ct)?;
917 let nonce = page_nonce(nonce_prefix, ENC_STATS_NONCE_COLUMN, ENC_STATS_NONCE_SEQ);
918 let plain = cipher.decrypt_page(&nonce, &ct)?;
919 let stats: EncryptedColumnStats = bincode::deserialize(&plain)
920 .map_err(|e| MongrelError::Encryption(format!("bad encrypted page-stats envelope: {e}")))?;
921 for (cid, minmax) in stats {
922 let Some(col) = dir.iter_mut().find(|c| c.column_id == cid) else {
923 continue;
924 };
925 for (stat, (mn, mx)) in col.page_stats.iter_mut().zip(minmax) {
926 stat.min = mn;
927 stat.max = mx;
928 }
929 }
930 Ok(())
931}
932
933pub(crate) fn page_cache_key(
940 table_id: u64,
941 run_id: u128,
942 column_id: u16,
943 page_seq: usize,
944) -> [u8; 32] {
945 let mut h = Sha256::new();
946 h.update(table_id.to_be_bytes());
947 h.update(run_id.to_be_bytes());
948 h.update(column_id.to_be_bytes());
949 h.update((page_seq as u64).to_be_bytes());
950 let out = h.finalize();
951 let mut k = [0u8; 32];
952 k.copy_from_slice(&out);
953 k
954}
955
956fn decrypt_or_passthrough(
960 cipher: Option<&dyn Cipher>,
961 nonce_prefix: [u8; 12],
962 column_id: u16,
963 page_seq: usize,
964 encrypted: bool,
965 buf: &[u8],
966) -> Result<Vec<u8>> {
967 if encrypted {
968 match cipher {
969 Some(c) => {
970 Ok(c.decrypt_page(&page_nonce(nonce_prefix, column_id, page_seq as u32), buf)?)
971 }
972 None => Err(MongrelError::Decryption(
973 "encrypted page but no cipher".into(),
974 )),
975 }
976 } else {
977 Ok(buf.to_vec())
978 }
979}
980
981fn read_header_fast_from_file(file: &mut File) -> Result<RunHeader> {
991 file.seek(SeekFrom::Start(0))?;
992 let mut header_buf = [0u8; RUN_HEADER_PAD];
993 file.read_exact(&mut header_buf)?;
994 let header: RunHeader = bincode::deserialize(&header_buf)
995 .map_err(|e| MongrelError::InvalidArgument(format!("bad run header: {e}")))?;
996 validate_run_header_bytes(&header, &header_buf)?;
997 validate_run_layout(&header, file.metadata()?.len())?;
998 file.seek(SeekFrom::Start(header.footer_offset))?;
999 let mut footer_magic = [0u8; 8];
1000 file.read_exact(&mut footer_magic)?;
1001 if footer_magic != RUN_MAGIC {
1002 return Err(MongrelError::MagicMismatch {
1003 what: "sorted run footer",
1004 expected: RUN_MAGIC,
1005 got: footer_magic,
1006 });
1007 }
1008 Ok(header)
1009}
1010
1011pub fn read_header(path: impl AsRef<Path>) -> Result<RunHeader> {
1013 let mut file = crate::durable_file::open_regular_nofollow(path.as_ref())?;
1014 read_header_from_file(&mut file)
1015}
1016
1017fn validate_run_layout(header: &RunHeader, file_len: u64) -> Result<()> {
1018 let footer_len = 8u64
1019 + 8
1020 + 32
1021 + if header.is_encrypted() {
1022 RUN_MAC_LEN as u64
1023 } else {
1024 0
1025 };
1026 let expected_len = header
1027 .footer_offset
1028 .checked_add(footer_len)
1029 .ok_or_else(|| MongrelError::InvalidArgument("sorted run length overflow".into()))?;
1030 if header.footer_offset < RUN_HEADER_PAD as u64 || expected_len != file_len {
1031 return Err(MongrelError::InvalidArgument(format!(
1032 "invalid sorted run layout: footer={} file={file_len}",
1033 header.footer_offset
1034 )));
1035 }
1036 let in_body = |offset: u64| {
1037 offset == 0 || (offset >= RUN_HEADER_PAD as u64 && offset <= header.footer_offset)
1038 };
1039 if header.column_dir_offset < RUN_HEADER_PAD as u64
1040 || header.column_dir_offset > header.footer_offset
1041 || !in_body(header.index_trailer_offset)
1042 || !in_body(header.encryption_descriptor_offset)
1043 || !in_body(header.encrypted_stats_offset)
1044 || header
1045 .encrypted_stats_offset
1046 .checked_add(header.encrypted_stats_len)
1047 .is_none_or(|end| end > header.footer_offset)
1048 {
1049 return Err(MongrelError::InvalidArgument(
1050 "sorted run metadata offsets are outside the file".into(),
1051 ));
1052 }
1053 Ok(())
1054}
1055
1056fn validate_run_header_bytes(header: &RunHeader, bytes: &[u8; RUN_HEADER_PAD]) -> Result<()> {
1057 if header.magic != RUN_MAGIC {
1058 return Err(MongrelError::MagicMismatch {
1059 what: "sorted run",
1060 expected: RUN_MAGIC,
1061 got: header.magic,
1062 });
1063 }
1064 const KNOWN_FLAGS: u8 =
1065 RUN_FLAG_ENCRYPTED | RUN_FLAG_TOMBSTONE_ONLY | RUN_FLAG_CLEAN | RUN_FLAG_UNIFORM_EPOCH;
1066 if header.format_version != RUN_FORMAT_VERSION
1067 || header.header_layout_version != RUN_HEADER_VERSION
1068 || header.flags & !KNOWN_FLAGS != 0
1069 || (header.row_count == 0 && (header.min_row_id != 0 || header.max_row_id != 0))
1070 || (header.row_count != 0 && header.min_row_id > header.max_row_id)
1071 {
1072 return Err(MongrelError::InvalidArgument(
1073 "unsupported or invalid sorted run header".into(),
1074 ));
1075 }
1076 let canonical = bincode::serialize(header)?;
1077 if canonical.len() > RUN_HEADER_PAD
1078 || bytes[..canonical.len()] != canonical
1079 || bytes[canonical.len()..].iter().any(|byte| *byte != 0)
1080 {
1081 return Err(MongrelError::InvalidArgument(
1082 "sorted run header has noncanonical bytes or nonzero padding".into(),
1083 ));
1084 }
1085 Ok(())
1086}
1087
1088fn read_header_from_file(file: &mut File) -> Result<RunHeader> {
1089 file.seek(SeekFrom::Start(0))?;
1090 let mut header_buf = [0u8; RUN_HEADER_PAD];
1091 file.read_exact(&mut header_buf)?;
1092 let header: RunHeader = bincode::deserialize(&header_buf)
1093 .map_err(|e| MongrelError::InvalidArgument(format!("bad run header: {e}")))?;
1094 validate_run_header_bytes(&header, &header_buf)?;
1095
1096 validate_run_layout(&header, file.metadata()?.len())?;
1097 file.seek(SeekFrom::Start(header.footer_offset))?;
1098 let mut footer = [0u8; 8 + 8 + 32];
1099 file.read_exact(&mut footer)?;
1100 if footer[..8] != RUN_MAGIC {
1101 return Err(MongrelError::MagicMismatch {
1102 what: "sorted run footer",
1103 expected: RUN_MAGIC,
1104 got: footer[..8].try_into().unwrap(),
1105 });
1106 }
1107 let mut hasher = Sha256::new();
1108 file.seek(SeekFrom::Start(0))?;
1109 let mut remaining = header.footer_offset;
1110 let mut buffer = [0u8; 64 * 1024];
1111 while remaining != 0 {
1112 let length = usize::try_from(remaining.min(buffer.len() as u64)).unwrap();
1113 file.read_exact(&mut buffer[..length])?;
1114 hasher.update(&buffer[..length]);
1115 remaining -= length as u64;
1116 }
1117 let computed: [u8; 32] = hasher.finalize().into();
1118 let stored: [u8; 32] = footer[16..].try_into().unwrap();
1119 if computed != stored {
1120 return Err(MongrelError::ChecksumMismatch {
1121 expected: u64::from_be_bytes(stored[..8].try_into().unwrap()),
1122 actual: u64::from_be_bytes(computed[..8].try_into().unwrap()),
1123 context: "sorted run footer".into(),
1124 });
1125 }
1126 file.seek(SeekFrom::Start(RUN_HEADER_PAD as u64))?;
1127 let mut remaining = header.column_dir_offset - RUN_HEADER_PAD as u64;
1128 let mut content = Sha256::new();
1129 while remaining != 0 {
1130 let length = usize::try_from(remaining.min(buffer.len() as u64)).unwrap();
1131 file.read_exact(&mut buffer[..length])?;
1132 content.update(&buffer[..length]);
1133 remaining -= length as u64;
1134 }
1135 let content_hash: [u8; 32] = content.finalize().into();
1136 if content_hash != header.content_hash {
1137 return Err(MongrelError::ChecksumMismatch {
1138 expected: u64::from_be_bytes(header.content_hash[..8].try_into().unwrap()),
1139 actual: u64::from_be_bytes(content_hash[..8].try_into().unwrap()),
1140 context: "sorted run content hash".into(),
1141 });
1142 }
1143 Ok(header)
1144}
1145
1146pub fn read_column_dir(
1148 path: impl AsRef<Path>,
1149 header: &RunHeader,
1150) -> Result<Vec<ColumnPageHeader>> {
1151 let mut file = crate::durable_file::open_regular_nofollow(path.as_ref())?;
1152 read_column_dir_from_file(&mut file, header)
1153}
1154
1155fn read_column_dir_from_file(file: &mut File, header: &RunHeader) -> Result<Vec<ColumnPageHeader>> {
1156 file.seek(SeekFrom::Start(header.column_dir_offset))?;
1157 let end = [
1158 header.index_trailer_offset,
1159 header.encryption_descriptor_offset,
1160 header.encrypted_stats_offset,
1161 header.footer_offset,
1162 ]
1163 .into_iter()
1164 .filter(|offset| *offset > header.column_dir_offset)
1165 .min()
1166 .ok_or_else(|| {
1167 MongrelError::InvalidArgument("sorted run column directory has no end".into())
1168 })?;
1169 let len = end.checked_sub(header.column_dir_offset).ok_or_else(|| {
1170 MongrelError::InvalidArgument("sorted run column directory offsets are reversed".into())
1171 })?;
1172 const MAX_COLUMN_DIR_BYTES: u64 = 64 * 1024 * 1024;
1173 if len > MAX_COLUMN_DIR_BYTES {
1174 return Err(MongrelError::InvalidArgument(format!(
1175 "sorted run column directory length {len} exceeds {MAX_COLUMN_DIR_BYTES}"
1176 )));
1177 }
1178 let mut buf = vec![0u8; len as usize];
1179 file.read_exact(&mut buf)?;
1180 let dir: Vec<ColumnPageHeader> = bincode::deserialize(&buf)
1181 .map_err(|e| MongrelError::InvalidArgument(format!("bad column dir: {e}")))?;
1182 Ok(dir)
1183}
1184
1185fn validate_column_directory_layout(header: &RunHeader, dir: &[ColumnPageHeader]) -> Result<()> {
1186 if header.column_count != dir.len() as u64 || dir.len() > u16::MAX as usize {
1187 return Err(MongrelError::InvalidArgument(
1188 "sorted run column count is invalid".into(),
1189 ));
1190 }
1191 let mut regions = Vec::with_capacity(dir.len());
1192 let mut ids = std::collections::HashSet::new();
1193 for column in dir {
1194 if !ids.insert(column.column_id)
1195 || column.flags & !ColumnPageHeader::PAGE_ENCRYPTED != 0
1196 || column.page_count as usize != column.page_stats.len()
1197 {
1198 return Err(MongrelError::InvalidArgument(
1199 "sorted run column directory identity is invalid".into(),
1200 ));
1201 }
1202 let region_end = column
1203 .page_region_offset
1204 .checked_add(column.page_region_len)
1205 .ok_or_else(|| MongrelError::InvalidArgument("run page region overflows".into()))?;
1206 if column.page_region_offset < RUN_HEADER_PAD as u64
1207 || region_end > header.column_dir_offset
1208 {
1209 return Err(MongrelError::InvalidArgument(
1210 "sorted run page region is outside its body".into(),
1211 ));
1212 }
1213 let mut cursor = column.page_region_offset;
1214 let mut rows = 0_u64;
1215 for stat in &column.page_stats {
1216 let length = stat.compressed_len as u64;
1217 let end = stat
1218 .offset
1219 .checked_add(length)
1220 .ok_or_else(|| MongrelError::InvalidArgument("run page offset overflows".into()))?;
1221 if stat.offset != cursor
1222 || length == 0
1223 || length > MAX_RUN_PAGE_BYTES
1224 || stat.uncompressed_len as u64 > MAX_RUN_PAGE_BYTES
1225 || stat.row_count as usize > PAGE_ROWS
1226 || end > region_end
1227 {
1228 return Err(MongrelError::InvalidArgument(
1229 "sorted run page metadata is outside its region".into(),
1230 ));
1231 }
1232 rows = rows.checked_add(stat.row_count as u64).ok_or_else(|| {
1233 MongrelError::InvalidArgument("sorted run row count overflows".into())
1234 })?;
1235 cursor = end;
1236 }
1237 if cursor != region_end || rows != header.row_count {
1238 return Err(MongrelError::InvalidArgument(
1239 "sorted run page region length or row count is inconsistent".into(),
1240 ));
1241 }
1242 regions.push((column.page_region_offset, region_end));
1243 }
1244 regions.sort_unstable();
1245 if regions.windows(2).any(|pair| pair[0].1 > pair[1].0) {
1246 return Err(MongrelError::InvalidArgument(
1247 "sorted run page regions overlap".into(),
1248 ));
1249 }
1250 Ok(())
1251}
1252
1253fn read_encryption_descriptor_bytes_from_file(
1254 file: &mut File,
1255 header: &RunHeader,
1256) -> Result<Vec<u8>> {
1257 file.seek(SeekFrom::Start(header.encryption_descriptor_offset))?;
1258 let mut len_buf = [0u8; 4];
1259 file.read_exact(&mut len_buf)?;
1260 let len = u32::from_le_bytes(len_buf) as usize;
1261 const MAX_DESCRIPTOR_BYTES: usize = 65_536;
1266 if len > MAX_DESCRIPTOR_BYTES {
1267 return Err(MongrelError::InvalidArgument(format!(
1268 "encryption descriptor length {len} exceeds {MAX_DESCRIPTOR_BYTES}"
1269 )));
1270 }
1271 let mut buf = vec![0u8; len];
1272 file.read_exact(&mut buf)?;
1273 Ok(buf)
1274}
1275
1276fn verify_run_mac(
1283 file: &mut File,
1284 header: &RunHeader,
1285 dir: &[ColumnPageHeader],
1286 kek: &Kek,
1287 desc_bytes: &[u8],
1288) -> Result<()> {
1289 let header_bytes = bincode::serialize(header)?;
1290 let dir_bytes = bincode::serialize(dir)?;
1291 let mac_key = kek.derive_run_mac_key();
1292 let expected =
1293 crate::encryption::run_metadata_mac(&mac_key, &header_bytes, &dir_bytes, desc_bytes);
1294 file.seek(SeekFrom::Start(header.footer_offset + 8 + 8 + 32))?;
1295 let mut tag = [0u8; RUN_MAC_LEN];
1296 file.read_exact(&mut tag).map_err(|_| {
1297 MongrelError::Decryption(
1298 "encrypted run is missing or truncated its metadata MAC; cannot \
1299 authenticate metadata"
1300 .into(),
1301 )
1302 })?;
1303 let mut diff = 0u8;
1305 for (x, y) in tag.iter().zip(expected.iter()) {
1306 diff |= x ^ y;
1307 }
1308 if diff != 0 {
1309 return Err(MongrelError::Decryption(
1310 "run metadata authentication failed — tampered run or wrong key".into(),
1311 ));
1312 }
1313 Ok(())
1314}
1315
1316pub struct RunWriter<'a> {
1325 schema: &'a Schema,
1326 run_id: u128,
1327 epoch_created: Epoch,
1328 level: u8,
1329 kek: Option<&'a Kek>,
1330 indexable_columns: Vec<(u16, u8)>,
1333 compress: columnar::Compress,
1337 clean: bool,
1342 uniform_epoch: bool,
1347 le: bool,
1354}
1355
1356impl<'a> RunWriter<'a> {
1357 pub fn new(schema: &'a Schema, run_id: u128, epoch_created: Epoch, level: u8) -> Self {
1358 Self {
1359 schema,
1360 run_id,
1361 epoch_created,
1362 level,
1363 kek: None,
1364 indexable_columns: Vec::new(),
1365 compress: columnar::Compress::Zstd(3),
1366 clean: false,
1367 uniform_epoch: false,
1368 le: false,
1369 }
1370 }
1371
1372 pub fn uniform_epoch(mut self, uniform: bool) -> Self {
1378 self.uniform_epoch = uniform;
1379 self
1380 }
1381
1382 pub fn with_encryption(mut self, kek: &'a Kek, indexable_columns: Vec<(u16, u8)>) -> Self {
1386 self.kek = Some(kek);
1387 self.indexable_columns = indexable_columns;
1388 self
1389 }
1390
1391 pub fn with_zstd_level(mut self, level: i32) -> Self {
1394 self.compress = columnar::Compress::Zstd(level);
1395 self
1396 }
1397
1398 pub fn with_lz4(mut self) -> Self {
1402 self.compress = columnar::Compress::Lz4;
1403 self
1404 }
1405
1406 pub fn with_plain(mut self) -> Self {
1409 self.compress = columnar::Compress::Plain;
1410 self
1411 }
1412
1413 pub fn clean(mut self, clean: bool) -> Self {
1419 self.clean = clean;
1420 self
1421 }
1422
1423 pub fn with_native_endian(mut self) -> Self {
1428 if cfg!(target_endian = "little") {
1429 self.le = true;
1430 }
1431 self
1432 }
1433
1434 pub fn write_native(
1441 self,
1442 path: impl AsRef<Path>,
1443 user_columns: &[(u16, columnar::NativeColumn)],
1444 n: usize,
1445 first_row_id: u64,
1446 ) -> Result<RunHeader> {
1447 self.write_native_target(Some(path.as_ref()), None, user_columns, n, first_row_id)
1448 }
1449
1450 pub(crate) fn write_native_file(
1451 self,
1452 file: File,
1453 user_columns: &[(u16, columnar::NativeColumn)],
1454 n: usize,
1455 first_row_id: u64,
1456 ) -> Result<RunHeader> {
1457 self.write_native_target(None, Some(file), user_columns, n, first_row_id)
1458 }
1459
1460 fn write_native_target(
1461 self,
1462 path: Option<&Path>,
1463 file: Option<File>,
1464 user_columns: &[(u16, columnar::NativeColumn)],
1465 n: usize,
1466 first_row_id: u64,
1467 ) -> Result<RunHeader> {
1468 use columnar::NativeColumn;
1469 let row_id_col = NativeColumn::int64_sequence(first_row_id as i64, n);
1470 let epoch_col = NativeColumn::int64_constant(self.epoch_created.0 as i64, n);
1471 let deleted_col = NativeColumn::bool_constant(false, n);
1472
1473 let learned_trailer = build_learned_trailer_native(&row_id_col);
1474
1475 let row_ids = match &row_id_col {
1478 NativeColumn::Int64 { data, .. } => data.as_slice(),
1479 _ => &[],
1480 };
1481 let bounds = page_bounds(row_ids);
1482 let compress = self.compress;
1483 let le = self.le;
1484 let plain = matches!(compress, columnar::Compress::Plain);
1485 let row_id_enc = if plain {
1488 Encoding::Plain
1489 } else {
1490 Encoding::Delta
1491 };
1492 let sys_enc = if plain {
1493 Encoding::Plain
1494 } else {
1495 Encoding::Zstd
1496 };
1497
1498 let mut columns: Vec<ColumnPayload> = Vec::with_capacity(3 + self.schema.columns.len());
1499 let (pages, stats) = native_column_pages(
1500 TypeId::Int64,
1501 &row_id_col,
1502 row_id_enc,
1503 compress,
1504 le,
1505 &bounds,
1506 )?;
1507 columns.push(ColumnPayload {
1508 column_id: SYS_ROW_ID,
1509 type_id_tag: type_tag(&TypeId::Int64),
1510 encoding: row_id_enc,
1511 pages,
1512 page_stats: stats,
1513 });
1514 let (pages, stats) =
1515 native_column_pages(TypeId::Int64, &epoch_col, sys_enc, compress, le, &bounds)?;
1516 columns.push(ColumnPayload {
1517 column_id: SYS_EPOCH,
1518 type_id_tag: type_tag(&TypeId::Int64),
1519 encoding: sys_enc,
1520 pages,
1521 page_stats: stats,
1522 });
1523 let (pages, stats) =
1524 native_column_pages(TypeId::Bool, &deleted_col, sys_enc, compress, le, &bounds)?;
1525 columns.push(ColumnPayload {
1526 column_id: SYS_DELETED,
1527 type_id_tag: type_tag(&TypeId::Bool),
1528 encoding: sys_enc,
1529 pages,
1530 page_stats: stats,
1531 });
1532 use rayon::prelude::*;
1538 let user_cols: Vec<ColumnPayload> = self
1539 .schema
1540 .columns
1541 .par_iter()
1542 .map(|cdef| -> Result<ColumnPayload> {
1543 let col = user_columns
1544 .iter()
1545 .find(|(id, _)| *id == cdef.id)
1546 .map(|(_, c)| c)
1547 .ok_or_else(|| {
1548 MongrelError::ColumnNotFound(format!("user column {}", cdef.id))
1549 })?;
1550 let encoding = if plain {
1551 Encoding::Plain
1552 } else {
1553 choose_encoding_native(&cdef.ty, col)
1554 };
1555 let (pages, stats) =
1556 native_column_pages(cdef.ty.clone(), col, encoding, compress, le, &bounds)?;
1557 Ok(ColumnPayload {
1558 column_id: cdef.id,
1559 type_id_tag: type_tag(&cdef.ty),
1560 encoding,
1561 pages,
1562 page_stats: stats,
1563 })
1564 })
1565 .collect::<Result<Vec<_>>>()?;
1566 columns.extend(user_cols);
1567
1568 let flags = if self.uniform_epoch {
1574 RUN_FLAG_UNIFORM_EPOCH
1575 } else {
1576 RUN_FLAG_CLEAN
1577 };
1578 let spec = RunSpec {
1579 run_id: self.run_id,
1580 schema_id: self.schema.schema_id,
1581 epoch_created: self.epoch_created.0,
1582 level: self.level,
1583 flags,
1584 sort_key_column_id: SYS_ROW_ID,
1585 row_count: n as u64,
1586 min_row_id: first_row_id,
1587 max_row_id: first_row_id + n as u64 - 1,
1588 columns: &columns,
1589 };
1590 match file {
1591 Some(file) => write_run_with_file(
1592 file,
1593 &spec,
1594 self.kek,
1595 &self.indexable_columns,
1596 Some(&learned_trailer),
1597 ),
1598 None => write_run_with(
1599 path.ok_or_else(|| {
1600 MongrelError::InvalidArgument("sorted run output is missing".into())
1601 })?,
1602 &spec,
1603 self.kek,
1604 &self.indexable_columns,
1605 Some(&learned_trailer),
1606 ),
1607 }
1608 }
1609
1610 pub fn write(self, path: impl AsRef<Path>, rows: &[Row]) -> Result<RunHeader> {
1611 self.write_target(Some(path.as_ref()), None, rows)
1612 }
1613
1614 pub(crate) fn write_file(self, file: File, rows: &[Row]) -> Result<RunHeader> {
1615 self.write_target(None, Some(file), rows)
1616 }
1617
1618 fn write_target(
1619 self,
1620 path: Option<&Path>,
1621 file: Option<File>,
1622 rows: &[Row],
1623 ) -> Result<RunHeader> {
1624 let n = rows.len();
1625 let mut row_ids = Vec::with_capacity(n);
1627 let mut epochs = Vec::with_capacity(n);
1628 let mut deleted = Vec::with_capacity(n);
1629 let mut commit_ts_vals = Vec::with_capacity(n);
1630 let has_commit_ts = rows.iter().any(|r| r.commit_ts.is_some());
1632 for r in rows {
1633 row_ids.push(Value::Int64(r.row_id.0 as i64));
1634 epochs.push(Value::Int64(r.committed_epoch.0 as i64));
1635 deleted.push(Value::Bool(r.deleted));
1636 if has_commit_ts {
1637 commit_ts_vals.push(encode_commit_ts_value(r.commit_ts));
1638 }
1639 }
1640 let learned_trailer = build_learned_trailer(&row_ids);
1641 let (min_rid, max_rid) = row_id_bounds(rows);
1642 let row_id_i64: Vec<i64> = rows.iter().map(|r| r.row_id.0 as i64).collect();
1643 let bounds = page_bounds(&row_id_i64);
1644
1645 let mut columns: Vec<ColumnPayload> =
1646 Vec::with_capacity(3 + usize::from(has_commit_ts) + self.schema.columns.len());
1647 let (pages, stats, enc) = value_column_pages(TypeId::Int64, &row_ids, &bounds)?;
1648 columns.push(ColumnPayload {
1649 column_id: SYS_ROW_ID,
1650 type_id_tag: type_tag(&TypeId::Int64),
1651 encoding: enc,
1652 pages,
1653 page_stats: stats,
1654 });
1655 let (pages, stats, enc) = value_column_pages(TypeId::Int64, &epochs, &bounds)?;
1656 columns.push(ColumnPayload {
1657 column_id: SYS_EPOCH,
1658 type_id_tag: type_tag(&TypeId::Int64),
1659 encoding: enc,
1660 pages,
1661 page_stats: stats,
1662 });
1663 let (pages, stats, enc) = value_column_pages(TypeId::Bool, &deleted, &bounds)?;
1664 columns.push(ColumnPayload {
1665 column_id: SYS_DELETED,
1666 type_id_tag: type_tag(&TypeId::Bool),
1667 encoding: enc,
1668 pages,
1669 page_stats: stats,
1670 });
1671 if has_commit_ts {
1672 let (pages, stats, enc) = value_column_pages(TypeId::Bytes, &commit_ts_vals, &bounds)?;
1673 columns.push(ColumnPayload {
1674 column_id: SYS_COMMIT_TS,
1675 type_id_tag: type_tag(&TypeId::Bytes),
1676 encoding: enc,
1677 pages,
1678 page_stats: stats,
1679 });
1680 }
1681 for cdef in &self.schema.columns {
1683 let vals: Vec<Value> = rows
1684 .iter()
1685 .map(|r| r.columns.get(&cdef.id).cloned().unwrap_or(Value::Null))
1686 .collect();
1687 let (pages, stats, encoding) = value_column_pages(cdef.ty.clone(), &vals, &bounds)?;
1688 columns.push(ColumnPayload {
1689 column_id: cdef.id,
1690 type_id_tag: type_tag(&cdef.ty),
1691 encoding,
1692 pages,
1693 page_stats: stats,
1694 });
1695 }
1696
1697 let spec = RunSpec {
1698 run_id: self.run_id,
1699 schema_id: self.schema.schema_id,
1700 epoch_created: self.epoch_created.0,
1701 level: self.level,
1702 flags: {
1703 let mut f = if self.clean { RUN_FLAG_CLEAN } else { 0 };
1704 if self.uniform_epoch {
1705 f |= RUN_FLAG_UNIFORM_EPOCH;
1706 }
1707 f
1708 },
1709 sort_key_column_id: SYS_ROW_ID,
1710 row_count: n as u64,
1711 min_row_id: min_rid,
1712 max_row_id: max_rid,
1713 columns: &columns,
1714 };
1715 match file {
1716 Some(file) => write_run_with_file(
1717 file,
1718 &spec,
1719 self.kek,
1720 &self.indexable_columns,
1721 Some(&learned_trailer),
1722 ),
1723 None => write_run_with(
1724 path.ok_or_else(|| {
1725 MongrelError::InvalidArgument("sorted run output is missing".into())
1726 })?,
1727 &spec,
1728 self.kek,
1729 &self.indexable_columns,
1730 Some(&learned_trailer),
1731 ),
1732 }
1733 }
1734}
1735
1736fn type_tag(ty: &TypeId) -> u16 {
1737 match ty {
1739 TypeId::Bool => 1,
1740 TypeId::Int64 => 8,
1741 TypeId::Float64 => 9,
1742 TypeId::Bytes => 12,
1743 TypeId::Embedding { .. } => 13,
1744 _ => 0,
1745 }
1746}
1747
1748pub(crate) fn be_i64(b: Option<&[u8]>) -> Option<i64> {
1751 let b = b?;
1752 (b.len() == 8).then(|| i64::from_be_bytes(b.try_into().unwrap()))
1753}
1754
1755pub(crate) fn be_f64(b: Option<&[u8]>) -> Option<f64> {
1757 let b = b?;
1758 (b.len() == 8).then(|| f64::from_bits(u64::from_be_bytes(b.try_into().unwrap())))
1759}
1760
1761const PAGE_ROWS: usize = 65_536;
1764
1765fn page_bounds(row_ids: &[i64]) -> Vec<(usize, usize, u64, u64)> {
1768 let n = row_ids.len();
1769 if n == 0 {
1770 return vec![(0, 0, 0, 0)];
1771 }
1772 let mut out = Vec::new();
1773 let mut start = 0;
1774 while start < n {
1775 let end = (start + PAGE_ROWS).min(n);
1776 out.push((start, end, row_ids[start] as u64, row_ids[end - 1] as u64));
1777 start = end;
1778 }
1779 out
1780}
1781
1782fn native_column_pages(
1788 ty: TypeId,
1789 col: &columnar::NativeColumn,
1790 encoding: Encoding,
1791 compress: columnar::Compress,
1792 le: bool,
1793 bounds: &[(usize, usize, u64, u64)],
1794) -> Result<(Vec<Vec<u8>>, Vec<PageStat>)> {
1795 use rayon::prelude::*;
1796 let encode_one =
1797 |&(s, e, frid, lrid): &(usize, usize, u64, u64)| -> Result<(Vec<u8>, PageStat)> {
1798 let chunk = col.slice_range(s, e);
1799 let stat = columnar::page_stat_for(ty.clone(), &chunk, frid, lrid);
1800 let page = columnar::encode_page_native(ty.clone(), &chunk, encoding, compress, le)?;
1801 Ok((page, stat))
1802 };
1803 let pairs: Vec<(Vec<u8>, PageStat)> = if bounds.len() > 1 {
1805 bounds
1806 .par_iter()
1807 .map(encode_one)
1808 .collect::<Result<Vec<_>>>()?
1809 } else {
1810 bounds.iter().map(encode_one).collect::<Result<Vec<_>>>()?
1811 };
1812 let (pages, stats) = pairs.into_iter().unzip();
1813 Ok((pages, stats))
1814}
1815
1816fn value_column_pages(
1818 ty: TypeId,
1819 vals: &[Value],
1820 bounds: &[(usize, usize, u64, u64)],
1821) -> Result<(Vec<Vec<u8>>, Vec<PageStat>, Encoding)> {
1822 let encoding = choose_encoding(&ty, vals);
1823 let mut pages = Vec::with_capacity(bounds.len());
1824 let mut stats = Vec::with_capacity(bounds.len());
1825 for &(s, e, frid, lrid) in bounds {
1826 let chunk = &vals[s..e];
1827 pages.push(columnar::encode_page(ty.clone(), chunk, encoding)?);
1828 let native = columnar::values_to_native(ty.clone(), chunk);
1829 stats.push(columnar::page_stat_for(ty.clone(), &native, frid, lrid));
1830 }
1831 Ok((pages, stats, encoding))
1832}
1833
1834fn choose_encoding(ty: &TypeId, values: &[Value]) -> Encoding {
1837 use std::collections::HashSet;
1838 if matches!(ty, TypeId::Bytes) {
1839 let n = values.len();
1840 if n > 0 {
1841 let distinct = values
1842 .iter()
1843 .filter(|v| !matches!(v, Value::Null))
1844 .map(|v| v.encode_key())
1845 .collect::<HashSet<_>>()
1846 .len();
1847 if (distinct as f64 / n as f64) < 0.5 {
1848 return Encoding::Dictionary;
1849 }
1850 }
1851 }
1852 Encoding::Zstd
1853}
1854
1855fn choose_encoding_native(ty: &TypeId, col: &columnar::NativeColumn) -> Encoding {
1858 use std::collections::HashSet;
1859 match (ty, col) {
1860 (TypeId::Int64 | TypeId::TimestampNanos, columnar::NativeColumn::Int64 { data, .. }) => {
1861 if data.windows(2).all(|w| w[0] <= w[1]) {
1862 Encoding::Delta
1863 } else {
1864 Encoding::Zstd
1865 }
1866 }
1867 (
1868 TypeId::Bytes,
1869 columnar::NativeColumn::Bytes {
1870 offsets, values, ..
1871 },
1872 ) => {
1873 let n = offsets.len().saturating_sub(1);
1874 if n > 0 {
1875 let distinct: HashSet<&[u8]> = (0..n)
1876 .map(|i| &values[offsets[i] as usize..offsets[i + 1] as usize])
1877 .collect();
1878 if (distinct.len() as f64 / n as f64) < 0.5 {
1879 return Encoding::Dictionary;
1880 }
1881 }
1882 Encoding::Zstd
1883 }
1884 _ => Encoding::Zstd,
1885 }
1886}
1887
1888fn build_learned_trailer_native(col: &columnar::NativeColumn) -> Vec<u8> {
1890 let points: Vec<(u64, usize)> = match col {
1891 columnar::NativeColumn::Int64 { data, .. } => data
1892 .iter()
1893 .enumerate()
1894 .map(|(i, v)| (*v as u64, i))
1895 .collect(),
1896 _ => Vec::new(),
1897 };
1898 let pgm = PgmIndex::build(&points, LEARNED_EPSILON);
1899 bincode::serialize(&pgm).expect("pgm serialize")
1900}
1901
1902fn row_id_bounds(rows: &[Row]) -> (u64, u64) {
1903 match (rows.first(), rows.last()) {
1904 (Some(f), Some(l)) => (f.row_id.0, l.row_id.0),
1905 _ => (0, 0),
1906 }
1907}
1908
1909pub struct RunReader {
1915 file: File,
1916 mmap: Option<memmap2::Mmap>,
1917 header: RunHeader,
1918 dir: Vec<ColumnPageHeader>,
1919 schema: Schema,
1920 table_id: u64,
1922 cipher: Option<Box<dyn Cipher>>,
1924 nonce_prefix: [u8; 12],
1926 col_cache: HashMap<u16, Vec<Value>>,
1927 page_cache: Option<Arc<crate::cache::Sharded<crate::cache::PageCache>>>,
1931 decoded_cache: Option<Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>>,
1935 epoch_override: Option<Epoch>,
1939}
1940
1941#[derive(Debug, Clone, Copy)]
1942pub struct RunVisibleVersion {
1943 pub row_id: RowId,
1944 pub committed_epoch: Epoch,
1945 pub deleted: bool,
1946 pub commit_ts: Option<HlcTimestamp>,
1950 pub(crate) page_seq: usize,
1951 pub(crate) within_page: usize,
1952}
1953
1954pub struct RunVisibleVersionCursor {
1962 reader: RunReader,
1963 snapshot: Snapshot,
1964 page_row_counts: Vec<usize>,
1965 page_seq: usize,
1966 within_page: usize,
1967 row_ids: Vec<i64>,
1968 epochs: Vec<i64>,
1969 deleted: Vec<u8>,
1970 commit_ts: Vec<Option<HlcTimestamp>>,
1973 has_commit_ts_col: bool,
1974 lookahead: Option<RunVisibleVersion>,
1975 materialized_page: Option<usize>,
1976 materialized_columns: Vec<(u16, columnar::NativeColumn)>,
1977}
1978
1979impl RunVisibleVersionCursor {
1980 pub(crate) fn new(reader: RunReader, snapshot: Snapshot) -> Result<Self> {
1981 let page_row_counts = reader.page_row_counts(SYS_ROW_ID)?;
1982 let has_commit_ts_col = reader.has_column(SYS_COMMIT_TS);
1983 Ok(Self {
1984 reader,
1985 snapshot,
1986 page_row_counts,
1987 page_seq: 0,
1988 within_page: 0,
1989 row_ids: Vec::new(),
1990 epochs: Vec::new(),
1991 deleted: Vec::new(),
1992 commit_ts: Vec::new(),
1993 has_commit_ts_col,
1994 lookahead: None,
1995 materialized_page: None,
1996 materialized_columns: Vec::new(),
1997 })
1998 }
1999
2000 fn load_system_page(&mut self, control: &crate::ExecutionControl) -> Result<bool> {
2001 while self.page_seq < self.page_row_counts.len() {
2002 control.checkpoint()?;
2003 let rows = self.page_row_counts[self.page_seq];
2004 if rows == 0 {
2005 self.page_seq += 1;
2006 continue;
2007 }
2008 self.row_ids = match columnar::decode_page_native(
2009 TypeId::Int64,
2010 &self.reader.read_page(SYS_ROW_ID, self.page_seq)?,
2011 rows,
2012 )? {
2013 columnar::NativeColumn::Int64 { data, .. } => data,
2014 _ => return Err(MongrelError::InvalidArgument("sys row_id not int64".into())),
2015 };
2016 self.epochs = if let Some(epoch) = self.reader.epoch_override {
2017 vec![epoch.0 as i64; rows]
2018 } else {
2019 match columnar::decode_page_native(
2020 TypeId::Int64,
2021 &self.reader.read_page(SYS_EPOCH, self.page_seq)?,
2022 rows,
2023 )? {
2024 columnar::NativeColumn::Int64 { data, .. } => data,
2025 _ => return Err(MongrelError::InvalidArgument("sys epoch not int64".into())),
2026 }
2027 };
2028 self.deleted = match columnar::decode_page_native(
2029 TypeId::Bool,
2030 &self.reader.read_page(SYS_DELETED, self.page_seq)?,
2031 rows,
2032 )? {
2033 columnar::NativeColumn::Bool { data, .. } => data,
2034 _ => return Err(MongrelError::InvalidArgument("sys deleted not bool".into())),
2035 };
2036 self.commit_ts.clear();
2037 if self.has_commit_ts_col {
2038 let page = self.reader.read_page(SYS_COMMIT_TS, self.page_seq)?;
2039 let native = columnar::decode_page_native(TypeId::Bytes, &page, rows)?;
2040 self.commit_ts.reserve(rows);
2041 for i in 0..rows {
2042 self.commit_ts
2043 .push(decode_commit_ts_value(native.value_at(i).as_ref()));
2044 }
2045 }
2046 self.within_page = 0;
2047 return Ok(true);
2048 }
2049 Ok(false)
2050 }
2051
2052 fn next_raw(&mut self, control: &crate::ExecutionControl) -> Result<Option<RunVisibleVersion>> {
2053 if self.within_page >= self.row_ids.len() {
2054 if !self.row_ids.is_empty() {
2055 self.page_seq += 1;
2056 self.row_ids.clear();
2057 self.epochs.clear();
2058 self.deleted.clear();
2059 self.commit_ts.clear();
2060 }
2061 if !self.load_system_page(control)? {
2062 return Ok(None);
2063 }
2064 }
2065 if self.within_page.is_multiple_of(256) {
2066 control.checkpoint()?;
2067 }
2068 let position = self.within_page;
2069 self.within_page += 1;
2070 let commit_ts = self.commit_ts.get(position).copied().flatten();
2071 Ok(Some(RunVisibleVersion {
2072 row_id: RowId(self.row_ids[position] as u64),
2073 committed_epoch: Epoch(self.epochs[position] as u64),
2074 deleted: self.deleted[position] != 0,
2075 commit_ts,
2076 page_seq: self.page_seq,
2077 within_page: position,
2078 }))
2079 }
2080
2081 pub fn next_visible_version(
2082 &mut self,
2083 control: &crate::ExecutionControl,
2084 ) -> Result<Option<RunVisibleVersion>> {
2085 loop {
2086 let first = match self.lookahead.take() {
2087 Some(version) => version,
2088 None => match self.next_raw(control)? {
2089 Some(version) => version,
2090 None => return Ok(None),
2091 },
2092 };
2093 let row_id = first.row_id;
2094 let mut best = self
2099 .snapshot
2100 .observes_row(first.committed_epoch, first.commit_ts)
2101 .then_some(first);
2102 while let Some(candidate) = self.next_raw(control)? {
2103 if candidate.row_id != row_id {
2104 self.lookahead = Some(candidate);
2105 break;
2106 }
2107 if self
2108 .snapshot
2109 .observes_row(candidate.committed_epoch, candidate.commit_ts)
2110 && best.is_none_or(|current| {
2111 crate::epoch::Snapshot::version_is_newer(
2112 candidate.committed_epoch,
2113 candidate.commit_ts,
2114 current.committed_epoch,
2115 current.commit_ts,
2116 )
2117 })
2118 {
2119 best = Some(candidate);
2120 }
2121 }
2122 if best.is_some() {
2123 return Ok(best);
2124 }
2125 }
2126 }
2127
2128 pub fn materialize(
2129 &mut self,
2130 version: RunVisibleVersion,
2131 control: &crate::ExecutionControl,
2132 ) -> Result<Row> {
2133 if self.materialized_page != Some(version.page_seq) {
2134 let rows = self.page_row_counts[version.page_seq];
2135 let columns = self.reader.schema.columns.clone();
2136 let mut materialized = Vec::with_capacity(columns.len());
2137 for (index, column) in columns.into_iter().enumerate() {
2138 if index % 16 == 0 {
2139 control.checkpoint()?;
2140 }
2141 let native = if self.reader.has_column(column.id) {
2142 columnar::decode_page_native(
2143 column.ty,
2144 &self.reader.read_page(column.id, version.page_seq)?,
2145 rows,
2146 )?
2147 } else {
2148 columnar::null_native(column.ty, rows)
2149 };
2150 materialized.push((column.id, native));
2151 }
2152 self.materialized_columns = materialized;
2153 self.materialized_page = Some(version.page_seq);
2154 }
2155 let columns = self
2156 .materialized_columns
2157 .iter()
2158 .map(|(column_id, column)| {
2159 (
2160 *column_id,
2161 column.value_at(version.within_page).unwrap_or(Value::Null),
2162 )
2163 })
2164 .collect();
2165 let commit_ts = version.commit_ts.or_else(|| {
2168 if self.reader.has_column(SYS_COMMIT_TS) {
2169 let page = self
2170 .reader
2171 .read_page(SYS_COMMIT_TS, version.page_seq)
2172 .ok()?;
2173 let native = columnar::decode_page_native(
2174 TypeId::Bytes,
2175 &page,
2176 self.page_row_counts[version.page_seq],
2177 )
2178 .ok()?;
2179 decode_commit_ts_value(native.value_at(version.within_page).as_ref())
2180 } else {
2181 None
2182 }
2183 });
2184 Ok(Row {
2185 row_id: version.row_id,
2186 committed_epoch: version.committed_epoch,
2187 columns,
2188 deleted: version.deleted,
2189 commit_ts,
2190 })
2191 }
2192}
2193
2194impl RunReader {
2195 pub fn open(path: impl AsRef<Path>, schema: Schema, kek: Option<Arc<Kek>>) -> Result<Self> {
2196 Self::open_with_cache(path, schema, kek, None, None, 0, None)
2197 }
2198
2199 pub(crate) fn open_file(file: File, schema: Schema, kek: Option<Arc<Kek>>) -> Result<Self> {
2200 Self::open_file_with_cache(file, schema, kek, None, None, 0, None, None)
2201 }
2202
2203 #[allow(clippy::too_many_arguments)]
2204 pub(crate) fn open_with_cache(
2205 path: impl AsRef<Path>,
2206 schema: Schema,
2207 kek: Option<Arc<Kek>>,
2208 page_cache: Option<Arc<crate::cache::Sharded<crate::cache::PageCache>>>,
2209 decoded_cache: Option<Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>>,
2210 table_id: u64,
2211 verified_runs: Option<&parking_lot::Mutex<std::collections::HashSet<u128>>>,
2212 ) -> Result<Self> {
2213 let path = path.as_ref().to_path_buf();
2214 let file = crate::durable_file::open_regular_nofollow(&path)?;
2215 Self::open_file_with_cache(
2216 file,
2217 schema,
2218 kek,
2219 page_cache,
2220 decoded_cache,
2221 table_id,
2222 verified_runs,
2223 Some(path),
2224 )
2225 }
2226
2227 #[allow(clippy::too_many_arguments)]
2228 pub(crate) fn open_file_with_cache(
2229 mut file: File,
2230 schema: Schema,
2231 kek: Option<Arc<Kek>>,
2232 page_cache: Option<Arc<crate::cache::Sharded<crate::cache::PageCache>>>,
2233 decoded_cache: Option<Arc<crate::cache::Sharded<crate::cache::DecodedPageCache>>>,
2234 table_id: u64,
2235 verified_runs: Option<&parking_lot::Mutex<std::collections::HashSet<u128>>>,
2236 path: Option<PathBuf>,
2237 ) -> Result<Self> {
2238 let header = match verified_runs {
2239 Some(cache) => {
2240 let header = read_header_fast_from_file(&mut file)?;
2241 if cache.lock().contains(&header.run_id) {
2242 header
2243 } else {
2244 let verified = read_header_from_file(&mut file)?;
2245 cache.lock().insert(verified.run_id);
2246 verified
2247 }
2248 }
2249 None => read_header_from_file(&mut file)?,
2250 };
2251 let mut dir = read_column_dir_from_file(&mut file, &header)?;
2252 validate_column_directory_layout(&header, &dir)?;
2253 if header.is_encrypted() != kek.is_some() {
2254 return Err(MongrelError::Encryption(
2255 "sorted-run encryption mode differs from the database".into(),
2256 ));
2257 }
2258 let (cipher, nonce_prefix): (Option<Box<dyn Cipher>>, [u8; 12]) = if header.is_encrypted() {
2261 let kek = kek.as_ref().ok_or_else(|| {
2262 MongrelError::Encryption(
2263 "run is encrypted but no key-encryption key was provided".into(),
2264 )
2265 })?;
2266 if header.encryption_descriptor_offset == 0 {
2267 return Err(MongrelError::Encryption(
2268 "encrypted run has no encryption descriptor".into(),
2269 ));
2270 }
2271 let desc_bytes = read_encryption_descriptor_bytes_from_file(&mut file, &header)?;
2272 verify_run_mac(&mut file, &header, &dir, kek, &desc_bytes)?;
2278 let enc = crate::encryption::build_run_cipher(kek, &desc_bytes)?;
2279 if header.encrypted_stats_offset != 0 {
2283 overlay_encrypted_stats(
2284 &mut file,
2285 &header,
2286 enc.cipher.as_ref(),
2287 enc.nonce_prefix,
2288 &mut dir,
2289 )?;
2290 }
2291 (Some(enc.cipher), enc.nonce_prefix)
2292 } else {
2293 (None, [0u8; 12])
2294 };
2295 let mmap = unsafe { memmap2::MmapOptions::new().map(&file) }.ok();
2302 let _ = path;
2303 Ok(Self {
2304 file,
2305 mmap,
2306 header,
2307 dir,
2308 schema,
2309 table_id,
2310 cipher,
2311 nonce_prefix,
2312 col_cache: HashMap::new(),
2313 epoch_override: None,
2314 page_cache,
2315 decoded_cache,
2316 })
2317 }
2318
2319 pub fn header(&self) -> &RunHeader {
2320 &self.header
2321 }
2322
2323 pub(crate) fn validate_all_pages(&mut self) -> Result<()> {
2324 let mut columns = std::collections::HashSet::new();
2325 let row_header = self.find_header(SYS_ROW_ID)?.clone();
2326 let expected_rows = row_header
2327 .page_stats
2328 .iter()
2329 .map(|stat| stat.row_count)
2330 .collect::<Vec<_>>();
2331 let mut row_bounds = Vec::with_capacity(row_header.page_stats.len());
2332 let mut previous = None::<u64>;
2333 let mut first = None::<u64>;
2334 let mut last = None::<u64>;
2335 let mut counted = 0_u64;
2336 for (page, stat) in row_header.page_stats.iter().enumerate() {
2337 let bytes = self.read_page(SYS_ROW_ID, page)?;
2338 let native =
2339 columnar::decode_page_native(TypeId::Int64, &bytes, stat.row_count as usize)?;
2340 if columnar::page_stat_for(TypeId::Int64, &native, 0, 0).null_count != 0 {
2341 return Err(MongrelError::InvalidArgument(
2342 "sorted run row-id page contains nulls".into(),
2343 ));
2344 }
2345 let columnar::NativeColumn::Int64 { data, validity } = native else {
2346 return Err(MongrelError::InvalidArgument(
2347 "sorted run row-id page has the wrong type".into(),
2348 ));
2349 };
2350 let _ = validity;
2351 if data.is_empty() {
2352 if self.header.row_count != 0 || stat.row_count != 0 {
2353 return Err(MongrelError::InvalidArgument(
2354 "sorted run row-id page is unexpectedly empty".into(),
2355 ));
2356 }
2357 row_bounds.push((0, 0));
2358 continue;
2359 }
2360 if data.iter().any(|value| *value < 0) {
2361 return Err(MongrelError::InvalidArgument(
2362 "sorted run contains a negative row id".into(),
2363 ));
2364 }
2365 let page_first = data[0] as u64;
2366 let page_last = data[data.len() - 1] as u64;
2367 for value in data {
2368 let value = value as u64;
2369 if previous.is_some_and(|prior| {
2370 value < prior || (self.header.is_clean() && value == prior)
2371 }) {
2372 return Err(MongrelError::InvalidArgument(
2373 "sorted run row ids are not ordered".into(),
2374 ));
2375 }
2376 first.get_or_insert(value);
2377 previous = Some(value);
2378 last = Some(value);
2379 counted += 1;
2380 }
2381 if stat.first_row_id != page_first || stat.last_row_id != page_last {
2382 return Err(MongrelError::InvalidArgument(
2383 "sorted run row-id page bounds are stale".into(),
2384 ));
2385 }
2386 row_bounds.push((page_first, page_last));
2387 }
2388 if counted != self.header.row_count
2389 || first.unwrap_or(0) != self.header.min_row_id
2390 || last.unwrap_or(0) != self.header.max_row_id
2391 {
2392 return Err(MongrelError::InvalidArgument(
2393 "sorted run header row bounds differ from its pages".into(),
2394 ));
2395 }
2396 for column in self.dir.clone() {
2397 if !columns.insert(column.column_id) {
2398 return Err(MongrelError::InvalidArgument(format!(
2399 "sorted run contains duplicate column {}",
2400 column.column_id
2401 )));
2402 }
2403 let rows = column
2404 .page_stats
2405 .iter()
2406 .map(|stat| stat.row_count)
2407 .collect::<Vec<_>>();
2408 if rows != expected_rows {
2409 return Err(MongrelError::InvalidArgument(
2410 "sorted run columns have inconsistent page row counts".into(),
2411 ));
2412 }
2413 let ty = self.resolve_type(column.column_id);
2414 if column.type_id_tag != type_tag(&ty)
2415 || column.encoding > Encoding::Zstd as u8
2416 || (!is_system_column_id(column.column_id)
2417 && self
2418 .schema
2419 .columns
2420 .iter()
2421 .all(|item| item.id != column.column_id))
2422 {
2423 return Err(MongrelError::InvalidArgument(
2424 "sorted run column type or encoding is invalid".into(),
2425 ));
2426 }
2427 for (page, stat) in column.page_stats.iter().enumerate() {
2428 let bytes = self.read_page(column.column_id, page)?;
2429 if bytes.len() != stat.uncompressed_len as usize {
2430 return Err(MongrelError::InvalidArgument(
2431 "sorted run page length differs from its metadata".into(),
2432 ));
2433 }
2434 let native =
2435 match columnar::decode_page_native(ty.clone(), &bytes, stat.row_count as usize)
2436 {
2437 Ok(native) => native,
2438 Err(MongrelError::InvalidArgument(message))
2439 if message.starts_with("decode_page_native: unsupported ty") =>
2440 {
2441 let values =
2442 columnar::decode_page(ty.clone(), &bytes, stat.row_count as usize)?;
2443 columnar::values_to_native(ty.clone(), &values)
2444 }
2445 Err(error) => return Err(error),
2446 };
2447 let (first_row_id, last_row_id) =
2448 row_bounds.get(page).copied().ok_or_else(|| {
2449 MongrelError::InvalidArgument(
2450 "sorted run column has more pages than its row-id column".into(),
2451 )
2452 })?;
2453 let expected =
2454 columnar::page_stat_for(ty.clone(), &native, first_row_id, last_row_id);
2455 if stat.first_row_id != expected.first_row_id
2456 || stat.last_row_id != expected.last_row_id
2457 || stat.null_count != expected.null_count
2458 || stat.min != expected.min
2459 || stat.max != expected.max
2460 {
2461 return Err(MongrelError::InvalidArgument(
2462 "sorted run page statistics differ from decoded values".into(),
2463 ));
2464 }
2465 }
2466 }
2467 if !columns.contains(&SYS_ROW_ID)
2468 || !columns.contains(&SYS_EPOCH)
2469 || !columns.contains(&SYS_DELETED)
2470 || expected_rows.into_iter().map(u64::from).sum::<u64>() != self.header.row_count
2471 {
2472 return Err(MongrelError::InvalidArgument(
2473 "sorted run system columns or row count are invalid".into(),
2474 ));
2475 }
2476 if self.header.schema_id == self.schema.schema_id
2477 && self
2478 .schema
2479 .columns
2480 .iter()
2481 .any(|column| !columns.contains(&column.id))
2482 {
2483 return Err(MongrelError::InvalidArgument(
2484 "sorted run is missing a column from its declared schema".into(),
2485 ));
2486 }
2487
2488 for row_index in 0..usize::try_from(self.header.row_count).map_err(|_| {
2492 MongrelError::InvalidArgument("sorted run row count exceeds this platform".into())
2493 })? {
2494 let epoch = match self.column(SYS_EPOCH)?.get(row_index) {
2495 Some(Value::Int64(value)) if *value >= 0 => *value as u64,
2496 _ => {
2497 return Err(MongrelError::InvalidArgument(
2498 "sorted run contains an invalid commit epoch".into(),
2499 ))
2500 }
2501 };
2502 if epoch > self.header.epoch_created || (self.header.is_uniform_epoch() && epoch != 0) {
2503 return Err(MongrelError::InvalidArgument(
2504 "sorted run commit epoch exceeds its creation epoch".into(),
2505 ));
2506 }
2507 let deleted = match self.column(SYS_DELETED)?.get(row_index) {
2508 Some(Value::Bool(value)) => *value,
2509 _ => {
2510 return Err(MongrelError::InvalidArgument(
2511 "sorted run contains an invalid tombstone marker".into(),
2512 ))
2513 }
2514 };
2515 let mut values = Vec::with_capacity(self.schema.columns.len());
2516 for column in self.schema.columns.clone() {
2517 let value = if columns.contains(&column.id) {
2518 self.column(column.id)?
2519 .get(row_index)
2520 .cloned()
2521 .unwrap_or(Value::Null)
2522 } else {
2523 Value::Null
2524 };
2525 values.push((column.id, value));
2526 }
2527 if !deleted {
2528 self.schema
2529 .validate_persisted_values(&values)
2530 .map_err(|error| {
2531 MongrelError::InvalidArgument(format!(
2532 "sorted run row violates its schema: {error}"
2533 ))
2534 })?;
2535 }
2536 }
2537 Ok(())
2538 }
2539
2540 pub(crate) fn set_uniform_epoch(&mut self, epoch: Epoch) {
2544 if self.header.is_uniform_epoch() {
2545 self.epoch_override = Some(epoch);
2546 self.col_cache.remove(&SYS_EPOCH);
2548 }
2549 }
2550
2551 #[allow(dead_code)] pub(crate) fn into_visible_version_cursor(
2554 self,
2555 snapshot: Epoch,
2556 ) -> Result<RunVisibleVersionCursor> {
2557 RunVisibleVersionCursor::new(self, Snapshot::at(snapshot))
2560 }
2561
2562 pub fn into_visible_version_cursor_at(
2566 self,
2567 snapshot: Snapshot,
2568 ) -> Result<RunVisibleVersionCursor> {
2569 RunVisibleVersionCursor::new(self, snapshot)
2570 }
2571
2572 pub fn is_clean(&self) -> bool {
2575 self.header.is_clean()
2576 }
2577
2578 pub fn row_count(&self) -> usize {
2579 self.header.row_count as usize
2580 }
2581
2582 pub(crate) fn clean_contiguous_row_ids(&self) -> bool {
2583 let n = self.row_count();
2584 n > 0
2585 && self.is_clean()
2586 && self.epoch_override.is_none()
2587 && self.header.max_row_id >= self.header.min_row_id
2588 && self
2589 .header
2590 .max_row_id
2591 .checked_sub(self.header.min_row_id)
2592 .and_then(|span| span.checked_add(1))
2593 == Some(n as u64)
2594 }
2595
2596 pub(crate) fn position_for_row_id_fast(&self, row_id: u64) -> Option<usize> {
2597 if !self.clean_contiguous_row_ids()
2598 || row_id < self.header.min_row_id
2599 || row_id > self.header.max_row_id
2600 {
2601 return None;
2602 }
2603 Some((row_id - self.header.min_row_id) as usize)
2604 }
2605
2606 pub(crate) fn positions_for_row_ids_fast(&self, row_ids: &[u64]) -> Option<Vec<usize>> {
2607 if !self.clean_contiguous_row_ids() {
2608 return None;
2609 }
2610 let mut positions = Vec::with_capacity(row_ids.len());
2611 for &row_id in row_ids {
2612 if let Some(pos) = self.position_for_row_id_fast(row_id) {
2613 positions.push(pos);
2614 }
2615 }
2616 positions.sort_unstable();
2617 Some(positions)
2618 }
2619
2620 fn resolve_type(&self, column_id: u16) -> TypeId {
2621 match column_id {
2622 SYS_ROW_ID | SYS_EPOCH => TypeId::Int64,
2623 SYS_DELETED => TypeId::Bool,
2624 SYS_COMMIT_TS => TypeId::Bytes,
2625 _ => self
2626 .schema
2627 .columns
2628 .iter()
2629 .find(|c| c.id == column_id)
2630 .map(|c| c.ty.clone())
2631 .unwrap_or(TypeId::Bytes),
2632 }
2633 }
2634
2635 fn commit_ts_at(&mut self, index: usize) -> Result<Option<HlcTimestamp>> {
2637 if !self.has_column(SYS_COMMIT_TS) {
2638 return Ok(None);
2639 }
2640 Ok(decode_commit_ts_value(
2641 self.column(SYS_COMMIT_TS)?.get(index),
2642 ))
2643 }
2644
2645 fn find_header(&self, column_id: u16) -> Result<&ColumnPageHeader> {
2646 self.dir
2647 .iter()
2648 .find(|h| h.column_id == column_id)
2649 .ok_or_else(|| MongrelError::ColumnNotFound(format!("column {column_id}")))
2650 }
2651
2652 fn col_encrypted(&self, column_id: u16) -> bool {
2658 self.dir
2659 .iter()
2660 .find(|h| h.column_id == column_id)
2661 .map(|h| h.flags & ColumnPageHeader::PAGE_ENCRYPTED != 0)
2662 .unwrap_or(false)
2663 }
2664
2665 pub(crate) fn read_page(&mut self, column_id: u16, page_seq: usize) -> Result<Vec<u8>> {
2666 let (offset, compressed_len, encrypted) = {
2667 let ch = self.find_header(column_id)?;
2668 let stat = ch
2669 .page_stats
2670 .get(page_seq)
2671 .ok_or_else(|| MongrelError::InvalidArgument("page seq out of range".into()))?;
2672 (
2673 stat.offset,
2674 stat.compressed_len,
2675 ch.flags & ColumnPageHeader::PAGE_ENCRYPTED != 0,
2676 )
2677 };
2678 let end = offset
2679 .checked_add(compressed_len as u64)
2680 .ok_or_else(|| MongrelError::InvalidArgument("run page offset overflows".into()))?;
2681 let start = usize::try_from(offset)
2682 .map_err(|_| MongrelError::InvalidArgument("run page offset is too large".into()))?;
2683 let end = usize::try_from(end)
2684 .map_err(|_| MongrelError::InvalidArgument("run page end is too large".into()))?;
2685 let key = page_cache_key(self.table_id, self.header.run_id, column_id, page_seq);
2688 if let Some(cache) = &self.page_cache {
2689 if let Some(bytes) = cache.lock(&key).get(
2690 &key,
2691 crate::epoch::Snapshot::at(crate::epoch::Epoch(u64::MAX)),
2692 ) {
2693 return decrypt_or_passthrough(
2694 self.cipher.as_deref(),
2695 self.nonce_prefix,
2696 column_id,
2697 page_seq,
2698 encrypted,
2699 &bytes,
2700 );
2701 }
2702 }
2703 let buf = match &self.mmap {
2704 Some(m) => m
2707 .get(start..end)
2708 .ok_or_else(|| {
2709 MongrelError::InvalidArgument("run page is outside the mapped file".into())
2710 })?
2711 .to_vec(),
2712 None => {
2713 self.file.seek(SeekFrom::Start(offset))?;
2714 let mut buf = vec![0u8; compressed_len as usize];
2715 self.file.read_exact(&mut buf)?;
2716 buf
2717 }
2718 };
2719 if let Some(cache) = &self.page_cache {
2721 cache.lock(&key).insert(crate::page::CachedPage {
2722 committed_epoch: crate::epoch::Epoch(self.header.epoch_created),
2723 content_hash: key,
2724 bytes: bytes::Bytes::copy_from_slice(&buf),
2725 });
2726 }
2727 decrypt_or_passthrough(
2728 self.cipher.as_deref(),
2729 self.nonce_prefix,
2730 column_id,
2731 page_seq,
2732 encrypted,
2733 &buf,
2734 )
2735 }
2736
2737 fn read_page_shared(&self, column_id: u16, page_seq: usize) -> Result<Vec<u8>> {
2742 let ch = self.find_header(column_id)?;
2743 let stat = ch
2744 .page_stats
2745 .get(page_seq)
2746 .ok_or_else(|| MongrelError::InvalidArgument("page seq out of range".into()))?;
2747 let encrypted = ch.flags & ColumnPageHeader::PAGE_ENCRYPTED != 0;
2748 let key = page_cache_key(self.table_id, self.header.run_id, column_id, page_seq);
2751 if let Some(cache) = &self.page_cache {
2752 if let Some(guard) = cache.try_lock(&key) {
2753 if let Some(bytes) = guard.try_get(
2754 &key,
2755 crate::epoch::Snapshot::at(crate::epoch::Epoch(u64::MAX)),
2756 ) {
2757 return decrypt_or_passthrough(
2758 self.cipher.as_deref(),
2759 self.nonce_prefix,
2760 column_id,
2761 page_seq,
2762 encrypted,
2763 &bytes,
2764 );
2765 }
2766 }
2767 }
2768 let mmap = self.mmap.as_ref().ok_or_else(|| {
2769 MongrelError::InvalidArgument("parallel page decode requires an mmap-backed run".into())
2770 })?;
2771 let end = stat
2772 .offset
2773 .checked_add(stat.compressed_len as u64)
2774 .ok_or_else(|| MongrelError::InvalidArgument("run page offset overflows".into()))?;
2775 let start = usize::try_from(stat.offset)
2776 .map_err(|_| MongrelError::InvalidArgument("run page offset is too large".into()))?;
2777 let end = usize::try_from(end)
2778 .map_err(|_| MongrelError::InvalidArgument("run page end is too large".into()))?;
2779 let buf = mmap
2780 .get(start..end)
2781 .ok_or_else(|| {
2782 MongrelError::InvalidArgument("run page is outside the mapped file".into())
2783 })?
2784 .to_vec();
2785 if let Some(cache) = &self.page_cache {
2789 if let Some(mut guard) = cache.try_lock(&key) {
2790 guard.insert(crate::page::CachedPage {
2791 committed_epoch: crate::epoch::Epoch(self.header.epoch_created),
2792 content_hash: key,
2793 bytes: bytes::Bytes::copy_from_slice(&buf),
2794 });
2795 }
2796 }
2797 decrypt_or_passthrough(
2798 self.cipher.as_deref(),
2799 self.nonce_prefix,
2800 column_id,
2801 page_seq,
2802 encrypted,
2803 &buf,
2804 )
2805 }
2806
2807 fn column(&mut self, column_id: u16) -> Result<&[Value]> {
2809 if column_id == SYS_EPOCH {
2812 if let Some(ov) = self.epoch_override {
2813 if !self.col_cache.contains_key(&column_id) {
2814 let n = self.row_count();
2815 self.col_cache
2816 .insert(column_id, vec![Value::Int64(ov.0 as i64); n]);
2817 }
2818 return Ok(self.col_cache.get(&column_id).unwrap().as_slice());
2819 }
2820 }
2821 if !self.col_cache.contains_key(&column_id) {
2822 let ty = self.resolve_type(column_id);
2823 let page_rows: Vec<usize> = {
2824 let ch = self.find_header(column_id)?;
2825 ch.page_stats.iter().map(|s| s.row_count as usize).collect()
2826 };
2827 let mut decoded: Vec<Value> = Vec::with_capacity(self.row_count());
2828 for (seq, &pr) in page_rows.iter().enumerate() {
2829 let page = self.read_page(column_id, seq)?;
2830 decoded.extend(columnar::decode_page(ty.clone(), &page, pr)?);
2831 }
2832 self.col_cache.insert(column_id, decoded);
2833 }
2834 Ok(self.col_cache.get(&column_id).unwrap().as_slice())
2835 }
2836
2837 pub fn get_version(&mut self, row_id: RowId, snapshot: Epoch) -> Result<Option<(Epoch, Row)>> {
2851 self.get_version_at(row_id, Snapshot::at(snapshot))
2852 }
2853
2854 pub fn get_version_at(
2866 &mut self,
2867 row_id: RowId,
2868 snapshot: Snapshot,
2869 ) -> Result<Option<(Epoch, Row)>> {
2870 match self.find_version_page_at(row_id, snapshot)? {
2871 None => Ok(None),
2872 Some((epoch, seq, local_index)) => Ok(Some((
2873 Epoch(epoch),
2874 self.materialize_in_page(seq, local_index)?,
2875 ))),
2876 }
2877 }
2878
2879 #[allow(dead_code)] fn find_version_page(
2886 &mut self,
2887 row_id: RowId,
2888 snapshot: Epoch,
2889 ) -> Result<Option<(u64, usize, usize)>> {
2890 self.find_version_page_at(row_id, Snapshot::at(snapshot))
2891 }
2892
2893 fn find_version_page_at(
2899 &mut self,
2900 row_id: RowId,
2901 snapshot: Snapshot,
2902 ) -> Result<Option<(u64, usize, usize)>> {
2903 let n = self.row_count();
2904 if n == 0 {
2905 return Ok(None);
2906 }
2907 let target = row_id.0 as i64;
2908 let ch = self.find_header(SYS_ROW_ID)?;
2909 let mut page_start = 0u64;
2910 let candidate_pages: Vec<(usize, u64)> = ch .page_stats
2912 .iter()
2913 .enumerate()
2914 .filter_map(|(seq, s)| {
2915 let start = page_start;
2916 page_start += s.row_count as u64;
2917 (s.first_row_id <= row_id.0 && row_id.0 <= s.last_row_id).then_some((seq, start))
2918 })
2919 .collect();
2920 if candidate_pages.is_empty() {
2921 return Ok(None);
2922 }
2923 let ty = self.resolve_type(SYS_ROW_ID);
2924 let has_commit_ts_col = self.has_column(SYS_COMMIT_TS);
2925 let mut best: Option<(u64, Option<HlcTimestamp>, usize, usize)> = None; for (seq, _page_row_start) in candidate_pages {
2930 let page_rows = self.find_header(SYS_ROW_ID)?.page_stats[seq].row_count as usize;
2931 let row_ids =
2932 match self.decode_page_native_cached(ty.clone(), SYS_ROW_ID, seq, page_rows)? {
2933 columnar::NativeColumn::Int64 { data, .. } => data,
2934 _ => return Err(MongrelError::InvalidArgument("sys row_id not int64".into())),
2935 };
2936 let local = match row_ids.binary_search(&target) {
2937 Ok(i) => i,
2938 Err(_) => continue,
2939 };
2940 let epoch_ty = self.resolve_type(SYS_EPOCH);
2941 let epochs =
2942 match self.decode_page_native_cached(epoch_ty, SYS_EPOCH, seq, page_rows)? {
2943 columnar::NativeColumn::Int64 { data, .. } => data,
2944 _ => return Err(MongrelError::InvalidArgument("sys epoch not int64".into())),
2945 };
2946 let mut lo = local;
2949 while lo > 0 && row_ids[lo - 1] == target {
2950 lo -= 1;
2951 }
2952 let mut hi = local;
2953 while hi + 1 < row_ids.len() && row_ids[hi + 1] == target {
2954 hi += 1;
2955 }
2956 let commit_ts_native = if has_commit_ts_col {
2960 let page = self.read_page(SYS_COMMIT_TS, seq)?;
2961 Some(columnar::decode_page_native(
2962 TypeId::Bytes,
2963 &page,
2964 page_rows,
2965 )?)
2966 } else {
2967 None
2968 };
2969 for (i, &epoch_val) in epochs[lo..=hi].iter().enumerate() {
2970 let epoch = epoch_val as u64;
2971 let commit_ts = commit_ts_native
2972 .as_ref()
2973 .and_then(|col| decode_commit_ts_value(col.value_at(lo + i).as_ref()));
2974 if !snapshot.observes_row(Epoch(epoch), commit_ts) {
2978 continue;
2979 }
2980 let candidate = (epoch, commit_ts, seq, lo + i);
2981 let is_newer = best.as_ref().is_none_or(|cur| {
2982 Snapshot::version_is_newer(Epoch(candidate.0), candidate.1, Epoch(cur.0), cur.1)
2983 });
2984 if is_newer {
2985 best = Some(candidate);
2986 }
2987 }
2988 }
2989 Ok(best.map(|(epoch, _, seq, local)| (epoch, seq, local)))
2990 }
2991
2992 pub fn get_version_column(
3000 &mut self,
3001 row_id: RowId,
3002 snapshot: Epoch,
3003 column_id: u16,
3004 ) -> Result<Option<(Epoch, bool, Option<Value>)>> {
3005 self.get_version_column_at(row_id, Snapshot::at(snapshot), column_id)
3006 }
3007
3008 pub fn get_version_column_at(
3010 &mut self,
3011 row_id: RowId,
3012 snapshot: Snapshot,
3013 column_id: u16,
3014 ) -> Result<Option<(Epoch, bool, Option<Value>)>> {
3015 let Some((epoch, seq, local_index)) = self.find_version_page_at(row_id, snapshot)? else {
3016 return Ok(None);
3017 };
3018 let page_rows = self.find_header(SYS_ROW_ID)?.page_stats[seq].row_count as usize;
3019 let page_start: usize = self.find_header(SYS_ROW_ID)?.page_stats[..seq]
3020 .iter()
3021 .map(|s| s.row_count as usize)
3022 .sum();
3023 let global_index = page_start + local_index;
3024 let native_at = |slf: &mut Self, cid: u16| -> Result<Option<Value>> {
3025 if !slf.dir.iter().any(|h| h.column_id == cid) {
3026 return Ok(None);
3027 }
3028 let ty = slf.resolve_type(cid);
3029 if !matches!(
3030 ty,
3031 TypeId::Bool
3032 | TypeId::Int8
3033 | TypeId::Int16
3034 | TypeId::Int32
3035 | TypeId::Int64
3036 | TypeId::UInt8
3037 | TypeId::UInt16
3038 | TypeId::UInt32
3039 | TypeId::UInt64
3040 | TypeId::Float32
3041 | TypeId::Float64
3042 | TypeId::TimestampNanos
3043 | TypeId::Date32
3044 | TypeId::Bytes
3045 ) {
3046 return Ok(slf.column(cid)?.get(global_index).cloned());
3047 }
3048 Ok(slf
3049 .decode_page_native_cached(ty, cid, seq, page_rows)?
3050 .value_at(local_index))
3051 };
3052 let deleted = matches!(native_at(self, SYS_DELETED)?, Some(Value::Bool(true)));
3053 let value = native_at(self, column_id)?;
3054 Ok(Some((Epoch(epoch), deleted, value)))
3055 }
3056
3057 pub fn get_version_visibility(
3059 &mut self,
3060 row_id: RowId,
3061 snapshot: Epoch,
3062 ) -> Result<Option<(Epoch, bool)>> {
3063 self.get_version_visibility_at(row_id, Snapshot::at(snapshot))
3064 }
3065
3066 pub fn get_version_visibility_at(
3068 &mut self,
3069 row_id: RowId,
3070 snapshot: Snapshot,
3071 ) -> Result<Option<(Epoch, bool)>> {
3072 let Some((epoch, seq, local_index)) = self.find_version_page_at(row_id, snapshot)? else {
3073 return Ok(None);
3074 };
3075 let page_rows = self.find_header(SYS_ROW_ID)?.page_stats[seq].row_count as usize;
3076 let deleted = match self.decode_page_native_cached(
3077 self.resolve_type(SYS_DELETED),
3078 SYS_DELETED,
3079 seq,
3080 page_rows,
3081 )? {
3082 columnar::NativeColumn::Bool { data, .. } => data[local_index] != 0,
3083 _ => return Err(MongrelError::InvalidArgument("sys deleted not bool".into())),
3084 };
3085 Ok(Some((Epoch(epoch), deleted)))
3086 }
3087
3088 fn materialize_in_page(&mut self, seq: usize, local_index: usize) -> Result<Row> {
3097 let page_rows = self.find_header(SYS_ROW_ID)?.page_stats[seq].row_count as usize;
3098 let page_start: usize = self.find_header(SYS_ROW_ID)?.page_stats[..seq]
3099 .iter()
3100 .map(|s| s.row_count as usize)
3101 .sum();
3102 let global_index = page_start + local_index;
3103 let native_at = |slf: &mut Self, column_id: u16| -> Result<Option<Value>> {
3104 if !slf.dir.iter().any(|h| h.column_id == column_id) {
3107 return Ok(None);
3108 }
3109 let ty = slf.resolve_type(column_id);
3110 if !matches!(
3111 ty,
3112 TypeId::Bool
3113 | TypeId::Int8
3114 | TypeId::Int16
3115 | TypeId::Int32
3116 | TypeId::Int64
3117 | TypeId::UInt8
3118 | TypeId::UInt16
3119 | TypeId::UInt32
3120 | TypeId::UInt64
3121 | TypeId::Float32
3122 | TypeId::Float64
3123 | TypeId::TimestampNanos
3124 | TypeId::Date32
3125 | TypeId::Bytes
3126 ) {
3127 return Ok(slf.column(column_id)?.get(global_index).cloned());
3130 }
3131 Ok(slf
3132 .decode_page_native_cached(ty, column_id, seq, page_rows)?
3133 .value_at(local_index))
3134 };
3135 let row_id = RowId(match native_at(self, SYS_ROW_ID)? {
3136 Some(Value::Int64(x)) => x as u64,
3137 _ => 0,
3138 });
3139 let committed_epoch = Epoch(match native_at(self, SYS_EPOCH)? {
3140 Some(Value::Int64(x)) => x as u64,
3141 _ => 0,
3142 });
3143 let deleted = matches!(native_at(self, SYS_DELETED)?, Some(Value::Bool(true)));
3144 let commit_ts = if self.has_column(SYS_COMMIT_TS) {
3145 decode_commit_ts_value(native_at(self, SYS_COMMIT_TS)?.as_ref())
3146 } else {
3147 None
3148 };
3149 let col_ids: Vec<u16> = self.schema.columns.iter().map(|c| c.id).collect();
3150 let mut columns = HashMap::new();
3151 for id in col_ids {
3152 columns.insert(id, native_at(self, id)?.unwrap_or(Value::Null));
3153 }
3154 Ok(Row {
3155 row_id,
3156 committed_epoch,
3157 columns,
3158 deleted,
3159 commit_ts,
3160 })
3161 }
3162
3163 pub fn all_rows(&mut self) -> Result<Vec<Row>> {
3166 let n = self.row_count();
3167 let mut out = Vec::with_capacity(n);
3168 for i in 0..n {
3169 out.push(self.materialize(i)?);
3170 }
3171 Ok(out)
3172 }
3173
3174 pub fn for_each_system<F>(&mut self, mut visit: F) -> Result<()>
3181 where
3182 F: FnMut(RowId, Epoch, Option<HlcTimestamp>, bool) -> Result<()>,
3183 {
3184 let row_ids = self.column(SYS_ROW_ID)?.to_vec();
3188 let epochs = self.column(SYS_EPOCH)?.to_vec();
3189 let deleted = self.column(SYS_DELETED)?.to_vec();
3190 let has_commit_ts = self.has_column(SYS_COMMIT_TS);
3191 let commit_ts: Vec<Option<HlcTimestamp>> = if has_commit_ts {
3192 let col = self.column(SYS_COMMIT_TS)?;
3193 let mut out = Vec::with_capacity(col.len());
3194 for v in col {
3195 out.push(decode_commit_ts_value(Some(v)));
3196 }
3197 out
3198 } else {
3199 Vec::new()
3200 };
3201 debug_assert_eq!(row_ids.len(), epochs.len());
3202 debug_assert_eq!(row_ids.len(), deleted.len());
3203 for (i, rid_val) in row_ids.iter().enumerate() {
3204 let rid = match rid_val {
3205 Value::Int64(n) => RowId(*n as u64),
3206 _ => continue,
3207 };
3208 let epoch = match epochs[i] {
3209 Value::Int64(n) => Epoch(n as u64),
3210 _ => Epoch(0),
3211 };
3212 let ts = commit_ts.get(i).copied().flatten();
3213 let del = match deleted[i] {
3214 Value::Bool(b) => b,
3215 _ => false,
3216 };
3217 visit(rid, epoch, ts, del)?;
3218 }
3219 Ok(())
3220 }
3221
3222 pub fn all_rows_controlled(
3224 &mut self,
3225 control: &crate::ExecutionControl,
3226 max_rows: usize,
3227 ) -> Result<Vec<Row>> {
3228 let n = self.row_count();
3229 if n > max_rows {
3230 return Err(MongrelError::ResourceLimitExceeded {
3231 resource: "controlled run row materialization",
3232 requested: n,
3233 limit: max_rows,
3234 });
3235 }
3236 let mut out = Vec::with_capacity(n);
3237 for i in 0..n {
3238 if i % 256 == 0 {
3239 control.checkpoint()?;
3240 }
3241 out.push(self.materialize(i)?);
3242 }
3243 control.checkpoint()?;
3244 Ok(out)
3245 }
3246
3247 pub fn visible_indices(&mut self, snapshot: Epoch) -> Result<Vec<usize>> {
3253 self.visible_indices_at(Snapshot::at(snapshot))
3254 }
3255
3256 pub fn visible_indices_at(&mut self, snapshot: Snapshot) -> Result<Vec<usize>> {
3260 let n = self.row_count();
3261 if n == 0 {
3262 return Ok(Vec::new());
3263 }
3264 let row_ids = self.column(SYS_ROW_ID)?.to_vec();
3265 let epochs = self.column(SYS_EPOCH)?.to_vec();
3266 let deleted = self.column(SYS_DELETED)?.to_vec();
3267 let commit_ts_native = if self.has_column(SYS_COMMIT_TS) {
3268 Some(self.column(SYS_COMMIT_TS)?.to_vec())
3269 } else {
3270 None
3271 };
3272 let mut best: HashMap<u64, (u64, Option<HlcTimestamp>, usize)> = HashMap::new();
3273 for i in 0..n {
3274 let rid = int_at(&row_ids, i);
3275 let e = int_at(&epochs, i);
3276 let commit_ts = commit_ts_native
3277 .as_ref()
3278 .and_then(|vals| decode_commit_ts_value(vals.get(i)));
3279 if !snapshot.observes_row(Epoch(e), commit_ts) {
3280 continue;
3281 }
3282 best.entry(rid)
3283 .and_modify(|(be, bts, bi)| {
3284 if Snapshot::version_is_newer(Epoch(e), commit_ts, Epoch(*be), *bts) {
3285 *be = e;
3286 *bts = commit_ts;
3287 *bi = i;
3288 }
3289 })
3290 .or_insert((e, commit_ts, i));
3291 }
3292 let mut idxs: Vec<usize> = best.into_values().map(|(_, _, i)| i).collect();
3293 idxs.retain(|&i| !bool_at(&deleted, i));
3294 idxs.sort_unstable();
3295 Ok(idxs)
3296 }
3297
3298 pub fn gather_column(&mut self, column_id: u16, indices: &[usize]) -> Result<Vec<Value>> {
3303 if !self.dir.iter().any(|h| h.column_id == column_id) {
3304 return Ok(vec![Value::Null; indices.len()]);
3305 }
3306 let col = self.column(column_id)?;
3307 Ok(indices
3308 .iter()
3309 .map(|&i| col.get(i).cloned().unwrap_or(Value::Null))
3310 .collect())
3311 }
3312
3313 pub fn column_native(&mut self, column_id: u16) -> Result<columnar::NativeColumn> {
3318 use rayon::prelude::*;
3319 if column_id == SYS_EPOCH {
3320 if let Some(ov) = self.epoch_override {
3321 return Ok(columnar::NativeColumn::int64_constant(
3322 ov.0 as i64,
3323 self.row_count(),
3324 ));
3325 }
3326 }
3327 let ty = self.resolve_type(column_id);
3328 let n = self.row_count();
3329 let Some(ch) = self.dir.iter().find(|h| h.column_id == column_id) else {
3330 return Ok(columnar::null_native(ty, n));
3331 };
3332 let page_count = ch.page_count as usize;
3333 let page_rows: Vec<usize> = ch.page_stats.iter().map(|s| s.row_count as usize).collect();
3334 if page_count == 0 {
3335 return Ok(columnar::null_native(ty, n));
3336 }
3337 let parts: Vec<columnar::NativeColumn> = if self.mmap.is_some() && page_count > 1 {
3338 let reader: &RunReader = self;
3342 (0..page_count)
3343 .into_par_iter()
3344 .map(|seq| {
3345 let raw = reader.read_page_shared(column_id, seq)?;
3346 columnar::decode_page_native(ty.clone(), &raw, page_rows[seq])
3347 })
3348 .collect::<Result<Vec<_>>>()?
3349 } else {
3350 let mut out = Vec::with_capacity(page_count);
3351 for (seq, &pr) in page_rows.iter().enumerate() {
3352 let page = self.read_page(column_id, seq)?;
3353 out.push(columnar::decode_page_native(ty.clone(), &page, pr)?);
3354 }
3355 out
3356 };
3357 Ok(columnar::NativeColumn::concat(&parts))
3358 }
3359
3360 pub fn has_mmap(&self) -> bool {
3364 self.mmap.is_some()
3365 }
3366
3367 pub fn column_native_shared(&self, column_id: u16) -> Result<columnar::NativeColumn> {
3374 use rayon::prelude::*;
3375 if column_id == SYS_EPOCH {
3376 if let Some(ov) = self.epoch_override {
3377 return Ok(columnar::NativeColumn::int64_constant(
3378 ov.0 as i64,
3379 self.row_count(),
3380 ));
3381 }
3382 }
3383 let ty = self.resolve_type(column_id);
3384 let n = self.row_count();
3385 let Some(ch) = self.dir.iter().find(|h| h.column_id == column_id) else {
3386 return Ok(columnar::null_native(ty, n));
3387 };
3388 let page_count = ch.page_count as usize;
3389 let page_rows: Vec<usize> = ch.page_stats.iter().map(|s| s.row_count as usize).collect();
3390 if page_count == 0 {
3391 return Ok(columnar::null_native(ty, n));
3392 }
3393 #[cfg(unix)]
3397 {
3398 if let (Some(m), Some(first)) = (&self.mmap, ch.page_stats.first()) {
3399 let start = first.offset as usize;
3400 let end = (ch.page_region_offset as usize) + (ch.page_region_len as usize);
3401 if end > start {
3402 let _ = m.advise_range(memmap2::Advice::WillNeed, start, end - start);
3403 }
3404 }
3405 }
3406 let run_id = self.header.run_id;
3407 let mut parts_keys: Vec<(columnar::NativeColumn, Option<[u8; 32]>)> = if page_count > 1 {
3411 (0..page_count)
3412 .into_par_iter()
3413 .map(|seq| {
3414 self.decode_page_cached(ty.clone(), column_id, seq, page_rows[seq], run_id)
3415 })
3416 .collect::<Result<Vec<_>>>()?
3417 } else {
3418 vec![self.decode_page_cached(ty, column_id, 0, page_rows[0], run_id)?]
3419 };
3420 if let Some(cache) = &self.decoded_cache {
3423 for (col, key) in parts_keys.iter_mut() {
3424 if let Some(k) = key.take() {
3425 cache.lock(&k).insert(k, Arc::new(col.clone()));
3426 }
3427 }
3428 }
3429 let parts: Vec<columnar::NativeColumn> = parts_keys.into_iter().map(|(c, _)| c).collect();
3430 Ok(columnar::NativeColumn::concat(&parts))
3431 }
3432
3433 fn decode_page_cached(
3438 &self,
3439 ty: TypeId,
3440 column_id: u16,
3441 seq: usize,
3442 nrows: usize,
3443 run_id: u128,
3444 ) -> Result<(columnar::NativeColumn, Option<[u8; 32]>)> {
3445 let key = page_cache_key(self.table_id, run_id, column_id, seq);
3446 if let Some(cache) = &self.decoded_cache {
3447 if let Some(g) = cache.try_lock(&key) {
3448 if let Some(hit) = g.try_get(&key) {
3449 return Ok(((*hit).clone(), None));
3450 }
3451 }
3452 }
3453 let raw = self.read_page_shared(column_id, seq)?;
3454 let col = columnar::decode_page_native(ty, &raw, nrows)?;
3455 Ok((col, Some(key)))
3456 }
3457
3458 fn decode_page_native_cached(
3469 &mut self,
3470 ty: TypeId,
3471 column_id: u16,
3472 seq: usize,
3473 nrows: usize,
3474 ) -> Result<columnar::NativeColumn> {
3475 let key = page_cache_key(self.table_id, self.header.run_id, column_id, seq);
3476 if let Some(cache) = &self.decoded_cache {
3477 if let Some(hit) = cache.lock(&key).try_get(&key) {
3478 return Ok((*hit).clone());
3479 }
3480 }
3481 let raw = self.read_page(column_id, seq)?;
3482 let col = columnar::decode_page_native(ty, &raw, nrows)?;
3483 if let Some(cache) = &self.decoded_cache {
3484 cache
3485 .lock(&key)
3486 .insert(key, std::sync::Arc::new(col.clone()));
3487 }
3488 Ok(col)
3489 }
3490
3491 pub fn range_row_ids_i64(
3496 &mut self,
3497 column_id: u16,
3498 lo: i64,
3499 hi: i64,
3500 ) -> Result<std::collections::HashSet<u64>> {
3501 Ok(self
3502 .range_row_id_set_i64(column_id, lo, hi)?
3503 .into_sorted_vec()
3504 .into_iter()
3505 .collect())
3506 }
3507
3508 pub(crate) fn range_row_id_set_i64(
3509 &mut self,
3510 column_id: u16,
3511 lo: i64,
3512 hi: i64,
3513 ) -> Result<RowIdSet> {
3514 let info: Vec<(Option<i64>, Option<i64>, usize)> =
3515 match self.dir.iter().find(|h| h.column_id == column_id) {
3516 Some(ch) => ch
3517 .page_stats
3518 .iter()
3519 .map(|s| {
3520 (
3521 be_i64(s.min.as_deref()),
3522 be_i64(s.max.as_deref()),
3523 s.row_count as usize,
3524 )
3525 })
3526 .collect(),
3527 None => return Ok(RowIdSet::empty()),
3528 };
3529 let stats_pruneable =
3535 !self.col_encrypted(column_id) || self.header.encrypted_stats_offset != 0;
3536 let clean_contiguous = self.clean_contiguous_row_ids();
3537 let mut out = Vec::new();
3538 let mut page_start = 0usize;
3539 for (seq, (mn, mx, nrows)) in info.into_iter().enumerate() {
3540 let current_page_start = page_start;
3541 page_start += nrows;
3542 let skip = stats_pruneable
3544 && match (mn, mx) {
3545 (Some(mn), Some(mx)) => mx < lo || mn > hi,
3546 _ => true,
3547 };
3548 if skip {
3549 continue;
3550 }
3551 let val_page = self.read_page(column_id, seq)?;
3552 let vals =
3553 columnar::decode_page_native(self.resolve_type(column_id), &val_page, nrows)?;
3554 if let columnar::NativeColumn::Int64 { data: v, validity } = vals {
3555 if clean_contiguous {
3556 for (i, val) in v.iter().enumerate() {
3557 if columnar::validity_bit(&validity, i) && *val >= lo && *val <= hi {
3558 out.push(self.header.min_row_id + current_page_start as u64 + i as u64);
3559 }
3560 }
3561 } else {
3562 let rid_page = self.read_page(SYS_ROW_ID, seq)?;
3563 let rids = columnar::decode_page_native(TypeId::Int64, &rid_page, nrows)?;
3564 if let columnar::NativeColumn::Int64 { data: r, .. } = rids {
3565 for (i, val) in v.iter().enumerate() {
3566 if columnar::validity_bit(&validity, i) && *val >= lo && *val <= hi {
3567 out.push(r[i] as u64);
3568 }
3569 }
3570 }
3571 }
3572 }
3573 }
3574 Ok(RowIdSet::from_unsorted(out))
3575 }
3576
3577 pub fn range_row_ids_f64(
3580 &mut self,
3581 column_id: u16,
3582 lo: f64,
3583 lo_inclusive: bool,
3584 hi: f64,
3585 hi_inclusive: bool,
3586 ) -> Result<std::collections::HashSet<u64>> {
3587 Ok(self
3588 .range_row_id_set_f64(column_id, lo, lo_inclusive, hi, hi_inclusive)?
3589 .into_sorted_vec()
3590 .into_iter()
3591 .collect())
3592 }
3593
3594 pub(crate) fn range_row_id_set_f64(
3595 &mut self,
3596 column_id: u16,
3597 lo: f64,
3598 lo_inclusive: bool,
3599 hi: f64,
3600 hi_inclusive: bool,
3601 ) -> Result<RowIdSet> {
3602 let info: Vec<(Option<f64>, Option<f64>, usize)> =
3603 match self.dir.iter().find(|h| h.column_id == column_id) {
3604 Some(ch) => ch
3605 .page_stats
3606 .iter()
3607 .map(|s| {
3608 (
3609 be_f64(s.min.as_deref()),
3610 be_f64(s.max.as_deref()),
3611 s.row_count as usize,
3612 )
3613 })
3614 .collect(),
3615 None => return Ok(RowIdSet::empty()),
3616 };
3617 let stats_pruneable =
3623 !self.col_encrypted(column_id) || self.header.encrypted_stats_offset != 0;
3624 let clean_contiguous = self.clean_contiguous_row_ids();
3625 let mut out = Vec::new();
3626 let mut page_start = 0usize;
3627 for (seq, (mn, mx, nrows)) in info.into_iter().enumerate() {
3628 let current_page_start = page_start;
3629 page_start += nrows;
3630 let skip = stats_pruneable
3633 && match (mn, mx) {
3634 (Some(mn), Some(mx)) => {
3635 let skip_lo = mx < lo || (!lo_inclusive && mx == lo);
3636 let skip_hi = mn > hi || (!hi_inclusive && mn == hi);
3637 skip_lo || skip_hi
3638 }
3639 _ => true,
3640 };
3641 if skip {
3642 continue;
3643 }
3644 let val_page = self.read_page(column_id, seq)?;
3645 let vals = columnar::decode_page_native(TypeId::Float64, &val_page, nrows)?;
3646 if let columnar::NativeColumn::Float64 { data: v, validity } = vals {
3647 if clean_contiguous {
3648 for (i, val) in v.iter().enumerate() {
3649 if !columnar::validity_bit(&validity, i) || val.is_nan() {
3650 continue;
3651 }
3652 let ok_lo = if lo_inclusive { *val >= lo } else { *val > lo };
3653 let ok_hi = if hi_inclusive { *val <= hi } else { *val < hi };
3654 if ok_lo && ok_hi {
3655 out.push(self.header.min_row_id + current_page_start as u64 + i as u64);
3656 }
3657 }
3658 } else {
3659 let rid_page = self.read_page(SYS_ROW_ID, seq)?;
3660 let rids = columnar::decode_page_native(TypeId::Int64, &rid_page, nrows)?;
3661 if let columnar::NativeColumn::Int64 { data: r, .. } = rids {
3662 for (i, val) in v.iter().enumerate() {
3663 if !columnar::validity_bit(&validity, i) || val.is_nan() {
3664 continue;
3665 }
3666 let ok_lo = if lo_inclusive { *val >= lo } else { *val > lo };
3667 let ok_hi = if hi_inclusive { *val <= hi } else { *val < hi };
3668 if ok_lo && ok_hi {
3669 out.push(r[i] as u64);
3670 }
3671 }
3672 }
3673 }
3674 }
3675 }
3676 Ok(RowIdSet::from_unsorted(out))
3677 }
3678
3679 pub(crate) fn null_row_id_set(&mut self, column_id: u16, want_nulls: bool) -> Result<RowIdSet> {
3684 let stats: Vec<(usize, usize)> = match self.dir.iter().find(|h| h.column_id == column_id) {
3685 Some(ch) => ch
3686 .page_stats
3687 .iter()
3688 .map(|s| (s.null_count as usize, s.row_count as usize))
3689 .collect(),
3690 None => return Ok(RowIdSet::empty()),
3691 };
3692 let ty = self.resolve_type(column_id);
3693 let clean_contiguous = self.clean_contiguous_row_ids();
3694 let mut out = Vec::new();
3695 let mut page_start = 0usize;
3696 for (seq, (null_count, nrows)) in stats.into_iter().enumerate() {
3697 let current_page_start = page_start;
3698 page_start += nrows;
3699 if want_nulls && null_count == 0 {
3701 continue;
3702 }
3703 if !want_nulls && null_count == nrows {
3704 continue;
3705 }
3706 let val_page = self.read_page(column_id, seq)?;
3707 let col = columnar::decode_page_native(ty.clone(), &val_page, nrows)?;
3708 let validity = col.validity();
3709 if clean_contiguous {
3710 for i in 0..nrows {
3711 let is_null = !columnar::validity_bit(validity, i);
3712 if is_null == want_nulls {
3713 out.push(self.header.min_row_id + current_page_start as u64 + i as u64);
3714 }
3715 }
3716 } else {
3717 let rid_page = self.read_page(SYS_ROW_ID, seq)?;
3718 let rids = columnar::decode_page_native(TypeId::Int64, &rid_page, nrows)?;
3719 if let columnar::NativeColumn::Int64 { data: r, .. } = rids {
3720 for (i, &rid) in r.iter().enumerate().take(nrows) {
3721 let is_null = !columnar::validity_bit(validity, i);
3722 if is_null == want_nulls {
3723 out.push(rid as u64);
3724 }
3725 }
3726 }
3727 }
3728 }
3729 Ok(RowIdSet::from_unsorted(out))
3730 }
3731
3732 pub fn visible_indices_native(&mut self, snapshot: Epoch) -> Result<Vec<usize>> {
3733 self.visible_indices_native_at(Snapshot::at(snapshot))
3734 }
3735
3736 pub fn visible_indices_native_at(&mut self, snapshot: Snapshot) -> Result<Vec<usize>> {
3738 let n = self.row_count();
3739 if n == 0 {
3740 return Ok(Vec::new());
3741 }
3742 let (row_ids, epochs, deleted) = self.system_columns_native()?;
3743 let commit_ts_native = if self.has_column(SYS_COMMIT_TS) {
3744 Some(self.column_native_shared(SYS_COMMIT_TS)?)
3745 } else {
3746 None
3747 };
3748 let mut best: HashMap<u64, (u64, Option<HlcTimestamp>, usize)> = HashMap::new();
3749 for i in 0..n {
3750 let rid = row_ids[i] as u64;
3751 let e = epochs[i] as u64;
3752 let commit_ts = commit_ts_native
3753 .as_ref()
3754 .and_then(|col| decode_commit_ts_value(col.value_at(i).as_ref()));
3755 if !snapshot.observes_row(Epoch(e), commit_ts) {
3756 continue;
3757 }
3758 best.entry(rid)
3759 .and_modify(|(be, bts, bi)| {
3760 if Snapshot::version_is_newer(Epoch(e), commit_ts, Epoch(*be), *bts) {
3761 *be = e;
3762 *bts = commit_ts;
3763 *bi = i;
3764 }
3765 })
3766 .or_insert((e, commit_ts, i));
3767 }
3768 let mut idxs: Vec<usize> = best.into_values().map(|(_, _, i)| i).collect();
3769 idxs.retain(|&i| deleted[i] == 0);
3770 idxs.sort_unstable();
3771 Ok(idxs)
3772 }
3773
3774 pub fn range_row_ids_visible_i64(
3783 &mut self,
3784 column_id: u16,
3785 lo: i64,
3786 hi: i64,
3787 snapshot: Epoch,
3788 ) -> Result<Vec<u64>> {
3789 self.range_row_ids_visible_i64_at(column_id, lo, hi, Snapshot::at(snapshot))
3790 }
3791
3792 pub fn range_row_ids_visible_i64_at(
3794 &mut self,
3795 column_id: u16,
3796 lo: i64,
3797 hi: i64,
3798 snapshot: Snapshot,
3799 ) -> Result<Vec<u64>> {
3800 let stats: Vec<(Option<i64>, Option<i64>, usize)> = match self.column_page_stats(column_id)
3801 {
3802 Some(s) => s
3803 .iter()
3804 .map(|st| {
3805 (
3806 be_i64(st.min.as_deref()),
3807 be_i64(st.max.as_deref()),
3808 st.row_count as usize,
3809 )
3810 })
3811 .collect(),
3812 None => return Ok(Vec::new()),
3813 };
3814 let stats_pruneable =
3820 !self.col_encrypted(column_id) || self.header.encrypted_stats_offset != 0;
3821 let (positions, rids) = self.visible_positions_with_rids_at(snapshot)?;
3822 let mut out: Vec<u64> = Vec::new();
3823 let mut vis = 0usize;
3824 let mut page_start = 0usize;
3825 for (seq, &(mn, mx, nrows)) in stats.iter().enumerate() {
3826 let page_end = page_start + nrows;
3827 let skip = stats_pruneable
3829 && match (mn, mx) {
3830 (Some(mn), Some(mx)) => mx < lo || mn > hi,
3831 _ => true, };
3833 if !skip {
3834 let val_page = self.read_page(column_id, seq)?;
3835 let vals = columnar::decode_page_native(TypeId::Int64, &val_page, nrows)?;
3836 if let columnar::NativeColumn::Int64 { data: v, validity } = vals {
3837 while vis < positions.len() && positions[vis] < page_end {
3838 let local = positions[vis] - page_start;
3839 if columnar::validity_bit(&validity, local)
3840 && v[local] >= lo
3841 && v[local] <= hi
3842 {
3843 out.push(rids[vis] as u64);
3844 }
3845 vis += 1;
3846 }
3847 }
3848 } else {
3849 while vis < positions.len() && positions[vis] < page_end {
3850 vis += 1;
3851 }
3852 }
3853 page_start = page_end;
3854 }
3855 Ok(out)
3856 }
3857
3858 pub fn range_row_ids_visible_f64(
3861 &mut self,
3862 column_id: u16,
3863 lo: f64,
3864 lo_inclusive: bool,
3865 hi: f64,
3866 hi_inclusive: bool,
3867 snapshot: Epoch,
3868 ) -> Result<Vec<u64>> {
3869 self.range_row_ids_visible_f64_at(
3870 column_id,
3871 lo,
3872 lo_inclusive,
3873 hi,
3874 hi_inclusive,
3875 Snapshot::at(snapshot),
3876 )
3877 }
3878
3879 pub fn range_row_ids_visible_f64_at(
3881 &mut self,
3882 column_id: u16,
3883 lo: f64,
3884 lo_inclusive: bool,
3885 hi: f64,
3886 hi_inclusive: bool,
3887 snapshot: Snapshot,
3888 ) -> Result<Vec<u64>> {
3889 let stats: Vec<(Option<f64>, Option<f64>, usize)> = match self.column_page_stats(column_id)
3890 {
3891 Some(s) => s
3892 .iter()
3893 .map(|st| {
3894 (
3895 be_f64(st.min.as_deref()),
3896 be_f64(st.max.as_deref()),
3897 st.row_count as usize,
3898 )
3899 })
3900 .collect(),
3901 None => return Ok(Vec::new()),
3902 };
3903 let stats_pruneable =
3909 !self.col_encrypted(column_id) || self.header.encrypted_stats_offset != 0;
3910 let (positions, rids) = self.visible_positions_with_rids_at(snapshot)?;
3911 let mut out: Vec<u64> = Vec::new();
3912 let mut vis = 0usize;
3913 let mut page_start = 0usize;
3914 for (seq, &(mn, mx, nrows)) in stats.iter().enumerate() {
3915 let page_end = page_start + nrows;
3916 let skip = stats_pruneable
3917 && match (mn, mx) {
3918 (Some(mn), Some(mx)) => {
3919 let skip_lo = mx < lo || (!lo_inclusive && mx == lo);
3920 let skip_hi = mn > hi || (!hi_inclusive && mn == hi);
3921 skip_lo || skip_hi
3922 }
3923 _ => true,
3924 };
3925 if !skip {
3926 let val_page = self.read_page(column_id, seq)?;
3927 let vals = columnar::decode_page_native(TypeId::Float64, &val_page, nrows)?;
3928 if let columnar::NativeColumn::Float64 { data: v, validity } = vals {
3929 while vis < positions.len() && positions[vis] < page_end {
3930 let local = positions[vis] - page_start;
3931 if columnar::validity_bit(&validity, local) && !v[local].is_nan() {
3932 let val = v[local];
3933 let ok_lo = if lo_inclusive { val >= lo } else { val > lo };
3934 let ok_hi = if hi_inclusive { val <= hi } else { val < hi };
3935 if ok_lo && ok_hi {
3936 out.push(rids[vis] as u64);
3937 }
3938 }
3939 vis += 1;
3940 }
3941 }
3942 } else {
3943 while vis < positions.len() && positions[vis] < page_end {
3944 vis += 1;
3945 }
3946 }
3947 page_start = page_end;
3948 }
3949 Ok(out)
3950 }
3951
3952 pub fn null_row_ids_visible(
3958 &mut self,
3959 column_id: u16,
3960 want_nulls: bool,
3961 snapshot: Epoch,
3962 ) -> Result<Vec<u64>> {
3963 self.null_row_ids_visible_at(column_id, want_nulls, Snapshot::at(snapshot))
3964 }
3965
3966 pub fn null_row_ids_visible_at(
3968 &mut self,
3969 column_id: u16,
3970 want_nulls: bool,
3971 snapshot: Snapshot,
3972 ) -> Result<Vec<u64>> {
3973 let stats: Vec<(usize, usize)> = match self.column_page_stats(column_id) {
3974 Some(s) => s
3975 .iter()
3976 .map(|st| (st.null_count as usize, st.row_count as usize))
3977 .collect(),
3978 None => return Ok(Vec::new()),
3979 };
3980 let ty = self.resolve_type(column_id);
3981 let (positions, rids) = self.visible_positions_with_rids_at(snapshot)?;
3982 let mut out: Vec<u64> = Vec::new();
3983 let mut vis = 0usize;
3984 let mut page_start = 0usize;
3985 for (seq, &(null_count, nrows)) in stats.iter().enumerate() {
3986 let page_end = page_start + nrows;
3987 let skip = (want_nulls && null_count == 0) || (!want_nulls && null_count == nrows);
3988 if !skip {
3989 let val_page = self.read_page(column_id, seq)?;
3990 let col = columnar::decode_page_native(ty.clone(), &val_page, nrows)?;
3991 let validity = col.validity();
3992 while vis < positions.len() && positions[vis] < page_end {
3993 let local = positions[vis] - page_start;
3994 let is_null = !columnar::validity_bit(validity, local);
3995 if is_null == want_nulls {
3996 out.push(rids[vis] as u64);
3997 }
3998 vis += 1;
3999 }
4000 } else {
4001 while vis < positions.len() && positions[vis] < page_end {
4002 vis += 1;
4003 }
4004 }
4005 page_start = page_end;
4006 }
4007 Ok(out)
4008 }
4009
4010 pub fn tombstoned_row_ids(&mut self, snapshot: Epoch) -> Result<Vec<u64>> {
4017 self.tombstoned_row_ids_at(Snapshot::at(snapshot))
4018 }
4019
4020 pub fn tombstoned_row_ids_at(&mut self, snapshot: Snapshot) -> Result<Vec<u64>> {
4022 let n = self.row_count();
4023 if n == 0 {
4024 return Ok(Vec::new());
4025 }
4026 if self.is_clean()
4028 && self.epoch_override.is_none()
4029 && self.header.epoch_created <= snapshot.epoch.0
4030 && (!snapshot.uses_hlc_authority() || !self.has_column(SYS_COMMIT_TS))
4031 {
4032 return Ok(Vec::new());
4033 }
4034 let (row_ids, epochs, deleted) = self.system_columns_native()?;
4035 let commit_ts_native = if self.has_column(SYS_COMMIT_TS) {
4036 Some(self.column_native_shared(SYS_COMMIT_TS)?)
4037 } else {
4038 None
4039 };
4040 let mut out = Vec::new();
4041 let mut i = 0;
4042 while i < n {
4043 let rid = row_ids[i] as u64;
4044 let mut best: Option<usize> = None;
4045 let mut j = i;
4046 while j < n && row_ids[j] as u64 == rid {
4047 let e = epochs[j] as u64;
4048 let commit_ts = commit_ts_native
4049 .as_ref()
4050 .and_then(|col| decode_commit_ts_value(col.value_at(j).as_ref()));
4051 if snapshot.observes_row(Epoch(e), commit_ts)
4052 && (best.is_none_or(|cur| {
4053 Snapshot::version_is_newer(
4054 Epoch(e),
4055 commit_ts,
4056 Epoch(epochs[cur] as u64),
4057 commit_ts_native
4058 .as_ref()
4059 .and_then(|col| decode_commit_ts_value(col.value_at(cur).as_ref())),
4060 )
4061 }))
4062 {
4063 best = Some(j);
4064 }
4065 j += 1;
4066 }
4067 if let Some(b) = best {
4068 if deleted[b] != 0 {
4069 out.push(rid);
4070 }
4071 }
4072 i = j;
4073 }
4074 Ok(out)
4075 }
4076
4077 pub fn visible_positions_with_rids(
4081 &mut self,
4082 snapshot: Epoch,
4083 ) -> Result<(Vec<usize>, Vec<i64>)> {
4084 self.visible_positions_with_rids_at(Snapshot::at(snapshot))
4085 }
4086
4087 pub fn visible_positions_with_rids_at(
4091 &mut self,
4092 snapshot: Snapshot,
4093 ) -> Result<(Vec<usize>, Vec<i64>)> {
4094 let n = self.row_count();
4095 if n == 0 {
4096 return Ok((Vec::new(), Vec::new()));
4097 }
4098 if self.is_clean()
4111 && self.epoch_override.is_none()
4112 && self.header.epoch_created <= snapshot.epoch.0
4113 {
4114 let row_ids = match self.column_native_shared(SYS_ROW_ID)? {
4115 columnar::NativeColumn::Int64 { data, .. } => data,
4116 _ => return Err(MongrelError::InvalidArgument("sys row_id not int64".into())),
4117 };
4118 if !snapshot.uses_hlc_authority() || !self.has_column(SYS_COMMIT_TS) {
4120 let positions: Vec<usize> = (0..n).collect();
4121 return Ok((positions, row_ids));
4122 }
4123 let commit_ts_native = self.column_native_shared(SYS_COMMIT_TS)?;
4125 let mut idxs = Vec::with_capacity(n);
4126 let mut rids = Vec::with_capacity(n);
4127 for (i, &rid) in row_ids.iter().enumerate().take(n) {
4128 let ts = decode_commit_ts_value(commit_ts_native.value_at(i).as_ref());
4129 if snapshot.observes_row(Epoch(self.header.epoch_created), ts) {
4131 idxs.push(i);
4132 rids.push(rid);
4133 }
4134 }
4135 return Ok((idxs, rids));
4136 }
4137 let (row_ids, epochs, deleted) = self.system_columns_native()?;
4138 let commit_ts_native = if self.has_column(SYS_COMMIT_TS) {
4139 Some(self.column_native_shared(SYS_COMMIT_TS)?)
4140 } else {
4141 None
4142 };
4143 let mut idxs: Vec<usize> = Vec::new();
4150 let mut i = 0;
4151 while i < n {
4152 let rid = row_ids[i] as u64;
4153 let mut best: Option<usize> = None;
4154 let mut j = i;
4155 while j < n && row_ids[j] as u64 == rid {
4156 let e = epochs[j] as u64;
4157 let commit_ts = commit_ts_native
4158 .as_ref()
4159 .and_then(|col| decode_commit_ts_value(col.value_at(j).as_ref()));
4160 if snapshot.observes_row(Epoch(e), commit_ts) {
4161 let candidate = j;
4162 let is_newer = best.is_none_or(|cur| {
4163 Snapshot::version_is_newer(
4164 Epoch(e),
4165 commit_ts,
4166 Epoch(epochs[cur] as u64),
4167 commit_ts_native
4168 .as_ref()
4169 .and_then(|col| decode_commit_ts_value(col.value_at(cur).as_ref())),
4170 )
4171 });
4172 if best.is_none() || is_newer {
4173 best = Some(candidate);
4174 }
4175 }
4176 j += 1;
4177 }
4178 if let Some(b) = best {
4179 if deleted[b] == 0 {
4180 idxs.push(b);
4181 }
4182 }
4183 i = j;
4184 }
4185 let rids: Vec<i64> = idxs.iter().map(|&k| row_ids[k]).collect();
4186 Ok((idxs, rids))
4187 }
4188
4189 pub fn page_row_counts(&self, column_id: u16) -> Result<Vec<usize>> {
4193 Ok(self
4194 .find_header(column_id)?
4195 .page_stats
4196 .iter()
4197 .map(|s| s.row_count as usize)
4198 .collect())
4199 }
4200
4201 pub fn has_column(&self, column_id: u16) -> bool {
4204 self.dir.iter().any(|h| h.column_id == column_id)
4205 }
4206
4207 pub fn column_page_stats(&self, column_id: u16) -> Option<&[crate::page::PageStat]> {
4211 self.dir
4212 .iter()
4213 .find(|h| h.column_id == column_id)
4214 .map(|ch| ch.page_stats.as_slice())
4215 }
4216
4217 pub(crate) fn system_columns_native(&mut self) -> Result<(Vec<i64>, Vec<i64>, Vec<u8>)> {
4222 let row_ids = match self.column_native_shared(SYS_ROW_ID)? {
4223 columnar::NativeColumn::Int64 { data, .. } => data,
4224 _ => return Err(MongrelError::InvalidArgument("sys row_id not int64".into())),
4225 };
4226 let epochs = match self.column_native_shared(SYS_EPOCH)? {
4227 columnar::NativeColumn::Int64 { data, .. } => data,
4228 _ => return Err(MongrelError::InvalidArgument("sys epoch not int64".into())),
4229 };
4230 let deleted = match self.column_native_shared(SYS_DELETED)? {
4231 columnar::NativeColumn::Bool { data, .. } => data,
4232 _ => return Err(MongrelError::InvalidArgument("sys deleted not bool".into())),
4233 };
4234 Ok((row_ids, epochs, deleted))
4235 }
4236
4237 pub fn visible_versions(&mut self, snapshot: Epoch) -> Result<Vec<Row>> {
4246 self.visible_versions_at(Snapshot::at(snapshot))
4247 }
4248
4249 pub fn visible_versions_at(&mut self, snapshot: Snapshot) -> Result<Vec<Row>> {
4253 let n = self.row_count();
4254 if n == 0 {
4255 return Ok(Vec::new());
4256 }
4257 let row_ids = self.column(SYS_ROW_ID)?.to_vec();
4258 let epochs = self.column(SYS_EPOCH)?.to_vec();
4259 let commit_ts_native = if self.has_column(SYS_COMMIT_TS) {
4260 Some(self.column(SYS_COMMIT_TS)?.to_vec())
4261 } else {
4262 None
4263 };
4264 let mut best: HashMap<u64, (u64, Option<HlcTimestamp>, usize)> = HashMap::new();
4265 for i in 0..n {
4266 let rid = int_at(&row_ids, i);
4267 let e = int_at(&epochs, i);
4268 let commit_ts = commit_ts_native
4269 .as_ref()
4270 .and_then(|vals| decode_commit_ts_value(vals.get(i)));
4271 if !snapshot.observes_row(Epoch(e), commit_ts) {
4272 continue;
4273 }
4274 best.entry(rid)
4275 .and_modify(|cur| {
4276 if Snapshot::version_is_newer(Epoch(e), commit_ts, Epoch(cur.0), cur.1) {
4277 *cur = (e, commit_ts, i);
4278 }
4279 })
4280 .or_insert((e, commit_ts, i));
4281 }
4282 let mut picks: Vec<usize> = best.into_values().map(|(_, _, i)| i).collect();
4283 picks.sort();
4284 let mut out = Vec::with_capacity(picks.len());
4285 for i in picks {
4286 out.push(self.materialize(i)?);
4287 }
4288 Ok(out)
4289 }
4290
4291 pub fn visible_rows(&mut self, snapshot: Epoch) -> Result<Vec<Row>> {
4293 Ok(self
4294 .visible_versions_at(Snapshot::at(snapshot))?
4295 .into_iter()
4296 .filter(|r| !r.deleted)
4297 .collect())
4298 }
4299
4300 pub(crate) fn materialize(&mut self, index: usize) -> Result<Row> {
4301 let row_id = RowId(int_at(self.column(SYS_ROW_ID)?, index));
4302 let epoch = Epoch(int_at(self.column(SYS_EPOCH)?, index));
4303 let deleted = bool_at(self.column(SYS_DELETED)?, index);
4304 let commit_ts = self.commit_ts_at(index)?;
4305 let col_ids: Vec<u16> = self.schema.columns.iter().map(|c| c.id).collect();
4306 let mut columns = HashMap::new();
4307 for id in col_ids {
4308 let val = if self.dir.iter().any(|h| h.column_id == id) {
4309 self.column(id)?.get(index).cloned().unwrap_or(Value::Null)
4310 } else {
4311 Value::Null
4313 };
4314 columns.insert(id, val);
4315 }
4316 Ok(Row {
4317 row_id,
4318 committed_epoch: epoch,
4319 columns,
4320 deleted,
4321 commit_ts,
4322 })
4323 }
4324
4325 pub(crate) fn materialize_batch(&mut self, indices: &[usize]) -> Result<Vec<Row>> {
4334 if indices.is_empty() {
4335 return Ok(Vec::new());
4336 }
4337 use std::collections::HashMap;
4338 let rid_col = self.column_native_shared(SYS_ROW_ID)?;
4339 let epoch_col = self.column_native_shared(SYS_EPOCH)?;
4340 let del_col = self.column_native_shared(SYS_DELETED)?;
4341 let commit_ts_col = if self.has_column(SYS_COMMIT_TS) {
4342 Some(self.column_native_shared(SYS_COMMIT_TS)?)
4343 } else {
4344 None
4345 };
4346 let mut user: HashMap<u16, columnar::NativeColumn> = HashMap::new();
4347 let present: Vec<u16> = self
4348 .schema
4349 .columns
4350 .iter()
4351 .map(|c| c.id)
4352 .filter(|id| self.dir.iter().any(|h| h.column_id == *id))
4353 .collect();
4354 for id in present {
4355 user.insert(id, self.column_native(id)?);
4356 }
4357 let i64_at = |col: &columnar::NativeColumn, i: usize| -> i64 {
4358 match col {
4359 columnar::NativeColumn::Int64 { data, .. } => data.get(i).copied().unwrap_or(0),
4360 _ => 0,
4361 }
4362 };
4363 let bool_at_native = |col: &columnar::NativeColumn, i: usize| -> bool {
4364 match col {
4365 columnar::NativeColumn::Bool { data, .. } => {
4366 data.get(i).copied().map(|b| b != 0).unwrap_or(false)
4367 }
4368 _ => false,
4369 }
4370 };
4371 let mut rows = Vec::with_capacity(indices.len());
4372 for &idx in indices {
4373 let row_id = RowId(i64_at(&rid_col, idx) as u64);
4374 let epoch = Epoch(i64_at(&epoch_col, idx) as u64);
4375 let deleted = bool_at_native(&del_col, idx);
4376 let commit_ts = commit_ts_col
4377 .as_ref()
4378 .and_then(|col| decode_commit_ts_value(col.value_at(idx).as_ref()));
4379 let mut columns = HashMap::with_capacity(self.schema.columns.len());
4380 for cdef in self.schema.columns.iter() {
4381 let val = match user.get(&cdef.id) {
4382 Some(col) => col.value_at(idx).unwrap_or(Value::Null),
4383 None => Value::Null,
4384 };
4385 columns.insert(cdef.id, val);
4386 }
4387 rows.push(Row {
4388 row_id,
4389 committed_epoch: epoch,
4390 columns,
4391 deleted,
4392 commit_ts,
4393 });
4394 }
4395 Ok(rows)
4396 }
4397}
4398
4399fn int_at(vals: &[Value], i: usize) -> u64 {
4400 match vals.get(i) {
4401 Some(Value::Int64(x)) => *x as u64,
4402 _ => 0,
4403 }
4404}
4405
4406fn bool_at(vals: &[Value], i: usize) -> bool {
4407 matches!(vals.get(i), Some(Value::Bool(true)))
4408}
4409
4410#[cfg(test)]
4411mod tests {
4412 use super::*;
4413 use crate::columnar::NativeColumn;
4414 use crate::memtable::Value;
4415 use crate::rowid::RowId;
4416 use crate::schema::{ColumnDef, ColumnFlags};
4417 use tempfile::tempdir;
4418
4419 fn schema() -> Schema {
4420 Schema {
4421 schema_id: 1,
4422 columns: vec![
4423 ColumnDef {
4424 id: 1,
4425 name: "id".into(),
4426 ty: TypeId::Int64,
4427 flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
4428 default_value: None,
4429 embedding_source: None,
4430 },
4431 ColumnDef {
4432 id: 2,
4433 name: "name".into(),
4434 ty: TypeId::Bytes,
4435 flags: ColumnFlags::empty(),
4436 default_value: None,
4437 embedding_source: None,
4438 },
4439 ],
4440 indexes: Vec::new(),
4441 colocation: vec![],
4442 constraints: Default::default(),
4443 clustered: false,
4444 }
4445 }
4446
4447 fn rows() -> Vec<Row> {
4448 vec![
4449 Row::new(RowId(1), Epoch(10))
4450 .with_column(1, Value::Int64(1))
4451 .with_column(2, Value::Bytes(b"alice".to_vec())),
4452 Row::new(RowId(2), Epoch(10))
4453 .with_column(1, Value::Int64(2))
4454 .with_column(2, Value::Bytes(b"bob".to_vec())),
4455 Row::new(RowId(3), Epoch(10))
4456 .with_column(1, Value::Int64(3))
4457 .with_column(2, Value::Bytes(b"carol".to_vec())),
4458 ]
4459 }
4460
4461 #[test]
4462 fn flush_then_read_mvcc() {
4463 let dir = tempdir().unwrap();
4464 let path = dir.path().join("r-1.sr");
4465 let header = RunWriter::new(&schema(), 1, Epoch(10), 0)
4466 .write(&path, &rows())
4467 .unwrap();
4468 assert_eq!(header.row_count, 3);
4469
4470 let mut r = RunReader::open(&path, schema(), None).unwrap();
4471 let (e, row) = r.get_version(RowId(2), Epoch(20)).unwrap().unwrap();
4473 assert_eq!(e, Epoch(10));
4474 assert_eq!(row.row_id, RowId(2));
4475 assert!(matches!(row.columns.get(&2), Some(Value::Bytes(_))));
4476 assert!(r.get_version(RowId(99), Epoch(20)).unwrap().is_none());
4478 let all = r.visible_rows(Epoch(20)).unwrap();
4480 assert_eq!(all.len(), 3);
4481 assert!(!r.has_column(SYS_COMMIT_TS));
4483 assert!(all.iter().all(|row| row.commit_ts.is_none()));
4484 }
4485
4486 #[test]
4489 fn p05_t3_sys_commit_ts_preserved_on_flush_and_legacy_is_none() {
4490 let dir = tempdir().unwrap();
4491 let stamped_path = dir.path().join("r-hlc.sr");
4492 let stamp = HlcTimestamp {
4493 physical_micros: 42_000_000,
4494 logical: 7,
4495 node_tiebreaker: 3,
4496 };
4497 let stamped = vec![
4498 Row::new_with_hlc(RowId(1), Epoch(10), stamp)
4499 .with_column(1, Value::Int64(1))
4500 .with_column(2, Value::Bytes(b"alice".to_vec())),
4501 Row::new(RowId(2), Epoch(11)) .with_column(1, Value::Int64(2))
4503 .with_column(2, Value::Bytes(b"bob".to_vec())),
4504 ];
4505 RunWriter::new(&schema(), 9, Epoch(11), 0)
4506 .write(&stamped_path, &stamped)
4507 .unwrap();
4508 let mut reader = RunReader::open(&stamped_path, schema(), None).unwrap();
4509 assert!(
4510 reader.has_column(SYS_COMMIT_TS),
4511 "stamped flush must emit optional SYS_COMMIT_TS"
4512 );
4513 let (_, row1) = reader
4514 .get_version(RowId(1), Epoch(20))
4515 .unwrap()
4516 .expect("row 1");
4517 assert_eq!(row1.commit_ts, Some(stamp));
4518 let (_, row2) = reader
4519 .get_version(RowId(2), Epoch(20))
4520 .unwrap()
4521 .expect("row 2");
4522 assert_eq!(row2.commit_ts, None, "Null stamp cell stays None");
4523 let versions = reader.visible_versions(Epoch(20)).unwrap();
4524 assert_eq!(versions.len(), 2);
4525 assert_eq!(
4526 versions
4527 .iter()
4528 .find(|r| r.row_id == RowId(1))
4529 .and_then(|r| r.commit_ts),
4530 Some(stamp)
4531 );
4532
4533 let legacy_path = dir.path().join("r-legacy.sr");
4535 RunWriter::new(&schema(), 10, Epoch(10), 0)
4536 .write(&legacy_path, &rows())
4537 .unwrap();
4538 let mut legacy = RunReader::open(&legacy_path, schema(), None).unwrap();
4539 assert!(!legacy.has_column(SYS_COMMIT_TS));
4540 let all = legacy.visible_versions(Epoch(20)).unwrap();
4541 assert!(all.iter().all(|r| r.commit_ts.is_none()));
4542 }
4543
4544 #[test]
4545 fn visible_version_cursor_preserves_order_snapshot_and_tombstones() {
4546 let dir = tempdir().unwrap();
4547 let path = dir.path().join("r-cursor.sr");
4548 let mut tombstone = Row::new(RowId(2), Epoch(2));
4549 tombstone.deleted = true;
4550 let versions = vec![
4551 Row::new(RowId(1), Epoch(4)).with_column(1, Value::Int64(10)),
4552 Row::new(RowId(2), Epoch(1)).with_column(1, Value::Int64(2)),
4553 tombstone,
4554 Row::new(RowId(3), Epoch(1)).with_column(1, Value::Int64(20)),
4555 Row::new(RowId(3), Epoch(3)).with_column(1, Value::Int64(23)),
4556 Row::new(RowId(4), Epoch(4)).with_column(1, Value::Int64(30)),
4557 ];
4558 RunWriter::new(&schema(), 7, Epoch(4), 0)
4559 .write(&path, &versions)
4560 .unwrap();
4561
4562 let control = crate::ExecutionControl::new(None);
4563 let mut cursor = RunReader::open(&path, schema(), None)
4564 .unwrap()
4565 .into_visible_version_cursor(Epoch(2))
4566 .unwrap();
4567 let first = cursor.next_visible_version(&control).unwrap().unwrap();
4568 assert_eq!(first.row_id, RowId(2));
4569 assert!(first.deleted);
4570 assert_eq!(first.committed_epoch, Epoch(2));
4571 let second = cursor.next_visible_version(&control).unwrap().unwrap();
4572 assert_eq!(second.row_id, RowId(3));
4573 assert_eq!(second.committed_epoch, Epoch(1));
4574 let row = cursor.materialize(second, &control).unwrap();
4575 assert_eq!(row.columns.get(&1), Some(&Value::Int64(20)));
4576 assert!(cursor.next_visible_version(&control).unwrap().is_none());
4577 }
4578
4579 #[test]
4580 fn learned_index_was_stored() {
4581 let dir = tempdir().unwrap();
4582 let path = dir.path().join("r-2.sr");
4583 RunWriter::new(&schema(), 2, Epoch(1), 0)
4584 .write(&path, &rows())
4585 .unwrap();
4586 let header = read_header(&path).unwrap();
4587 assert!(
4588 header.index_trailer_offset != 0,
4589 "trailer should be present"
4590 );
4591 }
4592
4593 #[test]
4594 fn low_level_container_still_round_trips() {
4595 let dir = tempdir().unwrap();
4596 let path = dir.path().join("r-3.sr");
4597 let cols = vec![ColumnPayload {
4598 column_id: 1,
4599 type_id_tag: 8,
4600 encoding: Encoding::Plain,
4601 pages: vec![vec![1, 2, 3, 4]],
4602 page_stats: Vec::new(),
4603 }];
4604 let header = write_run(
4605 &path,
4606 &RunSpec {
4607 run_id: 1,
4608 schema_id: 1,
4609 epoch_created: 1,
4610 level: 0,
4611 flags: 0,
4612 sort_key_column_id: SORT_KEY_ROW_ID,
4613 row_count: 1,
4614 min_row_id: 0,
4615 max_row_id: 0,
4616 columns: &cols,
4617 },
4618 )
4619 .unwrap();
4620 let back = read_header(&path).unwrap();
4621 assert_eq!(back.content_hash, header.content_hash);
4622 assert_eq!(read_column_dir(&path, &back).unwrap().len(), 1);
4623 }
4624
4625 #[test]
4626 fn detects_corruption() {
4627 let dir = tempdir().unwrap();
4628 let path = dir.path().join("r-4.sr");
4629 write_run(
4630 &path,
4631 &RunSpec {
4632 run_id: 1,
4633 schema_id: 1,
4634 epoch_created: 1,
4635 level: 0,
4636 flags: 0,
4637 sort_key_column_id: SORT_KEY_ROW_ID,
4638 row_count: 1,
4639 min_row_id: 0,
4640 max_row_id: 0,
4641 columns: &[ColumnPayload {
4642 column_id: 1,
4643 type_id_tag: 8,
4644 encoding: Encoding::Plain,
4645 pages: vec![vec![1, 2, 3]],
4646 page_stats: Vec::new(),
4647 }],
4648 },
4649 )
4650 .unwrap();
4651 let mut bytes = std::fs::read(&path).unwrap();
4652 bytes[300] ^= 0xFF;
4653 std::fs::write(&path, bytes).unwrap();
4654 let err = read_header(&path).unwrap_err();
4655 assert!(
4656 matches!(err, MongrelError::ChecksumMismatch { .. }),
4657 "got {err:?}"
4658 );
4659 }
4660
4661 #[test]
4667 fn mmap_and_vec_writers_are_byte_identical() {
4668 let dir = tempdir().unwrap();
4669 let stats = vec![PageStat {
4670 first_row_id: 0,
4671 last_row_id: 1,
4672 null_count: 0,
4673 row_count: 2,
4674 min: Some(vec![0]),
4675 max: Some(vec![9]),
4676 offset: 0,
4677 compressed_len: 0,
4678 uncompressed_len: 0,
4679 }];
4680 let spec = RunSpec {
4681 run_id: 7,
4682 schema_id: 1,
4683 epoch_created: 10,
4684 level: 0,
4685 flags: 0,
4686 sort_key_column_id: SORT_KEY_ROW_ID,
4687 row_count: 2,
4688 min_row_id: 0,
4689 max_row_id: 1,
4690 columns: &[
4691 ColumnPayload {
4692 column_id: 1,
4693 type_id_tag: 8,
4694 encoding: Encoding::Plain,
4695 pages: vec![vec![1, 2, 3, 4], vec![5, 6, 7, 8]],
4696 page_stats: stats.clone(),
4697 },
4698 ColumnPayload {
4699 column_id: 2,
4700 type_id_tag: 8,
4701 encoding: Encoding::Plain,
4702 pages: vec![vec![10, 20], vec![30, 40]],
4703 page_stats: stats.clone(),
4704 },
4705 ],
4706 };
4707 let trailer = b"learned-trailer-bytes";
4708
4709 let plan = plan_run(&spec, None, Some(trailer)).expect("plan");
4711 let mut buf = vec![0u8; plan.total];
4712 let h_place = place_run(&spec, None, Some(trailer), &plan, &mut buf)
4713 .expect("place_run into Vec succeeds");
4714
4715 let path_vec = dir.path().join("vec.sr");
4717 let h_vec = write_run_vec(&path_vec, &spec, None, Some(trailer)).unwrap();
4718 let bv = std::fs::read(&path_vec).unwrap();
4719
4720 assert_eq!(h_place.content_hash, h_vec.content_hash);
4721 assert_eq!(h_place.footer_offset, h_vec.footer_offset);
4722 assert_eq!(
4723 buf, bv,
4724 "place_run and write_run_vec must be byte-identical"
4725 );
4726 assert!(read_header(&path_vec).is_ok());
4727
4728 let path_mmap = dir.path().join("mmap.sr");
4732 match write_run_mmap(&path_mmap, &spec, None, Some(trailer)) {
4733 Ok(h_mmap) => {
4734 let bm = std::fs::read(&path_mmap).unwrap();
4735 assert_eq!(bm, bv, "mmap run must be byte-identical to vec run");
4736 assert_eq!(h_mmap.content_hash, h_vec.content_hash);
4737 }
4738 Err(e) if is_mmap_unavailable(&e) => {
4739 eprintln!("note: file mmap unavailable here; vec path covered (1)/(2)");
4740 }
4741 Err(e) => panic!("unexpected mmap error: {e:?}"),
4742 }
4743 }
4744
4745 #[test]
4750 fn column_native_shared_matches_column_native() {
4751 let dir = tempdir().unwrap();
4752 let path = dir.path().join("r.sr");
4753 let n = 65_536 * 2 + 9;
4756 let id_col = NativeColumn::int64_sequence(1, n);
4757 let mut offsets = vec![0u32];
4758 let mut values = Vec::new();
4759 for i in 0..n {
4760 values.extend_from_slice(format!("v{}", i % 17).as_bytes());
4761 offsets.push(values.len() as u32);
4762 }
4763 let name_col = NativeColumn::Bytes {
4764 offsets,
4765 values,
4766 validity: vec![0xFF; n.div_ceil(8)],
4767 };
4768 let header = RunWriter::new(&schema(), 9, Epoch(5), 0)
4769 .write_native(&path, &[(1, id_col), (2, name_col)], n, 1)
4770 .unwrap();
4771
4772 let mut reader = RunReader::open_with_cache(&path, schema(), None, None, None, 0, None)
4773 .expect("open reader");
4774 assert!(reader.has_mmap(), "test env must support read-only mmap");
4775 assert_eq!(reader.row_count(), header.row_count as usize);
4776
4777 for cid in [1u16, 2] {
4778 let a = reader.column_native(cid).expect("column_native");
4779 let b = reader
4780 .column_native_shared(cid)
4781 .expect("column_native_shared");
4782 assert_eq!(a.len(), b.len(), "len mismatch col {cid}");
4783 match (&a, &b) {
4785 (
4786 NativeColumn::Int64 {
4787 data: da,
4788 validity: va,
4789 },
4790 NativeColumn::Int64 {
4791 data: db,
4792 validity: vb,
4793 },
4794 ) => {
4795 assert_eq!(da, db, "Int64 data col {cid}");
4796 assert_eq!(va, vb, "Int64 validity col {cid}");
4797 }
4798 (
4799 NativeColumn::Bytes {
4800 offsets: oa,
4801 values: ua,
4802 validity: va,
4803 },
4804 NativeColumn::Bytes {
4805 offsets: ob,
4806 values: ub,
4807 validity: vb,
4808 },
4809 ) => {
4810 assert_eq!(oa, ob, "Bytes offsets col {cid}");
4811 assert_eq!(ua, ub, "Bytes values col {cid}");
4812 assert_eq!(va, vb, "Bytes validity col {cid}");
4813 }
4814 _ => panic!("type mismatch col {cid}: {a:?} vs {b:?}"),
4815 }
4816 }
4817 }
4818
4819 #[test]
4820 fn page_cache_key_distinguishes_tables() {
4821 let a = page_cache_key(1, 5, 2, 3);
4822 let b = page_cache_key(2, 5, 2, 3);
4823 assert_ne!(a, b, "same run/col/page, different table must differ");
4824 assert_ne!(page_cache_key(1, 5, 2, 3), page_cache_key(1, 6, 2, 3));
4826 assert_ne!(page_cache_key(1, 5, 2, 3), page_cache_key(1, 5, 2, 4));
4827 }
4828}