use std::collections::HashMap;
use std::path::{Path, PathBuf};
use nodedb_types::timeseries::{PartitionMeta, PartitionState};
use nodedb_wal::crypto::WalEncryptionKey;
use super::super::columnar_memtable::{ColumnType, ColumnarFlushView, ColumnarSchema};
use super::codec::encode_column;
use super::encrypt::encrypt_file;
use super::error::SegmentError;
use super::schema::schema_to_json;
use super::util::dir_size;
pub struct ColumnarSegmentWriter {
base_dir: PathBuf,
}
impl ColumnarSegmentWriter {
pub fn new(base_dir: impl Into<PathBuf>) -> Self {
Self {
base_dir: base_dir.into(),
}
}
pub fn write_partition(
&self,
partition_name: &str,
view: &ColumnarFlushView<'_>,
interval_ms: u64,
flush_wal_lsn: u64,
kek: Option<&WalEncryptionKey>,
) -> Result<PartitionMeta, SegmentError> {
let partition_dir = self.base_dir.join(partition_name);
std::fs::create_dir_all(&partition_dir)
.map_err(|e| SegmentError::Io(format!("create dir: {e}")))?;
let mut column_stats = HashMap::new();
let mut resolved_codecs = Vec::with_capacity(view.schema.columns.len());
for (i, (col_name, col_type)) in view.schema.columns.iter().enumerate() {
let col_data = &view.columns[i];
let requested_codec = view.schema.codec(i);
let (encoded, resolved_codec, stats) =
encode_column(col_data, *col_type, requested_codec)?;
let file_bytes = maybe_encrypt(kek, &encoded)?;
durable_write(&partition_dir.join(format!("{col_name}.col")), &file_bytes)?;
if *col_type == ColumnType::Symbol
&& let Some(dict) = view.symbol_dicts.get(&i)
{
let dict_json = sonic_rs::to_vec(dict)
.map_err(|e| SegmentError::Io(format!("serialize dict: {e}")))?;
let sym_bytes = maybe_encrypt(kek, &dict_json)?;
durable_write(&partition_dir.join(format!("{col_name}.sym")), &sym_bytes)?;
}
column_stats.insert(col_name.clone(), stats);
resolved_codecs.push(resolved_codec);
}
let schema_with_codecs = ColumnarSchema {
columns: view.schema.columns.clone(),
timestamp_idx: view.schema.timestamp_idx,
codecs: resolved_codecs
.iter()
.map(|c| c.into_column_codec())
.collect(),
};
let schema_json = sonic_rs::to_vec(&schema_to_json(&schema_with_codecs))
.map_err(|e| SegmentError::Io(format!("serialize schema: {e}")))?;
let schema_bytes = maybe_encrypt(kek, &schema_json)?;
durable_write(&partition_dir.join("schema.json"), &schema_bytes)?;
let sparse_idx = super::super::sparse_index::SparseIndex::build(
view.columns,
view.schema,
view.row_count,
super::super::sparse_index::DEFAULT_BLOCK_SIZE,
);
let sparse_bytes = sparse_idx.to_bytes();
let sparse_file_bytes = maybe_encrypt(kek, &sparse_bytes)?;
durable_write(&partition_dir.join("sparse_index.bin"), &sparse_file_bytes)?;
let size_bytes = dir_size(&partition_dir)?;
let meta = PartitionMeta {
min_ts: view.min_ts,
max_ts: view.max_ts,
row_count: view.row_count,
size_bytes,
schema_version: 1,
state: PartitionState::Sealed,
interval_ms,
last_flushed_wal_lsn: flush_wal_lsn,
column_stats,
max_system_ts: view.max_system_ts,
};
let meta_json = sonic_rs::to_vec(&meta)
.map_err(|e| SegmentError::Io(format!("serialize meta: {e}")))?;
let meta_bytes = maybe_encrypt(kek, &meta_json)?;
durable_write(&partition_dir.join("partition.meta"), &meta_bytes)?;
nodedb_wal::segment::fsync_directory(&self.base_dir)
.map_err(|e| SegmentError::Io(format!("fsync {}: {e}", self.base_dir.display())))?;
Ok(meta)
}
}
fn durable_write(path: &Path, bytes: &[u8]) -> Result<(), SegmentError> {
let mut tmp = path.to_path_buf();
let ext = path
.extension()
.map(|e| e.to_string_lossy().into_owned())
.unwrap_or_default();
tmp.set_extension(format!("{ext}.tmp"));
nodedb_wal::segment::atomic_write_fsync(&tmp, path, bytes)
.map_err(|e| SegmentError::Io(format!("write {}: {e}", path.display())))
}
fn maybe_encrypt(kek: Option<&WalEncryptionKey>, bytes: &[u8]) -> Result<Vec<u8>, SegmentError> {
match kek {
Some(key) => encrypt_file(key, bytes),
None => Ok(bytes.to_vec()),
}
}
#[cfg(test)]
pub(super) fn file_is_encrypted(bytes: &[u8]) -> Result<bool, SegmentError> {
super::encrypt::is_encrypted(bytes)
}