From a644245e0e3de3c07d57a1c6604f3ffa26b58425 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 23 Dec 2022 15:13:29 +0100 Subject: [PATCH] Check well-known database error codes in case of generic SQL state 23000 Closes gh-29699 --- .../SQLExceptionSubclassTranslator.java | 2 +- .../SQLStateSQLExceptionTranslator.java | 17 ++++++++++++++- .../SQLExceptionSubclassTranslatorTests.java | 3 +++ .../SQLStateSQLExceptionTranslatorTests.java | 21 ++++++++++++++++++- .../connection/ConnectionFactoryUtils.java | 15 ++++++++++++- .../ConnectionFactoryUtilsUnitTests.java | 12 +++++++++++ 6 files changed, 66 insertions(+), 4 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java index 56f227bf1f..058348f7ec 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java @@ -89,7 +89,7 @@ public class SQLExceptionSubclassTranslator extends AbstractFallbackSQLException return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex); } if (ex instanceof SQLIntegrityConstraintViolationException) { - if ("23505".equals(ex.getSQLState())) { + if (SQLStateSQLExceptionTranslator.indicatesDuplicateKey(ex.getSQLState(), ex.getErrorCode())) { return new DuplicateKeyException(buildMessage(task, sql, ex), ex); } return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java index 897a8321e1..18e816f5de 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java @@ -99,7 +99,7 @@ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLException return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex); } else if (DATA_INTEGRITY_VIOLATION_CODES.contains(classCode)) { - if ("23505".equals(sqlState)) { + if (indicatesDuplicateKey(sqlState, ex.getErrorCode())) { return new DuplicateKeyException(buildMessage(task, sql, ex), ex); } return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex); @@ -148,4 +148,19 @@ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLException return sqlState; } + /** + * Check whether the given SQL state (and the associated error code in case + * of a generic SQL state value) indicate a duplicate key exception: + * either SQL state 23505 as a specific indication, or the generic SQL state + * 23000 with well-known vendor codes (1 for Oracle, 1062 for MySQL/MariaDB, + * 2627 for MS SQL Server). + * @param sqlState the SQL state value + * @param errorCode the error code value + */ + static boolean indicatesDuplicateKey(@Nullable String sqlState, int errorCode) { + return ("23505".equals(sqlState) || + ("23000".equals(sqlState) && + (errorCode == 1 || errorCode == 1062 || errorCode == 2627))); + } + } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslatorTests.java index fc403e265a..b589514296 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslatorTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslatorTests.java @@ -57,6 +57,9 @@ public class SQLExceptionSubclassTranslatorTests { doTest(new SQLFeatureNotSupportedException("", "", 0), InvalidDataAccessApiUsageException.class); doTest(new SQLIntegrityConstraintViolationException("", "", 0), DataIntegrityViolationException.class); doTest(new SQLIntegrityConstraintViolationException("", "23505", 0), DuplicateKeyException.class); + doTest(new SQLIntegrityConstraintViolationException("", "23000", 1), DuplicateKeyException.class); + doTest(new SQLIntegrityConstraintViolationException("", "23000", 1062), DuplicateKeyException.class); + doTest(new SQLIntegrityConstraintViolationException("", "23505", 2627), DuplicateKeyException.class); doTest(new SQLInvalidAuthorizationSpecException("", "", 0), PermissionDeniedDataAccessException.class); doTest(new SQLNonTransientConnectionException("", "", 0), DataAccessResourceFailureException.class); doTest(new SQLRecoverableException("", "", 0), RecoverableDataAccessException.class); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java index b667d3fae2..1d4c3b2402 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java @@ -61,6 +61,21 @@ public class SQLStateSQLExceptionTranslatorTests { doTest("23505", DuplicateKeyException.class); } + @Test + public void translateDuplicateKeyOracle() { + doTest("23000", 1, DuplicateKeyException.class); + } + + @Test + public void translateDuplicateKeyMySQL() { + doTest("23000", 1062, DuplicateKeyException.class); + } + + @Test + public void translateDuplicateKeyMSSQL() { + doTest("23000", 2627, DuplicateKeyException.class); + } + @Test public void translateDataAccessResourceFailure() { doTest("53", DataAccessResourceFailureException.class); @@ -105,8 +120,12 @@ public class SQLStateSQLExceptionTranslatorTests { private void doTest(@Nullable String sqlState, @Nullable Class dataAccessExceptionType) { + doTest(sqlState, 0, dataAccessExceptionType); + } + + private void doTest(@Nullable String sqlState, int errorCode, @Nullable Class dataAccessExceptionType) { SQLExceptionTranslator translator = new SQLStateSQLExceptionTranslator(); - SQLException ex = new SQLException("reason", sqlState); + SQLException ex = new SQLException("reason", sqlState, errorCode); DataAccessException dax = translator.translate("task", "SQL", ex); if (dataAccessExceptionType == null) { diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java index 83dde60db7..e34c6db1c0 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java @@ -231,7 +231,7 @@ public abstract class ConnectionFactoryUtils { return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex); } if (ex instanceof R2dbcDataIntegrityViolationException) { - if ("23505".equals(ex.getSqlState())) { + if (indicatesDuplicateKey(ex.getSqlState(), ex.getErrorCode())) { return new DuplicateKeyException(buildMessage(task, sql, ex), ex); } return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex); @@ -246,6 +246,19 @@ public abstract class ConnectionFactoryUtils { return new UncategorizedR2dbcException(buildMessage(task, sql, ex), sql, ex); } + /** + * Check whether the given SQL state (and the associated error code in case + * of a generic SQL state value) indicate a duplicate key exception. See + * {@code org.springframework.jdbc.support.SQLStateSQLExceptionTranslator#indicatesDuplicateKey}. + * @param sqlState the SQL state value + * @param errorCode the error code value + */ + static boolean indicatesDuplicateKey(@Nullable String sqlState, int errorCode) { + return ("23505".equals(sqlState) || + ("23000".equals(sqlState) && + (errorCode == 1 || errorCode == 1062 || errorCode == 2627))); + } + /** * Build a message {@code String} for the given {@link R2dbcException}. *

To be called by translator subclasses when creating an instance of a generic diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/ConnectionFactoryUtilsUnitTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/ConnectionFactoryUtilsUnitTests.java index f8449ab778..caa3b6d4e2 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/ConnectionFactoryUtilsUnitTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/ConnectionFactoryUtilsUnitTests.java @@ -95,6 +95,18 @@ public class ConnectionFactoryUtilsUnitTests { exception = ConnectionFactoryUtils.convertR2dbcException("", "", new R2dbcDataIntegrityViolationException("reason", "23505")); assertThat(exception).isExactlyInstanceOf(DuplicateKeyException.class); + + exception = ConnectionFactoryUtils.convertR2dbcException("", "", + new R2dbcDataIntegrityViolationException("reason", "23000", 1)); + assertThat(exception).isExactlyInstanceOf(DuplicateKeyException.class); + + exception = ConnectionFactoryUtils.convertR2dbcException("", "", + new R2dbcDataIntegrityViolationException("reason", "23000", 1062)); + assertThat(exception).isExactlyInstanceOf(DuplicateKeyException.class); + + exception = ConnectionFactoryUtils.convertR2dbcException("", "", + new R2dbcDataIntegrityViolationException("reason", "23000", 2627)); + assertThat(exception).isExactlyInstanceOf(DuplicateKeyException.class); } @Test