use std::path::Path;
use crate::compression::CompressionType;
use crate::encoding::EncodingType;
use crate::persistence::{section, write_header_to};
use linkrs_core::{StorageError, StorageResult};
use super::super::core::VertexTable;
use super::encoding_select::COLUMNS_FORMAT_VERSION;
impl VertexTable {
pub fn flush<P: AsRef<Path>>(
&mut self,
path: P,
compression: crate::compression::CompressionType,
) -> StorageResult<()> {
use std::fs;
let path = path.as_ref();
fs::create_dir_all(path)?;
crate::compression::cleanup_shadow_files(path)?;
let CompressionType::Zstd { level } = compression;
let page_size = crate::compression::DEFAULT_PAGE_SIZE;
let meta_path = path.join("meta.bin");
let meta_payload = self.build_meta_payload()?;
Self::write_pages_to_file(&meta_path, &meta_payload, page_size, level, 1)?;
let id_indexer_path = path.join("id_indexer.bin");
self.flush_id_indexer_baseline(path, &id_indexer_path)?;
let columns_path = path.join("columns.bin");
let selections = self.flush_columns(&columns_path)?;
self.columns.flush_evict_snapshots(path)?;
for (name, encoding_type) in &selections {
if *encoding_type != EncodingType::None {
self.columns.apply_encoding_to_column(
name,
*encoding_type,
self.encoding_selector.thresholds().fsst_max_symbols,
)?;
}
}
let timestamps_path = path.join("timestamps.bin");
self.flush_timestamps(×tamps_path)?;
self.clear_dirty();
Ok(())
}
pub(super) fn build_meta_payload(&self) -> StorageResult<Vec<u8>> {
let mut buf = Vec::new();
write_header_to(&mut buf, section::VERTEX_META)
.map_err(|e| StorageError::io_error(format!("Failed to write meta header: {}", e)))?;
let label_bytes = self.label.to_le_bytes();
let label_name_bytes = self.label_name.as_bytes();
let label_name_len = label_name_bytes.len() as u32;
buf.extend_from_slice(&label_bytes);
buf.extend_from_slice(&label_name_len.to_le_bytes());
buf.extend_from_slice(label_name_bytes);
let schema_json = serde_json::to_string(&self.schema)
.map_err(|e| StorageError::serialize_error(e.to_string()))?;
let schema_bytes = schema_json.as_bytes();
buf.extend_from_slice(&(schema_bytes.len() as u32).to_le_bytes());
buf.extend_from_slice(schema_bytes);
Ok(buf)
}
pub(super) fn flush_id_indexer(&self, path: &Path) -> StorageResult<()> {
let mut payload = Vec::new();
write_header_to(&mut payload, section::VERTEX_ID_INDEXER).map_err(|e| {
StorageError::io_error(format!("Failed to write id_indexer header: {}", e))
})?;
let index_data = self.id_indexer.serialize();
payload.extend_from_slice(&index_data);
let page_size = crate::compression::DEFAULT_PAGE_SIZE;
let total_rows = self.id_indexer.len() as u32;
Self::write_pages_to_file(path, &payload, page_size, 3, total_rows)
}
pub(super) fn flush_id_indexer_baseline(&self, dir: &Path, path: &Path) -> StorageResult<()> {
self.flush_id_indexer(path)?;
let delta_path = dir.join("id_indexer.delta");
if delta_path.exists() {
std::fs::remove_file(&delta_path)?;
}
self.id_indexer.clear_index_delta();
Ok(())
}
pub(super) fn flush_id_indexer_delta(&self, path: &Path) -> StorageResult<()> {
let mut payload = Vec::new();
write_header_to(&mut payload, section::VERTEX_ID_INDEXER_DELTA).map_err(|e| {
StorageError::io_error(format!("Failed to write id_indexer delta header: {}", e))
})?;
let delta_data = self.id_indexer.serialize_delta();
payload.extend_from_slice(&delta_data);
let page_size = crate::compression::DEFAULT_PAGE_SIZE;
let total_rows = self.id_indexer.delta_len() as u32;
Self::write_pages_to_file(path, &payload, page_size, 3, total_rows)
}
fn flush_columns(
&mut self,
path: &Path,
) -> StorageResult<Vec<(String, crate::encoding::EncodingType)>> {
let mut payload = Vec::new();
write_header_to(&mut payload, section::VERTEX_COLUMNS).map_err(|e| {
StorageError::io_error(format!("Failed to write columns header: {}", e))
})?;
let column_count = self.columns.column_count() as u32;
payload.extend_from_slice(&column_count.to_le_bytes());
payload.push(COLUMNS_FORMAT_VERSION);
let col_names: Vec<String> = self.columns.column_names();
let mut selections = Vec::with_capacity(col_names.len());
for name in &col_names {
let snapshot = self
.columns
.get_column(name)
.map(|col| col.clone())
.ok_or_else(|| StorageError::column_not_found(name.clone()))?;
snapshot.rebuild_overflow()?;
match snapshot.ensure_all_resident() {
Ok(0) => {}
Ok(loaded) => {
log::debug!("flush promoted {} evicted chunks", loaded);
}
Err(e) => {
return Err(StorageError::io_error(format!(
"flush chunk promotion failed for column {}: {}",
name, e
)));
}
}
let selection = snapshot.select_encoding(&self.encoding_selector);
if selection != EncodingType::None {
snapshot.apply_selected_encoding(
selection,
self.encoding_selector.thresholds().fsst_max_symbols,
)?;
if let Ok(stats) = snapshot.compute_stats() {
log::debug!(
"flush column={} encoding={:?} ratio={:.2}% savings={:.2}% raw={} compressed={}",
name,
selection,
stats.compression_ratio() * 100.0,
stats.space_savings() * 100.0,
stats.raw_size,
stats.compressed_size,
);
let family: crate::encoding::DataTypeFamily =
crate::encoding::data_type_family(&snapshot.data_type);
self.encoding_selector.record_compression_result_for(
selection,
family,
stats.compression_ratio(),
);
if self
.encoding_selector
.should_reencode_for(selection, family)
{
log::debug!(
"column={} encoding={:?} avg_ratio={:.2} exceeds threshold, \
future flushes keep re-evaluating selection",
name,
selection,
self.encoding_selector.thresholds().reencode_threshold,
);
}
}
}
Self::append_column_payload(&mut payload, &snapshot)?;
if crate::vertex::column::overflow::OverflowStore::routes_for(&snapshot.data_type)
&& snapshot.has_overflow()
{
let bytes = snapshot.serialize_overflow().map_err(|e| {
StorageError::serialize_error(format!(
"failed to serialize overflow for {}: {}",
name, e
))
})?;
let sidecar = path
.parent()
.unwrap_or(Path::new("."))
.join(format!("{}.overflow", name));
crate::compression::write_shadow_file(&sidecar, &bytes).map_err(|e| {
StorageError::io_error(format!(
"failed to write overflow sidecar for {}: {}",
name, e
))
})?;
}
selections.push((name.clone(), selection));
}
let page_size = crate::compression::DEFAULT_PAGE_SIZE;
let total_rows = self.columns.row_count() as u32;
Self::write_pages_to_file(path, &payload, page_size, 3, total_rows)?;
Ok(selections)
}
fn append_column_payload(
payload: &mut Vec<u8>,
col: &crate::vertex::column::Column,
) -> StorageResult<()> {
let name_bytes = col.name.as_bytes();
payload.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
payload.extend_from_slice(name_bytes);
let views = col.chunk_flush_views();
payload.extend_from_slice(&(views.len() as u32).to_le_bytes());
for view in &views {
payload.extend_from_slice(&(view.row_offset as u32).to_le_bytes());
payload.extend_from_slice(&(view.row_count as u32).to_le_bytes());
if view.raw_form {
payload.push(0u8);
payload.extend_from_slice(&(view.raw_data.len() as u32).to_le_bytes());
payload.extend_from_slice(&view.raw_data);
payload.extend_from_slice(&(view.raw_offsets.len() as u32).to_le_bytes());
for &off in &view.raw_offsets {
payload.extend_from_slice(&off.to_le_bytes());
}
match &view.raw_bitmap {
Some(bitmap) => {
payload.push(1u8);
let bitmap_bytes = bitmap.as_raw_slice();
payload.extend_from_slice(&(bitmap.len() as u32).to_le_bytes());
payload.extend_from_slice(&(bitmap_bytes.len() as u32).to_le_bytes());
payload.extend_from_slice(bitmap_bytes);
}
None => payload.push(0u8),
}
} else {
payload.push(1u8);
let mut meta_buf = Vec::new();
view.encoding_meta.serialize(&mut meta_buf)?;
payload.extend_from_slice(&(meta_buf.len() as u32).to_le_bytes());
payload.extend_from_slice(&meta_buf);
let mut enc_buf = Vec::new();
view.encoding.serialize_meta(&mut enc_buf)?;
payload.extend_from_slice(&(enc_buf.len() as u32).to_le_bytes());
payload.extend_from_slice(&enc_buf);
}
let overlay_bytes = postcard::to_allocvec(&view.overlay)
.map_err(|e| StorageError::serialize_error(e.to_string()))?;
payload.extend_from_slice(&(overlay_bytes.len() as u32).to_le_bytes());
payload.extend_from_slice(&overlay_bytes);
}
let overflow_present = (col.has_overflow()
&& crate::vertex::column::overflow::OverflowStore::routes_for(&col.data_type))
as u8;
payload.push(overflow_present);
Self::write_stats_with_fallback(payload, col);
Ok(())
}
fn write_stats_with_fallback(payload: &mut Vec<u8>, col: &crate::vertex::column::Column) {
match col.compute_stats() {
Ok(stats) => match stats.serialize_meta(&mut Vec::new()) {
Ok(_) => {
let mut stats_buf = Vec::new();
let _ = stats.serialize_meta(&mut stats_buf);
payload.push(1u8);
payload.extend_from_slice(&(stats_buf.len() as u32).to_le_bytes());
payload.extend_from_slice(&stats_buf);
}
Err(e) => {
log::warn!(
"column {} stats not representable, skipping stats: {}",
col.name,
e
);
payload.push(0u8);
}
},
Err(e) => {
log::warn!("column {} stats failed, skipping stats: {}", col.name, e);
payload.push(0u8);
}
}
}
pub(super) fn flush_timestamps(&self, path: &Path) -> StorageResult<()> {
let mut payload = Vec::new();
write_header_to(&mut payload, section::VERTEX_TIMESTAMPS).map_err(|e| {
StorageError::io_error(format!("Failed to write timestamps header: {}", e))
})?;
let timestamps = self.timestamps.read().dump();
let count = timestamps.len() as u32;
payload.extend_from_slice(&count.to_le_bytes());
for ts in timestamps {
payload.extend_from_slice(&ts.to_le_bytes());
}
let page_size = crate::compression::DEFAULT_PAGE_SIZE;
Self::write_pages_to_file(path, &payload, page_size, 3, count)
}
}