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

jackylee-ch pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git


The following commit(s) were added to refs/heads/main by this push:
     new 0376299642 [GLUTEN-13201][CORE] Fall back on RANGE frame bounds that 
cannot be offloaded (#13202)
0376299642 is described below

commit 03762996427315e5217be5976a40dd8ac3a2d731
Author: YangJie <[email protected]>
AuthorDate: Sun Oct 11 22:37:56 2026 -0400

    [GLUTEN-13201][CORE] Fall back on RANGE frame bounds that cannot be 
offloaded (#13202)
---
 .../gluten/backendsapi/bolt/BoltBackend.scala      |   1 +
 .../execution/BoltWindowExpressionSuite.scala      |  37 +++++++
 .../gluten/backendsapi/clickhouse/CHBackend.scala  |   9 ++
 .../GlutenClickHouseTPCHSaltNullParquetSuite.scala |  24 +++++
 .../gluten/backendsapi/velox/VeloxBackend.scala    |   1 +
 .../execution/VeloxWindowExpressionSuite.scala     |  37 +++++++
 .../substrait/expression/WindowFunctionNode.java   |  22 ++++-
 .../gluten/expression/WindowFunctionsBuilder.scala |  22 +++++
 .../apache/gluten/utils/PullOutProjectHelper.scala |  36 ++++---
 .../expression/WindowFunctionNodeTest.java         | 109 +++++++++++++++++++++
 10 files changed, 282 insertions(+), 16 deletions(-)

diff --git 
a/backends-bolt/src/main/scala/org/apache/gluten/backendsapi/bolt/BoltBackend.scala
 
b/backends-bolt/src/main/scala/org/apache/gluten/backendsapi/bolt/BoltBackend.scala
index 70d6ada972..e0654764ed 100644
--- 
a/backends-bolt/src/main/scala/org/apache/gluten/backendsapi/bolt/BoltBackend.scala
+++ 
b/backends-bolt/src/main/scala/org/apache/gluten/backendsapi/bolt/BoltBackend.scala
@@ -462,6 +462,7 @@ object BoltBackendSettings extends BackendSettingsApi {
                             "Only integral type & date type are" +
                               " supported for sort key when literal bound type 
is used!")
                       })
+                  WindowFunctionsBuilder.checkRangeFrameLiteralBound(e)
                 case _ =>
               }
             }
diff --git 
a/backends-bolt/src/test/scala/org/apache/gluten/execution/BoltWindowExpressionSuite.scala
 
b/backends-bolt/src/test/scala/org/apache/gluten/execution/BoltWindowExpressionSuite.scala
index f0cadada76..ced3136297 100644
--- 
a/backends-bolt/src/test/scala/org/apache/gluten/execution/BoltWindowExpressionSuite.scala
+++ 
b/backends-bolt/src/test/scala/org/apache/gluten/execution/BoltWindowExpressionSuite.scala
@@ -20,6 +20,7 @@ import org.apache.gluten.config.BoltConfig
 
 import org.apache.spark.SparkConf
 import org.apache.spark.sql.Row
+import org.apache.spark.sql.execution.window.WindowExec
 import org.apache.spark.sql.types._
 
 class BoltWindowExpressionSuite extends WholeStageTransformerSuite {
@@ -171,4 +172,40 @@ class BoltWindowExpressionSuite extends 
WholeStageTransformerSuite {
       }
     }
   }
+
+  testWithMinSparkVersion("range frame with a day interval bound on a date key 
falls back", "4.0") {
+    // Spark 4.0 also accepts a day-time interval bound on a date key.
+    runQueryAndCompare(
+      "select l_commitdate, count(*) over (order by l_commitdate " +
+        "RANGE BETWEEN INTERVAL '1' DAY PRECEDING AND CURRENT ROW) from 
lineitem",
+      noFallBack = false
+    ) {
+      checkSparkPlan[WindowExec]
+    }
+  }
+
+  test("range frame with an interval bound on a date key falls back") {
+    Seq(
+      "RANGE BETWEEN INTERVAL '1' MONTH PRECEDING AND CURRENT ROW",
+      "RANGE BETWEEN CURRENT ROW AND INTERVAL '1' YEAR FOLLOWING"
+    ).foreach {
+      frame =>
+        runQueryAndCompare(
+          s"select l_commitdate, count(*) over (order by l_commitdate $frame) 
from lineitem",
+          noFallBack = false) {
+          checkSparkPlan[WindowExec]
+        }
+    }
+    // The integral bound triggers the pre-compute rewrite for the whole 
operator; the interval
+    // bound in the same operator must still make it fall back.
+    runQueryAndCompare(
+      "select l_commitdate, " +
+        "count(*) over (order by l_commitdate RANGE BETWEEN 1 PRECEDING AND 
CURRENT ROW), " +
+        "count(*) over (order by l_commitdate " +
+        "RANGE BETWEEN INTERVAL '1' MONTH PRECEDING AND CURRENT ROW) from 
lineitem",
+      noFallBack = false
+    ) {
+      checkSparkPlan[WindowExec]
+    }
+  }
 }
diff --git 
a/backends-clickhouse/src/main/scala/org/apache/gluten/backendsapi/clickhouse/CHBackend.scala
 
b/backends-clickhouse/src/main/scala/org/apache/gluten/backendsapi/clickhouse/CHBackend.scala
index 395ed6fd17..7e14f44b0d 100644
--- 
a/backends-clickhouse/src/main/scala/org/apache/gluten/backendsapi/clickhouse/CHBackend.scala
+++ 
b/backends-clickhouse/src/main/scala/org/apache/gluten/backendsapi/clickhouse/CHBackend.scala
@@ -354,6 +354,15 @@ object CHBackendSettings extends BackendSettingsApi with 
Logging {
               allSupported = false
               break
           }
+
+          wExpression.windowSpec.frameSpecification match {
+            case swf: SpecifiedWindowFrame
+                if !Seq(swf.lower, 
swf.upper).forall(WindowFunctionsBuilder.isLongOffset) =>
+              logDebug(s"Not support window frame with a non-long offset: 
$swf")
+              allSupported = false
+              break
+            case _ =>
+          }
         })
     }
     allSupported
diff --git 
a/backends-clickhouse/src/test/scala/org/apache/gluten/execution/tpch/GlutenClickHouseTPCHSaltNullParquetSuite.scala
 
b/backends-clickhouse/src/test/scala/org/apache/gluten/execution/tpch/GlutenClickHouseTPCHSaltNullParquetSuite.scala
index 8c4e55311b..6ba47dc901 100644
--- 
a/backends-clickhouse/src/test/scala/org/apache/gluten/execution/tpch/GlutenClickHouseTPCHSaltNullParquetSuite.scala
+++ 
b/backends-clickhouse/src/test/scala/org/apache/gluten/execution/tpch/GlutenClickHouseTPCHSaltNullParquetSuite.scala
@@ -25,6 +25,7 @@ import org.apache.spark.{SparkConf, SparkEnv, SparkException}
 import org.apache.spark.sql.DataFrame
 import org.apache.spark.sql.catalyst.optimizer.{BuildLeft, ConstantFolding, 
NullPropagation}
 import org.apache.spark.sql.execution.{ColumnarToRowExec, ReusedSubqueryExec, 
SubqueryExec}
+import org.apache.spark.sql.execution.window.WindowExec
 import org.apache.spark.sql.functions.{col, rand, when}
 import org.apache.spark.sql.internal.SQLConf
 
@@ -642,6 +643,29 @@ class GlutenClickHouseTPCHSaltNullParquetSuite
     compareResultsAgainstVanillaSpark(sql, true, { _ => })
   }
 
+  test("window range with a non-integral bound falls back") {
+    // Spark casts the bound to the decimal key type, so both arrive as 
non-integral literals. Check
+    // with and without native validation, which this suite turns off by 
default.
+    Seq("true", "false").foreach {
+      nativeValidation =>
+        withSQLConf(GlutenConfig.NATIVE_VALIDATION_ENABLED.key -> 
nativeValidation) {
+          Seq("1.5 preceding", "10 preceding").foreach {
+            bound =>
+              val sql =
+                s"""
+                   |select n_nationkey, n_regionkey,
+                   |  count(*) over (partition by n_regionkey
+                   |    order by cast(n_nationkey as decimal(10, 2))
+                   |    range between $bound and current row) as cnt
+                   |from nation
+                   |order by n_regionkey, n_nationkey
+                   |""".stripMargin
+              runQueryAndCompare(sql, noFallBack = 
false)(checkSparkPlan[WindowExec])
+          }
+        }
+    }
+  }
+
   test("windows") {
     val sql =
       """
diff --git 
a/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxBackend.scala
 
b/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxBackend.scala
index 12bf14d09f..5426471f37 100644
--- 
a/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxBackend.scala
+++ 
b/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxBackend.scala
@@ -448,6 +448,7 @@ object VeloxBackendSettings extends BackendSettingsApi {
                             "Only integral type & date type are" +
                               " supported for sort key when literal bound type 
is used!")
                       })
+                  WindowFunctionsBuilder.checkRangeFrameLiteralBound(e)
                 case _ =>
               }
             }
diff --git 
a/backends-velox/src/test/scala/org/apache/gluten/execution/VeloxWindowExpressionSuite.scala
 
b/backends-velox/src/test/scala/org/apache/gluten/execution/VeloxWindowExpressionSuite.scala
index 9cd2e0f8d9..9e7cac7695 100644
--- 
a/backends-velox/src/test/scala/org/apache/gluten/execution/VeloxWindowExpressionSuite.scala
+++ 
b/backends-velox/src/test/scala/org/apache/gluten/execution/VeloxWindowExpressionSuite.scala
@@ -20,6 +20,7 @@ import org.apache.gluten.config.VeloxConfig
 
 import org.apache.spark.SparkConf
 import org.apache.spark.sql.Row
+import org.apache.spark.sql.execution.window.WindowExec
 import org.apache.spark.sql.types._
 
 class VeloxWindowExpressionSuite extends WholeStageTransformerSuite {
@@ -171,4 +172,40 @@ class VeloxWindowExpressionSuite extends 
WholeStageTransformerSuite {
       }
     }
   }
+
+  testWithMinSparkVersion("range frame with a day interval bound on a date key 
falls back", "4.0") {
+    // Spark 4.0 also accepts a day-time interval bound on a date key.
+    runQueryAndCompare(
+      "select l_commitdate, count(*) over (order by l_commitdate " +
+        "RANGE BETWEEN INTERVAL '1' DAY PRECEDING AND CURRENT ROW) from 
lineitem",
+      noFallBack = false
+    ) {
+      checkSparkPlan[WindowExec]
+    }
+  }
+
+  test("range frame with an interval bound on a date key falls back") {
+    Seq(
+      "RANGE BETWEEN INTERVAL '1' MONTH PRECEDING AND CURRENT ROW",
+      "RANGE BETWEEN CURRENT ROW AND INTERVAL '1' YEAR FOLLOWING"
+    ).foreach {
+      frame =>
+        runQueryAndCompare(
+          s"select l_commitdate, count(*) over (order by l_commitdate $frame) 
from lineitem",
+          noFallBack = false) {
+          checkSparkPlan[WindowExec]
+        }
+    }
+    // The integral bound triggers the pre-compute rewrite for the whole 
operator; the interval
+    // bound in the same operator must still make it fall back.
+    runQueryAndCompare(
+      "select l_commitdate, " +
+        "count(*) over (order by l_commitdate RANGE BETWEEN 1 PRECEDING AND 
CURRENT ROW), " +
+        "count(*) over (order by l_commitdate " +
+        "RANGE BETWEEN INTERVAL '1' MONTH PRECEDING AND CURRENT ROW) from 
lineitem",
+      noFallBack = false
+    ) {
+      checkSparkPlan[WindowExec]
+    }
+  }
 }
diff --git 
a/gluten-substrait/src/main/java/org/apache/gluten/substrait/expression/WindowFunctionNode.java
 
b/gluten-substrait/src/main/java/org/apache/gluten/substrait/expression/WindowFunctionNode.java
index d4be6c6ba2..94a194cca1 100644
--- 
a/gluten-substrait/src/main/java/org/apache/gluten/substrait/expression/WindowFunctionNode.java
+++ 
b/gluten-substrait/src/main/java/org/apache/gluten/substrait/expression/WindowFunctionNode.java
@@ -17,6 +17,7 @@
 package org.apache.gluten.substrait.expression;
 
 import org.apache.gluten.exception.GlutenException;
+import org.apache.gluten.exception.GlutenNotSupportException;
 import org.apache.gluten.expression.ExpressionConverter;
 import org.apache.gluten.substrait.SubstraitContext;
 import org.apache.gluten.substrait.type.TypeNode;
@@ -72,6 +73,23 @@ public class WindowFunctionNode implements Serializable {
     this.originalInputAttributes = originalInputAttributes;
   }
 
+  // A bound that does not parse as a long, such as the -1.50 of RANGE BETWEEN 
1.5 PRECEDING on a
+  // decimal key, throws GlutenNotSupportException so the window falls back; a
+  // NumberFormatException would fail the query instead.
+  private static Long 
parseOffset(org.apache.spark.sql.catalyst.expressions.Expression boundType) {
+    String raw = String.valueOf(boundType.eval(null));
+    try {
+      return Long.parseLong(raw);
+    } catch (NumberFormatException e) {
+      throw new GlutenNotSupportException(
+          "Window frame bound is not a long offset: "
+              + raw
+              + " ("
+              + boundType.dataType().simpleString()
+              + ")");
+    }
+  }
+
   private Expression.WindowFunction.Bound.Builder setBound(
       Expression.WindowFunction.Bound.Builder builder,
       org.apache.spark.sql.catalyst.expressions.Expression boundType) {
@@ -102,7 +120,7 @@ public class WindowFunctionNode implements Serializable {
                           .asScala()
                           .toSeq())
                   .doTransform(new SubstraitContext());
-          Long offset = Long.valueOf(boundType.eval(null).toString());
+          Long offset = parseOffset(boundType);
           if (offset < 0) {
             Expression.WindowFunction.Bound.Preceding.Builder 
refPrecedingBuilder =
                 Expression.WindowFunction.Bound.Preceding.newBuilder();
@@ -118,7 +136,7 @@ public class WindowFunctionNode implements Serializable {
           // Used when
           // 1. Velox backend and frame type is ROW
           // 2. Clickhouse backend
-          Long offset = Long.valueOf(boundType.eval(null).toString());
+          Long offset = parseOffset(boundType);
           if (offset < 0) {
             Expression.WindowFunction.Bound.Preceding.Builder 
offsetPrecedingBuilder =
                 Expression.WindowFunction.Bound.Preceding.newBuilder();
diff --git 
a/gluten-substrait/src/main/scala/org/apache/gluten/expression/WindowFunctionsBuilder.scala
 
b/gluten-substrait/src/main/scala/org/apache/gluten/expression/WindowFunctionsBuilder.scala
index 5873c29f20..0d601f5508 100644
--- 
a/gluten-substrait/src/main/scala/org/apache/gluten/expression/WindowFunctionsBuilder.scala
+++ 
b/gluten-substrait/src/main/scala/org/apache/gluten/expression/WindowFunctionsBuilder.scala
@@ -22,7 +22,10 @@ import org.apache.gluten.expression.ExpressionNames.{LAG, 
LEAD}
 import org.apache.gluten.substrait.SubstraitContext
 
 import org.apache.spark.sql.catalyst.expressions.{EmptyRow, Expression, Lag, 
Lead, WindowExpression, WindowFunction}
+import org.apache.spark.sql.catalyst.expressions.SpecialFrameBoundary
+import org.apache.spark.sql.types.{ByteType, IntegerType, LongType, ShortType}
 
+import scala.util.Try
 import scala.util.control.Breaks.{break, breakable}
 
 object WindowFunctionsBuilder {
@@ -63,4 +66,23 @@ object WindowFunctionsBuilder {
         w
     }
   }
+
+  // Whether WindowFunctionNode can convert the frame bound: a special or 
non-foldable bound, or
+  // a literal that parses as the long offset it sends to the native engine.
+  def isLongOffset(bound: Expression): Boolean = bound match {
+    case _: SpecialFrameBoundary => true
+    case b if !b.foldable => true
+    case b => 
Try(java.lang.Long.parseLong(String.valueOf(b.eval(EmptyRow)))).isSuccess
+  }
+
+  // A literal RANGE frame bound is offloaded as a pre-computed boundary 
column, which only works
+  // for an integral bound that converts to an offset; any other literal bound 
makes the window
+  // fall back.
+  def checkRangeFrameLiteralBound(bound: Expression): Unit = bound.dataType 
match {
+    case ByteType | ShortType | IntegerType | LongType if isLongOffset(bound) 
=>
+    case other =>
+      throw new GlutenNotSupportException(
+        s"Only integral literal bounds are supported for a RANGE frame, got 
${bound.sql}: " +
+          other.simpleString)
+  }
 }
diff --git 
a/gluten-substrait/src/main/scala/org/apache/gluten/utils/PullOutProjectHelper.scala
 
b/gluten-substrait/src/main/scala/org/apache/gluten/utils/PullOutProjectHelper.scala
index a4f8d2cbf4..d2c5486611 100644
--- 
a/gluten-substrait/src/main/scala/org/apache/gluten/utils/PullOutProjectHelper.scala
+++ 
b/gluten-substrait/src/main/scala/org/apache/gluten/utils/PullOutProjectHelper.scala
@@ -23,7 +23,7 @@ import org.apache.spark.sql.catalyst.expressions._
 import 
org.apache.spark.sql.catalyst.expressions.aggregate.{AggregateExpression, 
AggregateFunction, Complete, Partial}
 import org.apache.spark.sql.execution.aggregate._
 import org.apache.spark.sql.execution.window.WindowExec
-import org.apache.spark.sql.types.{ByteType, DateType, IntegerType, LongType, 
ShortType}
+import org.apache.spark.sql.types.{ByteType, DataType, DateType, IntegerType, 
LongType, ShortType}
 
 import java.sql.Date
 import java.util.concurrent.atomic.AtomicInteger
@@ -194,10 +194,8 @@ trait PullOutProjectHelper {
     w.windowExpression.exists(_.find {
       case we: WindowExpression =>
         we.windowSpec.frameSpecification match {
-          case swf: SpecifiedWindowFrame
-              if needPreComputeRangeFrame(swf) && supportPreComputeRangeFrame(
-                we.windowSpec.orderSpec) =>
-            true
+          case swf: SpecifiedWindowFrame =>
+            canPreComputeRangeFrame(swf, we.windowSpec.orderSpec)
           case _ => false
         }
       case _ => false
@@ -209,16 +207,26 @@ trait PullOutProjectHelper {
     (needPreComputeRangeFrameBoundary(swf.lower) || 
needPreComputeRangeFrameBoundary(swf.upper))
   }
 
-  protected def supportPreComputeRangeFrame(sortOrders: Seq[SortOrder]): 
Boolean = {
-    sortOrders.forall {
-      _.dataType match {
-        case ByteType | ShortType | IntegerType | LongType | DateType => true
-        // Only integral type & date type are supported for sort key with 
Range Frame
-        case _ => false
-      }
-    }
+  // The pre-computed boundary is the order key plus the bound, so the bound 
has to be of an
+  // integral type too. A date key with an interval bound, for one, stays as 
it is and falls back.
+  private def canPreComputeRangeFrame(
+      swf: SpecifiedWindowFrame,
+      sortOrders: Seq[SortOrder]): Boolean = {
+    def integralBound(bound: Expression): Boolean =
+      !needPreComputeRangeFrameBoundary(bound) || 
isIntegralType(bound.dataType)
+    needPreComputeRangeFrame(swf) && supportPreComputeRangeFrame(sortOrders) &&
+    integralBound(swf.lower) && integralBound(swf.upper)
+  }
+
+  private def isIntegralType(dataType: DataType): Boolean = dataType match {
+    case ByteType | ShortType | IntegerType | LongType => true
+    case _ => false
   }
 
+  // Only integral and date sort keys are supported for a range frame.
+  protected def supportPreComputeRangeFrame(sortOrders: Seq[SortOrder]): 
Boolean =
+    sortOrders.forall(o => isIntegralType(o.dataType) || o.dataType == 
DateType)
+
   /**
    * Convert DateType to IntType for orderSpec if needPreComputeRangeFrame, 
because spark's frame
    * type does not support DateType. It does not affect the correctness of 
sort.
@@ -265,7 +273,7 @@ trait PullOutProjectHelper {
     }
 
     val newWindowSpec = we.windowSpec.frameSpecification match {
-      case swf: SpecifiedWindowFrame if needPreComputeRangeFrame(swf) =>
+      case swf: SpecifiedWindowFrame if canPreComputeRangeFrame(swf, 
orderSpecs) =>
         val orderSpec = orderSpecs.head
         val lowerFrameCol = preComputeRangeFrameBoundary(swf.lower, orderSpec, 
expressionMap)
         val upperFrameCol = preComputeRangeFrameBoundary(swf.upper, orderSpec, 
expressionMap)
diff --git 
a/gluten-substrait/src/test/java/org/apache/gluten/substrait/expression/WindowFunctionNodeTest.java
 
b/gluten-substrait/src/test/java/org/apache/gluten/substrait/expression/WindowFunctionNodeTest.java
new file mode 100644
index 0000000000..7ff5064cdc
--- /dev/null
+++ 
b/gluten-substrait/src/test/java/org/apache/gluten/substrait/expression/WindowFunctionNodeTest.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.gluten.substrait.expression;
+
+import org.apache.gluten.exception.GlutenNotSupportException;
+import org.apache.gluten.expression.WindowFunctionsBuilder;
+import org.apache.gluten.substrait.type.TypeBuilder;
+
+import org.apache.spark.sql.catalyst.expressions.CurrentRow$;
+import org.apache.spark.sql.catalyst.expressions.Literal;
+import org.apache.spark.sql.types.DataTypes;
+import org.junit.Test;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+public class WindowFunctionNodeTest {
+
+  private WindowFunctionNode nodeWithLowerBound(Literal lowerBound) {
+    return new WindowFunctionNode(
+        0,
+        Collections.emptyList(),
+        "w",
+        TypeBuilder.makeFP64(false),
+        Literal.create(1L, DataTypes.LongType),
+        lowerBound,
+        "RANGE",
+        false,
+        Collections.emptyList());
+  }
+
+  @Test
+  public void nonIntegralBoundFailsValidation() {
+    // Spark casts a RANGE bound to the order key type, so on a decimal key 
even 10 PRECEDING
+    // arrives as -10.00.
+    Literal[] bounds = {
+      Literal.create(new BigDecimal("-1.50"), DataTypes.createDecimalType(10, 
2)),
+      Literal.create(new BigDecimal("-10.00"), DataTypes.createDecimalType(10, 
2)),
+      Literal.create(-0.5d, DataTypes.DoubleType)
+    };
+    for (Literal bound : bounds) {
+      WindowFunctionNode node = nodeWithLowerBound(bound);
+      GlutenNotSupportException e = 
assertThrows(GlutenNotSupportException.class, node::toProtobuf);
+      String expected = bound.value() + " (" + bound.dataType().simpleString() 
+ ")";
+      assertTrue(e.getMessage(), e.getMessage().endsWith(expected));
+    }
+  }
+
+  @Test
+  public void nullBoundFailsValidation() {
+    WindowFunctionNode node = nodeWithLowerBound(Literal.create(null, 
DataTypes.LongType));
+    GlutenNotSupportException e = 
assertThrows(GlutenNotSupportException.class, node::toProtobuf);
+    assertTrue(e.getMessage(), e.getMessage().endsWith("null (bigint)"));
+  }
+
+  @Test
+  public void longOffsetCheckMatchesConversion() {
+    assertFalse(
+        WindowFunctionsBuilder.isLongOffset(
+            Literal.create(new BigDecimal("-10.00"), 
DataTypes.createDecimalType(10, 2))));
+    assertFalse(WindowFunctionsBuilder.isLongOffset(Literal.create(null, 
DataTypes.LongType)));
+    assertTrue(
+        WindowFunctionsBuilder.isLongOffset(
+            Literal.create(new BigDecimal("10"), 
DataTypes.createDecimalType(10, 0))));
+    assertTrue(WindowFunctionsBuilder.isLongOffset(Literal.create(-3L, 
DataTypes.LongType)));
+    assertTrue(WindowFunctionsBuilder.isLongOffset(CurrentRow$.MODULE$));
+  }
+
+  @Test
+  public void rangeLiteralBoundCheckRejectsWhatCannotConvert() {
+    // The Velox and Bolt check accepts an integral bound only if it converts, 
so a null one falls
+    // back even with native validation off.
+    assertThrows(
+        GlutenNotSupportException.class,
+        () ->
+            WindowFunctionsBuilder.checkRangeFrameLiteralBound(
+                Literal.create(null, DataTypes.LongType)));
+    assertThrows(
+        GlutenNotSupportException.class,
+        () ->
+            WindowFunctionsBuilder.checkRangeFrameLiteralBound(
+                Literal.create(new BigDecimal("-10"), 
DataTypes.createDecimalType(10, 0))));
+    WindowFunctionsBuilder.checkRangeFrameLiteralBound(Literal.create(-3L, 
DataTypes.LongType));
+  }
+
+  @Test
+  public void integralBoundConverts() {
+    assertNotNull(nodeWithLowerBound(Literal.create(-3L, 
DataTypes.LongType)).toProtobuf());
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to