This is an automated email from the ASF dual-hosted git repository.
cloud-fan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new b6f2fa017bfb [SPARK-58035][SQL] Add `try_variant_insert` expression
b6f2fa017bfb is described below
commit b6f2fa017bfb668a2f2f2aaa94a1849485640d94
Author: bojana-db <[email protected]>
AuthorDate: Sat Jul 11 17:11:56 2026 +0800
[SPARK-58035][SQL] Add `try_variant_insert` expression
### What changes were proposed in this pull request?
Adds the SQL function `try_variant_insert(v, path, val)`, the
error-tolerant counterpart of `variant_insert` (SPARK-57641). It inserts a
value into a Variant value at a single JSONPath location, returning NULL
instead of failing the query when the insertion hits a recoverable error.
Details:
- On a valid insertion, behaves exactly like `variant_insert`: an object
path (e.g.
`$.b`) adds a new field; an array path (e.g. `$[N]`) inserts at index
`N`, shifting
later elements right and padding gaps past the end with variant nulls;
missing
intermediate keys/indices along the path are created;
- Recoverable errors are caught and return NULL instead of throwing: a
duplicate key
(`VARIANT_DUPLICATE_KEY`) and any path type mismatch
(`VARIANT_PATH_TYPE_MISMATCH`,
e.g. an object key applied to an array);
- Non-recoverable errors still throw, matching `variant_insert`: a
malformed path is
rejected with `INVALID_VARIANT_PATH` (the path is parsed before the
insert), and a
result exceeding the size limit raises `VARIANT_SIZE_LIMIT`;
- Any NULL argument returns NULL;
- The value may be any expression castable to variant.
### Why are the changes needed?
Error-tolerant counterpart of `variant_insert`, matching `try_variant_get`
and other `try_*` functions.
### Does this PR introduce _any_ user-facing change?
Yes, a new SQL function `try_variant_insert` (and the corresponding
Scala/Python
`functions` API).
### How was this patch tested?
Unit tests.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Cursor with Claude Opus 4.8
Closes #57120 from bojana-db/try-variant-insert.
Authored-by: bojana-db <[email protected]>
Signed-off-by: Wenchen Fan <[email protected]>
---
.../source/reference/pyspark.sql/functions.rst | 1 +
python/pyspark/sql/connect/functions/builtin.py | 10 ++
python/pyspark/sql/functions/__init__.py | 1 +
python/pyspark/sql/functions/builtin.py | 63 +++++++-
python/pyspark/sql/tests/test_functions.py | 6 +
.../scala/org/apache/spark/sql/functions.scala | 46 +++++-
.../sql/catalyst/analysis/FunctionRegistry.scala | 3 +-
.../variant/VariantExpressionEvalUtils.scala | 17 +-
.../expressions/variant/variantExpressions.scala | 179 ++++++++++++++-------
.../variant/VariantExpressionSuite.scala | 130 +++++++++------
.../apache/spark/sql/PlanGenerationTestSuite.scala | 4 +
.../function_try_variant_insert.explain | 2 +
.../function_variant_insert.explain | 2 +-
.../queries/function_try_variant_insert.json | 125 ++++++++++++++
.../queries/function_try_variant_insert.proto.bin | Bin 0 -> 1151 bytes
.../sql-functions/sql-expression-schema.md | 3 +-
.../scala/org/apache/spark/sql/VariantSuite.scala | 55 +++++++
17 files changed, 532 insertions(+), 115 deletions(-)
diff --git a/python/docs/source/reference/pyspark.sql/functions.rst
b/python/docs/source/reference/pyspark.sql/functions.rst
index 70e5811180d0..9ae6cd12a13c 100644
--- a/python/docs/source/reference/pyspark.sql/functions.rst
+++ b/python/docs/source/reference/pyspark.sql/functions.rst
@@ -605,6 +605,7 @@ VARIANT Functions
variant_delete
variant_get
variant_insert
+ try_variant_insert
try_parse_json
to_variant_object
diff --git a/python/pyspark/sql/connect/functions/builtin.py
b/python/pyspark/sql/connect/functions/builtin.py
index 8e74aeef2828..8a33d3b2b38e 100644
--- a/python/pyspark/sql/connect/functions/builtin.py
+++ b/python/pyspark/sql/connect/functions/builtin.py
@@ -2227,6 +2227,16 @@ def variant_insert(v: "ColumnOrName", path:
Union[Column, str], value: "ColumnOr
variant_insert.__doc__ = pysparkfuncs.variant_insert.__doc__
+def try_variant_insert(
+ v: "ColumnOrName", path: Union[Column, str], value: "ColumnOrName"
+) -> Column:
+ path_col = path if isinstance(path, Column) else lit(path)
+ return _invoke_function("try_variant_insert", _to_col(v), path_col,
_to_col(value))
+
+
+try_variant_insert.__doc__ = pysparkfuncs.try_variant_insert.__doc__
+
+
def variant_get(v: "ColumnOrName", path: Union[Column, str], targetType: str)
-> Column:
assert isinstance(path, (Column, str))
if isinstance(path, str):
diff --git a/python/pyspark/sql/functions/__init__.py
b/python/pyspark/sql/functions/__init__.py
index ee9d06fcaa2d..052ca8a51737 100644
--- a/python/pyspark/sql/functions/__init__.py
+++ b/python/pyspark/sql/functions/__init__.py
@@ -483,6 +483,7 @@ __all__ = [ # noqa: F405
"variant_delete",
"variant_get",
"variant_insert",
+ "try_variant_insert",
"try_parse_json",
"to_variant_object",
# XML Functions
diff --git a/python/pyspark/sql/functions/builtin.py
b/python/pyspark/sql/functions/builtin.py
index 1e348cfd4287..2103822179d6 100644
--- a/python/pyspark/sql/functions/builtin.py
+++ b/python/pyspark/sql/functions/builtin.py
@@ -21731,7 +21731,8 @@ def variant_insert(v: "ColumnOrName", path:
Union[Column, str], value: "ColumnOr
"""
Inserts a value into a variant at the given JSONPath location. An object
path adds a new field
(error if it already exists); an array path inserts at the index, shifting
later elements
- right. Missing intermediate keys are created. Returns NULL if any argument
is NULL.
+ right. Missing intermediate keys are created. Throws an error if a path
segment hits a value
+ of an incompatible type. Returns NULL if any argument is NULL.
.. versionadded:: 4.3.0
@@ -21786,6 +21787,66 @@ def variant_insert(v: "ColumnOrName", path:
Union[Column, str], value: "ColumnOr
)
+@_try_remote_functions
+def try_variant_insert(
+ v: "ColumnOrName", path: Union[Column, str], value: "ColumnOrName"
+) -> Column:
+ """
+ Inserts a value into a variant at the given JSONPath location. An object
path adds a new field;
+ an array path inserts at the index, shifting later elements right. Missing
intermediate keys
+ are created. Returns NULL if the field already exists or a path segment
hits a value of an
+ incompatible type, or if any argument is NULL.
+
+ .. versionadded:: 4.3.0
+
+ Parameters
+ ----------
+ v : :class:`~pyspark.sql.Column` or str
+ a variant column or column name
+ path : :class:`~pyspark.sql.Column` or str
+ the JSONPath insertion target. A `str` is a literal path; a
+ :class:`~pyspark.sql.Column` supplies the path at runtime. A valid
path should start with
+ `$` and is followed by one or more segments like `[123]`, `.name`,
`['name']`, or
+ `["name"]`. The root path `$` is not allowed.
+ value : :class:`~pyspark.sql.Column` or str
+ the value to insert. Any expression castable to variant.
+
+ Returns
+ -------
+ :class:`~pyspark.sql.Column`
+ a variant column with `value` inserted at `path`, or NULL if the
insertion fails
+
+ Examples
+ --------
+ >>> from pyspark.sql.functions import lit, parse_json, to_json,
try_variant_insert
+ >>> df = spark.createDataFrame([{'json': '''{ "a": 1, "arr": ["x", "y"]
}'''}])
+ >>> v = parse_json(df.json)
+ >>> df.select(to_json(try_variant_insert(v, "$.b",
lit(2))).alias("r")).collect()
+ [Row(r='{"a":1,"arr":["x","y"],"b":2}')]
+ >>> df.select(to_json(try_variant_insert(v, "$.c.d",
lit(3))).alias("r")).collect()
+ [Row(r='{"a":1,"arr":["x","y"],"c":{"d":3}}')]
+ >>> df.select(to_json(try_variant_insert(v, "$.arr[1]",
lit("z"))).alias("r")).collect()
+ [Row(r='{"a":1,"arr":["x","z","y"]}')]
+ >>> df.select(to_json(try_variant_insert(v, "$.arr[5]",
lit("z"))).alias("r")).collect()
+ [Row(r='{"a":1,"arr":["x","y",null,null,null,"z"]}')]
+ >>> df.select(to_json(try_variant_insert(v, "$.a",
lit(2))).alias("r")).collect()
+ [Row(r=None)]
+ >>> df.select(to_json(try_variant_insert(v, "$.a.b",
lit(2))).alias("r")).collect()
+ [Row(r=None)]
+ >>> df.select(to_json(try_variant_insert(v, "$.b",
lit(None))).alias("r")).collect()
+ [Row(r=None)]
+ """
+ from pyspark.sql.classic.column import _to_java_column
+
+ path_col = path if isinstance(path, Column) else lit(path)
+ return _invoke_function(
+ "try_variant_insert",
+ _to_java_column(v),
+ _to_java_column(path_col),
+ _to_java_column(value),
+ )
+
+
@_try_remote_functions
def variant_get(v: "ColumnOrName", path: Union[Column, str], targetType: str)
-> Column:
"""
diff --git a/python/pyspark/sql/tests/test_functions.py
b/python/pyspark/sql/tests/test_functions.py
index 2ade286763de..3ac206d71f92 100644
--- a/python/pyspark/sql/tests/test_functions.py
+++ b/python/pyspark/sql/tests/test_functions.py
@@ -3534,6 +3534,12 @@ class FunctionsTestsMixin:
df.select(F.to_json(F.variant_insert(v, df.newpath, F.lit(9)))),
['{"a":1,"z":9}', '{"b":2,"z":9}'],
)
+ check(
+ df.select(F.to_json(F.try_variant_insert(v, "$.z", F.lit(9)))),
+ ['{"a":1,"z":9}', '{"b":2,"z":9}'],
+ )
+ check(df.select(F.to_json(F.try_variant_insert(v, df.path,
F.lit(9)))), [None, None])
+ check(df.select(F.to_json(F.try_variant_insert(v, "$.z",
F.lit(None)))), [None, None])
check(df.select(F.schema_of_variant(v)), ["OBJECT<a: BIGINT>",
"OBJECT<b: BIGINT>"])
check(df.select(F.schema_of_variant_agg(v)), ["OBJECT<a: BIGINT, b:
BIGINT>"])
diff --git a/sql/api/src/main/scala/org/apache/spark/sql/functions.scala
b/sql/api/src/main/scala/org/apache/spark/sql/functions.scala
index c8b269a63de5..0be5b0b30c01 100644
--- a/sql/api/src/main/scala/org/apache/spark/sql/functions.scala
+++ b/sql/api/src/main/scala/org/apache/spark/sql/functions.scala
@@ -9824,7 +9824,8 @@ object functions {
/**
* Inserts a value into a variant at the given JSONPath location. An object
path adds a new
* field (error if it already exists); an array path inserts at the index,
shifting later
- * elements right. Missing intermediate keys are created. Returns NULL if
any argument is NULL.
+ * elements right. Missing intermediate keys are created. Throws an error if
a path segment hits
+ * a value of an incompatible type. Returns NULL if any argument is NULL.
*
* @param v
* a variant column.
@@ -9843,7 +9844,8 @@ object functions {
/**
* Inserts a value into a variant at the given JSONPath location. An object
path adds a new
* field (error if it already exists); an array path inserts at the index,
shifting later
- * elements right. Missing intermediate keys are created. Returns NULL if
any argument is NULL.
+ * elements right. Missing intermediate keys are created. Throws an error if
a path segment hits
+ * a value of an incompatible type. Returns NULL if any argument is NULL.
*
* @param v
* a variant column.
@@ -9859,6 +9861,46 @@ object functions {
def variant_insert(v: Column, path: String, value: Column): Column =
Column.fn("variant_insert", v, lit(path), value)
+ /**
+ * Inserts a value into a variant at the given JSONPath location. An object
path adds a new
+ * field; an array path inserts at the index, shifting later elements right.
Missing
+ * intermediate keys are created. Returns NULL if the field already exists
or a path segment
+ * hits a value of an incompatible type, or if any argument is NULL.
+ *
+ * @param v
+ * a variant column.
+ * @param path
+ * the column containing the JSONPath string identifying the insertion
target. A valid path
+ * should start with `$` and is followed by one or more segments like
`[123]`, `.name`,
+ * `['name']`, or `["name"]`. The root path `$` is not allowed.
+ * @param value
+ * the value to insert. Any expression castable to variant.
+ * @group variant_funcs
+ * @since 4.3.0
+ */
+ def try_variant_insert(v: Column, path: Column, value: Column): Column =
+ Column.fn("try_variant_insert", v, path, value)
+
+ /**
+ * Inserts a value into a variant at the given JSONPath location. An object
path adds a new
+ * field; an array path inserts at the index, shifting later elements right.
Missing
+ * intermediate keys are created. Returns NULL if the field already exists
or a path segment
+ * hits a value of an incompatible type, or if any argument is NULL.
+ *
+ * @param v
+ * a variant column.
+ * @param path
+ * the JSONPath identifying the insertion target. A valid path should
start with `$` and is
+ * followed by one or more segments like `[123]`, `.name`, `['name']`, or
`["name"]`. The root
+ * path `$` is not allowed.
+ * @param value
+ * the value to insert. Any expression castable to variant.
+ * @group variant_funcs
+ * @since 4.3.0
+ */
+ def try_variant_insert(v: Column, path: String, value: Column): Column =
+ Column.fn("try_variant_insert", v, lit(path), value)
+
/**
* Extracts a sub-variant from `v` according to `path` string, and then cast
the sub-variant to
* `targetType`. Returns null if the path does not exist. Throws an
exception if the cast fails.
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
index 193e939889db..f1b788eb3e81 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala
@@ -986,7 +986,8 @@ object FunctionRegistry {
expression[ToVariantObject]("to_variant_object"),
expression[IsValidVariant]("is_valid_variant"),
expression[VariantDelete]("variant_delete"),
- expression[VariantInsert]("variant_insert"),
+ expressionBuilder("variant_insert", VariantInsertExpressionBuilder),
+ expressionBuilder("try_variant_insert", TryVariantInsertExpressionBuilder),
// Spatial
expression[ST_AsBinary]("st_asbinary"),
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/variant/VariantExpressionEvalUtils.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/variant/VariantExpressionEvalUtils.scala
index 20a55bccc779..bf5d22ee7482 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/variant/VariantExpressionEvalUtils.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/variant/VariantExpressionEvalUtils.scala
@@ -19,6 +19,7 @@ package org.apache.spark.sql.catalyst.expressions.variant
import scala.util.control.NonFatal
+import org.apache.spark.SparkRuntimeException
import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.catalyst.util.{ArrayData, MapData}
import org.apache.spark.sql.errors.QueryExecutionErrors
@@ -131,7 +132,10 @@ object VariantExpressionEvalUtils {
/**
* Insert `value` into `input` at `javaSegments`. `path` is the source
string used in error
* messages. The cast and insert share one try, so any size overflow maps to
`VARIANT_SIZE_LIMIT`
- * and a type mismatch maps to `VARIANT_PATH_TYPE_MISMATCH`.
+ * and a type mismatch maps to `VARIANT_PATH_TYPE_MISMATCH`. When
`failOnError` is false (the
+ * `try_variant_insert` mode), a duplicate key or path type mismatch returns
null instead of
+ * throwing; a size overflow (and a malformed path, rejected earlier during
parsing) is still
+ * raised.
*/
def insertAtPath(
input: VariantVal,
@@ -139,7 +143,8 @@ object VariantExpressionEvalUtils {
path: String,
value: Any,
valueDataType: DataType,
- functionName: String): VariantVal = {
+ functionName: String,
+ failOnError: Boolean): VariantVal = {
val v = new Variant(input.getValue, input.getMetadata)
try {
val valVal = castToVariant(value, valueDataType)
@@ -147,6 +152,9 @@ object VariantExpressionEvalUtils {
val out = VariantBuilder.insertAtPath(v, javaSegments, valVariant)
new VariantVal(out.getValue, out.getMetadata)
} catch {
+ case _: VariantPathTypeMismatchException if !failOnError => null
+ case e: SparkRuntimeException if !failOnError && e.getCondition ==
"VARIANT_DUPLICATE_KEY" =>
+ null
case e: VariantPathTypeMismatchException =>
throw QueryExecutionErrors.variantPathTypeMismatch(
path, renderVariantPath(javaSegments.take(e.depth)), functionName)
@@ -160,10 +168,11 @@ object VariantExpressionEvalUtils {
path: UTF8String,
value: Any,
valueDataType: DataType,
- functionName: String): VariantVal = {
+ functionName: String,
+ failOnError: Boolean): VariantVal = {
val pathStr = path.toString
val javaSegments = toJavaSegments(parseVariantPath(pathStr, functionName))
- insertAtPath(input, javaSegments, pathStr, value, valueDataType,
functionName)
+ insertAtPath(input, javaSegments, pathStr, value, valueDataType,
functionName, failOnError)
}
/** Cast a Spark value from `dataType` into the variant type. */
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/variant/variantExpressions.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/variant/variantExpressions.scala
index 0f8995ce829f..3e404cc9c190 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/variant/variantExpressions.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/variant/variantExpressions.scala
@@ -808,42 +808,11 @@ object VariantDelete {
}
}
-// scalastyle:off line.size.limit
-@ExpressionDescription(
- usage = "_FUNC_(v, path, val) - Inserts a value into a variant at the given
JSONPath " +
- "location. An object path adds a new field (error if it already exists);
an array path " +
- "inserts at the index, shifting later elements right. Missing intermediate
keys are " +
- "created. Returns NULL if any argument is NULL.",
- arguments = """
- Arguments:
- * v - A variant value to mutate.
- * path - A string expression evaluating to a JSONPath identifying the
insertion target. A
- valid path should start with `$` and is followed by one or more
segments like `[123]`,
- `.name`, `['name']`, or `["name"]`. The root path `$` is not allowed.
- * val - Any expression castable to variant.
- """,
- examples = """
- Examples:
- > SELECT _FUNC_(parse_json('{"a": 1}'), '$.b', 2);
- {"a":1,"b":2}
- > SELECT _FUNC_(parse_json('{}'), '$.a.b', 1);
- {"a":{"b":1}}
- > SELECT _FUNC_(parse_json('["a","b","c"]'), '$[1]', 'z');
- ["a","z","b","c"]
- > SELECT _FUNC_(parse_json('["a","b"]'), '$[5]', 'z');
- ["a","b",null,null,null,"z"]
- > SELECT _FUNC_(parse_json('{}'), '$.a', parse_json('{"x":1}'));
- {"a":{"x":1}}
- > SELECT _FUNC_(parse_json('{}'), '$.a', parse_json('null'));
- {"a":null}
- > SELECT _FUNC_(parse_json('{}'), '$.a', null);
- NULL
- """,
- since = "4.3.0",
- group = "variant_funcs"
-)
-// scalastyle:on line.size.limit
-case class VariantInsert(input: Expression, path: Expression, value:
Expression)
+case class VariantInsert(
+ input: Expression,
+ path: Expression,
+ value: Expression,
+ failOnError: Boolean = true)
extends TernaryExpression
with ExpectsInputTypes
with QueryErrorsBase {
@@ -853,6 +822,7 @@ case class VariantInsert(input: Expression, path:
Expression, value: Expression)
override def third: Expression = value
override def nullIntolerant: Boolean = true
+ override def nullable: Boolean = !failOnError || children.exists(_.nullable)
override def dataType: DataType = VariantType
override def inputTypes: Seq[AbstractDataType] =
@@ -898,34 +868,46 @@ case class VariantInsert(input: Expression, path:
Expression, value: Expression)
foldablePath match {
case Some(parsed) =>
VariantExpressionEvalUtils.insertAtPath(
- inputVariant, parsed.javaSegments, parsed.pathStr, valValue,
value.dataType, prettyName)
+ inputVariant, parsed.javaSegments, parsed.pathStr, valValue,
value.dataType, prettyName,
+ failOnError)
case None =>
VariantExpressionEvalUtils.insertAtPath(
- inputVariant, p.asInstanceOf[UTF8String], valValue, value.dataType,
prettyName)
+ inputVariant, p.asInstanceOf[UTF8String], valValue, value.dataType,
prettyName,
+ failOnError)
}
}
override protected def doGenCode(ctx: CodegenContext, ev: ExprCode):
ExprCode = {
val cls = VariantExpressionEvalUtils.getClass.getName.stripSuffix("$")
- nullSafeCodeGen(ctx, ev, (vVal, pVal, valVal) => {
- val fromArg = ctx.addReferenceObj("from", value.dataType)
- foldablePath match {
- case Some(parsed) =>
- val parsedArg = ctx.addReferenceObj("insertPath", parsed)
- s"""
- |${ev.value} = $cls.insertAtPath(
- | $vVal, $parsedArg.javaSegments(), $parsedArg.pathStr(),
$valVal, $fromArg,
- | "$prettyName");
- """.stripMargin
- case None =>
- s"""
- |${ev.value} = $cls.insertAtPath($vVal, $pVal, $valVal, $fromArg,
"$prettyName");
- """.stripMargin
- }
- })
+ val fromArg = ctx.addReferenceObj("from", value.dataType)
+ def call(vVal: String, pVal: String, valVal: String): String =
foldablePath match {
+ case Some(parsed) =>
+ val parsedArg = ctx.addReferenceObj("insertPath", parsed)
+ s"""$cls.insertAtPath(
+ | $vVal, $parsedArg.javaSegments(), $parsedArg.pathStr(), $valVal,
$fromArg,
+ | "$prettyName", $failOnError)""".stripMargin
+ case None =>
+ s"""$cls.insertAtPath($vVal, $pVal, $valVal, $fromArg, "$prettyName",
$failOnError)"""
+ }
+ if (failOnError) {
+ nullSafeCodeGen(ctx, ev,
+ (vVal, pVal, valVal) => s"${ev.value} = ${call(vVal, pVal, valVal)};")
+ } else {
+ val resultType = CodeGenerator.javaType(VariantType)
+ val tmp = ctx.freshName("insertResult")
+ nullSafeCodeGen(ctx, ev, (vVal, pVal, valVal) =>
+ s"""
+ |$resultType $tmp = ${call(vVal, pVal, valVal)};
+ |if ($tmp == null) {
+ | ${ev.isNull} = true;
+ |} else {
+ | ${ev.value} = $tmp;
+ |}
+ """.stripMargin)
+ }
}
- override def prettyName: String = "variant_insert"
+ override def prettyName: String = if (failOnError) "variant_insert" else
"try_variant_insert"
override protected def withNewChildrenInternal(
newFirst: Expression, newSecond: Expression, newThird: Expression):
VariantInsert =
@@ -942,6 +924,93 @@ object VariantInsert {
}
}
+abstract class VariantInsertExpressionBuilderBase(failOnError: Boolean)
extends ExpressionBuilder {
+ override def build(funcName: String, expressions: Seq[Expression]):
Expression = {
+ val numArgs = expressions.length
+ if (numArgs == 3) {
+ VariantInsert(expressions(0), expressions(1), expressions(2),
failOnError)
+ } else {
+ throw QueryCompilationErrors.wrongNumArgsError(funcName, Seq(3), numArgs)
+ }
+ }
+}
+
+// scalastyle:off line.size.limit
+@ExpressionDescription(
+ usage = "_FUNC_(v, path, val) - Inserts a value into a variant at the given
JSONPath " +
+ "location. An object path adds a new field (error if it already exists);
an array path " +
+ "inserts at the index, shifting later elements right. Missing intermediate
keys are created. " +
+ "Throws an error if a path segment hits a value of an incompatible type.
Returns NULL if any " +
+ "argument is NULL.",
+ arguments = """
+ Arguments:
+ * v - A variant value to mutate.
+ * path - A string expression evaluating to a JSONPath identifying the
insertion target. A
+ valid path should start with `$` and is followed by one or more
segments like `[123]`,
+ `.name`, `['name']`, or `["name"]`. The root path `$` is not allowed.
+ * val - Any expression castable to variant.
+ """,
+ examples = """
+ Examples:
+ > SELECT _FUNC_(parse_json('{"a": 1}'), '$.b', 2);
+ {"a":1,"b":2}
+ > SELECT _FUNC_(parse_json('{}'), '$.a.b', 1);
+ {"a":{"b":1}}
+ > SELECT _FUNC_(parse_json('["a","b","c"]'), '$[1]', 'z');
+ ["a","z","b","c"]
+ > SELECT _FUNC_(parse_json('["a","b"]'), '$[5]', 'z');
+ ["a","b",null,null,null,"z"]
+ > SELECT _FUNC_(parse_json('{}'), '$.a', parse_json('{"x":1}'));
+ {"a":{"x":1}}
+ > SELECT _FUNC_(parse_json('{}'), '$.a', parse_json('null'));
+ {"a":null}
+ > SELECT _FUNC_(parse_json('{}'), '$.a', null);
+ NULL
+ """,
+ since = "4.3.0",
+ group = "variant_funcs"
+)
+// scalastyle:on line.size.limit
+object VariantInsertExpressionBuilder extends
VariantInsertExpressionBuilderBase(true)
+
+// scalastyle:off line.size.limit
+@ExpressionDescription(
+ usage = "_FUNC_(v, path, val) - Inserts a value into a variant at the given
JSONPath " +
+ "location. An object path adds a new field; an array path inserts at the
index, shifting " +
+ "later elements right. Missing intermediate keys are created. Returns NULL
if the field " +
+ "already exists or a path segment hits a value of an incompatible type, or
if any argument is " +
+ "NULL.",
+ arguments = """
+ Arguments:
+ * v - A variant value to mutate.
+ * path - A string expression evaluating to a JSONPath identifying the
insertion target. A
+ valid path should start with `$` and is followed by one or more
segments like `[123]`,
+ `.name`, `['name']`, or `["name"]`. The root path `$` is not allowed.
+ * val - Any expression castable to variant.
+ """,
+ examples = """
+ Examples:
+ > SELECT _FUNC_(parse_json('{"a": 1}'), '$.b', 2);
+ {"a":1,"b":2}
+ > SELECT _FUNC_(parse_json('{}'), '$.a.b', 1);
+ {"a":{"b":1}}
+ > SELECT _FUNC_(parse_json('["a","b","c"]'), '$[1]', 'z');
+ ["a","z","b","c"]
+ > SELECT _FUNC_(parse_json('["a","b"]'), '$[5]', 'z');
+ ["a","b",null,null,null,"z"]
+ > SELECT _FUNC_(parse_json('{"a": 1}'), '$.a', 2);
+ NULL
+ > SELECT _FUNC_(parse_json('{"a": 1}'), '$.a.b', 2);
+ NULL
+ > SELECT _FUNC_(parse_json('{}'), '$.a', null);
+ NULL
+ """,
+ since = "4.3.0",
+ group = "variant_funcs"
+)
+// scalastyle:on line.size.limit
+object TryVariantInsertExpressionBuilder extends
VariantInsertExpressionBuilderBase(false)
+
case class VariantExplode(child: Expression) extends UnaryExpression with
Generator
with ExpectsInputTypes {
override def inputTypes: Seq[AbstractDataType] = Seq(VariantType)
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/variant/VariantExpressionSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/variant/VariantExpressionSuite.scala
index 6f1a410c5bb9..ec28ed514c19 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/variant/VariantExpressionSuite.scala
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/variant/VariantExpressionSuite.scala
@@ -1329,11 +1329,48 @@ class VariantExpressionSuite extends SparkFunSuite with
ExpressionEvalHelper {
test("variant_insert") {
def checkInsert(input: String, path: String, value: Expression, expected:
String): Unit = {
- val expr = VariantInsert(
- Literal(parseJson(input)), Literal.create(path, StringType), value)
+ Seq(true, false).foreach { failOnError =>
+ val expr = VariantInsert(
+ Literal(parseJson(input)), Literal.create(path, StringType), value,
failOnError)
+ checkEvaluation(
+ ResolveTimeZone.resolveTimeZones(Cast(expr, StringType)),
+ expected)
+ }
+ }
+
+ // A recoverable error: `variant_insert` throws, but `try_variant_insert`
returns NULL.
+ def checkInsertRecoverableError(
+ input: String,
+ path: String,
+ value: Expression,
+ condition: String,
+ parameters: Map[String, String]): Unit = {
+ checkErrorInExpression[SparkRuntimeException](
+ VariantInsert(Literal(parseJson(input)), Literal.create(path,
StringType), value),
+ condition,
+ parameters)
checkEvaluation(
- ResolveTimeZone.resolveTimeZones(Cast(expr, StringType)),
- expected)
+ VariantInsert(
+ Literal(parseJson(input)), Literal.create(path, StringType), value,
failOnError = false),
+ null)
+ }
+
+ // An unrecoverable error (a malformed path or a size overflow): both
functions throw, and only
+ // the `functionName` in the error message differs.
+ def checkInsertUnrecoverableError(
+ input: String,
+ path: String,
+ value: Expression,
+ condition: String,
+ parameters: String => Map[String, String]): Unit = {
+ Seq("variant_insert" -> true, "try_variant_insert" -> false).foreach {
+ case (name, failOnError) =>
+ checkErrorInExpression[SparkRuntimeException](
+ VariantInsert(
+ Literal(parseJson(input)), Literal.create(path, StringType),
value, failOnError),
+ condition,
+ parameters(name))
+ }
}
// Object inserts.
@@ -1385,55 +1422,48 @@ class VariantExpressionSuite extends SparkFunSuite with
ExpressionEvalHelper {
checkInsert(
"{}", "$.a", Literal.create(Array(1, 2, 3), ArrayType(IntegerType)),
"""{"a":[1,2,3]}""")
- // NULL-intolerant: any NULL argument yields NULL.
- checkEvaluation(
- VariantInsert(Literal.create(null, VariantType), Literal("$.a"),
Literal(1)),
- null)
+ Seq(true, false).foreach { failOnError =>
+ checkEvaluation(
+ VariantInsert(Literal.create(null, VariantType), Literal("$.a"),
Literal(1), failOnError),
+ null)
+ }
checkInsert("""{"a": 1}""", null, Literal(1), null)
checkInsert("""{"a": 1}""", "$.b", Literal.create(null, VariantType), null)
checkInsert("""{"a": 1}""", "$.b", Literal.create(null, NullType), null)
- val dynamic = VariantInsert(
- Literal(parseJson("""{"a": 1}""")),
- BoundReference(0, StringType, nullable = true),
- Literal(2))
- checkEvaluation(
- ResolveTimeZone.resolveTimeZones(Cast(dynamic, StringType)),
- """{"a":1,"b":2}""",
- InternalRow(UTF8String.fromString("$.b")))
+ Seq(true, false).foreach { failOnError =>
+ val dynamic = VariantInsert(
+ Literal(parseJson("""{"a": 1}""")),
+ BoundReference(0, StringType, nullable = true),
+ Literal(2),
+ failOnError)
+ checkEvaluation(
+ ResolveTimeZone.resolveTimeZones(Cast(dynamic, StringType)),
+ """{"a":1,"b":2}""",
+ InternalRow(UTF8String.fromString("$.b")))
+ }
- checkErrorInExpression[SparkRuntimeException](
- VariantInsert(Literal(parseJson("""{"a": 1}""")), Literal("$.a"),
Literal(2)),
+ // Recoverable errors: `variant_insert` throws; `try_variant_insert`
returns NULL. A duplicate
+ // key and every shape of path type mismatch are all recoverable.
+ checkInsertRecoverableError("""{"a": 1}""", "$.a", Literal(2),
"VARIANT_DUPLICATE_KEY",
Map("key" -> "a"))
-
- checkErrorInExpression[SparkRuntimeException](
- VariantInsert(Literal(parseJson("""{"a": 1}""")), Literal("$.a.b"),
Literal(2)),
+ checkInsertRecoverableError("""{"a": 1}""", "$.a.b", Literal(2),
"VARIANT_PATH_TYPE_MISMATCH",
Map("path" -> "$.a.b", "failedAt" -> "$.a", "functionName" ->
"`variant_insert`"))
-
- checkErrorInExpression[SparkRuntimeException](
- VariantInsert(Literal(parseJson("""{"a": 1}""")), Literal("$[0]"),
Literal(2)),
+ checkInsertRecoverableError("""{"a": 1}""", "$[0]", Literal(2),
"VARIANT_PATH_TYPE_MISMATCH",
Map("path" -> "$[0]", "failedAt" -> "$", "functionName" ->
"`variant_insert`"))
-
- checkErrorInExpression[SparkRuntimeException](
- VariantInsert(Literal(parseJson("""{"a": [1, 2]}""")), Literal("$.a.b"),
Literal(2)),
+ checkInsertRecoverableError("""{"a": [1, 2]}""", "$.a.b", Literal(2),
"VARIANT_PATH_TYPE_MISMATCH",
Map("path" -> "$.a.b", "failedAt" -> "$.a", "functionName" ->
"`variant_insert`"))
-
- checkErrorInExpression[SparkRuntimeException](
- VariantInsert(Literal(parseJson("""{"a": 5}""")), Literal("$.a[0]"),
Literal(2)),
+ checkInsertRecoverableError("""{"a": 5}""", "$.a[0]", Literal(2),
"VARIANT_PATH_TYPE_MISMATCH",
Map("path" -> "$.a[0]", "failedAt" -> "$.a", "functionName" ->
"`variant_insert`"))
-
- checkErrorInExpression[SparkRuntimeException](
- VariantInsert(Literal(parseJson("5")), Literal("$.a"), Literal(2)),
+ checkInsertRecoverableError("5", "$.a", Literal(2),
"VARIANT_PATH_TYPE_MISMATCH",
Map("path" -> "$.a", "failedAt" -> "$", "functionName" ->
"`variant_insert`"))
-
- checkErrorInExpression[SparkRuntimeException](
- VariantInsert(Literal(parseJson("""{"a.b": 5}""")),
Literal("""$['a.b'].c"""), Literal(2)),
+ checkInsertRecoverableError("""{"a.b": 5}""", """$['a.b'].c""", Literal(2),
"VARIANT_PATH_TYPE_MISMATCH",
Map("path" -> "$['a.b'].c", "failedAt" -> "$['a.b']", "functionName" ->
"`variant_insert`"))
@@ -1442,27 +1472,27 @@ class VariantExpressionSuite extends SparkFunSuite with
ExpressionEvalHelper {
Literal.create(null, MapType(StringType, IntegerType)),
Literal.create(null, StructType(Seq(StructField("x", IntegerType))))
).foreach { v =>
- assert(
- VariantInsert(Literal(parseJson("{}")), Literal("$.a"),
v).checkInputDataTypes().isFailure)
+ Seq(true, false).foreach { failOnError =>
+ assert(
+ VariantInsert(Literal(parseJson("{}")), Literal("$.a"), v,
failOnError)
+ .checkInputDataTypes().isFailure)
+ }
}
- checkErrorInExpression[SparkRuntimeException](
- VariantInsert(Literal(parseJson("{}")), Literal("$"), Literal(1)),
+ // Unrecoverable errors: both functions throw.
+ checkInsertUnrecoverableError("{}", "$", Literal(1),
"INVALID_VARIANT_PATH",
- Map("path" -> "$", "functionName" -> "`variant_insert`"))
- checkErrorInExpression[SparkRuntimeException](
- VariantInsert(Literal(parseJson("{}")), Literal("abc"), Literal(1)),
+ name => Map("path" -> "$", "functionName" -> s"`$name`"))
+ checkInsertUnrecoverableError("{}", "abc", Literal(1),
"INVALID_VARIANT_PATH",
- Map("path" -> "abc", "functionName" -> "`variant_insert`"))
+ name => Map("path" -> "abc", "functionName" -> s"`$name`"))
val tooBig = "x".repeat(16 * 1024 * 1024)
- checkErrorInExpression[SparkRuntimeException](
- VariantInsert(Literal(parseJson("{}")), Literal("$.a[2000000000]"),
Literal(1)),
+ checkInsertUnrecoverableError("{}", "$.a[2000000000]", Literal(1),
"VARIANT_SIZE_LIMIT",
- Map("sizeLimit" -> "16.0 MiB", "functionName" -> "`variant_insert`"))
- checkErrorInExpression[SparkRuntimeException](
- VariantInsert(Literal(parseJson("{}")), Literal("$.a"), Literal(tooBig)),
+ name => Map("sizeLimit" -> "16.0 MiB", "functionName" -> s"`$name`"))
+ checkInsertUnrecoverableError("{}", "$.a", Literal(tooBig),
"VARIANT_SIZE_LIMIT",
- Map("sizeLimit" -> "16.0 MiB", "functionName" -> "`variant_insert`"))
+ name => Map("sizeLimit" -> "16.0 MiB", "functionName" -> s"`$name`"))
}
}
diff --git
a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala
b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala
index 34faa4545b98..0222a40219ae 100644
---
a/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala
+++
b/sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/PlanGenerationTestSuite.scala
@@ -2767,6 +2767,10 @@ class PlanGenerationTestSuite extends ConnectFunSuite
with Logging {
fn.variant_insert(fn.parse_json(fn.col("g")), "$.a", fn.lit(1))
}
+ functionTest("try_variant_insert") {
+ fn.try_variant_insert(fn.parse_json(fn.col("g")), "$.a", fn.lit(1))
+ }
+
functionTest("variant_get") {
fn.variant_get(fn.parse_json(fn.col("g")), "$", "int")
}
diff --git
a/sql/connect/common/src/test/resources/query-tests/explain-results/function_try_variant_insert.explain
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_try_variant_insert.explain
new file mode 100644
index 000000000000..cec39e83751e
--- /dev/null
+++
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_try_variant_insert.explain
@@ -0,0 +1,2 @@
+Project
[try_variant_insert(static_invoke(VariantExpressionEvalUtils.parseJson(g#0,
false, true, true)), $.a, 1, false) AS try_variant_insert(parse_json(g), $.a,
1)#0]
++- LocalRelation <empty>, [id#0L, a#0, b#0, d#0, e#0, f#0, g#0]
diff --git
a/sql/connect/common/src/test/resources/query-tests/explain-results/function_variant_insert.explain
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_variant_insert.explain
index 2b4cdcebf273..583443ce13fc 100644
---
a/sql/connect/common/src/test/resources/query-tests/explain-results/function_variant_insert.explain
+++
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_variant_insert.explain
@@ -1,2 +1,2 @@
-Project
[variant_insert(static_invoke(VariantExpressionEvalUtils.parseJson(g#0, false,
true, true)), $.a, 1) AS variant_insert(parse_json(g), $.a, 1)#0]
+Project
[variant_insert(static_invoke(VariantExpressionEvalUtils.parseJson(g#0, false,
true, true)), $.a, 1, true) AS variant_insert(parse_json(g), $.a, 1)#0]
+- LocalRelation <empty>, [id#0L, a#0, b#0, d#0, e#0, f#0, g#0]
diff --git
a/sql/connect/common/src/test/resources/query-tests/queries/function_try_variant_insert.json
b/sql/connect/common/src/test/resources/query-tests/queries/function_try_variant_insert.json
new file mode 100644
index 000000000000..f7c0e623a966
--- /dev/null
+++
b/sql/connect/common/src/test/resources/query-tests/queries/function_try_variant_insert.json
@@ -0,0 +1,125 @@
+{
+ "common": {
+ "planId": "1"
+ },
+ "project": {
+ "input": {
+ "common": {
+ "planId": "0"
+ },
+ "localRelation": {
+ "schema":
"struct\u003cid:bigint,a:int,b:double,d:struct\u003cid:bigint,a:int,b:double\u003e,e:array\u003cint\u003e,f:map\u003cstring,struct\u003cid:bigint,a:int,b:double\u003e\u003e,g:string\u003e"
+ }
+ },
+ "expressions": [{
+ "unresolvedFunction": {
+ "functionName": "try_variant_insert",
+ "arguments": [{
+ "unresolvedFunction": {
+ "functionName": "parse_json",
+ "arguments": [{
+ "unresolvedAttribute": {
+ "unparsedIdentifier": "g"
+ },
+ "common": {
+ "origin": {
+ "jvmOrigin": {
+ "stackTrace": [{
+ "classLoaderName": "app",
+ "declaringClass": "org.apache.spark.sql.functions$",
+ "methodName": "col",
+ "fileName": "functions.scala"
+ }, {
+ "classLoaderName": "app",
+ "declaringClass":
"org.apache.spark.sql.PlanGenerationTestSuite",
+ "methodName": "~~trimmed~anonfun~~",
+ "fileName": "PlanGenerationTestSuite.scala"
+ }]
+ }
+ }
+ }
+ }],
+ "isInternal": false
+ },
+ "common": {
+ "origin": {
+ "jvmOrigin": {
+ "stackTrace": [{
+ "classLoaderName": "app",
+ "declaringClass": "org.apache.spark.sql.functions$",
+ "methodName": "parse_json",
+ "fileName": "functions.scala"
+ }, {
+ "classLoaderName": "app",
+ "declaringClass":
"org.apache.spark.sql.PlanGenerationTestSuite",
+ "methodName": "~~trimmed~anonfun~~",
+ "fileName": "PlanGenerationTestSuite.scala"
+ }]
+ }
+ }
+ }
+ }, {
+ "literal": {
+ "string": "$.a"
+ },
+ "common": {
+ "origin": {
+ "jvmOrigin": {
+ "stackTrace": [{
+ "classLoaderName": "app",
+ "declaringClass": "org.apache.spark.sql.functions$",
+ "methodName": "try_variant_insert",
+ "fileName": "functions.scala"
+ }, {
+ "classLoaderName": "app",
+ "declaringClass":
"org.apache.spark.sql.PlanGenerationTestSuite",
+ "methodName": "~~trimmed~anonfun~~",
+ "fileName": "PlanGenerationTestSuite.scala"
+ }]
+ }
+ }
+ }
+ }, {
+ "literal": {
+ "integer": 1
+ },
+ "common": {
+ "origin": {
+ "jvmOrigin": {
+ "stackTrace": [{
+ "classLoaderName": "app",
+ "declaringClass": "org.apache.spark.sql.functions$",
+ "methodName": "lit",
+ "fileName": "functions.scala"
+ }, {
+ "classLoaderName": "app",
+ "declaringClass":
"org.apache.spark.sql.PlanGenerationTestSuite",
+ "methodName": "~~trimmed~anonfun~~",
+ "fileName": "PlanGenerationTestSuite.scala"
+ }]
+ }
+ }
+ }
+ }],
+ "isInternal": false
+ },
+ "common": {
+ "origin": {
+ "jvmOrigin": {
+ "stackTrace": [{
+ "classLoaderName": "app",
+ "declaringClass": "org.apache.spark.sql.functions$",
+ "methodName": "try_variant_insert",
+ "fileName": "functions.scala"
+ }, {
+ "classLoaderName": "app",
+ "declaringClass": "org.apache.spark.sql.PlanGenerationTestSuite",
+ "methodName": "~~trimmed~anonfun~~",
+ "fileName": "PlanGenerationTestSuite.scala"
+ }]
+ }
+ }
+ }
+ }]
+ }
+}
\ No newline at end of file
diff --git
a/sql/connect/common/src/test/resources/query-tests/queries/function_try_variant_insert.proto.bin
b/sql/connect/common/src/test/resources/query-tests/queries/function_try_variant_insert.proto.bin
new file mode 100644
index 000000000000..77bccb64f2c2
Binary files /dev/null and
b/sql/connect/common/src/test/resources/query-tests/queries/function_try_variant_insert.proto.bin
differ
diff --git a/sql/core/src/test/resources/sql-functions/sql-expression-schema.md
b/sql/core/src/test/resources/sql-functions/sql-expression-schema.md
index 8e49cd70641e..ecdb01a2e034 100644
--- a/sql/core/src/test/resources/sql-functions/sql-expression-schema.md
+++ b/sql/core/src/test/resources/sql-functions/sql-expression-schema.md
@@ -555,9 +555,10 @@
| org.apache.spark.sql.catalyst.expressions.variant.ToVariantObject |
to_variant_object | SELECT to_variant_object(named_struct('a', 1, 'b', 2)) |
struct<to_variant_object(named_struct(a, 1, b, 2)):variant> |
|
org.apache.spark.sql.catalyst.expressions.variant.TryParseJsonExpressionBuilder
| try_parse_json | SELECT try_parse_json('{"a":1,"b":0.8}') |
struct<try_parse_json({"a":1,"b":0.8}):variant> |
|
org.apache.spark.sql.catalyst.expressions.variant.TryVariantGetExpressionBuilder
| try_variant_get | SELECT try_variant_get(parse_json('{"a": 1}'), '$.a',
'int') | struct<try_variant_get(parse_json({"a": 1}), $.a):int> |
+|
org.apache.spark.sql.catalyst.expressions.variant.TryVariantInsertExpressionBuilder
| try_variant_insert | SELECT try_variant_insert(parse_json('{"a": 1}'),
'$.b', 2) | struct<try_variant_insert(parse_json({"a": 1}), $.b, 2):variant> |
| org.apache.spark.sql.catalyst.expressions.variant.VariantDelete |
variant_delete | SELECT variant_delete(parse_json('{"a": 1, "b": 2, "c": 3,
"items": [1, 2, 3]}'), NULL, '$.a', '$.c') |
struct<variant_delete(parse_json({"a": 1, "b": 2, "c": 3, "items": [1, 2, 3]}),
NULL, $.a, $.c):variant> |
|
org.apache.spark.sql.catalyst.expressions.variant.VariantGetExpressionBuilder |
variant_get | SELECT variant_get(parse_json('{"a": 1}'), '$.a', 'int') |
struct<variant_get(parse_json({"a": 1}), $.a):int> |
-| org.apache.spark.sql.catalyst.expressions.variant.VariantInsert |
variant_insert | SELECT variant_insert(parse_json('{"a": 1}'), '$.b', 2) |
struct<variant_insert(parse_json({"a": 1}), $.b, 2):variant> |
+|
org.apache.spark.sql.catalyst.expressions.variant.VariantInsertExpressionBuilder
| variant_insert | SELECT variant_insert(parse_json('{"a": 1}'), '$.b', 2) |
struct<variant_insert(parse_json({"a": 1}), $.b, 2):variant> |
| org.apache.spark.sql.catalyst.expressions.xml.XPathBoolean | xpath_boolean |
SELECT xpath_boolean('<a><b>1</b></a>','a/b') |
struct<xpath_boolean(<a><b>1</b></a>, a/b):boolean> |
| org.apache.spark.sql.catalyst.expressions.xml.XPathDouble | xpath_double |
SELECT xpath_double('<a><b>1</b><b>2</b></a>', 'sum(a/b)') |
struct<xpath_double(<a><b>1</b><b>2</b></a>, sum(a/b)):double> |
| org.apache.spark.sql.catalyst.expressions.xml.XPathDouble | xpath_number |
SELECT xpath_number('<a><b>1</b><b>2</b></a>', 'sum(a/b)') |
struct<xpath_number(<a><b>1</b><b>2</b></a>, sum(a/b)):double> |
diff --git a/sql/core/src/test/scala/org/apache/spark/sql/VariantSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/VariantSuite.scala
index 0390507261f5..8d25633887f9 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/VariantSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/VariantSuite.scala
@@ -384,6 +384,61 @@ class VariantSuite extends SharedSparkSession with
ExpressionEvalHelper {
}
}
+ test("try_variant_insert with literal arguments") {
+ def rows(results: Any*): Seq[Row] = results.map(Row(_))
+
+ checkAnswer(
+ sql("SELECT to_json(try_variant_insert(parse_json('{\"a\": 1}'), '$.b',
2))"),
+ rows("""{"a":1,"b":2}"""))
+
+ checkAnswer(
+ sql("SELECT to_json(try_variant_insert(parse_json('{\"a\": 1}'), '$.a',
2))"),
+ rows(null))
+ checkAnswer(
+ sql("SELECT to_json(try_variant_insert(parse_json('{\"a\": 1}'),
'$.a.b', 2))"),
+ rows(null))
+
+ checkAnswer(
+ sql("SELECT to_json(try_variant_insert(parse_json('{\"a\": 1}'), '$[0]',
2))"),
+ rows(null))
+ checkAnswer(
+ sql("SELECT to_json(try_variant_insert(parse_json('{\"a\": 5}'),
'$.a[0]', 2))"),
+ rows(null))
+
+ checkAnswer(
+ sql("SELECT to_json(try_variant_insert(parse_json('{\"a\": 1}'), '$.b',
NULL))"),
+ rows(null))
+
+ checkError(
+ exception = intercept[SparkRuntimeException] {
+ sql("SELECT try_variant_insert(parse_json('{}'), '$', 1)").collect()
+ },
+ condition = "INVALID_VARIANT_PATH",
+ parameters = Map("path" -> "$", "functionName" ->
toSQLId("try_variant_insert")))
+ }
+
+ test("try_variant_insert with dynamic arguments") {
+ def rows(results: Any*): Seq[Row] = results.map(Row(_))
+ Seq("CODEGEN_ONLY", "NO_CODEGEN").foreach { codegenMode =>
+ withSQLConf(SQLConf.CODEGEN_FACTORY_MODE.key -> codegenMode) {
+ val df = Seq(
+ ("""{"a": 1}""", "$.b"),
+ ("""{"a": 1}""", "$.a"),
+ ("""{"a": 1}""", "$.a.b"),
+ (null, "$.b")
+ ).toDF("json", "path")
+ val v = parse_json(col("json"))
+ val out = df.select(to_json(try_variant_insert(v, col("path"),
lit(2))).alias("r"))
+ checkAnswer(out, rows("""{"a":1,"b":2}""", null, null, null))
+
+ val out2 = df.select(to_json(try_variant_insert(v, "$.b",
lit(2))).alias("r"))
+ checkAnswer(
+ out2,
+ rows("""{"a":1,"b":2}""", """{"a":1,"b":2}""", """{"a":1,"b":2}""",
null))
+ }
+ }
+ }
+
test("round trip tests") {
withSQLConf(SQLConf.VARIANT_INFER_SHREDDING_SCHEMA.key -> "false") {
val rand = new Random(42)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]