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_array_append #9609

Merged
merged 17 commits into from
Mar 14, 2019
Merged
Show file tree
Hide file tree
Changes from 11 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
92 changes: 91 additions & 1 deletion expression/builtin_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ var (
_ builtinFunc = &builtinJSONQuoteSig{}
_ builtinFunc = &builtinJSONUnquoteSig{}
_ builtinFunc = &builtinJSONArraySig{}
_ builtinFunc = &builtinJSONArrayAppendSig{}
_ builtinFunc = &builtinJSONObjectSig{}
_ builtinFunc = &builtinJSONExtractSig{}
_ builtinFunc = &builtinJSONSetSig{}
Expand Down Expand Up @@ -702,8 +703,97 @@ type jsonArrayAppendFunctionClass struct {
baseFunctionClass
}

type builtinJSONArrayAppendSig struct {
baseBuiltinFunc
}

func (c *jsonArrayAppendFunctionClass) verifyArgs(args []Expression) error {
if len(args) < 3 || (len(args)&1 != 1) {
return ErrIncorrectParameterCount.GenWithStackByArgs(c.funcName)
}
return nil
}

func (c *jsonArrayAppendFunctionClass) getFunction(ctx sessionctx.Context, args []Expression) (builtinFunc, error) {
return nil, errFunctionNotExists.GenWithStackByArgs("FUNCTION", "JSON_ARRAY_APPEND")
if err := c.verifyArgs(args); err != nil {
return nil, err
}
argTps := make([]types.EvalType, 0, len(args))
argTps = append(argTps, types.ETJson)
for i := 1; i < len(args)-1; i += 2 {
argTps = append(argTps, types.ETString, types.ETJson)
}
bf := newBaseBuiltinFuncWithTp(ctx, args, types.ETJson, argTps...)
for i := 2; i < len(args); i += 2 {
DisableParseJSONFlag4Expr(args[i])
}
sig := &builtinJSONArrayAppendSig{bf}
sig.setPbCode(tipb.ScalarFuncSig_JsonArrayAppendSig)
return sig, nil
}

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

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

for i := 1; i < len(b.args)-1; i += 2 {
// if JSON path is Null, then mysql break and return null
s, isNull, err := b.args[i].EvalString(b.ctx, row)
if isNull || err != nil {
return res, true, err
}
pathExpr, err := json.ParseJSONPathExpr(s)
if err != nil {
return res, true, json.ErrInvalidJSONPath.GenWithStackByArgs(s)
}
if pathExpr.ContainsAnyAsterisk() {
return res, true, json.ErrInvalidJSONPathWildcard.GenWithStackByArgs(s)
}

var exists bool
zz-jason marked this conversation as resolved.
Show resolved Hide resolved
obj, exists := res.Extract([]json.PathExpression{pathExpr})
if !exists {
// just do nothing for this
continue
}

if obj.TypeCode != json.TypeCodeArray {
// JSON_ARRAY_APPEND({"a": "b"}, "$", "c") => [{"a": "b"}, "c"]
// We should convert them to a single array first
obj = json.CreateBinary([]interface{}{obj})
}

var (
zz-jason marked this conversation as resolved.
Show resolved Hide resolved
value json.BinaryJSON
isnull bool
)

value, isnull, err = b.args[i+1].EvalJSON(b.ctx, row)
if err != nil {
return res, true, err
}

if isnull {
value = json.CreateBinary(nil)
eurekaka marked this conversation as resolved.
Show resolved Hide resolved
}

obj = json.MergeBinary([]json.BinaryJSON{obj, value})
res, err = res.Modify([]json.PathExpression{pathExpr}, []json.BinaryJSON{obj}, json.ModifySet)
if err != nil {
// err should always be nil, the function should never return here
return res, true, err
}
}
// res won't be nil, error should be nil
return res, false, nil
}

type jsonArrayInsertFunctionClass struct {
Expand Down
82 changes: 82 additions & 0 deletions expression/builtin_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package expression
import (
. "github.com/pingcap/check"
"github.com/pingcap/parser/ast"
"github.com/pingcap/parser/terror"
"github.com/pingcap/tidb/types"
"github.com/pingcap/tidb/types/json"
"github.com/pingcap/tidb/util/chunk"
Expand Down Expand Up @@ -686,3 +687,84 @@ func (s *testEvaluatorSuite) TestJSONDepth(c *C) {
}
}
}

func (s *testEvaluatorSuite) TestJSONArrayAppend(c *C) {
defer testleak.AfterTest(c)()
sampleJSON, err := json.ParseBinaryFromString(`{"b": 2}`)
c.Assert(err, IsNil)
fc := funcs[ast.JSONArrayAppend]
tbl := []struct {
input []interface{}
expected interface{}
err *terror.Error
}{
{[]interface{}{`{"a": 1, "b": [2, 3], "c": 4}`, `$.d`, `z`}, `{"a": 1, "b": [2, 3], "c": 4}`, nil},
{[]interface{}{`{"a": 1, "b": [2, 3], "c": 4}`, `$`, `w`}, `[{"a": 1, "b": [2, 3], "c": 4}, "w"]`, nil},
{[]interface{}{`{"a": 1, "b": [2, 3], "c": 4}`, `$`, nil}, `[{"a": 1, "b": [2, 3], "c": 4}, null]`, nil},
{[]interface{}{`{"a": 1}`, `$`, `{"b": 2}`}, `[{"a": 1}, "{\"b\": 2}"]`, nil},
{[]interface{}{`{"a": 1}`, `$`, sampleJSON}, `[{"a": 1}, {"b": 2}]`, nil},
{[]interface{}{`{"a": 1}`, `$.a`, sampleJSON}, `{"a": [1, {"b": 2}]}`, nil},
// explains hy call res.Modify every path-value pair and not do as JSON_SET do?
zz-jason marked this conversation as resolved.
Show resolved Hide resolved
{[]interface{}{`{"a": 1}`, `$.a`, sampleJSON, `$.a[1]`, sampleJSON}, `{"a": [1, [{"b": 2}, {"b": 2}]]}`, nil},
{[]interface{}{nil, `$`, nil}, nil, nil},
{[]interface{}{nil, `$`, `a`}, nil, nil},
{[]interface{}{`null`, `$`, nil}, `[null, null]`, nil},
// bad arguments
{[]interface{}{`asdf`, `$`, nil}, nil, json.ErrInvalidJSONText},
{[]interface{}{``, `$`, nil}, nil, json.ErrInvalidJSONText},
{[]interface{}{`[]`, `$`, nil}, `[null]`, nil},
{[]interface{}{`{}`, `$`, nil}, `[{}, null]`, nil},
{[]interface{}{`{"a": 1, "b": [2, 3], "c": 4}`, `$.d`}, nil, ErrIncorrectParameterCount},
{[]interface{}{`{"a": 1, "b": [2, 3], "c": 4}`, `$.c`, `y`, `$.b`}, nil, ErrIncorrectParameterCount},
// following tests comes from MySQL doc
zz-jason marked this conversation as resolved.
Show resolved Hide resolved
{[]interface{}{`["a", ["b", "c"], "d"]`, `$[1]`, 1}, `["a", ["b", "c", 1], "d"]`, nil},
{[]interface{}{`["a", ["b", "c"], "d"]`, `$[0]`, 2}, `[["a", 2], ["b", "c"], "d"]`, nil},
{[]interface{}{`["a", ["b", "c"], "d"]`, `$[1][0]`, 3}, `["a", [["b", 3], "c"], "d"]`, nil},
{[]interface{}{`{"a": 1, "b": [2, 3], "c": 4}`, `$.b`, `x`}, `{"a": 1, "b": [2, 3, "x"], "c": 4}`, nil},
{[]interface{}{`{"a": 1, "b": [2, 3], "c": 4}`, `$.c`, `y`}, `{"a": 1, "b": [2, 3], "c": [4, "y"]}`, nil},
// following tests comes from MySQL test
{[]interface{}{`[1,2,3, {"a":[4,5,6]}]`, `$`, 7}, `[1, 2, 3, {"a": [4, 5, 6]}, 7]`, nil},
{[]interface{}{`[1,2,3, {"a":[4,5,6]}]`, `$`, 7, `$[3].a`, 3.14}, `[1, 2, 3, {"a": [4, 5, 6, 3.14]}, 7]`, nil},
{[]interface{}{`[1,2,3, {"a":[4,5,6]}]`, `$`, 7, `$[3].b`, 8}, `[1, 2, 3, {"a": [4, 5, 6]}, 7]`, nil},
// warning instead of error for illegal params
zz-jason marked this conversation as resolved.
Show resolved Hide resolved
{[]interface{}{`{"a": 1, "b": [2, 3], "c": 4}`, nil, nil}, nil, nil},
{[]interface{}{`{"a": 1, "b": [2, 3], "c": 4}`, `asdf`, nil}, nil, json.ErrInvalidJSONPath},
{[]interface{}{`{"a": 1, "b": [2, 3], "c": 4}`, 42, nil}, nil, json.ErrInvalidJSONPath},
{[]interface{}{`{"a": 1, "b": [2, 3], "c": 4}`, `$.*`, nil}, nil, json.ErrInvalidJSONPathWildcard},
}

// non-strict mode
for i, t := range tbl {
args := types.MakeDatums(t.input...)
s.ctx.GetSessionVars().StmtCtx.SetWarnings(nil)
f, err := fc.getFunction(s.ctx, s.datumsToConstants(args))
// if t.success <= 1, no error should return in getFunction
if err != nil {
c.Assert(t.err.Equal(err), Equals, true)
// won't execute f
continue
}
c.Assert(f, NotNil)
d, err := evalBuiltinFunc(f, chunk.Row{})
comment := Commentf("case:%v \n input:%v \n output: %s \n expected: %v \n warnings: %v \n expected error %v", i, t.input, d.GetMysqlJSON(), t.expected, s.ctx.GetSessionVars().StmtCtx.GetWarnings(), t.err)
if t.err == nil {
zz-jason marked this conversation as resolved.
Show resolved Hide resolved
c.Assert(err, IsNil, comment)
c.Assert(int(s.ctx.GetSessionVars().StmtCtx.WarningCount()), Equals, 0, comment)

if t.expected == nil {
c.Assert(d.IsNull(), IsTrue, comment)
} else {
var j1 json.BinaryJSON
j1, err = json.ParseBinaryFromString(t.expected.(string))
c.Assert(err, IsNil)
j2 := d.GetMysqlJSON()
var cmp int
cmp = json.CompareBinary(j1, j2)
c.Assert(err, IsNil, comment)
c.Assert(cmp, Equals, 0, comment)
}
} else {
c.Assert(t.err.Equal(err), Equals, true, comment)
}
}
}
2 changes: 2 additions & 0 deletions expression/distsql_builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,8 @@ func getSignatureByPB(ctx sessionctx.Context, sigCode tipb.ScalarFuncSig, tp *ti
f = &builtinJSONUnquoteSig{base}
case tipb.ScalarFuncSig_JsonArraySig:
f = &builtinJSONArraySig{base}
case tipb.ScalarFuncSig_JsonArrayAppendSig:
f = &builtinJSONArrayAppendSig{base}
case tipb.ScalarFuncSig_JsonObjectSig:
f = &builtinJSONObjectSig{base}
case tipb.ScalarFuncSig_JsonExtractSig:
Expand Down