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 =
