blvm_sdk/module/
database.rs1use anyhow::Result;
7use blvm_node::storage::database::{create_database, default_backend, Database, DatabaseBackend};
8use std::path::Path;
9use std::sync::Arc;
10use tracing::warn;
11
12fn parse_backend(s: &str) -> DatabaseBackend {
13 match s.to_lowercase().as_str() {
14 "redb" => DatabaseBackend::Redb,
15 "rocksdb" => DatabaseBackend::RocksDB,
16 "sled" => DatabaseBackend::Sled,
17 "tidesdb" => DatabaseBackend::TidesDB,
18 _ => default_backend(),
20 }
21}
22
23fn supports_dynamic_trees(backend: DatabaseBackend) -> bool {
31 matches!(backend, DatabaseBackend::Sled | DatabaseBackend::TidesDB)
32}
33
34fn best_module_backend() -> DatabaseBackend {
39 DatabaseBackend::Sled
43}
44
45pub fn open_module_db<P: AsRef<Path>>(data_dir: P) -> Result<Arc<dyn Database>> {
67 let db_path = data_dir.as_ref().join("db");
68 std::fs::create_dir_all(&db_path)?;
69
70 let explicit = std::env::var("MODULE_CONFIG_DATABASE_BACKEND")
71 .or_else(|_| std::env::var("MODULE_DATABASE_BACKEND"))
72 .ok();
73
74 let backend = explicit
75 .as_deref()
76 .map(parse_backend)
77 .unwrap_or_else(best_module_backend);
78
79 if supports_dynamic_trees(backend) {
80 return create_database(&db_path, backend, None).map(Arc::from);
82 }
83
84 if explicit.is_some() {
86 warn!(
89 "MODULE_DATABASE_BACKEND={:?} does not support dynamic open_tree(). \
90 Module databases require Sled or TidesDB. \
91 Set MODULE_CONFIG_DATABASE_BACKEND=sled (or tidesdb) to remove this warning. \
92 Note: Redb only supports pre-declared tables (schema, items); \
93 RocksDB requires all column families at open time.",
94 backend
95 );
96 } else {
97 warn!(
99 "Default backend {:?} does not support dynamic open_tree(); \
100 auto-selecting best available module backend (sled or tidesdb). \
101 Set MODULE_CONFIG_DATABASE_BACKEND=sled to suppress this warning.",
102 backend
103 );
104 }
105
106 create_database(&db_path, DatabaseBackend::Sled, None)
108 .or_else(|_| create_database(&db_path, DatabaseBackend::TidesDB, None))
109 .map(Arc::from)
110 .map_err(|_| {
111 anyhow::anyhow!(
112 "No module-compatible database backend available. \
113 Backend {:?} requires pre-declared trees/column families. \
114 Recompile with the 'sled' or 'tidesdb' feature on blvm-node, \
115 or set MODULE_CONFIG_DATABASE_BACKEND=sled.",
116 backend
117 )
118 })
119}
120
121const SCHEMA_VERSION_KEY: &[u8] = b"schema_version";
123
124#[derive(Clone)]
130pub struct MigrationContext {
131 tree: Arc<dyn blvm_node::storage::database::Tree>,
132 db: Arc<dyn Database>,
133}
134
135impl MigrationContext {
136 pub fn new(tree: Arc<dyn blvm_node::storage::database::Tree>, db: Arc<dyn Database>) -> Self {
138 Self { tree, db }
139 }
140
141 pub fn put(&self, key: &[u8], value: &[u8]) -> Result<()> {
143 self.tree.insert(key, value)
144 }
145
146 pub fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
148 self.tree.get(key)
149 }
150
151 pub fn delete(&self, key: &[u8]) -> Result<()> {
153 self.tree.remove(key)
154 }
155
156 pub fn open_tree(&self, name: &str) -> Result<Box<dyn blvm_node::storage::database::Tree>> {
158 self.db.open_tree(name)
159 }
160}
161
162pub type MigrationUp = fn(&MigrationContext) -> Result<()>;
164
165pub type MigrationDown = fn(&MigrationContext) -> Result<()>;
167
168pub type Migration = (u32, MigrationUp, Option<MigrationDown>);
170
171pub fn run_migrations(db: &Arc<dyn Database>, migrations: &[(u32, MigrationUp)]) -> Result<()> {
180 run_migrations_with_down(
181 db,
182 &migrations
183 .iter()
184 .map(|(v, u)| (*v, *u, None))
185 .collect::<Vec<_>>(),
186 )
187}
188
189pub fn run_migrations_with_down(db: &Arc<dyn Database>, migrations: &[Migration]) -> Result<()> {
191 let tree = db.open_tree("schema")?;
192 let tree = Arc::from(tree);
193 let ctx = MigrationContext::new(tree, Arc::clone(db));
194
195 let current: u32 = ctx
196 .get(SCHEMA_VERSION_KEY)?
197 .and_then(|v| String::from_utf8(v).ok())
198 .and_then(|s| s.parse().ok())
199 .unwrap_or(0);
200
201 let mut pending: Vec<_> = migrations
202 .iter()
203 .filter(|(v, _, _)| *v > current)
204 .copied()
205 .collect();
206 pending.sort_by_key(|(v, _, _)| *v);
207
208 for (version, up, _down) in pending {
209 up(&ctx)?;
210 ctx.put(SCHEMA_VERSION_KEY, version.to_string().as_bytes())?;
211 }
212
213 Ok(())
214}
215
216pub fn run_migrations_down(
225 db: &Arc<dyn Database>,
226 migrations: &[Migration],
227 target_version: u32,
228) -> Result<()> {
229 let tree = db.open_tree("schema")?;
230 let tree = Arc::from(tree);
231 let ctx = MigrationContext::new(tree, Arc::clone(db));
232
233 let current: u32 = ctx
234 .get(SCHEMA_VERSION_KEY)?
235 .and_then(|v| String::from_utf8(v).ok())
236 .and_then(|s| s.parse().ok())
237 .unwrap_or(0);
238
239 if current <= target_version {
240 return Ok(());
241 }
242
243 let mut to_rollback: Vec<_> = migrations
244 .iter()
245 .filter(|(v, _, d)| *v > target_version && *v <= current && d.is_some())
246 .copied()
247 .collect();
248 to_rollback.sort_by_key(|(v, _, _)| std::cmp::Reverse(*v));
249
250 for (version, _up, down) in to_rollback {
251 if let Some(down_fn) = down {
252 down_fn(&ctx)?;
253 } else {
254 anyhow::bail!("Migration version {} has no down function", version);
255 }
256 }
257
258 ctx.put(SCHEMA_VERSION_KEY, target_version.to_string().as_bytes())?;
259 Ok(())
260}