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