sunchao commented on code in PR #5452:
URL: https://github.com/apache/datafusion-comet/pull/5452#discussion_r3848126069


##########
spark/src/main/scala/org/apache/comet/serde/literals.scala:
##########
@@ -215,4 +224,107 @@ object CometLiteral extends CometExpressionSerde[Literal] 
with Logging {
     }
     listLiteralBuilder
   }
+
+  /**
+   * True when a non-null Literal of this type is not encodable in the native 
`Literal` proto and
+   * `convert` should try to rebuild it from primitive-typed Literals. The 
native proto today
+   * carries scalars and nested `ListLiteral`s (arrays of arrays / arrays of 
scalars). It does not
+   * carry Map values, so any Literal whose type contains a `MapType` needs to 
be expanded before
+   * serialization.
+   *
+   * `StructType` (at any nesting depth) is deliberately excluded. Native 
`CometCreateNamedStruct`
+   * (`spark/src/main/scala/org/apache/comet/serde/structs.scala`) uses 
`values_to_arrays` in
+   * `native/spark-expr/src/struct_funcs/create_named_struct.rs`, which 
returns a 1-row
+   * `StructArray` whenever all children are scalar values. That collides with 
the row count of
+   * the surrounding batch and fails `make_array`'s length check. Field 
nullability is likewise
+   * inferred from the concrete child expressions, and `CometKnownNullable`
+   * 
(`spark/src/main/scala/org/apache/comet/serde/contraintExpressions.scala:99`) 
drops the tag
+   * on the wire, so wrapping a non-null child in `KnownNullable` does not 
carry across. Fall back
+   * to Spark for those shapes.
+   */
+  private def needsExpansion(dataType: DataType): Boolean = dataType match {
+    case _: MapType => true
+    case ArrayType(et, _) => needsExpansion(et)
+    case _ => false
+  }
+
+  /**
+   * True when the Literal is a non-null complex value that we can rebuild 
from primitive Literals
+   * via [[expandComplexLiteral]]. Empty top-level containers are excluded 
because a synthesized
+   * `Create[Array|Map]` with no children cannot recover the original element 
type. A folded
+   * `MapData` with duplicate keys is also excluded: Spark's `CreateMap.eval`
+   * (`sql/catalyst/.../complexTypeCreator.scala:250`) feeds every entry 
through
+   * `ArrayBasedMapBuilder`, which throws under the default 
`MAP_KEY_DEDUP_POLICY=EXCEPTION`
+   * (`sql/catalyst/.../util/ArrayBasedMapBuilder.scala`). Rebuilding a folded 
literal that came
+   * from `from_json` or a similar source would then throw where the original 
literal had executed
+   * cleanly.
+   */
+  private def canExpandComplexLiteral(expr: Literal): Boolean = {
+    if (expr.value == null) return false
+    expr.dataType match {
+      case at: ArrayType if needsExpansion(at) =>
+        expr.value.asInstanceOf[ArrayData].numElements() > 0
+      case MapType(kt, _, _) =>
+        val mapData = expr.value.asInstanceOf[MapData]
+        mapData.numElements() > 0 && !hasDuplicateMapKeys(mapData.keyArray(), 
kt)

Review Comment:
   [P2] Nested map values still bypass the key-semantics guard
   
   Retested `31f28b2e`: the direct floating/collated-key cases now fall back 
correctly, but maps nested inside map values still escape the guard. With 
normal folding, ANSI mode enabled, and Parquet `id` values 1, 2, 3:
   
   ```sql
   SELECT id, element_at(
     element_at(map(1, map(CAST(0 AS DOUBLE), 7)), id),
     CAST(concat('-', CAST(id - 1 AS STRING), '.0') AS DOUBLE)) AS v
   FROM t
   ```
   
   Spark returns `7, NULL, NULL`, while the native Comet projection returns 
`NULL, NULL, NULL` on both Spark 3.5.9 and 4.1.3. The outer key type is `INT`, 
so it passes the guard. `CometCreateMap` then closure-serializes the whole 
expression through the JVM dispatcher. The inner map literal never revisits 
`CometLiteral`, leaving its double keys available to native `map_extract`. A 
positive-zero lookup control passes, and the native lookup-key expression was 
checked to produce negative zero for the first row.
   
   Nested `UTF8_LCASE` map keys have the same bypass on Spark 4.1.3: an `A1 -> 
7` entry fails a dynamic `a1` lookup, while the exact-case control passes. 
Replacing only the literal serializer with the exact base version restores the 
expected results.
   
   Could the admission check inspect every contained map's key type, including 
maps inside values, or keep their lookups on a Spark-compatible path? Please 
cover a dynamic outer lookup so constant folding cannot extract the inner map 
into a separately guarded literal.



##########
spark/src/main/scala/org/apache/comet/serde/literals.scala:
##########
@@ -215,4 +225,82 @@ object CometLiteral extends CometExpressionSerde[Literal] 
with Logging {
     }
     listLiteralBuilder
   }
+
+  /**
+   * True when a non-null Literal of this type is not encodable in the native 
`Literal` proto and
+   * `convert` should try to rebuild it from primitive-typed Literals. The 
native proto today
+   * carries scalars and nested `ListLiteral`s (arrays of arrays / arrays of 
scalars). It does not
+   * carry Map or Struct values, so any Literal whose type contains a 
`MapType` or a `StructType`
+   * needs to be expanded before serialization.
+   */
+  private def needsExpansion(dataType: DataType): Boolean = dataType match {
+    case _: MapType | _: StructType => true
+    case ArrayType(et, _) => needsExpansion(et)
+    case _ => false
+  }
+
+  /**
+   * True when the Literal is a non-null complex value that we can rebuild 
from primitive Literals
+   * via [[expandComplexLiteral]]. Empty top-level containers are excluded 
because a synthesized
+   * `Create[Array|Map]` with no children cannot recover the original element 
type.
+   */
+  private def canExpandComplexLiteral(expr: Literal): Boolean = {
+    val value = expr.value
+    if (value == null || !needsExpansion(expr.dataType)) return false
+    expr.dataType match {
+      case _: ArrayType => value.asInstanceOf[ArrayData].numElements() > 0
+      case _: MapType => value.asInstanceOf[MapData].numElements() > 0
+      case _: StructType => true
+      case _ => false
+    }
+  }
+
+  /**
+   * Rebuild a folded complex Literal as an equivalent tree of `CreateArray` / 
`CreateMap` /
+   * `CreateNamedStruct` over primitive-typed Literals. Callers must gate on
+   * [[canExpandComplexLiteral]] so `dataType` is one of the three complex 
cases and top-level
+   * containers are non-empty.
+   */
+  private def expandComplexLiteral(value: Any, dataType: DataType): Expression 
=
+    dataType match {
+      case ArrayType(et, _) =>
+        val arr = value.asInstanceOf[ArrayData]
+        val elems = (0 until arr.numElements()).map { i =>
+          if (arr.isNullAt(i)) Literal(null, et)
+          else asNullable(Literal(arr.get(i, et), et))
+        }
+        CreateArray(elems, useStringTypeWhenEmpty = false)
+      case MapType(kt, vt, _) =>
+        val mapData = value.asInstanceOf[MapData]
+        val keys = mapData.keyArray()
+        val vals = mapData.valueArray()
+        val children = (0 until keys.numElements()).flatMap { i =>
+          val k = if (keys.isNullAt(i)) Literal(null, kt) else 
Literal(keys.get(i, kt), kt)
+          val v =
+            if (vals.isNullAt(i)) Literal(null, vt)
+            else asNullable(Literal(vals.get(i, vt), vt))
+          Seq(k, v)
+        }
+        CreateMap(children, useStringTypeWhenEmpty = false)

Review Comment:
   [P2] Duplicate binary keys still evade the duplicate-key check
   
   Retested `31f28b2e`: the string-key example now falls back correctly, but 
casting those keys to binary still admits the literal:
   
   ```sql
   SELECT id, CAST(
     from_json('{"a":1,"a":2}', 'MAP<STRING,INT>')
     AS MAP<BINARY,INT>) AS m
   FROM t
   ```
   
   Spark's map cast preserves both entries. The new `.distinct` check compares 
the extracted `Array[Byte]` keys by identity, whereas `ArrayBasedMapBuilder` 
compares binary keys by their contents using interpreted ordering. 
Reconstructing the admitted literal therefore still throws `DUPLICATED_MAP_KEY` 
under `EXCEPTION`. Under `LAST_WIN`, it silently drops the first entry instead 
of preserving the original map.
   
   I reproduced both policies at this head on Spark 3.5.9 and 4.1.3 with normal 
folding, three-row Parquet input, and native `CometProject` assertions. 
Replacing only the literal serializer with the exact base version preserves 
both entries under either policy.
   
   Could the duplicate check use Spark-compatible key equality and include this 
binary-key case under both deduplication policies?



-- 
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]

Reply via email to