850 lines
27 KiB
Go
850 lines
27 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 binary
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/cockroachdb/errors"
|
|
"github.com/dolthub/go-mysql-server/sql"
|
|
"github.com/dolthub/go-mysql-server/sql/types"
|
|
|
|
"github.com/dolthub/doltgresql/server/functions/framework"
|
|
pgtypes "github.com/dolthub/doltgresql/server/types"
|
|
)
|
|
|
|
// makeObjectKeyPath builds a JSON path that selects the given top-level
|
|
// object key, quoting and escaping the key so any characters are allowed.
|
|
func makeObjectKeyPath(key string) string {
|
|
return "$" + objectKeyPathSegment(key)
|
|
}
|
|
|
|
// objectKeyPathSegment builds a single object-key step (e.g. `."key"`), quoting
|
|
// and escaping the key so any characters are allowed. Used both on its own and
|
|
// when composing a multi-step JSON path.
|
|
func objectKeyPathSegment(key string) string {
|
|
return `."` + strings.ReplaceAll(key, `"`, `\"`) + `"`
|
|
}
|
|
|
|
// makeArrayIndexPath builds a JSON path that selects the given top-level
|
|
// array element by non-negative index.
|
|
func makeArrayIndexPath(idx int) string {
|
|
return "$" + arrayIndexPathSegment(idx)
|
|
}
|
|
|
|
// arrayIndexPathSegment builds a single array-index step (e.g. `[3]`). Used both
|
|
// on its own and when composing a multi-step JSON path.
|
|
func arrayIndexPathSegment(idx int) string {
|
|
return "[" + strconv.Itoa(idx) + "]"
|
|
}
|
|
|
|
// These functions can be gathered using the following query from a Postgres 15 instance:
|
|
// SELECT * FROM pg_operator o WHERE o.oprname = <OPERATOR> ORDER BY o.oprcode::varchar;
|
|
// Replace <OPERATOR> with the desired JSON operator
|
|
|
|
// initJSON registers the functions to the catalog.
|
|
func initJSON() {
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryJSONExtractJson, json_array_element)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryJSONExtractJson, jsonb_array_element)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryJSONExtractJson, json_object_field)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryJSONExtractJson, jsonb_object_field)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryJSONExtractText, json_array_element_text)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryJSONExtractText, jsonb_array_element_text)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryJSONExtractText, json_object_field_text)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryJSONExtractText, jsonb_object_field_text)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryJSONExtractPathJson, json_extract_path)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryJSONExtractPathJson, jsonb_extract_path)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryJSONExtractPathText, json_extract_path_text)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryJSONExtractPathText, jsonb_extract_path_text)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryJSONContainsRight, jsonb_contains)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryJSONContainsLeft, jsonb_contained)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryJSONTopLevel, jsonb_exists)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryJSONTopLevelAny, jsonb_exists_any)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryJSONTopLevelAll, jsonb_exists_all)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryMinus, jsonb_delete_text)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryMinus, jsonb_delete_text_array)
|
|
framework.RegisterBinaryFunction(framework.Operator_BinaryMinus, jsonb_delete_int32)
|
|
}
|
|
|
|
// toJSONWrapper converts a JSON value (either string or sql.JSONWrapper) to a sql.JSONWrapper.
|
|
func toJSONWrapper(ctx *sql.Context, val any) (sql.JSONWrapper, error) {
|
|
switch v := val.(type) {
|
|
case sql.JSONWrapper:
|
|
return v, nil
|
|
case sql.StringWrapper:
|
|
s, err := v.Unwrap(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
doc, err := pgtypes.JsonB.IoInput(ctx, s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if doc == nil {
|
|
return nil, nil
|
|
}
|
|
w, ok := doc.(sql.JSONWrapper)
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected type from IoInput: %T", doc)
|
|
}
|
|
return w, nil
|
|
case string:
|
|
doc, err := pgtypes.JsonB.IoInput(ctx, v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if doc == nil {
|
|
return nil, nil
|
|
}
|
|
w, ok := doc.(sql.JSONWrapper)
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected type from IoInput: %T", doc)
|
|
}
|
|
return w, nil
|
|
default:
|
|
return nil, fmt.Errorf("unexpected type for JSON operation: %T", val)
|
|
}
|
|
}
|
|
|
|
// jsonWrapperElementToText converts a JSON element (sql.JSONWrapper) to its text representation.
|
|
// For string values, it returns the raw string without quotes. For other types, it returns the JSON representation.
|
|
func jsonWrapperElementToText(ctx *sql.Context, wrapper sql.JSONWrapper) (string, error) {
|
|
v, err := wrapper.ToInterface(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if v == nil {
|
|
return "", nil
|
|
}
|
|
if s, ok := v.(string); ok {
|
|
return s, nil
|
|
}
|
|
return types.JSONDocument{Val: v}.JSONString()
|
|
}
|
|
|
|
// json_array_element represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var json_array_element = framework.Function2{
|
|
Name: "json_array_element",
|
|
Return: pgtypes.Json,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.Json, pgtypes.Int32},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
newVal, err := toJSONWrapper(ctx, val1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if newVal == nil {
|
|
return nil, nil
|
|
}
|
|
var unusedTypes [3]*pgtypes.DoltgresType
|
|
retVal, err := jsonb_array_element_callable(ctx, unusedTypes, newVal, val2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if retVal == nil {
|
|
return nil, nil
|
|
}
|
|
return pgtypes.JsonB.IoOutput(ctx, retVal)
|
|
},
|
|
}
|
|
|
|
// jsonb_array_element represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var jsonb_array_element = framework.Function2{
|
|
Name: "jsonb_array_element",
|
|
Return: pgtypes.JsonB,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.JsonB, pgtypes.Int32},
|
|
Strict: true,
|
|
Callable: jsonb_array_element_callable,
|
|
}
|
|
|
|
func jsonb_array_element_callable(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
wrapper, ok := val1.(sql.JSONWrapper)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
idx := int(val2.(int32))
|
|
// Fast path: for a ComparableJSON wrapper backed by an indexed JSON
|
|
// array, use Lookup to fetch the element without materializing the
|
|
// entire array. Negative indices need the array length, so they fall
|
|
// through to the materialized path even on indexed values.
|
|
if comparable, ok := wrapper.(types.ComparableJSON); ok {
|
|
jt, err := comparable.JsonType(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if jt != "ARRAY" {
|
|
return nil, nil
|
|
}
|
|
if idx >= 0 {
|
|
result, err := types.LookupJSONValue(ctx, wrapper, makeArrayIndexPath(idx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if result == nil {
|
|
return nil, nil
|
|
}
|
|
return result, nil
|
|
}
|
|
}
|
|
// Materialized fallback: covers wrappers that don't implement
|
|
// ComparableJSON (e.g. literal jsonb values) and the negative-index
|
|
// case on ComparableJSON arrays.
|
|
v, err := wrapper.ToInterface(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
array, ok := v.([]interface{})
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
if idx < 0 {
|
|
idx += len(array)
|
|
}
|
|
if idx < 0 || idx >= len(array) {
|
|
return nil, nil
|
|
}
|
|
return types.JSONDocument{Val: array[idx]}, nil
|
|
}
|
|
|
|
// json_object_field represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var json_object_field = framework.Function2{
|
|
Name: "json_object_field",
|
|
Return: pgtypes.Json,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.Json, pgtypes.Text},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
// TODO: make a bespoke implementation that preserves whitespace
|
|
newVal, err := toJSONWrapper(ctx, val1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if newVal == nil {
|
|
return nil, nil
|
|
}
|
|
var unusedTypes [3]*pgtypes.DoltgresType
|
|
retVal, err := jsonb_object_field.Callable(ctx, unusedTypes, newVal, val2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if retVal == nil {
|
|
return nil, nil
|
|
}
|
|
return pgtypes.JsonB.IoOutput(ctx, retVal)
|
|
},
|
|
}
|
|
|
|
// jsonb_object_field represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var jsonb_object_field = framework.Function2{
|
|
Name: "jsonb_object_field",
|
|
Return: pgtypes.JsonB,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.JsonB, pgtypes.Text},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
wrapper, ok := val1.(sql.JSONWrapper)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
key := val2.(string)
|
|
// Fast path: for a ComparableJSON wrapper backed by an indexed JSON
|
|
// object, use Lookup to fetch the value without materializing the
|
|
// entire document.
|
|
if isObj, err := isComparableJsonObject(ctx, wrapper); err != nil {
|
|
return nil, err
|
|
} else if isObj {
|
|
result, err := types.LookupJSONValue(ctx, wrapper, makeObjectKeyPath(key))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if result == nil {
|
|
return nil, nil
|
|
}
|
|
return result, nil
|
|
}
|
|
// Materialized fallback: covers wrappers that don't implement
|
|
// ComparableJSON (e.g. literal jsonb values), where the embedded
|
|
// jsonpath library has trouble with edge cases like keys that
|
|
// contain escaped quotes, or array operands with text paths.
|
|
v, err := wrapper.ToInterface(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
obj, ok := v.(map[string]any)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
val, ok := obj[key]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return types.JSONDocument{Val: val}, nil
|
|
},
|
|
}
|
|
|
|
// json_array_element_text represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var json_array_element_text = framework.Function2{
|
|
Name: "json_array_element_text",
|
|
Return: pgtypes.Text,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.Json, pgtypes.Int32},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
// TODO: make a bespoke implementation that preserves whitespace
|
|
newVal, err := toJSONWrapper(ctx, val1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if newVal == nil {
|
|
return nil, nil
|
|
}
|
|
var unusedTypes [3]*pgtypes.DoltgresType
|
|
return jsonb_array_element_text.Callable(ctx, unusedTypes, newVal, val2)
|
|
},
|
|
}
|
|
|
|
// jsonb_array_element_text represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var jsonb_array_element_text = framework.Function2{
|
|
Name: "jsonb_array_element_text",
|
|
Return: pgtypes.Text,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.JsonB, pgtypes.Int32},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, dt [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
elem, err := jsonb_array_element_callable(ctx, dt, val1, val2)
|
|
if err != nil || elem == nil {
|
|
return nil, err
|
|
}
|
|
wrapper, ok := elem.(sql.JSONWrapper)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return jsonWrapperElementToText(ctx, wrapper)
|
|
},
|
|
}
|
|
|
|
// json_object_field_text represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var json_object_field_text = framework.Function2{
|
|
Name: "json_object_field_text",
|
|
Return: pgtypes.Text,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.Json, pgtypes.Text},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
// TODO: make a bespoke implementation that preserves whitespace
|
|
newVal, err := toJSONWrapper(ctx, val1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if newVal == nil {
|
|
return nil, nil
|
|
}
|
|
var unusedTypes [3]*pgtypes.DoltgresType
|
|
return jsonb_object_field_text.Callable(ctx, unusedTypes, newVal, val2)
|
|
},
|
|
}
|
|
|
|
// jsonb_object_field_text represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var jsonb_object_field_text = framework.Function2{
|
|
Name: "jsonb_object_field_text",
|
|
Return: pgtypes.Text,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.JsonB, pgtypes.Text},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, dt [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
elem, err := jsonb_object_field.Callable(ctx, dt, val1, val2)
|
|
if err != nil || elem == nil {
|
|
return nil, err
|
|
}
|
|
wrapper, ok := elem.(sql.JSONWrapper)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return jsonWrapperElementToText(ctx, wrapper)
|
|
},
|
|
}
|
|
|
|
// json_extract_path represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var json_extract_path = framework.Function2{
|
|
Name: "json_extract_path",
|
|
Return: pgtypes.Json,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.Json, pgtypes.TextArray},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
// TODO: make a bespoke implementation that preserves whitespace
|
|
newVal, err := toJSONWrapper(ctx, val1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if newVal == nil {
|
|
return nil, nil
|
|
}
|
|
var unusedTypes [3]*pgtypes.DoltgresType
|
|
retVal, err := jsonb_extract_path_callable(ctx, unusedTypes, newVal, val2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if retVal == nil {
|
|
return nil, nil
|
|
}
|
|
return pgtypes.JsonB.IoOutput(ctx, retVal)
|
|
},
|
|
}
|
|
|
|
// jsonb_extract_path represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var jsonb_extract_path = framework.Function2{
|
|
Name: "jsonb_extract_path",
|
|
Return: pgtypes.JsonB,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.JsonB, pgtypes.TextArray},
|
|
Strict: true,
|
|
Callable: jsonb_extract_path_callable,
|
|
}
|
|
|
|
func jsonb_extract_path_callable(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
cur, ok := val1.(sql.JSONWrapper)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
paths := val2.([]interface{})
|
|
|
|
// Fast path: for an indexed JSON document, build a single JSON path spanning
|
|
// every step and resolve it in one traversal via SearchableJSON.Lookup,
|
|
if _, ok := cur.(types.SearchableJSON); ok && len(paths) > 0 {
|
|
result, authoritative, err := extractJsonPathBySingleLookup(ctx, cur, paths)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if authoritative {
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
for _, path := range paths {
|
|
if cur == nil {
|
|
return nil, nil
|
|
}
|
|
textPath, ok := path.(string)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
next, err := extractOneJsonPathStep(ctx, cur, textPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if next == nil {
|
|
return nil, nil
|
|
}
|
|
cur = next
|
|
}
|
|
return cur, nil
|
|
}
|
|
|
|
// extractJsonPathBySingleLookup resolves returns the path in a JSON document by building a JSON path expression
|
|
// from the text slice provided.
|
|
// For numeric values, there's ambiguity as to whether they should be interpreted as array indices or object keys,
|
|
// so a nil result in that case is not authoritative and the caller should fall back to the step-by-step walk.
|
|
func extractJsonPathBySingleLookup(ctx *sql.Context, doc sql.JSONWrapper, paths []interface{}) (sql.JSONWrapper, bool, error) {
|
|
var sb strings.Builder
|
|
sb.WriteString("$")
|
|
ambiguous := false
|
|
for _, path := range paths {
|
|
var textPath string
|
|
switch path := path.(type) {
|
|
case string:
|
|
textPath = path
|
|
case sql.StringWrapper:
|
|
var err error
|
|
textPath, err = path.Unwrap(ctx)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
case nil:
|
|
return nil, true, nil
|
|
}
|
|
|
|
if idx, err := strconv.Atoi(textPath); err == nil {
|
|
// An integer element could be an array index or a numeric object
|
|
// key (or a negative index), so the lookup result isn't conclusive.
|
|
ambiguous = true
|
|
if idx >= 0 {
|
|
sb.WriteString(arrayIndexPathSegment(idx))
|
|
continue
|
|
}
|
|
}
|
|
sb.WriteString(objectKeyPathSegment(textPath))
|
|
}
|
|
result, err := types.LookupJSONValue(ctx, doc, sb.String())
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
if result != nil {
|
|
// A non-nil result could still be semantically NULL
|
|
i, err := result.ToInterface(ctx)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
if ia, ok := i.([]interface{}); ok && len(ia) == 0 {
|
|
return nil, !ambiguous, nil
|
|
}
|
|
|
|
return result, true, nil
|
|
}
|
|
|
|
return nil, !ambiguous, nil
|
|
}
|
|
|
|
// extractOneJsonPathStep performs a single Postgres-style extract step against
|
|
// the given JSON wrapper. The text is treated as a key when the wrapper is an
|
|
// object and as an integer index when the wrapper is an array. Returns nil if
|
|
// the step cannot be resolved (missing key, out-of-range index, scalar
|
|
// wrapper, or non-integer text on an array).
|
|
func extractOneJsonPathStep(ctx *sql.Context, cur sql.JSONWrapper, textPath string) (sql.JSONWrapper, error) {
|
|
// Fast path: use the ComparableJSON.JsonType / SearchableJSON.Lookup
|
|
// interfaces to avoid materializing the entire document.
|
|
if comparable, ok := cur.(types.ComparableJSON); ok {
|
|
jt, err := comparable.JsonType(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch jt {
|
|
case "OBJECT":
|
|
return types.LookupJSONValue(ctx, cur, makeObjectKeyPath(textPath))
|
|
case "ARRAY":
|
|
idx, err := strconv.Atoi(textPath)
|
|
if err != nil {
|
|
return nil, nil
|
|
}
|
|
if idx >= 0 {
|
|
return types.LookupJSONValue(ctx, cur, makeArrayIndexPath(idx))
|
|
}
|
|
// Negative indices count from the end and require knowing the
|
|
// array length, so fall through to the materialized path below.
|
|
default:
|
|
return nil, nil
|
|
}
|
|
}
|
|
// Materialized fallback for wrappers that don't implement ComparableJSON,
|
|
// and for negative array indices on ones that do.
|
|
v, err := cur.ToInterface(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch currentValue := v.(type) {
|
|
case map[string]interface{}:
|
|
next, ok := currentValue[textPath]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return types.JSONDocument{Val: next}, nil
|
|
case []interface{}:
|
|
idx, err := strconv.Atoi(textPath)
|
|
if err != nil {
|
|
return nil, nil
|
|
}
|
|
if idx < 0 {
|
|
idx += len(currentValue)
|
|
}
|
|
if idx < 0 || idx >= len(currentValue) {
|
|
return nil, nil
|
|
}
|
|
return types.JSONDocument{Val: currentValue[idx]}, nil
|
|
default:
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
// json_extract_path_text represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var json_extract_path_text = framework.Function2{
|
|
Name: "json_extract_path_text",
|
|
Return: pgtypes.Text,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.Json, pgtypes.TextArray},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
// TODO: make a bespoke implementation that preserves whitespace
|
|
newVal, err := toJSONWrapper(ctx, val1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if newVal == nil {
|
|
return nil, nil
|
|
}
|
|
var unusedTypes [3]*pgtypes.DoltgresType
|
|
return jsonb_extract_path_text_callable(ctx, unusedTypes, newVal, val2)
|
|
},
|
|
}
|
|
|
|
// jsonb_extract_path_text represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var jsonb_extract_path_text = framework.Function2{
|
|
Name: "jsonb_extract_path_text",
|
|
Return: pgtypes.Text,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.JsonB, pgtypes.TextArray},
|
|
Strict: true,
|
|
Callable: jsonb_extract_path_text_callable,
|
|
}
|
|
|
|
func jsonb_extract_path_text_callable(ctx *sql.Context, dt [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
elem, err := jsonb_extract_path_callable(ctx, dt, val1, val2)
|
|
if err != nil || elem == nil {
|
|
return nil, err
|
|
}
|
|
wrapper, ok := elem.(sql.JSONWrapper)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return jsonWrapperElementToText(ctx, wrapper)
|
|
}
|
|
|
|
// jsonb_contains represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var jsonb_contains = framework.Function2{
|
|
Name: "jsonb_contains",
|
|
Return: pgtypes.Bool,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.JsonB, pgtypes.JsonB},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
return nil, errors.Errorf("JSON contains is not yet supported")
|
|
},
|
|
}
|
|
|
|
// jsonb_contained represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var jsonb_contained = framework.Function2{
|
|
Name: "jsonb_contained",
|
|
Return: pgtypes.Bool,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.JsonB, pgtypes.JsonB},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, dt [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
return jsonb_contains.Callable(ctx, dt, val2, val1)
|
|
},
|
|
}
|
|
|
|
// jsonb_exists represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var jsonb_exists = framework.Function2{
|
|
Name: "jsonb_exists",
|
|
Return: pgtypes.Bool,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.JsonB, pgtypes.Text},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
wrapper, ok := val1.(sql.JSONWrapper)
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
key := val2.(string)
|
|
// Fast path: for an indexed JSON object, use Lookup to test for the
|
|
// key without materializing the document.
|
|
if isObj, err := isComparableJsonObject(ctx, wrapper); err != nil {
|
|
return nil, err
|
|
} else if isObj {
|
|
found, err := types.LookupJSONValue(ctx, wrapper, makeObjectKeyPath(key))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return found != nil, nil
|
|
}
|
|
value, err := wrapper.ToInterface(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch v := value.(type) {
|
|
case map[string]interface{}:
|
|
_, ok := v[key]
|
|
return ok, nil
|
|
case []interface{}:
|
|
for _, item := range v {
|
|
if s, ok := item.(string); ok && s == key {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
case string:
|
|
return v == key, nil
|
|
default:
|
|
return false, nil
|
|
}
|
|
},
|
|
}
|
|
|
|
// isComparableJsonObject reports whether the JSON wrapper is a JSON object and
|
|
// also implements types.ComparableJSON
|
|
func isComparableJsonObject(ctx *sql.Context, wrapper sql.JSONWrapper) (bool, error) {
|
|
comparable, ok := wrapper.(types.ComparableJSON)
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
jt, err := comparable.JsonType(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return jt == "OBJECT", nil
|
|
}
|
|
|
|
// jsonb_exists_any represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var jsonb_exists_any = framework.Function2{
|
|
Name: "jsonb_exists_any",
|
|
Return: pgtypes.Bool,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.JsonB, pgtypes.TextArray},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
wrapper, ok := val1.(sql.JSONWrapper)
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
keys := val2.([]interface{})
|
|
// Fast path: for an indexed JSON object, test each key with Lookup
|
|
// instead of materializing the document.
|
|
if isObj, err := isComparableJsonObject(ctx, wrapper); err != nil {
|
|
return nil, err
|
|
} else if isObj {
|
|
for _, key := range keys {
|
|
found, err := types.LookupJSONValue(ctx, wrapper, makeObjectKeyPath(key.(string)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if found != nil {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
value, err := wrapper.ToInterface(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch v := value.(type) {
|
|
case map[string]interface{}:
|
|
for _, key := range keys {
|
|
if _, ok := v[key.(string)]; ok {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
case []interface{}:
|
|
for _, key := range keys {
|
|
for _, item := range v {
|
|
if s, ok := item.(string); ok && s == key.(string) {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
return false, nil
|
|
case string:
|
|
for _, key := range keys {
|
|
if v == key.(string) {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
default:
|
|
return false, nil
|
|
}
|
|
},
|
|
}
|
|
|
|
// jsonb_exists_all represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var jsonb_exists_all = framework.Function2{
|
|
Name: "jsonb_exists_all",
|
|
Return: pgtypes.Bool,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.JsonB, pgtypes.TextArray},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
wrapper, ok := val1.(sql.JSONWrapper)
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
keys := val2.([]interface{})
|
|
// Fast path: for an indexed JSON object, test each key with Lookup
|
|
// instead of materializing the document.
|
|
if isObj, err := isComparableJsonObject(ctx, wrapper); err != nil {
|
|
return nil, err
|
|
} else if isObj {
|
|
for _, key := range keys {
|
|
found, err := types.LookupJSONValue(ctx, wrapper, makeObjectKeyPath(key.(string)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if found == nil {
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
value, err := wrapper.ToInterface(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch v := value.(type) {
|
|
case map[string]interface{}:
|
|
for _, key := range keys {
|
|
if _, ok := v[key.(string)]; !ok {
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
case []interface{}:
|
|
for _, key := range keys {
|
|
found := false
|
|
for _, item := range v {
|
|
if s, ok := item.(string); ok && s == key.(string) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
case string:
|
|
for _, key := range keys {
|
|
if v != key.(string) {
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
default:
|
|
return false, nil
|
|
}
|
|
},
|
|
}
|
|
|
|
// jsonb_delete_text represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var jsonb_delete_text = framework.Function2{
|
|
Name: "jsonb_delete",
|
|
Return: pgtypes.JsonB,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.JsonB, pgtypes.Text},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
return nil, errors.Errorf("JSON deletions are not yet supported")
|
|
},
|
|
}
|
|
|
|
// jsonb_delete_text_array represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var jsonb_delete_text_array = framework.Function2{
|
|
Name: "jsonb_delete",
|
|
Return: pgtypes.JsonB,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.JsonB, pgtypes.TextArray},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
return nil, errors.Errorf("JSON deletions are not yet supported")
|
|
},
|
|
}
|
|
|
|
// jsonb_delete_int32 represents the PostgreSQL function of the same name, taking the same parameters.
|
|
var jsonb_delete_int32 = framework.Function2{
|
|
Name: "jsonb_delete",
|
|
Return: pgtypes.JsonB,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.JsonB, pgtypes.Int32},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) {
|
|
return nil, errors.Errorf("JSON deletions are not yet supported")
|
|
},
|
|
}
|