Skip to content
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,12 @@
## New Features / Improvements

* X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)).
* (Go) Added `schema.RegisterLogicalTypeConversion` and `schema.RegisterLogicalTypeCoder` to register portable logical types in one call, and Go types for the standard `beam:logical_type:date:v1`, `beam:logical_type:micros_instant:v1`, `beam:logical_type:millis_instant:v1`, `beam:logical_type:decimal:v1`, `beam:logical_type:fixed_char:v1`, `beam:logical_type:var_char:v1`, `beam:logical_type:fixed_bytes:v1`, `beam:logical_type:var_bytes:v1` and `beam:logical_type:timestamp:v1` (precisions 3, 6 and 9) logical types, so schemas from other SDKs with these types can be used in cross-language pipelines ([#39684](https://github.com/apache/beam/issues/39684)).

## Breaking Changes

* X behavior was changed ([#X](https://github.com/apache/beam/issues/X)).
* (Go) The row coder now encodes `int16` and `uint16` struct fields as 2 byte big endian INT16 values, matching the Java and Python SDKs. Rows with such fields written by earlier Go SDK versions decode differently with this version ([#40151](https://github.com/apache/beam/issues/40151)).

## Deprecations

Expand Down
31 changes: 31 additions & 0 deletions sdks/go/pkg/beam/core/graph/coder/int.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,34 @@ func DecodeInt32(r io.Reader) (int32, error) {
}
return int32(ret), nil
}

// EncodeUint16 encodes an uint16 in big endian format.
func EncodeUint16(value uint16, w io.Writer) error {
var data [2]byte
binary.BigEndian.PutUint16(data[:], value)
_, err := ioutilx.WriteUnsafe(w, data[:])
return err
}

// DecodeUint16 decodes an uint16 in big endian format.
func DecodeUint16(r io.Reader) (uint16, error) {
var data [2]byte
if err := ioutilx.ReadNBufUnsafe(r, data[:]); err != nil {
return 0, err
}
return binary.BigEndian.Uint16(data[:]), nil
}

// EncodeInt16 encodes an int16 in big endian format.
func EncodeInt16(value int16, w io.Writer) error {
return EncodeUint16(uint16(value), w)
}

// DecodeInt16 decodes an int16 in big endian format.
func DecodeInt16(r io.Reader) (int16, error) {
ret, err := DecodeUint16(r)
if err != nil {
return 0, err
}
return int16(ret), nil
}
32 changes: 32 additions & 0 deletions sdks/go/pkg/beam/core/graph/coder/int_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,35 @@ func TestEncodeDecodeInt32(t *testing.T) {
}
}
}

func TestEncodeDecodeInt16(t *testing.T) {
tests := []struct {
value int16
want []byte
}{
{-32768, []byte{0x80, 0x00}},
{-2, []byte{0xff, 0xfe}},
{-1, []byte{0xff, 0xff}},
{0, []byte{0x00, 0x00}},
{1, []byte{0x00, 0x01}},
{999, []byte{0x03, 0xe7}},
{32767, []byte{0x7f, 0xff}},
}

for _, test := range tests {
var buf bytes.Buffer
if err := EncodeInt16(test.value, &buf); err != nil {
t.Fatalf("EncodeInt16(%v) failed: %v", test.value, err)
}
if got := buf.Bytes(); !bytes.Equal(got, test.want) {
t.Errorf("EncodeInt16(%v) = %v, want %v", test.value, got, test.want)
}
actual, err := DecodeInt16(&buf)
if err != nil {
t.Fatalf("DecodeInt16(<%v>) failed: %v", test.value, err)
}
if actual != test.value {
t.Errorf("DecodeInt16(<%v>) = %v, want %v", test.value, actual, test.value)
}
}
}
16 changes: 16 additions & 0 deletions sdks/go/pkg/beam/core/graph/coder/row.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,19 @@ func ReadSimpleRowHeader(fields int, r io.Reader) error {
}
return nil
}

// RowFieldEncoderForType returns an encoding function for values of the given
// type, using the encoding of that type as a field of a beam schema row.
//
// Returns an error if the given type cannot be a field of a beam schema row.
func RowFieldEncoderForType(rt reflect.Type) (func(any, io.Writer) error, error) {
return defaultEnc.fieldEncoderForType(rt)
}

// RowFieldDecoderForType returns a decoding function for values of the given
// type, using the encoding of that type as a field of a beam schema row.
//
// Returns an error if the given type cannot be a field of a beam schema row.
func RowFieldDecoderForType(rt reflect.Type) (func(io.Reader) (any, error), error) {
return defaultDec.fieldDecoderForType(rt)
}
46 changes: 44 additions & 2 deletions sdks/go/pkg/beam/core/graph/coder/row_decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,24 @@ func reflectDecodeUint(rv reflect.Value, r io.Reader) error {
return nil
}

func reflectDecodeInt16(rv reflect.Value, r io.Reader) error {
v, err := DecodeInt16(r)
if err != nil {
return errors.Wrap(err, "error decoding int16 field")
}
rv.SetInt(int64(v))
return nil
}

func reflectDecodeUint16(rv reflect.Value, r io.Reader) error {
v, err := DecodeUint16(r)
if err != nil {
return errors.Wrap(err, "error decoding uint16 field")
}
rv.SetUint(uint64(v))
return nil
}

func reflectDecodeSinglePrecisionFloat(rv reflect.Value, r io.Reader) error {
v, err := DecodeSinglePrecisionFloat(r)
if err != nil {
Expand Down Expand Up @@ -341,10 +359,14 @@ func (b *RowDecoderBuilder) decoderForSingleTypeReflect(t reflect.Type) (typeDec
return typeDecoderFieldReflect{decode: reflectDecodeByte}, nil
case reflect.String:
return typeDecoderFieldReflect{decode: reflectDecodeString}, nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
case reflect.Int, reflect.Int8, reflect.Int32, reflect.Int64:
return typeDecoderFieldReflect{decode: reflectDecodeInt}, nil
case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16:
case reflect.Int16:
return typeDecoderFieldReflect{decode: reflectDecodeInt16}, nil
case reflect.Uint, reflect.Uint64, reflect.Uint32:
return typeDecoderFieldReflect{decode: reflectDecodeUint}, nil
case reflect.Uint16:
return typeDecoderFieldReflect{decode: reflectDecodeUint16}, nil
case reflect.Float32:
return typeDecoderFieldReflect{decode: reflectDecodeSinglePrecisionFloat}, nil
case reflect.Float64:
Expand Down Expand Up @@ -413,3 +435,23 @@ type typeDecoderFieldReflect struct {
// of the field value (i.e. &foo.bar) and not the field value (i.e. foo.bar).
addr bool
}

// fieldDecoderForType returns a decoder for values of t, using the encoding
// of t as a row field.
func (b *RowDecoderBuilder) fieldDecoderForType(t reflect.Type) (func(io.Reader) (any, error), error) {
decf, err := b.decoderForSingleTypeReflect(t)
if err != nil {
return nil, err
}
return func(r io.Reader) (any, error) {
rv := reflect.New(t)
v := rv.Elem()
if decf.addr {
v = rv
}
if err := decf.decode(v, r); err != nil {
return nil, err
}
return rv.Elem().Interface(), nil
}, nil
}
32 changes: 30 additions & 2 deletions sdks/go/pkg/beam/core/graph/coder/row_encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,24 @@ func (b *RowEncoderBuilder) encoderForSingleTypeReflect(t reflect.Type) (typeEnc
return typeEncoderFieldReflect{encode: func(rv reflect.Value, w io.Writer) error {
return EncodeStringUTF8(rv.String(), w)
}}, nil
case reflect.Int, reflect.Int64, reflect.Int16, reflect.Int32, reflect.Int8:
case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int8:
return typeEncoderFieldReflect{encode: func(rv reflect.Value, w io.Writer) error {
return EncodeVarInt(rv.Int(), w)
}}, nil
case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16:
case reflect.Int16:
// INT16 schema fields are big endian, matching the Java and Python row coders.
return typeEncoderFieldReflect{encode: func(rv reflect.Value, w io.Writer) error {
return EncodeInt16(int16(rv.Int()), w)
}}, nil
case reflect.Uint, reflect.Uint64, reflect.Uint32:
return typeEncoderFieldReflect{encode: func(rv reflect.Value, w io.Writer) error {
return EncodeVarUint64(rv.Uint(), w)
}}, nil
case reflect.Uint16:
// uint16 is stored as an INT16 schema field.
return typeEncoderFieldReflect{encode: func(rv reflect.Value, w io.Writer) error {
return EncodeUint16(uint16(rv.Uint()), w)
}}, nil
case reflect.Float32:
return typeEncoderFieldReflect{encode: func(rv reflect.Value, w io.Writer) error {
return EncodeSinglePrecisionFloat(float32(rv.Float()), w)
Expand Down Expand Up @@ -353,3 +363,21 @@ func (b *RowEncoderBuilder) encoderForStructReflect(t reflect.Type) (func(reflec
return nil
}, nil
}

// fieldEncoderForType returns an encoder for values of t, using the encoding
// of t as a row field.
func (b *RowEncoderBuilder) fieldEncoderForType(t reflect.Type) (func(any, io.Writer) error, error) {
encf, err := b.encoderForSingleTypeReflect(t)
if err != nil {
return nil, err
}
return func(v any, w io.Writer) error {
rv := reflect.ValueOf(v)
if encf.addr {
p := reflect.New(t)
p.Elem().Set(rv)
rv = p
}
return encf.encode(rv, w)
}, nil
}
93 changes: 93 additions & 0 deletions sdks/go/pkg/beam/core/graph/coder/row_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -905,3 +905,96 @@ func TestRowHeader_TrailingZeroBytes(t *testing.T) {
}

}

// TestReflectionRowCoderGeneration_Int16BigEndian checks that int16 and uint16
// fields use the 2 byte big endian encoding of the INT16 schema type, which is
// what the Java and Python row coders produce.
func TestReflectionRowCoderGeneration_Int16BigEndian(t *testing.T) {
type row struct {
A int16
B uint16
C int16
}
want := row{A: 999, B: 65535, C: -2}
wantBytes := []byte{
0x03, 0x00, // 3 fields, no nils.
0x03, 0xe7, // A
0xff, 0xff, // B
0xff, 0xfe, // C
}
rt := reflect.TypeOf(want)
enc, err := RowEncoderForStruct(rt)
if err != nil {
t.Fatalf("RowEncoderForStruct(%v) = %v, want nil error", rt, err)
}
var buf bytes.Buffer
if err := enc(want, &buf); err != nil {
t.Fatalf("enc(%v) = %v, want nil error", want, err)
}
if got := buf.Bytes(); !bytes.Equal(got, wantBytes) {
t.Fatalf("enc(%v) = %v, want %v", want, got, wantBytes)
}
dec, err := RowDecoderForStruct(rt)
if err != nil {
t.Fatalf("RowDecoderForStruct(%v) = %v, want nil error", rt, err)
}
got, err := dec(bytes.NewBuffer(wantBytes))
if err != nil {
t.Fatalf("dec(%v) = %v, want nil error", wantBytes, err)
}
if d := cmp.Diff(want, got); d != "" {
t.Fatalf("dec(enc(%v)) = %v\ndiff (-want, +got): %v", want, got, d)
}
}

func TestRowFieldCoderForType(t *testing.T) {
type pair struct {
A int64
B string
}
tests := []struct {
value any
wantBytes []byte
}{
{value: int64(999), wantBytes: []byte{0xe7, 0x07}},
{value: int16(999), wantBytes: []byte{0x03, 0xe7}},
{value: "abc", wantBytes: []byte{0x03, 'a', 'b', 'c'}},
{value: []byte{1, 2}, wantBytes: []byte{0x02, 1, 2}},
{value: true, wantBytes: []byte{0x01}},
{value: []int32{1, 2}, wantBytes: []byte{0x00, 0x00, 0x00, 0x02, 0x01, 0x02}},
{value: pair{A: 7, B: "x"}, wantBytes: []byte{0x02, 0x00, 0x07, 0x01, 'x'}},
}
for _, test := range tests {
rt := reflect.TypeOf(test.value)
t.Run(rt.String(), func(t *testing.T) {
enc, err := RowFieldEncoderForType(rt)
if err != nil {
t.Fatalf("RowFieldEncoderForType(%v) = %v, want nil error", rt, err)
}
var buf bytes.Buffer
if err := enc(test.value, &buf); err != nil {
t.Fatalf("enc(%v) = %v, want nil error", test.value, err)
}
if got := buf.Bytes(); !bytes.Equal(got, test.wantBytes) {
t.Fatalf("enc(%v) = %v, want %v", test.value, got, test.wantBytes)
}
dec, err := RowFieldDecoderForType(rt)
if err != nil {
t.Fatalf("RowFieldDecoderForType(%v) = %v, want nil error", rt, err)
}
got, err := dec(bytes.NewBuffer(test.wantBytes))
if err != nil {
t.Fatalf("dec(%v) = %v, want nil error", test.wantBytes, err)
}
if d := cmp.Diff(test.value, got); d != "" {
t.Fatalf("dec(enc(%v)) = %v\ndiff (-want, +got): %v", test.value, got, d)
}
})
}
if _, err := RowFieldEncoderForType(reflect.TypeOf(func() {})); err == nil {
t.Errorf("RowFieldEncoderForType(func) = nil error, want error")
}
if _, err := RowFieldDecoderForType(reflect.TypeOf(func() {})); err == nil {
t.Errorf("RowFieldDecoderForType(func) = nil error, want error")
}
}
Loading
Loading