fix!: derive KeyDelegate nullability from the underlying field - #437
Merged
Conversation
Metamodel.key() only wraps metamodels that do not carry the Key marker, so KeyDelegate.isNullable()'s instanceof check on the delegate could never match: every factory-built key reported non-nullable, and keyset pagination accepted nullable cursor keys that silently skip NULL rows. KeyDelegate now derives nullability from the record field the metamodel designates, through a new MetamodelFactory.isNullable bridged via MetamodelHelper: a Key answers for itself, a unique field applies its nullsDistinct setting, an inline record derives from its constituent fields, and a plain field reports its own nullability. The sealed-entity field-resolution block in MetamodelFactory is extracted into a shared helper. BREAKING CHANGE: scroll now rejects a Metamodel.key()-wrapped nullable field with a descriptive PersistenceException instead of silently skipping NULL rows; a wrapped metamodel whose path does not resolve to a record field fails the same way at validation time. Fixes #403
Method.invoke's only remaining checked reflective failure once InvocationTargetException is handled is IllegalAccessException; the broader ReflectiveOperationException catch is flagged as masked by CodeQL. The static initializer keeps the broad catch, where Class.forName and getMethod throw other reflective exceptions. Also names MetamodelFactory.root in root()'s failure message.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #403
Problem
Metamodel.key()returns the argument unchanged when it already implementsKeyand wraps it inKeyDelegateotherwise.KeyDelegate.isNullable()returneddelegate instanceof Key<?, ?> key && key.isNullable(), but by construction the delegate is never aKey, so every factory-built delegate reported non-nullable. That is precisely the check keyset pagination relies on to reject nullable cursor keys, so a wrapped nullable metamodel used as a pagination key silently skipped NULL rows across page boundaries: the exact failure mode thekey()docs warn about.Fix
KeyDelegate.isNullable()now derives the answer from the underlying metamodel. A delegate around aKeystill answers through that key; any other delegate resolves the record field at the metamodel's path through a newMetamodelFactory.isNullable(Metamodel), bridged from storm-foundation via the reflectiveMetamodelHelper(the same pattern asroot/of/flatten):nullsDistinctsetting, matching factory-built key metamodels;SimpleKeyMetamodel;Nullability comes from the same
RecordField.nullable()source the rest of the framework uses (null-marked-by-default contract; Kotlin resolves from the language type via the Kotlin reflection provider), so Java and Kotlin behave identically and no new dependency is introduced. The sealed-entity field-resolution block that appeared three times inMetamodelFactoryis extracted into a sharedfieldResolutionClasshelper.Breaking change
scrollwith aMetamodel.key()-wrapped nullable field now throws the descriptivePersistenceExceptioninstead of silently skipping NULL rows. This also applies to previously issuedScrollabletokens revalidated on their nextscrollcall. Callers that genuinely need such a key can implementKey/AbstractKeyMetamodelexplicitly.findBy/getBy/getByRefdo not consultisNullable()and are unaffected; non-nullable wrapped keys scroll exactly as before;KeyDelegate's record shape and equality semantics are unchanged.Tests
MetamodelTest: wrapped nullable and non-nullable scalar fields, the documentedMetamodel.offactory route, nested paths resolving at the leaf field, and a nullable@FKcolumn.RepositoryPreparedStatementIntegrationTest:scrollrejects a wrapped nullable key and still accepts a wrapped non-nullable key;testMetamodelKeyFactorytightened withassertSamefor the inline-record case, which already carries theKeymarker.KeyDelegateTest(storm-foundation, runs without storm-core): the test that asserted the always-false behavior is replaced by direct-key coverage of both nullability answers; derivation coverage lives in storm-core, where the factory is on the classpath.Full reactor green (
mvn test, all modules).