From 7d749b297373c0ecbf69316b5a67669f233f0e40 Mon Sep 17 00:00:00 2001 From: alekjarmov Date: Mon, 3 Aug 2026 15:19:31 +0200 Subject: [PATCH 1/2] [SQL] Exclude MySQL permission and limit errors from syntax error classification MySQL reports permission-denied and resource-limit failures with SQLState 42000, the same state used for syntax errors. Narrow isSyntaxErrorBestEffort so those cases are not treated as syntax errors. --- .../apache/spark/sql/jdbc/MySQLDialect.scala | 20 +++++++++- .../org/apache/spark/sql/jdbc/JDBCSuite.scala | 38 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/sql/core/src/main/scala/org/apache/spark/sql/jdbc/MySQLDialect.scala b/sql/core/src/main/scala/org/apache/spark/sql/jdbc/MySQLDialect.scala index 60cce5babe4c8..d381278793125 100644 --- a/sql/core/src/main/scala/org/apache/spark/sql/jdbc/MySQLDialect.scala +++ b/sql/core/src/main/scala/org/apache/spark/sql/jdbc/MySQLDialect.scala @@ -221,7 +221,25 @@ private case class MySQLDialect() extends JdbcDialect with SQLConfHelper with No // See https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html override def isSyntaxErrorBestEffort(exception: SQLException): Boolean = { - "42000".equals(exception.getSQLState) + "42000".equals(exception.getSQLState) && + !isPermissionDeniedErrorBestEffort(exception) + } + + // See https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html + private def isPermissionDeniedErrorBestEffort(exception: SQLException): Boolean = { + val permissionSubstrings = Set( + "command denied to user", + "access denied", + "must have privileges" + ) + val nonSyntaxErrorCodes = Set( + 1148, // ER_NOT_ALLOWED_COMMAND + 1203, // ER_TOO_MANY_USER_CONNECTIONS + 1226 // ER_USER_LIMIT_REACHED + ) + "42000".equals(exception.getSQLState) && + (nonSyntaxErrorCodes.contains(exception.getErrorCode) || + permissionSubstrings.exists(exception.getMessage.toLowerCase(Locale.ROOT).contains)) } // See https://dev.mysql.com/doc/refman/8.0/en/alter-table.html diff --git a/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala b/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala index b654deb12ad4e..752d8976634a2 100644 --- a/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala +++ b/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala @@ -2746,6 +2746,44 @@ class JDBCSuite extends SharedSparkSession { assert(!dialect.isSyntaxErrorBestEffort(new SQLException("Connection reset", "08001"))) } + test("MySQLDialect syntax error detection") { + val dialect = MySQLDialect() + assert(dialect.isSyntaxErrorBestEffort( + new SQLException( + "You have an error in your SQL syntax; check the manual that corresponds to your " + + "MySQL server version for the right syntax to use near 'SELCT' at line 1", + "42000"))) + assert(!dialect.isSyntaxErrorBestEffort( + new SQLException( + "execute command denied to user 'testuser'@'%' for table 't1'", + "42000"))) + assert(!dialect.isSyntaxErrorBestEffort( + new SQLException( + "You must have privileges to update tables in the mysql database to be able to " + + "change passwords for others", + "42000"))) + assert(!dialect.isSyntaxErrorBestEffort( + new SQLException( + "Access denied for user 'testuser'@'%' to database 'testdb'", + "42000"))) + assert(!dialect.isSyntaxErrorBestEffort( + new SQLException( + "User testuser already has more than 'max_user_connections' active connections", + "42000", + 1203))) + assert(!dialect.isSyntaxErrorBestEffort( + new SQLException( + "User 'testuser' has exceeded the 'max_queries_per_hour' resource (current value: 1000)", + "42000", + 1226))) + assert(!dialect.isSyntaxErrorBestEffort( + new SQLException( + "The used command is not allowed with this MySQL version", + "42000", + 1148))) + assert(!dialect.isSyntaxErrorBestEffort(new SQLException("Connection reset", "08001"))) + } + test("SPARK-45425: Mapped TINYINT to ShortType for MsSqlServerDialect") { val msSqlServerDialect = JdbcDialects.get("jdbc:sqlserver") val metadata = new MetadataBuilder().putLong("scale", 1) From 5281cf6ee793d8e7497dd6fbb9b1fb5446a0fe81 Mon Sep 17 00:00:00 2001 From: alekjarmov Date: Mon, 3 Aug 2026 15:34:10 +0200 Subject: [PATCH 2/2] [SQL] Harden MySQL non-syntax 42000 classification Prefer vendor error codes for access and limit failures, keep English message matching as a fallback, and null-safe the message check. --- .../apache/spark/sql/jdbc/MySQLDialect.scala | 32 ++++++++---- .../org/apache/spark/sql/jdbc/JDBCSuite.scala | 50 +++++++++++++++---- 2 files changed, 61 insertions(+), 21 deletions(-) diff --git a/sql/core/src/main/scala/org/apache/spark/sql/jdbc/MySQLDialect.scala b/sql/core/src/main/scala/org/apache/spark/sql/jdbc/MySQLDialect.scala index d381278793125..78bfd2da2982b 100644 --- a/sql/core/src/main/scala/org/apache/spark/sql/jdbc/MySQLDialect.scala +++ b/sql/core/src/main/scala/org/apache/spark/sql/jdbc/MySQLDialect.scala @@ -222,24 +222,34 @@ private case class MySQLDialect() extends JdbcDialect with SQLConfHelper with No // See https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html override def isSyntaxErrorBestEffort(exception: SQLException): Boolean = { "42000".equals(exception.getSQLState) && - !isPermissionDeniedErrorBestEffort(exception) + !isNonSyntaxErrorBestEffort(exception) } // See https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html - private def isPermissionDeniedErrorBestEffort(exception: SQLException): Boolean = { - val permissionSubstrings = Set( - "command denied to user", - "access denied", - "must have privileges" - ) + // MySQL uses SQLSTATE 42000 for both syntax errors and access/limit failures. + private def isNonSyntaxErrorBestEffort(exception: SQLException): Boolean = { val nonSyntaxErrorCodes = Set( + 1044, // ER_DBACCESS_DENIED_ERROR + 1142, // ER_TABLEACCESS_DENIED_ERROR + 1143, // ER_COLUMNACCESS_DENIED_ERROR 1148, // ER_NOT_ALLOWED_COMMAND 1203, // ER_TOO_MANY_USER_CONNECTIONS - 1226 // ER_USER_LIMIT_REACHED + 1226, // ER_USER_LIMIT_REACHED + 1227, // ER_SPECIFIC_ACCESS_DENIED_ERROR + 1370 // ER_PROCACCESS_DENIED_ERROR ) - "42000".equals(exception.getSQLState) && - (nonSyntaxErrorCodes.contains(exception.getErrorCode) || - permissionSubstrings.exists(exception.getMessage.toLowerCase(Locale.ROOT).contains)) + // Message matching is a best-effort fallback for drivers that omit error codes. + // Prefer error codes above; MySQL messages may be localized. + val nonSyntaxSubstrings = Set( + "command denied to user", + "access denied", + "must have privileges" + ) + val message = Option(exception.getMessage) + .map(_.toLowerCase(Locale.ROOT)) + .getOrElse("") + nonSyntaxErrorCodes.contains(exception.getErrorCode) || + nonSyntaxSubstrings.exists(message.contains) } // See https://dev.mysql.com/doc/refman/8.0/en/alter-table.html diff --git a/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala b/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala index 752d8976634a2..4cbf54d8cf9cd 100644 --- a/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala +++ b/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala @@ -2752,20 +2752,29 @@ class JDBCSuite extends SharedSparkSession { new SQLException( "You have an error in your SQL syntax; check the manual that corresponds to your " + "MySQL server version for the right syntax to use near 'SELCT' at line 1", - "42000"))) + "42000", + 1064))) + // Permission / limit failures share SQLSTATE 42000; detect by error code. assert(!dialect.isSyntaxErrorBestEffort( new SQLException( - "execute command denied to user 'testuser'@'%' for table 't1'", - "42000"))) + "Access denied for user 'testuser'@'%' to database 'testdb'", + "42000", + 1044))) assert(!dialect.isSyntaxErrorBestEffort( new SQLException( - "You must have privileges to update tables in the mysql database to be able to " + - "change passwords for others", - "42000"))) + "SELECT command denied to user 'testuser'@'%' for table 't1'", + "42000", + 1142))) assert(!dialect.isSyntaxErrorBestEffort( new SQLException( - "Access denied for user 'testuser'@'%' to database 'testdb'", - "42000"))) + "SELECT command denied to user 'testuser'@'%' for column 'c1' in table 't1'", + "42000", + 1143))) + assert(!dialect.isSyntaxErrorBestEffort( + new SQLException( + "The used command is not allowed with this MySQL version", + "42000", + 1148))) assert(!dialect.isSyntaxErrorBestEffort( new SQLException( "User testuser already has more than 'max_user_connections' active connections", @@ -2778,9 +2787,30 @@ class JDBCSuite extends SharedSparkSession { 1226))) assert(!dialect.isSyntaxErrorBestEffort( new SQLException( - "The used command is not allowed with this MySQL version", + "Access denied; you need (at least one of) the SUPER privilege(s) for this operation", "42000", - 1148))) + 1227))) + assert(!dialect.isSyntaxErrorBestEffort( + new SQLException( + "execute command denied to user 'testuser'@'%' for routine 'p1'", + "42000", + 1370))) + // Best-effort message fallback when the vendor error code is absent. + assert(!dialect.isSyntaxErrorBestEffort( + new SQLException( + "execute command denied to user 'testuser'@'%' for table 't1'", + "42000"))) + assert(!dialect.isSyntaxErrorBestEffort( + new SQLException( + "You must have privileges to update tables in the mysql database to be able to " + + "change passwords for others", + "42000"))) + assert(!dialect.isSyntaxErrorBestEffort( + new SQLException( + "Access denied for user 'testuser'@'%' to database 'testdb'", + "42000"))) + assert(!dialect.isSyntaxErrorBestEffort( + new SQLException(null, "42000", 1142))) assert(!dialect.isSyntaxErrorBestEffort(new SQLException("Connection reset", "08001"))) }