use std::sync::Arc;
use datafusion::datasource::MemTable;
use datafusion::prelude::SessionContext;
use time::OffsetDateTime;
use crate::arrow::array::{
BooleanArray, Int64Array, RecordBatch, StringArray, TimestampMicrosecondArray,
};
use crate::arrow::datatypes::{DataType, Field, Schema, SchemaRef, TimeUnit};
use crate::config::ValidatedTableConfig;
use crate::error::{Error, Result};
use crate::tiering::store::{ColdStore, HotStore};
pub const SCHEMA: &str = "system";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableStatus {
pub table: String,
pub watermark: OffsetDateTime,
pub watermark_lag_seconds: i64,
pub hot_partitions: i64,
pub partitions_ahead: i64,
pub invariant_violations: i64,
pub healthy: bool,
}
fn status_schema() -> SchemaRef {
Arc::new(Schema::new(vec![
Field::new("table", DataType::Utf8, false),
Field::new(
"watermark",
DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
false,
),
Field::new("watermark_lag_seconds", DataType::Int64, false),
Field::new("hot_partitions", DataType::Int64, false),
Field::new("partitions_ahead", DataType::Int64, false),
Field::new("invariant_violations", DataType::Int64, false),
Field::new("healthy", DataType::Boolean, false),
]))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigEntry {
pub table: String,
pub setting: String,
pub value: String,
}
fn config_schema() -> SchemaRef {
Arc::new(Schema::new(vec![
Field::new("table", DataType::Utf8, false),
Field::new("setting", DataType::Utf8, false),
Field::new("value", DataType::Utf8, false),
]))
}
pub struct SystemTables<'a> {
hot: &'a Arc<dyn HotStore>,
cold: &'a Arc<dyn ColdStore>,
config: &'a ValidatedTableConfig,
}
impl<'a> SystemTables<'a> {
pub fn new(
hot: &'a Arc<dyn HotStore>,
cold: &'a Arc<dyn ColdStore>,
config: &'a ValidatedTableConfig,
) -> Self {
Self { hot, cold, config }
}
pub async fn status(&self, now: OffsetDateTime) -> Result<TableStatus> {
let table = self.config.name();
let watermark = self.cold.watermark(table).await?;
let violations = self.hot.invariant_violations(table, watermark).await? as i64;
crate::observe::metrics()
.invariant_violations
.record(violations.max(0) as u64, &crate::observe::table(table));
let partitions = self.hot.partition_starts(table).await?;
let (hot_partitions, partitions_ahead) = match &partitions {
Some(starts) => (
starts.len() as i64,
crate::tiering::store::partitions_ahead(starts, now, self.config.partition_step())
as i64,
),
None => (-1, -1),
};
Ok(TableStatus {
table: table.to_string(),
watermark: watermark.get(),
watermark_lag_seconds: (now - watermark.get()).whole_seconds(),
hot_partitions,
partitions_ahead,
invariant_violations: violations,
healthy: violations == 0 && partitions_ahead != 0,
})
}
pub fn config_entries(&self) -> Vec<ConfigEntry> {
let c = self.config;
let entry = |setting: &str, value: String| ConfigEntry {
table: c.name().to_string(),
setting: setting.to_string(),
value,
};
vec![
entry("table", c.name().to_string()),
entry("merge_key", c.merge_key().join(", ")),
entry(
"partition_step",
format!("{}s", c.partition_step().whole_seconds()),
),
entry(
"archival_step",
format!("{}s", c.archival_step().whole_seconds()),
),
entry(
"settlement_lag",
format!("{}s", c.settlement_lag().whole_seconds()),
),
entry(
"partition_headroom",
format!("{}s", c.partition_headroom().whole_seconds()),
),
entry(
"expected_hot_partitions",
c.expected_hot_partitions().to_string(),
),
entry("target_file_size", c.target_file_size().to_string()),
entry("scan_chunk_rows", c.scan_chunk_rows().to_string()),
entry(
"identity_columns",
c.identity_columns()
.iter()
.map(|f| f.name().clone())
.collect::<Vec<_>>()
.join(", "),
),
]
}
pub fn resolution_entries(&self, raw_table: &str) -> Vec<ConfigEntry> {
let table = self.config.name().to_string();
vec![
ConfigEntry {
table: table.clone(),
setting: "raw_table".to_string(),
value: raw_table.to_string(),
},
ConfigEntry {
table: table.clone(),
setting: "resolution_sql".to_string(),
value: crate::planner::version::resolution_sql_with_key(
raw_table,
&self.config.merge_key(),
&self.config.extra_columns(),
None,
),
},
ConfigEntry {
table,
setting: "warning".to_string(),
value: format!(
"{raw_table} holds every version of every reading. Summing it without \
the SQL above double-counts every corrected interval."
),
},
]
}
pub async fn snapshot_entries(&self) -> Result<Vec<crate::tiering::store::SnapshotInfo>> {
self.cold.snapshots(self.config.name()).await
}
pub async fn register(&self, ctx: &SessionContext, now: OffsetDateTime) -> Result<()> {
register_all(ctx, std::slice::from_ref(self), now).await
}
}
pub async fn register_all(
ctx: &SessionContext,
tables: &[SystemTables<'_>],
now: OffsetDateTime,
) -> Result<()> {
use datafusion::catalog::MemorySchemaProvider;
use datafusion::catalog::SchemaProvider;
let catalog = ctx
.catalog("datafusion")
.ok_or_else(|| Error::Storage("default catalog missing".into()))?;
let schema = match catalog.schema(SCHEMA) {
Some(existing) => existing,
None => {
let created: Arc<dyn SchemaProvider> = Arc::new(MemorySchemaProvider::new());
catalog
.register_schema(SCHEMA, Arc::clone(&created))
.map_err(|e| Error::Storage(e.to_string()))?;
created
}
};
let mut statuses = Vec::with_capacity(tables.len());
let mut settings = Vec::new();
let mut resolution = Vec::new();
let mut snapshots = Vec::new();
for t in tables {
statuses.push(t.status(now).await?);
settings.extend(t.config_entries());
let raw = if t.config.name().ends_with("_versions") {
t.config.name().to_string()
} else {
format!("{}_versions", t.config.name())
};
resolution.extend(t.resolution_entries(&raw));
snapshots.push(snapshot_batch(
t.config.name(),
&t.snapshot_entries().await?,
)?);
}
let register = |name: &str, schema_ref: SchemaRef, batches: Vec<RecordBatch>| {
schema
.register_table(
name.to_string(),
Arc::new(MemTable::try_new(schema_ref, vec![batches])?),
)
.map_err(|e| Error::Storage(e.to_string()))?;
Ok::<(), Error>(())
};
register("tables", status_schema(), vec![status_batch(&statuses)?])?;
register("config", config_schema(), vec![config_batch(&settings)?])?;
register(
"resolution",
config_schema(),
vec![config_batch(&resolution)?],
)?;
register("snapshots", snapshot_schema(), snapshots)?;
Ok(())
}
fn snapshot_schema() -> SchemaRef {
Arc::new(Schema::new(vec![
Field::new("table", DataType::Utf8, false),
Field::new("snapshot_id", DataType::Int64, false),
Field::new(
"committed_at",
DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
false,
),
Field::new(
"watermark",
DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
true,
),
Field::new("rows", DataType::Int64, true),
Field::new("written_by_meterstore", DataType::Boolean, false),
]))
}
pub fn snapshot_batch(
table: &str,
rows: &[crate::tiering::store::SnapshotInfo],
) -> Result<RecordBatch> {
let micros = |t: OffsetDateTime| (t.unix_timestamp_nanos() / 1_000) as i64;
Ok(RecordBatch::try_new(
snapshot_schema(),
vec![
Arc::new(StringArray::from(vec![table; rows.len()])),
Arc::new(Int64Array::from(
rows.iter().map(|r| r.snapshot_id).collect::<Vec<_>>(),
)),
Arc::new(
TimestampMicrosecondArray::from(
rows.iter()
.map(|r| micros(r.committed_at))
.collect::<Vec<_>>(),
)
.with_timezone("UTC"),
),
Arc::new(
TimestampMicrosecondArray::from(
rows.iter()
.map(|r| r.watermark.map(|w| micros(w.get())))
.collect::<Vec<_>>(),
)
.with_timezone("UTC"),
),
Arc::new(Int64Array::from(
rows.iter()
.map(|r| r.rows.map(|n| i64::try_from(n).unwrap_or(i64::MAX)))
.collect::<Vec<_>>(),
)),
Arc::new(BooleanArray::from(
rows.iter()
.map(|r| r.watermark.is_some())
.collect::<Vec<_>>(),
)),
],
)?)
}
pub fn status_batch(rows: &[TableStatus]) -> Result<RecordBatch> {
let micros = |t: OffsetDateTime| (t.unix_timestamp_nanos() / 1_000) as i64;
Ok(RecordBatch::try_new(
status_schema(),
vec![
Arc::new(StringArray::from(
rows.iter().map(|r| r.table.as_str()).collect::<Vec<_>>(),
)),
Arc::new(
TimestampMicrosecondArray::from(
rows.iter().map(|r| micros(r.watermark)).collect::<Vec<_>>(),
)
.with_timezone("UTC"),
),
Arc::new(Int64Array::from(
rows.iter()
.map(|r| r.watermark_lag_seconds)
.collect::<Vec<_>>(),
)),
Arc::new(Int64Array::from(
rows.iter().map(|r| r.hot_partitions).collect::<Vec<_>>(),
)),
Arc::new(Int64Array::from(
rows.iter().map(|r| r.partitions_ahead).collect::<Vec<_>>(),
)),
Arc::new(Int64Array::from(
rows.iter()
.map(|r| r.invariant_violations)
.collect::<Vec<_>>(),
)),
Arc::new(BooleanArray::from(
rows.iter().map(|r| r.healthy).collect::<Vec<_>>(),
)),
],
)?)
}
pub fn config_batch(rows: &[ConfigEntry]) -> Result<RecordBatch> {
Ok(RecordBatch::try_new(
config_schema(),
vec![
Arc::new(StringArray::from(
rows.iter().map(|r| r.table.as_str()).collect::<Vec<_>>(),
)),
Arc::new(StringArray::from(
rows.iter().map(|r| r.setting.as_str()).collect::<Vec<_>>(),
)),
Arc::new(StringArray::from(
rows.iter().map(|r| r.value.as_str()).collect::<Vec<_>>(),
)),
],
)?)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::TableConfig;
use time::macros::datetime;
fn status(violations: i64) -> TableStatus {
TableStatus {
table: "readings_versions".to_string(),
watermark: datetime!(2026-07-20 00:00 UTC),
watermark_lag_seconds: 86_400,
hot_partitions: 21,
partitions_ahead: 14,
invariant_violations: violations,
healthy: violations == 0,
}
}
#[test]
fn a_status_batch_matches_its_schema() {
let batch = status_batch(&[status(0)]).unwrap();
assert_eq!(batch.schema(), status_schema());
assert_eq!(batch.num_rows(), 1);
}
#[test]
fn health_covers_both_ways_a_table_stops_working() {
assert!(status(0).healthy);
assert!(!status(1).healthy);
}
#[test]
fn the_write_runway_is_counted_from_partitions_that_exist() {
use crate::tiering::store::partitions_ahead;
use time::Duration;
let starts = [
datetime!(2026-07-18 00:00 UTC),
datetime!(2026-07-19 00:00 UTC),
datetime!(2026-07-20 00:00 UTC),
datetime!(2026-07-21 00:00 UTC),
];
assert_eq!(
partitions_ahead(&starts, datetime!(2026-07-20 13:47 UTC), Duration::DAY),
2
);
assert_eq!(
partitions_ahead(&starts, datetime!(2026-07-22 00:00 UTC), Duration::DAY),
0
);
assert_eq!(
partitions_ahead(&[], datetime!(2026-07-20 00:00 UTC), Duration::DAY),
0
);
}
#[test]
fn config_exposes_the_settings_that_interact() {
let config = TableConfig::new("readings").build().unwrap();
let hot: Arc<dyn HotStore> = Arc::new(NoStore);
let cold: Arc<dyn ColdStore> = Arc::new(NoStore);
let entries = SystemTables::new(&hot, &cold, &config).config_entries();
let names: Vec<_> = entries.iter().map(|e| e.setting.as_str()).collect();
for expected in [
"partition_step",
"archival_step",
"settlement_lag",
"merge_key",
"expected_hot_partitions",
] {
assert!(names.contains(&expected), "{expected} missing");
}
}
#[test]
fn config_reports_the_merge_key_including_identity_columns() {
let config = TableConfig::new("readings")
.identity_column(Field::new("tenant", DataType::Utf8, false))
.build()
.unwrap();
let hot: Arc<dyn HotStore> = Arc::new(NoStore);
let cold: Arc<dyn ColdStore> = Arc::new(NoStore);
let entries = SystemTables::new(&hot, &cold, &config).config_entries();
let key = entries.iter().find(|e| e.setting == "merge_key").unwrap();
assert!(key.value.contains("tenant"));
}
#[test]
fn a_config_batch_matches_its_schema() {
let batch = config_batch(&[ConfigEntry {
table: "readings_versions".to_string(),
setting: "x".into(),
value: "y".into(),
}])
.unwrap();
assert_eq!(batch.schema(), config_schema());
}
struct NoStore;
#[async_trait::async_trait]
impl HotStore for NoStore {
async fn append_reporting(
&self,
_: &str,
_: &[String],
_: &[RecordBatch],
) -> Result<Vec<crate::session::Displacement>> {
Ok(Vec::new())
}
async fn drop_table(&self, _: &str) -> Result<()> {
Ok(())
}
async fn create_tables(&self, _: &str, _: &[String], _: &[Field]) -> Result<()> {
unreachable!()
}
async fn append(&self, _: &str, _: &[String], _: &[RecordBatch]) -> Result<u64> {
unreachable!()
}
async fn scan_range(
&self,
_: &str,
_: crate::planner::TimeRange,
_: &crate::tiering::store::ScanSpec,
) -> Result<crate::tiering::store::BatchStream> {
unreachable!()
}
async fn ensure_partitions(
&self,
_: &str,
_: OffsetDateTime,
_: OffsetDateTime,
_: time::Duration,
) -> Result<Vec<crate::tiering::store::PartitionId>> {
unreachable!()
}
async fn partition_exists(&self, _: &crate::tiering::store::PartitionId) -> Result<bool> {
unreachable!()
}
async fn detach_partition(&self, _: &crate::tiering::store::PartitionId) -> Result<()> {
unreachable!()
}
async fn scan_detached(
&self,
_: &crate::tiering::store::PartitionId,
_: &crate::tiering::store::ScanSpec,
) -> Result<crate::tiering::store::BatchStream> {
unreachable!()
}
async fn drop_partition(&self, _: &crate::tiering::store::PartitionId) -> Result<()> {
unreachable!()
}
async fn orphaned_partitions(
&self,
_: &str,
) -> Result<Vec<crate::tiering::store::PartitionId>> {
unreachable!()
}
async fn invariant_violations(
&self,
_: &str,
_: crate::watermark::TieringWatermark,
) -> Result<u64> {
unreachable!()
}
}
#[async_trait::async_trait]
impl ColdStore for NoStore {
async fn purge_table(&self, _: &str) -> Result<()> {
Ok(())
}
async fn create_tables(&self, _: &str, _: &[String], _: &[Field]) -> Result<()> {
unreachable!()
}
async fn watermark(&self, _: &str) -> Result<crate::watermark::TieringWatermark> {
unreachable!()
}
async fn append_and_commit(
&self,
_: &str,
_: crate::tiering::store::BatchStream,
_: crate::tiering::store::WriteHints,
_: crate::watermark::ArchivalWindow,
) -> Result<crate::tiering::store::CommitInfo> {
unreachable!()
}
async fn append_only(
&self,
_: &str,
_: crate::tiering::store::BatchStream,
_: crate::tiering::store::WriteHints,
) -> Result<crate::tiering::store::CommitInfo> {
unreachable!()
}
async fn expire_snapshots(
&self,
_: &str,
_: time::Duration,
_: usize,
_: OffsetDateTime,
) -> Result<usize> {
unreachable!()
}
}
}