From e35dfdbe04e6cd01422c3a2b7b8bbb925e61b6ae Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Wed, 22 Feb 2023 15:31:39 +0100 Subject: [PATCH] [jOOQ/jOOQ#14675] Add a DataException for SQLState 22 and a IntegrityConstraintViolationException for SQLState 23 --- .../jooq/exception/DataAccessException.java | 46 +++++++++-- .../org/jooq/exception/DataException.java | 76 +++++++++++++++++++ .../org/jooq/exception/ExceptionTools.java | 3 + ...IntegrityConstraintViolationException.java | 74 ++++++++++++++++++ .../org/jooq/exception/SQLStateClass.java | 5 ++ .../org/jooq/exception/SQLStateSubclass.java | 7 ++ jOOQ/src/main/java/org/jooq/impl/Tools.java | 19 ++++- 7 files changed, 220 insertions(+), 10 deletions(-) create mode 100644 jOOQ/src/main/java/org/jooq/exception/DataException.java create mode 100644 jOOQ/src/main/java/org/jooq/exception/IntegrityConstraintViolationException.java diff --git a/jOOQ/src/main/java/org/jooq/exception/DataAccessException.java b/jOOQ/src/main/java/org/jooq/exception/DataAccessException.java index 50362a7f75..0bf2d085ca 100644 --- a/jOOQ/src/main/java/org/jooq/exception/DataAccessException.java +++ b/jOOQ/src/main/java/org/jooq/exception/DataAccessException.java @@ -37,8 +37,13 @@ */ package org.jooq.exception; +import static org.jooq.tools.StringUtils.defaultIfNull; + import java.sql.SQLException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import io.r2dbc.spi.R2dbcException; /** @@ -78,14 +83,15 @@ public class DataAccessException extends RuntimeException { * DataAccessException was caused by a {@link SQLException} or * {@link R2dbcException}. */ + @NotNull public String sqlState() { SQLException s = getCause(SQLException.class); if (s != null) - return s.getSQLState(); + return defaultIfNull(s.getSQLState(), "00000"); R2dbcException r = getCause(R2dbcException.class); if (r != null) - return r.getSqlState(); + return defaultIfNull(r.getSqlState(), "00000"); return "00000"; } @@ -96,28 +102,51 @@ public class DataAccessException extends RuntimeException { * {@link SQLStateClass}, if this DataAccessException was * caused by a {@link SQLException} or {@link R2dbcException}. */ + @NotNull public SQLStateClass sqlStateClass() { SQLException s = getCause(SQLException.class); if (s != null) - if (s.getSQLState() != null) - return SQLStateClass.fromCode(s.getSQLState()); - else if (s.getSQLState() == null && "org.sqlite.SQLiteException".equals(s.getClass().getName())) - return SQLStateClass.fromSQLiteVendorCode(s.getErrorCode()); + return sqlStateClass(s); R2dbcException r = getCause(R2dbcException.class); if (r != null) - if (r.getSqlState() != null) - return SQLStateClass.fromCode(r.getSqlState()); + return sqlStateClass(r); return SQLStateClass.NONE; } + /** + * Decode the {@link SQLException#getSQLState()} into {@link SQLStateClass}. + */ + @NotNull + public static SQLStateClass sqlStateClass(SQLException e) { + if (e.getSQLState() != null) + return SQLStateClass.fromCode(e.getSQLState()); + else if (e.getSQLState() == null && "org.sqlite.SQLiteException".equals(e.getClass().getName())) + return SQLStateClass.fromSQLiteVendorCode(e.getErrorCode()); + else + return SQLStateClass.NONE; + } + + /** + * Decode the {@link R2dbcException#getSqlState()} into + * {@link SQLStateClass}. + */ + @NotNull + public static SQLStateClass sqlStateClass(R2dbcException e) { + if (e.getSqlState() != null) + return SQLStateClass.fromCode(e.getSqlState()); + else + return SQLStateClass.NONE; + } + /** * Decode the {@link SQLException#getSQLState()} or * {@link R2dbcException#getSqlState()} from {@link #getCause()} into * {@link SQLStateSubclass}, if this DataAccessException was * caused by a {@link SQLException} or {@link R2dbcException}. */ + @NotNull public SQLStateSubclass sqlStateSubclass() { return SQLStateSubclass.fromCode(sqlState()); } @@ -131,6 +160,7 @@ public class DataAccessException extends RuntimeException { * Find a root cause of a given type, or null if no root cause * of that type was found. */ + @Nullable public T getCause(Class type) { return ExceptionTools.getCause(this, type); } diff --git a/jOOQ/src/main/java/org/jooq/exception/DataException.java b/jOOQ/src/main/java/org/jooq/exception/DataException.java new file mode 100644 index 0000000000..407f027433 --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/exception/DataException.java @@ -0,0 +1,76 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Other licenses: + * ----------------------------------------------------------------------------- + * Commercial licenses for this work are available. These replace the above + * ASL 2.0 and offer limited warranties, support, maintenance, and commercial + * database integrations. + * + * For more information, please visit: https://www.jooq.org/legal/licensing + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ +package org.jooq.exception; + +import java.sql.SQLDataException; +import java.sql.SQLException; + +import io.r2dbc.spi.R2dbcException; + +/** + * The DataException is jOOQ's equivalent of JDBC's + * {@link SQLDataException}. + *

+ * It is thrown by jOOQ whenever jOOQ detects + * {@link SQLStateClass#C22_DATA_EXCEPTION} from the JDBC driver. Whether this + * SQL state is available is JDBC driver implementation specific. + * + * @author Lukas Eder + */ +public class DataException extends DataAccessException { + + /** + * Constructor for DataException. + * + * @param message the detail message + */ + public DataException(String message) { + super(message); + } + + /** + * Constructor for DataException. + * + * @param message the detail message + * @param cause the root cause (usually from using a underlying data access + * API such as JDBC) + */ + public DataException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jOOQ/src/main/java/org/jooq/exception/ExceptionTools.java b/jOOQ/src/main/java/org/jooq/exception/ExceptionTools.java index be76006294..5358750246 100644 --- a/jOOQ/src/main/java/org/jooq/exception/ExceptionTools.java +++ b/jOOQ/src/main/java/org/jooq/exception/ExceptionTools.java @@ -37,6 +37,8 @@ */ package org.jooq.exception; +import org.jetbrains.annotations.Nullable; + /** * @author Lukas Eder */ @@ -52,6 +54,7 @@ public final class ExceptionTools { * of that type was found. */ @SuppressWarnings("unchecked") + @Nullable public static T getCause(Throwable t, Class type) { Throwable next = t.getCause(); Throwable prev; diff --git a/jOOQ/src/main/java/org/jooq/exception/IntegrityConstraintViolationException.java b/jOOQ/src/main/java/org/jooq/exception/IntegrityConstraintViolationException.java new file mode 100644 index 0000000000..37d9ea1c9a --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/exception/IntegrityConstraintViolationException.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Other licenses: + * ----------------------------------------------------------------------------- + * Commercial licenses for this work are available. These replace the above + * ASL 2.0 and offer limited warranties, support, maintenance, and commercial + * database integrations. + * + * For more information, please visit: https://www.jooq.org/legal/licensing + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ +package org.jooq.exception; + +import java.sql.SQLIntegrityConstraintViolationException; + +/** + * The IntegrityConstraintViolationException is jOOQ's equivalent + * of JDBC's {@link SQLIntegrityConstraintViolationException}. + *

+ * It is thrown by jOOQ whenever jOOQ detects + * {@link SQLStateClass#C23_INTEGRITY_CONSTRAINT_VIOLATION} from the JDBC + * driver. Whether this SQL state is available is JDBC driver implementation + * specific. + * + * @author Lukas Eder + */ +public class IntegrityConstraintViolationException extends DataAccessException { + + /** + * Constructor for IntegrityConstraintViolationException. + * + * @param message the detail message + */ + public IntegrityConstraintViolationException(String message) { + super(message); + } + + /** + * Constructor for IntegrityConstraintViolationException. + * + * @param message the detail message + * @param cause the root cause (usually from using a underlying data access + * API such as JDBC) + */ + public IntegrityConstraintViolationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jOOQ/src/main/java/org/jooq/exception/SQLStateClass.java b/jOOQ/src/main/java/org/jooq/exception/SQLStateClass.java index dd9929ee87..9f73b66d03 100644 --- a/jOOQ/src/main/java/org/jooq/exception/SQLStateClass.java +++ b/jOOQ/src/main/java/org/jooq/exception/SQLStateClass.java @@ -40,6 +40,8 @@ package org.jooq.exception; import java.util.HashMap; import java.util.Map; +import org.jetbrains.annotations.NotNull; + /** * The class of the SQL state as specified by the SQL:2011 standard, or by individual * vendors. @@ -165,10 +167,12 @@ public enum SQLStateClass { this.className = className; } + @NotNull public String className() { return className; } + @NotNull public static SQLStateClass fromCode(String code) { if (code == null || code.length() < 2) return SQLStateClass.OTHER; @@ -177,6 +181,7 @@ public enum SQLStateClass { return result != null ? result : SQLStateClass.OTHER; } + @NotNull static SQLStateClass fromSQLiteVendorCode(int errorCode) { // See https://sqlite.org/c3ref/c_abort.html diff --git a/jOOQ/src/main/java/org/jooq/exception/SQLStateSubclass.java b/jOOQ/src/main/java/org/jooq/exception/SQLStateSubclass.java index 59e67b8211..b94ae88b96 100644 --- a/jOOQ/src/main/java/org/jooq/exception/SQLStateSubclass.java +++ b/jOOQ/src/main/java/org/jooq/exception/SQLStateSubclass.java @@ -88,6 +88,8 @@ import static org.jooq.exception.SQLStateClass.CHZ_REMOTE_DATABASE_ACCESS; import java.util.HashMap; import java.util.Map; +import org.jetbrains.annotations.NotNull; + /** * The subclass of the SQL state class as specified by the SQL standard, or by individual * vendors. @@ -460,18 +462,22 @@ public enum SQLStateSubclass { this.subclass = subclass; } + @NotNull public String sqlStateSubclassName() { return subclass; } + @NotNull public SQLStateClass sqlStateClass() { return clazz; } + @NotNull public String sqlStateClassName() { return sqlStateClass().className(); } + @NotNull public static SQLStateSubclass fromCode(String code) { if (code == null || code.length() != 5) return SQLStateSubclass.OTHER; @@ -488,6 +494,7 @@ public enum SQLStateSubclass { return SQLStateSubclass.OTHER; } + @NotNull static SQLStateSubclass fromSQLiteVendorCode(int errorCode) { // See https://sqlite.org/c3ref/c_abort.html diff --git a/jOOQ/src/main/java/org/jooq/impl/Tools.java b/jOOQ/src/main/java/org/jooq/impl/Tools.java index 0bcc013c48..7d10fad725 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Tools.java +++ b/jOOQ/src/main/java/org/jooq/impl/Tools.java @@ -88,6 +88,7 @@ import static org.jooq.conf.SettingsTools.getBackslashEscaping; import static org.jooq.conf.SettingsTools.updatablePrimaryKeys; import static org.jooq.conf.ThrowExceptions.THROW_FIRST; import static org.jooq.conf.ThrowExceptions.THROW_NONE; +import static org.jooq.exception.DataAccessException.sqlStateClass; import static org.jooq.impl.CacheType.REFLECTION_CACHE_GET_ANNOTATED_GETTER; import static org.jooq.impl.CacheType.REFLECTION_CACHE_GET_ANNOTATED_MEMBERS; import static org.jooq.impl.CacheType.REFLECTION_CACHE_GET_ANNOTATED_SETTERS; @@ -339,11 +340,14 @@ import org.jooq.conf.Settings; import org.jooq.conf.SettingsTools; import org.jooq.conf.ThrowExceptions; import org.jooq.exception.DataAccessException; +import org.jooq.exception.DataException; import org.jooq.exception.DataTypeException; import org.jooq.exception.DetachedException; import org.jooq.exception.ExceptionTools; +import org.jooq.exception.IntegrityConstraintViolationException; import org.jooq.exception.MappingException; import org.jooq.exception.NoDataFoundException; +import org.jooq.exception.SQLStateClass; import org.jooq.exception.TemplatingException; import org.jooq.exception.TooManyRowsException; import org.jooq.impl.QOM.Quantifier; @@ -3412,7 +3416,7 @@ final class Tools { */ static final DataAccessException translate(String sql, R2dbcException e) { if (e != null) - return new DataAccessException("SQL [" + sql + "]; " + e.getMessage(), e); + return translate(sql, e, sqlStateClass(e)); else return new DataAccessException("SQL [" + sql + "]; Unspecified R2dbcException"); } @@ -3422,11 +3426,22 @@ final class Tools { */ static final DataAccessException translate(String sql, SQLException e) { if (e != null) - return new DataAccessException("SQL [" + sql + "]; " + e.getMessage(), e); + return translate(sql, e, sqlStateClass(e)); else return new DataAccessException("SQL [" + sql + "]; Unspecified SQLException"); } + private static final DataAccessException translate(String sql, Exception e, SQLStateClass sqlState) { + switch (sqlState) { + case C22_DATA_EXCEPTION: + return new DataException("SQL [" + sql + "]; " + e.getMessage(), e); + case C23_INTEGRITY_CONSTRAINT_VIOLATION: + return new IntegrityConstraintViolationException("SQL [" + sql + "]; " + e.getMessage(), e); + default: + return new DataAccessException("SQL [" + sql + "]; " + e.getMessage(), e); + } + } + /** * Translate a {@link RuntimeException} to a {@link DataAccessException} */