Skip to content
This repository was archived by the owner on Jan 28, 2021. It is now read-only.

Commit cf9bb2a

Browse files
authored
Implement JSON_UNQUOTE (#748)
Implement JSON_UNQUOTE
2 parents fac6924 + 6dbe96f commit cf9bb2a

File tree

7 files changed

+197
-2
lines changed

7 files changed

+197
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ We support and actively test against certain third-party clients to ensure compa
8383
|`IFNULL(expr1, expr2)`|If expr1 is not NULL, IFNULL() returns expr1; otherwise it returns expr2.|
8484
|`IS_BINARY(blob)`|Returns whether a BLOB is a binary file or not.|
8585
|`JSON_EXTRACT(json_doc, path, ...)`|Extracts data from a json document using json paths.|
86+
|`JSON_UNQUOTE(json)`|Unquotes JSON value and returns the result as a utf8mb4 string.|
8687
|`LEAST(...)`|Returns the smaller numeric or string value.|
8788
|`LENGTH(str)`|Return the length of the string in bytes.|
8889
|`LN(X)`|Return the natural logarithm of X.|

SUPPORTED.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
- IS_BINARY
9999
- IS_BINARY
100100
- JSON_EXTRACT
101+
- JSON_UNQUOTE
101102
- LEAST
102103
- LN
103104
- LOG10

engine_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,22 @@ var queries = []struct {
659659
`SELECT JSON_EXTRACT("foo", "$")`,
660660
[]sql.Row{{"foo"}},
661661
},
662+
{
663+
`SELECT JSON_UNQUOTE('"foo"')`,
664+
[]sql.Row{{"foo"}},
665+
},
666+
{
667+
`SELECT JSON_UNQUOTE('[1, 2, 3]')`,
668+
[]sql.Row{{"[1, 2, 3]"}},
669+
},
670+
{
671+
`SELECT JSON_UNQUOTE('"\\t\\u0032"')`,
672+
[]sql.Row{{"\t2"}},
673+
},
674+
{
675+
`SELECT JSON_UNQUOTE('"\t\\u0032"')`,
676+
[]sql.Row{{"\t2"}},
677+
},
662678
{
663679
`SELECT CONNECTION_ID()`,
664680
[]sql.Row{{uint32(1)}},
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package function
2+
3+
import (
4+
"bytes"
5+
"encoding/binary"
6+
"encoding/hex"
7+
"fmt"
8+
"reflect"
9+
"unicode/utf8"
10+
11+
"github.com/src-d/go-mysql-server/sql"
12+
"github.com/src-d/go-mysql-server/sql/expression"
13+
)
14+
15+
// JSONUnquote unquotes JSON value and returns the result as a utf8mb4 string.
16+
// Returns NULL if the argument is NULL.
17+
// An error occurs if the value starts and ends with double quotes but is not a valid JSON string literal.
18+
type JSONUnquote struct {
19+
expression.UnaryExpression
20+
}
21+
22+
// NewJSONUnquote creates a new JSONUnquote UDF.
23+
func NewJSONUnquote(json sql.Expression) sql.Expression {
24+
return &JSONUnquote{expression.UnaryExpression{Child: json}}
25+
}
26+
27+
func (js *JSONUnquote) String() string {
28+
return fmt.Sprintf("JSON_UNQUOTE(%s)", js.Child)
29+
}
30+
31+
// Type implements the Expression interface.
32+
func (*JSONUnquote) Type() sql.Type {
33+
return sql.Text
34+
}
35+
36+
// TransformUp implements the Expression interface.
37+
func (js *JSONUnquote) TransformUp(f sql.TransformExprFunc) (sql.Expression, error) {
38+
json, err := js.Child.TransformUp(f)
39+
if err != nil {
40+
return nil, err
41+
}
42+
return f(NewJSONUnquote(json))
43+
}
44+
45+
// Eval implements the Expression interface.
46+
func (js *JSONUnquote) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
47+
json, err := js.Child.Eval(ctx, row)
48+
if json == nil || err != nil {
49+
return json, err
50+
}
51+
52+
ex, err := sql.Text.Convert(json)
53+
if err != nil {
54+
return nil, err
55+
}
56+
str, ok := ex.(string)
57+
if !ok {
58+
return nil, sql.ErrInvalidType.New(reflect.TypeOf(ex).String())
59+
}
60+
61+
return unquote(str)
62+
}
63+
64+
// The implementation is taken from TiDB
65+
// https://github.com/pingcap/tidb/blob/a594287e9f402037b06930026906547000006bb6/types/json/binary_functions.go#L89
66+
func unquote(s string) (string, error) {
67+
ret := new(bytes.Buffer)
68+
for i := 0; i < len(s); i++ {
69+
if s[i] == '\\' {
70+
i++
71+
if i == len(s) {
72+
return "", fmt.Errorf("Missing a closing quotation mark in string")
73+
}
74+
switch s[i] {
75+
case '"':
76+
ret.WriteByte('"')
77+
case 'b':
78+
ret.WriteByte('\b')
79+
case 'f':
80+
ret.WriteByte('\f')
81+
case 'n':
82+
ret.WriteByte('\n')
83+
case 'r':
84+
ret.WriteByte('\r')
85+
case 't':
86+
ret.WriteByte('\t')
87+
case '\\':
88+
ret.WriteByte('\\')
89+
case 'u':
90+
if i+4 > len(s) {
91+
return "", fmt.Errorf("Invalid unicode: %s", s[i+1:])
92+
}
93+
char, size, err := decodeEscapedUnicode([]byte(s[i+1 : i+5]))
94+
if err != nil {
95+
return "", err
96+
}
97+
ret.Write(char[0:size])
98+
i += 4
99+
default:
100+
// For all other escape sequences, backslash is ignored.
101+
ret.WriteByte(s[i])
102+
}
103+
} else {
104+
ret.WriteByte(s[i])
105+
}
106+
}
107+
108+
str := ret.String()
109+
strlen := len(str)
110+
// Remove prefix and suffix '"'.
111+
if strlen > 1 {
112+
head, tail := str[0], str[strlen-1]
113+
if head == '"' && tail == '"' {
114+
return str[1 : strlen-1], nil
115+
}
116+
}
117+
return str, nil
118+
}
119+
120+
// decodeEscapedUnicode decodes unicode into utf8 bytes specified in RFC 3629.
121+
// According RFC 3629, the max length of utf8 characters is 4 bytes.
122+
// And MySQL use 4 bytes to represent the unicode which must be in [0, 65536).
123+
// The implementation is taken from TiDB:
124+
// https://github.com/pingcap/tidb/blob/a594287e9f402037b06930026906547000006bb6/types/json/binary_functions.go#L136
125+
func decodeEscapedUnicode(s []byte) (char [4]byte, size int, err error) {
126+
size, err = hex.Decode(char[0:2], s)
127+
if err != nil || size != 2 {
128+
// The unicode must can be represented in 2 bytes.
129+
return char, 0, err
130+
}
131+
var unicode uint16
132+
err = binary.Read(bytes.NewReader(char[0:2]), binary.BigEndian, &unicode)
133+
if err != nil {
134+
return char, 0, err
135+
}
136+
size = utf8.RuneLen(rune(unicode))
137+
utf8.EncodeRune(char[0:size], rune(unicode))
138+
return
139+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package function
2+
3+
import (
4+
"testing"
5+
6+
"github.com/src-d/go-mysql-server/sql"
7+
"github.com/src-d/go-mysql-server/sql/expression"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestJSONUnquote(t *testing.T) {
12+
require := require.New(t)
13+
js := NewJSONUnquote(expression.NewGetField(0, sql.Text, "json", false))
14+
15+
testCases := []struct {
16+
row sql.Row
17+
expected interface{}
18+
err bool
19+
}{
20+
{sql.Row{nil}, nil, false},
21+
{sql.Row{"\"abc\""}, `abc`, false},
22+
{sql.Row{"[1, 2, 3]"}, `[1, 2, 3]`, false},
23+
{sql.Row{"\"\t\u0032\""}, "\t2", false},
24+
{sql.Row{"\\"}, nil, true},
25+
}
26+
27+
for _, tt := range testCases {
28+
result, err := js.Eval(sql.NewEmptyContext(), tt.row)
29+
30+
if !tt.err {
31+
require.NoError(err)
32+
require.Equal(tt.expected, result)
33+
} else {
34+
require.NotNil(err)
35+
}
36+
}
37+
}

sql/expression/function/registry.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ var Defaults = []sql.Function{
6060
sql.Function0{Name: "connection_id", Fn: NewConnectionID},
6161
sql.Function1{Name: "soundex", Fn: NewSoundex},
6262
sql.FunctionN{Name: "json_extract", Fn: NewJSONExtract},
63+
sql.Function1{Name: "json_unquote", Fn: NewJSONUnquote},
6364
sql.Function1{Name: "ln", Fn: NewLogBaseFunc(float64(math.E))},
6465
sql.Function1{Name: "log2", Fn: NewLogBaseFunc(float64(2))},
6566
sql.Function1{Name: "log10", Fn: NewLogBaseFunc(float64(10))},

sql/functionregistry.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package sql
22

33
import (
4-
"gopkg.in/src-d/go-errors.v1"
54
"github.com/src-d/go-mysql-server/internal/similartext"
5+
"gopkg.in/src-d/go-errors.v1"
66
)
77

88
// ErrFunctionAlreadyRegistered is thrown when a function is already registered
@@ -13,7 +13,7 @@ var ErrFunctionNotFound = errors.NewKind("A function: '%s' not found.")
1313

1414
// ErrInvalidArgumentNumber is returned when the number of arguments to call a
1515
// function is different from the function arity.
16-
var ErrInvalidArgumentNumber = errors.NewKind("A function: '%s' expected %d arguments, %d received.")
16+
var ErrInvalidArgumentNumber = errors.NewKind("A function: '%s' expected %v arguments, %v received.")
1717

1818
// Function is a function defined by the user that can be applied in a SQL query.
1919
type Function interface {

0 commit comments

Comments
 (0)