Files
dolthub--doltgresql/core/casts/collection.go
T
2026-07-13 12:32:25 +08:00

713 lines
24 KiB
Go

// Copyright 2026 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 casts
import (
"context"
"fmt"
"io"
"strings"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/store/hash"
"github.com/dolthub/dolt/go/store/prolly"
"github.com/dolthub/dolt/go/store/prolly/tree"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/expression"
"github.com/dolthub/go-mysql-server/sql/procedures"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// Collection contains a collection of casts.
type Collection struct {
mapHash hash.Hash // This is cached so that we don't have to calculate the hash every time
underlyingMap prolly.AddressMap
ns tree.NodeStore
}
// CastType is the type of the cast, indicating which contexts it may be called in.
type CastType uint8
const (
CastType_Explicit CastType = 0
CastType_Assignment CastType = 1
CastType_Implicit CastType = 2
)
// builtInCasts contains all casts that are built into the database by default.
var builtInCasts = map[id.Cast]Cast{}
// Cast represents a cast between two types.
type Cast struct {
ID id.Cast
CastType CastType
Function id.Function
BuiltIn pgtypes.TypeCastFunction
UseInOut bool
request CastType // This contains the type of cast that was requested, as we may request an EXPLICIT cast and receive an IMPLICIT (which is valid)
}
var _ objinterface.Collection = (*Collection)(nil)
var _ objinterface.RootObject = Cast{}
// NewCollection returns a new Collection.
func NewCollection(ctx context.Context, underlyingMap prolly.AddressMap, ns tree.NodeStore) (*Collection, error) {
collection := &Collection{
mapHash: underlyingMap.HashOf(),
underlyingMap: underlyingMap,
ns: ns,
}
return collection, nil
}
// GetExplicitCast returns the explicit type cast function that will cast the source type to the target type. Returns
// a Cast with an invalid ID if such a cast is not valid.
func (pgc *Collection) GetExplicitCast(ctx *sql.Context, sourceType *pgtypes.DoltgresType, targetType *pgtypes.DoltgresType) (Cast, error) {
castID := id.NewCast(sourceType.ID, targetType.ID)
c, err := pgc.getCast(ctx, castID, sourceType, targetType, CastType_Explicit)
if err != nil {
return Cast{}, err
}
if c.ID.IsValid() {
return c, nil
}
// We check for the identity and sizing casts after checking the maps, as the identity may be overridden by a user.
if cast := pgc.getSizingOrIdentityCast(castID, sourceType, targetType, CastType_Explicit); cast.ID.IsValid() {
return cast, nil
}
// We then check for a record to composite cast
if recordCast := pgc.getRecordCast(sourceType, targetType, CastType_Explicit); recordCast.ID.IsValid() {
return recordCast, nil
}
// All types have a built-in explicit cast from string types: https://www.postgresql.org/docs/15/sql-createcast.html
if sourceType.TypCategory == pgtypes.TypeCategory_StringTypes {
return Cast{
ID: castID,
CastType: CastType_Explicit,
Function: id.NullFunction,
BuiltIn: nil,
UseInOut: true,
request: CastType_Explicit,
}, nil
} else if targetType.TypCategory == pgtypes.TypeCategory_StringTypes {
// All types have a built-in assignment cast to string types, which we can reference in an explicit cast
return Cast{
ID: castID,
CastType: CastType_Explicit,
Function: id.NullFunction,
BuiltIn: nil,
UseInOut: true,
request: CastType_Explicit,
}, nil
}
// It is always valid to convert from the `unknown` type
if sourceType.ID == pgtypes.Unknown.ID {
return Cast{
ID: castID,
CastType: CastType_Explicit,
Function: id.NullFunction,
BuiltIn: nil,
UseInOut: true,
request: CastType_Explicit,
}, nil
}
return Cast{}, nil
}
// GetAssignmentCast returns the assignment type cast function that will cast the source type to the target type.
// Returns a Cast with an invalid ID if such a cast is not valid.
func (pgc *Collection) GetAssignmentCast(ctx *sql.Context, sourceType *pgtypes.DoltgresType, targetType *pgtypes.DoltgresType) (Cast, error) {
castID := id.NewCast(sourceType.ID, targetType.ID)
c, err := pgc.getCast(ctx, castID, sourceType, targetType, CastType_Assignment)
if err != nil {
return Cast{}, err
}
if c.ID.IsValid() {
if c.CastType == CastType_Explicit {
return Cast{}, nil
}
return c, nil
}
// We check for the identity and sizing casts after checking the maps, as the identity may be overridden by a user.
if cast := pgc.getSizingOrIdentityCast(castID, sourceType, targetType, CastType_Assignment); cast.ID.IsValid() {
return cast, nil
}
// We then check for a record to composite cast
if recordCast := pgc.getRecordCast(sourceType, targetType, CastType_Assignment); recordCast.ID.IsValid() {
return recordCast, nil
}
// All types have a built-in assignment cast to string types: https://www.postgresql.org/docs/15/sql-createcast.html
if targetType.TypCategory == pgtypes.TypeCategory_StringTypes {
return Cast{
ID: castID,
CastType: CastType_Assignment,
Function: id.NullFunction,
BuiltIn: nil,
UseInOut: true,
request: CastType_Assignment,
}, nil
}
// It is always valid to convert from the `unknown` type
if sourceType.ID == pgtypes.Unknown.ID {
return Cast{
ID: castID,
CastType: CastType_Assignment,
Function: id.NullFunction,
BuiltIn: nil,
UseInOut: true,
request: CastType_Assignment,
}, nil
}
return Cast{}, nil
}
// GetImplicitCast returns the implicit type cast function that will cast the source type to the target type. Returns a
// Cast with an invalid ID if such a cast is not valid.
func (pgc *Collection) GetImplicitCast(ctx *sql.Context, sourceType *pgtypes.DoltgresType, targetType *pgtypes.DoltgresType) (Cast, error) {
castID := id.NewCast(sourceType.ID, targetType.ID)
c, err := pgc.getCast(ctx, castID, sourceType, targetType, CastType_Implicit)
if err != nil {
return Cast{}, err
}
if c.ID.IsValid() {
if c.CastType == CastType_Implicit {
return c, nil
}
return Cast{}, nil
}
// We check for the identity and sizing casts after checking the maps, as the identity may be overridden by a user.
if cast := pgc.getSizingOrIdentityCast(castID, sourceType, targetType, CastType_Implicit); cast.ID.IsValid() {
return cast, nil
}
// We then check for a record to composite cast
if recordCast := pgc.getRecordCast(sourceType, targetType, CastType_Implicit); recordCast.ID.IsValid() {
return recordCast, nil
}
// It is always valid to convert from the `unknown` type
if sourceType.ID == pgtypes.Unknown.ID {
return Cast{
ID: castID,
CastType: CastType_Implicit,
Function: id.NullFunction,
BuiltIn: nil,
UseInOut: true,
request: CastType_Implicit,
}, nil
}
return Cast{}, nil
}
// getCast is used by each individual Get function to handle the actual fetching of the cast.
func (pgc *Collection) getCast(ctx context.Context, castID id.Cast, sourceType *pgtypes.DoltgresType, targetType *pgtypes.DoltgresType, castType CastType) (Cast, error) {
if c, ok := builtInCasts[castID]; ok {
return c, nil
}
h, err := pgc.underlyingMap.Get(ctx, string(castID))
if err != nil {
return Cast{}, err
}
if h.IsEmpty() {
// If there isn't a direct mapping, then we need to check if the types are array variants.
// As long as the base types are convertable, the array variants are also convertable.
if sourceType != nil && targetType != nil && sourceType.IsArrayType() && targetType.IsArrayType() {
sqlCtx, ok := ctx.(*sql.Context)
if !ok {
return Cast{}, fmt.Errorf("non *sql.Context provided to Collection.getCast()")
}
fromBaseType := sourceType.ArrayBaseType()
toBaseType := targetType.ArrayBaseType()
var baseCast Cast
switch castType {
case CastType_Explicit:
baseCast, err = pgc.GetExplicitCast(sqlCtx, fromBaseType, toBaseType)
if err != nil {
return Cast{}, err
}
case CastType_Assignment:
baseCast, err = pgc.GetAssignmentCast(sqlCtx, fromBaseType, toBaseType)
if err != nil {
return Cast{}, err
}
case CastType_Implicit:
baseCast, err = pgc.GetImplicitCast(sqlCtx, fromBaseType, toBaseType)
if err != nil {
return Cast{}, err
}
}
if baseCast.ID.IsValid() {
// We use a closure that can unwrap the slice, since conversion functions expect a singular non-nil value
evalFunc := func(ctx *sql.Context, vals any, sourceType *pgtypes.DoltgresType, targetType *pgtypes.DoltgresType) (any, error) {
var err error
oldVals := vals.([]any)
newVals := make([]any, len(oldVals))
for i, oldVal := range oldVals {
if oldVal == nil {
continue
}
// Some errors are optional depending on the context, so we'll still process all values even
// after an error is received.
var nErr error
sourceBaseType := sourceType.ArrayBaseType()
targetBaseType := targetType.ArrayBaseType()
newVals[i], nErr = baseCast.Eval(ctx, oldVal, sourceBaseType, targetBaseType)
if nErr != nil && err == nil {
err = nErr
}
}
return newVals, err
}
return Cast{
ID: castID,
CastType: castType,
Function: id.NullFunction,
BuiltIn: evalFunc,
UseInOut: false,
request: castType,
}, nil
}
}
return Cast{}, nil
}
data, err := pgc.ns.ReadBytes(ctx, h)
if err != nil {
return Cast{}, err
}
c, err := DeserializeCast(ctx, data)
if err != nil {
return Cast{}, err
}
c.request = castType
return c, nil
}
// getSizingOrIdentityCast returns an identity cast if the two types are exactly the same, and a sizing cast if they
// only differ in their atttypmod values. Returns a Cast with an invalid ID if no cast is matched. This mirrors the
// behavior as described in:
// https://www.postgresql.org/docs/15/typeconv-query.html
func (pgc *Collection) getSizingOrIdentityCast(castID id.Cast, sourceType *pgtypes.DoltgresType, targetType *pgtypes.DoltgresType, castType CastType) Cast {
// If we receive different types, then we can return immediately
if sourceType.ID != targetType.ID {
return Cast{}
}
// If we have different atttypmod values, then we need to do a sizing cast only if one exists
// Otherwise, then we simply use the identity cast
// TODO: We don't have any sizing cast functions implemented, so for now we'll approximate using output to input.
// We can use the query below to find all implemented sizing cast functions. It's also detailed in the link above.
// Lastly, not all sizing functions accept a boolean, but for those that do, we need to see whether true is
// used for explicit casts, or whether true is used for implicit casts.
// SELECT
// format_type(c.castsource, NULL) AS source,
// format_type(c.casttarget, NULL) AS target,
// p.oid::regprocedure AS func
// FROM pg_cast c JOIN pg_proc p ON p.oid = c.castfunc WHERE c.castsource = c.casttarget ORDER BY 1,2;
useInOut := sourceType.GetAttTypMod() != targetType.GetAttTypMod()
return Cast{
ID: castID,
CastType: castType,
Function: id.NullFunction,
BuiltIn: nil,
UseInOut: useInOut,
request: castType,
}
}
// getRecordCast handles casting from a record type to a composite type (if applicable). Returns a Cast with an invalid
// ID if not applicable.
func (pgc *Collection) getRecordCast(sourceType *pgtypes.DoltgresType, targetType *pgtypes.DoltgresType, castType CastType) Cast {
// TODO: does casting to a record type always work for any composite type?
// https://www.postgresql.org/docs/15/sql-expressions.html#SQL-SYNTAX-ROW-CONSTRUCTORS seems to suggest so
// Also not sure if we should use the passthrough, or if we always default to implicit, assignment, or explicit
if sourceType.IsRecordType() && targetType.IsCompositeType() {
// When casting to a composite type, then we must match the arity and have valid casts for every position.
if targetType.IsRecordType() {
return Cast{
ID: id.NewCast(sourceType.ID, targetType.ID),
CastType: castType,
Function: id.NullFunction,
BuiltIn: nil,
UseInOut: false,
request: castType,
}
} else {
evalFunc := func(ctx *sql.Context, val any, sourceType *pgtypes.DoltgresType, targetType *pgtypes.DoltgresType) (_ any, err error) {
vals, ok := val.([]pgtypes.RecordValue)
if !ok {
return nil, errors.New("casting input error from record type")
}
if len(targetType.CompositeAttrs) != len(vals) {
// TODO: these should go in DETAIL depending on the size
// Input has too few columns.
// Input has too many columns.
return nil, errors.Errorf("cannot cast type %s to %s", sourceType.Name(), targetType.Name())
}
outputVals := make([]pgtypes.RecordValue, len(vals))
for i := range vals {
valType, ok := vals[i].Type.(*pgtypes.DoltgresType)
if !ok {
return nil, errors.New("cannot cast record containing GMS type")
}
outputType := targetType.CompositeAttrs[i].Type
outputVals[i].Type = outputType
if vals[i].Value != nil {
var positionCast Cast
switch castType {
case CastType_Explicit:
positionCast, err = pgc.GetExplicitCast(ctx, valType, outputType)
if err != nil {
return nil, err
}
case CastType_Assignment:
positionCast, err = pgc.GetAssignmentCast(ctx, valType, outputType)
if err != nil {
return nil, err
}
case CastType_Implicit:
positionCast, err = pgc.GetImplicitCast(ctx, valType, outputType)
if err != nil {
return nil, err
}
}
if !positionCast.ID.IsValid() {
// TODO: this should be the DETAIL, with the actual error being "cannot cast type <FROM_TYPE> to <TO_TYPE>"
return nil, errors.Errorf("Cannot cast type %s to %s in column %d", valType.Name(), outputType.Name(), i+1)
}
outputVals[i].Value, err = positionCast.Eval(ctx, vals[i].Value, valType, outputType)
if err != nil {
return nil, err
}
}
}
return outputVals, nil
}
return Cast{
ID: id.NewCast(sourceType.ID, targetType.ID),
CastType: castType,
Function: id.NullFunction,
BuiltIn: evalFunc,
UseInOut: false,
request: castType,
}
}
}
return Cast{}
}
// HasCast returns whether the given cast exists.
func (pgc *Collection) HasCast(ctx context.Context, castID id.Cast) bool {
if _, ok := builtInCasts[castID]; ok {
return true
}
ok, err := pgc.underlyingMap.Has(ctx, string(castID))
if err == nil && ok {
return true
}
return false
}
// AddCast adds a new cast.
func (pgc *Collection) AddCast(ctx context.Context, cast Cast) error {
// First we'll check to see if it exists
if pgc.HasCast(ctx, cast.ID) {
return errors.Errorf(`cast from type %s to type %s already exists`,
cast.ID.SourceType().TypeName(), cast.ID.TargetType().TypeName())
}
if cast.BuiltIn != nil {
return errors.Errorf(`cannot create a built-in cast from type %s to type %s`,
cast.ID.SourceType().TypeName(), cast.ID.TargetType().TypeName())
}
// Now we'll add the cast to our map
data, err := cast.Serialize(ctx)
if err != nil {
return err
}
h, err := pgc.ns.WriteBytes(ctx, data)
if err != nil {
return err
}
mapEditor := pgc.underlyingMap.Editor()
if err = mapEditor.Add(ctx, string(cast.ID), h); err != nil {
return err
}
newMap, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pgc.underlyingMap = newMap
pgc.mapHash = pgc.underlyingMap.HashOf()
return nil
}
// DropCast drops an existing cast.
func (pgc *Collection) DropCast(ctx context.Context, castIDs ...id.Cast) error {
if len(castIDs) == 0 {
return nil
}
// Check that each name exists before performing any deletions
for _, castID := range castIDs {
if _, ok := builtInCasts[castID]; ok {
return errors.Errorf(`cannot delete built-in cast from type %s to type %s`,
castID.SourceType().TypeName(), castID.TargetType().TypeName())
}
if ok, err := pgc.underlyingMap.Has(ctx, string(castID)); err != nil {
return err
} else if !ok {
return errors.Errorf(`cast from type %s to type %s does not exist`,
castID.SourceType().TypeName(), castID.TargetType().TypeName())
}
}
// Now we'll remove the casts from the map
mapEditor := pgc.underlyingMap.Editor()
for _, castID := range castIDs {
err := mapEditor.Delete(ctx, string(castID))
if err != nil {
return err
}
}
newMap, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pgc.underlyingMap = newMap
pgc.mapHash = pgc.underlyingMap.HashOf()
return nil
}
// resolveName returns the fully resolved name of the given cast. Returns an error if the name is ambiguous.
func (pgc *Collection) resolveName(ctx context.Context, schemaName string, formattedName string) (id.Cast, error) {
if len(formattedName) == 0 {
return id.NullCast, nil
}
// Check for an exact match
fullID := pgc.tableNameToID(schemaName, formattedName)
if pgc.HasCast(ctx, fullID) {
return fullID, nil
}
// Otherwise we'll iterate over all the names
var resolvedID id.Cast
err := pgc.IterateCasts(ctx, func(c Cast) (stop bool, err error) {
if !strings.EqualFold(string(c.ID), string(fullID)) {
return false, nil
}
// The above matches, so this counts as a match
if resolvedID.IsValid() {
castTableName := CastIDToTableName(c.ID)
resolvedTableName := CastIDToTableName(resolvedID)
return true, fmt.Errorf("`%s` is ambiguous, matches `%s` and `%s`",
formattedName, castTableName.String(), resolvedTableName.String())
}
resolvedID = c.ID
return false, nil
})
return resolvedID, err
}
// IterateCasts iterates over all casts in the collection.
func (pgc *Collection) IterateCasts(ctx context.Context, callback func(c Cast) (stop bool, err error)) error {
for _, cast := range builtInCasts {
stop, err := callback(cast)
if err != nil {
return err
} else if stop {
return nil
}
}
return pgc.underlyingMap.IterAll(ctx, func(_ string, v hash.Hash) error {
data, err := pgc.ns.ReadBytes(ctx, v)
if err != nil {
return err
}
c, err := DeserializeCast(ctx, data)
if err != nil {
return err
}
stop, err := callback(c)
if err != nil {
return err
} else if stop {
return io.EOF
} else {
return nil
}
})
}
// Clone returns a new *Collection with the same contents as the original.
func (pgc *Collection) Clone(ctx context.Context) *Collection {
return &Collection{
mapHash: pgc.mapHash,
underlyingMap: pgc.underlyingMap,
ns: pgc.ns,
}
}
// Map writes any cached sequences to the underlying map, and then returns the underlying map.
func (pgc *Collection) Map(ctx context.Context) (prolly.AddressMap, error) {
return pgc.underlyingMap, nil
}
// tableNameToID returns the ID that was encoded via the Name() call, as the returned TableName contains additional
// information (which this is able to process).
func (pgc *Collection) tableNameToID(schemaName string, formattedName string) id.Cast {
sections := strings.Split(strings.TrimSuffix(strings.TrimPrefix(formattedName, "("), ")"), ")|(")
if len(sections) != 4 {
return id.NullCast
}
return id.NewCast(id.NewType(sections[0], sections[1]), id.NewType(sections[2], sections[3]))
}
// GetID implements the interface objinterface.RootObject.
func (cast Cast) GetID() id.Id {
return cast.ID.AsId()
}
// DiffersFrom returns true when the hash that is associated with the underlying map for this collection is different
// from the hash in the given root.
func (pgc *Collection) DiffersFrom(ctx context.Context, root objinterface.RootValue) bool {
hashOnGivenRoot, err := pgc.LoadCollectionHash(ctx, root)
if err != nil {
return true
}
if pgc.mapHash.Equal(hashOnGivenRoot) {
return false
}
// An empty map should match an uninitialized collection on the root
count, err := pgc.underlyingMap.Count()
if err == nil && count == 0 && hashOnGivenRoot.IsEmpty() {
return false
}
return true
}
// GetRootObjectID implements the interface objinterface.RootObject.
func (cast Cast) GetRootObjectID() objinterface.RootObjectID {
return objinterface.RootObjectID_Casts
}
// HashOf implements the interface objinterface.RootObject.
func (cast Cast) HashOf(ctx context.Context) (hash.Hash, error) {
data, err := cast.Serialize(ctx)
if err != nil {
return hash.Hash{}, err
}
return hash.Of(data), nil
}
// Name implements the interface rootobject.RootObject.
func (cast Cast) Name() doltdb.TableName {
return CastIDToTableName(cast.ID)
}
// Eval evaluates the cast against the given value.
func (cast Cast) Eval(ctx *sql.Context, val any, sourceType *pgtypes.DoltgresType, targetType *pgtypes.DoltgresType) (any, error) {
if cast.UseInOut {
if val == nil {
return nil, nil
}
output, err := sourceType.IoOutput(ctx, val)
if err != nil {
return nil, err
}
return targetType.IoInput(ctx, output)
}
if cast.BuiltIn != nil {
// It may not be strictly true that all built-in casts are STRICT, but it seems true so we'll hold the assumption
if val == nil {
return nil, nil
}
return cast.BuiltIn(ctx, val, sourceType, targetType)
}
if cast.Function != id.NullFunction {
castFunc, ok := functionProvider.Function(ctx, cast.Function.SchemaName(), cast.Function.FunctionName())
if !ok {
return nil, sql.ErrFunctionNotFound.New(cast.Function.FunctionName())
}
var exprs []sql.Expression
switch cast.Function.ParameterCount() {
case 1:
exprs = []sql.Expression{
expression.NewLiteral(val, sourceType),
}
case 2:
exprs = []sql.Expression{
expression.NewLiteral(val, sourceType),
expression.NewLiteral(targetType.GetAttTypMod(), pgtypes.Int32),
}
case 3:
exprs = []sql.Expression{
expression.NewLiteral(val, sourceType),
expression.NewLiteral(targetType.GetAttTypMod(), pgtypes.Int32),
expression.NewLiteral(cast.request == CastType_Explicit, pgtypes.Bool),
}
default:
return nil, errors.New("invalid parameter count for cast function") // TODO: figure out the actual error
}
castFuncInstance, err := castFunc.NewInstance(ctx, exprs)
if err != nil {
return nil, err
}
if val == nil {
if getIsStrictFromFunction(castFuncInstance) {
return nil, nil
}
}
if setRunner, ok := castFuncInstance.(procedures.InterpreterExpr); ok {
runner, err := getRunnerFromContext(ctx)
if err != nil {
return nil, err
}
castFuncInstance = setRunner.SetStatementRunner(ctx, runner)
}
return castFuncInstance.Eval(ctx, nil)
}
// In this case, the values are binary-coercible, but we still check as we may deviate from Postgres for some reason
if _, _, err := targetType.Convert(ctx, val); err != nil {
return nil, errors.Errorf(`cast from type %s to type %s is mislabeled as binary-coercible`,
cast.ID.SourceType().TypeName(), cast.ID.TargetType().TypeName())
}
return val, nil
}
// CastIDToTableName returns the ID in a format that's better for user consumption.
func CastIDToTableName(castID id.Cast) doltdb.TableName {
name := fmt.Sprintf(`(%s)|(%s)|(%s)|(%s)`,
castID.SourceType().SchemaName(),
castID.SourceType().TypeName(),
castID.TargetType().SchemaName(),
castID.TargetType().TypeName())
return doltdb.TableName{
Name: name,
Schema: "",
}
}
// functionProvider is set by init and is used to avoid import cycles
var functionProvider sql.FunctionProvider
// getRunnerFromContext is set by init and is used to avoid import cycles
var getRunnerFromContext func(ctx *sql.Context) (sql.StatementRunner, error)
// getIsStrictFromFunction is set by init and is used to avoid import cycles
var getIsStrictFromFunction func(f sql.Expression) bool