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};
11use std::hash::{Hash, Hasher};
12use std::rc::{Rc, Weak};
13
14use wasm_dbms_api::prelude::{DbmsResult, TransactionId};
15use wasm_dbms_memory::prelude::{MemoryManager, MemoryProvider, SchemaRegistry, TableRegistryPage};
16
17mod access_stats;
18
19pub(crate) use self::access_stats::AccessKind;
20#[cfg(any(test, feature = "access-stats"))]
21pub use self::access_stats::AccessStats;
22use crate::transaction::journal::Journal;
23use crate::transaction::session::TransactionSession;
24
25/// Opaque identity of a [`DbmsContext`].
26///
27/// The identity remains stable when the context is moved. It does not keep the
28/// context alive, so context-associated caches can discard stale entries.
29#[derive(Clone, Debug)]
30pub struct ContextId(Weak<()>);
31
32impl ContextId {
33    /// Returns whether the context that owns this identity still exists.
34    #[must_use]
35    pub fn is_alive(&self) -> bool {
36        self.0.strong_count() > 0
37    }
38}
39
40impl PartialEq for ContextId {
41    fn eq(&self, other: &Self) -> bool {
42        Weak::ptr_eq(&self.0, &other.0)
43    }
44}
45
46impl Eq for ContextId {}
47
48impl Hash for ContextId {
49    fn hash<H>(&self, state: &mut H)
50    where
51        H: Hasher,
52    {
53        self.0.as_ptr().hash(state);
54    }
55}
56
57/// Owns all mutable DBMS state behind interior-mutable wrappers.
58///
59/// Each component is wrapped in a `RefCell` so that operations
60/// borrowing different components can coexist without requiring
61/// `&mut self` on the context.
62///
63/// # Threading
64///
65/// `DbmsContext` is `!Send` and `!Sync` because of the `RefCell`
66/// wrappers. This is intentional: WASM runtimes (both IC canisters
67/// and WASI preview 1 modules) execute single-threaded, so interior
68/// mutability via `RefCell` is sufficient and avoids the overhead of
69/// synchronization primitives. Embedders that need multi-threaded
70/// access should wrap the context in their own synchronization layer.
71pub struct DbmsContext<M>
72where
73    M: MemoryProvider,
74{
75    /// Stable identity used by state that is associated with this context.
76    identity: Rc<()>,
77
78    /// Memory manager for page-level operations.
79    pub(crate) mm: RefCell<MemoryManager<M>>,
80
81    /// Schema registry mapping table names to page locations.
82    pub(crate) schema_registry: RefCell<SchemaRegistry>,
83
84    /// Active transaction sessions.
85    pub(crate) transaction_session: RefCell<TransactionSession>,
86
87    /// Active write-ahead journal for atomic operations.
88    pub(crate) journal: RefCell<Option<Journal>>,
89
90    /// Cached `(compiled_schema_hash, drifted)` pair for the most recent
91    /// schema attached to a database session on this context.
92    pub(crate) drift: Cell<Option<(u64, bool)>>,
93
94    /// Set while a migration apply pass is mutating stable memory so the
95    /// per-CRUD drift gate does not block the engine's own internal reads
96    /// (e.g. tightening validation that scans existing rows).
97    pub(crate) migrating: Cell<bool>,
98
99    /// Storage access counters, see [`AccessStats`].
100    #[cfg(any(test, feature = "access-stats"))]
101    access_stats: Cell<AccessStats>,
102}
103
104impl<M> DbmsContext<M>
105where
106    M: MemoryProvider,
107{
108    /// Creates a new DBMS context, initializing the memory manager and
109    /// loading persisted state.
110    pub fn new(memory: M) -> Self {
111        let mut mm = MemoryManager::init(memory);
112        let schema_registry = SchemaRegistry::load(&mut mm).unwrap_or_default();
113        Self {
114            identity: Rc::new(()),
115            mm: RefCell::new(mm),
116            schema_registry: RefCell::new(schema_registry),
117            transaction_session: RefCell::new(TransactionSession::default()),
118            journal: RefCell::new(None),
119            drift: Cell::new(None),
120            migrating: Cell::new(false),
121            #[cfg(any(test, feature = "access-stats"))]
122            access_stats: Cell::new(AccessStats::default()),
123        }
124    }
125
126    /// Publishes changes made through this context to the memory provider.
127    ///
128    /// Providers without durable storage implement this as a no-op. Active
129    /// transaction overlays remain uncommitted and are not flushed.
130    pub fn flush(&self) -> DbmsResult<()> {
131        self.mm.borrow_mut().flush().map_err(Into::into)
132    }
133
134    /// Returns the stable identity of this context.
135    pub fn id(&self) -> ContextId {
136        ContextId(Rc::downgrade(&self.identity))
137    }
138
139    /// Registers a table schema, persisting it in stable memory.
140    pub fn register_table<T: wasm_dbms_api::prelude::TableSchema>(
141        &self,
142    ) -> DbmsResult<TableRegistryPage> {
143        let mut sr = self.schema_registry.borrow_mut();
144        let mut mm = self.mm.borrow_mut();
145        sr.register_table::<T>(&mut *mm).map_err(Into::into)
146    }
147
148    /// Returns whether `name` resolves to a registered table.
149    pub fn has_table(&self, name: &str) -> bool {
150        self.schema_registry
151            .borrow()
152            .table_registry_page_by_name(name)
153            .is_some()
154    }
155
156    /// Begins a new transaction and returns its ID.
157    ///
158    /// The engine does not record who opened the transaction: any code that
159    /// holds the ID can use, commit, or roll it back. An embedder that serves
160    /// several identities keeps its own ledger from ID to identity and checks
161    /// it before every transactional call.
162    pub fn begin_transaction(&self) -> TransactionId {
163        self.transaction_session.borrow_mut().begin_transaction()
164    }
165
166    /// Returns whether `tx_id` names a transaction that is still open.
167    ///
168    /// Embedders use it to drop ledger entries whose transaction was closed
169    /// through a path they did not see.
170    pub fn has_transaction(&self, tx_id: &TransactionId) -> bool {
171        self.transaction_session.borrow().has_transaction(tx_id)
172    }
173
174    /// Returns the cached drift flag for `compiled_hash`, if present.
175    pub(crate) fn cached_drift_for(&self, compiled_hash: u64) -> Option<bool> {
176        self.drift
177            .get()
178            .and_then(|(hash, drifted)| (hash == compiled_hash).then_some(drifted))
179    }
180
181    /// Caches the drift flag for the given compiled schema hash.
182    pub(crate) fn set_drift(&self, compiled_hash: u64, value: bool) {
183        self.drift.set(Some((compiled_hash, value)));
184    }
185
186    /// Clears the cached drift flag, forcing the next call to recompute.
187    pub(crate) fn clear_drift(&self) {
188        self.drift.set(None);
189    }
190
191    /// Returns `true` while a migration apply pass is mutating stable memory.
192    pub(crate) fn is_migrating(&self) -> bool {
193        self.migrating.get()
194    }
195
196    /// Sets the migration-in-progress guard.
197    pub(crate) fn set_migrating(&self, value: bool) {
198        self.migrating.set(value);
199    }
200
201    /// Records `count` storage accesses of `kind`.
202    ///
203    /// Compiles to nothing unless the `access-stats` feature is enabled.
204    #[inline]
205    pub(crate) fn count_access(&self, kind: AccessKind, count: u64) {
206        #[cfg(any(test, feature = "access-stats"))]
207        {
208            let mut stats = self.access_stats.get();
209            stats.add(kind, count);
210            self.access_stats.set(stats);
211        }
212        #[cfg(not(any(test, feature = "access-stats")))]
213        let _ = (kind, count);
214    }
215
216    /// Returns the storage accesses counted since the last reset.
217    #[cfg(any(test, feature = "access-stats"))]
218    #[cfg_attr(docsrs, doc(cfg(feature = "access-stats")))]
219    pub fn access_stats(&self) -> AccessStats {
220        self.access_stats.get()
221    }
222
223    /// Resets every storage access counter to zero.
224    #[cfg(any(test, feature = "access-stats"))]
225    #[cfg_attr(docsrs, doc(cfg(feature = "access-stats")))]
226    pub fn reset_access_stats(&self) {
227        self.access_stats.set(AccessStats::default());
228    }
229}
230
231impl<M> std::fmt::Debug for DbmsContext<M>
232where
233    M: MemoryProvider,
234{
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        f.debug_struct("DbmsContext")
237            .field("schema_registry", &self.schema_registry)
238            .field("transaction_session", &self.transaction_session)
239            .finish_non_exhaustive()
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use std::cell::Cell;
246    use std::rc::Rc;
247
248    use wasm_dbms_api::prelude::{MemoryError, MemoryResult};
249    use wasm_dbms_memory::MemoryProvider;
250    use wasm_dbms_memory::prelude::HeapMemoryProvider;
251
252    use super::*;
253
254    struct FlushProbe {
255        provider: HeapMemoryProvider,
256        calls: Rc<Cell<usize>>,
257        fail: Rc<Cell<bool>>,
258    }
259
260    impl FlushProbe {
261        fn new(calls: Rc<Cell<usize>>, fail: Rc<Cell<bool>>) -> Self {
262            Self {
263                provider: HeapMemoryProvider::default(),
264                calls,
265                fail,
266            }
267        }
268    }
269
270    impl MemoryProvider for FlushProbe {
271        const PAGE_SIZE: u64 = HeapMemoryProvider::PAGE_SIZE;
272
273        fn size(&self) -> u64 {
274            self.provider.size()
275        }
276
277        fn pages(&self) -> u64 {
278            self.provider.pages()
279        }
280
281        fn grow(&mut self, new_pages: u64) -> MemoryResult<u64> {
282            self.provider.grow(new_pages)
283        }
284
285        fn read(&mut self, offset: u64, buf: &mut [u8]) -> MemoryResult<()> {
286            self.provider.read(offset, buf)
287        }
288
289        fn write(&mut self, offset: u64, buf: &[u8]) -> MemoryResult<()> {
290            self.provider.write(offset, buf)
291        }
292
293        fn flush(&mut self) -> MemoryResult<()> {
294            self.calls.set(self.calls.get() + 1);
295            if self.fail.get() {
296                return Err(MemoryError::ProviderError("flush failed".to_owned()));
297            }
298            Ok(())
299        }
300    }
301
302    #[test]
303    fn test_should_create_context() {
304        let ctx = DbmsContext::new(HeapMemoryProvider::default());
305        assert!(!ctx.has_table("users"));
306    }
307
308    #[test]
309    fn test_should_begin_transaction() {
310        let ctx = DbmsContext::new(HeapMemoryProvider::default());
311        let tx_id = ctx.begin_transaction();
312        assert!(ctx.has_transaction(&tx_id));
313        assert!(!ctx.has_transaction(&(tx_id + 1)));
314    }
315
316    #[test]
317    fn test_transaction_ids_restart_with_the_context() {
318        let first = DbmsContext::new(HeapMemoryProvider::default());
319        let second = DbmsContext::new(HeapMemoryProvider::default());
320        assert_eq!(first.begin_transaction(), second.begin_transaction());
321    }
322
323    #[test]
324    fn test_context_identity_is_stable_and_unique() {
325        let first = DbmsContext::new(HeapMemoryProvider::default());
326        let first_id = first.id();
327        let moved = first;
328        let second = DbmsContext::new(HeapMemoryProvider::default());
329
330        assert_eq!(first_id, moved.id());
331        assert_ne!(first_id, second.id());
332    }
333
334    #[test]
335    fn test_should_debug_context() {
336        let ctx = DbmsContext::new(HeapMemoryProvider::default());
337        let debug = format!("{ctx:?}");
338        assert!(debug.contains("DbmsContext"));
339    }
340
341    #[test]
342    fn context_flush_reaches_provider_and_preserves_error() {
343        let calls = Rc::new(Cell::new(0));
344        let fail = Rc::new(Cell::new(false));
345        let ctx = DbmsContext::new(FlushProbe::new(calls.clone(), fail.clone()));
346        assert_eq!(calls.get(), 0);
347        ctx.flush().unwrap();
348        assert_eq!(calls.get(), 1);
349        fail.set(true);
350        assert!(
351            ctx.flush()
352                .unwrap_err()
353                .to_string()
354                .contains("flush failed")
355        );
356        assert_eq!(calls.get(), 2);
357    }
358
359    #[test]
360    fn default_flush_keeps_heap_context_and_transaction_usable() {
361        let ctx = DbmsContext::new(HeapMemoryProvider::default());
362        let tx = ctx.begin_transaction();
363        ctx.flush().unwrap();
364        assert!(ctx.has_transaction(&tx));
365    }
366}