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