use std::any::Any;
use std::collections::HashSet;
use std::sync::{Arc, PoisonError, RwLock};
use std::time::Duration;
use datafusion::common::tree_node::{Transformed, TreeNode, TreeNodeRecursion};
use datafusion::datasource::{provider_as_source, source_as_provider};
use datafusion::error::Result as DfResult;
use datafusion::logical_expr::LogicalPlan;
use datafusion::logical_expr::expr::AggregateFunction;
use datafusion::optimizer::{OptimizerConfig, OptimizerRule};
use datafusion::prelude::Expr;
use super::PLANNING_REUSE;
use super::dedup::overlapping_groups;
use super::provider::{Kind, TimelakeProvider};
use super::rollup::unalias;
use super::settings::SessionSettings;
use crate::lake::Lake;
use crate::name::TableName;
use crate::property;
pub(super) type OutOfOrder = Arc<RwLock<HashSet<TableName>>>;
pub(super) async fn prepare(
lake: &Lake,
name: &TableName,
interval: Option<Duration>,
read_dedup: Option<bool>,
tables: &OutOfOrder,
) {
let at_risk = async {
let table = lake.table_recent(name, interval, PLANNING_REUSE).await.ok()?;
let contract = table.contract();
if contract.time_column.is_none() || (contract.dedup && read_dedup.unwrap_or(contract.read_dedup)) {
return Some(false);
}
let files = table.files().await.ok()?;
let sort_order = i32::try_from(table.metadata().default_sort_order_id()).ok();
if files.iter().any(|f| f.data_file.sort_order_id() != sort_order) {
return Some(true);
}
let schema = table.schema();
let keys: Vec<usize> =
contract.sort_columns().iter().map(|k| schema.index_of(k)).collect::<Result<_, _>>().ok()?;
Some(!overlapping_groups(&schema, &keys, &files).is_empty())
}
.await;
let mut set = tables.write().unwrap_or_else(PoisonError::into_inner);
match at_risk {
Some(false) => set.remove(name),
_ => set.insert(name.clone()),
};
}
#[derive(Debug)]
pub(super) struct SeriesOrder {
pub(super) out_of_order: OutOfOrder,
}
impl OptimizerRule for SeriesOrder {
fn name(&self) -> &'static str {
"timelake_series_order"
}
fn rewrite(&self, plan: LogicalPlan, config: &dyn OptimizerConfig) -> DfResult<Transformed<LogicalPlan>> {
let options = config.options();
let session = options.extensions.get::<SessionSettings>().and_then(|s| s.get(property::TIME_WEIGHTED_MEAN));
let session = session.map(str::to_ascii_lowercase);
plan.transform_down(|node| {
let LogicalPlan::Aggregate(a) = &node else { return Ok(Transformed::no(node)) };
if !a.aggr_expr.iter().any(is_time_weighted) {
return Ok(Transformed::no(node));
}
let input = a.input.as_ref().clone().transform_up(|n| self.order(n, session.as_deref()))?;
if !input.transformed {
return Ok(Transformed::no(node));
}
let LogicalPlan::Aggregate(mut a) = node else { unreachable!("matched above") };
a.input = Arc::new(input.data);
Ok(Transformed::new(LogicalPlan::Aggregate(a), true, TreeNodeRecursion::Jump))
})
}
}
impl SeriesOrder {
fn order(&self, node: LogicalPlan, session: Option<&str>) -> DfResult<Transformed<LogicalPlan>> {
let LogicalPlan::TableScan(scan) = &node else { return Ok(Transformed::no(node)) };
let provider = source_as_provider(&scan.source)?;
let Some(p) = (provider.as_ref() as &dyn Any).downcast_ref::<TimelakeProvider>() else {
return Ok(Transformed::no(node));
};
let setting = session.map_or_else(|| p.table().contract().time_weighted_mean.clone(), str::to_string);
let merge = match setting.as_str() {
"merge" => true,
"segments" => false,
_ => self.out_of_order.read().unwrap_or_else(PoisonError::into_inner).contains(p.table().name()),
};
if !merge || p.kind() != Kind::Data || p.reads_in_series_order() {
return Ok(Transformed::no(node));
}
let mut scan = scan.clone();
scan.source = provider_as_source(Arc::new(p.in_series_order()));
Ok(Transformed::yes(LogicalPlan::TableScan(scan)))
}
}
fn is_time_weighted(e: &Expr) -> bool {
matches!(unalias(e), Expr::AggregateFunction(AggregateFunction { func, .. }) if func.name() == "time_weighted_mean")
}
#[cfg(test)]
mod tests {
use crate::testing::{T0, batch, faulty_lake, opts, schema};
use crate::{AppendOptions, Session, TableOptions};
const SECOND: i64 = 1_000_000;
async fn answer(s: &Session, sql: &str) -> crate::Result<String> {
let rows = s.sql(sql).await?.collect().await?;
Ok(arrow::util::pretty::pretty_format_batches(&rows).unwrap().to_string())
}
#[tokio::test]
async fn interleaved_files_are_merged_in_series_order() {
let (lake, _) = faulty_lake();
let o = TableOptions { dedup: Some(false), ..opts() };
let t = lake.create_table("iot.r", &schema(), o).await.unwrap();
let no_maintenance = AppendOptions { maintain: Some(false), ..Default::default() };
let even: Vec<(i64, i32, f64)> = (0..30).map(|i| (T0 + i * 10 * SECOND, 1, (i * 10) as f64)).collect();
let odd: Vec<(i64, i32, f64)> = (0..30).map(|i| (T0 + (i * 10 + 5) * SECOND, 1, (i * 10 + 5) as f64)).collect();
let (t, _) = t.append(&[batch(&even)], no_maintenance.clone()).await.unwrap();
t.append(&[batch(&odd)], no_maintenance).await.unwrap();
let sql =
"SELECT device, time_weighted_mean(time, value) AS ff, time_weighted_mean(time, value, 'linear') AS lin,
count(*) AS n FROM iot.r GROUP BY device";
let session = |mode: &'static str| {
let lake = lake.clone();
async move {
let s = Session::new(lake);
s.sql(&format!("SET timelake.time_weighted_mean = '{mode}'")).await.unwrap();
s
}
};
let auto = answer(&session("auto").await, sql).await.unwrap();
assert_eq!(auto, answer(&session("merge").await, sql).await.unwrap());
assert!(auto.contains("| 145.0 | 147.5 | 60 |"), "{auto}");
let err = answer(&session("segments").await, sql).await.unwrap_err().to_string();
assert!(err.contains("overlap in time"), "{err}");
}
#[tokio::test]
async fn merging_ordered_readings_changes_nothing() {
let (lake, _) = faulty_lake();
let t = lake.create_table("iot.r", &schema(), opts()).await.unwrap();
let rows: Vec<(i64, i32, f64)> =
(0..500).map(|i| (T0 + i * 7 * SECOND, i32::try_from(i % 3).unwrap(), ((i * 37) % 101) as f64)).collect();
t.append(&[batch(&rows)], AppendOptions::default()).await.unwrap();
let sql = "SELECT device, time_bucket(INTERVAL '10 minutes', time) AS b, time_weighted_mean(time, value) AS ff,
time_weighted_mean(time, value, 'linear') AS lin, max(value) AS m
FROM iot.r GROUP BY 1, 2 ORDER BY 1, 2";
let mut answers = vec![];
for mode in ["segments", "merge"] {
let s = Session::new(lake.clone());
s.sql(&format!("SET timelake.time_weighted_mean = '{mode}'")).await.unwrap();
answers.push(answer(&s, sql).await.unwrap());
}
assert_eq!(answers[0], answers[1]);
}
}