This is an automated email from the ASF dual-hosted git repository.
dtenedor pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.x by this push:
new c25f857a984b [SPARK-59917][SQL] Do not rtrim CHAR arguments to
from_json, from_csv, and from_xml
c25f857a984b is described below
commit c25f857a984b4abe37d2d4e4ffac61c7f2663c8c
Author: Serge Rielau <[email protected]>
AuthorDate: Wed Oct 7 12:37:21 2026 -0700
[SPARK-59917][SQL] Do not rtrim CHAR arguments to from_json, from_csv, and
from_xml
### What changes were proposed in this pull request?
Follow-up to
[SPARK-59274](https://issues.apache.org/jira/browse/SPARK-59274). `from_json`,
`from_csv`, `from_xml`, and `schema_of_*` take a STRING document. CHAR padding
is part of the value after CAST to STRING (`LENGTH('abc'::CHAR(10)::STRING)` is
10 under `spark.sql.charVarchar.standardSemantics.enabled`). This PR removes
`SupportTrimmedCharInput` so those functions no longer rtrim CHAR arguments.
Schema-driven parse of CHAR/VARCHAR *fields* from SPARK-59274 is unchanged:
pad, overflow, and FAILFAST still apply to parsed output.
JIRA: https://issues.apache.org/jira/browse/SPARK-59917
### Why are the changes needed?
These functions are STRING parsers. Special-casing CHAR by stripping
trailing blanks disagrees with CAST-to-STRING semantics and with VARCHAR/STRING
arguments, which keep trailing spaces.
In default mode, CHAR table columns are padded on read
(`spark.sql.readSideCharPadding`, on by default) and reach these functions as
padded STRINGs. The removed trait only matched first-class `CharType`, so on
master `from_csv(char_col, ...)` could disagree depending on
`spark.sql.charVarchar.standardSemantics.enabled`. This PR makes those paths
agree.
### Does this PR introduce _any_ user-facing change?
Yes, relative to current `master` (unreleased SPARK-59274). CHAR document
arguments are no longer rtrimmed before parse.
JSON still accepts trailing whitespace, so a padded CHAR JSON document can
still parse:
```sql
SELECT from_json(CAST('{"a":1}' AS CHAR(12)), 'a INT')
-- same result as
SELECT from_json('{"a":1} ', 'a INT')
```
CSV makes the padding visible with a space delimiter. Extra empty fields
are a token-count mismatch:
```sql
SELECT schema_of_csv(CAST('1' AS CHAR(3)), map('delimiter', ' '))
-- STRUCT<_c0: INT, _c1: STRING, _c2: STRING>
SELECT from_csv(CAST('1' AS CHAR(3)), '_c0 INT', map('delimiter', ' '))
-- PERMISSIVE still returns named_struct('_c0', 1)
SELECT from_csv(CAST('1' AS CHAR(3)), '_c0 INT', map('delimiter', ' ',
'mode', 'FAILFAST'))
-- MALFORMED_RECORD_IN_PARSING (previously parsed the trimmed value)
```
With the default comma delimiter, padding also lands on an unquoted last
field, because `from_csv` and `schema_of_csv` keep trailing whitespace by
default. Integral, decimal, and boolean converters reject those spaces; float
and double accept them, so inference can fall through to DOUBLE:
```sql
SELECT schema_of_csv(CAST('1' AS CHAR(3)))
-- STRUCT<_c0: DOUBLE> (was STRUCT<_c0: INT> after rtrim)
SELECT from_csv(CAST('1,2' AS CHAR(5)), 'a INT, b INT')
-- named_struct('a', 1, 'b', NULL) (was {1, 2})
SELECT from_csv(CAST('1,ab' AS CHAR(6)), 'a INT, b STRING')
-- named_struct('a', 1, 'b', 'ab ')
SELECT from_csv(
CAST('1,2' AS CHAR(5)),
'a INT, b INT',
map('ignoreTrailingWhiteSpace', 'true'))
-- named_struct('a', 1, 'b', 2)
```
`map('ignoreTrailingWhiteSpace', 'true')` restores the trimmed last-field
parse. FAILFAST on the INT last-field case raises `MALFORMED_RECORD_IN_PARSING`.
Default `spark.sql.charVarchar.standardSemantics.enabled` remains false.
Released Spark versions are unchanged.
### How was this patch tested?
- Dropped Catalyst tests that expected CHAR-document rtrim.
- `CsvExpressionsSuite`: space-delimiter CHAR padding (interpreted and
codegen, including FAILFAST); comma-delimiter last-field pad (`schema_of_csv`
DOUBLE, INT null, STRING pad, `ignoreTrailingWhiteSpace`).
- `BasicCharVarcharTestSuite`: padded CHAR `from_json` vs STRING with
trailing spaces; malformed CHAR corrupt records for JSON/XML; space-delimiter
`from_csv` FAILFAST; comma-delimiter `schema_of_csv` / `from_csv` including
`ignoreTrailingWhiteSpace` and a table-backed `CHAR(5)` column; schema-output
CHAR/VARCHAR cases from SPARK-59274 remain.
Targeted local run: `sql/testOnly
org.apache.spark.sql.BasicCharVarcharTestSuite -- -z SPARK-59274` and
`catalyst/testOnly
org.apache.spark.sql.catalyst.expressions.CsvExpressionsSuite -- -z
SPARK-59917`.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Cursor Grok 4.6
Closes #59188 from srielau/SPARK-59917.
Authored-by: Serge Rielau <[email protected]>
Signed-off-by: Daniel Tenedorio <[email protected]>
(cherry picked from commit 6fbec03caab5776014b3bfc23e8dfb8dcaab1dd7)
Signed-off-by: Daniel Tenedorio <[email protected]>
---
.../sql/catalyst/analysis/TypeCoercionHelper.scala | 9 +-
.../spark/sql/catalyst/expressions/ExprUtils.scala | 25 +----
.../sql/catalyst/expressions/csvExpressions.scala | 14 ++-
.../sql/catalyst/expressions/jsonExpressions.scala | 12 +--
.../sql/catalyst/expressions/xmlExpressions.scala | 12 +--
.../catalyst/expressions/CsvExpressionsSuite.scala | 39 +++++--
.../expressions/JsonExpressionsSuite.scala | 9 --
.../catalyst/expressions/XmlExpressionsSuite.scala | 9 --
.../apache/spark/sql/CharVarcharTestSuite.scala | 113 ++++++++++++++-------
9 files changed, 124 insertions(+), 118 deletions(-)
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionHelper.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionHelper.scala
index 12de23c06fa3..f76433684c40 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionHelper.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionHelper.scala
@@ -53,7 +53,6 @@ import org.apache.spark.sql.catalyst.expressions.{
SpecialFrameBoundary,
SpecifiedWindowFrame,
SubtractTimestamps,
- SupportTrimmedCharInput,
TimeBucket,
TimestampAddInterval,
WindowSpecDefinition
@@ -747,14 +746,10 @@ abstract class TypeCoercionHelper {
case e: ExpectsInputTypes if e.inputTypes.nonEmpty =>
// Convert NullType into some specific target type for
ExpectsInputTypes that don't do
// general implicit casting. Also promote CHAR/VARCHAR to STRING here
because these
- // expressions skip ImplicitCastInputTypes. Expressions that trim CHAR
padding themselves
- // retain the original type so they can distinguish CHAR from VARCHAR
and STRING.
+ // expressions skip ImplicitCastInputTypes.
val children: Seq[Expression] = e.children.zip(e.inputTypes).map {
case (in, expected) =>
- val promotedType = e match {
- case _: SupportTrimmedCharInput => None
- case _ => charVarcharToPlainString(in.dataType, expected)
- }
+ val promotedType = charVarcharToPlainString(in.dataType, expected)
promotedType
.map(dt => if (dt == in.dataType) in else Cast(in, dt))
.getOrElse {
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/ExprUtils.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/ExprUtils.scala
index 12c8d80f2dcc..e4f79b9ced09 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/ExprUtils.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/ExprUtils.scala
@@ -24,7 +24,6 @@ import org.apache.spark.sql.catalyst.analysis._
import org.apache.spark.sql.catalyst.analysis.TypeCheckResult
import
org.apache.spark.sql.catalyst.analysis.TypeCheckResult.{DataTypeMismatch,
TypeCheckSuccess}
import org.apache.spark.sql.catalyst.expressions.aggregate.AggregateExpression
-import org.apache.spark.sql.catalyst.expressions.objects.Invoke
import org.apache.spark.sql.catalyst.optimizer.ReplaceExpressions
import org.apache.spark.sql.catalyst.plans.logical.Aggregate
import org.apache.spark.sql.catalyst.trees.TreePattern.PLAN_EXPRESSION
@@ -32,7 +31,7 @@ import org.apache.spark.sql.catalyst.util.{ArrayBasedMapData,
CharVarcharUtils}
import org.apache.spark.sql.errors.{QueryCompilationErrors, QueryErrorsBase,
QueryExecutionErrors}
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.internal.types.{AbstractMapType,
StringTypeWithCollation}
-import org.apache.spark.sql.types.{CharType, DataType, MapType, StringType,
StructType, VariantType}
+import org.apache.spark.sql.types.{DataType, MapType, StringType, StructType,
VariantType}
import org.apache.spark.unsafe.types.UTF8String
object ExprUtils extends EvalHelper with QueryErrorsBase {
@@ -271,25 +270,3 @@ object ExprUtils extends EvalHelper with QueryErrorsBase {
case _ => false
}
}
-
-private[sql] trait SupportTrimmedCharInput extends UnaryExpression {
-
- // Keep this type-based so the effective input does not change if SQLConf
changes after analysis.
- // A first-class CharType child already establishes that CHAR semantics
apply.
- @transient
- protected final lazy val stringInput: Expression = child.dataType match {
- case _: CharType =>
- Invoke(
- child,
- "trimRight",
- child.dataType,
- returnNullable = false,
- isDeterministic = child.deterministic)
- case _ => child
- }
-
- protected final def trimStringInput(value: UTF8String): UTF8String =
child.dataType match {
- case _: CharType => value.trimRight()
- case _ => value
- }
-}
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 bfe3e8271372..9c2c644cd675 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
@@ -66,8 +66,7 @@ case class CsvToStructs(
requiredSchema: Option[StructType] = None)
extends UnaryExpression
with TimeZoneAwareExpression
- with ExpectsInputTypes
- with SupportTrimmedCharInput {
+ with ExpectsInputTypes {
override def nullable: Boolean = child.nullable
override def nullIntolerant: Boolean = true
@@ -113,12 +112,12 @@ case class CsvToStructs(
override def stateful: Boolean = true
override def nullSafeEval(input: Any): Any = {
- evaluator.evaluate(trimStringInput(input.asInstanceOf[UTF8String]))
+ evaluator.evaluate(input.asInstanceOf[UTF8String])
}
override def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = {
val refEvaluator = ctx.addReferenceObj("evaluator", evaluator)
- val eval = stringInput.genCode(ctx)
+ val eval = child.genCode(ctx)
val resultType = CodeGenerator.boxedType(dataType)
val resultTerm = ctx.freshName("result")
ev.copy(code =
@@ -161,8 +160,7 @@ case class SchemaOfCsv(
extends UnaryExpression
with RuntimeReplaceable
with DefaultStringProducingExpression
- with QueryErrorsBase
- with SupportTrimmedCharInput {
+ with QueryErrorsBase {
def this(child: Expression) = this(child, Map.empty[String, String])
@@ -210,8 +208,8 @@ case class SchemaOfCsv(
Literal.create(evaluator, ObjectType(classOf[SchemaOfCsvEvaluator])),
"evaluate",
dataType,
- Seq(stringInput),
- Seq(stringInput.dataType),
+ Seq(child),
+ Seq(child.dataType),
returnNullable = false)
}
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 b10efd91f4f7..91ddcec4473b 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
@@ -1878,8 +1878,7 @@ case class JsonToStructs(
with TimeZoneAwareExpression
with CodegenFallback
with ExpectsInputTypes
- with QueryErrorsBase
- with SupportTrimmedCharInput {
+ with QueryErrorsBase {
// The JSON input data might be missing certain fields. We force the
nullability
// of the user-provided schema to avoid data corruptions. In particular, the
parquet-mr encoder
@@ -1940,7 +1939,7 @@ case class JsonToStructs(
override def stateful: Boolean = true
override def nullSafeEval(json: Any): Any = {
- evaluator.evaluate(trimStringInput(json.asInstanceOf[UTF8String]))
+ evaluator.evaluate(json.asInstanceOf[UTF8String])
}
override def inputTypes: Seq[AbstractDataType] =
@@ -2083,8 +2082,7 @@ case class SchemaOfJson(
extends UnaryExpression
with RuntimeReplaceable
with DefaultStringProducingExpression
- with QueryErrorsBase
- with SupportTrimmedCharInput {
+ with QueryErrorsBase {
def this(child: Expression) = this(child, Map.empty[String, String])
@@ -2126,8 +2124,8 @@ case class SchemaOfJson(
Literal.create(evaluator, ObjectType(classOf[SchemaOfJsonEvaluator])),
"evaluate",
dataType,
- Seq(stringInput),
- Seq(stringInput.dataType),
+ Seq(child),
+ Seq(child.dataType),
returnNullable = false)
override def prettyName: String = "schema_of_json"
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 7567ad39055f..0c4f4890ac58 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
@@ -66,8 +66,7 @@ case class XmlToStructs(
extends UnaryExpression
with TimeZoneAwareExpression
with ExpectsInputTypes
- with QueryErrorsBase
- with SupportTrimmedCharInput {
+ with QueryErrorsBase {
def this(child: Expression, schema: Expression, options: Map[String,
String]) =
this(
@@ -116,7 +115,7 @@ case class XmlToStructs(
}
override def nullSafeEval(xml: Any): Any = {
- evaluator.evaluate(trimStringInput(xml.asInstanceOf[UTF8String]))
+ evaluator.evaluate(xml.asInstanceOf[UTF8String])
}
override protected def doGenCode(ctx: CodegenContext, ev: ExprCode):
ExprCode = {
@@ -177,8 +176,7 @@ case class SchemaOfXml(
extends UnaryExpression
with RuntimeReplaceable
with DefaultStringProducingExpression
- with QueryErrorsBase
- with SupportTrimmedCharInput {
+ with QueryErrorsBase {
def this(child: Expression) = this(child, Map.empty[String, String])
@@ -236,8 +234,8 @@ case class SchemaOfXml(
XmlExpressionEvalUtils.getClass,
dataType,
"schemaOfXml",
- Seq(Literal(xmlInferSchema, xmlInferSchemaObjectType), stringInput),
- Seq(xmlInferSchemaObjectType, stringInput.dataType),
+ Seq(Literal(xmlInferSchema, xmlInferSchemaObjectType), child),
+ Seq(xmlInferSchemaObjectType, child.dataType),
returnNullable = false)
}
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 7e9939ded587..3a36e276702b 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
@@ -22,7 +22,7 @@ import java.util.{Calendar, Locale, TimeZone}
import org.scalatest.exceptions.TestFailedException
-import org.apache.spark.SparkFunSuite
+import org.apache.spark.{SparkException, SparkFunSuite}
import org.apache.spark.sql.AnalysisException
import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.catalyst.util._
@@ -171,16 +171,39 @@ class CsvExpressionsSuite extends SparkFunSuite with
ExpressionEvalHelper {
"STRUCT<_c0: INT, _c1: STRING>")
}
- test("schema_of_csv and from_csv trim CHAR padding") {
- val input = Literal.create("1 2 ", CharType(6, "UTF8_LCASE"))
- val options = Map("delimiter" -> " ", "mode" -> FailFastMode.name)
- val schema = new StructType().add("_c0", IntegerType).add("_c1",
IntegerType)
+ test("SPARK-59917: schema_of_csv and from_csv keep CHAR padding") {
+ val padded = Literal.create("1 ", StringType)
+ val charInput = Literal.create("1 ", CharType(3))
+ val options = Map("delimiter" -> " ")
+ val schema = new StructType().add("_c0", IntegerType)
checkEvaluation(
- SchemaOfCsv(input, options),
- "STRUCT<_c0: INT, _c1: INT>")
+ SchemaOfCsv(padded, options),
+ "STRUCT<_c0: INT, _c1: STRING, _c2: STRING>")
checkEvaluation(
- CsvToStructs(schema, options, input, UTC_OPT),
+ SchemaOfCsv(charInput, options),
+ "STRUCT<_c0: INT, _c1: STRING, _c2: STRING>")
+ // Extra empty fields from CHAR padding: PERMISSIVE still parses the first
token.
+ checkEvaluation(CsvToStructs(schema, options, padded, UTC_OPT),
InternalRow(1))
+ checkEvaluation(CsvToStructs(schema, options, charInput, UTC_OPT),
InternalRow(1))
+ checkErrorInExpression[SparkException](
+ CsvToStructs(schema, options + ("mode" -> FailFastMode.name), charInput,
UTC_OPT),
+ "MALFORMED_RECORD_IN_PARSING.WITHOUT_SUGGESTION",
+ Map("badRecord" -> "[1]", "failFastMode" -> "FAILFAST"))
+
+ val commaChar = Literal.create("1,2 ", CharType(5))
+ val twoInts = new StructType().add("a", IntegerType).add("b", IntegerType)
+ val intString = new StructType().add("a", IntegerType).add("b", StringType)
+ checkEvaluation(
+ SchemaOfCsv(Literal.create("1 ", CharType(3)), Map.empty),
+ "STRUCT<_c0: DOUBLE>")
+ checkEvaluation(CsvToStructs(twoInts, Map.empty, commaChar, UTC_OPT),
InternalRow(1, null))
+ checkEvaluation(
+ CsvToStructs(intString, Map.empty, Literal.create("1,ab ",
CharType(6)), UTC_OPT),
+ InternalRow(1, UTF8String.fromString("ab ")))
+ checkEvaluation(
+ CsvToStructs(
+ twoInts, Map("ignoreTrailingWhiteSpace" -> "true"), commaChar,
UTC_OPT),
InternalRow(1, 2))
}
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 a5d30b417835..d8d68ef75c10 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
@@ -803,15 +803,6 @@ class JsonExpressionsSuite extends SparkFunSuite with
ExpressionEvalHelper {
"STRUCT<col: BIGINT>")
}
- test("schema_of_json and from_json trim CHAR padding") {
- val document = """{"col":1} """
- val input = Literal.create(document, CharType(document.length,
"UTF8_LCASE"))
- val schema = new StructType().add("col", LongType)
-
- checkEvaluation(SchemaOfJson(input, Map.empty), "STRUCT<col: BIGINT>")
- checkEvaluation(JsonToStructs(schema, Map.empty, input, UTC_OPT),
InternalRow(1L))
- }
-
test("parse date with locale") {
Seq("en-US", "ru-RU").foreach { langTag =>
val locale = Locale.forLanguageTag(langTag)
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 c1c53b720761..a0011e1dbbb9 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
@@ -371,15 +371,6 @@ class XmlExpressionsSuite extends SparkFunSuite with
ExpressionEvalHelper {
"STRUCT<col: BIGINT>")
}
- test("schema_of_xml and from_xml trim CHAR padding") {
- val document = "<ROW><col>1</col></ROW> "
- val input = Literal.create(document, CharType(document.length,
"UTF8_LCASE"))
- val schema = new StructType().add("col", LongType)
-
- checkEvaluation(SchemaOfXml(input, Map.empty), "STRUCT<col: BIGINT>")
- checkEvaluation(XmlToStructs(schema, Map.empty, input, UTC_OPT),
InternalRow(1L))
- }
-
test("parse date with locale") {
Seq("en-US", "ru-RU").foreach { langTag =>
val locale = Locale.forLanguageTag(langTag)
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala
index ab553de65de6..58397079b8d7 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala
@@ -24,7 +24,7 @@ import org.apache.spark.sql.catalyst.analysis.FunctionRegistry
import org.apache.spark.sql.catalyst.analysis.resolver.ResolverGuard
import org.apache.spark.sql.catalyst.expressions.{
Alias, ArrayJoin, Attribute, Concat, EqualTo, Expression, GreaterThan,
InSet, Literal,
- ScalarSubquery, StringRPad, StringToMap, SupportTrimmedCharInput, Upper
+ ScalarSubquery, StringRPad, StringToMap, Upper
}
import org.apache.spark.sql.catalyst.expressions.Cast.toSQLId
import org.apache.spark.sql.catalyst.parser.{CatalystSqlParser, ParseException}
@@ -2645,44 +2645,89 @@ class BasicCharVarcharTestSuite extends
SharedSparkSession {
test("SPARK-59274: from_json/csv/xml honor CHAR/VARCHAR under
standardSemantics") {
withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- def checkTextInput(query: String, inputType: DataType): DataFrame = {
- val df = sql(query)
- val inputTypes =
df.queryExecution.analyzed.expressions.flatMap(_.collect {
- case e: SupportTrimmedCharInput => e.child.dataType
- })
- assert(inputTypes === Seq(inputType), df.queryExecution.analyzed)
- df
- }
-
+ // CHAR padding is part of the STRING value after CAST. Parsers do not
rtrim it.
+ // from_json on padded CHAR must match the same document as STRING with
trailing spaces.
+ checkAnswer(
+ sql("""SELECT from_json(CAST('{"a":1}' AS CHAR(12)), 'a INT')"""),
+ sql("""SELECT from_json('{"a":1} ', 'a INT')"""))
+ // XML ignores trailing whitespace, so a padded CHAR document still
parses.
checkAnswer(
- checkTextInput(
- """SELECT from_json(CAST('{"a":1}' AS CHAR(12)), 'a INT')""",
- CharType(12)),
+ sql(
+ """SELECT from_xml(
+ | CAST('<ROW><a>1</a></ROW>' AS CHAR(30)),
+ | 'a INT')""".stripMargin),
Row(Row(1)))
+ // Malformed padded CHAR documents keep the full pad in the corrupt
record.
+ // Reinstating rtrim would drop those trailing spaces from _unparsed.
+ checkAnswer(
+ sql(
+ """SELECT from_json(
+ | CAST('{"a":' AS CHAR(12)),
+ | 'a INT, _unparsed STRING',
+ | map('columnNameOfCorruptRecord', '_unparsed'))""".stripMargin),
+ Row(Row(null, "{\"a\": ")))
+ checkAnswer(
+ sql(
+ """SELECT from_xml(
+ | CAST('<ROW>' AS CHAR(10)),
+ | 'a INT, _unparsed STRING',
+ | map('columnNameOfCorruptRecord', '_unparsed'))""".stripMargin),
+ Row(Row(null, "<ROW> ")))
+ // With a space delimiter, CHAR padding tokenizes into extra empty
fields.
checkAnswer(
- checkTextInput(
+ sql(
+ """SELECT schema_of_csv(
+ | CAST('1' AS CHAR(3)),
+ | map('delimiter', ' '))""".stripMargin),
+ Row("STRUCT<_c0: INT, _c1: STRING, _c2: STRING>"))
+ checkAnswer(
+ sql(
"""SELECT from_csv(
| CAST('1' AS CHAR(3)),
| '_c0 INT',
- | map('delimiter', ' ', 'mode', 'FAILFAST'))""".stripMargin,
- CharType(3)),
- Row(Row(1)))
- checkAnswer(
- checkTextInput(
- """SELECT from_xml(
- | CAST('<ROW><a>1</a></ROW>' AS CHAR(30)),
- | 'a INT')""".stripMargin,
- CharType(30)),
+ | map('delimiter', ' '))""".stripMargin),
Row(Row(1)))
+ checkError(
+ exception = intercept[SparkException] {
+ sql(
+ """SELECT from_csv(
+ | CAST('1' AS CHAR(3)),
+ | '_c0 INT',
+ | map('delimiter', ' ', 'mode',
'FAILFAST'))""".stripMargin).collect()
+ },
+ condition = "MALFORMED_RECORD_IN_PARSING.WITHOUT_SUGGESTION",
+ parameters = Map("badRecord" -> "[1]", "failFastMode" -> "FAILFAST"))
Seq(
- ("CAST('1 ' AS VARCHAR(2))", VarcharType(2)),
- ("CAST('1 ' AS STRING)", StringType)).foreach { case (input,
inputType) =>
+ "CAST('1 ' AS VARCHAR(2))",
+ "CAST('1 ' AS STRING)").foreach { input =>
checkAnswer(
- checkTextInput(
- s"SELECT schema_of_csv($input, map('delimiter', ' '))",
- inputType),
+ sql(s"SELECT schema_of_csv($input, map('delimiter', ' '))"),
Row("STRUCT<_c0: INT, _c1: STRING>"))
}
+ // Default comma delimiter: padding lands on the unquoted last field.
+ checkAnswer(
+ sql("SELECT schema_of_csv(CAST('1' AS CHAR(3)))"),
+ Row("STRUCT<_c0: DOUBLE>"))
+ checkAnswer(
+ sql("SELECT from_csv(CAST('1,2' AS CHAR(5)), 'a INT, b INT')"),
+ Row(Row(1, null)))
+ checkAnswer(
+ sql("SELECT from_csv(CAST('1,ab' AS CHAR(6)), 'a INT, b STRING')"),
+ Row(Row(1, "ab ")))
+ checkAnswer(
+ sql(
+ """SELECT from_csv(
+ | CAST('1,2' AS CHAR(5)),
+ | 'a INT, b INT',
+ | map('ignoreTrailingWhiteSpace', 'true'))""".stripMargin),
+ Row(Row(1, 2)))
+ withTable("csv_char_doc") {
+ sql("CREATE TABLE csv_char_doc(c CHAR(5)) USING parquet")
+ sql("INSERT INTO csv_char_doc VALUES ('1,2')")
+ checkAnswer(
+ sql("SELECT from_csv(c, 'a INT, b INT') FROM csv_char_doc"),
+ Row(Row(1, null)))
+ }
val jsonChar = sql("""SELECT from_json('{"a": "str"}', 'a CHAR(5)')""")
val jsonCharType = jsonChar.schema.head.dataType.asInstanceOf[StructType]
@@ -2814,16 +2859,6 @@ class BasicCharVarcharTestSuite extends
SharedSparkSession {
checkAnswer(
sql("SELECT length(CAST('1' AS CHAR(3)))"),
Row(3))
- checkAnswer(
- sql("SELECT schema_of_csv(CAST('1' AS CHAR(3)))"),
- Row("STRUCT<_c0: INT>"))
- // Without trailing-only CHAR padding removal, the space delimiter
creates empty columns.
- checkAnswer(
- sql(
- """SELECT schema_of_csv(
- | CAST('1' AS CHAR(3)),
- | map('delimiter', ' '))""".stripMargin),
- Row("STRUCT<_c0: INT>"))
checkAnswer(
sql("SELECT length(CAST('<ROW><a>1</a></ROW>' AS CHAR(30)))"),
Row(30))
@@ -2834,7 +2869,7 @@ class BasicCharVarcharTestSuite extends
SharedSparkSession {
"schema_of_json(CAST('{\"a\":1}' AS CHAR(20) COLLATE SR_AI))" ->
"STRUCT<a: BIGINT>",
"schema_of_csv(CAST('1' AS CHAR(3) COLLATE SR_AI), map('delimiter', '
'))" ->
- "STRUCT<_c0: INT>",
+ "STRUCT<_c0: INT, _c1: STRING, _c2: STRING>",
"schema_of_xml(CAST('<ROW><a>1</a></ROW>' AS CHAR(30) COLLATE SR_AI))"
->
"STRUCT<a: BIGINT>").foreach { case (expression, expected) =>
checkAnswer(sql(s"SELECT $expression"), Row(expected))
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]