@@ -26,9 +26,13 @@ mod nlj_spill_unmatched;
2626mod repartition_mem_limit;
2727mod union_nullable_spill;
2828mod view_spill_compaction;
29- use arrow:: array:: { ArrayRef , DictionaryArray , Int32Array , RecordBatch , StringViewArray } ;
29+ use arrow:: array:: {
30+ ArrayRef , DictionaryArray , Int32Array , Int64Array , Int64Builder , ListBuilder ,
31+ RecordBatch , StringViewArray , StructArray ,
32+ } ;
33+ use arrow:: buffer:: NullBuffer ;
3034use arrow:: compute:: SortOptions ;
31- use arrow:: datatypes:: { Int32Type , SchemaRef } ;
35+ use arrow:: datatypes:: { Fields , Int32Type , SchemaRef } ;
3236use arrow_schema:: { DataType , Field , Schema } ;
3337use datafusion:: assert_batches_eq;
3438use datafusion:: config:: SpillCompression ;
@@ -43,6 +47,7 @@ use datafusion::physical_plan::{ExecutionPlan, SendableRecordBatchStream};
4347use datafusion:: prelude:: { SessionConfig , SessionContext } ;
4448use datafusion_catalog:: Session ;
4549use datafusion_catalog:: streaming:: StreamingTable ;
50+ use datafusion_common:: test_util:: batches_to_sort_string;
4651use datafusion_common:: { Result , assert_contains} ;
4752use datafusion_execution:: TaskContext ;
4853use datafusion_execution:: disk_manager:: { DiskManagerBuilder , DiskManagerMode } ;
@@ -133,7 +138,6 @@ async fn group_by_hash() {
133138#[ cfg( not( feature = "force_hash_collisions" ) ) ]
134139mod count_distinct_spill {
135140 use super :: * ;
136- use arrow:: array:: Int64Array ;
137141 use datafusion:: assert_batches_sorted_eq;
138142
139143 /// `count(distinct)` over integers under a memory limit.
@@ -220,6 +224,117 @@ mod count_distinct_spill {
220224 }
221225}
222226
227+ /// `GROUP BY` on a single nested key in the legacy `GroupedHashAggregateStream`
228+ /// under a memory limit.
229+ ///
230+ /// After spilling, the legacy stream re-aggregates the merged spill files with
231+ /// `GroupOrderingFull`, which requires group ids in first-seen order. A single
232+ /// nested key has no specialized single-column group values implementation and
233+ /// is handled by the multi-column one, whose vectorized interning does not
234+ /// guarantee that order. If the stream keeps that implementation for the merge
235+ /// phase, groups are emitted while still in progress and come out duplicated.
236+ const NESTED_KEY_ROWS : usize = 200_000 ;
237+ const NESTED_KEY_GROUPS : i64 = 16 ;
238+ const NESTED_KEY_BATCH_ROWS : usize = 8_192 ;
239+
240+ /// Small enough that the final stages must spill their `count(distinct)`
241+ /// state, large enough for the merge of the spilled runs. A `FairSpillPool`
242+ /// caps every stage at its share, so the partial stages emit early and the
243+ /// final stages spill repeatedly, which produces the many small merged batches
244+ /// this bug needs.
245+ const NESTED_KEY_MEMORY_LIMIT : usize = 4 * 1024 * 1024 ;
246+
247+ fn nested_key_struct_fields ( ) -> Fields {
248+ Fields :: from ( vec ! [
249+ Field :: new( "list" , DataType :: new_list( DataType :: Int64 , true ) , true ) ,
250+ Field :: new( "num" , DataType :: Int64 , true ) ,
251+ ] )
252+ }
253+
254+ /// `st` is mostly `{list: [g, g + 1], num: g}` for group `g`, with a sprinkle
255+ /// of null lists, empty lists, null nums and null structs so that keys of
256+ /// different shapes meet in the same batches. `v` is unique.
257+ fn nested_key_table ( ) -> MemTable {
258+ let schema = Arc :: new ( Schema :: new ( vec ! [
259+ Field :: new_struct( "st" , nested_key_struct_fields( ) , true ) ,
260+ Field :: new( "v" , DataType :: Int64 , false ) ,
261+ ] ) ) ;
262+ let batches = ( 0 ..NESTED_KEY_ROWS )
263+ . step_by ( NESTED_KEY_BATCH_ROWS )
264+ . map ( |start| {
265+ let rows = start..( start + NESTED_KEY_BATCH_ROWS ) . min ( NESTED_KEY_ROWS ) ;
266+ let mut list = ListBuilder :: new ( Int64Builder :: new ( ) ) ;
267+ let mut num = Vec :: with_capacity ( rows. len ( ) ) ;
268+ let mut valid = Vec :: with_capacity ( rows. len ( ) ) ;
269+ for row in rows. clone ( ) {
270+ let group = row as i64 % NESTED_KEY_GROUPS ;
271+ match row % 37 {
272+ 0 => list. append_null ( ) ,
273+ 1 => list. append ( true ) ,
274+ _ => {
275+ list. values ( ) . append_value ( group) ;
276+ list. values ( ) . append_value ( group + 1 ) ;
277+ list. append ( true ) ;
278+ }
279+ }
280+ num. push ( ( row % 41 != 0 ) . then_some ( group) ) ;
281+ valid. push ( row % 43 != 0 ) ;
282+ }
283+ let st = StructArray :: new (
284+ nested_key_struct_fields ( ) ,
285+ vec ! [ Arc :: new( list. finish( ) ) , Arc :: new( Int64Array :: from( num) ) ] ,
286+ Some ( NullBuffer :: from ( valid) ) ,
287+ ) ;
288+ RecordBatch :: try_new (
289+ Arc :: clone ( & schema) ,
290+ vec ! [
291+ Arc :: new( st) ,
292+ Arc :: new( Int64Array :: from_iter_values( rows. map( |row| row as i64 ) ) ) ,
293+ ] ,
294+ )
295+ . unwrap ( )
296+ } )
297+ . collect ( ) ;
298+ MemTable :: try_new ( schema, vec ! [ batches] ) . unwrap ( )
299+ }
300+
301+ const NESTED_KEY_QUERY : & str = "select st, count(v), count(distinct v), sum(v), avg(v), min(v), max(v) \
302+ from t group by st";
303+
304+ /// Runs the query on the legacy stream, with or without a memory limit.
305+ async fn run_nested_key_query ( memory_limit : Option < usize > ) -> String {
306+ let mut runtime =
307+ RuntimeEnvBuilder :: new ( ) . with_disk_manager_builder ( DiskManagerBuilder :: default ( ) ) ;
308+ if let Some ( limit) = memory_limit {
309+ runtime = runtime. with_memory_pool ( Arc :: new ( FairSpillPool :: new ( limit) ) ) ;
310+ }
311+ let config = SessionConfig :: new ( )
312+ . with_target_partitions ( 4 )
313+ // small batches: the merged spill stream arrives in many batches and
314+ // groups span batch boundaries
315+ . with_batch_size ( 64 )
316+ . set_bool ( "datafusion.execution.enable_migration_aggregate" , false ) ;
317+ let ctx = SessionContext :: new_with_config_rt ( config, runtime. build_arc ( ) . unwrap ( ) ) ;
318+ ctx. register_table ( "t" , Arc :: new ( nested_key_table ( ) ) )
319+ . unwrap ( ) ;
320+ let batches = ctx
321+ . sql ( NESTED_KEY_QUERY )
322+ . await
323+ . unwrap ( )
324+ . collect ( )
325+ . await
326+ . unwrap ( ) ;
327+ batches_to_sort_string ( & batches)
328+ }
329+
330+ #[ tokio:: test]
331+ async fn legacy_stream_nested_key_spill_keeps_groups_unique ( ) {
332+ let expected = run_nested_key_query ( None ) . await ;
333+ let actual = run_nested_key_query ( Some ( NESTED_KEY_MEMORY_LIMIT ) ) . await ;
334+ // A duplicated group shows up as extra rows with the counts split
335+ assert_eq ! ( actual, expected) ;
336+ }
337+
223338#[ tokio:: test]
224339async fn join_by_key_multiple_partitions ( ) {
225340 let config = SessionConfig :: new ( ) . with_target_partitions ( 2 ) ;
0 commit comments