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 {
3647 let id = read_u64(buf, &mut pos)?;
3648 if id == 0 {
3649 return Err(io::Error::new(
3650 io::ErrorKind::InvalidData,
3651 "catalog next index id must be non-zero",
3652 ));
3653 }
3654 id
3655 } else {
3656 1
3657 };
3658
3659 if n_tables > buf.len() {
3666 return Err(io::Error::new(
3667 io::ErrorKind::InvalidData,
3668 format!("catalog file corrupt: implausible table count {n_tables}"),
3669 ));
3670 }
3671
3672 let mut entries = Vec::with_capacity(n_tables);
3673 for _ in 0..n_tables {
3674 let name_len = read_u32(buf, &mut pos)? as usize;
3675 let table_name = read_string(buf, &mut pos, name_len)?;
3676 let n_cols = read_u16(buf, &mut pos)? as usize;
3677
3678 let mut columns = Vec::with_capacity(n_cols);
3679 for _ in 0..n_cols {
3680 let cname_len = read_u32(buf, &mut pos)? as usize;
3681 let name = read_string(buf, &mut pos, cname_len)?;
3682 let type_id_raw = read_u8(buf, &mut pos)?;
3683 let type_id = type_id_from_u8(type_id_raw)?;
3684 let required = read_u8(buf, &mut pos)? != 0;
3685 let position = read_u16(buf, &mut pos)?;
3686 columns.push(ColumnDef {
3687 name,
3688 type_id,
3689 required,
3690 position,
3691 });
3692 }
3693
3694 let indexed_cols: Vec<IndexedColMeta> = if version >= 3 {
3698 let n = read_u16(buf, &mut pos)? as usize;
3699 let mut v = Vec::with_capacity(n);
3700 for _ in 0..n {
3701 let l = read_u32(buf, &mut pos)? as usize;
3702 let name = read_string(buf, &mut pos, l)?;
3703 let unique = read_u8(buf, &mut pos)? != 0;
3704 v.push(IndexedColMeta { name, unique });
3705 }
3706 v
3707 } else if version >= 2 {
3708 let n = read_u16(buf, &mut pos)? as usize;
3709 let mut v = Vec::with_capacity(n);
3710 for _ in 0..n {
3711 let l = read_u32(buf, &mut pos)? as usize;
3712 let name = read_string(buf, &mut pos, l)?;
3713 v.push(IndexedColMeta {
3714 name,
3715 unique: false,
3716 });
3717 }
3718 v
3719 } else {
3720 Vec::new()
3721 };
3722
3723 let defaults = if version >= 4 {
3725 decode_defaults_section(buf, &mut pos, columns.len()).ok_or_else(|| {
3726 io::Error::new(io::ErrorKind::InvalidData, "truncated catalog defaults")
3727 })?
3728 } else {
3729 Vec::new()
3730 };
3731
3732 let auto_cols = if version >= 5 {
3734 decode_auto_section(buf, &mut pos, columns.len()).ok_or_else(|| {
3735 io::Error::new(io::ErrorKind::InvalidData, "truncated catalog auto columns")
3736 })?
3737 } else {
3738 Vec::new()
3739 };
3740
3741 let expression_indexes = if version >= 6 {
3742 decode_expression_indexes(buf, &mut pos)?
3743 } else {
3744 Vec::new()
3745 };
3746
3747 entries.push(CatalogEntry {
3748 schema: Schema {
3749 table_name,
3750 columns,
3751 },
3752 indexed_cols,
3753 expression_indexes,
3754 defaults,
3755 auto_cols,
3756 });
3757 }
3758
3759 let mut seen_index_ids = FxHashMap::default();
3760 let mut max_index_id = 0;
3761 for entry in &entries {
3762 for index in &entry.expression_indexes {
3763 if index.canonical_version == 0 || index.canonical_text.is_empty() {
3764 return Err(io::Error::new(
3765 io::ErrorKind::InvalidData,
3766 "expression index has invalid canonical identity",
3767 ));
3768 }
3769 if index.canonical_version == 1
3770 && index.canonical_text != index.json_path.canonical_text()
3771 {
3772 return Err(io::Error::new(
3773 io::ErrorKind::InvalidData,
3774 "expression index canonical identity does not match its JSON path",
3775 ));
3776 }
3777 let Some(root) = entry
3778 .schema
3779 .columns
3780 .iter()
3781 .find(|column| column.name == index.json_path.column)
3782 else {
3783 return Err(io::Error::new(
3784 io::ErrorKind::InvalidData,
3785 "expression index JSON root is absent from its table",
3786 ));
3787 };
3788 if root.type_id != TypeId::Json {
3789 return Err(io::Error::new(
3790 io::ErrorKind::InvalidData,
3791 "expression index root column is not JSON",
3792 ));
3793 }
3794 if seen_index_ids.insert(index.index_id, ()).is_some() {
3795 return Err(io::Error::new(
3796 io::ErrorKind::InvalidData,
3797 "duplicate expression index id in catalog",
3798 ));
3799 }
3800 max_index_id = max_index_id.max(index.index_id);
3801 }
3802 }
3803 if next_index_id <= max_index_id {
3804 return Err(io::Error::new(
3805 io::ErrorKind::InvalidData,
3806 "catalog next index id does not exceed persisted index ids",
3807 ));
3808 }
3809 Ok(CatalogFile {
3810 version,
3811 next_index_id,
3812 entries,
3813 })
3814}
3815
3816fn read_u8(buf: &[u8], pos: &mut usize) -> io::Result<u8> {
3817 if *pos >= buf.len() {
3818 return Err(io::Error::new(
3819 io::ErrorKind::UnexpectedEof,
3820 "truncated catalog",
3821 ));
3822 }
3823 let v = buf[*pos];
3824 *pos += 1;
3825 Ok(v)
3826}
3827fn read_u16(buf: &[u8], pos: &mut usize) -> io::Result<u16> {
3828 if *pos + 2 > buf.len() {
3829 return Err(io::Error::new(
3830 io::ErrorKind::UnexpectedEof,
3831 "truncated catalog",
3832 ));
3833 }
3834 let v = u16::from_le_bytes(
3835 buf[*pos..*pos + 2]
3836 .try_into()
3837 .expect("bounds checked above"),
3838 );
3839 *pos += 2;
3840 Ok(v)
3841}
3842fn read_u32(buf: &[u8], pos: &mut usize) -> io::Result<u32> {
3843 if *pos + 4 > buf.len() {
3844 return Err(io::Error::new(
3845 io::ErrorKind::UnexpectedEof,
3846 "truncated catalog",
3847 ));
3848 }
3849 let v = u32::from_le_bytes(
3850 buf[*pos..*pos + 4]
3851 .try_into()
3852 .expect("bounds checked above"),
3853 );
3854 *pos += 4;
3855 Ok(v)
3856}
3857fn read_u64(buf: &[u8], pos: &mut usize) -> io::Result<u64> {
3858 if *pos + 8 > buf.len() {
3859 return Err(io::Error::new(
3860 io::ErrorKind::UnexpectedEof,
3861 "truncated catalog",
3862 ));
3863 }
3864 let value = u64::from_le_bytes(
3865 buf[*pos..*pos + 8]
3866 .try_into()
3867 .expect("bounds checked above"),
3868 );
3869 *pos += 8;
3870 Ok(value)
3871}
3872fn read_string(buf: &[u8], pos: &mut usize, len: usize) -> io::Result<String> {
3873 if *pos + len > buf.len() {
3874 return Err(io::Error::new(
3875 io::ErrorKind::UnexpectedEof,
3876 "truncated catalog string",
3877 ));
3878 }
3879 let s = std::str::from_utf8(&buf[*pos..*pos + len])
3880 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "non-utf8 in catalog"))?
3881 .to_string();
3882 *pos += len;
3883 Ok(s)
3884}
3885fn type_id_from_u8(v: u8) -> io::Result<TypeId> {
3886 match v {
3887 0 => Ok(TypeId::Empty),
3888 1 => Ok(TypeId::Int),
3889 2 => Ok(TypeId::Float),
3890 3 => Ok(TypeId::Bool),
3891 4 => Ok(TypeId::Str),
3892 5 => Ok(TypeId::DateTime),
3893 6 => Ok(TypeId::Uuid),
3894 7 => Ok(TypeId::Bytes),
3895 8 => Ok(TypeId::Json),
3896 _ => Err(io::Error::new(
3897 io::ErrorKind::InvalidData,
3898 format!("unknown type id: {v}"),
3899 )),
3900 }
3901}
3902
3903#[cfg(test)]
3904mod tests {
3905 use super::*;
3906
3907 fn fail_next_catalog_persist_at(stage: u8) {
3908 CATALOG_PERSIST_FAILPOINT.with(|failpoint| failpoint.set(stage));
3909 }
3910
3911 fn temp_catalog(name: &str) -> Catalog {
3912 let dir = std::env::temp_dir().join(format!("powdb_cat_{name}_{}", std::process::id()));
3913 Catalog::create(&dir).unwrap()
3914 }
3915
3916 fn hash_dir_tree(dir: &std::path::Path) -> String {
3921 let mut entries: Vec<std::path::PathBuf> = Vec::new();
3922 fn walk(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
3923 let mut items: Vec<_> = fs::read_dir(dir).unwrap().flatten().collect();
3924 items.sort_by_key(std::fs::DirEntry::path);
3925 for item in items {
3926 let path = item.path();
3927 if path.is_dir() {
3928 walk(&path, out);
3929 } else {
3930 out.push(path);
3931 }
3932 }
3933 }
3934 walk(dir, &mut entries);
3935 let mut hasher = crc32fast::Hasher::new();
3936 for path in &entries {
3937 hasher.update(path.to_string_lossy().as_bytes());
3938 hasher.update(&fs::read(path).unwrap());
3939 }
3940 format!("{:08x}", hasher.finalize())
3941 }
3942
3943 fn seed_quiescent_dir(dir: &std::path::Path) {
3944 let mut catalog = Catalog::create(dir).unwrap();
3945 catalog
3946 .create_table(Schema {
3947 table_name: "User".into(),
3948 columns: vec![
3949 ColumnDef {
3950 name: "name".into(),
3951 type_id: TypeId::Str,
3952 required: true,
3953 position: 0,
3954 },
3955 ColumnDef {
3956 name: "age".into(),
3957 type_id: TypeId::Int,
3958 required: false,
3959 position: 1,
3960 },
3961 ],
3962 })
3963 .unwrap();
3964 catalog.create_index("User", "age").unwrap();
3965 catalog
3966 .insert("User", &vec![Value::Str("Ada".into()), Value::Int(36)])
3967 .unwrap();
3968 catalog
3969 .insert("User", &vec![Value::Str("Bo".into()), Value::Int(20)])
3970 .unwrap();
3971 drop(catalog);
3974 }
3975
3976 #[test]
3977 fn open_read_only_serves_reads_on_clean_dir() {
3978 let dir = tempfile::tempdir().unwrap();
3979 seed_quiescent_dir(dir.path());
3980
3981 let catalog = Catalog::open_read_only(dir.path()).unwrap();
3982 let rows: Vec<_> = catalog.scan("User").unwrap().collect();
3983 assert_eq!(rows.len(), 2);
3984 let hit = catalog
3986 .index_lookup("User", "age", &Value::Int(36))
3987 .unwrap();
3988 assert_eq!(hit.unwrap()[0], Value::Str("Ada".into()));
3989 }
3990
3991 #[test]
3992 fn open_read_only_never_mutates_dir() {
3993 let dir = tempfile::tempdir().unwrap();
3994 seed_quiescent_dir(dir.path());
3995 let before = hash_dir_tree(dir.path());
3996
3997 {
3998 let catalog = Catalog::open_read_only(dir.path()).unwrap();
3999 let _ = catalog.scan("User").unwrap().count();
4000 let _ = catalog
4001 .index_lookup("User", "age", &Value::Int(20))
4002 .unwrap();
4003 }
4005 let after = hash_dir_tree(dir.path());
4006 assert_eq!(
4007 before, after,
4008 "read-only open + queries + drop must leave the directory byte-identical"
4009 );
4010 }
4011
4012 #[test]
4013 fn open_read_only_refuses_non_empty_wal() {
4014 let dir = tempfile::tempdir().unwrap();
4015 {
4018 let mut catalog = Catalog::create(dir.path()).unwrap();
4019 catalog
4020 .create_table(Schema {
4021 table_name: "T".into(),
4022 columns: vec![ColumnDef {
4023 name: "id".into(),
4024 type_id: TypeId::Int,
4025 required: true,
4026 position: 0,
4027 }],
4028 })
4029 .unwrap();
4030 catalog.insert("T", &vec![Value::Int(1)]).unwrap();
4031 catalog.sync_wal().unwrap();
4032 std::mem::forget(catalog); }
4034 let err = match Catalog::open_read_only(dir.path()) {
4035 Ok(_) => panic!("read-only open must refuse a non-empty WAL"),
4036 Err(err) => err,
4037 };
4038 assert!(
4039 err.to_string().contains("WAL is not empty"),
4040 "expected a WAL-not-empty refusal naming the remedy, got: {err}"
4041 );
4042 assert!(err.to_string().contains("read-write engine"));
4043 }
4044
4045 #[test]
4046 fn open_read_only_expression_index_reads_work() {
4047 let dir = tempfile::tempdir().unwrap();
4048 {
4049 let mut catalog = Catalog::create(dir.path()).unwrap();
4050 catalog
4051 .create_table(Schema {
4052 table_name: "Doc".into(),
4053 columns: vec![ColumnDef {
4054 name: "data".into(),
4055 type_id: TypeId::Json,
4056 required: false,
4057 position: 0,
4058 }],
4059 })
4060 .unwrap();
4061 let path =
4062 StoredJsonPathV1::new("data", vec![StoredJsonPathSegmentV1::Key("author".into())]);
4063 catalog
4064 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path, false)
4065 .unwrap();
4066 drop(catalog);
4067 }
4068 let before = hash_dir_tree(dir.path());
4070 let catalog = Catalog::open_read_only(dir.path()).unwrap();
4071 assert_eq!(catalog.scan("Doc").unwrap().count(), 0);
4072 drop(catalog);
4073 let after = hash_dir_tree(dir.path());
4074 assert_eq!(
4075 before, after,
4076 "read-only expression-index load must not write"
4077 );
4078 }
4079
4080 #[test]
4081 fn v5_reader_rejects_v6_catalog() {
4082 let dir = tempfile::tempdir().unwrap();
4083 let mut catalog = Catalog::create(dir.path()).unwrap();
4084 catalog
4085 .create_table(Schema {
4086 table_name: "Doc".into(),
4087 columns: vec![ColumnDef {
4088 name: "data".into(),
4089 type_id: TypeId::Json,
4090 required: false,
4091 position: 0,
4092 }],
4093 })
4094 .unwrap();
4095 let path =
4096 StoredJsonPathV1::new("data", vec![StoredJsonPathSegmentV1::Key("author".into())]);
4097 catalog
4098 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path, false)
4099 .unwrap();
4100 let result = read_catalog_file_with_max_version(
4101 &dir.path().join(CATALOG_FILE),
4102 LEGACY_CATALOG_VERSION,
4103 );
4104 let error = match result {
4105 Ok(_) => panic!("a v5 reader must reject v6 before decoding its payload"),
4106 Err(error) => error,
4107 };
4108 assert!(error.to_string().contains("unsupported catalog version: 6"));
4109 }
4110
4111 #[test]
4112 fn expression_index_rolls_back_only_before_catalog_rename() {
4113 let before_dir = tempfile::tempdir().unwrap();
4114 let mut before = Catalog::create(before_dir.path()).unwrap();
4115 before
4116 .create_table(Schema {
4117 table_name: "Doc".into(),
4118 columns: vec![ColumnDef {
4119 name: "data".into(),
4120 type_id: TypeId::Json,
4121 required: false,
4122 position: 0,
4123 }],
4124 })
4125 .unwrap();
4126 let path =
4127 StoredJsonPathV1::new("data", vec![StoredJsonPathSegmentV1::Key("score".into())]);
4128
4129 fail_next_catalog_persist_at(1);
4130 let error = before
4131 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path.clone(), false)
4132 .unwrap_err();
4133 assert!(error.to_string().contains("before rename"));
4134 assert_eq!(before.active_catalog_version(), LEGACY_CATALOG_VERSION);
4135 assert_eq!(before.next_index_id(), 1);
4136 assert!(before.expression_index_metadata("Doc").unwrap().is_empty());
4137 assert!(!before_dir
4138 .path()
4139 .join(expression_index_file_name("Doc", 1))
4140 .exists());
4141
4142 let before_index_id = before
4143 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path.clone(), false)
4144 .unwrap();
4145 fail_next_catalog_persist_at(1);
4146 let error = before
4147 .drop_expression_index("Doc", before_index_id)
4148 .unwrap_err();
4149 assert!(error.to_string().contains("before rename"));
4150 assert!(before
4151 .expression_index_btree("Doc", before_index_id)
4152 .is_some());
4153 assert!(before_dir
4154 .path()
4155 .join(expression_index_file_name("Doc", 1))
4156 .exists());
4157 std::mem::forget(before);
4158 let before_reopened = Catalog::open(before_dir.path()).unwrap();
4159 assert!(before_reopened
4160 .expression_index_btree("Doc", before_index_id)
4161 .is_some());
4162
4163 let after_dir = tempfile::tempdir().unwrap();
4164 let mut after = Catalog::create(after_dir.path()).unwrap();
4165 after
4166 .create_table(Schema {
4167 table_name: "Doc".into(),
4168 columns: vec![ColumnDef {
4169 name: "data".into(),
4170 type_id: TypeId::Json,
4171 required: false,
4172 position: 0,
4173 }],
4174 })
4175 .unwrap();
4176 fail_next_catalog_persist_at(2);
4177 let index_id = after
4178 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path.clone(), false)
4179 .unwrap();
4180 assert_eq!(index_id, 1);
4181 assert_eq!(after.active_catalog_version(), CATALOG_VERSION);
4182 assert_eq!(after.next_index_id(), 2);
4183 assert!(after.expression_index_btree("Doc", index_id).is_some());
4184 assert!(after_dir
4185 .path()
4186 .join(expression_index_file_name("Doc", 1))
4187 .exists());
4188 std::mem::forget(after);
4189
4190 let mut reopened = Catalog::open(after_dir.path()).unwrap();
4191 assert!(reopened.expression_index_btree("Doc", index_id).is_some());
4192 fail_next_catalog_persist_at(2);
4193 reopened.drop_expression_index("Doc", index_id).unwrap();
4194 assert!(reopened
4195 .expression_index_metadata("Doc")
4196 .unwrap()
4197 .is_empty());
4198 assert!(!after_dir
4199 .path()
4200 .join(expression_index_file_name("Doc", 1))
4201 .exists());
4202 std::mem::forget(reopened);
4203
4204 let final_open = Catalog::open(after_dir.path()).unwrap();
4205 assert!(final_open
4206 .expression_index_metadata("Doc")
4207 .unwrap()
4208 .is_empty());
4209 }
4210
4211 #[test]
4212 fn ordinary_catalog_persist_reports_post_rename_directory_sync_failure() {
4213 let dir = tempfile::tempdir().unwrap();
4214 let mut catalog = Catalog::create(dir.path()).unwrap();
4215 fail_next_catalog_persist_at(2);
4216 let error = catalog
4217 .create_table(Schema {
4218 table_name: "VisibleAfterRename".into(),
4219 columns: vec![ColumnDef {
4220 name: "id".into(),
4221 type_id: TypeId::Int,
4222 required: true,
4223 position: 0,
4224 }],
4225 })
4226 .unwrap_err();
4227 assert!(error.to_string().contains("after rename"));
4228 assert!(catalog.schema("VisibleAfterRename").is_some());
4229
4230 std::mem::forget(catalog);
4231 let reopened = Catalog::open(dir.path()).unwrap();
4232 assert!(reopened.schema("VisibleAfterRename").is_some());
4233 }
4234
4235 fn schema_two_cols() -> Schema {
4236 Schema {
4237 table_name: "T".into(),
4238 columns: vec![
4239 ColumnDef {
4240 name: "id".into(),
4241 type_id: TypeId::Int,
4242 required: true,
4243 position: 0,
4244 },
4245 ColumnDef {
4246 name: "status".into(),
4247 type_id: TypeId::Str,
4248 required: false,
4249 position: 1,
4250 },
4251 ],
4252 }
4253 }
4254
4255 #[test]
4256 fn replay_records_treats_reused_tx_ids_as_ordered_spans() {
4257 let mut cat = temp_catalog("reused_tx_ids");
4258 let schema = schema_two_cols();
4259 cat.create_table(schema.clone()).unwrap();
4260 cat.checkpoint().unwrap();
4261
4262 let mut committed_row = Vec::new();
4263 encode_row_into(
4264 &schema,
4265 &[Value::Int(1), Value::Str("committed".into())],
4266 &mut committed_row,
4267 );
4268 let mut incomplete_row = Vec::new();
4269 encode_row_into(
4270 &schema,
4271 &[Value::Int(2), Value::Str("incomplete".into())],
4272 &mut incomplete_row,
4273 );
4274
4275 let records = vec![
4276 WalRecord {
4277 tx_id: 1,
4278 record_type: WalRecordType::Begin,
4279 lsn: 1,
4280 data: Vec::new(),
4281 },
4282 WalRecord {
4283 tx_id: 1,
4284 record_type: WalRecordType::Insert,
4285 lsn: 2,
4286 data: encode_wal_payload(
4287 "T",
4288 RowId {
4289 page_id: 1,
4290 slot_index: 0,
4291 },
4292 &committed_row,
4293 ),
4294 },
4295 WalRecord {
4296 tx_id: 1,
4297 record_type: WalRecordType::Commit,
4298 lsn: 3,
4299 data: Vec::new(),
4300 },
4301 WalRecord {
4302 tx_id: 1,
4303 record_type: WalRecordType::Begin,
4304 lsn: 4,
4305 data: Vec::new(),
4306 },
4307 WalRecord {
4308 tx_id: 1,
4309 record_type: WalRecordType::Insert,
4310 lsn: 5,
4311 data: encode_wal_payload(
4312 "T",
4313 RowId {
4314 page_id: 1,
4315 slot_index: 1,
4316 },
4317 &incomplete_row,
4318 ),
4319 },
4320 ];
4321
4322 cat.apply_wal_records(&records).unwrap();
4323 let rows: Vec<_> = cat.scan("T").unwrap().collect();
4324 assert_eq!(rows.len(), 1);
4325 assert_eq!(rows[0].1[0], Value::Int(1));
4326 assert_eq!(rows[0].1[1], Value::Str("committed".into()));
4327 }
4328
4329 #[test]
4330 fn ddl_create_table_codec_roundtrips_defaults_and_auto() {
4331 let schema = schema_two_cols();
4332 let defaults = vec![None, Some(Value::Str("active".into()))];
4333 let auto_cols = vec![true, false];
4334 let encoded = encode_ddl_create_table(&schema, &defaults, &auto_cols);
4335 let (decoded_schema, decoded_defaults, decoded_auto) =
4336 decode_ddl_create_table(&encoded).unwrap();
4337 assert_eq!(decoded_schema.columns.len(), 2);
4338 assert_eq!(decoded_defaults, defaults);
4339 assert_eq!(decoded_auto, auto_cols);
4340 }
4341
4342 #[test]
4343 fn ddl_create_table_codec_back_compat_without_trailing_sections() {
4344 let schema = schema_two_cols();
4348 let full = encode_ddl_create_table(&schema, &[], &[]);
4349 let legacy = &full[..full.len() - 4];
4352 let (decoded_schema, decoded_defaults, decoded_auto) =
4353 decode_ddl_create_table(legacy).unwrap();
4354 assert_eq!(decoded_schema.columns.len(), 2);
4355 assert!(decoded_defaults.is_empty(), "no defaults section -> empty");
4356 assert!(decoded_auto.is_empty(), "no auto section -> empty");
4357 }
4358
4359 #[test]
4360 fn ddl_create_table_codec_back_compat_defaults_but_no_auto() {
4361 let schema = schema_two_cols();
4364 let defaults = vec![None, Some(Value::Str("active".into()))];
4365 let full = encode_ddl_create_table(&schema, &defaults, &[]);
4366 let legacy = &full[..full.len() - 2];
4368 let (_schema, decoded_defaults, decoded_auto) = decode_ddl_create_table(legacy).unwrap();
4369 assert_eq!(decoded_defaults, defaults);
4370 assert!(decoded_auto.is_empty());
4371 }
4372
4373 #[test]
4374 fn read_catalog_file_accepts_intermediate_versions_3_and_4() {
4375 use std::io::Write as _;
4383 fn write_legacy_catalog(path: &std::path::Path, version: u16) {
4384 let mut buf: Vec<u8> = Vec::new();
4385 buf.extend_from_slice(CATALOG_MAGIC);
4386 buf.extend_from_slice(&version.to_le_bytes());
4387 buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes());
4390 buf.extend_from_slice(b"T");
4391 buf.extend_from_slice(&2u16.to_le_bytes()); buf.extend_from_slice(&2u32.to_le_bytes());
4394 buf.extend_from_slice(b"id");
4395 buf.push(TypeId::Int as u8);
4396 buf.push(1);
4397 buf.extend_from_slice(&0u16.to_le_bytes());
4398 buf.extend_from_slice(&6u32.to_le_bytes());
4400 buf.extend_from_slice(b"status");
4401 buf.push(TypeId::Str as u8);
4402 buf.push(0);
4403 buf.extend_from_slice(&1u16.to_le_bytes());
4404 buf.extend_from_slice(&0u16.to_le_bytes());
4406 if version >= 4 {
4408 encode_defaults_section(&mut buf, &[None, None]);
4409 }
4410 let crc = crc32fast::hash(&buf);
4412 buf.extend_from_slice(&crc.to_le_bytes());
4413 let mut f = fs::File::create(path).unwrap();
4414 f.write_all(&buf).unwrap();
4415 }
4416
4417 for version in [3u16, 4u16] {
4418 let path = std::env::temp_dir().join(format!(
4419 "powdb_cat_v{version}_compat_{}.bin",
4420 std::process::id()
4421 ));
4422 write_legacy_catalog(&path, version);
4423 let catalog_file = read_catalog_file(&path)
4424 .unwrap_or_else(|e| panic!("version {version} catalog must load, got: {e}"));
4425 let entries = catalog_file.entries;
4426 assert_eq!(entries.len(), 1);
4427 assert_eq!(entries[0].schema.table_name, "T");
4428 assert_eq!(entries[0].schema.columns.len(), 2);
4429 assert!(
4430 entries[0].auto_cols.is_empty(),
4431 "v{version} has no auto cols"
4432 );
4433 fs::remove_file(&path).ok();
4434 }
4435 }
4436
4437 #[test]
4438 fn read_catalog_file_rejects_implausible_table_count() {
4439 use std::io::Write as _;
4447 let mut buf: Vec<u8> = Vec::new();
4448 buf.extend_from_slice(CATALOG_MAGIC);
4449 buf.extend_from_slice(&CATALOG_VERSION.to_le_bytes());
4450 buf.extend_from_slice(&1000u32.to_le_bytes()); buf.extend_from_slice(&1u64.to_le_bytes()); let crc = crc32fast::hash(&buf);
4454 buf.extend_from_slice(&crc.to_le_bytes());
4455 let path =
4456 std::env::temp_dir().join(format!("powdb_cat_badcount_{}.bin", std::process::id()));
4457 fs::File::create(&path).unwrap().write_all(&buf).unwrap();
4458
4459 let msg = match read_catalog_file(&path) {
4460 Ok(_) => panic!("implausible table count must be rejected, got Ok"),
4461 Err(e) => e.to_string(),
4462 };
4463 assert!(
4464 msg.contains("implausible table count"),
4465 "expected an implausible-table-count error, got: {msg}"
4466 );
4467 fs::remove_file(&path).ok();
4468 }
4469
4470 #[test]
4471 fn data_dir_and_max_lsn_accessors() {
4472 let dir = std::env::temp_dir().join(format!("powdb_cat_maxlsn_{}", std::process::id()));
4473 let mut cat = Catalog::create(&dir).unwrap();
4474
4475 assert_eq!(cat.data_dir(), dir.as_path());
4477
4478 assert_eq!(cat.max_lsn(), 0);
4480
4481 let schema = Schema {
4482 table_name: "users".into(),
4483 columns: vec![ColumnDef {
4484 name: "name".into(),
4485 type_id: TypeId::Str,
4486 required: true,
4487 position: 0,
4488 }],
4489 };
4490 cat.create_table(schema).unwrap();
4491
4492 cat.insert("users", &vec![Value::Str("Alice".into())])
4493 .unwrap();
4494 cat.sync_wal().unwrap();
4495
4496 assert!(cat.max_lsn() > 0);
4499 }
4500
4501 #[test]
4502 fn test_create_table_and_insert() {
4503 let mut cat = temp_catalog("basic");
4504 let schema = Schema {
4505 table_name: "users".into(),
4506 columns: vec![
4507 ColumnDef {
4508 name: "name".into(),
4509 type_id: TypeId::Str,
4510 required: true,
4511 position: 0,
4512 },
4513 ColumnDef {
4514 name: "age".into(),
4515 type_id: TypeId::Int,
4516 required: false,
4517 position: 1,
4518 },
4519 ],
4520 };
4521 cat.create_table(schema).unwrap();
4522
4523 let row = vec![Value::Str("Alice".into()), Value::Int(30)];
4524 let rid = cat.insert("users", &row).unwrap();
4525
4526 let result = cat.get("users", rid).unwrap();
4527 assert_eq!(result[0], Value::Str("Alice".into()));
4528 assert_eq!(result[1], Value::Int(30));
4529 }
4530
4531 #[test]
4532 fn test_scan_table() {
4533 let mut cat = temp_catalog("scan");
4534 let schema = Schema {
4535 table_name: "items".into(),
4536 columns: vec![
4537 ColumnDef {
4538 name: "name".into(),
4539 type_id: TypeId::Str,
4540 required: true,
4541 position: 0,
4542 },
4543 ColumnDef {
4544 name: "price".into(),
4545 type_id: TypeId::Float,
4546 required: true,
4547 position: 1,
4548 },
4549 ],
4550 };
4551 cat.create_table(schema).unwrap();
4552
4553 for i in 0..50 {
4554 cat.insert(
4555 "items",
4556 &vec![
4557 Value::Str(format!("item_{i}")),
4558 Value::Float(i as f64 * 1.5),
4559 ],
4560 )
4561 .unwrap();
4562 }
4563
4564 let rows: Vec<_> = cat.scan("items").unwrap().collect();
4565 assert_eq!(rows.len(), 50);
4566 }
4567
4568 #[test]
4569 fn test_index_lookup() {
4570 let mut cat = temp_catalog("idx");
4571 let schema = Schema {
4572 table_name: "users".into(),
4573 columns: vec![
4574 ColumnDef {
4575 name: "email".into(),
4576 type_id: TypeId::Str,
4577 required: true,
4578 position: 0,
4579 },
4580 ColumnDef {
4581 name: "name".into(),
4582 type_id: TypeId::Str,
4583 required: true,
4584 position: 1,
4585 },
4586 ],
4587 };
4588 cat.create_table(schema).unwrap();
4589 cat.create_index("users", "email").unwrap();
4590
4591 cat.insert(
4592 "users",
4593 &vec![
4594 Value::Str("alice@example.com".into()),
4595 Value::Str("Alice".into()),
4596 ],
4597 )
4598 .unwrap();
4599 cat.insert(
4600 "users",
4601 &vec![
4602 Value::Str("bob@example.com".into()),
4603 Value::Str("Bob".into()),
4604 ],
4605 )
4606 .unwrap();
4607
4608 let result = cat
4609 .index_lookup("users", "email", &Value::Str("bob@example.com".into()))
4610 .unwrap();
4611 assert!(result.is_some());
4612 let row = result.unwrap();
4613 assert_eq!(row[1], Value::Str("Bob".into()));
4614 }
4615
4616 #[test]
4617 fn test_delete_row() {
4618 let mut cat = temp_catalog("delete");
4619 let schema = Schema {
4620 table_name: "t".into(),
4621 columns: vec![ColumnDef {
4622 name: "v".into(),
4623 type_id: TypeId::Int,
4624 required: true,
4625 position: 0,
4626 }],
4627 };
4628 cat.create_table(schema).unwrap();
4629 let r1 = cat.insert("t", &vec![Value::Int(1)]).unwrap();
4630 let r2 = cat.insert("t", &vec![Value::Int(2)]).unwrap();
4631 cat.delete("t", r1).unwrap();
4632 assert!(cat.get("t", r1).is_none());
4633 assert!(cat.get("t", r2).is_some());
4634 }
4635
4636 #[test]
4637 fn test_update_row() {
4638 let mut cat = temp_catalog("update");
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 rid = cat.insert("t", &vec![Value::Int(1)]).unwrap();
4650 let new_rid = cat.update("t", rid, &vec![Value::Int(99)]).unwrap();
4651 let row = cat.get("t", new_rid).unwrap();
4652 assert_eq!(row[0], Value::Int(99));
4653 }
4654
4655 #[test]
4656 fn test_persist_and_reopen() {
4657 let dir = std::env::temp_dir().join(format!("powdb_cat_persist_{}", std::process::id()));
4658 let _ = std::fs::remove_dir_all(&dir);
4660
4661 {
4662 let mut cat = Catalog::create(&dir).unwrap();
4663 cat.create_table(Schema {
4664 table_name: "users".into(),
4665 columns: vec![
4666 ColumnDef {
4667 name: "name".into(),
4668 type_id: TypeId::Str,
4669 required: true,
4670 position: 0,
4671 },
4672 ColumnDef {
4673 name: "age".into(),
4674 type_id: TypeId::Int,
4675 required: false,
4676 position: 1,
4677 },
4678 ],
4679 })
4680 .unwrap();
4681 cat.insert("users", &vec![Value::Str("Alice".into()), Value::Int(30)])
4682 .unwrap();
4683 cat.insert("users", &vec![Value::Str("Bob".into()), Value::Int(25)])
4684 .unwrap();
4685 }
4686
4687 let cat = Catalog::open(&dir).unwrap();
4689 let schema = cat.schema("users").unwrap();
4690 assert_eq!(schema.columns.len(), 2);
4691 assert_eq!(schema.columns[0].name, "name");
4692 assert_eq!(schema.columns[0].type_id, TypeId::Str);
4693 assert_eq!(schema.columns[1].type_id, TypeId::Int);
4694
4695 let rows: Vec<_> = cat.scan("users").unwrap().collect();
4696 assert_eq!(rows.len(), 2);
4697
4698 std::fs::remove_dir_all(&dir).ok();
4699 }
4700
4701 #[test]
4702 fn test_open_missing_dir_errors() {
4703 let dir = std::env::temp_dir().join(format!("powdb_cat_missing_{}", std::process::id()));
4704 let _ = std::fs::remove_dir_all(&dir);
4705 std::fs::create_dir_all(&dir).unwrap();
4706 assert!(Catalog::open(&dir).is_err());
4708 std::fs::remove_dir_all(&dir).ok();
4709 }
4710
4711 #[test]
4712 fn test_list_tables() {
4713 let mut cat = temp_catalog("list");
4714 cat.create_table(Schema {
4715 table_name: "a".into(),
4716 columns: vec![ColumnDef {
4717 name: "x".into(),
4718 type_id: TypeId::Int,
4719 required: true,
4720 position: 0,
4721 }],
4722 })
4723 .unwrap();
4724 cat.create_table(Schema {
4725 table_name: "b".into(),
4726 columns: vec![ColumnDef {
4727 name: "y".into(),
4728 type_id: TypeId::Int,
4729 required: true,
4730 position: 0,
4731 }],
4732 })
4733 .unwrap();
4734 let mut tables = cat.list_tables();
4735 tables.sort();
4736 assert_eq!(tables, vec!["a", "b"]);
4737 }
4738
4739 #[test]
4740 fn test_path_traversal_table_name_rejected() {
4741 let mut cat = temp_catalog("path_trav");
4742 let bad_names = vec![
4744 "../etc/passwd",
4745 "foo/bar",
4746 "table\0name",
4747 "",
4748 "123starts_with_digit",
4749 "has-dashes",
4750 "has spaces",
4751 "has.dots",
4752 ];
4753 for name in bad_names {
4754 let schema = Schema {
4755 table_name: name.into(),
4756 columns: vec![ColumnDef {
4757 name: "x".into(),
4758 type_id: TypeId::Int,
4759 required: true,
4760 position: 0,
4761 }],
4762 };
4763 let result = cat.create_table(schema);
4764 assert!(result.is_err(), "expected error for table name '{name}'");
4765 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
4766 }
4767 let good_names = vec!["users", "_private", "Table_123", "_"];
4769 for name in good_names {
4770 let schema = Schema {
4771 table_name: name.into(),
4772 columns: vec![ColumnDef {
4773 name: "x".into(),
4774 type_id: TypeId::Int,
4775 required: true,
4776 position: 0,
4777 }],
4778 };
4779 assert!(
4780 cat.create_table(schema).is_ok(),
4781 "expected ok for table name '{name}'"
4782 );
4783 }
4784 }
4785
4786 #[test]
4787 fn test_path_traversal_column_name_rejected() {
4788 let mut cat = temp_catalog("col_path_trav");
4789 let schema = Schema {
4790 table_name: "valid_table".into(),
4791 columns: vec![ColumnDef {
4792 name: "../bad".into(),
4793 type_id: TypeId::Int,
4794 required: true,
4795 position: 0,
4796 }],
4797 };
4798 let result = cat.create_table(schema);
4799 assert!(result.is_err());
4800 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
4801 }
4802
4803 #[test]
4804 fn test_drop_table_validates_name() {
4805 let mut cat = temp_catalog("drop_trav");
4806 let result = cat.drop_table("../etc/passwd");
4807 assert!(result.is_err());
4808 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
4810 }
4811}