Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9fe1769
[SPARK-46654][SQL] df.show() of pyspark displayed different results b…
panbingkun Jan 10, 2024
22a7afb
[SPARK-46654][SQL] to_csv can display complex types data
panbingkun Jan 11, 2024
4e770cb
enable pyspark ut for to_csv
panbingkun Jan 11, 2024
8f72454
Merge branch 'master' into SPARK-46654
panbingkun Jan 11, 2024
bb6eec5
[SPARK-46654][SQL] Make to_csv can correctly display complex types data
panbingkun Jan 11, 2024
9695e97
[SPARK-46654][SQL] Make to_csv can correctly display complex types data
panbingkun Jan 24, 2024
2cef851
[SPARK-46654][SQL] Make to_csv can correctly display complex types data
panbingkun Jan 24, 2024
ab2243b
Merge branch 'master' into SPARK-46654
panbingkun Jan 24, 2024
daba8aa
Merge branch 'master' into SPARK-46654
panbingkun Feb 18, 2024
ad12661
Merge branch 'master' into SPARK-46654
panbingkun Feb 18, 2024
5ce4eab
[SPARK-46654][SQL] Make to_csv explicitly indicate that it does not s…
panbingkun Feb 18, 2024
d038ffa
[SPARK-46654][SQL] Make to_csv explicitly indicate that it does not s…
panbingkun Feb 18, 2024
aea0d66
Merge branch 'master' into SPARK-46654
panbingkun Feb 20, 2024
93d890e
fix paramIndex
panbingkun Feb 20, 2024
e28668a
Merge branch 'master' into SPARK-46654
panbingkun Feb 23, 2024
f3d215f
[SPARK-46654][SQL][Python] Make to_csv explicitly indicate that it do…
panbingkun Feb 23, 2024
b3fd021
Merge branch 'master' into SPARK-46654
panbingkun Feb 29, 2024
b1daa88
Merge branch 'master' into SPARK-46654
panbingkun Mar 11, 2024
7520803
[SPARK-46654][SQL][Python] Make to_csv explicitly indicate that it do…
panbingkun Mar 11, 2024
d012f0f
fix
panbingkun Mar 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/sql-migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ license: |
- Since Spark 4.0, the default value of `spark.sql.orc.compression.codec` is changed from `snappy` to `zstd`. To restore the previous behavior, set `spark.sql.orc.compression.codec` to `snappy`.
- Since Spark 4.0, `SELECT (*)` is equivalent to `SELECT struct(*)` instead of `SELECT *`. To restore the previous behavior, set `spark.sql.legacy.ignoreParenthesesAroundStar` to `true`.
- Since Spark 4.0, the SQL config `spark.sql.legacy.allowZeroIndexInFormatString` is deprecated. Consider to change `strfmt` of the `format_string` function to use 1-based indexes. The first argument must be referenced by "1$", the second by "2$", etc.
- Since Spark 4.0, the function `to_csv` no longer supports input with the data type `STRUCT`, `ARRAY`, `MAP`, `VARIANT` and `BINARY` (because the `CSV specification` does not have standards for these data types and cannot be read back using `from_csv`), Spark will throw `DATATYPE_MISMATCH.UNSUPPORTED_INPUT_TYPE` exception.

## Upgrading from Spark SQL 3.4 to 3.5

Expand Down
18 changes: 2 additions & 16 deletions python/pyspark/sql/functions/builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering that to_csv no longer supports complex type data, we will remove this example.


>>> 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]"|

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me update.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the document sql-migration-guide.md.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should simply disallow this. We could have a legacy conf, but it doesn't much make sense. What does it return for ArrayType and MapType?

@HyukjinKwon See what it does return for ArrayType in the example. I doubt that we should simply disallow it w/o any legacy config at least.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case of Python, it is because the data type is ArrayData (specifically implemented as GenericArrayData),
This case just goes into the following logic:

case dt: DataType =>
(row: InternalRow, ordinal: Int) =>
row.get(ordinal, dt).toString

override def toString(): String = array.mkString("[", ",", "]")

And it just so happens that GenericArrayData overrides the toString method, so it appears to display the result correctly, perhaps it's just a coincidence?

+-----------------------+

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
Expand All @@ -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))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -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",

@panbingkun panbingkun Mar 11, 2024 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using UNSUPPORTED_INPUT_TYPE instead of UNEXPECTED_INPUT_TYPE here will make the prompt information more clear to the user,

for example:

    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)

    df.select(to_csv($"value")).collect()

Before(Last review):
Users may feel confused about prompt: requires the "STRUCT" type, however "value" has the type "STRUCT...

[DATATYPE_MISMATCH.UNEXPECTED_INPUT_TYPE] Cannot resolve "to_csv(value)" due to data type mismatch: The 1 parameter requires the "STRUCT" type, however "value" has the type "STRUCT<age: BIGINT, name: STRING, scores: MAP<STRING, BIGINT>>". SQLSTATE: 42K09;

After(Right now):

[DATATYPE_MISMATCH.UNSUPPORTED_INPUT_TYPE] Cannot resolve "to_csv(value)" due to data type mismatch: The input of `to_csv` can't be "STRUCT<age: BIGINT, name: STRING, scores: MAP<STRING, BIGINT>>" type data. SQLSTATE: 42K09;

messageParameters = Map(
"functionName" -> toSQLId(prettyName),
"dataType" -> toSQLType(child.dataType)
)
)
}
}

@tailrec
private def isSupportedDataType(dataType: DataType): Boolean = dataType match {
case _: VariantType | BinaryType => false

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you clarify this? Why don't you support the types?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because these types cannot be read back through from_csv,
I have added related tests in UT CsvFunctionsSuite.

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(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's merge type checking into one place.

errorClass = "_LEGACY_ERROR_TEMP_3234",
messageParameters = Map("other" -> other.catalogString))
}
lazy val inputSchema: StructType = child.dataType.asInstanceOf[StructType]

@transient
lazy val gen = new UnivocityGenerator(
Expand Down
225 changes: 221 additions & 4 deletions sql/core/src/test/scala/org/apache/spark/sql/CsvFunctionsSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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._
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -294,10 +296,19 @@ class CsvFunctionsSuite extends QueryTest with SharedSparkSession {
}

test("to_csv with option (nullValue)") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain, please, why did you change the test?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because NullType cannot be read back through from_csv.
In the new logic, it will throw DATATYPE_MISMATCH.UNSUPPORTED_INPUT_TYPE exception.
And as far as I understand, this UT is mainly to test whether option (nullValue) is effective,
The test target of changed UT are consistent with this.

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)") {
Expand Down Expand Up @@ -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>\"")
)
}
}