Implementing originalType so that map/contramap work in R2dbc #23
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.
This PR fixes two fixes two issues
Issue A - Map/Contramap for R2dbc were not working
Previously, using
contramapon encoders ormapon decoders would break R2DBC's null handling because the type information used forbindNull()would change to the mapped type instead of preserving the original database type.Why This Mattered for R2DBC
Unlike JDBC, which uses integer type codes (e.g.,
Types.TIMESTAMP) forsetNull(), R2DBC requires the actual Java class forbindNull(). When you transform an encoder withcontramap, the type parameter changes, which previously meant the wrong class would be passed tobindNull().Previous Workaround Example
Before this fix, we had to use verbose
object: R2dbcEncoderAnydeclarations with customsetNullimplementations:This was necessary because:
java.util.DatedirectlyOffsetDateTimefor bindingbindNull()must be called withOffsetDateTime::class.java, notDate::class.javasetNull, it would incorrectly callstmt.bindNull(index, Date::class.javaObjectType)The same issue affected decoders - we couldn't use
mapto transform decoded values because the type information would be lost.Solution
Introduced an
originalType: KClass<*>field to bothEncoderAnyandDecoderAny:contramap/maptransformationsbindNull()to pass the correct Java class to the driverImplementation Details
originalTypeparameter (used bycontramap)originalType = type(convenient for normal usage)contramappasses through theoriginalTypeunchangedBenefits
1. Cleaner, More Idiomatic Code
The encoder automatically knows to use
OffsetDateTimeforbindNull()because that's theoriginalTypefromJOffsetDateTimeEncoder.2. Works for Complex Type Conversions
Another example - UUID as String in databases that don't support native UUID:
3. Consistent with Terpal Philosophy
contramapandmapare fundamental functional programming patterns that align with Terpal's compositional approach to building encoders/decoders. This fix removes the friction that previously prevented their use in R2DBC contexts.Changes Summary
originalTypefield toEncoderAnyandDecoderAnycontramap/map:R2dbcTimeEncodingSqlServer: 7 encoders/decodersR2dbcTimeEncodingOracle: 8 encoders/decodersR2dbcTimeEncodingH2: Already using the patternR2dbcUuidEncodingString: UUID ↔ String conversionobject: R2dbcEncoderAnyworkarounds with customsetNullimplementationsIssue B - SQL Server java.util.Date was decoding with wrong timezone
The
JavaTestEntitytests were failing on SQL Server with anAssertionErrorwhen comparingjava.util.Datevalues. The date being read back from the database didn't match the date that was inserted.Root Cause
There was a type mismatch between the SQL Server schema and the JDBC encoder/decoder:
java.util.Datecolumn was defined asDATETIMEOFFSET(SQL Server's timezone-aware datetime type)JdbcTimeEncodingwas usingTypes.TIMESTAMPwithsetTimestamp()/getTimestamp()methodsWhen
getTimestamp()is called on aDATETIMEOFFSETcolumn, the SQL Server JDBC driver performs implicit timezone conversions that can alter the actual timestamp value, causing the round-trip test to fail.Solution
Introduced
SqlServerTimeEncodingthat properly handlesjava.util.DateforDATETIMEOFFSETcolumns by:Types.TIMESTAMP_WITH_TIMEZONEinstead ofTypes.TIMESTAMPOffsetDateTimeas an intermediary typesetObject()/getObject()with the correct type classThis ensures proper handling of timezone-aware datetime columns and preserves the exact instant across encode/decode cycles.