use fsqlite_error::Result;
use fsqlite_types::cx::Cx;
use fsqlite_types::{PageData, PageNumber};
pub(crate) mod sealed {
pub trait Sealed {}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum JournalMode {
#[default]
Delete,
Wal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CheckpointMode {
#[default]
Passive,
Full,
Restart,
Truncate,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckpointResult {
pub total_frames: u32,
pub frames_backfilled: u32,
pub completed: bool,
pub wal_was_reset: bool,
}
pub trait WalBackend: Send {
fn begin_transaction(&mut self, _cx: &Cx) -> Result<()> {
Ok(())
}
fn append_frame(
&mut self,
cx: &Cx,
page_number: u32,
page_data: &[u8],
db_size_if_commit: u32,
) -> Result<()>;
fn read_page(&mut self, cx: &Cx, page_number: u32) -> Result<Option<Vec<u8>>>;
fn sync(&mut self, cx: &Cx) -> Result<()>;
fn frame_count(&self) -> usize;
fn checkpoint(
&mut self,
cx: &Cx,
mode: CheckpointMode,
writer: &mut dyn CheckpointPageWriter,
backfilled_frames: u32,
oldest_reader_frame: Option<u32>,
) -> Result<CheckpointResult>;
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum TransactionMode {
#[default]
Deferred,
Immediate,
Exclusive,
Concurrent,
ReadOnly,
}
pub trait MvccPager: sealed::Sealed + Send + Sync {
type Txn: TransactionHandle;
fn begin(&self, cx: &Cx, mode: TransactionMode) -> Result<Self::Txn>;
fn journal_mode(&self) -> JournalMode;
fn set_journal_mode(&self, cx: &Cx, mode: JournalMode) -> Result<JournalMode>;
fn set_wal_backend(&self, backend: Box<dyn WalBackend>) -> Result<()>;
}
pub trait TransactionHandle: sealed::Sealed + Send {
fn get_page(&self, cx: &Cx, page_no: PageNumber) -> Result<PageData>;
fn write_page(&mut self, cx: &Cx, page_no: PageNumber, data: &[u8]) -> Result<()>;
fn allocate_page(&mut self, cx: &Cx) -> Result<PageNumber>;
fn free_page(&mut self, cx: &Cx, page_no: PageNumber) -> Result<()>;
fn commit(&mut self, cx: &Cx) -> Result<()>;
fn rollback(&mut self, cx: &Cx) -> Result<()>;
fn savepoint(&mut self, cx: &Cx, name: &str) -> Result<()>;
fn release_savepoint(&mut self, cx: &Cx, name: &str) -> Result<()>;
fn rollback_to_savepoint(&mut self, cx: &Cx, name: &str) -> Result<()>;
}
pub trait CheckpointPageWriter: sealed::Sealed + Send {
fn write_page(&mut self, cx: &Cx, page_no: PageNumber, data: &[u8]) -> Result<()>;
fn truncate(&mut self, cx: &Cx, n_pages: u32) -> Result<()>;
fn sync(&mut self, cx: &Cx) -> Result<()>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct MockMvccPager;
impl sealed::Sealed for MockMvccPager {}
impl MvccPager for MockMvccPager {
type Txn = MockTransaction;
fn begin(&self, _cx: &Cx, _mode: TransactionMode) -> Result<Self::Txn> {
Ok(MockTransaction {
committed: false,
next_page: 2,
savepoint_names: Vec::new(),
})
}
fn journal_mode(&self) -> JournalMode {
JournalMode::Delete
}
fn set_journal_mode(&self, _cx: &Cx, mode: JournalMode) -> Result<JournalMode> {
Ok(mode)
}
fn set_wal_backend(&self, _backend: Box<dyn WalBackend>) -> Result<()> {
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct MockTransaction {
committed: bool,
next_page: u32,
savepoint_names: Vec<String>,
}
impl sealed::Sealed for MockTransaction {}
impl TransactionHandle for MockTransaction {
fn get_page(&self, _cx: &Cx, page_no: PageNumber) -> Result<PageData> {
let size = fsqlite_types::PageSize::default();
let mut data = PageData::zeroed(size);
data.as_bytes_mut()[..4].copy_from_slice(&page_no.get().to_le_bytes());
Ok(data)
}
fn write_page(&mut self, _cx: &Cx, _page_no: PageNumber, _data: &[u8]) -> Result<()> {
Ok(())
}
fn allocate_page(&mut self, _cx: &Cx) -> Result<PageNumber> {
let page = PageNumber::new(self.next_page)
.expect("mock allocator must always produce non-zero page numbers");
self.next_page += 1;
Ok(page)
}
fn free_page(&mut self, _cx: &Cx, _page_no: PageNumber) -> Result<()> {
Ok(())
}
fn commit(&mut self, _cx: &Cx) -> Result<()> {
self.committed = true;
Ok(())
}
fn rollback(&mut self, _cx: &Cx) -> Result<()> {
Ok(())
}
fn savepoint(&mut self, _cx: &Cx, name: &str) -> Result<()> {
self.savepoint_names.push(name.to_owned());
Ok(())
}
fn release_savepoint(&mut self, _cx: &Cx, name: &str) -> Result<()> {
if let Some(pos) = self.savepoint_names.iter().rposition(|n| n == name) {
self.savepoint_names.truncate(pos);
Ok(())
} else {
Err(fsqlite_error::FrankenError::internal(format!(
"no savepoint named '{name}'"
)))
}
}
fn rollback_to_savepoint(&mut self, _cx: &Cx, name: &str) -> Result<()> {
if let Some(pos) = self.savepoint_names.iter().rposition(|n| n == name) {
self.savepoint_names.truncate(pos + 1);
Ok(())
} else {
Err(fsqlite_error::FrankenError::internal(format!(
"no savepoint named '{name}'"
)))
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct MockCheckpointPageWriter;
impl sealed::Sealed for MockCheckpointPageWriter {}
impl CheckpointPageWriter for MockCheckpointPageWriter {
fn write_page(&mut self, _cx: &Cx, _page_no: PageNumber, _data: &[u8]) -> Result<()> {
Ok(())
}
fn truncate(&mut self, _cx: &Cx, _n_pages: u32) -> Result<()> {
Ok(())
}
fn sync(&mut self, _cx: &Cx) -> Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pager_trait_is_sealed_mock_impl() {
let pager = MockMvccPager;
let cx = Cx::new();
let _txn = pager.begin(&cx, TransactionMode::Deferred).unwrap();
}
#[test]
fn test_mvccpager_begin_commit_rollback_signatures() {
let pager = MockMvccPager;
let cx = Cx::new();
let mut txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let page_no = PageNumber::new(1).unwrap();
let data = txn.get_page(&cx, page_no).unwrap();
assert_eq!(
u32::from_le_bytes(data.as_bytes()[..4].try_into().unwrap()),
1
);
txn.write_page(&cx, page_no, &[0u8; 4096]).unwrap();
let new_page = txn.allocate_page(&cx).unwrap();
assert_eq!(new_page.get(), 2);
txn.free_page(&cx, new_page).unwrap();
txn.commit(&cx).unwrap();
}
#[test]
fn test_transaction_rollback_is_infallible() {
let pager = MockMvccPager;
let cx = Cx::new();
let mut txn = pager.begin(&cx, TransactionMode::Deferred).unwrap();
txn.rollback(&cx).unwrap();
}
#[test]
fn test_checkpoint_page_writer_signatures() {
let mut writer = MockCheckpointPageWriter;
let cx = Cx::new();
let page1 = PageNumber::new(1).unwrap();
writer.write_page(&cx, page1, &[0u8; 4096]).unwrap();
writer.truncate(&cx, 10).unwrap();
writer.sync(&cx).unwrap();
}
#[test]
fn test_transaction_mode_default_is_deferred() {
assert_eq!(TransactionMode::default(), TransactionMode::Deferred);
}
#[test]
fn test_open_traits_are_extensible() {
let pager = MockMvccPager;
let _: &dyn MvccPager<Txn = MockTransaction> = &pager;
}
}