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: support builtin function json_contains #7443

Merged
merged 12 commits into from
Sep 7, 2018
21 changes: 11 additions & 10 deletions expression/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -572,14 +572,15 @@ var funcs = map[string]functionClass{
ast.ValidatePasswordStrength: &validatePasswordStrengthFunctionClass{baseFunctionClass{ast.ValidatePasswordStrength, 1, 1}},

// json functions
ast.JSONType: &jsonTypeFunctionClass{baseFunctionClass{ast.JSONType, 1, 1}},
ast.JSONExtract: &jsonExtractFunctionClass{baseFunctionClass{ast.JSONExtract, 2, -1}},
ast.JSONUnquote: &jsonUnquoteFunctionClass{baseFunctionClass{ast.JSONUnquote, 1, 1}},
ast.JSONSet: &jsonSetFunctionClass{baseFunctionClass{ast.JSONSet, 3, -1}},
ast.JSONInsert: &jsonInsertFunctionClass{baseFunctionClass{ast.JSONInsert, 3, -1}},
ast.JSONReplace: &jsonReplaceFunctionClass{baseFunctionClass{ast.JSONReplace, 3, -1}},
ast.JSONRemove: &jsonRemoveFunctionClass{baseFunctionClass{ast.JSONRemove, 2, -1}},
ast.JSONMerge: &jsonMergeFunctionClass{baseFunctionClass{ast.JSONMerge, 2, -1}},
ast.JSONObject: &jsonObjectFunctionClass{baseFunctionClass{ast.JSONObject, 0, -1}},
ast.JSONArray: &jsonArrayFunctionClass{baseFunctionClass{ast.JSONArray, 0, -1}},
ast.JSONType: &jsonTypeFunctionClass{baseFunctionClass{ast.JSONType, 1, 1}},
ast.JSONExtract: &jsonExtractFunctionClass{baseFunctionClass{ast.JSONExtract, 2, -1}},
ast.JSONUnquote: &jsonUnquoteFunctionClass{baseFunctionClass{ast.JSONUnquote, 1, 1}},
ast.JSONSet: &jsonSetFunctionClass{baseFunctionClass{ast.JSONSet, 3, -1}},
ast.JSONInsert: &jsonInsertFunctionClass{baseFunctionClass{ast.JSONInsert, 3, -1}},
ast.JSONReplace: &jsonReplaceFunctionClass{baseFunctionClass{ast.JSONReplace, 3, -1}},
ast.JSONRemove: &jsonRemoveFunctionClass{baseFunctionClass{ast.JSONRemove, 2, -1}},
ast.JSONMerge: &jsonMergeFunctionClass{baseFunctionClass{ast.JSONMerge, 2, -1}},
ast.JSONObject: &jsonObjectFunctionClass{baseFunctionClass{ast.JSONObject, 0, -1}},
ast.JSONArray: &jsonArrayFunctionClass{baseFunctionClass{ast.JSONArray, 0, -1}},
ast.JSONContains: &jsonContainsFunctionClass{baseFunctionClass{ast.JSONContains, 2, 3}},
}
66 changes: 66 additions & 0 deletions expression/builtin_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ var (
_ functionClass = &jsonMergeFunctionClass{}
_ functionClass = &jsonObjectFunctionClass{}
_ functionClass = &jsonArrayFunctionClass{}
_ functionClass = &jsonContainsFunctionClass{}

// Type of JSON value.
_ builtinFunc = &builtinJSONTypeSig{}
Expand All @@ -56,6 +57,8 @@ var (
_ builtinFunc = &builtinJSONRemoveSig{}
// Merge JSON documents, preserving duplicate keys.
_ builtinFunc = &builtinJSONMergeSig{}
// Check JSON document contains specific target.
_ builtinFunc = &builtinJSONContainsSig{}
)

type jsonTypeFunctionClass struct {
Expand Down Expand Up @@ -548,3 +551,66 @@ func jsonModify(ctx sessionctx.Context, args []Expression, row chunk.Row, mt jso
}
return res, false, nil
}

type jsonContainsFunctionClass struct {
baseFunctionClass
}

type builtinJSONContainsSig struct {
baseBuiltinFunc
}

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

func (c *jsonContainsFunctionClass) getFunction(ctx sessionctx.Context, args []Expression) (builtinFunc, error) {
if err := c.verifyArgs(args); err != nil {
return nil, errors.Trace(err)
}
argTps := []types.EvalType{types.ETJson, types.ETJson}
if len(args) == 3 {
argTps = append(argTps, types.ETString)
}
bf := newBaseBuiltinFuncWithTp(ctx, args, types.ETInt, argTps...)
sig := &builtinJSONContainsSig{bf}
sig.setPbCode(tipb.ScalarFuncSig_JsonContainsSig)
return sig, nil
}

func (b *builtinJSONContainsSig) evalInt(row chunk.Row) (res int64, isNull bool, err error) {
obj, isNull, err := b.args[0].EvalJSON(b.ctx, row)
if isNull || err != nil {
return res, isNull, errors.Trace(err)
}
target, isNull, err := b.args[1].EvalJSON(b.ctx, row)
if isNull || err != nil {
return res, isNull, errors.Trace(err)
}
var pathExpr json.PathExpression
if len(b.args) == 3 {
Copy link
Member

Choose a reason for hiding this comment

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

It better to write another function signature to handle the scenario that the third argument is provided.

path, isNull, err := b.args[2].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
}
var exists bool
obj, exists = obj.Extract([]json.PathExpression{pathExpr})
if !exists {
return res, true, nil
}
}

if json.ContainsBinary(obj, target) {
return 1, false, nil
}
return 0, false, nil
}
65 changes: 64 additions & 1 deletion expression/builtin_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ func (s *testEvaluatorSuite) TestJSONObject(c *C) {
}
}

func (s *testEvaluatorSuite) TestJSONORemove(c *C) {
func (s *testEvaluatorSuite) TestJSONRemove(c *C) {
defer testleak.AfterTest(c)()
fc := funcs[ast.JSONRemove]
tbl := []struct {
Expand Down Expand Up @@ -308,3 +308,66 @@ func (s *testEvaluatorSuite) TestJSONORemove(c *C) {
}
}
}

func (s *testEvaluatorSuite) TestJSONContains(c *C) {
defer testleak.AfterTest(c)()
fc := funcs[ast.JSONContains]
tbl := []struct {
input []interface{}
expected interface{}
success bool
}{
// Tests nil arguments
{[]interface{}{nil, `1`, "$.c"}, nil, true},
{[]interface{}{`{"a": [1, 2, {"aa": "xx"}]}`, nil, "$.a[3]"}, nil, true},
{[]interface{}{`{"a": [1, 2, {"aa": "xx"}]}`, `1`, nil}, nil, true},
// Tests with path expression
{[]interface{}{`[1,2,[1,[5,[3]]]]`, `[1,3]`, "$[2]"}, 1, true},
{[]interface{}{`[1,2,[1,[5,{"a":[2,3]}]]]`, `[1,{"a":[3]}]`, "$[2]"}, 1, true},
{[]interface{}{`[{"a":1}]`, `{"a":1}`, "$"}, 1, true},
{[]interface{}{`[{"a":1,"b":2}]`, `{"a":1,"b":2}`, "$"}, 1, true},
{[]interface{}{`[{"a":{"a":1},"b":2}]`, `{"a":1}`, "$.a"}, 0, true},
// Tests without path expression
{[]interface{}{`{}`, `{}`}, 1, true},
{[]interface{}{`{"a":1}`, `{}`}, 1, true},
{[]interface{}{`{"a":1}`, `1`}, 0, true},
{[]interface{}{`{"a":[1]}`, `[1]`}, 0, true},
{[]interface{}{`{"b":2, "c":3}`, `{"c":3}`}, 1, true},
{[]interface{}{`1`, `1`}, 1, true},
{[]interface{}{`[1]`, `1`}, 1, true},
{[]interface{}{`[1,2]`, `[1]`}, 1, true},
{[]interface{}{`[1,2]`, `[1,3]`}, 0, true},
{[]interface{}{`[1,2]`, `["1"]`}, 0, true},
{[]interface{}{`[1,2,[1,3]]`, `[1,3]`}, 1, true},
{[]interface{}{`[1,2,[1,[5,[3]]]]`, `[1,3]`}, 1, true},
{[]interface{}{`[1,2,[1,[5,{"a":[2,3]}]]]`, `[1,{"a":[3]}]`}, 1, true},
{[]interface{}{`[{"a":1}]`, `{"a":1}`}, 1, true},
{[]interface{}{`[{"a":1,"b":2}]`, `{"a":1}`}, 1, true},
{[]interface{}{`[{"a":{"a":1},"b":2}]`, `{"a":1}`}, 0, true},
// Tests path expression contains any asterisk
{[]interface{}{`{"a": [1, 2, {"aa": "xx"}]}`, `1`, "$.*"}, nil, false},
{[]interface{}{`{"a": [1, 2, {"aa": "xx"}]}`, `1`, "$[*]"}, nil, false},
{[]interface{}{`{"a": [1, 2, {"aa": "xx"}]}`, `1`, "$**.a"}, nil, false},
// Tests path expression does not identify a section of the target document
{[]interface{}{`{"a": [1, 2, {"aa": "xx"}]}`, `1`, "$.c"}, nil, true},
{[]interface{}{`{"a": [1, 2, {"aa": "xx"}]}`, `1`, "$.a[3]"}, nil, true},
{[]interface{}{`{"a": [1, 2, {"aa": "xx"}]}`, `1`, "$.a[2].b"}, 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)
if t.expected == nil {
c.Assert(d.IsNull(), IsTrue)
} else {
c.Assert(d.GetInt64(), Equals, int64(t.expected.(int)))
}
} else {
c.Assert(err, NotNil)
}
}
}
2 changes: 2 additions & 0 deletions expression/distsql_builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,8 @@ func getSignatureByPB(ctx sessionctx.Context, sigCode tipb.ScalarFuncSig, tp *ti
f = &builtinJSONRemoveSig{base}
case tipb.ScalarFuncSig_JsonMergeSig:
f = &builtinJSONMergeSig{base}
case tipb.ScalarFuncSig_JsonContainsSig:
f = &builtinJSONContainsSig{base}
case tipb.ScalarFuncSig_LikeSig:
f = &builtinLikeSig{base}

Expand Down
13 changes: 13 additions & 0 deletions expression/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3205,6 +3205,19 @@ func (s *testIntegrationSuite) TestFuncJSON(c *C) {
tk.MustExec(`update table_json set a=json_set(a,'$.a',json_object('a',1,'b',2)) where json_extract(a,'$.a[1]') = '2'`)
r = tk.MustQuery(`select json_extract(a, '$.a.a'), json_extract(a, '$.a.b') from table_json`)
r.Check(testkit.Rows("1 2", "<nil> <nil>"))

r = tk.MustQuery(`select json_contains(NULL, '1'), json_contains('1', NULL), json_contains('1', '1', NULL)`)
r.Check(testkit.Rows("<nil> <nil> <nil>"))
r = tk.MustQuery(`select json_contains('{}','{}'), json_contains('[1]','1'), json_contains('[1]','"1"'), json_contains('[1,2,[1,[5,[3]]]]', '[1,3]', '$[2]'), json_contains('[1,2,[1,[5,{"a":[2,3]}]]]', '[1,{"a":[3]}]', "$[2]"), json_contains('{"a":1}', '{"a":1,"b":2}', "$")`)
r.Check(testkit.Rows("1 1 0 1 1 0"))
r = tk.MustQuery(`select json_contains('{"a": 1}', '1', "$.c"), json_contains('{"a": [1, 2]}', '1', "$.a[2]"), json_contains('{"a": [1, {"a": 1}]}', '1', "$.a[1].b")`)
r.Check(testkit.Rows("<nil> <nil> <nil>"))
rs, err := tk.Exec("select json_contains('1','1','$.*')")
c.Assert(err, IsNil)
c.Assert(rs, NotNil)
_, err = session.GetRows4Test(context.Background(), tk.Se, rs)
c.Assert(err, NotNil)
c.Assert(err.Error(), Equals, "[json:3149]In this situation, path expressions may not contain the * and ** tokens.")
}

func (s *testIntegrationSuite) TestColumnInfoModified(c *C) {
Expand Down
1 change: 1 addition & 0 deletions mysql/errcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,7 @@ const (
ErrInvalidJSONText = 3140
ErrInvalidJSONPath = 3143
ErrInvalidJSONData = 3146
ErrInvalidJSONPathWildcard = 3149
ErrJSONUsedAsKey = 3152

// TiDB self-defined errors.
Expand Down
1 change: 1 addition & 0 deletions mysql/errname.go
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,7 @@ var MySQLErrName = map[uint16]string{
ErrInvalidJSONText: "Invalid JSON text: %-.192s",
ErrInvalidJSONPath: "Invalid JSON path expression %s.",
ErrInvalidJSONData: "Invalid data type for JSON data",
ErrInvalidJSONPathWildcard: "In this situation, path expressions may not contain the * and ** tokens.",
ErrJSONUsedAsKey: "JSON column '%-.192s' cannot be used in key specification.",

// TiDB errors.
Expand Down
1 change: 1 addition & 0 deletions mysql/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,5 +253,6 @@ var MySQLState = map[uint16]string{
ErrInvalidJSONText: "22032",
ErrInvalidJSONPath: "42000",
ErrInvalidJSONData: "22032",
ErrInvalidJSONPathWildcard: "42000",
ErrJSONUsedAsKey: "42000",
}
42 changes: 42 additions & 0 deletions types/json/binary_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -689,3 +689,45 @@ func PeekBytesAsJSON(b []byte) (n int, err error) {
err = errors.New("Invalid JSON bytes")
return
}

// ContainsBinary check whether JSON document contains specific target according the following rules:
// 1) object contains a target object if and only if every key is contained in source object and the value associated with the target key is contained in the value associated with the source key;
// 2) array contains a target nonarray if and only if the target is contained in some element of the array;
// 3) array contains a target array if and only if every element is contained in some element of the array;
// 4) scalar contains a target scalar if and only if they are comparable and are equal;
func ContainsBinary(obj, target BinaryJSON) bool {
switch obj.TypeCode {
case TypeCodeObject:
if target.TypeCode == TypeCodeObject {
len := target.getElemCount()
for i := 0; i < len; i++ {
key := target.objectGetKey(i)
val := target.objectGetVal(i)
if exp, exists := obj.objectSearchKey(key); !exists || !ContainsBinary(exp, val) {
return false
}
}
return true
}
return false
case TypeCodeArray:
if target.TypeCode == TypeCodeArray {
len := target.getElemCount()
for i := 0; i < len; i++ {
if !ContainsBinary(obj, target.arrayGetElem(i)) {
return false
}
}
return true
}
len := obj.getElemCount()
for i := 0; i < len; i++ {
if ContainsBinary(obj.arrayGetElem(i), target) {
return true
}
}
return false
default:
return CompareBinary(obj, target) == 0
}
}
31 changes: 31 additions & 0 deletions types/json/binary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,34 @@ func BenchmarkBinaryMarshal(b *testing.B) {
bj.MarshalJSON()
}
}

func (s *testJSONSuite) TestBinaryJSONContains(c *C) {
var tests = []struct {
input string
target string
expected bool
}{
{`{}`, `{}`, true},
{`{"a":1}`, `{}`, true},
{`{"a":1}`, `1`, false},
{`{"a":[1]}`, `[1]`, false},
{`{"b":2, "c":3}`, `{"c":3}`, true},
{`1`, `1`, true},
{`[1]`, `1`, true},
{`[1,2]`, `[1]`, true},
{`[1,2]`, `[1,3]`, false},
{`[1,2]`, `["1"]`, false},
{`[1,2,[1,3]]`, `[1,3]`, true},
{`[1,2,[1,[5,[3]]]]`, `[1,3]`, true},
{`[1,2,[1,[5,{"a":[2,3]}]]]`, `[1,{"a":[3]}]`, true},
{`[{"a":1}]`, `{"a":1}`, true},
{`[{"a":1,"b":2}]`, `{"a":1}`, true},
{`[{"a":{"a":1},"b":2}]`, `{"a":1}`, false},
}

for _, tt := range tests {
obj := mustParseBinaryFromString(c, tt.input)
target := mustParseBinaryFromString(c, tt.target)
c.Assert(ContainsBinary(obj, target), Equals, tt.expected)
}
}
9 changes: 6 additions & 3 deletions types/json/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,15 @@ var (
ErrInvalidJSONPath = terror.ClassJSON.New(mysql.ErrInvalidJSONPath, mysql.MySQLErrName[mysql.ErrInvalidJSONPath])
// ErrInvalidJSONData means invalid JSON data.
ErrInvalidJSONData = terror.ClassJSON.New(mysql.ErrInvalidJSONData, mysql.MySQLErrName[mysql.ErrInvalidJSONData])
// ErrInvalidJSONPathWildcard means invalid JSON path that contain wildcard characters.
ErrInvalidJSONPathWildcard = terror.ClassJSON.New(mysql.ErrInvalidJSONPathWildcard, mysql.MySQLErrName[mysql.ErrInvalidJSONPathWildcard])
)

func init() {
terror.ErrClassToMySQLCodes[terror.ClassJSON] = map[terror.ErrCode]uint16{
mysql.ErrInvalidJSONText: mysql.ErrInvalidJSONText,
mysql.ErrInvalidJSONPath: mysql.ErrInvalidJSONPath,
mysql.ErrInvalidJSONData: mysql.ErrInvalidJSONData,
mysql.ErrInvalidJSONText: mysql.ErrInvalidJSONText,
mysql.ErrInvalidJSONPath: mysql.ErrInvalidJSONPath,
mysql.ErrInvalidJSONData: mysql.ErrInvalidJSONData,
mysql.ErrInvalidJSONPathWildcard: mysql.ErrInvalidJSONPathWildcard,
}
}
5 changes: 5 additions & 0 deletions types/json/path_expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ func (pe PathExpression) popOneLastLeg() (PathExpression, pathLeg) {
return PathExpression{legs: pe.legs[:lastLegIdx]}, lastLeg
}

// ContainsAnyAsterisk returns true if pe contains any asterisk.
func (pe PathExpression) ContainsAnyAsterisk() bool {
return pe.flags.containsAnyAsterisk()
}

// ParseJSONPathExpr parses a JSON path expression. Returns a PathExpression
// object which can be used in JSON_EXTRACT, JSON_SET and so on.
func ParseJSONPathExpr(pathExpr string) (pe PathExpression, err error) {
Expand Down