Skip to content

Commit e407f66

Browse files
committed
expression: support builtin function json_contains_path
1 parent 6fb1a63 commit e407f66

File tree

10 files changed

+470
-301
lines changed

10 files changed

+470
-301
lines changed

ast/functions.go

+13-12
Original file line numberDiff line numberDiff line change
@@ -290,18 +290,19 @@ const (
290290
ValidatePasswordStrength = "validate_password_strength"
291291

292292
// json functions
293-
JSONType = "json_type"
294-
JSONExtract = "json_extract"
295-
JSONUnquote = "json_unquote"
296-
JSONArray = "json_array"
297-
JSONObject = "json_object"
298-
JSONMerge = "json_merge"
299-
JSONValid = "json_valid"
300-
JSONSet = "json_set"
301-
JSONInsert = "json_insert"
302-
JSONReplace = "json_replace"
303-
JSONRemove = "json_remove"
304-
JSONContains = "json_contains"
293+
JSONType = "json_type"
294+
JSONExtract = "json_extract"
295+
JSONUnquote = "json_unquote"
296+
JSONArray = "json_array"
297+
JSONObject = "json_object"
298+
JSONMerge = "json_merge"
299+
JSONValid = "json_valid"
300+
JSONSet = "json_set"
301+
JSONInsert = "json_insert"
302+
JSONReplace = "json_replace"
303+
JSONRemove = "json_remove"
304+
JSONContains = "json_contains"
305+
JSONContainsPath = "json_contains_path"
305306
)
306307

307308
// FuncCallExpr is for function expression.

expression/builtin.go

+12-11
Original file line numberDiff line numberDiff line change
@@ -572,15 +572,16 @@ var funcs = map[string]functionClass{
572572
ast.ValidatePasswordStrength: &validatePasswordStrengthFunctionClass{baseFunctionClass{ast.ValidatePasswordStrength, 1, 1}},
573573

574574
// json functions
575-
ast.JSONType: &jsonTypeFunctionClass{baseFunctionClass{ast.JSONType, 1, 1}},
576-
ast.JSONExtract: &jsonExtractFunctionClass{baseFunctionClass{ast.JSONExtract, 2, -1}},
577-
ast.JSONUnquote: &jsonUnquoteFunctionClass{baseFunctionClass{ast.JSONUnquote, 1, 1}},
578-
ast.JSONSet: &jsonSetFunctionClass{baseFunctionClass{ast.JSONSet, 3, -1}},
579-
ast.JSONInsert: &jsonInsertFunctionClass{baseFunctionClass{ast.JSONInsert, 3, -1}},
580-
ast.JSONReplace: &jsonReplaceFunctionClass{baseFunctionClass{ast.JSONReplace, 3, -1}},
581-
ast.JSONRemove: &jsonRemoveFunctionClass{baseFunctionClass{ast.JSONRemove, 2, -1}},
582-
ast.JSONMerge: &jsonMergeFunctionClass{baseFunctionClass{ast.JSONMerge, 2, -1}},
583-
ast.JSONObject: &jsonObjectFunctionClass{baseFunctionClass{ast.JSONObject, 0, -1}},
584-
ast.JSONArray: &jsonArrayFunctionClass{baseFunctionClass{ast.JSONArray, 0, -1}},
585-
ast.JSONContains: &jsonContainsFunctionClass{baseFunctionClass{ast.JSONContains, 2, 3}},
575+
ast.JSONType: &jsonTypeFunctionClass{baseFunctionClass{ast.JSONType, 1, 1}},
576+
ast.JSONExtract: &jsonExtractFunctionClass{baseFunctionClass{ast.JSONExtract, 2, -1}},
577+
ast.JSONUnquote: &jsonUnquoteFunctionClass{baseFunctionClass{ast.JSONUnquote, 1, 1}},
578+
ast.JSONSet: &jsonSetFunctionClass{baseFunctionClass{ast.JSONSet, 3, -1}},
579+
ast.JSONInsert: &jsonInsertFunctionClass{baseFunctionClass{ast.JSONInsert, 3, -1}},
580+
ast.JSONReplace: &jsonReplaceFunctionClass{baseFunctionClass{ast.JSONReplace, 3, -1}},
581+
ast.JSONRemove: &jsonRemoveFunctionClass{baseFunctionClass{ast.JSONRemove, 2, -1}},
582+
ast.JSONMerge: &jsonMergeFunctionClass{baseFunctionClass{ast.JSONMerge, 2, -1}},
583+
ast.JSONObject: &jsonObjectFunctionClass{baseFunctionClass{ast.JSONObject, 0, -1}},
584+
ast.JSONArray: &jsonArrayFunctionClass{baseFunctionClass{ast.JSONArray, 0, -1}},
585+
ast.JSONContains: &jsonContainsFunctionClass{baseFunctionClass{ast.JSONContains, 2, 3}},
586+
ast.JSONContainsPath: &jsonContainsPathFunctionClass{baseFunctionClass{ast.JSONContainsPath, 3, -1}},
586587
}

expression/builtin_json.go

+64
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ var (
3636
_ functionClass = &jsonObjectFunctionClass{}
3737
_ functionClass = &jsonArrayFunctionClass{}
3838
_ functionClass = &jsonContainsFunctionClass{}
39+
_ functionClass = &jsonContainsPathFunctionClass{}
3940

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

517+
type jsonContainsPathFunctionClass struct {
518+
baseFunctionClass
519+
}
520+
521+
type builtinJSONContainsPathSig struct {
522+
baseBuiltinFunc
523+
}
524+
525+
func (b *builtinJSONContainsPathSig) Clone() builtinFunc {
526+
newSig := &builtinJSONContainsPathSig{}
527+
newSig.cloneFrom(&b.baseBuiltinFunc)
528+
return newSig
529+
}
530+
531+
func (c *jsonContainsPathFunctionClass) getFunction(ctx sessionctx.Context, args []Expression) (builtinFunc, error) {
532+
if err := c.verifyArgs(args); err != nil {
533+
return nil, errors.Trace(err)
534+
}
535+
argTps := []types.EvalType{types.ETJson, types.ETString}
536+
for i := 3; i <= len(args); i++ {
537+
argTps = append(argTps, types.ETString)
538+
}
539+
bf := newBaseBuiltinFuncWithTp(ctx, args, types.ETInt, argTps...)
540+
sig := &builtinJSONContainsPathSig{bf}
541+
sig.setPbCode(tipb.ScalarFuncSig_JsonContainsPathSig)
542+
return sig, nil
543+
}
544+
545+
func (b *builtinJSONContainsPathSig) evalInt(row chunk.Row) (res int64, isNull bool, err error) {
546+
obj, isNull, err := b.args[0].EvalJSON(b.ctx, row)
547+
if isNull || err != nil {
548+
return res, isNull, errors.Trace(err)
549+
}
550+
containType, isNull, err := b.args[1].EvalString(b.ctx, row)
551+
if isNull || err != nil {
552+
return res, isNull, errors.Trace(err)
553+
}
554+
if containType != json.ContainsPathAll && containType != json.ContainsPathOne {
555+
return res, true, json.ErrInvalidJSONContainsPathType
556+
}
557+
var pathExpr json.PathExpression
558+
contains := int64(1)
559+
for i := 2; i < len(b.args); i++ {
560+
path, isNull, err := b.args[i].EvalString(b.ctx, row)
561+
if isNull || err != nil {
562+
return res, isNull, errors.Trace(err)
563+
}
564+
if pathExpr, err = json.ParseJSONPathExpr(path); err != nil {
565+
return res, true, errors.Trace(err)
566+
}
567+
_, exists := obj.Extract([]json.PathExpression{pathExpr})
568+
switch {
569+
case exists && containType == json.ContainsPathOne:
570+
return 1, false, nil
571+
case !exists && containType == json.ContainsPathOne:
572+
contains = 0
573+
case !exists && containType == json.ContainsPathAll:
574+
return 0, false, nil
575+
}
576+
}
577+
return contains, false, nil
578+
}
579+
516580
func jsonModify(ctx sessionctx.Context, args []Expression, row chunk.Row, mt json.ModifyType) (res json.BinaryJSON, isNull bool, err error) {
517581
res, isNull, err = args[0].EvalJSON(ctx, row)
518582
if isNull || err != nil {

expression/builtin_json_test.go

+52
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,59 @@ func (s *testEvaluatorSuite) TestJSONContains(c *C) {
358358
f, err := fc.getFunction(s.ctx, s.datumsToConstants(args))
359359
c.Assert(err, IsNil)
360360
d, err := evalBuiltinFunc(f, chunk.Row{})
361+
if t.success {
362+
c.Assert(err, IsNil)
363+
if t.expected == nil {
364+
c.Assert(d.IsNull(), IsTrue)
365+
} else {
366+
c.Assert(d.GetInt64(), Equals, int64(t.expected.(int)))
367+
}
368+
} else {
369+
c.Assert(err, NotNil)
370+
}
371+
}
372+
}
361373

374+
func (s *testEvaluatorSuite) TestJSONContainsPath(c *C) {
375+
defer testleak.AfterTest(c)()
376+
fc := funcs[ast.JSONContainsPath]
377+
jsonString := `{"a": 1, "b": 2, "c": {"d": 4}}`
378+
invalidJSON := `{"a": 1`
379+
tbl := []struct {
380+
input []interface{}
381+
expected interface{}
382+
success bool
383+
}{
384+
// Tests nil arguments
385+
{[]interface{}{nil, json.ContainsPathOne, "$.c"}, nil, true},
386+
{[]interface{}{nil, json.ContainsPathAll, "$.c"}, nil, true},
387+
{[]interface{}{jsonString, nil, "$.a[3]"}, nil, true},
388+
{[]interface{}{jsonString, json.ContainsPathOne, nil}, nil, true},
389+
{[]interface{}{jsonString, json.ContainsPathAll, nil}, nil, true},
390+
// Tests with one path expression
391+
{[]interface{}{jsonString, json.ContainsPathOne, "$.c.d"}, 1, true},
392+
{[]interface{}{jsonString, json.ContainsPathOne, "$.a.d"}, 0, true},
393+
{[]interface{}{jsonString, json.ContainsPathAll, "$.c.d"}, 1, true},
394+
{[]interface{}{jsonString, json.ContainsPathAll, "$.a.d"}, 0, true},
395+
// Tests with multiple path expression
396+
{[]interface{}{jsonString, json.ContainsPathOne, "$.a", "$.e"}, 1, true},
397+
{[]interface{}{jsonString, json.ContainsPathOne, "$.a", "$.c"}, 1, true},
398+
{[]interface{}{jsonString, json.ContainsPathAll, "$.a", "$.e"}, 0, true},
399+
{[]interface{}{jsonString, json.ContainsPathAll, "$.a", "$.c"}, 1, true},
400+
// Tests path expression contains any asterisk
401+
{[]interface{}{jsonString, json.ContainsPathOne, "$.*"}, 1, true},
402+
{[]interface{}{jsonString, json.ContainsPathOne, "$[*]"}, 0, true},
403+
{[]interface{}{jsonString, json.ContainsPathAll, "$.*"}, 1, true},
404+
{[]interface{}{jsonString, json.ContainsPathAll, "$[*]"}, 0, true},
405+
// Tests invalid json document
406+
{[]interface{}{invalidJSON, json.ContainsPathOne, "$.a"}, nil, false},
407+
{[]interface{}{invalidJSON, json.ContainsPathAll, "$.a"}, nil, false},
408+
}
409+
for _, t := range tbl {
410+
args := types.MakeDatums(t.input...)
411+
f, err := fc.getFunction(s.ctx, s.datumsToConstants(args))
412+
c.Assert(err, IsNil)
413+
d, err := evalBuiltinFunc(f, chunk.Row{})
362414
if t.success {
363415
c.Assert(err, IsNil)
364416
if t.expected == nil {

expression/integration_test.go

+33
Original file line numberDiff line numberDiff line change
@@ -3256,6 +3256,39 @@ func (s *testIntegrationSuite) TestFuncJSON(c *C) {
32563256
_, err = session.GetRows4Test(context.Background(), tk.Se, rs)
32573257
c.Assert(err, NotNil)
32583258
c.Assert(err.Error(), Equals, "[json:3149]In this situation, path expressions may not contain the * and ** tokens.")
3259+
3260+
r = tk.MustQuery(`select
3261+
json_contains_path(NULL, 'one', "$.c"),
3262+
json_contains_path(NULL, 'all', "$.c"),
3263+
json_contains_path('{"a": 1}', NULL, "$.c"),
3264+
json_contains_path('{"a": 1}', 'one', NULL),
3265+
json_contains_path('{"a": 1}', 'all', NULL)
3266+
`)
3267+
r.Check(testkit.Rows("<nil> <nil> <nil> <nil> <nil>"))
3268+
3269+
r = tk.MustQuery(`select
3270+
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'one', '$.c.d'),
3271+
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'one', '$.a.d'),
3272+
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'all', '$.c.d'),
3273+
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'all', '$.a.d')
3274+
`)
3275+
r.Check(testkit.Rows("1 0 1 0"))
3276+
3277+
r = tk.MustQuery(`select
3278+
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'one', '$.a', '$.e'),
3279+
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'one', '$.a', '$.b'),
3280+
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'all', '$.a', '$.e'),
3281+
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'all', '$.a', '$.b')
3282+
`)
3283+
r.Check(testkit.Rows("1 1 0 1"))
3284+
3285+
r = tk.MustQuery(`select
3286+
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'one', '$.*'),
3287+
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'one', '$[*]'),
3288+
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'all', '$.*'),
3289+
json_contains_path('{"a": 1, "b": 2, "c": {"d": 4}}', 'all', '$[*]')
3290+
`)
3291+
r.Check(testkit.Rows("1 0 1 0"))
32593292
}
32603293

32613294
func (s *testIntegrationSuite) TestColumnInfoModified(c *C) {

mysql/errcode.go

+1
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,7 @@ const (
890890
ErrInvalidJSONPath = 3143
891891
ErrInvalidJSONData = 3146
892892
ErrInvalidJSONPathWildcard = 3149
893+
ErrInvalidJSONContainsPathType = 3150
893894
ErrJSONUsedAsKey = 3152
894895

895896
// TiDB self-defined errors.

mysql/errname.go

+1
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,7 @@ var MySQLErrName = map[uint16]string{
887887
ErrInvalidJSONPath: "Invalid JSON path expression %s.",
888888
ErrInvalidJSONData: "Invalid data type for JSON data",
889889
ErrInvalidJSONPathWildcard: "In this situation, path expressions may not contain the * and ** tokens.",
890+
ErrInvalidJSONContainsPathType: "The second argument can only be either 'one' or 'all'.",
890891
ErrJSONUsedAsKey: "JSON column '%-.192s' cannot be used in key specification.",
891892

892893
// TiDB errors.

types/json/binary_functions.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ func (bj BinaryJSON) extractTo(buf []BinaryJSON, pathExpr PathExpression) []Bina
170170
currentLeg, subPathExpr := pathExpr.popOneLeg()
171171
if currentLeg.typ == pathLegIndex {
172172
if bj.TypeCode != TypeCodeArray {
173-
if currentLeg.arrayIndex <= 0 {
173+
if currentLeg.arrayIndex <= 0 && currentLeg.arrayIndex != arrayIndexAsterisk {
174174
buf = bj.extractTo(buf, subPathExpr)
175175
}
176176
return buf

types/json/constants.go

+16-4
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,25 @@ var (
214214
ErrInvalidJSONData = terror.ClassJSON.New(mysql.ErrInvalidJSONData, mysql.MySQLErrName[mysql.ErrInvalidJSONData])
215215
// ErrInvalidJSONPathWildcard means invalid JSON path that contain wildcard characters.
216216
ErrInvalidJSONPathWildcard = terror.ClassJSON.New(mysql.ErrInvalidJSONPathWildcard, mysql.MySQLErrName[mysql.ErrInvalidJSONPathWildcard])
217+
// ErrInvalidJSONContainsPathType means invalid JSON contains path type.
218+
ErrInvalidJSONContainsPathType = terror.ClassJSON.New(mysql.ErrInvalidJSONContainsPathType, mysql.MySQLErrName[mysql.ErrInvalidJSONContainsPathType])
217219
)
218220

219221
func init() {
220222
terror.ErrClassToMySQLCodes[terror.ClassJSON] = map[terror.ErrCode]uint16{
221-
mysql.ErrInvalidJSONText: mysql.ErrInvalidJSONText,
222-
mysql.ErrInvalidJSONPath: mysql.ErrInvalidJSONPath,
223-
mysql.ErrInvalidJSONData: mysql.ErrInvalidJSONData,
224-
mysql.ErrInvalidJSONPathWildcard: mysql.ErrInvalidJSONPathWildcard,
223+
mysql.ErrInvalidJSONText: mysql.ErrInvalidJSONText,
224+
mysql.ErrInvalidJSONPath: mysql.ErrInvalidJSONPath,
225+
mysql.ErrInvalidJSONData: mysql.ErrInvalidJSONData,
226+
mysql.ErrInvalidJSONPathWildcard: mysql.ErrInvalidJSONPathWildcard,
227+
mysql.ErrInvalidJSONContainsPathType: mysql.ErrInvalidJSONContainsPathType,
225228
}
226229
}
230+
231+
// json_contains_path function type choices
232+
// See: https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html#function_json-contains-path
233+
const (
234+
// 'all': 1 if all paths exist within the document, 0 otherwise.
235+
ContainsPathAll = "all"
236+
// 'one': 1 if at least one path exists within the document, 0 otherwise.
237+
ContainsPathOne = "one"
238+
)

0 commit comments

Comments
 (0)