use std::collections::BTreeMap;
use chronix_core::SeriesKey;
use crate::error::{DbError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DeleteOutcome {
pub series_tombstoned: u64,
pub segments_skipped: u64,
}
impl DeleteOutcome {
#[must_use]
pub const fn is_complete(&self) -> bool {
self.segments_skipped == 0
}
}
#[derive(Debug, Clone)]
pub struct DeleteRequest {
pub measurement: String,
pub tag_filters: Vec<(String, String)>,
pub time_start: Option<i64>,
pub time_end: Option<i64>,
}
impl DeleteRequest {
#[must_use]
pub fn effective_range(&self) -> (i64, i64) {
(
self.time_start.unwrap_or(i64::MIN),
self.time_end.unwrap_or(i64::MAX),
)
}
pub fn series_key(&self) -> Result<Option<SeriesKey>> {
if self.tag_filters.is_empty() {
return Ok(None);
}
let tags: BTreeMap<String, String> = self.tag_filters.iter().cloned().collect();
let key = SeriesKey::new(&self.measurement, tags)
.map_err(|e| DbError::InvalidRequest(format!("invalid series key: {e}")))?;
Ok(Some(key))
}
}
#[derive(Debug, Default)]
pub struct DeleteBuilder {
measurement: Option<String>,
tag_filters: Vec<(String, String)>,
time_start: Option<i64>,
time_end: Option<i64>,
}
impl DeleteBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn measurement(mut self, name: &str) -> Self {
self.measurement = Some(name.to_string());
self
}
#[must_use]
pub fn tag(mut self, key: &str, value: &str) -> Self {
self.tag_filters.push((key.to_string(), value.to_string()));
self
}
#[must_use]
pub fn range(mut self, start: i64, end: i64) -> Self {
self.time_start = Some(start);
self.time_end = Some(end);
self
}
#[must_use]
pub fn after(mut self, start: i64) -> Self {
self.time_start = Some(start);
self
}
#[must_use]
pub fn before(mut self, end: i64) -> Self {
self.time_end = Some(end);
self
}
pub fn build(self) -> Result<DeleteRequest> {
let measurement = self
.measurement
.ok_or_else(|| DbError::Internal("delete requires a measurement".into()))?;
Ok(DeleteRequest {
measurement,
tag_filters: self.tag_filters,
time_start: self.time_start,
time_end: self.time_end,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_full() {
let req = DeleteBuilder::new()
.measurement("cpu")
.tag("host", "srv-1")
.tag("region", "us-east")
.range(1000, 5000)
.build()
.unwrap();
assert_eq!(req.measurement, "cpu");
assert_eq!(req.tag_filters.len(), 2);
assert_eq!(req.effective_range(), (1000, 5000));
}
#[test]
fn builder_no_tags() {
let req = DeleteBuilder::new()
.measurement("cpu")
.range(0, i64::MAX)
.build()
.unwrap();
assert!(req.series_key().unwrap().is_none());
}
#[test]
fn builder_missing_measurement_errors() {
let result = DeleteBuilder::new().tag("host", "srv-1").build();
assert!(result.is_err());
}
#[test]
fn builder_series_key_from_tags() {
let req = DeleteBuilder::new()
.measurement("cpu")
.tag("host", "srv-1")
.build()
.unwrap();
let key = req.series_key().unwrap().unwrap();
assert_eq!(key.measurement(), "cpu");
}
#[test]
fn effective_range_defaults() {
let req = DeleteBuilder::new().measurement("cpu").build().unwrap();
assert_eq!(req.effective_range(), (i64::MIN, i64::MAX));
}
#[test]
fn builder_after_only() {
let req = DeleteBuilder::new()
.measurement("cpu")
.after(5000)
.build()
.unwrap();
assert_eq!(req.effective_range(), (5000, i64::MAX));
}
}