Skip to content

Commit

Permalink
Merge pull request #105 from heetch/logical-type/duration
Browse files Browse the repository at this point in the history
  • Loading branch information
sixstone-qq authored Nov 18, 2021
2 parents 576777e + b464747 commit 180ed33
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 15 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ When the `avrogo` command generates Go datatypes from Avro schemas, it uses the
as `time.Time` type
- `{"type": "string", "logicalType": "uuid"}` is represented as
[github.com/google/uuid.UUID](https://pkg.go.dev/github.com/google/uuid#UUID) type.
- `{"type": "long", "name": "duration-nanos"}` is represented as `time.Duration` type.

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).

Expand Down
9 changes: 5 additions & 4 deletions analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import (
)

var (
timeType = reflect.TypeOf(time.Time{})
byteType = reflect.TypeOf(byte(0))
uuidType = reflect.TypeOf(gouuid.UUID{})
timeType = reflect.TypeOf(time.Time{})
durationType = reflect.TypeOf(time.Duration(0))
byteType = reflect.TypeOf(byte(0))
uuidType = reflect.TypeOf(gouuid.UUID{})
)

type decodeProgram struct {
Expand Down Expand Up @@ -492,7 +493,7 @@ func canAssignVMType(operand int, dstType reflect.Type) bool {
case vm.Boolean:
return dstKind == reflect.Bool
case vm.Int, vm.Long:
return dstType == timeType || reflect.Int <= dstKind && dstKind <= reflect.Int64
return dstType == timeType || dstType == durationType || reflect.Int <= dstKind && dstKind <= reflect.Int64
case vm.Float, vm.Double:
return dstKind == reflect.Float64 || dstKind == reflect.Float32
case vm.Bytes:
Expand Down
11 changes: 8 additions & 3 deletions cmd/avrogo/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
)

const (
durationNanos = "duration-nanos"
timestampMicros = "timestamp-micros"
timestampMillis = "timestamp-millis"
uuid = "uuid"
Expand Down Expand Up @@ -476,11 +477,15 @@ func (gc *generateContext) GoTypeOf(t schema.AvroType) typeInfo {
// Note: Go int is at least 32 bits.
info.GoType = "int"
case *schema.LongField:
// TODO support timestampMillis. https://github.com/heetch/avro/issues/3
if logicalType(t) == timestampMicros {
switch logicalType(t) {
case timestampMicros:
// TODO support timestampMillis. https://github.com/heetch/avro/issues/3
info.GoType = "time.Time"
gc.addImport("time")
} else {
case durationNanos:
info.GoType = "time.Duration"
gc.addImport("time")
default:
info.GoType = "int64"
}
case *schema.FloatField:
Expand Down
17 changes: 17 additions & 0 deletions cmd/avrogo/testdata/logicaltype.cue
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,20 @@ tests: invalidUUID: {
outData: null
expectError: unmarshal: "invalid UUID in Avro encoding: invalid UUID length: 12"
}

tests: durationNanos: {
inSchema: {
type: "record"
name: "R"
fields: [{
name: "D"
type: {
type: "long"
logicalType: "duration-nanos"
}
}]
}
outSchema: inSchema
inData: D: 15000000000
outData: inData
}
7 changes: 6 additions & 1 deletion cue.mod/pkg/github.com/heetch/cue-schema/avro/schema.cue
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ LogicalType :: {
logicalType: string
}

LogicalType :: DecimalBytes | DecimalFixed | UUID | Date | *TimeMillis | *TimeMicros | TimestampMillis | TimestampMicros
LogicalType :: DecimalBytes | DecimalFixed | UUID | Date | *TimeMillis | *TimeMicros | TimestampMillis | TimestampMicros | DurationNanos

DecimalBytes :: {
type: "bytes"
Expand Down Expand Up @@ -139,3 +139,8 @@ TimestampMicros :: {
type: "long"
logicalType: "timestamp-micros"
}

DurationNanos :: {
type: "long"
logicalType: "duration-nanos"
}
10 changes: 7 additions & 3 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,16 @@ func (d *decoder) eval(target reflect.Value) {
// need more information from the VM to be able to
// do that, so support only timestamp-micros for now.
// See https://github.com/heetch/avro/issues/3
if target.Type() == timeType {
switch target.Type() {
case timeType:
// timestamp-micros
target.Set(reflect.ValueOf(time.Unix(frame.Int/1e6, frame.Int%1e6*1e3)))
break
case durationType:
// duration-nanos
target.Set(reflect.ValueOf(time.Duration(frame.Int)))
default:
target.SetInt(frame.Int)
}
target.SetInt(frame.Int)
case vm.Int:
target.SetInt(frame.Int)
case vm.Float, vm.Double:
Expand Down
17 changes: 15 additions & 2 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,15 +219,23 @@ func (b *encoderBuilder) typeEncoder(at schema.AvroType, t reflect.Type, info ty
case *schema.NullField:
return nullEncoder
case *schema.LongField:
if t == timeType {
switch t {
case timeType:
if lt := logicalType(at); lt == timestampMicros {
return timestampMicrosEncoder
} else {
// TODO timestamp-millis support.
return errorEncoder(fmt.Errorf("cannot encode time.Time as long with logical type %q", lt))
}
case durationType:
if lt := logicalType(at); lt == durationNanos {
return durationNanosEncoder
} else {
return errorEncoder(fmt.Errorf("cannot encode %t as long with logical type %q", t, lt))
}
default:
return longEncoder
}
return longEncoder
case *schema.StringField:
if t == uuidType {
if lt := logicalType(at); lt == uuid {
Expand Down Expand Up @@ -277,6 +285,11 @@ func uuidEncoder(e *encodeState, v reflect.Value) {
}
}

func durationNanosEncoder(e *encodeState, v reflect.Value) {
d := v.Interface().(time.Duration)
e.writeLong(d.Nanoseconds())
}

type fixedEncoder struct {
size int
}
Expand Down
12 changes: 10 additions & 2 deletions gotype.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
)

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

type goTypeDef struct {
// name holds the Avro name for the Go type.
name string
name string
// schema holds the JSON-marshalable schema for the type.
schema interface{}
}
Expand All @@ -134,7 +136,7 @@ type goTypeSchema struct {
names *Names
// defs maps from Go type to Avro definition for all
// types being traversed by schemaForGoType..
defs map[reflect.Type]goTypeDef
defs map[reflect.Type]goTypeDef
}

func (gts *goTypeSchema) schemaForGoType(t reflect.Type) (interface{}, error) {
Expand Down Expand Up @@ -164,6 +166,12 @@ func (gts *goTypeSchema) schemaForGoType(t reflect.Type) (interface{}, error) {
case reflect.String:
return "string", nil
case reflect.Int, reflect.Int64, reflect.Uint32:
if t == durationType {
return map[string]interface{}{
"type": "long",
"logicalType": durationNanos,
}, nil
}
return "long", nil
case reflect.Int32, reflect.Int16, reflect.Uint16, reflect.Int8, reflect.Uint8:
return "int", nil
Expand Down
45 changes: 45 additions & 0 deletions gotype_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,51 @@ func TestGoTypeWithUUID(t *testing.T) {

}

func TestGoTypeWithDuration(t *testing.T) {
c := qt.New(t)
type R struct {
D time.Duration
}

data, wType, err := avro.Marshal(R{
D: 15 * time.Second,
})
c.Assert(err, qt.IsNil)
var x R
_, err = avro.Unmarshal(data, &x, wType)
c.Assert(err, qt.IsNil)
c.Assert(x, qt.DeepEquals, R{
D: 15 * time.Second,
})

c.Assert(mustTypeOf(R{}).String(), qt.JSONEquals, json.RawMessage(`{
"type": "record",
"name": "R",
"fields": [{
"name": "D",
"default": 0,
"type": {
"logicalType": "duration-nanos",
"type": "long"
}
}]
}`))

c.Run("zero", func(c *qt.C) {
data, wType, err := avro.Marshal(R{})
c.Assert(err, qt.IsNil)
{
type R struct {
D int64
}
var x R
_, err = avro.Unmarshal(data, &x, wType)
c.Assert(err, qt.IsNil)
c.Assert(x, qt.DeepEquals, R{})
}
})
}

func TestGoTypeWithStructField(t *testing.T) {
c := qt.New(t)
type F2 struct {
Expand Down

0 comments on commit 180ed33

Please sign in to comment.