Fix 16133: Unset default nullable domain property passes validation but fails at the database - #16136
Fix 16133: Unset default nullable domain property passes validation but fails at the database#16136matrei wants to merge 7 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #16136 +/- ##
==================================================
+ Coverage 52.5503% 52.6495% +0.0992%
- Complexity 18391 18442 +51
==================================================
Files 2037 2037
Lines 96498 96508 +10
Branches 16860 16862 +2
==================================================
+ Hits 50710 50811 +101
+ Misses 38353 38268 -85
+ Partials 7435 7429 -6
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR addresses #16133 by aligning GORM/Hibernate schema mapping nullability with Grails 8’s “nullable-by-default” validation behavior, preventing cases where validation passes but the database rejects inserts due to NOT NULL columns.
Changes:
- Plumb
grails.gorm.default.nullable(viaConnectionSourceSettings.DefaultSettings.nullable) into Hibernate mapping so generated column nullability matches the configured validation default. - Add an integration reproducer/spec for saving a domain instance with an unset unconstrained property.
- Update multiple test/example domain classes and GraphQL-related tests to make requiredness explicit with
nullable: false.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| grails-test-examples/hibernate5/grails-hibernate/src/integration-test/groovy/functional/tests/OrganizationValidationSpec.groovy | Adds an integration spec reproducing the nullable-by-default vs DB mismatch scenario. |
| grails-test-examples/hibernate5/grails-hibernate/grails-app/domain/functional/tests/Organization.groovy | Adds a minimal domain used by the new integration spec. |
| grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/UserIntegrationSpec.groovy | Adjusts GraphQL integration assertions around required embedded inputs. |
| grails-test-examples/graphql/grails-test-app/grails-app/domain/grails/test/app/UserRole.groovy | Makes user/role required explicitly via constraints. |
| grails-test-examples/graphql/grails-test-app/grails-app/domain/grails/test/app/User.groovy | Makes embedded profile/address required explicitly and aligns GraphQL mapping. |
| grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/config/AbstractGormMappingFactory.java | Introduces configurable default nullability for mapped forms. |
| grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContext.java | Wires default.nullable into the Hibernate mapping factory (Hibernate 7). |
| grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContext.java | Wires default.nullable into the Hibernate mapping factory (Hibernate 5). |
| grails-data-hibernate7/dbmigration/src/test-cli/groovy/org/apache/grails/data/hibernate7/dbmigration/cli/ApplicationContextDatabaseMigrationCommandSpec.groovy | Updates test domain constraints to be explicit about requiredness. |
| grails-data-hibernate5/dbmigration/src/test-cli/groovy/org/apache/grails/data/hibernate5/dbmigration/cli/ApplicationContextDatabaseMigrationCommandSpec.groovy | Updates test domain constraints to be explicit about requiredness. |
| grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/GrailsHibernatePersistentEntitySpec.groovy | Makes embedded property required explicitly in test domain. |
| grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/EnumTypeBinderSpec.groovy | Adds explicit non-null constraint for enum property in test entity. |
| grails-data-graphql/core/src/test/groovy/org/grails/gorm/graphql/types/input/EmbeddedInputObjectTypeBuilderSpec.groovy | Updates expectations for default nullability in GraphQL embedded input properties. |
| grails-data-graphql/core/src/test/groovy/org/grails/gorm/graphql/SchemaSpec.groovy | Adjusts schema assertions for input types (nullable wrapping expectations). |
| grails-data-graphql/core/src/test/groovy/org/grails/gorm/graphql/entity/property/impl/HibernatePersistentGraphQLPropertySpec.groovy | Updates test domain constraints/mapping to be explicit about nullability. |
Suppressed comments (3)
grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/UserIntegrationSpec.groovy:81
- Same issue here: the test case description says the profile is missing a required field, but the assertion no longer checks that the request is rejected. This weakens coverage and can mask regressions in GraphQL input validation / domain validation.
then:
obj.data.userCreate != null
grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/UserIntegrationSpec.groovy:104
- This assertion no longer verifies the behavior implied by the test name (missing required
addressshould fail). Please assert the mutation is rejected and that an error mentionsaddressto keep this test meaningful.
then:
obj.data.userCreate != null
grails-test-examples/graphql/grails-test-app/src/integration-test/groovy/grails/test/app/UserIntegrationSpec.groovy:129
- This assertion no longer checks the expected failure when a required
zipfield is missing. Without asserting an error (and/or thatuserCreateis null), the test can pass even if validation is broken.
then:
obj.data.userCreate != null
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
jdaugherty
left a comment
There was a problem hiding this comment.
My only concern is shouldn't the default in AbstractGormMappingFactory match the gorm default?
✅ All tests passed ✅🏷️ Commit: 07a30bb Learn more about TestLens at testlens.app. |
@jdaugherty I have changed |
codeconsole
left a comment
There was a problem hiding this comment.
nice fix, 2 other cases?
createMappedForm(PersistentProperty) has three return paths and the fix only touches one — the else branch where the property has no config at all. The two it misses are the common ones, and both reproduce #16133 verbatim:
1. Any property named in static constraints with a non-nullable constraint. Both the mapping and constraints closures are evaluated into the same builder
(AbstractGormMappingFactory.java:99-112), and HibernateMappingBuilder.handlePropertyInternal writes new PropertyConfig() (nullable=false) into mapping.columns unconditionally, overwriting nullable only if (namedArgs.nullable instanceof Boolean). So adding name maxSize: 255 to the PR's own new Organization fixture brings the bug straight back — validate() passes, save() throws. Most real domain classes have a constraints block.
2. A '*' wildcard. The clone path (:195-202) never applies defaultNullable either, so grails.gorm.default.mapping = { '*'(cache: true) } silently reverts every column in the app to NOT NULL. The documented legacy switch '*'(nullable: false) works through this same path — but only by accident.
Fix for both: seed the default at creation time in the builders, or track "nullable was explicitly configured" so the factory fills only the gap (which is how the validation side already reasons, via hasAppliedConstraint).
3. The upgrade guide contradicts the code. upgrading80x.adoc:1237-1238 currently states verbatim that the Grails 8 change "is a validation-layer change only. Column/DDL nullability … is unaffected." This PR makes that false, with no doc change in the diff — and the schema impact is real for dbCreate: validate/update and Liquibase gormDiff.
Also flagged: setDefaultNullable(false) has no test (NullableByDefaultSpec is the natural home), the regression spec lands only under hibernate5 while #16133 is a Hibernate 7 stack trace, the GrailsWebDataBinder guard removal widens behaviour with no coverage, and composite-id components now default to nullable — the PR's own fixtures had to add nullable: false to UserRole and Book2, which is a signal real apps will need the same edit.
Closes #16133