Skip to main content

reddb_server/
engine.rs

1//! Engine layer facade.
2//!
3//! This module keeps the physical storage concerns separated from unified domain APIs.
4
5use std::path::{Path, PathBuf};
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use crate::api::{
9    CatalogService, CatalogSnapshot, CollectionStats, RedDBError, RedDBOptions, RedDBResult,
10    StorageMode, StorageMode::Persistent,
11};
12use crate::health::{storage_file_health, HealthProvider, HealthReport};
13use crate::index::IndexCatalog;
14use crate::physical::PhysicalLayout;
15use crate::storage;
16use crate::storage::wal::CheckpointResult;
17
18#[derive(Debug, Clone, Copy)]
19pub struct EngineStats {
20    pub page_count: u32,
21    pub cache_hits: u64,
22    pub cache_misses: u64,
23    pub file_size_bytes: Option<u64>,
24    pub path_exists: bool,
25}
26
27#[derive(Debug, Clone)]
28pub struct EngineInfo {
29    pub path: Option<PathBuf>,
30    pub started_at_unix_ms: u128,
31    pub read_only: bool,
32    pub mode: StorageMode,
33    pub layout: PhysicalLayout,
34    pub options: RedDBOptions,
35}
36
37pub struct RedDBEngine {
38    options: RedDBOptions,
39    layout: PhysicalLayout,
40    db: Option<storage::engine::Database>,
41    indices: IndexCatalog,
42    started_at_unix_ms: u128,
43}
44
45impl RedDBEngine {
46    pub fn open<P: AsRef<Path>>(path: P) -> RedDBResult<Self> {
47        Self::with_options(RedDBOptions::persistent(path.as_ref()))
48    }
49
50    pub fn with_options(options: RedDBOptions) -> RedDBResult<Self> {
51        let started_at_unix_ms = SystemTime::now()
52            .duration_since(UNIX_EPOCH)
53            .unwrap_or_default()
54            .as_millis();
55        let layout = PhysicalLayout::from_options(&options);
56
57        let indices = IndexCatalog::register_default_vector_graph(
58            options.has_capability(crate::api::Capability::Table),
59            options.has_capability(crate::api::Capability::Graph),
60        );
61
62        let db = {
63            let path = options.resolved_path("data.rdb");
64            let config = storage::engine::DatabaseConfig {
65                read_only: options.read_only,
66                create: options.create_if_missing,
67                verify_checksums: options.verify_checksums,
68                auto_checkpoint_threshold: options.auto_checkpoint_pages,
69                ..Default::default()
70            };
71            Some(storage::engine::Database::open_with_config(path, config)?)
72        };
73
74        Ok(Self {
75            options,
76            layout,
77            db,
78            indices,
79            started_at_unix_ms,
80        })
81    }
82
83    pub fn options(&self) -> &RedDBOptions {
84        &self.options
85    }
86
87    pub fn layout(&self) -> &PhysicalLayout {
88        &self.layout
89    }
90
91    pub fn mode(&self) -> StorageMode {
92        self.options.mode
93    }
94
95    pub fn path(&self) -> Option<&Path> {
96        self.db.as_ref().map(|db| db.path())
97    }
98
99    pub fn begin_transaction(&self) -> RedDBResult<storage::wal::Transaction> {
100        let db = self
101            .db
102            .as_ref()
103            .ok_or_else(|| RedDBError::InvalidConfig("in-memory mode".to_string()))?;
104        Ok(db.begin()?)
105    }
106
107    pub fn sync(&self) -> RedDBResult<()> {
108        if let Some(db) = &self.db {
109            db.sync()?;
110        }
111        Ok(())
112    }
113
114    pub fn checkpoint(&self) -> RedDBResult<Option<CheckpointResult>> {
115        match &self.db {
116            Some(db) => db.checkpoint().map(Some).map_err(RedDBError::from),
117            None => Ok(None),
118        }
119    }
120
121    pub fn checkpoint_if_needed(&self) -> RedDBResult<Option<CheckpointResult>> {
122        match &self.db {
123            Some(db) => db.maybe_auto_checkpoint().map_err(RedDBError::from),
124            None => Ok(None),
125        }
126    }
127
128    pub fn stats(&self) -> EngineStats {
129        let mut stats = EngineStats {
130            page_count: 0,
131            cache_hits: 0,
132            cache_misses: 0,
133            file_size_bytes: None,
134            path_exists: false,
135        };
136
137        if let Some(db) = &self.db {
138            stats.page_count = db.page_count();
139            if let Ok(file_size) = db.file_size() {
140                stats.file_size_bytes = Some(file_size);
141            }
142            let cache = db.cache_stats();
143            stats.cache_hits = cache.hits;
144            stats.cache_misses = cache.misses;
145            stats.path_exists = db.path().exists();
146        }
147
148        stats
149    }
150
151    pub fn indices(&self) -> &IndexCatalog {
152        &self.indices
153    }
154
155    pub fn close(self) -> RedDBResult<()> {
156        if let Some(db) = self.db {
157            db.close().map_err(RedDBError::from)?;
158        }
159        Ok(())
160    }
161
162    pub fn info(&self) -> EngineInfo {
163        EngineInfo {
164            path: self.path().map(Path::to_path_buf),
165            started_at_unix_ms: self.started_at_unix_ms,
166            read_only: self.options.read_only,
167            mode: self.options.mode,
168            layout: self.layout.clone(),
169            options: self.options.clone(),
170        }
171    }
172
173    fn fallback_snapshot(&self) -> CatalogSnapshot {
174        CatalogSnapshot {
175            name: "reddb_engine".into(),
176            total_entities: 0,
177            total_collections: 0,
178            stats_by_collection: std::collections::BTreeMap::new(),
179            updated_at: SystemTime::UNIX_EPOCH,
180        }
181    }
182}
183
184impl CatalogService for RedDBEngine {
185    fn list_collections(&self) -> Vec<String> {
186        Vec::new()
187    }
188
189    fn collection_stats(&self, _: &str) -> Option<CollectionStats> {
190        None
191    }
192
193    fn catalog_snapshot(&self) -> CatalogSnapshot {
194        self.fallback_snapshot()
195    }
196}
197
198impl HealthProvider for RedDBEngine {
199    fn health(&self) -> HealthReport {
200        let path = self.path();
201        let mut report = match path {
202            Some(path) => storage_file_health(path),
203            None => HealthReport::healthy().with_diagnostic("mode", "in-memory"),
204        };
205        report = report.with_diagnostic("engine-started-at", self.started_at_unix_ms.to_string());
206        report = report.with_diagnostic("read-only", self.options.read_only.to_string());
207        report.with_diagnostic("persistent", self.layout.is_persistent().to_string())
208    }
209}
210
211impl crate::api::DataOps for RedDBEngine {
212    fn execute_query(&self, _query: &str) -> RedDBResult<()> {
213        Ok(())
214    }
215}
216
217impl crate::api::QueryPlanner for RedDBEngine {
218    fn plan_cost(&self, query: &str) -> Option<f64> {
219        if query.trim().is_empty() {
220            None
221        } else {
222            Some(query.len() as f64 * 0.25)
223        }
224    }
225}