Skip to content

Commit

Permalink
[AVRO-4056][Java] Add time-nanos logical type
Browse files Browse the repository at this point in the history
  • Loading branch information
glywk committed Sep 11, 2024
1 parent e00df25 commit 5fef664
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 38 deletions.
25 changes: 25 additions & 0 deletions lang/java/avro/src/main/java/org/apache/avro/LogicalTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ private static LogicalType fromSchemaImpl(Schema schema, boolean throwErrors) {
case TIME_MICROS:
logicalType = TIME_MICROS_TYPE;
break;
case TIME_NANOS:
logicalType = TIME_NANOS_TYPE;
break;
case LOCAL_TIMESTAMP_MICROS:
logicalType = LOCAL_TIMESTAMP_MICROS_TYPE;
break;
Expand Down Expand Up @@ -197,6 +200,7 @@ private static LogicalType fromSchemaImpl(Schema schema, boolean throwErrors) {
private static final String DATE = "date";
private static final String TIME_MILLIS = "time-millis";
private static final String TIME_MICROS = "time-micros";
private static final String TIME_NANOS = "time-nanos";
private static final String TIMESTAMP_MILLIS = "timestamp-millis";
private static final String TIMESTAMP_MICROS = "timestamp-micros";
private static final String TIMESTAMP_NANOS = "timestamp-nanos";
Expand Down Expand Up @@ -251,6 +255,12 @@ public static TimeMicros timeMicros() {
return TIME_MICROS_TYPE;
}

private static final TimeNanos TIME_NANOS_TYPE = new TimeNanos();

public static TimeNanos timeNanos() {
return TIME_NANOS_TYPE;
}

private static final TimestampMillis TIMESTAMP_MILLIS_TYPE = new TimestampMillis();

public static TimestampMillis timestampMillis() {
Expand Down Expand Up @@ -498,6 +508,21 @@ public void validate(Schema schema) {
}
}

/** TimeNanos represents a time in nanoseconds without a date */
public static class TimeNanos extends LogicalType {
private TimeNanos() {
super(TIME_NANOS);
}

@Override
public void validate(Schema schema) {
super.validate(schema);
if (schema.getType() != Schema.Type.LONG) {
throw new IllegalArgumentException("Time (nanos) can only be used with an underlying long type");
}
}
}

/** TimestampMillis represents a date and time in milliseconds */
public static class TimestampMillis extends LogicalType {
private TimestampMillis() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,38 @@ public Schema getRecommendedSchema() {
}
}

public static class TimeNanosConversion extends Conversion<LocalTime> {
@Override
public Class<LocalTime> getConvertedType() {
return LocalTime.class;
}

@Override
public String getLogicalTypeName() {
return "time-nanos";
}

@Override
public String adjustAndSetValue(String varName, String valParamName) {
return varName + " = " + valParamName + ".truncatedTo(java.time.temporal.ChronoUnit.NANOS);";
}

@Override
public LocalTime fromLong(Long nanosFromMidnight, Schema schema, LogicalType type) {
return LocalTime.ofNanoOfDay(TimeUnit.NANOSECONDS.toNanos(nanosFromMidnight));
}

@Override
public Long toLong(LocalTime time, Schema schema, LogicalType type) {
return TimeUnit.NANOSECONDS.toNanos(time.toNanoOfDay());
}

@Override
public Schema getRecommendedSchema() {
return LogicalTypes.timeNanos().addToSchema(Schema.create(Schema.Type.LONG));
}
}

public static class TimestampMillisConversion extends Conversion<Instant> {
@Override
public Class<Instant> getConvertedType() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.apache.avro.data.TimeConversions.DateConversion;
import org.apache.avro.data.TimeConversions.TimeMicrosConversion;
import org.apache.avro.data.TimeConversions.TimeMillisConversion;
import org.apache.avro.data.TimeConversions.TimeNanosConversion;
import org.apache.avro.data.TimeConversions.TimestampMicrosConversion;
import org.apache.avro.data.TimeConversions.TimestampMillisConversion;
import org.apache.avro.reflect.ReflectData;
Expand All @@ -42,6 +43,7 @@ public class TestTimeConversions {
public static Schema DATE_SCHEMA;
public static Schema TIME_MILLIS_SCHEMA;
public static Schema TIME_MICROS_SCHEMA;
public static Schema TIME_NANOS_SCHEMA;
public static Schema TIMESTAMP_MILLIS_SCHEMA;
public static Schema TIMESTAMP_MICROS_SCHEMA;

Expand All @@ -50,6 +52,7 @@ public static void createSchemas() {
TestTimeConversions.DATE_SCHEMA = LogicalTypes.date().addToSchema(Schema.create(Schema.Type.INT));
TestTimeConversions.TIME_MILLIS_SCHEMA = LogicalTypes.timeMillis().addToSchema(Schema.create(Schema.Type.INT));
TestTimeConversions.TIME_MICROS_SCHEMA = LogicalTypes.timeMicros().addToSchema(Schema.create(Schema.Type.LONG));
TestTimeConversions.TIME_NANOS_SCHEMA = LogicalTypes.timeNanos().addToSchema(Schema.create(Schema.Type.LONG));
TestTimeConversions.TIMESTAMP_MILLIS_SCHEMA = LogicalTypes.timestampMillis()
.addToSchema(Schema.create(Schema.Type.LONG));
TestTimeConversions.TIMESTAMP_MICROS_SCHEMA = LogicalTypes.timestampMicros()
Expand Down Expand Up @@ -116,6 +119,28 @@ void timeMicrosConversion() throws Exception {
"15:14:15.926551 should be " + afternoonMicros);
}

@Test
void timeNanosConversion() throws Exception {
TimeNanosConversion conversion = new TimeNanosConversion();
LocalTime oneAM = LocalTime.of(1, 0);
LocalTime afternoon = LocalTime.of(15, 14, 15, 926_551_123);
long afternoonNanos = ((long) (15 * 60 + 14) * 60 + 15) * 1_000_000_000 + 926_551_123;

assertEquals(LocalTime.MIDNIGHT, conversion.fromLong(0L, TIME_NANOS_SCHEMA, LogicalTypes.timeNanos()),
"Midnight should be 0");
assertEquals(oneAM, conversion.fromLong(3_600_000_000_000L, TIME_NANOS_SCHEMA, LogicalTypes.timeNanos()),
"01:00 should be 3,600,000,000,000");
assertEquals(afternoon, conversion.fromLong(afternoonNanos, TIME_NANOS_SCHEMA, LogicalTypes.timeNanos()),
"15:14:15.926551123 should be " + afternoonNanos);

assertEquals(0, (long) conversion.toLong(LocalTime.MIDNIGHT, TIME_NANOS_SCHEMA, LogicalTypes.timeNanos()),
"Midnight should be 0");
assertEquals(3_600_000_000_000L, (long) conversion.toLong(oneAM, TIME_NANOS_SCHEMA, LogicalTypes.timeNanos()),
"01:00 should be 3,600,000,000,000");
assertEquals(afternoonNanos, (long) conversion.toLong(afternoon, TIME_NANOS_SCHEMA, LogicalTypes.timeNanos()),
"15:14:15.926551123 should be " + afternoonNanos);
}

@Test
void timestampMillisConversion() throws Exception {
TimestampMillisConversion conversion = new TimestampMillisConversion();
Expand Down Expand Up @@ -209,6 +234,12 @@ void dynamicSchemaWithTimeMicrosConversion() throws ClassNotFoundException {
assertEquals(TIME_MICROS_SCHEMA, schema, "Reflected schema should be logicalType timeMicros");
}

@Test
void dynamicSchemaWithTimeNanosConversion() throws ClassNotFoundException {
Schema schema = getReflectedSchemaByName("java.time.LocalTime", new TimeConversions.TimeNanosConversion());
assertEquals(TIME_NANOS_SCHEMA, schema, "Reflected schema should be logicalType timeNanos");
}

@Test
void dynamicSchemaWithDateTimeConversion() throws ClassNotFoundException {
Schema schema = getReflectedSchemaByName("java.time.Instant", new TimeConversions.TimestampMillisConversion());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public class TestReflectLogicalTypes {
public static void addUUID() {
REFLECT.addLogicalTypeConversion(new Conversions.UUIDConversion());
REFLECT.addLogicalTypeConversion(new Conversions.DecimalConversion());
REFLECT.addLogicalTypeConversion(new TimeConversions.TimeNanosConversion());
REFLECT.addLogicalTypeConversion(new TimeConversions.LocalTimestampMillisConversion());
}

Expand Down
Loading

0 comments on commit 5fef664

Please sign in to comment.