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
17use crate::transaction::journal::Journal;
18use crate::transaction::session::TransactionSession;
19
20/// Opaque identity of a [`DbmsContext`].
21///
22/// The identity remains stable when the context is moved. It does not keep the
23/// context alive, so context-associated caches can discard stale entries.
24#[derive(Clone, Debug)]
25pub struct ContextId(Weak<()>);
26
27impl ContextId {
28    /// Returns whether the context that owns this identity still exists.
29    #[must_use]
30    pub fn is_alive(&self) -> bool {
31        self.0.strong_count() > 0
32    }
33}
34
35impl PartialEq for ContextId {
36    fn eq(&self, other: &Self) -> bool {
37        Weak::ptr_eq(&self.0, &other.0)
38    }
39}
40
41impl Eq for ContextId {}
42
43impl Hash for ContextId {
44    fn hash<H>(&self, state: &mut H)
45    where
46        H: Hasher,
47    {
48        self.0.as_ptr().hash(state);
49    }
50}
51
52/// Owns all mutable DBMS state behind interior-mutable wrappers.
53///
54/// Each component is wrapped in a `RefCell` so that operations
55/// borrowing different components can coexist without requiring
56/// `&mut self` on the context.
57///
58/// # Threading
59///
60/// `DbmsContext` is `!Send` and `!Sync` because of the `RefCell`
61/// wrappers. This is intentional: WASM runtimes (both IC canisters
62/// and WASI preview 1 modules) execute single-threaded, so interior
63/// mutability via `RefCell` is sufficient and avoids the overhead of
64/// synchronization primitives. Embedders that need multi-threaded
65/// access should wrap the context in their own synchronization layer.
66pub struct DbmsContext<M>
67where
68    M: MemoryProvider,
69{
70    /// Stable identity used by state that is associated with this context.
71    identity: Rc<()>,
72
73    /// Memory manager for page-level operations.
74    pub(crate) mm: RefCell<MemoryManager<M>>,
75
76    /// Schema registry mapping table names to page locations.
77    pub(crate) schema_registry: RefCell<SchemaRegistry>,
78
79    /// Active transaction sessions.
80    pub(crate) transaction_session: RefCell<TransactionSession>,
81
82    /// Active write-ahead journal for atomic operations.
83    pub(crate) journal: RefCell<Option<Journal>>,
84
85    /// Cached `(compiled_schema_hash, drifted)` pair for the most recent
86    /// schema attached to a database session on this context.
87    pub(crate) drift: Cell<Option<(u64, bool)>>,
88
89    /// Set while a migration apply pass is mutating stable memory so the
90    /// per-CRUD drift gate does not block the engine's own internal reads
91    /// (e.g. tightening validation that scans existing rows).
92    pub(crate) migrating: Cell<bool>,
93}
94
95impl<M> DbmsContext<M>
96where
97    M: MemoryProvider,
98{
99    /// Creates a new DBMS context, initializing the memory manager and
100    /// loading persisted state.
101    pub fn new(memory: M) -> Self {
102        let mut mm = MemoryManager::init(memory);
103        let schema_registry = SchemaRegistry::load(&mut mm).unwrap_or_default();
104        Self {
105            identity: Rc::new(()),
106            mm: RefCell::new(mm),
107            schema_registry: RefCell::new(schema_registry),
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    /// Returns the stable identity of this context.
116    pub fn id(&self) -> ContextId {
117        ContextId(Rc::downgrade(&self.identity))
118    }
119
120    /// Registers a table schema, persisting it in stable memory.
121    pub fn register_table<T: wasm_dbms_api::prelude::TableSchema>(
122        &self,
123    ) -> DbmsResult<TableRegistryPage> {
124        let mut sr = self.schema_registry.borrow_mut();
125        let mut mm = self.mm.borrow_mut();
126        sr.register_table::<T>(&mut *mm).map_err(Into::into)
127    }
128
129    /// Returns whether `name` resolves to a registered table.
130    pub fn has_table(&self, name: &str) -> bool {
131        self.schema_registry
132            .borrow()
133            .table_registry_page_by_name(name)
134            .is_some()
135    }
136
137    /// Begins a new transaction and returns its ID.
138    ///
139    /// The engine does not record who opened the transaction: any code that
140    /// holds the ID can use, commit, or roll it back. An embedder that serves
141    /// several identities keeps its own ledger from ID to identity and checks
142    /// it before every transactional call.
143    pub fn begin_transaction(&self) -> TransactionId {
144        self.transaction_session.borrow_mut().begin_transaction()
145    }
146
147    /// Returns whether `tx_id` names a transaction that is still open.
148    ///
149    /// Embedders use it to drop ledger entries whose transaction was closed
150    /// through a path they did not see.
151    pub fn has_transaction(&self, tx_id: &TransactionId) -> bool {
152        self.transaction_session.borrow().has_transaction(tx_id)
153    }
154
155    /// Returns the cached drift flag for `compiled_hash`, if present.
156    pub(crate) fn cached_drift_for(&self, compiled_hash: u64) -> Option<bool> {
157        self.drift
158            .get()
159            .and_then(|(hash, drifted)| (hash == compiled_hash).then_some(drifted))
160    }
161
162    /// Caches the drift flag for the given compiled schema hash.
163    pub(crate) fn set_drift(&self, compiled_hash: u64, value: bool) {
164        self.drift.set(Some((compiled_hash, value)));
165    }
166
167    /// Clears the cached drift flag, forcing the next call to recompute.
168    pub(crate) fn clear_drift(&self) {
169        self.drift.set(None);
170    }
171
172    /// Returns `true` while a migration apply pass is mutating stable memory.
173    pub(crate) fn is_migrating(&self) -> bool {
174        self.migrating.get()
175    }
176
177    /// Sets the migration-in-progress guard.
178    pub(crate) fn set_migrating(&self, value: bool) {
179        self.migrating.set(value);
180    }
181}
182
183impl<M> std::fmt::Debug for DbmsContext<M>
184where
185    M: MemoryProvider,
186{
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        f.debug_struct("DbmsContext")
189            .field("schema_registry", &self.schema_registry)
190            .field("transaction_session", &self.transaction_session)
191            .finish_non_exhaustive()
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use wasm_dbms_memory::prelude::HeapMemoryProvider;
198
199    use super::*;
200
201    #[test]
202    fn test_should_create_context() {
203        let ctx = DbmsContext::new(HeapMemoryProvider::default());
204        assert!(!ctx.has_table("users"));
205    }
206
207    #[test]
208    fn test_should_begin_transaction() {
209        let ctx = DbmsContext::new(HeapMemoryProvider::default());
210        let tx_id = ctx.begin_transaction();
211        assert!(ctx.has_transaction(&tx_id));
212        assert!(!ctx.has_transaction(&(tx_id + 1)));
213    }
214
215    #[test]
216    fn test_transaction_ids_restart_with_the_context() {
217        let first = DbmsContext::new(HeapMemoryProvider::default());
218        let second = DbmsContext::new(HeapMemoryProvider::default());
219        assert_eq!(first.begin_transaction(), second.begin_transaction());
220    }
221
222    #[test]
223    fn test_context_identity_is_stable_and_unique() {
224        let first = DbmsContext::new(HeapMemoryProvider::default());
225        let first_id = first.id();
226        let moved = first;
227        let second = DbmsContext::new(HeapMemoryProvider::default());
228
229        assert_eq!(first_id, moved.id());
230        assert_ne!(first_id, second.id());
231    }
232
233    #[test]
234    fn test_should_debug_context() {
235        let ctx = DbmsContext::new(HeapMemoryProvider::default());
236        let debug = format!("{ctx:?}");
237        assert!(debug.contains("DbmsContext"));
238    }
239}