1use std::fs::{self, File, OpenOptions};
10use std::path::{Path, PathBuf};
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::sync::{Arc, Mutex, RwLock};
13
14use crate::db::{DbError, DbErrorKind, State, StatementResult, dberr};
15use crate::sql::ast::Statement;
16use crate::sql::parser::parse;
17use crate::{storage, wal};
18
19struct Inner {
20 path: Option<PathBuf>,
21 wal_path: Option<PathBuf>,
22 _lock_file: Option<File>,
23 _workspace_lock_file: Option<File>,
24 state: RwLock<State>,
25 generation: AtomicU64,
26 commit_lock: Mutex<()>,
27}
28
29#[derive(Clone)]
31pub struct Database {
32 inner: Arc<Inner>,
33}
34
35impl Database {
36 pub fn in_memory() -> Database {
38 Database {
39 inner: Arc::new(Inner {
40 path: None,
41 wal_path: None,
42 _lock_file: None,
43 _workspace_lock_file: None,
44 state: RwLock::new(State::empty()),
45 generation: AtomicU64::new(0),
46 commit_lock: Mutex::new(()),
47 }),
48 }
49 }
50
51 pub fn open(path: impl AsRef<Path>) -> Result<Database, DbError> {
53 Self::open_internal(path, false)
54 }
55
56 pub(crate) fn open_in_workspace(path: impl AsRef<Path>) -> Result<Database, DbError> {
59 Self::open_internal(path, true)
60 }
61
62 fn open_internal(
63 path: impl AsRef<Path>,
64 workspace_lock_already_held: bool,
65 ) -> Result<Database, DbError> {
66 let path = path.as_ref().to_path_buf();
67 let workspace_lock_file = if workspace_lock_already_held {
68 None
69 } else {
70 acquire_workspace_lock(&path)?
71 };
72 let lock_file = acquire_lock(&path)?;
73 let wal_path = wal_path(&path);
74 let frame = wal::latest(&wal_path)?;
75 let snapshot = storage::read_snapshot(&path);
76 let (mut state, mut generation, mut repair_snapshot) = match snapshot {
77 Ok((state, generation)) => (state, generation, false),
78 Err(error) => {
79 let Some(frame) = &frame else {
80 return Err(error);
81 };
82 (State::decode(&frame.payload)?, frame.generation, true)
83 }
84 };
85 if let Some(frame) = frame {
86 if frame.generation > generation {
87 state = State::decode(&frame.payload)?;
88 generation = frame.generation;
89 storage::write_snapshot(&path, &state, generation)?;
92 wal::truncate(&wal_path)?;
93 repair_snapshot = false;
94 } else {
95 if repair_snapshot {
99 storage::write_snapshot(&path, &state, generation)?;
100 repair_snapshot = false;
101 }
102 wal::truncate(&wal_path)?;
103 }
104 }
105 if repair_snapshot || !path.exists() {
106 storage::write_snapshot(&path, &state, generation)?;
107 }
108 Ok(Database {
109 inner: Arc::new(Inner {
110 path: Some(path),
111 wal_path: Some(wal_path),
112 _lock_file: Some(lock_file),
113 _workspace_lock_file: workspace_lock_file,
114 state: RwLock::new(state),
115 generation: AtomicU64::new(generation),
116 commit_lock: Mutex::new(()),
117 }),
118 })
119 }
120
121 pub fn begin(&self) -> Result<Transaction, DbError> {
123 let mut budget = crate::engine::ExecutionBudget::unlimited();
124 self.begin_with_budget(&mut budget)
125 }
126
127 pub(crate) fn begin_with_budget(
128 &self,
129 budget: &mut crate::engine::ExecutionBudget,
130 ) -> Result<Transaction, DbError> {
131 let state_guard = self
132 .inner
133 .state
134 .read()
135 .map_err(|_| dberr(DbErrorKind::Transaction, "database state lock poisoned"))?;
136 budget.state_clone(&state_guard, "starting a database snapshot")?;
137 let state = state_guard.clone();
141 let generation = self.inner.generation.load(Ordering::Acquire);
142 Ok(Transaction {
143 db: self.clone(),
144 state,
145 base_generation: generation,
146 active: true,
147 dirty: false,
148 })
149 }
150
151 pub fn transaction(&self) -> Result<Transaction, DbError> {
153 self.begin()
154 }
155
156 pub fn connect(&self) -> Connection {
159 Connection {
160 db: self.clone(),
161 transaction: None,
162 }
163 }
164
165 pub fn execute(&self, stmt: &Statement) -> Result<StatementResult, DbError> {
167 let mut budget = crate::engine::ExecutionBudget::unlimited();
168 self.execute_with_budget(stmt, &mut budget)
169 }
170
171 pub(crate) fn execute_with_budget(
172 &self,
173 stmt: &Statement,
174 budget: &mut crate::engine::ExecutionBudget,
175 ) -> Result<StatementResult, DbError> {
176 match stmt {
177 Statement::Checkpoint => {
178 self.checkpoint_with_budget(budget)?;
179 Ok(StatementResult::Checkpoint)
180 }
181 Statement::Begin => Ok(StatementResult::Begin),
182 Statement::Commit => Ok(StatementResult::Commit),
183 Statement::Rollback => Ok(StatementResult::Rollback),
184 _ => {
185 let mut transaction = self.begin_with_budget(budget)?;
186 let result = transaction.execute_with_budget(stmt, budget)?;
187 if is_mutation(&result) {
188 transaction.commit_with_budget(budget)?;
189 } else {
190 transaction.rollback();
191 }
192 Ok(result)
193 }
194 }
195 }
196
197 pub fn execute_sql(&self, sql: &str) -> Result<Vec<StatementResult>, DbError> {
199 self.connect().execute_sql(sql)
200 }
201
202 pub(crate) fn execute_sql_with_budget(
203 &self,
204 sql: &str,
205 max_work: usize,
206 ) -> Result<Vec<StatementResult>, DbError> {
207 self.connect().execute_sql_with_budget(sql, max_work)
208 }
209
210 pub fn checkpoint(&self) -> Result<(), DbError> {
213 let mut budget = crate::engine::ExecutionBudget::unlimited();
214 self.checkpoint_with_budget(&mut budget)
215 }
216
217 pub(crate) fn checkpoint_with_budget(
218 &self,
219 budget: &mut crate::engine::ExecutionBudget,
220 ) -> Result<(), DbError> {
221 let _commit = self
222 .inner
223 .commit_lock
224 .lock()
225 .map_err(|_| dberr(DbErrorKind::Transaction, "database commit lock poisoned"))?;
226 let Some(path) = &self.inner.path else {
227 return Ok(());
228 };
229 let state = self
230 .inner
231 .state
232 .read()
233 .map_err(|_| dberr(DbErrorKind::Transaction, "database state lock poisoned"))?;
234 budget.state_clone(&state, "preparing a database checkpoint")?;
235 let state = state.clone();
236 let generation = self.inner.generation.load(Ordering::Acquire);
237 storage::write_snapshot(path, &state, generation)?;
238 if let Some(wal_path) = &self.inner.wal_path {
239 wal::truncate(wal_path)?;
240 }
241 Ok(())
242 }
243
244 pub fn generation(&self) -> u64 {
246 self.inner.generation.load(Ordering::Acquire)
247 }
248
249 pub fn table_names(&self) -> Result<Vec<String>, DbError> {
251 let state = self
252 .inner
253 .state
254 .read()
255 .map_err(|_| dberr(DbErrorKind::Transaction, "database state lock poisoned"))?;
256 let mut names: Vec<String> = state.tables.keys().cloned().collect();
257 names.sort_by_key(|name| name.to_ascii_lowercase());
258 Ok(names)
259 }
260
261 pub fn columns(&self, table: &str) -> Result<Vec<crate::db::Column>, DbError> {
263 let state = self
264 .inner
265 .state
266 .read()
267 .map_err(|_| dberr(DbErrorKind::Transaction, "database state lock poisoned"))?;
268 state
269 .table(table)
270 .map(|value| value.columns.clone())
271 .ok_or_else(|| dberr(DbErrorKind::UnknownTable, format!("no such table: {table}")))
272 }
273
274 pub fn row_count(&self, table: &str) -> Result<usize, DbError> {
276 let state = self
277 .inner
278 .state
279 .read()
280 .map_err(|_| dberr(DbErrorKind::Transaction, "database state lock poisoned"))?;
281 state
282 .table(table)
283 .map(crate::db::Table::row_count)
284 .ok_or_else(|| dberr(DbErrorKind::UnknownTable, format!("no such table: {table}")))
285 }
286
287 fn commit_state(&self, state: State, expected: u64) -> Result<u64, DbError> {
288 let _commit = self
289 .inner
290 .commit_lock
291 .lock()
292 .map_err(|_| dberr(DbErrorKind::Transaction, "database commit lock poisoned"))?;
293 let mut current = self
294 .inner
295 .state
296 .write()
297 .map_err(|_| dberr(DbErrorKind::Transaction, "database state lock poisoned"))?;
298 let actual = self.inner.generation.load(Ordering::Acquire);
299 if actual != expected {
300 return Err(dberr(
301 DbErrorKind::Transaction,
302 format!("transaction conflict: snapshot {expected}, database is at {actual}"),
303 ));
304 }
305 let generation = actual
306 .checked_add(1)
307 .ok_or_else(|| dberr(DbErrorKind::Transaction, "transaction generation exhausted"))?;
308 if let Some(wal_path) = &self.inner.wal_path {
309 let payload = state.encode();
310 wal::append(wal_path, generation, &payload)?;
311 }
312 *current = state;
313 self.inner.generation.store(generation, Ordering::Release);
314 Ok(generation)
315 }
316}
317
318pub struct Connection {
320 db: Database,
321 transaction: Option<Transaction>,
322}
323
324impl Connection {
325 pub fn execute(&mut self, stmt: &Statement) -> Result<StatementResult, DbError> {
326 let mut budget = crate::engine::ExecutionBudget::unlimited();
327 self.execute_with_budget(stmt, &mut budget)
328 }
329
330 pub(crate) fn execute_with_budget(
331 &mut self,
332 stmt: &Statement,
333 budget: &mut crate::engine::ExecutionBudget,
334 ) -> Result<StatementResult, DbError> {
335 match stmt {
336 Statement::Checkpoint => {
337 if self.transaction.is_some() {
338 return Err(dberr(
339 DbErrorKind::Transaction,
340 "cannot checkpoint while a transaction is active",
341 ));
342 }
343 self.db.checkpoint_with_budget(budget)?;
344 Ok(StatementResult::Checkpoint)
345 }
346 Statement::Begin => {
347 if self.transaction.is_some() {
348 return Err(dberr(
349 DbErrorKind::Transaction,
350 "transaction already active",
351 ));
352 }
353 self.transaction = Some(self.db.begin_with_budget(budget)?);
354 Ok(StatementResult::Begin)
355 }
356 Statement::Commit => {
357 let Some(transaction) = self.transaction.take() else {
358 return Err(dberr(DbErrorKind::Transaction, "no transaction is active"));
359 };
360 transaction.commit_with_budget(budget)?;
361 Ok(StatementResult::Commit)
362 }
363 Statement::Rollback => {
364 if let Some(transaction) = self.transaction.take() {
365 transaction.rollback();
366 }
367 Ok(StatementResult::Rollback)
368 }
369 _ => match self.transaction.as_mut() {
370 Some(transaction) => transaction.execute_with_budget(stmt, budget),
371 None => self.db.execute_with_budget(stmt, budget),
372 },
373 }
374 }
375
376 pub fn execute_sql(&mut self, sql: &str) -> Result<Vec<StatementResult>, DbError> {
377 let mut budget = crate::engine::ExecutionBudget::unlimited();
378 self.execute_sql_using_budget(sql, &mut budget)
379 }
380
381 pub(crate) fn execute_sql_with_budget(
382 &mut self,
383 sql: &str,
384 max_work: usize,
385 ) -> Result<Vec<StatementResult>, DbError> {
386 let mut budget = crate::engine::ExecutionBudget::bounded(max_work);
387 self.execute_sql_using_budget(sql, &mut budget)
388 }
389
390 pub(crate) fn execute_sql_using_budget(
391 &mut self,
392 sql: &str,
393 budget: &mut crate::engine::ExecutionBudget,
394 ) -> Result<Vec<StatementResult>, DbError> {
395 let statements = parse(sql).map_err(|e| {
396 dberr(
397 DbErrorKind::Syntax(e.message.clone()),
398 format!("{} at byte {}", e.message, e.offset),
399 )
400 })?;
401 let mut results = Vec::with_capacity(statements.len());
402 for statement in statements {
403 results.push(self.execute_with_budget(&statement, budget)?);
404 }
405 Ok(results)
406 }
407
408 pub fn in_transaction(&self) -> bool {
409 self.transaction.is_some()
410 }
411
412 pub fn generation(&self) -> u64 {
414 self.db.generation()
415 }
416}
417
418pub struct Transaction {
421 db: Database,
422 state: State,
423 base_generation: u64,
424 active: bool,
425 dirty: bool,
426}
427
428impl Transaction {
429 pub fn execute(&mut self, stmt: &Statement) -> Result<StatementResult, DbError> {
430 let mut budget = crate::engine::ExecutionBudget::unlimited();
431 self.execute_with_budget(stmt, &mut budget)
432 }
433
434 pub(crate) fn execute_with_budget(
435 &mut self,
436 stmt: &Statement,
437 budget: &mut crate::engine::ExecutionBudget,
438 ) -> Result<StatementResult, DbError> {
439 if !self.active {
440 return Err(dberr(DbErrorKind::Transaction, "transaction is closed"));
441 }
442 match stmt {
443 Statement::Begin | Statement::Commit | Statement::Rollback | Statement::Checkpoint => {
444 Err(dberr(
445 DbErrorKind::Transaction,
446 "transaction control is owned by the connection",
447 ))
448 }
449 _ => {
450 let result = crate::engine::execute_with_budget(&mut self.state, stmt, budget)?;
451 if is_mutation(&result) {
452 self.dirty = true;
453 }
454 Ok(result)
455 }
456 }
457 }
458
459 pub fn execute_sql(&mut self, sql: &str) -> Result<Vec<StatementResult>, DbError> {
460 let mut budget = crate::engine::ExecutionBudget::unlimited();
461 self.execute_sql_using_budget(sql, &mut budget)
462 }
463
464 fn execute_sql_using_budget(
465 &mut self,
466 sql: &str,
467 budget: &mut crate::engine::ExecutionBudget,
468 ) -> Result<Vec<StatementResult>, DbError> {
469 let statements = parse(sql).map_err(|e| {
470 dberr(
471 DbErrorKind::Syntax(e.message.clone()),
472 format!("{} at byte {}", e.message, e.offset),
473 )
474 })?;
475 let mut results = Vec::with_capacity(statements.len());
476 for statement in statements {
477 results.push(self.execute_with_budget(&statement, budget)?);
478 }
479 Ok(results)
480 }
481
482 pub fn commit(self) -> Result<u64, DbError> {
483 let mut budget = crate::engine::ExecutionBudget::unlimited();
484 self.commit_with_budget(&mut budget)
485 }
486
487 pub(crate) fn commit_with_budget(
488 mut self,
489 budget: &mut crate::engine::ExecutionBudget,
490 ) -> Result<u64, DbError> {
491 if !self.active {
492 return Err(dberr(DbErrorKind::Transaction, "transaction is closed"));
493 }
494 self.active = false;
495 if !self.dirty {
496 return Ok(self.db.generation());
497 }
498 budget.state_clone(&self.state, "preparing a database commit")?;
499 self.db.commit_state(self.state, self.base_generation)
500 }
501
502 pub fn rollback(mut self) {
503 self.active = false;
504 }
505
506 pub fn is_active(&self) -> bool {
507 self.active
508 }
509}
510
511fn is_mutation(result: &StatementResult) -> bool {
512 matches!(
513 result,
514 StatementResult::Insert { .. }
515 | StatementResult::Update { .. }
516 | StatementResult::Delete { .. }
517 | StatementResult::CreateTable { .. }
518 | StatementResult::DropTable { .. }
519 | StatementResult::CreateIndex { .. }
520 | StatementResult::DropIndex { .. }
521 )
522}
523
524fn wal_path(path: &Path) -> PathBuf {
525 let mut value = path.as_os_str().to_os_string();
526 value.push(".wal");
527 PathBuf::from(value)
528}
529
530fn acquire_lock(path: &Path) -> Result<File, DbError> {
531 if let Some(parent) = path
532 .parent()
533 .filter(|parent| !parent.as_os_str().is_empty())
534 {
535 fs::create_dir_all(parent).map_err(|error| {
536 dberr(
537 DbErrorKind::Io(format!("create database directory: {error}")),
538 format!("create database directory: {error}"),
539 )
540 })?;
541 }
542 let mut lock_os = path.as_os_str().to_os_string();
543 lock_os.push(".lock");
544 let lock_path = PathBuf::from(lock_os);
545 let file = OpenOptions::new()
546 .create(true)
547 .truncate(false)
548 .read(true)
549 .write(true)
550 .open(&lock_path)
551 .map_err(|error| {
552 dberr(
553 DbErrorKind::Io(format!("open database lock: {error}")),
554 format!("open database lock: {error}"),
555 )
556 })?;
557 match fs4::FileExt::try_lock(&file) {
558 Ok(()) => Ok(file),
559 Err(fs4::TryLockError::WouldBlock) => Err(dberr(
560 DbErrorKind::Busy,
561 format!("database is already open: {}", path.display()),
562 )),
563 Err(fs4::TryLockError::Error(error)) => Err(dberr(
564 DbErrorKind::Io(format!("lock database: {error}")),
565 format!("lock database: {error}"),
566 )),
567 }
568}
569
570fn acquire_workspace_lock(path: &Path) -> Result<Option<File>, DbError> {
571 if !path
572 .file_name()
573 .and_then(|name| name.to_str())
574 .is_some_and(|name| name.eq_ignore_ascii_case("data.basalt"))
575 {
576 return Ok(None);
577 }
578 let Some(parent) = path
579 .parent()
580 .filter(|parent| !parent.as_os_str().is_empty())
581 else {
582 return Ok(None);
583 };
584 let lock_path = parent.join(".workspace.lock");
585 let metadata = match fs::symlink_metadata(&lock_path) {
586 Ok(metadata) => metadata,
587 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
588 Err(error) => {
589 return Err(dberr(
590 DbErrorKind::Io(format!("inspect workspace lock: {error}")),
591 format!("inspect workspace lock: {error}"),
592 ));
593 }
594 };
595 if metadata.file_type().is_symlink() {
596 return Err(dberr(
597 DbErrorKind::Io("workspace lock cannot be a symbolic link".into()),
598 "workspace lock cannot be a symbolic link",
599 ));
600 }
601 let file = OpenOptions::new()
602 .read(true)
603 .write(true)
604 .open(&lock_path)
605 .map_err(|error| {
606 dberr(
607 DbErrorKind::Io(format!("open workspace lock: {error}")),
608 format!("open workspace lock: {error}"),
609 )
610 })?;
611 match fs4::FileExt::try_lock(&file) {
612 Ok(()) => Ok(Some(file)),
613 Err(fs4::TryLockError::WouldBlock) => Err(dberr(
614 DbErrorKind::Busy,
615 format!("workspace is already open: {}", parent.display()),
616 )),
617 Err(fs4::TryLockError::Error(error)) => Err(dberr(
618 DbErrorKind::Io(format!("lock workspace: {error}")),
619 format!("lock workspace: {error}"),
620 )),
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627 use std::fs;
628
629 #[test]
630 fn durable_commit_reopens() {
631 let dir = std::env::temp_dir().join(format!("basalt-db-{}", std::process::id()));
632 let _ = fs::remove_dir_all(&dir);
633 fs::create_dir_all(&dir).unwrap();
634 let path = dir.join("main.db");
635 {
636 let db = Database::open(&path).unwrap();
637 db.execute_sql(
638 "CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT); INSERT INTO t VALUES (1, 'one');",
639 )
640 .unwrap();
641 db.checkpoint().unwrap();
642 }
643 let db = Database::open(&path).unwrap();
644 let result = db.execute_sql("SELECT * FROM t").unwrap();
645 assert!(matches!(
646 &result[0],
647 StatementResult::Select { rows, .. } if rows.len() == 1
648 ));
649 let _ = fs::remove_dir_all(dir);
650 }
651
652 #[test]
653 fn concurrent_snapshot_conflicts() {
654 let db = Database::in_memory();
655 db.execute_sql("CREATE TABLE t (id INTEGER PRIMARY KEY)")
656 .unwrap();
657 let mut a = db.begin().unwrap();
658 let mut b = db.begin().unwrap();
659 a.execute_sql("INSERT INTO t VALUES (1)").unwrap();
660 b.execute_sql("INSERT INTO t VALUES (2)").unwrap();
661 a.commit().unwrap();
662 assert!(b.commit().is_err());
663 }
664
665 #[test]
666 fn bounded_sql_accounts_for_snapshot_and_keeps_failed_mutations_unpublished() {
667 let database = Database::in_memory();
668 database
669 .execute_sql("CREATE TABLE t (id INTEGER); INSERT INTO t VALUES (1)")
670 .unwrap();
671
672 let error = database
673 .execute_sql_with_budget("INSERT INTO t VALUES (2)", 5)
674 .unwrap_err();
675
676 assert_eq!(error.kind, DbErrorKind::Limit);
677 assert_eq!(database.row_count("t").unwrap(), 1);
678 }
679
680 #[test]
681 fn bounded_commit_rejects_before_publishing_its_snapshot() {
682 let database = Database::in_memory();
683 database.execute_sql("CREATE TABLE t (id INTEGER)").unwrap();
684 let mut transaction = database.begin().unwrap();
685 transaction.execute_sql("INSERT INTO t VALUES (1)").unwrap();
686 let mut budget = crate::engine::ExecutionBudget::bounded(0);
687
688 let error = transaction.commit_with_budget(&mut budget).unwrap_err();
689
690 assert_eq!(error.kind, DbErrorKind::Limit);
691 assert_eq!(database.row_count("t").unwrap(), 0);
692 }
693
694 #[test]
695 fn replays_wal_and_restores_user_indexes() {
696 let dir = std::env::temp_dir().join(format!("basalt-wal-recovery-{}", std::process::id()));
697 let _ = std::fs::remove_dir_all(&dir);
698 std::fs::create_dir_all(&dir).unwrap();
699 let path = dir.join("main.db");
700 {
701 let db = Database::open(&path).unwrap();
702 db.execute_sql("CREATE TABLE t (id INTEGER PRIMARY KEY, value INTEGER)")
703 .unwrap();
704 db.execute_sql("INSERT INTO t VALUES (1, 10), (2, 20), (3, 30)")
705 .unwrap();
706 db.execute_sql("CREATE INDEX value_idx ON t(value)")
707 .unwrap();
708 }
710 let db = Database::open(&path).unwrap();
711 let results = db.execute_sql("SELECT id FROM t WHERE value = 20").unwrap();
712 let StatementResult::Select { rows, .. } = &results[0] else {
713 panic!()
714 };
715 assert_eq!(rows.len(), 1);
716 assert_eq!(db.generation(), 3);
717 let _ = std::fs::remove_dir_all(dir);
718 }
719
720 #[test]
721 fn readers_and_writer_can_share_a_handle() {
722 let db = Database::in_memory();
723 db.execute_sql("CREATE TABLE t (id INTEGER PRIMARY KEY, value INTEGER)")
724 .unwrap();
725 db.execute_sql("INSERT INTO t VALUES (1, 0)").unwrap();
726 let writer_db = db.clone();
727 let writer = std::thread::spawn(move || {
728 for value in 1..=20 {
729 writer_db
730 .execute_sql(&format!("UPDATE t SET value = {value} WHERE id = 1"))
731 .unwrap();
732 }
733 });
734 let mut readers = Vec::new();
735 for _ in 0..4 {
736 let reader_db = db.clone();
737 readers.push(std::thread::spawn(move || {
738 for _ in 0..20 {
739 let result = reader_db.execute_sql("SELECT value FROM t").unwrap();
740 assert!(matches!(&result[0], StatementResult::Select { rows, .. } if rows.len() == 1));
741 }
742 }));
743 }
744 writer.join().unwrap();
745 for reader in readers {
746 reader.join().unwrap();
747 }
748 let result = db.execute_sql("SELECT value FROM t").unwrap();
749 assert!(
750 matches!(&result[0], StatementResult::Select { rows, .. } if rows[0][0] == crate::types::Value::Integer(20))
751 );
752 }
753
754 #[test]
755 fn relative_paths_are_supported() {
756 let filename = format!("basalt-relative-{}.tmp", std::process::id());
757 let path = std::path::Path::new(&filename);
758 let _ = std::fs::remove_file(path);
759 let _ = std::fs::remove_file(format!("{filename}.wal"));
760 let database = Database::open(path).unwrap();
761 database
762 .execute_sql("CREATE TABLE t (id INTEGER PRIMARY KEY)")
763 .unwrap();
764 database.checkpoint().unwrap();
765 assert!(path.exists());
766 drop(database);
767 let _ = std::fs::remove_file(path);
768 let _ = std::fs::remove_file(format!("{filename}.wal"));
769 }
770
771 #[test]
772 fn valid_wal_recovers_a_corrupt_snapshot() {
773 let dir =
774 std::env::temp_dir().join(format!("basalt-corrupt-recovery-{}", std::process::id()));
775 let _ = std::fs::remove_dir_all(&dir);
776 std::fs::create_dir_all(&dir).unwrap();
777 let path = dir.join("main.db");
778 {
779 let database = Database::open(&path).unwrap();
780 database
781 .execute_sql("CREATE TABLE t (id INTEGER PRIMARY KEY); INSERT INTO t VALUES (7)")
782 .unwrap();
783 }
784 let mut bytes = std::fs::read(&path).unwrap();
785 bytes[0] ^= 0xff;
786 std::fs::write(&path, bytes).unwrap();
787 let database = Database::open(&path).unwrap();
788 let result = database.execute_sql("SELECT * FROM t").unwrap();
789 assert!(matches!(&result[0], StatementResult::Select { rows, .. } if rows.len() == 1));
790 let _ = std::fs::remove_dir_all(dir);
791 }
792
793 #[test]
794 fn durable_database_rejects_a_second_open_handle() {
795 let dir = std::env::temp_dir().join(format!("basalt-db-lock-{}", std::process::id()));
796 let _ = fs::remove_dir_all(&dir);
797 fs::create_dir_all(&dir).unwrap();
798 let path = dir.join("main.db");
799 let first = Database::open(&path).unwrap();
800 let error = match Database::open(&path) {
801 Ok(_) => panic!("a second database open should be rejected"),
802 Err(error) => error,
803 };
804 assert_eq!(error.kind, DbErrorKind::Busy);
805 assert!(error.message.contains("already open"));
806 drop(first);
807 let second = Database::open(&path).unwrap();
808 drop(second);
809 let _ = fs::remove_dir_all(dir);
810 }
811}