use serde::{Deserialize, Serialize};
use crate::utils::error::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u32)]
#[non_exhaustive]
pub enum SectionType {
Catalog = 1,
LpgStore = 2,
RdfStore = 3,
CompactStore = 4,
VectorStore = 10,
TextIndex = 11,
RdfRing = 12,
PropertyIndex = 20,
}
impl SectionType {
#[must_use]
pub const fn is_data_section(self) -> bool {
(self as u32) < 10
}
#[must_use]
pub const fn is_index_section(self) -> bool {
(self as u32) >= 10
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SectionFlags {
pub required: bool,
pub mmap_able: bool,
}
impl SectionFlags {
#[must_use]
pub const fn to_byte(self) -> u8 {
let mut flags = 0u8;
if self.required {
flags |= 0x01;
}
if self.mmap_able {
flags |= 0x02;
}
flags
}
#[must_use]
pub const fn from_byte(byte: u8) -> Self {
Self {
required: byte & 0x01 != 0,
mmap_able: byte & 0x02 != 0,
}
}
}
impl SectionType {
#[must_use]
pub const fn default_flags(self) -> SectionFlags {
match self {
Self::Catalog => SectionFlags {
required: true,
mmap_able: false,
},
Self::LpgStore => SectionFlags {
required: true,
mmap_able: false,
},
Self::RdfStore => SectionFlags {
required: false,
mmap_able: false,
},
Self::CompactStore => SectionFlags {
required: true,
mmap_able: true,
},
Self::VectorStore | Self::TextIndex | Self::RdfRing | Self::PropertyIndex => {
SectionFlags {
required: false,
mmap_able: true,
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SectionDirectoryEntry {
pub section_type: SectionType,
pub version: u8,
pub flags: SectionFlags,
pub offset: u64,
pub length: u64,
pub checksum: u32,
}
impl SectionDirectoryEntry {
pub const SIZE: usize = 32;
}
pub trait Section: Send + Sync {
fn section_type(&self) -> SectionType;
fn version(&self) -> u8 {
1
}
fn serialize(&self) -> Result<Vec<u8>>;
fn deserialize(&mut self, data: &[u8]) -> Result<()>;
fn is_dirty(&self) -> bool;
fn mark_clean(&self);
fn memory_usage(&self) -> usize;
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum TierOverride {
#[default]
Auto,
ForceRam,
ForceDisk,
}
#[derive(Debug, Clone)]
pub struct SectionMemoryConfig {
pub max_ram: Option<usize>,
pub tier: TierOverride,
}
impl Default for SectionMemoryConfig {
fn default() -> Self {
Self {
max_ram: None,
tier: TierOverride::Auto,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn section_type_classification() {
assert!(SectionType::Catalog.is_data_section());
assert!(SectionType::LpgStore.is_data_section());
assert!(SectionType::RdfStore.is_data_section());
assert!(!SectionType::VectorStore.is_data_section());
assert!(!SectionType::Catalog.is_index_section());
assert!(SectionType::VectorStore.is_index_section());
assert!(SectionType::TextIndex.is_index_section());
assert!(SectionType::RdfRing.is_index_section());
assert!(SectionType::PropertyIndex.is_index_section());
}
#[test]
fn section_flags_roundtrip() {
let flags = SectionFlags {
required: true,
mmap_able: false,
};
assert_eq!(flags.to_byte(), 0x01);
assert_eq!(SectionFlags::from_byte(0x01), flags);
let flags = SectionFlags {
required: false,
mmap_able: true,
};
assert_eq!(flags.to_byte(), 0x02);
assert_eq!(SectionFlags::from_byte(0x02), flags);
let flags = SectionFlags {
required: true,
mmap_able: true,
};
assert_eq!(flags.to_byte(), 0x03);
assert_eq!(SectionFlags::from_byte(0x03), flags);
let empty = SectionFlags::default();
assert_eq!(empty.to_byte(), 0x00);
assert_eq!(SectionFlags::from_byte(0x00), empty);
}
#[test]
fn default_flags_by_type() {
let catalog = SectionType::Catalog.default_flags();
assert!(catalog.required);
assert!(!catalog.mmap_able);
let vector = SectionType::VectorStore.default_flags();
assert!(!vector.required);
assert!(vector.mmap_able);
let rdf = SectionType::RdfStore.default_flags();
assert!(!rdf.required);
assert!(
!rdf.mmap_able,
"data sections must be deserialized, not mmap'd"
);
}
#[test]
fn directory_entry_size() {
assert_eq!(SectionDirectoryEntry::SIZE, 32);
}
#[test]
fn alix_tier_override_variants() {
assert_eq!(TierOverride::Auto, TierOverride::default());
assert_ne!(TierOverride::Auto, TierOverride::ForceRam);
assert_ne!(TierOverride::Auto, TierOverride::ForceDisk);
assert_ne!(TierOverride::ForceRam, TierOverride::ForceDisk);
}
#[test]
fn gus_section_memory_config_default() {
let config = SectionMemoryConfig::default();
assert!(config.max_ram.is_none());
assert_eq!(config.tier, TierOverride::Auto);
}
#[test]
fn vincent_section_memory_config_with_cap() {
let config = SectionMemoryConfig {
max_ram: Some(1024 * 1024),
tier: TierOverride::ForceRam,
};
assert_eq!(config.max_ram, Some(1024 * 1024));
assert_eq!(config.tier, TierOverride::ForceRam);
}
#[test]
fn jules_force_disk_tier() {
let config = SectionMemoryConfig {
max_ram: None,
tier: TierOverride::ForceDisk,
};
assert_eq!(config.tier, TierOverride::ForceDisk);
}
#[test]
fn mia_lpg_store_default_flags_distinct_from_rdf() {
let lpg = SectionType::LpgStore.default_flags();
let rdf = SectionType::RdfStore.default_flags();
assert!(lpg.required);
assert!(!rdf.required);
assert!(!lpg.mmap_able, "LpgStore is a data section, not mmap-able");
assert!(!rdf.mmap_able, "RdfStore is a data section, not mmap-able");
}
#[test]
fn butch_index_section_default_flags_all_variants() {
for section_type in [
SectionType::VectorStore,
SectionType::TextIndex,
SectionType::RdfRing,
SectionType::PropertyIndex,
] {
let flags = section_type.default_flags();
assert!(!flags.required, "{section_type:?} should not be required");
assert!(flags.mmap_able, "{section_type:?} should be mmap-able");
}
}
#[test]
fn django_directory_entry_construction() {
let entry = SectionDirectoryEntry {
section_type: SectionType::LpgStore,
version: 1,
flags: SectionFlags {
required: true,
mmap_able: false,
},
offset: 4096,
length: 8192,
checksum: 0xDEAD_BEEF,
};
assert_eq!(entry.section_type, SectionType::LpgStore);
assert_eq!(entry.version, 1);
assert!(entry.flags.required);
assert!(!entry.flags.mmap_able);
assert_eq!(entry.offset, 4096);
assert_eq!(entry.length, 8192);
assert_eq!(entry.checksum, 0xDEAD_BEEF);
}
#[test]
fn shosanna_section_type_is_data_vs_index_boundary() {
assert!(SectionType::Catalog.is_data_section());
assert!(!SectionType::Catalog.is_index_section());
assert!(SectionType::VectorStore.is_index_section());
assert!(!SectionType::VectorStore.is_data_section());
assert!(SectionType::PropertyIndex.is_index_section());
assert!(!SectionType::PropertyIndex.is_data_section());
}
#[test]
fn hans_section_flags_extra_bits_ignored() {
let flags = SectionFlags::from_byte(0xFF);
assert!(flags.required);
assert!(flags.mmap_able);
let flags = SectionFlags::from_byte(0xFC);
assert!(!flags.required);
assert!(!flags.mmap_able);
}
#[test]
fn beatrix_directory_entry_clone_eq() {
let entry = SectionDirectoryEntry {
section_type: SectionType::RdfRing,
version: 2,
flags: SectionFlags {
required: false,
mmap_able: true,
},
offset: 0,
length: 1024,
checksum: 42,
};
let cloned = entry.clone();
assert_eq!(entry, cloned);
}
struct StubSection {
dirty: bool,
}
impl Section for StubSection {
fn section_type(&self) -> SectionType {
SectionType::LpgStore
}
fn serialize(&self) -> crate::utils::error::Result<Vec<u8>> {
Ok(vec![1, 2, 3])
}
fn deserialize(&mut self, _data: &[u8]) -> crate::utils::error::Result<()> {
Ok(())
}
fn is_dirty(&self) -> bool {
self.dirty
}
fn mark_clean(&self) {}
fn memory_usage(&self) -> usize {
64
}
}
#[test]
fn mia_section_trait_default_version() {
let stub = StubSection { dirty: false };
assert_eq!(stub.version(), 1);
assert_eq!(stub.section_type(), SectionType::LpgStore);
assert!(!stub.is_dirty());
assert_eq!(stub.memory_usage(), 64);
}
#[test]
fn butch_section_trait_serialize_deserialize() {
let mut stub = StubSection { dirty: true };
assert!(stub.is_dirty());
let data = stub.serialize().unwrap();
assert_eq!(data, vec![1, 2, 3]);
stub.deserialize(&[4, 5, 6]).unwrap();
stub.mark_clean();
}
}