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 bd2c46622a6d [SPARK-60090][SQL][HIVE] Apply CHAR/VARCHAR assignment
after TRANSFORM via Project
bd2c46622a6d is described below
commit bd2c46622a6d7eace772686028e18a71aab334f1
Author: Serge Rielau <[email protected]>
AuthorDate: Sun Oct 11 20:58:33 2026 -0700
[SPARK-60090][SQL][HIVE] Apply CHAR/VARCHAR assignment after TRANSFORM via
Project
### What changes were proposed in this pull request?
Keep Hive `TRANSFORM` / `MAP` / `REDUCE` script I/O as unbounded `STRING`
(JSON for complex types), the same as sources that have no native CHAR/VARCHAR.
After the script reader, apply Spark CHAR/VARCHAR assignment
(`CharVarcharUtils.stringLengthCheck`: pad `CHAR`, `EXCEED_LIMIT_LENGTH` on
overflow) in a `Project` so the query result type is the declared
`CHAR`/`VARCHAR` (including nested types).
This supersedes the SPARK-59683 codec inside the script/Hive path (that
codec is on master / branch-4.x only; it was never released in 4.2 or 4.3):
- Recursively replace CHAR/VARCHAR with unbounded STRING for script exec
output (including nested maps/arrays/structs).
- Parse delimited / JSON as today for STRING-shaped types.
- When `spark.sql.charVarchar.standardSemantics.enabled` or
`spark.sql.preserveCharVarcharTypeInfo` is true, wrap with a Project using
`stringLengthCheck` so the result type matches `AS (...)`.
- Both configs off: keep today's STRING result (`AS (c CHAR(5))` remains
STRING).
- Remove CHAR-specific logic from no-SerDe converters, Hive unwrappers,
SerDe schema rewrite, and the bind-at-parse I/O-schema flag.
Nullable struct assignment uses `CaseWhen` rather than `If`, so
`CollationTypeCoercion` does not widen collated CHAR/VARCHAR inside the struct
to unbounded STRING.
Assignment lives in a Project, so column pruning can skip an unreferenced
CHAR/VARCHAR column (including `SELECT count(*)` over overflowing output),
matching scan-side checks.
JSON non-string map keys (`MAP<INT, ...>`, `MAP<DATE, ...>`) remain out of
scope; `MAP<INT, CHAR(n)>` without a SerDe fails the same way as `MAP<INT,
STRING>` (`ClassCastException` on the JSON key). Nested field `COMMENT` / `NOT
NULL` inside a rebuilt struct is the same limitation as scan-side
`stringLengthCheck`.
A first-class CHAR/VARCHAR TRANSFORM column compared with a STRING literal
is analyzed as `cast(CHAR as STRING) = lit`, so a short literal does not match
the padded CHAR value. That matches CHAR vs STRING under standard semantics; it
is not scan-style metadata padding of the literal.
JIRA: https://issues.apache.org/jira/browse/SPARK-60090
Parent: SPARK-59683 / PR #58936
### Why are the changes needed?
SPARK-59683 mixed CHAR/VARCHAR assignment (pad / length, same as
CSV/JSON/Parquet) with TRANSFORM JSON / Hive wire issues that are not
CHAR-specific. That made `MAP<STRING, STRING>` vs `MAP<STRING, CHAR(2)>`
diverge. TRANSFORM is Hive-compat STRING-on-the-wire; assignment belongs in a
Spark `Project` like any other STRING source.
### Does this PR introduce _any_ user-facing change?
Yes.
On the unreleased `spark.sql.charVarchar.standardSemantics.enabled` path,
and on the **released** `spark.sql.preserveCharVarcharTypeInfo` path (since
4.0.0):
- `SELECT TRANSFORM(...) USING 'cat' AS (c CHAR(4))` returns typed
`CHAR(4)` with padding.
- Overflow raises `EXCEED_LIMIT_LENGTH`.
- Nested `ARRAY` / `STRUCT` / `MAP<CHAR, VARCHAR>` go through the same
Project assignment.
On branch-4.3 with only `spark.sql.preserveCharVarcharTypeInfo` set, scalar
CHAR/VARCHAR output without a SerDe failed with the unsupported-type error,
nested CHAR/VARCHAR output without a SerDe kept the declared type with no
padding or length check, and Hive SerDe deserialized `char(n)`/`varchar(n)`
with Hive truncation. None of the preserve-only no-SerDe path returned STRING.
When both configs are false, `AS (c CHAR(n))` remains STRING with no pad and no
length check.
Compared with the unreleased SPARK-59683 CHAR-in-the-exec codec (master /
branch-4.x only), no-SerDe nested CHAR is STRING-on-the-wire plus a Project
instead of CHAR inside the reader.
### How was this patch tested?
- `sql/testOnly
org.apache.spark.sql.execution.SparkScriptTransformationSuite -- -z SPARK-60090`
- `hive/testOnly
org.apache.spark.sql.hive.execution.HiveScriptTransformationSuite -- -z
SPARK-60090`
- `catalyst/testOnly org.apache.spark.sql.catalyst.parser.PlanParserSuite
-- -z TRANSFORM`
Coverage includes scalar pad/overflow/null token, mixed CHAR and non-CHAR
columns, CHAR vs STRING comparison (no literal padding), nested
ARRAY/STRUCT/MAP assignment and schema assertions, collated nested STRUCT,
quoted struct field names, MAP CHAR key collision, malformed JSON as null,
flag-off STRING results, preserve-only assignment, Hive SerDe through the same
Spark-side Project, views (PERSISTED vs stored-schema cast), pruning of
unreferenced overflow, and hand-built leftover CHA [...]
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Cursor Grok 4.6
Closes #59302 from srielau/SPARK-60090.
Authored-by: Serge Rielau <[email protected]>
Signed-off-by: Daniel Tenedorio <[email protected]>
(cherry picked from commit b4e88aa1f11994c941f0028cb4dc68e12ff042b6)
Signed-off-by: Daniel Tenedorio <[email protected]>
---
docs/sql-migration-guide.md | 1 +
.../sql/catalyst/util/SparkCharVarcharUtils.scala | 33 +-
.../spark/sql/catalyst/parser/AstBuilder.scala | 44 +-
.../plans/logical/ScriptTransformation.scala | 4 +-
.../spark/sql/catalyst/util/CharVarcharUtils.scala | 41 +-
.../org/apache/spark/sql/internal/SQLConf.scala | 4 +-
.../execution/BaseScriptTransformationExec.scala | 187 +--------
.../spark/sql/execution/SparkSqlParser.scala | 3 +-
.../sql-tests/analyzer-results/transform.sql.out | 86 ++--
.../apache/spark/sql/CharVarcharTestSuite.scala | 4 +-
.../execution/BaseScriptTransformationSuite.scala | 457 +++++++--------------
.../execution/SparkScriptTransformationSuite.scala | 184 ++++++++-
.../org/apache/spark/sql/hive/HiveInspectors.scala | 18 -
.../execution/HiveScriptTransformationExec.scala | 30 +-
.../execution/HiveScriptTransformationSuite.scala | 229 ++---------
15 files changed, 506 insertions(+), 819 deletions(-)
diff --git a/docs/sql-migration-guide.md b/docs/sql-migration-guide.md
index 4af273dcfe68..7b49f4881a26 100644
--- a/docs/sql-migration-guide.md
+++ b/docs/sql-migration-guide.md
@@ -26,6 +26,7 @@ license: |
- Since Spark 4.4, when `spark.sql.preserveCharVarcharTypeInfo` is true and
`spark.sql.charVarchar.standardSemantics.enabled` is false, ORC reads that
apply a CHAR/VARCHAR schema over STRING storage return the stored values
without ORC truncation, matching Parquet. Previously the ORC reader requested
`char(n)`/`varchar(n)` and truncated STRING-stored values to `n`. Read-side
length checks (`EXCEED_LIMIT_LENGTH`) apply only when
`spark.sql.charVarchar.standardSemantics.enabled` is true.
- Since Spark 4.4, when `spark.sql.charVarchar.standardSemantics.enabled` is
true, XML names used as `MAP<CHAR(n), _>` or `MAP<VARCHAR(n), _>` keys in
`from_xml` and the XML datasource are length-checked without padding or
trimming. The checked names are element names, `attributePrefix` plus attribute
names (default prefix `_`), and the `valueTag` (default `_VALUE`) when mixed
text is present. A `CHAR(n)` key must already be exactly `n` characters, and a
`VARCHAR(n)` key must already be [...]
+- Since Spark 4.4, Hive `TRANSFORM` / `MAP` / `REDUCE` with CHAR/VARCHAR `AS`
types keeps script I/O as unbounded STRING and applies Spark assignment (CHAR
padding and `EXCEED_LIMIT_LENGTH` on overflow) in a `Project` when
`spark.sql.charVarchar.standardSemantics.enabled` or
`spark.sql.preserveCharVarcharTypeInfo` is true. Previously, with only
`spark.sql.preserveCharVarcharTypeInfo` set, scalar CHAR/VARCHAR output without
a SerDe failed with an unsupported-type error, nested CHAR/VARCHA [...]
- Since Spark 4.4, the options maps passed to `from_csv`, `to_csv`,
`schema_of_csv`, `from_json`, `to_json`, `schema_of_json`, `from_xml`,
`to_xml`, and `schema_of_xml` must be foldable after replacing
`RuntimeReplaceable` expressions. Previously, Spark evaluated non-foldable
options during analysis, which allowed some constant expressions but could fail
with an internal error or incorrectly evaluate row-dependent expressions. To
allow deterministic and row-independent non-foldable optio [...]
- Since Spark 4.4, when an already-analyzed Data Source V2 query is refreshed
after a compatible schema change, connectors can return more data columns from
the current table schema in `Scan.readSchema()` than requested by
`SupportsPushDownRequiredColumns.pruneColumns`. Previously, this partial
pruning could fail planning because the scan reported columns absent from the
analyzed relation output.
- Since Spark 4.4, when an already-analyzed Data Source V2 query is refreshed,
if rebinding an expanded struct used as a map key causes distinct current keys
to collide under the analyzed schema, the query fails with `DUPLICATED_MAP_KEY`
by default instead of returning duplicate keys. With
`spark.sql.mapKeyDedupPolicy=LAST_WIN`, the last value is kept.
diff --git
a/sql/api/src/main/scala/org/apache/spark/sql/catalyst/util/SparkCharVarcharUtils.scala
b/sql/api/src/main/scala/org/apache/spark/sql/catalyst/util/SparkCharVarcharUtils.scala
index 70684d392042..c0b54f4c7e6a 100644
---
a/sql/api/src/main/scala/org/apache/spark/sql/catalyst/util/SparkCharVarcharUtils.scala
+++
b/sql/api/src/main/scala/org/apache/spark/sql/catalyst/util/SparkCharVarcharUtils.scala
@@ -55,20 +55,37 @@ trait SparkCharVarcharUtils {
}
/**
- * Replaces CharType/VarcharType with StringType recursively in the given
data type.
+ * Replaces CharType/VarcharType with StringType recursively in the given
data type. First-class
+ * CHAR/VARCHAR (standard semantics or preserveCharVarcharTypeInfo) is left
as-is.
*/
- def replaceCharVarcharWithString(dt: DataType): DataType = dt match {
+ def replaceCharVarcharWithString(dt: DataType): DataType = {
+ if (SqlApiConf.get.charVarcharFirstClassTypes) {
+ dt
+ } else {
+ replaceCharVarcharWithStringAlways(dt)
+ }
+ }
+
+ /**
+ * Always replaces CharType/VarcharType with unbounded StringType, including
when first-class
+ * CHAR/VARCHAR types are enabled. Used for Hive TRANSFORM script I/O, which
is
+ * STRING-on-the-wire regardless of the declared AS types.
+ */
+ private[sql] def replaceCharVarcharWithStringAlways(dt: DataType): DataType
= dt match {
case ArrayType(et, nullable) =>
- ArrayType(replaceCharVarcharWithString(et), nullable)
+ ArrayType(replaceCharVarcharWithStringAlways(et), nullable)
case MapType(kt, vt, nullable) =>
- MapType(replaceCharVarcharWithString(kt),
replaceCharVarcharWithString(vt), nullable)
+ MapType(
+ replaceCharVarcharWithStringAlways(kt),
+ replaceCharVarcharWithStringAlways(vt),
+ nullable)
case StructType(fields) =>
StructType(fields.map { field =>
- field.copy(dataType = replaceCharVarcharWithString(field.dataType))
+ field.copy(dataType =
replaceCharVarcharWithStringAlways(field.dataType))
})
- case c: CharType if !SqlApiConf.get.charVarcharFirstClassTypes =>
c.toStringType
- case v: VarcharType if !SqlApiConf.get.charVarcharFirstClassTypes =>
v.toStringType
- case _ => dt
+ case c: CharType => c.toStringType
+ case v: VarcharType => v.toStringType
+ case other => other
}
}
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala
index 3f0a83c51c46..84f4bd9ef25c 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala
@@ -1814,21 +1814,23 @@ class AstBuilder extends DataTypeAstBuilder
if (transformClause.setQuantifier != null) {
throw
QueryParsingErrors.transformNotSupportQuantifierError(transformClause.setQuantifier)
}
- // Create the attributes.
- val (attributes, schemaLess) = if (transformClause.colTypeList != null) {
+ // Create the attributes. Script I/O is always unbounded STRING (Hive wire
format).
+ // Declared CHAR/VARCHAR types are assigned in a Project below when
first-class types
+ // are enabled, using the original schema so quoted field names are not
re-parsed.
+ val (attributes, schema, schemaLess) = if (transformClause.colTypeList !=
null) {
// Typed return columns.
- val schema = createSchema(transformClause.colTypeList)
- val replacedSchema =
CharVarcharUtils.replaceCharVarcharWithStringInSchema(schema)
- (DataTypeUtils.toAttributes(replacedSchema), false)
+ val origSchema = createSchema(transformClause.colTypeList)
+ val replacedSchema =
CharVarcharUtils.replaceCharVarcharWithStringInSchemaAlways(origSchema)
+ (DataTypeUtils.toAttributes(replacedSchema), origSchema, false)
} else if (transformClause.identifierSeq != null) {
// Untyped return columns.
val attrs = visitIdentifierSeq(transformClause.identifierSeq).map { name
=>
AttributeReference(name, StringType, nullable = true)()
}
- (attrs, false)
+ (attrs, StructType(Nil), false)
} else {
(Seq(AttributeReference("key", StringType)(),
- AttributeReference("value", StringType)()), true)
+ AttributeReference("value", StringType)()), StructType(Nil), true)
}
val plan = visitCommonSelectQueryClausePlan(
@@ -1843,7 +1845,7 @@ class AstBuilder extends DataTypeAstBuilder
isDistinct = false,
isPipeOperatorSelect = false)
- ScriptTransformation(
+ val scriptTransform = ScriptTransformation(
string(visitStringLit(transformClause.script)),
attributes,
plan,
@@ -1856,6 +1858,29 @@ class AstBuilder extends DataTypeAstBuilder
schemaLess
)
)
+ // First-class CHAR/VARCHAR (standard semantics or
preserveCharVarcharTypeInfo):
+ // assign declared types in a Project so the result matches AS (...). Flag
off keeps
+ // the historical STRING result with no pad and no EXCEED_LIMIT_LENGTH.
The check can
+ // be pruned like scan-side assignment when the column is unreferenced.
+ if (conf.charVarcharFirstClassTypes &&
+ schema.exists(f => CharVarcharUtils.hasCharVarchar(f.dataType))) {
+ Project(
+ attributes.zip(schema).map { case (a, field) =>
+ if (CharVarcharUtils.hasCharVarchar(field.dataType)) {
+ // Keep the raw CHAR/VARCHAR metadata, matching scan-side
+ // readSidePadding. First-class CHAR vs STRING still promotes to
+ // STRING equality, so this metadata does not pad the literal.
+ Alias(
+ CharVarcharUtils.stringLengthCheck(a, field.dataType),
+ a.name)(explicitMetadata = Some(a.metadata))
+ } else {
+ a
+ }
+ },
+ scriptTransform)
+ } else {
+ scriptTransform
+ }
}
/**
@@ -2050,8 +2075,7 @@ class AstBuilder extends DataTypeAstBuilder
inSerdeClass, outSerdeClass,
inSerdeProps, outSerdeProps,
reader, writer,
- schemaLess,
- conf.charVarcharStandardSemantics)
+ schemaLess)
}
/**
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/ScriptTransformation.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/ScriptTransformation.scala
index 5532803eafc5..3a2cb2cd7309 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/ScriptTransformation.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/ScriptTransformation.scala
@@ -49,6 +49,4 @@ case class ScriptInputOutputSchema(
outputSerdeProps: Seq[(String, String)],
recordReaderClass: Option[String],
recordWriterClass: Option[String],
- schemaLess: Boolean,
- // Bound while the script is parsed under the direct-query or
persisted-view SQLConf.
- standardCharVarcharSemantics: Boolean = false)
+ schemaLess: Boolean)
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/CharVarcharUtils.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/CharVarcharUtils.scala
index dc628ac5551d..4ab621e672df 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/CharVarcharUtils.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/CharVarcharUtils.scala
@@ -63,11 +63,27 @@ object CharVarcharUtils extends Logging with
SparkCharVarcharUtils {
* re-construct the original data type with CharType/VarcharType later when
needed.
*/
def replaceCharVarcharWithStringInSchema(st: StructType): StructType = {
+ replaceCharVarcharWithStringInSchema(st, replaceCharVarcharWithString)
+ }
+
+ /**
+ * Like [[replaceCharVarcharWithStringInSchema]], but always rewrites the
physical type to
+ * unbounded STRING even when first-class CHAR/VARCHAR is enabled. Hive
TRANSFORM script I/O
+ * is STRING-on-the-wire; callers apply [[stringLengthCheck]] in a Project
when first-class
+ * CHAR/VARCHAR types are enabled.
+ */
+ def replaceCharVarcharWithStringInSchemaAlways(st: StructType): StructType =
{
+ replaceCharVarcharWithStringInSchema(st,
replaceCharVarcharWithStringAlways)
+ }
+
+ private def replaceCharVarcharWithStringInSchema(
+ st: StructType,
+ replace: DataType => DataType): StructType = {
StructType(st.map { field =>
if (hasCharVarchar(field.dataType)) {
val metadata = new MetadataBuilder().withMetadata(field.metadata)
.putString(CHAR_VARCHAR_TYPE_STRING_METADATA_KEY,
field.dataType.catalogString).build()
- field.copy(dataType = replaceCharVarcharWithString(field.dataType),
metadata = metadata)
+ field.copy(dataType = replace(field.dataType), metadata = metadata)
} else {
field
}
@@ -258,10 +274,8 @@ object CharVarcharUtils extends Logging with
SparkCharVarcharUtils {
if (struct.valExprs.forall(_.isInstanceOf[GetStructField])) {
// No field needs char/varchar processing, just return the original
expression.
expr
- } else if (expr.nullable) {
- If(IsNull(expr), Literal(null, struct.dataType), struct)
} else {
- struct
+ nullSafeRebuiltStruct(expr, struct)
}
case ArrayType(et, containsNull) =>
@@ -285,6 +299,19 @@ object CharVarcharUtils extends Logging with
SparkCharVarcharUtils {
}
}
+ /**
+ * Guard a rebuilt named_struct so a NULL input stays NULL. Use CaseWhen
rather than If:
+ * CollationTypeCoercion rewrites If (no same-type skip) and can widen
VARCHAR collations
+ * inside the struct to unbounded STRING. CaseWhen skips that when the
branches already match.
+ */
+ private def nullSafeRebuiltStruct(expr: Expression, struct:
CreateNamedStruct): Expression = {
+ if (expr.nullable) {
+ CaseWhen(Seq((IsNull(expr), Literal(null, struct.dataType))),
Some(struct))
+ } else {
+ struct
+ }
+ }
+
private def processStringForCharVarcharInArray(
arr: Expression,
et: DataType,
@@ -391,11 +418,7 @@ object CharVarcharUtils extends Logging with
SparkCharVarcharUtils {
// GetStructField on a NULL struct yields NULL fields, which
CreateNamedStruct would
// turn into a non-NULL struct of NULLs. Guard it the same way the
scan-side rewrite
// in processStringForCharVarchar does.
- Some(if (expr.nullable) {
- If(IsNull(expr), Literal(null, struct.dataType), struct)
- } else {
- struct
- })
+ Some(nullSafeRebuiltStruct(expr, struct))
} else {
None
}
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
index e05931e39143..933417a98188 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
@@ -7682,7 +7682,9 @@ object SQLConf {
"for SQL standard CHAR/VARCHAR behavior (CAST/LCT/STRING-returning
transforms). " +
"ORC reads with a CHAR/VARCHAR schema over STRING storage return the
stored values " +
"without ORC truncation, matching Parquet. Read-side length checks apply
only when " +
- "spark.sql.charVarchar.standardSemantics.enabled is true.")
+ "spark.sql.charVarchar.standardSemantics.enabled is true. Hive
TRANSFORM/MAP/REDUCE " +
+ "with CHAR/VARCHAR AS types still applies Spark assignment (pad /
EXCEED_LIMIT_LENGTH) " +
+ "when this flag is true, because script I/O is STRING-on-the-wire.")
.version("4.0.0")
.booleanConf
.createWithDefault(false)
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/BaseScriptTransformationExec.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/BaseScriptTransformationExec.scala
index 535db0d82fec..98d775102043 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/BaseScriptTransformationExec.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/BaseScriptTransformationExec.scala
@@ -31,28 +31,10 @@ import org.apache.spark.internal.Logging
import org.apache.spark.internal.LogKeys._
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.catalyst.{CatalystTypeConverters, InternalRow}
-import org.apache.spark.sql.catalyst.expressions.{
- Attribute,
- AttributeSet,
- BoundReference,
- Cast,
- Expression,
- GenericInternalRow,
- JsonToStructs,
- Literal,
- StructsToJson,
- UnsafeProjection}
+import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeSet,
Cast, Expression, GenericInternalRow, JsonToStructs, Literal, StructsToJson,
UnsafeProjection}
import org.apache.spark.sql.catalyst.plans.logical.ScriptInputOutputSchema
import org.apache.spark.sql.catalyst.plans.physical.Partitioning
-import org.apache.spark.sql.catalyst.util.{
- ArrayBasedMapBuilder,
- ArrayData,
- CharVarcharCodegenUtils,
- CharVarcharUtils,
- DateTimeUtils,
- GenericArrayData,
- IntervalUtils,
- MapData}
+import org.apache.spark.sql.catalyst.util.{CharVarcharUtils, DateTimeUtils,
IntervalUtils}
import org.apache.spark.sql.errors.QueryExecutionErrors
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.types._
@@ -80,11 +62,6 @@ trait BaseScriptTransformationExec extends UnaryExecNode {
override def outputPartitioning: Partitioning = child.outputPartitioning
- // Bound at parse into the I/O schema under the query or persisted-view
SQLConf.
- // Do not re-read SQLConf: planning and task conf can differ from that
captured mode.
- private def standardCharVarcharSemantics: Boolean =
- ioschema.standardCharVarcharSemantics
-
override def doExecute(): RDD[InternalRow] = {
val broadcastedHadoopConf =
new SerializableConfiguration(session.sessionState.newHadoopConf())
@@ -224,19 +201,12 @@ trait BaseScriptTransformationExec extends UnaryExecNode {
private lazy val outputFieldWriters: Seq[String => Any] = output.map { attr
=>
val converter =
CatalystTypeConverters.createToCatalystConverter(attr.dataType)
attr.dataType match {
- case dt @ (_: CharType | _: VarcharType) =>
- // Without standard semantics, no-SerDe scalar CHAR/VARCHAR stays
unsupported
- // (they extend StringType, so this case must come before `_:
StringType`).
- if (!standardCharVarcharSemantics) {
- throw
QueryExecutionErrors.scriptTransformWithoutSerdeUnsupportedTypeError(dt)
- }
- // Do not use the SerDe null-on-error wrapper; that would hide
EXCEED_LIMIT_LENGTH.
- (data: String) =>
- if (data == ioschema.outputRowFormatMap("TOK_TABLEROWFORMATNULL")) {
- null
- } else {
- converter(data)
- }
+ // CHAR/VARCHAR must not reach the script reader; AstBuilder rewrites
them to
+ // unbounded STRING and applies assignment in a Project when first-class
types
+ // are enabled. Reject leftover CHAR/VARCHAR, including nested
(hand-built plans).
+ case dt if CharVarcharUtils.hasCharVarchar(dt) =>
+ throw
QueryExecutionErrors.scriptTransformWithoutSerdeUnsupportedTypeError(dt)
+ // Match collated STRING too (CHAR/VARCHAR COLLATE rewrite via
toStringType).
case _: StringType => wrapperConvertException(data => data, converter)
case BooleanType => wrapperConvertException(data => data.toBoolean,
converter)
case ByteType => wrapperConvertException(data => data.toByte, converter)
@@ -277,34 +247,6 @@ trait BaseScriptTransformationExec extends UnaryExecNode {
data => IntervalUtils.microsToDuration(
IntervalUtils.castStringToDTInterval(UTF8String.fromString(data),
start, end)),
converter)
- case dt @ (_: ArrayType | _: MapType | _: StructType)
- if standardCharVarcharSemantics &&
CharVarcharUtils.hasCharVarchar(dt) =>
- val physicalType =
ScriptTransformationIOSchema.toUnboundedStringType(dt)
- // JSON object keys are strings. Restore non-string keys inside the
malformed
- // wrapper, then apply CHAR/VARCHAR (and CHAR/VARCHAR map-key dedup)
outside it.
- val jsonType =
ScriptTransformationIOSchema.toJsonMapKeyType(physicalType)
- val tz = Some(conf.sessionLocalTimeZone)
- val complexTypeFactory = JsonToStructs(
- jsonType,
- ioschema.outputSerdeProps.toMap,
- Literal(null),
- tz)
- val restoreJsonKeys = if (jsonType.sameType(physicalType)) {
- identity[Any] _
- } else {
- ScriptTransformationIOSchema.makeJsonToTargetRestorer(
- jsonType, physicalType, tz)
- }
- val toDeclaredType =
ScriptTransformationIOSchema.makeJsonToTargetRestorer(
- physicalType, dt, tz)
- val parser = wrapperConvertException(
- data => restoreJsonKeys(
- complexTypeFactory.nullSafeEval(UTF8String.fromString(data))),
- identity)
- (data: String) => {
- val parsed = parser(data)
- if (parsed == null) null else toDeclaredType(parsed)
- }
case _: ArrayType | _: MapType | _: StructType =>
val complexTypeFactory = JsonToStructs(attr.dataType,
ioschema.outputSerdeProps.toMap, Literal(null),
Some(conf.sessionLocalTimeZone))
@@ -432,8 +374,7 @@ case class ScriptTransformationIOSchema(
outputSerdeProps: Seq[(String, String)],
recordReaderClass: Option[String],
recordWriterClass: Option[String],
- schemaLess: Boolean,
- standardCharVarcharSemantics: Boolean = false) extends Serializable {
+ schemaLess: Boolean) extends Serializable {
import ScriptTransformationIOSchema._
val inputRowFormatMap = inputRowFormat.toMap.withDefault((k) =>
defaultFormat(k))
@@ -441,115 +382,13 @@ case class ScriptTransformationIOSchema(
}
object ScriptTransformationIOSchema {
- private[sql] def toUnboundedStringType(dataType: DataType): DataType = {
- dataType.transformRecursively {
- case c: CharType => c.toStringType
- case v: VarcharType => v.toStringType
- }
- }
-
- // JSON object keys are always strings. Rewrite every map key, including
nested maps.
- // `transformRecursively` would stop at the first matching MapType and skip
children.
- private[sql] def toJsonMapKeyType(dataType: DataType): DataType = dataType
match {
- case ArrayType(et, n) => ArrayType(toJsonMapKeyType(et), n)
- case MapType(kt, vt, n) =>
- val jsonKey = if (kt.isInstanceOf[StringType]) kt else StringType
- MapType(jsonKey, toJsonMapKeyType(vt), n)
- case StructType(fields) =>
- StructType(fields.map(f => f.copy(dataType =
toJsonMapKeyType(f.dataType))))
- case other => other
- }
-
- /**
- * Convert a Catalyst value from `sourceType` to `targetType`. `sourceType`
is
- * the parsed Catalyst type (unbounded STRING in place of CHAR/VARCHAR, and
- * STRING map keys where JSON cannot preserve the declared key type).
- * CHAR/VARCHAR leaves use write-side length checks. Maps rebuild through a
- * fresh [[ArrayBasedMapBuilder]] when the source and target map types differ
- * or the target contains CHAR/VARCHAR, so CHAR/VARCHAR key collisions follow
- * MAP_KEY_DEDUP_POLICY. Same-typed maps without CHAR/VARCHAR are unchanged.
- */
- private[sql] def makeJsonToTargetRestorer(
- sourceType: DataType,
- targetType: DataType,
- timeZoneId: Option[String]): Any => Any = {
-
- def make(jt: DataType, tt: DataType): Any => Any = {
- if (jt.sameType(tt) && !CharVarcharUtils.hasCharVarchar(tt)) {
- identity
- } else (jt, tt) match {
- case (_, c: CharType) =>
- (input: Any) =>
- CharVarcharCodegenUtils.charTypeWriteSideCheck(
- input.asInstanceOf[UTF8String], c.length)
- case (_, v: VarcharType) =>
- (input: Any) =>
- CharVarcharCodegenUtils.varcharTypeWriteSideCheck(
- input.asInstanceOf[UTF8String], v.length)
- case (ArrayType(jet, _), ArrayType(tet, _)) =>
- val elem = make(jet, tet)
- (input: Any) => {
- val arr = input.asInstanceOf[ArrayData]
- val n = arr.numElements()
- val out = new Array[Any](n)
- var i = 0
- while (i < n) {
- out(i) = if (arr.isNullAt(i)) null
- else elem(arr.get(i, jet))
- i += 1
- }
- new GenericArrayData(out)
- }
- case (MapType(jkt, jvt, _), MapType(tkt, tvt, _)) =>
- val keyConvert = make(jkt, tkt)
- val valRestore = make(jvt, tvt)
- (input: Any) => {
- val map = input.asInstanceOf[MapData]
- val n = map.numElements()
- val builder = new ArrayBasedMapBuilder(tkt, tvt)
- var i = 0
- while (i < n) {
- val k = keyConvert(map.keyArray().get(i, jkt))
- val v = if (map.valueArray().isNullAt(i)) null
- else valRestore(map.valueArray().get(i, jvt))
- builder.put(k, v)
- i += 1
- }
- builder.build()
- }
- case (js: StructType, ts: StructType) =>
- val restorers = js.fields.zip(ts.fields).map {
- case (jf, tf) => make(jf.dataType, tf.dataType)
- }
- (input: Any) => {
- val row = input.asInstanceOf[InternalRow]
- val out = new GenericInternalRow(ts.length)
- var i = 0
- while (i < ts.length) {
- if (row.isNullAt(i)) out.setNullAt(i)
- else out.update(i,
- restorers(i)(row.get(i, js(i).dataType)))
- i += 1
- }
- out
- }
- case _ if !jt.sameType(tt) =>
- val c = Cast(BoundReference(0, jt, nullable = false), tt, timeZoneId)
- (input: Any) => c.eval(InternalRow(input))
- case _ => identity
- }
- }
-
- make(sourceType, targetType)
- }
-
val defaultFormat = Map(
("TOK_TABLEROWFORMATFIELD", "\u0001"),
("TOK_TABLEROWFORMATLINES", "\n"),
("TOK_TABLEROWFORMATNULL" -> "\\N")
)
- def defaultIOSchema: ScriptTransformationIOSchema =
ScriptTransformationIOSchema(
+ val defaultIOSchema = ScriptTransformationIOSchema(
inputRowFormat = Seq.empty,
outputRowFormat = Seq.empty,
inputSerdeClass = None,
@@ -558,8 +397,7 @@ object ScriptTransformationIOSchema {
outputSerdeProps = Seq.empty,
recordReaderClass = None,
recordWriterClass = None,
- schemaLess = false,
- standardCharVarcharSemantics = SQLConf.get.charVarcharStandardSemantics
+ schemaLess = false
)
def apply(input: ScriptInputOutputSchema): ScriptTransformationIOSchema = {
@@ -572,7 +410,6 @@ object ScriptTransformationIOSchema {
input.outputSerdeProps,
input.recordReaderClass,
input.recordWriterClass,
- input.schemaLess,
- input.standardCharVarcharSemantics)
+ input.schemaLess)
}
}
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/SparkSqlParser.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/SparkSqlParser.scala
index 87213a1619ab..7747c94b584d 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/SparkSqlParser.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/SparkSqlParser.scala
@@ -1347,8 +1347,7 @@ class SparkSqlAstBuilder extends AstBuilder {
inSerdeClass, outSerdeClass,
inSerdeProps, outSerdeProps,
reader, writer,
- schemaLess,
- conf.charVarcharStandardSemantics)
+ schemaLess)
}
}
diff --git
a/sql/core/src/test/resources/sql-tests/analyzer-results/transform.sql.out
b/sql/core/src/test/resources/sql-tests/analyzer-results/transform.sql.out
index fe7aa8b4282b..1e3ddd1c198d 100644
--- a/sql/core/src/test/resources/sql-tests/analyzer-results/transform.sql.out
+++ b/sql/core/src/test/resources/sql-tests/analyzer-results/transform.sql.out
@@ -67,7 +67,7 @@ SELECT TRANSFORM(a)
USING 'cat' AS (a)
FROM t
-- !query analysis
-ScriptTransformation cat, [a#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [a#x]
+- SubqueryAlias t
+- View (`t`, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x, k#x,
l#x])
@@ -98,7 +98,7 @@ SELECT a, b, decode(c, 'UTF-8'), d, e, f, g, h, i, j, k, l
FROM (
-- !query analysis
Project [a#x, b#x, decode(c#x, UTF-8) AS decode(c, UTF-8)#x, d#x, e#x, f#x,
g#xL, h#x, i#x, j#x, k#x, l#x]
+- SubqueryAlias tmp
- +- ScriptTransformation cat, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x,
j#x, k#x, l#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ +- ScriptTransformation cat, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x,
j#x, k#x, l#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x, k#x, l#x]
+- SubqueryAlias t
+- View (`t`, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x,
k#x, l#x])
@@ -129,7 +129,7 @@ SELECT a, b, decode(c, 'UTF-8'), d, e, f, g, h, i, j, k, l
FROM (
-- !query analysis
Project [a#x, b#x, decode(c#x, UTF-8) AS decode(c, UTF-8)#x, d#x, e#x, f#x,
g#x, h#x, i#x, j#x, k#x, l#x]
+- SubqueryAlias tmp
- +- ScriptTransformation cat, [a#x, b#x, c#x, d#x, e#x, f#x, g#x, h#x, i#x,
j#x, k#x, l#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ +- ScriptTransformation cat, [a#x, b#x, c#x, d#x, e#x, f#x, g#x, h#x, i#x,
j#x, k#x, l#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x, k#x, l#x]
+- SubqueryAlias t
+- View (`t`, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x,
k#x, l#x])
@@ -144,7 +144,7 @@ SELECT TRANSFORM(a)
USING 'cat'
FROM t
-- !query analysis
-ScriptTransformation cat, [key#x, value#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,true,false)
+ScriptTransformation cat, [key#x, value#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,true)
+- Project [a#x]
+- SubqueryAlias t
+- View (`t`, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x, k#x,
l#x])
@@ -159,7 +159,7 @@ SELECT TRANSFORM(a, b)
USING 'cat'
FROM t
-- !query analysis
-ScriptTransformation cat, [key#x, value#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,true,false)
+ScriptTransformation cat, [key#x, value#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,true)
+- Project [a#x, b#x]
+- SubqueryAlias t
+- View (`t`, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x, k#x,
l#x])
@@ -174,7 +174,7 @@ SELECT TRANSFORM(a, b, c)
USING 'cat'
FROM t
-- !query analysis
-ScriptTransformation cat, [key#x, value#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,true,false)
+ScriptTransformation cat, [key#x, value#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,true)
+- Project [a#x, b#x, c#x]
+- SubqueryAlias t
+- View (`t`, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x, k#x,
l#x])
@@ -190,7 +190,7 @@ USING 'cat' AS (a int, b short, c long, d byte, e float, f
double, g decimal(38,
FROM VALUES
('a','','1231a','a','213.21a','213.21a','0a.21d','2000-04-01123','1997-0102
00:00:') tmp(a, b, c, d, e, f, g, h, i)
-- !query analysis
-ScriptTransformation cat, [a#x, b#x, c#xL, d#x, e#x, f#x, g#x, h#x, i#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x, c#xL, d#x, e#x, f#x, g#x, h#x, i#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x, c#x, d#x, e#x, f#x, g#x, h#x, i#x]
+- SubqueryAlias tmp
+- LocalRelation [a#x, b#x, c#x, d#x, e#x, f#x, g#x, h#x, i#x]
@@ -202,7 +202,7 @@ USING 'cat' AS (a, b)
FROM t
GROUP BY b
-- !query analysis
-ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Aggregate [b#x], [b#x, max(a#x) AS max(a)#x, sum(f#x) AS sum(f)#xL]
+- SubqueryAlias t
+- View (`t`, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x, k#x,
l#x])
@@ -215,7 +215,7 @@ ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None
-- !query
MAP a, b USING 'cat' AS (a, b) FROM t
-- !query analysis
-ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x]
+- SubqueryAlias t
+- View (`t`, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x, k#x,
l#x])
@@ -228,7 +228,7 @@ ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None
-- !query
REDUCE a, b USING 'cat' AS (a, b) FROM t
-- !query analysis
-ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x]
+- SubqueryAlias t
+- View (`t`, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x, k#x,
l#x])
@@ -253,7 +253,7 @@ FROM t
-- !query analysis
ScriptTransformation cat, [a#x, b#x, c#x, d#x],
ScriptInputOutputSchema(List((TOK_TABLEROWFORMATFIELD,@),
(TOK_TABLEROWFORMATNULL,NULL), (TOK_TABLEROWFORMATLINES,
)),List((TOK_TABLEROWFORMATFIELD,@), (TOK_TABLEROWFORMATNULL,NULL),
(TOK_TABLEROWFORMATLINES,
-)),None,None,List(),List(),None,None,false,false)
+)),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x, c#x, null AS NULL#x]
+- SubqueryAlias t
+- View (`t`, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x, k#x,
l#x])
@@ -278,7 +278,7 @@ FROM t
-- !query analysis
ScriptTransformation cat, [d#x],
ScriptInputOutputSchema(List((TOK_TABLEROWFORMATFIELD,@),
(TOK_TABLEROWFORMATNULL,NULL), (TOK_TABLEROWFORMATLINES,
)),List((TOK_TABLEROWFORMATFIELD,@), (TOK_TABLEROWFORMATNULL,NULL),
(TOK_TABLEROWFORMATLINES,
-)),None,None,List(),List(),None,None,false,false)
+)),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x, c#x, null AS NULL#x]
+- SubqueryAlias t
+- View (`t`, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x, k#x,
l#x])
@@ -302,7 +302,7 @@ FROM t
-- !query analysis
ScriptTransformation cat, [a#x, b#x, c#x, d#x],
ScriptInputOutputSchema(List((TOK_TABLEROWFORMATFIELD,@),
(TOK_TABLEROWFORMATLINES,
)),List((TOK_TABLEROWFORMATFIELD,@), (TOK_TABLEROWFORMATNULL,NULL),
(TOK_TABLEROWFORMATLINES,
-)),None,None,List(),List(),None,None,false,false)
+)),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x, c#x, null AS NULL#x]
+- SubqueryAlias t
+- View (`t`, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x, k#x,
l#x])
@@ -325,7 +325,7 @@ FROM t
-- !query analysis
ScriptTransformation cat, [a#x, b#x, c#x, d#x],
ScriptInputOutputSchema(List((TOK_TABLEROWFORMATFIELD,@),
(TOK_TABLEROWFORMATLINES,
)),List((TOK_TABLEROWFORMATFIELD,@), (TOK_TABLEROWFORMATLINES,
-)),None,None,List(),List(),None,None,false,false)
+)),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x, c#x, null AS NULL#x]
+- SubqueryAlias t
+- View (`t`, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x, k#x,
l#x])
@@ -349,7 +349,7 @@ FROM t
-- !query analysis
ScriptTransformation cat, [a#x, b#x, c#x, d#x],
ScriptInputOutputSchema(List((TOK_TABLEROWFORMATFIELD,@),
(TOK_TABLEROWFORMATNULL,XXXX), (TOK_TABLEROWFORMATLINES,
)),List((TOK_TABLEROWFORMATFIELD,@), (TOK_TABLEROWFORMATLINES,
-)),None,None,List(),List(),None,None,false,false)
+)),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x, c#x, null AS NULL#x]
+- SubqueryAlias t
+- View (`t`, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x, k#x,
l#x])
@@ -374,7 +374,7 @@ FROM t
ScriptTransformation cat, [a#x, b#x, c#x, d#x],
ScriptInputOutputSchema(List((TOK_TABLEROWFORMATFIELD,@),
(TOK_TABLEROWFORMATNULL,
), (TOK_TABLEROWFORMATLINES,
)),List((TOK_TABLEROWFORMATFIELD,@), (TOK_TABLEROWFORMATLINES,
-)),None,None,List(),List(),None,None,false,false)
+)),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x, c#x, null AS NULL#x]
+- SubqueryAlias t
+- View (`t`, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x, k#x,
l#x])
@@ -415,7 +415,7 @@ Project [a#x, b#x, decode(c#x, UTF-8) AS decode(c,
UTF-8)#x, d#x, e#x, f#x, g#xL
+- SubqueryAlias tmp
+- ScriptTransformation cat, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x,
j#x, k#x, l#x], ScriptInputOutputSchema(List((TOK_TABLEROWFORMATFIELD,,),
(TOK_TABLEROWFORMATNULL,NULL), (TOK_TABLEROWFORMATLINES,
)),List((TOK_TABLEROWFORMATFIELD,,), (TOK_TABLEROWFORMATNULL,NULL),
(TOK_TABLEROWFORMATLINES,
-)),None,None,List(),List(),None,None,false,false)
+)),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x, k#x, l#x]
+- SubqueryAlias t
+- View (`t`, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x,
k#x, l#x])
@@ -456,7 +456,7 @@ Project [a#x, b#xL, decode(c#x, UTF-8) AS decode(c,
UTF-8)#x, d#x, e#x, f#x, g#x
+- SubqueryAlias tmp
+- ScriptTransformation cat, [a#x, b#xL, c#x, d#x, e#x, f#x, g#xL, h#x,
i#x, j#x, k#x, l#xL], ScriptInputOutputSchema(List((TOK_TABLEROWFORMATFIELD,,),
(TOK_TABLEROWFORMATNULL,NULL), (TOK_TABLEROWFORMATLINES,
)),List((TOK_TABLEROWFORMATFIELD,,), (TOK_TABLEROWFORMATNULL,NULL),
(TOK_TABLEROWFORMATLINES,
-)),None,None,List(),List(),None,None,false,false)
+)),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x, k#x, l#x]
+- SubqueryAlias t
+- View (`t`, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x, j#x,
k#x, l#x])
@@ -516,7 +516,7 @@ SELECT TRANSFORM(b, a, CAST(c AS STRING))
FROM script_trans
WHERE a <= 4
-- !query analysis
-ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [b#x, a#x, cast(c#x as string) AS c#x]
+- Filter (a#x <= 4)
+- SubqueryAlias script_trans
@@ -533,7 +533,7 @@ SELECT TRANSFORM(1, 2, 3)
FROM script_trans
WHERE a <= 4
-- !query analysis
-ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [1 AS 1#x, 2 AS 2#x, 3 AS 3#x]
+- Filter (a#x <= 4)
+- SubqueryAlias script_trans
@@ -552,7 +552,7 @@ LIMIT 1
-- !query analysis
GlobalLimit 1
+- LocalLimit 1
- +- ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ +- ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [1 AS 1#x, 2 AS 2#x]
+- SubqueryAlias script_trans
+- View (`script_trans`, [a#x, b#x, c#x])
@@ -573,7 +573,7 @@ SELECT TRANSFORM(
FROM script_trans
WHERE a <= 4
-- !query analysis
-ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [b#x, a#x, CASE WHEN (c#x > 100) THEN 1 WHEN (c#x < 100) THEN 2
ELSE 3 END AS CASE WHEN (c > 100) THEN 1 WHEN (c < 100) THEN 2 ELSE 3 END#x]
+- Filter (a#x <= 4)
+- SubqueryAlias script_trans
@@ -590,7 +590,7 @@ SELECT TRANSFORM(b, a, c + 1)
FROM script_trans
WHERE a <= 4
-- !query analysis
-ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [b#x, a#x, (c#x + 1) AS (c + 1)#x]
+- Filter (a#x <= 4)
+- SubqueryAlias script_trans
@@ -607,7 +607,7 @@ SELECT TRANSFORM(*)
FROM script_trans
WHERE a <= 4
-- !query analysis
-ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x, c#x]
+- Filter (a#x <= 4)
+- SubqueryAlias script_trans
@@ -625,7 +625,7 @@ FROM script_trans
WHERE a <= 4
GROUP BY b
-- !query analysis
-ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Aggregate [b#x], [b#x, max(a#x) AS max(a)#x, cast(sum(c#x) as string) AS
CAST(sum(c) AS STRING)#x]
+- Filter (a#x <= 4)
+- SubqueryAlias script_trans
@@ -643,7 +643,7 @@ FROM script_trans
WHERE a <= 4
GROUP BY b
-- !query analysis
-ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Aggregate [b#x], [b#x, max(a#x) FILTER (WHERE (a#x > 3)) AS max(a) FILTER
(WHERE (a > 3))#x, cast(sum(c#x) as string) AS CAST(sum(c) AS STRING)#x]
+- Filter (a#x <= 4)
+- SubqueryAlias script_trans
@@ -661,7 +661,7 @@ FROM script_trans
WHERE a <= 2
GROUP BY b
-- !query analysis
-ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Aggregate [b#x], [b#x, max(a#x) AS max(a)#x, cast(sum(c#x) as string) AS
CAST(sum(c) AS STRING)#x]
+- Filter (a#x <= 2)
+- SubqueryAlias script_trans
@@ -680,7 +680,7 @@ WHERE a <= 4
GROUP BY b
HAVING MAX(a) > 0
-- !query analysis
-ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Filter (max(a)#x > 0)
+- Aggregate [b#x], [b#x, max(a#x) AS max(a)#x, cast(sum(c#x) as string) AS
CAST(sum(c) AS STRING)#x]
+- Filter (a#x <= 4)
@@ -700,7 +700,7 @@ WHERE a <= 4
GROUP BY b
HAVING MAX(a) > 1
-- !query analysis
-ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Filter (max(a)#x > 1)
+- Aggregate [b#x], [b#x, max(a#x) AS max(a)#x, cast(sum(c#x) as string) AS
CAST(sum(c) AS STRING)#x]
+- Filter (a#x <= 4)
@@ -719,7 +719,7 @@ FROM script_trans
WHERE a <= 4
WINDOW w AS (PARTITION BY b ORDER BY a)
-- !query analysis
-ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x, c#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [b#x, max(a) OVER (PARTITION BY b ORDER BY a ASC NULLS FIRST RANGE
BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)#x, CAST(sum(c) OVER (PARTITION BY
b ORDER BY a ASC NULLS FIRST RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
AS STRING)#x]
+- Project [b#x, a#x, c#x, max(a) OVER (PARTITION BY b ORDER BY a ASC NULLS
FIRST RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)#x, _we1#xL, max(a)
OVER (PARTITION BY b ORDER BY a ASC NULLS FIRST RANGE BETWEEN UNBOUNDED
PRECEDING AND CURRENT ROW)#x, cast(_we1#xL as string) AS CAST(sum(c) OVER
(PARTITION BY b ORDER BY a ASC NULLS FIRST RANGE BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW) AS STRING)#x]
+- Window [max(a#x) windowspecdefinition(b#x, a#x ASC NULLS FIRST,
specifiedwindowframe(RangeFrame, unboundedpreceding$(), currentrow$())) AS
max(a) OVER (PARTITION BY b ORDER BY a ASC NULLS FIRST RANGE BETWEEN UNBOUNDED
PRECEDING AND CURRENT ROW)#x, sum(c#x) windowspecdefinition(b#x, a#x ASC NULLS
FIRST, specifiedwindowframe(RangeFrame, unboundedpreceding$(), currentrow$()))
AS _we1#xL], [b#x], [a#x ASC NULLS FIRST]
@@ -743,7 +743,7 @@ WHERE a <= 4
GROUP BY b, myCol, myCol2
HAVING max(a) > 1
-- !query analysis
-ScriptTransformation cat, [a#x, b#x, c#x, d#x, e#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x, c#x, d#x, e#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Filter (max(a)#x > 1)
+- Aggregate [b#x, myCol#x, myCol2#x], [b#x, max(a#x) AS max(a)#x,
cast(sum(c#x) as string) AS CAST(sum(c) AS STRING)#x, myCol#x, myCol2#x]
+- Filter (a#x <= 4)
@@ -767,7 +767,7 @@ SELECT a + 1
-- !query analysis
Project [(a#x + 1) AS (a + 1)#x]
+- SubqueryAlias t
- +- ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ +- ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x]
+- SubqueryAlias script_trans
+- View (`script_trans`, [a#x, b#x, c#x])
@@ -790,7 +790,7 @@ ORDER BY result
Sort [result#xL ASC NULLS FIRST], true
+- Project [(cast(b#x as bigint) + cast(1 as bigint)) AS result#xL]
+- SubqueryAlias t
- +- ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ +- ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Aggregate [a#x], [a#x, sum(b#x) AS sum(b)#xL]
+- SubqueryAlias script_trans
+- View (`script_trans`, [a#x, b#x, c#x])
@@ -803,7 +803,7 @@ Sort [result#xL ASC NULLS FIRST], true
-- !query
MAP k / 10 USING 'cat' AS (one) FROM (SELECT 10 AS k)
-- !query analysis
-ScriptTransformation cat, [one#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [one#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [(cast(k#x as double) / cast(10 as double)) AS (k / 10)#x]
+- SubqueryAlias __auto_generated_subquery_name
+- Project [10 AS k#x]
@@ -815,7 +815,7 @@ FROM (SELECT 1 AS key, 100 AS value) src
MAP src.*, src.key, CAST(src.key / 10 AS INT), CAST(src.key % 10 AS INT),
src.value
USING 'cat' AS (k, v, tkey, ten, one, tvalue)
-- !query analysis
-ScriptTransformation cat, [k#x, v#x, tkey#x, ten#x, one#x, tvalue#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [k#x, v#x, tkey#x, ten#x, one#x, tvalue#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [key#x, value#x, key#x, cast((cast(key#x as double) / cast(10 as
double)) as int) AS CAST((key / 10) AS INT)#x, cast((key#x % 10) as int) AS
CAST((key % 10) AS INT)#x, value#x]
+- SubqueryAlias src
+- Project [1 AS key#x, 100 AS value#x]
@@ -828,7 +828,7 @@ SELECT TRANSFORM(1)
FROM script_trans
HAVING true
-- !query analysis
-ScriptTransformation cat, [a#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Filter cast(true as boolean)
+- Aggregate [1 AS 1#x]
+- SubqueryAlias script_trans
@@ -851,7 +851,7 @@ SELECT TRANSFORM(1)
FROM script_trans
HAVING true
-- !query analysis
-ScriptTransformation cat, [a#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Filter cast(true as boolean)
+- Project [1 AS 1#x]
+- SubqueryAlias script_trans
@@ -877,7 +877,7 @@ SELECT t1.b FROM temp t1 JOIN temp t2 ON t1.b = t2.b
WithCTE
:- CTERelationDef xxxx, false
: +- SubqueryAlias temp
-: +- ScriptTransformation cat, [b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+: +- ScriptTransformation cat, [b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
: +- Project [a#x]
: +- SubqueryAlias t
: +- View (`t`, [a#x, b#x, c#x, d#x, e#x, f#x, g#xL, h#x, i#x,
j#x, k#x, l#x])
@@ -999,12 +999,12 @@ FROM (
SELECT TRANSFORM(a, b)
USING 'cat' AS (a, b)
-- !query analysis
-ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x]
+- SubqueryAlias map_output
+- Sort [a#x ASC NULLS FIRST], false
+- RepartitionByExpression [a#x]
- +- ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ +- ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x]
+- SubqueryAlias complex_trans
+- View (`complex_trans`, [a#x, b#x])
@@ -1024,11 +1024,11 @@ FROM (
SELECT TRANSFORM(a, b)
USING 'cat' AS (a, b)
-- !query analysis
-ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x]
+- SubqueryAlias map_output
+- Sort [a#x ASC NULLS FIRST], true
- +- ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ +- ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x]
+- SubqueryAlias complex_trans
+- View (`complex_trans`, [a#x, b#x])
@@ -1043,7 +1043,7 @@ SELECT TRANSFORM (a, b)
USING 'cat' AS (a CHAR(10), b VARCHAR(10))
FROM VALUES('apache', 'spark') t(a, b)
-- !query analysis
-ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false,false)
+ScriptTransformation cat, [a#x, b#x],
ScriptInputOutputSchema(List(),List(),None,None,List(),List(),None,None,false)
+- Project [a#x, b#x]
+- SubqueryAlias t
+- LocalRelation [a#x, b#x]
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 f9f5062b1dfc..8276f34a2255 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
@@ -3413,7 +3413,9 @@ class FileSourceCharVarcharTestSuite extends
CharVarcharTestSuite with SharedSpa
test("SPARK-58794: EXTERNAL TABLE CHAR/VARCHAR pad, assignment, and
oversize") {
// External file tables use the same store assignment on write and pad +
- // length-check on scan. Hive TRANSFORM is out of scope for this epic.
+ // length-check on scan. Hive TRANSFORM uses the same assignment via
Project
+ // (SPARK-60090); covered in BaseScriptTransformationSuite,
+ // SparkScriptTransformationSuite, and HiveScriptTransformationSuite.
withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
withTempPath { dir =>
val path = dir.getCanonicalPath
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/BaseScriptTransformationSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/BaseScriptTransformationSuite.scala
index f7fb05526d96..e95f093c8c32 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/execution/BaseScriptTransformationSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/BaseScriptTransformationSuite.scala
@@ -26,7 +26,8 @@ import org.json4s.jackson.JsonMethods._
import org.scalatest.Assertions._
import org.scalatest.exceptions.TestFailedException
-import org.apache.spark.{SparkException, SparkThrowable, TaskContext,
TestUtils}
+import org.apache.spark.{SparkException, SparkRuntimeException,
SparkThrowable, TaskContext,
+ TestUtils}
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{QueryTest, Row}
import org.apache.spark.sql.catalyst.InternalRow
@@ -87,6 +88,20 @@ abstract class BaseScriptTransformationSuite extends
QueryTest {
parameters = Map("dataType" -> sqlType))
}
+ protected def checkExceedLimitLength(exception: Throwable, limit: String):
Unit = {
+ val runtimeException = Iterator.iterate[Throwable](exception)(_.getCause)
+ .takeWhile(_ != null)
+ .collectFirst {
+ case s: SparkRuntimeException if s.getCondition ==
"EXCEED_LIMIT_LENGTH" => s
+ }.getOrElse {
+ fail(s"expected EXCEED_LIMIT_LENGTH, got $exception")
+ }
+ checkError(
+ exception = runtimeException,
+ condition = "EXCEED_LIMIT_LENGTH",
+ parameters = Map("limit" -> limit))
+ }
+
test("cat without SerDe") {
assume(TestUtils.testCommandAvailable("/bin/bash"))
@@ -103,360 +118,196 @@ abstract class BaseScriptTransformationSuite extends
QueryTest {
assert(uncaughtExceptionHandler.exception.isEmpty)
}
- test("SPARK-59277: TRANSFORM output supports first-class CHAR/VARCHAR
without SerDe") {
+ test("SPARK-60090: TRANSFORM CHAR/VARCHAR assignment via Project") {
assume(TestUtils.testCommandAvailable("/bin/bash"))
withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- val input = Seq(("ab", "xyz")).toDF("c", "v")
- checkAnswer(
- input,
- (child: SparkPlan) => createScriptTransformationExec(
- script = "cat",
- output = Seq(
- AttributeReference("c", CharType(4, "UTF8_LCASE"))(),
- AttributeReference("v", VarcharType(5, "UNICODE_CI"))()),
- child = child,
- ioschema = defaultIOSchema),
- Seq(Row("ab ", "xyz")))
+ val query = sql(
+ """
+ |SELECT TRANSFORM(c, v)
+ |USING 'cat' AS (c CHAR(4) COLLATE UTF8_LCASE, v VARCHAR(5) COLLATE
UNICODE_CI)
+ |FROM VALUES ('ab', 'xyz') t(c, v)
+ |""".stripMargin)
+ assert(query.schema.map(_.dataType) === Seq(
+ CharType(4, "UTF8_LCASE"),
+ VarcharType(5, "UNICODE_CI")))
+ checkAnswer(query, Row("ab ", "xyz"))
}
assert(uncaughtExceptionHandler.exception.isEmpty)
}
- test("SPARK-59277: TRANSFORM CHAR/VARCHAR null token without SerDe") {
+ test("SPARK-60090: TRANSFORM mixed CHAR and non-CHAR columns") {
assume(TestUtils.testCommandAvailable("/bin/bash"))
withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- val charInput = Seq[String](null, "ab").toDF("c")
- checkAnswer(
- charInput,
- (child: SparkPlan) => createScriptTransformationExec(
- script = "cat",
- output = Seq(AttributeReference("c", CharType(4))()),
- child = child,
- ioschema = defaultIOSchema),
- Seq(Row(null), Row("ab ")))
- val varcharInput = Seq[String](null, "xy").toDF("v")
- checkAnswer(
- varcharInput,
- (child: SparkPlan) => createScriptTransformationExec(
- script = "cat",
- output = Seq(AttributeReference("v", VarcharType(5))()),
- child = child,
- ioschema = defaultIOSchema),
- Seq(Row(null), Row("xy")))
+ val query = sql(
+ """
+ |SELECT TRANSFORM(id, name, note)
+ |USING 'cat' AS (id INT, name CHAR(4), note STRING)
+ |FROM VALUES (1, 'ab', 'hello') t(id, name, note)
+ |""".stripMargin)
+ assert(query.schema.map(_.dataType) === Seq(IntegerType, CharType(4),
StringType))
+ checkAnswer(query, Row(1, "ab ", "hello"))
}
assert(uncaughtExceptionHandler.exception.isEmpty)
}
- test("SPARK-59277: TRANSFORM CHAR overflow without SerDe raises
EXCEED_LIMIT_LENGTH") {
- assume(TestUtils.testCommandAvailable("/bin/bash"))
- withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- val input = Seq("abcdef").toDF("c")
- val exception = intercept[Exception] {
- QueryTest.executePlan(
- createScriptTransformationExec(
- script = "cat",
- output = Seq(AttributeReference("c", CharType(4))()),
- child = input.queryExecution.sparkPlan,
- ioschema = defaultIOSchema),
- spark.sqlContext)
- }
- val runtimeException = exception match {
- case s: org.apache.spark.SparkRuntimeException => s
- case other =>
- other.getCause.asInstanceOf[org.apache.spark.SparkRuntimeException]
- }
- checkError(
- exception = runtimeException,
- condition = "EXCEED_LIMIT_LENGTH",
- parameters = Map("limit" -> "4"))
- }
- }
-
- test("SPARK-59277: TRANSFORM VARCHAR overflow without SerDe raises
EXCEED_LIMIT_LENGTH") {
- assume(TestUtils.testCommandAvailable("/bin/bash"))
- withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- val input = Seq("abcdefgh").toDF("v")
- val exception = intercept[Exception] {
- QueryTest.executePlan(
- createScriptTransformationExec(
- script = "cat",
- output = Seq(AttributeReference("v", VarcharType(5))()),
- child = input.queryExecution.sparkPlan,
- ioschema = defaultIOSchema),
- spark.sqlContext)
- }
- val runtimeException = exception match {
- case s: org.apache.spark.SparkRuntimeException => s
- case other =>
- other.getCause.asInstanceOf[org.apache.spark.SparkRuntimeException]
- }
- checkError(
- exception = runtimeException,
- condition = "EXCEED_LIMIT_LENGTH",
- parameters = Map("limit" -> "5"))
- }
- }
-
- test("SPARK-59277: TRANSFORM converts nested CHAR/VARCHAR without SerDe") {
+ test("SPARK-60090: TRANSFORM CHAR comparison with a STRING literal") {
assume(TestUtils.testCommandAvailable("/bin/bash"))
- withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- Seq(
- ("""["ab"]""", ArrayType(CharType(4)), Row(Seq("ab "))),
- ("""["xy"]""", ArrayType(VarcharType(4)), Row(Seq("xy"))),
- (
- """{"1":"ab"}""",
- MapType(IntegerType, CharType(4)),
- Row(Map(1 -> "ab "))),
- (
- """{"1":{"2":"ab"}}""",
- MapType(IntegerType, MapType(IntegerType, CharType(4))),
- Row(Map(1 -> Map(2 -> "ab ")))),
- (
- """[{"1":"ab"}]""",
- ArrayType(MapType(IntegerType, CharType(4))),
- Row(Seq(Map(1 -> "ab ")))),
- (
- """{"m":{"1":"ab"}}""",
- StructType(Seq(StructField("m", MapType(IntegerType, CharType(4))))),
- Row(Row(Map(1 -> "ab ")))),
- (
- """{"value":"xy"}""",
- StructType(Seq(StructField("value", CharType(5)))),
- Row(Row("xy "))),
- ("""[null]""", ArrayType(CharType(4)), Row(Seq(null))),
- (
- """{"1":null}""",
- MapType(IntegerType, CharType(4)),
- Row(Map(1 -> null))),
- (
- """{"value":null}""",
- StructType(Seq(StructField("value", CharType(5)))),
- Row(Row(null))),
- (
- """{"ab":1}""",
- MapType(CharType(4), IntegerType),
- Row(Map("ab " -> 1)))).foreach { case (json, dataType, expected) =>
- val input = Seq(json).toDF("value")
- checkAnswer(
- input,
- (child: SparkPlan) => createScriptTransformationExec(
- script = "cat",
- output = Seq(AttributeReference("value", dataType)()),
- child = child,
- ioschema = defaultIOSchema),
- Seq(expected))
+ val subquery =
+ """
+ |SELECT * FROM (
+ | SELECT TRANSFORM(c) USING 'cat' AS (c CHAR(4))
+ | FROM VALUES ('ab') t(c)
+ |)
+ |""".stripMargin
+ // Either first-class-types flag yields a CharType attribute, so `=`
promotes
+ // CHAR to STRING instead of padding the literal (table preserve-only
padding
+ // applies only to StringType attributes that carry CHAR metadata).
+ Seq(
+ Map(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true"),
+ Map(
+ SQLConf.PRESERVE_CHAR_VARCHAR_TYPE_INFO.key -> "true",
+ SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "false")).foreach {
confs =>
+ withSQLConf(confs.toSeq: _*) {
+ checkAnswer(sql(subquery + " WHERE c = 'ab'"), Seq.empty)
+ checkAnswer(sql(subquery), Row("ab "))
}
}
assert(uncaughtExceptionHandler.exception.isEmpty)
}
- test("SPARK-59277: TRANSFORM nested CHAR/VARCHAR overflow without SerDe") {
- assume(TestUtils.testCommandAvailable("/bin/bash"))
- withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- Seq(
- (ArrayType(CharType(4)), """["abcdef"]"""),
- (MapType(IntegerType, CharType(4)), """{"1":"abcdef"}"""),
- (
- MapType(IntegerType, MapType(IntegerType, CharType(4))),
- """{"1":{"2":"abcdef"}}"""),
- (
- StructType(Seq(StructField("value", VarcharType(4)))),
- """{"value":"abcdef"}""")).foreach { case (dataType, json) =>
- val input = Seq(json).toDF("value")
- val exception = intercept[Exception] {
- QueryTest.executePlan(
- createScriptTransformationExec(
- script = "cat",
- output = Seq(AttributeReference("value", dataType)()),
- child = input.queryExecution.sparkPlan,
- ioschema = defaultIOSchema),
- spark.sqlContext)
- }
- val runtimeException = exception match {
- case s: org.apache.spark.SparkRuntimeException => s
- case other =>
- other.getCause.asInstanceOf[org.apache.spark.SparkRuntimeException]
- }
- checkError(
- exception = runtimeException,
- condition = "EXCEED_LIMIT_LENGTH",
- parameters = Map("limit" -> "4"))
- }
- }
- }
-
- test("SPARK-59277: malformed nested CHAR JSON without SerDe returns null") {
+ test("SPARK-60090: TRANSFORM CHAR/VARCHAR null token") {
assume(TestUtils.testCommandAvailable("/bin/bash"))
withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- val input = Seq("""{"1":""").toDF("value")
checkAnswer(
- input,
- (child: SparkPlan) => createScriptTransformationExec(
- script = "cat",
- output = Seq(
- AttributeReference("value", MapType(IntegerType, CharType(4)))()),
- child = child,
- ioschema = defaultIOSchema),
- Seq(Row(null)))
+ sql(
+ """
+ |SELECT TRANSFORM(c)
+ |USING 'cat' AS (c CHAR(4))
+ |FROM VALUES (CAST(NULL AS STRING)), ('ab') t(c)
+ |""".stripMargin),
+ Seq(Row(null), Row("ab ")))
+ checkAnswer(
+ sql(
+ """
+ |SELECT TRANSFORM(v)
+ |USING 'cat' AS (v VARCHAR(5))
+ |FROM VALUES (CAST(NULL AS STRING)), ('xy') t(v)
+ |""".stripMargin),
+ Seq(Row(null), Row("xy")))
}
assert(uncaughtExceptionHandler.exception.isEmpty)
}
- test("SPARK-59277: TRANSFORM validates restored JSON map keys without
SerDe") {
+ test("SPARK-60090: TRANSFORM CHAR overflow raises EXCEED_LIMIT_LENGTH") {
assume(TestUtils.testCommandAvailable("/bin/bash"))
withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- val mapType = MapType(IntegerType, CharType(4))
- Seq(
- ("""{"1":"ab"}""", mapType, Row(Map(1 -> "ab "))),
- ("""{"not-an-int":"ab"}""", mapType, Row(null)),
- ("""{"1":"a","01":"b"}""", mapType, Row(null)),
- (
- """[{"not-an-int":"ab"}]""",
- ArrayType(mapType),
- Row(null)),
- (
- """{"m":{"1":"a","01":"b"}}""",
- StructType(Seq(StructField("m", mapType))),
- Row(null))).foreach { case (json, dataType, expected) =>
- val input = Seq(json).toDF("value")
- checkAnswer(
- input,
- (child: SparkPlan) => createScriptTransformationExec(
- script = "cat",
- output = Seq(AttributeReference("value", dataType)()),
- child = child,
- ioschema = defaultIOSchema),
- Seq(expected))
+ val exception = intercept[Exception] {
+ sql(
+ """
+ |SELECT TRANSFORM(c)
+ |USING 'cat' AS (c CHAR(4))
+ |FROM VALUES ('abcdef') t(c)
+ |""".stripMargin).collect()
}
+ checkExceedLimitLength(exception, "4")
}
- assert(uncaughtExceptionHandler.exception.isEmpty)
}
- test("SPARK-59277: colliding map key followed by valid row without SerDe") {
+ test("SPARK-60090: TRANSFORM VARCHAR overflow raises EXCEED_LIMIT_LENGTH") {
assume(TestUtils.testCommandAvailable("/bin/bash"))
withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- val mapType = MapType(IntegerType, CharType(4))
- // Row 1 has duplicate converted keys (1 and 01 both cast to 1).
- // Row 2 is valid. Both rows are in the same partition.
- val input = Seq(
- """{"1":"a","01":"b"}""",
- """{"2":"cd"}""").toDF("value").coalesce(1)
- checkAnswer(
- input,
- (child: SparkPlan) => createScriptTransformationExec(
- script = "cat",
- output = Seq(AttributeReference("value", mapType)()),
- child = child,
- ioschema = defaultIOSchema),
- Seq(Row(null), Row(Map(2 -> "cd "))))
+ val exception = intercept[Exception] {
+ sql(
+ """
+ |SELECT TRANSFORM(v)
+ |USING 'cat' AS (v VARCHAR(5))
+ |FROM VALUES ('abcdefgh') t(v)
+ |""".stripMargin).collect()
+ }
+ checkExceedLimitLength(exception, "5")
}
- assert(uncaughtExceptionHandler.exception.isEmpty)
}
- test("SPARK-59277: colliding CHAR map keys without SerDe follow
MAP_KEY_DEDUP_POLICY") {
+ test("SPARK-60090: TRANSFORM CHAR/VARCHAR flag off stays STRING") {
assume(TestUtils.testCommandAvailable("/bin/bash"))
- val mapType = MapType(CharType(4), IntegerType)
- val json = """{"a":1,"a ":2}"""
- withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- val input = Seq(json).toDF("value")
- val exception = intercept[Exception] {
- QueryTest.executePlan(
- createScriptTransformationExec(
- script = "cat",
- output = Seq(AttributeReference("value", mapType)()),
- child = input.queryExecution.sparkPlan,
- ioschema = defaultIOSchema),
- spark.sqlContext)
- }
- val runtimeException = Iterator.iterate[Throwable](exception)(_.getCause)
- .takeWhile(_ != null)
- .collectFirst {
- case s: org.apache.spark.SparkRuntimeException
- if s.getCondition == "DUPLICATED_MAP_KEY" => s
- }.getOrElse {
- fail(s"expected DUPLICATED_MAP_KEY, got $exception")
- }
- checkError(
- exception = runtimeException,
- condition = "DUPLICATED_MAP_KEY",
- parameters = Map(
- "key" -> "a ",
- "mapKeyDedupPolicy" -> "\"spark.sql.mapKeyDedupPolicy\""))
+ withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "false") {
+ val query = sql(
+ """
+ |SELECT TRANSFORM(c)
+ |USING 'cat' AS (c CHAR(4))
+ |FROM VALUES ('ab'), ('abcdef') t(c)
+ |""".stripMargin)
+ assert(query.schema.map(_.dataType) === Seq(StringType))
+ checkAnswer(query, Seq(Row("ab"), Row("abcdef")))
}
assert(uncaughtExceptionHandler.exception.isEmpty)
}
- test("SPARK-59683: preserve-only CHAR/VARCHAR without SerDe stays
unsupported") {
+ test("SPARK-60090: preserve-only TRANSFORM CHAR/VARCHAR still assigns") {
assume(TestUtils.testCommandAvailable("/bin/bash"))
withSQLConf(
SQLConf.PRESERVE_CHAR_VARCHAR_TYPE_INFO.key -> "true",
SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "false") {
- val input = Seq("ab").toDF("c")
- val exception = intercept[Exception] {
- QueryTest.executePlan(
- createScriptTransformationExec(
- script = "cat",
- output = Seq(AttributeReference("c", CharType(4))()),
- child = input.queryExecution.sparkPlan,
- ioschema = defaultIOSchema),
- spark.sqlContext)
- }
- checkTransformWithoutSerdeUnsupportedType(exception, "\"CHAR(4)\"")
+ val query = sql(
+ """
+ |SELECT TRANSFORM(c)
+ |USING 'cat' AS (c CHAR(4))
+ |FROM VALUES ('ab') t(c)
+ |""".stripMargin)
+ // First-class types keep CHAR in the result; assignment still pads /
length-checks.
+ assert(query.schema.map(_.dataType) === Seq(CharType(4)))
+ checkAnswer(query, Seq(Row("ab ")))
+ checkExceedLimitLength(
+ intercept[Exception] {
+ sql(
+ """
+ |SELECT TRANSFORM(c)
+ |USING 'cat' AS (c CHAR(4))
+ |FROM VALUES ('abcdef') t(c)
+ |""".stripMargin).collect()
+ },
+ "4")
}
+ assert(uncaughtExceptionHandler.exception.isEmpty)
}
- test("SPARK-59683: bound CHAR/VARCHAR mode survives execution conf change") {
+ test("SPARK-60090: unreferenced CHAR overflow is pruned like scans") {
assume(TestUtils.testCommandAvailable("/bin/bash"))
- val padded = Seq("ab").toDF("c")
- val enabledPlan = withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key
-> "true") {
- createScriptTransformationExec(
- script = "cat",
- output = Seq(AttributeReference("c", CharType(4))()),
- child = padded.queryExecution.sparkPlan,
- ioschema = defaultIOSchema)
- }
- withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "false") {
- assert(QueryTest.executePlan(enabledPlan, spark.sqlContext) ===
Seq(Row("ab ")))
- }
-
- val overflow = Seq("abcdef").toDF("c")
- val overflowPlan = withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key
-> "true") {
- createScriptTransformationExec(
- script = "cat",
- output = Seq(AttributeReference("c", CharType(4))()),
- child = overflow.queryExecution.sparkPlan,
- ioschema = defaultIOSchema)
- }
- withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "false") {
- val exception = intercept[Exception] {
- QueryTest.executePlan(overflowPlan, spark.sqlContext)
- }
- val runtimeException = exception match {
- case s: org.apache.spark.SparkRuntimeException => s
- case other =>
- other.getCause.asInstanceOf[org.apache.spark.SparkRuntimeException]
- }
- checkError(
- exception = runtimeException,
- condition = "EXCEED_LIMIT_LENGTH",
- parameters = Map("limit" -> "4"))
+ withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
+ // Assignment lives in a Project, so count(*) can skip the overflowing
column.
+ checkAnswer(
+ sql(
+ """
+ |SELECT count(*) FROM (
+ | SELECT TRANSFORM(c) USING 'cat' AS (c CHAR(4))
+ | FROM VALUES ('abcdef') t(c)
+ |)
+ |""".stripMargin),
+ Row(1))
}
+ }
- val disabledPlan = withSQLConf(
- SQLConf.PRESERVE_CHAR_VARCHAR_TYPE_INFO.key -> "true",
- SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "false") {
- createScriptTransformationExec(
- script = "cat",
- output = Seq(AttributeReference("c", CharType(4))()),
- child = padded.queryExecution.sparkPlan,
- ioschema = defaultIOSchema)
- }
- withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- val exception = intercept[Exception] {
- QueryTest.executePlan(disabledPlan, spark.sqlContext)
+ test("SPARK-60090: hand-built CHAR/VARCHAR script output stays unsupported
without SerDe") {
+ assume(TestUtils.testCommandAvailable("/bin/bash"))
+ // SQL AS (c CHAR(...)) is rewritten to STRING before the exec. A
hand-built plan
+ // that still puts CharType/VarcharType on script output must fail.
+ val input = Seq("ab").toDF("c")
+ Seq(
+ AttributeReference("c", CharType(4))() -> "\"CHAR(4)\"",
+ AttributeReference("v", VarcharType(5))() -> "\"VARCHAR(5)\"",
+ AttributeReference("a", ArrayType(CharType(4)))() ->
"\"ARRAY<CHAR(4)>\"").foreach {
+ case (attr, sqlType) =>
+ val exception = intercept[Exception] {
+ QueryTest.executePlan(
+ createScriptTransformationExec(
+ script = "cat",
+ output = Seq(attr),
+ child = input.queryExecution.sparkPlan,
+ ioschema = defaultIOSchema),
+ spark.sqlContext)
+ }
+ checkTransformWithoutSerdeUnsupportedType(exception, sqlType)
}
- checkTransformWithoutSerdeUnsupportedType(exception, "\"CHAR(4)\"")
- }
- assert(uncaughtExceptionHandler.exception.isEmpty)
}
test("script transformation should not swallow errors from upstream
operators (no serde)") {
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/SparkScriptTransformationSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/SparkScriptTransformationSuite.scala
index 692302e3af36..329e773a99dd 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/execution/SparkScriptTransformationSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/SparkScriptTransformationSuite.scala
@@ -17,12 +17,13 @@
package org.apache.spark.sql.execution
-import org.apache.spark.TestUtils
+import org.apache.spark.{SparkRuntimeException, TestUtils}
import org.apache.spark.sql.Row
import org.apache.spark.sql.catalyst.expressions.Attribute
import org.apache.spark.sql.catalyst.parser.ParseException
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.test.SharedSparkSession
+import org.apache.spark.sql.types._
class SparkScriptTransformationSuite extends BaseScriptTransformationSuite
with SharedSparkSession {
import testImplicits._
@@ -42,8 +43,152 @@ class SparkScriptTransformationSuite extends
BaseScriptTransformationSuite with
)
}
- test("SPARK-59683: TRANSFORM view keeps bound CHAR/VARCHAR mode") {
+ // Nested complex TRANSFORM output is JSON without SerDe. Hive sessions
default to
+ // LazySimpleSerDe, so keep these no-SerDe JSON cases in the Spark suite.
+ test("SPARK-60090: TRANSFORM nested CHAR/VARCHAR assignment via Project") {
assume(TestUtils.testCommandAvailable("/bin/bash"))
+ withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
+ val arrayQuery = sql(
+ """
+ |SELECT TRANSFORM(value)
+ |USING 'cat' AS (value ARRAY<CHAR(4)>)
+ |FROM VALUES ('["ab"]') t(value)
+ |""".stripMargin)
+ assert(arrayQuery.schema.head.dataType === ArrayType(CharType(4)))
+ checkAnswer(arrayQuery, Row(Seq("ab ")))
+ val varcharArray = sql(
+ """
+ |SELECT TRANSFORM(value)
+ |USING 'cat' AS (value ARRAY<VARCHAR(4)>)
+ |FROM VALUES ('["xy"]') t(value)
+ |""".stripMargin)
+ assert(varcharArray.schema.head.dataType === ArrayType(VarcharType(4)))
+ checkAnswer(varcharArray, Row(Seq("xy")))
+ val structQuery = sql(
+ """
+ |SELECT TRANSFORM(value)
+ |USING 'cat' AS (value STRUCT<value: CHAR(5)>)
+ |FROM VALUES ('{"value":"xy"}') t(value)
+ |""".stripMargin)
+ assert(structQuery.schema.head.dataType ===
+ StructType(Seq(StructField("value", CharType(5)))))
+ checkAnswer(structQuery, Row(Row("xy ")))
+ val collated = sql(
+ """
+ |SELECT TRANSFORM(value)
+ |USING 'cat' AS (value STRUCT<value: VARCHAR(6) COLLATE UNICODE_CI>)
+ |FROM VALUES ('{"value":"xyz"}') t(value)
+ |""".stripMargin)
+ assert(collated.schema.head.dataType ===
+ StructType(Seq(StructField("value", VarcharType(6, "UNICODE_CI")))))
+ checkAnswer(collated, Row(Row("xyz")))
+ val quoted = sql(
+ """
+ |SELECT TRANSFORM(value)
+ |USING 'cat' AS (s STRUCT<`a b`: CHAR(4)>)
+ |FROM VALUES ('{"a b":"ab"}') t(value)
+ |""".stripMargin)
+ assert(quoted.schema.head.dataType ===
+ StructType(Seq(StructField("a b", CharType(4)))))
+ checkAnswer(quoted, Row(Row("ab ")))
+ val mapVal = sql(
+ """
+ |SELECT TRANSFORM(value)
+ |USING 'cat' AS (value MAP<STRING, CHAR(4)>)
+ |FROM VALUES ('{"k":"ab"}') t(value)
+ |""".stripMargin)
+ assert(mapVal.schema.head.dataType === MapType(StringType, CharType(4)))
+ checkAnswer(mapVal, Row(Map("k" -> "ab ")))
+ val mapKey = sql(
+ """
+ |SELECT TRANSFORM(value)
+ |USING 'cat' AS (value MAP<CHAR(4), INT>)
+ |FROM VALUES ('{"ab":1}') t(value)
+ |""".stripMargin)
+ assert(mapKey.schema.head.dataType === MapType(CharType(4), IntegerType))
+ checkAnswer(mapKey, Row(Map("ab " -> 1)))
+ val nestedMap = sql(
+ """
+ |SELECT TRANSFORM(value)
+ |USING 'cat' AS (value MAP<STRING, MAP<STRING, CHAR(4)>>)
+ |FROM VALUES ('{"outer":{"k":"ab"}}') t(value)
+ |""".stripMargin)
+ assert(nestedMap.schema.head.dataType ===
+ MapType(StringType, MapType(StringType, CharType(4))))
+ checkAnswer(nestedMap, Row(Map("outer" -> Map("k" -> "ab "))))
+ checkAnswer(
+ sql(
+ """
+ |SELECT TRANSFORM(value)
+ |USING 'cat' AS (value ARRAY<CHAR(4)>)
+ |FROM VALUES ('[null]') t(value)
+ |""".stripMargin),
+ Row(Seq(null)))
+ checkAnswer(
+ sql(
+ """
+ |SELECT TRANSFORM(value)
+ |USING 'cat' AS (value ARRAY<CHAR(4)>)
+ |FROM VALUES ('not-json') t(value)
+ |""".stripMargin),
+ Row(null))
+ }
+ assert(uncaughtExceptionHandler.exception.isEmpty)
+ }
+
+ test("SPARK-60090: TRANSFORM MAP CHAR key collision is DUPLICATED_MAP_KEY") {
+ assume(TestUtils.testCommandAvailable("/bin/bash"))
+ withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
+ val error = intercept[Exception] {
+ sql(
+ """
+ |SELECT TRANSFORM(value)
+ |USING 'cat' AS (value MAP<CHAR(4), INT>)
+ |FROM VALUES ('{"a":1,"a ":2}') t(value)
+ |""".stripMargin).collect()
+ }
+ val duplicate = Iterator.iterate[Throwable](error)(_.getCause)
+ .takeWhile(_ != null)
+ .collectFirst {
+ case e: SparkRuntimeException if e.getCondition ==
"DUPLICATED_MAP_KEY" => e
+ }.getOrElse(fail(s"expected DUPLICATED_MAP_KEY, got $error"))
+ checkError(
+ exception = duplicate,
+ condition = "DUPLICATED_MAP_KEY",
+ parameters = Map(
+ "key" -> "a ",
+ "mapKeyDedupPolicy" -> "\"spark.sql.mapKeyDedupPolicy\""))
+ }
+ }
+
+ test("SPARK-60090: TRANSFORM nested CHAR/VARCHAR overflow via Project") {
+ assume(TestUtils.testCommandAvailable("/bin/bash"))
+ withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
+ Seq(
+ """
+ |SELECT TRANSFORM(value)
+ |USING 'cat' AS (value ARRAY<CHAR(4)>)
+ |FROM VALUES ('["abcdef"]') t(value)
+ |""".stripMargin,
+ """
+ |SELECT TRANSFORM(value)
+ |USING 'cat' AS (value STRUCT<value: VARCHAR(4)>)
+ |FROM VALUES ('{"value":"abcdef"}') t(value)
+ |""".stripMargin,
+ """
+ |SELECT TRANSFORM(value)
+ |USING 'cat' AS (value MAP<STRING, CHAR(4)>)
+ |FROM VALUES ('{"k":"abcdef"}') t(value)
+ |""".stripMargin).foreach { query =>
+ checkExceedLimitLength(intercept[Exception](sql(query).collect()), "4")
+ }
+ }
+ }
+
+ test("SPARK-60090: TRANSFORM view keeps CHAR/VARCHAR Project assignment") {
+ assume(TestUtils.testCommandAvailable("/bin/bash"))
+ // View SQL is re-parsed on each read. standardSemantics is PERSISTED, so
the view
+ // is parsed under the value saved at CREATE VIEW, and the assignment
Project stays.
withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
withView("v") {
sql(
@@ -62,38 +207,33 @@ class SparkScriptTransformationSuite extends
BaseScriptTransformationSuite with
|SELECT TRANSFORM('abcdef') USING 'cat' AS (c CHAR(4))
|FROM VALUES (1) input(dummy)""".stripMargin)
withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "false") {
- val exception = intercept[Exception] {
- sql("SELECT * FROM v_overflow").collect()
- }
- val runtimeException = exception match {
- case s: org.apache.spark.SparkRuntimeException => s
- case other =>
-
other.getCause.asInstanceOf[org.apache.spark.SparkRuntimeException]
- }
- checkError(
- exception = runtimeException,
- condition = "EXCEED_LIMIT_LENGTH",
- parameters = Map("limit" -> "4"))
+ checkExceedLimitLength(
+ intercept[Exception](sql("SELECT * FROM v_overflow").collect()),
+ "4")
}
}
}
- withSQLConf(
- SQLConf.PRESERVE_CHAR_VARCHAR_TYPE_INFO.key -> "true",
- SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "false") {
+ assert(uncaughtExceptionHandler.exception.isEmpty)
+ }
+
+ test("SPARK-60090: TRANSFORM view created with flag off uses stored schema
cast") {
+ assume(TestUtils.testCommandAvailable("/bin/bash"))
+ // PERSISTED parses the view text under the stored flag (off), so there is
no
+ // assignment Project. The CHAR(4) result and padding come from the view
output
+ // cast against the stored schema, not from re-parsing under the caller
session.
+ withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "false") {
withView("v_disabled") {
sql(
"""CREATE VIEW v_disabled AS
|SELECT TRANSFORM('ab') USING 'cat' AS (c CHAR(4))
|FROM VALUES (1) input(dummy)""".stripMargin)
withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- val exception = intercept[Exception] {
- sql("SELECT * FROM v_disabled").collect()
- }
- checkTransformWithoutSerdeUnsupportedType(exception, "\"CHAR(4)\"")
+ val query = sql("SELECT * FROM v_disabled")
+ assert(query.schema.map(_.dataType) === Seq(CharType(4)))
+ checkAnswer(query, Row("ab "))
}
}
}
- assert(uncaughtExceptionHandler.exception.isEmpty)
}
test("SPARK-32106: TRANSFORM with serde without hive should throw
exception") {
diff --git
a/sql/hive/src/main/scala/org/apache/spark/sql/hive/HiveInspectors.scala
b/sql/hive/src/main/scala/org/apache/spark/sql/hive/HiveInspectors.scala
index 1eb4b48e8338..b8758407877f 100644
--- a/sql/hive/src/main/scala/org/apache/spark/sql/hive/HiveInspectors.scala
+++ b/sql/hive/src/main/scala/org/apache/spark/sql/hive/HiveInspectors.scala
@@ -942,24 +942,6 @@ private[hive] trait HiveInspectors {
unwrapperFor(objectInspector)
}
- /**
- * In-place unwrapper that uses DataType-aware conversion when the target
- * contains CHAR/VARCHAR or nanosecond timestamps. Other fields keep the
- * specialized primitive setters from the field-only overload.
- */
- def unwrapperFor(
- field: HiveStructField,
- dataType: DataType): (Any, InternalRow, Int) => Unit = {
- if (CharVarcharUtils.hasCharVarchar(dataType) ||
- dataType.existsRecursively(_.isInstanceOf[AnyTimestampNanoType])) {
- val unwrapper = unwrapperFor(field.getFieldObjectInspector, dataType)
- (value: Any, row: InternalRow, ordinal: Int) =>
- row.update(ordinal, unwrapper(value))
- } else {
- unwrapperFor(field)
- }
- }
-
/**
* Builds unwrappers ahead of time according to object inspector
* types to avoid pattern matching and branching costs per row.
diff --git
a/sql/hive/src/main/scala/org/apache/spark/sql/hive/execution/HiveScriptTransformationExec.scala
b/sql/hive/src/main/scala/org/apache/spark/sql/hive/execution/HiveScriptTransformationExec.scala
index c53949889611..de2d15415837 100644
---
a/sql/hive/src/main/scala/org/apache/spark/sql/hive/execution/HiveScriptTransformationExec.scala
+++
b/sql/hive/src/main/scala/org/apache/spark/sql/hive/execution/HiveScriptTransformationExec.scala
@@ -27,7 +27,6 @@ import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.hive.ql.exec.{RecordReader, RecordWriter}
import org.apache.hadoop.hive.serde.serdeConstants
import org.apache.hadoop.hive.serde2.AbstractSerDe
-import org.apache.hadoop.hive.serde2.`lazy`.LazySimpleSerDe
import org.apache.hadoop.hive.serde2.objectinspector._
import org.apache.hadoop.io.Writable
@@ -37,7 +36,7 @@ import org.apache.spark.sql.catalyst.expressions._
import org.apache.spark.sql.execution._
import org.apache.spark.sql.hive.HiveInspectors
import org.apache.spark.sql.hive.HiveShim._
-import org.apache.spark.sql.types._
+import org.apache.spark.sql.types.DataType
import org.apache.spark.util.{CircularBuffer, Utils}
/**
@@ -76,9 +75,7 @@ private[hive] case class HiveScriptTransformationExec(
val mutableRow = new SpecificInternalRow(output.map(_.dataType))
@transient
- lazy val unwrappers =
outputSoi.getAllStructFieldRefs.asScala.zip(output).map {
- case (field, attr) => unwrapperFor(field, attr.dataType)
- }
+ lazy val unwrappers =
outputSoi.getAllStructFieldRefs.asScala.map(unwrapperFor)
override def hasNext: Boolean = {
if (completed) {
@@ -259,9 +256,7 @@ object HiveScriptIOSchema extends HiveInspectors {
output: Seq[Attribute]): Option[(AbstractSerDe, StructObjectInspector)]
= {
ioschema.outputSerdeClass.map { serdeClass =>
val (columns, columnTypes) = parseAttrs(output)
- val serdeTypes = outputTypesForSerDe(
- serdeClass, columnTypes, ioschema.standardCharVarcharSemantics)
- val serde = initSerDe(serdeClass, columns, serdeTypes,
ioschema.outputSerdeProps)
+ val serde = initSerDe(serdeClass, columns, columnTypes,
ioschema.outputSerdeProps)
val structObjectInspector =
serde.getObjectInspector().asInstanceOf[StructObjectInspector]
(serde, structObjectInspector)
}
@@ -273,25 +268,6 @@ object HiveScriptIOSchema extends HiveInspectors {
(columns, columnTypes)
}
- /**
- * Hive LazySimpleSerDe CHAR/VARCHAR types truncate on deserialize. Under
standard
- * semantics, map them to STRING so Spark applies length checks. Only
LazySimpleSerDe
- * and subclasses get this rewrite; other SerDes keep the declared schema.
- * preserveCharVarcharTypeInfo without standard semantics is unchanged.
- */
- private def outputTypesForSerDe(
- serdeClassName: String,
- columnTypes: Seq[DataType],
- standardCharVarcharSemantics: Boolean): Seq[DataType] = {
- val serdeClass = Utils.classForName[AbstractSerDe](serdeClassName)
- if (standardCharVarcharSemantics &&
- classOf[LazySimpleSerDe].isAssignableFrom(serdeClass)) {
- columnTypes.map(ScriptTransformationIOSchema.toUnboundedStringType)
- } else {
- columnTypes
- }
- }
-
def initSerDe(
serdeClassName: String,
columns: Seq[String],
diff --git
a/sql/hive/src/test/scala/org/apache/spark/sql/hive/execution/HiveScriptTransformationSuite.scala
b/sql/hive/src/test/scala/org/apache/spark/sql/hive/execution/HiveScriptTransformationSuite.scala
index a4df01db29f2..804fa8ab53c2 100644
---
a/sql/hive/src/test/scala/org/apache/spark/sql/hive/execution/HiveScriptTransformationSuite.scala
+++
b/sql/hive/src/test/scala/org/apache/spark/sql/hive/execution/HiveScriptTransformationSuite.scala
@@ -20,15 +20,8 @@ package org.apache.spark.sql.hive.execution
import java.sql.Timestamp
import java.time.{Duration, Period}
import java.time.temporal.ChronoUnit
-import java.util.{Arrays, Properties}
-import org.apache.hadoop.conf.Configuration
-import org.apache.hadoop.hive.serde.serdeConstants
-import org.apache.hadoop.hive.serde2.{AbstractSerDe, SerDeStats}
import org.apache.hadoop.hive.serde2.`lazy`.LazySimpleSerDe
-import org.apache.hadoop.hive.serde2.objectinspector.{ObjectInspector,
ObjectInspectorFactory}
-import
org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory
-import org.apache.hadoop.io.{Text, Writable}
import org.scalatest.exceptions.TestFailedException
import org.apache.spark.{SparkException, TestUtils}
@@ -380,14 +373,16 @@ class HiveScriptTransformationSuite extends
BaseScriptTransformationSuite with T
}
}
- test("SPARK-59277: TRANSFORM supports nested collated CHAR/VARCHAR with Hive
SerDe") {
+ test("SPARK-60090: TRANSFORM CHAR/VARCHAR with Hive SerDe uses Project
assignment") {
assume(TestUtils.testCommandAvailable("/bin/bash"))
withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
+ // Hive sessions default to LazySimpleSerDe; script I/O stays
STRING-shaped and
+ // Project applies pad / EXCEED_LIMIT_LENGTH for the declared AS types.
val query = sql(
"""
|SELECT TRANSFORM(
| array(CAST('ab' AS CHAR(4) COLLATE UTF8_LCASE)),
- | named_struct('value', CAST('xyz' AS VARCHAR(6) COLLATE
UNICODE_CI)))
+ | named_struct('value', CAST('xyz' AS VARCHAR(6))))
|USING 'cat'
|AS (
| chars ARRAY<CHAR(4) COLLATE UTF8_LCASE>,
@@ -398,60 +393,29 @@ class HiveScriptTransformationSuite extends
BaseScriptTransformationSuite with T
ArrayType(CharType(4, "UTF8_LCASE")),
StructType(Seq(StructField("value", VarcharType(6, "UNICODE_CI"))))))
checkAnswer(query, Row(Seq("ab "), Row("xyz")))
- }
- }
-
- test("SPARK-59277: TRANSFORM CHAR overflow with Hive SerDe raises
EXCEED_LIMIT_LENGTH") {
- assume(TestUtils.testCommandAvailable("/bin/bash"))
- withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- val exception = intercept[Exception] {
- sql(
- """
- |SELECT TRANSFORM('abcdef')
- |USING 'cat'
- |AS (c CHAR(4))
- |FROM VALUES (1) input(dummy)
- |""".stripMargin).collect()
- }
- val runtimeException = exception match {
- case s: org.apache.spark.SparkRuntimeException => s
- case other =>
- other.getCause.asInstanceOf[org.apache.spark.SparkRuntimeException]
- }
- checkError(
- exception = runtimeException,
- condition = "EXCEED_LIMIT_LENGTH",
- parameters = Map("limit" -> "4"))
- }
- }
-
- test("SPARK-59277: TRANSFORM VARCHAR overflow with Hive SerDe raises
EXCEED_LIMIT_LENGTH") {
- assume(TestUtils.testCommandAvailable("/bin/bash"))
- withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- val exception = intercept[Exception] {
- sql(
- """
- |SELECT TRANSFORM('abcdefgh')
- |USING 'cat'
- |AS (v VARCHAR(5))
- |FROM VALUES (1) input(dummy)
- |""".stripMargin).collect()
- }
- val runtimeException = exception match {
- case s: org.apache.spark.SparkRuntimeException => s
- case other =>
- other.getCause.asInstanceOf[org.apache.spark.SparkRuntimeException]
- }
- checkError(
- exception = runtimeException,
- condition = "EXCEED_LIMIT_LENGTH",
- parameters = Map("limit" -> "5"))
- }
- }
- test("SPARK-59277: nested CHAR/VARCHAR overflow with Hive SerDe") {
- assume(TestUtils.testCommandAvailable("/bin/bash"))
- withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
+ checkExceedLimitLength(
+ intercept[Exception] {
+ sql(
+ """
+ |SELECT TRANSFORM('abcdef')
+ |USING 'cat'
+ |AS (c CHAR(4))
+ |FROM VALUES (1) input(dummy)
+ |""".stripMargin).collect()
+ },
+ "4")
+ checkExceedLimitLength(
+ intercept[Exception] {
+ sql(
+ """
+ |SELECT TRANSFORM('abcdefgh')
+ |USING 'cat'
+ |AS (v VARCHAR(5))
+ |FROM VALUES (1) input(dummy)
+ |""".stripMargin).collect()
+ },
+ "5")
Seq(
"""
|SELECT TRANSFORM(array('abcdef'))
@@ -464,107 +428,13 @@ class HiveScriptTransformationSuite extends
BaseScriptTransformationSuite with T
|USING 'cat'
|AS (value STRUCT<value: VARCHAR(4)>)
|FROM VALUES (1) input(dummy)
- |""".stripMargin).foreach { query =>
- val exception = intercept[Exception] {
- sql(query).collect()
- }
- val runtimeException = exception match {
- case s: org.apache.spark.SparkRuntimeException => s
- case other =>
- other.getCause.asInstanceOf[org.apache.spark.SparkRuntimeException]
- }
- checkError(
- exception = runtimeException,
- condition = "EXCEED_LIMIT_LENGTH",
- parameters = Map("limit" -> "4"))
+ |""".stripMargin).foreach { q =>
+ checkExceedLimitLength(intercept[Exception](sql(q).collect()), "4")
}
}
}
- test("SPARK-59277: output SerDe CHAR/VARCHAR rewrite is
LazySimpleSerDe-only") {
- withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- val output = Seq(
- AttributeReference("c", CharType(4))(),
- AttributeReference("v", VarcharType(5))(),
- AttributeReference("nested", ArrayType(CharType(4)))())
-
- val (_, lazySoi) = HiveScriptIOSchema.initOutputSerDe(hiveIOSchema,
output).get
-
assert(lazySoi.getAllStructFieldRefs.get(0).getFieldObjectInspector.getTypeName
===
- "string")
-
assert(lazySoi.getAllStructFieldRefs.get(1).getFieldObjectInspector.getTypeName
===
- "string")
-
assert(lazySoi.getAllStructFieldRefs.get(2).getFieldObjectInspector.getTypeName
===
- "array<string>")
-
- val subclassSchema = hiveIOSchema.copy(
- outputSerdeClass = Some(classOf[TestLazySimpleSerDe].getCanonicalName))
- val (_, subclassSoi) =
HiveScriptIOSchema.initOutputSerDe(subclassSchema, output).get
-
assert(subclassSoi.getAllStructFieldRefs.get(0).getFieldObjectInspector.getTypeName
===
- "string")
-
- SchemaCapturingSerDe.lastColumnTypes = null
- val customSchema = defaultIOSchema.copy(
- outputSerdeClass =
Some(classOf[SchemaCapturingSerDe].getCanonicalName))
- HiveScriptIOSchema.initOutputSerDe(customSchema, output)
- val captured = SchemaCapturingSerDe.lastColumnTypes
- assert(captured.contains("char(4)"))
- assert(captured.contains("varchar(5)"))
- assert(captured.contains("array<char(4)>"))
- }
- }
-
- test("SPARK-59683: preserve-only does not rewrite LazySimpleSerDe
CHAR/VARCHAR") {
- withSQLConf(
- SQLConf.PRESERVE_CHAR_VARCHAR_TYPE_INFO.key -> "true",
- SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "false") {
- val output = Seq(
- AttributeReference("c", CharType(4))(),
- AttributeReference("v", VarcharType(5))(),
- AttributeReference("nested", ArrayType(CharType(4)))())
- val (_, soi) = HiveScriptIOSchema.initOutputSerDe(hiveIOSchema,
output).get
-
assert(soi.getAllStructFieldRefs.get(0).getFieldObjectInspector.getTypeName ===
- "char(4)")
-
assert(soi.getAllStructFieldRefs.get(1).getFieldObjectInspector.getTypeName ===
- "varchar(5)")
-
assert(soi.getAllStructFieldRefs.get(2).getFieldObjectInspector.getTypeName ===
- "array<char(4)>")
- }
- }
-
- test("SPARK-59683: bound Hive SerDe CHAR/VARCHAR mode survives conf change")
{
- val output = Seq(
- AttributeReference("c", CharType(4))(),
- AttributeReference("v", VarcharType(5))(),
- AttributeReference("nested", ArrayType(CharType(4)))())
- val enabledSchema =
withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- hiveIOSchema
- }
- withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "false") {
- val (_, soi) = HiveScriptIOSchema.initOutputSerDe(enabledSchema,
output).get
-
assert(soi.getAllStructFieldRefs.get(0).getFieldObjectInspector.getTypeName ===
- "string")
-
assert(soi.getAllStructFieldRefs.get(1).getFieldObjectInspector.getTypeName ===
- "string")
-
assert(soi.getAllStructFieldRefs.get(2).getFieldObjectInspector.getTypeName ===
- "array<string>")
- }
- val disabledSchema = withSQLConf(
- SQLConf.PRESERVE_CHAR_VARCHAR_TYPE_INFO.key -> "true",
- SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "false") {
- hiveIOSchema
- }
- withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- val (_, soi) = HiveScriptIOSchema.initOutputSerDe(disabledSchema,
output).get
-
assert(soi.getAllStructFieldRefs.get(0).getFieldObjectInspector.getTypeName ===
- "char(4)")
-
assert(soi.getAllStructFieldRefs.get(1).getFieldObjectInspector.getTypeName ===
- "varchar(5)")
-
assert(soi.getAllStructFieldRefs.get(2).getFieldObjectInspector.getTypeName ===
- "array<char(4)>")
- }
- }
-
- test("SPARK-59683: Hive TRANSFORM view keeps bound CHAR/VARCHAR mode") {
+ test("SPARK-60090: Hive TRANSFORM view keeps CHAR/VARCHAR Project
assignment") {
assume(TestUtils.testCommandAvailable("/bin/bash"))
withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
withView("v") {
@@ -584,18 +454,9 @@ class HiveScriptTransformationSuite extends
BaseScriptTransformationSuite with T
|SELECT TRANSFORM('abcdef') USING 'cat' AS (c CHAR(4))
|FROM VALUES (1) input(dummy)""".stripMargin)
withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "false") {
- val exception = intercept[Exception] {
- sql("SELECT * FROM v_overflow").collect()
- }
- val runtimeException = exception match {
- case s: org.apache.spark.SparkRuntimeException => s
- case other =>
-
other.getCause.asInstanceOf[org.apache.spark.SparkRuntimeException]
- }
- checkError(
- exception = runtimeException,
- condition = "EXCEED_LIMIT_LENGTH",
- parameters = Map("limit" -> "4"))
+ checkExceedLimitLength(
+ intercept[Exception](sql("SELECT * FROM v_overflow").collect()),
+ "4")
}
}
}
@@ -897,29 +758,3 @@ class HiveScriptTransformationSuite extends
BaseScriptTransformationSuite with T
}
}
}
-
-class TestLazySimpleSerDe extends LazySimpleSerDe
-
-class SchemaCapturingSerDe extends AbstractSerDe {
- override def initialize(conf: Configuration, tbl: Properties): Unit = {
- SchemaCapturingSerDe.lastColumnTypes =
- tbl.getProperty(serdeConstants.LIST_COLUMN_TYPES)
- }
-
- override def getObjectInspector: ObjectInspector =
- ObjectInspectorFactory.getStandardStructObjectInspector(
- Arrays.asList("col"),
- Arrays.asList(PrimitiveObjectInspectorFactory.javaStringObjectInspector))
-
- override def getSerializedClass: Class[_ <: Writable] = classOf[Text]
-
- override def getSerDeStats: SerDeStats = null
-
- override def serialize(obj: Any, inspector: ObjectInspector): Writable = null
-
- override def deserialize(blob: Writable): AnyRef = null
-}
-
-object SchemaCapturingSerDe {
- @volatile var lastColumnTypes: String = _
-}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]