1use crate::btree::BTree;
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 expression_index_btree_mut(&mut self, table: &str, index_id: u64) -> Option<&mut BTree> {
2227 self.get_table_mut(table)?
2228 .expression_index_btree_mut(index_id)
2229 }
2230
2231 pub fn expression_index_lookup_all(
2232 &self,
2233 table: &str,
2234 index_id: u64,
2235 key: &Value,
2236 ) -> io::Result<Vec<RowId>> {
2237 let tree = self
2238 .by_name(table)?
2239 .expression_index_btree(index_id)
2240 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2241 Ok(tree.lookup_all(key))
2242 }
2243
2244 pub fn expression_index_range_rids(
2245 &self,
2246 table: &str,
2247 index_id: u64,
2248 start: Option<&Value>,
2249 end: Option<&Value>,
2250 ) -> io::Result<Vec<RowId>> {
2251 let tree = self
2252 .by_name(table)?
2253 .expression_index_btree(index_id)
2254 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2255 Ok(tree.raw_range_rids(start, end))
2256 }
2257
2258 pub fn expression_index_ordered_rids(
2259 &self,
2260 table: &str,
2261 index_id: u64,
2262 ) -> io::Result<Vec<RowId>> {
2263 let tree = self
2264 .by_name(table)?
2265 .expression_index_btree(index_id)
2266 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2267 Ok(tree.ordered_rids_nulls_last())
2268 }
2269
2270 pub fn expression_index_ordered_rids_bounded(
2271 &self,
2272 table: &str,
2273 index_id: u64,
2274 direction: IndexOrderDirection,
2275 offset: usize,
2276 limit: usize,
2277 ) -> io::Result<Vec<RowId>> {
2278 let tree = self
2279 .by_name(table)?
2280 .expression_index_btree(index_id)
2281 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2282 Ok(tree.bounded_ordered_rids_nulls_last(
2283 direction == IndexOrderDirection::Desc,
2284 offset,
2285 limit,
2286 ))
2287 }
2288
2289 pub fn drop_expression_index(&mut self, table: &str, index_id: u64) -> io::Result<()> {
2290 self.invalidate_structure();
2291 validate_table_name(table)?;
2292 let removed = self
2293 .by_name_mut(table)?
2294 .take_expression_index(index_id)
2295 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2296 match self.persist_at_activation_boundary() {
2297 Ok(()) => {}
2298 Err(CatalogPersistError::BeforeActivation(error)) => {
2299 self.by_name_mut(table)?.restore_expression_index(removed);
2300 return Err(error);
2301 }
2302 Err(CatalogPersistError::AfterActivation(error)) => {
2303 warn!(
2304 path = %self.data_dir.display(),
2305 error = %error,
2306 "expression index drop committed but catalog directory sync failed"
2307 );
2308 }
2309 }
2310 let index_path = self
2311 .data_dir
2312 .join(expression_index_file_name(table, index_id));
2313 if let Err(error) = fs::remove_file(&index_path) {
2314 if error.kind() != io::ErrorKind::NotFound {
2315 warn!(path = %index_path.display(), error = %error, "failed to remove dropped expression index file");
2316 }
2317 } else if let Err(error) = sync_directory(&self.data_dir) {
2318 warn!(path = %self.data_dir.display(), error = %error, "failed to sync expression index deletion");
2319 }
2320 Ok(())
2321 }
2322
2323 pub fn create_expression_index_metadata(
2327 &mut self,
2328 table: &str,
2329 canonical_version: u16,
2330 canonical_text: impl Into<String>,
2331 json_path: StoredJsonPathV1,
2332 unique: bool,
2333 ) -> io::Result<u64> {
2334 self.invalidate_structure();
2335 validate_table_name(table)?;
2336 validate_column_name(&json_path.column)?;
2337 if canonical_version == 0 {
2338 return Err(io::Error::new(
2339 io::ErrorKind::InvalidInput,
2340 "expression canonical version must be non-zero",
2341 ));
2342 }
2343 let canonical_text = canonical_text.into();
2344 if canonical_text.is_empty() {
2345 return Err(io::Error::new(
2346 io::ErrorKind::InvalidInput,
2347 "expression canonical text must not be empty",
2348 ));
2349 }
2350 if canonical_version == 1 && canonical_text != json_path.canonical_text() {
2351 return Err(io::Error::new(
2352 io::ErrorKind::InvalidInput,
2353 "expression canonical text does not match its stored JSON path",
2354 ));
2355 }
2356 let table_ref = self.by_name(table)?;
2357 let root_index = table_ref
2358 .schema
2359 .column_index(&json_path.column)
2360 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "JSON root column not found"))?;
2361 if table_ref.schema.columns[root_index].type_id != TypeId::Json {
2362 return Err(io::Error::new(
2363 io::ErrorKind::InvalidInput,
2364 "expression index root column must have type json",
2365 ));
2366 }
2367 if table_ref.expression_index_metas().iter().any(|index| {
2368 index.canonical_version == canonical_version && index.canonical_text == canonical_text
2369 }) {
2370 return Err(io::Error::new(
2371 io::ErrorKind::AlreadyExists,
2372 "expression index already exists",
2373 ));
2374 }
2375
2376 let index_id = self.next_index_id;
2377 let next_index_id = index_id
2378 .checked_add(1)
2379 .ok_or_else(|| io::Error::other("expression index id space exhausted"))?;
2380 let index_path = self
2381 .data_dir
2382 .join(expression_index_file_name(table, index_id));
2383 if index_path.exists() {
2384 fs::remove_file(&index_path)?;
2388 sync_directory(&self.data_dir)?;
2389 }
2390 let meta = ExpressionIndexMeta {
2391 index_id,
2392 unique,
2393 canonical_version,
2394 canonical_text,
2395 json_path,
2396 };
2397 self.by_name_mut(table)?
2398 .install_expression_index(meta, &index_path)?;
2399 if let Err(error) = sync_directory(&self.data_dir) {
2400 self.by_name_mut(table)?
2401 .remove_expression_index_by_id(index_id);
2402 let _ = fs::remove_file(&index_path);
2403 return Err(error);
2404 }
2405
2406 let previous_version = self.active_catalog_version;
2407 let previous_next_id = self.next_index_id;
2408 self.active_catalog_version = CATALOG_VERSION;
2409 self.next_index_id = next_index_id;
2410 match self.persist_at_activation_boundary() {
2411 Ok(()) => {}
2412 Err(CatalogPersistError::BeforeActivation(error)) => {
2413 self.by_name_mut(table)?
2414 .remove_expression_index_by_id(index_id);
2415 self.active_catalog_version = previous_version;
2416 self.next_index_id = previous_next_id;
2417 let _ = fs::remove_file(&index_path);
2418 let _ = sync_directory(&self.data_dir);
2419 return Err(error);
2420 }
2421 Err(CatalogPersistError::AfterActivation(error)) => {
2422 warn!(
2423 path = %self.data_dir.display(),
2424 error = %error,
2425 "expression index creation committed but catalog directory sync failed"
2426 );
2427 }
2428 }
2429 Ok(index_id)
2430 }
2431
2432 pub fn is_index_unique(&self, table: &str, column: &str) -> Option<bool> {
2436 self.get_table(table)?.is_index_unique(column)
2437 }
2438
2439 pub fn has_index(&self, table: &str, column: &str) -> bool {
2441 self.get_table(table)
2442 .map(|t| t.has_index(column))
2443 .unwrap_or(false)
2444 }
2445
2446 pub fn index_lookup(&self, table: &str, column: &str, key: &Value) -> io::Result<Option<Row>> {
2447 Ok(self
2448 .by_name(table)?
2449 .index_lookup(column, key)
2450 .map(|(_, row)| row))
2451 }
2452
2453 pub fn list_tables(&self) -> Vec<&str> {
2454 self.tables
2458 .iter()
2459 .map(|t| t.schema.table_name.as_str())
2460 .collect()
2461 }
2462
2463 pub fn schema(&self, table: &str) -> Option<&Schema> {
2464 let slot = *self.name_to_slot.get(table)?;
2465 Some(&self.tables[slot].schema)
2466 }
2467
2468 pub fn drop_table(&mut self, name: &str) -> io::Result<()> {
2471 self.invalidate_structure();
2472 validate_table_name(name)?;
2473 let slot = *self.name_to_slot.get(name).ok_or_else(|| {
2474 io::Error::new(io::ErrorKind::NotFound, format!("table '{name}' not found"))
2475 })?;
2476 if !self.wal.is_off() {
2477 let payload = encode_ddl_drop_table(name);
2478 self.wal.append(0, WalRecordType::DdlDropTable, &payload)?;
2479 self.wal.flush()?;
2480 }
2481 let table = &self.tables[slot];
2483 let heap_path = self
2484 .data_dir
2485 .join(format!("{}.heap", table.schema.table_name));
2486 if heap_path.exists() {
2487 fs::remove_file(&heap_path)?;
2488 }
2489 for col_name in table.indexed_column_names() {
2495 let idx_path = self.data_dir.join(format!("{name}_{col_name}.idx"));
2496 if idx_path.exists() {
2497 let _ = fs::remove_file(&idx_path);
2498 }
2499 }
2500 let expression_index_ids = table.expression_index_ids();
2501 self.name_to_slot.remove(name);
2503 let last = self.tables.len() - 1;
2504 if slot != last {
2505 let moved_name = self.tables[last].schema.table_name.clone();
2506 self.tables.swap(slot, last);
2507 self.name_to_slot.insert(moved_name, slot);
2508 }
2509 self.tables.pop();
2510 self.persist()?;
2511 for index_id in expression_index_ids {
2512 let idx_path = self
2513 .data_dir
2514 .join(expression_index_file_name(name, index_id));
2515 let _ = fs::remove_file(idx_path);
2516 }
2517 Ok(())
2518 }
2519
2520 pub fn alter_table_add_column(&mut self, table: &str, col: ColumnDef) -> io::Result<()> {
2543 self.invalidate_structure();
2544 let data_dir = self.data_dir.clone();
2545 {
2546 let tbl = self.by_name_mut(table)?;
2547 if tbl.schema.columns.iter().any(|c| c.name == col.name) {
2548 return Err(io::Error::new(
2549 io::ErrorKind::AlreadyExists,
2550 format!("column '{}' already exists in table '{table}'", col.name),
2551 ));
2552 }
2553 }
2554 let barrier_lsn = if !self.wal.is_off() {
2555 let payload = encode_ddl_alter_add_column(table, &col);
2556 self.wal.append(0, WalRecordType::DdlAddColumn, &payload)?;
2557 self.wal.flush()?;
2558 self.wal.last_appended_lsn()
2559 } else {
2560 0
2561 };
2562 let tbl = self.by_name_mut(table)?;
2563
2564 let old_schema = tbl.schema.clone();
2565
2566 let has_rows = tbl.heap.scan().next().is_some();
2570
2571 if has_rows && col.required {
2572 return Err(io::Error::new(
2573 io::ErrorKind::InvalidInput,
2574 format!(
2575 "cannot add required column '{}' to non-empty table '{table}': \
2576 no default value to backfill existing rows with",
2577 col.name
2578 ),
2579 ));
2580 }
2581
2582 tbl.schema.columns.push(col);
2586 tbl.refresh_layout();
2587
2588 if has_rows {
2589 let fill: Vec<Value> = vec![Value::Empty; tbl.schema.columns.len()];
2594 tbl.rewrite_rows_for_schema_change(&old_schema, &fill, &data_dir)?;
2595 }
2596 if barrier_lsn > 0 {
2604 tbl.heap.stamp_all_pages_min_lsn(barrier_lsn)?;
2605 tbl.heap.flush()?;
2606 }
2607
2608 self.persist()?;
2609 Ok(())
2610 }
2611
2612 pub fn alter_table_drop_column(&mut self, table: &str, col_name: &str) -> io::Result<()> {
2635 self.invalidate_structure();
2636 let data_dir = self.data_dir.clone();
2637 {
2638 let tbl = self.by_name_mut(table)?;
2639 tbl.schema
2640 .columns
2641 .iter()
2642 .position(|c| c.name == col_name)
2643 .ok_or_else(|| {
2644 io::Error::new(
2645 io::ErrorKind::NotFound,
2646 format!("column '{col_name}' not found in table '{table}'"),
2647 )
2648 })?;
2649 }
2650 let removed_expression_index_ids = self
2651 .by_name_mut(table)?
2652 .remove_expression_indexes_for_root(col_name);
2653 let barrier_lsn = if !self.wal.is_off() {
2654 let payload = encode_ddl_alter_drop_column(table, col_name);
2655 self.wal.append(0, WalRecordType::DdlDropColumn, &payload)?;
2656 self.wal.flush()?;
2657 self.wal.last_appended_lsn()
2658 } else {
2659 0
2660 };
2661 let tbl = self.by_name_mut(table)?;
2662 let idx = tbl
2663 .schema
2664 .columns
2665 .iter()
2666 .position(|c| c.name == col_name)
2667 .ok_or_else(|| {
2668 io::Error::new(
2669 io::ErrorKind::NotFound,
2670 format!("column '{col_name}' not found in table '{table}'"),
2671 )
2672 })?;
2673
2674 let old_schema = tbl.schema.clone();
2676 let has_rows = tbl.heap.scan().next().is_some();
2677
2678 tbl.schema.columns.remove(idx);
2680 for (i, col) in tbl.schema.columns.iter_mut().enumerate() {
2681 col.position = i as u16;
2682 }
2683 tbl.refresh_layout();
2684
2685 if has_rows {
2686 let fill: Vec<Value> = vec![Value::Empty; tbl.schema.columns.len()];
2692 tbl.rewrite_rows_for_schema_change(&old_schema, &fill, &data_dir)?;
2693 }
2694 if barrier_lsn > 0 {
2696 tbl.heap.stamp_all_pages_min_lsn(barrier_lsn)?;
2697 tbl.heap.flush()?;
2698 }
2699
2700 self.persist()?;
2701 for index_id in removed_expression_index_ids {
2702 let idx_path = self
2703 .data_dir
2704 .join(expression_index_file_name(table, index_id));
2705 let _ = fs::remove_file(idx_path);
2706 }
2707 Ok(())
2708 }
2709}
2710
2711impl Drop for Catalog {
2712 fn drop(&mut self) {
2713 if self.read_only {
2717 return;
2718 }
2719 if self.active_tx_id.is_some() {
2720 if let Err(e) = self.abandon_active_transaction_for_drop() {
2721 warn!(error = %e, "catalog drop active transaction cleanup failed");
2722 }
2723 return;
2724 }
2725 if let Err(e) = self.checkpoint() {
2737 warn!(error = %e, "catalog drop checkpoint failed");
2738 }
2739 }
2740}
2741
2742fn encode_wal_payload(table: &str, rid: RowId, row_bytes: &[u8]) -> Vec<u8> {
2757 let name = table.as_bytes();
2758 let mut out = Vec::with_capacity(4 + name.len() + 4 + 2 + 4 + row_bytes.len());
2759 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
2760 out.extend_from_slice(name);
2761 out.extend_from_slice(&rid.page_id.to_le_bytes());
2762 out.extend_from_slice(&rid.slot_index.to_le_bytes());
2763 out.extend_from_slice(&(row_bytes.len() as u32).to_le_bytes());
2764 out.extend_from_slice(row_bytes);
2765 out
2766}
2767
2768fn decode_wal_payload(data: &[u8]) -> Option<(String, RowId, Vec<u8>)> {
2769 let mut pos = 0usize;
2770 if data.len() < 4 {
2771 return None;
2772 }
2773 let name_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2774 pos += 4;
2775 if pos + name_len > data.len() {
2776 return None;
2777 }
2778 let name = std::str::from_utf8(&data[pos..pos + name_len])
2779 .ok()?
2780 .to_string();
2781 pos += name_len;
2782 if pos + 4 + 2 + 4 > data.len() {
2783 return None;
2784 }
2785 let page_id = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
2786 pos += 4;
2787 let slot_index = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
2788 pos += 2;
2789 let row_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2790 pos += 4;
2791 if pos + row_len > data.len() {
2792 return None;
2793 }
2794 let row_bytes = data[pos..pos + row_len].to_vec();
2795 Some((
2796 name,
2797 RowId {
2798 page_id,
2799 slot_index,
2800 },
2801 row_bytes,
2802 ))
2803}
2804
2805fn write_overflow_chain_logged(
2811 heap: &mut HeapFile,
2812 wal: &mut Wal,
2813 table: &str,
2814 tx_id: u64,
2815 value: &[u8],
2816) -> io::Result<OverflowStub> {
2817 if value.len() > MAX_VALUE_SIZE {
2818 return Err(StorageError::ValueTooLarge {
2819 size: value.len(),
2820 max: MAX_VALUE_SIZE,
2821 }
2822 .into());
2823 }
2824 let n = value.len().div_ceil(OVERFLOW_PAYLOAD_CAP).max(1);
2825 let mut pages = Vec::with_capacity(n);
2826 for _ in 0..n {
2827 pages.push(heap.allocate_overflow_page()?);
2828 }
2829 for i in 0..n {
2830 let start = i * OVERFLOW_PAYLOAD_CAP;
2831 let end = (start + OVERFLOW_PAYLOAD_CAP).min(value.len());
2832 let chunk = &value[start..end];
2833 let next = if i + 1 < n {
2834 pages[i + 1]
2835 } else {
2836 OVERFLOW_CHAIN_END
2837 };
2838 let payload = encode_overflow_write_payload(table, pages[i], next, chunk);
2839 wal.append(tx_id, WalRecordType::OverflowWrite, &payload)?;
2840 let lsn = wal.last_appended_lsn();
2841 heap.write_overflow_page(pages[i], next, chunk, lsn)?;
2842 }
2843 Ok(OverflowStub::new(
2844 value.len() as u64,
2845 pages[0],
2846 crc32fast::hash(value),
2847 ))
2848}
2849
2850fn encode_row_with_spill_logged(
2855 tbl: &mut Table,
2856 wal: &mut Wal,
2857 tx_id: u64,
2858 values: &Row,
2859) -> io::Result<Vec<u8>> {
2860 let v1_len = crate::row::v1_encoded_len(tbl.row_layout(), values);
2863 let is_indexed = tbl.indexed_col_mask();
2864 let chosen = plan_spill(tbl.row_layout(), values, v1_len, &is_indexed);
2865 if chosen.is_empty() {
2866 let mut v1 = Vec::new();
2867 encode_row_into(&tbl.schema, values, &mut v1);
2868 return Ok(v1);
2869 }
2870 let table_name = tbl.schema.table_name.clone();
2871 let n_var = tbl.row_layout().n_var();
2872 let mut spilled: Vec<Option<OverflowStub>> = vec![None; n_var];
2873 for col_idx in chosen {
2874 let var_idx = tbl
2875 .row_layout()
2876 .var_index(col_idx)
2877 .expect("plan_spill only returns var columns");
2878 let bytes: Vec<u8> = match &values[col_idx] {
2879 Value::Str(s) => s.as_bytes().to_vec(),
2880 Value::Bytes(b) => b.to_vec(),
2881 Value::Json(b) => b.to_vec(),
2882 _ => continue,
2883 };
2884 let stub = write_overflow_chain_logged(&mut tbl.heap, wal, &table_name, tx_id, &bytes)?;
2885 spilled[var_idx] = Some(stub);
2886 }
2887 let mut out = Vec::new();
2888 encode_row_v2_into(&tbl.schema, tbl.row_layout(), values, &spilled, &mut out);
2889 Ok(out)
2890}
2891
2892fn encode_overflow_write_payload(
2901 table: &str,
2902 page_id: u32,
2903 next_page: u32,
2904 chunk: &[u8],
2905) -> Vec<u8> {
2906 let name = table.as_bytes();
2907 let mut out = Vec::with_capacity(2 + name.len() + 4 + 4 + 2 + chunk.len());
2908 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
2909 out.extend_from_slice(name);
2910 out.extend_from_slice(&page_id.to_le_bytes());
2911 out.extend_from_slice(&next_page.to_le_bytes());
2912 out.extend_from_slice(&(chunk.len() as u16).to_le_bytes());
2913 out.extend_from_slice(chunk);
2914 out
2915}
2916
2917fn decode_overflow_write_payload(data: &[u8]) -> Option<(String, u32, u32, Vec<u8>)> {
2918 let mut pos = 0usize;
2919 if data.len() < 2 {
2920 return None;
2921 }
2922 let name_len = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
2923 pos += 2;
2924 if pos + name_len + 4 + 4 + 2 > data.len() {
2925 return None;
2926 }
2927 let name = std::str::from_utf8(&data[pos..pos + name_len])
2928 .ok()?
2929 .to_string();
2930 pos += name_len;
2931 let page_id = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
2932 pos += 4;
2933 let next_page = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
2934 pos += 4;
2935 let chunk_len = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
2936 pos += 2;
2937 if pos + chunk_len > data.len() {
2938 return None;
2939 }
2940 Some((
2941 name,
2942 page_id,
2943 next_page,
2944 data[pos..pos + chunk_len].to_vec(),
2945 ))
2946}
2947
2948fn encode_overflow_free_payload(table: &str, pages: &[u32]) -> Vec<u8> {
2952 let name = table.as_bytes();
2953 let mut out = Vec::with_capacity(2 + name.len() + 4 + pages.len() * 4);
2954 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
2955 out.extend_from_slice(name);
2956 out.extend_from_slice(&(pages.len() as u32).to_le_bytes());
2957 for p in pages {
2958 out.extend_from_slice(&p.to_le_bytes());
2959 }
2960 out
2961}
2962
2963fn decode_overflow_free_payload(data: &[u8]) -> Option<(String, Vec<u32>)> {
2964 let mut pos = 0usize;
2965 if data.len() < 2 {
2966 return None;
2967 }
2968 let name_len = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
2969 pos += 2;
2970 if pos + name_len + 4 > data.len() {
2971 return None;
2972 }
2973 let name = std::str::from_utf8(&data[pos..pos + name_len])
2974 .ok()?
2975 .to_string();
2976 pos += name_len;
2977 let count = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2978 pos += 4;
2979 if pos + count * 4 > data.len() {
2980 return None;
2981 }
2982 let mut pages = Vec::with_capacity(count);
2983 for _ in 0..count {
2984 pages.push(u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?));
2985 pos += 4;
2986 }
2987 Some((name, pages))
2988}
2989
2990fn encode_ddl_create_table(
2993 schema: &Schema,
2994 defaults: &[Option<Value>],
2995 auto_cols: &[bool],
2996) -> Vec<u8> {
2997 let name = schema.table_name.as_bytes();
2998 let mut out = Vec::new();
2999 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3000 out.extend_from_slice(name);
3001 out.extend_from_slice(&(schema.columns.len() as u16).to_le_bytes());
3002 for col in &schema.columns {
3003 let cn = col.name.as_bytes();
3004 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3005 out.extend_from_slice(cn);
3006 out.push(col.type_id as u8);
3007 out.push(col.required as u8);
3008 out.extend_from_slice(&col.position.to_le_bytes());
3009 }
3010 encode_defaults_section(&mut out, defaults);
3014 encode_auto_section(&mut out, auto_cols);
3015 out
3016}
3017
3018fn decode_ddl_create_table(data: &[u8]) -> Option<(Schema, Vec<Option<Value>>, Vec<bool>)> {
3019 let mut pos = 0usize;
3020 if data.len() < 4 {
3021 return None;
3022 }
3023 let name_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3024 pos += 4;
3025 if pos + name_len > data.len() {
3026 return None;
3027 }
3028 let table_name = std::str::from_utf8(&data[pos..pos + name_len])
3029 .ok()?
3030 .to_string();
3031 pos += name_len;
3032 if pos + 2 > data.len() {
3033 return None;
3034 }
3035 let n_cols = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
3036 pos += 2;
3037 let mut columns = Vec::with_capacity(n_cols);
3038 for _ in 0..n_cols {
3039 if pos + 4 > data.len() {
3040 return None;
3041 }
3042 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3043 pos += 4;
3044 if pos + cn_len + 4 > data.len() {
3045 return None;
3046 }
3047 let col_name = std::str::from_utf8(&data[pos..pos + cn_len])
3048 .ok()?
3049 .to_string();
3050 pos += cn_len;
3051 let type_id = TypeId::from_u8(data[pos])?;
3052 pos += 1;
3053 let required = data[pos] != 0;
3054 pos += 1;
3055 if pos + 2 > data.len() {
3056 return None;
3057 }
3058 let position = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
3059 pos += 2;
3060 columns.push(ColumnDef {
3061 name: col_name,
3062 type_id,
3063 required,
3064 position,
3065 });
3066 }
3067 let defaults = if pos < data.len() {
3070 decode_defaults_section(data, &mut pos, columns.len())?
3071 } else {
3072 Vec::new()
3073 };
3074 let auto_cols = if pos < data.len() {
3075 decode_auto_section(data, &mut pos, columns.len())?
3076 } else {
3077 Vec::new()
3078 };
3079 Some((
3080 Schema {
3081 table_name,
3082 columns,
3083 },
3084 defaults,
3085 auto_cols,
3086 ))
3087}
3088
3089fn encode_ddl_drop_table(table_name: &str) -> Vec<u8> {
3090 let name = table_name.as_bytes();
3091 let mut out = Vec::with_capacity(4 + name.len());
3092 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3093 out.extend_from_slice(name);
3094 out
3095}
3096
3097fn encode_ddl_alter_add_column(table_name: &str, col: &ColumnDef) -> Vec<u8> {
3098 let name = table_name.as_bytes();
3099 let cn = col.name.as_bytes();
3100 let mut out = Vec::with_capacity(4 + name.len() + 4 + cn.len() + 4);
3101 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3102 out.extend_from_slice(name);
3103 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3104 out.extend_from_slice(cn);
3105 out.push(col.type_id as u8);
3106 out.push(col.required as u8);
3107 out.extend_from_slice(&col.position.to_le_bytes());
3108 out
3109}
3110
3111fn encode_ddl_alter_drop_column(table_name: &str, col_name: &str) -> Vec<u8> {
3112 let name = table_name.as_bytes();
3113 let cn = col_name.as_bytes();
3114 let mut out = Vec::with_capacity(4 + name.len() + 4 + cn.len());
3115 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3116 out.extend_from_slice(name);
3117 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3118 out.extend_from_slice(cn);
3119 out
3120}
3121
3122fn decode_ddl_table_name(data: &[u8]) -> Option<(String, usize)> {
3123 if data.len() < 4 {
3124 return None;
3125 }
3126 let name_len = u32::from_le_bytes(data[0..4].try_into().ok()?) as usize;
3127 if 4 + name_len > data.len() {
3128 return None;
3129 }
3130 let name = std::str::from_utf8(&data[4..4 + name_len])
3131 .ok()?
3132 .to_string();
3133 Some((name, 4 + name_len))
3134}
3135
3136fn decode_ddl_alter_add_column(data: &[u8]) -> Option<(String, ColumnDef)> {
3137 let (table_name, mut pos) = decode_ddl_table_name(data)?;
3138 if pos + 4 > data.len() {
3139 return None;
3140 }
3141 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3142 pos += 4;
3143 if pos + cn_len + 4 > data.len() {
3144 return None;
3145 }
3146 let col_name = std::str::from_utf8(&data[pos..pos + cn_len])
3147 .ok()?
3148 .to_string();
3149 pos += cn_len;
3150 let type_id = TypeId::from_u8(data[pos])?;
3151 pos += 1;
3152 let required = data[pos] != 0;
3153 pos += 1;
3154 if pos + 2 > data.len() {
3155 return None;
3156 }
3157 let position = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
3158 Some((
3159 table_name,
3160 ColumnDef {
3161 name: col_name,
3162 type_id,
3163 required,
3164 position,
3165 },
3166 ))
3167}
3168
3169fn decode_ddl_alter_drop_column(data: &[u8]) -> Option<(String, String)> {
3170 let (table_name, pos) = decode_ddl_table_name(data)?;
3171 if pos + 4 > data.len() {
3172 return None;
3173 }
3174 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3175 if pos + 4 + cn_len > data.len() {
3176 return None;
3177 }
3178 let col_name = std::str::from_utf8(&data[pos + 4..pos + 4 + cn_len])
3179 .ok()?
3180 .to_string();
3181 Some((table_name, col_name))
3182}
3183
3184pub(crate) struct IndexedColMeta {
3212 pub name: String,
3213 pub unique: bool,
3214}
3215
3216pub(crate) struct CatalogEntry {
3219 pub schema: Schema,
3220 pub indexed_cols: Vec<IndexedColMeta>,
3221 pub expression_indexes: Vec<ExpressionIndexMeta>,
3222 pub defaults: Vec<Option<Value>>,
3225 pub auto_cols: Vec<bool>,
3228}
3229
3230pub(crate) struct CatalogEntryRef<'a> {
3232 pub schema: &'a Schema,
3233 pub indexed_cols: Vec<IndexedColMeta>,
3234 pub expression_indexes: Vec<ExpressionIndexMeta>,
3235 pub defaults: &'a [Option<Value>],
3236 pub auto_cols: &'a [bool],
3237}
3238
3239fn encode_value_blob(out: &mut Vec<u8>, v: &Value) {
3245 out.push(v.type_id() as u8);
3246 match v {
3247 Value::Int(n) => out.extend_from_slice(&n.to_le_bytes()),
3248 Value::Float(f) => out.extend_from_slice(&f.to_bits().to_le_bytes()),
3249 Value::Bool(b) => out.push(*b as u8),
3250 Value::Str(s) => {
3251 out.extend_from_slice(&(s.len() as u32).to_le_bytes());
3252 out.extend_from_slice(s.as_bytes());
3253 }
3254 Value::DateTime(n) => out.extend_from_slice(&n.to_le_bytes()),
3255 Value::Uuid(u) => out.extend_from_slice(u),
3256 Value::Bytes(b) => {
3257 out.extend_from_slice(&(b.len() as u32).to_le_bytes());
3258 out.extend_from_slice(b);
3259 }
3260 Value::Json(b) => {
3261 out.extend_from_slice(&(b.len() as u32).to_le_bytes());
3262 out.extend_from_slice(b);
3263 }
3264 Value::Empty => {}
3265 }
3266}
3267
3268fn decode_value_blob(data: &[u8], pos: &mut usize) -> Option<Value> {
3271 let tag = *data.get(*pos)?;
3272 *pos += 1;
3273 let type_id = TypeId::from_u8(tag)?;
3274 let take_fixed = |pos: &mut usize, n: usize| -> Option<Vec<u8>> {
3275 if *pos + n > data.len() {
3276 return None;
3277 }
3278 let slice = data[*pos..*pos + n].to_vec();
3279 *pos += n;
3280 Some(slice)
3281 };
3282 match type_id {
3283 TypeId::Empty => Some(Value::Empty),
3284 TypeId::Int => Some(Value::Int(i64::from_le_bytes(
3285 take_fixed(pos, 8)?.try_into().ok()?,
3286 ))),
3287 TypeId::Float => Some(Value::Float(f64::from_bits(u64::from_le_bytes(
3288 take_fixed(pos, 8)?.try_into().ok()?,
3289 )))),
3290 TypeId::Bool => Some(Value::Bool(take_fixed(pos, 1)?[0] != 0)),
3291 TypeId::DateTime => Some(Value::DateTime(i64::from_le_bytes(
3292 take_fixed(pos, 8)?.try_into().ok()?,
3293 ))),
3294 TypeId::Uuid => Some(Value::Uuid(take_fixed(pos, 16)?.try_into().ok()?)),
3295 TypeId::Str => {
3296 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
3297 Some(Value::Str(String::from_utf8(take_fixed(pos, len)?).ok()?))
3298 }
3299 TypeId::Bytes => {
3300 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
3301 Some(Value::Bytes(take_fixed(pos, len)?))
3302 }
3303 TypeId::Json => {
3304 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
3305 Some(Value::Json(take_fixed(pos, len)?.into()))
3306 }
3307 }
3308}
3309
3310fn encode_defaults_section(out: &mut Vec<u8>, defaults: &[Option<Value>]) {
3314 let present: Vec<(u16, &Value)> = defaults
3315 .iter()
3316 .enumerate()
3317 .filter_map(|(i, d)| d.as_ref().map(|v| (i as u16, v)))
3318 .collect();
3319 out.extend_from_slice(&(present.len() as u16).to_le_bytes());
3320 for (pos, v) in present {
3321 out.extend_from_slice(&pos.to_le_bytes());
3322 encode_value_blob(out, v);
3323 }
3324}
3325
3326fn decode_defaults_section(
3329 data: &[u8],
3330 pos: &mut usize,
3331 n_cols: usize,
3332) -> Option<Vec<Option<Value>>> {
3333 if *pos + 2 > data.len() {
3334 return None;
3335 }
3336 let count = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3337 *pos += 2;
3338 let mut out = vec![None; n_cols];
3339 for _ in 0..count {
3340 if *pos + 2 > data.len() {
3341 return None;
3342 }
3343 let col = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3344 *pos += 2;
3345 let value = decode_value_blob(data, pos)?;
3346 if col < n_cols {
3347 out[col] = Some(value);
3348 }
3349 }
3350 Some(out)
3351}
3352
3353fn encode_auto_section(out: &mut Vec<u8>, auto_cols: &[bool]) {
3357 let present: Vec<u16> = auto_cols
3358 .iter()
3359 .enumerate()
3360 .filter_map(|(i, &a)| if a { Some(i as u16) } else { None })
3361 .collect();
3362 out.extend_from_slice(&(present.len() as u16).to_le_bytes());
3363 for pos in present {
3364 out.extend_from_slice(&pos.to_le_bytes());
3365 }
3366}
3367
3368fn decode_auto_section(data: &[u8], pos: &mut usize, n_cols: usize) -> Option<Vec<bool>> {
3371 if *pos + 2 > data.len() {
3372 return None;
3373 }
3374 let count = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3375 *pos += 2;
3376 let mut out = vec![false; n_cols];
3377 for _ in 0..count {
3378 if *pos + 2 > data.len() {
3379 return None;
3380 }
3381 let col = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3382 *pos += 2;
3383 if col < n_cols {
3384 out[col] = true;
3385 }
3386 }
3387 Some(out)
3388}
3389
3390fn push_catalog_string(out: &mut Vec<u8>, value: &str) -> io::Result<()> {
3391 let len = u32::try_from(value.len())
3392 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "catalog string is too large"))?;
3393 out.extend_from_slice(&len.to_le_bytes());
3394 out.extend_from_slice(value.as_bytes());
3395 Ok(())
3396}
3397
3398fn encode_expression_indexes(out: &mut Vec<u8>, indexes: &[ExpressionIndexMeta]) -> io::Result<()> {
3399 let count = u16::try_from(indexes.len()).map_err(|_| {
3400 io::Error::new(
3401 io::ErrorKind::InvalidInput,
3402 "too many expression indexes on one table",
3403 )
3404 })?;
3405 out.extend_from_slice(&count.to_le_bytes());
3406 for index in indexes {
3407 out.extend_from_slice(&index.index_id.to_le_bytes());
3408 out.push(u8::from(index.unique));
3409 out.extend_from_slice(&index.canonical_version.to_le_bytes());
3410 push_catalog_string(out, &index.canonical_text)?;
3411 push_catalog_string(out, &index.json_path.column)?;
3412 let segment_count = u16::try_from(index.json_path.segments.len()).map_err(|_| {
3413 io::Error::new(
3414 io::ErrorKind::InvalidInput,
3415 "JSON path has too many segments",
3416 )
3417 })?;
3418 out.extend_from_slice(&segment_count.to_le_bytes());
3419 for segment in &index.json_path.segments {
3420 match segment {
3421 StoredJsonPathSegmentV1::Key(key) => {
3422 out.push(1);
3423 push_catalog_string(out, key)?;
3424 }
3425 StoredJsonPathSegmentV1::Index(position) => {
3426 out.push(2);
3427 out.extend_from_slice(&position.to_le_bytes());
3428 }
3429 }
3430 }
3431 }
3432 Ok(())
3433}
3434
3435fn decode_expression_indexes(data: &[u8], pos: &mut usize) -> io::Result<Vec<ExpressionIndexMeta>> {
3436 let count = read_u16(data, pos)? as usize;
3437 let mut indexes = Vec::with_capacity(count);
3438 for _ in 0..count {
3439 let index_id = read_u64(data, pos)?;
3440 if index_id == 0 {
3441 return Err(io::Error::new(
3442 io::ErrorKind::InvalidData,
3443 "expression index id must be non-zero",
3444 ));
3445 }
3446 let unique = read_u8(data, pos)? != 0;
3447 let canonical_version = read_u16(data, pos)?;
3448 let canonical_len = read_u32(data, pos)? as usize;
3449 let canonical_text = read_string(data, pos, canonical_len)?;
3450 let column_len = read_u32(data, pos)? as usize;
3451 let column = read_string(data, pos, column_len)?;
3452 let segment_count = read_u16(data, pos)? as usize;
3453 let mut segments = Vec::with_capacity(segment_count);
3454 for _ in 0..segment_count {
3455 match read_u8(data, pos)? {
3456 1 => {
3457 let len = read_u32(data, pos)? as usize;
3458 segments.push(StoredJsonPathSegmentV1::Key(read_string(data, pos, len)?));
3459 }
3460 2 => segments.push(StoredJsonPathSegmentV1::Index(read_u32(data, pos)?)),
3461 tag => {
3462 return Err(io::Error::new(
3463 io::ErrorKind::InvalidData,
3464 format!("unknown stored JSON path segment tag: {tag}"),
3465 ));
3466 }
3467 }
3468 }
3469 indexes.push(ExpressionIndexMeta {
3470 index_id,
3471 unique,
3472 canonical_version,
3473 canonical_text,
3474 json_path: StoredJsonPathV1 { column, segments },
3475 });
3476 }
3477 Ok(indexes)
3478}
3479
3480fn write_catalog_file(
3481 path: &Path,
3482 version: u16,
3483 next_index_id: u64,
3484 entries: &[CatalogEntryRef<'_>],
3485) -> io::Result<()> {
3486 if !(1..=CATALOG_VERSION).contains(&version) {
3487 return Err(io::Error::new(
3488 io::ErrorKind::InvalidInput,
3489 format!("unsupported catalog write version: {version}"),
3490 ));
3491 }
3492 let mut buf: Vec<u8> = Vec::with_capacity(64);
3493 buf.extend_from_slice(CATALOG_MAGIC);
3494 buf.extend_from_slice(&version.to_le_bytes());
3495 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
3496 if version >= 6 {
3497 buf.extend_from_slice(&next_index_id.to_le_bytes());
3498 }
3499
3500 for entry in entries {
3501 let schema = entry.schema;
3502 let name = schema.table_name.as_bytes();
3503 buf.extend_from_slice(&(name.len() as u32).to_le_bytes());
3504 buf.extend_from_slice(name);
3505 buf.extend_from_slice(&(schema.columns.len() as u16).to_le_bytes());
3506 for col in &schema.columns {
3507 let cn = col.name.as_bytes();
3508 buf.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3509 buf.extend_from_slice(cn);
3510 buf.push(col.type_id as u8);
3511 buf.push(if col.required { 1 } else { 0 });
3512 buf.extend_from_slice(&col.position.to_le_bytes());
3513 }
3514 buf.extend_from_slice(&(entry.indexed_cols.len() as u16).to_le_bytes());
3516 for meta in &entry.indexed_cols {
3517 let cn = meta.name.as_bytes();
3518 buf.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3519 buf.extend_from_slice(cn);
3520 buf.push(if meta.unique { 1 } else { 0 });
3521 }
3522 encode_defaults_section(&mut buf, entry.defaults);
3524 encode_auto_section(&mut buf, entry.auto_cols);
3526 if version >= 6 {
3527 encode_expression_indexes(&mut buf, &entry.expression_indexes)?;
3528 }
3529 }
3530
3531 let crc = crc32fast::hash(&buf);
3535 buf.extend_from_slice(&crc.to_le_bytes());
3536
3537 let mut f = fs::OpenOptions::new()
3538 .create(true)
3539 .write(true)
3540 .truncate(true)
3541 .open(path)?;
3542 f.write_all(&buf)?;
3543 f.sync_data()?;
3544 Ok(())
3545}
3546
3547struct CatalogFile {
3548 version: u16,
3549 next_index_id: u64,
3550 entries: Vec<CatalogEntry>,
3551}
3552
3553fn read_catalog_file(path: &Path) -> io::Result<CatalogFile> {
3554 read_catalog_file_with_max_version(path, CATALOG_VERSION)
3555}
3556
3557pub fn read_active_catalog_version(data_dir: &Path) -> io::Result<u16> {
3563 let cat_path = data_dir.join(CATALOG_FILE);
3564 Ok(read_catalog_file(&cat_path)?.version)
3565}
3566
3567fn read_catalog_file_with_max_version(
3568 path: &Path,
3569 max_supported_version: u16,
3570) -> io::Result<CatalogFile> {
3571 let mut f = fs::File::open(path)?;
3572 let mut buf = Vec::new();
3573 f.read_to_end(&mut buf)?;
3574
3575 let mut pos = 0usize;
3576 if buf.len() < 14 || &buf[0..4] != CATALOG_MAGIC {
3578 return Err(io::Error::new(
3579 io::ErrorKind::InvalidData,
3580 "bad catalog magic",
3581 ));
3582 }
3583
3584 let payload = &buf[..buf.len() - 4];
3586 let stored_crc = u32::from_le_bytes(
3587 buf[buf.len() - 4..]
3588 .try_into()
3589 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog CRC"))?,
3590 );
3591 let computed_crc = crc32fast::hash(payload);
3592 if stored_crc != computed_crc {
3593 return Err(io::Error::new(
3594 io::ErrorKind::InvalidData,
3595 format!(
3596 "catalog CRC32 mismatch: expected {stored_crc:#010x}, got {computed_crc:#010x}"
3597 ),
3598 ));
3599 }
3600 let buf = &buf[..buf.len() - 4];
3602 pos += 4;
3603 let version = u16::from_le_bytes(
3604 buf[pos..pos + 2]
3605 .try_into()
3606 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog header"))?,
3607 );
3608 pos += 2;
3609 if version == 0 || version > max_supported_version {
3619 return Err(io::Error::new(
3620 io::ErrorKind::InvalidData,
3621 format!("unsupported catalog version: {version}"),
3622 ));
3623 }
3624 let n_tables = u32::from_le_bytes(
3625 buf[pos..pos + 4]
3626 .try_into()
3627 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog header"))?,
3628 ) as usize;
3629 pos += 4;
3630 let next_index_id = if version >= 6 {
3631 let id = read_u64(buf, &mut pos)?;
3632 if id == 0 {
3633 return Err(io::Error::new(
3634 io::ErrorKind::InvalidData,
3635 "catalog next index id must be non-zero",
3636 ));
3637 }
3638 id
3639 } else {
3640 1
3641 };
3642
3643 if n_tables > buf.len() {
3650 return Err(io::Error::new(
3651 io::ErrorKind::InvalidData,
3652 format!("catalog file corrupt: implausible table count {n_tables}"),
3653 ));
3654 }
3655
3656 let mut entries = Vec::with_capacity(n_tables);
3657 for _ in 0..n_tables {
3658 let name_len = read_u32(buf, &mut pos)? as usize;
3659 let table_name = read_string(buf, &mut pos, name_len)?;
3660 let n_cols = read_u16(buf, &mut pos)? as usize;
3661
3662 let mut columns = Vec::with_capacity(n_cols);
3663 for _ in 0..n_cols {
3664 let cname_len = read_u32(buf, &mut pos)? as usize;
3665 let name = read_string(buf, &mut pos, cname_len)?;
3666 let type_id_raw = read_u8(buf, &mut pos)?;
3667 let type_id = type_id_from_u8(type_id_raw)?;
3668 let required = read_u8(buf, &mut pos)? != 0;
3669 let position = read_u16(buf, &mut pos)?;
3670 columns.push(ColumnDef {
3671 name,
3672 type_id,
3673 required,
3674 position,
3675 });
3676 }
3677
3678 let indexed_cols: Vec<IndexedColMeta> = if version >= 3 {
3682 let n = read_u16(buf, &mut pos)? as usize;
3683 let mut v = Vec::with_capacity(n);
3684 for _ in 0..n {
3685 let l = read_u32(buf, &mut pos)? as usize;
3686 let name = read_string(buf, &mut pos, l)?;
3687 let unique = read_u8(buf, &mut pos)? != 0;
3688 v.push(IndexedColMeta { name, unique });
3689 }
3690 v
3691 } else if version >= 2 {
3692 let n = read_u16(buf, &mut pos)? as usize;
3693 let mut v = Vec::with_capacity(n);
3694 for _ in 0..n {
3695 let l = read_u32(buf, &mut pos)? as usize;
3696 let name = read_string(buf, &mut pos, l)?;
3697 v.push(IndexedColMeta {
3698 name,
3699 unique: false,
3700 });
3701 }
3702 v
3703 } else {
3704 Vec::new()
3705 };
3706
3707 let defaults = if version >= 4 {
3709 decode_defaults_section(buf, &mut pos, columns.len()).ok_or_else(|| {
3710 io::Error::new(io::ErrorKind::InvalidData, "truncated catalog defaults")
3711 })?
3712 } else {
3713 Vec::new()
3714 };
3715
3716 let auto_cols = if version >= 5 {
3718 decode_auto_section(buf, &mut pos, columns.len()).ok_or_else(|| {
3719 io::Error::new(io::ErrorKind::InvalidData, "truncated catalog auto columns")
3720 })?
3721 } else {
3722 Vec::new()
3723 };
3724
3725 let expression_indexes = if version >= 6 {
3726 decode_expression_indexes(buf, &mut pos)?
3727 } else {
3728 Vec::new()
3729 };
3730
3731 entries.push(CatalogEntry {
3732 schema: Schema {
3733 table_name,
3734 columns,
3735 },
3736 indexed_cols,
3737 expression_indexes,
3738 defaults,
3739 auto_cols,
3740 });
3741 }
3742
3743 let mut seen_index_ids = FxHashMap::default();
3744 let mut max_index_id = 0;
3745 for entry in &entries {
3746 for index in &entry.expression_indexes {
3747 if index.canonical_version == 0 || index.canonical_text.is_empty() {
3748 return Err(io::Error::new(
3749 io::ErrorKind::InvalidData,
3750 "expression index has invalid canonical identity",
3751 ));
3752 }
3753 if index.canonical_version == 1
3754 && index.canonical_text != index.json_path.canonical_text()
3755 {
3756 return Err(io::Error::new(
3757 io::ErrorKind::InvalidData,
3758 "expression index canonical identity does not match its JSON path",
3759 ));
3760 }
3761 let Some(root) = entry
3762 .schema
3763 .columns
3764 .iter()
3765 .find(|column| column.name == index.json_path.column)
3766 else {
3767 return Err(io::Error::new(
3768 io::ErrorKind::InvalidData,
3769 "expression index JSON root is absent from its table",
3770 ));
3771 };
3772 if root.type_id != TypeId::Json {
3773 return Err(io::Error::new(
3774 io::ErrorKind::InvalidData,
3775 "expression index root column is not JSON",
3776 ));
3777 }
3778 if seen_index_ids.insert(index.index_id, ()).is_some() {
3779 return Err(io::Error::new(
3780 io::ErrorKind::InvalidData,
3781 "duplicate expression index id in catalog",
3782 ));
3783 }
3784 max_index_id = max_index_id.max(index.index_id);
3785 }
3786 }
3787 if next_index_id <= max_index_id {
3788 return Err(io::Error::new(
3789 io::ErrorKind::InvalidData,
3790 "catalog next index id does not exceed persisted index ids",
3791 ));
3792 }
3793 Ok(CatalogFile {
3794 version,
3795 next_index_id,
3796 entries,
3797 })
3798}
3799
3800fn read_u8(buf: &[u8], pos: &mut usize) -> io::Result<u8> {
3801 if *pos >= buf.len() {
3802 return Err(io::Error::new(
3803 io::ErrorKind::UnexpectedEof,
3804 "truncated catalog",
3805 ));
3806 }
3807 let v = buf[*pos];
3808 *pos += 1;
3809 Ok(v)
3810}
3811fn read_u16(buf: &[u8], pos: &mut usize) -> io::Result<u16> {
3812 if *pos + 2 > buf.len() {
3813 return Err(io::Error::new(
3814 io::ErrorKind::UnexpectedEof,
3815 "truncated catalog",
3816 ));
3817 }
3818 let v = u16::from_le_bytes(
3819 buf[*pos..*pos + 2]
3820 .try_into()
3821 .expect("bounds checked above"),
3822 );
3823 *pos += 2;
3824 Ok(v)
3825}
3826fn read_u32(buf: &[u8], pos: &mut usize) -> io::Result<u32> {
3827 if *pos + 4 > buf.len() {
3828 return Err(io::Error::new(
3829 io::ErrorKind::UnexpectedEof,
3830 "truncated catalog",
3831 ));
3832 }
3833 let v = u32::from_le_bytes(
3834 buf[*pos..*pos + 4]
3835 .try_into()
3836 .expect("bounds checked above"),
3837 );
3838 *pos += 4;
3839 Ok(v)
3840}
3841fn read_u64(buf: &[u8], pos: &mut usize) -> io::Result<u64> {
3842 if *pos + 8 > buf.len() {
3843 return Err(io::Error::new(
3844 io::ErrorKind::UnexpectedEof,
3845 "truncated catalog",
3846 ));
3847 }
3848 let value = u64::from_le_bytes(
3849 buf[*pos..*pos + 8]
3850 .try_into()
3851 .expect("bounds checked above"),
3852 );
3853 *pos += 8;
3854 Ok(value)
3855}
3856fn read_string(buf: &[u8], pos: &mut usize, len: usize) -> io::Result<String> {
3857 if *pos + len > buf.len() {
3858 return Err(io::Error::new(
3859 io::ErrorKind::UnexpectedEof,
3860 "truncated catalog string",
3861 ));
3862 }
3863 let s = std::str::from_utf8(&buf[*pos..*pos + len])
3864 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "non-utf8 in catalog"))?
3865 .to_string();
3866 *pos += len;
3867 Ok(s)
3868}
3869fn type_id_from_u8(v: u8) -> io::Result<TypeId> {
3870 match v {
3871 0 => Ok(TypeId::Empty),
3872 1 => Ok(TypeId::Int),
3873 2 => Ok(TypeId::Float),
3874 3 => Ok(TypeId::Bool),
3875 4 => Ok(TypeId::Str),
3876 5 => Ok(TypeId::DateTime),
3877 6 => Ok(TypeId::Uuid),
3878 7 => Ok(TypeId::Bytes),
3879 8 => Ok(TypeId::Json),
3880 _ => Err(io::Error::new(
3881 io::ErrorKind::InvalidData,
3882 format!("unknown type id: {v}"),
3883 )),
3884 }
3885}
3886
3887#[cfg(test)]
3888mod tests {
3889 use super::*;
3890
3891 fn fail_next_catalog_persist_at(stage: u8) {
3892 CATALOG_PERSIST_FAILPOINT.with(|failpoint| failpoint.set(stage));
3893 }
3894
3895 fn temp_catalog(name: &str) -> Catalog {
3896 let dir = std::env::temp_dir().join(format!("powdb_cat_{name}_{}", std::process::id()));
3897 Catalog::create(&dir).unwrap()
3898 }
3899
3900 fn hash_dir_tree(dir: &std::path::Path) -> String {
3905 let mut entries: Vec<std::path::PathBuf> = Vec::new();
3906 fn walk(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
3907 let mut items: Vec<_> = fs::read_dir(dir).unwrap().flatten().collect();
3908 items.sort_by_key(std::fs::DirEntry::path);
3909 for item in items {
3910 let path = item.path();
3911 if path.is_dir() {
3912 walk(&path, out);
3913 } else {
3914 out.push(path);
3915 }
3916 }
3917 }
3918 walk(dir, &mut entries);
3919 let mut hasher = crc32fast::Hasher::new();
3920 for path in &entries {
3921 hasher.update(path.to_string_lossy().as_bytes());
3922 hasher.update(&fs::read(path).unwrap());
3923 }
3924 format!("{:08x}", hasher.finalize())
3925 }
3926
3927 fn seed_quiescent_dir(dir: &std::path::Path) {
3928 let mut catalog = Catalog::create(dir).unwrap();
3929 catalog
3930 .create_table(Schema {
3931 table_name: "User".into(),
3932 columns: vec![
3933 ColumnDef {
3934 name: "name".into(),
3935 type_id: TypeId::Str,
3936 required: true,
3937 position: 0,
3938 },
3939 ColumnDef {
3940 name: "age".into(),
3941 type_id: TypeId::Int,
3942 required: false,
3943 position: 1,
3944 },
3945 ],
3946 })
3947 .unwrap();
3948 catalog.create_index("User", "age").unwrap();
3949 catalog
3950 .insert("User", &vec![Value::Str("Ada".into()), Value::Int(36)])
3951 .unwrap();
3952 catalog
3953 .insert("User", &vec![Value::Str("Bo".into()), Value::Int(20)])
3954 .unwrap();
3955 drop(catalog);
3958 }
3959
3960 #[test]
3961 fn open_read_only_serves_reads_on_clean_dir() {
3962 let dir = tempfile::tempdir().unwrap();
3963 seed_quiescent_dir(dir.path());
3964
3965 let catalog = Catalog::open_read_only(dir.path()).unwrap();
3966 let rows: Vec<_> = catalog.scan("User").unwrap().collect();
3967 assert_eq!(rows.len(), 2);
3968 let hit = catalog
3970 .index_lookup("User", "age", &Value::Int(36))
3971 .unwrap();
3972 assert_eq!(hit.unwrap()[0], Value::Str("Ada".into()));
3973 }
3974
3975 #[test]
3976 fn open_read_only_never_mutates_dir() {
3977 let dir = tempfile::tempdir().unwrap();
3978 seed_quiescent_dir(dir.path());
3979 let before = hash_dir_tree(dir.path());
3980
3981 {
3982 let catalog = Catalog::open_read_only(dir.path()).unwrap();
3983 let _ = catalog.scan("User").unwrap().count();
3984 let _ = catalog
3985 .index_lookup("User", "age", &Value::Int(20))
3986 .unwrap();
3987 }
3989 let after = hash_dir_tree(dir.path());
3990 assert_eq!(
3991 before, after,
3992 "read-only open + queries + drop must leave the directory byte-identical"
3993 );
3994 }
3995
3996 #[test]
3997 fn open_read_only_refuses_non_empty_wal() {
3998 let dir = tempfile::tempdir().unwrap();
3999 {
4002 let mut catalog = Catalog::create(dir.path()).unwrap();
4003 catalog
4004 .create_table(Schema {
4005 table_name: "T".into(),
4006 columns: vec![ColumnDef {
4007 name: "id".into(),
4008 type_id: TypeId::Int,
4009 required: true,
4010 position: 0,
4011 }],
4012 })
4013 .unwrap();
4014 catalog.insert("T", &vec![Value::Int(1)]).unwrap();
4015 catalog.sync_wal().unwrap();
4016 std::mem::forget(catalog); }
4018 let err = match Catalog::open_read_only(dir.path()) {
4019 Ok(_) => panic!("read-only open must refuse a non-empty WAL"),
4020 Err(err) => err,
4021 };
4022 assert!(
4023 err.to_string().contains("WAL is not empty"),
4024 "expected a WAL-not-empty refusal naming the remedy, got: {err}"
4025 );
4026 assert!(err.to_string().contains("read-write engine"));
4027 }
4028
4029 #[test]
4030 fn open_read_only_expression_index_reads_work() {
4031 let dir = tempfile::tempdir().unwrap();
4032 {
4033 let mut catalog = Catalog::create(dir.path()).unwrap();
4034 catalog
4035 .create_table(Schema {
4036 table_name: "Doc".into(),
4037 columns: vec![ColumnDef {
4038 name: "data".into(),
4039 type_id: TypeId::Json,
4040 required: false,
4041 position: 0,
4042 }],
4043 })
4044 .unwrap();
4045 let path =
4046 StoredJsonPathV1::new("data", vec![StoredJsonPathSegmentV1::Key("author".into())]);
4047 catalog
4048 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path, false)
4049 .unwrap();
4050 drop(catalog);
4051 }
4052 let before = hash_dir_tree(dir.path());
4054 let catalog = Catalog::open_read_only(dir.path()).unwrap();
4055 assert_eq!(catalog.scan("Doc").unwrap().count(), 0);
4056 drop(catalog);
4057 let after = hash_dir_tree(dir.path());
4058 assert_eq!(
4059 before, after,
4060 "read-only expression-index load must not write"
4061 );
4062 }
4063
4064 #[test]
4065 fn v5_reader_rejects_v6_catalog() {
4066 let dir = tempfile::tempdir().unwrap();
4067 let mut catalog = Catalog::create(dir.path()).unwrap();
4068 catalog
4069 .create_table(Schema {
4070 table_name: "Doc".into(),
4071 columns: vec![ColumnDef {
4072 name: "data".into(),
4073 type_id: TypeId::Json,
4074 required: false,
4075 position: 0,
4076 }],
4077 })
4078 .unwrap();
4079 let path =
4080 StoredJsonPathV1::new("data", vec![StoredJsonPathSegmentV1::Key("author".into())]);
4081 catalog
4082 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path, false)
4083 .unwrap();
4084 let result = read_catalog_file_with_max_version(
4085 &dir.path().join(CATALOG_FILE),
4086 LEGACY_CATALOG_VERSION,
4087 );
4088 let error = match result {
4089 Ok(_) => panic!("a v5 reader must reject v6 before decoding its payload"),
4090 Err(error) => error,
4091 };
4092 assert!(error.to_string().contains("unsupported catalog version: 6"));
4093 }
4094
4095 #[test]
4096 fn expression_index_rolls_back_only_before_catalog_rename() {
4097 let before_dir = tempfile::tempdir().unwrap();
4098 let mut before = Catalog::create(before_dir.path()).unwrap();
4099 before
4100 .create_table(Schema {
4101 table_name: "Doc".into(),
4102 columns: vec![ColumnDef {
4103 name: "data".into(),
4104 type_id: TypeId::Json,
4105 required: false,
4106 position: 0,
4107 }],
4108 })
4109 .unwrap();
4110 let path =
4111 StoredJsonPathV1::new("data", vec![StoredJsonPathSegmentV1::Key("score".into())]);
4112
4113 fail_next_catalog_persist_at(1);
4114 let error = before
4115 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path.clone(), false)
4116 .unwrap_err();
4117 assert!(error.to_string().contains("before rename"));
4118 assert_eq!(before.active_catalog_version(), LEGACY_CATALOG_VERSION);
4119 assert_eq!(before.next_index_id(), 1);
4120 assert!(before.expression_index_metadata("Doc").unwrap().is_empty());
4121 assert!(!before_dir
4122 .path()
4123 .join(expression_index_file_name("Doc", 1))
4124 .exists());
4125
4126 let before_index_id = before
4127 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path.clone(), false)
4128 .unwrap();
4129 fail_next_catalog_persist_at(1);
4130 let error = before
4131 .drop_expression_index("Doc", before_index_id)
4132 .unwrap_err();
4133 assert!(error.to_string().contains("before rename"));
4134 assert!(before
4135 .expression_index_btree("Doc", before_index_id)
4136 .is_some());
4137 assert!(before_dir
4138 .path()
4139 .join(expression_index_file_name("Doc", 1))
4140 .exists());
4141 std::mem::forget(before);
4142 let before_reopened = Catalog::open(before_dir.path()).unwrap();
4143 assert!(before_reopened
4144 .expression_index_btree("Doc", before_index_id)
4145 .is_some());
4146
4147 let after_dir = tempfile::tempdir().unwrap();
4148 let mut after = Catalog::create(after_dir.path()).unwrap();
4149 after
4150 .create_table(Schema {
4151 table_name: "Doc".into(),
4152 columns: vec![ColumnDef {
4153 name: "data".into(),
4154 type_id: TypeId::Json,
4155 required: false,
4156 position: 0,
4157 }],
4158 })
4159 .unwrap();
4160 fail_next_catalog_persist_at(2);
4161 let index_id = after
4162 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path.clone(), false)
4163 .unwrap();
4164 assert_eq!(index_id, 1);
4165 assert_eq!(after.active_catalog_version(), CATALOG_VERSION);
4166 assert_eq!(after.next_index_id(), 2);
4167 assert!(after.expression_index_btree("Doc", index_id).is_some());
4168 assert!(after_dir
4169 .path()
4170 .join(expression_index_file_name("Doc", 1))
4171 .exists());
4172 std::mem::forget(after);
4173
4174 let mut reopened = Catalog::open(after_dir.path()).unwrap();
4175 assert!(reopened.expression_index_btree("Doc", index_id).is_some());
4176 fail_next_catalog_persist_at(2);
4177 reopened.drop_expression_index("Doc", index_id).unwrap();
4178 assert!(reopened
4179 .expression_index_metadata("Doc")
4180 .unwrap()
4181 .is_empty());
4182 assert!(!after_dir
4183 .path()
4184 .join(expression_index_file_name("Doc", 1))
4185 .exists());
4186 std::mem::forget(reopened);
4187
4188 let final_open = Catalog::open(after_dir.path()).unwrap();
4189 assert!(final_open
4190 .expression_index_metadata("Doc")
4191 .unwrap()
4192 .is_empty());
4193 }
4194
4195 #[test]
4196 fn ordinary_catalog_persist_reports_post_rename_directory_sync_failure() {
4197 let dir = tempfile::tempdir().unwrap();
4198 let mut catalog = Catalog::create(dir.path()).unwrap();
4199 fail_next_catalog_persist_at(2);
4200 let error = catalog
4201 .create_table(Schema {
4202 table_name: "VisibleAfterRename".into(),
4203 columns: vec![ColumnDef {
4204 name: "id".into(),
4205 type_id: TypeId::Int,
4206 required: true,
4207 position: 0,
4208 }],
4209 })
4210 .unwrap_err();
4211 assert!(error.to_string().contains("after rename"));
4212 assert!(catalog.schema("VisibleAfterRename").is_some());
4213
4214 std::mem::forget(catalog);
4215 let reopened = Catalog::open(dir.path()).unwrap();
4216 assert!(reopened.schema("VisibleAfterRename").is_some());
4217 }
4218
4219 fn schema_two_cols() -> Schema {
4220 Schema {
4221 table_name: "T".into(),
4222 columns: vec![
4223 ColumnDef {
4224 name: "id".into(),
4225 type_id: TypeId::Int,
4226 required: true,
4227 position: 0,
4228 },
4229 ColumnDef {
4230 name: "status".into(),
4231 type_id: TypeId::Str,
4232 required: false,
4233 position: 1,
4234 },
4235 ],
4236 }
4237 }
4238
4239 #[test]
4240 fn replay_records_treats_reused_tx_ids_as_ordered_spans() {
4241 let mut cat = temp_catalog("reused_tx_ids");
4242 let schema = schema_two_cols();
4243 cat.create_table(schema.clone()).unwrap();
4244 cat.checkpoint().unwrap();
4245
4246 let mut committed_row = Vec::new();
4247 encode_row_into(
4248 &schema,
4249 &[Value::Int(1), Value::Str("committed".into())],
4250 &mut committed_row,
4251 );
4252 let mut incomplete_row = Vec::new();
4253 encode_row_into(
4254 &schema,
4255 &[Value::Int(2), Value::Str("incomplete".into())],
4256 &mut incomplete_row,
4257 );
4258
4259 let records = vec![
4260 WalRecord {
4261 tx_id: 1,
4262 record_type: WalRecordType::Begin,
4263 lsn: 1,
4264 data: Vec::new(),
4265 },
4266 WalRecord {
4267 tx_id: 1,
4268 record_type: WalRecordType::Insert,
4269 lsn: 2,
4270 data: encode_wal_payload(
4271 "T",
4272 RowId {
4273 page_id: 1,
4274 slot_index: 0,
4275 },
4276 &committed_row,
4277 ),
4278 },
4279 WalRecord {
4280 tx_id: 1,
4281 record_type: WalRecordType::Commit,
4282 lsn: 3,
4283 data: Vec::new(),
4284 },
4285 WalRecord {
4286 tx_id: 1,
4287 record_type: WalRecordType::Begin,
4288 lsn: 4,
4289 data: Vec::new(),
4290 },
4291 WalRecord {
4292 tx_id: 1,
4293 record_type: WalRecordType::Insert,
4294 lsn: 5,
4295 data: encode_wal_payload(
4296 "T",
4297 RowId {
4298 page_id: 1,
4299 slot_index: 1,
4300 },
4301 &incomplete_row,
4302 ),
4303 },
4304 ];
4305
4306 cat.apply_wal_records(&records).unwrap();
4307 let rows: Vec<_> = cat.scan("T").unwrap().collect();
4308 assert_eq!(rows.len(), 1);
4309 assert_eq!(rows[0].1[0], Value::Int(1));
4310 assert_eq!(rows[0].1[1], Value::Str("committed".into()));
4311 }
4312
4313 #[test]
4314 fn ddl_create_table_codec_roundtrips_defaults_and_auto() {
4315 let schema = schema_two_cols();
4316 let defaults = vec![None, Some(Value::Str("active".into()))];
4317 let auto_cols = vec![true, false];
4318 let encoded = encode_ddl_create_table(&schema, &defaults, &auto_cols);
4319 let (decoded_schema, decoded_defaults, decoded_auto) =
4320 decode_ddl_create_table(&encoded).unwrap();
4321 assert_eq!(decoded_schema.columns.len(), 2);
4322 assert_eq!(decoded_defaults, defaults);
4323 assert_eq!(decoded_auto, auto_cols);
4324 }
4325
4326 #[test]
4327 fn ddl_create_table_codec_back_compat_without_trailing_sections() {
4328 let schema = schema_two_cols();
4332 let full = encode_ddl_create_table(&schema, &[], &[]);
4333 let legacy = &full[..full.len() - 4];
4336 let (decoded_schema, decoded_defaults, decoded_auto) =
4337 decode_ddl_create_table(legacy).unwrap();
4338 assert_eq!(decoded_schema.columns.len(), 2);
4339 assert!(decoded_defaults.is_empty(), "no defaults section -> empty");
4340 assert!(decoded_auto.is_empty(), "no auto section -> empty");
4341 }
4342
4343 #[test]
4344 fn ddl_create_table_codec_back_compat_defaults_but_no_auto() {
4345 let schema = schema_two_cols();
4348 let defaults = vec![None, Some(Value::Str("active".into()))];
4349 let full = encode_ddl_create_table(&schema, &defaults, &[]);
4350 let legacy = &full[..full.len() - 2];
4352 let (_schema, decoded_defaults, decoded_auto) = decode_ddl_create_table(legacy).unwrap();
4353 assert_eq!(decoded_defaults, defaults);
4354 assert!(decoded_auto.is_empty());
4355 }
4356
4357 #[test]
4358 fn read_catalog_file_accepts_intermediate_versions_3_and_4() {
4359 use std::io::Write as _;
4367 fn write_legacy_catalog(path: &std::path::Path, version: u16) {
4368 let mut buf: Vec<u8> = Vec::new();
4369 buf.extend_from_slice(CATALOG_MAGIC);
4370 buf.extend_from_slice(&version.to_le_bytes());
4371 buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes());
4374 buf.extend_from_slice(b"T");
4375 buf.extend_from_slice(&2u16.to_le_bytes()); buf.extend_from_slice(&2u32.to_le_bytes());
4378 buf.extend_from_slice(b"id");
4379 buf.push(TypeId::Int as u8);
4380 buf.push(1);
4381 buf.extend_from_slice(&0u16.to_le_bytes());
4382 buf.extend_from_slice(&6u32.to_le_bytes());
4384 buf.extend_from_slice(b"status");
4385 buf.push(TypeId::Str as u8);
4386 buf.push(0);
4387 buf.extend_from_slice(&1u16.to_le_bytes());
4388 buf.extend_from_slice(&0u16.to_le_bytes());
4390 if version >= 4 {
4392 encode_defaults_section(&mut buf, &[None, None]);
4393 }
4394 let crc = crc32fast::hash(&buf);
4396 buf.extend_from_slice(&crc.to_le_bytes());
4397 let mut f = fs::File::create(path).unwrap();
4398 f.write_all(&buf).unwrap();
4399 }
4400
4401 for version in [3u16, 4u16] {
4402 let path = std::env::temp_dir().join(format!(
4403 "powdb_cat_v{version}_compat_{}.bin",
4404 std::process::id()
4405 ));
4406 write_legacy_catalog(&path, version);
4407 let catalog_file = read_catalog_file(&path)
4408 .unwrap_or_else(|e| panic!("version {version} catalog must load, got: {e}"));
4409 let entries = catalog_file.entries;
4410 assert_eq!(entries.len(), 1);
4411 assert_eq!(entries[0].schema.table_name, "T");
4412 assert_eq!(entries[0].schema.columns.len(), 2);
4413 assert!(
4414 entries[0].auto_cols.is_empty(),
4415 "v{version} has no auto cols"
4416 );
4417 fs::remove_file(&path).ok();
4418 }
4419 }
4420
4421 #[test]
4422 fn read_catalog_file_rejects_implausible_table_count() {
4423 use std::io::Write as _;
4431 let mut buf: Vec<u8> = Vec::new();
4432 buf.extend_from_slice(CATALOG_MAGIC);
4433 buf.extend_from_slice(&CATALOG_VERSION.to_le_bytes());
4434 buf.extend_from_slice(&1000u32.to_le_bytes()); buf.extend_from_slice(&1u64.to_le_bytes()); let crc = crc32fast::hash(&buf);
4438 buf.extend_from_slice(&crc.to_le_bytes());
4439 let path =
4440 std::env::temp_dir().join(format!("powdb_cat_badcount_{}.bin", std::process::id()));
4441 fs::File::create(&path).unwrap().write_all(&buf).unwrap();
4442
4443 let msg = match read_catalog_file(&path) {
4444 Ok(_) => panic!("implausible table count must be rejected, got Ok"),
4445 Err(e) => e.to_string(),
4446 };
4447 assert!(
4448 msg.contains("implausible table count"),
4449 "expected an implausible-table-count error, got: {msg}"
4450 );
4451 fs::remove_file(&path).ok();
4452 }
4453
4454 #[test]
4455 fn data_dir_and_max_lsn_accessors() {
4456 let dir = std::env::temp_dir().join(format!("powdb_cat_maxlsn_{}", std::process::id()));
4457 let mut cat = Catalog::create(&dir).unwrap();
4458
4459 assert_eq!(cat.data_dir(), dir.as_path());
4461
4462 assert_eq!(cat.max_lsn(), 0);
4464
4465 let schema = Schema {
4466 table_name: "users".into(),
4467 columns: vec![ColumnDef {
4468 name: "name".into(),
4469 type_id: TypeId::Str,
4470 required: true,
4471 position: 0,
4472 }],
4473 };
4474 cat.create_table(schema).unwrap();
4475
4476 cat.insert("users", &vec![Value::Str("Alice".into())])
4477 .unwrap();
4478 cat.sync_wal().unwrap();
4479
4480 assert!(cat.max_lsn() > 0);
4483 }
4484
4485 #[test]
4486 fn test_create_table_and_insert() {
4487 let mut cat = temp_catalog("basic");
4488 let schema = Schema {
4489 table_name: "users".into(),
4490 columns: vec![
4491 ColumnDef {
4492 name: "name".into(),
4493 type_id: TypeId::Str,
4494 required: true,
4495 position: 0,
4496 },
4497 ColumnDef {
4498 name: "age".into(),
4499 type_id: TypeId::Int,
4500 required: false,
4501 position: 1,
4502 },
4503 ],
4504 };
4505 cat.create_table(schema).unwrap();
4506
4507 let row = vec![Value::Str("Alice".into()), Value::Int(30)];
4508 let rid = cat.insert("users", &row).unwrap();
4509
4510 let result = cat.get("users", rid).unwrap();
4511 assert_eq!(result[0], Value::Str("Alice".into()));
4512 assert_eq!(result[1], Value::Int(30));
4513 }
4514
4515 #[test]
4516 fn test_scan_table() {
4517 let mut cat = temp_catalog("scan");
4518 let schema = Schema {
4519 table_name: "items".into(),
4520 columns: vec![
4521 ColumnDef {
4522 name: "name".into(),
4523 type_id: TypeId::Str,
4524 required: true,
4525 position: 0,
4526 },
4527 ColumnDef {
4528 name: "price".into(),
4529 type_id: TypeId::Float,
4530 required: true,
4531 position: 1,
4532 },
4533 ],
4534 };
4535 cat.create_table(schema).unwrap();
4536
4537 for i in 0..50 {
4538 cat.insert(
4539 "items",
4540 &vec![
4541 Value::Str(format!("item_{i}")),
4542 Value::Float(i as f64 * 1.5),
4543 ],
4544 )
4545 .unwrap();
4546 }
4547
4548 let rows: Vec<_> = cat.scan("items").unwrap().collect();
4549 assert_eq!(rows.len(), 50);
4550 }
4551
4552 #[test]
4553 fn test_index_lookup() {
4554 let mut cat = temp_catalog("idx");
4555 let schema = Schema {
4556 table_name: "users".into(),
4557 columns: vec![
4558 ColumnDef {
4559 name: "email".into(),
4560 type_id: TypeId::Str,
4561 required: true,
4562 position: 0,
4563 },
4564 ColumnDef {
4565 name: "name".into(),
4566 type_id: TypeId::Str,
4567 required: true,
4568 position: 1,
4569 },
4570 ],
4571 };
4572 cat.create_table(schema).unwrap();
4573 cat.create_index("users", "email").unwrap();
4574
4575 cat.insert(
4576 "users",
4577 &vec![
4578 Value::Str("alice@example.com".into()),
4579 Value::Str("Alice".into()),
4580 ],
4581 )
4582 .unwrap();
4583 cat.insert(
4584 "users",
4585 &vec![
4586 Value::Str("bob@example.com".into()),
4587 Value::Str("Bob".into()),
4588 ],
4589 )
4590 .unwrap();
4591
4592 let result = cat
4593 .index_lookup("users", "email", &Value::Str("bob@example.com".into()))
4594 .unwrap();
4595 assert!(result.is_some());
4596 let row = result.unwrap();
4597 assert_eq!(row[1], Value::Str("Bob".into()));
4598 }
4599
4600 #[test]
4601 fn test_delete_row() {
4602 let mut cat = temp_catalog("delete");
4603 let schema = Schema {
4604 table_name: "t".into(),
4605 columns: vec![ColumnDef {
4606 name: "v".into(),
4607 type_id: TypeId::Int,
4608 required: true,
4609 position: 0,
4610 }],
4611 };
4612 cat.create_table(schema).unwrap();
4613 let r1 = cat.insert("t", &vec![Value::Int(1)]).unwrap();
4614 let r2 = cat.insert("t", &vec![Value::Int(2)]).unwrap();
4615 cat.delete("t", r1).unwrap();
4616 assert!(cat.get("t", r1).is_none());
4617 assert!(cat.get("t", r2).is_some());
4618 }
4619
4620 #[test]
4621 fn test_update_row() {
4622 let mut cat = temp_catalog("update");
4623 let schema = Schema {
4624 table_name: "t".into(),
4625 columns: vec![ColumnDef {
4626 name: "v".into(),
4627 type_id: TypeId::Int,
4628 required: true,
4629 position: 0,
4630 }],
4631 };
4632 cat.create_table(schema).unwrap();
4633 let rid = cat.insert("t", &vec![Value::Int(1)]).unwrap();
4634 let new_rid = cat.update("t", rid, &vec![Value::Int(99)]).unwrap();
4635 let row = cat.get("t", new_rid).unwrap();
4636 assert_eq!(row[0], Value::Int(99));
4637 }
4638
4639 #[test]
4640 fn test_persist_and_reopen() {
4641 let dir = std::env::temp_dir().join(format!("powdb_cat_persist_{}", std::process::id()));
4642 let _ = std::fs::remove_dir_all(&dir);
4644
4645 {
4646 let mut cat = Catalog::create(&dir).unwrap();
4647 cat.create_table(Schema {
4648 table_name: "users".into(),
4649 columns: vec![
4650 ColumnDef {
4651 name: "name".into(),
4652 type_id: TypeId::Str,
4653 required: true,
4654 position: 0,
4655 },
4656 ColumnDef {
4657 name: "age".into(),
4658 type_id: TypeId::Int,
4659 required: false,
4660 position: 1,
4661 },
4662 ],
4663 })
4664 .unwrap();
4665 cat.insert("users", &vec![Value::Str("Alice".into()), Value::Int(30)])
4666 .unwrap();
4667 cat.insert("users", &vec![Value::Str("Bob".into()), Value::Int(25)])
4668 .unwrap();
4669 }
4670
4671 let cat = Catalog::open(&dir).unwrap();
4673 let schema = cat.schema("users").unwrap();
4674 assert_eq!(schema.columns.len(), 2);
4675 assert_eq!(schema.columns[0].name, "name");
4676 assert_eq!(schema.columns[0].type_id, TypeId::Str);
4677 assert_eq!(schema.columns[1].type_id, TypeId::Int);
4678
4679 let rows: Vec<_> = cat.scan("users").unwrap().collect();
4680 assert_eq!(rows.len(), 2);
4681
4682 std::fs::remove_dir_all(&dir).ok();
4683 }
4684
4685 #[test]
4686 fn test_open_missing_dir_errors() {
4687 let dir = std::env::temp_dir().join(format!("powdb_cat_missing_{}", std::process::id()));
4688 let _ = std::fs::remove_dir_all(&dir);
4689 std::fs::create_dir_all(&dir).unwrap();
4690 assert!(Catalog::open(&dir).is_err());
4692 std::fs::remove_dir_all(&dir).ok();
4693 }
4694
4695 #[test]
4696 fn test_list_tables() {
4697 let mut cat = temp_catalog("list");
4698 cat.create_table(Schema {
4699 table_name: "a".into(),
4700 columns: vec![ColumnDef {
4701 name: "x".into(),
4702 type_id: TypeId::Int,
4703 required: true,
4704 position: 0,
4705 }],
4706 })
4707 .unwrap();
4708 cat.create_table(Schema {
4709 table_name: "b".into(),
4710 columns: vec![ColumnDef {
4711 name: "y".into(),
4712 type_id: TypeId::Int,
4713 required: true,
4714 position: 0,
4715 }],
4716 })
4717 .unwrap();
4718 let mut tables = cat.list_tables();
4719 tables.sort();
4720 assert_eq!(tables, vec!["a", "b"]);
4721 }
4722
4723 #[test]
4724 fn test_path_traversal_table_name_rejected() {
4725 let mut cat = temp_catalog("path_trav");
4726 let bad_names = vec![
4728 "../etc/passwd",
4729 "foo/bar",
4730 "table\0name",
4731 "",
4732 "123starts_with_digit",
4733 "has-dashes",
4734 "has spaces",
4735 "has.dots",
4736 ];
4737 for name in bad_names {
4738 let schema = Schema {
4739 table_name: name.into(),
4740 columns: vec![ColumnDef {
4741 name: "x".into(),
4742 type_id: TypeId::Int,
4743 required: true,
4744 position: 0,
4745 }],
4746 };
4747 let result = cat.create_table(schema);
4748 assert!(result.is_err(), "expected error for table name '{name}'");
4749 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
4750 }
4751 let good_names = vec!["users", "_private", "Table_123", "_"];
4753 for name in good_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 assert!(
4764 cat.create_table(schema).is_ok(),
4765 "expected ok for table name '{name}'"
4766 );
4767 }
4768 }
4769
4770 #[test]
4771 fn test_path_traversal_column_name_rejected() {
4772 let mut cat = temp_catalog("col_path_trav");
4773 let schema = Schema {
4774 table_name: "valid_table".into(),
4775 columns: vec![ColumnDef {
4776 name: "../bad".into(),
4777 type_id: TypeId::Int,
4778 required: true,
4779 position: 0,
4780 }],
4781 };
4782 let result = cat.create_table(schema);
4783 assert!(result.is_err());
4784 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
4785 }
4786
4787 #[test]
4788 fn test_drop_table_validates_name() {
4789 let mut cat = temp_catalog("drop_trav");
4790 let result = cat.drop_table("../etc/passwd");
4791 assert!(result.is_err());
4792 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
4794 }
4795}