Unwrap domain values in @Struct fields and HQL literals

XMLWordPrintableJSON

    • Type: Bug
    • Resolution: Unresolved
    • Priority: Major - P3
    • 1.0.0
    • Affects Version/s: None
    • Component/s: Model
    • 3
    • None
    • Needed
    • None
    • None
    • None
    • None
    • None
    • None

      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.

            Assignee:
            Jeffrey Yemin
            Reporter:
            Jeffrey Yemin
            None
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

              Created:
              Updated: