Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public LocalDate convertField(final Object field, final Optional<String> pattern
return localDate;
}
case Date date -> {
return date.toLocalDate();
return ofInstant(Instant.ofEpochMilli(date.getTime()));
}
case java.util.Date date -> {
final Instant instant = date.toInstant();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public LocalDateTime convertField(final Object field, final Optional<String> pat
return localDateTime;
}
case Timestamp timestamp -> {
return timestamp.toLocalDateTime();
return ofInstant(timestamp.toInstant());
}
case Date date -> {
// java.sql.Date and java.sql.Time do not support the toInstant() method so using getTime() is required
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import java.sql.Clob;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
Expand Down Expand Up @@ -58,10 +57,7 @@ public String convertField(final Object field, final Optional<String> pattern, f
return Long.toString(timestamp.getTime());
}
final DateTimeFormatter formatter = DateTimeFormatterRegistry.getDateTimeFormatter(pattern.get());
final LocalDateTime localDateTime = timestamp.toLocalDateTime();

// Convert LocalDateTime to ZonedDateTime using system default zone to support offsets in Date Time Formatter
final ZonedDateTime dateTime = ZonedDateTime.of(localDateTime, ZoneId.systemDefault());
final ZonedDateTime dateTime = timestamp.toInstant().atZone(ZoneId.systemDefault());
return formatter.format(dateTime);
}
case Date date -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Optional;

/**
Expand All @@ -40,6 +42,11 @@ class ObjectTimestampFieldConverter implements FieldConverter<Object, Timestamp>
@Override
public Timestamp convertField(final Object field, final Optional<String> pattern, final String name) {
final LocalDateTime localDateTime = CONVERTER.convertField(field, pattern, name);
return localDateTime == null ? null : Timestamp.valueOf(localDateTime);
if (localDateTime == null) {
return null;
}

final ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.systemDefault());
return Timestamp.from(zonedDateTime.toInstant());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,12 @@ public static Object convertType(
case DATE:
final FieldConverter<Object, LocalDate> localDateConverter = StandardFieldConverterRegistry.getRegistry().getFieldConverter(LocalDate.class);
final LocalDate localDate = localDateConverter.convertField(value, dateFormat, fieldName);
return localDate == null ? null : Date.valueOf(localDate);
if (localDate == null) {
return null;
}

final ZonedDateTime zonedDate = localDate.atStartOfDay(ZoneId.systemDefault());
return new Date(zonedDate.toInstant().toEpochMilli());
case DECIMAL:
return toBigDecimal(value, fieldName);
case DOUBLE:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.serialization.record.field;

import org.junit.jupiter.api.Test;

import java.sql.Date;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

class ObjectLocalDateFieldConverterTest {

private static final ObjectLocalDateFieldConverter CONVERTER = new ObjectLocalDateFieldConverter();
private static final String FIELD_NAME = LocalDate.class.getSimpleName();

@Test
void testConvertFieldNull() {
assertNull(CONVERTER.convertField(null, Optional.empty(), FIELD_NAME));
}

@Test
void testConvertFieldLocalDate() {
final LocalDate input = LocalDate.of(2025, 5, 25);
assertEquals(input, CONVERTER.convertField(input, Optional.empty(), FIELD_NAME));
}

@Test
void testConvertFieldSqlDateModernYear() {
final LocalDate localDate = LocalDate.of(2025, 5, 25);
final ZonedDateTime zonedDate = localDate.atStartOfDay(ZoneId.systemDefault());
final Date sqlDate = new Date(zonedDate.toInstant().toEpochMilli());
final LocalDate result = CONVERTER.convertField(sqlDate, Optional.empty(), FIELD_NAME);

assertEquals(localDate, result);
}

@Test
void testConvertFieldSqlDateYearOneIsProlepticGregorian() {
final LocalDate yearOne = LocalDate.of(1, 1, 1);
final ZonedDateTime zonedDate = yearOne.atStartOfDay(ZoneId.systemDefault());
final Date sqlDate = new Date(zonedDate.toInstant().toEpochMilli());
final LocalDate result = CONVERTER.convertField(sqlDate, Optional.empty(), FIELD_NAME);

assertEquals(yearOne, result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.zone.ZoneRules;
import java.util.Date;
import java.util.Locale;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
Expand Down Expand Up @@ -158,6 +161,17 @@ void testConvertFieldObjectArrayEmpty() {
assertEquals(EMPTY_ARRAY_STRING, string);
}

@Test
void testConvertFieldTimestampYearOneIsProlepticGregorian() {
final LocalDateTime yearOne = LocalDateTime.of(1, 1, 1, 12, 0, 0);
final ZonedDateTime zonedYearOne = yearOne.atZone(ZoneId.systemDefault());
final Timestamp timestamp = Timestamp.from(zonedYearOne.toInstant());
final String formatted = CONVERTER.convertField(timestamp, Optional.of(DEFAULT_PATTERN), FIELD_NAME);
final String expected = DateTimeFormatter.ofPattern(DEFAULT_PATTERN, Locale.ROOT).format(yearOne);

assertEquals(expected, formatted);
}

private String getDateTimeZoneOffset() {
final Timestamp inputTimestamp = Timestamp.valueOf(DATE_TIME_DEFAULT);
final LocalDateTime inputLocalDateTime = inputTimestamp.toLocalDateTime();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,26 @@ public void testConvertFieldStringFormatCustomZoneOffsetCoordinatedUniversalTime
assertEquals(expected, timestamp);
}

@Test
public void testConvertFieldStringYearOneIsProlepticGregorian() {
final LocalDateTime yearOne = LocalDateTime.of(1, 1, 1, 12, 0, 0);
final ZonedDateTime zonedYearOne = yearOne.atZone(ZoneId.systemDefault());
final Timestamp timestamp = CONVERTER.convertField("0001-01-01 12:00:00", DEFAULT_PATTERN, FIELD_NAME);
final Timestamp expected = Timestamp.from(zonedYearOne.toInstant());

assertEquals(expected, timestamp);
}

@Test
public void testConvertFieldLocalDateTimeYearOneIsProlepticGregorian() {
final LocalDateTime yearOne = LocalDateTime.of(1, 1, 1, 12, 0, 0);
final ZonedDateTime zonedYearOne = yearOne.atZone(ZoneId.systemDefault());
final Timestamp timestamp = CONVERTER.convertField(yearOne, DEFAULT_PATTERN, FIELD_NAME);
final Timestamp expected = Timestamp.from(zonedYearOne.toInstant());

assertEquals(expected, timestamp);
}

private Timestamp getDateTimeCoordinatedUniversalTime() {
final Timestamp dateTime = Timestamp.valueOf(DATE_TIME_DEFAULT);
final LocalDateTime localDateTime = dateTime.toLocalDateTime();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@

import org.junit.jupiter.api.Test;

import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Optional;

Expand Down Expand Up @@ -100,4 +102,21 @@ public void testWithDateFormatMicrosecondPrecision() {
final LocalDateTime result = converter.convertField(MICROS_TIMESTAMP_LONG, Optional.of("yyyy-MM-dd'T'HH:mm:ss.SSSSSS"), FIELD_NAME);
assertEquals(LOCAL_DATE_TIME_MICROS_PRECISION, result);
}

@Test
public void testConvertTimestampYearOneIsProlepticGregorian() {
final LocalDateTime yearOne = LocalDateTime.of(1, 1, 1, 12, 0, 0);
final ZonedDateTime zonedYearOne = yearOne.atZone(ZoneId.systemDefault());
final Timestamp timestamp = Timestamp.from(zonedYearOne.toInstant());
final LocalDateTime result = converter.convertField(timestamp, Optional.empty(), FIELD_NAME);

assertEquals(yearOne, result);
}

@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);
final LocalDateTime expected = LocalDateTime.of(1, 1, 1, 12, 0, 0);
assertEquals(expected, result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.sql.Blob;
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.Duration;
Expand Down Expand Up @@ -1153,11 +1154,16 @@ private static Object normalizeValue(final Object value, final Schema avroSchema
final String logicalName = logicalType.getName();
if (LOGICAL_TYPE_DATE.equals(logicalName)) {
// date logical name means that the value is number of days since Jan 1, 1970
// Handle both Integer (legacy) and LocalDate (newer Avro libraries)
if (value instanceof LocalDate localDate) {
return java.sql.Date.valueOf(localDate);
// Handle both Integer (legacy) and LocalDate (newer Avro libraries).
final LocalDate localDate;
if (value instanceof LocalDate ld) {
localDate = ld;
} else {
localDate = LocalDate.ofEpochDay((int) value);
}
return java.sql.Date.valueOf(LocalDate.ofEpochDay((int) value));

final ZonedDateTime zonedDate = localDate.atStartOfDay(ZoneId.systemDefault());
return new Date(zonedDate.toInstant().toEpochMilli());
} else if (LOGICAL_TYPE_TIME_MILLIS.equals(logicalName)) {
// time-millis logical name means that the value is number of milliseconds since midnight.
// Handle both Integer (legacy) and LocalTime (newer Avro libraries)
Expand Down
Loading