Files
2026-07-13 12:32:25 +08:00

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
},
}