andygrove commented on code in PR #5567:
URL: https://github.com/apache/datafusion-comet/pull/5567#discussion_r3895434859
##########
spark/src/test/scala/org/apache/comet/exec/CometNativeShuffleSuite.scala:
##########
@@ -672,6 +680,194 @@ class CometNativeShuffleSuite extends CometTestBase with
AdaptiveSparkPlanHelper
}
}
+ test("native shuffle on struct hash partitioning key") {
+ Seq(10, 201).foreach { numPartitions =>
+ withParquetTable((0 until 50).map(i => (i, (i % 7, (i % 5).toString))),
"tbl") {
+ val df = sql("SELECT * FROM tbl")
+ .repartition(numPartitions, $"_2")
+ .sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, 1)
+ }
+ }
+ }
+
+ test("native shuffle on array hash partitioning key") {
+ Seq(10, 201).foreach { numPartitions =>
+ withParquetTable((0 until 50).map(i => (i, Seq(i % 7, i % 5))), "tbl") {
+ val df = sql("SELECT * FROM tbl")
+ .repartition(numPartitions, $"_2")
+ .sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, 1)
+ }
+ }
+ }
+
+ test("native shuffle on two-level nested hash partitioning key") {
+ // struct<array<int>, string> and array<struct<int, string>>: one level of
nesting inside the
+ // top-level type, covering both recursive branches of the type gate.
+ Seq(10, 201).foreach { numPartitions =>
+ withParquetTable(
+ (0 until 50).map(i => (i, (Seq(i % 7, i % 3), (i % 5).toString),
Seq((i % 4, "x")))),
+ "tbl") {
+ val df = sql("SELECT * FROM tbl")
+ .repartition(numPartitions, $"_2", $"_3")
+ .sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, 1)
+ }
+ }
+ }
+
+ test("native shuffle on deeply nested hash partitioning key") {
+ // Four levels of nesting, mixing all three recursive branches:
+ // struct< array< struct< m: map<string, array<int>>, s: string > >, i:
int >
+ // so the gate and the native hasher both have to descend struct -> array
-> struct -> map
+ // -> array -> int.
+ assume(isSpark40Plus, "map shuffle keys are only normalized with mapsort
on Spark 4.0+")
+ withTable("tbl") {
+ sql("""CREATE TABLE tbl(
+ id INT,
+ k STRUCT<
+ a: ARRAY<STRUCT<m: MAP<STRING, ARRAY<INT>>, s: STRING>>,
+ i: INT>)
+ USING parquet""")
+ sql("""INSERT INTO tbl VALUES
+ (1, named_struct('a', array(named_struct('m', map('x', array(1,
2)), 's', 'p')), 'i', 1)),
+ (2, named_struct('a', array(named_struct('m', map('y', array(3)),
's', 'q')), 'i', 2)),
+ (3, named_struct('a', array(named_struct('m', map('x', array(1,
2)), 's', 'p')), 'i', 1)),
+ (4, named_struct('a', array(), 'i', 4)),
+ (5, null)""")
+ val df = sql("SELECT * FROM tbl").repartition(10,
$"k").sortWithinPartitions($"id")
+
+ checkShuffleAnswer(df, 1)
+ }
+ }
+
+ test("native shuffle on map hash partitioning key") {
Review Comment:
Every map in the new tests has a single entry, so `mapsort` is a no-op and
the property this case is gated on never actually gets exercised. The reason we
only admit maps on 4.0+ is that two equal maps with different physical entry
order have to hash alike, and nothing here would notice if that stopped being
true.
Could you add multi-entry maps written in different key orders, something
like `map('a', 1, 'b', 2)` and `map('b', 2, 'a', 1)`, and assert the two rows
get the same `spark_partition_id()`?
##########
spark/src/test/scala/org/apache/comet/exec/CometNativeShuffleSuite.scala:
##########
@@ -672,6 +680,194 @@ class CometNativeShuffleSuite extends CometTestBase with
AdaptiveSparkPlanHelper
}
}
+ test("native shuffle on struct hash partitioning key") {
+ Seq(10, 201).foreach { numPartitions =>
+ withParquetTable((0 until 50).map(i => (i, (i % 7, (i % 5).toString))),
"tbl") {
+ val df = sql("SELECT * FROM tbl")
+ .repartition(numPartitions, $"_2")
+ .sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, 1)
+ }
+ }
+ }
+
+ test("native shuffle on array hash partitioning key") {
+ Seq(10, 201).foreach { numPartitions =>
+ withParquetTable((0 until 50).map(i => (i, Seq(i % 7, i % 5))), "tbl") {
+ val df = sql("SELECT * FROM tbl")
+ .repartition(numPartitions, $"_2")
+ .sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, 1)
+ }
+ }
+ }
+
+ test("native shuffle on two-level nested hash partitioning key") {
+ // struct<array<int>, string> and array<struct<int, string>>: one level of
nesting inside the
+ // top-level type, covering both recursive branches of the type gate.
+ Seq(10, 201).foreach { numPartitions =>
+ withParquetTable(
+ (0 until 50).map(i => (i, (Seq(i % 7, i % 3), (i % 5).toString),
Seq((i % 4, "x")))),
+ "tbl") {
+ val df = sql("SELECT * FROM tbl")
+ .repartition(numPartitions, $"_2", $"_3")
+ .sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, 1)
+ }
+ }
+ }
+
+ test("native shuffle on deeply nested hash partitioning key") {
+ // Four levels of nesting, mixing all three recursive branches:
+ // struct< array< struct< m: map<string, array<int>>, s: string > >, i:
int >
+ // so the gate and the native hasher both have to descend struct -> array
-> struct -> map
+ // -> array -> int.
+ assume(isSpark40Plus, "map shuffle keys are only normalized with mapsort
on Spark 4.0+")
+ withTable("tbl") {
+ sql("""CREATE TABLE tbl(
+ id INT,
+ k STRUCT<
+ a: ARRAY<STRUCT<m: MAP<STRING, ARRAY<INT>>, s: STRING>>,
+ i: INT>)
+ USING parquet""")
+ sql("""INSERT INTO tbl VALUES
+ (1, named_struct('a', array(named_struct('m', map('x', array(1,
2)), 's', 'p')), 'i', 1)),
+ (2, named_struct('a', array(named_struct('m', map('y', array(3)),
's', 'q')), 'i', 2)),
+ (3, named_struct('a', array(named_struct('m', map('x', array(1,
2)), 's', 'p')), 'i', 1)),
+ (4, named_struct('a', array(), 'i', 4)),
+ (5, null)""")
+ val df = sql("SELECT * FROM tbl").repartition(10,
$"k").sortWithinPartitions($"id")
+
+ checkShuffleAnswer(df, 1)
+ }
+ }
+
+ test("native shuffle on map hash partitioning key") {
+ // Map entry order carries no meaning, so equal maps must hash alike.
Spark 4.0+ normalizes a
+ // map shuffle key with `mapsort(...)`; earlier versions do not, so Comet
must not hash a raw
+ // map there. The gate therefore only admits map keys on Spark 4.0+, and
only when the
+ // `mapsort` itself is convertible (CometMapSort supports scalar map keys
only).
+ withParquetTable((0 until 50).map(i => (i, Map(i % 7 -> (i % 5)))), "tbl")
{
+ val df = sql("SELECT * FROM tbl").repartition(10,
$"_2").sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, if (isSpark40Plus) 1 else 0)
+ }
+ }
+
+ test("native shuffle on map hash partitioning key with non-scalar map key
falls back") {
+ // A map whose own key is nested cannot be `mapsort`ed by Comet (Arrow's
sort_to_indices
+ // handles scalar keys only), so the normalization Spark 4.0+ requires is
unavailable and the
+ // shuffle must fall back rather than hash an unnormalized map.
+ assume(isSpark40Plus, "map shuffle keys are only normalized with mapsort
on Spark 4.0+")
+ withParquetTable((0 until 50).map(i => (i, Map(Seq(i % 7) -> (i % 5)))),
"tbl") {
+ val df = sql("SELECT * FROM tbl").repartition(10,
$"_2").sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, 0)
+ }
+ }
+
+ test("native shuffle on struct hash partitioning key with collated string
falls back") {
+ // The top-level gate rejects collated strings because Comet hashes raw
bytes, which would
+ // misroute rows that are equal under the collation. Recursing through the
nested cases must
+ // preserve that: a collated leaf disqualifies the whole key.
+ assume(isSpark40Plus, "string collation requires Spark 4.0+")
+ withTable("tbl") {
+ sql(
+ "CREATE TABLE tbl(id INT, s STRUCT<a: STRING COLLATE UTF8_LCASE, b:
INT>) USING parquet")
+ sql("INSERT INTO tbl VALUES (1, named_struct('a', 'x', 'b', 1))")
+ sql("INSERT INTO tbl VALUES (2, named_struct('a', 'X', 'b', 2))")
+ val df = sql("SELECT * FROM tbl").repartition(10, $"s")
+
+ checkShuffleAnswer(df, 0)
+ }
+ }
+
+ test("native shuffle nested hash partitioning key honors its config") {
+ withParquetTable((0 until 50).map(i => (i, (i % 7, (i % 5).toString))),
"tbl") {
+ Seq("true" -> 1, "false" -> 0).foreach { case (enabled,
expectedShuffles) =>
+ withSQLConf(
+ CometConf.COMET_SHUFFLE_NATIVE_HASH_PARTITIONING_NESTED_ENABLED.key
-> enabled) {
+ val df = sql("SELECT * FROM tbl").repartition(10,
$"_2").sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, expectedShuffles)
+ }
+ }
+ }
+ }
+ test("native shuffle nested hash partitioning key matches Spark's partition
assignment") {
Review Comment:
This is the only test that checks routing rather than the answer, so I would
like it to be hard to fool, and right now it never asserts that Comet ran the
shuffle. If the gate ever stops admitting these keys this quietly becomes Spark
compared against Spark and still passes. Can you add a `checkCometExchange(df,
1, true)` alongside the `sparkRows.nonEmpty` check?
The other gap is nulls. None of the 200 shallow rows or the 40 deep rows
have a null key at any level, and that is the case I would most want covered.
The struct branch in `hash_funcs/utils.rs` is the one nested branch that does
not look at its own null mask. `List` and `Map` both guard on
`is_null(row_idx)`, but `DataType::Struct(_)` just takes
`struct_array.columns()` and recurses, while Spark is `case null => seed`. So
the two only agree when the children happen to be null under a null parent.
Parquet gives us that, which is why `CometHashExpressionSuite` is green, but a
struct built in the plan may not, and Spark's own rewrite for a nested map key
is `If(IsNull(k), null, named_struct(...))`, which is exactly that shape.
If a null struct ever hashes off leftover child values then two rows with
the same key can land in different partitions, and that breaks grouping and
joins whatever Spark would have done. Could you add null keys at each level
here?
##########
spark/src/test/scala/org/apache/comet/CometFuzzTestSuite.scala:
##########
@@ -171,14 +171,9 @@ class CometFuzzTestSuite extends CometFuzzTestBase {
// check for Comet shuffle
val plan =
df.queryExecution.executedPlan.asInstanceOf[AdaptiveSparkPlanExec].executedPlan
val cometShuffleExchanges = collectCometShuffleExchanges(plan)
- val expectedNumCometShuffles = CometConf.COMET_SHUFFLE_MODE.get() match {
- case "jvm" =>
- 1
- case "native" =>
- // native shuffle does not support complex types as partitioning keys
- 0
- }
- assert(cometShuffleExchanges.length == expectedNumCometShuffles)
+ // Both shuffle modes support the struct and array partitioning keys in
this file (it is
+ // generated without maps and without nested complex types), so one
Comet shuffle either way.
Review Comment:
This file does have nested complex types. `FuzzDataGenerator.generateSchema`
adds `StructType(arraysOfPrimitives)` and
`createArrayType(StructType(primitives))` when both `generateArray` and
`generateStruct` are set, so `struct<array<...>>` and `array<struct<...>>` are
both in there. Maps are the only thing missing. The comment in
`CometFuzzTestBase` says the same thing and is equally wrong, so this was
inherited, but could you fix both while you are here?
The knock-on is that this now asserts a native shuffle for
`array<decimal(36,18)>` and `struct<... decimal(36,18) ...>`, where we hash 16
LE `i128` bytes and Spark hashes the minimal BE `BigDecimal` bytes. I closed
#3079 on the grounds that we do not need to allocate the same partitions as
Spark, so I am not asking you to change the gate. But the description says this
matches the `hash` expression's coverage and it does not, since
`HashUtils.unsupportedReasonFor` rejects `decimal(precision > 18)` and
`TimeType` at any depth. Worth saying so explicitly.
Last thing, this test only asserts a shuffle count and then calls
`collect()`. Now that these keys run natively, is it worth comparing
`spark_partition_id()` against Spark here too? This is the widest nested type
coverage we have, and it is the one place a leaf type difference would show up
across all of them at once.
##########
spark/src/test/scala/org/apache/comet/exec/CometNativeShuffleSuite.scala:
##########
@@ -672,6 +680,194 @@ class CometNativeShuffleSuite extends CometTestBase with
AdaptiveSparkPlanHelper
}
}
+ test("native shuffle on struct hash partitioning key") {
+ Seq(10, 201).foreach { numPartitions =>
+ withParquetTable((0 until 50).map(i => (i, (i % 7, (i % 5).toString))),
"tbl") {
+ val df = sql("SELECT * FROM tbl")
+ .repartition(numPartitions, $"_2")
+ .sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, 1)
+ }
+ }
+ }
+
+ test("native shuffle on array hash partitioning key") {
+ Seq(10, 201).foreach { numPartitions =>
+ withParquetTable((0 until 50).map(i => (i, Seq(i % 7, i % 5))), "tbl") {
+ val df = sql("SELECT * FROM tbl")
+ .repartition(numPartitions, $"_2")
+ .sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, 1)
+ }
+ }
+ }
+
+ test("native shuffle on two-level nested hash partitioning key") {
+ // struct<array<int>, string> and array<struct<int, string>>: one level of
nesting inside the
+ // top-level type, covering both recursive branches of the type gate.
+ Seq(10, 201).foreach { numPartitions =>
+ withParquetTable(
+ (0 until 50).map(i => (i, (Seq(i % 7, i % 3), (i % 5).toString),
Seq((i % 4, "x")))),
+ "tbl") {
+ val df = sql("SELECT * FROM tbl")
+ .repartition(numPartitions, $"_2", $"_3")
+ .sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, 1)
+ }
+ }
+ }
+
+ test("native shuffle on deeply nested hash partitioning key") {
+ // Four levels of nesting, mixing all three recursive branches:
+ // struct< array< struct< m: map<string, array<int>>, s: string > >, i:
int >
+ // so the gate and the native hasher both have to descend struct -> array
-> struct -> map
+ // -> array -> int.
+ assume(isSpark40Plus, "map shuffle keys are only normalized with mapsort
on Spark 4.0+")
+ withTable("tbl") {
+ sql("""CREATE TABLE tbl(
+ id INT,
+ k STRUCT<
+ a: ARRAY<STRUCT<m: MAP<STRING, ARRAY<INT>>, s: STRING>>,
+ i: INT>)
+ USING parquet""")
+ sql("""INSERT INTO tbl VALUES
+ (1, named_struct('a', array(named_struct('m', map('x', array(1,
2)), 's', 'p')), 'i', 1)),
+ (2, named_struct('a', array(named_struct('m', map('y', array(3)),
's', 'q')), 'i', 2)),
+ (3, named_struct('a', array(named_struct('m', map('x', array(1,
2)), 's', 'p')), 'i', 1)),
+ (4, named_struct('a', array(), 'i', 4)),
+ (5, null)""")
+ val df = sql("SELECT * FROM tbl").repartition(10,
$"k").sortWithinPartitions($"id")
+
+ checkShuffleAnswer(df, 1)
+ }
+ }
+
+ test("native shuffle on map hash partitioning key") {
+ // Map entry order carries no meaning, so equal maps must hash alike.
Spark 4.0+ normalizes a
+ // map shuffle key with `mapsort(...)`; earlier versions do not, so Comet
must not hash a raw
+ // map there. The gate therefore only admits map keys on Spark 4.0+, and
only when the
+ // `mapsort` itself is convertible (CometMapSort supports scalar map keys
only).
+ withParquetTable((0 until 50).map(i => (i, Map(i % 7 -> (i % 5)))), "tbl")
{
+ val df = sql("SELECT * FROM tbl").repartition(10,
$"_2").sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, if (isSpark40Plus) 1 else 0)
+ }
+ }
+
+ test("native shuffle on map hash partitioning key with non-scalar map key
falls back") {
+ // A map whose own key is nested cannot be `mapsort`ed by Comet (Arrow's
sort_to_indices
+ // handles scalar keys only), so the normalization Spark 4.0+ requires is
unavailable and the
+ // shuffle must fall back rather than hash an unnormalized map.
+ assume(isSpark40Plus, "map shuffle keys are only normalized with mapsort
on Spark 4.0+")
+ withParquetTable((0 until 50).map(i => (i, Map(Seq(i % 7) -> (i % 5)))),
"tbl") {
+ val df = sql("SELECT * FROM tbl").repartition(10,
$"_2").sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, 0)
+ }
+ }
+
+ test("native shuffle on struct hash partitioning key with collated string
falls back") {
+ // The top-level gate rejects collated strings because Comet hashes raw
bytes, which would
+ // misroute rows that are equal under the collation. Recursing through the
nested cases must
+ // preserve that: a collated leaf disqualifies the whole key.
+ assume(isSpark40Plus, "string collation requires Spark 4.0+")
+ withTable("tbl") {
+ sql(
+ "CREATE TABLE tbl(id INT, s STRUCT<a: STRING COLLATE UTF8_LCASE, b:
INT>) USING parquet")
+ sql("INSERT INTO tbl VALUES (1, named_struct('a', 'x', 'b', 1))")
+ sql("INSERT INTO tbl VALUES (2, named_struct('a', 'X', 'b', 2))")
+ val df = sql("SELECT * FROM tbl").repartition(10, $"s")
+
+ checkShuffleAnswer(df, 0)
+ }
+ }
+
+ test("native shuffle nested hash partitioning key honors its config") {
+ withParquetTable((0 until 50).map(i => (i, (i % 7, (i % 5).toString))),
"tbl") {
+ Seq("true" -> 1, "false" -> 0).foreach { case (enabled,
expectedShuffles) =>
+ withSQLConf(
+ CometConf.COMET_SHUFFLE_NATIVE_HASH_PARTITIONING_NESTED_ENABLED.key
-> enabled) {
+ val df = sql("SELECT * FROM tbl").repartition(10,
$"_2").sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, expectedShuffles)
+ }
+ }
+ }
+ }
+ test("native shuffle nested hash partitioning key matches Spark's partition
assignment") {
+ // checkShuffleAnswer only compares the query answer, which is
order-insensitive and so would
+ // pass even if Comet routed rows to different partitions than Spark.
Nested keys are only safe
+ // if partition ASSIGNMENT matches, so compare spark_partition_id() per
row against Spark.
+ withParquetTable(
+ (0 until 200).map(i => (i, (i % 13, (i % 7).toString), Seq(i % 11, i %
5))),
+ "tbl") {
+ Seq("_2", "_3", "_2, _3").foreach { keys =>
+ val query =
+ "SELECT _1, spark_partition_id() AS pid FROM (" +
+ s"SELECT /*+ REPARTITION(10, $keys) */ * FROM tbl)"
+ val cometRows = sql(query).collect().map(r => (r.getInt(0),
r.getInt(1))).sorted
+ // `withSQLConf` returns Unit, so capture the Spark-side rows via a
var rather than
+ // relying on the block's value.
+ var sparkRows: Array[(Int, Int)] = Array.empty
+ withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
+ sparkRows = sql(query).collect().map(r => (r.getInt(0),
r.getInt(1))).sorted
+ }
+ assert(sparkRows.nonEmpty, "Spark produced no rows; the comparison
would be vacuous")
+ assert(
+ cometRows === sparkRows,
+ s"partition assignment differs from Spark for keys ($keys)")
+ }
+ }
+
+ // Same check for a deeply nested key, where Spark rewrites the
partitioning expression into a
+ // transform(...) containing a nested mapsort(...).
+ if (isSpark40Plus) {
+ withTable("deep") {
+ sql("""CREATE TABLE deep(
+ id INT,
+ k STRUCT<a: ARRAY<STRUCT<m: MAP<STRING, ARRAY<INT>>, s:
STRING>>, i: INT>)
+ USING parquet""")
+ (0 until 40).foreach { i =>
+ sql(s"""INSERT INTO deep VALUES
+ ($i, named_struct('a', array(named_struct(
+ 'm', map('k${i % 6}', array(${i % 4}, ${i % 3})), 's', 's${i
% 5}')),
+ 'i', ${i % 7}))""")
+ }
+ val query =
+ "SELECT id, spark_partition_id() AS pid FROM (" +
+ "SELECT /*+ REPARTITION(10, k) */ * FROM deep)"
+ val cometRows = sql(query).collect().map(r => (r.getInt(0),
r.getInt(1))).sorted
+ var sparkRows: Array[(Int, Int)] = Array.empty
+ withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
+ sparkRows = sql(query).collect().map(r => (r.getInt(0),
r.getInt(1))).sorted
+ }
+ assert(sparkRows.nonEmpty, "Spark produced no rows; the comparison
would be vacuous")
+ assert(cometRows === sparkRows, "partition assignment differs from
Spark for a deep key")
+ }
+ }
+ }
+ test("native shuffle on nested hash partitioning key with interval leaf
falls back") {
+ // CalendarIntervalType is allowed as a shuffle DATA column but the native
hasher has no
Review Comment:
I do not think this is testing `CalendarIntervalType`. `INTERVAL '1' MONTH`
goes through `constructMultiUnitsIntervalLiteral` and comes out as
`YearMonthIntervalType(MONTH, MONTH)`. The gate rejects both so the assertion
still holds, but #5059 and the `CalendarIntervalType` case in
`supportedSerializableDataType` are about the calendar type, and that one stays
uncovered.
Could you use `make_interval(...)`, or set
`spark.sql.legacy.interval.enabled=true`, so the test matches its comment?
##########
spark/src/main/scala/org/apache/spark/sql/comet/execution/shuffle/CometShuffleExchangeExec.scala:
##########
@@ -405,6 +414,22 @@ object CometShuffleExchangeExec
true
case dt if isTimeType(dt) =>
true
+ case StructType(fields) if nestedHashPartitioningEnabled =>
Review Comment:
Do you have any numbers for this? What I am worried about is the shapes that
miss the vectorized paths. `array<struct<...>>` and a map nested inside a
struct both fall through `hash_list_with_primitive_elements!` into
`hash_list_array!`, which slices a one element array and re-enters
`create_murmur3_hashes` for every element, plus a `.columns().to_vec()` per
struct element on top of that. The four level key in the new tests pays that at
every level.
Since the config defaults to true we are opting everyone into this, and if
it turns out slower than just letting Spark do the shuffle then the default is
wrong. `struct<int, string>` is not the interesting case here.
##########
spark/src/test/scala/org/apache/comet/exec/CometNativeShuffleSuite.scala:
##########
@@ -672,6 +680,194 @@ class CometNativeShuffleSuite extends CometTestBase with
AdaptiveSparkPlanHelper
}
}
+ test("native shuffle on struct hash partitioning key") {
+ Seq(10, 201).foreach { numPartitions =>
+ withParquetTable((0 until 50).map(i => (i, (i % 7, (i % 5).toString))),
"tbl") {
+ val df = sql("SELECT * FROM tbl")
+ .repartition(numPartitions, $"_2")
+ .sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, 1)
+ }
+ }
+ }
+
+ test("native shuffle on array hash partitioning key") {
+ Seq(10, 201).foreach { numPartitions =>
+ withParquetTable((0 until 50).map(i => (i, Seq(i % 7, i % 5))), "tbl") {
+ val df = sql("SELECT * FROM tbl")
+ .repartition(numPartitions, $"_2")
+ .sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, 1)
+ }
+ }
+ }
+
+ test("native shuffle on two-level nested hash partitioning key") {
+ // struct<array<int>, string> and array<struct<int, string>>: one level of
nesting inside the
+ // top-level type, covering both recursive branches of the type gate.
+ Seq(10, 201).foreach { numPartitions =>
+ withParquetTable(
+ (0 until 50).map(i => (i, (Seq(i % 7, i % 3), (i % 5).toString),
Seq((i % 4, "x")))),
+ "tbl") {
+ val df = sql("SELECT * FROM tbl")
+ .repartition(numPartitions, $"_2", $"_3")
+ .sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, 1)
+ }
+ }
+ }
+
+ test("native shuffle on deeply nested hash partitioning key") {
+ // Four levels of nesting, mixing all three recursive branches:
+ // struct< array< struct< m: map<string, array<int>>, s: string > >, i:
int >
+ // so the gate and the native hasher both have to descend struct -> array
-> struct -> map
+ // -> array -> int.
+ assume(isSpark40Plus, "map shuffle keys are only normalized with mapsort
on Spark 4.0+")
+ withTable("tbl") {
+ sql("""CREATE TABLE tbl(
+ id INT,
+ k STRUCT<
+ a: ARRAY<STRUCT<m: MAP<STRING, ARRAY<INT>>, s: STRING>>,
+ i: INT>)
+ USING parquet""")
+ sql("""INSERT INTO tbl VALUES
+ (1, named_struct('a', array(named_struct('m', map('x', array(1,
2)), 's', 'p')), 'i', 1)),
+ (2, named_struct('a', array(named_struct('m', map('y', array(3)),
's', 'q')), 'i', 2)),
+ (3, named_struct('a', array(named_struct('m', map('x', array(1,
2)), 's', 'p')), 'i', 1)),
+ (4, named_struct('a', array(), 'i', 4)),
+ (5, null)""")
+ val df = sql("SELECT * FROM tbl").repartition(10,
$"k").sortWithinPartitions($"id")
+
+ checkShuffleAnswer(df, 1)
+ }
+ }
+
+ test("native shuffle on map hash partitioning key") {
+ // Map entry order carries no meaning, so equal maps must hash alike.
Spark 4.0+ normalizes a
+ // map shuffle key with `mapsort(...)`; earlier versions do not, so Comet
must not hash a raw
+ // map there. The gate therefore only admits map keys on Spark 4.0+, and
only when the
+ // `mapsort` itself is convertible (CometMapSort supports scalar map keys
only).
+ withParquetTable((0 until 50).map(i => (i, Map(i % 7 -> (i % 5)))), "tbl")
{
+ val df = sql("SELECT * FROM tbl").repartition(10,
$"_2").sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, if (isSpark40Plus) 1 else 0)
+ }
+ }
+
+ test("native shuffle on map hash partitioning key with non-scalar map key
falls back") {
+ // A map whose own key is nested cannot be `mapsort`ed by Comet (Arrow's
sort_to_indices
+ // handles scalar keys only), so the normalization Spark 4.0+ requires is
unavailable and the
+ // shuffle must fall back rather than hash an unnormalized map.
+ assume(isSpark40Plus, "map shuffle keys are only normalized with mapsort
on Spark 4.0+")
+ withParquetTable((0 until 50).map(i => (i, Map(Seq(i % 7) -> (i % 5)))),
"tbl") {
+ val df = sql("SELECT * FROM tbl").repartition(10,
$"_2").sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, 0)
+ }
+ }
+
+ test("native shuffle on struct hash partitioning key with collated string
falls back") {
+ // The top-level gate rejects collated strings because Comet hashes raw
bytes, which would
+ // misroute rows that are equal under the collation. Recursing through the
nested cases must
+ // preserve that: a collated leaf disqualifies the whole key.
+ assume(isSpark40Plus, "string collation requires Spark 4.0+")
+ withTable("tbl") {
+ sql(
+ "CREATE TABLE tbl(id INT, s STRUCT<a: STRING COLLATE UTF8_LCASE, b:
INT>) USING parquet")
+ sql("INSERT INTO tbl VALUES (1, named_struct('a', 'x', 'b', 1))")
+ sql("INSERT INTO tbl VALUES (2, named_struct('a', 'X', 'b', 2))")
+ val df = sql("SELECT * FROM tbl").repartition(10, $"s")
+
+ checkShuffleAnswer(df, 0)
+ }
+ }
+
+ test("native shuffle nested hash partitioning key honors its config") {
+ withParquetTable((0 until 50).map(i => (i, (i % 7, (i % 5).toString))),
"tbl") {
+ Seq("true" -> 1, "false" -> 0).foreach { case (enabled,
expectedShuffles) =>
+ withSQLConf(
+ CometConf.COMET_SHUFFLE_NATIVE_HASH_PARTITIONING_NESTED_ENABLED.key
-> enabled) {
+ val df = sql("SELECT * FROM tbl").repartition(10,
$"_2").sortWithinPartitions($"_1")
+
+ checkShuffleAnswer(df, expectedShuffles)
+ }
+ }
+ }
+ }
+ test("native shuffle nested hash partitioning key matches Spark's partition
assignment") {
+ // checkShuffleAnswer only compares the query answer, which is
order-insensitive and so would
+ // pass even if Comet routed rows to different partitions than Spark.
Nested keys are only safe
+ // if partition ASSIGNMENT matches, so compare spark_partition_id() per
row against Spark.
+ withParquetTable(
+ (0 until 200).map(i => (i, (i % 13, (i % 7).toString), Seq(i % 11, i %
5))),
+ "tbl") {
+ Seq("_2", "_3", "_2, _3").foreach { keys =>
+ val query =
+ "SELECT _1, spark_partition_id() AS pid FROM (" +
+ s"SELECT /*+ REPARTITION(10, $keys) */ * FROM tbl)"
+ val cometRows = sql(query).collect().map(r => (r.getInt(0),
r.getInt(1))).sorted
+ // `withSQLConf` returns Unit, so capture the Spark-side rows via a
var rather than
+ // relying on the block's value.
Review Comment:
`withSQLConf` returns `T`, not Unit. `SQLTestUtils` declares `protected
override def withSQLConf[T](pairs: (String, String)*)(f: => T): T` and
`CometTestBase` mixes it in, so both of these can be `val sparkRows =
withSQLConf(...) { ... }`. Also missing a blank line before this test and
before the interval one below it.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]