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:
parent
1323012cff
commit
706caa44bf
@ -150,6 +150,12 @@ func initOptions() {
|
||||
fmt.Println(readonlyWarning)
|
||||
}
|
||||
|
||||
if options.BinaryCodec != "" {
|
||||
if err := client.SetBinaryCodec(options.BinaryCodec); err != nil {
|
||||
exitWithMessage(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
printVersion()
|
||||
}
|
||||
|
||||
|
@ -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
40
pkg/client/codec.go
Normal 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
49
pkg/client/codec_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user