blvm_sdk/module/database.rs
1//! Module database utilities
2//!
3//! Each module has its own database at `{data_dir}/db/`. Use this helper to open it.
4//!
5//! `MODULE_CONFIG_DATABASE_BACKEND` is normally set by the node when spawning a module, from
6//! `[storage] database_backend` and optional `[modules] module_database_backend` (see
7//! `blvm_node::storage::database::module_subprocess_database_backend_preference`). That value
8//! matches the chain store name; if it is not suitable for dynamic module trees (RocksDB / Redb),
9//! this crate falls back via [`try_create_module_kv_database`](blvm_node::storage::database::try_create_module_kv_database)
10//! to **Sled** and/or **TidesDB** depending on what that `blvm-node` binary was compiled with.
11
12use anyhow::Result;
13use blvm_node::storage::database::{
14 create_database, default_backend, try_create_module_kv_database, Database, DatabaseBackend,
15};
16use std::path::Path;
17use std::sync::Arc;
18use tracing::warn;
19
20fn parse_backend(s: &str) -> DatabaseBackend {
21 match s.to_lowercase().as_str() {
22 "redb" => DatabaseBackend::Redb,
23 "rocksdb" => DatabaseBackend::RocksDB,
24 "sled" => DatabaseBackend::Sled,
25 "tidesdb" => DatabaseBackend::TidesDB,
26 // "auto" and anything unrecognised: let the caller fall through to the default selection
27 _ => default_backend(),
28 }
29}
30
31/// Returns true for backends that support fully dynamic `open_tree()` without any pre-declaration.
32///
33/// - **Sled**: any tree name is created on demand.
34/// - **TidesDB**: uses `get_or_create_cf`, fully dynamic.
35/// - **Redb**: only works for a hard-coded set of node/module tables (`schema`, `items`, etc.).
36/// Arbitrary tree names return an error — use Sled or TidesDB for general module storage.
37/// - **RocksDB**: column families must be declared at database open time.
38fn supports_dynamic_trees(backend: DatabaseBackend) -> bool {
39 matches!(backend, DatabaseBackend::Sled | DatabaseBackend::TidesDB)
40}
41
42/// Best available backend for module databases (prefers fully-dynamic backends).
43///
44/// Returns Sled as the preferred target; `create_database` will surface an error if the
45/// feature is not compiled into blvm-node, and the caller falls through to TidesDB.
46fn best_module_backend() -> DatabaseBackend {
47 // Prefer Sled: lightweight, embedded, fully dynamic open_tree().
48 // If Sled is not compiled in, create_database will return an error and the caller
49 // falls back to TidesDB. The node default (rocksdb) is intentionally NOT used here.
50 DatabaseBackend::Sled
51}
52
53/// Open the module's database at `{data_dir}/db/`.
54///
55/// Module databases need to create named trees on demand (`open_tree()`). Only **Sled** and
56/// **TidesDB** support fully dynamic tree creation. Redb only supports a fixed set of
57/// pre-declared tables; RocksDB requires all column families to be declared at open time.
58///
59/// Backend selection (first match wins):
60/// 1. `MODULE_CONFIG_DATABASE_BACKEND` — set by the node from effective module DB policy (see
61/// `blvm_node::storage::database::module_subprocess_database_backend_preference`)
62/// 2. `MODULE_DATABASE_BACKEND` env var (same values; manual override)
63/// 3. Auto-select Sled as first choice, then fall back through [`try_create_module_kv_database`](blvm_node::storage::database::try_create_module_kv_database)
64///
65/// If the resolved backend does not support dynamic trees the function warns and falls back
66/// as above. If no dynamic backend is compiled into `blvm-node`, it returns an error.
67///
68/// # Example
69/// ```ignore
70/// let db = open_module_db(module_data_dir)?;
71/// let tree = db.open_tree("my_tree")?;
72/// tree.insert(b"key", b"value")?;
73/// ```
74pub fn open_module_db<P: AsRef<Path>>(data_dir: P) -> Result<Arc<dyn Database>> {
75 let db_path = data_dir.as_ref().join("db");
76 std::fs::create_dir_all(&db_path)?;
77
78 let explicit = std::env::var("MODULE_CONFIG_DATABASE_BACKEND")
79 .or_else(|_| std::env::var("MODULE_DATABASE_BACKEND"))
80 .ok();
81
82 let backend = explicit
83 .as_deref()
84 .map(parse_backend)
85 .unwrap_or_else(best_module_backend);
86
87 if supports_dynamic_trees(backend) {
88 // Explicitly requested or auto-selected a good module backend.
89 return create_database(&db_path, backend, None).map(Arc::from);
90 }
91
92 // The resolved backend does not support fully-dynamic open_tree().
93 if explicit.is_some() {
94 // User explicitly asked for this backend — warn but still try to fall back so
95 // existing module processes don't hard-fail on start-up.
96 warn!(
97 "MODULE_DATABASE_BACKEND={:?} does not support dynamic open_tree(). \
98 Module databases require Sled or TidesDB. \
99 Set MODULE_CONFIG_DATABASE_BACKEND=sled (or tidesdb) to remove this warning. \
100 Note: Redb only supports pre-declared tables (schema, items); \
101 RocksDB requires all column families at open time.",
102 backend
103 );
104 } else {
105 // Auto-detected a static backend (e.g. rocksdb is the node default).
106 warn!(
107 "Default backend {:?} does not support dynamic open_tree(); \
108 auto-selecting best available module backend (sled or tidesdb). \
109 Set MODULE_CONFIG_DATABASE_BACKEND=sled to suppress this warning.",
110 backend
111 );
112 }
113
114 // Use whichever dynamic KV backends are compiled into blvm-node (Sled / TidesDB).
115 try_create_module_kv_database(&db_path)
116 .map(Arc::from)
117 .map_err(|e| {
118 anyhow::anyhow!(
119 "{e} (hint: {:?} is not usable for arbitrary module `open_tree` names; \
120 `blvm-sdk` feature `node` enables `blvm-node/sled`, or build `blvm-node` with `tidesdb`)",
121 backend
122 )
123 })
124}
125
126/// Schema version key (stored in the schema tree).
127const SCHEMA_VERSION_KEY: &[u8] = b"schema_version";
128
129/// Context passed to migration functions. Provides put/get/delete against the module's schema tree
130/// and access to the database for opening other trees.
131///
132/// Migrations run locally in the module process. Use `put`/`get`/`delete` for schema metadata.
133/// For application data migrations, use `open_tree` to open and migrate other trees.
134#[derive(Clone)]
135pub struct MigrationContext {
136 tree: Arc<dyn blvm_node::storage::database::Tree>,
137 db: Arc<dyn Database>,
138}
139
140impl MigrationContext {
141 /// Create a new MigrationContext wrapping the schema tree and database.
142 pub fn new(tree: Arc<dyn blvm_node::storage::database::Tree>, db: Arc<dyn Database>) -> Self {
143 Self { tree, db }
144 }
145
146 /// Insert a key-value pair into the schema tree.
147 pub fn put(&self, key: &[u8], value: &[u8]) -> Result<()> {
148 self.tree.insert(key, value)
149 }
150
151 /// Get a value by key from the schema tree.
152 pub fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
153 self.tree.get(key)
154 }
155
156 /// Remove a key from the schema tree.
157 pub fn delete(&self, key: &[u8]) -> Result<()> {
158 self.tree.remove(key)
159 }
160
161 /// Open a named tree for application data migrations.
162 pub fn open_tree(&self, name: &str) -> Result<Box<dyn blvm_node::storage::database::Tree>> {
163 self.db.open_tree(name)
164 }
165}
166
167/// A single up migration step.
168pub type MigrationUp = fn(&MigrationContext) -> Result<()>;
169
170/// A single down migration step (for rollback).
171pub type MigrationDown = fn(&MigrationContext) -> Result<()>;
172
173/// Migration pair: (version, up, optional down for rollback).
174pub type Migration = (u32, MigrationUp, Option<MigrationDown>);
175
176/// Run pending up migrations. Opens the "schema" tree, reads current version, runs each migration
177/// with version > current in order, then updates schema_version.
178///
179/// # Example
180/// ```ignore
181/// let db = open_module_db(data_dir)?;
182/// run_migrations(&db, &[(1, up_initial, Some(down_initial)), (2, up_add_cache, None)])?;
183/// ```
184pub fn run_migrations(db: &Arc<dyn Database>, migrations: &[(u32, MigrationUp)]) -> Result<()> {
185 run_migrations_with_down(
186 db,
187 &migrations
188 .iter()
189 .map(|(v, u)| (*v, *u, None))
190 .collect::<Vec<_>>(),
191 )
192}
193
194/// Run pending up migrations. Supports optional down migrations for rollback.
195pub fn run_migrations_with_down(db: &Arc<dyn Database>, migrations: &[Migration]) -> Result<()> {
196 let tree = db.open_tree("schema")?;
197 let tree = Arc::from(tree);
198 let ctx = MigrationContext::new(tree, Arc::clone(db));
199
200 let current: u32 = ctx
201 .get(SCHEMA_VERSION_KEY)?
202 .and_then(|v| String::from_utf8(v).ok())
203 .and_then(|s| s.parse().ok())
204 .unwrap_or(0);
205
206 let mut pending: Vec<_> = migrations
207 .iter()
208 .filter(|(v, _, _)| *v > current)
209 .copied()
210 .collect();
211 pending.sort_by_key(|(v, _, _)| *v);
212
213 for (version, up, _down) in pending {
214 up(&ctx)?;
215 ctx.put(SCHEMA_VERSION_KEY, version.to_string().as_bytes())?;
216 }
217
218 Ok(())
219}
220
221/// Rollback migrations down to `target_version` (exclusive). Runs down migrations in reverse
222/// order for each applied version > target_version. Requires down functions to be provided.
223///
224/// # Example
225/// ```ignore
226/// run_migrations_down(&db, &[(1, up_initial, Some(down_initial)), (2, up_add_cache, Some(down_cache))], 1)?;
227/// // Rolls back from 2 to 1 (runs down_cache only).
228/// ```
229pub fn run_migrations_down(
230 db: &Arc<dyn Database>,
231 migrations: &[Migration],
232 target_version: u32,
233) -> Result<()> {
234 let tree = db.open_tree("schema")?;
235 let tree = Arc::from(tree);
236 let ctx = MigrationContext::new(tree, Arc::clone(db));
237
238 let current: u32 = ctx
239 .get(SCHEMA_VERSION_KEY)?
240 .and_then(|v| String::from_utf8(v).ok())
241 .and_then(|s| s.parse().ok())
242 .unwrap_or(0);
243
244 if current <= target_version {
245 return Ok(());
246 }
247
248 let mut to_rollback: Vec<_> = migrations
249 .iter()
250 .filter(|(v, _, d)| *v > target_version && *v <= current && d.is_some())
251 .copied()
252 .collect();
253 to_rollback.sort_by_key(|(v, _, _)| std::cmp::Reverse(*v));
254
255 for (version, _up, down) in to_rollback {
256 if let Some(down_fn) = down {
257 down_fn(&ctx)?;
258 } else {
259 anyhow::bail!("Migration version {} has no down function", version);
260 }
261 }
262
263 ctx.put(SCHEMA_VERSION_KEY, target_version.to_string().as_bytes())?;
264 Ok(())
265}