Skip to content

Commit

Permalink
decimal: fix default values
Browse files Browse the repository at this point in the history
Unlike timestamp types which accepts native values, decimal types must
be big.Rat, which means defaults need to be converted.

This fixes: #202
  • Loading branch information
rockwotj committed Dec 18, 2024
1 parent d517b19 commit 750e1de
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 15 deletions.
2 changes: 1 addition & 1 deletion binary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func testBinaryDecodePass(t *testing.T, schema string, datum interface{}, encode
t.Helper()
codec, err := NewCodec(schema)
if err != nil {
t.Fatal(err)
t.Fatalf("unable to create codec: %s", err)
}

value, remaining, err := codec.NativeFromBinary(encoded)
Expand Down
7 changes: 6 additions & 1 deletion logical_type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ func TestDecimalBytesLogicalTypeEncode(t *testing.T) {
d, _ := new(big.Int).SetString("100000000000000000000000000000000000000", 10)
largeRat := new(big.Rat).SetFrac(n, d)
testBinaryCodecPass(t, largeDecimalSchema, largeRat, []byte("\x40\x1b\x4b\x68\x19\x26\x11\xfa\xea\x20\x8f\xca\x21\x62\x7b\xe9\xda\xee\x32\x19\x83\x83\x95\x5d\xe8\x13\x1f\x4b\xf1\xc7\x1c\x71\xc7"))

}

func TestDecimalFixedLogicalTypeEncode(t *testing.T) {
Expand All @@ -178,6 +177,12 @@ func TestDecimalBytesLogicalTypeInRecordEncode(t *testing.T) {
testBinaryCodecPass(t, schema, map[string]interface{}{"mydecimal": big.NewRat(617, 50)}, []byte("\x04\x04\xd2"))
}

func TestDecimalBytesLogicalTypeInRecordDecodeWithDefault(t *testing.T) {
schema := `{"type": "record", "name": "myrecord", "fields" : [
{"name": "mydecimal", "type": "bytes", "logicalType": "decimal", "precision": 4, "scale": 2, "default":"\u0000"}]}`
testBinaryCodecPass(t, schema, map[string]interface{}{"mydecimal": big.NewRat(617, 50)}, []byte("\x04\x04\xd2"))
}

func TestValidatedStringLogicalTypeInRecordEncode(t *testing.T) {
schema := `{
"type": "record",
Expand Down
29 changes: 22 additions & 7 deletions record.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,43 +66,43 @@ func makeRecordCodec(st map[string]*Codec, enclosingNamespace string, schemaMap
case "boolean":
v, ok := defaultValue.(bool)
if !ok {
return nil, fmt.Errorf("Record %q field %q: default value ought to encode using field schema: %s", c.typeName, fieldName, err)
return nil, fmt.Errorf("Record %q field %q: default value ought to have a bool type, got: %T", c.typeName, fieldName, defaultValue)
}
defaultValue = v
case "bytes":
v, ok := defaultValue.(string)
if !ok {
return nil, fmt.Errorf("Record %q field %q: default value ought to encode using field schema: %s", c.typeName, fieldName, err)
return nil, fmt.Errorf("Record %q field %q: default value ought to have a string type got: %T", c.typeName, fieldName, defaultValue)
}
defaultValue = []byte(v)
case "double":
v, ok := defaultValue.(float64)
if !ok {
return nil, fmt.Errorf("Record %q field %q: default value ought to encode using field schema: %s", c.typeName, fieldName, err)
return nil, fmt.Errorf("Record %q field %q: default value ought to have a double type got: %T", c.typeName, fieldName, defaultValue)
}
defaultValue = v
case "float":
v, ok := defaultValue.(float64)
if !ok {
return nil, fmt.Errorf("Record %q field %q: default value ought to encode using field schema: %s", c.typeName, fieldName, err)
return nil, fmt.Errorf("Record %q field %q: default value ought to have a float type got: %T", c.typeName, fieldName, defaultValue)
}
defaultValue = float32(v)
case "int":
v, ok := defaultValue.(float64)
if !ok {
return nil, fmt.Errorf("Record %q field %q: default value ought to encode using field schema: %s", c.typeName, fieldName, err)
return nil, fmt.Errorf("Record %q field %q: default value ought to have a number type got: %T", c.typeName, fieldName, defaultValue)
}
defaultValue = int32(v)
case "long":
v, ok := defaultValue.(float64)
if !ok {
return nil, fmt.Errorf("Record %q field %q: default value ought to encode using field schema: %s", c.typeName, fieldName, err)
return nil, fmt.Errorf("Record %q field %q: default value ought to have a number type got: %T", c.typeName, fieldName, defaultValue)
}
defaultValue = int64(v)
case "string":
v, ok := defaultValue.(string)
if !ok {
return nil, fmt.Errorf("Record %q field %q: default value ought to encode using field schema: %s", c.typeName, fieldName, err)
return nil, fmt.Errorf("Record %q field %q: default value ought to have a string type got: %T", c.typeName, fieldName, defaultValue)
}
defaultValue = v
case "union":
Expand All @@ -118,6 +118,21 @@ func makeRecordCodec(st map[string]*Codec, enclosingNamespace string, schemaMap
defaultValue = Union(fieldCodec.schemaOriginal, defaultValue)
default:
debug("fieldName: %q; type: %q; defaultValue: %T(%#v)\n", fieldName, c.typeName, defaultValue, defaultValue)

// Support defaults for logical types
if logicalType, ok := fieldSchemaMap["logicalType"]; ok {
switch logicalType {

Check failure on line 124 in record.go

View workflow job for this annotation

GitHub Actions / lint

singleCaseSwitch: should rewrite switch statement to if statement (gocritic)
case "decimal":
v, ok := defaultValue.(string)
if !ok {
return nil, fmt.Errorf("Record %q field %q: default value ought to have a string type got: %T", c.typeName, fieldName, defaultValue)
}
defaultValue, _, err = fieldCodec.nativeFromBinary([]byte(v))
if err != nil {
return nil, fmt.Errorf("Record %q field %q: default value ought to decode from textual: %w", c.typeName, fieldName, err)
}
}
}
}

// attempt to encode default value using codec
Expand Down
21 changes: 15 additions & 6 deletions record_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package goavro
import (
"bytes"
"fmt"
"math/big"
"testing"
)

Expand Down Expand Up @@ -389,7 +390,7 @@ func TestRecordFieldDefaultValue(t *testing.T) {
testSchemaValid(t, `{"type":"record","name":"r1","fields":[{"name":"f1","type":"string","default":"foo"}]}`)
testSchemaInvalid(t,
`{"type":"record","name":"r1","fields":[{"name":"f1","type":"int","default":"foo"}]}`,
"default value ought to encode using field schema")
"default value ought to have a number type")
}

func TestRecordFieldUnionDefaultValue(t *testing.T) {
Expand Down Expand Up @@ -618,7 +619,7 @@ func TestRecordFieldFixedDefaultValue(t *testing.T) {

func TestRecordFieldDefaultValueTypes(t *testing.T) {
t.Run("success", func(t *testing.T) {
codec, err := NewCodec(`{"type": "record", "name": "r1", "fields":[{"name": "someBoolean", "type": "boolean", "default": true},{"name": "someBytes", "type": "bytes", "default": "0"},{"name": "someDouble", "type": "double", "default": 0},{"name": "someFloat", "type": "float", "default": 0},{"name": "someInt", "type": "int", "default": 0},{"name": "someLong", "type": "long", "default": 0},{"name": "someString", "type": "string", "default": "0"}]}`)
codec, err := NewCodec(`{"type": "record", "name": "r1", "fields":[{"name": "someBoolean", "type": "boolean", "default": true},{"name": "someBytes", "type": "bytes", "default": "0"},{"name": "someDouble", "type": "double", "default": 0},{"name": "someFloat", "type": "float", "default": 0},{"name": "someInt", "type": "int", "default": 0},{"name": "someLong", "type": "long", "default": 0},{"name": "someString", "type": "string", "default": "0"}, {"name":"someTimestamp", "type":"long", "logicalType":"timestamp-millis","default":0}, {"name": "someDecimal", "type": "bytes", "logicalType": "decimal", "precision": 4, "scale": 2, "default":"\u0000"}]}`)
ensureError(t, err)

r1, _, err := codec.NativeFromTextual([]byte("{}"))
Expand Down Expand Up @@ -660,24 +661,32 @@ func TestRecordFieldDefaultValueTypes(t *testing.T) {
if _, ok := someString.(string); !ok {
t.Errorf("GOT: %T; WANT: string", someString)
}
someTimestamp := r1m["someTimestamp"]
if _, ok := someTimestamp.(float64); !ok {
t.Errorf("GOT: %T; WANT: float64", someTimestamp)
}
someDecimal := r1m["someDecimal"]
if _, ok := someDecimal.(*big.Rat); !ok {
t.Errorf("GOT: %T; WANT: *big.Rat", someDecimal)
}
})

t.Run("provided default is wrong type", func(t *testing.T) {
t.Run("long", func(t *testing.T) {
_, err := NewCodec(`{"type": "record", "name": "r1", "fields":[{"name": "someLong", "type": "long", "default": "0"},{"name": "someInt", "type": "int", "default": 0},{"name": "someFloat", "type": "float", "default": 0},{"name": "someDouble", "type": "double", "default": 0}]}`)
ensureError(t, err, "field schema")
ensureError(t, err, "default value ought to have a number type")
})
t.Run("int", func(t *testing.T) {
_, err := NewCodec(`{"type": "record", "name": "r1", "fields":[{"name": "someLong", "type": "long", "default": 0},{"name": "someInt", "type": "int", "default": "0"},{"name": "someFloat", "type": "float", "default": 0},{"name": "someDouble", "type": "double", "default": 0}]}`)
ensureError(t, err, "field schema")
ensureError(t, err, "default value ought to have a number type")
})
t.Run("float", func(t *testing.T) {
_, err := NewCodec(`{"type": "record", "name": "r1", "fields":[{"name": "someLong", "type": "long", "default": 0},{"name": "someInt", "type": "int", "default": 0},{"name": "someFloat", "type": "float", "default": "0"},{"name": "someDouble", "type": "double", "default": 0}]}`)
ensureError(t, err, "field schema")
ensureError(t, err, "default value ought to have a float type")
})
t.Run("double", func(t *testing.T) {
_, err := NewCodec(`{"type": "record", "name": "r1", "fields":[{"name": "someLong", "type": "long", "default": 0},{"name": "someInt", "type": "int", "default": 0},{"name": "someFloat", "type": "float", "default": 0},{"name": "someDouble", "type": "double", "default": "0"}]}`)
ensureError(t, err, "field schema")
ensureError(t, err, "default value ought to have a double type")
})
})

Expand Down

0 comments on commit 750e1de

Please sign in to comment.