Serialize binary bytea cols into hex/base64 (#537)

- Adds binary serialization into hex/base64
- Default codec is base64
- Codec can be changed via `--binary-codec` CLI option
This commit is contained in:
Dan Sosedoff 2021-12-29 11:03:50 -06:00 committed by GitHub
parent 1323012cff
commit 706caa44bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 159 additions and 32 deletions

View File

@ -150,6 +150,12 @@ func initOptions() {
fmt.Println(readonlyWarning)
}
if options.BinaryCodec != "" {
if err := client.SetBinaryCodec(options.BinaryCodec); err != nil {
exitWithMessage(err.Error())
}
}
printVersion()
}

View File

@ -416,7 +416,7 @@ func (client *Client) query(query string, args ...interface{}) (*Result, error)
}
}
result.PrepareBigints()
result.PostProcess()
return &result, nil
}

40
pkg/client/codec.go Normal file
View File

@ -0,0 +1,40 @@
package client
import (
"encoding/base64"
"encoding/hex"
"fmt"
)
const (
CodecNone = "none"
CodecHex = "hex"
CodecBase64 = "base64"
)
var (
// BinaryEncodingFormat specifies the default serialization format of binary data
BinaryCodec = CodecBase64
)
func SetBinaryCodec(codec string) error {
switch codec {
case CodecNone, CodecHex, CodecBase64:
BinaryCodec = codec
default:
return fmt.Errorf("invalid binary codec: %v", codec)
}
return nil
}
func encodeBinaryData(data []byte, codec string) string {
switch codec {
case CodecHex:
return hex.EncodeToString(data)
case CodecBase64:
return base64.StdEncoding.EncodeToString(data)
default:
return string(data)
}
}

49
pkg/client/codec_test.go Normal file
View File

@ -0,0 +1,49 @@
package client
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSetBinaryCodec(t *testing.T) {
examples := []struct {
input string
err error
}{
{input: CodecNone, err: nil},
{input: CodecBase64, err: nil},
{input: CodecHex, err: nil},
{input: "foobar", err: errors.New("invalid binary codec: foobar")},
}
for _, ex := range examples {
t.Run(ex.input, func(t *testing.T) {
val := BinaryCodec
defer func() {
BinaryCodec = val
}()
assert.Equal(t, ex.err, SetBinaryCodec(ex.input))
})
}
}
func Test_encodeBinaryData(t *testing.T) {
examples := []struct {
input string
expected string
encoding string
}{
{input: "hello world", expected: "hello world", encoding: CodecNone},
{input: "hello world", expected: "aGVsbG8gd29ybGQ=", encoding: CodecBase64},
{input: "hello world", expected: "68656c6c6f20776f726c64", encoding: CodecHex},
}
for _, ex := range examples {
t.Run(ex.input, func(t *testing.T) {
assert.Equal(t, ex.expected, encodeBinaryData([]byte(ex.input), ex.encoding))
})
}
}

View File

@ -6,7 +6,6 @@ import (
"encoding/json"
"fmt"
"math"
"reflect"
"strconv"
"time"
@ -37,22 +36,19 @@ type Objects struct {
// Due to big int number limitations in javascript, numbers should be encoded
// as strings so they could be properly loaded on the frontend.
func (res *Result) PrepareBigints() {
func (res *Result) PostProcess() {
for i, row := range res.Rows {
for j, col := range row {
if col == nil {
continue
}
switch reflect.TypeOf(col).Kind() {
case reflect.Int64:
val := col.(int64)
switch val := col.(type) {
case int64:
if val < -9007199254740991 || val > 9007199254740991 {
res.Rows[i][j] = strconv.FormatInt(col.(int64), 10)
}
case reflect.Float64:
val := col.(float64)
case float64:
// json.Marshal panics when dealing with NaN/Inf values
// issue: https://github.com/golang/go/issues/25721
if math.IsNaN(val) {
@ -63,6 +59,10 @@ func (res *Result) PrepareBigints() {
if val < -999999999999999 || val > 999999999999999 {
res.Rows[i][j] = strconv.FormatFloat(val, 'e', -1, 64)
}
case string:
if hasBinary(val, 8) && BinaryCodec != CodecNone {
res.Rows[i][j] = encodeBinaryData([]byte(val), BinaryCodec)
}
}
}
}

View File

@ -7,33 +7,52 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_PrepareBigints(t *testing.T) {
result := Result{
Columns: []string{"value"},
Rows: []Row{
Row{int(1234)},
Row{int64(9223372036854775807)},
Row{int64(-9223372036854775808)},
Row{float64(9223372036854775808.9223372036854775808)},
Row{float64(999999999999999.9)},
},
}
func TestPostProcess(t *testing.T) {
t.Run("large numbers", func(t *testing.T) {
result := Result{
Columns: []string{"value"},
Rows: []Row{
{int(1234)},
{int64(9223372036854775807)},
{int64(-9223372036854775808)},
{float64(9223372036854775808.9223372036854775808)},
{float64(999999999999999.9)},
},
}
result.PrepareBigints()
result.PostProcess()
assert.Equal(t, 1234, result.Rows[0][0])
assert.Equal(t, "9223372036854775807", result.Rows[1][0])
assert.Equal(t, "-9223372036854775808", result.Rows[2][0])
assert.Equal(t, "9.223372036854776e+18", result.Rows[3][0])
assert.Equal(t, "9.999999999999999e+14", result.Rows[4][0])
assert.Equal(t, 1234, result.Rows[0][0])
assert.Equal(t, "9223372036854775807", result.Rows[1][0])
assert.Equal(t, "-9223372036854775808", result.Rows[2][0])
assert.Equal(t, "9.223372036854776e+18", result.Rows[3][0])
assert.Equal(t, "9.999999999999999e+14", result.Rows[4][0])
})
t.Run("binary encoding", func(t *testing.T) {
result := Result{
Columns: []string{"data"},
Rows: []Row{
{"text value"},
{"text with symbols !@#$%"},
{string([]byte{10, 11, 12, 13})},
},
}
result.PostProcess()
assert.Equal(t, "text value", result.Rows[0][0])
assert.Equal(t, "text with symbols !@#$%", result.Rows[1][0])
assert.Equal(t, "CgsMDQ==", result.Rows[2][0])
})
}
func Test_CSV(t *testing.T) {
func TestCSV(t *testing.T) {
result := Result{
Columns: []string{"id", "name", "email"},
Rows: []Row{
Row{1, "John", "john@example.com"},
Row{2, "Bob", "bob@example.com"},
{1, "John", "john@example.com"},
{2, "Bob", "bob@example.com"},
},
}
@ -43,12 +62,12 @@ func Test_CSV(t *testing.T) {
assert.Equal(t, expected, output)
}
func Test_JSON(t *testing.T) {
func TestJSON(t *testing.T) {
result := Result{
Columns: []string{"id", "name", "email"},
Rows: []Row{
Row{1, "John", "john@example.com"},
Row{2, "Bob", "bob@example.com"},
{1, "John", "john@example.com"},
{2, "Bob", "bob@example.com"},
},
}

View File

@ -31,3 +31,15 @@ func containsRestrictedKeywords(str string) bool {
return reRestrictedKeywords.MatchString(str)
}
func hasBinary(data string, checkLen int) bool {
for idx, chr := range data {
if int(chr) < 32 || int(chr) > 126 {
return true
}
if idx >= checkLen {
break
}
}
return false
}

View File

@ -42,6 +42,7 @@ type Options struct {
ConnectionIdleTimeout int `long:"idle-timeout" description:"Set connection idle timeout in minutes" default:"180"`
Cors bool `long:"cors" description:"Enable Cross-Origin Resource Sharing (CORS)"`
CorsOrigin string `long:"cors-origin" description:"Allowed CORS origins" default:"*"`
BinaryCodec string `long:"binary-codec" description:"Codec for binary data serialization"`
}
var Opts Options