andygrove commented on code in PR #5603:
URL: https://github.com/apache/datafusion-comet/pull/5603#discussion_r3899504780
##########
spark/src/main/scala/org/apache/comet/vector/NativeUtil.scala:
##########
@@ -322,6 +330,80 @@ class NativeUtil extends AutoCloseable {
}
object NativeUtil {
+
+ /**
+ * Create a vector whose physical struct children remain positional when the
exported Arrow
+ * schema contains duplicate names. Arrow's default struct factory indexes
children by name and
+ * collapses such fields.
+ */
+ private[comet] def createVector(field: Field, allocator: BufferAllocator):
FieldVector = {
+ val runtimeField = fieldForAllocation(field)
+ field.getType match {
+ case _: ArrowType.List | _: ArrowType.LargeList | _:
ArrowType.FixedSizeList =>
+ val vector = new RenamedListVector(runtimeField, field, allocator)
+ vector.initializeChildrenFromFields(runtimeField.getChildren)
+ vector
+ case _: ArrowType.Map =>
+ val vector = new RenamedMapVector(runtimeField, field, allocator)
+ vector.initializeChildrenFromFields(runtimeField.getChildren)
+ vector
+ case _: ArrowType.Struct =>
+ val vector = new RenamedStructVector(runtimeField, field, allocator)
+ vector.initializeChildrenFromFields(runtimeField.getChildren)
+ vector
+ case _ => field.createVector(allocator).asInstanceOf[FieldVector]
+ }
+ }
+
+ private def fieldForAllocation(field: Field): Field = {
+ val children =
field.getChildren.asScala.map(fieldForAllocation).toIndexedSeq
+ val runtimeChildren = field.getType match {
+ case _: ArrowType.Struct if children.map(_.getName).distinct.size !=
children.size =>
+ children.zipWithIndex.map { case (child, ordinal) =>
+ new Field(s"__comet_runtime_field_$ordinal", child.getFieldType,
child.getChildren)
+ }
+ case _ => children
+ }
+ new Field(field.getName, field.getFieldType, runtimeChildren.asJava)
+ }
+
+ /**
+ * Pin `getField()` to the imported Field so FFI keeps the original child
labels. ListVector's
+ * runtime data-vector label is `"$data$"`; struct runtime names may be
private and unique.
+ */
+ private final class RenamedListVector(
+ runtimeField: Field,
+ exportField: Field,
+ allocator: BufferAllocator)
+ extends ListVector(runtimeField, allocator, null) {
+ override def getField: Field = exportField
+ }
+
+ private final class RenamedMapVector(
+ runtimeField: Field,
+ exportField: Field,
+ allocator: BufferAllocator)
+ extends MapVector(runtimeField, allocator, null) {
+ override def getField: Field = exportField
+ }
+
+ private final class RenamedStructVector(
+ runtimeField: Field,
+ exportField: Field,
+ allocator: BufferAllocator)
+ extends StructVector(
+ runtimeField.getName,
+ allocator,
+ runtimeField.getFieldType,
+ null,
+ AbstractStructVector.ConflictPolicy.CONFLICT_ERROR,
+ true) {
+ private var constructed = false
Review Comment:
What is this flag guarding? The only thing it can be is a `getField()` call
from inside the superclass constructor, when `exportField` is still null. But I
walked Arrow 18.3's `StructVector` / `NonNullableStructVector` /
`AbstractStructVector` constructor chain and none of them call `getField()`,
and `RenamedListVector` and `RenamedMapVector` just above have no equivalent
guard.
If a superclass constructor really does reach `getField()` here, a comment
saying so would stop someone deleting this later. If it does not, can it go?
Separately, Arrow 18.3 has `StructVector(Field, allocator, callBack,
conflictPolicy, allowConflictPolicyChanges)`, so you can pass `runtimeField`
directly and still get `CONFLICT_ERROR` without dropping to the `name` /
`fieldType` constructor.
##########
spark/src/test/scala/org/apache/comet/CometExpressionSuite.scala:
##########
@@ -2480,15 +2480,10 @@ class CometExpressionSuite extends CometTestBase with
AdaptiveSparkPlanHelper {
val path = new Path(dir.toURI.toString, "test.parquet")
makeParquetFileAllPrimitiveTypes(path, dictionaryEnabled =
dictionaryEnabled, 10000)
withParquetTable(path.toString, "tbl") {
+ checkSparkAnswerAndOperator("SELECT named_struct('a', _1, 'a', _2)
FROM tbl")
Review Comment:
`spark/src/test/resources/sql-tests/expressions/struct/create_named_struct.sql`
already exists, and the SQL file framework runs each query through both engines
and asserts native execution with no Scala. Would it be worth moving the
duplicate-name cases there? That gets them running under 3.4 and 3.5 as well,
which matters given this was only validated against 4.0 locally.
A few cases that would be good to cover wherever they land:
- `SELECT struct(a, a) FROM tbl`. `CreateStruct` lowers to
`CreateNamedStruct` and names each field after its child expression, so this
goes through the same serde and is currently untested.
- A duplicate-name struct inside an array or a map, for example `SELECT
array(named_struct('a', _1, 'a', _2)) FROM tbl`. That exercises the branch of
`fieldForAllocation` where a `ListVector` or `MapVector` root has to carry a
renamed subtree while the pinned root field keeps the original names. None of
the current tests reach it, and it is the case most likely to break.
- Three or more duplicates, and all-null rows.
- A case that crosses a shuffle (`ORDER BY` or `repartition`) rather than
going straight to the driver.
##########
spark/src/main/scala/org/apache/comet/vector/NativeUtil.scala:
##########
@@ -322,6 +330,80 @@ class NativeUtil extends AutoCloseable {
}
object NativeUtil {
+
+ /**
+ * Create a vector whose physical struct children remain positional when the
exported Arrow
+ * schema contains duplicate names. Arrow's default struct factory indexes
children by name and
+ * collapses such fields.
+ */
+ private[comet] def createVector(field: Field, allocator: BufferAllocator):
FieldVector = {
+ val runtimeField = fieldForAllocation(field)
+ field.getType match {
+ case _: ArrowType.List | _: ArrowType.LargeList | _:
ArrowType.FixedSizeList =>
+ val vector = new RenamedListVector(runtimeField, field, allocator)
+ vector.initializeChildrenFromFields(runtimeField.getChildren)
+ vector
+ case _: ArrowType.Map =>
+ val vector = new RenamedMapVector(runtimeField, field, allocator)
+ vector.initializeChildrenFromFields(runtimeField.getChildren)
+ vector
+ case _: ArrowType.Struct =>
+ val vector = new RenamedStructVector(runtimeField, field, allocator)
+ vector.initializeChildrenFromFields(runtimeField.getChildren)
+ vector
+ case _ => field.createVector(allocator).asInstanceOf[FieldVector]
Review Comment:
Routing all imports through `createVector` also pins `getField()` on every
list, map and struct column, not just the duplicate-name ones. Before this PR
the import path used `field.createVector(allocator)` for all of them.
That is a visible change for columns that have nothing to do with this fix.
Arrow's `ListVector.getField()` rebuilds children from the data vector, which
`addOrGetVector` names `$data$`, so imported list columns used to report a
child named `$data$` and now report whatever name native sent. That may well be
the right thing, but it is a separate change, it is untested, and the PR
description does not mention it.
Could `createVector` short-circuit to `field.createVector(allocator)` when
the field tree contains no duplicate struct names? That fixes the per-batch
cost above and keeps the blast radius on the case this PR is actually about.
##########
spark/src/test/scala/org/apache/comet/vector/NativeUtilSuite.scala:
##########
@@ -365,4 +366,47 @@ class NativeUtilSuite extends CometTestBase {
nativeUtil.close()
}
}
+
+ test("importVector preserves duplicate struct fields positionally") {
+ val allocator = new RootAllocator(Long.MaxValue)
+ val nativeUtil = new NativeUtil
+ val children = Arrays.asList(
+ new Field("a", FieldType.nullable(ArrowType.Bool.INSTANCE),
Collections.emptyList[Field]()),
+ new Field(
+ "a",
+ FieldType.nullable(new ArrowType.Int(8, true)),
+ Collections.emptyList[Field]()))
+ val field = new Field("value",
FieldType.nullable(ArrowType.Struct.INSTANCE), children)
+ val source = NativeUtil.createVector(field,
allocator).asInstanceOf[StructVector]
+ var imported: CometVector = null
+
+ try {
+ source.allocateNew()
+ val bool = source.getChildByOrdinal(0).asInstanceOf[BitVector]
+ val byte = source.getChildByOrdinal(1).asInstanceOf[TinyIntVector]
+ bool.setSafe(0, 1)
+ byte.setSafe(0, 7)
+ bool.setValueCount(1)
+ byte.setValueCount(1)
+ source.setIndexDefined(0)
+ source.setValueCount(1)
+
+ val array = ArrowArray.allocateNew(allocator)
+ val schema = ArrowSchema.allocateNew(allocator)
+ Data.exportVector(allocator, source, null, array, schema)
+ source.close()
Review Comment:
`source.close()` runs here and again at line 406.
Also, the `ArrowArray` and `ArrowSchema` allocated at 394 and 395 are not
closed if anything between here and the import throws, so `allocator.close()`
in the `finally` will raise a leak error that hides the real failure. Wrapping
them in `Using` or closing them in the `finally` would make a failure in this
test readable.
##########
spark/src/main/scala/org/apache/comet/serde/structs.scala:
##########
@@ -31,7 +31,9 @@ import
org.apache.comet.CometSparkSessionExtensions.withFallbackReason
import org.apache.comet.DataTypeSupport
import org.apache.comet.serde.QueryPlanSerde.{exprToProtoInternal,
serializeDataType}
-object CometCreateNamedStruct extends CometExpressionSerde[CreateNamedStruct] {
+object CometCreateNamedStruct
+ extends CometExpressionSerde[CreateNamedStruct]
+ with CodegenDispatchFallback {
private val duplicateNamesReason =
Review Comment:
Now that `CodegenDispatchFallback` is mixed in, `GenerateDocs` renders
`getUnsupportedReasons()` under the header "The following cases have no native
implementation and always run in the JVM using Spark's code-generated
implementation (inside the Comet pipeline)" (`GenerateDocs.scala:400`). So the
generated `struct.md` will say duplicate names run in the JVM and then bullet
that they are "not supported", which reads as a contradiction.
Could the doc-facing string be split from the `Unsupported(...)` note, the
way `ComparisonUtils.nonDefaultCollationDocReason` does it in
`predicates.scala`? Something along the lines of "Duplicate field names are
routed through the JVM codegen dispatcher (Spark's own `doGenCode`)".
##########
spark/src/main/scala/org/apache/comet/vector/NativeUtil.scala:
##########
@@ -322,6 +330,80 @@ class NativeUtil extends AutoCloseable {
}
object NativeUtil {
+
+ /**
+ * Create a vector whose physical struct children remain positional when the
exported Arrow
+ * schema contains duplicate names. Arrow's default struct factory indexes
children by name and
+ * collapses such fields.
+ */
+ private[comet] def createVector(field: Field, allocator: BufferAllocator):
FieldVector = {
+ val runtimeField = fieldForAllocation(field)
Review Comment:
`importVector` runs this for every column of every batch coming back from
native, so `createVector` is now on the hottest JVM path in Comet.
`fieldForAllocation` is computed here before the match, which means a plain
`IntVector` column pays a full recursive `Field` tree rebuild (with the
`asScala` / `toIndexedSeq` / `asJava` conversions and a fresh `Field` per node)
and then discards it in the `case _` branch below. `fieldForAllocation` also
allocates a new `Field` at every node even when nothing needed renaming.
Could `fieldForAllocation` return the original `field` by identity when no
struct in the tree has duplicate names, so this becomes a cheap walk instead of
a rebuild? Related, line 279 builds a fresh closure per column per batch, which
could be hoisted to a `val` on `NativeUtil`.
##########
docs/source/user-guide/latest/expressions.md:
##########
@@ -624,7 +624,7 @@ The type-name conversion functions (`bigint`, `binary`,
`boolean`, `date`, `deci
| Function | Status | Implementation | Notes |
| --- | --- | --- | --- |
-| `named_struct` | ✅ | Native | Duplicate field names fall back |
+| `named_struct` | ✅ | Hybrid | Duplicate field names route through the JVM
codegen dispatcher |
| `struct` | ✅ | Native | |
Review Comment:
The `struct` row should move to `Hybrid` too.
`GenerateDocs.buildFunctionNameToKind` resolves both `named_struct` and
`struct` through Spark's `FunctionRegistry` to `CometCreateNamedStruct`, and
`CreateStruct` lowers to `CreateNamedStruct` naming each field after its child
expression, so `struct(a, a)` produces duplicate names and takes exactly this
path.
--
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]