Skip to content

Commit 060b45b

Browse files
authored
implement json_depth() (#2347)
1 parent 0f1cfb7 commit 060b45b

File tree

5 files changed

+309
-35
lines changed

5 files changed

+309
-35
lines changed

enginetest/queries/queries.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8386,6 +8386,13 @@ SELECT * FROM ladder;`,
83868386
{types.MustJSON(`{}`)},
83878387
},
83888388
},
8389+
{
8390+
Query: `SELECT json_depth('{"a": 1, "b": {"aa": 1, "bb": {"aaa": 1, "bbb": {"aaaa": 1}}}}') FROM dual;`,
8391+
Expected: []sql.Row{
8392+
{5},
8393+
},
8394+
},
8395+
83898396
{
83908397
Query: "SELECT i, I, s, S FROM mytable;",
83918398
Expected: []sql.Row{

server/server_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import (
1010
vsql "github.com/dolthub/vitess/go/mysql"
1111
"github.com/go-sql-driver/mysql"
1212
"github.com/stretchr/testify/require"
13+
"google.golang.org/grpc/test/bufconn"
1314

1415
sqle "github.com/dolthub/go-mysql-server"
1516
"github.com/dolthub/go-mysql-server/memory"
1617
"github.com/dolthub/go-mysql-server/server"
1718
gsql "github.com/dolthub/go-mysql-server/sql"
1819
"github.com/dolthub/go-mysql-server/sql/mysql_db"
19-
"google.golang.org/grpc/test/bufconn"
2020
)
2121

2222
// TestSeverCustomListener verifies a caller can provide their own net.Conn implementation for the server to use
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright 2024 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+
20+
"github.com/dolthub/go-mysql-server/sql"
21+
"github.com/dolthub/go-mysql-server/sql/types"
22+
)
23+
24+
// JSONDepth (json_doc)
25+
//
26+
// JSONDepth Returns the maximum depth of a JSON document. Returns NULL if the argument is NULL. An error occurs if the
27+
// argument is not a valid JSON document. An empty array, empty object, or scalar value has depth 1. A nonempty array
28+
// containing only elements of depth 1 or nonempty object containing only member values of depth 1 has depth 2.
29+
// Otherwise, a JSON document has depth greater than 2.
30+
//
31+
// https://dev.mysql.com/doc/refman/8.0/en/json-attribute-functions.html#function_json-depth
32+
type JSONDepth struct {
33+
JSON sql.Expression
34+
}
35+
36+
var _ sql.FunctionExpression = &JSONDepth{}
37+
38+
// NewJSONDepth creates a new JSONDepth function.
39+
func NewJSONDepth(args ...sql.Expression) (sql.Expression, error) {
40+
if len(args) != 1 {
41+
return nil, sql.ErrInvalidArgumentNumber.New("JSON_DEPTH", "1", len(args))
42+
}
43+
return &JSONDepth{JSON: args[0]}, nil
44+
}
45+
46+
// FunctionName implements sql.FunctionExpression interface.
47+
func (j JSONDepth) FunctionName() string {
48+
return "json_depth"
49+
}
50+
51+
// Description implements sql.FunctionExpression interface.
52+
func (j JSONDepth) Description() string {
53+
return "returns maximum depth of JSON document."
54+
}
55+
56+
// Resolved implements sql.Expression interface.
57+
func (j JSONDepth) Resolved() bool {
58+
return j.JSON.Resolved()
59+
}
60+
61+
// String implements sql.Expression interface.
62+
func (j JSONDepth) String() string {
63+
return fmt.Sprintf("%s(%s)", j.FunctionName(), j.JSON.String())
64+
}
65+
66+
// Type implements sql.Expression interface.
67+
func (j JSONDepth) Type() sql.Type {
68+
return types.Int64
69+
}
70+
71+
// IsNullable implements sql.Expression interface.
72+
func (j JSONDepth) IsNullable() bool {
73+
return j.JSON.IsNullable()
74+
}
75+
76+
// depth returns the maximum depth of a JSON document.
77+
func depth(obj interface{}) (int, error) {
78+
var maxDepth int
79+
switch o := obj.(type) {
80+
case []interface{}:
81+
for _, v := range o {
82+
d, err := depth(v)
83+
if err != nil {
84+
return 0, err
85+
}
86+
if d > maxDepth {
87+
maxDepth = d
88+
}
89+
}
90+
case map[string]interface{}:
91+
for _, v := range o {
92+
d, err := depth(v)
93+
if err != nil {
94+
return 0, err
95+
}
96+
if d > maxDepth {
97+
maxDepth = d
98+
}
99+
}
100+
}
101+
return maxDepth + 1, nil
102+
}
103+
104+
// Eval implements sql.Expression interface.
105+
func (j JSONDepth) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
106+
span, ctx := ctx.Span(fmt.Sprintf("function.%s", j.FunctionName()))
107+
defer span.End()
108+
109+
doc, err := getJSONDocumentFromRow(ctx, row, j.JSON)
110+
if err != nil {
111+
return nil, err
112+
}
113+
if doc == nil {
114+
return nil, nil
115+
}
116+
117+
d, err := depth(doc.Val)
118+
if err != nil {
119+
return nil, err
120+
}
121+
122+
return d, nil
123+
}
124+
125+
// Children implements sql.Expression interface.
126+
func (j JSONDepth) Children() []sql.Expression {
127+
return []sql.Expression{j.JSON}
128+
}
129+
130+
// WithChildren implements sql.Expression interface.
131+
func (j JSONDepth) WithChildren(children ...sql.Expression) (sql.Expression, error) {
132+
return NewJSONDepth(children...)
133+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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+
)
27+
28+
func TestJSONDepth(t *testing.T) {
29+
_, err := NewJSONDepth()
30+
require.True(t, errors.Is(err, sql.ErrInvalidArgumentNumber))
31+
32+
f1 := buildGetFieldExpressions(t, NewJSONDepth, 1)
33+
testCases := []struct {
34+
f sql.Expression
35+
row sql.Row
36+
exp interface{}
37+
err bool
38+
}{
39+
{
40+
f: f1,
41+
row: sql.Row{``},
42+
err: true,
43+
},
44+
{
45+
f: f1,
46+
row: sql.Row{`badjson`},
47+
err: true,
48+
},
49+
{
50+
f: f1,
51+
row: sql.Row{true},
52+
err: true,
53+
},
54+
{
55+
f: f1,
56+
row: sql.Row{1},
57+
err: true,
58+
},
59+
60+
{
61+
f: f1,
62+
row: sql.Row{nil},
63+
exp: nil,
64+
},
65+
66+
{
67+
f: f1,
68+
row: sql.Row{`null`},
69+
exp: 1,
70+
},
71+
{
72+
f: f1,
73+
row: sql.Row{`1`},
74+
exp: 1,
75+
},
76+
{
77+
f: f1,
78+
row: sql.Row{`true`},
79+
exp: 1,
80+
},
81+
{
82+
f: f1,
83+
row: sql.Row{`123.456`},
84+
exp: 1,
85+
},
86+
{
87+
f: f1,
88+
row: sql.Row{`"abcdef"`},
89+
exp: 1,
90+
},
91+
92+
{
93+
f: f1,
94+
row: sql.Row{`[]`},
95+
exp: 1,
96+
},
97+
{
98+
f: f1,
99+
row: sql.Row{`{}`},
100+
exp: 1,
101+
},
102+
103+
{
104+
f: f1,
105+
row: sql.Row{`[null]`},
106+
exp: 2,
107+
},
108+
{
109+
f: f1,
110+
row: sql.Row{`{"a": null}`},
111+
exp: 2,
112+
},
113+
{
114+
f: f1,
115+
row: sql.Row{`[1]`},
116+
exp: 2,
117+
},
118+
{
119+
f: f1,
120+
row: sql.Row{`{"a": 1}`},
121+
exp: 2,
122+
},
123+
{
124+
f: f1,
125+
row: sql.Row{`[1, 2, 3]`},
126+
exp: 2,
127+
},
128+
{
129+
f: f1,
130+
row: sql.Row{`{"aa": 1, "bb": 2, "c": 3}`},
131+
exp: 2,
132+
},
133+
134+
{
135+
f: f1,
136+
row: sql.Row{`{"a": 1, "b": [1, 2, 3]}`},
137+
exp: 3,
138+
},
139+
{
140+
f: f1,
141+
row: sql.Row{`[0, {"a": 1, "b": 2}]`},
142+
exp: 3,
143+
},
144+
145+
{
146+
f: f1,
147+
row: sql.Row{`{"a": 1, "b": {"aa": 1, "bb": {"aaa": 1, "bbb": {"aaaa": 1}}}}`},
148+
exp: 5,
149+
},
150+
}
151+
152+
for _, tt := range testCases {
153+
var args []string
154+
for _, a := range tt.row {
155+
args = append(args, fmt.Sprintf("%v", a))
156+
}
157+
t.Run(strings.Join(args, ", "), func(t *testing.T) {
158+
require := require.New(t)
159+
result, err := tt.f.Eval(sql.NewEmptyContext(), tt.row)
160+
if tt.err {
161+
require.Error(err)
162+
} else {
163+
require.NoError(err)
164+
}
165+
require.Equal(tt.exp, result)
166+
})
167+
}
168+
}

sql/expression/function/json/json_unsupported.go

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -236,40 +236,6 @@ type JSONMerge struct {
236236
// JSON attribute functions //
237237
//////////////////////////////
238238

239-
// JSON_DEPTH(json_doc)
240-
//
241-
// JSONDepth Returns the maximum depth of a JSON document. Returns NULL if the argument is NULL. An error occurs if the
242-
// argument is not a valid JSON document. An empty array, empty object, or scalar value has depth 1. A nonempty array
243-
// containing only elements of depth 1 or nonempty object containing only member values of depth 1 has depth 2.
244-
// Otherwise, a JSON document has depth greater than 2.
245-
//
246-
// https://dev.mysql.com/doc/refman/8.0/en/json-attribute-functions.html#function_json-depth
247-
type JSONDepth struct {
248-
sql.Expression
249-
}
250-
251-
var _ sql.FunctionExpression = JSONDepth{}
252-
253-
// NewJSONDepth creates a new JSONDepth function.
254-
func NewJSONDepth(args ...sql.Expression) (sql.Expression, error) {
255-
return nil, ErrUnsupportedJSONFunction.New(JSONDepth{}.FunctionName())
256-
}
257-
258-
// FunctionName implements sql.FunctionExpression
259-
func (j JSONDepth) FunctionName() string {
260-
return "json_depth"
261-
}
262-
263-
// Description implements sql.FunctionExpression
264-
func (j JSONDepth) Description() string {
265-
return "returns maximum depth of JSON document."
266-
}
267-
268-
// IsUnsupported implements sql.UnsupportedFunctionStub
269-
func (j JSONDepth) IsUnsupported() bool {
270-
return true
271-
}
272-
273239
// JSON_TYPE(json_val)
274240
//
275241
// Returns a utf8mb4 string indicating the type of a JSON value. This can be an object, an array, or a scalar type.

0 commit comments

Comments
 (0)