1use crate::btree::BTree;
2use crate::error::StorageError;
3use crate::heap::HeapFile;
4use crate::page::{OVERFLOW_CHAIN_END, OVERFLOW_PAYLOAD_CAP};
5use crate::row::{encode_row_into, encode_row_v2_into, plan_spill, OverflowStub, MAX_VALUE_SIZE};
6use crate::stored_json_path::{StoredJsonPathSegmentV1, StoredJsonPathV1};
7use crate::table::Table;
8use crate::types::*;
9use crate::wal::{Wal, WalDurabilityTicket, WalRecord, WalRecordType, WalSyncMode};
10use rustc_hash::FxHashMap;
11use std::fs;
12use std::io::{self, Read, Write};
13use std::path::{Path, PathBuf};
14use std::sync::atomic::{AtomicU64, Ordering};
15use tracing::{info, warn};
16
17static NEXT_STRUCTURE_GENERATION: AtomicU64 = AtomicU64::new(1);
18
19fn next_structure_generation() -> u64 {
20 NEXT_STRUCTURE_GENERATION.fetch_add(1, Ordering::Relaxed)
21}
22
23fn check_encoded_row_size(encoded: &[u8]) -> io::Result<()> {
28 if encoded.len() > crate::page::MAX_ROW_DATA_SIZE {
29 return Err(crate::error::StorageError::RowTooLarge {
30 size: encoded.len(),
31 max: crate::page::MAX_ROW_DATA_SIZE,
32 }
33 .into());
34 }
35 Ok(())
36}
37
38fn validate_identifier(kind: &str, name: &str) -> io::Result<()> {
42 if name.is_empty() {
43 return Err(io::Error::new(
44 io::ErrorKind::InvalidInput,
45 format!("invalid {kind} name: must not be empty"),
46 ));
47 }
48 let mut chars = name.chars();
49 let first = chars.next().expect("non-empty name");
51 if !first.is_ascii_alphabetic() && first != '_' {
52 return Err(io::Error::new(
53 io::ErrorKind::InvalidInput,
54 format!("invalid {kind} name '{name}': must start with a letter or underscore"),
55 ));
56 }
57 for ch in chars {
58 if !ch.is_ascii_alphanumeric() && ch != '_' {
59 return Err(io::Error::new(
60 io::ErrorKind::InvalidInput,
61 format!(
62 "invalid {kind} name '{name}': must contain only letters, digits, and underscores"
63 ),
64 ));
65 }
66 }
67 Ok(())
68}
69
70fn validate_table_name(name: &str) -> io::Result<()> {
72 validate_identifier("table", name)
73}
74
75fn validate_column_name(name: &str) -> io::Result<()> {
77 validate_identifier("column", name)
78}
79
80const CATALOG_FILE: &str = "catalog.bin";
89pub const CATALOG_LSN_FILE: &str = "catalog.lsn";
90const CATALOG_MAGIC: &[u8; 4] = b"BCAT";
91pub const LEGACY_CATALOG_VERSION: u16 = 5;
95pub const CATALOG_VERSION: u16 = 6;
96
97#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ExpressionIndexMeta {
102 pub index_id: u64,
103 pub unique: bool,
104 pub canonical_version: u16,
105 pub canonical_text: String,
106 pub json_path: StoredJsonPathV1,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub enum IndexKeySource {
111 Column {
112 column: String,
113 },
114 Expression {
115 index_id: u64,
116 canonical_version: u16,
117 canonical_text: String,
118 json_path: StoredJsonPathV1,
119 },
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct IndexMetadata {
124 pub unique: bool,
125 pub source: IndexKeySource,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum IndexOrderDirection {
130 Asc,
131 Desc,
132}
133
134pub fn expression_index_file_name(table: &str, index_id: u64) -> String {
139 format!("{table}_{index_id}.eidx")
140}
141
142const WAL_FILE: &str = "wal.log";
145const SYNC_STATE_DIR: &str = ".powdb-sync";
146const SYNC_IDENTITY_FILE: &str = "identity.json";
147
148const WAL_BATCH_SIZE: usize = 64;
152type WalArchiveCallback<'a> = &'a mut dyn FnMut(&Path, &[WalRecord]) -> io::Result<()>;
153
154fn read_durable_lsn(data_dir: &Path) -> io::Result<u64> {
155 let path = data_dir.join(CATALOG_LSN_FILE);
156 let bytes = match fs::read(path) {
157 Ok(bytes) => bytes,
158 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(0),
159 Err(err) => return Err(err),
160 };
161 if bytes.len() != 8 {
162 return Err(io::Error::new(
163 io::ErrorKind::InvalidData,
164 "catalog LSN sidecar has invalid length",
165 ));
166 }
167 let mut buf = [0u8; 8];
168 buf.copy_from_slice(&bytes);
169 Ok(u64::from_le_bytes(buf))
170}
171
172fn write_durable_lsn(data_dir: &Path, lsn: u64) -> io::Result<()> {
173 let path = data_dir.join(CATALOG_LSN_FILE);
174 let tmp_path = data_dir.join(format!("{CATALOG_LSN_FILE}.tmp"));
175 let mut file = fs::File::create(&tmp_path)?;
176 file.write_all(&lsn.to_le_bytes())?;
177 file.sync_all()?;
178 drop(file);
179 fs::rename(&tmp_path, &path)?;
180 sync_directory(data_dir)?;
181 Ok(())
182}
183
184#[cfg(unix)]
185fn sync_directory(path: &Path) -> io::Result<()> {
186 fs::File::open(path)?.sync_all()
187}
188
189#[cfg(not(unix))]
190fn sync_directory(path: &Path) -> io::Result<()> {
191 let _ = path;
192 Ok(())
193}
194
195#[cfg(test)]
196thread_local! {
197 static CATALOG_PERSIST_FAILPOINT: std::cell::Cell<u8> = const { std::cell::Cell::new(0) };
198}
199
200#[cfg(test)]
201fn take_catalog_persist_failpoint(stage: u8) -> bool {
202 CATALOG_PERSIST_FAILPOINT.with(|failpoint| {
203 if failpoint.get() == stage {
204 failpoint.set(0);
205 true
206 } else {
207 false
208 }
209 })
210}
211
212enum CatalogPersistError {
213 BeforeActivation(io::Error),
214 AfterActivation(io::Error),
215}
216
217impl CatalogPersistError {
218 fn into_io_error(self) -> io::Error {
219 match self {
220 Self::BeforeActivation(error) | Self::AfterActivation(error) => error,
221 }
222 }
223}
224
225fn max_record_lsn(records: &[WalRecord]) -> Option<u64> {
226 records.iter().map(|record| record.lsn).max()
227}
228
229pub struct Catalog {
241 tables: Vec<Table>,
243 name_to_slot: FxHashMap<String, usize>,
246 data_dir: PathBuf,
247 wal: Wal,
252 next_tx_id: u64,
256 active_tx_id: Option<u64>,
259 tx_start_len: Option<u64>,
262 pending_autocommit_tx_ids: Vec<u64>,
265 checkpointed: bool,
269 durable_lsn: u64,
272 pending_free_overflow: Vec<(usize, Vec<u32>)>,
281 active_catalog_version: u16,
284 next_index_id: u64,
286 structure_generation: u64,
291}
292
293impl Catalog {
294 pub fn create(data_dir: &Path) -> io::Result<Self> {
315 crate::create_data_dir_secure(data_dir)?;
316 let wal_path = data_dir.join(WAL_FILE);
317 let wal = Wal::create(&wal_path, WAL_BATCH_SIZE)?;
318 let cat = Catalog {
319 tables: Vec::new(),
320 name_to_slot: FxHashMap::default(),
321 data_dir: data_dir.to_path_buf(),
322 wal,
323 next_tx_id: 1,
324 active_tx_id: None,
325 tx_start_len: None,
326 pending_autocommit_tx_ids: Vec::new(),
327 pending_free_overflow: Vec::new(),
328 checkpointed: false,
329 durable_lsn: 0,
330 active_catalog_version: LEGACY_CATALOG_VERSION,
331 next_index_id: 1,
332 structure_generation: next_structure_generation(),
333 };
334 cat.persist()?;
335 Ok(cat)
336 }
337
338 pub fn open(data_dir: &Path) -> io::Result<Self> {
347 Self::open_inner(data_dir, None)
348 }
349
350 pub fn open_with_wal_archive<F>(data_dir: &Path, mut archive: F) -> io::Result<Self>
359 where
360 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
361 {
362 let archive: WalArchiveCallback<'_> = &mut archive;
363 Self::open_inner(data_dir, Some(archive))
364 }
365
366 fn open_inner(data_dir: &Path, archive: Option<WalArchiveCallback<'_>>) -> io::Result<Self> {
367 let cat_path = data_dir.join(CATALOG_FILE);
368 if !cat_path.exists() {
369 return Err(io::Error::new(io::ErrorKind::NotFound, "no catalog file"));
370 }
371 let catalog_file = read_catalog_file(&cat_path)?;
372 let active_catalog_version = catalog_file.version;
373 let next_index_id = catalog_file.next_index_id;
374 let entries = catalog_file.entries;
375 let durable_lsn = read_durable_lsn(data_dir)?;
376 let mut tables: Vec<Table> = Vec::with_capacity(entries.len());
377 let mut name_to_slot =
378 FxHashMap::with_capacity_and_hasher(entries.len(), Default::default());
379 for CatalogEntry {
380 schema,
381 indexed_cols,
382 expression_indexes: expression_metas,
383 defaults,
384 auto_cols,
385 } in entries
386 {
387 let name = schema.table_name.clone();
388 let mut table =
394 Table::open_with_indexes(schema, data_dir, &indexed_cols, &expression_metas)?;
395 table.set_defaults(defaults);
396 table.set_auto_cols(auto_cols);
397 name_to_slot.insert(name.clone(), tables.len());
398 tables.push(table);
399 }
400 let wal_path = data_dir.join(WAL_FILE);
401 let wal = Wal::open(&wal_path, WAL_BATCH_SIZE)?;
402 let mut cat = Catalog {
403 tables,
404 name_to_slot,
405 data_dir: data_dir.to_path_buf(),
406 wal,
407 next_tx_id: 1,
408 active_tx_id: None,
409 tx_start_len: None,
410 pending_autocommit_tx_ids: Vec::new(),
411 pending_free_overflow: Vec::new(),
412 checkpointed: false,
413 durable_lsn,
414 active_catalog_version,
415 next_index_id,
416 structure_generation: next_structure_generation(),
417 };
418 cat.replay_wal(archive)?;
419 let max_page_lsn = cat
429 .tables
430 .iter()
431 .map(|t| t.heap.max_page_lsn())
432 .max()
433 .unwrap_or(0);
434 let max_known_lsn = max_page_lsn.max(cat.durable_lsn);
435 cat.wal.set_next_lsn_at_least(max_known_lsn + 1);
436 if let Err(e) = cat.sweep_all() {
441 warn!(error = %e, "post-recovery overflow sweep failed (non-fatal)");
442 }
443 Ok(cat)
444 }
445
446 fn replay_wal(&mut self, mut archive: Option<WalArchiveCallback<'_>>) -> io::Result<()> {
472 let records = self.wal.read_all()?;
473 if records.is_empty() {
474 return Ok(());
475 }
476 if archive.is_none() {
477 self.ensure_plain_wal_truncate_allowed(&records)?;
478 }
479 self.replay_records(&records)?;
480 if let Some(archive) = archive.as_mut() {
481 archive(&self.data_dir, &records)?;
482 }
483 self.wal.truncate()?;
484 Ok(())
485 }
486
487 pub fn apply_wal_records(&mut self, records: &[WalRecord]) -> io::Result<()> {
496 self.ensure_no_active_transaction_for_checkpoint()?;
497 self.ensure_no_pending_wal_records()?;
498 self.replay_records(records)
499 }
500
501 pub fn ensure_no_pending_wal_records(&self) -> io::Result<()> {
504 if self.wal.has_pending() || !self.wal.read_all()?.is_empty() {
505 return Err(io::Error::other(
506 "cannot apply replicated WAL records while local WAL records are pending",
507 ));
508 }
509 Ok(())
510 }
511
512 fn replay_records(&mut self, records: &[WalRecord]) -> io::Result<()> {
513 if records.is_empty() {
514 return Ok(());
515 }
516
517 info!(count = records.len(), "applying WAL records");
518
519 let has_boundaries = records.iter().any(|rec| {
528 matches!(
529 rec.record_type,
530 WalRecordType::Begin | WalRecordType::Commit | WalRecordType::Rollback
531 )
532 });
533 let mut committed_row_records = vec![true; records.len()];
534 if has_boundaries {
535 committed_row_records.fill(false);
536 let mut pending_tx_spans: Vec<(u64, Vec<usize>)> = Vec::new();
537 for (index, rec) in records.iter().enumerate() {
538 match rec.record_type {
539 WalRecordType::Insert
540 | WalRecordType::Update
541 | WalRecordType::Delete
542 | WalRecordType::OverflowWrite
543 | WalRecordType::OverflowFree
544 if rec.tx_id == 0 =>
545 {
546 committed_row_records[index] = true;
547 }
548 WalRecordType::Insert
549 | WalRecordType::Update
550 | WalRecordType::Delete
551 | WalRecordType::OverflowWrite
552 | WalRecordType::OverflowFree => {
553 if let Some((_, rows)) = pending_tx_spans
554 .iter_mut()
555 .rev()
556 .find(|(tx_id, _)| *tx_id == rec.tx_id)
557 {
558 rows.push(index);
559 } else {
560 pending_tx_spans.push((rec.tx_id, vec![index]));
561 }
562 }
563 WalRecordType::Begin if rec.tx_id != 0 => {
564 pending_tx_spans.push((rec.tx_id, Vec::new()));
565 }
566 WalRecordType::Commit if rec.tx_id != 0 => {
567 if let Some(span_index) = pending_tx_spans
568 .iter()
569 .rposition(|(tx_id, _)| *tx_id == rec.tx_id)
570 {
571 let (_, rows) = pending_tx_spans.remove(span_index);
572 for row_index in rows {
573 committed_row_records[row_index] = true;
574 }
575 }
576 }
577 WalRecordType::Rollback if rec.tx_id != 0 => {
578 if let Some(span_index) = pending_tx_spans
579 .iter()
580 .rposition(|(tx_id, _)| *tx_id == rec.tx_id)
581 {
582 pending_tx_spans.remove(span_index);
583 }
584 }
585 _ => {}
586 }
587 }
588 }
589
590 let mut replayed_inserts = 0usize;
591 let mut replayed_updates = 0usize;
592 let mut replayed_deletes = 0usize;
593 let mut skipped = 0usize;
594 let mut skipped_uncommitted = 0usize;
595 let mut saw_ddl = false;
596 for (index, rec) in records.iter().enumerate() {
597 if has_boundaries
598 && !committed_row_records[index]
599 && matches!(
600 rec.record_type,
601 WalRecordType::Insert
602 | WalRecordType::Update
603 | WalRecordType::Delete
604 | WalRecordType::OverflowWrite
605 | WalRecordType::OverflowFree
606 )
607 {
608 skipped_uncommitted += 1;
609 continue;
610 }
611 match rec.record_type {
612 WalRecordType::Insert => {
613 if let Some((table_name, rid, row_bytes)) = decode_wal_payload(&rec.data) {
614 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
615 let tbl = &mut self.tables[slot];
616 if rec.lsn > 0 && tbl.heap.page_lsn(rid.page_id) >= rec.lsn {
620 skipped += 1;
621 continue;
622 }
623 tbl.heap.insert_at(rid, &row_bytes)?;
630 tbl.heap.set_page_lsn(rid.page_id, rec.lsn)?;
631 replayed_inserts += 1;
632 }
633 }
634 }
635 WalRecordType::Update => {
636 if let Some((table_name, rid, row_bytes)) = decode_wal_payload(&rec.data) {
637 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
638 let tbl = &mut self.tables[slot];
639 if rec.lsn > 0 && tbl.heap.page_lsn(rid.page_id) >= rec.lsn {
640 skipped += 1;
641 continue;
642 }
643 let new_rid = tbl.heap.update(rid, &row_bytes)?;
644 tbl.heap.set_page_lsn(new_rid.page_id, rec.lsn)?;
645 replayed_updates += 1;
646 }
647 }
648 }
649 WalRecordType::Delete => {
650 if let Some((table_name, rid, _)) = decode_wal_payload(&rec.data) {
651 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
652 let tbl = &mut self.tables[slot];
653 if rec.lsn > 0 && tbl.heap.page_lsn(rid.page_id) >= rec.lsn {
654 skipped += 1;
655 continue;
656 }
657 let _ = tbl.heap.delete(rid);
658 tbl.heap.set_page_lsn(rid.page_id, rec.lsn)?;
659 replayed_deletes += 1;
660 }
661 }
662 }
663 WalRecordType::OverflowWrite => {
664 if let Some((table_name, page_id, next_page, chunk)) =
669 decode_overflow_write_payload(&rec.data)
670 {
671 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
672 let tbl = &mut self.tables[slot];
673 if rec.lsn > 0 && tbl.heap.overflow_page_lsn(page_id) >= rec.lsn {
674 skipped += 1;
675 continue;
676 }
677 tbl.heap
678 .write_overflow_page(page_id, next_page, &chunk, rec.lsn)?;
679 }
680 }
681 }
682 WalRecordType::OverflowFree => {
683 if let Some((table_name, pages)) = decode_overflow_free_payload(&rec.data) {
688 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
689 self.tables[slot].heap.release_overflow_pages(&pages);
690 }
691 }
692 }
693 WalRecordType::Begin | WalRecordType::Commit | WalRecordType::Rollback => {
694 }
696 WalRecordType::DdlCreateTable => {
697 saw_ddl = true;
698 if let Some((schema, defaults, auto_cols)) = decode_ddl_create_table(&rec.data)
699 {
700 if !self.name_to_slot.contains_key(&schema.table_name) {
701 if let Ok(mut table) = Table::create(schema, &self.data_dir) {
702 table.set_defaults(defaults);
703 table.set_auto_cols(auto_cols);
704 let slot = self.tables.len();
705 let name = table.schema.table_name.clone();
706 self.tables.push(table);
707 self.name_to_slot.insert(name, slot);
708 }
709 }
710 }
711 }
712 WalRecordType::DdlDropTable => {
713 saw_ddl = true;
714 if let Some((table_name, _)) = decode_ddl_table_name(&rec.data) {
715 if let Some(&slot) = self.name_to_slot.get(&table_name) {
716 let heap_path = self.data_dir.join(format!("{table_name}.heap"));
717 if heap_path.exists() {
718 let _ = fs::remove_file(&heap_path);
719 }
720 for col_name in self.tables[slot].indexed_column_names() {
721 let idx_path =
722 self.data_dir.join(format!("{table_name}_{col_name}.idx"));
723 if idx_path.exists() {
724 let _ = fs::remove_file(&idx_path);
725 }
726 }
727 for index_id in self.tables[slot].expression_index_ids() {
728 let idx_path = self
729 .data_dir
730 .join(expression_index_file_name(&table_name, index_id));
731 let _ = fs::remove_file(idx_path);
732 }
733 self.name_to_slot.remove(&table_name);
734 let last = self.tables.len() - 1;
735 if slot != last {
736 let moved_name = self.tables[last].schema.table_name.clone();
737 self.tables.swap(slot, last);
738 self.name_to_slot.insert(moved_name, slot);
739 }
740 self.tables.pop();
741 }
742 }
743 }
744 WalRecordType::DdlAddColumn => {
745 saw_ddl = true;
746 if let Some((table_name, col)) = decode_ddl_alter_add_column(&rec.data) {
747 if let Some(&slot) = self.name_to_slot.get(&table_name) {
748 let tbl = &mut self.tables[slot];
749 if !tbl.schema.columns.iter().any(|c| c.name == col.name) {
750 let old_schema = tbl.schema.clone();
751 let has_rows = tbl.heap.scan().next().is_some();
752 tbl.schema.columns.push(col);
753 tbl.refresh_layout();
754 if has_rows {
755 let fill = vec![Value::Empty; tbl.schema.columns.len()];
756 let data_dir = self.data_dir.clone();
757 let _ = tbl.rewrite_rows_for_schema_change(
758 &old_schema,
759 &fill,
760 &data_dir,
761 );
762 }
763 }
764 if rec.lsn > 0 {
771 let _ = tbl.heap.stamp_all_pages_min_lsn(rec.lsn);
772 }
773 }
774 }
775 }
776 WalRecordType::DdlDropColumn => {
777 saw_ddl = true;
778 if let Some((table_name, col_name)) = decode_ddl_alter_drop_column(&rec.data) {
779 if let Some(&slot) = self.name_to_slot.get(&table_name) {
780 {
781 let tbl = &mut self.tables[slot];
782 if let Some(idx) =
783 tbl.schema.columns.iter().position(|c| c.name == col_name)
784 {
785 let old_schema = tbl.schema.clone();
786 let has_rows = tbl.heap.scan().next().is_some();
787 tbl.schema.columns.remove(idx);
788 for (i, c) in tbl.schema.columns.iter_mut().enumerate() {
789 c.position = i as u16;
790 }
791 tbl.refresh_layout();
792 if has_rows {
793 let fill = vec![Value::Empty; tbl.schema.columns.len()];
794 let data_dir = self.data_dir.clone();
795 let _ = tbl.rewrite_rows_for_schema_change(
796 &old_schema,
797 &fill,
798 &data_dir,
799 );
800 }
801 }
802 if rec.lsn > 0 {
803 let _ = tbl.heap.stamp_all_pages_min_lsn(rec.lsn);
804 }
805 }
806
807 let removed_ids =
808 self.tables[slot].remove_expression_indexes_for_root(&col_name);
809 for index_id in removed_ids {
810 let idx_path = self
811 .data_dir
812 .join(expression_index_file_name(&table_name, index_id));
813 let _ = fs::remove_file(idx_path);
814 }
815 }
816 }
817 }
818 }
819 }
820 info!(
821 inserts = replayed_inserts,
822 updates = replayed_updates,
823 deletes = replayed_deletes,
824 skipped = skipped,
825 skipped_uncommitted = skipped_uncommitted,
826 "WAL record apply complete (commit-boundary + LSN idempotent)"
827 );
828 if saw_ddl {
829 self.persist()?;
830 }
831 for tbl in &mut self.tables {
845 tbl.heap.flush_all_dirty()?;
846 tbl.heap.flush()?;
847 tbl.rebuild_indexes_from_heap()?;
848 tbl.save_dirty_indexes()?;
853 }
854 if let Some(max_lsn) = max_record_lsn(records) {
855 self.record_durable_lsn_at_least(max_lsn)?;
856 self.wal.set_next_lsn_at_least(max_lsn.saturating_add(1));
857 }
858 Ok(())
859 }
860
861 pub fn checkpoint(&mut self) -> io::Result<()> {
870 self.ensure_no_active_transaction_for_checkpoint()?;
871 self.ensure_plain_checkpoint_allowed_before_flush()?;
872 self.flush_checkpoint_state()?;
873 self.wal.flush()?;
874 self.record_durable_lsn_at_least(self.wal.last_appended_lsn())?;
875 self.wal.truncate()?;
876 self.checkpointed = true;
877 Ok(())
878 }
879
880 pub fn checkpoint_with_wal_archive<F>(&mut self, mut archive: F) -> io::Result<()>
888 where
889 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
890 {
891 self.ensure_no_active_transaction_for_checkpoint()?;
892 self.commit_autocommit()?;
893 self.flush_checkpoint_state()?;
894 self.wal.flush()?;
895 let records = self.wal.read_all()?;
896 let archive: WalArchiveCallback<'_> = &mut archive;
897 archive(&self.data_dir, &records)?;
898 if let Some(max_lsn) = max_record_lsn(&records) {
899 self.record_durable_lsn_at_least(max_lsn)?;
900 } else {
901 self.record_durable_lsn_at_least(self.wal.last_appended_lsn())?;
902 }
903 self.wal.truncate()?;
904 self.checkpointed = true;
905 Ok(())
906 }
907
908 fn ensure_no_active_transaction_for_checkpoint(&self) -> io::Result<()> {
909 if self.active_tx_id.is_some() {
910 return Err(io::Error::other(
911 "cannot checkpoint while an explicit transaction is active",
912 ));
913 }
914 Ok(())
915 }
916
917 fn flush_checkpoint_state(&mut self) -> io::Result<()> {
918 for tbl in &mut self.tables {
919 tbl.heap.flush_all_dirty()?;
920 tbl.heap.flush()?;
921 tbl.save_dirty_indexes()?;
926 }
927 Ok(())
928 }
929
930 fn ensure_plain_checkpoint_allowed_before_flush(&self) -> io::Result<()> {
931 if !self.sync_identity_file_exists() {
932 return Ok(());
933 }
934 if self.wal.has_pending() {
935 return Err(io::Error::other(
936 "sync identity exists but checkpoint/recovery was called without a WAL archive hook; refusing to truncate retained history",
937 ));
938 }
939 let records = self.wal.read_all()?;
940 self.ensure_plain_wal_truncate_allowed(&records)
941 }
942
943 fn ensure_plain_wal_truncate_allowed(&self, records: &[WalRecord]) -> io::Result<()> {
944 if records.is_empty() {
945 return Ok(());
946 }
947 if self.sync_identity_file_exists() {
948 return Err(io::Error::other(
949 "sync identity exists but checkpoint/recovery was called without a WAL archive hook; refusing to truncate retained history",
950 ));
951 }
952 Ok(())
953 }
954
955 fn sync_identity_file_exists(&self) -> bool {
956 self.data_dir
957 .join(SYNC_STATE_DIR)
958 .join(SYNC_IDENTITY_FILE)
959 .exists()
960 }
961
962 fn record_durable_lsn_at_least(&mut self, lsn: u64) -> io::Result<()> {
963 if lsn <= self.durable_lsn {
964 return Ok(());
965 }
966 self.durable_lsn = lsn;
967 write_durable_lsn(&self.data_dir, lsn)
968 }
969
970 #[inline]
972 fn free_overflow_chain(&mut self, slot: usize, pages: Vec<u32>) {
983 if pages.is_empty() {
984 return;
985 }
986 if self.active_tx_id.is_some() {
987 self.pending_free_overflow.push((slot, pages));
988 } else {
989 self.tables[slot].release_overflow_pages(&pages);
990 }
991 }
992
993 fn next_tx(&mut self) -> u64 {
994 if let Some(id) = self.active_tx_id {
995 return id;
996 }
997 let id = self.next_tx_id;
998 self.next_tx_id = self.next_tx_id.wrapping_add(1);
999 id
1000 }
1001
1002 pub fn begin_transaction(&mut self) -> io::Result<()> {
1004 if self.active_tx_id.is_some() {
1005 return Err(io::Error::new(
1006 io::ErrorKind::InvalidInput,
1007 "explicit transaction is already active",
1008 ));
1009 }
1010 let start_len = self.wal.synced_len()?;
1011 let id = self.next_tx_id;
1012 self.next_tx_id = self.next_tx_id.wrapping_add(1);
1013 self.active_tx_id = Some(id);
1014 self.tx_start_len = Some(start_len);
1015 self.pending_autocommit_tx_ids.clear();
1016 if !self.wal.is_off() {
1017 self.wal.append(id, WalRecordType::Begin, &[])?;
1018 self.wal.flush()?;
1019 }
1020 Ok(())
1021 }
1022
1023 pub fn commit_transaction(&mut self) -> io::Result<()> {
1026 if let Some(id) = self.active_tx_id.take() {
1027 if !self.wal.is_off() {
1028 self.wal.append(id, WalRecordType::Commit, &[])?;
1029 self.wal.flush()?;
1030 }
1031 }
1032 self.tx_start_len = None;
1033 for (slot, pages) in std::mem::take(&mut self.pending_free_overflow) {
1037 self.tables[slot].release_overflow_pages(&pages);
1038 }
1039 Ok(())
1040 }
1041
1042 pub fn commit_autocommit(&mut self) -> io::Result<()> {
1046 if !self.wal.is_off() && !self.pending_autocommit_tx_ids.is_empty() {
1047 self.pending_autocommit_tx_ids.sort_unstable();
1048 self.pending_autocommit_tx_ids.dedup();
1049 for id in self.pending_autocommit_tx_ids.drain(..) {
1050 self.wal.append(id, WalRecordType::Commit, &[])?;
1051 }
1052 }
1053 self.wal.flush()
1054 }
1055
1056 fn wal_log(
1071 &mut self,
1072 tx_id: u64,
1073 record_type: WalRecordType,
1074 table: &str,
1075 rid: RowId,
1076 row_bytes: &[u8],
1077 ) -> io::Result<()> {
1078 if self.wal.is_off() {
1085 return Ok(());
1086 }
1087 let payload = encode_wal_payload(table, rid, row_bytes);
1088 self.wal.append(tx_id, record_type, &payload)?;
1089 if self.active_tx_id.is_none() {
1090 self.pending_autocommit_tx_ids.push(tx_id);
1091 }
1092 Ok(())
1093 }
1094
1095 #[inline]
1101 pub fn sync_wal(&mut self) -> io::Result<()> {
1102 self.wal.flush()
1103 }
1104
1105 pub fn set_wal_sync_mode(&mut self, mode: WalSyncMode) {
1113 self.wal.set_sync_mode(mode);
1114 }
1115
1116 pub fn set_wal_sync_deferred(&mut self, defer: bool) {
1124 self.wal.set_defer_sync(defer);
1125 }
1126
1127 pub fn take_wal_durability_ticket(&mut self) -> Option<WalDurabilityTicket> {
1130 self.wal.take_durability_ticket()
1131 }
1132
1133 pub fn wal_fsync_count(&self) -> u64 {
1135 self.wal.fsync_count()
1136 }
1137
1138 pub fn rollback_to_last_sync(&mut self) -> io::Result<()> {
1153 self.rollback_to_last_sync_inner(None)
1154 }
1155
1156 pub fn rollback_to_last_sync_with_wal_archive<F>(&mut self, mut archive: F) -> io::Result<()>
1161 where
1162 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
1163 {
1164 let archive: WalArchiveCallback<'_> = &mut archive;
1165 self.rollback_to_last_sync_inner(Some(archive))
1166 }
1167
1168 fn rollback_to_last_sync_inner(
1169 &mut self,
1170 mut archive: Option<WalArchiveCallback<'_>>,
1171 ) -> io::Result<()> {
1172 let start_len = self.tx_start_len.unwrap_or(0);
1173 let prearchived = if let Some(archive) = archive.as_mut() {
1174 let records = self.wal.read_through_len(start_len)?;
1175 if !records.is_empty() {
1176 archive(&self.data_dir, &records)?;
1177 }
1178 true
1179 } else {
1180 false
1181 };
1182
1183 let start_len = self.tx_start_len.take().unwrap_or(0);
1184 if let Some(id) = self.active_tx_id.take() {
1185 if !self.wal.is_off() {
1186 let _ = self.wal.append(id, WalRecordType::Rollback, &[]);
1187 }
1188 }
1189 self.wal.discard_and_truncate_to(start_len)?;
1190
1191 for tbl in &mut self.tables {
1201 tbl.heap.discard_dirty();
1202 tbl.discard_dirty_indexes();
1203 }
1204 self.wal.discard_pending()?;
1210 let data_dir = self.data_dir.clone();
1214 let sync_mode = self.wal.sync_mode();
1215 let mut restored = if prearchived {
1216 let mut already_archived = |_dir: &Path, _records: &[WalRecord]| Ok(());
1217 let archive: WalArchiveCallback<'_> = &mut already_archived;
1218 Self::open_inner(&data_dir, Some(archive))?
1219 } else {
1220 Self::open_inner(&data_dir, archive)?
1221 };
1222 if self.has_same_prepared_structure(&restored) {
1228 restored.structure_generation = self.structure_generation;
1229 }
1230 *self = restored;
1231 self.wal.set_sync_mode(sync_mode);
1232 Ok(())
1233 }
1234
1235 fn abandon_active_transaction_for_drop(&mut self) -> io::Result<()> {
1236 for tbl in &mut self.tables {
1237 tbl.heap.discard_dirty();
1238 }
1239 self.pending_autocommit_tx_ids.clear();
1240 let truncate_result = match self.tx_start_len.take() {
1241 Some(start_len) => self.wal.discard_and_truncate_to(start_len),
1242 None => self.wal.discard_pending(),
1243 };
1244 self.active_tx_id = None;
1245 truncate_result
1246 }
1247
1248 pub fn data_dir(&self) -> &Path {
1250 &self.data_dir
1251 }
1252
1253 pub fn max_lsn(&self) -> u64 {
1258 let max_page_lsn = self
1259 .tables
1260 .iter()
1261 .map(|t| t.heap.max_page_lsn())
1262 .max()
1263 .unwrap_or(0);
1264 max_page_lsn
1265 .max(self.durable_lsn)
1266 .max(self.wal.last_appended_lsn())
1267 }
1268
1269 pub fn create_table(&mut self, schema: Schema) -> io::Result<()> {
1270 self.create_table_full(schema, Vec::new(), Vec::new())
1271 }
1272
1273 pub fn create_table_with_defaults(
1277 &mut self,
1278 schema: Schema,
1279 defaults: Vec<Option<Value>>,
1280 ) -> io::Result<()> {
1281 self.create_table_full(schema, defaults, Vec::new())
1282 }
1283
1284 pub fn create_table_full(
1289 &mut self,
1290 schema: Schema,
1291 defaults: Vec<Option<Value>>,
1292 auto_cols: Vec<bool>,
1293 ) -> io::Result<()> {
1294 self.invalidate_structure();
1295 validate_table_name(&schema.table_name)?;
1296 for col in &schema.columns {
1297 validate_column_name(&col.name)?;
1298 }
1299 let name = schema.table_name.clone();
1300 if self.name_to_slot.contains_key(&name) {
1301 return Err(io::Error::new(
1302 io::ErrorKind::AlreadyExists,
1303 format!("table '{name}' already exists"),
1304 ));
1305 }
1306 if !self.wal.is_off() {
1307 let payload = encode_ddl_create_table(&schema, &defaults, &auto_cols);
1308 self.wal
1309 .append(0, WalRecordType::DdlCreateTable, &payload)?;
1310 self.wal.flush()?;
1311 }
1312 let mut table = Table::create(schema, &self.data_dir)?;
1313 table.set_defaults(defaults);
1314 table.set_auto_cols(auto_cols);
1315 let slot = self.tables.len();
1316 self.tables.push(table);
1317 self.name_to_slot.insert(name, slot);
1318 self.persist()?;
1319 Ok(())
1320 }
1321
1322 pub fn column_defaults(&self, table: &str) -> Option<&[Option<Value>]> {
1326 let slot = *self.name_to_slot.get(table)?;
1327 Some(self.tables[slot].defaults())
1328 }
1329
1330 pub fn auto_columns(&self, table: &str) -> Option<&[bool]> {
1333 let slot = *self.name_to_slot.get(table)?;
1334 Some(self.tables[slot].auto_cols())
1335 }
1336
1337 pub fn assign_auto_columns(&mut self, table: &str, values: &mut [Value]) {
1341 if let Some(&slot) = self.name_to_slot.get(table) {
1342 self.tables[slot].assign_auto(values);
1343 }
1344 }
1345
1346 fn persist_at_activation_boundary(&self) -> Result<(), CatalogPersistError> {
1351 let cat_path = self.data_dir.join(CATALOG_FILE);
1352 let tmp_path = self.data_dir.join(format!("{CATALOG_FILE}.tmp"));
1353 let entries: Vec<CatalogEntryRef<'_>> = self
1354 .tables
1355 .iter()
1356 .map(|t| CatalogEntryRef {
1357 schema: &t.schema,
1358 indexed_cols: t.indexed_column_metas(),
1359 expression_indexes: t.expression_index_metas(),
1360 defaults: t.defaults(),
1361 auto_cols: t.auto_cols(),
1362 })
1363 .collect();
1364 write_catalog_file(
1365 &tmp_path,
1366 self.active_catalog_version,
1367 self.next_index_id,
1368 &entries,
1369 )
1370 .map_err(CatalogPersistError::BeforeActivation)?;
1371 #[cfg(test)]
1372 if take_catalog_persist_failpoint(1) {
1373 return Err(CatalogPersistError::BeforeActivation(io::Error::other(
1374 "injected catalog failure before rename",
1375 )));
1376 }
1377 fs::rename(&tmp_path, &cat_path).map_err(CatalogPersistError::BeforeActivation)?;
1378 #[cfg(test)]
1379 let directory_sync = if take_catalog_persist_failpoint(2) {
1380 Err(io::Error::other(
1381 "injected catalog directory sync failure after rename",
1382 ))
1383 } else {
1384 sync_directory(&self.data_dir)
1385 };
1386 #[cfg(not(test))]
1387 let directory_sync = sync_directory(&self.data_dir);
1388 directory_sync.map_err(CatalogPersistError::AfterActivation)
1389 }
1390
1391 fn persist(&self) -> io::Result<()> {
1392 self.persist_at_activation_boundary()
1393 .map_err(CatalogPersistError::into_io_error)
1394 }
1395
1396 #[inline]
1400 pub fn table_slot(&self, name: &str) -> Option<usize> {
1401 self.name_to_slot.get(name).copied()
1402 }
1403
1404 #[inline]
1408 pub fn structure_generation(&self) -> u64 {
1409 self.structure_generation
1410 }
1411
1412 #[inline]
1413 fn invalidate_structure(&mut self) {
1414 self.structure_generation = next_structure_generation();
1415 }
1416
1417 fn has_same_prepared_structure(&self, other: &Self) -> bool {
1418 self.tables.len() == other.tables.len()
1419 && self.tables.iter().zip(&other.tables).all(|(left, right)| {
1420 let left_schema = &left.schema;
1421 let right_schema = &right.schema;
1422 left_schema.table_name == right_schema.table_name
1423 && left_schema.columns.len() == right_schema.columns.len()
1424 && left_schema.columns.iter().zip(&right_schema.columns).all(
1425 |(left_col, right_col)| {
1426 left_col.name == right_col.name
1427 && left_col.type_id == right_col.type_id
1428 && left_col.required == right_col.required
1429 && left_col.position == right_col.position
1430 },
1431 )
1432 && left.defaults() == right.defaults()
1433 && left.auto_cols() == right.auto_cols()
1434 && {
1435 let left_indexes = left.indexed_column_metas();
1436 let right_indexes = right.indexed_column_metas();
1437 left_indexes.len() == right_indexes.len()
1438 && left_indexes.iter().zip(&right_indexes).all(
1439 |(left_index, right_index)| {
1440 left_index.name == right_index.name
1441 && left_index.unique == right_index.unique
1442 },
1443 )
1444 }
1445 && left.expression_index_metas() == right.expression_index_metas()
1446 })
1447 }
1448
1449 #[inline]
1452 pub fn table_by_slot(&self, slot: usize) -> &Table {
1453 &self.tables[slot]
1454 }
1455
1456 #[inline]
1458 pub fn table_by_slot_mut(&mut self, slot: usize) -> &mut Table {
1459 &mut self.tables[slot]
1460 }
1461
1462 pub fn get_table(&self, name: &str) -> Option<&Table> {
1463 let slot = *self.name_to_slot.get(name)?;
1464 Some(&self.tables[slot])
1465 }
1466
1467 pub fn get_table_mut(&mut self, name: &str) -> Option<&mut Table> {
1468 let slot = *self.name_to_slot.get(name)?;
1469 Some(&mut self.tables[slot])
1470 }
1471
1472 #[inline]
1476 pub fn table_has_overflow(&self, table: &str) -> bool {
1477 self.get_table(table)
1478 .map(|t| t.has_overflow_rows())
1479 .unwrap_or(false)
1480 }
1481
1482 #[inline]
1487 fn by_name(&self, table: &str) -> io::Result<&Table> {
1488 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1489 io::Error::new(
1490 io::ErrorKind::NotFound,
1491 format!("table '{table}' not found"),
1492 )
1493 })?;
1494 Ok(&self.tables[slot])
1495 }
1496
1497 #[inline]
1499 fn by_name_mut(&mut self, table: &str) -> io::Result<&mut Table> {
1500 let slot = self.slot_of(table)?;
1501 Ok(&mut self.tables[slot])
1502 }
1503
1504 pub fn sweep(&mut self, table: &str) -> io::Result<usize> {
1509 let slot = self.slot_of(table)?;
1510 let reclaimed = self.tables[slot].sweep_overflow()?;
1511 if !reclaimed.is_empty() && !self.wal.is_off() {
1512 let payload = encode_overflow_free_payload(table, &reclaimed);
1513 self.wal.append(0, WalRecordType::OverflowFree, &payload)?;
1514 self.wal.flush()?;
1515 }
1516 Ok(reclaimed.len())
1517 }
1518
1519 pub fn sweep_all(&mut self) -> io::Result<usize> {
1521 let names: Vec<String> = self
1522 .tables
1523 .iter()
1524 .map(|t| t.schema.table_name.clone())
1525 .collect();
1526 let mut total = 0;
1527 for name in names {
1528 total += self.sweep(&name)?;
1529 }
1530 Ok(total)
1531 }
1532
1533 fn slot_of(&self, table: &str) -> io::Result<usize> {
1534 self.name_to_slot.get(table).copied().ok_or_else(|| {
1535 io::Error::new(
1536 io::ErrorKind::NotFound,
1537 format!("table '{table}' not found"),
1538 )
1539 })
1540 }
1541
1542 pub fn insert(&mut self, table: &str, values: &Row) -> io::Result<RowId> {
1543 if self.wal.is_off() {
1552 return self.by_name_mut(table)?.insert(values);
1553 }
1554 let slot = self.slot_of(table)?;
1555 let _ = self.tables[slot].preflight_insert(values)?;
1556 let tx_id = self.next_tx();
1560 let row_bytes = {
1561 let Catalog { tables, wal, .. } = self;
1562 encode_row_with_spill_logged(&mut tables[slot], wal, tx_id, values)?
1563 };
1564 let new_rid = self.tables[slot].insert_encoded(values, &row_bytes)?;
1569 self.wal_log(tx_id, WalRecordType::Insert, table, new_rid, &row_bytes)?;
1570 let lsn = self.wal.last_appended_lsn();
1571 if lsn > 0 {
1572 self.tables[slot].heap.set_page_lsn(new_rid.page_id, lsn)?;
1573 }
1574 Ok(new_rid)
1575 }
1576
1577 pub fn insert_by_slot(&mut self, slot: usize, values: &Row) -> io::Result<RowId> {
1585 if self.wal.is_off() {
1586 return self.tables[slot].insert(values);
1587 }
1588 let _ = self.tables[slot].preflight_insert(values)?;
1589 let tx_id = self.next_tx();
1590 let autocommit = self.active_tx_id.is_none();
1591 let Catalog { tables, wal, .. } = self;
1592 let tbl = &mut tables[slot];
1593 let row_bytes = encode_row_with_spill_logged(tbl, wal, tx_id, values)?;
1596 let new_rid = tbl.insert_encoded(values, &row_bytes)?;
1599 let payload = encode_wal_payload(&tbl.schema.table_name, new_rid, &row_bytes);
1600 wal.append(tx_id, WalRecordType::Insert, &payload)?;
1601 if autocommit {
1602 self.pending_autocommit_tx_ids.push(tx_id);
1603 }
1604 let lsn = wal.last_appended_lsn();
1605 if lsn > 0 {
1606 tbl.heap.set_page_lsn(new_rid.page_id, lsn)?;
1607 }
1608 Ok(new_rid)
1609 }
1610
1611 pub fn get(&self, table: &str, rid: RowId) -> Option<Row> {
1612 self.get_table(table)?.get(rid)
1613 }
1614
1615 pub fn get_projected(
1616 &self,
1617 table: &str,
1618 rid: RowId,
1619 column_indices: &[usize],
1620 ) -> io::Result<Option<Vec<Value>>> {
1621 self.by_name(table)?.get_projected(rid, column_indices)
1622 }
1623
1624 pub fn delete(&mut self, table: &str, rid: RowId) -> io::Result<()> {
1625 let slot = self.slot_of(table)?;
1626 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
1630 if self.wal.is_off() {
1633 self.tables[slot].delete(rid)?;
1634 self.free_overflow_chain(slot, old_pages);
1635 return Ok(());
1636 }
1637 let tx_id = self.next_tx();
1638 self.wal_log(tx_id, WalRecordType::Delete, table, rid, &[])?;
1640 self.tables[slot].delete(rid)?;
1641 self.free_overflow_chain(slot, old_pages);
1642 Ok(())
1643 }
1644
1645 pub fn delete_many(&mut self, table: &str, rids: &[RowId]) -> io::Result<u64> {
1649 let slot = self.slot_of(table)?;
1657 let old_pages = self.collect_overflow_pages(slot, rids)?;
1660 if self.wal.is_off() {
1661 let count = self.tables[slot].delete_many(rids)?;
1662 self.free_overflow_chain(slot, old_pages);
1663 return Ok(count);
1664 }
1665 let tx_id = self.next_tx();
1666 for &rid in rids {
1667 let payload = encode_wal_payload(table, rid, &[]);
1668 self.wal.append(tx_id, WalRecordType::Delete, &payload)?;
1669 }
1670 if self.active_tx_id.is_none() && !rids.is_empty() {
1671 self.pending_autocommit_tx_ids.push(tx_id);
1672 }
1673 let count = self.tables[slot].delete_many(rids)?;
1674 self.free_overflow_chain(slot, old_pages);
1675 Ok(count)
1676 }
1677
1678 fn collect_overflow_pages(&self, slot: usize, rids: &[RowId]) -> io::Result<Vec<u32>> {
1681 if !self.tables[slot].has_overflow_rows() {
1682 return Ok(Vec::new());
1683 }
1684 let mut pages = Vec::new();
1685 for &rid in rids {
1686 pages.extend(self.tables[slot].overflow_chain_pages_at(rid)?);
1687 }
1688 Ok(pages)
1689 }
1690
1691 pub fn scan_delete_matching<P>(&mut self, table: &str, pred: P) -> io::Result<u64>
1701 where
1702 P: FnMut(&[u8]) -> bool,
1703 {
1704 self.by_name_mut(table)?.scan_delete_matching(pred)
1705 }
1706
1707 pub fn scan_delete_matching_logged<P>(&mut self, table: &str, pred: P) -> io::Result<u64>
1718 where
1719 P: FnMut(&[u8]) -> bool,
1720 {
1721 if self.wal.is_off() {
1727 return self.by_name_mut(table)?.scan_delete_matching(pred);
1728 }
1729 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1733 io::Error::new(
1734 io::ErrorKind::NotFound,
1735 format!("table '{table}' not found"),
1736 )
1737 })?;
1738 let tx_id = self.next_tx();
1739 let autocommit = self.active_tx_id.is_none();
1740 let Catalog { tables, wal, .. } = self;
1743 let tbl = &mut tables[slot];
1744 let name_bytes = table.as_bytes();
1748 let count = tbl.scan_delete_matching_with_hook(pred, |rid, row_bytes| {
1749 let mut payload: Vec<u8> =
1750 Vec::with_capacity(4 + name_bytes.len() + 10 + row_bytes.len());
1751 payload.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
1752 payload.extend_from_slice(name_bytes);
1753 payload.extend_from_slice(&rid.page_id.to_le_bytes());
1754 payload.extend_from_slice(&rid.slot_index.to_le_bytes());
1755 payload.extend_from_slice(&0u32.to_le_bytes());
1759 let _ = wal.append(tx_id, WalRecordType::Delete, &payload);
1766 })?;
1767 if autocommit && count > 0 {
1768 self.pending_autocommit_tx_ids.push(tx_id);
1769 }
1770 Ok(count)
1772 }
1773
1774 pub fn scan_patch_matching_logged<P, M>(
1782 &mut self,
1783 table: &str,
1784 pred: P,
1785 try_mutate: M,
1786 ) -> io::Result<(u64, Vec<RowId>)>
1787 where
1788 P: FnMut(&[u8]) -> bool,
1789 M: FnMut(&mut [u8]) -> Option<u16>,
1790 {
1791 if self.wal.is_off() {
1792 return self.by_name_mut(table)?.scan_patch_matching_with_hook(
1793 pred,
1794 try_mutate,
1795 |_, _| {},
1796 );
1797 }
1798 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1799 io::Error::new(
1800 io::ErrorKind::NotFound,
1801 format!("table '{table}' not found"),
1802 )
1803 })?;
1804 let tx_id = self.next_tx();
1805 let autocommit = self.active_tx_id.is_none();
1806 let Catalog { tables, wal, .. } = self;
1807 let tbl = &mut tables[slot];
1808 let name_bytes = table.as_bytes();
1809 let result = tbl.scan_patch_matching_with_hook(pred, try_mutate, |rid, row_bytes| {
1810 let mut payload: Vec<u8> =
1811 Vec::with_capacity(4 + name_bytes.len() + 10 + row_bytes.len());
1812 payload.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
1813 payload.extend_from_slice(name_bytes);
1814 payload.extend_from_slice(&rid.page_id.to_le_bytes());
1815 payload.extend_from_slice(&rid.slot_index.to_le_bytes());
1816 payload.extend_from_slice(&(row_bytes.len() as u32).to_le_bytes());
1817 payload.extend_from_slice(row_bytes);
1818 let _ = wal.append(tx_id, WalRecordType::Update, &payload);
1819 })?;
1820 if autocommit && result.0 > 0 {
1821 self.pending_autocommit_tx_ids.push(tx_id);
1822 }
1823 Ok(result)
1824 }
1825
1826 pub fn update(&mut self, table: &str, rid: RowId, values: &Row) -> io::Result<RowId> {
1827 if self.wal.is_off() {
1830 let slot = self.slot_of(table)?;
1831 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
1832 let new_rid = self.tables[slot].update(rid, values)?;
1833 self.free_overflow_chain(slot, old_pages);
1834 return Ok(new_rid);
1835 }
1836 let slot = self.slot_of(table)?;
1837 self.tables[slot].preflight_update(rid, values)?;
1838 let tx_id = self.next_tx();
1839 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
1843 let row_bytes = {
1848 let Catalog { tables, wal, .. } = self;
1849 encode_row_with_spill_logged(&mut tables[slot], wal, tx_id, values)?
1850 };
1851 check_encoded_row_size(&row_bytes)?;
1855 self.wal_log(tx_id, WalRecordType::Update, table, rid, &row_bytes)?;
1856 let new_rid = self.tables[slot].update_encoded(rid, values, &row_bytes, None)?;
1857 self.free_overflow_chain(slot, old_pages);
1858 Ok(new_rid)
1859 }
1860
1861 pub fn update_hinted(
1865 &mut self,
1866 table: &str,
1867 rid: RowId,
1868 values: &Row,
1869 changed_col_indices: Option<&[usize]>,
1870 ) -> io::Result<RowId> {
1871 if self.wal.is_off() {
1875 let slot = self.slot_of(table)?;
1876 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
1877 let new_rid = self.tables[slot].update_hinted(rid, values, changed_col_indices)?;
1878 self.free_overflow_chain(slot, old_pages);
1879 return Ok(new_rid);
1880 }
1881 let slot = self.slot_of(table)?;
1882 self.tables[slot].preflight_update(rid, values)?;
1883 let tx_id = self.next_tx();
1884 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
1885 let row_bytes = {
1886 let Catalog { tables, wal, .. } = self;
1887 encode_row_with_spill_logged(&mut tables[slot], wal, tx_id, values)?
1888 };
1889 check_encoded_row_size(&row_bytes)?;
1891 self.wal_log(tx_id, WalRecordType::Update, table, rid, &row_bytes)?;
1892 let new_rid =
1893 self.tables[slot].update_encoded(rid, values, &row_bytes, changed_col_indices)?;
1894 self.free_overflow_chain(slot, old_pages);
1895 Ok(new_rid)
1896 }
1897
1898 #[inline]
1910 pub fn with_row_bytes_mut<F>(&mut self, table: &str, rid: RowId, f: F) -> io::Result<bool>
1911 where
1912 F: FnOnce(&mut [u8]),
1913 {
1914 self.by_name_mut(table)?.with_row_bytes_mut(rid, f)
1915 }
1916
1917 #[inline]
1931 pub fn update_row_bytes_logged<F>(&mut self, table: &str, rid: RowId, f: F) -> io::Result<bool>
1932 where
1933 F: FnOnce(&mut [u8]),
1934 {
1935 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1936 io::Error::new(
1937 io::ErrorKind::NotFound,
1938 format!("table '{table}' not found"),
1939 )
1940 })?;
1941 self.update_row_bytes_logged_by_slot(slot, rid, f)
1942 }
1943
1944 #[inline]
1949 pub fn update_row_bytes_logged_by_slot<F>(
1950 &mut self,
1951 slot: usize,
1952 rid: RowId,
1953 f: F,
1954 ) -> io::Result<bool>
1955 where
1956 F: FnOnce(&mut [u8]),
1957 {
1958 let tbl = &mut self.tables[slot];
1961 let ok = tbl.with_row_bytes_mut(rid, f)?;
1962 if !ok {
1963 return Ok(false);
1964 }
1965 if self.wal.is_off() {
1969 return Ok(true);
1970 }
1971 let new_bytes = match tbl.heap.get(rid) {
1974 Some(b) => b,
1975 None => return Ok(false),
1977 };
1978 let table_name = tbl.schema.table_name.clone();
1981 let tx_id = self.next_tx();
1982 self.wal_log(tx_id, WalRecordType::Update, &table_name, rid, &new_bytes)?;
1983 Ok(true)
1984 }
1985
1986 #[inline]
1998 pub fn patch_var_col_in_place(
1999 &mut self,
2000 table: &str,
2001 rid: RowId,
2002 col_idx: usize,
2003 new_value: Option<&[u8]>,
2004 ) -> io::Result<bool> {
2005 self.by_name_mut(table)?
2006 .patch_var_col_in_place(rid, col_idx, new_value)
2007 }
2008
2009 pub fn patch_var_col_logged(
2015 &mut self,
2016 table: &str,
2017 rid: RowId,
2018 col_idx: usize,
2019 new_value: Option<&[u8]>,
2020 ) -> io::Result<bool> {
2021 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
2022 io::Error::new(
2023 io::ErrorKind::NotFound,
2024 format!("table '{table}' not found"),
2025 )
2026 })?;
2027 let tbl = &mut self.tables[slot];
2028 let ok = tbl.patch_var_col_in_place(rid, col_idx, new_value)?;
2029 if !ok {
2030 return Ok(false);
2031 }
2032 if self.wal.is_off() {
2035 return Ok(true);
2036 }
2037 let new_bytes = match tbl.heap.get(rid) {
2038 Some(b) => b,
2039 None => return Ok(false),
2040 };
2041 let table_name = tbl.schema.table_name.clone();
2042 let tx_id = self.next_tx();
2043 self.wal_log(tx_id, WalRecordType::Update, &table_name, rid, &new_bytes)?;
2044 Ok(true)
2045 }
2046
2047 pub fn scan(&self, table: &str) -> io::Result<impl Iterator<Item = (RowId, Row)> + '_> {
2048 Ok(self.by_name(table)?.scan())
2049 }
2050
2051 pub fn for_each_row_raw<F>(&self, table: &str, f: F) -> io::Result<()>
2054 where
2055 F: FnMut(RowId, &[u8]),
2056 {
2057 self.by_name(table)?.for_each_row_raw(f);
2058 Ok(())
2059 }
2060
2061 pub fn try_for_each_row_raw<F>(&self, table: &str, f: F) -> io::Result<()>
2066 where
2067 F: FnMut(RowId, &[u8]) -> std::ops::ControlFlow<()>,
2068 {
2069 self.by_name(table)?.try_for_each_row_raw(f);
2070 Ok(())
2071 }
2072
2073 pub fn create_index(&mut self, table: &str, column: &str) -> io::Result<()> {
2074 self.create_index_unique(table, column, false)
2075 }
2076
2077 pub fn create_index_unique(
2082 &mut self,
2083 table: &str,
2084 column: &str,
2085 unique: bool,
2086 ) -> io::Result<()> {
2087 self.invalidate_structure();
2088 let data_dir = self.data_dir.clone();
2089 self.by_name_mut(table)?
2090 .create_index_with_unique(column, &data_dir, unique)?;
2091 self.persist()
2095 }
2096
2097 pub fn active_catalog_version(&self) -> u16 {
2098 self.active_catalog_version
2099 }
2100
2101 pub fn next_index_id(&self) -> u64 {
2102 self.next_index_id
2103 }
2104
2105 pub fn index_metadata(&self, table: &str) -> Option<Vec<IndexMetadata>> {
2107 let table_ref = self.get_table(table)?;
2108 let mut metadata = table_ref
2109 .indexed_column_metas()
2110 .into_iter()
2111 .map(|index| IndexMetadata {
2112 unique: index.unique,
2113 source: IndexKeySource::Column { column: index.name },
2114 })
2115 .collect::<Vec<_>>();
2116 metadata.extend(table_ref.expression_index_metas().into_iter().map(|index| {
2117 IndexMetadata {
2118 unique: index.unique,
2119 source: IndexKeySource::Expression {
2120 index_id: index.index_id,
2121 canonical_version: index.canonical_version,
2122 canonical_text: index.canonical_text,
2123 json_path: index.json_path,
2124 },
2125 }
2126 }));
2127 Some(metadata)
2128 }
2129
2130 pub fn expression_index_metadata(&self, table: &str) -> Option<Vec<ExpressionIndexMeta>> {
2131 Some(self.get_table(table)?.expression_index_metas())
2132 }
2133
2134 pub fn expression_index_btree(&self, table: &str, index_id: u64) -> Option<&BTree> {
2135 self.get_table(table)?.expression_index_btree(index_id)
2136 }
2137
2138 pub fn expression_index_btree_mut(&mut self, table: &str, index_id: u64) -> Option<&mut BTree> {
2139 self.get_table_mut(table)?
2140 .expression_index_btree_mut(index_id)
2141 }
2142
2143 pub fn expression_index_lookup_all(
2144 &self,
2145 table: &str,
2146 index_id: u64,
2147 key: &Value,
2148 ) -> io::Result<Vec<RowId>> {
2149 let tree = self
2150 .by_name(table)?
2151 .expression_index_btree(index_id)
2152 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2153 Ok(tree.lookup_all(key))
2154 }
2155
2156 pub fn expression_index_range_rids(
2157 &self,
2158 table: &str,
2159 index_id: u64,
2160 start: Option<&Value>,
2161 end: Option<&Value>,
2162 ) -> io::Result<Vec<RowId>> {
2163 let tree = self
2164 .by_name(table)?
2165 .expression_index_btree(index_id)
2166 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2167 Ok(tree.raw_range_rids(start, end))
2168 }
2169
2170 pub fn expression_index_ordered_rids(
2171 &self,
2172 table: &str,
2173 index_id: u64,
2174 ) -> io::Result<Vec<RowId>> {
2175 let tree = self
2176 .by_name(table)?
2177 .expression_index_btree(index_id)
2178 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2179 Ok(tree.ordered_rids_nulls_last())
2180 }
2181
2182 pub fn expression_index_ordered_rids_bounded(
2183 &self,
2184 table: &str,
2185 index_id: u64,
2186 direction: IndexOrderDirection,
2187 offset: usize,
2188 limit: usize,
2189 ) -> io::Result<Vec<RowId>> {
2190 let tree = self
2191 .by_name(table)?
2192 .expression_index_btree(index_id)
2193 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2194 Ok(tree.bounded_ordered_rids_nulls_last(
2195 direction == IndexOrderDirection::Desc,
2196 offset,
2197 limit,
2198 ))
2199 }
2200
2201 pub fn drop_expression_index(&mut self, table: &str, index_id: u64) -> io::Result<()> {
2202 self.invalidate_structure();
2203 validate_table_name(table)?;
2204 let removed = self
2205 .by_name_mut(table)?
2206 .take_expression_index(index_id)
2207 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "expression index not found"))?;
2208 match self.persist_at_activation_boundary() {
2209 Ok(()) => {}
2210 Err(CatalogPersistError::BeforeActivation(error)) => {
2211 self.by_name_mut(table)?.restore_expression_index(removed);
2212 return Err(error);
2213 }
2214 Err(CatalogPersistError::AfterActivation(error)) => {
2215 warn!(
2216 path = %self.data_dir.display(),
2217 error = %error,
2218 "expression index drop committed but catalog directory sync failed"
2219 );
2220 }
2221 }
2222 let index_path = self
2223 .data_dir
2224 .join(expression_index_file_name(table, index_id));
2225 if let Err(error) = fs::remove_file(&index_path) {
2226 if error.kind() != io::ErrorKind::NotFound {
2227 warn!(path = %index_path.display(), error = %error, "failed to remove dropped expression index file");
2228 }
2229 } else if let Err(error) = sync_directory(&self.data_dir) {
2230 warn!(path = %self.data_dir.display(), error = %error, "failed to sync expression index deletion");
2231 }
2232 Ok(())
2233 }
2234
2235 pub fn create_expression_index_metadata(
2239 &mut self,
2240 table: &str,
2241 canonical_version: u16,
2242 canonical_text: impl Into<String>,
2243 json_path: StoredJsonPathV1,
2244 unique: bool,
2245 ) -> io::Result<u64> {
2246 self.invalidate_structure();
2247 validate_table_name(table)?;
2248 validate_column_name(&json_path.column)?;
2249 if canonical_version == 0 {
2250 return Err(io::Error::new(
2251 io::ErrorKind::InvalidInput,
2252 "expression canonical version must be non-zero",
2253 ));
2254 }
2255 let canonical_text = canonical_text.into();
2256 if canonical_text.is_empty() {
2257 return Err(io::Error::new(
2258 io::ErrorKind::InvalidInput,
2259 "expression canonical text must not be empty",
2260 ));
2261 }
2262 if canonical_version == 1 && canonical_text != json_path.canonical_text() {
2263 return Err(io::Error::new(
2264 io::ErrorKind::InvalidInput,
2265 "expression canonical text does not match its stored JSON path",
2266 ));
2267 }
2268 let table_ref = self.by_name(table)?;
2269 let root_index = table_ref
2270 .schema
2271 .column_index(&json_path.column)
2272 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "JSON root column not found"))?;
2273 if table_ref.schema.columns[root_index].type_id != TypeId::Json {
2274 return Err(io::Error::new(
2275 io::ErrorKind::InvalidInput,
2276 "expression index root column must have type json",
2277 ));
2278 }
2279 if table_ref.expression_index_metas().iter().any(|index| {
2280 index.canonical_version == canonical_version && index.canonical_text == canonical_text
2281 }) {
2282 return Err(io::Error::new(
2283 io::ErrorKind::AlreadyExists,
2284 "expression index already exists",
2285 ));
2286 }
2287
2288 let index_id = self.next_index_id;
2289 let next_index_id = index_id
2290 .checked_add(1)
2291 .ok_or_else(|| io::Error::other("expression index id space exhausted"))?;
2292 let index_path = self
2293 .data_dir
2294 .join(expression_index_file_name(table, index_id));
2295 if index_path.exists() {
2296 fs::remove_file(&index_path)?;
2300 sync_directory(&self.data_dir)?;
2301 }
2302 let meta = ExpressionIndexMeta {
2303 index_id,
2304 unique,
2305 canonical_version,
2306 canonical_text,
2307 json_path,
2308 };
2309 self.by_name_mut(table)?
2310 .install_expression_index(meta, &index_path)?;
2311 if let Err(error) = sync_directory(&self.data_dir) {
2312 self.by_name_mut(table)?
2313 .remove_expression_index_by_id(index_id);
2314 let _ = fs::remove_file(&index_path);
2315 return Err(error);
2316 }
2317
2318 let previous_version = self.active_catalog_version;
2319 let previous_next_id = self.next_index_id;
2320 self.active_catalog_version = CATALOG_VERSION;
2321 self.next_index_id = next_index_id;
2322 match self.persist_at_activation_boundary() {
2323 Ok(()) => {}
2324 Err(CatalogPersistError::BeforeActivation(error)) => {
2325 self.by_name_mut(table)?
2326 .remove_expression_index_by_id(index_id);
2327 self.active_catalog_version = previous_version;
2328 self.next_index_id = previous_next_id;
2329 let _ = fs::remove_file(&index_path);
2330 let _ = sync_directory(&self.data_dir);
2331 return Err(error);
2332 }
2333 Err(CatalogPersistError::AfterActivation(error)) => {
2334 warn!(
2335 path = %self.data_dir.display(),
2336 error = %error,
2337 "expression index creation committed but catalog directory sync failed"
2338 );
2339 }
2340 }
2341 Ok(index_id)
2342 }
2343
2344 pub fn is_index_unique(&self, table: &str, column: &str) -> Option<bool> {
2348 self.get_table(table)?.is_index_unique(column)
2349 }
2350
2351 pub fn has_index(&self, table: &str, column: &str) -> bool {
2353 self.get_table(table)
2354 .map(|t| t.has_index(column))
2355 .unwrap_or(false)
2356 }
2357
2358 pub fn index_lookup(&self, table: &str, column: &str, key: &Value) -> io::Result<Option<Row>> {
2359 Ok(self
2360 .by_name(table)?
2361 .index_lookup(column, key)
2362 .map(|(_, row)| row))
2363 }
2364
2365 pub fn list_tables(&self) -> Vec<&str> {
2366 self.tables
2370 .iter()
2371 .map(|t| t.schema.table_name.as_str())
2372 .collect()
2373 }
2374
2375 pub fn schema(&self, table: &str) -> Option<&Schema> {
2376 let slot = *self.name_to_slot.get(table)?;
2377 Some(&self.tables[slot].schema)
2378 }
2379
2380 pub fn drop_table(&mut self, name: &str) -> io::Result<()> {
2383 self.invalidate_structure();
2384 validate_table_name(name)?;
2385 let slot = *self.name_to_slot.get(name).ok_or_else(|| {
2386 io::Error::new(io::ErrorKind::NotFound, format!("table '{name}' not found"))
2387 })?;
2388 if !self.wal.is_off() {
2389 let payload = encode_ddl_drop_table(name);
2390 self.wal.append(0, WalRecordType::DdlDropTable, &payload)?;
2391 self.wal.flush()?;
2392 }
2393 let table = &self.tables[slot];
2395 let heap_path = self
2396 .data_dir
2397 .join(format!("{}.heap", table.schema.table_name));
2398 if heap_path.exists() {
2399 fs::remove_file(&heap_path)?;
2400 }
2401 for col_name in table.indexed_column_names() {
2407 let idx_path = self.data_dir.join(format!("{name}_{col_name}.idx"));
2408 if idx_path.exists() {
2409 let _ = fs::remove_file(&idx_path);
2410 }
2411 }
2412 let expression_index_ids = table.expression_index_ids();
2413 self.name_to_slot.remove(name);
2415 let last = self.tables.len() - 1;
2416 if slot != last {
2417 let moved_name = self.tables[last].schema.table_name.clone();
2418 self.tables.swap(slot, last);
2419 self.name_to_slot.insert(moved_name, slot);
2420 }
2421 self.tables.pop();
2422 self.persist()?;
2423 for index_id in expression_index_ids {
2424 let idx_path = self
2425 .data_dir
2426 .join(expression_index_file_name(name, index_id));
2427 let _ = fs::remove_file(idx_path);
2428 }
2429 Ok(())
2430 }
2431
2432 pub fn alter_table_add_column(&mut self, table: &str, col: ColumnDef) -> io::Result<()> {
2455 self.invalidate_structure();
2456 let data_dir = self.data_dir.clone();
2457 {
2458 let tbl = self.by_name_mut(table)?;
2459 if tbl.schema.columns.iter().any(|c| c.name == col.name) {
2460 return Err(io::Error::new(
2461 io::ErrorKind::AlreadyExists,
2462 format!("column '{}' already exists in table '{table}'", col.name),
2463 ));
2464 }
2465 }
2466 let barrier_lsn = if !self.wal.is_off() {
2467 let payload = encode_ddl_alter_add_column(table, &col);
2468 self.wal.append(0, WalRecordType::DdlAddColumn, &payload)?;
2469 self.wal.flush()?;
2470 self.wal.last_appended_lsn()
2471 } else {
2472 0
2473 };
2474 let tbl = self.by_name_mut(table)?;
2475
2476 let old_schema = tbl.schema.clone();
2477
2478 let has_rows = tbl.heap.scan().next().is_some();
2482
2483 if has_rows && col.required {
2484 return Err(io::Error::new(
2485 io::ErrorKind::InvalidInput,
2486 format!(
2487 "cannot add required column '{}' to non-empty table '{table}': \
2488 no default value to backfill existing rows with",
2489 col.name
2490 ),
2491 ));
2492 }
2493
2494 tbl.schema.columns.push(col);
2498 tbl.refresh_layout();
2499
2500 if has_rows {
2501 let fill: Vec<Value> = vec![Value::Empty; tbl.schema.columns.len()];
2506 tbl.rewrite_rows_for_schema_change(&old_schema, &fill, &data_dir)?;
2507 }
2508 if barrier_lsn > 0 {
2516 tbl.heap.stamp_all_pages_min_lsn(barrier_lsn)?;
2517 tbl.heap.flush()?;
2518 }
2519
2520 self.persist()?;
2521 Ok(())
2522 }
2523
2524 pub fn alter_table_drop_column(&mut self, table: &str, col_name: &str) -> io::Result<()> {
2547 self.invalidate_structure();
2548 let data_dir = self.data_dir.clone();
2549 {
2550 let tbl = self.by_name_mut(table)?;
2551 tbl.schema
2552 .columns
2553 .iter()
2554 .position(|c| c.name == col_name)
2555 .ok_or_else(|| {
2556 io::Error::new(
2557 io::ErrorKind::NotFound,
2558 format!("column '{col_name}' not found in table '{table}'"),
2559 )
2560 })?;
2561 }
2562 let removed_expression_index_ids = self
2563 .by_name_mut(table)?
2564 .remove_expression_indexes_for_root(col_name);
2565 let barrier_lsn = if !self.wal.is_off() {
2566 let payload = encode_ddl_alter_drop_column(table, col_name);
2567 self.wal.append(0, WalRecordType::DdlDropColumn, &payload)?;
2568 self.wal.flush()?;
2569 self.wal.last_appended_lsn()
2570 } else {
2571 0
2572 };
2573 let tbl = self.by_name_mut(table)?;
2574 let idx = tbl
2575 .schema
2576 .columns
2577 .iter()
2578 .position(|c| c.name == col_name)
2579 .ok_or_else(|| {
2580 io::Error::new(
2581 io::ErrorKind::NotFound,
2582 format!("column '{col_name}' not found in table '{table}'"),
2583 )
2584 })?;
2585
2586 let old_schema = tbl.schema.clone();
2588 let has_rows = tbl.heap.scan().next().is_some();
2589
2590 tbl.schema.columns.remove(idx);
2592 for (i, col) in tbl.schema.columns.iter_mut().enumerate() {
2593 col.position = i as u16;
2594 }
2595 tbl.refresh_layout();
2596
2597 if has_rows {
2598 let fill: Vec<Value> = vec![Value::Empty; tbl.schema.columns.len()];
2604 tbl.rewrite_rows_for_schema_change(&old_schema, &fill, &data_dir)?;
2605 }
2606 if barrier_lsn > 0 {
2608 tbl.heap.stamp_all_pages_min_lsn(barrier_lsn)?;
2609 tbl.heap.flush()?;
2610 }
2611
2612 self.persist()?;
2613 for index_id in removed_expression_index_ids {
2614 let idx_path = self
2615 .data_dir
2616 .join(expression_index_file_name(table, index_id));
2617 let _ = fs::remove_file(idx_path);
2618 }
2619 Ok(())
2620 }
2621}
2622
2623impl Drop for Catalog {
2624 fn drop(&mut self) {
2625 if self.active_tx_id.is_some() {
2626 if let Err(e) = self.abandon_active_transaction_for_drop() {
2627 warn!(error = %e, "catalog drop active transaction cleanup failed");
2628 }
2629 return;
2630 }
2631 if let Err(e) = self.checkpoint() {
2643 warn!(error = %e, "catalog drop checkpoint failed");
2644 }
2645 }
2646}
2647
2648fn encode_wal_payload(table: &str, rid: RowId, row_bytes: &[u8]) -> Vec<u8> {
2663 let name = table.as_bytes();
2664 let mut out = Vec::with_capacity(4 + name.len() + 4 + 2 + 4 + row_bytes.len());
2665 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
2666 out.extend_from_slice(name);
2667 out.extend_from_slice(&rid.page_id.to_le_bytes());
2668 out.extend_from_slice(&rid.slot_index.to_le_bytes());
2669 out.extend_from_slice(&(row_bytes.len() as u32).to_le_bytes());
2670 out.extend_from_slice(row_bytes);
2671 out
2672}
2673
2674fn decode_wal_payload(data: &[u8]) -> Option<(String, RowId, Vec<u8>)> {
2675 let mut pos = 0usize;
2676 if data.len() < 4 {
2677 return None;
2678 }
2679 let name_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2680 pos += 4;
2681 if pos + name_len > data.len() {
2682 return None;
2683 }
2684 let name = std::str::from_utf8(&data[pos..pos + name_len])
2685 .ok()?
2686 .to_string();
2687 pos += name_len;
2688 if pos + 4 + 2 + 4 > data.len() {
2689 return None;
2690 }
2691 let page_id = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
2692 pos += 4;
2693 let slot_index = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
2694 pos += 2;
2695 let row_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2696 pos += 4;
2697 if pos + row_len > data.len() {
2698 return None;
2699 }
2700 let row_bytes = data[pos..pos + row_len].to_vec();
2701 Some((
2702 name,
2703 RowId {
2704 page_id,
2705 slot_index,
2706 },
2707 row_bytes,
2708 ))
2709}
2710
2711fn write_overflow_chain_logged(
2717 heap: &mut HeapFile,
2718 wal: &mut Wal,
2719 table: &str,
2720 tx_id: u64,
2721 value: &[u8],
2722) -> io::Result<OverflowStub> {
2723 if value.len() > MAX_VALUE_SIZE {
2724 return Err(StorageError::ValueTooLarge {
2725 size: value.len(),
2726 max: MAX_VALUE_SIZE,
2727 }
2728 .into());
2729 }
2730 let n = value.len().div_ceil(OVERFLOW_PAYLOAD_CAP).max(1);
2731 let mut pages = Vec::with_capacity(n);
2732 for _ in 0..n {
2733 pages.push(heap.allocate_overflow_page()?);
2734 }
2735 for i in 0..n {
2736 let start = i * OVERFLOW_PAYLOAD_CAP;
2737 let end = (start + OVERFLOW_PAYLOAD_CAP).min(value.len());
2738 let chunk = &value[start..end];
2739 let next = if i + 1 < n {
2740 pages[i + 1]
2741 } else {
2742 OVERFLOW_CHAIN_END
2743 };
2744 let payload = encode_overflow_write_payload(table, pages[i], next, chunk);
2745 wal.append(tx_id, WalRecordType::OverflowWrite, &payload)?;
2746 let lsn = wal.last_appended_lsn();
2747 heap.write_overflow_page(pages[i], next, chunk, lsn)?;
2748 }
2749 Ok(OverflowStub::new(
2750 value.len() as u64,
2751 pages[0],
2752 crc32fast::hash(value),
2753 ))
2754}
2755
2756fn encode_row_with_spill_logged(
2761 tbl: &mut Table,
2762 wal: &mut Wal,
2763 tx_id: u64,
2764 values: &Row,
2765) -> io::Result<Vec<u8>> {
2766 let v1_len = crate::row::v1_encoded_len(tbl.row_layout(), values);
2769 let is_indexed = tbl.indexed_col_mask();
2770 let chosen = plan_spill(tbl.row_layout(), values, v1_len, &is_indexed);
2771 if chosen.is_empty() {
2772 let mut v1 = Vec::new();
2773 encode_row_into(&tbl.schema, values, &mut v1);
2774 return Ok(v1);
2775 }
2776 let table_name = tbl.schema.table_name.clone();
2777 let n_var = tbl.row_layout().n_var();
2778 let mut spilled: Vec<Option<OverflowStub>> = vec![None; n_var];
2779 for col_idx in chosen {
2780 let var_idx = tbl
2781 .row_layout()
2782 .var_index(col_idx)
2783 .expect("plan_spill only returns var columns");
2784 let bytes: Vec<u8> = match &values[col_idx] {
2785 Value::Str(s) => s.as_bytes().to_vec(),
2786 Value::Bytes(b) => b.to_vec(),
2787 Value::Json(b) => b.to_vec(),
2788 _ => continue,
2789 };
2790 let stub = write_overflow_chain_logged(&mut tbl.heap, wal, &table_name, tx_id, &bytes)?;
2791 spilled[var_idx] = Some(stub);
2792 }
2793 let mut out = Vec::new();
2794 encode_row_v2_into(&tbl.schema, tbl.row_layout(), values, &spilled, &mut out);
2795 Ok(out)
2796}
2797
2798fn encode_overflow_write_payload(
2807 table: &str,
2808 page_id: u32,
2809 next_page: u32,
2810 chunk: &[u8],
2811) -> Vec<u8> {
2812 let name = table.as_bytes();
2813 let mut out = Vec::with_capacity(2 + name.len() + 4 + 4 + 2 + chunk.len());
2814 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
2815 out.extend_from_slice(name);
2816 out.extend_from_slice(&page_id.to_le_bytes());
2817 out.extend_from_slice(&next_page.to_le_bytes());
2818 out.extend_from_slice(&(chunk.len() as u16).to_le_bytes());
2819 out.extend_from_slice(chunk);
2820 out
2821}
2822
2823fn decode_overflow_write_payload(data: &[u8]) -> Option<(String, u32, u32, Vec<u8>)> {
2824 let mut pos = 0usize;
2825 if data.len() < 2 {
2826 return None;
2827 }
2828 let name_len = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
2829 pos += 2;
2830 if pos + name_len + 4 + 4 + 2 > data.len() {
2831 return None;
2832 }
2833 let name = std::str::from_utf8(&data[pos..pos + name_len])
2834 .ok()?
2835 .to_string();
2836 pos += name_len;
2837 let page_id = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
2838 pos += 4;
2839 let next_page = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
2840 pos += 4;
2841 let chunk_len = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
2842 pos += 2;
2843 if pos + chunk_len > data.len() {
2844 return None;
2845 }
2846 Some((
2847 name,
2848 page_id,
2849 next_page,
2850 data[pos..pos + chunk_len].to_vec(),
2851 ))
2852}
2853
2854fn encode_overflow_free_payload(table: &str, pages: &[u32]) -> Vec<u8> {
2858 let name = table.as_bytes();
2859 let mut out = Vec::with_capacity(2 + name.len() + 4 + pages.len() * 4);
2860 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
2861 out.extend_from_slice(name);
2862 out.extend_from_slice(&(pages.len() as u32).to_le_bytes());
2863 for p in pages {
2864 out.extend_from_slice(&p.to_le_bytes());
2865 }
2866 out
2867}
2868
2869fn decode_overflow_free_payload(data: &[u8]) -> Option<(String, Vec<u32>)> {
2870 let mut pos = 0usize;
2871 if data.len() < 2 {
2872 return None;
2873 }
2874 let name_len = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
2875 pos += 2;
2876 if pos + name_len + 4 > data.len() {
2877 return None;
2878 }
2879 let name = std::str::from_utf8(&data[pos..pos + name_len])
2880 .ok()?
2881 .to_string();
2882 pos += name_len;
2883 let count = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2884 pos += 4;
2885 if pos + count * 4 > data.len() {
2886 return None;
2887 }
2888 let mut pages = Vec::with_capacity(count);
2889 for _ in 0..count {
2890 pages.push(u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?));
2891 pos += 4;
2892 }
2893 Some((name, pages))
2894}
2895
2896fn encode_ddl_create_table(
2899 schema: &Schema,
2900 defaults: &[Option<Value>],
2901 auto_cols: &[bool],
2902) -> Vec<u8> {
2903 let name = schema.table_name.as_bytes();
2904 let mut out = Vec::new();
2905 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
2906 out.extend_from_slice(name);
2907 out.extend_from_slice(&(schema.columns.len() as u16).to_le_bytes());
2908 for col in &schema.columns {
2909 let cn = col.name.as_bytes();
2910 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
2911 out.extend_from_slice(cn);
2912 out.push(col.type_id as u8);
2913 out.push(col.required as u8);
2914 out.extend_from_slice(&col.position.to_le_bytes());
2915 }
2916 encode_defaults_section(&mut out, defaults);
2920 encode_auto_section(&mut out, auto_cols);
2921 out
2922}
2923
2924fn decode_ddl_create_table(data: &[u8]) -> Option<(Schema, Vec<Option<Value>>, Vec<bool>)> {
2925 let mut pos = 0usize;
2926 if data.len() < 4 {
2927 return None;
2928 }
2929 let name_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2930 pos += 4;
2931 if pos + name_len > data.len() {
2932 return None;
2933 }
2934 let table_name = std::str::from_utf8(&data[pos..pos + name_len])
2935 .ok()?
2936 .to_string();
2937 pos += name_len;
2938 if pos + 2 > data.len() {
2939 return None;
2940 }
2941 let n_cols = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
2942 pos += 2;
2943 let mut columns = Vec::with_capacity(n_cols);
2944 for _ in 0..n_cols {
2945 if pos + 4 > data.len() {
2946 return None;
2947 }
2948 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2949 pos += 4;
2950 if pos + cn_len + 4 > data.len() {
2951 return None;
2952 }
2953 let col_name = std::str::from_utf8(&data[pos..pos + cn_len])
2954 .ok()?
2955 .to_string();
2956 pos += cn_len;
2957 let type_id = TypeId::from_u8(data[pos])?;
2958 pos += 1;
2959 let required = data[pos] != 0;
2960 pos += 1;
2961 if pos + 2 > data.len() {
2962 return None;
2963 }
2964 let position = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
2965 pos += 2;
2966 columns.push(ColumnDef {
2967 name: col_name,
2968 type_id,
2969 required,
2970 position,
2971 });
2972 }
2973 let defaults = if pos < data.len() {
2976 decode_defaults_section(data, &mut pos, columns.len())?
2977 } else {
2978 Vec::new()
2979 };
2980 let auto_cols = if pos < data.len() {
2981 decode_auto_section(data, &mut pos, columns.len())?
2982 } else {
2983 Vec::new()
2984 };
2985 Some((
2986 Schema {
2987 table_name,
2988 columns,
2989 },
2990 defaults,
2991 auto_cols,
2992 ))
2993}
2994
2995fn encode_ddl_drop_table(table_name: &str) -> Vec<u8> {
2996 let name = table_name.as_bytes();
2997 let mut out = Vec::with_capacity(4 + name.len());
2998 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
2999 out.extend_from_slice(name);
3000 out
3001}
3002
3003fn encode_ddl_alter_add_column(table_name: &str, col: &ColumnDef) -> Vec<u8> {
3004 let name = table_name.as_bytes();
3005 let cn = col.name.as_bytes();
3006 let mut out = Vec::with_capacity(4 + name.len() + 4 + cn.len() + 4);
3007 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3008 out.extend_from_slice(name);
3009 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3010 out.extend_from_slice(cn);
3011 out.push(col.type_id as u8);
3012 out.push(col.required as u8);
3013 out.extend_from_slice(&col.position.to_le_bytes());
3014 out
3015}
3016
3017fn encode_ddl_alter_drop_column(table_name: &str, col_name: &str) -> Vec<u8> {
3018 let name = table_name.as_bytes();
3019 let cn = col_name.as_bytes();
3020 let mut out = Vec::with_capacity(4 + name.len() + 4 + cn.len());
3021 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
3022 out.extend_from_slice(name);
3023 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3024 out.extend_from_slice(cn);
3025 out
3026}
3027
3028fn decode_ddl_table_name(data: &[u8]) -> Option<(String, usize)> {
3029 if data.len() < 4 {
3030 return None;
3031 }
3032 let name_len = u32::from_le_bytes(data[0..4].try_into().ok()?) as usize;
3033 if 4 + name_len > data.len() {
3034 return None;
3035 }
3036 let name = std::str::from_utf8(&data[4..4 + name_len])
3037 .ok()?
3038 .to_string();
3039 Some((name, 4 + name_len))
3040}
3041
3042fn decode_ddl_alter_add_column(data: &[u8]) -> Option<(String, ColumnDef)> {
3043 let (table_name, mut pos) = decode_ddl_table_name(data)?;
3044 if pos + 4 > data.len() {
3045 return None;
3046 }
3047 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3048 pos += 4;
3049 if pos + cn_len + 4 > data.len() {
3050 return None;
3051 }
3052 let col_name = std::str::from_utf8(&data[pos..pos + cn_len])
3053 .ok()?
3054 .to_string();
3055 pos += cn_len;
3056 let type_id = TypeId::from_u8(data[pos])?;
3057 pos += 1;
3058 let required = data[pos] != 0;
3059 pos += 1;
3060 if pos + 2 > data.len() {
3061 return None;
3062 }
3063 let position = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
3064 Some((
3065 table_name,
3066 ColumnDef {
3067 name: col_name,
3068 type_id,
3069 required,
3070 position,
3071 },
3072 ))
3073}
3074
3075fn decode_ddl_alter_drop_column(data: &[u8]) -> Option<(String, String)> {
3076 let (table_name, pos) = decode_ddl_table_name(data)?;
3077 if pos + 4 > data.len() {
3078 return None;
3079 }
3080 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
3081 if pos + 4 + cn_len > data.len() {
3082 return None;
3083 }
3084 let col_name = std::str::from_utf8(&data[pos + 4..pos + 4 + cn_len])
3085 .ok()?
3086 .to_string();
3087 Some((table_name, col_name))
3088}
3089
3090pub(crate) struct IndexedColMeta {
3118 pub name: String,
3119 pub unique: bool,
3120}
3121
3122pub(crate) struct CatalogEntry {
3125 pub schema: Schema,
3126 pub indexed_cols: Vec<IndexedColMeta>,
3127 pub expression_indexes: Vec<ExpressionIndexMeta>,
3128 pub defaults: Vec<Option<Value>>,
3131 pub auto_cols: Vec<bool>,
3134}
3135
3136pub(crate) struct CatalogEntryRef<'a> {
3138 pub schema: &'a Schema,
3139 pub indexed_cols: Vec<IndexedColMeta>,
3140 pub expression_indexes: Vec<ExpressionIndexMeta>,
3141 pub defaults: &'a [Option<Value>],
3142 pub auto_cols: &'a [bool],
3143}
3144
3145fn encode_value_blob(out: &mut Vec<u8>, v: &Value) {
3151 out.push(v.type_id() as u8);
3152 match v {
3153 Value::Int(n) => out.extend_from_slice(&n.to_le_bytes()),
3154 Value::Float(f) => out.extend_from_slice(&f.to_bits().to_le_bytes()),
3155 Value::Bool(b) => out.push(*b as u8),
3156 Value::Str(s) => {
3157 out.extend_from_slice(&(s.len() as u32).to_le_bytes());
3158 out.extend_from_slice(s.as_bytes());
3159 }
3160 Value::DateTime(n) => out.extend_from_slice(&n.to_le_bytes()),
3161 Value::Uuid(u) => out.extend_from_slice(u),
3162 Value::Bytes(b) => {
3163 out.extend_from_slice(&(b.len() as u32).to_le_bytes());
3164 out.extend_from_slice(b);
3165 }
3166 Value::Json(b) => {
3167 out.extend_from_slice(&(b.len() as u32).to_le_bytes());
3168 out.extend_from_slice(b);
3169 }
3170 Value::Empty => {}
3171 }
3172}
3173
3174fn decode_value_blob(data: &[u8], pos: &mut usize) -> Option<Value> {
3177 let tag = *data.get(*pos)?;
3178 *pos += 1;
3179 let type_id = TypeId::from_u8(tag)?;
3180 let take_fixed = |pos: &mut usize, n: usize| -> Option<Vec<u8>> {
3181 if *pos + n > data.len() {
3182 return None;
3183 }
3184 let slice = data[*pos..*pos + n].to_vec();
3185 *pos += n;
3186 Some(slice)
3187 };
3188 match type_id {
3189 TypeId::Empty => Some(Value::Empty),
3190 TypeId::Int => Some(Value::Int(i64::from_le_bytes(
3191 take_fixed(pos, 8)?.try_into().ok()?,
3192 ))),
3193 TypeId::Float => Some(Value::Float(f64::from_bits(u64::from_le_bytes(
3194 take_fixed(pos, 8)?.try_into().ok()?,
3195 )))),
3196 TypeId::Bool => Some(Value::Bool(take_fixed(pos, 1)?[0] != 0)),
3197 TypeId::DateTime => Some(Value::DateTime(i64::from_le_bytes(
3198 take_fixed(pos, 8)?.try_into().ok()?,
3199 ))),
3200 TypeId::Uuid => Some(Value::Uuid(take_fixed(pos, 16)?.try_into().ok()?)),
3201 TypeId::Str => {
3202 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
3203 Some(Value::Str(String::from_utf8(take_fixed(pos, len)?).ok()?))
3204 }
3205 TypeId::Bytes => {
3206 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
3207 Some(Value::Bytes(take_fixed(pos, len)?))
3208 }
3209 TypeId::Json => {
3210 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
3211 Some(Value::Json(take_fixed(pos, len)?.into()))
3212 }
3213 }
3214}
3215
3216fn encode_defaults_section(out: &mut Vec<u8>, defaults: &[Option<Value>]) {
3220 let present: Vec<(u16, &Value)> = defaults
3221 .iter()
3222 .enumerate()
3223 .filter_map(|(i, d)| d.as_ref().map(|v| (i as u16, v)))
3224 .collect();
3225 out.extend_from_slice(&(present.len() as u16).to_le_bytes());
3226 for (pos, v) in present {
3227 out.extend_from_slice(&pos.to_le_bytes());
3228 encode_value_blob(out, v);
3229 }
3230}
3231
3232fn decode_defaults_section(
3235 data: &[u8],
3236 pos: &mut usize,
3237 n_cols: usize,
3238) -> Option<Vec<Option<Value>>> {
3239 if *pos + 2 > data.len() {
3240 return None;
3241 }
3242 let count = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3243 *pos += 2;
3244 let mut out = vec![None; n_cols];
3245 for _ in 0..count {
3246 if *pos + 2 > data.len() {
3247 return None;
3248 }
3249 let col = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3250 *pos += 2;
3251 let value = decode_value_blob(data, pos)?;
3252 if col < n_cols {
3253 out[col] = Some(value);
3254 }
3255 }
3256 Some(out)
3257}
3258
3259fn encode_auto_section(out: &mut Vec<u8>, auto_cols: &[bool]) {
3263 let present: Vec<u16> = auto_cols
3264 .iter()
3265 .enumerate()
3266 .filter_map(|(i, &a)| if a { Some(i as u16) } else { None })
3267 .collect();
3268 out.extend_from_slice(&(present.len() as u16).to_le_bytes());
3269 for pos in present {
3270 out.extend_from_slice(&pos.to_le_bytes());
3271 }
3272}
3273
3274fn decode_auto_section(data: &[u8], pos: &mut usize, n_cols: usize) -> Option<Vec<bool>> {
3277 if *pos + 2 > data.len() {
3278 return None;
3279 }
3280 let count = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3281 *pos += 2;
3282 let mut out = vec![false; n_cols];
3283 for _ in 0..count {
3284 if *pos + 2 > data.len() {
3285 return None;
3286 }
3287 let col = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
3288 *pos += 2;
3289 if col < n_cols {
3290 out[col] = true;
3291 }
3292 }
3293 Some(out)
3294}
3295
3296fn push_catalog_string(out: &mut Vec<u8>, value: &str) -> io::Result<()> {
3297 let len = u32::try_from(value.len())
3298 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "catalog string is too large"))?;
3299 out.extend_from_slice(&len.to_le_bytes());
3300 out.extend_from_slice(value.as_bytes());
3301 Ok(())
3302}
3303
3304fn encode_expression_indexes(out: &mut Vec<u8>, indexes: &[ExpressionIndexMeta]) -> io::Result<()> {
3305 let count = u16::try_from(indexes.len()).map_err(|_| {
3306 io::Error::new(
3307 io::ErrorKind::InvalidInput,
3308 "too many expression indexes on one table",
3309 )
3310 })?;
3311 out.extend_from_slice(&count.to_le_bytes());
3312 for index in indexes {
3313 out.extend_from_slice(&index.index_id.to_le_bytes());
3314 out.push(u8::from(index.unique));
3315 out.extend_from_slice(&index.canonical_version.to_le_bytes());
3316 push_catalog_string(out, &index.canonical_text)?;
3317 push_catalog_string(out, &index.json_path.column)?;
3318 let segment_count = u16::try_from(index.json_path.segments.len()).map_err(|_| {
3319 io::Error::new(
3320 io::ErrorKind::InvalidInput,
3321 "JSON path has too many segments",
3322 )
3323 })?;
3324 out.extend_from_slice(&segment_count.to_le_bytes());
3325 for segment in &index.json_path.segments {
3326 match segment {
3327 StoredJsonPathSegmentV1::Key(key) => {
3328 out.push(1);
3329 push_catalog_string(out, key)?;
3330 }
3331 StoredJsonPathSegmentV1::Index(position) => {
3332 out.push(2);
3333 out.extend_from_slice(&position.to_le_bytes());
3334 }
3335 }
3336 }
3337 }
3338 Ok(())
3339}
3340
3341fn decode_expression_indexes(data: &[u8], pos: &mut usize) -> io::Result<Vec<ExpressionIndexMeta>> {
3342 let count = read_u16(data, pos)? as usize;
3343 let mut indexes = Vec::with_capacity(count);
3344 for _ in 0..count {
3345 let index_id = read_u64(data, pos)?;
3346 if index_id == 0 {
3347 return Err(io::Error::new(
3348 io::ErrorKind::InvalidData,
3349 "expression index id must be non-zero",
3350 ));
3351 }
3352 let unique = read_u8(data, pos)? != 0;
3353 let canonical_version = read_u16(data, pos)?;
3354 let canonical_len = read_u32(data, pos)? as usize;
3355 let canonical_text = read_string(data, pos, canonical_len)?;
3356 let column_len = read_u32(data, pos)? as usize;
3357 let column = read_string(data, pos, column_len)?;
3358 let segment_count = read_u16(data, pos)? as usize;
3359 let mut segments = Vec::with_capacity(segment_count);
3360 for _ in 0..segment_count {
3361 match read_u8(data, pos)? {
3362 1 => {
3363 let len = read_u32(data, pos)? as usize;
3364 segments.push(StoredJsonPathSegmentV1::Key(read_string(data, pos, len)?));
3365 }
3366 2 => segments.push(StoredJsonPathSegmentV1::Index(read_u32(data, pos)?)),
3367 tag => {
3368 return Err(io::Error::new(
3369 io::ErrorKind::InvalidData,
3370 format!("unknown stored JSON path segment tag: {tag}"),
3371 ));
3372 }
3373 }
3374 }
3375 indexes.push(ExpressionIndexMeta {
3376 index_id,
3377 unique,
3378 canonical_version,
3379 canonical_text,
3380 json_path: StoredJsonPathV1 { column, segments },
3381 });
3382 }
3383 Ok(indexes)
3384}
3385
3386fn write_catalog_file(
3387 path: &Path,
3388 version: u16,
3389 next_index_id: u64,
3390 entries: &[CatalogEntryRef<'_>],
3391) -> io::Result<()> {
3392 if !(1..=CATALOG_VERSION).contains(&version) {
3393 return Err(io::Error::new(
3394 io::ErrorKind::InvalidInput,
3395 format!("unsupported catalog write version: {version}"),
3396 ));
3397 }
3398 let mut buf: Vec<u8> = Vec::with_capacity(64);
3399 buf.extend_from_slice(CATALOG_MAGIC);
3400 buf.extend_from_slice(&version.to_le_bytes());
3401 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
3402 if version >= 6 {
3403 buf.extend_from_slice(&next_index_id.to_le_bytes());
3404 }
3405
3406 for entry in entries {
3407 let schema = entry.schema;
3408 let name = schema.table_name.as_bytes();
3409 buf.extend_from_slice(&(name.len() as u32).to_le_bytes());
3410 buf.extend_from_slice(name);
3411 buf.extend_from_slice(&(schema.columns.len() as u16).to_le_bytes());
3412 for col in &schema.columns {
3413 let cn = col.name.as_bytes();
3414 buf.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3415 buf.extend_from_slice(cn);
3416 buf.push(col.type_id as u8);
3417 buf.push(if col.required { 1 } else { 0 });
3418 buf.extend_from_slice(&col.position.to_le_bytes());
3419 }
3420 buf.extend_from_slice(&(entry.indexed_cols.len() as u16).to_le_bytes());
3422 for meta in &entry.indexed_cols {
3423 let cn = meta.name.as_bytes();
3424 buf.extend_from_slice(&(cn.len() as u32).to_le_bytes());
3425 buf.extend_from_slice(cn);
3426 buf.push(if meta.unique { 1 } else { 0 });
3427 }
3428 encode_defaults_section(&mut buf, entry.defaults);
3430 encode_auto_section(&mut buf, entry.auto_cols);
3432 if version >= 6 {
3433 encode_expression_indexes(&mut buf, &entry.expression_indexes)?;
3434 }
3435 }
3436
3437 let crc = crc32fast::hash(&buf);
3441 buf.extend_from_slice(&crc.to_le_bytes());
3442
3443 let mut f = fs::OpenOptions::new()
3444 .create(true)
3445 .write(true)
3446 .truncate(true)
3447 .open(path)?;
3448 f.write_all(&buf)?;
3449 f.sync_data()?;
3450 Ok(())
3451}
3452
3453struct CatalogFile {
3454 version: u16,
3455 next_index_id: u64,
3456 entries: Vec<CatalogEntry>,
3457}
3458
3459fn read_catalog_file(path: &Path) -> io::Result<CatalogFile> {
3460 read_catalog_file_with_max_version(path, CATALOG_VERSION)
3461}
3462
3463pub fn read_active_catalog_version(data_dir: &Path) -> io::Result<u16> {
3469 let cat_path = data_dir.join(CATALOG_FILE);
3470 Ok(read_catalog_file(&cat_path)?.version)
3471}
3472
3473fn read_catalog_file_with_max_version(
3474 path: &Path,
3475 max_supported_version: u16,
3476) -> io::Result<CatalogFile> {
3477 let mut f = fs::File::open(path)?;
3478 let mut buf = Vec::new();
3479 f.read_to_end(&mut buf)?;
3480
3481 let mut pos = 0usize;
3482 if buf.len() < 14 || &buf[0..4] != CATALOG_MAGIC {
3484 return Err(io::Error::new(
3485 io::ErrorKind::InvalidData,
3486 "bad catalog magic",
3487 ));
3488 }
3489
3490 let payload = &buf[..buf.len() - 4];
3492 let stored_crc = u32::from_le_bytes(
3493 buf[buf.len() - 4..]
3494 .try_into()
3495 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog CRC"))?,
3496 );
3497 let computed_crc = crc32fast::hash(payload);
3498 if stored_crc != computed_crc {
3499 return Err(io::Error::new(
3500 io::ErrorKind::InvalidData,
3501 format!(
3502 "catalog CRC32 mismatch: expected {stored_crc:#010x}, got {computed_crc:#010x}"
3503 ),
3504 ));
3505 }
3506 let buf = &buf[..buf.len() - 4];
3508 pos += 4;
3509 let version = u16::from_le_bytes(
3510 buf[pos..pos + 2]
3511 .try_into()
3512 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog header"))?,
3513 );
3514 pos += 2;
3515 if version == 0 || version > max_supported_version {
3525 return Err(io::Error::new(
3526 io::ErrorKind::InvalidData,
3527 format!("unsupported catalog version: {version}"),
3528 ));
3529 }
3530 let n_tables = u32::from_le_bytes(
3531 buf[pos..pos + 4]
3532 .try_into()
3533 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog header"))?,
3534 ) as usize;
3535 pos += 4;
3536 let next_index_id = if version >= 6 {
3537 let id = read_u64(buf, &mut pos)?;
3538 if id == 0 {
3539 return Err(io::Error::new(
3540 io::ErrorKind::InvalidData,
3541 "catalog next index id must be non-zero",
3542 ));
3543 }
3544 id
3545 } else {
3546 1
3547 };
3548
3549 if n_tables > buf.len() {
3556 return Err(io::Error::new(
3557 io::ErrorKind::InvalidData,
3558 format!("catalog file corrupt: implausible table count {n_tables}"),
3559 ));
3560 }
3561
3562 let mut entries = Vec::with_capacity(n_tables);
3563 for _ in 0..n_tables {
3564 let name_len = read_u32(buf, &mut pos)? as usize;
3565 let table_name = read_string(buf, &mut pos, name_len)?;
3566 let n_cols = read_u16(buf, &mut pos)? as usize;
3567
3568 let mut columns = Vec::with_capacity(n_cols);
3569 for _ in 0..n_cols {
3570 let cname_len = read_u32(buf, &mut pos)? as usize;
3571 let name = read_string(buf, &mut pos, cname_len)?;
3572 let type_id_raw = read_u8(buf, &mut pos)?;
3573 let type_id = type_id_from_u8(type_id_raw)?;
3574 let required = read_u8(buf, &mut pos)? != 0;
3575 let position = read_u16(buf, &mut pos)?;
3576 columns.push(ColumnDef {
3577 name,
3578 type_id,
3579 required,
3580 position,
3581 });
3582 }
3583
3584 let indexed_cols: Vec<IndexedColMeta> = if version >= 3 {
3588 let n = read_u16(buf, &mut pos)? as usize;
3589 let mut v = Vec::with_capacity(n);
3590 for _ in 0..n {
3591 let l = read_u32(buf, &mut pos)? as usize;
3592 let name = read_string(buf, &mut pos, l)?;
3593 let unique = read_u8(buf, &mut pos)? != 0;
3594 v.push(IndexedColMeta { name, unique });
3595 }
3596 v
3597 } else if version >= 2 {
3598 let n = read_u16(buf, &mut pos)? as usize;
3599 let mut v = Vec::with_capacity(n);
3600 for _ in 0..n {
3601 let l = read_u32(buf, &mut pos)? as usize;
3602 let name = read_string(buf, &mut pos, l)?;
3603 v.push(IndexedColMeta {
3604 name,
3605 unique: false,
3606 });
3607 }
3608 v
3609 } else {
3610 Vec::new()
3611 };
3612
3613 let defaults = if version >= 4 {
3615 decode_defaults_section(buf, &mut pos, columns.len()).ok_or_else(|| {
3616 io::Error::new(io::ErrorKind::InvalidData, "truncated catalog defaults")
3617 })?
3618 } else {
3619 Vec::new()
3620 };
3621
3622 let auto_cols = if version >= 5 {
3624 decode_auto_section(buf, &mut pos, columns.len()).ok_or_else(|| {
3625 io::Error::new(io::ErrorKind::InvalidData, "truncated catalog auto columns")
3626 })?
3627 } else {
3628 Vec::new()
3629 };
3630
3631 let expression_indexes = if version >= 6 {
3632 decode_expression_indexes(buf, &mut pos)?
3633 } else {
3634 Vec::new()
3635 };
3636
3637 entries.push(CatalogEntry {
3638 schema: Schema {
3639 table_name,
3640 columns,
3641 },
3642 indexed_cols,
3643 expression_indexes,
3644 defaults,
3645 auto_cols,
3646 });
3647 }
3648
3649 let mut seen_index_ids = FxHashMap::default();
3650 let mut max_index_id = 0;
3651 for entry in &entries {
3652 for index in &entry.expression_indexes {
3653 if index.canonical_version == 0 || index.canonical_text.is_empty() {
3654 return Err(io::Error::new(
3655 io::ErrorKind::InvalidData,
3656 "expression index has invalid canonical identity",
3657 ));
3658 }
3659 if index.canonical_version == 1
3660 && index.canonical_text != index.json_path.canonical_text()
3661 {
3662 return Err(io::Error::new(
3663 io::ErrorKind::InvalidData,
3664 "expression index canonical identity does not match its JSON path",
3665 ));
3666 }
3667 let Some(root) = entry
3668 .schema
3669 .columns
3670 .iter()
3671 .find(|column| column.name == index.json_path.column)
3672 else {
3673 return Err(io::Error::new(
3674 io::ErrorKind::InvalidData,
3675 "expression index JSON root is absent from its table",
3676 ));
3677 };
3678 if root.type_id != TypeId::Json {
3679 return Err(io::Error::new(
3680 io::ErrorKind::InvalidData,
3681 "expression index root column is not JSON",
3682 ));
3683 }
3684 if seen_index_ids.insert(index.index_id, ()).is_some() {
3685 return Err(io::Error::new(
3686 io::ErrorKind::InvalidData,
3687 "duplicate expression index id in catalog",
3688 ));
3689 }
3690 max_index_id = max_index_id.max(index.index_id);
3691 }
3692 }
3693 if next_index_id <= max_index_id {
3694 return Err(io::Error::new(
3695 io::ErrorKind::InvalidData,
3696 "catalog next index id does not exceed persisted index ids",
3697 ));
3698 }
3699 Ok(CatalogFile {
3700 version,
3701 next_index_id,
3702 entries,
3703 })
3704}
3705
3706fn read_u8(buf: &[u8], pos: &mut usize) -> io::Result<u8> {
3707 if *pos >= buf.len() {
3708 return Err(io::Error::new(
3709 io::ErrorKind::UnexpectedEof,
3710 "truncated catalog",
3711 ));
3712 }
3713 let v = buf[*pos];
3714 *pos += 1;
3715 Ok(v)
3716}
3717fn read_u16(buf: &[u8], pos: &mut usize) -> io::Result<u16> {
3718 if *pos + 2 > buf.len() {
3719 return Err(io::Error::new(
3720 io::ErrorKind::UnexpectedEof,
3721 "truncated catalog",
3722 ));
3723 }
3724 let v = u16::from_le_bytes(
3725 buf[*pos..*pos + 2]
3726 .try_into()
3727 .expect("bounds checked above"),
3728 );
3729 *pos += 2;
3730 Ok(v)
3731}
3732fn read_u32(buf: &[u8], pos: &mut usize) -> io::Result<u32> {
3733 if *pos + 4 > buf.len() {
3734 return Err(io::Error::new(
3735 io::ErrorKind::UnexpectedEof,
3736 "truncated catalog",
3737 ));
3738 }
3739 let v = u32::from_le_bytes(
3740 buf[*pos..*pos + 4]
3741 .try_into()
3742 .expect("bounds checked above"),
3743 );
3744 *pos += 4;
3745 Ok(v)
3746}
3747fn read_u64(buf: &[u8], pos: &mut usize) -> io::Result<u64> {
3748 if *pos + 8 > buf.len() {
3749 return Err(io::Error::new(
3750 io::ErrorKind::UnexpectedEof,
3751 "truncated catalog",
3752 ));
3753 }
3754 let value = u64::from_le_bytes(
3755 buf[*pos..*pos + 8]
3756 .try_into()
3757 .expect("bounds checked above"),
3758 );
3759 *pos += 8;
3760 Ok(value)
3761}
3762fn read_string(buf: &[u8], pos: &mut usize, len: usize) -> io::Result<String> {
3763 if *pos + len > buf.len() {
3764 return Err(io::Error::new(
3765 io::ErrorKind::UnexpectedEof,
3766 "truncated catalog string",
3767 ));
3768 }
3769 let s = std::str::from_utf8(&buf[*pos..*pos + len])
3770 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "non-utf8 in catalog"))?
3771 .to_string();
3772 *pos += len;
3773 Ok(s)
3774}
3775fn type_id_from_u8(v: u8) -> io::Result<TypeId> {
3776 match v {
3777 0 => Ok(TypeId::Empty),
3778 1 => Ok(TypeId::Int),
3779 2 => Ok(TypeId::Float),
3780 3 => Ok(TypeId::Bool),
3781 4 => Ok(TypeId::Str),
3782 5 => Ok(TypeId::DateTime),
3783 6 => Ok(TypeId::Uuid),
3784 7 => Ok(TypeId::Bytes),
3785 8 => Ok(TypeId::Json),
3786 _ => Err(io::Error::new(
3787 io::ErrorKind::InvalidData,
3788 format!("unknown type id: {v}"),
3789 )),
3790 }
3791}
3792
3793#[cfg(test)]
3794mod tests {
3795 use super::*;
3796
3797 fn fail_next_catalog_persist_at(stage: u8) {
3798 CATALOG_PERSIST_FAILPOINT.with(|failpoint| failpoint.set(stage));
3799 }
3800
3801 fn temp_catalog(name: &str) -> Catalog {
3802 let dir = std::env::temp_dir().join(format!("powdb_cat_{name}_{}", std::process::id()));
3803 Catalog::create(&dir).unwrap()
3804 }
3805
3806 #[test]
3807 fn v5_reader_rejects_v6_catalog() {
3808 let dir = tempfile::tempdir().unwrap();
3809 let mut catalog = Catalog::create(dir.path()).unwrap();
3810 catalog
3811 .create_table(Schema {
3812 table_name: "Doc".into(),
3813 columns: vec![ColumnDef {
3814 name: "data".into(),
3815 type_id: TypeId::Json,
3816 required: false,
3817 position: 0,
3818 }],
3819 })
3820 .unwrap();
3821 let path =
3822 StoredJsonPathV1::new("data", vec![StoredJsonPathSegmentV1::Key("author".into())]);
3823 catalog
3824 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path, false)
3825 .unwrap();
3826 let result = read_catalog_file_with_max_version(
3827 &dir.path().join(CATALOG_FILE),
3828 LEGACY_CATALOG_VERSION,
3829 );
3830 let error = match result {
3831 Ok(_) => panic!("a v5 reader must reject v6 before decoding its payload"),
3832 Err(error) => error,
3833 };
3834 assert!(error.to_string().contains("unsupported catalog version: 6"));
3835 }
3836
3837 #[test]
3838 fn expression_index_rolls_back_only_before_catalog_rename() {
3839 let before_dir = tempfile::tempdir().unwrap();
3840 let mut before = Catalog::create(before_dir.path()).unwrap();
3841 before
3842 .create_table(Schema {
3843 table_name: "Doc".into(),
3844 columns: vec![ColumnDef {
3845 name: "data".into(),
3846 type_id: TypeId::Json,
3847 required: false,
3848 position: 0,
3849 }],
3850 })
3851 .unwrap();
3852 let path =
3853 StoredJsonPathV1::new("data", vec![StoredJsonPathSegmentV1::Key("score".into())]);
3854
3855 fail_next_catalog_persist_at(1);
3856 let error = before
3857 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path.clone(), false)
3858 .unwrap_err();
3859 assert!(error.to_string().contains("before rename"));
3860 assert_eq!(before.active_catalog_version(), LEGACY_CATALOG_VERSION);
3861 assert_eq!(before.next_index_id(), 1);
3862 assert!(before.expression_index_metadata("Doc").unwrap().is_empty());
3863 assert!(!before_dir
3864 .path()
3865 .join(expression_index_file_name("Doc", 1))
3866 .exists());
3867
3868 let before_index_id = before
3869 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path.clone(), false)
3870 .unwrap();
3871 fail_next_catalog_persist_at(1);
3872 let error = before
3873 .drop_expression_index("Doc", before_index_id)
3874 .unwrap_err();
3875 assert!(error.to_string().contains("before rename"));
3876 assert!(before
3877 .expression_index_btree("Doc", before_index_id)
3878 .is_some());
3879 assert!(before_dir
3880 .path()
3881 .join(expression_index_file_name("Doc", 1))
3882 .exists());
3883 std::mem::forget(before);
3884 let before_reopened = Catalog::open(before_dir.path()).unwrap();
3885 assert!(before_reopened
3886 .expression_index_btree("Doc", before_index_id)
3887 .is_some());
3888
3889 let after_dir = tempfile::tempdir().unwrap();
3890 let mut after = Catalog::create(after_dir.path()).unwrap();
3891 after
3892 .create_table(Schema {
3893 table_name: "Doc".into(),
3894 columns: vec![ColumnDef {
3895 name: "data".into(),
3896 type_id: TypeId::Json,
3897 required: false,
3898 position: 0,
3899 }],
3900 })
3901 .unwrap();
3902 fail_next_catalog_persist_at(2);
3903 let index_id = after
3904 .create_expression_index_metadata("Doc", 1, path.canonical_text(), path.clone(), false)
3905 .unwrap();
3906 assert_eq!(index_id, 1);
3907 assert_eq!(after.active_catalog_version(), CATALOG_VERSION);
3908 assert_eq!(after.next_index_id(), 2);
3909 assert!(after.expression_index_btree("Doc", index_id).is_some());
3910 assert!(after_dir
3911 .path()
3912 .join(expression_index_file_name("Doc", 1))
3913 .exists());
3914 std::mem::forget(after);
3915
3916 let mut reopened = Catalog::open(after_dir.path()).unwrap();
3917 assert!(reopened.expression_index_btree("Doc", index_id).is_some());
3918 fail_next_catalog_persist_at(2);
3919 reopened.drop_expression_index("Doc", index_id).unwrap();
3920 assert!(reopened
3921 .expression_index_metadata("Doc")
3922 .unwrap()
3923 .is_empty());
3924 assert!(!after_dir
3925 .path()
3926 .join(expression_index_file_name("Doc", 1))
3927 .exists());
3928 std::mem::forget(reopened);
3929
3930 let final_open = Catalog::open(after_dir.path()).unwrap();
3931 assert!(final_open
3932 .expression_index_metadata("Doc")
3933 .unwrap()
3934 .is_empty());
3935 }
3936
3937 #[test]
3938 fn ordinary_catalog_persist_reports_post_rename_directory_sync_failure() {
3939 let dir = tempfile::tempdir().unwrap();
3940 let mut catalog = Catalog::create(dir.path()).unwrap();
3941 fail_next_catalog_persist_at(2);
3942 let error = catalog
3943 .create_table(Schema {
3944 table_name: "VisibleAfterRename".into(),
3945 columns: vec![ColumnDef {
3946 name: "id".into(),
3947 type_id: TypeId::Int,
3948 required: true,
3949 position: 0,
3950 }],
3951 })
3952 .unwrap_err();
3953 assert!(error.to_string().contains("after rename"));
3954 assert!(catalog.schema("VisibleAfterRename").is_some());
3955
3956 std::mem::forget(catalog);
3957 let reopened = Catalog::open(dir.path()).unwrap();
3958 assert!(reopened.schema("VisibleAfterRename").is_some());
3959 }
3960
3961 fn schema_two_cols() -> Schema {
3962 Schema {
3963 table_name: "T".into(),
3964 columns: vec![
3965 ColumnDef {
3966 name: "id".into(),
3967 type_id: TypeId::Int,
3968 required: true,
3969 position: 0,
3970 },
3971 ColumnDef {
3972 name: "status".into(),
3973 type_id: TypeId::Str,
3974 required: false,
3975 position: 1,
3976 },
3977 ],
3978 }
3979 }
3980
3981 #[test]
3982 fn replay_records_treats_reused_tx_ids_as_ordered_spans() {
3983 let mut cat = temp_catalog("reused_tx_ids");
3984 let schema = schema_two_cols();
3985 cat.create_table(schema.clone()).unwrap();
3986 cat.checkpoint().unwrap();
3987
3988 let mut committed_row = Vec::new();
3989 encode_row_into(
3990 &schema,
3991 &[Value::Int(1), Value::Str("committed".into())],
3992 &mut committed_row,
3993 );
3994 let mut incomplete_row = Vec::new();
3995 encode_row_into(
3996 &schema,
3997 &[Value::Int(2), Value::Str("incomplete".into())],
3998 &mut incomplete_row,
3999 );
4000
4001 let records = vec![
4002 WalRecord {
4003 tx_id: 1,
4004 record_type: WalRecordType::Begin,
4005 lsn: 1,
4006 data: Vec::new(),
4007 },
4008 WalRecord {
4009 tx_id: 1,
4010 record_type: WalRecordType::Insert,
4011 lsn: 2,
4012 data: encode_wal_payload(
4013 "T",
4014 RowId {
4015 page_id: 1,
4016 slot_index: 0,
4017 },
4018 &committed_row,
4019 ),
4020 },
4021 WalRecord {
4022 tx_id: 1,
4023 record_type: WalRecordType::Commit,
4024 lsn: 3,
4025 data: Vec::new(),
4026 },
4027 WalRecord {
4028 tx_id: 1,
4029 record_type: WalRecordType::Begin,
4030 lsn: 4,
4031 data: Vec::new(),
4032 },
4033 WalRecord {
4034 tx_id: 1,
4035 record_type: WalRecordType::Insert,
4036 lsn: 5,
4037 data: encode_wal_payload(
4038 "T",
4039 RowId {
4040 page_id: 1,
4041 slot_index: 1,
4042 },
4043 &incomplete_row,
4044 ),
4045 },
4046 ];
4047
4048 cat.apply_wal_records(&records).unwrap();
4049 let rows: Vec<_> = cat.scan("T").unwrap().collect();
4050 assert_eq!(rows.len(), 1);
4051 assert_eq!(rows[0].1[0], Value::Int(1));
4052 assert_eq!(rows[0].1[1], Value::Str("committed".into()));
4053 }
4054
4055 #[test]
4056 fn ddl_create_table_codec_roundtrips_defaults_and_auto() {
4057 let schema = schema_two_cols();
4058 let defaults = vec![None, Some(Value::Str("active".into()))];
4059 let auto_cols = vec![true, false];
4060 let encoded = encode_ddl_create_table(&schema, &defaults, &auto_cols);
4061 let (decoded_schema, decoded_defaults, decoded_auto) =
4062 decode_ddl_create_table(&encoded).unwrap();
4063 assert_eq!(decoded_schema.columns.len(), 2);
4064 assert_eq!(decoded_defaults, defaults);
4065 assert_eq!(decoded_auto, auto_cols);
4066 }
4067
4068 #[test]
4069 fn ddl_create_table_codec_back_compat_without_trailing_sections() {
4070 let schema = schema_two_cols();
4074 let full = encode_ddl_create_table(&schema, &[], &[]);
4075 let legacy = &full[..full.len() - 4];
4078 let (decoded_schema, decoded_defaults, decoded_auto) =
4079 decode_ddl_create_table(legacy).unwrap();
4080 assert_eq!(decoded_schema.columns.len(), 2);
4081 assert!(decoded_defaults.is_empty(), "no defaults section -> empty");
4082 assert!(decoded_auto.is_empty(), "no auto section -> empty");
4083 }
4084
4085 #[test]
4086 fn ddl_create_table_codec_back_compat_defaults_but_no_auto() {
4087 let schema = schema_two_cols();
4090 let defaults = vec![None, Some(Value::Str("active".into()))];
4091 let full = encode_ddl_create_table(&schema, &defaults, &[]);
4092 let legacy = &full[..full.len() - 2];
4094 let (_schema, decoded_defaults, decoded_auto) = decode_ddl_create_table(legacy).unwrap();
4095 assert_eq!(decoded_defaults, defaults);
4096 assert!(decoded_auto.is_empty());
4097 }
4098
4099 #[test]
4100 fn read_catalog_file_accepts_intermediate_versions_3_and_4() {
4101 use std::io::Write as _;
4109 fn write_legacy_catalog(path: &std::path::Path, version: u16) {
4110 let mut buf: Vec<u8> = Vec::new();
4111 buf.extend_from_slice(CATALOG_MAGIC);
4112 buf.extend_from_slice(&version.to_le_bytes());
4113 buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes());
4116 buf.extend_from_slice(b"T");
4117 buf.extend_from_slice(&2u16.to_le_bytes()); buf.extend_from_slice(&2u32.to_le_bytes());
4120 buf.extend_from_slice(b"id");
4121 buf.push(TypeId::Int as u8);
4122 buf.push(1);
4123 buf.extend_from_slice(&0u16.to_le_bytes());
4124 buf.extend_from_slice(&6u32.to_le_bytes());
4126 buf.extend_from_slice(b"status");
4127 buf.push(TypeId::Str as u8);
4128 buf.push(0);
4129 buf.extend_from_slice(&1u16.to_le_bytes());
4130 buf.extend_from_slice(&0u16.to_le_bytes());
4132 if version >= 4 {
4134 encode_defaults_section(&mut buf, &[None, None]);
4135 }
4136 let crc = crc32fast::hash(&buf);
4138 buf.extend_from_slice(&crc.to_le_bytes());
4139 let mut f = fs::File::create(path).unwrap();
4140 f.write_all(&buf).unwrap();
4141 }
4142
4143 for version in [3u16, 4u16] {
4144 let path = std::env::temp_dir().join(format!(
4145 "powdb_cat_v{version}_compat_{}.bin",
4146 std::process::id()
4147 ));
4148 write_legacy_catalog(&path, version);
4149 let catalog_file = read_catalog_file(&path)
4150 .unwrap_or_else(|e| panic!("version {version} catalog must load, got: {e}"));
4151 let entries = catalog_file.entries;
4152 assert_eq!(entries.len(), 1);
4153 assert_eq!(entries[0].schema.table_name, "T");
4154 assert_eq!(entries[0].schema.columns.len(), 2);
4155 assert!(
4156 entries[0].auto_cols.is_empty(),
4157 "v{version} has no auto cols"
4158 );
4159 fs::remove_file(&path).ok();
4160 }
4161 }
4162
4163 #[test]
4164 fn read_catalog_file_rejects_implausible_table_count() {
4165 use std::io::Write as _;
4173 let mut buf: Vec<u8> = Vec::new();
4174 buf.extend_from_slice(CATALOG_MAGIC);
4175 buf.extend_from_slice(&CATALOG_VERSION.to_le_bytes());
4176 buf.extend_from_slice(&1000u32.to_le_bytes()); buf.extend_from_slice(&1u64.to_le_bytes()); let crc = crc32fast::hash(&buf);
4180 buf.extend_from_slice(&crc.to_le_bytes());
4181 let path =
4182 std::env::temp_dir().join(format!("powdb_cat_badcount_{}.bin", std::process::id()));
4183 fs::File::create(&path).unwrap().write_all(&buf).unwrap();
4184
4185 let msg = match read_catalog_file(&path) {
4186 Ok(_) => panic!("implausible table count must be rejected, got Ok"),
4187 Err(e) => e.to_string(),
4188 };
4189 assert!(
4190 msg.contains("implausible table count"),
4191 "expected an implausible-table-count error, got: {msg}"
4192 );
4193 fs::remove_file(&path).ok();
4194 }
4195
4196 #[test]
4197 fn data_dir_and_max_lsn_accessors() {
4198 let dir = std::env::temp_dir().join(format!("powdb_cat_maxlsn_{}", std::process::id()));
4199 let mut cat = Catalog::create(&dir).unwrap();
4200
4201 assert_eq!(cat.data_dir(), dir.as_path());
4203
4204 assert_eq!(cat.max_lsn(), 0);
4206
4207 let schema = Schema {
4208 table_name: "users".into(),
4209 columns: vec![ColumnDef {
4210 name: "name".into(),
4211 type_id: TypeId::Str,
4212 required: true,
4213 position: 0,
4214 }],
4215 };
4216 cat.create_table(schema).unwrap();
4217
4218 cat.insert("users", &vec![Value::Str("Alice".into())])
4219 .unwrap();
4220 cat.sync_wal().unwrap();
4221
4222 assert!(cat.max_lsn() > 0);
4225 }
4226
4227 #[test]
4228 fn test_create_table_and_insert() {
4229 let mut cat = temp_catalog("basic");
4230 let schema = Schema {
4231 table_name: "users".into(),
4232 columns: vec![
4233 ColumnDef {
4234 name: "name".into(),
4235 type_id: TypeId::Str,
4236 required: true,
4237 position: 0,
4238 },
4239 ColumnDef {
4240 name: "age".into(),
4241 type_id: TypeId::Int,
4242 required: false,
4243 position: 1,
4244 },
4245 ],
4246 };
4247 cat.create_table(schema).unwrap();
4248
4249 let row = vec![Value::Str("Alice".into()), Value::Int(30)];
4250 let rid = cat.insert("users", &row).unwrap();
4251
4252 let result = cat.get("users", rid).unwrap();
4253 assert_eq!(result[0], Value::Str("Alice".into()));
4254 assert_eq!(result[1], Value::Int(30));
4255 }
4256
4257 #[test]
4258 fn test_scan_table() {
4259 let mut cat = temp_catalog("scan");
4260 let schema = Schema {
4261 table_name: "items".into(),
4262 columns: vec![
4263 ColumnDef {
4264 name: "name".into(),
4265 type_id: TypeId::Str,
4266 required: true,
4267 position: 0,
4268 },
4269 ColumnDef {
4270 name: "price".into(),
4271 type_id: TypeId::Float,
4272 required: true,
4273 position: 1,
4274 },
4275 ],
4276 };
4277 cat.create_table(schema).unwrap();
4278
4279 for i in 0..50 {
4280 cat.insert(
4281 "items",
4282 &vec![
4283 Value::Str(format!("item_{i}")),
4284 Value::Float(i as f64 * 1.5),
4285 ],
4286 )
4287 .unwrap();
4288 }
4289
4290 let rows: Vec<_> = cat.scan("items").unwrap().collect();
4291 assert_eq!(rows.len(), 50);
4292 }
4293
4294 #[test]
4295 fn test_index_lookup() {
4296 let mut cat = temp_catalog("idx");
4297 let schema = Schema {
4298 table_name: "users".into(),
4299 columns: vec![
4300 ColumnDef {
4301 name: "email".into(),
4302 type_id: TypeId::Str,
4303 required: true,
4304 position: 0,
4305 },
4306 ColumnDef {
4307 name: "name".into(),
4308 type_id: TypeId::Str,
4309 required: true,
4310 position: 1,
4311 },
4312 ],
4313 };
4314 cat.create_table(schema).unwrap();
4315 cat.create_index("users", "email").unwrap();
4316
4317 cat.insert(
4318 "users",
4319 &vec![
4320 Value::Str("alice@example.com".into()),
4321 Value::Str("Alice".into()),
4322 ],
4323 )
4324 .unwrap();
4325 cat.insert(
4326 "users",
4327 &vec![
4328 Value::Str("bob@example.com".into()),
4329 Value::Str("Bob".into()),
4330 ],
4331 )
4332 .unwrap();
4333
4334 let result = cat
4335 .index_lookup("users", "email", &Value::Str("bob@example.com".into()))
4336 .unwrap();
4337 assert!(result.is_some());
4338 let row = result.unwrap();
4339 assert_eq!(row[1], Value::Str("Bob".into()));
4340 }
4341
4342 #[test]
4343 fn test_delete_row() {
4344 let mut cat = temp_catalog("delete");
4345 let schema = Schema {
4346 table_name: "t".into(),
4347 columns: vec![ColumnDef {
4348 name: "v".into(),
4349 type_id: TypeId::Int,
4350 required: true,
4351 position: 0,
4352 }],
4353 };
4354 cat.create_table(schema).unwrap();
4355 let r1 = cat.insert("t", &vec![Value::Int(1)]).unwrap();
4356 let r2 = cat.insert("t", &vec![Value::Int(2)]).unwrap();
4357 cat.delete("t", r1).unwrap();
4358 assert!(cat.get("t", r1).is_none());
4359 assert!(cat.get("t", r2).is_some());
4360 }
4361
4362 #[test]
4363 fn test_update_row() {
4364 let mut cat = temp_catalog("update");
4365 let schema = Schema {
4366 table_name: "t".into(),
4367 columns: vec![ColumnDef {
4368 name: "v".into(),
4369 type_id: TypeId::Int,
4370 required: true,
4371 position: 0,
4372 }],
4373 };
4374 cat.create_table(schema).unwrap();
4375 let rid = cat.insert("t", &vec![Value::Int(1)]).unwrap();
4376 let new_rid = cat.update("t", rid, &vec![Value::Int(99)]).unwrap();
4377 let row = cat.get("t", new_rid).unwrap();
4378 assert_eq!(row[0], Value::Int(99));
4379 }
4380
4381 #[test]
4382 fn test_persist_and_reopen() {
4383 let dir = std::env::temp_dir().join(format!("powdb_cat_persist_{}", std::process::id()));
4384 let _ = std::fs::remove_dir_all(&dir);
4386
4387 {
4388 let mut cat = Catalog::create(&dir).unwrap();
4389 cat.create_table(Schema {
4390 table_name: "users".into(),
4391 columns: vec![
4392 ColumnDef {
4393 name: "name".into(),
4394 type_id: TypeId::Str,
4395 required: true,
4396 position: 0,
4397 },
4398 ColumnDef {
4399 name: "age".into(),
4400 type_id: TypeId::Int,
4401 required: false,
4402 position: 1,
4403 },
4404 ],
4405 })
4406 .unwrap();
4407 cat.insert("users", &vec![Value::Str("Alice".into()), Value::Int(30)])
4408 .unwrap();
4409 cat.insert("users", &vec![Value::Str("Bob".into()), Value::Int(25)])
4410 .unwrap();
4411 }
4412
4413 let cat = Catalog::open(&dir).unwrap();
4415 let schema = cat.schema("users").unwrap();
4416 assert_eq!(schema.columns.len(), 2);
4417 assert_eq!(schema.columns[0].name, "name");
4418 assert_eq!(schema.columns[0].type_id, TypeId::Str);
4419 assert_eq!(schema.columns[1].type_id, TypeId::Int);
4420
4421 let rows: Vec<_> = cat.scan("users").unwrap().collect();
4422 assert_eq!(rows.len(), 2);
4423
4424 std::fs::remove_dir_all(&dir).ok();
4425 }
4426
4427 #[test]
4428 fn test_open_missing_dir_errors() {
4429 let dir = std::env::temp_dir().join(format!("powdb_cat_missing_{}", std::process::id()));
4430 let _ = std::fs::remove_dir_all(&dir);
4431 std::fs::create_dir_all(&dir).unwrap();
4432 assert!(Catalog::open(&dir).is_err());
4434 std::fs::remove_dir_all(&dir).ok();
4435 }
4436
4437 #[test]
4438 fn test_list_tables() {
4439 let mut cat = temp_catalog("list");
4440 cat.create_table(Schema {
4441 table_name: "a".into(),
4442 columns: vec![ColumnDef {
4443 name: "x".into(),
4444 type_id: TypeId::Int,
4445 required: true,
4446 position: 0,
4447 }],
4448 })
4449 .unwrap();
4450 cat.create_table(Schema {
4451 table_name: "b".into(),
4452 columns: vec![ColumnDef {
4453 name: "y".into(),
4454 type_id: TypeId::Int,
4455 required: true,
4456 position: 0,
4457 }],
4458 })
4459 .unwrap();
4460 let mut tables = cat.list_tables();
4461 tables.sort();
4462 assert_eq!(tables, vec!["a", "b"]);
4463 }
4464
4465 #[test]
4466 fn test_path_traversal_table_name_rejected() {
4467 let mut cat = temp_catalog("path_trav");
4468 let bad_names = vec![
4470 "../etc/passwd",
4471 "foo/bar",
4472 "table\0name",
4473 "",
4474 "123starts_with_digit",
4475 "has-dashes",
4476 "has spaces",
4477 "has.dots",
4478 ];
4479 for name in bad_names {
4480 let schema = Schema {
4481 table_name: name.into(),
4482 columns: vec![ColumnDef {
4483 name: "x".into(),
4484 type_id: TypeId::Int,
4485 required: true,
4486 position: 0,
4487 }],
4488 };
4489 let result = cat.create_table(schema);
4490 assert!(result.is_err(), "expected error for table name '{name}'");
4491 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
4492 }
4493 let good_names = vec!["users", "_private", "Table_123", "_"];
4495 for name in good_names {
4496 let schema = Schema {
4497 table_name: name.into(),
4498 columns: vec![ColumnDef {
4499 name: "x".into(),
4500 type_id: TypeId::Int,
4501 required: true,
4502 position: 0,
4503 }],
4504 };
4505 assert!(
4506 cat.create_table(schema).is_ok(),
4507 "expected ok for table name '{name}'"
4508 );
4509 }
4510 }
4511
4512 #[test]
4513 fn test_path_traversal_column_name_rejected() {
4514 let mut cat = temp_catalog("col_path_trav");
4515 let schema = Schema {
4516 table_name: "valid_table".into(),
4517 columns: vec![ColumnDef {
4518 name: "../bad".into(),
4519 type_id: TypeId::Int,
4520 required: true,
4521 position: 0,
4522 }],
4523 };
4524 let result = cat.create_table(schema);
4525 assert!(result.is_err());
4526 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
4527 }
4528
4529 #[test]
4530 fn test_drop_table_validates_name() {
4531 let mut cat = temp_catalog("drop_trav");
4532 let result = cat.drop_table("../etc/passwd");
4533 assert!(result.is_err());
4534 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
4536 }
4537}