From aee2b57b47cc80c60e96cf7b767863da96611dee Mon Sep 17 00:00:00 2001 From: hermes Date: Sun, 12 Apr 2026 02:27:50 +0000 Subject: [PATCH 1/3] feat: implement exception hierarchy and ExceptionTranslator - Add OrmException as base exception class - Add EntityValidationException for entity structure validation errors - Add EntityNotFoundException for Find() with zero rows - Add DuplicateKeyException for UNIQUE/PK constraint violations (SQLState 23505) - Add DatabaseException to wrap unexpected SQLExceptions - Add ExceptionTranslator to map SQLException to typed ORM exceptions Implements exception hierarchy from spec: - RuntimeException -> OrmException -> EntityValidationException - OrmException -> EntityNotFoundException - OrmException -> DuplicateKeyException - OrmException -> DatabaseException --- .../database/exception/DatabaseException.java | 26 ++++++++ .../exception/DuplicateKeyException.java | 30 +++++++++ .../exception/EntityNotFoundException.java | 24 +++++++ .../exception/EntityValidationException.java | 18 ++++++ .../exception/ExceptionTranslator.java | 62 +++++++++++++++++++ .../database/exception/OrmException.java | 16 +++++ 6 files changed, 176 insertions(+) create mode 100644 src/main/java/org/example/database/exception/DatabaseException.java create mode 100644 src/main/java/org/example/database/exception/DuplicateKeyException.java create mode 100644 src/main/java/org/example/database/exception/EntityNotFoundException.java create mode 100644 src/main/java/org/example/database/exception/EntityValidationException.java create mode 100644 src/main/java/org/example/database/exception/ExceptionTranslator.java create mode 100644 src/main/java/org/example/database/exception/OrmException.java diff --git a/src/main/java/org/example/database/exception/DatabaseException.java b/src/main/java/org/example/database/exception/DatabaseException.java new file mode 100644 index 0000000..0872ce6 --- /dev/null +++ b/src/main/java/org/example/database/exception/DatabaseException.java @@ -0,0 +1,26 @@ +package org.example.database.exception; + +import java.sql.SQLException; + +/** + * Wraps any other unexpected SQLException. + */ +public class DatabaseException extends OrmException { + + private final String sqlQuery; + private final SQLException cause; + + public DatabaseException(String message, String sqlQuery, SQLException cause) { + super(message, cause); + this.sqlQuery = sqlQuery; + this.cause = cause; + } + + public String getSqlQuery() { + return sqlQuery; + } + + public SQLException getCause() { + return cause; + } +} diff --git a/src/main/java/org/example/database/exception/DuplicateKeyException.java b/src/main/java/org/example/database/exception/DuplicateKeyException.java new file mode 100644 index 0000000..9dbdc03 --- /dev/null +++ b/src/main/java/org/example/database/exception/DuplicateKeyException.java @@ -0,0 +1,30 @@ +package org.example.database.exception; + +/** + * Thrown when INSERT violates a UNIQUE or PRIMARY KEY constraint (PostgreSQL SQLState 23505). + */ +public class DuplicateKeyException extends OrmException { + + private final Class entityClass; + private final String sqlQuery; + private final String constraintName; + + public DuplicateKeyException(String message, Class entityClass, String sqlQuery, String constraintName) { + super(message); + this.entityClass = entityClass; + this.sqlQuery = sqlQuery; + this.constraintName = constraintName; + } + + public Class getEntityClass() { + return entityClass; + } + + public String getSqlQuery() { + return sqlQuery; + } + + public String getConstraintName() { + return constraintName; + } +} diff --git a/src/main/java/org/example/database/exception/EntityNotFoundException.java b/src/main/java/org/example/database/exception/EntityNotFoundException.java new file mode 100644 index 0000000..9275ec8 --- /dev/null +++ b/src/main/java/org/example/database/exception/EntityNotFoundException.java @@ -0,0 +1,24 @@ +package org.example.database.exception; + +/** + * Thrown when Find() returns zero rows. + */ +public class EntityNotFoundException extends OrmException { + + private final Class entityClass; + private final String sqlQuery; + + public EntityNotFoundException(String message, Class entityClass, String sqlQuery) { + super(message); + this.entityClass = entityClass; + this.sqlQuery = sqlQuery; + } + + public Class getEntityClass() { + return entityClass; + } + + public String getSqlQuery() { + return sqlQuery; + } +} diff --git a/src/main/java/org/example/database/exception/EntityValidationException.java b/src/main/java/org/example/database/exception/EntityValidationException.java new file mode 100644 index 0000000..28d6416 --- /dev/null +++ b/src/main/java/org/example/database/exception/EntityValidationException.java @@ -0,0 +1,18 @@ +package org.example.database.exception; + +/** + * Thrown when entity structure is invalid (missing @Entity, @Id issues). + */ +public class EntityValidationException extends OrmException { + + private final Class entityClass; + + public EntityValidationException(String message, Class entityClass) { + super(message); + this.entityClass = entityClass; + } + + public Class getEntityClass() { + return entityClass; + } +} diff --git a/src/main/java/org/example/database/exception/ExceptionTranslator.java b/src/main/java/org/example/database/exception/ExceptionTranslator.java new file mode 100644 index 0000000..66187f4 --- /dev/null +++ b/src/main/java/org/example/database/exception/ExceptionTranslator.java @@ -0,0 +1,62 @@ +package org.example.database.exception; + +import java.sql.SQLException; + +/** + * Translates SQLException into typed ORM exceptions. + */ +public class ExceptionTranslator { + + private static final String UNIQUE_VIOLATION_CODE = "23505"; + + /** + * Translates a SQLException into the appropriate ORM exception. + * + * @param e the SQLException to translate + * @param entityClass the entity class involved (may be null) + * @param sql the SQL query that caused the error + * @return the translated OrmException + */ + public static OrmException translate(SQLException e, Class entityClass, String sql) { + String sqlState = e.getSQLState(); + + // SQLState equals 23505 -> DuplicateKeyException + if (UNIQUE_VIOLATION_CODE.equals(sqlState)) { + String constraintName = extractConstraintName(e); + String message = String.format("Duplicate key violation on entity '%s': unique constraint '%s'", + entityClass != null ? entityClass.getSimpleName() : "unknown", + constraintName); + return new DuplicateKeyException(message, entityClass, sql, constraintName); + } + + // SQLState starts with 23 -> DatabaseException (other integrity violations) + if (sqlState != null && sqlState.startsWith("23")) { + String message = String.format("Database error executing query '%s': %s", sql, e.getMessage()); + return new DatabaseException(message, sql, e); + } + + // Any other SQLException -> DatabaseException + String message = String.format("Database error executing query '%s': %s", sql, e.getMessage()); + return new DatabaseException(message, sql, e); + } + + /** + * Extracts the constraint name from the SQLException message. + */ + private static String extractConstraintName(SQLException e) { + String message = e.getMessage(); + // PostgreSQL typically includes the constraint name in the message + // Example: "duplicate key value violates unique constraint "users_pkey"" + if (message != null && message.contains("unique constraint")) { + int start = message.lastIndexOf("unique constraint \""); + if (start != -1) { + start += "unique constraint \"".length(); + int end = message.indexOf("\"", start); + if (end > start) { + return message.substring(start, end); + } + } + } + return "unknown"; + } +} diff --git a/src/main/java/org/example/database/exception/OrmException.java b/src/main/java/org/example/database/exception/OrmException.java new file mode 100644 index 0000000..30b6c81 --- /dev/null +++ b/src/main/java/org/example/database/exception/OrmException.java @@ -0,0 +1,16 @@ +package org.example.database.exception; + +/** + * Base exception class for all ORM-related errors. + * All ORM exceptions extend this class and are unchecked (extend RuntimeException). + */ +public class OrmException extends RuntimeException { + + public OrmException(String message) { + super(message); + } + + public OrmException(String message, Throwable cause) { + super(message, cause); + } +} From b8cb2441edb18918d3362c53cb0f498b17de4a00 Mon Sep 17 00:00:00 2001 From: hermes Date: Sun, 12 Apr 2026 03:06:35 +0000 Subject: [PATCH 2/3] refactor: remove comments from exception classes --- .../database/exception/DatabaseException.java | 3 --- .../exception/DuplicateKeyException.java | 3 --- .../exception/EntityNotFoundException.java | 3 --- .../exception/EntityValidationException.java | 3 --- .../exception/ExceptionTranslator.java | 19 ------------------- .../database/exception/OrmException.java | 4 ---- 6 files changed, 35 deletions(-) diff --git a/src/main/java/org/example/database/exception/DatabaseException.java b/src/main/java/org/example/database/exception/DatabaseException.java index 0872ce6..96dd53d 100644 --- a/src/main/java/org/example/database/exception/DatabaseException.java +++ b/src/main/java/org/example/database/exception/DatabaseException.java @@ -2,9 +2,6 @@ import java.sql.SQLException; -/** - * Wraps any other unexpected SQLException. - */ public class DatabaseException extends OrmException { private final String sqlQuery; diff --git a/src/main/java/org/example/database/exception/DuplicateKeyException.java b/src/main/java/org/example/database/exception/DuplicateKeyException.java index 9dbdc03..17cfb90 100644 --- a/src/main/java/org/example/database/exception/DuplicateKeyException.java +++ b/src/main/java/org/example/database/exception/DuplicateKeyException.java @@ -1,8 +1,5 @@ package org.example.database.exception; -/** - * Thrown when INSERT violates a UNIQUE or PRIMARY KEY constraint (PostgreSQL SQLState 23505). - */ public class DuplicateKeyException extends OrmException { private final Class entityClass; diff --git a/src/main/java/org/example/database/exception/EntityNotFoundException.java b/src/main/java/org/example/database/exception/EntityNotFoundException.java index 9275ec8..91c6133 100644 --- a/src/main/java/org/example/database/exception/EntityNotFoundException.java +++ b/src/main/java/org/example/database/exception/EntityNotFoundException.java @@ -1,8 +1,5 @@ package org.example.database.exception; -/** - * Thrown when Find() returns zero rows. - */ public class EntityNotFoundException extends OrmException { private final Class entityClass; diff --git a/src/main/java/org/example/database/exception/EntityValidationException.java b/src/main/java/org/example/database/exception/EntityValidationException.java index 28d6416..e066054 100644 --- a/src/main/java/org/example/database/exception/EntityValidationException.java +++ b/src/main/java/org/example/database/exception/EntityValidationException.java @@ -1,8 +1,5 @@ package org.example.database.exception; -/** - * Thrown when entity structure is invalid (missing @Entity, @Id issues). - */ public class EntityValidationException extends OrmException { private final Class entityClass; diff --git a/src/main/java/org/example/database/exception/ExceptionTranslator.java b/src/main/java/org/example/database/exception/ExceptionTranslator.java index 66187f4..597251b 100644 --- a/src/main/java/org/example/database/exception/ExceptionTranslator.java +++ b/src/main/java/org/example/database/exception/ExceptionTranslator.java @@ -2,25 +2,13 @@ import java.sql.SQLException; -/** - * Translates SQLException into typed ORM exceptions. - */ public class ExceptionTranslator { private static final String UNIQUE_VIOLATION_CODE = "23505"; - /** - * Translates a SQLException into the appropriate ORM exception. - * - * @param e the SQLException to translate - * @param entityClass the entity class involved (may be null) - * @param sql the SQL query that caused the error - * @return the translated OrmException - */ public static OrmException translate(SQLException e, Class entityClass, String sql) { String sqlState = e.getSQLState(); - // SQLState equals 23505 -> DuplicateKeyException if (UNIQUE_VIOLATION_CODE.equals(sqlState)) { String constraintName = extractConstraintName(e); String message = String.format("Duplicate key violation on entity '%s': unique constraint '%s'", @@ -29,24 +17,17 @@ public static OrmException translate(SQLException e, Class entityClass, Strin return new DuplicateKeyException(message, entityClass, sql, constraintName); } - // SQLState starts with 23 -> DatabaseException (other integrity violations) if (sqlState != null && sqlState.startsWith("23")) { String message = String.format("Database error executing query '%s': %s", sql, e.getMessage()); return new DatabaseException(message, sql, e); } - // Any other SQLException -> DatabaseException String message = String.format("Database error executing query '%s': %s", sql, e.getMessage()); return new DatabaseException(message, sql, e); } - /** - * Extracts the constraint name from the SQLException message. - */ private static String extractConstraintName(SQLException e) { String message = e.getMessage(); - // PostgreSQL typically includes the constraint name in the message - // Example: "duplicate key value violates unique constraint "users_pkey"" if (message != null && message.contains("unique constraint")) { int start = message.lastIndexOf("unique constraint \""); if (start != -1) { diff --git a/src/main/java/org/example/database/exception/OrmException.java b/src/main/java/org/example/database/exception/OrmException.java index 30b6c81..b2ed9cd 100644 --- a/src/main/java/org/example/database/exception/OrmException.java +++ b/src/main/java/org/example/database/exception/OrmException.java @@ -1,9 +1,5 @@ package org.example.database.exception; -/** - * Base exception class for all ORM-related errors. - * All ORM exceptions extend this class and are unchecked (extend RuntimeException). - */ public class OrmException extends RuntimeException { public OrmException(String message) { From 9b7c4ace04e89e21163b250fd9ca9ba9946c3ee8 Mon Sep 17 00:00:00 2001 From: hermes Date: Sun, 12 Apr 2026 03:13:30 +0000 Subject: [PATCH 3/3] refactor: simplify exceptions to only message field --- .../database/exception/DatabaseException.java | 21 +++-------------- .../exception/DuplicateKeyException.java | 23 ++----------------- .../exception/EntityNotFoundException.java | 17 ++------------ .../exception/EntityValidationException.java | 11 ++------- .../exception/ExceptionTranslator.java | 8 +++---- .../database/exception/OrmException.java | 6 +---- 6 files changed, 14 insertions(+), 72 deletions(-) diff --git a/src/main/java/org/example/database/exception/DatabaseException.java b/src/main/java/org/example/database/exception/DatabaseException.java index 96dd53d..7605983 100644 --- a/src/main/java/org/example/database/exception/DatabaseException.java +++ b/src/main/java/org/example/database/exception/DatabaseException.java @@ -1,23 +1,8 @@ package org.example.database.exception; -import java.sql.SQLException; - public class DatabaseException extends OrmException { - private final String sqlQuery; - private final SQLException cause; - - public DatabaseException(String message, String sqlQuery, SQLException cause) { - super(message, cause); - this.sqlQuery = sqlQuery; - this.cause = cause; - } - - public String getSqlQuery() { - return sqlQuery; - } - - public SQLException getCause() { - return cause; + public DatabaseException(String message) { + super(message); } -} +} \ No newline at end of file diff --git a/src/main/java/org/example/database/exception/DuplicateKeyException.java b/src/main/java/org/example/database/exception/DuplicateKeyException.java index 17cfb90..12c6282 100644 --- a/src/main/java/org/example/database/exception/DuplicateKeyException.java +++ b/src/main/java/org/example/database/exception/DuplicateKeyException.java @@ -2,26 +2,7 @@ public class DuplicateKeyException extends OrmException { - private final Class entityClass; - private final String sqlQuery; - private final String constraintName; - - public DuplicateKeyException(String message, Class entityClass, String sqlQuery, String constraintName) { + public DuplicateKeyException(String message) { super(message); - this.entityClass = entityClass; - this.sqlQuery = sqlQuery; - this.constraintName = constraintName; - } - - public Class getEntityClass() { - return entityClass; - } - - public String getSqlQuery() { - return sqlQuery; - } - - public String getConstraintName() { - return constraintName; } -} +} \ No newline at end of file diff --git a/src/main/java/org/example/database/exception/EntityNotFoundException.java b/src/main/java/org/example/database/exception/EntityNotFoundException.java index 91c6133..2a85c59 100644 --- a/src/main/java/org/example/database/exception/EntityNotFoundException.java +++ b/src/main/java/org/example/database/exception/EntityNotFoundException.java @@ -2,20 +2,7 @@ public class EntityNotFoundException extends OrmException { - private final Class entityClass; - private final String sqlQuery; - - public EntityNotFoundException(String message, Class entityClass, String sqlQuery) { + public EntityNotFoundException(String message) { super(message); - this.entityClass = entityClass; - this.sqlQuery = sqlQuery; - } - - public Class getEntityClass() { - return entityClass; - } - - public String getSqlQuery() { - return sqlQuery; } -} +} \ No newline at end of file diff --git a/src/main/java/org/example/database/exception/EntityValidationException.java b/src/main/java/org/example/database/exception/EntityValidationException.java index e066054..60024e9 100644 --- a/src/main/java/org/example/database/exception/EntityValidationException.java +++ b/src/main/java/org/example/database/exception/EntityValidationException.java @@ -2,14 +2,7 @@ public class EntityValidationException extends OrmException { - private final Class entityClass; - - public EntityValidationException(String message, Class entityClass) { + public EntityValidationException(String message) { super(message); - this.entityClass = entityClass; - } - - public Class getEntityClass() { - return entityClass; } -} +} \ No newline at end of file diff --git a/src/main/java/org/example/database/exception/ExceptionTranslator.java b/src/main/java/org/example/database/exception/ExceptionTranslator.java index 597251b..f54e2b2 100644 --- a/src/main/java/org/example/database/exception/ExceptionTranslator.java +++ b/src/main/java/org/example/database/exception/ExceptionTranslator.java @@ -14,16 +14,16 @@ public static OrmException translate(SQLException e, Class entityClass, Strin String message = String.format("Duplicate key violation on entity '%s': unique constraint '%s'", entityClass != null ? entityClass.getSimpleName() : "unknown", constraintName); - return new DuplicateKeyException(message, entityClass, sql, constraintName); + return new DuplicateKeyException(message); } if (sqlState != null && sqlState.startsWith("23")) { String message = String.format("Database error executing query '%s': %s", sql, e.getMessage()); - return new DatabaseException(message, sql, e); + return new DatabaseException(message); } String message = String.format("Database error executing query '%s': %s", sql, e.getMessage()); - return new DatabaseException(message, sql, e); + return new DatabaseException(message); } private static String extractConstraintName(SQLException e) { @@ -40,4 +40,4 @@ private static String extractConstraintName(SQLException e) { } return "unknown"; } -} +} \ No newline at end of file diff --git a/src/main/java/org/example/database/exception/OrmException.java b/src/main/java/org/example/database/exception/OrmException.java index b2ed9cd..0c43ac4 100644 --- a/src/main/java/org/example/database/exception/OrmException.java +++ b/src/main/java/org/example/database/exception/OrmException.java @@ -5,8 +5,4 @@ public class OrmException extends RuntimeException { public OrmException(String message) { super(message); } - - public OrmException(String message, Throwable cause) { - super(message, cause); - } -} +} \ No newline at end of file