use crate::datatypes::values::Value;
use crate::graph::schema::InternedKey;
use crate::graph::storage::type_build_meta::ColType;
use chrono::NaiveDate;
use memmap2::MmapMut;
use std::collections::HashMap;
use std::sync::Arc;
const UNIX_EPOCH_DATE: NaiveDate = match NaiveDate::from_ymd_opt(1970, 1, 1) {
Some(d) => d,
None => unreachable!(),
};
#[derive(Clone, Copy, Debug)]
pub struct Region {
pub offset: usize,
pub len: usize, }
impl Region {
pub const EMPTY: Region = Region { offset: 0, len: 0 };
}
#[derive(Clone, Debug)]
pub struct FixedColumnMeta {
pub col_type: ColType,
pub data: Region, pub nulls: Region, }
#[derive(Clone, Debug)]
pub struct StrColumnMeta {
pub data: Region, pub offsets: Region, pub nulls: Region, }
#[derive(Clone, Copy, Debug)]
pub(crate) enum ColRef {
Fixed(usize), Str(usize), }
#[derive(Clone, Debug)]
pub struct MmapColumnStore {
pub(crate) mmap: Arc<MmapMut>,
pub(crate) row_count: u32,
pub(crate) id_is_string: bool,
pub(crate) id_fixed: Option<FixedColumnMeta>,
pub(crate) id_str: Option<StrColumnMeta>,
pub(crate) title: StrColumnMeta,
pub(crate) col_map: HashMap<InternedKey, ColRef>,
pub(crate) fixed_cols: Vec<FixedColumnMeta>,
pub(crate) str_cols: Vec<StrColumnMeta>,
pub(crate) overflow_offsets: Region,
pub(crate) overflow_data: Region,
pub(crate) has_overflow: bool,
}
impl MmapColumnStore {
#[inline]
pub fn row_count(&self) -> u32 {
self.row_count
}
}
impl MmapColumnStore {
#[inline]
fn read_null(&self, region: &Region, row: usize) -> bool {
if region.len == 0 {
return true;
}
self.mmap[region.offset + row] != 0
}
#[inline]
fn read_i64(&self, region: &Region, row: usize) -> i64 {
let off = region.offset + row * 8;
i64::from_ne_bytes(self.mmap[off..off + 8].try_into().unwrap())
}
#[inline]
fn read_f64(&self, region: &Region, row: usize) -> f64 {
let off = region.offset + row * 8;
f64::from_ne_bytes(self.mmap[off..off + 8].try_into().unwrap())
}
#[inline]
fn read_u64(&self, region: &Region, row: usize) -> u64 {
let off = region.offset + row * 8;
u64::from_ne_bytes(self.mmap[off..off + 8].try_into().unwrap())
}
#[inline]
fn read_u32(&self, region: &Region, row: usize) -> u32 {
let off = region.offset + row * 4;
u32::from_ne_bytes(self.mmap[off..off + 4].try_into().unwrap())
}
#[inline]
fn read_i32(&self, region: &Region, row: usize) -> i32 {
let off = region.offset + row * 4;
i32::from_ne_bytes(self.mmap[off..off + 4].try_into().unwrap())
}
#[inline]
fn read_u8(&self, region: &Region, row: usize) -> u8 {
self.mmap[region.offset + row]
}
#[inline]
fn read_str(&self, data_region: &Region, offsets_region: &Region, row: usize) -> &str {
let end = self.read_u64(offsets_region, row) as usize;
let start = if row > 0 {
self.read_u64(offsets_region, row - 1) as usize
} else {
0
};
let bytes = &self.mmap[data_region.offset + start..data_region.offset + end];
unsafe { std::str::from_utf8_unchecked(bytes) }
}
}
impl MmapColumnStore {
pub fn validate_utf8(&self, type_name: &str) -> std::io::Result<()> {
if let Some(sc) = &self.id_str {
self.validate_str_column(sc, type_name, "__id__")?;
}
self.validate_str_column(&self.title, type_name, "__title__")?;
for (i, sc) in self.str_cols.iter().enumerate() {
self.validate_str_column(sc, type_name, &format!("str_col[{i}]"))?;
}
Ok(())
}
fn validate_str_column(
&self,
sc: &StrColumnMeta,
type_name: &str,
col: &str,
) -> std::io::Result<()> {
let corrupt = |what: &str| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"columns.bin string column '{type_name}.{col}' is corrupt: {what} \
(refusing to load — rebuild the graph or restore the file from backup)"
),
)
};
if sc.data.len == 0 && sc.offsets.len == 0 && sc.nulls.len == 0 {
return Ok(());
}
for (region, what) in [
(&sc.data, "data"),
(&sc.offsets, "offsets"),
(&sc.nulls, "nulls"),
] {
let end = region
.offset
.checked_add(region.len)
.ok_or_else(|| corrupt(&format!("{what} region overflows usize")))?;
if end > self.mmap.len() {
return Err(corrupt(&format!("{what} region extends past the file")));
}
}
let rows = self.row_count as usize;
let need_offsets = rows
.checked_mul(8)
.ok_or_else(|| corrupt("offset count overflows usize"))?;
if sc.offsets.len < need_offsets {
return Err(corrupt("offsets region too small for the row count"));
}
let blob = &self.mmap[sc.data.offset..sc.data.offset + sc.data.len];
let s = std::str::from_utf8(blob).map_err(|e| corrupt(&format!("invalid UTF-8: {e}")))?;
let mut prev = 0u64;
for row in 0..rows {
let end = self.read_u64(&sc.offsets, row);
if end < prev || end as usize > blob.len() {
return Err(corrupt(&format!(
"non-monotonic or out-of-range offset at row {row}"
)));
}
if !s.is_char_boundary(end as usize) {
return Err(corrupt(&format!(
"offset at row {row} splits a UTF-8 code point"
)));
}
prev = end;
}
Ok(())
}
}
impl MmapColumnStore {
pub fn get_id(&self, row_id: u32) -> Option<Value> {
if row_id >= self.row_count {
return None;
}
let row = row_id as usize;
if self.id_is_string {
let sc = self.id_str.as_ref()?;
if self.read_null(&sc.nulls, row) {
return None;
}
Some(Value::String(
self.read_str(&sc.data, &sc.offsets, row).to_string(),
))
} else {
let fc = self.id_fixed.as_ref()?;
if self.read_null(&fc.nulls, row) {
return None;
}
Some(Value::UniqueId(self.read_u32(&fc.data, row)))
}
}
pub fn get_title(&self, row_id: u32) -> Option<Value> {
if row_id >= self.row_count {
return None;
}
let row = row_id as usize;
if self.title.nulls.len == 0 && self.title.data.len == 0 {
return None;
}
if self.read_null(&self.title.nulls, row) {
return None;
}
Some(Value::String(
self.read_str(&self.title.data, &self.title.offsets, row)
.to_string(),
))
}
pub fn str_prop_eq(&self, row_id: u32, key: InternedKey, target: &str) -> Option<bool> {
if row_id >= self.row_count {
return None;
}
let row = row_id as usize;
if let Some(col_ref) = self.col_map.get(&key) {
match col_ref {
ColRef::Str(idx) => {
let sc = &self.str_cols[*idx];
if self.read_null(&sc.nulls, row) {
return None;
}
return Some(self.read_str(&sc.data, &sc.offsets, row) == target);
}
ColRef::Fixed(_) => {
return Some(false);
}
}
}
self.get_overflow_property(row_id, key)
.map(|v| matches!(v, Value::String(ref s) if s == target))
}
pub fn get(&self, row_id: u32, key: InternedKey) -> Option<Value> {
if row_id >= self.row_count {
return None;
}
let row = row_id as usize;
if let Some(col_ref) = self.col_map.get(&key) {
match col_ref {
ColRef::Fixed(idx) => {
let fc = &self.fixed_cols[*idx];
if !self.read_null(&fc.nulls, row) {
return Some(self.read_fixed_value(fc, row));
}
}
ColRef::Str(idx) => {
let sc = &self.str_cols[*idx];
if !self.read_null(&sc.nulls, row) {
let s = self.read_str(&sc.data, &sc.offsets, row);
return Some(Value::String(s.to_string()));
}
}
}
}
self.get_overflow_property(row_id, key)
}
#[inline]
fn read_fixed_value(&self, fc: &FixedColumnMeta, row: usize) -> Value {
match fc.col_type {
ColType::Int64 => Value::Int64(self.read_i64(&fc.data, row)),
ColType::Float64 => Value::Float64(self.read_f64(&fc.data, row)),
ColType::UniqueId => Value::UniqueId(self.read_u32(&fc.data, row)),
ColType::Bool => Value::Boolean(self.read_u8(&fc.data, row) != 0),
ColType::Date => {
let days = self.read_i32(&fc.data, row);
let date = UNIX_EPOCH_DATE + chrono::Duration::days(days as i64);
Value::DateTime(date)
}
ColType::Str => unreachable!("string columns use ColRef::Str"),
}
}
pub fn id_borrowed(&self, row_id: u32) -> Option<crate::datatypes::values::BorrowedValue<'_>> {
use crate::datatypes::values::BorrowedValue;
if row_id >= self.row_count {
return None;
}
let row = row_id as usize;
if self.id_is_string {
let sc = self.id_str.as_ref()?;
if self.read_null(&sc.nulls, row) {
return None;
}
Some(BorrowedValue::String(self.read_str(
&sc.data,
&sc.offsets,
row,
)))
} else {
let fc = self.id_fixed.as_ref()?;
if self.read_null(&fc.nulls, row) {
return None;
}
Some(BorrowedValue::UniqueId(self.read_u32(&fc.data, row)))
}
}
pub fn title_borrowed(&self, row_id: u32) -> Option<&str> {
if row_id >= self.row_count {
return None;
}
let row = row_id as usize;
if self.title.nulls.len == 0 && self.title.data.len == 0 {
return None;
}
if self.read_null(&self.title.nulls, row) {
return None;
}
Some(self.read_str(&self.title.data, &self.title.offsets, row))
}
pub fn try_for_each_property_borrowed<F, E>(&self, row_id: u32, mut f: F) -> Result<(), E>
where
F: FnMut(InternedKey, crate::datatypes::values::BorrowedValue<'_>) -> Result<(), E>,
{
use crate::datatypes::values::BorrowedValue;
if row_id >= self.row_count {
return Ok(());
}
let row = row_id as usize;
for (&key, col_ref) in &self.col_map {
match col_ref {
ColRef::Fixed(idx) => {
let fc = &self.fixed_cols[*idx];
if !self.read_null(&fc.nulls, row) {
let bv = match fc.col_type {
ColType::Int64 => BorrowedValue::Int64(self.read_i64(&fc.data, row)),
ColType::Float64 => {
BorrowedValue::Float64(self.read_f64(&fc.data, row))
}
ColType::UniqueId => {
BorrowedValue::UniqueId(self.read_u32(&fc.data, row))
}
ColType::Bool => {
BorrowedValue::Boolean(self.read_u8(&fc.data, row) != 0)
}
ColType::Date => {
let days = self.read_i32(&fc.data, row);
BorrowedValue::DateTime(
UNIX_EPOCH_DATE + chrono::Duration::days(days as i64),
)
}
ColType::Str => unreachable!("string columns use ColRef::Str"),
};
f(key, bv)?;
}
}
ColRef::Str(idx) => {
let sc = &self.str_cols[*idx];
if !self.read_null(&sc.nulls, row) {
let s = self.read_str(&sc.data, &sc.offsets, row);
f(key, BorrowedValue::String(s))?;
}
}
}
}
if let Some(blob) = self.overflow_blob(row_id) {
crate::graph::storage::overflow::try_for_each_borrowed(blob, f)?;
}
Ok(())
}
pub fn row_properties(&self, row_id: u32) -> Vec<(InternedKey, Value)> {
if row_id >= self.row_count {
return Vec::new();
}
let row = row_id as usize;
let mut result = Vec::new();
for (&key, col_ref) in &self.col_map {
match col_ref {
ColRef::Fixed(idx) => {
let fc = &self.fixed_cols[*idx];
if !self.read_null(&fc.nulls, row) {
result.push((key, self.read_fixed_value(fc, row)));
}
}
ColRef::Str(idx) => {
let sc = &self.str_cols[*idx];
if !self.read_null(&sc.nulls, row) {
let s = self.read_str(&sc.data, &sc.offsets, row);
result.push((key, Value::String(s.to_string())));
}
}
}
}
result.extend(self.overflow_row_properties(row_id));
result
}
}
impl MmapColumnStore {
pub fn get_overflow_property(&self, row_id: u32, key: InternedKey) -> Option<Value> {
let blob = self.overflow_blob(row_id)?;
crate::graph::storage::overflow::scan_blob(blob, key)
}
fn overflow_row_properties(&self, row_id: u32) -> Vec<(InternedKey, Value)> {
match self.overflow_blob(row_id) {
Some(blob) => crate::graph::storage::overflow::decode_blob(blob),
None => Vec::new(),
}
}
fn overflow_blob(&self, row_id: u32) -> Option<&[u8]> {
if !self.has_overflow {
return None;
}
let idx = row_id as usize;
let expected_len = (self.row_count as usize + 1) * 8;
if self.overflow_offsets.len < expected_len {
return None;
}
let start = self.read_u64(&self.overflow_offsets, idx) as usize;
let end = self.read_u64(&self.overflow_offsets, idx + 1) as usize;
if start >= end || end > self.overflow_data.len {
return None;
}
Some(&self.mmap[self.overflow_data.offset + start..self.overflow_data.offset + end])
}
}
#[cfg(test)]
mod tests {
use super::*;
use memmap2::MmapMut;
fn store_with_titles(titles: &[&str]) -> (MmapColumnStore, usize) {
let data_bytes: Vec<u8> = titles.iter().flat_map(|t| t.bytes()).collect();
let mut offsets: Vec<u8> = Vec::new();
let mut end = 0u64;
for t in titles {
end += t.len() as u64;
offsets.extend_from_slice(&end.to_le_bytes());
}
let nulls = vec![0u8; titles.len()];
let total = data_bytes.len() + offsets.len() + nulls.len();
let mut mmap = MmapMut::map_anon(total.max(1)).unwrap();
mmap[..data_bytes.len()].copy_from_slice(&data_bytes);
mmap[data_bytes.len()..data_bytes.len() + offsets.len()].copy_from_slice(&offsets);
mmap[data_bytes.len() + offsets.len()..total].copy_from_slice(&nulls);
let store = MmapColumnStore {
mmap: Arc::new(mmap),
row_count: titles.len() as u32,
id_is_string: false,
id_fixed: None,
id_str: None,
title: StrColumnMeta {
data: Region {
offset: 0,
len: data_bytes.len(),
},
offsets: Region {
offset: data_bytes.len(),
len: offsets.len(),
},
nulls: Region {
offset: data_bytes.len() + offsets.len(),
len: titles.len(),
},
},
col_map: HashMap::new(),
fixed_cols: Vec::new(),
str_cols: Vec::new(),
overflow_offsets: Region::EMPTY,
overflow_data: Region::EMPTY,
has_overflow: false,
};
(store, data_bytes.len())
}
#[test]
fn validate_utf8_accepts_valid_store() {
let (store, _) = store_with_titles(&["Zebra", "blåbær", "日本語"]);
store.validate_utf8("T").unwrap();
assert_eq!(store.title_borrowed(1), Some("blåbær"));
}
#[test]
fn validate_utf8_rejects_invalid_bytes() {
let (mut store, _) = store_with_titles(&["Zebra", "Fjord"]);
{
let m = Arc::get_mut(&mut store.mmap).unwrap();
m[2] = 0xFF;
}
let err = store.validate_utf8("T").unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
assert!(err.to_string().contains("invalid UTF-8"), "{err}");
}
#[test]
fn validate_utf8_rejects_offset_splitting_code_point() {
let (mut store, data_len) = store_with_titles(&["blåbær", "xyz"]);
{
let m = Arc::get_mut(&mut store.mmap).unwrap();
m[data_len..data_len + 8].copy_from_slice(&3u64.to_le_bytes());
}
let err = store.validate_utf8("T").unwrap_err();
assert!(
err.to_string().contains("splits a UTF-8 code point"),
"{err}"
);
}
#[test]
fn validate_utf8_rejects_out_of_range_offset() {
let (mut store, data_len) = store_with_titles(&["abc", "def"]);
{
let m = Arc::get_mut(&mut store.mmap).unwrap();
m[data_len + 8..data_len + 16].copy_from_slice(&999u64.to_le_bytes());
}
let err = store.validate_utf8("T").unwrap_err();
assert!(err.to_string().contains("out-of-range"), "{err}");
}
#[test]
fn validate_utf8_rejects_region_past_file() {
let (mut store, _) = store_with_titles(&["abc"]);
store.title.data.len = 1 << 20; let err = store.validate_utf8("T").unwrap_err();
assert!(err.to_string().contains("extends past the file"), "{err}");
}
}