Skip to content

Commit de4d1c6

Browse files
committed
Always set a sql state in spark connect client
1 parent 3ac4a48 commit de4d1c6

File tree

3 files changed

+93
-13
lines changed

3 files changed

+93
-13
lines changed

common/utils/src/main/resources/error/error-conditions.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,18 @@
870870
},
871871
"sqlState" : "56K00"
872872
},
873+
"CONNECT_CLIENT_INTERNAL_ERROR" : {
874+
"message" : [
875+
"<message>"
876+
],
877+
"sqlState" : "XXKCI"
878+
},
879+
"CONNECT_CLIENT_UNEXPECTED_MISSING_SQL_STATE" : {
880+
"message" : [
881+
"<message>"
882+
],
883+
"sqlState" : "XXKCM"
884+
},
873885
"CONNECT_ML" : {
874886
"message" : [
875887
"Generic Spark Connect ML error."

sql/connect/client/jvm/src/test/scala/org/apache/spark/sql/connect/ClientE2ETestSuite.scala

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1792,6 +1792,46 @@ class ClientE2ETestSuite
17921792
assert(result.length === 1)
17931793
assert(result(0).getAs[Array[Integer]]("arr_col") === Array(1, null))
17941794
}
1795+
1796+
test("SPARK-53883: GrpcExceptionConverter ensures all exceptions have errorClass and sqlState") {
1797+
// Test that normal Spark errors have proper errorClass and sqlState
1798+
val analysisEx = intercept[AnalysisException] {
1799+
spark.sql("select nonexistent_column").collect()
1800+
}
1801+
assert(analysisEx.getErrorClass != null,
1802+
"AnalysisException should have non-null errorClass")
1803+
assert(analysisEx.getSqlState != null,
1804+
"AnalysisException should have non-null sqlState")
1805+
assert(analysisEx.getErrorClass == "UNRESOLVED_COLUMN.WITHOUT_SUGGESTION",
1806+
s"AnalysisException should have correct errorClass, got: ${analysisEx.getErrorClass}")
1807+
1808+
val parseEx = intercept[ParseException] {
1809+
spark.sql("SELECT !!!").collect()
1810+
}
1811+
assert(parseEx.getErrorClass != null,
1812+
"ParseException should have non-null errorClass")
1813+
assert(parseEx.getSqlState != null,
1814+
"ParseException should have non-null sqlState")
1815+
assert(parseEx.getErrorClass.startsWith("PARSE_SYNTAX_ERROR"),
1816+
s"ParseException should have correct errorClass, got: ${parseEx.getErrorClass}")
1817+
1818+
// Test that runtime errors have proper errorClass and sqlState
1819+
val runtimeEx = intercept[SparkException] {
1820+
spark.udf.register("badUdf", () => { throw new RuntimeException("test error") })
1821+
spark.sql("SELECT badUdf()").collect()
1822+
}
1823+
assert(runtimeEx.getErrorClass != null,
1824+
"Runtime SparkException should have non-null errorClass")
1825+
assert(runtimeEx.getSqlState != null,
1826+
"Runtime SparkException should have non-null sqlState")
1827+
1828+
// Verify that the error classes we added exist in error-conditions.json
1829+
// by checking they would be valid if used
1830+
assert(analysisEx.getSqlState != "XXKCI",
1831+
"Normal errors should not use CONNECT_CLIENT_INTERNAL_ERROR sqlState")
1832+
assert(analysisEx.getSqlState != "XXKCM",
1833+
"Normal errors should not use CONNECT_CLIENT_UNEXPECTED_MISSING_SQL_STATE sqlState")
1834+
}
17951835
}
17961836

17971837
private[sql] case class ClassData(a: String, b: Int)

sql/connect/common/src/main/scala/org/apache/spark/sql/connect/client/GrpcExceptionConverter.scala

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import java.time.DateTimeException
2020

2121
import scala.jdk.CollectionConverters._
2222
import scala.reflect.ClassTag
23+
import scala.util.control.NonFatal
2324

2425
import com.google.rpc.ErrorInfo
2526
import io.grpc.{ManagedChannel, StatusRuntimeException}
@@ -59,6 +60,11 @@ private[client] class GrpcExceptionConverter(channel: ManagedChannel) extends Lo
5960
} catch {
6061
case e: StatusRuntimeException =>
6162
throw toThrowable(e, sessionId, userContext, clientType)
63+
case NonFatal(e) =>
64+
throw new SparkException(
65+
errorClass = "CONNECT_CLIENT_INTERNAL_ERROR",
66+
messageParameters = Map("message" -> e.toString),
67+
cause = e)
6268
}
6369
}
6470

@@ -139,6 +145,22 @@ private[client] class GrpcExceptionConverter(channel: ManagedChannel) extends Lo
139145
clientType: String): Throwable = {
140146
val status = StatusProto.fromThrowable(ex)
141147

148+
if (status == null) {
149+
val statusCode = ex.getStatus.getCode
150+
val (errorClass, sqlState) = statusCode match {
151+
case io.grpc.Status.Code.PERMISSION_DENIED =>
152+
("INSUFFICIENT_PERMISSIONS", "42501")
153+
case io.grpc.Status.Code.UNAUTHENTICATED =>
154+
("UNAUTHENTICATED", "08000")
155+
case _ =>
156+
("CONNECT_CLIENT_INTERNAL_ERROR", "XXKCI")
157+
}
158+
return new SparkException(
159+
errorClass = errorClass,
160+
messageParameters = Map("message" -> ex.toString),
161+
cause = ex)
162+
}
163+
142164
// Extract the ErrorInfo from the StatusProto, if present.
143165
val errorInfoOpt = status.getDetailsList.asScala
144166
.find(_.is(classOf[ErrorInfo]))
@@ -159,7 +181,10 @@ private[client] class GrpcExceptionConverter(channel: ManagedChannel) extends Lo
159181
}
160182

161183
// If no ErrorInfo is found, create a SparkException based on the StatusRuntimeException.
162-
new SparkException(ex.toString, ex.getCause)
184+
new SparkException(
185+
errorClass = "CONNECT_CLIENT_UNEXPECTED_MISSING_SQL_STATE",
186+
messageParameters = Map("message" -> ex.toString),
187+
cause = ex.getCause)
163188
}
164189
}
165190

@@ -168,7 +193,6 @@ private[client] object GrpcExceptionConverter {
168193
private[client] case class ErrorParams(
169194
message: String,
170195
cause: Option[Throwable],
171-
// errorClass will only be set if the error is SparkThrowable.
172196
errorClass: Option[String],
173197
// messageParameters will only be set if the error is both enriched and SparkThrowable.
174198
messageParameters: Map[String, String],
@@ -186,13 +210,13 @@ private[client] object GrpcExceptionConverter {
186210
new StreamingQueryException(
187211
params.message,
188212
params.cause.orNull,
189-
params.errorClass.orNull,
213+
params.errorClass.getOrElse("CONNECT_CLIENT_UNEXPECTED_MISSING_SQL_STATE"),
190214
params.messageParameters)),
191215
errorConstructor(params =>
192216
new ParseException(
193217
None,
194218
Origin(),
195-
errorClass = params.errorClass.orNull,
219+
errorClass = params.errorClass.getOrElse("CONNECT_CLIENT_UNEXPECTED_MISSING_SQL_STATE"),
196220
messageParameters = params.messageParameters,
197221
queryContext = params.queryContext)),
198222
errorConstructor(params =>
@@ -202,26 +226,29 @@ private[client] object GrpcExceptionConverter {
202226
cause = params.cause,
203227
context = params.queryContext)),
204228
errorConstructor(params =>
205-
new NamespaceAlreadyExistsException(params.errorClass.orNull, params.messageParameters)),
229+
new NamespaceAlreadyExistsException(params.errorClass.getOrElse(
230+
"CONNECT_CLIENT_UNEXPECTED_MISSING_SQL_STATE"), params.messageParameters)),
206231
errorConstructor(params =>
207232
new TableAlreadyExistsException(
208-
params.errorClass.orNull,
233+
params.errorClass.getOrElse("CONNECT_CLIENT_UNEXPECTED_MISSING_SQL_STATE"),
209234
params.messageParameters,
210235
params.cause)),
211236
errorConstructor(params =>
212237
new TempTableAlreadyExistsException(
213-
params.errorClass.orNull,
238+
params.errorClass.getOrElse("CONNECT_CLIENT_UNEXPECTED_MISSING_SQL_STATE"),
214239
params.messageParameters,
215240
params.cause)),
216241
errorConstructor(params =>
217242
new NoSuchDatabaseException(
218-
params.errorClass.orNull,
243+
params.errorClass.getOrElse("CONNECT_CLIENT_UNEXPECTED_MISSING_SQL_STATE"),
219244
params.messageParameters,
220245
params.cause)),
221246
errorConstructor(params =>
222-
new NoSuchNamespaceException(params.errorClass.orNull, params.messageParameters)),
247+
new NoSuchNamespaceException(params.errorClass.getOrElse(
248+
"CONNECT_CLIENT_UNEXPECTED_MISSING_SQL_STATE"), params.messageParameters)),
223249
errorConstructor(params =>
224-
new NoSuchTableException(params.errorClass.orNull, params.messageParameters, params.cause)),
250+
new NoSuchTableException(params.errorClass.getOrElse(
251+
"CONNECT_CLIENT_UNEXPECTED_MISSING_SQL_STATE"), params.messageParameters, params.cause)),
225252
errorConstructor[NumberFormatException](params =>
226253
new SparkNumberFormatException(
227254
errorClass = params.errorClass.getOrElse("_LEGACY_ERROR_TEMP_3104"),
@@ -255,20 +282,21 @@ private[client] object GrpcExceptionConverter {
255282
params.queryContext)),
256283
errorConstructor(params =>
257284
new SparkRuntimeException(
258-
params.errorClass.orNull,
285+
params.errorClass.getOrElse("CONNECT_CLIENT_UNEXPECTED_MISSING_SQL_STATE"),
259286
params.messageParameters,
260287
params.cause.orNull,
261288
params.queryContext)),
262289
errorConstructor(params =>
263290
new SparkUpgradeException(
264-
params.errorClass.orNull,
291+
params.errorClass.getOrElse("CONNECT_CLIENT_UNEXPECTED_MISSING_SQL_STATE"),
265292
params.messageParameters,
266293
params.cause.orNull)),
267294
errorConstructor(params =>
268295
new SparkException(
269296
message = params.message,
270297
cause = params.cause.orNull,
271-
errorClass = params.errorClass,
298+
errorClass = params.errorClass.orElse(
299+
Option("CONNECT_CLIENT_UNEXPECTED_MISSING_SQL_STATE")),
272300
messageParameters = params.messageParameters,
273301
context = params.queryContext)))
274302

0 commit comments

Comments
 (0)