diff --git a/enginetest/queries/queries.go b/enginetest/queries/queries.go index ff61626757..5112bf32b2 100644 --- a/enginetest/queries/queries.go +++ b/enginetest/queries/queries.go @@ -9354,6 +9354,90 @@ from typestable`, {false}, }, }, + { + 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"}, + }, + }, + { + 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{ @@ -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{ diff --git a/go.mod b/go.mod index fb5000e4db..cb8732ea44 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 0209f4f0e8..41c663bca1 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/sql/expression/convert.go b/sql/expression/convert.go index c4139c3255..21a74a6e01 100644 --- a/sql/expression/convert.go +++ b/sql/expression/convert.go @@ -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" ) @@ -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 } @@ -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 } @@ -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 } diff --git a/sql/expression/function/json/json_depth_test.go b/sql/expression/function/json/json_depth_test.go index dc4f7886b7..aaa0bb2a4a 100644 --- a/sql/expression/function/json/json_depth_test.go +++ b/sql/expression/function/json/json_depth_test.go @@ -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. diff --git a/sql/expression/function/json/json_type.go b/sql/expression/function/json/json_type.go new file mode 100644 index 0000000000..2c70f34be6 --- /dev/null +++ b/sql/expression/function/json/json_type.go @@ -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 + } +} + +// 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...) +} diff --git a/sql/expression/function/json/json_type_test.go b/sql/expression/function/json/json_type_test.go new file mode 100644 index 0000000000..9e5d4c7dd3 --- /dev/null +++ b/sql/expression/function/json/json_type_test.go @@ -0,0 +1,126 @@ +// 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" + "gopkg.in/src-d/go-errors.v1" + + "github.com/dolthub/go-mysql-server/sql" +) + +func TestJSONType(t *testing.T) { + _, err := NewJSONType() + require.True(t, errors.Is(err, sql.ErrInvalidArgumentNumber)) + + f1 := buildGetFieldExpressions(t, NewJSONType, 1) + testCases := []struct { + f sql.Expression + row sql.Row + exp interface{} + err bool + }{ + { + f: f1, + row: sql.Row{``}, + err: true, + }, + { + f: f1, + row: sql.Row{`badjson`}, + err: true, + }, + { + f: f1, + row: sql.Row{true}, + err: true, + }, + { + f: f1, + row: sql.Row{1}, + err: true, + }, + + { + f: f1, + row: sql.Row{nil}, + exp: "NULL", + }, + + { + f: f1, + row: sql.Row{`null`}, + exp: "NULL", + }, + { + f: f1, + row: sql.Row{`1`}, + exp: "INTEGER", + }, + { + f: f1, + row: sql.Row{`true`}, + exp: "BOOLEAN", + }, + { + f: f1, + row: sql.Row{`123.456`}, + exp: "DOUBLE", + }, + + { + f: f1, + row: sql.Row{`[]`}, + exp: "ARRAY", + }, + { + f: f1, + row: sql.Row{`{}`}, + exp: "OBJECT", + }, + + { + f: f1, + row: sql.Row{`[1, 2, 3]`}, + exp: "ARRAY", + }, + { + f: f1, + row: sql.Row{`{"aa": 1, "bb": 2, "c": 3}`}, + exp: "OBJECT", + }, + } + + 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) + result, err := tt.f.Eval(sql.NewEmptyContext(), tt.row) + if tt.err { + require.Error(err) + } else { + require.NoError(err) + } + require.Equal(tt.exp, result) + }) + } +} diff --git a/sql/expression/function/json/json_unsupported.go b/sql/expression/function/json/json_unsupported.go index 51c2808855..deeb140776 100644 --- a/sql/expression/function/json/json_unsupported.go +++ b/sql/expression/function/json/json_unsupported.go @@ -232,42 +232,6 @@ type JSONMerge struct { sql.Expression } -////////////////////////////// -// JSON attribute functions // -////////////////////////////// - -// JSON_TYPE(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 { - sql.Expression -} - -var _ sql.FunctionExpression = JSONType{} - -// NewJSONType creates a new JSONType function. -func NewJSONType(args ...sql.Expression) (sql.Expression, error) { - return nil, ErrUnsupportedJSONFunction.New(JSONType{}.FunctionName()) -} - -// 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." -} - -// IsUnsupported implements sql.UnsupportedFunctionStub -func (j JSONType) IsUnsupported() bool { - return true -} - ////////////////////////// // JSON table functions // //////////////////////////