Skip to content

NIFI-15973 Fix issues caused by the use of deprecated GregorianCalendar#11283

Open
Kamilkime wants to merge 4 commits into
apache:mainfrom
Kamilkime:kamilkime/NIFI-15973-julian-calendar-fix
Open

NIFI-15973 Fix issues caused by the use of deprecated GregorianCalendar#11283
Kamilkime wants to merge 4 commits into
apache:mainfrom
Kamilkime:kamilkime/NIFI-15973-julian-calendar-fix

Conversation

@Kamilkime
Copy link
Copy Markdown
Contributor

Summary

NIFI-15973

Avoid Julian-calendar shift when converting java.sql.Date / java.sql.Timestamp values to java.time types (and back) in NiFi's record framework.

java.sql.Timestamp#toLocalDateTime, java.sql.Timestamp#valueOf(LocalDateTime), java.sql.Date#toLocalDate, and java.sql.Date#valueOf(LocalDate) all route through GregorianCalendar, which applies Julian-calendar semantics for years before 1582-10-15. As a result, a value whose epoch milliseconds represent year 0001 in the proleptic Gregorian calendar (the calendar java.time, Avro, BigQuery, or Snowflake all use) is rendered as year 0000 — about a 2-day backward shift. Downstream sinks that validate against an inclusive year-1-to-9999 range (e.g., the Snowflake Ingest SDK) then reject the row: Timestamp out of representable inclusive range of years between 1 and 9999, ... value:0000-12-30T10:47Z.

The fix replaces every Julian-aware bridge between java.sql.* and java.time.* with an Instant-based one, which preserves the proleptic-Gregorian instant the source actually carried.

Tests:

  • ObjectLocalDateFieldConverterTest (new): null input, LocalDate passthrough, java.sql.Date modern-year round-trip, and a year-0001 java.sql.Date regression test that asserts the result is year 0001 (not year 0000).
  • TestObjectLocalDateTimeFieldConverter: add testConvertTimestampYearOneIsProlepticGregorian (year-0001 Timestamp round-trip) and testConvertStringYearOneIsProlepticGregorian (year-0001 string parsed via formatter).
  • ObjectTimestampFieldConverterTest: add testConvertFieldStringYearOneIsProlepticGregorian and testConvertFieldLocalDateTimeYearOneIsProlepticGregorian to lock in the year-0001 result for both string and LocalDateTime inputs.
  • ObjectStringFieldConverterTest: add testConvertFieldTimestampYearOneIsProlepticGregorian covering the formatted-output path.
  • New regression tests fail on main and pass on this branch.

Compatibility:

Backwards compatible. Instant-based conversion and Julian-aware Timestamp / Date conversion agree for every date from 1582-10-15 onward, so all values produced after the Gregorian cutover (i.e., everything the vast majority of users have ever ingested) are unchanged. Pre-cutover values — including the 1582-10-05 → 1582-10-14 gap that Julian had but proleptic Gregorian does not — now match the proleptic-Gregorian instant the source actually carried, which is what java.time, Avro, and downstream sinks already assume.

No public API signatures change, the fix is purely internal to the existing converter implementations.

Tracking

Please complete the following tracking steps prior to pull request creation.

Issue Tracking

Pull Request Tracking

  • Pull Request title starts with Apache NiFi Jira issue number, such as NIFI-00000
  • Pull Request commit message starts with Apache NiFi Jira issue number, as such NIFI-00000
  • Pull request contains commits signed with a registered key indicating Verified status

Pull Request Formatting

  • Pull Request based on current revision of the main branch
  • Pull Request refers to a feature branch with one commit containing changes

Verification

Please indicate the verification steps performed prior to pull request creation.

Build

  • Build completed using ./mvnw clean install -P contrib-check
    • JDK 21
    • JDK 25

Licensing

  • New dependencies are compatible with the Apache License 2.0 according to the License Policy
  • New dependencies are documented in applicable LICENSE and NOTICE files

Documentation

  • Documentation formatting appears as expected in rendered files

@Kamilkime Kamilkime force-pushed the kamilkime/NIFI-15973-julian-calendar-fix branch from 2592587 to d7a238f Compare May 25, 2026 17:00
@Kamilkime Kamilkime marked this pull request as ready for review May 25, 2026 17:01
Copy link
Copy Markdown
Contributor

@exceptionfactory exceptionfactory left a comment

Choose a reason for hiding this comment

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

Thanks for describing the issues with the current Date and Timestamp conversion @Kamilkime. This pull request highlights the subtle behavior of different time classes.

I noted some recommendations related to the unit test methods for readability and clear construction.

Regarding the functional change, it appears reasonable from one perspective, since date conversion of all modern values are unchanged. I would expect this to cover many use cases without concern.

The key question is whether the change in behavior for ancient dates is acceptable. At minimum, the nuance should be commented in the Converter classes that the described approach uses the Proleptic Gregorian Calendar System, avoid the implicit use of deprecated Date conversion methods. Declaring the intermediate values would also helpful emphasize the behavioral adjustment.

Comment on lines +169 to +170
final Instant expected = LocalDateTime.of(1, 1, 1, 12, 0, 0).atZone(ZoneId.systemDefault()).toInstant();
assertEquals(expected, timestamp.toInstant());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Unit tests should assert expected Timestamp values, not values converted to Instant objects.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed to Timestamp assertions

@Test
public void testConvertStringYearOneIsProlepticGregorian() {
final LocalDateTime result = converter.convertField("0001-01-01 12:00:00", Optional.of("yyyy-MM-dd HH:mm:ss"), FIELD_NAME);
assertEquals(LocalDateTime.of(1, 1, 1, 12, 0, 0), result);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The expected value should be declared separately instead of inline instance creation for clarity.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Extracted expected variable

Comment on lines +1157 to +1158
final LocalDate localDate = (value instanceof LocalDate ld) ? ld : LocalDate.ofEpochDay((int) value);
return new java.sql.Date(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The inline ternary and cast is difficult to read, and the multiple levels of conversion to create the new java.sql.Date() should be split out and declared.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Used an if statement for the localDate value, and extracted zonedDate

@Test
void testConvertFieldSqlDateModernYear() {
final LocalDate localDate = LocalDate.of(2025, 5, 25);
final Date sqlDate = new Date(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Intermediate values should be declared for readability.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Extracted zonedDate

Copy link
Copy Markdown
Contributor Author

@Kamilkime Kamilkime left a comment

Choose a reason for hiding this comment

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

Thanks @exceptionfactory, I addressed your comments, please let me know if it looks good now

@Test
void testConvertFieldSqlDateModernYear() {
final LocalDate localDate = LocalDate.of(2025, 5, 25);
final Date sqlDate = new Date(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli());
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Extracted zonedDate

@Test
public void testConvertStringYearOneIsProlepticGregorian() {
final LocalDateTime result = converter.convertField("0001-01-01 12:00:00", Optional.of("yyyy-MM-dd HH:mm:ss"), FIELD_NAME);
assertEquals(LocalDateTime.of(1, 1, 1, 12, 0, 0), result);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Extracted expected variable

Comment on lines +1157 to +1158
final LocalDate localDate = (value instanceof LocalDate ld) ? ld : LocalDate.ofEpochDay((int) value);
return new java.sql.Date(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli());
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Used an if statement for the localDate value, and extracted zonedDate

Comment on lines +169 to +170
final Instant expected = LocalDateTime.of(1, 1, 1, 12, 0, 0).atZone(ZoneId.systemDefault()).toInstant();
assertEquals(expected, timestamp.toInstant());
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed to Timestamp assertions

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