ganeshashree commented on code in PR #57630:
URL: https://github.com/apache/spark/pull/57630#discussion_r3687854202


##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/GeneratorExpressionSuite.scala:
##########
@@ -114,4 +115,106 @@ class GeneratorExpressionSuite extends SparkFunSuite with 
ExpressionEvalHelper {
       )
     )
   }
+
+  test("unnest - eval is lazy and only reads elements as rows are pulled") {
+    // Backing array that records which ordinals were read, to prove that 
pulling the first N rows
+    // touches only the first N elements rather than materializing the whole 
expansion up front.
+    val readOrdinals = scala.collection.mutable.ArrayBuffer.empty[Int]
+    val tracking = new GenericArrayData(Array[Any](10, 20, 30, 40, 50)) {
+      override def get(ordinal: Int, elementType: DataType): AnyRef = {
+        readOrdinals += ordinal
+        super.get(ordinal, elementType)
+      }
+    }
+    val result = Unnest(Seq(Literal(tracking, ArrayType(IntegerType))), 
withOrdinality = true)
+      .eval(null)
+    // The returned value is a lazy Iterator, not an eagerly materialized 
collection, and building
+    // it must not read any element.
+    assert(result.isInstanceOf[Iterator[_]])
+    // Literal's constructor validates its value by reading element 0; ignore 
reads made before the
+    // iterator is created and observe only what pulling rows drives.
+    readOrdinals.clear()
+
+    val it = result.iterator
+    assert(readOrdinals.isEmpty, "no element should be read before the 
iterator is advanced")
+    assert(it.next() === create_row(10, 1L))
+    assert(it.next() === create_row(20, 2L))
+    // Only the two consumed rows' elements were read; rows 2..4 remain 
untouched.
+    assert(readOrdinals.toSeq === Seq(0, 1))
+  }
+
+  test("unnest - single array") {
+    checkTuple(Unnest(Seq(empty_array), withOrdinality = false), Seq.empty)
+    checkTuple(
+      Unnest(Seq(int_array), withOrdinality = false),
+      Seq(create_row(1), create_row(2), create_row(3)))
+    // A null array is treated as empty and contributes no rows.
+    checkTuple(
+      Unnest(Seq(Literal.create(null, ArrayType(IntegerType))), withOrdinality 
= false),
+      Seq.empty)
+  }
+
+  test("unnest - single column naming and ordinality") {
+    // With a single array the output column keeps the default name `col`.
+    assert(Unnest(Seq(int_array), withOrdinality = false).elementSchema ===
+      new StructType().add("col", IntegerType, nullable = false))
+    // WITH ORDINALITY appends a 1-based, non-nullable bigint column.
+    assert(Unnest(Seq(int_array), withOrdinality = true).elementSchema ===
+      new StructType()
+        .add("col", IntegerType, nullable = false)
+        .add("ordinality", LongType, nullable = false))
+    checkTuple(
+      Unnest(Seq(str_array), withOrdinality = true),
+      Seq(create_row("a", 1L), create_row("b", 2L), create_row("c", 3L)))
+  }
+
+  test("unnest - multiple arrays are zipped and padded with nulls") {
+    val short_array = CreateArray(Seq(10, 20).map(Literal(_)))
+    // With several arrays the columns are named positionally and padded 
columns are nullable.
+    assert(Unnest(Seq(int_array, short_array), withOrdinality = 
false).elementSchema ===
+      new StructType()
+        .add("col0", IntegerType, nullable = true)
+        .add("col1", IntegerType, nullable = true))
+    checkTuple(
+      Unnest(Seq(int_array, short_array), withOrdinality = false),
+      Seq(create_row(1, 10), create_row(2, 20), create_row(3, null)))
+    // WITH ORDINALITY spans the full (longest) length.
+    checkTuple(
+      Unnest(Seq(int_array, short_array), withOrdinality = true),
+      Seq(create_row(1, 10, 1L), create_row(2, 20, 2L), create_row(3, null, 
3L)))
+  }
+
+  test("unnest - type checks") {
+    assert(Unnest(Seq(int_array), withOrdinality = 
false).checkInputDataTypes().isSuccess)
+
+    // No arguments is rejected.

Review Comment:
   Done.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/generators.scala:
##########
@@ -701,6 +701,126 @@ object InlineOuterGeneratorBuilder extends 
InlineGeneratorBuilderBase {
   override def isOuter: Boolean = true
 }
 
+/**
+ * Expands one or more arrays into a table, one row per element, implementing 
the ANSI SQL
+ * `UNNEST` collection derived table used in the FROM clause.
+ *
+ * When several arrays are supplied they are expanded in parallel: the number 
of output rows is
+ * the length of the longest array, and shorter arrays are padded with NULLs. 
A NULL array is
+ * treated as an empty array (contributes no elements). This matches the 
multi-array semantics of
+ * PostgreSQL and Trino.
+ *
+ * Each array contributes exactly one output column holding its element as-is; 
unlike `inline`,
+ * arrays of structs are not expanded into one column per field. When 
`withOrdinality` is set, a
+ * trailing 1-based `BIGINT` ordinality column is appended, matching `WITH 
ORDINALITY` in
+ * PostgreSQL and Trino (BigQuery's 0-based `WITH OFFSET` is intentionally not 
adopted).
+ *
+ * {{{
+ *   SELECT * FROM UNNEST(array(10, 20), array(30)) WITH ORDINALITY ->
+ *   10   30     1
+ *   20   NULL   2
+ * }}}
+ *
+ * This generator uses interpreted evaluation ([[CodegenFallback]]); 
[[GenerateExec]] therefore
+ * disables whole-stage codegen for the enclosing `Generate`. The 
per-array/per-ordinality zip with
+ * NULL padding does not map onto the existing [[CollectionGenerator]] codegen 
path (which emits a
+ * single `ArrayData`/`MapData`), and its interpreted `eval` is already lazy 
(one row built per
+ * pull). Interpreted generation is the same choice made by other 
non-`CollectionGenerator`
+ * generators such as [[ReplicateRows]]. A dedicated codegen path (analogous 
to `arrays_zip`) is
+ * possible future work if UNNEST becomes hot in whole-stage-codegen pipelines.
+ */
+case class Unnest(children: Seq[Expression], withOrdinality: Boolean)
+  extends Generator with CodegenFallback {
+
+  private lazy val arrayElementTypes: Seq[ArrayType] =
+    children.map(_.dataType.asInstanceOf[ArrayType])
+
+  override def checkInputDataTypes(): TypeCheckResult = {
+    if (children.isEmpty) {
+      throw QueryCompilationErrors.wrongNumArgsError(
+        toSQLId(prettyName), Seq("> 0"), children.length)
+    }
+    val nonArray = children.zipWithIndex.collectFirst {
+      case (e, idx) if !e.dataType.isInstanceOf[ArrayType] => (e, idx)
+    }
+    nonArray match {
+      case Some((e, idx)) =>
+        DataTypeMismatch(
+          errorSubClass = "UNEXPECTED_INPUT_TYPE",
+          messageParameters = Map(
+            "paramIndex" -> ordinalNumber(idx),
+            "requiredType" -> toSQLType(ArrayType),
+            "inputSql" -> toSQLExpr(e),
+            "inputType" -> toSQLType(e.dataType)))
+      case None =>
+        TypeCheckResult.TypeCheckSuccess
+    }
+  }
+
+  override def elementSchema: StructType = {
+    // With a single array keep the `explode`-compatible default name `col`; 
with several arrays
+    // use positional names `col0`, `col1`, .... A shorter array yields NULLs 
in the padded rows,

Review Comment:
   Done.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/generators.scala:
##########
@@ -701,6 +701,126 @@ object InlineOuterGeneratorBuilder extends 
InlineGeneratorBuilderBase {
   override def isOuter: Boolean = true
 }
 
+/**
+ * Expands one or more arrays into a table, one row per element, implementing 
the ANSI SQL
+ * `UNNEST` collection derived table used in the FROM clause.
+ *
+ * When several arrays are supplied they are expanded in parallel: the number 
of output rows is
+ * the length of the longest array, and shorter arrays are padded with NULLs. 
A NULL array is
+ * treated as an empty array (contributes no elements). This matches the 
multi-array semantics of
+ * PostgreSQL and Trino.
+ *
+ * Each array contributes exactly one output column holding its element as-is; 
unlike `inline`,
+ * arrays of structs are not expanded into one column per field. When 
`withOrdinality` is set, a
+ * trailing 1-based `BIGINT` ordinality column is appended, matching `WITH 
ORDINALITY` in
+ * PostgreSQL and Trino (BigQuery's 0-based `WITH OFFSET` is intentionally not 
adopted).
+ *
+ * {{{
+ *   SELECT * FROM UNNEST(array(10, 20), array(30)) WITH ORDINALITY ->
+ *   10   30     1
+ *   20   NULL   2
+ * }}}
+ *
+ * This generator uses interpreted evaluation ([[CodegenFallback]]); 
[[GenerateExec]] therefore

Review Comment:
   Done.



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