#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::sync::Arc;
use std::time::Duration;
use chronix::cold_archive::ArchiveConfig;
use chronix::prelude::*;
use chronix::{Chronix, fields, tags};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let tmp = tempfile::tempdir()?;
let archive_dir = tmp.path().join("archive");
std::fs::create_dir_all(&archive_dir)?;
let config = ChronixConfig::builder()
.data_dir(tmp.path().join("hot"))
.build()?;
let db = Arc::new(Chronix::open(config)?);
let now_ns = i64::try_from(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_nanos(),
)?;
let year_ago = now_ns - 365 * 86_400 * 1_000_000_000;
for (label, base) in [("old", year_ago), ("recent", now_ns - 60_000_000_000)] {
let points: Vec<Point> = (0..2_000)
.map(|i| {
let host = if i % 2 == 0 { "meter-a" } else { "meter-b" };
let key = SeriesKey::new("power", tags! { "host" => host }).unwrap();
Point::new(
key,
fields! { "watts" => 200.0 + f64::from(i % 50) * 0.25 },
base + i64::from(i),
)
.unwrap()
})
.collect();
assert!(db.insert_batch(&points)?.is_complete());
db.flush()?; println!("wrote 2000 {label} readings");
}
let hot_rows = |db: &Chronix| -> usize {
let plan = db.query().measurement("power").build().unwrap();
db.execute_iter(&plan)
.unwrap()
.map(|b| b.unwrap().num_rows())
.sum()
};
println!("hot tier : {} rows", hot_rows(&db));
let archive_url = format!("file://{}/", archive_dir.display());
let outcome = db
.archive_cold_segments(&ArchiveConfig {
cold_after: Duration::from_secs(30 * 86_400),
remote_url: archive_url.clone(),
..Default::default()
})
.await?;
println!(
"archived : {} object(s) from {} segment(s), {} rows, {} bytes, {} left hot",
outcome.objects, outcome.segments, outcome.rows, outcome.bytes, outcome.failed
);
println!(
"hot tier : {} rows (archived rows have left)",
hot_rows(&db)
);
let cold_object = find_one_parquet(&archive_dir).expect("a .parquet object was written");
let bytes = std::fs::read(&cold_object)?;
println!(
"cold object : {} ({} bytes, magic {:?})",
cold_object.file_name().unwrap().to_string_lossy(),
bytes.len(),
std::str::from_utf8(&bytes[..4]).unwrap_or("??"),
);
let reader = parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder::try_new(
std::fs::File::open(&cold_object)?,
)?
.build()?;
let mut rows = 0usize;
let mut columns = Vec::new();
for batch in reader {
let batch = batch?;
if columns.is_empty() {
columns = batch
.schema()
.fields()
.iter()
.map(|f| f.name().clone())
.collect();
}
rows += batch.num_rows();
}
println!("stock reader : {rows} rows, columns {columns:?}");
assert_eq!(rows, 2_000, "every archived row survives the re-encode");
let ctx = datafusion::prelude::SessionContext::new();
chronix::sql::cold_tier::register_cold_tier(&ctx, &archive_url, "power", "power_archive")
.await?;
println!("\nSELECT host, count(*), avg(watts) FROM power_archive GROUP BY host:");
ctx.sql(
"SELECT host, count(*) AS n, round(avg(watts), 2) AS avg_w \
FROM power_archive GROUP BY host ORDER BY host",
)
.await?
.show()
.await?;
println!("\narchive dir : {}", archive_dir.display());
println!("\nPoint DuckDB or Polars at that directory — it is ordinary Parquet.");
db.close()?;
Ok(())
}
fn find_one_parquet(dir: &std::path::Path) -> Option<std::path::PathBuf> {
for entry in std::fs::read_dir(dir).ok()? {
let path = entry.ok()?.path();
if path.is_dir() {
if let Some(found) = find_one_parquet(&path) {
return Some(found);
}
} else if path.extension().is_some_and(|e| e == "parquet") {
return Some(path);
}
}
None
}