pub mod buffer_pool;
pub mod page_manager;
pub const PAGE_SIZE: usize = 4096;
pub const MAGIC: u32 = 0x4359_4C54;
#[cfg(feature = "hypergraph")]
pub const FORMAT_VERSION: u32 = 5;
#[cfg(all(feature = "subgraph", not(feature = "hypergraph")))]
pub const FORMAT_VERSION: u32 = 4;
#[cfg(not(feature = "subgraph"))]
pub const FORMAT_VERSION: u32 = 3;
pub const HEADER_PAGE_ID: u32 = 0;
pub const FSM_PAGE_ID: u32 = 1;
pub const FIRST_DATA_PAGE: u32 = 2;
pub const FSM_MAX_PAGES: u32 = PAGE_SIZE as u32 * 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum PageType {
Header = 0,
FreeSpaceMap = 1,
BTreeInterior = 2,
BTreeLeaf = 3,
Overflow = 4,
Data = 5,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PageHeader {
pub page_type: u8,
pub flags: u8,
pub free_start: u16,
pub free_end: u16,
pub overflow_page: u32,
pub item_count: u16,
pub _reserved: [u8; 20],
}
impl PageHeader {
pub const SIZE: usize = 32;
pub fn new(page_type: PageType) -> Self {
Self {
page_type: page_type as u8,
flags: 0,
free_start: Self::SIZE as u16,
free_end: PAGE_SIZE as u16,
overflow_page: 0,
item_count: 0,
_reserved: [0u8; 20],
}
}
pub fn write_to(&self, buf: &mut [u8]) {
debug_assert!(buf.len() >= Self::SIZE);
buf[0] = self.page_type;
buf[1] = self.flags;
buf[2..4].copy_from_slice(&self.free_start.to_le_bytes());
buf[4..6].copy_from_slice(&self.free_end.to_le_bytes());
buf[6..10].copy_from_slice(&self.overflow_page.to_le_bytes());
buf[10..12].copy_from_slice(&self.item_count.to_le_bytes());
buf[12..32].copy_from_slice(&self._reserved);
}
pub fn read_from(buf: &[u8]) -> Self {
debug_assert!(buf.len() >= Self::SIZE);
let mut reserved = [0u8; 20];
reserved.copy_from_slice(&buf[12..32]);
Self {
page_type: buf[0],
flags: buf[1],
free_start: u16::from_le_bytes([buf[2], buf[3]]),
free_end: u16::from_le_bytes([buf[4], buf[5]]),
overflow_page: u32::from_le_bytes([buf[6], buf[7], buf[8], buf[9]]),
item_count: u16::from_le_bytes([buf[10], buf[11]]),
_reserved: reserved,
}
}
}
#[derive(Debug, Clone)]
pub struct DatabaseHeader {
pub magic: u32,
pub version: u32,
pub page_count: u32,
pub root_node_page: u32,
pub root_edge_page: u32,
pub next_node_id: u64,
pub next_edge_id: u64,
pub version_store_root_page: u64,
pub feature_flags: u32,
#[cfg(feature = "subgraph")]
pub subgraph_root_page: u64,
#[cfg(feature = "subgraph")]
pub next_subgraph_id: u64,
#[cfg(feature = "hypergraph")]
pub hyperedge_root_page: u64,
#[cfg(feature = "hypergraph")]
pub next_hyperedge_id: u64,
}
impl DatabaseHeader {
pub const FLAG_TEMPORAL_CORE: u32 = 1 << 0;
pub const FLAG_TEMPORAL_EDGE: u32 = 1 << 1;
#[cfg(feature = "subgraph")]
pub const FLAG_SUBGRAPH: u32 = 1 << 2;
#[cfg(feature = "hypergraph")]
pub const FLAG_HYPERGRAPH: u32 = 1 << 3;
pub fn new() -> Self {
Self {
magic: MAGIC,
version: FORMAT_VERSION,
page_count: FIRST_DATA_PAGE,
root_node_page: 0,
root_edge_page: 0,
next_node_id: 1,
next_edge_id: 1,
version_store_root_page: 0,
feature_flags: Self::compiled_feature_flags(),
#[cfg(feature = "subgraph")]
subgraph_root_page: 0,
#[cfg(feature = "subgraph")]
next_subgraph_id: 1,
#[cfg(feature = "hypergraph")]
hyperedge_root_page: 0,
#[cfg(feature = "hypergraph")]
next_hyperedge_id: 1,
}
}
pub fn compiled_feature_flags() -> u32 {
let mut flags = 0u32;
flags |= Self::FLAG_TEMPORAL_CORE;
#[cfg(feature = "temporal-edge")]
{
flags |= Self::FLAG_TEMPORAL_EDGE;
}
#[cfg(feature = "subgraph")]
{
flags |= Self::FLAG_SUBGRAPH;
}
#[cfg(feature = "hypergraph")]
{
flags |= Self::FLAG_HYPERGRAPH;
}
flags
}
pub fn to_page(&self) -> [u8; PAGE_SIZE] {
let mut page = [0u8; PAGE_SIZE];
page[0..4].copy_from_slice(&self.magic.to_le_bytes());
page[4..8].copy_from_slice(&self.version.to_le_bytes());
page[8..12].copy_from_slice(&self.page_count.to_le_bytes());
page[12..16].copy_from_slice(&self.root_node_page.to_le_bytes());
page[16..20].copy_from_slice(&self.root_edge_page.to_le_bytes());
page[20..28].copy_from_slice(&self.next_node_id.to_le_bytes());
page[28..36].copy_from_slice(&self.next_edge_id.to_le_bytes());
page[36..44].copy_from_slice(&self.version_store_root_page.to_le_bytes());
page[44..48].copy_from_slice(&self.feature_flags.to_le_bytes());
#[cfg(feature = "subgraph")]
{
page[48..56].copy_from_slice(&self.subgraph_root_page.to_le_bytes());
page[56..64].copy_from_slice(&self.next_subgraph_id.to_le_bytes());
}
#[cfg(feature = "hypergraph")]
{
page[64..72].copy_from_slice(&self.hyperedge_root_page.to_le_bytes());
page[72..80].copy_from_slice(&self.next_hyperedge_id.to_le_bytes());
}
page
}
pub fn from_page(page: &[u8; PAGE_SIZE]) -> Self {
let version = u32::from_le_bytes([page[4], page[5], page[6], page[7]]);
let version_store_root_page = if version >= 2 {
u64::from_le_bytes([
page[36], page[37], page[38], page[39], page[40], page[41], page[42], page[43],
])
} else {
0 };
let feature_flags = if version >= 3 {
u32::from_le_bytes([page[44], page[45], page[46], page[47]])
} else {
Self::FLAG_TEMPORAL_CORE
};
#[cfg(feature = "subgraph")]
let subgraph_root_page = if version >= 4 {
u64::from_le_bytes([
page[48], page[49], page[50], page[51], page[52], page[53], page[54], page[55],
])
} else {
0 };
#[cfg(feature = "subgraph")]
let next_subgraph_id = if version >= 4 {
u64::from_le_bytes([
page[56], page[57], page[58], page[59], page[60], page[61], page[62], page[63],
])
} else {
0 };
#[cfg(feature = "hypergraph")]
let hyperedge_root_page = if version >= 5 {
u64::from_le_bytes([
page[64], page[65], page[66], page[67], page[68], page[69], page[70], page[71],
])
} else {
0 };
#[cfg(feature = "hypergraph")]
let next_hyperedge_id = if version >= 5 {
u64::from_le_bytes([
page[72], page[73], page[74], page[75], page[76], page[77], page[78], page[79],
])
} else {
0 };
Self {
magic: u32::from_le_bytes([page[0], page[1], page[2], page[3]]),
version,
page_count: u32::from_le_bytes([page[8], page[9], page[10], page[11]]),
root_node_page: u32::from_le_bytes([page[12], page[13], page[14], page[15]]),
root_edge_page: u32::from_le_bytes([page[16], page[17], page[18], page[19]]),
next_node_id: u64::from_le_bytes([
page[20], page[21], page[22], page[23], page[24], page[25], page[26], page[27],
]),
next_edge_id: u64::from_le_bytes([
page[28], page[29], page[30], page[31], page[32], page[33], page[34], page[35],
]),
version_store_root_page,
feature_flags,
#[cfg(feature = "subgraph")]
subgraph_root_page,
#[cfg(feature = "subgraph")]
next_subgraph_id,
#[cfg(feature = "hypergraph")]
hyperedge_root_page,
#[cfg(feature = "hypergraph")]
next_hyperedge_id,
}
}
}
impl Default for DatabaseHeader {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_page_size_is_4096() {
assert_eq!(PAGE_SIZE, 4096);
}
#[test]
fn test_magic_bytes() {
assert_eq!(MAGIC, 0x4359_4C54);
let bytes = MAGIC.to_be_bytes();
assert_eq!(&bytes, b"CYLT");
}
#[test]
fn test_header_page_id_is_zero() {
assert_eq!(HEADER_PAGE_ID, 0);
}
#[test]
fn test_fsm_page_id_is_one() {
assert_eq!(FSM_PAGE_ID, 1);
}
#[test]
fn test_page_header_size_is_32() {
assert_eq!(PageHeader::SIZE, 32);
}
#[test]
fn test_page_header_new() {
let hdr = PageHeader::new(PageType::BTreeLeaf);
assert_eq!(hdr.page_type, PageType::BTreeLeaf as u8);
assert_eq!(hdr.free_start, 32);
assert_eq!(hdr.free_end, 4096);
assert_eq!(hdr.item_count, 0);
}
#[test]
fn test_page_header_roundtrip() {
let hdr = PageHeader::new(PageType::BTreeInterior);
let mut buf = [0u8; PAGE_SIZE];
hdr.write_to(&mut buf);
let decoded = PageHeader::read_from(&buf);
assert_eq!(hdr, decoded);
}
#[test]
fn test_database_header_new() {
let hdr = DatabaseHeader::new();
assert_eq!(hdr.magic, MAGIC);
assert_eq!(hdr.version, FORMAT_VERSION);
assert_eq!(hdr.page_count, FIRST_DATA_PAGE);
assert_eq!(hdr.next_node_id, 1);
assert_eq!(hdr.next_edge_id, 1);
assert_eq!(hdr.version_store_root_page, 0);
}
#[test]
#[allow(clippy::needless_update)]
fn test_database_header_roundtrip() {
let hdr = DatabaseHeader {
magic: MAGIC,
version: FORMAT_VERSION,
page_count: 100,
root_node_page: 5,
root_edge_page: 10,
next_node_id: 42,
next_edge_id: 99,
version_store_root_page: 0,
feature_flags: DatabaseHeader::FLAG_TEMPORAL_CORE,
..DatabaseHeader::new()
};
let page = hdr.to_page();
let decoded = DatabaseHeader::from_page(&page);
assert_eq!(decoded.magic, MAGIC);
assert_eq!(decoded.version, FORMAT_VERSION);
assert_eq!(decoded.page_count, 100);
assert_eq!(decoded.root_node_page, 5);
assert_eq!(decoded.root_edge_page, 10);
assert_eq!(decoded.next_node_id, 42);
assert_eq!(decoded.next_edge_id, 99);
assert_eq!(decoded.version_store_root_page, 0);
assert_eq!(decoded.feature_flags, DatabaseHeader::FLAG_TEMPORAL_CORE);
}
#[test]
fn test_database_header_version_store_root_page() {
let hdr = DatabaseHeader {
version_store_root_page: 42,
..DatabaseHeader::new()
};
let page = hdr.to_page();
let decoded = DatabaseHeader::from_page(&page);
assert_eq!(decoded.version_store_root_page, 42);
}
#[test]
fn test_database_header_v1_migration() {
let mut page = [0u8; PAGE_SIZE];
page[0..4].copy_from_slice(&MAGIC.to_le_bytes());
page[4..8].copy_from_slice(&1u32.to_le_bytes()); page[8..12].copy_from_slice(&FIRST_DATA_PAGE.to_le_bytes());
page[20..28].copy_from_slice(&1u64.to_le_bytes());
page[28..36].copy_from_slice(&1u64.to_le_bytes());
let decoded = DatabaseHeader::from_page(&page);
assert_eq!(decoded.version, 1); assert_eq!(decoded.version_store_root_page, 0); }
#[cfg(not(feature = "subgraph"))]
#[test]
fn test_format_version_is_3() {
assert_eq!(FORMAT_VERSION, 3);
}
#[test]
fn test_database_header_feature_flags_roundtrip() {
let hdr = DatabaseHeader {
feature_flags: DatabaseHeader::FLAG_TEMPORAL_CORE | DatabaseHeader::FLAG_TEMPORAL_EDGE,
..DatabaseHeader::new()
};
let page = hdr.to_page();
let decoded = DatabaseHeader::from_page(&page);
assert_eq!(
decoded.feature_flags,
DatabaseHeader::FLAG_TEMPORAL_CORE | DatabaseHeader::FLAG_TEMPORAL_EDGE
);
}
#[test]
fn test_database_header_v2_migration_feature_flags() {
let mut page = [0u8; PAGE_SIZE];
page[0..4].copy_from_slice(&MAGIC.to_le_bytes());
page[4..8].copy_from_slice(&2u32.to_le_bytes()); page[8..12].copy_from_slice(&FIRST_DATA_PAGE.to_le_bytes());
page[20..28].copy_from_slice(&1u64.to_le_bytes());
page[28..36].copy_from_slice(&1u64.to_le_bytes());
let decoded = DatabaseHeader::from_page(&page);
assert_eq!(decoded.version, 2);
assert_eq!(decoded.feature_flags, DatabaseHeader::FLAG_TEMPORAL_CORE);
}
#[test]
fn test_compiled_feature_flags() {
let flags = DatabaseHeader::compiled_feature_flags();
assert!(flags & DatabaseHeader::FLAG_TEMPORAL_CORE != 0);
}
#[test]
fn test_database_header_new_sets_feature_flags() {
let hdr = DatabaseHeader::new();
assert!(hdr.feature_flags & DatabaseHeader::FLAG_TEMPORAL_CORE != 0);
}
#[test]
fn test_fsm_max_pages() {
assert_eq!(FSM_MAX_PAGES, 32768);
}
#[test]
fn test_page_type_variants() {
assert_eq!(PageType::Header as u8, 0);
assert_eq!(PageType::FreeSpaceMap as u8, 1);
assert_eq!(PageType::BTreeInterior as u8, 2);
assert_eq!(PageType::BTreeLeaf as u8, 3);
assert_eq!(PageType::Overflow as u8, 4);
assert_eq!(PageType::Data as u8, 5);
}
#[cfg(feature = "subgraph")]
mod subgraph_header_tests {
use super::*;
#[cfg(not(feature = "hypergraph"))]
#[test]
fn test_format_version_is_4() {
assert_eq!(FORMAT_VERSION, 4);
}
#[test]
fn test_flag_subgraph_constant() {
assert_eq!(DatabaseHeader::FLAG_SUBGRAPH, 0x04);
}
#[test]
fn test_database_header_new_subgraph_fields() {
let hdr = DatabaseHeader::new();
assert_eq!(hdr.subgraph_root_page, 0);
assert_eq!(hdr.next_subgraph_id, 1);
assert!(hdr.feature_flags & DatabaseHeader::FLAG_SUBGRAPH != 0);
}
#[test]
fn test_subgraph_root_page_roundtrip() {
let hdr = DatabaseHeader {
subgraph_root_page: 42,
..DatabaseHeader::new()
};
let page = hdr.to_page();
let decoded = DatabaseHeader::from_page(&page);
assert_eq!(decoded.subgraph_root_page, 42);
}
#[test]
fn test_next_subgraph_id_roundtrip() {
let hdr = DatabaseHeader {
next_subgraph_id: 999,
..DatabaseHeader::new()
};
let page = hdr.to_page();
let decoded = DatabaseHeader::from_page(&page);
assert_eq!(decoded.next_subgraph_id, 999);
}
#[test]
#[allow(clippy::needless_update)]
fn test_database_header_v4_full_roundtrip() {
let hdr = DatabaseHeader {
magic: MAGIC,
version: FORMAT_VERSION,
page_count: 100,
root_node_page: 5,
root_edge_page: 10,
next_node_id: 42,
next_edge_id: 99,
version_store_root_page: 7,
feature_flags: DatabaseHeader::FLAG_TEMPORAL_CORE
| DatabaseHeader::FLAG_TEMPORAL_EDGE
| DatabaseHeader::FLAG_SUBGRAPH,
subgraph_root_page: 15,
next_subgraph_id: 200,
..DatabaseHeader::new()
};
let page = hdr.to_page();
let decoded = DatabaseHeader::from_page(&page);
assert_eq!(decoded.magic, MAGIC);
assert_eq!(decoded.version, FORMAT_VERSION);
assert_eq!(decoded.page_count, 100);
assert_eq!(decoded.root_node_page, 5);
assert_eq!(decoded.root_edge_page, 10);
assert_eq!(decoded.next_node_id, 42);
assert_eq!(decoded.next_edge_id, 99);
assert_eq!(decoded.version_store_root_page, 7);
assert_eq!(decoded.subgraph_root_page, 15);
assert_eq!(decoded.next_subgraph_id, 200);
assert_eq!(
decoded.feature_flags,
DatabaseHeader::FLAG_TEMPORAL_CORE
| DatabaseHeader::FLAG_TEMPORAL_EDGE
| DatabaseHeader::FLAG_SUBGRAPH
);
}
#[test]
fn test_database_header_v3_migration() {
let mut page = [0u8; PAGE_SIZE];
page[0..4].copy_from_slice(&MAGIC.to_le_bytes());
page[4..8].copy_from_slice(&3u32.to_le_bytes()); page[8..12].copy_from_slice(&FIRST_DATA_PAGE.to_le_bytes());
page[20..28].copy_from_slice(&1u64.to_le_bytes());
page[28..36].copy_from_slice(&1u64.to_le_bytes());
let flags = DatabaseHeader::FLAG_TEMPORAL_CORE | DatabaseHeader::FLAG_TEMPORAL_EDGE;
page[44..48].copy_from_slice(&flags.to_le_bytes());
let decoded = DatabaseHeader::from_page(&page);
assert_eq!(decoded.version, 3);
assert_eq!(decoded.subgraph_root_page, 0); assert_eq!(decoded.next_subgraph_id, 0); }
#[test]
fn test_compiled_feature_flags_includes_subgraph() {
let flags = DatabaseHeader::compiled_feature_flags();
assert!(flags & DatabaseHeader::FLAG_SUBGRAPH != 0);
}
}
#[cfg(feature = "hypergraph")]
mod hypergraph_header_tests {
use super::*;
#[test]
fn test_format_version_is_5_with_hypergraph() {
assert_eq!(FORMAT_VERSION, 5);
}
#[test]
fn test_flag_hypergraph_constant() {
assert_eq!(DatabaseHeader::FLAG_HYPERGRAPH, 0x08);
}
#[test]
fn test_database_header_new_hypergraph_fields() {
let hdr = DatabaseHeader::new();
assert_eq!(hdr.hyperedge_root_page, 0);
assert_eq!(hdr.next_hyperedge_id, 1);
assert!(hdr.feature_flags & DatabaseHeader::FLAG_HYPERGRAPH != 0);
}
#[test]
fn test_hyperedge_root_page_roundtrip() {
let hdr = DatabaseHeader {
hyperedge_root_page: 77,
..DatabaseHeader::new()
};
let page = hdr.to_page();
let decoded = DatabaseHeader::from_page(&page);
assert_eq!(decoded.hyperedge_root_page, 77);
}
#[test]
fn test_next_hyperedge_id_roundtrip() {
let hdr = DatabaseHeader {
next_hyperedge_id: 500,
..DatabaseHeader::new()
};
let page = hdr.to_page();
let decoded = DatabaseHeader::from_page(&page);
assert_eq!(decoded.next_hyperedge_id, 500);
}
#[test]
fn test_database_header_v5_full_roundtrip() {
let hdr = DatabaseHeader {
magic: MAGIC,
version: FORMAT_VERSION,
page_count: 100,
root_node_page: 5,
root_edge_page: 10,
next_node_id: 42,
next_edge_id: 99,
version_store_root_page: 7,
feature_flags: DatabaseHeader::FLAG_TEMPORAL_CORE
| DatabaseHeader::FLAG_TEMPORAL_EDGE
| DatabaseHeader::FLAG_SUBGRAPH
| DatabaseHeader::FLAG_HYPERGRAPH,
subgraph_root_page: 15,
next_subgraph_id: 200,
hyperedge_root_page: 25,
next_hyperedge_id: 300,
};
let page = hdr.to_page();
let decoded = DatabaseHeader::from_page(&page);
assert_eq!(decoded.magic, MAGIC);
assert_eq!(decoded.version, FORMAT_VERSION);
assert_eq!(decoded.page_count, 100);
assert_eq!(decoded.root_node_page, 5);
assert_eq!(decoded.root_edge_page, 10);
assert_eq!(decoded.next_node_id, 42);
assert_eq!(decoded.next_edge_id, 99);
assert_eq!(decoded.version_store_root_page, 7);
assert_eq!(decoded.subgraph_root_page, 15);
assert_eq!(decoded.next_subgraph_id, 200);
assert_eq!(decoded.hyperedge_root_page, 25);
assert_eq!(decoded.next_hyperedge_id, 300);
assert_eq!(
decoded.feature_flags,
DatabaseHeader::FLAG_TEMPORAL_CORE
| DatabaseHeader::FLAG_TEMPORAL_EDGE
| DatabaseHeader::FLAG_SUBGRAPH
| DatabaseHeader::FLAG_HYPERGRAPH
);
}
#[test]
fn test_database_header_v4_to_v5_migration() {
let mut page = [0u8; PAGE_SIZE];
page[0..4].copy_from_slice(&MAGIC.to_le_bytes());
page[4..8].copy_from_slice(&4u32.to_le_bytes()); page[8..12].copy_from_slice(&FIRST_DATA_PAGE.to_le_bytes());
page[20..28].copy_from_slice(&1u64.to_le_bytes());
page[28..36].copy_from_slice(&1u64.to_le_bytes());
let flags = DatabaseHeader::FLAG_TEMPORAL_CORE
| DatabaseHeader::FLAG_TEMPORAL_EDGE
| DatabaseHeader::FLAG_SUBGRAPH;
page[44..48].copy_from_slice(&flags.to_le_bytes());
page[48..56].copy_from_slice(&10u64.to_le_bytes()); page[56..64].copy_from_slice(&5u64.to_le_bytes());
let decoded = DatabaseHeader::from_page(&page);
assert_eq!(decoded.version, 4);
assert_eq!(decoded.subgraph_root_page, 10);
assert_eq!(decoded.next_subgraph_id, 5);
assert_eq!(decoded.hyperedge_root_page, 0); assert_eq!(decoded.next_hyperedge_id, 0); }
#[test]
fn test_compiled_feature_flags_includes_hypergraph() {
let flags = DatabaseHeader::compiled_feature_flags();
assert!(flags & DatabaseHeader::FLAG_HYPERGRAPH != 0);
}
}
}