use std::collections::BTreeMap;
use datafusion::prelude::SessionContext;
use time::OffsetDateTime;
use crate::error::{Error, Result};
use super::query::QueryResult;
use super::store::{MeterStore, MeterStoreBuilder};
pub struct MeterCatalog {
ctx: SessionContext,
stores: BTreeMap<String, MeterStore>,
}
impl std::fmt::Debug for MeterCatalog {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MeterCatalog")
.field("tables", &self.stores.keys().collect::<Vec<_>>())
.finish_non_exhaustive()
}
}
impl MeterCatalog {
pub fn builder() -> MeterCatalogBuilder {
MeterCatalogBuilder::default()
}
pub fn context(&self) -> &SessionContext {
&self.ctx
}
pub fn table(&self, name: &str) -> Option<&MeterStore> {
self.stores.get(name).or_else(|| {
self.stores
.values()
.find(|s| super::store::resolved_name(s.config().name()) == name)
})
}
pub async fn isolated(&self, name: &str) -> Result<MeterStore> {
let store = self.table(name).ok_or_else(|| {
Error::config(format!(
"this catalog holds no table {name:?}: it holds [{}]",
self.stores
.keys()
.map(String::as_str)
.collect::<Vec<_>>()
.join(", ")
))
})?;
store.in_own_session().await
}
pub fn tables(&self) -> impl Iterator<Item = &MeterStore> {
self.stores.values()
}
pub fn len(&self) -> usize {
self.stores.len()
}
pub fn is_empty(&self) -> bool {
self.stores.is_empty()
}
pub async fn query(&self, sql: &str) -> Result<QueryResult> {
self.query_with_params(sql, Vec::new()).await
}
pub async fn query_with_params(
&self,
sql: &str,
params: Vec<datafusion::scalar::ScalarValue>,
) -> Result<QueryResult> {
let (first, watermarks) = self.watermarks_for(sql).await?;
first.run(sql, params, watermarks).await
}
async fn watermarks_for(
&self,
sql: &str,
) -> Result<(
&MeterStore,
Vec<(String, crate::watermark::TieringWatermark)>,
)> {
let first = self
.stores
.values()
.next()
.ok_or_else(|| Error::config("catalog hosts no tables"))?;
let plan = self
.ctx
.state()
.create_logical_plan(sql)
.await
.map_err(Error::from)?;
let scanned = scanned_relations(&plan);
let mut watermarks = Vec::with_capacity(self.stores.len());
for (name, store) in &self.stores {
let touched =
scanned.contains(&store.raw_table()) || scanned.contains(&store.resolved_table());
if touched {
watermarks.push((name.clone(), store.watermark().await?));
}
}
Ok((first, watermarks))
}
pub async fn describe(&self, sql: &str) -> Result<super::QueryDescription> {
let (first, watermarks) = self.watermarks_for(sql).await?;
first.describe_with(sql, watermarks).await
}
pub async fn stream(
&self,
sql: &str,
) -> Result<(
super::QueryDescription,
datafusion::execution::SendableRecordBatchStream,
)> {
self.stream_with_params(sql, Vec::new()).await
}
pub async fn stream_with_params(
&self,
sql: &str,
params: Vec<datafusion::scalar::ScalarValue>,
) -> Result<(
super::QueryDescription,
datafusion::execution::SendableRecordBatchStream,
)> {
let (first, watermarks) = self.watermarks_for(sql).await?;
first.stream_at(sql, params, watermarks).await
}
pub fn maintenance(&self) -> super::Maintenance {
super::Maintenance::over(self.stores.values().cloned().collect())
}
pub async fn create_tables(&self) -> Result<()> {
for store in self.stores.values() {
store.create_tables().await?;
}
Ok(())
}
pub async fn refresh_system_tables(&self, now: OffsetDateTime) -> Result<()> {
let views: Vec<super::system::SystemTables<'_>> = self
.stores
.values()
.map(|s| super::system::SystemTables::new(s.hot_store(), s.cold_store(), s.config()))
.collect();
super::system::register_all(&self.ctx, &views, now).await
}
pub async fn status(&self, now: OffsetDateTime) -> Result<Vec<super::system::TableStatus>> {
let mut out = Vec::with_capacity(self.stores.len());
for store in self.stores.values() {
out.push(store.status(now).await?);
}
Ok(out)
}
pub async fn archive_all(
&self,
now: OffsetDateTime,
max_windows: usize,
) -> Result<Vec<(String, Vec<crate::tiering::archive::ArchivalOutcome>)>> {
let mut out = Vec::with_capacity(self.stores.len());
for (name, store) in &self.stores {
let outcome = store
.archive(now, max_windows)
.await
.map_err(|e| Error::Storage(format!("archiving {name}: {e}")))?;
out.push((name.clone(), outcome));
}
Ok(out)
}
pub async fn verify_invariant(&self) -> Result<Vec<(String, String)>> {
let mut out = Vec::new();
for (name, store) in &self.stores {
if let Err(e) = store.verify_invariant().await {
match e {
Error::InvariantViolated { detail, .. } => out.push((name.clone(), detail)),
other => return Err(other),
}
}
}
Ok(out)
}
}
fn scanned_relations(
plan: &datafusion::logical_expr::LogicalPlan,
) -> std::collections::HashSet<String> {
use datafusion::common::tree_node::TreeNodeRecursion;
use datafusion::logical_expr::LogicalPlan;
let mut found = std::collections::HashSet::new();
let _ = plan.apply_with_subqueries(|node| {
if let LogicalPlan::TableScan(scan) = node {
found.insert(scan.table_name.table().to_string());
}
Ok(TreeNodeRecursion::Continue)
});
found
}
#[async_trait::async_trait]
impl super::SqlSurface for MeterCatalog {
fn label(&self) -> String {
self.stores
.values()
.map(MeterStore::resolved_table)
.collect::<Vec<_>>()
.join(", ")
}
async fn describe_sql(&self, sql: &str) -> Result<super::QueryDescription> {
self.describe(sql).await
}
async fn stream_sql(
&self,
sql: &str,
params: Vec<datafusion::scalar::ScalarValue>,
) -> Result<(
super::QueryDescription,
datafusion::execution::SendableRecordBatchStream,
)> {
self.stream_with_params(sql, params).await
}
}
#[derive(Default)]
pub struct MeterCatalogBuilder {
tables: Vec<MeterStoreBuilder>,
}
impl std::fmt::Debug for MeterCatalogBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MeterCatalogBuilder")
.field("tables", &self.tables.len())
.finish()
}
}
impl MeterCatalogBuilder {
pub fn table(mut self, table: MeterStoreBuilder) -> Self {
self.tables.push(table);
self
}
pub async fn build(self) -> Result<MeterCatalog> {
if self.tables.is_empty() {
return Err(Error::config(
"a catalog needs at least one table: an empty one can answer no query \
and hides the missing configuration until the first request",
));
}
let ctx = SessionContext::new_with_config(
datafusion::prelude::SessionConfig::new().with_information_schema(true),
);
let mut seen: BTreeMap<String, String> = BTreeMap::new();
for builder in &self.tables {
let configured = builder.table_name().ok_or_else(|| {
Error::config("every table in a catalog needs a table configuration")
})?;
let (raw, resolved) = builder
.registered_names()
.expect("a builder with a table name has registered names");
for relation in [raw, resolved] {
if let Some(owner) = seen.get(&relation) {
return Err(Error::config(format!(
"tables {owner:?} and {configured:?} both register the relation \
{relation:?}: only one of them could answer to it, and a query \
naming it would silently read whichever won"
)));
}
seen.insert(relation, configured.to_string());
}
}
let mut stores = BTreeMap::new();
for builder in self.tables {
let store = builder.session(ctx.clone()).build().await?;
stores.insert(store.config().name().to_string(), store);
}
let modes: Vec<(&str, _)> = stores
.values()
.map(|s| (s.config().name(), s.read_mode()))
.collect();
let uniform = modes.windows(2).all(|w| w[0].1 == w[1].1);
if !uniform {
return Err(Error::config(format!(
"a catalog's tables must share one read mode, but these are {modes:?}: \
a result reports the single mode its statement ran under, and mixing \
them would attribute a figure half-computed from the mutable tier to \
a mode that excludes it"
)));
}
Ok(MeterCatalog { ctx, stores })
}
}