1use crate::btree::{BTree, IndexStats};
2use crate::error::StorageError;
3use crate::heap::{DirtyPageBudget, HeapFile};
4use crate::page::{OVERFLOW_CHAIN_END, OVERFLOW_PAYLOAD_CAP};
5use crate::row::{encode_row_into, encode_row_v2_into, plan_spill, OverflowStub, MAX_VALUE_SIZE};
6use crate::stored_json_path::{StoredJsonPathSegmentV1, StoredJsonPathV1};
7use crate::table::Table;
8use crate::types::*;
9use crate::wal::{Wal, WalDurabilityTicket, WalRecord, WalRecordType, WalSyncMode};
10use rustc_hash::FxHashMap;
11use std::fs;
12use std::io::{self, Read, Write};
13use std::path::{Path, PathBuf};
14use std::sync::atomic::{AtomicU64, Ordering};
15use 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)]
123pub enum LinkKind {
124 ToOne,
127 ToMany,
130}
131
132impl LinkKind {
133 fn to_u8(self) -> u8 {
134 match self {
135 LinkKind::ToOne => 0,
136 LinkKind::ToMany => 1,
137 }
138 }
139
140 fn from_u8(tag: u8) -> io::Result<Self> {
141 match tag {
142 0 => Ok(LinkKind::ToOne),
143 1 => Ok(LinkKind::ToMany),
144 other => Err(io::Error::new(
145 io::ErrorKind::InvalidData,
146 format!("unknown link kind tag: {other}"),
147 )),
148 }
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct LinkDef {
159 pub owner_type: String,
161 pub name: String,
163 pub target_type: String,
165 pub local_key: String,
167 pub target_key: String,
169 pub kind: LinkKind,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq)]
174pub enum IndexKeySource {
175 Column {
176 column: String,
177 },
178 Expression {
179 index_id: u64,
180 canonical_version: u16,
181 canonical_text: String,
182 json_path: StoredJsonPathV1,
183 },
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct IndexMetadata {
188 pub unique: bool,
189 pub source: IndexKeySource,
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub enum IndexOrderDirection {
194 Asc,
195 Desc,
196}
197
198pub fn expression_index_file_name(table: &str, index_id: u64) -> String {
203 format!("{table}_{index_id}.eidx")
204}
205
206const WAL_FILE: &str = "wal.log";
209const SYNC_STATE_DIR: &str = ".powdb-sync";
210const SYNC_IDENTITY_FILE: &str = "identity.json";
211
212const WAL_BATCH_SIZE: usize = 64;
216type WalArchiveCallback<'a> = &'a mut dyn FnMut(&Path, &[WalRecord]) -> io::Result<()>;
217
218fn read_durable_lsn(data_dir: &Path) -> io::Result<u64> {
219 let path = data_dir.join(CATALOG_LSN_FILE);
220 let bytes = match fs::read(path) {
221 Ok(bytes) => bytes,
222 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(0),
223 Err(err) => return Err(err),
224 };
225 if bytes.len() != 8 {
226 return Err(io::Error::new(
227 io::ErrorKind::InvalidData,
228 "catalog LSN sidecar has invalid length",
229 ));
230 }
231 let mut buf = [0u8; 8];
232 buf.copy_from_slice(&bytes);
233 Ok(u64::from_le_bytes(buf))
234}
235
236fn write_durable_lsn(data_dir: &Path, lsn: u64) -> io::Result<()> {
237 let path = data_dir.join(CATALOG_LSN_FILE);
238 let tmp_path = data_dir.join(format!("{CATALOG_LSN_FILE}.tmp"));
239 let mut file = fs::File::create(&tmp_path)?;
240 file.write_all(&lsn.to_le_bytes())?;
241 file.sync_all()?;
242 drop(file);
243 fs::rename(&tmp_path, &path)?;
244 sync_directory(data_dir)?;
245 Ok(())
246}
247
248#[cfg(unix)]
249fn sync_directory(path: &Path) -> io::Result<()> {
250 fs::File::open(path)?.sync_all()
251}
252
253#[cfg(not(unix))]
254fn sync_directory(path: &Path) -> io::Result<()> {
255 let _ = path;
256 Ok(())
257}
258
259#[cfg(test)]
260thread_local! {
261 static CATALOG_PERSIST_FAILPOINT: std::cell::Cell<u8> = const { std::cell::Cell::new(0) };
262}
263
264#[cfg(test)]
265fn take_catalog_persist_failpoint(stage: u8) -> bool {
266 CATALOG_PERSIST_FAILPOINT.with(|failpoint| {
267 if failpoint.get() == stage {
268 failpoint.set(0);
269 true
270 } else {
271 false
272 }
273 })
274}
275
276enum CatalogPersistError {
277 BeforeActivation(io::Error),
278 AfterActivation(io::Error),
279}
280
281impl CatalogPersistError {
282 fn into_io_error(self) -> io::Error {
283 match self {
284 Self::BeforeActivation(error) | Self::AfterActivation(error) => error,
285 }
286 }
287}
288
289fn max_record_lsn(records: &[WalRecord]) -> Option<u64> {
290 records.iter().map(|record| record.lsn).max()
291}
292
293pub struct Catalog {
305 tables: Vec<Table>,
307 name_to_slot: FxHashMap<String, usize>,
310 data_dir: PathBuf,
311 wal: Wal,
316 next_tx_id: u64,
320 active_tx_id: Option<u64>,
323 tx_start_len: Option<u64>,
326 pending_autocommit_tx_ids: Vec<u64>,
329 checkpointed: bool,
333 durable_lsn: u64,
336 pending_free_overflow: Vec<(usize, Vec<u32>)>,
345 active_catalog_version: u16,
348 next_index_id: u64,
350 links: Vec<LinkDef>,
356 structure_generation: u64,
361 read_only: bool,
366 dirty_budget: Arc<DirtyPageBudget>,
370}
371
372impl Catalog {
373 pub fn create(data_dir: &Path) -> io::Result<Self> {
394 crate::create_data_dir_secure(data_dir)?;
395 let wal_path = data_dir.join(WAL_FILE);
396 let wal = Wal::create(&wal_path, WAL_BATCH_SIZE)?;
397 let cat = Catalog {
398 tables: Vec::new(),
399 name_to_slot: FxHashMap::default(),
400 data_dir: data_dir.to_path_buf(),
401 wal,
402 next_tx_id: 1,
403 active_tx_id: None,
404 tx_start_len: None,
405 pending_autocommit_tx_ids: Vec::new(),
406 pending_free_overflow: Vec::new(),
407 checkpointed: false,
408 durable_lsn: 0,
409 active_catalog_version: LEGACY_CATALOG_VERSION,
410 next_index_id: 1,
411 links: Vec::new(),
412 structure_generation: next_structure_generation(),
413 read_only: false,
414 dirty_budget: Arc::new(DirtyPageBudget::default()),
415 };
416 cat.persist()?;
417 Ok(cat)
418 }
419
420 pub fn open(data_dir: &Path) -> io::Result<Self> {
429 Self::open_inner(data_dir, None)
430 }
431
432 pub fn open_with_wal_archive<F>(data_dir: &Path, mut archive: F) -> io::Result<Self>
441 where
442 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
443 {
444 let archive: WalArchiveCallback<'_> = &mut archive;
445 Self::open_inner(data_dir, Some(archive))
446 }
447
448 fn open_inner(data_dir: &Path, archive: Option<WalArchiveCallback<'_>>) -> io::Result<Self> {
449 let cat_path = data_dir.join(CATALOG_FILE);
450 if !cat_path.exists() {
451 return Err(io::Error::new(io::ErrorKind::NotFound, "no catalog file"));
452 }
453 let catalog_file = read_catalog_file(&cat_path)?;
454 let active_catalog_version = catalog_file.version;
455 let next_index_id = catalog_file.next_index_id;
456 let links = catalog_file.links;
457 let entries = catalog_file.entries;
458 let durable_lsn = read_durable_lsn(data_dir)?;
459 let mut tables: Vec<Table> = Vec::with_capacity(entries.len());
460 let mut name_to_slot =
461 FxHashMap::with_capacity_and_hasher(entries.len(), Default::default());
462 let dirty_budget = Arc::new(DirtyPageBudget::default());
463 for CatalogEntry {
464 schema,
465 indexed_cols,
466 expression_indexes: expression_metas,
467 defaults,
468 auto_cols,
469 } in entries
470 {
471 let name = schema.table_name.clone();
472 let mut table =
478 Table::open_with_indexes(schema, data_dir, &indexed_cols, &expression_metas)?;
479 table.heap.set_dirty_budget(Arc::clone(&dirty_budget));
480 table.set_defaults(defaults);
481 table.set_auto_cols(auto_cols);
482 name_to_slot.insert(name.clone(), tables.len());
483 tables.push(table);
484 }
485 let wal_path = data_dir.join(WAL_FILE);
486 let wal = Wal::open(&wal_path, WAL_BATCH_SIZE)?;
487 let mut cat = Catalog {
488 tables,
489 name_to_slot,
490 data_dir: data_dir.to_path_buf(),
491 wal,
492 next_tx_id: 1,
493 active_tx_id: None,
494 tx_start_len: None,
495 pending_autocommit_tx_ids: Vec::new(),
496 pending_free_overflow: Vec::new(),
497 checkpointed: false,
498 durable_lsn,
499 active_catalog_version,
500 next_index_id,
501 links,
502 structure_generation: next_structure_generation(),
503 read_only: false,
504 dirty_budget,
505 };
506 cat.replay_wal(archive)?;
507 let max_page_lsn = cat
517 .tables
518 .iter()
519 .map(|t| t.heap.max_page_lsn())
520 .max()
521 .unwrap_or(0);
522 let max_known_lsn = max_page_lsn.max(cat.durable_lsn);
523 cat.wal.set_next_lsn_at_least(max_known_lsn + 1);
524 if let Err(e) = cat.sweep_all() {
529 warn!(error = %e, "post-recovery overflow sweep failed (non-fatal)");
530 }
531 Ok(cat)
532 }
533
534 pub fn open_read_only(data_dir: &Path) -> io::Result<Self> {
548 crate::validate_data_dir_read_only(data_dir)?;
549 let cat_path = data_dir.join(CATALOG_FILE);
550 if !cat_path.exists() {
551 return Err(io::Error::new(io::ErrorKind::NotFound, "no catalog file"));
552 }
553
554 let wal_path = data_dir.join(WAL_FILE);
558 if crate::wal::wal_has_committed_records(&wal_path)? {
559 return Err(io::Error::new(
560 io::ErrorKind::InvalidData,
561 "cannot open read-only: the WAL is not empty (the directory has \
562 un-checkpointed writes). Open the directory once with a read-write \
563 engine to recover, or restore from a backup, then serve it read-only",
564 ));
565 }
566
567 let catalog_file = read_catalog_file(&cat_path)?;
568 let active_catalog_version = catalog_file.version;
569 let next_index_id = catalog_file.next_index_id;
570 let links = catalog_file.links;
571 let entries = catalog_file.entries;
572 let durable_lsn = read_durable_lsn(data_dir)?;
573 let mut tables: Vec<Table> = Vec::with_capacity(entries.len());
574 let mut name_to_slot =
575 FxHashMap::with_capacity_and_hasher(entries.len(), Default::default());
576 let dirty_budget = Arc::new(DirtyPageBudget::default());
577 for CatalogEntry {
578 schema,
579 indexed_cols,
580 expression_indexes: expression_metas,
581 defaults,
582 auto_cols,
583 } in entries
584 {
585 let name = schema.table_name.clone();
586 let mut table = Table::open_with_indexes_read_only(
587 schema,
588 data_dir,
589 &indexed_cols,
590 &expression_metas,
591 )?;
592 table.heap.set_dirty_budget(Arc::clone(&dirty_budget));
593 table.set_defaults(defaults);
594 table.set_auto_cols(auto_cols);
595 name_to_slot.insert(name.clone(), tables.len());
596 tables.push(table);
597 }
598 let wal = Wal::open_read_only(&wal_path, WAL_BATCH_SIZE)?;
599 Ok(Catalog {
600 tables,
601 name_to_slot,
602 data_dir: data_dir.to_path_buf(),
603 wal,
604 next_tx_id: 1,
605 active_tx_id: None,
606 tx_start_len: None,
607 pending_autocommit_tx_ids: Vec::new(),
608 pending_free_overflow: Vec::new(),
609 checkpointed: false,
610 durable_lsn,
611 active_catalog_version,
612 next_index_id,
613 links,
614 structure_generation: next_structure_generation(),
615 read_only: true,
616 dirty_budget,
617 })
618 }
619
620 fn replay_wal(&mut self, mut archive: Option<WalArchiveCallback<'_>>) -> io::Result<()> {
646 let records = self.wal.read_all()?;
647 if records.is_empty() {
648 return Ok(());
649 }
650 if archive.is_none() {
651 self.ensure_plain_wal_truncate_allowed(&records)?;
652 }
653 self.replay_records(&records)?;
654 if let Some(archive) = archive.as_mut() {
655 archive(&self.data_dir, &records)?;
656 }
657 self.wal.truncate()?;
658 Ok(())
659 }
660
661 pub fn apply_wal_records(&mut self, records: &[WalRecord]) -> io::Result<()> {
670 self.ensure_no_active_transaction_for_checkpoint()?;
671 self.ensure_no_pending_wal_records()?;
672 self.replay_records(records)
673 }
674
675 pub fn ensure_no_pending_wal_records(&self) -> io::Result<()> {
678 if self.wal.has_pending() || !self.wal.read_all()?.is_empty() {
679 return Err(io::Error::other(
680 "cannot apply replicated WAL records while local WAL records are pending",
681 ));
682 }
683 Ok(())
684 }
685
686 fn replay_records(&mut self, records: &[WalRecord]) -> io::Result<()> {
687 if records.is_empty() {
688 return Ok(());
689 }
690
691 info!(count = records.len(), "applying WAL records");
692
693 let has_boundaries = records.iter().any(|rec| {
702 matches!(
703 rec.record_type,
704 WalRecordType::Begin | WalRecordType::Commit | WalRecordType::Rollback
705 )
706 });
707 let mut committed_row_records = vec![true; records.len()];
708 if has_boundaries {
709 committed_row_records.fill(false);
710 let mut pending_tx_spans: Vec<(u64, Vec<usize>)> = Vec::new();
711 for (index, rec) in records.iter().enumerate() {
712 match rec.record_type {
713 WalRecordType::Insert
714 | WalRecordType::Update
715 | WalRecordType::Delete
716 | WalRecordType::OverflowWrite
717 | WalRecordType::OverflowFree
718 if rec.tx_id == 0 =>
719 {
720 committed_row_records[index] = true;
721 }
722 WalRecordType::Insert
723 | WalRecordType::Update
724 | WalRecordType::Delete
725 | WalRecordType::OverflowWrite
726 | WalRecordType::OverflowFree => {
727 if let Some((_, rows)) = pending_tx_spans
728 .iter_mut()
729 .rev()
730 .find(|(tx_id, _)| *tx_id == rec.tx_id)
731 {
732 rows.push(index);
733 } else {
734 pending_tx_spans.push((rec.tx_id, vec![index]));
735 }
736 }
737 WalRecordType::Begin if rec.tx_id != 0 => {
738 pending_tx_spans.push((rec.tx_id, Vec::new()));
739 }
740 WalRecordType::Commit if rec.tx_id != 0 => {
741 if let Some(span_index) = pending_tx_spans
742 .iter()
743 .rposition(|(tx_id, _)| *tx_id == rec.tx_id)
744 {
745 let (_, rows) = pending_tx_spans.remove(span_index);
746 for row_index in rows {
747 committed_row_records[row_index] = true;
748 }
749 }
750 }
751 WalRecordType::Rollback if rec.tx_id != 0 => {
752 if let Some(span_index) = pending_tx_spans
753 .iter()
754 .rposition(|(tx_id, _)| *tx_id == rec.tx_id)
755 {
756 pending_tx_spans.remove(span_index);
757 }
758 }
759 _ => {}
760 }
761 }
762 }
763
764 let mut replayed_inserts = 0usize;
765 let mut replayed_updates = 0usize;
766 let mut replayed_deletes = 0usize;
767 let mut skipped = 0usize;
768 let mut skipped_uncommitted = 0usize;
769 let mut saw_ddl = false;
770 for (index, rec) in records.iter().enumerate() {
771 if has_boundaries
772 && !committed_row_records[index]
773 && matches!(
774 rec.record_type,
775 WalRecordType::Insert
776 | WalRecordType::Update
777 | WalRecordType::Delete
778 | WalRecordType::OverflowWrite
779 | WalRecordType::OverflowFree
780 )
781 {
782 skipped_uncommitted += 1;
783 continue;
784 }
785 match rec.record_type {
786 WalRecordType::Insert => {
787 if let Some((table_name, rid, row_bytes)) = decode_wal_payload(&rec.data) {
788 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
789 let tbl = &mut self.tables[slot];
790 if rec.lsn > 0 && tbl.heap.page_lsn(rid.page_id) >= rec.lsn {
794 skipped += 1;
795 continue;
796 }
797 tbl.heap.insert_at(rid, &row_bytes)?;
804 tbl.heap.set_page_lsn(rid.page_id, rec.lsn)?;
805 replayed_inserts += 1;
806 }
807 }
808 }
809 WalRecordType::Update => {
810 if let Some((table_name, rid, row_bytes)) = decode_wal_payload(&rec.data) {
811 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
812 let tbl = &mut self.tables[slot];
813 if rec.lsn > 0 && tbl.heap.page_lsn(rid.page_id) >= rec.lsn {
814 skipped += 1;
815 continue;
816 }
817 let new_rid = tbl.heap.update(rid, &row_bytes)?;
818 tbl.heap.set_page_lsn(new_rid.page_id, rec.lsn)?;
819 replayed_updates += 1;
820 }
821 }
822 }
823 WalRecordType::Delete => {
824 if let Some((table_name, rid, _)) = 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 _ = tbl.heap.delete(rid);
832 tbl.heap.set_page_lsn(rid.page_id, rec.lsn)?;
833 replayed_deletes += 1;
834 }
835 }
836 }
837 WalRecordType::OverflowWrite => {
838 if let Some((table_name, page_id, next_page, chunk)) =
843 decode_overflow_write_payload(&rec.data)
844 {
845 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
846 let tbl = &mut self.tables[slot];
847 if rec.lsn > 0 && tbl.heap.overflow_page_lsn(page_id) >= rec.lsn {
848 skipped += 1;
849 continue;
850 }
851 tbl.heap
852 .write_overflow_page(page_id, next_page, &chunk, rec.lsn)?;
853 }
854 }
855 }
856 WalRecordType::OverflowFree => {
857 if let Some((table_name, pages)) = decode_overflow_free_payload(&rec.data) {
862 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
863 self.tables[slot].heap.release_overflow_pages(&pages);
864 }
865 }
866 }
867 WalRecordType::Begin | WalRecordType::Commit | WalRecordType::Rollback => {
868 }
870 WalRecordType::DdlCreateTable => {
871 saw_ddl = true;
872 if let Some((schema, defaults, auto_cols)) = decode_ddl_create_table(&rec.data)
873 {
874 if !self.name_to_slot.contains_key(&schema.table_name) {
875 if let Ok(mut table) = Table::create(schema, &self.data_dir) {
876 table.heap.set_dirty_budget(Arc::clone(&self.dirty_budget));
877 table.set_defaults(defaults);
878 table.set_auto_cols(auto_cols);
879 let slot = self.tables.len();
880 let name = table.schema.table_name.clone();
881 self.tables.push(table);
882 self.name_to_slot.insert(name, slot);
883 }
884 }
885 }
886 }
887 WalRecordType::DdlDropTable => {
888 saw_ddl = true;
889 if let Some((table_name, _)) = decode_ddl_table_name(&rec.data) {
890 if let Some(&slot) = self.name_to_slot.get(&table_name) {
891 let heap_path = self.data_dir.join(format!("{table_name}.heap"));
892 if heap_path.exists() {
893 let _ = fs::remove_file(&heap_path);
894 }
895 for col_name in self.tables[slot].indexed_column_names() {
896 let idx_path =
897 self.data_dir.join(format!("{table_name}_{col_name}.idx"));
898 if idx_path.exists() {
899 let _ = fs::remove_file(&idx_path);
900 }
901 }
902 for index_id in self.tables[slot].expression_index_ids() {
903 let idx_path = self
904 .data_dir
905 .join(expression_index_file_name(&table_name, index_id));
906 let _ = fs::remove_file(idx_path);
907 }
908 self.name_to_slot.remove(&table_name);
909 let last = self.tables.len() - 1;
910 if slot != last {
911 let moved_name = self.tables[last].schema.table_name.clone();
912 self.tables.swap(slot, last);
913 self.name_to_slot.insert(moved_name, slot);
914 }
915 self.tables.pop();
916 }
917 }
918 }
919 WalRecordType::DdlAddColumn => {
920 saw_ddl = true;
921 if let Some((table_name, col)) = decode_ddl_alter_add_column(&rec.data) {
922 if let Some(&slot) = self.name_to_slot.get(&table_name) {
923 let tbl = &mut self.tables[slot];
924 if !tbl.schema.columns.iter().any(|c| c.name == col.name) {
925 let old_schema = tbl.schema.clone();
926 let has_rows = tbl.heap.scan().next().is_some();
927 tbl.schema.columns.push(col);
928 tbl.refresh_layout();
929 if has_rows {
930 let fill = vec![Value::Empty; tbl.schema.columns.len()];
931 let data_dir = self.data_dir.clone();
932 let _ = tbl.rewrite_rows_for_schema_change(
933 &old_schema,
934 &fill,
935 &data_dir,
936 );
937 }
938 }
939 if rec.lsn > 0 {
946 let _ = tbl.heap.stamp_all_pages_min_lsn(rec.lsn);
947 }
948 }
949 }
950 }
951 WalRecordType::DdlDropColumn => {
952 saw_ddl = true;
953 if let Some((table_name, col_name)) = decode_ddl_alter_drop_column(&rec.data) {
954 if let Some(&slot) = self.name_to_slot.get(&table_name) {
955 {
956 let tbl = &mut self.tables[slot];
957 if let Some(idx) =
958 tbl.schema.columns.iter().position(|c| c.name == col_name)
959 {
960 let old_schema = tbl.schema.clone();
961 let has_rows = tbl.heap.scan().next().is_some();
962 tbl.schema.columns.remove(idx);
963 for (i, c) in tbl.schema.columns.iter_mut().enumerate() {
964 c.position = i as u16;
965 }
966 tbl.refresh_layout();
967 if has_rows {
968 let fill = vec![Value::Empty; tbl.schema.columns.len()];
969 let data_dir = self.data_dir.clone();
970 let _ = tbl.rewrite_rows_for_schema_change(
971 &old_schema,
972 &fill,
973 &data_dir,
974 );
975 }
976 }
977 if rec.lsn > 0 {
978 let _ = tbl.heap.stamp_all_pages_min_lsn(rec.lsn);
979 }
980 }
981
982 let removed_ids =
983 self.tables[slot].remove_expression_indexes_for_root(&col_name);
984 for index_id in removed_ids {
985 let idx_path = self
986 .data_dir
987 .join(expression_index_file_name(&table_name, index_id));
988 let _ = fs::remove_file(idx_path);
989 }
990 }
991 }
992 }
993 }
994 }
995 info!(
996 inserts = replayed_inserts,
997 updates = replayed_updates,
998 deletes = replayed_deletes,
999 skipped = skipped,
1000 skipped_uncommitted = skipped_uncommitted,
1001 "WAL record apply complete (commit-boundary + LSN idempotent)"
1002 );
1003 if saw_ddl {
1004 self.persist()?;
1005 }
1006 for tbl in &mut self.tables {
1020 tbl.heap.flush_all_dirty()?;
1021 tbl.heap.flush()?;
1022 tbl.rebuild_indexes_from_heap()?;
1023 tbl.save_dirty_indexes()?;
1028 }
1029 if let Some(max_lsn) = max_record_lsn(records) {
1030 self.record_durable_lsn_at_least(max_lsn)?;
1031 self.wal.set_next_lsn_at_least(max_lsn.saturating_add(1));
1032 }
1033 Ok(())
1034 }
1035
1036 pub fn checkpoint(&mut self) -> io::Result<()> {
1045 self.ensure_no_active_transaction_for_checkpoint()?;
1046 self.ensure_plain_checkpoint_allowed_before_flush()?;
1047 self.flush_checkpoint_state()?;
1048 self.wal.flush()?;
1049 self.record_durable_lsn_at_least(self.wal.last_appended_lsn())?;
1050 self.wal.truncate()?;
1051 self.checkpointed = true;
1052 Ok(())
1053 }
1054
1055 pub fn checkpoint_with_wal_archive<F>(&mut self, mut archive: F) -> io::Result<()>
1063 where
1064 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
1065 {
1066 self.ensure_no_active_transaction_for_checkpoint()?;
1067 self.commit_autocommit()?;
1068 self.flush_checkpoint_state()?;
1069 self.wal.flush()?;
1070 let records = self.wal.read_all()?;
1071 let archive: WalArchiveCallback<'_> = &mut archive;
1072 archive(&self.data_dir, &records)?;
1073 if let Some(max_lsn) = max_record_lsn(&records) {
1074 self.record_durable_lsn_at_least(max_lsn)?;
1075 } else {
1076 self.record_durable_lsn_at_least(self.wal.last_appended_lsn())?;
1077 }
1078 self.wal.truncate()?;
1079 self.checkpointed = true;
1080 Ok(())
1081 }
1082
1083 fn ensure_no_active_transaction_for_checkpoint(&self) -> io::Result<()> {
1084 if self.active_tx_id.is_some() {
1085 return Err(io::Error::other(
1086 "cannot checkpoint while an explicit transaction is active",
1087 ));
1088 }
1089 Ok(())
1090 }
1091
1092 fn ensure_no_active_transaction_for_ddl(&self, verb: &'static str) -> io::Result<()> {
1102 if self.active_tx_id.is_some() {
1103 return Err(io::Error::new(
1104 io::ErrorKind::InvalidInput,
1105 StorageError::DdlInTransaction { verb },
1106 ));
1107 }
1108 Ok(())
1109 }
1110
1111 fn flush_checkpoint_state(&mut self) -> io::Result<()> {
1112 for tbl in &mut self.tables {
1113 tbl.heap.flush_all_dirty()?;
1114 tbl.heap.flush()?;
1115 tbl.save_dirty_indexes()?;
1120 }
1121 Ok(())
1122 }
1123
1124 fn ensure_plain_checkpoint_allowed_before_flush(&self) -> io::Result<()> {
1125 if !self.sync_identity_file_exists() {
1126 return Ok(());
1127 }
1128 if self.wal.has_pending() {
1129 return Err(io::Error::other(
1130 "sync identity exists but checkpoint/recovery was called without a WAL archive hook; refusing to truncate retained history",
1131 ));
1132 }
1133 let records = self.wal.read_all()?;
1134 self.ensure_plain_wal_truncate_allowed(&records)
1135 }
1136
1137 fn ensure_plain_wal_truncate_allowed(&self, records: &[WalRecord]) -> io::Result<()> {
1138 if records.is_empty() {
1139 return Ok(());
1140 }
1141 if self.sync_identity_file_exists() {
1142 return Err(io::Error::other(
1143 "sync identity exists but checkpoint/recovery was called without a WAL archive hook; refusing to truncate retained history",
1144 ));
1145 }
1146 Ok(())
1147 }
1148
1149 fn sync_identity_file_exists(&self) -> bool {
1150 self.data_dir
1151 .join(SYNC_STATE_DIR)
1152 .join(SYNC_IDENTITY_FILE)
1153 .exists()
1154 }
1155
1156 fn record_durable_lsn_at_least(&mut self, lsn: u64) -> io::Result<()> {
1157 if lsn <= self.durable_lsn {
1158 return Ok(());
1159 }
1160 self.durable_lsn = lsn;
1161 write_durable_lsn(&self.data_dir, lsn)
1162 }
1163
1164 #[inline]
1166 fn free_overflow_chain(&mut self, slot: usize, pages: Vec<u32>) {
1177 if pages.is_empty() {
1178 return;
1179 }
1180 if self.active_tx_id.is_some() {
1181 self.pending_free_overflow.push((slot, pages));
1182 } else {
1183 self.tables[slot].release_overflow_pages(&pages);
1184 }
1185 }
1186
1187 fn next_tx(&mut self) -> u64 {
1188 if let Some(id) = self.active_tx_id {
1189 return id;
1190 }
1191 let id = self.next_tx_id;
1192 self.next_tx_id = self.next_tx_id.wrapping_add(1);
1193 id
1194 }
1195
1196 pub fn begin_transaction(&mut self) -> io::Result<()> {
1198 if self.active_tx_id.is_some() {
1199 return Err(io::Error::new(
1200 io::ErrorKind::InvalidInput,
1201 "explicit transaction is already active",
1202 ));
1203 }
1204 let start_len = self.wal.synced_len()?;
1205 let id = self.next_tx_id;
1206 self.next_tx_id = self.next_tx_id.wrapping_add(1);
1207 self.active_tx_id = Some(id);
1208 self.dirty_budget.set_rollback_pinned(true);
1212 self.tx_start_len = Some(start_len);
1213 self.pending_autocommit_tx_ids.clear();
1214 if !self.wal.is_off() {
1215 self.wal.append(id, WalRecordType::Begin, &[])?;
1216 self.wal.flush()?;
1217 }
1218 Ok(())
1219 }
1220
1221 pub fn commit_transaction(&mut self) -> io::Result<()> {
1224 self.dirty_budget.set_rollback_pinned(false);
1225 if let Some(id) = self.active_tx_id.take() {
1226 if !self.wal.is_off() {
1227 self.wal.append(id, WalRecordType::Commit, &[])?;
1228 self.wal.flush()?;
1229 }
1230 }
1231 self.tx_start_len = None;
1232 for (slot, pages) in std::mem::take(&mut self.pending_free_overflow) {
1236 self.tables[slot].release_overflow_pages(&pages);
1237 }
1238 Ok(())
1239 }
1240
1241 pub fn commit_autocommit(&mut self) -> io::Result<()> {
1245 if !self.wal.is_off() && !self.pending_autocommit_tx_ids.is_empty() {
1246 self.pending_autocommit_tx_ids.sort_unstable();
1247 self.pending_autocommit_tx_ids.dedup();
1248 for id in self.pending_autocommit_tx_ids.drain(..) {
1249 self.wal.append(id, WalRecordType::Commit, &[])?;
1250 }
1251 }
1252 self.wal.flush()
1253 }
1254
1255 fn wal_log(
1270 &mut self,
1271 tx_id: u64,
1272 record_type: WalRecordType,
1273 table: &str,
1274 rid: RowId,
1275 row_bytes: &[u8],
1276 ) -> io::Result<()> {
1277 if self.wal.is_off() {
1284 return Ok(());
1285 }
1286 let payload = encode_wal_payload(table, rid, row_bytes);
1287 self.wal.append(tx_id, record_type, &payload)?;
1288 if self.active_tx_id.is_none() {
1289 self.pending_autocommit_tx_ids.push(tx_id);
1290 }
1291 Ok(())
1292 }
1293
1294 #[inline]
1300 pub fn sync_wal(&mut self) -> io::Result<()> {
1301 self.wal.flush()
1302 }
1303
1304 pub fn set_wal_sync_mode(&mut self, mode: WalSyncMode) {
1312 self.wal.set_sync_mode(mode);
1313 }
1314
1315 pub fn set_dirty_page_budget_bytes(&mut self, limit_bytes: usize) {
1320 self.dirty_budget.set_limit_bytes(limit_bytes);
1321 }
1322
1323 pub fn dirty_page_budget_bytes(&self) -> usize {
1324 self.dirty_budget.limit_bytes()
1325 }
1326
1327 pub fn dirty_pages_buffered(&self) -> usize {
1329 self.dirty_budget.charged_pages()
1330 }
1331
1332 pub fn set_wal_sync_deferred(&mut self, defer: bool) {
1340 self.wal.set_defer_sync(defer);
1341 }
1342
1343 pub fn take_wal_durability_ticket(&mut self) -> Option<WalDurabilityTicket> {
1346 self.wal.take_durability_ticket()
1347 }
1348
1349 pub fn wal_fsync_count(&self) -> u64 {
1351 self.wal.fsync_count()
1352 }
1353
1354 pub fn rollback_to_last_sync(&mut self) -> io::Result<()> {
1369 self.rollback_to_last_sync_inner(None)
1370 }
1371
1372 pub fn rollback_to_last_sync_with_wal_archive<F>(&mut self, mut archive: F) -> io::Result<()>
1377 where
1378 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
1379 {
1380 let archive: WalArchiveCallback<'_> = &mut archive;
1381 self.rollback_to_last_sync_inner(Some(archive))
1382 }
1383
1384 fn rollback_to_last_sync_inner(
1385 &mut self,
1386 mut archive: Option<WalArchiveCallback<'_>>,
1387 ) -> io::Result<()> {
1388 let start_len = self.tx_start_len.unwrap_or(0);
1389 let prearchived = if let Some(archive) = archive.as_mut() {
1390 let records = self.wal.read_through_len(start_len)?;
1391 if !records.is_empty() {
1392 archive(&self.data_dir, &records)?;
1393 }
1394 true
1395 } else {
1396 false
1397 };
1398
1399 let start_len = self.tx_start_len.take().unwrap_or(0);
1400 if let Some(id) = self.active_tx_id.take() {
1401 if !self.wal.is_off() {
1402 let _ = self.wal.append(id, WalRecordType::Rollback, &[]);
1403 }
1404 }
1405 self.wal.discard_and_truncate_to(start_len)?;
1406
1407 for tbl in &mut self.tables {
1417 tbl.heap.discard_dirty();
1418 tbl.discard_dirty_indexes();
1419 }
1420 self.wal.discard_pending()?;
1426 let data_dir = self.data_dir.clone();
1430 let sync_mode = self.wal.sync_mode();
1431 let mut restored = if prearchived {
1432 let mut already_archived = |_dir: &Path, _records: &[WalRecord]| Ok(());
1433 let archive: WalArchiveCallback<'_> = &mut already_archived;
1434 Self::open_inner(&data_dir, Some(archive))?
1435 } else {
1436 Self::open_inner(&data_dir, archive)?
1437 };
1438 if self.has_same_prepared_structure(&restored) {
1444 restored.structure_generation = self.structure_generation;
1445 }
1446 let dirty_budget_limit = self.dirty_budget.limit_bytes();
1447 *self = restored;
1448 self.wal.set_sync_mode(sync_mode);
1449 self.dirty_budget.set_limit_bytes(dirty_budget_limit);
1452 Ok(())
1453 }
1454
1455 fn abandon_active_transaction_for_drop(&mut self) -> io::Result<()> {
1456 self.dirty_budget.set_rollback_pinned(false);
1457 for tbl in &mut self.tables {
1458 tbl.heap.discard_dirty();
1459 }
1460 self.pending_autocommit_tx_ids.clear();
1461 let truncate_result = match self.tx_start_len.take() {
1462 Some(start_len) => self.wal.discard_and_truncate_to(start_len),
1463 None => self.wal.discard_pending(),
1464 };
1465 self.active_tx_id = None;
1466 truncate_result
1467 }
1468
1469 pub fn data_dir(&self) -> &Path {
1471 &self.data_dir
1472 }
1473
1474 pub fn max_lsn(&self) -> u64 {
1479 let max_page_lsn = self
1480 .tables
1481 .iter()
1482 .map(|t| t.heap.max_page_lsn())
1483 .max()
1484 .unwrap_or(0);
1485 max_page_lsn
1486 .max(self.durable_lsn)
1487 .max(self.wal.last_appended_lsn())
1488 }
1489
1490 pub fn create_table(&mut self, schema: Schema) -> io::Result<()> {
1491 self.create_table_full(schema, Vec::new(), Vec::new())
1492 }
1493
1494 pub fn create_table_with_defaults(
1498 &mut self,
1499 schema: Schema,
1500 defaults: Vec<Option<Value>>,
1501 ) -> io::Result<()> {
1502 self.create_table_full(schema, defaults, Vec::new())
1503 }
1504
1505 pub fn create_table_full(
1510 &mut self,
1511 schema: Schema,
1512 defaults: Vec<Option<Value>>,
1513 auto_cols: Vec<bool>,
1514 ) -> io::Result<()> {
1515 self.ensure_no_active_transaction_for_ddl("create table")?;
1516 self.invalidate_structure();
1517 validate_table_name(&schema.table_name)?;
1518 for col in &schema.columns {
1519 validate_column_name(&col.name)?;
1520 }
1521 let name = schema.table_name.clone();
1522 if self.name_to_slot.contains_key(&name) {
1523 return Err(io::Error::new(
1524 io::ErrorKind::AlreadyExists,
1525 format!("table '{name}' already exists"),
1526 ));
1527 }
1528 if !self.wal.is_off() {
1529 let payload = encode_ddl_create_table(&schema, &defaults, &auto_cols);
1530 self.wal
1531 .append(0, WalRecordType::DdlCreateTable, &payload)?;
1532 self.wal.flush()?;
1533 }
1534 let mut table = Table::create(schema, &self.data_dir)?;
1535 table.heap.set_dirty_budget(Arc::clone(&self.dirty_budget));
1536 table.set_defaults(defaults);
1537 table.set_auto_cols(auto_cols);
1538 let slot = self.tables.len();
1539 self.tables.push(table);
1540 self.name_to_slot.insert(name, slot);
1541 self.persist()?;
1542 Ok(())
1543 }
1544
1545 pub fn column_defaults(&self, table: &str) -> Option<&[Option<Value>]> {
1549 let slot = *self.name_to_slot.get(table)?;
1550 Some(self.tables[slot].defaults())
1551 }
1552
1553 pub fn auto_columns(&self, table: &str) -> Option<&[bool]> {
1556 let slot = *self.name_to_slot.get(table)?;
1557 Some(self.tables[slot].auto_cols())
1558 }
1559
1560 pub fn assign_auto_columns(&mut self, table: &str, values: &mut [Value]) {
1564 if let Some(&slot) = self.name_to_slot.get(table) {
1565 self.tables[slot].assign_auto(values);
1566 }
1567 }
1568
1569 fn persist_at_activation_boundary(&self) -> Result<(), CatalogPersistError> {
1574 let cat_path = self.data_dir.join(CATALOG_FILE);
1575 let tmp_path = self.data_dir.join(format!("{CATALOG_FILE}.tmp"));
1576 let entries: Vec<CatalogEntryRef<'_>> = self
1577 .tables
1578 .iter()
1579 .map(|t| CatalogEntryRef {
1580 schema: &t.schema,
1581 indexed_cols: t.indexed_column_metas(),
1582 expression_indexes: t.expression_index_metas(),
1583 defaults: t.defaults(),
1584 auto_cols: t.auto_cols(),
1585 })
1586 .collect();
1587 write_catalog_file(
1588 &tmp_path,
1589 self.active_catalog_version,
1590 self.next_index_id,
1591 &entries,
1592 &self.links,
1593 )
1594 .map_err(CatalogPersistError::BeforeActivation)?;
1595 #[cfg(test)]
1596 if take_catalog_persist_failpoint(1) {
1597 return Err(CatalogPersistError::BeforeActivation(io::Error::other(
1598 "injected catalog failure before rename",
1599 )));
1600 }
1601 fs::rename(&tmp_path, &cat_path).map_err(CatalogPersistError::BeforeActivation)?;
1602 #[cfg(test)]
1603 let directory_sync = if take_catalog_persist_failpoint(2) {
1604 Err(io::Error::other(
1605 "injected catalog directory sync failure after rename",
1606 ))
1607 } else {
1608 sync_directory(&self.data_dir)
1609 };
1610 #[cfg(not(test))]
1611 let directory_sync = sync_directory(&self.data_dir);
1612 directory_sync.map_err(CatalogPersistError::AfterActivation)
1613 }
1614
1615 fn persist(&self) -> io::Result<()> {
1616 self.persist_at_activation_boundary()
1617 .map_err(CatalogPersistError::into_io_error)
1618 }
1619
1620 #[inline]
1624 pub fn table_slot(&self, name: &str) -> Option<usize> {
1625 self.name_to_slot.get(name).copied()
1626 }
1627
1628 #[inline]
1632 pub fn structure_generation(&self) -> u64 {
1633 self.structure_generation
1634 }
1635
1636 #[inline]
1637 fn invalidate_structure(&mut self) {
1638 self.structure_generation = next_structure_generation();
1639 }
1640
1641 fn has_same_prepared_structure(&self, other: &Self) -> bool {
1642 self.tables.len() == other.tables.len()
1643 && self.tables.iter().zip(&other.tables).all(|(left, right)| {
1644 let left_schema = &left.schema;
1645 let right_schema = &right.schema;
1646 left_schema.table_name == right_schema.table_name
1647 && left_schema.columns.len() == right_schema.columns.len()
1648 && left_schema.columns.iter().zip(&right_schema.columns).all(
1649 |(left_col, right_col)| {
1650 left_col.name == right_col.name
1651 && left_col.type_id == right_col.type_id
1652 && left_col.required == right_col.required
1653 && left_col.position == right_col.position
1654 },
1655 )
1656 && left.defaults() == right.defaults()
1657 && left.auto_cols() == right.auto_cols()
1658 && {
1659 let left_indexes = left.indexed_column_metas();
1660 let right_indexes = right.indexed_column_metas();
1661 left_indexes.len() == right_indexes.len()
1662 && left_indexes.iter().zip(&right_indexes).all(
1663 |(left_index, right_index)| {
1664 left_index.name == right_index.name
1665 && left_index.unique == right_index.unique
1666 },
1667 )
1668 }
1669 && left.expression_index_metas() == right.expression_index_metas()
1670 })
1671 }
1672
1673 #[inline]
1676 pub fn table_by_slot(&self, slot: usize) -> &Table {
1677 &self.tables[slot]
1678 }
1679
1680 #[inline]
1682 pub fn table_by_slot_mut(&mut self, slot: usize) -> &mut Table {
1683 &mut self.tables[slot]
1684 }
1685
1686 pub fn get_table(&self, name: &str) -> Option<&Table> {
1687 let slot = *self.name_to_slot.get(name)?;
1688 Some(&self.tables[slot])
1689 }
1690
1691 pub fn get_table_mut(&mut self, name: &str) -> Option<&mut Table> {
1692 let slot = *self.name_to_slot.get(name)?;
1693 Some(&mut self.tables[slot])
1694 }
1695
1696 #[inline]
1700 pub fn table_has_overflow(&self, table: &str) -> bool {
1701 self.get_table(table)
1702 .map(|t| t.has_overflow_rows())
1703 .unwrap_or(false)
1704 }
1705
1706 #[inline]
1711 fn by_name(&self, table: &str) -> io::Result<&Table> {
1712 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1713 io::Error::new(
1714 io::ErrorKind::NotFound,
1715 format!("table '{table}' not found"),
1716 )
1717 })?;
1718 Ok(&self.tables[slot])
1719 }
1720
1721 #[inline]
1723 fn by_name_mut(&mut self, table: &str) -> io::Result<&mut Table> {
1724 let slot = self.slot_of(table)?;
1725 Ok(&mut self.tables[slot])
1726 }
1727
1728 pub fn sweep(&mut self, table: &str) -> io::Result<usize> {
1733 let slot = self.slot_of(table)?;
1734 let reclaimed = self.tables[slot].sweep_overflow()?;
1735 if !reclaimed.is_empty() && !self.wal.is_off() {
1736 let payload = encode_overflow_free_payload(table, &reclaimed);
1737 self.wal.append(0, WalRecordType::OverflowFree, &payload)?;
1738 self.wal.flush()?;
1739 }
1740 Ok(reclaimed.len())
1741 }
1742
1743 pub fn sweep_all(&mut self) -> io::Result<usize> {
1745 let names: Vec<String> = self
1746 .tables
1747 .iter()
1748 .map(|t| t.schema.table_name.clone())
1749 .collect();
1750 let mut total = 0;
1751 for name in names {
1752 total += self.sweep(&name)?;
1753 }
1754 Ok(total)
1755 }
1756
1757 fn slot_of(&self, table: &str) -> io::Result<usize> {
1758 self.name_to_slot.get(table).copied().ok_or_else(|| {
1759 io::Error::new(
1760 io::ErrorKind::NotFound,
1761 format!("table '{table}' not found"),
1762 )
1763 })
1764 }
1765
1766 pub fn insert(&mut self, table: &str, values: &Row) -> io::Result<RowId> {
1767 if self.wal.is_off() {
1776 return self.by_name_mut(table)?.insert(values);
1777 }
1778 let slot = self.slot_of(table)?;
1779 let _ = self.tables[slot].preflight_insert(values)?;
1780 let tx_id = self.next_tx();
1784 let row_bytes = {
1785 let Catalog { tables, wal, .. } = self;
1786 encode_row_with_spill_logged(&mut tables[slot], wal, tx_id, values)?
1787 };
1788 let new_rid = self.tables[slot].insert_encoded(values, &row_bytes)?;
1793 self.wal_log(tx_id, WalRecordType::Insert, table, new_rid, &row_bytes)?;
1794 let lsn = self.wal.last_appended_lsn();
1795 if lsn > 0 {
1796 self.tables[slot].heap.set_page_lsn(new_rid.page_id, lsn)?;
1797 }
1798 Ok(new_rid)
1799 }
1800
1801 pub fn insert_by_slot(&mut self, slot: usize, values: &Row) -> io::Result<RowId> {
1809 if self.wal.is_off() {
1810 return self.tables[slot].insert(values);
1811 }
1812 let _ = self.tables[slot].preflight_insert(values)?;
1813 let tx_id = self.next_tx();
1814 let autocommit = self.active_tx_id.is_none();
1815 let Catalog { tables, wal, .. } = self;
1816 let tbl = &mut tables[slot];
1817 let row_bytes = encode_row_with_spill_logged(tbl, wal, tx_id, values)?;
1820 let new_rid = tbl.insert_encoded(values, &row_bytes)?;
1823 let payload = encode_wal_payload(&tbl.schema.table_name, new_rid, &row_bytes);
1824 wal.append(tx_id, WalRecordType::Insert, &payload)?;
1825 if autocommit {
1826 self.pending_autocommit_tx_ids.push(tx_id);
1827 }
1828 let lsn = wal.last_appended_lsn();
1829 if lsn > 0 {
1830 tbl.heap.set_page_lsn(new_rid.page_id, lsn)?;
1831 }
1832 Ok(new_rid)
1833 }
1834
1835 pub fn get(&self, table: &str, rid: RowId) -> Option<Row> {
1836 self.get_table(table)?.get(rid)
1837 }
1838
1839 pub fn get_projected(
1840 &self,
1841 table: &str,
1842 rid: RowId,
1843 column_indices: &[usize],
1844 ) -> io::Result<Option<Vec<Value>>> {
1845 self.by_name(table)?.get_projected(rid, column_indices)
1846 }
1847
1848 pub fn delete(&mut self, table: &str, rid: RowId) -> io::Result<()> {
1849 let slot = self.slot_of(table)?;
1850 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
1854 if self.wal.is_off() {
1857 self.tables[slot].delete(rid)?;
1858 self.free_overflow_chain(slot, old_pages);
1859 return Ok(());
1860 }
1861 let tx_id = self.next_tx();
1862 self.wal_log(tx_id, WalRecordType::Delete, table, rid, &[])?;
1864 self.tables[slot].delete(rid)?;
1865 self.free_overflow_chain(slot, old_pages);
1866 Ok(())
1867 }
1868
1869 pub fn delete_many(&mut self, table: &str, rids: &[RowId]) -> io::Result<u64> {
1873 let slot = self.slot_of(table)?;
1881 let old_pages = self.collect_overflow_pages(slot, rids)?;
1884 if self.wal.is_off() {
1885 let count = self.tables[slot].delete_many(rids)?;
1886 self.free_overflow_chain(slot, old_pages);
1887 return Ok(count);
1888 }
1889 let tx_id = self.next_tx();
1890 for &rid in rids {
1891 let payload = encode_wal_payload(table, rid, &[]);
1892 self.wal.append(tx_id, WalRecordType::Delete, &payload)?;
1893 }
1894 if self.active_tx_id.is_none() && !rids.is_empty() {
1895 self.pending_autocommit_tx_ids.push(tx_id);
1896 }
1897 let count = self.tables[slot].delete_many(rids)?;
1898 self.free_overflow_chain(slot, old_pages);
1899 Ok(count)
1900 }
1901
1902 fn collect_overflow_pages(&self, slot: usize, rids: &[RowId]) -> io::Result<Vec<u32>> {
1905 if !self.tables[slot].has_overflow_rows() {
1906 return Ok(Vec::new());
1907 }
1908 let mut pages = Vec::new();
1909 for &rid in rids {
1910 pages.extend(self.tables[slot].overflow_chain_pages_at(rid)?);
1911 }
1912 Ok(pages)
1913 }
1914
1915 pub fn scan_delete_matching<P>(&mut self, table: &str, pred: P) -> io::Result<u64>
1925 where
1926 P: FnMut(&[u8]) -> bool,
1927 {
1928 self.by_name_mut(table)?.scan_delete_matching(pred)
1929 }
1930
1931 pub fn scan_delete_matching_logged<P>(&mut self, table: &str, pred: P) -> io::Result<u64>
1942 where
1943 P: FnMut(&[u8]) -> bool,
1944 {
1945 if self.wal.is_off() {
1951 return self.by_name_mut(table)?.scan_delete_matching(pred);
1952 }
1953 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1957 io::Error::new(
1958 io::ErrorKind::NotFound,
1959 format!("table '{table}' not found"),
1960 )
1961 })?;
1962 let tx_id = self.next_tx();
1963 let autocommit = self.active_tx_id.is_none();
1964 let Catalog { tables, wal, .. } = self;
1967 let tbl = &mut tables[slot];
1968 let name_bytes = table.as_bytes();
1972 let count = tbl.scan_delete_matching_with_hook(pred, |rid, row_bytes| {
1973 let mut payload: Vec<u8> =
1974 Vec::with_capacity(4 + name_bytes.len() + 10 + row_bytes.len());
1975 payload.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
1976 payload.extend_from_slice(name_bytes);
1977 payload.extend_from_slice(&rid.page_id.to_le_bytes());
1978 payload.extend_from_slice(&rid.slot_index.to_le_bytes());
1979 payload.extend_from_slice(&0u32.to_le_bytes());
1983 let _ = wal.append(tx_id, WalRecordType::Delete, &payload);
1990 })?;
1991 if autocommit && count > 0 {
1992 self.pending_autocommit_tx_ids.push(tx_id);
1993 }
1994 Ok(count)
1996 }
1997
1998 pub fn scan_patch_matching_logged<P, M>(
2006 &mut self,
2007 table: &str,
2008 pred: P,
2009 try_mutate: M,
2010 ) -> io::Result<(u64, Vec<RowId>)>
2011 where
2012 P: FnMut(&[u8]) -> bool,
2013 M: FnMut(&mut [u8]) -> Option<u16>,
2014 {
2015 if self.wal.is_off() {
2016 return self.by_name_mut(table)?.scan_patch_matching_with_hook(
2017 pred,
2018 try_mutate,
2019 |_, _| {},
2020 );
2021 }
2022 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
2023 io::Error::new(
2024 io::ErrorKind::NotFound,
2025 format!("table '{table}' not found"),
2026 )
2027 })?;
2028 let tx_id = self.next_tx();
2029 let autocommit = self.active_tx_id.is_none();
2030 let Catalog { tables, wal, .. } = self;
2031 let tbl = &mut tables[slot];
2032 let name_bytes = table.as_bytes();
2033 let result = tbl.scan_patch_matching_with_hook(pred, try_mutate, |rid, row_bytes| {
2034 let mut payload: Vec<u8> =
2035 Vec::with_capacity(4 + name_bytes.len() + 10 + row_bytes.len());
2036 payload.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
2037 payload.extend_from_slice(name_bytes);
2038 payload.extend_from_slice(&rid.page_id.to_le_bytes());
2039 payload.extend_from_slice(&rid.slot_index.to_le_bytes());
2040 payload.extend_from_slice(&(row_bytes.len() as u32).to_le_bytes());
2041 payload.extend_from_slice(row_bytes);
2042 let _ = wal.append(tx_id, WalRecordType::Update, &payload);
2043 })?;
2044 if autocommit && result.0 > 0 {
2045 self.pending_autocommit_tx_ids.push(tx_id);
2046 }
2047 Ok(result)
2048 }
2049
2050 pub fn update(&mut self, table: &str, rid: RowId, values: &Row) -> io::Result<RowId> {
2051 if self.wal.is_off() {
2054 let slot = self.slot_of(table)?;
2055 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
2056 let new_rid = self.tables[slot].update(rid, values)?;
2057 self.free_overflow_chain(slot, old_pages);
2058 return Ok(new_rid);
2059 }
2060 let slot = self.slot_of(table)?;
2061 self.tables[slot].preflight_update(rid, values)?;
2062 let tx_id = self.next_tx();
2063 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
2067 let row_bytes = {
2072 let Catalog { tables, wal, .. } = self;
2073 encode_row_with_spill_logged(&mut tables[slot], wal, tx_id, values)?
2074 };
2075 check_encoded_row_size(&row_bytes)?;
2079 self.wal_log(tx_id, WalRecordType::Update, table, rid, &row_bytes)?;
2080 let new_rid = self.tables[slot].update_encoded(rid, values, &row_bytes, None)?;
2081 self.free_overflow_chain(slot, old_pages);
2082 Ok(new_rid)
2083 }
2084
2085 pub fn update_hinted(
2089 &mut self,
2090 table: &str,
2091 rid: RowId,
2092 values: &Row,
2093 changed_col_indices: Option<&[usize]>,
2094 ) -> io::Result<RowId> {
2095 if self.wal.is_off() {
2099 let slot = self.slot_of(table)?;
2100 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
2101 let new_rid = self.tables[slot].update_hinted(rid, values, changed_col_indices)?;
2102 self.free_overflow_chain(slot, old_pages);
2103 return Ok(new_rid);
2104 }
2105 let slot = self.slot_of(table)?;
2106 self.tables[slot].preflight_update(rid, values)?;
2107 let tx_id = self.next_tx();
2108 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
2109 let row_bytes = {
2110 let Catalog { tables, wal, .. } = self;
2111 encode_row_with_spill_logged(&mut tables[slot], wal, tx_id, values)?
2112 };
2113 check_encoded_row_size(&row_bytes)?;
2115 self.wal_log(tx_id, WalRecordType::Update, table, rid, &row_bytes)?;
2116 let new_rid =
2117 self.tables[slot].update_encoded(rid, values, &row_bytes, changed_col_indices)?;
2118 self.free_overflow_chain(slot, old_pages);
2119 Ok(new_rid)
2120 }
2121
2122 #[inline]
2134 pub fn with_row_bytes_mut<F>(&mut self, table: &str, rid: RowId, f: F) -> io::Result<bool>
2135 where
2136 F: FnOnce(&mut [u8]),
2137 {
2138 self.by_name_mut(table)?.with_row_bytes_mut(rid, f)
2139 }
2140
2141 #[inline]
2155 pub fn update_row_bytes_logged<F>(&mut self, table: &str, rid: RowId, f: F) -> io::Result<bool>
2156 where
2157 F: FnOnce(&mut [u8]),
2158 {
2159 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
2160 io::Error::new(
2161 io::ErrorKind::NotFound,
2162 format!("table '{table}' not found"),
2163 )
2164 })?;
2165 self.update_row_bytes_logged_by_slot(slot, rid, f)
2166 }
2167
2168 #[inline]
2173 pub fn update_row_bytes_logged_by_slot<F>(
2174 &mut self,
2175 slot: usize,
2176 rid: RowId,
2177 f: F,
2178 ) -> io::Result<bool>
2179 where
2180 F: FnOnce(&mut [u8]),
2181 {
2182 let tbl = &mut self.tables[slot];
2185 let ok = tbl.with_row_bytes_mut(rid, f)?;
2186 if !ok {
2187 return Ok(false);
2188 }
2189 if self.wal.is_off() {
2193 return Ok(true);
2194 }
2195 let new_bytes = match tbl.heap.get(rid) {
2198 Some(b) => b,
2199 None => return Ok(false),
2201 };
2202 let table_name = tbl.schema.table_name.clone();
2205 let tx_id = self.next_tx();
2206 self.wal_log(tx_id, WalRecordType::Update, &table_name, rid, &new_bytes)?;
2207 Ok(true)
2208 }
2209
2210 #[inline]
2222 pub fn patch_var_col_in_place(
2223 &mut self,
2224 table: &str,
2225 rid: RowId,
2226 col_idx: usize,
2227 new_value: Option<&[u8]>,
2228 ) -> io::Result<bool> {
2229 self.by_name_mut(table)?
2230 .patch_var_col_in_place(rid, col_idx, new_value)
2231 }
2232
2233 pub fn patch_var_col_logged(
2239 &mut self,
2240 table: &str,
2241 rid: RowId,
2242 col_idx: usize,
2243 new_value: Option<&[u8]>,
2244 ) -> io::Result<bool> {
2245 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
2246 io::Error::new(
2247 io::ErrorKind::NotFound,
2248 format!("table '{table}' not found"),
2249 )
2250 })?;
2251 let tbl = &mut self.tables[slot];
2252 let ok = tbl.patch_var_col_in_place(rid, col_idx, new_value)?;
2253 if !ok {
2254 return Ok(false);
2255 }
2256 if self.wal.is_off() {
2259 return Ok(true);
2260 }
2261 let new_bytes = match tbl.heap.get(rid) {
2262 Some(b) => b,
2263 None => return Ok(false),
2264 };
2265 let table_name = tbl.schema.table_name.clone();
2266 let tx_id = self.next_tx();
2267 self.wal_log(tx_id, WalRecordType::Update, &table_name, rid, &new_bytes)?;
2268 Ok(true)
2269 }
2270
2271 pub fn scan(&self, table: &str) -> io::Result<impl Iterator<Item = (RowId, Row)> + '_> {
2272 Ok(self.by_name(table)?.scan())
2273 }
2274
2275 pub fn for_each_row_raw<F>(&self, table: &str, f: F) -> io::Result<()>
2278 where
2279 F: FnMut(RowId, &[u8]),
2280 {
2281 self.by_name(table)?.for_each_row_raw(f);
2282 Ok(())
2283 }
2284
2285 pub fn try_for_each_row_raw<F>(&self, table: &str, f: F) -> io::Result<()>
2290 where
2291 F: FnMut(RowId, &[u8]) -> std::ops::ControlFlow<()>,
2292 {
2293 self.by_name(table)?.try_for_each_row_raw(f);
2294 Ok(())
2295 }
2296
2297 pub fn create_index(&mut self, table: &str, column: &str) -> io::Result<()> {
2298 self.create_index_unique(table, column, false)
2299 }
2300
2301 pub fn create_index_unique(
2306 &mut self,
2307 table: &str,
2308 column: &str,
2309 unique: bool,
2310 ) -> io::Result<()> {
2311 self.ensure_no_active_transaction_for_ddl("create index")?;
2312 self.invalidate_structure();
2313 let data_dir = self.data_dir.clone();
2314 self.by_name_mut(table)?
2315 .create_index_with_unique(column, &data_dir, unique)?;
2316 self.persist()
2320 }
2321
2322 pub fn active_catalog_version(&self) -> u16 {
2323 self.active_catalog_version
2324 }
2325
2326 pub fn next_index_id(&self) -> u64 {
2327 self.next_index_id
2328 }
2329
2330 pub fn index_metadata(&self, table: &str) -> Option<Vec<IndexMetadata>> {
2332 let table_ref = self.get_table(table)?;
2333 let mut metadata = table_ref
2334 .indexed_column_metas()
2335 .into_iter()
2336 .map(|index| IndexMetadata {
2337 unique: index.unique,
2338 source: IndexKeySource::Column { column: index.name },
2339 })
2340 .collect::<Vec<_>>();
2341 metadata.extend(table_ref.expression_index_metas().into_iter().map(|index| {
2342 IndexMetadata {
2343 unique: index.unique,
2344 source: IndexKeySource::Expression {
2345 index_id: index.index_id,
2346 canonical_version: index.canonical_version,
2347 canonical_text: index.canonical_text,
2348 json_path: index.json_path,
2349 },
2350 }
2351 }));
2352 Some(metadata)
2353 }
2354
2355 pub fn expression_index_metadata(&self, table: &str) -> Option<Vec<ExpressionIndexMeta>> {
2356 Some(self.get_table(table)?.expression_index_metas())
2357 }
2358
2359 pub fn expression_index_btree(&self, table: &str, index_id: u64) -> Option<&BTree> {
2360 self.get_table(table)?.expression_index_btree(index_id)
2361 }
2362
2363 pub fn index_stats(&self, table: &str, column: &str) -> Option<IndexStats> {
2367 Some(self.get_table(table)?.index(column)?.stats())
2368 }
2369
2370 pub fn expression_index_stats(&self, table: &str, index_id: u64) -> Option<IndexStats> {
2372 Some(
2373 self.get_table(table)?
2374 .expression_index_btree(index_id)?
2375 .stats(),
2376 )
2377 }
2378
2379 pub fn index_key_count_capped(
2385 &self,
2386 table: &str,
2387 column: &str,
2388 key: &Value,
2389 cap: usize,
2390 ) -> Option<usize> {
2391 let unique = self.is_index_unique(table, column)?;
2392 let tree = self.get_table(table)?.index(column)?;
2393 Some(if unique {
2394 tree.count_key_capped(key, cap)
2395 } else {
2396 tree.count_prefix_capped(key, cap)
2397 })
2398 }
2399
2400 pub fn expression_index_key_count_capped(
2403 &self,
2404 table: &str,
2405 index_id: u64,
2406 key: &Value,
2407 cap: usize,
2408 ) -> Option<usize> {
2409 Some(
2410 self.get_table(table)?
2411 .expression_index_btree(index_id)?
2412 .count_key_capped(key, cap),
2413 )
2414 }
2415
2416 pub fn expression_index_btree_mut(&mut self, table: &str, index_id: u64) -> Option<&mut BTree> {
2417 self.get_table_mut(table)?
2418 .expression_index_btree_mut(index_id)
2419 }
2420
2421 pub fn expression_index_lookup_all(
2422 &self,
2423 table: &str,
2424 index_id: u64,
2425 key: &Value,
2426 ) -> io::Result<Vec<RowId>> {
2427 let tree = self
2428 .by_name(table)?
2429 .expression_index_btree(index_id)
2430 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2431 Ok(tree.lookup_all(key))
2432 }
2433
2434 pub fn expression_index_range_rids(
2435 &self,
2436 table: &str,
2437 index_id: u64,
2438 start: Option<&Value>,
2439 end: Option<&Value>,
2440 ) -> io::Result<Vec<RowId>> {
2441 let tree = self
2442 .by_name(table)?
2443 .expression_index_btree(index_id)
2444 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2445 Ok(tree.raw_range_rids(start, end))
2446 }
2447
2448 pub fn expression_index_ordered_rids(
2449 &self,
2450 table: &str,
2451 index_id: u64,
2452 ) -> io::Result<Vec<RowId>> {
2453 let tree = self
2454 .by_name(table)?
2455 .expression_index_btree(index_id)
2456 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2457 Ok(tree.ordered_rids_nulls_last())
2458 }
2459
2460 pub fn expression_index_ordered_rids_bounded(
2461 &self,
2462 table: &str,
2463 index_id: u64,
2464 direction: IndexOrderDirection,
2465 offset: usize,
2466 limit: usize,
2467 ) -> io::Result<Vec<RowId>> {
2468 let tree = self
2469 .by_name(table)?
2470 .expression_index_btree(index_id)
2471 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2472 Ok(tree.bounded_ordered_rids_nulls_last(
2473 direction == IndexOrderDirection::Desc,
2474 offset,
2475 limit,
2476 ))
2477 }
2478
2479 pub fn drop_expression_index(&mut self, table: &str, index_id: u64) -> io::Result<()> {
2480 self.ensure_no_active_transaction_for_ddl("drop index")?;
2481 self.invalidate_structure();
2482 validate_table_name(table)?;
2483 let removed = self
2484 .by_name_mut(table)?
2485 .take_expression_index(index_id)
2486 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2487 match self.persist_at_activation_boundary() {
2488 Ok(()) => {}
2489 Err(CatalogPersistError::BeforeActivation(error)) => {
2490 self.by_name_mut(table)?.restore_expression_index(removed);
2491 return Err(error);
2492 }
2493 Err(CatalogPersistError::AfterActivation(error)) => {
2494 warn!(
2495 path = %self.data_dir.display(),
2496 error = %error,
2497 "expression index drop committed but catalog directory sync failed"
2498 );
2499 }
2500 }
2501 let index_path = self
2502 .data_dir
2503 .join(expression_index_file_name(table, index_id));
2504 if let Err(error) = fs::remove_file(&index_path) {
2505 if error.kind() != io::ErrorKind::NotFound {
2506 warn!(path = %index_path.display(), error = %error, "failed to remove dropped expression index file");
2507 }
2508 } else if let Err(error) = sync_directory(&self.data_dir) {
2509 warn!(path = %self.data_dir.display(), error = %error, "failed to sync expression index deletion");
2510 }
2511 Ok(())
2512 }
2513
2514 pub fn create_expression_index_metadata(
2518 &mut self,
2519 table: &str,
2520 canonical_version: u16,
2521 canonical_text: impl Into<String>,
2522 json_path: StoredJsonPathV1,
2523 unique: bool,
2524 ) -> io::Result<u64> {
2525 self.ensure_no_active_transaction_for_ddl("create index")?;
2526 self.invalidate_structure();
2527 validate_table_name(table)?;
2528 validate_column_name(&json_path.column)?;
2529 if canonical_version == 0 {
2530 return Err(io::Error::new(
2531 io::ErrorKind::InvalidInput,
2532 "expression canonical version must be non-zero",
2533 ));
2534 }
2535 let canonical_text = canonical_text.into();
2536 if canonical_text.is_empty() {
2537 return Err(io::Error::new(
2538 io::ErrorKind::InvalidInput,
2539 "expression canonical text must not be empty",
2540 ));
2541 }
2542 if canonical_version == 1 && canonical_text != json_path.canonical_text() {
2543 return Err(io::Error::new(
2544 io::ErrorKind::InvalidInput,
2545 "expression canonical text does not match its stored JSON path",
2546 ));
2547 }
2548 let table_ref = self.by_name(table)?;
2549 let root_index = table_ref
2550 .schema
2551 .column_index(&json_path.column)
2552 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "JSON root column not found"))?;
2553 if table_ref.schema.columns[root_index].type_id != TypeId::Json {
2554 return Err(io::Error::new(
2555 io::ErrorKind::InvalidInput,
2556 "expression index root column must have type json",
2557 ));
2558 }
2559 if table_ref.expression_index_metas().iter().any(|index| {
2560 index.canonical_version == canonical_version && index.canonical_text == canonical_text
2561 }) {
2562 return Err(io::Error::new(
2563 io::ErrorKind::AlreadyExists,
2564 "expression index already exists",
2565 ));
2566 }
2567
2568 let index_id = self.next_index_id;
2569 let next_index_id = index_id
2570 .checked_add(1)
2571 .ok_or_else(|| io::Error::other("expression index id space exhausted"))?;
2572 let index_path = self
2573 .data_dir
2574 .join(expression_index_file_name(table, index_id));
2575 if index_path.exists() {
2576 fs::remove_file(&index_path)?;
2580 sync_directory(&self.data_dir)?;
2581 }
2582 let meta = ExpressionIndexMeta {
2583 index_id,
2584 unique,
2585 canonical_version,
2586 canonical_text,
2587 json_path,
2588 };
2589 self.by_name_mut(table)?
2590 .install_expression_index(meta, &index_path)?;
2591 if let Err(error) = sync_directory(&self.data_dir) {
2592 self.by_name_mut(table)?
2593 .remove_expression_index_by_id(index_id);
2594 let _ = fs::remove_file(&index_path);
2595 return Err(error);
2596 }
2597
2598 let previous_version = self.active_catalog_version;
2599 let previous_next_id = self.next_index_id;
2600 self.active_catalog_version = self
2605 .active_catalog_version
2606 .max(EXPRESSION_INDEX_CATALOG_VERSION);
2607 self.next_index_id = next_index_id;
2608 match self.persist_at_activation_boundary() {
2609 Ok(()) => {}
2610 Err(CatalogPersistError::BeforeActivation(error)) => {
2611 self.by_name_mut(table)?
2612 .remove_expression_index_by_id(index_id);
2613 self.active_catalog_version = previous_version;
2614 self.next_index_id = previous_next_id;
2615 let _ = fs::remove_file(&index_path);
2616 let _ = sync_directory(&self.data_dir);
2617 return Err(error);
2618 }
2619 Err(CatalogPersistError::AfterActivation(error)) => {
2620 warn!(
2621 path = %self.data_dir.display(),
2622 error = %error,
2623 "expression index creation committed but catalog directory sync failed"
2624 );
2625 }
2626 }
2627 Ok(index_id)
2628 }
2629
2630 pub fn create_link(&mut self, def: LinkDef) -> io::Result<()> {
2647 self.ensure_no_active_transaction_for_ddl("create link")?;
2648 self.invalidate_structure();
2649 validate_table_name(&def.owner_type)?;
2650 validate_table_name(&def.target_type)?;
2651 validate_column_name(&def.name)?;
2652 validate_column_name(&def.local_key)?;
2653 validate_column_name(&def.target_key)?;
2654
2655 {
2658 let owner = self.by_name(&def.owner_type)?;
2659 if owner.schema.column_index(&def.local_key).is_none() {
2660 return Err(io::Error::new(
2661 io::ErrorKind::NotFound,
2662 format!(
2663 "link local key '{}' is not a column on owner type '{}'",
2664 def.local_key, def.owner_type
2665 ),
2666 ));
2667 }
2668 if owner.schema.column_index(&def.name).is_some() {
2669 return Err(io::Error::new(
2670 io::ErrorKind::AlreadyExists,
2671 format!(
2672 "link name '{}' collides with a column on owner type '{}'",
2673 def.name, def.owner_type
2674 ),
2675 ));
2676 }
2677 }
2678 if self
2679 .links
2680 .iter()
2681 .any(|l| l.owner_type == def.owner_type && l.name == def.name)
2682 {
2683 return Err(io::Error::new(
2684 io::ErrorKind::AlreadyExists,
2685 format!(
2686 "link '{}' already exists on owner type '{}'",
2687 def.name, def.owner_type
2688 ),
2689 ));
2690 }
2691
2692 {
2694 let target = self.by_name(&def.target_type)?;
2695 if target.schema.column_index(&def.target_key).is_none() {
2696 return Err(io::Error::new(
2697 io::ErrorKind::NotFound,
2698 format!(
2699 "link target key '{}' is not a column on target type '{}'",
2700 def.target_key, def.target_type
2701 ),
2702 ));
2703 }
2704 }
2705
2706 let kind = if self.is_index_unique(&def.target_type, &def.target_key) == Some(true) {
2709 LinkKind::ToOne
2710 } else {
2711 LinkKind::ToMany
2712 };
2713 let stored = LinkDef { kind, ..def };
2714
2715 self.links.push(stored);
2719 let previous_version = self.active_catalog_version;
2720 self.active_catalog_version = self.active_catalog_version.max(CATALOG_VERSION);
2721 match self.persist_at_activation_boundary() {
2722 Ok(()) => {}
2723 Err(CatalogPersistError::BeforeActivation(error)) => {
2724 self.links.pop();
2725 self.active_catalog_version = previous_version;
2726 let _ = sync_directory(&self.data_dir);
2727 return Err(error);
2728 }
2729 Err(CatalogPersistError::AfterActivation(error)) => {
2730 warn!(
2731 path = %self.data_dir.display(),
2732 error = %error,
2733 "link creation committed but catalog directory sync failed"
2734 );
2735 }
2736 }
2737 Ok(())
2738 }
2739
2740 pub fn link(&self, owner_type: &str, name: &str) -> Option<&LinkDef> {
2743 self.links
2744 .iter()
2745 .find(|l| l.owner_type == owner_type && l.name == name)
2746 }
2747
2748 pub fn links(&self) -> impl Iterator<Item = &LinkDef> + '_ {
2750 self.links.iter()
2751 }
2752
2753 pub fn drop_link(&mut self, owner_type: &str, name: &str) -> io::Result<()> {
2758 self.ensure_no_active_transaction_for_ddl("drop link")?;
2759 self.invalidate_structure();
2760 let idx = self
2761 .links
2762 .iter()
2763 .position(|l| l.owner_type == owner_type && l.name == name)
2764 .ok_or_else(|| {
2765 io::Error::new(
2766 io::ErrorKind::NotFound,
2767 format!("link '{name}' not found on owner type '{owner_type}'"),
2768 )
2769 })?;
2770 let removed = self.links.remove(idx);
2771 if let Err(error) = self.persist() {
2772 self.links.insert(idx, removed);
2774 return Err(error);
2775 }
2776 Ok(())
2777 }
2778
2779 fn link_referencing_table(&self, table: &str) -> Option<&LinkDef> {
2783 self.links
2784 .iter()
2785 .find(|l| l.owner_type == table || l.target_type == table)
2786 }
2787
2788 fn link_referencing_column(&self, table: &str, column: &str) -> Option<&LinkDef> {
2791 self.links.iter().find(|l| {
2792 (l.owner_type == table && l.local_key == column)
2793 || (l.target_type == table && l.target_key == column)
2794 })
2795 }
2796
2797 pub fn is_index_unique(&self, table: &str, column: &str) -> Option<bool> {
2801 self.get_table(table)?.is_index_unique(column)
2802 }
2803
2804 pub fn has_index(&self, table: &str, column: &str) -> bool {
2806 self.get_table(table)
2807 .map(|t| t.has_index(column))
2808 .unwrap_or(false)
2809 }
2810
2811 pub fn index_lookup(&self, table: &str, column: &str, key: &Value) -> io::Result<Option<Row>> {
2812 Ok(self
2813 .by_name(table)?
2814 .index_lookup(column, key)
2815 .map(|(_, row)| row))
2816 }
2817
2818 pub fn list_tables(&self) -> Vec<&str> {
2819 self.tables
2823 .iter()
2824 .map(|t| t.schema.table_name.as_str())
2825 .collect()
2826 }
2827
2828 pub fn schema(&self, table: &str) -> Option<&Schema> {
2829 let slot = *self.name_to_slot.get(table)?;
2830 Some(&self.tables[slot].schema)
2831 }
2832
2833 pub fn drop_table(&mut self, name: &str) -> io::Result<()> {
2836 self.ensure_no_active_transaction_for_ddl("drop table")?;
2837 self.invalidate_structure();
2838 validate_table_name(name)?;
2839 let slot = *self.name_to_slot.get(name).ok_or_else(|| {
2840 io::Error::new(io::ErrorKind::NotFound, format!("table '{name}' not found"))
2841 })?;
2842 if let Some(link) = self.link_referencing_table(name) {
2846 return Err(io::Error::new(
2847 io::ErrorKind::InvalidInput,
2848 format!(
2849 "cannot drop table '{name}': link '{}' on '{}' references it (drop the link first)",
2850 link.name, link.owner_type
2851 ),
2852 ));
2853 }
2854 if !self.wal.is_off() {
2855 let payload = encode_ddl_drop_table(name);
2856 self.wal.append(0, WalRecordType::DdlDropTable, &payload)?;
2857 self.wal.flush()?;
2858 }
2859 let table = &self.tables[slot];
2861 let heap_path = self
2862 .data_dir
2863 .join(format!("{}.heap", table.schema.table_name));
2864 if heap_path.exists() {
2865 fs::remove_file(&heap_path)?;
2866 }
2867 for col_name in table.indexed_column_names() {
2873 let idx_path = self.data_dir.join(format!("{name}_{col_name}.idx"));
2874 if idx_path.exists() {
2875 let _ = fs::remove_file(&idx_path);
2876 }
2877 }
2878 let expression_index_ids = table.expression_index_ids();
2879 self.name_to_slot.remove(name);
2881 let last = self.tables.len() - 1;
2882 if slot != last {
2883 let moved_name = self.tables[last].schema.table_name.clone();
2884 self.tables.swap(slot, last);
2885 self.name_to_slot.insert(moved_name, slot);
2886 }
2887 self.tables.pop();
2888 self.persist()?;
2889 for index_id in expression_index_ids {
2890 let idx_path = self
2891 .data_dir
2892 .join(expression_index_file_name(name, index_id));
2893 let _ = fs::remove_file(idx_path);
2894 }
2895 Ok(())
2896 }
2897
2898 pub fn alter_table_add_column(&mut self, table: &str, col: ColumnDef) -> io::Result<()> {
2921 self.ensure_no_active_transaction_for_ddl("alter table add column")?;
2922 self.invalidate_structure();
2923 let data_dir = self.data_dir.clone();
2924 {
2925 let tbl = self.by_name_mut(table)?;
2926 if tbl.schema.columns.iter().any(|c| c.name == col.name) {
2927 return Err(io::Error::new(
2928 io::ErrorKind::AlreadyExists,
2929 format!("column '{}' already exists in table '{table}'", col.name),
2930 ));
2931 }
2932 }
2933 let barrier_lsn = if !self.wal.is_off() {
2934 let payload = encode_ddl_alter_add_column(table, &col);
2935 self.wal.append(0, WalRecordType::DdlAddColumn, &payload)?;
2936 self.wal.flush()?;
2937 self.wal.last_appended_lsn()
2938 } else {
2939 0
2940 };
2941 let tbl = self.by_name_mut(table)?;
2942
2943 let old_schema = tbl.schema.clone();
2944
2945 let has_rows = tbl.heap.scan().next().is_some();
2949
2950 if has_rows && col.required {
2951 return Err(io::Error::new(
2952 io::ErrorKind::InvalidInput,
2953 format!(
2954 "cannot add required column '{}' to non-empty table '{table}': \
2955 no default value to backfill existing rows with",
2956 col.name
2957 ),
2958 ));
2959 }
2960
2961 tbl.schema.columns.push(col);
2965 tbl.refresh_layout();
2966
2967 if has_rows {
2968 let fill: Vec<Value> = vec![Value::Empty; tbl.schema.columns.len()];
2973 tbl.rewrite_rows_for_schema_change(&old_schema, &fill, &data_dir)?;
2974 }
2975 if barrier_lsn > 0 {
2983 tbl.heap.stamp_all_pages_min_lsn(barrier_lsn)?;
2984 tbl.heap.flush()?;
2985 }
2986
2987 self.persist()?;
2988 Ok(())
2989 }
2990
2991 pub fn alter_table_drop_column(&mut self, table: &str, col_name: &str) -> io::Result<()> {
3014 self.ensure_no_active_transaction_for_ddl("alter table drop column")?;
3015 self.invalidate_structure();
3016 let data_dir = self.data_dir.clone();
3017 {
3018 let tbl = self.by_name_mut(table)?;
3019 tbl.schema
3020 .columns
3021 .iter()
3022 .position(|c| c.name == col_name)
3023 .ok_or_else(|| {
3024 io::Error::new(
3025 io::ErrorKind::NotFound,
3026 format!("column '{col_name}' not found in table '{table}'"),
3027 )
3028 })?;
3029 }
3030 if let Some(link) = self.link_referencing_column(table, col_name) {
3033 return Err(io::Error::new(
3034 io::ErrorKind::InvalidInput,
3035 format!(
3036 "cannot drop column '{col_name}' from '{table}': link '{}' on '{}' references it (drop the link first)",
3037 link.name, link.owner_type
3038 ),
3039 ));
3040 }
3041 let removed_expression_index_ids = self
3042 .by_name_mut(table)?
3043 .remove_expression_indexes_for_root(col_name);
3044 let barrier_lsn = if !self.wal.is_off() {
3045 let payload = encode_ddl_alter_drop_column(table, col_name);
3046 self.wal.append(0, WalRecordType::DdlDropColumn, &payload)?;
3047 self.wal.flush()?;
3048 self.wal.last_appended_lsn()
3049 } else {
3050 0
3051 };
3052 let tbl = self.by_name_mut(table)?;
3053 let idx = tbl
3054 .schema
3055 .columns
3056 .iter()
3057 .position(|c| c.name == col_name)
3058 .ok_or_else(|| {
3059 io::Error::new(
3060 io::ErrorKind::NotFound,
3061 format!("column '{col_name}' not found in table '{table}'"),
3062 )
3063 })?;
3064
3065 let old_schema = tbl.schema.clone();
3067 let has_rows = tbl.heap.scan().next().is_some();
3068
3069 tbl.schema.columns.remove(idx);
3071 for (i, col) in tbl.schema.columns.iter_mut().enumerate() {
3072 col.position = i as u16;
3073 }
3074 tbl.refresh_layout();
3075
3076 if has_rows {
3077 let fill: Vec<Value> = vec![Value::Empty; tbl.schema.columns.len()];
3083 tbl.rewrite_rows_for_schema_change(&old_schema, &fill, &data_dir)?;
3084 }
3085 if barrier_lsn > 0 {
3087 tbl.heap.stamp_all_pages_min_lsn(barrier_lsn)?;
3088 tbl.heap.flush()?;
3089 }
3090
3091 self.persist()?;
3092 for index_id in removed_expression_index_ids {
3093 let idx_path = self
3094 .data_dir
3095 .join(expression_index_file_name(table, index_id));
3096 let _ = fs::remove_file(idx_path);
3097 }
3098 Ok(())
3099 }
3100}
3101
3102impl Drop for Catalog {
3103 fn drop(&mut self) {
3104 if self.read_only {
3108 return;
3109 }
3110 if self.active_tx_id.is_some() {
3111 if let Err(e) = self.abandon_active_transaction_for_drop() {
3112 warn!(error = %e, "catalog drop active transaction cleanup failed");
3113 }
3114 return;
3115 }
3116 if let Err(e) = self.checkpoint() {
3128 warn!(error = %e, "catalog drop checkpoint failed");
3129 }
3130 }
3131}
3132
3133fn encode_wal_payload(table: &str, rid: RowId, row_bytes: &[u8]) -> Vec<u8> {
3148 let name = table.as_bytes();
3149 let mut out = Vec::with_capacity(4 + name.len() + 4 + 2 + 4 + row_bytes.len());
3150 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3151 out.extend_from_slice(name);
3152 out.extend_from_slice(&rid.page_id.to_le_bytes());
3153 out.extend_from_slice(&rid.slot_index.to_le_bytes());
3154 out.extend_from_slice(&(row_bytes.len() as u32).to_le_bytes());
3155 out.extend_from_slice(row_bytes);
3156 out
3157}
3158
3159fn decode_wal_payload(data: &[u8]) -> Option<(String, RowId, Vec<u8>)> {
3160 let mut pos = 0usize;
3161 if data.len() < 4 {
3162 return None;
3163 }
3164 let name_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3165 pos += 4;
3166 if pos + name_len > data.len() {
3167 return None;
3168 }
3169 let name = std::str::from_utf8(&data[pos..pos + name_len])
3170 .ok()?
3171 .to_string();
3172 pos += name_len;
3173 if pos + 4 + 2 + 4 > data.len() {
3174 return None;
3175 }
3176 let page_id = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
3177 pos += 4;
3178 let slot_index = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
3179 pos += 2;
3180 let row_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3181 pos += 4;
3182 if pos + row_len > data.len() {
3183 return None;
3184 }
3185 let row_bytes = data[pos..pos + row_len].to_vec();
3186 Some((
3187 name,
3188 RowId {
3189 page_id,
3190 slot_index,
3191 },
3192 row_bytes,
3193 ))
3194}
3195
3196fn write_overflow_chain_logged(
3202 heap: &mut HeapFile,
3203 wal: &mut Wal,
3204 table: &str,
3205 tx_id: u64,
3206 value: &[u8],
3207) -> io::Result<OverflowStub> {
3208 if value.len() > MAX_VALUE_SIZE {
3209 return Err(StorageError::ValueTooLarge {
3210 size: value.len(),
3211 max: MAX_VALUE_SIZE,
3212 }
3213 .into());
3214 }
3215 let n = value.len().div_ceil(OVERFLOW_PAYLOAD_CAP).max(1);
3216 let mut pages = Vec::with_capacity(n);
3217 for _ in 0..n {
3218 pages.push(heap.allocate_overflow_page()?);
3219 }
3220 for i in 0..n {
3221 let start = i * OVERFLOW_PAYLOAD_CAP;
3222 let end = (start + OVERFLOW_PAYLOAD_CAP).min(value.len());
3223 let chunk = &value[start..end];
3224 let next = if i + 1 < n {
3225 pages[i + 1]
3226 } else {
3227 OVERFLOW_CHAIN_END
3228 };
3229 let payload = encode_overflow_write_payload(table, pages[i], next, chunk);
3230 wal.append(tx_id, WalRecordType::OverflowWrite, &payload)?;
3231 let lsn = wal.last_appended_lsn();
3232 heap.write_overflow_page(pages[i], next, chunk, lsn)?;
3233 }
3234 Ok(OverflowStub::new(
3235 value.len() as u64,
3236 pages[0],
3237 crc32fast::hash(value),
3238 ))
3239}
3240
3241fn encode_row_with_spill_logged(
3246 tbl: &mut Table,
3247 wal: &mut Wal,
3248 tx_id: u64,
3249 values: &Row,
3250) -> io::Result<Vec<u8>> {
3251 let v1_len = crate::row::v1_encoded_len(tbl.row_layout(), values);
3254 let is_indexed = tbl.indexed_col_mask();
3255 let chosen = plan_spill(tbl.row_layout(), values, v1_len, &is_indexed);
3256 if chosen.is_empty() {
3257 let mut v1 = Vec::new();
3258 encode_row_into(&tbl.schema, values, &mut v1);
3259 return Ok(v1);
3260 }
3261 let table_name = tbl.schema.table_name.clone();
3262 let n_var = tbl.row_layout().n_var();
3263 let mut spilled: Vec<Option<OverflowStub>> = vec![None; n_var];
3264 for col_idx in chosen {
3265 let var_idx = tbl
3266 .row_layout()
3267 .var_index(col_idx)
3268 .expect("plan_spill only returns var columns");
3269 let bytes: Vec<u8> = match &values[col_idx] {
3270 Value::Str(s) => s.as_bytes().to_vec(),
3271 Value::Bytes(b) => b.to_vec(),
3272 Value::Json(b) => b.to_vec(),
3273 _ => continue,
3274 };
3275 let stub = write_overflow_chain_logged(&mut tbl.heap, wal, &table_name, tx_id, &bytes)?;
3276 spilled[var_idx] = Some(stub);
3277 }
3278 let mut out = Vec::new();
3279 encode_row_v2_into(&tbl.schema, tbl.row_layout(), values, &spilled, &mut out);
3280 Ok(out)
3281}
3282
3283fn encode_overflow_write_payload(
3292 table: &str,
3293 page_id: u32,
3294 next_page: u32,
3295 chunk: &[u8],
3296) -> Vec<u8> {
3297 let name = table.as_bytes();
3298 let mut out = Vec::with_capacity(2 + name.len() + 4 + 4 + 2 + chunk.len());
3299 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
3300 out.extend_from_slice(name);
3301 out.extend_from_slice(&page_id.to_le_bytes());
3302 out.extend_from_slice(&next_page.to_le_bytes());
3303 out.extend_from_slice(&(chunk.len() as u16).to_le_bytes());
3304 out.extend_from_slice(chunk);
3305 out
3306}
3307
3308fn decode_overflow_write_payload(data: &[u8]) -> Option<(String, u32, u32, Vec<u8>)> {
3309 let mut pos = 0usize;
3310 if data.len() < 2 {
3311 return None;
3312 }
3313 let name_len = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
3314 pos += 2;
3315 if pos + name_len + 4 + 4 + 2 > data.len() {
3316 return None;
3317 }
3318 let name = std::str::from_utf8(&data[pos..pos + name_len])
3319 .ok()?
3320 .to_string();
3321 pos += name_len;
3322 let page_id = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
3323 pos += 4;
3324 let next_page = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
3325 pos += 4;
3326 let chunk_len = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
3327 pos += 2;
3328 if pos + chunk_len > data.len() {
3329 return None;
3330 }
3331 Some((
3332 name,
3333 page_id,
3334 next_page,
3335 data[pos..pos + chunk_len].to_vec(),
3336 ))
3337}
3338
3339fn encode_overflow_free_payload(table: &str, pages: &[u32]) -> Vec<u8> {
3343 let name = table.as_bytes();
3344 let mut out = Vec::with_capacity(2 + name.len() + 4 + pages.len() * 4);
3345 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
3346 out.extend_from_slice(name);
3347 out.extend_from_slice(&(pages.len() as u32).to_le_bytes());
3348 for p in pages {
3349 out.extend_from_slice(&p.to_le_bytes());
3350 }
3351 out
3352}
3353
3354fn decode_overflow_free_payload(data: &[u8]) -> Option<(String, Vec<u32>)> {
3355 let mut pos = 0usize;
3356 if data.len() < 2 {
3357 return None;
3358 }
3359 let name_len = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
3360 pos += 2;
3361 if pos + name_len + 4 > data.len() {
3362 return None;
3363 }
3364 let name = std::str::from_utf8(&data[pos..pos + name_len])
3365 .ok()?
3366 .to_string();
3367 pos += name_len;
3368 let count = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3369 pos += 4;
3370 if pos + count * 4 > data.len() {
3371 return None;
3372 }
3373 let mut pages = Vec::with_capacity(count);
3374 for _ in 0..count {
3375 pages.push(u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?));
3376 pos += 4;
3377 }
3378 Some((name, pages))
3379}
3380
3381fn encode_ddl_create_table(
3384 schema: &Schema,
3385 defaults: &[Option<Value>],
3386 auto_cols: &[bool],
3387) -> Vec<u8> {
3388 let name = schema.table_name.as_bytes();
3389 let mut out = Vec::new();
3390 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3391 out.extend_from_slice(name);
3392 out.extend_from_slice(&(schema.columns.len() as u16).to_le_bytes());
3393 for col in &schema.columns {
3394 let cn = col.name.as_bytes();
3395 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3396 out.extend_from_slice(cn);
3397 out.push(col.type_id as u8);
3398 out.push(col.required as u8);
3399 out.extend_from_slice(&col.position.to_le_bytes());
3400 }
3401 encode_defaults_section(&mut out, defaults);
3405 encode_auto_section(&mut out, auto_cols);
3406 out
3407}
3408
3409fn decode_ddl_create_table(data: &[u8]) -> Option<(Schema, Vec<Option<Value>>, Vec<bool>)> {
3410 let mut pos = 0usize;
3411 if data.len() < 4 {
3412 return None;
3413 }
3414 let name_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3415 pos += 4;
3416 if pos + name_len > data.len() {
3417 return None;
3418 }
3419 let table_name = std::str::from_utf8(&data[pos..pos + name_len])
3420 .ok()?
3421 .to_string();
3422 pos += name_len;
3423 if pos + 2 > data.len() {
3424 return None;
3425 }
3426 let n_cols = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
3427 pos += 2;
3428 let mut columns = Vec::with_capacity(n_cols);
3429 for _ in 0..n_cols {
3430 if pos + 4 > data.len() {
3431 return None;
3432 }
3433 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3434 pos += 4;
3435 if pos + cn_len + 4 > data.len() {
3436 return None;
3437 }
3438 let col_name = std::str::from_utf8(&data[pos..pos + cn_len])
3439 .ok()?
3440 .to_string();
3441 pos += cn_len;
3442 let type_id = TypeId::from_u8(data[pos])?;
3443 pos += 1;
3444 let required = data[pos] != 0;
3445 pos += 1;
3446 if pos + 2 > data.len() {
3447 return None;
3448 }
3449 let position = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
3450 pos += 2;
3451 columns.push(ColumnDef {
3452 name: col_name,
3453 type_id,
3454 required,
3455 position,
3456 });
3457 }
3458 let defaults = if pos < data.len() {
3461 decode_defaults_section(data, &mut pos, columns.len())?
3462 } else {
3463 Vec::new()
3464 };
3465 let auto_cols = if pos < data.len() {
3466 decode_auto_section(data, &mut pos, columns.len())?
3467 } else {
3468 Vec::new()
3469 };
3470 Some((
3471 Schema {
3472 table_name,
3473 columns,
3474 },
3475 defaults,
3476 auto_cols,
3477 ))
3478}
3479
3480fn encode_ddl_drop_table(table_name: &str) -> Vec<u8> {
3481 let name = table_name.as_bytes();
3482 let mut out = Vec::with_capacity(4 + name.len());
3483 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3484 out.extend_from_slice(name);
3485 out
3486}
3487
3488fn encode_ddl_alter_add_column(table_name: &str, col: &ColumnDef) -> Vec<u8> {
3489 let name = table_name.as_bytes();
3490 let cn = col.name.as_bytes();
3491 let mut out = Vec::with_capacity(4 + name.len() + 4 + cn.len() + 4);
3492 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3493 out.extend_from_slice(name);
3494 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3495 out.extend_from_slice(cn);
3496 out.push(col.type_id as u8);
3497 out.push(col.required as u8);
3498 out.extend_from_slice(&col.position.to_le_bytes());
3499 out
3500}
3501
3502fn encode_ddl_alter_drop_column(table_name: &str, col_name: &str) -> Vec<u8> {
3503 let name = table_name.as_bytes();
3504 let cn = col_name.as_bytes();
3505 let mut out = Vec::with_capacity(4 + name.len() + 4 + cn.len());
3506 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3507 out.extend_from_slice(name);
3508 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3509 out.extend_from_slice(cn);
3510 out
3511}
3512
3513fn decode_ddl_table_name(data: &[u8]) -> Option<(String, usize)> {
3514 if data.len() < 4 {
3515 return None;
3516 }
3517 let name_len = u32::from_le_bytes(data[0..4].try_into().ok()?) as usize;
3518 if 4 + name_len > data.len() {
3519 return None;
3520 }
3521 let name = std::str::from_utf8(&data[4..4 + name_len])
3522 .ok()?
3523 .to_string();
3524 Some((name, 4 + name_len))
3525}
3526
3527fn decode_ddl_alter_add_column(data: &[u8]) -> Option<(String, ColumnDef)> {
3528 let (table_name, mut pos) = decode_ddl_table_name(data)?;
3529 if pos + 4 > data.len() {
3530 return None;
3531 }
3532 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3533 pos += 4;
3534 if pos + cn_len + 4 > data.len() {
3535 return None;
3536 }
3537 let col_name = std::str::from_utf8(&data[pos..pos + cn_len])
3538 .ok()?
3539 .to_string();
3540 pos += cn_len;
3541 let type_id = TypeId::from_u8(data[pos])?;
3542 pos += 1;
3543 let required = data[pos] != 0;
3544 pos += 1;
3545 if pos + 2 > data.len() {
3546 return None;
3547 }
3548 let position = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
3549 Some((
3550 table_name,
3551 ColumnDef {
3552 name: col_name,
3553 type_id,
3554 required,
3555 position,
3556 },
3557 ))
3558}
3559
3560fn decode_ddl_alter_drop_column(data: &[u8]) -> Option<(String, String)> {
3561 let (table_name, pos) = decode_ddl_table_name(data)?;
3562 if pos + 4 > data.len() {
3563 return None;
3564 }
3565 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3566 if pos + 4 + cn_len > data.len() {
3567 return None;
3568 }
3569 let col_name = std::str::from_utf8(&data[pos + 4..pos + 4 + cn_len])
3570 .ok()?
3571 .to_string();
3572 Some((table_name, col_name))
3573}
3574
3575pub(crate) struct IndexedColMeta {
3603 pub name: String,
3604 pub unique: bool,
3605}
3606
3607pub(crate) struct CatalogEntry {
3610 pub schema: Schema,
3611 pub indexed_cols: Vec<IndexedColMeta>,
3612 pub expression_indexes: Vec<ExpressionIndexMeta>,
3613 pub defaults: Vec<Option<Value>>,
3616 pub auto_cols: Vec<bool>,
3619}
3620
3621pub(crate) struct CatalogEntryRef<'a> {
3623 pub schema: &'a Schema,
3624 pub indexed_cols: Vec<IndexedColMeta>,
3625 pub expression_indexes: Vec<ExpressionIndexMeta>,
3626 pub defaults: &'a [Option<Value>],
3627 pub auto_cols: &'a [bool],
3628}
3629
3630fn encode_value_blob(out: &mut Vec<u8>, v: &Value) {
3636 out.push(v.type_id() as u8);
3637 match v {
3638 Value::Int(n) => out.extend_from_slice(&n.to_le_bytes()),
3639 Value::Float(f) => out.extend_from_slice(&f.to_bits().to_le_bytes()),
3640 Value::Bool(b) => out.push(*b as u8),
3641 Value::Str(s) => {
3642 out.extend_from_slice(&(s.len() as u32).to_le_bytes());
3643 out.extend_from_slice(s.as_bytes());
3644 }
3645 Value::DateTime(n) => out.extend_from_slice(&n.to_le_bytes()),
3646 Value::Uuid(u) => out.extend_from_slice(u),
3647 Value::Bytes(b) => {
3648 out.extend_from_slice(&(b.len() as u32).to_le_bytes());
3649 out.extend_from_slice(b);
3650 }
3651 Value::Json(b) => {
3652 out.extend_from_slice(&(b.len() as u32).to_le_bytes());
3653 out.extend_from_slice(b);
3654 }
3655 Value::Empty => {}
3656 }
3657}
3658
3659fn decode_value_blob(data: &[u8], pos: &mut usize) -> Option<Value> {
3662 let tag = *data.get(*pos)?;
3663 *pos += 1;
3664 let type_id = TypeId::from_u8(tag)?;
3665 let take_fixed = |pos: &mut usize, n: usize| -> Option<Vec<u8>> {
3666 if *pos + n > data.len() {
3667 return None;
3668 }
3669 let slice = data[*pos..*pos + n].to_vec();
3670 *pos += n;
3671 Some(slice)
3672 };
3673 match type_id {
3674 TypeId::Empty => Some(Value::Empty),
3675 TypeId::Int => Some(Value::Int(i64::from_le_bytes(
3676 take_fixed(pos, 8)?.try_into().ok()?,
3677 ))),
3678 TypeId::Float => Some(Value::Float(f64::from_bits(u64::from_le_bytes(
3679 take_fixed(pos, 8)?.try_into().ok()?,
3680 )))),
3681 TypeId::Bool => Some(Value::Bool(take_fixed(pos, 1)?[0] != 0)),
3682 TypeId::DateTime => Some(Value::DateTime(i64::from_le_bytes(
3683 take_fixed(pos, 8)?.try_into().ok()?,
3684 ))),
3685 TypeId::Uuid => Some(Value::Uuid(take_fixed(pos, 16)?.try_into().ok()?)),
3686 TypeId::Str => {
3687 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
3688 Some(Value::Str(String::from_utf8(take_fixed(pos, len)?).ok()?))
3689 }
3690 TypeId::Bytes => {
3691 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
3692 Some(Value::Bytes(take_fixed(pos, len)?))
3693 }
3694 TypeId::Json => {
3695 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
3696 Some(Value::Json(take_fixed(pos, len)?.into()))
3697 }
3698 }
3699}
3700
3701fn encode_defaults_section(out: &mut Vec<u8>, defaults: &[Option<Value>]) {
3705 let present: Vec<(u16, &Value)> = defaults
3706 .iter()
3707 .enumerate()
3708 .filter_map(|(i, d)| d.as_ref().map(|v| (i as u16, v)))
3709 .collect();
3710 out.extend_from_slice(&(present.len() as u16).to_le_bytes());
3711 for (pos, v) in present {
3712 out.extend_from_slice(&pos.to_le_bytes());
3713 encode_value_blob(out, v);
3714 }
3715}
3716
3717fn decode_defaults_section(
3720 data: &[u8],
3721 pos: &mut usize,
3722 n_cols: usize,
3723) -> Option<Vec<Option<Value>>> {
3724 if *pos + 2 > data.len() {
3725 return None;
3726 }
3727 let count = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3728 *pos += 2;
3729 let mut out = vec![None; n_cols];
3730 for _ in 0..count {
3731 if *pos + 2 > data.len() {
3732 return None;
3733 }
3734 let col = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3735 *pos += 2;
3736 let value = decode_value_blob(data, pos)?;
3737 if col < n_cols {
3738 out[col] = Some(value);
3739 }
3740 }
3741 Some(out)
3742}
3743
3744fn encode_auto_section(out: &mut Vec<u8>, auto_cols: &[bool]) {
3748 let present: Vec<u16> = auto_cols
3749 .iter()
3750 .enumerate()
3751 .filter_map(|(i, &a)| if a { Some(i as u16) } else { None })
3752 .collect();
3753 out.extend_from_slice(&(present.len() as u16).to_le_bytes());
3754 for pos in present {
3755 out.extend_from_slice(&pos.to_le_bytes());
3756 }
3757}
3758
3759fn decode_auto_section(data: &[u8], pos: &mut usize, n_cols: usize) -> Option<Vec<bool>> {
3762 if *pos + 2 > data.len() {
3763 return None;
3764 }
3765 let count = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3766 *pos += 2;
3767 let mut out = vec![false; n_cols];
3768 for _ in 0..count {
3769 if *pos + 2 > data.len() {
3770 return None;
3771 }
3772 let col = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3773 *pos += 2;
3774 if col < n_cols {
3775 out[col] = true;
3776 }
3777 }
3778 Some(out)
3779}
3780
3781fn push_catalog_string(out: &mut Vec<u8>, value: &str) -> io::Result<()> {
3782 let len = u32::try_from(value.len())
3783 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "catalog string is too large"))?;
3784 out.extend_from_slice(&len.to_le_bytes());
3785 out.extend_from_slice(value.as_bytes());
3786 Ok(())
3787}
3788
3789fn encode_expression_indexes(out: &mut Vec<u8>, indexes: &[ExpressionIndexMeta]) -> io::Result<()> {
3790 let count = u16::try_from(indexes.len()).map_err(|_| {
3791 io::Error::new(
3792 io::ErrorKind::InvalidInput,
3793 "too many expression indexes on one table",
3794 )
3795 })?;
3796 out.extend_from_slice(&count.to_le_bytes());
3797 for index in indexes {
3798 out.extend_from_slice(&index.index_id.to_le_bytes());
3799 out.push(u8::from(index.unique));
3800 out.extend_from_slice(&index.canonical_version.to_le_bytes());
3801 push_catalog_string(out, &index.canonical_text)?;
3802 push_catalog_string(out, &index.json_path.column)?;
3803 let segment_count = u16::try_from(index.json_path.segments.len()).map_err(|_| {
3804 io::Error::new(
3805 io::ErrorKind::InvalidInput,
3806 "JSON path has too many segments",
3807 )
3808 })?;
3809 out.extend_from_slice(&segment_count.to_le_bytes());
3810 for segment in &index.json_path.segments {
3811 match segment {
3812 StoredJsonPathSegmentV1::Key(key) => {
3813 out.push(1);
3814 push_catalog_string(out, key)?;
3815 }
3816 StoredJsonPathSegmentV1::Index(position) => {
3817 out.push(2);
3818 out.extend_from_slice(&position.to_le_bytes());
3819 }
3820 }
3821 }
3822 }
3823 Ok(())
3824}
3825
3826fn decode_expression_indexes(data: &[u8], pos: &mut usize) -> io::Result<Vec<ExpressionIndexMeta>> {
3827 let count = read_u16(data, pos)? as usize;
3828 let mut indexes = Vec::with_capacity(count);
3829 for _ in 0..count {
3830 let index_id = read_u64(data, pos)?;
3831 if index_id == 0 {
3832 return Err(io::Error::new(
3833 io::ErrorKind::InvalidData,
3834 "expression index id must be non-zero",
3835 ));
3836 }
3837 let unique = read_u8(data, pos)? != 0;
3838 let canonical_version = read_u16(data, pos)?;
3839 let canonical_len = read_u32(data, pos)? as usize;
3840 let canonical_text = read_string(data, pos, canonical_len)?;
3841 let column_len = read_u32(data, pos)? as usize;
3842 let column = read_string(data, pos, column_len)?;
3843 let segment_count = read_u16(data, pos)? as usize;
3844 let mut segments = Vec::with_capacity(segment_count);
3845 for _ in 0..segment_count {
3846 match read_u8(data, pos)? {
3847 1 => {
3848 let len = read_u32(data, pos)? as usize;
3849 segments.push(StoredJsonPathSegmentV1::Key(read_string(data, pos, len)?));
3850 }
3851 2 => segments.push(StoredJsonPathSegmentV1::Index(read_u32(data, pos)?)),
3852 tag => {
3853 return Err(io::Error::new(
3854 io::ErrorKind::InvalidData,
3855 format!("unknown stored JSON path segment tag: {tag}"),
3856 ));
3857 }
3858 }
3859 }
3860 indexes.push(ExpressionIndexMeta {
3861 index_id,
3862 unique,
3863 canonical_version,
3864 canonical_text,
3865 json_path: StoredJsonPathV1 { column, segments },
3866 });
3867 }
3868 Ok(indexes)
3869}
3870
3871fn encode_links_section(out: &mut Vec<u8>, links: &[LinkDef]) -> io::Result<()> {
3875 let count = u32::try_from(links.len())
3876 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "too many catalog links"))?;
3877 out.extend_from_slice(&count.to_le_bytes());
3878 for link in links {
3879 push_catalog_string(out, &link.owner_type)?;
3880 push_catalog_string(out, &link.name)?;
3881 push_catalog_string(out, &link.target_type)?;
3882 push_catalog_string(out, &link.local_key)?;
3883 push_catalog_string(out, &link.target_key)?;
3884 out.push(link.kind.to_u8());
3885 }
3886 Ok(())
3887}
3888
3889fn decode_links_section(data: &[u8], pos: &mut usize) -> io::Result<Vec<LinkDef>> {
3893 let count = read_u32(data, pos)? as usize;
3894 if count > data.len() {
3895 return Err(io::Error::new(
3896 io::ErrorKind::InvalidData,
3897 format!("catalog file corrupt: implausible link count {count}"),
3898 ));
3899 }
3900 let mut links = Vec::with_capacity(count);
3901 for _ in 0..count {
3902 let owner_type = read_len_prefixed_string(data, pos)?;
3903 let name = read_len_prefixed_string(data, pos)?;
3904 let target_type = read_len_prefixed_string(data, pos)?;
3905 let local_key = read_len_prefixed_string(data, pos)?;
3906 let target_key = read_len_prefixed_string(data, pos)?;
3907 let kind = LinkKind::from_u8(read_u8(data, pos)?)?;
3908 links.push(LinkDef {
3909 owner_type,
3910 name,
3911 target_type,
3912 local_key,
3913 target_key,
3914 kind,
3915 });
3916 }
3917 Ok(links)
3918}
3919
3920fn read_len_prefixed_string(data: &[u8], pos: &mut usize) -> io::Result<String> {
3921 let len = read_u32(data, pos)? as usize;
3922 read_string(data, pos, len)
3923}
3924
3925fn write_catalog_file(
3926 path: &Path,
3927 version: u16,
3928 next_index_id: u64,
3929 entries: &[CatalogEntryRef<'_>],
3930 links: &[LinkDef],
3931) -> io::Result<()> {
3932 if !(1..=CATALOG_VERSION).contains(&version) {
3933 return Err(io::Error::new(
3934 io::ErrorKind::InvalidInput,
3935 format!("unsupported catalog write version: {version}"),
3936 ));
3937 }
3938 let mut buf: Vec<u8> = Vec::with_capacity(64);
3939 buf.extend_from_slice(CATALOG_MAGIC);
3940 buf.extend_from_slice(&version.to_le_bytes());
3941 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
3942 if version >= 6 {
3943 buf.extend_from_slice(&next_index_id.to_le_bytes());
3944 }
3945
3946 for entry in entries {
3947 let schema = entry.schema;
3948 let name = schema.table_name.as_bytes();
3949 buf.extend_from_slice(&(name.len() as u32).to_le_bytes());
3950 buf.extend_from_slice(name);
3951 buf.extend_from_slice(&(schema.columns.len() as u16).to_le_bytes());
3952 for col in &schema.columns {
3953 let cn = col.name.as_bytes();
3954 buf.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3955 buf.extend_from_slice(cn);
3956 buf.push(col.type_id as u8);
3957 buf.push(if col.required { 1 } else { 0 });
3958 buf.extend_from_slice(&col.position.to_le_bytes());
3959 }
3960 buf.extend_from_slice(&(entry.indexed_cols.len() as u16).to_le_bytes());
3962 for meta in &entry.indexed_cols {
3963 let cn = meta.name.as_bytes();
3964 buf.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3965 buf.extend_from_slice(cn);
3966 buf.push(if meta.unique { 1 } else { 0 });
3967 }
3968 encode_defaults_section(&mut buf, entry.defaults);
3970 encode_auto_section(&mut buf, entry.auto_cols);
3972 if version >= 6 {
3973 encode_expression_indexes(&mut buf, &entry.expression_indexes)?;
3974 }
3975 }
3976
3977 if version >= 7 {
3982 encode_links_section(&mut buf, links)?;
3983 }
3984
3985 let crc = crc32fast::hash(&buf);
3989 buf.extend_from_slice(&crc.to_le_bytes());
3990
3991 let mut f = fs::OpenOptions::new()
3992 .create(true)
3993 .write(true)
3994 .truncate(true)
3995 .open(path)?;
3996 f.write_all(&buf)?;
3997 f.sync_data()?;
3998 Ok(())
3999}
4000
4001struct CatalogFile {
4002 version: u16,
4003 next_index_id: u64,
4004 entries: Vec<CatalogEntry>,
4005 links: Vec<LinkDef>,
4006}
4007
4008fn read_catalog_file(path: &Path) -> io::Result<CatalogFile> {
4009 read_catalog_file_with_max_version(path, CATALOG_VERSION)
4010}
4011
4012pub fn read_active_catalog_version(data_dir: &Path) -> io::Result<u16> {
4018 let cat_path = data_dir.join(CATALOG_FILE);
4019 Ok(read_catalog_file(&cat_path)?.version)
4020}
4021
4022fn read_catalog_file_with_max_version(
4023 path: &Path,
4024 max_supported_version: u16,
4025) -> io::Result<CatalogFile> {
4026 let mut f = fs::File::open(path)?;
4027 let mut buf = Vec::new();
4028 f.read_to_end(&mut buf)?;
4029
4030 let mut pos = 0usize;
4031 if buf.len() < 14 || &buf[0..4] != CATALOG_MAGIC {
4033 return Err(io::Error::new(
4034 io::ErrorKind::InvalidData,
4035 "bad catalog magic",
4036 ));
4037 }
4038
4039 let payload = &buf[..buf.len() - 4];
4041 let stored_crc = u32::from_le_bytes(
4042 buf[buf.len() - 4..]
4043 .try_into()
4044 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog CRC"))?,
4045 );
4046 let computed_crc = crc32fast::hash(payload);
4047 if stored_crc != computed_crc {
4048 return Err(io::Error::new(
4049 io::ErrorKind::InvalidData,
4050 format!(
4051 "catalog CRC32 mismatch: expected {stored_crc:#010x}, got {computed_crc:#010x}"
4052 ),
4053 ));
4054 }
4055 let buf = &buf[..buf.len() - 4];
4057 pos += 4;
4058 let version = u16::from_le_bytes(
4059 buf[pos..pos + 2]
4060 .try_into()
4061 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog header"))?,
4062 );
4063 pos += 2;
4064 if version == 0 || version > max_supported_version {
4074 return Err(io::Error::new(
4075 io::ErrorKind::InvalidData,
4076 format!("unsupported catalog version: {version}"),
4077 ));
4078 }
4079 let n_tables = u32::from_le_bytes(
4080 buf[pos..pos + 4]
4081 .try_into()
4082 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog header"))?,
4083 ) as usize;
4084 pos += 4;
4085 let next_index_id = if version >= 6 {
4095 let id = read_u64(buf, &mut pos)?;
4096 if id == 0 {
4097 return Err(io::Error::new(
4098 io::ErrorKind::InvalidData,
4099 "catalog next index id must be non-zero",
4100 ));
4101 }
4102 id
4103 } else {
4104 1
4107 };
4108
4109 if n_tables > buf.len() {
4116 return Err(io::Error::new(
4117 io::ErrorKind::InvalidData,
4118 format!("catalog file corrupt: implausible table count {n_tables}"),
4119 ));
4120 }
4121
4122 let mut entries = Vec::with_capacity(n_tables);
4123 for _ in 0..n_tables {
4124 let name_len = read_u32(buf, &mut pos)? as usize;
4125 let table_name = read_string(buf, &mut pos, name_len)?;
4126 let n_cols = read_u16(buf, &mut pos)? as usize;
4127
4128 let mut columns = Vec::with_capacity(n_cols);
4129 for _ in 0..n_cols {
4130 let cname_len = read_u32(buf, &mut pos)? as usize;
4131 let name = read_string(buf, &mut pos, cname_len)?;
4132 let type_id_raw = read_u8(buf, &mut pos)?;
4133 let type_id = type_id_from_u8(type_id_raw)?;
4134 let required = read_u8(buf, &mut pos)? != 0;
4135 let position = read_u16(buf, &mut pos)?;
4136 columns.push(ColumnDef {
4137 name,
4138 type_id,
4139 required,
4140 position,
4141 });
4142 }
4143
4144 let indexed_cols: Vec<IndexedColMeta> = if version >= 3 {
4150 let n = read_u16(buf, &mut pos)? as usize;
4151 let mut v = Vec::with_capacity(n);
4152 for _ in 0..n {
4153 let l = read_u32(buf, &mut pos)? as usize;
4154 let name = read_string(buf, &mut pos, l)?;
4155 let unique = read_u8(buf, &mut pos)? != 0;
4156 v.push(IndexedColMeta { name, unique });
4157 }
4158 v
4159 } else if version >= 2 {
4160 let n = read_u16(buf, &mut pos)? as usize;
4161 let mut v = Vec::with_capacity(n);
4162 for _ in 0..n {
4163 let l = read_u32(buf, &mut pos)? as usize;
4164 let name = read_string(buf, &mut pos, l)?;
4165 v.push(IndexedColMeta {
4166 name,
4167 unique: false,
4168 });
4169 }
4170 v
4171 } else {
4172 Vec::new()
4173 };
4174
4175 let defaults = if version >= 4 {
4179 decode_defaults_section(buf, &mut pos, columns.len()).ok_or_else(|| {
4180 io::Error::new(io::ErrorKind::InvalidData, "truncated catalog defaults")
4181 })?
4182 } else {
4183 Vec::new()
4184 };
4185
4186 let auto_cols = if version >= 5 {
4190 decode_auto_section(buf, &mut pos, columns.len()).ok_or_else(|| {
4191 io::Error::new(io::ErrorKind::InvalidData, "truncated catalog auto columns")
4192 })?
4193 } else {
4194 Vec::new()
4195 };
4196
4197 let expression_indexes = if version >= 6 {
4201 decode_expression_indexes(buf, &mut pos)?
4202 } else {
4203 Vec::new()
4204 };
4205
4206 entries.push(CatalogEntry {
4207 schema: Schema {
4208 table_name,
4209 columns,
4210 },
4211 indexed_cols,
4212 expression_indexes,
4213 defaults,
4214 auto_cols,
4215 });
4216 }
4217
4218 let links = if version >= 7 {
4223 decode_links_section(buf, &mut pos)?
4224 } else {
4225 Vec::new()
4226 };
4227
4228 let mut seen_index_ids = FxHashMap::default();
4229 let mut max_index_id = 0;
4230 for entry in &entries {
4231 for index in &entry.expression_indexes {
4232 if index.canonical_version == 0 || index.canonical_text.is_empty() {
4233 return Err(io::Error::new(
4234 io::ErrorKind::InvalidData,
4235 "expression index has invalid canonical identity",
4236 ));
4237 }
4238 if index.canonical_version == 1
4239 && index.canonical_text != index.json_path.canonical_text()
4240 {
4241 return Err(io::Error::new(
4242 io::ErrorKind::InvalidData,
4243 "expression index canonical identity does not match its JSON path",
4244 ));
4245 }
4246 let Some(root) = entry
4247 .schema
4248 .columns
4249 .iter()
4250 .find(|column| column.name == index.json_path.column)
4251 else {
4252 return Err(io::Error::new(
4253 io::ErrorKind::InvalidData,
4254 "expression index JSON root is absent from its table",
4255 ));
4256 };
4257 if root.type_id != TypeId::Json {
4258 return Err(io::Error::new(
4259 io::ErrorKind::InvalidData,
4260 "expression index root column is not JSON",
4261 ));
4262 }
4263 if seen_index_ids.insert(index.index_id, ()).is_some() {
4264 return Err(io::Error::new(
4265 io::ErrorKind::InvalidData,
4266 "duplicate expression index id in catalog",
4267 ));
4268 }
4269 max_index_id = max_index_id.max(index.index_id);
4270 }
4271 }
4272 if next_index_id <= max_index_id {
4273 return Err(io::Error::new(
4274 io::ErrorKind::InvalidData,
4275 "catalog next index id does not exceed persisted index ids",
4276 ));
4277 }
4278 Ok(CatalogFile {
4279 version,
4280 next_index_id,
4281 entries,
4282 links,
4283 })
4284}
4285
4286fn read_u8(buf: &[u8], pos: &mut usize) -> io::Result<u8> {
4287 if *pos >= buf.len() {
4288 return Err(io::Error::new(
4289 io::ErrorKind::UnexpectedEof,
4290 "truncated catalog",
4291 ));
4292 }
4293 let v = buf[*pos];
4294 *pos += 1;
4295 Ok(v)
4296}
4297fn read_u16(buf: &[u8], pos: &mut usize) -> io::Result<u16> {
4298 if *pos + 2 > buf.len() {
4299 return Err(io::Error::new(
4300 io::ErrorKind::UnexpectedEof,
4301 "truncated catalog",
4302 ));
4303 }
4304 let v = u16::from_le_bytes(
4305 buf[*pos..*pos + 2]
4306 .try_into()
4307 .expect("bounds checked above"),
4308 );
4309 *pos += 2;
4310 Ok(v)
4311}
4312fn read_u32(buf: &[u8], pos: &mut usize) -> io::Result<u32> {
4313 if *pos + 4 > buf.len() {
4314 return Err(io::Error::new(
4315 io::ErrorKind::UnexpectedEof,
4316 "truncated catalog",
4317 ));
4318 }
4319 let v = u32::from_le_bytes(
4320 buf[*pos..*pos + 4]
4321 .try_into()
4322 .expect("bounds checked above"),
4323 );
4324 *pos += 4;
4325 Ok(v)
4326}
4327fn read_u64(buf: &[u8], pos: &mut usize) -> io::Result<u64> {
4328 if *pos + 8 > buf.len() {
4329 return Err(io::Error::new(
4330 io::ErrorKind::UnexpectedEof,
4331 "truncated catalog",
4332 ));
4333 }
4334 let value = u64::from_le_bytes(
4335 buf[*pos..*pos + 8]
4336 .try_into()
4337 .expect("bounds checked above"),
4338 );
4339 *pos += 8;
4340 Ok(value)
4341}
4342fn read_string(buf: &[u8], pos: &mut usize, len: usize) -> io::Result<String> {
4343 if *pos + len > buf.len() {
4344 return Err(io::Error::new(
4345 io::ErrorKind::UnexpectedEof,
4346 "truncated catalog string",
4347 ));
4348 }
4349 let s = std::str::from_utf8(&buf[*pos..*pos + len])
4350 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "non-utf8 in catalog"))?
4351 .to_string();
4352 *pos += len;
4353 Ok(s)
4354}
4355fn type_id_from_u8(v: u8) -> io::Result<TypeId> {
4356 match v {
4357 0 => Ok(TypeId::Empty),
4358 1 => Ok(TypeId::Int),
4359 2 => Ok(TypeId::Float),
4360 3 => Ok(TypeId::Bool),
4361 4 => Ok(TypeId::Str),
4362 5 => Ok(TypeId::DateTime),
4363 6 => Ok(TypeId::Uuid),
4364 7 => Ok(TypeId::Bytes),
4365 8 => Ok(TypeId::Json),
4366 _ => Err(io::Error::new(
4367 io::ErrorKind::InvalidData,
4368 format!("unknown type id: {v}"),
4369 )),
4370 }
4371}
4372
4373#[cfg(test)]
4374mod tests {
4375 use super::*;
4376
4377 fn fail_next_catalog_persist_at(stage: u8) {
4378 CATALOG_PERSIST_FAILPOINT.with(|failpoint| failpoint.set(stage));
4379 }
4380
4381 fn temp_catalog(name: &str) -> Catalog {
4382 let dir = std::env::temp_dir().join(format!("powdb_cat_{name}_{}", std::process::id()));
4383 Catalog::create(&dir).unwrap()
4384 }
4385
4386 fn hash_dir_tree(dir: &std::path::Path) -> String {
4391 let mut entries: Vec<std::path::PathBuf> = Vec::new();
4392 fn walk(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
4393 let mut items: Vec<_> = fs::read_dir(dir).unwrap().flatten().collect();
4394 items.sort_by_key(std::fs::DirEntry::path);
4395 for item in items {
4396 let path = item.path();
4397 if path.is_dir() {
4398 walk(&path, out);
4399 } else {
4400 out.push(path);
4401 }
4402 }
4403 }
4404 walk(dir, &mut entries);
4405 let mut hasher = crc32fast::Hasher::new();
4406 for path in &entries {
4407 hasher.update(path.to_string_lossy().as_bytes());
4408 hasher.update(&fs::read(path).unwrap());
4409 }
4410 format!("{:08x}", hasher.finalize())
4411 }
4412
4413 fn seed_quiescent_dir(dir: &std::path::Path) {
4414 let mut catalog = Catalog::create(dir).unwrap();
4415 catalog
4416 .create_table(Schema {
4417 table_name: "User".into(),
4418 columns: vec![
4419 ColumnDef {
4420 name: "name".into(),
4421 type_id: TypeId::Str,
4422 required: true,
4423 position: 0,
4424 },
4425 ColumnDef {
4426 name: "age".into(),
4427 type_id: TypeId::Int,
4428 required: false,
4429 position: 1,
4430 },
4431 ],
4432 })
4433 .unwrap();
4434 catalog.create_index("User", "age").unwrap();
4435 catalog
4436 .insert("User", &vec![Value::Str("Ada".into()), Value::Int(36)])
4437 .unwrap();
4438 catalog
4439 .insert("User", &vec![Value::Str("Bo".into()), Value::Int(20)])
4440 .unwrap();
4441 drop(catalog);
4444 }
4445
4446 #[test]
4447 fn open_read_only_serves_reads_on_clean_dir() {
4448 let dir = tempfile::tempdir().unwrap();
4449 seed_quiescent_dir(dir.path());
4450
4451 let catalog = Catalog::open_read_only(dir.path()).unwrap();
4452 let rows: Vec<_> = catalog.scan("User").unwrap().collect();
4453 assert_eq!(rows.len(), 2);
4454 let hit = catalog
4456 .index_lookup("User", "age", &Value::Int(36))
4457 .unwrap();
4458 assert_eq!(hit.unwrap()[0], Value::Str("Ada".into()));
4459 }
4460
4461 #[test]
4462 fn open_read_only_never_mutates_dir() {
4463 let dir = tempfile::tempdir().unwrap();
4464 seed_quiescent_dir(dir.path());
4465 let before = hash_dir_tree(dir.path());
4466
4467 {
4468 let catalog = Catalog::open_read_only(dir.path()).unwrap();
4469 let _ = catalog.scan("User").unwrap().count();
4470 let _ = catalog
4471 .index_lookup("User", "age", &Value::Int(20))
4472 .unwrap();
4473 }
4475 let after = hash_dir_tree(dir.path());
4476 assert_eq!(
4477 before, after,
4478 "read-only open + queries + drop must leave the directory byte-identical"
4479 );
4480 }
4481
4482 #[test]
4483 fn open_read_only_refuses_non_empty_wal() {
4484 let dir = tempfile::tempdir().unwrap();
4485 {
4488 let mut catalog = Catalog::create(dir.path()).unwrap();
4489 catalog
4490 .create_table(Schema {
4491 table_name: "T".into(),
4492 columns: vec![ColumnDef {
4493 name: "id".into(),
4494 type_id: TypeId::Int,
4495 required: true,
4496 position: 0,
4497 }],
4498 })
4499 .unwrap();
4500 catalog.insert("T", &vec![Value::Int(1)]).unwrap();
4501 catalog.sync_wal().unwrap();
4502 std::mem::forget(catalog); }
4504 let err = match Catalog::open_read_only(dir.path()) {
4505 Ok(_) => panic!("read-only open must refuse a non-empty WAL"),
4506 Err(err) => err,
4507 };
4508 assert!(
4509 err.to_string().contains("WAL is not empty"),
4510 "expected a WAL-not-empty refusal naming the remedy, got: {err}"
4511 );
4512 assert!(err.to_string().contains("read-write engine"));
4513 }
4514
4515 #[test]
4516 fn open_read_only_expression_index_reads_work() {
4517 let dir = tempfile::tempdir().unwrap();
4518 {
4519 let mut catalog = Catalog::create(dir.path()).unwrap();
4520 catalog
4521 .create_table(Schema {
4522 table_name: "Doc".into(),
4523 columns: vec![ColumnDef {
4524 name: "data".into(),
4525 type_id: TypeId::Json,
4526 required: false,
4527 position: 0,
4528 }],
4529 })
4530 .unwrap();
4531 let path =
4532 StoredJsonPathV1::new("data", vec![StoredJsonPathSegmentV1::Key("author".into())]);
4533 catalog
4534 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path, false)
4535 .unwrap();
4536 drop(catalog);
4537 }
4538 let before = hash_dir_tree(dir.path());
4540 let catalog = Catalog::open_read_only(dir.path()).unwrap();
4541 assert_eq!(catalog.scan("Doc").unwrap().count(), 0);
4542 drop(catalog);
4543 let after = hash_dir_tree(dir.path());
4544 assert_eq!(
4545 before, after,
4546 "read-only expression-index load must not write"
4547 );
4548 }
4549
4550 #[test]
4551 fn v5_reader_rejects_v6_catalog() {
4552 let dir = tempfile::tempdir().unwrap();
4553 let mut catalog = Catalog::create(dir.path()).unwrap();
4554 catalog
4555 .create_table(Schema {
4556 table_name: "Doc".into(),
4557 columns: vec![ColumnDef {
4558 name: "data".into(),
4559 type_id: TypeId::Json,
4560 required: false,
4561 position: 0,
4562 }],
4563 })
4564 .unwrap();
4565 let path =
4566 StoredJsonPathV1::new("data", vec![StoredJsonPathSegmentV1::Key("author".into())]);
4567 catalog
4568 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path, false)
4569 .unwrap();
4570 let result = read_catalog_file_with_max_version(
4571 &dir.path().join(CATALOG_FILE),
4572 LEGACY_CATALOG_VERSION,
4573 );
4574 let error = match result {
4575 Ok(_) => panic!("a v5 reader must reject v6 before decoding its payload"),
4576 Err(error) => error,
4577 };
4578 assert!(error.to_string().contains("unsupported catalog version: 6"));
4579 }
4580
4581 #[test]
4585 fn v6_reader_rejects_v7_catalog() {
4586 let dir = tempfile::tempdir().unwrap();
4587 let mut catalog = Catalog::create(dir.path()).unwrap();
4588 catalog
4589 .create_table(Schema {
4590 table_name: "Order".into(),
4591 columns: vec![ColumnDef {
4592 name: "user_id".into(),
4593 type_id: TypeId::Int,
4594 required: false,
4595 position: 0,
4596 }],
4597 })
4598 .unwrap();
4599 catalog
4600 .create_table(Schema {
4601 table_name: "User".into(),
4602 columns: vec![ColumnDef {
4603 name: "id".into(),
4604 type_id: TypeId::Int,
4605 required: true,
4606 position: 0,
4607 }],
4608 })
4609 .unwrap();
4610 catalog
4611 .create_link(LinkDef {
4612 owner_type: "Order".into(),
4613 name: "user".into(),
4614 target_type: "User".into(),
4615 local_key: "user_id".into(),
4616 target_key: "id".into(),
4617 kind: LinkKind::ToMany,
4618 })
4619 .unwrap();
4620 assert_eq!(catalog.active_catalog_version(), CATALOG_VERSION);
4621
4622 let result = read_catalog_file_with_max_version(
4623 &dir.path().join(CATALOG_FILE),
4624 EXPRESSION_INDEX_CATALOG_VERSION,
4625 );
4626 let error = match result {
4627 Ok(_) => panic!("a v6 reader must reject v7 before decoding its payload"),
4628 Err(error) => error,
4629 };
4630 assert!(error.to_string().contains("unsupported catalog version: 7"));
4631 }
4632
4633 #[test]
4634 fn expression_index_rolls_back_only_before_catalog_rename() {
4635 let before_dir = tempfile::tempdir().unwrap();
4636 let mut before = Catalog::create(before_dir.path()).unwrap();
4637 before
4638 .create_table(Schema {
4639 table_name: "Doc".into(),
4640 columns: vec![ColumnDef {
4641 name: "data".into(),
4642 type_id: TypeId::Json,
4643 required: false,
4644 position: 0,
4645 }],
4646 })
4647 .unwrap();
4648 let path =
4649 StoredJsonPathV1::new("data", vec![StoredJsonPathSegmentV1::Key("score".into())]);
4650
4651 fail_next_catalog_persist_at(1);
4652 let error = before
4653 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path.clone(), false)
4654 .unwrap_err();
4655 assert!(error.to_string().contains("before rename"));
4656 assert_eq!(before.active_catalog_version(), LEGACY_CATALOG_VERSION);
4657 assert_eq!(before.next_index_id(), 1);
4658 assert!(before.expression_index_metadata("Doc").unwrap().is_empty());
4659 assert!(!before_dir
4660 .path()
4661 .join(expression_index_file_name("Doc", 1))
4662 .exists());
4663
4664 let before_index_id = before
4665 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path.clone(), false)
4666 .unwrap();
4667 fail_next_catalog_persist_at(1);
4668 let error = before
4669 .drop_expression_index("Doc", before_index_id)
4670 .unwrap_err();
4671 assert!(error.to_string().contains("before rename"));
4672 assert!(before
4673 .expression_index_btree("Doc", before_index_id)
4674 .is_some());
4675 assert!(before_dir
4676 .path()
4677 .join(expression_index_file_name("Doc", 1))
4678 .exists());
4679 std::mem::forget(before);
4680 let before_reopened = Catalog::open(before_dir.path()).unwrap();
4681 assert!(before_reopened
4682 .expression_index_btree("Doc", before_index_id)
4683 .is_some());
4684
4685 let after_dir = tempfile::tempdir().unwrap();
4686 let mut after = Catalog::create(after_dir.path()).unwrap();
4687 after
4688 .create_table(Schema {
4689 table_name: "Doc".into(),
4690 columns: vec![ColumnDef {
4691 name: "data".into(),
4692 type_id: TypeId::Json,
4693 required: false,
4694 position: 0,
4695 }],
4696 })
4697 .unwrap();
4698 fail_next_catalog_persist_at(2);
4699 let index_id = after
4700 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path.clone(), false)
4701 .unwrap();
4702 assert_eq!(index_id, 1);
4703 assert_eq!(
4704 after.active_catalog_version(),
4705 EXPRESSION_INDEX_CATALOG_VERSION
4706 );
4707 assert_eq!(after.next_index_id(), 2);
4708 assert!(after.expression_index_btree("Doc", index_id).is_some());
4709 assert!(after_dir
4710 .path()
4711 .join(expression_index_file_name("Doc", 1))
4712 .exists());
4713 std::mem::forget(after);
4714
4715 let mut reopened = Catalog::open(after_dir.path()).unwrap();
4716 assert!(reopened.expression_index_btree("Doc", index_id).is_some());
4717 fail_next_catalog_persist_at(2);
4718 reopened.drop_expression_index("Doc", index_id).unwrap();
4719 assert!(reopened
4720 .expression_index_metadata("Doc")
4721 .unwrap()
4722 .is_empty());
4723 assert!(!after_dir
4724 .path()
4725 .join(expression_index_file_name("Doc", 1))
4726 .exists());
4727 std::mem::forget(reopened);
4728
4729 let final_open = Catalog::open(after_dir.path()).unwrap();
4730 assert!(final_open
4731 .expression_index_metadata("Doc")
4732 .unwrap()
4733 .is_empty());
4734 }
4735
4736 #[test]
4740 fn first_create_link_rolls_back_version_and_registry_before_rename() {
4741 let dir = tempfile::tempdir().unwrap();
4742 let mut catalog = Catalog::create(dir.path()).unwrap();
4743 catalog
4744 .create_table(Schema {
4745 table_name: "Order".into(),
4746 columns: vec![ColumnDef {
4747 name: "user_id".into(),
4748 type_id: TypeId::Int,
4749 required: false,
4750 position: 0,
4751 }],
4752 })
4753 .unwrap();
4754 catalog
4755 .create_table(Schema {
4756 table_name: "User".into(),
4757 columns: vec![ColumnDef {
4758 name: "id".into(),
4759 type_id: TypeId::Int,
4760 required: true,
4761 position: 0,
4762 }],
4763 })
4764 .unwrap();
4765 catalog.create_index_unique("User", "id", true).unwrap();
4766
4767 let version_before = catalog.active_catalog_version();
4768 assert_eq!(version_before, LEGACY_CATALOG_VERSION);
4769
4770 fail_next_catalog_persist_at(1);
4771 let error = catalog
4772 .create_link(LinkDef {
4773 owner_type: "Order".into(),
4774 name: "user".into(),
4775 target_type: "User".into(),
4776 local_key: "user_id".into(),
4777 target_key: "id".into(),
4778 kind: LinkKind::ToMany,
4779 })
4780 .unwrap_err();
4781 assert!(error.to_string().contains("before rename"));
4782 assert_eq!(catalog.active_catalog_version(), version_before);
4784 assert!(catalog.link("Order", "user").is_none());
4785 assert_eq!(catalog.links().count(), 0);
4786 assert_eq!(
4787 read_active_catalog_version(dir.path()).unwrap(),
4788 version_before
4789 );
4790
4791 catalog
4793 .create_link(LinkDef {
4794 owner_type: "Order".into(),
4795 name: "user".into(),
4796 target_type: "User".into(),
4797 local_key: "user_id".into(),
4798 target_key: "id".into(),
4799 kind: LinkKind::ToMany, })
4801 .unwrap();
4802 assert_eq!(catalog.active_catalog_version(), CATALOG_VERSION);
4803 assert_eq!(catalog.link("Order", "user").unwrap().kind, LinkKind::ToOne);
4804 }
4805
4806 #[test]
4807 fn ordinary_catalog_persist_reports_post_rename_directory_sync_failure() {
4808 let dir = tempfile::tempdir().unwrap();
4809 let mut catalog = Catalog::create(dir.path()).unwrap();
4810 fail_next_catalog_persist_at(2);
4811 let error = catalog
4812 .create_table(Schema {
4813 table_name: "VisibleAfterRename".into(),
4814 columns: vec![ColumnDef {
4815 name: "id".into(),
4816 type_id: TypeId::Int,
4817 required: true,
4818 position: 0,
4819 }],
4820 })
4821 .unwrap_err();
4822 assert!(error.to_string().contains("after rename"));
4823 assert!(catalog.schema("VisibleAfterRename").is_some());
4824
4825 std::mem::forget(catalog);
4826 let reopened = Catalog::open(dir.path()).unwrap();
4827 assert!(reopened.schema("VisibleAfterRename").is_some());
4828 }
4829
4830 fn schema_two_cols() -> Schema {
4831 Schema {
4832 table_name: "T".into(),
4833 columns: vec![
4834 ColumnDef {
4835 name: "id".into(),
4836 type_id: TypeId::Int,
4837 required: true,
4838 position: 0,
4839 },
4840 ColumnDef {
4841 name: "status".into(),
4842 type_id: TypeId::Str,
4843 required: false,
4844 position: 1,
4845 },
4846 ],
4847 }
4848 }
4849
4850 #[test]
4851 fn replay_records_treats_reused_tx_ids_as_ordered_spans() {
4852 let mut cat = temp_catalog("reused_tx_ids");
4853 let schema = schema_two_cols();
4854 cat.create_table(schema.clone()).unwrap();
4855 cat.checkpoint().unwrap();
4856
4857 let mut committed_row = Vec::new();
4858 encode_row_into(
4859 &schema,
4860 &[Value::Int(1), Value::Str("committed".into())],
4861 &mut committed_row,
4862 );
4863 let mut incomplete_row = Vec::new();
4864 encode_row_into(
4865 &schema,
4866 &[Value::Int(2), Value::Str("incomplete".into())],
4867 &mut incomplete_row,
4868 );
4869
4870 let records = vec![
4871 WalRecord {
4872 tx_id: 1,
4873 record_type: WalRecordType::Begin,
4874 lsn: 1,
4875 data: Vec::new(),
4876 },
4877 WalRecord {
4878 tx_id: 1,
4879 record_type: WalRecordType::Insert,
4880 lsn: 2,
4881 data: encode_wal_payload(
4882 "T",
4883 RowId {
4884 page_id: 1,
4885 slot_index: 0,
4886 },
4887 &committed_row,
4888 ),
4889 },
4890 WalRecord {
4891 tx_id: 1,
4892 record_type: WalRecordType::Commit,
4893 lsn: 3,
4894 data: Vec::new(),
4895 },
4896 WalRecord {
4897 tx_id: 1,
4898 record_type: WalRecordType::Begin,
4899 lsn: 4,
4900 data: Vec::new(),
4901 },
4902 WalRecord {
4903 tx_id: 1,
4904 record_type: WalRecordType::Insert,
4905 lsn: 5,
4906 data: encode_wal_payload(
4907 "T",
4908 RowId {
4909 page_id: 1,
4910 slot_index: 1,
4911 },
4912 &incomplete_row,
4913 ),
4914 },
4915 ];
4916
4917 cat.apply_wal_records(&records).unwrap();
4918 let rows: Vec<_> = cat.scan("T").unwrap().collect();
4919 assert_eq!(rows.len(), 1);
4920 assert_eq!(rows[0].1[0], Value::Int(1));
4921 assert_eq!(rows[0].1[1], Value::Str("committed".into()));
4922 }
4923
4924 #[test]
4925 fn ddl_create_table_codec_roundtrips_defaults_and_auto() {
4926 let schema = schema_two_cols();
4927 let defaults = vec![None, Some(Value::Str("active".into()))];
4928 let auto_cols = vec![true, false];
4929 let encoded = encode_ddl_create_table(&schema, &defaults, &auto_cols);
4930 let (decoded_schema, decoded_defaults, decoded_auto) =
4931 decode_ddl_create_table(&encoded).unwrap();
4932 assert_eq!(decoded_schema.columns.len(), 2);
4933 assert_eq!(decoded_defaults, defaults);
4934 assert_eq!(decoded_auto, auto_cols);
4935 }
4936
4937 #[test]
4938 fn ddl_create_table_codec_back_compat_without_trailing_sections() {
4939 let schema = schema_two_cols();
4943 let full = encode_ddl_create_table(&schema, &[], &[]);
4944 let legacy = &full[..full.len() - 4];
4947 let (decoded_schema, decoded_defaults, decoded_auto) =
4948 decode_ddl_create_table(legacy).unwrap();
4949 assert_eq!(decoded_schema.columns.len(), 2);
4950 assert!(decoded_defaults.is_empty(), "no defaults section -> empty");
4951 assert!(decoded_auto.is_empty(), "no auto section -> empty");
4952 }
4953
4954 #[test]
4955 fn ddl_create_table_codec_back_compat_defaults_but_no_auto() {
4956 let schema = schema_two_cols();
4959 let defaults = vec![None, Some(Value::Str("active".into()))];
4960 let full = encode_ddl_create_table(&schema, &defaults, &[]);
4961 let legacy = &full[..full.len() - 2];
4963 let (_schema, decoded_defaults, decoded_auto) = decode_ddl_create_table(legacy).unwrap();
4964 assert_eq!(decoded_defaults, defaults);
4965 assert!(decoded_auto.is_empty());
4966 }
4967
4968 #[test]
4969 fn read_catalog_file_accepts_intermediate_versions_3_and_4() {
4970 use std::io::Write as _;
4978 fn write_legacy_catalog(path: &std::path::Path, version: u16) {
4979 let mut buf: Vec<u8> = Vec::new();
4980 buf.extend_from_slice(CATALOG_MAGIC);
4981 buf.extend_from_slice(&version.to_le_bytes());
4982 buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes());
4985 buf.extend_from_slice(b"T");
4986 buf.extend_from_slice(&2u16.to_le_bytes()); buf.extend_from_slice(&2u32.to_le_bytes());
4989 buf.extend_from_slice(b"id");
4990 buf.push(TypeId::Int as u8);
4991 buf.push(1);
4992 buf.extend_from_slice(&0u16.to_le_bytes());
4993 buf.extend_from_slice(&6u32.to_le_bytes());
4995 buf.extend_from_slice(b"status");
4996 buf.push(TypeId::Str as u8);
4997 buf.push(0);
4998 buf.extend_from_slice(&1u16.to_le_bytes());
4999 buf.extend_from_slice(&0u16.to_le_bytes());
5001 if version >= 4 {
5003 encode_defaults_section(&mut buf, &[None, None]);
5004 }
5005 let crc = crc32fast::hash(&buf);
5007 buf.extend_from_slice(&crc.to_le_bytes());
5008 let mut f = fs::File::create(path).unwrap();
5009 f.write_all(&buf).unwrap();
5010 }
5011
5012 for version in [3u16, 4u16] {
5013 let path = std::env::temp_dir().join(format!(
5014 "powdb_cat_v{version}_compat_{}.bin",
5015 std::process::id()
5016 ));
5017 write_legacy_catalog(&path, version);
5018 let catalog_file = read_catalog_file(&path)
5019 .unwrap_or_else(|e| panic!("version {version} catalog must load, got: {e}"));
5020 let entries = catalog_file.entries;
5021 assert_eq!(entries.len(), 1);
5022 assert_eq!(entries[0].schema.table_name, "T");
5023 assert_eq!(entries[0].schema.columns.len(), 2);
5024 assert!(
5025 entries[0].auto_cols.is_empty(),
5026 "v{version} has no auto cols"
5027 );
5028 fs::remove_file(&path).ok();
5029 }
5030 }
5031
5032 #[test]
5033 fn read_catalog_file_rejects_implausible_table_count() {
5034 use std::io::Write as _;
5042 let mut buf: Vec<u8> = Vec::new();
5043 buf.extend_from_slice(CATALOG_MAGIC);
5044 buf.extend_from_slice(&CATALOG_VERSION.to_le_bytes());
5045 buf.extend_from_slice(&1000u32.to_le_bytes()); buf.extend_from_slice(&1u64.to_le_bytes()); let crc = crc32fast::hash(&buf);
5049 buf.extend_from_slice(&crc.to_le_bytes());
5050 let path =
5051 std::env::temp_dir().join(format!("powdb_cat_badcount_{}.bin", std::process::id()));
5052 fs::File::create(&path).unwrap().write_all(&buf).unwrap();
5053
5054 let msg = match read_catalog_file(&path) {
5055 Ok(_) => panic!("implausible table count must be rejected, got Ok"),
5056 Err(e) => e.to_string(),
5057 };
5058 assert!(
5059 msg.contains("implausible table count"),
5060 "expected an implausible-table-count error, got: {msg}"
5061 );
5062 fs::remove_file(&path).ok();
5063 }
5064
5065 #[test]
5066 fn data_dir_and_max_lsn_accessors() {
5067 let dir = std::env::temp_dir().join(format!("powdb_cat_maxlsn_{}", std::process::id()));
5068 let mut cat = Catalog::create(&dir).unwrap();
5069
5070 assert_eq!(cat.data_dir(), dir.as_path());
5072
5073 assert_eq!(cat.max_lsn(), 0);
5075
5076 let schema = Schema {
5077 table_name: "users".into(),
5078 columns: vec![ColumnDef {
5079 name: "name".into(),
5080 type_id: TypeId::Str,
5081 required: true,
5082 position: 0,
5083 }],
5084 };
5085 cat.create_table(schema).unwrap();
5086
5087 cat.insert("users", &vec![Value::Str("Alice".into())])
5088 .unwrap();
5089 cat.sync_wal().unwrap();
5090
5091 assert!(cat.max_lsn() > 0);
5094 }
5095
5096 #[test]
5097 fn test_create_table_and_insert() {
5098 let mut cat = temp_catalog("basic");
5099 let schema = Schema {
5100 table_name: "users".into(),
5101 columns: vec![
5102 ColumnDef {
5103 name: "name".into(),
5104 type_id: TypeId::Str,
5105 required: true,
5106 position: 0,
5107 },
5108 ColumnDef {
5109 name: "age".into(),
5110 type_id: TypeId::Int,
5111 required: false,
5112 position: 1,
5113 },
5114 ],
5115 };
5116 cat.create_table(schema).unwrap();
5117
5118 let row = vec![Value::Str("Alice".into()), Value::Int(30)];
5119 let rid = cat.insert("users", &row).unwrap();
5120
5121 let result = cat.get("users", rid).unwrap();
5122 assert_eq!(result[0], Value::Str("Alice".into()));
5123 assert_eq!(result[1], Value::Int(30));
5124 }
5125
5126 #[test]
5127 fn test_scan_table() {
5128 let mut cat = temp_catalog("scan");
5129 let schema = Schema {
5130 table_name: "items".into(),
5131 columns: vec![
5132 ColumnDef {
5133 name: "name".into(),
5134 type_id: TypeId::Str,
5135 required: true,
5136 position: 0,
5137 },
5138 ColumnDef {
5139 name: "price".into(),
5140 type_id: TypeId::Float,
5141 required: true,
5142 position: 1,
5143 },
5144 ],
5145 };
5146 cat.create_table(schema).unwrap();
5147
5148 for i in 0..50 {
5149 cat.insert(
5150 "items",
5151 &vec![
5152 Value::Str(format!("item_{i}")),
5153 Value::Float(i as f64 * 1.5),
5154 ],
5155 )
5156 .unwrap();
5157 }
5158
5159 let rows: Vec<_> = cat.scan("items").unwrap().collect();
5160 assert_eq!(rows.len(), 50);
5161 }
5162
5163 #[test]
5164 fn test_index_lookup() {
5165 let mut cat = temp_catalog("idx");
5166 let schema = Schema {
5167 table_name: "users".into(),
5168 columns: vec![
5169 ColumnDef {
5170 name: "email".into(),
5171 type_id: TypeId::Str,
5172 required: true,
5173 position: 0,
5174 },
5175 ColumnDef {
5176 name: "name".into(),
5177 type_id: TypeId::Str,
5178 required: true,
5179 position: 1,
5180 },
5181 ],
5182 };
5183 cat.create_table(schema).unwrap();
5184 cat.create_index("users", "email").unwrap();
5185
5186 cat.insert(
5187 "users",
5188 &vec![
5189 Value::Str("alice@example.com".into()),
5190 Value::Str("Alice".into()),
5191 ],
5192 )
5193 .unwrap();
5194 cat.insert(
5195 "users",
5196 &vec![
5197 Value::Str("bob@example.com".into()),
5198 Value::Str("Bob".into()),
5199 ],
5200 )
5201 .unwrap();
5202
5203 let result = cat
5204 .index_lookup("users", "email", &Value::Str("bob@example.com".into()))
5205 .unwrap();
5206 assert!(result.is_some());
5207 let row = result.unwrap();
5208 assert_eq!(row[1], Value::Str("Bob".into()));
5209 }
5210
5211 #[test]
5212 fn test_delete_row() {
5213 let mut cat = temp_catalog("delete");
5214 let schema = Schema {
5215 table_name: "t".into(),
5216 columns: vec![ColumnDef {
5217 name: "v".into(),
5218 type_id: TypeId::Int,
5219 required: true,
5220 position: 0,
5221 }],
5222 };
5223 cat.create_table(schema).unwrap();
5224 let r1 = cat.insert("t", &vec![Value::Int(1)]).unwrap();
5225 let r2 = cat.insert("t", &vec![Value::Int(2)]).unwrap();
5226 cat.delete("t", r1).unwrap();
5227 assert!(cat.get("t", r1).is_none());
5228 assert!(cat.get("t", r2).is_some());
5229 }
5230
5231 #[test]
5232 fn test_update_row() {
5233 let mut cat = temp_catalog("update");
5234 let schema = Schema {
5235 table_name: "t".into(),
5236 columns: vec![ColumnDef {
5237 name: "v".into(),
5238 type_id: TypeId::Int,
5239 required: true,
5240 position: 0,
5241 }],
5242 };
5243 cat.create_table(schema).unwrap();
5244 let rid = cat.insert("t", &vec![Value::Int(1)]).unwrap();
5245 let new_rid = cat.update("t", rid, &vec![Value::Int(99)]).unwrap();
5246 let row = cat.get("t", new_rid).unwrap();
5247 assert_eq!(row[0], Value::Int(99));
5248 }
5249
5250 #[test]
5251 fn test_persist_and_reopen() {
5252 let dir = std::env::temp_dir().join(format!("powdb_cat_persist_{}", std::process::id()));
5253 let _ = std::fs::remove_dir_all(&dir);
5255
5256 {
5257 let mut cat = Catalog::create(&dir).unwrap();
5258 cat.create_table(Schema {
5259 table_name: "users".into(),
5260 columns: vec![
5261 ColumnDef {
5262 name: "name".into(),
5263 type_id: TypeId::Str,
5264 required: true,
5265 position: 0,
5266 },
5267 ColumnDef {
5268 name: "age".into(),
5269 type_id: TypeId::Int,
5270 required: false,
5271 position: 1,
5272 },
5273 ],
5274 })
5275 .unwrap();
5276 cat.insert("users", &vec![Value::Str("Alice".into()), Value::Int(30)])
5277 .unwrap();
5278 cat.insert("users", &vec![Value::Str("Bob".into()), Value::Int(25)])
5279 .unwrap();
5280 }
5281
5282 let cat = Catalog::open(&dir).unwrap();
5284 let schema = cat.schema("users").unwrap();
5285 assert_eq!(schema.columns.len(), 2);
5286 assert_eq!(schema.columns[0].name, "name");
5287 assert_eq!(schema.columns[0].type_id, TypeId::Str);
5288 assert_eq!(schema.columns[1].type_id, TypeId::Int);
5289
5290 let rows: Vec<_> = cat.scan("users").unwrap().collect();
5291 assert_eq!(rows.len(), 2);
5292
5293 std::fs::remove_dir_all(&dir).ok();
5294 }
5295
5296 #[test]
5297 fn test_open_missing_dir_errors() {
5298 let dir = std::env::temp_dir().join(format!("powdb_cat_missing_{}", std::process::id()));
5299 let _ = std::fs::remove_dir_all(&dir);
5300 std::fs::create_dir_all(&dir).unwrap();
5301 assert!(Catalog::open(&dir).is_err());
5303 std::fs::remove_dir_all(&dir).ok();
5304 }
5305
5306 #[test]
5307 fn test_list_tables() {
5308 let mut cat = temp_catalog("list");
5309 cat.create_table(Schema {
5310 table_name: "a".into(),
5311 columns: vec![ColumnDef {
5312 name: "x".into(),
5313 type_id: TypeId::Int,
5314 required: true,
5315 position: 0,
5316 }],
5317 })
5318 .unwrap();
5319 cat.create_table(Schema {
5320 table_name: "b".into(),
5321 columns: vec![ColumnDef {
5322 name: "y".into(),
5323 type_id: TypeId::Int,
5324 required: true,
5325 position: 0,
5326 }],
5327 })
5328 .unwrap();
5329 let mut tables = cat.list_tables();
5330 tables.sort();
5331 assert_eq!(tables, vec!["a", "b"]);
5332 }
5333
5334 #[test]
5335 fn test_path_traversal_table_name_rejected() {
5336 let mut cat = temp_catalog("path_trav");
5337 let bad_names = vec![
5339 "../etc/passwd",
5340 "foo/bar",
5341 "table\0name",
5342 "",
5343 "123starts_with_digit",
5344 "has-dashes",
5345 "has spaces",
5346 "has.dots",
5347 ];
5348 for name in bad_names {
5349 let schema = Schema {
5350 table_name: name.into(),
5351 columns: vec![ColumnDef {
5352 name: "x".into(),
5353 type_id: TypeId::Int,
5354 required: true,
5355 position: 0,
5356 }],
5357 };
5358 let result = cat.create_table(schema);
5359 assert!(result.is_err(), "expected error for table name '{name}'");
5360 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
5361 }
5362 let good_names = vec!["users", "_private", "Table_123", "_"];
5364 for name in good_names {
5365 let schema = Schema {
5366 table_name: name.into(),
5367 columns: vec![ColumnDef {
5368 name: "x".into(),
5369 type_id: TypeId::Int,
5370 required: true,
5371 position: 0,
5372 }],
5373 };
5374 assert!(
5375 cat.create_table(schema).is_ok(),
5376 "expected ok for table name '{name}'"
5377 );
5378 }
5379 }
5380
5381 #[test]
5382 fn test_path_traversal_column_name_rejected() {
5383 let mut cat = temp_catalog("col_path_trav");
5384 let schema = Schema {
5385 table_name: "valid_table".into(),
5386 columns: vec![ColumnDef {
5387 name: "../bad".into(),
5388 type_id: TypeId::Int,
5389 required: true,
5390 position: 0,
5391 }],
5392 };
5393 let result = cat.create_table(schema);
5394 assert!(result.is_err());
5395 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
5396 }
5397
5398 #[test]
5399 fn test_drop_table_validates_name() {
5400 let mut cat = temp_catalog("drop_trav");
5401 let result = cat.drop_table("../etc/passwd");
5402 assert!(result.is_err());
5403 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
5405 }
5406
5407 fn ddl_guard_schema(name: &str) -> Schema {
5409 Schema {
5410 table_name: name.into(),
5411 columns: vec![
5412 ColumnDef {
5413 name: "id".into(),
5414 type_id: TypeId::Int,
5415 required: true,
5416 position: 0,
5417 },
5418 ColumnDef {
5419 name: "label".into(),
5420 type_id: TypeId::Str,
5421 required: false,
5422 position: 1,
5423 },
5424 ],
5425 }
5426 }
5427
5428 fn seed_ddl_guard_catalog(dir: &std::path::Path) -> Catalog {
5429 let mut cat = Catalog::create(dir).unwrap();
5430 cat.create_table(ddl_guard_schema("Keep")).unwrap();
5431 cat.create_index_unique("Keep", "id", true).unwrap();
5432 cat.insert("Keep", &vec![Value::Int(1), Value::Str("one".into())])
5433 .unwrap();
5434 cat.sync_wal().unwrap();
5435 cat
5436 }
5437
5438 fn assert_refused_in_transaction(result: io::Result<()>, verb: &str) {
5439 let err = result.unwrap_err();
5440 assert_eq!(
5441 err.kind(),
5442 io::ErrorKind::InvalidInput,
5443 "{verb} inside a transaction must be refused as InvalidInput"
5444 );
5445 let message = err.to_string();
5446 assert!(
5447 message.starts_with("cannot ") && message.contains("explicit transaction"),
5448 "{verb} refusal must name the active transaction, got: {message}"
5449 );
5450 }
5451
5452 #[test]
5453 fn drop_table_inside_transaction_is_refused_and_rollback_keeps_data() {
5454 let dir = tempfile::tempdir().unwrap();
5455 let mut cat = seed_ddl_guard_catalog(dir.path());
5456 cat.checkpoint().unwrap();
5460
5461 cat.begin_transaction().unwrap();
5462 assert_refused_in_transaction(cat.drop_table("Keep"), "drop table");
5463 cat.rollback_to_last_sync().unwrap();
5464
5465 let rows: Vec<_> = cat.scan("Keep").unwrap().collect();
5466 assert_eq!(rows.len(), 1, "rolled-back DROP must not destroy data");
5467 assert_eq!(rows[0].1[0], Value::Int(1));
5468
5469 drop(cat);
5471 let reopened = Catalog::open(dir.path()).unwrap();
5472 assert_eq!(reopened.scan("Keep").unwrap().count(), 1);
5473 }
5474
5475 #[test]
5476 fn every_ddl_verb_inside_transaction_is_refused() {
5477 let dir = tempfile::tempdir().unwrap();
5478 let mut cat = seed_ddl_guard_catalog(dir.path());
5479 cat.create_table(ddl_guard_schema("Other")).unwrap();
5480 cat.create_index_unique("Other", "id", true).unwrap();
5481 cat.checkpoint().unwrap();
5482
5483 cat.begin_transaction().unwrap();
5484
5485 assert_refused_in_transaction(cat.create_table(ddl_guard_schema("New")), "create table");
5486 assert_refused_in_transaction(cat.drop_table("Keep"), "drop table");
5487 assert_refused_in_transaction(
5488 cat.alter_table_add_column(
5489 "Keep",
5490 ColumnDef {
5491 name: "extra".into(),
5492 type_id: TypeId::Int,
5493 required: false,
5494 position: 2,
5495 },
5496 ),
5497 "alter table add column",
5498 );
5499 assert_refused_in_transaction(
5500 cat.alter_table_drop_column("Keep", "label"),
5501 "alter table drop column",
5502 );
5503 assert_refused_in_transaction(cat.create_index("Keep", "label"), "create index");
5504 assert_refused_in_transaction(
5505 cat.create_index_unique("Keep", "label", true),
5506 "create unique index",
5507 );
5508 assert_refused_in_transaction(
5509 cat.create_link(LinkDef {
5510 owner_type: "Other".into(),
5511 name: "keep".into(),
5512 target_type: "Keep".into(),
5513 local_key: "id".into(),
5514 target_key: "id".into(),
5515 kind: LinkKind::ToOne,
5516 }),
5517 "create link",
5518 );
5519 assert_refused_in_transaction(cat.drop_link("Other", "keep"), "drop link");
5520
5521 cat.rollback_to_last_sync().unwrap();
5522
5523 let mut tables = cat.list_tables();
5525 tables.sort_unstable();
5526 assert_eq!(tables, vec!["Keep", "Other"]);
5527 let schema = cat.schema("Keep").unwrap();
5528 assert_eq!(schema.columns.len(), 2);
5529 assert!(!cat.has_index("Keep", "label"));
5530 assert!(cat.links().next().is_none());
5531 assert_eq!(cat.scan("Keep").unwrap().count(), 1);
5532 }
5533
5534 #[test]
5535 fn ddl_still_works_after_commit_and_rollback() {
5536 let dir = tempfile::tempdir().unwrap();
5537 let mut cat = seed_ddl_guard_catalog(dir.path());
5538
5539 cat.begin_transaction().unwrap();
5540 assert!(cat.drop_table("Keep").is_err());
5541 cat.rollback_to_last_sync().unwrap();
5542 cat.create_table(ddl_guard_schema("AfterRollback")).unwrap();
5543
5544 cat.begin_transaction().unwrap();
5545 cat.insert("Keep", &vec![Value::Int(2), Value::Str("two".into())])
5546 .unwrap();
5547 cat.commit_transaction().unwrap();
5548 cat.drop_table("AfterRollback").unwrap();
5549
5550 let mut tables = cat.list_tables();
5551 tables.sort_unstable();
5552 assert_eq!(tables, vec!["Keep"]);
5553 assert_eq!(cat.scan("Keep").unwrap().count(), 2);
5554 }
5555
5556 #[test]
5557 fn transaction_over_dirty_page_budget_is_refused_and_catalog_stays_usable() {
5558 let dir = tempfile::tempdir().unwrap();
5559 let mut cat = Catalog::create(dir.path()).unwrap();
5560 cat.create_table(ddl_guard_schema("Big")).unwrap();
5561 cat.set_dirty_page_budget_bytes(8 * crate::page::PAGE_SIZE);
5564 assert_eq!(cat.dirty_page_budget_bytes(), 8 * crate::page::PAGE_SIZE);
5565
5566 cat.begin_transaction().unwrap();
5567 let mut refusal = None;
5568 for i in 0..100_000i64 {
5569 let row = vec![Value::Int(i), Value::Str(format!("row-{i:06}"))];
5570 if let Err(e) = cat.insert("Big", &row) {
5571 refusal = Some(e);
5572 break;
5573 }
5574 }
5575 let err = refusal.expect("an 8-page budget must refuse an unbounded transaction");
5576 let typed = err
5577 .get_ref()
5578 .and_then(|source| source.downcast_ref::<StorageError>());
5579 assert!(
5580 matches!(typed, Some(StorageError::TransactionTooLarge { .. })),
5581 "expected a typed TransactionTooLarge, got: {err}"
5582 );
5583 assert!(cat.dirty_pages_buffered() <= 8);
5584
5585 cat.rollback_to_last_sync().unwrap();
5587 assert_eq!(cat.dirty_page_budget_bytes(), 8 * crate::page::PAGE_SIZE);
5588 assert_eq!(cat.scan("Big").unwrap().count(), 0);
5589 cat.insert("Big", &vec![Value::Int(1), Value::Str("after".into())])
5590 .unwrap();
5591 assert_eq!(cat.scan("Big").unwrap().count(), 1);
5592 }
5593
5594 #[test]
5595 fn autocommit_writes_are_not_capped_by_the_dirty_page_budget() {
5596 let dir = tempfile::tempdir().unwrap();
5597 let mut cat = Catalog::create(dir.path()).unwrap();
5598 cat.create_table(ddl_guard_schema("Bulk")).unwrap();
5599 cat.set_dirty_page_budget_bytes(8 * crate::page::PAGE_SIZE);
5600
5601 for i in 0..5_000i64 {
5604 let row = vec![Value::Int(i), Value::Str(format!("row-{i:06}"))];
5605 cat.insert("Bulk", &row).unwrap();
5606 }
5607 assert!(cat.dirty_pages_buffered() <= 8);
5608 assert_eq!(cat.scan("Bulk").unwrap().count(), 5_000);
5609 }
5610}