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