Skip to main content
Version: Next

Schema Validation

This page covers checking entity definitions against the database. To check the values an application writes, see Validating Data.

Storm validates your entity and projection definitions at two levels: structural validation ensures your records follow the ORM's rules (valid primary key types, correct use of annotations, no circular dependencies), while schema validation compares your definitions against the actual database to catch mismatches before they surface as runtime errors.

Both levels are optional and configurable. Structural validation runs automatically on first use; schema validation must be explicitly enabled.


Record Validation​

When Storm first encounters an entity or projection type, it inspects the record structure and validates that the definition is well-formed. This catches common modeling mistakes early, at startup rather than at query time.

What Gets Checked​

Primary key rules:

  • The @PK type must be one of: boolean, int, long, short, String, UUID, BigInteger, LocalDate, LocalDateTime, Instant, Enum, or Ref. Floating-point types (float, double, BigDecimal) are rejected because they cannot reliably serve as identity values. OffsetDateTime and ZonedDateTime are rejected because the database does not store the offset, so a key would not equal itself after a read. The legacy java.util.Date family is rejected because it is mutable.
  • Compound keys (inline records annotated with @PK) follow the same type restrictions for each component.
  • A temporal key must hold a value at the precision the column stores. LocalDateTime.now() carries nanoseconds that a microsecond column drops, after which an update or delete by that entity misses its row. Truncate before constructing the entity: Instant.now().truncatedTo(ChronoUnit.MICROS).

Foreign key rules:

  • Fields annotated with @FK must be a Data type (entity, projection, or data class with a @PK) or a Ref wrapping such a type. Scalars like String or Integer cannot be foreign keys.
  • Auto-generated foreign keys (@FK(generation = ...)) cannot be inlined.

Inline component rules:

  • Fields annotated with @Inline must be record types. Scalars cannot be inlined.
  • Inline records must not declare their own @PK, since they are embedded within a parent entity.

Version fields:

  • At most one field per entity can be annotated with @Version. Multiple version fields are rejected.

Structural integrity:

  • Records must be immutable. Mutable fields (Kotlin var) are rejected.
  • Entities or projections that contain other entities or projections must annotate them as @FK or @Inline. Storm needs to know the relationship type to generate correct SQL.
  • The record graph is checked for cycles. If entity A inlines entity B, which inlines entity A, the circular dependency is reported.

Configuration​

Record validation runs by default and causes startup to fail on the first error. The record-mode property controls this behavior:

ValueBehavior
failValidation errors cause startup to fail (default).
warnErrors are logged as warnings; startup continues.
noneRecord validation is skipped entirely.

This can be set as a system property, via StormConfig, or in Spring Boot's application.yml:

storm:
validation:
record-mode: fail # or "warn" or "none" (default: fail)

Schema Validation​

Schema validation compares your entity and projection definitions against the actual database schema. It catches mismatches before they surface as runtime errors, similar to Hibernate's ddl-auto=validate. Storm never modifies the schema; it only reports mismatches.

What Gets Checked​

CheckError KindSeverity
Table exists in the databaseTABLE_NOT_FOUNDError
Each mapped column exists in the tableCOLUMN_NOT_FOUNDError
Kotlin/Java type is compatible with the SQL column typeTYPE_INCOMPATIBLEError
Entity primary key columns match the database primary keyPRIMARY_KEY_MISMATCHError
@FK constraint references the correct target tableFOREIGN_KEY_MISMATCHError
Sequences referenced by @PK(generation = SEQUENCE) existSEQUENCE_NOT_FOUNDError
Numeric cross-category conversions (e.g., Integer mapped to DECIMAL)TYPE_NARROWINGWarning
Non-nullable entity field mapped to a nullable database columnNULLABILITY_MISMATCHWarning
Entity declares @PK but the database has no primary key constraintPRIMARY_KEY_MISSINGWarning
@UK field has a matching unique constraint or unique index in the databaseUNIQUE_KEY_MISSINGWarning
@FK field has a matching foreign key constraint in the databaseFOREIGN_KEY_MISSINGWarning
A NOT NULL column without a default that no field maps, and that the database does not generate; a query column of an entity counts, since every insert leaves it outUNMAPPED_COLUMNWarning

Errors indicate definitive mismatches that will cause runtime failures, such as missing tables or columns.

Warnings indicate situations where the mapping works at runtime but may involve subtle differences, such as precision loss when mapping a Kotlin Int to an Oracle NUMBER column. Warnings are logged but do not cause validation to fail (unless strict mode is enabled).

An entity can map to any relation the database reports, whatever type name its driver gives it: a table, a view, a materialized view, a partitioned table or a foreign table. System objects, sequences, indexes, types and temporary tables are not looked up. A view declares no column NOT NULL, so a non-nullable field on a view column is not reported as a nullability mismatch.

Constraint Validation​

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 scrolling 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 or unique index, 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.
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:

// No FK constraint for performance reasons.
data class Order(
@PK val id: Int = 0,
@FK(constraint = false) val customer: Customer
) : Entity<Int>

// No unique index in the database.
data class User(
@PK val id: Int = 0,
@UK(constraint = false) val email: String
) : Entity<Int>

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.

In 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​

Any ORMTemplate created from a DataSource supports schema validation:

val orm = dataSource.orm

// Inspect errors programmatically
val errors: List<String> = orm.validateSchema()

// Or validate and throw on failure
orm.validateSchemaOrThrow()

Both methods have overloads that accept specific types to validate:

orm.validateSchema(User::class, Order::class)

The no-argument variants discover all entity and projection types on the classpath automatically.

On success, a confirmation message is logged at INFO level. On failure, each error is logged at ERROR level, and validateSchemaOrThrow() throws a PersistenceException with a summary of all errors. Warnings are always logged at WARN level regardless of the outcome.

Validation also loads the schema: Storm keeps what it read for the data source and uses it for advice and optimization, such as naming an index a slow statement lacks, to resolve an integrity violation to the entity's fields, and for validate. To load the schema without validating it, call orm.loadSchema(). Loading is optional, and each call reads the schema anew. See Loading the schema.

Templates created from a raw Connection or JPA EntityManager do not support schema validation, since they lack the DataSource needed to query database metadata.

Strict Mode​

By default, warnings (type narrowing and nullability mismatches) do not cause validation to fail. In strict mode, all findings are treated as errors:

val config = StormConfig.of(mapOf(VALIDATION_STRICT to "true"))
val orm = ORMTemplate.of(dataSource, config)
orm.validateSchemaOrThrow() // Warnings now cause failure

Suppressing Validation with @DbIgnore​

Use @DbIgnore to suppress schema validation for specific entities or fields. This is useful for legacy tables, columns handled by custom converters, or known mismatches that are safe to ignore.

Suppress validation for an entire entity:

@DbIgnore
data class LegacyUser(
@PK val id: Int = 0,
val name: String
) : Entity<Int>

Suppress validation for a specific field:

data class User(
@PK val id: Int = 0,
val name: String,
@DbIgnore("DB uses FLOAT, but column only stores whole numbers")
val age: Int
) : Entity<Int>

The optional value parameter documents why the mismatch is acceptable. When @DbIgnore is placed on an inline component field, validation is suppressed for all columns within that component.

Custom Schemas​

Schema validation respects @DbTable(schema = "..."). Each entity is validated against the schema specified in its annotation, or the connection's default schema if none is specified.

@DbTable(schema = "reporting")
data class Report(
@PK val id: Int = 0,
val name: String
) : Entity<Int>

Spring Boot Configuration​

When using the Spring Boot Starter, both record and schema validation can be configured through application.yml:

storm:
validation:
record-mode: fail # or "warn" or "none" (default: fail)
schema-mode: fail # or "warn", "load" or "none" (default: fail)
strict: false # treat schema warnings as errors (default: false)

The schema-mode values:

ValueBehavior
failMismatches cause startup to fail with a PersistenceException (default).
warnMismatches are logged at WARN level; startup continues.
loadThe schema is loaded without being validated, which Storm uses for advice and optimization. See Loading the schema.
noneSchema validation is skipped, and the schema is not loaded: validate refuses to run, and a constraint resolves to no field.

Validation loads the schema as it reads it, so fail and warn also give Storm the knowledge load gives it.

Configuration Properties​

PropertyDefaultDescription
storm.validation.record_modefailRecord validation mode: fail, warn, or none
storm.validation.schema_modefailSchema validation mode: none, load, warn, or fail (Spring Boot and Ktor)
storm.validation.strictfalseWhen true, schema validation warnings are treated as errors

Mode values are matched case-insensitively after trimming, and a blank value means the default. Any other value is a configuration error: Storm fails fast with an error naming the property, the given value, and the valid values, so a typo cannot silently change validation behavior.