-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-57780][SQL] Do not classify Postgres permission-denied as a syntax error #56899
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When we get permission error, we should report table not found as well. Can you take a look on method |
||
| .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") && | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The base contract (JdbcDialect Scaladoc) is "if this method returns true, the exception is guaranteed to be a syntax error." This denylist ( It's not purely cosmetic: in For a genuine "true ⟹ syntax" guarantee, consider allowlisting the real syntax states (e.g. |
||
| !isAccessControlError | ||
| } | ||
|
|
||
| // SHOW INDEX syntax | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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") { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we have docker instance here, can we create new user and do integration testing as well |
||
| 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"))) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Too verbose, can we make this more concise, e.g.