344 lines
11 KiB
Go
344 lines
11 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 (
|
|
"strings"
|
|
|
|
"github.com/cockroachdb/errors"
|
|
"github.com/dolthub/go-mysql-server/sql"
|
|
|
|
"github.com/dolthub/doltgresql/core"
|
|
"github.com/dolthub/doltgresql/core/id"
|
|
"github.com/dolthub/doltgresql/server/functions/framework"
|
|
pgtypes "github.com/dolthub/doltgresql/server/types"
|
|
"github.com/dolthub/doltgresql/utils"
|
|
)
|
|
|
|
// initArray registers the functions to the catalog.
|
|
func initArray() {
|
|
framework.RegisterFunction(array_in)
|
|
framework.RegisterFunction(array_out)
|
|
framework.RegisterFunction(array_recv)
|
|
framework.RegisterFunction(array_send)
|
|
framework.RegisterFunction(btarraycmp)
|
|
framework.RegisterFunction(array_subscript_handler)
|
|
}
|
|
|
|
// array_in represents the PostgreSQL function of array type IO input.
|
|
var array_in = framework.Function3{
|
|
Name: "array_in",
|
|
Return: pgtypes.AnyArray,
|
|
Parameters: [3]*pgtypes.DoltgresType{pgtypes.Cstring, pgtypes.Oid, pgtypes.Int32},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, _ [4]*pgtypes.DoltgresType, val1, val2, val3 any) (any, error) {
|
|
input := val1.(string)
|
|
baseTypeOid := val2.(id.Id)
|
|
baseType := pgtypes.IDToBuiltInDoltgresType[id.Type(baseTypeOid)]
|
|
if baseType == nil {
|
|
if typColl, err := pgtypes.GetTypesCollectionFromContext(ctx, ""); err == nil && typColl != nil {
|
|
if t, err := typColl.GetType(ctx, id.Type(baseTypeOid)); err == nil {
|
|
baseType = t
|
|
}
|
|
}
|
|
}
|
|
if baseType == nil {
|
|
return nil, errors.Errorf("unknown array element type: %s", string(baseTypeOid))
|
|
}
|
|
typmod := val3.(int32)
|
|
baseType = baseType.WithAttTypMod(typmod)
|
|
if len(input) < 2 || input[0] != '{' || input[len(input)-1] != '}' {
|
|
// This error is regarded as a critical error, and thus we immediately return the error alongside a nil
|
|
// value. Returning a nil value is a signal to not ignore the error.
|
|
return nil, errors.Errorf(`malformed array literal: "%s"`, input)
|
|
}
|
|
// We'll remove the surrounding braces since we've already verified that they're there
|
|
input = input[1 : len(input)-1]
|
|
var values []any
|
|
var err error
|
|
sb := strings.Builder{}
|
|
quoteStartCount := 0
|
|
quoteEndCount := 0
|
|
escaped := false
|
|
// Iterate over each rune in the input to collect and process the rune elements
|
|
for _, r := range input {
|
|
if escaped {
|
|
sb.WriteRune(r)
|
|
escaped = false
|
|
} else if quoteStartCount > quoteEndCount {
|
|
switch r {
|
|
case '\\':
|
|
escaped = true
|
|
case '"':
|
|
quoteEndCount++
|
|
default:
|
|
sb.WriteRune(r)
|
|
}
|
|
} else {
|
|
switch r {
|
|
case ' ', '\t', '\n', '\r':
|
|
continue
|
|
case '\\':
|
|
escaped = true
|
|
case '"':
|
|
quoteStartCount++
|
|
case ',':
|
|
if quoteStartCount >= 2 {
|
|
// This is a malformed string, thus we treat it as a critical error.
|
|
return nil, errors.Errorf(`malformed array literal: "%s"`, input)
|
|
}
|
|
str := sb.String()
|
|
var innerValue any
|
|
if quoteStartCount == 0 && strings.EqualFold(str, "null") {
|
|
// An unquoted case-insensitive NULL is treated as an actual null value
|
|
innerValue = nil
|
|
} else {
|
|
var nErr error
|
|
innerValue, nErr = baseType.IoInput(ctx, str)
|
|
if nErr != nil && err == nil {
|
|
// This is a non-critical error, therefore the error may be ignored at a higher layer (such as
|
|
// an explicit cast) and the inner type will still return a valid result, so we must allow the
|
|
// values to propagate.
|
|
err = nErr
|
|
}
|
|
}
|
|
values = append(values, innerValue)
|
|
sb.Reset()
|
|
quoteStartCount = 0
|
|
quoteEndCount = 0
|
|
default:
|
|
sb.WriteRune(r)
|
|
}
|
|
}
|
|
}
|
|
// Use anything remaining in the buffer as the last element
|
|
if sb.Len() > 0 {
|
|
if escaped || quoteStartCount > quoteEndCount || quoteStartCount >= 2 {
|
|
// These errors are regarded as critical errors, and thus we immediately return the error alongside a nil
|
|
// value. Returning a nil value is a signal to not ignore the error.
|
|
return nil, errors.Errorf(`malformed array literal: "%s"`, input)
|
|
} else {
|
|
str := sb.String()
|
|
var innerValue any
|
|
if quoteStartCount == 0 && strings.EqualFold(str, "NULL") {
|
|
// An unquoted case-insensitive NULL is treated as an actual null value
|
|
innerValue = nil
|
|
} else {
|
|
var nErr error
|
|
innerValue, nErr = baseType.IoInput(ctx, str)
|
|
if nErr != nil && err == nil {
|
|
// This is a non-critical error, therefore the error may be ignored at a higher layer (such as
|
|
// an explicit cast) and the inner type will still return a valid result, so we must allow the
|
|
// values to propagate.
|
|
err = nErr
|
|
}
|
|
}
|
|
values = append(values, innerValue)
|
|
}
|
|
}
|
|
|
|
return values, err
|
|
},
|
|
}
|
|
|
|
// array_out represents the PostgreSQL function of array type IO output.
|
|
var array_out = framework.Function1{
|
|
Name: "array_out",
|
|
Return: pgtypes.Cstring,
|
|
Parameters: [1]*pgtypes.DoltgresType{pgtypes.AnyArray},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, t [2]*pgtypes.DoltgresType, val any) (any, error) {
|
|
arrType := t[0]
|
|
baseType := arrType.ArrayBaseType()
|
|
return pgtypes.ArrToString(ctx, val.([]any), baseType, false)
|
|
},
|
|
}
|
|
|
|
// array_recv represents the PostgreSQL function of array type IO receive.
|
|
var array_recv = framework.Function3{
|
|
Name: "array_recv",
|
|
Return: pgtypes.AnyArray,
|
|
Parameters: [3]*pgtypes.DoltgresType{pgtypes.Internal, pgtypes.Oid, pgtypes.Int32},
|
|
Strict: true,
|
|
Callable: array_recv_callable,
|
|
}
|
|
|
|
// array_recv_callable is the function definition of array_recv.
|
|
func array_recv_callable(ctx *sql.Context, t [4]*pgtypes.DoltgresType, val1, val2, val3 any) (any, error) {
|
|
data := val1.([]byte)
|
|
if data == nil {
|
|
return nil, nil
|
|
}
|
|
typeColl, err := core.GetTypesCollectionFromContext(ctx, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reader := utils.NewWireReader(data)
|
|
dimensions := reader.ReadInt32()
|
|
_ = reader.ReadInt32() // Whether the array has a null, doesn't seem useful
|
|
baseTypeID := id.Type(id.Cache().ToInternal(reader.ReadUint32()))
|
|
baseType, err := typeColl.GetType(ctx, baseTypeID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if baseType == nil {
|
|
return nil, pgtypes.ErrTypeDoesNotExist.New(baseTypeID.TypeName())
|
|
}
|
|
// TODO: handle more than 1 dimension
|
|
if dimensions > 1 {
|
|
return nil, errors.Errorf("array dimensions greater than 1 are not yet supported")
|
|
}
|
|
var vals []any
|
|
for dimensionIdx := int32(0); dimensionIdx < dimensions; dimensionIdx++ {
|
|
elementsCount := reader.ReadInt32()
|
|
_ = reader.ReadInt32() // Lower bound, not sure what to do with this
|
|
for i := int32(0); i < elementsCount; i++ {
|
|
elementLen := reader.ReadInt32()
|
|
if elementLen != -1 {
|
|
valBytes := reader.ReadBytes(uint32(elementLen))
|
|
val, err := baseType.CallReceive(ctx, valBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
vals = append(vals, val)
|
|
} else {
|
|
vals = append(vals, nil)
|
|
}
|
|
}
|
|
}
|
|
return vals, nil
|
|
}
|
|
|
|
// array_send represents the PostgreSQL function of array type IO send.
|
|
var array_send = framework.Function1{
|
|
Name: "array_send",
|
|
Return: pgtypes.Bytea,
|
|
Parameters: [1]*pgtypes.DoltgresType{pgtypes.AnyArray},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, t [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
|
|
}
|
|
}
|
|
vals := val.([]any)
|
|
// Check for nulls first
|
|
hasNull := false
|
|
for _, val := range vals {
|
|
if val == nil {
|
|
hasNull = true
|
|
break
|
|
}
|
|
}
|
|
// Count the number of dimensions
|
|
dimensions := int32(0)
|
|
innerVals := vals
|
|
for len(innerVals) > 0 {
|
|
dimensions++
|
|
slice, ok := innerVals[0].([]any)
|
|
if !ok {
|
|
break
|
|
}
|
|
innerVals = slice
|
|
}
|
|
if dimensions > 1 {
|
|
return nil, errors.Errorf("arrays with %d dimensions are not yet supported using the binary format", dimensions)
|
|
}
|
|
writer := utils.NewWireWriter()
|
|
writer.WriteInt32(dimensions) // Write the number of dimensions
|
|
if hasNull {
|
|
writer.WriteInt32(1)
|
|
} else {
|
|
writer.WriteInt32(0)
|
|
}
|
|
writer.WriteUint32(id.Cache().ToOID(t[0].BaseType().ID.AsId())) // Element OID
|
|
for i := int32(0); i < dimensions; i++ {
|
|
writer.WriteInt32(int32(len(vals))) // Elements in this dimension
|
|
if t[0].IsArrayType() {
|
|
writer.WriteInt32(1) // Lower bound, or what index number we start at (seems to always be 1?)
|
|
} else {
|
|
writer.WriteInt32(0)
|
|
}
|
|
for _, val := range vals {
|
|
if val == nil {
|
|
writer.WriteInt32(-1)
|
|
} else {
|
|
valBytes, err := t[0].BaseType().CallSend(ctx, val)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
writer.WriteInt32(int32(len(valBytes)))
|
|
writer.WriteBytes(valBytes)
|
|
}
|
|
}
|
|
}
|
|
return writer.BufferData(), nil
|
|
},
|
|
}
|
|
|
|
// btarraycmp represents the PostgreSQL function of array type byte compare.
|
|
var btarraycmp = framework.Function2{
|
|
Name: "btarraycmp",
|
|
Return: pgtypes.Int32,
|
|
Parameters: [2]*pgtypes.DoltgresType{pgtypes.AnyArray, pgtypes.AnyArray},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, t [3]*pgtypes.DoltgresType, val1, val2 any) (any, error) {
|
|
at := t[0]
|
|
bt := t[1]
|
|
if !at.Equals(bt) {
|
|
// TODO: currently, types should match.
|
|
// Technically, does not have to e.g.: float4 vs float8
|
|
return nil, errors.Errorf("different type comparison is not supported yet")
|
|
}
|
|
|
|
ab := val1.([]any)
|
|
bb := val2.([]any)
|
|
minLength := utils.Min(len(ab), len(bb))
|
|
for i := 0; i < minLength; i++ {
|
|
res, err := at.ArrayBaseType().Compare(ctx, ab[i], bb[i])
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if res != 0 {
|
|
return res, nil
|
|
}
|
|
}
|
|
if len(ab) == len(bb) {
|
|
return int32(0), nil
|
|
} else if len(ab) < len(bb) {
|
|
return int32(-1), nil
|
|
} else {
|
|
return int32(1), nil
|
|
}
|
|
},
|
|
}
|
|
|
|
// array_subscript_handler represents the PostgreSQL function of array type subscript handler.
|
|
var array_subscript_handler = framework.Function1{
|
|
Name: "array_subscript_handler",
|
|
Return: pgtypes.Internal,
|
|
Parameters: [1]*pgtypes.DoltgresType{pgtypes.Internal},
|
|
Strict: true,
|
|
Callable: func(ctx *sql.Context, t [2]*pgtypes.DoltgresType, val any) (any, error) {
|
|
// TODO
|
|
return []byte{}, nil
|
|
},
|
|
}
|