From 32d6ef94ae4d053229c0243c512db293cac399b6 Mon Sep 17 00:00:00 2001 From: Leon van Zantvoort Date: Fri, 13 Mar 2026 20:23:32 +0100 Subject: [PATCH 1/2] Add constraint validation flag to @PK, @FK, and @UK annotations. (#87) --- docs/entities.md | 4 +- docs/validation.md | 52 +++++- .../template/impl/SchemaValidationError.java | 4 + .../core/template/impl/SchemaValidator.java | 46 +++++- .../core/template/SchemaValidatorTest.java | 154 ++++++++++++++++++ storm-foundation/src/main/java/st/orm/FK.java | 15 ++ storm-foundation/src/main/java/st/orm/PK.java | 15 ++ storm-foundation/src/main/java/st/orm/UK.java | 15 ++ 8 files changed, 295 insertions(+), 10 deletions(-) diff --git a/docs/entities.md b/docs/entities.md index c3bc2ea27..25e40e563 100644 --- a/docs/entities.md +++ b/docs/entities.md @@ -736,7 +736,9 @@ record Order(@PK Integer id, ## Suppressing Schema Validation -Use `@DbIgnore` to suppress [schema validation](configuration.md#schema-validation) for an entity or a specific field. This is useful for legacy tables, columns handled by [custom converters](converters.md), or known type mismatches that are safe at runtime. +To suppress constraint-specific warnings (missing primary key, foreign key, or unique constraint), use the `constraint` attribute on `@PK`, `@FK`, or `@UK`. This is more targeted than `@DbIgnore` because it only suppresses the constraint check while preserving all other validation (column existence, type compatibility, nullability). See [Constraint Validation](validation.md#constraint-validation) for details and examples. + +Use `@DbIgnore` to suppress [schema validation](configuration.md#schema-validation) for an entity or a specific field entirely. This is useful for legacy tables, columns handled by [custom converters](converters.md), or known type mismatches that are safe at runtime. diff --git a/docs/validation.md b/docs/validation.md index 6bb21ae0e..842e2ec27 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -72,10 +72,12 @@ Schema validation compares your entity and projection definitions against the ac | Each mapped column exists in the table | `COLUMN_NOT_FOUND` | Error | | Kotlin/Java type is compatible with the SQL column type | `TYPE_INCOMPATIBLE` | Error | | Entity primary key columns match the database primary key | `PRIMARY_KEY_MISMATCH` | Error | +| `@FK` constraint references the correct target table | `FOREIGN_KEY_MISMATCH` | Error | | Sequences referenced by `@PK(generation = SEQUENCE)` exist | `SEQUENCE_NOT_FOUND` | Error | | | | | | Numeric cross-category conversions (e.g., `Integer` mapped to `DECIMAL`) | `TYPE_NARROWING` | Warning | | Non-nullable entity field mapped to a nullable database column | `NULLABILITY_MISMATCH` | Warning | +| Entity declares `@PK` but the database has no primary key constraint | `PRIMARY_KEY_MISSING` | Warning | | `@UK` field has a matching unique constraint in the database | `UNIQUE_KEY_MISSING` | Warning | | `@FK` field has a matching foreign key constraint in the database | `FOREIGN_KEY_MISSING` | Warning | @@ -85,16 +87,60 @@ Schema validation compares your entity and projection definitions against the ac #### Constraint Validation -The `UNIQUE_KEY_MISSING` and `FOREIGN_KEY_MISSING` checks verify that the database has the constraints your entity model declares. These are warnings rather than errors because the ORM functions correctly without database-level enforcement: queries return the same results, inserts and updates succeed, and keyset pagination works as expected. +Schema validation checks that the database has the constraints your entity model declares. There are two categories of constraint findings: + +**Mismatches (errors)** occur when a constraint exists in the database but contradicts the entity definition. For example, if `@FK val city: City` expects a foreign key referencing the `city` table, but the database has a foreign key on that column referencing the `account` table, that is a `FOREIGN_KEY_MISMATCH`. Similarly, if the entity declares `@PK` with columns `(id)` but the database primary key is `(user_id, role_id)`, that is a `PRIMARY_KEY_MISMATCH`. Mismatches are always hard errors because they indicate a bug in the entity definition. + +**Missing constraints (warnings)** occur when the database has no constraint at all for a declared `@PK`, `@FK`, or `@UK` field. These are warnings rather than errors because the ORM functions correctly without database-level enforcement: queries return the same results, inserts and updates succeed, and keyset pagination works as expected. However, database constraints serve as a safety net that the application layer cannot replace: +- **Primary key constraints** ensure row uniqueness at the database level. Without one, duplicate primary key values could be inserted by other applications or direct SQL. - **Unique constraints** protect against application bugs and concurrent modifications that could insert duplicate values. Without a database-level unique constraint, a `@UK` field might contain duplicates that go undetected until a `findBy` call unexpectedly returns multiple results. - **Foreign key constraints** protect referential integrity. Without a database-level foreign key constraint, orphaned rows can accumulate when referenced rows are deleted. -In [strict mode](#strict-mode), these warnings are promoted to errors, causing validation to fail if the constraints are missing. Use `@DbIgnore` to suppress these warnings for fields where the missing constraint is intentional (for example, when using application-level deduplication or soft deletes that make database constraints impractical). +##### Suppressing Constraint Warnings + +When the database intentionally omits a constraint (for performance, for views, or because integrity is enforced at the application level), use the `constraint` attribute to suppress the warning for that specific field: + + + + +```kotlin +// No FK constraint for performance reasons. +data class Order( + @PK val id: Int = 0, + @FK(constraint = false) val customer: Customer +) : Entity + +// No unique index in the database. +data class User( + @PK val id: Int = 0, + @UK(constraint = false) val email: String +) : Entity +``` + + + + +```java +// No FK constraint for performance reasons. +record Order(@PK Integer id, + @FK(constraint = false) Customer customer +) implements Entity {} + +// No unique index in the database. +record User(@PK Integer id, + @UK(constraint = false) String email +) implements Entity {} +``` + + + + +Setting `constraint = false` only suppresses the "missing" warning. If the database *does* have a constraint that contradicts the entity definition (a mismatch), it is always reported as a hard error regardless of this flag. -Use `@DbIgnore` to exclude entity fields from schema validation. See [Entities](entities.md) for annotation details. +In [strict mode](#strict-mode), missing constraint warnings are promoted to errors, causing validation to fail. The `constraint = false` flag takes precedence: fields marked with it are excluded from validation even in strict mode. ### Programmatic API diff --git a/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidationError.java b/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidationError.java index d90ab7ed6..a412b7bbd 100644 --- a/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidationError.java +++ b/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidationError.java @@ -47,10 +47,14 @@ public enum ErrorKind { NULLABILITY_MISMATCH(true), /** The primary key columns in the entity do not match the database primary key. */ PRIMARY_KEY_MISMATCH, + /** The entity declares a {@code @PK} but the database table has no primary key constraint. @since 1.10 */ + PRIMARY_KEY_MISSING(true), /** A sequence referenced by the entity does not exist in the database. */ SEQUENCE_NOT_FOUND, /** A {@code @UK} field does not have a matching unique constraint in the database. */ UNIQUE_KEY_MISSING(true), + /** A {@code @FK} field has a foreign key constraint that references a different table than expected. @since 1.10 */ + FOREIGN_KEY_MISMATCH, /** A {@code @FK} field does not have a matching foreign key constraint in the database. */ FOREIGN_KEY_MISSING(true); diff --git a/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidator.java b/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidator.java index 23b20e9f9..934b6e4e8 100644 --- a/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidator.java +++ b/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidator.java @@ -418,12 +418,24 @@ private void validateType( .filter(Column::primaryKey) .map(column -> column.name().toUpperCase()) .collect(Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER))); - Set dbPkColumns = schema.getPrimaryKeys(tableName).stream() .map(pk -> pk.columnName().toUpperCase()) .collect(Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER))); - - if (!dbPkColumns.isEmpty() && !entityPkColumns.equals(dbPkColumns)) { + if (dbPkColumns.isEmpty() && !entityPkColumns.isEmpty()) { + // No PK constraint in the database. Only warn if @PK(constraint = true). + boolean validatePkConstraint = model.recordType().fields().stream() + .filter(field -> field.isAnnotationPresent(PK.class)) + .findFirst() + .map(field -> field.getAnnotation(PK.class)) + .map(PK::constraint) + .orElse(true); + if (validatePkConstraint) { + errors.add(new SchemaValidationError(type, ErrorKind.PRIMARY_KEY_MISSING, + "No primary key constraint found in table '%s', but entity defines primary key columns %s." + .formatted(qualifiedTableName, entityPkColumns))); + } + } else if (!dbPkColumns.isEmpty() && !entityPkColumns.equals(dbPkColumns)) { + // PK constraint exists but differs: always a hard error regardless of constraint flag. errors.add(new SchemaValidationError(type, ErrorKind.PRIMARY_KEY_MISMATCH, "Primary key mismatch for table '%s': entity defines %s, database has %s." .formatted(qualifiedTableName, entityPkColumns, dbPkColumns))); @@ -484,6 +496,11 @@ private void validateUniqueKeys( if (ignoredComponents.contains(field.name())) { continue; } + // Skip if the @UK annotation indicates no constraint is expected. + UK ukAnnotation = field.getAnnotation(UK.class); + if (ukAnnotation != null && !ukAnnotation.constraint()) { + continue; + } // Collect the expected column names for this @UK field. SortedSet expectedColumns = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); for (Column column : model.declaredColumns()) { @@ -577,15 +594,32 @@ private void validateForeignKeys( if (fkColumnNames.isEmpty()) { continue; } + // Check if the @FK annotation requests constraint validation. + FK fkAnnotation = field.getAnnotation(FK.class); + boolean validateFkConstraint = fkAnnotation == null || fkAnnotation.constraint(); // Check if the database has matching FK constraints for each FK column. for (String fkColumnName : fkColumnNames) { + // Check for an exact match (correct column and correct target table). boolean found = dbForeignKeys.stream() .anyMatch(fk -> fk.fkColumnName().equalsIgnoreCase(fkColumnName) && fk.pkTableName().equalsIgnoreCase(targetTableName)); if (!found) { - errors.add(new SchemaValidationError(type, ErrorKind.FOREIGN_KEY_MISSING, - "No foreign key constraint found on column '%s' in table '%s' referencing table '%s'." - .formatted(fkColumnName, qualifiedTableName, targetTableName))); + // Check if there is a FK constraint on this column that references a different table. + Optional mismatch = dbForeignKeys.stream() + .filter(fk -> fk.fkColumnName().equalsIgnoreCase(fkColumnName)) + .findFirst(); + if (mismatch.isPresent()) { + // FK constraint exists but points to the wrong table: always a hard error. + errors.add(new SchemaValidationError(type, ErrorKind.FOREIGN_KEY_MISMATCH, + "Foreign key mismatch on column '%s' in table '%s': entity expects reference to table '%s', but database references table '%s'." + .formatted(fkColumnName, qualifiedTableName, targetTableName, + mismatch.get().pkTableName()))); + } else if (validateFkConstraint) { + // No FK constraint at all: only warn if constraint validation is enabled. + errors.add(new SchemaValidationError(type, ErrorKind.FOREIGN_KEY_MISSING, + "No foreign key constraint found on column '%s' in table '%s' referencing table '%s'." + .formatted(fkColumnName, qualifiedTableName, targetTableName))); + } } } } diff --git a/storm-core/src/test/java/st/orm/core/template/SchemaValidatorTest.java b/storm-core/src/test/java/st/orm/core/template/SchemaValidatorTest.java index 34689466d..9afb97ea2 100644 --- a/storm-core/src/test/java/st/orm/core/template/SchemaValidatorTest.java +++ b/storm-core/src/test/java/st/orm/core/template/SchemaValidatorTest.java @@ -151,6 +151,52 @@ public record FieldIgnoredEntity( @DbIgnore @Nonnull LocalDate description // type mismatch, but ignored ) implements Entity {} + // Constraint flag test entities + + public record PrimaryKeyMissingEntity( + @PK Integer id, + @Nonnull String name + ) implements Entity {} + + public record PrimaryKeyMissingNoConstraintEntity( + @PK(constraint = false) Integer id, + @Nonnull String name + ) implements Entity {} + + public record ForeignKeyMismatchTarget( + @PK Integer id, + @Nonnull String name + ) implements Entity {} + + public record WrongForeignKeyTarget( + @PK Integer id, + @Nonnull String label + ) implements Entity {} + + public record ForeignKeyMismatchEntity( + @PK Integer id, + @Nonnull String name, + @Nullable @FK Ref foreignKeyMismatchTarget + ) implements Entity {} + + public record ForeignKeyNoConstraintEntity( + @PK Integer id, + @Nonnull String name, + @Nullable @FK(constraint = false) Ref foreignKeyTarget + ) implements Entity {} + + public record ForeignKeyMismatchNoConstraintEntity( + @PK Integer id, + @Nonnull String name, + @Nullable @FK(constraint = false) Ref foreignKeyMismatchTarget + ) implements Entity {} + + public record UniqueKeyNoConstraintEntity( + @PK Integer id, + @UK(constraint = false) @Nonnull String email, + @Nonnull String name + ) implements Entity {} + // UK/FK validation test entities public record UniqueKeyEntity( @@ -634,6 +680,114 @@ void testPolymorphicFkSkipsValidation() throws SQLException { "Expected no FOREIGN_KEY_MISSING for polymorphic FK, but got: " + errors); } + // Primary key missing tests + + @Test + void testPrimaryKeyMissing() throws SQLException { + // Table has no PRIMARY KEY constraint. + execute("CREATE TABLE primary_key_missing_entity (id INTEGER NOT NULL, name VARCHAR(255) NOT NULL)"); + + List errors = SchemaValidator.of(dataSource) + .validate(List.of(PrimaryKeyMissingEntity.class)); + + assertTrue(errors.stream().anyMatch(error -> error.kind() == ErrorKind.PRIMARY_KEY_MISSING), + "Expected PRIMARY_KEY_MISSING when DB has no PK constraint, but got: " + errors); + // Should be a warning, not an error. + assertTrue(errors.stream() + .filter(error -> error.kind() == ErrorKind.PRIMARY_KEY_MISSING) + .allMatch(error -> error.kind().warning())); + } + + @Test + void testPrimaryKeyMissingSuppressedByConstraintFalse() throws SQLException { + // Table has no PRIMARY KEY constraint. + execute("CREATE TABLE primary_key_missing_no_constraint_entity (id INTEGER NOT NULL, name VARCHAR(255) NOT NULL)"); + + List errors = SchemaValidator.of(dataSource) + .validate(List.of(PrimaryKeyMissingNoConstraintEntity.class)); + + assertFalse(errors.stream().anyMatch(error -> error.kind() == ErrorKind.PRIMARY_KEY_MISSING), + "Expected no PRIMARY_KEY_MISSING with @PK(constraint = false), but got: " + errors); + } + + // Foreign key mismatch tests + + @Test + void testForeignKeyMismatch() throws SQLException { + // Create the wrong target table and the entity's expected target. + execute("CREATE TABLE wrong_foreign_key_target (id INTEGER AUTO_INCREMENT, label VARCHAR(255) NOT NULL, PRIMARY KEY (id))"); + execute("CREATE TABLE foreign_key_mismatch_target (id INTEGER AUTO_INCREMENT, name VARCHAR(255) NOT NULL, PRIMARY KEY (id))"); + // FK constraint references wrong_foreign_key_target instead of foreign_key_mismatch_target. + execute("CREATE TABLE foreign_key_mismatch_entity (" + + "id INTEGER AUTO_INCREMENT, " + + "name VARCHAR(255) NOT NULL, " + + "foreign_key_mismatch_target_id INTEGER, " + + "PRIMARY KEY (id), " + + "FOREIGN KEY (foreign_key_mismatch_target_id) REFERENCES wrong_foreign_key_target(id))"); + + List errors = SchemaValidator.of(dataSource) + .validate(List.of(ForeignKeyMismatchEntity.class)); + + assertTrue(errors.stream().anyMatch(error -> error.kind() == ErrorKind.FOREIGN_KEY_MISMATCH), + "Expected FOREIGN_KEY_MISMATCH when FK points to wrong table, but got: " + errors); + // Should be a hard error, not a warning. + assertFalse(errors.stream() + .filter(error -> error.kind() == ErrorKind.FOREIGN_KEY_MISMATCH) + .anyMatch(error -> error.kind().warning())); + } + + @Test + void testForeignKeyMismatchNotSuppressedByConstraintFalse() throws SQLException { + // FK mismatch should still be reported even with @FK(constraint = false). + execute("CREATE TABLE wrong_foreign_key_target (id INTEGER AUTO_INCREMENT, label VARCHAR(255) NOT NULL, PRIMARY KEY (id))"); + execute("CREATE TABLE foreign_key_mismatch_target (id INTEGER AUTO_INCREMENT, name VARCHAR(255) NOT NULL, PRIMARY KEY (id))"); + execute("CREATE TABLE foreign_key_mismatch_no_constraint_entity (" + + "id INTEGER AUTO_INCREMENT, " + + "name VARCHAR(255) NOT NULL, " + + "foreign_key_mismatch_target_id INTEGER, " + + "PRIMARY KEY (id), " + + "FOREIGN KEY (foreign_key_mismatch_target_id) REFERENCES wrong_foreign_key_target(id))"); + + List errors = SchemaValidator.of(dataSource) + .validate(List.of(ForeignKeyMismatchNoConstraintEntity.class)); + + assertTrue(errors.stream().anyMatch(error -> error.kind() == ErrorKind.FOREIGN_KEY_MISMATCH), + "Expected FOREIGN_KEY_MISMATCH even with @FK(constraint = false), but got: " + errors); + } + + // Constraint flag suppression tests + + @Test + void testForeignKeyMissingSuppressedByConstraintFalse() throws SQLException { + execute("CREATE TABLE foreign_key_target (id INTEGER AUTO_INCREMENT, name VARCHAR(255) NOT NULL, PRIMARY KEY (id))"); + execute("CREATE TABLE foreign_key_no_constraint_entity (" + + "id INTEGER AUTO_INCREMENT, " + + "name VARCHAR(255) NOT NULL, " + + "foreign_key_target_id INTEGER, " + + "PRIMARY KEY (id))"); + + List errors = SchemaValidator.of(dataSource) + .validate(List.of(ForeignKeyNoConstraintEntity.class)); + + assertFalse(errors.stream().anyMatch(error -> error.kind() == ErrorKind.FOREIGN_KEY_MISSING), + "Expected no FOREIGN_KEY_MISSING with @FK(constraint = false), but got: " + errors); + } + + @Test + void testUniqueKeyMissingSuppressedByConstraintFalse() throws SQLException { + execute("CREATE TABLE unique_key_no_constraint_entity (" + + "id INTEGER AUTO_INCREMENT, " + + "email VARCHAR(255) NOT NULL, " + + "name VARCHAR(255) NOT NULL, " + + "PRIMARY KEY (id))"); + + List errors = SchemaValidator.of(dataSource) + .validate(List.of(UniqueKeyNoConstraintEntity.class)); + + assertFalse(errors.stream().anyMatch(error -> error.kind() == ErrorKind.UNIQUE_KEY_MISSING), + "Expected no UNIQUE_KEY_MISSING with @UK(constraint = false), but got: " + errors); + } + // Helpers private void execute(String sql) throws SQLException { diff --git a/storm-foundation/src/main/java/st/orm/FK.java b/storm-foundation/src/main/java/st/orm/FK.java index 4e78cae7c..1a2068166 100644 --- a/storm-foundation/src/main/java/st/orm/FK.java +++ b/storm-foundation/src/main/java/st/orm/FK.java @@ -131,4 +131,19 @@ * The database column name for the foreign key. Acts as an alias for {@link #value()}. */ String name() default ""; + + /** + * Indicates whether a corresponding foreign key constraint is expected to exist in the database. + * + *

When {@code true} (the default), schema validation will warn if no matching foreign key constraint is found + * in the database. Set to {@code false} when the database intentionally omits the foreign key constraint, for + * example for performance reasons or because referential integrity is enforced at the application level.

+ * + *

Setting this to {@code false} only suppresses the constraint check during schema validation. The field is + * still fully functional as a foreign key for Storm's query generation and relationship resolution.

+ * + * @return {@code true} if the foreign key constraint is expected in the database, {@code false} to skip the check. + * @since 1.10 + */ + boolean constraint() default true; } diff --git a/storm-foundation/src/main/java/st/orm/PK.java b/storm-foundation/src/main/java/st/orm/PK.java index 21bb9c279..ddf438a36 100644 --- a/storm-foundation/src/main/java/st/orm/PK.java +++ b/storm-foundation/src/main/java/st/orm/PK.java @@ -131,4 +131,19 @@ * @return the sequence name. */ String sequence() default ""; + + /** + * Indicates whether a corresponding primary key constraint is expected to exist in the database. + * + *

When {@code true} (the default), schema validation will check that the database table has a primary key + * constraint matching the entity's primary key columns. Set to {@code false} when the database intentionally + * omits the primary key constraint, for example for legacy tables without constraints.

+ * + *

Setting this to {@code false} only suppresses the constraint check during schema validation. The field is + * still fully functional as a primary key for Storm's identity resolution and query generation.

+ * + * @return {@code true} if the primary key constraint is expected in the database, {@code false} to skip the check. + * @since 1.10 + */ + boolean constraint() default true; } diff --git a/storm-foundation/src/main/java/st/orm/UK.java b/storm-foundation/src/main/java/st/orm/UK.java index dafdf7f72..c0f50faab 100644 --- a/storm-foundation/src/main/java/st/orm/UK.java +++ b/storm-foundation/src/main/java/st/orm/UK.java @@ -121,4 +121,19 @@ * @since 1.9 */ boolean nullsDistinct() default true; + + /** + * Indicates whether a corresponding unique constraint is expected to exist in the database. + * + *

When {@code true} (the default), schema validation will warn if no matching unique constraint is found + * in the database. Set to {@code false} when the database intentionally omits the unique constraint, for example + * because uniqueness is enforced at the application level.

+ * + *

Setting this to {@code false} only suppresses the constraint check during schema validation. The field is + * still fully functional as a unique key for keyset pagination and unique lookups.

+ * + * @return {@code true} if the unique constraint is expected in the database, {@code false} to skip the check. + * @since 1.10 + */ + boolean constraint() default true; } From f4777e82a9530b1f49aa162324da8f8a24409ca2 Mon Sep 17 00:00:00 2001 From: Leon van Zantvoort Date: Sat, 14 Mar 2026 07:31:06 +0100 Subject: [PATCH 2/2] Improve error reporting. (#84) --- .../orm/core/template/impl/SchemaValidationException.java | 3 ++- .../java/st/orm/core/template/impl/SchemaValidator.java | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidationException.java b/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidationException.java index da3fa94ee..931c88f62 100644 --- a/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidationException.java +++ b/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidationException.java @@ -52,6 +52,7 @@ private static String formatMessage(@Nonnull List errors) return "Schema validation failed with %d error(s):\n".formatted(errors.size()) + errors.stream() .map(error -> " - " + error.toString()) - .collect(Collectors.joining("\n")); + .collect(Collectors.joining("\n")) + + "\nIf intentional, use @DbIgnore to exclude specific types or fields from validation."; } } diff --git a/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidator.java b/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidator.java index 934b6e4e8..7e1e27467 100644 --- a/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidator.java +++ b/storm-core/src/main/java/st/orm/core/template/impl/SchemaValidator.java @@ -282,7 +282,7 @@ private static int countTypes(@Nonnull Iterable types) { } static String formatErrors(@Nonnull List errors) { - return "Schema validation failed with %d error(s):\n%s".formatted( + return "Schema validation failed with %d error(s):\n%s\nIf intentional, use @DbIgnore to exclude specific types or fields from validation.".formatted( errors.size(), String.join("\n", errors.stream().map(e -> " - " + e).toList())); } @@ -431,7 +431,7 @@ private void validateType( .orElse(true); if (validatePkConstraint) { errors.add(new SchemaValidationError(type, ErrorKind.PRIMARY_KEY_MISSING, - "No primary key constraint found in table '%s', but entity defines primary key columns %s." + "No primary key constraint found in table '%s', but entity defines primary key columns %s. If intentional, use @PK(constraint = false) to suppress this check." .formatted(qualifiedTableName, entityPkColumns))); } } else if (!dbPkColumns.isEmpty() && !entityPkColumns.equals(dbPkColumns)) { @@ -520,7 +520,7 @@ private void validateUniqueKeys( ? "column '%s'".formatted(expectedColumns.first()) : "columns %s".formatted(expectedColumns); errors.add(new SchemaValidationError(type, ErrorKind.UNIQUE_KEY_MISSING, - "No unique constraint found on %s in table '%s' for @UK field '%s'." + "No unique constraint found on %s in table '%s' for @UK field '%s'. If intentional, use @UK(constraint = false) to suppress this check." .formatted(columnDescription, qualifiedTableName, field.name()))); } } @@ -617,7 +617,7 @@ private void validateForeignKeys( } else if (validateFkConstraint) { // No FK constraint at all: only warn if constraint validation is enabled. errors.add(new SchemaValidationError(type, ErrorKind.FOREIGN_KEY_MISSING, - "No foreign key constraint found on column '%s' in table '%s' referencing table '%s'." + "No foreign key constraint found on column '%s' in table '%s' referencing table '%s'. If intentional, use @FK(constraint = false) to suppress this check." .formatted(fkColumnName, qualifiedTableName, targetTableName))); } }