use datafusion::common::tree_node::Transformed;
use datafusion::common::{Column, DFSchema, NullEquality, Result};
use datafusion::logical_expr::{
Aggregate, Expr, Join, JoinType, LogicalPlan, LogicalPlanBuilder, Projection, SubqueryAlias,
};
use datafusion::optimizer::{ApplyOrder, OptimizerConfig, OptimizerRule};
use std::sync::Arc;
pub const SEMI_JOIN_REDUCTION_ENV: &str = "KRISHIV_SEMI_JOIN_REDUCTION";
pub const SEMI_JOIN_PUSHDOWN_ENV: &str = "KRISHIV_SEMI_JOIN_PUSHDOWN";
pub const SEMI_JOIN_DIMENSION_ENV: &str = "KRISHIV_SEMI_JOIN_DIMENSION";
pub fn semi_join_dimension_reduction_enabled() -> bool {
semi_join_reduction_enabled()
&& opt_in_from(&std::env::var(SEMI_JOIN_DIMENSION_ENV).unwrap_or_default())
}
fn opt_in_from(value: &str) -> bool {
matches!(
value.trim().to_ascii_lowercase().as_str(),
"1" | "on" | "true" | "yes"
)
}
pub fn semi_join_reduction_enabled() -> bool {
enabled_from(&std::env::var(SEMI_JOIN_REDUCTION_ENV).unwrap_or_default())
}
pub fn semi_join_pushdown_enabled() -> bool {
semi_join_reduction_enabled()
&& enabled_from(&std::env::var(SEMI_JOIN_PUSHDOWN_ENV).unwrap_or_default())
}
fn enabled_from(value: &str) -> bool {
!matches!(
value.trim().to_ascii_lowercase().as_str(),
"0" | "off" | "false" | "no"
)
}
#[derive(Debug, Default)]
pub struct SemiJoinPushdownThroughInnerJoin {
forced: bool,
}
impl SemiJoinPushdownThroughInnerJoin {
pub fn forced() -> Self {
Self { forced: true }
}
}
impl OptimizerRule for SemiJoinPushdownThroughInnerJoin {
fn name(&self) -> &str {
"semi_join_pushdown_through_inner_join"
}
fn apply_order(&self) -> Option<ApplyOrder> {
Some(ApplyOrder::TopDown)
}
fn rewrite(
&self,
plan: LogicalPlan,
_config: &dyn OptimizerConfig,
) -> Result<Transformed<LogicalPlan>> {
if !self.forced && !semi_join_pushdown_enabled() {
return Ok(Transformed::no(plan));
}
let LogicalPlan::Join(semi) = &plan else {
return Ok(Transformed::no(plan));
};
let filtered_is_right = match semi.join_type {
JoinType::LeftSemi | JoinType::LeftAnti => false,
JoinType::RightSemi | JoinType::RightAnti => true,
_ => return Ok(Transformed::no(plan)),
};
if semi.on.is_empty() {
return Ok(Transformed::no(plan));
}
let (filtered, probe) = if filtered_is_right {
(semi.right.as_ref(), semi.left.as_ref())
} else {
(semi.left.as_ref(), semi.right.as_ref())
};
let mut pairs = Vec::with_capacity(semi.on.len());
for (l, r) in &semi.on {
let (Expr::Column(lc), Expr::Column(rc)) = (l, r) else {
return Ok(Transformed::no(plan));
};
pairs.push(if filtered_is_right {
(rc.clone(), lc.clone())
} else {
(lc.clone(), rc.clone())
});
}
match push_semi_below(
filtered,
&pairs,
probe,
filtered_is_right,
semi.filter.as_ref(),
semi.join_type,
)? {
Some(rewritten) => Ok(Transformed::yes(rewritten)),
None => Ok(Transformed::no(plan)),
}
}
}
fn remap_residual(
residual: Option<&Expr>,
schema: &DFSchema,
lower: &dyn Fn(usize) -> Option<Column>,
) -> Option<Option<Expr>> {
use datafusion::common::tree_node::TreeNode;
let Some(expr) = residual.cloned() else {
return Some(None);
};
let mut unfollowable = false;
let rewritten = expr
.transform(|e| {
if let Expr::Column(c) = &e
&& let Some(idx) = index_of(schema, c)
{
return match lower(idx) {
Some(inner) => Ok(Transformed::yes(Expr::Column(inner))),
None => {
unfollowable = true;
Ok(Transformed::no(e))
}
};
}
Ok(Transformed::no(e))
})
.ok()?;
if unfollowable {
return None;
}
Some(Some(rewritten.data))
}
fn push_semi_below(
plan: &LogicalPlan,
pairs: &[(Column, Column)],
probe: &LogicalPlan,
filtered_is_right: bool,
residual: Option<&Expr>,
join_type: JoinType,
) -> Result<Option<LogicalPlan>> {
match plan {
LogicalPlan::Projection(proj) => {
let mut mapped = Vec::with_capacity(pairs.len());
for (fk, pk) in pairs {
let Some(idx) = index_of(&proj.schema, fk) else {
return Ok(None);
};
let Some(Expr::Column(inner)) = proj.expr.get(idx) else {
return Ok(None);
};
mapped.push((inner.clone(), pk.clone()));
}
let lower = |idx: usize| match proj.expr.get(idx) {
Some(Expr::Column(inner)) => Some(inner.clone()),
_ => None,
};
let Some(residual) = remap_residual(residual, &proj.schema, &lower) else {
return Ok(None);
};
let Some(new_input) = push_semi_below(
&proj.input,
&mapped,
probe,
filtered_is_right,
residual.as_ref(),
join_type,
)?
else {
return Ok(None);
};
Ok(Some(LogicalPlan::Projection(Projection::try_new(
proj.expr.clone(),
Arc::new(new_input),
)?)))
}
LogicalPlan::SubqueryAlias(alias) => {
let mut mapped = Vec::with_capacity(pairs.len());
for (fk, pk) in pairs {
let Some(idx) = index_of(&alias.schema, fk) else {
return Ok(None);
};
let (qualifier, field) = alias.input.schema().qualified_field(idx);
mapped.push((Column::new(qualifier.cloned(), field.name()), pk.clone()));
}
let lower = |idx: usize| {
let (qualifier, field) = alias.input.schema().qualified_field(idx);
Some(Column::new(qualifier.cloned(), field.name()))
};
let Some(residual) = remap_residual(residual, &alias.schema, &lower) else {
return Ok(None);
};
let Some(new_input) = push_semi_below(
&alias.input,
&mapped,
probe,
filtered_is_right,
residual.as_ref(),
join_type,
)?
else {
return Ok(None);
};
Ok(Some(LogicalPlan::SubqueryAlias(SubqueryAlias::try_new(
Arc::new(new_input),
alias.alias.clone(),
)?)))
}
LogicalPlan::Join(inner) if inner.join_type == JoinType::Inner => {
let all_in = |side: &LogicalPlan| {
pairs
.iter()
.all(|(fk, _)| index_of(side.schema(), fk).is_some())
};
let target_is_right = if all_in(&inner.left) {
false
} else if all_in(&inner.right) {
true
} else {
return Ok(None);
};
let target = if target_is_right {
&inner.right
} else {
&inner.left
};
let (left_keys, right_keys): (Vec<Column>, Vec<Column>) = pairs
.iter()
.map(|(fk, pk)| {
if filtered_is_right {
(pk.clone(), fk.clone())
} else {
(fk.clone(), pk.clone())
}
})
.unzip();
if let Some(filter) = residual {
for col in filter.column_refs() {
if index_of(target.schema(), col).is_none()
&& index_of(probe.schema(), col).is_none()
{
return Ok(None);
}
}
}
let residual = residual.cloned();
let reduced = if filtered_is_right {
LogicalPlanBuilder::from(probe.clone()).join_detailed(
target.as_ref().clone(),
join_type,
(left_keys, right_keys),
residual,
NullEquality::NullEqualsNothing,
)?
} else {
LogicalPlanBuilder::from(target.as_ref().clone()).join_detailed(
probe.clone(),
join_type,
(left_keys, right_keys),
residual,
NullEquality::NullEqualsNothing,
)?
}
.build()?;
let rebuilt = if target_is_right {
Join {
right: Arc::new(reduced),
..inner.clone()
}
} else {
Join {
left: Arc::new(reduced),
..inner.clone()
}
};
Ok(Some(LogicalPlan::Join(rebuilt)))
}
_ => Ok(None),
}
}
#[derive(Debug, Default)]
pub struct SemiJoinReductionFromSelectiveDimension {
forced: bool,
}
impl SemiJoinReductionFromSelectiveDimension {
pub fn forced() -> Self {
Self { forced: true }
}
}
impl OptimizerRule for SemiJoinReductionFromSelectiveDimension {
fn name(&self) -> &str {
"semi_join_reduction_from_selective_dimension"
}
fn apply_order(&self) -> Option<ApplyOrder> {
Some(ApplyOrder::BottomUp)
}
fn rewrite(
&self,
plan: LogicalPlan,
_config: &dyn OptimizerConfig,
) -> Result<Transformed<LogicalPlan>> {
if !self.forced && !semi_join_dimension_reduction_enabled() {
return Ok(Transformed::no(plan));
}
let LogicalPlan::Join(join) = &plan else {
return Ok(Transformed::no(plan));
};
if join.join_type != JoinType::Inner || join.on.is_empty() {
return Ok(Transformed::no(plan));
}
for (left_key, right_key) in &join.on {
let (Expr::Column(left_col), Expr::Column(right_col)) = (left_key, right_key) else {
continue;
};
for (dimension_is_right, dimension, dimension_key, big, big_key) in [
(true, &join.right, right_col, &join.left, left_col),
(false, &join.left, left_col, &join.right, right_col),
] {
let Some((probe, probe_col)) = selective_key_source(dimension, dimension_key)?
else {
continue;
};
if !contains_inner_join(big) || carries_reducer(big, &probe) {
continue;
}
let reduced = LogicalPlanBuilder::from(big.as_ref().clone())
.join_detailed(
probe,
JoinType::LeftSemi,
(vec![big_key.clone()], vec![probe_col]),
None,
NullEquality::NullEqualsNothing,
)?
.build()?;
let rebuilt = if dimension_is_right {
Join {
left: Arc::new(reduced),
..join.clone()
}
} else {
Join {
right: Arc::new(reduced),
..join.clone()
}
};
return Ok(Transformed::yes(LogicalPlan::Join(rebuilt)));
}
}
Ok(Transformed::no(plan))
}
}
fn contains_inner_join(plan: &LogicalPlan) -> bool {
if matches!(plan, LogicalPlan::Join(j) if j.join_type == JoinType::Inner) {
return true;
}
plan.inputs().iter().any(|child| contains_inner_join(child))
}
fn carries_reducer(plan: &LogicalPlan, probe: &LogicalPlan) -> bool {
if let LogicalPlan::Join(join) = plan
&& join.join_type == JoinType::LeftSemi
&& join.right.as_ref() == probe
{
return true;
}
plan.inputs()
.iter()
.any(|child| carries_reducer(child, probe))
}
#[derive(Debug, Default)]
pub struct SemiJoinReductionThroughAggregate;
impl OptimizerRule for SemiJoinReductionThroughAggregate {
fn name(&self) -> &str {
"semi_join_reduction_through_aggregate"
}
fn apply_order(&self) -> Option<ApplyOrder> {
Some(ApplyOrder::BottomUp)
}
fn rewrite(
&self,
plan: LogicalPlan,
_config: &dyn OptimizerConfig,
) -> Result<Transformed<LogicalPlan>> {
if !semi_join_reduction_enabled() {
return Ok(Transformed::no(plan));
}
let LogicalPlan::Join(join) = &plan else {
return Ok(Transformed::no(plan));
};
if join.join_type != JoinType::Inner || join.on.is_empty() {
return Ok(Transformed::no(plan));
}
for (left_key, right_key) in &join.on {
let (Expr::Column(left_col), Expr::Column(right_col)) = (left_key, right_key) else {
continue;
};
for (agg_is_right, agg_side, agg_key, probe_side, probe_key) in [
(true, &join.right, right_col, &join.left, left_col),
(false, &join.left, left_col, &join.right, right_col),
] {
let Some((probe, probe_col)) = selective_key_source(probe_side, probe_key)? else {
continue;
};
let Some(new_side) = push_through(agg_side, agg_key, &probe, &probe_col)? else {
continue;
};
let rebuilt = if agg_is_right {
Join {
right: Arc::new(new_side),
..join.clone()
}
} else {
Join {
left: Arc::new(new_side),
..join.clone()
}
};
return Ok(Transformed::yes(LogicalPlan::Join(rebuilt)));
}
}
Ok(Transformed::no(plan))
}
}
fn index_of(schema: &DFSchema, col: &Column) -> Option<usize> {
schema.index_of_column(col).ok()
}
fn push_through(
plan: &LogicalPlan,
key: &Column,
probe: &LogicalPlan,
probe_key: &Column,
) -> Result<Option<LogicalPlan>> {
let Some(idx) = index_of(plan.schema(), key) else {
return Ok(None);
};
match plan {
LogicalPlan::SubqueryAlias(alias) => {
let (qualifier, field) = alias.input.schema().qualified_field(idx);
let inner = Column::new(qualifier.cloned(), field.name());
let Some(new_input) = push_through(&alias.input, &inner, probe, probe_key)? else {
return Ok(None);
};
Ok(Some(LogicalPlan::SubqueryAlias(SubqueryAlias::try_new(
Arc::new(new_input),
alias.alias.clone(),
)?)))
}
LogicalPlan::Projection(proj) => {
let Some(Expr::Column(inner)) = proj.expr.get(idx) else {
return Ok(None);
};
let inner = inner.clone();
let Some(new_input) = push_through(&proj.input, &inner, probe, probe_key)? else {
return Ok(None);
};
Ok(Some(LogicalPlan::Projection(Projection::try_new(
proj.expr.clone(),
Arc::new(new_input),
)?)))
}
LogicalPlan::Aggregate(agg) => {
if idx >= agg.group_expr.len() {
return Ok(None);
}
let Some(Expr::Column(group_col)) = agg.group_expr.get(idx) else {
return Ok(None);
};
if already_reduced(&agg.input) {
return Ok(None);
}
let reduced = LogicalPlanBuilder::from(agg.input.as_ref().clone())
.join_detailed(
probe.clone(),
JoinType::LeftSemi,
(vec![group_col.clone()], vec![probe_key.clone()]),
None,
NullEquality::NullEqualsNothing,
)?
.build()?;
Ok(Some(LogicalPlan::Aggregate(Aggregate::try_new(
Arc::new(reduced),
agg.group_expr.clone(),
agg.aggr_expr.clone(),
)?)))
}
_ => Ok(None),
}
}
fn already_reduced(plan: &LogicalPlan) -> bool {
matches!(plan, LogicalPlan::Join(j) if j.join_type == JoinType::LeftSemi)
}
fn selective_key_source(plan: &LogicalPlan, key: &Column) -> Result<Option<(LogicalPlan, Column)>> {
let Some(source) = descend_to_filter(plan, key) else {
return Ok(None);
};
let (subtree, col) = source;
let projected = LogicalPlanBuilder::from(subtree)
.project([Expr::Column(col.clone())])?
.build()?;
Ok(Some((projected, col)))
}
fn descend_to_filter(plan: &LogicalPlan, key: &Column) -> Option<(LogicalPlan, Column)> {
let idx = index_of(plan.schema(), key)?;
match plan {
LogicalPlan::Filter(_) => Some((plan.clone(), key.clone())),
LogicalPlan::SubqueryAlias(alias) => {
let (qualifier, field) = alias.input.schema().qualified_field(idx);
descend_to_filter(&alias.input, &Column::new(qualifier.cloned(), field.name()))
}
LogicalPlan::Projection(proj) => match proj.expr.get(idx) {
Some(Expr::Column(inner)) => descend_to_filter(&proj.input, &inner.clone()),
_ => None,
},
LogicalPlan::Join(join) => {
if !matches!(join.join_type, JoinType::Inner) {
return None;
}
for side in [&join.left, &join.right] {
if let Some(found) =
index_of(side.schema(), key).and_then(|_| descend_to_filter(side, key))
{
return Some(found);
}
}
None
}
_ => None,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use datafusion::arrow::array::{Int64Array, StringArray};
use datafusion::arrow::datatypes::{DataType, Field, Schema};
use datafusion::arrow::record_batch::RecordBatch;
use datafusion::datasource::MemTable;
use datafusion::execution::session_state::SessionStateBuilder;
use datafusion::prelude::SessionContext;
fn line_table() -> Arc<MemTable> {
let schema = Arc::new(Schema::new(vec![
Field::new("l_partkey", DataType::Int64, false),
Field::new("l_orderkey", DataType::Int64, false),
Field::new("l_quantity", DataType::Int64, false),
Field::new("l_suppkey", DataType::Int64, false),
Field::new("l_commitdate", DataType::Int64, false),
Field::new("l_receiptdate", DataType::Int64, false),
]));
let mut keys = Vec::new();
let mut orders = Vec::new();
let mut qty = Vec::new();
let mut supp = Vec::new();
let mut commit = Vec::new();
let mut receipt = Vec::new();
for k in 1..=5i64 {
for q in 1..=4i64 {
keys.push(k);
orders.push(k);
qty.push(k * 10 + q);
supp.push(q);
commit.push(100i64);
receipt.push(if q == (k % 4) + 1 { 200 } else { 50 });
}
}
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Int64Array::from(keys)),
Arc::new(Int64Array::from(orders)),
Arc::new(Int64Array::from(qty)),
Arc::new(Int64Array::from(supp)),
Arc::new(Int64Array::from(commit)),
Arc::new(Int64Array::from(receipt)),
],
)
.unwrap();
Arc::new(MemTable::try_new(schema, vec![vec![batch]]).unwrap())
}
fn supplier_table() -> Arc<MemTable> {
let schema = Arc::new(Schema::new(vec![
Field::new("s_suppkey", DataType::Int64, false),
Field::new("s_name", DataType::Utf8, false),
Field::new("s_nationkey", DataType::Int64, false),
]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Int64Array::from(vec![1i64, 2, 3, 4])),
Arc::new(StringArray::from(vec!["s1", "s2", "s3", "s4"])),
Arc::new(Int64Array::from(vec![7i64, 7, 8, 7])),
],
)
.unwrap();
Arc::new(MemTable::try_new(schema, vec![vec![batch]]).unwrap())
}
fn nation_table() -> Arc<MemTable> {
let schema = Arc::new(Schema::new(vec![
Field::new("n_nationkey", DataType::Int64, false),
Field::new("n_name", DataType::Utf8, false),
]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Int64Array::from(vec![7i64, 8])),
Arc::new(StringArray::from(vec!["SAUDI ARABIA", "OTHER"])),
],
)
.unwrap();
Arc::new(MemTable::try_new(schema, vec![vec![batch]]).unwrap())
}
fn orders_table() -> Arc<MemTable> {
let schema = Arc::new(Schema::new(vec![
Field::new("o_orderkey", DataType::Int64, false),
Field::new("o_custkey", DataType::Int64, false),
Field::new("o_totalprice", DataType::Int64, false),
Field::new("o_orderdate", DataType::Int64, false),
Field::new("o_orderstatus", DataType::Utf8, false),
]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Int64Array::from(vec![1i64, 2, 3, 4, 5])),
Arc::new(Int64Array::from(vec![10i64, 20, 30, 40, 50])),
Arc::new(Int64Array::from(vec![100i64, 200, 300, 400, 500])),
Arc::new(Int64Array::from(vec![
20260101i64,
20260102,
20260103,
20260104,
20260105,
])),
Arc::new(StringArray::from(vec!["F", "F", "F", "O", "F"])),
],
)
.unwrap();
Arc::new(MemTable::try_new(schema, vec![vec![batch]]).unwrap())
}
fn customer_table() -> Arc<MemTable> {
let schema = Arc::new(Schema::new(vec![
Field::new("c_custkey", DataType::Int64, false),
Field::new("c_name", DataType::Utf8, false),
]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Int64Array::from(vec![10i64, 20, 30, 40, 50])),
Arc::new(StringArray::from(vec!["a", "b", "c", "d", "e"])),
],
)
.unwrap();
Arc::new(MemTable::try_new(schema, vec![vec![batch]]).unwrap())
}
fn part_table() -> Arc<MemTable> {
let schema = Arc::new(Schema::new(vec![
Field::new("p_partkey", DataType::Int64, false),
Field::new("p_brand", DataType::Utf8, false),
]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Int64Array::from(vec![1i64, 2, 3, 4, 5])),
Arc::new(StringArray::from(vec![
"keep", "skip", "keep", "skip", "skip",
])),
],
)
.unwrap();
Arc::new(MemTable::try_new(schema, vec![vec![batch]]).unwrap())
}
fn context(with_rule: bool) -> SessionContext {
let mut builder = SessionStateBuilder::new().with_default_features();
if with_rule {
builder = builder
.with_optimizer_rule(Arc::new(SemiJoinReductionThroughAggregate))
.with_optimizer_rule(Arc::new(SemiJoinPushdownThroughInnerJoin::forced()));
}
let ctx = SessionContext::new_with_state(builder.build());
ctx.register_table("lineitem", line_table()).unwrap();
ctx.register_table("part", part_table()).unwrap();
ctx.register_table("orders", orders_table()).unwrap();
ctx.register_table("customer", customer_table()).unwrap();
ctx.register_table("supplier", supplier_table()).unwrap();
ctx.register_table("nation", nation_table()).unwrap();
ctx
}
async fn rows(ctx: &SessionContext, sql: &str) -> Vec<String> {
let batches = ctx.sql(sql).await.unwrap().collect().await.unwrap();
let mut out = Vec::new();
for b in &batches {
for r in 0..b.num_rows() {
let mut cells = Vec::new();
for c in 0..b.num_columns() {
cells.push(
datafusion::common::cast::as_string_array(
&datafusion::arrow::compute::cast(b.column(c), &DataType::Utf8)
.unwrap(),
)
.unwrap()
.value(r)
.to_string(),
);
}
out.push(cells.join("|"));
}
}
out.sort();
out
}
fn dimension_context(with_rule: bool) -> SessionContext {
let mut builder = SessionStateBuilder::new().with_default_features();
if with_rule {
builder = builder
.with_optimizer_rule(Arc::new(SemiJoinPushdownThroughInnerJoin::forced()))
.with_optimizer_rule(Arc::new(SemiJoinReductionFromSelectiveDimension::forced()));
}
let ctx = SessionContext::new_with_state(builder.build());
ctx.register_table("lineitem", line_table()).unwrap();
ctx.register_table("supplier", supplier_table()).unwrap();
ctx.register_table("nation", nation_table()).unwrap();
ctx
}
const Q7_SHAPE: &str = "SELECT n.n_name, sum(l.l_quantity) AS q \
FROM supplier s, lineitem l, nation n \
WHERE s.s_suppkey = l.l_suppkey AND s.s_nationkey = n.n_nationkey \
AND n.n_name = 'SAUDI ARABIA' \
GROUP BY n.n_name";
#[tokio::test]
async fn the_reducer_lands_on_the_dimension_keyed_scan() {
let plan = plan_of(&dimension_context(true), Q7_SHAPE).await;
let semi = plan
.lines()
.position(|l| l.contains("LeftSemi"))
.unwrap_or_else(|| panic!("no reducer was introduced:\n{plan}"));
let supplier = plan
.lines()
.position(|l| l.contains("TableScan: supplier"))
.unwrap_or_else(|| panic!("no supplier scan:\n{plan}"));
let lineitem = plan
.lines()
.position(|l| l.contains("TableScan: lineitem"))
.unwrap_or_else(|| panic!("no lineitem scan:\n{plan}"));
assert!(
semi < supplier,
"the reducer must sit above the supplier scan, not below it:\n{plan}"
);
assert!(
semi > lineitem || supplier < lineitem,
"the reducer swallowed the lineitem scan, so it did not land on \
supplier alone:\n{plan}"
);
}
#[tokio::test]
async fn reducing_by_the_dimension_keeps_the_same_rows() {
let expected = rows(&dimension_context(false), Q7_SHAPE).await;
assert!(!expected.is_empty(), "fixture must produce rows");
assert_eq!(rows(&dimension_context(true), Q7_SHAPE).await, expected);
}
#[test]
fn the_dimension_rule_is_off_unless_explicitly_asked_for() {
for unset_or_no in ["", " ", "off", "0", "false", "no", "maybe"] {
assert!(
!opt_in_from(unset_or_no),
"{unset_or_no:?} must not enable the rule"
);
}
for yes in ["1", "on", "true", "yes", "ON", " On "] {
assert!(opt_in_from(yes), "{yes:?} must enable the rule");
}
}
#[tokio::test]
async fn an_unfiltered_dimension_does_not_get_a_reducer() {
let sql = "SELECT n.n_name, sum(l.l_quantity) AS q \
FROM supplier s, lineitem l, nation n \
WHERE s.s_suppkey = l.l_suppkey AND s.s_nationkey = n.n_nationkey \
GROUP BY n.n_name";
let plan = plan_of(&dimension_context(true), sql).await;
assert!(
!plan.contains("LeftSemi"),
"no filter on the dimension means nothing to reduce by:\n{plan}"
);
}
#[tokio::test]
async fn the_reducer_is_introduced_exactly_once() {
let plan = plan_of(&dimension_context(true), Q7_SHAPE).await;
assert_eq!(
plan.matches("LeftSemi").count(),
1,
"the rule stacked reducers instead of converging:\n{plan}"
);
}
async fn plan_of(ctx: &SessionContext, sql: &str) -> String {
format!(
"{}",
ctx.sql(sql)
.await
.unwrap()
.into_optimized_plan()
.unwrap()
.display_indent()
)
}
const Q17_SHAPE: &str = "SELECT p.p_partkey, s.avg_q FROM part p JOIN \
(SELECT l_partkey, avg(l_quantity) AS avg_q FROM lineitem GROUP BY l_partkey) s \
ON p.p_partkey = s.l_partkey WHERE p.p_brand = 'keep'";
#[tokio::test]
async fn the_rule_pushes_a_semi_join_into_the_aggregate_input() {
let plan = plan_of(&context(true), Q17_SHAPE).await;
assert!(
plan.contains("LeftSemi"),
"expected a LeftSemi reduction in:\n{plan}"
);
let baseline = plan_of(&context(false), Q17_SHAPE).await;
assert!(
!baseline.contains("LeftSemi"),
"baseline should not already contain one:\n{baseline}"
);
}
#[tokio::test]
async fn results_are_identical_with_and_without_the_rule() {
for sql in [
Q17_SHAPE,
"SELECT s.l_partkey, s.total FROM \
(SELECT l_partkey, sum(l_quantity) AS total FROM lineitem GROUP BY l_partkey) s \
JOIN part p ON s.l_partkey = p.p_partkey WHERE p.p_brand = 'keep'",
"SELECT p.p_partkey, s.n, s.total FROM part p JOIN \
(SELECT l_partkey, count(*) AS n, sum(l_quantity) AS total \
FROM lineitem GROUP BY l_partkey) s \
ON p.p_partkey = s.l_partkey WHERE p.p_brand = 'keep'",
] {
let with = rows(&context(true), sql).await;
let without = rows(&context(false), sql).await;
assert_eq!(with, without, "results diverged for:\n{sql}");
assert!(!with.is_empty(), "test query returned nothing: {sql}");
}
}
#[tokio::test]
async fn outer_joins_are_left_alone() {
let sql = "SELECT p.p_partkey, s.avg_q FROM part p LEFT JOIN \
(SELECT l_partkey, avg(l_quantity) AS avg_q FROM lineitem GROUP BY l_partkey) s \
ON p.p_partkey = s.l_partkey WHERE p.p_brand = 'keep'";
let plan = plan_of(&context(true), sql).await;
assert!(
!plan.contains("LeftSemi"),
"must not reduce under an outer join:\n{plan}"
);
assert_eq!(
rows(&context(true), sql).await,
rows(&context(false), sql).await
);
}
#[tokio::test]
async fn joining_on_an_aggregate_output_is_not_reduced() {
let sql = "SELECT p.p_partkey FROM part p JOIN \
(SELECT l_partkey, sum(l_quantity) AS total FROM lineitem GROUP BY l_partkey) s \
ON p.p_partkey = s.total WHERE p.p_brand = 'keep'";
let plan = plan_of(&context(true), sql).await;
assert!(
!plan.contains("LeftSemi"),
"grouping keys only; an aggregate output is not one:\n{plan}"
);
}
#[tokio::test]
async fn an_unfiltered_probe_side_is_not_worth_reducing() {
let sql = "SELECT p.p_partkey, s.avg_q FROM part p JOIN \
(SELECT l_partkey, avg(l_quantity) AS avg_q FROM lineitem GROUP BY l_partkey) s \
ON p.p_partkey = s.l_partkey";
let plan = plan_of(&context(true), sql).await;
assert!(
!plan.contains("LeftSemi"),
"no filter means no selectivity to exploit:\n{plan}"
);
}
#[tokio::test]
async fn reduction_is_applied_at_most_once() {
let plan = plan_of(&context(true), Q17_SHAPE).await;
assert_eq!(
plan.matches("LeftSemi").count(),
1,
"expected exactly one reduction:\n{plan}"
);
}
#[test]
fn the_env_switch_is_honoured() {
for off in ["off", "OFF", "0", "false", "no", " off "] {
assert!(!enabled_from(off), "{off:?} should disable the rule");
}
for on in ["", "on", "1", "true", "anything-else"] {
assert!(enabled_from(on), "{on:?} should leave the rule enabled");
}
}
#[tokio::test]
async fn the_reduction_keeps_exactly_the_surviving_groups() {
let out = rows(&context(true), Q17_SHAPE).await;
assert_eq!(out.len(), 2, "expected two surviving groups, got {out:?}");
}
const Q18_SHAPE: &str = "SELECT o.o_orderkey, sum(l.l_quantity) \
FROM customer c, orders o, lineitem l \
WHERE o.o_orderkey IN \
(SELECT l_orderkey FROM lineitem GROUP BY l_orderkey HAVING sum(l_quantity) > 100) \
AND c.c_custkey = o.o_custkey AND o.o_orderkey = l.l_orderkey \
GROUP BY o.o_orderkey";
#[tokio::test]
async fn the_semi_join_is_pushed_below_the_inner_join() {
let with = plan_of(&context(true), Q18_SHAPE).await;
let without = plan_of(&context(false), Q18_SHAPE).await;
fn depth_of_semi(plan: &str) -> Option<usize> {
plan.lines().position(|l| l.contains("Semi"))
}
fn depth_of_first_inner(plan: &str) -> Option<usize> {
plan.lines().position(|l| l.contains("Inner Join"))
}
let (ws, wi) = (depth_of_semi(&with), depth_of_first_inner(&with));
let (bs, bi) = (depth_of_semi(&without), depth_of_first_inner(&without));
assert!(ws.is_some() && wi.is_some(), "expected both joins:\n{with}");
assert!(
bs < bi,
"baseline should have the semi-join above the inner join:\n{without}"
);
assert!(
ws > wi,
"rule should push the semi-join below the inner join:\n{with}"
);
}
#[tokio::test]
async fn q18_results_are_identical_with_and_without_the_rule() {
for sql in [
Q18_SHAPE,
"SELECT o.o_orderkey, c.c_name FROM customer c, orders o \
WHERE o.o_orderkey IN (SELECT l_orderkey FROM lineitem \
GROUP BY l_orderkey HAVING sum(l_quantity) > 100) \
AND c.c_custkey = o.o_custkey",
"SELECT o.o_orderkey FROM customer c, orders o \
WHERE o.o_orderkey NOT IN (SELECT l_orderkey FROM lineitem \
GROUP BY l_orderkey HAVING sum(l_quantity) > 100) \
AND c.c_custkey = o.o_custkey",
] {
let with = rows(&context(true), sql).await;
let without = rows(&context(false), sql).await;
assert_eq!(with, without, "results diverged for:\n{sql}");
}
}
const Q18_VERBATIM: &str = "SELECT c_name, c_custkey, o_orderkey, o_orderdate, o_totalprice, \
sum(l_quantity) FROM customer, orders, lineitem \
WHERE o_orderkey IN (SELECT l_orderkey FROM lineitem GROUP BY l_orderkey \
HAVING sum(l_quantity) > 100) \
AND c_custkey = o_custkey AND o_orderkey = l_orderkey \
GROUP BY c_name, c_custkey, o_orderkey, o_orderdate, o_totalprice \
ORDER BY o_totalprice DESC, o_orderdate LIMIT 100";
#[tokio::test]
async fn the_verbatim_q18_shape_is_also_pushed_down() {
let with = plan_of(&context(true), Q18_VERBATIM).await;
let semi = with.lines().position(|l| l.contains("Semi"));
let inner = with.lines().position(|l| l.contains("Inner Join"));
assert!(
semi.is_some() && inner.is_some(),
"expected both joins in:\n{with}"
);
assert!(
semi > inner,
"the real q18 shape must be pushed below the inner join too:\n{with}"
);
assert_eq!(
rows(&context(true), Q18_VERBATIM).await,
rows(&context(false), Q18_VERBATIM).await
);
}
const Q21_VERBATIM: &str = "SELECT s_name, count(*) AS numwait \
FROM supplier, lineitem l1, orders, nation \
WHERE s_suppkey = l1.l_suppkey AND o_orderkey = l1.l_orderkey \
AND o_orderstatus = 'F' AND l1.l_receiptdate > l1.l_commitdate \
AND EXISTS (SELECT * FROM lineitem l2 \
WHERE l2.l_orderkey = l1.l_orderkey \
AND l2.l_suppkey <> l1.l_suppkey) \
AND NOT EXISTS (SELECT * FROM lineitem l3 \
WHERE l3.l_orderkey = l1.l_orderkey \
AND l3.l_suppkey <> l1.l_suppkey \
AND l3.l_receiptdate > l3.l_commitdate) \
AND s_nationkey = n_nationkey AND n_name = 'SAUDI ARABIA' \
GROUP BY s_name ORDER BY numwait DESC, s_name LIMIT 100";
#[tokio::test]
async fn q21_results_are_identical_with_and_without_the_rule() {
let with = rows(&context(true), Q21_VERBATIM).await;
let without = rows(&context(false), Q21_VERBATIM).await;
assert_eq!(with, without, "q21 diverged under the rewrite");
assert!(
!with.is_empty(),
"the q21 fixture must produce rows or it proves nothing"
);
}
#[tokio::test]
async fn semi_and_anti_with_a_residual_each_keep_their_answers() {
for sql in [
"SELECT s_name FROM supplier, lineitem l1 \
WHERE s_suppkey = l1.l_suppkey \
AND EXISTS (SELECT * FROM lineitem l2 \
WHERE l2.l_orderkey = l1.l_orderkey \
AND l2.l_suppkey <> l1.l_suppkey)",
"SELECT s_name FROM supplier, lineitem l1 \
WHERE s_suppkey = l1.l_suppkey \
AND NOT EXISTS (SELECT * FROM lineitem l3 \
WHERE l3.l_orderkey = l1.l_orderkey \
AND l3.l_suppkey <> l1.l_suppkey \
AND l3.l_receiptdate > l3.l_commitdate)",
"SELECT s_name FROM supplier, lineitem l1 \
WHERE s_suppkey = l1.l_suppkey \
AND NOT EXISTS (SELECT * FROM lineitem l3 \
WHERE l3.l_orderkey = l1.l_orderkey \
AND l3.l_suppkey <> l1.l_suppkey \
AND l3.l_quantity > 100000)",
] {
let with = rows(&context(true), sql).await;
let without = rows(&context(false), sql).await;
assert_eq!(with, without, "results diverged for:\n{sql}");
}
}
#[tokio::test]
async fn the_q21_semi_and_anti_joins_are_pushed_below_the_inner_join() {
let with = plan_of(&context(true), Q21_VERBATIM).await;
let without = plan_of(&context(false), Q21_VERBATIM).await;
let first_inner = |p: &str| p.lines().position(|l| l.contains("Inner Join"));
let first_semi = |p: &str| {
p.lines()
.position(|l| l.contains("LeftSemi") || l.contains("LeftAnti"))
};
let (bs, bi) = (first_semi(&without), first_inner(&without));
assert!(
bs.is_some() && bi.is_some() && bs < bi,
"baseline should have the existence joins above the inner join:\n{without}"
);
let (ws, wi) = (first_semi(&with), first_inner(&with));
assert!(
ws.is_some() && wi.is_some(),
"expected both join kinds in:\n{with}"
);
assert!(
ws > wi,
"q21's existence joins must be pushed below the inner join:\n{with}"
);
}
#[tokio::test]
async fn a_residual_straddling_both_children_is_not_pushed() {
let sql = "SELECT s_name FROM supplier, lineitem l1, orders \
WHERE s_suppkey = l1.l_suppkey AND o_orderkey = l1.l_orderkey \
AND EXISTS (SELECT * FROM lineitem l2 \
WHERE l2.l_orderkey = l1.l_orderkey \
AND l2.l_quantity > orders.o_totalprice)";
assert_eq!(
rows(&context(true), sql).await,
rows(&context(false), sql).await,
"a straddling residual must not change the answer"
);
}
async fn physical_plan_of(ctx: &SessionContext, sql: &str) -> String {
let logical = ctx.sql(sql).await.unwrap().into_optimized_plan().unwrap();
let physical = ctx.state().create_physical_plan(&logical).await.unwrap();
format!(
"{}",
datafusion::physical_plan::displayable(physical.as_ref()).indent(false)
)
}
#[tokio::test]
async fn the_rewrites_never_produce_a_nested_loop_join() {
for sql in [
Q17_SHAPE,
Q18_SHAPE,
Q18_VERBATIM,
Q21_VERBATIM,
"SELECT s.s_name FROM supplier s, lineitem l \
WHERE s.s_suppkey = l.l_suppkey \
AND l.l_quantity = (SELECT min(l2.l_quantity) FROM lineitem l2 \
WHERE l2.l_orderkey = l.l_orderkey)",
] {
let plan = physical_plan_of(&context(true), sql).await;
assert!(
!plan.contains("NestedLoopJoin"),
"the rewrite produced a nested-loop join — an equi-join lost \
its keys — for:\n{sql}\n\n{plan}"
);
}
}
#[tokio::test]
async fn a_semi_join_is_not_pushed_through_an_outer_join() {
let sql = "SELECT o.o_orderkey FROM orders o LEFT JOIN customer c \
ON c.c_custkey = o.o_custkey \
WHERE o.o_orderkey IN (SELECT l_orderkey FROM lineitem \
GROUP BY l_orderkey HAVING sum(l_quantity) > 100)";
assert_eq!(
rows(&context(true), sql).await,
rows(&context(false), sql).await,
"outer join below must not change the answer"
);
}
}