252 lines
7.9 KiB
Go
252 lines
7.9 KiB
Go
// 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 functions
|
|
|
|
import (
|
|
"fmt"
|
|
"unsafe"
|
|
|
|
"github.com/dolthub/go-mysql-server/sql"
|
|
"github.com/dolthub/go-mysql-server/sql/types"
|
|
"github.com/goccy/go-json"
|
|
|
|
"github.com/dolthub/doltgresql/server/functions/framework"
|
|
pgtypes "github.com/dolthub/doltgresql/server/types"
|
|
"github.com/dolthub/doltgresql/utils"
|
|
)
|
|
|
|
// jsonWrapperToFormattedString converts a sql.JSONWrapper to a formatted JSON string with spaces (JSONB format).
|
|
func jsonWrapperToFormattedString(ctx *sql.Context, val sql.JSONWrapper) (string, error) {
|
|
v, err := val.ToInterface(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return types.JSONDocument{Val: v}.JSONString()
|
|
}
|
|
|
|
// initJson registers the functions to the catalog.
|
|
func initJson() {
|
|
framework.RegisterFunction(json_in)
|
|
framework.RegisterFunction(json_out)
|
|
framework.RegisterFunction(json_recv)
|
|
framework.RegisterFunction(json_send)
|
|
framework.RegisterFunction(json_build_array)
|
|
framework.RegisterFunction(json_build_object)
|
|
}
|
|
|
|
// json_in represents the PostgreSQL function of json type IO input.
|
|
var json_in = framework.Function1{
|
|
Name: "json_in",
|
|
Return: pgtypes.Json,
|
|
Parameters: [1]*pgtypes.DoltgresType{pgtypes.Cstring},
|
|
Strict: true,
|
|
Callable: json_in_callable,
|
|
}
|
|
|
|
func json_in_callable(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) {
|
|
input := val.(string)
|
|
var jsonVal any
|
|
err := json.Unmarshal(unsafe.Slice(unsafe.StringData(input), len(input)), &jsonVal)
|
|
if err != nil {
|
|
if len(input) > 10 {
|
|
input = input[:10] + "..."
|
|
}
|
|
return nil, pgtypes.ErrInvalidSyntaxForType.New("json", input)
|
|
}
|
|
return types.JSONDocument{Val: jsonVal}, nil
|
|
}
|
|
|
|
// json_out represents the PostgreSQL function of json type IO output.
|
|
var json_out = framework.Function1{
|
|
Name: "json_out",
|
|
Return: pgtypes.Cstring,
|
|
Parameters: [1]*pgtypes.DoltgresType{pgtypes.Json},
|
|
Strict: true,
|
|
Callable: json_out_callable,
|
|
}
|
|
|
|
func json_out_callable(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) {
|
|
// Large values stored with the old ExtendedAdaptiveEnc encoding are returned as a
|
|
// *val.ExtendedValueWrapper (sql.AnyWrapper). Unwrap to get the underlying JSONDocument.
|
|
if wrapper, ok := val.(sql.AnyWrapper); ok {
|
|
unwrapped, err := wrapper.UnwrapAny(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
val = unwrapped
|
|
}
|
|
switch v := val.(type) {
|
|
case string:
|
|
return v, nil
|
|
case types.JSONBytes:
|
|
bytes, err := v.GetBytes(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return string(bytes), err
|
|
case sql.JSONWrapper:
|
|
// JSON type is stored as binary JSON (same as JSONB), so output is normalized with spaces
|
|
return jsonWrapperToFormattedString(ctx, v)
|
|
default:
|
|
return nil, fmt.Errorf("unexpected type for json_out: %T", val)
|
|
}
|
|
}
|
|
|
|
// JsonOutCallable is exported so that tests outside this package can exercise the json output
|
|
// function directly without going through the global function registry.
|
|
var JsonOutCallable = json_out_callable
|
|
|
|
// JsonbOutCallable is exported so that tests outside this package can exercise the jsonb output
|
|
// function directly without going through the global function registry.
|
|
var JsonbOutCallable = jsonb_out_callable
|
|
|
|
// jsonb_out_callable formats a JSONB value for output. JSONB normalizes JSON with spaces after ':' and ','.
|
|
func jsonb_out_callable(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) {
|
|
// Large values stored with the old ExtendedAdaptiveEnc encoding are returned as a
|
|
// *val.ExtendedValueWrapper (sql.AnyWrapper). Unwrap to get the underlying JSONDocument.
|
|
if wrapper, ok := val.(sql.AnyWrapper); ok {
|
|
unwrapped, err := wrapper.UnwrapAny(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
val = unwrapped
|
|
}
|
|
switch v := val.(type) {
|
|
case string:
|
|
// Parse and reformat the string as proper JSONB (normalized with spaces)
|
|
doc, err := pgtypes.JsonB.IoInput(ctx, v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if doc == nil {
|
|
return nil, nil
|
|
}
|
|
return jsonWrapperToFormattedString(ctx, doc.(sql.JSONWrapper))
|
|
case sql.JSONWrapper:
|
|
return jsonWrapperToFormattedString(ctx, v)
|
|
default:
|
|
return nil, fmt.Errorf("unexpected type for jsonb_out: %T", val)
|
|
}
|
|
}
|
|
|
|
// json_recv represents the PostgreSQL function of json type IO receive.
|
|
var json_recv = framework.Function1{
|
|
Name: "json_recv",
|
|
Return: pgtypes.Json,
|
|
Parameters: [1]*pgtypes.DoltgresType{pgtypes.Internal},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) {
|
|
data := val.([]byte)
|
|
if data == nil {
|
|
return nil, nil
|
|
}
|
|
return json_in_callable(ctx, [2]*pgtypes.DoltgresType{}, string(data))
|
|
},
|
|
}
|
|
|
|
// json_send represents the PostgreSQL function of json type IO send.
|
|
var json_send = framework.Function1{
|
|
Name: "json_send",
|
|
Return: pgtypes.Bytea,
|
|
Parameters: [1]*pgtypes.DoltgresType{pgtypes.Json},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) {
|
|
if wrapper, ok := val.(sql.AnyWrapper); ok {
|
|
var err error
|
|
val, err = wrapper.UnwrapAny(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if val == nil {
|
|
return nil, nil
|
|
}
|
|
}
|
|
writer := utils.NewWireWriter()
|
|
var jsonStr string
|
|
switch v := val.(type) {
|
|
case string:
|
|
jsonStr = v
|
|
case sql.JSONWrapper:
|
|
var err error
|
|
jsonStr, err = jsonWrapperToFormattedString(ctx, v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("unexpected type for json_send: %T", val)
|
|
}
|
|
writer.WriteString(jsonStr)
|
|
return writer.BufferData(), nil
|
|
},
|
|
}
|
|
|
|
// json_build_array represents the PostgreSQL function json_build_array.
|
|
var json_build_array = framework.Function1{
|
|
Name: "json_build_array",
|
|
Return: pgtypes.Json,
|
|
Parameters: [1]*pgtypes.DoltgresType{pgtypes.AnyArray},
|
|
Variadic: true,
|
|
Callable: json_build_array_callable,
|
|
}
|
|
|
|
func json_build_array_callable(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val1 any) (any, error) {
|
|
inputArray := val1.([]any)
|
|
return types.JSONDocument{Val: inputArray}, nil
|
|
}
|
|
|
|
// json_build_object represents the PostgreSQL function json_build_object.
|
|
var json_build_object = framework.Function1{
|
|
Name: "json_build_object",
|
|
Return: pgtypes.Json,
|
|
Parameters: [1]*pgtypes.DoltgresType{pgtypes.AnyArray},
|
|
Variadic: true,
|
|
Callable: json_build_object_callable,
|
|
}
|
|
|
|
func json_build_object_callable(ctx *sql.Context, argTypes [2]*pgtypes.DoltgresType, val1 any) (any, error) {
|
|
json, err := buildJsonObject("json_build_object", argTypes, val1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return json, nil
|
|
}
|
|
|
|
// buildJsonObject constructs a json object from the input array provided, which are alternating keys and values.
|
|
func buildJsonObject(fnName string, _ [2]*pgtypes.DoltgresType, val1 any) (types.JSONDocument, error) {
|
|
inputArray := val1.([]any)
|
|
if len(inputArray)%2 != 0 {
|
|
return types.JSONDocument{}, sql.ErrInvalidArgumentNumber.New(fnName, "even number of arguments", len(inputArray))
|
|
}
|
|
jsonObject := make(map[string]any)
|
|
var key string
|
|
for i, e := range inputArray {
|
|
if i%2 == 0 {
|
|
var ok bool
|
|
key, ok = e.(string)
|
|
if !ok {
|
|
// TODO: This isn't correct for every type we might use as a value. To get better type info to transform
|
|
// every value into its string format, we need to pass detailed arg type info for the vararg params (the
|
|
// unused param in the function call).
|
|
key = fmt.Sprintf("%v", e)
|
|
}
|
|
} else {
|
|
jsonObject[key] = e
|
|
key = ""
|
|
}
|
|
}
|
|
|
|
return types.JSONDocument{Val: jsonObject}, nil
|
|
}
|