use super::record::NodeRecordV3;
use crate::backend::native::NativeBackendError;
use crate::backend::native::NativeResult;
use crate::backend::native::v3::compression;
use crate::backend::native::v3::compression::delta::{decode_id_delta, encode_id_delta};
use crate::backend::native::v3::compression::varint::{
decode_varint, decode_varint_u16, encode_varint, encode_varint_u16,
};
use crate::backend::native::v3::constants as v3_constants;
mod codec;
mod codec_support;
#[cfg(test)]
mod tests;
pub mod constants {
pub const PAGE_HEADER_SIZE: usize = 32;
pub const FIXED_METADATA_SIZE: usize = 44;
pub const MAX_INLINE_DATA: usize = 64;
pub const PAGE_ID_OFFSET: usize = 0;
pub const NEXT_PAGE_ID_OFFSET: usize = 8;
pub const NODE_COUNT_OFFSET: usize = 16;
pub const USED_BYTES_OFFSET: usize = 18;
pub const BASE_ID_OFFSET: usize = 20;
pub const CHECKSUM_OFFSET: usize = 28;
pub const MAX_PAGE_SIZE: usize = 4096;
pub const USABLE_SIZE: usize = MAX_PAGE_SIZE - PAGE_HEADER_SIZE;
pub const ESTIMATED_NODE_SLOT_SIZE: usize = 80;
pub const MAX_NODE_CAPACITY: usize = USABLE_SIZE / ESTIMATED_NODE_SLOT_SIZE;
pub const PAGE_ID_SIZE: usize = 8;
pub const NEXT_PAGE_ID_SIZE: usize = 8;
pub const NODE_COUNT_SIZE: usize = 2;
pub const USED_BYTES_SIZE: usize = 2;
pub const BASE_ID_SIZE: usize = 8;
pub const CHECKSUM_SIZE: usize = 4;
pub const MIN_COMPRESSED_RECORD_SIZE: usize = 12;
}
pub use constants::{
BASE_ID_OFFSET, ESTIMATED_NODE_SLOT_SIZE, MAX_NODE_CAPACITY, MAX_PAGE_SIZE,
MIN_COMPRESSED_RECORD_SIZE, PAGE_HEADER_SIZE, USABLE_SIZE, USED_BYTES_OFFSET,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NodePage {
pub page_id: u64,
pub next_page_id: u64,
pub nodes: Vec<NodeRecordV3>,
pub used_bytes: u16,
pub base_id: i64,
pub checksum: u32,
pub block_id: i64,
}
pub const BLOCK_SIZE: i64 = 128;
#[inline]
pub const fn node_id_to_block(node_id: i64) -> i64 {
if node_id < 1 {
return 0;
}
(node_id - 1) / BLOCK_SIZE
}
impl NodePage {
pub fn new(page_id: u64) -> Self {
NodePage {
page_id,
next_page_id: 0,
nodes: Vec::new(),
used_bytes: 0,
base_id: 0,
checksum: 0,
block_id: 0, }
}
pub fn with_capacity(page_id: u64, capacity: usize) -> Self {
NodePage {
page_id,
next_page_id: 0,
nodes: Vec::with_capacity(capacity.min(MAX_NODE_CAPACITY)),
used_bytes: 0,
base_id: 0,
checksum: 0,
block_id: 0,
}
}
pub fn block_id(&self) -> i64 {
if self.block_id != 0 {
self.block_id
} else {
node_id_to_block(self.base_id)
}
}
pub fn recompute_block_id(&mut self) {
if let Some(min_id) = self.nodes.iter().map(|n| n.id()).min() {
self.base_id = min_id;
self.block_id = node_id_to_block(min_id);
} else {
self.base_id = 0;
self.block_id = 0;
}
}
pub fn node_count(&self) -> u16 {
self.nodes.len() as u16
}
pub fn has_overflow(&self) -> bool {
self.next_page_id != 0
}
pub fn is_full(&self) -> bool {
(self.capacity() as usize) < ESTIMATED_NODE_SLOT_SIZE
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn used_size(&self) -> usize {
self.used_bytes as usize
}
pub fn remaining_capacity(&self) -> usize {
USABLE_SIZE.saturating_sub(self.used_bytes as usize)
}
pub fn capacity(&self) -> u16 {
(USABLE_SIZE as u16).saturating_sub(self.used_bytes)
}
#[inline]
pub fn find_node(&self, node_id: i64) -> Option<&NodeRecordV3> {
let mut left = 0;
let mut right = self.nodes.len();
while left < right {
let mid = left + (right - left) / 2;
let mid_id = self.nodes[mid].id();
if mid_id == node_id {
return Some(&self.nodes[mid]);
} else if mid_id < node_id {
left = mid + 1;
} else {
right = mid;
}
}
if left < self.nodes.len() && self.nodes[left].id() == node_id {
Some(&self.nodes[left])
} else {
None
}
}
pub fn add_node(&mut self, node: NodeRecordV3) -> NativeResult<()> {
let temp_base_id = if self.nodes.is_empty() {
node.id()
} else {
self.base_id.min(node.id())
};
let compressed_size = self.estimate_compressed_size_with_base(&node, temp_base_id)?;
if self.capacity() < compressed_size {
return Err(NativeBackendError::InvalidHeader {
field: "node_page".to_string(),
reason: format!(
"adding node would exceed page capacity: need {} bytes, have {} remaining",
compressed_size,
self.capacity()
),
});
}
self.base_id = temp_base_id;
self.nodes.push(node);
self.used_bytes += compressed_size;
let actual_packed_size = self.pack_nodes()?.len();
if actual_packed_size > USABLE_SIZE {
self.nodes.pop();
self.used_bytes -= compressed_size;
if !self.nodes.is_empty() {
self.base_id = self
.nodes
.iter()
.map(|n| n.id())
.min()
.unwrap_or(self.base_id);
}
#[cfg(feature = "v3-page-overflow-debug")]
{
eprintln!(
"Page {} overflow: node_count={}, used_bytes={}, actual_packed_size={}, USABLE_SIZE={}, estimated={}",
self.page_id,
self.nodes.len() + 1, self.used_bytes + compressed_size,
actual_packed_size,
USABLE_SIZE,
compressed_size
);
}
return Err(NativeBackendError::InvalidHeader {
field: "node_page".to_string(),
reason: format!(
"page {} full: node_count={}, used_bytes={}, actual packed {} exceeds USABLE_SIZE {} (new node estimated {})",
self.page_id,
self.nodes.len(),
self.used_bytes,
actual_packed_size,
USABLE_SIZE,
compressed_size
),
});
}
self.used_bytes = actual_packed_size as u16;
self.block_id = node_id_to_block(self.base_id);
Ok(())
}
fn estimate_compressed_size_with_base(
&self,
node: &NodeRecordV3,
base_id: i64,
) -> NativeResult<u16> {
let mut size: usize = 0;
let delta = encode_id_delta(node.id(), base_id);
size += compression::varint::varint_size(delta as u64);
size += 4;
size += compression::varint::varint_size(node.kind_offset as u64);
size += compression::varint::varint_size(node.name_offset as u64);
size += compression::varint::varint_size(node.data_len() as u64);
size += compression::varint::varint_size(node.outgoing_cluster_offset);
size += compression::varint::varint_size(node.outgoing_edge_count as u64);
size += compression::varint::varint_size(node.incoming_cluster_offset);
size += compression::varint::varint_size(node.incoming_edge_count as u64);
if let Some(ref data) = node.data_inline {
size += data.len();
} else if node.data_external_offset.is_some() {
size += 8; }
if size > u16::MAX as usize {
return Err(NativeBackendError::InvalidHeader {
field: "compressed_size".to_string(),
reason: format!("compressed size {} exceeds u16::MAX", size),
});
}
Ok(size as u16)
}
pub fn disk_size(&self) -> usize {
MAX_PAGE_SIZE
}
pub fn space_efficiency(&self) -> f64 {
if MAX_PAGE_SIZE == 0 {
return 0.0;
}
(self.used_size() as f64) / (USABLE_SIZE as f64)
}
}
impl Default for NodePage {
fn default() -> Self {
Self::new(0)
}
}