Skip to content
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
31 changes: 31 additions & 0 deletions enginetest/queries/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -1727,6 +1727,33 @@ Select * from (
{true},
},
},
{
Query: `SELECT JSON_MERGE(c3, '{"a": 1}') FROM jsontable`,
Expected: []sql.Row{
{types.MustJSON(`{"a": [2, 1]}`)},
{types.MustJSON(`{"a": 1, "b": 2}`)},
{types.MustJSON(`{"a": 1, "c": 2}`)},
{types.MustJSON(`{"a": 1, "d": 2}`)},
},
},
{
Query: `SELECT JSON_MERGE_PRESERVE(c3, '{"a": 1}') FROM jsontable`,
Expected: []sql.Row{
{types.MustJSON(`{"a": [2, 1]}`)},
{types.MustJSON(`{"a": 1, "b": 2}`)},
{types.MustJSON(`{"a": 1, "c": 2}`)},
{types.MustJSON(`{"a": 1, "d": 2}`)},
},
},
{
Query: `SELECT JSON_MERGE_PATCH(c3, '{"a": 1}') FROM jsontable`,
Expected: []sql.Row{
{types.MustJSON(`{"a": 1}`)},
{types.MustJSON(`{"a": 1, "b": 2}`)},
{types.MustJSON(`{"a": 1, "c": 2}`)},
{types.MustJSON(`{"a": 1, "d": 2}`)},
},
},
{
Query: `SELECT CONCAT(JSON_OBJECT('aa', JSON_OBJECT('bb', 123, 'y', 456), 'z', JSON_OBJECT('cc', 321, 'x', 654)), "")`,
Expected: []sql.Row{
Expand Down Expand Up @@ -10428,6 +10455,10 @@ var ErrorQueries = []QueryErrorTest{
Query: "SELECT json_insert() FROM dual;",
ExpectedErr: sql.ErrInvalidArgumentNumber,
},
{
Query: "SELECT json_merge() FROM dual;",
ExpectedErr: sql.ErrInvalidArgumentNumber,
},
{
Query: "SELECT json_merge_preserve() FROM dual;",
ExpectedErr: sql.ErrInvalidArgumentNumber,
Expand Down
4 changes: 2 additions & 2 deletions sql/expression/function/json/json_depth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,9 @@ func TestJSONDepth(t *testing.T) {
result, err := tt.f.Eval(sql.NewEmptyContext(), tt.row)
if tt.err {
require.Error(err)
} else {
require.NoError(err)
return
}
require.NoError(err)
require.Equal(tt.exp, result)
})
}
Expand Down
142 changes: 142 additions & 0 deletions sql/expression/function/json/json_merge_patch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright 2024 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package json

import (
"fmt"
"strings"

"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/types"
)

// JSONMergePatch (json_doc, json_doc[, json_doc] ...)
//
// JSONMergePatch Performs an RFC 7396 compliant merge of two or more JSON documents and returns the merged result,
// without preserving members having duplicate keys. Raises an error if at least one of the documents passed as arguments
// to this function is not valid. JSONMergePatch performs a merge as follows:
// - If the first argument is not an object, the result of the merge is the same as if an empty object had been merged
// with the second argument.
// - If the second argument is not an object, the result of the merge is the second argument.
// - If both arguments are objects, the result of the merge is an object with the following members:
// - All members of the first object which do not have a corresponding member with the same key in the second
// object.
// - All members of the second object which do not have a corresponding key in the first object, and whose value is
// not the JSON null literal.
// - All members with a key that exists in both the first and the second object, and whose value in the second
// object is not the JSON null literal. The values of these members are the results of recursively merging the
// value in the first object with the value in the second object.
//
// The behavior of JSONMergePatch is the same as that of JSONMergePreserve, with the following two exceptions:
// - JSONMergePatch removes any member in the first object with a matching key in the second object, provided that
// the value associated with the key in the second object is not JSON null.
// - If the second object has a member with a key matching a member in the first object, JSONMergePatch replaces
// the value in the first object with the value in the second object, whereas JSONMergePreserve appends the
// second value to the first value.
//
// https://dev.mysql.com/doc/refman/8.0/en/json-modification-functions.html#function_json-merge-patch
type JSONMergePatch struct {
JSONs []sql.Expression
}

var _ sql.FunctionExpression = &JSONMergePatch{}

// NewJSONMergePatch creates a new JSONMergePatch function.
func NewJSONMergePatch(args ...sql.Expression) (sql.Expression, error) {
if len(args) < 2 {
return nil, sql.ErrInvalidArgumentNumber.New("JSON_MERGE_PATCH", 2, len(args))
}
return &JSONMergePatch{JSONs: args}, nil
}

// FunctionName implements sql.FunctionExpression
func (j *JSONMergePatch) FunctionName() string {
return "json_merge_patch"
}

// Description implements sql.FunctionExpression
func (j *JSONMergePatch) Description() string {
return "merges JSON documents, replacing values of duplicate keys"
}

// Resolved implements sql.Expression
func (j *JSONMergePatch) Resolved() bool {
for _, arg := range j.JSONs {
if !arg.Resolved() {
return false
}
}
return true
}

// String implements sql.Expression
func (j *JSONMergePatch) String() string {
children := j.Children()
var parts = make([]string, len(children))
for i, c := range children {
parts[i] = c.String()
}
return fmt.Sprintf("%s(%s)", j.FunctionName(), strings.Join(parts, ","))
}

// Type implements the Expression interface.
func (j *JSONMergePatch) Type() sql.Type {
return types.JSON
}

// IsNullable implements the Expression interface.
func (j *JSONMergePatch) IsNullable() bool {
for _, arg := range j.JSONs {
if arg.IsNullable() {
return true
}
}
return false
}

// Eval implements the Expression interface.
func (j *JSONMergePatch) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
initDoc, err := getJSONDocumentFromRow(ctx, row, j.JSONs[0])
if err != nil {
return nil, err
}
if initDoc == nil {
return nil, nil
}

result := types.DeepCopyJson(initDoc.Val)
for _, json := range j.JSONs[1:] {
var doc *types.JSONDocument
doc, err = getJSONDocumentFromRow(ctx, row, json)
if err != nil {
return nil, err
}
if doc == nil {
return nil, nil
}
result = merge(result, doc.Val, true)
}
return types.JSONDocument{Val: result}, nil
}

// Children implements the Expression interface.
func (j *JSONMergePatch) Children() []sql.Expression {
return j.JSONs
}

// WithChildren implements the Expression interface.
func (j *JSONMergePatch) WithChildren(children ...sql.Expression) (sql.Expression, error) {
return NewJSONMergePatch(children...)
}
151 changes: 151 additions & 0 deletions sql/expression/function/json/json_merge_patch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright 2024 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package json

import (
"fmt"
"strings"
"testing"

"github.com/stretchr/testify/require"

"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/types"
)

func TestJSONMergePatch(t *testing.T) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only test case I noticed missing was an object with a key with a null value. Would be good to test the key deletion behavior for json_merge_patch and probably also worth added a test for json_merge_preserve just to assert that keys with null values aren't deleted there.

f2 := buildGetFieldExpressions(t, NewJSONMergePatch, 2)
f3 := buildGetFieldExpressions(t, NewJSONMergePatch, 3)
testCases := []struct {
f sql.Expression
row sql.Row
exp interface{}
err bool
}{
{
f: f2,
row: sql.Row{nil, nil},
exp: nil,
},
{
f: f2,
row: sql.Row{`null`, `null`},
exp: types.MustJSON(`null`),
},
{
f: f2,
row: sql.Row{`1`, `true`},
exp: types.MustJSON(`true`),
},
{
f: f2,
row: sql.Row{`"abc"`, `"def"`},
exp: types.MustJSON(`"def"`),
},
{
f: f2,
row: sql.Row{`[1, 2]`, `null`},
exp: types.MustJSON(`null`),
},
{
f: f2,
row: sql.Row{`[1, 2]`, `{"id": 47}`},
exp: types.MustJSON(`{"id": 47}`),
},
{
f: f2,
row: sql.Row{`[1, 2]`, `[true, false]`},
exp: types.MustJSON(`[true, false]`),
},
{
f: f2,
row: sql.Row{`{"name": "x"}`, `{"id": 47}`},
exp: types.MustJSON(`{"id": 47, "name": "x"}`),
},
{
f: f2,
row: sql.Row{`{"id": 123}`, `{"id": null}`},
exp: types.MustJSON(`{}`),
},
{
f: f2,
row: sql.Row{
`{
"Suspect": {
"Name": "Bart",
"Hobbies": ["Skateboarding", "Mischief"]
},
"Victim": "Lisa",
"Case": {
"Id": 33845,
"Date": "2006-01-02T15:04:05-07:00",
"Closed": true
}
}`,
`{
"Suspect": {
"Age": 10,
"Parents": ["Marge", "Homer"],
"Hobbies": ["Trouble"]
},
"Witnesses": ["Maggie", "Ned"]
}`,
},
exp: types.MustJSON(
`{
"Case": {
"Id": 33845,
"Date": "2006-01-02T15:04:05-07:00",
"Closed": true
},
"Victim": "Lisa",
"Suspect": {
"Age": 10,
"Name": "Bart",
"Hobbies": ["Trouble"],
"Parents": ["Marge", "Homer"]
},
"Witnesses": ["Maggie", "Ned"]
}`),
},
{
f: f3,
row: sql.Row{`{"a": 1, "b": 2}`, `{"a": 3, "c": 4}`, `{"a": 5, "d": 6}`},
exp: types.MustJSON(`{"a": 5, "b": 2, "c": 4, "d": 6}`),
},
{
f: f3,
row: sql.Row{`{"a": 1, "b": 2}`, `{"a": {"one": false, "two": 2.55, "e": 8}}`, `"single value"`},
exp: types.MustJSON(`"single value"`),
},
}

for _, tt := range testCases {
var args []string
for _, a := range tt.row {
args = append(args, fmt.Sprintf("%v", a))
}
t.Run(strings.Join(args, ", "), func(t *testing.T) {
require := require.New(t)
res, err := tt.f.Eval(sql.NewEmptyContext(), tt.row)
if tt.err {
require.Error(err)
return
}
require.NoError(err)
require.Equal(tt.exp, res)
})
}
}
Loading