359 lines
12 KiB
Go
359 lines
12 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 types
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
cerrors "github.com/cockroachdb/errors"
|
|
"github.com/dolthub/go-mysql-server/sql"
|
|
"github.com/dolthub/go-mysql-server/sql/types"
|
|
"github.com/dolthub/vitess/go/vt/proto/query"
|
|
"gopkg.in/src-d/go-errors.v1"
|
|
|
|
"github.com/dolthub/doltgresql/core/id"
|
|
"github.com/dolthub/doltgresql/utils"
|
|
)
|
|
|
|
// ErrTypeAlreadyExists is returned when creating given type when it already exists.
|
|
var ErrTypeAlreadyExists = errors.NewKind(`type "%s" already exists`)
|
|
|
|
// ErrTypeDoesNotExist is returned when using given type that does not exist.
|
|
var ErrTypeDoesNotExist = errors.NewKind(`type "%s" does not exist`)
|
|
|
|
// ErrUnhandledType is returned when the type of value does not match given type.
|
|
var ErrUnhandledType = errors.NewKind(`%s: unhandled type: %T`)
|
|
|
|
// ErrInvalidSyntaxForType is returned when the type of value is invalid for given type.
|
|
var ErrInvalidSyntaxForType = errors.NewKind(`invalid input syntax for type %s: %q`)
|
|
|
|
// ErrValueIsOutOfRangeForType is returned when the value is out-of-range for given type.
|
|
var ErrValueIsOutOfRangeForType = errors.NewKind(`value %q is out of range for type %s`)
|
|
|
|
// ErrTypmodArrayMustBe1D is returned when type modifier value is empty array.
|
|
var ErrTypmodArrayMustBe1D = errors.NewKind(`typmod array must be one-dimensional`)
|
|
|
|
// ErrInvalidTypMod is returned when given value is invalid for type modifier.
|
|
var ErrInvalidTypMod = errors.NewKind(`invalid %s type modifier`)
|
|
|
|
// ErrCannotDropSystemType is returned when given type is system/pg_catalog type.
|
|
var ErrCannotDropSystemType = errors.NewKind(`cannot drop type %s because it is required by the database system`)
|
|
|
|
// ErrCannotDropArrayType is returned when given type to drop is array type that is required for its base type.
|
|
var ErrCannotDropArrayType = errors.NewKind(`cannot drop type %s because type %s requires it`)
|
|
|
|
// TypeCollection is an interface from the core package, redeclared here to get around import cycles.
|
|
type TypeCollection interface {
|
|
GetType(context.Context, id.Type) (*DoltgresType, error)
|
|
ResolveType(ctx context.Context, name id.Type) (*DoltgresType, error)
|
|
WithCachedType(typeToCache *DoltgresType, f func())
|
|
}
|
|
|
|
// Cast is an interface from the core package, redeclared here to get around import cycles.
|
|
type Cast interface {
|
|
Eval(ctx *sql.Context, val any, sourceType *DoltgresType, targetType *DoltgresType) (any, error)
|
|
}
|
|
|
|
// CastsCollection is an interface from the core package, redeclared here to get around import cycles.
|
|
type CastsCollection interface {
|
|
GetExplicitCast(ctx *sql.Context, sourceType *DoltgresType, targetType *DoltgresType) (Cast, error)
|
|
GetAssignmentCast(ctx *sql.Context, sourceType *DoltgresType, targetType *DoltgresType) (Cast, error)
|
|
GetImplicitCast(ctx *sql.Context, sourceType *DoltgresType, targetType *DoltgresType) (Cast, error)
|
|
}
|
|
|
|
// GetTypesCollectionFromContext is a function from the core package, redeclared here to get around import cycles.
|
|
var GetTypesCollectionFromContext func(*sql.Context, string) (TypeCollection, error)
|
|
|
|
// GetCastsCollectionFromContext is a function from the core package, redeclared here to get around import cycles.
|
|
var GetCastsCollectionFromContext func(*sql.Context, string) (CastsCollection, error)
|
|
|
|
// FromGmsType returns a DoltgresType that is most similar to the given GMS type.
|
|
// It returns UNKNOWN type for GMS types that are not handled.
|
|
func FromGmsType(typ sql.Type) *DoltgresType {
|
|
dt, err := FromGmsTypeToDoltgresType(typ)
|
|
if err != nil {
|
|
return Unknown
|
|
}
|
|
return dt
|
|
}
|
|
|
|
// FromGmsTypeToDoltgresType returns a DoltgresType that is most similar to the given GMS type.
|
|
// It errors if GMS type is not handled.
|
|
func FromGmsTypeToDoltgresType(typ sql.Type) (*DoltgresType, error) {
|
|
switch typ.Type() {
|
|
case query.Type_INT8:
|
|
// Special treatment for boolean types when we can detect them
|
|
if typ == types.Boolean {
|
|
return Bool, nil
|
|
}
|
|
return Int32, nil
|
|
case query.Type_INT16, query.Type_INT24, query.Type_INT32, query.Type_YEAR:
|
|
return Int32, nil
|
|
case query.Type_INT64, query.Type_BIT, query.Type_UINT8, query.Type_UINT16, query.Type_UINT24, query.Type_UINT32:
|
|
return Int64, nil
|
|
case query.Type_UINT64:
|
|
return Numeric, nil
|
|
case query.Type_FLOAT32:
|
|
return Float32, nil
|
|
case query.Type_FLOAT64:
|
|
return Float64, nil
|
|
case query.Type_DECIMAL:
|
|
return Numeric, nil
|
|
case query.Type_DATE:
|
|
return Date, nil
|
|
case query.Type_TIME:
|
|
return Text, nil
|
|
case query.Type_DATETIME, query.Type_TIMESTAMP:
|
|
return Timestamp, nil
|
|
case query.Type_CHAR, query.Type_VARCHAR, query.Type_TEXT, query.Type_BINARY, query.Type_VARBINARY, query.Type_BLOB, query.Type_SET, query.Type_ENUM:
|
|
return Text, nil
|
|
case query.Type_JSON:
|
|
return Json, nil
|
|
case query.Type_NULL_TYPE, query.Type_EXPRESSION, query.Type_GEOMETRY:
|
|
return Unknown, nil
|
|
default:
|
|
return nil, cerrors.Errorf("encountered a GMS type that cannot be handled: %d", int32(typ.Type()))
|
|
}
|
|
}
|
|
|
|
// serializedStringCompare handles the efficient comparison of two strings that have been serialized using utils.Writer.
|
|
// The writer writes the string by prepending the string length, which prevents direct comparison of the byte slices. We
|
|
// thus read the string length manually, and extract the byte slices without converting to a string. This function
|
|
// assumes that neither byte slice is nil nor empty.
|
|
func serializedStringCompare(v1 []byte, v2 []byte) int {
|
|
readerV1 := utils.NewReader(v1)
|
|
readerV2 := utils.NewReader(v2)
|
|
v1Bytes := utils.AdvanceReader(readerV1, readerV1.VariableUint())
|
|
v2Bytes := utils.AdvanceReader(readerV2, readerV2.VariableUint())
|
|
return bytes.Compare(v1Bytes, v2Bytes)
|
|
}
|
|
|
|
// sqlString converts given type value to output string. This is the same as IoOutput function
|
|
// with an exception to BOOLEAN type. It returns "t" instead of "true".
|
|
func sqlString(ctx *sql.Context, t *DoltgresType, val any) (string, error) {
|
|
if t.IsArrayType() {
|
|
baseType := t.ArrayBaseType()
|
|
return ArrToString(ctx, val.([]any), baseType, true)
|
|
} else if t.ID == Bool.ID {
|
|
if val.(bool) {
|
|
return "t", nil
|
|
} else {
|
|
return "f", nil
|
|
}
|
|
}
|
|
return t.IoOutput(ctx, val)
|
|
}
|
|
|
|
// ArrToString is used for array_out function. |trimBool| parameter allows replacing
|
|
// boolean result of "true" to "t" if the function is `Type.SQL()`.
|
|
func ArrToString(ctx *sql.Context, arr []any, baseType *DoltgresType, trimBool bool) (string, error) {
|
|
sb := strings.Builder{}
|
|
sb.WriteRune('{')
|
|
for i, v := range arr {
|
|
if i > 0 {
|
|
sb.WriteString(",")
|
|
}
|
|
if v != nil {
|
|
str, err := baseType.IoOutput(ctx, v)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if baseType.ID == Bool.ID && trimBool {
|
|
str = string(str[0])
|
|
}
|
|
sb.WriteString(quoteString(str))
|
|
} else {
|
|
sb.WriteString("NULL")
|
|
}
|
|
}
|
|
sb.WriteRune('}')
|
|
return sb.String(), nil
|
|
}
|
|
|
|
// RecordToString is used for the record_out function, to serialize record values for wire transfer.
|
|
// |fields| contains the values to serialize.
|
|
func RecordToString(ctx *sql.Context, fields []RecordValue) (any, error) {
|
|
sb := strings.Builder{}
|
|
sb.WriteRune('(')
|
|
for i, value := range fields {
|
|
if i > 0 {
|
|
sb.WriteString(",")
|
|
}
|
|
|
|
if value.Value == nil {
|
|
continue
|
|
}
|
|
|
|
doltgresType, ok := value.Type.(*DoltgresType)
|
|
if !ok {
|
|
return nil, fmt.Errorf(`expected *DoltgresType but found: %T`, value.Type)
|
|
}
|
|
|
|
str, err := doltgresType.IoOutput(ctx, value.Value)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if doltgresType.ID == Bool.ID {
|
|
str = string(str[0])
|
|
}
|
|
|
|
sb.WriteString(quoteString(str))
|
|
}
|
|
sb.WriteRune(')')
|
|
|
|
return sb.String(), nil
|
|
}
|
|
|
|
// VectorToString is used for *vectorout functions, to serialize vector values for wire transfer.
|
|
func VectorToString(ctx *sql.Context, arr []any, baseType *DoltgresType) (string, error) {
|
|
sb := strings.Builder{}
|
|
for i, v := range arr {
|
|
if i > 0 {
|
|
sb.WriteString(" ")
|
|
}
|
|
if v != nil {
|
|
str, err := baseType.IoOutput(ctx, v)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
sb.WriteString(quoteString(str))
|
|
} else {
|
|
sb.WriteString("NULL")
|
|
}
|
|
}
|
|
return sb.String(), nil
|
|
}
|
|
|
|
// ParseCompositeLiteral parses a Postgres composite literal string like "(1,2,hello)" into a slice
|
|
// of RecordValues using the field types from |compositeType|.
|
|
func ParseCompositeLiteral(ctx *sql.Context, input string, compositeType *DoltgresType) ([]RecordValue, error) {
|
|
if len(input) < 2 || input[0] != '(' || input[len(input)-1] != ')' {
|
|
return nil, cerrors.Errorf(`malformed composite literal: "%s"`, input)
|
|
}
|
|
inner := input[1 : len(input)-1]
|
|
|
|
attrs := compositeType.CompositeAttrs
|
|
if len(attrs) == 0 {
|
|
return []RecordValue{}, nil
|
|
}
|
|
|
|
fieldStrs, err := parseCompositeLiteralFields(inner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(fieldStrs) != len(attrs) {
|
|
return nil, cerrors.Errorf("composite literal has %d fields but type %s has %d",
|
|
len(fieldStrs), compositeType.ID.TypeName(), len(attrs))
|
|
}
|
|
|
|
values := make([]RecordValue, len(attrs))
|
|
for i, attr := range attrs {
|
|
if fieldStrs[i] == nil {
|
|
values[i] = RecordValue{Value: nil, Type: attr.Type}
|
|
} else {
|
|
val, err := attr.Type.IoInput(ctx, *fieldStrs[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
values[i] = RecordValue{Value: val, Type: attr.Type}
|
|
}
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
// parseCompositeLiteralFields splits the inner part of a composite literal (with parens already
|
|
// removed) into per-field string pointers. A nil pointer indicates a SQL NULL value.
|
|
func parseCompositeLiteralFields(input string) ([]*string, error) {
|
|
if len(input) == 0 {
|
|
return []*string{}, nil
|
|
}
|
|
var fields []*string
|
|
i := 0
|
|
for {
|
|
if i >= len(input) {
|
|
fields = append(fields, nil)
|
|
break
|
|
}
|
|
if input[i] == '"' {
|
|
// Double-quoted field value
|
|
i++ // skip opening quote
|
|
var sb strings.Builder
|
|
for i < len(input) && input[i] != '"' {
|
|
if input[i] == '\\' && i+1 < len(input) {
|
|
i++
|
|
sb.WriteByte(input[i])
|
|
} else {
|
|
sb.WriteByte(input[i])
|
|
}
|
|
i++
|
|
}
|
|
if i >= len(input) {
|
|
return nil, cerrors.Errorf("unterminated quoted string in composite literal")
|
|
}
|
|
i++ // skip closing quote
|
|
s := sb.String()
|
|
fields = append(fields, &s)
|
|
} else {
|
|
// Unquoted field value — read until next comma
|
|
start := i
|
|
for i < len(input) && input[i] != ',' {
|
|
i++
|
|
}
|
|
fieldStr := input[start:i]
|
|
if fieldStr == "" || strings.EqualFold(fieldStr, "NULL") {
|
|
fields = append(fields, nil)
|
|
} else {
|
|
s := fieldStr
|
|
fields = append(fields, &s)
|
|
}
|
|
}
|
|
|
|
if i >= len(input) {
|
|
break
|
|
}
|
|
if input[i] != ',' {
|
|
return nil, cerrors.Errorf("expected ',' in composite literal, got '%c'", input[i])
|
|
}
|
|
i++ // skip comma
|
|
}
|
|
return fields, nil
|
|
}
|
|
|
|
// quoteString determines if |s| needs to be quoted, by looking for special characters like ' ' or ',',
|
|
// and if so, quotes the string and returns it. If quoting is not needed, then |s| is returned as is.
|
|
func quoteString(s string) string {
|
|
shouldQuote := false
|
|
for _, r := range s {
|
|
switch r {
|
|
case ' ', ',', '{', '}', '\\', '"':
|
|
shouldQuote = true
|
|
}
|
|
}
|
|
if shouldQuote || strings.EqualFold(s, "NULL") || len(s) == 0 {
|
|
return fmt.Sprintf(`"%s"`, strings.ReplaceAll(s, `"`, `\"`))
|
|
} else {
|
|
return s
|
|
}
|
|
}
|
|
|
|
// toInternal returns an Internal ID for the given type. This is only used for the built-in types, since they all share
|
|
// the same schema (pg_catalog).
|
|
func toInternal(typeName string) id.Type {
|
|
return id.NewType("pg_catalog", typeName)
|
|
}
|