Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
6 changes: 6 additions & 0 deletions core/src/main/resources/error/error-classes.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
"INCOMPATIBLE_DATASOURCE_REGISTER" : {
"message" : [ "Detected an incompatible DataSourceRegister. Please remove the incompatible library from classpath or upgrade it. Error: %s" ]
},
"INCONSISTENT_BEHAVIOR_CROSS_VERSION" : {
"message" : [ "You may get a different result due to the upgrading of Spark %s: %s" ]
},
"INDEX_OUT_OF_BOUNDS" : {
"message" : [ "Index %s must be between 0 and the length of the ArrayData." ],
"sqlState" : "22023"
Expand Down Expand Up @@ -152,6 +155,9 @@
"UNSUPPORTED_GROUPING_EXPRESSION" : {
"message" : [ "grouping()/grouping_id() can only be used with GroupingSets/Cube/Rollup" ]
},
"UNSUPPORTED_OPERATION" : {
"message" : [ "The operation is not supported: %s" ]
},
"WRITING_JOB_ABORTED" : {
"message" : [ "Writing job aborted" ],
"sqlState" : "40000"
Expand Down
32 changes: 29 additions & 3 deletions core/src/main/scala/org/apache/spark/SparkException.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,35 @@ private[spark] case class ExecutorDeadException(message: String)
/**
* Exception thrown when Spark returns different result after upgrading to a new version.
*/
private[spark] class SparkUpgradeException(version: String, message: String, cause: Throwable)
extends RuntimeException("You may get a different result due to the upgrading of Spark" +
s" $version: $message", cause)
private[spark] class SparkUpgradeException(
version: String,
message: String,
cause: Throwable,
errorClass: Option[String],
messageParameters: Array[String])
extends RuntimeException(message, cause) with SparkThrowable {

def this(version: String, message: String, cause: Throwable) =
this (
version = version,
message = s"You may get a different result due to the upgrading of Spark $version: $message",

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.

Let's avoid error message duplication. error-classes.json should be one source of truth:

  "INCONSISTENT_BEHAVIOR_CROSS_VERSION" : {
    "message" : [ "You may get a different result due to the upgrading of Spark %s: %s" ]
  },

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.

Thanks @MaxGekk. Will fix this.

cause = cause,
errorClass = None,
messageParameters = Array.empty
)

def this(version: String, errorClass: String,
messageParameters: Array[String], cause: Throwable) =
this(
version = version,
message = SparkThrowableHelper.getMessage(errorClass, messageParameters),
cause = cause,
errorClass = Some(errorClass),
messageParameters = messageParameters
)

override def getErrorClass: String = errorClass.orNull
}

/**
* Arithmetic exception thrown from Spark with an error class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -533,30 +533,42 @@ object QueryExecutionErrors {

def sparkUpgradeInReadingDatesError(
format: String, config: String, option: String): SparkUpgradeException = {
new SparkUpgradeException("3.0",
s"""
|reading dates before 1582-10-15 or timestamps before 1900-01-01T00:00:00Z from $format
|files can be ambiguous, as the files may be written by Spark 2.x or legacy versions of
|Hive, which uses a legacy hybrid calendar that is different from Spark 3.0+'s Proleptic
|Gregorian calendar. See more details in SPARK-31404. You can set the SQL config
|'$config' or the datasource option '$option' to 'LEGACY' to rebase the datetime values
|w.r.t. the calendar difference during reading. To read the datetime values as it is,
|set the SQL config '$config' or the datasource option '$option' to 'CORRECTED'.
""".stripMargin, null)
new SparkUpgradeException(
version = "3.0",
errorClass = "INCONSISTENT_BEHAVIOR_CROSS_VERSION",
messageParameters = Array(
"3.0",
s"""
|reading dates before 1582-10-15 or timestamps before 1900-01-01T00:00:00Z from $format
|files can be ambiguous, as the files may be written by Spark 2.x or legacy versions of
|Hive, which uses a legacy hybrid calendar that is different from Spark 3.0+'s Proleptic
|Gregorian calendar. See more details in SPARK-31404. You can set the SQL config
|'$config' or the datasource option '$option' to 'LEGACY' to rebase the datetime values
|w.r.t. the calendar difference during reading. To read the datetime values as it is,
|set the SQL config '$config' or the datasource option '$option' to 'CORRECTED'.
""".stripMargin),
cause = null
)
}

def sparkUpgradeInWritingDatesError(format: String, config: String): SparkUpgradeException = {
new SparkUpgradeException("3.0",
s"""
|writing dates before 1582-10-15 or timestamps before 1900-01-01T00:00:00Z into $format
|files can be dangerous, as the files may be read by Spark 2.x or legacy versions of Hive
|later, which uses a legacy hybrid calendar that is different from Spark 3.0+'s Proleptic
|Gregorian calendar. See more details in SPARK-31404. You can set $config to 'LEGACY' to
|rebase the datetime values w.r.t. the calendar difference during writing, to get maximum
|interoperability. Or set $config to 'CORRECTED' to write the datetime values as it is,
|if you are 100% sure that the written files will only be read by Spark 3.0+ or other
|systems that use Proleptic Gregorian calendar.
""".stripMargin, null)
new SparkUpgradeException(
version = "3.0",
errorClass = "INCONSISTENT_BEHAVIOR_CROSS_VERSION",
messageParameters = Array(
"3.0",
s"""
|writing dates before 1582-10-15 or timestamps before 1900-01-01T00:00:00Z into $format
|files can be dangerous, as the files may be read by Spark 2.x or legacy versions of Hive
|later, which uses a legacy hybrid calendar that is different from Spark 3.0+'s Proleptic
|Gregorian calendar. See more details in SPARK-31404. You can set $config to 'LEGACY' to
|rebase the datetime values w.r.t. the calendar difference during writing, to get maximum
|interoperability. Or set $config to 'CORRECTED' to write the datetime values as it is,
|if you are 100% sure that the written files will only be read by Spark 3.0+ or other
|systems that use Proleptic Gregorian calendar.
""".stripMargin),
cause = null
)
}

def buildReaderUnsupportedForFileFormatError(format: String): Throwable = {
Expand Down Expand Up @@ -1617,8 +1629,12 @@ object QueryExecutionErrors {
}

def timeZoneIdNotSpecifiedForTimestampTypeError(): Throwable = {
new UnsupportedOperationException(
s"${TimestampType.catalogString} must supply timeZoneId parameter")
new SparkUnsupportedOperationException(
errorClass = "UNSUPPORTED_OPERATION",
messageParameters = Array(
s"${TimestampType.catalogString} must supply timeZoneId parameter " +
s"while converting to ArrowType")
)
}

def notPublicClassError(name: String): Throwable = {
Expand Down Expand Up @@ -1932,7 +1948,9 @@ object QueryExecutionErrors {
}

def cannotConvertOrcTimestampToTimestampNTZError(): Throwable = {
new RuntimeException("Unable to convert timestamp of Orc to data type 'timestamp_ntz'")
new SparkUnsupportedOperationException(
errorClass = "UNSUPPORTED_OPERATION",
messageParameters = Array("Unable to convert timestamp of Orc to data type 'timestamp_ntz'"))
}

def writePartitionExceedConfigSizeWhenDynamicPartitionError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,22 @@

package org.apache.spark.sql.errors

import org.apache.spark.{SparkException, SparkIllegalArgumentException, SparkRuntimeException, SparkUnsupportedOperationException}
import org.apache.spark.sql.{DataFrame, QueryTest}
import java.sql.Timestamp

import org.apache.spark.{SparkException, SparkIllegalArgumentException, SparkRuntimeException, SparkUnsupportedOperationException, SparkUpgradeException}
import org.apache.spark.sql.{DataFrame, QueryTest, Row}
import org.apache.spark.sql.execution.datasources.orc.OrcTest
import org.apache.spark.sql.execution.datasources.parquet.ParquetTest
import org.apache.spark.sql.functions.{lit, lower, struct, sum}
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.internal.SQLConf.LegacyBehaviorPolicy.EXCEPTION
import org.apache.spark.sql.test.SharedSparkSession
import org.apache.spark.sql.types.{StructField, StructType, TimestampNTZType, TimestampType}
import org.apache.spark.sql.util.ArrowUtils

class QueryExecutionErrorsSuite extends QueryTest
with ParquetTest with OrcTest with SharedSparkSession {

class QueryExecutionErrorsSuite extends QueryTest with SharedSparkSession {
import testImplicits._

private def getAesInputs(): (DataFrame, DataFrame) = {
Expand Down Expand Up @@ -171,4 +181,72 @@ class QueryExecutionErrorsSuite extends QueryTest with SharedSparkSession {
assert(e2.getSqlState === "0A000")
assert(e2.getMessage === "The feature is not supported: Pivot not after a groupBy.")
}

test("INCONSISTENT_BEHAVIOR_CROSS_VERSION: " +
"compatibility with Spark 2.4/3.2 in reading/writing dates") {

// Fail to read ancient datetime values.
withSQLConf(SQLConf.PARQUET_REBASE_MODE_IN_READ.key -> EXCEPTION.toString) {
val fileName = "before_1582_date_v2_4_5.snappy.parquet"
val filePath = getResourceParquetFilePath("test-data/" + fileName)
val e = intercept[SparkException] {
spark.read.parquet(filePath).collect()
}.getCause.asInstanceOf[SparkUpgradeException]

assert(e.getErrorClass === "INCONSISTENT_BEHAVIOR_CROSS_VERSION")
assert(e.getMessage
.startsWith("You may get a different result due to the upgrading of Spark 3.0: \n" +

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.

The message confuses slightly. We run Spark 3.3.0-SNAPSHOT (almost 3.3) and try to read a file written by Spark 2.4 but the message says: due to the upgrading of Spark 3.0.

Should be somehow: ... due to upgrading to Spark >= 3.0?

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.

Spark >= 3.0 sounds good. Will address in next iteration.

"reading dates before"))
}

// Fail to write ancient datetime values.
withSQLConf(SQLConf.PARQUET_REBASE_MODE_IN_WRITE.key -> EXCEPTION.toString) {
withTempPath { dir =>
val df = Seq(java.sql.Date.valueOf("1001-01-01")).toDF("dt")
val e = intercept[SparkException] {
df.write.parquet(dir.getCanonicalPath)
}.getCause.getCause.getCause.asInstanceOf[SparkUpgradeException]

assert(e.getErrorClass === "INCONSISTENT_BEHAVIOR_CROSS_VERSION")
assert(e.getMessage
.startsWith("You may get a different result due to the upgrading of Spark 3.0: \n" +
"writing dates before"))
}
}
}

test("UNSUPPORTED_OPERATION: timeZoneId not specified while converting TimestampType to Arrow") {
val schema = new StructType().add("value", TimestampType)
val e = intercept[SparkUnsupportedOperationException] {
ArrowUtils.toArrowSchema(schema, null)
}

assert(e.getErrorClass === "UNSUPPORTED_OPERATION")
assert(e.getMessage === "The operation is not supported: " +
"timestamp must supply timeZoneId parameter while converting to ArrowType")
}

test("UNSUPPORTED_OPERATION - SPARK-36346: can't read Timestamp as TimestampNTZ") {
val data = (1 to 10).map { i =>
val ts = new Timestamp(i)
Row(ts)
}

val actualSchema = StructType(Seq(StructField("time", TimestampType, false)))
val providedSchema = StructType(Seq(StructField("time", TimestampNTZType, false)))

withTempPath { file =>
val df = spark.createDataFrame(sparkContext.parallelize(data), actualSchema)
df.write.orc(file.getCanonicalPath)
withAllNativeOrcReaders {
val e = intercept[SparkException] {
spark.read.schema(providedSchema).orc(file.getCanonicalPath).collect()
}.getCause.asInstanceOf[SparkUnsupportedOperationException]

assert(e.getErrorClass === "UNSUPPORTED_OPERATION")
assert(e.getMessage === "The operation is not supported: " +
"Unable to convert timestamp of Orc to data type 'timestamp_ntz'")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -804,32 +804,6 @@ abstract class OrcQuerySuite extends OrcQueryTest with SharedSparkSession {
}
}

test("SPARK-36346: can't read TimestampLTZ as TimestampNTZ") {
val data = (1 to 10).map { i =>
val ts = new Timestamp(i)
Row(ts)
}
val answer = (1 to 10).map { i =>
// The second parameter is `nanoOfSecond`, while java.sql.Timestamp accepts milliseconds
// as input. So here we multiple the `nanoOfSecond` by NANOS_PER_MILLIS
val ts = LocalDateTime.ofEpochSecond(0, i * 1000000, ZoneOffset.UTC)
Row(ts)
}
val actualSchema = StructType(Seq(StructField("time", TimestampType, false)))
val providedSchema = StructType(Seq(StructField("time", TimestampNTZType, false)))

withTempPath { file =>
val df = spark.createDataFrame(sparkContext.parallelize(data), actualSchema)
df.write.orc(file.getCanonicalPath)
withAllNativeOrcReaders {
val msg = intercept[SparkException] {
spark.read.schema(providedSchema).orc(file.getCanonicalPath).collect()
}.getMessage
assert(msg.contains("Unable to convert timestamp of Orc to data type 'timestamp_ntz'"))
}
}
}

test("SPARK-36346: read TimestampNTZ as TimestampLTZ") {
val data = (1 to 10).map { i =>
// The second parameter is `nanoOfSecond`, while java.sql.Timestamp accepts milliseconds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import org.apache.spark.sql.internal.SQLConf.ORC_IMPLEMENTATION
* -> HiveOrcPartitionDiscoverySuite
* -> OrcFilterSuite
*/
abstract class OrcTest extends QueryTest with FileBasedDataSourceTest with BeforeAndAfterAll {
trait OrcTest extends QueryTest with FileBasedDataSourceTest with BeforeAndAfterAll {

val orcImp: String = "native"

Expand Down