Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

logicalType/decimal: fix default values #291

Merged
merged 1 commit into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
decimal: fix default values
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 Jan 10, 2025
commit f28e4641e76a27e17f16a78f6843bc3a3c9178ae
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
28 changes: 21 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,20 @@ 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 {
if logicalType == "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
Loading