Skip to content

Commit

Permalink
expression: support builtin function json_contains_path
Browse files Browse the repository at this point in the history
  • Loading branch information
xiangyuf committed Sep 7, 2018
1 parent 6fb1a63 commit e407f66
Show file tree
Hide file tree
Showing 10 changed files with 470 additions and 301 deletions.
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 {
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

0 comments on commit e407f66

Please sign in to comment.