mod autoincrement_ledger;
mod free_segments_ledger;
mod index_ledger;
mod page_ledger;
mod raw_record;
mod raw_table_reader;
mod record_address;
mod schema_snapshot_ledger;
mod sized_read;
mod table_reader;
mod write_at;
use std::borrow::Cow;
use wasm_dbms_api::prelude::{
DataSize, DecodeError, Encode, MSize, MemoryError, MemoryResult, Page, PageOffset, Value,
};
pub use self::autoincrement_ledger::AutoincrementLedger;
use self::free_segments_ledger::FreeSegmentsLedger;
pub use self::index_ledger::{IndexLedger, IndexTreeWalker};
use self::page_ledger::PageLedger;
use self::raw_record::{RAW_RECORD_HEADER_SIZE, RawRecord};
pub use self::raw_table_reader::{RawRecordBytes, RawTableReader};
pub use self::record_address::RecordAddress;
pub use self::schema_snapshot_ledger::SchemaSnapshotLedger;
pub use self::table_reader::{NextRecord, TableReader};
use self::write_at::WriteAt;
use crate::{MemoryAccess, TableRegistryPage, align_up};
pub struct TableRegistry {
schema_snapshot_page: Page,
pub(crate) page_ledger: PageLedger,
free_segments_ledger: FreeSegmentsLedger,
index_ledger: IndexLedger,
auto_increment_ledger: Option<AutoincrementLedger>,
}
impl TableRegistry {
pub fn load(table_pages: TableRegistryPage, mm: &mut impl MemoryAccess) -> MemoryResult<Self> {
Ok(Self {
schema_snapshot_page: table_pages.schema_snapshot_page,
page_ledger: PageLedger::load(table_pages.pages_list_page, mm)?,
free_segments_ledger: FreeSegmentsLedger::load(table_pages.free_segments_page, mm)?,
index_ledger: IndexLedger::load(table_pages.index_registry_page, mm)?,
auto_increment_ledger: if let Some(page) = table_pages.autoincrement_registry_page {
Some(AutoincrementLedger::load(page, mm)?)
} else {
None
},
})
}
pub fn insert<E>(
&mut self,
record: E,
mm: &mut impl MemoryAccess,
) -> MemoryResult<RecordAddress>
where
E: Encode,
{
ensure_record_fits(&record, mm.page_size())?;
let raw_record = RawRecord::new(record);
let write_at = self.get_write_position(&raw_record, mm)?;
let aligned_offset = align_up::<RawRecord<E>>(write_at.offset() as usize) as PageOffset;
mm.write_at(write_at.page(), aligned_offset, &raw_record)?;
let pointer = RecordAddress {
page: write_at.page(),
offset: aligned_offset,
};
self.post_write(write_at, &raw_record, mm)?;
Ok(pointer)
}
pub fn read<'a, E, MA>(&'a self, mm: &'a mut MA) -> TableReader<'a, E, MA>
where
E: Encode,
MA: MemoryAccess,
{
TableReader::new(&self.page_ledger, mm)
}
pub fn read_at<E, MA>(&self, address: RecordAddress, mm: &mut MA) -> MemoryResult<E>
where
E: Encode,
MA: MemoryAccess,
{
if let DataSize::Fixed(_) = E::SIZE {
let raw_record: RawRecord<E> = mm.read_at(address.page, address.offset)?;
return Ok(raw_record.data);
}
let alignment = <RawRecord<E> as Encode>::ALIGNMENT;
if alignment != 0 && !address.offset.is_multiple_of(alignment) {
return Err(MemoryError::OffsetNotAligned {
offset: address.offset,
alignment,
});
}
let mut header = [0u8; RAW_RECORD_HEADER_SIZE as usize];
if mm.read_at_raw(address.page, address.offset, &mut header)? < header.len() {
return Err(MemoryError::DecodeError(DecodeError::TooShort));
}
let length = MSize::from_le_bytes(header) as usize;
let body_offset = address.offset as usize + RAW_RECORD_HEADER_SIZE as usize;
if body_offset + length > mm.page_size() as usize {
return Err(MemoryError::DecodeError(DecodeError::TooShort));
}
let mut body = vec![0u8; length];
mm.read_at_raw(address.page, body_offset as PageOffset, &mut body)?;
E::decode(Cow::Owned(body))
}
pub fn delete(
&mut self,
record: impl Encode,
address: RecordAddress,
mm: &mut impl MemoryAccess,
) -> MemoryResult<()> {
let raw_record = RawRecord::new(record);
mm.zero(address.page, address.offset, &raw_record)?;
self.free_segments_ledger
.insert_free_segment(address.page, address.offset, &raw_record, mm)
}
pub fn update(
&mut self,
new_record: impl Encode,
old_record: impl Encode,
old_address: RecordAddress,
mm: &mut impl MemoryAccess,
) -> MemoryResult<RecordAddress> {
ensure_record_fits(&new_record, mm.page_size())?;
if new_record.size() == old_record.size() {
self.update_in_place(new_record, old_address, mm)
} else {
self.update_by_realloc(new_record, old_record, old_address, mm)
}
}
pub fn iter_raw<'a, MA>(
&'a self,
alignment: PageOffset,
mm: &'a mut MA,
) -> RawTableReader<'a, MA>
where
MA: MemoryAccess,
{
RawTableReader::new(&self.page_ledger, alignment, mm)
}
pub fn insert_raw(
&mut self,
bytes: &[u8],
alignment: PageOffset,
mm: &mut impl MemoryAccess,
) -> MemoryResult<RecordAddress> {
let physical_size_msize = raw_record_footprint(bytes.len(), alignment, mm.page_size())?;
let length = bytes.len() as MSize;
let total = (RAW_RECORD_HEADER_SIZE + length) as u64;
let physical_size_u64 = physical_size_msize as u64;
let (page, offset) = if let Some(segment) = self
.free_segments_ledger
.find_reusable_segment_raw(physical_size_msize, mm)?
{
let page = segment.segment.page;
let offset = segment.segment.offset;
self.free_segments_ledger
.commit_reused_space_raw(physical_size_msize, segment, mm)?;
(page, offset)
} else {
let (page, offset) =
self.page_ledger
.get_page_and_offset_raw(physical_size_u64, alignment, mm)?;
self.page_ledger.commit_raw(page, total, alignment, mm)?;
(page, offset)
};
let mut full = Vec::with_capacity(total as usize);
full.extend_from_slice(&length.to_le_bytes());
full.extend_from_slice(bytes);
mm.write_at_raw(page, offset, &full)?;
let padding = physical_size_msize - (RAW_RECORD_HEADER_SIZE + length);
if padding > 0 {
let padding_offset = offset + RAW_RECORD_HEADER_SIZE + length;
mm.zero_raw(page, padding_offset, padding)?;
}
Ok(RecordAddress { page, offset })
}
pub fn read_raw_at(
&self,
address: RecordAddress,
mm: &mut impl MemoryAccess,
) -> MemoryResult<Vec<u8>> {
let mut header = [0u8; RAW_RECORD_HEADER_SIZE as usize];
mm.read_at_raw(address.page, address.offset, &mut header)?;
let length = u16::from_le_bytes(header) as usize;
let mut body = vec![0u8; length];
mm.read_at_raw(
address.page,
address.offset + RAW_RECORD_HEADER_SIZE,
&mut body,
)?;
Ok(body)
}
pub fn delete_raw(
&mut self,
address: RecordAddress,
body_len: MSize,
alignment: PageOffset,
mm: &mut impl MemoryAccess,
) -> MemoryResult<()> {
let physical_size = raw_record_footprint(body_len as usize, alignment, mm.page_size())?;
mm.zero_raw(address.page, address.offset, physical_size)?;
self.free_segments_ledger.insert_free_segment_raw(
address.page,
address.offset,
physical_size,
mm,
)
}
pub fn release_pages(
self,
table_pages: TableRegistryPage,
mm: &mut impl MemoryAccess,
) -> MemoryResult<()> {
self.page_ledger.release_pages(mm)?;
self.free_segments_ledger.release_pages(mm)?;
self.index_ledger.release_pages(mm)?;
mm.unclaim_page(table_pages.schema_snapshot_page)?;
if let Some(page) = table_pages.autoincrement_registry_page {
mm.unclaim_page(page)?;
}
Ok(())
}
pub fn releasable_pages_count(
&self,
table_pages: TableRegistryPage,
mm: &mut impl MemoryAccess,
) -> MemoryResult<usize> {
let mut count = self.page_ledger.releasable_pages_count();
count += self.free_segments_ledger.releasable_pages_count();
count += self.index_ledger.releasable_pages_count(mm)?;
count += 1; if table_pages.autoincrement_registry_page.is_some() {
count += 1;
}
Ok(count)
}
pub fn index_ledger(&self) -> &IndexLedger {
&self.index_ledger
}
pub fn index_ledger_mut(&mut self) -> &mut IndexLedger {
&mut self.index_ledger
}
pub fn schema_snapshot_ledger(
&self,
mm: &mut impl MemoryAccess,
) -> MemoryResult<SchemaSnapshotLedger> {
SchemaSnapshotLedger::load(self.schema_snapshot_page, mm)
}
pub fn next_autoincrement(
&mut self,
column_name: &str,
mm: &mut impl MemoryAccess,
) -> MemoryResult<Option<Value>> {
if let Some(ledger) = &mut self.auto_increment_ledger {
ledger.next(column_name, mm).map(Some)
} else {
Ok(None)
}
}
fn update_in_place(
&mut self,
record: impl Encode,
address: RecordAddress,
mm: &mut impl MemoryAccess,
) -> MemoryResult<RecordAddress> {
let raw_record = RawRecord::new(record);
mm.write_at(address.page, address.offset, &raw_record)?;
Ok(address)
}
fn update_by_realloc(
&mut self,
new_record: impl Encode,
old_record: impl Encode,
old_address: RecordAddress,
mm: &mut impl MemoryAccess,
) -> MemoryResult<RecordAddress> {
self.delete(old_record, old_address, mm)?;
self.insert(new_record, mm)
}
fn get_write_position<E>(
&mut self,
record: &RawRecord<E>,
mm: &mut impl MemoryAccess,
) -> MemoryResult<WriteAt>
where
E: Encode,
{
if let Some(segment) = self
.free_segments_ledger
.find_reusable_segment(record, mm)?
{
return Ok(WriteAt::ReusedSegment(segment));
}
self.page_ledger
.get_page_and_offset_for_record(record, mm)
.map(|(page, offset)| WriteAt::End(page, offset))
}
fn post_write<E>(
&mut self,
write_at: WriteAt,
record: &RawRecord<E>,
mm: &mut impl MemoryAccess,
) -> MemoryResult<()>
where
E: Encode,
{
match write_at {
WriteAt::ReusedSegment(free_segment) => {
self.free_segments_ledger
.commit_reused_space(record, free_segment, mm)
}
WriteAt::End(page, ..) => {
self.page_ledger.commit(page, record, mm)
}
}
}
}
fn ensure_record_fits<E>(record: &E, page_size: u64) -> MemoryResult<()>
where
E: Encode,
{
raw_record_footprint(
record.size() as usize,
<RawRecord<E> as Encode>::ALIGNMENT,
page_size,
)
.map(|_| ())
}
fn raw_record_footprint(
body_len: usize,
alignment: PageOffset,
page_size: u64,
) -> MemoryResult<MSize> {
let unaligned = (body_len as u64).saturating_add(RAW_RECORD_HEADER_SIZE as u64);
let aligned = if alignment == 0 {
unaligned
} else {
unaligned
.div_ceil(alignment as u64)
.saturating_mul(alignment as u64)
};
MSize::try_from(aligned).map_err(|_| MemoryError::DataTooLarge {
page_size,
requested: aligned,
})
}
#[cfg(test)]
pub(crate) mod test_utils {
use wasm_dbms_api::prelude::{
DEFAULT_ALIGNMENT, DataSize, DecodeError, Encode, MSize, MemoryError, MemoryResult, Page,
PageOffset, TableSchemaSnapshot,
};
use crate::MemoryAccess;
pub fn write_dummy_schema_snapshot(page: Page, mm: &mut impl MemoryAccess) {
let dummy = TableSchemaSnapshot {
version: TableSchemaSnapshot::latest_version(),
name: "dummy".to_string(),
primary_key: "id".to_string(),
alignment: 8,
columns: vec![],
indexes: vec![],
};
mm.write_at(page, 0, &dummy)
.expect("failed to write dummy snapshot");
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct User {
pub id: u32,
pub name: String,
pub email: String,
pub age: u32,
}
impl Encode for User {
const SIZE: DataSize = DataSize::Dynamic;
const ALIGNMENT: PageOffset = DEFAULT_ALIGNMENT;
fn encode(&'_ self) -> std::borrow::Cow<'_, [u8]> {
let mut buf = Vec::new();
buf.extend_from_slice(&self.id.to_le_bytes());
buf.extend_from_slice(&(self.name.len() as u16).to_le_bytes());
buf.extend_from_slice(self.name.as_bytes());
buf.extend_from_slice(&(self.email.len() as u16).to_le_bytes());
buf.extend_from_slice(self.email.as_bytes());
buf.extend_from_slice(&self.age.to_le_bytes());
std::borrow::Cow::Owned(buf)
}
fn decode(data: std::borrow::Cow<[u8]>) -> MemoryResult<Self>
where
Self: Sized,
{
if data.len() < 12 {
return Err(MemoryError::DecodeError(DecodeError::TooShort));
}
let mut offset = 0;
let id = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
offset += 4;
let name_len =
u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap()) as usize;
offset += 2;
let name = String::from_utf8_lossy(&data[offset..offset + name_len]).to_string();
offset += name_len;
let email_len =
u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap()) as usize;
offset += 2;
let email = String::from_utf8_lossy(&data[offset..offset + email_len]).to_string();
offset += email_len;
let age = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
Ok(User {
id,
name,
email,
age,
})
}
fn size(&self) -> MSize {
(4 + 2 + self.name.len() + 2 + self.email.len() + 4) as MSize
}
}
}
#[cfg(test)]
mod tests {
use self::test_utils::User;
use super::free_segments_ledger::FreeSegment;
use super::table_reader::NextRecord;
use super::*;
use crate::{HeapMemoryProvider, MemoryManager};
#[test]
fn test_should_create_table_registry() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let schema_snapshot_page = mm.claim_page().expect("failed to get page");
let page_ledger_page = mm.claim_page().expect("failed to get page");
let free_segments_page = mm.claim_page().expect("failed to get page");
let index_registry_page = mm.claim_page().expect("failed to get page");
let autoincrement_page = mm.claim_page().expect("failed to get page");
super::test_utils::write_dummy_schema_snapshot(schema_snapshot_page, &mut mm);
let table_pages = TableRegistryPage {
schema_snapshot_page,
pages_list_page: page_ledger_page,
free_segments_page,
index_registry_page,
autoincrement_registry_page: Some(autoincrement_page),
};
let registry: MemoryResult<TableRegistry> = TableRegistry::load(table_pages, &mut mm);
assert!(registry.is_ok());
}
#[test]
fn test_should_get_write_at_end() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let record = RawRecord::new(User {
id: 1,
name: "Test".to_string(),
email: "new_user@example.com".to_string(),
age: 25,
});
let write_at = registry
.get_write_position(&record, &mut mm)
.expect("failed to get write at");
assert!(matches!(write_at, WriteAt::End(_, 0)));
}
#[test]
fn test_should_get_write_at_free_segment() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let record = RawRecord::new(User {
id: 1,
name: "Test".to_string(),
email: "new_user@example.com".to_string(),
age: 25,
});
let (page, _) = registry
.page_ledger
.get_page_and_offset_for_record(&record, &mut mm)
.expect("failed to get page and offset");
registry
.page_ledger
.commit(page, &record, &mut mm)
.expect("failed to commit page ledger");
registry
.free_segments_ledger
.insert_free_segment(page, 256, &record, &mut mm)
.expect("failed to insert free segment");
let write_at = registry
.get_write_position(&record, &mut mm)
.expect("failed to get write at");
let reused_segment = match write_at {
WriteAt::ReusedSegment(segment) => segment.segment,
_ => panic!("expected reused segment"),
};
assert_eq!(
reused_segment,
FreeSegment {
page,
offset: 256,
size: 64, }
);
}
#[test]
fn test_should_insert_record_into_table_registry() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let record = User {
id: 1,
name: "Test".to_string(),
email: "new_user@example.com".to_string(),
age: 25,
};
assert!(registry.insert(record, &mut mm).is_ok());
}
#[test]
fn test_should_manage_to_insert_users_to_exceed_one_page() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
for id in 0..4000 {
let record = User {
id,
name: format!("User {}", id),
email: "new_user@example.com".to_string(),
age: 20 + id,
};
registry
.insert(record, &mut mm)
.expect("failed to insert record");
}
}
#[test]
fn test_should_delete_record() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let record = User {
id: 1,
name: "Test".to_string(),
email: "new_user@example.com".to_string(),
age: 25,
};
registry
.insert(record.clone(), &mut mm)
.expect("failed to insert");
let mut reader = registry.read(&mut mm);
let next_record: NextRecord<User> = reader
.try_next()
.expect("failed to read")
.expect("no record");
let page = next_record.page;
let offset = next_record.offset;
let record = next_record.record;
let raw_user = RawRecord::new(record.clone());
let raw_user_size = raw_user.size();
assert!(
registry
.delete(record, RecordAddress { page, offset }, &mut mm)
.is_ok()
);
let mut reader = registry.read::<User, _>(&mut mm);
assert!(reader.try_next().expect("failed to read").is_none());
let free_segment = registry
.free_segments_ledger
.find_reusable_segment(
&User {
id: 2,
name: "Test".to_string(),
email: "new_user@example.com".to_string(),
age: 25,
},
&mut mm,
)
.expect("failed to find free segment")
.expect("could not find the free segment after free")
.segment;
assert_eq!(free_segment.page, page);
assert_eq!(free_segment.offset, offset);
assert_eq!(free_segment.size, 64);
let mut buffer = vec![0u8; raw_user_size as usize];
mm.read_at_raw(page, offset, &mut buffer)
.expect("failed to read memory");
assert!(buffer.iter().all(|&b| b == 0));
}
#[test]
fn test_read_at_returns_record_at_address() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let record = User {
id: 1,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
age: 30,
};
let address = registry
.insert(record.clone(), &mut mm)
.expect("failed to insert record");
let stored: User = registry
.read_at(address, &mut mm)
.expect("failed to read record");
assert_eq!(stored, record);
}
#[test]
fn test_read_at_rejects_record_length_past_page_end() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let address = registry
.insert(
User {
id: 1,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
age: 30,
},
&mut mm,
)
.expect("failed to insert record");
mm.write_at_raw(address.page, address.offset, &MSize::MAX.to_le_bytes())
.expect("failed to corrupt header");
let result: MemoryResult<User> = registry.read_at(address, &mut mm);
assert!(
matches!(result, Err(MemoryError::DecodeError(DecodeError::TooShort))),
"expected TooShort, got {result:?}"
);
}
#[test]
fn test_read_at_rejects_misaligned_address() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let address = registry
.insert(
User {
id: 1,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
age: 30,
},
&mut mm,
)
.expect("failed to insert record");
let misaligned = RecordAddress {
page: address.page,
offset: address.offset + 1,
};
let result: MemoryResult<User> = registry.read_at(misaligned, &mut mm);
assert!(
matches!(result, Err(MemoryError::OffsetNotAligned { .. })),
"expected OffsetNotAligned, got {result:?}"
);
}
#[test]
fn test_schema_snapshot_ledger_is_read_on_demand() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let registry = registry_with_autoincrement(&mut mm);
let ledger = registry
.schema_snapshot_ledger(&mut mm)
.expect("failed to load snapshot ledger");
assert_eq!(ledger.get(), &AutoincUser::schema_snapshot());
}
#[test]
fn test_read_at_after_update_returns_updated_record() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let old_record = User {
id: 1,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
age: 30,
};
let new_record = User {
id: 1,
name: "Alice Updated".to_string(),
email: "alice.updated@example.com".to_string(),
age: 31,
};
let old_address = registry
.insert(old_record.clone(), &mut mm)
.expect("failed to insert record");
let new_address = registry
.update(new_record.clone(), old_record, old_address, &mut mm)
.expect("failed to update record");
let stored: User = registry
.read_at(new_address, &mut mm)
.expect("failed to read updated record");
assert_eq!(stored, new_record);
}
#[test]
fn test_should_update_record_in_place() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let old_record = User {
id: 1,
name: "John".to_string(),
email: "new_user@example.com".to_string(),
age: 28,
};
let new_record = User {
id: 1,
name: "Mark".to_string(), email: "new_user@example.com".to_string(),
age: 30,
};
registry
.insert(old_record.clone(), &mut mm)
.expect("failed to insert");
let mut reader = registry.read::<User, _>(&mut mm);
let next_record = reader
.try_next()
.expect("failed to read")
.expect("no record");
let page = next_record.page;
let offset = next_record.offset;
let old_address = RecordAddress { page, offset };
let new_location = registry
.update(
new_record.clone(),
next_record.record.clone(),
old_address,
&mut mm,
)
.expect("failed to update record");
assert_eq!(new_location, old_address);
let mut reader = registry.read::<User, _>(&mut mm);
let next_record = reader
.try_next()
.expect("failed to read")
.expect("no record");
assert_eq!(next_record.page, page); assert_eq!(next_record.offset, offset); assert_eq!(next_record.record, new_record);
}
#[test]
fn test_should_update_record_reallocating() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let old_record = User {
id: 1,
name: "John".to_string(),
email: "new_user@example.com".to_string(),
age: 28,
};
let extra_user = User {
id: 2,
name: "Extra".to_string(),
email: "new_user@example.com".to_string(),
age: 25,
};
let new_record = User {
id: 1,
name: "Alexanderejruwgjowergjioewrgjioewrigjewriogjweoirgjiowerjgoiwerjiogewirogjowejrgiwer".to_string(), email: "new_user@example.com".to_string(),
age: 30,
};
registry
.insert(old_record.clone(), &mut mm)
.expect("failed to insert");
registry
.insert(extra_user.clone(), &mut mm)
.expect("failed to insert extra user");
let mut reader = registry.read::<User, _>(&mut mm);
let old_record_from_db = reader
.try_next()
.expect("failed to read")
.expect("no record");
assert_eq!(old_record_from_db.record, old_record);
let page = old_record_from_db.page;
let offset = old_record_from_db.offset;
let old_address = RecordAddress { page, offset };
let new_location = registry
.update(
new_record.clone(),
old_record_from_db.record.clone(),
old_address,
&mut mm,
)
.expect("failed to update record");
assert_ne!(new_location, old_address);
let mut reader = registry.read::<User, _>(&mut mm);
let _ = reader
.try_next()
.expect("failed to read")
.expect("no record");
let updated_record = reader
.try_next()
.expect("failed to read")
.expect("no record");
assert_ne!(updated_record.offset, offset); assert_eq!(updated_record.record, new_record);
}
#[test]
fn test_should_insert_delete_insert_many() {
const COUNT: u32 = 1_000;
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
for id in 0..COUNT {
let record = User {
id,
name: format!("User {id}"),
email: format!("user_{id}@example.com"),
age: 20,
};
registry
.insert(record.clone(), &mut mm)
.expect("failed to insert");
}
for id in (0..COUNT).filter(|id| id % 2 == 1) {
let record = User {
id,
name: format!("User {id}"),
email: format!("user_{id}@example.com"),
age: 20,
};
let mut reader = registry.read::<User, _>(&mut mm);
let mut deleted = false;
while let Some(next_record) = reader.try_next().expect("failed to read") {
if next_record.record.id == id {
registry
.delete(
record.clone(),
RecordAddress {
page: next_record.page,
offset: next_record.offset,
},
&mut mm,
)
.expect("failed to delete");
deleted = true;
break;
}
}
assert!(deleted, "record with id {} was not found", id);
}
for id in (0..COUNT).filter(|id| id % 2 == 0) {
let record = User {
id,
name: format!("User {id}"),
email: format!("user_{id}@example.com"),
age: 20,
};
let mut reader = registry.read::<User, _>(&mut mm);
let mut deleted = false;
while let Some(next_record) = reader.try_next().expect("failed to read") {
if next_record.record.id == id {
registry
.delete(
record.clone(),
RecordAddress {
page: next_record.page,
offset: next_record.offset,
},
&mut mm,
)
.expect("failed to delete");
deleted = true;
break;
}
}
assert!(deleted, "record with id {} was not found", id);
}
for id in 0..COUNT {
let record = User {
id,
name: format!("User {id}"),
email: format!("user_{id}@example.com"),
age: 20,
};
registry
.insert(record.clone(), &mut mm)
.expect("failed to insert");
}
}
#[test]
fn test_should_reduce_free_segment_size_with_padding() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let long_name = vec!['A'; 1024].into_iter().collect::<String>();
let record = User {
id: 1,
name: "Test User".to_string(),
email: long_name,
age: 30,
};
registry
.insert(record.clone(), &mut mm)
.expect("failed to insert");
let mut reader = registry.read::<User, _>(&mut mm);
let next_record = reader
.try_next()
.expect("failed to read")
.expect("no record");
registry
.delete(
next_record.record,
RecordAddress {
page: next_record.page,
offset: next_record.offset,
},
&mut mm,
)
.expect("failed to delete");
let raw_record = RawRecord::new(record.clone());
let free_segment = registry
.free_segments_ledger
.find_reusable_segment(&raw_record, &mut mm)
.expect("failed to find reusable segment")
.expect("could not find the free segment after free")
.segment;
assert!(free_segment.size >= 1024);
let previous_size = free_segment.size;
let small_record = User {
id: 2,
name: "Bob The Builder".to_string(),
email: "bob@hotmail.com".to_string(),
age: 22,
};
registry
.insert(small_record.clone(), &mut mm)
.expect("failed to insert small user");
let free_segment_after = registry
.free_segments_ledger
.find_reusable_segment(&small_record, &mut mm)
.expect("failed to find reusable segment")
.expect("could not find the free segment after inserting small user")
.segment;
assert_eq!(
free_segment_after.offset, 64,
"expected offset to be 64, but had: {}",
free_segment_after.offset
); assert_eq!(
free_segment_after.size,
previous_size - 64,
"Expected free segment to have size: {} but got: {}",
previous_size - 64,
free_segment_after.size
);
}
#[test]
fn test_raw_reuse_preserves_the_next_live_record() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let deleted = registry
.insert_raw(&[1], 32, &mut mm)
.expect("insert deleted");
let live = registry
.insert_raw(&[2; 10], 32, &mut mm)
.expect("insert live");
registry
.delete_raw(deleted, 1, 32, &mut mm)
.expect("delete");
registry
.insert_raw(&[9; 31], 32, &mut mm)
.expect("insert replacement");
assert_eq!(
registry.read_raw_at(live, &mut mm).expect("read live"),
vec![2; 10]
);
}
#[test]
fn test_raw_reuse_selects_segment_when_full_footprint_fits() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let deleted = registry
.insert_raw(&[1; 30], 32, &mut mm)
.expect("insert deleted");
let live = registry
.insert_raw(&[2; 10], 32, &mut mm)
.expect("insert live");
registry
.delete_raw(deleted, 30, 32, &mut mm)
.expect("delete");
let reused = registry
.insert_raw(&[9; 30], 32, &mut mm)
.expect("insert replacement");
assert_eq!(reused, deleted);
assert_eq!(
registry.read_raw_at(live, &mut mm).expect("read live"),
vec![2; 10]
);
}
fn registry(mm: &mut MemoryManager<HeapMemoryProvider>) -> TableRegistry {
let schema_snapshot_page = mm.claim_page().expect("failed to get page");
let page_ledger_page = mm.claim_page().expect("failed to get page");
let free_segments_page = mm.claim_page().expect("failed to get page");
let index_registry_page = mm.claim_page().expect("failed to get page");
let autoincrement_page = mm.claim_page().expect("failed to get page");
super::test_utils::write_dummy_schema_snapshot(schema_snapshot_page, mm);
let table_pages = TableRegistryPage {
schema_snapshot_page,
pages_list_page: page_ledger_page,
free_segments_page,
index_registry_page,
autoincrement_registry_page: Some(autoincrement_page),
};
TableRegistry::load(table_pages, mm).expect("failed to load")
}
fn registry_with_autoincrement(mm: &mut MemoryManager<HeapMemoryProvider>) -> TableRegistry {
use crate::SchemaRegistry;
let mut schema = SchemaRegistry::load(mm).expect("failed to load schema");
let pages = schema
.register_table::<AutoincUser>(mm)
.expect("failed to register table");
TableRegistry::load(pages, mm).expect("failed to load")
}
fn registry_without_autoincrement(mm: &mut MemoryManager<HeapMemoryProvider>) -> TableRegistry {
let schema_snapshot_page = mm.claim_page().expect("failed to get page");
let page_ledger_page = mm.claim_page().expect("failed to get page");
let free_segments_page = mm.claim_page().expect("failed to get page");
let index_registry_page = mm.claim_page().expect("failed to get page");
super::test_utils::write_dummy_schema_snapshot(schema_snapshot_page, mm);
let table_pages = TableRegistryPage {
schema_snapshot_page,
pages_list_page: page_ledger_page,
free_segments_page,
index_registry_page,
autoincrement_registry_page: None,
};
TableRegistry::load(table_pages, mm).expect("failed to load")
}
use candid::CandidType;
use serde::{Deserialize, Serialize};
use wasm_dbms_api::prelude::{
ColumnDef, DbmsResult, IndexDef, InsertRecord, NoForeignFetcher, TableColumns, TableRecord,
TableSchema, UpdateRecord,
};
#[derive(Clone, CandidType)]
struct AutoincUser;
impl Encode for AutoincUser {
const SIZE: wasm_dbms_api::prelude::DataSize = wasm_dbms_api::prelude::DataSize::Dynamic;
const ALIGNMENT: PageOffset = wasm_dbms_api::prelude::DEFAULT_ALIGNMENT;
fn encode(&'_ self) -> std::borrow::Cow<'_, [u8]> {
std::borrow::Cow::Owned(vec![])
}
fn decode(_data: std::borrow::Cow<[u8]>) -> MemoryResult<Self>
where
Self: Sized,
{
Ok(Self)
}
fn size(&self) -> wasm_dbms_api::prelude::MSize {
0
}
}
#[derive(Clone, CandidType, Deserialize)]
struct AutoincUserRecord;
impl TableRecord for AutoincUserRecord {
type Schema = AutoincUser;
fn from_values(_values: TableColumns) -> Self {
Self
}
fn to_values(&self) -> Vec<(ColumnDef, Value)> {
vec![]
}
}
#[derive(Clone, CandidType, Serialize)]
struct AutoincUserInsert;
impl InsertRecord for AutoincUserInsert {
type Record = AutoincUserRecord;
type Schema = AutoincUser;
fn from_values(_values: &[(ColumnDef, Value)]) -> DbmsResult<Self> {
Ok(Self)
}
fn into_values(self) -> Vec<(ColumnDef, Value)> {
vec![]
}
fn into_record(self) -> Self::Schema {
AutoincUser
}
}
#[derive(Clone, CandidType, Serialize)]
struct AutoincUserUpdate;
impl UpdateRecord for AutoincUserUpdate {
type Record = AutoincUserRecord;
type Schema = AutoincUser;
fn from_values(
_values: &[(ColumnDef, Value)],
_where_clause: Option<wasm_dbms_api::prelude::Filter>,
) -> wasm_dbms_api::prelude::DbmsResult<Self> {
Ok(Self)
}
fn update_values(&self) -> Vec<(ColumnDef, Value)> {
vec![]
}
fn where_clause(&self) -> Option<wasm_dbms_api::prelude::Filter> {
None
}
}
impl TableSchema for AutoincUser {
type Record = AutoincUserRecord;
type Insert = AutoincUserInsert;
type Update = AutoincUserUpdate;
type ForeignFetcher = NoForeignFetcher;
fn table_name() -> &'static str {
"autoinc_users"
}
fn columns() -> &'static [ColumnDef] {
use wasm_dbms_api::prelude::DataTypeKind;
&[ColumnDef {
name: "id",
data_type: DataTypeKind::Uint32,
auto_increment: true,
nullable: false,
primary_key: true,
unique: true,
foreign_key: None,
default: None,
renamed_from: &[],
}]
}
fn primary_key() -> &'static str {
"id"
}
fn indexes() -> &'static [IndexDef] {
&[IndexDef(&["id"])]
}
fn to_values(self) -> Vec<(ColumnDef, Value)> {
vec![]
}
fn sanitizer(
_column_name: &'static str,
) -> Option<Box<dyn wasm_dbms_api::prelude::Sanitize>> {
None
}
fn validator(
_column_name: &'static str,
) -> Option<Box<dyn wasm_dbms_api::prelude::Validate>> {
None
}
}
#[test]
fn test_next_autoincrement_returns_sequential_values() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry_with_autoincrement(&mut mm);
let v1 = registry
.next_autoincrement("id", &mut mm)
.expect("failed")
.expect("expected Some");
let v2 = registry
.next_autoincrement("id", &mut mm)
.expect("failed")
.expect("expected Some");
let v3 = registry
.next_autoincrement("id", &mut mm)
.expect("failed")
.expect("expected Some");
assert_eq!(v1, Value::Uint32(1u32.into()));
assert_eq!(v2, Value::Uint32(2u32.into()));
assert_eq!(v3, Value::Uint32(3u32.into()));
}
#[test]
fn test_next_autoincrement_returns_none_without_ledger() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry_without_autoincrement(&mut mm);
let result = registry.next_autoincrement("id", &mut mm).expect("failed");
assert!(result.is_none());
}
#[test]
fn test_next_autoincrement_persists_across_reload() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
use crate::SchemaRegistry;
let mut schema = SchemaRegistry::load(&mut mm).expect("failed to load schema");
let pages = schema
.register_table::<AutoincUser>(&mut mm)
.expect("failed to register table");
let mut registry = TableRegistry::load(pages, &mut mm).expect("failed to load");
for _ in 0..5 {
let _ = registry
.next_autoincrement("id", &mut mm)
.expect("next failed");
}
let mut reloaded = TableRegistry::load(pages, &mut mm).expect("failed to reload");
let value = reloaded
.next_autoincrement("id", &mut mm)
.expect("failed")
.expect("expected Some");
assert_eq!(value, Value::Uint32(6u32.into()));
}
#[test]
fn test_next_autoincrement_overflow_returns_error() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let schema_snapshot_page = mm.claim_page().expect("failed to get page");
let page_ledger_page = mm.claim_page().expect("failed to get page");
let free_segments_page = mm.claim_page().expect("failed to get page");
let index_registry_page = mm.claim_page().expect("failed to get page");
let autoinc_page = mm.claim_page().expect("failed to get page");
{
let mut registry_data = super::autoincrement_ledger::AutoincrementLedger::init::<
Uint8AutoincSchema,
>(autoinc_page, &mut mm)
.expect("failed to init autoinc ledger");
for _ in 0..255 {
let _ = registry_data.next("val", &mut mm).expect("next failed");
}
}
IndexLedger::init(index_registry_page, &[], &mut mm).expect("failed to init index ledger");
super::test_utils::write_dummy_schema_snapshot(schema_snapshot_page, &mut mm);
let table_pages = TableRegistryPage {
schema_snapshot_page,
pages_list_page: page_ledger_page,
free_segments_page,
index_registry_page,
autoincrement_registry_page: Some(autoinc_page),
};
let mut registry = TableRegistry::load(table_pages, &mut mm).expect("failed to load");
let result = registry.next_autoincrement("val", &mut mm);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
wasm_dbms_api::prelude::MemoryError::AutoincrementOverflow(_)
));
}
#[derive(Clone, CandidType)]
struct Uint8AutoincSchema;
impl Encode for Uint8AutoincSchema {
const SIZE: wasm_dbms_api::prelude::DataSize = wasm_dbms_api::prelude::DataSize::Dynamic;
const ALIGNMENT: PageOffset = wasm_dbms_api::prelude::DEFAULT_ALIGNMENT;
fn encode(&'_ self) -> std::borrow::Cow<'_, [u8]> {
std::borrow::Cow::Owned(vec![])
}
fn decode(_data: std::borrow::Cow<[u8]>) -> MemoryResult<Self>
where
Self: Sized,
{
Ok(Self)
}
fn size(&self) -> wasm_dbms_api::prelude::MSize {
0
}
}
#[derive(Clone, CandidType, Deserialize)]
struct Uint8AutoincSchemaRecord;
impl TableRecord for Uint8AutoincSchemaRecord {
type Schema = Uint8AutoincSchema;
fn from_values(_values: TableColumns) -> Self {
Self
}
fn to_values(&self) -> Vec<(ColumnDef, Value)> {
vec![]
}
}
#[derive(Clone, CandidType, Serialize)]
struct Uint8AutoincSchemaInsert;
impl InsertRecord for Uint8AutoincSchemaInsert {
type Record = Uint8AutoincSchemaRecord;
type Schema = Uint8AutoincSchema;
fn from_values(_values: &[(ColumnDef, Value)]) -> DbmsResult<Self> {
Ok(Self)
}
fn into_values(self) -> Vec<(ColumnDef, Value)> {
vec![]
}
fn into_record(self) -> Self::Schema {
Uint8AutoincSchema
}
}
#[derive(Clone, CandidType, Serialize)]
struct Uint8AutoincSchemaUpdate;
impl UpdateRecord for Uint8AutoincSchemaUpdate {
type Record = Uint8AutoincSchemaRecord;
type Schema = Uint8AutoincSchema;
fn from_values(
_values: &[(ColumnDef, Value)],
_where_clause: Option<wasm_dbms_api::prelude::Filter>,
) -> wasm_dbms_api::prelude::DbmsResult<Self> {
Ok(Self)
}
fn update_values(&self) -> Vec<(ColumnDef, Value)> {
vec![]
}
fn where_clause(&self) -> Option<wasm_dbms_api::prelude::Filter> {
None
}
}
impl TableSchema for Uint8AutoincSchema {
type Record = Uint8AutoincSchemaRecord;
type Insert = Uint8AutoincSchemaInsert;
type Update = Uint8AutoincSchemaUpdate;
type ForeignFetcher = NoForeignFetcher;
fn table_name() -> &'static str {
"uint8_autoinc_schema"
}
fn columns() -> &'static [ColumnDef] {
use wasm_dbms_api::prelude::DataTypeKind;
&[ColumnDef {
name: "val",
data_type: DataTypeKind::Uint8,
auto_increment: true,
nullable: false,
primary_key: true,
unique: true,
foreign_key: None,
default: None,
renamed_from: &[],
}]
}
fn primary_key() -> &'static str {
"val"
}
fn indexes() -> &'static [IndexDef] {
&[IndexDef(&["val"])]
}
fn to_values(self) -> Vec<(ColumnDef, Value)> {
vec![]
}
fn sanitizer(
_column_name: &'static str,
) -> Option<Box<dyn wasm_dbms_api::prelude::Sanitize>> {
None
}
fn validator(
_column_name: &'static str,
) -> Option<Box<dyn wasm_dbms_api::prelude::Validate>> {
None
}
}
use wasm_dbms_api::prelude::{DataSize, DecodeError, MSize, MemoryError};
#[derive(Debug, Clone, PartialEq, Eq)]
struct FixedSizeRecord {
id: u64,
timestamp: u64,
tag: u8,
}
impl Encode for FixedSizeRecord {
const SIZE: DataSize = DataSize::Fixed(17);
const ALIGNMENT: PageOffset = 17;
fn encode(&'_ self) -> std::borrow::Cow<'_, [u8]> {
let mut buf = Vec::with_capacity(17);
buf.extend_from_slice(&self.id.to_le_bytes());
buf.extend_from_slice(&self.timestamp.to_le_bytes());
buf.push(self.tag);
std::borrow::Cow::Owned(buf)
}
fn decode(data: std::borrow::Cow<[u8]>) -> MemoryResult<Self>
where
Self: Sized,
{
if data.len() < 17 {
return Err(MemoryError::DecodeError(DecodeError::TooShort));
}
let id = u64::from_le_bytes(data[0..8].try_into().unwrap());
let timestamp = u64::from_le_bytes(data[8..16].try_into().unwrap());
let tag = data[16];
Ok(Self { id, timestamp, tag })
}
fn size(&self) -> MSize {
17
}
}
#[test]
fn test_should_insert_multiple_fixed_size_records() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
for i in 0..10 {
let record = FixedSizeRecord {
id: i,
timestamp: 1000 + i,
tag: (i % 2) as u8,
};
registry
.insert(record, &mut mm)
.unwrap_or_else(|e| panic!("failed to insert record {i}: {e}"));
}
let mut reader = registry.read::<FixedSizeRecord, _>(&mut mm);
let mut count = 0;
while let Some(next) = reader.try_next().expect("failed to read") {
assert_eq!(next.record.id, count);
count += 1;
}
assert_eq!(count, 10);
}
#[test]
fn test_reader_skips_deleted_fixed_size_record() {
use wasm_dbms_api::prelude::Uint32;
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let first = registry.insert(Uint32(11), &mut mm).expect("insert 11");
registry.insert(Uint32(22), &mut mm).expect("insert 22");
registry
.delete(Uint32(11), first, &mut mm)
.expect("delete 11");
let mut reader = registry.read::<Uint32, _>(&mut mm);
let mut rows = Vec::new();
while let Some(next) = reader.try_next().expect("scan") {
rows.push(next.record);
}
assert_eq!(rows, vec![Uint32(22)]);
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct TenAlignedRecord(u32);
impl Encode for TenAlignedRecord {
const SIZE: DataSize = DataSize::Dynamic;
const ALIGNMENT: PageOffset = 10;
fn encode(&'_ self) -> std::borrow::Cow<'_, [u8]> {
std::borrow::Cow::Owned(self.0.to_le_bytes().to_vec())
}
fn decode(data: std::borrow::Cow<[u8]>) -> MemoryResult<Self>
where
Self: Sized,
{
if data.len() < 4 {
return Err(MemoryError::DecodeError(DecodeError::TooShort));
}
Ok(Self(u32::from_le_bytes(data[0..4].try_into().unwrap())))
}
fn size(&self) -> MSize {
4
}
}
#[test]
fn test_insert_moves_to_new_page_when_padding_would_cross_page_end() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
const COUNT: u32 = 6_554;
for id in 0..COUNT {
registry
.insert(TenAlignedRecord(id), &mut mm)
.unwrap_or_else(|e| panic!("failed to insert record {id}: {e}"));
}
assert_eq!(registry.page_ledger.pages().len(), 2);
let mut reader = registry.read::<TenAlignedRecord, _>(&mut mm);
let mut count = 0;
while let Some(next) = reader.try_next().expect("scan") {
assert_eq!(next.record, TenAlignedRecord(count));
count += 1;
}
assert_eq!(count, COUNT);
}
#[test]
fn test_insert_raw_rejects_footprint_of_a_full_page() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let result = registry.insert_raw(&vec![0u8; 65_503], 32, &mut mm);
assert!(matches!(
result,
Err(MemoryError::DataTooLarge {
requested: 65_536,
..
})
));
}
#[test]
fn test_insert_raw_accepts_largest_representable_footprint() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let payload = vec![7u8; 65_502];
let address = registry
.insert_raw(&payload, 32, &mut mm)
.expect("65_504-byte footprint fits");
assert_eq!(
registry.read_raw_at(address, &mut mm).expect("read back"),
payload
);
}
#[test]
fn test_delete_raw_rejects_unrepresentable_footprint() {
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let result = registry.delete_raw(RecordAddress::new(0, 0), MSize::MAX, 32, &mut mm);
assert!(matches!(result, Err(MemoryError::DataTooLarge { .. })));
}
#[test]
fn test_insert_rejects_record_that_does_not_fit_a_page() {
use wasm_dbms_api::prelude::Text;
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
for len in [65_533, 65_534, 65_536, 70_000] {
let result = registry.insert(Text("x".repeat(len)), &mut mm);
assert!(
matches!(result, Err(MemoryError::DataTooLarge { .. })),
"{len}-byte text: expected DataTooLarge, got {result:?}"
);
}
assert!(registry.page_ledger.pages().is_empty());
}
#[test]
fn test_insert_rejects_footprint_of_a_full_page() {
use wasm_dbms_api::prelude::Text;
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let result = registry.insert(Text("x".repeat(65_501)), &mut mm);
assert!(matches!(
result,
Err(MemoryError::DataTooLarge {
requested: 65_536,
..
})
));
let text = Text("x".repeat(65_500));
let address = registry
.insert(text.clone(), &mut mm)
.expect("65_504-byte footprint fits");
let read: Text = registry.read_at(address, &mut mm).expect("read back");
assert_eq!(read, text);
}
#[test]
fn test_update_rejects_record_that_does_not_fit_a_page() {
use wasm_dbms_api::prelude::Text;
let mut mm = MemoryManager::init(HeapMemoryProvider::default());
let mut registry = registry(&mut mm);
let old = Text("small".to_string());
let address = registry.insert(old.clone(), &mut mm).expect("insert");
let result = registry.update(Text("x".repeat(70_000)), old.clone(), address, &mut mm);
assert!(
matches!(result, Err(MemoryError::DataTooLarge { .. })),
"expected DataTooLarge, got {result:?}"
);
let read: Text = registry.read_at(address, &mut mm).expect("read back");
assert_eq!(read, old);
}
}