Skip to content

Commit 3506840

Browse files
committed
Merge upstream DataFusion 55 maintenance fixes
2 parents 0409f67 + 1540853 commit 3506840

16 files changed

Lines changed: 2692 additions & 65 deletions

File tree

‎datafusion/common/src/nested_struct.rs‎

Lines changed: 711 additions & 2 deletions
Large diffs are not rendered by default.

‎datafusion/core/tests/physical_optimizer/enforce_sorting.rs‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,40 @@ async fn test_remove_unnecessary_sort5() -> Result<()> {
243243
Ok(())
244244
}
245245

246+
#[tokio::test]
247+
async fn test_propagate_in_right_mark_join() -> Result<()> {
248+
let left_schema = create_test_schema2()?;
249+
let right_schema = create_test_schema3()?;
250+
let left_input = memory_exec(&left_schema);
251+
let parquet_ordering = [sort_expr("a", &right_schema)].into();
252+
let right_input =
253+
parquet_exec_with_sort(right_schema.clone(), vec![parquet_ordering]);
254+
let on = vec![(
255+
Arc::new(Column::new_with_schema("col_a", &left_schema)?) as _,
256+
Arc::new(Column::new_with_schema("c", &right_schema)?) as _,
257+
)];
258+
let join = hash_join_exec(left_input, right_input, on, None, &JoinType::RightMark)?;
259+
let physical_plan = sort_exec([sort_expr("a", &join.schema())].into(), join);
260+
261+
let test = EnforceSortingTest::new(physical_plan).with_repartition_sorts(true);
262+
assert_snapshot!(test.run(), @r"
263+
Input Plan:
264+
SortExec: expr=[a@0 ASC], preserve_partitioning=[false]
265+
HashJoinExec: mode=Partitioned, join_type=RightMark, on=[(col_a@0, c@2)]
266+
DataSourceExec: partitions=1, partition_sizes=[0]
267+
DataSourceExec: file_groups={1 group: [[x]]}, projection=[a, b, c, d, e], output_ordering=[a@0 ASC], file_type=parquet
268+
269+
Optimized Plan:
270+
SortPreservingMergeExec: [a@0 ASC]
271+
HashJoinExec: mode=Partitioned, join_type=RightMark, on=[(col_a@0, c@2)]
272+
RepartitionExec: partitioning=Hash([col_a@0], 10), input_partitions=1
273+
DataSourceExec: partitions=1, partition_sizes=[0]
274+
RepartitionExec: partitioning=Hash([c@2], 10), input_partitions=1, maintains_sort_order=true
275+
DataSourceExec: file_groups={1 group: [[x]]}, projection=[a, b, c, d, e], output_ordering=[a@0 ASC], file_type=parquet
276+
");
277+
Ok(())
278+
}
279+
246280
#[tokio::test]
247281
async fn test_hash_join_interleaved_projection_preserves_parent_sort() -> Result<()> {
248282
let left_schema = create_test_schema()?;

‎datafusion/core/tests/sql/aggregates/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,3 +1021,4 @@ pub fn split_fuzz_timestamp_data_into_batches(
10211021

10221022
pub mod basic;
10231023
pub mod dict_nulls;
1024+
mod nested_nullability;
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
//! Regression tests for aggregating batches whose data types are *stricter*
19+
//! than the table's declared schema. See
20+
//! <https://github.com/apache/datafusion/issues/24069>.
21+
//!
22+
//! Builds on the end-to-end reproducer from #24278 by @alamb.
23+
//!
24+
//! A `RecordBatch` is a valid instance of a schema that is a superset of its
25+
//! own (see [`Schema::contains`] / `Field::contains`): most commonly the
26+
//! schema declares a (possibly nested) field as nullable while the batch's
27+
//! arrays mark it non-nullable. `MemTable::try_new` accepts such batches via
28+
//! exactly that check, and engines embedding DataFusion (e.g. Comet) feed
29+
//! such batches over FFI. Aggregations must therefore not fail when the
30+
//! runtime arrays are stricter than the planned schema.
31+
//!
32+
//! [`Schema::contains`]: arrow::datatypes::Schema::contains
33+
34+
use std::sync::Arc;
35+
36+
use arrow::array::{BooleanArray, RecordBatch, StructArray, UInt32Array};
37+
use arrow::datatypes::{DataType, Field, Fields, Schema, SchemaRef};
38+
use datafusion::datasource::MemTable;
39+
use datafusion::datasource::memory::MemorySourceConfig;
40+
use datafusion::physical_expr::aggregate::AggregateExprBuilder;
41+
use datafusion::physical_plan::ExecutionPlan;
42+
use datafusion::physical_plan::aggregates::{
43+
AggregateExec, AggregateMode, PhysicalGroupBy,
44+
};
45+
use datafusion::physical_plan::collect;
46+
use datafusion::physical_plan::expressions::col;
47+
use datafusion::prelude::*;
48+
use datafusion_common::Result;
49+
use datafusion_execution::TaskContext;
50+
use datafusion_execution::memory_pool::FairSpillPool;
51+
use datafusion_execution::runtime_env::RuntimeEnvBuilder;
52+
use datafusion_functions_aggregate::array_agg::array_agg_udaf;
53+
54+
/// Returns the fields of the struct column `b`: a single `colA Boolean`.
55+
///
56+
/// `col_a_nullable` controls whether `colA` is declared nullable — the only
57+
/// difference between the table's declared schema (`true`) and the actual
58+
/// batches (`false`).
59+
fn make_struct_fields(col_a_nullable: bool) -> Fields {
60+
Fields::from(vec![Field::new("colA", DataType::Boolean, col_a_nullable)])
61+
}
62+
63+
/// Returns the schema `(a UInt32 NOT NULL, b Struct("colA" Boolean) NOT NULL)`
64+
/// with the nested field `b.colA` nullable per `col_a_nullable`.
65+
///
66+
/// See [`make_struct_fields`].
67+
fn make_schema(col_a_nullable: bool) -> SchemaRef {
68+
Arc::new(Schema::new(vec![
69+
Field::new("a", DataType::UInt32, false),
70+
Field::new(
71+
"b",
72+
DataType::Struct(make_struct_fields(col_a_nullable)),
73+
false,
74+
),
75+
]))
76+
}
77+
78+
/// Runs a SQL aggregation over a table whose batches are stricter than its
79+
/// declared schema.
80+
///
81+
/// [`Self::run`] registers table `t(a UInt32, b Struct("colA" Boolean))`
82+
/// where the declared schema marks the nested field `colA` as nullable, but
83+
/// the batches carry a stricter, non-nullable `colA`, then runs the query
84+
/// and returns the collected result.
85+
struct AggregateBatchesTest {
86+
/// Number of rows in the table. `a` is `0..num_rows` (so also the number
87+
/// of groups for `GROUP BY a`) and `b.colA` alternates `true` / `false`.
88+
num_rows: u32,
89+
/// If set, the context uses a [`FairSpillPool`] of this size (and a small
90+
/// batch size) so the aggregation is forced to spill.
91+
memory_limit: Option<usize>,
92+
}
93+
94+
impl AggregateBatchesTest {
95+
fn new() -> Self {
96+
Self {
97+
num_rows: 100,
98+
memory_limit: None,
99+
}
100+
}
101+
102+
fn with_num_rows(mut self, num_rows: u32) -> Self {
103+
self.num_rows = num_rows;
104+
self
105+
}
106+
107+
fn with_memory_limit(mut self, memory_limit: usize) -> Self {
108+
self.memory_limit = Some(memory_limit);
109+
self
110+
}
111+
112+
/// Runs `sql` against the table described above and asserts the result
113+
/// has one output row per group (i.e. [`Self::num_rows`] rows in total).
114+
async fn run(self, sql: &str) -> Result<()> {
115+
// The table's declared schema: the nested field `b.colA` is
116+
// nullable ...
117+
let declared_schema = make_schema(true);
118+
119+
// ... while the batches are stricter: `b.colA` is non-nullable.
120+
// `MemTable::try_new` accepts this combination via
121+
// `Schema::contains`.
122+
let batch_struct_fields = make_struct_fields(false);
123+
let batch = RecordBatch::try_new(
124+
make_schema(false),
125+
vec![
126+
Arc::new(UInt32Array::from_iter_values(0..self.num_rows)),
127+
Arc::new(StructArray::new(
128+
batch_struct_fields,
129+
vec![Arc::new(BooleanArray::from_iter(
130+
(0..self.num_rows).map(|i| Some(i % 2 == 0)),
131+
))],
132+
None,
133+
)),
134+
],
135+
)?;
136+
137+
let table = MemTable::try_new(declared_schema, vec![vec![batch]])?;
138+
139+
let ctx = match self.memory_limit {
140+
Some(limit) => {
141+
let runtime = RuntimeEnvBuilder::new()
142+
.with_memory_pool(Arc::new(FairSpillPool::new(limit)))
143+
.build_arc()?;
144+
SessionContext::new_with_config_rt(
145+
SessionConfig::new().with_batch_size(100),
146+
runtime,
147+
)
148+
}
149+
None => SessionContext::new(),
150+
};
151+
ctx.register_table("t", Arc::new(table))?;
152+
153+
let result = ctx.sql(sql).await?.collect().await?;
154+
155+
let total_rows: usize = result.iter().map(|batch| batch.num_rows()).sum();
156+
assert_eq!(total_rows, self.num_rows as usize);
157+
Ok(())
158+
}
159+
}
160+
161+
#[tokio::test]
162+
async fn array_agg_struct_from_stricter_batches() -> Result<()> {
163+
AggregateBatchesTest::new()
164+
.run("SELECT a, array_agg(b) FROM t GROUP BY a")
165+
.await
166+
}
167+
168+
#[tokio::test]
169+
async fn array_agg_distinct_struct_from_stricter_batches() -> Result<()> {
170+
AggregateBatchesTest::new()
171+
.run("SELECT a, array_agg(DISTINCT b) FROM t GROUP BY a")
172+
.await
173+
}
174+
175+
#[tokio::test]
176+
async fn array_agg_struct_from_stricter_batches_with_spilling() -> Result<()> {
177+
AggregateBatchesTest::new()
178+
.with_num_rows(10_000)
179+
.with_memory_limit(4_000_000)
180+
.run("SELECT a, array_agg(b) FROM t GROUP BY a")
181+
.await
182+
}
183+
184+
#[tokio::test]
185+
async fn array_agg_distinct_struct_from_stricter_batches_with_spilling() -> Result<()> {
186+
AggregateBatchesTest::new()
187+
.with_num_rows(10_000)
188+
.with_memory_limit(4_000_000)
189+
.run("SELECT a, array_agg(DISTINCT b) FROM t GROUP BY a")
190+
.await
191+
}
192+
193+
/// Direct unit test for `AggregateExec` boundary adaptation:
194+
/// Feeds `AggregateExec` directly from a `MemorySourceConfig` whose batches carry
195+
/// a stricter nested struct nullability than the plan schema without going
196+
/// through `MemTable`.
197+
#[tokio::test]
198+
async fn test_aggregate_exec_direct_input_adaptation() -> Result<()> {
199+
let declared_schema = make_schema(true);
200+
let batch_struct_fields = make_struct_fields(false);
201+
let num_rows = 100_u32;
202+
let stricter_batch = RecordBatch::try_new(
203+
make_schema(false),
204+
vec![
205+
Arc::new(UInt32Array::from_iter_values(0..num_rows)),
206+
Arc::new(StructArray::new(
207+
batch_struct_fields,
208+
vec![Arc::new(BooleanArray::from_iter(
209+
(0..num_rows).map(|i| Some(i % 2 == 0)),
210+
))],
211+
None,
212+
)),
213+
],
214+
)?;
215+
216+
let input_plan: Arc<dyn ExecutionPlan> = MemorySourceConfig::try_new_exec(
217+
&[vec![stricter_batch]],
218+
Arc::clone(&declared_schema),
219+
None,
220+
)?;
221+
222+
let grouping_set =
223+
PhysicalGroupBy::new_single(vec![(col("a", &declared_schema)?, "a".to_string())]);
224+
let aggregates = vec![Arc::new(
225+
AggregateExprBuilder::new(array_agg_udaf(), vec![col("b", &declared_schema)?])
226+
.schema(Arc::clone(&declared_schema))
227+
.alias("array_agg(b)")
228+
.build()?,
229+
)];
230+
231+
let agg_exec = Arc::new(AggregateExec::try_new(
232+
AggregateMode::Single,
233+
grouping_set,
234+
aggregates,
235+
vec![None],
236+
input_plan,
237+
Arc::clone(&declared_schema),
238+
)?);
239+
240+
let task_ctx = Arc::new(TaskContext::default());
241+
let results = collect(agg_exec, task_ctx).await?;
242+
243+
let total_rows: usize = results.iter().map(|b| b.num_rows()).sum();
244+
assert_eq!(total_rows, num_rows as usize);
245+
Ok(())
246+
}

‎datafusion/ffi/src/proto/logical_extension_codec.rs‎

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,15 @@ impl Drop for FFI_LogicalExtensionCodec {
294294

295295
impl FFI_LogicalExtensionCodec {
296296
/// Creates a new [`FFI_LogicalExtensionCodec`].
297+
///
298+
/// If `codec` is already foreign, this re-exports its original FFI handle
299+
/// rather than adding another wrapper layer. The handle still adopts the
300+
/// `task_ctx_provider` supplied here, so it is never silently discarded and
301+
/// an imported codec can be rebound to a different session.
302+
///
303+
/// `runtime` is only honored when a new wrapper is created. An
304+
/// already-foreign handle keeps the runtime of the library that owns it,
305+
/// because that value lives in private data this side cannot reach.
297306
pub fn new(
298307
codec: Arc<dyn LogicalExtensionCodec>,
299308
runtime: Option<Handle>,
@@ -302,7 +311,9 @@ impl FFI_LogicalExtensionCodec {
302311
if let Some(codec) = (Arc::clone(&codec) as Arc<dyn Any>)
303312
.downcast_ref::<ForeignLogicalExtensionCodec>()
304313
{
305-
return codec.0.clone();
314+
let mut codec = codec.0.clone();
315+
codec.task_ctx_provider = task_ctx_provider.into();
316+
return codec;
306317
}
307318

308319
let task_ctx_provider = task_ctx_provider.into();
@@ -507,7 +518,9 @@ mod tests {
507518
use datafusion_proto::logical_plan::LogicalExtensionCodec;
508519
use datafusion_proto::physical_plan::PhysicalExtensionCodec;
509520

510-
use crate::proto::logical_extension_codec::FFI_LogicalExtensionCodec;
521+
use crate::proto::logical_extension_codec::{
522+
FFI_LogicalExtensionCodec, ForeignLogicalExtensionCodec,
523+
};
511524
use crate::proto::physical_extension_codec::tests::TestExtensionCodec;
512525

513526
fn create_test_table() -> MemTable {
@@ -727,4 +740,61 @@ mod tests {
727740
let foreign_codec: Arc<dyn LogicalExtensionCodec> = (&ffi_codec).into();
728741
assert!(!arc_ptr_eq(&foreign_codec, &codec));
729742
}
743+
744+
/// Importing a codec and re-wrapping it with a different task context
745+
/// provider must rebind the handle. See
746+
/// <https://github.com/apache/datafusion/issues/24722>.
747+
#[test]
748+
fn ffi_logical_extension_codec_rebind_adopts_task_ctx_provider() {
749+
let (_ctx_a, provider_a) = crate::util::tests::test_session_and_ctx();
750+
let (ctx_b, provider_b) = crate::util::tests::test_session_and_ctx();
751+
752+
let mut ffi_codec = FFI_LogicalExtensionCodec::new(
753+
Arc::new(TestExtensionCodec {}) as Arc<dyn LogicalExtensionCodec>,
754+
None,
755+
provider_a,
756+
);
757+
ffi_codec.library_marker_id = crate::mock_foreign_marker_id;
758+
759+
let imported: Arc<dyn LogicalExtensionCodec> = (&ffi_codec).into();
760+
assert!(
761+
(Arc::clone(&imported) as Arc<dyn std::any::Any>)
762+
.downcast_ref::<ForeignLogicalExtensionCodec>()
763+
.is_some()
764+
);
765+
766+
let rebound = FFI_LogicalExtensionCodec::new(imported, None, provider_b);
767+
768+
let task_ctx: Arc<TaskContext> = (&rebound.task_ctx_provider)
769+
.try_into()
770+
.expect("rebound codec resolves");
771+
assert_eq!(task_ctx.session_id(), ctx_b.task_ctx().session_id());
772+
}
773+
774+
/// Because the provider is held as a `Weak`, a codec that cannot be rebound
775+
/// forces callers to keep the original session alive. Once rebinding works,
776+
/// dropping it must not invalidate the handle.
777+
#[test]
778+
fn ffi_logical_extension_codec_rebind_releases_original_session() {
779+
let (ctx_b, provider_b) = crate::util::tests::test_session_and_ctx();
780+
781+
let rebound = {
782+
let (ctx_a, provider_a) = crate::util::tests::test_session_and_ctx();
783+
let mut ffi_codec = FFI_LogicalExtensionCodec::new(
784+
Arc::new(TestExtensionCodec {}) as Arc<dyn LogicalExtensionCodec>,
785+
None,
786+
provider_a,
787+
);
788+
ffi_codec.library_marker_id = crate::mock_foreign_marker_id;
789+
let imported: Arc<dyn LogicalExtensionCodec> = (&ffi_codec).into();
790+
drop(ctx_a);
791+
792+
FFI_LogicalExtensionCodec::new(imported, None, provider_b)
793+
};
794+
795+
let task_ctx: Arc<TaskContext> = (&rebound.task_ctx_provider)
796+
.try_into()
797+
.expect("rebound codec must not depend on the original session");
798+
assert_eq!(task_ctx.session_id(), ctx_b.task_ctx().session_id());
799+
}
730800
}

0 commit comments

Comments
 (0)