1use 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}