blvm_sdk/module/module_db.rs
1//! Standard module storage API.
2//!
3//! All modules use this single system: `ModuleDb` for persistent storage.
4//! No ad-hoc storage—open your DB, run migrations, use named trees.
5//!
6//! ## Usage
7//!
8//! ```ignore
9//! let db = ModuleDb::open(&bootstrap.data_dir)?;
10//! db.run_migrations(migrations!(1 => up_initial, 2 => up_add_proposals))?;
11//! let tree = db.tree("proposals")?;
12//! tree.insert(b"key", b"value")?;
13//! ```
14//!
15//! ## Tree naming
16//!
17//! Define tree names as constants in your module (e.g. `const PROPOSALS_TREE: &str = "proposals"`).
18//! Avoid generic names like `"items"`—use descriptive names for each logical store.
19
20use anyhow::Result;
21use blvm_node::storage::database::{Database, Tree};
22use std::path::Path;
23use std::sync::Arc;
24
25use crate::module::database::{open_module_db, run_migrations, MigrationUp};
26
27/// Standard module database.
28///
29/// Single entry point for all module storage. Opens DB at `{data_dir}/db/`,
30/// runs migrations, provides named trees.
31#[derive(Clone)]
32pub struct ModuleDb {
33 db: Arc<dyn Database>,
34}
35
36impl ModuleDb {
37 /// Open the module database at `{data_dir}/db/`.
38 ///
39 /// Uses the same backend as the node when `MODULE_CONFIG_DATABASE_BACKEND` is set.
40 pub fn open<P: AsRef<Path>>(data_dir: P) -> Result<Self> {
41 let db = open_module_db(data_dir)?;
42 Ok(Self { db })
43 }
44
45 /// Open and run migrations. Convention for modules that use migrations.
46 pub fn open_with_migrations<P: AsRef<Path>>(
47 data_dir: P,
48 migrations: &[(u32, MigrationUp)],
49 ) -> Result<Self> {
50 let db = Self::open(data_dir)?;
51 db.run_migrations(migrations)?;
52 Ok(db)
53 }
54
55 /// Open at data_dir, or fallback to temp dir when primary open fails.
56 ///
57 /// Both attempts use [`open_module_db`](super::database::open_module_db) (Sled/TidesDB).
58 /// Avoids a hard-coded Redb fallback, which fails when `blvm-node` is built without `redb`.
59 pub fn open_or_temp<P: AsRef<Path>>(data_dir: P, module_name: &str) -> Result<Self> {
60 let data_dir = data_dir.as_ref().to_path_buf();
61 Self::open(&data_dir).or_else(|_| {
62 let temp = std::env::temp_dir().join(module_name);
63 let _ = std::fs::create_dir_all(&temp);
64 Self::open(&temp)
65 })
66 }
67
68 /// Like `open_or_temp` but runs migrations when the primary data_dir succeeds.
69 pub fn open_or_temp_with_migrations<P: AsRef<Path>>(
70 data_dir: P,
71 module_name: &str,
72 migrations: &[(u32, MigrationUp)],
73 ) -> Result<Self> {
74 let db = Self::open_or_temp(data_dir, module_name)?;
75 let _ = db.run_migrations(migrations);
76 Ok(db)
77 }
78
79 /// Run pending migrations.
80 pub fn run_migrations(&self, migrations: &[(u32, MigrationUp)]) -> Result<()> {
81 run_migrations(&self.db, migrations)
82 }
83
84 /// Open a named tree. Use descriptive constants defined in your module.
85 pub fn tree(&self, name: &str) -> Result<Arc<dyn Tree>> {
86 let t = self.db.open_tree(name)?;
87 Ok(Arc::from(t))
88 }
89
90 /// Access the underlying database (for compatibility with code expecting `Arc<dyn Database>`).
91 pub fn as_db(&self) -> Arc<dyn Database> {
92 Arc::clone(&self.db)
93 }
94}