diff --git a/expression/builtin_json.go b/expression/builtin_json.go index 0690f334b45b9..089b2d71a3ec8 100644 --- a/expression/builtin_json.go +++ b/expression/builtin_json.go @@ -1048,7 +1048,56 @@ type jsonMergePatchFunctionClass struct { } func (c *jsonMergePatchFunctionClass) getFunction(ctx sessionctx.Context, args []Expression) (builtinFunc, error) { - return nil, errFunctionNotExists.GenWithStackByArgs("FUNCTION", "JSON_MERGE_PATCH") + if err := c.verifyArgs(args); err != nil { + return nil, err + } + argTps := make([]types.EvalType, 0, len(args)) + for range args { + argTps = append(argTps, types.ETJson) + } + bf, err := newBaseBuiltinFuncWithTp(ctx, c.funcName, args, types.ETJson, argTps...) + if err != nil { + return nil, err + } + sig := &builtinJSONMergePatchSig{bf} + sig.setPbCode(tipb.ScalarFuncSig_JsonMergePatchSig) + return sig, nil +} + +type builtinJSONMergePatchSig struct { + baseBuiltinFunc +} + +func (b *builtinJSONMergePatchSig) Clone() builtinFunc { + newSig := &builtinJSONMergePatchSig{} + newSig.cloneFrom(&b.baseBuiltinFunc) + return newSig +} + +func (b *builtinJSONMergePatchSig) evalJSON(row chunk.Row) (res json.BinaryJSON, isNull bool, err error) { + values := make([]*json.BinaryJSON, 0, len(b.args)) + for _, arg := range b.args { + var value json.BinaryJSON + value, isNull, err = arg.EvalJSON(b.ctx, row) + if err != nil { + return + } + if isNull { + values = append(values, nil) + } else { + values = append(values, &value) + } + } + tmpRes, err := json.MergePatchBinary(values) + if err != nil { + return + } + if tmpRes != nil { + res = *tmpRes + } else { + isNull = true + } + return res, isNull, nil } type jsonMergePreserveFunctionClass struct { diff --git a/expression/builtin_json_test.go b/expression/builtin_json_test.go index 4cb744bb47969..0a9c2e9922724 100644 --- a/expression/builtin_json_test.go +++ b/expression/builtin_json_test.go @@ -1076,3 +1076,94 @@ func (s *testEvaluatorSuite) TestJSONPretty(c *C) { } } } + +func (s *testEvaluatorSuite) TestJSONMergePatch(c *C) { + fc := funcs[ast.JSONMergePatch] + tbl := []struct { + input []interface{} + expected interface{} + success bool + }{ + // RFC 7396 document: https://datatracker.ietf.org/doc/html/rfc7396 + // RFC 7396 Example Test Cases + {[]interface{}{`{"a":"b"}`, `{"a":"c"}`}, `{"a": "c"}`, true}, + {[]interface{}{`{"a":"b"}`, `{"b":"c"}`}, `{"a": "b","b": "c"}`, true}, + {[]interface{}{`{"a":"b"}`, `{"a":null}`}, `{}`, true}, + {[]interface{}{`{"a":"b", "b":"c"}`, `{"a":null}`}, `{"b": "c"}`, true}, + {[]interface{}{`{"a":["b"]}`, `{"a":"c"}`}, `{"a": "c"}`, true}, + {[]interface{}{`{"a":"c"}`, `{"a":["b"]}`}, `{"a": ["b"]}`, true}, + {[]interface{}{`{"a":{"b":"c"}}`, `{"a":{"b":"d","c":null}}`}, `{"a": {"b": "d"}}`, true}, + {[]interface{}{`{"a":[{"b":"c"}]}`, `{"a": [1]}`}, `{"a": [1]}`, true}, + {[]interface{}{`["a","b"]`, `["c","d"]`}, `["c", "d"]`, true}, + {[]interface{}{`{"a":"b"}`, `["c"]`}, `["c"]`, true}, + {[]interface{}{`{"a":"foo"}`, `null`}, `null`, true}, + {[]interface{}{`{"a":"foo"}`, `"bar"`}, `"bar"`, true}, + {[]interface{}{`{"e":null}`, `{"a":1}`}, `{"e": null,"a": 1}`, true}, + {[]interface{}{`[1,2]`, `{"a":"b","c":null}`}, `{"a":"b"}`, true}, + {[]interface{}{`{}`, `{"a":{"bb":{"ccc":null}}}`}, `{"a":{"bb": {}}}`, true}, + // RFC 7396 Example Document + {[]interface{}{`{"title":"Goodbye!","author":{"givenName":"John","familyName":"Doe"},"tags":["example","sample"],"content":"This will be unchanged"}`, `{"title":"Hello!","phoneNumber":"+01-123-456-7890","author":{"familyName":null},"tags":["example"]}`}, `{"title":"Hello!","author":{"givenName":"John"},"tags":["example"],"content":"This will be unchanged","phoneNumber":"+01-123-456-7890"}`, true}, + + // From mysql Example Test Cases + {[]interface{}{nil, `null`, `[1,2,3]`, `{"a":1}`}, `{"a": 1}`, true}, + {[]interface{}{`null`, nil, `[1,2,3]`, `{"a":1}`}, `{"a": 1}`, true}, + {[]interface{}{`null`, `[1,2,3]`, nil, `{"a":1}`}, nil, true}, + {[]interface{}{`null`, `[1,2,3]`, `{"a":1}`, nil}, nil, true}, + + {[]interface{}{nil, `null`, `{"a":1}`, `[1,2,3]`}, `[1,2,3]`, true}, + {[]interface{}{`null`, nil, `{"a":1}`, `[1,2,3]`}, `[1,2,3]`, true}, + {[]interface{}{`null`, `{"a":1}`, nil, `[1,2,3]`}, `[1,2,3]`, true}, + {[]interface{}{`null`, `{"a":1}`, `[1,2,3]`, nil}, nil, true}, + + {[]interface{}{nil, `null`, `{"a":1}`, `true`}, `true`, true}, + {[]interface{}{`null`, nil, `{"a":1}`, `true`}, `true`, true}, + {[]interface{}{`null`, `{"a":1}`, nil, `true`}, `true`, true}, + {[]interface{}{`null`, `{"a":1}`, `true`, nil}, nil, true}, + + // non-object last item + {[]interface{}{"true", "false", "[]", "{}", "null"}, "null", true}, + {[]interface{}{"false", "[]", "{}", "null", "true"}, "true", true}, + {[]interface{}{"true", "[]", "{}", "null", "false"}, "false", true}, + {[]interface{}{"true", "false", "{}", "null", "[]"}, "[]", true}, + {[]interface{}{"true", "false", "{}", "null", "1"}, "1", true}, + {[]interface{}{"true", "false", "{}", "null", "1.8"}, "1.8", true}, + {[]interface{}{"true", "false", "{}", "null", `"112"`}, `"112"`, true}, + + {[]interface{}{`{"a":"foo"}`, nil}, nil, true}, + {[]interface{}{nil, `{"a":"foo"}`}, nil, true}, + {[]interface{}{`{"a":"foo"}`, `false`}, `false`, true}, + {[]interface{}{`{"a":"foo"}`, `123`}, `123`, true}, + {[]interface{}{`{"a":"foo"}`, `123.1`}, `123.1`, true}, + {[]interface{}{`{"a":"foo"}`, `[1,2,3]`}, `[1,2,3]`, true}, + {[]interface{}{`null`, `{"a":1}`}, `{"a":1}`, true}, + {[]interface{}{`{"a":1}`, `null`}, `null`, true}, + {[]interface{}{`{"a":"foo"}`, `{"a":null}`, `{"b":"123"}`, `{"c":1}`}, `{"b":"123","c":1}`, true}, + {[]interface{}{`{"a":"foo"}`, `{"a":null}`, `{"c":1}`}, `{"c":1}`, true}, + {[]interface{}{`{"a":"foo"}`, `{"a":null}`, `true`}, `true`, true}, + {[]interface{}{`{"a":"foo"}`, `{"d":1}`, `{"a":{"bb":{"ccc":null}}}`}, `{"a":{"bb":{}},"d":1}`, true}, + + // Invalid json text + {[]interface{}{`{"a":1}`, `[1]}`}, nil, false}, + {[]interface{}{`{{"a":1}`, `[1]`, `null`}, nil, false}, + {[]interface{}{`{"a":1}`, `jjj`, `null`}, 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 { + c.Assert(d.IsNull(), IsTrue) + } else { + j, e := json.ParseBinaryFromString(t.expected.(string)) + c.Assert(e, IsNil) + c.Assert(d.GetMysqlJSON().String(), Equals, j.String()) + } + } else { + c.Assert(err, NotNil) + } + } +} diff --git a/expression/builtin_json_vec.go b/expression/builtin_json_vec.go index 767029cd1a141..57644d2d25be2 100644 --- a/expression/builtin_json_vec.go +++ b/expression/builtin_json_vec.go @@ -1218,3 +1218,51 @@ func (b *builtinJSONSPrettySig) vecEvalString(input *chunk.Chunk, result *chunk. } return nil } + +func (b *builtinJSONMergePatchSig) vectorized() bool { + return true +} + +func (b *builtinJSONMergePatchSig) vecEvalJSON(input *chunk.Chunk, result *chunk.Column) error { + nr := input.NumRows() + argBuffers := make([]*chunk.Column, len(b.args)) + var err error + for i, arg := range b.args { + if argBuffers[i], err = b.bufAllocator.get(types.ETJson, nr); err != nil { + return err + } + defer func(buf *chunk.Column) { + b.bufAllocator.put(buf) + }(argBuffers[i]) + + if err := arg.VecEvalJSON(b.ctx, input, argBuffers[i]); err != nil { + return err + } + } + + result.ReserveJSON(nr) + jsonValue := make([]*json.BinaryJSON, 0, len(b.args)) + for i := 0; i < nr; i++ { + jsonValue = jsonValue[:0] + for j := 0; j < len(b.args); j++ { + if argBuffers[j].IsNull(i) { + jsonValue = append(jsonValue, nil) + } else { + v := argBuffers[j].GetJSON(i) + jsonValue = append(jsonValue, &v) + } + } + + tmpJSON, e := json.MergePatchBinary(jsonValue) + if e != nil { + return e + } + if tmpJSON == nil { + result.AppendNull() + } else { + result.AppendJSON(*tmpJSON) + } + } + + return nil +} diff --git a/expression/integration_test.go b/expression/integration_test.go index 30cda81775346..99609dbe9f153 100644 --- a/expression/integration_test.go +++ b/expression/integration_test.go @@ -46,6 +46,7 @@ import ( "github.com/pingcap/tidb/table/tables" "github.com/pingcap/tidb/tablecodec" "github.com/pingcap/tidb/types" + "github.com/pingcap/tidb/types/json" "github.com/pingcap/tidb/util/codec" "github.com/pingcap/tidb/util/collate" "github.com/pingcap/tidb/util/kvcache" @@ -9761,6 +9762,192 @@ OR Variable_name = 'license' OR Variable_name = 'init_connect'`).Rows(), HasLen, } +func (s *testIntegrationSuite) TestBuiltinFuncJSONMergePatch_InColumn(c *C) { + ctx := context.Background() + tk := testkit.NewTestKit(c, s.store) + defer s.cleanEnv(c) + tests := []struct { + input [2]interface{} + expected interface{} + success bool + errCode int + }{ + // RFC 7396 document: https://datatracker.ietf.org/doc/html/rfc7396 + // RFC 7396 Example Test Cases + {[2]interface{}{`{"a":"b"}`, `{"a":"c"}`}, `{"a": "c"}`, true, 0}, + {[2]interface{}{`{"a":"b"}`, `{"b":"c"}`}, `{"a": "b", "b": "c"}`, true, 0}, + {[2]interface{}{`{"a":"b"}`, `{"a":null}`}, `{}`, true, 0}, + {[2]interface{}{`{"a":"b", "b":"c"}`, `{"a":null}`}, `{"b": "c"}`, true, 0}, + {[2]interface{}{`{"a":["b"]}`, `{"a":"c"}`}, `{"a": "c"}`, true, 0}, + {[2]interface{}{`{"a":"c"}`, `{"a":["b"]}`}, `{"a": ["b"]}`, true, 0}, + {[2]interface{}{`{"a":{"b":"c"}}`, `{"a":{"b":"d","c":null}}`}, `{"a": {"b": "d"}}`, true, 0}, + {[2]interface{}{`{"a":[{"b":"c"}]}`, `{"a": [1]}`}, `{"a": [1]}`, true, 0}, + {[2]interface{}{`["a","b"]`, `["c","d"]`}, `["c", "d"]`, true, 0}, + {[2]interface{}{`{"a":"b"}`, `["c"]`}, `["c"]`, true, 0}, + {[2]interface{}{`{"a":"foo"}`, `null`}, `null`, true, 0}, + {[2]interface{}{`{"a":"foo"}`, `"bar"`}, `"bar"`, true, 0}, + {[2]interface{}{`{"e":null}`, `{"a":1}`}, `{"e": null, "a": 1}`, true, 0}, + {[2]interface{}{`[1,2]`, `{"a":"b","c":null}`}, `{"a": "b"}`, true, 0}, + {[2]interface{}{`{}`, `{"a":{"bb":{"ccc":null}}}`}, `{"a": {"bb": {}}}`, true, 0}, + // RFC 7396 Example Document + {[2]interface{}{`{"title":"Goodbye!","author":{"givenName":"John","familyName":"Doe"},"tags":["example","sample"],"content":"This will be unchanged"}`, `{"title":"Hello!","phoneNumber":"+01-123-456-7890","author":{"familyName":null},"tags":["example"]}`}, `{"title":"Hello!","author":{"givenName":"John"},"tags":["example"],"content":"This will be unchanged","phoneNumber":"+01-123-456-7890"}`, true, 0}, + + // From mysql Example Test Cases + {[2]interface{}{nil, `{"a":1}`}, nil, true, 0}, + {[2]interface{}{`{"a":1}`, nil}, nil, true, 0}, + {[2]interface{}{`{"a":"foo"}`, `true`}, `true`, true, 0}, + {[2]interface{}{`{"a":"foo"}`, `false`}, `false`, true, 0}, + {[2]interface{}{`{"a":"foo"}`, `123`}, `123`, true, 0}, + {[2]interface{}{`{"a":"foo"}`, `123.1`}, `123.1`, true, 0}, + {[2]interface{}{`{"a":"foo"}`, `[1,2,3]`}, `[1,2,3]`, true, 0}, + {[2]interface{}{"null", `{"a":1}`}, `{"a":1}`, true, 0}, + {[2]interface{}{`{"a":1}`, "null"}, `null`, true, 0}, + + // Invalid json text + {[2]interface{}{`{"a":1}`, `[1]}`}, nil, false, mysql.ErrInvalidJSONText}, + } + + tk.MustExec(`use test;`) + tk.MustExec(`drop table if exists t;`) + tk.MustExec("CREATE TABLE t ( `id` INT NOT NULL AUTO_INCREMENT, `j` json NULL, `vc` VARCHAR ( 5000 ) NULL, PRIMARY KEY ( `id` ) );") + for id, t := range tests { + tk.MustExec("insert into t values(?,?,?)", id+1, t.input[0], t.input[1]) + if t.success { + result := tk.MustQuery("select json_merge_patch(j,vc) from t where id = ?", id+1) + if t.expected == nil { + result.Check(testkit.Rows("")) + } else { + j, e := json.ParseBinaryFromString(t.expected.(string)) + c.Assert(e, IsNil) + result.Check(testkit.Rows(j.String())) + } + } else { + rs, _ := tk.Exec("select json_merge_patch(j,vc) from t where id = ?;", id+1) + _, err := session.GetRows4Test(ctx, tk.Se, rs) + terr := errors.Cause(err).(*terror.Error) + c.Assert(terr.Code(), Equals, errors.ErrCode(t.errCode)) + } + } +} + +func (s *testIntegrationSuite) TestBuiltinFuncJSONMergePatch_InExpression(c *C) { + ctx := context.Background() + tk := testkit.NewTestKit(c, s.store) + defer s.cleanEnv(c) + tests := []struct { + input []interface{} + expected interface{} + success bool + errCode int + }{ + // RFC 7396 document: https://datatracker.ietf.org/doc/html/rfc7396 + // RFC 7396 Example Test Cases + {[]interface{}{`{"a":"b"}`, `{"a":"c"}`}, `{"a": "c"}`, true, 0}, + {[]interface{}{`{"a":"b"}`, `{"b":"c"}`}, `{"a": "b","b": "c"}`, true, 0}, + {[]interface{}{`{"a":"b"}`, `{"a":null}`}, `{}`, true, 0}, + {[]interface{}{`{"a":"b", "b":"c"}`, `{"a":null}`}, `{"b": "c"}`, true, 0}, + {[]interface{}{`{"a":["b"]}`, `{"a":"c"}`}, `{"a": "c"}`, true, 0}, + {[]interface{}{`{"a":"c"}`, `{"a":["b"]}`}, `{"a": ["b"]}`, true, 0}, + {[]interface{}{`{"a":{"b":"c"}}`, `{"a":{"b":"d","c":null}}`}, `{"a": {"b": "d"}}`, true, 0}, + {[]interface{}{`{"a":[{"b":"c"}]}`, `{"a": [1]}`}, `{"a": [1]}`, true, 0}, + {[]interface{}{`["a","b"]`, `["c","d"]`}, `["c", "d"]`, true, 0}, + {[]interface{}{`{"a":"b"}`, `["c"]`}, `["c"]`, true, 0}, + {[]interface{}{`{"a":"foo"}`, `null`}, `null`, true, 0}, + {[]interface{}{`{"a":"foo"}`, `"bar"`}, `"bar"`, true, 0}, + {[]interface{}{`{"e":null}`, `{"a":1}`}, `{"e": null,"a": 1}`, true, 0}, + {[]interface{}{`[1,2]`, `{"a":"b","c":null}`}, `{"a":"b"}`, true, 0}, + {[]interface{}{`{}`, `{"a":{"bb":{"ccc":null}}}`}, `{"a":{"bb": {}}}`, true, 0}, + // RFC 7396 Example Document + {[]interface{}{`{"title":"Goodbye!","author":{"givenName":"John","familyName":"Doe"},"tags":["example","sample"],"content":"This will be unchanged"}`, `{"title":"Hello!","phoneNumber":"+01-123-456-7890","author":{"familyName":null},"tags":["example"]}`}, `{"title":"Hello!","author":{"givenName":"John"},"tags":["example"],"content":"This will be unchanged","phoneNumber":"+01-123-456-7890"}`, true, 0}, + + // test cases + {[]interface{}{nil, `1`}, `1`, true, 0}, + {[]interface{}{`1`, nil}, nil, true, 0}, + {[]interface{}{nil, `null`}, `null`, true, 0}, + {[]interface{}{`null`, nil}, nil, true, 0}, + {[]interface{}{nil, `true`}, `true`, true, 0}, + {[]interface{}{`true`, nil}, nil, true, 0}, + {[]interface{}{nil, `false`}, `false`, true, 0}, + {[]interface{}{`false`, nil}, nil, true, 0}, + {[]interface{}{nil, `[1,2,3]`}, `[1,2,3]`, true, 0}, + {[]interface{}{`[1,2,3]`, nil}, nil, true, 0}, + {[]interface{}{nil, `{"a":"foo"}`}, nil, true, 0}, + {[]interface{}{`{"a":"foo"}`, nil}, nil, true, 0}, + + {[]interface{}{`{"a":"foo"}`, `{"a":null}`, `{"b":"123"}`, `{"c":1}`}, `{"b":"123","c":1}`, true, 0}, + {[]interface{}{`{"a":"foo"}`, `{"a":null}`, `{"c":1}`}, `{"c":1}`, true, 0}, + {[]interface{}{`{"a":"foo"}`, `{"a":null}`, `true`}, `true`, true, 0}, + {[]interface{}{`{"a":"foo"}`, `{"d":1}`, `{"a":{"bb":{"ccc":null}}}`}, `{"a":{"bb":{}},"d":1}`, true, 0}, + {[]interface{}{`null`, `true`, `[1,2,3]`}, `[1,2,3]`, true, 0}, + + // From mysql Example Test Cases + {[]interface{}{nil, `null`, `[1,2,3]`, `{"a":1}`}, `{"a": 1}`, true, 0}, + {[]interface{}{`null`, nil, `[1,2,3]`, `{"a":1}`}, `{"a": 1}`, true, 0}, + {[]interface{}{`null`, `[1,2,3]`, nil, `{"a":1}`}, nil, true, 0}, + {[]interface{}{`null`, `[1,2,3]`, `{"a":1}`, nil}, nil, true, 0}, + + {[]interface{}{nil, `null`, `{"a":1}`, `[1,2,3]`}, `[1,2,3]`, true, 0}, + {[]interface{}{`null`, nil, `{"a":1}`, `[1,2,3]`}, `[1,2,3]`, true, 0}, + {[]interface{}{`null`, `{"a":1}`, nil, `[1,2,3]`}, `[1,2,3]`, true, 0}, + {[]interface{}{`null`, `{"a":1}`, `[1,2,3]`, nil}, nil, true, 0}, + + {[]interface{}{nil, `null`, `{"a":1}`, `true`}, `true`, true, 0}, + {[]interface{}{`null`, nil, `{"a":1}`, `true`}, `true`, true, 0}, + {[]interface{}{`null`, `{"a":1}`, nil, `true`}, `true`, true, 0}, + {[]interface{}{`null`, `{"a":1}`, `true`, nil}, nil, true, 0}, + + // non-object last item + {[]interface{}{"true", "false", "[]", "{}", "null"}, "null", true, 0}, + {[]interface{}{"false", "[]", "{}", "null", "true"}, "true", true, 0}, + {[]interface{}{"true", "[]", "{}", "null", "false"}, "false", true, 0}, + {[]interface{}{"true", "false", "{}", "null", "[]"}, "[]", true, 0}, + {[]interface{}{"true", "false", "{}", "null", "1"}, "1", true, 0}, + {[]interface{}{"true", "false", "{}", "null", "1.8"}, "1.8", true, 0}, + {[]interface{}{"true", "false", "{}", "null", `"112"`}, `"112"`, true, 0}, + + {[]interface{}{`{"a":"foo"}`, nil}, nil, true, 0}, + {[]interface{}{nil, `{"a":"foo"}`}, nil, true, 0}, + {[]interface{}{`{"a":"foo"}`, `false`}, `false`, true, 0}, + {[]interface{}{`{"a":"foo"}`, `123`}, `123`, true, 0}, + {[]interface{}{`{"a":"foo"}`, `123.1`}, `123.1`, true, 0}, + {[]interface{}{`{"a":"foo"}`, `[1,2,3]`}, `[1,2,3]`, true, 0}, + {[]interface{}{`null`, `{"a":1}`}, `{"a":1}`, true, 0}, + {[]interface{}{`{"a":1}`, `null`}, `null`, true, 0}, + {[]interface{}{`{"a":"foo"}`, `{"a":null}`, `{"b":"123"}`, `{"c":1}`}, `{"b":"123","c":1}`, true, 0}, + {[]interface{}{`{"a":"foo"}`, `{"a":null}`, `{"c":1}`}, `{"c":1}`, true, 0}, + {[]interface{}{`{"a":"foo"}`, `{"a":null}`, `true`}, `true`, true, 0}, + {[]interface{}{`{"a":"foo"}`, `{"d":1}`, `{"a":{"bb":{"ccc":null}}}`}, `{"a":{"bb":{}},"d":1}`, true, 0}, + + // Invalid json text + {[]interface{}{`{"a":1}`, `[1]}`}, nil, false, mysql.ErrInvalidJSONText}, + {[]interface{}{`{{"a":1}`, `[1]`, `null`}, nil, false, mysql.ErrInvalidJSONText}, + {[]interface{}{`{"a":1}`, `jjj`, `null`}, nil, false, mysql.ErrInvalidJSONText}, + } + + for _, t := range tests { + marks := make([]string, len(t.input)) + for i := 0; i < len(marks); i++ { + marks[i] = "?" + } + sql := fmt.Sprintf("select json_merge_patch(%s);", strings.Join(marks, ",")) + if t.success { + result := tk.MustQuery(sql, t.input...) + if t.expected == nil { + result.Check(testkit.Rows("")) + } else { + j, e := json.ParseBinaryFromString(t.expected.(string)) + c.Assert(e, IsNil) + result.Check(testkit.Rows(j.String())) + } + } else { + rs, _ := tk.Exec(sql, t.input...) + _, err := session.GetRows4Test(ctx, tk.Se, rs) + terr := errors.Cause(err).(*terror.Error) + c.Assert(terr.Code(), Equals, errors.ErrCode(t.errCode)) + } + } +} + func (s *testIntegrationSuite) TestFloat64Inf(c *C) { defer s.cleanEnv(c) tk := testkit.NewTestKit(c, s.store) diff --git a/types/json/binary_functions.go b/types/json/binary_functions.go index 8f6b0a626be10..3e587bec08c85 100644 --- a/types/json/binary_functions.go +++ b/types/json/binary_functions.go @@ -781,6 +781,96 @@ func CompareBinary(left, right BinaryJSON) int { return cmp } +// MergePatchBinary implements RFC7396 +// https://datatracker.ietf.org/doc/html/rfc7396 +func MergePatchBinary(bjs []*BinaryJSON) (*BinaryJSON, error) { + var err error + length := len(bjs) + + // according to the implements of RFC7396 + // when the last item is not object + // we can return the last item directly + for i := length - 1; i >= 0; i-- { + if bjs[i] == nil || bjs[i].TypeCode != TypeCodeObject { + bjs = bjs[i:] + break + } + } + + target := bjs[0] + for _, patch := range bjs[1:] { + target, err = mergePatchBinary(target, patch) + if err != nil { + return nil, err + } + } + return target, nil +} + +func mergePatchBinary(target, patch *BinaryJSON) (result *BinaryJSON, err error) { + if patch == nil { + return nil, nil + } + + if patch.TypeCode == TypeCodeObject { + if target == nil { + return nil, nil + } + + keyValMap := make(map[string]BinaryJSON) + if target.TypeCode == TypeCodeObject { + elemCount := target.GetElemCount() + for i := 0; i < elemCount; i++ { + key := target.objectGetKey(i) + val := target.objectGetVal(i) + keyValMap[string(key)] = val + } + } + var tmp *BinaryJSON + elemCount := patch.GetElemCount() + for i := 0; i < elemCount; i++ { + key := patch.objectGetKey(i) + val := patch.objectGetVal(i) + k := string(key) + + targetKV, exists := keyValMap[k] + if val.TypeCode == TypeCodeLiteral && val.Value[0] == LiteralNil { + if exists { + delete(keyValMap, k) + } + } else { + tmp, err = mergePatchBinary(&targetKV, &val) + if err != nil { + return result, err + } + + keyValMap[k] = *tmp + } + } + + length := len(keyValMap) + keys := make([][]byte, 0, length) + for key := range keyValMap { + keys = append(keys, []byte(key)) + } + sort.Slice(keys, func(i, j int) bool { + return bytes.Compare(keys[i], keys[j]) < 0 + }) + length = len(keys) + values := make([]BinaryJSON, 0, len(keys)) + for i := 0; i < length; i++ { + values = append(values, keyValMap[string(keys[i])]) + } + + binaryObject, e := buildBinaryObject(keys, values) + if e != nil { + return nil, e + } + return &binaryObject, nil + } + return patch, nil +} + // MergeBinary merges multiple BinaryJSON into one according the following rules: // 1) adjacent arrays are merged to a single array; // 2) adjacent object are merged to a single object; diff --git a/types/json/binary_functions_test.go b/types/json/binary_functions_test.go index cecda34ea468f..c5da4701d850a 100644 --- a/types/json/binary_functions_test.go +++ b/types/json/binary_functions_test.go @@ -37,3 +37,19 @@ func BenchmarkDecodeEscapedUnicode(b *testing.B) { _, _, _ = decodeEscapedUnicode([]byte(in)) } } + +func BenchmarkMergePatchBinary(b *testing.B) { + valueA, _ := ParseBinaryFromString(`{"title":"Goodbye!","author":{"givenName":"John","familyName":"Doe"},"tags":["example","sample"],"content":"This will be unchanged"}`) + valueB, _ := ParseBinaryFromString(`{"title":"Hello!","phoneNumber":"+01-123-456-7890","author":{"familyName":null},"tags":["example"]}`) + for i := 0; i < b.N; i++ { + _, _ = MergePatchBinary([]*BinaryJSON{&valueA, &valueB}) + } +} + +func BenchmarkMergeBinary(b *testing.B) { + valueA, _ := ParseBinaryFromString(`{"title":"Goodbye!","author":{"givenName":"John","familyName":"Doe"},"tags":["example","sample"],"content":"This will be unchanged"}`) + valueB, _ := ParseBinaryFromString(`{"title":"Hello!","phoneNumber":"+01-123-456-7890","author":{"familyName":null},"tags":["example"]}`) + for i := 0; i < b.N; i++ { + _ = MergeBinary([]BinaryJSON{valueA, valueB}) + } +}