mod common;
use std::sync::Arc;
use datafusion::arrow::array::Int64Array;
use datafusion::physical_plan::{displayable, ExecutionPlan};
use paimon_datafusion::SQLContext;
async fn setup_table(schema_sql: &str) -> (tempfile::TempDir, SQLContext) {
let (tmp, sql_context) = common::setup_sql_context().await;
sql_context
.sql(&format!("CREATE TABLE paimon.test_db.t {schema_sql}"))
.await
.expect("CREATE TABLE should succeed");
(tmp, sql_context)
}
async fn setup_partitioned_table(
columns: &str,
partition_cols: &str,
) -> (tempfile::TempDir, SQLContext) {
let (tmp, sql_context) = common::setup_sql_context().await;
sql_context
.sql(&format!(
"CREATE TABLE paimon.test_db.t ({columns}) PARTITIONED BY ({partition_cols})"
))
.await
.expect("CREATE TABLE should succeed");
(tmp, sql_context)
}
fn plan_contains_scan(plan: &Arc<dyn ExecutionPlan>) -> bool {
let plan_str = displayable(plan.as_ref()).indent(true).to_string();
plan_str.contains("PaimonTableScan")
}
async fn verify_count_pushdown(
sql_context: &SQLContext,
sql: &str,
) -> Result<Arc<dyn ExecutionPlan>, Arc<dyn ExecutionPlan>> {
let df = sql_context.sql(sql).await.expect("Query should succeed");
let plan = df
.create_physical_plan()
.await
.expect("Physical plan should succeed");
if plan_contains_scan(&plan) {
Err(plan)
} else {
Ok(plan)
}
}
async fn run_count_query(sql_context: &SQLContext, sql: &str) -> i64 {
let batches = sql_context
.sql(sql)
.await
.expect("Query should succeed")
.collect()
.await
.expect("Collect should succeed");
let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
assert_eq!(total_rows, 1, "COUNT(*) should return exactly one row");
let count_array = batches[0]
.column(0)
.as_any()
.downcast_ref::<Int64Array>()
.expect("COUNT should return Int64");
count_array.value(0)
}
#[tokio::test]
async fn test_count_star_no_filter_pushes_down() {
let (_tmp, sql_context) = setup_table("(id INT, value INT)").await;
sql_context
.sql("INSERT INTO paimon.test_db.t VALUES (1, 10), (2, 20), (3, 30)")
.await
.unwrap()
.collect()
.await
.unwrap();
verify_count_pushdown(&sql_context, "SELECT COUNT(*) FROM paimon.test_db.t")
.await
.expect("COUNT(*) should push down (no filter)");
let count = run_count_query(&sql_context, "SELECT COUNT(*) FROM paimon.test_db.t").await;
assert_eq!(count, 3, "COUNT(*) should return 3");
}
#[tokio::test]
async fn test_count_star_with_data_filter_does_not_push_down() {
let (_tmp, sql_context) = setup_table("(id INT, value INT)").await;
sql_context
.sql("INSERT INTO paimon.test_db.t VALUES (1, 10), (2, 20), (3, 30)")
.await
.unwrap()
.collect()
.await
.unwrap();
let plan = verify_count_pushdown(
&sql_context,
"SELECT COUNT(*) FROM paimon.test_db.t WHERE id > 1",
)
.await;
assert!(
plan.is_err(),
"COUNT(*) with data filter should NOT push down - must scan data"
);
let count = run_count_query(
&sql_context,
"SELECT COUNT(*) FROM paimon.test_db.t WHERE id > 1",
)
.await;
assert_eq!(count, 2, "COUNT(*) should return 2");
}
#[tokio::test]
async fn test_count_star_with_non_partition_equality_does_not_push_down() {
let (_tmp, sql_context) = setup_table("(id INT, value INT)").await;
sql_context
.sql("INSERT INTO paimon.test_db.t VALUES (1, 10), (2, 20), (3, 30)")
.await
.unwrap()
.collect()
.await
.unwrap();
let plan = verify_count_pushdown(
&sql_context,
"SELECT COUNT(*) FROM paimon.test_db.t WHERE id = 2",
)
.await;
assert!(
plan.is_err(),
"COUNT(*) with non-partition equality filter should NOT push down"
);
let count = run_count_query(
&sql_context,
"SELECT COUNT(*) FROM paimon.test_db.t WHERE id = 2",
)
.await;
assert_eq!(count, 1, "COUNT(*) should return 1");
}
#[tokio::test]
async fn test_count_star_single_row_pushes_down() {
let (_tmp, sql_context) = setup_table("(id INT, value INT)").await;
sql_context
.sql("INSERT INTO paimon.test_db.t VALUES (1, 10)")
.await
.unwrap()
.collect()
.await
.unwrap();
verify_count_pushdown(&sql_context, "SELECT COUNT(*) FROM paimon.test_db.t")
.await
.expect("COUNT(*) should push down");
let count = run_count_query(&sql_context, "SELECT COUNT(*) FROM paimon.test_db.t").await;
assert_eq!(count, 1, "COUNT(*) should return 1");
}
#[tokio::test]
async fn test_count_star_empty_table_pushes_down() {
let (_tmp, sql_context) = setup_table("(id INT, value INT)").await;
verify_count_pushdown(&sql_context, "SELECT COUNT(*) FROM paimon.test_db.t")
.await
.expect("COUNT(*) on empty table should push down");
let count = run_count_query(&sql_context, "SELECT COUNT(*) FROM paimon.test_db.t").await;
assert_eq!(count, 0, "COUNT(*) should return 0");
}
#[tokio::test]
async fn test_count_star_with_partition_filter_pushes_down() {
let (_tmp, sql_context) = setup_partitioned_table("id INT, value INT, dt STRING", "dt").await;
sql_context
.sql("INSERT INTO paimon.test_db.t VALUES (1, 10, '2024-01-01'), (2, 20, '2024-01-01'), (3, 30, '2024-01-02')")
.await
.unwrap()
.collect()
.await
.unwrap();
verify_count_pushdown(
&sql_context,
"SELECT COUNT(*) FROM paimon.test_db.t WHERE dt = '2024-01-01'",
)
.await
.expect("COUNT(*) with partition filter should push down");
let count = run_count_query(
&sql_context,
"SELECT COUNT(*) FROM paimon.test_db.t WHERE dt = '2024-01-01'",
)
.await;
assert_eq!(count, 2, "COUNT(*) should return 2");
}
#[tokio::test]
async fn test_count_star_with_mixed_partition_data_filter_does_not_push_down() {
let (_tmp, sql_context) = setup_partitioned_table("id INT, value INT, dt STRING", "dt").await;
sql_context
.sql("INSERT INTO paimon.test_db.t VALUES (1, 10, '2024-01-01'), (2, 20, '2024-01-01'), (3, 30, '2024-01-02')")
.await
.unwrap()
.collect()
.await
.unwrap();
let plan = verify_count_pushdown(
&sql_context,
"SELECT COUNT(*) FROM paimon.test_db.t WHERE dt = '2024-01-01' AND value > 15",
)
.await;
assert!(
plan.is_err(),
"COUNT(*) with mixed partition + data filter should NOT push down"
);
let count = run_count_query(
&sql_context,
"SELECT COUNT(*) FROM paimon.test_db.t WHERE dt = '2024-01-01' AND value > 15",
)
.await;
assert_eq!(count, 1, "COUNT(*) should return 1");
}
#[tokio::test]
async fn test_count_star_with_partition_in_filter_pushes_down() {
let (_tmp, sql_context) = setup_partitioned_table("id INT, value INT, dt STRING", "dt").await;
sql_context
.sql("INSERT INTO paimon.test_db.t VALUES (1, 10, '2024-01-01'), (2, 20, '2024-01-02'), (3, 30, '2024-01-03')")
.await
.unwrap()
.collect()
.await
.unwrap();
verify_count_pushdown(
&sql_context,
"SELECT COUNT(*) FROM paimon.test_db.t WHERE dt IN ('2024-01-01', '2024-01-02')",
)
.await
.expect("COUNT(*) with partition IN filter should push down");
let count = run_count_query(
&sql_context,
"SELECT COUNT(*) FROM paimon.test_db.t WHERE dt IN ('2024-01-01', '2024-01-02')",
)
.await;
assert_eq!(count, 2, "COUNT(*) should return 2");
}
#[tokio::test]
async fn test_count_star_partitioned_no_filter_pushes_down() {
let (_tmp, sql_context) = setup_partitioned_table("id INT, value INT, dt STRING", "dt").await;
sql_context
.sql("INSERT INTO paimon.test_db.t VALUES (1, 10, '2024-01-01'), (2, 20, '2024-01-02')")
.await
.unwrap()
.collect()
.await
.unwrap();
verify_count_pushdown(&sql_context, "SELECT COUNT(*) FROM paimon.test_db.t")
.await
.expect("COUNT(*) without filter should push down");
let count = run_count_query(&sql_context, "SELECT COUNT(*) FROM paimon.test_db.t").await;
assert_eq!(count, 2, "COUNT(*) should return 2");
}