001/* 002 * Copyright 2015-2022 Transmogrify LLC, 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.pyranid; 018 019import org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.NotThreadSafe; 023import java.sql.SQLException; 024import java.util.ArrayList; 025import java.util.List; 026import java.util.Optional; 027import java.util.stream.Collectors; 028 029import static java.lang.String.format; 030 031/** 032 * Thrown when an error occurs when interacting with a {@link Database}. 033 * <p> 034 * If the {@code cause} of this exception is a {@link SQLException}, the {@link #getErrorCode()} and {@link #getSqlState()} 035 * accessors are shorthand for retrieving the corresponding {@link SQLException} values. 036 * 037 * @author <a href="https://www.revetkn.com">Mark Allen</a> 038 * @since 1.0.0 039 */ 040@NotThreadSafe 041public class DatabaseException extends RuntimeException { 042 @Nullable 043 private final Integer errorCode; 044 @Nullable 045 private final String sqlState; 046 @Nullable 047 private final String column; 048 @Nullable 049 private final String constraint; 050 @Nullable 051 private final String datatype; 052 @Nullable 053 private final String detail; 054 @Nullable 055 private final String file; 056 @Nullable 057 private final String hint; 058 @Nullable 059 private final Integer internalPosition; 060 @Nullable 061 private final String internalQuery; 062 @Nullable 063 private final Integer line; 064 @Nullable 065 private final String dbmsMessage; 066 @Nullable 067 private final Integer position; 068 @Nullable 069 private final String routine; 070 @Nullable 071 private final String schema; 072 @Nullable 073 private final String severity; 074 @Nullable 075 private final String table; 076 @Nullable 077 private final String where; 078 @NonNull 079 private final Boolean uniqueConstraintViolation; 080 @NonNull 081 private final Boolean foreignKeyViolation; 082 @NonNull 083 private final Boolean deadlock; 084 @NonNull 085 private final Boolean transientException; 086 @NonNull 087 private final Boolean serializationFailure; 088 @NonNull 089 private final Boolean timeout; 090 091 /** 092 * Creates a {@code DatabaseException} with the given {@code message}. 093 * 094 * @param message a message describing this exception 095 */ 096 public DatabaseException(@Nullable String message) { 097 this(message, null); 098 } 099 100 /** 101 * Creates a {@code DatabaseException} which wraps the given {@code cause}. 102 * 103 * @param cause the cause of this exception 104 */ 105 public DatabaseException(@Nullable Throwable cause) { 106 this(cause == null ? null : cause.getMessage(), cause); 107 } 108 109 /** 110 * Creates a {@code DatabaseException} which wraps the given {@code cause}. 111 * 112 * @param message a message describing this exception 113 * @param cause the cause of this exception 114 */ 115 public DatabaseException(@Nullable String message, 116 @Nullable Throwable cause) { 117 this(message, cause, DatabaseDialect.forExceptionCause(cause)); 118 } 119 120 DatabaseException(@Nullable String message, 121 @Nullable Throwable cause, 122 @NonNull DatabaseDialect databaseDialect) { 123 super(message, cause); 124 125 DatabaseExceptionMetadata metadata = databaseDialect.databaseExceptionMetadata(cause); 126 127 this.errorCode = metadata.errorCode; 128 this.sqlState = metadata.sqlState; 129 this.column = metadata.column; 130 this.constraint = metadata.constraint; 131 this.datatype = metadata.datatype; 132 this.detail = metadata.detail; 133 this.file = metadata.file; 134 this.hint = metadata.hint; 135 this.internalPosition = metadata.internalPosition; 136 this.internalQuery = metadata.internalQuery; 137 this.line = metadata.line; 138 this.dbmsMessage = metadata.dbmsMessage; 139 this.position = metadata.position; 140 this.routine = metadata.routine; 141 this.schema = metadata.schema; 142 this.severity = metadata.severity; 143 this.table = metadata.table; 144 this.where = metadata.where; 145 this.uniqueConstraintViolation = databaseDialect.isUniqueConstraintViolation(metadata, cause); 146 this.foreignKeyViolation = databaseDialect.isForeignKeyViolation(metadata, cause); 147 this.deadlock = databaseDialect.isDeadlock(metadata, cause); 148 this.transientException = databaseDialect.isTransient(metadata, cause); 149 this.serializationFailure = databaseDialect.isSerializationFailure(metadata, cause); 150 this.timeout = databaseDialect.isTimeout(metadata, cause); 151 } 152 153 @Override 154 public String toString() { 155 List<String> components = new ArrayList<>(20); 156 157 if (getMessage() != null && getMessage().trim().length() > 0) 158 components.add(format("message=%s", getMessage().trim())); 159 160 if (getErrorCode().isPresent()) 161 components.add(format("errorCode=%s", getErrorCode().get())); 162 if (getSqlState().isPresent()) 163 components.add(format("sqlState=%s", getSqlState().get())); 164 if (getColumn().isPresent()) 165 components.add(format("column=%s", getColumn().get())); 166 if (getConstraint().isPresent()) 167 components.add(format("constraint=%s", getConstraint().get())); 168 if (getDatatype().isPresent()) 169 components.add(format("datatype=%s", getDatatype().get())); 170 if (getDetail().isPresent()) 171 components.add(format("detail=%s", getDetail().get())); 172 if (getFile().isPresent()) 173 components.add(format("file=%s", getFile().get())); 174 if (getHint().isPresent()) 175 components.add(format("hint=%s", getHint().get())); 176 if (getInternalPosition().isPresent()) 177 components.add(format("internalPosition=%s", getInternalPosition().get())); 178 if (getInternalQuery().isPresent()) 179 components.add(format("internalQuery=%s", getInternalQuery().get())); 180 if (getLine().isPresent()) 181 components.add(format("line=%s", getLine().get())); 182 if (getDbmsMessage().isPresent()) 183 components.add(format("dbmsMessage=%s", getDbmsMessage().get())); 184 if (getPosition().isPresent()) 185 components.add(format("position=%s", getPosition().get())); 186 if (getRoutine().isPresent()) 187 components.add(format("routine=%s", getRoutine().get())); 188 if (getSchema().isPresent()) 189 components.add(format("schema=%s", getSchema().get())); 190 if (getSeverity().isPresent()) 191 components.add(format("severity=%s", getSeverity().get())); 192 if (getTable().isPresent()) 193 components.add(format("table=%s", getTable().get())); 194 if (getWhere().isPresent()) 195 components.add(format("where=%s", getWhere().get())); 196 197 return format("%s: %s", getClass().getName(), components.stream().collect(Collectors.joining(", "))); 198 } 199 200 /** 201 * Shorthand for {@link SQLException#getErrorCode()} if this exception was caused by a {@link SQLException}. 202 * 203 * @return the value of {@link SQLException#getErrorCode()}, or empty if not available 204 */ 205 @NonNull 206 public Optional<Integer> getErrorCode() { 207 return Optional.ofNullable(this.errorCode); 208 } 209 210 /** 211 * Shorthand for {@link SQLException#getSQLState()} if this exception was caused by a {@link SQLException}. 212 * 213 * @return the value of {@link SQLException#getSQLState()}, or empty if not available 214 */ 215 @NonNull 216 public Optional<String> getSqlState() { 217 return Optional.ofNullable(this.sqlState); 218 } 219 220 /** 221 * Determines if this exception is recognized as a unique constraint violation. 222 * <p> 223 * This method is intentionally conservative: it returns {@code true} only when Pyranid recognizes a database-specific 224 * error code or SQLState as a unique or primary-key uniqueness violation. 225 * 226 * @return {@code true} if this exception is recognized as a unique constraint violation 227 * @since 4.3.0 228 */ 229 @NonNull 230 public Boolean isUniqueConstraintViolation() { 231 return this.uniqueConstraintViolation; 232 } 233 234 /** 235 * Determines if this exception is recognized as a foreign-key violation. 236 * <p> 237 * This method is intentionally conservative: it returns {@code true} only when Pyranid recognizes a database-specific 238 * error code or SQLState as a foreign-key violation. 239 * For example, SQL Server error {@code 547} is not classified as a foreign-key violation by code alone because the 240 * same error code also covers other constraint failures, such as {@code CHECK} constraints. 241 * 242 * @return {@code true} if this exception is recognized as a foreign-key violation 243 * @since 4.3.0 244 */ 245 @NonNull 246 public Boolean isForeignKeyViolation() { 247 return this.foreignKeyViolation; 248 } 249 250 /** 251 * Determines if this exception is recognized as a deadlock. 252 * <p> 253 * This method is intentionally conservative: it returns {@code true} only when Pyranid recognizes a database-specific 254 * error code or SQLState as a deadlock. 255 * 256 * @return {@code true} if this exception is recognized as a deadlock 257 * @since 4.3.0 258 */ 259 @NonNull 260 public Boolean isDeadlock() { 261 return this.deadlock; 262 } 263 264 /** 265 * Determines if this exception is recognized as transient. 266 * <p> 267 * This method is intentionally conservative: it returns {@code true} for JDBC transient/recoverable exception classes, 268 * SQLState classes for connection exceptions and transaction rollbacks, or database-specific deadlock/lock-timeout 269 * signals recognized by Pyranid. A {@code true} result does not guarantee that retrying the operation will succeed. 270 * 271 * @return {@code true} if this exception is recognized as transient 272 * @since 4.3.0 273 */ 274 @NonNull 275 public Boolean isTransient() { 276 return this.transientException; 277 } 278 279 /** 280 * Determines if this exception is recognized as a serialization failure. 281 * <p> 282 * This method is intentionally conservative: it returns {@code true} only when Pyranid recognizes a database-specific 283 * error code or SQLState as a serialization failure. 284 * 285 * @return {@code true} if this exception is recognized as a serialization failure 286 * @since 4.4.0 287 */ 288 @NonNull 289 public Boolean isSerializationFailure() { 290 return this.serializationFailure; 291 } 292 293 /** 294 * Determines if this exception is recognized as a timeout or cancellation. 295 * <p> 296 * This method is intentionally conservative: it returns {@code true} only when Pyranid recognizes a JDBC timeout, 297 * database-specific timeout, or database-specific cancellation signal. A {@code true} result does not mean the operation 298 * is automatically safe to retry. 299 * 300 * @return {@code true} if this exception is recognized as a timeout or cancellation 301 * @since 4.4.0 302 */ 303 @NonNull 304 public Boolean isTimeout() { 305 return this.timeout; 306 } 307 308 /** 309 * @return the value of the offending {@code column}, or empty if not available 310 * @since 1.0.12 311 */ 312 @NonNull 313 public Optional<String> getColumn() { 314 return Optional.ofNullable(this.column); 315 } 316 317 /** 318 * @return the value of the offending {@code constraint}, or empty if not available 319 * @since 1.0.12 320 */ 321 @NonNull 322 public Optional<String> getConstraint() { 323 return Optional.ofNullable(this.constraint); 324 } 325 326 /** 327 * @return the value of the offending {@code datatype}, or empty if not available 328 * @since 1.0.12 329 */ 330 @NonNull 331 public Optional<String> getDatatype() { 332 return Optional.ofNullable(this.datatype); 333 } 334 335 /** 336 * @return the value of the offending {@code detail}, or empty if not available 337 * @since 1.0.12 338 */ 339 @NonNull 340 public Optional<String> getDetail() { 341 return Optional.ofNullable(this.detail); 342 } 343 344 /** 345 * @return the value of the offending {@code file}, or empty if not available 346 * @since 1.0.12 347 */ 348 @NonNull 349 public Optional<String> getFile() { 350 return Optional.ofNullable(this.file); 351 } 352 353 /** 354 * @return the value of the error {@code hint}, or empty if not available 355 * @since 1.0.12 356 */ 357 @NonNull 358 public Optional<String> getHint() { 359 return Optional.ofNullable(this.hint); 360 } 361 362 /** 363 * @return the value of the offending {@code internalPosition}, or empty if not available 364 * @since 1.0.12 365 */ 366 @NonNull 367 public Optional<Integer> getInternalPosition() { 368 return Optional.ofNullable(this.internalPosition); 369 } 370 371 /** 372 * @return the value of the offending {@code internalQuery}, or empty if not available 373 * @since 1.0.12 374 */ 375 @NonNull 376 public Optional<String> getInternalQuery() { 377 return Optional.ofNullable(this.internalQuery); 378 } 379 380 /** 381 * @return the value of the offending {@code line}, or empty if not available 382 * @since 1.0.12 383 */ 384 @NonNull 385 public Optional<Integer> getLine() { 386 return Optional.ofNullable(this.line); 387 } 388 389 /** 390 * @return the value of the error {@code dbmsMessage}, or empty if not available 391 * @since 1.0.12 392 */ 393 @NonNull 394 public Optional<String> getDbmsMessage() { 395 return Optional.ofNullable(this.dbmsMessage); 396 } 397 398 /** 399 * @return the value of the offending {@code position}, or empty if not available 400 * @since 1.0.12 401 */ 402 @NonNull 403 public Optional<Integer> getPosition() { 404 return Optional.ofNullable(this.position); 405 } 406 407 /** 408 * @return the value of the offending {@code routine}, or empty if not available 409 * @since 1.0.12 410 */ 411 @NonNull 412 public Optional<String> getRoutine() { 413 return Optional.ofNullable(this.routine); 414 } 415 416 /** 417 * @return the value of the offending {@code schema}, or empty if not available 418 * @since 1.0.12 419 */ 420 @NonNull 421 public Optional<String> getSchema() { 422 return Optional.ofNullable(this.schema); 423 } 424 425 /** 426 * @return the error {@code severity}, or empty if not available 427 * @since 1.0.12 428 */ 429 @NonNull 430 public Optional<String> getSeverity() { 431 return Optional.ofNullable(this.severity); 432 } 433 434 /** 435 * @return the value of the offending {@code table}, or empty if not available 436 * @since 1.0.12 437 */ 438 @NonNull 439 public Optional<String> getTable() { 440 return Optional.ofNullable(this.table); 441 } 442 443 /** 444 * @return the value of the offending {@code where}, or empty if not available 445 * @since 1.0.12 446 */ 447 @NonNull 448 public Optional<String> getWhere() { 449 return Optional.ofNullable(this.where); 450 } 451}