1use crate::error::StorageError;
2use crate::heap::HeapFile;
3use crate::page::{OVERFLOW_CHAIN_END, OVERFLOW_PAYLOAD_CAP};
4use crate::row::{encode_row_into, encode_row_v2_into, plan_spill, OverflowStub, MAX_VALUE_SIZE};
5use crate::table::Table;
6use crate::types::*;
7use crate::wal::{Wal, WalDurabilityTicket, WalRecord, WalRecordType, WalSyncMode};
8use rustc_hash::FxHashMap;
9use std::fs;
10use std::io::{self, Read, Write};
11use std::path::{Path, PathBuf};
12use tracing::{info, warn};
13
14fn check_encoded_row_size(encoded: &[u8]) -> io::Result<()> {
19 if encoded.len() > crate::page::MAX_ROW_DATA_SIZE {
20 return Err(crate::error::StorageError::RowTooLarge {
21 size: encoded.len(),
22 max: crate::page::MAX_ROW_DATA_SIZE,
23 }
24 .into());
25 }
26 Ok(())
27}
28
29fn validate_identifier(kind: &str, name: &str) -> io::Result<()> {
33 if name.is_empty() {
34 return Err(io::Error::new(
35 io::ErrorKind::InvalidInput,
36 format!("invalid {kind} name: must not be empty"),
37 ));
38 }
39 let mut chars = name.chars();
40 let first = chars.next().expect("non-empty name");
42 if !first.is_ascii_alphabetic() && first != '_' {
43 return Err(io::Error::new(
44 io::ErrorKind::InvalidInput,
45 format!("invalid {kind} name '{name}': must start with a letter or underscore"),
46 ));
47 }
48 for ch in chars {
49 if !ch.is_ascii_alphanumeric() && ch != '_' {
50 return Err(io::Error::new(
51 io::ErrorKind::InvalidInput,
52 format!(
53 "invalid {kind} name '{name}': must contain only letters, digits, and underscores"
54 ),
55 ));
56 }
57 }
58 Ok(())
59}
60
61fn validate_table_name(name: &str) -> io::Result<()> {
63 validate_identifier("table", name)
64}
65
66fn validate_column_name(name: &str) -> io::Result<()> {
68 validate_identifier("column", name)
69}
70
71const CATALOG_FILE: &str = "catalog.bin";
80pub const CATALOG_LSN_FILE: &str = "catalog.lsn";
81const CATALOG_MAGIC: &[u8; 4] = b"BCAT";
82pub const CATALOG_VERSION: u16 = 5;
86
87const WAL_FILE: &str = "wal.log";
90const SYNC_STATE_DIR: &str = ".powdb-sync";
91const SYNC_IDENTITY_FILE: &str = "identity.json";
92
93const WAL_BATCH_SIZE: usize = 64;
97type WalArchiveCallback<'a> = &'a mut dyn FnMut(&Path, &[WalRecord]) -> io::Result<()>;
98
99fn read_durable_lsn(data_dir: &Path) -> io::Result<u64> {
100 let path = data_dir.join(CATALOG_LSN_FILE);
101 let bytes = match fs::read(path) {
102 Ok(bytes) => bytes,
103 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(0),
104 Err(err) => return Err(err),
105 };
106 if bytes.len() != 8 {
107 return Err(io::Error::new(
108 io::ErrorKind::InvalidData,
109 "catalog LSN sidecar has invalid length",
110 ));
111 }
112 let mut buf = [0u8; 8];
113 buf.copy_from_slice(&bytes);
114 Ok(u64::from_le_bytes(buf))
115}
116
117fn write_durable_lsn(data_dir: &Path, lsn: u64) -> io::Result<()> {
118 let path = data_dir.join(CATALOG_LSN_FILE);
119 let tmp_path = data_dir.join(format!("{CATALOG_LSN_FILE}.tmp"));
120 let mut file = fs::File::create(&tmp_path)?;
121 file.write_all(&lsn.to_le_bytes())?;
122 file.sync_all()?;
123 drop(file);
124 fs::rename(&tmp_path, &path)?;
125 sync_directory(data_dir)?;
126 Ok(())
127}
128
129#[cfg(unix)]
130fn sync_directory(path: &Path) -> io::Result<()> {
131 fs::File::open(path)?.sync_all()
132}
133
134#[cfg(not(unix))]
135fn sync_directory(path: &Path) -> io::Result<()> {
136 let _ = path;
137 Ok(())
138}
139
140fn max_record_lsn(records: &[WalRecord]) -> Option<u64> {
141 records.iter().map(|record| record.lsn).max()
142}
143
144pub struct Catalog {
158 tables: Vec<Table>,
161 name_to_slot: FxHashMap<String, usize>,
164 data_dir: PathBuf,
165 wal: Wal,
170 next_tx_id: u64,
174 active_tx_id: Option<u64>,
177 tx_start_len: Option<u64>,
180 pending_autocommit_tx_ids: Vec<u64>,
183 checkpointed: bool,
187 durable_lsn: u64,
190 pending_free_overflow: Vec<(usize, Vec<u32>)>,
199}
200
201impl Catalog {
202 pub fn create(data_dir: &Path) -> io::Result<Self> {
223 crate::create_data_dir_secure(data_dir)?;
224 let wal_path = data_dir.join(WAL_FILE);
225 let wal = Wal::create(&wal_path, WAL_BATCH_SIZE)?;
226 let cat = Catalog {
227 tables: Vec::new(),
228 name_to_slot: FxHashMap::default(),
229 data_dir: data_dir.to_path_buf(),
230 wal,
231 next_tx_id: 1,
232 active_tx_id: None,
233 tx_start_len: None,
234 pending_autocommit_tx_ids: Vec::new(),
235 pending_free_overflow: Vec::new(),
236 checkpointed: false,
237 durable_lsn: 0,
238 };
239 cat.persist()?;
240 Ok(cat)
241 }
242
243 pub fn open(data_dir: &Path) -> io::Result<Self> {
252 Self::open_inner(data_dir, None)
253 }
254
255 pub fn open_with_wal_archive<F>(data_dir: &Path, mut archive: F) -> io::Result<Self>
264 where
265 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
266 {
267 let archive: WalArchiveCallback<'_> = &mut archive;
268 Self::open_inner(data_dir, Some(archive))
269 }
270
271 fn open_inner(data_dir: &Path, archive: Option<WalArchiveCallback<'_>>) -> io::Result<Self> {
272 let cat_path = data_dir.join(CATALOG_FILE);
273 if !cat_path.exists() {
274 return Err(io::Error::new(io::ErrorKind::NotFound, "no catalog file"));
275 }
276 let entries = read_catalog_file(&cat_path)?;
277 let durable_lsn = read_durable_lsn(data_dir)?;
278 let mut tables: Vec<Table> = Vec::with_capacity(entries.len());
279 let mut name_to_slot =
280 FxHashMap::with_capacity_and_hasher(entries.len(), Default::default());
281 for CatalogEntry {
282 schema,
283 indexed_cols,
284 defaults,
285 auto_cols,
286 } in entries
287 {
288 let name = schema.table_name.clone();
289 let mut table = Table::open_with_indexes(schema, data_dir, &indexed_cols)?;
295 table.set_defaults(defaults);
296 table.set_auto_cols(auto_cols);
297 name_to_slot.insert(name, tables.len());
298 tables.push(table);
299 }
300 let wal_path = data_dir.join(WAL_FILE);
301 let wal = Wal::open(&wal_path, WAL_BATCH_SIZE)?;
302 let mut cat = Catalog {
303 tables,
304 name_to_slot,
305 data_dir: data_dir.to_path_buf(),
306 wal,
307 next_tx_id: 1,
308 active_tx_id: None,
309 tx_start_len: None,
310 pending_autocommit_tx_ids: Vec::new(),
311 pending_free_overflow: Vec::new(),
312 checkpointed: false,
313 durable_lsn,
314 };
315 cat.replay_wal(archive)?;
316 let max_page_lsn = cat
326 .tables
327 .iter()
328 .map(|t| t.heap.max_page_lsn())
329 .max()
330 .unwrap_or(0);
331 let max_known_lsn = max_page_lsn.max(cat.durable_lsn);
332 cat.wal.set_next_lsn_at_least(max_known_lsn + 1);
333 if let Err(e) = cat.sweep_all() {
338 warn!(error = %e, "post-recovery overflow sweep failed (non-fatal)");
339 }
340 Ok(cat)
341 }
342
343 fn replay_wal(&mut self, mut archive: Option<WalArchiveCallback<'_>>) -> io::Result<()> {
369 let records = self.wal.read_all()?;
370 if records.is_empty() {
371 return Ok(());
372 }
373 if archive.is_none() {
374 self.ensure_plain_wal_truncate_allowed(&records)?;
375 }
376 self.replay_records(&records)?;
377 if let Some(archive) = archive.as_mut() {
378 archive(&self.data_dir, &records)?;
379 }
380 self.wal.truncate()?;
381 Ok(())
382 }
383
384 pub fn apply_wal_records(&mut self, records: &[WalRecord]) -> io::Result<()> {
393 self.ensure_no_active_transaction_for_checkpoint()?;
394 self.ensure_no_pending_wal_records()?;
395 self.replay_records(records)
396 }
397
398 pub fn ensure_no_pending_wal_records(&self) -> io::Result<()> {
401 if self.wal.has_pending() || !self.wal.read_all()?.is_empty() {
402 return Err(io::Error::other(
403 "cannot apply replicated WAL records while local WAL records are pending",
404 ));
405 }
406 Ok(())
407 }
408
409 fn replay_records(&mut self, records: &[WalRecord]) -> io::Result<()> {
410 if records.is_empty() {
411 return Ok(());
412 }
413
414 info!(count = records.len(), "applying WAL records");
415
416 let has_boundaries = records.iter().any(|rec| {
425 matches!(
426 rec.record_type,
427 WalRecordType::Begin | WalRecordType::Commit | WalRecordType::Rollback
428 )
429 });
430 let mut committed_row_records = vec![true; records.len()];
431 if has_boundaries {
432 committed_row_records.fill(false);
433 let mut pending_tx_spans: Vec<(u64, Vec<usize>)> = Vec::new();
434 for (index, rec) in records.iter().enumerate() {
435 match rec.record_type {
436 WalRecordType::Insert
437 | WalRecordType::Update
438 | WalRecordType::Delete
439 | WalRecordType::OverflowWrite
440 | WalRecordType::OverflowFree
441 if rec.tx_id == 0 =>
442 {
443 committed_row_records[index] = true;
444 }
445 WalRecordType::Insert
446 | WalRecordType::Update
447 | WalRecordType::Delete
448 | WalRecordType::OverflowWrite
449 | WalRecordType::OverflowFree => {
450 if let Some((_, rows)) = pending_tx_spans
451 .iter_mut()
452 .rev()
453 .find(|(tx_id, _)| *tx_id == rec.tx_id)
454 {
455 rows.push(index);
456 } else {
457 pending_tx_spans.push((rec.tx_id, vec![index]));
458 }
459 }
460 WalRecordType::Begin if rec.tx_id != 0 => {
461 pending_tx_spans.push((rec.tx_id, Vec::new()));
462 }
463 WalRecordType::Commit if rec.tx_id != 0 => {
464 if let Some(span_index) = pending_tx_spans
465 .iter()
466 .rposition(|(tx_id, _)| *tx_id == rec.tx_id)
467 {
468 let (_, rows) = pending_tx_spans.remove(span_index);
469 for row_index in rows {
470 committed_row_records[row_index] = true;
471 }
472 }
473 }
474 WalRecordType::Rollback if rec.tx_id != 0 => {
475 if let Some(span_index) = pending_tx_spans
476 .iter()
477 .rposition(|(tx_id, _)| *tx_id == rec.tx_id)
478 {
479 pending_tx_spans.remove(span_index);
480 }
481 }
482 _ => {}
483 }
484 }
485 }
486
487 let mut replayed_inserts = 0usize;
488 let mut replayed_updates = 0usize;
489 let mut replayed_deletes = 0usize;
490 let mut skipped = 0usize;
491 let mut skipped_uncommitted = 0usize;
492 let mut saw_ddl = false;
493 for (index, rec) in records.iter().enumerate() {
494 if has_boundaries
495 && !committed_row_records[index]
496 && matches!(
497 rec.record_type,
498 WalRecordType::Insert
499 | WalRecordType::Update
500 | WalRecordType::Delete
501 | WalRecordType::OverflowWrite
502 | WalRecordType::OverflowFree
503 )
504 {
505 skipped_uncommitted += 1;
506 continue;
507 }
508 match rec.record_type {
509 WalRecordType::Insert => {
510 if let Some((table_name, rid, row_bytes)) = decode_wal_payload(&rec.data) {
511 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
512 let tbl = &mut self.tables[slot];
513 if rec.lsn > 0 && tbl.heap.page_lsn(rid.page_id) >= rec.lsn {
517 skipped += 1;
518 continue;
519 }
520 tbl.heap.insert_at(rid, &row_bytes)?;
527 tbl.heap.set_page_lsn(rid.page_id, rec.lsn)?;
528 replayed_inserts += 1;
529 }
530 }
531 }
532 WalRecordType::Update => {
533 if let Some((table_name, rid, row_bytes)) = decode_wal_payload(&rec.data) {
534 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
535 let tbl = &mut self.tables[slot];
536 if rec.lsn > 0 && tbl.heap.page_lsn(rid.page_id) >= rec.lsn {
537 skipped += 1;
538 continue;
539 }
540 let new_rid = tbl.heap.update(rid, &row_bytes)?;
541 tbl.heap.set_page_lsn(new_rid.page_id, rec.lsn)?;
542 replayed_updates += 1;
543 }
544 }
545 }
546 WalRecordType::Delete => {
547 if let Some((table_name, rid, _)) = decode_wal_payload(&rec.data) {
548 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
549 let tbl = &mut self.tables[slot];
550 if rec.lsn > 0 && tbl.heap.page_lsn(rid.page_id) >= rec.lsn {
551 skipped += 1;
552 continue;
553 }
554 let _ = tbl.heap.delete(rid);
555 tbl.heap.set_page_lsn(rid.page_id, rec.lsn)?;
556 replayed_deletes += 1;
557 }
558 }
559 }
560 WalRecordType::OverflowWrite => {
561 if let Some((table_name, page_id, next_page, chunk)) =
566 decode_overflow_write_payload(&rec.data)
567 {
568 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
569 let tbl = &mut self.tables[slot];
570 if rec.lsn > 0 && tbl.heap.overflow_page_lsn(page_id) >= rec.lsn {
571 skipped += 1;
572 continue;
573 }
574 tbl.heap
575 .write_overflow_page(page_id, next_page, &chunk, rec.lsn)?;
576 }
577 }
578 }
579 WalRecordType::OverflowFree => {
580 if let Some((table_name, pages)) = decode_overflow_free_payload(&rec.data) {
585 if let Some(slot) = self.name_to_slot.get(&table_name).copied() {
586 self.tables[slot].heap.release_overflow_pages(&pages);
587 }
588 }
589 }
590 WalRecordType::Begin | WalRecordType::Commit | WalRecordType::Rollback => {
591 }
593 WalRecordType::DdlCreateTable => {
594 saw_ddl = true;
595 if let Some((schema, defaults, auto_cols)) = decode_ddl_create_table(&rec.data)
596 {
597 if !self.name_to_slot.contains_key(&schema.table_name) {
598 if let Ok(mut table) = Table::create(schema, &self.data_dir) {
599 table.set_defaults(defaults);
600 table.set_auto_cols(auto_cols);
601 let slot = self.tables.len();
602 let name = table.schema.table_name.clone();
603 self.tables.push(table);
604 self.name_to_slot.insert(name, slot);
605 }
606 }
607 }
608 }
609 WalRecordType::DdlDropTable => {
610 saw_ddl = true;
611 if let Some((table_name, _)) = decode_ddl_table_name(&rec.data) {
612 if let Some(&slot) = self.name_to_slot.get(&table_name) {
613 let heap_path = self.data_dir.join(format!("{table_name}.heap"));
614 if heap_path.exists() {
615 let _ = fs::remove_file(&heap_path);
616 }
617 for col_name in self.tables[slot].indexed_column_names() {
618 let idx_path =
619 self.data_dir.join(format!("{table_name}_{col_name}.idx"));
620 if idx_path.exists() {
621 let _ = fs::remove_file(&idx_path);
622 }
623 }
624 self.name_to_slot.remove(&table_name);
625 let last = self.tables.len() - 1;
626 if slot != last {
627 let moved_name = self.tables[last].schema.table_name.clone();
628 self.tables.swap(slot, last);
629 self.name_to_slot.insert(moved_name, slot);
630 }
631 self.tables.pop();
632 }
633 }
634 }
635 WalRecordType::DdlAddColumn => {
636 saw_ddl = true;
637 if let Some((table_name, col)) = decode_ddl_alter_add_column(&rec.data) {
638 if let Some(&slot) = self.name_to_slot.get(&table_name) {
639 let tbl = &mut self.tables[slot];
640 if !tbl.schema.columns.iter().any(|c| c.name == col.name) {
641 let old_schema = tbl.schema.clone();
642 let has_rows = tbl.heap.scan().next().is_some();
643 tbl.schema.columns.push(col);
644 tbl.refresh_layout();
645 if has_rows {
646 let fill = vec![Value::Empty; tbl.schema.columns.len()];
647 let data_dir = self.data_dir.clone();
648 let _ = tbl.rewrite_rows_for_schema_change(
649 &old_schema,
650 &fill,
651 &data_dir,
652 );
653 }
654 }
655 if rec.lsn > 0 {
662 let _ = tbl.heap.stamp_all_pages_min_lsn(rec.lsn);
663 }
664 }
665 }
666 }
667 WalRecordType::DdlDropColumn => {
668 saw_ddl = true;
669 if let Some((table_name, col_name)) = decode_ddl_alter_drop_column(&rec.data) {
670 if let Some(&slot) = self.name_to_slot.get(&table_name) {
671 let tbl = &mut self.tables[slot];
672 if let Some(idx) =
673 tbl.schema.columns.iter().position(|c| c.name == col_name)
674 {
675 let old_schema = tbl.schema.clone();
676 let has_rows = tbl.heap.scan().next().is_some();
677 tbl.schema.columns.remove(idx);
678 for (i, c) in tbl.schema.columns.iter_mut().enumerate() {
679 c.position = i as u16;
680 }
681 tbl.refresh_layout();
682 if has_rows {
683 let fill = vec![Value::Empty; tbl.schema.columns.len()];
684 let data_dir = self.data_dir.clone();
685 let _ = tbl.rewrite_rows_for_schema_change(
686 &old_schema,
687 &fill,
688 &data_dir,
689 );
690 }
691 }
692 if rec.lsn > 0 {
693 let _ = tbl.heap.stamp_all_pages_min_lsn(rec.lsn);
694 }
695 }
696 }
697 }
698 }
699 }
700 info!(
701 inserts = replayed_inserts,
702 updates = replayed_updates,
703 deletes = replayed_deletes,
704 skipped = skipped,
705 skipped_uncommitted = skipped_uncommitted,
706 "WAL record apply complete (commit-boundary + LSN idempotent)"
707 );
708 if saw_ddl {
709 self.persist()?;
710 }
711 for tbl in &mut self.tables {
725 tbl.heap.flush_all_dirty()?;
726 tbl.heap.flush()?;
727 tbl.rebuild_indexes_from_heap()?;
728 tbl.save_dirty_indexes()?;
733 }
734 if let Some(max_lsn) = max_record_lsn(records) {
735 self.record_durable_lsn_at_least(max_lsn)?;
736 self.wal.set_next_lsn_at_least(max_lsn.saturating_add(1));
737 }
738 Ok(())
739 }
740
741 pub fn checkpoint(&mut self) -> io::Result<()> {
750 self.ensure_no_active_transaction_for_checkpoint()?;
751 self.ensure_plain_checkpoint_allowed_before_flush()?;
752 self.flush_checkpoint_state()?;
753 self.wal.flush()?;
754 self.record_durable_lsn_at_least(self.wal.last_appended_lsn())?;
755 self.wal.truncate()?;
756 self.checkpointed = true;
757 Ok(())
758 }
759
760 pub fn checkpoint_with_wal_archive<F>(&mut self, mut archive: F) -> io::Result<()>
768 where
769 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
770 {
771 self.ensure_no_active_transaction_for_checkpoint()?;
772 self.commit_autocommit()?;
773 self.flush_checkpoint_state()?;
774 self.wal.flush()?;
775 let records = self.wal.read_all()?;
776 let archive: WalArchiveCallback<'_> = &mut archive;
777 archive(&self.data_dir, &records)?;
778 if let Some(max_lsn) = max_record_lsn(&records) {
779 self.record_durable_lsn_at_least(max_lsn)?;
780 } else {
781 self.record_durable_lsn_at_least(self.wal.last_appended_lsn())?;
782 }
783 self.wal.truncate()?;
784 self.checkpointed = true;
785 Ok(())
786 }
787
788 fn ensure_no_active_transaction_for_checkpoint(&self) -> io::Result<()> {
789 if self.active_tx_id.is_some() {
790 return Err(io::Error::other(
791 "cannot checkpoint while an explicit transaction is active",
792 ));
793 }
794 Ok(())
795 }
796
797 fn flush_checkpoint_state(&mut self) -> io::Result<()> {
798 for tbl in &mut self.tables {
799 tbl.heap.flush_all_dirty()?;
800 tbl.heap.flush()?;
801 tbl.save_dirty_indexes()?;
806 }
807 Ok(())
808 }
809
810 fn ensure_plain_checkpoint_allowed_before_flush(&self) -> io::Result<()> {
811 if !self.sync_identity_file_exists() {
812 return Ok(());
813 }
814 if self.wal.has_pending() {
815 return Err(io::Error::other(
816 "sync identity exists but checkpoint/recovery was called without a WAL archive hook; refusing to truncate retained history",
817 ));
818 }
819 let records = self.wal.read_all()?;
820 self.ensure_plain_wal_truncate_allowed(&records)
821 }
822
823 fn ensure_plain_wal_truncate_allowed(&self, records: &[WalRecord]) -> io::Result<()> {
824 if records.is_empty() {
825 return Ok(());
826 }
827 if self.sync_identity_file_exists() {
828 return Err(io::Error::other(
829 "sync identity exists but checkpoint/recovery was called without a WAL archive hook; refusing to truncate retained history",
830 ));
831 }
832 Ok(())
833 }
834
835 fn sync_identity_file_exists(&self) -> bool {
836 self.data_dir
837 .join(SYNC_STATE_DIR)
838 .join(SYNC_IDENTITY_FILE)
839 .exists()
840 }
841
842 fn record_durable_lsn_at_least(&mut self, lsn: u64) -> io::Result<()> {
843 if lsn <= self.durable_lsn {
844 return Ok(());
845 }
846 self.durable_lsn = lsn;
847 write_durable_lsn(&self.data_dir, lsn)
848 }
849
850 #[inline]
852 fn free_overflow_chain(&mut self, slot: usize, pages: Vec<u32>) {
863 if pages.is_empty() {
864 return;
865 }
866 if self.active_tx_id.is_some() {
867 self.pending_free_overflow.push((slot, pages));
868 } else {
869 self.tables[slot].release_overflow_pages(&pages);
870 }
871 }
872
873 fn next_tx(&mut self) -> u64 {
874 if let Some(id) = self.active_tx_id {
875 return id;
876 }
877 let id = self.next_tx_id;
878 self.next_tx_id = self.next_tx_id.wrapping_add(1);
879 id
880 }
881
882 pub fn begin_transaction(&mut self) -> io::Result<()> {
884 if self.active_tx_id.is_some() {
885 return Err(io::Error::new(
886 io::ErrorKind::InvalidInput,
887 "explicit transaction is already active",
888 ));
889 }
890 let start_len = self.wal.synced_len()?;
891 let id = self.next_tx_id;
892 self.next_tx_id = self.next_tx_id.wrapping_add(1);
893 self.active_tx_id = Some(id);
894 self.tx_start_len = Some(start_len);
895 self.pending_autocommit_tx_ids.clear();
896 if !self.wal.is_off() {
897 self.wal.append(id, WalRecordType::Begin, &[])?;
898 self.wal.flush()?;
899 }
900 Ok(())
901 }
902
903 pub fn commit_transaction(&mut self) -> io::Result<()> {
906 if let Some(id) = self.active_tx_id.take() {
907 if !self.wal.is_off() {
908 self.wal.append(id, WalRecordType::Commit, &[])?;
909 self.wal.flush()?;
910 }
911 }
912 self.tx_start_len = None;
913 for (slot, pages) in std::mem::take(&mut self.pending_free_overflow) {
917 self.tables[slot].release_overflow_pages(&pages);
918 }
919 Ok(())
920 }
921
922 pub fn commit_autocommit(&mut self) -> io::Result<()> {
926 if !self.wal.is_off() && !self.pending_autocommit_tx_ids.is_empty() {
927 self.pending_autocommit_tx_ids.sort_unstable();
928 self.pending_autocommit_tx_ids.dedup();
929 for id in self.pending_autocommit_tx_ids.drain(..) {
930 self.wal.append(id, WalRecordType::Commit, &[])?;
931 }
932 }
933 self.wal.flush()
934 }
935
936 fn wal_log(
951 &mut self,
952 tx_id: u64,
953 record_type: WalRecordType,
954 table: &str,
955 rid: RowId,
956 row_bytes: &[u8],
957 ) -> io::Result<()> {
958 if self.wal.is_off() {
965 return Ok(());
966 }
967 let payload = encode_wal_payload(table, rid, row_bytes);
968 self.wal.append(tx_id, record_type, &payload)?;
969 if self.active_tx_id.is_none() {
970 self.pending_autocommit_tx_ids.push(tx_id);
971 }
972 Ok(())
973 }
974
975 #[inline]
981 pub fn sync_wal(&mut self) -> io::Result<()> {
982 self.wal.flush()
983 }
984
985 pub fn set_wal_sync_mode(&mut self, mode: WalSyncMode) {
993 self.wal.set_sync_mode(mode);
994 }
995
996 pub fn set_wal_sync_deferred(&mut self, defer: bool) {
1004 self.wal.set_defer_sync(defer);
1005 }
1006
1007 pub fn take_wal_durability_ticket(&mut self) -> Option<WalDurabilityTicket> {
1010 self.wal.take_durability_ticket()
1011 }
1012
1013 pub fn wal_fsync_count(&self) -> u64 {
1015 self.wal.fsync_count()
1016 }
1017
1018 pub fn rollback_to_last_sync(&mut self) -> io::Result<()> {
1033 self.rollback_to_last_sync_inner(None)
1034 }
1035
1036 pub fn rollback_to_last_sync_with_wal_archive<F>(&mut self, mut archive: F) -> io::Result<()>
1041 where
1042 F: FnMut(&Path, &[WalRecord]) -> io::Result<()>,
1043 {
1044 let archive: WalArchiveCallback<'_> = &mut archive;
1045 self.rollback_to_last_sync_inner(Some(archive))
1046 }
1047
1048 fn rollback_to_last_sync_inner(
1049 &mut self,
1050 mut archive: Option<WalArchiveCallback<'_>>,
1051 ) -> io::Result<()> {
1052 let start_len = self.tx_start_len.unwrap_or(0);
1053 let prearchived = if let Some(archive) = archive.as_mut() {
1054 let records = self.wal.read_through_len(start_len)?;
1055 if !records.is_empty() {
1056 archive(&self.data_dir, &records)?;
1057 }
1058 true
1059 } else {
1060 false
1061 };
1062
1063 let start_len = self.tx_start_len.take().unwrap_or(0);
1064 if let Some(id) = self.active_tx_id.take() {
1065 if !self.wal.is_off() {
1066 let _ = self.wal.append(id, WalRecordType::Rollback, &[]);
1067 }
1068 }
1069 self.wal.discard_and_truncate_to(start_len)?;
1070
1071 for tbl in &mut self.tables {
1081 tbl.heap.discard_dirty();
1082 tbl.discard_dirty_indexes();
1083 }
1084 self.wal.discard_pending()?;
1090 let data_dir = self.data_dir.clone();
1094 let sync_mode = self.wal.sync_mode();
1095 let restored = if prearchived {
1096 let mut already_archived = |_dir: &Path, _records: &[WalRecord]| Ok(());
1097 let archive: WalArchiveCallback<'_> = &mut already_archived;
1098 Self::open_inner(&data_dir, Some(archive))?
1099 } else {
1100 Self::open_inner(&data_dir, archive)?
1101 };
1102 *self = restored;
1103 self.wal.set_sync_mode(sync_mode);
1104 Ok(())
1105 }
1106
1107 fn abandon_active_transaction_for_drop(&mut self) -> io::Result<()> {
1108 for tbl in &mut self.tables {
1109 tbl.heap.discard_dirty();
1110 }
1111 self.pending_autocommit_tx_ids.clear();
1112 let truncate_result = match self.tx_start_len.take() {
1113 Some(start_len) => self.wal.discard_and_truncate_to(start_len),
1114 None => self.wal.discard_pending(),
1115 };
1116 self.active_tx_id = None;
1117 truncate_result
1118 }
1119
1120 pub fn data_dir(&self) -> &Path {
1122 &self.data_dir
1123 }
1124
1125 pub fn max_lsn(&self) -> u64 {
1130 let max_page_lsn = self
1131 .tables
1132 .iter()
1133 .map(|t| t.heap.max_page_lsn())
1134 .max()
1135 .unwrap_or(0);
1136 max_page_lsn
1137 .max(self.durable_lsn)
1138 .max(self.wal.last_appended_lsn())
1139 }
1140
1141 pub fn create_table(&mut self, schema: Schema) -> io::Result<()> {
1142 self.create_table_full(schema, Vec::new(), Vec::new())
1143 }
1144
1145 pub fn create_table_with_defaults(
1149 &mut self,
1150 schema: Schema,
1151 defaults: Vec<Option<Value>>,
1152 ) -> io::Result<()> {
1153 self.create_table_full(schema, defaults, Vec::new())
1154 }
1155
1156 pub fn create_table_full(
1161 &mut self,
1162 schema: Schema,
1163 defaults: Vec<Option<Value>>,
1164 auto_cols: Vec<bool>,
1165 ) -> io::Result<()> {
1166 validate_table_name(&schema.table_name)?;
1167 for col in &schema.columns {
1168 validate_column_name(&col.name)?;
1169 }
1170 let name = schema.table_name.clone();
1171 if self.name_to_slot.contains_key(&name) {
1172 return Err(io::Error::new(
1173 io::ErrorKind::AlreadyExists,
1174 format!("table '{name}' already exists"),
1175 ));
1176 }
1177 if !self.wal.is_off() {
1178 let payload = encode_ddl_create_table(&schema, &defaults, &auto_cols);
1179 self.wal
1180 .append(0, WalRecordType::DdlCreateTable, &payload)?;
1181 self.wal.flush()?;
1182 }
1183 let mut table = Table::create(schema, &self.data_dir)?;
1184 table.set_defaults(defaults);
1185 table.set_auto_cols(auto_cols);
1186 let slot = self.tables.len();
1187 self.tables.push(table);
1188 self.name_to_slot.insert(name, slot);
1189 self.persist()?;
1190 Ok(())
1191 }
1192
1193 pub fn column_defaults(&self, table: &str) -> Option<&[Option<Value>]> {
1197 let slot = *self.name_to_slot.get(table)?;
1198 Some(self.tables[slot].defaults())
1199 }
1200
1201 pub fn auto_columns(&self, table: &str) -> Option<&[bool]> {
1204 let slot = *self.name_to_slot.get(table)?;
1205 Some(self.tables[slot].auto_cols())
1206 }
1207
1208 pub fn assign_auto_columns(&mut self, table: &str, values: &mut [Value]) {
1212 if let Some(&slot) = self.name_to_slot.get(table) {
1213 self.tables[slot].assign_auto(values);
1214 }
1215 }
1216
1217 fn persist(&self) -> io::Result<()> {
1222 let cat_path = self.data_dir.join(CATALOG_FILE);
1223 let tmp_path = self.data_dir.join(format!("{CATALOG_FILE}.tmp"));
1224 let entries: Vec<CatalogEntryRef<'_>> = self
1225 .tables
1226 .iter()
1227 .map(|t| CatalogEntryRef {
1228 schema: &t.schema,
1229 indexed_cols: t.indexed_column_metas(),
1230 defaults: t.defaults(),
1231 auto_cols: t.auto_cols(),
1232 })
1233 .collect();
1234 write_catalog_file(&tmp_path, &entries)?;
1235 fs::rename(&tmp_path, &cat_path)?;
1236 Ok(())
1237 }
1238
1239 #[inline]
1243 pub fn table_slot(&self, name: &str) -> Option<usize> {
1244 self.name_to_slot.get(name).copied()
1245 }
1246
1247 #[inline]
1250 pub fn table_by_slot(&self, slot: usize) -> &Table {
1251 &self.tables[slot]
1252 }
1253
1254 #[inline]
1256 pub fn table_by_slot_mut(&mut self, slot: usize) -> &mut Table {
1257 &mut self.tables[slot]
1258 }
1259
1260 pub fn get_table(&self, name: &str) -> Option<&Table> {
1261 let slot = *self.name_to_slot.get(name)?;
1262 Some(&self.tables[slot])
1263 }
1264
1265 pub fn get_table_mut(&mut self, name: &str) -> Option<&mut Table> {
1266 let slot = *self.name_to_slot.get(name)?;
1267 Some(&mut self.tables[slot])
1268 }
1269
1270 #[inline]
1274 pub fn table_has_overflow(&self, table: &str) -> bool {
1275 self.get_table(table)
1276 .map(|t| t.has_overflow_rows())
1277 .unwrap_or(false)
1278 }
1279
1280 #[inline]
1285 fn by_name(&self, table: &str) -> io::Result<&Table> {
1286 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1287 io::Error::new(
1288 io::ErrorKind::NotFound,
1289 format!("table '{table}' not found"),
1290 )
1291 })?;
1292 Ok(&self.tables[slot])
1293 }
1294
1295 #[inline]
1297 fn by_name_mut(&mut self, table: &str) -> io::Result<&mut Table> {
1298 let slot = self.slot_of(table)?;
1299 Ok(&mut self.tables[slot])
1300 }
1301
1302 pub fn sweep(&mut self, table: &str) -> io::Result<usize> {
1307 let slot = self.slot_of(table)?;
1308 let reclaimed = self.tables[slot].sweep_overflow()?;
1309 if !reclaimed.is_empty() && !self.wal.is_off() {
1310 let payload = encode_overflow_free_payload(table, &reclaimed);
1311 self.wal.append(0, WalRecordType::OverflowFree, &payload)?;
1312 self.wal.flush()?;
1313 }
1314 Ok(reclaimed.len())
1315 }
1316
1317 pub fn sweep_all(&mut self) -> io::Result<usize> {
1319 let names: Vec<String> = self
1320 .tables
1321 .iter()
1322 .map(|t| t.schema.table_name.clone())
1323 .collect();
1324 let mut total = 0;
1325 for name in names {
1326 total += self.sweep(&name)?;
1327 }
1328 Ok(total)
1329 }
1330
1331 fn slot_of(&self, table: &str) -> io::Result<usize> {
1332 self.name_to_slot.get(table).copied().ok_or_else(|| {
1333 io::Error::new(
1334 io::ErrorKind::NotFound,
1335 format!("table '{table}' not found"),
1336 )
1337 })
1338 }
1339
1340 pub fn insert(&mut self, table: &str, values: &Row) -> io::Result<RowId> {
1341 if self.wal.is_off() {
1350 return self.by_name_mut(table)?.insert(values);
1351 }
1352 let tx_id = self.next_tx();
1356 let slot = self.slot_of(table)?;
1357 let row_bytes = {
1358 let Catalog { tables, wal, .. } = self;
1359 encode_row_with_spill_logged(&mut tables[slot], wal, tx_id, values)?
1360 };
1361 let new_rid = self.tables[slot].insert_encoded(values, &row_bytes)?;
1366 self.wal_log(tx_id, WalRecordType::Insert, table, new_rid, &row_bytes)?;
1367 let lsn = self.wal.last_appended_lsn();
1368 if lsn > 0 {
1369 self.tables[slot].heap.set_page_lsn(new_rid.page_id, lsn)?;
1370 }
1371 Ok(new_rid)
1372 }
1373
1374 pub fn insert_by_slot(&mut self, slot: usize, values: &Row) -> io::Result<RowId> {
1382 if self.wal.is_off() {
1383 return self.tables[slot].insert(values);
1384 }
1385 let tx_id = self.next_tx();
1386 let autocommit = self.active_tx_id.is_none();
1387 let Catalog { tables, wal, .. } = self;
1388 let tbl = &mut tables[slot];
1389 let row_bytes = encode_row_with_spill_logged(tbl, wal, tx_id, values)?;
1392 let new_rid = tbl.insert_encoded(values, &row_bytes)?;
1395 let payload = encode_wal_payload(&tbl.schema.table_name, new_rid, &row_bytes);
1396 wal.append(tx_id, WalRecordType::Insert, &payload)?;
1397 if autocommit {
1398 self.pending_autocommit_tx_ids.push(tx_id);
1399 }
1400 let lsn = wal.last_appended_lsn();
1401 if lsn > 0 {
1402 tbl.heap.set_page_lsn(new_rid.page_id, lsn)?;
1403 }
1404 Ok(new_rid)
1405 }
1406
1407 pub fn get(&self, table: &str, rid: RowId) -> Option<Row> {
1408 self.get_table(table)?.get(rid)
1409 }
1410
1411 pub fn delete(&mut self, table: &str, rid: RowId) -> io::Result<()> {
1412 let slot = self.slot_of(table)?;
1413 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
1417 if self.wal.is_off() {
1420 self.tables[slot].delete(rid)?;
1421 self.free_overflow_chain(slot, old_pages);
1422 return Ok(());
1423 }
1424 let tx_id = self.next_tx();
1425 self.wal_log(tx_id, WalRecordType::Delete, table, rid, &[])?;
1427 self.tables[slot].delete(rid)?;
1428 self.free_overflow_chain(slot, old_pages);
1429 Ok(())
1430 }
1431
1432 pub fn delete_many(&mut self, table: &str, rids: &[RowId]) -> io::Result<u64> {
1436 let slot = self.slot_of(table)?;
1444 let old_pages = self.collect_overflow_pages(slot, rids)?;
1447 if self.wal.is_off() {
1448 let count = self.tables[slot].delete_many(rids)?;
1449 self.free_overflow_chain(slot, old_pages);
1450 return Ok(count);
1451 }
1452 let tx_id = self.next_tx();
1453 for &rid in rids {
1454 let payload = encode_wal_payload(table, rid, &[]);
1455 self.wal.append(tx_id, WalRecordType::Delete, &payload)?;
1456 }
1457 if self.active_tx_id.is_none() && !rids.is_empty() {
1458 self.pending_autocommit_tx_ids.push(tx_id);
1459 }
1460 let count = self.tables[slot].delete_many(rids)?;
1461 self.free_overflow_chain(slot, old_pages);
1462 Ok(count)
1463 }
1464
1465 fn collect_overflow_pages(&self, slot: usize, rids: &[RowId]) -> io::Result<Vec<u32>> {
1468 if !self.tables[slot].has_overflow_rows() {
1469 return Ok(Vec::new());
1470 }
1471 let mut pages = Vec::new();
1472 for &rid in rids {
1473 pages.extend(self.tables[slot].overflow_chain_pages_at(rid)?);
1474 }
1475 Ok(pages)
1476 }
1477
1478 pub fn scan_delete_matching<P>(&mut self, table: &str, pred: P) -> io::Result<u64>
1488 where
1489 P: FnMut(&[u8]) -> bool,
1490 {
1491 self.by_name_mut(table)?.scan_delete_matching(pred)
1492 }
1493
1494 pub fn scan_delete_matching_logged<P>(&mut self, table: &str, pred: P) -> io::Result<u64>
1505 where
1506 P: FnMut(&[u8]) -> bool,
1507 {
1508 if self.wal.is_off() {
1514 return self.by_name_mut(table)?.scan_delete_matching(pred);
1515 }
1516 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1520 io::Error::new(
1521 io::ErrorKind::NotFound,
1522 format!("table '{table}' not found"),
1523 )
1524 })?;
1525 let tx_id = self.next_tx();
1526 let autocommit = self.active_tx_id.is_none();
1527 let Catalog { tables, wal, .. } = self;
1530 let tbl = &mut tables[slot];
1531 let name_bytes = table.as_bytes();
1535 let count = tbl.scan_delete_matching_with_hook(pred, |rid, row_bytes| {
1536 let mut payload: Vec<u8> =
1537 Vec::with_capacity(4 + name_bytes.len() + 10 + row_bytes.len());
1538 payload.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
1539 payload.extend_from_slice(name_bytes);
1540 payload.extend_from_slice(&rid.page_id.to_le_bytes());
1541 payload.extend_from_slice(&rid.slot_index.to_le_bytes());
1542 payload.extend_from_slice(&0u32.to_le_bytes());
1546 let _ = wal.append(tx_id, WalRecordType::Delete, &payload);
1553 })?;
1554 if autocommit && count > 0 {
1555 self.pending_autocommit_tx_ids.push(tx_id);
1556 }
1557 Ok(count)
1559 }
1560
1561 pub fn scan_patch_matching_logged<P, M>(
1569 &mut self,
1570 table: &str,
1571 pred: P,
1572 try_mutate: M,
1573 ) -> io::Result<(u64, Vec<RowId>)>
1574 where
1575 P: FnMut(&[u8]) -> bool,
1576 M: FnMut(&mut [u8]) -> Option<u16>,
1577 {
1578 if self.wal.is_off() {
1579 return self.by_name_mut(table)?.scan_patch_matching_with_hook(
1580 pred,
1581 try_mutate,
1582 |_, _| {},
1583 );
1584 }
1585 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1586 io::Error::new(
1587 io::ErrorKind::NotFound,
1588 format!("table '{table}' not found"),
1589 )
1590 })?;
1591 let tx_id = self.next_tx();
1592 let autocommit = self.active_tx_id.is_none();
1593 let Catalog { tables, wal, .. } = self;
1594 let tbl = &mut tables[slot];
1595 let name_bytes = table.as_bytes();
1596 let result = tbl.scan_patch_matching_with_hook(pred, try_mutate, |rid, row_bytes| {
1597 let mut payload: Vec<u8> =
1598 Vec::with_capacity(4 + name_bytes.len() + 10 + row_bytes.len());
1599 payload.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
1600 payload.extend_from_slice(name_bytes);
1601 payload.extend_from_slice(&rid.page_id.to_le_bytes());
1602 payload.extend_from_slice(&rid.slot_index.to_le_bytes());
1603 payload.extend_from_slice(&(row_bytes.len() as u32).to_le_bytes());
1604 payload.extend_from_slice(row_bytes);
1605 let _ = wal.append(tx_id, WalRecordType::Update, &payload);
1606 })?;
1607 if autocommit && result.0 > 0 {
1608 self.pending_autocommit_tx_ids.push(tx_id);
1609 }
1610 Ok(result)
1611 }
1612
1613 pub fn update(&mut self, table: &str, rid: RowId, values: &Row) -> io::Result<RowId> {
1614 if self.wal.is_off() {
1617 let slot = self.slot_of(table)?;
1618 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
1619 let new_rid = self.tables[slot].update(rid, values)?;
1620 self.free_overflow_chain(slot, old_pages);
1621 return Ok(new_rid);
1622 }
1623 let tx_id = self.next_tx();
1624 let slot = self.slot_of(table)?;
1625 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
1629 let row_bytes = {
1634 let Catalog { tables, wal, .. } = self;
1635 encode_row_with_spill_logged(&mut tables[slot], wal, tx_id, values)?
1636 };
1637 check_encoded_row_size(&row_bytes)?;
1641 self.wal_log(tx_id, WalRecordType::Update, table, rid, &row_bytes)?;
1642 let new_rid = self.tables[slot].update_encoded(rid, values, &row_bytes, None)?;
1643 self.free_overflow_chain(slot, old_pages);
1644 Ok(new_rid)
1645 }
1646
1647 pub fn update_hinted(
1651 &mut self,
1652 table: &str,
1653 rid: RowId,
1654 values: &Row,
1655 changed_col_indices: Option<&[usize]>,
1656 ) -> io::Result<RowId> {
1657 if self.wal.is_off() {
1661 let slot = self.slot_of(table)?;
1662 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
1663 let new_rid = self.tables[slot].update_hinted(rid, values, changed_col_indices)?;
1664 self.free_overflow_chain(slot, old_pages);
1665 return Ok(new_rid);
1666 }
1667 let tx_id = self.next_tx();
1668 let slot = self.slot_of(table)?;
1669 let old_pages = self.tables[slot].overflow_chain_pages_at(rid)?;
1670 let row_bytes = {
1671 let Catalog { tables, wal, .. } = self;
1672 encode_row_with_spill_logged(&mut tables[slot], wal, tx_id, values)?
1673 };
1674 check_encoded_row_size(&row_bytes)?;
1676 self.wal_log(tx_id, WalRecordType::Update, table, rid, &row_bytes)?;
1677 let new_rid =
1678 self.tables[slot].update_encoded(rid, values, &row_bytes, changed_col_indices)?;
1679 self.free_overflow_chain(slot, old_pages);
1680 Ok(new_rid)
1681 }
1682
1683 #[inline]
1695 pub fn with_row_bytes_mut<F>(&mut self, table: &str, rid: RowId, f: F) -> io::Result<bool>
1696 where
1697 F: FnOnce(&mut [u8]),
1698 {
1699 self.by_name_mut(table)?.with_row_bytes_mut(rid, f)
1700 }
1701
1702 #[inline]
1716 pub fn update_row_bytes_logged<F>(&mut self, table: &str, rid: RowId, f: F) -> io::Result<bool>
1717 where
1718 F: FnOnce(&mut [u8]),
1719 {
1720 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1721 io::Error::new(
1722 io::ErrorKind::NotFound,
1723 format!("table '{table}' not found"),
1724 )
1725 })?;
1726 self.update_row_bytes_logged_by_slot(slot, rid, f)
1727 }
1728
1729 #[inline]
1734 pub fn update_row_bytes_logged_by_slot<F>(
1735 &mut self,
1736 slot: usize,
1737 rid: RowId,
1738 f: F,
1739 ) -> io::Result<bool>
1740 where
1741 F: FnOnce(&mut [u8]),
1742 {
1743 let tbl = &mut self.tables[slot];
1746 let ok = tbl.with_row_bytes_mut(rid, f)?;
1747 if !ok {
1748 return Ok(false);
1749 }
1750 if self.wal.is_off() {
1754 return Ok(true);
1755 }
1756 let new_bytes = match tbl.heap.get(rid) {
1759 Some(b) => b,
1760 None => return Ok(false),
1762 };
1763 let table_name = tbl.schema.table_name.clone();
1766 let tx_id = self.next_tx();
1767 self.wal_log(tx_id, WalRecordType::Update, &table_name, rid, &new_bytes)?;
1768 Ok(true)
1769 }
1770
1771 #[inline]
1783 pub fn patch_var_col_in_place(
1784 &mut self,
1785 table: &str,
1786 rid: RowId,
1787 col_idx: usize,
1788 new_value: Option<&[u8]>,
1789 ) -> io::Result<bool> {
1790 self.by_name_mut(table)?
1791 .patch_var_col_in_place(rid, col_idx, new_value)
1792 }
1793
1794 pub fn patch_var_col_logged(
1800 &mut self,
1801 table: &str,
1802 rid: RowId,
1803 col_idx: usize,
1804 new_value: Option<&[u8]>,
1805 ) -> io::Result<bool> {
1806 let slot = *self.name_to_slot.get(table).ok_or_else(|| {
1807 io::Error::new(
1808 io::ErrorKind::NotFound,
1809 format!("table '{table}' not found"),
1810 )
1811 })?;
1812 let tbl = &mut self.tables[slot];
1813 let ok = tbl.patch_var_col_in_place(rid, col_idx, new_value)?;
1814 if !ok {
1815 return Ok(false);
1816 }
1817 if self.wal.is_off() {
1820 return Ok(true);
1821 }
1822 let new_bytes = match tbl.heap.get(rid) {
1823 Some(b) => b,
1824 None => return Ok(false),
1825 };
1826 let table_name = tbl.schema.table_name.clone();
1827 let tx_id = self.next_tx();
1828 self.wal_log(tx_id, WalRecordType::Update, &table_name, rid, &new_bytes)?;
1829 Ok(true)
1830 }
1831
1832 pub fn scan(&self, table: &str) -> io::Result<impl Iterator<Item = (RowId, Row)> + '_> {
1833 Ok(self.by_name(table)?.scan())
1834 }
1835
1836 pub fn for_each_row_raw<F>(&self, table: &str, f: F) -> io::Result<()>
1839 where
1840 F: FnMut(RowId, &[u8]),
1841 {
1842 self.by_name(table)?.for_each_row_raw(f);
1843 Ok(())
1844 }
1845
1846 pub fn try_for_each_row_raw<F>(&self, table: &str, f: F) -> io::Result<()>
1851 where
1852 F: FnMut(RowId, &[u8]) -> std::ops::ControlFlow<()>,
1853 {
1854 self.by_name(table)?.try_for_each_row_raw(f);
1855 Ok(())
1856 }
1857
1858 pub fn create_index(&mut self, table: &str, column: &str) -> io::Result<()> {
1859 self.create_index_unique(table, column, false)
1860 }
1861
1862 pub fn create_index_unique(
1867 &mut self,
1868 table: &str,
1869 column: &str,
1870 unique: bool,
1871 ) -> io::Result<()> {
1872 let data_dir = self.data_dir.clone();
1873 self.by_name_mut(table)?
1874 .create_index_with_unique(column, &data_dir, unique)?;
1875 self.persist()
1879 }
1880
1881 pub fn is_index_unique(&self, table: &str, column: &str) -> Option<bool> {
1885 self.get_table(table)?.is_index_unique(column)
1886 }
1887
1888 pub fn has_index(&self, table: &str, column: &str) -> bool {
1890 self.get_table(table)
1891 .map(|t| t.has_index(column))
1892 .unwrap_or(false)
1893 }
1894
1895 pub fn index_lookup(&self, table: &str, column: &str, key: &Value) -> io::Result<Option<Row>> {
1896 Ok(self
1897 .by_name(table)?
1898 .index_lookup(column, key)
1899 .map(|(_, row)| row))
1900 }
1901
1902 pub fn list_tables(&self) -> Vec<&str> {
1903 self.tables
1907 .iter()
1908 .map(|t| t.schema.table_name.as_str())
1909 .collect()
1910 }
1911
1912 pub fn schema(&self, table: &str) -> Option<&Schema> {
1913 let slot = *self.name_to_slot.get(table)?;
1914 Some(&self.tables[slot].schema)
1915 }
1916
1917 pub fn drop_table(&mut self, name: &str) -> io::Result<()> {
1920 validate_table_name(name)?;
1921 let slot = *self.name_to_slot.get(name).ok_or_else(|| {
1922 io::Error::new(io::ErrorKind::NotFound, format!("table '{name}' not found"))
1923 })?;
1924 if !self.wal.is_off() {
1925 let payload = encode_ddl_drop_table(name);
1926 self.wal.append(0, WalRecordType::DdlDropTable, &payload)?;
1927 self.wal.flush()?;
1928 }
1929 let table = &self.tables[slot];
1931 let heap_path = self
1932 .data_dir
1933 .join(format!("{}.heap", table.schema.table_name));
1934 if heap_path.exists() {
1935 fs::remove_file(&heap_path)?;
1936 }
1937 for col_name in table.indexed_column_names() {
1943 let idx_path = self.data_dir.join(format!("{name}_{col_name}.idx"));
1944 if idx_path.exists() {
1945 let _ = fs::remove_file(&idx_path);
1946 }
1947 }
1948 self.name_to_slot.remove(name);
1950 let last = self.tables.len() - 1;
1951 if slot != last {
1952 let moved_name = self.tables[last].schema.table_name.clone();
1953 self.tables.swap(slot, last);
1954 self.name_to_slot.insert(moved_name, slot);
1955 }
1956 self.tables.pop();
1957 self.persist()?;
1958 Ok(())
1959 }
1960
1961 pub fn alter_table_add_column(&mut self, table: &str, col: ColumnDef) -> io::Result<()> {
1984 let data_dir = self.data_dir.clone();
1985 {
1986 let tbl = self.by_name_mut(table)?;
1987 if tbl.schema.columns.iter().any(|c| c.name == col.name) {
1988 return Err(io::Error::new(
1989 io::ErrorKind::AlreadyExists,
1990 format!("column '{}' already exists in table '{table}'", col.name),
1991 ));
1992 }
1993 }
1994 let barrier_lsn = if !self.wal.is_off() {
1995 let payload = encode_ddl_alter_add_column(table, &col);
1996 self.wal.append(0, WalRecordType::DdlAddColumn, &payload)?;
1997 self.wal.flush()?;
1998 self.wal.last_appended_lsn()
1999 } else {
2000 0
2001 };
2002 let tbl = self.by_name_mut(table)?;
2003
2004 let old_schema = tbl.schema.clone();
2005
2006 let has_rows = tbl.heap.scan().next().is_some();
2010
2011 if has_rows && col.required {
2012 return Err(io::Error::new(
2013 io::ErrorKind::InvalidInput,
2014 format!(
2015 "cannot add required column '{}' to non-empty table '{table}': \
2016 no default value to backfill existing rows with",
2017 col.name
2018 ),
2019 ));
2020 }
2021
2022 tbl.schema.columns.push(col);
2026 tbl.refresh_layout();
2027
2028 if has_rows {
2029 let fill: Vec<Value> = vec![Value::Empty; tbl.schema.columns.len()];
2034 tbl.rewrite_rows_for_schema_change(&old_schema, &fill, &data_dir)?;
2035 }
2036 if barrier_lsn > 0 {
2044 tbl.heap.stamp_all_pages_min_lsn(barrier_lsn)?;
2045 tbl.heap.flush()?;
2046 }
2047
2048 self.persist()?;
2049 Ok(())
2050 }
2051
2052 pub fn alter_table_drop_column(&mut self, table: &str, col_name: &str) -> io::Result<()> {
2075 let data_dir = self.data_dir.clone();
2076 {
2077 let tbl = self.by_name_mut(table)?;
2078 tbl.schema
2079 .columns
2080 .iter()
2081 .position(|c| c.name == col_name)
2082 .ok_or_else(|| {
2083 io::Error::new(
2084 io::ErrorKind::NotFound,
2085 format!("column '{col_name}' not found in table '{table}'"),
2086 )
2087 })?;
2088 }
2089 let barrier_lsn = if !self.wal.is_off() {
2090 let payload = encode_ddl_alter_drop_column(table, col_name);
2091 self.wal.append(0, WalRecordType::DdlDropColumn, &payload)?;
2092 self.wal.flush()?;
2093 self.wal.last_appended_lsn()
2094 } else {
2095 0
2096 };
2097 let tbl = self.by_name_mut(table)?;
2098 let idx = tbl
2099 .schema
2100 .columns
2101 .iter()
2102 .position(|c| c.name == col_name)
2103 .ok_or_else(|| {
2104 io::Error::new(
2105 io::ErrorKind::NotFound,
2106 format!("column '{col_name}' not found in table '{table}'"),
2107 )
2108 })?;
2109
2110 let old_schema = tbl.schema.clone();
2112 let has_rows = tbl.heap.scan().next().is_some();
2113
2114 tbl.schema.columns.remove(idx);
2116 for (i, col) in tbl.schema.columns.iter_mut().enumerate() {
2117 col.position = i as u16;
2118 }
2119 tbl.refresh_layout();
2120
2121 if has_rows {
2122 let fill: Vec<Value> = vec![Value::Empty; tbl.schema.columns.len()];
2128 tbl.rewrite_rows_for_schema_change(&old_schema, &fill, &data_dir)?;
2129 }
2130 if barrier_lsn > 0 {
2132 tbl.heap.stamp_all_pages_min_lsn(barrier_lsn)?;
2133 tbl.heap.flush()?;
2134 }
2135
2136 self.persist()?;
2137 Ok(())
2138 }
2139}
2140
2141impl Drop for Catalog {
2142 fn drop(&mut self) {
2143 if self.active_tx_id.is_some() {
2144 if let Err(e) = self.abandon_active_transaction_for_drop() {
2145 warn!(error = %e, "catalog drop active transaction cleanup failed");
2146 }
2147 return;
2148 }
2149 if let Err(e) = self.checkpoint() {
2161 warn!(error = %e, "catalog drop checkpoint failed");
2162 }
2163 }
2164}
2165
2166fn encode_wal_payload(table: &str, rid: RowId, row_bytes: &[u8]) -> Vec<u8> {
2181 let name = table.as_bytes();
2182 let mut out = Vec::with_capacity(4 + name.len() + 4 + 2 + 4 + row_bytes.len());
2183 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
2184 out.extend_from_slice(name);
2185 out.extend_from_slice(&rid.page_id.to_le_bytes());
2186 out.extend_from_slice(&rid.slot_index.to_le_bytes());
2187 out.extend_from_slice(&(row_bytes.len() as u32).to_le_bytes());
2188 out.extend_from_slice(row_bytes);
2189 out
2190}
2191
2192fn decode_wal_payload(data: &[u8]) -> Option<(String, RowId, Vec<u8>)> {
2193 let mut pos = 0usize;
2194 if data.len() < 4 {
2195 return None;
2196 }
2197 let name_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2198 pos += 4;
2199 if pos + name_len > data.len() {
2200 return None;
2201 }
2202 let name = std::str::from_utf8(&data[pos..pos + name_len])
2203 .ok()?
2204 .to_string();
2205 pos += name_len;
2206 if pos + 4 + 2 + 4 > data.len() {
2207 return None;
2208 }
2209 let page_id = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
2210 pos += 4;
2211 let slot_index = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
2212 pos += 2;
2213 let row_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2214 pos += 4;
2215 if pos + row_len > data.len() {
2216 return None;
2217 }
2218 let row_bytes = data[pos..pos + row_len].to_vec();
2219 Some((
2220 name,
2221 RowId {
2222 page_id,
2223 slot_index,
2224 },
2225 row_bytes,
2226 ))
2227}
2228
2229fn write_overflow_chain_logged(
2235 heap: &mut HeapFile,
2236 wal: &mut Wal,
2237 table: &str,
2238 tx_id: u64,
2239 value: &[u8],
2240) -> io::Result<OverflowStub> {
2241 if value.len() > MAX_VALUE_SIZE {
2242 return Err(StorageError::ValueTooLarge {
2243 size: value.len(),
2244 max: MAX_VALUE_SIZE,
2245 }
2246 .into());
2247 }
2248 let n = value.len().div_ceil(OVERFLOW_PAYLOAD_CAP).max(1);
2249 let mut pages = Vec::with_capacity(n);
2250 for _ in 0..n {
2251 pages.push(heap.allocate_overflow_page()?);
2252 }
2253 for i in 0..n {
2254 let start = i * OVERFLOW_PAYLOAD_CAP;
2255 let end = (start + OVERFLOW_PAYLOAD_CAP).min(value.len());
2256 let chunk = &value[start..end];
2257 let next = if i + 1 < n {
2258 pages[i + 1]
2259 } else {
2260 OVERFLOW_CHAIN_END
2261 };
2262 let payload = encode_overflow_write_payload(table, pages[i], next, chunk);
2263 wal.append(tx_id, WalRecordType::OverflowWrite, &payload)?;
2264 let lsn = wal.last_appended_lsn();
2265 heap.write_overflow_page(pages[i], next, chunk, lsn)?;
2266 }
2267 Ok(OverflowStub::new(
2268 value.len() as u64,
2269 pages[0],
2270 crc32fast::hash(value),
2271 ))
2272}
2273
2274fn encode_row_with_spill_logged(
2279 tbl: &mut Table,
2280 wal: &mut Wal,
2281 tx_id: u64,
2282 values: &Row,
2283) -> io::Result<Vec<u8>> {
2284 let v1_len = crate::row::v1_encoded_len(tbl.row_layout(), values);
2287 let is_indexed = tbl.indexed_col_mask();
2288 let chosen = plan_spill(tbl.row_layout(), values, v1_len, &is_indexed);
2289 if chosen.is_empty() {
2290 let mut v1 = Vec::new();
2291 encode_row_into(&tbl.schema, values, &mut v1);
2292 return Ok(v1);
2293 }
2294 let table_name = tbl.schema.table_name.clone();
2295 let n_var = tbl.row_layout().n_var();
2296 let mut spilled: Vec<Option<OverflowStub>> = vec![None; n_var];
2297 for col_idx in chosen {
2298 let var_idx = tbl
2299 .row_layout()
2300 .var_index(col_idx)
2301 .expect("plan_spill only returns var columns");
2302 let bytes: Vec<u8> = match &values[col_idx] {
2303 Value::Str(s) => s.as_bytes().to_vec(),
2304 Value::Bytes(b) => b.to_vec(),
2305 Value::Json(b) => b.to_vec(),
2306 _ => continue,
2307 };
2308 let stub = write_overflow_chain_logged(&mut tbl.heap, wal, &table_name, tx_id, &bytes)?;
2309 spilled[var_idx] = Some(stub);
2310 }
2311 let mut out = Vec::new();
2312 encode_row_v2_into(&tbl.schema, tbl.row_layout(), values, &spilled, &mut out);
2313 Ok(out)
2314}
2315
2316fn encode_overflow_write_payload(
2325 table: &str,
2326 page_id: u32,
2327 next_page: u32,
2328 chunk: &[u8],
2329) -> Vec<u8> {
2330 let name = table.as_bytes();
2331 let mut out = Vec::with_capacity(2 + name.len() + 4 + 4 + 2 + chunk.len());
2332 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
2333 out.extend_from_slice(name);
2334 out.extend_from_slice(&page_id.to_le_bytes());
2335 out.extend_from_slice(&next_page.to_le_bytes());
2336 out.extend_from_slice(&(chunk.len() as u16).to_le_bytes());
2337 out.extend_from_slice(chunk);
2338 out
2339}
2340
2341fn decode_overflow_write_payload(data: &[u8]) -> Option<(String, u32, u32, Vec<u8>)> {
2342 let mut pos = 0usize;
2343 if data.len() < 2 {
2344 return None;
2345 }
2346 let name_len = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
2347 pos += 2;
2348 if pos + name_len + 4 + 4 + 2 > data.len() {
2349 return None;
2350 }
2351 let name = std::str::from_utf8(&data[pos..pos + name_len])
2352 .ok()?
2353 .to_string();
2354 pos += name_len;
2355 let page_id = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
2356 pos += 4;
2357 let next_page = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?);
2358 pos += 4;
2359 let chunk_len = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
2360 pos += 2;
2361 if pos + chunk_len > data.len() {
2362 return None;
2363 }
2364 Some((
2365 name,
2366 page_id,
2367 next_page,
2368 data[pos..pos + chunk_len].to_vec(),
2369 ))
2370}
2371
2372fn encode_overflow_free_payload(table: &str, pages: &[u32]) -> Vec<u8> {
2376 let name = table.as_bytes();
2377 let mut out = Vec::with_capacity(2 + name.len() + 4 + pages.len() * 4);
2378 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
2379 out.extend_from_slice(name);
2380 out.extend_from_slice(&(pages.len() as u32).to_le_bytes());
2381 for p in pages {
2382 out.extend_from_slice(&p.to_le_bytes());
2383 }
2384 out
2385}
2386
2387fn decode_overflow_free_payload(data: &[u8]) -> Option<(String, Vec<u32>)> {
2388 let mut pos = 0usize;
2389 if data.len() < 2 {
2390 return None;
2391 }
2392 let name_len = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
2393 pos += 2;
2394 if pos + name_len + 4 > data.len() {
2395 return None;
2396 }
2397 let name = std::str::from_utf8(&data[pos..pos + name_len])
2398 .ok()?
2399 .to_string();
2400 pos += name_len;
2401 let count = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2402 pos += 4;
2403 if pos + count * 4 > data.len() {
2404 return None;
2405 }
2406 let mut pages = Vec::with_capacity(count);
2407 for _ in 0..count {
2408 pages.push(u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?));
2409 pos += 4;
2410 }
2411 Some((name, pages))
2412}
2413
2414fn encode_ddl_create_table(
2417 schema: &Schema,
2418 defaults: &[Option<Value>],
2419 auto_cols: &[bool],
2420) -> Vec<u8> {
2421 let name = schema.table_name.as_bytes();
2422 let mut out = Vec::new();
2423 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
2424 out.extend_from_slice(name);
2425 out.extend_from_slice(&(schema.columns.len() as u16).to_le_bytes());
2426 for col in &schema.columns {
2427 let cn = col.name.as_bytes();
2428 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
2429 out.extend_from_slice(cn);
2430 out.push(col.type_id as u8);
2431 out.push(col.required as u8);
2432 out.extend_from_slice(&col.position.to_le_bytes());
2433 }
2434 encode_defaults_section(&mut out, defaults);
2438 encode_auto_section(&mut out, auto_cols);
2439 out
2440}
2441
2442fn decode_ddl_create_table(data: &[u8]) -> Option<(Schema, Vec<Option<Value>>, Vec<bool>)> {
2443 let mut pos = 0usize;
2444 if data.len() < 4 {
2445 return None;
2446 }
2447 let name_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2448 pos += 4;
2449 if pos + name_len > data.len() {
2450 return None;
2451 }
2452 let table_name = std::str::from_utf8(&data[pos..pos + name_len])
2453 .ok()?
2454 .to_string();
2455 pos += name_len;
2456 if pos + 2 > data.len() {
2457 return None;
2458 }
2459 let n_cols = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?) as usize;
2460 pos += 2;
2461 let mut columns = Vec::with_capacity(n_cols);
2462 for _ in 0..n_cols {
2463 if pos + 4 > data.len() {
2464 return None;
2465 }
2466 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2467 pos += 4;
2468 if pos + cn_len + 4 > data.len() {
2469 return None;
2470 }
2471 let col_name = std::str::from_utf8(&data[pos..pos + cn_len])
2472 .ok()?
2473 .to_string();
2474 pos += cn_len;
2475 let type_id = TypeId::from_u8(data[pos])?;
2476 pos += 1;
2477 let required = data[pos] != 0;
2478 pos += 1;
2479 if pos + 2 > data.len() {
2480 return None;
2481 }
2482 let position = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
2483 pos += 2;
2484 columns.push(ColumnDef {
2485 name: col_name,
2486 type_id,
2487 required,
2488 position,
2489 });
2490 }
2491 let defaults = if pos < data.len() {
2494 decode_defaults_section(data, &mut pos, columns.len())?
2495 } else {
2496 Vec::new()
2497 };
2498 let auto_cols = if pos < data.len() {
2499 decode_auto_section(data, &mut pos, columns.len())?
2500 } else {
2501 Vec::new()
2502 };
2503 Some((
2504 Schema {
2505 table_name,
2506 columns,
2507 },
2508 defaults,
2509 auto_cols,
2510 ))
2511}
2512
2513fn encode_ddl_drop_table(table_name: &str) -> Vec<u8> {
2514 let name = table_name.as_bytes();
2515 let mut out = Vec::with_capacity(4 + name.len());
2516 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
2517 out.extend_from_slice(name);
2518 out
2519}
2520
2521fn encode_ddl_alter_add_column(table_name: &str, col: &ColumnDef) -> Vec<u8> {
2522 let name = table_name.as_bytes();
2523 let cn = col.name.as_bytes();
2524 let mut out = Vec::with_capacity(4 + name.len() + 4 + cn.len() + 4);
2525 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
2526 out.extend_from_slice(name);
2527 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
2528 out.extend_from_slice(cn);
2529 out.push(col.type_id as u8);
2530 out.push(col.required as u8);
2531 out.extend_from_slice(&col.position.to_le_bytes());
2532 out
2533}
2534
2535fn encode_ddl_alter_drop_column(table_name: &str, col_name: &str) -> Vec<u8> {
2536 let name = table_name.as_bytes();
2537 let cn = col_name.as_bytes();
2538 let mut out = Vec::with_capacity(4 + name.len() + 4 + cn.len());
2539 out.extend_from_slice(&(name.len() as u32).to_le_bytes());
2540 out.extend_from_slice(name);
2541 out.extend_from_slice(&(cn.len() as u32).to_le_bytes());
2542 out.extend_from_slice(cn);
2543 out
2544}
2545
2546fn decode_ddl_table_name(data: &[u8]) -> Option<(String, usize)> {
2547 if data.len() < 4 {
2548 return None;
2549 }
2550 let name_len = u32::from_le_bytes(data[0..4].try_into().ok()?) as usize;
2551 if 4 + name_len > data.len() {
2552 return None;
2553 }
2554 let name = std::str::from_utf8(&data[4..4 + name_len])
2555 .ok()?
2556 .to_string();
2557 Some((name, 4 + name_len))
2558}
2559
2560fn decode_ddl_alter_add_column(data: &[u8]) -> Option<(String, ColumnDef)> {
2561 let (table_name, mut pos) = decode_ddl_table_name(data)?;
2562 if pos + 4 > data.len() {
2563 return None;
2564 }
2565 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2566 pos += 4;
2567 if pos + cn_len + 4 > data.len() {
2568 return None;
2569 }
2570 let col_name = std::str::from_utf8(&data[pos..pos + cn_len])
2571 .ok()?
2572 .to_string();
2573 pos += cn_len;
2574 let type_id = TypeId::from_u8(data[pos])?;
2575 pos += 1;
2576 let required = data[pos] != 0;
2577 pos += 1;
2578 if pos + 2 > data.len() {
2579 return None;
2580 }
2581 let position = u16::from_le_bytes(data[pos..pos + 2].try_into().ok()?);
2582 Some((
2583 table_name,
2584 ColumnDef {
2585 name: col_name,
2586 type_id,
2587 required,
2588 position,
2589 },
2590 ))
2591}
2592
2593fn decode_ddl_alter_drop_column(data: &[u8]) -> Option<(String, String)> {
2594 let (table_name, pos) = decode_ddl_table_name(data)?;
2595 if pos + 4 > data.len() {
2596 return None;
2597 }
2598 let cn_len = u32::from_le_bytes(data[pos..pos + 4].try_into().ok()?) as usize;
2599 if pos + 4 + cn_len > data.len() {
2600 return None;
2601 }
2602 let col_name = std::str::from_utf8(&data[pos + 4..pos + 4 + cn_len])
2603 .ok()?
2604 .to_string();
2605 Some((table_name, col_name))
2606}
2607
2608pub(crate) struct IndexedColMeta {
2636 pub name: String,
2637 pub unique: bool,
2638}
2639
2640pub(crate) struct CatalogEntry {
2643 pub schema: Schema,
2644 pub indexed_cols: Vec<IndexedColMeta>,
2645 pub defaults: Vec<Option<Value>>,
2648 pub auto_cols: Vec<bool>,
2651}
2652
2653pub(crate) struct CatalogEntryRef<'a> {
2655 pub schema: &'a Schema,
2656 pub indexed_cols: Vec<IndexedColMeta>,
2657 pub defaults: &'a [Option<Value>],
2658 pub auto_cols: &'a [bool],
2659}
2660
2661fn encode_value_blob(out: &mut Vec<u8>, v: &Value) {
2667 out.push(v.type_id() as u8);
2668 match v {
2669 Value::Int(n) => out.extend_from_slice(&n.to_le_bytes()),
2670 Value::Float(f) => out.extend_from_slice(&f.to_bits().to_le_bytes()),
2671 Value::Bool(b) => out.push(*b as u8),
2672 Value::Str(s) => {
2673 out.extend_from_slice(&(s.len() as u32).to_le_bytes());
2674 out.extend_from_slice(s.as_bytes());
2675 }
2676 Value::DateTime(n) => out.extend_from_slice(&n.to_le_bytes()),
2677 Value::Uuid(u) => out.extend_from_slice(u),
2678 Value::Bytes(b) => {
2679 out.extend_from_slice(&(b.len() as u32).to_le_bytes());
2680 out.extend_from_slice(b);
2681 }
2682 Value::Json(b) => {
2683 out.extend_from_slice(&(b.len() as u32).to_le_bytes());
2684 out.extend_from_slice(b);
2685 }
2686 Value::Empty => {}
2687 }
2688}
2689
2690fn decode_value_blob(data: &[u8], pos: &mut usize) -> Option<Value> {
2693 let tag = *data.get(*pos)?;
2694 *pos += 1;
2695 let type_id = TypeId::from_u8(tag)?;
2696 let take_fixed = |pos: &mut usize, n: usize| -> Option<Vec<u8>> {
2697 if *pos + n > data.len() {
2698 return None;
2699 }
2700 let slice = data[*pos..*pos + n].to_vec();
2701 *pos += n;
2702 Some(slice)
2703 };
2704 match type_id {
2705 TypeId::Empty => Some(Value::Empty),
2706 TypeId::Int => Some(Value::Int(i64::from_le_bytes(
2707 take_fixed(pos, 8)?.try_into().ok()?,
2708 ))),
2709 TypeId::Float => Some(Value::Float(f64::from_bits(u64::from_le_bytes(
2710 take_fixed(pos, 8)?.try_into().ok()?,
2711 )))),
2712 TypeId::Bool => Some(Value::Bool(take_fixed(pos, 1)?[0] != 0)),
2713 TypeId::DateTime => Some(Value::DateTime(i64::from_le_bytes(
2714 take_fixed(pos, 8)?.try_into().ok()?,
2715 ))),
2716 TypeId::Uuid => Some(Value::Uuid(take_fixed(pos, 16)?.try_into().ok()?)),
2717 TypeId::Str => {
2718 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
2719 Some(Value::Str(String::from_utf8(take_fixed(pos, len)?).ok()?))
2720 }
2721 TypeId::Bytes => {
2722 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
2723 Some(Value::Bytes(take_fixed(pos, len)?))
2724 }
2725 TypeId::Json => {
2726 let len = u32::from_le_bytes(take_fixed(pos, 4)?.try_into().ok()?) as usize;
2727 Some(Value::Json(take_fixed(pos, len)?.into()))
2728 }
2729 }
2730}
2731
2732fn encode_defaults_section(out: &mut Vec<u8>, defaults: &[Option<Value>]) {
2736 let present: Vec<(u16, &Value)> = defaults
2737 .iter()
2738 .enumerate()
2739 .filter_map(|(i, d)| d.as_ref().map(|v| (i as u16, v)))
2740 .collect();
2741 out.extend_from_slice(&(present.len() as u16).to_le_bytes());
2742 for (pos, v) in present {
2743 out.extend_from_slice(&pos.to_le_bytes());
2744 encode_value_blob(out, v);
2745 }
2746}
2747
2748fn decode_defaults_section(
2751 data: &[u8],
2752 pos: &mut usize,
2753 n_cols: usize,
2754) -> Option<Vec<Option<Value>>> {
2755 if *pos + 2 > data.len() {
2756 return None;
2757 }
2758 let count = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
2759 *pos += 2;
2760 let mut out = vec![None; n_cols];
2761 for _ in 0..count {
2762 if *pos + 2 > data.len() {
2763 return None;
2764 }
2765 let col = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
2766 *pos += 2;
2767 let value = decode_value_blob(data, pos)?;
2768 if col < n_cols {
2769 out[col] = Some(value);
2770 }
2771 }
2772 Some(out)
2773}
2774
2775fn encode_auto_section(out: &mut Vec<u8>, auto_cols: &[bool]) {
2779 let present: Vec<u16> = auto_cols
2780 .iter()
2781 .enumerate()
2782 .filter_map(|(i, &a)| if a { Some(i as u16) } else { None })
2783 .collect();
2784 out.extend_from_slice(&(present.len() as u16).to_le_bytes());
2785 for pos in present {
2786 out.extend_from_slice(&pos.to_le_bytes());
2787 }
2788}
2789
2790fn decode_auto_section(data: &[u8], pos: &mut usize, n_cols: usize) -> Option<Vec<bool>> {
2793 if *pos + 2 > data.len() {
2794 return None;
2795 }
2796 let count = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
2797 *pos += 2;
2798 let mut out = vec![false; n_cols];
2799 for _ in 0..count {
2800 if *pos + 2 > data.len() {
2801 return None;
2802 }
2803 let col = u16::from_le_bytes(data[*pos..*pos + 2].try_into().ok()?) as usize;
2804 *pos += 2;
2805 if col < n_cols {
2806 out[col] = true;
2807 }
2808 }
2809 Some(out)
2810}
2811
2812fn write_catalog_file(path: &Path, entries: &[CatalogEntryRef<'_>]) -> io::Result<()> {
2813 let mut buf: Vec<u8> = Vec::with_capacity(64);
2814 buf.extend_from_slice(CATALOG_MAGIC);
2815 buf.extend_from_slice(&CATALOG_VERSION.to_le_bytes());
2816 buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
2817
2818 for entry in entries {
2819 let schema = entry.schema;
2820 let name = schema.table_name.as_bytes();
2821 buf.extend_from_slice(&(name.len() as u32).to_le_bytes());
2822 buf.extend_from_slice(name);
2823 buf.extend_from_slice(&(schema.columns.len() as u16).to_le_bytes());
2824 for col in &schema.columns {
2825 let cn = col.name.as_bytes();
2826 buf.extend_from_slice(&(cn.len() as u32).to_le_bytes());
2827 buf.extend_from_slice(cn);
2828 buf.push(col.type_id as u8);
2829 buf.push(if col.required { 1 } else { 0 });
2830 buf.extend_from_slice(&col.position.to_le_bytes());
2831 }
2832 buf.extend_from_slice(&(entry.indexed_cols.len() as u16).to_le_bytes());
2834 for meta in &entry.indexed_cols {
2835 let cn = meta.name.as_bytes();
2836 buf.extend_from_slice(&(cn.len() as u32).to_le_bytes());
2837 buf.extend_from_slice(cn);
2838 buf.push(if meta.unique { 1 } else { 0 });
2839 }
2840 encode_defaults_section(&mut buf, entry.defaults);
2842 encode_auto_section(&mut buf, entry.auto_cols);
2844 }
2845
2846 let crc = crc32fast::hash(&buf);
2850 buf.extend_from_slice(&crc.to_le_bytes());
2851
2852 let mut f = fs::OpenOptions::new()
2853 .create(true)
2854 .write(true)
2855 .truncate(true)
2856 .open(path)?;
2857 f.write_all(&buf)?;
2858 f.sync_data()?;
2859 Ok(())
2860}
2861
2862fn read_catalog_file(path: &Path) -> io::Result<Vec<CatalogEntry>> {
2863 let mut f = fs::File::open(path)?;
2864 let mut buf = Vec::new();
2865 f.read_to_end(&mut buf)?;
2866
2867 let mut pos = 0usize;
2868 if buf.len() < 14 || &buf[0..4] != CATALOG_MAGIC {
2870 return Err(io::Error::new(
2871 io::ErrorKind::InvalidData,
2872 "bad catalog magic",
2873 ));
2874 }
2875
2876 let payload = &buf[..buf.len() - 4];
2878 let stored_crc = u32::from_le_bytes(
2879 buf[buf.len() - 4..]
2880 .try_into()
2881 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog CRC"))?,
2882 );
2883 let computed_crc = crc32fast::hash(payload);
2884 if stored_crc != computed_crc {
2885 return Err(io::Error::new(
2886 io::ErrorKind::InvalidData,
2887 format!(
2888 "catalog CRC32 mismatch: expected {stored_crc:#010x}, got {computed_crc:#010x}"
2889 ),
2890 ));
2891 }
2892 let buf = &buf[..buf.len() - 4];
2894 pos += 4;
2895 let version = u16::from_le_bytes(
2896 buf[pos..pos + 2]
2897 .try_into()
2898 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog header"))?,
2899 );
2900 pos += 2;
2901 if version == 0 || version > CATALOG_VERSION {
2911 return Err(io::Error::new(
2912 io::ErrorKind::InvalidData,
2913 format!("unsupported catalog version: {version}"),
2914 ));
2915 }
2916 let n_tables = u32::from_le_bytes(
2917 buf[pos..pos + 4]
2918 .try_into()
2919 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "truncated catalog header"))?,
2920 ) as usize;
2921 pos += 4;
2922
2923 if n_tables > buf.len() {
2930 return Err(io::Error::new(
2931 io::ErrorKind::InvalidData,
2932 format!("catalog file corrupt: implausible table count {n_tables}"),
2933 ));
2934 }
2935
2936 let mut entries = Vec::with_capacity(n_tables);
2937 for _ in 0..n_tables {
2938 let name_len = read_u32(buf, &mut pos)? as usize;
2939 let table_name = read_string(buf, &mut pos, name_len)?;
2940 let n_cols = read_u16(buf, &mut pos)? as usize;
2941
2942 let mut columns = Vec::with_capacity(n_cols);
2943 for _ in 0..n_cols {
2944 let cname_len = read_u32(buf, &mut pos)? as usize;
2945 let name = read_string(buf, &mut pos, cname_len)?;
2946 let type_id_raw = read_u8(buf, &mut pos)?;
2947 let type_id = type_id_from_u8(type_id_raw)?;
2948 let required = read_u8(buf, &mut pos)? != 0;
2949 let position = read_u16(buf, &mut pos)?;
2950 columns.push(ColumnDef {
2951 name,
2952 type_id,
2953 required,
2954 position,
2955 });
2956 }
2957
2958 let indexed_cols: Vec<IndexedColMeta> = if version >= 3 {
2962 let n = read_u16(buf, &mut pos)? as usize;
2963 let mut v = Vec::with_capacity(n);
2964 for _ in 0..n {
2965 let l = read_u32(buf, &mut pos)? as usize;
2966 let name = read_string(buf, &mut pos, l)?;
2967 let unique = read_u8(buf, &mut pos)? != 0;
2968 v.push(IndexedColMeta { name, unique });
2969 }
2970 v
2971 } else if version >= 2 {
2972 let n = read_u16(buf, &mut pos)? as usize;
2973 let mut v = Vec::with_capacity(n);
2974 for _ in 0..n {
2975 let l = read_u32(buf, &mut pos)? as usize;
2976 let name = read_string(buf, &mut pos, l)?;
2977 v.push(IndexedColMeta {
2978 name,
2979 unique: false,
2980 });
2981 }
2982 v
2983 } else {
2984 Vec::new()
2985 };
2986
2987 let defaults = if version >= 4 {
2989 decode_defaults_section(buf, &mut pos, columns.len()).ok_or_else(|| {
2990 io::Error::new(io::ErrorKind::InvalidData, "truncated catalog defaults")
2991 })?
2992 } else {
2993 Vec::new()
2994 };
2995
2996 let auto_cols = if version >= 5 {
2998 decode_auto_section(buf, &mut pos, columns.len()).ok_or_else(|| {
2999 io::Error::new(io::ErrorKind::InvalidData, "truncated catalog auto columns")
3000 })?
3001 } else {
3002 Vec::new()
3003 };
3004
3005 entries.push(CatalogEntry {
3006 schema: Schema {
3007 table_name,
3008 columns,
3009 },
3010 indexed_cols,
3011 defaults,
3012 auto_cols,
3013 });
3014 }
3015
3016 Ok(entries)
3017}
3018
3019fn read_u8(buf: &[u8], pos: &mut usize) -> io::Result<u8> {
3020 if *pos >= buf.len() {
3021 return Err(io::Error::new(
3022 io::ErrorKind::UnexpectedEof,
3023 "truncated catalog",
3024 ));
3025 }
3026 let v = buf[*pos];
3027 *pos += 1;
3028 Ok(v)
3029}
3030fn read_u16(buf: &[u8], pos: &mut usize) -> io::Result<u16> {
3031 if *pos + 2 > buf.len() {
3032 return Err(io::Error::new(
3033 io::ErrorKind::UnexpectedEof,
3034 "truncated catalog",
3035 ));
3036 }
3037 let v = u16::from_le_bytes(
3038 buf[*pos..*pos + 2]
3039 .try_into()
3040 .expect("bounds checked above"),
3041 );
3042 *pos += 2;
3043 Ok(v)
3044}
3045fn read_u32(buf: &[u8], pos: &mut usize) -> io::Result<u32> {
3046 if *pos + 4 > buf.len() {
3047 return Err(io::Error::new(
3048 io::ErrorKind::UnexpectedEof,
3049 "truncated catalog",
3050 ));
3051 }
3052 let v = u32::from_le_bytes(
3053 buf[*pos..*pos + 4]
3054 .try_into()
3055 .expect("bounds checked above"),
3056 );
3057 *pos += 4;
3058 Ok(v)
3059}
3060fn read_string(buf: &[u8], pos: &mut usize, len: usize) -> io::Result<String> {
3061 if *pos + len > buf.len() {
3062 return Err(io::Error::new(
3063 io::ErrorKind::UnexpectedEof,
3064 "truncated catalog string",
3065 ));
3066 }
3067 let s = std::str::from_utf8(&buf[*pos..*pos + len])
3068 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "non-utf8 in catalog"))?
3069 .to_string();
3070 *pos += len;
3071 Ok(s)
3072}
3073fn type_id_from_u8(v: u8) -> io::Result<TypeId> {
3074 match v {
3075 0 => Ok(TypeId::Empty),
3076 1 => Ok(TypeId::Int),
3077 2 => Ok(TypeId::Float),
3078 3 => Ok(TypeId::Bool),
3079 4 => Ok(TypeId::Str),
3080 5 => Ok(TypeId::DateTime),
3081 6 => Ok(TypeId::Uuid),
3082 7 => Ok(TypeId::Bytes),
3083 8 => Ok(TypeId::Json),
3084 _ => Err(io::Error::new(
3085 io::ErrorKind::InvalidData,
3086 format!("unknown type id: {v}"),
3087 )),
3088 }
3089}
3090
3091#[cfg(test)]
3092mod tests {
3093 use super::*;
3094 fn temp_catalog(name: &str) -> Catalog {
3095 let dir = std::env::temp_dir().join(format!("powdb_cat_{name}_{}", std::process::id()));
3096 Catalog::create(&dir).unwrap()
3097 }
3098
3099 fn schema_two_cols() -> Schema {
3100 Schema {
3101 table_name: "T".into(),
3102 columns: vec![
3103 ColumnDef {
3104 name: "id".into(),
3105 type_id: TypeId::Int,
3106 required: true,
3107 position: 0,
3108 },
3109 ColumnDef {
3110 name: "status".into(),
3111 type_id: TypeId::Str,
3112 required: false,
3113 position: 1,
3114 },
3115 ],
3116 }
3117 }
3118
3119 #[test]
3120 fn replay_records_treats_reused_tx_ids_as_ordered_spans() {
3121 let mut cat = temp_catalog("reused_tx_ids");
3122 let schema = schema_two_cols();
3123 cat.create_table(schema.clone()).unwrap();
3124 cat.checkpoint().unwrap();
3125
3126 let mut committed_row = Vec::new();
3127 encode_row_into(
3128 &schema,
3129 &[Value::Int(1), Value::Str("committed".into())],
3130 &mut committed_row,
3131 );
3132 let mut incomplete_row = Vec::new();
3133 encode_row_into(
3134 &schema,
3135 &[Value::Int(2), Value::Str("incomplete".into())],
3136 &mut incomplete_row,
3137 );
3138
3139 let records = vec![
3140 WalRecord {
3141 tx_id: 1,
3142 record_type: WalRecordType::Begin,
3143 lsn: 1,
3144 data: Vec::new(),
3145 },
3146 WalRecord {
3147 tx_id: 1,
3148 record_type: WalRecordType::Insert,
3149 lsn: 2,
3150 data: encode_wal_payload(
3151 "T",
3152 RowId {
3153 page_id: 1,
3154 slot_index: 0,
3155 },
3156 &committed_row,
3157 ),
3158 },
3159 WalRecord {
3160 tx_id: 1,
3161 record_type: WalRecordType::Commit,
3162 lsn: 3,
3163 data: Vec::new(),
3164 },
3165 WalRecord {
3166 tx_id: 1,
3167 record_type: WalRecordType::Begin,
3168 lsn: 4,
3169 data: Vec::new(),
3170 },
3171 WalRecord {
3172 tx_id: 1,
3173 record_type: WalRecordType::Insert,
3174 lsn: 5,
3175 data: encode_wal_payload(
3176 "T",
3177 RowId {
3178 page_id: 1,
3179 slot_index: 1,
3180 },
3181 &incomplete_row,
3182 ),
3183 },
3184 ];
3185
3186 cat.apply_wal_records(&records).unwrap();
3187 let rows: Vec<_> = cat.scan("T").unwrap().collect();
3188 assert_eq!(rows.len(), 1);
3189 assert_eq!(rows[0].1[0], Value::Int(1));
3190 assert_eq!(rows[0].1[1], Value::Str("committed".into()));
3191 }
3192
3193 #[test]
3194 fn ddl_create_table_codec_roundtrips_defaults_and_auto() {
3195 let schema = schema_two_cols();
3196 let defaults = vec![None, Some(Value::Str("active".into()))];
3197 let auto_cols = vec![true, false];
3198 let encoded = encode_ddl_create_table(&schema, &defaults, &auto_cols);
3199 let (decoded_schema, decoded_defaults, decoded_auto) =
3200 decode_ddl_create_table(&encoded).unwrap();
3201 assert_eq!(decoded_schema.columns.len(), 2);
3202 assert_eq!(decoded_defaults, defaults);
3203 assert_eq!(decoded_auto, auto_cols);
3204 }
3205
3206 #[test]
3207 fn ddl_create_table_codec_back_compat_without_trailing_sections() {
3208 let schema = schema_two_cols();
3212 let full = encode_ddl_create_table(&schema, &[], &[]);
3213 let legacy = &full[..full.len() - 4];
3216 let (decoded_schema, decoded_defaults, decoded_auto) =
3217 decode_ddl_create_table(legacy).unwrap();
3218 assert_eq!(decoded_schema.columns.len(), 2);
3219 assert!(decoded_defaults.is_empty(), "no defaults section -> empty");
3220 assert!(decoded_auto.is_empty(), "no auto section -> empty");
3221 }
3222
3223 #[test]
3224 fn ddl_create_table_codec_back_compat_defaults_but_no_auto() {
3225 let schema = schema_two_cols();
3228 let defaults = vec![None, Some(Value::Str("active".into()))];
3229 let full = encode_ddl_create_table(&schema, &defaults, &[]);
3230 let legacy = &full[..full.len() - 2];
3232 let (_schema, decoded_defaults, decoded_auto) = decode_ddl_create_table(legacy).unwrap();
3233 assert_eq!(decoded_defaults, defaults);
3234 assert!(decoded_auto.is_empty());
3235 }
3236
3237 #[test]
3238 fn read_catalog_file_accepts_intermediate_versions_3_and_4() {
3239 use std::io::Write as _;
3247 fn write_legacy_catalog(path: &std::path::Path, version: u16) {
3248 let mut buf: Vec<u8> = Vec::new();
3249 buf.extend_from_slice(CATALOG_MAGIC);
3250 buf.extend_from_slice(&version.to_le_bytes());
3251 buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes());
3254 buf.extend_from_slice(b"T");
3255 buf.extend_from_slice(&2u16.to_le_bytes()); buf.extend_from_slice(&2u32.to_le_bytes());
3258 buf.extend_from_slice(b"id");
3259 buf.push(TypeId::Int as u8);
3260 buf.push(1);
3261 buf.extend_from_slice(&0u16.to_le_bytes());
3262 buf.extend_from_slice(&6u32.to_le_bytes());
3264 buf.extend_from_slice(b"status");
3265 buf.push(TypeId::Str as u8);
3266 buf.push(0);
3267 buf.extend_from_slice(&1u16.to_le_bytes());
3268 buf.extend_from_slice(&0u16.to_le_bytes());
3270 if version >= 4 {
3272 encode_defaults_section(&mut buf, &[None, None]);
3273 }
3274 let crc = crc32fast::hash(&buf);
3276 buf.extend_from_slice(&crc.to_le_bytes());
3277 let mut f = fs::File::create(path).unwrap();
3278 f.write_all(&buf).unwrap();
3279 }
3280
3281 for version in [3u16, 4u16] {
3282 let path = std::env::temp_dir().join(format!(
3283 "powdb_cat_v{version}_compat_{}.bin",
3284 std::process::id()
3285 ));
3286 write_legacy_catalog(&path, version);
3287 let entries = read_catalog_file(&path)
3288 .unwrap_or_else(|e| panic!("version {version} catalog must load, got: {e}"));
3289 assert_eq!(entries.len(), 1);
3290 assert_eq!(entries[0].schema.table_name, "T");
3291 assert_eq!(entries[0].schema.columns.len(), 2);
3292 assert!(
3293 entries[0].auto_cols.is_empty(),
3294 "v{version} has no auto cols"
3295 );
3296 fs::remove_file(&path).ok();
3297 }
3298 }
3299
3300 #[test]
3301 fn read_catalog_file_rejects_implausible_table_count() {
3302 use std::io::Write as _;
3310 let mut buf: Vec<u8> = Vec::new();
3311 buf.extend_from_slice(CATALOG_MAGIC);
3312 buf.extend_from_slice(&CATALOG_VERSION.to_le_bytes());
3313 buf.extend_from_slice(&1000u32.to_le_bytes()); let crc = crc32fast::hash(&buf);
3316 buf.extend_from_slice(&crc.to_le_bytes());
3317 let path =
3318 std::env::temp_dir().join(format!("powdb_cat_badcount_{}.bin", std::process::id()));
3319 fs::File::create(&path).unwrap().write_all(&buf).unwrap();
3320
3321 let msg = match read_catalog_file(&path) {
3322 Ok(_) => panic!("implausible table count must be rejected, got Ok"),
3323 Err(e) => e.to_string(),
3324 };
3325 assert!(
3326 msg.contains("implausible table count"),
3327 "expected an implausible-table-count error, got: {msg}"
3328 );
3329 fs::remove_file(&path).ok();
3330 }
3331
3332 #[test]
3333 fn data_dir_and_max_lsn_accessors() {
3334 let dir = std::env::temp_dir().join(format!("powdb_cat_maxlsn_{}", std::process::id()));
3335 let mut cat = Catalog::create(&dir).unwrap();
3336
3337 assert_eq!(cat.data_dir(), dir.as_path());
3339
3340 assert_eq!(cat.max_lsn(), 0);
3342
3343 let schema = Schema {
3344 table_name: "users".into(),
3345 columns: vec![ColumnDef {
3346 name: "name".into(),
3347 type_id: TypeId::Str,
3348 required: true,
3349 position: 0,
3350 }],
3351 };
3352 cat.create_table(schema).unwrap();
3353
3354 cat.insert("users", &vec![Value::Str("Alice".into())])
3355 .unwrap();
3356 cat.sync_wal().unwrap();
3357
3358 assert!(cat.max_lsn() > 0);
3361 }
3362
3363 #[test]
3364 fn test_create_table_and_insert() {
3365 let mut cat = temp_catalog("basic");
3366 let schema = Schema {
3367 table_name: "users".into(),
3368 columns: vec![
3369 ColumnDef {
3370 name: "name".into(),
3371 type_id: TypeId::Str,
3372 required: true,
3373 position: 0,
3374 },
3375 ColumnDef {
3376 name: "age".into(),
3377 type_id: TypeId::Int,
3378 required: false,
3379 position: 1,
3380 },
3381 ],
3382 };
3383 cat.create_table(schema).unwrap();
3384
3385 let row = vec![Value::Str("Alice".into()), Value::Int(30)];
3386 let rid = cat.insert("users", &row).unwrap();
3387
3388 let result = cat.get("users", rid).unwrap();
3389 assert_eq!(result[0], Value::Str("Alice".into()));
3390 assert_eq!(result[1], Value::Int(30));
3391 }
3392
3393 #[test]
3394 fn test_scan_table() {
3395 let mut cat = temp_catalog("scan");
3396 let schema = Schema {
3397 table_name: "items".into(),
3398 columns: vec![
3399 ColumnDef {
3400 name: "name".into(),
3401 type_id: TypeId::Str,
3402 required: true,
3403 position: 0,
3404 },
3405 ColumnDef {
3406 name: "price".into(),
3407 type_id: TypeId::Float,
3408 required: true,
3409 position: 1,
3410 },
3411 ],
3412 };
3413 cat.create_table(schema).unwrap();
3414
3415 for i in 0..50 {
3416 cat.insert(
3417 "items",
3418 &vec![
3419 Value::Str(format!("item_{i}")),
3420 Value::Float(i as f64 * 1.5),
3421 ],
3422 )
3423 .unwrap();
3424 }
3425
3426 let rows: Vec<_> = cat.scan("items").unwrap().collect();
3427 assert_eq!(rows.len(), 50);
3428 }
3429
3430 #[test]
3431 fn test_index_lookup() {
3432 let mut cat = temp_catalog("idx");
3433 let schema = Schema {
3434 table_name: "users".into(),
3435 columns: vec![
3436 ColumnDef {
3437 name: "email".into(),
3438 type_id: TypeId::Str,
3439 required: true,
3440 position: 0,
3441 },
3442 ColumnDef {
3443 name: "name".into(),
3444 type_id: TypeId::Str,
3445 required: true,
3446 position: 1,
3447 },
3448 ],
3449 };
3450 cat.create_table(schema).unwrap();
3451 cat.create_index("users", "email").unwrap();
3452
3453 cat.insert(
3454 "users",
3455 &vec![
3456 Value::Str("alice@example.com".into()),
3457 Value::Str("Alice".into()),
3458 ],
3459 )
3460 .unwrap();
3461 cat.insert(
3462 "users",
3463 &vec![
3464 Value::Str("bob@example.com".into()),
3465 Value::Str("Bob".into()),
3466 ],
3467 )
3468 .unwrap();
3469
3470 let result = cat
3471 .index_lookup("users", "email", &Value::Str("bob@example.com".into()))
3472 .unwrap();
3473 assert!(result.is_some());
3474 let row = result.unwrap();
3475 assert_eq!(row[1], Value::Str("Bob".into()));
3476 }
3477
3478 #[test]
3479 fn test_delete_row() {
3480 let mut cat = temp_catalog("delete");
3481 let schema = Schema {
3482 table_name: "t".into(),
3483 columns: vec![ColumnDef {
3484 name: "v".into(),
3485 type_id: TypeId::Int,
3486 required: true,
3487 position: 0,
3488 }],
3489 };
3490 cat.create_table(schema).unwrap();
3491 let r1 = cat.insert("t", &vec![Value::Int(1)]).unwrap();
3492 let r2 = cat.insert("t", &vec![Value::Int(2)]).unwrap();
3493 cat.delete("t", r1).unwrap();
3494 assert!(cat.get("t", r1).is_none());
3495 assert!(cat.get("t", r2).is_some());
3496 }
3497
3498 #[test]
3499 fn test_update_row() {
3500 let mut cat = temp_catalog("update");
3501 let schema = Schema {
3502 table_name: "t".into(),
3503 columns: vec![ColumnDef {
3504 name: "v".into(),
3505 type_id: TypeId::Int,
3506 required: true,
3507 position: 0,
3508 }],
3509 };
3510 cat.create_table(schema).unwrap();
3511 let rid = cat.insert("t", &vec![Value::Int(1)]).unwrap();
3512 let new_rid = cat.update("t", rid, &vec![Value::Int(99)]).unwrap();
3513 let row = cat.get("t", new_rid).unwrap();
3514 assert_eq!(row[0], Value::Int(99));
3515 }
3516
3517 #[test]
3518 fn test_persist_and_reopen() {
3519 let dir = std::env::temp_dir().join(format!("powdb_cat_persist_{}", std::process::id()));
3520 let _ = std::fs::remove_dir_all(&dir);
3522
3523 {
3524 let mut cat = Catalog::create(&dir).unwrap();
3525 cat.create_table(Schema {
3526 table_name: "users".into(),
3527 columns: vec![
3528 ColumnDef {
3529 name: "name".into(),
3530 type_id: TypeId::Str,
3531 required: true,
3532 position: 0,
3533 },
3534 ColumnDef {
3535 name: "age".into(),
3536 type_id: TypeId::Int,
3537 required: false,
3538 position: 1,
3539 },
3540 ],
3541 })
3542 .unwrap();
3543 cat.insert("users", &vec![Value::Str("Alice".into()), Value::Int(30)])
3544 .unwrap();
3545 cat.insert("users", &vec![Value::Str("Bob".into()), Value::Int(25)])
3546 .unwrap();
3547 }
3548
3549 let cat = Catalog::open(&dir).unwrap();
3551 let schema = cat.schema("users").unwrap();
3552 assert_eq!(schema.columns.len(), 2);
3553 assert_eq!(schema.columns[0].name, "name");
3554 assert_eq!(schema.columns[0].type_id, TypeId::Str);
3555 assert_eq!(schema.columns[1].type_id, TypeId::Int);
3556
3557 let rows: Vec<_> = cat.scan("users").unwrap().collect();
3558 assert_eq!(rows.len(), 2);
3559
3560 std::fs::remove_dir_all(&dir).ok();
3561 }
3562
3563 #[test]
3564 fn test_open_missing_dir_errors() {
3565 let dir = std::env::temp_dir().join(format!("powdb_cat_missing_{}", std::process::id()));
3566 let _ = std::fs::remove_dir_all(&dir);
3567 std::fs::create_dir_all(&dir).unwrap();
3568 assert!(Catalog::open(&dir).is_err());
3570 std::fs::remove_dir_all(&dir).ok();
3571 }
3572
3573 #[test]
3574 fn test_list_tables() {
3575 let mut cat = temp_catalog("list");
3576 cat.create_table(Schema {
3577 table_name: "a".into(),
3578 columns: vec![ColumnDef {
3579 name: "x".into(),
3580 type_id: TypeId::Int,
3581 required: true,
3582 position: 0,
3583 }],
3584 })
3585 .unwrap();
3586 cat.create_table(Schema {
3587 table_name: "b".into(),
3588 columns: vec![ColumnDef {
3589 name: "y".into(),
3590 type_id: TypeId::Int,
3591 required: true,
3592 position: 0,
3593 }],
3594 })
3595 .unwrap();
3596 let mut tables = cat.list_tables();
3597 tables.sort();
3598 assert_eq!(tables, vec!["a", "b"]);
3599 }
3600
3601 #[test]
3602 fn test_path_traversal_table_name_rejected() {
3603 let mut cat = temp_catalog("path_trav");
3604 let bad_names = vec![
3606 "../etc/passwd",
3607 "foo/bar",
3608 "table\0name",
3609 "",
3610 "123starts_with_digit",
3611 "has-dashes",
3612 "has spaces",
3613 "has.dots",
3614 ];
3615 for name in bad_names {
3616 let schema = Schema {
3617 table_name: name.into(),
3618 columns: vec![ColumnDef {
3619 name: "x".into(),
3620 type_id: TypeId::Int,
3621 required: true,
3622 position: 0,
3623 }],
3624 };
3625 let result = cat.create_table(schema);
3626 assert!(result.is_err(), "expected error for table name '{name}'");
3627 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
3628 }
3629 let good_names = vec!["users", "_private", "Table_123", "_"];
3631 for name in good_names {
3632 let schema = Schema {
3633 table_name: name.into(),
3634 columns: vec![ColumnDef {
3635 name: "x".into(),
3636 type_id: TypeId::Int,
3637 required: true,
3638 position: 0,
3639 }],
3640 };
3641 assert!(
3642 cat.create_table(schema).is_ok(),
3643 "expected ok for table name '{name}'"
3644 );
3645 }
3646 }
3647
3648 #[test]
3649 fn test_path_traversal_column_name_rejected() {
3650 let mut cat = temp_catalog("col_path_trav");
3651 let schema = Schema {
3652 table_name: "valid_table".into(),
3653 columns: vec![ColumnDef {
3654 name: "../bad".into(),
3655 type_id: TypeId::Int,
3656 required: true,
3657 position: 0,
3658 }],
3659 };
3660 let result = cat.create_table(schema);
3661 assert!(result.is_err());
3662 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
3663 }
3664
3665 #[test]
3666 fn test_drop_table_validates_name() {
3667 let mut cat = temp_catalog("drop_trav");
3668 let result = cat.drop_table("../etc/passwd");
3669 assert!(result.is_err());
3670 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
3672 }
3673}