Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2a542b3
fix: preserve duplicate counts in set operations
1fanwang Sep 25, 2026
92e52a7
fix: share multiset set operation planning across APIs
1fanwang Sep 25, 2026
09c4eaa
Merge current main and preserve Substrait nullability
1fanwang Sep 25, 2026
a03258c
fix: keep input qualifiers in INTERSECT ALL and EXCEPT ALL
1fanwang Sep 26, 2026
145bee4
docs: note the row_number requirement for INTERSECT ALL and EXCEPT ALL
1fanwang Sep 26, 2026
84ef8f7
test: cover INTERSECT ALL and EXCEPT ALL duplicate counts
1fanwang Sep 26, 2026
c765112
perf: plan INTERSECT ALL and EXCEPT ALL with grouped counts
1fanwang Sep 26, 2026
35fbc65
Merge upstream/main into 1fanwang-fix-set-operation-duplicates
1fanwang Sep 30, 2026
c42d494
Merge 1fanwang-fix-set-operation-duplicates into 1fannnw/set-operatio…
1fanwang Sep 30, 2026
08c3e79
Merge upstream/main into 1fanwang-fix-set-operation-duplicates
1fanwang Oct 1, 2026
1830174
Merge latest main and resolve set operation conflicts
1fanwang Oct 2, 2026
77e67eb
Merge origin/main into 1fanwang-fix-set-operation-duplicates
1fanwang Oct 7, 2026
9e378c6
Merge parent set-operation correctness fixes
1fanwang Oct 7, 2026
3372b2b
Merge latest main
1fanwang Oct 7, 2026
1f95f9a
Merge branch 'main' into 1fannnw/set-operation-grouped-count
kosiew Oct 15, 2026
bc0acb6
fix: count INTERSECT ALL and EXCEPT ALL rows in the type they are com…
1fanwang Oct 15, 2026
7bed929
test: cover empty inputs for INTERSECT ALL and EXCEPT ALL
1fanwang Oct 15, 2026
4dc0cdd
docs: note that INTERSECT ALL and EXCEPT ALL plans cannot be exported…
1fanwang Oct 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions datafusion/core/src/dataframe/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1963,7 +1963,16 @@ impl DataFrame {
pub fn intersect(self, dataframe: DataFrame) -> Result<DataFrame> {
let left_plan = self.plan;
let right_plan = dataframe.plan;
let plan = LogicalPlanBuilder::intersect(left_plan, right_plan, true)?;
let (Some(count), Some(range)) = (
self.session_state.aggregate_functions().get("count"),
self.session_state.scalar_functions().get("range"),
) else {
return plan_err!(
"DataFrame::intersect requires the count aggregate function and the range scalar function to be registered"
);
};
let plan =
LogicalPlanBuilder::intersect_all(left_plan, right_plan, count, range)?;
Ok(DataFrame {
session_state: self.session_state,
plan,
Expand Down Expand Up @@ -2041,7 +2050,15 @@ impl DataFrame {
pub fn except(self, dataframe: DataFrame) -> Result<DataFrame> {
let left_plan = self.plan;
let right_plan = dataframe.plan;
let plan = LogicalPlanBuilder::except(left_plan, right_plan, true)?;
let (Some(count), Some(range)) = (
self.session_state.aggregate_functions().get("count"),
self.session_state.scalar_functions().get("range"),
) else {
return plan_err!(
"DataFrame::except requires the count aggregate function and the range scalar function to be registered"
);
};
let plan = LogicalPlanBuilder::except_all(left_plan, right_plan, count, range)?;
Ok(DataFrame {
session_state: self.session_state,
plan,
Expand Down
101 changes: 80 additions & 21 deletions datafusion/core/tests/dataframe/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use arrow::datatypes::{
use arrow::error::ArrowError;
use arrow::util::pretty::pretty_format_batches;
use arrow_schema::{SortOptions, TimeUnit};
use datafusion::{assert_batches_eq, dataframe};
use datafusion::{assert_batches_eq, assert_batches_sorted_eq, dataframe};
use datafusion_common::metadata::FieldMetadata;
use datafusion_functions_aggregate::count::{count_all, count_all_window};
use datafusion_functions_aggregate::expr_fn::{
Expand Down Expand Up @@ -1846,31 +1846,90 @@ async fn sendable() {

#[tokio::test]
async fn intersect() -> Result<()> {
let df = test_table().await?.select_columns(&["c1", "c3"])?;
let d2 = df.clone();
let plan = df.intersect(d2)?;
let result = plan.logical_plan().clone();
let expected = create_plan(
"SELECT c1, c3 FROM aggregate_test_100
INTERSECT ALL SELECT c1, c3 FROM aggregate_test_100",
)
.await?;
assert_same_plan(&result, &expected);
let ctx = SessionContext::new();
let left = ctx
.sql(
"SELECT * FROM VALUES (NULL, 'n'), (NULL, 'n'), (1, 'x'), (1, 'x'), (2, 'y')",
)
.await?;
let right = ctx
.sql("SELECT * FROM VALUES (NULL, 'n'), (1, 'x'), (1, 'x'), (1, 'x'), (3, 'z')")
.await?;
let result = left.intersect(right)?.collect().await?;
assert_batches_sorted_eq!(
[
"+---------+---------+",
"| column1 | column2 |",
"+---------+---------+",
"| | n |",
"| 1 | x |",
"| 1 | x |",
"+---------+---------+",
],
&result
);
Ok(())
}

#[tokio::test]
async fn except() -> Result<()> {
let df = test_table().await?.select_columns(&["c1", "c3"])?;
let d2 = df.clone();
let plan = df.except(d2)?;
let result = plan.logical_plan().clone();
let expected = create_plan(
"SELECT c1, c3 FROM aggregate_test_100
EXCEPT ALL SELECT c1, c3 FROM aggregate_test_100",
)
.await?;
assert_same_plan(&result, &expected);
let ctx = SessionContext::new();
let left = ctx
.sql(
"SELECT * FROM VALUES (NULL, 'n'), (NULL, 'n'), (1, 'x'), (1, 'x'), (2, 'y')",
)
.await?;
let right = ctx
.sql("SELECT * FROM VALUES (NULL, 'n'), (1, 'x'), (1, 'x'), (1, 'x'), (3, 'z')")
.await?;
let result = left.except(right)?.collect().await?;
assert_batches_sorted_eq!(
[
"+---------+---------+",
"| column1 | column2 |",
"+---------+---------+",
"| | n |",
"| 2 | y |",
"+---------+---------+",
],
&result
);
Ok(())
}

#[tokio::test]
async fn intersect_and_except_count_rows_in_the_type_they_are_compared_in() -> Result<()>
{
// '1' and '01' are both the integer 1, so the strings are two copies of the
// row that the integers repeat twice, and the result is an integer column.
let ctx = SessionContext::new();
let integers = ctx.sql("SELECT * FROM VALUES (1), (1)").await?;
let strings = ctx.sql("SELECT * FROM VALUES ('1'), ('01')").await?;

let result = integers.clone().except(strings.clone())?.collect().await?;
assert_eq!(
result.iter().map(|batch| batch.num_rows()).sum::<usize>(),
0
);
let result = strings.clone().except(integers.clone())?.collect().await?;
assert_eq!(
result.iter().map(|batch| batch.num_rows()).sum::<usize>(),
0
);

let intersection = integers.intersect(strings)?;
assert_eq!(intersection.schema().field(0).data_type(), &DataType::Int64);
assert_batches_sorted_eq!(
[
"+---------+",
"| column1 |",
"+---------+",
"| 1 |",
"| 1 |",
"+---------+",
],
&intersection.collect().await?
);
Ok(())
}

Expand Down
1 change: 1 addition & 0 deletions datafusion/core/tests/sql/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ pub mod joins;
mod path_partition;
mod runtime_config;
pub mod select;
mod set_operations;
mod sql_api;
mod union_comparison;
mod union_nullable;
Expand Down
Loading
Loading