From 1ac6670b6cebaba10704156f9a91aeaaa98b2a2c Mon Sep 17 00:00:00 2001 From: Vinod KC Date: Sun, 19 Jul 2026 01:40:33 +0000 Subject: [PATCH 1/3] Mark JSON/CSV/XML expressions stateful to prevent shared-evaluator races --- .../sql/catalyst/expressions/csvExpressions.scala | 2 ++ .../catalyst/expressions/jsonExpressions.scala | 3 +++ .../sql/catalyst/expressions/xmlExpressions.scala | 2 ++ .../expressions/CsvExpressionsSuite.scala | 13 +++++++++++++ .../expressions/JsonExpressionsSuite.scala | 15 +++++++++++++++ .../expressions/XmlExpressionsSuite.scala | 13 +++++++++++++ 6 files changed, 48 insertions(+) diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/csvExpressions.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/csvExpressions.scala index 5110aab9c6f03..cd6af4098001a 100644 --- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/csvExpressions.scala +++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/csvExpressions.scala @@ -102,6 +102,7 @@ case class CsvToStructs( @transient private lazy val evaluator: CsvToStructsEvaluator = CsvToStructsEvaluator( options, nullableSchema, nameOfCorruptRecord, timeZoneId, requiredSchema) + override def stateful: Boolean = true override def nullSafeEval(input: Any): Any = { evaluator.evaluate(input.asInstanceOf[UTF8String]) @@ -275,6 +276,7 @@ case class StructsToCsv( lazy val converter: Any => UTF8String = { (row: Any) => UTF8String.fromString(gen.writeToString(row.asInstanceOf[InternalRow])) } + override def stateful: Boolean = true override def withTimeZone(timeZoneId: String): TimeZoneAwareExpression = copy(timeZoneId = Option(timeZoneId)) diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala index b4f41fda42f00..949d73d04cb79 100644 --- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala +++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala @@ -75,6 +75,7 @@ case class GetJsonObject(json: Expression, path: Expression) } else { new GetJsonObjectEvaluator() } + override def stateful: Boolean = true override def eval(input: InternalRow): Any = { evaluator.setJson(json.eval(input).asInstanceOf[UTF8String]) @@ -293,6 +294,7 @@ case class JsonTuple(children: Seq[Expression]) @transient private lazy val evaluator: JsonTupleEvaluator = JsonTupleEvaluator(foldableFieldNames) + override def stateful: Boolean = true override def eval(input: InternalRow): IterableOnce[InternalRow] = { val json = jsonExpr.eval(input).asInstanceOf[UTF8String] @@ -420,6 +422,7 @@ case class JsonToStructs( @transient private lazy val evaluator = new JsonToStructsEvaluator( options, nullableSchema, nameOfCorruptRecord, timeZoneId, variantAllowDuplicateKeys) + override def stateful: Boolean = true override def nullSafeEval(json: Any): Any = evaluator.evaluate(json.asInstanceOf[UTF8String]) diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/xmlExpressions.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/xmlExpressions.scala index 5a91d37203754..08e1f46bddeef 100644 --- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/xmlExpressions.scala +++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/xmlExpressions.scala @@ -96,6 +96,7 @@ case class XmlToStructs( @transient private lazy val evaluator: XmlToStructsEvaluator = XmlToStructsEvaluator(options, nullableSchema, nameOfCorruptRecord, timeZoneId, child) + override def stateful: Boolean = true private val nameOfCorruptRecord = SQLConf.get.getConf(SQLConf.COLUMN_NAME_OF_CORRUPT_RECORD) @@ -278,6 +279,7 @@ case class StructsToXml( @transient private lazy val evaluator = StructsToXmlEvaluator(options, child.dataType, timeZoneId) + override def stateful: Boolean = true override def withTimeZone(timeZoneId: String): TimeZoneAwareExpression = copy(timeZoneId = Option(timeZoneId)) diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CsvExpressionsSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CsvExpressionsSuite.scala index 631b08b5395f9..2ccde7421739c 100644 --- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CsvExpressionsSuite.scala +++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CsvExpressionsSuite.scala @@ -314,4 +314,17 @@ class CsvExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper { CsvToStructs(schema, Map.empty, Literal.create(null, StringType), UTC_OPT), null) } + + test("CsvToStructs and StructsToCsv are stateful and produce fresh copies") { + val schema = StructType(StructField("a", IntegerType) :: Nil) + + val csvToStructs = CsvToStructs(schema, Map.empty, Literal("1"), UTC_OPT) + assert(csvToStructs.stateful) + assert(csvToStructs.freshCopyIfContainsStatefulExpression() ne csvToStructs) + + val struct = Literal.create(create_row(1), schema) + val structsToCsv = StructsToCsv(Map.empty, struct, UTC_OPT) + assert(structsToCsv.stateful) + assert(structsToCsv.freshCopyIfContainsStatefulExpression() ne structsToCsv) + } } diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/JsonExpressionsSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/JsonExpressionsSuite.scala index 37916f5a93be0..d122bead9a8fe 100644 --- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/JsonExpressionsSuite.scala +++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/JsonExpressionsSuite.scala @@ -1041,4 +1041,19 @@ class JsonExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper { input) } + test("JsonToStructs, GetJsonObject, JsonTuple are stateful and produce fresh copies") { + val schema = StructType(StructField("a", IntegerType) :: Nil) + val jsonToStructs = JsonToStructs(schema, Map.empty, Literal("{}"), UTC_OPT) + assert(jsonToStructs.stateful) + assert(jsonToStructs.freshCopyIfContainsStatefulExpression() ne jsonToStructs) + + val getJsonObject = GetJsonObject(Literal("{}"), Literal("$.a")) + assert(getJsonObject.stateful) + assert(getJsonObject.freshCopyIfContainsStatefulExpression() ne getJsonObject) + + val jsonTuple = JsonTuple(Literal("{}") :: Literal("a") :: Nil) + assert(jsonTuple.stateful) + assert(jsonTuple.freshCopyIfContainsStatefulExpression() ne jsonTuple) + } + } diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/XmlExpressionsSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/XmlExpressionsSuite.scala index 54bef646739fd..a0011e1dbbb94 100644 --- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/XmlExpressionsSuite.scala +++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/XmlExpressionsSuite.scala @@ -522,4 +522,17 @@ class XmlExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper { ) } + test("XmlToStructs and StructsToXml are stateful and produce fresh copies") { + val schema = StructType(StructField("a", IntegerType) :: Nil) + + val xmlToStructs = XmlToStructs(schema, Map.empty, Literal("1"), UTC_OPT) + assert(xmlToStructs.stateful) + assert(xmlToStructs.freshCopyIfContainsStatefulExpression() ne xmlToStructs) + + val struct = Literal.create(InternalRow(1), schema) + val structsToXml = StructsToXml(Map.empty, struct, UTC_OPT) + assert(structsToXml.stateful) + assert(structsToXml.freshCopyIfContainsStatefulExpression() ne structsToXml) + } + } From e793ca03dd05177add57056022523038e57a3d5f Mon Sep 17 00:00:00 2001 From: Vinod KC Date: Sun, 19 Jul 2026 02:18:51 +0000 Subject: [PATCH 2/3] Fix evaluator sharing in StructsToJson, SchemaOfJson, SchemaOfCsv via stateful=true --- .../catalyst/expressions/csvExpressions.scala | 2 ++ .../expressions/jsonExpressions.scala | 4 ++++ .../expressions/CsvExpressionsSuite.scala | 16 +++++++++++++ .../expressions/JsonExpressionsSuite.scala | 24 +++++++++++++++++++ 4 files changed, 46 insertions(+) diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/csvExpressions.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/csvExpressions.scala index cd6af4098001a..e1902374838b9 100644 --- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/csvExpressions.scala +++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/csvExpressions.scala @@ -188,6 +188,8 @@ case class SchemaOfCsv( override protected def withNewChildInternal(newChild: Expression): SchemaOfCsv = copy(child = newChild) + override def stateful: Boolean = true + @transient private lazy val evaluator: SchemaOfCsvEvaluator = SchemaOfCsvEvaluator(options) diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala index 949d73d04cb79..c93400ea1603a 100644 --- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala +++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala @@ -521,6 +521,8 @@ case class StructsToJson( override protected def withNewChildInternal(newChild: Expression): StructsToJson = copy(child = newChild) + override def stateful: Boolean = true + @transient private lazy val evaluator = StructsToJsonEvaluator(options, inputSchema, timeZoneId) @@ -588,6 +590,8 @@ case class SchemaOfJson( } } + override def stateful: Boolean = true + @transient private lazy val evaluator: SchemaOfJsonEvaluator = SchemaOfJsonEvaluator(options) diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CsvExpressionsSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CsvExpressionsSuite.scala index 2ccde7421739c..c30735217ab4f 100644 --- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CsvExpressionsSuite.scala +++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CsvExpressionsSuite.scala @@ -25,6 +25,7 @@ import org.scalatest.exceptions.TestFailedException import org.apache.spark.SparkFunSuite import org.apache.spark.sql.AnalysisException import org.apache.spark.sql.catalyst.InternalRow +import org.apache.spark.sql.catalyst.expressions.objects.Invoke import org.apache.spark.sql.catalyst.util._ import org.apache.spark.sql.catalyst.util.DateTimeTestUtils.{PST, UTC_OPT} import org.apache.spark.sql.types._ @@ -327,4 +328,19 @@ class CsvExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper { assert(structsToCsv.stateful) assert(structsToCsv.freshCopyIfContainsStatefulExpression() ne structsToCsv) } + + test("SchemaOfCsv is stateful and produces fresh copies with independent evaluators") { + val schemaOfCsv = new SchemaOfCsv(Literal.create("1,abc")) + assert(schemaOfCsv.stateful) + val copy1 = schemaOfCsv.freshCopyIfContainsStatefulExpression() + assert(copy1 ne schemaOfCsv) + val copy2 = schemaOfCsv.freshCopyIfContainsStatefulExpression() + assert(copy2 ne schemaOfCsv) + // Each copy owns an independent evaluator - mutable state must not be shared. + val eval1 = copy1.asInstanceOf[SchemaOfCsv].replacement + .asInstanceOf[Invoke].targetObject.asInstanceOf[Literal].value.asInstanceOf[AnyRef] + val eval2 = copy2.asInstanceOf[SchemaOfCsv].replacement + .asInstanceOf[Invoke].targetObject.asInstanceOf[Literal].value.asInstanceOf[AnyRef] + assert(eval1 ne eval2) + } } diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/JsonExpressionsSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/JsonExpressionsSuite.scala index d122bead9a8fe..765e5ec02f629 100644 --- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/JsonExpressionsSuite.scala +++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/JsonExpressionsSuite.scala @@ -29,6 +29,7 @@ import org.apache.spark.sql.catalyst.InternalRow import org.apache.spark.sql.catalyst.analysis.TypeCheckResult.DataTypeMismatch import org.apache.spark.sql.catalyst.expressions.Cast._ import org.apache.spark.sql.catalyst.expressions.codegen.GenerateUnsafeProjection +import org.apache.spark.sql.catalyst.expressions.objects.Invoke import org.apache.spark.sql.catalyst.util._ import org.apache.spark.sql.catalyst.util.DateTimeTestUtils.{PST, UTC, UTC_OPT} import org.apache.spark.sql.internal.SQLConf @@ -1056,4 +1057,27 @@ class JsonExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper { assert(jsonTuple.freshCopyIfContainsStatefulExpression() ne jsonTuple) } + test("StructsToJson and SchemaOfJson are stateful and produce fresh copies with " + + "independent evaluators") { + val schema = StructType(StructField("a", IntegerType) :: Nil) + val struct = Literal.create(create_row(1), schema) + + val structsToJson = StructsToJson(Map.empty, struct, UTC_OPT) + assert(structsToJson.stateful) + val copy1 = structsToJson.freshCopyIfContainsStatefulExpression() + assert(copy1 ne structsToJson) + val copy2 = structsToJson.freshCopyIfContainsStatefulExpression() + assert(copy2 ne structsToJson) + // Each copy owns an independent evaluator - mutable state must not be shared. + val eval1 = copy1.asInstanceOf[StructsToJson].replacement + .asInstanceOf[Invoke].targetObject.asInstanceOf[Literal].value.asInstanceOf[AnyRef] + val eval2 = copy2.asInstanceOf[StructsToJson].replacement + .asInstanceOf[Invoke].targetObject.asInstanceOf[Literal].value.asInstanceOf[AnyRef] + assert(eval1 ne eval2) + + val schemaOfJson = SchemaOfJson(Literal("""{"a":1}"""), Map.empty) + assert(schemaOfJson.stateful) + assert(schemaOfJson.freshCopyIfContainsStatefulExpression() ne schemaOfJson) + } + } From eb2e65cb66aa2b766056ae42696b178883149e3c Mon Sep 17 00:00:00 2001 From: Vinod KC Date: Mon, 20 Jul 2026 01:45:53 +0000 Subject: [PATCH 3/3] Address review: add MultiGetJsonObject, drop RuntimeReplaceable expressions, fix test --- .../catalyst/expressions/csvExpressions.scala | 2 -- .../expressions/jsonExpressions.scala | 6 ++-- .../expressions/CsvExpressionsSuite.scala | 15 ---------- .../expressions/JsonExpressionsSuite.scala | 29 ++++--------------- 4 files changed, 7 insertions(+), 45 deletions(-) diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/csvExpressions.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/csvExpressions.scala index e1902374838b9..cd6af4098001a 100644 --- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/csvExpressions.scala +++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/csvExpressions.scala @@ -188,8 +188,6 @@ case class SchemaOfCsv( override protected def withNewChildInternal(newChild: Expression): SchemaOfCsv = copy(child = newChild) - override def stateful: Boolean = true - @transient private lazy val evaluator: SchemaOfCsvEvaluator = SchemaOfCsvEvaluator(options) diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala index c93400ea1603a..4bb93588074d3 100644 --- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala +++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala @@ -207,6 +207,8 @@ case class MultiGetJsonObject( } } + override def stateful: Boolean = true + @transient private lazy val evaluator = MultiGetJsonObjectEvaluator( fallbackPaths.map(UTF8String.fromString), @@ -521,8 +523,6 @@ case class StructsToJson( override protected def withNewChildInternal(newChild: Expression): StructsToJson = copy(child = newChild) - override def stateful: Boolean = true - @transient private lazy val evaluator = StructsToJsonEvaluator(options, inputSchema, timeZoneId) @@ -590,8 +590,6 @@ case class SchemaOfJson( } } - override def stateful: Boolean = true - @transient private lazy val evaluator: SchemaOfJsonEvaluator = SchemaOfJsonEvaluator(options) diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CsvExpressionsSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CsvExpressionsSuite.scala index c30735217ab4f..0cda94d7b188d 100644 --- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CsvExpressionsSuite.scala +++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CsvExpressionsSuite.scala @@ -25,7 +25,6 @@ import org.scalatest.exceptions.TestFailedException import org.apache.spark.SparkFunSuite import org.apache.spark.sql.AnalysisException import org.apache.spark.sql.catalyst.InternalRow -import org.apache.spark.sql.catalyst.expressions.objects.Invoke import org.apache.spark.sql.catalyst.util._ import org.apache.spark.sql.catalyst.util.DateTimeTestUtils.{PST, UTC_OPT} import org.apache.spark.sql.types._ @@ -329,18 +328,4 @@ class CsvExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper { assert(structsToCsv.freshCopyIfContainsStatefulExpression() ne structsToCsv) } - test("SchemaOfCsv is stateful and produces fresh copies with independent evaluators") { - val schemaOfCsv = new SchemaOfCsv(Literal.create("1,abc")) - assert(schemaOfCsv.stateful) - val copy1 = schemaOfCsv.freshCopyIfContainsStatefulExpression() - assert(copy1 ne schemaOfCsv) - val copy2 = schemaOfCsv.freshCopyIfContainsStatefulExpression() - assert(copy2 ne schemaOfCsv) - // Each copy owns an independent evaluator - mutable state must not be shared. - val eval1 = copy1.asInstanceOf[SchemaOfCsv].replacement - .asInstanceOf[Invoke].targetObject.asInstanceOf[Literal].value.asInstanceOf[AnyRef] - val eval2 = copy2.asInstanceOf[SchemaOfCsv].replacement - .asInstanceOf[Invoke].targetObject.asInstanceOf[Literal].value.asInstanceOf[AnyRef] - assert(eval1 ne eval2) - } } diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/JsonExpressionsSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/JsonExpressionsSuite.scala index 765e5ec02f629..160470b722665 100644 --- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/JsonExpressionsSuite.scala +++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/JsonExpressionsSuite.scala @@ -29,7 +29,6 @@ import org.apache.spark.sql.catalyst.InternalRow import org.apache.spark.sql.catalyst.analysis.TypeCheckResult.DataTypeMismatch import org.apache.spark.sql.catalyst.expressions.Cast._ import org.apache.spark.sql.catalyst.expressions.codegen.GenerateUnsafeProjection -import org.apache.spark.sql.catalyst.expressions.objects.Invoke import org.apache.spark.sql.catalyst.util._ import org.apache.spark.sql.catalyst.util.DateTimeTestUtils.{PST, UTC, UTC_OPT} import org.apache.spark.sql.internal.SQLConf @@ -1042,7 +1041,8 @@ class JsonExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper { input) } - test("JsonToStructs, GetJsonObject, JsonTuple are stateful and produce fresh copies") { + test("JsonToStructs, GetJsonObject, JsonTuple, MultiGetJsonObject are stateful " + + "and produce fresh copies") { val schema = StructType(StructField("a", IntegerType) :: Nil) val jsonToStructs = JsonToStructs(schema, Map.empty, Literal("{}"), UTC_OPT) assert(jsonToStructs.stateful) @@ -1055,29 +1055,10 @@ class JsonExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper { val jsonTuple = JsonTuple(Literal("{}") :: Literal("a") :: Nil) assert(jsonTuple.stateful) assert(jsonTuple.freshCopyIfContainsStatefulExpression() ne jsonTuple) - } - - test("StructsToJson and SchemaOfJson are stateful and produce fresh copies with " + - "independent evaluators") { - val schema = StructType(StructField("a", IntegerType) :: Nil) - val struct = Literal.create(create_row(1), schema) - val structsToJson = StructsToJson(Map.empty, struct, UTC_OPT) - assert(structsToJson.stateful) - val copy1 = structsToJson.freshCopyIfContainsStatefulExpression() - assert(copy1 ne structsToJson) - val copy2 = structsToJson.freshCopyIfContainsStatefulExpression() - assert(copy2 ne structsToJson) - // Each copy owns an independent evaluator - mutable state must not be shared. - val eval1 = copy1.asInstanceOf[StructsToJson].replacement - .asInstanceOf[Invoke].targetObject.asInstanceOf[Literal].value.asInstanceOf[AnyRef] - val eval2 = copy2.asInstanceOf[StructsToJson].replacement - .asInstanceOf[Invoke].targetObject.asInstanceOf[Literal].value.asInstanceOf[AnyRef] - assert(eval1 ne eval2) - - val schemaOfJson = SchemaOfJson(Literal("""{"a":1}"""), Map.empty) - assert(schemaOfJson.stateful) - assert(schemaOfJson.freshCopyIfContainsStatefulExpression() ne schemaOfJson) + val multiGetJsonObject = MultiGetJsonObject(Literal("{}"), Seq("$.a", "$.b")) + assert(multiGetJsonObject.stateful) + assert(multiGetJsonObject.freshCopyIfContainsStatefulExpression() ne multiGetJsonObject) } }