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 15e278160fd [FLINK-40463][table] Widen `JSON_VALUE` and `JSON_QUERY` 
`RETURNING` type support (#29506)
15e278160fd is described below

commit 15e278160fd0572ae06f70d073aeb743ee344315
Author: Sergey Nuyanzin <[email protected]>
AuthorDate: Wed Oct 21 12:22:57 2026 +0200

    [FLINK-40463][table] Widen `JSON_VALUE` and `JSON_QUERY` `RETURNING` type 
support (#29506)
    
    [FLINK-40463][table] Widen `JSON_VALUE` and `JSON_QUERY` `RETURNING` type 
support
    
    `JSON_VALUE ... RETURNING` now also accepts `TINYINT`, `SMALLINT`, 
`BIGINT`, `FLOAT` and `DECIMAL`. `JSON_QUERY ... RETURNING` accepts an `ARRAY` 
of any of these scalar types, not just `ARRAY<STRING>`.
    
    JSON strings are coerced to the requested type, for example `"42"` to a 
number or `"true"` to `TRUE`. A value that does not fit, such as an overflow or 
a non-numeric string, follows the `ON ERROR` behavior.
    
    This closes #29506.
    ---------
    Generated-by: Claude <[email protected]>
    Co-authored-by: Chris Johnson <[email protected]>
---
 docs/data/sql_functions.yml                        |  16 +-
 docs/data/sql_functions_zh.yml                     |  22 +-
 flink-python/pyflink/table/expression.py           |   5 +
 .../flink/table/api/internal/BaseExpressions.java  |  11 +-
 .../functions/sql/SqlJsonQueryFunctionWrapper.java |   9 +-
 .../planner/codegen/calls/JsonQueryCallGen.scala   |  82 ++-
 .../planner/codegen/calls/JsonValueCallGen.scala   | 105 ++--
 .../planner/functions/CastFunctionITCase.java      |   7 +-
 .../planner/functions/JsonFunctionsITCase.java     | 343 ++++++++++++-
 .../src/test/resources/json/json-value.json        |   2 +
 .../table/data/binary/BinaryStringDataUtil.java    |   6 +-
 .../table/runtime/functions/SqlJsonUtils.java      | 321 +++++++++++-
 .../functions/SqlJsonUtilsConversionTest.java      | 559 +++++++++++++++++++++
 13 files changed, 1413 insertions(+), 75 deletions(-)

diff --git a/docs/data/sql_functions.yml b/docs/data/sql_functions.yml
index 4244ed6a49e..22463dcdf00 100644
--- a/docs/data/sql_functions.yml
+++ b/docs/data/sql_functions.yml
@@ -1086,8 +1086,18 @@ json:
 
       * `VARCHAR` / `STRING`
       * `BOOLEAN`
+      * `TINYINT`
+      * `SMALLINT`
       * `INTEGER`
+      * `BIGINT`
+      * `FLOAT`
       * `DOUBLE`
+      * `DECIMAL`
+
+      JSON string values are coerced to the requested numeric or boolean type 
(for example `"42"`
+      to a number, or `"true"`/`"t"`/`"yes"`/`"y"`/`"1"` to `TRUE`).
+      Values that do not fit the requested type (overflow, non-numeric string, 
non-0/1 number for
+      `BOOLEAN`) are treated as an error and follow the `ON ERROR` behavior.
 
       For empty path expressions or errors a behavior can be defined to either 
return `null`, raise
       an error or return a defined default value instead. When omitted, the 
default is
@@ -1127,7 +1137,11 @@ json:
     description: |
       Extracts JSON values from a JSON string.
 
-      The result is returned as a `STRING` or `ARRAY<STRING>`. This can be 
controlled with the `RETURNING` clause.
+      The result is returned as a `STRING` by default. Using the `RETURNING` 
clause it can also be
+      returned as an `ARRAY` whose element type is `STRING` or any of the 
scalar types supported by
+      `JSON_VALUE` (`BOOLEAN`, `TINYINT`, `SMALLINT`, `INTEGER`, `BIGINT`, 
`FLOAT`, `DOUBLE`,
+      `DECIMAL`). Array elements are converted with the same coercion and `ON 
ERROR` rules as
+      `JSON_VALUE`.
 
       The `wrappingBehavior` determines whether the extracted value should be 
wrapped into an array,
       and whether to do so unconditionally or only if the value itself isn't 
an array already.
diff --git a/docs/data/sql_functions_zh.yml b/docs/data/sql_functions_zh.yml
index 74b5b0b9389..d1dc90163fb 100644
--- a/docs/data/sql_functions_zh.yml
+++ b/docs/data/sql_functions_zh.yml
@@ -1219,8 +1219,18 @@ json:
 
       * `VARCHAR` / `STRING`
       * `BOOLEAN`
+      * `TINYINT`
+      * `SMALLINT`
       * `INTEGER`
+      * `BIGINT`
+      * `FLOAT`
       * `DOUBLE`
+      * `DECIMAL`
+
+      JSON string values are coerced to the requested numeric or boolean type 
(for example `"42"`
+      to a number, or `"true"`/`"t"`/`"yes"`/`"y"`/`"1"` to `TRUE`).
+      Values that do not fit the requested type (overflow, non-numeric string, 
non-0/1 number for
+      `BOOLEAN`) are treated as an error and follow the `ON ERROR` behavior.
 
       对于空路径表达式或错误,可以将行为定义为返回 `null`、引发错误或返回定义的默认值。当省略时,默认为 `NULL ON EMPTY` 或
       `NULL ON ERROR`。默认值可以是文字或表达式。如果默认值本身引发错误,就会造成 `ON EMPTY` 的错误行为,并引发 `ON 
ERROR` 的错误。
@@ -1250,12 +1260,16 @@ json:
       -- "right"
       JSON_VALUE('{"contains blank": "right"}', 'strict $.[''contains 
blank'']' NULL ON EMPTY DEFAULT 'wrong' ON ERROR)
       ```
-  - sql: JSON_QUERY(jsonValue, path [ { WITHOUT | WITH CONDITIONAL | WITH 
UNCONDITIONAL } [ ARRAY ] WRAPPER ] [ { NULL | EMPTY ARRAY | EMPTY OBJECT | 
ERROR } ON EMPTY ] [ { NULL | EMPTY ARRAY | EMPTY OBJECT | ERROR } ON ERROR ])
-    table: STRING.jsonQuery(path [, JsonQueryWrapper [, 
JsonQueryOnEmptyOrError, JsonQueryOnEmptyOrError ] ])
+  - sql: JSON_QUERY(jsonValue, path [RETURNING <dataType>] [ { WITHOUT | WITH 
CONDITIONAL | WITH UNCONDITIONAL } [ ARRAY ] WRAPPER ] [ { NULL | EMPTY ARRAY | 
EMPTY OBJECT | ERROR } ON EMPTY ] [ { NULL | EMPTY ARRAY | EMPTY OBJECT | ERROR 
} ON ERROR ])
+    table: STRING.jsonQuery(path [, returnType [, JsonQueryWrapper [, 
JsonQueryOnEmptyOrError, JsonQueryOnEmptyOrError ] ] ])
     description: |
-      从 JSON 字符串中提取 JSON 值。
+      Extracts JSON values from a JSON string.
 
-      结果总是以 `STRING` 的形式返回。目前尚不支持 `RETURNING` 子句。
+      The result is returned as a `STRING` by default. Using the `RETURNING` 
clause it can also be
+      returned as an `ARRAY` whose element type is `STRING` or any of the 
scalar types supported by
+      `JSON_VALUE` (`BOOLEAN`, `TINYINT`, `SMALLINT`, `INTEGER`, `BIGINT`, 
`FLOAT`, `DOUBLE`,
+      `DECIMAL`). Array elements are converted with the same coercion and `ON 
ERROR` rules as
+      `JSON_VALUE`.
 
       `wrappingBehavior` 决定是否将提取的值包装到一个数组中,以及是否无条件地这样做,还是只有当值本身不是数组时才这样做。
 
diff --git a/flink-python/pyflink/table/expression.py 
b/flink-python/pyflink/table/expression.py
index 4adaa96ca4b..0160904e263 100644
--- a/flink-python/pyflink/table/expression.py
+++ b/flink-python/pyflink/table/expression.py
@@ -2305,8 +2305,13 @@ class Expression(Generic[T]):
 
         * `STRING`
         * `BOOLEAN`
+        * `TINYINT`
+        * `SMALLINT`
         * `INT`
+        * `BIGINT`
+        * `FLOAT`
         * `DOUBLE`
+        * `DECIMAL`
 
         For empty path expressions or errors a behavior can be defined to 
either return `null`,
         raise an error or return a defined default value instead.
diff --git 
a/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/api/internal/BaseExpressions.java
 
b/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/api/internal/BaseExpressions.java
index c4b7f9677d5..29f12604fc8 100644
--- 
a/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/api/internal/BaseExpressions.java
+++ 
b/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/api/internal/BaseExpressions.java
@@ -2436,8 +2436,13 @@ public abstract class BaseExpressions<InType, OutType> {
      * <ul>
      *   <li>{@link DataTypes#STRING()}
      *   <li>{@link DataTypes#BOOLEAN()}
+     *   <li>{@link DataTypes#TINYINT()}
+     *   <li>{@link DataTypes#SMALLINT()}
      *   <li>{@link DataTypes#INT()}
+     *   <li>{@link DataTypes#BIGINT()}
+     *   <li>{@link DataTypes#FLOAT()}
      *   <li>{@link DataTypes#DOUBLE()}
+     *   <li>{@link DataTypes#DECIMAL(int, int)}
      * </ul>
      *
      * <p>For empty path expressions or errors a behavior can be defined to 
either return {@code
@@ -2571,7 +2576,11 @@ public abstract class BaseExpressions<InType, OutType> {
      * Extracts JSON values from a JSON string.
      *
      * <p>This follows the ISO/IEC TR 19075-6 specification for JSON support 
in SQL. The result is
-     * always returned as a {@link DataTypes#STRING()}.
+     * returned as a {@link DataTypes#STRING()} by default. Using {@param 
returnType} it can also be
+     * returned as an {@link DataTypes#ARRAY(DataType)} whose element type is 
{@code STRING} or any
+     * of the scalar types supported by {@link #jsonValue(String, DataType)} 
({@code BOOLEAN},
+     * {@code TINYINT}, {@code SMALLINT}, {@code INTEGER}, {@code BIGINT}, 
{@code FLOAT}, {@code
+     * DOUBLE}, {@code DECIMAL}).
      *
      * <p>The {@param wrappingBehavior} determines whether the extracted value 
should be wrapped
      * into an array, and whether to do so unconditionally or only if the 
value itself isn't an
diff --git 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/sql/SqlJsonQueryFunctionWrapper.java
 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/sql/SqlJsonQueryFunctionWrapper.java
index ddae97fa232..31cacfa55f7 100644
--- 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/sql/SqlJsonQueryFunctionWrapper.java
+++ 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/sql/SqlJsonQueryFunctionWrapper.java
@@ -19,6 +19,8 @@
 package org.apache.flink.table.planner.functions.sql;
 
 import org.apache.flink.table.api.ValidationException;
+import org.apache.flink.table.planner.calcite.FlinkTypeFactory;
+import org.apache.flink.table.runtime.functions.SqlJsonUtils;
 
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.sql.SqlCallBinding;
@@ -89,12 +91,15 @@ public class SqlJsonQueryFunctionWrapper extends 
SqlJsonQueryFunction {
 
     private static boolean checkOperandsForArrayReturnType(
             boolean throwOnFailure, RelDataType type, SqlCallBinding 
callBinding) {
-        if (!SqlTypeUtil.isCharacter(type.getComponentType())) {
+        RelDataType elementType = type.getComponentType();
+        if (!SqlTypeUtil.isCharacter(elementType)
+                && !SqlJsonUtils.isSupportedJsonReturningType(
+                        
FlinkTypeFactory.toLogicalType(elementType).getTypeRoot())) {
             if (throwOnFailure) {
                 throw new ValidationException(
                         String.format(
                                 "Unsupported array element type '%s' for 
RETURNING ARRAY in JSON_QUERY().",
-                                type.getComponentType()));
+                                elementType));
             } else {
                 return false;
             }
diff --git 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/JsonQueryCallGen.scala
 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/JsonQueryCallGen.scala
index 5eb269ff4f8..ef5e1e2349b 100644
--- 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/JsonQueryCallGen.scala
+++ 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/JsonQueryCallGen.scala
@@ -17,14 +17,14 @@
  */
 package org.apache.flink.table.planner.codegen.calls
 
-import org.apache.flink.table.api.{JsonQueryOnEmptyOrError, JsonQueryWrapper, 
JsonValueOnEmptyOrError}
+import org.apache.flink.table.api.{JsonQueryOnEmptyOrError, JsonQueryWrapper}
+import org.apache.flink.table.api.ValidationException
 import org.apache.flink.table.planner.codegen.{CodeGeneratorContext, 
CodeGenException, CodeGenUtils, GeneratedExpression}
 import org.apache.flink.table.planner.codegen.CodeGenUtils.{qualifyEnum, 
qualifyMethod, BINARY_STRING, GENERIC_ARRAY}
 import 
org.apache.flink.table.planner.codegen.GenerateUtils.generateCallWithStmtIfArgsNotNull
+import org.apache.flink.table.runtime.functions.SqlJsonUtils
 import 
org.apache.flink.table.runtime.functions.SqlJsonUtils.JsonQueryReturnType
-import org.apache.flink.table.types.logical.{ArrayType, LogicalType, 
LogicalTypeRoot}
-
-import org.apache.calcite.sql.SqlJsonEmptyOrError
+import org.apache.flink.table.types.logical.{ArrayType, DecimalType, 
LogicalType, LogicalTypeRoot}
 
 /**
  * [[CallGenerator]] for [[BuiltInMethods.JSON_QUERY]].
@@ -72,11 +72,24 @@ class JsonQueryCallGen extends CallGenerator {
           val emptyBehavior = 
operands(3).literalValue.get.asInstanceOf[JsonQueryOnEmptyOrError]
           val errorBehavior = 
operands(4).literalValue.get.asInstanceOf[JsonQueryOnEmptyOrError]
           val wrapperBehavior = 
operands(2).literalValue.get.asInstanceOf[JsonQueryWrapper]
-          val jsonQueryReturnType = if (returnType.getTypeRoot == 
LogicalTypeRoot.ARRAY) {
-            JsonQueryReturnType.ARRAY
-          } else {
-            JsonQueryReturnType.STRING
+
+          // Three return paths:
+          //   VARCHAR         -> STRING    -> serialize to StringData
+          //   ARRAY<VARCHAR>  -> ARRAY     -> GenericArrayData of StringData 
(existing)
+          //   ARRAY<typed T>  -> RAW_ARRAY -> Object[] post-converted via 
convertJsonArray
+          val (isTypedArray, elementType) = returnType.getTypeRoot match {
+            case LogicalTypeRoot.ARRAY =>
+              val et = returnType.asInstanceOf[ArrayType].getElementType
+              (et.getTypeRoot != LogicalTypeRoot.VARCHAR, et)
+            case _ => (false, null)
+          }
+
+          val jsonQueryReturnType = returnType.getTypeRoot match {
+            case LogicalTypeRoot.ARRAY if isTypedArray => 
JsonQueryReturnType.RAW_ARRAY
+            case LogicalTypeRoot.ARRAY => JsonQueryReturnType.ARRAY
+            case _ => JsonQueryReturnType.STRING
           }
+
           val terms = Seq(
             parsed.resultTerm,
             s"${argTerms(1)}.toString()",
@@ -87,29 +100,54 @@ class JsonQueryCallGen extends CallGenerator {
           )
 
           val rawResultTerm = CodeGenUtils.newName(ctx, "rawResult")
-          val call = s"""
-                        |Object $rawResultTerm =
-                        |    
${qualifyMethod(BuiltInMethods.JSON_QUERY_PARSED)}(${terms
-                         .mkString(", ")});
+          val baseCall = s"""
+                            |Object $rawResultTerm =
+                            |    
${qualifyMethod(BuiltInMethods.JSON_QUERY_PARSED)}(${terms
+                             .mkString(", ")});
            """.stripMargin
 
-          val convertedResult = returnType.getTypeRoot match {
+          val (additionalCall, convertedResult) = returnType.getTypeRoot match 
{
             case LogicalTypeRoot.VARCHAR =>
-              
s"$BINARY_STRING.fromString(java.lang.String.valueOf($rawResultTerm))"
+              ("", 
s"$BINARY_STRING.fromString(java.lang.String.valueOf($rawResultTerm))")
+            case LogicalTypeRoot.ARRAY if !isTypedArray =>
+              ("", s"($GENERIC_ARRAY) $rawResultTerm")
+            case LogicalTypeRoot.ARRAY
+                if 
!SqlJsonUtils.isSupportedJsonReturningType(elementType.getTypeRoot) =>
+              throw new CodeGenException(
+                s"Unsupported element type '${elementType.getTypeRoot}' "
+                  + "for RETURNING ARRAY in JSON_QUERY().")
             case LogicalTypeRoot.ARRAY =>
-              val elementType = 
returnType.asInstanceOf[ArrayType].getElementType
-              if (elementType.getTypeRoot == LogicalTypeRoot.VARCHAR) {
-                s"($GENERIC_ARRAY) $rawResultTerm"
-              } else {
-                throw new CodeGenException(
-                  s"Unsupported array element type '$elementType' for 
RETURNING ARRAY in JSON_QUERY().")
+              val jsonUtils =
+                classOf[SqlJsonUtils].getName
+              val typeRootEnum =
+                
s"${classOf[LogicalTypeRoot].getName}.${elementType.getTypeRoot.name()}"
+              val (precisionStr, scaleStr) = elementType.getTypeRoot match {
+                case LogicalTypeRoot.DECIMAL =>
+                  val dt = elementType.asInstanceOf[DecimalType]
+                  (dt.getPrecision.toString, dt.getScale.toString)
+                case _ => ("0", "0")
               }
+              val errorBehaviorEnum = qualifyEnum(errorBehavior)
+
+              val elementNullableStr = elementType.isNullable.toString
+              val convertedTerm = CodeGenUtils.newName(ctx, "convertedArr")
+              val conversionCode =
+                s"""
+                   |$GENERIC_ARRAY $convertedTerm = 
$jsonUtils.convertJsonArray(
+                   |    $rawResultTerm, $typeRootEnum,
+                   |    $precisionStr, $scaleStr,
+                   |    $elementNullableStr,
+                   |    $errorBehaviorEnum);
+                 """.stripMargin
+
+              (conversionCode, s"$convertedTerm")
             case _ =>
-              throw new CodeGenException(
+              throw new ValidationException(
                 s"Unsupported type '$returnType' "
-                  + "for RETURNING in JSON_VALUE().")
+                  + "for RETURNING in JSON_QUERY().")
           }
 
+          val call = baseCall + additionalCall
           val result = s"($rawResultTerm == null) ? null : ($convertedResult)"
           (call, result)
         }
diff --git 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/JsonValueCallGen.scala
 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/JsonValueCallGen.scala
index 321a6d072c3..1bf570f3312 100644
--- 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/JsonValueCallGen.scala
+++ 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/JsonValueCallGen.scala
@@ -19,9 +19,10 @@ package org.apache.flink.table.planner.codegen.calls
 
 import org.apache.flink.table.api.JsonValueOnEmptyOrError
 import org.apache.flink.table.planner.codegen.{CodeGeneratorContext, 
CodeGenException, CodeGenUtils, GeneratedExpression}
-import org.apache.flink.table.planner.codegen.CodeGenUtils.{qualifyEnum, 
qualifyMethod, BINARY_STRING}
+import 
org.apache.flink.table.planner.codegen.CodeGenUtils.{isPrimitiveNullable, 
primitiveDefaultValue, primitiveTypeTermForType, qualifyEnum, qualifyMethod, 
BINARY_STRING}
 import 
org.apache.flink.table.planner.codegen.GenerateUtils.generateCallWithStmtIfArgsNotNull
-import org.apache.flink.table.types.logical.{LogicalType, LogicalTypeRoot}
+import org.apache.flink.table.runtime.functions.SqlJsonUtils
+import org.apache.flink.table.types.logical.{DecimalType, LogicalType, 
LogicalTypeRoot}
 
 import org.apache.calcite.sql.SqlJsonEmptyOrError
 
@@ -66,43 +67,81 @@ class JsonValueCallGen extends CallGenerator {
 
     val parsed = JsonParseReuse.parseSharedInput(ctx, operands)
 
-    generateCallWithStmtIfArgsNotNull(ctx, returnType, operands, 
resultNullable = true) {
-      argTerms =>
-        {
-          val emptyBehavior = getBehavior(operands, SqlJsonEmptyOrError.EMPTY)
-          val errorBehavior = getBehavior(operands, SqlJsonEmptyOrError.ERROR)
+    val generated =
+      generateCallWithStmtIfArgsNotNull(ctx, returnType, operands, 
resultNullable = true) {
+        argTerms =>
+          {
+            val emptyBehavior = getBehavior(operands, 
SqlJsonEmptyOrError.EMPTY)
+            val errorBehavior = getBehavior(operands, 
SqlJsonEmptyOrError.ERROR)
 
-          val terms = Seq(
-            parsed.resultTerm,
-            s"${argTerms(1)}.toString()",
-            qualifyEnum(emptyBehavior._1),
-            emptyBehavior._2,
-            qualifyEnum(errorBehavior._1),
-            errorBehavior._2
-          )
+            val terms = Seq(
+              parsed.resultTerm,
+              s"${argTerms(1)}.toString()",
+              qualifyEnum(emptyBehavior._1),
+              emptyBehavior._2,
+              qualifyEnum(errorBehavior._1),
+              errorBehavior._2
+            )
 
-          val rawResultTerm = CodeGenUtils.newName(ctx, "rawResult")
-          val call = s"""
-                        |Object $rawResultTerm =
-                        |    
${qualifyMethod(BuiltInMethods.JSON_VALUE)}(${terms
-                         .mkString(", ")});
+            val rawResultTerm = CodeGenUtils.newName(ctx, "rawResult")
+            val baseCall = s"""
+                              |Object $rawResultTerm =
+                              |    
${qualifyMethod(BuiltInMethods.JSON_VALUE)}(${terms
+                               .mkString(", ")});
            """.stripMargin
 
-          val convertedResult = returnType.getTypeRoot match {
-            case LogicalTypeRoot.VARCHAR =>
-              
s"$BINARY_STRING.fromString(java.lang.String.valueOf($rawResultTerm))"
-            case LogicalTypeRoot.BOOLEAN => s"(java.lang.Boolean) 
$rawResultTerm"
-            case LogicalTypeRoot.INTEGER => s"(java.lang.Integer) 
$rawResultTerm"
-            case LogicalTypeRoot.DOUBLE => s"((java.math.BigDecimal) 
$rawResultTerm).doubleValue()"
-            case _ =>
-              throw new CodeGenException(
-                s"Unsupported type '$returnType' "
-                  + "for RETURNING in JSON_VALUE().")
+            returnType.getTypeRoot match {
+              case LogicalTypeRoot.VARCHAR =>
+                val result =
+                  s"($rawResultTerm == null) ? null : " +
+                    
s"($BINARY_STRING.fromString(java.lang.String.valueOf($rawResultTerm)))"
+                (baseCall, result)
+              case typeRoot if 
!SqlJsonUtils.isSupportedJsonReturningType(typeRoot) =>
+                throw new CodeGenException(
+                  s"Unsupported type '$returnType' for RETURNING in 
JSON_VALUE().")
+              case _ =>
+                val jsonUtils =
+                  classOf[SqlJsonUtils].getName
+                val typeRootEnum =
+                  
s"${classOf[LogicalTypeRoot].getName}.${returnType.getTypeRoot.name()}"
+                val (precisionStr, scaleStr) = returnType.getTypeRoot match {
+                  case LogicalTypeRoot.DECIMAL =>
+                    val dt = returnType.asInstanceOf[DecimalType]
+                    (dt.getPrecision.toString, dt.getScale.toString)
+                  case _ => ("0", "0")
+                }
+                val errorBehaviorEnum =
+                  
qualifyEnum(errorBehavior._1.asInstanceOf[JsonValueOnEmptyOrError])
+                val defaultExpr =
+                  if (errorBehavior._2 != null) errorBehavior._2 else "null"
+
+                val boxedType = CodeGenUtils.boxedTypeTermForType(returnType)
+                val convertedTerm = CodeGenUtils.newName(ctx, "converted")
+                val call = baseCall +
+                  s"""
+                     |$boxedType $convertedTerm = ($boxedType) 
$jsonUtils.convertJsonScalar(
+                     |    $rawResultTerm, $typeRootEnum,
+                     |    $precisionStr, $scaleStr,
+                     |    $errorBehaviorEnum, $defaultExpr);
+                 """.stripMargin
+                (call, s"$convertedTerm")
+            }
           }
+      }
 
-          val result = s"($rawResultTerm == null) ? null : ($convertedResult)"
-          (call, result)
-        }
+    if (isPrimitiveNullable(returnType)) {
+      generated
+    } else {
+      val unboxedTerm =
+        ctx.addReusableLocalVariable(primitiveTypeTermForType(returnType), 
"jsonValueUnboxed")
+      val unboxedCode =
+        s"""
+           |${generated.code}
+           |$unboxedTerm = ${generated.nullTerm}
+           |    ? ${primitiveDefaultValue(returnType)}
+           |    : ${generated.resultTerm};
+           |""".stripMargin
+      generated.copy(resultTerm = unboxedTerm, code = unboxedCode)
     }
   }
 
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/CastFunctionITCase.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/CastFunctionITCase.java
index e3eba6e9a73..6163ee022f3 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/CastFunctionITCase.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/CastFunctionITCase.java
@@ -20,7 +20,6 @@ package org.apache.flink.table.planner.functions;
 
 import org.apache.flink.configuration.Configuration;
 import org.apache.flink.table.annotation.DataTypeHint;
-import org.apache.flink.table.api.TableException;
 import org.apache.flink.table.api.TableRuntimeException;
 import org.apache.flink.table.api.config.TableConfigOptions;
 import org.apache.flink.table.functions.BuiltInFunctionDefinitions;
@@ -1252,12 +1251,12 @@ public class CastFunctionITCase extends 
BuiltInFunctionTestBase {
         return Arrays.asList(
                 CastTestSpecBuilder.testCastTo(BOOLEAN())
                         .fromCase(BOOLEAN(), null, null)
-                        .failRuntime(CHAR(3), "foo", TableException.class)
+                        .failRuntime(CHAR(3), "foo", 
TableRuntimeException.class)
                         .fromCase(CHAR(4), "true", true)
                         .fromCase(VARCHAR(5), "FalsE", false)
-                        .failRuntime(STRING(), "Apache Flink", 
TableException.class)
+                        .failRuntime(STRING(), "Apache Flink", 
TableRuntimeException.class)
                         .fromCase(STRING(), "TRUE", true)
-                        .failRuntime(STRING(), "", TableException.class)
+                        .failRuntime(STRING(), "", TableRuntimeException.class)
                         .fromCase(BOOLEAN(), true, true)
                         .fromCase(BOOLEAN(), false, false)
                         // Not supported - no fix
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/JsonFunctionsITCase.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/JsonFunctionsITCase.java
index 2cd54053210..aaeb24f6a0a 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/JsonFunctionsITCase.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/JsonFunctionsITCase.java
@@ -38,6 +38,7 @@ import org.apache.commons.io.IOUtils;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.math.BigDecimal;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.time.Instant;
@@ -51,18 +52,22 @@ import java.util.Map;
 import java.util.stream.Stream;
 
 import static org.apache.flink.table.api.DataTypes.ARRAY;
+import static org.apache.flink.table.api.DataTypes.BIGINT;
 import static org.apache.flink.table.api.DataTypes.BINARY;
 import static org.apache.flink.table.api.DataTypes.BOOLEAN;
 import static org.apache.flink.table.api.DataTypes.BYTES;
 import static org.apache.flink.table.api.DataTypes.DECIMAL;
 import static org.apache.flink.table.api.DataTypes.DOUBLE;
 import static org.apache.flink.table.api.DataTypes.FIELD;
+import static org.apache.flink.table.api.DataTypes.FLOAT;
 import static org.apache.flink.table.api.DataTypes.INT;
 import static org.apache.flink.table.api.DataTypes.MAP;
 import static org.apache.flink.table.api.DataTypes.ROW;
+import static org.apache.flink.table.api.DataTypes.SMALLINT;
 import static org.apache.flink.table.api.DataTypes.STRING;
 import static org.apache.flink.table.api.DataTypes.TIMESTAMP;
 import static 
org.apache.flink.table.api.DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE;
+import static org.apache.flink.table.api.DataTypes.TINYINT;
 import static org.apache.flink.table.api.DataTypes.VARBINARY;
 import static org.apache.flink.table.api.DataTypes.VARIANT;
 import static org.apache.flink.table.api.Expressions.$;
@@ -482,6 +487,11 @@ class JsonFunctionsITCase extends BuiltInFunctionTestBase {
                         "JSON_VALUE(f0, '$.activated' RETURNING BOOLEAN)",
                         true,
                         BOOLEAN())
+                // string-to-boolean coercion matches CAST 
(BinaryStringDataUtil.toBoolean)
+                .testSqlResult(
+                        "JSON_VALUE('{\"a\": \"y\"}', '$.a' RETURNING 
BOOLEAN)", true, BOOLEAN())
+                .testSqlResult(
+                        "JSON_VALUE('{\"a\": \"N\"}', '$.a' RETURNING 
BOOLEAN)", false, BOOLEAN())
                 .testResult(
                         $("f0").jsonValue("$.age", INT()),
                         "JSON_VALUE(f0, '$.age' RETURNING INT)",
@@ -497,7 +507,36 @@ class JsonFunctionsITCase extends BuiltInFunctionTestBase {
                         "JSON_VALUE(f0, '$.longBalance' RETURNING DOUBLE)",
                         123456789.987654321,
                         DOUBLE())
-
+                .testResult(
+                        $("f0").jsonValue("$.age", TINYINT()),
+                        "JSON_VALUE(f0, '$.age' RETURNING TINYINT)",
+                        (byte) 42,
+                        TINYINT())
+                .testResult(
+                        $("f0").jsonValue("$.age", SMALLINT()),
+                        "JSON_VALUE(f0, '$.age' RETURNING SMALLINT)",
+                        (short) 42,
+                        SMALLINT())
+                .testResult(
+                        $("f0").jsonValue("$.age", BIGINT()),
+                        "JSON_VALUE(f0, '$.age' RETURNING BIGINT)",
+                        42L,
+                        BIGINT())
+                .testResult(
+                        $("f0").jsonValue("$.bigCount", BIGINT()),
+                        "JSON_VALUE(f0, '$.bigCount' RETURNING BIGINT)",
+                        9999999999L,
+                        BIGINT())
+                .testResult(
+                        $("f0").jsonValue("$.balance", FLOAT()),
+                        "JSON_VALUE(f0, '$.balance' RETURNING FLOAT)",
+                        13.37f,
+                        FLOAT())
+                .testResult(
+                        $("f0").jsonValue("$.balance", DECIMAL(10, 2)),
+                        "JSON_VALUE(f0, '$.balance' RETURNING DECIMAL(10, 2))",
+                        new BigDecimal("13.37"),
+                        DECIMAL(10, 2))
                 // ON EMPTY / ON ERROR
                 .testResult(
                         $("f0").jsonValue(
@@ -522,6 +561,16 @@ class JsonFunctionsITCase extends BuiltInFunctionTestBase {
                         "JSON_VALUE(f0, 'lax $.invalid' RETURNING INTEGER 
DEFAULT 42 ON EMPTY ERROR ON ERROR)",
                         42,
                         INT())
+                // A DECIMAL-typed default into a non-DECIMAL RETURNING type 
must be converted,
+                // not dropped to NULL
+                .testSqlResult(
+                        "JSON_VALUE('{\"a\": 1}', 'lax $.missing' RETURNING 
INTEGER DEFAULT 1.5 ON EMPTY)",
+                        1,
+                        INT())
+                .testSqlResult(
+                        "JSON_VALUE('{\"a\": 1}', 'lax $.missing' RETURNING 
BIGINT DEFAULT 1.5 ON EMPTY)",
+                        1L,
+                        BIGINT())
                 .testResult(
                         $("f0").jsonValue(
                                         "strict $.invalid",
@@ -545,7 +594,149 @@ class JsonFunctionsITCase extends BuiltInFunctionTestBase 
{
                         "JSON_VALUE(f0, 'strict $.invalid' RETURNING INTEGER 
NULL ON EMPTY DEFAULT 42 ON ERROR)",
                         42,
                         INT())
+                // JSON null at a valid path triggers ON EMPTY (default: NULL)
+                .testResult(
+                        $("f0").jsonValue("$.nullField", INT()),
+                        "JSON_VALUE(f0, '$.nullField' RETURNING INTEGER)",
+                        null,
+                        INT())
+
+                // Type mismatch: string value cast to numeric triggers ON 
ERROR
+                .testResult(
+                        $("f0").jsonValue(
+                                        "$.type",
+                                        INT(),
+                                        JsonValueOnEmptyOrError.NULL,
+                                        null,
+                                        JsonValueOnEmptyOrError.DEFAULT,
+                                        42),
+                        "JSON_VALUE(f0, '$.type' RETURNING INTEGER DEFAULT 42 
ON ERROR)",
+                        42,
+                        INT())
+                .testSqlRuntimeError(
+                        "JSON_VALUE(f0, '$.type' RETURNING INTEGER ERROR ON 
ERROR)",
+                        TableRuntimeException.class,
+                        "Cannot cast")
+
+                // Numeric overflow triggers ON ERROR (not silent wrapping)
+                .testResult(
+                        $("f0").jsonValue("$.bigCount", INT()),
+                        "JSON_VALUE(f0, '$.bigCount' RETURNING INTEGER)",
+                        null,
+                        INT())
+                // Fractional truncation toward zero (13.89 -> 13, -13.89 -> 
-13):
+                // MySQL truncates, PostgreSQL errors. We match MySQL behavior
+                // (CAST(JSON_UNQUOTE(JSON_EXTRACT(...)) AS type)).
+                .testResult(
+                        $("f0").jsonValue("$.balance", INT()),
+                        "JSON_VALUE(f0, '$.balance' RETURNING INTEGER)",
+                        13,
+                        INT())
+                // DECIMAL precision overflow triggers ON ERROR
+                .testResult(
+                        $("f0").jsonValue("$.longBalance", DECIMAL(5, 2)),
+                        "JSON_VALUE(f0, '$.longBalance' RETURNING DECIMAL(5, 
2))",
+                        null,
+                        DECIMAL(5, 2))
+                // String-literal DEFAULT with numeric RETURNING type (codegen 
compile regression)
+                .testSqlResult(
+                        "JSON_VALUE(f0, '$.longBalance' RETURNING DECIMAL(5, 
2) DEFAULT '0.00' ON ERROR)",
+                        new BigDecimal("0.00"),
+                        DECIMAL(5, 2))
+                // Non-numeric JSON value RETURNING DECIMAL must route to ON 
ERROR, not throw
+                .testSqlResult(
+                        "JSON_VALUE('{\"a\": true}', '$.a' RETURNING 
DECIMAL(5, 2))",
+                        null,
+                        DECIMAL(5, 2))
+                .testSqlRuntimeError(
+                        "JSON_VALUE('{\"a\": true}', '$.a' RETURNING 
DECIMAL(5, 2) ERROR ON ERROR)",
+                        TableRuntimeException.class,
+                        "Cannot cast java.lang.Boolean to DECIMAL")
+                // Floating JSON value out of integer range overflows to ON 
ERROR
+                .testSqlResult(
+                        "JSON_VALUE('{\"a\": 1e20}', '$.a' RETURNING 
TINYINT)", null, TINYINT())
+                .testSqlResult(
+                        "JSON_VALUE('{\"a\": 1e20}', '$.a' RETURNING 
SMALLINT)", null, SMALLINT())
+                .testSqlResult("JSON_VALUE('{\"a\": 1e20}', '$.a' RETURNING 
INTEGER)", null, INT())
+                .testSqlResult(
+                        "JSON_VALUE('{\"a\": 1e20}', '$.a' RETURNING BIGINT)", 
null, BIGINT())
+                .testSqlRuntimeError(
+                        "JSON_VALUE('{\"a\": 1e20}', '$.a' RETURNING BIGINT 
ERROR ON ERROR)",
+                        TableRuntimeException.class,
+                        "Cannot cast java.math.BigDecimal to BIGINT")
+
+                // Outer CAST on JSON_VALUE result (boxed-to-primitive codegen)
+                .testSqlResult(
+                        "CAST(JSON_VALUE(f0, '$.balance' RETURNING DOUBLE) AS 
BIGINT)",
+                        13L,
+                        BIGINT())
+                .testSqlResult(
+                        "CAST(JSON_VALUE(f0, '$.age' RETURNING INTEGER) AS 
DOUBLE)", 42.0, DOUBLE())
+                .testSqlResult(
+                        "CAST(JSON_VALUE(f0, '$.age' RETURNING INTEGER) AS 
BIGINT)", 42L, BIGINT())
+                .testSqlResult(
+                        "CAST(JSON_VALUE(f0, '$.balance' RETURNING DOUBLE) AS 
INTEGER)", 13, INT())
+                .testSqlResult(
+                        "CAST(JSON_VALUE(f0, '$.longBalance' RETURNING DOUBLE) 
AS BIGINT)",
+                        123456789L,
+                        BIGINT())
+
+                // RETURNING BOOLEAN used directly as a condition: a null 
condition takes ELSE
+                .testSqlResult(
+                        "IF(JSON_VALUE(f0, '$.activated' RETURNING BOOLEAN), 
1, 2)", 1, INT())
+                .testSqlResult("IF(JSON_VALUE(f0, '$.missing' RETURNING 
BOOLEAN), 1, 2)", 2, INT())
+
+                // A real value equal to the primitive default is kept, a null 
stays null
+                .testSqlResult("JSON_VALUE('{\"a\": -1}', '$.a' RETURNING 
INTEGER)", -1, INT())
+                .testSqlResult(
+                        "JSON_VALUE('{\"a\": false}', '$.a' RETURNING 
BOOLEAN)", false, BOOLEAN())
+                .testSqlResult(
+                        "JSON_VALUE('{\"a\": false}', '$.missing' RETURNING 
BOOLEAN)",
+                        null,
+                        BOOLEAN())
+
+                // Non-boolean value RETURNING BOOLEAN: default NULL ON ERROR 
yields null,
+                // explicit ERROR ON ERROR fails
+                .testSqlResult(
+                        "JSON_VALUE('{\"a\": 1.9}', '$.a' RETURNING BOOLEAN)", 
null, BOOLEAN())
+                .testSqlRuntimeError(
+                        "JSON_VALUE('{\"a\": 1.9}', '$.a' RETURNING BOOLEAN 
ERROR ON ERROR)",
+                        TableRuntimeException.class,
+                        "Cannot cast java.math.BigDecimal to BOOLEAN")
+                .testSqlResult(
+                        "JSON_VALUE('{\"a\": \"maybe\"}', '$.a' RETURNING 
BOOLEAN)",
+                        null,
+                        BOOLEAN())
+                .testSqlRuntimeError(
+                        "JSON_VALUE('{\"a\": \"maybe\"}', '$.a' RETURNING 
BOOLEAN ERROR ON ERROR)",
+                        TableRuntimeException.class,
+                        "Cannot cast java.lang.String to BOOLEAN")
 
+                // Equality on typed RETURNING results from different paths
+                // (distinct objects, same value -- must use value comparison, 
not reference)
+                .testSqlResult(
+                        "JSON_VALUE('{\"a\": 99999, \"b\": 99999}', '$.a' 
RETURNING INT) = JSON_VALUE('{\"a\": 99999, \"b\": 99999}', '$.b' RETURNING 
INT)",
+                        true,
+                        BOOLEAN())
+                .testSqlResult(
+                        "JSON_VALUE('{\"a\": 13.37, \"b\": 13.37}', '$.a' 
RETURNING DOUBLE) = JSON_VALUE('{\"a\": 13.37, \"b\": 13.37}', '$.b' RETURNING 
DOUBLE)",
+                        true,
+                        BOOLEAN())
+                // 200 is outside Short cache (-128..127), so boxed == would 
fail
+                .testSqlResult(
+                        "JSON_VALUE('{\"a\": 200, \"b\": 200}', '$.a' 
RETURNING SMALLINT) = JSON_VALUE('{\"a\": 200, \"b\": 200}', '$.b' RETURNING 
SMALLINT)",
+                        true,
+                        BOOLEAN())
+                // 9999999999 is outside Long cache (-128..127)
+                .testSqlResult(
+                        "JSON_VALUE('{\"a\": 9999999999, \"b\": 9999999999}', 
'$.a' RETURNING BIGINT) = JSON_VALUE('{\"a\": 9999999999, \"b\": 9999999999}', 
'$.b' RETURNING BIGINT)",
+                        true,
+                        BOOLEAN())
+                // Float has no JVM cache; exercises boxed Float equality 
codegen
+                .testSqlResult(
+                        "JSON_VALUE('{\"a\": 13.37, \"b\": 13.37}', '$.a' 
RETURNING FLOAT) = JSON_VALUE('{\"a\": 13.37, \"b\": 13.37}', '$.b' RETURNING 
FLOAT)",
+                        true,
+                        BOOLEAN())
                 // path contains blank characters.
                 .testResult(
                         $("f0").jsonValue(
@@ -726,9 +917,14 @@ class JsonFunctionsITCase extends BuiltInFunctionTestBase {
                                 "JSON_QUERY(f0, '$.b' RETURNING ARRAY<STRING> 
WITH CONDITIONAL WRAPPER)",
                                 new String[] {"1", "2"},
                                 DataTypes.ARRAY(DataTypes.STRING()))
+                        .testResult(
+                                $("f0").jsonQuery("$.b", ARRAY(INT()), 
CONDITIONAL_ARRAY),
+                                "JSON_QUERY(f0, '$.b' RETURNING ARRAY<INTEGER> 
WITH CONDITIONAL WRAPPER)",
+                                new Integer[] {1, 2},
+                                ARRAY(INT()))
                         .testSqlValidationError(
-                                "JSON_QUERY(f0, '$.b' RETURNING ARRAY<INTEGER> 
 WITH CONDITIONAL WRAPPER ERROR ON ERROR)",
-                                " Unsupported array element type 'INTEGER' for 
RETURNING ARRAY in JSON_QUERY()")
+                                "JSON_QUERY(f0, '$.n1' RETURNING INTEGER)",
+                                "for RETURNING in JSON_QUERY().")
                         .testResult(
                                 $("f0").jsonQuery("$.a"),
                                 "JSON_QUERY(f0, '$.a')",
@@ -903,7 +1099,146 @@ class JsonFunctionsITCase extends 
BuiltInFunctionTestBase {
                         .testTableApiRuntimeError(
                                 $("f0").jsonQuery("strict $.err10", 
WITHOUT_ARRAY, NULL, ERROR),
                                 TableRuntimeException.class,
-                                "No results for path"));
+                                "No results for path"),
+
+                // Typed RETURNING ARRAY<T> support
+                TestSetSpec.forFunction(BuiltInFunctionDefinitions.JSON_QUERY)
+                        .onFieldsWithData(
+                                "{\"ints\": [1, 2, 3], \"doubles\": [1.5, 
2.5], \"bools\": [true, false], \"withNull\": [1, null, 3], \"bigints\": [1, 
9999999999]}")
+                        .andDataTypes(STRING())
+                        .testResult(
+                                $("f0").jsonQuery("$.ints", ARRAY(INT())),
+                                "JSON_QUERY(f0, '$.ints' RETURNING 
ARRAY<INT>)",
+                                new Integer[] {1, 2, 3},
+                                ARRAY(INT()))
+                        .testResult(
+                                $("f0").jsonQuery("$.bigints", 
ARRAY(BIGINT())),
+                                "JSON_QUERY(f0, '$.bigints' RETURNING 
ARRAY<BIGINT>)",
+                                new Long[] {1L, 9999999999L},
+                                ARRAY(BIGINT()))
+                        .testResult(
+                                $("f0").jsonQuery("$.doubles", 
ARRAY(DOUBLE())),
+                                "JSON_QUERY(f0, '$.doubles' RETURNING 
ARRAY<DOUBLE>)",
+                                new Double[] {1.5, 2.5},
+                                ARRAY(DOUBLE()))
+                        .testResult(
+                                $("f0").jsonQuery("$.bools", ARRAY(BOOLEAN())),
+                                "JSON_QUERY(f0, '$.bools' RETURNING 
ARRAY<BOOLEAN>)",
+                                new Boolean[] {true, false},
+                                ARRAY(BOOLEAN()))
+                        // nested element is not a BOOLEAN -> ON ERROR (NULL)
+                        .testSqlResult(
+                                "JSON_QUERY('{\"a\": [[1], true]}', '$.a' 
RETURNING ARRAY<BOOLEAN>)",
+                                null,
+                                ARRAY(BOOLEAN()))
+                        .testResult(
+                                $("f0").jsonQuery("$.ints", 
ARRAY(INT().notNull())),
+                                "JSON_QUERY(f0, '$.ints' RETURNING ARRAY<INT 
NOT NULL>)",
+                                new Integer[] {1, 2, 3},
+                                ARRAY(INT().notNull()))
+                        .testSqlValidationError(
+                                "JSON_QUERY(f0, '$.ints' RETURNING 
ARRAY<TIMESTAMP>)",
+                                "Unsupported array element type"),
+
+                // Type mismatch with ON ERROR behavior
+                TestSetSpec.forFunction(BuiltInFunctionDefinitions.JSON_QUERY)
+                        .onFieldsWithData("{\"strings\": [\"a\", \"b\"]}", 
"{\"empty\": []}")
+                        .andDataTypes(STRING(), STRING())
+                        .testResult(
+                                $("f0").jsonQuery(
+                                                "$.strings",
+                                                ARRAY(INT()),
+                                                WITHOUT_ARRAY,
+                                                NULL,
+                                                NULL),
+                                "JSON_QUERY(f0, '$.strings' RETURNING 
ARRAY<INT> NULL ON ERROR)",
+                                null,
+                                ARRAY(INT()))
+                        .testResult(
+                                $("f0").jsonQuery(
+                                                "$.strings",
+                                                ARRAY(INT()),
+                                                WITHOUT_ARRAY,
+                                                NULL,
+                                                EMPTY_ARRAY),
+                                "JSON_QUERY(f0, '$.strings' RETURNING 
ARRAY<INT> EMPTY ARRAY ON ERROR)",
+                                new Integer[] {},
+                                ARRAY(INT()))
+                        .testSqlRuntimeError(
+                                "JSON_QUERY(f0, '$.strings' RETURNING 
ARRAY<INT> ERROR ON ERROR)",
+                                TableRuntimeException.class,
+                                "Cannot parse string 'a' as INTEGER")
+
+                        // Empty JSON array and missing path
+                        .testResult(
+                                $("f1").jsonQuery("$.empty", ARRAY(INT())),
+                                "JSON_QUERY(f1, '$.empty' RETURNING 
ARRAY<INT>)",
+                                new Integer[] {},
+                                ARRAY(INT()))
+                        .testResult(
+                                $("f0").jsonQuery(
+                                                "lax $.missing",
+                                                ARRAY(INT()),
+                                                WITHOUT_ARRAY,
+                                                NULL,
+                                                NULL),
+                                "JSON_QUERY(f0, 'lax $.missing' RETURNING 
ARRAY<INT> NULL ON EMPTY)",
+                                null,
+                                ARRAY(INT()))
+                        .testResult(
+                                $("f0").jsonQuery(
+                                                "lax $.missing",
+                                                ARRAY(INT()),
+                                                WITHOUT_ARRAY,
+                                                EMPTY_ARRAY,
+                                                NULL),
+                                "JSON_QUERY(f0, 'lax $.missing' RETURNING 
ARRAY<INT> EMPTY ARRAY ON EMPTY)",
+                                new Integer[] {},
+                                ARRAY(INT())),
+
+                // ARRAY<BIGINT> near Long.MAX_VALUE boundary
+                TestSetSpec.forFunction(BuiltInFunctionDefinitions.JSON_QUERY)
+                        .onFieldsWithData("{\"big\": [9223372036854775807, 
-9223372036854775808]}")
+                        .andDataTypes(STRING())
+                        .testResult(
+                                $("f0").jsonQuery("$.big", ARRAY(BIGINT())),
+                                "JSON_QUERY(f0, '$.big' RETURNING 
ARRAY<BIGINT>)",
+                                new Long[] {Long.MAX_VALUE, Long.MIN_VALUE},
+                                ARRAY(BIGINT())),
+
+                // Edge cases: quoted number strings, nested arrays
+                TestSetSpec.forFunction(BuiltInFunctionDefinitions.JSON_VALUE)
+                        .onFieldsWithData("{\"v\":\"42\"}", "{\"neg\":-100}")
+                        .andDataTypes(STRING(), STRING())
+                        // Jayway returns String "42" -- string is parsed as 
number (MySQL/PG
+                        // compatible)
+                        .testResult(
+                                $("f0").jsonValue(
+                                                "$.v",
+                                                INT(),
+                                                JsonValueOnEmptyOrError.NULL,
+                                                null,
+                                                JsonValueOnEmptyOrError.NULL,
+                                                null),
+                                "JSON_VALUE(f0, '$.v' RETURNING INTEGER NULL 
ON ERROR)",
+                                42,
+                                INT())
+                        // Negative value within TINYINT range
+                        .testResult(
+                                $("f1").jsonValue("$.neg", TINYINT()),
+                                "JSON_VALUE(f1, '$.neg' RETURNING TINYINT)",
+                                (byte) -100,
+                                TINYINT()),
+
+                // Nested arrays: inner arrays are Lists, not Numbers
+                TestSetSpec.forFunction(BuiltInFunctionDefinitions.JSON_QUERY)
+                        .onFieldsWithData("{\"a\":[[1,2],[3,4]]}")
+                        .andDataTypes(STRING())
+                        .testResult(
+                                $("f0").jsonQuery("$.a", ARRAY(INT()), 
WITHOUT_ARRAY, NULL, NULL),
+                                "JSON_QUERY(f0, '$.a' RETURNING ARRAY<INT> 
NULL ON ERROR)",
+                                null,
+                                ARRAY(INT())));
     }
 
     private static List<TestSetSpec> jsonStringSpec() {
diff --git 
a/flink-table/flink-table-planner/src/test/resources/json/json-value.json 
b/flink-table/flink-table-planner/src/test/resources/json/json-value.json
index ca934d761c9..7a6e6f117b8 100644
--- a/flink-table/flink-table-planner/src/test/resources/json/json-value.json
+++ b/flink-table/flink-table-planner/src/test/resources/json/json-value.json
@@ -8,5 +8,7 @@
   "roles": ["user", "viewer"],
   "balance": 13.37,
   "longBalance": 123456789.987654321,
+  "bigCount": 9999999999,
+  "nullField": null,
   "contains blank": "right"
 }
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/data/binary/BinaryStringDataUtil.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/data/binary/BinaryStringDataUtil.java
index 490dd3324aa..e73160abf73 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/data/binary/BinaryStringDataUtil.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/data/binary/BinaryStringDataUtil.java
@@ -18,7 +18,7 @@
 package org.apache.flink.table.data.binary;
 
 import org.apache.flink.core.memory.MemorySegment;
-import org.apache.flink.table.api.TableException;
+import org.apache.flink.table.api.TableRuntimeException;
 import org.apache.flink.table.data.DecimalData;
 import org.apache.flink.table.data.DecimalDataUtils;
 import org.apache.flink.table.data.StringData;
@@ -155,7 +155,7 @@ public class BinaryStringDataUtil {
     }
 
     /** Parse a {@link StringData} to boolean. */
-    public static boolean toBoolean(BinaryStringData str) throws 
TableException {
+    public static boolean toBoolean(BinaryStringData str) {
         BinaryStringData lowerCase = str.toLowerCase();
         if (TRUE_STRINGS.contains(lowerCase)) {
             return true;
@@ -163,7 +163,7 @@ public class BinaryStringDataUtil {
         if (FALSE_STRINGS.contains(lowerCase)) {
             return false;
         }
-        throw new TableException("Cannot parse '" + str + "' as BOOLEAN.");
+        throw new TableRuntimeException("Cannot cast '" + str + "' as 
BOOLEAN.");
     }
 
     /** Calculate the hash value of the given bytes use {@link MessageDigest}. 
*/
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 b524854b574..b319d519e73 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
@@ -24,9 +24,14 @@ import org.apache.flink.table.api.JsonQueryOnEmptyOrError;
 import org.apache.flink.table.api.JsonQueryWrapper;
 import org.apache.flink.table.api.JsonValueOnEmptyOrError;
 import org.apache.flink.table.api.TableRuntimeException;
+import org.apache.flink.table.data.DecimalData;
+import org.apache.flink.table.data.DecimalDataUtils;
 import org.apache.flink.table.data.GenericArrayData;
 import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.binary.BinaryStringData;
+import org.apache.flink.table.data.binary.BinaryStringDataUtil;
 import org.apache.flink.table.data.binary.StringUtf8Utils;
+import org.apache.flink.table.types.logical.LogicalTypeRoot;
 import org.apache.flink.types.variant.BinaryVariantInternalBuilder;
 import org.apache.flink.types.variant.Variant;
 
@@ -51,9 +56,14 @@ import 
org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind.node.Arra
 import 
org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind.node.JsonNodeFactory;
 import 
org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind.node.ObjectNode;
 
+import javax.annotation.Nullable;
+
 import java.lang.reflect.Array;
+import java.math.BigDecimal;
+import java.math.BigInteger;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -244,7 +254,8 @@ public class SqlJsonUtils {
 
     public enum JsonQueryReturnType {
         STRING,
-        ARRAY
+        ARRAY,
+        RAW_ARRAY
     }
 
     public static Object jsonQuery(
@@ -336,6 +347,9 @@ public class SqlJsonUtils {
                             }
 
                             return new GenericArrayData(arr);
+                        case RAW_ARRAY:
+                            final List<Object> rawList = (List<Object>) value;
+                            return rawList.toArray();
                         default:
                             throw new TableRuntimeException("illegal return 
type");
                     }
@@ -358,6 +372,8 @@ public class SqlJsonUtils {
                 switch (returnType) {
                     case ARRAY:
                         return new GenericArrayData(new StringData[0]);
+                    case RAW_ARRAY:
+                        return new Object[0];
                     case STRING:
                         return "[]";
                     default:
@@ -384,6 +400,8 @@ public class SqlJsonUtils {
                 switch (returnType) {
                     case ARRAY:
                         return new GenericArrayData(new StringData[0]);
+                    case RAW_ARRAY:
+                        return new Object[0];
                     case STRING:
                         return "[]";
                     default:
@@ -838,6 +856,307 @@ public class SqlJsonUtils {
         }
     }
 
+    // --- Type conversion for JSON_VALUE / JSON_QUERY RETURNING ---
+
+    private static final EnumSet<LogicalTypeRoot> 
SUPPORTED_JSON_RETURNING_TYPES =
+            EnumSet.of(
+                    LogicalTypeRoot.BOOLEAN,
+                    LogicalTypeRoot.TINYINT,
+                    LogicalTypeRoot.SMALLINT,
+                    LogicalTypeRoot.INTEGER,
+                    LogicalTypeRoot.BIGINT,
+                    LogicalTypeRoot.FLOAT,
+                    LogicalTypeRoot.DOUBLE,
+                    LogicalTypeRoot.DECIMAL);
+
+    public static boolean isSupportedJsonReturningType(LogicalTypeRoot 
typeRoot) {
+        return SUPPORTED_JSON_RETURNING_TYPES.contains(typeRoot);
+    }
+
+    public static Object convertJsonScalar(
+            Object raw,
+            LogicalTypeRoot typeRoot,
+            int precision,
+            int scale,
+            JsonValueOnEmptyOrError errorBehavior,
+            Object defaultValue) {
+        if (raw == null) {
+            return null;
+        }
+        try {
+            return convertToType(raw, typeRoot, precision, scale);
+        } catch (TableRuntimeException e) {
+            switch (errorBehavior) {
+                case NULL:
+                    return null;
+                case DEFAULT:
+                    return convertDefault(defaultValue, typeRoot, precision, 
scale);
+                case ERROR:
+                    throw new TableRuntimeException(
+                            "Cannot cast " + raw.getClass().getName() + " to " 
+ typeRoot, e);
+                default:
+                    throw new TableRuntimeException(
+                            "Unreachable: unknown error behavior " + 
errorBehavior);
+            }
+        }
+    }
+
+    public static GenericArrayData convertJsonArray(
+            Object rawResult,
+            LogicalTypeRoot elementTypeRoot,
+            int precision,
+            int scale,
+            boolean elementNullable,
+            JsonQueryOnEmptyOrError errorBehavior) {
+        if (rawResult == null) {
+            return null;
+        }
+        try {
+            Object[] rawArr = (Object[]) rawResult;
+            Object[] converted = new Object[rawArr.length];
+            for (int i = 0; i < rawArr.length; i++) {
+                if (rawArr[i] != null) {
+                    converted[i] = convertToType(rawArr[i], elementTypeRoot, 
precision, scale);
+                } else if (!elementNullable) {
+                    throw new TableRuntimeException(
+                            "Null element at index " + i + " in NOT NULL 
array");
+                }
+            }
+            return new GenericArrayData(converted);
+        } catch (TableRuntimeException e) {
+            switch (errorBehavior) {
+                case NULL:
+                    return null;
+                case EMPTY_ARRAY:
+                    return new GenericArrayData(new Object[0]);
+                case ERROR:
+                    throw new TableRuntimeException(
+                            "JSON_QUERY array element conversion failed", e);
+                default:
+                    return null;
+            }
+        }
+    }
+
+    private static Object convertToType(
+            Object raw, LogicalTypeRoot typeRoot, int precision, int scale) {
+        if (raw instanceof DecimalData) {
+            return convertToType(((DecimalData) raw).toBigDecimal(), typeRoot, 
precision, scale);
+        }
+        if (raw instanceof StringData) {
+            return convertToType(raw.toString(), typeRoot, precision, scale);
+        }
+        if (raw instanceof String) {
+            if (typeRoot == LogicalTypeRoot.BOOLEAN) {
+                return BinaryStringDataUtil.toBoolean(
+                        BinaryStringData.fromString(((String) raw).trim()));
+            }
+            try {
+                String trimmed = ((String) raw).trim();
+                return convertToType(new BigDecimal(trimmed), typeRoot, 
precision, scale);
+            } catch (NumberFormatException e) {
+                throw new TableRuntimeException(
+                        "Cannot parse string '" + raw + "' as " + typeRoot, e);
+            }
+        }
+        switch (typeRoot) {
+            case BOOLEAN:
+                if (raw instanceof Number) {
+                    BigDecimal bd =
+                            raw instanceof BigDecimal
+                                    ? (BigDecimal) raw
+                                    : new BigDecimal(raw.toString());
+                    if (BigDecimal.ZERO.compareTo(bd) == 0) {
+                        return false;
+                    }
+                    if (BigDecimal.ONE.compareTo(bd) == 0) {
+                        return true;
+                    }
+                    throw new TableRuntimeException("Cannot convert " + raw + 
" to BOOLEAN");
+                }
+                if (raw instanceof Boolean) {
+                    return raw;
+                }
+                throw new TableRuntimeException(
+                        "Cannot convert " + raw.getClass().getName() + " to 
BOOLEAN");
+            case TINYINT:
+                return toCheckedByte(asNumber(raw, typeRoot));
+            case SMALLINT:
+                return toCheckedShort(asNumber(raw, typeRoot));
+            case INTEGER:
+                return toCheckedInt(asNumber(raw, typeRoot));
+            case BIGINT:
+                return toCheckedLong(asNumber(raw, typeRoot));
+            case FLOAT:
+                return toCheckedFloat(asNumber(raw, typeRoot));
+            case DOUBLE:
+                return toCheckedDouble(asNumber(raw, typeRoot));
+            case DECIMAL:
+                return toCheckedDecimal(raw, precision, scale);
+            default:
+                throw new TableRuntimeException(
+                        "Unsupported type for JSON conversion: " + typeRoot);
+        }
+    }
+
+    private static Object convertDefault(
+            Object defaultValue, LogicalTypeRoot typeRoot, int precision, int 
scale) {
+        if (defaultValue == null) {
+            return null;
+        }
+        try {
+            return convertToType(defaultValue, typeRoot, precision, scale);
+        } catch (TableRuntimeException e) {
+            throw new TableRuntimeException(
+                    "Default value " + defaultValue + " cannot be represented 
as " + typeRoot, e);
+        }
+    }
+
+    private static @Nullable BigInteger toBigIntegerTruncated(Number n) {
+        if (n instanceof BigDecimal) {
+            final BigDecimal bd = (BigDecimal) n;
+            final int integerDigits = bd.precision() - bd.scale();
+            if (integerDigits <= 0) {
+                return BigInteger.ZERO;
+            }
+            if (integerDigits > 19) {
+                throw new TableRuntimeException("Value " + n + " is out of 
range");
+            }
+            return bd.toBigInteger();
+        }
+        if (n instanceof BigInteger) {
+            return (BigInteger) n;
+        }
+        if (n instanceof Double || n instanceof Float) {
+            double d = n.doubleValue();
+            if (Double.isNaN(d) || Double.isInfinite(d)) {
+                throw new TableRuntimeException("Value " + n + " is out of 
range");
+            }
+            return new BigDecimal(d).toBigInteger();
+        }
+        return null;
+    }
+
+    static byte toCheckedByte(Number n) {
+        BigInteger bi = toBigIntegerTruncated(n);
+        if (bi != null) {
+            if (bi.compareTo(BigInteger.valueOf(Byte.MAX_VALUE)) > 0
+                    || bi.compareTo(BigInteger.valueOf(Byte.MIN_VALUE)) < 0) {
+                throw new TableRuntimeException("Value " + n + " is out of 
range for TINYINT");
+            }
+            return bi.byteValue();
+        }
+        long v = n.longValue();
+        if (v < Byte.MIN_VALUE || v > Byte.MAX_VALUE) {
+            throw new TableRuntimeException("Value " + n + " is out of range 
for TINYINT");
+        }
+        return (byte) v;
+    }
+
+    static short toCheckedShort(Number n) {
+        BigInteger bi = toBigIntegerTruncated(n);
+        if (bi != null) {
+            if (bi.compareTo(BigInteger.valueOf(Short.MAX_VALUE)) > 0
+                    || bi.compareTo(BigInteger.valueOf(Short.MIN_VALUE)) < 0) {
+                throw new TableRuntimeException("Value " + n + " is out of 
range for SMALLINT");
+            }
+            return bi.shortValue();
+        }
+        long v = n.longValue();
+        if (v < Short.MIN_VALUE || v > Short.MAX_VALUE) {
+            throw new TableRuntimeException("Value " + n + " is out of range 
for SMALLINT");
+        }
+        return (short) v;
+    }
+
+    static int toCheckedInt(Number n) {
+        BigInteger bi = toBigIntegerTruncated(n);
+        if (bi != null) {
+            if (bi.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0
+                    || bi.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 
0) {
+                throw new TableRuntimeException("Value " + n + " is out of 
range for INTEGER");
+            }
+            return bi.intValue();
+        }
+        long v = n.longValue();
+        if (v < Integer.MIN_VALUE || v > Integer.MAX_VALUE) {
+            throw new TableRuntimeException("Value " + n + " is out of range 
for INTEGER");
+        }
+        return (int) v;
+    }
+
+    static float toCheckedFloat(Number n) {
+        float f = n.floatValue();
+        if (Float.isInfinite(f) || Float.isNaN(f)) {
+            throw new TableRuntimeException("Value " + n + " is out of range 
for FLOAT");
+        }
+        return f;
+    }
+
+    static double toCheckedDouble(Number n) {
+        double d = n.doubleValue();
+        if (Double.isInfinite(d) || Double.isNaN(d)) {
+            throw new TableRuntimeException("Value " + n + " is out of range 
for DOUBLE");
+        }
+        return d;
+    }
+
+    static long toCheckedLong(Number n) {
+        BigInteger bi = toBigIntegerTruncated(n);
+        if (bi != null) {
+            if (bi.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0
+                    || bi.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) {
+                throw new TableRuntimeException("Value " + n + " is out of 
range for BIGINT");
+            }
+            return bi.longValue();
+        }
+        return n.longValue();
+    }
+
+    private static Number asNumber(Object raw, LogicalTypeRoot typeRoot) {
+        if (!(raw instanceof Number)) {
+            throw new TableRuntimeException(
+                    "Cannot convert " + raw.getClass().getName() + " to " + 
typeRoot);
+        }
+        return (Number) raw;
+    }
+
+    static DecimalData toCheckedDecimal(Object raw, int precision, int scale) {
+        if (!(raw instanceof Number)) {
+            throw new TableRuntimeException(
+                    "Cannot convert " + raw.getClass().getName() + " to 
DECIMAL");
+        }
+        if (raw instanceof BigDecimal) {
+            final BigDecimal bd = (BigDecimal) raw;
+            final long integerDigits = (long) bd.precision() - bd.scale();
+            if (integerDigits <= -(long) scale - 1) {
+                return DecimalData.fromBigDecimal(BigDecimal.ZERO, precision, 
scale);
+            }
+            if (integerDigits > precision - scale) {
+                throw new TableRuntimeException(
+                        "Value "
+                                + raw
+                                + " cannot be represented as DECIMAL("
+                                + precision
+                                + ", "
+                                + scale
+                                + ")");
+            }
+        }
+        DecimalData result = DecimalDataUtils.castFrom(raw.toString(), 
precision, scale);
+        if (result == null) {
+            throw new TableRuntimeException(
+                    "Value "
+                            + raw
+                            + " cannot be represented as DECIMAL("
+                            + precision
+                            + ", "
+                            + scale
+                            + ")");
+        }
+        return result;
+    }
+
     public static class JsonValueContext {
         @JsonValue public final Object obj;
         public final Exception exc;
diff --git 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/functions/SqlJsonUtilsConversionTest.java
 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/functions/SqlJsonUtilsConversionTest.java
new file mode 100644
index 00000000000..2ca162680ec
--- /dev/null
+++ 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/functions/SqlJsonUtilsConversionTest.java
@@ -0,0 +1,559 @@
+/*
+ * 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.flink.table.runtime.functions;
+
+import org.apache.flink.table.api.JsonValueOnEmptyOrError;
+import org.apache.flink.table.api.TableRuntimeException;
+import org.apache.flink.table.data.DecimalData;
+import org.apache.flink.table.data.GenericArrayData;
+import org.apache.flink.table.types.logical.LogicalTypeRoot;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.stream.Stream;
+
+import static org.apache.flink.table.api.JsonQueryOnEmptyOrError.EMPTY_ARRAY;
+import static org.apache.flink.table.api.JsonQueryOnEmptyOrError.ERROR;
+import static org.apache.flink.table.api.JsonQueryOnEmptyOrError.NULL;
+import static org.apache.flink.table.types.logical.LogicalTypeRoot.BIGINT;
+import static org.apache.flink.table.types.logical.LogicalTypeRoot.BOOLEAN;
+import static org.apache.flink.table.types.logical.LogicalTypeRoot.DECIMAL;
+import static org.apache.flink.table.types.logical.LogicalTypeRoot.DOUBLE;
+import static org.apache.flink.table.types.logical.LogicalTypeRoot.FLOAT;
+import static org.apache.flink.table.types.logical.LogicalTypeRoot.INTEGER;
+import static org.apache.flink.table.types.logical.LogicalTypeRoot.SMALLINT;
+import static org.apache.flink.table.types.logical.LogicalTypeRoot.TINYINT;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class SqlJsonUtilsConversionTest {
+
+    private static final BigInteger HUGE = new 
BigInteger("18446744073709551616");
+
+    private static Object scalar(Object raw, LogicalTypeRoot type) {
+        return SqlJsonUtils.convertJsonScalar(raw, type, 0, 0, 
JsonValueOnEmptyOrError.NULL, null);
+    }
+
+    private static Object scalar(
+            Object raw,
+            LogicalTypeRoot type,
+            JsonValueOnEmptyOrError onError,
+            Object defaultValue) {
+        return SqlJsonUtils.convertJsonScalar(raw, type, 0, 0, onError, 
defaultValue);
+    }
+
+    private static Object scalarDecimal(Object raw, int precision, int scale) {
+        return SqlJsonUtils.convertJsonScalar(
+                raw, DECIMAL, precision, scale, JsonValueOnEmptyOrError.NULL, 
null);
+    }
+
+    private static GenericArrayData array(Object[] raw, LogicalTypeRoot 
elementType) {
+        return SqlJsonUtils.convertJsonArray(raw, elementType, 0, 0, true, 
NULL);
+    }
+
+    private static GenericArrayData arrayDecimal(
+            Object[] raw,
+            int precision,
+            int scale,
+            org.apache.flink.table.api.JsonQueryOnEmptyOrError onError) {
+        return SqlJsonUtils.convertJsonArray(raw, DECIMAL, precision, scale, 
true, onError);
+    }
+
+    // ------------------------------------------------------------------------
+    //  Numeric range conversion
+    // ------------------------------------------------------------------------
+
+    static Stream<Arguments> numericValueConversions() {
+        return Stream.of(
+                Arguments.of(42, TINYINT, (byte) 42),
+                Arguments.of(127L, TINYINT, Byte.MAX_VALUE),
+                Arguments.of(-128L, TINYINT, Byte.MIN_VALUE),
+                Arguments.of(1000, SMALLINT, (short) 1000),
+                Arguments.of((long) Short.MAX_VALUE, SMALLINT, 
Short.MAX_VALUE),
+                Arguments.of(42L, INTEGER, 42),
+                Arguments.of((long) Integer.MAX_VALUE, INTEGER, 
Integer.MAX_VALUE),
+                Arguments.of(42, BIGINT, 42L),
+                Arguments.of(9_999_999_999L, BIGINT, 9_999_999_999L),
+                Arguments.of(new BigDecimal("9223372036854775807.5"), BIGINT, 
Long.MAX_VALUE),
+                Arguments.of(13.37, FLOAT, 13.37f),
+                Arguments.of(13.37, DOUBLE, 13.37),
+                // fraction truncation
+                Arguments.of(13.37, INTEGER, 13),
+                Arguments.of(-13.99, INTEGER, -13),
+                Arguments.of(new BigDecimal("9999999999.5"), BIGINT, 
9_999_999_999L),
+                Arguments.of(new BigDecimal("-42.9"), BIGINT, -42L),
+                // silent precision loss when the target type cannot hold the 
value exactly
+                Arguments.of(16777217, FLOAT, 16777216.0f),
+                Arguments.of(9007199254740993L, DOUBLE, 9.007199254740992E15));
+    }
+
+    static Stream<Arguments> numericOverflowCases() {
+        return Stream.of(
+                Arguments.of(128, TINYINT),
+                Arguments.of(-129, TINYINT),
+                Arguments.of(200L, TINYINT),
+                Arguments.of(-200L, TINYINT),
+                Arguments.of(HUGE, TINYINT),
+                Arguments.of(HUGE.negate(), TINYINT),
+                Arguments.of(new BigDecimal("18446744073709551616"), TINYINT),
+                Arguments.of((long) Short.MAX_VALUE + 1, SMALLINT),
+                Arguments.of((long) Short.MIN_VALUE - 1, SMALLINT),
+                Arguments.of(40_000L, SMALLINT),
+                Arguments.of(-40_000L, SMALLINT),
+                Arguments.of(HUGE, SMALLINT),
+                Arguments.of(HUGE.negate(), SMALLINT),
+                Arguments.of(new BigDecimal("18446744073709551616"), SMALLINT),
+                Arguments.of(9_999_999_999L, INTEGER),
+                Arguments.of(-9_999_999_999L, INTEGER),
+                Arguments.of(HUGE, INTEGER),
+                Arguments.of(HUGE.negate(), INTEGER),
+                Arguments.of(new BigDecimal("18446744073709551616"), INTEGER),
+                Arguments.of(HUGE, BIGINT),
+                Arguments.of(HUGE.negate(), BIGINT),
+                Arguments.of(new BigDecimal("18446744073709551616"), BIGINT),
+                
Arguments.of(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE), BIGINT),
+                
Arguments.of(BigInteger.valueOf(Long.MIN_VALUE).subtract(BigInteger.ONE), 
BIGINT),
+                Arguments.of(new 
BigDecimal(Long.MAX_VALUE).add(BigDecimal.ONE), BIGINT),
+                Arguments.of(1e40, FLOAT),
+                Arguments.of(-1e40, FLOAT),
+                Arguments.of(new BigDecimal("1E309"), DOUBLE),
+                Arguments.of(new BigDecimal("-1E309"), DOUBLE),
+                // Double/Float inputs to integer types must range-check, not 
saturate via
+                // Number.xxxValue() ((long) 1e20 -> Long.MAX_VALUE, (long) 
NaN -> 0)
+                Arguments.of(1e20, TINYINT),
+                Arguments.of(1e20, SMALLINT),
+                Arguments.of(1e20, INTEGER),
+                Arguments.of(1e20, BIGINT),
+                Arguments.of(1e20f, BIGINT),
+                Arguments.of(Double.NaN, BIGINT),
+                Arguments.of(Double.POSITIVE_INFINITY, BIGINT),
+                // huge exponent: reject before toBigInteger() materializes 
10^999999999
+                Arguments.of(new BigDecimal("1E999999999"), INTEGER),
+                Arguments.of(new BigDecimal("1E999999999"), BIGINT));
+    }
+
+    @Nested
+    class NumericRangeConversion {
+
+        @ParameterizedTest(name = "{0} -> {1}")
+        @MethodSource(
+                
"org.apache.flink.table.runtime.functions.SqlJsonUtilsConversionTest#numericValueConversions")
+        void convertsToExpectedValue(Number input, LogicalTypeRoot type, 
Object expected) {
+            assertThat(scalar(input, type)).isEqualTo(expected);
+        }
+
+        @ParameterizedTest(name = "{0} -> {1}")
+        @MethodSource(
+                
"org.apache.flink.table.runtime.functions.SqlJsonUtilsConversionTest#numericOverflowCases")
+        void overflowReturnsNull(Number input, LogicalTypeRoot type) {
+            assertThat(scalar(input, type)).isNull();
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    //  Decimal conversion
+    // ------------------------------------------------------------------------
+
+    static Stream<Arguments> decimalConversions() {
+        return Stream.of(
+                Arguments.of(13.37, 10, 2, "13.37"),
+                Arguments.of(
+                        "99999999999999999999999999999999999999",
+                        38,
+                        0,
+                        "99999999999999999999999999999999999999"),
+                Arguments.of("123.456", 5, 2, "123.46"),
+                // huge positive scale: round to zero before setScale() 
materializes 10^999999999
+                Arguments.of(new BigDecimal("1E-999999999"), 10, 2, "0"));
+    }
+
+    static Stream<Arguments> decimalOverflowCases() {
+        return Stream.of(
+                Arguments.of(123456789.99, 5, 2),
+                Arguments.of("999999999999999999999999999999999999999", 38, 0),
+                // 999.999 rounds to 1000.00 (precision 6) which overflows 
DECIMAL(5,2)
+                Arguments.of("999.999", 5, 2),
+                // huge exponent: reject before setScale() materializes 
10^999999999
+                Arguments.of(new BigDecimal("1E999999999"), 10, 2));
+    }
+
+    @Nested
+    class DecimalConversion {
+
+        @ParameterizedTest(name = "{0} -> DECIMAL({1},{2})")
+        @MethodSource(
+                
"org.apache.flink.table.runtime.functions.SqlJsonUtilsConversionTest#decimalConversions")
+        void convertsToDecimal(Object raw, int precision, int scale, String 
expected) {
+            Object result = scalarDecimal(raw, precision, scale);
+            assertThat(result).isInstanceOf(DecimalData.class);
+            assertThat(((DecimalData) result).toBigDecimal())
+                    .isEqualByComparingTo(new BigDecimal(expected));
+        }
+
+        @ParameterizedTest(name = "{0} -> DECIMAL({1},{2})")
+        @MethodSource(
+                
"org.apache.flink.table.runtime.functions.SqlJsonUtilsConversionTest#decimalOverflowCases")
+        void overflowReturnsNull(Object raw, int precision, int scale) {
+            assertThat(scalarDecimal(raw, precision, scale)).isNull();
+        }
+
+        @Test
+        void scientificNotation() {
+            Object result = scalarDecimal("1.5E1", 10, 2);
+            assertThat(result).isInstanceOf(DecimalData.class);
+            assertThat(((DecimalData) result).toBigDecimal())
+                    .isEqualByComparingTo(new BigDecimal("15.00"));
+        }
+
+        @Test
+        void precisionOverflowWithStringDefault() {
+            Object result =
+                    SqlJsonUtils.convertJsonScalar(
+                            123456789.99, DECIMAL, 5, 2, 
JsonValueOnEmptyOrError.DEFAULT, "0.00");
+            assertThat(result).isInstanceOf(DecimalData.class);
+            assertThat(((DecimalData) result).toBigDecimal())
+                    .isEqualByComparingTo(new BigDecimal("0.00"));
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    //  Boolean conversion
+    // ------------------------------------------------------------------------
+
+    static Stream<Arguments> booleanConversions() {
+        return Stream.of(
+                Arguments.of(0, false),
+                Arguments.of(1, true),
+                Arguments.of(0L, false),
+                Arguments.of(1L, true),
+                Arguments.of(0.0, false),
+                Arguments.of(1.0, true),
+                Arguments.of("true", true),
+                Arguments.of("false", false),
+                Arguments.of("TRUE", true),
+                Arguments.of("t", true),
+                Arguments.of("f", false),
+                Arguments.of("yes", true),
+                Arguments.of("no", false),
+                Arguments.of("y", true),
+                Arguments.of("n", false),
+                Arguments.of("Y", true),
+                Arguments.of("N", false),
+                Arguments.of("1", true),
+                Arguments.of("0", false),
+                // leading/trailing whitespace is trimmed before matching
+                Arguments.of(" true ", true),
+                Arguments.of(" FALSE ", false));
+    }
+
+    static Stream<Arguments> nonBooleanInputs() {
+        return Stream.of(
+                Arguments.of(42, "Cannot convert 42 to BOOLEAN"),
+                Arguments.of(-1, "Cannot convert -1 to BOOLEAN"),
+                Arguments.of(2, "Cannot convert 2 to BOOLEAN"),
+                Arguments.of(1.9, "Cannot convert 1.9 to BOOLEAN"),
+                Arguments.of(0.5, "Cannot convert 0.5 to BOOLEAN"),
+                Arguments.of(-0.1, "Cannot convert -0.1 to BOOLEAN"),
+                // exact BigDecimal comparison rejects values that 
doubleValue() would round to 0/1
+                Arguments.of(
+                        new BigDecimal("1.00000000000000000001"),
+                        "Cannot convert 1.00000000000000000001 to BOOLEAN"),
+                Arguments.of(
+                        new BigDecimal("0.99999999999999999999"),
+                        "Cannot convert 0.99999999999999999999 to BOOLEAN"),
+                Arguments.of(new BigDecimal("1E-400"), "Cannot convert 1E-400 
to BOOLEAN"),
+                Arguments.of("maybe", "Cannot cast 'maybe' as BOOLEAN."),
+                Arguments.of("2", "Cannot cast '2' as BOOLEAN."));
+    }
+
+    @Nested
+    class BooleanConversion {
+
+        @ParameterizedTest(name = "{0} -> {1}")
+        @MethodSource(
+                
"org.apache.flink.table.runtime.functions.SqlJsonUtilsConversionTest#booleanConversions")
+        void convertsToBoolean(Object raw, Boolean expected) {
+            assertThat(scalar(raw, BOOLEAN)).isEqualTo(expected);
+        }
+
+        @ParameterizedTest(name = "{0}")
+        @MethodSource(
+                
"org.apache.flink.table.runtime.functions.SqlJsonUtilsConversionTest#nonBooleanInputs")
+        void failsForNonBoolean(Object raw, String causeMessage) {
+            assertThatThrownBy(() -> scalar(raw, BOOLEAN, 
JsonValueOnEmptyOrError.ERROR, null))
+                    .isInstanceOf(TableRuntimeException.class)
+                    .hasMessage("Cannot cast " + raw.getClass().getName() + " 
to BOOLEAN")
+                    .cause()
+                    .hasMessage(causeMessage);
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    //  String input parsing
+    // ------------------------------------------------------------------------
+
+    static Stream<Arguments> stringConversions() {
+        return Stream.of(
+                Arguments.of("42", INTEGER, 42),
+                Arguments.of("13.37", FLOAT, 13.37f),
+                Arguments.of("9999999999", BIGINT, 9_999_999_999L),
+                // CAST trims leading/trailing whitespace (matches PostgreSQL)
+                Arguments.of(" 42 ", INTEGER, 42),
+                Arguments.of(" 13.37 ", DOUBLE, 13.37));
+    }
+
+    static Stream<Arguments> invalidStringInputs() {
+        return Stream.of(
+                Arguments.of("9999999999", INTEGER),
+                Arguments.of("", INTEGER),
+                Arguments.of("NaN", FLOAT),
+                Arguments.of("NaN", DOUBLE),
+                Arguments.of("NaN", INTEGER),
+                Arguments.of("Infinity", FLOAT),
+                Arguments.of("-Infinity", DOUBLE));
+    }
+
+    @Nested
+    class StringInputParsing {
+
+        @ParameterizedTest(name = "{0} -> {1}")
+        @MethodSource(
+                
"org.apache.flink.table.runtime.functions.SqlJsonUtilsConversionTest#stringConversions")
+        void parsesToExpectedValue(String raw, LogicalTypeRoot type, Object 
expected) {
+            assertThat(scalar(raw, type)).isEqualTo(expected);
+        }
+
+        @ParameterizedTest(name = "{0} -> {1}")
+        @MethodSource(
+                
"org.apache.flink.table.runtime.functions.SqlJsonUtilsConversionTest#invalidStringInputs")
+        void returnsNull(String raw, LogicalTypeRoot type) {
+            assertThat(scalar(raw, type)).isNull();
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    //  On-error behavior
+    // ------------------------------------------------------------------------
+
+    static Stream<Arguments> onErrorNullInputs() {
+        return Stream.of(
+                // null input passes straight through
+                Arguments.of(null, INTEGER),
+                // a boolean cannot be converted to a number
+                Arguments.of(Boolean.TRUE, INTEGER));
+    }
+
+    @Nested
+    class OnErrorBehavior {
+
+        @ParameterizedTest(name = "{0} -> {1}")
+        @MethodSource(
+                
"org.apache.flink.table.runtime.functions.SqlJsonUtilsConversionTest#onErrorNullInputs")
+        void returnsNull(Object raw, LogicalTypeRoot type) {
+            assertThat(scalar(raw, type)).isNull();
+        }
+
+        @Test
+        void returnsDefaultForUnparsableString() {
+            assertThat(scalar("notANumber", INTEGER, 
JsonValueOnEmptyOrError.DEFAULT, 42))
+                    .isEqualTo(42);
+        }
+
+        @Test
+        void throwsOnErrorMode() {
+            assertThatThrownBy(
+                            () ->
+                                    scalar(
+                                            9_999_999_999L,
+                                            INTEGER,
+                                            JsonValueOnEmptyOrError.ERROR,
+                                            null))
+                    .isInstanceOf(TableRuntimeException.class)
+                    .hasMessage("Cannot cast java.lang.Long to INTEGER")
+                    .cause()
+                    .hasMessage("Value 9999999999 is out of range for 
INTEGER");
+        }
+
+        @Test
+        void errorModeSurfacesParseCause() {
+            assertThatThrownBy(() -> scalar("abc", INTEGER, 
JsonValueOnEmptyOrError.ERROR, null))
+                    .isInstanceOf(TableRuntimeException.class)
+                    .hasMessage("Cannot cast java.lang.String to INTEGER")
+                    .cause()
+                    .hasMessage("Cannot parse string 'abc' as INTEGER");
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    //  Array conversion
+    // ------------------------------------------------------------------------
+
+    static Stream<Arguments> arrayConversions() {
+        return Stream.of(
+                Arguments.of(new Object[] {1, 2, 3}, INTEGER, new Object[] {1, 
2, 3}),
+                Arguments.of(new Object[] {1, null, 3}, INTEGER, new Object[] 
{1, null, 3}),
+                Arguments.of(new Object[] {true, false}, BOOLEAN, new Object[] 
{true, false}),
+                Arguments.of(new Object[] {0, 1}, BOOLEAN, new Object[] 
{false, true}),
+                Arguments.of(new Object[] {"1", "2", "3"}, INTEGER, new 
Object[] {1, 2, 3}),
+                Arguments.of(new Object[] {"yes", "no"}, BOOLEAN, new Object[] 
{true, false}),
+                Arguments.of(new Object[] {"Y", "n"}, BOOLEAN, new Object[] 
{true, false}),
+                Arguments.of(
+                        new Object[] {1, 9999999999L}, BIGINT, new Object[] 
{1L, 9999999999L}));
+    }
+
+    static Stream<Arguments> arrayNullResultCases() {
+        return Stream.of(
+                Arguments.of(new Object[] {"a", "b"}, INTEGER, 0, 0, true),
+                Arguments.of(new Object[] {1, "notANumber", 3}, INTEGER, 0, 0, 
true),
+                // element 2 is neither 0 nor 1, so conversion fails atomically
+                Arguments.of(new Object[] {1, 2, 3}, BOOLEAN, 0, 0, true),
+                Arguments.of(new Object[] {999999}, DECIMAL, 2, 1, true),
+                // 999.999 rounds to 1000.00 which overflows DECIMAL(5,2); 
whole array fails
+                Arguments.of(new Object[] {1.5, 999.999}, DECIMAL, 5, 2, true),
+                // a null element in a NOT NULL array fails
+                Arguments.of(new Object[] {1, null, 3}, INTEGER, 0, 0, false));
+    }
+
+    static Stream<Arguments> arrayEmptyResultCases() {
+        return Stream.of(
+                Arguments.of(new Object[] {"a", "b"}, INTEGER, 0, 0),
+                Arguments.of(new Object[] {999999}, DECIMAL, 2, 1));
+    }
+
+    static Stream<Arguments> arrayThrowCases() {
+        return Stream.of(
+                Arguments.of(
+                        new Object[] {"a", "b"},
+                        INTEGER,
+                        0,
+                        0,
+                        true,
+                        "Cannot parse string 'a' as INTEGER"),
+                Arguments.of(
+                        new Object[] {999999},
+                        DECIMAL,
+                        2,
+                        1,
+                        true,
+                        "Value 999999 cannot be represented as DECIMAL(2, 1)"),
+                // a null element in a NOT NULL array must surface the real 
cause through the chain
+                Arguments.of(
+                        new Object[] {1, null, 3},
+                        INTEGER,
+                        0,
+                        0,
+                        false,
+                        "Null element at index 1 in NOT NULL array"));
+    }
+
+    static Stream<Arguments> emptyArrayInputs() {
+        return Stream.of(Arguments.of(INTEGER, 0, 0), Arguments.of(DECIMAL, 
10, 2));
+    }
+
+    @Nested
+    class ArrayConversion {
+
+        @ParameterizedTest(name = "{1}")
+        @MethodSource(
+                
"org.apache.flink.table.runtime.functions.SqlJsonUtilsConversionTest#arrayConversions")
+        void convertsArray(Object[] raw, LogicalTypeRoot elementType, Object[] 
expected) {
+            assertThat(array(raw, 
elementType).toObjectArray()).containsExactly(expected);
+        }
+
+        @Test
+        void convertsDecimalArrayWithRounding() {
+            GenericArrayData result = arrayDecimal(new Object[] {1.5, 
123.456}, 5, 2, NULL);
+            assertThat(result).isNotNull();
+            Object[] elements = result.toObjectArray();
+            assertThat(((DecimalData) elements[0]).toBigDecimal())
+                    .isEqualByComparingTo(new BigDecimal("1.50"));
+            assertThat(((DecimalData) elements[1]).toBigDecimal())
+                    .isEqualByComparingTo(new BigDecimal("123.46"));
+        }
+
+        @ParameterizedTest(name = "{1}")
+        @MethodSource(
+                
"org.apache.flink.table.runtime.functions.SqlJsonUtilsConversionTest#arrayNullResultCases")
+        void returnsNullOnErrorMode(
+                Object[] raw,
+                LogicalTypeRoot elementType,
+                int precision,
+                int scale,
+                boolean nullable) {
+            assertThat(
+                            SqlJsonUtils.convertJsonArray(
+                                    raw, elementType, precision, scale, 
nullable, NULL))
+                    .isNull();
+        }
+
+        @ParameterizedTest(name = "{1}")
+        @MethodSource(
+                
"org.apache.flink.table.runtime.functions.SqlJsonUtilsConversionTest#arrayEmptyResultCases")
+        void returnsEmptyArrayOnErrorMode(
+                Object[] raw, LogicalTypeRoot elementType, int precision, int 
scale) {
+            GenericArrayData result =
+                    SqlJsonUtils.convertJsonArray(
+                            raw, elementType, precision, scale, true, 
EMPTY_ARRAY);
+            assertThat(result).isNotNull();
+            assertThat(result.size()).isZero();
+        }
+
+        @ParameterizedTest(name = "{1}")
+        @MethodSource(
+                
"org.apache.flink.table.runtime.functions.SqlJsonUtilsConversionTest#arrayThrowCases")
+        void throwsOnErrorMode(
+                Object[] raw,
+                LogicalTypeRoot elementType,
+                int precision,
+                int scale,
+                boolean nullable,
+                String causeMessage) {
+            assertThatThrownBy(
+                            () ->
+                                    SqlJsonUtils.convertJsonArray(
+                                            raw, elementType, precision, 
scale, nullable, ERROR))
+                    .isInstanceOf(TableRuntimeException.class)
+                    .hasMessage("JSON_QUERY array element conversion failed")
+                    .cause()
+                    .hasMessage(causeMessage);
+        }
+
+        @ParameterizedTest(name = "{0}")
+        @MethodSource(
+                
"org.apache.flink.table.runtime.functions.SqlJsonUtilsConversionTest#emptyArrayInputs")
+        void emptyInputStaysEmpty(LogicalTypeRoot elementType, int precision, 
int scale) {
+            GenericArrayData result =
+                    SqlJsonUtils.convertJsonArray(
+                            new Object[0], elementType, precision, scale, 
true, NULL);
+            assertThat(result).isNotNull();
+            assertThat(result.size()).isZero();
+        }
+
+        @Test
+        void nullInputPassesThrough() {
+            assertThat(SqlJsonUtils.convertJsonArray(null, INTEGER, 0, 0, 
true, NULL)).isNull();
+        }
+    }
+}

Reply via email to