Skip to content

Commit abe6d42

Browse files
authored
AVRO-2123: Java duration logical type (#2520)
This adds a new `java.time.TemporalAmount` implementation, which supports the Avro `duration` logical type (neither `java.time.Period` nor `java.time.Duration` support it). This type is then used in the conversion for the (new) logical type implementation "duration". Last, the logical type "uuid" is refactored to include validation.
1 parent 119c3fc commit abe6d42

File tree

6 files changed

+876
-34
lines changed

6 files changed

+876
-34
lines changed

lang/java/avro/src/main/java/org/apache/avro/Conversions.java

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,18 @@
1818

1919
package org.apache.avro;
2020

21-
import java.math.RoundingMode;
2221
import org.apache.avro.generic.GenericData;
2322
import org.apache.avro.generic.GenericEnumSymbol;
2423
import org.apache.avro.generic.GenericFixed;
2524
import org.apache.avro.generic.IndexedRecord;
25+
import org.apache.avro.util.TimePeriod;
2626

2727
import java.math.BigDecimal;
2828
import java.math.BigInteger;
29+
import java.math.RoundingMode;
2930
import java.nio.ByteBuffer;
31+
import java.nio.ByteOrder;
32+
import java.nio.IntBuffer;
3033
import java.util.Arrays;
3134
import java.util.Collection;
3235
import java.util.Map;
@@ -147,6 +150,43 @@ private static BigDecimal validate(final LogicalTypes.Decimal decimal, BigDecima
147150
}
148151
}
149152

153+
public static class DurationConversion extends Conversion<TimePeriod> {
154+
@Override
155+
public Class<TimePeriod> getConvertedType() {
156+
return TimePeriod.class;
157+
}
158+
159+
@Override
160+
public String getLogicalTypeName() {
161+
return "duration";
162+
}
163+
164+
@Override
165+
public Schema getRecommendedSchema() {
166+
return LogicalTypes.duration().addToSchema(Schema.createFixed("time.Duration",
167+
"A 12-byte byte array encoding a duration in months, days and milliseconds.", null, 12));
168+
}
169+
170+
@Override
171+
public TimePeriod fromFixed(GenericFixed value, Schema schema, LogicalType type) {
172+
IntBuffer buffer = ByteBuffer.wrap(value.bytes()).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer();
173+
long months = Integer.toUnsignedLong(buffer.get());
174+
long days = Integer.toUnsignedLong(buffer.get());
175+
long millis = Integer.toUnsignedLong(buffer.get());
176+
return TimePeriod.of(months, days, millis);
177+
}
178+
179+
@Override
180+
public GenericFixed toFixed(TimePeriod value, Schema schema, LogicalType type) {
181+
ByteBuffer buffer = ByteBuffer.allocate(12).order(ByteOrder.LITTLE_ENDIAN);
182+
IntBuffer intBuffer = buffer.asIntBuffer();
183+
intBuffer.put((int) value.getMonths());
184+
intBuffer.put((int) value.getDays());
185+
intBuffer.put((int) value.getMillis());
186+
return new GenericData.Fixed(schema, buffer.array());
187+
}
188+
}
189+
150190
/**
151191
* Convert an underlying representation of a logical type (such as a ByteBuffer)
152192
* to a higher level object (such as a BigDecimal).

lang/java/avro/src/main/java/org/apache/avro/LogicalTypes.java

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@
1818

1919
package org.apache.avro;
2020

21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
2124
import java.util.Collections;
2225
import java.util.Map;
2326
import java.util.Objects;
2427
import java.util.ServiceLoader;
2528
import java.util.concurrent.ConcurrentHashMap;
2629

27-
import org.slf4j.Logger;
28-
import org.slf4j.LoggerFactory;
29-
3030
public class LogicalTypes {
3131

3232
private static final Logger LOG = LoggerFactory.getLogger(LogicalTypes.class);
@@ -182,6 +182,7 @@ private static LogicalType fromSchemaImpl(Schema schema, boolean throwErrors) {
182182
}
183183

184184
private static final String DECIMAL = "decimal";
185+
private static final String DURATION = "duration";
185186
private static final String UUID = "uuid";
186187
private static final String DATE = "date";
187188
private static final String TIME_MILLIS = "time-millis";
@@ -201,12 +202,18 @@ public static Decimal decimal(int precision, int scale) {
201202
return new Decimal(precision, scale);
202203
}
203204

204-
private static final LogicalType UUID_TYPE = new LogicalType("uuid");
205+
private static final LogicalType UUID_TYPE = new Uuid();
205206

206207
public static LogicalType uuid() {
207208
return UUID_TYPE;
208209
}
209210

211+
private static final LogicalType DURATION_TYPE = new Duration();
212+
213+
public static LogicalType duration() {
214+
return DURATION_TYPE;
215+
}
216+
210217
private static final Date DATE_TYPE = new Date();
211218

212219
public static Date date() {
@@ -249,6 +256,38 @@ public static LocalTimestampMicros localTimestampMicros() {
249256
return LOCAL_TIMESTAMP_MICROS_TYPE;
250257
}
251258

259+
/** Uuid represents a uuid without a time */
260+
public static class Uuid extends LogicalType {
261+
private Uuid() {
262+
super(UUID);
263+
}
264+
265+
@Override
266+
public void validate(Schema schema) {
267+
super.validate(schema);
268+
if (schema.getType() != Schema.Type.STRING) {
269+
throw new IllegalArgumentException("Uuid can only be used with an underlying string type");
270+
}
271+
}
272+
}
273+
274+
/**
275+
* Duration represents a duration, consisting on months, days and milliseconds
276+
*/
277+
public static class Duration extends LogicalType {
278+
private Duration() {
279+
super(DURATION);
280+
}
281+
282+
@Override
283+
public void validate(Schema schema) {
284+
super.validate(schema);
285+
if (schema.getType() != Schema.Type.FIXED || schema.getFixedSize() != 12) {
286+
throw new IllegalArgumentException("Duration can only be used with an underlying fixed type of size 12.");
287+
}
288+
}
289+
}
290+
252291
/** Decimal represents arbitrary-precision fixed-scale decimal numbers */
253292
public static class Decimal extends LogicalType {
254293
private static final String PRECISION_PROP = "precision";

0 commit comments

Comments
 (0)