This is an automated email from the ASF dual-hosted git repository.

dianfu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git


The following commit(s) were added to refs/heads/master by this push:
     new 2e0056a483f [FLINK-40658][table] Preserve null ROW elements in UNNEST 
(#29398)
2e0056a483f is described below

commit 2e0056a483f16fb4df91640271458b0063a978bb
Author: Beetle <[email protected]>
AuthorDate: Mon Oct 12 14:34:26 2026 +0800

    [FLINK-40658][table] Preserve null ROW elements in UNNEST (#29398)
---
 .../planner/runtime/batch/sql/UnnestITCase.scala   | 79 ++++++++++++++++++++++
 .../planner/runtime/stream/sql/UnnestITCase.scala  | 58 ++++++++++++++++
 .../functions/table/UnnestRowsFunction.java        | 12 +++-
 .../functions/table/UnnestRowsFunctionBase.java    |  9 +++
 .../table/UnnestRowsWithOrdinalityFunction.java    | 14 ++--
 5 files changed, 164 insertions(+), 8 deletions(-)

diff --git 
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/runtime/batch/sql/UnnestITCase.scala
 
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/runtime/batch/sql/UnnestITCase.scala
index 09587cc5f61..c1590d2402f 100644
--- 
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/runtime/batch/sql/UnnestITCase.scala
+++ 
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/runtime/batch/sql/UnnestITCase.scala
@@ -19,6 +19,7 @@ package org.apache.flink.table.planner.runtime.batch.sql
 
 import org.apache.flink.api.java.typeutils.{ObjectArrayTypeInfo, RowTypeInfo}
 import org.apache.flink.table.legacy.api.Types
+import org.apache.flink.table.planner.factories.TestValuesTableFactory
 import org.apache.flink.table.planner.runtime.utils.{BatchTestBase, TestData}
 import org.apache.flink.table.planner.runtime.utils.BatchTestBase.row
 import org.apache.flink.table.utils.DateTimeUtils.toLocalDateTime
@@ -315,6 +316,68 @@ class UnnestITCase extends BatchTestBase {
       Seq(row('a', 1), row('a', 2), row('a', 3)))
   }
 
+  @Test
+  def testUnnestArrayWithNullRow(): Unit = {
+    checkResult(
+      """
+        |SELECT *
+        |FROM UNNEST(ARRAY[
+        |  CAST(NULL AS ROW<n INT, s STRING>),
+        |  CAST(NULL AS ROW<n INT, s STRING>),
+        |  CAST(ROW(1, 'a') AS ROW<n INT, s STRING>),
+        |  CAST(ROW(CAST(NULL AS INT), CAST(NULL AS STRING)) AS ROW<n INT, s 
STRING>)])
+        |""".stripMargin,
+      Seq(row(null, null), row(null, null), row(1, "a"), row(null, null))
+    )
+  }
+
+  @Test
+  def testLeftUnnestArrayWithNullRow(): Unit = {
+    checkResult(
+      """
+        |SELECT id, n, s
+        |FROM (VALUES (1, ARRAY[
+        |  CAST(NULL AS ROW<n INT, s STRING>),
+        |  CAST(NULL AS ROW<n INT, s STRING>),
+        |  CAST(ROW(1, 'a') AS ROW<n INT, s STRING>)])) AS T(id, items)
+        |LEFT JOIN UNNEST(T.items) AS U(n, s) ON TRUE
+        |""".stripMargin,
+      Seq(row(1, null, null), row(1, null, null), row(1, 1, "a"))
+    )
+  }
+
+  @Test
+  def testUnnestMultisetWithNullRow(): Unit = {
+    registerNullRowMultiset()
+
+    checkResult(
+      "SELECT n, s FROM M, UNNEST(M.items) AS U(n, s)",
+      Seq(row(null, null), row(null, null)))
+  }
+
+  @Test
+  def testUnnestMultisetWithNullRowAndOrdinality(): Unit = {
+    registerNullRowMultiset()
+
+    checkResult(
+      "SELECT n, s, o FROM M, UNNEST(M.items) WITH ORDINALITY AS U(n, s, o)",
+      Seq(row(null, null, 1), row(null, null, 2)))
+  }
+
+  private def registerNullRowMultiset(): Unit = {
+    val items = new java.util.HashMap[Row, Integer]()
+    items.put(null, 2)
+    val dataId = TestValuesTableFactory.registerData(List(row(items)))
+    tEnv.executeSql(s"""
+                       |CREATE TABLE M (items MULTISET<ROW<n INT, s STRING>>)
+                       |WITH (
+                       |  'connector' = 'values',
+                       |  'data-id' = '$dataId',
+                       |  'bounded' = 'true'
+                       |)
+                       |""".stripMargin)
+  }
+
   @Test
   def testUnnestWithOrdinalityWithValuesStream(): Unit = {
     checkResult(
@@ -323,6 +386,22 @@ class UnnestITCase extends BatchTestBase {
     )
   }
 
+  @Test
+  def testUnnestArrayWithNullRowAndOrdinality(): Unit = {
+    checkResult(
+      """
+        |SELECT *
+        |FROM UNNEST(ARRAY[
+        |  CAST(NULL AS ROW<n INT, s STRING>),
+        |  CAST(NULL AS ROW<n INT, s STRING>),
+        |  CAST(ROW(1, 'a') AS ROW<n INT, s STRING>),
+        |  CAST(ROW(CAST(NULL AS INT), CAST(NULL AS STRING)) AS ROW<n INT, s 
STRING>)])
+        |WITH ORDINALITY
+        |""".stripMargin,
+      Seq(row(null, null, 1), row(null, null, 2), row(1, "a", 3), row(null, 
null, 4))
+    )
+  }
+
   @Test
   def testUnnestArrayWithOrdinality(): Unit = {
     val data = List(
diff --git 
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/runtime/stream/sql/UnnestITCase.scala
 
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/runtime/stream/sql/UnnestITCase.scala
index 17f0093ec39..37458e6d8cc 100644
--- 
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/runtime/stream/sql/UnnestITCase.scala
+++ 
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/runtime/stream/sql/UnnestITCase.scala
@@ -356,6 +356,44 @@ class UnnestITCase(mode: StateBackendMode) extends 
StreamingWithStateTestBase(mo
     )
   }
 
+  @TestTemplate
+  def testUnnestArrayWithNullRow(): Unit = {
+    assertUnnest(
+      testData = List(1),
+      typeInfo = createTypeInformation[Int],
+      sqlQuery = """
+                   |SELECT *
+                   |FROM UNNEST(ARRAY[
+                   |  CAST(NULL AS ROW<n INT, s STRING>),
+                   |  CAST(NULL AS ROW<n INT, s STRING>),
+                   |  CAST(ROW(1, 'a') AS ROW<n INT, s STRING>),
+                   |  CAST(ROW(CAST(NULL AS INT), CAST(NULL AS STRING)) AS 
ROW<n INT, s STRING>)])
+                   |""".stripMargin,
+      expectedResults = List("null,null", "null,null", "1,a", "null,null"),
+      isRetract = false,
+      fieldNames = 'dummy
+    )
+  }
+
+  @TestTemplate
+  def testLeftUnnestArrayWithNullRow(): Unit = {
+    assertUnnest(
+      testData = List(1),
+      typeInfo = createTypeInformation[Int],
+      sqlQuery = """
+                   |SELECT id, n, s
+                   |FROM (VALUES (1, ARRAY[
+                   |  CAST(NULL AS ROW<n INT, s STRING>),
+                   |  CAST(NULL AS ROW<n INT, s STRING>),
+                   |  CAST(ROW(1, 'a') AS ROW<n INT, s STRING>)])) AS V(id, 
items)
+                   |LEFT JOIN UNNEST(V.items) AS U(n, s) ON TRUE
+                   |""".stripMargin,
+      expectedResults = List("1,null,null", "1,null,null", "1,1,a"),
+      isRetract = false,
+      fieldNames = 'dummy
+    )
+  }
+
   @TestTemplate
   def testUnnestWithOrdinalityWithValuesStream(): Unit = {
     assertUnnest(
@@ -368,6 +406,26 @@ class UnnestITCase(mode: StateBackendMode) extends 
StreamingWithStateTestBase(mo
     )
   }
 
+  @TestTemplate
+  def testUnnestArrayWithNullRowAndOrdinality(): Unit = {
+    assertUnnest(
+      testData = List(1),
+      typeInfo = createTypeInformation[Int],
+      sqlQuery = """
+                   |SELECT *
+                   |FROM UNNEST(ARRAY[
+                   |  CAST(NULL AS ROW<n INT, s STRING>),
+                   |  CAST(NULL AS ROW<n INT, s STRING>),
+                   |  CAST(ROW(1, 'a') AS ROW<n INT, s STRING>),
+                   |  CAST(ROW(CAST(NULL AS INT), CAST(NULL AS STRING)) AS 
ROW<n INT, s STRING>)])
+                   |WITH ORDINALITY
+                   |""".stripMargin,
+      expectedResults = List("null,null,1", "null,null,2", "1,a,3", 
"null,null,4"),
+      isRetract = false,
+      fieldNames = 'dummy
+    )
+  }
+
   @TestTemplate
   def testUnnestArrayWithOrdinality(): Unit = {
     val data = List(
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/table/UnnestRowsFunction.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/table/UnnestRowsFunction.java
index cece2e1e34a..db1096fe59f 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/table/UnnestRowsFunction.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/table/UnnestRowsFunction.java
@@ -60,6 +60,7 @@ public class UnnestRowsFunction extends 
UnnestRowsFunctionBase {
         private static final long serialVersionUID = 1L;
 
         private final ArrayData.ElementGetter elementGetter;
+        private final int rowArity;
 
         public CollectionUnnestFunction(
                 SpecializedContext context,
@@ -67,14 +68,21 @@ public class UnnestRowsFunction extends 
UnnestRowsFunctionBase {
                 ArrayData.ElementGetter elementGetter) {
             super(context, elementType, false);
             this.elementGetter = elementGetter;
+            this.rowArity = getRowArity(elementType);
         }
 
         public void eval(ArrayData arrayData) {
-            evalArrayData(arrayData, elementGetter, (element, position) -> 
collect(element));
+            evalArrayData(
+                    arrayData,
+                    elementGetter,
+                    (element, position) -> 
collect(normalizeCollectionElement(element, rowArity)));
         }
 
         public void eval(MapData mapData) {
-            evalMultisetData(mapData, elementGetter, (element, position) -> 
collect(element));
+            evalMultisetData(
+                    mapData,
+                    elementGetter,
+                    (element, position) -> 
collect(normalizeCollectionElement(element, rowArity)));
         }
     }
 
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/table/UnnestRowsFunctionBase.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/table/UnnestRowsFunctionBase.java
index ea340adf2e7..ac9a4987131 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/table/UnnestRowsFunctionBase.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/table/UnnestRowsFunctionBase.java
@@ -21,6 +21,7 @@ package org.apache.flink.table.runtime.functions.table;
 import org.apache.flink.annotation.Internal;
 import org.apache.flink.table.api.DataTypes;
 import org.apache.flink.table.data.ArrayData;
+import org.apache.flink.table.data.GenericRowData;
 import org.apache.flink.table.data.MapData;
 import org.apache.flink.table.functions.BuiltInFunctionDefinitions;
 import org.apache.flink.table.functions.UserDefinedFunction;
@@ -156,6 +157,14 @@ public abstract class UnnestRowsFunctionBase extends 
BuiltInSpecializedFunction
             return outputDataType;
         }
 
+        protected static int getRowArity(LogicalType elementType) {
+            return elementType instanceof RowType ? ((RowType) 
elementType).getFieldCount() : -1;
+        }
+
+        protected static Object normalizeCollectionElement(Object element, int 
rowArity) {
+            return element == null && rowArity >= 0 ? new 
GenericRowData(rowArity) : element;
+        }
+
         protected void evalArrayData(
                 ArrayData arrayData,
                 ArrayData.ElementGetter elementGetter,
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/table/UnnestRowsWithOrdinalityFunction.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/table/UnnestRowsWithOrdinalityFunction.java
index 6bd738a09e7..87ec87a75a7 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/table/UnnestRowsWithOrdinalityFunction.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/table/UnnestRowsWithOrdinalityFunction.java
@@ -45,8 +45,7 @@ public class UnnestRowsWithOrdinalityFunction extends 
UnnestRowsFunctionBase {
             SpecializedContext context,
             LogicalType elementType,
             ArrayData.ElementGetter elementGetter) {
-        return new CollectionUnnestWithOrdinalityFunction(
-                context, wrapWithOrdinality(elementType), elementGetter);
+        return new CollectionUnnestWithOrdinalityFunction(context, 
elementType, elementGetter);
     }
 
     @Override
@@ -67,13 +66,15 @@ public class UnnestRowsWithOrdinalityFunction extends 
UnnestRowsFunctionBase {
         private static final long serialVersionUID = 1L;
 
         private final ArrayData.ElementGetter elementGetter;
+        private final int rowArity;
 
         public CollectionUnnestWithOrdinalityFunction(
                 SpecializedContext context,
                 LogicalType elementType,
                 ArrayData.ElementGetter elementGetter) {
-            super(context, elementType, true);
+            super(context, wrapWithOrdinality(elementType), true);
             this.elementGetter = elementGetter;
+            this.rowArity = getRowArity(elementType);
         }
 
         public void eval(ArrayData arrayData) {
@@ -85,11 +86,12 @@ public class UnnestRowsWithOrdinalityFunction extends 
UnnestRowsFunctionBase {
         }
 
         private void collectWithOrdinality(Object element, int position) {
-            if (element instanceof RowData) {
-                RowData row = (RowData) element;
+            final Object normalizedElement = 
normalizeCollectionElement(element, rowArity);
+            if (normalizedElement instanceof RowData) {
+                RowData row = (RowData) normalizedElement;
                 collect(new JoinedRowData(row.getRowKind(), row, 
GenericRowData.of(position)));
             } else {
-                collect(GenericRowData.of(element, position));
+                collect(GenericRowData.of(normalizedElement, position));
             }
         }
     }

Reply via email to