Repository navigation
[SPARK-46654][SQL][PYTHON] Make to_csv explicitly indicate that it does not support some types of data
#44665
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9fe1769
22a7afb
4e770cb
8f72454
bb6eec5
9695e97
2cef851
ab2243b
daba8aa
ad12661
5ce4eab
d038ffa
aea0d66
93d890e
e28668a
f3d215f
b3fd021
b1daa88
7520803
d012f0f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15491,8 +15491,6 @@ def schema_of_csv(csv: Union[Column, str], options: Optional[Dict[str, str]] = N | |||||||||
| return _invoke_function("schema_of_csv", col, _options_to_str(options)) | ||||||||||
|
|
||||||||||
|
|
||||||||||
| # TODO(SPARK-46654) Re-enable the `Example 2` test after fixing the display | ||||||||||
| # difference between Regular Spark and Spark Connect on `df.show`. | ||||||||||
| @_try_remote_functions | ||||||||||
| def to_csv(col: "ColumnOrName", options: Optional[Dict[str, str]] = None) -> Column: | ||||||||||
| """ | ||||||||||
|
|
@@ -15534,19 +15532,7 @@ def to_csv(col: "ColumnOrName", options: Optional[Dict[str, str]] = None) -> Col | |||||||||
| | 2,Alice| | ||||||||||
| +-------------+ | ||||||||||
|
|
||||||||||
| Example 2: Converting a complex StructType to a CSV string | ||||||||||
|
|
||||||||||
| >>> from pyspark.sql import Row, functions as sf | ||||||||||
| >>> data = [(1, Row(age=2, name='Alice', scores=[100, 200, 300]))] | ||||||||||
| >>> df = spark.createDataFrame(data, ("key", "value")) | ||||||||||
| >>> df.select(sf.to_csv(df.value)).show(truncate=False) # doctest: +SKIP | ||||||||||
| +-----------------------+ | ||||||||||
| |to_csv(value) | | ||||||||||
| +-----------------------+ | ||||||||||
| |2,Alice,"[100,200,300]"| | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a breaking change in fact. We need to update the SQL migration guide at least.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me update.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have updated the document
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@HyukjinKwon See what it does return for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In spark/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/csv/UnivocityGenerator.scala Lines 91 to 93 in 09bdf2a
spark/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/GenericArrayData.scala Line 85 in 09bdf2a
And it just so happens that |
||||||||||
| +-----------------------+ | ||||||||||
|
|
||||||||||
| Example 3: Converting a StructType with null values to a CSV string | ||||||||||
| Example 2: Converting a StructType with null values to a CSV string | ||||||||||
|
|
||||||||||
| >>> from pyspark.sql import Row, functions as sf | ||||||||||
| >>> from pyspark.sql.types import StructType, StructField, IntegerType, StringType | ||||||||||
|
|
@@ -15566,7 +15552,7 @@ def to_csv(col: "ColumnOrName", options: Optional[Dict[str, str]] = None) -> Col | |||||||||
| | ,Alice| | ||||||||||
| +-------------+ | ||||||||||
|
|
||||||||||
| Example 4: Converting a StructType with different data types to a CSV string | ||||||||||
| Example 3: Converting a StructType with different data types to a CSV string | ||||||||||
|
|
||||||||||
| >>> from pyspark.sql import Row, functions as sf | ||||||||||
| >>> data = [(1, Row(age=2, name='Alice', isStudent=True))] | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,15 +19,18 @@ package org.apache.spark.sql.catalyst.expressions | |
|
|
||
| import java.io.CharArrayWriter | ||
|
|
||
| import scala.annotation.tailrec | ||
|
|
||
| import com.univocity.parsers.csv.CsvParser | ||
|
|
||
| import org.apache.spark.{SparkException, SparkIllegalArgumentException} | ||
| import org.apache.spark.SparkException | ||
| import org.apache.spark.sql.catalyst.InternalRow | ||
| import org.apache.spark.sql.catalyst.analysis.TypeCheckResult | ||
| import org.apache.spark.sql.catalyst.analysis.TypeCheckResult.DataTypeMismatch | ||
| import org.apache.spark.sql.catalyst.analysis.TypeCheckResult.{DataTypeMismatch, TypeCheckSuccess} | ||
| import org.apache.spark.sql.catalyst.csv._ | ||
| import org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext, CodegenFallback, ExprCode} | ||
| import org.apache.spark.sql.catalyst.util._ | ||
| import org.apache.spark.sql.catalyst.util.TypeUtils._ | ||
| import org.apache.spark.sql.errors.{QueryCompilationErrors, QueryErrorsBase} | ||
| import org.apache.spark.sql.internal.SQLConf | ||
| import org.apache.spark.sql.types._ | ||
|
|
@@ -260,16 +263,33 @@ case class StructsToCsv( | |
| child = child, | ||
| timeZoneId = None) | ||
|
|
||
| override def checkInputDataTypes(): TypeCheckResult = { | ||
| child.dataType match { | ||
| case schema: StructType if schema.map(_.dataType).forall( | ||
| dt => isSupportedDataType(dt)) => TypeCheckSuccess | ||
| case _ => DataTypeMismatch( | ||
| errorSubClass = "UNSUPPORTED_INPUT_TYPE", | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using for example: Before(Last review): After(Right now): |
||
| messageParameters = Map( | ||
| "functionName" -> toSQLId(prettyName), | ||
| "dataType" -> toSQLType(child.dataType) | ||
| ) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| @tailrec | ||
| private def isSupportedDataType(dataType: DataType): Boolean = dataType match { | ||
| case _: VariantType | BinaryType => false | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you clarify this? Why don't you support the types?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because |
||
| case _: AtomicType | CalendarIntervalType => true | ||
| case udt: UserDefinedType[_] => isSupportedDataType(udt.sqlType) | ||
| case _ => false | ||
| } | ||
|
|
||
| @transient | ||
| lazy val writer = new CharArrayWriter() | ||
|
|
||
| @transient | ||
| lazy val inputSchema: StructType = child.dataType match { | ||
| case st: StructType => st | ||
| case other => throw new SparkIllegalArgumentException( | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's |
||
| errorClass = "_LEGACY_ERROR_TEMP_3234", | ||
| messageParameters = Map("other" -> other.catalogString)) | ||
| } | ||
| lazy val inputSchema: StructType = child.dataType.asInstanceOf[StructType] | ||
|
|
||
| @transient | ||
| lazy val gen = new UnivocityGenerator( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
|
|
||
| package org.apache.spark.sql | ||
|
|
||
| import java.nio.charset.StandardCharsets | ||
| import java.text.SimpleDateFormat | ||
| import java.time.{Duration, LocalDateTime, Period} | ||
| import java.util.Locale | ||
|
|
@@ -31,6 +32,7 @@ import org.apache.spark.sql.test.SharedSparkSession | |
| import org.apache.spark.sql.types._ | ||
| import org.apache.spark.sql.types.DayTimeIntervalType.{DAY, HOUR, MINUTE, SECOND} | ||
| import org.apache.spark.sql.types.YearMonthIntervalType.{MONTH, YEAR} | ||
| import org.apache.spark.unsafe.types._ | ||
|
|
||
| class CsvFunctionsSuite extends QueryTest with SharedSparkSession { | ||
| import testImplicits._ | ||
|
|
@@ -176,7 +178,7 @@ class CsvFunctionsSuite extends QueryTest with SharedSparkSession { | |
|
|
||
| checkError( | ||
| exception = intercept[SparkUnsupportedOperationException] { | ||
| df.select(from_csv(to_csv($"value"), schema, options)).collect() | ||
| df.select(from_csv(lit("any value"), schema, options)).collect() | ||
| }, | ||
| errorClass = "UNSUPPORTED_DATATYPE", | ||
| parameters = Map("typeName" -> toSQLType(valueType)) | ||
|
|
@@ -294,10 +296,19 @@ class CsvFunctionsSuite extends QueryTest with SharedSparkSession { | |
| } | ||
|
|
||
| test("to_csv with option (nullValue)") { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you explain, please, why did you change the test?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because |
||
| val df = Seq(Tuple1(Tuple1(null))).toDF("a") | ||
| val options = Map("nullValue" -> "-").asJava | ||
| val rows = new java.util.ArrayList[Row]() | ||
| rows.add(Row(1L, Row(2L, "Alice", null))) | ||
|
|
||
| checkAnswer(df.select(to_csv($"a", options)), Row("-") :: Nil) | ||
| val valueType = StructType(Seq( | ||
| StructField("age", LongType), | ||
| StructField("name", StringType), | ||
| StructField("score", IntegerType))) | ||
|
|
||
| val schema = StructType(Seq(StructField("key", LongType), StructField("value", valueType))) | ||
| val df = spark.createDataFrame(rows, schema) | ||
|
|
||
| val options = Map("nullValue" -> "-").asJava | ||
| checkAnswer(df.select(to_csv($"value", options)), Row("2,Alice,-") :: Nil) | ||
| } | ||
|
|
||
| test("to_csv with option (dateFormat)") { | ||
|
|
@@ -603,4 +614,210 @@ class CsvFunctionsSuite extends QueryTest with SharedSparkSession { | |
| $"csv", schema_of_csv("1,2\n2"), Map.empty[String, String].asJava)) | ||
| checkAnswer(actual, Row(Row(1, "2\n2"))) | ||
| } | ||
|
|
||
| test("SPARK-46654: from_csv/to_csv does not support ArrayType data") { | ||
| val rows = new java.util.ArrayList[Row]() | ||
| rows.add(Row(1L, Row(2L, "Alice", Array(100L, 200L, null, 300L)))) | ||
|
|
||
| val valueSchema = StructType(Seq( | ||
| StructField("age", LongType), | ||
| StructField("name", StringType), | ||
| StructField("scores", ArrayType(LongType)))) | ||
| val schema = StructType(Seq( | ||
| StructField("key", LongType), | ||
| StructField("value", valueSchema))) | ||
|
|
||
| val df = spark.createDataFrame(rows, schema) | ||
|
|
||
| checkError( | ||
| exception = intercept[AnalysisException] { | ||
| df.select(to_csv($"value")).collect() | ||
| }, | ||
| errorClass = "DATATYPE_MISMATCH.UNSUPPORTED_INPUT_TYPE", | ||
| parameters = Map( | ||
| "functionName" -> "`to_csv`", | ||
| "dataType" -> "\"STRUCT<age: BIGINT, name: STRING, scores: ARRAY<BIGINT>>\"", | ||
| "sqlExpr" -> "\"to_csv(value)\""), | ||
| context = ExpectedContext(fragment = "to_csv", getCurrentClassCallSitePattern) | ||
| ) | ||
|
|
||
| checkError( | ||
| exception = intercept[SparkUnsupportedOperationException] { | ||
| df.select(from_csv(lit("data"), valueSchema, Map.empty[String, String])).collect() | ||
| }, | ||
| errorClass = "UNSUPPORTED_DATATYPE", | ||
| parameters = Map("typeName" -> "\"ARRAY<BIGINT>\"") | ||
| ) | ||
| } | ||
|
|
||
| test("SPARK-46654: from_csv/to_csv does not support MapType data") { | ||
| val rows = new java.util.ArrayList[Row]() | ||
| rows.add(Row(1L, Row(2L, "Alice", | ||
| Map("math" -> 100L, "english" -> 200L, "science" -> null)))) | ||
|
|
||
| val valueSchema = StructType(Seq( | ||
| StructField("age", LongType), | ||
| StructField("name", StringType), | ||
| StructField("scores", MapType(StringType, LongType)))) | ||
| val schema = StructType(Seq( | ||
| StructField("key", LongType), | ||
| StructField("value", valueSchema))) | ||
|
|
||
| val df = spark.createDataFrame(rows, schema) | ||
|
|
||
| checkError( | ||
| exception = intercept[AnalysisException] { | ||
| df.select(to_csv($"value")).collect() | ||
| }, | ||
| errorClass = "DATATYPE_MISMATCH.UNSUPPORTED_INPUT_TYPE", | ||
| parameters = Map( | ||
| "functionName" -> "`to_csv`", | ||
| "dataType" -> "\"STRUCT<age: BIGINT, name: STRING, scores: MAP<STRING, BIGINT>>\"", | ||
| "sqlExpr" -> "\"to_csv(value)\""), | ||
| context = ExpectedContext(fragment = "to_csv", getCurrentClassCallSitePattern) | ||
| ) | ||
|
|
||
| checkError( | ||
| exception = intercept[SparkUnsupportedOperationException] { | ||
| df.select(from_csv(lit("data"), valueSchema, Map.empty[String, String])).collect() | ||
| }, | ||
| errorClass = "UNSUPPORTED_DATATYPE", | ||
| parameters = Map("typeName" -> "\"MAP<STRING, BIGINT>\"") | ||
| ) | ||
| } | ||
|
|
||
| test("SPARK-46654: from_csv/to_csv does not support StructType data") { | ||
| val rows = new java.util.ArrayList[Row]() | ||
| rows.add(Row(1L, Row(2L, "Alice", Row(100L, 200L, null)))) | ||
|
|
||
| val valueSchema = StructType(Seq( | ||
| StructField("age", LongType), | ||
| StructField("name", StringType), | ||
| StructField("scores", StructType(Seq( | ||
| StructField("id1", LongType), | ||
| StructField("id2", LongType), | ||
| StructField("id3", LongType)))))) | ||
| val schema = StructType(Seq( | ||
| StructField("key", LongType), | ||
| StructField("value", valueSchema))) | ||
|
|
||
| val df = spark.createDataFrame(rows, schema) | ||
|
|
||
| checkError( | ||
| exception = intercept[AnalysisException] { | ||
| df.select(to_csv($"value")).collect() | ||
| }, | ||
| errorClass = "DATATYPE_MISMATCH.UNSUPPORTED_INPUT_TYPE", | ||
| parameters = Map( | ||
| "functionName" -> "`to_csv`", | ||
| "dataType" -> ("\"STRUCT<age: BIGINT, name: STRING, " + | ||
| "scores: STRUCT<id1: BIGINT, id2: BIGINT, id3: BIGINT>>\""), | ||
| "sqlExpr" -> "\"to_csv(value)\""), | ||
| context = ExpectedContext(fragment = "to_csv", getCurrentClassCallSitePattern) | ||
| ) | ||
|
|
||
| checkError( | ||
| exception = intercept[SparkUnsupportedOperationException] { | ||
| df.select(from_csv(lit("data"), valueSchema, Map.empty[String, String])).collect() | ||
| }, | ||
| errorClass = "UNSUPPORTED_DATATYPE", | ||
| parameters = Map("typeName" -> "\"STRUCT<id1: BIGINT, id2: BIGINT, id3: BIGINT>\"") | ||
| ) | ||
| } | ||
|
|
||
| test("SPARK-46654: from_csv/to_csv does not support VariantType data") { | ||
| val rows = new java.util.ArrayList[Row]() | ||
| rows.add(Row(1L, Row(2L, "Alice", new VariantVal(Array[Byte](1, 2, 3), Array[Byte](4, 5))))) | ||
|
|
||
| val valueSchema = StructType(Seq( | ||
| StructField("age", LongType), | ||
| StructField("name", StringType), | ||
| StructField("v", VariantType))) | ||
| val schema = StructType(Seq( | ||
| StructField("key", LongType), | ||
| StructField("value", valueSchema))) | ||
|
|
||
| val df = spark.createDataFrame(rows, schema) | ||
|
|
||
| checkError( | ||
| exception = intercept[AnalysisException] { | ||
| df.select(to_csv($"value")).collect() | ||
| }, | ||
| errorClass = "DATATYPE_MISMATCH.UNSUPPORTED_INPUT_TYPE", | ||
| parameters = Map( | ||
| "functionName" -> "`to_csv`", | ||
| "dataType" -> "\"STRUCT<age: BIGINT, name: STRING, v: VARIANT>\"", | ||
| "sqlExpr" -> "\"to_csv(value)\""), | ||
| context = ExpectedContext(fragment = "to_csv", getCurrentClassCallSitePattern) | ||
| ) | ||
|
|
||
| checkError( | ||
| exception = intercept[SparkUnsupportedOperationException] { | ||
| df.select(from_csv(lit("data"), valueSchema, Map.empty[String, String])).collect() | ||
| }, | ||
| errorClass = "UNSUPPORTED_DATATYPE", | ||
| parameters = Map("typeName" -> "\"VARIANT\"") | ||
| ) | ||
| } | ||
|
|
||
| test("SPARK-46654: from_csv/to_csv does not support BinaryType data") { | ||
| val rows = new java.util.ArrayList[Row]() | ||
| rows.add(Row(1L, Row(2L, "Alice", "b".getBytes(StandardCharsets.UTF_8)))) | ||
|
|
||
| val valueSchema = StructType(Seq( | ||
| StructField("age", LongType), | ||
| StructField("name", StringType), | ||
| StructField("b", BinaryType))) | ||
| val schema = StructType(Seq( | ||
| StructField("key", LongType), | ||
| StructField("value", valueSchema))) | ||
|
|
||
| val df = spark.createDataFrame(rows, schema) | ||
|
|
||
| checkError( | ||
| exception = intercept[AnalysisException] { | ||
| df.select(to_csv($"value")).collect() | ||
| }, | ||
| errorClass = "DATATYPE_MISMATCH.UNSUPPORTED_INPUT_TYPE", | ||
| parameters = Map( | ||
| "functionName" -> "`to_csv`", | ||
| "dataType" -> "\"STRUCT<age: BIGINT, name: STRING, b: BINARY>\"", | ||
| "sqlExpr" -> "\"to_csv(value)\""), | ||
| context = ExpectedContext(fragment = "to_csv", getCurrentClassCallSitePattern) | ||
| ) | ||
|
|
||
| checkError( | ||
| exception = intercept[SparkUnsupportedOperationException] { | ||
| df.select(from_csv(lit("data"), valueSchema, Map.empty[String, String])).collect() | ||
| }, | ||
| errorClass = "UNSUPPORTED_DATATYPE", | ||
| parameters = Map("typeName" -> "\"BINARY\"") | ||
| ) | ||
| } | ||
|
|
||
| test("SPARK-46654: from_csv/to_csv does not support NullType data") { | ||
| val df = Seq(Tuple1(Tuple1(null))).toDF("value") | ||
| val valueSchema = df.schema | ||
| val options = Map("nullValue" -> "-") | ||
|
|
||
| checkError( | ||
| exception = intercept[AnalysisException] { | ||
| df.select(to_csv($"value", options.asJava)).collect() | ||
| }, | ||
| errorClass = "DATATYPE_MISMATCH.UNSUPPORTED_INPUT_TYPE", | ||
| parameters = Map( | ||
| "functionName" -> "`to_csv`", | ||
| "dataType" -> "\"STRUCT<_1: VOID>\"", | ||
| "sqlExpr" -> "\"to_csv(value)\""), | ||
| context = ExpectedContext(fragment = "to_csv", getCurrentClassCallSitePattern) | ||
| ) | ||
|
|
||
| checkError( | ||
| exception = intercept[SparkUnsupportedOperationException] { | ||
| df.select(from_csv(lit("data"), valueSchema, options)).collect() | ||
| }, | ||
| errorClass = "UNSUPPORTED_DATATYPE", | ||
| parameters = Map("typeName" -> "\"STRUCT<_1: VOID>\"") | ||
| ) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering that
to_csvno longer supportscomplex type data, we will remove this example.