Skip to content

Commit af57313

Browse files
authored
Improve error reporting. (#82) (#85)
1 parent 6218dbe commit af57313

18 files changed

Lines changed: 162 additions & 133 deletions

storm-core/src/main/java/st/orm/core/repository/impl/EntityRepositoryImpl.java

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public EntityRepositoryImpl(@Nonnull ORMTemplate ormTemplate, @Nonnull Model<E,
102102
.findFirst()
103103
.orElse(NONE);
104104
if (generationStrategy == SEQUENCE && primaryKeyColumns.size() != 1) {
105-
throw new PersistenceException("Sequence generation is only supported for single-column primary keys.");
105+
throw new PersistenceException("Sequence generation is only supported for single-column primary keys for %s.".formatted(model.type().getSimpleName()));
106106
}
107107
this.dirtySupport = new DirtySupport<>(model, ormTemplate.config());
108108
this.cacheRetention = CacheRetention.fromConfig(ormTemplate.config());
@@ -425,11 +425,11 @@ public int getDefaultBatchSize() {
425425
protected E validateInsert(@Nonnull E entity) {
426426
if (isAutoGeneratedPrimaryKey()) {
427427
if (!model.isDefaultPrimaryKey(entity.id())) {
428-
throw new PersistenceException("Primary key must not be set for auto-generated primary keys for inserts.");
428+
throw new PersistenceException("Primary key must not be set when inserting %s with an auto-generated primary key. Either omit the primary key value, or use insert(entity, true) to explicitly provide it.".formatted(model.type().getSimpleName()));
429429
}
430430
} else {
431431
if (model.isDefaultPrimaryKey(entity.id())) {
432-
throw new PersistenceException("Primary key must be set for non-auto-generated primary keys for inserts.");
432+
throw new PersistenceException("Primary key must be set when inserting %s because the primary key is not auto-generated. Provide a non-default primary key value.".formatted(model.type().getSimpleName()));
433433
}
434434
}
435435
return entity;
@@ -440,21 +440,21 @@ protected E validateInsert(@Nonnull E entity, boolean ignoreAutoGenerate) {
440440
return validateInsert(entity);
441441
}
442442
if (model.isDefaultPrimaryKey(entity.id())) {
443-
throw new PersistenceException("Primary key must be set.");
443+
throw new PersistenceException("Primary key must be set when inserting %s with ignoreAutoGenerate enabled.".formatted(model.type().getSimpleName()));
444444
}
445445
return entity;
446446
}
447447

448448
protected E validateUpdate(@Nonnull E entity) {
449449
if (model.isDefaultPrimaryKey(entity.id())) {
450-
throw new PersistenceException("Primary key must be set for updates.");
450+
throw new PersistenceException("Primary key must be set when updating %s. Provide a non-default primary key value to identify the row to update.".formatted(model.type().getSimpleName()));
451451
}
452452
return entity;
453453
}
454454

455455
protected E validateDelete(@Nonnull E entity) {
456456
if (model.isDefaultPrimaryKey(entity.id())) {
457-
throw new PersistenceException("Primary key must be set for deletes.");
457+
throw new PersistenceException("Primary key must be set when deleting %s. Provide a non-default primary key value to identify the row to delete.".formatted(model.type().getSimpleName()));
458458
}
459459
return entity;
460460
}
@@ -471,7 +471,7 @@ protected E validateDelete(@Nonnull E entity) {
471471
*/
472472
protected E validateUpsert(@Nonnull E entity) {
473473
if (!isAutoGeneratedPrimaryKey() && model.isDefaultPrimaryKey(entity.id())) {
474-
throw new PersistenceException("Primary key must be set for non-auto-generated primary keys for upserts.");
474+
throw new PersistenceException("Primary key must be set when upserting %s because the primary key is not auto-generated. Provide a non-default primary key value.".formatted(model.type().getSimpleName()));
475475
}
476476
return entity;
477477
}
@@ -558,7 +558,7 @@ public void insert(@Nonnull E entity) {
558558
VALUES \0""", model.type(), entity))
559559
.managed();
560560
if (query.executeUpdate() != 1) {
561-
throw new PersistenceException("Insert failed.");
561+
throw new PersistenceException("Insert of %s failed. 0 rows were affected, which may indicate a trigger, constraint, or BEFORE INSERT hook prevented the row from being written.".formatted(model.type().getSimpleName()));
562562
}
563563
fireAfterInsert(entity);
564564
}
@@ -586,7 +586,7 @@ public void insert(@Nonnull E entity, boolean ignoreAutoGenerate) {
586586
VALUES \0""", Templates.insert(model.type(), ignoreAutoGenerate), Templates.values(entity, ignoreAutoGenerate)))
587587
.managed();
588588
if (query.executeUpdate() != 1) {
589-
throw new PersistenceException("Insert failed.");
589+
throw new PersistenceException("Insert of %s failed. 0 rows were affected, which may indicate a trigger, constraint, or BEFORE INSERT hook prevented the row from being written.".formatted(model.type().getSimpleName()));
590590
}
591591
fireAfterInsert(entity);
592592
}
@@ -616,7 +616,7 @@ public ID insertAndFetchId(@Nonnull E entity) {
616616
INSERT INTO \0
617617
VALUES \0""", model.type(), entity)).managed().prepare()) {
618618
if (query.executeUpdate() != 1) {
619-
throw new PersistenceException("Insert failed.");
619+
throw new PersistenceException("Insert of %s failed. 0 rows were affected, which may indicate a trigger, constraint, or BEFORE INSERT hook prevented the row from being written.".formatted(model.type().getSimpleName()));
620620
}
621621
ID id = singleResult(isAutoGeneratedPrimaryKey()
622622
? query.getGeneratedKeys(model.primaryKeyType())
@@ -871,9 +871,9 @@ public void update(@Nonnull E entity) {
871871
.managed();
872872
int result = query.executeUpdate();
873873
if (query.isVersionAware() && result == 0) {
874-
throw new OptimisticLockException("Update failed due to optimistic lock.");
874+
throw new OptimisticLockException("Update of %s failed due to optimistic lock. The entity may have been modified or deleted by another transaction.".formatted(model.type().getSimpleName()));
875875
} else if (result != 1) {
876-
throw new PersistenceException("Update failed.");
876+
throw new PersistenceException("Update of %s failed. 0 rows were affected, possibly because the row does not exist or a constraint prevented the update.".formatted(model.type().getSimpleName()));
877877
}
878878
fireAfterUpdate(e);
879879
}
@@ -905,7 +905,7 @@ private PersistenceException upsertNotAvailable() {
905905

906906
private void requireNonJoinedSealedEntity() {
907907
if (model.isJoinedInheritance()) {
908-
throw new PersistenceException("Upsert is not supported for joined sealed entities.");
908+
throw new PersistenceException("Upsert is not supported for joined sealed entity %s.".formatted(model.type().getSimpleName()));
909909
}
910910
}
911911

@@ -1057,7 +1057,7 @@ public void delete(@Nonnull E entity) {
10571057
.managed()
10581058
.executeUpdate();
10591059
if (result != 1) {
1060-
throw new PersistenceException("Delete failed.");
1060+
throw new PersistenceException("Delete of %s failed. 0 rows were affected, possibly because the entity does not exist or a foreign key constraint prevents deletion.".formatted(model.type().getSimpleName()));
10611061
}
10621062
fireAfterDelete(entity);
10631063
}
@@ -1557,7 +1557,7 @@ protected void insert(@Nonnull List<E> batch, @Nonnull PreparedQuery query, bool
15571557
.forEach(query::addBatch);
15581558
int[] result = query.executeBatch();
15591559
if (IntStream.of(result).anyMatch(r -> r != 1)) {
1560-
throw new PersistenceException("Batch insert failed.");
1560+
throw new PersistenceException("Batch insert of %s failed. One or more rows were not affected.".formatted(model.type().getSimpleName()));
15611561
}
15621562
transformed.forEach(this::fireAfterInsert);
15631563
}
@@ -1579,7 +1579,7 @@ private List<ID> insertAndFetchIds(@Nonnull List<E> batch, @Nonnull PreparedQuer
15791579
.forEach(query::addBatch);
15801580
int[] result = query.executeBatch();
15811581
if (IntStream.of(result).anyMatch(r -> r != 1)) {
1582-
throw new PersistenceException("Batch insert failed.");
1582+
throw new PersistenceException("Batch insert of %s failed. One or more rows were not affected.".formatted(model.type().getSimpleName()));
15831583
}
15841584
transformed.forEach(this::fireAfterInsert);
15851585
if (isAutoGeneratedPrimaryKey() && !ignoreAutoGenerate) {
@@ -1716,9 +1716,9 @@ protected void update(@Nonnull List<E> batch, @Nonnull PreparedQuery query, @Nul
17161716
}
17171717
int[] result = query.executeBatch();
17181718
if (query.isVersionAware() && IntStream.of(result).anyMatch(r -> r == 0)) {
1719-
throw new OptimisticLockException("Update failed due to optimistic lock.");
1719+
throw new OptimisticLockException("Batch update of %s failed due to optimistic lock. One or more entities may have been modified or deleted by another transaction.".formatted(model.type().getSimpleName()));
17201720
} else if (IntStream.of(result).anyMatch(r -> r != 1)) {
1721-
throw new PersistenceException("Batch update failed.");
1721+
throw new PersistenceException("Batch update of %s failed. One or more rows were not affected.".formatted(model.type().getSimpleName()));
17221722
}
17231723
batch.forEach(this::fireAfterUpdate);
17241724
}
@@ -1735,9 +1735,9 @@ protected List<ID> updateAndFetchIds(@Nonnull List<E> batch, @Nonnull PreparedQu
17351735
}
17361736
int[] result = query.executeBatch();
17371737
if (query.isVersionAware() && IntStream.of(result).anyMatch(r -> r == 0)) {
1738-
throw new OptimisticLockException("Update failed due to optimistic lock.");
1738+
throw new OptimisticLockException("Batch update of %s failed due to optimistic lock. One or more entities may have been modified or deleted by another transaction.".formatted(model.type().getSimpleName()));
17391739
} else if (IntStream.of(result).anyMatch(r -> r != 1)) {
1740-
throw new PersistenceException("Batch update failed.");
1740+
throw new PersistenceException("Batch update of %s failed. One or more rows were not affected.".formatted(model.type().getSimpleName()));
17411741
}
17421742
batch.forEach(this::fireAfterUpdate);
17431743
return batch.stream().map(Entity::id).toList();
@@ -1959,7 +1959,7 @@ public void delete(@Nonnull Stream<E> entities, int batchSize) {
19591959
.forEach(e -> cache.remove(e.id())));
19601960
int[] result = query.executeBatch();
19611961
if (IntStream.of(result).anyMatch(r -> r != 1)) {
1962-
throw new PersistenceException("Batch delete failed.");
1962+
throw new PersistenceException("Batch delete of %s failed. One or more rows were not affected.".formatted(model.type().getSimpleName()));
19631963
}
19641964
chunk.forEach(this::fireAfterDelete);
19651965
});
@@ -2052,7 +2052,7 @@ private <T> T singleResult(Stream<T> stream) {
20522052
try (stream) {
20532053
return stream
20542054
.reduce((a, b) -> {
2055-
throw new NonUniqueResultException("Expected single result, but found more than one.");
2055+
throw new NonUniqueResultException("Expected single result for %s, but found more than one.".formatted(model.type().getSimpleName()));
20562056
}).orElseThrow(() -> new NoResultException("Expected single result, but found none."));
20572057
}
20582058
}

storm-core/src/main/java/st/orm/core/spi/DefaultORMConverterImpl.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public List<Object> toDatabase(@Nullable Object record) throws SqlTemplateExcept
107107
D dbValue = converter.toDatabase(value);
108108
return singletonList(dbValue);
109109
} catch (Throwable e) {
110-
throw new SqlTemplateException(e);
110+
throw new SqlTemplateException("Failed to convert value to database format for field '%s.%s'.".formatted(field.type().getSimpleName(), field.name()), e);
111111
}
112112
}
113113

@@ -135,7 +135,7 @@ public Object fromDatabase(@Nonnull Object[] values,
135135
D dbValue = databaseType.cast(raw);
136136
return converter.fromDatabase(dbValue);
137137
} catch (Throwable e) {
138-
throw new SqlTemplateException(e);
138+
throw new SqlTemplateException("Failed to convert database value to entity format for field '%s.%s'.".formatted(field.type().getSimpleName(), field.name()), e);
139139
}
140140
}
141141
}

storm-core/src/main/java/st/orm/core/spi/DefaultORMConverterProviderImpl.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ private static List<ConverterEntry> scanDefaultConverters() {
9292
try {
9393
return (Converter<?, ?>) converterClass.getDeclaredConstructor().newInstance();
9494
} catch (ReflectiveOperationException e) {
95-
throw new PersistenceException("Failed to instantiate converter " + converterClass.getName(), e);
95+
throw new PersistenceException("Failed to instantiate converter %s. Ensure the converter class has a public no-argument constructor.".formatted(converterClass.getName()), e);
9696
}
9797
}
9898

@@ -205,9 +205,7 @@ private ORMConverter createExplicitConverter(@Nonnull RecordField field,
205205
ConverterTypes types = resolveConverterTypes(converterClass);
206206
if (types == null) {
207207
throw new PersistenceException(
208-
"Cannot resolve generic types for converter " + converterClass.getName() +
209-
" used on " + field.type().getName() + "." +
210-
field.name()
208+
"Cannot resolve generic types for converter %s used on %s.%s. Ensure the converter directly implements Converter<DatabaseType, EntityValueType> with concrete (non-generic) type arguments.".formatted(converterClass.getName(), field.type().getName(), field.name())
211209
);
212210
}
213211
@SuppressWarnings("unchecked")

storm-core/src/main/java/st/orm/core/template/impl/CacheableProcessor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ private static Object getObjectShape(@Nonnull Object object) throws SqlTemplateE
110110
*/
111111
private static Class<?> getTypeShape(@Nonnull Object object) throws SqlTemplateException {
112112
return switch (object) {
113-
case null -> throw new SqlTemplateException("Null object not allowed, use IS_NULL operator instead.");
113+
case null -> throw new SqlTemplateException("Null value not allowed as a direct parameter in a WHERE clause. To check for NULL, use the IS_NULL operator instead (e.g., where(field, IS_NULL)).");
114114
case Ref<?> ref -> ref.type();
115115
case Data data -> data.getClass();
116116
default -> CONSTANT_SHAPE;

storm-core/src/main/java/st/orm/core/template/impl/EnumMapper.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static java.util.Optional.empty;
1919

2020
import jakarta.annotation.Nonnull;
21+
import java.util.Arrays;
2122
import java.util.Optional;
2223
import st.orm.PersistenceException;
2324
import st.orm.core.template.SqlTemplateException;
@@ -62,7 +63,7 @@ public T newInstance(@Nonnull Object[] args) throws SqlTemplateException {
6263
if (arg instanceof Integer n) {
6364
return (T) getEnumFromOrdinal(type, n);
6465
}
65-
throw new SqlTemplateException("Invalid value '%s' for enum %s.".formatted(arg, type.getName()));
66+
throw new SqlTemplateException("Invalid value '%s' for enum %s. Valid values are: %s.".formatted(arg, type.getName(), Arrays.toString(type.getEnumConstants())));
6667
}
6768
});
6869
}
@@ -75,7 +76,7 @@ private static Enum<?> getEnumFromName(@Nonnull Class<?> enumType, @Nonnull Stri
7576
//noinspection unchecked,rawtypes
7677
return Enum.valueOf((Class<? extends Enum>) enumType, name);
7778
} catch (IllegalArgumentException e) {
78-
throw new SqlTemplateException("No enum constant %s for value '%s'.".formatted(enumType.getName(), name), e);
79+
throw new SqlTemplateException("No enum constant in %s matches the value '%s'. Valid constants are: %s.".formatted(enumType.getName(), name, Arrays.toString(enumType.getEnumConstants())), e);
7980
}
8081
}
8182

@@ -85,7 +86,7 @@ private static Enum<?> getEnumFromOrdinal(@Nonnull Class<?> enumType, @Nonnull I
8586
if (ordinal >= 0 && ordinal < enumConstants.length) {
8687
return enumConstants[ordinal];
8788
} else {
88-
throw new SqlTemplateException("Invalid ordinal '%d' for enum %s.".formatted(ordinal, enumType.getName()));
89+
throw new SqlTemplateException("Invalid ordinal %d for enum %s. Valid ordinals are 0 to %d.".formatted(ordinal, enumType.getName(), enumType.getEnumConstants().length - 1));
8990
}
9091
}
9192
}

storm-core/src/main/java/st/orm/core/template/impl/JoinProcessor.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ private String compileJoin(@Nonnull Join join, @Nonnull TemplateCompiler compile
108108
case TableTarget(var toTable) when join.source() instanceof TableSource(var fromTable) ->
109109
compileJoinCondition(fromTable, join.sourceAlias(), toTable, join.targetAlias(), compiler);
110110
case TemplateTarget ts -> compiler.compile(ts.template(), true);
111-
default -> throw new SqlTemplateException("Unsupported join target.");
111+
default -> throw new SqlTemplateException("Unsupported join target type: %s. Join targets must be either a table type (Class<? extends Data>) or a template expression.".formatted(join.target().getClass().getSimpleName()));
112112
} : "";
113113
final String clause = onClause.isEmpty() ? "" : " ON " + onClause;
114114
return switch (join.source()) {
@@ -147,7 +147,7 @@ private String compileJoinCondition(
147147
findPkField(toTable).orElseThrow(), compiler);
148148
}
149149
throw new SqlTemplateException(
150-
"Failed to join %s with %s. No matching foreign key found.".formatted(fromTable.getSimpleName(), toTable.getSimpleName()));
150+
"Failed to join %s with %s: no matching foreign key relationship found. Ensure one of the types has an @FK-annotated field referencing the other, or use an explicit ON clause with a template-based join.".formatted(fromTable.getSimpleName(), toTable.getSimpleName()));
151151
}
152152

153153
@SuppressWarnings("DuplicatedCode")
@@ -167,7 +167,7 @@ private String compileJoinCondition(
167167
var fkColumns = getForeignKeys(left, foreignKeyResolver, columnNameResolver);
168168
var pkColumns = getPrimaryKeys(right, foreignKeyResolver, columnNameResolver);
169169
if (fkColumns.size() != pkColumns.size()) {
170-
throw new SqlTemplateException("Mismatch in PK/FK columns between tables.");
170+
throw new SqlTemplateException("Mismatch in PK/FK column count between %s and %s: found %d foreign key column(s) but %d primary key column(s). Ensure the foreign key definition matches the referenced primary key structure.".formatted(toTable.getSimpleName(), fromTable.getSimpleName(), fkColumns.size(), pkColumns.size()));
171171
}
172172
StringBuilder joinCondition = new StringBuilder();
173173
for (int i = 0; i < fkColumns.size(); i++) {

0 commit comments

Comments
 (0)