use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::ingest::{LogEntry, TimeRange};
#[derive(Debug, thiserror::Error)]
pub enum IntervalParseError {
#[error("invalid interval format: '{input}' — expected format like '1h', '3d', '1w'")]
InvalidFormat { input: String },
#[error("invalid number in interval: '{input}'")]
InvalidNumber { input: String },
#[error("partition interval must be > 0")]
ZeroInterval,
#[error("unknown unit '{unit}': expected s, m, h, d, w, M, y")]
UnknownUnit { unit: String },
#[error("unsupported calendar interval '{input}': {hint}")]
UnsupportedCalendar { input: String, hint: &'static str },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PartitionState {
Active,
Sealed,
Merging,
Merged,
Deleted,
Archived,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PartitionMeta {
pub min_ts: i64,
pub max_ts: i64,
pub row_count: u64,
pub size_bytes: u64,
pub schema_version: u32,
pub state: PartitionState,
pub interval_ms: u64,
pub last_flushed_wal_lsn: u64,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub column_stats: HashMap<String, nodedb_codec::ColumnStatistics>,
}
impl PartitionMeta {
pub fn overlaps(&self, range: &TimeRange) -> bool {
self.min_ts <= range.end_ms && range.start_ms <= self.max_ts
}
pub fn is_queryable(&self) -> bool {
matches!(
self.state,
PartitionState::Active | PartitionState::Sealed | PartitionState::Merged
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PartitionInterval {
Duration(u64),
Month,
Year,
Unbounded,
Auto,
}
impl PartitionInterval {
pub fn parse(s: &str) -> Result<Self, IntervalParseError> {
let s = s.trim();
match s.to_uppercase().as_str() {
"AUTO" => return Ok(Self::Auto),
"UNBOUNDED" | "NONE" => return Ok(Self::Unbounded),
_ => {}
}
if s.ends_with('M') && s.len() > 1 && s[..s.len() - 1].chars().all(|c| c.is_ascii_digit()) {
let n: u64 = s[..s.len() - 1]
.parse()
.map_err(|_| IntervalParseError::InvalidNumber { input: s.into() })?;
if n != 1 {
return Err(IntervalParseError::UnsupportedCalendar {
input: s.into(),
hint: "only '1M' (one calendar month) is supported",
});
}
return Ok(Self::Month);
}
if s.ends_with('y') && s.len() > 1 && s[..s.len() - 1].chars().all(|c| c.is_ascii_digit()) {
let n: u64 = s[..s.len() - 1]
.parse()
.map_err(|_| IntervalParseError::InvalidNumber { input: s.into() })?;
if n != 1 {
return Err(IntervalParseError::UnsupportedCalendar {
input: s.into(),
hint: "only '1y' (one calendar year) is supported",
});
}
return Ok(Self::Year);
}
let (num_str, unit) = if s.len() > 1 && s.as_bytes()[s.len() - 1].is_ascii_alphabetic() {
(&s[..s.len() - 1], &s[s.len() - 1..])
} else {
return Err(IntervalParseError::InvalidFormat { input: s.into() });
};
let n: u64 = num_str
.parse()
.map_err(|_| IntervalParseError::InvalidNumber { input: s.into() })?;
if n == 0 {
return Err(IntervalParseError::ZeroInterval);
}
let ms = match unit {
"s" => n * 1_000,
"m" => n * 60_000,
"h" => n * 3_600_000,
"d" => n * 86_400_000,
"w" => n * 604_800_000,
_ => {
return Err(IntervalParseError::UnknownUnit { unit: unit.into() });
}
};
Ok(Self::Duration(ms))
}
pub fn as_millis(&self) -> Option<u64> {
match self {
Self::Duration(ms) => Some(*ms),
_ => None,
}
}
}
impl std::fmt::Display for PartitionInterval {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Duration(ms) => {
if *ms % 604_800_000 == 0 {
write!(f, "{}w", ms / 604_800_000)
} else if *ms % 86_400_000 == 0 {
write!(f, "{}d", ms / 86_400_000)
} else if *ms % 3_600_000 == 0 {
write!(f, "{}h", ms / 3_600_000)
} else if *ms % 60_000 == 0 {
write!(f, "{}m", ms / 60_000)
} else {
write!(f, "{}s", ms / 1_000)
}
}
Self::Month => write!(f, "1M"),
Self::Year => write!(f, "1y"),
Self::Unbounded => write!(f, "UNBOUNDED"),
Self::Auto => write!(f, "AUTO"),
}
}
}
#[derive(Debug)]
pub struct FlushedSeries {
pub series_id: super::series::SeriesId,
pub kind: FlushedKind,
pub min_ts: i64,
pub max_ts: i64,
}
#[derive(Debug)]
pub enum FlushedKind {
Metric {
gorilla_block: Vec<u8>,
sample_count: u64,
},
Log {
entries: Vec<LogEntry>,
total_bytes: usize,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SegmentRef {
pub path: String,
pub min_ts: i64,
pub max_ts: i64,
pub kind: SegmentKind,
pub size_bytes: u64,
pub created_at_ms: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SegmentKind {
Metric,
Log,
}