1use crate::btree::{BTree, IndexStats};
2use crate::error::StorageError;
3use crate::heap::HeapFile;
4use crate::page::{OVERFLOW_CHAIN_END, OVERFLOW_PAYLOAD_CAP};
5use crate::row::{encode_row_into, encode_row_v2_into, plan_spill, OverflowStub, MAX_VALUE_SIZE};
6use crate::stored_json_path::{StoredJsonPathSegmentV1, StoredJsonPathV1};
7use crate::table::Table;
8use crate::types::*;
9use crate::wal::{Wal, WalDurabilityTicket, WalRecord, WalRecordType, WalSyncMode};
10use rustc_hash::FxHashMap;
11use std::fs;
12use std::io::{self, Read, Write};
13use std::path::{Path, PathBuf};
14use std::sync::atomic::{AtomicU64, Ordering};
15use tracing::{info, warn};
16
17static NEXT_STRUCTURE_GENERATION: AtomicU64 = AtomicU64::new(1);
18
19fn next_structure_generation() -> u64 {
20 NEXT_STRUCTURE_GENERATION.fetch_add(1, Ordering::Relaxed)
21}
22
23fn check_encoded_row_size(encoded: &[u8]) -> io::Result<()> {
28 if encoded.len() > crate::page::MAX_ROW_DATA_SIZE {
29 return Err(crate::error::StorageError::RowTooLarge {
30 size: encoded.len(),
31 max: crate::page::MAX_ROW_DATA_SIZE,
32 }
33 .into());
34 }
35 Ok(())
36}
37
38fn validate_identifier(kind: &str, name: &str) -> io::Result<()> {
42 if name.is_empty() {
43 return Err(io::Error::new(
44 io::ErrorKind::InvalidInput,
45 format!("invalid {kind} name: must not be empty"),
46 ));
47 }
48 let mut chars = name.chars();
49 let first = chars.next().expect("non-empty name");
51 if !first.is_ascii_alphabetic() && first != '_' {
52 return Err(io::Error::new(
53 io::ErrorKind::InvalidInput,
54 format!("invalid {kind} name '{name}': must start with a letter or underscore"),
55 ));
56 }
57 for ch in chars {
58 if !ch.is_ascii_alphanumeric() && ch != '_' {
59 return Err(io::Error::new(
60 io::ErrorKind::InvalidInput,
61 format!(
62 "invalid {kind} name '{name}': must contain only letters, digits, and underscores"
63 ),
64 ));
65 }
66 }
67 Ok(())
68}
69
70fn validate_table_name(name: &str) -> io::Result<()> {
72 validate_identifier("table", name)
73}
74
75fn validate_column_name(name: &str) -> io::Result<()> {
77 validate_identifier("column", name)
78}
79
80const CATALOG_FILE: &str = "catalog.bin";
89pub const CATALOG_LSN_FILE: &str = "catalog.lsn";
90const CATALOG_MAGIC: &[u8; 4] = b"BCAT";
91pub const LEGACY_CATALOG_VERSION: u16 = 5;
95pub const CATALOG_VERSION: u16 = 6;
96
97#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ExpressionIndexMeta {
102 pub index_id: u64,
103 pub unique: bool,
104 pub canonical_version: u16,
105 pub canonical_text: String,
106 pub json_path: StoredJsonPathV1,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub enum IndexKeySource {
111 Column {
112 column: String,
113 },
114 Expression {
115 index_id: u64,
116 canonical_version: u16,
117 canonical_text: String,
118 json_path: StoredJsonPathV1,
119 },
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct IndexMetadata {
124 pub unique: bool,
125 pub source: IndexKeySource,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum IndexOrderDirection {
130 Asc,
131 Desc,
132}
133
134pub fn expression_index_file_name(table: &str, index_id: u64) -> String {
139 format!("{table}_{index_id}.eidx")
140}
141
142const WAL_FILE: &str = "wal.log";
145const SYNC_STATE_DIR: &str = ".powdb-sync";
146const SYNC_IDENTITY_FILE: &str = "identity.json";
147
148const WAL_BATCH_SIZE: usize = 64;
152type WalArchiveCallback<'a> = &'a mut dyn FnMut(&Path, &[WalRecord]) -> io::Result<()>;
153
154fn read_durable_lsn(data_dir: &Path) -> io::Result<u64> {
155 let path = data_dir.join(CATALOG_LSN_FILE);
156 let bytes = match fs::read(path) {
157 Ok(bytes) => bytes,
158 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(0),
159 Err(err) => return Err(err),
160 };
161 if bytes.len() != 8 {
162 return Err(io::Error::new(
163 io::ErrorKind::InvalidData,
164 "catalog LSN sidecar has invalid length",
165 ));
166 }
167 let mut buf = [0u8; 8];
168 buf.copy_from_slice(&bytes);
169 Ok(u64::from_le_bytes(buf))
170}
171
172fn write_durable_lsn(data_dir: &Path, lsn: u64) -> io::Result<()> {
173 let path = data_dir.join(CATALOG_LSN_FILE);
174 let tmp_path = data_dir.join(format!("{CATALOG_LSN_FILE}.tmp"));
175 let mut file = fs::File::create(&tmp_path)?;
176 file.write_all(&lsn.to_le_bytes())?;
177 file.sync_all()?;
178 drop(file);
179 fs::rename(&tmp_path, &path)?;
180 sync_directory(data_dir)?;
181 Ok(())
182}
183
184#[cfg(unix)]
185fn sync_directory(path: &Path) -> io::Result<()> {
186 fs::File::open(path)?.sync_all()
187}
188
189#[cfg(not(unix))]
190fn sync_directory(path: &Path) -> io::Result<()> {
191 let _ = path;
192 Ok(())
193}
194
195#[cfg(test)]
196thread_local! {
197 static CATALOG_PERSIST_FAILPOINT: std::cell::Cell<u8> = const { std::cell::Cell::new(0) };
198}
199
200#[cfg(test)]
201fn take_catalog_persist_failpoint(stage: u8) -> bool {
202 CATALOG_PERSIST_FAILPOINT.with(|failpoint| {
203 if failpoint.get() == stage {
204 failpoint.set(0);
205 true
206 } else {
207 false
208 }
209 })
210}
211
212enum CatalogPersistError {
213 BeforeActivation(io::Error),
214 AfterActivation(io::Error),
215}
216
217impl CatalogPersistError {
218 fn into_io_error(self) -> io::Error {
219 match self {
220 Self::BeforeActivation(error) | Self::AfterActivation(error) => error,
221 }
222 }
223}
224
225fn max_record_lsn(records: &[WalRecord]) -> Option<u64> {
226 records.iter().map(|record| record.lsn).max()
227}
228
229pub struct Catalog {
241 tables: Vec<Table>,
243 name_to_slot: FxHashMap<String, usize>,
246 data_dir: PathBuf,
247 wal: Wal,
252 next_tx_id: u64,
256 active_tx_id: Option<u64>,
259 tx_start_len: Option<u64>,
262 pending_autocommit_tx_ids: Vec<u64>,
265 checkpointed: bool,
269 durable_lsn: u64,
272 pending_free_overflow: Vec<(usize, Vec<u32>)>,
281 active_catalog_version: u16,
284 next_index_id: u64,
286 structure_generation: u64,
291 read_only: bool,
296}
297
298impl Catalog {
299 pub fn create(data_dir: &Path) -> io::Result<Self> {
320 crate::create_data_dir_secure(data_dir)?;
321 let wal_path = data_dir.join(WAL_FILE);
322 let wal = Wal::create(&wal_path, WAL_BATCH_SIZE)?;
323 let cat = Catalog {
324 tables: Vec::new(),
325 name_to_slot: FxHashMap::default(),
326 data_dir: data_dir.to_path_buf(),
327 wal,
328 next_tx_id: 1,
329 active_tx_id: None,
330 tx_start_len: None,
331 pending_autocommit_tx_ids: Vec::new(),
332 pending_free_overflow: Vec::new(),
333 checkpointed: false,
334 durable_lsn: 0,
335 active_catalog_version: LEGACY_CATALOG_VERSION,
336 next_index_id: 1,
337 structure_generation: next_structure_generation(),
338 read_only: false,
339 };
340 cat.persist()?;
341 Ok(cat)
342 }
343
344 pub fn open(data_dir: &Path) -> io::Result<Self> {
353 Self::open_inner(data_dir, None)
354 }
355
356 pub fn open_with_wal_archive<F>(data_dir: &Path, mut archive: F) -> io::Result<Self>
365 where
366 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
367 {
368 let archive: WalArchiveCallback<'_> = &mut archive;
369 Self::open_inner(data_dir, Some(archive))
370 }
371
372 fn open_inner(data_dir: &Path, archive: Option<WalArchiveCallback<'_>>) -> io::Result<Self> {
373 let cat_path = data_dir.join(CATALOG_FILE);
374 if !cat_path.exists() {
375 return Err(io::Error::new(io::ErrorKind::NotFound, "no catalog file"));
376 }
377 let catalog_file = read_catalog_file(&cat_path)?;
378 let active_catalog_version = catalog_file.version;
379 let next_index_id = catalog_file.next_index_id;
380 let entries = catalog_file.entries;
381 let durable_lsn = read_durable_lsn(data_dir)?;
382 let mut tables: Vec<Table> = Vec::with_capacity(entries.len());
383 let mut name_to_slot =
384 FxHashMap::with_capacity_and_hasher(entries.len(), Default::default());
385 for CatalogEntry {
386 schema,
387 indexed_cols,
388 expression_indexes: expression_metas,
389 defaults,
390 auto_cols,
391 } in entries
392 {
393 let name = schema.table_name.clone();
394 let mut table =
400 Table::open_with_indexes(schema, data_dir, &indexed_cols, &expression_metas)?;
401 table.set_defaults(defaults);
402 table.set_auto_cols(auto_cols);
403 name_to_slot.insert(name.clone(), tables.len());
404 tables.push(table);
405 }
406 let wal_path = data_dir.join(WAL_FILE);
407 let wal = Wal::open(&wal_path, WAL_BATCH_SIZE)?;
408 let mut cat = Catalog {
409 tables,
410 name_to_slot,
411 data_dir: data_dir.to_path_buf(),
412 wal,
413 next_tx_id: 1,
414 active_tx_id: None,
415 tx_start_len: None,
416 pending_autocommit_tx_ids: Vec::new(),
417 pending_free_overflow: Vec::new(),
418 checkpointed: false,
419 durable_lsn,
420 active_catalog_version,
421 next_index_id,
422 structure_generation: next_structure_generation(),
423 read_only: false,
424 };
425 cat.replay_wal(archive)?;
426 let max_page_lsn = cat
436 .tables
437 .iter()
438 .map(|t| t.heap.max_page_lsn())
439 .max()
440 .unwrap_or(0);
441 let max_known_lsn = max_page_lsn.max(cat.durable_lsn);
442 cat.wal.set_next_lsn_at_least(max_known_lsn + 1);
443 if let Err(e) = cat.sweep_all() {
448 warn!(error = %e, "post-recovery overflow sweep failed (non-fatal)");
449 }
450 Ok(cat)
451 }
452
453 pub fn open_read_only(data_dir: &Path) -> io::Result<Self> {
467 crate::validate_data_dir_read_only(data_dir)?;
468 let cat_path = data_dir.join(CATALOG_FILE);
469 if !cat_path.exists() {
470 return Err(io::Error::new(io::ErrorKind::NotFound, "no catalog file"));
471 }
472
473 let wal_path = data_dir.join(WAL_FILE);
477 if crate::wal::wal_has_committed_records(&wal_path)? {
478 return Err(io::Error::new(
479 io::ErrorKind::InvalidData,
480 "cannot open read-only: the WAL is not empty (the directory has \
481 un-checkpointed writes). Open the directory once with a read-write \
482 engine to recover, or restore from a backup, then serve it read-only",
483 ));
484 }
485
486 let catalog_file = read_catalog_file(&cat_path)?;
487 let active_catalog_version = catalog_file.version;
488 let next_index_id = catalog_file.next_index_id;
489 let entries = catalog_file.entries;
490 let durable_lsn = read_durable_lsn(data_dir)?;
491 let mut tables: Vec<Table> = Vec::with_capacity(entries.len());
492 let mut name_to_slot =
493 FxHashMap::with_capacity_and_hasher(entries.len(), Default::default());
494 for CatalogEntry {
495 schema,
496 indexed_cols,
497 expression_indexes: expression_metas,
498 defaults,
499 auto_cols,
500 } in entries
501 {
502 let name = schema.table_name.clone();
503 let mut table = Table::open_with_indexes_read_only(
504 schema,
505 data_dir,
506 &indexed_cols,
507 &expression_metas,
508 )?;
509 table.set_defaults(defaults);
510 table.set_auto_cols(auto_cols);
511 name_to_slot.insert(name.clone(), tables.len());
512 tables.push(table);
513 }
514 let wal = Wal::open_read_only(&wal_path, WAL_BATCH_SIZE)?;
515 Ok(Catalog {
516 tables,
517 name_to_slot,
518 data_dir: data_dir.to_path_buf(),
519 wal,
520 next_tx_id: 1,
521 active_tx_id: None,
522 tx_start_len: None,
523 pending_autocommit_tx_ids: Vec::new(),
524 pending_free_overflow: Vec::new(),
525 checkpointed: false,
526 durable_lsn,
527 active_catalog_version,
528 next_index_id,
529 structure_generation: next_structure_generation(),
530 read_only: true,
531 })
532 }
533
534 fn replay_wal(&mut self, mut archive: Option<WalArchiveCallback<'_>>) -> io::Result<()> {
560 let records = self.wal.read_all()?;
561 if records.is_empty() {
562 return Ok(());
563 }
564 if archive.is_none() {
565 self.ensure_plain_wal_truncate_allowed(&records)?;
566 }
567 self.replay_records(&records)?;
568 if let Some(archive) = archive.as_mut() {
569 archive(&self.data_dir, &records)?;
570 }
571 self.wal.truncate()?;
572 Ok(())
573 }
574
575 pub fn apply_wal_records(&mut self, records: &[WalRecord]) -> io::Result<()> {
584 self.ensure_no_active_transaction_for_checkpoint()?;
585 self.ensure_no_pending_wal_records()?;
586 self.replay_records(records)
587 }
588
589 pub fn ensure_no_pending_wal_records(&self) -> io::Result<()> {
592 if self.wal.has_pending() || !self.wal.read_all()?.is_empty() {
593 return Err(io::Error::other(
594 "cannot apply replicated WAL records while local WAL records are pending",
595 ));
596 }
597 Ok(())
598 }
599
600 fn replay_records(&mut self, records: &[WalRecord]) -> io::Result<()> {
601 if records.is_empty() {
602 return Ok(());
603 }
604
605 info!(count = records.len(), "applying WAL records");
606
607 let has_boundaries = records.iter().any(|rec| {
616 matches!(
617 rec.record_type,
618 WalRecordType::Begin | WalRecordType::Commit | WalRecordType::Rollback
619 )
620 });
621 let mut committed_row_records = vec![true; records.len()];
622 if has_boundaries {
623 committed_row_records.fill(false);
624 let mut pending_tx_spans: Vec<(u64, Vec<usize>)> = Vec::new();
625 for (index, rec) in records.iter().enumerate() {
626 match rec.record_type {
627 WalRecordType::Insert
628 | WalRecordType::Update
629 | WalRecordType::Delete
630 | WalRecordType::OverflowWrite
631 | WalRecordType::OverflowFree
632 if rec.tx_id == 0 =>
633 {
634 committed_row_records[index] = true;
635 }
636 WalRecordType::Insert
637 | WalRecordType::Update
638 | WalRecordType::Delete
639 | WalRecordType::OverflowWrite
640 | WalRecordType::OverflowFree => {
641 if let Some((_, rows)) = pending_tx_spans
642 .iter_mut()
643 .rev()
644 .find(|(tx_id, _)| *tx_id == rec.tx_id)
645 {
646 rows.push(index);
647 } else {
648 pending_tx_spans.push((rec.tx_id, vec![index]));
649 }
650 }
651 WalRecordType::Begin if rec.tx_id != 0 => {
652 pending_tx_spans.push((rec.tx_id, Vec::new()));
653 }
654 WalRecordType::Commit if rec.tx_id != 0 => {
655 if let Some(span_index) = pending_tx_spans
656 .iter()
657 .rposition(|(tx_id, _)| *tx_id == rec.tx_id)
658 {
659 let (_, rows) = pending_tx_spans.remove(span_index);
660 for row_index in rows {
661 committed_row_records[row_index] = true;
662 }
663 }
664 }
665 WalRecordType::Rollback if rec.tx_id != 0 => {
666 if let Some(span_index) = pending_tx_spans
667 .iter()
668 .rposition(|(tx_id, _)| *tx_id == rec.tx_id)
669 {
670 pending_tx_spans.remove(span_index);
671 }
672 }
673 _ => {}
674 }
675 }
676 }
677
678 let mut replayed_inserts = 0usize;
679 let mut replayed_updates = 0usize;
680 let mut replayed_deletes = 0usize;
681 let mut skipped = 0usize;
682 let mut skipped_uncommitted = 0usize;
683 let mut saw_ddl = false;
684 for (index, rec) in records.iter().enumerate() {
685 if has_boundaries
686 && !committed_row_records[index]
687 && matches!(
688 rec.record_type,
689 WalRecordType::Insert
690 | WalRecordType::Update
691 | WalRecordType::Delete
692 | WalRecordType::OverflowWrite
693 | WalRecordType::OverflowFree
694 )
695 {
696 skipped_uncommitted += 1;
697 continue;
698 }
699 match rec.record_type {
700 WalRecordType::Insert => {
701 if let Some((table_name, rid, row_bytes)) = decode_wal_payload(&rec.data) {
702 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
703 let tbl = &mut self.tables[slot];
704 if rec.lsn > 0 && tbl.heap.page_lsn(rid.page_id) >= rec.lsn {
708 skipped += 1;
709 continue;
710 }
711 tbl.heap.insert_at(rid, &row_bytes)?;
718 tbl.heap.set_page_lsn(rid.page_id, rec.lsn)?;
719 replayed_inserts += 1;
720 }
721 }
722 }
723 WalRecordType::Update => {
724 if let Some((table_name, rid, row_bytes)) = decode_wal_payload(&rec.data) {
725 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
726 let tbl = &mut self.tables[slot];
727 if rec.lsn > 0 && tbl.heap.page_lsn(rid.page_id) >= rec.lsn {
728 skipped += 1;
729 continue;
730 }
731 let new_rid = tbl.heap.update(rid, &row_bytes)?;
732 tbl.heap.set_page_lsn(new_rid.page_id, rec.lsn)?;
733 replayed_updates += 1;
734 }
735 }
736 }
737 WalRecordType::Delete => {
738 if let Some((table_name, rid, _)) = decode_wal_payload(&rec.data) {
739 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
740 let tbl = &mut self.tables[slot];
741 if rec.lsn > 0 && tbl.heap.page_lsn(rid.page_id) >= rec.lsn {
742 skipped += 1;
743 continue;
744 }
745 let _ = tbl.heap.delete(rid);
746 tbl.heap.set_page_lsn(rid.page_id, rec.lsn)?;
747 replayed_deletes += 1;
748 }
749 }
750 }
751 WalRecordType::OverflowWrite => {
752 if let Some((table_name, page_id, next_page, chunk)) =
757 decode_overflow_write_payload(&rec.data)
758 {
759 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
760 let tbl = &mut self.tables[slot];
761 if rec.lsn > 0 && tbl.heap.overflow_page_lsn(page_id) >= rec.lsn {
762 skipped += 1;
763 continue;
764 }
765 tbl.heap
766 .write_overflow_page(page_id, next_page, &chunk, rec.lsn)?;
767 }
768 }
769 }
770 WalRecordType::OverflowFree => {
771 if let Some((table_name, pages)) = decode_overflow_free_payload(&rec.data) {
776 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
777 self.tables[slot].heap.release_overflow_pages(&pages);
778 }
779 }
780 }
781 WalRecordType::Begin | WalRecordType::Commit | WalRecordType::Rollback => {
782 }
784 WalRecordType::DdlCreateTable => {
785 saw_ddl = true;
786 if let Some((schema, defaults, auto_cols)) = decode_ddl_create_table(&rec.data)
787 {
788 if !self.name_to_slot.contains_key(&schema.table_name) {
789 if let Ok(mut table) = Table::create(schema, &self.data_dir) {
790 table.set_defaults(defaults);
791 table.set_auto_cols(auto_cols);
792 let slot = self.tables.len();
793 let name = table.schema.table_name.clone();
794 self.tables.push(table);
795 self.name_to_slot.insert(name, slot);
796 }
797 }
798 }
799 }
800 WalRecordType::DdlDropTable => {
801 saw_ddl = true;
802 if let Some((table_name, _)) = decode_ddl_table_name(&rec.data) {
803 if let Some(&slot) = self.name_to_slot.get(&table_name) {
804 let heap_path = self.data_dir.join(format!("{table_name}.heap"));
805 if heap_path.exists() {
806 let _ = fs::remove_file(&heap_path);
807 }
808 for col_name in self.tables[slot].indexed_column_names() {
809 let idx_path =
810 self.data_dir.join(format!("{table_name}_{col_name}.idx"));
811 if idx_path.exists() {
812 let _ = fs::remove_file(&idx_path);
813 }
814 }
815 for index_id in self.tables[slot].expression_index_ids() {
816 let idx_path = self
817 .data_dir
818 .join(expression_index_file_name(&table_name, index_id));
819 let _ = fs::remove_file(idx_path);
820 }
821 self.name_to_slot.remove(&table_name);
822 let last = self.tables.len() - 1;
823 if slot != last {
824 let moved_name = self.tables[last].schema.table_name.clone();
825 self.tables.swap(slot, last);
826 self.name_to_slot.insert(moved_name, slot);
827 }
828 self.tables.pop();
829 }
830 }
831 }
832 WalRecordType::DdlAddColumn => {
833 saw_ddl = true;
834 if let Some((table_name, col)) = decode_ddl_alter_add_column(&rec.data) {
835 if let Some(&slot) = self.name_to_slot.get(&table_name) {
836 let tbl = &mut self.tables[slot];
837 if !tbl.schema.columns.iter().any(|c| c.name == col.name) {
838 let old_schema = tbl.schema.clone();
839 let has_rows = tbl.heap.scan().next().is_some();
840 tbl.schema.columns.push(col);
841 tbl.refresh_layout();
842 if has_rows {
843 let fill = vec![Value::Empty; tbl.schema.columns.len()];
844 let data_dir = self.data_dir.clone();
845 let _ = tbl.rewrite_rows_for_schema_change(
846 &old_schema,
847 &fill,
848 &data_dir,
849 );
850 }
851 }
852 if rec.lsn > 0 {
859 let _ = tbl.heap.stamp_all_pages_min_lsn(rec.lsn);
860 }
861 }
862 }
863 }
864 WalRecordType::DdlDropColumn => {
865 saw_ddl = true;
866 if let Some((table_name, col_name)) = decode_ddl_alter_drop_column(&rec.data) {
867 if let Some(&slot) = self.name_to_slot.get(&table_name) {
868 {
869 let tbl = &mut self.tables[slot];
870 if let Some(idx) =
871 tbl.schema.columns.iter().position(|c| c.name == col_name)
872 {
873 let old_schema = tbl.schema.clone();
874 let has_rows = tbl.heap.scan().next().is_some();
875 tbl.schema.columns.remove(idx);
876 for (i, c) in tbl.schema.columns.iter_mut().enumerate() {
877 c.position = i as u16;
878 }
879 tbl.refresh_layout();
880 if has_rows {
881 let fill = vec![Value::Empty; tbl.schema.columns.len()];
882 let data_dir = self.data_dir.clone();
883 let _ = tbl.rewrite_rows_for_schema_change(
884 &old_schema,
885 &fill,
886 &data_dir,
887 );
888 }
889 }
890 if rec.lsn > 0 {
891 let _ = tbl.heap.stamp_all_pages_min_lsn(rec.lsn);
892 }
893 }
894
895 let removed_ids =
896 self.tables[slot].remove_expression_indexes_for_root(&col_name);
897 for index_id in removed_ids {
898 let idx_path = self
899 .data_dir
900 .join(expression_index_file_name(&table_name, index_id));
901 let _ = fs::remove_file(idx_path);
902 }
903 }
904 }
905 }
906 }
907 }
908 info!(
909 inserts = replayed_inserts,
910 updates = replayed_updates,
911 deletes = replayed_deletes,
912 skipped = skipped,
913 skipped_uncommitted = skipped_uncommitted,
914 "WAL record apply complete (commit-boundary + LSN idempotent)"
915 );
916 if saw_ddl {
917 self.persist()?;
918 }
919 for tbl in &mut self.tables {
933 tbl.heap.flush_all_dirty()?;
934 tbl.heap.flush()?;
935 tbl.rebuild_indexes_from_heap()?;
936 tbl.save_dirty_indexes()?;
941 }
942 if let Some(max_lsn) = max_record_lsn(records) {
943 self.record_durable_lsn_at_least(max_lsn)?;
944 self.wal.set_next_lsn_at_least(max_lsn.saturating_add(1));
945 }
946 Ok(())
947 }
948
949 pub fn checkpoint(&mut self) -> io::Result<()> {
958 self.ensure_no_active_transaction_for_checkpoint()?;
959 self.ensure_plain_checkpoint_allowed_before_flush()?;
960 self.flush_checkpoint_state()?;
961 self.wal.flush()?;
962 self.record_durable_lsn_at_least(self.wal.last_appended_lsn())?;
963 self.wal.truncate()?;
964 self.checkpointed = true;
965 Ok(())
966 }
967
968 pub fn checkpoint_with_wal_archive<F>(&mut self, mut archive: F) -> io::Result<()>
976 where
977 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
978 {
979 self.ensure_no_active_transaction_for_checkpoint()?;
980 self.commit_autocommit()?;
981 self.flush_checkpoint_state()?;
982 self.wal.flush()?;
983 let records = self.wal.read_all()?;
984 let archive: WalArchiveCallback<'_> = &mut archive;
985 archive(&self.data_dir, &records)?;
986 if let Some(max_lsn) = max_record_lsn(&records) {
987 self.record_durable_lsn_at_least(max_lsn)?;
988 } else {
989 self.record_durable_lsn_at_least(self.wal.last_appended_lsn())?;
990 }
991 self.wal.truncate()?;
992 self.checkpointed = true;
993 Ok(())
994 }
995
996 fn ensure_no_active_transaction_for_checkpoint(&self) -> io::Result<()> {
997 if self.active_tx_id.is_some() {
998 return Err(io::Error::other(
999 "cannot checkpoint while an explicit transaction is active",
1000 ));
1001 }
1002 Ok(())
1003 }
1004
1005 fn flush_checkpoint_state(&mut self) -> io::Result<()> {
1006 for tbl in &mut self.tables {
1007 tbl.heap.flush_all_dirty()?;
1008 tbl.heap.flush()?;
1009 tbl.save_dirty_indexes()?;
1014 }
1015 Ok(())
1016 }
1017
1018 fn ensure_plain_checkpoint_allowed_before_flush(&self) -> io::Result<()> {
1019 if !self.sync_identity_file_exists() {
1020 return Ok(());
1021 }
1022 if self.wal.has_pending() {
1023 return Err(io::Error::other(
1024 "sync identity exists but checkpoint/recovery was called without a WAL archive hook; refusing to truncate retained history",
1025 ));
1026 }
1027 let records = self.wal.read_all()?;
1028 self.ensure_plain_wal_truncate_allowed(&records)
1029 }
1030
1031 fn ensure_plain_wal_truncate_allowed(&self, records: &[WalRecord]) -> io::Result<()> {
1032 if records.is_empty() {
1033 return Ok(());
1034 }
1035 if self.sync_identity_file_exists() {
1036 return Err(io::Error::other(
1037 "sync identity exists but checkpoint/recovery was called without a WAL archive hook; refusing to truncate retained history",
1038 ));
1039 }
1040 Ok(())
1041 }
1042
1043 fn sync_identity_file_exists(&self) -> bool {
1044 self.data_dir
1045 .join(SYNC_STATE_DIR)
1046 .join(SYNC_IDENTITY_FILE)
1047 .exists()
1048 }
1049
1050 fn record_durable_lsn_at_least(&mut self, lsn: u64) -> io::Result<()> {
1051 if lsn <= self.durable_lsn {
1052 return Ok(());
1053 }
1054 self.durable_lsn = lsn;
1055 write_durable_lsn(&self.data_dir, lsn)
1056 }
1057
1058 #[inline]
1060 fn free_overflow_chain(&mut self, slot: usize, pages: Vec<u32>) {
1071 if pages.is_empty() {
1072 return;
1073 }
1074 if self.active_tx_id.is_some() {
1075 self.pending_free_overflow.push((slot, pages));
1076 } else {
1077 self.tables[slot].release_overflow_pages(&pages);
1078 }
1079 }
1080
1081 fn next_tx(&mut self) -> u64 {
1082 if let Some(id) = self.active_tx_id {
1083 return id;
1084 }
1085 let id = self.next_tx_id;
1086 self.next_tx_id = self.next_tx_id.wrapping_add(1);
1087 id
1088 }
1089
1090 pub fn begin_transaction(&mut self) -> io::Result<()> {
1092 if self.active_tx_id.is_some() {
1093 return Err(io::Error::new(
1094 io::ErrorKind::InvalidInput,
1095 "explicit transaction is already active",
1096 ));
1097 }
1098 let start_len = self.wal.synced_len()?;
1099 let id = self.next_tx_id;
1100 self.next_tx_id = self.next_tx_id.wrapping_add(1);
1101 self.active_tx_id = Some(id);
1102 self.tx_start_len = Some(start_len);
1103 self.pending_autocommit_tx_ids.clear();
1104 if !self.wal.is_off() {
1105 self.wal.append(id, WalRecordType::Begin, &[])?;
1106 self.wal.flush()?;
1107 }
1108 Ok(())
1109 }
1110
1111 pub fn commit_transaction(&mut self) -> io::Result<()> {
1114 if let Some(id) = self.active_tx_id.take() {
1115 if !self.wal.is_off() {
1116 self.wal.append(id, WalRecordType::Commit, &[])?;
1117 self.wal.flush()?;
1118 }
1119 }
1120 self.tx_start_len = None;
1121 for (slot, pages) in std::mem::take(&mut self.pending_free_overflow) {
1125 self.tables[slot].release_overflow_pages(&pages);
1126 }
1127 Ok(())
1128 }
1129
1130 pub fn commit_autocommit(&mut self) -> io::Result<()> {
1134 if !self.wal.is_off() && !self.pending_autocommit_tx_ids.is_empty() {
1135 self.pending_autocommit_tx_ids.sort_unstable();
1136 self.pending_autocommit_tx_ids.dedup();
1137 for id in self.pending_autocommit_tx_ids.drain(..) {
1138 self.wal.append(id, WalRecordType::Commit, &[])?;
1139 }
1140 }
1141 self.wal.flush()
1142 }
1143
1144 fn wal_log(
1159 &mut self,
1160 tx_id: u64,
1161 record_type: WalRecordType,
1162 table: &str,
1163 rid: RowId,
1164 row_bytes: &[u8],
1165 ) -> io::Result<()> {
1166 if self.wal.is_off() {
1173 return Ok(());
1174 }
1175 let payload = encode_wal_payload(table, rid, row_bytes);
1176 self.wal.append(tx_id, record_type, &payload)?;
1177 if self.active_tx_id.is_none() {
1178 self.pending_autocommit_tx_ids.push(tx_id);
1179 }
1180 Ok(())
1181 }
1182
1183 #[inline]
1189 pub fn sync_wal(&mut self) -> io::Result<()> {
1190 self.wal.flush()
1191 }
1192
1193 pub fn set_wal_sync_mode(&mut self, mode: WalSyncMode) {
1201 self.wal.set_sync_mode(mode);
1202 }
1203
1204 pub fn set_wal_sync_deferred(&mut self, defer: bool) {
1212 self.wal.set_defer_sync(defer);
1213 }
1214
1215 pub fn take_wal_durability_ticket(&mut self) -> Option<WalDurabilityTicket> {
1218 self.wal.take_durability_ticket()
1219 }
1220
1221 pub fn wal_fsync_count(&self) -> u64 {
1223 self.wal.fsync_count()
1224 }
1225
1226 pub fn rollback_to_last_sync(&mut self) -> io::Result<()> {
1241 self.rollback_to_last_sync_inner(None)
1242 }
1243
1244 pub fn rollback_to_last_sync_with_wal_archive<F>(&mut self, mut archive: F) -> io::Result<()>
1249 where
1250 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
1251 {
1252 let archive: WalArchiveCallback<'_> = &mut archive;
1253 self.rollback_to_last_sync_inner(Some(archive))
1254 }
1255
1256 fn rollback_to_last_sync_inner(
1257 &mut self,
1258 mut archive: Option<WalArchiveCallback<'_>>,
1259 ) -> io::Result<()> {
1260 let start_len = self.tx_start_len.unwrap_or(0);
1261 let prearchived = if let Some(archive) = archive.as_mut() {
1262 let records = self.wal.read_through_len(start_len)?;
1263 if !records.is_empty() {
1264 archive(&self.data_dir, &records)?;
1265 }
1266 true
1267 } else {
1268 false
1269 };
1270
1271 let start_len = self.tx_start_len.take().unwrap_or(0);
1272 if let Some(id) = self.active_tx_id.take() {
1273 if !self.wal.is_off() {
1274 let _ = self.wal.append(id, WalRecordType::Rollback, &[]);
1275 }
1276 }
1277 self.wal.discard_and_truncate_to(start_len)?;
1278
1279 for tbl in &mut self.tables {
1289 tbl.heap.discard_dirty();
1290 tbl.discard_dirty_indexes();
1291 }
1292 self.wal.discard_pending()?;
1298 let data_dir = self.data_dir.clone();
1302 let sync_mode = self.wal.sync_mode();
1303 let mut restored = if prearchived {
1304 let mut already_archived = |_dir: &Path, _records: &[WalRecord]| Ok(());
1305 let archive: WalArchiveCallback<'_> = &mut already_archived;
1306 Self::open_inner(&data_dir, Some(archive))?
1307 } else {
1308 Self::open_inner(&data_dir, archive)?
1309 };
1310 if self.has_same_prepared_structure(&restored) {
1316 restored.structure_generation = self.structure_generation;
1317 }
1318 *self = restored;
1319 self.wal.set_sync_mode(sync_mode);
1320 Ok(())
1321 }
1322
1323 fn abandon_active_transaction_for_drop(&mut self) -> io::Result<()> {
1324 for tbl in &mut self.tables {
1325 tbl.heap.discard_dirty();
1326 }
1327 self.pending_autocommit_tx_ids.clear();
1328 let truncate_result = match self.tx_start_len.take() {
1329 Some(start_len) => self.wal.discard_and_truncate_to(start_len),
1330 None => self.wal.discard_pending(),
1331 };
1332 self.active_tx_id = None;
1333 truncate_result
1334 }
1335
1336 pub fn data_dir(&self) -> &Path {
1338 &self.data_dir
1339 }
1340
1341 pub fn max_lsn(&self) -> u64 {
1346 let max_page_lsn = self
1347 .tables
1348 .iter()
1349 .map(|t| t.heap.max_page_lsn())
1350 .max()
1351 .unwrap_or(0);
1352 max_page_lsn
1353 .max(self.durable_lsn)
1354 .max(self.wal.last_appended_lsn())
1355 }
1356
1357 pub fn create_table(&mut self, schema: Schema) -> io::Result<()> {
1358 self.create_table_full(schema, Vec::new(), Vec::new())
1359 }
1360
1361 pub fn create_table_with_defaults(
1365 &mut self,
1366 schema: Schema,
1367 defaults: Vec<Option<Value>>,
1368 ) -> io::Result<()> {
1369 self.create_table_full(schema, defaults, Vec::new())
1370 }
1371
1372 pub fn create_table_full(
1377 &mut self,
1378 schema: Schema,
1379 defaults: Vec<Option<Value>>,
1380 auto_cols: Vec<bool>,
1381 ) -> io::Result<()> {
1382 self.invalidate_structure();
1383 validate_table_name(&schema.table_name)?;
1384 for col in &schema.columns {
1385 validate_column_name(&col.name)?;
1386 }
1387 let name = schema.table_name.clone();
1388 if self.name_to_slot.contains_key(&name) {
1389 return Err(io::Error::new(
1390 io::ErrorKind::AlreadyExists,
1391 format!("table '{name}' already exists"),
1392 ));
1393 }
1394 if !self.wal.is_off() {
1395 let payload = encode_ddl_create_table(&schema, &defaults, &auto_cols);
1396 self.wal
1397 .append(0, WalRecordType::DdlCreateTable, &payload)?;
1398 self.wal.flush()?;
1399 }
1400 let mut table = Table::create(schema, &self.data_dir)?;
1401 table.set_defaults(defaults);
1402 table.set_auto_cols(auto_cols);
1403 let slot = self.tables.len();
1404 self.tables.push(table);
1405 self.name_to_slot.insert(name, slot);
1406 self.persist()?;
1407 Ok(())
1408 }
1409
1410 pub fn column_defaults(&self, table: &str) -> Option<&[Option<Value>]> {
1414 let slot = *self.name_to_slot.get(table)?;
1415 Some(self.tables[slot].defaults())
1416 }
1417
1418 pub fn auto_columns(&self, table: &str) -> Option<&[bool]> {
1421 let slot = *self.name_to_slot.get(table)?;
1422 Some(self.tables[slot].auto_cols())
1423 }
1424
1425 pub fn assign_auto_columns(&mut self, table: &str, values: &mut [Value]) {
1429 if let Some(&slot) = self.name_to_slot.get(table) {
1430 self.tables[slot].assign_auto(values);
1431 }
1432 }
1433
1434 fn persist_at_activation_boundary(&self) -> Result<(), CatalogPersistError> {
1439 let cat_path = self.data_dir.join(CATALOG_FILE);
1440 let tmp_path = self.data_dir.join(format!("{CATALOG_FILE}.tmp"));
1441 let entries: Vec<CatalogEntryRef<'_>> = self
1442 .tables
1443 .iter()
1444 .map(|t| CatalogEntryRef {
1445 schema: &t.schema,
1446 indexed_cols: t.indexed_column_metas(),
1447 expression_indexes: t.expression_index_metas(),
1448 defaults: t.defaults(),
1449 auto_cols: t.auto_cols(),
1450 })
1451 .collect();
1452 write_catalog_file(
1453 &tmp_path,
1454 self.active_catalog_version,
1455 self.next_index_id,
1456 &entries,
1457 )
1458 .map_err(CatalogPersistError::BeforeActivation)?;
1459 #[cfg(test)]
1460 if take_catalog_persist_failpoint(1) {
1461 return Err(CatalogPersistError::BeforeActivation(io::Error::other(
1462 "injected catalog failure before rename",
1463 )));
1464 }
1465 fs::rename(&tmp_path, &cat_path).map_err(CatalogPersistError::BeforeActivation)?;
1466 #[cfg(test)]
1467 let directory_sync = if take_catalog_persist_failpoint(2) {
1468 Err(io::Error::other(
1469 "injected catalog directory sync failure after rename",
1470 ))
1471 } else {
1472 sync_directory(&self.data_dir)
1473 };
1474 #[cfg(not(test))]
1475 let directory_sync = sync_directory(&self.data_dir);
1476 directory_sync.map_err(CatalogPersistError::AfterActivation)
1477 }
1478
1479 fn persist(&self) -> io::Result<()> {
1480 self.persist_at_activation_boundary()
1481 .map_err(CatalogPersistError::into_io_error)
1482 }
1483
1484 #[inline]
1488 pub fn table_slot(&self, name: &str) -> Option<usize> {
1489 self.name_to_slot.get(name).copied()
1490 }
1491
1492 #[inline]
1496 pub fn structure_generation(&self) -> u64 {
1497 self.structure_generation
1498 }
1499
1500 #[inline]
1501 fn invalidate_structure(&mut self) {
1502 self.structure_generation = next_structure_generation();
1503 }
1504
1505 fn has_same_prepared_structure(&self, other: &Self) -> bool {
1506 self.tables.len() == other.tables.len()
1507 && self.tables.iter().zip(&other.tables).all(|(left, right)| {
1508 let left_schema = &left.schema;
1509 let right_schema = &right.schema;
1510 left_schema.table_name == right_schema.table_name
1511 && left_schema.columns.len() == right_schema.columns.len()
1512 && left_schema.columns.iter().zip(&right_schema.columns).all(
1513 |(left_col, right_col)| {
1514 left_col.name == right_col.name
1515 && left_col.type_id == right_col.type_id
1516 && left_col.required == right_col.required
1517 && left_col.position == right_col.position
1518 },
1519 )
1520 && left.defaults() == right.defaults()
1521 && left.auto_cols() == right.auto_cols()
1522 && {
1523 let left_indexes = left.indexed_column_metas();
1524 let right_indexes = right.indexed_column_metas();
1525 left_indexes.len() == right_indexes.len()
1526 && left_indexes.iter().zip(&right_indexes).all(
1527 |(left_index, right_index)| {
1528 left_index.name == right_index.name
1529 && left_index.unique == right_index.unique
1530 },
1531 )
1532 }
1533 && left.expression_index_metas() == right.expression_index_metas()
1534 })
1535 }
1536
1537 #[inline]
1540 pub fn table_by_slot(&self, slot: usize) -> &Table {
1541 &self.tables[slot]
1542 }
1543
1544 #[inline]
1546 pub fn table_by_slot_mut(&mut self, slot: usize) -> &mut Table {
1547 &mut self.tables[slot]
1548 }
1549
1550 pub fn get_table(&self, name: &str) -> Option<&Table> {
1551 let slot = *self.name_to_slot.get(name)?;
1552 Some(&self.tables[slot])
1553 }
1554
1555 pub fn get_table_mut(&mut self, name: &str) -> Option<&mut Table> {
1556 let slot = *self.name_to_slot.get(name)?;
1557 Some(&mut self.tables[slot])
1558 }
1559
1560 #[inline]
1564 pub fn table_has_overflow(&self, table: &str) -> bool {
1565 self.get_table(table)
1566 .map(|t| t.has_overflow_rows())
1567 .unwrap_or(false)
1568 }
1569
1570 #[inline]
1575 fn by_name(&self, table: &str) -> io::Result<&Table> {
1576 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1577 io::Error::new(
1578 io::ErrorKind::NotFound,
1579 format!("table '{table}' not found"),
1580 )
1581 })?;
1582 Ok(&self.tables[slot])
1583 }
1584
1585 #[inline]
1587 fn by_name_mut(&mut self, table: &str) -> io::Result<&mut Table> {
1588 let slot = self.slot_of(table)?;
1589 Ok(&mut self.tables[slot])
1590 }
1591
1592 pub fn sweep(&mut self, table: &str) -> io::Result<usize> {
1597 let slot = self.slot_of(table)?;
1598 let reclaimed = self.tables[slot].sweep_overflow()?;
1599 if !reclaimed.is_empty() && !self.wal.is_off() {
1600 let payload = encode_overflow_free_payload(table, &reclaimed);
1601 self.wal.append(0, WalRecordType::OverflowFree, &payload)?;
1602 self.wal.flush()?;
1603 }
1604 Ok(reclaimed.len())
1605 }
1606
1607 pub fn sweep_all(&mut self) -> io::Result<usize> {
1609 let names: Vec<String> = self
1610 .tables
1611 .iter()
1612 .map(|t| t.schema.table_name.clone())
1613 .collect();
1614 let mut total = 0;
1615 for name in names {
1616 total += self.sweep(&name)?;
1617 }
1618 Ok(total)
1619 }
1620
1621 fn slot_of(&self, table: &str) -> io::Result<usize> {
1622 self.name_to_slot.get(table).copied().ok_or_else(|| {
1623 io::Error::new(
1624 io::ErrorKind::NotFound,
1625 format!("table '{table}' not found"),
1626 )
1627 })
1628 }
1629
1630 pub fn insert(&mut self, table: &str, values: &Row) -> io::Result<RowId> {
1631 if self.wal.is_off() {
1640 return self.by_name_mut(table)?.insert(values);
1641 }
1642 let slot = self.slot_of(table)?;
1643 let _ = self.tables[slot].preflight_insert(values)?;
1644 let tx_id = self.next_tx();
1648 let row_bytes = {
1649 let Catalog { tables, wal, .. } = self;
1650 encode_row_with_spill_logged(&mut tables[slot], wal, tx_id, values)?
1651 };
1652 let new_rid = self.tables[slot].insert_encoded(values, &row_bytes)?;
1657 self.wal_log(tx_id, WalRecordType::Insert, table, new_rid, &row_bytes)?;
1658 let lsn = self.wal.last_appended_lsn();
1659 if lsn > 0 {
1660 self.tables[slot].heap.set_page_lsn(new_rid.page_id, lsn)?;
1661 }
1662 Ok(new_rid)
1663 }
1664
1665 pub fn insert_by_slot(&mut self, slot: usize, values: &Row) -> io::Result<RowId> {
1673 if self.wal.is_off() {
1674 return self.tables[slot].insert(values);
1675 }
1676 let _ = self.tables[slot].preflight_insert(values)?;
1677 let tx_id = self.next_tx();
1678 let autocommit = self.active_tx_id.is_none();
1679 let Catalog { tables, wal, .. } = self;
1680 let tbl = &mut tables[slot];
1681 let row_bytes = encode_row_with_spill_logged(tbl, wal, tx_id, values)?;
1684 let new_rid = tbl.insert_encoded(values, &row_bytes)?;
1687 let payload = encode_wal_payload(&tbl.schema.table_name, new_rid, &row_bytes);
1688 wal.append(tx_id, WalRecordType::Insert, &payload)?;
1689 if autocommit {
1690 self.pending_autocommit_tx_ids.push(tx_id);
1691 }
1692 let lsn = wal.last_appended_lsn();
1693 if lsn > 0 {
1694 tbl.heap.set_page_lsn(new_rid.page_id, lsn)?;
1695 }
1696 Ok(new_rid)
1697 }
1698
1699 pub fn get(&self, table: &str, rid: RowId) -> Option<Row> {
1700 self.get_table(table)?.get(rid)
1701 }
1702
1703 pub fn get_projected(
1704 &self,
1705 table: &str,
1706 rid: RowId,
1707 column_indices: &[usize],
1708 ) -> io::Result<Option<Vec<Value>>> {
1709 self.by_name(table)?.get_projected(rid, column_indices)
1710 }
1711
1712 pub fn delete(&mut self, table: &str, rid: RowId) -> io::Result<()> {
1713 let slot = self.slot_of(table)?;
1714 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
1718 if self.wal.is_off() {
1721 self.tables[slot].delete(rid)?;
1722 self.free_overflow_chain(slot, old_pages);
1723 return Ok(());
1724 }
1725 let tx_id = self.next_tx();
1726 self.wal_log(tx_id, WalRecordType::Delete, table, rid, &[])?;
1728 self.tables[slot].delete(rid)?;
1729 self.free_overflow_chain(slot, old_pages);
1730 Ok(())
1731 }
1732
1733 pub fn delete_many(&mut self, table: &str, rids: &[RowId]) -> io::Result<u64> {
1737 let slot = self.slot_of(table)?;
1745 let old_pages = self.collect_overflow_pages(slot, rids)?;
1748 if self.wal.is_off() {
1749 let count = self.tables[slot].delete_many(rids)?;
1750 self.free_overflow_chain(slot, old_pages);
1751 return Ok(count);
1752 }
1753 let tx_id = self.next_tx();
1754 for &rid in rids {
1755 let payload = encode_wal_payload(table, rid, &[]);
1756 self.wal.append(tx_id, WalRecordType::Delete, &payload)?;
1757 }
1758 if self.active_tx_id.is_none() && !rids.is_empty() {
1759 self.pending_autocommit_tx_ids.push(tx_id);
1760 }
1761 let count = self.tables[slot].delete_many(rids)?;
1762 self.free_overflow_chain(slot, old_pages);
1763 Ok(count)
1764 }
1765
1766 fn collect_overflow_pages(&self, slot: usize, rids: &[RowId]) -> io::Result<Vec<u32>> {
1769 if !self.tables[slot].has_overflow_rows() {
1770 return Ok(Vec::new());
1771 }
1772 let mut pages = Vec::new();
1773 for &rid in rids {
1774 pages.extend(self.tables[slot].overflow_chain_pages_at(rid)?);
1775 }
1776 Ok(pages)
1777 }
1778
1779 pub fn scan_delete_matching<P>(&mut self, table: &str, pred: P) -> io::Result<u64>
1789 where
1790 P: FnMut(&[u8]) -> bool,
1791 {
1792 self.by_name_mut(table)?.scan_delete_matching(pred)
1793 }
1794
1795 pub fn scan_delete_matching_logged<P>(&mut self, table: &str, pred: P) -> io::Result<u64>
1806 where
1807 P: FnMut(&[u8]) -> bool,
1808 {
1809 if self.wal.is_off() {
1815 return self.by_name_mut(table)?.scan_delete_matching(pred);
1816 }
1817 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1821 io::Error::new(
1822 io::ErrorKind::NotFound,
1823 format!("table '{table}' not found"),
1824 )
1825 })?;
1826 let tx_id = self.next_tx();
1827 let autocommit = self.active_tx_id.is_none();
1828 let Catalog { tables, wal, .. } = self;
1831 let tbl = &mut tables[slot];
1832 let name_bytes = table.as_bytes();
1836 let count = tbl.scan_delete_matching_with_hook(pred, |rid, row_bytes| {
1837 let mut payload: Vec<u8> =
1838 Vec::with_capacity(4 + name_bytes.len() + 10 + row_bytes.len());
1839 payload.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
1840 payload.extend_from_slice(name_bytes);
1841 payload.extend_from_slice(&rid.page_id.to_le_bytes());
1842 payload.extend_from_slice(&rid.slot_index.to_le_bytes());
1843 payload.extend_from_slice(&0u32.to_le_bytes());
1847 let _ = wal.append(tx_id, WalRecordType::Delete, &payload);
1854 })?;
1855 if autocommit && count > 0 {
1856 self.pending_autocommit_tx_ids.push(tx_id);
1857 }
1858 Ok(count)
1860 }
1861
1862 pub fn scan_patch_matching_logged<P, M>(
1870 &mut self,
1871 table: &str,
1872 pred: P,
1873 try_mutate: M,
1874 ) -> io::Result<(u64, Vec<RowId>)>
1875 where
1876 P: FnMut(&[u8]) -> bool,
1877 M: FnMut(&mut [u8]) -> Option<u16>,
1878 {
1879 if self.wal.is_off() {
1880 return self.by_name_mut(table)?.scan_patch_matching_with_hook(
1881 pred,
1882 try_mutate,
1883 |_, _| {},
1884 );
1885 }
1886 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1887 io::Error::new(
1888 io::ErrorKind::NotFound,
1889 format!("table '{table}' not found"),
1890 )
1891 })?;
1892 let tx_id = self.next_tx();
1893 let autocommit = self.active_tx_id.is_none();
1894 let Catalog { tables, wal, .. } = self;
1895 let tbl = &mut tables[slot];
1896 let name_bytes = table.as_bytes();
1897 let result = tbl.scan_patch_matching_with_hook(pred, try_mutate, |rid, row_bytes| {
1898 let mut payload: Vec<u8> =
1899 Vec::with_capacity(4 + name_bytes.len() + 10 + row_bytes.len());
1900 payload.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
1901 payload.extend_from_slice(name_bytes);
1902 payload.extend_from_slice(&rid.page_id.to_le_bytes());
1903 payload.extend_from_slice(&rid.slot_index.to_le_bytes());
1904 payload.extend_from_slice(&(row_bytes.len() as u32).to_le_bytes());
1905 payload.extend_from_slice(row_bytes);
1906 let _ = wal.append(tx_id, WalRecordType::Update, &payload);
1907 })?;
1908 if autocommit && result.0 > 0 {
1909 self.pending_autocommit_tx_ids.push(tx_id);
1910 }
1911 Ok(result)
1912 }
1913
1914 pub fn update(&mut self, table: &str, rid: RowId, values: &Row) -> io::Result<RowId> {
1915 if self.wal.is_off() {
1918 let slot = self.slot_of(table)?;
1919 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
1920 let new_rid = self.tables[slot].update(rid, values)?;
1921 self.free_overflow_chain(slot, old_pages);
1922 return Ok(new_rid);
1923 }
1924 let slot = self.slot_of(table)?;
1925 self.tables[slot].preflight_update(rid, values)?;
1926 let tx_id = self.next_tx();
1927 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
1931 let row_bytes = {
1936 let Catalog { tables, wal, .. } = self;
1937 encode_row_with_spill_logged(&mut tables[slot], wal, tx_id, values)?
1938 };
1939 check_encoded_row_size(&row_bytes)?;
1943 self.wal_log(tx_id, WalRecordType::Update, table, rid, &row_bytes)?;
1944 let new_rid = self.tables[slot].update_encoded(rid, values, &row_bytes, None)?;
1945 self.free_overflow_chain(slot, old_pages);
1946 Ok(new_rid)
1947 }
1948
1949 pub fn update_hinted(
1953 &mut self,
1954 table: &str,
1955 rid: RowId,
1956 values: &Row,
1957 changed_col_indices: Option<&[usize]>,
1958 ) -> io::Result<RowId> {
1959 if self.wal.is_off() {
1963 let slot = self.slot_of(table)?;
1964 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
1965 let new_rid = self.tables[slot].update_hinted(rid, values, changed_col_indices)?;
1966 self.free_overflow_chain(slot, old_pages);
1967 return Ok(new_rid);
1968 }
1969 let slot = self.slot_of(table)?;
1970 self.tables[slot].preflight_update(rid, values)?;
1971 let tx_id = self.next_tx();
1972 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
1973 let row_bytes = {
1974 let Catalog { tables, wal, .. } = self;
1975 encode_row_with_spill_logged(&mut tables[slot], wal, tx_id, values)?
1976 };
1977 check_encoded_row_size(&row_bytes)?;
1979 self.wal_log(tx_id, WalRecordType::Update, table, rid, &row_bytes)?;
1980 let new_rid =
1981 self.tables[slot].update_encoded(rid, values, &row_bytes, changed_col_indices)?;
1982 self.free_overflow_chain(slot, old_pages);
1983 Ok(new_rid)
1984 }
1985
1986 #[inline]
1998 pub fn with_row_bytes_mut<F>(&mut self, table: &str, rid: RowId, f: F) -> io::Result<bool>
1999 where
2000 F: FnOnce(&mut [u8]),
2001 {
2002 self.by_name_mut(table)?.with_row_bytes_mut(rid, f)
2003 }
2004
2005 #[inline]
2019 pub fn update_row_bytes_logged<F>(&mut self, table: &str, rid: RowId, f: F) -> io::Result<bool>
2020 where
2021 F: FnOnce(&mut [u8]),
2022 {
2023 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
2024 io::Error::new(
2025 io::ErrorKind::NotFound,
2026 format!("table '{table}' not found"),
2027 )
2028 })?;
2029 self.update_row_bytes_logged_by_slot(slot, rid, f)
2030 }
2031
2032 #[inline]
2037 pub fn update_row_bytes_logged_by_slot<F>(
2038 &mut self,
2039 slot: usize,
2040 rid: RowId,
2041 f: F,
2042 ) -> io::Result<bool>
2043 where
2044 F: FnOnce(&mut [u8]),
2045 {
2046 let tbl = &mut self.tables[slot];
2049 let ok = tbl.with_row_bytes_mut(rid, f)?;
2050 if !ok {
2051 return Ok(false);
2052 }
2053 if self.wal.is_off() {
2057 return Ok(true);
2058 }
2059 let new_bytes = match tbl.heap.get(rid) {
2062 Some(b) => b,
2063 None => return Ok(false),
2065 };
2066 let table_name = tbl.schema.table_name.clone();
2069 let tx_id = self.next_tx();
2070 self.wal_log(tx_id, WalRecordType::Update, &table_name, rid, &new_bytes)?;
2071 Ok(true)
2072 }
2073
2074 #[inline]
2086 pub fn patch_var_col_in_place(
2087 &mut self,
2088 table: &str,
2089 rid: RowId,
2090 col_idx: usize,
2091 new_value: Option<&[u8]>,
2092 ) -> io::Result<bool> {
2093 self.by_name_mut(table)?
2094 .patch_var_col_in_place(rid, col_idx, new_value)
2095 }
2096
2097 pub fn patch_var_col_logged(
2103 &mut self,
2104 table: &str,
2105 rid: RowId,
2106 col_idx: usize,
2107 new_value: Option<&[u8]>,
2108 ) -> io::Result<bool> {
2109 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
2110 io::Error::new(
2111 io::ErrorKind::NotFound,
2112 format!("table '{table}' not found"),
2113 )
2114 })?;
2115 let tbl = &mut self.tables[slot];
2116 let ok = tbl.patch_var_col_in_place(rid, col_idx, new_value)?;
2117 if !ok {
2118 return Ok(false);
2119 }
2120 if self.wal.is_off() {
2123 return Ok(true);
2124 }
2125 let new_bytes = match tbl.heap.get(rid) {
2126 Some(b) => b,
2127 None => return Ok(false),
2128 };
2129 let table_name = tbl.schema.table_name.clone();
2130 let tx_id = self.next_tx();
2131 self.wal_log(tx_id, WalRecordType::Update, &table_name, rid, &new_bytes)?;
2132 Ok(true)
2133 }
2134
2135 pub fn scan(&self, table: &str) -> io::Result<impl Iterator<Item = (RowId, Row)> + '_> {
2136 Ok(self.by_name(table)?.scan())
2137 }
2138
2139 pub fn for_each_row_raw<F>(&self, table: &str, f: F) -> io::Result<()>
2142 where
2143 F: FnMut(RowId, &[u8]),
2144 {
2145 self.by_name(table)?.for_each_row_raw(f);
2146 Ok(())
2147 }
2148
2149 pub fn try_for_each_row_raw<F>(&self, table: &str, f: F) -> io::Result<()>
2154 where
2155 F: FnMut(RowId, &[u8]) -> std::ops::ControlFlow<()>,
2156 {
2157 self.by_name(table)?.try_for_each_row_raw(f);
2158 Ok(())
2159 }
2160
2161 pub fn create_index(&mut self, table: &str, column: &str) -> io::Result<()> {
2162 self.create_index_unique(table, column, false)
2163 }
2164
2165 pub fn create_index_unique(
2170 &mut self,
2171 table: &str,
2172 column: &str,
2173 unique: bool,
2174 ) -> io::Result<()> {
2175 self.invalidate_structure();
2176 let data_dir = self.data_dir.clone();
2177 self.by_name_mut(table)?
2178 .create_index_with_unique(column, &data_dir, unique)?;
2179 self.persist()
2183 }
2184
2185 pub fn active_catalog_version(&self) -> u16 {
2186 self.active_catalog_version
2187 }
2188
2189 pub fn next_index_id(&self) -> u64 {
2190 self.next_index_id
2191 }
2192
2193 pub fn index_metadata(&self, table: &str) -> Option<Vec<IndexMetadata>> {
2195 let table_ref = self.get_table(table)?;
2196 let mut metadata = table_ref
2197 .indexed_column_metas()
2198 .into_iter()
2199 .map(|index| IndexMetadata {
2200 unique: index.unique,
2201 source: IndexKeySource::Column { column: index.name },
2202 })
2203 .collect::<Vec<_>>();
2204 metadata.extend(table_ref.expression_index_metas().into_iter().map(|index| {
2205 IndexMetadata {
2206 unique: index.unique,
2207 source: IndexKeySource::Expression {
2208 index_id: index.index_id,
2209 canonical_version: index.canonical_version,
2210 canonical_text: index.canonical_text,
2211 json_path: index.json_path,
2212 },
2213 }
2214 }));
2215 Some(metadata)
2216 }
2217
2218 pub fn expression_index_metadata(&self, table: &str) -> Option<Vec<ExpressionIndexMeta>> {
2219 Some(self.get_table(table)?.expression_index_metas())
2220 }
2221
2222 pub fn expression_index_btree(&self, table: &str, index_id: u64) -> Option<&BTree> {
2223 self.get_table(table)?.expression_index_btree(index_id)
2224 }
2225
2226 pub fn index_stats(&self, table: &str, column: &str) -> Option<IndexStats> {
2230 Some(self.get_table(table)?.index(column)?.stats())
2231 }
2232
2233 pub fn expression_index_stats(&self, table: &str, index_id: u64) -> Option<IndexStats> {
2235 Some(
2236 self.get_table(table)?
2237 .expression_index_btree(index_id)?
2238 .stats(),
2239 )
2240 }
2241
2242 pub fn expression_index_btree_mut(&mut self, table: &str, index_id: u64) -> Option<&mut BTree> {
2243 self.get_table_mut(table)?
2244 .expression_index_btree_mut(index_id)
2245 }
2246
2247 pub fn expression_index_lookup_all(
2248 &self,
2249 table: &str,
2250 index_id: u64,
2251 key: &Value,
2252 ) -> io::Result<Vec<RowId>> {
2253 let tree = self
2254 .by_name(table)?
2255 .expression_index_btree(index_id)
2256 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2257 Ok(tree.lookup_all(key))
2258 }
2259
2260 pub fn expression_index_range_rids(
2261 &self,
2262 table: &str,
2263 index_id: u64,
2264 start: Option<&Value>,
2265 end: Option<&Value>,
2266 ) -> io::Result<Vec<RowId>> {
2267 let tree = self
2268 .by_name(table)?
2269 .expression_index_btree(index_id)
2270 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2271 Ok(tree.raw_range_rids(start, end))
2272 }
2273
2274 pub fn expression_index_ordered_rids(
2275 &self,
2276 table: &str,
2277 index_id: u64,
2278 ) -> io::Result<Vec<RowId>> {
2279 let tree = self
2280 .by_name(table)?
2281 .expression_index_btree(index_id)
2282 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2283 Ok(tree.ordered_rids_nulls_last())
2284 }
2285
2286 pub fn expression_index_ordered_rids_bounded(
2287 &self,
2288 table: &str,
2289 index_id: u64,
2290 direction: IndexOrderDirection,
2291 offset: usize,
2292 limit: usize,
2293 ) -> io::Result<Vec<RowId>> {
2294 let tree = self
2295 .by_name(table)?
2296 .expression_index_btree(index_id)
2297 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2298 Ok(tree.bounded_ordered_rids_nulls_last(
2299 direction == IndexOrderDirection::Desc,
2300 offset,
2301 limit,
2302 ))
2303 }
2304
2305 pub fn drop_expression_index(&mut self, table: &str, index_id: u64) -> io::Result<()> {
2306 self.invalidate_structure();
2307 validate_table_name(table)?;
2308 let removed = self
2309 .by_name_mut(table)?
2310 .take_expression_index(index_id)
2311 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2312 match self.persist_at_activation_boundary() {
2313 Ok(()) => {}
2314 Err(CatalogPersistError::BeforeActivation(error)) => {
2315 self.by_name_mut(table)?.restore_expression_index(removed);
2316 return Err(error);
2317 }
2318 Err(CatalogPersistError::AfterActivation(error)) => {
2319 warn!(
2320 path = %self.data_dir.display(),
2321 error = %error,
2322 "expression index drop committed but catalog directory sync failed"
2323 );
2324 }
2325 }
2326 let index_path = self
2327 .data_dir
2328 .join(expression_index_file_name(table, index_id));
2329 if let Err(error) = fs::remove_file(&index_path) {
2330 if error.kind() != io::ErrorKind::NotFound {
2331 warn!(path = %index_path.display(), error = %error, "failed to remove dropped expression index file");
2332 }
2333 } else if let Err(error) = sync_directory(&self.data_dir) {
2334 warn!(path = %self.data_dir.display(), error = %error, "failed to sync expression index deletion");
2335 }
2336 Ok(())
2337 }
2338
2339 pub fn create_expression_index_metadata(
2343 &mut self,
2344 table: &str,
2345 canonical_version: u16,
2346 canonical_text: impl Into<String>,
2347 json_path: StoredJsonPathV1,
2348 unique: bool,
2349 ) -> io::Result<u64> {
2350 self.invalidate_structure();
2351 validate_table_name(table)?;
2352 validate_column_name(&json_path.column)?;
2353 if canonical_version == 0 {
2354 return Err(io::Error::new(
2355 io::ErrorKind::InvalidInput,
2356 "expression canonical version must be non-zero",
2357 ));
2358 }
2359 let canonical_text = canonical_text.into();
2360 if canonical_text.is_empty() {
2361 return Err(io::Error::new(
2362 io::ErrorKind::InvalidInput,
2363 "expression canonical text must not be empty",
2364 ));
2365 }
2366 if canonical_version == 1 && canonical_text != json_path.canonical_text() {
2367 return Err(io::Error::new(
2368 io::ErrorKind::InvalidInput,
2369 "expression canonical text does not match its stored JSON path",
2370 ));
2371 }
2372 let table_ref = self.by_name(table)?;
2373 let root_index = table_ref
2374 .schema
2375 .column_index(&json_path.column)
2376 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "JSON root column not found"))?;
2377 if table_ref.schema.columns[root_index].type_id != TypeId::Json {
2378 return Err(io::Error::new(
2379 io::ErrorKind::InvalidInput,
2380 "expression index root column must have type json",
2381 ));
2382 }
2383 if table_ref.expression_index_metas().iter().any(|index| {
2384 index.canonical_version == canonical_version && index.canonical_text == canonical_text
2385 }) {
2386 return Err(io::Error::new(
2387 io::ErrorKind::AlreadyExists,
2388 "expression index already exists",
2389 ));
2390 }
2391
2392 let index_id = self.next_index_id;
2393 let next_index_id = index_id
2394 .checked_add(1)
2395 .ok_or_else(|| io::Error::other("expression index id space exhausted"))?;
2396 let index_path = self
2397 .data_dir
2398 .join(expression_index_file_name(table, index_id));
2399 if index_path.exists() {
2400 fs::remove_file(&index_path)?;
2404 sync_directory(&self.data_dir)?;
2405 }
2406 let meta = ExpressionIndexMeta {
2407 index_id,
2408 unique,
2409 canonical_version,
2410 canonical_text,
2411 json_path,
2412 };
2413 self.by_name_mut(table)?
2414 .install_expression_index(meta, &index_path)?;
2415 if let Err(error) = sync_directory(&self.data_dir) {
2416 self.by_name_mut(table)?
2417 .remove_expression_index_by_id(index_id);
2418 let _ = fs::remove_file(&index_path);
2419 return Err(error);
2420 }
2421
2422 let previous_version = self.active_catalog_version;
2423 let previous_next_id = self.next_index_id;
2424 self.active_catalog_version = CATALOG_VERSION;
2425 self.next_index_id = next_index_id;
2426 match self.persist_at_activation_boundary() {
2427 Ok(()) => {}
2428 Err(CatalogPersistError::BeforeActivation(error)) => {
2429 self.by_name_mut(table)?
2430 .remove_expression_index_by_id(index_id);
2431 self.active_catalog_version = previous_version;
2432 self.next_index_id = previous_next_id;
2433 let _ = fs::remove_file(&index_path);
2434 let _ = sync_directory(&self.data_dir);
2435 return Err(error);
2436 }
2437 Err(CatalogPersistError::AfterActivation(error)) => {
2438 warn!(
2439 path = %self.data_dir.display(),
2440 error = %error,
2441 "expression index creation committed but catalog directory sync failed"
2442 );
2443 }
2444 }
2445 Ok(index_id)
2446 }
2447
2448 pub fn is_index_unique(&self, table: &str, column: &str) -> Option<bool> {
2452 self.get_table(table)?.is_index_unique(column)
2453 }
2454
2455 pub fn has_index(&self, table: &str, column: &str) -> bool {
2457 self.get_table(table)
2458 .map(|t| t.has_index(column))
2459 .unwrap_or(false)
2460 }
2461
2462 pub fn index_lookup(&self, table: &str, column: &str, key: &Value) -> io::Result<Option<Row>> {
2463 Ok(self
2464 .by_name(table)?
2465 .index_lookup(column, key)
2466 .map(|(_, row)| row))
2467 }
2468
2469 pub fn list_tables(&self) -> Vec<&str> {
2470 self.tables
2474 .iter()
2475 .map(|t| t.schema.table_name.as_str())
2476 .collect()
2477 }
2478
2479 pub fn schema(&self, table: &str) -> Option<&Schema> {
2480 let slot = *self.name_to_slot.get(table)?;
2481 Some(&self.tables[slot].schema)
2482 }
2483
2484 pub fn drop_table(&mut self, name: &str) -> io::Result<()> {
2487 self.invalidate_structure();
2488 validate_table_name(name)?;
2489 let slot = *self.name_to_slot.get(name).ok_or_else(|| {
2490 io::Error::new(io::ErrorKind::NotFound, format!("table '{name}' not found"))
2491 })?;
2492 if !self.wal.is_off() {
2493 let payload = encode_ddl_drop_table(name);
2494 self.wal.append(0, WalRecordType::DdlDropTable, &payload)?;
2495 self.wal.flush()?;
2496 }
2497 let table = &self.tables[slot];
2499 let heap_path = self
2500 .data_dir
2501 .join(format!("{}.heap", table.schema.table_name));
2502 if heap_path.exists() {
2503 fs::remove_file(&heap_path)?;
2504 }
2505 for col_name in table.indexed_column_names() {
2511 let idx_path = self.data_dir.join(format!("{name}_{col_name}.idx"));
2512 if idx_path.exists() {
2513 let _ = fs::remove_file(&idx_path);
2514 }
2515 }
2516 let expression_index_ids = table.expression_index_ids();
2517 self.name_to_slot.remove(name);
2519 let last = self.tables.len() - 1;
2520 if slot != last {
2521 let moved_name = self.tables[last].schema.table_name.clone();
2522 self.tables.swap(slot, last);
2523 self.name_to_slot.insert(moved_name, slot);
2524 }
2525 self.tables.pop();
2526 self.persist()?;
2527 for index_id in expression_index_ids {
2528 let idx_path = self
2529 .data_dir
2530 .join(expression_index_file_name(name, index_id));
2531 let _ = fs::remove_file(idx_path);
2532 }
2533 Ok(())
2534 }
2535
2536 pub fn alter_table_add_column(&mut self, table: &str, col: ColumnDef) -> io::Result<()> {
2559 self.invalidate_structure();
2560 let data_dir = self.data_dir.clone();
2561 {
2562 let tbl = self.by_name_mut(table)?;
2563 if tbl.schema.columns.iter().any(|c| c.name == col.name) {
2564 return Err(io::Error::new(
2565 io::ErrorKind::AlreadyExists,
2566 format!("column '{}' already exists in table '{table}'", col.name),
2567 ));
2568 }
2569 }
2570 let barrier_lsn = if !self.wal.is_off() {
2571 let payload = encode_ddl_alter_add_column(table, &col);
2572 self.wal.append(0, WalRecordType::DdlAddColumn, &payload)?;
2573 self.wal.flush()?;
2574 self.wal.last_appended_lsn()
2575 } else {
2576 0
2577 };
2578 let tbl = self.by_name_mut(table)?;
2579
2580 let old_schema = tbl.schema.clone();
2581
2582 let has_rows = tbl.heap.scan().next().is_some();
2586
2587 if has_rows && col.required {
2588 return Err(io::Error::new(
2589 io::ErrorKind::InvalidInput,
2590 format!(
2591 "cannot add required column '{}' to non-empty table '{table}': \
2592 no default value to backfill existing rows with",
2593 col.name
2594 ),
2595 ));
2596 }
2597
2598 tbl.schema.columns.push(col);
2602 tbl.refresh_layout();
2603
2604 if has_rows {
2605 let fill: Vec<Value> = vec![Value::Empty; tbl.schema.columns.len()];
2610 tbl.rewrite_rows_for_schema_change(&old_schema, &fill, &data_dir)?;
2611 }
2612 if barrier_lsn > 0 {
2620 tbl.heap.stamp_all_pages_min_lsn(barrier_lsn)?;
2621 tbl.heap.flush()?;
2622 }
2623
2624 self.persist()?;
2625 Ok(())
2626 }
2627
2628 pub fn alter_table_drop_column(&mut self, table: &str, col_name: &str) -> io::Result<()> {
2651 self.invalidate_structure();
2652 let data_dir = self.data_dir.clone();
2653 {
2654 let tbl = self.by_name_mut(table)?;
2655 tbl.schema
2656 .columns
2657 .iter()
2658 .position(|c| c.name == col_name)
2659 .ok_or_else(|| {
2660 io::Error::new(
2661 io::ErrorKind::NotFound,
2662 format!("column '{col_name}' not found in table '{table}'"),
2663 )
2664 })?;
2665 }
2666 let removed_expression_index_ids = self
2667 .by_name_mut(table)?
2668 .remove_expression_indexes_for_root(col_name);
2669 let barrier_lsn = if !self.wal.is_off() {
2670 let payload = encode_ddl_alter_drop_column(table, col_name);
2671 self.wal.append(0, WalRecordType::DdlDropColumn, &payload)?;
2672 self.wal.flush()?;
2673 self.wal.last_appended_lsn()
2674 } else {
2675 0
2676 };
2677 let tbl = self.by_name_mut(table)?;
2678 let idx = tbl
2679 .schema
2680 .columns
2681 .iter()
2682 .position(|c| c.name == col_name)
2683 .ok_or_else(|| {
2684 io::Error::new(
2685 io::ErrorKind::NotFound,
2686 format!("column '{col_name}' not found in table '{table}'"),
2687 )
2688 })?;
2689
2690 let old_schema = tbl.schema.clone();
2692 let has_rows = tbl.heap.scan().next().is_some();
2693
2694 tbl.schema.columns.remove(idx);
2696 for (i, col) in tbl.schema.columns.iter_mut().enumerate() {
2697 col.position = i as u16;
2698 }
2699 tbl.refresh_layout();
2700
2701 if has_rows {
2702 let fill: Vec<Value> = vec![Value::Empty; tbl.schema.columns.len()];
2708 tbl.rewrite_rows_for_schema_change(&old_schema, &fill, &data_dir)?;
2709 }
2710 if barrier_lsn > 0 {
2712 tbl.heap.stamp_all_pages_min_lsn(barrier_lsn)?;
2713 tbl.heap.flush()?;
2714 }
2715
2716 self.persist()?;
2717 for index_id in removed_expression_index_ids {
2718 let idx_path = self
2719 .data_dir
2720 .join(expression_index_file_name(table, index_id));
2721 let _ = fs::remove_file(idx_path);
2722 }
2723 Ok(())
2724 }
2725}
2726
2727impl Drop for Catalog {
2728 fn drop(&mut self) {
2729 if self.read_only {
2733 return;
2734 }
2735 if self.active_tx_id.is_some() {
2736 if let Err(e) = self.abandon_active_transaction_for_drop() {
2737 warn!(error = %e, "catalog drop active transaction cleanup failed");
2738 }
2739 return;
2740 }
2741 if let Err(e) = self.checkpoint() {
2753 warn!(error = %e, "catalog drop checkpoint failed");
2754 }
2755 }
2756}
2757
2758fn encode_wal_payload(table: &str, rid: RowId, row_bytes: &[u8]) -> Vec<u8> {
2773 let name = table.as_bytes();
2774 let mut out = Vec::with_capacity(4 + name.len() + 4 + 2 + 4 + row_bytes.len());
2775 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
2776 out.extend_from_slice(name);
2777 out.extend_from_slice(&rid.page_id.to_le_bytes());
2778 out.extend_from_slice(&rid.slot_index.to_le_bytes());
2779 out.extend_from_slice(&(row_bytes.len() as u32).to_le_bytes());
2780 out.extend_from_slice(row_bytes);
2781 out
2782}
2783
2784fn decode_wal_payload(data: &[u8]) -> Option<(String, RowId, Vec<u8>)> {
2785 let mut pos = 0usize;
2786 if data.len() < 4 {
2787 return None;
2788 }
2789 let name_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2790 pos += 4;
2791 if pos + name_len > data.len() {
2792 return None;
2793 }
2794 let name = std::str::from_utf8(&data[pos..pos + name_len])
2795 .ok()?
2796 .to_string();
2797 pos += name_len;
2798 if pos + 4 + 2 + 4 > data.len() {
2799 return None;
2800 }
2801 let page_id = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
2802 pos += 4;
2803 let slot_index = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
2804 pos += 2;
2805 let row_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2806 pos += 4;
2807 if pos + row_len > data.len() {
2808 return None;
2809 }
2810 let row_bytes = data[pos..pos + row_len].to_vec();
2811 Some((
2812 name,
2813 RowId {
2814 page_id,
2815 slot_index,
2816 },
2817 row_bytes,
2818 ))
2819}
2820
2821fn write_overflow_chain_logged(
2827 heap: &mut HeapFile,
2828 wal: &mut Wal,
2829 table: &str,
2830 tx_id: u64,
2831 value: &[u8],
2832) -> io::Result<OverflowStub> {
2833 if value.len() > MAX_VALUE_SIZE {
2834 return Err(StorageError::ValueTooLarge {
2835 size: value.len(),
2836 max: MAX_VALUE_SIZE,
2837 }
2838 .into());
2839 }
2840 let n = value.len().div_ceil(OVERFLOW_PAYLOAD_CAP).max(1);
2841 let mut pages = Vec::with_capacity(n);
2842 for _ in 0..n {
2843 pages.push(heap.allocate_overflow_page()?);
2844 }
2845 for i in 0..n {
2846 let start = i * OVERFLOW_PAYLOAD_CAP;
2847 let end = (start + OVERFLOW_PAYLOAD_CAP).min(value.len());
2848 let chunk = &value[start..end];
2849 let next = if i + 1 < n {
2850 pages[i + 1]
2851 } else {
2852 OVERFLOW_CHAIN_END
2853 };
2854 let payload = encode_overflow_write_payload(table, pages[i], next, chunk);
2855 wal.append(tx_id, WalRecordType::OverflowWrite, &payload)?;
2856 let lsn = wal.last_appended_lsn();
2857 heap.write_overflow_page(pages[i], next, chunk, lsn)?;
2858 }
2859 Ok(OverflowStub::new(
2860 value.len() as u64,
2861 pages[0],
2862 crc32fast::hash(value),
2863 ))
2864}
2865
2866fn encode_row_with_spill_logged(
2871 tbl: &mut Table,
2872 wal: &mut Wal,
2873 tx_id: u64,
2874 values: &Row,
2875) -> io::Result<Vec<u8>> {
2876 let v1_len = crate::row::v1_encoded_len(tbl.row_layout(), values);
2879 let is_indexed = tbl.indexed_col_mask();
2880 let chosen = plan_spill(tbl.row_layout(), values, v1_len, &is_indexed);
2881 if chosen.is_empty() {
2882 let mut v1 = Vec::new();
2883 encode_row_into(&tbl.schema, values, &mut v1);
2884 return Ok(v1);
2885 }
2886 let table_name = tbl.schema.table_name.clone();
2887 let n_var = tbl.row_layout().n_var();
2888 let mut spilled: Vec<Option<OverflowStub>> = vec![None; n_var];
2889 for col_idx in chosen {
2890 let var_idx = tbl
2891 .row_layout()
2892 .var_index(col_idx)
2893 .expect("plan_spill only returns var columns");
2894 let bytes: Vec<u8> = match &values[col_idx] {
2895 Value::Str(s) => s.as_bytes().to_vec(),
2896 Value::Bytes(b) => b.to_vec(),
2897 Value::Json(b) => b.to_vec(),
2898 _ => continue,
2899 };
2900 let stub = write_overflow_chain_logged(&mut tbl.heap, wal, &table_name, tx_id, &bytes)?;
2901 spilled[var_idx] = Some(stub);
2902 }
2903 let mut out = Vec::new();
2904 encode_row_v2_into(&tbl.schema, tbl.row_layout(), values, &spilled, &mut out);
2905 Ok(out)
2906}
2907
2908fn encode_overflow_write_payload(
2917 table: &str,
2918 page_id: u32,
2919 next_page: u32,
2920 chunk: &[u8],
2921) -> Vec<u8> {
2922 let name = table.as_bytes();
2923 let mut out = Vec::with_capacity(2 + name.len() + 4 + 4 + 2 + chunk.len());
2924 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
2925 out.extend_from_slice(name);
2926 out.extend_from_slice(&page_id.to_le_bytes());
2927 out.extend_from_slice(&next_page.to_le_bytes());
2928 out.extend_from_slice(&(chunk.len() as u16).to_le_bytes());
2929 out.extend_from_slice(chunk);
2930 out
2931}
2932
2933fn decode_overflow_write_payload(data: &[u8]) -> Option<(String, u32, u32, Vec<u8>)> {
2934 let mut pos = 0usize;
2935 if data.len() < 2 {
2936 return None;
2937 }
2938 let name_len = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
2939 pos += 2;
2940 if pos + name_len + 4 + 4 + 2 > data.len() {
2941 return None;
2942 }
2943 let name = std::str::from_utf8(&data[pos..pos + name_len])
2944 .ok()?
2945 .to_string();
2946 pos += name_len;
2947 let page_id = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
2948 pos += 4;
2949 let next_page = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
2950 pos += 4;
2951 let chunk_len = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
2952 pos += 2;
2953 if pos + chunk_len > data.len() {
2954 return None;
2955 }
2956 Some((
2957 name,
2958 page_id,
2959 next_page,
2960 data[pos..pos + chunk_len].to_vec(),
2961 ))
2962}
2963
2964fn encode_overflow_free_payload(table: &str, pages: &[u32]) -> Vec<u8> {
2968 let name = table.as_bytes();
2969 let mut out = Vec::with_capacity(2 + name.len() + 4 + pages.len() * 4);
2970 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
2971 out.extend_from_slice(name);
2972 out.extend_from_slice(&(pages.len() as u32).to_le_bytes());
2973 for p in pages {
2974 out.extend_from_slice(&p.to_le_bytes());
2975 }
2976 out
2977}
2978
2979fn decode_overflow_free_payload(data: &[u8]) -> Option<(String, Vec<u32>)> {
2980 let mut pos = 0usize;
2981 if data.len() < 2 {
2982 return None;
2983 }
2984 let name_len = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
2985 pos += 2;
2986 if pos + name_len + 4 > data.len() {
2987 return None;
2988 }
2989 let name = std::str::from_utf8(&data[pos..pos + name_len])
2990 .ok()?
2991 .to_string();
2992 pos += name_len;
2993 let count = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2994 pos += 4;
2995 if pos + count * 4 > data.len() {
2996 return None;
2997 }
2998 let mut pages = Vec::with_capacity(count);
2999 for _ in 0..count {
3000 pages.push(u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?));
3001 pos += 4;
3002 }
3003 Some((name, pages))
3004}
3005
3006fn encode_ddl_create_table(
3009 schema: &Schema,
3010 defaults: &[Option<Value>],
3011 auto_cols: &[bool],
3012) -> Vec<u8> {
3013 let name = schema.table_name.as_bytes();
3014 let mut out = Vec::new();
3015 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3016 out.extend_from_slice(name);
3017 out.extend_from_slice(&(schema.columns.len() as u16).to_le_bytes());
3018 for col in &schema.columns {
3019 let cn = col.name.as_bytes();
3020 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3021 out.extend_from_slice(cn);
3022 out.push(col.type_id as u8);
3023 out.push(col.required as u8);
3024 out.extend_from_slice(&col.position.to_le_bytes());
3025 }
3026 encode_defaults_section(&mut out, defaults);
3030 encode_auto_section(&mut out, auto_cols);
3031 out
3032}
3033
3034fn decode_ddl_create_table(data: &[u8]) -> Option<(Schema, Vec<Option<Value>>, Vec<bool>)> {
3035 let mut pos = 0usize;
3036 if data.len() < 4 {
3037 return None;
3038 }
3039 let name_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3040 pos += 4;
3041 if pos + name_len > data.len() {
3042 return None;
3043 }
3044 let table_name = std::str::from_utf8(&data[pos..pos + name_len])
3045 .ok()?
3046 .to_string();
3047 pos += name_len;
3048 if pos + 2 > data.len() {
3049 return None;
3050 }
3051 let n_cols = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
3052 pos += 2;
3053 let mut columns = Vec::with_capacity(n_cols);
3054 for _ in 0..n_cols {
3055 if pos + 4 > data.len() {
3056 return None;
3057 }
3058 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3059 pos += 4;
3060 if pos + cn_len + 4 > data.len() {
3061 return None;
3062 }
3063 let col_name = std::str::from_utf8(&data[pos..pos + cn_len])
3064 .ok()?
3065 .to_string();
3066 pos += cn_len;
3067 let type_id = TypeId::from_u8(data[pos])?;
3068 pos += 1;
3069 let required = data[pos] != 0;
3070 pos += 1;
3071 if pos + 2 > data.len() {
3072 return None;
3073 }
3074 let position = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
3075 pos += 2;
3076 columns.push(ColumnDef {
3077 name: col_name,
3078 type_id,
3079 required,
3080 position,
3081 });
3082 }
3083 let defaults = if pos < data.len() {
3086 decode_defaults_section(data, &mut pos, columns.len())?
3087 } else {
3088 Vec::new()
3089 };
3090 let auto_cols = if pos < data.len() {
3091 decode_auto_section(data, &mut pos, columns.len())?
3092 } else {
3093 Vec::new()
3094 };
3095 Some((
3096 Schema {
3097 table_name,
3098 columns,
3099 },
3100 defaults,
3101 auto_cols,
3102 ))
3103}
3104
3105fn encode_ddl_drop_table(table_name: &str) -> Vec<u8> {
3106 let name = table_name.as_bytes();
3107 let mut out = Vec::with_capacity(4 + name.len());
3108 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3109 out.extend_from_slice(name);
3110 out
3111}
3112
3113fn encode_ddl_alter_add_column(table_name: &str, col: &ColumnDef) -> Vec<u8> {
3114 let name = table_name.as_bytes();
3115 let cn = col.name.as_bytes();
3116 let mut out = Vec::with_capacity(4 + name.len() + 4 + cn.len() + 4);
3117 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3118 out.extend_from_slice(name);
3119 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3120 out.extend_from_slice(cn);
3121 out.push(col.type_id as u8);
3122 out.push(col.required as u8);
3123 out.extend_from_slice(&col.position.to_le_bytes());
3124 out
3125}
3126
3127fn encode_ddl_alter_drop_column(table_name: &str, col_name: &str) -> Vec<u8> {
3128 let name = table_name.as_bytes();
3129 let cn = col_name.as_bytes();
3130 let mut out = Vec::with_capacity(4 + name.len() + 4 + cn.len());
3131 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3132 out.extend_from_slice(name);
3133 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3134 out.extend_from_slice(cn);
3135 out
3136}
3137
3138fn decode_ddl_table_name(data: &[u8]) -> Option<(String, usize)> {
3139 if data.len() < 4 {
3140 return None;
3141 }
3142 let name_len = u32::from_le_bytes(data[0..4].try_into().ok()?) as usize;
3143 if 4 + name_len > data.len() {
3144 return None;
3145 }
3146 let name = std::str::from_utf8(&data[4..4 + name_len])
3147 .ok()?
3148 .to_string();
3149 Some((name, 4 + name_len))
3150}
3151
3152fn decode_ddl_alter_add_column(data: &[u8]) -> Option<(String, ColumnDef)> {
3153 let (table_name, mut pos) = decode_ddl_table_name(data)?;
3154 if pos + 4 > data.len() {
3155 return None;
3156 }
3157 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3158 pos += 4;
3159 if pos + cn_len + 4 > data.len() {
3160 return None;
3161 }
3162 let col_name = std::str::from_utf8(&data[pos..pos + cn_len])
3163 .ok()?
3164 .to_string();
3165 pos += cn_len;
3166 let type_id = TypeId::from_u8(data[pos])?;
3167 pos += 1;
3168 let required = data[pos] != 0;
3169 pos += 1;
3170 if pos + 2 > data.len() {
3171 return None;
3172 }
3173 let position = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
3174 Some((
3175 table_name,
3176 ColumnDef {
3177 name: col_name,
3178 type_id,
3179 required,
3180 position,
3181 },
3182 ))
3183}
3184
3185fn decode_ddl_alter_drop_column(data: &[u8]) -> Option<(String, String)> {
3186 let (table_name, pos) = decode_ddl_table_name(data)?;
3187 if pos + 4 > data.len() {
3188 return None;
3189 }
3190 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3191 if pos + 4 + cn_len > data.len() {
3192 return None;
3193 }
3194 let col_name = std::str::from_utf8(&data[pos + 4..pos + 4 + cn_len])
3195 .ok()?
3196 .to_string();
3197 Some((table_name, col_name))
3198}
3199
3200pub(crate) struct IndexedColMeta {
3228 pub name: String,
3229 pub unique: bool,
3230}
3231
3232pub(crate) struct CatalogEntry {
3235 pub schema: Schema,
3236 pub indexed_cols: Vec<IndexedColMeta>,
3237 pub expression_indexes: Vec<ExpressionIndexMeta>,
3238 pub defaults: Vec<Option<Value>>,
3241 pub auto_cols: Vec<bool>,
3244}
3245
3246pub(crate) struct CatalogEntryRef<'a> {
3248 pub schema: &'a Schema,
3249 pub indexed_cols: Vec<IndexedColMeta>,
3250 pub expression_indexes: Vec<ExpressionIndexMeta>,
3251 pub defaults: &'a [Option<Value>],
3252 pub auto_cols: &'a [bool],
3253}
3254
3255fn encode_value_blob(out: &mut Vec<u8>, v: &Value) {
3261 out.push(v.type_id() as u8);
3262 match v {
3263 Value::Int(n) => out.extend_from_slice(&n.to_le_bytes()),
3264 Value::Float(f) => out.extend_from_slice(&f.to_bits().to_le_bytes()),
3265 Value::Bool(b) => out.push(*b as u8),
3266 Value::Str(s) => {
3267 out.extend_from_slice(&(s.len() as u32).to_le_bytes());
3268 out.extend_from_slice(s.as_bytes());
3269 }
3270 Value::DateTime(n) => out.extend_from_slice(&n.to_le_bytes()),
3271 Value::Uuid(u) => out.extend_from_slice(u),
3272 Value::Bytes(b) => {
3273 out.extend_from_slice(&(b.len() as u32).to_le_bytes());
3274 out.extend_from_slice(b);
3275 }
3276 Value::Json(b) => {
3277 out.extend_from_slice(&(b.len() as u32).to_le_bytes());
3278 out.extend_from_slice(b);
3279 }
3280 Value::Empty => {}
3281 }
3282}
3283
3284fn decode_value_blob(data: &[u8], pos: &mut usize) -> Option<Value> {
3287 let tag = *data.get(*pos)?;
3288 *pos += 1;
3289 let type_id = TypeId::from_u8(tag)?;
3290 let take_fixed = |pos: &mut usize, n: usize| -> Option<Vec<u8>> {
3291 if *pos + n > data.len() {
3292 return None;
3293 }
3294 let slice = data[*pos..*pos + n].to_vec();
3295 *pos += n;
3296 Some(slice)
3297 };
3298 match type_id {
3299 TypeId::Empty => Some(Value::Empty),
3300 TypeId::Int => Some(Value::Int(i64::from_le_bytes(
3301 take_fixed(pos, 8)?.try_into().ok()?,
3302 ))),
3303 TypeId::Float => Some(Value::Float(f64::from_bits(u64::from_le_bytes(
3304 take_fixed(pos, 8)?.try_into().ok()?,
3305 )))),
3306 TypeId::Bool => Some(Value::Bool(take_fixed(pos, 1)?[0] != 0)),
3307 TypeId::DateTime => Some(Value::DateTime(i64::from_le_bytes(
3308 take_fixed(pos, 8)?.try_into().ok()?,
3309 ))),
3310 TypeId::Uuid => Some(Value::Uuid(take_fixed(pos, 16)?.try_into().ok()?)),
3311 TypeId::Str => {
3312 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
3313 Some(Value::Str(String::from_utf8(take_fixed(pos, len)?).ok()?))
3314 }
3315 TypeId::Bytes => {
3316 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
3317 Some(Value::Bytes(take_fixed(pos, len)?))
3318 }
3319 TypeId::Json => {
3320 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
3321 Some(Value::Json(take_fixed(pos, len)?.into()))
3322 }
3323 }
3324}
3325
3326fn encode_defaults_section(out: &mut Vec<u8>, defaults: &[Option<Value>]) {
3330 let present: Vec<(u16, &Value)> = defaults
3331 .iter()
3332 .enumerate()
3333 .filter_map(|(i, d)| d.as_ref().map(|v| (i as u16, v)))
3334 .collect();
3335 out.extend_from_slice(&(present.len() as u16).to_le_bytes());
3336 for (pos, v) in present {
3337 out.extend_from_slice(&pos.to_le_bytes());
3338 encode_value_blob(out, v);
3339 }
3340}
3341
3342fn decode_defaults_section(
3345 data: &[u8],
3346 pos: &mut usize,
3347 n_cols: usize,
3348) -> Option<Vec<Option<Value>>> {
3349 if *pos + 2 > data.len() {
3350 return None;
3351 }
3352 let count = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3353 *pos += 2;
3354 let mut out = vec![None; n_cols];
3355 for _ in 0..count {
3356 if *pos + 2 > data.len() {
3357 return None;
3358 }
3359 let col = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3360 *pos += 2;
3361 let value = decode_value_blob(data, pos)?;
3362 if col < n_cols {
3363 out[col] = Some(value);
3364 }
3365 }
3366 Some(out)
3367}
3368
3369fn encode_auto_section(out: &mut Vec<u8>, auto_cols: &[bool]) {
3373 let present: Vec<u16> = auto_cols
3374 .iter()
3375 .enumerate()
3376 .filter_map(|(i, &a)| if a { Some(i as u16) } else { None })
3377 .collect();
3378 out.extend_from_slice(&(present.len() as u16).to_le_bytes());
3379 for pos in present {
3380 out.extend_from_slice(&pos.to_le_bytes());
3381 }
3382}
3383
3384fn decode_auto_section(data: &[u8], pos: &mut usize, n_cols: usize) -> Option<Vec<bool>> {
3387 if *pos + 2 > data.len() {
3388 return None;
3389 }
3390 let count = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3391 *pos += 2;
3392 let mut out = vec![false; n_cols];
3393 for _ in 0..count {
3394 if *pos + 2 > data.len() {
3395 return None;
3396 }
3397 let col = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3398 *pos += 2;
3399 if col < n_cols {
3400 out[col] = true;
3401 }
3402 }
3403 Some(out)
3404}
3405
3406fn push_catalog_string(out: &mut Vec<u8>, value: &str) -> io::Result<()> {
3407 let len = u32::try_from(value.len())
3408 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "catalog string is too large"))?;
3409 out.extend_from_slice(&len.to_le_bytes());
3410 out.extend_from_slice(value.as_bytes());
3411 Ok(())
3412}
3413
3414fn encode_expression_indexes(out: &mut Vec<u8>, indexes: &[ExpressionIndexMeta]) -> io::Result<()> {
3415 let count = u16::try_from(indexes.len()).map_err(|_| {
3416 io::Error::new(
3417 io::ErrorKind::InvalidInput,
3418 "too many expression indexes on one table",
3419 )
3420 })?;
3421 out.extend_from_slice(&count.to_le_bytes());
3422 for index in indexes {
3423 out.extend_from_slice(&index.index_id.to_le_bytes());
3424 out.push(u8::from(index.unique));
3425 out.extend_from_slice(&index.canonical_version.to_le_bytes());
3426 push_catalog_string(out, &index.canonical_text)?;
3427 push_catalog_string(out, &index.json_path.column)?;
3428 let segment_count = u16::try_from(index.json_path.segments.len()).map_err(|_| {
3429 io::Error::new(
3430 io::ErrorKind::InvalidInput,
3431 "JSON path has too many segments",
3432 )
3433 })?;
3434 out.extend_from_slice(&segment_count.to_le_bytes());
3435 for segment in &index.json_path.segments {
3436 match segment {
3437 StoredJsonPathSegmentV1::Key(key) => {
3438 out.push(1);
3439 push_catalog_string(out, key)?;
3440 }
3441 StoredJsonPathSegmentV1::Index(position) => {
3442 out.push(2);
3443 out.extend_from_slice(&position.to_le_bytes());
3444 }
3445 }
3446 }
3447 }
3448 Ok(())
3449}
3450
3451fn decode_expression_indexes(data: &[u8], pos: &mut usize) -> io::Result<Vec<ExpressionIndexMeta>> {
3452 let count = read_u16(data, pos)? as usize;
3453 let mut indexes = Vec::with_capacity(count);
3454 for _ in 0..count {
3455 let index_id = read_u64(data, pos)?;
3456 if index_id == 0 {
3457 return Err(io::Error::new(
3458 io::ErrorKind::InvalidData,
3459 "expression index id must be non-zero",
3460 ));
3461 }
3462 let unique = read_u8(data, pos)? != 0;
3463 let canonical_version = read_u16(data, pos)?;
3464 let canonical_len = read_u32(data, pos)? as usize;
3465 let canonical_text = read_string(data, pos, canonical_len)?;
3466 let column_len = read_u32(data, pos)? as usize;
3467 let column = read_string(data, pos, column_len)?;
3468 let segment_count = read_u16(data, pos)? as usize;
3469 let mut segments = Vec::with_capacity(segment_count);
3470 for _ in 0..segment_count {
3471 match read_u8(data, pos)? {
3472 1 => {
3473 let len = read_u32(data, pos)? as usize;
3474 segments.push(StoredJsonPathSegmentV1::Key(read_string(data, pos, len)?));
3475 }
3476 2 => segments.push(StoredJsonPathSegmentV1::Index(read_u32(data, pos)?)),
3477 tag => {
3478 return Err(io::Error::new(
3479 io::ErrorKind::InvalidData,
3480 format!("unknown stored JSON path segment tag: {tag}"),
3481 ));
3482 }
3483 }
3484 }
3485 indexes.push(ExpressionIndexMeta {
3486 index_id,
3487 unique,
3488 canonical_version,
3489 canonical_text,
3490 json_path: StoredJsonPathV1 { column, segments },
3491 });
3492 }
3493 Ok(indexes)
3494}
3495
3496fn write_catalog_file(
3497 path: &Path,
3498 version: u16,
3499 next_index_id: u64,
3500 entries: &[CatalogEntryRef<'_>],
3501) -> io::Result<()> {
3502 if !(1..=CATALOG_VERSION).contains(&version) {
3503 return Err(io::Error::new(
3504 io::ErrorKind::InvalidInput,
3505 format!("unsupported catalog write version: {version}"),
3506 ));
3507 }
3508 let mut buf: Vec<u8> = Vec::with_capacity(64);
3509 buf.extend_from_slice(CATALOG_MAGIC);
3510 buf.extend_from_slice(&version.to_le_bytes());
3511 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
3512 if version >= 6 {
3513 buf.extend_from_slice(&next_index_id.to_le_bytes());
3514 }
3515
3516 for entry in entries {
3517 let schema = entry.schema;
3518 let name = schema.table_name.as_bytes();
3519 buf.extend_from_slice(&(name.len() as u32).to_le_bytes());
3520 buf.extend_from_slice(name);
3521 buf.extend_from_slice(&(schema.columns.len() as u16).to_le_bytes());
3522 for col in &schema.columns {
3523 let cn = col.name.as_bytes();
3524 buf.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3525 buf.extend_from_slice(cn);
3526 buf.push(col.type_id as u8);
3527 buf.push(if col.required { 1 } else { 0 });
3528 buf.extend_from_slice(&col.position.to_le_bytes());
3529 }
3530 buf.extend_from_slice(&(entry.indexed_cols.len() as u16).to_le_bytes());
3532 for meta in &entry.indexed_cols {
3533 let cn = meta.name.as_bytes();
3534 buf.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3535 buf.extend_from_slice(cn);
3536 buf.push(if meta.unique { 1 } else { 0 });
3537 }
3538 encode_defaults_section(&mut buf, entry.defaults);
3540 encode_auto_section(&mut buf, entry.auto_cols);
3542 if version >= 6 {
3543 encode_expression_indexes(&mut buf, &entry.expression_indexes)?;
3544 }
3545 }
3546
3547 let crc = crc32fast::hash(&buf);
3551 buf.extend_from_slice(&crc.to_le_bytes());
3552
3553 let mut f = fs::OpenOptions::new()
3554 .create(true)
3555 .write(true)
3556 .truncate(true)
3557 .open(path)?;
3558 f.write_all(&buf)?;
3559 f.sync_data()?;
3560 Ok(())
3561}
3562
3563struct CatalogFile {
3564 version: u16,
3565 next_index_id: u64,
3566 entries: Vec<CatalogEntry>,
3567}
3568
3569fn read_catalog_file(path: &Path) -> io::Result<CatalogFile> {
3570 read_catalog_file_with_max_version(path, CATALOG_VERSION)
3571}
3572
3573pub fn read_active_catalog_version(data_dir: &Path) -> io::Result<u16> {
3579 let cat_path = data_dir.join(CATALOG_FILE);
3580 Ok(read_catalog_file(&cat_path)?.version)
3581}
3582
3583fn read_catalog_file_with_max_version(
3584 path: &Path,
3585 max_supported_version: u16,
3586) -> io::Result<CatalogFile> {
3587 let mut f = fs::File::open(path)?;
3588 let mut buf = Vec::new();
3589 f.read_to_end(&mut buf)?;
3590
3591 let mut pos = 0usize;
3592 if buf.len() < 14 || &buf[0..4] != CATALOG_MAGIC {
3594 return Err(io::Error::new(
3595 io::ErrorKind::InvalidData,
3596 "bad catalog magic",
3597 ));
3598 }
3599
3600 let payload = &buf[..buf.len() - 4];
3602 let stored_crc = u32::from_le_bytes(
3603 buf[buf.len() - 4..]
3604 .try_into()
3605 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog CRC"))?,
3606 );
3607 let computed_crc = crc32fast::hash(payload);
3608 if stored_crc != computed_crc {
3609 return Err(io::Error::new(
3610 io::ErrorKind::InvalidData,
3611 format!(
3612 "catalog CRC32 mismatch: expected {stored_crc:#010x}, got {computed_crc:#010x}"
3613 ),
3614 ));
3615 }
3616 let buf = &buf[..buf.len() - 4];
3618 pos += 4;
3619 let version = u16::from_le_bytes(
3620 buf[pos..pos + 2]
3621 .try_into()
3622 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog header"))?,
3623 );
3624 pos += 2;
3625 if version == 0 || version > max_supported_version {
3635 return Err(io::Error::new(
3636 io::ErrorKind::InvalidData,
3637 format!("unsupported catalog version: {version}"),
3638 ));
3639 }
3640 let n_tables = u32::from_le_bytes(
3641 buf[pos..pos + 4]
3642 .try_into()
3643 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog header"))?,
3644 ) as usize;
3645 pos += 4;
3646 let next_index_id = if version >= 6 {
3656 let id = read_u64(buf, &mut pos)?;
3657 if id == 0 {
3658 return Err(io::Error::new(
3659 io::ErrorKind::InvalidData,
3660 "catalog next index id must be non-zero",
3661 ));
3662 }
3663 id
3664 } else {
3665 1
3668 };
3669
3670 if n_tables > buf.len() {
3677 return Err(io::Error::new(
3678 io::ErrorKind::InvalidData,
3679 format!("catalog file corrupt: implausible table count {n_tables}"),
3680 ));
3681 }
3682
3683 let mut entries = Vec::with_capacity(n_tables);
3684 for _ in 0..n_tables {
3685 let name_len = read_u32(buf, &mut pos)? as usize;
3686 let table_name = read_string(buf, &mut pos, name_len)?;
3687 let n_cols = read_u16(buf, &mut pos)? as usize;
3688
3689 let mut columns = Vec::with_capacity(n_cols);
3690 for _ in 0..n_cols {
3691 let cname_len = read_u32(buf, &mut pos)? as usize;
3692 let name = read_string(buf, &mut pos, cname_len)?;
3693 let type_id_raw = read_u8(buf, &mut pos)?;
3694 let type_id = type_id_from_u8(type_id_raw)?;
3695 let required = read_u8(buf, &mut pos)? != 0;
3696 let position = read_u16(buf, &mut pos)?;
3697 columns.push(ColumnDef {
3698 name,
3699 type_id,
3700 required,
3701 position,
3702 });
3703 }
3704
3705 let indexed_cols: Vec<IndexedColMeta> = if version >= 3 {
3711 let n = read_u16(buf, &mut pos)? as usize;
3712 let mut v = Vec::with_capacity(n);
3713 for _ in 0..n {
3714 let l = read_u32(buf, &mut pos)? as usize;
3715 let name = read_string(buf, &mut pos, l)?;
3716 let unique = read_u8(buf, &mut pos)? != 0;
3717 v.push(IndexedColMeta { name, unique });
3718 }
3719 v
3720 } else if version >= 2 {
3721 let n = read_u16(buf, &mut pos)? as usize;
3722 let mut v = Vec::with_capacity(n);
3723 for _ in 0..n {
3724 let l = read_u32(buf, &mut pos)? as usize;
3725 let name = read_string(buf, &mut pos, l)?;
3726 v.push(IndexedColMeta {
3727 name,
3728 unique: false,
3729 });
3730 }
3731 v
3732 } else {
3733 Vec::new()
3734 };
3735
3736 let defaults = if version >= 4 {
3740 decode_defaults_section(buf, &mut pos, columns.len()).ok_or_else(|| {
3741 io::Error::new(io::ErrorKind::InvalidData, "truncated catalog defaults")
3742 })?
3743 } else {
3744 Vec::new()
3745 };
3746
3747 let auto_cols = if version >= 5 {
3751 decode_auto_section(buf, &mut pos, columns.len()).ok_or_else(|| {
3752 io::Error::new(io::ErrorKind::InvalidData, "truncated catalog auto columns")
3753 })?
3754 } else {
3755 Vec::new()
3756 };
3757
3758 let expression_indexes = if version >= 6 {
3762 decode_expression_indexes(buf, &mut pos)?
3763 } else {
3764 Vec::new()
3765 };
3766
3767 entries.push(CatalogEntry {
3768 schema: Schema {
3769 table_name,
3770 columns,
3771 },
3772 indexed_cols,
3773 expression_indexes,
3774 defaults,
3775 auto_cols,
3776 });
3777 }
3778
3779 let mut seen_index_ids = FxHashMap::default();
3780 let mut max_index_id = 0;
3781 for entry in &entries {
3782 for index in &entry.expression_indexes {
3783 if index.canonical_version == 0 || index.canonical_text.is_empty() {
3784 return Err(io::Error::new(
3785 io::ErrorKind::InvalidData,
3786 "expression index has invalid canonical identity",
3787 ));
3788 }
3789 if index.canonical_version == 1
3790 && index.canonical_text != index.json_path.canonical_text()
3791 {
3792 return Err(io::Error::new(
3793 io::ErrorKind::InvalidData,
3794 "expression index canonical identity does not match its JSON path",
3795 ));
3796 }
3797 let Some(root) = entry
3798 .schema
3799 .columns
3800 .iter()
3801 .find(|column| column.name == index.json_path.column)
3802 else {
3803 return Err(io::Error::new(
3804 io::ErrorKind::InvalidData,
3805 "expression index JSON root is absent from its table",
3806 ));
3807 };
3808 if root.type_id != TypeId::Json {
3809 return Err(io::Error::new(
3810 io::ErrorKind::InvalidData,
3811 "expression index root column is not JSON",
3812 ));
3813 }
3814 if seen_index_ids.insert(index.index_id, ()).is_some() {
3815 return Err(io::Error::new(
3816 io::ErrorKind::InvalidData,
3817 "duplicate expression index id in catalog",
3818 ));
3819 }
3820 max_index_id = max_index_id.max(index.index_id);
3821 }
3822 }
3823 if next_index_id <= max_index_id {
3824 return Err(io::Error::new(
3825 io::ErrorKind::InvalidData,
3826 "catalog next index id does not exceed persisted index ids",
3827 ));
3828 }
3829 Ok(CatalogFile {
3830 version,
3831 next_index_id,
3832 entries,
3833 })
3834}
3835
3836fn read_u8(buf: &[u8], pos: &mut usize) -> io::Result<u8> {
3837 if *pos >= buf.len() {
3838 return Err(io::Error::new(
3839 io::ErrorKind::UnexpectedEof,
3840 "truncated catalog",
3841 ));
3842 }
3843 let v = buf[*pos];
3844 *pos += 1;
3845 Ok(v)
3846}
3847fn read_u16(buf: &[u8], pos: &mut usize) -> io::Result<u16> {
3848 if *pos + 2 > buf.len() {
3849 return Err(io::Error::new(
3850 io::ErrorKind::UnexpectedEof,
3851 "truncated catalog",
3852 ));
3853 }
3854 let v = u16::from_le_bytes(
3855 buf[*pos..*pos + 2]
3856 .try_into()
3857 .expect("bounds checked above"),
3858 );
3859 *pos += 2;
3860 Ok(v)
3861}
3862fn read_u32(buf: &[u8], pos: &mut usize) -> io::Result<u32> {
3863 if *pos + 4 > buf.len() {
3864 return Err(io::Error::new(
3865 io::ErrorKind::UnexpectedEof,
3866 "truncated catalog",
3867 ));
3868 }
3869 let v = u32::from_le_bytes(
3870 buf[*pos..*pos + 4]
3871 .try_into()
3872 .expect("bounds checked above"),
3873 );
3874 *pos += 4;
3875 Ok(v)
3876}
3877fn read_u64(buf: &[u8], pos: &mut usize) -> io::Result<u64> {
3878 if *pos + 8 > buf.len() {
3879 return Err(io::Error::new(
3880 io::ErrorKind::UnexpectedEof,
3881 "truncated catalog",
3882 ));
3883 }
3884 let value = u64::from_le_bytes(
3885 buf[*pos..*pos + 8]
3886 .try_into()
3887 .expect("bounds checked above"),
3888 );
3889 *pos += 8;
3890 Ok(value)
3891}
3892fn read_string(buf: &[u8], pos: &mut usize, len: usize) -> io::Result<String> {
3893 if *pos + len > buf.len() {
3894 return Err(io::Error::new(
3895 io::ErrorKind::UnexpectedEof,
3896 "truncated catalog string",
3897 ));
3898 }
3899 let s = std::str::from_utf8(&buf[*pos..*pos + len])
3900 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "non-utf8 in catalog"))?
3901 .to_string();
3902 *pos += len;
3903 Ok(s)
3904}
3905fn type_id_from_u8(v: u8) -> io::Result<TypeId> {
3906 match v {
3907 0 => Ok(TypeId::Empty),
3908 1 => Ok(TypeId::Int),
3909 2 => Ok(TypeId::Float),
3910 3 => Ok(TypeId::Bool),
3911 4 => Ok(TypeId::Str),
3912 5 => Ok(TypeId::DateTime),
3913 6 => Ok(TypeId::Uuid),
3914 7 => Ok(TypeId::Bytes),
3915 8 => Ok(TypeId::Json),
3916 _ => Err(io::Error::new(
3917 io::ErrorKind::InvalidData,
3918 format!("unknown type id: {v}"),
3919 )),
3920 }
3921}
3922
3923#[cfg(test)]
3924mod tests {
3925 use super::*;
3926
3927 fn fail_next_catalog_persist_at(stage: u8) {
3928 CATALOG_PERSIST_FAILPOINT.with(|failpoint| failpoint.set(stage));
3929 }
3930
3931 fn temp_catalog(name: &str) -> Catalog {
3932 let dir = std::env::temp_dir().join(format!("powdb_cat_{name}_{}", std::process::id()));
3933 Catalog::create(&dir).unwrap()
3934 }
3935
3936 fn hash_dir_tree(dir: &std::path::Path) -> String {
3941 let mut entries: Vec<std::path::PathBuf> = Vec::new();
3942 fn walk(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
3943 let mut items: Vec<_> = fs::read_dir(dir).unwrap().flatten().collect();
3944 items.sort_by_key(std::fs::DirEntry::path);
3945 for item in items {
3946 let path = item.path();
3947 if path.is_dir() {
3948 walk(&path, out);
3949 } else {
3950 out.push(path);
3951 }
3952 }
3953 }
3954 walk(dir, &mut entries);
3955 let mut hasher = crc32fast::Hasher::new();
3956 for path in &entries {
3957 hasher.update(path.to_string_lossy().as_bytes());
3958 hasher.update(&fs::read(path).unwrap());
3959 }
3960 format!("{:08x}", hasher.finalize())
3961 }
3962
3963 fn seed_quiescent_dir(dir: &std::path::Path) {
3964 let mut catalog = Catalog::create(dir).unwrap();
3965 catalog
3966 .create_table(Schema {
3967 table_name: "User".into(),
3968 columns: vec![
3969 ColumnDef {
3970 name: "name".into(),
3971 type_id: TypeId::Str,
3972 required: true,
3973 position: 0,
3974 },
3975 ColumnDef {
3976 name: "age".into(),
3977 type_id: TypeId::Int,
3978 required: false,
3979 position: 1,
3980 },
3981 ],
3982 })
3983 .unwrap();
3984 catalog.create_index("User", "age").unwrap();
3985 catalog
3986 .insert("User", &vec![Value::Str("Ada".into()), Value::Int(36)])
3987 .unwrap();
3988 catalog
3989 .insert("User", &vec![Value::Str("Bo".into()), Value::Int(20)])
3990 .unwrap();
3991 drop(catalog);
3994 }
3995
3996 #[test]
3997 fn open_read_only_serves_reads_on_clean_dir() {
3998 let dir = tempfile::tempdir().unwrap();
3999 seed_quiescent_dir(dir.path());
4000
4001 let catalog = Catalog::open_read_only(dir.path()).unwrap();
4002 let rows: Vec<_> = catalog.scan("User").unwrap().collect();
4003 assert_eq!(rows.len(), 2);
4004 let hit = catalog
4006 .index_lookup("User", "age", &Value::Int(36))
4007 .unwrap();
4008 assert_eq!(hit.unwrap()[0], Value::Str("Ada".into()));
4009 }
4010
4011 #[test]
4012 fn open_read_only_never_mutates_dir() {
4013 let dir = tempfile::tempdir().unwrap();
4014 seed_quiescent_dir(dir.path());
4015 let before = hash_dir_tree(dir.path());
4016
4017 {
4018 let catalog = Catalog::open_read_only(dir.path()).unwrap();
4019 let _ = catalog.scan("User").unwrap().count();
4020 let _ = catalog
4021 .index_lookup("User", "age", &Value::Int(20))
4022 .unwrap();
4023 }
4025 let after = hash_dir_tree(dir.path());
4026 assert_eq!(
4027 before, after,
4028 "read-only open + queries + drop must leave the directory byte-identical"
4029 );
4030 }
4031
4032 #[test]
4033 fn open_read_only_refuses_non_empty_wal() {
4034 let dir = tempfile::tempdir().unwrap();
4035 {
4038 let mut catalog = Catalog::create(dir.path()).unwrap();
4039 catalog
4040 .create_table(Schema {
4041 table_name: "T".into(),
4042 columns: vec![ColumnDef {
4043 name: "id".into(),
4044 type_id: TypeId::Int,
4045 required: true,
4046 position: 0,
4047 }],
4048 })
4049 .unwrap();
4050 catalog.insert("T", &vec![Value::Int(1)]).unwrap();
4051 catalog.sync_wal().unwrap();
4052 std::mem::forget(catalog); }
4054 let err = match Catalog::open_read_only(dir.path()) {
4055 Ok(_) => panic!("read-only open must refuse a non-empty WAL"),
4056 Err(err) => err,
4057 };
4058 assert!(
4059 err.to_string().contains("WAL is not empty"),
4060 "expected a WAL-not-empty refusal naming the remedy, got: {err}"
4061 );
4062 assert!(err.to_string().contains("read-write engine"));
4063 }
4064
4065 #[test]
4066 fn open_read_only_expression_index_reads_work() {
4067 let dir = tempfile::tempdir().unwrap();
4068 {
4069 let mut catalog = Catalog::create(dir.path()).unwrap();
4070 catalog
4071 .create_table(Schema {
4072 table_name: "Doc".into(),
4073 columns: vec![ColumnDef {
4074 name: "data".into(),
4075 type_id: TypeId::Json,
4076 required: false,
4077 position: 0,
4078 }],
4079 })
4080 .unwrap();
4081 let path =
4082 StoredJsonPathV1::new("data", vec![StoredJsonPathSegmentV1::Key("author".into())]);
4083 catalog
4084 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path, false)
4085 .unwrap();
4086 drop(catalog);
4087 }
4088 let before = hash_dir_tree(dir.path());
4090 let catalog = Catalog::open_read_only(dir.path()).unwrap();
4091 assert_eq!(catalog.scan("Doc").unwrap().count(), 0);
4092 drop(catalog);
4093 let after = hash_dir_tree(dir.path());
4094 assert_eq!(
4095 before, after,
4096 "read-only expression-index load must not write"
4097 );
4098 }
4099
4100 #[test]
4101 fn v5_reader_rejects_v6_catalog() {
4102 let dir = tempfile::tempdir().unwrap();
4103 let mut catalog = Catalog::create(dir.path()).unwrap();
4104 catalog
4105 .create_table(Schema {
4106 table_name: "Doc".into(),
4107 columns: vec![ColumnDef {
4108 name: "data".into(),
4109 type_id: TypeId::Json,
4110 required: false,
4111 position: 0,
4112 }],
4113 })
4114 .unwrap();
4115 let path =
4116 StoredJsonPathV1::new("data", vec![StoredJsonPathSegmentV1::Key("author".into())]);
4117 catalog
4118 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path, false)
4119 .unwrap();
4120 let result = read_catalog_file_with_max_version(
4121 &dir.path().join(CATALOG_FILE),
4122 LEGACY_CATALOG_VERSION,
4123 );
4124 let error = match result {
4125 Ok(_) => panic!("a v5 reader must reject v6 before decoding its payload"),
4126 Err(error) => error,
4127 };
4128 assert!(error.to_string().contains("unsupported catalog version: 6"));
4129 }
4130
4131 #[test]
4132 fn expression_index_rolls_back_only_before_catalog_rename() {
4133 let before_dir = tempfile::tempdir().unwrap();
4134 let mut before = Catalog::create(before_dir.path()).unwrap();
4135 before
4136 .create_table(Schema {
4137 table_name: "Doc".into(),
4138 columns: vec![ColumnDef {
4139 name: "data".into(),
4140 type_id: TypeId::Json,
4141 required: false,
4142 position: 0,
4143 }],
4144 })
4145 .unwrap();
4146 let path =
4147 StoredJsonPathV1::new("data", vec![StoredJsonPathSegmentV1::Key("score".into())]);
4148
4149 fail_next_catalog_persist_at(1);
4150 let error = before
4151 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path.clone(), false)
4152 .unwrap_err();
4153 assert!(error.to_string().contains("before rename"));
4154 assert_eq!(before.active_catalog_version(), LEGACY_CATALOG_VERSION);
4155 assert_eq!(before.next_index_id(), 1);
4156 assert!(before.expression_index_metadata("Doc").unwrap().is_empty());
4157 assert!(!before_dir
4158 .path()
4159 .join(expression_index_file_name("Doc", 1))
4160 .exists());
4161
4162 let before_index_id = before
4163 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path.clone(), false)
4164 .unwrap();
4165 fail_next_catalog_persist_at(1);
4166 let error = before
4167 .drop_expression_index("Doc", before_index_id)
4168 .unwrap_err();
4169 assert!(error.to_string().contains("before rename"));
4170 assert!(before
4171 .expression_index_btree("Doc", before_index_id)
4172 .is_some());
4173 assert!(before_dir
4174 .path()
4175 .join(expression_index_file_name("Doc", 1))
4176 .exists());
4177 std::mem::forget(before);
4178 let before_reopened = Catalog::open(before_dir.path()).unwrap();
4179 assert!(before_reopened
4180 .expression_index_btree("Doc", before_index_id)
4181 .is_some());
4182
4183 let after_dir = tempfile::tempdir().unwrap();
4184 let mut after = Catalog::create(after_dir.path()).unwrap();
4185 after
4186 .create_table(Schema {
4187 table_name: "Doc".into(),
4188 columns: vec![ColumnDef {
4189 name: "data".into(),
4190 type_id: TypeId::Json,
4191 required: false,
4192 position: 0,
4193 }],
4194 })
4195 .unwrap();
4196 fail_next_catalog_persist_at(2);
4197 let index_id = after
4198 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path.clone(), false)
4199 .unwrap();
4200 assert_eq!(index_id, 1);
4201 assert_eq!(after.active_catalog_version(), CATALOG_VERSION);
4202 assert_eq!(after.next_index_id(), 2);
4203 assert!(after.expression_index_btree("Doc", index_id).is_some());
4204 assert!(after_dir
4205 .path()
4206 .join(expression_index_file_name("Doc", 1))
4207 .exists());
4208 std::mem::forget(after);
4209
4210 let mut reopened = Catalog::open(after_dir.path()).unwrap();
4211 assert!(reopened.expression_index_btree("Doc", index_id).is_some());
4212 fail_next_catalog_persist_at(2);
4213 reopened.drop_expression_index("Doc", index_id).unwrap();
4214 assert!(reopened
4215 .expression_index_metadata("Doc")
4216 .unwrap()
4217 .is_empty());
4218 assert!(!after_dir
4219 .path()
4220 .join(expression_index_file_name("Doc", 1))
4221 .exists());
4222 std::mem::forget(reopened);
4223
4224 let final_open = Catalog::open(after_dir.path()).unwrap();
4225 assert!(final_open
4226 .expression_index_metadata("Doc")
4227 .unwrap()
4228 .is_empty());
4229 }
4230
4231 #[test]
4232 fn ordinary_catalog_persist_reports_post_rename_directory_sync_failure() {
4233 let dir = tempfile::tempdir().unwrap();
4234 let mut catalog = Catalog::create(dir.path()).unwrap();
4235 fail_next_catalog_persist_at(2);
4236 let error = catalog
4237 .create_table(Schema {
4238 table_name: "VisibleAfterRename".into(),
4239 columns: vec![ColumnDef {
4240 name: "id".into(),
4241 type_id: TypeId::Int,
4242 required: true,
4243 position: 0,
4244 }],
4245 })
4246 .unwrap_err();
4247 assert!(error.to_string().contains("after rename"));
4248 assert!(catalog.schema("VisibleAfterRename").is_some());
4249
4250 std::mem::forget(catalog);
4251 let reopened = Catalog::open(dir.path()).unwrap();
4252 assert!(reopened.schema("VisibleAfterRename").is_some());
4253 }
4254
4255 fn schema_two_cols() -> Schema {
4256 Schema {
4257 table_name: "T".into(),
4258 columns: vec![
4259 ColumnDef {
4260 name: "id".into(),
4261 type_id: TypeId::Int,
4262 required: true,
4263 position: 0,
4264 },
4265 ColumnDef {
4266 name: "status".into(),
4267 type_id: TypeId::Str,
4268 required: false,
4269 position: 1,
4270 },
4271 ],
4272 }
4273 }
4274
4275 #[test]
4276 fn replay_records_treats_reused_tx_ids_as_ordered_spans() {
4277 let mut cat = temp_catalog("reused_tx_ids");
4278 let schema = schema_two_cols();
4279 cat.create_table(schema.clone()).unwrap();
4280 cat.checkpoint().unwrap();
4281
4282 let mut committed_row = Vec::new();
4283 encode_row_into(
4284 &schema,
4285 &[Value::Int(1), Value::Str("committed".into())],
4286 &mut committed_row,
4287 );
4288 let mut incomplete_row = Vec::new();
4289 encode_row_into(
4290 &schema,
4291 &[Value::Int(2), Value::Str("incomplete".into())],
4292 &mut incomplete_row,
4293 );
4294
4295 let records = vec![
4296 WalRecord {
4297 tx_id: 1,
4298 record_type: WalRecordType::Begin,
4299 lsn: 1,
4300 data: Vec::new(),
4301 },
4302 WalRecord {
4303 tx_id: 1,
4304 record_type: WalRecordType::Insert,
4305 lsn: 2,
4306 data: encode_wal_payload(
4307 "T",
4308 RowId {
4309 page_id: 1,
4310 slot_index: 0,
4311 },
4312 &committed_row,
4313 ),
4314 },
4315 WalRecord {
4316 tx_id: 1,
4317 record_type: WalRecordType::Commit,
4318 lsn: 3,
4319 data: Vec::new(),
4320 },
4321 WalRecord {
4322 tx_id: 1,
4323 record_type: WalRecordType::Begin,
4324 lsn: 4,
4325 data: Vec::new(),
4326 },
4327 WalRecord {
4328 tx_id: 1,
4329 record_type: WalRecordType::Insert,
4330 lsn: 5,
4331 data: encode_wal_payload(
4332 "T",
4333 RowId {
4334 page_id: 1,
4335 slot_index: 1,
4336 },
4337 &incomplete_row,
4338 ),
4339 },
4340 ];
4341
4342 cat.apply_wal_records(&records).unwrap();
4343 let rows: Vec<_> = cat.scan("T").unwrap().collect();
4344 assert_eq!(rows.len(), 1);
4345 assert_eq!(rows[0].1[0], Value::Int(1));
4346 assert_eq!(rows[0].1[1], Value::Str("committed".into()));
4347 }
4348
4349 #[test]
4350 fn ddl_create_table_codec_roundtrips_defaults_and_auto() {
4351 let schema = schema_two_cols();
4352 let defaults = vec![None, Some(Value::Str("active".into()))];
4353 let auto_cols = vec![true, false];
4354 let encoded = encode_ddl_create_table(&schema, &defaults, &auto_cols);
4355 let (decoded_schema, decoded_defaults, decoded_auto) =
4356 decode_ddl_create_table(&encoded).unwrap();
4357 assert_eq!(decoded_schema.columns.len(), 2);
4358 assert_eq!(decoded_defaults, defaults);
4359 assert_eq!(decoded_auto, auto_cols);
4360 }
4361
4362 #[test]
4363 fn ddl_create_table_codec_back_compat_without_trailing_sections() {
4364 let schema = schema_two_cols();
4368 let full = encode_ddl_create_table(&schema, &[], &[]);
4369 let legacy = &full[..full.len() - 4];
4372 let (decoded_schema, decoded_defaults, decoded_auto) =
4373 decode_ddl_create_table(legacy).unwrap();
4374 assert_eq!(decoded_schema.columns.len(), 2);
4375 assert!(decoded_defaults.is_empty(), "no defaults section -> empty");
4376 assert!(decoded_auto.is_empty(), "no auto section -> empty");
4377 }
4378
4379 #[test]
4380 fn ddl_create_table_codec_back_compat_defaults_but_no_auto() {
4381 let schema = schema_two_cols();
4384 let defaults = vec![None, Some(Value::Str("active".into()))];
4385 let full = encode_ddl_create_table(&schema, &defaults, &[]);
4386 let legacy = &full[..full.len() - 2];
4388 let (_schema, decoded_defaults, decoded_auto) = decode_ddl_create_table(legacy).unwrap();
4389 assert_eq!(decoded_defaults, defaults);
4390 assert!(decoded_auto.is_empty());
4391 }
4392
4393 #[test]
4394 fn read_catalog_file_accepts_intermediate_versions_3_and_4() {
4395 use std::io::Write as _;
4403 fn write_legacy_catalog(path: &std::path::Path, version: u16) {
4404 let mut buf: Vec<u8> = Vec::new();
4405 buf.extend_from_slice(CATALOG_MAGIC);
4406 buf.extend_from_slice(&version.to_le_bytes());
4407 buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes());
4410 buf.extend_from_slice(b"T");
4411 buf.extend_from_slice(&2u16.to_le_bytes()); buf.extend_from_slice(&2u32.to_le_bytes());
4414 buf.extend_from_slice(b"id");
4415 buf.push(TypeId::Int as u8);
4416 buf.push(1);
4417 buf.extend_from_slice(&0u16.to_le_bytes());
4418 buf.extend_from_slice(&6u32.to_le_bytes());
4420 buf.extend_from_slice(b"status");
4421 buf.push(TypeId::Str as u8);
4422 buf.push(0);
4423 buf.extend_from_slice(&1u16.to_le_bytes());
4424 buf.extend_from_slice(&0u16.to_le_bytes());
4426 if version >= 4 {
4428 encode_defaults_section(&mut buf, &[None, None]);
4429 }
4430 let crc = crc32fast::hash(&buf);
4432 buf.extend_from_slice(&crc.to_le_bytes());
4433 let mut f = fs::File::create(path).unwrap();
4434 f.write_all(&buf).unwrap();
4435 }
4436
4437 for version in [3u16, 4u16] {
4438 let path = std::env::temp_dir().join(format!(
4439 "powdb_cat_v{version}_compat_{}.bin",
4440 std::process::id()
4441 ));
4442 write_legacy_catalog(&path, version);
4443 let catalog_file = read_catalog_file(&path)
4444 .unwrap_or_else(|e| panic!("version {version} catalog must load, got: {e}"));
4445 let entries = catalog_file.entries;
4446 assert_eq!(entries.len(), 1);
4447 assert_eq!(entries[0].schema.table_name, "T");
4448 assert_eq!(entries[0].schema.columns.len(), 2);
4449 assert!(
4450 entries[0].auto_cols.is_empty(),
4451 "v{version} has no auto cols"
4452 );
4453 fs::remove_file(&path).ok();
4454 }
4455 }
4456
4457 #[test]
4458 fn read_catalog_file_rejects_implausible_table_count() {
4459 use std::io::Write as _;
4467 let mut buf: Vec<u8> = Vec::new();
4468 buf.extend_from_slice(CATALOG_MAGIC);
4469 buf.extend_from_slice(&CATALOG_VERSION.to_le_bytes());
4470 buf.extend_from_slice(&1000u32.to_le_bytes()); buf.extend_from_slice(&1u64.to_le_bytes()); let crc = crc32fast::hash(&buf);
4474 buf.extend_from_slice(&crc.to_le_bytes());
4475 let path =
4476 std::env::temp_dir().join(format!("powdb_cat_badcount_{}.bin", std::process::id()));
4477 fs::File::create(&path).unwrap().write_all(&buf).unwrap();
4478
4479 let msg = match read_catalog_file(&path) {
4480 Ok(_) => panic!("implausible table count must be rejected, got Ok"),
4481 Err(e) => e.to_string(),
4482 };
4483 assert!(
4484 msg.contains("implausible table count"),
4485 "expected an implausible-table-count error, got: {msg}"
4486 );
4487 fs::remove_file(&path).ok();
4488 }
4489
4490 #[test]
4491 fn data_dir_and_max_lsn_accessors() {
4492 let dir = std::env::temp_dir().join(format!("powdb_cat_maxlsn_{}", std::process::id()));
4493 let mut cat = Catalog::create(&dir).unwrap();
4494
4495 assert_eq!(cat.data_dir(), dir.as_path());
4497
4498 assert_eq!(cat.max_lsn(), 0);
4500
4501 let schema = Schema {
4502 table_name: "users".into(),
4503 columns: vec![ColumnDef {
4504 name: "name".into(),
4505 type_id: TypeId::Str,
4506 required: true,
4507 position: 0,
4508 }],
4509 };
4510 cat.create_table(schema).unwrap();
4511
4512 cat.insert("users", &vec![Value::Str("Alice".into())])
4513 .unwrap();
4514 cat.sync_wal().unwrap();
4515
4516 assert!(cat.max_lsn() > 0);
4519 }
4520
4521 #[test]
4522 fn test_create_table_and_insert() {
4523 let mut cat = temp_catalog("basic");
4524 let schema = Schema {
4525 table_name: "users".into(),
4526 columns: vec![
4527 ColumnDef {
4528 name: "name".into(),
4529 type_id: TypeId::Str,
4530 required: true,
4531 position: 0,
4532 },
4533 ColumnDef {
4534 name: "age".into(),
4535 type_id: TypeId::Int,
4536 required: false,
4537 position: 1,
4538 },
4539 ],
4540 };
4541 cat.create_table(schema).unwrap();
4542
4543 let row = vec![Value::Str("Alice".into()), Value::Int(30)];
4544 let rid = cat.insert("users", &row).unwrap();
4545
4546 let result = cat.get("users", rid).unwrap();
4547 assert_eq!(result[0], Value::Str("Alice".into()));
4548 assert_eq!(result[1], Value::Int(30));
4549 }
4550
4551 #[test]
4552 fn test_scan_table() {
4553 let mut cat = temp_catalog("scan");
4554 let schema = Schema {
4555 table_name: "items".into(),
4556 columns: vec![
4557 ColumnDef {
4558 name: "name".into(),
4559 type_id: TypeId::Str,
4560 required: true,
4561 position: 0,
4562 },
4563 ColumnDef {
4564 name: "price".into(),
4565 type_id: TypeId::Float,
4566 required: true,
4567 position: 1,
4568 },
4569 ],
4570 };
4571 cat.create_table(schema).unwrap();
4572
4573 for i in 0..50 {
4574 cat.insert(
4575 "items",
4576 &vec![
4577 Value::Str(format!("item_{i}")),
4578 Value::Float(i as f64 * 1.5),
4579 ],
4580 )
4581 .unwrap();
4582 }
4583
4584 let rows: Vec<_> = cat.scan("items").unwrap().collect();
4585 assert_eq!(rows.len(), 50);
4586 }
4587
4588 #[test]
4589 fn test_index_lookup() {
4590 let mut cat = temp_catalog("idx");
4591 let schema = Schema {
4592 table_name: "users".into(),
4593 columns: vec![
4594 ColumnDef {
4595 name: "email".into(),
4596 type_id: TypeId::Str,
4597 required: true,
4598 position: 0,
4599 },
4600 ColumnDef {
4601 name: "name".into(),
4602 type_id: TypeId::Str,
4603 required: true,
4604 position: 1,
4605 },
4606 ],
4607 };
4608 cat.create_table(schema).unwrap();
4609 cat.create_index("users", "email").unwrap();
4610
4611 cat.insert(
4612 "users",
4613 &vec![
4614 Value::Str("alice@example.com".into()),
4615 Value::Str("Alice".into()),
4616 ],
4617 )
4618 .unwrap();
4619 cat.insert(
4620 "users",
4621 &vec![
4622 Value::Str("bob@example.com".into()),
4623 Value::Str("Bob".into()),
4624 ],
4625 )
4626 .unwrap();
4627
4628 let result = cat
4629 .index_lookup("users", "email", &Value::Str("bob@example.com".into()))
4630 .unwrap();
4631 assert!(result.is_some());
4632 let row = result.unwrap();
4633 assert_eq!(row[1], Value::Str("Bob".into()));
4634 }
4635
4636 #[test]
4637 fn test_delete_row() {
4638 let mut cat = temp_catalog("delete");
4639 let schema = Schema {
4640 table_name: "t".into(),
4641 columns: vec![ColumnDef {
4642 name: "v".into(),
4643 type_id: TypeId::Int,
4644 required: true,
4645 position: 0,
4646 }],
4647 };
4648 cat.create_table(schema).unwrap();
4649 let r1 = cat.insert("t", &vec![Value::Int(1)]).unwrap();
4650 let r2 = cat.insert("t", &vec![Value::Int(2)]).unwrap();
4651 cat.delete("t", r1).unwrap();
4652 assert!(cat.get("t", r1).is_none());
4653 assert!(cat.get("t", r2).is_some());
4654 }
4655
4656 #[test]
4657 fn test_update_row() {
4658 let mut cat = temp_catalog("update");
4659 let schema = Schema {
4660 table_name: "t".into(),
4661 columns: vec![ColumnDef {
4662 name: "v".into(),
4663 type_id: TypeId::Int,
4664 required: true,
4665 position: 0,
4666 }],
4667 };
4668 cat.create_table(schema).unwrap();
4669 let rid = cat.insert("t", &vec![Value::Int(1)]).unwrap();
4670 let new_rid = cat.update("t", rid, &vec![Value::Int(99)]).unwrap();
4671 let row = cat.get("t", new_rid).unwrap();
4672 assert_eq!(row[0], Value::Int(99));
4673 }
4674
4675 #[test]
4676 fn test_persist_and_reopen() {
4677 let dir = std::env::temp_dir().join(format!("powdb_cat_persist_{}", std::process::id()));
4678 let _ = std::fs::remove_dir_all(&dir);
4680
4681 {
4682 let mut cat = Catalog::create(&dir).unwrap();
4683 cat.create_table(Schema {
4684 table_name: "users".into(),
4685 columns: vec![
4686 ColumnDef {
4687 name: "name".into(),
4688 type_id: TypeId::Str,
4689 required: true,
4690 position: 0,
4691 },
4692 ColumnDef {
4693 name: "age".into(),
4694 type_id: TypeId::Int,
4695 required: false,
4696 position: 1,
4697 },
4698 ],
4699 })
4700 .unwrap();
4701 cat.insert("users", &vec![Value::Str("Alice".into()), Value::Int(30)])
4702 .unwrap();
4703 cat.insert("users", &vec![Value::Str("Bob".into()), Value::Int(25)])
4704 .unwrap();
4705 }
4706
4707 let cat = Catalog::open(&dir).unwrap();
4709 let schema = cat.schema("users").unwrap();
4710 assert_eq!(schema.columns.len(), 2);
4711 assert_eq!(schema.columns[0].name, "name");
4712 assert_eq!(schema.columns[0].type_id, TypeId::Str);
4713 assert_eq!(schema.columns[1].type_id, TypeId::Int);
4714
4715 let rows: Vec<_> = cat.scan("users").unwrap().collect();
4716 assert_eq!(rows.len(), 2);
4717
4718 std::fs::remove_dir_all(&dir).ok();
4719 }
4720
4721 #[test]
4722 fn test_open_missing_dir_errors() {
4723 let dir = std::env::temp_dir().join(format!("powdb_cat_missing_{}", std::process::id()));
4724 let _ = std::fs::remove_dir_all(&dir);
4725 std::fs::create_dir_all(&dir).unwrap();
4726 assert!(Catalog::open(&dir).is_err());
4728 std::fs::remove_dir_all(&dir).ok();
4729 }
4730
4731 #[test]
4732 fn test_list_tables() {
4733 let mut cat = temp_catalog("list");
4734 cat.create_table(Schema {
4735 table_name: "a".into(),
4736 columns: vec![ColumnDef {
4737 name: "x".into(),
4738 type_id: TypeId::Int,
4739 required: true,
4740 position: 0,
4741 }],
4742 })
4743 .unwrap();
4744 cat.create_table(Schema {
4745 table_name: "b".into(),
4746 columns: vec![ColumnDef {
4747 name: "y".into(),
4748 type_id: TypeId::Int,
4749 required: true,
4750 position: 0,
4751 }],
4752 })
4753 .unwrap();
4754 let mut tables = cat.list_tables();
4755 tables.sort();
4756 assert_eq!(tables, vec!["a", "b"]);
4757 }
4758
4759 #[test]
4760 fn test_path_traversal_table_name_rejected() {
4761 let mut cat = temp_catalog("path_trav");
4762 let bad_names = vec![
4764 "../etc/passwd",
4765 "foo/bar",
4766 "table\0name",
4767 "",
4768 "123starts_with_digit",
4769 "has-dashes",
4770 "has spaces",
4771 "has.dots",
4772 ];
4773 for name in bad_names {
4774 let schema = Schema {
4775 table_name: name.into(),
4776 columns: vec![ColumnDef {
4777 name: "x".into(),
4778 type_id: TypeId::Int,
4779 required: true,
4780 position: 0,
4781 }],
4782 };
4783 let result = cat.create_table(schema);
4784 assert!(result.is_err(), "expected error for table name '{name}'");
4785 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
4786 }
4787 let good_names = vec!["users", "_private", "Table_123", "_"];
4789 for name in good_names {
4790 let schema = Schema {
4791 table_name: name.into(),
4792 columns: vec![ColumnDef {
4793 name: "x".into(),
4794 type_id: TypeId::Int,
4795 required: true,
4796 position: 0,
4797 }],
4798 };
4799 assert!(
4800 cat.create_table(schema).is_ok(),
4801 "expected ok for table name '{name}'"
4802 );
4803 }
4804 }
4805
4806 #[test]
4807 fn test_path_traversal_column_name_rejected() {
4808 let mut cat = temp_catalog("col_path_trav");
4809 let schema = Schema {
4810 table_name: "valid_table".into(),
4811 columns: vec![ColumnDef {
4812 name: "../bad".into(),
4813 type_id: TypeId::Int,
4814 required: true,
4815 position: 0,
4816 }],
4817 };
4818 let result = cat.create_table(schema);
4819 assert!(result.is_err());
4820 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
4821 }
4822
4823 #[test]
4824 fn test_drop_table_validates_name() {
4825 let mut cat = temp_catalog("drop_trav");
4826 let result = cat.drop_table("../etc/passwd");
4827 assert!(result.is_err());
4828 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
4830 }
4831}