Skip to content

Commit bc7e4f5

Browse files
committed
[SPARK-36953][PYTHON] Expose SQL state and error class in PySpark exceptions
### What changes were proposed in this pull request? This PR proposes to leverage the error message framework by exposing the methods below: - `getErrorClass` - `getSqlState` at captured PySpark SQL exceptions (from `SparkThrowable`). In addition, this PR adds a bit of refactoring. Previously the exception capture was done by string comparison which is flaky. Now, the logic leverages `isInstanceOf` on JVM. ### Why are the changes needed? Users can leverage the error class and SQL state codes by: ```python try: ... except AnalysisException as e: if e.getSqlState().startswith("4"): ... ``` ### Does this PR introduce _any_ user-facing change? Yes, users now can get `getErrorClass` and `getSqlState` from SQL exceptions. ### How was this patch tested? Manually tested, and unittests were added. Closes #34219 from HyukjinKwon/SPARK-36953. Authored-by: Hyukjin Kwon <gurwls223@apache.org> Signed-off-by: Hyukjin Kwon <gurwls223@apache.org>
1 parent e861b0d commit bc7e4f5

2 files changed

Lines changed: 70 additions & 21 deletions

File tree

python/pyspark/sql/tests/test_utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ def test_capture_illegalargument_exception(self):
4848
self.assertRegex(e.desc, "1024 is not in the permitted values")
4949
self.assertRegex(e.stackTrace, "org.apache.spark.sql.functions")
5050

51+
def test_get_error_class_state(self):
52+
# SPARK-36953: test CapturedException.getErrorClass and getSqlState (from SparkThrowable)
53+
try:
54+
self.spark.sql("""SELECT a""")
55+
except AnalysisException as e:
56+
self.assertEquals(e.getErrorClass(), "MISSING_COLUMN")
57+
self.assertEquals(e.getSqlState(), "42000")
58+
5159

5260
if __name__ == "__main__":
5361
import unittest

python/pyspark/sql/utils.py

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,59 @@
1616
#
1717

1818
import py4j
19+
from py4j.java_gateway import is_instance_of
1920

2021
from pyspark import SparkContext
2122

2223

2324
class CapturedException(Exception):
24-
def __init__(self, desc, stackTrace, cause=None):
25-
self.desc = desc
26-
self.stackTrace = stackTrace
25+
def __init__(self, desc=None, stackTrace=None, cause=None, origin=None):
26+
# desc & stackTrace vs origin are mutually exclusive.
27+
# cause is optional.
28+
assert ((origin is not None and desc is None and stackTrace is None)
29+
or (origin is None and desc is not None and stackTrace is not None))
30+
31+
self.desc = desc if desc is not None else origin.getMessage()
32+
self.stackTrace = (
33+
stackTrace if stackTrace is not None
34+
else SparkContext._jvm.org.apache.spark.util.Utils.exceptionString(origin)
35+
)
2736
self.cause = convert_exception(cause) if cause is not None else None
37+
if self.cause is None and origin is not None and origin.getCause() is not None:
38+
self.cause = convert_exception(origin.getCause())
39+
self._origin = origin
2840

2941
def __str__(self):
30-
sql_conf = SparkContext._jvm.org.apache.spark.sql.internal.SQLConf.get()
42+
assert SparkContext._jvm is not None
43+
44+
jvm = SparkContext._jvm
45+
sql_conf = jvm.org.apache.spark.sql.internal.SQLConf.get()
3146
debug_enabled = sql_conf.pysparkJVMStacktraceEnabled()
3247
desc = self.desc
3348
if debug_enabled:
3449
desc = desc + "\n\nJVM stacktrace:\n%s" % self.stackTrace
3550
return str(desc)
3651

52+
def getErrorClass(self):
53+
assert SparkContext._gateway is not None
54+
55+
gw = SparkContext._gateway
56+
if self._origin is not None and is_instance_of(
57+
gw, self._origin, "org.apache.spark.SparkThrowable"):
58+
return self._origin.getErrorClass()
59+
else:
60+
return None
61+
62+
def getSqlState(self):
63+
assert SparkContext._gateway is not None
64+
65+
gw = SparkContext._gateway
66+
if self._origin is not None and is_instance_of(
67+
gw, self._origin, "org.apache.spark.SparkThrowable"):
68+
return self._origin.getSqlState()
69+
else:
70+
return None
71+
3772

3873
class AnalysisException(CapturedException):
3974
"""
@@ -78,31 +113,37 @@ class UnknownException(CapturedException):
78113

79114

80115
def convert_exception(e):
81-
s = e.toString()
116+
assert e is not None
117+
assert SparkContext._jvm is not None
118+
assert SparkContext._gateway is not None
119+
120+
jvm = SparkContext._jvm
121+
gw = SparkContext._gateway
122+
123+
if is_instance_of(gw, e, "org.apache.spark.sql.catalyst.parser.ParseException"):
124+
return ParseException(origin=e)
125+
# Order matters. ParseException inherits AnalysisException.
126+
elif is_instance_of(gw, e, 'org.apache.spark.sql.AnalysisException'):
127+
return AnalysisException(origin=e)
128+
elif is_instance_of(gw, e, 'org.apache.spark.sql.streaming.StreamingQueryException'):
129+
return StreamingQueryException(origin=e)
130+
elif is_instance_of(gw, e, 'org.apache.spark.sql.execution.QueryExecutionException'):
131+
return QueryExecutionException(origin=e)
132+
elif is_instance_of(gw, e, 'java.lang.IllegalArgumentException'):
133+
return IllegalArgumentException(origin=e)
134+
82135
c = e.getCause()
83-
stacktrace = SparkContext._jvm.org.apache.spark.util.Utils.exceptionString(e)
84-
85-
if s.startswith('org.apache.spark.sql.AnalysisException: '):
86-
return AnalysisException(s.split(': ', 1)[1], stacktrace, c)
87-
if s.startswith('org.apache.spark.sql.catalyst.analysis'):
88-
return AnalysisException(s.split(': ', 1)[1], stacktrace, c)
89-
if s.startswith('org.apache.spark.sql.catalyst.parser.ParseException: '):
90-
return ParseException(s.split(': ', 1)[1], stacktrace, c)
91-
if s.startswith('org.apache.spark.sql.streaming.StreamingQueryException: '):
92-
return StreamingQueryException(s.split(': ', 1)[1], stacktrace, c)
93-
if s.startswith('org.apache.spark.sql.execution.QueryExecutionException: '):
94-
return QueryExecutionException(s.split(': ', 1)[1], stacktrace, c)
95-
if s.startswith('java.lang.IllegalArgumentException: '):
96-
return IllegalArgumentException(s.split(': ', 1)[1], stacktrace, c)
136+
stacktrace = jvm.org.apache.spark.util.Utils.exceptionString(e)
97137
if c is not None and (
98-
c.toString().startswith('org.apache.spark.api.python.PythonException: ')
138+
is_instance_of(gw, c, 'org.apache.spark.api.python.PythonException')
99139
# To make sure this only catches Python UDFs.
100140
and any(map(lambda v: "org.apache.spark.sql.execution.python" in v.toString(),
101141
c.getStackTrace()))):
102142
msg = ("\n An exception was thrown from the Python worker. "
103143
"Please see the stack trace below.\n%s" % c.getMessage())
104144
return PythonException(msg, stacktrace)
105-
return UnknownException(s, stacktrace, c)
145+
146+
return UnknownException(desc=e.toString(), stackTrace=stacktrace, cause=c)
106147

107148

108149
def capture_sql_exception(f):

0 commit comments

Comments
 (0)