use polars_core::prelude::*;
use polars_lazy::prelude::*;
#[cfg(feature = "semi_anti_join")]
use polars_plan::utils::{expr_to_leaf_column_names_iter, has_expr};
#[cfg(feature = "semi_anti_join")]
use polars_utils::aliases::PlHashSet;
#[cfg(feature = "semi_anti_join")]
use polars_utils::unique_column_name;
use sqlparser::ast::{BinaryOperator as SQLBinaryOperator, Expr as SQLExpr, Query};
#[cfg(feature = "semi_anti_join")]
use sqlparser::ast::{Distinct, GroupByExpr, Select, SelectItem, SetExpr, TableWithJoins};
use crate::SQLContext;
use crate::context::FilterMode;
#[cfg(feature = "semi_anti_join")]
use crate::context::get_table_name;
#[cfg(feature = "semi_anti_join")]
use crate::sql_expr::parse_sql_expr;
impl SQLContext {
pub(crate) fn rewrite_subquery_conjuncts<'a>(
&mut self,
mut lf: LazyFrame,
expr: &'a SQLExpr,
filter_mode: FilterMode,
schema: &Schema,
) -> PolarsResult<(LazyFrame, Vec<&'a SQLExpr>)> {
let residual = match filter_mode {
FilterMode::RemoveTrue => {
let mut unwrapped = expr;
while let SQLExpr::Nested(inner) = unwrapped {
unwrapped = inner;
}
match self.try_rewrite_subquery_conjunct(&lf, unwrapped, filter_mode, schema)? {
Some(new_lf) => {
lf = new_lf;
Vec::new()
},
None => vec![expr],
}
},
FilterMode::KeepTrue => {
let mut residual = Vec::new();
for conj in MintermIter::new(expr) {
if let Some(new_lf) =
self.try_rewrite_subquery_conjunct(&lf, conj, filter_mode, schema)?
{
lf = new_lf;
} else {
residual.push(conj);
}
}
residual
},
};
Ok((lf, residual))
}
fn try_rewrite_subquery_conjunct(
&mut self,
lf: &LazyFrame,
conj: &SQLExpr,
filter_mode: FilterMode,
schema: &Schema,
) -> PolarsResult<Option<LazyFrame>> {
let removing = filter_mode == FilterMode::RemoveTrue;
match conj {
SQLExpr::Exists { subquery, negated } => {
self.try_rewrite_exists_as_join(lf, subquery, *negated != removing, schema)
},
SQLExpr::InSubquery {
expr: lhs,
subquery,
negated,
} if !(*negated && removing) => self.try_rewrite_in_subquery_as_join(
lf,
lhs,
subquery,
*negated != removing,
filter_mode,
schema,
),
_ => Ok(None),
}
}
#[cfg(feature = "semi_anti_join")]
fn try_rewrite_exists_as_join(
&mut self,
lf: &LazyFrame,
subquery: &Query,
negated: bool,
outer_schema: &Schema,
) -> PolarsResult<Option<LazyFrame>> {
let Some(select) = eligible_subquery_select(subquery) else {
return Ok(None);
};
let Some(selection) = &select.selection else {
return Ok(None);
};
let mut ctx = self.isolated();
let Some((inner_names, inner_lf, inner_schema)) =
ctx.resolve_subquery_from(&select.from[0])?
else {
return Ok(None);
};
let Some(SubqueryConjuncts {
left_on,
right_on,
local_filters,
}) = ctx.split_subquery_conjuncts(selection, &inner_names, &inner_schema, outer_schema)?
else {
return Ok(None);
};
if left_on.is_empty() {
return Ok(None);
}
Ok(Some(ctx.finish_decorrelated_join(
lf,
inner_lf,
left_on,
right_on,
local_filters,
negated,
)))
}
#[cfg(feature = "semi_anti_join")]
fn try_rewrite_in_subquery_as_join(
&mut self,
lf: &LazyFrame,
lhs: &SQLExpr,
subquery: &Query,
anti: bool,
filter_mode: FilterMode,
outer_schema: &Schema,
) -> PolarsResult<Option<LazyFrame>> {
let Some(select) = eligible_subquery_select(subquery) else {
return Ok(None);
};
if matches!(&select.distinct, Some(Distinct::On(_))) {
return Ok(None);
}
let [SelectItem::UnnamedExpr(proj) | SelectItem::ExprWithAlias { expr: proj, .. }] =
select.projection.as_slice()
else {
return Ok(None);
};
let left_key = parse_sql_expr(lhs, self, Some(outer_schema))?
.meta()
.undo_aliases();
if has_expr(&left_key, |e| matches!(e, Expr::SubPlan(_, _)))
|| !expr_to_leaf_column_names_iter(&left_key)
.all(|name| outer_schema.contains(name.as_str()))
{
return Ok(None);
}
let mut ctx = self.isolated();
let Some((inner_names, inner_lf, inner_schema)) =
ctx.resolve_subquery_from(&select.from[0])?
else {
return Ok(None);
};
let Some(right_key) = ctx.try_parse_inner_only_expr(proj, &inner_schema, outer_schema)?
else {
return Ok(None);
};
let right_key = right_key.meta().undo_aliases();
let SubqueryConjuncts {
mut left_on,
mut right_on,
local_filters,
} = match &select.selection {
Some(selection) => {
let Some(split) = ctx.split_subquery_conjuncts(
selection,
&inner_names,
&inner_schema,
outer_schema,
)?
else {
return Ok(None);
};
split
},
None => SubqueryConjuncts::default(),
};
let corr_outer = left_on.clone();
let corr_inner = right_on.clone();
left_on.insert(0, left_key.clone());
right_on.insert(0, right_key.clone());
let inner_lf = local_filters.into_iter().fold(inner_lf, LazyFrame::filter);
inner_lf.set_cached_arena(ctx.lp_arena, ctx.expr_arena);
let joined = build_semi_anti_join(lf, inner_lf.clone(), left_on, right_on, anti);
if !(anti && filter_mode == FilterMode::KeepTrue) {
return Ok(Some(joined));
}
Ok(Some(refine_not_in_anti_join(
joined,
inner_lf,
&left_key,
&right_key,
&corr_outer,
&corr_inner,
)))
}
#[cfg(feature = "semi_anti_join")]
fn finish_decorrelated_join(
self,
lf: &LazyFrame,
inner_lf: LazyFrame,
left_on: Vec<Expr>,
right_on: Vec<Expr>,
local_filters: Vec<Expr>,
anti: bool,
) -> LazyFrame {
let inner_lf = local_filters.into_iter().fold(inner_lf, LazyFrame::filter);
inner_lf.set_cached_arena(self.lp_arena, self.expr_arena);
build_semi_anti_join(lf, inner_lf, left_on, right_on, anti)
}
#[cfg(feature = "semi_anti_join")]
fn resolve_subquery_from(
&mut self,
tbl_expr: &TableWithJoins,
) -> PolarsResult<Option<(PlHashSet<String>, LazyFrame, SchemaRef)>> {
let Some(inner_names) = std::iter::once(&tbl_expr.relation)
.chain(tbl_expr.joins.iter().map(|j| &j.relation))
.map(get_table_name)
.collect::<Option<PlHashSet<_>>>()
else {
return Ok(None);
};
let mut inner_lf = self.execute_from_statement(tbl_expr)?;
let inner_schema = self.get_frame_schema(&mut inner_lf)?;
Ok(Some((inner_names, inner_lf, inner_schema)))
}
#[cfg(feature = "semi_anti_join")]
fn split_subquery_conjuncts(
&mut self,
selection: &SQLExpr,
inner_names: &PlHashSet<String>,
inner_schema: &Schema,
outer_schema: &Schema,
) -> PolarsResult<Option<SubqueryConjuncts>> {
let mut left_on = Vec::new();
let mut right_on = Vec::new();
let mut local_filters = Vec::new();
for conj in MintermIter::new(selection) {
if let Some((outer_key, inner_key)) =
correlation_key_pair(conj, inner_names, inner_schema, outer_schema)
{
left_on.push(col(outer_key));
right_on.push(col(inner_key));
continue;
}
let Some(filter) = self.try_parse_inner_only_expr(conj, inner_schema, outer_schema)?
else {
return Ok(None);
};
local_filters.push(filter);
}
Ok(Some(SubqueryConjuncts {
left_on,
right_on,
local_filters,
}))
}
#[cfg(feature = "semi_anti_join")]
fn try_parse_inner_only_expr(
&mut self,
sql_expr: &SQLExpr,
inner_schema: &Schema,
outer_schema: &Schema,
) -> PolarsResult<Option<Expr>> {
let expr = parse_sql_expr(sql_expr, self, Some(inner_schema))?;
if has_expr(&expr, |e| matches!(e, Expr::SubPlan(_, _))) {
return Ok(None);
}
let only_inner = expr_to_leaf_column_names_iter(&expr).all(|name| {
inner_schema.contains(name.as_str()) && !outer_schema.contains(name.as_str())
});
Ok(only_inner.then_some(expr))
}
#[cfg(not(feature = "semi_anti_join"))]
fn try_rewrite_exists_as_join(
&mut self,
_lf: &LazyFrame,
_subquery: &Query,
_negated: bool,
_outer_schema: &Schema,
) -> PolarsResult<Option<LazyFrame>> {
Ok(None)
}
#[cfg(not(feature = "semi_anti_join"))]
#[expect(clippy::too_many_arguments)]
fn try_rewrite_in_subquery_as_join(
&mut self,
_lf: &LazyFrame,
_lhs: &SQLExpr,
_subquery: &Query,
_anti: bool,
_filter_mode: FilterMode,
_outer_schema: &Schema,
) -> PolarsResult<Option<LazyFrame>> {
Ok(None)
}
}
#[cfg(feature = "semi_anti_join")]
fn build_semi_anti_join(
lf: &LazyFrame,
inner_lf: LazyFrame,
left_on: Vec<Expr>,
right_on: Vec<Expr>,
anti: bool,
) -> LazyFrame {
let join_type = if anti { JoinType::Anti } else { JoinType::Semi };
lf.clone()
.join_builder()
.with(inner_lf)
.left_on(left_on)
.right_on(right_on)
.how(join_type)
.finish()
}
#[cfg(feature = "semi_anti_join")]
fn refine_not_in_anti_join(
joined: LazyFrame,
inner_lf: LazyFrame,
left_key: &Expr,
right_key: &Expr,
corr_outer: &[Expr],
corr_inner: &[Expr],
) -> LazyFrame {
if corr_inner.is_empty() {
let flag_name = unique_column_name();
let flag = when(len().eq(lit(0u32)))
.then(lit(NULL).cast(DataType::Boolean))
.otherwise(right_key.clone().is_null().any(true))
.alias(flag_name.clone());
let keep = when(col(flag_name.clone()).is_null())
.then(lit(true)) .when(col(flag_name.clone())) .then(lit(false))
.otherwise(left_key.clone().is_not_null());
return joined
.join_builder()
.with(inner_lf.select([flag]))
.how(JoinType::Cross)
.finish()
.filter(keep)
.drop(Selector::ByName {
names: [flag_name].into(),
strict: true,
});
}
let corr_keys = |lf: LazyFrame| lf.select(corr_inner).unique(None, UniqueKeepStrategy::Any);
let exclude_groups = |rows: LazyFrame, groups: LazyFrame| {
rows.join_builder()
.with(groups)
.left_on(corr_outer)
.right_on(corr_inner)
.how(JoinType::Anti)
.finish()
};
let kept_non_null = exclude_groups(
joined.clone().filter(left_key.clone().is_not_null()),
corr_keys(inner_lf.clone().filter(right_key.clone().is_null())),
);
let kept_null = exclude_groups(
joined.filter(left_key.clone().is_null()),
corr_keys(inner_lf),
);
concat(
[kept_non_null, kept_null],
UnionArgs {
rechunk: false,
parallel: true,
..Default::default()
},
)
.expect("'NOT IN' 3VL union has identical schemas")
}
struct MintermIter<'a> {
stack: Vec<&'a SQLExpr>,
}
impl<'a> Iterator for MintermIter<'a> {
type Item = &'a SQLExpr;
fn next(&mut self) -> Option<Self::Item> {
let mut top = self.stack.pop()?;
loop {
match top {
SQLExpr::Nested(inner) => top = inner,
SQLExpr::BinaryOp {
left,
op: SQLBinaryOperator::And,
right,
} => {
self.stack.push(right);
top = left;
},
_ => return Some(top),
}
}
}
}
impl<'a> MintermIter<'a> {
fn new(root: &'a SQLExpr) -> Self {
Self { stack: vec![root] }
}
}
#[cfg(feature = "semi_anti_join")]
enum CorrelationSide {
Inner,
Outer,
}
#[cfg(feature = "semi_anti_join")]
#[derive(Default)]
struct SubqueryConjuncts {
left_on: Vec<Expr>,
right_on: Vec<Expr>,
local_filters: Vec<Expr>,
}
#[cfg(feature = "semi_anti_join")]
fn correlation_key_pair(
conj: &SQLExpr,
inner_names: &PlHashSet<String>,
inner_schema: &Schema,
outer_schema: &Schema,
) -> Option<(PlSmallStr, PlSmallStr)> {
let SQLExpr::BinaryOp {
left,
op: SQLBinaryOperator::Eq,
right,
} = conj
else {
return None;
};
let (lside, lname) =
classify_correlation_column(left, inner_names, inner_schema, outer_schema)?;
let (rside, rname) =
classify_correlation_column(right, inner_names, inner_schema, outer_schema)?;
match (lside, rside) {
(CorrelationSide::Outer, CorrelationSide::Inner) => Some((lname, rname)),
(CorrelationSide::Inner, CorrelationSide::Outer) => Some((rname, lname)),
_ => None,
}
}
#[cfg(feature = "semi_anti_join")]
fn classify_correlation_column(
expr: &SQLExpr,
inner_names: &PlHashSet<String>,
inner_schema: &Schema,
outer_schema: &Schema,
) -> Option<(CorrelationSide, PlSmallStr)> {
let (qualifier, name): (Option<&str>, PlSmallStr) = match expr {
SQLExpr::Identifier(ident) => (None, ident.value.as_str().into()),
SQLExpr::CompoundIdentifier(parts) => {
let (last, init) = parts.split_last()?;
(
init.last().map(|q| q.value.as_str()),
last.value.as_str().into(),
)
},
_ => return None,
};
match qualifier {
Some(q) if inner_names.contains(q) => inner_schema
.contains(name.as_str())
.then_some((CorrelationSide::Inner, name)),
Some(_) => outer_schema
.contains(name.as_str())
.then_some((CorrelationSide::Outer, name)),
None => match (
inner_schema.contains(name.as_str()),
outer_schema.contains(name.as_str()),
) {
(true, false) => Some((CorrelationSide::Inner, name)),
(false, true) => Some((CorrelationSide::Outer, name)),
_ => None,
},
}
}
#[cfg(feature = "semi_anti_join")]
fn eligible_subquery_select(subquery: &Query) -> Option<&Select> {
let Query {
with, body,
order_by: _, limit_clause, fetch, locks: _, for_clause, settings, format_clause: _, pipe_operators, } = subquery;
if with.is_some()
|| limit_clause.is_some()
|| fetch.is_some()
|| for_clause.is_some()
|| settings.is_some()
|| !pipe_operators.is_empty()
{
return None;
}
let SetExpr::Select(select) = body.as_ref() else {
return None;
};
let Select {
select_token: _,
distinct: _,
top, top_before_distinct: _, projection: _,
exclude: _,
into, from, lateral_views, prewhere, selection: _, group_by, cluster_by: _, distribute_by: _,
sort_by: _,
having, named_window: _, qualify, window_before_qualify: _, value_table_mode, connect_by, optimizer_hints, select_modifiers, flavor: _, } = select.as_ref();
let no_group_by = matches!(
group_by,
GroupByExpr::Expressions(e, m) if e.is_empty() && m.is_empty()
);
if from.len() != 1
|| !no_group_by
|| top.is_some()
|| into.is_some()
|| having.is_some()
|| qualify.is_some()
|| prewhere.is_some()
|| !connect_by.is_empty()
|| value_table_mode.is_some()
|| !lateral_views.is_empty()
|| !optimizer_hints.is_empty()
|| select_modifiers.is_some()
{
return None;
}
Some(select)
}