From 2d3b073950f8c875c2e3e5ef7f8cdb5c6b221ef3 Mon Sep 17 00:00:00 2001 From: Max Gekk Date: Mon, 15 Aug 2022 11:19:50 +0300 Subject: [PATCH 01/16] Add ErrorMessageFormat --- .../scala/org/apache/spark/ErrorInfo.scala | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/core/src/main/scala/org/apache/spark/ErrorInfo.scala b/core/src/main/scala/org/apache/spark/ErrorInfo.scala index 6c72a27aa4b3d..c8bac415765e8 100644 --- a/core/src/main/scala/org/apache/spark/ErrorInfo.scala +++ b/core/src/main/scala/org/apache/spark/ErrorInfo.scala @@ -25,6 +25,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.core.`type`.TypeReference import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.scala.DefaultScalaModule +import org.json4s.{JInt, JString} +import org.json4s.JsonAST.{JArray, JObject} +import org.json4s.JsonDSL._ +import org.json4s.jackson.JsonMethods.{compact, render} import org.apache.spark.util.Utils @@ -135,4 +139,39 @@ private[spark] object SparkThrowableHelper { def isInternalError(errorClass: String): Boolean = { errorClass == "INTERNAL_ERROR" } + + object ErrorMessageFormat extends Enumeration { + val PRETTY, MINIMAL, STANDARD = Value + } + + def getMessage(e: SparkThrowable with Throwable, format: ErrorMessageFormat.Value): String = { + import ErrorMessageFormat._ + format match { + case PRETTY => e.getMessage + case MINIMAL | STANDARD if e.getErrorClass == null => + val jValue = ("errorClass" -> "legacy") ~ + ("messageParameters" -> JObject(List("message" -> JString(e.getMessage)))) ~ + ("queryContext" -> JArray(List.empty)) + compact(render(jValue)) + case MINIMAL | STANDARD => + val message = if (format == STANDARD) Some(e.getMessage) else None + assert(e.getParameterNames.size == e.getMessageParameters.size, + "Number of message parameter names and values must be the same") + val jValue = ("errorClass" -> e.getErrorClass) ~ + ("errorSubClass" -> Option(e.getErrorSubClass)) ~ + ("message" -> message) ~ + ("sqlState" -> Option(e.getSqlState)) ~ + ("messageParameters" -> + JObject((e.getParameterNames zip e.getMessageParameters.map(JString)).toList)) ~ + ("queryContext" -> JArray( + e.getQueryContext.map(c => JObject( + "objectType" -> JString(c.objectType()), + "objectName" -> JString(c.objectName()), + "startIndex" -> JInt(c.startIndex()), + "stopIndex" -> JInt(c.stopIndex()), + "fragment" -> JString(c.fragment()))).toList) + ) + compact(render(jValue)) + } + } } From f15dc89361273106b2345badc125de3e027e114d Mon Sep 17 00:00:00 2001 From: Max Gekk Date: Mon, 15 Aug 2022 12:11:27 +0300 Subject: [PATCH 02/16] Rename the file ErrorInfo.scala to SparkThrowableHelper.scala --- .../apache/spark/{ErrorInfo.scala => SparkThrowableHelper.scala} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename core/src/main/scala/org/apache/spark/{ErrorInfo.scala => SparkThrowableHelper.scala} (100%) diff --git a/core/src/main/scala/org/apache/spark/ErrorInfo.scala b/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala similarity index 100% rename from core/src/main/scala/org/apache/spark/ErrorInfo.scala rename to core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala From 69e478b9cfce795422c31cd990bd0e12d29826b7 Mon Sep 17 00:00:00 2001 From: Max Gekk Date: Mon, 15 Aug 2022 12:13:19 +0300 Subject: [PATCH 03/16] Move ErrorMessageFormat out of the SparkThrowableHelper object --- .../scala/org/apache/spark/SparkThrowableHelper.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala b/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala index c8bac415765e8..6c41b3b3298cd 100644 --- a/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala +++ b/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala @@ -61,6 +61,10 @@ private[spark] case class ErrorInfo( val messageFormat: String = message.mkString("\n") } +object ErrorMessageFormat extends Enumeration { + val PRETTY, MINIMAL, STANDARD = Value +} + /** * Companion object used by instances of [[SparkThrowable]] to access error class information and * construct error messages. @@ -140,10 +144,6 @@ private[spark] object SparkThrowableHelper { errorClass == "INTERNAL_ERROR" } - object ErrorMessageFormat extends Enumeration { - val PRETTY, MINIMAL, STANDARD = Value - } - def getMessage(e: SparkThrowable with Throwable, format: ErrorMessageFormat.Value): String = { import ErrorMessageFormat._ format match { From f19710d259b79095f9968bf3bdc80f8ff33cdd0a Mon Sep 17 00:00:00 2001 From: Max Gekk Date: Mon, 15 Aug 2022 13:48:01 +0300 Subject: [PATCH 04/16] Add a test --- .../apache/spark/SparkThrowableHelper.scala | 14 +++-- .../apache/spark/SparkThrowableSuite.scala | 53 +++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala b/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala index 6c41b3b3298cd..39a98a1dbd8be 100644 --- a/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala +++ b/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala @@ -28,7 +28,7 @@ import com.fasterxml.jackson.module.scala.DefaultScalaModule import org.json4s.{JInt, JString} import org.json4s.JsonAST.{JArray, JObject} import org.json4s.JsonDSL._ -import org.json4s.jackson.JsonMethods.{compact, render} +import org.json4s.jackson.JsonMethods.{compact, pretty, render} import org.apache.spark.util.Utils @@ -154,10 +154,15 @@ private[spark] object SparkThrowableHelper { ("queryContext" -> JArray(List.empty)) compact(render(jValue)) case MINIMAL | STANDARD => - val message = if (format == STANDARD) Some(e.getMessage) else None + val errorClass = e.getErrorClass + val message = if (format == STANDARD) { + val errorInfo = errorClassToInfoMap.getOrElse(errorClass, + throw new IllegalArgumentException(s"Cannot find error class '$errorClass'")) + Some(errorInfo.messageFormat) + } else None assert(e.getParameterNames.size == e.getMessageParameters.size, "Number of message parameter names and values must be the same") - val jValue = ("errorClass" -> e.getErrorClass) ~ + val jValue = ("errorClass" -> errorClass) ~ ("errorSubClass" -> Option(e.getErrorSubClass)) ~ ("message" -> message) ~ ("sqlState" -> Option(e.getSqlState)) ~ @@ -171,7 +176,8 @@ private[spark] object SparkThrowableHelper { "stopIndex" -> JInt(c.stopIndex()), "fragment" -> JString(c.fragment()))).toList) ) - compact(render(jValue)) + val rendered = render(jValue) + if (format == MINIMAL) compact(rendered) else pretty(rendered) } } } diff --git a/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala b/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala index 76d7e3048d79a..6f07b7fc03860 100644 --- a/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala +++ b/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala @@ -222,4 +222,57 @@ class SparkThrowableSuite extends SparkFunSuite { assert(false) } } + + test("get message in the specified format") { + import ErrorMessageFormat._ + class TestQueryContext extends QueryContext { + override val objectName = "v1" + override val objectType = "VIEW" + override val startIndex = 1 + override val stopIndex = 5 + override val fragment = "1 / 0" + } + val e = new SparkArithmeticException( + errorClass = "DIVIDE_BY_ZERO", + errorSubClass = None, + messageParameters = Array("CONFIG"), + context = Array(new TestQueryContext), + summary = "Query summary") + + assert(SparkThrowableHelper.getMessage(e, PRETTY) === + "[DIVIDE_BY_ZERO] Division by zero. Use `try_divide` to tolerate divisor being 0 " + + "and return NULL instead. If necessary set CONFIG to \"false\" to bypass this error." + + "\nQuery summary") + assert(SparkThrowableHelper.getMessage(e, MINIMAL) === + """{"errorClass":"DIVIDE_BY_ZERO","sqlState":"22012",""" + + """"messageParameters":{"config":"CONFIG"},"queryContext":[{"objectType":"VIEW",""" + + """"objectName":"v1","startIndex":1,"stopIndex":5,"fragment":"1 / 0"}]}""") + assert(SparkThrowableHelper.getMessage(e, STANDARD) === + // scalastyle:off line.size.limit + """{ + | "errorClass" : "DIVIDE_BY_ZERO", + | "message" : "Division by zero. Use `try_divide` to tolerate divisor being 0 and return NULL instead. If necessary set to \"false\" to bypass this error.", + | "sqlState" : "22012", + | "messageParameters" : { + | "config" : "CONFIG" + | }, + | "queryContext" : [ { + | "objectType" : "VIEW", + | "objectName" : "v1", + | "startIndex" : 1, + | "stopIndex" : 5, + | "fragment" : "1 / 0" + | } ] + |}""".stripMargin) + // scalastyle:on line.size.limit + // Legacy mode when an exception does not have any error class + class LegacyException extends Throwable with SparkThrowable { + override def getErrorClass: String = null + override def getMessage: String = "Test message" + } + val e2 = new LegacyException + assert(SparkThrowableHelper.getMessage(e2, MINIMAL) === + """{"errorClass":"legacy","messageParameters":{"message":"Test message"},""" + + """"queryContext":[]}""") + } } From 363323d146509a354490be8a08822b34339e3480 Mon Sep 17 00:00:00 2001 From: Max Gekk Date: Mon, 15 Aug 2022 17:39:29 +0300 Subject: [PATCH 05/16] Add the SQL config spark.sql.error.messageFormat --- .../scala/org/apache/spark/sql/internal/SQLConf.scala | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 f78fa8c9ef251..0d3998455b628 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 @@ -31,7 +31,7 @@ import scala.util.matching.Regex import org.apache.hadoop.fs.Path -import org.apache.spark.{SparkConf, SparkContext, TaskContext} +import org.apache.spark.{ErrorMessageFormat, SparkConf, SparkContext, TaskContext} import org.apache.spark.internal.Logging import org.apache.spark.internal.config._ import org.apache.spark.internal.config.{IGNORE_MISSING_FILES => SPARK_IGNORE_MISSING_FILES} @@ -3872,6 +3872,15 @@ object SQLConf { .booleanConf .createWithDefault(false) + val ERROR_MESSAGE_FORMAT = buildConf("spark.sql.error.messageFormat") + .doc("When PRETTY, the error message consists of textual representation of error class, " + + " message and query context. The MINIMAL and STANDARD formats are JSON formats where " + + "STANDARD is pretty JSON with additional JSON field `message`.") + .version("3.4.0") + .stringConf.transform(_.toUpperCase(Locale.ROOT)) + .checkValues(ErrorMessageFormat.values.map(_.toString)) + .createWithDefault(ErrorMessageFormat.PRETTY.toString) + /** * Holds information about keys that have been deprecated. * From fd2ca73f3987e8b31d2e43a07f626bfc21270416 Mon Sep 17 00:00:00 2001 From: Max Gekk Date: Mon, 15 Aug 2022 18:01:34 +0300 Subject: [PATCH 06/16] Modify message in runningQueryError --- .../scala/org/apache/spark/sql/internal/SQLConf.scala | 3 +++ .../hive/thriftserver/HiveThriftServerErrors.scala | 11 +++++------ .../thriftserver/SparkExecuteStatementOperation.scala | 3 ++- 3 files changed, 10 insertions(+), 7 deletions(-) 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 0d3998455b628..afa5d4449a5ba 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 @@ -4659,6 +4659,9 @@ class SQLConf extends Serializable with Logging { def histogramNumericPropagateInputType: Boolean = getConf(SQLConf.HISTOGRAM_NUMERIC_PROPAGATE_INPUT_TYPE) + def errorMessageFormat: ErrorMessageFormat.Value = + ErrorMessageFormat.withName(getConf(SQLConf.ERROR_MESSAGE_FORMAT)) + /** ********************** SQLConf functionality methods ************ */ /** Set Spark SQL configuration properties. */ diff --git a/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/HiveThriftServerErrors.scala b/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/HiveThriftServerErrors.scala index 4d786fd716b33..eaec382e78209 100644 --- a/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/HiveThriftServerErrors.scala +++ b/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/HiveThriftServerErrors.scala @@ -23,7 +23,7 @@ import java.util.concurrent.RejectedExecutionException import org.apache.hive.service.ServiceException import org.apache.hive.service.cli.{HiveSQLException, OperationType} -import org.apache.spark.SparkThrowable +import org.apache.spark.{ErrorMessageFormat, SparkThrowable, SparkThrowableHelper} /** * Object for grouping error messages from (most) exceptions thrown during @@ -36,11 +36,10 @@ object HiveThriftServerErrors { " new task for execution, please retry the operation", rejected) } - def runningQueryError(e: Throwable): Throwable = e match { - case st: SparkThrowable => - val errorClassPrefix = Option(st.getErrorClass).map(e => s"[$e] ").getOrElse("") - new HiveSQLException( - s"Error running query: ${errorClassPrefix}${st.toString}", st.getSqlState, st) + def runningQueryError(e: Throwable, format: ErrorMessageFormat.Value): Throwable = e match { + case st: SparkThrowable with Throwable => + val message = SparkThrowableHelper.getMessage(st, format) + new HiveSQLException(s"Error running query: $message", st.getSqlState, st) case _ => new HiveSQLException(s"Error running query: ${e.toString}", e) } diff --git a/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/SparkExecuteStatementOperation.scala b/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/SparkExecuteStatementOperation.scala index 090d741d9eed2..3292cbef41772 100644 --- a/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/SparkExecuteStatementOperation.scala +++ b/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/SparkExecuteStatementOperation.scala @@ -257,7 +257,8 @@ private[hive] class SparkExecuteStatementOperation( statementId, e.getMessage, SparkUtils.exceptionString(e)) e match { case _: HiveSQLException => throw e - case _ => throw HiveThriftServerErrors.runningQueryError(e) + case _ => throw HiveThriftServerErrors.runningQueryError( + e, sqlContext.conf.errorMessageFormat) } } } finally { From 3011fc180bf62370cd772a639e222ecf37251eff Mon Sep 17 00:00:00 2001 From: Max Gekk Date: Mon, 15 Aug 2022 21:45:24 +0300 Subject: [PATCH 07/16] Add a test for the Thrift server --- .../ThriftServerWithSparkContextSuite.scala | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala b/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala index 639a5e3a59892..5656e64fd4775 100644 --- a/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala +++ b/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala @@ -22,7 +22,7 @@ import java.util.concurrent.atomic.AtomicBoolean import org.apache.hive.service.cli.{HiveSQLException, OperationHandle} -import org.apache.spark.TaskKilled +import org.apache.spark.{ErrorMessageFormat, TaskKilled} import org.apache.spark.scheduler.{SparkListener, SparkListenerTaskEnd} import org.apache.spark.sql.internal.SQLConf @@ -149,6 +149,51 @@ trait ThriftServerWithSparkContextSuite extends SharedThriftServer { } } } + + test("formats of error messages") { + val sql = "select 1 / 0" + withCLIServiceClient() { client => + val sessionHandle = client.openSession(user, "") + val confOverlay = new java.util.HashMap[java.lang.String, java.lang.String] + val exec: String => OperationHandle = client.executeStatement(sessionHandle, _, confOverlay) + + exec(s"set ${SQLConf.ANSI_ENABLED.key}=true") + exec(s"set ${SQLConf.ERROR_MESSAGE_FORMAT.key}=${ErrorMessageFormat.PRETTY}") + val e1 = intercept[HiveSQLException](exec(sql)) + // scalastyle:off line.size.limit + assert(e1.getMessage === + """Error running query: [DIVIDE_BY_ZERO] Division by zero. Use `try_divide` to tolerate divisor being 0 and return NULL instead. If necessary set "spark.sql.ansi.enabled" to "false" to bypass this error. + |== SQL(line 1, position 8) == + |select 1 / 0 + | ^^^^^ + |""".stripMargin) + + exec(s"set ${SQLConf.ERROR_MESSAGE_FORMAT.key}=${ErrorMessageFormat.MINIMAL}") + val e2 = intercept[HiveSQLException](exec(sql)) + assert(e2.getMessage === + """Error running query: {"errorClass":"DIVIDE_BY_ZERO","sqlState":"22012","messageParameters":{"config":"\"spark.sql.ansi.enabled\""},"queryContext":[{"objectType":"","objectName":"","startIndex":7,"stopIndex":11,"fragment":"1 / "}]}""") + + exec(s"set ${SQLConf.ERROR_MESSAGE_FORMAT.key}=${ErrorMessageFormat.STANDARD}") + val e3 = intercept[HiveSQLException](exec(sql)) + assert(e3.getMessage === + """Error running query: { + | "errorClass" : "DIVIDE_BY_ZERO", + | "message" : "Division by zero. Use `try_divide` to tolerate divisor being 0 and return NULL instead. If necessary set to \"false\" to bypass this error.", + | "sqlState" : "22012", + | "messageParameters" : { + | "config" : "\"spark.sql.ansi.enabled\"" + | }, + | "queryContext" : [ { + | "objectType" : "", + | "objectName" : "", + | "startIndex" : 7, + | "stopIndex" : 11, + | "fragment" : "1 / " + | } ] + |}""".stripMargin) + // scalastyle:on line.size.limit + } + } } class ThriftServerWithSparkContextInBinarySuite extends ThriftServerWithSparkContextSuite { From e8c7182145e20e6f3112f2d4496b2afee4e68bc2 Mon Sep 17 00:00:00 2001 From: Max Gekk Date: Tue, 16 Aug 2022 20:17:41 +0300 Subject: [PATCH 08/16] Use SparkException.internalError --- core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala b/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala index 39a98a1dbd8be..11962817d019b 100644 --- a/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala +++ b/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala @@ -157,7 +157,7 @@ private[spark] object SparkThrowableHelper { val errorClass = e.getErrorClass val message = if (format == STANDARD) { val errorInfo = errorClassToInfoMap.getOrElse(errorClass, - throw new IllegalArgumentException(s"Cannot find error class '$errorClass'")) + throw SparkException.internalError(s"Cannot find the error class '$errorClass'")) Some(errorInfo.messageFormat) } else None assert(e.getParameterNames.size == e.getMessageParameters.size, From 247b7e2481569c32bd7389f9aff97cb4b5a8bfbe Mon Sep 17 00:00:00 2001 From: Max Gekk Date: Tue, 16 Aug 2022 21:35:02 +0300 Subject: [PATCH 09/16] Use Jackson instead of json4s --- .../apache/spark/SparkThrowableHelper.scala | 71 +++++++++++-------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala b/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala index 11962817d019b..0f5375f0b045a 100644 --- a/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala +++ b/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala @@ -25,11 +25,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.core.`type`.TypeReference import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.scala.DefaultScalaModule -import org.json4s.{JInt, JString} -import org.json4s.JsonAST.{JArray, JObject} -import org.json4s.JsonDSL._ -import org.json4s.jackson.JsonMethods.{compact, pretty, render} +import org.apache.spark.util.JsonProtocol.toJsonString import org.apache.spark.util.Utils /** @@ -149,35 +146,51 @@ private[spark] object SparkThrowableHelper { format match { case PRETTY => e.getMessage case MINIMAL | STANDARD if e.getErrorClass == null => - val jValue = ("errorClass" -> "legacy") ~ - ("messageParameters" -> JObject(List("message" -> JString(e.getMessage)))) ~ - ("queryContext" -> JArray(List.empty)) - compact(render(jValue)) + toJsonString { g => + g.writeStartObject() + g.writeStringField("errorClass", "legacy") + g.writeObjectFieldStart("messageParameters") + g.writeStringField("message", e.getMessage) + g.writeEndObject() + g.writeArrayFieldStart("queryContext") + g.writeEndArray() + g.writeEndObject() + } case MINIMAL | STANDARD => val errorClass = e.getErrorClass - val message = if (format == STANDARD) { - val errorInfo = errorClassToInfoMap.getOrElse(errorClass, - throw SparkException.internalError(s"Cannot find the error class '$errorClass'")) - Some(errorInfo.messageFormat) - } else None assert(e.getParameterNames.size == e.getMessageParameters.size, "Number of message parameter names and values must be the same") - val jValue = ("errorClass" -> errorClass) ~ - ("errorSubClass" -> Option(e.getErrorSubClass)) ~ - ("message" -> message) ~ - ("sqlState" -> Option(e.getSqlState)) ~ - ("messageParameters" -> - JObject((e.getParameterNames zip e.getMessageParameters.map(JString)).toList)) ~ - ("queryContext" -> JArray( - e.getQueryContext.map(c => JObject( - "objectType" -> JString(c.objectType()), - "objectName" -> JString(c.objectName()), - "startIndex" -> JInt(c.startIndex()), - "stopIndex" -> JInt(c.stopIndex()), - "fragment" -> JString(c.fragment()))).toList) - ) - val rendered = render(jValue) - if (format == MINIMAL) compact(rendered) else pretty(rendered) + toJsonString { generator => + val g = if (format == STANDARD) generator.useDefaultPrettyPrinter() else generator + g.writeStartObject() + g.writeStringField("errorClass", errorClass) + val errorSubClass = e.getErrorSubClass + if (errorSubClass != null) g.writeStringField("errorSubClass", errorSubClass) + if (format == STANDARD) { + val errorInfo = errorClassToInfoMap.getOrElse(errorClass, + throw SparkException.internalError(s"Cannot find the error class '$errorClass'")) + g.writeStringField("message", errorInfo.messageFormat) + } + val sqlState = e.getSqlState + if (sqlState != null) g.writeStringField("sqlState", sqlState) + g.writeObjectFieldStart("messageParameters") + (e.getParameterNames zip e.getMessageParameters).foreach { case (name, value) => + g.writeStringField(name, value) + } + g.writeEndObject() + g.writeArrayFieldStart("queryContext") + e.getQueryContext.map { c => + g.writeStartObject() + g.writeStringField("objectType", c.objectType()) + g.writeStringField("objectName", c.objectName()) + g.writeNumberField("startIndex", c.startIndex()) + g.writeNumberField("stopIndex", c.stopIndex()) + g.writeStringField("fragment", c.fragment()) + g.writeEndObject() + } + g.writeEndArray() + g.writeEndObject() + } } } } From 254aa7544e275042254160a4b01a4ef0f439f141 Mon Sep 17 00:00:00 2001 From: Max Gekk Date: Wed, 17 Aug 2022 09:54:58 +0300 Subject: [PATCH 10/16] 1-based indexes --- .../scala/org/apache/spark/SparkThrowableHelper.scala | 6 ++++-- .../scala/org/apache/spark/SparkThrowableSuite.scala | 9 ++++----- .../thriftserver/ThriftServerWithSparkContextSuite.scala | 6 +++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala b/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala index 0f5375f0b045a..24a7d51b99a70 100644 --- a/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala +++ b/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala @@ -183,8 +183,10 @@ private[spark] object SparkThrowableHelper { g.writeStartObject() g.writeStringField("objectType", c.objectType()) g.writeStringField("objectName", c.objectName()) - g.writeNumberField("startIndex", c.startIndex()) - g.writeNumberField("stopIndex", c.stopIndex()) + val startIndex = c.startIndex() + 1 + if (startIndex > 0) g.writeNumberField("startIndex", startIndex) + val stopIndex = c.stopIndex() + 1 + if (stopIndex > 0) g.writeNumberField("stopIndex", stopIndex) g.writeStringField("fragment", c.fragment()) g.writeEndObject() } diff --git a/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala b/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala index 6f07b7fc03860..e6642498ae4fc 100644 --- a/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala +++ b/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala @@ -228,8 +228,8 @@ class SparkThrowableSuite extends SparkFunSuite { class TestQueryContext extends QueryContext { override val objectName = "v1" override val objectType = "VIEW" - override val startIndex = 1 - override val stopIndex = 5 + override val startIndex = 2 + override val stopIndex = -1 override val fragment = "1 / 0" } val e = new SparkArithmeticException( @@ -246,7 +246,7 @@ class SparkThrowableSuite extends SparkFunSuite { assert(SparkThrowableHelper.getMessage(e, MINIMAL) === """{"errorClass":"DIVIDE_BY_ZERO","sqlState":"22012",""" + """"messageParameters":{"config":"CONFIG"},"queryContext":[{"objectType":"VIEW",""" + - """"objectName":"v1","startIndex":1,"stopIndex":5,"fragment":"1 / 0"}]}""") + """"objectName":"v1","startIndex":3,"fragment":"1 / 0"}]}""") assert(SparkThrowableHelper.getMessage(e, STANDARD) === // scalastyle:off line.size.limit """{ @@ -259,8 +259,7 @@ class SparkThrowableSuite extends SparkFunSuite { | "queryContext" : [ { | "objectType" : "VIEW", | "objectName" : "v1", - | "startIndex" : 1, - | "stopIndex" : 5, + | "startIndex" : 3, | "fragment" : "1 / 0" | } ] |}""".stripMargin) diff --git a/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala b/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala index 5656e64fd4775..3ed82cdd094ec 100644 --- a/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala +++ b/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala @@ -171,7 +171,7 @@ trait ThriftServerWithSparkContextSuite extends SharedThriftServer { exec(s"set ${SQLConf.ERROR_MESSAGE_FORMAT.key}=${ErrorMessageFormat.MINIMAL}") val e2 = intercept[HiveSQLException](exec(sql)) assert(e2.getMessage === - """Error running query: {"errorClass":"DIVIDE_BY_ZERO","sqlState":"22012","messageParameters":{"config":"\"spark.sql.ansi.enabled\""},"queryContext":[{"objectType":"","objectName":"","startIndex":7,"stopIndex":11,"fragment":"1 / "}]}""") + """Error running query: {"errorClass":"DIVIDE_BY_ZERO","sqlState":"22012","messageParameters":{"config":"\"spark.sql.ansi.enabled\""},"queryContext":[{"objectType":"","objectName":"","startIndex":8,"stopIndex":12,"fragment":"1 / "}]}""") exec(s"set ${SQLConf.ERROR_MESSAGE_FORMAT.key}=${ErrorMessageFormat.STANDARD}") val e3 = intercept[HiveSQLException](exec(sql)) @@ -186,8 +186,8 @@ trait ThriftServerWithSparkContextSuite extends SharedThriftServer { | "queryContext" : [ { | "objectType" : "", | "objectName" : "", - | "startIndex" : 7, - | "stopIndex" : 11, + | "startIndex" : 8, + | "stopIndex" : 12, | "fragment" : "1 / " | } ] |}""".stripMargin) From 671e49d6781fad2a07ffa6e79b72848c46f35786 Mon Sep 17 00:00:00 2001 From: Max Gekk Date: Wed, 17 Aug 2022 13:01:24 +0300 Subject: [PATCH 11/16] Print MINIMAL as pretty JSON --- .../apache/spark/SparkThrowableHelper.scala | 2 +- .../org/apache/spark/SparkThrowableSuite.scala | 18 ++++++++++++++---- .../ThriftServerWithSparkContextSuite.scala | 15 ++++++++++++++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala b/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala index 24a7d51b99a70..10ffb8a4fab91 100644 --- a/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala +++ b/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala @@ -161,7 +161,7 @@ private[spark] object SparkThrowableHelper { assert(e.getParameterNames.size == e.getMessageParameters.size, "Number of message parameter names and values must be the same") toJsonString { generator => - val g = if (format == STANDARD) generator.useDefaultPrettyPrinter() else generator + val g = generator.useDefaultPrettyPrinter() g.writeStartObject() g.writeStringField("errorClass", errorClass) val errorSubClass = e.getErrorSubClass diff --git a/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala b/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala index e6642498ae4fc..6effa09fe8981 100644 --- a/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala +++ b/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala @@ -243,12 +243,22 @@ class SparkThrowableSuite extends SparkFunSuite { "[DIVIDE_BY_ZERO] Division by zero. Use `try_divide` to tolerate divisor being 0 " + "and return NULL instead. If necessary set CONFIG to \"false\" to bypass this error." + "\nQuery summary") + // scalastyle:off line.size.limit assert(SparkThrowableHelper.getMessage(e, MINIMAL) === - """{"errorClass":"DIVIDE_BY_ZERO","sqlState":"22012",""" + - """"messageParameters":{"config":"CONFIG"},"queryContext":[{"objectType":"VIEW",""" + - """"objectName":"v1","startIndex":3,"fragment":"1 / 0"}]}""") + """{ + | "errorClass" : "DIVIDE_BY_ZERO", + | "sqlState" : "22012", + | "messageParameters" : { + | "config" : "CONFIG" + | }, + | "queryContext" : [ { + | "objectType" : "VIEW", + | "objectName" : "v1", + | "startIndex" : 3, + | "fragment" : "1 / 0" + | } ] + |}""".stripMargin) assert(SparkThrowableHelper.getMessage(e, STANDARD) === - // scalastyle:off line.size.limit """{ | "errorClass" : "DIVIDE_BY_ZERO", | "message" : "Division by zero. Use `try_divide` to tolerate divisor being 0 and return NULL instead. If necessary set to \"false\" to bypass this error.", diff --git a/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala b/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala index 3ed82cdd094ec..ff5cec9641e18 100644 --- a/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala +++ b/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala @@ -171,7 +171,20 @@ trait ThriftServerWithSparkContextSuite extends SharedThriftServer { exec(s"set ${SQLConf.ERROR_MESSAGE_FORMAT.key}=${ErrorMessageFormat.MINIMAL}") val e2 = intercept[HiveSQLException](exec(sql)) assert(e2.getMessage === - """Error running query: {"errorClass":"DIVIDE_BY_ZERO","sqlState":"22012","messageParameters":{"config":"\"spark.sql.ansi.enabled\""},"queryContext":[{"objectType":"","objectName":"","startIndex":8,"stopIndex":12,"fragment":"1 / "}]}""") + """Error running query: { + | "errorClass" : "DIVIDE_BY_ZERO", + | "sqlState" : "22012", + | "messageParameters" : { + | "config" : "\"spark.sql.ansi.enabled\"" + | }, + | "queryContext" : [ { + | "objectType" : "", + | "objectName" : "", + | "startIndex" : 8, + | "stopIndex" : 12, + | "fragment" : "1 / " + | } ] + |}""".stripMargin) exec(s"set ${SQLConf.ERROR_MESSAGE_FORMAT.key}=${ErrorMessageFormat.STANDARD}") val e3 = intercept[HiveSQLException](exec(sql)) From 3c26416c64a768acecbd7b2b082a317e4babcdd6 Mon Sep 17 00:00:00 2001 From: Maxim Gekk Date: Wed, 17 Aug 2022 19:49:13 +0300 Subject: [PATCH 12/16] Update sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala Co-authored-by: Serge Rielau --- .../main/scala/org/apache/spark/sql/internal/SQLConf.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 f1a904bab191f..569e2227b3daf 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 @@ -3878,8 +3878,8 @@ object SQLConf { val ERROR_MESSAGE_FORMAT = buildConf("spark.sql.error.messageFormat") .doc("When PRETTY, the error message consists of textual representation of error class, " + - " message and query context. The MINIMAL and STANDARD formats are JSON formats where " + - "STANDARD is pretty JSON with additional JSON field `message`.") + " message and query context. The MINIMAL and STANDARD formats are pretty JSON formats where " + + "STANDARD includes an additional JSON field `message`.") .version("3.4.0") .stringConf.transform(_.toUpperCase(Locale.ROOT)) .checkValues(ErrorMessageFormat.values.map(_.toString)) From e89e355fb6aa8df96f4e34cdf9526c82df027181 Mon Sep 17 00:00:00 2001 From: Max Gekk Date: Wed, 17 Aug 2022 20:18:17 +0300 Subject: [PATCH 13/16] legacy -> LEGACY --- core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala | 2 +- core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala | 2 +- .../src/main/scala/org/apache/spark/sql/internal/SQLConf.scala | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala b/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala index 10ffb8a4fab91..1c6c762aecc03 100644 --- a/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala +++ b/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala @@ -148,7 +148,7 @@ private[spark] object SparkThrowableHelper { case MINIMAL | STANDARD if e.getErrorClass == null => toJsonString { g => g.writeStartObject() - g.writeStringField("errorClass", "legacy") + g.writeStringField("errorClass", "LEGACY") g.writeObjectFieldStart("messageParameters") g.writeStringField("message", e.getMessage) g.writeEndObject() diff --git a/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala b/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala index 6effa09fe8981..a55862fb08595 100644 --- a/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala +++ b/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala @@ -281,7 +281,7 @@ class SparkThrowableSuite extends SparkFunSuite { } val e2 = new LegacyException assert(SparkThrowableHelper.getMessage(e2, MINIMAL) === - """{"errorClass":"legacy","messageParameters":{"message":"Test message"},""" + + """{"errorClass":"LEGACY","messageParameters":{"message":"Test message"},""" + """"queryContext":[]}""") } } 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 569e2227b3daf..45f29a3567a9b 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 @@ -3878,7 +3878,7 @@ object SQLConf { val ERROR_MESSAGE_FORMAT = buildConf("spark.sql.error.messageFormat") .doc("When PRETTY, the error message consists of textual representation of error class, " + - " message and query context. The MINIMAL and STANDARD formats are pretty JSON formats where " + + "message and query context. The MINIMAL and STANDARD formats are pretty JSON formats where " + "STANDARD includes an additional JSON field `message`.") .version("3.4.0") .stringConf.transform(_.toUpperCase(Locale.ROOT)) From c871fdd35d1505afd1207afd279bc3aa97137f9b Mon Sep 17 00:00:00 2001 From: Max Gekk Date: Thu, 18 Aug 2022 09:52:40 +0300 Subject: [PATCH 14/16] Address Wenchen's comments --- .../apache/spark/SparkThrowableHelper.scala | 32 ++++++++++--------- .../apache/spark/SparkThrowableSuite.scala | 8 +++-- .../apache/spark/sql/internal/SQLConf.scala | 3 +- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala b/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala index 1c6c762aecc03..dd9f093017df3 100644 --- a/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala +++ b/core/src/main/scala/org/apache/spark/SparkThrowableHelper.scala @@ -146,14 +146,13 @@ private[spark] object SparkThrowableHelper { format match { case PRETTY => e.getMessage case MINIMAL | STANDARD if e.getErrorClass == null => - toJsonString { g => + toJsonString { generator => + val g = generator.useDefaultPrettyPrinter() g.writeStartObject() g.writeStringField("errorClass", "LEGACY") g.writeObjectFieldStart("messageParameters") g.writeStringField("message", e.getMessage) g.writeEndObject() - g.writeArrayFieldStart("queryContext") - g.writeEndArray() g.writeEndObject() } case MINIMAL | STANDARD => @@ -178,19 +177,22 @@ private[spark] object SparkThrowableHelper { g.writeStringField(name, value) } g.writeEndObject() - g.writeArrayFieldStart("queryContext") - e.getQueryContext.map { c => - g.writeStartObject() - g.writeStringField("objectType", c.objectType()) - g.writeStringField("objectName", c.objectName()) - val startIndex = c.startIndex() + 1 - if (startIndex > 0) g.writeNumberField("startIndex", startIndex) - val stopIndex = c.stopIndex() + 1 - if (stopIndex > 0) g.writeNumberField("stopIndex", stopIndex) - g.writeStringField("fragment", c.fragment()) - g.writeEndObject() + val queryContext = e.getQueryContext + if (!queryContext.isEmpty) { + g.writeArrayFieldStart("queryContext") + e.getQueryContext.foreach { c => + g.writeStartObject() + g.writeStringField("objectType", c.objectType()) + g.writeStringField("objectName", c.objectName()) + val startIndex = c.startIndex() + 1 + if (startIndex > 0) g.writeNumberField("startIndex", startIndex) + val stopIndex = c.stopIndex() + 1 + if (stopIndex > 0) g.writeNumberField("stopIndex", stopIndex) + g.writeStringField("fragment", c.fragment()) + g.writeEndObject() + } + g.writeEndArray() } - g.writeEndArray() g.writeEndObject() } } diff --git a/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala b/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala index a55862fb08595..8915f93d2064b 100644 --- a/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala +++ b/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala @@ -281,7 +281,11 @@ class SparkThrowableSuite extends SparkFunSuite { } val e2 = new LegacyException assert(SparkThrowableHelper.getMessage(e2, MINIMAL) === - """{"errorClass":"LEGACY","messageParameters":{"message":"Test message"},""" + - """"queryContext":[]}""") + """{ + | "errorClass" : "LEGACY", + | "messageParameters" : { + | "message" : "Test message" + | } + |}""".stripMargin) } } 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 45f29a3567a9b..2f5ae50b89568 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 @@ -3879,7 +3879,8 @@ object SQLConf { val ERROR_MESSAGE_FORMAT = buildConf("spark.sql.error.messageFormat") .doc("When PRETTY, the error message consists of textual representation of error class, " + "message and query context. The MINIMAL and STANDARD formats are pretty JSON formats where " + - "STANDARD includes an additional JSON field `message`.") + "STANDARD includes an additional JSON field `message`. This configuration property " + + "influences on error messages of Thrift Server while running queries.") .version("3.4.0") .stringConf.transform(_.toUpperCase(Locale.ROOT)) .checkValues(ErrorMessageFormat.values.map(_.toString)) From 9bf3cb403e100226340f8ffb28f781b6f1c3785f Mon Sep 17 00:00:00 2001 From: Max Gekk Date: Thu, 18 Aug 2022 21:57:36 +0300 Subject: [PATCH 15/16] Fix ThriftServerWithSparkContextSuite --- .../hive/thriftserver/ThriftServerWithSparkContextSuite.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala b/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala index ff5cec9641e18..3a38efd27cb8f 100644 --- a/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala +++ b/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala @@ -182,7 +182,7 @@ trait ThriftServerWithSparkContextSuite extends SharedThriftServer { | "objectName" : "", | "startIndex" : 8, | "stopIndex" : 12, - | "fragment" : "1 / " + | "fragment" : "1 / 0" | } ] |}""".stripMargin) @@ -201,7 +201,7 @@ trait ThriftServerWithSparkContextSuite extends SharedThriftServer { | "objectName" : "", | "startIndex" : 8, | "stopIndex" : 12, - | "fragment" : "1 / " + | "fragment" : "1 / 0" | } ] |}""".stripMargin) // scalastyle:on line.size.limit From a5724eaeee9fb1abeaf44de15f9b2320a6a20b71 Mon Sep 17 00:00:00 2001 From: Max Gekk Date: Fri, 19 Aug 2022 08:34:16 +0300 Subject: [PATCH 16/16] Test w/o query context --- .../apache/spark/SparkThrowableSuite.scala | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala b/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala index 8915f93d2064b..9c6175192178b 100644 --- a/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala +++ b/core/src/test/scala/org/apache/spark/SparkThrowableSuite.scala @@ -223,7 +223,7 @@ class SparkThrowableSuite extends SparkFunSuite { } } - test("get message in the specified format") { + test("Get message in the specified format") { import ErrorMessageFormat._ class TestQueryContext extends QueryContext { override val objectName = "v1" @@ -274,13 +274,27 @@ class SparkThrowableSuite extends SparkFunSuite { | } ] |}""".stripMargin) // scalastyle:on line.size.limit + // STANDARD w/ errorSubClass but w/o queryContext + val e2 = new SparkIllegalArgumentException( + errorClass = "UNSUPPORTED_SAVE_MODE", + errorSubClass = Some("EXISTENT_PATH"), + messageParameters = Array("UNSUPPORTED_MODE")) + assert(SparkThrowableHelper.getMessage(e2, STANDARD) === + """{ + | "errorClass" : "UNSUPPORTED_SAVE_MODE", + | "errorSubClass" : "EXISTENT_PATH", + | "message" : "The save mode is not supported for:", + | "messageParameters" : { + | "saveMode" : "UNSUPPORTED_MODE" + | } + |}""".stripMargin) // Legacy mode when an exception does not have any error class class LegacyException extends Throwable with SparkThrowable { override def getErrorClass: String = null override def getMessage: String = "Test message" } - val e2 = new LegacyException - assert(SparkThrowableHelper.getMessage(e2, MINIMAL) === + val e3 = new LegacyException + assert(SparkThrowableHelper.getMessage(e3, MINIMAL) === """{ | "errorClass" : "LEGACY", | "messageParameters" : {