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            disk_residency: false,
218            cache_size: config.cache_size,
219            read_only: config.read_only,
220            create: config.create,
221            verify_checksums: config.verify_checksums,
222            double_write: true,
223            encryption: None,
224        };
225
226        // Open pager
227        let pager =
228            Pager::open(&path, pager_config).map_err(|e| DatabaseError::Pager(e.to_string()))?;
229        let pager = Arc::new(pager);
230        let snapshot_manager = Arc::new(SnapshotManager::new());
231
232        // Perform crash recovery if WAL exists
233        if wal_path.exists() && !config.read_only {
234            let recovery_result = Checkpointer::recover(&pager, &wal_path)?;
235            snapshot_manager.observe_committed_xid(recovery_result.max_transaction_id);
236            if recovery_result.pages_checkpointed > 0 {
237                tracing::info!(
238                    transactions = recovery_result.transactions_processed,
239                    pages = recovery_result.pages_checkpointed,
240                    "WAL recovery applied"
241                );
242            }
243        }
244
245        // Create transaction manager
246        let tx_manager = Arc::new(
247            TransactionManager::new_with_snapshot_manager(
248                Arc::clone(&pager),
249                &wal_path,
250                snapshot_manager,
251            )
252            .map_err(DatabaseError::Io)?,
253        );
254
255        // P6.T1 — bgwriter wiring is gated behind `REDDB_BGWRITER=1`
256        // for now. The current `PagerDirtyFlusher::flush_some` calls
257        // `write_page` directly without enforcing WAL-first ordering;
258        // bench `update_single` triggered "B-tree insert error: Pager
259        // error: I/O error: failed to fill whole buffer" when the
260        // background flusher raced with the foreground commit on the
261        // same dirty page. Off by default until the flusher is
262        // taught to respect the per-page LSN gate the commit path
263        // uses (max_lsn → wal.flush(max_lsn) → write_page).
264        let bgwriter = if config.read_only
265            || !matches!(
266                std::env::var("REDDB_BGWRITER").ok().as_deref(),
267                Some("1") | Some("true") | Some("on")
268            ) {
269            None
270        } else {
271            let flusher = std::sync::Arc::new(
272                crate::storage::cache::bgwriter::PagerDirtyFlusher::new(Arc::downgrade(&pager)),
273            );
274            Some(crate::storage::cache::bgwriter::spawn(
275                flusher,
276                crate::storage::cache::bgwriter::BgWriterConfig::default(),
277            ))
278        };
279
280        Ok(Self {
281            path,
282            wal_path,
283            pager,
284            tx_manager,
285            config,
286            state: RwLock::new(DbState::Open),
287            pages_since_checkpoint: RwLock::new(0),
288            bgwriter,
289        })
290    }
291
292    /// Check if database is open
293    fn check_open(&self) -> Result<(), DatabaseError> {
294        if *self.state_read()? == DbState::Closed {
295            return Err(DatabaseError::Closed);
296        }
297        Ok(())
298    }
299
300    /// Begin a new transaction
301    pub fn begin(&self) -> Result<Transaction, DatabaseError> {
302        self.check_open()?;
303        Ok(self.tx_manager.begin()?)
304    }
305
306    /// Get a reference to the pager (for advanced operations)
307    pub fn pager(&self) -> &Arc<Pager> {
308        &self.pager
309    }
310
311    /// Get a reference to the transaction manager
312    pub fn tx_manager(&self) -> &Arc<TransactionManager> {
313        &self.tx_manager
314    }
315
316    /// Allocate a new page
317    pub fn allocate_page(&self, page_type: PageType) -> Result<Page, DatabaseError> {
318        self.check_open()?;
319        if self.config.read_only {
320            return Err(DatabaseError::ReadOnly);
321        }
322        self.pager
323            .allocate_page(page_type)
324            .map_err(|e| DatabaseError::Pager(e.to_string()))
325    }
326
327    /// Read a page
328    pub fn read_page(&self, page_id: u32) -> Result<Page, DatabaseError> {
329        self.check_open()?;
330        self.pager
331            .read_page(page_id)
332            .map_err(|e| DatabaseError::Pager(e.to_string()))
333    }
334
335    /// Perform a checkpoint
336    pub fn checkpoint(&self) -> Result<CheckpointResult, DatabaseError> {
337        self.check_open()?;
338        if self.config.read_only {
339            return Err(DatabaseError::ReadOnly);
340        }
341
342        let checkpointer = Checkpointer::new(self.config.checkpoint_mode);
343        let result = checkpointer.checkpoint(&self.pager, &self.wal_path)?;
344
345        // Reset counter
346        *self.pages_since_checkpoint_write()? = 0;
347
348        Ok(result)
349    }
350
351    /// Check if auto-checkpoint is needed and perform it
352    pub fn maybe_auto_checkpoint(&self) -> Result<Option<CheckpointResult>, DatabaseError> {
353        if self.config.auto_checkpoint_threshold == 0 {
354            return Ok(None);
355        }
356
357        let pages = *self.pages_since_checkpoint_read()?;
358        if pages >= self.config.auto_checkpoint_threshold {
359            Ok(Some(self.checkpoint()?))
360        } else {
361            Ok(None)
362        }
363    }
364
365    /// Increment pages-since-checkpoint counter
366    pub fn increment_page_count(&self, count: u32) {
367        let mut pages = self
368            .pages_since_checkpoint
369            .write()
370            .unwrap_or_else(|poisoned| poisoned.into_inner());
371        *pages = pages.saturating_add(count);
372    }
373
374    /// Sync all data to disk
375    pub fn sync(&self) -> Result<(), DatabaseError> {
376        self.check_open()?;
377        self.pager
378            .sync()
379            .map_err(|e| DatabaseError::Pager(e.to_string()))?;
380        self.tx_manager.sync_wal()?;
381        Ok(())
382    }
383
384    /// Close the database
385    ///
386    /// Performs a final checkpoint and syncs all data to disk.
387    pub fn close(self) -> Result<(), DatabaseError> {
388        // Mark as closed
389        *self.state_write()? = DbState::Closed;
390
391        // Wait for active transactions to complete
392        if self.tx_manager.has_active_transactions() {
393            tracing::warn!("closing database with active transactions");
394        }
395
396        // Final checkpoint if not read-only
397        if !self.config.read_only {
398            let checkpointer = Checkpointer::new(CheckpointMode::Truncate);
399            let _ = checkpointer.checkpoint(&self.pager, &self.wal_path);
400        }
401
402        // Sync pager
403        let _ = self.pager.sync();
404
405        Ok(())
406    }
407
408    /// Get database file path
409    pub fn path(&self) -> &Path {
410        &self.path
411    }
412
413    /// Get WAL file path
414    pub fn wal_path(&self) -> &Path {
415        &self.wal_path
416    }
417
418    /// Check if database is read-only
419    pub fn is_read_only(&self) -> bool {
420        self.config.read_only
421    }
422
423    /// Get page count
424    pub fn page_count(&self) -> u32 {
425        self.pager.page_count().unwrap_or(0)
426    }
427
428    /// Get database file size
429    pub fn file_size(&self) -> Result<u64, DatabaseError> {
430        self.pager
431            .file_size()
432            .map_err(|e| DatabaseError::Pager(e.to_string()))
433    }
434
435    /// Get cache statistics
436    pub fn cache_stats(&self) -> super::page_cache::CacheStats {
437        self.pager.cache_stats()
438    }
439}
440
441impl Drop for Database {
442    fn drop(&mut self) {
443        // Try to sync on drop
444        let state = self
445            .state
446            .read()
447            .unwrap_or_else(|poisoned| poisoned.into_inner());
448        if *state == DbState::Open {
449            drop(state);
450            let mut state = self
451                .state
452                .write()
453                .unwrap_or_else(|poisoned| poisoned.into_inner());
454            *state = DbState::Closed;
455            drop(state);
456
457            // Best-effort checkpoint and sync
458            if !self.config.read_only {
459                let checkpointer = Checkpointer::new(CheckpointMode::Full);
460                let _ = checkpointer.checkpoint(&self.pager, &self.wal_path);
461            }
462            let _ = self.pager.sync();
463        }
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470    use std::fs;
471    use std::time::{SystemTime, UNIX_EPOCH};
472
473    struct TempDbPath(PathBuf);
474
475    impl std::ops::Deref for TempDbPath {
476        type Target = PathBuf;
477
478        fn deref(&self) -> &PathBuf {
479            &self.0
480        }
481    }
482
483    impl AsRef<Path> for TempDbPath {
484        fn as_ref(&self) -> &Path {
485            &self.0
486        }
487    }
488
489    impl Drop for TempDbPath {
490        fn drop(&mut self) {
491            cleanup(&self.0);
492        }
493    }
494
495    fn temp_db_path() -> TempDbPath {
496        let timestamp = SystemTime::now()
497            .duration_since(UNIX_EPOCH)
498            .unwrap()
499            .as_nanos();
500        TempDbPath(std::env::temp_dir().join(format!("reddb_engine_test_{}.rdb", timestamp)))
501    }
502
503    fn cleanup(path: &Path) {
504        let _ = fs::remove_file(path);
505        let wal_path = reddb_file::layout::engine_wal_path(path);
506        let _ = fs::remove_file(wal_path);
507        for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
508            let _ = fs::remove_file(sidecar);
509        }
510    }
511
512    #[test]
513    fn test_database_open_create() {
514        let path = temp_db_path();
515        cleanup(&path);
516
517        {
518            let db = Database::open(&path).unwrap();
519            assert!(!db.is_read_only());
520            // Header + reserved metadata/vault pages + ADR 0038 phase-3
521            // in-file double-write zone (FIRST_ALLOCATABLE_PAGE = 67).
522            assert_eq!(db.page_count(), 67);
523        }
524
525        // Should be able to reopen
526        {
527            let db = Database::open(&path).unwrap();
528            assert_eq!(db.page_count(), 67);
529        }
530
531        cleanup(&path);
532    }
533
534    #[test]
535    fn test_database_transaction() {
536        let path = temp_db_path();
537        cleanup(&path);
538
539        {
540            let db = Database::open(&path).unwrap();
541
542            // Allocate a page
543            let page = db.allocate_page(PageType::BTreeLeaf).unwrap();
544            let page_id = page.page_id();
545
546            // Begin transaction
547            let mut tx = db.begin().unwrap();
548
549            // Write through transaction
550            let mut page = Page::new(PageType::BTreeLeaf, page_id);
551            page.as_bytes_mut()[100] = 0xAB;
552            tx.write_page(page_id, page).unwrap();
553
554            // Commit
555            tx.commit().unwrap();
556
557            // Verify
558            let read_page = db.read_page(page_id).unwrap();
559            assert_eq!(read_page.as_bytes()[100], 0xAB);
560        }
561
562        cleanup(&path);
563    }
564
565    #[test]
566    fn test_database_crash_recovery() {
567        let path = temp_db_path();
568        cleanup(&path);
569
570        let page_id;
571
572        // First session: write data but don't checkpoint
573        {
574            let db = Database::open(&path).unwrap();
575
576            // Allocate a page
577            let page = db.allocate_page(PageType::BTreeLeaf).unwrap();
578            page_id = page.page_id();
579
580            // Write through transaction
581            let mut tx = db.begin().unwrap();
582            let mut page = Page::new(PageType::BTreeLeaf, page_id);
583            page.as_bytes_mut()[100] = 0xCD;
584            tx.write_page(page_id, page).unwrap();
585            tx.commit().unwrap();
586
587            // Sync WAL but don't checkpoint
588            db.sync().unwrap();
589
590            // Drop without calling close (simulate crash)
591        }
592
593        // Second session: should recover from WAL
594        {
595            let db = Database::open(&path).unwrap();
596
597            // Data should be recovered
598            let read_page = db.read_page(page_id).unwrap();
599            assert_eq!(read_page.as_bytes()[100], 0xCD);
600        }
601
602        cleanup(&path);
603    }
604
605    #[test]
606    fn test_database_checkpoint() {
607        let path = temp_db_path();
608        cleanup(&path);
609
610        {
611            let db = Database::open(&path).unwrap();
612
613            // Allocate pages
614            let page1 = db.allocate_page(PageType::BTreeLeaf).unwrap();
615            let page2 = db.allocate_page(PageType::BTreeLeaf).unwrap();
616
617            // Write through transactions
618            let mut tx1 = db.begin().unwrap();
619            let mut p1 = Page::new(PageType::BTreeLeaf, page1.page_id());
620            p1.as_bytes_mut()[100] = 0x11;
621            tx1.write_page(page1.page_id(), p1).unwrap();
622            tx1.commit().unwrap();
623
624            let mut tx2 = db.begin().unwrap();
625            let mut p2 = Page::new(PageType::BTreeLeaf, page2.page_id());
626            p2.as_bytes_mut()[100] = 0x22;
627            tx2.write_page(page2.page_id(), p2).unwrap();
628            tx2.commit().unwrap();
629
630            // Checkpoint
631            let result = db.checkpoint().unwrap();
632            assert_eq!(result.transactions_processed, 2);
633            assert!(result.pages_checkpointed >= 2);
634
635            // Close properly
636            db.close().unwrap();
637        }
638
639        // Reopen and verify
640        {
641            let db = Database::open(&path).unwrap();
642            // Pages should still be there
643            assert!(db.page_count() >= 3); // header + 2 data pages
644        }
645
646        cleanup(&path);
647    }
648
649    #[test]
650    fn test_database_read_only() {
651        let path = temp_db_path();
652        cleanup(&path);
653
654        // Create database first
655        {
656            let db = Database::open(&path).unwrap();
657            let page = db.allocate_page(PageType::BTreeLeaf).unwrap();
658            db.close().unwrap();
659        }
660
661        // Open read-only
662        {
663            let config = DatabaseConfig {
664                read_only: true,
665                ..Default::default()
666            };
667            let db = Database::open_with_config(&path, config).unwrap();
668            assert!(db.is_read_only());
669
670            // Should not be able to allocate
671            assert!(db.allocate_page(PageType::BTreeLeaf).is_err());
672        }
673
674        cleanup(&path);
675    }
676
677    #[test]
678    fn test_database_multiple_transactions() {
679        let path = temp_db_path();
680        cleanup(&path);
681
682        {
683            let db = Database::open(&path).unwrap();
684
685            // Allocate pages
686            let page1 = db.allocate_page(PageType::BTreeLeaf).unwrap();
687            let page2 = db.allocate_page(PageType::BTreeLeaf).unwrap();
688
689            // Multiple concurrent transactions (interleaved)
690            let mut tx1 = db.begin().unwrap();
691            let mut tx2 = db.begin().unwrap();
692
693            // tx1 writes to page1
694            let mut p1 = Page::new(PageType::BTreeLeaf, page1.page_id());
695            p1.as_bytes_mut()[100] = 0x11;
696            tx1.write_page(page1.page_id(), p1).unwrap();
697
698            // tx2 writes to page2
699            let mut p2 = Page::new(PageType::BTreeLeaf, page2.page_id());
700            p2.as_bytes_mut()[100] = 0x22;
701            tx2.write_page(page2.page_id(), p2).unwrap();
702
703            // Commit both
704            tx1.commit().unwrap();
705            tx2.commit().unwrap();
706
707            // Verify
708            let r1 = db.read_page(page1.page_id()).unwrap();
709            let r2 = db.read_page(page2.page_id()).unwrap();
710            assert_eq!(r1.as_bytes()[100], 0x11);
711            assert_eq!(r2.as_bytes()[100], 0x22);
712        }
713
714        cleanup(&path);
715    }
716
717    #[test]
718    fn test_begin_returns_structured_error_when_state_lock_is_poisoned() {
719        let path = temp_db_path();
720        cleanup(&path);
721
722        {
723            let db = Arc::new(Database::open(&path).unwrap());
724            let poison_target = Arc::clone(&db);
725            let _ = std::thread::spawn(move || {
726                let _guard = poison_target
727                    .state
728                    .write()
729                    .expect("state lock should be acquired");
730                panic!("poison database state");
731            })
732            .join();
733
734            match db.begin() {
735                Ok(_) => panic!("begin should fail after state lock poisoning"),
736                Err(err) => {
737                    assert!(matches!(err, DatabaseError::LockPoisoned("database state")))
738                }
739            }
740        }
741
742        cleanup(&path);
743    }
744}