-
Type:
Bug
-
Resolution: Unresolved
-
Priority:
Major - P3
-
Affects Version/s: None
-
Component/s: Model
Problem
ValueConversions.toBsonValue(Object) dispatches on the runtime class of a value and throws SQLFeatureNotSupportedException for anything it does not recognize. It recognizes the JDBC-level classes: Boolean, Character, Integer, Long, Double, BigDecimal, String, byte[], char[], ObjectId, Instant, and arrays.
That vocabulary is the right one, because every write path reaches ValueConversions through BasicBinder.getBindValue, which first unwraps the domain value to JdbcType.getPreferredJavaTypeClass. Two paths skip that step and pass the domain value straight through:* MongoStructJdbcType.createBindValue, for the fields of a @Struct aggregate embeddable. It already routes arrays through the binder, and everything else directly.
- AbstractMqlTranslator.visitQueryLiteral, for an inlined HQL literal.
So a type whose domain class is not one of the recognized set works in every position except those two. Measured on main, each of java.time.Duration, java.time.Year, java.time.ZoneId, java.time.ZoneOffset and java.util.TimeZone persists and loads correctly as a top-level and as a plural attribute, and works in select, in a parameter predicate, in in, in is null, in order by and in a mutation set, yet fails both inside a @Struct embeddable and as an HQL literal:
SQLFeatureNotSupportedException: Value \[PT1M30.0000005S\] of type \[java.time.Duration\] is not supported
at ValueConversions.java:92
at MongoStructJdbcType.createBindValue
The read side has the mirror-image defect. MongoStructJdbcType.extractJdbcValues asks ValueConversions for the mapped domain class directly, so reading a ZoneId out of a @Struct fails:
Could not extract column \[3\] from JDBC ResultSet
\[Value \[BsonString{value='Europe/Paris'}\] of type \[org.bson.BsonString\]
is not supported for the Java type \[java.time.ZoneId\]\]
Fix
- MongoStructJdbcType.createBindValue: unwrap through jdbcMapping.getJdbcValueBinder().getBindValue before calling toBsonValue. This absorbs the existing array special case, whose body already does exactly that. Apply the unwrap only when the value is an instance of its selectable's mapped Java type, and otherwise keep the existing direct call. That fallback is reachable for shapes we do not support, notably a flattened @Embeddable nested inside a @Struct one, where the unwrap would otherwise throw a bare ClassCastException from IntegerJavaType.unwrap instead of the SQLFeatureNotSupportedException that names the offending value.
- MongoStructJdbcType.extractJdbcValues: read the JDBC-level value the binder would have written, then wrap it back through the mapped JavaType. This is not symmetric in form with the write side. The ordinary column path has BasicExtractor.doExtract do the wrapping, but the @Struct read path has no ValueExtractor, so it must wrap explicitly. A JdbcType reporting no preferred Java type binds the domain value unchanged, so it is read back unchanged.
- AbstractMqlTranslator.visitQueryLiteral: unwrap the same way, reaching the mapping through QueryLiteral.getJdbcMapping and WrapperOptions through getSessionFactory().getWrapperOptions(). The latter does not require an open session, which matters because translation happens without one.
The alternative fix, a ValueConversions branch per domain class, is rejected. It re-encodes mapping decisions Hibernate's JavaType registry already owns, and it needs a new branch for every type anyone ever adds.
Also in scope
Make java.time.Duration, java.time.Year, java.time.ZoneId, java.time.ZoneOffset and java.util.TimeZone properly supported rather than accidentally working. Add them to the default type mapping table in module-info: Duration as Decimal128 nanoseconds, Year as 32-bit integer, and the three zone types as String.
Tests
- Regression coverage in @Struct and in literal position for String, int, ObjectId, Instant and arrays. The change sits on a path every type flows through, so types this work does not otherwise touch need explicit coverage.
- StructAggregateEmbeddableIntegrationTests.Unsupported.testEmbeddable must keep passing unchanged. It is what catches removal of the mapped-type guard.
- Per type, for the five above: a round trip asserting the exact BSON, a @Struct nesting case, an HQL literal predicate, and one case per already-working position so the measured behavior stays covered.
Verification so far
A prototype of the three methods passes ./gradlew build and a full integration run of 576 tests with no failures.
- is depended on by
-
HIBERNATE-225 Support instant-like temporal types
-
- Needs Triage
-