From 89c2201718ac028b957cb440d2c152e8617e42bd Mon Sep 17 00:00:00 2001 From: Emanuele Russo Date: Sun, 7 Jun 2026 21:10:42 +0200 Subject: [PATCH] AVRO-4269: Fix timestamp-nanos conversion before epoch --- .../org/apache/avro/data/TimeConversions.java | 2 +- .../apache/avro/data/TestTimeConversions.java | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lang/java/avro/src/main/java/org/apache/avro/data/TimeConversions.java b/lang/java/avro/src/main/java/org/apache/avro/data/TimeConversions.java index e63ebaae6e0..0e24e0aec29 100644 --- a/lang/java/avro/src/main/java/org/apache/avro/data/TimeConversions.java +++ b/lang/java/avro/src/main/java/org/apache/avro/data/TimeConversions.java @@ -235,7 +235,7 @@ public Long toLong(Instant instant, Schema schema, LogicalType type) { if (seconds < 0 && nanos > 0) { long micros = Math.multiplyExact(seconds + 1, 1_000_000_000L); - long adjustment = nanos - 1_000_000; + long adjustment = nanos - 1_000_000_000L; return Math.addExact(micros, adjustment); } else { diff --git a/lang/java/avro/src/test/java/org/apache/avro/data/TestTimeConversions.java b/lang/java/avro/src/test/java/org/apache/avro/data/TestTimeConversions.java index 089915803a0..07df0934e4f 100644 --- a/lang/java/avro/src/test/java/org/apache/avro/data/TestTimeConversions.java +++ b/lang/java/avro/src/test/java/org/apache/avro/data/TestTimeConversions.java @@ -33,6 +33,7 @@ import org.apache.avro.data.TimeConversions.TimeMillisConversion; import org.apache.avro.data.TimeConversions.TimestampMicrosConversion; import org.apache.avro.data.TimeConversions.TimestampMillisConversion; +import org.apache.avro.data.TimeConversions.TimestampNanosConversion; import org.apache.avro.reflect.ReflectData; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -44,6 +45,7 @@ public class TestTimeConversions { public static Schema TIME_MICROS_SCHEMA; public static Schema TIMESTAMP_MILLIS_SCHEMA; public static Schema TIMESTAMP_MICROS_SCHEMA; + public static Schema TIMESTAMP_NANOS_SCHEMA; @BeforeAll public static void createSchemas() { @@ -54,6 +56,8 @@ public static void createSchemas() { .addToSchema(Schema.create(Schema.Type.LONG)); TestTimeConversions.TIMESTAMP_MICROS_SCHEMA = LogicalTypes.timestampMicros() .addToSchema(Schema.create(Schema.Type.LONG)); + TestTimeConversions.TIMESTAMP_NANOS_SCHEMA = LogicalTypes.timestampNanos() + .addToSchema(Schema.create(Schema.Type.LONG)); } @Test @@ -191,6 +195,21 @@ void timestampMicrosConversion() throws Exception { "Pre 1970 date should be correct"); } + @Test + void timestampNanosConversionBeforeEpochWithPositiveNanos() { + TimestampNanosConversion conversion = new TimestampNanosConversion(); + + // < Epoch + long Dec_31_1969_23_59_59_999_999_999_instant = -1L; + Instant Dec_31_1969_23_59_59_999_999_999 = ZonedDateTime.of(1969, 12, 31, 23, 59, 59, 999_999_999, ZoneOffset.UTC) + .toInstant(); + + assertEquals(Dec_31_1969_23_59_59_999_999_999, conversion.fromLong(Dec_31_1969_23_59_59_999_999_999_instant, + TIMESTAMP_NANOS_SCHEMA, LogicalTypes.timestampNanos()), "Pre 1970 nanos date should be correct"); + assertEquals(Dec_31_1969_23_59_59_999_999_999_instant, (long) conversion.toLong(Dec_31_1969_23_59_59_999_999_999, + TIMESTAMP_NANOS_SCHEMA, LogicalTypes.timestampNanos()), "Pre 1970 nanos date should be correct"); + } + @Test void dynamicSchemaWithDateConversion() throws ClassNotFoundException { Schema schema = getReflectedSchemaByName("java.time.LocalDate", new TimeConversions.DateConversion());