cloud-fan commented on code in PR #59297:
URL: https://github.com/apache/spark/pull/59297#discussion_r4226654853
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/jdbc/JDBCRDD.scala:
##########
@@ -72,6 +72,17 @@ object JDBCRDD extends Logging {
try {
getQueryOutputSchema(fullQuery, options, dialect, conn)
} catch {
+ // This must come before the checks below, because some dialects treat
any class-42
+ // SQLSTATE as object-not-found or as a syntax error.
+ case e: SQLException if dialect.isInsufficientPrivilegeException(e) =>
+ throw new SparkException(
+ errorClass = "JDBC_EXTERNAL_ENGINE_INSUFFICIENT_PRIVILEGE",
+ messageParameters = Map(
+ "jdbcQuery" -> fullQuery,
+ "externalEngineError" -> e.getMessage.replaceAll("\\.+$", ""),
Review Comment:
**Non-blocking (P2):** A positive classifier result does not guarantee that
SQLException.getMessage is non-null. PostgreSQL recognizes SQLSTATE 42501
alone, so a reason-less exception reaches this replaceAll call and throws
NullPointerException instead of JDBC_EXTERNAL_ENGINE_INSUFFICIENT_PRIVILEGE,
masking the original diagnostic. The positive fixtures all carry messages, so
this path currently has no regression signal.
**Verification:**
- **Regression:** Exercise a positive SQLSTATE-only privilege exception with
a null reason through schema resolution and assert the dedicated condition plus
preserved diagnostic fields and cause.
- **Behavior:** Confirm existing non-null vendor errors still trim trailing
periods and report the same query and external SQLSTATE.
##########
sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala:
##########
@@ -3290,6 +3290,86 @@ class JDBCSuite extends SharedSparkSession {
assert(!dialect.isSyntaxErrorBestEffort(new SQLException("Connection
reset", "08001")))
}
+ test("SPARK-60089: dialects classify remote insufficient-privilege errors") {
+ // (dialect, privilege errors, non-privilege errors)
+ val cases: Seq[(JdbcDialect, Seq[SQLException], Seq[SQLException])] = Seq(
+ (PostgresDialect(),
+ Seq(new SQLException("permission denied for table t", "42501")),
+ Seq(new SQLException("relation does not exist", "42P01"),
+ new SQLException("syntax error", "42601"))),
+ (MySQLDialect(),
+ Seq(1044, 1142, 1143, 1227, 1370).map(code => new
SQLException("denied", "42000", code)),
+ Seq(new SQLException("syntax error", "42000", 1064),
+ new SQLException("unknown table", "42S02", 1146))),
+ (MsSqlServerDialect(),
+ Seq(229, 230, 262, 297, 300).map(code => new SQLException("denied",
"S0001", code)),
+ Seq(new SQLException("invalid object name", "S0002", 208),
+ new SQLException("incorrect syntax", "S0001", 102))),
+ (OracleDialect(),
+ Seq(new SQLException("ORA-01031: insufficient privileges", "42000",
1031)),
+ Seq(new SQLException("ORA-00942: table or view does not exist",
"42000", 942),
+ new SQLException(null: String))),
+ (SnowflakeDialect(),
+ Seq(new SQLException("Insufficient privileges to operate on table",
"42501", 3001)),
+ Seq(new SQLException("does not exist or not authorized", "42S02",
2003))),
+ (TeradataDialect(),
+ Seq(3523, 3524, 5315).map(code => new SQLException("no access",
"42000", code)),
+ Seq(new SQLException("Object does not exist", "42S02", 3807))),
+ (DB2Dialect(),
+ Seq(new SQLException("SQL0551N", "42501", -551),
+ new SQLException("SQL0552N", "42502", -552)),
+ Seq(new SQLException("SQL0204N", "42704", -204))),
+ (DatabricksDialect(),
+ Seq(new SQLException("[INSUFFICIENT_PERMISSIONS] Insufficient
privileges", "42501"),
+ new SQLException("[INSUFFICIENT_PERMISSIONS] Insufficient
privileges", "07000")),
+ Seq(new SQLException("[TABLE_OR_VIEW_NOT_FOUND]", "42P01"),
+ new SQLException(null: String))))
+
+ cases.foreach { case (dialect, privilegeErrors, otherErrors) =>
+ privilegeErrors.foreach(e =>
assert(dialect.isInsufficientPrivilegeException(e), s"$dialect"))
+ (otherErrors :+ new SQLException("connection reset", "08001")).foreach {
e =>
+ assert(!dialect.isInsufficientPrivilegeException(e), s"$dialect")
+ }
+ }
+ assert(!H2Dialect().isInsufficientPrivilegeException(new
SQLException("denied", "42501")))
+ }
+
+ test("SPARK-60089: schema resolution surfaces insufficient-privilege
errors") {
+ // Treat H2 "table not found" as a privilege error. Registering this next
to the built-in H2
+ // dialect also covers AggregatedDialect delegation.
+ val testDialect = new JdbcDialect {
+ override def canHandle(url: String): Boolean = url.startsWith("jdbc:h2")
+ override def isInsufficientPrivilegeException(e: SQLException): Boolean =
+ Set(42102, 42103, 42104).contains(e.getErrorCode)
+ }
+ JdbcDialects.registerDialect(testDialect)
+ try {
+ val e = intercept[SparkException] {
+ spark.read.jdbc(urlWithUserAndPass, "TEST.NO_SUCH_TABLE", new
Properties())
Review Comment:
**Non-blocking (P2):** This V1 read reaches resolveTable with ident=None, so
the object-not-found guard is disabled even though the synthetic H2 exception
matches both classifiers. It therefore does not protect the stated catch
precedence on JDBCTableCatalog.loadTable, where ident is defined; moving
privilege handling below object-not-found would break V2 behavior while this
test remained green.
**Verification:**
- **Regression:** Exercise V2 loadTable with an overlapping
privilege/object-not-found exception and assert the privilege condition,
SQLSTATE, and original cause.
##########
sql/core/src/main/scala/org/apache/spark/sql/jdbc/JdbcDialects.scala:
##########
@@ -819,6 +819,13 @@ abstract class JdbcDialect extends Serializable with
Logging {
@Since("4.3.0")
def isNotSelectableObjectException(e: SQLException): Boolean = false
+ /**
+ * Returns true if the given exception indicates that the remote user lacks
a privilege needed to
+ * run the query. Dialects override this to recognize their own error codes;
the default is false.
+ */
+ @Since("4.4.0")
+ def isInsufficientPrivilegeException(e: SQLException): Boolean = false
Review Comment:
**Non-blocking (P2):** JdbcDialect already exposes classifyException for
vendor-specific error mapping, and JdbcUtils wraps schema-loading operations
with it. Adding this second public classifier makes resolveTable consult a
competing path first: for a registered PostgreSQL-compatible dialect that
handles 42501 in classifyException but inherits this new default,
AggregatedDialect can take the built-in positive and throw the new condition
before the custom classifier runs. Please integrate this category with the
established classification contract so custom-first behavior and built-in
fallback have one precedence model.
**Recommended change:** Implement insufficient-privilege mapping through the
existing JdbcDialect.classifyException API, refactor its aggregation to
distinguish unhandled results from definitive custom classifications, and
remove the parallel public Boolean hook. Preserve
privilege-before-object-not-found/syntax behavior at schema resolution while
routing the final mapping through one classification contract.
**Why this works:** Give the established classification path an explicit
internal notion of unhandled versus classified, compose matching dialects in
documented order, and have JDBCRDD/JdbcUtils consume the resulting
SparkThrowable. Built-in privilege mappings become classifyException cases
rather than implementations of a separate predicate.
**Scope:** Add the new category to Spark's established dialect
classification path instead of creating a competing public API.
**Compatibility:** Vendor code coverage, structured error parameters, and
custom-without-opinion built-in fallback remain unchanged.
**Risks:** Refactoring the established DeveloperApi classification path must
preserve source and binary compatibility where required. Incorrect unhandled
detection could mask built-in mappings or override custom classifications.
**Constraints:** An existing custom classifyException result must not be
bypassed by a later built-in dialect. A custom dialect with no relevant
classification must still fall through to the built-in mapping. Privilege
classification must retain precedence over overlapping object-not-found and
syntax classifications during schema resolution.
**Success:** JdbcDialect exposes one authoritative exception-classification
contract to external implementors. A registered custom classifyException result
wins over later built-ins, while an unhandled result falls through. Built-in
insufficient-privilege errors still produce
JDBC_EXTERNAL_ENGINE_INSUFFICIENT_PRIVILEGE during schema resolution.
**Verification:**
- **Compatibility:** Register custom dialects with a definitive
classifyException result and with no relevant result ahead of a matching
built-in, then verify custom-first behavior and built-in fallback.
- **Behavior:** Verify every built-in vendor privilege vector still reaches
the dedicated condition through the unified classification path.
- **Regression:** Verify overlapping privilege, object-not-found, and syntax
signals retain the intended schema-resolution precedence.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]