diff --git a/sql/core/src/main/scala/org/apache/spark/sql/jdbc/PostgresDialect.scala b/sql/core/src/main/scala/org/apache/spark/sql/jdbc/PostgresDialect.scala index 8941767ec3573..4391cdb8fbc73 100644 --- a/sql/core/src/main/scala/org/apache/spark/sql/jdbc/PostgresDialect.scala +++ b/sql/core/src/main/scala/org/apache/spark/sql/jdbc/PostgresDialect.scala @@ -259,8 +259,22 @@ private case class PostgresDialect() } // See https://www.postgresql.org/docs/current/errcodes-appendix.html + // Class 42 is "Syntax Error or Access Rule Violation", so it bundles genuine syntax errors + // together with authorization failures. 42501 is the insufficient_privilege state (e.g. + // "permission denied for table ..."), an access rule violation, not a syntax error. Additionally + // guard by message: some Postgres-compatible engines surface permission errors under the generic + // 42000 state, and access-control failures ("permission denied", "access denied", "unauthorized") + // must never be wrapped as JDBC_EXTERNAL_ENGINE_SYNTAX_ERROR. override def isSyntaxErrorBestEffort(exception: SQLException): Boolean = { - Option(exception.getSQLState).exists(_.startsWith("42")) + lazy val isAccessControlError = Option(exception.getMessage) + .map(_.toLowerCase(Locale.ROOT)) + .exists { message => + message.contains("permission denied") || + message.contains("access denied") || + message.contains("unauthorized") + } + Option(exception.getSQLState).exists(s => s.startsWith("42") && s != "42501") && + !isAccessControlError } // SHOW INDEX syntax diff --git a/sql/core/src/test/scala/org/apache/spark/sql/jdbc/PostgresDialectSuite.scala b/sql/core/src/test/scala/org/apache/spark/sql/jdbc/PostgresDialectSuite.scala index df20588e7062b..5a4ea0b14e5c8 100644 --- a/sql/core/src/test/scala/org/apache/spark/sql/jdbc/PostgresDialectSuite.scala +++ b/sql/core/src/test/scala/org/apache/spark/sql/jdbc/PostgresDialectSuite.scala @@ -18,7 +18,7 @@ package org.apache.spark.sql.jdbc -import java.sql.Connection +import java.sql.{Connection, SQLException} import org.mockito.Mockito._ import org.scalatestplus.mockito.MockitoSugar @@ -78,4 +78,34 @@ class PostgresDialectSuite extends SparkFunSuite with MockitoSugar { dialect.beforeFetch(conn, createJDBCOptions(Map.empty)) verify(conn).setAutoCommit(false) } + + test("isSyntaxErrorBestEffort: genuine syntax error (42601) is classified as a syntax error") { + assert(dialect.isSyntaxErrorBestEffort( + new SQLException("ERROR: syntax error at or near \"FROM\"", "42601"))) + } + + // Cases that must NOT be classified as syntax errors because they are access-control failures, + // regardless of whether the engine reports the precise 42501 or the generic 42000 state. + private case class AccessControlErrorCase(name: String, sqlState: String, message: String) { + override def toString: String = name + } + + gridTest("isSyntaxErrorBestEffort: access-control failures are not syntax errors")(Seq( + AccessControlErrorCase( + "42501 insufficient_privilege", "42501", "ERROR: permission denied for table foo"), + AccessControlErrorCase( + "42000 permission denied", "42000", "ERROR: permission denied for table pg_statistic"), + AccessControlErrorCase("42000 access denied", "42000", "ERROR: access denied for relation foo"), + AccessControlErrorCase("42000 unauthorized", "42000", "ERROR: unauthorized") + )) { c => + assert(!dialect.isSyntaxErrorBestEffort(new SQLException(c.message, c.sqlState))) + } + + test("isSyntaxErrorBestEffort: non-class-42 SQLState is not a syntax error") { + assert(!dialect.isSyntaxErrorBestEffort(new SQLException("ERROR: some failure", "28000"))) + } + + test("isSyntaxErrorBestEffort: null SQLState is not a syntax error") { + assert(!dialect.isSyntaxErrorBestEffort(new SQLException("ERROR: connection lost"))) + } }