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

raminqaf 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 34c36e63714 [FLINK-41087][table] Don't build the `PARSE_JSON` error 
for rows `TRY_PARSE_JSON` rejects (#29579)
34c36e63714 is described below

commit 34c36e6371467181bd0a130b760b5fd681d608dc
Author: Ramin Gharib <[email protected]>
AuthorDate: Sun Oct 25 17:43:34 2026 +0100

    [FLINK-41087][table] Don't build the `PARSE_JSON` error for rows 
`TRY_PARSE_JSON` rejects (#29579)
    
    PARSE_JSON and TRY_PARSE_JSON share one generated parse per input. The 
shared parse called SqlJsonUtils#parseJsonToVariant, which scanned the input 
for invalid UTF-8, formatted it into a message and created a 
TableRuntimeException on every failure. TRY_PARSE_JSON then dropped that 
exception and returned NULL, so every row it rejected paid for a second 
exception with its stack trace and a copy of the input.
    
    parseJsonToVariant now lets the parser's exception through. The new 
SqlJsonUtils#parseJsonError builds the TableRuntimeException with the UTF-8 
diagnostics, and only a PARSE_JSON call site calls it. Error messages and 
results do not change.
    
    Generated-by: Claude Code 2.1.309 (Claude Opus 5.5)
---
 .../table/planner/codegen/JsonCodeGenUtils.java    | 14 +++++--
 .../planner/codegen/calls/BuiltInMethods.scala     |  6 +++
 .../table/planner/codegen/JsonParseReuseTest.java  | 33 +++++++++++++++--
 .../table/runtime/functions/SqlJsonUtils.java      | 43 ++++++++++++++--------
 4 files changed, 72 insertions(+), 24 deletions(-)

diff --git 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/codegen/JsonCodeGenUtils.java
 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/codegen/JsonCodeGenUtils.java
index 2dd2f068c71..8ab8c83580b 100644
--- 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/codegen/JsonCodeGenUtils.java
+++ 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/codegen/JsonCodeGenUtils.java
@@ -43,7 +43,9 @@ public final class JsonCodeGenUtils {
      *
      * <p>The input is parsed at most once per record for a given (input, 
allowDuplicateKeys) pair
      * and shared across all such calls; {@code PARSE_JSON} rethrows a parse 
error while {@code
-     * TRY_PARSE_JSON} returns NULL.
+     * TRY_PARSE_JSON} returns NULL. The shared parse keeps the parser's 
exception as is. Only a
+     * {@code PARSE_JSON} call site turns it into the user-facing error, so a 
row that {@code
+     * TRY_PARSE_JSON} rejects costs no more than the failed parse.
      *
      * <p>For {@code PARSE_JSON(str)} the generated code looks like:
      *
@@ -66,7 +68,7 @@ public final class JsonCodeGenUtils {
      *         SqlJsonUtils.parseJsonToVariant(in, 
java.lang.Boolean.TRUE.equals(allowDup));
      *   } catch (Exception e) {
      *     variantParsed$0 = null;
-     *     variantParseError$0 = t;
+     *     variantParseError$0 = e;
      *   }
      *   return variantParsed$0;
      * }
@@ -77,7 +79,7 @@ public final class JsonCodeGenUtils {
      * if (!inIsNull) {
      *   result$0 = parseVariant$0(in, null);
      *   if (variantParseError$0 != null) { // only emitted for PARSE_JSON, 
not TRY_PARSE_JSON
-     *     org.apache.flink.util.ExceptionUtils.rethrow(variantParseError$0);
+     *     throw SqlJsonUtils.parseJsonError(in, variantParseError$0);
      *   }
      *   isNull$0 = result$0 == null;
      * }
@@ -207,7 +209,11 @@ public final class JsonCodeGenUtils {
                         : "  if ("
                                 + errorTerm
                                 + " != null) {\n"
-                                + "    
org.apache.flink.util.ExceptionUtils.rethrow("
+                                + "    throw "
+                                + 
CodeGenUtils.qualifyMethod(BuiltInMethods.PARSE_JSON_ERROR())
+                                + "("
+                                + input.resultTerm()
+                                + ", "
                                 + errorTerm
                                 + ");\n"
                                 + "  }\n";
diff --git 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/BuiltInMethods.scala
 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/BuiltInMethods.scala
index 9bb1de380a5..e1d78cae339 100644
--- 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/BuiltInMethods.scala
+++ 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/BuiltInMethods.scala
@@ -520,6 +520,12 @@ object BuiltInMethods {
     classOf[StringData],
     classOf[Boolean])
 
+  val PARSE_JSON_ERROR = Types.lookupMethod(
+    classOf[SqlJsonUtils],
+    "parseJsonError",
+    classOf[StringData],
+    classOf[Throwable])
+
   val JSON_TYPE =
     Types.lookupMethod(classOf[SqlJsonUtils], "jsonType", 
classOf[SqlJsonUtils.JsonValueContext])
 
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/codegen/JsonParseReuseTest.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/codegen/JsonParseReuseTest.java
index a429e541911..b6f4e63189e 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/codegen/JsonParseReuseTest.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/codegen/JsonParseReuseTest.java
@@ -53,6 +53,10 @@ class JsonParseReuseTest {
     private static final Pattern PARSE_JSON_PATTERN =
             Pattern.compile("\\b" + 
Pattern.quote(BuiltInMethods.PARSE_JSON().getName()) + "\\(");
 
+    private static final Pattern PARSE_JSON_ERROR_PATTERN =
+            Pattern.compile(
+                    "\\b" + 
Pattern.quote(BuiltInMethods.PARSE_JSON_ERROR().getName()) + "\\(");
+
     private StreamTableEnvironment tEnv;
 
     private static final String JSON_ROW1 =
@@ -84,6 +88,10 @@ class JsonParseReuseTest {
         return count(PARSE_JSON_PATTERN, code);
     }
 
+    private static int countParseJsonError(final String code) {
+        return count(PARSE_JSON_ERROR_PATTERN, code);
+    }
+
     private static int count(final Pattern pattern, final String code) {
         final Matcher m = pattern.matcher(code);
         int count = 0;
@@ -529,14 +537,31 @@ class JsonParseReuseTest {
         final String badSrc = "(VALUES ('not json')) AS t(j)";
         final String sharedSql =
                 "SELECT JSON_STRING(TRY_PARSE_JSON(j)), 
JSON_STRING(PARSE_JSON(j)) FROM " + badSrc;
-        assertThat(countParseJson(extractGeneratedCode(sharedSql)))
+        final String sharedCode = extractGeneratedCode(sharedSql);
+        assertThat(countParseJson(sharedCode))
                 .as("PARSE_JSON + TRY_PARSE_JSON on the same input should 
parse once")
                 .isOne();
+        assertThat(countParseJsonError(sharedCode))
+                .as("Only the PARSE_JSON call site builds the parse error")
+                .isOne();
         assertThatThrownBy(() -> collect(sharedSql))
-                .hasStackTraceContaining("Failed to parse JSON string");
+                .hasStackTraceContaining("Failed to parse JSON string: not 
json");
+    }
 
-        assertThat(collect("SELECT JSON_STRING(TRY_PARSE_JSON(j)) FROM " + 
badSrc))
-                .containsExactly(Row.of((String) null));
+    @Test
+    void testTryParseJsonDoesNotBuildParseError() {
+        tEnv.createTemporaryView(
+                "mixed_src", tEnv.fromValues(Row.of("not json"), 
Row.of("{\"a\":1}")).as("j"));
+        final String sql = "SELECT JSON_STRING(TRY_PARSE_JSON(j)) FROM 
mixed_src";
+        assertThat(collect(sql))
+                .containsExactlyInAnyOrder(Row.of((String) null), 
Row.of("{\"a\":1}"));
+        final String code = extractGeneratedCode(sql);
+        assertThat(countParseJson(code))
+                .as("TRY_PARSE_JSON must be generated, not constant-folded 
away")
+                .isOne();
+        assertThat(countParseJsonError(code))
+                .as("TRY_PARSE_JSON drops the parser's exception without 
building the error")
+                .isZero();
     }
 
     @Test
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/SqlJsonUtils.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/SqlJsonUtils.java
index b319d519e73..431d0248cf6 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/SqlJsonUtils.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/SqlJsonUtils.java
@@ -58,6 +58,7 @@ import 
org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind.node.Obje
 
 import javax.annotation.Nullable;
 
+import java.io.IOException;
 import java.lang.reflect.Array;
 import java.math.BigDecimal;
 import java.math.BigInteger;
@@ -632,24 +633,34 @@ public class SqlJsonUtils {
         return jsonValueExpression(input);
     }
 
-    /** Parses a JSON string into a {@link Variant}, throwing on malformed 
input. */
-    public static Variant parseJsonToVariant(StringData jsonStr, boolean 
allowDuplicateKeys) {
+    /**
+     * Parses a JSON string into a {@link Variant}.
+     *
+     * <p>Malformed input fails with the parser's own exception, which {@code 
TRY_PARSE_JSON} can
+     * drop cheaply. {@code PARSE_JSON} reports it through {@link 
#parseJsonError}.
+     */
+    public static Variant parseJsonToVariant(StringData jsonStr, boolean 
allowDuplicateKeys)
+            throws IOException {
+        return BinaryVariantInternalBuilder.parseJson(jsonStr.toBytes(), 
allowDuplicateKeys);
+    }
+
+    /**
+     * Builds the error that {@code PARSE_JSON} throws when {@link 
#parseJsonToVariant} rejects
+     * {@code jsonStr}. Input that is not valid UTF-8 is reported by the index 
of its first invalid
+     * byte rather than by its content.
+     */
+    public static TableRuntimeException parseJsonError(StringData jsonStr, 
Throwable cause) {
         final byte[] bytes = jsonStr.toBytes();
-        try {
-            return BinaryVariantInternalBuilder.parseJson(bytes, 
allowDuplicateKeys);
-        } catch (Exception e) {
-            final int invalidIndex =
-                    StringUtf8Utils.firstInvalidUtf8ByteIndex(bytes, 0, 
bytes.length);
-            if (invalidIndex >= 0) {
-                throw new TableRuntimeException(
-                        String.format(
-                                "Failed to parse JSON string: Invalid UTF-8 
byte at index %d of %d.",
-                                invalidIndex, bytes.length),
-                        e);
-            }
-            throw new TableRuntimeException(
-                    String.format("Failed to parse JSON string: %s", jsonStr), 
e);
+        final int invalidIndex = 
StringUtf8Utils.firstInvalidUtf8ByteIndex(bytes, 0, bytes.length);
+        if (invalidIndex >= 0) {
+            return new TableRuntimeException(
+                    String.format(
+                            "Failed to parse JSON string: Invalid UTF-8 byte 
at index %d of %d.",
+                            invalidIndex, bytes.length),
+                    cause);
         }
+        return new TableRuntimeException(
+                String.format("Failed to parse JSON string: %s", jsonStr), 
cause);
     }
 
     private static JsonValueContext jsonValueExpression(String input) {

Reply via email to