diff --git a/enginetest/queries/queries.go b/enginetest/queries/queries.go index 4f8f076a25..a96291f5b4 100644 --- a/enginetest/queries/queries.go +++ b/enginetest/queries/queries.go @@ -1766,6 +1766,42 @@ Select * from ( {`[{"z": 456, "aa": 123}, {"Y": 654, "BB": 321}]`}, }, }, + { + Query: `select json_pretty(c3) from jsontable`, + Expected: []sql.Row{ + { + `{ + "a": 2 +}`, + }, + { + `{ + "b": 2 +}`, + }, + { + `{ + "c": 2 +}`, + }, + { + `{ + "d": 2 +}`, + }, + }, + }, + { + Query: `select json_pretty(json_object("id", 1, "name", "test"));`, + Expected: []sql.Row{ + { + `{ + "id": 1, + "name": "test" +}`, + }, + }, + }, { Query: `SELECT column_0, sum(column_1) FROM (values row(1,1), row(1,3), row(2,2), row(2,5), row(3,9)) a diff --git a/sql/expression/function/json/json_pretty.go b/sql/expression/function/json/json_pretty.go new file mode 100644 index 0000000000..f65a1cd542 --- /dev/null +++ b/sql/expression/function/json/json_pretty.go @@ -0,0 +1,100 @@ +// 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 ( + "encoding/json" + "fmt" + + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/expression" + "github.com/dolthub/go-mysql-server/sql/types" +) + +// JSONPretty (json_val) +// +// JSONPretty Provides pretty-printing of JSON values similar to that implemented in PHP and by other languages and +// database systems. The value supplied must be a JSON value or a valid string representation of a JSON value. +// Extraneous whitespaces and newlines present in this value have no effect on the output. For a NULL value, the +// function returns NULL. If the value is not a JSON document, or if it cannot be parsed as one, the function fails +// with an error. Formatting of the output from this function adheres to the following rules: +// - Each array element or object member appears on a separate line, indented by one additional level as compared to +// its parent. +// - Each level of indentation adds two leading spaces. +// - A comma separating individual array elements or object members is printed before the newline that separates the +// two elements or members. +// - The key and the value of an object member are separated by a colon followed by a space (': '). +// - An empty object or array is printed on a single line. No space is printed between the opening and closing brace. +// - Special characters in string scalars and key names are escaped employing the same rules used by JSONQuote. +// +// https://dev.mysql.com/doc/refman/8.0/en/json-utility-functions.html#function_json-pretty +type JSONPretty struct { + expression.UnaryExpression +} + +var _ sql.FunctionExpression = &JSONPretty{} + +// NewJSONPretty creates a new JSONPretty function. +func NewJSONPretty(arg sql.Expression) sql.Expression { + return &JSONPretty{expression.UnaryExpression{Child: arg}} +} + +// FunctionName implements sql.FunctionExpression +func (j *JSONPretty) FunctionName() string { + return "json_pretty" +} + +// Description implements sql.FunctionExpression +func (j *JSONPretty) Description() string { + return "prints a JSON document in human-readable format." +} + +// String implements sql.Expression +func (j *JSONPretty) String() string { + return fmt.Sprintf("%s(%s)", j.FunctionName(), j.Child.String()) +} + +// Type implements sql.Expression +func (j *JSONPretty) Type() sql.Type { + return types.LongText +} + +// Eval implements sql.Expression +func (j *JSONPretty) 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.Child) + if err != nil { + return nil, err + } + if doc == nil { + return nil, nil + } + res, err := json.MarshalIndent(doc.Val, "", " ") + if err != nil { + return nil, err + } + + return string(res), nil +} + +// WithChildren implements sql.Expression +func (j *JSONPretty) WithChildren(children ...sql.Expression) (sql.Expression, error) { + if len(children) != 1 { + return nil, sql.ErrInvalidChildrenNumber.New(j, len(children), 1) + } + return NewJSONPretty(children[0]), nil +} diff --git a/sql/expression/function/json/json_pretty_test.go b/sql/expression/function/json/json_pretty_test.go new file mode 100644 index 0000000000..d2133e5d77 --- /dev/null +++ b/sql/expression/function/json/json_pretty_test.go @@ -0,0 +1,123 @@ +// 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 ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/expression" + "github.com/dolthub/go-mysql-server/sql/types" +) + +func TestJSONPretty(t *testing.T) { + testCases := []struct { + arg sql.Expression + exp interface{} + err bool + }{ + { + arg: expression.NewLiteral(``, types.Text), + err: true, + }, + { + arg: expression.NewLiteral(`badjson`, types.Text), + err: true, + }, + + { + arg: expression.NewLiteral(nil, types.Null), + exp: nil, + }, + { + arg: expression.NewLiteral(`null`, types.Text), + exp: `null`, + }, + { + arg: expression.NewLiteral(`true`, types.Text), + exp: `true`, + }, + { + arg: expression.NewLiteral(`false`, types.Text), + exp: `false`, + }, + { + arg: expression.NewLiteral(`123`, types.Text), + exp: `123`, + }, + { + arg: expression.NewLiteral(`123.456`, types.Text), + exp: `123.456`, + }, + { + arg: expression.NewLiteral(`"hello"`, types.Text), + exp: `"hello"`, + }, + + { + arg: expression.NewLiteral(`[]`, types.Text), + exp: `[]`, + }, + { + arg: expression.NewLiteral(`{}`, types.Text), + exp: `{}`, + }, + { + arg: expression.NewLiteral(`[1,3,5]`, types.Text), + exp: `[ + 1, + 3, + 5 +]`, + }, + { + arg: expression.NewLiteral(`["a",1,{"key1": "value1"},"5", "77" , {"key2":["value3","valueX", "valueY"]},"j", "2" ]`, types.Text), + exp: `[ + "a", + 1, + { + "key1": "value1" + }, + "5", + "77", + { + "key2": [ + "value3", + "valueX", + "valueY" + ] + }, + "j", + "2" +]`, + }, + } + + for _, tt := range testCases { + t.Run(tt.arg.String(), func(t *testing.T) { + require := require.New(t) + f := NewJSONPretty(tt.arg) + res, err := f.Eval(sql.NewEmptyContext(), nil) + if tt.err { + require.Error(err) + return + } + require.NoError(err) + require.Equal(tt.exp, res) + }) + } +} diff --git a/sql/expression/function/json/json_unsupported.go b/sql/expression/function/json/json_unsupported.go index 96e1271239..787ab0afb7 100644 --- a/sql/expression/function/json/json_unsupported.go +++ b/sql/expression/function/json/json_unsupported.go @@ -208,49 +208,6 @@ func (j JSONSchemaValidationReport) IsUnsupported() bool { // JSON utility functions // //////////////////////////// -// JSON_PRETTY(json_val) -// -// JSONPretty Provides pretty-printing of JSON values similar to that implemented in PHP and by other languages and -// database systems. The value supplied must be a JSON value or a valid string representation of a JSON value. -// Extraneous whitespaces and newlines present in this value have no effect on the output. For a NULL value, the -// function returns NULL. If the value is not a JSON document, or if it cannot be parsed as one, the function fails -// with an error. Formatting of the output from this function adheres to the following rules: -// - Each array element or object member appears on a separate line, indented by one additional level as compared to -// its parent. -// - Each level of indentation adds two leading spaces. -// - A comma separating individual array elements or object members is printed before the newline that separates the -// two elements or members. -// - The key and the value of an object member are separated by a colon followed by a space (': '). -// - An empty object or array is printed on a single line. No space is printed between the opening and closing brace. -// - Special characters in string scalars and key names are escaped employing the same rules used by JSONQuote. -// -// https://dev.mysql.com/doc/refman/8.0/en/json-utility-functions.html#function_json-pretty -type JSONPretty struct { - sql.Expression -} - -var _ sql.FunctionExpression = JSONPretty{} - -// NewJSONPretty creates a new JSONPretty function. -func NewJSONPretty(args ...sql.Expression) (sql.Expression, error) { - return nil, ErrUnsupportedJSONFunction.New(JSONPretty{}.FunctionName()) -} - -// FunctionName implements sql.FunctionExpression -func (j JSONPretty) FunctionName() string { - return "json_pretty" -} - -// Description implements sql.FunctionExpression -func (j JSONPretty) Description() string { - return "prints a JSON document in human-readable format." -} - -// IsUnsupported implements sql.UnsupportedFunctionStub -func (j JSONPretty) IsUnsupported() bool { - return true -} - // JSON_STORAGE_FREE(json_val) // // JSONStorageFree For a JSON column value, this function shows how much storage space was freed in its binary diff --git a/sql/expression/function/registry.go b/sql/expression/function/registry.go index aebe706157..70ffc454f6 100644 --- a/sql/expression/function/registry.go +++ b/sql/expression/function/registry.go @@ -131,7 +131,7 @@ var BuiltIns = []sql.Function{ sql.FunctionN{Name: "json_merge_preserve", Fn: json.NewJSONMergePreserve}, sql.FunctionN{Name: "json_object", Fn: json.NewJSONObject}, sql.FunctionN{Name: "json_overlaps", Fn: json.NewJSONOverlaps}, - sql.FunctionN{Name: "json_pretty", Fn: json.NewJSONPretty}, + sql.Function1{Name: "json_pretty", Fn: json.NewJSONPretty}, sql.Function1{Name: "json_quote", Fn: json.NewJSONQuote}, sql.FunctionN{Name: "json_remove", Fn: json.NewJSONRemove}, sql.FunctionN{Name: "json_replace", Fn: json.NewJSONReplace},