Skip to content

Conversation

vbabanin
Copy link
Member

@vbabanin vbabanin commented Sep 6, 2025

Hibernate ORM currently handles temporal types (Instant, etc.) inconsistently depending on where they appear in an entity. For example, saving an Instant of 10:15 (always UTC) on a host in GMT+1 leads to inconsistent results once read (tested with Postgres dialect):

top-level field -> 10:15 (correct)
array/collection element -> 09:15
struct field -> 11:15

This inconsistency makes it difficult to predict how temporal values will be stored and retrieved.

This PR ensures that temporal types are handled consistently in MongoDB extension across top-level fields, arrays/collections, and structs, so that serialization and deserialization behave uniformly regardless of context.

HIBERNATE-42

@vbabanin vbabanin self-assigned this Sep 6, 2025
@vbabanin vbabanin requested a review from stIncMale September 6, 2025 02:47
@vbabanin vbabanin marked this pull request as ready for review September 9, 2025 06:22
@vbabanin vbabanin requested a review from a team as a code owner September 9, 2025 06:22
Object enabled = configurationValues.get("hibernate.type.java_time_use_direct_jdbc");
if (enabled instanceof Boolean && (Boolean) enabled) {
throw new HibernateException(format(
"Configuration property [%s] is incubating and not supported in MongoDB dialect",
Copy link
Member Author

@vbabanin vbabanin Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Offline review with Valentin:
Remove "is incubating" and "Mongo dialect"

checkClosed();
checkParameterIndex(parameterIndex);
throw new SQLFeatureNotSupportedException("TODO-HIBERNATE-42 https://jira.mongodb.org/browse/HIBERNATE-42");
if (!"UTC".equals(calendar.getTimeZone().getID())) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Offline review with Valentin:
Change to "calendarZoneId.getRules().equals(ETC_UTC_ZONE_RULES)"

HIBERNATE-42
@vbabanin vbabanin force-pushed the HIBERNATE-42-instant branch from ac167e0 to d7b51e5 Compare September 10, 2025 03:25
Copy link
Member

@stIncMale stIncMale left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Submitting intermediate review results.


@Override
public TimeZoneSupport getTimeZoneSupport() {
return TimeZoneSupport.NONE;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out the default implementation is the same. Do we need the override at all?

The unnecessary override is removed in stIncMale@a7d9ae1.


@Override
public void appendDatetimeFormat(final SqlAppender appender, final String format) {
throw new FeatureNotSupportedException("TODO-HIBERNATE-88 https://jira.mongodb.org/browse/HIBERNATE-88");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we add a comment like this, we also mention

Addressing the source code notes tagged with TODO-HIBERNATE-88 is in scope of this ticket.

in the description of the ticket, to remind the assignee to look for such TODOs. I have added note to the description.

Comment on lines 66 to 74
@Override
public TimeZoneSupport getTimeZoneSupport() {
return TimeZoneSupport.NONE;
}

@Override
public void appendDatetimeFormat(final SqlAppender appender, final String format) {
throw new FeatureNotSupportedException("TODO-HIBERNATE-88 https://jira.mongodb.org/browse/HIBERNATE-88");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a weird placement of the methods: after one of the MongoDialect constructors, and before the other one. I propose adding these methods to the end of the class.

The proposed change is done in stIncMale@19afd7f.

Comment on lines 65 to 70
return Stream.of(
Arguments.of(TimeZone.getTimeZone("UTC"), TimeZone.getTimeZone("GMT+1")),
Arguments.of(TimeZone.getTimeZone("GMT+1"), TimeZone.getTimeZone("GMT+1")),
Arguments.of(TimeZone.getTimeZone("GMT+1"), TimeZone.getTimeZone("GMT+2")),
Arguments.of(TimeZone.getTimeZone("GMT+05:31"), TimeZone.getTimeZone("GMT+05:45")),
Arguments.of(TimeZone.getTimeZone("America/New_York"), TimeZone.getTimeZone("America/Los_Angeles")));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running InstantIntegrationTests takes 17 seconds, with the whole test set barring InstantIntegrationTests taking less than 1 minute. Let's reduce the number of variants, unless they seem very useful.

To me it seems that the only pairs worth checking are when the timezones differ (maybe in both directions), regardless of by how much.

Also, the canonical variants of the UTC and GMT+.../GMT-... timezone IDs are prefixed with Etc/ (Etc is short for "et cetera", as timezone IDs should follow the {area}/{city} form): see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones (unfortunately, there is no authoritative web view of the time zone database).

The proposed change is done in stIncMale@baa1594.

@ExtendWith(MongoExtension.class)
class InstantIntegrationTests implements SessionFactoryScopeAware {

private static final TimeZone CURRENT_JVM_TIMEZONE = TimeZone.getDefault();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename current->original, because as soon as we change the default time zone, the "current" one stops being the current.

The suggestion is done in stIncMale@bdaf0b2.

Comment on lines 84 to 114
return Stream.of(
Arguments.of(
systemDefaultTimeZone,
jdbcTimezone,
// Attribute or an element of an attribute to save.
Instant.parse("2007-12-03T10:15:30.00Z"),
// Expected attribute or an element of an attribute after read.
Instant.parse("2007-12-03T10:15:30.00Z")),
Arguments.of(
systemDefaultTimeZone,
jdbcTimezone,
Instant.parse("1500-12-03T10:15:30Z"),
Instant.parse("1500-12-03T10:15:30Z")),
Arguments.of(
systemDefaultTimeZone,
jdbcTimezone,
Instant.parse("-000001-12-03T10:15:30Z"),
Instant.parse("-000001-12-03T10:15:30Z")),
Arguments.of(
systemDefaultTimeZone,
jdbcTimezone,
// Other dialects might support nanoseconds precision, however, in our case the precision is
// to within milliseconds.
Instant.parse("2007-12-03T10:15:30.000000999Z"),
Instant.parse("2007-12-03T10:15:30Z")),
Arguments.of(
systemDefaultTimeZone,
jdbcTimezone,
// We support milliseconds precision, so nanoseconds are rounded down to milliseconds.
Instant.parse("2007-12-03T10:15:30.002900000Z"),
Instant.parse("2007-12-03T10:15:30.002000000Z")));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 4th of the Arguments does not seem to add anything to the 5th (last) one in terms of what is being tested. Given that running InstantIntegrationTests takes quite a lot of time (17 seconds, with the whole test set barring InstantIntegrationTests taking less than 1 minute), let's get rid of the 4th Arguments.

This proposed change is done in stIncMale@4e72f84.

Further, the 1st one does not seem to add anything to the 5th one, so let's remove it also.

This proposed change is done in stIncMale@d81ffa1.

});
}

/** Write: system tz: T1, session tz: T2 Read: system tz: T1, session tz: T2 */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be readable, this comment needs punctuation between the sentences. The same applies to other similar comments.

The proposed change is done in stIncMale@08eaa77.

Comment on lines 138 to 140
@ParameterizedTest(
name = "Instant: system TZ differ per read/write; session TZ equals system TZ;"
+ "systemDefaultTimeZone={0}, jdbcTimeZone={1}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name of the testInstantRoundTripWhenSessionTzEqual test says

system TZ not equal session TZ

But the name here says

session TZ equals system TZ

It would have been helpful if those parts were mentioning time zones consistently. For example, at first the system TZ, then the session TZ.


On one hand, the name is needed here to make the test cases distinguishable in Evergreen. On the other hand, it duplicates the information specified in the test method documentation comment. Given that names are unavoidable, let's make them such that comments are not needed.


The testInstantRoundTripWhenSessionTzEqual, testInstantRoundTripWhenSessionTzNotEqual, testInstantPersistAndReadDifferentTimeZones test method names are hardly meaningful. Given that the name is used and is supposed to describe the meaning, let's simplify the test method names.


  1. The Instant: prefix in the names is not adding any information, because the class name is already InstantIntegrationTests.

  1. The arguments of the test methods should be of the ZoneId type, not TimeZone, because the TimeZone.toString emits rules, which makes the name unreadable: com.mongodb.hibernate.InstantIntegrationTests.Instant__system_TZ_equal_per_read_write__system_TZ_not_equal_session_TZ__session_TZ_equal_per_read_write_systemDefaultTimeZone_sun.util.calendar.ZoneInfo_id__UTC__offset_0_dstSavings_0_useDaylight_false_transitions_0_lastRule_null___jdbcTimeZone_sun.util.calendar.ZoneInfo_id__UTC__offset_0_dstSavings_0_useDaylight_false_transitions_0_lastRule_null_

The proposed changes are done in stIncMale@c10a702.

Copy link
Member Author

@vbabanin vbabanin Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 - Made consistent as proposed.
2 - In the referenced commit, all parameterized tests render the same Write: system tz {n}, session tz {n}. Read: system tz {n}, session tz {n} pattern.

When multiple tests fail at once, it can become tricker to identify which configuration failed without cross-referencing parameters or examining the log for the method name.

I agree we can move explanatory comments into method names. We could also prefix each test name with its invariants. This would keep the Write(sys={0}, sess={1}). Read(sys={0}, sess={1}) format while making failures distinguishable in Evergreen.

Example
sys TZ equal per write/read; sys TZ not equal sess TZ; sess TZ equal per write/read.
Write(sys={0}, sess={1}). Read(sys={0}, sess={1})
Updated to:

methodName: Write(sys={0}, sess={1}). Read(sys={0}, sess={1})

3 - Scenario-based method names are easier to locate in the IDE when browsing by methods, since the @ParameterizedTest display name is not shown there. I suggest we keep short semantic method names that summarize each scenario (I’ve shortened them accordingly).
4 - Removed the redundant "Instant:" prefix.
5 - Adopted ZoneId instead of TimeZone.

Let me know what you think.

Copy link
Member Author

@vbabanin vbabanin Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: use method name instead of lengthy test name (which evergreen substitutes it with _ ; hard to copy)

UPD: done

Collection<StructAggregateEmbeddableIntegrationTests.Single> structAggregateEmbeddablesCollection) {
Collection<StructAggregateEmbeddableIntegrationTests.Single> structAggregateEmbeddablesCollection,
Instant[] instants,
List<Instant> instantsCollection) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the sake of writing code that follows the existing code structure and style:

  1. The Instant[] instants parameter should be positioned immediately after the ObjectId[] objectIds parameter.
  2. The List<Instant> instantsCollection parameter should be positioned immediately after the Collection<ObjectId> objectIdsCollection parameter.
  3. The type of the instantsCollection parameter should be Collection<Instant>.
  4. The this.instants = instants initialization should be placed immediately after this.objectIds = objectIds.
  5. StructAggregateEmbeddableIntegrationTests.ArraysAndCollections, ArrayAndCollectionIntegrationTests.ItemWithArrayAndCollectionValues have similar, though not exactly the same issues.
  6. The field order in the expected documents should be adjusted to match the previous changes for readability.
  7. When creating ArraysAndCollections in EmbeddableIntegrationTests.testFlattenedValueHavingArraysAndCollections/StructAggregateEmbeddableIntegrationTests.testNestedValueHavingArraysAndCollections, the null element should be added to arrays and collections.

The proposed changes are done in stIncMale@9b7cc21.


@Entity
@Table(name = COLLECTION_NAME)
@Table(name = "items")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is not needed, and should be reverted.

The proposed change is done in stIncMale@579e32d.

ItemDynamicallyUpdated() {}

ItemDynamicallyUpdated(int id, boolean primitiveBoolean, Boolean boxedBoolean) {
ItemDynamicallyUpdated(int id, boolean primitiveBoolean, Boolean boxedBoolean, Instant instant) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No changes to ItemDynamicallyUpdated is needed here. This entity is not supposed to test all the basic types.

The proposed change is done in stIncMale@0765459.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I skipped reviewing the changes to this file, in hope that the alternative design I propose (use setObject/getObject for Instant) will be accepted. If it is, we'll just revert the changes to this file. If not, I'll have to review it.

TODO for @stIncMale: review what was skipped, if still needed.


abstract List<I> getData();

@ParameterizedTest(name = "testComparisonByEq: temporal={0}, expectedItems={2}, expectedRender={3}, timeZone={1}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no timeZone parameter here, or in any other test methods declared in this class, which suggests that something is wrong with the file.

I skipped reviewing AbstractSelectionTemporalIntegrationTest and SelectionInstantIntegrationTest. I am asking to please at first self-review them, taking into account the rest of the comments in this PR, and do the changes, if needed.

TODO for @stIncMale: review what was skipped.

checkParameterIndex(parameterIndex);
throw new SQLFeatureNotSupportedException("TODO-HIBERNATE-42 https://jira.mongodb.org/browse/HIBERNATE-42");
checkTimeZone(calendar.getTimeZone());
setParameter(parameterIndex, toBsonValue(timestamp));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a way to make Hibernate ORM to use setObject/getObject for Instant, thus saving us from dealing with Timestamp and Calendar, without dealing with the JAVA_TIME_USE_DIRECT_JDBC, PREFERRED_INSTANT_JDBC_TYPE configuration properties. I propose us to use it, as it makes the implementation and even testing simpler.

The proposed change is done in stIncMale@5bebe91.

@stIncMale
Copy link
Member

The PR with all the proposed changes: vbabanin#1.

# Conflicts:
#	src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java
- Add tests for the forbidden temporal types.
- Use TimestampUtcAsInstantJdbcType.
- Rename test method and display names.

HIBERNATE-42
Add test case for flattened unsupported id types.
@vbabanin vbabanin requested a review from stIncMale October 15, 2025 20:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants