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

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

commit 6526aff7af5a6070d917a5d307fc651ab965773a
Author: Sergey Nuyanzin <[email protected]>
AuthorDate: Fri Oct 9 11:42:23 2026 +0200

    [FLINK-40965][table] Treat `IFNULL` as conditional
---
 .../table/planner/codegen/ExprCodeGenerator.scala  | 13 ++++++++++--
 .../planner/runtime/stream/sql/FunctionITCase.java | 24 ++++++++++++++++++++++
 2 files changed, 35 insertions(+), 2 deletions(-)

diff --git 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/ExprCodeGenerator.scala
 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/ExprCodeGenerator.scala
index e6feb660c18..584aab8693f 100644
--- 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/ExprCodeGenerator.scala
+++ 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/ExprCodeGenerator.scala
@@ -566,15 +566,24 @@ class ExprCodeGenerator(
    *   - `AND(a_0, a_1, ..., a_n)` / `OR(...)`: only `a_0` is unconditional; 
subsequent operands are
    *     short-circuited by the operator semantics and the codegen.
    *   - `IF(cond, then, else)`: only `cond` is unconditional; `IfCallGen` 
guards `then`/`else`.
+   *   - `COALESCE(v_0, ..., v_n)` / `IFNULL(input, null_replacement)`: only 
`v_0` / `input` is
+   *     unconditional; `generateCoalesce` guards the rest.
    */
   private def conditionalOperandIndices(call: RexCall): Set[Int] = 
call.getKind match {
     case SqlKind.CASE | SqlKind.AND | SqlKind.OR | SqlKind.COALESCE =>
       (1 until call.getOperands.size).toSet
-    case SqlKind.OTHER_FUNCTION if call.getOperator == IF =>
+    case SqlKind.OTHER_FUNCTION if call.getOperator == IF || 
isCoalesceLike(call.getOperator) =>
       (1 until call.getOperands.size).toSet
     case _ => Set.empty
   }
 
+  private def isCoalesceLike(operator: SqlOperator): Boolean = operator match {
+    case bsf: BridgingSqlFunction =>
+      bsf.getDefinition == BuiltInFunctionDefinitions.COALESCE ||
+      bsf.getDefinition == BuiltInFunctionDefinitions.IF_NULL
+    case _ => false
+  }
+
   private def visitOperandInScopedCache(operand: RexNode): GeneratedExpression 
= {
     ctx.pushLocalRefScope()
     val (operandExpr, scopedBodies) = {
@@ -971,7 +980,7 @@ class ExprCodeGenerator(
           case BuiltInFunctionDefinitions.REGEXP_REPLACE =>
             StringCallGen.generateRegexpReplace(ctx, operands, resultType)
 
-          case BuiltInFunctionDefinitions.COALESCE =>
+          case BuiltInFunctionDefinitions.COALESCE | 
BuiltInFunctionDefinitions.IF_NULL =>
             generateCoalesce(ctx, operands, resultType)
 
           case _ =>
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/runtime/stream/sql/FunctionITCase.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/runtime/stream/sql/FunctionITCase.java
index 01d2e8f5f06..9e0466b7ac2 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/runtime/stream/sql/FunctionITCase.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/runtime/stream/sql/FunctionITCase.java
@@ -1371,6 +1371,30 @@ public class FunctionITCase extends StreamingTestBase {
         assertThat(actual).containsExactly(Row.of((Object) null), 
Row.of(1.5d));
     }
 
+    /**
+     * Pins the IFNULL guard interaction with the RexLocalRef cache. IFNULL is 
COALESCE, so
+     * null_replacement runs only when input is NULL. Input is never NULL 
here, so the {@code CAST(s
+     * AS DOUBLE)} in null_replacement stays guarded and never throws on 
{@code s = ""}.
+     */
+    @Test
+    void testCalcIfNullGuardShortCircuit() {
+        final List<Row> sourceData = List.of(Row.of(""), Row.of("1.5"));
+
+        TestCollectionTableFactory.reset();
+        TestCollectionTableFactory.initData(sourceData);
+        tEnv().executeSql("CREATE TABLE SourceTable (s STRING) WITH 
('connector' = 'COLLECTION')");
+
+        final List<Row> actual =
+                CollectionUtil.iteratorToList(
+                        tEnv().executeSql(
+                                        "SELECT IFNULL(CAST(CHAR_LENGTH(s) AS 
DOUBLE),"
+                                                + " CAST(s AS DOUBLE))"
+                                                + " FROM SourceTable")
+                                .collect());
+
+        assertThat(actual).containsExactly(Row.of(0.0d), Row.of(3.0d));
+    }
+
     @Test
     void testStructuredScalarFunction() throws Exception {
         final List<Row> sourceData =

Reply via email to