use std::cell::{Cell, RefCell};
use std::hash::{Hash, Hasher};
use std::rc::{Rc, Weak};
use wasm_dbms_api::prelude::{DbmsResult, TransactionId};
use wasm_dbms_memory::prelude::{MemoryManager, MemoryProvider, SchemaRegistry, TableRegistryPage};
mod access_stats;
pub(crate) use self::access_stats::AccessKind;
#[cfg(any(test, feature = "access-stats"))]
pub use self::access_stats::AccessStats;
use crate::transaction::journal::Journal;
use crate::transaction::session::TransactionSession;
#[derive(Clone, Debug)]
pub struct ContextId(Weak<()>);
impl ContextId {
#[must_use]
pub fn is_alive(&self) -> bool {
self.0.strong_count() > 0
}
}
impl PartialEq for ContextId {
fn eq(&self, other: &Self) -> bool {
Weak::ptr_eq(&self.0, &other.0)
}
}
impl Eq for ContextId {}
impl Hash for ContextId {
fn hash<H>(&self, state: &mut H)
where
H: Hasher,
{
self.0.as_ptr().hash(state);
}
}
pub struct DbmsContext<M>
where
M: MemoryProvider,
{
identity: Rc<()>,
pub(crate) mm: RefCell<MemoryManager<M>>,
pub(crate) schema_registry: RefCell<SchemaRegistry>,
pub(crate) transaction_session: RefCell<TransactionSession>,
pub(crate) journal: RefCell<Option<Journal>>,
pub(crate) drift: Cell<Option<(u64, bool)>>,
pub(crate) migrating: Cell<bool>,
#[cfg(any(test, feature = "access-stats"))]
access_stats: Cell<AccessStats>,
}
impl<M> DbmsContext<M>
where
M: MemoryProvider,
{
pub fn new(memory: M) -> Self {
let mut mm = MemoryManager::init(memory);
let schema_registry = SchemaRegistry::load(&mut mm).unwrap_or_default();
Self {
identity: Rc::new(()),
mm: RefCell::new(mm),
schema_registry: RefCell::new(schema_registry),
transaction_session: RefCell::new(TransactionSession::default()),
journal: RefCell::new(None),
drift: Cell::new(None),
migrating: Cell::new(false),
#[cfg(any(test, feature = "access-stats"))]
access_stats: Cell::new(AccessStats::default()),
}
}
pub fn flush(&self) -> DbmsResult<()> {
self.mm.borrow_mut().flush().map_err(Into::into)
}
pub fn id(&self) -> ContextId {
ContextId(Rc::downgrade(&self.identity))
}
pub fn register_table<T: wasm_dbms_api::prelude::TableSchema>(
&self,
) -> DbmsResult<TableRegistryPage> {
let mut sr = self.schema_registry.borrow_mut();
let mut mm = self.mm.borrow_mut();
sr.register_table::<T>(&mut *mm).map_err(Into::into)
}
pub fn has_table(&self, name: &str) -> bool {
self.schema_registry
.borrow()
.table_registry_page_by_name(name)
.is_some()
}
pub fn begin_transaction(&self) -> TransactionId {
self.transaction_session.borrow_mut().begin_transaction()
}
pub fn has_transaction(&self, tx_id: &TransactionId) -> bool {
self.transaction_session.borrow().has_transaction(tx_id)
}
pub(crate) fn cached_drift_for(&self, compiled_hash: u64) -> Option<bool> {
self.drift
.get()
.and_then(|(hash, drifted)| (hash == compiled_hash).then_some(drifted))
}
pub(crate) fn set_drift(&self, compiled_hash: u64, value: bool) {
self.drift.set(Some((compiled_hash, value)));
}
pub(crate) fn clear_drift(&self) {
self.drift.set(None);
}
pub(crate) fn is_migrating(&self) -> bool {
self.migrating.get()
}
pub(crate) fn set_migrating(&self, value: bool) {
self.migrating.set(value);
}
#[inline]
pub(crate) fn count_access(&self, kind: AccessKind, count: u64) {
#[cfg(any(test, feature = "access-stats"))]
{
let mut stats = self.access_stats.get();
stats.add(kind, count);
self.access_stats.set(stats);
}
#[cfg(not(any(test, feature = "access-stats")))]
let _ = (kind, count);
}
#[cfg(any(test, feature = "access-stats"))]
#[cfg_attr(docsrs, doc(cfg(feature = "access-stats")))]
pub fn access_stats(&self) -> AccessStats {
self.access_stats.get()
}
#[cfg(any(test, feature = "access-stats"))]
#[cfg_attr(docsrs, doc(cfg(feature = "access-stats")))]
pub fn reset_access_stats(&self) {
self.access_stats.set(AccessStats::default());
}
}
impl<M> std::fmt::Debug for DbmsContext<M>
where
M: MemoryProvider,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DbmsContext")
.field("schema_registry", &self.schema_registry)
.field("transaction_session", &self.transaction_session)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use std::rc::Rc;
use wasm_dbms_api::prelude::{MemoryError, MemoryResult};
use wasm_dbms_memory::MemoryProvider;
use wasm_dbms_memory::prelude::HeapMemoryProvider;
use super::*;
struct FlushProbe {
provider: HeapMemoryProvider,
calls: Rc<Cell<usize>>,
fail: Rc<Cell<bool>>,
}
impl FlushProbe {
fn new(calls: Rc<Cell<usize>>, fail: Rc<Cell<bool>>) -> Self {
Self {
provider: HeapMemoryProvider::default(),
calls,
fail,
}
}
}
impl MemoryProvider for FlushProbe {
const PAGE_SIZE: u64 = HeapMemoryProvider::PAGE_SIZE;
fn size(&self) -> u64 {
self.provider.size()
}
fn pages(&self) -> u64 {
self.provider.pages()
}
fn grow(&mut self, new_pages: u64) -> MemoryResult<u64> {
self.provider.grow(new_pages)
}
fn read(&mut self, offset: u64, buf: &mut [u8]) -> MemoryResult<()> {
self.provider.read(offset, buf)
}
fn write(&mut self, offset: u64, buf: &[u8]) -> MemoryResult<()> {
self.provider.write(offset, buf)
}
fn flush(&mut self) -> MemoryResult<()> {
self.calls.set(self.calls.get() + 1);
if self.fail.get() {
return Err(MemoryError::ProviderError("flush failed".to_owned()));
}
Ok(())
}
}
#[test]
fn test_should_create_context() {
let ctx = DbmsContext::new(HeapMemoryProvider::default());
assert!(!ctx.has_table("users"));
}
#[test]
fn test_should_begin_transaction() {
let ctx = DbmsContext::new(HeapMemoryProvider::default());
let tx_id = ctx.begin_transaction();
assert!(ctx.has_transaction(&tx_id));
assert!(!ctx.has_transaction(&(tx_id + 1)));
}
#[test]
fn test_transaction_ids_restart_with_the_context() {
let first = DbmsContext::new(HeapMemoryProvider::default());
let second = DbmsContext::new(HeapMemoryProvider::default());
assert_eq!(first.begin_transaction(), second.begin_transaction());
}
#[test]
fn test_context_identity_is_stable_and_unique() {
let first = DbmsContext::new(HeapMemoryProvider::default());
let first_id = first.id();
let moved = first;
let second = DbmsContext::new(HeapMemoryProvider::default());
assert_eq!(first_id, moved.id());
assert_ne!(first_id, second.id());
}
#[test]
fn test_should_debug_context() {
let ctx = DbmsContext::new(HeapMemoryProvider::default());
let debug = format!("{ctx:?}");
assert!(debug.contains("DbmsContext"));
}
#[test]
fn context_flush_reaches_provider_and_preserves_error() {
let calls = Rc::new(Cell::new(0));
let fail = Rc::new(Cell::new(false));
let ctx = DbmsContext::new(FlushProbe::new(calls.clone(), fail.clone()));
assert_eq!(calls.get(), 0);
ctx.flush().unwrap();
assert_eq!(calls.get(), 1);
fail.set(true);
assert!(
ctx.flush()
.unwrap_err()
.to_string()
.contains("flush failed")
);
assert_eq!(calls.get(), 2);
}
#[test]
fn default_flush_keeps_heap_context_and_transaction_usable() {
let ctx = DbmsContext::new(HeapMemoryProvider::default());
let tx = ctx.begin_transaction();
ctx.flush().unwrap();
assert!(ctx.has_transaction(&tx));
}
}