1use crate::row::encode_row_into;
2use crate::table::Table;
3use crate::types::*;
4use crate::wal::{Wal, WalDurabilityTicket, 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 set_wal_sync_deferred(&mut self, defer: bool) {
914 self.wal.set_defer_sync(defer);
915 }
916
917 pub fn take_wal_durability_ticket(&mut self) -> Option<WalDurabilityTicket> {
920 self.wal.take_durability_ticket()
921 }
922
923 pub fn wal_fsync_count(&self) -> u64 {
925 self.wal.fsync_count()
926 }
927
928 pub fn rollback_to_last_sync(&mut self) -> io::Result<()> {
943 self.rollback_to_last_sync_inner(None)
944 }
945
946 pub fn rollback_to_last_sync_with_wal_archive<F>(&mut self, mut archive: F) -> io::Result<()>
951 where
952 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
953 {
954 let archive: WalArchiveCallback<'_> = &mut archive;
955 self.rollback_to_last_sync_inner(Some(archive))
956 }
957
958 fn rollback_to_last_sync_inner(
959 &mut self,
960 mut archive: Option<WalArchiveCallback<'_>>,
961 ) -> io::Result<()> {
962 let start_len = self.tx_start_len.unwrap_or(0);
963 let prearchived = if let Some(archive) = archive.as_mut() {
964 let records = self.wal.read_through_len(start_len)?;
965 if !records.is_empty() {
966 archive(&self.data_dir, &records)?;
967 }
968 true
969 } else {
970 false
971 };
972
973 let start_len = self.tx_start_len.take().unwrap_or(0);
974 if let Some(id) = self.active_tx_id.take() {
975 if !self.wal.is_off() {
976 let _ = self.wal.append(id, WalRecordType::Rollback, &[]);
977 }
978 }
979 self.wal.discard_and_truncate_to(start_len)?;
980
981 for tbl in &mut self.tables {
984 tbl.heap.discard_dirty();
985 }
986 self.wal.discard_pending()?;
992 let data_dir = self.data_dir.clone();
996 let sync_mode = self.wal.sync_mode();
997 let restored = if prearchived {
998 let mut already_archived = |_dir: &Path, _records: &[WalRecord]| Ok(());
999 let archive: WalArchiveCallback<'_> = &mut already_archived;
1000 Self::open_inner(&data_dir, Some(archive))?
1001 } else {
1002 Self::open_inner(&data_dir, archive)?
1003 };
1004 *self = restored;
1005 self.wal.set_sync_mode(sync_mode);
1006 Ok(())
1007 }
1008
1009 fn abandon_active_transaction_for_drop(&mut self) -> io::Result<()> {
1010 for tbl in &mut self.tables {
1011 tbl.heap.discard_dirty();
1012 }
1013 self.pending_autocommit_tx_ids.clear();
1014 let truncate_result = match self.tx_start_len.take() {
1015 Some(start_len) => self.wal.discard_and_truncate_to(start_len),
1016 None => self.wal.discard_pending(),
1017 };
1018 self.active_tx_id = None;
1019 truncate_result
1020 }
1021
1022 pub fn data_dir(&self) -> &Path {
1024 &self.data_dir
1025 }
1026
1027 pub fn max_lsn(&self) -> u64 {
1032 let max_page_lsn = self
1033 .tables
1034 .iter()
1035 .map(|t| t.heap.max_page_lsn())
1036 .max()
1037 .unwrap_or(0);
1038 max_page_lsn
1039 .max(self.durable_lsn)
1040 .max(self.wal.last_appended_lsn())
1041 }
1042
1043 pub fn create_table(&mut self, schema: Schema) -> io::Result<()> {
1044 self.create_table_full(schema, Vec::new(), Vec::new())
1045 }
1046
1047 pub fn create_table_with_defaults(
1051 &mut self,
1052 schema: Schema,
1053 defaults: Vec<Option<Value>>,
1054 ) -> io::Result<()> {
1055 self.create_table_full(schema, defaults, Vec::new())
1056 }
1057
1058 pub fn create_table_full(
1063 &mut self,
1064 schema: Schema,
1065 defaults: Vec<Option<Value>>,
1066 auto_cols: Vec<bool>,
1067 ) -> io::Result<()> {
1068 validate_table_name(&schema.table_name)?;
1069 for col in &schema.columns {
1070 validate_column_name(&col.name)?;
1071 }
1072 let name = schema.table_name.clone();
1073 if self.name_to_slot.contains_key(&name) {
1074 return Err(io::Error::new(
1075 io::ErrorKind::AlreadyExists,
1076 format!("table '{name}' already exists"),
1077 ));
1078 }
1079 if !self.wal.is_off() {
1080 let payload = encode_ddl_create_table(&schema, &defaults, &auto_cols);
1081 self.wal
1082 .append(0, WalRecordType::DdlCreateTable, &payload)?;
1083 self.wal.flush()?;
1084 }
1085 let mut table = Table::create(schema, &self.data_dir)?;
1086 table.set_defaults(defaults);
1087 table.set_auto_cols(auto_cols);
1088 let slot = self.tables.len();
1089 self.tables.push(table);
1090 self.name_to_slot.insert(name, slot);
1091 self.persist()?;
1092 Ok(())
1093 }
1094
1095 pub fn column_defaults(&self, table: &str) -> Option<&[Option<Value>]> {
1099 let slot = *self.name_to_slot.get(table)?;
1100 Some(self.tables[slot].defaults())
1101 }
1102
1103 pub fn auto_columns(&self, table: &str) -> Option<&[bool]> {
1106 let slot = *self.name_to_slot.get(table)?;
1107 Some(self.tables[slot].auto_cols())
1108 }
1109
1110 pub fn assign_auto_columns(&mut self, table: &str, values: &mut [Value]) {
1114 if let Some(&slot) = self.name_to_slot.get(table) {
1115 self.tables[slot].assign_auto(values);
1116 }
1117 }
1118
1119 fn persist(&self) -> io::Result<()> {
1124 let cat_path = self.data_dir.join(CATALOG_FILE);
1125 let tmp_path = self.data_dir.join(format!("{CATALOG_FILE}.tmp"));
1126 let entries: Vec<CatalogEntryRef<'_>> = self
1127 .tables
1128 .iter()
1129 .map(|t| CatalogEntryRef {
1130 schema: &t.schema,
1131 indexed_cols: t.indexed_column_metas(),
1132 defaults: t.defaults(),
1133 auto_cols: t.auto_cols(),
1134 })
1135 .collect();
1136 write_catalog_file(&tmp_path, &entries)?;
1137 fs::rename(&tmp_path, &cat_path)?;
1138 Ok(())
1139 }
1140
1141 #[inline]
1145 pub fn table_slot(&self, name: &str) -> Option<usize> {
1146 self.name_to_slot.get(name).copied()
1147 }
1148
1149 #[inline]
1152 pub fn table_by_slot(&self, slot: usize) -> &Table {
1153 &self.tables[slot]
1154 }
1155
1156 #[inline]
1158 pub fn table_by_slot_mut(&mut self, slot: usize) -> &mut Table {
1159 &mut self.tables[slot]
1160 }
1161
1162 pub fn get_table(&self, name: &str) -> Option<&Table> {
1163 let slot = *self.name_to_slot.get(name)?;
1164 Some(&self.tables[slot])
1165 }
1166
1167 pub fn get_table_mut(&mut self, name: &str) -> Option<&mut Table> {
1168 let slot = *self.name_to_slot.get(name)?;
1169 Some(&mut self.tables[slot])
1170 }
1171
1172 #[inline]
1177 fn by_name(&self, table: &str) -> io::Result<&Table> {
1178 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1179 io::Error::new(
1180 io::ErrorKind::NotFound,
1181 format!("table '{table}' not found"),
1182 )
1183 })?;
1184 Ok(&self.tables[slot])
1185 }
1186
1187 #[inline]
1189 fn by_name_mut(&mut self, table: &str) -> io::Result<&mut Table> {
1190 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1191 io::Error::new(
1192 io::ErrorKind::NotFound,
1193 format!("table '{table}' not found"),
1194 )
1195 })?;
1196 Ok(&mut self.tables[slot])
1197 }
1198
1199 pub fn insert(&mut self, table: &str, values: &Row) -> io::Result<RowId> {
1200 if self.wal.is_off() {
1209 return self.by_name_mut(table)?.insert(values);
1210 }
1211 let tbl = self.by_name_mut(table)?;
1212 let mut wal_bytes: Vec<u8> = Vec::new();
1213 encode_row_into(&tbl.schema, values, &mut wal_bytes);
1214 let new_rid = tbl.insert(values)?;
1228 let tx_id = self.next_tx();
1229 self.wal_log(tx_id, WalRecordType::Insert, table, new_rid, &wal_bytes)?;
1230 let lsn = self.wal.last_appended_lsn();
1235 if lsn > 0 {
1236 self.by_name_mut(table)?
1237 .heap
1238 .set_page_lsn(new_rid.page_id, lsn)?;
1239 }
1240 Ok(new_rid)
1241 }
1242
1243 pub fn insert_by_slot(&mut self, slot: usize, values: &Row) -> io::Result<RowId> {
1251 if self.wal.is_off() {
1252 return self.tables[slot].insert(values);
1253 }
1254 let tx_id = self.next_tx();
1255 let autocommit = self.active_tx_id.is_none();
1256 let Catalog { tables, wal, .. } = self;
1257 let tbl = &mut tables[slot];
1258 let mut wal_bytes: Vec<u8> = Vec::new();
1259 encode_row_into(&tbl.schema, values, &mut wal_bytes);
1260 let new_rid = tbl.insert(values)?;
1263 let payload = encode_wal_payload(&tbl.schema.table_name, new_rid, &wal_bytes);
1264 wal.append(tx_id, WalRecordType::Insert, &payload)?;
1265 if autocommit {
1266 self.pending_autocommit_tx_ids.push(tx_id);
1267 }
1268 let lsn = wal.last_appended_lsn();
1269 if lsn > 0 {
1270 tbl.heap.set_page_lsn(new_rid.page_id, lsn)?;
1271 }
1272 Ok(new_rid)
1273 }
1274
1275 pub fn get(&self, table: &str, rid: RowId) -> Option<Row> {
1276 self.get_table(table)?.get(rid)
1277 }
1278
1279 pub fn delete(&mut self, table: &str, rid: RowId) -> io::Result<()> {
1280 if self.wal.is_off() {
1283 return self.by_name_mut(table)?.delete(rid);
1284 }
1285 let tx_id = self.next_tx();
1286 self.wal_log(tx_id, WalRecordType::Delete, table, rid, &[])?;
1288 self.by_name_mut(table)?.delete(rid)
1289 }
1290
1291 pub fn delete_many(&mut self, table: &str, rids: &[RowId]) -> io::Result<u64> {
1295 if self.wal.is_off() {
1303 return self.by_name_mut(table)?.delete_many(rids);
1304 }
1305 let tx_id = self.next_tx();
1306 for &rid in rids {
1307 let payload = encode_wal_payload(table, rid, &[]);
1308 self.wal.append(tx_id, WalRecordType::Delete, &payload)?;
1309 }
1310 if self.active_tx_id.is_none() && !rids.is_empty() {
1311 self.pending_autocommit_tx_ids.push(tx_id);
1312 }
1313 self.by_name_mut(table)?.delete_many(rids)
1314 }
1315
1316 pub fn scan_delete_matching<P>(&mut self, table: &str, pred: P) -> io::Result<u64>
1326 where
1327 P: FnMut(&[u8]) -> bool,
1328 {
1329 self.by_name_mut(table)?.scan_delete_matching(pred)
1330 }
1331
1332 pub fn scan_delete_matching_logged<P>(&mut self, table: &str, pred: P) -> io::Result<u64>
1343 where
1344 P: FnMut(&[u8]) -> bool,
1345 {
1346 if self.wal.is_off() {
1352 return self.by_name_mut(table)?.scan_delete_matching(pred);
1353 }
1354 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1358 io::Error::new(
1359 io::ErrorKind::NotFound,
1360 format!("table '{table}' not found"),
1361 )
1362 })?;
1363 let tx_id = self.next_tx();
1364 let autocommit = self.active_tx_id.is_none();
1365 let Catalog { tables, wal, .. } = self;
1368 let tbl = &mut tables[slot];
1369 let name_bytes = table.as_bytes();
1373 let count = tbl.scan_delete_matching_with_hook(pred, |rid, row_bytes| {
1374 let mut payload: Vec<u8> =
1375 Vec::with_capacity(4 + name_bytes.len() + 10 + row_bytes.len());
1376 payload.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
1377 payload.extend_from_slice(name_bytes);
1378 payload.extend_from_slice(&rid.page_id.to_le_bytes());
1379 payload.extend_from_slice(&rid.slot_index.to_le_bytes());
1380 payload.extend_from_slice(&0u32.to_le_bytes());
1384 let _ = wal.append(tx_id, WalRecordType::Delete, &payload);
1391 })?;
1392 if autocommit && count > 0 {
1393 self.pending_autocommit_tx_ids.push(tx_id);
1394 }
1395 Ok(count)
1397 }
1398
1399 pub fn scan_patch_matching_logged<P, M>(
1407 &mut self,
1408 table: &str,
1409 pred: P,
1410 try_mutate: M,
1411 ) -> io::Result<(u64, Vec<RowId>)>
1412 where
1413 P: FnMut(&[u8]) -> bool,
1414 M: FnMut(&mut [u8]) -> Option<u16>,
1415 {
1416 if self.wal.is_off() {
1417 return self.by_name_mut(table)?.scan_patch_matching_with_hook(
1418 pred,
1419 try_mutate,
1420 |_, _| {},
1421 );
1422 }
1423 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1424 io::Error::new(
1425 io::ErrorKind::NotFound,
1426 format!("table '{table}' not found"),
1427 )
1428 })?;
1429 let tx_id = self.next_tx();
1430 let autocommit = self.active_tx_id.is_none();
1431 let Catalog { tables, wal, .. } = self;
1432 let tbl = &mut tables[slot];
1433 let name_bytes = table.as_bytes();
1434 let result = tbl.scan_patch_matching_with_hook(pred, try_mutate, |rid, row_bytes| {
1435 let mut payload: Vec<u8> =
1436 Vec::with_capacity(4 + name_bytes.len() + 10 + row_bytes.len());
1437 payload.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
1438 payload.extend_from_slice(name_bytes);
1439 payload.extend_from_slice(&rid.page_id.to_le_bytes());
1440 payload.extend_from_slice(&rid.slot_index.to_le_bytes());
1441 payload.extend_from_slice(&(row_bytes.len() as u32).to_le_bytes());
1442 payload.extend_from_slice(row_bytes);
1443 let _ = wal.append(tx_id, WalRecordType::Update, &payload);
1444 })?;
1445 if autocommit && result.0 > 0 {
1446 self.pending_autocommit_tx_ids.push(tx_id);
1447 }
1448 Ok(result)
1449 }
1450
1451 pub fn update(&mut self, table: &str, rid: RowId, values: &Row) -> io::Result<RowId> {
1452 if self.wal.is_off() {
1455 return self.by_name_mut(table)?.update(rid, values);
1456 }
1457 let tbl = self.by_name_mut(table)?;
1458 let mut wal_bytes: Vec<u8> = Vec::new();
1459 encode_row_into(&tbl.schema, values, &mut wal_bytes);
1460 check_encoded_row_size(&wal_bytes)?;
1463 let tx_id = self.next_tx();
1464 self.wal_log(tx_id, WalRecordType::Update, table, rid, &wal_bytes)?;
1465 self.by_name_mut(table)?.update(rid, values)
1466 }
1467
1468 pub fn update_hinted(
1472 &mut self,
1473 table: &str,
1474 rid: RowId,
1475 values: &Row,
1476 changed_col_indices: Option<&[usize]>,
1477 ) -> io::Result<RowId> {
1478 if self.wal.is_off() {
1482 return self
1483 .by_name_mut(table)?
1484 .update_hinted(rid, values, changed_col_indices);
1485 }
1486 let tbl = self.by_name_mut(table)?;
1487 let mut wal_bytes: Vec<u8> = Vec::new();
1488 encode_row_into(&tbl.schema, values, &mut wal_bytes);
1489 check_encoded_row_size(&wal_bytes)?;
1491 let tx_id = self.next_tx();
1492 self.wal_log(tx_id, WalRecordType::Update, table, rid, &wal_bytes)?;
1493 self.by_name_mut(table)?
1494 .update_hinted(rid, values, changed_col_indices)
1495 }
1496
1497 #[inline]
1509 pub fn with_row_bytes_mut<F>(&mut self, table: &str, rid: RowId, f: F) -> io::Result<bool>
1510 where
1511 F: FnOnce(&mut [u8]),
1512 {
1513 self.by_name_mut(table)?.with_row_bytes_mut(rid, f)
1514 }
1515
1516 #[inline]
1530 pub fn update_row_bytes_logged<F>(&mut self, table: &str, rid: RowId, f: F) -> io::Result<bool>
1531 where
1532 F: FnOnce(&mut [u8]),
1533 {
1534 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1535 io::Error::new(
1536 io::ErrorKind::NotFound,
1537 format!("table '{table}' not found"),
1538 )
1539 })?;
1540 self.update_row_bytes_logged_by_slot(slot, rid, f)
1541 }
1542
1543 #[inline]
1548 pub fn update_row_bytes_logged_by_slot<F>(
1549 &mut self,
1550 slot: usize,
1551 rid: RowId,
1552 f: F,
1553 ) -> io::Result<bool>
1554 where
1555 F: FnOnce(&mut [u8]),
1556 {
1557 let tbl = &mut self.tables[slot];
1560 let ok = tbl.with_row_bytes_mut(rid, f)?;
1561 if !ok {
1562 return Ok(false);
1563 }
1564 if self.wal.is_off() {
1568 return Ok(true);
1569 }
1570 let new_bytes = match tbl.heap.get(rid) {
1573 Some(b) => b,
1574 None => return Ok(false),
1576 };
1577 let table_name = tbl.schema.table_name.clone();
1580 let tx_id = self.next_tx();
1581 self.wal_log(tx_id, WalRecordType::Update, &table_name, rid, &new_bytes)?;
1582 Ok(true)
1583 }
1584
1585 #[inline]
1597 pub fn patch_var_col_in_place(
1598 &mut self,
1599 table: &str,
1600 rid: RowId,
1601 col_idx: usize,
1602 new_value: Option<&[u8]>,
1603 ) -> io::Result<bool> {
1604 self.by_name_mut(table)?
1605 .patch_var_col_in_place(rid, col_idx, new_value)
1606 }
1607
1608 pub fn patch_var_col_logged(
1614 &mut self,
1615 table: &str,
1616 rid: RowId,
1617 col_idx: usize,
1618 new_value: Option<&[u8]>,
1619 ) -> io::Result<bool> {
1620 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1621 io::Error::new(
1622 io::ErrorKind::NotFound,
1623 format!("table '{table}' not found"),
1624 )
1625 })?;
1626 let tbl = &mut self.tables[slot];
1627 let ok = tbl.patch_var_col_in_place(rid, col_idx, new_value)?;
1628 if !ok {
1629 return Ok(false);
1630 }
1631 if self.wal.is_off() {
1634 return Ok(true);
1635 }
1636 let new_bytes = match tbl.heap.get(rid) {
1637 Some(b) => b,
1638 None => return Ok(false),
1639 };
1640 let table_name = tbl.schema.table_name.clone();
1641 let tx_id = self.next_tx();
1642 self.wal_log(tx_id, WalRecordType::Update, &table_name, rid, &new_bytes)?;
1643 Ok(true)
1644 }
1645
1646 pub fn scan(&self, table: &str) -> io::Result<impl Iterator<Item = (RowId, Row)> + '_> {
1647 Ok(self.by_name(table)?.scan())
1648 }
1649
1650 pub fn for_each_row_raw<F>(&self, table: &str, f: F) -> io::Result<()>
1653 where
1654 F: FnMut(RowId, &[u8]),
1655 {
1656 self.by_name(table)?.for_each_row_raw(f);
1657 Ok(())
1658 }
1659
1660 pub fn try_for_each_row_raw<F>(&self, table: &str, f: F) -> io::Result<()>
1665 where
1666 F: FnMut(RowId, &[u8]) -> std::ops::ControlFlow<()>,
1667 {
1668 self.by_name(table)?.try_for_each_row_raw(f);
1669 Ok(())
1670 }
1671
1672 pub fn create_index(&mut self, table: &str, column: &str) -> io::Result<()> {
1673 self.create_index_unique(table, column, false)
1674 }
1675
1676 pub fn create_index_unique(
1681 &mut self,
1682 table: &str,
1683 column: &str,
1684 unique: bool,
1685 ) -> io::Result<()> {
1686 let data_dir = self.data_dir.clone();
1687 self.by_name_mut(table)?
1688 .create_index_with_unique(column, &data_dir, unique)?;
1689 self.persist()
1693 }
1694
1695 pub fn is_index_unique(&self, table: &str, column: &str) -> Option<bool> {
1699 self.get_table(table)?.is_index_unique(column)
1700 }
1701
1702 pub fn has_index(&self, table: &str, column: &str) -> bool {
1704 self.get_table(table)
1705 .map(|t| t.has_index(column))
1706 .unwrap_or(false)
1707 }
1708
1709 pub fn index_lookup(&self, table: &str, column: &str, key: &Value) -> io::Result<Option<Row>> {
1710 Ok(self
1711 .by_name(table)?
1712 .index_lookup(column, key)
1713 .map(|(_, row)| row))
1714 }
1715
1716 pub fn list_tables(&self) -> Vec<&str> {
1717 self.tables
1721 .iter()
1722 .map(|t| t.schema.table_name.as_str())
1723 .collect()
1724 }
1725
1726 pub fn schema(&self, table: &str) -> Option<&Schema> {
1727 let slot = *self.name_to_slot.get(table)?;
1728 Some(&self.tables[slot].schema)
1729 }
1730
1731 pub fn drop_table(&mut self, name: &str) -> io::Result<()> {
1734 validate_table_name(name)?;
1735 let slot = *self.name_to_slot.get(name).ok_or_else(|| {
1736 io::Error::new(io::ErrorKind::NotFound, format!("table '{name}' not found"))
1737 })?;
1738 if !self.wal.is_off() {
1739 let payload = encode_ddl_drop_table(name);
1740 self.wal.append(0, WalRecordType::DdlDropTable, &payload)?;
1741 self.wal.flush()?;
1742 }
1743 let table = &self.tables[slot];
1745 let heap_path = self
1746 .data_dir
1747 .join(format!("{}.heap", table.schema.table_name));
1748 if heap_path.exists() {
1749 fs::remove_file(&heap_path)?;
1750 }
1751 for col_name in table.indexed_column_names() {
1757 let idx_path = self.data_dir.join(format!("{name}_{col_name}.idx"));
1758 if idx_path.exists() {
1759 let _ = fs::remove_file(&idx_path);
1760 }
1761 }
1762 self.name_to_slot.remove(name);
1764 let last = self.tables.len() - 1;
1765 if slot != last {
1766 let moved_name = self.tables[last].schema.table_name.clone();
1767 self.tables.swap(slot, last);
1768 self.name_to_slot.insert(moved_name, slot);
1769 }
1770 self.tables.pop();
1771 self.persist()?;
1772 Ok(())
1773 }
1774
1775 pub fn alter_table_add_column(&mut self, table: &str, col: ColumnDef) -> io::Result<()> {
1798 let data_dir = self.data_dir.clone();
1799 {
1800 let tbl = self.by_name_mut(table)?;
1801 if tbl.schema.columns.iter().any(|c| c.name == col.name) {
1802 return Err(io::Error::new(
1803 io::ErrorKind::AlreadyExists,
1804 format!("column '{}' already exists in table '{table}'", col.name),
1805 ));
1806 }
1807 }
1808 let barrier_lsn = if !self.wal.is_off() {
1809 let payload = encode_ddl_alter_add_column(table, &col);
1810 self.wal.append(0, WalRecordType::DdlAddColumn, &payload)?;
1811 self.wal.flush()?;
1812 self.wal.last_appended_lsn()
1813 } else {
1814 0
1815 };
1816 let tbl = self.by_name_mut(table)?;
1817
1818 let old_schema = tbl.schema.clone();
1819
1820 let has_rows = tbl.heap.scan().next().is_some();
1824
1825 if has_rows && col.required {
1826 return Err(io::Error::new(
1827 io::ErrorKind::InvalidInput,
1828 format!(
1829 "cannot add required column '{}' to non-empty table '{table}': \
1830 no default value to backfill existing rows with",
1831 col.name
1832 ),
1833 ));
1834 }
1835
1836 tbl.schema.columns.push(col);
1840 tbl.refresh_layout();
1841
1842 if has_rows {
1843 let fill: Vec<Value> = vec![Value::Empty; tbl.schema.columns.len()];
1848 tbl.rewrite_rows_for_schema_change(&old_schema, &fill, &data_dir)?;
1849 }
1850 if barrier_lsn > 0 {
1858 tbl.heap.stamp_all_pages_min_lsn(barrier_lsn)?;
1859 tbl.heap.flush()?;
1860 }
1861
1862 self.persist()?;
1863 Ok(())
1864 }
1865
1866 pub fn alter_table_drop_column(&mut self, table: &str, col_name: &str) -> io::Result<()> {
1889 let data_dir = self.data_dir.clone();
1890 {
1891 let tbl = self.by_name_mut(table)?;
1892 tbl.schema
1893 .columns
1894 .iter()
1895 .position(|c| c.name == col_name)
1896 .ok_or_else(|| {
1897 io::Error::new(
1898 io::ErrorKind::NotFound,
1899 format!("column '{col_name}' not found in table '{table}'"),
1900 )
1901 })?;
1902 }
1903 let barrier_lsn = if !self.wal.is_off() {
1904 let payload = encode_ddl_alter_drop_column(table, col_name);
1905 self.wal.append(0, WalRecordType::DdlDropColumn, &payload)?;
1906 self.wal.flush()?;
1907 self.wal.last_appended_lsn()
1908 } else {
1909 0
1910 };
1911 let tbl = self.by_name_mut(table)?;
1912 let idx = tbl
1913 .schema
1914 .columns
1915 .iter()
1916 .position(|c| c.name == col_name)
1917 .ok_or_else(|| {
1918 io::Error::new(
1919 io::ErrorKind::NotFound,
1920 format!("column '{col_name}' not found in table '{table}'"),
1921 )
1922 })?;
1923
1924 let old_schema = tbl.schema.clone();
1926 let has_rows = tbl.heap.scan().next().is_some();
1927
1928 tbl.schema.columns.remove(idx);
1930 for (i, col) in tbl.schema.columns.iter_mut().enumerate() {
1931 col.position = i as u16;
1932 }
1933 tbl.refresh_layout();
1934
1935 if has_rows {
1936 let fill: Vec<Value> = vec![Value::Empty; tbl.schema.columns.len()];
1942 tbl.rewrite_rows_for_schema_change(&old_schema, &fill, &data_dir)?;
1943 }
1944 if barrier_lsn > 0 {
1946 tbl.heap.stamp_all_pages_min_lsn(barrier_lsn)?;
1947 tbl.heap.flush()?;
1948 }
1949
1950 self.persist()?;
1951 Ok(())
1952 }
1953}
1954
1955impl Drop for Catalog {
1956 fn drop(&mut self) {
1957 if self.active_tx_id.is_some() {
1958 if let Err(e) = self.abandon_active_transaction_for_drop() {
1959 warn!(error = %e, "catalog drop active transaction cleanup failed");
1960 }
1961 return;
1962 }
1963 if let Err(e) = self.checkpoint() {
1975 warn!(error = %e, "catalog drop checkpoint failed");
1976 }
1977 }
1978}
1979
1980fn encode_wal_payload(table: &str, rid: RowId, row_bytes: &[u8]) -> Vec<u8> {
1995 let name = table.as_bytes();
1996 let mut out = Vec::with_capacity(4 + name.len() + 4 + 2 + 4 + row_bytes.len());
1997 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
1998 out.extend_from_slice(name);
1999 out.extend_from_slice(&rid.page_id.to_le_bytes());
2000 out.extend_from_slice(&rid.slot_index.to_le_bytes());
2001 out.extend_from_slice(&(row_bytes.len() as u32).to_le_bytes());
2002 out.extend_from_slice(row_bytes);
2003 out
2004}
2005
2006fn decode_wal_payload(data: &[u8]) -> Option<(String, RowId, Vec<u8>)> {
2007 let mut pos = 0usize;
2008 if data.len() < 4 {
2009 return None;
2010 }
2011 let name_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2012 pos += 4;
2013 if pos + name_len > data.len() {
2014 return None;
2015 }
2016 let name = std::str::from_utf8(&data[pos..pos + name_len])
2017 .ok()?
2018 .to_string();
2019 pos += name_len;
2020 if pos + 4 + 2 + 4 > data.len() {
2021 return None;
2022 }
2023 let page_id = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
2024 pos += 4;
2025 let slot_index = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
2026 pos += 2;
2027 let row_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2028 pos += 4;
2029 if pos + row_len > data.len() {
2030 return None;
2031 }
2032 let row_bytes = data[pos..pos + row_len].to_vec();
2033 Some((
2034 name,
2035 RowId {
2036 page_id,
2037 slot_index,
2038 },
2039 row_bytes,
2040 ))
2041}
2042
2043fn encode_ddl_create_table(
2046 schema: &Schema,
2047 defaults: &[Option<Value>],
2048 auto_cols: &[bool],
2049) -> Vec<u8> {
2050 let name = schema.table_name.as_bytes();
2051 let mut out = Vec::new();
2052 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
2053 out.extend_from_slice(name);
2054 out.extend_from_slice(&(schema.columns.len() as u16).to_le_bytes());
2055 for col in &schema.columns {
2056 let cn = col.name.as_bytes();
2057 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
2058 out.extend_from_slice(cn);
2059 out.push(col.type_id as u8);
2060 out.push(col.required as u8);
2061 out.extend_from_slice(&col.position.to_le_bytes());
2062 }
2063 encode_defaults_section(&mut out, defaults);
2067 encode_auto_section(&mut out, auto_cols);
2068 out
2069}
2070
2071fn decode_ddl_create_table(data: &[u8]) -> Option<(Schema, Vec<Option<Value>>, Vec<bool>)> {
2072 let mut pos = 0usize;
2073 if data.len() < 4 {
2074 return None;
2075 }
2076 let name_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2077 pos += 4;
2078 if pos + name_len > data.len() {
2079 return None;
2080 }
2081 let table_name = std::str::from_utf8(&data[pos..pos + name_len])
2082 .ok()?
2083 .to_string();
2084 pos += name_len;
2085 if pos + 2 > data.len() {
2086 return None;
2087 }
2088 let n_cols = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
2089 pos += 2;
2090 let mut columns = Vec::with_capacity(n_cols);
2091 for _ in 0..n_cols {
2092 if pos + 4 > data.len() {
2093 return None;
2094 }
2095 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2096 pos += 4;
2097 if pos + cn_len + 4 > data.len() {
2098 return None;
2099 }
2100 let col_name = std::str::from_utf8(&data[pos..pos + cn_len])
2101 .ok()?
2102 .to_string();
2103 pos += cn_len;
2104 let type_id = TypeId::from_u8(data[pos])?;
2105 pos += 1;
2106 let required = data[pos] != 0;
2107 pos += 1;
2108 if pos + 2 > data.len() {
2109 return None;
2110 }
2111 let position = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
2112 pos += 2;
2113 columns.push(ColumnDef {
2114 name: col_name,
2115 type_id,
2116 required,
2117 position,
2118 });
2119 }
2120 let defaults = if pos < data.len() {
2123 decode_defaults_section(data, &mut pos, columns.len())?
2124 } else {
2125 Vec::new()
2126 };
2127 let auto_cols = if pos < data.len() {
2128 decode_auto_section(data, &mut pos, columns.len())?
2129 } else {
2130 Vec::new()
2131 };
2132 Some((
2133 Schema {
2134 table_name,
2135 columns,
2136 },
2137 defaults,
2138 auto_cols,
2139 ))
2140}
2141
2142fn encode_ddl_drop_table(table_name: &str) -> Vec<u8> {
2143 let name = table_name.as_bytes();
2144 let mut out = Vec::with_capacity(4 + name.len());
2145 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
2146 out.extend_from_slice(name);
2147 out
2148}
2149
2150fn encode_ddl_alter_add_column(table_name: &str, col: &ColumnDef) -> Vec<u8> {
2151 let name = table_name.as_bytes();
2152 let cn = col.name.as_bytes();
2153 let mut out = Vec::with_capacity(4 + name.len() + 4 + cn.len() + 4);
2154 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
2155 out.extend_from_slice(name);
2156 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
2157 out.extend_from_slice(cn);
2158 out.push(col.type_id as u8);
2159 out.push(col.required as u8);
2160 out.extend_from_slice(&col.position.to_le_bytes());
2161 out
2162}
2163
2164fn encode_ddl_alter_drop_column(table_name: &str, col_name: &str) -> Vec<u8> {
2165 let name = table_name.as_bytes();
2166 let cn = col_name.as_bytes();
2167 let mut out = Vec::with_capacity(4 + name.len() + 4 + cn.len());
2168 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
2169 out.extend_from_slice(name);
2170 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
2171 out.extend_from_slice(cn);
2172 out
2173}
2174
2175fn decode_ddl_table_name(data: &[u8]) -> Option<(String, usize)> {
2176 if data.len() < 4 {
2177 return None;
2178 }
2179 let name_len = u32::from_le_bytes(data[0..4].try_into().ok()?) as usize;
2180 if 4 + name_len > data.len() {
2181 return None;
2182 }
2183 let name = std::str::from_utf8(&data[4..4 + name_len])
2184 .ok()?
2185 .to_string();
2186 Some((name, 4 + name_len))
2187}
2188
2189fn decode_ddl_alter_add_column(data: &[u8]) -> Option<(String, ColumnDef)> {
2190 let (table_name, mut pos) = decode_ddl_table_name(data)?;
2191 if pos + 4 > data.len() {
2192 return None;
2193 }
2194 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2195 pos += 4;
2196 if pos + cn_len + 4 > data.len() {
2197 return None;
2198 }
2199 let col_name = std::str::from_utf8(&data[pos..pos + cn_len])
2200 .ok()?
2201 .to_string();
2202 pos += cn_len;
2203 let type_id = TypeId::from_u8(data[pos])?;
2204 pos += 1;
2205 let required = data[pos] != 0;
2206 pos += 1;
2207 if pos + 2 > data.len() {
2208 return None;
2209 }
2210 let position = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
2211 Some((
2212 table_name,
2213 ColumnDef {
2214 name: col_name,
2215 type_id,
2216 required,
2217 position,
2218 },
2219 ))
2220}
2221
2222fn decode_ddl_alter_drop_column(data: &[u8]) -> Option<(String, String)> {
2223 let (table_name, pos) = decode_ddl_table_name(data)?;
2224 if pos + 4 > data.len() {
2225 return None;
2226 }
2227 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2228 if pos + 4 + cn_len > data.len() {
2229 return None;
2230 }
2231 let col_name = std::str::from_utf8(&data[pos + 4..pos + 4 + cn_len])
2232 .ok()?
2233 .to_string();
2234 Some((table_name, col_name))
2235}
2236
2237pub(crate) struct IndexedColMeta {
2265 pub name: String,
2266 pub unique: bool,
2267}
2268
2269pub(crate) struct CatalogEntry {
2272 pub schema: Schema,
2273 pub indexed_cols: Vec<IndexedColMeta>,
2274 pub defaults: Vec<Option<Value>>,
2277 pub auto_cols: Vec<bool>,
2280}
2281
2282pub(crate) struct CatalogEntryRef<'a> {
2284 pub schema: &'a Schema,
2285 pub indexed_cols: Vec<IndexedColMeta>,
2286 pub defaults: &'a [Option<Value>],
2287 pub auto_cols: &'a [bool],
2288}
2289
2290fn encode_value_blob(out: &mut Vec<u8>, v: &Value) {
2296 out.push(v.type_id() as u8);
2297 match v {
2298 Value::Int(n) => out.extend_from_slice(&n.to_le_bytes()),
2299 Value::Float(f) => out.extend_from_slice(&f.to_bits().to_le_bytes()),
2300 Value::Bool(b) => out.push(*b as u8),
2301 Value::Str(s) => {
2302 out.extend_from_slice(&(s.len() as u32).to_le_bytes());
2303 out.extend_from_slice(s.as_bytes());
2304 }
2305 Value::DateTime(n) => out.extend_from_slice(&n.to_le_bytes()),
2306 Value::Uuid(u) => out.extend_from_slice(u),
2307 Value::Bytes(b) => {
2308 out.extend_from_slice(&(b.len() as u32).to_le_bytes());
2309 out.extend_from_slice(b);
2310 }
2311 Value::Empty => {}
2312 }
2313}
2314
2315fn decode_value_blob(data: &[u8], pos: &mut usize) -> Option<Value> {
2318 let tag = *data.get(*pos)?;
2319 *pos += 1;
2320 let type_id = TypeId::from_u8(tag)?;
2321 let take_fixed = |pos: &mut usize, n: usize| -> Option<Vec<u8>> {
2322 if *pos + n > data.len() {
2323 return None;
2324 }
2325 let slice = data[*pos..*pos + n].to_vec();
2326 *pos += n;
2327 Some(slice)
2328 };
2329 match type_id {
2330 TypeId::Empty => Some(Value::Empty),
2331 TypeId::Int => Some(Value::Int(i64::from_le_bytes(
2332 take_fixed(pos, 8)?.try_into().ok()?,
2333 ))),
2334 TypeId::Float => Some(Value::Float(f64::from_bits(u64::from_le_bytes(
2335 take_fixed(pos, 8)?.try_into().ok()?,
2336 )))),
2337 TypeId::Bool => Some(Value::Bool(take_fixed(pos, 1)?[0] != 0)),
2338 TypeId::DateTime => Some(Value::DateTime(i64::from_le_bytes(
2339 take_fixed(pos, 8)?.try_into().ok()?,
2340 ))),
2341 TypeId::Uuid => Some(Value::Uuid(take_fixed(pos, 16)?.try_into().ok()?)),
2342 TypeId::Str => {
2343 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
2344 Some(Value::Str(String::from_utf8(take_fixed(pos, len)?).ok()?))
2345 }
2346 TypeId::Bytes => {
2347 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
2348 Some(Value::Bytes(take_fixed(pos, len)?))
2349 }
2350 }
2351}
2352
2353fn encode_defaults_section(out: &mut Vec<u8>, defaults: &[Option<Value>]) {
2357 let present: Vec<(u16, &Value)> = defaults
2358 .iter()
2359 .enumerate()
2360 .filter_map(|(i, d)| d.as_ref().map(|v| (i as u16, v)))
2361 .collect();
2362 out.extend_from_slice(&(present.len() as u16).to_le_bytes());
2363 for (pos, v) in present {
2364 out.extend_from_slice(&pos.to_le_bytes());
2365 encode_value_blob(out, v);
2366 }
2367}
2368
2369fn decode_defaults_section(
2372 data: &[u8],
2373 pos: &mut usize,
2374 n_cols: usize,
2375) -> Option<Vec<Option<Value>>> {
2376 if *pos + 2 > data.len() {
2377 return None;
2378 }
2379 let count = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
2380 *pos += 2;
2381 let mut out = vec![None; n_cols];
2382 for _ in 0..count {
2383 if *pos + 2 > data.len() {
2384 return None;
2385 }
2386 let col = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
2387 *pos += 2;
2388 let value = decode_value_blob(data, pos)?;
2389 if col < n_cols {
2390 out[col] = Some(value);
2391 }
2392 }
2393 Some(out)
2394}
2395
2396fn encode_auto_section(out: &mut Vec<u8>, auto_cols: &[bool]) {
2400 let present: Vec<u16> = auto_cols
2401 .iter()
2402 .enumerate()
2403 .filter_map(|(i, &a)| if a { Some(i as u16) } else { None })
2404 .collect();
2405 out.extend_from_slice(&(present.len() as u16).to_le_bytes());
2406 for pos in present {
2407 out.extend_from_slice(&pos.to_le_bytes());
2408 }
2409}
2410
2411fn decode_auto_section(data: &[u8], pos: &mut usize, n_cols: usize) -> Option<Vec<bool>> {
2414 if *pos + 2 > data.len() {
2415 return None;
2416 }
2417 let count = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
2418 *pos += 2;
2419 let mut out = vec![false; n_cols];
2420 for _ in 0..count {
2421 if *pos + 2 > data.len() {
2422 return None;
2423 }
2424 let col = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
2425 *pos += 2;
2426 if col < n_cols {
2427 out[col] = true;
2428 }
2429 }
2430 Some(out)
2431}
2432
2433fn write_catalog_file(path: &Path, entries: &[CatalogEntryRef<'_>]) -> io::Result<()> {
2434 let mut buf: Vec<u8> = Vec::with_capacity(64);
2435 buf.extend_from_slice(CATALOG_MAGIC);
2436 buf.extend_from_slice(&CATALOG_VERSION.to_le_bytes());
2437 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
2438
2439 for entry in entries {
2440 let schema = entry.schema;
2441 let name = schema.table_name.as_bytes();
2442 buf.extend_from_slice(&(name.len() as u32).to_le_bytes());
2443 buf.extend_from_slice(name);
2444 buf.extend_from_slice(&(schema.columns.len() as u16).to_le_bytes());
2445 for col in &schema.columns {
2446 let cn = col.name.as_bytes();
2447 buf.extend_from_slice(&(cn.len() as u32).to_le_bytes());
2448 buf.extend_from_slice(cn);
2449 buf.push(col.type_id as u8);
2450 buf.push(if col.required { 1 } else { 0 });
2451 buf.extend_from_slice(&col.position.to_le_bytes());
2452 }
2453 buf.extend_from_slice(&(entry.indexed_cols.len() as u16).to_le_bytes());
2455 for meta in &entry.indexed_cols {
2456 let cn = meta.name.as_bytes();
2457 buf.extend_from_slice(&(cn.len() as u32).to_le_bytes());
2458 buf.extend_from_slice(cn);
2459 buf.push(if meta.unique { 1 } else { 0 });
2460 }
2461 encode_defaults_section(&mut buf, entry.defaults);
2463 encode_auto_section(&mut buf, entry.auto_cols);
2465 }
2466
2467 let crc = crc32fast::hash(&buf);
2471 buf.extend_from_slice(&crc.to_le_bytes());
2472
2473 let mut f = fs::OpenOptions::new()
2474 .create(true)
2475 .write(true)
2476 .truncate(true)
2477 .open(path)?;
2478 f.write_all(&buf)?;
2479 f.sync_data()?;
2480 Ok(())
2481}
2482
2483fn read_catalog_file(path: &Path) -> io::Result<Vec<CatalogEntry>> {
2484 let mut f = fs::File::open(path)?;
2485 let mut buf = Vec::new();
2486 f.read_to_end(&mut buf)?;
2487
2488 let mut pos = 0usize;
2489 if buf.len() < 14 || &buf[0..4] != CATALOG_MAGIC {
2491 return Err(io::Error::new(
2492 io::ErrorKind::InvalidData,
2493 "bad catalog magic",
2494 ));
2495 }
2496
2497 let payload = &buf[..buf.len() - 4];
2499 let stored_crc = u32::from_le_bytes(
2500 buf[buf.len() - 4..]
2501 .try_into()
2502 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog CRC"))?,
2503 );
2504 let computed_crc = crc32fast::hash(payload);
2505 if stored_crc != computed_crc {
2506 return Err(io::Error::new(
2507 io::ErrorKind::InvalidData,
2508 format!(
2509 "catalog CRC32 mismatch: expected {stored_crc:#010x}, got {computed_crc:#010x}"
2510 ),
2511 ));
2512 }
2513 let buf = &buf[..buf.len() - 4];
2515 pos += 4;
2516 let version = u16::from_le_bytes(
2517 buf[pos..pos + 2]
2518 .try_into()
2519 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog header"))?,
2520 );
2521 pos += 2;
2522 if version == 0 || version > CATALOG_VERSION {
2532 return Err(io::Error::new(
2533 io::ErrorKind::InvalidData,
2534 format!("unsupported catalog version: {version}"),
2535 ));
2536 }
2537 let n_tables = u32::from_le_bytes(
2538 buf[pos..pos + 4]
2539 .try_into()
2540 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog header"))?,
2541 ) as usize;
2542 pos += 4;
2543
2544 if n_tables > buf.len() {
2551 return Err(io::Error::new(
2552 io::ErrorKind::InvalidData,
2553 format!("catalog file corrupt: implausible table count {n_tables}"),
2554 ));
2555 }
2556
2557 let mut entries = Vec::with_capacity(n_tables);
2558 for _ in 0..n_tables {
2559 let name_len = read_u32(buf, &mut pos)? as usize;
2560 let table_name = read_string(buf, &mut pos, name_len)?;
2561 let n_cols = read_u16(buf, &mut pos)? as usize;
2562
2563 let mut columns = Vec::with_capacity(n_cols);
2564 for _ in 0..n_cols {
2565 let cname_len = read_u32(buf, &mut pos)? as usize;
2566 let name = read_string(buf, &mut pos, cname_len)?;
2567 let type_id_raw = read_u8(buf, &mut pos)?;
2568 let type_id = type_id_from_u8(type_id_raw)?;
2569 let required = read_u8(buf, &mut pos)? != 0;
2570 let position = read_u16(buf, &mut pos)?;
2571 columns.push(ColumnDef {
2572 name,
2573 type_id,
2574 required,
2575 position,
2576 });
2577 }
2578
2579 let indexed_cols: Vec<IndexedColMeta> = if version >= 3 {
2583 let n = read_u16(buf, &mut pos)? as usize;
2584 let mut v = Vec::with_capacity(n);
2585 for _ in 0..n {
2586 let l = read_u32(buf, &mut pos)? as usize;
2587 let name = read_string(buf, &mut pos, l)?;
2588 let unique = read_u8(buf, &mut pos)? != 0;
2589 v.push(IndexedColMeta { name, unique });
2590 }
2591 v
2592 } else if version >= 2 {
2593 let n = read_u16(buf, &mut pos)? as usize;
2594 let mut v = Vec::with_capacity(n);
2595 for _ in 0..n {
2596 let l = read_u32(buf, &mut pos)? as usize;
2597 let name = read_string(buf, &mut pos, l)?;
2598 v.push(IndexedColMeta {
2599 name,
2600 unique: false,
2601 });
2602 }
2603 v
2604 } else {
2605 Vec::new()
2606 };
2607
2608 let defaults = if version >= 4 {
2610 decode_defaults_section(buf, &mut pos, columns.len()).ok_or_else(|| {
2611 io::Error::new(io::ErrorKind::InvalidData, "truncated catalog defaults")
2612 })?
2613 } else {
2614 Vec::new()
2615 };
2616
2617 let auto_cols = if version >= 5 {
2619 decode_auto_section(buf, &mut pos, columns.len()).ok_or_else(|| {
2620 io::Error::new(io::ErrorKind::InvalidData, "truncated catalog auto columns")
2621 })?
2622 } else {
2623 Vec::new()
2624 };
2625
2626 entries.push(CatalogEntry {
2627 schema: Schema {
2628 table_name,
2629 columns,
2630 },
2631 indexed_cols,
2632 defaults,
2633 auto_cols,
2634 });
2635 }
2636
2637 Ok(entries)
2638}
2639
2640fn read_u8(buf: &[u8], pos: &mut usize) -> io::Result<u8> {
2641 if *pos >= buf.len() {
2642 return Err(io::Error::new(
2643 io::ErrorKind::UnexpectedEof,
2644 "truncated catalog",
2645 ));
2646 }
2647 let v = buf[*pos];
2648 *pos += 1;
2649 Ok(v)
2650}
2651fn read_u16(buf: &[u8], pos: &mut usize) -> io::Result<u16> {
2652 if *pos + 2 > buf.len() {
2653 return Err(io::Error::new(
2654 io::ErrorKind::UnexpectedEof,
2655 "truncated catalog",
2656 ));
2657 }
2658 let v = u16::from_le_bytes(
2659 buf[*pos..*pos + 2]
2660 .try_into()
2661 .expect("bounds checked above"),
2662 );
2663 *pos += 2;
2664 Ok(v)
2665}
2666fn read_u32(buf: &[u8], pos: &mut usize) -> io::Result<u32> {
2667 if *pos + 4 > buf.len() {
2668 return Err(io::Error::new(
2669 io::ErrorKind::UnexpectedEof,
2670 "truncated catalog",
2671 ));
2672 }
2673 let v = u32::from_le_bytes(
2674 buf[*pos..*pos + 4]
2675 .try_into()
2676 .expect("bounds checked above"),
2677 );
2678 *pos += 4;
2679 Ok(v)
2680}
2681fn read_string(buf: &[u8], pos: &mut usize, len: usize) -> io::Result<String> {
2682 if *pos + len > buf.len() {
2683 return Err(io::Error::new(
2684 io::ErrorKind::UnexpectedEof,
2685 "truncated catalog string",
2686 ));
2687 }
2688 let s = std::str::from_utf8(&buf[*pos..*pos + len])
2689 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "non-utf8 in catalog"))?
2690 .to_string();
2691 *pos += len;
2692 Ok(s)
2693}
2694fn type_id_from_u8(v: u8) -> io::Result<TypeId> {
2695 match v {
2696 0 => Ok(TypeId::Empty),
2697 1 => Ok(TypeId::Int),
2698 2 => Ok(TypeId::Float),
2699 3 => Ok(TypeId::Bool),
2700 4 => Ok(TypeId::Str),
2701 5 => Ok(TypeId::DateTime),
2702 6 => Ok(TypeId::Uuid),
2703 7 => Ok(TypeId::Bytes),
2704 _ => Err(io::Error::new(
2705 io::ErrorKind::InvalidData,
2706 format!("unknown type id: {v}"),
2707 )),
2708 }
2709}
2710
2711#[cfg(test)]
2712mod tests {
2713 use super::*;
2714 fn temp_catalog(name: &str) -> Catalog {
2715 let dir = std::env::temp_dir().join(format!("powdb_cat_{name}_{}", std::process::id()));
2716 Catalog::create(&dir).unwrap()
2717 }
2718
2719 fn schema_two_cols() -> Schema {
2720 Schema {
2721 table_name: "T".into(),
2722 columns: vec![
2723 ColumnDef {
2724 name: "id".into(),
2725 type_id: TypeId::Int,
2726 required: true,
2727 position: 0,
2728 },
2729 ColumnDef {
2730 name: "status".into(),
2731 type_id: TypeId::Str,
2732 required: false,
2733 position: 1,
2734 },
2735 ],
2736 }
2737 }
2738
2739 #[test]
2740 fn replay_records_treats_reused_tx_ids_as_ordered_spans() {
2741 let mut cat = temp_catalog("reused_tx_ids");
2742 let schema = schema_two_cols();
2743 cat.create_table(schema.clone()).unwrap();
2744 cat.checkpoint().unwrap();
2745
2746 let mut committed_row = Vec::new();
2747 encode_row_into(
2748 &schema,
2749 &[Value::Int(1), Value::Str("committed".into())],
2750 &mut committed_row,
2751 );
2752 let mut incomplete_row = Vec::new();
2753 encode_row_into(
2754 &schema,
2755 &[Value::Int(2), Value::Str("incomplete".into())],
2756 &mut incomplete_row,
2757 );
2758
2759 let records = vec![
2760 WalRecord {
2761 tx_id: 1,
2762 record_type: WalRecordType::Begin,
2763 lsn: 1,
2764 data: Vec::new(),
2765 },
2766 WalRecord {
2767 tx_id: 1,
2768 record_type: WalRecordType::Insert,
2769 lsn: 2,
2770 data: encode_wal_payload(
2771 "T",
2772 RowId {
2773 page_id: 1,
2774 slot_index: 0,
2775 },
2776 &committed_row,
2777 ),
2778 },
2779 WalRecord {
2780 tx_id: 1,
2781 record_type: WalRecordType::Commit,
2782 lsn: 3,
2783 data: Vec::new(),
2784 },
2785 WalRecord {
2786 tx_id: 1,
2787 record_type: WalRecordType::Begin,
2788 lsn: 4,
2789 data: Vec::new(),
2790 },
2791 WalRecord {
2792 tx_id: 1,
2793 record_type: WalRecordType::Insert,
2794 lsn: 5,
2795 data: encode_wal_payload(
2796 "T",
2797 RowId {
2798 page_id: 1,
2799 slot_index: 1,
2800 },
2801 &incomplete_row,
2802 ),
2803 },
2804 ];
2805
2806 cat.apply_wal_records(&records).unwrap();
2807 let rows: Vec<_> = cat.scan("T").unwrap().collect();
2808 assert_eq!(rows.len(), 1);
2809 assert_eq!(rows[0].1[0], Value::Int(1));
2810 assert_eq!(rows[0].1[1], Value::Str("committed".into()));
2811 }
2812
2813 #[test]
2814 fn ddl_create_table_codec_roundtrips_defaults_and_auto() {
2815 let schema = schema_two_cols();
2816 let defaults = vec![None, Some(Value::Str("active".into()))];
2817 let auto_cols = vec![true, false];
2818 let encoded = encode_ddl_create_table(&schema, &defaults, &auto_cols);
2819 let (decoded_schema, decoded_defaults, decoded_auto) =
2820 decode_ddl_create_table(&encoded).unwrap();
2821 assert_eq!(decoded_schema.columns.len(), 2);
2822 assert_eq!(decoded_defaults, defaults);
2823 assert_eq!(decoded_auto, auto_cols);
2824 }
2825
2826 #[test]
2827 fn ddl_create_table_codec_back_compat_without_trailing_sections() {
2828 let schema = schema_two_cols();
2832 let full = encode_ddl_create_table(&schema, &[], &[]);
2833 let legacy = &full[..full.len() - 4];
2836 let (decoded_schema, decoded_defaults, decoded_auto) =
2837 decode_ddl_create_table(legacy).unwrap();
2838 assert_eq!(decoded_schema.columns.len(), 2);
2839 assert!(decoded_defaults.is_empty(), "no defaults section -> empty");
2840 assert!(decoded_auto.is_empty(), "no auto section -> empty");
2841 }
2842
2843 #[test]
2844 fn ddl_create_table_codec_back_compat_defaults_but_no_auto() {
2845 let schema = schema_two_cols();
2848 let defaults = vec![None, Some(Value::Str("active".into()))];
2849 let full = encode_ddl_create_table(&schema, &defaults, &[]);
2850 let legacy = &full[..full.len() - 2];
2852 let (_schema, decoded_defaults, decoded_auto) = decode_ddl_create_table(legacy).unwrap();
2853 assert_eq!(decoded_defaults, defaults);
2854 assert!(decoded_auto.is_empty());
2855 }
2856
2857 #[test]
2858 fn read_catalog_file_accepts_intermediate_versions_3_and_4() {
2859 use std::io::Write as _;
2867 fn write_legacy_catalog(path: &std::path::Path, version: u16) {
2868 let mut buf: Vec<u8> = Vec::new();
2869 buf.extend_from_slice(CATALOG_MAGIC);
2870 buf.extend_from_slice(&version.to_le_bytes());
2871 buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes());
2874 buf.extend_from_slice(b"T");
2875 buf.extend_from_slice(&2u16.to_le_bytes()); buf.extend_from_slice(&2u32.to_le_bytes());
2878 buf.extend_from_slice(b"id");
2879 buf.push(TypeId::Int as u8);
2880 buf.push(1);
2881 buf.extend_from_slice(&0u16.to_le_bytes());
2882 buf.extend_from_slice(&6u32.to_le_bytes());
2884 buf.extend_from_slice(b"status");
2885 buf.push(TypeId::Str as u8);
2886 buf.push(0);
2887 buf.extend_from_slice(&1u16.to_le_bytes());
2888 buf.extend_from_slice(&0u16.to_le_bytes());
2890 if version >= 4 {
2892 encode_defaults_section(&mut buf, &[None, None]);
2893 }
2894 let crc = crc32fast::hash(&buf);
2896 buf.extend_from_slice(&crc.to_le_bytes());
2897 let mut f = fs::File::create(path).unwrap();
2898 f.write_all(&buf).unwrap();
2899 }
2900
2901 for version in [3u16, 4u16] {
2902 let path = std::env::temp_dir().join(format!(
2903 "powdb_cat_v{version}_compat_{}.bin",
2904 std::process::id()
2905 ));
2906 write_legacy_catalog(&path, version);
2907 let entries = read_catalog_file(&path)
2908 .unwrap_or_else(|e| panic!("version {version} catalog must load, got: {e}"));
2909 assert_eq!(entries.len(), 1);
2910 assert_eq!(entries[0].schema.table_name, "T");
2911 assert_eq!(entries[0].schema.columns.len(), 2);
2912 assert!(
2913 entries[0].auto_cols.is_empty(),
2914 "v{version} has no auto cols"
2915 );
2916 fs::remove_file(&path).ok();
2917 }
2918 }
2919
2920 #[test]
2921 fn read_catalog_file_rejects_implausible_table_count() {
2922 use std::io::Write as _;
2930 let mut buf: Vec<u8> = Vec::new();
2931 buf.extend_from_slice(CATALOG_MAGIC);
2932 buf.extend_from_slice(&CATALOG_VERSION.to_le_bytes());
2933 buf.extend_from_slice(&1000u32.to_le_bytes()); let crc = crc32fast::hash(&buf);
2936 buf.extend_from_slice(&crc.to_le_bytes());
2937 let path =
2938 std::env::temp_dir().join(format!("powdb_cat_badcount_{}.bin", std::process::id()));
2939 fs::File::create(&path).unwrap().write_all(&buf).unwrap();
2940
2941 let msg = match read_catalog_file(&path) {
2942 Ok(_) => panic!("implausible table count must be rejected, got Ok"),
2943 Err(e) => e.to_string(),
2944 };
2945 assert!(
2946 msg.contains("implausible table count"),
2947 "expected an implausible-table-count error, got: {msg}"
2948 );
2949 fs::remove_file(&path).ok();
2950 }
2951
2952 #[test]
2953 fn data_dir_and_max_lsn_accessors() {
2954 let dir = std::env::temp_dir().join(format!("powdb_cat_maxlsn_{}", std::process::id()));
2955 let mut cat = Catalog::create(&dir).unwrap();
2956
2957 assert_eq!(cat.data_dir(), dir.as_path());
2959
2960 assert_eq!(cat.max_lsn(), 0);
2962
2963 let schema = Schema {
2964 table_name: "users".into(),
2965 columns: vec![ColumnDef {
2966 name: "name".into(),
2967 type_id: TypeId::Str,
2968 required: true,
2969 position: 0,
2970 }],
2971 };
2972 cat.create_table(schema).unwrap();
2973
2974 cat.insert("users", &vec![Value::Str("Alice".into())])
2975 .unwrap();
2976 cat.sync_wal().unwrap();
2977
2978 assert!(cat.max_lsn() > 0);
2981 }
2982
2983 #[test]
2984 fn test_create_table_and_insert() {
2985 let mut cat = temp_catalog("basic");
2986 let schema = Schema {
2987 table_name: "users".into(),
2988 columns: vec![
2989 ColumnDef {
2990 name: "name".into(),
2991 type_id: TypeId::Str,
2992 required: true,
2993 position: 0,
2994 },
2995 ColumnDef {
2996 name: "age".into(),
2997 type_id: TypeId::Int,
2998 required: false,
2999 position: 1,
3000 },
3001 ],
3002 };
3003 cat.create_table(schema).unwrap();
3004
3005 let row = vec![Value::Str("Alice".into()), Value::Int(30)];
3006 let rid = cat.insert("users", &row).unwrap();
3007
3008 let result = cat.get("users", rid).unwrap();
3009 assert_eq!(result[0], Value::Str("Alice".into()));
3010 assert_eq!(result[1], Value::Int(30));
3011 }
3012
3013 #[test]
3014 fn test_scan_table() {
3015 let mut cat = temp_catalog("scan");
3016 let schema = Schema {
3017 table_name: "items".into(),
3018 columns: vec![
3019 ColumnDef {
3020 name: "name".into(),
3021 type_id: TypeId::Str,
3022 required: true,
3023 position: 0,
3024 },
3025 ColumnDef {
3026 name: "price".into(),
3027 type_id: TypeId::Float,
3028 required: true,
3029 position: 1,
3030 },
3031 ],
3032 };
3033 cat.create_table(schema).unwrap();
3034
3035 for i in 0..50 {
3036 cat.insert(
3037 "items",
3038 &vec![
3039 Value::Str(format!("item_{i}")),
3040 Value::Float(i as f64 * 1.5),
3041 ],
3042 )
3043 .unwrap();
3044 }
3045
3046 let rows: Vec<_> = cat.scan("items").unwrap().collect();
3047 assert_eq!(rows.len(), 50);
3048 }
3049
3050 #[test]
3051 fn test_index_lookup() {
3052 let mut cat = temp_catalog("idx");
3053 let schema = Schema {
3054 table_name: "users".into(),
3055 columns: vec![
3056 ColumnDef {
3057 name: "email".into(),
3058 type_id: TypeId::Str,
3059 required: true,
3060 position: 0,
3061 },
3062 ColumnDef {
3063 name: "name".into(),
3064 type_id: TypeId::Str,
3065 required: true,
3066 position: 1,
3067 },
3068 ],
3069 };
3070 cat.create_table(schema).unwrap();
3071 cat.create_index("users", "email").unwrap();
3072
3073 cat.insert(
3074 "users",
3075 &vec![
3076 Value::Str("alice@example.com".into()),
3077 Value::Str("Alice".into()),
3078 ],
3079 )
3080 .unwrap();
3081 cat.insert(
3082 "users",
3083 &vec![
3084 Value::Str("bob@example.com".into()),
3085 Value::Str("Bob".into()),
3086 ],
3087 )
3088 .unwrap();
3089
3090 let result = cat
3091 .index_lookup("users", "email", &Value::Str("bob@example.com".into()))
3092 .unwrap();
3093 assert!(result.is_some());
3094 let row = result.unwrap();
3095 assert_eq!(row[1], Value::Str("Bob".into()));
3096 }
3097
3098 #[test]
3099 fn test_delete_row() {
3100 let mut cat = temp_catalog("delete");
3101 let schema = Schema {
3102 table_name: "t".into(),
3103 columns: vec![ColumnDef {
3104 name: "v".into(),
3105 type_id: TypeId::Int,
3106 required: true,
3107 position: 0,
3108 }],
3109 };
3110 cat.create_table(schema).unwrap();
3111 let r1 = cat.insert("t", &vec![Value::Int(1)]).unwrap();
3112 let r2 = cat.insert("t", &vec![Value::Int(2)]).unwrap();
3113 cat.delete("t", r1).unwrap();
3114 assert!(cat.get("t", r1).is_none());
3115 assert!(cat.get("t", r2).is_some());
3116 }
3117
3118 #[test]
3119 fn test_update_row() {
3120 let mut cat = temp_catalog("update");
3121 let schema = Schema {
3122 table_name: "t".into(),
3123 columns: vec![ColumnDef {
3124 name: "v".into(),
3125 type_id: TypeId::Int,
3126 required: true,
3127 position: 0,
3128 }],
3129 };
3130 cat.create_table(schema).unwrap();
3131 let rid = cat.insert("t", &vec![Value::Int(1)]).unwrap();
3132 let new_rid = cat.update("t", rid, &vec![Value::Int(99)]).unwrap();
3133 let row = cat.get("t", new_rid).unwrap();
3134 assert_eq!(row[0], Value::Int(99));
3135 }
3136
3137 #[test]
3138 fn test_persist_and_reopen() {
3139 let dir = std::env::temp_dir().join(format!("powdb_cat_persist_{}", std::process::id()));
3140 let _ = std::fs::remove_dir_all(&dir);
3142
3143 {
3144 let mut cat = Catalog::create(&dir).unwrap();
3145 cat.create_table(Schema {
3146 table_name: "users".into(),
3147 columns: vec![
3148 ColumnDef {
3149 name: "name".into(),
3150 type_id: TypeId::Str,
3151 required: true,
3152 position: 0,
3153 },
3154 ColumnDef {
3155 name: "age".into(),
3156 type_id: TypeId::Int,
3157 required: false,
3158 position: 1,
3159 },
3160 ],
3161 })
3162 .unwrap();
3163 cat.insert("users", &vec![Value::Str("Alice".into()), Value::Int(30)])
3164 .unwrap();
3165 cat.insert("users", &vec![Value::Str("Bob".into()), Value::Int(25)])
3166 .unwrap();
3167 }
3168
3169 let cat = Catalog::open(&dir).unwrap();
3171 let schema = cat.schema("users").unwrap();
3172 assert_eq!(schema.columns.len(), 2);
3173 assert_eq!(schema.columns[0].name, "name");
3174 assert_eq!(schema.columns[0].type_id, TypeId::Str);
3175 assert_eq!(schema.columns[1].type_id, TypeId::Int);
3176
3177 let rows: Vec<_> = cat.scan("users").unwrap().collect();
3178 assert_eq!(rows.len(), 2);
3179
3180 std::fs::remove_dir_all(&dir).ok();
3181 }
3182
3183 #[test]
3184 fn test_open_missing_dir_errors() {
3185 let dir = std::env::temp_dir().join(format!("powdb_cat_missing_{}", std::process::id()));
3186 let _ = std::fs::remove_dir_all(&dir);
3187 std::fs::create_dir_all(&dir).unwrap();
3188 assert!(Catalog::open(&dir).is_err());
3190 std::fs::remove_dir_all(&dir).ok();
3191 }
3192
3193 #[test]
3194 fn test_list_tables() {
3195 let mut cat = temp_catalog("list");
3196 cat.create_table(Schema {
3197 table_name: "a".into(),
3198 columns: vec![ColumnDef {
3199 name: "x".into(),
3200 type_id: TypeId::Int,
3201 required: true,
3202 position: 0,
3203 }],
3204 })
3205 .unwrap();
3206 cat.create_table(Schema {
3207 table_name: "b".into(),
3208 columns: vec![ColumnDef {
3209 name: "y".into(),
3210 type_id: TypeId::Int,
3211 required: true,
3212 position: 0,
3213 }],
3214 })
3215 .unwrap();
3216 let mut tables = cat.list_tables();
3217 tables.sort();
3218 assert_eq!(tables, vec!["a", "b"]);
3219 }
3220
3221 #[test]
3222 fn test_path_traversal_table_name_rejected() {
3223 let mut cat = temp_catalog("path_trav");
3224 let bad_names = vec![
3226 "../etc/passwd",
3227 "foo/bar",
3228 "table\0name",
3229 "",
3230 "123starts_with_digit",
3231 "has-dashes",
3232 "has spaces",
3233 "has.dots",
3234 ];
3235 for name in bad_names {
3236 let schema = Schema {
3237 table_name: name.into(),
3238 columns: vec![ColumnDef {
3239 name: "x".into(),
3240 type_id: TypeId::Int,
3241 required: true,
3242 position: 0,
3243 }],
3244 };
3245 let result = cat.create_table(schema);
3246 assert!(result.is_err(), "expected error for table name '{name}'");
3247 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
3248 }
3249 let good_names = vec!["users", "_private", "Table_123", "_"];
3251 for name in good_names {
3252 let schema = Schema {
3253 table_name: name.into(),
3254 columns: vec![ColumnDef {
3255 name: "x".into(),
3256 type_id: TypeId::Int,
3257 required: true,
3258 position: 0,
3259 }],
3260 };
3261 assert!(
3262 cat.create_table(schema).is_ok(),
3263 "expected ok for table name '{name}'"
3264 );
3265 }
3266 }
3267
3268 #[test]
3269 fn test_path_traversal_column_name_rejected() {
3270 let mut cat = temp_catalog("col_path_trav");
3271 let schema = Schema {
3272 table_name: "valid_table".into(),
3273 columns: vec![ColumnDef {
3274 name: "../bad".into(),
3275 type_id: TypeId::Int,
3276 required: true,
3277 position: 0,
3278 }],
3279 };
3280 let result = cat.create_table(schema);
3281 assert!(result.is_err());
3282 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
3283 }
3284
3285 #[test]
3286 fn test_drop_table_validates_name() {
3287 let mut cat = temp_catalog("drop_trav");
3288 let result = cat.drop_table("../etc/passwd");
3289 assert!(result.is_err());
3290 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
3292 }
3293}