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

KevinyhZou 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 9747480c38 feat(gluten-flink): support stateless scalar expressions in 
planner (#12666)
9747480c38 is described below

commit 9747480c389cb0d19f894936047521e5e91eab58
Author: GGboom <[email protected]>
AuthorDate: Wed Oct 14 15:14:53 2026 +0800

    feat(gluten-flink): support stateless scalar expressions in planner (#12666)
---
 .../apache/gluten/rexnode/RexNodeConverter.java    |  8 ++
 .../rexnode/functions/RexCallConverterFactory.java |  9 ++-
 .../functions/SubstringRexCallConverter.java       | 67 ++++++++++++++++
 .../runtime/stream/custom/ScalarFunctionsTest.java | 93 ++++++++++++++++++++++
 4 files changed, 176 insertions(+), 1 deletion(-)

diff --git 
a/gluten-flink/planner/src/main/java/org/apache/gluten/rexnode/RexNodeConverter.java
 
b/gluten-flink/planner/src/main/java/org/apache/gluten/rexnode/RexNodeConverter.java
index 3cc61a2593..297063cf1a 100644
--- 
a/gluten-flink/planner/src/main/java/org/apache/gluten/rexnode/RexNodeConverter.java
+++ 
b/gluten-flink/planner/src/main/java/org/apache/gluten/rexnode/RexNodeConverter.java
@@ -32,6 +32,7 @@ import io.github.zhztheplayer.velox4j.variant.DoubleValue;
 import io.github.zhztheplayer.velox4j.variant.HugeIntValue;
 import io.github.zhztheplayer.velox4j.variant.IntegerValue;
 import io.github.zhztheplayer.velox4j.variant.SmallIntValue;
+import io.github.zhztheplayer.velox4j.variant.TimestampValue;
 import io.github.zhztheplayer.velox4j.variant.TinyIntValue;
 import io.github.zhztheplayer.velox4j.variant.VarBinaryValue;
 import io.github.zhztheplayer.velox4j.variant.VarCharValue;
@@ -130,6 +131,13 @@ public class RexNodeConverter {
         }
       case SYMBOL:
         return new VarCharValue(literal.getValue().toString());
+      case TIMESTAMP:
+        // RexLiteral#getValueAs does not support java.sql.Timestamp, so take 
the
+        // epoch millis directly. It keeps the wall time as UTC, which is the 
same
+        // base as TimestampData#getMillisecond used by the row serializer.
+        long epochMillis = literal.getValueAs(Long.class);
+        return TimestampValue.create(
+            Math.floorDiv(epochMillis, 1000L), Math.floorMod(epochMillis, 
1000L) * 1_000_000L);
       default:
         throw new RuntimeException(
             "Unsupported rex node type: " + 
literal.getType().getSqlTypeName());
diff --git 
a/gluten-flink/planner/src/main/java/org/apache/gluten/rexnode/functions/RexCallConverterFactory.java
 
b/gluten-flink/planner/src/main/java/org/apache/gluten/rexnode/functions/RexCallConverterFactory.java
index cb990204e9..4254e8ff82 100644
--- 
a/gluten-flink/planner/src/main/java/org/apache/gluten/rexnode/functions/RexCallConverterFactory.java
+++ 
b/gluten-flink/planner/src/main/java/org/apache/gluten/rexnode/functions/RexCallConverterFactory.java
@@ -112,7 +112,14 @@ public class RexCallConverterFactory {
           Map.entry("EXTRACT", Arrays.asList(() -> new 
DefaultRexCallConverter("extract"))),
           Map.entry("IS TRUE", Arrays.asList(() -> new 
IsTrueRexCallConverter())),
           Map.entry("CONCAT", Arrays.asList(() -> new 
DefaultRexCallConverter("concat"))),
-          Map.entry("CONCAT_WS", Arrays.asList(() -> new 
DefaultRexCallConverter("concat_ws"))));
+          Map.entry("CONCAT_WS", Arrays.asList(() -> new 
DefaultRexCallConverter("concat_ws"))),
+          // A literal negative length is rejected by SubstringRexCallConverter
+          // (stopgap until a Flink-semantics substring in the velox flinksql
+          // function set returns NULL): Flink returns NULL while velox
+          // substring returns an empty string. Start semantics (0 counts as 1,
+          // negative counts from the end) already match Flink.
+          Map.entry("SUBSTRING", Arrays.asList(() -> new 
SubstringRexCallConverter())),
+          Map.entry("COALESCE", Arrays.asList(() -> new 
DefaultRexCallConverter("coalesce"))));
 
   public static RexCallConverter getConverter(RexCall callNode, 
RexConversionContext context) {
     String operatorName = callNode.getOperator().getName();
diff --git 
a/gluten-flink/planner/src/main/java/org/apache/gluten/rexnode/functions/SubstringRexCallConverter.java
 
b/gluten-flink/planner/src/main/java/org/apache/gluten/rexnode/functions/SubstringRexCallConverter.java
new file mode 100644
index 0000000000..d134c489bf
--- /dev/null
+++ 
b/gluten-flink/planner/src/main/java/org/apache/gluten/rexnode/functions/SubstringRexCallConverter.java
@@ -0,0 +1,67 @@
+/*
+ * 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.rexnode.functions;
+
+import org.apache.gluten.rexnode.RexConversionContext;
+import org.apache.gluten.rexnode.ValidationResult;
+
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+
+/**
+ * Converts Flink SUBSTRING to the velox substring function (1-based; the SQL 
{@code SUBSTRING(x
+ * FROM n FOR m)} form desugars to the same call).
+ *
+ * <p>Start semantics already match: Flink's runtime ({@code 
BinaryStringDataUtil#substringSQL})
+ * treats a start of 0 like 1 and counts a negative start from the string end 
— the same rules velox
+ * substring implements — so starts convert as-is. Only a start more negative 
than the string length
+ * diverges (Flink returns an empty string, velox returns a prefix from the 
head), but whether it
+ * triggers depends on row data and cannot be checked at plan time.
+ *
+ * <p>A literal negative length is rejected: Flink returns NULL while velox 
returns an empty string.
+ *
+ * <p>TODO: the rejection is a stopgap. The proper fix is a Flink-semantics 
substring function in
+ * the velox flinksql function set that returns NULL for a negative length 
(and an empty string for
+ * an over-long negative start). A non-literal length cannot be checked at 
plan time and is still
+ * converted as-is; both gaps close once SUBSTRING maps to that function and 
this rejection is
+ * lifted.
+ */
+public class SubstringRexCallConverter extends DefaultRexCallConverter {
+
+  public SubstringRexCallConverter() {
+    super("substring");
+  }
+
+  @Override
+  public ValidationResult isSuitable(RexCall callNode, RexConversionContext 
context) {
+    if (callNode.getOperands().size() > 2) {
+      RexNode length = callNode.getOperands().get(2);
+      if (length instanceof RexLiteral) {
+        Integer lengthValue = ((RexLiteral) length).getValueAs(Integer.class);
+        if (lengthValue != null && lengthValue < 0) {
+          return ValidationResult.failure(
+              String.format(
+                  "SUBSTRING with literal length %d is not supported: Flink 
returns NULL for a"
+                      + " negative length while velox substring returns an 
empty string",
+                  lengthValue));
+        }
+      }
+    }
+    return ValidationResult.success();
+  }
+}
diff --git 
a/gluten-flink/ut/src/test/java/org/apache/gluten/table/runtime/stream/custom/ScalarFunctionsTest.java
 
b/gluten-flink/ut/src/test/java/org/apache/gluten/table/runtime/stream/custom/ScalarFunctionsTest.java
index ffc2c9f99f..750d06648f 100644
--- 
a/gluten-flink/ut/src/test/java/org/apache/gluten/table/runtime/stream/custom/ScalarFunctionsTest.java
+++ 
b/gluten-flink/ut/src/test/java/org/apache/gluten/table/runtime/stream/custom/ScalarFunctionsTest.java
@@ -32,6 +32,8 @@ import java.time.format.DateTimeFormatter;
 import java.util.Arrays;
 import java.util.List;
 
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
 class ScalarFunctionsTest extends GlutenStreamingTestBase {
 
   @Override
@@ -354,4 +356,95 @@ class ScalarFunctionsTest extends GlutenStreamingTestBase {
     String query = "select a from tblIsNull where b is null";
     runAndCheck(query, Arrays.asList("+I[2]"));
   }
+
+  @Test
+  void testSubstring() {
+    List<Row> rows =
+        Arrays.asList(Row.of("hello world"), Row.of("abcdefghij"), 
Row.of((Object) null));
+    createSimpleBoundedValuesTable("tblSubstr", "s varchar NULL", rows);
+    // 3-arg and 2-arg forms; start beyond the string length yields an empty
+    // string; a zero length is legal on both sides and anchors the rejection
+    // threshold at length < 0; NULL input propagates.
+    String query =
+        "select SUBSTRING(s, 1, 5), SUBSTRING(s, 7), SUBSTRING(s, 20), 
SUBSTRING(s, 1, 0)"
+            + " from tblSubstr";
+    runAndCheck(
+        query,
+        Arrays.asList("+I[hello, world, , ]", "+I[abcde, ghij, , ]", "+I[null, 
null, null, null]"));
+  }
+
+  @Test
+  void testSubstringDynamicStart() {
+    // A non-literal start cannot be checked at plan time; with runtime values
+    // >= 1 the velox results match Flink.
+    List<Row> rows = Arrays.asList(Row.of("hello world", 7), 
Row.of("abcdefghij", 2));
+    createSimpleBoundedValuesTable("tblSubstrDyn", "s varchar, p int", rows);
+    String query = "select SUBSTRING(s, p), SUBSTRING(s, p, 3) from 
tblSubstrDyn";
+    runAndCheck(query, Arrays.asList("+I[world, wor]", "+I[bcdefghij, bcd]"));
+  }
+
+  @Test
+  void testSubstringNonPositiveStart() {
+    // Flink treats a start of 0 like 1 and counts a negative start from the
+    // string end (BinaryStringDataUtil#substringSQL) — the same rules velox
+    // substring implements — so these forms convert as-is and match the
+    // vanilla Flink results (verified on a local cluster). A start more
+    // negative than the string length diverges but depends on row data; see
+    // SubstringRexCallConverter.
+    List<Row> rows = Arrays.asList(Row.of("hello world"), 
Row.of("abcdefghij"));
+    createSimpleBoundedValuesTable("tblSubstrStart", "s varchar", rows);
+    String query =
+        "select SUBSTRING(s, 0, 2), SUBSTRING(s, -3, 5), SUBSTRING(s, -3) from 
tblSubstrStart";
+    runAndCheck(query, Arrays.asList("+I[he, rld, rld]", "+I[ab, hij, hij]"));
+  }
+
+  @Test
+  void testSubstringNegativeLengthRejected() {
+    // Flink returns NULL for a negative length while velox returns an empty
+    // string; a literal negative length fails fast instead of silently
+    // returning the velox result. Stopgap until a Flink-semantics substring
+    // in the velox flinksql function set returns NULL.
+    List<Row> rows = Arrays.asList(Row.of("hello world"));
+    createSimpleBoundedValuesTable("tblSubstrLen", "s varchar", rows);
+    tEnv().executeSql("CREATE TABLE printT (s VARCHAR) WITH ('connector' = 
'print')");
+    assertThatThrownBy(
+            () ->
+                tEnv()
+                    .executeSql("insert into printT select SUBSTRING(s, 1, -3) 
from tblSubstrLen"))
+        .hasStackTraceContaining("SUBSTRING with literal length -3 is not 
supported");
+    tEnv().executeSql("drop table if exists printT");
+  }
+
+  @Test
+  void testCoalesce() {
+    List<Row> rows = Arrays.asList(Row.of("a", "b"), Row.of(null, "b"), 
Row.of("a", null));
+    createSimpleBoundedValuesTable("tblCoalesce", "s1 varchar, s2 varchar", 
rows);
+    String query = "select COALESCE(s1, s2) from tblCoalesce";
+    runAndCheck(query, Arrays.asList("+I[a]", "+I[b]", "+I[a]"));
+
+    // Mixed numeric types (common type BIGINT) and an all-NULL row.
+    List<Row> numRows = Arrays.asList(Row.of(1, 10L), Row.of(null, 20L), 
Row.of(null, null));
+    createSimpleBoundedValuesTable("tblCoalesceNum", "a int, b bigint", 
numRows);
+    String numQuery = "select COALESCE(a, b) from tblCoalesceNum";
+    runAndCheck(numQuery, Arrays.asList("+I[1]", "+I[20]", "+I[null]"));
+  }
+
+  @Test
+  void testTimestampLiteral() {
+    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd 
HH:mm:ss");
+    List<Row> rows =
+        Arrays.asList(
+            Row.of(1, LocalDateTime.parse("2024-01-15 03:04:05", formatter)), 
Row.of(2, null));
+    createSimpleBoundedValuesTable("tblTsLiteral", "a int, b Timestamp(3) 
NULL", rows);
+    String query =
+        "select DATE_FORMAT(COALESCE(b, TIMESTAMP '2024-03-15 10:15:30'), 
'yyyy-MM-dd HH:mm:ss') "
+            + "from tblTsLiteral";
+    runAndCheck(query, Arrays.asList("+I[2024-01-15 03:04:05]", "+I[2024-03-15 
10:15:30]"));
+
+    // Project the literal alone: anchors the converted epoch value directly,
+    // without the COALESCE + DATE_FORMAT round trip (formatting sensitive).
+    String literalQuery = "select TIMESTAMP '2024-03-15 10:15:30' from 
tblTsLiteral";
+    runAndCheck(
+        literalQuery, Arrays.asList("+I[2024-03-15T10:15:30.000]", 
"+I[2024-03-15T10:15:30.000]"));
+  }
 }


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

Reply via email to