use clap::ValueEnum;
use serde_json::{Value, json};
use time::format_description::well_known::Rfc3339;
use crate::error::{Error, Result};
use crate::session::system::TableStatus;
use crate::session::{QueryDescription, QueryResult};
use crate::tiering::ArchivalOutcome;
use crate::tiering::store::SnapshotInfo;
use crate::watermark::Tier;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
pub enum Format {
#[default]
Table,
Json,
}
fn emit(format: Format, json: &Value, table: impl FnOnce()) -> Result<()> {
match format {
Format::Json => {
println!(
"{}",
serde_json::to_string_pretty(json).map_err(Error::Json)?
);
Ok(())
}
Format::Table => {
table();
Ok(())
}
}
}
fn instant(at: time::OffsetDateTime) -> String {
at.format(&Rfc3339).unwrap_or_else(|_| at.to_string())
}
pub fn status(rows: &[TableStatus], format: Format) -> Result<()> {
let document = json!({
"tables": rows
.iter()
.map(|r| json!({
"table": r.table,
"watermark": instant(r.watermark),
"watermark_lag_seconds": r.watermark_lag_seconds,
"hot_partitions": r.hot_partitions,
"partitions_ahead": r.partitions_ahead,
"invariant_violations": r.invariant_violations,
"healthy": r.healthy,
}))
.collect::<Vec<_>>(),
"healthy": rows.iter().all(|r| r.healthy),
});
emit(format, &document, || {
println!(
"{:<28} {:<26} {:>10} {:>10} {:>10} {:>10} HEALTH",
"TABLE", "WATERMARK", "LAG", "PARTS", "AHEAD", "STRANDED"
);
for r in rows {
println!(
"{:<28} {:<26} {:>10} {:>10} {:>10} {:>10} {}",
r.table,
instant(r.watermark),
fmt_lag(r.watermark_lag_seconds),
r.hot_partitions,
r.partitions_ahead,
r.invariant_violations,
match r.healthy {
true => "ok",
false => "DEGRADED",
},
);
}
if rows.iter().any(|r| r.invariant_violations > 0) {
println!(
"\nSTRANDED is non-zero: rows sit below the watermark in PostgreSQL, \
where no query looks. Query results may be wrong."
);
}
if rows
.iter()
.any(|r| r.hot_partitions > 0 && r.partitions_ahead <= 1)
{
println!(
"\nAHEAD is nearly exhausted: at zero, inserts fail outright. \
Archival pre-creates partitions — check that it is running."
);
}
})
}
#[derive(Debug, Clone)]
pub struct ArchiveLine {
pub table: String,
pub windows: usize,
pub rows: u64,
pub orphans_reclaimed: usize,
pub lease_contended: bool,
pub deferred: bool,
pub watermark: time::OffsetDateTime,
}
impl ArchiveLine {
pub fn of(table: &str, outcomes: &[ArchivalOutcome]) -> Self {
Self {
table: table.to_string(),
windows: outcomes.iter().filter(|o| o.archived_anything()).count(),
rows: outcomes.iter().map(|o| o.rows).sum(),
orphans_reclaimed: outcomes.iter().map(|o| o.orphans_reclaimed).sum(),
lease_contended: outcomes.iter().any(|o| o.lease_contended),
deferred: outcomes.iter().any(|o| o.deferred),
watermark: outcomes
.last()
.map_or(time::OffsetDateTime::UNIX_EPOCH, |o| o.watermark.get()),
}
}
}
pub fn archive(lines: &[ArchiveLine], format: Format) -> Result<()> {
let document = json!({
"tables": lines
.iter()
.map(|l| json!({
"table": l.table,
"windows": l.windows,
"rows": l.rows,
"orphans_reclaimed": l.orphans_reclaimed,
"lease_contended": l.lease_contended,
"deferred": l.deferred,
"watermark": instant(l.watermark),
}))
.collect::<Vec<_>>(),
"rows": lines.iter().map(|l| l.rows).sum::<u64>(),
});
emit(format, &document, || {
println!(
"{:<28} {:>8} {:>12} {:<26} NOTE",
"TABLE", "WINDOWS", "ROWS", "WATERMARK"
);
for l in lines {
println!(
"{:<28} {:>8} {:>12} {:<26} {}",
l.table,
l.windows,
l.rows,
instant(l.watermark),
match (l.lease_contended, l.deferred, l.orphans_reclaimed) {
(true, _, _) => "another archiver holds the lease".to_string(),
(_, true, _) => "deferred: a lock was not available".to_string(),
(_, _, n) if n > 0 => format!("reclaimed {n} orphaned partition(s)"),
_ => String::new(),
},
);
}
})
}
pub fn query(result: &QueryResult, format: Format) -> Result<()> {
let document = json!({
"rows": result.to_json()?,
"row_count": result.num_rows(),
"provenance": provenance(
result.watermarks(),
result.tiers_scanned(),
result.touched_hot_tier(),
),
});
emit(format, &document, || {
match crate::arrow::util::pretty::pretty_format_batches(result.batches()) {
Ok(rendered) => println!("{rendered}"),
Err(e) => println!("(cannot render {} row(s): {e})", result.num_rows()),
}
println!();
print_provenance(
result.watermarks(),
result.tiers_scanned(),
result.touched_hot_tier(),
);
})
}
pub fn describe(described: &QueryDescription, format: Format) -> Result<()> {
let document = json!({
"schema": described
.schema()
.fields()
.iter()
.map(|f| json!({ "name": f.name(), "type": f.data_type().to_string() }))
.collect::<Vec<_>>(),
"provenance": provenance(
described.watermarks(),
described.tiers_scanned(),
described.touched_hot_tier(),
),
});
emit(format, &document, || {
println!("{:<28} TYPE", "COLUMN");
for field in described.schema().fields() {
println!("{:<28} {}", field.name(), field.data_type());
}
println!();
print_provenance(
described.watermarks(),
described.tiers_scanned(),
described.touched_hot_tier(),
);
})
}
pub fn snapshots(rows: &[(String, SnapshotInfo)], format: Format) -> Result<()> {
let document = json!({
"snapshots": rows
.iter()
.map(|(table, s)| json!({
"table": table,
"snapshot_id": s.snapshot_id,
"committed_at": instant(s.committed_at),
"watermark": s.watermark.map(|w| instant(w.get())),
"rows": s.rows,
}))
.collect::<Vec<_>>(),
});
emit(format, &document, || {
println!(
"{:<28} {:>21} {:<26} {:<26} {:>12}",
"TABLE", "SNAPSHOT", "COMMITTED", "WATERMARK", "ROWS"
);
for (table, s) in rows {
println!(
"{:<28} {:>21} {:<26} {:<26} {:>12}",
table,
s.snapshot_id,
instant(s.committed_at),
s.watermark
.map_or_else(|| "— (foreign commit)".to_string(), |w| instant(w.get())),
s.rows.map_or_else(|| "—".to_string(), |n| n.to_string()),
);
}
})
}
fn provenance(
watermarks: &[(String, crate::watermark::TieringWatermark)],
tiers: &[Tier],
touched_hot: bool,
) -> Value {
json!({
"watermarks": watermarks
.iter()
.map(|(table, w)| json!({ "table": table, "watermark": instant(w.get()) }))
.collect::<Vec<_>>(),
"tiers_scanned": tiers.iter().map(tier_name).collect::<Vec<_>>(),
"reproducible": !touched_hot,
})
}
fn print_provenance(
watermarks: &[(String, crate::watermark::TieringWatermark)],
tiers: &[Tier],
touched_hot: bool,
) {
for (table, w) in watermarks {
println!("boundary {table}: {}", instant(w.get()));
}
println!(
"tiers {}",
match tiers.is_empty() {
true => "none (the range matched nothing)".to_string(),
false => tiers
.iter()
.map(|t| tier_name(t).to_string())
.collect::<Vec<_>>()
.join(" + "),
}
);
println!(
"{}",
match touched_hot {
true =>
"warning this answer includes the hot window, so it is only valid \
for now — those intervals are still being corrected",
false => "reproducible cold tier only: this answer does not change",
}
);
}
const fn tier_name(tier: &Tier) -> &'static str {
match tier {
Tier::Cold => "cold",
Tier::Hot => "hot",
}
}
fn fmt_lag(seconds: i64) -> String {
let (n, unit) = match seconds.abs() {
s if s >= 86_400 => (s / 86_400, "d"),
s if s >= 3_600 => (s / 3_600, "h"),
s if s >= 60 => (s / 60, "m"),
s => (s, "s"),
};
format!("{n}{unit}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_lag_is_reported_in_a_unit_a_person_can_read() {
assert_eq!(fmt_lag(45), "45s");
assert_eq!(fmt_lag(900), "15m");
assert_eq!(fmt_lag(7_200), "2h");
assert_eq!(fmt_lag(691_200), "8d");
assert_eq!(fmt_lag(-30), "30s");
}
#[test]
fn provenance_says_whether_an_answer_reproduces() {
let watermark =
crate::watermark::TieringWatermark::new(time::macros::datetime!(2026-07-20 00:00 UTC));
let marks = vec![("readings_versions".to_string(), watermark)];
let spanning = provenance(&marks, &[Tier::Cold, Tier::Hot], true);
assert_eq!(spanning["reproducible"], json!(false));
assert_eq!(spanning["tiers_scanned"], json!(["cold", "hot"]));
let settled = provenance(&marks, &[Tier::Cold], false);
assert_eq!(settled["reproducible"], json!(true));
assert_eq!(
settled["watermarks"][0]["watermark"],
json!("2026-07-20T00:00:00Z")
);
}
#[test]
fn an_archive_line_folds_every_window_of_a_catch_up() {
use crate::watermark::{ArchivalWindow, TieringWatermark};
let window = |day: i64| {
let from = time::macros::datetime!(2026-07-20 00:00 UTC) + time::Duration::days(day);
ArchivalWindow::new(from, from + time::Duration::DAY).unwrap()
};
let outcome = |day: i64, rows: u64| ArchivalOutcome {
window: Some(window(day)),
rows,
watermark: window(day).resulting_watermark(),
orphans_reclaimed: 0,
partitions_created: 0,
lease_contended: false,
deferred: false,
};
let line = ArchiveLine::of("readings_versions", &[outcome(0, 96), outcome(1, 100)]);
assert_eq!(line.windows, 2);
assert_eq!(line.rows, 196);
assert_eq!(
line.watermark,
time::macros::datetime!(2026-07-22 00:00 UTC)
);
let idle = ArchivalOutcome {
window: None,
rows: 0,
watermark: TieringWatermark::empty(),
orphans_reclaimed: 0,
partitions_created: 0,
lease_contended: false,
deferred: true,
};
let line = ArchiveLine::of("readings_versions", &[idle]);
assert_eq!(line.windows, 0);
assert!(line.deferred);
}
}