Skip to content

Commit 3b60300

Browse files
authored
JSON Array Mutation support added.
Specific functions now supported: JSON_ARRAY_APPEND() and JSON_ARRAY_INSERT().
2 parents d02332f + 0ae382b commit 3b60300

File tree

8 files changed

+780
-107
lines changed

8 files changed

+780
-107
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright 2023 Dolthub, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package json
16+
17+
import (
18+
"fmt"
19+
"strings"
20+
21+
"github.com/dolthub/go-mysql-server/sql"
22+
"github.com/dolthub/go-mysql-server/sql/types"
23+
)
24+
25+
// JSON_ARRAY_APPEND(json_doc, path, val[, path, val] ...)
26+
//
27+
// JSONArrayAppend Appends values to the end of the indicated arrays within a JSON document and returns the result.
28+
// Returns NULL if any argument is NULL. An error occurs if the json_doc argument is not a valid JSON document or any
29+
// path argument is not a valid path expression or contains a * or ** wildcard. The path-value pairs are evaluated left
30+
// to right. The document produced by evaluating one pair becomes the new value against which the next pair is
31+
// evaluated. If a path selects a scalar or object value, that value is autowrapped within an array and the new value is
32+
// added to that array. Pairs for which the path does not identify any value in the JSON document are ignored.
33+
//
34+
// https://dev.mysql.com/doc/refman/8.0/en/json-modification-functions.html#function_json-array-append
35+
type JSONArrayAppend struct {
36+
doc sql.Expression
37+
pathVals []sql.Expression
38+
}
39+
40+
func (j JSONArrayAppend) Resolved() bool {
41+
for _, child := range j.Children() {
42+
if child != nil && !child.Resolved() {
43+
return false
44+
}
45+
}
46+
return true
47+
}
48+
49+
func (j JSONArrayAppend) String() string {
50+
children := j.Children()
51+
var parts = make([]string, len(children))
52+
53+
for i, c := range children {
54+
parts[i] = c.String()
55+
}
56+
57+
return fmt.Sprintf("%s(%s)", j.FunctionName(), strings.Join(parts, ","))
58+
}
59+
60+
func (j JSONArrayAppend) Type() sql.Type {
61+
return types.JSON
62+
}
63+
64+
func (j JSONArrayAppend) IsNullable() bool {
65+
for _, arg := range j.pathVals {
66+
if arg.IsNullable() {
67+
return true
68+
}
69+
}
70+
return j.doc.IsNullable()
71+
}
72+
73+
func (j JSONArrayAppend) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
74+
doc, err := getMutableJSONVal(ctx, row, j.doc)
75+
if err != nil || doc == nil {
76+
return nil, err
77+
}
78+
79+
pairs := make([]pathValPair, 0, len(j.pathVals)/2)
80+
for i := 0; i < len(j.pathVals); i += 2 {
81+
argPair, err := buildPathValue(ctx, j.pathVals[i], j.pathVals[i+1], row)
82+
if argPair == nil || err != nil {
83+
return nil, err
84+
}
85+
pairs = append(pairs, *argPair)
86+
}
87+
88+
// Apply the path-value pairs to the document.
89+
for _, pair := range pairs {
90+
doc, _, err = doc.ArrayAppend(ctx, pair.path, pair.val)
91+
if err != nil {
92+
return nil, err
93+
}
94+
}
95+
96+
return doc, nil
97+
}
98+
99+
func (j JSONArrayAppend) Children() []sql.Expression {
100+
return append([]sql.Expression{j.doc}, j.pathVals...)
101+
}
102+
103+
func (j JSONArrayAppend) WithChildren(children ...sql.Expression) (sql.Expression, error) {
104+
if len(j.Children()) != len(children) {
105+
return nil, fmt.Errorf("json_array_append did not receive the correct number of args")
106+
}
107+
return NewJSONArrayAppend(children...)
108+
}
109+
110+
var _ sql.FunctionExpression = JSONArrayAppend{}
111+
112+
// NewJSONArrayAppend creates a new JSONArrayAppend function.
113+
func NewJSONArrayAppend(args ...sql.Expression) (sql.Expression, error) {
114+
if len(args) <= 1 {
115+
return nil, sql.ErrInvalidArgumentNumber.New("JSON_ARRAY_APPEND", "more than 1", len(args))
116+
} else if (len(args)-1)%2 == 1 {
117+
return nil, sql.ErrInvalidArgumentNumber.New("JSON_ARRAY_APPEND", "even number of path/val", len(args)-1)
118+
}
119+
120+
return JSONArrayAppend{args[0], args[1:]}, nil
121+
}
122+
123+
// FunctionName implements sql.FunctionExpression
124+
func (j JSONArrayAppend) FunctionName() string {
125+
return "json_array_append"
126+
}
127+
128+
// Description implements sql.FunctionExpression
129+
func (j JSONArrayAppend) Description() string {
130+
return "appends data to JSON document."
131+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright 2023 Dolthub, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package json
16+
17+
import (
18+
"fmt"
19+
"strings"
20+
"testing"
21+
22+
"github.com/stretchr/testify/require"
23+
"gopkg.in/src-d/go-errors.v1"
24+
25+
"github.com/dolthub/go-mysql-server/sql"
26+
"github.com/dolthub/go-mysql-server/sql/types"
27+
)
28+
29+
func TestArrayAppend(t *testing.T) {
30+
_, err := NewJSONArrayInsert()
31+
require.True(t, errors.Is(err, sql.ErrInvalidArgumentNumber))
32+
33+
f1 := buildGetFieldExpressions(t, NewJSONArrayAppend, 3)
34+
f2 := buildGetFieldExpressions(t, NewJSONArrayAppend, 5)
35+
36+
json := `{"a": 1, "b": [2, 3], "c": {"d": "foo"}}`
37+
38+
testCases := []struct {
39+
f sql.Expression
40+
row sql.Row
41+
expected interface{}
42+
err error
43+
}{
44+
45+
{f1, sql.Row{json, "$.b[0]", 4.1}, `{"a": 1, "b": [[2,4.1], 3], "c": {"d": "foo"}}`, nil},
46+
{f1, sql.Row{json, "$.a", 4.1}, `{"a": [1, 4.1], "b": [2, 3], "c": {"d": "foo"}}`, nil},
47+
{f1, sql.Row{json, "$.e", "new"}, json, nil},
48+
{f1, sql.Row{json, "$.c.d", "test"}, `{"a": 1, "b": [2, 3], "c": {"d": ["foo", "test"]}}`, nil},
49+
{f2, sql.Row{json, "$.b[0]", 4.1, "$.c.d", "test"}, `{"a": 1, "b": [[2, 4.1], 3], "c": {"d": ["foo", "test"]}}`, nil},
50+
{f1, sql.Row{json, "$.b[5]", 4.1}, json, nil},
51+
{f1, sql.Row{json, "$.b.c", 4}, json, nil},
52+
{f1, sql.Row{json, "$.a[51]", 4.1}, json, nil},
53+
{f1, sql.Row{json, "$.a[last-1]", 4.1}, json, nil},
54+
{f1, sql.Row{json, "$.a[0]", 4.1}, `{"a": [1, 4.1], "b": [2, 3], "c": {"d": "foo"}}`, nil},
55+
{f1, sql.Row{json, "$.a[last]", 4.1}, `{"a": [1, 4.1], "b": [2, 3], "c": {"d": "foo"}}`, nil},
56+
{f1, sql.Row{json, "$[0]", 4.1}, `[{"a": 1, "b": [2, 3], "c": {"d": "foo"}}, 4.1]`, nil},
57+
{f1, sql.Row{json, "$.[0]", 4.1}, nil, fmt.Errorf("Invalid JSON path expression")},
58+
{f1, sql.Row{json, "foo", "test"}, nil, fmt.Errorf("Invalid JSON path expression")},
59+
{f1, sql.Row{json, "$.c.*", "test"}, nil, fmt.Errorf("Path expressions may not contain the * and ** tokens")},
60+
{f1, sql.Row{json, "$.c.**", "test"}, nil, fmt.Errorf("Path expressions may not contain the * and ** tokens")},
61+
{f1, sql.Row{json, "$", 10.1}, `[{"a": 1, "b": [2, 3], "c": {"d": "foo"}}, 10.1]`, nil},
62+
{f1, sql.Row{nil, "$", 42.7}, nil, nil},
63+
{f1, sql.Row{json, nil, 10}, nil, nil},
64+
65+
// mysql> select JSON_ARRAY_APPEND(JSON_ARRAY(1,2,3), "$[1]", 51, "$[1]", 52, "$[1]", 53);
66+
// +--------------------------------------------------------------------------+
67+
// | JSON_ARRAY_APPEND(JSON_ARRAY(1,2,3), "$[1]", 51, "$[1]", 52, "$[1]", 53) |
68+
// +--------------------------------------------------------------------------+
69+
// | [1, [2, 51, 52, 53], 3] |
70+
// +--------------------------------------------------------------------------+
71+
{buildGetFieldExpressions(t, NewJSONArrayAppend, 7),
72+
sql.Row{`[1.0,2.0,3.0]`,
73+
"$[1]", 51.0, // [1, 2, 3] -> [1, [2, 51], 3]
74+
"$[1]", 52.0, // [1, [2, 51], 2, 3] -> [1, [2, 51, 52] 3]
75+
"$[1]", 53.0, // [1, [2, 51, 52], 3] -> [1, [2, 51, 52, 53], 3]
76+
},
77+
`[1,[2, 51, 52, 53], 3]`, nil},
78+
}
79+
80+
for _, tstC := range testCases {
81+
var paths []string
82+
for _, path := range tstC.row[1:] {
83+
if _, ok := path.(string); ok {
84+
paths = append(paths, path.(string))
85+
}
86+
}
87+
88+
t.Run(tstC.f.String()+"."+strings.Join(paths, ","), func(t *testing.T) {
89+
req := require.New(t)
90+
result, err := tstC.f.Eval(sql.NewEmptyContext(), tstC.row)
91+
if tstC.err == nil {
92+
req.NoError(err)
93+
94+
var expect interface{}
95+
if tstC.expected != nil {
96+
expect, _, err = types.JSON.Convert(tstC.expected)
97+
if err != nil {
98+
panic("Bad test string. Can't convert string to JSONDocument: " + tstC.expected.(string))
99+
}
100+
}
101+
102+
req.Equal(expect, result)
103+
} else {
104+
req.Nil(result)
105+
req.Error(tstC.err, err)
106+
}
107+
})
108+
}
109+
110+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright 2023 Dolthub, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package json
16+
17+
import (
18+
"fmt"
19+
"strings"
20+
21+
"github.com/dolthub/go-mysql-server/sql"
22+
"github.com/dolthub/go-mysql-server/sql/types"
23+
)
24+
25+
// JSON_ARRAY_INSERT(json_doc, path, val[, path, val] ...)
26+
//
27+
// JSONArrayInsert Updates a JSON document, inserting into an array within the document and returning the modified
28+
// document. Returns NULL if any argument is NULL. An error occurs if the json_doc argument is not a valid JSON document
29+
// or any path argument is not a valid path expression or contains a * or ** wildcard or does not end with an array
30+
// element identifier. The path-value pairs are evaluated left to right. The document produced by evaluating one pair
31+
// becomes the new value against which the next pair is evaluated. Pairs for which the path does not identify any array
32+
// in the JSON document are ignored. If a path identifies an array element, the corresponding value is inserted at that
33+
// element position, shifting any following values to the right. If a path identifies an array position past the end of
34+
// an array, the value is inserted at the end of the array.
35+
//
36+
// https://dev.mysql.com/doc/refman/8.0/en/json-modification-functions.html#function_json-array-insert
37+
type JSONArrayInsert struct {
38+
doc sql.Expression
39+
pathVals []sql.Expression
40+
}
41+
42+
func (j JSONArrayInsert) Resolved() bool {
43+
for _, child := range j.Children() {
44+
if child != nil && !child.Resolved() {
45+
return false
46+
}
47+
}
48+
return true
49+
}
50+
51+
func (j JSONArrayInsert) String() string {
52+
children := j.Children()
53+
var parts = make([]string, len(children))
54+
55+
for i, c := range children {
56+
parts[i] = c.String()
57+
}
58+
59+
return fmt.Sprintf("%s(%s)", j.FunctionName(), strings.Join(parts, ","))
60+
}
61+
62+
func (j JSONArrayInsert) Type() sql.Type {
63+
return types.JSON
64+
}
65+
66+
func (j JSONArrayInsert) IsNullable() bool {
67+
for _, arg := range j.pathVals {
68+
if arg.IsNullable() {
69+
return true
70+
}
71+
}
72+
return j.doc.IsNullable()
73+
}
74+
75+
func (j JSONArrayInsert) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
76+
doc, err := getMutableJSONVal(ctx, row, j.doc)
77+
if err != nil || doc == nil {
78+
return nil, err
79+
}
80+
81+
pairs := make([]pathValPair, 0, len(j.pathVals)/2)
82+
for i := 0; i < len(j.pathVals); i += 2 {
83+
argPair, err := buildPathValue(ctx, j.pathVals[i], j.pathVals[i+1], row)
84+
if argPair == nil || err != nil {
85+
return nil, err
86+
}
87+
pairs = append(pairs, *argPair)
88+
}
89+
90+
// Apply the path-value pairs to the document.
91+
for _, pair := range pairs {
92+
doc, _, err = doc.ArrayInsert(ctx, pair.path, pair.val)
93+
if err != nil {
94+
return nil, err
95+
}
96+
}
97+
98+
return doc, nil
99+
}
100+
101+
func (j JSONArrayInsert) Children() []sql.Expression {
102+
return append([]sql.Expression{j.doc}, j.pathVals...)
103+
}
104+
105+
func (j JSONArrayInsert) WithChildren(children ...sql.Expression) (sql.Expression, error) {
106+
if len(j.Children()) != len(children) {
107+
return nil, fmt.Errorf("json_array_insert did not receive the correct amount of args")
108+
}
109+
return NewJSONArrayInsert(children...)
110+
}
111+
112+
var _ sql.FunctionExpression = JSONArrayInsert{}
113+
114+
// NewJSONArrayInsert creates a new JSONArrayInsert function.
115+
func NewJSONArrayInsert(args ...sql.Expression) (sql.Expression, error) {
116+
if len(args) <= 1 {
117+
return nil, sql.ErrInvalidArgumentNumber.New("JSON_ARRAY_INSERT", "more than 1", len(args))
118+
} else if (len(args)-1)%2 == 1 {
119+
return nil, sql.ErrInvalidArgumentNumber.New("JSON_ARRAY_INSERT", "even number of path/val", len(args)-1)
120+
}
121+
122+
return JSONArrayInsert{args[0], args[1:]}, nil
123+
}
124+
125+
// FunctionName implements sql.FunctionExpression
126+
func (j JSONArrayInsert) FunctionName() string {
127+
return "json_array_insert"
128+
}
129+
130+
// Description implements sql.FunctionExpression
131+
func (j JSONArrayInsert) Description() string {
132+
return "inserts into JSON array."
133+
}

0 commit comments

Comments
 (0)