Skip to main content

wasm_dbms/
context.rs

1// Rust guideline compliant 2026-03-01
2// X-WHERE-CLAUSE, M-PUBLIC-DEBUG, M-CANONICAL-DOCS
3
4//! DBMS context that owns all database state.
5//!
6//! `DbmsContext` provides a runtime-agnostic container for the full DBMS
7//! state. Internal `RefCell` wrappers allow shared-reference mutation
8//! through a single shared reference.
9
10use std::cell::{Cell, RefCell};
11
12use wasm_dbms_api::prelude::{
13    DbmsResult, IdentityPerms, PermGrant, PermRevoke, TableFingerprint, TablePerms, TransactionId,
14};
15use wasm_dbms_memory::prelude::{
16    AccessControl, AccessControlList, MemoryManager, MemoryProvider, SchemaRegistry,
17    TableRegistryPage,
18};
19
20use crate::transaction::journal::Journal;
21use crate::transaction::session::TransactionSession;
22
23/// Owns all mutable DBMS state behind interior-mutable wrappers.
24///
25/// Each component is wrapped in a `RefCell` so that operations
26/// borrowing different components can coexist without requiring
27/// `&mut self` on the context.
28///
29/// The access-control provider `A` defaults to [`AccessControlList`].
30/// Runtimes that do not need ACL can use [`NoAccessControl`](wasm_dbms_memory::NoAccessControl).
31///
32/// # Threading
33///
34/// `DbmsContext` is `!Send` and `!Sync` because of the `RefCell`
35/// wrappers. This is intentional: WASM runtimes (both IC canisters
36/// and WASI preview 1 modules) execute single-threaded, so interior
37/// mutability via `RefCell` is sufficient and avoids the overhead of
38/// synchronization primitives. Embedders that need multi-threaded
39/// access should wrap the context in their own synchronization layer.
40pub struct DbmsContext<M, A = AccessControlList>
41where
42    M: MemoryProvider,
43    A: AccessControl,
44{
45    /// Memory manager for page-level operations.
46    pub(crate) mm: RefCell<MemoryManager<M>>,
47
48    /// Schema registry mapping table names to page locations.
49    pub(crate) schema_registry: RefCell<SchemaRegistry>,
50
51    /// Access-control provider storing allowed identities.
52    pub(crate) acl: RefCell<A>,
53
54    /// Active transaction sessions.
55    pub(crate) transaction_session: RefCell<TransactionSession>,
56
57    /// Active write-ahead journal for atomic operations.
58    pub(crate) journal: RefCell<Option<Journal>>,
59
60    /// Cached `(compiled_schema_hash, drifted)` pair for the most recent
61    /// schema attached to a database session on this context.
62    pub(crate) drift: Cell<Option<(u64, bool)>>,
63
64    /// Set while a migration apply pass is mutating stable memory so the
65    /// per-CRUD drift gate does not block the engine's own internal reads
66    /// (e.g. tightening validation that scans existing rows).
67    pub(crate) migrating: Cell<bool>,
68}
69
70impl<M> DbmsContext<M>
71where
72    M: MemoryProvider,
73{
74    /// Creates a new DBMS context with the default [`AccessControlList`],
75    /// initializing the memory manager and loading persisted state.
76    pub fn new(memory: M) -> Self {
77        let mut mm = MemoryManager::init(memory);
78        let schema_registry = SchemaRegistry::load(&mut mm).unwrap_or_default();
79        let acl = AccessControlList::load(&mut mm).unwrap_or_default();
80
81        Self {
82            mm: RefCell::new(mm),
83            schema_registry: RefCell::new(schema_registry),
84            acl: RefCell::new(acl),
85            transaction_session: RefCell::new(TransactionSession::default()),
86            journal: RefCell::new(None),
87            drift: Cell::new(None),
88            migrating: Cell::new(false),
89        }
90    }
91}
92
93impl<M, A> DbmsContext<M, A>
94where
95    M: MemoryProvider,
96    A: AccessControl,
97{
98    /// Creates a new DBMS context with a custom access control provider.
99    pub fn with_acl(memory: M) -> Self {
100        let mut mm = MemoryManager::init(memory);
101        let schema_registry = SchemaRegistry::load(&mut mm).unwrap_or_default();
102        let acl = A::load(&mut mm).unwrap_or_default();
103
104        Self {
105            mm: RefCell::new(mm),
106            schema_registry: RefCell::new(schema_registry),
107            acl: RefCell::new(acl),
108            transaction_session: RefCell::new(TransactionSession::default()),
109            journal: RefCell::new(None),
110            drift: Cell::new(None),
111            migrating: Cell::new(false),
112        }
113    }
114
115    /// Registers a table schema, persisting it in stable memory.
116    pub fn register_table<T: wasm_dbms_api::prelude::TableSchema>(
117        &self,
118    ) -> DbmsResult<TableRegistryPage> {
119        let mut sr = self.schema_registry.borrow_mut();
120        let mut mm = self.mm.borrow_mut();
121        sr.register_table::<T>(&mut *mm).map_err(Into::into)
122    }
123
124    /// Returns whether `name` resolves to a registered table.
125    pub fn has_table(&self, name: &str) -> bool {
126        self.schema_registry
127            .borrow()
128            .table_registry_page_by_name(name)
129            .is_some()
130    }
131
132    /// Returns whether `id` is granted `required` on `table`.
133    pub fn granted(&self, id: &A::Id, table: TableFingerprint, required: TablePerms) -> bool {
134        self.acl.borrow().granted(id, table, required)
135    }
136
137    /// Returns whether `id` carries the `admin` bypass flag.
138    pub fn granted_admin(&self, id: &A::Id) -> bool {
139        self.acl.borrow().granted_admin(id)
140    }
141
142    /// Returns whether `id` carries the `manage_acl` flag.
143    pub fn granted_manage_acl(&self, id: &A::Id) -> bool {
144        self.acl.borrow().granted_manage_acl(id)
145    }
146
147    /// Returns whether `id` carries the `migrate` flag.
148    pub fn granted_migrate(&self, id: &A::Id) -> bool {
149        self.acl.borrow().granted_migrate(id)
150    }
151
152    /// Applies a grant. **Does not** enforce `manage_acl` on the caller —
153    /// callers must check `granted_manage_acl` first or use the
154    /// `Dbms::grant` wrapper which self-enforces.
155    pub fn acl_grant(&self, id: A::Id, grant: PermGrant) -> DbmsResult<()> {
156        let mut acl = self.acl.borrow_mut();
157        let mut mm = self.mm.borrow_mut();
158        acl.grant(id, grant, &mut mm).map_err(Into::into)
159    }
160
161    /// Applies a revoke. Does not enforce `manage_acl` on the caller.
162    pub fn acl_revoke(&self, id: &A::Id, revoke: PermRevoke) -> DbmsResult<()> {
163        let mut acl = self.acl.borrow_mut();
164        let mut mm = self.mm.borrow_mut();
165        acl.revoke(id, revoke, &mut mm).map_err(Into::into)
166    }
167
168    /// Removes an identity entirely. Does not enforce `manage_acl` on the
169    /// caller.
170    pub fn acl_remove_identity(&self, id: &A::Id) -> DbmsResult<()> {
171        let mut acl = self.acl.borrow_mut();
172        let mut mm = self.mm.borrow_mut();
173        acl.remove_identity(id, &mut mm).map_err(Into::into)
174    }
175
176    /// Returns the [`IdentityPerms`] currently held by `id`.
177    pub fn acl_perms(&self, id: &A::Id) -> IdentityPerms {
178        self.acl.borrow().perms(id)
179    }
180
181    /// Returns every identity in the ACL together with its perms.
182    pub fn acl_identities(&self) -> Vec<(A::Id, IdentityPerms)> {
183        self.acl.borrow().identities()
184    }
185
186    /// Begins a new transaction for the given owner identity.
187    pub fn begin_transaction(&self, owner: Vec<u8>) -> TransactionId {
188        let mut ts = self.transaction_session.borrow_mut();
189        ts.begin_transaction(owner)
190    }
191
192    /// Returns whether the given transaction is owned by the given identity.
193    pub fn has_transaction(&self, tx_id: &TransactionId, caller: &[u8]) -> bool {
194        let ts = self.transaction_session.borrow();
195        ts.has_transaction(tx_id, caller)
196    }
197
198    /// Returns the cached drift flag for `compiled_hash`, if present.
199    pub(crate) fn cached_drift_for(&self, compiled_hash: u64) -> Option<bool> {
200        self.drift
201            .get()
202            .and_then(|(hash, drifted)| (hash == compiled_hash).then_some(drifted))
203    }
204
205    /// Caches the drift flag for the given compiled schema hash.
206    pub(crate) fn set_drift(&self, compiled_hash: u64, value: bool) {
207        self.drift.set(Some((compiled_hash, value)));
208    }
209
210    /// Clears the cached drift flag, forcing the next call to recompute.
211    pub(crate) fn clear_drift(&self) {
212        self.drift.set(None);
213    }
214
215    /// Returns `true` while a migration apply pass is mutating stable memory.
216    pub(crate) fn is_migrating(&self) -> bool {
217        self.migrating.get()
218    }
219
220    /// Sets the migration-in-progress guard.
221    pub(crate) fn set_migrating(&self, value: bool) {
222        self.migrating.set(value);
223    }
224}
225
226impl<M, A> std::fmt::Debug for DbmsContext<M, A>
227where
228    M: MemoryProvider,
229    A: AccessControl + std::fmt::Debug,
230{
231    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232        f.debug_struct("DbmsContext")
233            .field("schema_registry", &self.schema_registry)
234            .field("acl", &self.acl)
235            .field("transaction_session", &self.transaction_session)
236            .finish_non_exhaustive()
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use wasm_dbms_memory::prelude::HeapMemoryProvider;
243
244    use super::*;
245
246    #[test]
247    fn test_should_create_context() {
248        let ctx = DbmsContext::new(HeapMemoryProvider::default());
249        assert!(ctx.acl_identities().is_empty());
250    }
251
252    #[test]
253    fn test_should_grant_admin_to_identity() {
254        let ctx = DbmsContext::new(HeapMemoryProvider::default());
255        ctx.acl_grant(vec![1, 2, 3], PermGrant::Admin).unwrap();
256        assert!(ctx.granted_admin(&vec![1, 2, 3]));
257        assert!(!ctx.granted_admin(&vec![4, 5, 6]));
258    }
259
260    #[test]
261    fn test_should_remove_identity() {
262        let ctx = DbmsContext::new(HeapMemoryProvider::default());
263        ctx.acl_grant(vec![1, 2, 3], PermGrant::ManageAcl).unwrap();
264        ctx.acl_grant(vec![4, 5, 6], PermGrant::Admin).unwrap();
265        ctx.acl_remove_identity(&vec![4, 5, 6]).unwrap();
266        assert!(!ctx.granted_admin(&vec![4, 5, 6]));
267        assert!(ctx.granted_manage_acl(&vec![1, 2, 3]));
268    }
269
270    #[test]
271    fn test_should_begin_transaction() {
272        let ctx = DbmsContext::new(HeapMemoryProvider::default());
273        let owner = vec![1, 2, 3];
274        let tx_id = ctx.begin_transaction(owner.clone());
275        assert!(ctx.has_transaction(&tx_id, &owner));
276        assert!(!ctx.has_transaction(&tx_id, &[4, 5, 6]));
277    }
278
279    #[test]
280    fn test_should_debug_context() {
281        let ctx = DbmsContext::new(HeapMemoryProvider::default());
282        let debug = format!("{ctx:?}");
283        assert!(debug.contains("DbmsContext"));
284    }
285}