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

lincoln-lil 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 e7084a6d634 [FLINK-39966][table-planner] 
FlinkRelMdModifiedMonotonicity wrongly reports a non-time-attribute Top-1 Rank 
as insert-only
e7084a6d634 is described below

commit e7084a6d634a33ec48e74a4bd1bc3c5f43759a03
Author: lincoln lee <[email protected]>
AuthorDate: Tue Jun 23 10:47:13 2026 +0800

    [FLINK-39966][table-planner] FlinkRelMdModifiedMonotonicity wrongly reports 
a non-time-attribute Top-1 Rank as insert-only
    
    FLINK-34702 removed the dedicated StreamPhysicalDeduplicate metadata 
handler and
    re-routed deduplication monotonicity through StreamPhysicalRank, but the new
    dispatch guard only checked RankUtil.isDeduplication (Top-1 ROW_NUMBER 
without
    rank number). It dropped the sortOnTimeAttributeOnly invariant the old node 
type
    implicitly carried.
    
    Tighten the guard to isDeduplication && sortOnTimeAttributeOnly restoring 
the
    original logic.
    
    This closes #28505.
---
 .../metadata/FlinkRelMdModifiedMonotonicity.scala  |  17 +++-
 .../flink/table/planner/plan/utils/RankUtil.scala  |   4 +-
 .../table/planner/plan/stream/sql/RankTest.xml     | 105 +++++++++++++++++++++
 .../plan/metadata/FlinkRelMdHandlerTestBase.scala  |  72 ++++++++++++++
 .../FlinkRelMdModifiedMonotonicityTest.scala       |  18 ++++
 .../table/planner/plan/stream/sql/RankTest.scala   | 103 +++++++++++++++++++-
 6 files changed, 313 insertions(+), 6 deletions(-)

diff --git 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdModifiedMonotonicity.scala
 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdModifiedMonotonicity.scala
index 0f8b5e83ad8..8b638bef45c 100644
--- 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdModifiedMonotonicity.scala
+++ 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdModifiedMonotonicity.scala
@@ -188,7 +188,19 @@ class FlinkRelMdModifiedMonotonicity private extends 
MetadataHandler[ModifiedMon
 
   def getRelModifiedMonotonicity(rel: Rank, mq: RelMetadataQuery): 
RelModifiedMonotonicity = {
     rel match {
-      case physicalRank: StreamPhysicalRank if RankUtil.isDeduplication(rel) =>
+      // Only ranks that can be converted to a Deduplicate operator are 
handled here, mirroring the
+      // pre-FLINK-34702 behavior when a dedicated StreamPhysicalDeduplicate 
node carried these
+      // invariants. This is the same condition as 
RankUtil.canConvertToDeduplicate(FlinkLogicalRank):
+      // a Top-1 ROW_NUMBER without rank number output, sorted on a single 
time attribute. We inline
+      // it rather than calling canConvertToDeduplicate(StreamPhysicalRank) 
because the latter reads
+      // the ModifyKindSetTrait, which is still undefined at the time this 
metadata is computed.
+      // Any other rank (multi-column or non-time-attribute ORDER BY, i.e. a 
real Top-1 Rank that
+      // retracts and re-emits the kept row) falls through to the generic 
logic below.
+      case physicalRank: StreamPhysicalRank
+          if RankUtil.isDeduplication(rel) &&
+            RankUtil.sortOnTimeAttributeOnly(
+              physicalRank.orderKey,
+              physicalRank.getInput.getRowType) =>
         getPhysicalRankModifiedMonotonicity(physicalRank, mq)
 
       case _ =>
@@ -249,8 +261,9 @@ class FlinkRelMdModifiedMonotonicity private extends 
MetadataHandler[ModifiedMon
   private def getPhysicalRankModifiedMonotonicity(
       rank: StreamPhysicalRank,
       mq: RelMetadataQuery): RelModifiedMonotonicity = {
-    // Can't use RankUtil.canConvertToDeduplicate directly because 
modifyKindSetTrait is undefined.
     if (allAppend(mq, rank.getInput)) {
+      // A LastRow (ORDER BY time DESC) or rowtime deduplication retracts and 
re-emits the kept row
+      // when a new winner arrives, hence generates updates; only FirstRow on 
proctime is append-only.
       if (RankUtil.keepLastDeduplicateRow(rank.orderKey) || 
rank.sortOnRowTime) {
         val mono = new RelModifiedMonotonicity(
           Array.fill(rank.getRowType.getFieldCount)(NOT_MONOTONIC))
diff --git 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/utils/RankUtil.scala
 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/utils/RankUtil.scala
index 2314eb5a88f..adac0e1d188 100644
--- 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/utils/RankUtil.scala
+++ 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/utils/RankUtil.scala
@@ -357,9 +357,7 @@ object RankUtil {
     !rank.outputRankNumber && isLimit1 && isSortOnTimeAttribute && 
isRowNumberType
   }
 
-  private def sortOnTimeAttributeOnly(
-      sortCollation: RelCollation,
-      inputRowType: RelDataType): Boolean = {
+  def sortOnTimeAttributeOnly(sortCollation: RelCollation, inputRowType: 
RelDataType): Boolean = {
     if (sortCollation.getFieldCollations.size() != 1) {
       return false
     }
diff --git 
a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/RankTest.xml
 
b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/RankTest.xml
index ef7fa02162e..f27964e04e9 100644
--- 
a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/RankTest.xml
+++ 
b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/stream/sql/RankTest.xml
@@ -115,6 +115,111 @@ Sink(table=[default_catalog.default_database.sink], 
fields=[name, eat, cnt])
       +- GroupAggregate(groupBy=[name, eat], select=[name, eat, SUM(age) AS 
cnt])
          +- Exchange(distribution=[hash[name, eat]])
             +- TableSourceScan(table=[[default_catalog, default_database, 
test_source]], fields=[name, eat, age])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testDeduplicateOnMultipleColumnsGeneratesUpdates">
+    <Resource name="sql">
+      <![CDATA[
+SELECT b, MIN(c) AS min_c
+FROM (
+  SELECT a, b, c,
+    ROW_NUMBER() OVER (PARTITION BY a ORDER BY b DESC, c DESC) AS rn
+  FROM MyTable
+) WHERE rn = 1
+GROUP BY b
+      ]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalAggregate(group=[{0}], min_c=[MIN($1)])
++- LogicalProject(b=[$1], c=[$2])
+   +- LogicalFilter(condition=[=($3, 1)])
+      +- LogicalProject(a=[$0], b=[$1], c=[$2], rn=[ROW_NUMBER() OVER 
(PARTITION BY $0 ORDER BY $1 DESC NULLS LAST, $2 DESC NULLS LAST)])
+         +- LogicalTableScan(table=[[default_catalog, default_database, 
MyTable]])
+]]>
+    </Resource>
+    <Resource name="optimized exec plan">
+      <![CDATA[
+GlobalGroupAggregate(groupBy=[b], select=[b, MIN_RETRACT(min$0) AS min_c])
++- Exchange(distribution=[hash[b]])
+   +- LocalGroupAggregate(groupBy=[b], select=[b, MIN_RETRACT(c) AS min$0, 
COUNT_RETRACT(*) AS count1$1])
+      +- Calc(select=[b, c])
+         +- Rank(strategy=[AppendFastStrategy], rankType=[ROW_NUMBER], 
rankRange=[rankStart=1, rankEnd=1], partitionBy=[a], orderBy=[b DESC, c DESC], 
select=[a, b, c])
+            +- Exchange(distribution=[hash[a]])
+               +- Calc(select=[a, b, c])
+                  +- MiniBatchAssigner(interval=[1000ms], mode=[ProcTime])
+                     +- DataStreamScan(table=[[default_catalog, 
default_database, MyTable]], fields=[a, b, c, proctime, rowtime])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testDeduplicateOnNonTimeAttributeGeneratesUpdates">
+    <Resource name="sql">
+      <![CDATA[
+SELECT b, MIN(c) AS min_c
+FROM (
+  SELECT a, b, c,
+    ROW_NUMBER() OVER (PARTITION BY a ORDER BY b) AS rn
+  FROM MyTable
+) WHERE rn = 1
+GROUP BY b
+      ]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalAggregate(group=[{0}], min_c=[MIN($1)])
++- LogicalProject(b=[$1], c=[$2])
+   +- LogicalFilter(condition=[=($3, 1)])
+      +- LogicalProject(a=[$0], b=[$1], c=[$2], rn=[ROW_NUMBER() OVER 
(PARTITION BY $0 ORDER BY $1 NULLS FIRST)])
+         +- LogicalTableScan(table=[[default_catalog, default_database, 
MyTable]])
+]]>
+    </Resource>
+    <Resource name="optimized exec plan">
+      <![CDATA[
+GlobalGroupAggregate(groupBy=[b], select=[b, MIN_RETRACT(min$0) AS min_c])
++- Exchange(distribution=[hash[b]])
+   +- LocalGroupAggregate(groupBy=[b], select=[b, MIN_RETRACT(c) AS min$0, 
COUNT_RETRACT(*) AS count1$1])
+      +- Calc(select=[b, c])
+         +- Rank(strategy=[AppendFastStrategy], rankType=[ROW_NUMBER], 
rankRange=[rankStart=1, rankEnd=1], partitionBy=[a], orderBy=[b ASC], 
select=[a, b, c])
+            +- Exchange(distribution=[hash[a]])
+               +- Calc(select=[a, b, c])
+                  +- MiniBatchAssigner(interval=[1000ms], mode=[ProcTime])
+                     +- DataStreamScan(table=[[default_catalog, 
default_database, MyTable]], fields=[a, b, c, proctime, rowtime])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testDeduplicateOnTimeAttributeIsInsertOnly">
+    <Resource name="sql">
+      <![CDATA[
+SELECT b, MIN(c) AS min_c
+FROM (
+  SELECT a, b, c,
+    ROW_NUMBER() OVER (PARTITION BY a ORDER BY proctime) AS rn
+  FROM MyTable
+) WHERE rn = 1
+GROUP BY b
+      ]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalAggregate(group=[{0}], min_c=[MIN($1)])
++- LogicalProject(b=[$1], c=[$2])
+   +- LogicalFilter(condition=[=($3, 1)])
+      +- LogicalProject(a=[$0], b=[$1], c=[$2], rn=[ROW_NUMBER() OVER 
(PARTITION BY $0 ORDER BY $3 NULLS FIRST)])
+         +- LogicalTableScan(table=[[default_catalog, default_database, 
MyTable]])
+]]>
+    </Resource>
+    <Resource name="optimized exec plan">
+      <![CDATA[
+GlobalGroupAggregate(groupBy=[b], select=[b, MIN(min$0) AS min_c])
++- Exchange(distribution=[hash[b]])
+   +- LocalGroupAggregate(groupBy=[b], select=[b, MIN(c) AS min$0, 
COUNT_RETRACT(*) AS count1$1])
+      +- Calc(select=[b, c])
+         +- Deduplicate(keep=[FirstRow], key=[a], order=[PROCTIME], 
outputInsertOnly=[false])
+            +- Exchange(distribution=[hash[a]])
+               +- Calc(select=[a, b, c, proctime])
+                  +- MiniBatchAssigner(interval=[1000ms], mode=[ProcTime])
+                     +- DataStreamScan(table=[[default_catalog, 
default_database, MyTable]], fields=[a, b, c, proctime, rowtime])
 ]]>
     </Resource>
   </TestCase>
diff --git 
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdHandlerTestBase.scala
 
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdHandlerTestBase.scala
index 2662d3c3139..d210e06b43f 100644
--- 
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdHandlerTestBase.scala
+++ 
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdHandlerTestBase.scala
@@ -934,6 +934,78 @@ class FlinkRelMdHandlerTestBase {
     (calcOfFirstRow, calcOfLastRow)
   }
 
+  // A Top-1 ROW_NUMBER whose ORDER BY is NOT a single time attribute. It is 
logically a
+  // deduplication (RankUtil.isDeduplication), but it cannot be converted to a 
Deduplicate operator
+  // (RankUtil.canConvertToDeduplicate is false), so it stays a regular Top-1 
Rank that retracts and
+  // re-emits the kept row. These guard the dispatch boundary in 
FlinkRelMdModifiedMonotonicity that
+  // FLINK-34702 introduced.
+  //
+  // equivalent SQL is
+  // select a, b, c from (
+  //  select a, b, c, ...,
+  //  ROW_NUMBER() over (partition by b order by a) rn from TemporalTable3
+  // ) t where rn <= 1
+  protected lazy val streamTop1RankOnNonTimeAttribute: RelNode = {
+    buildTop1RankNotConvertibleToDeduplicate(RelCollations.of(0))
+  }
+
+  // equivalent SQL is
+  // select a, b, c from (
+  //  select a, b, c, ...,
+  //  ROW_NUMBER() over (partition by b order by a desc, c desc) rn from 
TemporalTable3
+  // ) t where rn <= 1
+  protected lazy val streamTop1RankOnMultipleColumns: RelNode = {
+    buildTop1RankNotConvertibleToDeduplicate(
+      RelCollations.of(
+        new RelFieldCollation(
+          0,
+          RelFieldCollation.Direction.DESCENDING,
+          RelFieldCollation.NullDirection.LAST),
+        new RelFieldCollation(
+          2,
+          RelFieldCollation.Direction.DESCENDING,
+          RelFieldCollation.NullDirection.LAST)
+      ))
+  }
+
+  def buildTop1RankNotConvertibleToDeduplicate(orderKey: RelCollation): 
RelNode = {
+    val scan: StreamPhysicalDataStreamScan =
+      createDataStreamScan(ImmutableList.of("TemporalTable3"), 
streamPhysicalTraits)
+    val hash1 = FlinkRelDistribution.hash(Array(1), requireStrict = true)
+    val streamExchange =
+      new StreamPhysicalExchange(cluster, scan.getTraitSet.replace(hash1), 
scan, hash1)
+    val rank = new StreamPhysicalRank(
+      cluster,
+      streamPhysicalTraits,
+      streamExchange,
+      ImmutableBitSet.of(1),
+      orderKey,
+      RankType.ROW_NUMBER,
+      new ConstantRankRange(1, 1),
+      new RelDataTypeFieldImpl("rn", 7, longType),
+      outputRankNumber = false,
+      RankProcessStrategy.UNDEFINED_STRATEGY,
+      sortOnRowTime = false
+    )
+
+    val builder = typeFactory.builder()
+    rank.getRowType.getFieldList.asScala.dropRight(2).foreach(builder.add)
+    val projectProgram = RexProgram.create(
+      rank.getRowType,
+      Array(0, 1, 2).map(i => RexInputRef.of(i, 
rank.getRowType)).toList.asJava,
+      null,
+      builder.build(),
+      rexBuilder
+    )
+    new StreamPhysicalCalc(
+      cluster,
+      streamPhysicalTraits,
+      rank,
+      projectProgram,
+      projectProgram.getOutputRowType
+    )
+  }
+
   protected lazy val streamChangelogNormalize = {
     val key = Array(1, 0)
     val hash1 = FlinkRelDistribution.hash(key, requireStrict = true)
diff --git 
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdModifiedMonotonicityTest.scala
 
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdModifiedMonotonicityTest.scala
index 3cc1813bc32..4d826e62b52 100644
--- 
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdModifiedMonotonicityTest.scala
+++ 
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/metadata/FlinkRelMdModifiedMonotonicityTest.scala
@@ -487,6 +487,24 @@ class FlinkRelMdModifiedMonotonicityTest extends 
FlinkRelMdHandlerTestBase {
       mq.getRelModifiedMonotonicity(streamRowTimeDeduplicateLastRow))
   }
 
+  @Test
+  def testGetRelMonotonicityOnRankNotConvertibleToDeduplicate(): Unit = {
+    // A Top-1 ROW_NUMBER whose ORDER BY is not a single time attribute is 
logically a
+    // deduplication but cannot be converted to a Deduplicate operator (see
+    // RankUtil.canConvertToDeduplicate). It is a regular Top-1 Rank that 
retracts and re-emits the
+    // kept row when a new winner arrives, so it must NOT be reported as 
all-CONSTANT (insert-only).
+    // Instead it falls through to the generic Rank monotonicity logic, which 
derives the order-by
+    // field from the input monotonicity and the sort direction (CONSTANT 
input + ASC => DECREASING,
+    // CONSTANT input + DESC => INCREASING). Guards against the FLINK-34702 
dispatch regression.
+    assertEquals(
+      new RelModifiedMonotonicity(Array(DECREASING, CONSTANT, NOT_MONOTONIC)),
+      mq.getRelModifiedMonotonicity(streamTop1RankOnNonTimeAttribute))
+
+    assertEquals(
+      new RelModifiedMonotonicity(Array(INCREASING, CONSTANT, NOT_MONOTONIC)),
+      mq.getRelModifiedMonotonicity(streamTop1RankOnMultipleColumns))
+  }
+
   @Test
   def testGetRelMonotonicityOnChangelogNormalize(): Unit = {
     assertEquals(
diff --git 
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/RankTest.scala
 
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/RankTest.scala
index a63fbec3ae0..14db205c4a5 100644
--- 
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/RankTest.scala
+++ 
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/RankTest.scala
@@ -18,12 +18,14 @@
 package org.apache.flink.table.planner.plan.stream.sql
 
 import org.apache.flink.table.api._
-import org.apache.flink.table.api.config.OptimizerConfigOptions
+import org.apache.flink.table.api.config.{ExecutionConfigOptions, 
OptimizerConfigOptions}
 import org.apache.flink.table.planner.utils.TableTestBase
 
 import org.assertj.core.api.Assertions.{assertThatExceptionOfType, 
assertThatThrownBy}
 import org.junit.jupiter.api.Test
 
+import java.time.Duration
+
 class RankTest extends TableTestBase {
 
   private val util = streamTestUtil()
@@ -1014,5 +1016,104 @@ class RankTest extends TableTestBase {
     util.verifyExplainInsert(sql, ExplainDetail.CHANGELOG_MODE)
   }
 
+  // A minibatch-enabled util with MyTable registered. Minibatch is required 
for these cases to
+  // exercise the FLINK-34702 fix end-to-end: a deduplication-style Top-1 Rank 
only forwards its
+  // updates downstream when minibatch is enabled (see 
RankUtil.outputInsertOnlyInDeduplicate); with
+  // minibatch disabled a FirstRow rank is forced insert-only and the 
modified-monotonicity guard is
+  // never reached. A fresh util is used (rather than mutating the shared one) 
to keep minibatch from
+  // leaking into the other test cases.
+  private def miniBatchUtil() = {
+    val mbUtil = streamTestUtil()
+    mbUtil.addDataStream[(Int, String, Long)](
+      "MyTable",
+      'a,
+      'b,
+      'c,
+      'proctime.proctime,
+      'rowtime.rowtime)
+    mbUtil.tableEnv.getConfig
+      .set(ExecutionConfigOptions.TABLE_EXEC_MINIBATCH_ENABLED, 
Boolean.box(true))
+      .set(ExecutionConfigOptions.TABLE_EXEC_MINIBATCH_ALLOW_LATENCY, 
Duration.ofSeconds(1))
+      .set(ExecutionConfigOptions.TABLE_EXEC_MINIBATCH_SIZE, Long.box(1000L))
+    mbUtil
+  }
+
+  /**
+   * A Top-1 ROW_NUMBER (i.e. RankUtil.isDeduplication) whose ORDER BY is a 
single time attribute in
+   * ascending order can be converted to a FirstRow Deduplicate, which is 
insert-only. Its modified
+   * monotonicity is CONSTANT, so the downstream MIN aggregate uses the 
non-retract Min. This is the
+   * insert-only control for the two cases below.
+   */
+  @Test
+  def testDeduplicateOnTimeAttributeIsInsertOnly(): Unit = {
+    val util = miniBatchUtil()
+    val sql =
+      """
+        |SELECT b, MIN(c) AS min_c
+        |FROM (
+        |  SELECT a, b, c,
+        |    ROW_NUMBER() OVER (PARTITION BY a ORDER BY proctime) AS rn
+        |  FROM MyTable
+        |) WHERE rn = 1
+        |GROUP BY b
+      """.stripMargin
+
+    util.verifyExecPlan(sql)
+  }
+
+  /**
+   * A Top-1 ROW_NUMBER whose ORDER BY is a single NON-time attribute is a 
deduplication
+   * (RankUtil.isDeduplication) but can NOT be converted to a Deduplicate 
operator
+   * (RankUtil.canConvertToDeduplicate is false because it is not sorted on a 
time attribute). It
+   * stays a Top-1 Rank operator that retracts and re-emits the kept row when 
a new winner arrives,
+   * hence generates updates. Its modified monotonicity must NOT be CONSTANT, 
so the downstream MIN
+   * aggregate must use Min_Retract.
+   *
+   * Before the fix, FlinkRelMdModifiedMonotonicity wrongly treated this 
ascending single-column
+   * case as an insert-only FirstRow and marked it CONSTANT, planning a 
non-retract Min.
+   */
+  @Test
+  def testDeduplicateOnNonTimeAttributeGeneratesUpdates(): Unit = {
+    val util = miniBatchUtil()
+    val sql =
+      """
+        |SELECT b, MIN(c) AS min_c
+        |FROM (
+        |  SELECT a, b, c,
+        |    ROW_NUMBER() OVER (PARTITION BY a ORDER BY b) AS rn
+        |  FROM MyTable
+        |) WHERE rn = 1
+        |GROUP BY b
+      """.stripMargin
+
+    util.verifyExecPlan(sql)
+  }
+
+  /**
+   * Same as above but with a multi-column ORDER BY. A multi-column order key 
can never be a single
+   * time attribute, so the rank stays a Top-1 Rank operator and generates 
updates; the downstream
+   * MIN aggregate must use Min_Retract.
+   *
+   * This is the shape that originally exposed the bug: 
RankUtil.keepLastDeduplicateRow returns
+   * false for a multi-column order key (size != 1), so before the fix it fell 
through to the
+   * insert-only CONSTANT branch.
+   */
+  @Test
+  def testDeduplicateOnMultipleColumnsGeneratesUpdates(): Unit = {
+    val util = miniBatchUtil()
+    val sql =
+      """
+        |SELECT b, MIN(c) AS min_c
+        |FROM (
+        |  SELECT a, b, c,
+        |    ROW_NUMBER() OVER (PARTITION BY a ORDER BY b DESC, c DESC) AS rn
+        |  FROM MyTable
+        |) WHERE rn = 1
+        |GROUP BY b
+      """.stripMargin
+
+    util.verifyExecPlan(sql)
+  }
+
   // TODO add tests about multi-sinks and udf
 }

Reply via email to