This is an automated email from the ASF dual-hosted git repository.
snuyanzin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git
The following commit(s) were added to refs/heads/master by this push:
new bf4a71a990d [FLINK-40545][table] Add `parseJson()` and
`tryParseJson()` Table API expression methods
bf4a71a990d is described below
commit bf4a71a990dd82225ab33ba331c2f80021b5f1da
Author: Ramin Gharib <[email protected]>
AuthorDate: Sun Sep 6 10:41:44 2026 +0200
[FLINK-40545][table] Add `parseJson()` and `tryParseJson()` Table API
expression methods
---
docs/data/sql_functions.yml | 12 +-
docs/data/sql_functions_zh.yml | 2 +
flink-python/pyflink/table/expression.py | 35 +++++-
.../flink/table/api/internal/BaseExpressions.java | 52 ++++++++
.../planner/functions/CastFunctionITCase.java | 136 ++++++++++-----------
.../planner/functions/JsonFunctionsITCase.java | 86 +++++++++----
6 files changed, 227 insertions(+), 96 deletions(-)
diff --git a/docs/data/sql_functions.yml b/docs/data/sql_functions.yml
index e6ce79e47f7..8dc465da407 100644
--- a/docs/data/sql_functions.yml
+++ b/docs/data/sql_functions.yml
@@ -1338,17 +1338,19 @@ json:
variant:
- sql: PARSE_JSON(json_string[, allow_duplicate_keys])
+ table: STRING.parseJson([allowDuplicateKeys])
description: |
- Parse a JSON string into a Variant. If the JSON string is invalid, an
error will be thrown.
+ Parse a JSON string into a Variant. If the JSON string is invalid, an
error will be thrown.
To return NULL instead of an error, use the `TRY_PARSE_JSON` function.
-
- If there are duplicate keys in the input JSON string, when
`allowDuplicateKeys` is true, the
- parser will keep the last occurrence of all fields with the same key,
otherwise when
- `allowDuplicateKeys` is false it will throw an error. The default value
of
+
+ If there are duplicate keys in the input JSON string, when
`allowDuplicateKeys` is true, the
+ parser will keep the last occurrence of all fields with the same key,
otherwise when
+ `allowDuplicateKeys` is false it will throw an error. The default value
of
`allowDuplicateKeys` is false.
See the `VARIANT` data type documentation for how JSON values are mapped
to variant kinds.
- sql: TRY_PARSE_JSON(json_string[, allow_duplicate_keys])
+ table: STRING.tryParseJson([allowDuplicateKeys])
description: |
Try to parse a JSON string into a Variant if possible. If the JSON
string is invalid, return
NULL. To throw an error instead of returning NULL, use the `PARSE_JSON`
function.
diff --git a/docs/data/sql_functions_zh.yml b/docs/data/sql_functions_zh.yml
index b5819bbede2..85e28b198c3 100644
--- a/docs/data/sql_functions_zh.yml
+++ b/docs/data/sql_functions_zh.yml
@@ -1423,6 +1423,7 @@ json:
variant:
- sql: PARSE_JSON(json_string[, allow_duplicate_keys])
+ table: STRING.parseJson([allowDuplicateKeys])
description: |
将 JSON 字符串解析为 Variant。如果 JSON 字符串无效,将抛出错误。如果希望返回 NULL 而不是抛出错误,
请使用 `TRY_PARSE_JSON` 函数。
@@ -1434,6 +1435,7 @@ variant:
See the `VARIANT` data type documentation for how JSON values are mapped
to variant kinds.
- sql: TRY_PARSE_JSON(json_string[, allow_duplicate_keys])
+ table: STRING.tryParseJson([allowDuplicateKeys])
description: |
尽可能将 JSON 字符串解析为 Variant。如果 JSON 字符串无效,则返回 NULL。如果希望抛出错误而不是返回 NULL,
请使用 `PARSE_JSON` 函数。
diff --git a/flink-python/pyflink/table/expression.py
b/flink-python/pyflink/table/expression.py
index e82e3bc02d2..be21605124a 100644
--- a/flink-python/pyflink/table/expression.py
+++ b/flink-python/pyflink/table/expression.py
@@ -95,7 +95,8 @@ _string_doc_seealso = """
:func:`~Expression.regexp_extract`, :func:`~Expression.substring`,
:py:attr:`~Expression.from_base64`,
:py:attr:`~Expression.to_base64`,
:func:`~Expression.ltrim`, :func:`~Expression.rtrim`,
:func:`~Expression.repeat`,
- :func:`~Expression.json_quote`, :func:`~Expression.json_unquote`
+ :func:`~Expression.json_quote`, :func:`~Expression.json_unquote`,
+ :func:`~Expression.parse_json`, :func:`~Expression.try_parse_json`
"""
_temporal_doc_seealso = """
@@ -194,7 +195,8 @@ def _make_string_doc():
Expression.lpad, Expression.rpad, Expression.overlay,
Expression.regexp_replace,
Expression.regexp_extract, Expression.from_base64,
Expression.to_base64,
Expression.ltrim, Expression.rtrim, Expression.repeat,
- Expression.json_quote, Expression.json_unquote
+ Expression.json_quote, Expression.json_unquote,
+ Expression.parse_json, Expression.try_parse_json
]
for func in string_funcs:
@@ -2280,6 +2282,35 @@ class Expression(Generic[T]):
"""
return _unary_op("jsonUnquote")(self)
+ def parse_json(self, allow_duplicate_keys=None) -> 'Expression':
+ """
+ Parses a JSON string into a value of VARIANT type. If the JSON string
is invalid,
+ an error is thrown. To return None instead of an error, use
+ :func:`~Expression.try_parse_json`.
+
+ If there are duplicate keys in the input, allow_duplicate_keys
controls whether the
+ parser keeps the last occurrence of each duplicated key (True) or
throws an error
+ (False). The default value of allow_duplicate_keys is False.
+ """
+ if allow_duplicate_keys is None:
+ return _unary_op("parseJson")(self)
+ else:
+ return _binary_op("parseJson")(self, allow_duplicate_keys)
+
+ def try_parse_json(self, allow_duplicate_keys=None) -> 'Expression':
+ """
+ Parses a JSON string into a value of VARIANT type. If the JSON string
is invalid,
+ None is returned. To throw an error instead, use
:func:`~Expression.parse_json`.
+
+ If there are duplicate keys in the input, allow_duplicate_keys
controls whether the
+ parser keeps the last occurrence of each duplicated key (True) or
throws an error
+ (False). The default value of allow_duplicate_keys is False.
+ """
+ if allow_duplicate_keys is None:
+ return _unary_op("tryParseJson")(self)
+ else:
+ return _binary_op("tryParseJson")(self, allow_duplicate_keys)
+
def json_length(self, path=None) -> 'Expression':
"""
Returns the number of elements in a JSON document, or the length of
the value at the
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 bd95cb9a73f..536105ea6bc 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
@@ -180,6 +180,7 @@ import static
org.apache.flink.table.functions.BuiltInFunctionDefinitions.ORDER_
import static
org.apache.flink.table.functions.BuiltInFunctionDefinitions.ORDER_DESC;
import static org.apache.flink.table.functions.BuiltInFunctionDefinitions.OVER;
import static
org.apache.flink.table.functions.BuiltInFunctionDefinitions.OVERLAY;
+import static
org.apache.flink.table.functions.BuiltInFunctionDefinitions.PARSE_JSON;
import static
org.apache.flink.table.functions.BuiltInFunctionDefinitions.PARSE_URL;
import static
org.apache.flink.table.functions.BuiltInFunctionDefinitions.PERCENTILE;
import static org.apache.flink.table.functions.BuiltInFunctionDefinitions.PLUS;
@@ -232,6 +233,7 @@ import static
org.apache.flink.table.functions.BuiltInFunctionDefinitions.TRANSL
import static org.apache.flink.table.functions.BuiltInFunctionDefinitions.TRIM;
import static
org.apache.flink.table.functions.BuiltInFunctionDefinitions.TRUNCATE;
import static
org.apache.flink.table.functions.BuiltInFunctionDefinitions.TRY_CAST;
+import static
org.apache.flink.table.functions.BuiltInFunctionDefinitions.TRY_PARSE_JSON;
import static
org.apache.flink.table.functions.BuiltInFunctionDefinitions.UNHEX;
import static
org.apache.flink.table.functions.BuiltInFunctionDefinitions.UPPER;
import static
org.apache.flink.table.functions.BuiltInFunctionDefinitions.URL_DECODE;
@@ -1367,6 +1369,56 @@ public abstract class BaseExpressions<InType, OutType> {
return toApiSpecificExpression(unresolvedCall(JSON_UNQUOTE,
objectToExpression(toExpr())));
}
+ /**
+ * Parses a JSON string into a value of type {@link DataTypes#VARIANT()}.
If the JSON string is
+ * invalid, an error is thrown. To return {@code NULL} instead of an
error, use {@link
+ * #tryParseJson()}.
+ *
+ * <p>This is a shortcut for {@code parseJson(false)}. See {@link
#parseJson(boolean)}.
+ */
+ public OutType parseJson() {
+ return toApiSpecificExpression(unresolvedCall(PARSE_JSON,
objectToExpression(toExpr())));
+ }
+
+ /**
+ * Parses a JSON string into a value of type {@link DataTypes#VARIANT()}.
If the JSON string is
+ * invalid, an error is thrown. To return {@code NULL} instead of an
error, use {@link
+ * #tryParseJson(boolean)}.
+ *
+ * <p>If there are duplicate keys in the input, {@code allowDuplicateKeys}
controls whether the
+ * parser keeps the last occurrence of each duplicated key ({@code true})
or throws an error
+ * ({@code false}).
+ */
+ public OutType parseJson(boolean allowDuplicateKeys) {
+ return toApiSpecificExpression(
+ unresolvedCall(PARSE_JSON, toExpr(),
valueLiteral(allowDuplicateKeys)));
+ }
+
+ /**
+ * Parses a JSON string into a value of type {@link DataTypes#VARIANT()}.
If the JSON string is
+ * invalid, {@code NULL} is returned. To throw an error instead, use
{@link #parseJson()}.
+ *
+ * <p>This is a shortcut for {@code tryParseJson(false)}. See {@link
#tryParseJson(boolean)}.
+ */
+ public OutType tryParseJson() {
+ return toApiSpecificExpression(
+ unresolvedCall(TRY_PARSE_JSON, objectToExpression(toExpr())));
+ }
+
+ /**
+ * Parses a JSON string into a value of type {@link DataTypes#VARIANT()}.
If the JSON string is
+ * invalid, {@code NULL} is returned. To throw an error instead, use {@link
+ * #parseJson(boolean)}.
+ *
+ * <p>If there are duplicate keys in the input, {@code allowDuplicateKeys}
controls whether the
+ * parser keeps the last occurrence of each duplicated key ({@code true})
or throws an error
+ * ({@code false}).
+ */
+ public OutType tryParseJson(boolean allowDuplicateKeys) {
+ return toApiSpecificExpression(
+ unresolvedCall(TRY_PARSE_JSON, toExpr(),
valueLiteral(allowDuplicateKeys)));
+ }
+
/** Returns the base string decoded with base64. */
public OutType fromBase64() {
return toApiSpecificExpression(unresolvedCall(FROM_BASE64, toExpr()));
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 083fda47dff..4c87bcb62bb 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
@@ -79,7 +79,7 @@ import static org.apache.flink.table.api.DataTypes.VARCHAR;
import static org.apache.flink.table.api.DataTypes.VARIANT;
import static org.apache.flink.table.api.DataTypes.YEAR;
import static org.apache.flink.table.api.Expressions.$;
-import static org.apache.flink.table.api.Expressions.call;
+import static org.apache.flink.table.api.Expressions.lit;
import static org.apache.flink.util.CollectionUtil.entry;
import static org.apache.flink.util.CollectionUtil.map;
import static org.assertj.core.api.Assertions.assertThat;
@@ -147,238 +147,238 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
private static List<TestSetSpec> variantPrimitiveCasts() {
return List.of(
- TestSetSpec.forExpression("Cast a VARIANT produced by
PARSE_JSON to a primitive")
+ TestSetSpec.forExpression("Cast a VARIANT produced by
parseJson() to a primitive")
.onFieldsWithData("unused")
.andDataTypes(STRING())
// An integer converts to any integer target while the
value stays in
// range, and to FLOAT or DOUBLE which are approximate
by definition.
.testResult(
- call("PARSE_JSON", "42").cast(TINYINT()),
+ lit("42").parseJson().cast(TINYINT()),
"CAST(PARSE_JSON('42') AS TINYINT)",
(byte) 42,
TINYINT().notNull())
.testResult(
- call("PARSE_JSON", "42").cast(SMALLINT()),
+ lit("42").parseJson().cast(SMALLINT()),
"CAST(PARSE_JSON('42') AS SMALLINT)",
(short) 42,
SMALLINT().notNull())
.testResult(
- call("PARSE_JSON", "42").cast(INT()),
+ lit("42").parseJson().cast(INT()),
"CAST(PARSE_JSON('42') AS INT)",
42,
INT().notNull())
.testResult(
- call("PARSE_JSON", "42").cast(BIGINT()),
+ lit("42").parseJson().cast(BIGINT()),
"CAST(PARSE_JSON('42') AS BIGINT)",
42L,
BIGINT().notNull())
.testResult(
- call("PARSE_JSON", "42").cast(FLOAT()),
+ lit("42").parseJson().cast(FLOAT()),
"CAST(PARSE_JSON('42') AS FLOAT)",
42.0f,
FLOAT().notNull())
.testResult(
- call("PARSE_JSON", "42").cast(DOUBLE()),
+ lit("42").parseJson().cast(DOUBLE()),
"CAST(PARSE_JSON('42') AS DOUBLE)",
42.0d,
DOUBLE().notNull())
// An out-of-range value is rejected rather than
wrapped.
.testResult(
- call("PARSE_JSON", "1000").cast(SMALLINT()),
+ lit("1000").parseJson().cast(SMALLINT()),
"CAST(PARSE_JSON('1000') AS SMALLINT)",
(short) 1000,
SMALLINT().notNull())
.testTableApiRuntimeError(
- call("PARSE_JSON", "1000").cast(TINYINT()),
"overflowed")
+ lit("1000").parseJson().cast(TINYINT()),
"overflowed")
.testSqlRuntimeError("CAST(PARSE_JSON('1000') AS
TINYINT)", "overflowed")
.testResult(
- call("PARSE_JSON", "1000").tryCast(TINYINT()),
+ lit("1000").parseJson().tryCast(TINYINT()),
"TRY_CAST(PARSE_JSON('1000') AS TINYINT)",
null,
TINYINT())
// A decimal reaches an integer target when it is
already integral.
.testResult(
- call("PARSE_JSON", "7.0").cast(INT()),
+ lit("7.0").parseJson().cast(INT()),
"CAST(PARSE_JSON('7.0') AS INT)",
7,
INT().notNull())
// A fractional value is rejected, since converting
would drop digits.
.testTableApiRuntimeError(
- call("PARSE_JSON", "123.456").cast(INT()),
"lose precision")
+ lit("123.456").parseJson().cast(INT()), "lose
precision")
.testResult(
- call("PARSE_JSON", "123.456").tryCast(INT()),
+ lit("123.456").parseJson().tryCast(INT()),
"TRY_CAST(PARSE_JSON('123.456') AS INT)",
null,
INT())
// A DECIMAL target has to hold the value exactly.
.testResult(
- call("PARSE_JSON", "123.456").cast(DECIMAL(6,
3)),
+ lit("123.456").parseJson().cast(DECIMAL(6, 3)),
"CAST(PARSE_JSON('123.456') AS DECIMAL(6, 3))",
new BigDecimal("123.456"),
DECIMAL(6, 3).notNull())
.testTableApiRuntimeError(
- call("PARSE_JSON", "123.456").cast(DECIMAL(6,
2)), "lose precision")
+ lit("123.456").parseJson().cast(DECIMAL(6,
2)), "lose precision")
.testResult(
- call("PARSE_JSON",
"123.456").tryCast(DECIMAL(6, 2)),
+ lit("123.456").parseJson().tryCast(DECIMAL(6,
2)),
"TRY_CAST(PARSE_JSON('123.456') AS DECIMAL(6,
2))",
null,
DECIMAL(6, 2))
.testTableApiRuntimeError(
- call("PARSE_JSON", "123.456").cast(DECIMAL(5,
3)), "overflowed")
+ lit("123.456").parseJson().cast(DECIMAL(5,
3)), "overflowed")
.testResult(
- call("PARSE_JSON",
"123.456").tryCast(DECIMAL(5, 3)),
+ lit("123.456").parseJson().tryCast(DECIMAL(5,
3)),
"TRY_CAST(PARSE_JSON('123.456') AS DECIMAL(5,
3))",
null,
DECIMAL(5, 3))
// An integer is exact, so it reaches a DECIMAL that
has room for it.
.testResult(
- call("PARSE_JSON", "42").cast(DECIMAL(5, 2)),
+ lit("42").parseJson().cast(DECIMAL(5, 2)),
"CAST(PARSE_JSON('42') AS DECIMAL(5, 2))",
new BigDecimal("42.00"),
DECIMAL(5, 2).notNull())
// A decimal reaches an approximate target, where
losing digits is expected.
.testResult(
- call("PARSE_JSON", "123.456").cast(FLOAT()),
+ lit("123.456").parseJson().cast(FLOAT()),
"CAST(PARSE_JSON('123.456') AS FLOAT)",
123.456f,
FLOAT().notNull())
.testResult(
- call("PARSE_JSON", "123.456").cast(DOUBLE()),
+ lit("123.456").parseJson().cast(DOUBLE()),
"CAST(PARSE_JSON('123.456') AS DOUBLE)",
123.456d,
DOUBLE().notNull())
// A magnitude the target cannot represent is still
rejected.
.testTableApiRuntimeError(
- call("PARSE_JSON", "1e40").cast(FLOAT()),
"overflowed")
+ lit("1e40").parseJson().cast(FLOAT()),
"overflowed")
.testResult(
- call("PARSE_JSON", "1e40").tryCast(FLOAT()),
+ lit("1e40").parseJson().tryCast(FLOAT()),
"TRY_CAST(PARSE_JSON('1e40') AS FLOAT)",
null,
FLOAT())
.testResult(
- call("PARSE_JSON", "1e20").cast(DOUBLE()),
+ lit("1e20").parseJson().cast(DOUBLE()),
"CAST(PARSE_JSON('1e20') AS DOUBLE)",
1e20,
DOUBLE().notNull())
.testResult(
- call("PARSE_JSON", "true").cast(BOOLEAN()),
+ lit("true").parseJson().cast(BOOLEAN()),
"CAST(PARSE_JSON('true') AS BOOLEAN)",
true,
BOOLEAN().notNull())
// CAST returns the raw scalar value (string unquoted)
.testResult(
- call("PARSE_JSON", "\"foo\"").cast(STRING()),
+ lit("\"foo\"").parseJson().cast(STRING()),
"CAST(PARSE_JSON('\"foo\"') AS STRING)",
"foo",
STRING().notNull())
.testResult(
- call("PARSE_JSON", "123.456").cast(STRING()),
+ lit("123.456").parseJson().cast(STRING()),
"CAST(PARSE_JSON('123.456') AS STRING)",
"123.456",
STRING().notNull())
// The rendering matches a regular cast of the stored
kind, so a boolean
// becomes TRUE rather than the JSON true.
.testResult(
- call("PARSE_JSON", "true").cast(STRING()),
+ lit("true").parseJson().cast(STRING()),
"CAST(PARSE_JSON('true') AS STRING)",
"TRUE",
STRING().notNull())
// An object or array has no scalar value, so the
error points to
// JSON_STRING.
.testTableApiRuntimeError(
- call("PARSE_JSON", "[\"a\",
\"b\"]").cast(STRING()), "JSON_STRING")
+ lit("[\"a\",
\"b\"]").parseJson().cast(STRING()), "JSON_STRING")
.testResult(
- call("PARSE_JSON", "[\"a\",
\"b\"]").tryCast(STRING()),
+ lit("[\"a\",
\"b\"]").parseJson().tryCast(STRING()),
"TRY_CAST(PARSE_JSON('[\"a\", \"b\"]') AS
STRING)",
null,
STRING())
.testTableApiRuntimeError(
- call("PARSE_JSON", "{\"a\":
1}").cast(STRING()), "JSON_STRING")
+ lit("{\"a\": 1}").parseJson().cast(STRING()),
"JSON_STRING")
.testResult(
- call("PARSE_JSON", "{\"a\":
1}").tryCast(STRING()),
+ lit("{\"a\":
1}").parseJson().tryCast(STRING()),
"TRY_CAST(PARSE_JSON('{\"a\": 1}') AS STRING)",
null,
STRING())
// A bounded CHAR/VARCHAR target trims a longer value,
and CHAR pads a
// shorter one to its fixed width, the same as a
regular cast into it.
.testResult(
- call("PARSE_JSON", "\"ab\"").cast(VARCHAR(3)),
+ lit("\"ab\"").parseJson().cast(VARCHAR(3)),
"CAST(PARSE_JSON('\"ab\"') AS VARCHAR(3))",
"ab",
VARCHAR(3).notNull())
.testResult(
- call("PARSE_JSON",
"\"foobar\"").cast(VARCHAR(3)),
+ lit("\"foobar\"").parseJson().cast(VARCHAR(3)),
"CAST(PARSE_JSON('\"foobar\"') AS VARCHAR(3))",
"foo",
VARCHAR(3).notNull())
.testResult(
- call("PARSE_JSON",
"\"foobar\"").tryCast(VARCHAR(3)),
+
lit("\"foobar\"").parseJson().tryCast(VARCHAR(3)),
"TRY_CAST(PARSE_JSON('\"foobar\"') AS
VARCHAR(3))",
"foo",
VARCHAR(3))
.testResult(
- call("PARSE_JSON", "\"abc\"").cast(CHAR(3)),
+ lit("\"abc\"").parseJson().cast(CHAR(3)),
"CAST(PARSE_JSON('\"abc\"') AS CHAR(3))",
"abc",
CHAR(3).notNull())
.testResult(
- call("PARSE_JSON", "\"abcdef\"").cast(CHAR(3)),
+ lit("\"abcdef\"").parseJson().cast(CHAR(3)),
"CAST(PARSE_JSON('\"abcdef\"') AS CHAR(3))",
"abc",
CHAR(3).notNull())
.testResult(
- call("PARSE_JSON", "\"ab\"").cast(CHAR(5)),
+ lit("\"ab\"").parseJson().cast(CHAR(5)),
"CAST(PARSE_JSON('\"ab\"') AS CHAR(5))",
"ab ",
CHAR(5).notNull())
.testResult(
- call("PARSE_JSON", "\"ab\"").tryCast(CHAR(5)),
+ lit("\"ab\"").parseJson().tryCast(CHAR(5)),
"TRY_CAST(PARSE_JSON('\"ab\"') AS CHAR(5))",
"ab ",
CHAR(5))
// A variant holding a JSON null casts to SQL NULL,
not to the text 'null'.
// The length of that text must not be checked against
the target either.
.testResult(
- call("TRY_PARSE_JSON", "null").cast(STRING()),
+ lit("null").tryParseJson().cast(STRING()),
"CAST(TRY_PARSE_JSON('null') AS STRING)",
null,
STRING())
.testResult(
- call("PARSE_JSON", "null").tryCast(STRING()),
+ lit("null").parseJson().tryCast(STRING()),
"TRY_CAST(PARSE_JSON('null') AS STRING)",
null,
STRING())
.testResult(
- call("TRY_PARSE_JSON", "null").cast(CHAR(2)),
+ lit("null").tryParseJson().cast(CHAR(2)),
"CAST(TRY_PARSE_JSON('null') AS CHAR(2))",
null,
CHAR(2))
.testResult(
- call("PARSE_JSON", "null").tryCast(VARCHAR(2)),
+ lit("null").parseJson().tryCast(VARCHAR(2)),
"TRY_CAST(PARSE_JSON('null') AS VARCHAR(2))",
null,
VARCHAR(2))
// TRY_CAST of a value whose kind does not match the
target returns NULL
.testResult(
- call("PARSE_JSON", "\"foo\"").tryCast(INT()),
+ lit("\"foo\"").parseJson().tryCast(INT()),
"TRY_CAST(PARSE_JSON('\"foo\"') AS INT)",
null,
INT())
// A variant that stores a JSON null casts to SQL NULL
when the target is
// nullable: a nullable variant source, or TRY_CAST
which forces nullable.
.testResult(
- call("TRY_PARSE_JSON", "null").cast(INT()),
+ lit("null").tryParseJson().cast(INT()),
"CAST(TRY_PARSE_JSON('null') AS INT)",
null,
INT())
.testResult(
- call("PARSE_JSON", "null").tryCast(BOOLEAN()),
+ lit("null").parseJson().tryCast(BOOLEAN()),
"TRY_CAST(PARSE_JSON('null') AS BOOLEAN)",
null,
BOOLEAN())
// A nullable variant with a concrete value still
casts normally.
.testResult(
- call("TRY_PARSE_JSON", "42").cast(TINYINT()),
+ lit("42").tryParseJson().cast(TINYINT()),
"CAST(TRY_PARSE_JSON('42') AS TINYINT)",
(byte) 42,
TINYINT()));
@@ -386,91 +386,91 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
private static List<TestSetSpec> variantArrayCasts() {
return List.of(
- TestSetSpec.forExpression("Cast a VARIANT produced by
PARSE_JSON to an ARRAY")
+ TestSetSpec.forExpression("Cast a VARIANT produced by
parseJson() to an ARRAY")
.onFieldsWithData("unused")
.andDataTypes(STRING())
// ARRAY: each element casts by the same
VARIANT-to-element rule.
.testResult(
- call("PARSE_JSON", "[1, 2,
3]").cast(ARRAY(INT())),
+ lit("[1, 2,
3]").parseJson().cast(ARRAY(INT())),
"CAST(PARSE_JSON('[1, 2, 3]') AS ARRAY<INT>)",
new Integer[] {1, 2, 3},
ARRAY(INT()).notNull())
// an approximate leaf takes any numeric kind
.testResult(
- call("PARSE_JSON", "[1, 2,
3]").cast(ARRAY(DOUBLE())),
+ lit("[1, 2,
3]").parseJson().cast(ARRAY(DOUBLE())),
"CAST(PARSE_JSON('[1, 2, 3]') AS
ARRAY<DOUBLE>)",
new Double[] {1.0, 2.0, 3.0},
ARRAY(DOUBLE()).notNull())
// each element renders to string like the scalar cast
.testResult(
- call("PARSE_JSON", "[1, 2,
3]").cast(ARRAY(STRING())),
+ lit("[1, 2,
3]").parseJson().cast(ARRAY(STRING())),
"CAST(PARSE_JSON('[1, 2, 3]') AS
ARRAY<STRING>)",
new String[] {"1", "2", "3"},
ARRAY(STRING()).notNull())
// a heterogeneous array renders every element to
string
.testResult(
- call("PARSE_JSON", "[1, \"a\", 2,
\"b\"]").cast(ARRAY(STRING())),
+ lit("[1, \"a\", 2,
\"b\"]").parseJson().cast(ARRAY(STRING())),
"CAST(PARSE_JSON('[1, \"a\", 2, \"b\"]') AS
ARRAY<STRING>)",
new String[] {"1", "a", "2", "b"},
ARRAY(STRING()).notNull())
.testResult(
- call("PARSE_JSON", "[]").cast(ARRAY(INT())),
+ lit("[]").parseJson().cast(ARRAY(INT())),
"CAST(PARSE_JSON('[]') AS ARRAY<INT>)",
new Integer[] {},
ARRAY(INT()).notNull())
// a VARIANT null element maps to SQL NULL for a
nullable element type
.testResult(
- call("PARSE_JSON", "[1, null,
3]").cast(ARRAY(INT())),
+ lit("[1, null,
3]").parseJson().cast(ARRAY(INT())),
"CAST(PARSE_JSON('[1, null, 3]') AS
ARRAY<INT>)",
new Integer[] {1, null, 3},
ARRAY(INT()).notNull())
// a VARIANT null element fails a NOT NULL element type
.testTableApiRuntimeError(
- call("PARSE_JSON", "[1, null,
3]").cast(ARRAY(INT().notNull())),
+ lit("[1, null,
3]").parseJson().cast(ARRAY(INT().notNull())),
"NOT NULL element type")
.testSqlRuntimeError(
"CAST(PARSE_JSON('[1, null, 3]') AS ARRAY<INT
NOT NULL>)",
"NOT NULL element type")
// a stored string is never parsed into an integer
.testTableApiRuntimeError(
- call("PARSE_JSON", "[\"1\",
\"2\"]").cast(ARRAY(INT())),
+ lit("[\"1\",
\"2\"]").parseJson().cast(ARRAY(INT())),
"does not change the type")
.testSqlRuntimeError(
"CAST(PARSE_JSON('[\"1\", \"2\"]') AS
ARRAY<INT>)",
"does not change the type")
.testResult(
- call("PARSE_JSON", "[\"1\",
\"2\"]").tryCast(ARRAY(INT())),
+ lit("[\"1\",
\"2\"]").parseJson().tryCast(ARRAY(INT())),
"TRY_CAST(PARSE_JSON('[\"1\", \"2\"]') AS
ARRAY<INT>)",
null,
ARRAY(INT()))
// a heterogeneous array fails on the first element
that is not an integer
.testTableApiRuntimeError(
- call("PARSE_JSON", "[1, \"a\", 2,
\"b\"]").cast(ARRAY(INT())),
+ lit("[1, \"a\", 2,
\"b\"]").parseJson().cast(ARRAY(INT())),
"does not change the type")
// a fractional element cannot narrow to INT without
dropping digits
.testTableApiRuntimeError(
- call("PARSE_JSON", "[1.23, 2.45,
3.67]").cast(ARRAY(INT())),
+ lit("[1.23, 2.45,
3.67]").parseJson().cast(ARRAY(INT())),
"lose precision")
.testSqlRuntimeError(
"CAST(PARSE_JSON('[1.23, 2.45, 3.67]') AS
ARRAY<INT>)",
"lose precision")
// TRY_CAST swallows the failure and returns NULL for
the whole array
.testResult(
- call("PARSE_JSON", "[1.23, 2.45,
3.67]").tryCast(ARRAY(INT())),
+ lit("[1.23, 2.45,
3.67]").parseJson().tryCast(ARRAY(INT())),
"TRY_CAST(PARSE_JSON('[1.23, 2.45, 3.67]') AS
ARRAY<INT>)",
null,
ARRAY(INT()))
// an object is not an array
.testTableApiRuntimeError(
- call(
- "PARSE_JSON",
- "{\"id\": 7, \"name\":
\"ada\", \"active\": true}")
+ lit("{\"id\": 7, \"name\": \"ada\",
\"active\": true}")
+ .parseJson()
.cast(ARRAY(INT())),
"requires an array")
// ARRAY<VARIANT> shreds one level and keeps the
elements as variants, which
// then cast back to INT unchanged
.testResult(
- call("PARSE_JSON", "[1, 2, 3]")
+ lit("[1, 2, 3]")
+ .parseJson()
.cast(ARRAY(VARIANT()))
.cast(ARRAY(INT())),
"CAST(CAST(PARSE_JSON('[1, 2, 3]') AS
ARRAY<VARIANT>) AS ARRAY<INT>)",
@@ -478,19 +478,19 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
ARRAY(INT()).notNull())
// the recursion composes for a nested array of arrays
.testResult(
- call("PARSE_JSON", "[[1, 2],
[3]]").cast(ARRAY(ARRAY(INT()))),
+ lit("[[1, 2],
[3]]").parseJson().cast(ARRAY(ARRAY(INT()))),
"CAST(PARSE_JSON('[[1, 2], [3]]') AS
ARRAY<ARRAY<INT>>)",
new Integer[][] {{1, 2}, {3}},
ARRAY(ARRAY(INT())).notNull())
// a top-level VARIANT null casts to SQL NULL for a
nullable target
.testResult(
- call("TRY_PARSE_JSON",
"null").cast(ARRAY(INT())),
+ lit("null").tryParseJson().cast(ARRAY(INT())),
"CAST(TRY_PARSE_JSON('null') AS ARRAY<INT>)",
null,
ARRAY(INT()))
// an element with no variant counterpart is rejected
at validation
.testTableApiValidationError(
- call("PARSE_JSON",
"[1]").cast(ARRAY(INTERVAL(MONTH()))),
+
lit("[1]").parseJson().cast(ARRAY(INTERVAL(MONTH()))),
"Unsupported cast"));
}
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 3d2e36ba482..358e5dd0bd0 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
@@ -254,8 +254,11 @@ class JsonFunctionsITCase extends BuiltInFunctionTestBase {
INT().nullable())
// WILDCARDS matching MULTIPLE nodes -> NULL
- // PARSE_JSON has no Table API equivalent, so this stays
SQL-only
- .testSqlResult("JSON_LENGTH(PARSE_JSON(f0), '$.*')", null,
INT().nullable())
+ .testResult(
+ $("f0").parseJson().jsonLength("$.*"),
+ "JSON_LENGTH(PARSE_JSON(f0), '$.*')",
+ null,
+ INT().nullable())
.testResult(
$("f0").jsonLength("$.*"), "JSON_LENGTH(f0, '$.*')",
null, INT().nullable())
.testResult(
@@ -302,12 +305,26 @@ class JsonFunctionsITCase extends BuiltInFunctionTestBase
{
1,
INT().nullable())
// JSON_LENGTH variant support (runtime path, no constant
folding)
- // PARSE_JSON has no Table API equivalent, so these stay
SQL-only
- .testSqlResult("JSON_LENGTH(PARSE_JSON(f0))", 3,
INT().nullable())
- .testSqlResult("JSON_LENGTH(PARSE_JSON('[1,2,3,4,5]'))", 5,
INT().nullable())
- .testSqlResult("JSON_LENGTH(PARSE_JSON('\"hello\"'))", 1,
INT().nullable())
- .testSqlResult(
- "JSON_LENGTH(PARSE_JSON(f0), '$.metadata.tags')", 3,
INT().nullable())
+ .testResult(
+ $("f0").parseJson().jsonLength(),
+ "JSON_LENGTH(PARSE_JSON(f0))",
+ 3,
+ INT().nullable())
+ .testResult(
+ lit("[1,2,3,4,5]").parseJson().jsonLength(),
+ "JSON_LENGTH(PARSE_JSON('[1,2,3,4,5]'))",
+ 5,
+ INT().nullable())
+ .testResult(
+ lit("\"hello\"").parseJson().jsonLength(),
+ "JSON_LENGTH(PARSE_JSON('\"hello\"'))",
+ 1,
+ INT().nullable())
+ .testResult(
+ $("f0").parseJson().jsonLength("$.metadata.tags"),
+ "JSON_LENGTH(PARSE_JSON(f0), '$.metadata.tags')",
+ 3,
+ INT().nullable())
.testResult(
$("f0").jsonLength("$.items[*]"),
"JSON_LENGTH(f0, '$.items[*]')",
@@ -994,7 +1011,7 @@ class JsonFunctionsITCase extends BuiltInFunctionTestBase {
"{\"f0\":[{\"f0\":1,\"f1\":2}]}",
STRING().notNull())
.testResult(
- jsonString(call("PARSE_JSON", $("f14"))),
+ jsonString($("f14").parseJson()),
"JSON_STRING(PARSE_JSON('{\"key\":\"value\"}'))",
"{\"key\":\"value\"}",
STRING().notNull()),
@@ -1019,15 +1036,15 @@ class JsonFunctionsITCase extends
BuiltInFunctionTestBase {
// The bulk of parsing behavior is covered by
BinaryVariantInternalBuilderTest.
return List.of(
TestSetSpec.forFunction(BuiltInFunctionDefinitions.PARSE_JSON)
- .onFieldsWithData("{\"a\":1,\"b\":[2,3]}", "1e400")
- .andDataTypes(STRING().notNull(), STRING().notNull())
+ .onFieldsWithData("{\"a\":1,\"b\":[2,3]}", "1e400",
"{\"a\":1,\"a\":2}")
+ .andDataTypes(STRING().notNull(), STRING().notNull(),
STRING().notNull())
.testResult(
- jsonString(call("PARSE_JSON", $("f0"))),
+ jsonString($("f0").parseJson()),
"JSON_STRING(PARSE_JSON(f0))",
"{\"a\":1,\"b\":[2,3]}",
STRING().notNull())
.testResult(
- jsonString(call("PARSE_JSON",
nullOf(STRING()))),
+ jsonString(nullOf(STRING()).parseJson()),
"JSON_STRING(PARSE_JSON(CAST(NULL AS
STRING)))",
null,
STRING().nullable())
@@ -1036,21 +1053,48 @@ class JsonFunctionsITCase extends
BuiltInFunctionTestBase {
TableRuntimeException.class,
"Failed to parse json string")
.testTableApiRuntimeError(
- call("PARSE_JSON", $("f1")),
+ $("f1").parseJson(),
TableRuntimeException.class,
- "Failed to parse json string"),
+ "Failed to parse json string")
+ // allowDuplicateKeys: false (the default) rejects
duplicate keys
+ .testSqlRuntimeError(
+ "PARSE_JSON(f2, false)",
+ TableRuntimeException.class,
+ "Failed to parse json string")
+ .testTableApiRuntimeError(
+ $("f2").parseJson(false),
+ TableRuntimeException.class,
+ "Failed to parse json string")
+ // allowDuplicateKeys: true keeps the last occurrence
of the duplicated key
+ .testResult(
+ jsonString($("f2").parseJson(true)),
+ "JSON_STRING(PARSE_JSON(f2, true))",
+ "{\"a\":2}",
+ STRING().notNull()),
TestSetSpec.forFunction(BuiltInFunctionDefinitions.TRY_PARSE_JSON)
- .onFieldsWithData("{\"a\":1}", "1e400")
- .andDataTypes(STRING().notNull(), STRING().notNull())
+ .onFieldsWithData("{\"a\":1}", "1e400",
"{\"a\":1,\"a\":2}")
+ .andDataTypes(STRING().notNull(), STRING().notNull(),
STRING().notNull())
.testResult(
- jsonString(call("TRY_PARSE_JSON", $("f0"))),
+ jsonString($("f0").tryParseJson()),
"JSON_STRING(TRY_PARSE_JSON(f0))",
"{\"a\":1}",
STRING())
.testResult(
- jsonString(call("TRY_PARSE_JSON", $("f1"))),
+ jsonString($("f1").tryParseJson()),
"JSON_STRING(TRY_PARSE_JSON(f1))",
null,
+ STRING())
+ // allowDuplicateKeys: false (the default) yields NULL
on duplicate keys
+ .testResult(
+ jsonString($("f2").tryParseJson(false)),
+ "JSON_STRING(TRY_PARSE_JSON(f2, false))",
+ null,
+ STRING())
+ // allowDuplicateKeys: true keeps the last occurrence
of the duplicated key
+ .testResult(
+ jsonString($("f2").tryParseJson(true)),
+ "JSON_STRING(TRY_PARSE_JSON(f2, true))",
+ "{\"a\":2}",
STRING()),
TestSetSpec.forFunction(
BuiltInFunctionDefinitions.PARSE_JSON,
@@ -1061,13 +1105,13 @@ class JsonFunctionsITCase extends
BuiltInFunctionTestBase {
.withConstantFoldingEnabled()
.testResult(
resultSpec(
- call("PARSE_JSON", $("f0")),
+ $("f0").parseJson(),
"PARSE_JSON(f0)",
getVariantForJson("{\"a\": 1}"),
VARIANT().notNull(),
VARIANT().notNull()),
resultSpec(
- jsonString(call("PARSE_JSON",
$("f0"))),
+ jsonString($("f0").parseJson()),
"JSON_STRING(PARSE_JSON(f0))",
"{\"a\":1}",
STRING().notNull(),