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