Skip to content

Commit b464747

Browse files
committedNov 18, 2021
Implement time.Duration as logical type "duration-nanos"
That uses "long" as underlying type. This is not part of the Avro specification that holds "duration" logical type that does not have a 1on1 relationship with any std library package thus this extension has been implemented.
1 parent 018484b commit b464747

File tree

9 files changed

+114
-15
lines changed

9 files changed

+114
-15
lines changed
 

‎README.md

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ When the `avrogo` command generates Go datatypes from Avro schemas, it uses the
3838
as `time.Time` type
3939
- `{"type": "string", "logicalType": "uuid"}` is represented as
4040
[github.com/google/uuid.UUID](https://pkg.go.dev/github.com/google/uuid#UUID) type.
41+
- `{"type": "long", "name": "duration-nanos"}` is represented as `time.Duration` type.
4142

4243
If a definition has a `go.package` annotation the type from that package will be used instead of generating a Go type. The type must be compatible with the Avro schema (it may contain extra fields, but all fields in common must be compatible).
4344

‎analyze.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ import (
1616
)
1717

1818
var (
19-
timeType = reflect.TypeOf(time.Time{})
20-
byteType = reflect.TypeOf(byte(0))
21-
uuidType = reflect.TypeOf(gouuid.UUID{})
19+
timeType = reflect.TypeOf(time.Time{})
20+
durationType = reflect.TypeOf(time.Duration(0))
21+
byteType = reflect.TypeOf(byte(0))
22+
uuidType = reflect.TypeOf(gouuid.UUID{})
2223
)
2324

2425
type decodeProgram struct {
@@ -492,7 +493,7 @@ func canAssignVMType(operand int, dstType reflect.Type) bool {
492493
case vm.Boolean:
493494
return dstKind == reflect.Bool
494495
case vm.Int, vm.Long:
495-
return dstType == timeType || reflect.Int <= dstKind && dstKind <= reflect.Int64
496+
return dstType == timeType || dstType == durationType || reflect.Int <= dstKind && dstKind <= reflect.Int64
496497
case vm.Float, vm.Double:
497498
return dstKind == reflect.Float64 || dstKind == reflect.Float32
498499
case vm.Bytes:

‎cmd/avrogo/generate.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
)
1616

1717
const (
18+
durationNanos = "duration-nanos"
1819
timestampMicros = "timestamp-micros"
1920
timestampMillis = "timestamp-millis"
2021
uuid = "uuid"
@@ -476,11 +477,15 @@ func (gc *generateContext) GoTypeOf(t schema.AvroType) typeInfo {
476477
// Note: Go int is at least 32 bits.
477478
info.GoType = "int"
478479
case *schema.LongField:
479-
// TODO support timestampMillis. https://github.com/heetch/avro/issues/3
480-
if logicalType(t) == timestampMicros {
480+
switch logicalType(t) {
481+
case timestampMicros:
482+
// TODO support timestampMillis. https://github.com/heetch/avro/issues/3
481483
info.GoType = "time.Time"
482484
gc.addImport("time")
483-
} else {
485+
case durationNanos:
486+
info.GoType = "time.Duration"
487+
gc.addImport("time")
488+
default:
484489
info.GoType = "int64"
485490
}
486491
case *schema.FloatField:

‎cmd/avrogo/testdata/logicaltype.cue

+17
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,20 @@ tests: invalidUUID: {
5151
outData: null
5252
expectError: unmarshal: "invalid UUID in Avro encoding: invalid UUID length: 12"
5353
}
54+
55+
tests: durationNanos: {
56+
inSchema: {
57+
type: "record"
58+
name: "R"
59+
fields: [{
60+
name: "D"
61+
type: {
62+
type: "long"
63+
logicalType: "duration-nanos"
64+
}
65+
}]
66+
}
67+
outSchema: inSchema
68+
inData: D: 15000000000
69+
outData: inData
70+
}

‎cue.mod/pkg/github.com/heetch/cue-schema/avro/schema.cue

+6-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ LogicalType :: {
9191
logicalType: string
9292
}
9393

94-
LogicalType :: DecimalBytes | DecimalFixed | UUID | Date | *TimeMillis | *TimeMicros | TimestampMillis | TimestampMicros
94+
LogicalType :: DecimalBytes | DecimalFixed | UUID | Date | *TimeMillis | *TimeMicros | TimestampMillis | TimestampMicros | DurationNanos
9595

9696
DecimalBytes :: {
9797
type: "bytes"
@@ -139,3 +139,8 @@ TimestampMicros :: {
139139
type: "long"
140140
logicalType: "timestamp-micros"
141141
}
142+
143+
DurationNanos :: {
144+
type: "long"
145+
logicalType: "duration-nanos"
146+
}

‎decode.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,16 @@ func (d *decoder) eval(target reflect.Value) {
149149
// need more information from the VM to be able to
150150
// do that, so support only timestamp-micros for now.
151151
// See https://github.com/heetch/avro/issues/3
152-
if target.Type() == timeType {
152+
switch target.Type() {
153+
case timeType:
153154
// timestamp-micros
154155
target.Set(reflect.ValueOf(time.Unix(frame.Int/1e6, frame.Int%1e6*1e3)))
155-
break
156+
case durationType:
157+
// duration-nanos
158+
target.Set(reflect.ValueOf(time.Duration(frame.Int)))
159+
default:
160+
target.SetInt(frame.Int)
156161
}
157-
target.SetInt(frame.Int)
158162
case vm.Int:
159163
target.SetInt(frame.Int)
160164
case vm.Float, vm.Double:

‎encode.go

+15-2
Original file line numberDiff line numberDiff line change
@@ -219,15 +219,23 @@ func (b *encoderBuilder) typeEncoder(at schema.AvroType, t reflect.Type, info ty
219219
case *schema.NullField:
220220
return nullEncoder
221221
case *schema.LongField:
222-
if t == timeType {
222+
switch t {
223+
case timeType:
223224
if lt := logicalType(at); lt == timestampMicros {
224225
return timestampMicrosEncoder
225226
} else {
226227
// TODO timestamp-millis support.
227228
return errorEncoder(fmt.Errorf("cannot encode time.Time as long with logical type %q", lt))
228229
}
230+
case durationType:
231+
if lt := logicalType(at); lt == durationNanos {
232+
return durationNanosEncoder
233+
} else {
234+
return errorEncoder(fmt.Errorf("cannot encode %t as long with logical type %q", t, lt))
235+
}
236+
default:
237+
return longEncoder
229238
}
230-
return longEncoder
231239
case *schema.StringField:
232240
if t == uuidType {
233241
if lt := logicalType(at); lt == uuid {
@@ -277,6 +285,11 @@ func uuidEncoder(e *encodeState, v reflect.Value) {
277285
}
278286
}
279287

288+
func durationNanosEncoder(e *encodeState, v reflect.Value) {
289+
d := v.Interface().(time.Duration)
290+
e.writeLong(d.Nanoseconds())
291+
}
292+
280293
type fixedEncoder struct {
281294
size int
282295
}

‎gotype.go

+10-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
)
1414

1515
const (
16+
durationNanos = "duration-nanos"
1617
timestampMicros = "timestamp-micros"
1718
timestampMillis = "timestamp-millis"
1819
uuid = "uuid"
@@ -45,6 +46,7 @@ type errorSchema struct {
4546
// - float64 encodes as "double"
4647
// - string encodes as "string"
4748
// - Null{} encodes as "null"
49+
// - time.Duration encodes as {"type": "long", "logicalType": "duration-nanos"}
4850
// - time.Time encodes as {"type": "long", "logicalType": "timestamp-micros"}
4951
// - github.com/google/uuid.UUID encodes as {"type": "string", "logicalType": "string"}
5052
// - [N]byte encodes as {"type": "fixed", "name": "go.FixedN", "size": N}
@@ -123,7 +125,7 @@ func avroTypeOfUncached(names *Names, t reflect.Type) (*Type, error) {
123125

124126
type goTypeDef struct {
125127
// name holds the Avro name for the Go type.
126-
name string
128+
name string
127129
// schema holds the JSON-marshalable schema for the type.
128130
schema interface{}
129131
}
@@ -134,7 +136,7 @@ type goTypeSchema struct {
134136
names *Names
135137
// defs maps from Go type to Avro definition for all
136138
// types being traversed by schemaForGoType..
137-
defs map[reflect.Type]goTypeDef
139+
defs map[reflect.Type]goTypeDef
138140
}
139141

140142
func (gts *goTypeSchema) schemaForGoType(t reflect.Type) (interface{}, error) {
@@ -164,6 +166,12 @@ func (gts *goTypeSchema) schemaForGoType(t reflect.Type) (interface{}, error) {
164166
case reflect.String:
165167
return "string", nil
166168
case reflect.Int, reflect.Int64, reflect.Uint32:
169+
if t == durationType {
170+
return map[string]interface{}{
171+
"type": "long",
172+
"logicalType": durationNanos,
173+
}, nil
174+
}
167175
return "long", nil
168176
case reflect.Int32, reflect.Int16, reflect.Uint16, reflect.Int8, reflect.Uint8:
169177
return "int", nil

‎gotype_test.go

+45
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,51 @@ func TestGoTypeWithUUID(t *testing.T) {
251251

252252
}
253253

254+
func TestGoTypeWithDuration(t *testing.T) {
255+
c := qt.New(t)
256+
type R struct {
257+
D time.Duration
258+
}
259+
260+
data, wType, err := avro.Marshal(R{
261+
D: 15 * time.Second,
262+
})
263+
c.Assert(err, qt.IsNil)
264+
var x R
265+
_, err = avro.Unmarshal(data, &x, wType)
266+
c.Assert(err, qt.IsNil)
267+
c.Assert(x, qt.DeepEquals, R{
268+
D: 15 * time.Second,
269+
})
270+
271+
c.Assert(mustTypeOf(R{}).String(), qt.JSONEquals, json.RawMessage(`{
272+
"type": "record",
273+
"name": "R",
274+
"fields": [{
275+
"name": "D",
276+
"default": 0,
277+
"type": {
278+
"logicalType": "duration-nanos",
279+
"type": "long"
280+
}
281+
}]
282+
}`))
283+
284+
c.Run("zero", func(c *qt.C) {
285+
data, wType, err := avro.Marshal(R{})
286+
c.Assert(err, qt.IsNil)
287+
{
288+
type R struct {
289+
D int64
290+
}
291+
var x R
292+
_, err = avro.Unmarshal(data, &x, wType)
293+
c.Assert(err, qt.IsNil)
294+
c.Assert(x, qt.DeepEquals, R{})
295+
}
296+
})
297+
}
298+
254299
func TestGoTypeWithStructField(t *testing.T) {
255300
c := qt.New(t)
256301
type F2 struct {

0 commit comments

Comments
 (0)
Please sign in to comment.