Skip to content

Commit 90555f7

Browse files
authored
Merge branch 'main' into upstream-case-field-metadata
2 parents 279401f + d137137 commit 90555f7

32 files changed

Lines changed: 1924 additions & 166 deletions

File tree

‎.github/workflows/docs.yaml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,12 @@ jobs:
6767
run: |
6868
set -x
6969
cd asf-site/
70+
# don't delete old versions of the docs (see versions/ in asf-site)
7071
rsync \
7172
-a \
7273
--delete \
7374
--exclude '/.git/' \
75+
--exclude '/versions/' \
7476
../docs/build/html/ \
7577
./
7678
cp ../.asf.yaml .
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name Q18
2+
group nlj
3+
4+
expect_plan NestedLoopJoinExec
5+
expect_plan join_type=RightSemi
6+
7+
run
8+
-- Q18: RIGHT SEMI 4K x 4K | no matches; the maximum sum is 4096 * 4096 - 1
9+
SELECT r.value
10+
FROM range(4096) AS l
11+
RIGHT SEMI JOIN range(4096) AS r
12+
ON l.value * 4096 + r.value >= 4096 * 4096;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name Q19
2+
group nlj
3+
4+
expect_plan NestedLoopJoinExec
5+
expect_plan join_type=RightAnti
6+
7+
run
8+
-- Q19: RIGHT ANTI 4K x 4K | no matches; the maximum sum is 4096 * 4096 - 1
9+
SELECT r.value
10+
FROM range(4096) AS l
11+
RIGHT ANTI JOIN range(4096) AS r
12+
ON l.value * 4096 + r.value >= 4096 * 4096;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name Q20
2+
group nlj
3+
4+
expect_plan NestedLoopJoinExec
5+
expect_plan join_type=RightSemi
6+
7+
run
8+
-- Q20: RIGHT SEMI 2K x 2K | staged matches with 260-byte filter strings
9+
-- With batch_size=8192, candidates shrink 2048 -> 1024 -> 512 -> ... -> 16.
10+
-- Under that setting, the multi-left-row path starts at 512 candidates.
11+
SELECT r.value
12+
FROM (
13+
SELECT repeat('x', 256) || lpad(CAST(
14+
CASE WHEN value < 128 THEN 1024
15+
WHEN value < 256 THEN 1536
16+
WHEN value < 384 THEN 1792
17+
WHEN value < 512 THEN 1920
18+
WHEN value < 640 THEN 1984
19+
WHEN value < 768 THEN 2016
20+
ELSE 2032 END AS VARCHAR), 4, '0') AS threshold
21+
FROM range(2048)
22+
) AS l
23+
RIGHT SEMI JOIN (
24+
SELECT value,
25+
repeat('x', 256) || lpad(CAST(value AS VARCHAR), 4, '0') AS key
26+
FROM range(2048)
27+
) AS r
28+
ON l.threshold > r.key;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name Q21
2+
group nlj
3+
4+
expect_plan NestedLoopJoinExec
5+
expect_plan join_type=RightAnti
6+
7+
run
8+
-- Q21: RIGHT ANTI 2K x 2K | staged matches with 260-byte filter strings
9+
-- With batch_size=8192, candidates shrink 2048 -> 1024 -> 512 -> ... -> 16.
10+
-- Under that setting, the multi-left-row path starts at 512 candidates.
11+
SELECT r.value
12+
FROM (
13+
SELECT repeat('x', 256) || lpad(CAST(
14+
CASE WHEN value < 128 THEN 1024
15+
WHEN value < 256 THEN 1536
16+
WHEN value < 384 THEN 1792
17+
WHEN value < 512 THEN 1920
18+
WHEN value < 640 THEN 1984
19+
WHEN value < 768 THEN 2016
20+
ELSE 2032 END AS VARCHAR), 4, '0') AS threshold
21+
FROM range(2048)
22+
) AS l
23+
RIGHT ANTI JOIN (
24+
SELECT value,
25+
repeat('x', 256) || lpad(CAST(value AS VARCHAR), 4, '0') AS key
26+
FROM range(2048)
27+
) AS r
28+
ON l.threshold > r.key;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name Q22
2+
group nlj
3+
4+
expect_plan NestedLoopJoinExec
5+
expect_plan join_type=RightSemi
6+
7+
# Materialize the threshold so the filter keeps both join inputs.
8+
# Keep the left input smaller so the default optimizer preserves RightSemi.
9+
init
10+
CREATE TABLE nlj_repeated_left AS
11+
SELECT 3277 AS threshold FROM range(4096);
12+
13+
init
14+
CREATE TABLE nlj_repeated_right AS
15+
SELECT value AS id FROM range(8192);
16+
17+
run
18+
-- Repeated 40% matches: only the first probe adds matches.
19+
-- With one partition and batch_size=8192, 4915 survivors exceed the 4096-row
20+
-- compaction threshold, so every left row probes the full right batch.
21+
SELECT r.id
22+
FROM nlj_repeated_left l
23+
RIGHT SEMI JOIN nlj_repeated_right r ON l.threshold > r.id;
24+
25+
cleanup
26+
DROP TABLE nlj_repeated_left;
27+
28+
cleanup
29+
DROP TABLE nlj_repeated_right;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name Q23
2+
group nlj
3+
4+
expect_plan NestedLoopJoinExec
5+
expect_plan join_type=RightAnti
6+
7+
init
8+
CREATE TABLE nlj_final_left AS
9+
SELECT arrow_cast('m', 'Utf8') AS threshold;
10+
11+
# Materialize 32 MiB of Utf8 strings outside the measured query. Compaction
12+
# would copy these strings; different first bytes keep comparison cheap.
13+
init
14+
CREATE TABLE nlj_final_right AS
15+
SELECT value AS id,
16+
arrow_cast((CASE WHEN value < 4096 THEN 'a' ELSE 'z' END)
17+
|| repeat('x', 4095), 'Utf8') AS payload
18+
FROM range(8192);
19+
20+
run
21+
-- With one partition and batch_size=8192, half the batch survives the only
22+
-- left row. Compacting after that final probe would needlessly copy 16 MiB.
23+
-- Return IDs only to exclude string copying from output materialization.
24+
SELECT r.id
25+
FROM nlj_final_left l
26+
RIGHT ANTI JOIN nlj_final_right r ON l.threshold > r.payload;
27+
28+
cleanup
29+
DROP TABLE nlj_final_left;
30+
31+
cleanup
32+
DROP TABLE nlj_final_right;

‎datafusion/functions-nested/src/set_ops.rs‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -234,15 +234,15 @@ impl ScalarUDFImpl for ArrayIntersect {
234234

235235
#[user_doc(
236236
doc_section(label = "Array Functions"),
237-
description = "Returns distinct values from the array after removing duplicates.",
237+
description = "Returns distinct values from the array after removing duplicates, in the order they first appear.",
238238
syntax_example = "array_distinct(array)",
239239
sql_example = r#"```sql
240240
> select array_distinct([1, 3, 2, 3, 1, 2, 4]);
241-
+---------------------------------+
242-
| array_distinct(List([1,2,3,4])) |
243-
+---------------------------------+
244-
| [1, 2, 3, 4] |
245-
+---------------------------------+
241+
+---------------------------------------+
242+
| array_distinct(List([1,3,2,3,1,2,4])) |
243+
+---------------------------------------+
244+
| [1, 3, 2, 4] |
245+
+---------------------------------------+
246246
```"#,
247247
argument(
248248
name = "array",
@@ -297,7 +297,7 @@ impl ScalarUDFImpl for ArrayDistinct {
297297
}
298298

299299
/// array_distinct SQL function
300-
/// example: from list [1, 3, 2, 3, 1, 2, 4] to [1, 2, 3, 4]
300+
/// example: from list [1, 3, 2, 3, 1, 2, 4] to [1, 3, 2, 4]
301301
fn array_distinct_inner(args: &[ArrayRef]) -> Result<ArrayRef> {
302302
let [array] = take_function_args("array_distinct", args)?;
303303
match array.data_type() {

‎datafusion/optimizer/src/push_down_filter.rs‎

Lines changed: 68 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,8 +1135,8 @@ impl OptimizerRule for PushDownFilter {
11351135
// multiple window functions, each with potentially different partition keys.
11361136
// Therefore, we need to ensure that any potential partition key returned is used in
11371137
// ALL window functions. Otherwise, filters cannot be pushed by through that column.
1138-
fn extract_partition_keys(func: &WindowFunction) -> HashSet<Column> {
1139-
expr_columns(&func.params.partition_by)
1138+
fn extract_partition_keys(func: &WindowFunction) -> HashSet<&Expr> {
1139+
func.params.partition_by.iter().collect()
11401140
}
11411141

11421142
let potential_partition_keys = window
@@ -1172,21 +1172,23 @@ impl OptimizerRule for PushDownFilter {
11721172
let mut keep_predicates = vec![];
11731173
let mut push_predicates = vec![];
11741174
for expr in predicates {
1175-
let cols = expr.column_refs();
1176-
if cols.iter().all(|c| potential_partition_keys.contains(c)) {
1175+
// A volatile predicate has to stay above the window: pushing it
1176+
// changes which rows the window function sees.
1177+
if !expr.is_volatile()
1178+
&& reads_only_partition_keys(&expr, &potential_partition_keys)?
1179+
{
11771180
push_predicates.push(expr);
11781181
} else {
11791182
keep_predicates.push(expr);
11801183
}
11811184
}
11821185

11831186
// Unlike with aggregations, there are no cases where we have to replace, e.g.,
1184-
// `a+b` with Column(a)+Column(b). This is because partition expressions are not
1185-
// available as standalone columns to the user. For example, while an aggregation on
1186-
// `a+b` becomes Column(a + b), in a window partition it becomes
1187-
// `func() PARTITION BY [a + b] ...`. Thus, filters on expressions always remain in
1188-
// place, so we can use `push_predicates` directly. This is consistent with other
1189-
// optimizers, such as the one used by Postgres.
1187+
// `a+b` with Column(a+b). This is because partition expressions are not available
1188+
// as standalone columns to the user: while an aggregation on `a+b` becomes
1189+
// Column(a + b), in a window partition it stays `func() PARTITION BY [a + b] ...`.
1190+
// That is why the predicate is matched against the key expressions themselves and
1191+
// can be pushed unchanged.
11901192

11911193
// If we have a filter to push, we push it down to the input of the aggregate
11921194
let result = if let Some(predicate) = conjunction(push_predicates) {
@@ -1585,6 +1587,54 @@ fn with_filters(predicates: Vec<Expr>, plan: LogicalPlan) -> LogicalPlan {
15851587
}
15861588
}
15871589

1590+
/// Can `expr` be evaluated below a window with these `PARTITION BY` keys?
1591+
///
1592+
/// A predicate that reads only the partition keys is constant within each
1593+
/// partition, so filtering before the window drops whole partitions and leaves
1594+
/// the surviving rows' window values unchanged. "Reads only the keys" is checked
1595+
/// structurally: every column reference must sit inside a subtree that is equal
1596+
/// to one of the keys. Given `PARTITION BY a, b + c`:
1597+
///
1598+
/// * `a < 5`, `b + c = 4` and `(b + c) + 1 > 10` can be pushed down
1599+
/// * `d < 5` and `b < 5` cannot (`b` on its own is not a key), and neither can
1600+
/// `c + b = 4` (the match is structural, `c + b` is not `b + c`)
1601+
///
1602+
/// A predicate containing a subquery is never pushed: what the subquery reads is
1603+
/// not visible from the expression tree.
1604+
fn reads_only_partition_keys(
1605+
expr: &Expr,
1606+
partition_keys: &HashSet<&Expr>,
1607+
) -> Result<bool> {
1608+
let mut reads_something_else = false;
1609+
expr.apply(|node| {
1610+
Ok(if partition_keys.contains(&node) {
1611+
// the whole key was matched, so whatever it reads is accounted for
1612+
TreeNodeRecursion::Jump
1613+
} else if reads_beyond_this_node(node) {
1614+
reads_something_else = true;
1615+
TreeNodeRecursion::Stop
1616+
} else {
1617+
TreeNodeRecursion::Continue
1618+
})
1619+
})?;
1620+
Ok(!reads_something_else)
1621+
}
1622+
1623+
/// Does this node read data that walking its children cannot account for? A
1624+
/// column reads itself; a subquery reads whatever its plan reads, which
1625+
/// `Expr::apply` does not visit.
1626+
fn reads_beyond_this_node(node: &Expr) -> bool {
1627+
matches!(
1628+
node,
1629+
Expr::Column(_)
1630+
| Expr::OuterReferenceColumn(..)
1631+
| Expr::ScalarSubquery(_)
1632+
| Expr::Exists(_)
1633+
| Expr::InSubquery(_)
1634+
| Expr::SetComparison(_)
1635+
)
1636+
}
1637+
15881638
fn expr_columns(exprs: &[Expr]) -> HashSet<Column> {
15891639
exprs
15901640
.iter()
@@ -1956,10 +2006,12 @@ mod tests {
19562006
)
19572007
}
19582008

1959-
/// verifies that filters on partition expressions are not pushed, as the single expression
1960-
/// column is not available to the user, unlike with aggregations
2009+
/// verifies that a filter on an expression partition key is pushed; the
2010+
/// remaining shapes (mixed keys, operand order, subqueries, volatile
2011+
/// predicates, several windows) are covered in
2012+
/// `push_down_filter_regression.slt`
19612013
#[test]
1962-
fn filter_expression_keep_window() -> Result<()> {
2014+
fn filter_expression_move_window() -> Result<()> {
19632015
let table_scan = test_table_scan()?;
19642016

19652017
let window = Expr::from(WindowFunction::new(
@@ -1975,49 +2027,14 @@ mod tests {
19752027

19762028
let plan = LogicalPlanBuilder::from(table_scan)
19772029
.window(vec![window])?
1978-
// unlike with aggregations, single partition column "test.a + test.b" is not available
1979-
// to the plan, so we use multiple columns when filtering
1980-
.filter(add(col("a"), col("b")).gt(lit(10i64)))?
1981-
.build()?;
1982-
1983-
assert_optimized_plan_equal!(
1984-
plan,
1985-
@r"
1986-
Filter: test.a + test.b > Int64(10)
1987-
WindowAggr: windowExpr=[[rank() PARTITION BY [test.a + test.b] ORDER BY [test.c ASC NULLS FIRST] ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]
1988-
TableScan: test
1989-
"
1990-
)
1991-
}
1992-
1993-
/// verifies that filters are not pushed on order by columns (that are not used in partitioning)
1994-
#[test]
1995-
fn filter_order_keep_window() -> Result<()> {
1996-
let table_scan = test_table_scan()?;
1997-
1998-
let window = Expr::from(WindowFunction::new(
1999-
WindowFunctionDefinition::WindowUDF(
2000-
datafusion_functions_window::rank::rank_udwf(),
2001-
),
2002-
vec![],
2003-
))
2004-
.partition_by(vec![col("a")])
2005-
.order_by(vec![col("c").sort(true, true)])
2006-
.build()
2007-
.unwrap();
2008-
2009-
let plan = LogicalPlanBuilder::from(table_scan)
2010-
.window(vec![window])?
2011-
.filter(col("c").gt(lit(10i64)))?
2030+
.filter(add(col("a"), col("b")).gt(lit(10i64)))? // a + b > 10
20122031
.build()?;
2013-
assert_plan_not_transformed!(plan.clone());
20142032

20152033
assert_optimized_plan_equal!(
20162034
plan,
20172035
@r"
2018-
Filter: test.c > Int64(10)
2019-
WindowAggr: windowExpr=[[rank() PARTITION BY [test.a] ORDER BY [test.c ASC NULLS FIRST] ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]
2020-
TableScan: test
2036+
WindowAggr: windowExpr=[[rank() PARTITION BY [test.a + test.b] ORDER BY [test.c ASC NULLS FIRST] ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]
2037+
TableScan: test, full_filters=[test.a + test.b > Int64(10)]
20212038
"
20222039
)
20232040
}

0 commit comments

Comments
 (0)