Describe the bug
nativeShuffleFailureReasons's supportedSerializableDataType accepts a StructType whose fields have duplicate names, so native shuffle is chosen for the plan. The batch is written and decoded natively without complaint, but the JVM cannot import the decoded struct back across the Arrow C data interface, and the task dies:
java.lang.IllegalStateException: ArrowArray struct has 2 children (expected 1)
at org.apache.arrow.util.Preconditions.checkState(Preconditions.java:562)
at org.apache.arrow.c.ArrayImporter.doImport(ArrayImporter.java:92)
at org.apache.arrow.c.ArrayImporter.importArray(ArrayImporter.java:68)
at org.apache.arrow.c.ArrowImporter.importVector(ArrowImporter.java:56)
at org.apache.comet.vector.NativeUtil.importVector(NativeUtil.scala:257)
at org.apache.comet.vector.NativeUtil.getNextBatch(NativeUtil.scala:210)
at org.apache.spark.sql.comet.execution.shuffle.NativeBatchDecoderIterator.fetchNext(NativeBatchDecoderIterator.scala:104)
at org.apache.spark.sql.comet.execution.shuffle.NativeBatchDecoderIterator.hasNext(NativeBatchDecoderIterator.scala:80)
Java Arrow keys a struct vector's children by field name, so two same-named children collapse into one and the import-time arity check fails.
The columnar shuffle path already excludes this shape, for exactly this reason:
case StructType(fields) =>
fields.nonEmpty && fields.forall(f => supportedSerializableDataType(f.dataType)) &&
// Java Arrow stream reader cannot work on duplicate field name
fields.map(f => f.name).distinct.length == fields.length
spark/src/main/scala/org/apache/spark/sql/comet/execution/shuffle/CometShuffleExchangeExec.scala:551-554
The native path's copy of the predicate does not:
case StructType(fields) =>
fields.nonEmpty && fields.forall(f => supportedSerializableDataType(f.dataType))
spark/src/main/scala/org/apache/spark/sql/comet/execution/shuffle/CometShuffleExchangeExec.scala:426-427
CometCreateNamedStruct also guards against duplicate names (spark/src/main/scala/org/apache/comet/serde/structs.scala:42-43, returning Unsupported), so this is the one place among the three that knows about the shape and does not exclude it. Because the expression serde declines, a plain SELECT named_struct('a', x, 'a', y) ... projection falls back to Spark and never reaches native shuffle. The gate is reached when the duplicate-named struct arrives from something other than a Comet projection.
Steps to reproduce
Reproduced on 199a910bd (main), Spark 4.1 profile, macOS aarch64.
Two paths reach it.
1. Cached relation feeding CometSparkRowToColumnar. This needs no non-default Comet configuration beyond shuffle being enabled, and reproduces under the default spark.comet.shuffle.mode=auto as well as native:
val base = spark.range(50).selectExpr("id", "named_struct('a', id, 'a', id + 1) AS st")
base.cache()
base.repartition(4, col("id")).collect()
Chosen plan:
CometExchange hashpartitioning(id#2L, 4), REPARTITION_BY_NUM, CometNativeShuffle
+- CometSparkRowToColumnar
+- InMemoryTableScan [id#2L, st#3]
+- InMemoryRelation [id#2L, st#3], StorageLevel(disk, memory, deserialized, 1 replicas)
+- *(1) Project [id#2L, named_struct(a, id#2L, a, (id#2L + 1)) AS st#3]
+- *(1) Range (0, 50, step=1, splits=5)
spark.comet.shuffle.convertFromSparkPlan.enabled defaults to true, which is what makes CometSparkRowToColumnar an eligible native-shuffle child here.
2. CometLocalTableScan (spark.comet.exec.localTableScan.enabled=true). As a test in CometNativeShuffleSuite:
test("native shuffle on struct data column with duplicate field names") {
withSQLConf(CometConf.COMET_EXEC_LOCAL_TABLE_SCAN_ENABLED.key -> "true") {
val df = spark.sql(
"SELECT id, named_struct('a', id, 'a', id + 1) AS st " +
"FROM VALUES (1), (2), (3) AS t(id)")
checkShuffleAnswer(df.repartition(2, $"id"), 1)
}
}
Not reachable through Parquet: Spark rejects the schema at write time with COLUMN_ALREADY_EXISTS.
Expected behavior
supportedSerializableDataType on the native path should reject structs with duplicate field names, the same way the columnar path does, so the shuffle falls back to Spark and the query returns correct results instead of failing the task. Spark itself permits duplicate field names in a struct, and the same query succeeds with spark.comet.shuffle.mode=jvm and with Comet disabled.
Additional context
Found while writing extra coverage around #5563 / #5564, which added the first native-shuffle tests for struct data columns. The four tests added there do not exercise this shape. I also probed the other uncovered native-shuffle paths for struct columns (spill and merge, many small batches, directRead both settings, range and round-robin partitioning, map<int, struct>, struct<null, int>, binary/decimal/timestamp/date struct fields, three levels of nesting, nested output from an aggregate, empty input) and they all behave correctly. Duplicate field names were the only failure.
Prior art on the same Java Arrow limitation: #777 and #2457, both closed. #1015 is the expression-side version.
Two notes for adjacent work:
Describe the bug
nativeShuffleFailureReasons'ssupportedSerializableDataTypeaccepts aStructTypewhose fields have duplicate names, so native shuffle is chosen for the plan. The batch is written and decoded natively without complaint, but the JVM cannot import the decoded struct back across the Arrow C data interface, and the task dies:Java Arrow keys a struct vector's children by field name, so two same-named children collapse into one and the import-time arity check fails.
The columnar shuffle path already excludes this shape, for exactly this reason:
spark/src/main/scala/org/apache/spark/sql/comet/execution/shuffle/CometShuffleExchangeExec.scala:551-554The native path's copy of the predicate does not:
spark/src/main/scala/org/apache/spark/sql/comet/execution/shuffle/CometShuffleExchangeExec.scala:426-427CometCreateNamedStructalso guards against duplicate names (spark/src/main/scala/org/apache/comet/serde/structs.scala:42-43, returningUnsupported), so this is the one place among the three that knows about the shape and does not exclude it. Because the expression serde declines, a plainSELECT named_struct('a', x, 'a', y) ...projection falls back to Spark and never reaches native shuffle. The gate is reached when the duplicate-named struct arrives from something other than a Comet projection.Steps to reproduce
Reproduced on
199a910bd(main), Spark 4.1 profile, macOS aarch64.Two paths reach it.
1. Cached relation feeding
CometSparkRowToColumnar. This needs no non-default Comet configuration beyond shuffle being enabled, and reproduces under the defaultspark.comet.shuffle.mode=autoas well asnative:Chosen plan:
spark.comet.shuffle.convertFromSparkPlan.enableddefaults totrue, which is what makesCometSparkRowToColumnaran eligible native-shuffle child here.2.
CometLocalTableScan(spark.comet.exec.localTableScan.enabled=true). As a test inCometNativeShuffleSuite:Not reachable through Parquet: Spark rejects the schema at write time with
COLUMN_ALREADY_EXISTS.Expected behavior
supportedSerializableDataTypeon the native path should reject structs with duplicate field names, the same way the columnar path does, so the shuffle falls back to Spark and the query returns correct results instead of failing the task. Spark itself permits duplicate field names in a struct, and the same query succeeds withspark.comet.shuffle.mode=jvmand with Comet disabled.Additional context
Found while writing extra coverage around #5563 / #5564, which added the first native-shuffle tests for struct data columns. The four tests added there do not exercise this shape. I also probed the other uncovered native-shuffle paths for struct columns (spill and merge, many small batches,
directReadboth settings, range and round-robin partitioning,map<int, struct>,struct<null, int>, binary/decimal/timestamp/date struct fields, three levels of nesting, nested output from an aggregate, empty input) and they all behave correctly. Duplicate field names were the only failure.Prior art on the same Java Arrow limitation: #777 and #2457, both closed. #1015 is the expression-side version.
Two notes for adjacent work:
named_structwith duplicate field names falls back to Spark #5586 proposes routingnamed_structwith duplicate field names through codegen dispatch instead of falling back, and says "duplicate names are not a problem on the Arrow side ... Worth confirming with a test that the resulting Arrow schema round-trips". This issue is evidence that they are a problem once the value crosses the JVM Arrow FFI import boundary, so that round-trip test matters, and enablingnamed_structwith duplicate field names falls back to Spark #5586 would widen the surface that reaches this gate.