Skip to main content

reddb_server/storage/engine/
database.rs

1//! RedDB Database Engine
2//!
3//! The main entry point for the RedDB storage engine. Integrates all components:
4//! - Pager for page I/O
5//! - WAL for durability
6//! - Transactions for ACID properties
7//! - Checkpointing for WAL management
8//! - B-tree for indexing
9//!
10//! # Usage
11//!
12//! ```rust,ignore
13//! use reddb::storage::engine::Database;
14//!
15//! // Open or create a database
16//! let db = Database::open("mydata.rdb")?;
17//!
18//! // Begin a transaction
19//! let tx = db.begin()?;
20//!
21//! // Perform operations
22//! tx.put(b"key", b"value")?;
23//!
24//! // Commit
25//! tx.commit()?;
26//!
27//! // Close (or let it drop)
28//! db.close()?;
29//! ```
30//!
31//! # File Layout
32//!
33//! ```text
34//! mydata.rdb     - Main database file (pages)
35//! engine WAL sidecar - Write-ahead log
36//! ```
37//!
38//! # References
39//!
40//! - Turso `core/database.rs` - Database lifecycle
41//! - SQLite architecture documentation
42
43use std::io;
44use std::path::{Path, PathBuf};
45use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
46
47use super::{Page, PageType, Pager, PagerConfig};
48use crate::storage::transaction::snapshot::SnapshotManager;
49use crate::storage::wal::{
50    CheckpointError, CheckpointMode, CheckpointResult, Checkpointer, Transaction,
51    TransactionManager, TxError,
52};
53
54/// Database configuration
55#[derive(Debug, Clone)]
56pub struct DatabaseConfig {
57    /// Page cache size (number of pages)
58    pub cache_size: usize,
59    /// Whether to open read-only
60    pub read_only: bool,
61    /// Whether to create if not exists
62    pub create: bool,
63    /// Checkpoint mode
64    pub checkpoint_mode: CheckpointMode,
65    /// Auto-checkpoint threshold (pages)
66    /// Set to 0 to disable auto-checkpoint
67    pub auto_checkpoint_threshold: u32,
68    /// Whether to verify checksums on read
69    pub verify_checksums: bool,
70}
71
72impl Default for DatabaseConfig {
73    fn default() -> Self {
74        Self {
75            cache_size: 10_000,
76            read_only: false,
77            create: true,
78            checkpoint_mode: CheckpointMode::Full,
79            auto_checkpoint_threshold: 1000,
80            verify_checksums: true,
81        }
82    }
83}
84
85/// Database error types
86#[derive(Debug)]
87pub enum DatabaseError {
88    /// I/O error
89    Io(io::Error),
90    /// Pager error
91    Pager(String),
92    /// Internal lock was poisoned by a panic
93    LockPoisoned(&'static str),
94    /// Transaction error
95    Transaction(TxError),
96    /// Checkpoint error
97    Checkpoint(CheckpointError),
98    /// Database is read-only
99    ReadOnly,
100    /// Database is closed
101    Closed,
102}
103
104impl std::fmt::Display for DatabaseError {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        match self {
107            Self::Io(e) => write!(f, "I/O error: {}", e),
108            Self::Pager(msg) => write!(f, "Pager error: {}", msg),
109            Self::LockPoisoned(name) => write!(f, "Lock poisoned: {}", name),
110            Self::Transaction(e) => write!(f, "Transaction error: {}", e),
111            Self::Checkpoint(e) => write!(f, "Checkpoint error: {}", e),
112            Self::ReadOnly => write!(f, "Database is read-only"),
113            Self::Closed => write!(f, "Database is closed"),
114        }
115    }
116}
117
118impl std::error::Error for DatabaseError {}
119
120impl From<io::Error> for DatabaseError {
121    fn from(e: io::Error) -> Self {
122        Self::Io(e)
123    }
124}
125
126impl From<TxError> for DatabaseError {
127    fn from(e: TxError) -> Self {
128        Self::Transaction(e)
129    }
130}
131
132impl From<CheckpointError> for DatabaseError {
133    fn from(e: CheckpointError) -> Self {
134        Self::Checkpoint(e)
135    }
136}
137
138/// Database state
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140enum DbState {
141    Open,
142    Closed,
143}
144
145/// RedDB Database Engine
146///
147/// The main entry point for database operations. Thread-safe.
148pub struct Database {
149    /// Database file path
150    path: PathBuf,
151    /// WAL file path
152    wal_path: PathBuf,
153    /// Pager (shared)
154    pager: Arc<Pager>,
155    /// Transaction manager (shared)
156    tx_manager: Arc<TransactionManager>,
157    /// Configuration
158    config: DatabaseConfig,
159    /// Database state
160    state: RwLock<DbState>,
161    /// Pages written since last checkpoint
162    pages_since_checkpoint: RwLock<u32>,
163    /// Background writer handle (P6.T1). `None` when the database
164    /// runs in read-only mode or the spawn failed. Dropping the
165    /// handle signals the writer thread to exit at the next round.
166    #[allow(dead_code)]
167    bgwriter: Option<crate::storage::cache::bgwriter::BgWriterHandle>,
168}
169
170impl Database {
171    fn state_read(&self) -> Result<RwLockReadGuard<'_, DbState>, DatabaseError> {
172        self.state
173            .read()
174            .map_err(|_| DatabaseError::LockPoisoned("database state"))
175    }
176
177    fn state_write(&self) -> Result<RwLockWriteGuard<'_, DbState>, DatabaseError> {
178        self.state
179            .write()
180            .map_err(|_| DatabaseError::LockPoisoned("database state"))
181    }
182
183    fn pages_since_checkpoint_read(&self) -> Result<RwLockReadGuard<'_, u32>, DatabaseError> {
184        self.pages_since_checkpoint
185            .read()
186            .map_err(|_| DatabaseError::LockPoisoned("pages since checkpoint"))
187    }
188
189    fn pages_since_checkpoint_write(&self) -> Result<RwLockWriteGuard<'_, u32>, DatabaseError> {
190        self.pages_since_checkpoint
191            .write()
192            .map_err(|_| DatabaseError::LockPoisoned("pages since checkpoint"))
193    }
194
195    /// Open or create a database
196    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, DatabaseError> {
197        Self::open_with_config(path, DatabaseConfig::default())
198    }
199
200    /// Background-writer stats snapshot — `None` when the writer
201    /// isn't running (read-only mode, or spawn skipped). Exposed for
202    /// tests + introspection.
203    pub fn bgwriter_stats(&self) -> Option<crate::storage::cache::bgwriter::BgWriterStatsSnapshot> {
204        self.bgwriter.as_ref().map(|h| h.stats.snapshot())
205    }
206
207    /// Open or create a database with custom configuration
208    pub fn open_with_config<P: AsRef<Path>>(
209        path: P,
210        config: DatabaseConfig,
211    ) -> Result<Self, DatabaseError> {
212        let path = path.as_ref().to_path_buf();
213        let wal_path = reddb_file::layout::engine_wal_path(&path);
214
215        // Create pager config
216        let pager_config = PagerConfig {
217            cache_size: config.cache_size,
218            read_only: config.read_only,
219            create: config.create,
220            verify_checksums: config.verify_checksums,
221            double_write: true,
222            encryption: None,
223        };
224
225        // Open pager
226        let pager =
227            Pager::open(&path, pager_config).map_err(|e| DatabaseError::Pager(e.to_string()))?;
228        let pager = Arc::new(pager);
229        let snapshot_manager = Arc::new(SnapshotManager::new());
230
231        // Perform crash recovery if WAL exists
232        if wal_path.exists() && !config.read_only {
233            let recovery_result = Checkpointer::recover(&pager, &wal_path)?;
234            snapshot_manager.observe_committed_xid(recovery_result.max_transaction_id);
235            if recovery_result.pages_checkpointed > 0 {
236                tracing::info!(
237                    transactions = recovery_result.transactions_processed,
238                    pages = recovery_result.pages_checkpointed,
239                    "WAL recovery applied"
240                );
241            }
242        }
243
244        // Create transaction manager
245        let tx_manager = Arc::new(
246            TransactionManager::new_with_snapshot_manager(
247                Arc::clone(&pager),
248                &wal_path,
249                snapshot_manager,
250            )
251            .map_err(DatabaseError::Io)?,
252        );
253
254        // P6.T1 — bgwriter wiring is gated behind `REDDB_BGWRITER=1`
255        // for now. The current `PagerDirtyFlusher::flush_some` calls
256        // `write_page` directly without enforcing WAL-first ordering;
257        // bench `update_single` triggered "B-tree insert error: Pager
258        // error: I/O error: failed to fill whole buffer" when the
259        // background flusher raced with the foreground commit on the
260        // same dirty page. Off by default until the flusher is
261        // taught to respect the per-page LSN gate the commit path
262        // uses (max_lsn → wal.flush(max_lsn) → write_page).
263        let bgwriter = if config.read_only
264            || !matches!(
265                std::env::var("REDDB_BGWRITER").ok().as_deref(),
266                Some("1") | Some("true") | Some("on")
267            ) {
268            None
269        } else {
270            let flusher = std::sync::Arc::new(
271                crate::storage::cache::bgwriter::PagerDirtyFlusher::new(Arc::downgrade(&pager)),
272            );
273            Some(crate::storage::cache::bgwriter::spawn(
274                flusher,
275                crate::storage::cache::bgwriter::BgWriterConfig::default(),
276            ))
277        };
278
279        Ok(Self {
280            path,
281            wal_path,
282            pager,
283            tx_manager,
284            config,
285            state: RwLock::new(DbState::Open),
286            pages_since_checkpoint: RwLock::new(0),
287            bgwriter,
288        })
289    }
290
291    /// Check if database is open
292    fn check_open(&self) -> Result<(), DatabaseError> {
293        if *self.state_read()? == DbState::Closed {
294            return Err(DatabaseError::Closed);
295        }
296        Ok(())
297    }
298
299    /// Begin a new transaction
300    pub fn begin(&self) -> Result<Transaction, DatabaseError> {
301        self.check_open()?;
302        Ok(self.tx_manager.begin()?)
303    }
304
305    /// Get a reference to the pager (for advanced operations)
306    pub fn pager(&self) -> &Arc<Pager> {
307        &self.pager
308    }
309
310    /// Get a reference to the transaction manager
311    pub fn tx_manager(&self) -> &Arc<TransactionManager> {
312        &self.tx_manager
313    }
314
315    /// Allocate a new page
316    pub fn allocate_page(&self, page_type: PageType) -> Result<Page, DatabaseError> {
317        self.check_open()?;
318        if self.config.read_only {
319            return Err(DatabaseError::ReadOnly);
320        }
321        self.pager
322            .allocate_page(page_type)
323            .map_err(|e| DatabaseError::Pager(e.to_string()))
324    }
325
326    /// Read a page
327    pub fn read_page(&self, page_id: u32) -> Result<Page, DatabaseError> {
328        self.check_open()?;
329        self.pager
330            .read_page(page_id)
331            .map_err(|e| DatabaseError::Pager(e.to_string()))
332    }
333
334    /// Perform a checkpoint
335    pub fn checkpoint(&self) -> Result<CheckpointResult, DatabaseError> {
336        self.check_open()?;
337        if self.config.read_only {
338            return Err(DatabaseError::ReadOnly);
339        }
340
341        let checkpointer = Checkpointer::new(self.config.checkpoint_mode);
342        let result = checkpointer.checkpoint(&self.pager, &self.wal_path)?;
343
344        // Reset counter
345        *self.pages_since_checkpoint_write()? = 0;
346
347        Ok(result)
348    }
349
350    /// Check if auto-checkpoint is needed and perform it
351    pub fn maybe_auto_checkpoint(&self) -> Result<Option<CheckpointResult>, DatabaseError> {
352        if self.config.auto_checkpoint_threshold == 0 {
353            return Ok(None);
354        }
355
356        let pages = *self.pages_since_checkpoint_read()?;
357        if pages >= self.config.auto_checkpoint_threshold {
358            Ok(Some(self.checkpoint()?))
359        } else {
360            Ok(None)
361        }
362    }
363
364    /// Increment pages-since-checkpoint counter
365    pub fn increment_page_count(&self, count: u32) {
366        let mut pages = self
367            .pages_since_checkpoint
368            .write()
369            .unwrap_or_else(|poisoned| poisoned.into_inner());
370        *pages = pages.saturating_add(count);
371    }
372
373    /// Sync all data to disk
374    pub fn sync(&self) -> Result<(), DatabaseError> {
375        self.check_open()?;
376        self.pager
377            .sync()
378            .map_err(|e| DatabaseError::Pager(e.to_string()))?;
379        self.tx_manager.sync_wal()?;
380        Ok(())
381    }
382
383    /// Close the database
384    ///
385    /// Performs a final checkpoint and syncs all data to disk.
386    pub fn close(self) -> Result<(), DatabaseError> {
387        // Mark as closed
388        *self.state_write()? = DbState::Closed;
389
390        // Wait for active transactions to complete
391        if self.tx_manager.has_active_transactions() {
392            tracing::warn!("closing database with active transactions");
393        }
394
395        // Final checkpoint if not read-only
396        if !self.config.read_only {
397            let checkpointer = Checkpointer::new(CheckpointMode::Truncate);
398            let _ = checkpointer.checkpoint(&self.pager, &self.wal_path);
399        }
400
401        // Sync pager
402        let _ = self.pager.sync();
403
404        Ok(())
405    }
406
407    /// Get database file path
408    pub fn path(&self) -> &Path {
409        &self.path
410    }
411
412    /// Get WAL file path
413    pub fn wal_path(&self) -> &Path {
414        &self.wal_path
415    }
416
417    /// Check if database is read-only
418    pub fn is_read_only(&self) -> bool {
419        self.config.read_only
420    }
421
422    /// Get page count
423    pub fn page_count(&self) -> u32 {
424        self.pager.page_count().unwrap_or(0)
425    }
426
427    /// Get database file size
428    pub fn file_size(&self) -> Result<u64, DatabaseError> {
429        self.pager
430            .file_size()
431            .map_err(|e| DatabaseError::Pager(e.to_string()))
432    }
433
434    /// Get cache statistics
435    pub fn cache_stats(&self) -> super::page_cache::CacheStats {
436        self.pager.cache_stats()
437    }
438}
439
440impl Drop for Database {
441    fn drop(&mut self) {
442        // Try to sync on drop
443        let state = self
444            .state
445            .read()
446            .unwrap_or_else(|poisoned| poisoned.into_inner());
447        if *state == DbState::Open {
448            drop(state);
449            let mut state = self
450                .state
451                .write()
452                .unwrap_or_else(|poisoned| poisoned.into_inner());
453            *state = DbState::Closed;
454            drop(state);
455
456            // Best-effort checkpoint and sync
457            if !self.config.read_only {
458                let checkpointer = Checkpointer::new(CheckpointMode::Full);
459                let _ = checkpointer.checkpoint(&self.pager, &self.wal_path);
460            }
461            let _ = self.pager.sync();
462        }
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469    use std::fs;
470    use std::time::{SystemTime, UNIX_EPOCH};
471
472    struct TempDbPath(PathBuf);
473
474    impl std::ops::Deref for TempDbPath {
475        type Target = PathBuf;
476
477        fn deref(&self) -> &PathBuf {
478            &self.0
479        }
480    }
481
482    impl AsRef<Path> for TempDbPath {
483        fn as_ref(&self) -> &Path {
484            &self.0
485        }
486    }
487
488    impl Drop for TempDbPath {
489        fn drop(&mut self) {
490            cleanup(&self.0);
491        }
492    }
493
494    fn temp_db_path() -> TempDbPath {
495        let timestamp = SystemTime::now()
496            .duration_since(UNIX_EPOCH)
497            .unwrap()
498            .as_nanos();
499        TempDbPath(std::env::temp_dir().join(format!("reddb_engine_test_{}.rdb", timestamp)))
500    }
501
502    fn cleanup(path: &Path) {
503        let _ = fs::remove_file(path);
504        let wal_path = reddb_file::layout::engine_wal_path(path);
505        let _ = fs::remove_file(wal_path);
506        for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
507            let _ = fs::remove_file(sidecar);
508        }
509    }
510
511    #[test]
512    fn test_database_open_create() {
513        let path = temp_db_path();
514        cleanup(&path);
515
516        {
517            let db = Database::open(&path).unwrap();
518            assert!(!db.is_read_only());
519            assert_eq!(db.page_count(), 3); // Header + reserved pages
520        }
521
522        // Should be able to reopen
523        {
524            let db = Database::open(&path).unwrap();
525            assert_eq!(db.page_count(), 3);
526        }
527
528        cleanup(&path);
529    }
530
531    #[test]
532    fn test_database_transaction() {
533        let path = temp_db_path();
534        cleanup(&path);
535
536        {
537            let db = Database::open(&path).unwrap();
538
539            // Allocate a page
540            let page = db.allocate_page(PageType::BTreeLeaf).unwrap();
541            let page_id = page.page_id();
542
543            // Begin transaction
544            let mut tx = db.begin().unwrap();
545
546            // Write through transaction
547            let mut page = Page::new(PageType::BTreeLeaf, page_id);
548            page.as_bytes_mut()[100] = 0xAB;
549            tx.write_page(page_id, page).unwrap();
550
551            // Commit
552            tx.commit().unwrap();
553
554            // Verify
555            let read_page = db.read_page(page_id).unwrap();
556            assert_eq!(read_page.as_bytes()[100], 0xAB);
557        }
558
559        cleanup(&path);
560    }
561
562    #[test]
563    fn test_database_crash_recovery() {
564        let path = temp_db_path();
565        cleanup(&path);
566
567        let page_id;
568
569        // First session: write data but don't checkpoint
570        {
571            let db = Database::open(&path).unwrap();
572
573            // Allocate a page
574            let page = db.allocate_page(PageType::BTreeLeaf).unwrap();
575            page_id = page.page_id();
576
577            // Write through transaction
578            let mut tx = db.begin().unwrap();
579            let mut page = Page::new(PageType::BTreeLeaf, page_id);
580            page.as_bytes_mut()[100] = 0xCD;
581            tx.write_page(page_id, page).unwrap();
582            tx.commit().unwrap();
583
584            // Sync WAL but don't checkpoint
585            db.sync().unwrap();
586
587            // Drop without calling close (simulate crash)
588        }
589
590        // Second session: should recover from WAL
591        {
592            let db = Database::open(&path).unwrap();
593
594            // Data should be recovered
595            let read_page = db.read_page(page_id).unwrap();
596            assert_eq!(read_page.as_bytes()[100], 0xCD);
597        }
598
599        cleanup(&path);
600    }
601
602    #[test]
603    fn test_database_checkpoint() {
604        let path = temp_db_path();
605        cleanup(&path);
606
607        {
608            let db = Database::open(&path).unwrap();
609
610            // Allocate pages
611            let page1 = db.allocate_page(PageType::BTreeLeaf).unwrap();
612            let page2 = db.allocate_page(PageType::BTreeLeaf).unwrap();
613
614            // Write through transactions
615            let mut tx1 = db.begin().unwrap();
616            let mut p1 = Page::new(PageType::BTreeLeaf, page1.page_id());
617            p1.as_bytes_mut()[100] = 0x11;
618            tx1.write_page(page1.page_id(), p1).unwrap();
619            tx1.commit().unwrap();
620
621            let mut tx2 = db.begin().unwrap();
622            let mut p2 = Page::new(PageType::BTreeLeaf, page2.page_id());
623            p2.as_bytes_mut()[100] = 0x22;
624            tx2.write_page(page2.page_id(), p2).unwrap();
625            tx2.commit().unwrap();
626
627            // Checkpoint
628            let result = db.checkpoint().unwrap();
629            assert_eq!(result.transactions_processed, 2);
630            assert!(result.pages_checkpointed >= 2);
631
632            // Close properly
633            db.close().unwrap();
634        }
635
636        // Reopen and verify
637        {
638            let db = Database::open(&path).unwrap();
639            // Pages should still be there
640            assert!(db.page_count() >= 3); // header + 2 data pages
641        }
642
643        cleanup(&path);
644    }
645
646    #[test]
647    fn test_database_read_only() {
648        let path = temp_db_path();
649        cleanup(&path);
650
651        // Create database first
652        {
653            let db = Database::open(&path).unwrap();
654            let page = db.allocate_page(PageType::BTreeLeaf).unwrap();
655            db.close().unwrap();
656        }
657
658        // Open read-only
659        {
660            let config = DatabaseConfig {
661                read_only: true,
662                ..Default::default()
663            };
664            let db = Database::open_with_config(&path, config).unwrap();
665            assert!(db.is_read_only());
666
667            // Should not be able to allocate
668            assert!(db.allocate_page(PageType::BTreeLeaf).is_err());
669        }
670
671        cleanup(&path);
672    }
673
674    #[test]
675    fn test_database_multiple_transactions() {
676        let path = temp_db_path();
677        cleanup(&path);
678
679        {
680            let db = Database::open(&path).unwrap();
681
682            // Allocate pages
683            let page1 = db.allocate_page(PageType::BTreeLeaf).unwrap();
684            let page2 = db.allocate_page(PageType::BTreeLeaf).unwrap();
685
686            // Multiple concurrent transactions (interleaved)
687            let mut tx1 = db.begin().unwrap();
688            let mut tx2 = db.begin().unwrap();
689
690            // tx1 writes to page1
691            let mut p1 = Page::new(PageType::BTreeLeaf, page1.page_id());
692            p1.as_bytes_mut()[100] = 0x11;
693            tx1.write_page(page1.page_id(), p1).unwrap();
694
695            // tx2 writes to page2
696            let mut p2 = Page::new(PageType::BTreeLeaf, page2.page_id());
697            p2.as_bytes_mut()[100] = 0x22;
698            tx2.write_page(page2.page_id(), p2).unwrap();
699
700            // Commit both
701            tx1.commit().unwrap();
702            tx2.commit().unwrap();
703
704            // Verify
705            let r1 = db.read_page(page1.page_id()).unwrap();
706            let r2 = db.read_page(page2.page_id()).unwrap();
707            assert_eq!(r1.as_bytes()[100], 0x11);
708            assert_eq!(r2.as_bytes()[100], 0x22);
709        }
710
711        cleanup(&path);
712    }
713
714    #[test]
715    fn test_begin_returns_structured_error_when_state_lock_is_poisoned() {
716        let path = temp_db_path();
717        cleanup(&path);
718
719        {
720            let db = Arc::new(Database::open(&path).unwrap());
721            let poison_target = Arc::clone(&db);
722            let _ = std::thread::spawn(move || {
723                let _guard = poison_target
724                    .state
725                    .write()
726                    .expect("state lock should be acquired");
727                panic!("poison database state");
728            })
729            .join();
730
731            match db.begin() {
732                Ok(_) => panic!("begin should fail after state lock poisoning"),
733                Err(err) => {
734                    assert!(matches!(err, DatabaseError::LockPoisoned("database state")))
735                }
736            }
737        }
738
739        cleanup(&path);
740    }
741}