1use crate::row::encode_row_into;
2use crate::table::Table;
3use crate::types::*;
4use crate::wal::{Wal, WalRecord, WalRecordType, WalSyncMode};
5use rustc_hash::FxHashMap;
6use std::fs;
7use std::io::{self, Read, Write};
8use std::path::{Path, PathBuf};
9use tracing::{info, warn};
10
11fn check_encoded_row_size(encoded: &[u8]) -> io::Result<()> {
16 if encoded.len() > crate::page::MAX_ROW_DATA_SIZE {
17 return Err(crate::error::StorageError::RowTooLarge {
18 size: encoded.len(),
19 max: crate::page::MAX_ROW_DATA_SIZE,
20 }
21 .into());
22 }
23 Ok(())
24}
25
26fn validate_identifier(kind: &str, name: &str) -> io::Result<()> {
30 if name.is_empty() {
31 return Err(io::Error::new(
32 io::ErrorKind::InvalidInput,
33 format!("invalid {kind} name: must not be empty"),
34 ));
35 }
36 let mut chars = name.chars();
37 let first = chars.next().expect("non-empty name");
39 if !first.is_ascii_alphabetic() && first != '_' {
40 return Err(io::Error::new(
41 io::ErrorKind::InvalidInput,
42 format!("invalid {kind} name '{name}': must start with a letter or underscore"),
43 ));
44 }
45 for ch in chars {
46 if !ch.is_ascii_alphanumeric() && ch != '_' {
47 return Err(io::Error::new(
48 io::ErrorKind::InvalidInput,
49 format!(
50 "invalid {kind} name '{name}': must contain only letters, digits, and underscores"
51 ),
52 ));
53 }
54 }
55 Ok(())
56}
57
58fn validate_table_name(name: &str) -> io::Result<()> {
60 validate_identifier("table", name)
61}
62
63fn validate_column_name(name: &str) -> io::Result<()> {
65 validate_identifier("column", name)
66}
67
68const CATALOG_FILE: &str = "catalog.bin";
77pub const CATALOG_LSN_FILE: &str = "catalog.lsn";
78const CATALOG_MAGIC: &[u8; 4] = b"BCAT";
79pub const CATALOG_VERSION: u16 = 5;
83
84const WAL_FILE: &str = "wal.log";
87const SYNC_STATE_DIR: &str = ".powdb-sync";
88const SYNC_IDENTITY_FILE: &str = "identity.json";
89
90const WAL_BATCH_SIZE: usize = 64;
94type WalArchiveCallback<'a> = &'a mut dyn FnMut(&Path, &[WalRecord]) -> io::Result<()>;
95
96fn read_durable_lsn(data_dir: &Path) -> io::Result<u64> {
97 let path = data_dir.join(CATALOG_LSN_FILE);
98 let bytes = match fs::read(path) {
99 Ok(bytes) => bytes,
100 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(0),
101 Err(err) => return Err(err),
102 };
103 if bytes.len() != 8 {
104 return Err(io::Error::new(
105 io::ErrorKind::InvalidData,
106 "catalog LSN sidecar has invalid length",
107 ));
108 }
109 let mut buf = [0u8; 8];
110 buf.copy_from_slice(&bytes);
111 Ok(u64::from_le_bytes(buf))
112}
113
114fn write_durable_lsn(data_dir: &Path, lsn: u64) -> io::Result<()> {
115 let path = data_dir.join(CATALOG_LSN_FILE);
116 let tmp_path = data_dir.join(format!("{CATALOG_LSN_FILE}.tmp"));
117 let mut file = fs::File::create(&tmp_path)?;
118 file.write_all(&lsn.to_le_bytes())?;
119 file.sync_all()?;
120 drop(file);
121 fs::rename(&tmp_path, &path)?;
122 sync_directory(data_dir)?;
123 Ok(())
124}
125
126#[cfg(unix)]
127fn sync_directory(path: &Path) -> io::Result<()> {
128 fs::File::open(path)?.sync_all()
129}
130
131#[cfg(not(unix))]
132fn sync_directory(path: &Path) -> io::Result<()> {
133 let _ = path;
134 Ok(())
135}
136
137fn max_record_lsn(records: &[WalRecord]) -> Option<u64> {
138 records.iter().map(|record| record.lsn).max()
139}
140
141pub struct Catalog {
155 tables: Vec<Table>,
158 name_to_slot: FxHashMap<String, usize>,
161 data_dir: PathBuf,
162 wal: Wal,
167 next_tx_id: u64,
171 active_tx_id: Option<u64>,
174 tx_start_len: Option<u64>,
177 pending_autocommit_tx_ids: Vec<u64>,
180 checkpointed: bool,
184 durable_lsn: u64,
187}
188
189impl Catalog {
190 pub fn create(data_dir: &Path) -> io::Result<Self> {
211 crate::create_data_dir_secure(data_dir)?;
212 let wal_path = data_dir.join(WAL_FILE);
213 let wal = Wal::create(&wal_path, WAL_BATCH_SIZE)?;
214 let cat = Catalog {
215 tables: Vec::new(),
216 name_to_slot: FxHashMap::default(),
217 data_dir: data_dir.to_path_buf(),
218 wal,
219 next_tx_id: 1,
220 active_tx_id: None,
221 tx_start_len: None,
222 pending_autocommit_tx_ids: Vec::new(),
223 checkpointed: false,
224 durable_lsn: 0,
225 };
226 cat.persist()?;
227 Ok(cat)
228 }
229
230 pub fn open(data_dir: &Path) -> io::Result<Self> {
239 Self::open_inner(data_dir, None)
240 }
241
242 pub fn open_with_wal_archive<F>(data_dir: &Path, mut archive: F) -> io::Result<Self>
251 where
252 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
253 {
254 let archive: WalArchiveCallback<'_> = &mut archive;
255 Self::open_inner(data_dir, Some(archive))
256 }
257
258 fn open_inner(data_dir: &Path, archive: Option<WalArchiveCallback<'_>>) -> io::Result<Self> {
259 let cat_path = data_dir.join(CATALOG_FILE);
260 if !cat_path.exists() {
261 return Err(io::Error::new(io::ErrorKind::NotFound, "no catalog file"));
262 }
263 let entries = read_catalog_file(&cat_path)?;
264 let durable_lsn = read_durable_lsn(data_dir)?;
265 let mut tables: Vec<Table> = Vec::with_capacity(entries.len());
266 let mut name_to_slot =
267 FxHashMap::with_capacity_and_hasher(entries.len(), Default::default());
268 for CatalogEntry {
269 schema,
270 indexed_cols,
271 defaults,
272 auto_cols,
273 } in entries
274 {
275 let name = schema.table_name.clone();
276 let mut table = Table::open_with_indexes(schema, data_dir, &indexed_cols)?;
282 table.set_defaults(defaults);
283 table.set_auto_cols(auto_cols);
284 name_to_slot.insert(name, tables.len());
285 tables.push(table);
286 }
287 let wal_path = data_dir.join(WAL_FILE);
288 let wal = Wal::open(&wal_path, WAL_BATCH_SIZE)?;
289 let mut cat = Catalog {
290 tables,
291 name_to_slot,
292 data_dir: data_dir.to_path_buf(),
293 wal,
294 next_tx_id: 1,
295 active_tx_id: None,
296 tx_start_len: None,
297 pending_autocommit_tx_ids: Vec::new(),
298 checkpointed: false,
299 durable_lsn,
300 };
301 cat.replay_wal(archive)?;
302 let max_page_lsn = cat
312 .tables
313 .iter()
314 .map(|t| t.heap.max_page_lsn())
315 .max()
316 .unwrap_or(0);
317 let max_known_lsn = max_page_lsn.max(cat.durable_lsn);
318 cat.wal.set_next_lsn_at_least(max_known_lsn + 1);
319 Ok(cat)
320 }
321
322 fn replay_wal(&mut self, mut archive: Option<WalArchiveCallback<'_>>) -> io::Result<()> {
348 let records = self.wal.read_all()?;
349 if records.is_empty() {
350 return Ok(());
351 }
352 if archive.is_none() {
353 self.ensure_plain_wal_truncate_allowed(&records)?;
354 }
355 self.replay_records(&records)?;
356 if let Some(archive) = archive.as_mut() {
357 archive(&self.data_dir, &records)?;
358 }
359 self.wal.truncate()?;
360 Ok(())
361 }
362
363 pub fn apply_wal_records(&mut self, records: &[WalRecord]) -> io::Result<()> {
372 self.ensure_no_active_transaction_for_checkpoint()?;
373 self.ensure_no_pending_wal_records()?;
374 self.replay_records(records)
375 }
376
377 pub fn ensure_no_pending_wal_records(&self) -> io::Result<()> {
380 if self.wal.has_pending() || !self.wal.read_all()?.is_empty() {
381 return Err(io::Error::other(
382 "cannot apply replicated WAL records while local WAL records are pending",
383 ));
384 }
385 Ok(())
386 }
387
388 fn replay_records(&mut self, records: &[WalRecord]) -> io::Result<()> {
389 if records.is_empty() {
390 return Ok(());
391 }
392
393 info!(count = records.len(), "applying WAL records");
394
395 let has_boundaries = records.iter().any(|rec| {
404 matches!(
405 rec.record_type,
406 WalRecordType::Begin | WalRecordType::Commit | WalRecordType::Rollback
407 )
408 });
409 let mut committed_row_records = vec![true; records.len()];
410 if has_boundaries {
411 committed_row_records.fill(false);
412 let mut pending_tx_spans: Vec<(u64, Vec<usize>)> = Vec::new();
413 for (index, rec) in records.iter().enumerate() {
414 match rec.record_type {
415 WalRecordType::Insert | WalRecordType::Update | WalRecordType::Delete
416 if rec.tx_id == 0 =>
417 {
418 committed_row_records[index] = true;
419 }
420 WalRecordType::Insert | WalRecordType::Update | WalRecordType::Delete => {
421 if let Some((_, rows)) = pending_tx_spans
422 .iter_mut()
423 .rev()
424 .find(|(tx_id, _)| *tx_id == rec.tx_id)
425 {
426 rows.push(index);
427 } else {
428 pending_tx_spans.push((rec.tx_id, vec![index]));
429 }
430 }
431 WalRecordType::Begin if rec.tx_id != 0 => {
432 pending_tx_spans.push((rec.tx_id, Vec::new()));
433 }
434 WalRecordType::Commit if rec.tx_id != 0 => {
435 if let Some(span_index) = pending_tx_spans
436 .iter()
437 .rposition(|(tx_id, _)| *tx_id == rec.tx_id)
438 {
439 let (_, rows) = pending_tx_spans.remove(span_index);
440 for row_index in rows {
441 committed_row_records[row_index] = true;
442 }
443 }
444 }
445 WalRecordType::Rollback if rec.tx_id != 0 => {
446 if let Some(span_index) = pending_tx_spans
447 .iter()
448 .rposition(|(tx_id, _)| *tx_id == rec.tx_id)
449 {
450 pending_tx_spans.remove(span_index);
451 }
452 }
453 _ => {}
454 }
455 }
456 }
457
458 let mut replayed_inserts = 0usize;
459 let mut replayed_updates = 0usize;
460 let mut replayed_deletes = 0usize;
461 let mut skipped = 0usize;
462 let mut skipped_uncommitted = 0usize;
463 let mut saw_ddl = false;
464 for (index, rec) in records.iter().enumerate() {
465 if has_boundaries
466 && !committed_row_records[index]
467 && matches!(
468 rec.record_type,
469 WalRecordType::Insert | WalRecordType::Update | WalRecordType::Delete
470 )
471 {
472 skipped_uncommitted += 1;
473 continue;
474 }
475 match rec.record_type {
476 WalRecordType::Insert => {
477 if let Some((table_name, rid, row_bytes)) = decode_wal_payload(&rec.data) {
478 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
479 let tbl = &mut self.tables[slot];
480 if rec.lsn > 0 && tbl.heap.page_lsn(rid.page_id) >= rec.lsn {
484 skipped += 1;
485 continue;
486 }
487 tbl.heap.insert_at(rid, &row_bytes)?;
494 tbl.heap.set_page_lsn(rid.page_id, rec.lsn)?;
495 replayed_inserts += 1;
496 }
497 }
498 }
499 WalRecordType::Update => {
500 if let Some((table_name, rid, row_bytes)) = decode_wal_payload(&rec.data) {
501 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
502 let tbl = &mut self.tables[slot];
503 if rec.lsn > 0 && tbl.heap.page_lsn(rid.page_id) >= rec.lsn {
504 skipped += 1;
505 continue;
506 }
507 let new_rid = tbl.heap.update(rid, &row_bytes)?;
508 tbl.heap.set_page_lsn(new_rid.page_id, rec.lsn)?;
509 replayed_updates += 1;
510 }
511 }
512 }
513 WalRecordType::Delete => {
514 if let Some((table_name, rid, _)) = decode_wal_payload(&rec.data) {
515 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
516 let tbl = &mut self.tables[slot];
517 if rec.lsn > 0 && tbl.heap.page_lsn(rid.page_id) >= rec.lsn {
518 skipped += 1;
519 continue;
520 }
521 let _ = tbl.heap.delete(rid);
522 tbl.heap.set_page_lsn(rid.page_id, rec.lsn)?;
523 replayed_deletes += 1;
524 }
525 }
526 }
527 WalRecordType::Begin | WalRecordType::Commit | WalRecordType::Rollback => {
528 }
530 WalRecordType::DdlCreateTable => {
531 saw_ddl = true;
532 if let Some((schema, defaults, auto_cols)) = decode_ddl_create_table(&rec.data)
533 {
534 if !self.name_to_slot.contains_key(&schema.table_name) {
535 if let Ok(mut table) = Table::create(schema, &self.data_dir) {
536 table.set_defaults(defaults);
537 table.set_auto_cols(auto_cols);
538 let slot = self.tables.len();
539 let name = table.schema.table_name.clone();
540 self.tables.push(table);
541 self.name_to_slot.insert(name, slot);
542 }
543 }
544 }
545 }
546 WalRecordType::DdlDropTable => {
547 saw_ddl = true;
548 if let Some((table_name, _)) = decode_ddl_table_name(&rec.data) {
549 if let Some(&slot) = self.name_to_slot.get(&table_name) {
550 let heap_path = self.data_dir.join(format!("{table_name}.heap"));
551 if heap_path.exists() {
552 let _ = fs::remove_file(&heap_path);
553 }
554 for col_name in self.tables[slot].indexed_column_names() {
555 let idx_path =
556 self.data_dir.join(format!("{table_name}_{col_name}.idx"));
557 if idx_path.exists() {
558 let _ = fs::remove_file(&idx_path);
559 }
560 }
561 self.name_to_slot.remove(&table_name);
562 let last = self.tables.len() - 1;
563 if slot != last {
564 let moved_name = self.tables[last].schema.table_name.clone();
565 self.tables.swap(slot, last);
566 self.name_to_slot.insert(moved_name, slot);
567 }
568 self.tables.pop();
569 }
570 }
571 }
572 WalRecordType::DdlAddColumn => {
573 saw_ddl = true;
574 if let Some((table_name, col)) = decode_ddl_alter_add_column(&rec.data) {
575 if let Some(&slot) = self.name_to_slot.get(&table_name) {
576 let tbl = &mut self.tables[slot];
577 if !tbl.schema.columns.iter().any(|c| c.name == col.name) {
578 let old_schema = tbl.schema.clone();
579 let has_rows = tbl.heap.scan().next().is_some();
580 tbl.schema.columns.push(col);
581 tbl.refresh_layout();
582 if has_rows {
583 let fill = vec![Value::Empty; tbl.schema.columns.len()];
584 let data_dir = self.data_dir.clone();
585 let _ = tbl.rewrite_rows_for_schema_change(
586 &old_schema,
587 &fill,
588 &data_dir,
589 );
590 }
591 }
592 if rec.lsn > 0 {
599 let _ = tbl.heap.stamp_all_pages_min_lsn(rec.lsn);
600 }
601 }
602 }
603 }
604 WalRecordType::DdlDropColumn => {
605 saw_ddl = true;
606 if let Some((table_name, col_name)) = decode_ddl_alter_drop_column(&rec.data) {
607 if let Some(&slot) = self.name_to_slot.get(&table_name) {
608 let tbl = &mut self.tables[slot];
609 if let Some(idx) =
610 tbl.schema.columns.iter().position(|c| c.name == col_name)
611 {
612 let old_schema = tbl.schema.clone();
613 let has_rows = tbl.heap.scan().next().is_some();
614 tbl.schema.columns.remove(idx);
615 for (i, c) in tbl.schema.columns.iter_mut().enumerate() {
616 c.position = i as u16;
617 }
618 tbl.refresh_layout();
619 if has_rows {
620 let fill = vec![Value::Empty; tbl.schema.columns.len()];
621 let data_dir = self.data_dir.clone();
622 let _ = tbl.rewrite_rows_for_schema_change(
623 &old_schema,
624 &fill,
625 &data_dir,
626 );
627 }
628 }
629 if rec.lsn > 0 {
630 let _ = tbl.heap.stamp_all_pages_min_lsn(rec.lsn);
631 }
632 }
633 }
634 }
635 }
636 }
637 info!(
638 inserts = replayed_inserts,
639 updates = replayed_updates,
640 deletes = replayed_deletes,
641 skipped = skipped,
642 skipped_uncommitted = skipped_uncommitted,
643 "WAL record apply complete (commit-boundary + LSN idempotent)"
644 );
645 if saw_ddl {
646 self.persist()?;
647 }
648 for tbl in &mut self.tables {
662 tbl.heap.flush_all_dirty()?;
663 tbl.heap.flush()?;
664 tbl.rebuild_indexes_from_heap()?;
665 tbl.save_dirty_indexes()?;
670 }
671 if let Some(max_lsn) = max_record_lsn(records) {
672 self.record_durable_lsn_at_least(max_lsn)?;
673 self.wal.set_next_lsn_at_least(max_lsn.saturating_add(1));
674 }
675 Ok(())
676 }
677
678 pub fn checkpoint(&mut self) -> io::Result<()> {
687 self.ensure_no_active_transaction_for_checkpoint()?;
688 self.ensure_plain_checkpoint_allowed_before_flush()?;
689 self.flush_checkpoint_state()?;
690 self.wal.flush()?;
691 self.record_durable_lsn_at_least(self.wal.last_appended_lsn())?;
692 self.wal.truncate()?;
693 self.checkpointed = true;
694 Ok(())
695 }
696
697 pub fn checkpoint_with_wal_archive<F>(&mut self, mut archive: F) -> io::Result<()>
705 where
706 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
707 {
708 self.ensure_no_active_transaction_for_checkpoint()?;
709 self.commit_autocommit()?;
710 self.flush_checkpoint_state()?;
711 self.wal.flush()?;
712 let records = self.wal.read_all()?;
713 let archive: WalArchiveCallback<'_> = &mut archive;
714 archive(&self.data_dir, &records)?;
715 if let Some(max_lsn) = max_record_lsn(&records) {
716 self.record_durable_lsn_at_least(max_lsn)?;
717 } else {
718 self.record_durable_lsn_at_least(self.wal.last_appended_lsn())?;
719 }
720 self.wal.truncate()?;
721 self.checkpointed = true;
722 Ok(())
723 }
724
725 fn ensure_no_active_transaction_for_checkpoint(&self) -> io::Result<()> {
726 if self.active_tx_id.is_some() {
727 return Err(io::Error::other(
728 "cannot checkpoint while an explicit transaction is active",
729 ));
730 }
731 Ok(())
732 }
733
734 fn flush_checkpoint_state(&mut self) -> io::Result<()> {
735 for tbl in &mut self.tables {
736 tbl.heap.flush_all_dirty()?;
737 tbl.heap.flush()?;
738 tbl.save_dirty_indexes()?;
743 }
744 Ok(())
745 }
746
747 fn ensure_plain_checkpoint_allowed_before_flush(&self) -> io::Result<()> {
748 if !self.sync_identity_file_exists() {
749 return Ok(());
750 }
751 if self.wal.has_pending() {
752 return Err(io::Error::other(
753 "sync identity exists but checkpoint/recovery was called without a WAL archive hook; refusing to truncate retained history",
754 ));
755 }
756 let records = self.wal.read_all()?;
757 self.ensure_plain_wal_truncate_allowed(&records)
758 }
759
760 fn ensure_plain_wal_truncate_allowed(&self, records: &[WalRecord]) -> io::Result<()> {
761 if records.is_empty() {
762 return Ok(());
763 }
764 if self.sync_identity_file_exists() {
765 return Err(io::Error::other(
766 "sync identity exists but checkpoint/recovery was called without a WAL archive hook; refusing to truncate retained history",
767 ));
768 }
769 Ok(())
770 }
771
772 fn sync_identity_file_exists(&self) -> bool {
773 self.data_dir
774 .join(SYNC_STATE_DIR)
775 .join(SYNC_IDENTITY_FILE)
776 .exists()
777 }
778
779 fn record_durable_lsn_at_least(&mut self, lsn: u64) -> io::Result<()> {
780 if lsn <= self.durable_lsn {
781 return Ok(());
782 }
783 self.durable_lsn = lsn;
784 write_durable_lsn(&self.data_dir, lsn)
785 }
786
787 #[inline]
789 fn next_tx(&mut self) -> u64 {
790 if let Some(id) = self.active_tx_id {
791 return id;
792 }
793 let id = self.next_tx_id;
794 self.next_tx_id = self.next_tx_id.wrapping_add(1);
795 id
796 }
797
798 pub fn begin_transaction(&mut self) -> io::Result<()> {
800 if self.active_tx_id.is_some() {
801 return Err(io::Error::new(
802 io::ErrorKind::InvalidInput,
803 "explicit transaction is already active",
804 ));
805 }
806 let start_len = self.wal.synced_len()?;
807 let id = self.next_tx_id;
808 self.next_tx_id = self.next_tx_id.wrapping_add(1);
809 self.active_tx_id = Some(id);
810 self.tx_start_len = Some(start_len);
811 self.pending_autocommit_tx_ids.clear();
812 if !self.wal.is_off() {
813 self.wal.append(id, WalRecordType::Begin, &[])?;
814 self.wal.flush()?;
815 }
816 Ok(())
817 }
818
819 pub fn commit_transaction(&mut self) -> io::Result<()> {
822 if let Some(id) = self.active_tx_id.take() {
823 if !self.wal.is_off() {
824 self.wal.append(id, WalRecordType::Commit, &[])?;
825 self.wal.flush()?;
826 }
827 }
828 self.tx_start_len = None;
829 Ok(())
830 }
831
832 pub fn commit_autocommit(&mut self) -> io::Result<()> {
836 if !self.wal.is_off() && !self.pending_autocommit_tx_ids.is_empty() {
837 self.pending_autocommit_tx_ids.sort_unstable();
838 self.pending_autocommit_tx_ids.dedup();
839 for id in self.pending_autocommit_tx_ids.drain(..) {
840 self.wal.append(id, WalRecordType::Commit, &[])?;
841 }
842 }
843 self.wal.flush()
844 }
845
846 fn wal_log(
861 &mut self,
862 tx_id: u64,
863 record_type: WalRecordType,
864 table: &str,
865 rid: RowId,
866 row_bytes: &[u8],
867 ) -> io::Result<()> {
868 if self.wal.is_off() {
875 return Ok(());
876 }
877 let payload = encode_wal_payload(table, rid, row_bytes);
878 self.wal.append(tx_id, record_type, &payload)?;
879 if self.active_tx_id.is_none() {
880 self.pending_autocommit_tx_ids.push(tx_id);
881 }
882 Ok(())
883 }
884
885 #[inline]
891 pub fn sync_wal(&mut self) -> io::Result<()> {
892 self.wal.flush()
893 }
894
895 pub fn set_wal_sync_mode(&mut self, mode: WalSyncMode) {
903 self.wal.set_sync_mode(mode);
904 }
905
906 pub fn rollback_to_last_sync(&mut self) -> io::Result<()> {
921 self.rollback_to_last_sync_inner(None)
922 }
923
924 pub fn rollback_to_last_sync_with_wal_archive<F>(&mut self, mut archive: F) -> io::Result<()>
929 where
930 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
931 {
932 let archive: WalArchiveCallback<'_> = &mut archive;
933 self.rollback_to_last_sync_inner(Some(archive))
934 }
935
936 fn rollback_to_last_sync_inner(
937 &mut self,
938 mut archive: Option<WalArchiveCallback<'_>>,
939 ) -> io::Result<()> {
940 let start_len = self.tx_start_len.unwrap_or(0);
941 let prearchived = if let Some(archive) = archive.as_mut() {
942 let records = self.wal.read_through_len(start_len)?;
943 if !records.is_empty() {
944 archive(&self.data_dir, &records)?;
945 }
946 true
947 } else {
948 false
949 };
950
951 let start_len = self.tx_start_len.take().unwrap_or(0);
952 if let Some(id) = self.active_tx_id.take() {
953 if !self.wal.is_off() {
954 let _ = self.wal.append(id, WalRecordType::Rollback, &[]);
955 }
956 }
957 self.wal.discard_and_truncate_to(start_len)?;
958
959 for tbl in &mut self.tables {
962 tbl.heap.discard_dirty();
963 }
964 self.wal.discard_pending()?;
970 let data_dir = self.data_dir.clone();
974 let sync_mode = self.wal.sync_mode();
975 let restored = if prearchived {
976 let mut already_archived = |_dir: &Path, _records: &[WalRecord]| Ok(());
977 let archive: WalArchiveCallback<'_> = &mut already_archived;
978 Self::open_inner(&data_dir, Some(archive))?
979 } else {
980 Self::open_inner(&data_dir, archive)?
981 };
982 *self = restored;
983 self.wal.set_sync_mode(sync_mode);
984 Ok(())
985 }
986
987 fn abandon_active_transaction_for_drop(&mut self) -> io::Result<()> {
988 for tbl in &mut self.tables {
989 tbl.heap.discard_dirty();
990 }
991 self.pending_autocommit_tx_ids.clear();
992 let truncate_result = match self.tx_start_len.take() {
993 Some(start_len) => self.wal.discard_and_truncate_to(start_len),
994 None => self.wal.discard_pending(),
995 };
996 self.active_tx_id = None;
997 truncate_result
998 }
999
1000 pub fn data_dir(&self) -> &Path {
1002 &self.data_dir
1003 }
1004
1005 pub fn max_lsn(&self) -> u64 {
1010 let max_page_lsn = self
1011 .tables
1012 .iter()
1013 .map(|t| t.heap.max_page_lsn())
1014 .max()
1015 .unwrap_or(0);
1016 max_page_lsn
1017 .max(self.durable_lsn)
1018 .max(self.wal.last_appended_lsn())
1019 }
1020
1021 pub fn create_table(&mut self, schema: Schema) -> io::Result<()> {
1022 self.create_table_full(schema, Vec::new(), Vec::new())
1023 }
1024
1025 pub fn create_table_with_defaults(
1029 &mut self,
1030 schema: Schema,
1031 defaults: Vec<Option<Value>>,
1032 ) -> io::Result<()> {
1033 self.create_table_full(schema, defaults, Vec::new())
1034 }
1035
1036 pub fn create_table_full(
1041 &mut self,
1042 schema: Schema,
1043 defaults: Vec<Option<Value>>,
1044 auto_cols: Vec<bool>,
1045 ) -> io::Result<()> {
1046 validate_table_name(&schema.table_name)?;
1047 for col in &schema.columns {
1048 validate_column_name(&col.name)?;
1049 }
1050 let name = schema.table_name.clone();
1051 if self.name_to_slot.contains_key(&name) {
1052 return Err(io::Error::new(
1053 io::ErrorKind::AlreadyExists,
1054 format!("table '{name}' already exists"),
1055 ));
1056 }
1057 if !self.wal.is_off() {
1058 let payload = encode_ddl_create_table(&schema, &defaults, &auto_cols);
1059 self.wal
1060 .append(0, WalRecordType::DdlCreateTable, &payload)?;
1061 self.wal.flush()?;
1062 }
1063 let mut table = Table::create(schema, &self.data_dir)?;
1064 table.set_defaults(defaults);
1065 table.set_auto_cols(auto_cols);
1066 let slot = self.tables.len();
1067 self.tables.push(table);
1068 self.name_to_slot.insert(name, slot);
1069 self.persist()?;
1070 Ok(())
1071 }
1072
1073 pub fn column_defaults(&self, table: &str) -> Option<&[Option<Value>]> {
1077 let slot = *self.name_to_slot.get(table)?;
1078 Some(self.tables[slot].defaults())
1079 }
1080
1081 pub fn auto_columns(&self, table: &str) -> Option<&[bool]> {
1084 let slot = *self.name_to_slot.get(table)?;
1085 Some(self.tables[slot].auto_cols())
1086 }
1087
1088 pub fn assign_auto_columns(&mut self, table: &str, values: &mut [Value]) {
1092 if let Some(&slot) = self.name_to_slot.get(table) {
1093 self.tables[slot].assign_auto(values);
1094 }
1095 }
1096
1097 fn persist(&self) -> io::Result<()> {
1102 let cat_path = self.data_dir.join(CATALOG_FILE);
1103 let tmp_path = self.data_dir.join(format!("{CATALOG_FILE}.tmp"));
1104 let entries: Vec<CatalogEntryRef<'_>> = self
1105 .tables
1106 .iter()
1107 .map(|t| CatalogEntryRef {
1108 schema: &t.schema,
1109 indexed_cols: t.indexed_column_metas(),
1110 defaults: t.defaults(),
1111 auto_cols: t.auto_cols(),
1112 })
1113 .collect();
1114 write_catalog_file(&tmp_path, &entries)?;
1115 fs::rename(&tmp_path, &cat_path)?;
1116 Ok(())
1117 }
1118
1119 #[inline]
1123 pub fn table_slot(&self, name: &str) -> Option<usize> {
1124 self.name_to_slot.get(name).copied()
1125 }
1126
1127 #[inline]
1130 pub fn table_by_slot(&self, slot: usize) -> &Table {
1131 &self.tables[slot]
1132 }
1133
1134 #[inline]
1136 pub fn table_by_slot_mut(&mut self, slot: usize) -> &mut Table {
1137 &mut self.tables[slot]
1138 }
1139
1140 pub fn get_table(&self, name: &str) -> Option<&Table> {
1141 let slot = *self.name_to_slot.get(name)?;
1142 Some(&self.tables[slot])
1143 }
1144
1145 pub fn get_table_mut(&mut self, name: &str) -> Option<&mut Table> {
1146 let slot = *self.name_to_slot.get(name)?;
1147 Some(&mut self.tables[slot])
1148 }
1149
1150 #[inline]
1155 fn by_name(&self, table: &str) -> io::Result<&Table> {
1156 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1157 io::Error::new(
1158 io::ErrorKind::NotFound,
1159 format!("table '{table}' not found"),
1160 )
1161 })?;
1162 Ok(&self.tables[slot])
1163 }
1164
1165 #[inline]
1167 fn by_name_mut(&mut self, table: &str) -> io::Result<&mut Table> {
1168 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1169 io::Error::new(
1170 io::ErrorKind::NotFound,
1171 format!("table '{table}' not found"),
1172 )
1173 })?;
1174 Ok(&mut self.tables[slot])
1175 }
1176
1177 pub fn insert(&mut self, table: &str, values: &Row) -> io::Result<RowId> {
1178 if self.wal.is_off() {
1187 return self.by_name_mut(table)?.insert(values);
1188 }
1189 let tbl = self.by_name_mut(table)?;
1190 let mut wal_bytes: Vec<u8> = Vec::new();
1191 encode_row_into(&tbl.schema, values, &mut wal_bytes);
1192 let new_rid = tbl.insert(values)?;
1206 let tx_id = self.next_tx();
1207 self.wal_log(tx_id, WalRecordType::Insert, table, new_rid, &wal_bytes)?;
1208 let lsn = self.wal.last_appended_lsn();
1213 if lsn > 0 {
1214 self.by_name_mut(table)?
1215 .heap
1216 .set_page_lsn(new_rid.page_id, lsn)?;
1217 }
1218 Ok(new_rid)
1219 }
1220
1221 pub fn insert_by_slot(&mut self, slot: usize, values: &Row) -> io::Result<RowId> {
1229 if self.wal.is_off() {
1230 return self.tables[slot].insert(values);
1231 }
1232 let tx_id = self.next_tx();
1233 let autocommit = self.active_tx_id.is_none();
1234 let Catalog { tables, wal, .. } = self;
1235 let tbl = &mut tables[slot];
1236 let mut wal_bytes: Vec<u8> = Vec::new();
1237 encode_row_into(&tbl.schema, values, &mut wal_bytes);
1238 let new_rid = tbl.insert(values)?;
1241 let payload = encode_wal_payload(&tbl.schema.table_name, new_rid, &wal_bytes);
1242 wal.append(tx_id, WalRecordType::Insert, &payload)?;
1243 if autocommit {
1244 self.pending_autocommit_tx_ids.push(tx_id);
1245 }
1246 let lsn = wal.last_appended_lsn();
1247 if lsn > 0 {
1248 tbl.heap.set_page_lsn(new_rid.page_id, lsn)?;
1249 }
1250 Ok(new_rid)
1251 }
1252
1253 pub fn get(&self, table: &str, rid: RowId) -> Option<Row> {
1254 self.get_table(table)?.get(rid)
1255 }
1256
1257 pub fn delete(&mut self, table: &str, rid: RowId) -> io::Result<()> {
1258 if self.wal.is_off() {
1261 return self.by_name_mut(table)?.delete(rid);
1262 }
1263 let tx_id = self.next_tx();
1264 self.wal_log(tx_id, WalRecordType::Delete, table, rid, &[])?;
1266 self.by_name_mut(table)?.delete(rid)
1267 }
1268
1269 pub fn delete_many(&mut self, table: &str, rids: &[RowId]) -> io::Result<u64> {
1273 if self.wal.is_off() {
1281 return self.by_name_mut(table)?.delete_many(rids);
1282 }
1283 let tx_id = self.next_tx();
1284 for &rid in rids {
1285 let payload = encode_wal_payload(table, rid, &[]);
1286 self.wal.append(tx_id, WalRecordType::Delete, &payload)?;
1287 }
1288 if self.active_tx_id.is_none() && !rids.is_empty() {
1289 self.pending_autocommit_tx_ids.push(tx_id);
1290 }
1291 self.by_name_mut(table)?.delete_many(rids)
1292 }
1293
1294 pub fn scan_delete_matching<P>(&mut self, table: &str, pred: P) -> io::Result<u64>
1304 where
1305 P: FnMut(&[u8]) -> bool,
1306 {
1307 self.by_name_mut(table)?.scan_delete_matching(pred)
1308 }
1309
1310 pub fn scan_delete_matching_logged<P>(&mut self, table: &str, pred: P) -> io::Result<u64>
1321 where
1322 P: FnMut(&[u8]) -> bool,
1323 {
1324 if self.wal.is_off() {
1330 return self.by_name_mut(table)?.scan_delete_matching(pred);
1331 }
1332 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1336 io::Error::new(
1337 io::ErrorKind::NotFound,
1338 format!("table '{table}' not found"),
1339 )
1340 })?;
1341 let tx_id = self.next_tx();
1342 let autocommit = self.active_tx_id.is_none();
1343 let Catalog { tables, wal, .. } = self;
1346 let tbl = &mut tables[slot];
1347 let name_bytes = table.as_bytes();
1351 let count = tbl.scan_delete_matching_with_hook(pred, |rid, row_bytes| {
1352 let mut payload: Vec<u8> =
1353 Vec::with_capacity(4 + name_bytes.len() + 10 + row_bytes.len());
1354 payload.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
1355 payload.extend_from_slice(name_bytes);
1356 payload.extend_from_slice(&rid.page_id.to_le_bytes());
1357 payload.extend_from_slice(&rid.slot_index.to_le_bytes());
1358 payload.extend_from_slice(&0u32.to_le_bytes());
1362 let _ = wal.append(tx_id, WalRecordType::Delete, &payload);
1369 })?;
1370 if autocommit && count > 0 {
1371 self.pending_autocommit_tx_ids.push(tx_id);
1372 }
1373 Ok(count)
1375 }
1376
1377 pub fn scan_patch_matching_logged<P, M>(
1385 &mut self,
1386 table: &str,
1387 pred: P,
1388 try_mutate: M,
1389 ) -> io::Result<(u64, Vec<RowId>)>
1390 where
1391 P: FnMut(&[u8]) -> bool,
1392 M: FnMut(&mut [u8]) -> Option<u16>,
1393 {
1394 if self.wal.is_off() {
1395 return self.by_name_mut(table)?.scan_patch_matching_with_hook(
1396 pred,
1397 try_mutate,
1398 |_, _| {},
1399 );
1400 }
1401 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1402 io::Error::new(
1403 io::ErrorKind::NotFound,
1404 format!("table '{table}' not found"),
1405 )
1406 })?;
1407 let tx_id = self.next_tx();
1408 let autocommit = self.active_tx_id.is_none();
1409 let Catalog { tables, wal, .. } = self;
1410 let tbl = &mut tables[slot];
1411 let name_bytes = table.as_bytes();
1412 let result = tbl.scan_patch_matching_with_hook(pred, try_mutate, |rid, row_bytes| {
1413 let mut payload: Vec<u8> =
1414 Vec::with_capacity(4 + name_bytes.len() + 10 + row_bytes.len());
1415 payload.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
1416 payload.extend_from_slice(name_bytes);
1417 payload.extend_from_slice(&rid.page_id.to_le_bytes());
1418 payload.extend_from_slice(&rid.slot_index.to_le_bytes());
1419 payload.extend_from_slice(&(row_bytes.len() as u32).to_le_bytes());
1420 payload.extend_from_slice(row_bytes);
1421 let _ = wal.append(tx_id, WalRecordType::Update, &payload);
1422 })?;
1423 if autocommit && result.0 > 0 {
1424 self.pending_autocommit_tx_ids.push(tx_id);
1425 }
1426 Ok(result)
1427 }
1428
1429 pub fn update(&mut self, table: &str, rid: RowId, values: &Row) -> io::Result<RowId> {
1430 if self.wal.is_off() {
1433 return self.by_name_mut(table)?.update(rid, values);
1434 }
1435 let tbl = self.by_name_mut(table)?;
1436 let mut wal_bytes: Vec<u8> = Vec::new();
1437 encode_row_into(&tbl.schema, values, &mut wal_bytes);
1438 check_encoded_row_size(&wal_bytes)?;
1441 let tx_id = self.next_tx();
1442 self.wal_log(tx_id, WalRecordType::Update, table, rid, &wal_bytes)?;
1443 self.by_name_mut(table)?.update(rid, values)
1444 }
1445
1446 pub fn update_hinted(
1450 &mut self,
1451 table: &str,
1452 rid: RowId,
1453 values: &Row,
1454 changed_col_indices: Option<&[usize]>,
1455 ) -> io::Result<RowId> {
1456 if self.wal.is_off() {
1460 return self
1461 .by_name_mut(table)?
1462 .update_hinted(rid, values, changed_col_indices);
1463 }
1464 let tbl = self.by_name_mut(table)?;
1465 let mut wal_bytes: Vec<u8> = Vec::new();
1466 encode_row_into(&tbl.schema, values, &mut wal_bytes);
1467 check_encoded_row_size(&wal_bytes)?;
1469 let tx_id = self.next_tx();
1470 self.wal_log(tx_id, WalRecordType::Update, table, rid, &wal_bytes)?;
1471 self.by_name_mut(table)?
1472 .update_hinted(rid, values, changed_col_indices)
1473 }
1474
1475 #[inline]
1487 pub fn with_row_bytes_mut<F>(&mut self, table: &str, rid: RowId, f: F) -> io::Result<bool>
1488 where
1489 F: FnOnce(&mut [u8]),
1490 {
1491 self.by_name_mut(table)?.with_row_bytes_mut(rid, f)
1492 }
1493
1494 #[inline]
1508 pub fn update_row_bytes_logged<F>(&mut self, table: &str, rid: RowId, f: F) -> io::Result<bool>
1509 where
1510 F: FnOnce(&mut [u8]),
1511 {
1512 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1513 io::Error::new(
1514 io::ErrorKind::NotFound,
1515 format!("table '{table}' not found"),
1516 )
1517 })?;
1518 self.update_row_bytes_logged_by_slot(slot, rid, f)
1519 }
1520
1521 #[inline]
1526 pub fn update_row_bytes_logged_by_slot<F>(
1527 &mut self,
1528 slot: usize,
1529 rid: RowId,
1530 f: F,
1531 ) -> io::Result<bool>
1532 where
1533 F: FnOnce(&mut [u8]),
1534 {
1535 let tbl = &mut self.tables[slot];
1538 let ok = tbl.with_row_bytes_mut(rid, f)?;
1539 if !ok {
1540 return Ok(false);
1541 }
1542 if self.wal.is_off() {
1546 return Ok(true);
1547 }
1548 let new_bytes = match tbl.heap.get(rid) {
1551 Some(b) => b,
1552 None => return Ok(false),
1554 };
1555 let table_name = tbl.schema.table_name.clone();
1558 let tx_id = self.next_tx();
1559 self.wal_log(tx_id, WalRecordType::Update, &table_name, rid, &new_bytes)?;
1560 Ok(true)
1561 }
1562
1563 #[inline]
1575 pub fn patch_var_col_in_place(
1576 &mut self,
1577 table: &str,
1578 rid: RowId,
1579 col_idx: usize,
1580 new_value: Option<&[u8]>,
1581 ) -> io::Result<bool> {
1582 self.by_name_mut(table)?
1583 .patch_var_col_in_place(rid, col_idx, new_value)
1584 }
1585
1586 pub fn patch_var_col_logged(
1592 &mut self,
1593 table: &str,
1594 rid: RowId,
1595 col_idx: usize,
1596 new_value: Option<&[u8]>,
1597 ) -> io::Result<bool> {
1598 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1599 io::Error::new(
1600 io::ErrorKind::NotFound,
1601 format!("table '{table}' not found"),
1602 )
1603 })?;
1604 let tbl = &mut self.tables[slot];
1605 let ok = tbl.patch_var_col_in_place(rid, col_idx, new_value)?;
1606 if !ok {
1607 return Ok(false);
1608 }
1609 if self.wal.is_off() {
1612 return Ok(true);
1613 }
1614 let new_bytes = match tbl.heap.get(rid) {
1615 Some(b) => b,
1616 None => return Ok(false),
1617 };
1618 let table_name = tbl.schema.table_name.clone();
1619 let tx_id = self.next_tx();
1620 self.wal_log(tx_id, WalRecordType::Update, &table_name, rid, &new_bytes)?;
1621 Ok(true)
1622 }
1623
1624 pub fn scan(&self, table: &str) -> io::Result<impl Iterator<Item = (RowId, Row)> + '_> {
1625 Ok(self.by_name(table)?.scan())
1626 }
1627
1628 pub fn for_each_row_raw<F>(&self, table: &str, f: F) -> io::Result<()>
1631 where
1632 F: FnMut(RowId, &[u8]),
1633 {
1634 self.by_name(table)?.for_each_row_raw(f);
1635 Ok(())
1636 }
1637
1638 pub fn try_for_each_row_raw<F>(&self, table: &str, f: F) -> io::Result<()>
1643 where
1644 F: FnMut(RowId, &[u8]) -> std::ops::ControlFlow<()>,
1645 {
1646 self.by_name(table)?.try_for_each_row_raw(f);
1647 Ok(())
1648 }
1649
1650 pub fn create_index(&mut self, table: &str, column: &str) -> io::Result<()> {
1651 self.create_index_unique(table, column, false)
1652 }
1653
1654 pub fn create_index_unique(
1659 &mut self,
1660 table: &str,
1661 column: &str,
1662 unique: bool,
1663 ) -> io::Result<()> {
1664 let data_dir = self.data_dir.clone();
1665 self.by_name_mut(table)?
1666 .create_index_with_unique(column, &data_dir, unique)?;
1667 self.persist()
1671 }
1672
1673 pub fn is_index_unique(&self, table: &str, column: &str) -> Option<bool> {
1677 self.get_table(table)?.is_index_unique(column)
1678 }
1679
1680 pub fn has_index(&self, table: &str, column: &str) -> bool {
1682 self.get_table(table)
1683 .map(|t| t.has_index(column))
1684 .unwrap_or(false)
1685 }
1686
1687 pub fn index_lookup(&self, table: &str, column: &str, key: &Value) -> io::Result<Option<Row>> {
1688 Ok(self
1689 .by_name(table)?
1690 .index_lookup(column, key)
1691 .map(|(_, row)| row))
1692 }
1693
1694 pub fn list_tables(&self) -> Vec<&str> {
1695 self.tables
1699 .iter()
1700 .map(|t| t.schema.table_name.as_str())
1701 .collect()
1702 }
1703
1704 pub fn schema(&self, table: &str) -> Option<&Schema> {
1705 let slot = *self.name_to_slot.get(table)?;
1706 Some(&self.tables[slot].schema)
1707 }
1708
1709 pub fn drop_table(&mut self, name: &str) -> io::Result<()> {
1712 validate_table_name(name)?;
1713 let slot = *self.name_to_slot.get(name).ok_or_else(|| {
1714 io::Error::new(io::ErrorKind::NotFound, format!("table '{name}' not found"))
1715 })?;
1716 if !self.wal.is_off() {
1717 let payload = encode_ddl_drop_table(name);
1718 self.wal.append(0, WalRecordType::DdlDropTable, &payload)?;
1719 self.wal.flush()?;
1720 }
1721 let table = &self.tables[slot];
1723 let heap_path = self
1724 .data_dir
1725 .join(format!("{}.heap", table.schema.table_name));
1726 if heap_path.exists() {
1727 fs::remove_file(&heap_path)?;
1728 }
1729 for col_name in table.indexed_column_names() {
1735 let idx_path = self.data_dir.join(format!("{name}_{col_name}.idx"));
1736 if idx_path.exists() {
1737 let _ = fs::remove_file(&idx_path);
1738 }
1739 }
1740 self.name_to_slot.remove(name);
1742 let last = self.tables.len() - 1;
1743 if slot != last {
1744 let moved_name = self.tables[last].schema.table_name.clone();
1745 self.tables.swap(slot, last);
1746 self.name_to_slot.insert(moved_name, slot);
1747 }
1748 self.tables.pop();
1749 self.persist()?;
1750 Ok(())
1751 }
1752
1753 pub fn alter_table_add_column(&mut self, table: &str, col: ColumnDef) -> io::Result<()> {
1776 let data_dir = self.data_dir.clone();
1777 {
1778 let tbl = self.by_name_mut(table)?;
1779 if tbl.schema.columns.iter().any(|c| c.name == col.name) {
1780 return Err(io::Error::new(
1781 io::ErrorKind::AlreadyExists,
1782 format!("column '{}' already exists in table '{table}'", col.name),
1783 ));
1784 }
1785 }
1786 let barrier_lsn = if !self.wal.is_off() {
1787 let payload = encode_ddl_alter_add_column(table, &col);
1788 self.wal.append(0, WalRecordType::DdlAddColumn, &payload)?;
1789 self.wal.flush()?;
1790 self.wal.last_appended_lsn()
1791 } else {
1792 0
1793 };
1794 let tbl = self.by_name_mut(table)?;
1795
1796 let old_schema = tbl.schema.clone();
1797
1798 let has_rows = tbl.heap.scan().next().is_some();
1802
1803 if has_rows && col.required {
1804 return Err(io::Error::new(
1805 io::ErrorKind::InvalidInput,
1806 format!(
1807 "cannot add required column '{}' to non-empty table '{table}': \
1808 no default value to backfill existing rows with",
1809 col.name
1810 ),
1811 ));
1812 }
1813
1814 tbl.schema.columns.push(col);
1818 tbl.refresh_layout();
1819
1820 if has_rows {
1821 let fill: Vec<Value> = vec![Value::Empty; tbl.schema.columns.len()];
1826 tbl.rewrite_rows_for_schema_change(&old_schema, &fill, &data_dir)?;
1827 }
1828 if barrier_lsn > 0 {
1836 tbl.heap.stamp_all_pages_min_lsn(barrier_lsn)?;
1837 tbl.heap.flush()?;
1838 }
1839
1840 self.persist()?;
1841 Ok(())
1842 }
1843
1844 pub fn alter_table_drop_column(&mut self, table: &str, col_name: &str) -> io::Result<()> {
1867 let data_dir = self.data_dir.clone();
1868 {
1869 let tbl = self.by_name_mut(table)?;
1870 tbl.schema
1871 .columns
1872 .iter()
1873 .position(|c| c.name == col_name)
1874 .ok_or_else(|| {
1875 io::Error::new(
1876 io::ErrorKind::NotFound,
1877 format!("column '{col_name}' not found in table '{table}'"),
1878 )
1879 })?;
1880 }
1881 let barrier_lsn = if !self.wal.is_off() {
1882 let payload = encode_ddl_alter_drop_column(table, col_name);
1883 self.wal.append(0, WalRecordType::DdlDropColumn, &payload)?;
1884 self.wal.flush()?;
1885 self.wal.last_appended_lsn()
1886 } else {
1887 0
1888 };
1889 let tbl = self.by_name_mut(table)?;
1890 let idx = tbl
1891 .schema
1892 .columns
1893 .iter()
1894 .position(|c| c.name == col_name)
1895 .ok_or_else(|| {
1896 io::Error::new(
1897 io::ErrorKind::NotFound,
1898 format!("column '{col_name}' not found in table '{table}'"),
1899 )
1900 })?;
1901
1902 let old_schema = tbl.schema.clone();
1904 let has_rows = tbl.heap.scan().next().is_some();
1905
1906 tbl.schema.columns.remove(idx);
1908 for (i, col) in tbl.schema.columns.iter_mut().enumerate() {
1909 col.position = i as u16;
1910 }
1911 tbl.refresh_layout();
1912
1913 if has_rows {
1914 let fill: Vec<Value> = vec![Value::Empty; tbl.schema.columns.len()];
1920 tbl.rewrite_rows_for_schema_change(&old_schema, &fill, &data_dir)?;
1921 }
1922 if barrier_lsn > 0 {
1924 tbl.heap.stamp_all_pages_min_lsn(barrier_lsn)?;
1925 tbl.heap.flush()?;
1926 }
1927
1928 self.persist()?;
1929 Ok(())
1930 }
1931}
1932
1933impl Drop for Catalog {
1934 fn drop(&mut self) {
1935 if self.active_tx_id.is_some() {
1936 if let Err(e) = self.abandon_active_transaction_for_drop() {
1937 warn!(error = %e, "catalog drop active transaction cleanup failed");
1938 }
1939 return;
1940 }
1941 if let Err(e) = self.checkpoint() {
1953 warn!(error = %e, "catalog drop checkpoint failed");
1954 }
1955 }
1956}
1957
1958fn encode_wal_payload(table: &str, rid: RowId, row_bytes: &[u8]) -> Vec<u8> {
1973 let name = table.as_bytes();
1974 let mut out = Vec::with_capacity(4 + name.len() + 4 + 2 + 4 + row_bytes.len());
1975 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
1976 out.extend_from_slice(name);
1977 out.extend_from_slice(&rid.page_id.to_le_bytes());
1978 out.extend_from_slice(&rid.slot_index.to_le_bytes());
1979 out.extend_from_slice(&(row_bytes.len() as u32).to_le_bytes());
1980 out.extend_from_slice(row_bytes);
1981 out
1982}
1983
1984fn decode_wal_payload(data: &[u8]) -> Option<(String, RowId, Vec<u8>)> {
1985 let mut pos = 0usize;
1986 if data.len() < 4 {
1987 return None;
1988 }
1989 let name_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
1990 pos += 4;
1991 if pos + name_len > data.len() {
1992 return None;
1993 }
1994 let name = std::str::from_utf8(&data[pos..pos + name_len])
1995 .ok()?
1996 .to_string();
1997 pos += name_len;
1998 if pos + 4 + 2 + 4 > data.len() {
1999 return None;
2000 }
2001 let page_id = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
2002 pos += 4;
2003 let slot_index = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
2004 pos += 2;
2005 let row_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2006 pos += 4;
2007 if pos + row_len > data.len() {
2008 return None;
2009 }
2010 let row_bytes = data[pos..pos + row_len].to_vec();
2011 Some((
2012 name,
2013 RowId {
2014 page_id,
2015 slot_index,
2016 },
2017 row_bytes,
2018 ))
2019}
2020
2021fn encode_ddl_create_table(
2024 schema: &Schema,
2025 defaults: &[Option<Value>],
2026 auto_cols: &[bool],
2027) -> Vec<u8> {
2028 let name = schema.table_name.as_bytes();
2029 let mut out = Vec::new();
2030 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
2031 out.extend_from_slice(name);
2032 out.extend_from_slice(&(schema.columns.len() as u16).to_le_bytes());
2033 for col in &schema.columns {
2034 let cn = col.name.as_bytes();
2035 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
2036 out.extend_from_slice(cn);
2037 out.push(col.type_id as u8);
2038 out.push(col.required as u8);
2039 out.extend_from_slice(&col.position.to_le_bytes());
2040 }
2041 encode_defaults_section(&mut out, defaults);
2045 encode_auto_section(&mut out, auto_cols);
2046 out
2047}
2048
2049fn decode_ddl_create_table(data: &[u8]) -> Option<(Schema, Vec<Option<Value>>, Vec<bool>)> {
2050 let mut pos = 0usize;
2051 if data.len() < 4 {
2052 return None;
2053 }
2054 let name_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2055 pos += 4;
2056 if pos + name_len > data.len() {
2057 return None;
2058 }
2059 let table_name = std::str::from_utf8(&data[pos..pos + name_len])
2060 .ok()?
2061 .to_string();
2062 pos += name_len;
2063 if pos + 2 > data.len() {
2064 return None;
2065 }
2066 let n_cols = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
2067 pos += 2;
2068 let mut columns = Vec::with_capacity(n_cols);
2069 for _ in 0..n_cols {
2070 if pos + 4 > data.len() {
2071 return None;
2072 }
2073 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2074 pos += 4;
2075 if pos + cn_len + 4 > data.len() {
2076 return None;
2077 }
2078 let col_name = std::str::from_utf8(&data[pos..pos + cn_len])
2079 .ok()?
2080 .to_string();
2081 pos += cn_len;
2082 let type_id = TypeId::from_u8(data[pos])?;
2083 pos += 1;
2084 let required = data[pos] != 0;
2085 pos += 1;
2086 if pos + 2 > data.len() {
2087 return None;
2088 }
2089 let position = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
2090 pos += 2;
2091 columns.push(ColumnDef {
2092 name: col_name,
2093 type_id,
2094 required,
2095 position,
2096 });
2097 }
2098 let defaults = if pos < data.len() {
2101 decode_defaults_section(data, &mut pos, columns.len())?
2102 } else {
2103 Vec::new()
2104 };
2105 let auto_cols = if pos < data.len() {
2106 decode_auto_section(data, &mut pos, columns.len())?
2107 } else {
2108 Vec::new()
2109 };
2110 Some((
2111 Schema {
2112 table_name,
2113 columns,
2114 },
2115 defaults,
2116 auto_cols,
2117 ))
2118}
2119
2120fn encode_ddl_drop_table(table_name: &str) -> Vec<u8> {
2121 let name = table_name.as_bytes();
2122 let mut out = Vec::with_capacity(4 + name.len());
2123 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
2124 out.extend_from_slice(name);
2125 out
2126}
2127
2128fn encode_ddl_alter_add_column(table_name: &str, col: &ColumnDef) -> Vec<u8> {
2129 let name = table_name.as_bytes();
2130 let cn = col.name.as_bytes();
2131 let mut out = Vec::with_capacity(4 + name.len() + 4 + cn.len() + 4);
2132 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
2133 out.extend_from_slice(name);
2134 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
2135 out.extend_from_slice(cn);
2136 out.push(col.type_id as u8);
2137 out.push(col.required as u8);
2138 out.extend_from_slice(&col.position.to_le_bytes());
2139 out
2140}
2141
2142fn encode_ddl_alter_drop_column(table_name: &str, col_name: &str) -> Vec<u8> {
2143 let name = table_name.as_bytes();
2144 let cn = col_name.as_bytes();
2145 let mut out = Vec::with_capacity(4 + name.len() + 4 + cn.len());
2146 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
2147 out.extend_from_slice(name);
2148 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
2149 out.extend_from_slice(cn);
2150 out
2151}
2152
2153fn decode_ddl_table_name(data: &[u8]) -> Option<(String, usize)> {
2154 if data.len() < 4 {
2155 return None;
2156 }
2157 let name_len = u32::from_le_bytes(data[0..4].try_into().ok()?) as usize;
2158 if 4 + name_len > data.len() {
2159 return None;
2160 }
2161 let name = std::str::from_utf8(&data[4..4 + name_len])
2162 .ok()?
2163 .to_string();
2164 Some((name, 4 + name_len))
2165}
2166
2167fn decode_ddl_alter_add_column(data: &[u8]) -> Option<(String, ColumnDef)> {
2168 let (table_name, mut pos) = decode_ddl_table_name(data)?;
2169 if pos + 4 > data.len() {
2170 return None;
2171 }
2172 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2173 pos += 4;
2174 if pos + cn_len + 4 > data.len() {
2175 return None;
2176 }
2177 let col_name = std::str::from_utf8(&data[pos..pos + cn_len])
2178 .ok()?
2179 .to_string();
2180 pos += cn_len;
2181 let type_id = TypeId::from_u8(data[pos])?;
2182 pos += 1;
2183 let required = data[pos] != 0;
2184 pos += 1;
2185 if pos + 2 > data.len() {
2186 return None;
2187 }
2188 let position = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
2189 Some((
2190 table_name,
2191 ColumnDef {
2192 name: col_name,
2193 type_id,
2194 required,
2195 position,
2196 },
2197 ))
2198}
2199
2200fn decode_ddl_alter_drop_column(data: &[u8]) -> Option<(String, String)> {
2201 let (table_name, pos) = decode_ddl_table_name(data)?;
2202 if pos + 4 > data.len() {
2203 return None;
2204 }
2205 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2206 if pos + 4 + cn_len > data.len() {
2207 return None;
2208 }
2209 let col_name = std::str::from_utf8(&data[pos + 4..pos + 4 + cn_len])
2210 .ok()?
2211 .to_string();
2212 Some((table_name, col_name))
2213}
2214
2215pub(crate) struct IndexedColMeta {
2243 pub name: String,
2244 pub unique: bool,
2245}
2246
2247pub(crate) struct CatalogEntry {
2250 pub schema: Schema,
2251 pub indexed_cols: Vec<IndexedColMeta>,
2252 pub defaults: Vec<Option<Value>>,
2255 pub auto_cols: Vec<bool>,
2258}
2259
2260pub(crate) struct CatalogEntryRef<'a> {
2262 pub schema: &'a Schema,
2263 pub indexed_cols: Vec<IndexedColMeta>,
2264 pub defaults: &'a [Option<Value>],
2265 pub auto_cols: &'a [bool],
2266}
2267
2268fn encode_value_blob(out: &mut Vec<u8>, v: &Value) {
2274 out.push(v.type_id() as u8);
2275 match v {
2276 Value::Int(n) => out.extend_from_slice(&n.to_le_bytes()),
2277 Value::Float(f) => out.extend_from_slice(&f.to_bits().to_le_bytes()),
2278 Value::Bool(b) => out.push(*b as u8),
2279 Value::Str(s) => {
2280 out.extend_from_slice(&(s.len() as u32).to_le_bytes());
2281 out.extend_from_slice(s.as_bytes());
2282 }
2283 Value::DateTime(n) => out.extend_from_slice(&n.to_le_bytes()),
2284 Value::Uuid(u) => out.extend_from_slice(u),
2285 Value::Bytes(b) => {
2286 out.extend_from_slice(&(b.len() as u32).to_le_bytes());
2287 out.extend_from_slice(b);
2288 }
2289 Value::Empty => {}
2290 }
2291}
2292
2293fn decode_value_blob(data: &[u8], pos: &mut usize) -> Option<Value> {
2296 let tag = *data.get(*pos)?;
2297 *pos += 1;
2298 let type_id = TypeId::from_u8(tag)?;
2299 let take_fixed = |pos: &mut usize, n: usize| -> Option<Vec<u8>> {
2300 if *pos + n > data.len() {
2301 return None;
2302 }
2303 let slice = data[*pos..*pos + n].to_vec();
2304 *pos += n;
2305 Some(slice)
2306 };
2307 match type_id {
2308 TypeId::Empty => Some(Value::Empty),
2309 TypeId::Int => Some(Value::Int(i64::from_le_bytes(
2310 take_fixed(pos, 8)?.try_into().ok()?,
2311 ))),
2312 TypeId::Float => Some(Value::Float(f64::from_bits(u64::from_le_bytes(
2313 take_fixed(pos, 8)?.try_into().ok()?,
2314 )))),
2315 TypeId::Bool => Some(Value::Bool(take_fixed(pos, 1)?[0] != 0)),
2316 TypeId::DateTime => Some(Value::DateTime(i64::from_le_bytes(
2317 take_fixed(pos, 8)?.try_into().ok()?,
2318 ))),
2319 TypeId::Uuid => Some(Value::Uuid(take_fixed(pos, 16)?.try_into().ok()?)),
2320 TypeId::Str => {
2321 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
2322 Some(Value::Str(String::from_utf8(take_fixed(pos, len)?).ok()?))
2323 }
2324 TypeId::Bytes => {
2325 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
2326 Some(Value::Bytes(take_fixed(pos, len)?))
2327 }
2328 }
2329}
2330
2331fn encode_defaults_section(out: &mut Vec<u8>, defaults: &[Option<Value>]) {
2335 let present: Vec<(u16, &Value)> = defaults
2336 .iter()
2337 .enumerate()
2338 .filter_map(|(i, d)| d.as_ref().map(|v| (i as u16, v)))
2339 .collect();
2340 out.extend_from_slice(&(present.len() as u16).to_le_bytes());
2341 for (pos, v) in present {
2342 out.extend_from_slice(&pos.to_le_bytes());
2343 encode_value_blob(out, v);
2344 }
2345}
2346
2347fn decode_defaults_section(
2350 data: &[u8],
2351 pos: &mut usize,
2352 n_cols: usize,
2353) -> Option<Vec<Option<Value>>> {
2354 if *pos + 2 > data.len() {
2355 return None;
2356 }
2357 let count = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
2358 *pos += 2;
2359 let mut out = vec![None; n_cols];
2360 for _ in 0..count {
2361 if *pos + 2 > data.len() {
2362 return None;
2363 }
2364 let col = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
2365 *pos += 2;
2366 let value = decode_value_blob(data, pos)?;
2367 if col < n_cols {
2368 out[col] = Some(value);
2369 }
2370 }
2371 Some(out)
2372}
2373
2374fn encode_auto_section(out: &mut Vec<u8>, auto_cols: &[bool]) {
2378 let present: Vec<u16> = auto_cols
2379 .iter()
2380 .enumerate()
2381 .filter_map(|(i, &a)| if a { Some(i as u16) } else { None })
2382 .collect();
2383 out.extend_from_slice(&(present.len() as u16).to_le_bytes());
2384 for pos in present {
2385 out.extend_from_slice(&pos.to_le_bytes());
2386 }
2387}
2388
2389fn decode_auto_section(data: &[u8], pos: &mut usize, n_cols: usize) -> Option<Vec<bool>> {
2392 if *pos + 2 > data.len() {
2393 return None;
2394 }
2395 let count = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
2396 *pos += 2;
2397 let mut out = vec![false; n_cols];
2398 for _ in 0..count {
2399 if *pos + 2 > data.len() {
2400 return None;
2401 }
2402 let col = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
2403 *pos += 2;
2404 if col < n_cols {
2405 out[col] = true;
2406 }
2407 }
2408 Some(out)
2409}
2410
2411fn write_catalog_file(path: &Path, entries: &[CatalogEntryRef<'_>]) -> io::Result<()> {
2412 let mut buf: Vec<u8> = Vec::with_capacity(64);
2413 buf.extend_from_slice(CATALOG_MAGIC);
2414 buf.extend_from_slice(&CATALOG_VERSION.to_le_bytes());
2415 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
2416
2417 for entry in entries {
2418 let schema = entry.schema;
2419 let name = schema.table_name.as_bytes();
2420 buf.extend_from_slice(&(name.len() as u32).to_le_bytes());
2421 buf.extend_from_slice(name);
2422 buf.extend_from_slice(&(schema.columns.len() as u16).to_le_bytes());
2423 for col in &schema.columns {
2424 let cn = col.name.as_bytes();
2425 buf.extend_from_slice(&(cn.len() as u32).to_le_bytes());
2426 buf.extend_from_slice(cn);
2427 buf.push(col.type_id as u8);
2428 buf.push(if col.required { 1 } else { 0 });
2429 buf.extend_from_slice(&col.position.to_le_bytes());
2430 }
2431 buf.extend_from_slice(&(entry.indexed_cols.len() as u16).to_le_bytes());
2433 for meta in &entry.indexed_cols {
2434 let cn = meta.name.as_bytes();
2435 buf.extend_from_slice(&(cn.len() as u32).to_le_bytes());
2436 buf.extend_from_slice(cn);
2437 buf.push(if meta.unique { 1 } else { 0 });
2438 }
2439 encode_defaults_section(&mut buf, entry.defaults);
2441 encode_auto_section(&mut buf, entry.auto_cols);
2443 }
2444
2445 let crc = crc32fast::hash(&buf);
2449 buf.extend_from_slice(&crc.to_le_bytes());
2450
2451 let mut f = fs::OpenOptions::new()
2452 .create(true)
2453 .write(true)
2454 .truncate(true)
2455 .open(path)?;
2456 f.write_all(&buf)?;
2457 f.sync_data()?;
2458 Ok(())
2459}
2460
2461fn read_catalog_file(path: &Path) -> io::Result<Vec<CatalogEntry>> {
2462 let mut f = fs::File::open(path)?;
2463 let mut buf = Vec::new();
2464 f.read_to_end(&mut buf)?;
2465
2466 let mut pos = 0usize;
2467 if buf.len() < 14 || &buf[0..4] != CATALOG_MAGIC {
2469 return Err(io::Error::new(
2470 io::ErrorKind::InvalidData,
2471 "bad catalog magic",
2472 ));
2473 }
2474
2475 let payload = &buf[..buf.len() - 4];
2477 let stored_crc = u32::from_le_bytes(
2478 buf[buf.len() - 4..]
2479 .try_into()
2480 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog CRC"))?,
2481 );
2482 let computed_crc = crc32fast::hash(payload);
2483 if stored_crc != computed_crc {
2484 return Err(io::Error::new(
2485 io::ErrorKind::InvalidData,
2486 format!(
2487 "catalog CRC32 mismatch: expected {stored_crc:#010x}, got {computed_crc:#010x}"
2488 ),
2489 ));
2490 }
2491 let buf = &buf[..buf.len() - 4];
2493 pos += 4;
2494 let version = u16::from_le_bytes(
2495 buf[pos..pos + 2]
2496 .try_into()
2497 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog header"))?,
2498 );
2499 pos += 2;
2500 if version == 0 || version > CATALOG_VERSION {
2510 return Err(io::Error::new(
2511 io::ErrorKind::InvalidData,
2512 format!("unsupported catalog version: {version}"),
2513 ));
2514 }
2515 let n_tables = u32::from_le_bytes(
2516 buf[pos..pos + 4]
2517 .try_into()
2518 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog header"))?,
2519 ) as usize;
2520 pos += 4;
2521
2522 if n_tables > buf.len() {
2529 return Err(io::Error::new(
2530 io::ErrorKind::InvalidData,
2531 format!("catalog file corrupt: implausible table count {n_tables}"),
2532 ));
2533 }
2534
2535 let mut entries = Vec::with_capacity(n_tables);
2536 for _ in 0..n_tables {
2537 let name_len = read_u32(buf, &mut pos)? as usize;
2538 let table_name = read_string(buf, &mut pos, name_len)?;
2539 let n_cols = read_u16(buf, &mut pos)? as usize;
2540
2541 let mut columns = Vec::with_capacity(n_cols);
2542 for _ in 0..n_cols {
2543 let cname_len = read_u32(buf, &mut pos)? as usize;
2544 let name = read_string(buf, &mut pos, cname_len)?;
2545 let type_id_raw = read_u8(buf, &mut pos)?;
2546 let type_id = type_id_from_u8(type_id_raw)?;
2547 let required = read_u8(buf, &mut pos)? != 0;
2548 let position = read_u16(buf, &mut pos)?;
2549 columns.push(ColumnDef {
2550 name,
2551 type_id,
2552 required,
2553 position,
2554 });
2555 }
2556
2557 let indexed_cols: Vec<IndexedColMeta> = if version >= 3 {
2561 let n = read_u16(buf, &mut pos)? as usize;
2562 let mut v = Vec::with_capacity(n);
2563 for _ in 0..n {
2564 let l = read_u32(buf, &mut pos)? as usize;
2565 let name = read_string(buf, &mut pos, l)?;
2566 let unique = read_u8(buf, &mut pos)? != 0;
2567 v.push(IndexedColMeta { name, unique });
2568 }
2569 v
2570 } else if version >= 2 {
2571 let n = read_u16(buf, &mut pos)? as usize;
2572 let mut v = Vec::with_capacity(n);
2573 for _ in 0..n {
2574 let l = read_u32(buf, &mut pos)? as usize;
2575 let name = read_string(buf, &mut pos, l)?;
2576 v.push(IndexedColMeta {
2577 name,
2578 unique: false,
2579 });
2580 }
2581 v
2582 } else {
2583 Vec::new()
2584 };
2585
2586 let defaults = if version >= 4 {
2588 decode_defaults_section(buf, &mut pos, columns.len()).ok_or_else(|| {
2589 io::Error::new(io::ErrorKind::InvalidData, "truncated catalog defaults")
2590 })?
2591 } else {
2592 Vec::new()
2593 };
2594
2595 let auto_cols = if version >= 5 {
2597 decode_auto_section(buf, &mut pos, columns.len()).ok_or_else(|| {
2598 io::Error::new(io::ErrorKind::InvalidData, "truncated catalog auto columns")
2599 })?
2600 } else {
2601 Vec::new()
2602 };
2603
2604 entries.push(CatalogEntry {
2605 schema: Schema {
2606 table_name,
2607 columns,
2608 },
2609 indexed_cols,
2610 defaults,
2611 auto_cols,
2612 });
2613 }
2614
2615 Ok(entries)
2616}
2617
2618fn read_u8(buf: &[u8], pos: &mut usize) -> io::Result<u8> {
2619 if *pos >= buf.len() {
2620 return Err(io::Error::new(
2621 io::ErrorKind::UnexpectedEof,
2622 "truncated catalog",
2623 ));
2624 }
2625 let v = buf[*pos];
2626 *pos += 1;
2627 Ok(v)
2628}
2629fn read_u16(buf: &[u8], pos: &mut usize) -> io::Result<u16> {
2630 if *pos + 2 > buf.len() {
2631 return Err(io::Error::new(
2632 io::ErrorKind::UnexpectedEof,
2633 "truncated catalog",
2634 ));
2635 }
2636 let v = u16::from_le_bytes(
2637 buf[*pos..*pos + 2]
2638 .try_into()
2639 .expect("bounds checked above"),
2640 );
2641 *pos += 2;
2642 Ok(v)
2643}
2644fn read_u32(buf: &[u8], pos: &mut usize) -> io::Result<u32> {
2645 if *pos + 4 > buf.len() {
2646 return Err(io::Error::new(
2647 io::ErrorKind::UnexpectedEof,
2648 "truncated catalog",
2649 ));
2650 }
2651 let v = u32::from_le_bytes(
2652 buf[*pos..*pos + 4]
2653 .try_into()
2654 .expect("bounds checked above"),
2655 );
2656 *pos += 4;
2657 Ok(v)
2658}
2659fn read_string(buf: &[u8], pos: &mut usize, len: usize) -> io::Result<String> {
2660 if *pos + len > buf.len() {
2661 return Err(io::Error::new(
2662 io::ErrorKind::UnexpectedEof,
2663 "truncated catalog string",
2664 ));
2665 }
2666 let s = std::str::from_utf8(&buf[*pos..*pos + len])
2667 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "non-utf8 in catalog"))?
2668 .to_string();
2669 *pos += len;
2670 Ok(s)
2671}
2672fn type_id_from_u8(v: u8) -> io::Result<TypeId> {
2673 match v {
2674 0 => Ok(TypeId::Empty),
2675 1 => Ok(TypeId::Int),
2676 2 => Ok(TypeId::Float),
2677 3 => Ok(TypeId::Bool),
2678 4 => Ok(TypeId::Str),
2679 5 => Ok(TypeId::DateTime),
2680 6 => Ok(TypeId::Uuid),
2681 7 => Ok(TypeId::Bytes),
2682 _ => Err(io::Error::new(
2683 io::ErrorKind::InvalidData,
2684 format!("unknown type id: {v}"),
2685 )),
2686 }
2687}
2688
2689#[cfg(test)]
2690mod tests {
2691 use super::*;
2692 fn temp_catalog(name: &str) -> Catalog {
2693 let dir = std::env::temp_dir().join(format!("powdb_cat_{name}_{}", std::process::id()));
2694 Catalog::create(&dir).unwrap()
2695 }
2696
2697 fn schema_two_cols() -> Schema {
2698 Schema {
2699 table_name: "T".into(),
2700 columns: vec![
2701 ColumnDef {
2702 name: "id".into(),
2703 type_id: TypeId::Int,
2704 required: true,
2705 position: 0,
2706 },
2707 ColumnDef {
2708 name: "status".into(),
2709 type_id: TypeId::Str,
2710 required: false,
2711 position: 1,
2712 },
2713 ],
2714 }
2715 }
2716
2717 #[test]
2718 fn replay_records_treats_reused_tx_ids_as_ordered_spans() {
2719 let mut cat = temp_catalog("reused_tx_ids");
2720 let schema = schema_two_cols();
2721 cat.create_table(schema.clone()).unwrap();
2722 cat.checkpoint().unwrap();
2723
2724 let mut committed_row = Vec::new();
2725 encode_row_into(
2726 &schema,
2727 &[Value::Int(1), Value::Str("committed".into())],
2728 &mut committed_row,
2729 );
2730 let mut incomplete_row = Vec::new();
2731 encode_row_into(
2732 &schema,
2733 &[Value::Int(2), Value::Str("incomplete".into())],
2734 &mut incomplete_row,
2735 );
2736
2737 let records = vec![
2738 WalRecord {
2739 tx_id: 1,
2740 record_type: WalRecordType::Begin,
2741 lsn: 1,
2742 data: Vec::new(),
2743 },
2744 WalRecord {
2745 tx_id: 1,
2746 record_type: WalRecordType::Insert,
2747 lsn: 2,
2748 data: encode_wal_payload(
2749 "T",
2750 RowId {
2751 page_id: 1,
2752 slot_index: 0,
2753 },
2754 &committed_row,
2755 ),
2756 },
2757 WalRecord {
2758 tx_id: 1,
2759 record_type: WalRecordType::Commit,
2760 lsn: 3,
2761 data: Vec::new(),
2762 },
2763 WalRecord {
2764 tx_id: 1,
2765 record_type: WalRecordType::Begin,
2766 lsn: 4,
2767 data: Vec::new(),
2768 },
2769 WalRecord {
2770 tx_id: 1,
2771 record_type: WalRecordType::Insert,
2772 lsn: 5,
2773 data: encode_wal_payload(
2774 "T",
2775 RowId {
2776 page_id: 1,
2777 slot_index: 1,
2778 },
2779 &incomplete_row,
2780 ),
2781 },
2782 ];
2783
2784 cat.apply_wal_records(&records).unwrap();
2785 let rows: Vec<_> = cat.scan("T").unwrap().collect();
2786 assert_eq!(rows.len(), 1);
2787 assert_eq!(rows[0].1[0], Value::Int(1));
2788 assert_eq!(rows[0].1[1], Value::Str("committed".into()));
2789 }
2790
2791 #[test]
2792 fn ddl_create_table_codec_roundtrips_defaults_and_auto() {
2793 let schema = schema_two_cols();
2794 let defaults = vec![None, Some(Value::Str("active".into()))];
2795 let auto_cols = vec![true, false];
2796 let encoded = encode_ddl_create_table(&schema, &defaults, &auto_cols);
2797 let (decoded_schema, decoded_defaults, decoded_auto) =
2798 decode_ddl_create_table(&encoded).unwrap();
2799 assert_eq!(decoded_schema.columns.len(), 2);
2800 assert_eq!(decoded_defaults, defaults);
2801 assert_eq!(decoded_auto, auto_cols);
2802 }
2803
2804 #[test]
2805 fn ddl_create_table_codec_back_compat_without_trailing_sections() {
2806 let schema = schema_two_cols();
2810 let full = encode_ddl_create_table(&schema, &[], &[]);
2811 let legacy = &full[..full.len() - 4];
2814 let (decoded_schema, decoded_defaults, decoded_auto) =
2815 decode_ddl_create_table(legacy).unwrap();
2816 assert_eq!(decoded_schema.columns.len(), 2);
2817 assert!(decoded_defaults.is_empty(), "no defaults section -> empty");
2818 assert!(decoded_auto.is_empty(), "no auto section -> empty");
2819 }
2820
2821 #[test]
2822 fn ddl_create_table_codec_back_compat_defaults_but_no_auto() {
2823 let schema = schema_two_cols();
2826 let defaults = vec![None, Some(Value::Str("active".into()))];
2827 let full = encode_ddl_create_table(&schema, &defaults, &[]);
2828 let legacy = &full[..full.len() - 2];
2830 let (_schema, decoded_defaults, decoded_auto) = decode_ddl_create_table(legacy).unwrap();
2831 assert_eq!(decoded_defaults, defaults);
2832 assert!(decoded_auto.is_empty());
2833 }
2834
2835 #[test]
2836 fn read_catalog_file_accepts_intermediate_versions_3_and_4() {
2837 use std::io::Write as _;
2845 fn write_legacy_catalog(path: &std::path::Path, version: u16) {
2846 let mut buf: Vec<u8> = Vec::new();
2847 buf.extend_from_slice(CATALOG_MAGIC);
2848 buf.extend_from_slice(&version.to_le_bytes());
2849 buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes());
2852 buf.extend_from_slice(b"T");
2853 buf.extend_from_slice(&2u16.to_le_bytes()); buf.extend_from_slice(&2u32.to_le_bytes());
2856 buf.extend_from_slice(b"id");
2857 buf.push(TypeId::Int as u8);
2858 buf.push(1);
2859 buf.extend_from_slice(&0u16.to_le_bytes());
2860 buf.extend_from_slice(&6u32.to_le_bytes());
2862 buf.extend_from_slice(b"status");
2863 buf.push(TypeId::Str as u8);
2864 buf.push(0);
2865 buf.extend_from_slice(&1u16.to_le_bytes());
2866 buf.extend_from_slice(&0u16.to_le_bytes());
2868 if version >= 4 {
2870 encode_defaults_section(&mut buf, &[None, None]);
2871 }
2872 let crc = crc32fast::hash(&buf);
2874 buf.extend_from_slice(&crc.to_le_bytes());
2875 let mut f = fs::File::create(path).unwrap();
2876 f.write_all(&buf).unwrap();
2877 }
2878
2879 for version in [3u16, 4u16] {
2880 let path = std::env::temp_dir().join(format!(
2881 "powdb_cat_v{version}_compat_{}.bin",
2882 std::process::id()
2883 ));
2884 write_legacy_catalog(&path, version);
2885 let entries = read_catalog_file(&path)
2886 .unwrap_or_else(|e| panic!("version {version} catalog must load, got: {e}"));
2887 assert_eq!(entries.len(), 1);
2888 assert_eq!(entries[0].schema.table_name, "T");
2889 assert_eq!(entries[0].schema.columns.len(), 2);
2890 assert!(
2891 entries[0].auto_cols.is_empty(),
2892 "v{version} has no auto cols"
2893 );
2894 fs::remove_file(&path).ok();
2895 }
2896 }
2897
2898 #[test]
2899 fn read_catalog_file_rejects_implausible_table_count() {
2900 use std::io::Write as _;
2908 let mut buf: Vec<u8> = Vec::new();
2909 buf.extend_from_slice(CATALOG_MAGIC);
2910 buf.extend_from_slice(&CATALOG_VERSION.to_le_bytes());
2911 buf.extend_from_slice(&1000u32.to_le_bytes()); let crc = crc32fast::hash(&buf);
2914 buf.extend_from_slice(&crc.to_le_bytes());
2915 let path =
2916 std::env::temp_dir().join(format!("powdb_cat_badcount_{}.bin", std::process::id()));
2917 fs::File::create(&path).unwrap().write_all(&buf).unwrap();
2918
2919 let msg = match read_catalog_file(&path) {
2920 Ok(_) => panic!("implausible table count must be rejected, got Ok"),
2921 Err(e) => e.to_string(),
2922 };
2923 assert!(
2924 msg.contains("implausible table count"),
2925 "expected an implausible-table-count error, got: {msg}"
2926 );
2927 fs::remove_file(&path).ok();
2928 }
2929
2930 #[test]
2931 fn data_dir_and_max_lsn_accessors() {
2932 let dir = std::env::temp_dir().join(format!("powdb_cat_maxlsn_{}", std::process::id()));
2933 let mut cat = Catalog::create(&dir).unwrap();
2934
2935 assert_eq!(cat.data_dir(), dir.as_path());
2937
2938 assert_eq!(cat.max_lsn(), 0);
2940
2941 let schema = Schema {
2942 table_name: "users".into(),
2943 columns: vec![ColumnDef {
2944 name: "name".into(),
2945 type_id: TypeId::Str,
2946 required: true,
2947 position: 0,
2948 }],
2949 };
2950 cat.create_table(schema).unwrap();
2951
2952 cat.insert("users", &vec![Value::Str("Alice".into())])
2953 .unwrap();
2954 cat.sync_wal().unwrap();
2955
2956 assert!(cat.max_lsn() > 0);
2959 }
2960
2961 #[test]
2962 fn test_create_table_and_insert() {
2963 let mut cat = temp_catalog("basic");
2964 let schema = Schema {
2965 table_name: "users".into(),
2966 columns: vec![
2967 ColumnDef {
2968 name: "name".into(),
2969 type_id: TypeId::Str,
2970 required: true,
2971 position: 0,
2972 },
2973 ColumnDef {
2974 name: "age".into(),
2975 type_id: TypeId::Int,
2976 required: false,
2977 position: 1,
2978 },
2979 ],
2980 };
2981 cat.create_table(schema).unwrap();
2982
2983 let row = vec![Value::Str("Alice".into()), Value::Int(30)];
2984 let rid = cat.insert("users", &row).unwrap();
2985
2986 let result = cat.get("users", rid).unwrap();
2987 assert_eq!(result[0], Value::Str("Alice".into()));
2988 assert_eq!(result[1], Value::Int(30));
2989 }
2990
2991 #[test]
2992 fn test_scan_table() {
2993 let mut cat = temp_catalog("scan");
2994 let schema = Schema {
2995 table_name: "items".into(),
2996 columns: vec![
2997 ColumnDef {
2998 name: "name".into(),
2999 type_id: TypeId::Str,
3000 required: true,
3001 position: 0,
3002 },
3003 ColumnDef {
3004 name: "price".into(),
3005 type_id: TypeId::Float,
3006 required: true,
3007 position: 1,
3008 },
3009 ],
3010 };
3011 cat.create_table(schema).unwrap();
3012
3013 for i in 0..50 {
3014 cat.insert(
3015 "items",
3016 &vec![
3017 Value::Str(format!("item_{i}")),
3018 Value::Float(i as f64 * 1.5),
3019 ],
3020 )
3021 .unwrap();
3022 }
3023
3024 let rows: Vec<_> = cat.scan("items").unwrap().collect();
3025 assert_eq!(rows.len(), 50);
3026 }
3027
3028 #[test]
3029 fn test_index_lookup() {
3030 let mut cat = temp_catalog("idx");
3031 let schema = Schema {
3032 table_name: "users".into(),
3033 columns: vec![
3034 ColumnDef {
3035 name: "email".into(),
3036 type_id: TypeId::Str,
3037 required: true,
3038 position: 0,
3039 },
3040 ColumnDef {
3041 name: "name".into(),
3042 type_id: TypeId::Str,
3043 required: true,
3044 position: 1,
3045 },
3046 ],
3047 };
3048 cat.create_table(schema).unwrap();
3049 cat.create_index("users", "email").unwrap();
3050
3051 cat.insert(
3052 "users",
3053 &vec![
3054 Value::Str("alice@example.com".into()),
3055 Value::Str("Alice".into()),
3056 ],
3057 )
3058 .unwrap();
3059 cat.insert(
3060 "users",
3061 &vec![
3062 Value::Str("bob@example.com".into()),
3063 Value::Str("Bob".into()),
3064 ],
3065 )
3066 .unwrap();
3067
3068 let result = cat
3069 .index_lookup("users", "email", &Value::Str("bob@example.com".into()))
3070 .unwrap();
3071 assert!(result.is_some());
3072 let row = result.unwrap();
3073 assert_eq!(row[1], Value::Str("Bob".into()));
3074 }
3075
3076 #[test]
3077 fn test_delete_row() {
3078 let mut cat = temp_catalog("delete");
3079 let schema = Schema {
3080 table_name: "t".into(),
3081 columns: vec![ColumnDef {
3082 name: "v".into(),
3083 type_id: TypeId::Int,
3084 required: true,
3085 position: 0,
3086 }],
3087 };
3088 cat.create_table(schema).unwrap();
3089 let r1 = cat.insert("t", &vec![Value::Int(1)]).unwrap();
3090 let r2 = cat.insert("t", &vec![Value::Int(2)]).unwrap();
3091 cat.delete("t", r1).unwrap();
3092 assert!(cat.get("t", r1).is_none());
3093 assert!(cat.get("t", r2).is_some());
3094 }
3095
3096 #[test]
3097 fn test_update_row() {
3098 let mut cat = temp_catalog("update");
3099 let schema = Schema {
3100 table_name: "t".into(),
3101 columns: vec![ColumnDef {
3102 name: "v".into(),
3103 type_id: TypeId::Int,
3104 required: true,
3105 position: 0,
3106 }],
3107 };
3108 cat.create_table(schema).unwrap();
3109 let rid = cat.insert("t", &vec![Value::Int(1)]).unwrap();
3110 let new_rid = cat.update("t", rid, &vec![Value::Int(99)]).unwrap();
3111 let row = cat.get("t", new_rid).unwrap();
3112 assert_eq!(row[0], Value::Int(99));
3113 }
3114
3115 #[test]
3116 fn test_persist_and_reopen() {
3117 let dir = std::env::temp_dir().join(format!("powdb_cat_persist_{}", std::process::id()));
3118 let _ = std::fs::remove_dir_all(&dir);
3120
3121 {
3122 let mut cat = Catalog::create(&dir).unwrap();
3123 cat.create_table(Schema {
3124 table_name: "users".into(),
3125 columns: vec![
3126 ColumnDef {
3127 name: "name".into(),
3128 type_id: TypeId::Str,
3129 required: true,
3130 position: 0,
3131 },
3132 ColumnDef {
3133 name: "age".into(),
3134 type_id: TypeId::Int,
3135 required: false,
3136 position: 1,
3137 },
3138 ],
3139 })
3140 .unwrap();
3141 cat.insert("users", &vec![Value::Str("Alice".into()), Value::Int(30)])
3142 .unwrap();
3143 cat.insert("users", &vec![Value::Str("Bob".into()), Value::Int(25)])
3144 .unwrap();
3145 }
3146
3147 let cat = Catalog::open(&dir).unwrap();
3149 let schema = cat.schema("users").unwrap();
3150 assert_eq!(schema.columns.len(), 2);
3151 assert_eq!(schema.columns[0].name, "name");
3152 assert_eq!(schema.columns[0].type_id, TypeId::Str);
3153 assert_eq!(schema.columns[1].type_id, TypeId::Int);
3154
3155 let rows: Vec<_> = cat.scan("users").unwrap().collect();
3156 assert_eq!(rows.len(), 2);
3157
3158 std::fs::remove_dir_all(&dir).ok();
3159 }
3160
3161 #[test]
3162 fn test_open_missing_dir_errors() {
3163 let dir = std::env::temp_dir().join(format!("powdb_cat_missing_{}", std::process::id()));
3164 let _ = std::fs::remove_dir_all(&dir);
3165 std::fs::create_dir_all(&dir).unwrap();
3166 assert!(Catalog::open(&dir).is_err());
3168 std::fs::remove_dir_all(&dir).ok();
3169 }
3170
3171 #[test]
3172 fn test_list_tables() {
3173 let mut cat = temp_catalog("list");
3174 cat.create_table(Schema {
3175 table_name: "a".into(),
3176 columns: vec![ColumnDef {
3177 name: "x".into(),
3178 type_id: TypeId::Int,
3179 required: true,
3180 position: 0,
3181 }],
3182 })
3183 .unwrap();
3184 cat.create_table(Schema {
3185 table_name: "b".into(),
3186 columns: vec![ColumnDef {
3187 name: "y".into(),
3188 type_id: TypeId::Int,
3189 required: true,
3190 position: 0,
3191 }],
3192 })
3193 .unwrap();
3194 let mut tables = cat.list_tables();
3195 tables.sort();
3196 assert_eq!(tables, vec!["a", "b"]);
3197 }
3198
3199 #[test]
3200 fn test_path_traversal_table_name_rejected() {
3201 let mut cat = temp_catalog("path_trav");
3202 let bad_names = vec![
3204 "../etc/passwd",
3205 "foo/bar",
3206 "table\0name",
3207 "",
3208 "123starts_with_digit",
3209 "has-dashes",
3210 "has spaces",
3211 "has.dots",
3212 ];
3213 for name in bad_names {
3214 let schema = Schema {
3215 table_name: name.into(),
3216 columns: vec![ColumnDef {
3217 name: "x".into(),
3218 type_id: TypeId::Int,
3219 required: true,
3220 position: 0,
3221 }],
3222 };
3223 let result = cat.create_table(schema);
3224 assert!(result.is_err(), "expected error for table name '{name}'");
3225 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
3226 }
3227 let good_names = vec!["users", "_private", "Table_123", "_"];
3229 for name in good_names {
3230 let schema = Schema {
3231 table_name: name.into(),
3232 columns: vec![ColumnDef {
3233 name: "x".into(),
3234 type_id: TypeId::Int,
3235 required: true,
3236 position: 0,
3237 }],
3238 };
3239 assert!(
3240 cat.create_table(schema).is_ok(),
3241 "expected ok for table name '{name}'"
3242 );
3243 }
3244 }
3245
3246 #[test]
3247 fn test_path_traversal_column_name_rejected() {
3248 let mut cat = temp_catalog("col_path_trav");
3249 let schema = Schema {
3250 table_name: "valid_table".into(),
3251 columns: vec![ColumnDef {
3252 name: "../bad".into(),
3253 type_id: TypeId::Int,
3254 required: true,
3255 position: 0,
3256 }],
3257 };
3258 let result = cat.create_table(schema);
3259 assert!(result.is_err());
3260 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
3261 }
3262
3263 #[test]
3264 fn test_drop_table_validates_name() {
3265 let mut cat = temp_catalog("drop_trav");
3266 let result = cat.drop_table("../etc/passwd");
3267 assert!(result.is_err());
3268 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
3270 }
3271}