This is an automated email from the ASF dual-hosted git repository.
fokko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/avro.git
The following commit(s) were added to refs/heads/main by this push:
new ff3245302 AVRO-3914: Add nanos support (#2608)
ff3245302 is described below
commit ff324530251ce4facc8e8c6f3c12a1f19081c5b1
Author: Fokko Driesprong <[email protected]>
AuthorDate: Tue Dec 12 09:31:43 2023 +0100
AVRO-3914: Add nanos support (#2608)
---
.../main/java/org/apache/avro/LogicalTypes.java | 49 ++++++++++++++
.../java/org/apache/avro/data/TimeConversions.java | 78 ++++++++++++++++++++++
.../avro/compiler/specific/SpecificCompiler.java | 2 +
.../compiler/specific/TestSpecificCompiler.java | 10 ++-
.../org/apache/avro/protobuf/ProtoConversions.java | 44 ++++++++++--
5 files changed, 176 insertions(+), 7 deletions(-)
diff --git a/lang/java/avro/src/main/java/org/apache/avro/LogicalTypes.java
b/lang/java/avro/src/main/java/org/apache/avro/LogicalTypes.java
index dbf1a1fd8..5ccc1c966 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/LogicalTypes.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/LogicalTypes.java
@@ -149,6 +149,9 @@ public class LogicalTypes {
case TIMESTAMP_MICROS:
logicalType = TIMESTAMP_MICROS_TYPE;
break;
+ case TIMESTAMP_NANOS:
+ logicalType = TIMESTAMP_NANOS_TYPE;
+ break;
case TIME_MILLIS:
logicalType = TIME_MILLIS_TYPE;
break;
@@ -161,6 +164,9 @@ public class LogicalTypes {
case LOCAL_TIMESTAMP_MILLIS:
logicalType = LOCAL_TIMESTAMP_MILLIS_TYPE;
break;
+ case LOCAL_TIMESTAMP_NANOS:
+ logicalType = LOCAL_TIMESTAMP_NANOS_TYPE;
+ break;
default:
final LogicalTypeFactory typeFactory = REGISTERED_TYPES.get(typeName);
logicalType = (typeFactory == null) ? null :
typeFactory.fromSchema(schema);
@@ -193,8 +199,10 @@ public class LogicalTypes {
private static final String TIME_MICROS = "time-micros";
private static final String TIMESTAMP_MILLIS = "timestamp-millis";
private static final String TIMESTAMP_MICROS = "timestamp-micros";
+ private static final String TIMESTAMP_NANOS = "timestamp-nanos";
private static final String LOCAL_TIMESTAMP_MILLIS =
"local-timestamp-millis";
private static final String LOCAL_TIMESTAMP_MICROS =
"local-timestamp-micros";
+ private static final String LOCAL_TIMESTAMP_NANOS = "local-timestamp-nanos";
/** Create a Decimal LogicalType with the given precision and scale 0 */
public static Decimal decimal(int precision) {
@@ -255,6 +263,12 @@ public class LogicalTypes {
return TIMESTAMP_MICROS_TYPE;
}
+ private static final TimestampNanos TIMESTAMP_NANOS_TYPE = new
TimestampNanos();
+
+ public static TimestampNanos timestampNanos() {
+ return TIMESTAMP_NANOS_TYPE;
+ }
+
private static final LocalTimestampMillis LOCAL_TIMESTAMP_MILLIS_TYPE = new
LocalTimestampMillis();
public static LocalTimestampMillis localTimestampMillis() {
@@ -267,6 +281,12 @@ public class LogicalTypes {
return LOCAL_TIMESTAMP_MICROS_TYPE;
}
+ private static final LocalTimestampMicros LOCAL_TIMESTAMP_NANOS_TYPE = new
LocalTimestampMicros();
+
+ public static LocalTimestampMicros localTimestampNanos() {
+ return LOCAL_TIMESTAMP_NANOS_TYPE;
+ }
+
/** Uuid represents a uuid without a time */
public static class Uuid extends LogicalType {
private Uuid() {
@@ -502,6 +522,21 @@ public class LogicalTypes {
}
}
+ /** TimestampNanos represents a date and time in nanoseconds */
+ public static class TimestampNanos extends LogicalType {
+ private TimestampNanos() {
+ super(TIMESTAMP_NANOS);
+ }
+
+ @Override
+ public void validate(Schema schema) {
+ super.validate(schema);
+ if (schema.getType() != Schema.Type.LONG) {
+ throw new IllegalArgumentException("Timestamp (nanos) can only be used
with an underlying long type");
+ }
+ }
+ }
+
public static class LocalTimestampMillis extends LogicalType {
private LocalTimestampMillis() {
super(LOCAL_TIMESTAMP_MILLIS);
@@ -530,4 +565,18 @@ public class LogicalTypes {
}
}
+ public static class LocalTimestampNanos extends LogicalType {
+ private LocalTimestampNanos() {
+ super(LOCAL_TIMESTAMP_NANOS);
+ }
+
+ @Override
+ public void validate(Schema schema) {
+ super.validate(schema);
+ if (schema.getType() != Schema.Type.LONG) {
+ throw new IllegalArgumentException("Local timestamp (micros) can only
be used with an underlying long type");
+ }
+ }
+ }
+
}
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 785d31a51..e63ebaae6 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
@@ -204,6 +204,53 @@ public class TimeConversions {
}
}
+ public static class TimestampNanosConversion extends Conversion<Instant> {
+ @Override
+ public Class<Instant> getConvertedType() {
+ return Instant.class;
+ }
+
+ @Override
+ public String getLogicalTypeName() {
+ return "timestamp-nanos";
+ }
+
+ @Override
+ public String adjustAndSetValue(String varName, String valParamName) {
+ return varName + " = " + valParamName +
".truncatedTo(java.time.temporal.ChronoUnit.NANOS);";
+ }
+
+ @Override
+ public Instant fromLong(Long microsFromEpoch, Schema schema, LogicalType
type) {
+ long epochSeconds = microsFromEpoch / 1_000_000_000L;
+ long nanoAdjustment = microsFromEpoch % 1_000_000_000L;
+
+ return Instant.ofEpochSecond(epochSeconds, nanoAdjustment);
+ }
+
+ @Override
+ public Long toLong(Instant instant, Schema schema, LogicalType type) {
+ long seconds = instant.getEpochSecond();
+ int nanos = instant.getNano();
+
+ if (seconds < 0 && nanos > 0) {
+ long micros = Math.multiplyExact(seconds + 1, 1_000_000_000L);
+ long adjustment = nanos - 1_000_000;
+
+ return Math.addExact(micros, adjustment);
+ } else {
+ long micros = Math.multiplyExact(seconds, 1_000_000_000L);
+
+ return Math.addExact(micros, nanos);
+ }
+ }
+
+ @Override
+ public Schema getRecommendedSchema() {
+ return
LogicalTypes.timestampNanos().addToSchema(Schema.create(Schema.Type.LONG));
+ }
+ }
+
public static class LocalTimestampMillisConversion extends
Conversion<LocalDateTime> {
private final TimestampMillisConversion timestampMillisConversion = new
TimestampMillisConversion();
@@ -265,4 +312,35 @@ public class TimeConversions {
return
LogicalTypes.localTimestampMicros().addToSchema(Schema.create(Schema.Type.LONG));
}
}
+
+ public static class LocalTimestampNanosConversion extends
Conversion<LocalDateTime> {
+ private final TimestampNanosConversion timestampNanosConversion = new
TimestampNanosConversion();
+
+ @Override
+ public Class<LocalDateTime> getConvertedType() {
+ return LocalDateTime.class;
+ }
+
+ @Override
+ public String getLogicalTypeName() {
+ return "local-timestamp-nanos";
+ }
+
+ @Override
+ public LocalDateTime fromLong(Long microsFromEpoch, Schema schema,
LogicalType type) {
+ Instant instant = timestampNanosConversion.fromLong(microsFromEpoch,
schema, type);
+ return LocalDateTime.ofInstant(instant, ZoneOffset.UTC);
+ }
+
+ @Override
+ public Long toLong(LocalDateTime timestamp, Schema schema, LogicalType
type) {
+ Instant instant = timestamp.toInstant(ZoneOffset.UTC);
+ return timestampNanosConversion.toLong(instant, schema, type);
+ }
+
+ @Override
+ public Schema getRecommendedSchema() {
+ return
LogicalTypes.localTimestampNanos().addToSchema(Schema.create(Schema.Type.LONG));
+ }
+ }
}
diff --git
a/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java
b/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java
index 117fd2ed6..c6917a25f 100644
---
a/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java
+++
b/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java
@@ -106,8 +106,10 @@ public class SpecificCompiler {
specificData.addLogicalTypeConversion(new
TimeConversions.TimeMicrosConversion());
specificData.addLogicalTypeConversion(new
TimeConversions.TimestampMillisConversion());
specificData.addLogicalTypeConversion(new
TimeConversions.TimestampMicrosConversion());
+ specificData.addLogicalTypeConversion(new
TimeConversions.TimestampNanosConversion());
specificData.addLogicalTypeConversion(new
TimeConversions.LocalTimestampMicrosConversion());
specificData.addLogicalTypeConversion(new
TimeConversions.LocalTimestampMillisConversion());
+ specificData.addLogicalTypeConversion(new
TimeConversions.LocalTimestampNanosConversion());
specificData.addLogicalTypeConversion(new Conversions.UUIDConversion());
}
diff --git
a/lang/java/compiler/src/test/java/org/apache/avro/compiler/specific/TestSpecificCompiler.java
b/lang/java/compiler/src/test/java/org/apache/avro/compiler/specific/TestSpecificCompiler.java
index cc3fcd312..a55aaec31 100644
---
a/lang/java/compiler/src/test/java/org/apache/avro/compiler/specific/TestSpecificCompiler.java
+++
b/lang/java/compiler/src/test/java/org/apache/avro/compiler/specific/TestSpecificCompiler.java
@@ -393,6 +393,7 @@ public class TestSpecificCompiler {
Schema timeMicrosSchema =
LogicalTypes.timeMicros().addToSchema(Schema.create(Schema.Type.LONG));
Schema timestampSchema =
LogicalTypes.timestampMillis().addToSchema(Schema.create(Schema.Type.LONG));
Schema timestampMicrosSchema =
LogicalTypes.timestampMicros().addToSchema(Schema.create(Schema.Type.LONG));
+ Schema timestampNanosSchema =
LogicalTypes.timestampNanos().addToSchema(Schema.create(Schema.Type.LONG));
// Date/time types should always use upper level java classes
assertEquals("java.time.LocalDate", compiler.javaType(dateSchema), "Should
use java.time.LocalDate for date type");
@@ -404,6 +405,8 @@ public class TestSpecificCompiler {
"Should use java.time.LocalTime for time-micros type");
assertEquals("java.time.Instant", compiler.javaType(timestampMicrosSchema),
"Should use java.time.Instant for timestamp-micros type");
+ assertEquals("java.time.Instant", compiler.javaType(timestampNanosSchema),
+ "Should use java.time.Instant for timestamp-nanos type");
}
@Test
@@ -582,15 +585,18 @@ public class TestSpecificCompiler {
// present or added as converters (AVRO-2481).
final Schema tsMillis =
LogicalTypes.timestampMillis().addToSchema(Schema.create(Schema.Type.LONG));
final Schema tsMicros =
LogicalTypes.timestampMicros().addToSchema(Schema.create(Schema.Type.LONG));
+ final Schema tsNanos =
LogicalTypes.timestampNanos().addToSchema(Schema.create(Schema.Type.LONG));
final Collection<String> conversions =
compiler.getUsedConversionClasses(SchemaBuilder.record("WithTimestamps")
.fields().name("tsMillis").type(tsMillis).noDefault().name("tsMillisOpt").type().unionOf().nullType().and()
.type(tsMillis).endUnion().noDefault().name("tsMicros").type(tsMicros).noDefault().name("tsMicrosOpt").type()
-
.unionOf().nullType().and().type(tsMicros).endUnion().noDefault().endRecord());
+
.unionOf().nullType().and().type(tsMicros).endUnion().noDefault().name("tsNanos").type(tsNanos).noDefault()
+
.name("tsNanosOpt").type().unionOf().nullType().and().type(tsNanos).endUnion().noDefault().endRecord());
- assertEquals(2, conversions.size());
+ assertEquals(3, conversions.size());
assertThat(conversions,
hasItem("org.apache.avro.data.TimeConversions.TimestampMillisConversion"));
assertThat(conversions,
hasItem("org.apache.avro.data.TimeConversions.TimestampMicrosConversion"));
+ assertThat(conversions,
hasItem("org.apache.avro.data.TimeConversions.TimestampNanosConversion"));
}
@Test
diff --git
a/lang/java/protobuf/src/main/java/org/apache/avro/protobuf/ProtoConversions.java
b/lang/java/protobuf/src/main/java/org/apache/avro/protobuf/ProtoConversions.java
index 424e94c7e..c65d0eb57 100644
---
a/lang/java/protobuf/src/main/java/org/apache/avro/protobuf/ProtoConversions.java
+++
b/lang/java/protobuf/src/main/java/org/apache/avro/protobuf/ProtoConversions.java
@@ -25,8 +25,9 @@ import org.apache.avro.Schema;
public class ProtoConversions {
- private static final int THOUSAND = 1000;
- private static final int MILLION = 1000000;
+ private static final int THOUSAND = 1_000;
+ private static final int MILLION = 1_000_000;
+ private static final int BILLION = 1_000_000_000;
// second value must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z
// inclusive.
@@ -39,7 +40,7 @@ public class ProtoConversions {
// timestamp precise of conversion from long
private enum TimestampPrecise {
- Millis, Micros
+ Millis, Micros, Nanos
};
public static class TimestampMillisConversion extends Conversion<Timestamp> {
@@ -96,6 +97,33 @@ public class ProtoConversions {
}
}
+ public static class TimestampNanosConversion extends Conversion<Timestamp> {
+ @Override
+ public Class<Timestamp> getConvertedType() {
+ return Timestamp.class;
+ }
+
+ @Override
+ public String getLogicalTypeName() {
+ return "timestamp-nanos";
+ }
+
+ @Override
+ public Timestamp fromLong(Long nanosFromEpoch, Schema schema, LogicalType
type) throws IllegalArgumentException {
+ return ProtoConversions.fromLong(nanosFromEpoch, TimestampPrecise.Nanos);
+ }
+
+ @Override
+ public Long toLong(Timestamp value, Schema schema, LogicalType type) {
+ return ProtoConversions.toLong(value, TimestampPrecise.Nanos);
+ }
+
+ @Override
+ public Schema getRecommendedSchema() {
+ return
LogicalTypes.timestampNanos().addToSchema(Schema.create(Schema.Type.LONG));
+ }
+ }
+
private static long toLong(Timestamp value, TimestampPrecise precise) {
long rv = 0L;
@@ -112,8 +140,8 @@ public class ProtoConversions {
}
private static Timestamp fromLong(Long elapsedSinceEpoch, TimestampPrecise
precise) throws IllegalArgumentException {
- long seconds = 0L;
- int nanos = 0;
+ final long seconds;
+ final int nanos;
switch (precise) {
case Millis:
@@ -124,6 +152,12 @@ public class ProtoConversions {
seconds = Math.floorDiv(elapsedSinceEpoch, (long) MILLION);
nanos = (int) Math.floorMod(elapsedSinceEpoch, (long) MILLION) *
THOUSAND;
break;
+ case Nanos:
+ seconds = Math.floorDiv(elapsedSinceEpoch, (long) BILLION);
+ nanos = (int) Math.floorMod(elapsedSinceEpoch, (long) BILLION);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown precision: " + precise);
}
if (seconds < SECONDS_LOWERLIMIT || seconds > SECONDS_UPPERLIMIT) {