use chronix_encoding::EncodingType;
use serde::{Deserialize, Serialize};
use crate::segment::error::{Result, SegmentError};
use crate::segment::stats::{ColumnStats, COLUMN_STATS_SIZE};
use crate::segment::to_array;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ColumnMeta {
pub name: String,
pub data_type: u8,
pub role: u8,
pub default_encoding: u8,
pub stats: ColumnStats,
#[serde(default)]
pub bloom_filter: Option<Vec<u8>>,
#[serde(default)]
pub encrypted: bool,
#[serde(default)]
pub key_id: Option<String>,
#[serde(default)]
pub row_group_blooms: Option<Vec<Vec<u8>>>,
}
pub mod data_types {
pub const TIMESTAMP: u8 = 0;
pub const STRING: u8 = 1;
pub const F64: u8 = 2;
pub const I64: u8 = 3;
pub const U64: u8 = 4;
pub const BOOL: u8 = 5;
}
pub mod roles {
pub const TIMESTAMP: u8 = 0;
pub const TAG: u8 = 1;
pub const FIELD: u8 = 2;
pub const ARROW_ROLE_KEY: &str = "role";
#[must_use]
pub fn arrow_metadata(role: u8) -> std::collections::HashMap<String, String> {
let name = match role {
TIMESTAMP => "timestamp",
TAG => "tag",
_ => "field",
};
[(ARROW_ROLE_KEY.to_string(), name.to_string())].into()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ColumnBlockMeta {
pub column_index: u16,
pub encoding: EncodingType,
pub compressed: bool,
pub offset: u64,
pub length: u32,
pub value_count: u32,
pub block_crc: u32,
pub encrypted: bool,
pub validity_offset: u64,
pub validity_length: u32,
pub stats: ColumnStats,
}
impl ColumnBlockMeta {
pub const SIZE: usize = 2 + 1 + 1 + 8 + 4 + 4 + 4 + 1 + 8 + 4 + COLUMN_STATS_SIZE;
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(Self::SIZE);
buf.extend_from_slice(&self.column_index.to_le_bytes());
buf.push(self.encoding.tag());
buf.push(u8::from(self.compressed));
buf.extend_from_slice(&self.offset.to_le_bytes());
buf.extend_from_slice(&self.length.to_le_bytes());
buf.extend_from_slice(&self.value_count.to_le_bytes());
buf.extend_from_slice(&self.block_crc.to_le_bytes());
buf.push(u8::from(self.encrypted));
buf.extend_from_slice(&self.validity_offset.to_le_bytes());
buf.extend_from_slice(&self.validity_length.to_le_bytes());
buf.extend_from_slice(&self.stats.to_bytes());
buf
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < Self::SIZE {
return Err(SegmentError::CorruptFile {
detail: format!(
"column block meta too short: expected {}, got {}",
Self::SIZE,
data.len()
),
});
}
let column_index = u16::from_le_bytes([data[0], data[1]]);
let encoding = EncodingType::from_tag(data[2]).map_err(|e| SegmentError::CorruptFile {
detail: format!("invalid encoding tag: {e}"),
})?;
let compressed = data[3] != 0;
let offset = u64::from_le_bytes(to_array!(data[4..12], "block meta offset")?);
let length = u32::from_le_bytes(to_array!(data[12..16], "block meta length")?);
let value_count = u32::from_le_bytes(to_array!(data[16..20], "block meta value_count")?);
let block_crc = u32::from_le_bytes(to_array!(data[20..24], "block meta block_crc")?);
let encrypted = data[24] != 0;
let validity_offset =
u64::from_le_bytes(to_array!(data[25..33], "block meta validity_offset")?);
let validity_length =
u32::from_le_bytes(to_array!(data[33..37], "block meta validity_length")?);
let stats = ColumnStats::from_bytes(&data[37..])?;
Ok(Self {
column_index,
encoding,
compressed,
offset,
length,
value_count,
block_crc,
encrypted,
validity_offset,
validity_length,
stats,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SegmentMetadata {
pub columns: Vec<ColumnMeta>,
pub row_group_blocks: Vec<Vec<ColumnBlockMeta>>,
pub zstd_dictionary: Option<Vec<u8>>,
}
impl SegmentMetadata {
pub fn to_bytes(&self) -> Result<Vec<u8>> {
let encoded =
postcard::to_stdvec(&self.columns).map_err(|e| SegmentError::CorruptFile {
detail: format!("column metadata serialization failed: {e}"),
})?;
let mut buf = Vec::new();
buf.extend_from_slice(&(encoded.len() as u32).to_le_bytes());
buf.extend_from_slice(&encoded);
buf.extend_from_slice(&(self.row_group_blocks.len() as u32).to_le_bytes());
for rg_blocks in &self.row_group_blocks {
buf.extend_from_slice(&(rg_blocks.len() as u16).to_le_bytes());
for block in rg_blocks {
buf.extend_from_slice(&block.to_bytes());
}
}
if let Some(dict) = &self.zstd_dictionary {
buf.extend_from_slice(&(dict.len() as u32).to_le_bytes());
buf.extend_from_slice(dict);
} else {
buf.extend_from_slice(&0u32.to_le_bytes());
}
Ok(buf)
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < 4 {
return Err(SegmentError::CorruptFile {
detail: "metadata section too short".to_string(),
});
}
let json_len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let mut offset = 4;
if data.len() < offset + json_len {
return Err(SegmentError::CorruptFile {
detail: "metadata column section truncated".to_string(),
});
}
let columns: Vec<ColumnMeta> = postcard::from_bytes(&data[offset..offset + json_len])
.map_err(|e| SegmentError::CorruptFile {
detail: format!("invalid column metadata: {e}"),
})?;
offset += json_len;
if data.len() < offset + 4 {
return Err(SegmentError::CorruptFile {
detail: "metadata missing row group count".to_string(),
});
}
let rg_count = u32::from_le_bytes(to_array!(
data[offset..offset + 4],
"metadata row group count"
)?) as usize;
offset += 4;
let remaining = data.len().saturating_sub(offset);
if rg_count > remaining / 2 {
return Err(SegmentError::CorruptFile {
detail: format!(
"row group count ({rg_count}) exceeds available metadata ({remaining} bytes)"
),
});
}
let mut row_group_blocks = Vec::with_capacity(rg_count);
for _ in 0..rg_count {
if data.len() < offset + 2 {
return Err(SegmentError::CorruptFile {
detail: "metadata missing block count".to_string(),
});
}
let block_count = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
offset += 2;
let mut blocks = Vec::with_capacity(block_count);
for _ in 0..block_count {
let block = ColumnBlockMeta::from_bytes(&data[offset..])?;
offset += ColumnBlockMeta::SIZE;
blocks.push(block);
}
row_group_blocks.push(blocks);
}
let zstd_dictionary = if data.len() >= offset + 4 {
let dict_len = u32::from_le_bytes(to_array!(
data[offset..offset + 4],
"metadata zstd_dict_len"
)?) as usize;
offset += 4;
if dict_len > 0 {
if data.len() < offset + dict_len {
return Err(SegmentError::CorruptFile {
detail: "metadata zstd dictionary truncated".to_string(),
});
}
let dict = data[offset..offset + dict_len].to_vec();
Some(dict)
} else {
None
}
} else {
None
};
Ok(Self {
columns,
row_group_blocks,
zstd_dictionary,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn column_block_meta_roundtrip() {
let meta = ColumnBlockMeta {
column_index: 3,
encoding: EncodingType::Chimp,
compressed: true,
offset: 1024,
length: 512,
value_count: 1000,
block_crc: 0xDEAD_BEEF,
encrypted: false,
validity_offset: 0,
validity_length: 0,
stats: ColumnStats::empty(),
};
let bytes = meta.to_bytes();
assert_eq!(bytes.len(), ColumnBlockMeta::SIZE);
let recovered = ColumnBlockMeta::from_bytes(&bytes).unwrap();
assert_eq!(meta, recovered);
}
#[test]
fn segment_metadata_roundtrip() {
let meta = SegmentMetadata {
columns: vec![
ColumnMeta {
name: chronix_core::TIME_COLUMN.to_string(),
data_type: data_types::TIMESTAMP,
role: roles::TIMESTAMP,
default_encoding: EncodingType::DeltaOfDelta.tag(),
stats: ColumnStats::empty(),
bloom_filter: None,
encrypted: false,
key_id: None,
row_group_blooms: None,
},
ColumnMeta {
name: "host".to_string(),
data_type: data_types::STRING,
role: roles::TAG,
default_encoding: EncodingType::Dictionary.tag(),
stats: ColumnStats::empty(),
bloom_filter: None,
encrypted: false,
key_id: None,
row_group_blooms: None,
},
ColumnMeta {
name: "cpu".to_string(),
data_type: data_types::F64,
role: roles::FIELD,
default_encoding: EncodingType::Chimp.tag(),
stats: ColumnStats::empty(),
bloom_filter: None,
encrypted: false,
key_id: None,
row_group_blooms: None,
},
],
row_group_blocks: vec![vec![
ColumnBlockMeta {
column_index: 0,
encoding: EncodingType::DeltaOfDelta,
compressed: false,
offset: 48,
length: 200,
value_count: 1000,
block_crc: 0,
encrypted: false,
validity_offset: 0,
validity_length: 0,
stats: ColumnStats::empty(),
},
ColumnBlockMeta {
column_index: 1,
encoding: EncodingType::Dictionary,
compressed: true,
offset: 248,
length: 50,
value_count: 1000,
block_crc: 0,
encrypted: false,
validity_offset: 0,
validity_length: 0,
stats: ColumnStats::empty(),
},
]],
zstd_dictionary: None,
};
let bytes = meta.to_bytes().unwrap();
let recovered = SegmentMetadata::from_bytes(&bytes).unwrap();
assert_eq!(meta, recovered);
}
#[test]
fn corrupt_metadata_detected() {
assert!(SegmentMetadata::from_bytes(&[0; 2]).is_err());
}
}