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

expression: add builtin json_keys #7776

Merged
merged 9 commits into from
Oct 18, 2018
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
85 changes: 84 additions & 1 deletion expression/builtin_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ var (
_ builtinFunc = &builtinJSONRemoveSig{}
_ builtinFunc = &builtinJSONMergeSig{}
_ builtinFunc = &builtinJSONContainsSig{}
_ builtinFunc = &builtinJSONKeysSig{}
_ builtinFunc = &builtinJSONKeys2ArgsSig{}
_ builtinFunc = &builtinJSONLengthSig{}
)

Expand Down Expand Up @@ -766,7 +768,88 @@ type jsonKeysFunctionClass struct {
}

func (c *jsonKeysFunctionClass) getFunction(ctx sessionctx.Context, args []Expression) (builtinFunc, error) {
return nil, errFunctionNotExists.GenWithStackByArgs("FUNCTION", "JSON_KEYS")
if err := c.verifyArgs(args); err != nil {
return nil, errors.Trace(err)
}
argTps := []types.EvalType{types.ETJson}
if len(args) == 2 {
argTps = append(argTps, types.ETString)
}
bf := newBaseBuiltinFuncWithTp(ctx, args, types.ETJson, argTps...)
var sig builtinFunc
switch len(args) {
case 1:
sig = &builtinJSONKeysSig{bf}
sig.setPbCode(tipb.ScalarFuncSig_JsonKeysSig)
case 2:
sig = &builtinJSONKeys2ArgsSig{bf}
sig.setPbCode(tipb.ScalarFuncSig_JsonKeys2ArgsSig)
}
return sig, nil
}

type builtinJSONKeysSig struct {
baseBuiltinFunc
}

func (b *builtinJSONKeysSig) Clone() builtinFunc {
newSig := &builtinJSONKeysSig{}
newSig.cloneFrom(&b.baseBuiltinFunc)
return newSig
}

func (b *builtinJSONKeysSig) evalJSON(row chunk.Row) (res json.BinaryJSON, isNull bool, err error) {
res, isNull, err = b.args[0].EvalJSON(b.ctx, row)
if isNull || err != nil {
return res, isNull, errors.Trace(err)
}
if res.TypeCode != json.TypeCodeObject {
return res, true, json.ErrInvalidJSONData
}
return res.GetKeys(), false, nil
}

type builtinJSONKeys2ArgsSig struct {
baseBuiltinFunc
}

func (b *builtinJSONKeys2ArgsSig) Clone() builtinFunc {
newSig := &builtinJSONKeys2ArgsSig{}
newSig.cloneFrom(&b.baseBuiltinFunc)
return newSig
}

func (b *builtinJSONKeys2ArgsSig) evalJSON(row chunk.Row) (res json.BinaryJSON, isNull bool, err error) {
res, isNull, err = b.args[0].EvalJSON(b.ctx, row)
if isNull || err != nil {
return res, isNull, errors.Trace(err)
}
if res.TypeCode != json.TypeCodeObject {
return res, true, json.ErrInvalidJSONData
}

path, isNull, err := b.args[1].EvalString(b.ctx, row)
if isNull || err != nil {
return res, isNull, errors.Trace(err)
}

pathExpr, err := json.ParseJSONPathExpr(path)
if err != nil {
return res, true, errors.Trace(err)
}
if pathExpr.ContainsAnyAsterisk() {
return res, true, json.ErrInvalidJSONPathWildcard
}

res, exists := res.Extract([]json.PathExpression{pathExpr})
if !exists {
return res, true, nil
}
if res.TypeCode != json.TypeCodeObject {
return res, true, json.ErrInvalidJSONData
}

return res.GetKeys(), false, nil
}

type jsonLengthFunctionClass struct {
Expand Down
70 changes: 70 additions & 0 deletions expression/builtin_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ func (s *testEvaluatorSuite) TestJSONLength(c *C) {
d, err := evalBuiltinFunc(f, chunk.Row{})
if t.success {
c.Assert(err, IsNil)

if t.expected == nil {
c.Assert(d.IsNull(), IsTrue)
} else {
Expand All @@ -495,3 +496,72 @@ func (s *testEvaluatorSuite) TestJSONLength(c *C) {
}
}
}

func (s *testEvaluatorSuite) TestJSONKeys(c *C) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may add a test for this what mentioned in the MySQL manual:
If the top-level value has nested subobjects, the return value does not include keys from those subobjects

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems already existed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean line 526?

defer testleak.AfterTest(c)()
fc := funcs[ast.JSONKeys]
tbl := []struct {
input []interface{}
expected interface{}
success bool
}{
// Tests nil arguments
{[]interface{}{nil}, nil, true},
{[]interface{}{nil, "$.c"}, nil, true},
{[]interface{}{`{"a": 1}`, nil}, nil, true},
{[]interface{}{nil, nil}, nil, true},

// Tests with other type
{[]interface{}{`1`}, nil, false},
{[]interface{}{`"str"`}, nil, false},
{[]interface{}{`true`}, nil, false},
{[]interface{}{`null`}, nil, false},
{[]interface{}{`[1, 2]`}, nil, false},
{[]interface{}{`["1", "2"]`}, nil, false},

// Tests without path expression
{[]interface{}{`{}`}, `[]`, true},
{[]interface{}{`{"a": 1}`}, `["a"]`, true},
{[]interface{}{`{"a": 1, "b": 2}`}, `["a", "b"]`, true},
{[]interface{}{`{"a": {"c": 3}, "b": 2}`}, `["a", "b"]`, true},

// Tests with path expression
{[]interface{}{`{"a": 1}`, "$.a"}, nil, false},
{[]interface{}{`{"a": {"c": 3}, "b": 2}`, "$.a"}, `["c"]`, true},
{[]interface{}{`{"a": {"c": 3}, "b": 2}`, "$.a.c"}, nil, false},

// Tests path expression contains any asterisk
{[]interface{}{`{}`, "$.*"}, nil, false},
{[]interface{}{`{"a": 1}`, "$.*"}, nil, false},
{[]interface{}{`{"a": {"c": 3}, "b": 2}`, "$.*"}, nil, false},
{[]interface{}{`{"a": {"c": 3}, "b": 2}`, "$.a.*"}, nil, false},

// Tests path expression does not identify a section of the target document
{[]interface{}{`{"a": 1}`, "$.b"}, nil, true},
{[]interface{}{`{"a": {"c": 3}, "b": 2}`, "$.c"}, nil, true},
{[]interface{}{`{"a": {"c": 3}, "b": 2}`, "$.a.d"}, nil, true},
}
for _, t := range tbl {
args := types.MakeDatums(t.input...)
f, err := fc.getFunction(s.ctx, s.datumsToConstants(args))
c.Assert(err, IsNil)
d, err := evalBuiltinFunc(f, chunk.Row{})
if t.success {
c.Assert(err, IsNil)
switch x := t.expected.(type) {
case string:
var j1 json.BinaryJSON
j1, err = json.ParseBinaryFromString(x)
c.Assert(err, IsNil)
j2 := d.GetMysqlJSON()
var cmp int
cmp = json.CompareBinary(j1, j2)
c.Assert(cmp, Equals, 0)
case nil:
c.Assert(d.IsNull(), IsTrue)
}
} else {
c.Assert(err, NotNil)
}
}
}
9 changes: 9 additions & 0 deletions expression/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3367,6 +3367,15 @@ func (s *testIntegrationSuite) TestFuncJSON(c *C) {
`)
r.Check(testkit.Rows("1 0 1 0"))

r = tk.MustQuery(`select

json_keys('{}'),
json_keys('{"a": 1, "b": 2}'),
json_keys('{"a": {"c": 3}, "b": 2}'),
json_keys('{"a": {"c": 3}, "b": 2}', "$.a")
`)
r.Check(testkit.Rows(`[] ["a", "b"] ["a", "b"] ["c"]`))

r = tk.MustQuery(`select
json_length('1'),
json_length('{}'),
Expand Down
10 changes: 10 additions & 0 deletions types/json/binary.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,16 @@ func (bj BinaryJSON) GetString() []byte {
return bj.Value[lenLen : lenLen+int(strLen)]
}

// GetKeys gets the keys of the object
func (bj BinaryJSON) GetKeys() BinaryJSON {
count := bj.GetElemCount()
ret := make([]BinaryJSON, 0, count)
for i := 0; i < count; i++ {
ret = append(ret, CreateBinary(string(bj.objectGetKey(i))))
}
return buildBinaryArray(ret)
}

// GetElemCount gets the count of Object or Array.
func (bj BinaryJSON) GetElemCount() int {
return int(endian.Uint32(bj.Value))
Expand Down