Skip to main content

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}