1use crate::btree::{BTree, IndexStats};
2use crate::error::StorageError;
3use crate::heap::{DirtyPageBudget, HeapFile};
4use crate::page::{UpdateFit, 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 std::sync::Arc;
16use tracing::{info, warn};
17
18static NEXT_STRUCTURE_GENERATION: AtomicU64 = AtomicU64::new(1);
19
20fn next_structure_generation() -> u64 {
21 NEXT_STRUCTURE_GENERATION.fetch_add(1, Ordering::Relaxed)
22}
23
24fn check_encoded_row_size(encoded: &[u8]) -> io::Result<()> {
29 if encoded.len() > crate::page::MAX_ROW_DATA_SIZE {
30 return Err(crate::error::StorageError::RowTooLarge {
31 size: encoded.len(),
32 max: crate::page::MAX_ROW_DATA_SIZE,
33 }
34 .into());
35 }
36 Ok(())
37}
38
39fn validate_identifier(kind: &str, name: &str) -> io::Result<()> {
43 if name.is_empty() {
44 return Err(io::Error::new(
45 io::ErrorKind::InvalidInput,
46 format!("invalid {kind} name: must not be empty"),
47 ));
48 }
49 let mut chars = name.chars();
50 let first = chars.next().expect("non-empty name");
52 if !first.is_ascii_alphabetic() && first != '_' {
53 return Err(io::Error::new(
54 io::ErrorKind::InvalidInput,
55 format!("invalid {kind} name '{name}': must start with a letter or underscore"),
56 ));
57 }
58 for ch in chars {
59 if !ch.is_ascii_alphanumeric() && ch != '_' {
60 return Err(io::Error::new(
61 io::ErrorKind::InvalidInput,
62 format!(
63 "invalid {kind} name '{name}': must contain only letters, digits, and underscores"
64 ),
65 ));
66 }
67 }
68 Ok(())
69}
70
71fn validate_table_name(name: &str) -> io::Result<()> {
73 validate_identifier("table", name)
74}
75
76fn validate_column_name(name: &str) -> io::Result<()> {
78 validate_identifier("column", name)
79}
80
81const CATALOG_FILE: &str = "catalog.bin";
90pub const CATALOG_LSN_FILE: &str = "catalog.lsn";
91const CATALOG_MAGIC: &[u8; 4] = b"BCAT";
92pub const LEGACY_CATALOG_VERSION: u16 = 5;
96pub const EXPRESSION_INDEX_CATALOG_VERSION: u16 = 6;
100pub const CATALOG_VERSION: u16 = 7;
106
107#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct ExpressionIndexMeta {
112 pub index_id: u64,
113 pub unique: bool,
114 pub canonical_version: u16,
115 pub canonical_text: String,
116 pub json_path: StoredJsonPathV1,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum LinkKind {
130 ToOne,
133 ToMany,
136}
137
138impl LinkKind {
139 fn to_u8(self) -> u8 {
140 match self {
141 LinkKind::ToOne => 0,
142 LinkKind::ToMany => 1,
143 }
144 }
145
146 fn from_u8(tag: u8) -> io::Result<Self> {
147 match tag {
148 0 => Ok(LinkKind::ToOne),
149 1 => Ok(LinkKind::ToMany),
150 other => Err(io::Error::new(
151 io::ErrorKind::InvalidData,
152 format!("unknown link kind tag: {other}"),
153 )),
154 }
155 }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct LinkDef {
165 pub owner_type: String,
167 pub name: String,
169 pub target_type: String,
171 pub local_key: String,
173 pub target_key: String,
175 pub kind: LinkKind,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
188pub enum IndexKeySource {
189 Column {
190 column: String,
191 },
192 Expression {
193 index_id: u64,
194 canonical_version: u16,
195 canonical_text: String,
196 json_path: StoredJsonPathV1,
197 },
198}
199
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct IndexMetadata {
202 pub unique: bool,
203 pub source: IndexKeySource,
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum IndexOrderDirection {
208 Asc,
209 Desc,
210}
211
212pub fn expression_index_file_name(table: &str, index_id: u64) -> String {
217 format!("{table}_{index_id}.eidx")
218}
219
220const WAL_FILE: &str = "wal.log";
223const SYNC_STATE_DIR: &str = ".powdb-sync";
224const SYNC_IDENTITY_FILE: &str = "identity.json";
225
226const WAL_BATCH_SIZE: usize = 64;
230type WalArchiveCallback<'a> = &'a mut dyn FnMut(&Path, &[WalRecord]) -> io::Result<()>;
231
232fn read_durable_lsn(data_dir: &Path) -> io::Result<u64> {
233 let path = data_dir.join(CATALOG_LSN_FILE);
234 let bytes = match fs::read(path) {
235 Ok(bytes) => bytes,
236 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(0),
237 Err(err) => return Err(err),
238 };
239 if bytes.len() != 8 {
240 return Err(io::Error::new(
241 io::ErrorKind::InvalidData,
242 "catalog LSN sidecar has invalid length",
243 ));
244 }
245 let mut buf = [0u8; 8];
246 buf.copy_from_slice(&bytes);
247 Ok(u64::from_le_bytes(buf))
248}
249
250fn write_durable_lsn(data_dir: &Path, lsn: u64) -> io::Result<()> {
251 let path = data_dir.join(CATALOG_LSN_FILE);
252 let tmp_path = data_dir.join(format!("{CATALOG_LSN_FILE}.tmp"));
253 let mut file = fs::File::create(&tmp_path)?;
254 file.write_all(&lsn.to_le_bytes())?;
255 file.sync_all()?;
256 drop(file);
257 fs::rename(&tmp_path, &path)?;
258 sync_directory(data_dir)?;
259 Ok(())
260}
261
262#[cfg(unix)]
263fn sync_directory(path: &Path) -> io::Result<()> {
264 fs::File::open(path)?.sync_all()
265}
266
267#[cfg(not(unix))]
268fn sync_directory(path: &Path) -> io::Result<()> {
269 let _ = path;
270 Ok(())
271}
272
273#[cfg(test)]
274thread_local! {
275 static CATALOG_PERSIST_FAILPOINT: std::cell::Cell<u8> = const { std::cell::Cell::new(0) };
276}
277
278#[cfg(test)]
279fn take_catalog_persist_failpoint(stage: u8) -> bool {
280 CATALOG_PERSIST_FAILPOINT.with(|failpoint| {
281 if failpoint.get() == stage {
282 failpoint.set(0);
283 true
284 } else {
285 false
286 }
287 })
288}
289
290enum CatalogPersistError {
291 BeforeActivation(io::Error),
292 AfterActivation(io::Error),
293}
294
295impl CatalogPersistError {
296 fn into_io_error(self) -> io::Error {
297 match self {
298 Self::BeforeActivation(error) | Self::AfterActivation(error) => error,
299 }
300 }
301}
302
303fn max_record_lsn(records: &[WalRecord]) -> Option<u64> {
304 records.iter().map(|record| record.lsn).max()
305}
306
307pub struct Catalog {
319 tables: Vec<Table>,
321 name_to_slot: FxHashMap<String, usize>,
324 data_dir: PathBuf,
325 wal: Wal,
330 next_tx_id: u64,
334 active_tx_id: Option<u64>,
337 tx_start_len: Option<u64>,
340 pending_autocommit_tx_ids: Vec<u64>,
343 checkpointed: bool,
347 durable_lsn: u64,
350 pending_free_overflow: Vec<(usize, Vec<u32>)>,
359 active_catalog_version: u16,
362 next_index_id: u64,
364 links: Vec<LinkDef>,
370 structure_generation: u64,
375 read_only: bool,
380 dirty_budget: Arc<DirtyPageBudget>,
384}
385
386impl Catalog {
387 pub fn create(data_dir: &Path) -> io::Result<Self> {
408 crate::create_data_dir_secure(data_dir)?;
409 let wal_path = data_dir.join(WAL_FILE);
410 let wal = Wal::create(&wal_path, WAL_BATCH_SIZE)?;
411 let cat = Catalog {
412 tables: Vec::new(),
413 name_to_slot: FxHashMap::default(),
414 data_dir: data_dir.to_path_buf(),
415 wal,
416 next_tx_id: 1,
417 active_tx_id: None,
418 tx_start_len: None,
419 pending_autocommit_tx_ids: Vec::new(),
420 pending_free_overflow: Vec::new(),
421 checkpointed: false,
422 durable_lsn: 0,
423 active_catalog_version: LEGACY_CATALOG_VERSION,
424 next_index_id: 1,
425 links: Vec::new(),
426 structure_generation: next_structure_generation(),
427 read_only: false,
428 dirty_budget: Arc::new(DirtyPageBudget::default()),
429 };
430 cat.persist()?;
431 Ok(cat)
432 }
433
434 pub fn open(data_dir: &Path) -> io::Result<Self> {
443 Self::open_inner(data_dir, None)
444 }
445
446 pub fn open_with_wal_archive<F>(data_dir: &Path, mut archive: F) -> io::Result<Self>
455 where
456 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
457 {
458 let archive: WalArchiveCallback<'_> = &mut archive;
459 Self::open_inner(data_dir, Some(archive))
460 }
461
462 fn open_inner(data_dir: &Path, archive: Option<WalArchiveCallback<'_>>) -> io::Result<Self> {
463 let cat_path = data_dir.join(CATALOG_FILE);
464 if !cat_path.exists() {
465 return Err(io::Error::new(io::ErrorKind::NotFound, "no catalog file"));
466 }
467 let catalog_file = read_catalog_file(&cat_path)?;
468 let active_catalog_version = catalog_file.version;
469 let next_index_id = catalog_file.next_index_id;
470 let links = catalog_file.links;
471 let entries = catalog_file.entries;
472 let durable_lsn = read_durable_lsn(data_dir)?;
473 let mut tables: Vec<Table> = Vec::with_capacity(entries.len());
474 let mut name_to_slot =
475 FxHashMap::with_capacity_and_hasher(entries.len(), Default::default());
476 let dirty_budget = Arc::new(DirtyPageBudget::default());
477 for CatalogEntry {
478 schema,
479 indexed_cols,
480 expression_indexes: expression_metas,
481 defaults,
482 auto_cols,
483 } in entries
484 {
485 let name = schema.table_name.clone();
486 let mut table =
492 Table::open_with_indexes(schema, data_dir, &indexed_cols, &expression_metas)?;
493 table.heap.set_dirty_budget(Arc::clone(&dirty_budget));
494 table.set_defaults(defaults);
495 table.set_auto_cols(auto_cols);
496 name_to_slot.insert(name.clone(), tables.len());
497 tables.push(table);
498 }
499 let wal_path = data_dir.join(WAL_FILE);
500 let wal = Wal::open(&wal_path, WAL_BATCH_SIZE)?;
501 let mut cat = Catalog {
502 tables,
503 name_to_slot,
504 data_dir: data_dir.to_path_buf(),
505 wal,
506 next_tx_id: 1,
507 active_tx_id: None,
508 tx_start_len: None,
509 pending_autocommit_tx_ids: Vec::new(),
510 pending_free_overflow: Vec::new(),
511 checkpointed: false,
512 durable_lsn,
513 active_catalog_version,
514 next_index_id,
515 links,
516 structure_generation: next_structure_generation(),
517 read_only: false,
518 dirty_budget,
519 };
520 cat.replay_wal(archive)?;
521 let max_page_lsn = cat
531 .tables
532 .iter()
533 .map(|t| t.heap.max_page_lsn())
534 .max()
535 .unwrap_or(0);
536 let max_known_lsn = max_page_lsn.max(cat.durable_lsn);
537 cat.wal.set_next_lsn_at_least(max_known_lsn + 1);
538 if let Err(e) = cat.sweep_all() {
543 warn!(error = %e, "post-recovery overflow sweep failed (non-fatal)");
544 }
545 Ok(cat)
546 }
547
548 pub fn open_read_only(data_dir: &Path) -> io::Result<Self> {
562 crate::validate_data_dir_read_only(data_dir)?;
563 let cat_path = data_dir.join(CATALOG_FILE);
564 if !cat_path.exists() {
565 return Err(io::Error::new(io::ErrorKind::NotFound, "no catalog file"));
566 }
567
568 let wal_path = data_dir.join(WAL_FILE);
572 if crate::wal::wal_has_committed_records(&wal_path)? {
573 return Err(io::Error::new(
574 io::ErrorKind::InvalidData,
575 "cannot open read-only: the WAL is not empty (the directory has \
576 un-checkpointed writes). Open the directory once with a read-write \
577 engine to recover, or restore from a backup, then serve it read-only",
578 ));
579 }
580
581 let catalog_file = read_catalog_file(&cat_path)?;
582 let active_catalog_version = catalog_file.version;
583 let next_index_id = catalog_file.next_index_id;
584 let links = catalog_file.links;
585 let entries = catalog_file.entries;
586 let durable_lsn = read_durable_lsn(data_dir)?;
587 let mut tables: Vec<Table> = Vec::with_capacity(entries.len());
588 let mut name_to_slot =
589 FxHashMap::with_capacity_and_hasher(entries.len(), Default::default());
590 let dirty_budget = Arc::new(DirtyPageBudget::default());
591 for CatalogEntry {
592 schema,
593 indexed_cols,
594 expression_indexes: expression_metas,
595 defaults,
596 auto_cols,
597 } in entries
598 {
599 let name = schema.table_name.clone();
600 let mut table = Table::open_with_indexes_read_only(
601 schema,
602 data_dir,
603 &indexed_cols,
604 &expression_metas,
605 )?;
606 table.heap.set_dirty_budget(Arc::clone(&dirty_budget));
607 table.set_defaults(defaults);
608 table.set_auto_cols(auto_cols);
609 name_to_slot.insert(name.clone(), tables.len());
610 tables.push(table);
611 }
612 let wal = Wal::open_read_only(&wal_path, WAL_BATCH_SIZE)?;
613 Ok(Catalog {
614 tables,
615 name_to_slot,
616 data_dir: data_dir.to_path_buf(),
617 wal,
618 next_tx_id: 1,
619 active_tx_id: None,
620 tx_start_len: None,
621 pending_autocommit_tx_ids: Vec::new(),
622 pending_free_overflow: Vec::new(),
623 checkpointed: false,
624 durable_lsn,
625 active_catalog_version,
626 next_index_id,
627 links,
628 structure_generation: next_structure_generation(),
629 read_only: true,
630 dirty_budget,
631 })
632 }
633
634 fn replay_wal(&mut self, mut archive: Option<WalArchiveCallback<'_>>) -> io::Result<()> {
660 let records = self.wal.read_all()?;
661 if records.is_empty() {
662 return Ok(());
663 }
664 if archive.is_none() {
665 self.ensure_plain_wal_truncate_allowed(&records)?;
666 }
667 self.replay_records(&records)?;
668 if let Some(archive) = archive.as_mut() {
669 archive(&self.data_dir, &records)?;
670 }
671 self.wal.truncate()?;
672 Ok(())
673 }
674
675 pub fn apply_wal_records(&mut self, records: &[WalRecord]) -> io::Result<()> {
684 self.ensure_no_active_transaction_for_checkpoint()?;
685 self.ensure_no_pending_wal_records()?;
686 self.replay_records(records)
687 }
688
689 pub fn ensure_no_pending_wal_records(&self) -> io::Result<()> {
692 if self.wal.has_pending() || !self.wal.read_all()?.is_empty() {
693 return Err(io::Error::other(
694 "cannot apply replicated WAL records while local WAL records are pending",
695 ));
696 }
697 Ok(())
698 }
699
700 fn replay_records(&mut self, records: &[WalRecord]) -> io::Result<()> {
701 if records.is_empty() {
702 return Ok(());
703 }
704
705 info!(count = records.len(), "applying WAL records");
706
707 let has_boundaries = records.iter().any(|rec| {
716 matches!(
717 rec.record_type,
718 WalRecordType::Begin | WalRecordType::Commit | WalRecordType::Rollback
719 )
720 });
721 let mut committed_row_records = vec![true; records.len()];
722 if has_boundaries {
723 committed_row_records.fill(false);
724 let mut pending_tx_spans: Vec<(u64, Vec<usize>)> = Vec::new();
725 for (index, rec) in records.iter().enumerate() {
726 match rec.record_type {
727 WalRecordType::Insert
728 | WalRecordType::Update
729 | WalRecordType::Delete
730 | WalRecordType::OverflowWrite
731 | WalRecordType::OverflowFree
732 if rec.tx_id == 0 =>
733 {
734 committed_row_records[index] = true;
735 }
736 WalRecordType::Insert
737 | WalRecordType::Update
738 | WalRecordType::Delete
739 | WalRecordType::OverflowWrite
740 | WalRecordType::OverflowFree => {
741 if let Some((_, rows)) = pending_tx_spans
742 .iter_mut()
743 .rev()
744 .find(|(tx_id, _)| *tx_id == rec.tx_id)
745 {
746 rows.push(index);
747 } else {
748 pending_tx_spans.push((rec.tx_id, vec![index]));
749 }
750 }
751 WalRecordType::Begin if rec.tx_id != 0 => {
752 pending_tx_spans.push((rec.tx_id, Vec::new()));
753 }
754 WalRecordType::Commit if rec.tx_id != 0 => {
755 if let Some(span_index) = pending_tx_spans
756 .iter()
757 .rposition(|(tx_id, _)| *tx_id == rec.tx_id)
758 {
759 let (_, rows) = pending_tx_spans.remove(span_index);
760 for row_index in rows {
761 committed_row_records[row_index] = true;
762 }
763 }
764 }
765 WalRecordType::Rollback if rec.tx_id != 0 => {
766 if let Some(span_index) = pending_tx_spans
767 .iter()
768 .rposition(|(tx_id, _)| *tx_id == rec.tx_id)
769 {
770 pending_tx_spans.remove(span_index);
771 }
772 }
773 _ => {}
774 }
775 }
776 }
777
778 let mut replayed_inserts = 0usize;
779 let mut replayed_updates = 0usize;
780 let mut replayed_deletes = 0usize;
781 let mut skipped = 0usize;
782 let mut skipped_uncommitted = 0usize;
783 let mut saw_ddl = false;
784 for (index, rec) in records.iter().enumerate() {
785 if has_boundaries
786 && !committed_row_records[index]
787 && matches!(
788 rec.record_type,
789 WalRecordType::Insert
790 | WalRecordType::Update
791 | WalRecordType::Delete
792 | WalRecordType::OverflowWrite
793 | WalRecordType::OverflowFree
794 )
795 {
796 skipped_uncommitted += 1;
797 continue;
798 }
799 match rec.record_type {
800 WalRecordType::Insert => {
801 if let Some((table_name, rid, row_bytes)) = decode_wal_payload(&rec.data) {
802 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
803 let tbl = &mut self.tables[slot];
804 if rec.lsn > 0 && tbl.heap.page_lsn(rid.page_id) >= rec.lsn {
808 skipped += 1;
809 continue;
810 }
811 tbl.heap.insert_at(rid, &row_bytes)?;
818 tbl.heap.set_page_lsn(rid.page_id, rec.lsn)?;
819 replayed_inserts += 1;
820 }
821 }
822 }
823 WalRecordType::Update => {
824 if let Some((table_name, rid, row_bytes)) = decode_wal_payload(&rec.data) {
825 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
826 let tbl = &mut self.tables[slot];
827 if rec.lsn > 0 && tbl.heap.page_lsn(rid.page_id) >= rec.lsn {
828 skipped += 1;
829 continue;
830 }
831 let new_rid = tbl.heap.update(rid, &row_bytes)?;
832 if new_rid != rid {
844 tbl.heap.set_page_lsn(rid.page_id, rec.lsn)?;
845 }
846 tbl.heap.set_page_lsn(new_rid.page_id, rec.lsn)?;
847 replayed_updates += 1;
848 }
849 }
850 }
851 WalRecordType::Delete => {
852 if let Some((table_name, rid, _)) = decode_wal_payload(&rec.data) {
853 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
854 let tbl = &mut self.tables[slot];
855 if rec.lsn > 0 && tbl.heap.page_lsn(rid.page_id) >= rec.lsn {
856 skipped += 1;
857 continue;
858 }
859 let _ = tbl.heap.delete(rid);
860 tbl.heap.set_page_lsn(rid.page_id, rec.lsn)?;
861 replayed_deletes += 1;
862 }
863 }
864 }
865 WalRecordType::OverflowWrite => {
866 if let Some((table_name, page_id, next_page, chunk)) =
871 decode_overflow_write_payload(&rec.data)
872 {
873 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
874 let tbl = &mut self.tables[slot];
875 if rec.lsn > 0 && tbl.heap.overflow_page_lsn(page_id) >= rec.lsn {
876 skipped += 1;
877 continue;
878 }
879 tbl.heap
880 .write_overflow_page(page_id, next_page, &chunk, rec.lsn)?;
881 }
882 }
883 }
884 WalRecordType::OverflowFree => {
885 if let Some((table_name, pages)) = decode_overflow_free_payload(&rec.data) {
890 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
891 self.tables[slot].heap.release_overflow_pages(&pages);
892 }
893 }
894 }
895 WalRecordType::Begin | WalRecordType::Commit | WalRecordType::Rollback => {
896 }
898 WalRecordType::DdlCreateTable => {
899 saw_ddl = true;
900 if let Some((schema, defaults, auto_cols)) = decode_ddl_create_table(&rec.data)
901 {
902 if !self.name_to_slot.contains_key(&schema.table_name) {
903 if let Ok(mut table) = Table::create(schema, &self.data_dir) {
904 table.heap.set_dirty_budget(Arc::clone(&self.dirty_budget));
905 table.set_defaults(defaults);
906 table.set_auto_cols(auto_cols);
907 let slot = self.tables.len();
908 let name = table.schema.table_name.clone();
909 self.tables.push(table);
910 self.name_to_slot.insert(name, slot);
911 }
912 }
913 }
914 }
915 WalRecordType::DdlDropTable => {
916 saw_ddl = true;
917 if let Some((table_name, _)) = decode_ddl_table_name(&rec.data) {
918 if let Some(&slot) = self.name_to_slot.get(&table_name) {
919 let heap_path = self.data_dir.join(format!("{table_name}.heap"));
920 if heap_path.exists() {
921 let _ = fs::remove_file(&heap_path);
922 }
923 for col_name in self.tables[slot].indexed_column_names() {
924 let idx_path =
925 self.data_dir.join(format!("{table_name}_{col_name}.idx"));
926 if idx_path.exists() {
927 let _ = fs::remove_file(&idx_path);
928 }
929 }
930 for index_id in self.tables[slot].expression_index_ids() {
931 let idx_path = self
932 .data_dir
933 .join(expression_index_file_name(&table_name, index_id));
934 let _ = fs::remove_file(idx_path);
935 }
936 self.name_to_slot.remove(&table_name);
937 let last = self.tables.len() - 1;
938 if slot != last {
939 let moved_name = self.tables[last].schema.table_name.clone();
940 self.tables.swap(slot, last);
941 self.name_to_slot.insert(moved_name, slot);
942 }
943 self.tables.pop();
944 }
945 }
946 }
947 WalRecordType::DdlAddColumn => {
948 saw_ddl = true;
949 if let Some((table_name, col)) = decode_ddl_alter_add_column(&rec.data) {
950 if let Some(&slot) = self.name_to_slot.get(&table_name) {
951 let tbl = &mut self.tables[slot];
952 if !tbl.schema.columns.iter().any(|c| c.name == col.name) {
953 let old_schema = tbl.schema.clone();
954 let has_rows = tbl.heap.scan().next().is_some();
955 tbl.schema.columns.push(col);
956 tbl.refresh_layout();
957 if has_rows {
958 let fill = vec![Value::Empty; tbl.schema.columns.len()];
959 let data_dir = self.data_dir.clone();
960 let _ = tbl.rewrite_rows_for_schema_change(
961 &old_schema,
962 &fill,
963 &data_dir,
964 );
965 }
966 }
967 if rec.lsn > 0 {
974 let _ = tbl.heap.stamp_all_pages_min_lsn(rec.lsn);
975 }
976 }
977 }
978 }
979 WalRecordType::DdlDropColumn => {
980 saw_ddl = true;
981 if let Some((table_name, col_name)) = decode_ddl_alter_drop_column(&rec.data) {
982 if let Some(&slot) = self.name_to_slot.get(&table_name) {
983 {
984 let tbl = &mut self.tables[slot];
985 if let Some(idx) =
986 tbl.schema.columns.iter().position(|c| c.name == col_name)
987 {
988 let old_schema = tbl.schema.clone();
989 let has_rows = tbl.heap.scan().next().is_some();
990 tbl.schema.columns.remove(idx);
991 for (i, c) in tbl.schema.columns.iter_mut().enumerate() {
992 c.position = i as u16;
993 }
994 tbl.refresh_layout();
995 if has_rows {
996 let fill = vec![Value::Empty; tbl.schema.columns.len()];
997 let data_dir = self.data_dir.clone();
998 let _ = tbl.rewrite_rows_for_schema_change(
999 &old_schema,
1000 &fill,
1001 &data_dir,
1002 );
1003 }
1004 }
1005 if rec.lsn > 0 {
1006 let _ = tbl.heap.stamp_all_pages_min_lsn(rec.lsn);
1007 }
1008 }
1009
1010 let removed_ids =
1011 self.tables[slot].remove_expression_indexes_for_root(&col_name);
1012 for index_id in removed_ids {
1013 let idx_path = self
1014 .data_dir
1015 .join(expression_index_file_name(&table_name, index_id));
1016 let _ = fs::remove_file(idx_path);
1017 }
1018 }
1019 }
1020 }
1021 }
1022 }
1023 info!(
1024 inserts = replayed_inserts,
1025 updates = replayed_updates,
1026 deletes = replayed_deletes,
1027 skipped = skipped,
1028 skipped_uncommitted = skipped_uncommitted,
1029 "WAL record apply complete (commit-boundary + LSN idempotent)"
1030 );
1031 if saw_ddl {
1032 self.persist()?;
1033 }
1034 for tbl in &mut self.tables {
1048 tbl.heap.flush_all_dirty()?;
1049 tbl.heap.flush()?;
1050 tbl.rebuild_indexes_from_heap()?;
1051 tbl.save_dirty_indexes()?;
1056 }
1057 if let Some(max_lsn) = max_record_lsn(records) {
1058 self.record_durable_lsn_at_least(max_lsn)?;
1059 self.wal.set_next_lsn_at_least(max_lsn.saturating_add(1));
1060 }
1061 Ok(())
1062 }
1063
1064 pub fn checkpoint(&mut self) -> io::Result<()> {
1073 self.ensure_no_active_transaction_for_checkpoint()?;
1074 self.ensure_plain_checkpoint_allowed_before_flush()?;
1075 self.flush_checkpoint_state()?;
1076 self.wal.flush()?;
1077 self.record_durable_lsn_at_least(self.wal.last_appended_lsn())?;
1078 self.wal.truncate()?;
1079 self.checkpointed = true;
1080 Ok(())
1081 }
1082
1083 pub fn checkpoint_with_wal_archive<F>(&mut self, mut archive: F) -> io::Result<()>
1091 where
1092 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
1093 {
1094 self.ensure_no_active_transaction_for_checkpoint()?;
1095 self.commit_autocommit()?;
1096 self.flush_checkpoint_state()?;
1097 self.wal.flush()?;
1098 let records = self.wal.read_all()?;
1099 let archive: WalArchiveCallback<'_> = &mut archive;
1100 archive(&self.data_dir, &records)?;
1101 if let Some(max_lsn) = max_record_lsn(&records) {
1102 self.record_durable_lsn_at_least(max_lsn)?;
1103 } else {
1104 self.record_durable_lsn_at_least(self.wal.last_appended_lsn())?;
1105 }
1106 self.wal.truncate()?;
1107 self.checkpointed = true;
1108 Ok(())
1109 }
1110
1111 fn ensure_no_active_transaction_for_checkpoint(&self) -> io::Result<()> {
1112 if self.active_tx_id.is_some() {
1113 return Err(io::Error::other(
1114 "cannot checkpoint while an explicit transaction is active",
1115 ));
1116 }
1117 Ok(())
1118 }
1119
1120 fn ensure_no_active_transaction_for_ddl(&self, verb: &'static str) -> io::Result<()> {
1130 if self.active_tx_id.is_some() {
1131 return Err(io::Error::new(
1132 io::ErrorKind::InvalidInput,
1133 StorageError::DdlInTransaction { verb },
1134 ));
1135 }
1136 Ok(())
1137 }
1138
1139 fn flush_checkpoint_state(&mut self) -> io::Result<()> {
1140 for tbl in &mut self.tables {
1141 tbl.heap.flush_all_dirty()?;
1142 tbl.heap.flush()?;
1143 tbl.save_dirty_indexes()?;
1148 }
1149 Ok(())
1150 }
1151
1152 fn ensure_plain_checkpoint_allowed_before_flush(&self) -> io::Result<()> {
1153 if !self.sync_identity_file_exists() {
1154 return Ok(());
1155 }
1156 if self.wal.has_pending() {
1157 return Err(io::Error::other(
1158 "sync identity exists but checkpoint/recovery was called without a WAL archive hook; refusing to truncate retained history",
1159 ));
1160 }
1161 let records = self.wal.read_all()?;
1162 self.ensure_plain_wal_truncate_allowed(&records)
1163 }
1164
1165 fn ensure_plain_wal_truncate_allowed(&self, records: &[WalRecord]) -> io::Result<()> {
1166 if records.is_empty() {
1167 return Ok(());
1168 }
1169 if self.sync_identity_file_exists() {
1170 return Err(io::Error::other(
1171 "sync identity exists but checkpoint/recovery was called without a WAL archive hook; refusing to truncate retained history",
1172 ));
1173 }
1174 Ok(())
1175 }
1176
1177 fn sync_identity_file_exists(&self) -> bool {
1178 self.data_dir
1179 .join(SYNC_STATE_DIR)
1180 .join(SYNC_IDENTITY_FILE)
1181 .exists()
1182 }
1183
1184 fn record_durable_lsn_at_least(&mut self, lsn: u64) -> io::Result<()> {
1185 if lsn <= self.durable_lsn {
1186 return Ok(());
1187 }
1188 self.durable_lsn = lsn;
1189 write_durable_lsn(&self.data_dir, lsn)
1190 }
1191
1192 #[inline]
1194 fn free_overflow_chain(&mut self, slot: usize, pages: Vec<u32>) {
1205 if pages.is_empty() {
1206 return;
1207 }
1208 if self.active_tx_id.is_some() {
1209 self.pending_free_overflow.push((slot, pages));
1210 } else {
1211 self.tables[slot].release_overflow_pages(&pages);
1212 }
1213 }
1214
1215 fn next_tx(&mut self) -> u64 {
1216 if let Some(id) = self.active_tx_id {
1217 return id;
1218 }
1219 let id = self.next_tx_id;
1220 self.next_tx_id = self.next_tx_id.wrapping_add(1);
1221 id
1222 }
1223
1224 pub fn begin_transaction(&mut self) -> io::Result<()> {
1226 if self.active_tx_id.is_some() {
1227 return Err(io::Error::new(
1228 io::ErrorKind::InvalidInput,
1229 "explicit transaction is already active",
1230 ));
1231 }
1232 let start_len = self.wal.synced_len()?;
1233 let id = self.next_tx_id;
1234 self.next_tx_id = self.next_tx_id.wrapping_add(1);
1235 self.active_tx_id = Some(id);
1236 self.dirty_budget.set_rollback_pinned(true);
1240 self.tx_start_len = Some(start_len);
1241 self.pending_autocommit_tx_ids.clear();
1242 if !self.wal.is_off() {
1243 self.wal.append(id, WalRecordType::Begin, &[])?;
1244 self.wal.flush()?;
1245 }
1246 Ok(())
1247 }
1248
1249 pub fn commit_transaction(&mut self) -> io::Result<()> {
1252 self.dirty_budget.set_rollback_pinned(false);
1253 if let Some(id) = self.active_tx_id.take() {
1254 if !self.wal.is_off() {
1255 self.wal.append(id, WalRecordType::Commit, &[])?;
1256 self.wal.flush()?;
1257 }
1258 }
1259 self.tx_start_len = None;
1260 for (slot, pages) in std::mem::take(&mut self.pending_free_overflow) {
1264 self.tables[slot].release_overflow_pages(&pages);
1265 }
1266 Ok(())
1267 }
1268
1269 pub fn commit_autocommit(&mut self) -> io::Result<()> {
1273 if !self.wal.is_off() && !self.pending_autocommit_tx_ids.is_empty() {
1274 self.pending_autocommit_tx_ids.sort_unstable();
1275 self.pending_autocommit_tx_ids.dedup();
1276 for id in self.pending_autocommit_tx_ids.drain(..) {
1277 self.wal.append(id, WalRecordType::Commit, &[])?;
1278 }
1279 }
1280 self.wal.flush()
1281 }
1282
1283 fn wal_log(
1298 &mut self,
1299 tx_id: u64,
1300 record_type: WalRecordType,
1301 table: &str,
1302 rid: RowId,
1303 row_bytes: &[u8],
1304 ) -> io::Result<()> {
1305 if self.wal.is_off() {
1312 return Ok(());
1313 }
1314 let payload = encode_wal_payload(table, rid, row_bytes);
1315 self.wal.append(tx_id, record_type, &payload)?;
1316 if self.active_tx_id.is_none() {
1317 self.pending_autocommit_tx_ids.push(tx_id);
1318 }
1319 Ok(())
1320 }
1321
1322 #[inline]
1328 pub fn sync_wal(&mut self) -> io::Result<()> {
1329 self.wal.flush()
1330 }
1331
1332 pub fn set_wal_sync_mode(&mut self, mode: WalSyncMode) {
1340 self.wal.set_sync_mode(mode);
1341 }
1342
1343 pub fn set_dirty_page_budget_bytes(&mut self, limit_bytes: usize) {
1348 self.dirty_budget.set_limit_bytes(limit_bytes);
1349 }
1350
1351 pub fn dirty_page_budget_bytes(&self) -> usize {
1352 self.dirty_budget.limit_bytes()
1353 }
1354
1355 pub fn dirty_pages_buffered(&self) -> usize {
1357 self.dirty_budget.charged_pages()
1358 }
1359
1360 pub fn set_wal_sync_deferred(&mut self, defer: bool) {
1368 self.wal.set_defer_sync(defer);
1369 }
1370
1371 pub fn take_wal_durability_ticket(&mut self) -> Option<WalDurabilityTicket> {
1374 self.wal.take_durability_ticket()
1375 }
1376
1377 pub fn wal_fsync_count(&self) -> u64 {
1379 self.wal.fsync_count()
1380 }
1381
1382 pub fn rollback_to_last_sync(&mut self) -> io::Result<()> {
1397 self.rollback_to_last_sync_inner(None)
1398 }
1399
1400 pub fn rollback_to_last_sync_with_wal_archive<F>(&mut self, mut archive: F) -> io::Result<()>
1405 where
1406 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
1407 {
1408 let archive: WalArchiveCallback<'_> = &mut archive;
1409 self.rollback_to_last_sync_inner(Some(archive))
1410 }
1411
1412 fn rollback_to_last_sync_inner(
1413 &mut self,
1414 mut archive: Option<WalArchiveCallback<'_>>,
1415 ) -> io::Result<()> {
1416 let start_len = self.tx_start_len.unwrap_or(0);
1417 let prearchived = if let Some(archive) = archive.as_mut() {
1418 let records = self.wal.read_through_len(start_len)?;
1419 if !records.is_empty() {
1420 archive(&self.data_dir, &records)?;
1421 }
1422 true
1423 } else {
1424 false
1425 };
1426
1427 let start_len = self.tx_start_len.take().unwrap_or(0);
1428 if let Some(id) = self.active_tx_id.take() {
1429 if !self.wal.is_off() {
1430 let _ = self.wal.append(id, WalRecordType::Rollback, &[]);
1431 }
1432 }
1433 self.wal.discard_and_truncate_to(start_len)?;
1434
1435 for tbl in &mut self.tables {
1445 tbl.heap.discard_dirty();
1446 tbl.discard_dirty_indexes();
1447 }
1448 self.wal.discard_pending()?;
1454 let data_dir = self.data_dir.clone();
1458 let sync_mode = self.wal.sync_mode();
1459 let mut restored = if prearchived {
1460 let mut already_archived = |_dir: &Path, _records: &[WalRecord]| Ok(());
1461 let archive: WalArchiveCallback<'_> = &mut already_archived;
1462 Self::open_inner(&data_dir, Some(archive))?
1463 } else {
1464 Self::open_inner(&data_dir, archive)?
1465 };
1466 if self.has_same_prepared_structure(&restored) {
1472 restored.structure_generation = self.structure_generation;
1473 }
1474 let dirty_budget_limit = self.dirty_budget.limit_bytes();
1475 *self = restored;
1476 self.wal.set_sync_mode(sync_mode);
1477 self.dirty_budget.set_limit_bytes(dirty_budget_limit);
1480 Ok(())
1481 }
1482
1483 fn abandon_active_transaction_for_drop(&mut self) -> io::Result<()> {
1484 self.dirty_budget.set_rollback_pinned(false);
1485 for tbl in &mut self.tables {
1486 tbl.heap.discard_dirty();
1487 }
1488 self.pending_autocommit_tx_ids.clear();
1489 let truncate_result = match self.tx_start_len.take() {
1490 Some(start_len) => self.wal.discard_and_truncate_to(start_len),
1491 None => self.wal.discard_pending(),
1492 };
1493 self.active_tx_id = None;
1494 truncate_result
1495 }
1496
1497 pub fn data_dir(&self) -> &Path {
1499 &self.data_dir
1500 }
1501
1502 pub fn max_lsn(&self) -> u64 {
1507 let max_page_lsn = self
1508 .tables
1509 .iter()
1510 .map(|t| t.heap.max_page_lsn())
1511 .max()
1512 .unwrap_or(0);
1513 max_page_lsn
1514 .max(self.durable_lsn)
1515 .max(self.wal.last_appended_lsn())
1516 }
1517
1518 pub fn create_table(&mut self, schema: Schema) -> io::Result<()> {
1519 self.create_table_full(schema, Vec::new(), Vec::new())
1520 }
1521
1522 pub fn create_table_with_defaults(
1526 &mut self,
1527 schema: Schema,
1528 defaults: Vec<Option<Value>>,
1529 ) -> io::Result<()> {
1530 self.create_table_full(schema, defaults, Vec::new())
1531 }
1532
1533 pub fn create_table_full(
1538 &mut self,
1539 schema: Schema,
1540 defaults: Vec<Option<Value>>,
1541 auto_cols: Vec<bool>,
1542 ) -> io::Result<()> {
1543 self.ensure_no_active_transaction_for_ddl("create table")?;
1544 self.invalidate_structure();
1545 validate_table_name(&schema.table_name)?;
1546 for col in &schema.columns {
1547 validate_column_name(&col.name)?;
1548 }
1549 let name = schema.table_name.clone();
1550 if self.name_to_slot.contains_key(&name) {
1551 return Err(io::Error::new(
1552 io::ErrorKind::AlreadyExists,
1553 format!("table '{name}' already exists"),
1554 ));
1555 }
1556 if !self.wal.is_off() {
1557 let payload = encode_ddl_create_table(&schema, &defaults, &auto_cols);
1558 self.wal
1559 .append(0, WalRecordType::DdlCreateTable, &payload)?;
1560 self.wal.flush()?;
1561 }
1562 let mut table = Table::create(schema, &self.data_dir)?;
1563 table.heap.set_dirty_budget(Arc::clone(&self.dirty_budget));
1564 table.set_defaults(defaults);
1565 table.set_auto_cols(auto_cols);
1566 let slot = self.tables.len();
1567 self.tables.push(table);
1568 self.name_to_slot.insert(name, slot);
1569 self.persist()?;
1570 Ok(())
1571 }
1572
1573 pub fn column_defaults(&self, table: &str) -> Option<&[Option<Value>]> {
1577 let slot = *self.name_to_slot.get(table)?;
1578 Some(self.tables[slot].defaults())
1579 }
1580
1581 pub fn auto_columns(&self, table: &str) -> Option<&[bool]> {
1584 let slot = *self.name_to_slot.get(table)?;
1585 Some(self.tables[slot].auto_cols())
1586 }
1587
1588 pub fn assign_auto_columns(&mut self, table: &str, values: &mut [Value]) {
1592 if let Some(&slot) = self.name_to_slot.get(table) {
1593 self.tables[slot].assign_auto(values);
1594 }
1595 }
1596
1597 fn persist_at_activation_boundary(&self) -> Result<(), CatalogPersistError> {
1602 let cat_path = self.data_dir.join(CATALOG_FILE);
1603 let tmp_path = self.data_dir.join(format!("{CATALOG_FILE}.tmp"));
1604 let entries: Vec<CatalogEntryRef<'_>> = self
1605 .tables
1606 .iter()
1607 .map(|t| CatalogEntryRef {
1608 schema: &t.schema,
1609 indexed_cols: t.indexed_column_metas(),
1610 expression_indexes: t.expression_index_metas(),
1611 defaults: t.defaults(),
1612 auto_cols: t.auto_cols(),
1613 })
1614 .collect();
1615 write_catalog_file(
1616 &tmp_path,
1617 self.active_catalog_version,
1618 self.next_index_id,
1619 &entries,
1620 &self.links,
1621 )
1622 .map_err(CatalogPersistError::BeforeActivation)?;
1623 #[cfg(test)]
1624 if take_catalog_persist_failpoint(1) {
1625 return Err(CatalogPersistError::BeforeActivation(io::Error::other(
1626 "injected catalog failure before rename",
1627 )));
1628 }
1629 fs::rename(&tmp_path, &cat_path).map_err(CatalogPersistError::BeforeActivation)?;
1630 #[cfg(test)]
1631 let directory_sync = if take_catalog_persist_failpoint(2) {
1632 Err(io::Error::other(
1633 "injected catalog directory sync failure after rename",
1634 ))
1635 } else {
1636 sync_directory(&self.data_dir)
1637 };
1638 #[cfg(not(test))]
1639 let directory_sync = sync_directory(&self.data_dir);
1640 directory_sync.map_err(CatalogPersistError::AfterActivation)
1641 }
1642
1643 fn persist(&self) -> io::Result<()> {
1644 self.persist_at_activation_boundary()
1645 .map_err(CatalogPersistError::into_io_error)
1646 }
1647
1648 #[inline]
1652 pub fn table_slot(&self, name: &str) -> Option<usize> {
1653 self.name_to_slot.get(name).copied()
1654 }
1655
1656 #[inline]
1660 pub fn structure_generation(&self) -> u64 {
1661 self.structure_generation
1662 }
1663
1664 #[inline]
1665 fn invalidate_structure(&mut self) {
1666 self.structure_generation = next_structure_generation();
1667 }
1668
1669 fn has_same_prepared_structure(&self, other: &Self) -> bool {
1670 self.links == other.links
1674 && self.tables.len() == other.tables.len()
1675 && self.tables.iter().zip(&other.tables).all(|(left, right)| {
1676 let left_schema = &left.schema;
1677 let right_schema = &right.schema;
1678 left_schema.table_name == right_schema.table_name
1679 && left_schema.columns.len() == right_schema.columns.len()
1680 && left_schema.columns.iter().zip(&right_schema.columns).all(
1681 |(left_col, right_col)| {
1682 left_col.name == right_col.name
1683 && left_col.type_id == right_col.type_id
1684 && left_col.required == right_col.required
1685 && left_col.position == right_col.position
1686 },
1687 )
1688 && left.defaults() == right.defaults()
1689 && left.auto_cols() == right.auto_cols()
1690 && {
1691 let left_indexes = left.indexed_column_metas();
1692 let right_indexes = right.indexed_column_metas();
1693 left_indexes.len() == right_indexes.len()
1694 && left_indexes.iter().zip(&right_indexes).all(
1695 |(left_index, right_index)| {
1696 left_index.name == right_index.name
1697 && left_index.unique == right_index.unique
1698 },
1699 )
1700 }
1701 && left.expression_index_metas() == right.expression_index_metas()
1702 })
1703 }
1704
1705 #[inline]
1708 pub fn table_by_slot(&self, slot: usize) -> &Table {
1709 &self.tables[slot]
1710 }
1711
1712 #[inline]
1714 pub fn table_by_slot_mut(&mut self, slot: usize) -> &mut Table {
1715 &mut self.tables[slot]
1716 }
1717
1718 pub fn get_table(&self, name: &str) -> Option<&Table> {
1719 let slot = *self.name_to_slot.get(name)?;
1720 Some(&self.tables[slot])
1721 }
1722
1723 pub fn get_table_mut(&mut self, name: &str) -> Option<&mut Table> {
1724 let slot = *self.name_to_slot.get(name)?;
1725 Some(&mut self.tables[slot])
1726 }
1727
1728 #[inline]
1732 pub fn table_has_overflow(&self, table: &str) -> bool {
1733 self.get_table(table)
1734 .map(|t| t.has_overflow_rows())
1735 .unwrap_or(false)
1736 }
1737
1738 #[inline]
1743 fn by_name(&self, table: &str) -> io::Result<&Table> {
1744 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1745 io::Error::new(
1746 io::ErrorKind::NotFound,
1747 format!("table '{table}' not found"),
1748 )
1749 })?;
1750 Ok(&self.tables[slot])
1751 }
1752
1753 #[inline]
1755 fn by_name_mut(&mut self, table: &str) -> io::Result<&mut Table> {
1756 let slot = self.slot_of(table)?;
1757 Ok(&mut self.tables[slot])
1758 }
1759
1760 pub fn sweep(&mut self, table: &str) -> io::Result<usize> {
1765 let slot = self.slot_of(table)?;
1766 let reclaimed = self.tables[slot].sweep_overflow()?;
1767 if !reclaimed.is_empty() && !self.wal.is_off() {
1768 let payload = encode_overflow_free_payload(table, &reclaimed);
1769 self.wal.append(0, WalRecordType::OverflowFree, &payload)?;
1770 self.wal.flush()?;
1771 }
1772 Ok(reclaimed.len())
1773 }
1774
1775 pub fn sweep_all(&mut self) -> io::Result<usize> {
1777 let names: Vec<String> = self
1778 .tables
1779 .iter()
1780 .map(|t| t.schema.table_name.clone())
1781 .collect();
1782 let mut total = 0;
1783 for name in names {
1784 total += self.sweep(&name)?;
1785 }
1786 Ok(total)
1787 }
1788
1789 fn slot_of(&self, table: &str) -> io::Result<usize> {
1790 self.name_to_slot.get(table).copied().ok_or_else(|| {
1791 io::Error::new(
1792 io::ErrorKind::NotFound,
1793 format!("table '{table}' not found"),
1794 )
1795 })
1796 }
1797
1798 pub fn insert(&mut self, table: &str, values: &Row) -> io::Result<RowId> {
1799 if self.wal.is_off() {
1808 return self.by_name_mut(table)?.insert(values);
1809 }
1810 let slot = self.slot_of(table)?;
1811 let _ = self.tables[slot].preflight_insert(values)?;
1812 let tx_id = self.next_tx();
1816 let row_bytes = {
1817 let Catalog { tables, wal, .. } = self;
1818 encode_row_with_spill_logged(&mut tables[slot], wal, tx_id, values)?
1819 };
1820 let new_rid = self.tables[slot].insert_encoded(values, &row_bytes)?;
1825 self.wal_log(tx_id, WalRecordType::Insert, table, new_rid, &row_bytes)?;
1826 let lsn = self.wal.last_appended_lsn();
1827 if lsn > 0 {
1828 self.tables[slot].heap.set_page_lsn(new_rid.page_id, lsn)?;
1829 }
1830 Ok(new_rid)
1831 }
1832
1833 pub fn insert_by_slot(&mut self, slot: usize, values: &Row) -> io::Result<RowId> {
1841 if self.wal.is_off() {
1842 return self.tables[slot].insert(values);
1843 }
1844 let _ = self.tables[slot].preflight_insert(values)?;
1845 let tx_id = self.next_tx();
1846 let autocommit = self.active_tx_id.is_none();
1847 let Catalog { tables, wal, .. } = self;
1848 let tbl = &mut tables[slot];
1849 let row_bytes = encode_row_with_spill_logged(tbl, wal, tx_id, values)?;
1852 let new_rid = tbl.insert_encoded(values, &row_bytes)?;
1855 let payload = encode_wal_payload(&tbl.schema.table_name, new_rid, &row_bytes);
1856 wal.append(tx_id, WalRecordType::Insert, &payload)?;
1857 if autocommit {
1858 self.pending_autocommit_tx_ids.push(tx_id);
1859 }
1860 let lsn = wal.last_appended_lsn();
1861 if lsn > 0 {
1862 tbl.heap.set_page_lsn(new_rid.page_id, lsn)?;
1863 }
1864 Ok(new_rid)
1865 }
1866
1867 pub fn get(&self, table: &str, rid: RowId) -> Option<Row> {
1868 self.get_table(table)?.get(rid)
1869 }
1870
1871 pub fn get_projected(
1872 &self,
1873 table: &str,
1874 rid: RowId,
1875 column_indices: &[usize],
1876 ) -> io::Result<Option<Vec<Value>>> {
1877 self.by_name(table)?.get_projected(rid, column_indices)
1878 }
1879
1880 pub fn delete(&mut self, table: &str, rid: RowId) -> io::Result<()> {
1881 let slot = self.slot_of(table)?;
1882 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
1886 if self.wal.is_off() {
1889 self.tables[slot].delete(rid)?;
1890 self.free_overflow_chain(slot, old_pages);
1891 return Ok(());
1892 }
1893 let tx_id = self.next_tx();
1894 self.wal_log(tx_id, WalRecordType::Delete, table, rid, &[])?;
1896 let lsn = self.wal.last_appended_lsn();
1897 self.tables[slot].delete(rid)?;
1898 if lsn > 0 {
1903 self.tables[slot].heap.set_page_lsn(rid.page_id, lsn)?;
1904 }
1905 self.free_overflow_chain(slot, old_pages);
1906 Ok(())
1907 }
1908
1909 pub fn delete_many(&mut self, table: &str, rids: &[RowId]) -> io::Result<u64> {
1913 let slot = self.slot_of(table)?;
1921 let old_pages = self.collect_overflow_pages(slot, rids)?;
1924 if self.wal.is_off() {
1925 let count = self.tables[slot].delete_many(rids)?;
1926 self.free_overflow_chain(slot, old_pages);
1927 return Ok(count);
1928 }
1929 let tx_id = self.next_tx();
1930 for &rid in rids {
1931 let payload = encode_wal_payload(table, rid, &[]);
1932 self.wal.append(tx_id, WalRecordType::Delete, &payload)?;
1933 }
1934 if self.active_tx_id.is_none() && !rids.is_empty() {
1935 self.pending_autocommit_tx_ids.push(tx_id);
1936 }
1937 let count = self.tables[slot].delete_many(rids)?;
1938 self.free_overflow_chain(slot, old_pages);
1939 Ok(count)
1940 }
1941
1942 fn collect_overflow_pages(&self, slot: usize, rids: &[RowId]) -> io::Result<Vec<u32>> {
1945 if !self.tables[slot].has_overflow_rows() {
1946 return Ok(Vec::new());
1947 }
1948 let mut pages = Vec::new();
1949 for &rid in rids {
1950 pages.extend(self.tables[slot].overflow_chain_pages_at(rid)?);
1951 }
1952 Ok(pages)
1953 }
1954
1955 pub fn scan_delete_matching<P>(&mut self, table: &str, pred: P) -> io::Result<u64>
1965 where
1966 P: FnMut(&[u8]) -> bool,
1967 {
1968 self.by_name_mut(table)?.scan_delete_matching(pred)
1969 }
1970
1971 pub fn scan_delete_matching_logged<P>(&mut self, table: &str, pred: P) -> io::Result<u64>
1982 where
1983 P: FnMut(&[u8]) -> bool,
1984 {
1985 if self.wal.is_off() {
1991 return self.by_name_mut(table)?.scan_delete_matching(pred);
1992 }
1993 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1997 io::Error::new(
1998 io::ErrorKind::NotFound,
1999 format!("table '{table}' not found"),
2000 )
2001 })?;
2002 let tx_id = self.next_tx();
2003 let autocommit = self.active_tx_id.is_none();
2004 let Catalog { tables, wal, .. } = self;
2007 let tbl = &mut tables[slot];
2008 let name_bytes = table.as_bytes();
2012 let count = tbl.scan_delete_matching_with_hook(pred, |rid, row_bytes| {
2013 let mut payload: Vec<u8> =
2014 Vec::with_capacity(4 + name_bytes.len() + 10 + row_bytes.len());
2015 payload.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
2016 payload.extend_from_slice(name_bytes);
2017 payload.extend_from_slice(&rid.page_id.to_le_bytes());
2018 payload.extend_from_slice(&rid.slot_index.to_le_bytes());
2019 payload.extend_from_slice(&0u32.to_le_bytes());
2023 let _ = wal.append(tx_id, WalRecordType::Delete, &payload);
2030 })?;
2031 if autocommit && count > 0 {
2032 self.pending_autocommit_tx_ids.push(tx_id);
2033 }
2034 Ok(count)
2036 }
2037
2038 pub fn scan_patch_matching_logged<P, M>(
2046 &mut self,
2047 table: &str,
2048 pred: P,
2049 try_mutate: M,
2050 ) -> io::Result<(u64, Vec<RowId>)>
2051 where
2052 P: FnMut(&[u8]) -> bool,
2053 M: FnMut(&mut [u8]) -> Option<u16>,
2054 {
2055 if self.wal.is_off() {
2056 return self.by_name_mut(table)?.scan_patch_matching_with_hook(
2057 pred,
2058 try_mutate,
2059 |_, _| {},
2060 );
2061 }
2062 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
2063 io::Error::new(
2064 io::ErrorKind::NotFound,
2065 format!("table '{table}' not found"),
2066 )
2067 })?;
2068 let tx_id = self.next_tx();
2069 let autocommit = self.active_tx_id.is_none();
2070 let Catalog { tables, wal, .. } = self;
2071 let tbl = &mut tables[slot];
2072 let name_bytes = table.as_bytes();
2073 let result = tbl.scan_patch_matching_with_hook(pred, try_mutate, |rid, row_bytes| {
2074 let mut payload: Vec<u8> =
2075 Vec::with_capacity(4 + name_bytes.len() + 10 + row_bytes.len());
2076 payload.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
2077 payload.extend_from_slice(name_bytes);
2078 payload.extend_from_slice(&rid.page_id.to_le_bytes());
2079 payload.extend_from_slice(&rid.slot_index.to_le_bytes());
2080 payload.extend_from_slice(&(row_bytes.len() as u32).to_le_bytes());
2081 payload.extend_from_slice(row_bytes);
2082 let _ = wal.append(tx_id, WalRecordType::Update, &payload);
2083 })?;
2084 if autocommit && result.0 > 0 {
2085 self.pending_autocommit_tx_ids.push(tx_id);
2086 }
2087 Ok(result)
2088 }
2089
2090 pub fn update(&mut self, table: &str, rid: RowId, values: &Row) -> io::Result<RowId> {
2091 if self.wal.is_off() {
2094 let slot = self.slot_of(table)?;
2095 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
2096 let new_rid = self.tables[slot].update(rid, values)?;
2097 self.free_overflow_chain(slot, old_pages);
2098 return Ok(new_rid);
2099 }
2100 let slot = self.slot_of(table)?;
2101 self.update_logged(slot, table, rid, values, None)
2102 }
2103
2104 fn update_logged(
2109 &mut self,
2110 slot: usize,
2111 table: &str,
2112 rid: RowId,
2113 values: &Row,
2114 changed_col_indices: Option<&[usize]>,
2115 ) -> io::Result<RowId> {
2116 self.tables[slot].preflight_update(rid, values)?;
2117 let tx_id = self.next_tx();
2118 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
2122 let row_bytes = {
2127 let Catalog { tables, wal, .. } = self;
2128 encode_row_with_spill_logged(&mut tables[slot], wal, tx_id, values)?
2129 };
2130 check_encoded_row_size(&row_bytes)?;
2134 let fit = self.tables[slot].heap.update_fit(rid, row_bytes.len())?;
2135 if fit == UpdateFit::Relocates {
2136 self.wal_log(tx_id, WalRecordType::Delete, table, rid, &[])?;
2146 let delete_lsn = self.wal.last_appended_lsn();
2147 let new_rid =
2148 self.tables[slot].update_encoded(rid, values, &row_bytes, changed_col_indices)?;
2149 self.wal_log(tx_id, WalRecordType::Insert, table, new_rid, &row_bytes)?;
2152 let insert_lsn = self.wal.last_appended_lsn();
2153 if delete_lsn > 0 {
2157 self.tables[slot]
2158 .heap
2159 .set_page_lsn(rid.page_id, delete_lsn)?;
2160 }
2161 if insert_lsn > 0 {
2162 self.tables[slot]
2163 .heap
2164 .set_page_lsn(new_rid.page_id, insert_lsn)?;
2165 }
2166 self.free_overflow_chain(slot, old_pages);
2167 return Ok(new_rid);
2168 }
2169 self.wal_log(tx_id, WalRecordType::Update, table, rid, &row_bytes)?;
2173 let lsn = self.wal.last_appended_lsn();
2174 let new_rid =
2175 self.tables[slot].update_encoded(rid, values, &row_bytes, changed_col_indices)?;
2176 if lsn > 0 {
2182 self.tables[slot].heap.set_page_lsn(new_rid.page_id, lsn)?;
2183 }
2184 self.free_overflow_chain(slot, old_pages);
2185 Ok(new_rid)
2186 }
2187
2188 pub fn update_hinted(
2192 &mut self,
2193 table: &str,
2194 rid: RowId,
2195 values: &Row,
2196 changed_col_indices: Option<&[usize]>,
2197 ) -> io::Result<RowId> {
2198 if self.wal.is_off() {
2202 let slot = self.slot_of(table)?;
2203 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
2204 let new_rid = self.tables[slot].update_hinted(rid, values, changed_col_indices)?;
2205 self.free_overflow_chain(slot, old_pages);
2206 return Ok(new_rid);
2207 }
2208 let slot = self.slot_of(table)?;
2209 self.update_logged(slot, table, rid, values, changed_col_indices)
2210 }
2211
2212 #[inline]
2224 pub fn with_row_bytes_mut<F>(&mut self, table: &str, rid: RowId, f: F) -> io::Result<bool>
2225 where
2226 F: FnOnce(&mut [u8]),
2227 {
2228 self.by_name_mut(table)?.with_row_bytes_mut(rid, f)
2229 }
2230
2231 #[inline]
2245 pub fn update_row_bytes_logged<F>(&mut self, table: &str, rid: RowId, f: F) -> io::Result<bool>
2246 where
2247 F: FnOnce(&mut [u8]),
2248 {
2249 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
2250 io::Error::new(
2251 io::ErrorKind::NotFound,
2252 format!("table '{table}' not found"),
2253 )
2254 })?;
2255 self.update_row_bytes_logged_by_slot(slot, rid, f)
2256 }
2257
2258 #[inline]
2263 pub fn update_row_bytes_logged_by_slot<F>(
2264 &mut self,
2265 slot: usize,
2266 rid: RowId,
2267 f: F,
2268 ) -> io::Result<bool>
2269 where
2270 F: FnOnce(&mut [u8]),
2271 {
2272 let tbl = &mut self.tables[slot];
2275 let ok = tbl.with_row_bytes_mut(rid, f)?;
2276 if !ok {
2277 return Ok(false);
2278 }
2279 if self.wal.is_off() {
2283 return Ok(true);
2284 }
2285 let new_bytes = match tbl.heap.get(rid) {
2288 Some(b) => b,
2289 None => return Ok(false),
2291 };
2292 let table_name = tbl.schema.table_name.clone();
2295 let tx_id = self.next_tx();
2296 self.wal_log(tx_id, WalRecordType::Update, &table_name, rid, &new_bytes)?;
2297 Ok(true)
2298 }
2299
2300 #[inline]
2312 pub fn patch_var_col_in_place(
2313 &mut self,
2314 table: &str,
2315 rid: RowId,
2316 col_idx: usize,
2317 new_value: Option<&[u8]>,
2318 ) -> io::Result<bool> {
2319 self.by_name_mut(table)?
2320 .patch_var_col_in_place(rid, col_idx, new_value)
2321 }
2322
2323 pub fn patch_var_col_logged(
2329 &mut self,
2330 table: &str,
2331 rid: RowId,
2332 col_idx: usize,
2333 new_value: Option<&[u8]>,
2334 ) -> io::Result<bool> {
2335 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
2336 io::Error::new(
2337 io::ErrorKind::NotFound,
2338 format!("table '{table}' not found"),
2339 )
2340 })?;
2341 let tbl = &mut self.tables[slot];
2342 let ok = tbl.patch_var_col_in_place(rid, col_idx, new_value)?;
2343 if !ok {
2344 return Ok(false);
2345 }
2346 if self.wal.is_off() {
2349 return Ok(true);
2350 }
2351 let new_bytes = match tbl.heap.get(rid) {
2352 Some(b) => b,
2353 None => return Ok(false),
2354 };
2355 let table_name = tbl.schema.table_name.clone();
2356 let tx_id = self.next_tx();
2357 self.wal_log(tx_id, WalRecordType::Update, &table_name, rid, &new_bytes)?;
2358 Ok(true)
2359 }
2360
2361 pub fn scan(&self, table: &str) -> io::Result<impl Iterator<Item = (RowId, Row)> + '_> {
2362 Ok(self.by_name(table)?.scan())
2363 }
2364
2365 pub fn for_each_row_raw<F>(&self, table: &str, f: F) -> io::Result<()>
2368 where
2369 F: FnMut(RowId, &[u8]),
2370 {
2371 self.by_name(table)?.for_each_row_raw(f);
2372 Ok(())
2373 }
2374
2375 pub fn try_for_each_row_raw<F>(&self, table: &str, f: F) -> io::Result<()>
2380 where
2381 F: FnMut(RowId, &[u8]) -> std::ops::ControlFlow<()>,
2382 {
2383 self.by_name(table)?.try_for_each_row_raw(f);
2384 Ok(())
2385 }
2386
2387 pub fn create_index(&mut self, table: &str, column: &str) -> io::Result<()> {
2388 self.create_index_unique(table, column, false)
2389 }
2390
2391 pub fn create_index_unique(
2396 &mut self,
2397 table: &str,
2398 column: &str,
2399 unique: bool,
2400 ) -> io::Result<()> {
2401 self.ensure_no_active_transaction_for_ddl("create index")?;
2402 self.invalidate_structure();
2403 let data_dir = self.data_dir.clone();
2404 self.by_name_mut(table)?
2405 .create_index_with_unique(column, &data_dir, unique)?;
2406 self.persist()
2410 }
2411
2412 pub fn active_catalog_version(&self) -> u16 {
2413 self.active_catalog_version
2414 }
2415
2416 pub fn next_index_id(&self) -> u64 {
2417 self.next_index_id
2418 }
2419
2420 pub fn index_metadata(&self, table: &str) -> Option<Vec<IndexMetadata>> {
2422 let table_ref = self.get_table(table)?;
2423 let mut metadata = table_ref
2424 .indexed_column_metas()
2425 .into_iter()
2426 .map(|index| IndexMetadata {
2427 unique: index.unique,
2428 source: IndexKeySource::Column { column: index.name },
2429 })
2430 .collect::<Vec<_>>();
2431 metadata.extend(table_ref.expression_index_metas().into_iter().map(|index| {
2432 IndexMetadata {
2433 unique: index.unique,
2434 source: IndexKeySource::Expression {
2435 index_id: index.index_id,
2436 canonical_version: index.canonical_version,
2437 canonical_text: index.canonical_text,
2438 json_path: index.json_path,
2439 },
2440 }
2441 }));
2442 Some(metadata)
2443 }
2444
2445 pub fn expression_index_metadata(&self, table: &str) -> Option<Vec<ExpressionIndexMeta>> {
2446 Some(self.get_table(table)?.expression_index_metas())
2447 }
2448
2449 pub fn expression_index_btree(&self, table: &str, index_id: u64) -> Option<&BTree> {
2450 self.get_table(table)?.expression_index_btree(index_id)
2451 }
2452
2453 pub fn index_stats(&self, table: &str, column: &str) -> Option<IndexStats> {
2457 Some(self.get_table(table)?.index(column)?.stats())
2458 }
2459
2460 pub fn expression_index_stats(&self, table: &str, index_id: u64) -> Option<IndexStats> {
2462 Some(
2463 self.get_table(table)?
2464 .expression_index_btree(index_id)?
2465 .stats(),
2466 )
2467 }
2468
2469 pub fn index_key_count_capped(
2475 &self,
2476 table: &str,
2477 column: &str,
2478 key: &Value,
2479 cap: usize,
2480 ) -> Option<usize> {
2481 let unique = self.is_index_unique(table, column)?;
2482 let tree = self.get_table(table)?.index(column)?;
2483 Some(if unique {
2484 tree.count_key_capped(key, cap)
2485 } else {
2486 tree.count_prefix_capped(key, cap)
2487 })
2488 }
2489
2490 pub fn expression_index_key_count_capped(
2493 &self,
2494 table: &str,
2495 index_id: u64,
2496 key: &Value,
2497 cap: usize,
2498 ) -> Option<usize> {
2499 Some(
2500 self.get_table(table)?
2501 .expression_index_btree(index_id)?
2502 .count_key_capped(key, cap),
2503 )
2504 }
2505
2506 pub fn expression_index_btree_mut(&mut self, table: &str, index_id: u64) -> Option<&mut BTree> {
2507 self.get_table_mut(table)?
2508 .expression_index_btree_mut(index_id)
2509 }
2510
2511 pub fn expression_index_lookup_all(
2512 &self,
2513 table: &str,
2514 index_id: u64,
2515 key: &Value,
2516 ) -> io::Result<Vec<RowId>> {
2517 let tree = self
2518 .by_name(table)?
2519 .expression_index_btree(index_id)
2520 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2521 Ok(tree.lookup_all(key))
2522 }
2523
2524 pub fn expression_index_range_rids(
2525 &self,
2526 table: &str,
2527 index_id: u64,
2528 start: Option<&Value>,
2529 end: Option<&Value>,
2530 ) -> io::Result<Vec<RowId>> {
2531 let tree = self
2532 .by_name(table)?
2533 .expression_index_btree(index_id)
2534 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2535 Ok(tree.raw_range_rids(start, end))
2536 }
2537
2538 pub fn expression_index_ordered_rids(
2539 &self,
2540 table: &str,
2541 index_id: u64,
2542 ) -> io::Result<Vec<RowId>> {
2543 let tree = self
2544 .by_name(table)?
2545 .expression_index_btree(index_id)
2546 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2547 Ok(tree.ordered_rids_nulls_last())
2548 }
2549
2550 pub fn expression_index_ordered_rids_bounded(
2551 &self,
2552 table: &str,
2553 index_id: u64,
2554 direction: IndexOrderDirection,
2555 offset: usize,
2556 limit: usize,
2557 ) -> io::Result<Vec<RowId>> {
2558 let tree = self
2559 .by_name(table)?
2560 .expression_index_btree(index_id)
2561 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2562 Ok(tree.bounded_ordered_rids_nulls_last(
2563 direction == IndexOrderDirection::Desc,
2564 offset,
2565 limit,
2566 ))
2567 }
2568
2569 pub fn drop_expression_index(&mut self, table: &str, index_id: u64) -> io::Result<()> {
2570 self.ensure_no_active_transaction_for_ddl("drop index")?;
2571 self.invalidate_structure();
2572 validate_table_name(table)?;
2573 let removed = self
2574 .by_name_mut(table)?
2575 .take_expression_index(index_id)
2576 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2577 match self.persist_at_activation_boundary() {
2578 Ok(()) => {}
2579 Err(CatalogPersistError::BeforeActivation(error)) => {
2580 self.by_name_mut(table)?.restore_expression_index(removed);
2581 return Err(error);
2582 }
2583 Err(CatalogPersistError::AfterActivation(error)) => {
2584 warn!(
2585 path = %self.data_dir.display(),
2586 error = %error,
2587 "expression index drop committed but catalog directory sync failed"
2588 );
2589 }
2590 }
2591 let index_path = self
2592 .data_dir
2593 .join(expression_index_file_name(table, index_id));
2594 if let Err(error) = fs::remove_file(&index_path) {
2595 if error.kind() != io::ErrorKind::NotFound {
2596 warn!(path = %index_path.display(), error = %error, "failed to remove dropped expression index file");
2597 }
2598 } else if let Err(error) = sync_directory(&self.data_dir) {
2599 warn!(path = %self.data_dir.display(), error = %error, "failed to sync expression index deletion");
2600 }
2601 Ok(())
2602 }
2603
2604 pub fn create_expression_index_metadata(
2608 &mut self,
2609 table: &str,
2610 canonical_version: u16,
2611 canonical_text: impl Into<String>,
2612 json_path: StoredJsonPathV1,
2613 unique: bool,
2614 ) -> io::Result<u64> {
2615 self.ensure_no_active_transaction_for_ddl("create index")?;
2616 self.invalidate_structure();
2617 validate_table_name(table)?;
2618 validate_column_name(&json_path.column)?;
2619 if canonical_version == 0 {
2620 return Err(io::Error::new(
2621 io::ErrorKind::InvalidInput,
2622 "expression canonical version must be non-zero",
2623 ));
2624 }
2625 let canonical_text = canonical_text.into();
2626 if canonical_text.is_empty() {
2627 return Err(io::Error::new(
2628 io::ErrorKind::InvalidInput,
2629 "expression canonical text must not be empty",
2630 ));
2631 }
2632 if canonical_version == 1 && canonical_text != json_path.canonical_text() {
2633 return Err(io::Error::new(
2634 io::ErrorKind::InvalidInput,
2635 "expression canonical text does not match its stored JSON path",
2636 ));
2637 }
2638 let table_ref = self.by_name(table)?;
2639 let root_index = table_ref
2640 .schema
2641 .column_index(&json_path.column)
2642 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "JSON root column not found"))?;
2643 if table_ref.schema.columns[root_index].type_id != TypeId::Json {
2644 return Err(io::Error::new(
2645 io::ErrorKind::InvalidInput,
2646 "expression index root column must have type json",
2647 ));
2648 }
2649 if table_ref.expression_index_metas().iter().any(|index| {
2650 index.canonical_version == canonical_version && index.canonical_text == canonical_text
2651 }) {
2652 return Err(io::Error::new(
2653 io::ErrorKind::AlreadyExists,
2654 "expression index already exists",
2655 ));
2656 }
2657
2658 let index_id = self.next_index_id;
2659 let next_index_id = index_id
2660 .checked_add(1)
2661 .ok_or_else(|| io::Error::other("expression index id space exhausted"))?;
2662 let index_path = self
2663 .data_dir
2664 .join(expression_index_file_name(table, index_id));
2665 if index_path.exists() {
2666 fs::remove_file(&index_path)?;
2670 sync_directory(&self.data_dir)?;
2671 }
2672 let meta = ExpressionIndexMeta {
2673 index_id,
2674 unique,
2675 canonical_version,
2676 canonical_text,
2677 json_path,
2678 };
2679 self.by_name_mut(table)?
2680 .install_expression_index(meta, &index_path)?;
2681 if let Err(error) = sync_directory(&self.data_dir) {
2682 self.by_name_mut(table)?
2683 .remove_expression_index_by_id(index_id);
2684 let _ = fs::remove_file(&index_path);
2685 return Err(error);
2686 }
2687
2688 let previous_version = self.active_catalog_version;
2689 let previous_next_id = self.next_index_id;
2690 self.active_catalog_version = self
2695 .active_catalog_version
2696 .max(EXPRESSION_INDEX_CATALOG_VERSION);
2697 self.next_index_id = next_index_id;
2698 match self.persist_at_activation_boundary() {
2699 Ok(()) => {}
2700 Err(CatalogPersistError::BeforeActivation(error)) => {
2701 self.by_name_mut(table)?
2702 .remove_expression_index_by_id(index_id);
2703 self.active_catalog_version = previous_version;
2704 self.next_index_id = previous_next_id;
2705 let _ = fs::remove_file(&index_path);
2706 let _ = sync_directory(&self.data_dir);
2707 return Err(error);
2708 }
2709 Err(CatalogPersistError::AfterActivation(error)) => {
2710 warn!(
2711 path = %self.data_dir.display(),
2712 error = %error,
2713 "expression index creation committed but catalog directory sync failed"
2714 );
2715 }
2716 }
2717 Ok(index_id)
2718 }
2719
2720 pub fn create_link(&mut self, def: LinkDef) -> io::Result<()> {
2743 self.ensure_no_active_transaction_for_ddl("create link")?;
2744 self.invalidate_structure();
2745 validate_table_name(&def.owner_type)?;
2746 validate_table_name(&def.target_type)?;
2747 validate_column_name(&def.name)?;
2748 validate_column_name(&def.local_key)?;
2749 validate_column_name(&def.target_key)?;
2750
2751 {
2754 let owner = self.by_name(&def.owner_type)?;
2755 if owner.schema.column_index(&def.local_key).is_none() {
2756 return Err(io::Error::new(
2757 io::ErrorKind::NotFound,
2758 format!(
2759 "link local key '{}' is not a column on owner type '{}'",
2760 def.local_key, def.owner_type
2761 ),
2762 ));
2763 }
2764 if owner.schema.column_index(&def.name).is_some() {
2765 return Err(io::Error::new(
2766 io::ErrorKind::AlreadyExists,
2767 format!(
2768 "link name '{}' collides with a column on owner type '{}'",
2769 def.name, def.owner_type
2770 ),
2771 ));
2772 }
2773 }
2774 if self
2775 .links
2776 .iter()
2777 .any(|l| l.owner_type == def.owner_type && l.name == def.name)
2778 {
2779 return Err(io::Error::new(
2780 io::ErrorKind::AlreadyExists,
2781 format!(
2782 "link '{}' already exists on owner type '{}'",
2783 def.name, def.owner_type
2784 ),
2785 ));
2786 }
2787
2788 {
2790 let target = self.by_name(&def.target_type)?;
2791 if target.schema.column_index(&def.target_key).is_none() {
2792 return Err(io::Error::new(
2793 io::ErrorKind::NotFound,
2794 format!(
2795 "link target key '{}' is not a column on target type '{}'",
2796 def.target_key, def.target_type
2797 ),
2798 ));
2799 }
2800 }
2801
2802 let kind = self.derive_link_kind(&def.target_type, &def.target_key);
2806 let stored = LinkDef { kind, ..def };
2807
2808 self.links.push(stored);
2812 let previous_version = self.active_catalog_version;
2813 self.active_catalog_version = self.active_catalog_version.max(CATALOG_VERSION);
2814 match self.persist_at_activation_boundary() {
2815 Ok(()) => {}
2816 Err(CatalogPersistError::BeforeActivation(error)) => {
2817 self.links.pop();
2818 self.active_catalog_version = previous_version;
2819 let _ = sync_directory(&self.data_dir);
2820 return Err(error);
2821 }
2822 Err(CatalogPersistError::AfterActivation(error)) => {
2823 warn!(
2824 path = %self.data_dir.display(),
2825 error = %error,
2826 "link creation committed but catalog directory sync failed"
2827 );
2828 }
2829 }
2830 Ok(())
2831 }
2832
2833 pub fn derive_link_kind(&self, target_type: &str, target_key: &str) -> LinkKind {
2844 if self.is_index_unique(target_type, target_key) == Some(true) {
2845 LinkKind::ToOne
2846 } else {
2847 LinkKind::ToMany
2848 }
2849 }
2850
2851 pub fn link_kind(&self, owner_type: &str, name: &str) -> Option<LinkKind> {
2856 let link = self.link(owner_type, name)?;
2857 Some(self.derive_link_kind(&link.target_type, &link.target_key))
2858 }
2859
2860 pub fn link(&self, owner_type: &str, name: &str) -> Option<&LinkDef> {
2863 self.links
2864 .iter()
2865 .find(|l| l.owner_type == owner_type && l.name == name)
2866 }
2867
2868 pub fn links(&self) -> impl Iterator<Item = &LinkDef> + '_ {
2870 self.links.iter()
2871 }
2872
2873 pub fn drop_link(&mut self, owner_type: &str, name: &str) -> io::Result<()> {
2878 self.ensure_no_active_transaction_for_ddl("drop link")?;
2879 self.invalidate_structure();
2880 let idx = self
2881 .links
2882 .iter()
2883 .position(|l| l.owner_type == owner_type && l.name == name)
2884 .ok_or_else(|| {
2885 io::Error::new(
2886 io::ErrorKind::NotFound,
2887 format!("link '{name}' not found on owner type '{owner_type}'"),
2888 )
2889 })?;
2890 let removed = self.links.remove(idx);
2891 if let Err(error) = self.persist() {
2892 self.links.insert(idx, removed);
2894 return Err(error);
2895 }
2896 Ok(())
2897 }
2898
2899 fn link_referencing_table(&self, table: &str) -> Option<&LinkDef> {
2903 self.links
2904 .iter()
2905 .find(|l| l.owner_type == table || l.target_type == table)
2906 }
2907
2908 fn link_referencing_column(&self, table: &str, column: &str) -> Option<&LinkDef> {
2911 self.links.iter().find(|l| {
2912 (l.owner_type == table && l.local_key == column)
2913 || (l.target_type == table && l.target_key == column)
2914 })
2915 }
2916
2917 pub fn is_index_unique(&self, table: &str, column: &str) -> Option<bool> {
2921 self.get_table(table)?.is_index_unique(column)
2922 }
2923
2924 pub fn has_index(&self, table: &str, column: &str) -> bool {
2926 self.get_table(table)
2927 .map(|t| t.has_index(column))
2928 .unwrap_or(false)
2929 }
2930
2931 pub fn index_lookup(&self, table: &str, column: &str, key: &Value) -> io::Result<Option<Row>> {
2932 Ok(self
2933 .by_name(table)?
2934 .index_lookup(column, key)
2935 .map(|(_, row)| row))
2936 }
2937
2938 pub fn list_tables(&self) -> Vec<&str> {
2939 self.tables
2943 .iter()
2944 .map(|t| t.schema.table_name.as_str())
2945 .collect()
2946 }
2947
2948 pub fn schema(&self, table: &str) -> Option<&Schema> {
2949 let slot = *self.name_to_slot.get(table)?;
2950 Some(&self.tables[slot].schema)
2951 }
2952
2953 pub fn drop_table(&mut self, name: &str) -> io::Result<()> {
2956 self.ensure_no_active_transaction_for_ddl("drop table")?;
2957 self.invalidate_structure();
2958 validate_table_name(name)?;
2959 let slot = *self.name_to_slot.get(name).ok_or_else(|| {
2960 io::Error::new(io::ErrorKind::NotFound, format!("table '{name}' not found"))
2961 })?;
2962 if let Some(link) = self.link_referencing_table(name) {
2968 return Err(io::Error::new(
2969 io::ErrorKind::InvalidInput,
2970 format!(
2971 "cannot drop table '{name}': link '{}' on '{}' references it. \
2972 Remove the link first with the embedded API \
2973 `Catalog::drop_link(\"{}\", \"{}\")`; PowQL has no statement \
2974 that removes a link",
2975 link.name, link.owner_type, link.owner_type, link.name
2976 ),
2977 ));
2978 }
2979 if !self.wal.is_off() {
2980 let payload = encode_ddl_drop_table(name);
2981 self.wal.append(0, WalRecordType::DdlDropTable, &payload)?;
2982 self.wal.flush()?;
2983 }
2984 let table = &self.tables[slot];
2986 let heap_path = self
2987 .data_dir
2988 .join(format!("{}.heap", table.schema.table_name));
2989 let mut doomed_paths: Vec<PathBuf> = table
2995 .indexed_column_names()
2996 .into_iter()
2997 .map(|col_name| self.data_dir.join(format!("{name}_{col_name}.idx")))
2998 .collect();
2999 doomed_paths.extend(table.expression_index_ids().into_iter().map(|index_id| {
3000 self.data_dir
3001 .join(expression_index_file_name(name, index_id))
3002 }));
3003 self.name_to_slot.remove(name);
3005 let last = self.tables.len() - 1;
3006 if slot != last {
3007 let moved_name = self.tables[last].schema.table_name.clone();
3008 self.tables.swap(slot, last);
3009 self.name_to_slot.insert(moved_name, slot);
3010 }
3011 self.tables.pop();
3012 self.persist()?;
3021 if heap_path.exists() {
3022 fs::remove_file(&heap_path)?;
3023 }
3024 for idx_path in doomed_paths {
3025 if idx_path.exists() {
3026 let _ = fs::remove_file(idx_path);
3027 }
3028 }
3029 Ok(())
3030 }
3031
3032 pub fn alter_table_add_column(&mut self, table: &str, col: ColumnDef) -> io::Result<()> {
3055 self.ensure_no_active_transaction_for_ddl("alter table add column")?;
3056 self.invalidate_structure();
3057 let data_dir = self.data_dir.clone();
3058 {
3059 let tbl = self.by_name_mut(table)?;
3060 if tbl.schema.columns.iter().any(|c| c.name == col.name) {
3061 return Err(io::Error::new(
3062 io::ErrorKind::AlreadyExists,
3063 format!("column '{}' already exists in table '{table}'", col.name),
3064 ));
3065 }
3066 }
3067 let barrier_lsn = if !self.wal.is_off() {
3068 let payload = encode_ddl_alter_add_column(table, &col);
3069 self.wal.append(0, WalRecordType::DdlAddColumn, &payload)?;
3070 self.wal.flush()?;
3071 self.wal.last_appended_lsn()
3072 } else {
3073 0
3074 };
3075 let tbl = self.by_name_mut(table)?;
3076
3077 let old_schema = tbl.schema.clone();
3078
3079 let has_rows = tbl.heap.scan().next().is_some();
3083
3084 if has_rows && col.required {
3085 return Err(io::Error::new(
3086 io::ErrorKind::InvalidInput,
3087 format!(
3088 "cannot add required column '{}' to non-empty table '{table}': \
3089 no default value to backfill existing rows with",
3090 col.name
3091 ),
3092 ));
3093 }
3094
3095 tbl.schema.columns.push(col);
3099 tbl.refresh_layout();
3100
3101 if has_rows {
3102 let fill: Vec<Value> = vec![Value::Empty; tbl.schema.columns.len()];
3107 tbl.rewrite_rows_for_schema_change(&old_schema, &fill, &data_dir)?;
3108 }
3109 if barrier_lsn > 0 {
3117 tbl.heap.stamp_all_pages_min_lsn(barrier_lsn)?;
3118 tbl.heap.flush()?;
3119 }
3120
3121 self.persist()?;
3122 Ok(())
3123 }
3124
3125 pub fn alter_table_drop_column(&mut self, table: &str, col_name: &str) -> io::Result<()> {
3148 self.ensure_no_active_transaction_for_ddl("alter table drop column")?;
3149 self.invalidate_structure();
3150 let data_dir = self.data_dir.clone();
3151 {
3152 let tbl = self.by_name_mut(table)?;
3153 tbl.schema
3154 .columns
3155 .iter()
3156 .position(|c| c.name == col_name)
3157 .ok_or_else(|| {
3158 io::Error::new(
3159 io::ErrorKind::NotFound,
3160 format!("column '{col_name}' not found in table '{table}'"),
3161 )
3162 })?;
3163 }
3164 if let Some(link) = self.link_referencing_column(table, col_name) {
3169 return Err(io::Error::new(
3170 io::ErrorKind::InvalidInput,
3171 format!(
3172 "cannot drop column '{col_name}' from '{table}': link '{}' on '{}' \
3173 references it. Remove the link first with the embedded API \
3174 `Catalog::drop_link(\"{}\", \"{}\")`; PowQL has no statement \
3175 that removes a link",
3176 link.name, link.owner_type, link.owner_type, link.name
3177 ),
3178 ));
3179 }
3180 let removed_expression_index_ids = self
3181 .by_name_mut(table)?
3182 .remove_expression_indexes_for_root(col_name);
3183 let barrier_lsn = if !self.wal.is_off() {
3184 let payload = encode_ddl_alter_drop_column(table, col_name);
3185 self.wal.append(0, WalRecordType::DdlDropColumn, &payload)?;
3186 self.wal.flush()?;
3187 self.wal.last_appended_lsn()
3188 } else {
3189 0
3190 };
3191 let tbl = self.by_name_mut(table)?;
3192 let idx = tbl
3193 .schema
3194 .columns
3195 .iter()
3196 .position(|c| c.name == col_name)
3197 .ok_or_else(|| {
3198 io::Error::new(
3199 io::ErrorKind::NotFound,
3200 format!("column '{col_name}' not found in table '{table}'"),
3201 )
3202 })?;
3203
3204 let old_schema = tbl.schema.clone();
3206 let has_rows = tbl.heap.scan().next().is_some();
3207
3208 tbl.schema.columns.remove(idx);
3210 for (i, col) in tbl.schema.columns.iter_mut().enumerate() {
3211 col.position = i as u16;
3212 }
3213 tbl.refresh_layout();
3214
3215 if has_rows {
3216 let fill: Vec<Value> = vec![Value::Empty; tbl.schema.columns.len()];
3222 tbl.rewrite_rows_for_schema_change(&old_schema, &fill, &data_dir)?;
3223 }
3224 if barrier_lsn > 0 {
3226 tbl.heap.stamp_all_pages_min_lsn(barrier_lsn)?;
3227 tbl.heap.flush()?;
3228 }
3229
3230 self.persist()?;
3231 for index_id in removed_expression_index_ids {
3232 let idx_path = self
3233 .data_dir
3234 .join(expression_index_file_name(table, index_id));
3235 let _ = fs::remove_file(idx_path);
3236 }
3237 Ok(())
3238 }
3239}
3240
3241impl Drop for Catalog {
3242 fn drop(&mut self) {
3243 if self.read_only {
3247 return;
3248 }
3249 if self.active_tx_id.is_some() {
3250 if let Err(e) = self.abandon_active_transaction_for_drop() {
3251 warn!(error = %e, "catalog drop active transaction cleanup failed");
3252 }
3253 return;
3254 }
3255 if let Err(e) = self.checkpoint() {
3267 warn!(error = %e, "catalog drop checkpoint failed");
3268 }
3269 }
3270}
3271
3272fn encode_wal_payload(table: &str, rid: RowId, row_bytes: &[u8]) -> Vec<u8> {
3287 let name = table.as_bytes();
3288 let mut out = Vec::with_capacity(4 + name.len() + 4 + 2 + 4 + row_bytes.len());
3289 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3290 out.extend_from_slice(name);
3291 out.extend_from_slice(&rid.page_id.to_le_bytes());
3292 out.extend_from_slice(&rid.slot_index.to_le_bytes());
3293 out.extend_from_slice(&(row_bytes.len() as u32).to_le_bytes());
3294 out.extend_from_slice(row_bytes);
3295 out
3296}
3297
3298fn decode_wal_payload(data: &[u8]) -> Option<(String, RowId, Vec<u8>)> {
3299 let mut pos = 0usize;
3300 if data.len() < 4 {
3301 return None;
3302 }
3303 let name_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3304 pos += 4;
3305 if pos + name_len > data.len() {
3306 return None;
3307 }
3308 let name = std::str::from_utf8(&data[pos..pos + name_len])
3309 .ok()?
3310 .to_string();
3311 pos += name_len;
3312 if pos + 4 + 2 + 4 > data.len() {
3313 return None;
3314 }
3315 let page_id = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
3316 pos += 4;
3317 let slot_index = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
3318 pos += 2;
3319 let row_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3320 pos += 4;
3321 if pos + row_len > data.len() {
3322 return None;
3323 }
3324 let row_bytes = data[pos..pos + row_len].to_vec();
3325 Some((
3326 name,
3327 RowId {
3328 page_id,
3329 slot_index,
3330 },
3331 row_bytes,
3332 ))
3333}
3334
3335fn write_overflow_chain_logged(
3341 heap: &mut HeapFile,
3342 wal: &mut Wal,
3343 table: &str,
3344 tx_id: u64,
3345 value: &[u8],
3346) -> io::Result<OverflowStub> {
3347 if value.len() > MAX_VALUE_SIZE {
3348 return Err(StorageError::ValueTooLarge {
3349 size: value.len(),
3350 max: MAX_VALUE_SIZE,
3351 }
3352 .into());
3353 }
3354 let n = value.len().div_ceil(OVERFLOW_PAYLOAD_CAP).max(1);
3355 let mut pages = Vec::with_capacity(n);
3356 for _ in 0..n {
3357 pages.push(heap.allocate_overflow_page()?);
3358 }
3359 for i in 0..n {
3360 let start = i * OVERFLOW_PAYLOAD_CAP;
3361 let end = (start + OVERFLOW_PAYLOAD_CAP).min(value.len());
3362 let chunk = &value[start..end];
3363 let next = if i + 1 < n {
3364 pages[i + 1]
3365 } else {
3366 OVERFLOW_CHAIN_END
3367 };
3368 let payload = encode_overflow_write_payload(table, pages[i], next, chunk);
3369 wal.append(tx_id, WalRecordType::OverflowWrite, &payload)?;
3370 let lsn = wal.last_appended_lsn();
3371 heap.write_overflow_page(pages[i], next, chunk, lsn)?;
3372 }
3373 Ok(OverflowStub::new(
3374 value.len() as u64,
3375 pages[0],
3376 crc32fast::hash(value),
3377 ))
3378}
3379
3380fn encode_row_with_spill_logged(
3385 tbl: &mut Table,
3386 wal: &mut Wal,
3387 tx_id: u64,
3388 values: &Row,
3389) -> io::Result<Vec<u8>> {
3390 let v1_len = crate::row::v1_encoded_len(tbl.row_layout(), values);
3393 let is_indexed = tbl.indexed_col_mask();
3394 let chosen = plan_spill(tbl.row_layout(), values, v1_len, &is_indexed);
3395 if chosen.is_empty() {
3396 let mut v1 = Vec::new();
3397 encode_row_into(&tbl.schema, values, &mut v1);
3398 return Ok(v1);
3399 }
3400 let table_name = tbl.schema.table_name.clone();
3401 let n_var = tbl.row_layout().n_var();
3402 let mut spilled: Vec<Option<OverflowStub>> = vec![None; n_var];
3403 for col_idx in chosen {
3404 let var_idx = tbl
3405 .row_layout()
3406 .var_index(col_idx)
3407 .expect("plan_spill only returns var columns");
3408 let bytes: Vec<u8> = match &values[col_idx] {
3409 Value::Str(s) => s.as_bytes().to_vec(),
3410 Value::Bytes(b) => b.to_vec(),
3411 Value::Json(b) => b.to_vec(),
3412 _ => continue,
3413 };
3414 let stub = write_overflow_chain_logged(&mut tbl.heap, wal, &table_name, tx_id, &bytes)?;
3415 spilled[var_idx] = Some(stub);
3416 }
3417 let mut out = Vec::new();
3418 encode_row_v2_into(&tbl.schema, tbl.row_layout(), values, &spilled, &mut out);
3419 Ok(out)
3420}
3421
3422fn encode_overflow_write_payload(
3431 table: &str,
3432 page_id: u32,
3433 next_page: u32,
3434 chunk: &[u8],
3435) -> Vec<u8> {
3436 let name = table.as_bytes();
3437 let mut out = Vec::with_capacity(2 + name.len() + 4 + 4 + 2 + chunk.len());
3438 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
3439 out.extend_from_slice(name);
3440 out.extend_from_slice(&page_id.to_le_bytes());
3441 out.extend_from_slice(&next_page.to_le_bytes());
3442 out.extend_from_slice(&(chunk.len() as u16).to_le_bytes());
3443 out.extend_from_slice(chunk);
3444 out
3445}
3446
3447fn decode_overflow_write_payload(data: &[u8]) -> Option<(String, u32, u32, Vec<u8>)> {
3448 let mut pos = 0usize;
3449 if data.len() < 2 {
3450 return None;
3451 }
3452 let name_len = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
3453 pos += 2;
3454 if pos + name_len + 4 + 4 + 2 > data.len() {
3455 return None;
3456 }
3457 let name = std::str::from_utf8(&data[pos..pos + name_len])
3458 .ok()?
3459 .to_string();
3460 pos += name_len;
3461 let page_id = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
3462 pos += 4;
3463 let next_page = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
3464 pos += 4;
3465 let chunk_len = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
3466 pos += 2;
3467 if pos + chunk_len > data.len() {
3468 return None;
3469 }
3470 Some((
3471 name,
3472 page_id,
3473 next_page,
3474 data[pos..pos + chunk_len].to_vec(),
3475 ))
3476}
3477
3478fn encode_overflow_free_payload(table: &str, pages: &[u32]) -> Vec<u8> {
3482 let name = table.as_bytes();
3483 let mut out = Vec::with_capacity(2 + name.len() + 4 + pages.len() * 4);
3484 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
3485 out.extend_from_slice(name);
3486 out.extend_from_slice(&(pages.len() as u32).to_le_bytes());
3487 for p in pages {
3488 out.extend_from_slice(&p.to_le_bytes());
3489 }
3490 out
3491}
3492
3493fn decode_overflow_free_payload(data: &[u8]) -> Option<(String, Vec<u32>)> {
3494 let mut pos = 0usize;
3495 if data.len() < 2 {
3496 return None;
3497 }
3498 let name_len = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
3499 pos += 2;
3500 if pos + name_len + 4 > data.len() {
3501 return None;
3502 }
3503 let name = std::str::from_utf8(&data[pos..pos + name_len])
3504 .ok()?
3505 .to_string();
3506 pos += name_len;
3507 let count = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3508 pos += 4;
3509 if pos + count * 4 > data.len() {
3510 return None;
3511 }
3512 let mut pages = Vec::with_capacity(count);
3513 for _ in 0..count {
3514 pages.push(u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?));
3515 pos += 4;
3516 }
3517 Some((name, pages))
3518}
3519
3520fn encode_ddl_create_table(
3523 schema: &Schema,
3524 defaults: &[Option<Value>],
3525 auto_cols: &[bool],
3526) -> Vec<u8> {
3527 let name = schema.table_name.as_bytes();
3528 let mut out = Vec::new();
3529 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3530 out.extend_from_slice(name);
3531 out.extend_from_slice(&(schema.columns.len() as u16).to_le_bytes());
3532 for col in &schema.columns {
3533 let cn = col.name.as_bytes();
3534 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3535 out.extend_from_slice(cn);
3536 out.push(col.type_id as u8);
3537 out.push(col.required as u8);
3538 out.extend_from_slice(&col.position.to_le_bytes());
3539 }
3540 encode_defaults_section(&mut out, defaults);
3544 encode_auto_section(&mut out, auto_cols);
3545 out
3546}
3547
3548fn decode_ddl_create_table(data: &[u8]) -> Option<(Schema, Vec<Option<Value>>, Vec<bool>)> {
3549 let mut pos = 0usize;
3550 if data.len() < 4 {
3551 return None;
3552 }
3553 let name_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3554 pos += 4;
3555 if pos + name_len > data.len() {
3556 return None;
3557 }
3558 let table_name = std::str::from_utf8(&data[pos..pos + name_len])
3559 .ok()?
3560 .to_string();
3561 pos += name_len;
3562 if pos + 2 > data.len() {
3563 return None;
3564 }
3565 let n_cols = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
3566 pos += 2;
3567 let mut columns = Vec::with_capacity(n_cols);
3568 for _ in 0..n_cols {
3569 if pos + 4 > data.len() {
3570 return None;
3571 }
3572 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3573 pos += 4;
3574 if pos + cn_len + 4 > data.len() {
3575 return None;
3576 }
3577 let col_name = std::str::from_utf8(&data[pos..pos + cn_len])
3578 .ok()?
3579 .to_string();
3580 pos += cn_len;
3581 let type_id = TypeId::from_u8(data[pos])?;
3582 pos += 1;
3583 let required = data[pos] != 0;
3584 pos += 1;
3585 if pos + 2 > data.len() {
3586 return None;
3587 }
3588 let position = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
3589 pos += 2;
3590 columns.push(ColumnDef {
3591 name: col_name,
3592 type_id,
3593 required,
3594 position,
3595 });
3596 }
3597 let defaults = if pos < data.len() {
3600 decode_defaults_section(data, &mut pos, columns.len())?
3601 } else {
3602 Vec::new()
3603 };
3604 let auto_cols = if pos < data.len() {
3605 decode_auto_section(data, &mut pos, columns.len())?
3606 } else {
3607 Vec::new()
3608 };
3609 Some((
3610 Schema {
3611 table_name,
3612 columns,
3613 },
3614 defaults,
3615 auto_cols,
3616 ))
3617}
3618
3619fn encode_ddl_drop_table(table_name: &str) -> Vec<u8> {
3620 let name = table_name.as_bytes();
3621 let mut out = Vec::with_capacity(4 + name.len());
3622 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3623 out.extend_from_slice(name);
3624 out
3625}
3626
3627fn encode_ddl_alter_add_column(table_name: &str, col: &ColumnDef) -> Vec<u8> {
3628 let name = table_name.as_bytes();
3629 let cn = col.name.as_bytes();
3630 let mut out = Vec::with_capacity(4 + name.len() + 4 + cn.len() + 4);
3631 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3632 out.extend_from_slice(name);
3633 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3634 out.extend_from_slice(cn);
3635 out.push(col.type_id as u8);
3636 out.push(col.required as u8);
3637 out.extend_from_slice(&col.position.to_le_bytes());
3638 out
3639}
3640
3641fn encode_ddl_alter_drop_column(table_name: &str, col_name: &str) -> Vec<u8> {
3642 let name = table_name.as_bytes();
3643 let cn = col_name.as_bytes();
3644 let mut out = Vec::with_capacity(4 + name.len() + 4 + cn.len());
3645 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3646 out.extend_from_slice(name);
3647 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3648 out.extend_from_slice(cn);
3649 out
3650}
3651
3652fn decode_ddl_table_name(data: &[u8]) -> Option<(String, usize)> {
3653 if data.len() < 4 {
3654 return None;
3655 }
3656 let name_len = u32::from_le_bytes(data[0..4].try_into().ok()?) as usize;
3657 if 4 + name_len > data.len() {
3658 return None;
3659 }
3660 let name = std::str::from_utf8(&data[4..4 + name_len])
3661 .ok()?
3662 .to_string();
3663 Some((name, 4 + name_len))
3664}
3665
3666fn decode_ddl_alter_add_column(data: &[u8]) -> Option<(String, ColumnDef)> {
3667 let (table_name, mut pos) = decode_ddl_table_name(data)?;
3668 if pos + 4 > data.len() {
3669 return None;
3670 }
3671 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3672 pos += 4;
3673 if pos + cn_len + 4 > data.len() {
3674 return None;
3675 }
3676 let col_name = std::str::from_utf8(&data[pos..pos + cn_len])
3677 .ok()?
3678 .to_string();
3679 pos += cn_len;
3680 let type_id = TypeId::from_u8(data[pos])?;
3681 pos += 1;
3682 let required = data[pos] != 0;
3683 pos += 1;
3684 if pos + 2 > data.len() {
3685 return None;
3686 }
3687 let position = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
3688 Some((
3689 table_name,
3690 ColumnDef {
3691 name: col_name,
3692 type_id,
3693 required,
3694 position,
3695 },
3696 ))
3697}
3698
3699fn decode_ddl_alter_drop_column(data: &[u8]) -> Option<(String, String)> {
3700 let (table_name, pos) = decode_ddl_table_name(data)?;
3701 if pos + 4 > data.len() {
3702 return None;
3703 }
3704 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3705 if pos + 4 + cn_len > data.len() {
3706 return None;
3707 }
3708 let col_name = std::str::from_utf8(&data[pos + 4..pos + 4 + cn_len])
3709 .ok()?
3710 .to_string();
3711 Some((table_name, col_name))
3712}
3713
3714pub(crate) struct IndexedColMeta {
3742 pub name: String,
3743 pub unique: bool,
3744}
3745
3746pub(crate) struct CatalogEntry {
3749 pub schema: Schema,
3750 pub indexed_cols: Vec<IndexedColMeta>,
3751 pub expression_indexes: Vec<ExpressionIndexMeta>,
3752 pub defaults: Vec<Option<Value>>,
3755 pub auto_cols: Vec<bool>,
3758}
3759
3760pub(crate) struct CatalogEntryRef<'a> {
3762 pub schema: &'a Schema,
3763 pub indexed_cols: Vec<IndexedColMeta>,
3764 pub expression_indexes: Vec<ExpressionIndexMeta>,
3765 pub defaults: &'a [Option<Value>],
3766 pub auto_cols: &'a [bool],
3767}
3768
3769fn encode_value_blob(out: &mut Vec<u8>, v: &Value) {
3775 out.push(v.type_id() as u8);
3776 match v {
3777 Value::Int(n) => out.extend_from_slice(&n.to_le_bytes()),
3778 Value::Float(f) => out.extend_from_slice(&f.to_bits().to_le_bytes()),
3779 Value::Bool(b) => out.push(*b as u8),
3780 Value::Str(s) => {
3781 out.extend_from_slice(&(s.len() as u32).to_le_bytes());
3782 out.extend_from_slice(s.as_bytes());
3783 }
3784 Value::DateTime(n) => out.extend_from_slice(&n.to_le_bytes()),
3785 Value::Uuid(u) => out.extend_from_slice(u),
3786 Value::Bytes(b) => {
3787 out.extend_from_slice(&(b.len() as u32).to_le_bytes());
3788 out.extend_from_slice(b);
3789 }
3790 Value::Json(b) => {
3791 out.extend_from_slice(&(b.len() as u32).to_le_bytes());
3792 out.extend_from_slice(b);
3793 }
3794 Value::Empty => {}
3795 }
3796}
3797
3798fn decode_value_blob(data: &[u8], pos: &mut usize) -> Option<Value> {
3801 let tag = *data.get(*pos)?;
3802 *pos += 1;
3803 let type_id = TypeId::from_u8(tag)?;
3804 let take_fixed = |pos: &mut usize, n: usize| -> Option<Vec<u8>> {
3805 if *pos + n > data.len() {
3806 return None;
3807 }
3808 let slice = data[*pos..*pos + n].to_vec();
3809 *pos += n;
3810 Some(slice)
3811 };
3812 match type_id {
3813 TypeId::Empty => Some(Value::Empty),
3814 TypeId::Int => Some(Value::Int(i64::from_le_bytes(
3815 take_fixed(pos, 8)?.try_into().ok()?,
3816 ))),
3817 TypeId::Float => Some(Value::Float(f64::from_bits(u64::from_le_bytes(
3818 take_fixed(pos, 8)?.try_into().ok()?,
3819 )))),
3820 TypeId::Bool => Some(Value::Bool(take_fixed(pos, 1)?[0] != 0)),
3821 TypeId::DateTime => Some(Value::DateTime(i64::from_le_bytes(
3822 take_fixed(pos, 8)?.try_into().ok()?,
3823 ))),
3824 TypeId::Uuid => Some(Value::Uuid(take_fixed(pos, 16)?.try_into().ok()?)),
3825 TypeId::Str => {
3826 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
3827 Some(Value::Str(String::from_utf8(take_fixed(pos, len)?).ok()?))
3828 }
3829 TypeId::Bytes => {
3830 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
3831 Some(Value::Bytes(take_fixed(pos, len)?))
3832 }
3833 TypeId::Json => {
3834 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
3835 Some(Value::Json(take_fixed(pos, len)?.into()))
3836 }
3837 }
3838}
3839
3840fn encode_defaults_section(out: &mut Vec<u8>, defaults: &[Option<Value>]) {
3844 let present: Vec<(u16, &Value)> = defaults
3845 .iter()
3846 .enumerate()
3847 .filter_map(|(i, d)| d.as_ref().map(|v| (i as u16, v)))
3848 .collect();
3849 out.extend_from_slice(&(present.len() as u16).to_le_bytes());
3850 for (pos, v) in present {
3851 out.extend_from_slice(&pos.to_le_bytes());
3852 encode_value_blob(out, v);
3853 }
3854}
3855
3856fn decode_defaults_section(
3859 data: &[u8],
3860 pos: &mut usize,
3861 n_cols: usize,
3862) -> Option<Vec<Option<Value>>> {
3863 if *pos + 2 > data.len() {
3864 return None;
3865 }
3866 let count = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3867 *pos += 2;
3868 let mut out = vec![None; n_cols];
3869 for _ in 0..count {
3870 if *pos + 2 > data.len() {
3871 return None;
3872 }
3873 let col = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3874 *pos += 2;
3875 let value = decode_value_blob(data, pos)?;
3876 if col < n_cols {
3877 out[col] = Some(value);
3878 }
3879 }
3880 Some(out)
3881}
3882
3883fn encode_auto_section(out: &mut Vec<u8>, auto_cols: &[bool]) {
3887 let present: Vec<u16> = auto_cols
3888 .iter()
3889 .enumerate()
3890 .filter_map(|(i, &a)| if a { Some(i as u16) } else { None })
3891 .collect();
3892 out.extend_from_slice(&(present.len() as u16).to_le_bytes());
3893 for pos in present {
3894 out.extend_from_slice(&pos.to_le_bytes());
3895 }
3896}
3897
3898fn decode_auto_section(data: &[u8], pos: &mut usize, n_cols: usize) -> Option<Vec<bool>> {
3901 if *pos + 2 > data.len() {
3902 return None;
3903 }
3904 let count = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3905 *pos += 2;
3906 let mut out = vec![false; n_cols];
3907 for _ in 0..count {
3908 if *pos + 2 > data.len() {
3909 return None;
3910 }
3911 let col = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3912 *pos += 2;
3913 if col < n_cols {
3914 out[col] = true;
3915 }
3916 }
3917 Some(out)
3918}
3919
3920fn push_catalog_string(out: &mut Vec<u8>, value: &str) -> io::Result<()> {
3921 let len = u32::try_from(value.len())
3922 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "catalog string is too large"))?;
3923 out.extend_from_slice(&len.to_le_bytes());
3924 out.extend_from_slice(value.as_bytes());
3925 Ok(())
3926}
3927
3928fn encode_expression_indexes(out: &mut Vec<u8>, indexes: &[ExpressionIndexMeta]) -> io::Result<()> {
3929 let count = u16::try_from(indexes.len()).map_err(|_| {
3930 io::Error::new(
3931 io::ErrorKind::InvalidInput,
3932 "too many expression indexes on one table",
3933 )
3934 })?;
3935 out.extend_from_slice(&count.to_le_bytes());
3936 for index in indexes {
3937 out.extend_from_slice(&index.index_id.to_le_bytes());
3938 out.push(u8::from(index.unique));
3939 out.extend_from_slice(&index.canonical_version.to_le_bytes());
3940 push_catalog_string(out, &index.canonical_text)?;
3941 push_catalog_string(out, &index.json_path.column)?;
3942 let segment_count = u16::try_from(index.json_path.segments.len()).map_err(|_| {
3943 io::Error::new(
3944 io::ErrorKind::InvalidInput,
3945 "JSON path has too many segments",
3946 )
3947 })?;
3948 out.extend_from_slice(&segment_count.to_le_bytes());
3949 for segment in &index.json_path.segments {
3950 match segment {
3951 StoredJsonPathSegmentV1::Key(key) => {
3952 out.push(1);
3953 push_catalog_string(out, key)?;
3954 }
3955 StoredJsonPathSegmentV1::Index(position) => {
3956 out.push(2);
3957 out.extend_from_slice(&position.to_le_bytes());
3958 }
3959 }
3960 }
3961 }
3962 Ok(())
3963}
3964
3965fn decode_expression_indexes(data: &[u8], pos: &mut usize) -> io::Result<Vec<ExpressionIndexMeta>> {
3966 let count = read_u16(data, pos)? as usize;
3967 let mut indexes = Vec::with_capacity(count);
3968 for _ in 0..count {
3969 let index_id = read_u64(data, pos)?;
3970 if index_id == 0 {
3971 return Err(io::Error::new(
3972 io::ErrorKind::InvalidData,
3973 "expression index id must be non-zero",
3974 ));
3975 }
3976 let unique = read_u8(data, pos)? != 0;
3977 let canonical_version = read_u16(data, pos)?;
3978 let canonical_len = read_u32(data, pos)? as usize;
3979 let canonical_text = read_string(data, pos, canonical_len)?;
3980 let column_len = read_u32(data, pos)? as usize;
3981 let column = read_string(data, pos, column_len)?;
3982 let segment_count = read_u16(data, pos)? as usize;
3983 let mut segments = Vec::with_capacity(segment_count);
3984 for _ in 0..segment_count {
3985 match read_u8(data, pos)? {
3986 1 => {
3987 let len = read_u32(data, pos)? as usize;
3988 segments.push(StoredJsonPathSegmentV1::Key(read_string(data, pos, len)?));
3989 }
3990 2 => segments.push(StoredJsonPathSegmentV1::Index(read_u32(data, pos)?)),
3991 tag => {
3992 return Err(io::Error::new(
3993 io::ErrorKind::InvalidData,
3994 format!("unknown stored JSON path segment tag: {tag}"),
3995 ));
3996 }
3997 }
3998 }
3999 indexes.push(ExpressionIndexMeta {
4000 index_id,
4001 unique,
4002 canonical_version,
4003 canonical_text,
4004 json_path: StoredJsonPathV1 { column, segments },
4005 });
4006 }
4007 Ok(indexes)
4008}
4009
4010fn encode_links_section(out: &mut Vec<u8>, links: &[LinkDef]) -> io::Result<()> {
4014 let count = u32::try_from(links.len())
4015 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "too many catalog links"))?;
4016 out.extend_from_slice(&count.to_le_bytes());
4017 for link in links {
4018 push_catalog_string(out, &link.owner_type)?;
4019 push_catalog_string(out, &link.name)?;
4020 push_catalog_string(out, &link.target_type)?;
4021 push_catalog_string(out, &link.local_key)?;
4022 push_catalog_string(out, &link.target_key)?;
4023 out.push(link.kind.to_u8());
4024 }
4025 Ok(())
4026}
4027
4028fn decode_links_section(data: &[u8], pos: &mut usize) -> io::Result<Vec<LinkDef>> {
4032 let count = read_u32(data, pos)? as usize;
4033 if count > data.len() {
4034 return Err(io::Error::new(
4035 io::ErrorKind::InvalidData,
4036 format!("catalog file corrupt: implausible link count {count}"),
4037 ));
4038 }
4039 let mut links = Vec::with_capacity(count);
4040 for _ in 0..count {
4041 let owner_type = read_len_prefixed_string(data, pos)?;
4042 let name = read_len_prefixed_string(data, pos)?;
4043 let target_type = read_len_prefixed_string(data, pos)?;
4044 let local_key = read_len_prefixed_string(data, pos)?;
4045 let target_key = read_len_prefixed_string(data, pos)?;
4046 let kind = LinkKind::from_u8(read_u8(data, pos)?)?;
4047 links.push(LinkDef {
4048 owner_type,
4049 name,
4050 target_type,
4051 local_key,
4052 target_key,
4053 kind,
4054 });
4055 }
4056 Ok(links)
4057}
4058
4059fn read_len_prefixed_string(data: &[u8], pos: &mut usize) -> io::Result<String> {
4060 let len = read_u32(data, pos)? as usize;
4061 read_string(data, pos, len)
4062}
4063
4064fn write_catalog_file(
4065 path: &Path,
4066 version: u16,
4067 next_index_id: u64,
4068 entries: &[CatalogEntryRef<'_>],
4069 links: &[LinkDef],
4070) -> io::Result<()> {
4071 if !(1..=CATALOG_VERSION).contains(&version) {
4072 return Err(io::Error::new(
4073 io::ErrorKind::InvalidInput,
4074 format!("unsupported catalog write version: {version}"),
4075 ));
4076 }
4077 let mut buf: Vec<u8> = Vec::with_capacity(64);
4078 buf.extend_from_slice(CATALOG_MAGIC);
4079 buf.extend_from_slice(&version.to_le_bytes());
4080 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
4081 if version >= 6 {
4082 buf.extend_from_slice(&next_index_id.to_le_bytes());
4083 }
4084
4085 for entry in entries {
4086 let schema = entry.schema;
4087 let name = schema.table_name.as_bytes();
4088 buf.extend_from_slice(&(name.len() as u32).to_le_bytes());
4089 buf.extend_from_slice(name);
4090 buf.extend_from_slice(&(schema.columns.len() as u16).to_le_bytes());
4091 for col in &schema.columns {
4092 let cn = col.name.as_bytes();
4093 buf.extend_from_slice(&(cn.len() as u32).to_le_bytes());
4094 buf.extend_from_slice(cn);
4095 buf.push(col.type_id as u8);
4096 buf.push(if col.required { 1 } else { 0 });
4097 buf.extend_from_slice(&col.position.to_le_bytes());
4098 }
4099 buf.extend_from_slice(&(entry.indexed_cols.len() as u16).to_le_bytes());
4101 for meta in &entry.indexed_cols {
4102 let cn = meta.name.as_bytes();
4103 buf.extend_from_slice(&(cn.len() as u32).to_le_bytes());
4104 buf.extend_from_slice(cn);
4105 buf.push(if meta.unique { 1 } else { 0 });
4106 }
4107 encode_defaults_section(&mut buf, entry.defaults);
4109 encode_auto_section(&mut buf, entry.auto_cols);
4111 if version >= 6 {
4112 encode_expression_indexes(&mut buf, &entry.expression_indexes)?;
4113 }
4114 }
4115
4116 if version >= 7 {
4121 encode_links_section(&mut buf, links)?;
4122 }
4123
4124 let crc = crc32fast::hash(&buf);
4128 buf.extend_from_slice(&crc.to_le_bytes());
4129
4130 let mut f = fs::OpenOptions::new()
4131 .create(true)
4132 .write(true)
4133 .truncate(true)
4134 .open(path)?;
4135 f.write_all(&buf)?;
4136 f.sync_data()?;
4137 Ok(())
4138}
4139
4140struct CatalogFile {
4141 version: u16,
4142 next_index_id: u64,
4143 entries: Vec<CatalogEntry>,
4144 links: Vec<LinkDef>,
4145}
4146
4147fn read_catalog_file(path: &Path) -> io::Result<CatalogFile> {
4148 read_catalog_file_with_max_version(path, CATALOG_VERSION)
4149}
4150
4151pub fn read_active_catalog_version(data_dir: &Path) -> io::Result<u16> {
4157 let cat_path = data_dir.join(CATALOG_FILE);
4158 Ok(read_catalog_file(&cat_path)?.version)
4159}
4160
4161fn read_catalog_file_with_max_version(
4162 path: &Path,
4163 max_supported_version: u16,
4164) -> io::Result<CatalogFile> {
4165 let mut f = fs::File::open(path)?;
4166 let mut buf = Vec::new();
4167 f.read_to_end(&mut buf)?;
4168
4169 let mut pos = 0usize;
4170 if buf.len() < 14 || &buf[0..4] != CATALOG_MAGIC {
4172 return Err(io::Error::new(
4173 io::ErrorKind::InvalidData,
4174 "bad catalog magic",
4175 ));
4176 }
4177
4178 let payload = &buf[..buf.len() - 4];
4180 let stored_crc = u32::from_le_bytes(
4181 buf[buf.len() - 4..]
4182 .try_into()
4183 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog CRC"))?,
4184 );
4185 let computed_crc = crc32fast::hash(payload);
4186 if stored_crc != computed_crc {
4187 return Err(io::Error::new(
4188 io::ErrorKind::InvalidData,
4189 format!(
4190 "catalog CRC32 mismatch: expected {stored_crc:#010x}, got {computed_crc:#010x}"
4191 ),
4192 ));
4193 }
4194 let buf = &buf[..buf.len() - 4];
4196 pos += 4;
4197 let version = u16::from_le_bytes(
4198 buf[pos..pos + 2]
4199 .try_into()
4200 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog header"))?,
4201 );
4202 pos += 2;
4203 if version == 0 || version > max_supported_version {
4213 return Err(io::Error::new(
4214 io::ErrorKind::InvalidData,
4215 format!("unsupported catalog version: {version}"),
4216 ));
4217 }
4218 let n_tables = u32::from_le_bytes(
4219 buf[pos..pos + 4]
4220 .try_into()
4221 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog header"))?,
4222 ) as usize;
4223 pos += 4;
4224 let next_index_id = if version >= 6 {
4234 let id = read_u64(buf, &mut pos)?;
4235 if id == 0 {
4236 return Err(io::Error::new(
4237 io::ErrorKind::InvalidData,
4238 "catalog next index id must be non-zero",
4239 ));
4240 }
4241 id
4242 } else {
4243 1
4246 };
4247
4248 if n_tables > buf.len() {
4255 return Err(io::Error::new(
4256 io::ErrorKind::InvalidData,
4257 format!("catalog file corrupt: implausible table count {n_tables}"),
4258 ));
4259 }
4260
4261 let mut entries = Vec::with_capacity(n_tables);
4262 for _ in 0..n_tables {
4263 let name_len = read_u32(buf, &mut pos)? as usize;
4264 let table_name = read_string(buf, &mut pos, name_len)?;
4265 let n_cols = read_u16(buf, &mut pos)? as usize;
4266
4267 let mut columns = Vec::with_capacity(n_cols);
4268 for _ in 0..n_cols {
4269 let cname_len = read_u32(buf, &mut pos)? as usize;
4270 let name = read_string(buf, &mut pos, cname_len)?;
4271 let type_id_raw = read_u8(buf, &mut pos)?;
4272 let type_id = type_id_from_u8(type_id_raw)?;
4273 let required = read_u8(buf, &mut pos)? != 0;
4274 let position = read_u16(buf, &mut pos)?;
4275 columns.push(ColumnDef {
4276 name,
4277 type_id,
4278 required,
4279 position,
4280 });
4281 }
4282
4283 let indexed_cols: Vec<IndexedColMeta> = if version >= 3 {
4289 let n = read_u16(buf, &mut pos)? as usize;
4290 let mut v = Vec::with_capacity(n);
4291 for _ in 0..n {
4292 let l = read_u32(buf, &mut pos)? as usize;
4293 let name = read_string(buf, &mut pos, l)?;
4294 let unique = read_u8(buf, &mut pos)? != 0;
4295 v.push(IndexedColMeta { name, unique });
4296 }
4297 v
4298 } else if version >= 2 {
4299 let n = read_u16(buf, &mut pos)? as usize;
4300 let mut v = Vec::with_capacity(n);
4301 for _ in 0..n {
4302 let l = read_u32(buf, &mut pos)? as usize;
4303 let name = read_string(buf, &mut pos, l)?;
4304 v.push(IndexedColMeta {
4305 name,
4306 unique: false,
4307 });
4308 }
4309 v
4310 } else {
4311 Vec::new()
4312 };
4313
4314 let defaults = if version >= 4 {
4318 decode_defaults_section(buf, &mut pos, columns.len()).ok_or_else(|| {
4319 io::Error::new(io::ErrorKind::InvalidData, "truncated catalog defaults")
4320 })?
4321 } else {
4322 Vec::new()
4323 };
4324
4325 let auto_cols = if version >= 5 {
4329 decode_auto_section(buf, &mut pos, columns.len()).ok_or_else(|| {
4330 io::Error::new(io::ErrorKind::InvalidData, "truncated catalog auto columns")
4331 })?
4332 } else {
4333 Vec::new()
4334 };
4335
4336 let expression_indexes = if version >= 6 {
4340 decode_expression_indexes(buf, &mut pos)?
4341 } else {
4342 Vec::new()
4343 };
4344
4345 entries.push(CatalogEntry {
4346 schema: Schema {
4347 table_name,
4348 columns,
4349 },
4350 indexed_cols,
4351 expression_indexes,
4352 defaults,
4353 auto_cols,
4354 });
4355 }
4356
4357 let links = if version >= 7 {
4362 decode_links_section(buf, &mut pos)?
4363 } else {
4364 Vec::new()
4365 };
4366
4367 let mut seen_index_ids = FxHashMap::default();
4368 let mut max_index_id = 0;
4369 for entry in &entries {
4370 for index in &entry.expression_indexes {
4371 if index.canonical_version == 0 || index.canonical_text.is_empty() {
4372 return Err(io::Error::new(
4373 io::ErrorKind::InvalidData,
4374 "expression index has invalid canonical identity",
4375 ));
4376 }
4377 if index.canonical_version == 1
4378 && index.canonical_text != index.json_path.canonical_text()
4379 {
4380 return Err(io::Error::new(
4381 io::ErrorKind::InvalidData,
4382 "expression index canonical identity does not match its JSON path",
4383 ));
4384 }
4385 let Some(root) = entry
4386 .schema
4387 .columns
4388 .iter()
4389 .find(|column| column.name == index.json_path.column)
4390 else {
4391 return Err(io::Error::new(
4392 io::ErrorKind::InvalidData,
4393 "expression index JSON root is absent from its table",
4394 ));
4395 };
4396 if root.type_id != TypeId::Json {
4397 return Err(io::Error::new(
4398 io::ErrorKind::InvalidData,
4399 "expression index root column is not JSON",
4400 ));
4401 }
4402 if seen_index_ids.insert(index.index_id, ()).is_some() {
4403 return Err(io::Error::new(
4404 io::ErrorKind::InvalidData,
4405 "duplicate expression index id in catalog",
4406 ));
4407 }
4408 max_index_id = max_index_id.max(index.index_id);
4409 }
4410 }
4411 if next_index_id <= max_index_id {
4412 return Err(io::Error::new(
4413 io::ErrorKind::InvalidData,
4414 "catalog next index id does not exceed persisted index ids",
4415 ));
4416 }
4417 Ok(CatalogFile {
4418 version,
4419 next_index_id,
4420 entries,
4421 links,
4422 })
4423}
4424
4425fn read_u8(buf: &[u8], pos: &mut usize) -> io::Result<u8> {
4426 if *pos >= buf.len() {
4427 return Err(io::Error::new(
4428 io::ErrorKind::UnexpectedEof,
4429 "truncated catalog",
4430 ));
4431 }
4432 let v = buf[*pos];
4433 *pos += 1;
4434 Ok(v)
4435}
4436fn read_u16(buf: &[u8], pos: &mut usize) -> io::Result<u16> {
4437 if *pos + 2 > buf.len() {
4438 return Err(io::Error::new(
4439 io::ErrorKind::UnexpectedEof,
4440 "truncated catalog",
4441 ));
4442 }
4443 let v = u16::from_le_bytes(
4444 buf[*pos..*pos + 2]
4445 .try_into()
4446 .expect("bounds checked above"),
4447 );
4448 *pos += 2;
4449 Ok(v)
4450}
4451fn read_u32(buf: &[u8], pos: &mut usize) -> io::Result<u32> {
4452 if *pos + 4 > buf.len() {
4453 return Err(io::Error::new(
4454 io::ErrorKind::UnexpectedEof,
4455 "truncated catalog",
4456 ));
4457 }
4458 let v = u32::from_le_bytes(
4459 buf[*pos..*pos + 4]
4460 .try_into()
4461 .expect("bounds checked above"),
4462 );
4463 *pos += 4;
4464 Ok(v)
4465}
4466fn read_u64(buf: &[u8], pos: &mut usize) -> io::Result<u64> {
4467 if *pos + 8 > buf.len() {
4468 return Err(io::Error::new(
4469 io::ErrorKind::UnexpectedEof,
4470 "truncated catalog",
4471 ));
4472 }
4473 let value = u64::from_le_bytes(
4474 buf[*pos..*pos + 8]
4475 .try_into()
4476 .expect("bounds checked above"),
4477 );
4478 *pos += 8;
4479 Ok(value)
4480}
4481fn read_string(buf: &[u8], pos: &mut usize, len: usize) -> io::Result<String> {
4482 if *pos + len > buf.len() {
4483 return Err(io::Error::new(
4484 io::ErrorKind::UnexpectedEof,
4485 "truncated catalog string",
4486 ));
4487 }
4488 let s = std::str::from_utf8(&buf[*pos..*pos + len])
4489 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "non-utf8 in catalog"))?
4490 .to_string();
4491 *pos += len;
4492 Ok(s)
4493}
4494fn type_id_from_u8(v: u8) -> io::Result<TypeId> {
4495 match v {
4496 0 => Ok(TypeId::Empty),
4497 1 => Ok(TypeId::Int),
4498 2 => Ok(TypeId::Float),
4499 3 => Ok(TypeId::Bool),
4500 4 => Ok(TypeId::Str),
4501 5 => Ok(TypeId::DateTime),
4502 6 => Ok(TypeId::Uuid),
4503 7 => Ok(TypeId::Bytes),
4504 8 => Ok(TypeId::Json),
4505 _ => Err(io::Error::new(
4506 io::ErrorKind::InvalidData,
4507 format!("unknown type id: {v}"),
4508 )),
4509 }
4510}
4511
4512#[cfg(test)]
4513mod tests {
4514 use super::*;
4515
4516 fn fail_next_catalog_persist_at(stage: u8) {
4517 CATALOG_PERSIST_FAILPOINT.with(|failpoint| failpoint.set(stage));
4518 }
4519
4520 fn temp_catalog(name: &str) -> Catalog {
4521 let dir = std::env::temp_dir().join(format!("powdb_cat_{name}_{}", std::process::id()));
4522 Catalog::create(&dir).unwrap()
4523 }
4524
4525 fn hash_dir_tree(dir: &std::path::Path) -> String {
4530 let mut entries: Vec<std::path::PathBuf> = Vec::new();
4531 fn walk(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
4532 let mut items: Vec<_> = fs::read_dir(dir).unwrap().flatten().collect();
4533 items.sort_by_key(std::fs::DirEntry::path);
4534 for item in items {
4535 let path = item.path();
4536 if path.is_dir() {
4537 walk(&path, out);
4538 } else {
4539 out.push(path);
4540 }
4541 }
4542 }
4543 walk(dir, &mut entries);
4544 let mut hasher = crc32fast::Hasher::new();
4545 for path in &entries {
4546 hasher.update(path.to_string_lossy().as_bytes());
4547 hasher.update(&fs::read(path).unwrap());
4548 }
4549 format!("{:08x}", hasher.finalize())
4550 }
4551
4552 fn seed_quiescent_dir(dir: &std::path::Path) {
4553 let mut catalog = Catalog::create(dir).unwrap();
4554 catalog
4555 .create_table(Schema {
4556 table_name: "User".into(),
4557 columns: vec![
4558 ColumnDef {
4559 name: "name".into(),
4560 type_id: TypeId::Str,
4561 required: true,
4562 position: 0,
4563 },
4564 ColumnDef {
4565 name: "age".into(),
4566 type_id: TypeId::Int,
4567 required: false,
4568 position: 1,
4569 },
4570 ],
4571 })
4572 .unwrap();
4573 catalog.create_index("User", "age").unwrap();
4574 catalog
4575 .insert("User", &vec![Value::Str("Ada".into()), Value::Int(36)])
4576 .unwrap();
4577 catalog
4578 .insert("User", &vec![Value::Str("Bo".into()), Value::Int(20)])
4579 .unwrap();
4580 drop(catalog);
4583 }
4584
4585 #[test]
4586 fn open_read_only_serves_reads_on_clean_dir() {
4587 let dir = tempfile::tempdir().unwrap();
4588 seed_quiescent_dir(dir.path());
4589
4590 let catalog = Catalog::open_read_only(dir.path()).unwrap();
4591 let rows: Vec<_> = catalog.scan("User").unwrap().collect();
4592 assert_eq!(rows.len(), 2);
4593 let hit = catalog
4595 .index_lookup("User", "age", &Value::Int(36))
4596 .unwrap();
4597 assert_eq!(hit.unwrap()[0], Value::Str("Ada".into()));
4598 }
4599
4600 #[test]
4601 fn open_read_only_never_mutates_dir() {
4602 let dir = tempfile::tempdir().unwrap();
4603 seed_quiescent_dir(dir.path());
4604 let before = hash_dir_tree(dir.path());
4605
4606 {
4607 let catalog = Catalog::open_read_only(dir.path()).unwrap();
4608 let _ = catalog.scan("User").unwrap().count();
4609 let _ = catalog
4610 .index_lookup("User", "age", &Value::Int(20))
4611 .unwrap();
4612 }
4614 let after = hash_dir_tree(dir.path());
4615 assert_eq!(
4616 before, after,
4617 "read-only open + queries + drop must leave the directory byte-identical"
4618 );
4619 }
4620
4621 #[test]
4622 fn open_read_only_refuses_non_empty_wal() {
4623 let dir = tempfile::tempdir().unwrap();
4624 {
4627 let mut catalog = Catalog::create(dir.path()).unwrap();
4628 catalog
4629 .create_table(Schema {
4630 table_name: "T".into(),
4631 columns: vec![ColumnDef {
4632 name: "id".into(),
4633 type_id: TypeId::Int,
4634 required: true,
4635 position: 0,
4636 }],
4637 })
4638 .unwrap();
4639 catalog.insert("T", &vec![Value::Int(1)]).unwrap();
4640 catalog.sync_wal().unwrap();
4641 std::mem::forget(catalog); }
4643 let err = match Catalog::open_read_only(dir.path()) {
4644 Ok(_) => panic!("read-only open must refuse a non-empty WAL"),
4645 Err(err) => err,
4646 };
4647 assert!(
4648 err.to_string().contains("WAL is not empty"),
4649 "expected a WAL-not-empty refusal naming the remedy, got: {err}"
4650 );
4651 assert!(err.to_string().contains("read-write engine"));
4652 }
4653
4654 #[test]
4655 fn open_read_only_expression_index_reads_work() {
4656 let dir = tempfile::tempdir().unwrap();
4657 {
4658 let mut catalog = Catalog::create(dir.path()).unwrap();
4659 catalog
4660 .create_table(Schema {
4661 table_name: "Doc".into(),
4662 columns: vec![ColumnDef {
4663 name: "data".into(),
4664 type_id: TypeId::Json,
4665 required: false,
4666 position: 0,
4667 }],
4668 })
4669 .unwrap();
4670 let path =
4671 StoredJsonPathV1::new("data", vec![StoredJsonPathSegmentV1::Key("author".into())]);
4672 catalog
4673 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path, false)
4674 .unwrap();
4675 drop(catalog);
4676 }
4677 let before = hash_dir_tree(dir.path());
4679 let catalog = Catalog::open_read_only(dir.path()).unwrap();
4680 assert_eq!(catalog.scan("Doc").unwrap().count(), 0);
4681 drop(catalog);
4682 let after = hash_dir_tree(dir.path());
4683 assert_eq!(
4684 before, after,
4685 "read-only expression-index load must not write"
4686 );
4687 }
4688
4689 #[test]
4690 fn v5_reader_rejects_v6_catalog() {
4691 let dir = tempfile::tempdir().unwrap();
4692 let mut catalog = Catalog::create(dir.path()).unwrap();
4693 catalog
4694 .create_table(Schema {
4695 table_name: "Doc".into(),
4696 columns: vec![ColumnDef {
4697 name: "data".into(),
4698 type_id: TypeId::Json,
4699 required: false,
4700 position: 0,
4701 }],
4702 })
4703 .unwrap();
4704 let path =
4705 StoredJsonPathV1::new("data", vec![StoredJsonPathSegmentV1::Key("author".into())]);
4706 catalog
4707 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path, false)
4708 .unwrap();
4709 let result = read_catalog_file_with_max_version(
4710 &dir.path().join(CATALOG_FILE),
4711 LEGACY_CATALOG_VERSION,
4712 );
4713 let error = match result {
4714 Ok(_) => panic!("a v5 reader must reject v6 before decoding its payload"),
4715 Err(error) => error,
4716 };
4717 assert!(error.to_string().contains("unsupported catalog version: 6"));
4718 }
4719
4720 #[test]
4724 fn v6_reader_rejects_v7_catalog() {
4725 let dir = tempfile::tempdir().unwrap();
4726 let mut catalog = Catalog::create(dir.path()).unwrap();
4727 catalog
4728 .create_table(Schema {
4729 table_name: "Order".into(),
4730 columns: vec![ColumnDef {
4731 name: "user_id".into(),
4732 type_id: TypeId::Int,
4733 required: false,
4734 position: 0,
4735 }],
4736 })
4737 .unwrap();
4738 catalog
4739 .create_table(Schema {
4740 table_name: "User".into(),
4741 columns: vec![ColumnDef {
4742 name: "id".into(),
4743 type_id: TypeId::Int,
4744 required: true,
4745 position: 0,
4746 }],
4747 })
4748 .unwrap();
4749 catalog
4750 .create_link(LinkDef {
4751 owner_type: "Order".into(),
4752 name: "user".into(),
4753 target_type: "User".into(),
4754 local_key: "user_id".into(),
4755 target_key: "id".into(),
4756 kind: LinkKind::ToMany,
4757 })
4758 .unwrap();
4759 assert_eq!(catalog.active_catalog_version(), CATALOG_VERSION);
4760
4761 let result = read_catalog_file_with_max_version(
4762 &dir.path().join(CATALOG_FILE),
4763 EXPRESSION_INDEX_CATALOG_VERSION,
4764 );
4765 let error = match result {
4766 Ok(_) => panic!("a v6 reader must reject v7 before decoding its payload"),
4767 Err(error) => error,
4768 };
4769 assert!(error.to_string().contains("unsupported catalog version: 7"));
4770 }
4771
4772 #[test]
4773 fn expression_index_rolls_back_only_before_catalog_rename() {
4774 let before_dir = tempfile::tempdir().unwrap();
4775 let mut before = Catalog::create(before_dir.path()).unwrap();
4776 before
4777 .create_table(Schema {
4778 table_name: "Doc".into(),
4779 columns: vec![ColumnDef {
4780 name: "data".into(),
4781 type_id: TypeId::Json,
4782 required: false,
4783 position: 0,
4784 }],
4785 })
4786 .unwrap();
4787 let path =
4788 StoredJsonPathV1::new("data", vec![StoredJsonPathSegmentV1::Key("score".into())]);
4789
4790 fail_next_catalog_persist_at(1);
4791 let error = before
4792 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path.clone(), false)
4793 .unwrap_err();
4794 assert!(error.to_string().contains("before rename"));
4795 assert_eq!(before.active_catalog_version(), LEGACY_CATALOG_VERSION);
4796 assert_eq!(before.next_index_id(), 1);
4797 assert!(before.expression_index_metadata("Doc").unwrap().is_empty());
4798 assert!(!before_dir
4799 .path()
4800 .join(expression_index_file_name("Doc", 1))
4801 .exists());
4802
4803 let before_index_id = before
4804 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path.clone(), false)
4805 .unwrap();
4806 fail_next_catalog_persist_at(1);
4807 let error = before
4808 .drop_expression_index("Doc", before_index_id)
4809 .unwrap_err();
4810 assert!(error.to_string().contains("before rename"));
4811 assert!(before
4812 .expression_index_btree("Doc", before_index_id)
4813 .is_some());
4814 assert!(before_dir
4815 .path()
4816 .join(expression_index_file_name("Doc", 1))
4817 .exists());
4818 std::mem::forget(before);
4819 let before_reopened = Catalog::open(before_dir.path()).unwrap();
4820 assert!(before_reopened
4821 .expression_index_btree("Doc", before_index_id)
4822 .is_some());
4823
4824 let after_dir = tempfile::tempdir().unwrap();
4825 let mut after = Catalog::create(after_dir.path()).unwrap();
4826 after
4827 .create_table(Schema {
4828 table_name: "Doc".into(),
4829 columns: vec![ColumnDef {
4830 name: "data".into(),
4831 type_id: TypeId::Json,
4832 required: false,
4833 position: 0,
4834 }],
4835 })
4836 .unwrap();
4837 fail_next_catalog_persist_at(2);
4838 let index_id = after
4839 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path.clone(), false)
4840 .unwrap();
4841 assert_eq!(index_id, 1);
4842 assert_eq!(
4843 after.active_catalog_version(),
4844 EXPRESSION_INDEX_CATALOG_VERSION
4845 );
4846 assert_eq!(after.next_index_id(), 2);
4847 assert!(after.expression_index_btree("Doc", index_id).is_some());
4848 assert!(after_dir
4849 .path()
4850 .join(expression_index_file_name("Doc", 1))
4851 .exists());
4852 std::mem::forget(after);
4853
4854 let mut reopened = Catalog::open(after_dir.path()).unwrap();
4855 assert!(reopened.expression_index_btree("Doc", index_id).is_some());
4856 fail_next_catalog_persist_at(2);
4857 reopened.drop_expression_index("Doc", index_id).unwrap();
4858 assert!(reopened
4859 .expression_index_metadata("Doc")
4860 .unwrap()
4861 .is_empty());
4862 assert!(!after_dir
4863 .path()
4864 .join(expression_index_file_name("Doc", 1))
4865 .exists());
4866 std::mem::forget(reopened);
4867
4868 let final_open = Catalog::open(after_dir.path()).unwrap();
4869 assert!(final_open
4870 .expression_index_metadata("Doc")
4871 .unwrap()
4872 .is_empty());
4873 }
4874
4875 #[test]
4879 fn first_create_link_rolls_back_version_and_registry_before_rename() {
4880 let dir = tempfile::tempdir().unwrap();
4881 let mut catalog = Catalog::create(dir.path()).unwrap();
4882 catalog
4883 .create_table(Schema {
4884 table_name: "Order".into(),
4885 columns: vec![ColumnDef {
4886 name: "user_id".into(),
4887 type_id: TypeId::Int,
4888 required: false,
4889 position: 0,
4890 }],
4891 })
4892 .unwrap();
4893 catalog
4894 .create_table(Schema {
4895 table_name: "User".into(),
4896 columns: vec![ColumnDef {
4897 name: "id".into(),
4898 type_id: TypeId::Int,
4899 required: true,
4900 position: 0,
4901 }],
4902 })
4903 .unwrap();
4904 catalog.create_index_unique("User", "id", true).unwrap();
4905
4906 let version_before = catalog.active_catalog_version();
4907 assert_eq!(version_before, LEGACY_CATALOG_VERSION);
4908
4909 fail_next_catalog_persist_at(1);
4910 let error = catalog
4911 .create_link(LinkDef {
4912 owner_type: "Order".into(),
4913 name: "user".into(),
4914 target_type: "User".into(),
4915 local_key: "user_id".into(),
4916 target_key: "id".into(),
4917 kind: LinkKind::ToMany,
4918 })
4919 .unwrap_err();
4920 assert!(error.to_string().contains("before rename"));
4921 assert_eq!(catalog.active_catalog_version(), version_before);
4923 assert!(catalog.link("Order", "user").is_none());
4924 assert_eq!(catalog.links().count(), 0);
4925 assert_eq!(
4926 read_active_catalog_version(dir.path()).unwrap(),
4927 version_before
4928 );
4929
4930 catalog
4932 .create_link(LinkDef {
4933 owner_type: "Order".into(),
4934 name: "user".into(),
4935 target_type: "User".into(),
4936 local_key: "user_id".into(),
4937 target_key: "id".into(),
4938 kind: LinkKind::ToMany, })
4940 .unwrap();
4941 assert_eq!(catalog.active_catalog_version(), CATALOG_VERSION);
4942 assert_eq!(catalog.link("Order", "user").unwrap().kind, LinkKind::ToOne);
4943 }
4944
4945 #[test]
4946 fn ordinary_catalog_persist_reports_post_rename_directory_sync_failure() {
4947 let dir = tempfile::tempdir().unwrap();
4948 let mut catalog = Catalog::create(dir.path()).unwrap();
4949 fail_next_catalog_persist_at(2);
4950 let error = catalog
4951 .create_table(Schema {
4952 table_name: "VisibleAfterRename".into(),
4953 columns: vec![ColumnDef {
4954 name: "id".into(),
4955 type_id: TypeId::Int,
4956 required: true,
4957 position: 0,
4958 }],
4959 })
4960 .unwrap_err();
4961 assert!(error.to_string().contains("after rename"));
4962 assert!(catalog.schema("VisibleAfterRename").is_some());
4963
4964 std::mem::forget(catalog);
4965 let reopened = Catalog::open(dir.path()).unwrap();
4966 assert!(reopened.schema("VisibleAfterRename").is_some());
4967 }
4968
4969 fn schema_two_cols() -> Schema {
4970 Schema {
4971 table_name: "T".into(),
4972 columns: vec![
4973 ColumnDef {
4974 name: "id".into(),
4975 type_id: TypeId::Int,
4976 required: true,
4977 position: 0,
4978 },
4979 ColumnDef {
4980 name: "status".into(),
4981 type_id: TypeId::Str,
4982 required: false,
4983 position: 1,
4984 },
4985 ],
4986 }
4987 }
4988
4989 #[test]
4990 fn replay_records_treats_reused_tx_ids_as_ordered_spans() {
4991 let mut cat = temp_catalog("reused_tx_ids");
4992 let schema = schema_two_cols();
4993 cat.create_table(schema.clone()).unwrap();
4994 cat.checkpoint().unwrap();
4995
4996 let mut committed_row = Vec::new();
4997 encode_row_into(
4998 &schema,
4999 &[Value::Int(1), Value::Str("committed".into())],
5000 &mut committed_row,
5001 );
5002 let mut incomplete_row = Vec::new();
5003 encode_row_into(
5004 &schema,
5005 &[Value::Int(2), Value::Str("incomplete".into())],
5006 &mut incomplete_row,
5007 );
5008
5009 let records = vec![
5010 WalRecord {
5011 tx_id: 1,
5012 record_type: WalRecordType::Begin,
5013 lsn: 1,
5014 data: Vec::new(),
5015 },
5016 WalRecord {
5017 tx_id: 1,
5018 record_type: WalRecordType::Insert,
5019 lsn: 2,
5020 data: encode_wal_payload(
5021 "T",
5022 RowId {
5023 page_id: 1,
5024 slot_index: 0,
5025 },
5026 &committed_row,
5027 ),
5028 },
5029 WalRecord {
5030 tx_id: 1,
5031 record_type: WalRecordType::Commit,
5032 lsn: 3,
5033 data: Vec::new(),
5034 },
5035 WalRecord {
5036 tx_id: 1,
5037 record_type: WalRecordType::Begin,
5038 lsn: 4,
5039 data: Vec::new(),
5040 },
5041 WalRecord {
5042 tx_id: 1,
5043 record_type: WalRecordType::Insert,
5044 lsn: 5,
5045 data: encode_wal_payload(
5046 "T",
5047 RowId {
5048 page_id: 1,
5049 slot_index: 1,
5050 },
5051 &incomplete_row,
5052 ),
5053 },
5054 ];
5055
5056 cat.apply_wal_records(&records).unwrap();
5057 let rows: Vec<_> = cat.scan("T").unwrap().collect();
5058 assert_eq!(rows.len(), 1);
5059 assert_eq!(rows[0].1[0], Value::Int(1));
5060 assert_eq!(rows[0].1[1], Value::Str("committed".into()));
5061 }
5062
5063 #[test]
5064 fn ddl_create_table_codec_roundtrips_defaults_and_auto() {
5065 let schema = schema_two_cols();
5066 let defaults = vec![None, Some(Value::Str("active".into()))];
5067 let auto_cols = vec![true, false];
5068 let encoded = encode_ddl_create_table(&schema, &defaults, &auto_cols);
5069 let (decoded_schema, decoded_defaults, decoded_auto) =
5070 decode_ddl_create_table(&encoded).unwrap();
5071 assert_eq!(decoded_schema.columns.len(), 2);
5072 assert_eq!(decoded_defaults, defaults);
5073 assert_eq!(decoded_auto, auto_cols);
5074 }
5075
5076 #[test]
5077 fn ddl_create_table_codec_back_compat_without_trailing_sections() {
5078 let schema = schema_two_cols();
5082 let full = encode_ddl_create_table(&schema, &[], &[]);
5083 let legacy = &full[..full.len() - 4];
5086 let (decoded_schema, decoded_defaults, decoded_auto) =
5087 decode_ddl_create_table(legacy).unwrap();
5088 assert_eq!(decoded_schema.columns.len(), 2);
5089 assert!(decoded_defaults.is_empty(), "no defaults section -> empty");
5090 assert!(decoded_auto.is_empty(), "no auto section -> empty");
5091 }
5092
5093 #[test]
5094 fn ddl_create_table_codec_back_compat_defaults_but_no_auto() {
5095 let schema = schema_two_cols();
5098 let defaults = vec![None, Some(Value::Str("active".into()))];
5099 let full = encode_ddl_create_table(&schema, &defaults, &[]);
5100 let legacy = &full[..full.len() - 2];
5102 let (_schema, decoded_defaults, decoded_auto) = decode_ddl_create_table(legacy).unwrap();
5103 assert_eq!(decoded_defaults, defaults);
5104 assert!(decoded_auto.is_empty());
5105 }
5106
5107 #[test]
5108 fn read_catalog_file_accepts_intermediate_versions_3_and_4() {
5109 use std::io::Write as _;
5117 fn write_legacy_catalog(path: &std::path::Path, version: u16) {
5118 let mut buf: Vec<u8> = Vec::new();
5119 buf.extend_from_slice(CATALOG_MAGIC);
5120 buf.extend_from_slice(&version.to_le_bytes());
5121 buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes());
5124 buf.extend_from_slice(b"T");
5125 buf.extend_from_slice(&2u16.to_le_bytes()); buf.extend_from_slice(&2u32.to_le_bytes());
5128 buf.extend_from_slice(b"id");
5129 buf.push(TypeId::Int as u8);
5130 buf.push(1);
5131 buf.extend_from_slice(&0u16.to_le_bytes());
5132 buf.extend_from_slice(&6u32.to_le_bytes());
5134 buf.extend_from_slice(b"status");
5135 buf.push(TypeId::Str as u8);
5136 buf.push(0);
5137 buf.extend_from_slice(&1u16.to_le_bytes());
5138 buf.extend_from_slice(&0u16.to_le_bytes());
5140 if version >= 4 {
5142 encode_defaults_section(&mut buf, &[None, None]);
5143 }
5144 let crc = crc32fast::hash(&buf);
5146 buf.extend_from_slice(&crc.to_le_bytes());
5147 let mut f = fs::File::create(path).unwrap();
5148 f.write_all(&buf).unwrap();
5149 }
5150
5151 for version in [3u16, 4u16] {
5152 let path = std::env::temp_dir().join(format!(
5153 "powdb_cat_v{version}_compat_{}.bin",
5154 std::process::id()
5155 ));
5156 write_legacy_catalog(&path, version);
5157 let catalog_file = read_catalog_file(&path)
5158 .unwrap_or_else(|e| panic!("version {version} catalog must load, got: {e}"));
5159 let entries = catalog_file.entries;
5160 assert_eq!(entries.len(), 1);
5161 assert_eq!(entries[0].schema.table_name, "T");
5162 assert_eq!(entries[0].schema.columns.len(), 2);
5163 assert!(
5164 entries[0].auto_cols.is_empty(),
5165 "v{version} has no auto cols"
5166 );
5167 fs::remove_file(&path).ok();
5168 }
5169 }
5170
5171 #[test]
5172 fn read_catalog_file_rejects_implausible_table_count() {
5173 use std::io::Write as _;
5181 let mut buf: Vec<u8> = Vec::new();
5182 buf.extend_from_slice(CATALOG_MAGIC);
5183 buf.extend_from_slice(&CATALOG_VERSION.to_le_bytes());
5184 buf.extend_from_slice(&1000u32.to_le_bytes()); buf.extend_from_slice(&1u64.to_le_bytes()); let crc = crc32fast::hash(&buf);
5188 buf.extend_from_slice(&crc.to_le_bytes());
5189 let path =
5190 std::env::temp_dir().join(format!("powdb_cat_badcount_{}.bin", std::process::id()));
5191 fs::File::create(&path).unwrap().write_all(&buf).unwrap();
5192
5193 let msg = match read_catalog_file(&path) {
5194 Ok(_) => panic!("implausible table count must be rejected, got Ok"),
5195 Err(e) => e.to_string(),
5196 };
5197 assert!(
5198 msg.contains("implausible table count"),
5199 "expected an implausible-table-count error, got: {msg}"
5200 );
5201 fs::remove_file(&path).ok();
5202 }
5203
5204 #[test]
5205 fn data_dir_and_max_lsn_accessors() {
5206 let dir = std::env::temp_dir().join(format!("powdb_cat_maxlsn_{}", std::process::id()));
5207 let mut cat = Catalog::create(&dir).unwrap();
5208
5209 assert_eq!(cat.data_dir(), dir.as_path());
5211
5212 assert_eq!(cat.max_lsn(), 0);
5214
5215 let schema = Schema {
5216 table_name: "users".into(),
5217 columns: vec![ColumnDef {
5218 name: "name".into(),
5219 type_id: TypeId::Str,
5220 required: true,
5221 position: 0,
5222 }],
5223 };
5224 cat.create_table(schema).unwrap();
5225
5226 cat.insert("users", &vec![Value::Str("Alice".into())])
5227 .unwrap();
5228 cat.sync_wal().unwrap();
5229
5230 assert!(cat.max_lsn() > 0);
5233 }
5234
5235 #[test]
5236 fn test_create_table_and_insert() {
5237 let mut cat = temp_catalog("basic");
5238 let schema = Schema {
5239 table_name: "users".into(),
5240 columns: vec![
5241 ColumnDef {
5242 name: "name".into(),
5243 type_id: TypeId::Str,
5244 required: true,
5245 position: 0,
5246 },
5247 ColumnDef {
5248 name: "age".into(),
5249 type_id: TypeId::Int,
5250 required: false,
5251 position: 1,
5252 },
5253 ],
5254 };
5255 cat.create_table(schema).unwrap();
5256
5257 let row = vec![Value::Str("Alice".into()), Value::Int(30)];
5258 let rid = cat.insert("users", &row).unwrap();
5259
5260 let result = cat.get("users", rid).unwrap();
5261 assert_eq!(result[0], Value::Str("Alice".into()));
5262 assert_eq!(result[1], Value::Int(30));
5263 }
5264
5265 #[test]
5266 fn test_scan_table() {
5267 let mut cat = temp_catalog("scan");
5268 let schema = Schema {
5269 table_name: "items".into(),
5270 columns: vec![
5271 ColumnDef {
5272 name: "name".into(),
5273 type_id: TypeId::Str,
5274 required: true,
5275 position: 0,
5276 },
5277 ColumnDef {
5278 name: "price".into(),
5279 type_id: TypeId::Float,
5280 required: true,
5281 position: 1,
5282 },
5283 ],
5284 };
5285 cat.create_table(schema).unwrap();
5286
5287 for i in 0..50 {
5288 cat.insert(
5289 "items",
5290 &vec![
5291 Value::Str(format!("item_{i}")),
5292 Value::Float(i as f64 * 1.5),
5293 ],
5294 )
5295 .unwrap();
5296 }
5297
5298 let rows: Vec<_> = cat.scan("items").unwrap().collect();
5299 assert_eq!(rows.len(), 50);
5300 }
5301
5302 #[test]
5303 fn test_index_lookup() {
5304 let mut cat = temp_catalog("idx");
5305 let schema = Schema {
5306 table_name: "users".into(),
5307 columns: vec![
5308 ColumnDef {
5309 name: "email".into(),
5310 type_id: TypeId::Str,
5311 required: true,
5312 position: 0,
5313 },
5314 ColumnDef {
5315 name: "name".into(),
5316 type_id: TypeId::Str,
5317 required: true,
5318 position: 1,
5319 },
5320 ],
5321 };
5322 cat.create_table(schema).unwrap();
5323 cat.create_index("users", "email").unwrap();
5324
5325 cat.insert(
5326 "users",
5327 &vec![
5328 Value::Str("alice@example.com".into()),
5329 Value::Str("Alice".into()),
5330 ],
5331 )
5332 .unwrap();
5333 cat.insert(
5334 "users",
5335 &vec![
5336 Value::Str("bob@example.com".into()),
5337 Value::Str("Bob".into()),
5338 ],
5339 )
5340 .unwrap();
5341
5342 let result = cat
5343 .index_lookup("users", "email", &Value::Str("bob@example.com".into()))
5344 .unwrap();
5345 assert!(result.is_some());
5346 let row = result.unwrap();
5347 assert_eq!(row[1], Value::Str("Bob".into()));
5348 }
5349
5350 #[test]
5351 fn test_delete_row() {
5352 let mut cat = temp_catalog("delete");
5353 let schema = Schema {
5354 table_name: "t".into(),
5355 columns: vec![ColumnDef {
5356 name: "v".into(),
5357 type_id: TypeId::Int,
5358 required: true,
5359 position: 0,
5360 }],
5361 };
5362 cat.create_table(schema).unwrap();
5363 let r1 = cat.insert("t", &vec![Value::Int(1)]).unwrap();
5364 let r2 = cat.insert("t", &vec![Value::Int(2)]).unwrap();
5365 cat.delete("t", r1).unwrap();
5366 assert!(cat.get("t", r1).is_none());
5367 assert!(cat.get("t", r2).is_some());
5368 }
5369
5370 #[test]
5371 fn test_update_row() {
5372 let mut cat = temp_catalog("update");
5373 let schema = Schema {
5374 table_name: "t".into(),
5375 columns: vec![ColumnDef {
5376 name: "v".into(),
5377 type_id: TypeId::Int,
5378 required: true,
5379 position: 0,
5380 }],
5381 };
5382 cat.create_table(schema).unwrap();
5383 let rid = cat.insert("t", &vec![Value::Int(1)]).unwrap();
5384 let new_rid = cat.update("t", rid, &vec![Value::Int(99)]).unwrap();
5385 let row = cat.get("t", new_rid).unwrap();
5386 assert_eq!(row[0], Value::Int(99));
5387 }
5388
5389 #[test]
5390 fn test_persist_and_reopen() {
5391 let dir = std::env::temp_dir().join(format!("powdb_cat_persist_{}", std::process::id()));
5392 let _ = std::fs::remove_dir_all(&dir);
5394
5395 {
5396 let mut cat = Catalog::create(&dir).unwrap();
5397 cat.create_table(Schema {
5398 table_name: "users".into(),
5399 columns: vec![
5400 ColumnDef {
5401 name: "name".into(),
5402 type_id: TypeId::Str,
5403 required: true,
5404 position: 0,
5405 },
5406 ColumnDef {
5407 name: "age".into(),
5408 type_id: TypeId::Int,
5409 required: false,
5410 position: 1,
5411 },
5412 ],
5413 })
5414 .unwrap();
5415 cat.insert("users", &vec![Value::Str("Alice".into()), Value::Int(30)])
5416 .unwrap();
5417 cat.insert("users", &vec![Value::Str("Bob".into()), Value::Int(25)])
5418 .unwrap();
5419 }
5420
5421 let cat = Catalog::open(&dir).unwrap();
5423 let schema = cat.schema("users").unwrap();
5424 assert_eq!(schema.columns.len(), 2);
5425 assert_eq!(schema.columns[0].name, "name");
5426 assert_eq!(schema.columns[0].type_id, TypeId::Str);
5427 assert_eq!(schema.columns[1].type_id, TypeId::Int);
5428
5429 let rows: Vec<_> = cat.scan("users").unwrap().collect();
5430 assert_eq!(rows.len(), 2);
5431
5432 std::fs::remove_dir_all(&dir).ok();
5433 }
5434
5435 #[test]
5436 fn test_open_missing_dir_errors() {
5437 let dir = std::env::temp_dir().join(format!("powdb_cat_missing_{}", std::process::id()));
5438 let _ = std::fs::remove_dir_all(&dir);
5439 std::fs::create_dir_all(&dir).unwrap();
5440 assert!(Catalog::open(&dir).is_err());
5442 std::fs::remove_dir_all(&dir).ok();
5443 }
5444
5445 #[test]
5446 fn test_list_tables() {
5447 let mut cat = temp_catalog("list");
5448 cat.create_table(Schema {
5449 table_name: "a".into(),
5450 columns: vec![ColumnDef {
5451 name: "x".into(),
5452 type_id: TypeId::Int,
5453 required: true,
5454 position: 0,
5455 }],
5456 })
5457 .unwrap();
5458 cat.create_table(Schema {
5459 table_name: "b".into(),
5460 columns: vec![ColumnDef {
5461 name: "y".into(),
5462 type_id: TypeId::Int,
5463 required: true,
5464 position: 0,
5465 }],
5466 })
5467 .unwrap();
5468 let mut tables = cat.list_tables();
5469 tables.sort();
5470 assert_eq!(tables, vec!["a", "b"]);
5471 }
5472
5473 #[test]
5474 fn test_path_traversal_table_name_rejected() {
5475 let mut cat = temp_catalog("path_trav");
5476 let bad_names = vec![
5478 "../etc/passwd",
5479 "foo/bar",
5480 "table\0name",
5481 "",
5482 "123starts_with_digit",
5483 "has-dashes",
5484 "has spaces",
5485 "has.dots",
5486 ];
5487 for name in bad_names {
5488 let schema = Schema {
5489 table_name: name.into(),
5490 columns: vec![ColumnDef {
5491 name: "x".into(),
5492 type_id: TypeId::Int,
5493 required: true,
5494 position: 0,
5495 }],
5496 };
5497 let result = cat.create_table(schema);
5498 assert!(result.is_err(), "expected error for table name '{name}'");
5499 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
5500 }
5501 let good_names = vec!["users", "_private", "Table_123", "_"];
5503 for name in good_names {
5504 let schema = Schema {
5505 table_name: name.into(),
5506 columns: vec![ColumnDef {
5507 name: "x".into(),
5508 type_id: TypeId::Int,
5509 required: true,
5510 position: 0,
5511 }],
5512 };
5513 assert!(
5514 cat.create_table(schema).is_ok(),
5515 "expected ok for table name '{name}'"
5516 );
5517 }
5518 }
5519
5520 #[test]
5521 fn test_path_traversal_column_name_rejected() {
5522 let mut cat = temp_catalog("col_path_trav");
5523 let schema = Schema {
5524 table_name: "valid_table".into(),
5525 columns: vec![ColumnDef {
5526 name: "../bad".into(),
5527 type_id: TypeId::Int,
5528 required: true,
5529 position: 0,
5530 }],
5531 };
5532 let result = cat.create_table(schema);
5533 assert!(result.is_err());
5534 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
5535 }
5536
5537 #[test]
5538 fn test_drop_table_validates_name() {
5539 let mut cat = temp_catalog("drop_trav");
5540 let result = cat.drop_table("../etc/passwd");
5541 assert!(result.is_err());
5542 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
5544 }
5545
5546 fn ddl_guard_schema(name: &str) -> Schema {
5548 Schema {
5549 table_name: name.into(),
5550 columns: vec![
5551 ColumnDef {
5552 name: "id".into(),
5553 type_id: TypeId::Int,
5554 required: true,
5555 position: 0,
5556 },
5557 ColumnDef {
5558 name: "label".into(),
5559 type_id: TypeId::Str,
5560 required: false,
5561 position: 1,
5562 },
5563 ],
5564 }
5565 }
5566
5567 fn seed_ddl_guard_catalog(dir: &std::path::Path) -> Catalog {
5568 let mut cat = Catalog::create(dir).unwrap();
5569 cat.create_table(ddl_guard_schema("Keep")).unwrap();
5570 cat.create_index_unique("Keep", "id", true).unwrap();
5571 cat.insert("Keep", &vec![Value::Int(1), Value::Str("one".into())])
5572 .unwrap();
5573 cat.sync_wal().unwrap();
5574 cat
5575 }
5576
5577 fn assert_refused_in_transaction(result: io::Result<()>, verb: &str) {
5578 let err = result.unwrap_err();
5579 assert_eq!(
5580 err.kind(),
5581 io::ErrorKind::InvalidInput,
5582 "{verb} inside a transaction must be refused as InvalidInput"
5583 );
5584 let message = err.to_string();
5585 assert!(
5586 message.starts_with("cannot ") && message.contains("explicit transaction"),
5587 "{verb} refusal must name the active transaction, got: {message}"
5588 );
5589 }
5590
5591 #[test]
5592 fn drop_table_inside_transaction_is_refused_and_rollback_keeps_data() {
5593 let dir = tempfile::tempdir().unwrap();
5594 let mut cat = seed_ddl_guard_catalog(dir.path());
5595 cat.checkpoint().unwrap();
5599
5600 cat.begin_transaction().unwrap();
5601 assert_refused_in_transaction(cat.drop_table("Keep"), "drop table");
5602 cat.rollback_to_last_sync().unwrap();
5603
5604 let rows: Vec<_> = cat.scan("Keep").unwrap().collect();
5605 assert_eq!(rows.len(), 1, "rolled-back DROP must not destroy data");
5606 assert_eq!(rows[0].1[0], Value::Int(1));
5607
5608 drop(cat);
5610 let reopened = Catalog::open(dir.path()).unwrap();
5611 assert_eq!(reopened.scan("Keep").unwrap().count(), 1);
5612 }
5613
5614 #[test]
5615 fn every_ddl_verb_inside_transaction_is_refused() {
5616 let dir = tempfile::tempdir().unwrap();
5617 let mut cat = seed_ddl_guard_catalog(dir.path());
5618 cat.create_table(ddl_guard_schema("Other")).unwrap();
5619 cat.create_index_unique("Other", "id", true).unwrap();
5620 cat.checkpoint().unwrap();
5621
5622 cat.begin_transaction().unwrap();
5623
5624 assert_refused_in_transaction(cat.create_table(ddl_guard_schema("New")), "create table");
5625 assert_refused_in_transaction(cat.drop_table("Keep"), "drop table");
5626 assert_refused_in_transaction(
5627 cat.alter_table_add_column(
5628 "Keep",
5629 ColumnDef {
5630 name: "extra".into(),
5631 type_id: TypeId::Int,
5632 required: false,
5633 position: 2,
5634 },
5635 ),
5636 "alter table add column",
5637 );
5638 assert_refused_in_transaction(
5639 cat.alter_table_drop_column("Keep", "label"),
5640 "alter table drop column",
5641 );
5642 assert_refused_in_transaction(cat.create_index("Keep", "label"), "create index");
5643 assert_refused_in_transaction(
5644 cat.create_index_unique("Keep", "label", true),
5645 "create unique index",
5646 );
5647 assert_refused_in_transaction(
5648 cat.create_link(LinkDef {
5649 owner_type: "Other".into(),
5650 name: "keep".into(),
5651 target_type: "Keep".into(),
5652 local_key: "id".into(),
5653 target_key: "id".into(),
5654 kind: LinkKind::ToOne,
5655 }),
5656 "create link",
5657 );
5658 assert_refused_in_transaction(cat.drop_link("Other", "keep"), "drop link");
5659
5660 cat.rollback_to_last_sync().unwrap();
5661
5662 let mut tables = cat.list_tables();
5664 tables.sort_unstable();
5665 assert_eq!(tables, vec!["Keep", "Other"]);
5666 let schema = cat.schema("Keep").unwrap();
5667 assert_eq!(schema.columns.len(), 2);
5668 assert!(!cat.has_index("Keep", "label"));
5669 assert!(cat.links().next().is_none());
5670 assert_eq!(cat.scan("Keep").unwrap().count(), 1);
5671 }
5672
5673 #[test]
5674 fn ddl_still_works_after_commit_and_rollback() {
5675 let dir = tempfile::tempdir().unwrap();
5676 let mut cat = seed_ddl_guard_catalog(dir.path());
5677
5678 cat.begin_transaction().unwrap();
5679 assert!(cat.drop_table("Keep").is_err());
5680 cat.rollback_to_last_sync().unwrap();
5681 cat.create_table(ddl_guard_schema("AfterRollback")).unwrap();
5682
5683 cat.begin_transaction().unwrap();
5684 cat.insert("Keep", &vec![Value::Int(2), Value::Str("two".into())])
5685 .unwrap();
5686 cat.commit_transaction().unwrap();
5687 cat.drop_table("AfterRollback").unwrap();
5688
5689 let mut tables = cat.list_tables();
5690 tables.sort_unstable();
5691 assert_eq!(tables, vec!["Keep"]);
5692 assert_eq!(cat.scan("Keep").unwrap().count(), 2);
5693 }
5694
5695 #[test]
5696 fn transaction_over_dirty_page_budget_is_refused_and_catalog_stays_usable() {
5697 let dir = tempfile::tempdir().unwrap();
5698 let mut cat = Catalog::create(dir.path()).unwrap();
5699 cat.create_table(ddl_guard_schema("Big")).unwrap();
5700 cat.set_dirty_page_budget_bytes(8 * crate::page::PAGE_SIZE);
5703 assert_eq!(cat.dirty_page_budget_bytes(), 8 * crate::page::PAGE_SIZE);
5704
5705 cat.begin_transaction().unwrap();
5706 let mut refusal = None;
5707 for i in 0..100_000i64 {
5708 let row = vec![Value::Int(i), Value::Str(format!("row-{i:06}"))];
5709 if let Err(e) = cat.insert("Big", &row) {
5710 refusal = Some(e);
5711 break;
5712 }
5713 }
5714 let err = refusal.expect("an 8-page budget must refuse an unbounded transaction");
5715 let typed = err
5716 .get_ref()
5717 .and_then(|source| source.downcast_ref::<StorageError>());
5718 assert!(
5719 matches!(typed, Some(StorageError::TransactionTooLarge { .. })),
5720 "expected a typed TransactionTooLarge, got: {err}"
5721 );
5722 assert!(cat.dirty_pages_buffered() <= 8);
5723
5724 cat.rollback_to_last_sync().unwrap();
5726 assert_eq!(cat.dirty_page_budget_bytes(), 8 * crate::page::PAGE_SIZE);
5727 assert_eq!(cat.scan("Big").unwrap().count(), 0);
5728 cat.insert("Big", &vec![Value::Int(1), Value::Str("after".into())])
5729 .unwrap();
5730 assert_eq!(cat.scan("Big").unwrap().count(), 1);
5731 }
5732
5733 #[test]
5743 fn drop_table_persists_the_catalog_before_unlinking_the_heap() {
5744 let dir = tempfile::tempdir().unwrap();
5745 let mut cat = Catalog::create(dir.path()).unwrap();
5746 cat.create_table(ddl_guard_schema("Gone")).unwrap();
5747 cat.insert("Gone", &vec![Value::Int(1), Value::Str("row".into())])
5748 .unwrap();
5749 cat.checkpoint().unwrap();
5750
5751 let heap_path = dir.path().join("Gone.heap");
5752 assert!(heap_path.exists());
5753
5754 fail_next_catalog_persist_at(1);
5755 let error = cat.drop_table("Gone").unwrap_err();
5756 assert!(
5757 error.to_string().contains("before rename"),
5758 "expected the injected pre-rename persist failure, got: {error}"
5759 );
5760 assert!(
5761 heap_path.exists(),
5762 "the heap must not be unlinked until the catalog no longer names it"
5763 );
5764
5765 std::mem::forget(cat);
5766 let reopened = Catalog::open(dir.path()).unwrap();
5770 assert!(reopened.schema("Gone").is_none());
5771 assert!(!heap_path.exists());
5772 }
5773
5774 #[test]
5775 fn autocommit_writes_are_not_capped_by_the_dirty_page_budget() {
5776 let dir = tempfile::tempdir().unwrap();
5777 let mut cat = Catalog::create(dir.path()).unwrap();
5778 cat.create_table(ddl_guard_schema("Bulk")).unwrap();
5779 cat.set_dirty_page_budget_bytes(8 * crate::page::PAGE_SIZE);
5780
5781 for i in 0..5_000i64 {
5784 let row = vec![Value::Int(i), Value::Str(format!("row-{i:06}"))];
5785 cat.insert("Bulk", &row).unwrap();
5786 }
5787 assert!(cat.dirty_pages_buffered() <= 8);
5788 assert_eq!(cat.scan("Bulk").unwrap().count(), 5_000);
5789 }
5790}