This is an automated email from the ASF dual-hosted git repository.
uros-b 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 759fbd9ea34c [SPARK-58221][SQL] Add `try_variant_set` expression
759fbd9ea34c is described below
commit 759fbd9ea34c2356e4d042b27d14557908d8c7d3
Author: bojana-db <[email protected]>
AuthorDate: Fri Jul 24 11:25:58 2026 +0000
[SPARK-58221][SQL] Add `try_variant_set` expression
### What changes were proposed in this pull request?
Adds the SQL function `try_variant_set(v, path, val[, create_if_missing])`,
the error-tolerant counterpart of `variant_set` (SPARK-57804). It sets (inserts
or replaces) a value in a Variant value at a single JSONPath location,
returning NULL instead of failing the query when the operation hits a
recoverable error.
Details:
- On a valid set, behaves exactly like `variant_set`: an object path (e.g.
`$.a`) replaces the field if it exists and creates it when `create_if_missing`
is true (the default); an array path (e.g. `$[N]`) replaces the element at
index `N`, and when `N` is at or past the end and `create_if_missing` is true,
pads the array with variant nulls up to `N`; missing intermediate keys/indices
along the path are created when `create_if_missing` is true;
- When `create_if_missing` is false, a missing leaf, a missing
intermediate, or an out-of-range array index leaves `v` unchanged;
- Recoverable errors are caught and return NULL instead of throwing: any
path type mismatch (`VARIANT_PATH_TYPE_MISMATCH`, e.g. an object key applied to
an array);
- Non-recoverable errors still throw, matching `variant_set`: a malformed
path, including the root path `$`, is rejected with `INVALID_VARIANT_PATH` (the
path is parsed before the set), 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_set`, matching `try_variant_get`,
`try_variant_insert`, and other `try_*` functions.
### Does this PR introduce _any_ user-facing change?
Yes, a new SQL function `try_variant_set` (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 #57373 from bojana-db/try-variant-set.
Authored-by: bojana-db <[email protected]>
Signed-off-by: Uros Bojanic <[email protected]>
---
.../source/reference/pyspark.sql/functions.rst | 1 +
python/pyspark/sql/connect/functions/builtin.py | 15 ++
python/pyspark/sql/functions/__init__.py | 1 +
python/pyspark/sql/functions/builtin.py | 65 ++++++++
python/pyspark/sql/tests/test_functions.py | 13 ++
.../scala/org/apache/spark/sql/functions.scala | 86 +++++++++++
.../sql/catalyst/analysis/FunctionRegistry.scala | 1 +
.../variant/VariantExpressionEvalUtils.scala | 14 +-
.../expressions/variant/variantExpressions.scala | 120 +++++++++++----
.../variant/VariantExpressionSuite.scala | 168 ++++++++++++++-------
.../apache/spark/sql/PlanGenerationTestSuite.scala | 8 +
.../function_try_variant_set.explain | 2 +
..._try_variant_set_with_create_if_missing.explain | 2 +
.../explain-results/function_variant_set.explain | 2 +-
...tion_variant_set_with_create_if_missing.explain | 2 +-
.../queries/function_try_variant_set.json | 125 +++++++++++++++
.../queries/function_try_variant_set.proto.bin | Bin 0 -> 1142 bytes
...ion_try_variant_set_with_create_if_missing.json | 146 ++++++++++++++++++
...ry_variant_set_with_create_if_missing.proto.bin | Bin 0 -> 1338 bytes
.../sql-functions/sql-expression-schema.md | 1 +
.../scala/org/apache/spark/sql/VariantSuite.scala | 49 ++++++
21 files changed, 728 insertions(+), 93 deletions(-)
diff --git a/python/docs/source/reference/pyspark.sql/functions.rst
b/python/docs/source/reference/pyspark.sql/functions.rst
index 7ce413fde0bb..91b722d50efd 100644
--- a/python/docs/source/reference/pyspark.sql/functions.rst
+++ b/python/docs/source/reference/pyspark.sql/functions.rst
@@ -609,6 +609,7 @@ VARIANT Functions
variant_insert
try_variant_insert
variant_set
+ try_variant_set
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 acf1ee6b069f..f3e1a0c058f2 100644
--- a/python/pyspark/sql/connect/functions/builtin.py
+++ b/python/pyspark/sql/connect/functions/builtin.py
@@ -2252,6 +2252,21 @@ def variant_set(
variant_set.__doc__ = pysparkfuncs.variant_set.__doc__
+def try_variant_set(
+ v: "ColumnOrName",
+ path: Union[Column, str],
+ value: "ColumnOrName",
+ create_if_missing: bool = True,
+) -> Column:
+ path_col = path if isinstance(path, Column) else lit(path)
+ return _invoke_function(
+ "try_variant_set", _to_col(v), path_col, _to_col(value),
lit(create_if_missing)
+ )
+
+
+try_variant_set.__doc__ = pysparkfuncs.try_variant_set.__doc__
+
+
def variant_array_append(
v: "ColumnOrName", path: Union[Column, str], value: "ColumnOrName"
) -> Column:
diff --git a/python/pyspark/sql/functions/__init__.py
b/python/pyspark/sql/functions/__init__.py
index 7780bae38ee9..50f0fd440dd9 100644
--- a/python/pyspark/sql/functions/__init__.py
+++ b/python/pyspark/sql/functions/__init__.py
@@ -487,6 +487,7 @@ __all__ = [ # noqa: F405
"variant_insert",
"try_variant_insert",
"variant_set",
+ "try_variant_set",
"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 3cd42dc2e0e2..a9f3769b0831 100644
--- a/python/pyspark/sql/functions/builtin.py
+++ b/python/pyspark/sql/functions/builtin.py
@@ -22853,6 +22853,71 @@ def variant_set(
)
+@_try_remote_functions
+def try_variant_set(
+ v: "ColumnOrName",
+ path: Union[Column, str],
+ value: "ColumnOrName",
+ create_if_missing: bool = True,
+) -> Column:
+ """
+ Sets or upserts a value in a variant at the given JSONPath location. An
existing object field
+ or array element at the target is replaced. A missing field, array index,
or intermediate path
+ is created, unless `create_if_missing` is false, in which case the variant
is left unchanged.
+ Returns NULL if 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 set 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 set. Any expression castable to variant.
+ create_if_missing : bool, optional
+ whether to create missing keys or out-of-range array indices (default
True).
+
+ Returns
+ -------
+ :class:`~pyspark.sql.Column`
+ a variant column with `value` set at `path`
+
+ Examples
+ --------
+ >>> from pyspark.sql.functions import lit, parse_json, to_json,
try_variant_set
+ >>> df = spark.createDataFrame([{'json': '''{"a": 1, "arr": [1, 2,
3]}'''}])
+ >>> v = parse_json(df.json)
+ >>> df.select(to_json(try_variant_set(v, "$.a",
lit(9))).alias("r")).collect()
+ [Row(r='{"a":9,"arr":[1,2,3]}')]
+ >>> df.select(to_json(try_variant_set(v, "$.b",
lit(2))).alias("r")).collect()
+ [Row(r='{"a":1,"arr":[1,2,3],"b":2}')]
+ >>> df.select(to_json(try_variant_set(v, "$.arr[1]",
lit(9))).alias("r")).collect()
+ [Row(r='{"a":1,"arr":[1,9,3]}')]
+ >>> df.select(to_json(try_variant_set(v, "$.arr[5]",
lit(9))).alias("r")).collect()
+ [Row(r='{"a":1,"arr":[1,2,3,null,null,9]}')]
+ >>> df.select(to_json(try_variant_set(v, "$.b", lit(2),
False)).alias("r")).collect()
+ [Row(r='{"a":1,"arr":[1,2,3]}')]
+ >>> df.select(to_json(try_variant_set(v, "$.a.b",
lit(9))).alias("r")).collect()
+ [Row(r=None)]
+ >>> df.select(to_json(try_variant_set(v, "$.a",
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_set",
+ _to_java_column(v),
+ _to_java_column(path_col),
+ _to_java_column(value),
+ _enum_to_value(create_if_missing),
+ )
+
+
@_try_remote_functions
def variant_array_append(
v: "ColumnOrName", path: Union[Column, str], value: "ColumnOrName"
diff --git a/python/pyspark/sql/tests/test_functions.py
b/python/pyspark/sql/tests/test_functions.py
index 2594c8c60b76..d3a4072c707e 100644
--- a/python/pyspark/sql/tests/test_functions.py
+++ b/python/pyspark/sql/tests/test_functions.py
@@ -3559,6 +3559,19 @@ class FunctionsTestsMixin:
df.select(F.to_json(F.variant_set(v, df.newpath, F.lit(9)))),
['{"a":1,"z":9}', '{"b":2,"z":9}'],
)
+ check(
+ df.select(F.to_json(F.try_variant_set(v, "$.z", F.lit(9)))),
+ ['{"a":1,"z":9}', '{"b":2,"z":9}'],
+ )
+ check(
+ df.select(F.to_json(F.try_variant_set(v, "$.z", F.lit(9), False))),
+ ['{"a":1}', '{"b":2}'],
+ )
+ check(df.select(F.to_json(F.try_variant_set(v, "$[0]", F.lit(9)))),
[None, None])
+ check(
+ df.select(F.to_json(F.try_variant_set(v, df.newpath, F.lit(9)))),
+ ['{"a":1,"z":9}', '{"b":2,"z":9}'],
+ )
arr = F.parse_json(df.arr)
check(
df.select(F.to_json(F.variant_array_append(arr, "$", F.lit(9)))),
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 8b8cd98960e0..3961a2d9de1d 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
@@ -14324,6 +14324,92 @@ object functions {
def variant_set(v: Column, path: String, value: Column, createIfMissing:
Boolean): Column =
Column.fn("variant_set", v, lit(path), value, lit(createIfMissing))
+ /**
+ * Sets or upserts a value in a variant at the given JSONPath location. An
existing object field
+ * or array element at the target is replaced. A missing field, array index,
or intermediate
+ * path is created. Returns NULL if 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 set 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 set. Any expression castable to variant.
+ * @group variant_funcs
+ * @since 4.3.0
+ */
+ def try_variant_set(v: Column, path: Column, value: Column): Column =
+ Column.fn("try_variant_set", v, path, value)
+
+ /**
+ * Sets or upserts a value in a variant at the given JSONPath location. An
existing object field
+ * or array element at the target is replaced. A missing field, array index,
or intermediate
+ * path is created. Returns NULL if 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 set 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 set. Any expression castable to variant.
+ * @group variant_funcs
+ * @since 4.3.0
+ */
+ def try_variant_set(v: Column, path: String, value: Column): Column =
+ Column.fn("try_variant_set", v, lit(path), value)
+
+ /**
+ * Sets or upserts a value in a variant at the given JSONPath location. An
existing object field
+ * or array element at the target is replaced. A missing field, array index,
or intermediate
+ * path is created, unless `createIfMissing` is false, in which case the
variant is left
+ * unchanged. Returns NULL if 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 set 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 set. Any expression castable to variant.
+ * @param createIfMissing
+ * whether to create missing keys or out-of-range array indices.
+ * @group variant_funcs
+ * @since 4.3.0
+ */
+ def try_variant_set(v: Column, path: Column, value: Column, createIfMissing:
Boolean): Column =
+ Column.fn("try_variant_set", v, path, value, lit(createIfMissing))
+
+ /**
+ * Sets or upserts a value in a variant at the given JSONPath location. An
existing object field
+ * or array element at the target is replaced. A missing field, array index,
or intermediate
+ * path is created, unless `createIfMissing` is false, in which case the
variant is left
+ * unchanged. Returns NULL if 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 set 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 set. Any expression castable to variant.
+ * @param createIfMissing
+ * whether to create missing keys or out-of-range array indices.
+ * @group variant_funcs
+ * @since 4.3.0
+ */
+ def try_variant_set(v: Column, path: String, value: Column, createIfMissing:
Boolean): Column =
+ Column.fn("try_variant_set", v, lit(path), value, lit(createIfMissing))
+
/**
* Appends a value to the array in a variant at the given JSONPath location.
Returns the variant
* unchanged if a path key or index is absent. Throws an error if a path
segment hits a value of
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 5b39aa06f1e4..965330db5828 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
@@ -990,6 +990,7 @@ object FunctionRegistry {
expressionBuilder("variant_insert", VariantInsertExpressionBuilder),
expressionBuilder("try_variant_insert", TryVariantInsertExpressionBuilder),
expressionBuilder("variant_set", VariantSetExpressionBuilder),
+ expressionBuilder("try_variant_set", TryVariantSetExpressionBuilder),
expressionBuilder("variant_array_append",
VariantArrayAppendExpressionBuilder),
expressionBuilder("try_variant_array_append",
TryVariantArrayAppendExpressionBuilder),
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 0872cda99801..da1bfbcea829 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
@@ -183,7 +183,9 @@ object VariantExpressionEvalUtils {
* Set `input` at `javaSegments` to `value`. `path` is the source string
used in error messages.
* The cast and set share one try, so any size overflow maps to
`VARIANT_SIZE_LIMIT` and a type
* mismatch maps to `VARIANT_PATH_TYPE_MISMATCH`. When `createIfMissing` is
false, a missing
- * key/index leaves the variant unchanged.
+ * key/index leaves the variant unchanged. When `failOnError` is false (the
`try_variant_set`
+ * mode), a path type mismatch returns null instead of throwing; a size
overflow (and a malformed
+ * path, rejected earlier during parsing) is still raised.
*/
def setAtPath(
input: VariantVal,
@@ -192,7 +194,8 @@ object VariantExpressionEvalUtils {
value: Any,
valueDataType: DataType,
createIfMissing: Boolean,
- functionName: String): VariantVal = {
+ functionName: String,
+ failOnError: Boolean): VariantVal = {
val v = new Variant(input.getValue, input.getMetadata)
try {
val valVal = castToVariant(value, valueDataType)
@@ -200,6 +203,7 @@ object VariantExpressionEvalUtils {
val out = VariantBuilder.setAtPath(v, javaSegments, valVariant,
createIfMissing)
new VariantVal(out.getValue, out.getMetadata)
} catch {
+ case _: VariantPathTypeMismatchException if !failOnError => null
case e: VariantPathTypeMismatchException =>
throw QueryExecutionErrors.variantPathTypeMismatch(
path, renderVariantPath(javaSegments.take(e.depth)), functionName)
@@ -214,10 +218,12 @@ object VariantExpressionEvalUtils {
value: Any,
valueDataType: DataType,
createIfMissing: Boolean,
- functionName: String): VariantVal = {
+ functionName: String,
+ failOnError: Boolean): VariantVal = {
val pathStr = path.toString
val javaSegments = toJavaSegments(parseVariantPath(pathStr, functionName))
- setAtPath(input, javaSegments, pathStr, value, valueDataType,
createIfMissing, functionName)
+ setAtPath(input, javaSegments, pathStr, value, valueDataType,
createIfMissing, functionName,
+ failOnError)
}
/**
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 fa6a955097d9..e9ce291b655c 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
@@ -1025,7 +1025,8 @@ case class VariantSet(
input: Expression,
path: Expression,
value: Expression,
- createIfMissing: Expression)
+ createIfMissing: Expression,
+ failOnError: Boolean = true)
extends QuaternaryExpression
with ExpectsInputTypes
with QueryErrorsBase {
@@ -1036,6 +1037,7 @@ case class VariantSet(
override def fourth: Expression = createIfMissing
override def nullIntolerant: Boolean = true
+ override def nullable: Boolean = !failOnError || children.exists(_.nullable)
override def dataType: DataType = VariantType
override def inputTypes: Seq[AbstractDataType] =
@@ -1080,6 +1082,11 @@ case class VariantSet(
}
}
+ override def eval(input: InternalRow): Any = {
+ val _ = foldablePath
+ super.eval(input)
+ }
+
override protected def nullSafeEval(v: Any, p: Any, valValue: Any, create:
Any): Any = {
val inputVariant = v.asInstanceOf[VariantVal]
val createIfMissingValue = create.asInstanceOf[Boolean]
@@ -1087,36 +1094,48 @@ case class VariantSet(
case Some(parsed) =>
VariantExpressionEvalUtils.setAtPath(
inputVariant, parsed.javaSegments, parsed.pathStr, valValue,
value.dataType,
- createIfMissingValue, prettyName)
+ createIfMissingValue, prettyName, failOnError)
case None =>
VariantExpressionEvalUtils.setAtPath(
inputVariant, p.asInstanceOf[UTF8String], valValue, value.dataType,
- createIfMissingValue, prettyName)
+ createIfMissingValue, prettyName, failOnError)
}
}
override protected def doGenCode(ctx: CodegenContext, ev: ExprCode):
ExprCode = {
val cls = VariantExpressionEvalUtils.getClass.getName.stripSuffix("$")
- nullSafeCodeGen(ctx, ev, (vVal, pVal, valVal, createVal) => {
- val fromArg = ctx.addReferenceObj("from", value.dataType)
+ val fromArg = ctx.addReferenceObj("from", value.dataType)
+ def call(vVal: String, pVal: String, valVal: String, createVal: String):
String =
foldablePath match {
case Some(parsed) =>
val parsedArg = ctx.addReferenceObj("setPath", parsed)
- s"""
- |${ev.value} = $cls.setAtPath(
+ s"""$cls.setAtPath(
| $vVal, $parsedArg.javaSegments(), $parsedArg.pathStr(),
$valVal, $fromArg,
- | $createVal, "$prettyName");
- """.stripMargin
+ | $createVal, "$prettyName", $failOnError)""".stripMargin
case None =>
- s"""
- |${ev.value} = $cls.setAtPath(
- | $vVal, $pVal, $valVal, $fromArg, $createVal, "$prettyName");
- """.stripMargin
+ s"""$cls.setAtPath(
+ | $vVal, $pVal, $valVal, $fromArg, $createVal, "$prettyName",
$failOnError)"""
+ .stripMargin
}
- })
+ if (failOnError) {
+ nullSafeCodeGen(ctx, ev,
+ (vVal, pVal, valVal, createVal) => s"${ev.value} = ${call(vVal, pVal,
valVal, createVal)};")
+ } else {
+ val resultType = CodeGenerator.javaType(VariantType)
+ val tmp = ctx.freshName("setResult")
+ nullSafeCodeGen(ctx, ev, (vVal, pVal, valVal, createVal) =>
+ s"""
+ |$resultType $tmp = ${call(vVal, pVal, valVal, createVal)};
+ |if ($tmp == null) {
+ | ${ev.isNull} = true;
+ |} else {
+ | ${ev.value} = $tmp;
+ |}
+ """.stripMargin)
+ }
}
- override def prettyName: String = "variant_set"
+ override def prettyName: String = if (failOnError) "variant_set" else
"try_variant_set"
override protected def withNewChildrenInternal(
newFirst: Expression,
@@ -1136,6 +1155,22 @@ object VariantSet {
}
}
+abstract class VariantSetExpressionBuilderBase(failOnError: Boolean) extends
ExpressionBuilder {
+ override def functionSignature: Option[FunctionSignature] = {
+ val vArg = InputParameter("v")
+ val pathArg = InputParameter("path")
+ val valArg = InputParameter("val")
+ val createIfMissingArg =
+ InputParameter("create_if_missing", Some(Literal.create(true,
BooleanType)))
+ Some(FunctionSignature(Seq(vArg, pathArg, valArg, createIfMissingArg)))
+ }
+
+ override def build(funcName: String, expressions: Seq[Expression]):
Expression = {
+ assert(expressions.size == 4)
+ VariantSet(expressions(0), expressions(1), expressions(2), expressions(3),
failOnError)
+ }
+}
+
// scalastyle:off line.size.limit
@ExpressionDescription(
usage = "_FUNC_(v, path, val[, create_if_missing]) - Sets or upserts a value
in a variant at " +
@@ -1176,21 +1211,48 @@ object VariantSet {
group = "variant_funcs"
)
// scalastyle:on line.size.limit
-object VariantSetExpressionBuilder extends ExpressionBuilder {
- override def functionSignature: Option[FunctionSignature] = {
- val vArg = InputParameter("v")
- val pathArg = InputParameter("path")
- val valArg = InputParameter("val")
- val createIfMissingArg =
- InputParameter("create_if_missing", Some(Literal.create(true,
BooleanType)))
- Some(FunctionSignature(Seq(vArg, pathArg, valArg, createIfMissingArg)))
- }
+object VariantSetExpressionBuilder extends
VariantSetExpressionBuilderBase(true)
- override def build(funcName: String, expressions: Seq[Expression]):
Expression = {
- assert(expressions.size == 4)
- VariantSet(expressions(0), expressions(1), expressions(2), expressions(3))
- }
-}
+// scalastyle:off line.size.limit
+@ExpressionDescription(
+ usage = "_FUNC_(v, path, val[, create_if_missing]) - Sets or upserts a value
in a variant at " +
+ "the given JSONPath location. An existing object field or array element at
the target is " +
+ "replaced. A missing field, array index, or intermediate path is created,
unless " +
+ "`create_if_missing` is false, in which case the variant is left
unchanged. Returns NULL if " +
+ "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
set 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.
+ * create_if_missing - An optional boolean (default true).
+ """,
+ examples = """
+ Examples:
+ > SELECT _FUNC_(parse_json('{"a": 1}'), '$.a', 2);
+ {"a":2}
+ > SELECT _FUNC_(parse_json('{"a": 1}'), '$.b', 3);
+ {"a":1,"b":3}
+ > SELECT _FUNC_(parse_json('{"a": {"b": 1}}'), '$.a.c.d', 2);
+ {"a":{"b":1,"c":{"d":2}}}
+ > SELECT _FUNC_(parse_json('["a","b","c"]'), '$[1]', 'z');
+ ["a","z","c"]
+ > SELECT _FUNC_(parse_json('["a","b","c"]'), '$[5]', 'z');
+ ["a","b","c",null,null,"z"]
+ > SELECT _FUNC_(parse_json('{"a": 1}'), '$.b', 2, false);
+ {"a":1}
+ > SELECT _FUNC_(parse_json('{"a": 1}'), '$.a.b', 2);
+ NULL
+ > SELECT _FUNC_(parse_json('{"a": 1}'), '$.a', null);
+ NULL
+ """,
+ since = "4.3.0",
+ group = "variant_funcs"
+)
+// scalastyle:on line.size.limit
+object TryVariantSetExpressionBuilder extends
VariantSetExpressionBuilderBase(false)
case class VariantArrayAppend(
input: Expression,
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 92f20bcc3127..eaf9ac267ff0 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
@@ -1503,14 +1503,55 @@ class VariantExpressionSuite extends SparkFunSuite with
ExpressionEvalHelper {
value: Expression,
expected: String,
createIfMissing: Boolean = true): Unit = {
- val expr = VariantSet(
- Literal(parseJson(input)),
- Literal.create(path, StringType),
- value,
- Literal(createIfMissing))
+ // On success `variant_set` and `try_variant_set` behave identically.
+ Seq(true, false).foreach { failOnError =>
+ val expr = VariantSet(
+ Literal(parseJson(input)),
+ Literal.create(path, StringType),
+ value,
+ Literal(createIfMissing),
+ failOnError)
+ checkEvaluation(
+ ResolveTimeZone.resolveTimeZones(Cast(expr, StringType)),
+ expected)
+ }
+ }
+
+ // A recoverable error: `variant_set` throws, but `try_variant_set`
returns NULL.
+ def checkSetRecoverableError(
+ input: String,
+ path: String,
+ value: Expression,
+ condition: String,
+ parameters: Map[String, String],
+ createIfMissing: Boolean = true): Unit = {
+ checkErrorInExpression[SparkRuntimeException](
+ VariantSet(Literal(parseJson(input)), Literal.create(path,
StringType), value,
+ Literal(createIfMissing)),
+ condition,
+ parameters)
checkEvaluation(
- ResolveTimeZone.resolveTimeZones(Cast(expr, StringType)),
- expected)
+ VariantSet(Literal(parseJson(input)), Literal.create(path,
StringType), value,
+ Literal(createIfMissing), 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 checkSetUnrecoverableError(
+ input: String,
+ path: String,
+ value: Expression,
+ condition: String,
+ parameters: String => Map[String, String]): Unit = {
+ Seq("variant_set" -> true, "try_variant_set" -> false).foreach {
+ case (name, failOnError) =>
+ checkErrorInExpression[SparkRuntimeException](
+ VariantSet(Literal(parseJson(input)), Literal.create(path,
StringType), value,
+ Literal(true), failOnError),
+ condition,
+ parameters(name))
+ }
}
// Object sets: replace an existing key, or add a missing one.
@@ -1561,59 +1602,57 @@ class VariantExpressionSuite extends SparkFunSuite with
ExpressionEvalHelper {
checkSet(
"{}", "$.a", Literal.create(Array(1, 2, 3), ArrayType(IntegerType)),
"""{"a":[1,2,3]}""")
- // NULL-intolerant: any NULL argument yields NULL.
- checkEvaluation(
- VariantSet(Literal.create(null, VariantType), Literal("$.a"),
Literal(1), Literal(true)),
- null)
+ Seq(true, false).foreach { failOnError =>
+ checkEvaluation(
+ VariantSet(Literal.create(null, VariantType), Literal("$.a"),
Literal(1), Literal(true),
+ failOnError),
+ null)
+ checkEvaluation(
+ VariantSet(Literal(parseJson("""{"a": 1}""")), Literal("$.a"),
Literal(2),
+ Literal.create(null, BooleanType), failOnError),
+ null)
+ }
checkSet("""{"a": 1}""", null, Literal(1), null)
checkSet("""{"a": 1}""", "$.a", Literal.create(null, VariantType), null)
checkSet("""{"a": 1}""", "$.a", Literal.create(null, NullType), null)
- checkEvaluation(
- VariantSet(Literal(parseJson("""{"a": 1}""")), Literal("$.a"),
Literal(2),
- Literal.create(null, BooleanType)),
- null)
- // Dynamic (non-foldable) path and create_if_missing.
- val dynamic = VariantSet(
- Literal(parseJson("""{"a": 1}""")),
- BoundReference(0, StringType, nullable = true),
- Literal(2),
- BoundReference(1, BooleanType, nullable = true))
- checkEvaluation(
- ResolveTimeZone.resolveTimeZones(Cast(dynamic, StringType)),
- """{"a":1,"b":2}""",
- InternalRow(UTF8String.fromString("$.b"), true))
- checkEvaluation(
- ResolveTimeZone.resolveTimeZones(Cast(dynamic, StringType)),
- """{"a":1}""",
- InternalRow(UTF8String.fromString("$.b"), false))
+ Seq(true, false).foreach { failOnError =>
+ val dynamic = VariantSet(
+ Literal(parseJson("""{"a": 1}""")),
+ BoundReference(0, StringType, nullable = true),
+ Literal(2),
+ BoundReference(1, BooleanType, nullable = true),
+ failOnError)
+ checkEvaluation(
+ ResolveTimeZone.resolveTimeZones(Cast(dynamic, StringType)),
+ """{"a":1,"b":2}""",
+ InternalRow(UTF8String.fromString("$.b"), true))
+ checkEvaluation(
+ ResolveTimeZone.resolveTimeZones(Cast(dynamic, StringType)),
+ """{"a":1}""",
+ InternalRow(UTF8String.fromString("$.b"), false))
+ }
- // Type mismatch throws regardless of create_if_missing.
+ // Recoverable errors: `variant_set` throws; `try_variant_set` returns
NULL. Every shape of
+ // path type mismatch is recoverable, regardless of create_if_missing.
Seq(true, false).foreach { create =>
- checkErrorInExpression[SparkRuntimeException](
- VariantSet(Literal(parseJson("""{"a": 1}""")), Literal("$.a.b"),
Literal(2),
- Literal(create)),
+ checkSetRecoverableError("""{"a": 1}""", "$.a.b", Literal(2),
"VARIANT_PATH_TYPE_MISMATCH",
- Map("path" -> "$.a.b", "failedAt" -> "$.a", "functionName" ->
"`variant_set`"))
+ Map("path" -> "$.a.b", "failedAt" -> "$.a", "functionName" ->
"`variant_set`"),
+ create)
}
- checkErrorInExpression[SparkRuntimeException](
- VariantSet(Literal(parseJson("5")), Literal("$.a"), Literal(2),
Literal(true)),
+ checkSetRecoverableError("5", "$.a", Literal(2),
"VARIANT_PATH_TYPE_MISMATCH",
Map("path" -> "$.a", "failedAt" -> "$", "functionName" ->
"`variant_set`"))
-
// Segment kind not matching the container: array index on an object,
object key on an array.
- checkErrorInExpression[SparkRuntimeException](
- VariantSet(Literal(parseJson("""{"a": 1}""")), Literal("$[0]"),
Literal(2), Literal(true)),
+ checkSetRecoverableError("""{"a": 1}""", "$[0]", Literal(2),
"VARIANT_PATH_TYPE_MISMATCH",
Map("path" -> "$[0]", "failedAt" -> "$", "functionName" ->
"`variant_set`"))
- checkErrorInExpression[SparkRuntimeException](
- VariantSet(Literal(parseJson("[1, 2]")), Literal("$.a"), Literal(2),
Literal(true)),
+ checkSetRecoverableError("[1, 2]", "$.a", Literal(2),
"VARIANT_PATH_TYPE_MISMATCH",
Map("path" -> "$.a", "failedAt" -> "$", "functionName" ->
"`variant_set`"))
// A key that needs bracket notation is rendered with brackets in
`failedAt`.
- checkErrorInExpression[SparkRuntimeException](
- VariantSet(Literal(parseJson("""{"a.b": 5}""")),
Literal("""$['a.b'].c"""), Literal(2),
- Literal(true)),
+ checkSetRecoverableError("""{"a.b": 5}""", """$['a.b'].c""", Literal(2),
"VARIANT_PATH_TYPE_MISMATCH",
Map("path" -> "$['a.b'].c", "failedAt" -> "$['a.b']", "functionName" ->
"`variant_set`"))
@@ -1622,9 +1661,11 @@ class VariantExpressionSuite extends SparkFunSuite with
ExpressionEvalHelper {
Literal.create(null, MapType(StringType, IntegerType)),
Literal.create(null, StructType(Seq(StructField("x", IntegerType))))
).foreach { v =>
- assert(
- VariantSet(Literal(parseJson("{}")), Literal("$.a"), v, Literal(true))
- .checkInputDataTypes().isFailure)
+ Seq(true, false).foreach { failOnError =>
+ assert(
+ VariantSet(Literal(parseJson("{}")), Literal("$.a"), v,
Literal(true), failOnError)
+ .checkInputDataTypes().isFailure)
+ }
}
val structVal = ToVariantObject(Literal.create(create_row(1, "x"),
@@ -1639,20 +1680,31 @@ class VariantExpressionSuite extends SparkFunSuite with
ExpressionEvalHelper {
checkSet("""{"k": 1}""", "$.k", mapVal, """{"k":{"x":3,"y":2,"z":1}}""")
checkSet("""{"a": {"b": 1}}""", "$.a.b", structVal,
"""{"a":{"b":{"a":1,"b":"x"}}}""")
- checkErrorInExpression[SparkRuntimeException](
- VariantSet(Literal(parseJson("{}")), Literal("$"), Literal(1),
Literal(true)),
+ // Unrecoverable errors: both functions throw.
+ checkSetUnrecoverableError("{}", "$", Literal(1),
"INVALID_VARIANT_PATH",
- Map("path" -> "$", "functionName" -> "`variant_set`"))
-
- checkErrorInExpression[SparkRuntimeException](
- VariantSet(Literal(parseJson("{}")), Literal("$.a[2000000000]"),
Literal(1), Literal(true)),
- "VARIANT_SIZE_LIMIT",
- Map("sizeLimit" -> "16.0 MiB", "functionName" -> "`variant_set`"))
+ name => Map("path" -> "$", "functionName" -> s"`$name`"))
val tooBig = "x".repeat(16 * 1024 * 1024)
- checkErrorInExpression[SparkRuntimeException](
- VariantSet(Literal(parseJson("{}")), Literal("$.a"), Literal(tooBig),
Literal(true)),
+ checkSetUnrecoverableError("{}", "$.a[2000000000]", Literal(1),
+ "VARIANT_SIZE_LIMIT",
+ name => Map("sizeLimit" -> "16.0 MiB", "functionName" -> s"`$name`"))
+ checkSetUnrecoverableError("{}", "$.a", Literal(tooBig),
"VARIANT_SIZE_LIMIT",
- Map("sizeLimit" -> "16.0 MiB", "functionName" -> "`variant_set`"))
+ name => Map("sizeLimit" -> "16.0 MiB", "functionName" -> s"`$name`"))
+
+ Seq("variant_set" -> true, "try_variant_set" -> false).foreach {
+ case (name, failOnError) =>
+ checkErrorInExpression[SparkRuntimeException](
+ VariantSet(
+ BoundReference(0, VariantType, nullable = true),
+ Literal.create("$", StringType),
+ Literal(1),
+ Literal(true),
+ failOnError),
+ InternalRow(null),
+ "INVALID_VARIANT_PATH",
+ Map("path" -> "$", "functionName" -> s"`$name`"))
+ }
}
test("variant_array_append") {
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 068460293f15..266de35706da 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
@@ -2779,6 +2779,14 @@ class PlanGenerationTestSuite extends ConnectFunSuite
with Logging {
fn.variant_set(fn.parse_json(fn.col("g")), "$.a", fn.lit(1), false)
}
+ functionTest("try_variant_set") {
+ fn.try_variant_set(fn.parse_json(fn.col("g")), "$.a", fn.lit(1))
+ }
+
+ functionTest("try_variant_set with create_if_missing") {
+ fn.try_variant_set(fn.parse_json(fn.col("g")), "$.a", fn.lit(1), false)
+ }
+
functionTest("variant_array_append") {
fn.variant_array_append(fn.parse_json(fn.col("g")), "$.a", fn.lit(1))
}
diff --git
a/sql/connect/common/src/test/resources/query-tests/explain-results/function_try_variant_set.explain
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_try_variant_set.explain
new file mode 100644
index 000000000000..30503b185e1e
--- /dev/null
+++
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_try_variant_set.explain
@@ -0,0 +1,2 @@
+Project
[try_variant_set(static_invoke(VariantExpressionEvalUtils.parseJson(g#0, false,
true, true)), $.a, 1, true, false) AS try_variant_set(parse_json(g), $.a, 1,
true)#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_try_variant_set_with_create_if_missing.explain
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_try_variant_set_with_create_if_missing.explain
new file mode 100644
index 000000000000..831e27641964
--- /dev/null
+++
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_try_variant_set_with_create_if_missing.explain
@@ -0,0 +1,2 @@
+Project
[try_variant_set(static_invoke(VariantExpressionEvalUtils.parseJson(g#0, false,
true, true)), $.a, 1, false, false) AS try_variant_set(parse_json(g), $.a, 1,
false)#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_set.explain
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_variant_set.explain
index 3f695273d79b..37dbd79d5071 100644
---
a/sql/connect/common/src/test/resources/query-tests/explain-results/function_variant_set.explain
+++
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_variant_set.explain
@@ -1,2 +1,2 @@
-Project [variant_set(static_invoke(VariantExpressionEvalUtils.parseJson(g#0,
false, true, true)), $.a, 1, true) AS variant_set(parse_json(g), $.a, 1,
true)#0]
+Project [variant_set(static_invoke(VariantExpressionEvalUtils.parseJson(g#0,
false, true, true)), $.a, 1, true, true) AS variant_set(parse_json(g), $.a, 1,
true)#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_set_with_create_if_missing.explain
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_variant_set_with_create_if_missing.explain
index 2ad5e4938e92..c0a38e55e436 100644
---
a/sql/connect/common/src/test/resources/query-tests/explain-results/function_variant_set_with_create_if_missing.explain
+++
b/sql/connect/common/src/test/resources/query-tests/explain-results/function_variant_set_with_create_if_missing.explain
@@ -1,2 +1,2 @@
-Project [variant_set(static_invoke(VariantExpressionEvalUtils.parseJson(g#0,
false, true, true)), $.a, 1, false) AS variant_set(parse_json(g), $.a, 1,
false)#0]
+Project [variant_set(static_invoke(VariantExpressionEvalUtils.parseJson(g#0,
false, true, true)), $.a, 1, false, true) AS variant_set(parse_json(g), $.a, 1,
false)#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_set.json
b/sql/connect/common/src/test/resources/query-tests/queries/function_try_variant_set.json
new file mode 100644
index 000000000000..5f539f810892
--- /dev/null
+++
b/sql/connect/common/src/test/resources/query-tests/queries/function_try_variant_set.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_set",
+ "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_set",
+ "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_set",
+ "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_set.proto.bin
b/sql/connect/common/src/test/resources/query-tests/queries/function_try_variant_set.proto.bin
new file mode 100644
index 000000000000..074a58129fe5
Binary files /dev/null and
b/sql/connect/common/src/test/resources/query-tests/queries/function_try_variant_set.proto.bin
differ
diff --git
a/sql/connect/common/src/test/resources/query-tests/queries/function_try_variant_set_with_create_if_missing.json
b/sql/connect/common/src/test/resources/query-tests/queries/function_try_variant_set_with_create_if_missing.json
new file mode 100644
index 000000000000..323e483f9713
--- /dev/null
+++
b/sql/connect/common/src/test/resources/query-tests/queries/function_try_variant_set_with_create_if_missing.json
@@ -0,0 +1,146 @@
+{
+ "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_set",
+ "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_set",
+ "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"
+ }]
+ }
+ }
+ }
+ }, {
+ "literal": {
+ "boolean": false
+ },
+ "common": {
+ "origin": {
+ "jvmOrigin": {
+ "stackTrace": [{
+ "classLoaderName": "app",
+ "declaringClass": "org.apache.spark.sql.functions$",
+ "methodName": "try_variant_set",
+ "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_set",
+ "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_set_with_create_if_missing.proto.bin
b/sql/connect/common/src/test/resources/query-tests/queries/function_try_variant_set_with_create_if_missing.proto.bin
new file mode 100644
index 000000000000..ccf0814fe095
Binary files /dev/null and
b/sql/connect/common/src/test/resources/query-tests/queries/function_try_variant_set_with_create_if_missing.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 99285d126acd..2637fbc6f077 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
@@ -558,6 +558,7 @@
|
org.apache.spark.sql.catalyst.expressions.variant.TryVariantArrayAppendExpressionBuilder
| try_variant_array_append | SELECT try_variant_array_append(parse_json('[1,
2, 3]'), '$', 4) | struct<try_variant_array_append(parse_json([1, 2, 3]), $,
4):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.TryVariantSetExpressionBuilder
| try_variant_set | SELECT try_variant_set(parse_json('{"a": 1}'), '$.a', 2) |
struct<try_variant_set(parse_json({"a": 1}), $.a, 2, true):variant> |
|
org.apache.spark.sql.catalyst.expressions.variant.VariantArrayAppendExpressionBuilder
| variant_array_append | SELECT variant_array_append(parse_json('[1, 2, 3]'),
'$', 4) | struct<variant_array_append(parse_json([1, 2, 3]), $, 4):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> |
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 fe4dde44ec99..864d0c9b631b 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
@@ -521,6 +521,55 @@ class VariantSuite extends SharedSparkSession with
ExpressionEvalHelper {
}
}
+ test("try_variant_set with literal arguments") {
+ def rows(results: Any*): Seq[Row] = results.map(Row(_))
+
+ checkAnswer(
+ sql("SELECT to_json(try_variant_set(parse_json('{\"a\": 1}'), '$.a',
2))"),
+ rows("""{"a":2}"""))
+ checkAnswer(
+ sql("SELECT to_json(try_variant_set(parse_json('{\"a\": 1}'), '$.b', 2,
false))"),
+ rows("""{"a":1}"""))
+
+ checkAnswer(
+ sql("SELECT to_json(try_variant_set(parse_json('{\"a\": 1}'), '$.a.b',
2))"),
+ rows(null))
+
+ checkAnswer(
+ sql("SELECT to_json(try_variant_set(CAST(NULL AS VARIANT), '$.a', 1))"),
+ rows(null))
+
+ // A malformed path is still raised.
+ checkError(
+ exception = intercept[SparkRuntimeException] {
+ sql("SELECT try_variant_set(parse_json('{}'), '$', 1)").collect()
+ },
+ condition = "INVALID_VARIANT_PATH",
+ parameters = Map("path" -> "$", "functionName" ->
toSQLId("try_variant_set")))
+ }
+
+ test("try_variant_set 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}""", "$.a", 2),
+ ("""{"a": 1}""", "$.a.b", 9),
+ (null, "$.a", 2)
+ ).toDF("json", "path", "val")
+ val v = parse_json(col("json"))
+
+ checkAnswer(
+ df.select(to_json(try_variant_set(v, col("path"),
col("val"))).alias("r")),
+ rows("""{"a":2}""", null, null))
+
+ checkAnswer(
+ df.select(to_json(try_variant_set(v, "$.b", col("val"),
false)).alias("r")),
+ rows("""{"a":1}""", """{"a":1}""", null))
+ }
+ }
+ }
+
test("variant_array_append with literal arguments") {
def rows(results: Any*): Seq[Row] = results.map(Row(_))
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]