use crate::foundation::{AlgoError, Result};
use crate::store::layout::{Layout, MAGIC};
use crate::store::schema::{Dtype, LaneKind, StoreSchema};
use memmap2::MmapMut;
use std::fs::OpenOptions;
use std::path::Path;
#[derive(Debug)]
pub struct StoreWriter {
schema: StoreSchema,
layout: Layout,
mmap: MmapMut,
flush_behind: bool,
}
impl StoreWriter {
pub fn create(path: &Path, schema: StoreSchema) -> Result<Self> {
schema.validate()?;
let header = serde_json::to_vec(&schema).map_err(|e| AlgoError::Parse(e.to_string()))?;
let layout = Layout::compute(&schema, header.len() as u64);
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(path)?;
file.set_len(layout.total_len)?;
#[allow(unsafe_code)]
let mut mmap = unsafe { MmapMut::map_mut(&file)? };
mmap[0..4].copy_from_slice(&MAGIC);
mmap[4..8].copy_from_slice(&(header.len() as u32).to_le_bytes());
mmap[8..8 + header.len()].copy_from_slice(&header);
Ok(StoreWriter {
schema,
layout,
mmap,
flush_behind: false,
})
}
pub fn with_flush_behind(mut self, on: bool) -> Self {
self.flush_behind = on;
self
}
pub fn flush_behind_enabled(&self) -> bool {
self.flush_behind
}
pub fn schema(&self) -> &StoreSchema {
&self.schema
}
fn slab_byte_range(&self, idx: usize, slab: u64) -> Result<(usize, usize)> {
let lane = &self.schema.lanes[idx];
let elems_per_slab = match lane.kind {
LaneKind::Slab { elems_per_slab } => elems_per_slab,
LaneKind::Flat { .. } => {
return Err(AlgoError::InvalidArgument(format!(
"lane '{}' is flat — use write_flat_*/flat_mut_*",
lane.name
)))
}
};
if slab >= self.schema.nslabs {
return Err(AlgoError::InvalidArgument(format!(
"slab {slab} out of range (nslabs = {})",
self.schema.nslabs
)));
}
let size = lane.dtype.size() as u64;
let start = self.layout.lanes[idx].offset + slab * elems_per_slab * size;
let len = elems_per_slab * size;
Ok((start as usize, len as usize))
}
fn slab_region_mut(&mut self, name: &str, slab: u64, dtype: Dtype) -> Result<&mut [u8]> {
let idx = self.schema.lane_index(name)?;
if self.schema.lanes[idx].dtype != dtype {
return Err(AlgoError::InvalidArgument(format!(
"lane '{name}' is {}, not {}",
self.schema.lanes[idx].dtype.as_str(),
dtype.as_str()
)));
}
let (start, len) = self.slab_byte_range(idx, slab)?;
Ok(&mut self.mmap[start..start + len])
}
pub fn flush_behind_slab(&mut self, name: &str, slab: u64) -> Result<()> {
let idx = self.schema.lane_index(name)?;
let (off, len) = self.slab_byte_range(idx, slab)?;
self.evict_range(off, len)
}
fn evict_range(&mut self, offset: usize, len: usize) -> Result<()> {
if len == 0 {
return Ok(());
}
self.mmap.flush_range(offset, len)?;
#[cfg(unix)]
{
#[allow(unsafe_code)]
unsafe {
self.mmap.unchecked_advise_range(
memmap2::UncheckedAdvice::DontNeed,
offset,
len,
)?;
}
}
Ok(())
}
fn flat_region_mut(&mut self, name: &str, dtype: Dtype) -> Result<&mut [u8]> {
let idx = self.schema.lane_index(name)?;
let lane = &self.schema.lanes[idx];
if lane.dtype != dtype {
return Err(AlgoError::InvalidArgument(format!(
"lane '{name}' is {}, not {}",
lane.dtype.as_str(),
dtype.as_str()
)));
}
if !matches!(lane.kind, LaneKind::Flat { .. }) {
return Err(AlgoError::InvalidArgument(format!(
"lane '{name}' is a slab lane — use write_slab_*/slab_mut_*"
)));
}
let ext = self.layout.lanes[idx];
Ok(&mut self.mmap[ext.offset as usize..(ext.offset + ext.byte_len) as usize])
}
pub fn flush(&mut self) -> Result<()> {
self.mmap.flush()?;
Ok(())
}
pub fn finalize(mut self) -> Result<()> {
let nslabs = self.schema.nslabs;
self.layout.write_seal(&mut self.mmap, nslabs);
self.mmap.flush()?;
Ok(())
}
}
macro_rules! typed_writers {
($ty:ty, $variant:expr,
$write_slab:ident, $slab_mut:ident, $write_flat:ident, $flat_mut:ident) => {
impl StoreWriter {
#[doc = concat!("Write one k-slab of a `", stringify!($ty), "` lane. `data.len()` must equal the lane's `elems_per_slab`.")]
pub fn $write_slab(&mut self, name: &str, slab: u64, data: &[$ty]) -> Result<()> {
let bytes: &[u8] = bytemuck::cast_slice(data);
let region = self.slab_region_mut(name, slab, $variant)?;
if bytes.len() != region.len() {
return Err(AlgoError::InvalidArgument(format!(
"lane '{name}' slab expects {} elements, got {}",
region.len() / std::mem::size_of::<$ty>(),
data.len()
)));
}
region.copy_from_slice(bytes);
if self.flush_behind {
self.flush_behind_slab(name, slab)?;
}
Ok(())
}
#[doc = concat!("A mutable, zero-copy `", stringify!($ty), "` view of one k-slab (fill it in place — the streaming write path).")]
pub fn $slab_mut(&mut self, name: &str, slab: u64) -> Result<&mut [$ty]> {
let region = self.slab_region_mut(name, slab, $variant)?;
bytemuck::try_cast_slice_mut(region)
.map_err(|e| AlgoError::InvalidArgument(format!("lane '{name}': {e}")))
}
#[doc = concat!("Write a whole flat `", stringify!($ty), "` lane. `data.len()` must equal the lane's `len`.")]
pub fn $write_flat(&mut self, name: &str, data: &[$ty]) -> Result<()> {
let bytes: &[u8] = bytemuck::cast_slice(data);
let region = self.flat_region_mut(name, $variant)?;
if bytes.len() != region.len() {
return Err(AlgoError::InvalidArgument(format!(
"flat lane '{name}' expects {} elements, got {}",
region.len() / std::mem::size_of::<$ty>(),
data.len()
)));
}
region.copy_from_slice(bytes);
Ok(())
}
#[doc = concat!("A mutable, zero-copy `", stringify!($ty), "` view of a whole flat lane.")]
pub fn $flat_mut(&mut self, name: &str) -> Result<&mut [$ty]> {
let region = self.flat_region_mut(name, $variant)?;
bytemuck::try_cast_slice_mut(region)
.map_err(|e| AlgoError::InvalidArgument(format!("lane '{name}': {e}")))
}
}
};
}
typed_writers!(
f32,
Dtype::F32,
write_slab_f32,
slab_mut_f32,
write_flat_f32,
flat_mut_f32
);
typed_writers!(
f64,
Dtype::F64,
write_slab_f64,
slab_mut_f64,
write_flat_f64,
flat_mut_f64
);
typed_writers!(
u32,
Dtype::U32,
write_slab_u32,
slab_mut_u32,
write_flat_u32,
flat_mut_u32
);
typed_writers!(
u16,
Dtype::U16,
write_slab_u16,
slab_mut_u16,
write_flat_u16,
flat_mut_u16
);