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
105 changes: 105 additions & 0 deletions enginetest/queries/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -9354,6 +9354,90 @@ from typestable`,
{false},
},
},
{
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.

Also add tests for json_type returning "NULL" and "DOUBLE". You can express a double in a JSON literal using scientific notation (ie "{a: 10e2}")

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.

I think I was wrong about being able to use scientific notation here.

I think we should add the following tests, to make sure that we respect round-trip conversions of types:

SELECT json_type(json_extract(json_object("a", cast(10 as double)), "$.a")) -> "DOUBLE"
SELECT json_type(json_extract(json_object("a", cast(10 as unsigned)), "$.a")) -> "UNSIGNED INTEGER"
SELECT json_type(json_extract(json_object("a", cast(10 as signed)), "$.a")) -> "INTEGER"
SELECT json_type(json_extract(json_object("a", cast(10 as decimal)), "$.a")) -> "DECIMAL"

It's possible that the way we currently store JSON causes some of these to not work properly. We can disable those tests if it's out of scope to fix them now. But we should still have them as tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

didn't see this comment sorry!

test are added here:
#2355

Query: `SELECT json_type('{"a": [10, true, "abc"]}');`,
Expected: []sql.Row{
{"OBJECT"},
},
},
{
Query: `SELECT json_type(json_extract('{"a": [10, true, "abc"]}', '$.a'));`,
Expected: []sql.Row{
{"ARRAY"},
},
},
{
Query: `SELECT json_type(json_extract('{"a": [10, true, "abc"]}', '$.a[0]'));`,
Expected: []sql.Row{
{"INTEGER"},
},
},
{
Query: `SELECT json_type(json_extract('{"a": [10, true, "abc"]}', '$.a[1]'));`,
Expected: []sql.Row{
{"BOOLEAN"},
},
},
{
Query: `SELECT json_type(json_extract('{"a": [10, true, "abc"]}', '$.a[2]'));`,
Expected: []sql.Row{
{"STRING"},
},
},
{
Query: `SELECT json_type(json_extract('{"a": 123.456}', '$.a'));`,
Expected: []sql.Row{
{"DOUBLE"},
},
},
{
Query: `SELECT json_type(json_extract('{"a": null}', '$.a'));`,
Expected: []sql.Row{
{"NULL"},
},
},
{
Query: "SELECT json_type(cast(cast('2001-01-01 12:34:56.123456' as datetime) as json));",
Expected: []sql.Row{
{"DATETIME"},
},
},
{
Query: "SELECT json_type(cast(cast('2001-01-01 12:34:56.123456' as date) as json));",
Expected: []sql.Row{
{"DATE"},
},
},
{
Query: "select json_type(cast(cast(1 as unsigned) as json));",
Expected: []sql.Row{
{"UNSIGNED INTEGER"},
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.

oooh, nice work! The docs don't mention "UNSIGNED INTEGER" as a possible response, but that's exactly what MySQL returns! Good job reversing engineering MySQL's behavior here.

},
},
{
Query: "select json_type('4294967295');",
Expected: []sql.Row{
{"INTEGER"},
},
},
{
Query: "select json_type('4294967296');",
Expected: []sql.Row{
{"UNSIGNED INTEGER"},
},
},
{
Query: "SELECT json_type(cast(1.0 as json));",
Expected: []sql.Row{
{"DECIMAL"},
},
},
{
Query: "select json_type(cast(cast(2001 as year) as json));",
Expected: []sql.Row{
{"UNSIGNED INTEGER"},
},
},
}

var KeylessQueries = []QueryTest{
Expand Down Expand Up @@ -9559,6 +9643,27 @@ FROM mytable;`,
Query: "select TABLE_NAME, IS_UPDATABLE from information_schema.views where table_schema = 'mydb'",
Expected: []sql.Row{{"myview1", "YES"}, {"myview2", "YES"}, {"myview3", "NO"}, {"myview4", "NO"}, {"myview5", "YES"}},
},
// time to json cast is broken
{
Query: "SELECT json_type(cast(cast('2001-01-01 12:34:56.123456' as time) as json));",
Expected: []sql.Row{
{"TIME"},
},
},
// binary to json cast is broken
{
Query: "SELECT json_type(cast(cast('123abc' as binary) as json));",
Expected: []sql.Row{
{"BLOB"},
},
},
// 1e2 -> 100, so we can't tell the difference between float and integer in this case
{
Query: `SELECT json_type(json_extract('{"a": 1e2}', '$.a'));`,
Expected: []sql.Row{
{"DOUBLE"},
},
},
}

var VersionedQueries = []QueryTest{
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
github.com/dolthub/go-icu-regex v0.0.0-20230524105445-af7e7991c97e
github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71
github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81
github.com/dolthub/vitess v0.0.0-20240228192915-d55088cef56a
github.com/dolthub/vitess v0.0.0-20240228234620-13c0f62e6b4a
github.com/go-kit/kit v0.10.0
github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d
github.com/gocraft/dbr/v2 v2.7.2
Expand Down
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,8 @@ github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTE
github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI=
github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81 h1:7/v8q9XGFa6q5Ap4Z/OhNkAMBaK5YeuEzwJt+NZdhiE=
github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81/go.mod h1:siLfyv2c92W1eN/R4QqG/+RjjX5W2+gCTRjZxBjI3TY=
github.com/dolthub/vitess v0.0.0-20240209125211-6c93b0341608 h1:jnInva1KcJJf/QQsxbN9tTJckOZf73EzUen8rrik0Yw=
github.com/dolthub/vitess v0.0.0-20240209125211-6c93b0341608/go.mod h1:IwjNXSQPymrja5pVqmfnYdcy7Uv7eNJNBPK/MEh9OOw=
github.com/dolthub/vitess v0.0.0-20240228192915-d55088cef56a h1:o/hVrAnMos6KVGFQz27IDZNz1F61QPnmWxoB6BGv6vM=
github.com/dolthub/vitess v0.0.0-20240228192915-d55088cef56a/go.mod h1:IwjNXSQPymrja5pVqmfnYdcy7Uv7eNJNBPK/MEh9OOw=
github.com/dolthub/vitess v0.0.0-20240228234620-13c0f62e6b4a h1:aXFMPhMpOYiyPo2wqgpPLjxqr8mvfxSW2wSpLpzboZ0=
github.com/dolthub/vitess v0.0.0-20240228234620-13c0f62e6b4a/go.mod h1:IwjNXSQPymrja5pVqmfnYdcy7Uv7eNJNBPK/MEh9OOw=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
Expand Down
20 changes: 20 additions & 0 deletions sql/expression/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ const (
ConvertToSigned = "signed"
// ConvertToTime is a conversion to time.
ConvertToTime = "time"
// ConvertToYear isa convert to year.
ConvertToYear = "year"
// ConvertToUnsigned is a conversion to unsigned.
ConvertToUnsigned = "unsigned"
)
Expand Down Expand Up @@ -171,6 +173,8 @@ func (c *Convert) Type() sql.Type {
return types.Time
case ConvertToUnsigned:
return types.Uint64
case ConvertToYear:
return types.Year
default:
return types.Null
}
Expand Down Expand Up @@ -199,6 +203,8 @@ func (c *Convert) CollationCoercibility(ctx *sql.Context) (collation sql.Collati
return sql.Collation_binary, 5
case ConvertToUnsigned:
return sql.Collation_binary, 5
case ConvertToYear:
return sql.Collation_binary, 5
default:
return sql.Collation_binary, 7
}
Expand Down Expand Up @@ -391,6 +397,20 @@ func convertValue(val interface{}, castTo string, originType sql.Type, typeLengt
return uint64(num.(int64)), nil
}
return num, nil
case ConvertToYear:
value, err := convertHexBlobToDecimalForNumericContext(val, originType)
if err != nil {
return nil, err
}
num, _, err := types.Uint64.Convert(value)
if err != nil {
num, _, err = types.Int64.Convert(value)
if err != nil {
return types.Uint64.Zero(), nil
}
return uint64(num.(int64)), nil
}
return num, nil
default:
return nil, nil
}
Expand Down
2 changes: 1 addition & 1 deletion sql/expression/function/json/json_depth_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023 Dolthub, Inc.
// 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.
Expand Down
142 changes: 142 additions & 0 deletions sql/expression/function/json/json_type.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"
"math"

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

// JSONType (json_val)
//
// Returns a utf8mb4 string indicating the type of a JSON value. This can be an object, an array, or a scalar type.
// JSONType returns NULL if the argument is NULL. An error occurs if the argument is not a valid JSON value
//
// https://dev.mysql.com/doc/refman/8.0/en/json-attribute-functions.html#function_json-type
type JSONType struct {
JSON sql.Expression
}

var _ sql.FunctionExpression = &JSONType{}

// NewJSONType creates a new JSONType function.
func NewJSONType(args ...sql.Expression) (sql.Expression, error) {
if len(args) != 1 {
return nil, sql.ErrInvalidArgumentNumber.New("JSON_TYPE", "1", len(args))
}
return &JSONType{JSON: args[0]}, nil
}

// FunctionName implements sql.FunctionExpression
func (j JSONType) FunctionName() string {
return "json_type"
}

// Description implements sql.FunctionExpression
func (j JSONType) Description() string {
return "returns type of JSON value."
}

// Resolved implements the Expression interface.
func (j JSONType) Resolved() bool {
return j.JSON.Resolved()
}

// String implements the fmt.Stringer interface.
func (j JSONType) String() string {
return fmt.Sprintf("%s(%s)", j.FunctionName(), j.JSON.String())
}

// Type implements the Expression interface.
func (j JSONType) Type() sql.Type {
return types.Text
}

// IsNullable implements the Expression interface.
func (j JSONType) IsNullable() bool {
return j.JSON.IsNullable()
}

// Eval implements the Expression interface.
func (j JSONType) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
span, ctx := ctx.Span(fmt.Sprintf("function.%s", j.FunctionName()))
defer span.End()

doc, err := getJSONDocumentFromRow(ctx, row, j.JSON)
if err != nil {
return nil, err
}
if doc == nil {
return "NULL", nil
}

switch v := doc.Val.(type) {
case nil:
return "NULL", nil
case bool:
return "BOOLEAN", nil
case float64:
if conv, ok := j.JSON.(*expression.Convert); ok {
typ := conv.Child.Type()
if types.IsUnsigned(typ) || types.IsYear(typ) {
return "UNSIGNED INTEGER", nil
}
}
if math.Floor(v) == v {
if v >= (math.MaxInt32+1)*2 {
return "UNSIGNED INTEGER", nil
}
return "INTEGER", nil
}
return "DOUBLE", nil
case string:
if conv, ok := j.JSON.(*expression.Convert); ok {
typ := conv.Child.Type()
if types.IsDecimal(typ) {
return "DECIMAL", nil
}
if types.IsDatetimeType(typ) {
return "DATETIME", nil
}
if types.IsDateType(typ) {
return "DATE", nil
}
if types.IsTime(typ) {
return "TIME", nil
}
}
return "STRING", nil
case []interface{}:
return "ARRAY", nil
case map[string]interface{}:
return "OBJECT", nil
default:
return "OPAQUE", nil
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.

Any way to trigger OPAQUE? I don't think I saw that in any of the tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I haven't found a query that spits out OPAQUE in MySQL or dolt

}
}

// Children implements the Expression interface.
func (j JSONType) Children() []sql.Expression {
return []sql.Expression{j.JSON}
}

// WithChildren implements the Expression interface.
func (j JSONType) WithChildren(children ...sql.Expression) (sql.Expression, error) {
return NewJSONType(children...)
}
Loading