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_path #7596

Merged
merged 3 commits into from
Sep 9, 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
25 changes: 13 additions & 12 deletions ast/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,18 +290,19 @@ const (
ValidatePasswordStrength = "validate_password_strength"

// json functions
JSONType = "json_type"
JSONExtract = "json_extract"
JSONUnquote = "json_unquote"
JSONArray = "json_array"
JSONObject = "json_object"
JSONMerge = "json_merge"
JSONValid = "json_valid"
JSONSet = "json_set"
JSONInsert = "json_insert"
JSONReplace = "json_replace"
JSONRemove = "json_remove"
JSONContains = "json_contains"
JSONType = "json_type"
JSONExtract = "json_extract"
JSONUnquote = "json_unquote"
JSONArray = "json_array"
JSONObject = "json_object"
JSONMerge = "json_merge"
JSONValid = "json_valid"
JSONSet = "json_set"
JSONInsert = "json_insert"
JSONReplace = "json_replace"
JSONRemove = "json_remove"
JSONContains = "json_contains"
JSONContainsPath = "json_contains_path"
)

// FuncCallExpr is for function expression.
Expand Down
23 changes: 12 additions & 11 deletions expression/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -572,15 +572,16 @@ 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.JSONContains: &jsonContainsFunctionClass{baseFunctionClass{ast.JSONContains, 2, 3}},
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}},
ast.JSONContainsPath: &jsonContainsPathFunctionClass{baseFunctionClass{ast.JSONContainsPath, 3, -1}},
}
64 changes: 64 additions & 0 deletions expression/builtin_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ var (
_ functionClass = &jsonObjectFunctionClass{}
_ functionClass = &jsonArrayFunctionClass{}
_ functionClass = &jsonContainsFunctionClass{}
_ functionClass = &jsonContainsPathFunctionClass{}

// Type of JSON value.
_ builtinFunc = &builtinJSONTypeSig{}
Expand Down Expand Up @@ -513,6 +514,69 @@ func (b *builtinJSONArraySig) evalJSON(row chunk.Row) (res json.BinaryJSON, isNu
return json.CreateBinary(jsons), false, nil
}

type jsonContainsPathFunctionClass struct {
baseFunctionClass
}

type builtinJSONContainsPathSig struct {
baseBuiltinFunc
}

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

func (c *jsonContainsPathFunctionClass) 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.ETString}
for i := 3; i <= len(args); i++ {
argTps = append(argTps, types.ETString)
}
bf := newBaseBuiltinFuncWithTp(ctx, args, types.ETInt, argTps...)
sig := &builtinJSONContainsPathSig{bf}
sig.setPbCode(tipb.ScalarFuncSig_JsonContainsPathSig)
return sig, nil
}

func (b *builtinJSONContainsPathSig) 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)
}
containType, isNull, err := b.args[1].EvalString(b.ctx, row)
if isNull || err != nil {
return res, isNull, errors.Trace(err)
}
if containType != json.ContainsPathAll && containType != json.ContainsPathOne {
return res, true, json.ErrInvalidJSONContainsPathType
}
var pathExpr json.PathExpression
contains := int64(1)
for i := 2; i < len(b.args); i++ {
path, isNull, err := b.args[i].EvalString(b.ctx, row)
if isNull || err != nil {
return res, isNull, errors.Trace(err)
}
if pathExpr, err = json.ParseJSONPathExpr(path); err != nil {
return res, true, errors.Trace(err)
}
_, exists := obj.Extract([]json.PathExpression{pathExpr})
switch {
case exists && containType == json.ContainsPathOne:
return 1, false, nil
case !exists && containType == json.ContainsPathOne:
contains = 0
case !exists && containType == json.ContainsPathAll:
return 0, false, nil
}
}
return contains, false, nil
}

func jsonModify(ctx sessionctx.Context, args []Expression, row chunk.Row, mt json.ModifyType) (res json.BinaryJSON, isNull bool, err error) {
res, isNull, err = args[0].EvalJSON(ctx, row)
if isNull || err != nil {
Expand Down
52 changes: 52 additions & 0 deletions expression/builtin_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,59 @@ func (s *testEvaluatorSuite) TestJSONContains(c *C) {
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)
}
}
}

func (s *testEvaluatorSuite) TestJSONContainsPath(c *C) {
defer testleak.AfterTest(c)()
fc := funcs[ast.JSONContainsPath]
jsonString := `{"a": 1, "b": 2, "c": {"d": 4}}`
invalidJSON := `{"a": 1`
tbl := []struct {
input []interface{}
expected interface{}
success bool
}{
// Tests nil arguments
{[]interface{}{nil, json.ContainsPathOne, "$.c"}, nil, true},
{[]interface{}{nil, json.ContainsPathAll, "$.c"}, nil, true},
{[]interface{}{jsonString, nil, "$.a[3]"}, nil, true},
{[]interface{}{jsonString, json.ContainsPathOne, nil}, nil, true},
{[]interface{}{jsonString, json.ContainsPathAll, nil}, nil, true},
// Tests with one path expression
{[]interface{}{jsonString, json.ContainsPathOne, "$.c.d"}, 1, true},
{[]interface{}{jsonString, json.ContainsPathOne, "$.a.d"}, 0, true},
{[]interface{}{jsonString, json.ContainsPathAll, "$.c.d"}, 1, true},
{[]interface{}{jsonString, json.ContainsPathAll, "$.a.d"}, 0, true},
// Tests with multiple path expression
{[]interface{}{jsonString, json.ContainsPathOne, "$.a", "$.e"}, 1, true},
{[]interface{}{jsonString, json.ContainsPathOne, "$.a", "$.c"}, 1, true},
{[]interface{}{jsonString, json.ContainsPathAll, "$.a", "$.e"}, 0, true},
{[]interface{}{jsonString, json.ContainsPathAll, "$.a", "$.c"}, 1, true},
// Tests path expression contains any asterisk
{[]interface{}{jsonString, json.ContainsPathOne, "$.*"}, 1, true},
{[]interface{}{jsonString, json.ContainsPathOne, "$[*]"}, 0, true},
{[]interface{}{jsonString, json.ContainsPathAll, "$.*"}, 1, true},
{[]interface{}{jsonString, json.ContainsPathAll, "$[*]"}, 0, true},
// Tests invalid json document
{[]interface{}{invalidJSON, json.ContainsPathOne, "$.a"}, nil, false},
{[]interface{}{invalidJSON, json.ContainsPathAll, "$.a"}, nil, false},
}
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 {
Expand Down
33 changes: 33 additions & 0 deletions expression/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3256,6 +3256,39 @@ func (s *testIntegrationSuite) TestFuncJSON(c *C) {
_, 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.")

r = tk.MustQuery(`select
json_contains_path(NULL, 'one', "$.c"),
json_contains_path(NULL, 'all', "$.c"),
json_contains_path('{"a": 1}', NULL, "$.c"),
json_contains_path('{"a": 1}', 'one', NULL),
json_contains_path('{"a": 1}', 'all', NULL)
`)
r.Check(testkit.Rows("<nil> <nil> <nil> <nil> <nil>"))

r = tk.MustQuery(`select
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'one', '$.c.d'),
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'one', '$.a.d'),
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'all', '$.c.d'),
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'all', '$.a.d')
`)
r.Check(testkit.Rows("1 0 1 0"))

r = tk.MustQuery(`select
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'one', '$.a', '$.e'),
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'one', '$.a', '$.b'),
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'all', '$.a', '$.e'),
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'all', '$.a', '$.b')
`)
r.Check(testkit.Rows("1 1 0 1"))

r = tk.MustQuery(`select
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'one', '$.*'),
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'one', '$[*]'),
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'all', '$.*'),
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'all', '$[*]')
`)
r.Check(testkit.Rows("1 0 1 0"))
}

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 @@ -890,6 +890,7 @@ const (
ErrInvalidJSONPath = 3143
ErrInvalidJSONData = 3146
ErrInvalidJSONPathWildcard = 3149
ErrInvalidJSONContainsPathType = 3150
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 @@ -887,6 +887,7 @@ var MySQLErrName = map[uint16]string{
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.",
ErrInvalidJSONContainsPathType: "The second argument can only be either 'one' or 'all'.",
ErrJSONUsedAsKey: "JSON column '%-.192s' cannot be used in key specification.",

// TiDB errors.
Expand Down
2 changes: 1 addition & 1 deletion types/json/binary_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func (bj BinaryJSON) extractTo(buf []BinaryJSON, pathExpr PathExpression) []Bina
currentLeg, subPathExpr := pathExpr.popOneLeg()
if currentLeg.typ == pathLegIndex {
if bj.TypeCode != TypeCodeArray {
if currentLeg.arrayIndex <= 0 {
if currentLeg.arrayIndex <= 0 && currentLeg.arrayIndex != arrayIndexAsterisk {
xiangyuf marked this conversation as resolved.
Show resolved Hide resolved
buf = bj.extractTo(buf, subPathExpr)
}
return buf
Expand Down
20 changes: 16 additions & 4 deletions types/json/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,25 @@ var (
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])
// ErrInvalidJSONContainsPathType means invalid JSON contains path type.
ErrInvalidJSONContainsPathType = terror.ClassJSON.New(mysql.ErrInvalidJSONContainsPathType, mysql.MySQLErrName[mysql.ErrInvalidJSONContainsPathType])
)

func init() {
terror.ErrClassToMySQLCodes[terror.ClassJSON] = map[terror.ErrCode]uint16{
mysql.ErrInvalidJSONText: mysql.ErrInvalidJSONText,
mysql.ErrInvalidJSONPath: mysql.ErrInvalidJSONPath,
mysql.ErrInvalidJSONData: mysql.ErrInvalidJSONData,
mysql.ErrInvalidJSONPathWildcard: mysql.ErrInvalidJSONPathWildcard,
mysql.ErrInvalidJSONText: mysql.ErrInvalidJSONText,
mysql.ErrInvalidJSONPath: mysql.ErrInvalidJSONPath,
mysql.ErrInvalidJSONData: mysql.ErrInvalidJSONData,
mysql.ErrInvalidJSONPathWildcard: mysql.ErrInvalidJSONPathWildcard,
mysql.ErrInvalidJSONContainsPathType: mysql.ErrInvalidJSONContainsPathType,
}
}

// json_contains_path function type choices
// See: https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html#function_json-contains-path
const (
// 'all': 1 if all paths exist within the document, 0 otherwise.
ContainsPathAll = "all"
// 'one': 1 if at least one path exists within the document, 0 otherwise.
ContainsPathOne = "one"
)
Loading