chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:32:25 +08:00
commit e014feafe1
2285 changed files with 1131979 additions and 0 deletions
+712
View File
@@ -0,0 +1,712 @@
// 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
+133
View File
@@ -0,0 +1,133 @@
// 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"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/merge"
"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/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/flatbuffers/gen/serial"
)
// storage is used to read from and write to the root.
var storage = objinterface.RootObjectSerializer{
Bytes: (*serial.RootValue).CastsBytes,
RootValueAdd: serial.RootValueAddCasts,
}
// HandleMerge implements the interface objinterface.Collection.
func (*Collection) HandleMerge(ctx context.Context, mro merge.MergeRootObject) (doltdb.RootObject, *merge.MergeStats, error) {
ourCast := mro.OurRootObj.(Cast)
theirCast := mro.TheirRootObj.(Cast)
// Ensure that they have the same identifier
if ourCast.ID != theirCast.ID {
return nil, nil, errors.Newf("attempted to merge different casts: `%s` and `%s`",
ourCast.Name().String(), theirCast.Name().String())
}
ourHash, err := ourCast.HashOf(ctx)
if err != nil {
return nil, nil, err
}
theirHash, err := theirCast.HashOf(ctx)
if err != nil {
return nil, nil, err
}
if ourHash.Equal(theirHash) {
return mro.OurRootObj, &merge.MergeStats{
Operation: merge.TableUnmodified,
Adds: 0,
Deletes: 0,
Modifications: 0,
DataConflicts: 0,
SchemaConflicts: 0,
ConstraintViolations: 0,
}, nil
}
// TODO: figure out a decent merge strategy
return nil, nil, errors.Errorf("unable to merge `%s`", theirCast.Name().String())
}
// LoadCollection implements the interface objinterface.Collection.
func (*Collection) LoadCollection(ctx context.Context, root objinterface.RootValue) (objinterface.Collection, error) {
return LoadCasts(ctx, root)
}
// LoadCollectionHash implements the interface objinterface.Collection.
func (*Collection) LoadCollectionHash(ctx context.Context, root objinterface.RootValue) (hash.Hash, error) {
m, ok, err := storage.GetProllyMap(ctx, root)
if err != nil || !ok {
return hash.Hash{}, err
}
return m.HashOf(), nil
}
// LoadCasts loads the casts collection from the given root.
func LoadCasts(ctx context.Context, root objinterface.RootValue) (*Collection, error) {
m, ok, err := storage.GetProllyMap(ctx, root)
if err != nil {
return nil, err
}
if !ok {
m, err = prolly.NewEmptyAddressMap(root.NodeStore())
if err != nil {
return nil, err
}
}
return NewCollection(ctx, m, root.NodeStore())
}
// ResolveNameFromObjects implements the interface objinterface.Collection.
func (*Collection) ResolveNameFromObjects(ctx context.Context, name doltdb.TableName, rootObjects []objinterface.RootObject) (doltdb.TableName, id.Id, error) {
// There are root objects to search through, so we'll create a temporary store
ns := tree.NewTestNodeStore()
addressMap, err := prolly.NewEmptyAddressMap(ns)
if err != nil {
return doltdb.TableName{}, id.Null, err
}
tempCollection, err := NewCollection(ctx, addressMap, ns)
if err != nil {
return doltdb.TableName{}, id.Null, err
}
for _, rootObject := range rootObjects {
if c, ok := rootObject.(Cast); ok {
if err = tempCollection.AddCast(ctx, c); err != nil {
return doltdb.TableName{}, id.Null, err
}
}
}
return tempCollection.ResolveName(ctx, name)
}
// Serializer implements the interface objinterface.Collection.
func (*Collection) Serializer() objinterface.RootObjectSerializer {
return storage
}
// UpdateRoot implements the interface objinterface.Collection.
func (pgc *Collection) UpdateRoot(ctx context.Context, root objinterface.RootValue) (objinterface.RootValue, error) {
m, err := pgc.Map(ctx)
if err != nil {
return nil, err
}
return storage.WriteProllyMap(ctx, root, m)
}
+33
View File
@@ -0,0 +1,33 @@
// 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 (
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core/id"
)
// Init initializes this package.
func Init(
getRunnerFromCtx func(ctx *sql.Context) (sql.StatementRunner, error),
getIsStrictFromFunc func(f sql.Expression) bool,
provider sql.FunctionProvider,
) map[id.Cast]Cast {
functionProvider = provider
getRunnerFromContext = getRunnerFromCtx
getIsStrictFromFunction = getIsStrictFromFunc
return builtInCasts
}
+149
View File
@@ -0,0 +1,149 @@
// 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"
"io"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/store/hash"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// DeserializeRootObject implements the interface objinterface.Collection.
func (pgc *Collection) DeserializeRootObject(ctx context.Context, data []byte) (objinterface.RootObject, error) {
return DeserializeCast(ctx, data)
}
// DiffRootObjects implements the interface objinterface.Collection.
func (pgc *Collection) DiffRootObjects(ctx context.Context, fromHash string, ours objinterface.RootObject, theirs objinterface.RootObject, ancestor objinterface.RootObject) ([]objinterface.RootObjectDiff, objinterface.RootObject, error) {
return nil, nil, errors.New("cast conflict detection has not yet been implemented")
}
// DropRootObject implements the interface objinterface.Collection.
func (pgc *Collection) DropRootObject(ctx context.Context, identifier id.Id) error {
if identifier.Section() != id.Section_Cast {
return errors.Errorf(`cast %s does not exist`, identifier.String())
}
return pgc.DropCast(ctx, id.Cast(identifier))
}
// GetFieldType implements the interface objinterface.Collection.
func (pgc *Collection) GetFieldType(ctx context.Context, fieldName string) *pgtypes.DoltgresType {
return nil
}
// GetID implements the interface objinterface.Collection.
func (pgc *Collection) GetID() objinterface.RootObjectID {
return objinterface.RootObjectID_Casts
}
// GetRootObject implements the interface objinterface.Collection.
func (pgc *Collection) GetRootObject(ctx context.Context, identifier id.Id) (objinterface.RootObject, bool, error) {
if identifier.Section() != id.Section_Cast {
return nil, false, nil
}
c, err := pgc.getCast(ctx, id.Cast(identifier), nil, nil, CastType_Explicit)
return c, err == nil && c.ID.IsValid(), err
}
// HasRootObject implements the interface objinterface.Collection.
func (pgc *Collection) HasRootObject(ctx context.Context, identifier id.Id) (bool, error) {
if identifier.Section() != id.Section_Cast {
return false, nil
}
return pgc.HasCast(ctx, id.Cast(identifier)), nil
}
// IDToTableName implements the interface objinterface.Collection.
func (pgc *Collection) IDToTableName(identifier id.Id) doltdb.TableName {
if identifier.Section() != id.Section_Cast {
return doltdb.TableName{}
}
return CastIDToTableName(id.Cast(identifier))
}
// IterAll implements the interface objinterface.Collection.
func (pgc *Collection) IterAll(ctx context.Context, callback func(rootObj objinterface.RootObject) (stop bool, err error)) error {
return pgc.IterateCasts(ctx, func(c Cast) (stop bool, err error) {
return callback(c)
})
}
// IterIDs implements the interface objinterface.Collection.
func (pgc *Collection) IterIDs(ctx context.Context, callback func(identifier id.Id) (stop bool, err error)) error {
err := pgc.underlyingMap.IterAll(ctx, func(k string, _ hash.Hash) error {
stop, err := callback(id.Id(k))
if err != nil {
return err
} else if stop {
return io.EOF
} else {
return nil
}
})
return err
}
// PutRootObject implements the interface objinterface.Collection.
func (pgc *Collection) PutRootObject(ctx context.Context, rootObj objinterface.RootObject) error {
c, ok := rootObj.(Cast)
if !ok {
return errors.Newf("invalid cast root object: %T", rootObj)
}
return pgc.AddCast(ctx, c)
}
// RenameRootObject implements the interface objinterface.Collection.
func (pgc *Collection) RenameRootObject(ctx context.Context, oldName id.Id, newName id.Id) error {
if !oldName.IsValid() || !newName.IsValid() || oldName.Section() != newName.Section() || oldName.Section() != id.Section_Cast {
return errors.New("cannot rename cast due to invalid id")
}
oldCastName := id.Cast(oldName)
newCastName := id.Cast(newName)
c, err := pgc.getCast(ctx, oldCastName, nil, nil, CastType_Explicit)
if err != nil {
return err
}
if err = pgc.DropCast(ctx, newCastName); err != nil {
return err
}
c.ID = newCastName
return pgc.AddCast(ctx, c)
}
// ResolveName implements the interface objinterface.Collection.
func (pgc *Collection) ResolveName(ctx context.Context, name doltdb.TableName) (doltdb.TableName, id.Id, error) {
rawID, err := pgc.resolveName(ctx, name.Schema, name.Name)
if err != nil || !rawID.IsValid() {
return doltdb.TableName{}, id.Null, err
}
return CastIDToTableName(rawID), rawID.AsId(), nil
}
// TableNameToID implements the interface objinterface.Collection.
func (pgc *Collection) TableNameToID(name doltdb.TableName) id.Id {
return pgc.tableNameToID(name.Schema, name.Name).AsId()
}
// UpdateField implements the interface objinterface.Collection.
func (pgc *Collection) UpdateField(ctx context.Context, rootObject objinterface.RootObject, fieldName string, newValue any) (objinterface.RootObject, error) {
return nil, errors.New("updating through the conflicts table for this object type is not yet supported")
}
+67
View File
@@ -0,0 +1,67 @@
// 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"
"github.com/cockroachdb/errors"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/utils"
)
// Serialize returns the Cast as a byte slice. If the Cast is invalid, then this returns a nil slice.
func (cast Cast) Serialize(ctx context.Context) ([]byte, error) {
if !cast.ID.IsValid() {
return nil, nil
}
// Initialize the writer and version
writer := utils.NewWriter(256)
writer.VariableUint(0) // Version
// Write the cast data
writer.Id(cast.ID.AsId())
writer.Uint8(uint8(cast.CastType))
writer.Id(cast.Function.AsId())
writer.Bool(cast.UseInOut)
// Returns the data
return writer.Data(), nil
}
// DeserializeCast returns the Cast that was serialized in the byte slice. Returns an empty Cast (invalid ID) if data is
// nil or empty.
func DeserializeCast(ctx context.Context, data []byte) (Cast, error) {
if len(data) == 0 {
return Cast{}, nil
}
reader := utils.NewReader(data)
version := reader.VariableUint()
if version != 0 {
return Cast{}, errors.Errorf("version %d of casts is not supported, please upgrade the server", version)
}
// Read from the reader
t := Cast{}
t.ID = id.Cast(reader.Id())
t.CastType = CastType(reader.Uint8())
t.Function = id.Function(reader.Id())
t.UseInOut = reader.Bool()
if !reader.IsEmpty() {
return Cast{}, errors.Errorf("extra data found while deserializing a cast")
}
// Return the deserialized object
return t, nil
}
+401
View File
@@ -0,0 +1,401 @@
// Copyright 2025 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 conflicts
import (
"context"
"maps"
"slices"
"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/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// Collection contains a collection of conflicts.
type Collection struct {
accessCache map[id.Id]Conflict // This cache is used for general access when you know the exact ID
nameCache map[doltdb.TableName]id.Id // This cache is used for ID resolution, since exact table names are always used
idCache []id.Id // This cache simply contains the name of every root object
mapHash hash.Hash // This is cached so that we don't have to calculate the hash every time
underlyingMap prolly.AddressMap
ns tree.NodeStore
}
// Conflict represents a root object conflict.
type Conflict struct {
ID id.Id
FromHash string
RootObjectID objinterface.RootObjectID
Ours objinterface.RootObject
Theirs objinterface.RootObject
Ancestor objinterface.RootObject
}
var _ objinterface.Collection = (*Collection)(nil)
var _ objinterface.RootObject = Conflict{}
var _ objinterface.Conflict = Conflict{}
var _ doltdb.ConflictRootObject = Conflict{}
// NewCollection returns a new Collection.
func NewCollection(ctx context.Context, underlyingMap prolly.AddressMap, ns tree.NodeStore) (*Collection, error) {
collection := &Collection{
accessCache: make(map[id.Id]Conflict),
nameCache: make(map[doltdb.TableName]id.Id),
idCache: nil,
mapHash: hash.Hash{},
underlyingMap: underlyingMap,
ns: ns,
}
return collection, collection.reloadCaches(ctx)
}
// GetConflict returns the conflict with the given ID. Returns a conflict with an invalid ID if it cannot be found
// (Conflict.ID.IsValid() == false).
func (pgc *Collection) GetConflict(ctx context.Context, conflictID id.Id) (Conflict, error) {
if conflict, ok := pgc.accessCache[conflictID]; ok {
return conflict, nil
}
return Conflict{}, nil
}
// HasConflict returns whether the conflict is present.
func (pgc *Collection) HasConflict(ctx context.Context, conflictID id.Id) bool {
_, ok := pgc.accessCache[conflictID]
return ok
}
// AddConflict adds a new conflict.
func (pgc *Collection) AddConflict(ctx context.Context, conflict Conflict) error {
// First we'll check to see if it exists
if _, ok := pgc.accessCache[conflict.ID]; ok {
return errors.Errorf(`"%s" already has a conflict`, conflict.Ours.Name())
}
// Now we'll add the conflict to our map
data, err := conflict.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(conflict.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 pgc.reloadCaches(ctx)
}
// DropConflict drops an existing conflict.
func (pgc *Collection) DropConflict(ctx context.Context, conflictIDs ...id.Id) error {
if len(conflictIDs) == 0 {
return nil
}
// Check that each name exists before performing any deletions
for _, conflictID := range conflictIDs {
if _, ok := pgc.accessCache[conflictID]; !ok {
return errors.Errorf(`conflict %s does not exist`, conflictID.String())
}
}
// Now we'll remove the conflicts from the map
mapEditor := pgc.underlyingMap.Editor()
for _, conflictID := range conflictIDs {
err := mapEditor.Delete(ctx, string(conflictID))
if err != nil {
return err
}
}
newMap, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pgc.underlyingMap = newMap
pgc.mapHash = pgc.underlyingMap.HashOf()
return pgc.reloadCaches(ctx)
}
// iterateIDs iterates over all conflict IDs in the collection.
func (pgc *Collection) iterateIDs(ctx context.Context, callback func(conflictID id.Id) (stop bool, err error)) error {
for _, conflictID := range pgc.idCache {
stop, err := callback(conflictID)
if err != nil {
return err
} else if stop {
return nil
}
}
return nil
}
// IterateConflicts iterates over all conflicts in the collection.
func (pgc *Collection) IterateConflicts(ctx context.Context, callback func(conflict Conflict) (stop bool, err error)) error {
for _, conflictID := range pgc.idCache {
stop, err := callback(pgc.accessCache[conflictID])
if err != nil {
return err
} else if stop {
return nil
}
}
return nil
}
// Clone returns a new *Collection with the same contents as the original.
func (pgc *Collection) Clone(ctx context.Context) *Collection {
return &Collection{
accessCache: maps.Clone(pgc.accessCache),
nameCache: maps.Clone(pgc.nameCache),
idCache: slices.Clone(pgc.idCache),
underlyingMap: pgc.underlyingMap,
mapHash: pgc.mapHash,
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
}
// 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
}
// reloadCaches writes the underlying map's contents to the caches.
func (pgc *Collection) reloadCaches(ctx context.Context) error {
count, err := pgc.underlyingMap.Count()
if err != nil {
return err
}
clear(pgc.accessCache)
clear(pgc.nameCache)
pgc.mapHash = pgc.underlyingMap.HashOf()
pgc.idCache = make([]id.Id, 0, count)
return pgc.underlyingMap.IterAll(ctx, func(_ string, h hash.Hash) error {
if h.IsEmpty() {
return nil
}
data, err := pgc.ns.ReadBytes(ctx, h)
if err != nil {
return err
}
conflict, err := DeserializeConflict(ctx, data)
if err != nil {
return err
}
pgc.accessCache[conflict.ID] = conflict
pgc.nameCache[conflict.Name()] = conflict.ID
pgc.idCache = append(pgc.idCache, conflict.ID)
return nil
})
}
// DiffCount implements the interface objinterface.Conflict.
func (conflict Conflict) DiffCount(ctx *sql.Context) (int, error) {
diffs, _, err := conflict.Diffs(ctx)
return len(diffs), err
}
// Diffs implements the interface objinterface.Conflict.
func (conflict Conflict) Diffs(ctx context.Context) ([]objinterface.RootObjectDiff, objinterface.RootObject, error) {
return DiffRootObjects(ctx, conflict.RootObjectID, conflict.FromHash, conflict.Ours, conflict.Theirs, conflict.Ancestor)
}
// FieldType implements the interface objinterface.Conflict.
func (conflict Conflict) FieldType(ctx context.Context, name string) *pgtypes.DoltgresType {
return GetFieldType(ctx, conflict.RootObjectID, name)
}
// GetContainedRootObjectID implements the interface objinterface.RootObject.
func (conflict Conflict) GetContainedRootObjectID() objinterface.RootObjectID {
return conflict.RootObjectID
}
// GetID implements the interface objinterface.RootObject.
func (conflict Conflict) GetID() id.Id {
return conflict.ID
}
// GetRootObjectID implements the interface objinterface.RootObject.
func (conflict Conflict) GetRootObjectID() objinterface.RootObjectID {
return objinterface.RootObjectID_Conflicts
}
// HashOf implements the interface objinterface.RootObject.
func (conflict Conflict) HashOf(ctx context.Context) (hash.Hash, error) {
data, err := conflict.Serialize(ctx)
if err != nil {
return hash.Hash{}, err
}
return hash.Of(data), nil
}
// Name implements the interface objinterface.RootObject.
func (conflict Conflict) Name() doltdb.TableName {
if conflict.Ours != nil {
return conflict.Ours.Name()
} else {
return conflict.Theirs.Name()
}
}
// RemoveDiffs implements the interface doltdb.ConflictRootObject.
func (conflict Conflict) RemoveDiffs(ctx *sql.Context, diffs []doltdb.RootObjectDiff) (doltdb.ConflictRootObject, error) {
// This is only called from within Dolt, and it specifically deals with modifying the conflict collection on the
// root. It is therefore safe to clear context values, to ensure the root changes are not overwritten.
ClearContextValues(ctx)
// We need to handle the root object field name in a special way, since it's how we select which side to use in full
if len(diffs) == 1 {
diff := diffs[0].(objinterface.RootObjectDiff)
if diff.FieldName == objinterface.FIELD_NAME_ROOT_OBJECT {
if conflict.Ours == nil {
conflict.Theirs = nil
} else {
conflict.Theirs = conflict.Ours
}
conflict.Ancestor = nil
return conflict, nil
}
}
// Deletion is equivalent to setting the value in "theirs" to "ours", as the same value cannot conflict with itself.
// We will only reach this point if both "ours" and "theirs" are non-nil entries, so the above relies on safe assumptions.
for _, doltDiff := range diffs {
diff := doltDiff.(objinterface.RootObjectDiff)
newTheirs, err := UpdateField(ctx, conflict.RootObjectID, conflict.Theirs, diff.FieldName, diff.OurValue)
if err != nil {
return nil, err
}
conflict.Theirs = newTheirs
}
return conflict, nil
}
// Rows implements the interface doltdb.ConflictRootObject.
func (conflict Conflict) Rows(ctx *sql.Context) (sql.RowIter, error) {
diffs, _, err := conflict.Diffs(ctx)
if err != nil {
return nil, err
}
rows := make([]sql.Row, len(diffs))
for i, diff := range diffs {
rows[i], err = diff.ToRow(ctx)
if err != nil {
return nil, err
}
}
return sql.RowsToRowIter(rows...), nil
}
// Schema implements the interface doltdb.ConflictRootObject.
func (conflict Conflict) Schema(originatingTableName string) sql.Schema {
sch := objinterface.RootObjectDiffSchema.Copy()
for _, col := range sch {
col.Source = originatingTableName
}
return sch
}
// UpdateField implements the interface doltdb.ConflictRootObject.
func (conflict Conflict) UpdateField(ctx *sql.Context, o doltdb.RootObjectDiff, n doltdb.RootObjectDiff) (doltdb.ConflictRootObject, error) {
// This is only called from within Dolt, and it specifically deals with modifying the conflict collection on the
// root. It is therefore safe to clear context values, to ensure the root changes are not overwritten.
ClearContextValues(ctx)
oldDiff := o.(objinterface.RootObjectDiff)
newDiff := n.(objinterface.RootObjectDiff)
// We need to handle the root object field name in a special way, since it's how we select which side to use in full
if oldDiff.FieldName == objinterface.FIELD_NAME_ROOT_OBJECT {
switch newDiff.OurValue.(string) {
case objinterface.FIELD_NAME_OURS:
conflict.Theirs = conflict.Ours
case objinterface.FIELD_NAME_THEIRS:
conflict.Ours = conflict.Theirs
case objinterface.FIELD_NAME_ANCESTOR:
conflict.Ours = conflict.Ancestor
conflict.Theirs = conflict.Ancestor
default:
return nil, errors.Newf("cannot replace the object with `%s`", newDiff.OurValue.(string))
}
conflict.Ancestor = nil
return conflict, nil
}
newOurs, err := UpdateField(ctx, conflict.RootObjectID, conflict.Ours, oldDiff.FieldName, newDiff.OurValue)
if err != nil {
return nil, err
}
conflict.Ours = newOurs
return conflict, nil
}
// ClearContextValues clears context values, and is declared in a different package. It is assigned here by an Init
// function to get around import cycles.
var ClearContextValues = func(ctx *sql.Context) {
panic("ClearContextValues was never initialized")
}
// DiffRootObjects handles conflict diffs, and is declared in a different package. It is assigned here by an Init
// function to get around import cycles.
var DiffRootObjects = func(ctx context.Context, rootObjID objinterface.RootObjectID, fromHash string, ours, theirs, ancestor objinterface.RootObject) ([]objinterface.RootObjectDiff, objinterface.RootObject, error) {
return nil, nil, errors.New("DiffRootObjects was never initialized")
}
// GetFieldType handles type fetching for fields, and is declared in a different package. It is assigned here by an Init
// function to get around import cycles.
var GetFieldType = func(ctx context.Context, rootObjID objinterface.RootObjectID, fieldName string) *pgtypes.DoltgresType {
panic("GetFieldType was never initialized")
}
// ResolveNameExternal handles name resolution across all collection types, and is declared in a different package. It
// is assigned here by an Init function to get around import cycles.
var ResolveNameExternal = func(ctx context.Context, name doltdb.TableName, rootObjects []objinterface.RootObject) (doltdb.TableName, id.Id, error) {
panic("ResolveNameExternal was never initialized")
}
// UpdateField handles updating fields in a root object, and is declared in a different package. It is assigned here by
// an Init function to get around import cycles.
var UpdateField = func(ctx context.Context, rootObjID objinterface.RootObjectID, rootObject objinterface.RootObject, fieldName string, newValue any) (objinterface.RootObject, error) {
return nil, errors.New("UpdateField was never initialized")
}
+117
View File
@@ -0,0 +1,117 @@
// Copyright 2025 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 conflicts
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/merge"
"github.com/dolthub/dolt/go/store/hash"
"github.com/dolthub/dolt/go/store/prolly"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/flatbuffers/gen/serial"
)
// storage is used to read from and write to the root.
var storage = objinterface.RootObjectSerializer{
Bytes: (*serial.RootValue).ConflictsBytes,
RootValueAdd: serial.RootValueAddConflicts,
}
// HandleMerge implements the interface objinterface.Collection.
func (*Collection) HandleMerge(ctx context.Context, mro merge.MergeRootObject) (doltdb.RootObject, *merge.MergeStats, error) {
// It technically doesn't make sense to merge conflicts, but we'll only error if there are differences
ourConflict := mro.OurRootObj.(Conflict)
theirConflict := mro.TheirRootObj.(Conflict)
// Ensure that they have the same identifier
if ourConflict.ID != theirConflict.ID {
return nil, nil, errors.Newf("attempted to merge different conflicts: `%s` and `%s`",
ourConflict.Name().String(), theirConflict.Name().String())
}
ourHash, err := ourConflict.HashOf(ctx)
if err != nil {
return nil, nil, err
}
theirHash, err := theirConflict.HashOf(ctx)
if err != nil {
return nil, nil, err
}
if ourHash.Equal(theirHash) {
return mro.OurRootObj, &merge.MergeStats{
Operation: merge.TableUnmodified,
Adds: 0,
Deletes: 0,
Modifications: 0,
DataConflicts: 0,
SchemaConflicts: 0,
RootObjectConflicts: 0,
ConstraintViolations: 0,
}, nil
}
return nil, nil, errors.New("attempted to merge conflicts")
}
// LoadCollection implements the interface objinterface.Collection.
func (*Collection) LoadCollection(ctx context.Context, root objinterface.RootValue) (objinterface.Collection, error) {
return LoadConflicts(ctx, root)
}
// LoadCollectionHash implements the interface objinterface.Collection.
func (*Collection) LoadCollectionHash(ctx context.Context, root objinterface.RootValue) (hash.Hash, error) {
m, ok, err := storage.GetProllyMap(ctx, root)
if err != nil || !ok {
return hash.Hash{}, err
}
return m.HashOf(), nil
}
// LoadConflicts loads the conflicts collection from the given root.
func LoadConflicts(ctx context.Context, root objinterface.RootValue) (*Collection, error) {
m, ok, err := storage.GetProllyMap(ctx, root)
if err != nil {
return nil, err
}
if !ok {
m, err = prolly.NewEmptyAddressMap(root.NodeStore())
if err != nil {
return nil, err
}
}
return NewCollection(ctx, m, root.NodeStore())
}
// ResolveNameFromObjects implements the interface objinterface.Collection.
func (*Collection) ResolveNameFromObjects(ctx context.Context, name doltdb.TableName, rootObjects []objinterface.RootObject) (doltdb.TableName, id.Id, error) {
// Conflicts make use of this interface function, so we must return nil to prevent infinite recursion
return doltdb.TableName{}, id.Null, nil
}
// Serializer implements the interface objinterface.Collection.
func (*Collection) Serializer() objinterface.RootObjectSerializer {
return storage
}
// UpdateRoot implements the interface objinterface.Collection.
func (pgc *Collection) UpdateRoot(ctx context.Context, root objinterface.RootValue) (objinterface.RootValue, error) {
m, err := pgc.Map(ctx)
if err != nil {
return nil, err
}
return storage.WriteProllyMap(ctx, root, m)
}
+126
View File
@@ -0,0 +1,126 @@
// Copyright 2025 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 conflicts
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// DeserializeRootObject implements the interface objinterface.Collection.
func (pgc *Collection) DeserializeRootObject(ctx context.Context, data []byte) (objinterface.RootObject, error) {
return DeserializeConflict(ctx, data)
}
// DiffRootObjects implements the interface objinterface.Collection.
func (pgc *Collection) DiffRootObjects(ctx context.Context, fromHash string, ours objinterface.RootObject, theirs objinterface.RootObject, ancestor objinterface.RootObject) ([]objinterface.RootObjectDiff, objinterface.RootObject, error) {
return nil, nil, errors.Errorf("cannot diff the conflict root objects themselves")
}
// DropRootObject implements the interface objinterface.Collection.
func (pgc *Collection) DropRootObject(ctx context.Context, identifier id.Id) error {
return pgc.DropConflict(ctx, identifier)
}
// GetFieldType implements the interface objinterface.Collection.
func (pgc *Collection) GetFieldType(ctx context.Context, fieldName string) *pgtypes.DoltgresType {
return nil
}
// GetID implements the interface objinterface.Collection.
func (pgc *Collection) GetID() objinterface.RootObjectID {
return objinterface.RootObjectID_Conflicts
}
// GetRootObject implements the interface objinterface.Collection.
func (pgc *Collection) GetRootObject(ctx context.Context, identifier id.Id) (objinterface.RootObject, bool, error) {
conflict, err := pgc.GetConflict(ctx, identifier)
return conflict, err == nil && conflict.ID.IsValid(), err
}
// HasRootObject implements the interface objinterface.Collection.
func (pgc *Collection) HasRootObject(ctx context.Context, identifier id.Id) (bool, error) {
return pgc.HasConflict(ctx, identifier), nil
}
// IDToTableName implements the interface objinterface.Collection.
func (pgc *Collection) IDToTableName(identifier id.Id) doltdb.TableName {
return doltdb.TableName{Name: string(identifier)}
}
// IterAll implements the interface objinterface.Collection.
func (pgc *Collection) IterAll(ctx context.Context, callback func(rootObj objinterface.RootObject) (stop bool, err error)) error {
return pgc.IterateConflicts(ctx, func(conflict Conflict) (stop bool, err error) {
return callback(conflict)
})
}
// IterIDs implements the interface objinterface.Collection.
func (pgc *Collection) IterIDs(ctx context.Context, callback func(identifier id.Id) (stop bool, err error)) error {
return pgc.iterateIDs(ctx, func(conflictID id.Id) (stop bool, err error) {
return callback(conflictID)
})
}
// PutRootObject implements the interface objinterface.Collection.
func (pgc *Collection) PutRootObject(ctx context.Context, rootObj objinterface.RootObject) error {
conflict, ok := rootObj.(Conflict)
if !ok {
return errors.Newf("invalid conflict root object: %T", rootObj)
}
return pgc.AddConflict(ctx, conflict)
}
// RenameRootObject implements the interface objinterface.Collection.
func (pgc *Collection) RenameRootObject(ctx context.Context, oldName id.Id, newName id.Id) error {
return errors.New("cannot rename root object conflicts")
}
// ResolveName implements the interface objinterface.Collection.
func (pgc *Collection) ResolveName(ctx context.Context, name doltdb.TableName) (doltdb.TableName, id.Id, error) {
if len(pgc.idCache) == 0 {
return doltdb.TableName{}, id.Null, nil
}
objs := make([]objinterface.RootObject, 0, len(pgc.idCache))
for _, conflict := range pgc.accessCache {
if conflict.Ours != nil {
objs = append(objs, conflict.Ours)
} else if conflict.Theirs != nil {
objs = append(objs, conflict.Theirs)
} else if conflict.Ancestor != nil {
objs = append(objs, conflict.Ancestor)
}
}
return ResolveNameExternal(ctx, name, objs)
}
// TableNameToID implements the interface objinterface.Collection.
func (pgc *Collection) TableNameToID(name doltdb.TableName) id.Id {
if resolvedId, ok := pgc.nameCache[name]; ok {
return resolvedId
}
return id.Null
}
// UpdateField implements the interface objinterface.Collection.
func (pgc *Collection) UpdateField(ctx context.Context, rootObject objinterface.RootObject, fieldName string, newValue any) (objinterface.RootObject, error) {
return nil, errors.New("conflicts should not have conflict tables themselves, so this update should be impossible")
}
+122
View File
@@ -0,0 +1,122 @@
// Copyright 2025 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 conflicts
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/utils"
)
// Serialize returns the Conflict as a byte slice. If the Conflict is invalid, then this returns a nil slice.
func (conflict Conflict) Serialize(ctx context.Context) (_ []byte, err error) {
if !conflict.ID.IsValid() {
return nil, nil
}
// Write all of the conflicts to the writer
writer := utils.NewWriter(512)
writer.VariableUint(0) // Version
// Serialize "ours", "theirs", and "ancestor"
var ours, theirs, ancestor []byte
if conflict.Ours != nil {
ours, err = conflict.Ours.Serialize(ctx)
if err != nil {
return nil, err
}
}
if conflict.Theirs != nil {
theirs, err = conflict.Theirs.Serialize(ctx)
if err != nil {
return nil, err
}
}
if conflict.Ancestor != nil {
ancestor, err = conflict.Ancestor.Serialize(ctx)
if err != nil {
return nil, err
}
}
// Write the conflict data
writer.Id(conflict.ID)
writer.String(conflict.FromHash)
writer.Int64(int64(conflict.RootObjectID))
writer.Bool(conflict.Ours != nil)
writer.Bool(conflict.Theirs != nil)
writer.Bool(conflict.Ancestor != nil)
writer.ByteSlice(ours)
writer.ByteSlice(theirs)
writer.ByteSlice(ancestor)
// Returns the data
return writer.Data(), nil
}
// DeserializeConflict returns the Conflict that was serialized in the byte slice. Returns an empty Conflict (invalid
// ID) if data is nil or empty.
func DeserializeConflict(ctx context.Context, data []byte) (_ Conflict, err error) {
if len(data) == 0 {
return Conflict{}, nil
}
reader := utils.NewReader(data)
version := reader.VariableUint()
if version > 0 {
return Conflict{}, errors.Errorf("version %d of conflicts is not supported, please upgrade the server", version)
}
// Read from the reader
conflict := Conflict{}
conflict.ID = reader.Id()
conflict.FromHash = reader.String()
conflict.RootObjectID = objinterface.RootObjectID(reader.Int64())
hasOurs := reader.Bool()
hasTheirs := reader.Bool()
hasAncestor := reader.Bool()
ours := reader.ByteSlice()
theirs := reader.ByteSlice()
ancestor := reader.ByteSlice()
// Deserialize "ours", "theirs", and "ancestor"
if hasOurs {
conflict.Ours, err = DeserializeRootObject(ctx, conflict.RootObjectID, ours)
if err != nil {
return Conflict{}, err
}
}
if hasTheirs {
conflict.Theirs, err = DeserializeRootObject(ctx, conflict.RootObjectID, theirs)
if err != nil {
return Conflict{}, err
}
}
if hasAncestor {
conflict.Ancestor, err = DeserializeRootObject(ctx, conflict.RootObjectID, ancestor)
if err != nil {
return Conflict{}, err
}
}
if !reader.IsEmpty() {
return Conflict{}, errors.Errorf("extra data found while deserializing a conflict")
}
// Return the deserialized object
return conflict, nil
}
// DeserializeRootObject handles root object deserialization, and is declared in a different package. It is assigned
// here by an Init function to get around import cycles.
var DeserializeRootObject = func(ctx context.Context, rootObjID objinterface.RootObjectID, data []byte) (objinterface.RootObject, error) {
return nil, errors.New("DeserializeRootObject was never initialized")
}
+672
View File
@@ -0,0 +1,672 @@
// 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 core
import (
"maps"
"slices"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/resolve"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core/casts"
"github.com/dolthub/doltgresql/core/extensions"
"github.com/dolthub/doltgresql/core/functions"
"github.com/dolthub/doltgresql/core/procedures"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/core/sequences"
"github.com/dolthub/doltgresql/core/triggers"
"github.com/dolthub/doltgresql/core/typecollection"
)
// contextValues contains a set of cached data passed alongside the context. This data is considered temporary
// and may be refreshed at any point, including during the middle of a query. Callers should not assume that
// data stored in contextValues is persisted, and other types of data should not be added to contextValues.
type contextValues struct {
seqs map[string]*sequences.Collection
types map[string]*typecollection.TypeCollection
funcs map[string]*functions.Collection
procs map[string]*procedures.Collection
trigs map[string]*triggers.Collection
exts map[string]*extensions.Collection
casts map[string]*casts.Collection
pgCatalogCache any
runner sql.StatementRunner
// cache the dateOutputFormat, this is refreshed on SET
dateOutputFormat string
}
// getContextValues accesses the contextValues in the given context. If the context does not have a contextValues, then
// it creates one and adds it to the context.
func getContextValues(ctx *sql.Context) (*contextValues, error) {
if ctx == nil {
return nil, errors.New("context is nil")
}
sess := dsess.DSessFromSess(ctx.Session)
if sess.DoltgresSessObj == nil {
cv := &contextValues{}
sess.DoltgresSessObj = cv
return cv, nil
}
cv, ok := sess.DoltgresSessObj.(*contextValues)
if !ok {
return nil, errors.Errorf("context contains an unknown values struct of type: %T", sess.DoltgresSessObj)
}
return cv, nil
}
// ClearContextValues clears all context values. This is primarily for operations that are directly called from Dolt, as
// Dolt does not have the Doltgres concept of context values. Care must be taken to ensure that intermediate state
// written to the context values are not overwritten.
func ClearContextValues(ctx *sql.Context) {
sess := dsess.DSessFromSess(ctx.Session)
if sess.DoltgresSessObj != nil {
// We want to persist the runner between clears since it's static
var runner sql.StatementRunner
if cv, ok := sess.DoltgresSessObj.(*contextValues); ok {
runner = cv.runner
}
sess.DoltgresSessObj = &contextValues{
runner: runner,
}
}
}
// GetRootFromContext returns the working session's root from the context, along with the session.
func GetRootFromContext(ctx *sql.Context) (*dsess.DoltSession, *RootValue, error) {
return getRootFromContextForDatabase(ctx, "")
}
// getRootFromContextForDatabase returns the working session's root from the context for a specific database, along with the session.
func getRootFromContextForDatabase(ctx *sql.Context, database string) (*dsess.DoltSession, *RootValue, error) {
session := dsess.DSessFromSess(ctx.Session)
if len(database) == 0 {
database = ctx.GetCurrentDatabase()
}
state, ok, err := session.LookupDbState(ctx, database)
if err != nil {
return nil, nil, err
}
if !ok {
return nil, nil, sql.ErrDatabaseNotFound.New(database)
}
return session, state.WorkingRoot().(*RootValue), nil
}
// IsContextValid returns whether the context is valid for use with any of the functions in the package. If this is not
// false, then there's a high likelihood that the context is being used in a temporary scenario (such as setting up the
// database, etc.).
func IsContextValid(ctx *sql.Context) bool {
if ctx == nil {
return false
}
_, ok := ctx.Session.(*dsess.DoltSession)
return ok
}
// GetPgCatalogCache returns a cache of data for pg_catalog tables. This function should only be used by
// pg_catalog table handlers. The catalog cache instance stores generated pg_catalog table data so that
// it only has to generated table data once per query.
//
// TODO: The return type here is currently any, to avoid a package import cycle. This could be cleaned up by
// introducing a new interface type, in a package that does not depend on core or pgcatalog packages.
func GetPgCatalogCache(ctx *sql.Context) (any, error) {
cv, err := getContextValues(ctx)
if err != nil {
return nil, err
}
return cv.pgCatalogCache, nil
}
// SetPgCatalogCache sets |pgCatalogCache| as the catalog cache instance for this session.
//
// TODO: The input type here is currently any, to avoid a package import cycle. This could be cleaned up by
// introducing a new interface type, in a package that does not depend on core or pgcatalog packages.
func SetPgCatalogCache(ctx *sql.Context, pgCatalogCache any) error {
cv, err := getContextValues(ctx)
if err != nil {
return err
}
cv.pgCatalogCache = pgCatalogCache
return nil
}
// SetRunnerOnContext sets the given runner within the context values.
func SetRunnerOnContext(ctx *sql.Context, runner sql.StatementRunner) error {
if runner == nil {
return nil
}
cv, err := getContextValues(ctx)
if err != nil {
return err
}
cv.runner = runner
return nil
}
// GetRunnerFromContext returns the sql.StatementRunner from within the context.
func GetRunnerFromContext(ctx *sql.Context) (sql.StatementRunner, error) {
cv, err := getContextValues(ctx)
if err != nil {
return nil, err
}
return cv.runner, nil
}
// GetDoltTableFromContext returns the Dolt table from the context. Returns nil if no table was found.
func GetDoltTableFromContext(ctx *sql.Context, tableName doltdb.TableName) (*doltdb.Table, error) {
_, root, err := GetRootFromContext(ctx)
if err != nil {
return nil, err
}
var table *doltdb.Table
if tableName.Schema == "" {
_, table, _, err = resolve.Table(ctx, root, tableName.Name)
if err != nil {
return nil, err
}
} else {
table, _, err = root.GetTable(ctx, tableName)
}
if err != nil {
return nil, err
}
return table, nil
}
// GetSqlDatabaseFromContext returns the database from the context. Uses the context's current database if an empty
// string is provided. Returns nil if the database was not found.
func GetSqlDatabaseFromContext(ctx *sql.Context, database string) (sql.Database, error) {
session := dsess.DSessFromSess(ctx.Session)
if len(database) == 0 {
database = ctx.GetCurrentDatabase()
}
db, err := session.Provider().Database(ctx, database)
if err != nil {
if sql.ErrDatabaseNotFound.Is(err) {
return nil, nil
}
return nil, err
}
return db, nil
}
// GetSqlTableFromContext returns the table from the context. Uses the context's current database if an empty database
// name is provided. Returns nil if no table was found.
func GetSqlTableFromContext(ctx *sql.Context, databaseName string, tableName doltdb.TableName) (sql.Table, error) {
db, err := GetSqlDatabaseFromContext(ctx, databaseName)
if err != nil || db == nil {
return nil, err
}
schemaDb, ok := db.(sql.SchemaDatabase)
if !ok {
// Fairly confident that Dolt only has database implementations that inherit sql.SchemaDatabase, so only GMS
// databases may fail here (like information_schema). In this scenario, we expect that no schema will be passed,
// so we'll special-case it here.
if len(tableName.Schema) == 0 {
tbl, ok, err := db.GetTableInsensitive(ctx, tableName.Name)
if err != nil || !ok {
return nil, err
}
return tbl, nil
}
return nil, nil
}
var searchPath []string
if len(tableName.Schema) == 0 {
// If a schema was not provided, then we'll use the search path
searchPath, err = SearchPath(ctx)
if err != nil {
return nil, err
}
} else {
// A specific schema is given, so we'll only use that one for the search path
searchPath = []string{tableName.Schema}
}
for _, schema := range searchPath {
db, ok, err = schemaDb.GetSchema(ctx, schema)
if err != nil {
return nil, err
}
if !ok {
continue
}
tbl, ok, err := db.GetTableInsensitive(ctx, tableName.Name)
if err != nil {
return nil, err
}
if !ok {
continue
}
return tbl, nil
}
return nil, nil
}
// SearchPath returns the effective schema search path for the current session
func SearchPath(ctx *sql.Context) ([]string, error) {
path, err := resolve.SearchPath(ctx)
if err != nil {
return nil, err
}
// pg_catalog is *always* implicitly part of the search path as the first element, unless it's specifically
// included later. This allows users to override built-in names with user-defined names, but they have to
// opt in to that behavior.
hasPgCatalog := false
for _, schema := range path {
if schema == "pg_catalog" {
hasPgCatalog = true
break
}
}
if !hasPgCatalog {
path = append([]string{"pg_catalog"}, path...)
}
return path, nil
}
// GetExtensionsCollectionFromContext returns the extensions collection from the given context. Will always return a
// collection if no error is returned.
func GetExtensionsCollectionFromContext(ctx *sql.Context, database string) (*extensions.Collection, error) {
cv, err := getContextValues(ctx)
if err != nil {
return nil, err
}
if cv.exts == nil {
cv.exts = make(map[string]*extensions.Collection)
}
if len(database) == 0 {
database = ctx.GetCurrentDatabase()
}
if cv.exts[database] == nil {
_, root, err := getRootFromContextForDatabase(ctx, database)
if err != nil {
return nil, err
}
cv.exts[database], err = extensions.LoadExtensions(ctx, root)
if err != nil {
return nil, err
}
}
return cv.exts[database], nil
}
// GetFunctionsCollectionFromContext returns the functions collection from the given context. Will always return a
// collection if no error is returned.
func GetFunctionsCollectionFromContext(ctx *sql.Context, database string) (*functions.Collection, error) {
cv, err := getContextValues(ctx)
if err != nil {
return nil, err
}
if cv.funcs == nil {
cv.funcs = make(map[string]*functions.Collection)
}
if len(database) == 0 {
database = ctx.GetCurrentDatabase()
}
if cv.funcs[database] == nil {
_, root, err := getRootFromContextForDatabase(ctx, database)
if err != nil {
return nil, err
}
cv.funcs[database], err = functions.LoadFunctions(ctx, root)
if err != nil {
return nil, err
}
}
return cv.funcs[database], nil
}
// GetProceduresCollectionFromContext returns the procedures collection from the given context. Will always return a
// collection if no error is returned.
func GetProceduresCollectionFromContext(ctx *sql.Context, database string) (*procedures.Collection, error) {
cv, err := getContextValues(ctx)
if err != nil {
return nil, err
}
if cv.procs == nil {
cv.procs = make(map[string]*procedures.Collection)
}
if len(database) == 0 {
database = ctx.GetCurrentDatabase()
}
if cv.procs[database] == nil {
_, root, err := getRootFromContextForDatabase(ctx, database)
if err != nil {
return nil, err
}
cv.procs[database], err = procedures.LoadProcedures(ctx, root)
if err != nil {
return nil, err
}
}
return cv.procs[database], nil
}
// GetSequencesCollectionFromContext returns the given sequence collection from the context for the database
// named. If no database is provided, the context's current database is used.
// Will always return a collection if no error is returned.
func GetSequencesCollectionFromContext(ctx *sql.Context, database string) (*sequences.Collection, error) {
cv, err := getContextValues(ctx)
if err != nil {
return nil, err
}
if cv.seqs == nil {
cv.seqs = make(map[string]*sequences.Collection)
}
if len(database) == 0 {
database = ctx.GetCurrentDatabase()
}
if cv.seqs[database] == nil {
_, root, err := getRootFromContextForDatabase(ctx, database)
if err != nil {
return nil, err
}
cv.seqs[database], err = sequences.LoadSequences(ctx, root)
if err != nil {
return nil, err
}
}
return cv.seqs[database], nil
}
// GetTriggersCollectionFromContext returns the triggers collection from the given context. Will always return a
// collection if no error is returned.
func GetTriggersCollectionFromContext(ctx *sql.Context, database string) (*triggers.Collection, error) {
cv, err := getContextValues(ctx)
if err != nil {
return nil, err
}
if cv.trigs == nil {
cv.trigs = make(map[string]*triggers.Collection)
}
if len(database) == 0 {
database = ctx.GetCurrentDatabase()
}
if cv.trigs[database] == nil {
_, root, err := getRootFromContextForDatabase(ctx, database)
if err != nil {
return nil, err
}
cv.trigs[database], err = triggers.LoadTriggers(ctx, root)
if err != nil {
return nil, err
}
}
return cv.trigs[database], nil
}
// GetTypesCollectionFromContext returns the given type collection from the context.
// Will always return a collection if no error is returned.
func GetTypesCollectionFromContext(ctx *sql.Context, database string) (*typecollection.TypeCollection, error) {
cv, err := getContextValues(ctx)
if err != nil {
return nil, err
}
if cv.types == nil {
cv.types = make(map[string]*typecollection.TypeCollection)
}
if len(database) == 0 {
database = ctx.GetCurrentDatabase()
}
if cv.types[database] == nil {
_, root, err := getRootFromContextForDatabase(ctx, database)
if err != nil {
return nil, err
}
cv.types[database], err = typecollection.LoadTypes(ctx, root)
if err != nil {
return nil, err
}
}
return cv.types[database], nil
}
// GetCastsCollectionFromContext returns the given casts collection from the context.
// Will always return a collection if no error is returned.
func GetCastsCollectionFromContext(ctx *sql.Context, database string) (*casts.Collection, error) {
cv, err := getContextValues(ctx)
if err != nil {
return nil, err
}
if cv.casts == nil {
cv.casts = make(map[string]*casts.Collection)
}
if len(database) == 0 {
database = ctx.GetCurrentDatabase()
}
if cv.casts[database] == nil {
_, root, err := getRootFromContextForDatabase(ctx, database)
if err != nil {
return nil, err
}
cv.casts[database], err = casts.LoadCasts(ctx, root)
if err != nil {
return nil, err
}
}
return cv.casts[database], nil
}
// CloseContextRootFinalizer finalizes any changes persisted within the context by writing them to the working root.
// This should ONLY be called by the ContextRootFinalizer node.
func CloseContextRootFinalizer(ctx *sql.Context) error {
sess := dsess.DSessFromSess(ctx.Session)
if sess.DoltgresSessObj == nil {
return nil
}
cv, ok := sess.DoltgresSessObj.(*contextValues)
if !ok {
return nil
}
// We need to update the root for all databases used by this context. This logic parallels what happens during
// transaction commit in the dolt/sqle layer, where we check each branch state to see if it's dirty
for _, db := range databasesInContext(ctx, cv) {
err := updateSessionRootForDatabase(ctx, db, cv)
if err != nil {
return err
}
}
return nil
}
// GetDateStyleOutputFormat returns the cached DateOutputFormat
func GetDateStyleOutputFormat(ctx *sql.Context) (string, error) {
cv, err := getContextValues(ctx)
if err != nil {
return "", err
}
return cv.dateOutputFormat, nil
}
// SetDateStyleOutputFormat cached the provided dateOutputFormat
func SetDateStyleOutputFormat(ctx *sql.Context, dateOutputFormat string) error {
cv, err := getContextValues(ctx)
if err != nil {
return err
}
cv.dateOutputFormat = dateOutputFormat
return nil
}
// updateSessionRootForDatabase updates the root for all changes made to root object collections within the context
// values.
func updateSessionRootForDatabase(ctx *sql.Context, db string, cv *contextValues) error {
session, root, err := getRootFromContextForDatabase(ctx, db)
if err != nil {
return err
}
newRoot := root
if cv.seqs != nil && cv.seqs[db] != nil {
retRoot, err := cv.seqs[db].UpdateRoot(ctx, newRoot)
if err != nil {
return err
}
newRoot = retRoot.(*RootValue)
delete(cv.seqs, db)
}
if cv.funcs != nil && cv.funcs[db] != nil && cv.funcs[db].DiffersFrom(ctx, root) {
retRoot, err := cv.funcs[db].UpdateRoot(ctx, newRoot)
if err != nil {
return err
}
newRoot = retRoot.(*RootValue)
delete(cv.funcs, db)
}
if cv.procs != nil && cv.procs[db] != nil && cv.procs[db].DiffersFrom(ctx, root) {
retRoot, err := cv.procs[db].UpdateRoot(ctx, newRoot)
if err != nil {
return err
}
newRoot = retRoot.(*RootValue)
delete(cv.procs, db)
}
if cv.trigs != nil && cv.trigs[db] != nil && cv.trigs[db].DiffersFrom(ctx, root) {
retRoot, err := cv.trigs[db].UpdateRoot(ctx, newRoot)
if err != nil {
return err
}
newRoot = retRoot.(*RootValue)
delete(cv.trigs, db)
}
if cv.exts != nil && cv.exts[db] != nil && cv.exts[db].DiffersFrom(ctx, root) {
retRoot, err := cv.exts[db].UpdateRoot(ctx, newRoot)
if err != nil {
return err
}
newRoot = retRoot.(*RootValue)
delete(cv.exts, db)
}
if cv.types != nil && cv.types[db] != nil {
retRoot, err := cv.types[db].UpdateRoot(ctx, newRoot)
if err != nil {
return err
}
newRoot = retRoot.(*RootValue)
delete(cv.types, db)
}
if cv.casts != nil && cv.casts[db] != nil && cv.casts[db].DiffersFrom(ctx, root) {
retRoot, err := cv.casts[db].UpdateRoot(ctx, newRoot)
if err != nil {
return err
}
newRoot = retRoot.(*RootValue)
delete(cv.casts, db)
}
// Setting the session working root doesn't do a check to see if anything actually changed or not before marking that
// branch state dirty, and dolt only allows a single dirty working set per commit. So it's important here to only
// update the session root if something actually changed for that db.
if err, rootChanged := rootValueChanged(newRoot, root); rootChanged {
if err = session.SetWorkingRoot(ctx, db, newRoot); err != nil {
// TODO: We need a way to see if the session has a writeable working root
// (new interface method on session probably), and avoid setting it if so
if errors.Is(err, doltdb.ErrOperationNotSupportedInDetachedHead) {
return nil
}
return err
}
} else if err != nil {
return err
}
return nil
}
// rootValueChanged returns whether the new root value is different from the old one
func rootValueChanged(newRoot *RootValue, root *RootValue) (error, bool) {
if newRoot == root {
return nil, false
}
newHash, err := newRoot.HashOf()
if err != nil {
return err, false
}
oldHash, err := root.HashOf()
if err != nil {
return err, false
}
if newHash == oldHash {
return nil, false
}
return nil, true
}
// databasesInContext returns all databases found within the context values.
func databasesInContext(ctx *sql.Context, cv *contextValues) []string {
dbs := make(map[string]struct{})
if cv.seqs != nil {
for db := range cv.seqs {
dbs[db] = struct{}{}
}
}
currentDb := ctx.GetCurrentDatabase()
if len(currentDb) > 0 {
dbs[currentDb] = struct{}{}
}
return slices.Sorted(maps.Keys(dbs))
}
// clear removes the collection from the cache.
func (cv *contextValues) clear(objID objinterface.RootObjectID) {
switch objID {
case objinterface.RootObjectID_None:
// Nothing to cache with this
case objinterface.RootObjectID_Sequences:
cv.seqs = nil
case objinterface.RootObjectID_Types:
cv.types = nil
case objinterface.RootObjectID_Functions:
cv.funcs = nil
case objinterface.RootObjectID_Triggers:
cv.trigs = nil
case objinterface.RootObjectID_Extensions:
// We don't cache these
case objinterface.RootObjectID_Conflicts:
// We don't cache these
case objinterface.RootObjectID_Procedures:
cv.procs = nil
case objinterface.RootObjectID_Casts:
cv.casts = nil
default:
panic("unhandled context clear object ID")
}
}
+212
View File
@@ -0,0 +1,212 @@
// 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 dataloader
import (
"bufio"
"fmt"
"io"
"strings"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/table"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/server/types"
)
// CsvDataLoader is an implementation of DataLoader that reads data from chunks of CSV files and inserts them into a table.
type CsvDataLoader struct {
results LoadDataResults
partialRecord string
nextDataChunk *bufio.Reader
colTypes []*types.DoltgresType
sch sql.Schema
removeHeader bool
delimiter string
}
func (cdl *CsvDataLoader) SetNextDataChunk(ctx *sql.Context, data *bufio.Reader) error {
cdl.nextDataChunk = data
return nil
}
var _ DataLoader = (*CsvDataLoader)(nil)
const defaultCsvDelimiter = ","
// NewCsvDataLoader creates a new DataLoader instance that will produce rows for the schema provided.
// |header| is true, the first line of the data will be treated as a header and ignored. If |delimiter| is not the empty
// string, it will be used as the delimiter separating value.
func NewCsvDataLoader(colNames []string, sch sql.Schema, delimiter string, header bool) (*CsvDataLoader, error) {
colTypes, reducedSch, err := getColumnTypes(colNames, sch)
if err != nil {
return nil, err
}
if delimiter == "" {
delimiter = defaultCsvDelimiter
}
return &CsvDataLoader{
colTypes: colTypes,
sch: reducedSch,
removeHeader: header,
delimiter: delimiter,
}, nil
}
// nextRow attempts to read the next row from the data and return it, and returns true if a row was read
func (cdl *CsvDataLoader) nextRow(ctx *sql.Context, reader *csvReader) (sql.Row, bool, error) {
if cdl.removeHeader {
_, err := reader.readLine()
cdl.removeHeader = false
if err != nil {
return nil, false, err
}
}
record, err := reader.ReadSqlRow()
if err != nil {
if ple, ok := err.(*partialLineError); ok {
cdl.partialRecord = ple.partialLine
return nil, false, nil
}
// csvReader will return a BadRow error if it encounters an input line without the
// correct number of columns. If we see the end of data marker, then break out of the
// loop and return from this function without returning an error.
if _, ok := err.(*table.BadRow); ok {
if len(record) == 1 && record[0] == "\\." {
return nil, false, nil
}
}
if err != io.EOF {
return nil, false, err
}
recordValues := make([]string, 0, len(record))
for _, v := range record {
recordValues = append(recordValues, fmt.Sprintf("%v", v))
}
cdl.partialRecord = strings.Join(recordValues, ",")
return nil, false, nil
}
// If we see the end of data marker, then break out of the loop. Normally this will happen in the code
// above when we receive a BadRow error, since there won't be enough values, but if a table only has
// one column, we won't get a BadRow error, and we'll handle the end of data marker here.
if len(record) == 1 && record[0] == "\\." {
return nil, false, nil
}
if len(record) > len(cdl.colTypes) {
return nil, false, errors.Errorf("extra data after last expected column")
} else if len(record) < len(cdl.colTypes) {
return nil, false, errors.Errorf(`missing data for column "%s"`, cdl.sch[len(record)].Name)
}
// Cast the values using I/O input
row := make(sql.Row, len(cdl.colTypes))
for i := range cdl.colTypes {
if record[i] == nil {
row[i] = nil
} else {
row[i], err = cdl.colTypes[i].IoInput(ctx, fmt.Sprintf("%v", record[i]))
if err != nil {
return nil, false, err
}
}
}
return row, true, nil
}
// Finish implements the DataLoader interface
func (cdl *CsvDataLoader) Finish(ctx *sql.Context) (*LoadDataResults, error) {
// If there is partial data from the last chunk that hasn't been inserted, return an error.
if cdl.partialRecord != "" {
return nil, errors.Errorf("partial record (%s) found at end of data load", cdl.partialRecord)
}
return &cdl.results, nil
}
func (cdl *CsvDataLoader) Resolved() bool {
return true
}
func (cdl *CsvDataLoader) String() string {
return "CsvDataLoader"
}
func (cdl *CsvDataLoader) Schema(ctx *sql.Context) sql.Schema {
return cdl.sch
}
func (cdl *CsvDataLoader) Children() []sql.Node {
return nil
}
func (cdl *CsvDataLoader) WithChildren(ctx *sql.Context, children ...sql.Node) (sql.Node, error) {
if len(children) != 0 {
return nil, sql.ErrInvalidChildrenNumber.New(cdl, len(children), 0)
}
return cdl, nil
}
func (cdl *CsvDataLoader) IsReadOnly() bool {
return true
}
type csvRowIter struct {
cdl *CsvDataLoader
reader *csvReader
}
func (c csvRowIter) Next(ctx *sql.Context) (sql.Row, error) {
row, hasNext, err := c.cdl.nextRow(ctx, c.reader)
if err != nil {
return nil, err
}
// TODO: this isn't the best way to handle the count of rows, something like a RowUpdateAccumulator would be better
if hasNext {
c.cdl.results.RowsLoaded++
} else {
return nil, io.EOF
}
return row, nil
}
func (c csvRowIter) Close(context *sql.Context) error {
return nil
}
var _ sql.RowIter = (*csvRowIter)(nil)
func (cdl *CsvDataLoader) RowIter(ctx *sql.Context, r sql.Row) (sql.RowIter, error) {
combinedReader := NewStringPrefixReader(cdl.partialRecord, cdl.nextDataChunk)
cdl.partialRecord = ""
csvReader, err := newCsvReaderWithDelimiter(combinedReader, cdl.delimiter)
if err != nil {
return nil, err
}
return &csvRowIter{cdl: cdl, reader: csvReader}, nil
}
+364
View File
@@ -0,0 +1,364 @@
// 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 dataloader
import (
"bufio"
"bytes"
"context"
"encoding/csv"
"io"
"unicode/utf8"
"github.com/dolthub/dolt/go/libraries/doltcore/table"
"github.com/dolthub/go-mysql-server/sql"
textunicode "golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
)
// csvReadBufSize is the size of the buffer used when reading the csv file.
var csvReadBufSize = 256 * 1024
// partialLineError is an error type that is returned when an incomplete record is read from a CSV
// file. This can occur when a CSV document is split across multiple messages and the message
// boundaries don't line up with CSV record boundaries. Callers should use this error to record the
// partial line, so that it can be prepended to the next message.
type partialLineError struct {
partialLine string
}
var _ error = partialLineError{}
func (ple partialLineError) Error() string {
return "incomplete record found at end of CSV data: " + ple.partialLine
}
// csvReader implements TableReader. It reads csv files and returns rows.
//
// This implementation is adapted from the CSVReader in dolt, which is a fork
// of the standard Golang CSV reader. The main differences with the Golang std
// library implementation are that this parser has been adapted to differentiate
// between quoted and unquoted empty strings (for distinguishing between the empty
// string and NULL), and to use multi-rune delimiters. This adaptation removes the
// comment feature and the lazyQuotes option.
//
// Additionally, this fork of the dolt implementation removes some dolt specific
// features and adds support for a few Postgres requirements, such as allowing for
// the full CSV document to be arbitrarily split into multiple messages and for
// incomplete/partial lines to be communicated to the caller.
type csvReader struct {
closer io.Closer
bRd *bufio.Reader
isDone bool
delim []byte
numLine int
fieldsPerRecord int
}
// NewCsvReader creates a csvReader from a given ReadCloser.
//
// The interpretation of the bytes of the supplied reader is a little murky. If
// there is a UTF8, UTF16LE or UTF16BE BOM as the first bytes read, then the
// BOM is stripped and the remaining contents of the reader are treated as that
// encoding. If we are not in any of those marked encodings, then some of the
// bytes go uninterpreted until we get to the SQL layer. It is currently the
// case that newlines must be encoded as a '0xa' byte.
func NewCsvReader(r io.ReadCloser) (*csvReader, error) {
return newCsvReaderWithDelimiter(r, ",")
}
// newCsvReaderWithDelimiter creates a csvReader from a given ReadCloser, |r|, using
// the |delimiter| as the field delimiter in the parsed data.
func newCsvReaderWithDelimiter(r io.ReadCloser, delimiter string) (*csvReader, error) {
textReader := transform.NewReader(r, textunicode.BOMOverride(transform.Nop))
br := bufio.NewReaderSize(textReader, csvReadBufSize)
return &csvReader{
closer: r,
bRd: br,
isDone: false,
delim: []byte(delimiter),
}, nil
}
func (csvr *csvReader) ReadSqlRow() (sql.Row, error) {
if csvr.isDone {
return nil, io.EOF
}
rowVals, err := csvr.csvReadRecords(nil)
if err == io.EOF {
csvr.isDone = true
return nil, io.EOF
}
sqlRows := rowValsToSQLRows(rowVals)
if err != nil {
if _, ok := err.(*partialLineError); ok {
return nil, err
}
return sqlRows, table.NewBadRow(nil, err.Error())
}
return sqlRows, nil
}
func rowValsToSQLRows(rowVals []*string) sql.Row {
var sqlRow sql.Row
for _, rowVal := range rowVals {
if rowVal == nil {
sqlRow = append(sqlRow, nil)
} else {
sqlRow = append(sqlRow, *rowVal)
}
}
return sqlRow
}
// Close should release resources being held
func (csvr *csvReader) Close(ctx context.Context) error {
if csvr.closer != nil {
err := csvr.closer.Close()
csvr.closer = nil
return err
}
return nil
}
// Functions below this line are borrowed or adapted from encoding/csv/reader.go
// lengthNL returns 1 if the last byte in b is a newline, 0 otherwise.
func lengthNL(b []byte) int {
if len(b) > 0 && b[len(b)-1] == '\n' {
return 1
}
return 0
}
// readLine reads the next line (with the trailing endline).
// If EOF is hit without a trailing endline, it will be omitted.
// If some bytes were read, then the error is never io.EOF.
// The result is only valid until the next call to readLine.
func (csvr *csvReader) readLine() ([]byte, error) {
var rawBuffer []byte
line, err := csvr.bRd.ReadSlice('\n')
if err == bufio.ErrBufferFull {
rawBuffer = append(rawBuffer[:0], line...)
for err == bufio.ErrBufferFull {
line, err = csvr.bRd.ReadSlice('\n')
rawBuffer = append(rawBuffer, line...)
}
line = rawBuffer
}
if len(line) > 0 && err == io.EOF {
err = nil
// For backwards compatibility, drop trailing \r before EOF.
if line[len(line)-1] == '\r' {
line = line[:len(line)-1]
}
}
csvr.numLine++
// Normalize \r\n to \n on all input lines.
if n := len(line); n >= 2 && line[n-2] == '\r' && line[n-1] == '\n' {
line[n-2] = '\n'
line = line[:n-1]
}
// If the line does NOT end with a newline, then we must have read a partial record
if len(line) > 0 && lengthNL(line) == 0 {
return nil, &partialLineError{string(line)}
}
return line, err
}
type recordState struct {
line []byte
// recordBuffer holds the unescaped fields, one after another.
// The fields can be accessed by using the indexes in fieldIndexes.
// E.g., For the row `a,"b","c""d",e`, recordBuffer will contain `abc"de`
// and fieldIndexes will contain the indexes [1, 2, 5, 6].
recordBuffer []byte
fieldIndexes []int
rawData []byte
}
func (csvr *csvReader) csvReadRecords(dst []*string) ([]*string, error) {
recordStartline := csvr.numLine // Starting line for record
var rs recordState
var err error
for err == nil {
rs = recordState{}
rs.line, err = csvr.readLine()
rs.rawData = append(rs.rawData, rs.line...)
if err == nil && len(rs.line) == lengthNL(rs.line) {
continue // Skip empty lines
}
break
}
if err != nil {
return nil, err
}
// nullString indicates whether to interpret an empty string as a NULL
// only empty strings escaped with double quotes will be non-null
nullString := make(map[int]bool)
fieldIdx := 0
kontinue := true
for kontinue {
// Parse each field in the record.
keep := true
if len(rs.line) == 0 || rs.line[0] != '"' {
kontinue, keep, err = csvr.parseField(&rs)
if !keep {
nullString[fieldIdx] = true
}
} else {
kontinue, err = csvr.parseQuotedField(&rs)
if err != nil {
return nil, err
}
}
fieldIdx++
}
// Create a single string and create slices out of it.
// This pins the memory of the fields together, but allocates once.
str := string(rs.recordBuffer) // Convert to string once to batch allocations
dst = dst[:0]
if cap(dst) < len(rs.fieldIndexes) {
dst = make([]*string, len(rs.fieldIndexes))
}
dst = dst[:len(rs.fieldIndexes)]
var preIdx int
for i, idx := range rs.fieldIndexes {
_, ok := nullString[i]
if ok {
dst[i] = nil
} else {
s := str[preIdx:idx]
dst[i] = &s
}
preIdx = idx
}
// Check or update the expected fields per record.
if csvr.fieldsPerRecord > 0 {
if len(dst) != csvr.fieldsPerRecord && err == nil {
err = &csv.ParseError{StartLine: recordStartline, Line: csvr.numLine, Err: csv.ErrFieldCount}
}
} else if csvr.fieldsPerRecord == 0 {
csvr.fieldsPerRecord = len(dst)
}
return dst, err
}
func (csvr *csvReader) parseField(rs *recordState) (kontinue bool, keep bool, err error) {
i := bytes.Index(rs.line, csvr.delim)
field := rs.line
if i >= 0 {
field = field[:i]
} else {
field = field[:len(field)-lengthNL(field)]
}
rs.recordBuffer = append(rs.recordBuffer, field...)
rs.fieldIndexes = append(rs.fieldIndexes, len(rs.recordBuffer))
keep = len(field) != 0 // discard unquoted empty strings
if i >= 0 {
dl := len(csvr.delim)
rs.line = rs.line[i+dl:]
return true, keep, err
}
return false, keep, err
}
func (csvr *csvReader) parseQuotedField(rs *recordState) (kontinue bool, err error) {
const quoteLen = len(`"`)
dl := len(csvr.delim)
recordStartLine := csvr.numLine
// full copy needed here because we append rs.line to fullField, and this can result in buffer corruption in
// some cases (namely when windows line endings are present)
fullField := make([]byte, len(rs.line))
copy(fullField, rs.line)
// Quoted string field
rs.line = rs.line[quoteLen:]
for {
i := bytes.IndexByte(rs.line, '"')
if i >= 0 {
// Hit next quote.
rs.recordBuffer = append(rs.recordBuffer, rs.line[:i]...)
rs.line = rs.line[i+quoteLen:]
atDelimiter := len(rs.line) >= dl && bytes.Equal(rs.line[:dl], csvr.delim)
nextRune, _ := utf8.DecodeRune(rs.line)
switch {
case atDelimiter:
// `"<delimiter>` sequence (end of field).
rs.line = rs.line[dl:]
rs.fieldIndexes = append(rs.fieldIndexes, len(rs.recordBuffer))
return true, err
case nextRune == '"':
// `""` sequence (append quote).
rs.recordBuffer = append(rs.recordBuffer, '"')
rs.line = rs.line[quoteLen:]
case lengthNL(rs.line) == len(rs.line):
// `"\n` sequence (end of line).
rs.fieldIndexes = append(rs.fieldIndexes, len(rs.recordBuffer))
return false, err
default:
// `"*` sequence (invalid non-escaped quote).
col := utf8.RuneCount(fullField[:len(fullField)-len(rs.line)-quoteLen])
err = &csv.ParseError{StartLine: recordStartLine, Line: csvr.numLine, Column: col, Err: csv.ErrQuote}
return false, err
}
} else if len(rs.line) > 0 {
// Hit end of line (copy all data so far).
rs.recordBuffer = append(rs.recordBuffer, rs.line...)
if err != nil {
return false, err
}
rs.line, err = csvr.readLine()
rs.rawData = append(rs.rawData, rs.line...)
if err == io.EOF {
err = nil
}
// If we get a partialLineError, populate the partialLine field with the full record data
// since quoted fields can span multiple lines, otherwise we wouldn't capture the initial
// lines of this record.
if ple, ok := err.(*partialLineError); ok {
ple.partialLine = string(rs.rawData)
return true, ple
}
fullField = append(fullField, rs.line...)
} else {
// Abrupt end of file
if err == nil {
return false, &partialLineError{string(rs.rawData)}
}
rs.fieldIndexes = append(rs.fieldIndexes, len(rs.recordBuffer))
return false, err
}
}
}
+73
View File
@@ -0,0 +1,73 @@
// 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 dataloader
import (
"bufio"
"github.com/cockroachdb/errors"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/server/types"
)
// DataLoader allows callers to insert rows from multiple chunks into a table. Rows encoded in each chunk will not
// necessarily end cleanly on a chunk boundary, so DataLoader implementations must handle recognizing partial, or
// incomplete records, and saving that partial record until the next call to LoadChunk, so that it may be prefixed
// with the incomplete record.
type DataLoader interface {
sql.ExecSourceRel
// SetNextDataChunk sets the next data chunk to be processed by the DataLoader. Data records
// are not guaranteed to start and end cleanly on chunk boundaries, so implementations must recognize incomplete
// records and save them to prepend on the next processed chunk.
SetNextDataChunk(ctx *sql.Context, data *bufio.Reader) error
// Finish finalizes the current load operation and cleans up any resources used. Implementations should check that
// the last call to LoadChunk did not end with an incomplete record and return an error to the caller if so. The
// returned LoadDataResults describe the load operation, including how many rows were inserted.
Finish(ctx *sql.Context) (*LoadDataResults, error)
}
// LoadDataResults contains the results of a load data operation, including the number of rows loaded.
type LoadDataResults struct {
// RowsLoaded contains the total number of rows inserted during a load data operation.
RowsLoaded int32
}
// getColumnTypes returns the types of the columns in the schema that match the provided column names, in the order
// they are provided. If a subset of column names are provided, the returned types will only contain those columns.
// If the column names are not found in the schema, an error is returned.
func getColumnTypes(colNames []string, sch sql.Schema) ([]*types.DoltgresType, sql.Schema, error) {
colTypes := make([]*types.DoltgresType, len(colNames))
reducedSch := make(sql.Schema, len(colNames))
for i, colName := range colNames {
colIdx := sch.IndexOfColName(colName)
if colIdx < 0 {
// should be impossible
return nil, nil, errors.Errorf("column %s not found in schema", colName)
}
col := sch[colIdx]
var ok bool
colTypes[i], ok = col.Type.(*types.DoltgresType)
if !ok {
return nil, nil, errors.Errorf("unsupported column type: name: %s, type: %T", col.Name, col.Type)
}
reducedSch[i] = col
}
return colTypes, reducedSch, nil
}
+59
View File
@@ -0,0 +1,59 @@
// 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 dataloader
import "io"
// stringPrefixReader is an io.ReadCloser that reads from a string prefix before reading from
// another io.Reader. This is used for reassembling partial records across multi-message
// exchanges, since the end of a wire message does not typically line up with the end of a record.
type stringPrefixReader struct {
prefix string
prefixPosition uint
reader io.Reader
}
var _ io.ReadCloser = (*stringPrefixReader)(nil)
// NewStringPrefixReader creates a new stringPrefixReader that first returns the data in |prefix| and
// then returns data from |reader|.
func NewStringPrefixReader(prefix string, reader io.Reader) *stringPrefixReader {
return &stringPrefixReader{
prefix: prefix,
reader: reader,
}
}
// Read implements the io.Reader interface
func (spr *stringPrefixReader) Read(p []byte) (n int, err error) {
if spr.prefixPosition < uint(len(spr.prefix)) {
n = copy(p, spr.prefix[spr.prefixPosition:])
spr.prefixPosition += uint(n)
if n == len(p) {
return n, nil
}
}
read, err := spr.reader.Read(p[n:])
return n + read, err
}
// Close implements the io.Closer interface
func (spr *stringPrefixReader) Close() error {
if closer, ok := spr.reader.(io.Closer); ok {
return closer.Close()
}
return nil
}
+215
View File
@@ -0,0 +1,215 @@
// 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 dataloader
import (
"bufio"
"io"
"strings"
"github.com/cockroachdb/errors"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/server/types"
)
const defaultTextDelimiter = "\t"
const defaultNullChar = "\\N"
// TabularDataLoader tracks the state of a load data operation from a tabular data source.
type TabularDataLoader struct {
results LoadDataResults
partialLine strings.Builder
nextDataChunk *bufio.Reader
colTypes []*types.DoltgresType
sch sql.Schema
delimiterChar string
nullChar string
removeHeader bool
}
var _ DataLoader = (*TabularDataLoader)(nil)
// NewTabularDataLoader creates a new TabularDataLoader to insert into the specified |table| using the specified
// |delimiterChar| and |nullChar|. If |header| is true, the first line of the data will be treated as a header and
// ignored.
func NewTabularDataLoader(colNames []string, tableSch sql.Schema, delimiterChar, nullChar string, header bool) (*TabularDataLoader, error) {
colTypes, reducedSch, err := getColumnTypes(colNames, tableSch)
if err != nil {
return nil, err
}
if delimiterChar == "" {
delimiterChar = defaultTextDelimiter
}
if nullChar == "" {
nullChar = defaultNullChar
}
return &TabularDataLoader{
colTypes: colTypes,
sch: reducedSch,
delimiterChar: delimiterChar,
nullChar: nullChar,
removeHeader: header,
}, nil
}
// nextRow returns the next SQL row from the reader provided, using any previously saved partial line. Returns true if
// there was another row.
func (tdl *TabularDataLoader) nextRow(ctx *sql.Context, data *bufio.Reader) (sql.Row, bool, error) {
if tdl.removeHeader {
_, err := data.ReadString('\n')
tdl.removeHeader = false
if err != nil {
return nil, false, err
}
}
for {
// Read the next line from the file
line, err := data.ReadString('\n')
if err != nil {
if err != io.EOF {
return nil, false, err
}
// bufio.Reader.ReadString will return an error AND a line
// if the final contents of the data does NOT end in the
// delimiter. In this case, that means that we need to save
// the partial line and use it in the next chunk.
tdl.partialLine.WriteString(line)
return nil, false, nil
}
// If we've not reached EOF, then there will be a newline appended to the end that we must remove.
line = strings.TrimSuffix(line, "\n")
// Data with windows line endings will also have a carriage return character that we need to remove.
line = strings.TrimSuffix(line, "\r")
if tdl.partialLine.Len() > 0 {
tdl.partialLine.WriteString(line)
line = tdl.partialLine.String()
tdl.partialLine.Reset()
}
// If we see the end of data marker, return early
if line == `\.` {
return nil, false, nil
}
// Skip over empty lines
if len(line) == 0 {
continue
}
// Split the values by the delimiter, ensuring the correct number of values have been read
values := strings.Split(line, tdl.delimiterChar)
if len(values) > len(tdl.colTypes) {
return nil, false, errors.Errorf("extra data after last expected column")
} else if len(values) < len(tdl.colTypes) {
return nil, false, errors.Errorf(`missing data for column "%s"`, tdl.sch[len(values)].Name)
}
// Cast the values using I/O input
row := make(sql.Row, len(tdl.colTypes))
for i := range tdl.colTypes {
if values[i] == tdl.nullChar {
row[i] = nil
} else {
// We must un-escape strings here since we're receiving everything verbatim
values[i] = strings.ReplaceAll(values[i], `\\`, `\`)
row[i], err = tdl.colTypes[i].IoInput(ctx, values[i])
if err != nil {
return nil, false, err
}
}
}
return row, true, nil
}
}
func (tdl *TabularDataLoader) SetNextDataChunk(ctx *sql.Context, data *bufio.Reader) error {
tdl.nextDataChunk = data
return nil
}
// Finish completes the current load data operation and finalizes the data that has been inserted.
func (tdl *TabularDataLoader) Finish(ctx *sql.Context) (*LoadDataResults, error) {
// If there is partial data from the last chunk that hasn't been inserted, return an error.
if tdl.partialLine.Len() > 0 {
return nil, errors.Errorf("partial line found at end of data load")
}
return &tdl.results, nil
}
func (tdl *TabularDataLoader) Resolved() bool {
return true
}
func (tdl *TabularDataLoader) String() string {
return "TabularDataLoader"
}
func (tdl *TabularDataLoader) Schema(ctx *sql.Context) sql.Schema {
return tdl.sch
}
func (tdl *TabularDataLoader) Children() []sql.Node {
return nil
}
func (tdl *TabularDataLoader) WithChildren(ctx *sql.Context, children ...sql.Node) (sql.Node, error) {
if len(children) != 0 {
return nil, sql.ErrInvalidChildrenNumber.New(tdl, len(children), 0)
}
return tdl, nil
}
func (tdl *TabularDataLoader) IsReadOnly() bool {
return true
}
type tabularRowIter struct {
tdl *TabularDataLoader
reader *bufio.Reader
}
func (t tabularRowIter) Next(ctx *sql.Context) (sql.Row, error) {
row, hasNext, err := t.tdl.nextRow(ctx, t.reader)
if err != nil {
return nil, err
}
// TODO: this isn't the best way to handle the count of rows, something like a RowUpdateAccumulator would be better
if hasNext {
t.tdl.results.RowsLoaded++
} else {
return nil, io.EOF
}
return row, nil
}
func (t tabularRowIter) Close(context *sql.Context) error {
return nil
}
func (tdl *TabularDataLoader) RowIter(ctx *sql.Context, r sql.Row) (sql.RowIter, error) {
return &tabularRowIter{tdl: tdl, reader: tdl.nextDataChunk}, nil
}
+234
View File
@@ -0,0 +1,234 @@
// Copyright 2025 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 extensions
import (
"cmp"
"context"
"maps"
"slices"
"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/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
)
// Collection contains a collection of loaded extensions.
type Collection struct {
accessCache map[id.Extension]Extension // This cache is used for general access
idCache []id.Extension // This cache simply contains the name of every loaded extension
mapHash hash.Hash // This is cached so that we don't have to calculate the hash every time
underlyingMap prolly.AddressMap
ns tree.NodeStore
}
// Extension represents a loaded extension.
type Extension struct {
ExtName id.Extension
Namespace id.Namespace
Relocatable bool
LibIdentifier LibraryIdentifier
// TODO: keep track of what it references so I can later delete them
}
var _ objinterface.Collection = (*Collection)(nil)
var _ objinterface.RootObject = Extension{}
// NewCollection returns a new Collection.
func NewCollection(ctx context.Context, underlyingMap prolly.AddressMap, ns tree.NodeStore) (*Collection, error) {
collection := &Collection{
accessCache: make(map[id.Extension]Extension),
idCache: nil,
mapHash: hash.Hash{},
underlyingMap: underlyingMap,
ns: ns,
}
return collection, collection.reloadCaches(ctx)
}
// GetLoadedExtension returns the loaded extension with the given name. Returns an extension with an invalid ID if it
// cannot be found.
func (pge *Collection) GetLoadedExtension(ctx context.Context, name id.Extension) (Extension, error) {
if f, ok := pge.accessCache[name]; ok {
return f, nil
}
return Extension{}, nil
}
// HasLoadedExtension returns whether the extension has been loaded.
func (pge *Collection) HasLoadedExtension(ctx context.Context, name id.Extension) bool {
_, ok := pge.accessCache[name]
return ok
}
// AddLoadedExtension adds a new extension, that has already been loaded, to the collection.
func (pge *Collection) AddLoadedExtension(ctx context.Context, ext Extension) error {
// First we'll check to see if it exists
if _, ok := pge.accessCache[ext.ExtName]; ok {
return errors.Errorf(`extension "%s" already exists`, ext.ExtName)
}
// Now we'll add the extension to our map
data, err := ext.Serialize(ctx)
if err != nil {
return err
}
h, err := pge.ns.WriteBytes(ctx, data)
if err != nil {
return err
}
mapEditor := pge.underlyingMap.Editor()
if err = mapEditor.Add(ctx, string(ext.ExtName), h); err != nil {
return err
}
newMap, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pge.underlyingMap = newMap
pge.mapHash = pge.underlyingMap.HashOf()
return pge.reloadCaches(ctx)
}
// DropLoadedExtension drops a loaded extension. This should be called when unloading an extension, but this function
// itself does not perform the necessary logic.
func (pge *Collection) DropLoadedExtension(ctx context.Context, names ...id.Extension) error {
// TODO: should this also handle the unloading logic?
if len(names) == 0 {
return nil
}
// Check that each name exists before performing any deletions
for _, name := range names {
if _, ok := pge.accessCache[name]; !ok {
return errors.Errorf(`extension "%s" does not exist`, name)
}
}
// Now we'll remove the extensions from the map
mapEditor := pge.underlyingMap.Editor()
for _, name := range names {
err := mapEditor.Delete(ctx, string(name))
if err != nil {
return err
}
}
newMap, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pge.underlyingMap = newMap
pge.mapHash = pge.underlyingMap.HashOf()
return pge.reloadCaches(ctx)
}
// Clone returns a new *Collection with the same contents as the original.
func (pge *Collection) Clone(ctx context.Context) *Collection {
return &Collection{
accessCache: maps.Clone(pge.accessCache),
idCache: slices.Clone(pge.idCache),
mapHash: pge.mapHash,
underlyingMap: pge.underlyingMap,
ns: pge.ns,
}
}
// Map writes any cached sequences to the underlying map, and then returns the underlying map.
func (pge *Collection) Map(ctx context.Context) (prolly.AddressMap, error) {
return pge.underlyingMap, nil
}
// 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 (pge *Collection) DiffersFrom(ctx context.Context, root objinterface.RootValue) bool {
hashOnGivenRoot, err := pge.LoadCollectionHash(ctx, root)
if err != nil {
return true
}
if pge.mapHash.Equal(hashOnGivenRoot) {
return false
}
// An empty map should match an uninitialized collection on the root
count, err := pge.underlyingMap.Count()
if err == nil && count == 0 && hashOnGivenRoot.IsEmpty() {
return false
}
return true
}
// reloadCaches writes the underlying map's contents to the caches.
func (pge *Collection) reloadCaches(ctx context.Context) error {
count, err := pge.underlyingMap.Count()
if err != nil {
return err
}
clear(pge.accessCache)
pge.mapHash = pge.underlyingMap.HashOf()
pge.idCache = make([]id.Extension, 0, count)
return pge.underlyingMap.IterAll(ctx, func(_ string, h hash.Hash) error {
if h.IsEmpty() {
return nil
}
data, err := pge.ns.ReadBytes(ctx, h)
if err != nil {
return err
}
ext, err := DeserializeExtension(ctx, data)
if err != nil {
return err
}
pge.accessCache[ext.ExtName] = ext
pge.idCache = append(pge.idCache, ext.ExtName)
return nil
})
}
// CompareVersions compares the major and minor version of the extension versus the given extension.
func (ext Extension) CompareVersions(other Extension) int {
return cmp.Or(
cmp.Compare(ext.LibIdentifier.Version().Major(), other.LibIdentifier.Version().Major()),
cmp.Compare(ext.LibIdentifier.Version().Minor(), other.LibIdentifier.Version().Minor()),
)
}
// GetID implements the interface objinterface.RootObject.
func (ext Extension) GetID() id.Id {
return ext.ExtName.AsId()
}
// GetRootObjectID implements the interface objinterface.RootObject.
func (ext Extension) GetRootObjectID() objinterface.RootObjectID {
return objinterface.RootObjectID_Extensions
}
// HashOf implements the interface objinterface.RootObject.
func (ext Extension) HashOf(ctx context.Context) (hash.Hash, error) {
data, err := ext.Serialize(ctx)
if err != nil {
return hash.Hash{}, err
}
return hash.Of(data), nil
}
// Name implements the interface objinterface.RootObject.
func (ext Extension) Name() doltdb.TableName {
return doltdb.TableName{Name: ext.ExtName.Name()}
}
+134
View File
@@ -0,0 +1,134 @@
// Copyright 2025 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 extensions
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/merge"
"github.com/dolthub/dolt/go/store/hash"
"github.com/dolthub/dolt/go/store/prolly"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/flatbuffers/gen/serial"
)
// storage is used to read from and write to the root.
var storage = objinterface.RootObjectSerializer{
Bytes: (*serial.RootValue).ExtensionsBytes,
RootValueAdd: serial.RootValueAddExtensions,
}
// HandleMerge implements the interface objinterface.Collection.
func (*Collection) HandleMerge(ctx context.Context, mro merge.MergeRootObject) (doltdb.RootObject, *merge.MergeStats, error) {
ourExt := mro.OurRootObj.(Extension)
theirExt := mro.TheirRootObj.(Extension)
// Ensure that they have the same ID
if ourExt.ExtName != theirExt.ExtName {
return nil, nil, errors.Newf("attempted to merge different extensions: `%s` and `%s`",
ourExt.Name().String(), theirExt.Name().String())
}
ourHash, err := ourExt.HashOf(ctx)
if err != nil {
return nil, nil, err
}
theirHash, err := theirExt.HashOf(ctx)
if err != nil {
return nil, nil, err
}
// We always keep the newest extension. I don't think this is actually valid since old extensions are very likely
// to have invalid function signatures, but this is a start for now.
// TODO: figure out a better method
if ourHash.Equal(theirHash) || ourExt.CompareVersions(theirExt) >= 0 {
return mro.OurRootObj, &merge.MergeStats{
Operation: merge.TableUnmodified,
Adds: 0,
Deletes: 0,
Modifications: 0,
DataConflicts: 0,
SchemaConflicts: 0,
ConstraintViolations: 0,
}, nil
} else {
return mro.TheirRootObj, &merge.MergeStats{
Operation: merge.TableModified,
Adds: 0,
Deletes: 0,
Modifications: 1,
DataConflicts: 0,
SchemaConflicts: 0,
ConstraintViolations: 0,
}, nil
}
}
// LoadCollection implements the interface objinterface.Collection.
func (*Collection) LoadCollection(ctx context.Context, root objinterface.RootValue) (objinterface.Collection, error) {
return LoadExtensions(ctx, root)
}
// LoadCollectionHash implements the interface objinterface.Collection.
func (*Collection) LoadCollectionHash(ctx context.Context, root objinterface.RootValue) (hash.Hash, error) {
m, ok, err := storage.GetProllyMap(ctx, root)
if err != nil || !ok {
return hash.Hash{}, err
}
return m.HashOf(), nil
}
// LoadExtensions loads the extensions collection from the given root.
func LoadExtensions(ctx context.Context, root objinterface.RootValue) (*Collection, error) {
m, ok, err := storage.GetProllyMap(ctx, root)
if err != nil {
return nil, err
}
if !ok {
m, err = prolly.NewEmptyAddressMap(root.NodeStore())
if err != nil {
return nil, err
}
}
return NewCollection(ctx, m, root.NodeStore())
}
// ResolveNameFromObjects implements the interface objinterface.Collection.
func (*Collection) ResolveNameFromObjects(ctx context.Context, name doltdb.TableName, rootObjects []objinterface.RootObject) (doltdb.TableName, id.Id, error) {
tempCollection := Collection{
accessCache: make(map[id.Extension]Extension),
}
for _, rootObject := range rootObjects {
if obj, ok := rootObject.(Extension); ok {
tempCollection.accessCache[obj.ExtName] = obj
}
}
return tempCollection.ResolveName(ctx, name)
}
// Serializer implements the interface objinterface.Collection.
func (*Collection) Serializer() objinterface.RootObjectSerializer {
return storage
}
// UpdateRoot implements the interface objinterface.Collection.
func (pge *Collection) UpdateRoot(ctx context.Context, root objinterface.RootValue) (objinterface.RootValue, error) {
m, err := pge.Map(ctx)
if err != nil {
return nil, err
}
return storage.WriteProllyMap(ctx, root, m)
}
+199
View File
@@ -0,0 +1,199 @@
// Copyright 2025 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 extensions
import (
"fmt"
"strconv"
"sync"
"github.com/cockroachdb/errors"
"github.com/dolthub/doltgresql/core/extensions/pg_extension"
)
var (
cachedError error // TODO: is it better for this to be a local object instead of a global since it's always returned?
allExtensions map[string]*pg_extension.ExtensionFiles // TODO: should use id.Extension instead of a string
allLibraries map[string]*pg_extension.Library // TODO: close these at some point
extMutex = &sync.Mutex{}
libMutex = &sync.Mutex{}
)
// Version 0 identifiers are encoded like the following:
// 00AAA1111111111BBB
// `00` is a two-digit version specifier, and we only have version 0 for now
// `AAA` is the three-letter platform specifier
// `1111111111` is the ten-digit library version specifier (i.e. an encoded form of 1.0, 1.5, etc.)
// `BBB` is the extension name, and may be of any length (will have at least 1 character)
// LibraryIdentifier points to a specific extension, as extension functions are dependent on the extension name,
// version, and originating platform (as different platforms may encode data differently).
type LibraryIdentifier string
// InvalidIdentifierReason gives a reason as to why an Identifier is invalid.
type InvalidIdentifierReason uint8
const (
InvalidIdentifierReason_MismatchedPlatform InvalidIdentifierReason = iota
InvalidIdentifierReason_MissingLibrary
InvalidIdentifierReason_InvalidVersion
)
// InvalidIdentifier represents an invalid LibraryIdentifier, providing both the LibraryIdentifier and the reason that
// it is invalid.
type InvalidIdentifier struct {
Identifier LibraryIdentifier
Reason InvalidIdentifierReason
}
// GetExtension returns the extension matching the given name. Returns an error if the extension cannot be found.
func GetExtension(name string) (_ *pg_extension.ExtensionFiles, err error) {
extMutex.Lock()
defer extMutex.Unlock()
if cachedError != nil {
return nil, cachedError
}
if allExtensions == nil {
allLibraries = make(map[string]*pg_extension.Library)
allExtensions, err = pg_extension.LoadExtensions()
if err != nil {
allExtensions = make(map[string]*pg_extension.ExtensionFiles)
cachedError = err
return nil, err
}
}
ext, ok := allExtensions[name]
if !ok {
return nil, errors.Errorf(`could not open extension control file "%s.control"`, name)
}
return ext, nil
}
// GetExtensionFunction returns the function inside the extension matching the given names. Returns an error if the
// extension or function cannot be found.
func GetExtensionFunction(identifier LibraryIdentifier, funcName string) (_ pg_extension.Function, err error) {
libMutex.Lock()
defer libMutex.Unlock()
extName := identifier.ExtensionName()
if identifier.Platform() != pg_extension.PLATFORM {
return pg_extension.Function{}, errors.Errorf(
`function "%s" was initialized through "%s" on a different platform, which is not supported`, funcName, extName)
}
lib, ok := allLibraries[extName]
if !ok {
ext, err := GetExtension(extName)
if err != nil {
return pg_extension.Function{}, err
}
lib, err = ext.LoadLibrary()
if err != nil {
return pg_extension.Function{}, err
}
lib.Version = ext.Control.DefaultVersion
allLibraries[extName] = lib
}
if lib.Version != identifier.Version() {
return pg_extension.Function{}, errors.Errorf(
`function "%s" was initialized through "%s" v%s, the current platform only supports v%s"`,
funcName, extName, identifier.Version().String(), lib.Version.String())
}
libFunc, ok := lib.Funcs[funcName]
if !ok {
return pg_extension.Function{}, errors.Errorf(`extension "%s" does not declare the function "%s"`, extName, funcName)
}
return libFunc, nil
}
// FindInvalidIdentifiers returns all identifiers that are not valid for the current environment. If the return is
// empty, then that means all of the given identifiers are valid.
func FindInvalidIdentifiers(identifiers ...LibraryIdentifier) []InvalidIdentifier {
extMutex.Lock()
defer extMutex.Unlock()
var invalidIdentifiers []InvalidIdentifier
if cachedError != nil {
invalidIdentifiers = make([]InvalidIdentifier, 0, len(identifiers))
for _, identifier := range identifiers {
invalidIdentifiers = append(invalidIdentifiers, InvalidIdentifier{
Identifier: identifier,
Reason: InvalidIdentifierReason_MissingLibrary,
})
}
return invalidIdentifiers
}
for _, identifier := range identifiers {
if identifier.Platform() != pg_extension.PLATFORM {
invalidIdentifiers = append(invalidIdentifiers, InvalidIdentifier{
Identifier: identifier,
Reason: InvalidIdentifierReason_MismatchedPlatform,
})
continue
}
ext, ok := allExtensions[identifier.ExtensionName()]
if !ok {
invalidIdentifiers = append(invalidIdentifiers, InvalidIdentifier{
Identifier: identifier,
Reason: InvalidIdentifierReason_MissingLibrary,
})
continue
}
if ext.Control.DefaultVersion != identifier.Version() {
invalidIdentifiers = append(invalidIdentifiers, InvalidIdentifier{
Identifier: identifier,
Reason: InvalidIdentifierReason_InvalidVersion,
})
continue
}
}
return invalidIdentifiers
}
// GetPlatform returns the current platform that Doltgres is being executed on. This is encoded as a three-letter string.
func GetPlatform() string {
return pg_extension.PLATFORM
}
// CreateLibraryIdentifier creates a LibraryIdentifier using the given information.
func CreateLibraryIdentifier(name string, version pg_extension.Version) LibraryIdentifier {
return LibraryIdentifier(fmt.Sprintf("00%s%010d%s", pg_extension.PLATFORM, version, name))
}
// Platform returns the platform that this LibraryIdentifier was created on.
func (id LibraryIdentifier) Platform() string {
return string(id[2:5])
}
// Version returns the library version that this LibraryIdentifier references.
func (id LibraryIdentifier) Version() pg_extension.Version {
val, err := strconv.ParseUint(string(id[5:15]), 10, 32)
if err != nil {
// We'll panic for now since this should never happen
panic(err)
}
return pg_extension.Version(val)
}
// ExtensionName returns the extension referenced by this LibraryIdentifier.
func (id LibraryIdentifier) ExtensionName() string {
return string(id[15:])
}
// DisplayString returns the identifier as a human-readable string.
func (id LibraryIdentifier) DisplayString() string {
return fmt.Sprintf("%s--%s:%s", id.ExtensionName(), id.Version().String(), id.Platform())
}
+12
View File
@@ -0,0 +1,12 @@
# Finding Extension Function Imports
These are commands that can be used to find the functions that an extension imports, so that we know which ones we need to implement for the extension to load.
## Windows
On Windows, we make use of `dumpbin`, which is installed alongside Visual Studio (the full version, _not_ Code). We are generally only interested in the functions under `postgres.exe`, as the library should load other DLLs as necessary.
```cmd
dumpbin /imports "C:/Program Files/PostgreSQL/15/lib/LIBRARY_NAME.dll"
```
## Linux
On Linux, we make use of the built-in `nm` command. We are interested in the `U` functions that do not have an `@` near the end (as those are usually implemented in external libraries).
```bash
nm -D -u /usr/lib/postgresql/15/lib/LIBRARY_NAME.so
```
+56
View File
@@ -0,0 +1,56 @@
// Copyright 2025 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 pg_extension
/*
#cgo CFLAGS: "-I${SRCDIR}/library"
#include "exports.h"
static inline Datum CallFmgrFunctionC(FunctionCallInfo fcinfo) {
return ((PGFunction)fcinfo->flinfo->fn_addr)(fcinfo);
}
*/
import "C"
import "unsafe"
// Datum is a C pointer to some data. Depending on the function being called, it may not be a pointer that should be
// freed, as some functions return pointers to static memory.
type Datum uintptr
// NullableDatum is used for arguments to Fmgr function calls.
type NullableDatum struct {
Value Datum
IsNull bool
}
// CallFmgrFunction calls the given function and forwards the arguments.
func CallFmgrFunction(fn uintptr, args ...NullableDatum) (result Datum, isNotNull bool) {
fi := Malloc[C.FmgrInfo]()
defer Free(fi)
ZeroMemory(fi)
fc := Malloc[C.FunctionCallInfoBaseData]()
defer Free(fc)
ZeroMemory(fc)
fi.fn_addr = unsafe.Pointer(fn)
fc.flinfo = fi
fc.nargs = C.int16_t(len(args))
for i, arg := range args {
fc.args[i].value = C.Datum(arg.Value)
fc.args[i].isnull = C.bool(arg.IsNull)
}
result = Datum(C.CallFmgrFunctionC(fc))
return result, !bool(fc.isnull) && result != 0
}
@@ -0,0 +1,380 @@
// Copyright 2025 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 pg_extension
import (
"cmp"
"fmt"
"maps"
"os"
"regexp"
"slices"
"strconv"
"strings"
)
// sqlFunctionCapture is a regex to capture the function name as defined in the library. We'll eventually replace this
// and use the nodes from the parser, but this is good enough for the default extensions.
var sqlFunctionCapture = regexp.MustCompile(`(?is)create\s+(?:or\s+replace\s+)?function\s+(.*?)\s*\(.*?\)\s+(?:.*?language c.*?as\s+'.*?'\s*,\s*'(.*?)'.*?;|.*?as\s+'.*?'\s*,\s*'(.*?)'.*?language c.*?;|.*?language c.*?;)`)
// createFunctionStart is a regex to find the beginning of a CREATE FUNCTION statement.
var createFunctionStart = regexp.MustCompile(`(?is)create\s+(?:or\s+replace\s+)?function`)
// ExtensionFiles contains all of the files that are related to or used by an extension.
type ExtensionFiles struct {
Name string
ControlFileName string
SQLFileNames []string
LibraryFileName string
ControlFileDir string
LibraryFileDir string
Control Control
}
// Control contains the contents of the control file.
// https://www.postgresql.org/docs/15/extend-extensions.html#id-1.8.3.20.11
type Control struct {
Directory string
DefaultVersion Version
Comment string
Encoding string
ModulePathname string
Requires []string
Superuser bool
Trusted bool
Relocatable bool
Schema string
Extra map[string]string // All entries in here could not be matched to an expected field
}
// Version specifies the major and minor version numbers for an extension.
type Version uint32
// FilenameVersions returns the versions that were encoded in a filename. `From` is the first number, while `To` is the
// second number. If a filename only specifies a single version, this both `From` and `To` will equal one another.
type FilenameVersions struct {
From Version
To Version
}
// LoadExtensions loads information for all extensions that are in the extensions directory of a local Postgres installation.
func LoadExtensions() (map[string]*ExtensionFiles, error) {
libDir, extDir, err := PostgresDirectories()
if err != nil {
return nil, err
}
dirEntries, err := os.ReadDir(extDir)
if err != nil {
return nil, err
}
libEntries, err := os.ReadDir(libDir)
if err != nil {
return nil, err
}
extensionFiles := make(map[string]*ExtensionFiles)
// Look for the control files first
for _, dirEntry := range dirEntries {
fileName := dirEntry.Name()
if !dirEntry.IsDir() && strings.HasSuffix(fileName, ".control") {
extensionName := strings.TrimSuffix(fileName, ".control")
extensionFiles[extensionName] = &ExtensionFiles{
Name: extensionName,
ControlFileName: fileName,
ControlFileDir: extDir,
}
}
}
// Associate the SQL files and libraries
for _, extFile := range extensionFiles {
for _, dirEntry := range dirEntries {
fileName := dirEntry.Name()
if !dirEntry.IsDir() && strings.HasPrefix(fileName, extFile.Name+"--") && strings.HasSuffix(fileName, ".sql") {
extFile.SQLFileNames = append(extFile.SQLFileNames, fileName)
}
}
for _, libEntry := range libEntries {
fileName := libEntry.Name()
if !libEntry.IsDir() && strings.HasPrefix(fileName, extFile.Name+".") {
extFile.LibraryFileName = fileName
extFile.LibraryFileDir = libDir
}
}
slices.SortFunc(extFile.SQLFileNames, func(aStr, bStr string) int {
a := DecodeFilenameVersions(extFile.Name, aStr)
b := DecodeFilenameVersions(extFile.Name, bStr)
return cmp.Or(
cmp.Compare(a.From, b.From),
cmp.Compare(a.To, b.To),
)
})
// Some SQL files are old migration files that won't apply to us, so we can remove them by starting at the first
// non-migration file.
for nextLoop := true; nextLoop; {
nextLoop = false
for i := 1; i < len(extFile.SQLFileNames); i++ {
if strings.Count(extFile.SQLFileNames[i], "--") == 1 {
extFile.SQLFileNames = extFile.SQLFileNames[i:]
nextLoop = true
break
}
}
}
// Load the control file
if err = extFile.loadControl(); err != nil {
return nil, err
}
}
return extensionFiles, nil
}
// loadControl loads the control file of an extension.
func (extFile *ExtensionFiles) loadControl() error {
data, err := os.ReadFile(fmt.Sprintf("%s/%s", extFile.ControlFileDir, extFile.ControlFileName))
if err != nil {
return err
}
extFile.Control = Control{ // These are the default values
Directory: "",
DefaultVersion: 0,
Comment: "",
Encoding: "",
ModulePathname: "",
Requires: nil,
Superuser: true,
Trusted: false,
Relocatable: false,
Schema: "",
Extra: make(map[string]string),
}
lines := strings.Split(strings.ReplaceAll(string(data), "\r", ""), "\n")
for _, originalLine := range lines {
line := strings.TrimSpace(originalLine)
if commentIdx := strings.Index(line, "#"); commentIdx != -1 {
line = line[:commentIdx]
}
// Line may be empty if it only contained a comment
if len(line) == 0 {
continue
}
equalsSplit := strings.Index(line, "=")
if equalsSplit == -1 {
return fmt.Errorf("malformed `%s.control`:\n%s", extFile.Name, string(data))
}
name := strings.ToLower(strings.TrimSpace(line[:equalsSplit]))
value := line[equalsSplit+1:]
switch name {
case "directory":
extFile.Control.Directory = removeStringQuotations(value)
case "default_version":
value = removeStringQuotations(value)
separator := strings.Index(value, ".")
if separator == -1 {
return fmt.Errorf("malformed `%s.control` line:\n%s", extFile.Name, originalLine)
}
major, err := strconv.Atoi(value[:separator])
if err != nil {
return fmt.Errorf("malformed `%s.control` line:\n%s", extFile.Name, originalLine)
}
minor, err := strconv.Atoi(value[separator+1:])
if err != nil {
return fmt.Errorf("malformed `%s.control` line:\n%s", extFile.Name, originalLine)
}
extFile.Control.DefaultVersion = ToVersion(uint16(major), uint16(minor))
case "comment":
extFile.Control.Comment = removeStringQuotations(value)
case "encoding":
extFile.Control.Encoding = removeStringQuotations(value)
case "module_pathname":
extFile.Control.ModulePathname = removeStringQuotations(value)
case "requires":
value = removeStringQuotations(value)
var entries []string
for _, entry := range strings.Split(value, ",") {
entries = append(entries, strings.TrimSpace(entry))
}
extFile.Control.Requires = entries
case "superuser", "trusted", "relocatable":
value = strings.ToLower(strings.TrimSpace(value))
var boolValue bool
if value == "true" {
boolValue = true
} else if value == "false" {
boolValue = false
} else {
return fmt.Errorf("malformed `%s.control` line:\n%s", extFile.Name, originalLine)
}
switch name {
case "superuser":
extFile.Control.Superuser = boolValue
case "trusted":
extFile.Control.Trusted = boolValue
case "relocatable":
extFile.Control.Relocatable = boolValue
}
case "schema":
extFile.Control.Schema = removeStringQuotations(value)
default:
extFile.Control.Extra[name] = value
}
}
return nil
}
// LoadSQLFiles loads the contents of the SQL files used by the extension. These will be in the order that they need to
// be executed.
func (extFile *ExtensionFiles) LoadSQLFiles() ([]string, error) {
sqlFiles := make([]string, len(extFile.SQLFileNames))
for i, sqlFileName := range extFile.SQLFileNames {
data, err := os.ReadFile(fmt.Sprintf("%s/%s", extFile.ControlFileDir, sqlFileName))
if err != nil {
return nil, err
}
sqlFiles[i] = string(data)
}
return sqlFiles, nil
}
// LoadSQLFunctionNames loads all of the library function names that are used by the extension.
func (extFile *ExtensionFiles) LoadSQLFunctionNames() ([]string, error) {
funcNames := make(map[string]struct{})
for _, sqlFileName := range extFile.SQLFileNames {
data, err := os.ReadFile(fmt.Sprintf("%s/%s", extFile.ControlFileDir, sqlFileName))
if err != nil {
return nil, err
}
fileRemaining := string(data)
OuterLoop:
for {
// We want to advance the file to the start of the next CREATE FUNCTION if one is present
startIdx := createFunctionStart.FindStringIndex(fileRemaining)
if startIdx == nil {
break
}
fileRemaining = fileRemaining[startIdx[0]:]
// We capture the ending semicolon so the regex doesn't match beyond the function definition's boundaries.
endIdx := strings.IndexRune(fileRemaining, ';')
if endIdx == -1 {
break
}
matches := sqlFunctionCapture.FindStringSubmatch(fileRemaining[:endIdx+1])
switch len(matches) {
case 0:
break OuterLoop
case 4:
if len(matches[2]) > 0 {
funcNames[matches[2]] = struct{}{}
} else if len(matches[3]) > 0 {
funcNames[matches[3]] = struct{}{}
} else {
funcNames[matches[1]] = struct{}{}
}
default:
return nil, fmt.Errorf("invalid CREATE FUNCTION string: %s", string(data))
}
// We nudge it forward to guarantee that our next CREATE FUNCTION search will grab the next one
fileRemaining = fileRemaining[6:]
}
}
sortedFuncNames := slices.Sorted(maps.Keys(funcNames))
return sortedFuncNames, nil
}
// LoadLibrary loads the extension as a library.
func (extFile *ExtensionFiles) LoadLibrary() (*Library, error) {
if len(extFile.LibraryFileName) == 0 {
return nil, fmt.Errorf("extension `%s` does not reference a library", extFile.Name)
}
funcNames, err := extFile.LoadSQLFunctionNames()
if err != nil {
return nil, err
}
return LoadLibrary(fmt.Sprintf("%s/%s", extFile.LibraryFileDir, extFile.LibraryFileName), funcNames)
}
// ToVersion creates a version from the given major and minor version numbers.
func ToVersion(major uint16, minor uint16) Version {
return (Version(major) << 16) + Version(minor)
}
// Major returns the encoded major version number.
func (v Version) Major() uint16 {
return uint16(v >> 16)
}
// Minor returns the encoded minor version number.
func (v Version) Minor() uint16 {
return uint16(v)
}
// String returns the version in the `major.minor` format.
func (v Version) String() string {
return fmt.Sprintf("%d.%d", v.Major(), v.Minor())
}
// DecodeFilenameVersions decodes the version information within the file name. The `sqlFileName` should be the full
// file name (excluding the path), and the SQL file name should contain only the name as
func DecodeFilenameVersions(name string, fileName string) FilenameVersions {
var versionSubsection string
if strings.HasSuffix(fileName, ".sql") {
versionSubsection = strings.TrimSuffix(fileName[len(name)+2: /* We add 2 to account for the -- */], ".sql")
} else if strings.HasSuffix(fileName, ".control") {
versionSubsection = strings.TrimSuffix(fileName[len(name)+2: /* We add 2 to account for the -- */], ".control")
} else {
// The given name is not a .SQL or .CONTROL file, so we'll just return
return FilenameVersions{}
}
var from, to string
if dashIdx := strings.Index(versionSubsection, "--"); dashIdx == -1 {
from = versionSubsection
to = versionSubsection
} else {
from = versionSubsection[:dashIdx]
to = versionSubsection[dashIdx+2:]
}
fromSplit := strings.Index(from, ".")
toSplit := strings.Index(to, ".")
if fromSplit == -1 || toSplit == -1 {
return FilenameVersions{}
}
fromMajor, err := strconv.Atoi(from[:fromSplit])
if err != nil {
return FilenameVersions{}
}
fromMinor, err := strconv.Atoi(from[fromSplit+1:])
if err != nil {
return FilenameVersions{}
}
toMajor, err := strconv.Atoi(to[:toSplit])
if err != nil {
return FilenameVersions{}
}
toMinor, err := strconv.Atoi(to[toSplit+1:])
if err != nil {
return FilenameVersions{}
}
return FilenameVersions{
From: ToVersion(uint16(fromMajor), uint16(fromMinor)),
To: ToVersion(uint16(toMajor), uint16(toMinor)),
}
}
// removeStringQuotations removes the single quotes that are used to specify that a value is a string.
func removeStringQuotations(str string) string {
str = strings.TrimSpace(str)
if strings.HasPrefix(str, "'") {
return (str[:len(str)-1])[1:]
}
return str
}
@@ -0,0 +1,40 @@
// Copyright 2025 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 pg_extension
import (
"bytes"
"os/exec"
"strings"
)
// PostgresDirectories returns the installation directories of a local Postgres instance.
func PostgresDirectories() (libDir string, extensionDir string, err error) {
var buffer bytes.Buffer
cmd := exec.Command("pg_config", "--pkglibdir")
cmd.Stdout = &buffer
if err := cmd.Run(); err != nil {
return "", "", err
}
libDir = strings.TrimSpace(buffer.String())
buffer.Reset()
cmd = exec.Command("pg_config", "--sharedir")
cmd.Stdout = &buffer
if err := cmd.Run(); err != nil {
return "", "", err
}
extensionDir = strings.TrimSpace(buffer.String()) + "/extension"
return libDir, extensionDir, nil
}
@@ -0,0 +1,58 @@
$defFile = "postgres.def"
$outDir = Resolve-Path '..\output' -ErrorAction SilentlyContinue -ErrorVariable _dummy
if (-not $outDir) { $outDir = (New-Item -ItemType Directory -Path '..\output').FullName }
$outFile = Join-Path $outDir 'postgres.exe'
function TryVS {
$vswhere = "$Env:ProgramFiles (x86)\Microsoft Visual Studio\Installer\vswhere.exe"
if (-not (Test-Path $vswhere)) { return $false }
$vsRoot = & $vswhere -latest `
-products * `
-requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 `
-property installationPath |
Select-Object -First 1
if (-not $vsRoot) { return $false }
$msvcDir = Get-ChildItem -Path (Join-Path $vsRoot 'VC\Tools\MSVC') |
Sort-Object Name -Descending |
Select-Object -First 1
$linkExe = Join-Path $msvcDir.FullName 'bin\Hostx64\x64\link.exe'
& cmd /c "`"$vsRoot\VC\Auxiliary\Build\vcvars64.bat`" >nul `&`& `"$linkExe`" /DLL /NOENTRY /DEF:$defFile /OUT:`"$outFile`""
return $true
}
function TryGCC {
$gcc = (& where.exe gcc.exe 2>$null | Select-Object -First 1)
if (-not $gcc) { return $false }
$args = @(
"-shared",
"-nostdlib",
$defFile,
"-o", $outFile
)
& $gcc @args
return $true
}
function TryClang {
$lld = (& where.exe lld-link.exe 2>$null | Select-Object -First 1)
if ($lld) {
& $lld /DLL /NOENTRY /DEF:$defFile /OUT:"$outFile"
return $true
}
$clang = (& where.exe clang.exe 2>$null | Select-Object -First 1)
if (-not $clang) { return $false }
$args = @(
"-shared",
"-nostdlib",
$defFile,
"-o", $outFile
)
& $clang @args
return $true
}
if (TryVS) { Write-Host "Definition file built using Visual Studio"; exit 0 }
if (TryGCC) { Write-Host "Definition file built using GCC"; exit 0 }
if (TryClang) { Write-Host "Definition file built using Clang"; exit 0 }
throw "Could not build the definition file"
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
case "$(go env GOOS)" in
windows) ext="dll" ;;
darwin) ext="dylib" ;;
*) ext="so" ;;
esac
# For now, other platforms directly embed the library, but removing this check will allow them to build a shared library as well
if [[ "$(go env GOOS)" != "windows" ]]; then
exit 0
fi
# MacOS requires that exported functions are present in the calling binary, but other platforms use a dynamic library.
# To account for this, we put the exported functions in a normal package by default.
# For MacOS, this works just fine as we import the package.
# To make a dynamic library, we copy the files into a temporary directory and modify the files to create a valid library.
# This lets us use the same code for both scenarios.
mkdir -p temp_lib
trap 'rm -rf temp_lib' EXIT
cp ./*.* ./temp_lib
# For Windows, we also need to build the definition file
if [[ "$(go env GOOS)" == "windows" ]]; then
powershell.exe -File "build_definitions.ps1"
fi
for f in temp_lib/*.go; do
sed 's/^package extension_cgo$/package main/' "$f" > "$f".tmp
mv "$f".tmp "$f"
done
printf "module github.com/dolthub/pg_extension\n\ngo 1.24" > ./temp_lib/go.mod
(
cd temp_lib
CGO_ENABLED=1 go build -buildmode=c-shared -o "../../output/pg_extension.${ext}" .
)
@@ -0,0 +1,57 @@
// Copyright 2025 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.
#include <stdarg.h>
#include <stdio.h>
#include <stdbool.h>
#if defined(_WIN32) || defined(_WIN64)
#define DLLEXPORT __declspec(dllexport)
#else
#define DLLEXPORT __attribute__((visibility("default")))
#endif
static char last_error[512];
DLLEXPORT bool errstart(int elevel, const char* domain) {
last_error[0] = '\0';
return 1;
}
DLLEXPORT bool errstart_cold(int elevel, const char *domain) {
return errstart(elevel, domain);
}
DLLEXPORT int errmsg(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vsnprintf(last_error, sizeof(last_error), fmt, ap);
va_end(ap);
return 0;
}
DLLEXPORT int errmsg_internal(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vsnprintf(last_error, sizeof(last_error), fmt, ap);
va_end(ap);
return 0;
}
DLLEXPORT int errfinish(int dummy, ...) {
if (last_error[0]) {
fprintf(stderr, "Postgres ERROR: %s\n", last_error);
}
return 0;
}
@@ -0,0 +1,145 @@
// Copyright 2025 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 extension_cgo
/*
#include "exports.h"
static inline Datum FunctionPassthrough(PGFunction f, FunctionCallInfoBaseData *fcinfo) {
return (*f)(fcinfo);
}
*/
import "C"
import (
"encoding/hex"
"fmt"
"os"
"unsafe"
)
func main() {}
//export errcode
func errcode(code C.int) C.int {
return code
}
//export palloc
func palloc(sz C.size_t) unsafe.Pointer {
// TODO: should track this pointer so we know to free it later
return C.malloc(sz)
}
//export palloc0
func palloc0(sz C.size_t) unsafe.Pointer {
// TODO: should track this pointer so we know to free it later
ptr := C.malloc(sz)
if ptr != nil {
C.memset(ptr, 0, sz)
}
return ptr
}
//export MemoryContextAlloc
func MemoryContextAlloc(c unsafe.Pointer, sz C.size_t) unsafe.Pointer {
// TODO: should track this pointer so we know to free it later, could use the memory context
return C.malloc(sz)
}
//export MemoryContextAllocExtended
func MemoryContextAllocExtended(c unsafe.Pointer, sz C.size_t, f C.int) unsafe.Pointer {
// TODO: should track this pointer so we know to free it later, could use the memory context
return C.malloc(sz)
}
//export pg_detoast_datum_packed
func pg_detoast_datum_packed(d unsafe.Pointer) unsafe.Pointer {
return d
}
//export text_to_cstring
func text_to_cstring(t unsafe.Pointer) *C.char {
return C.CString(C.GoString((*C.char)(t)))
}
//export uuid_in
func uuid_in(fc C.FunctionCallInfo) C.Datum {
uuidInputStr := (*C.pgext_const_char)(unsafe.Pointer(uintptr(fc.args[0].value)))
uuidInputBytes := decodeUuidStr([]byte(C.GoString(uuidInputStr)))
outputBytes := (*C.pgext_unsigned_char)(C.malloc(C.size_t(len(uuidInputBytes))))
C.memcpy(unsafe.Pointer(outputBytes), unsafe.Pointer(&uuidInputBytes[0]), C.size_t(len(uuidInputBytes)))
return C.Datum(uintptr(unsafe.Pointer(outputBytes)))
}
// decodeUuidStr is a helper function for uuid_in, which converts the given byte slice to the static array representation.
func decodeUuidStr(strBytes []byte) [16]byte {
if strBytes[8] != '-' || strBytes[13] != '-' || strBytes[18] != '-' || strBytes[23] != '-' {
return [16]byte{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}
}
u := [16]byte{}
src := strBytes
dst := u[:]
for i, byteGroup := range []int{8, 4, 4, 4, 12} {
if i > 0 {
src = src[1:]
}
_, err := hex.Decode(dst[:byteGroup/2], src[:byteGroup])
if err != nil {
return [16]byte{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}
}
src = src[byteGroup:]
dst = dst[byteGroup/2:]
}
return u
}
//export uuid_out
func uuid_out(ptr unsafe.Pointer) C.Datum {
uuidInputBytes := C.GoBytes(ptr, 16)
textBuffer := make([]byte, 36)
_ = hex.Encode(textBuffer[0:8], uuidInputBytes[0:4])
textBuffer[8] = '-'
_ = hex.Encode(textBuffer[9:13], uuidInputBytes[4:6])
textBuffer[13] = '-'
_ = hex.Encode(textBuffer[14:18], uuidInputBytes[6:8])
textBuffer[18] = '-'
_ = hex.Encode(textBuffer[19:23], uuidInputBytes[8:10])
textBuffer[23] = '-'
_ = hex.Encode(textBuffer[24:], uuidInputBytes[10:])
return C.Datum(uintptr(unsafe.Pointer(C.CString(string(textBuffer)))))
}
//export DirectFunctionCall1Coll
func DirectFunctionCall1Coll(fn unsafe.Pointer, collation C.uint32_t, arg1 C.Datum) C.Datum {
fc := (*C.FunctionCallInfoBaseData)(C.malloc(C.SZ_FCINFO))
if fc == nil {
_, _ = fmt.Fprintln(os.Stderr, "DirectFunctionCall1Coll: out of memory")
return 0
}
defer C.free(unsafe.Pointer(fc))
C.memset(unsafe.Pointer(fc), 0, C.SZ_FCINFO)
fc.isnull = false
fc.fncollation = collation
fc.nargs = 1
fc.args[0].value = arg1
fc.args[0].isnull = false
result := C.FunctionPassthrough(C.PGFunction(fn), fc)
if fc.isnull {
_, _ = fmt.Fprintf(os.Stderr, "function %p returned NULL\n", fn)
}
return result
}
@@ -0,0 +1,68 @@
// Copyright 2025 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.
#ifndef PG_EXT_EXPORTS_H
#define PG_EXT_EXPORTS_H
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdbool.h>
// This doesn't compile unless it has a value, but Postgres defines this as an empty value intentionally
#define FLEXIBLE_ARRAY_MEMBER 8
typedef uintptr_t Datum;
typedef struct FunctionCallInfoBaseData* FunctionCallInfo;
typedef Datum (*PGFunction) (FunctionCallInfo fcinfo);
typedef struct NullableDatum {
Datum value;
bool isnull;
} NullableDatum;
typedef struct FmgrInfo {
void* fn_addr;
uint32_t fn_oid;
short fn_nargs;
bool fn_strict;
bool fn_retset;
unsigned char fn_stats;
void* fn_extra;
void* fn_mcxt;
void* fn_expr;
} FmgrInfo;
typedef struct FunctionCallInfoBaseData {
FmgrInfo* flinfo;
void* context;
void* resultinfo;
uint32_t fncollation;
bool isnull;
short nargs;
NullableDatum args[FLEXIBLE_ARRAY_MEMBER];
} FunctionCallInfoBaseData;
enum {
SZ_FMGRINFO = sizeof(FmgrInfo),
SZ_FCINFO = sizeof(FunctionCallInfoBaseData)
};
typedef const char pgext_const_char;
typedef unsigned char pgext_unsigned_char;
typedef const uint8_t pgext_const_uint8;
#endif //PG_EXT_EXPORTS_H
@@ -0,0 +1,132 @@
// Copyright 2025 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 extension_cgo
/*
#include "exports.h"
typedef enum
{
PG_MD5 = 0,
PG_SHA1,
PG_SHA224,
PG_SHA256,
PG_SHA384,
PG_SHA512,
} pg_cryptohash_type;
typedef struct pg_cryptohash_ctx {
pg_cryptohash_type hashType;
} pg_cryptohash_ctx;
*/
import "C"
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"hash"
"sync"
"unsafe"
)
var pg_cryptohash_store sync.Map
//export pg_cryptohash_create
func pg_cryptohash_create(typ C.pg_cryptohash_type) *C.pg_cryptohash_ctx {
ctx := (*C.pg_cryptohash_ctx)(C.malloc(C.size_t(unsafe.Sizeof(C.pg_cryptohash_ctx{}))))
ctx.hashType = typ
ctxPtr := uintptr(unsafe.Pointer(ctx))
switch typ {
case 1:
pg_cryptohash_store.Store(ctxPtr, sha1.New())
case C.PG_SHA224:
pg_cryptohash_store.Store(ctxPtr, sha512.New512_224())
case C.PG_SHA256:
pg_cryptohash_store.Store(ctxPtr, sha256.New())
case C.PG_SHA384:
pg_cryptohash_store.Store(ctxPtr, sha512.New384())
case C.PG_SHA512:
pg_cryptohash_store.Store(ctxPtr, sha512.New())
default:
// Default to MD5
pg_cryptohash_store.Store(ctxPtr, md5.New())
}
return ctx
}
//export pg_cryptohash_init
func pg_cryptohash_init(ctx *C.pg_cryptohash_ctx) C.int {
if ctx == nil {
return -1
}
return 0
}
//export pg_cryptohash_update
func pg_cryptohash_update(ctx *C.pg_cryptohash_ctx, data *C.pgext_const_uint8, len C.size_t) C.int {
if ctx == nil {
return -1
}
if len == 0 {
return 0
}
ctxPtr := uintptr(unsafe.Pointer(ctx))
storedHashAny, ok := pg_cryptohash_store.Load(ctxPtr)
if !ok {
return -1
}
storedHash := storedHashAny.(hash.Hash)
dataSlice := unsafe.Slice((*byte)(unsafe.Pointer(data)), int(len))
if _, err := storedHash.Write(dataSlice); err != nil {
return 1
}
return 0
}
//export pg_cryptohash_final
func pg_cryptohash_final(ctx *C.pg_cryptohash_ctx, dest *C.uint8_t, destLen C.size_t) C.int {
if ctx == nil {
return -1
}
ctxPtr := uintptr(unsafe.Pointer(ctx))
storedHashAny, ok := pg_cryptohash_store.Load(ctxPtr)
if !ok {
return -1
}
storedHash := storedHashAny.(hash.Hash)
sum := storedHash.Sum(nil)
destSlice := unsafe.Slice((*byte)(unsafe.Pointer(dest)), int(destLen))
// If the destination slice is too small, then it's invalid
if len(sum) > len(destSlice) {
return -1
}
copy(destSlice, sum)
return 0
}
//export pg_cryptohash_free
func pg_cryptohash_free(ctx *C.pg_cryptohash_ctx) {
if ctx != nil {
ctxPtr := uintptr(unsafe.Pointer(ctx))
pg_cryptohash_store.Delete(ctxPtr)
C.free(unsafe.Pointer(ctx))
}
}
//export pg_cryptohash_error
func pg_cryptohash_error(ctx *C.pg_cryptohash_ctx) *C.pgext_const_char {
return C.CString("")
}
@@ -0,0 +1,26 @@
LIBRARY "postgres.exe"
EXPORTS
; ---- functions ----
DirectFunctionCall1Coll = pg_extension.DirectFunctionCall1Coll
errcode = pg_extension.errcode
errfinish = pg_extension.errfinish
errmsg = pg_extension.errmsg
errmsg_internal = pg_extension.errmsg_internal
errstart = pg_extension.errstart
errstart_cold = pg_extension.errstart_cold
MemoryContextAlloc = pg_extension.MemoryContextAlloc
MemoryContextAllocExtended = pg_extension.MemoryContextAllocExtended
palloc = pg_extension.palloc
palloc0 = pg_extension.palloc0
palloc_extended = pg_extension.palloc_extended
pg_cryptohash_create = pg_extension.pg_cryptohash_create
pg_cryptohash_error = pg_extension.pg_cryptohash_error
pg_cryptohash_final = pg_extension.pg_cryptohash_final
pg_cryptohash_free = pg_extension.pg_cryptohash_free
pg_cryptohash_init = pg_extension.pg_cryptohash_init
pg_cryptohash_update = pg_extension.pg_cryptohash_update
pg_detoast_datum_packed = pg_extension.pg_detoast_datum_packed
strlcpy = pg_extension.strlcpy
text_to_cstring = pg_extension.text_to_cstring
uuid_in = pg_extension.uuid_in
uuid_out = pg_extension.uuid_out
@@ -0,0 +1,45 @@
// Copyright 2025 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.
//go:build !darwin
package extension_cgo
/*
#include "exports.h"
*/
import "C"
import "unsafe"
//export strlcpy
func strlcpy(dst *C.char, src *C.pgext_const_char, size C.size_t) C.size_t {
var srcLen C.size_t
for {
if *(*C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(src)) + uintptr(srcLen))) == 0 {
break
}
srcLen++
}
if size != 0 {
n := srcLen
if n >= size {
n = size - 1
}
dstSlice := unsafe.Slice((*byte)(unsafe.Pointer(dst)), int(n+1))
srcSlice := unsafe.Slice((*byte)(unsafe.Pointer(src)), int(n))
copy(dstSlice, srcSlice)
dstSlice[n] = 0
}
return srcLen
}
@@ -0,0 +1,26 @@
// Copyright 2025 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.
//go:build darwin
package extension_cgo
/*
#include "exports.h"
*/
import "C"
func strlcpy(dst *C.char, src *C.pgext_const_char, size C.size_t) C.size_t {
return C.strlcpy(dst, src, size)
}
@@ -0,0 +1,120 @@
// Copyright 2025 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 pg_extension
import (
"fmt"
"sync"
)
// Library is a fully-loaded extension library.
type Library struct {
Magic PgMagicStruct
Funcs map[string]Function
Version Version
internal InternalLoadedLibrary
}
// InternalLoadedLibrary is an interface that is implemented by the specific platform to handle library operations.
type InternalLoadedLibrary interface {
Lookup(sym string) (uintptr, error)
Close() error
}
// Function represents an internal library function.
type Function struct {
Name string
Ptr uintptr
Args []int
APIVersion int
// TODO: return type?
}
// PgFunctionInfo is a stand-in for the C struct that reports the function information.
type PgFunctionInfo struct {
APIVersion int32
}
// PgMagicStruct is a stand-in for the C struct that reports the information of the library.
type PgMagicStruct struct {
Len int32
Version int32
FuncMaxArgs int32
IndexMaxKeys int32
NameDataLen int32
Float4ByVal int32
Float8ByVal int32
}
var (
// loadedLibraries contains all of the loaded libraries.
// TODO: need to close all of these before the program ends
loadedLibraries = make(map[string]*Library)
// loadedLibrariesMutex gates access to the cached libraries.
loadedLibrariesMutex = &sync.Mutex{}
)
// LoadLibrary loads the library of the extension, along with preloading all of the functions given.
func LoadLibrary(path string, funcNames []string) (*Library, error) {
loadedLibrariesMutex.Lock()
defer loadedLibrariesMutex.Unlock()
if lib, ok := loadedLibraries[path]; ok {
return lib, nil
}
internalLib, err := loadLibraryInternal(path)
if err != nil {
return nil, err
}
magicPtr, err := internalLib.Lookup("Pg_magic_func")
if err != nil {
return nil, err
}
// We don't free the magic struct since it's a pointer to static memory
magicStructDatum, isNotNull := CallFmgrFunction(magicPtr)
if !isNotNull {
return nil, fmt.Errorf("unable to find magic function for `%s`", path)
}
magicStruct := *(FromDatum[PgMagicStruct](magicStructDatum))
lib := &Library{
Magic: magicStruct,
Funcs: make(map[string]Function),
internal: internalLib,
}
for _, funcName := range funcNames {
finfoPtr, err := internalLib.Lookup(fmt.Sprintf("pg_finfo_%s", funcName))
if err != nil {
return nil, err
}
// We don't free finfo since it's a pointer to static memory
finfoDatum, isNotNull := CallFmgrFunction(finfoPtr)
apiVersion := 0
if isNotNull {
apiVersion = int(FromDatum[PgFunctionInfo](finfoDatum).APIVersion)
}
funcPtr, err := internalLib.Lookup(funcName)
if err != nil {
return nil, err
}
lib.Funcs[funcName] = Function{
Name: funcName,
Ptr: funcPtr,
Args: nil,
APIVersion: apiVersion,
}
}
loadedLibraries[path] = lib
return lib, nil
}
@@ -0,0 +1,77 @@
// Copyright 2025 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.
//go:build darwin
package pg_extension
/*
#cgo LDFLAGS: -ldl
#include <dlfcn.h>
#include <stdlib.h>
*/
import "C"
import (
"fmt"
"unsafe"
_ "github.com/dolthub/doltgresql/core/extensions/pg_extension/library"
)
// PLATFORM specifies which platform applies to the current library loader. This will always be a three-letter string.
const PLATFORM = "MAC"
// darwinLib is the Linux-specific implementation of InternalLoadedLibrary.
type darwinLib struct {
path string
handle unsafe.Pointer
}
var _ InternalLoadedLibrary = (*darwinLib)(nil)
// loadLibraryInternal handles the loading of an extension's SO.
func loadLibraryInternal(path string) (InternalLoadedLibrary, error) {
pathC := C.CString(path)
defer C.free(unsafe.Pointer(pathC))
handle := C.dlopen(pathC, C.RTLD_LAZY|C.RTLD_GLOBAL)
if handle == nil {
return nil, fmt.Errorf("error while loading extension `%s`\n%s", path, C.GoString(C.dlerror()))
}
return &darwinLib{
path: path,
handle: handle,
}, nil
}
// Lookup implements the interface InternalLoadedLibrary.
func (u *darwinLib) Lookup(sym string) (uintptr, error) {
symC := C.CString(sym)
defer C.free(unsafe.Pointer(symC))
ptr := C.dlsym(u.handle, symC)
if ptr == nil {
return 0, fmt.Errorf("symbol %s not found", sym)
}
return uintptr(ptr), nil
}
// Close implements the interface InternalLoadedLibrary.
func (u *darwinLib) Close() error {
if C.dlclose(u.handle) != 0 {
return fmt.Errorf("error while closing extension `%s`\n%s", u.path, C.GoString(C.dlerror()))
}
return nil
}
@@ -0,0 +1,78 @@
// Copyright 2025 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.
//go:build linux
package pg_extension
/*
#cgo LDFLAGS: -ldl
#cgo LDFLAGS: -Wl,-E
#include <dlfcn.h>
#include <stdlib.h>
*/
import "C"
import (
"fmt"
"unsafe"
_ "github.com/dolthub/doltgresql/core/extensions/pg_extension/library"
)
// PLATFORM specifies which platform applies to the current library loader. This will always be a three-letter string.
const PLATFORM = "LIN"
// unixLib is the Linux-specific implementation of InternalLoadedLibrary.
type unixLib struct {
path string
handle unsafe.Pointer
}
var _ InternalLoadedLibrary = (*unixLib)(nil)
// loadLibraryInternal handles the loading of an extension's SO.
func loadLibraryInternal(path string) (InternalLoadedLibrary, error) {
pathC := C.CString(path)
defer C.free(unsafe.Pointer(pathC))
handle := C.dlopen(pathC, C.RTLD_LAZY|C.RTLD_GLOBAL)
if handle == nil {
return nil, fmt.Errorf("error while loading extension `%s`\n%s", path, C.GoString(C.dlerror()))
}
return &unixLib{
path: path,
handle: handle,
}, nil
}
// Lookup implements the interface InternalLoadedLibrary.
func (u *unixLib) Lookup(sym string) (uintptr, error) {
symC := C.CString(sym)
defer C.free(unsafe.Pointer(symC))
ptr := C.dlsym(u.handle, symC)
if ptr == nil {
return 0, fmt.Errorf("symbol %s not found", sym)
}
return uintptr(ptr), nil
}
// Close implements the interface InternalLoadedLibrary.
func (u *unixLib) Close() error {
if C.dlclose(u.handle) != 0 {
return fmt.Errorf("error while closing extension `%s`\n%s", u.path, C.GoString(C.dlerror()))
}
return nil
}
@@ -0,0 +1,136 @@
// Copyright 2025 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.
//go:build windows
package pg_extension
import (
"bytes"
"crypto/sha256"
_ "embed"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"sync"
"syscall"
"unsafe"
)
// PLATFORM specifies which platform applies to the current library loader. This will always be a three-letter string.
const PLATFORM = "WIN"
//go:embed output/postgres.exe
var libDefBytes []byte
//go:embed output/pg_extension.dll
var dllBytes []byte
// winLib is the Windows-specific implementation of InternalLoadedLibrary.
type winLib struct{ dll syscall.Handle }
var _ InternalLoadedLibrary = (*winLib)(nil)
var addPGBinDir = &sync.Once{}
// loadLibraryInternal handles the loading of an extension's DLL.
func loadLibraryInternal(path string) (InternalLoadedLibrary, error) {
addPGBinDir.Do(func() {
_, currentFileLocation, _, ok := runtime.Caller(0)
if !ok || len(currentFileLocation) == 0 {
panic("cannot find the directory where this file exists")
}
// There are three scenarios that we need to consider when attempting to load the DLL:
// 1) The DLL exists in an output folder (this will be true for development)
// 2) The DLL exists alongside the binary
// 3) The DLL does not exist alongside the binary (or is the wrong version)
// In the third situation, we write the contained DLL and definition file alongside the binary, so that we'll
// always end up in the second situation. This enables both developmental and deployment workflows without
// explicit configuration.
var dllDir string
if _, err := os.Stat(filepath.Join(filepath.Dir(currentFileLocation), "output", "postgres.exe")); err == nil {
dllDir = filepath.Join(filepath.Dir(currentFileLocation), "output")
} else {
currentBinaryLocation, err := os.Executable()
if err != nil {
panic(fmt.Errorf("cannot find where the executable was launched:\n%s", err.Error()))
}
dllDir = filepath.Dir(currentBinaryLocation)
shouldWriteFiles := false
if _, err := os.Stat(filepath.Join(dllDir, "postgres.exe")); err != nil {
shouldWriteFiles = true
} else {
func() {
// If the DLL hash doesn't match our hash, then we overwrite it
extDll, err := os.Open(filepath.Join(filepath.Dir(currentBinaryLocation), "pg_extension.dll"))
if err != nil {
shouldWriteFiles = true
return
}
defer func() {
_ = extDll.Close()
}()
dllSha := sha256.Sum256(dllBytes)
extDllSha := sha256.New()
_, _ = io.Copy(extDllSha, extDll)
shouldWriteFiles = !bytes.Equal(extDllSha.Sum(nil), dllSha[:])
}()
}
if shouldWriteFiles {
writeLocation := filepath.Dir(currentBinaryLocation)
_ = os.WriteFile(filepath.Join(writeLocation, "postgres.exe"), libDefBytes, 0755)
_ = os.WriteFile(filepath.Join(writeLocation, "pg_extension.dll"), dllBytes, 0755)
}
}
dirPtr, err := syscall.UTF16PtrFromString(dllDir)
if err != nil {
panic(err)
}
_, _, _ = syscall.MustLoadDLL("kernel32.dll").MustFindProc("SetDllDirectoryW").Call(uintptr(unsafe.Pointer(dirPtr)))
_, _ = syscall.LoadLibrary(filepath.Join(dllDir, "pg_extension.dll"))
})
d, err := syscall.LoadLibrary(path)
if err != nil {
return nil, err
}
return &winLib{dll: d}, nil
}
// Lookup implements the interface InternalLoadedLibrary.
func (w *winLib) Lookup(sym string) (uintptr, error) {
candidates := []string{
sym,
"_" + sym,
sym + "@0",
"_" + sym + "@0",
}
for bytes := 4; bytes <= 64; bytes += 4 {
candidates = append(candidates,
fmt.Sprintf("%s@%d", sym, bytes),
fmt.Sprintf("_%s@%d", sym, bytes))
}
for _, name := range candidates {
if p, err := syscall.GetProcAddress(w.dll, name); err == nil {
return p, nil
}
}
return 0, fmt.Errorf("symbol %s not found", sym)
}
// Close implements the interface InternalLoadedLibrary.
func (w *winLib) Close() error {
return syscall.FreeLibrary(w.dll)
}
+88
View File
@@ -0,0 +1,88 @@
// Copyright 2025 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 pg_extension
/*
#cgo CFLAGS: "-I${SRCDIR}/library"
#include "exports.h"
*/
import "C"
import "unsafe"
// FromDatum converts the given datum to the type.
func FromDatum[T any](d Datum) *T {
if d == 0 {
return nil
}
return (*T)(unsafe.Pointer(d))
}
// FromDatumGoString converts the given datum to a string.
func FromDatumGoString(d Datum) string {
if d == 0 {
return ""
}
return C.GoString((*C.char)(unsafe.Pointer(d)))
}
// FromDatumGoBytes converts the given datum to a byte array of length N.
func FromDatumGoBytes(d Datum, n uint) []byte {
if d == 0 {
return []byte{}
}
return C.GoBytes(unsafe.Pointer(d), C.int(n))
}
// ToDatum converts the given pointer to a Datum.
func ToDatum[T any](val *T) Datum {
if val == nil {
return 0
}
return Datum(unsafe.Pointer(val))
}
// ToDatumGoString converts the given string to a Datum.
func ToDatumGoString(str string) Datum {
return Datum(unsafe.Pointer(C.CString(str)))
}
// ToDatumGoBytes converts the given byte slice to a Datum.
func ToDatumGoBytes(data []byte) Datum {
return Datum(unsafe.Pointer(C.CBytes(data)))
}
// Malloc allocates the given type within the C heap. These should always be followed up with a Free at some point
// afterward.
func Malloc[T any]() *T {
var structToDetermineSize T
return (*T)(C.malloc(C.size_t(unsafe.Sizeof(structToDetermineSize))))
}
// ZeroMemory writes all zeroes to the memory location occupied by the given pointer.
func ZeroMemory[T any](val *T) {
var structToDetermineSize T
C.memset(unsafe.Pointer(val), 0, C.size_t(unsafe.Sizeof(structToDetermineSize)))
}
// Free frees the given pointer from C heap. Generally, this is paired with a pointer returned from Malloc.
func Free[T any](val *T) {
C.free(unsafe.Pointer(val))
}
// FreeDatum frees the given Datum. Care should be exercised as datums may refer to static memory, and attempting to
// free static memory will result in a crash.
func FreeDatum(val Datum) {
C.free(unsafe.Pointer(val))
}
+138
View File
@@ -0,0 +1,138 @@
// Copyright 2025 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 extensions
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// DeserializeRootObject implements the interface objinterface.Collection.
func (pge *Collection) DeserializeRootObject(ctx context.Context, data []byte) (objinterface.RootObject, error) {
return DeserializeExtension(ctx, data)
}
// DiffRootObjects implements the interface objinterface.Collection.
func (pge *Collection) DiffRootObjects(ctx context.Context, fromHash string, ours objinterface.RootObject, theirs objinterface.RootObject, ancestor objinterface.RootObject) ([]objinterface.RootObjectDiff, objinterface.RootObject, error) {
return nil, nil, errors.Errorf("extensions should never produce conflicts")
}
// DropRootObject implements the interface objinterface.Collection.
func (pge *Collection) DropRootObject(ctx context.Context, identifier id.Id) error {
if identifier.Section() != id.Section_Extension {
return errors.Errorf(`extension "%s" does not exist`, identifier.String())
}
return pge.DropLoadedExtension(ctx, id.Extension(identifier))
}
// GetFieldType implements the interface objinterface.Collection.
func (pge *Collection) GetFieldType(ctx context.Context, fieldName string) *pgtypes.DoltgresType {
return nil
}
// GetID implements the interface objinterface.Collection.
func (pge *Collection) GetID() objinterface.RootObjectID {
return objinterface.RootObjectID_Extensions
}
// GetRootObject implements the interface objinterface.Collection.
func (pge *Collection) GetRootObject(ctx context.Context, identifier id.Id) (objinterface.RootObject, bool, error) {
if identifier.Section() != id.Section_Extension {
return nil, false, nil
}
ext, err := pge.GetLoadedExtension(ctx, id.Extension(identifier))
return ext, err == nil && ext.Namespace.IsValid(), err
}
// HasRootObject implements the interface objinterface.Collection.
func (pge *Collection) HasRootObject(ctx context.Context, identifier id.Id) (bool, error) {
if identifier.Section() != id.Section_Extension {
return false, nil
}
return pge.HasLoadedExtension(ctx, id.Extension(identifier)), nil
}
// IDToTableName implements the interface objinterface.Collection.
func (pge *Collection) IDToTableName(identifier id.Id) doltdb.TableName {
if identifier.Section() != id.Section_Extension {
return doltdb.TableName{}
}
return doltdb.TableName{Name: id.Extension(identifier).Name()}
}
// IterAll implements the interface objinterface.Collection.
func (pge *Collection) IterAll(ctx context.Context, callback func(rootObj objinterface.RootObject) (stop bool, err error)) error {
for _, extID := range pge.idCache {
stop, err := callback(pge.accessCache[extID])
if err != nil {
return err
} else if stop {
return nil
}
}
return nil
}
// IterIDs implements the interface objinterface.Collection.
func (pge *Collection) IterIDs(ctx context.Context, callback func(identifier id.Id) (stop bool, err error)) error {
for _, extID := range pge.idCache {
stop, err := callback(extID.AsId())
if err != nil {
return err
} else if stop {
return nil
}
}
return nil
}
// PutRootObject implements the interface objinterface.Collection.
func (pge *Collection) PutRootObject(ctx context.Context, rootObj objinterface.RootObject) error {
ext, ok := rootObj.(Extension)
if !ok {
return errors.Newf("invalid extension root object: %T", rootObj)
}
return pge.AddLoadedExtension(ctx, ext)
}
// RenameRootObject implements the interface objinterface.Collection.
func (pge *Collection) RenameRootObject(ctx context.Context, oldName id.Id, newName id.Id) error {
return errors.New(`extensions cannot be renamed`)
}
// ResolveName implements the interface objinterface.Collection.
func (pge *Collection) ResolveName(ctx context.Context, name doltdb.TableName) (doltdb.TableName, id.Id, error) {
extID := id.NewExtension(name.Name)
if pge.HasLoadedExtension(ctx, extID) {
return doltdb.TableName{Name: name.Name}, extID.AsId(), nil
}
return doltdb.TableName{}, id.Null, nil
}
// TableNameToID implements the interface objinterface.Collection.
func (pge *Collection) TableNameToID(name doltdb.TableName) id.Id {
return id.NewExtension(name.Name).AsId()
}
// UpdateField implements the interface objinterface.Collection.
func (pge *Collection) UpdateField(ctx context.Context, rootObject objinterface.RootObject, fieldName string, newValue any) (objinterface.RootObject, error) {
return nil, errors.New("updating through the conflicts table for this object type is not yet supported")
}
+68
View File
@@ -0,0 +1,68 @@
// Copyright 2025 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 extensions
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/utils"
)
// Serialize returns the Extension as a byte slice. If the Extension is invalid (invalid ExtName), then this returns a
// nil slice.
func (ext Extension) Serialize(ctx context.Context) ([]byte, error) {
if !ext.ExtName.IsValid() {
return nil, nil
}
// Initialize the writer
writer := utils.NewWriter(256)
writer.VariableUint(0) // Version
// Write the extension data
writer.Id(ext.ExtName.AsId())
writer.Id(ext.Namespace.AsId())
writer.Bool(ext.Relocatable)
writer.String(string(ext.LibIdentifier))
// Returns the data
return writer.Data(), nil
}
// DeserializeExtension returns the Extension that was serialized in the byte slice. Returns an empty Extension (has an
// invalid ID) if data is nil or empty.
func DeserializeExtension(ctx context.Context, data []byte) (Extension, error) {
if len(data) == 0 {
return Extension{}, nil
}
reader := utils.NewReader(data)
version := reader.VariableUint()
if version != 0 {
return Extension{}, errors.Errorf("version %d of extensions are not supported, please upgrade the server", version)
}
// Read from the reader
ext := Extension{}
ext.ExtName = id.Extension(reader.Id())
ext.Namespace = id.Namespace(reader.Id())
ext.Relocatable = reader.Bool()
ext.LibIdentifier = LibraryIdentifier(reader.String())
if !reader.IsEmpty() {
return Extension{}, errors.Errorf("extra data found while deserializing an extension")
}
// Return the deserialized object
return ext, nil
}
+427
View File
@@ -0,0 +1,427 @@
// 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 (
"context"
"fmt"
"maps"
"slices"
"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/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/server/plpgsql"
)
// Collection contains a collection of functions.
type Collection struct {
accessCache map[id.Function]Function // This cache is used for general access when you know the exact ID
overloadCache map[id.Function][]id.Function // This cache is used to find overloads if you know the name
idCache []id.Function // This cache simply contains the name of every function
mapHash hash.Hash // This is cached so that we don't have to calculate the hash every time
underlyingMap prolly.AddressMap
ns tree.NodeStore
}
// Function represents a created function.
type Function struct {
ID id.Function
ReturnType id.Type
ParameterNames []string
ParameterTypes []id.Type
ParameterDefaults []string
Variadic bool
IsNonDeterministic bool
Strict bool
Definition string
ExtensionName string // Only used when this is an extension function
ExtensionSymbol string // Only used when this is an extension function
Operations []plpgsql.InterpreterOperation // Only used when this is a plpgsql language
SQLDefinition string // Only used when this is a sql language
SetOf bool
}
var _ objinterface.Collection = (*Collection)(nil)
var _ objinterface.RootObject = Function{}
// NewCollection returns a new Collection.
func NewCollection(ctx context.Context, underlyingMap prolly.AddressMap, ns tree.NodeStore) (*Collection, error) {
collection := &Collection{
accessCache: make(map[id.Function]Function),
overloadCache: make(map[id.Function][]id.Function),
idCache: nil,
mapHash: hash.Hash{},
underlyingMap: underlyingMap,
ns: ns,
}
return collection, collection.reloadCaches(ctx)
}
// GetFunction returns the function with the given ID. Returns a function with an invalid ID if it cannot be found
// (Function.ID.IsValid() == false).
func (pgf *Collection) GetFunction(ctx context.Context, funcID id.Function) (Function, error) {
if f, ok := pgf.accessCache[funcID]; ok {
return f, nil
}
return Function{}, nil
}
// GetFunctionOverloads returns the overloads for the function matching the schema and the function name. The parameter
// types are ignored when searching for overloads.
func (pgf *Collection) GetFunctionOverloads(ctx context.Context, funcID id.Function) ([]Function, error) {
overloads, ok := pgf.overloadCache[id.NewFunction(funcID.SchemaName(), funcID.FunctionName())]
if !ok || len(overloads) == 0 {
return nil, nil
}
funcs := make([]Function, len(overloads))
for i, overload := range overloads {
funcs[i] = pgf.accessCache[overload]
}
return funcs, nil
}
// HasFunction returns whether the function is present.
func (pgf *Collection) HasFunction(ctx context.Context, funcID id.Function) bool {
_, ok := pgf.accessCache[funcID]
return ok
}
// AddFunction adds a new function.
func (pgf *Collection) AddFunction(ctx context.Context, f Function) error {
// First we'll check to see if it exists
if _, ok := pgf.accessCache[f.ID]; ok {
return errors.Errorf(`function "%s" already exists with same argument types`, f.ID.FunctionName())
}
// Now we'll add the function to our map
data, err := f.Serialize(ctx)
if err != nil {
return err
}
h, err := pgf.ns.WriteBytes(ctx, data)
if err != nil {
return err
}
mapEditor := pgf.underlyingMap.Editor()
if err = mapEditor.Add(ctx, string(f.ID), h); err != nil {
return err
}
newMap, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pgf.underlyingMap = newMap
pgf.mapHash = pgf.underlyingMap.HashOf()
return pgf.reloadCaches(ctx)
}
// DropFunction drops an existing function.
func (pgf *Collection) DropFunction(ctx context.Context, funcIDs ...id.Function) error {
if len(funcIDs) == 0 {
return nil
}
// Check that each name exists before performing any deletions
for _, funcID := range funcIDs {
if _, ok := pgf.accessCache[funcID]; !ok {
return errors.Errorf(`function %s does not exist`, funcID.FunctionName())
}
}
// Now we'll remove the functions from the map
mapEditor := pgf.underlyingMap.Editor()
for _, funcID := range funcIDs {
err := mapEditor.Delete(ctx, string(funcID))
if err != nil {
return err
}
}
newMap, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pgf.underlyingMap = newMap
pgf.mapHash = pgf.underlyingMap.HashOf()
return pgf.reloadCaches(ctx)
}
// resolveName returns the fully resolved name of the given function. Returns an error if the name is ambiguous.
//
// The following formats are examples of a formatted name:
// name()
// name(type1, schema.type2)
// name(,,)
func (pgf *Collection) resolveName(ctx context.Context, schemaName string, formattedName string) (id.Function, error) {
if len(pgf.accessCache) == 0 || len(formattedName) == 0 {
return id.NullFunction, nil
}
// Extract the actual name from the format
leftParenIndex := strings.IndexByte(formattedName, '(')
if leftParenIndex == -1 {
return id.NullFunction, nil
}
if formattedName[len(formattedName)-1] != ')' {
return id.NullFunction, nil
}
functionName := strings.TrimSpace(formattedName[:leftParenIndex])
var typeIDs []id.Type
typePortion := strings.TrimSpace(formattedName[leftParenIndex+1 : len(formattedName)-1])
if len(typePortion) > 0 {
// If the type portion is just an empty string, then we don't want any type IDs
typeStrings := strings.Split(strings.TrimSpace(formattedName[leftParenIndex+1:len(formattedName)-1]), ",")
typeIDs = make([]id.Type, len(typeStrings))
for i, typeString := range typeStrings {
typeParts := strings.Split(typeString, ".")
switch len(typeParts) {
case 1:
typeIDs[i] = id.NewType("", strings.TrimSpace(typeParts[0]))
case 2:
typeIDs[i] = id.NewType(strings.TrimSpace(typeParts[0]), strings.TrimSpace(typeParts[1]))
default:
return id.NullFunction, nil
}
}
}
// If there's an exact match, then we return exactly that
fullID := id.NewFunction(schemaName, functionName, typeIDs...)
if _, ok := pgf.accessCache[fullID]; ok {
return fullID, nil
}
// Otherwise we'll iterate over all the names
var resolvedID id.Function
OuterLoop:
for _, funcID := range pgf.idCache {
if !strings.EqualFold(functionName, funcID.FunctionName()) {
continue
}
if len(schemaName) > 0 && !strings.EqualFold(schemaName, funcID.SchemaName()) {
continue
}
if len(typeIDs) > 0 {
if funcID.ParameterCount() != len(typeIDs) {
continue
}
for i, param := range funcID.Parameters() {
if len(typeIDs[i].TypeName()) > 0 && !strings.EqualFold(typeIDs[i].TypeName(), param.TypeName()) {
continue OuterLoop
}
if len(typeIDs[i].SchemaName()) > 0 && !strings.EqualFold(typeIDs[i].SchemaName(), param.SchemaName()) {
continue OuterLoop
}
}
}
// Everything must have matched to have made it here
if resolvedID.IsValid() {
funcTableName := FunctionIDToTableName(funcID)
resolvedTableName := FunctionIDToTableName(resolvedID)
return id.NullFunction, fmt.Errorf("`%s.%s` is ambiguous, matches `%s` and `%s`",
schemaName, formattedName, funcTableName.String(), resolvedTableName.String())
}
resolvedID = funcID
}
return resolvedID, nil
}
// iterateIDs iterates over all function IDs in the collection.
func (pgf *Collection) iterateIDs(ctx context.Context, callback func(funcID id.Function) (stop bool, err error)) error {
for _, funcID := range pgf.idCache {
stop, err := callback(funcID)
if err != nil {
return err
} else if stop {
return nil
}
}
return nil
}
// IterateFunctions iterates over all functions in the collection.
func (pgf *Collection) IterateFunctions(ctx context.Context, callback func(f Function) (stop bool, err error)) error {
for _, funcID := range pgf.idCache {
stop, err := callback(pgf.accessCache[funcID])
if err != nil {
return err
} else if stop {
return nil
}
}
return nil
}
// Clone returns a new *Collection with the same contents as the original.
func (pgf *Collection) Clone(ctx context.Context) *Collection {
return &Collection{
accessCache: maps.Clone(pgf.accessCache),
overloadCache: maps.Clone(pgf.overloadCache),
idCache: slices.Clone(pgf.idCache),
underlyingMap: pgf.underlyingMap,
mapHash: pgf.mapHash,
ns: pgf.ns,
}
}
// Map writes any cached sequences to the underlying map, and then returns the underlying map.
func (pgf *Collection) Map(ctx context.Context) (prolly.AddressMap, error) {
return pgf.underlyingMap, nil
}
// 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 (pgf *Collection) DiffersFrom(ctx context.Context, root objinterface.RootValue) bool {
hashOnGivenRoot, err := pgf.LoadCollectionHash(ctx, root)
if err != nil {
return true
}
if pgf.mapHash.Equal(hashOnGivenRoot) {
return false
}
// An empty map should match an uninitialized collection on the root
count, err := pgf.underlyingMap.Count()
if err == nil && count == 0 && hashOnGivenRoot.IsEmpty() {
return false
}
return true
}
// reloadCaches writes the underlying map's contents to the caches.
func (pgf *Collection) reloadCaches(ctx context.Context) error {
count, err := pgf.underlyingMap.Count()
if err != nil {
return err
}
clear(pgf.accessCache)
clear(pgf.overloadCache)
pgf.mapHash = pgf.underlyingMap.HashOf()
pgf.idCache = make([]id.Function, 0, count)
return pgf.underlyingMap.IterAll(ctx, func(_ string, h hash.Hash) error {
if h.IsEmpty() {
return nil
}
data, err := pgf.ns.ReadBytes(ctx, h)
if err != nil {
return err
}
f, err := DeserializeFunction(ctx, data)
if err != nil {
return err
}
pgf.accessCache[f.ID] = f
partialID := id.NewFunction(f.ID.SchemaName(), f.ID.FunctionName())
pgf.overloadCache[partialID] = append(pgf.overloadCache[partialID], f.ID)
pgf.idCache = append(pgf.idCache, f.ID)
return 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 (pgf *Collection) tableNameToID(schemaName string, formattedName string) id.Function {
leftParenIndex := strings.IndexByte(formattedName, '(')
if leftParenIndex == -1 {
return id.NullFunction
}
if formattedName[len(formattedName)-1] != ')' {
return id.NullFunction
}
functionName := strings.TrimSpace(formattedName[:leftParenIndex])
var typeIDs []id.Type
typePortion := strings.TrimSpace(formattedName[leftParenIndex+1 : len(formattedName)-1])
if len(typePortion) > 0 {
// If the type portion is just an empty string, then we don't want any type IDs
typeStrings := strings.Split(strings.TrimSpace(formattedName[leftParenIndex+1:len(formattedName)-1]), ",")
typeIDs = make([]id.Type, len(typeStrings))
for i, typeString := range typeStrings {
typeParts := strings.Split(typeString, ".")
switch len(typeParts) {
case 1:
typeIDs[i] = id.NewType("", strings.TrimSpace(typeParts[0]))
case 2:
typeIDs[i] = id.NewType(strings.TrimSpace(typeParts[0]), strings.TrimSpace(typeParts[1]))
default:
return id.NullFunction
}
}
}
return id.NewFunction(schemaName, functionName, typeIDs...)
}
// GetID implements the interface objinterface.RootObject.
func (function Function) GetID() id.Id {
return function.ID.AsId()
}
// GetInnerDefinition returns the inner definition inside the CREATE FUNCTION statement.
func (function Function) GetInnerDefinition() string {
// TODO: right now we're hardcode searching for $$, which will fail for some definition strings
start := strings.Index(function.Definition, "$$")
end := strings.LastIndex(function.Definition, "$$")
if start == -1 || end == -1 {
// Return the whole definition for now
return function.Definition
}
return strings.TrimSpace(function.Definition[start+2 : end])
}
// ReplaceDefinition returns a new definition with the inner portion replaced with the given string.
func (function Function) ReplaceDefinition(newInner string) string {
return strings.Replace(function.Definition, function.GetInnerDefinition(), newInner, 1)
}
// GetRootObjectID implements the interface objinterface.RootObject.
func (function Function) GetRootObjectID() objinterface.RootObjectID {
return objinterface.RootObjectID_Functions
}
// HashOf implements the interface objinterface.RootObject.
func (function Function) HashOf(ctx context.Context) (hash.Hash, error) {
data, err := function.Serialize(ctx)
if err != nil {
return hash.Hash{}, err
}
return hash.Of(data), nil
}
// Name implements the interface objinterface.RootObject.
func (function Function) Name() doltdb.TableName {
return FunctionIDToTableName(function.ID)
}
// FunctionIDToTableName returns the ID in a format that's better for user consumption.
func FunctionIDToTableName(funcID id.Function) doltdb.TableName {
paramTypes := funcID.Parameters()
strTypes := make([]string, len(paramTypes))
for i, paramType := range paramTypes {
if paramType.SchemaName() == "pg_catalog" || paramType.SchemaName() == funcID.SchemaName() {
strTypes[i] = paramType.TypeName()
} else {
strTypes[i] = fmt.Sprintf("%s.%s", paramType.SchemaName(), paramType.TypeName())
}
}
return doltdb.TableName{
Name: fmt.Sprintf("%s(%s)", funcID.FunctionName(), strings.Join(strTypes, ",")),
Schema: funcID.SchemaName(),
}
}
+126
View File
@@ -0,0 +1,126 @@
// Copyright 2025 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 (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/merge"
"github.com/dolthub/dolt/go/store/hash"
"github.com/dolthub/dolt/go/store/prolly"
"github.com/dolthub/doltgresql/core/id"
pgmerge "github.com/dolthub/doltgresql/core/merge"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/flatbuffers/gen/serial"
)
// storage is used to read from and write to the root.
var storage = objinterface.RootObjectSerializer{
Bytes: (*serial.RootValue).FunctionsBytes,
RootValueAdd: serial.RootValueAddFunctions,
}
// HandleMerge implements the interface objinterface.Collection.
func (*Collection) HandleMerge(ctx context.Context, mro merge.MergeRootObject) (doltdb.RootObject, *merge.MergeStats, error) {
ourFunc := mro.OurRootObj.(Function)
theirFunc := mro.TheirRootObj.(Function)
// Ensure that they have the same identifier
if ourFunc.ID != theirFunc.ID {
return nil, nil, errors.Newf("attempted to merge different functions: `%s` and `%s`",
ourFunc.Name().String(), theirFunc.Name().String())
}
ourHash, err := ourFunc.HashOf(ctx)
if err != nil {
return nil, nil, err
}
theirHash, err := theirFunc.HashOf(ctx)
if err != nil {
return nil, nil, err
}
if ourHash.Equal(theirHash) {
return mro.OurRootObj, &merge.MergeStats{
Operation: merge.TableUnmodified,
Adds: 0,
Deletes: 0,
Modifications: 0,
DataConflicts: 0,
SchemaConflicts: 0,
RootObjectConflicts: 0,
ConstraintViolations: 0,
}, nil
}
return pgmerge.CreateConflict(ctx, mro.RightSrc, ourFunc, theirFunc, mro.AncestorRootObj)
}
// LoadCollection implements the interface objinterface.Collection.
func (*Collection) LoadCollection(ctx context.Context, root objinterface.RootValue) (objinterface.Collection, error) {
return LoadFunctions(ctx, root)
}
// LoadCollectionHash implements the interface objinterface.Collection.
func (*Collection) LoadCollectionHash(ctx context.Context, root objinterface.RootValue) (hash.Hash, error) {
m, ok, err := storage.GetProllyMap(ctx, root)
if err != nil || !ok {
return hash.Hash{}, err
}
return m.HashOf(), nil
}
// LoadFunctions loads the functions collection from the given root.
func LoadFunctions(ctx context.Context, root objinterface.RootValue) (*Collection, error) {
m, ok, err := storage.GetProllyMap(ctx, root)
if err != nil {
return nil, err
}
if !ok {
m, err = prolly.NewEmptyAddressMap(root.NodeStore())
if err != nil {
return nil, err
}
}
return NewCollection(ctx, m, root.NodeStore())
}
// ResolveNameFromObjects implements the interface objinterface.Collection.
func (*Collection) ResolveNameFromObjects(ctx context.Context, name doltdb.TableName, rootObjects []objinterface.RootObject) (doltdb.TableName, id.Id, error) {
tempCollection := Collection{
accessCache: make(map[id.Function]Function),
idCache: make([]id.Function, 0, len(rootObjects)),
}
for _, rootObject := range rootObjects {
if obj, ok := rootObject.(Function); ok {
tempCollection.accessCache[obj.ID] = obj
tempCollection.idCache = append(tempCollection.idCache, obj.ID)
}
}
return tempCollection.ResolveName(ctx, name)
}
// Serializer implements the interface objinterface.Collection.
func (*Collection) Serializer() objinterface.RootObjectSerializer {
return storage
}
// UpdateRoot implements the interface objinterface.Collection.
func (pgf *Collection) UpdateRoot(ctx context.Context, root objinterface.RootValue) (objinterface.RootValue, error) {
m, err := pgf.Map(ctx)
if err != nil {
return nil, err
}
return storage.WriteProllyMap(ctx, root, m)
}
+324
View File
@@ -0,0 +1,324 @@
// Copyright 2025 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 (
"context"
"strings"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/doltgresql/core/id"
pgmerge "github.com/dolthub/doltgresql/core/merge"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/server/plpgsql"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
const (
FIELD_NAME_PARAMETER_NAMES = "parameter_names"
FIELD_NAME_RETURN_TYPE = "return_type"
FIELD_NAME_NON_DETERMINISTIC = "non_deterministic"
FIELD_NAME_STRICT = "strict"
FIELD_NAME_DEFINITION = "definition"
FIELD_NAME_EXTENSION_NAME = "extension_name"
FIELD_NAME_EXTENSION_SYMBOL = "extension_symbol"
FIELD_NAME_SQL_DEFINITION = "sql_definition"
FIELD_NAME_SET_OF = "set_of"
)
// DeserializeRootObject implements the interface objinterface.Collection.
func (pgf *Collection) DeserializeRootObject(ctx context.Context, data []byte) (objinterface.RootObject, error) {
return DeserializeFunction(ctx, data)
}
// DiffRootObjects implements the interface objinterface.Collection.
func (pgf *Collection) DiffRootObjects(ctx context.Context, fromHash string, o objinterface.RootObject, t objinterface.RootObject, a objinterface.RootObject) ([]objinterface.RootObjectDiff, objinterface.RootObject, error) {
// We ignore many fields when diffing, as differences in these fields would result in a different function due to overloading
// For example, "func_name(text)" and "func_name(varchar)" cannot produce a conflict as they're different functions
ours := o.(Function)
theirs := t.(Function)
ancestor, hasAncestor := a.(Function)
var diffs []objinterface.RootObjectDiff
{
ourParamNames := strings.Join(ours.ParameterNames, ",")
theirParamNames := strings.Join(theirs.ParameterNames, ",")
ancParamNames := strings.Join(ancestor.ParameterNames, ",")
diff := objinterface.RootObjectDiff{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: FIELD_NAME_PARAMETER_NAMES,
}
if pgmerge.DiffValues(&diff, ourParamNames, theirParamNames, ancParamNames, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.ParameterNames = strings.Split(diff.OurValue.(string), ",")
}
}
if ours.ReturnType != theirs.ReturnType {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: FIELD_NAME_RETURN_TYPE,
}
if pgmerge.DiffValues(&diff, ours.ReturnType.TypeName(), theirs.ReturnType.TypeName(), ancestor.ReturnType.TypeName(), hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.ReturnType = id.NewType(ours.ReturnType.SchemaName(), diff.OurValue.(string))
}
}
if ours.IsNonDeterministic != theirs.IsNonDeterministic {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Bool,
FromHash: fromHash,
FieldName: FIELD_NAME_NON_DETERMINISTIC,
}
if pgmerge.DiffValues(&diff, ours.IsNonDeterministic, theirs.IsNonDeterministic, ancestor.IsNonDeterministic, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.IsNonDeterministic = diff.OurValue.(bool)
}
}
if ours.Strict != theirs.Strict {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Bool,
FromHash: fromHash,
FieldName: FIELD_NAME_STRICT,
}
if pgmerge.DiffValues(&diff, ours.Strict, theirs.Strict, ancestor.Strict, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Strict = diff.OurValue.(bool)
}
}
if ours.Definition != theirs.Definition {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: FIELD_NAME_DEFINITION,
}
if pgmerge.DiffValues(&diff, ours.GetInnerDefinition(), theirs.GetInnerDefinition(), ancestor.GetInnerDefinition(), hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Definition = ours.ReplaceDefinition(diff.OurValue.(string))
}
}
if ours.ExtensionName != theirs.ExtensionName {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: FIELD_NAME_EXTENSION_NAME,
}
if pgmerge.DiffValues(&diff, ours.ExtensionName, theirs.ExtensionName, ancestor.ExtensionName, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.ExtensionName = diff.OurValue.(string)
}
}
if ours.ExtensionSymbol != theirs.ExtensionSymbol {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: FIELD_NAME_EXTENSION_SYMBOL,
}
if pgmerge.DiffValues(&diff, ours.ExtensionSymbol, theirs.ExtensionSymbol, ancestor.ExtensionSymbol, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.ExtensionSymbol = diff.OurValue.(string)
}
}
if ours.SQLDefinition != theirs.SQLDefinition {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: FIELD_NAME_SQL_DEFINITION,
}
if pgmerge.DiffValues(&diff, ours.SQLDefinition, theirs.SQLDefinition, ancestor.SQLDefinition, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.SQLDefinition = diff.OurValue.(string)
}
}
if ours.SetOf != theirs.SetOf {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Bool,
FromHash: fromHash,
FieldName: FIELD_NAME_SET_OF,
}
if pgmerge.DiffValues(&diff, ours.SetOf, theirs.SetOf, ancestor.SetOf, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.SetOf = diff.OurValue.(bool)
}
}
return diffs, ours, nil
}
// DropRootObject implements the interface objinterface.Collection.
func (pgf *Collection) DropRootObject(ctx context.Context, identifier id.Id) error {
if identifier.Section() != id.Section_Function {
return errors.Errorf(`function %s does not exist`, identifier.String())
}
return pgf.DropFunction(ctx, id.Function(identifier))
}
// GetFieldType implements the interface objinterface.Collection.
func (pgf *Collection) GetFieldType(ctx context.Context, fieldName string) *pgtypes.DoltgresType {
switch fieldName {
case FIELD_NAME_PARAMETER_NAMES:
return pgtypes.Text
case FIELD_NAME_RETURN_TYPE:
return pgtypes.Text
case FIELD_NAME_NON_DETERMINISTIC:
return pgtypes.Bool
case FIELD_NAME_STRICT:
return pgtypes.Bool
case FIELD_NAME_DEFINITION:
return pgtypes.Text
case FIELD_NAME_EXTENSION_NAME:
return pgtypes.Text
case FIELD_NAME_EXTENSION_SYMBOL:
return pgtypes.Text
case FIELD_NAME_SQL_DEFINITION:
return pgtypes.Text
case FIELD_NAME_SET_OF:
return pgtypes.Bool
default:
return nil
}
}
// GetID implements the interface objinterface.Collection.
func (pgf *Collection) GetID() objinterface.RootObjectID {
return objinterface.RootObjectID_Functions
}
// GetRootObject implements the interface objinterface.Collection.
func (pgf *Collection) GetRootObject(ctx context.Context, identifier id.Id) (objinterface.RootObject, bool, error) {
if identifier.Section() != id.Section_Function {
return nil, false, nil
}
f, err := pgf.GetFunction(ctx, id.Function(identifier))
return f, err == nil && f.ID.IsValid(), err
}
// HasRootObject implements the interface objinterface.Collection.
func (pgf *Collection) HasRootObject(ctx context.Context, identifier id.Id) (bool, error) {
if identifier.Section() != id.Section_Function {
return false, nil
}
return pgf.HasFunction(ctx, id.Function(identifier)), nil
}
// IDToTableName implements the interface objinterface.Collection.
func (pgf *Collection) IDToTableName(identifier id.Id) doltdb.TableName {
if identifier.Section() != id.Section_Function {
return doltdb.TableName{}
}
return FunctionIDToTableName(id.Function(identifier))
}
// IterAll implements the interface objinterface.Collection.
func (pgf *Collection) IterAll(ctx context.Context, callback func(rootObj objinterface.RootObject) (stop bool, err error)) error {
return pgf.IterateFunctions(ctx, func(f Function) (stop bool, err error) {
return callback(f)
})
}
// IterIDs implements the interface objinterface.Collection.
func (pgf *Collection) IterIDs(ctx context.Context, callback func(identifier id.Id) (stop bool, err error)) error {
return pgf.iterateIDs(ctx, func(funcID id.Function) (stop bool, err error) {
return callback(funcID.AsId())
})
}
// PutRootObject implements the interface objinterface.Collection.
func (pgf *Collection) PutRootObject(ctx context.Context, rootObj objinterface.RootObject) error {
f, ok := rootObj.(Function)
if !ok {
return errors.Newf("invalid function root object: %T", rootObj)
}
return pgf.AddFunction(ctx, f)
}
// RenameRootObject implements the interface objinterface.Collection.
func (pgf *Collection) RenameRootObject(ctx context.Context, oldName id.Id, newName id.Id) error {
if !oldName.IsValid() || !newName.IsValid() || oldName.Section() != newName.Section() || oldName.Section() != id.Section_Function {
return errors.New("cannot rename function due to invalid name")
}
oldFuncName := id.Function(oldName)
newFuncName := id.Function(newName)
if oldFuncName.ParameterCount() != newFuncName.ParameterCount() {
return errors.Newf(`old function id had "%d" parameters, new function id has "%d" parameters`,
oldFuncName.ParameterCount(), newFuncName.ParameterCount())
}
f, err := pgf.GetFunction(ctx, oldFuncName)
if err != nil {
return err
}
if err = pgf.DropFunction(ctx, oldFuncName); err != nil {
return err
}
f.ID = newFuncName
return pgf.AddFunction(ctx, f)
}
// ResolveName implements the interface objinterface.Collection.
func (pgf *Collection) ResolveName(ctx context.Context, name doltdb.TableName) (doltdb.TableName, id.Id, error) {
rawID, err := pgf.resolveName(ctx, name.Schema, name.Name)
if err != nil || !rawID.IsValid() {
return doltdb.TableName{}, id.Null, err
}
return FunctionIDToTableName(rawID), rawID.AsId(), nil
}
// TableNameToID implements the interface objinterface.Collection.
func (pgf *Collection) TableNameToID(name doltdb.TableName) id.Id {
return pgf.tableNameToID(name.Schema, name.Name).AsId()
}
// UpdateField implements the interface objinterface.Collection.
func (pgf *Collection) UpdateField(ctx context.Context, rootObject objinterface.RootObject, fieldName string, newValue any) (objinterface.RootObject, error) {
function := rootObject.(Function)
switch fieldName {
case FIELD_NAME_PARAMETER_NAMES:
function.ParameterNames = strings.Split(newValue.(string), ",")
case FIELD_NAME_RETURN_TYPE:
function.ReturnType = id.NewType(function.ReturnType.SchemaName(), newValue.(string))
case FIELD_NAME_NON_DETERMINISTIC:
function.IsNonDeterministic = newValue.(bool)
case FIELD_NAME_STRICT:
function.Strict = newValue.(bool)
case FIELD_NAME_DEFINITION:
function.Definition = function.ReplaceDefinition(newValue.(string))
parsedBody, err := plpgsql.Parse(function.Definition)
if err != nil {
return nil, err
}
function.Operations = parsedBody
case FIELD_NAME_EXTENSION_NAME:
function.ExtensionName = newValue.(string)
case FIELD_NAME_EXTENSION_SYMBOL:
function.ExtensionSymbol = newValue.(string)
case FIELD_NAME_SQL_DEFINITION:
function.SQLDefinition = newValue.(string)
case FIELD_NAME_SET_OF:
function.SetOf = newValue.(bool)
default:
return nil, errors.Newf("unknown field name: `%s`", fieldName)
}
return function, nil
}
+118
View File
@@ -0,0 +1,118 @@
// 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 (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/server/plpgsql"
"github.com/dolthub/doltgresql/utils"
)
// Serialize returns the Function as a byte slice. If the Function is invalid, then this returns a nil slice.
func (function Function) Serialize(ctx context.Context) ([]byte, error) {
if !function.ID.IsValid() {
return nil, nil
}
// Write all of the functions to the writer
writer := utils.NewWriter(256)
writer.VariableUint(3) // Version
// Write the function data
writer.Id(function.ID.AsId())
writer.Id(function.ReturnType.AsId())
writer.StringSlice(function.ParameterNames)
writer.IdTypeSlice(function.ParameterTypes)
writer.Bool(function.Variadic)
writer.Bool(function.IsNonDeterministic)
writer.Bool(function.Strict)
writer.String(function.Definition)
// Write the operations
writer.VariableUint(uint64(len(function.Operations)))
for _, op := range function.Operations {
writer.Uint16(uint16(op.OpCode))
writer.String(op.PrimaryData)
writer.StringSlice(op.SecondaryData)
writer.String(op.Target)
writer.Int32(int32(op.Index))
writer.StringMap(op.Options)
}
// Write version 1 data
writer.String(function.ExtensionName)
writer.String(function.ExtensionSymbol)
// Write version 2 data
writer.String(function.SQLDefinition)
writer.Bool(function.SetOf)
// Write version 3 data
writer.StringSlice(function.ParameterDefaults)
// Returns the data
return writer.Data(), nil
}
// DeserializeFunction returns the Function that was serialized in the byte slice. Returns an empty Function (invalid
// ID) if data is nil or empty.
func DeserializeFunction(ctx context.Context, data []byte) (Function, error) {
if len(data) == 0 {
return Function{}, nil
}
reader := utils.NewReader(data)
version := reader.VariableUint()
if version > 3 {
return Function{}, errors.Errorf("version %d of functions is not supported, please upgrade the server", version)
}
// Read from the reader
f := Function{}
f.ID = id.Function(reader.Id())
f.ReturnType = id.Type(reader.Id())
f.ParameterNames = reader.StringSlice()
f.ParameterTypes = reader.IdTypeSlice()
f.Variadic = reader.Bool()
f.IsNonDeterministic = reader.Bool()
f.Strict = reader.Bool()
f.Definition = reader.String()
// Read the operations
opCount := reader.VariableUint()
f.Operations = make([]plpgsql.InterpreterOperation, opCount)
for opIdx := uint64(0); opIdx < opCount; opIdx++ {
op := plpgsql.InterpreterOperation{}
op.OpCode = plpgsql.OpCode(reader.Uint16())
op.PrimaryData = reader.String()
op.SecondaryData = reader.StringSlice()
op.Target = reader.String()
op.Index = int(reader.Int32())
op.Options = reader.StringMap()
f.Operations[opIdx] = op
}
if version >= 1 {
f.ExtensionName = reader.String()
f.ExtensionSymbol = reader.String()
}
if version >= 2 {
f.SQLDefinition = reader.String()
f.SetOf = reader.Bool()
}
if version >= 3 {
f.ParameterDefaults = reader.StringSlice()
}
if !reader.IsEmpty() {
return Function{}, errors.Errorf("extra data found while deserializing a function")
}
// Return the deserialized object
return f, nil
}
+144
View File
@@ -0,0 +1,144 @@
// 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 id
import (
"hash/crc32"
"sync"
)
// builtinOidLimit is the largest OID that Postgres will assign to built-in items, so we use this to mitigate conflicts
// with existing and future built-in OIDs.
const builtinOidLimit = 65535
var (
// crcTable is the table that is used for our CRC operations.
crcTable = crc32.MakeTable(crc32.Castagnoli)
// globalCache is the cache structure that is used for the server session.
globalCache = &cacheStruct{
mutex: &sync.RWMutex{},
toOID: map[Id]uint32{Null: 0},
toInternal: map[uint32]Id{0: Null},
}
)
// cacheStruct is the cache structure that holds mappings between the internal ID and external OID (used by Postgres).
// The mappings are temporary, and exist only within a server session. We must discourage users from storing converted
// OIDs, and to use the actual OID type, since the type uses internal IDs so long as it's not returned to the user.
type cacheStruct struct {
mutex *sync.RWMutex
toOID map[Id]uint32
toInternal map[uint32]Id
}
// Cache returns the global cache that is used for the server session.
func Cache() *cacheStruct {
return globalCache
}
// ToOID returns the OID associated with the given internal ID.
func (cache *cacheStruct) ToOID(id Id) uint32 {
// If the ID is in the cache, then we can just return its associated OID
cache.mutex.RLock()
if oid, ok := cache.toOID[id]; ok {
cache.mutex.RUnlock()
return oid
}
cache.mutex.RUnlock()
if id.Section() == Section_OID {
return Oid(id).OID()
}
cache.mutex.Lock()
defer cache.mutex.Unlock()
underlyingBytes := id.UnderlyingBytes()
oid := crc32.Checksum(underlyingBytes, crcTable)
// If the generated OID is valid, then we'll add it to the cache and return it
if _, ok := cache.toInternal[oid]; !ok && oid > builtinOidLimit {
cache.toOID[id] = oid
cache.toInternal[oid] = id
return oid
}
// In this case, the OID is not valid, so we'll run a small loop to generate an OID based on the actual ID.
// This retains some level of determinism for OID to ID relationships.
modifiedBytes := make([]byte, len(underlyingBytes)+1)
copy(modifiedBytes[1:], underlyingBytes)
for i := byte(0); i < 255; i++ {
modifiedBytes[0] = i
oid = crc32.Checksum(underlyingBytes, crcTable)
if _, ok := cache.toInternal[oid]; !ok && oid > builtinOidLimit {
cache.toOID[id] = oid
cache.toInternal[oid] = id
return oid
}
}
// If we're here, then we'll just search for an empty OID as a last resort
for i := uint32(4294967295); i > builtinOidLimit; i-- {
if _, ok := cache.toInternal[oid]; !ok {
cache.toOID[id] = oid
cache.toInternal[oid] = id
return oid
}
}
// We must have over 4 billion items in the database, so we'll panic since there's nothing we can do
panic("all OIDs have been taken")
}
// ToInternal returns the internal ID associated with the given OID.
func (cache *cacheStruct) ToInternal(oid uint32) Id {
cache.mutex.RLock()
defer cache.mutex.RUnlock()
if id, ok := cache.toInternal[oid]; ok {
return id
}
// The OID is not in the cache, so it's invalid
return ""
}
// Exists returns whether the given internal ID exists within the cache. This should primarily be used for the default
// functions, as it's not guaranteed that user functions will be in the cache, especially after a server restart.
func (cache *cacheStruct) Exists(id Id) bool {
cache.mutex.RLock()
defer cache.mutex.RUnlock()
_, ok := cache.toOID[id]
return ok
}
// setBuiltIn sets the given ID to the OID. This should only be used for the built-in items.
func (cache *cacheStruct) setBuiltIn(id Id, oid uint32) {
if oid > builtinOidLimit {
panic("oid is not a built-in")
}
cache.toOID[id] = oid
cache.toInternal[oid] = id
}
// update is used to change the OID mapping of an existing internal ID that has been changed (where the internal ID
// points to the same logical item).
//
//lint:ignore U1000 For future use
func (cache *cacheStruct) update(old Id, new Id) {
cache.mutex.Lock()
defer cache.mutex.Unlock()
// If the old ID doesn't exist in the cache, then we don't have anything to update
oid, ok := cache.toOID[old]
if !ok {
return
}
// We'll delete the old entry and add the new entry, keeping the OID the same for the server session
delete(cache.toOID, old)
delete(cache.toInternal, oid)
cache.toOID[new] = oid
cache.toInternal[oid] = new
}
+26
View File
@@ -0,0 +1,26 @@
// 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 id
// This adds all of the built-in schemas to the cache.
func init() {
globalCache.setBuiltIn(NewId(Section_AccessMethod, "heap"), 2)
globalCache.setBuiltIn(NewId(Section_AccessMethod, "btree"), 403)
globalCache.setBuiltIn(NewId(Section_AccessMethod, "hash"), 405)
globalCache.setBuiltIn(NewId(Section_AccessMethod, "gist"), 783)
globalCache.setBuiltIn(NewId(Section_AccessMethod, "gin"), 2742)
globalCache.setBuiltIn(NewId(Section_AccessMethod, "brin"), 3580)
globalCache.setBuiltIn(NewId(Section_AccessMethod, "spgist"), 4000)
}
+806
View File
@@ -0,0 +1,806 @@
// 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 id
// This adds all of the built-in schemas to the cache.
func init() {
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "default"), 100)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "C"), 950)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "POSIX"), 951)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ucs_basic"), 12340)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "und-x-icu"), 12341)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "af-x-icu"), 12342)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "af-NA-x-icu"), 12343)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "af-ZA-x-icu"), 12344)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "agq-x-icu"), 12345)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "agq-CM-x-icu"), 12346)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ak-x-icu"), 12347)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ak-GH-x-icu"), 12348)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "am-x-icu"), 12349)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "am-ET-x-icu"), 12350)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-x-icu"), 12351)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-001-x-icu"), 12352)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-AE-x-icu"), 12353)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-BH-x-icu"), 12354)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-DJ-x-icu"), 12355)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-DZ-x-icu"), 12356)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-EG-x-icu"), 12357)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-EH-x-icu"), 12358)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-ER-x-icu"), 12359)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-IL-x-icu"), 12360)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-IQ-x-icu"), 12361)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-JO-x-icu"), 12362)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-KM-x-icu"), 12363)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-KW-x-icu"), 12364)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-LB-x-icu"), 12365)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-LY-x-icu"), 12366)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-MA-x-icu"), 12367)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-MR-x-icu"), 12368)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-OM-x-icu"), 12369)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-PS-x-icu"), 12370)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-QA-x-icu"), 12371)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-SA-x-icu"), 12372)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-SD-x-icu"), 12373)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-SO-x-icu"), 12374)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-SS-x-icu"), 12375)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-SY-x-icu"), 12376)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-TD-x-icu"), 12377)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-TN-x-icu"), 12378)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ar-YE-x-icu"), 12379)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "as-x-icu"), 12380)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "as-IN-x-icu"), 12381)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "asa-x-icu"), 12382)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "asa-TZ-x-icu"), 12383)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ast-x-icu"), 12384)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ast-ES-x-icu"), 12385)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "az-x-icu"), 12386)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "az-Cyrl-x-icu"), 12387)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "az-Cyrl-AZ-x-icu"), 12388)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "az-Latn-x-icu"), 12389)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "az-Latn-AZ-x-icu"), 12390)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bas-x-icu"), 12391)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bas-CM-x-icu"), 12392)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "be-x-icu"), 12393)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "be-BY-x-icu"), 12394)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bem-x-icu"), 12395)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bem-ZM-x-icu"), 12396)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bez-x-icu"), 12397)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bez-TZ-x-icu"), 12398)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bg-x-icu"), 12399)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bg-BG-x-icu"), 12400)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bm-x-icu"), 12401)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bm-ML-x-icu"), 12402)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bn-x-icu"), 12403)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bn-BD-x-icu"), 12404)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bn-IN-x-icu"), 12405)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bo-x-icu"), 12406)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bo-CN-x-icu"), 12407)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bo-IN-x-icu"), 12408)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "br-x-icu"), 12409)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "br-FR-x-icu"), 12410)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "brx-x-icu"), 12411)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "brx-IN-x-icu"), 12412)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bs-x-icu"), 12413)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bs-Cyrl-x-icu"), 12414)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bs-Cyrl-BA-x-icu"), 12415)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bs-Latn-x-icu"), 12416)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "bs-Latn-BA-x-icu"), 12417)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ca-x-icu"), 12418)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ca-AD-x-icu"), 12419)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ca-ES-x-icu"), 12420)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ca-FR-x-icu"), 12421)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ca-IT-x-icu"), 12422)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ccp-x-icu"), 12423)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ccp-BD-x-icu"), 12424)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ccp-IN-x-icu"), 12425)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ce-x-icu"), 12426)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ce-RU-x-icu"), 12427)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ceb-x-icu"), 12428)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ceb-PH-x-icu"), 12429)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "cgg-x-icu"), 12430)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "cgg-UG-x-icu"), 12431)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "chr-x-icu"), 12432)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "chr-US-x-icu"), 12433)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ckb-x-icu"), 12434)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ckb-IQ-x-icu"), 12435)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ckb-IR-x-icu"), 12436)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "cs-x-icu"), 12437)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "cs-CZ-x-icu"), 12438)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "cy-x-icu"), 12439)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "cy-GB-x-icu"), 12440)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "da-x-icu"), 12441)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "da-DK-x-icu"), 12442)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "da-GL-x-icu"), 12443)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "dav-x-icu"), 12444)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "dav-KE-x-icu"), 12445)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "de-x-icu"), 12446)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "de-AT-x-icu"), 12447)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "de-BE-x-icu"), 12448)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "de-CH-x-icu"), 12449)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "de-DE-x-icu"), 12450)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "de-IT-x-icu"), 12451)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "de-LI-x-icu"), 12452)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "de-LU-x-icu"), 12453)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "dje-x-icu"), 12454)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "dje-NE-x-icu"), 12455)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "dsb-x-icu"), 12456)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "dsb-DE-x-icu"), 12457)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "dua-x-icu"), 12458)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "dua-CM-x-icu"), 12459)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "dyo-x-icu"), 12460)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "dyo-SN-x-icu"), 12461)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "dz-x-icu"), 12462)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "dz-BT-x-icu"), 12463)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ebu-x-icu"), 12464)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ebu-KE-x-icu"), 12465)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ee-x-icu"), 12466)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ee-GH-x-icu"), 12467)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ee-TG-x-icu"), 12468)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "el-x-icu"), 12469)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "el-CY-x-icu"), 12470)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "el-GR-x-icu"), 12471)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-x-icu"), 12472)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-001-x-icu"), 12473)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-150-x-icu"), 12474)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-AE-x-icu"), 12475)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-AG-x-icu"), 12476)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-AI-x-icu"), 12477)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-AS-x-icu"), 12478)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-AT-x-icu"), 12479)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-AU-x-icu"), 12480)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-BB-x-icu"), 12481)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-BE-x-icu"), 12482)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-BI-x-icu"), 12483)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-BM-x-icu"), 12484)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-BS-x-icu"), 12485)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-BW-x-icu"), 12486)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-BZ-x-icu"), 12487)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-CA-x-icu"), 12488)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-CC-x-icu"), 12489)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-CH-x-icu"), 12490)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-CK-x-icu"), 12491)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-CM-x-icu"), 12492)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-CX-x-icu"), 12493)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-CY-x-icu"), 12494)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-DE-x-icu"), 12495)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-DG-x-icu"), 12496)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-DK-x-icu"), 12497)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-DM-x-icu"), 12498)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-ER-x-icu"), 12499)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-FI-x-icu"), 12500)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-FJ-x-icu"), 12501)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-FK-x-icu"), 12502)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-FM-x-icu"), 12503)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-GB-x-icu"), 12504)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-GD-x-icu"), 12505)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-GG-x-icu"), 12506)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-GH-x-icu"), 12507)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-GI-x-icu"), 12508)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-GM-x-icu"), 12509)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-GU-x-icu"), 12510)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-GY-x-icu"), 12511)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-HK-x-icu"), 12512)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-IE-x-icu"), 12513)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-IL-x-icu"), 12514)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-IM-x-icu"), 12515)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-IN-x-icu"), 12516)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-IO-x-icu"), 12517)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-JE-x-icu"), 12518)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-JM-x-icu"), 12519)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-KE-x-icu"), 12520)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-KI-x-icu"), 12521)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-KN-x-icu"), 12522)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-KY-x-icu"), 12523)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-LC-x-icu"), 12524)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-LR-x-icu"), 12525)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-LS-x-icu"), 12526)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-MG-x-icu"), 12527)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-MH-x-icu"), 12528)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-MO-x-icu"), 12529)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-MP-x-icu"), 12530)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-MS-x-icu"), 12531)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-MT-x-icu"), 12532)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-MU-x-icu"), 12533)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-MW-x-icu"), 12534)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-MY-x-icu"), 12535)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-NA-x-icu"), 12536)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-NF-x-icu"), 12537)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-NG-x-icu"), 12538)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-NL-x-icu"), 12539)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-NR-x-icu"), 12540)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-NU-x-icu"), 12541)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-NZ-x-icu"), 12542)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-PG-x-icu"), 12543)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-PH-x-icu"), 12544)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-PK-x-icu"), 12545)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-PN-x-icu"), 12546)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-PR-x-icu"), 12547)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-PW-x-icu"), 12548)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-RW-x-icu"), 12549)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-SB-x-icu"), 12550)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-SC-x-icu"), 12551)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-SD-x-icu"), 12552)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-SE-x-icu"), 12553)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-SG-x-icu"), 12554)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-SH-x-icu"), 12555)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-SI-x-icu"), 12556)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-SL-x-icu"), 12557)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-SS-x-icu"), 12558)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-SX-x-icu"), 12559)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-SZ-x-icu"), 12560)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-TC-x-icu"), 12561)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-TK-x-icu"), 12562)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-TO-x-icu"), 12563)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-TT-x-icu"), 12564)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-TV-x-icu"), 12565)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-TZ-x-icu"), 12566)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-UG-x-icu"), 12567)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-UM-x-icu"), 12568)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-US-x-icu"), 12569)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-US-u-va-posix-x-icu"), 12570)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-VC-x-icu"), 12571)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-VG-x-icu"), 12572)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-VI-x-icu"), 12573)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-VU-x-icu"), 12574)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-WS-x-icu"), 12575)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-ZA-x-icu"), 12576)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-ZM-x-icu"), 12577)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "en-ZW-x-icu"), 12578)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "eo-x-icu"), 12579)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "eo-001-x-icu"), 12580)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-x-icu"), 12581)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-419-x-icu"), 12582)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-AR-x-icu"), 12583)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-BO-x-icu"), 12584)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-BR-x-icu"), 12585)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-BZ-x-icu"), 12586)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-CL-x-icu"), 12587)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-CO-x-icu"), 12588)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-CR-x-icu"), 12589)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-CU-x-icu"), 12590)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-DO-x-icu"), 12591)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-EA-x-icu"), 12592)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-EC-x-icu"), 12593)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-ES-x-icu"), 12594)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-GQ-x-icu"), 12595)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-GT-x-icu"), 12596)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-HN-x-icu"), 12597)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-IC-x-icu"), 12598)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-MX-x-icu"), 12599)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-NI-x-icu"), 12600)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-PA-x-icu"), 12601)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-PE-x-icu"), 12602)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-PH-x-icu"), 12603)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-PR-x-icu"), 12604)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-PY-x-icu"), 12605)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-SV-x-icu"), 12606)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-US-x-icu"), 12607)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-UY-x-icu"), 12608)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "es-VE-x-icu"), 12609)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "et-x-icu"), 12610)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "et-EE-x-icu"), 12611)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "eu-x-icu"), 12612)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "eu-ES-x-icu"), 12613)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ewo-x-icu"), 12614)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ewo-CM-x-icu"), 12615)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fa-x-icu"), 12616)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fa-AF-x-icu"), 12617)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fa-IR-x-icu"), 12618)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-x-icu"), 12619)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Adlm-x-icu"), 12620)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Adlm-BF-x-icu"), 12621)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Adlm-CM-x-icu"), 12622)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Adlm-GH-x-icu"), 12623)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Adlm-GM-x-icu"), 12624)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Adlm-GN-x-icu"), 12625)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Adlm-GW-x-icu"), 12626)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Adlm-LR-x-icu"), 12627)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Adlm-MR-x-icu"), 12628)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Adlm-NE-x-icu"), 12629)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Adlm-NG-x-icu"), 12630)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Adlm-SL-x-icu"), 12631)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Adlm-SN-x-icu"), 12632)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Latn-x-icu"), 12633)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Latn-BF-x-icu"), 12634)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Latn-CM-x-icu"), 12635)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Latn-GH-x-icu"), 12636)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Latn-GM-x-icu"), 12637)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Latn-GN-x-icu"), 12638)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Latn-GW-x-icu"), 12639)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Latn-LR-x-icu"), 12640)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Latn-MR-x-icu"), 12641)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Latn-NE-x-icu"), 12642)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Latn-NG-x-icu"), 12643)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Latn-SL-x-icu"), 12644)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ff-Latn-SN-x-icu"), 12645)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fi-x-icu"), 12646)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fi-FI-x-icu"), 12647)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fil-x-icu"), 12648)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fil-PH-x-icu"), 12649)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fo-x-icu"), 12650)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fo-DK-x-icu"), 12651)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fo-FO-x-icu"), 12652)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-x-icu"), 12653)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-BE-x-icu"), 12654)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-BF-x-icu"), 12655)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-BI-x-icu"), 12656)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-BJ-x-icu"), 12657)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-BL-x-icu"), 12658)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-CA-x-icu"), 12659)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-CD-x-icu"), 12660)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-CF-x-icu"), 12661)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-CG-x-icu"), 12662)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-CH-x-icu"), 12663)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-CI-x-icu"), 12664)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-CM-x-icu"), 12665)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-DJ-x-icu"), 12666)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-DZ-x-icu"), 12667)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-FR-x-icu"), 12668)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-GA-x-icu"), 12669)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-GF-x-icu"), 12670)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-GN-x-icu"), 12671)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-GP-x-icu"), 12672)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-GQ-x-icu"), 12673)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-HT-x-icu"), 12674)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-KM-x-icu"), 12675)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-LU-x-icu"), 12676)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-MA-x-icu"), 12677)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-MC-x-icu"), 12678)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-MF-x-icu"), 12679)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-MG-x-icu"), 12680)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-ML-x-icu"), 12681)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-MQ-x-icu"), 12682)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-MR-x-icu"), 12683)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-MU-x-icu"), 12684)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-NC-x-icu"), 12685)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-NE-x-icu"), 12686)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-PF-x-icu"), 12687)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-PM-x-icu"), 12688)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-RE-x-icu"), 12689)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-RW-x-icu"), 12690)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-SC-x-icu"), 12691)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-SN-x-icu"), 12692)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-SY-x-icu"), 12693)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-TD-x-icu"), 12694)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-TG-x-icu"), 12695)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-TN-x-icu"), 12696)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-VU-x-icu"), 12697)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-WF-x-icu"), 12698)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fr-YT-x-icu"), 12699)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fur-x-icu"), 12700)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fur-IT-x-icu"), 12701)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fy-x-icu"), 12702)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "fy-NL-x-icu"), 12703)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ga-x-icu"), 12704)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ga-GB-x-icu"), 12705)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ga-IE-x-icu"), 12706)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "gd-x-icu"), 12707)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "gd-GB-x-icu"), 12708)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "gl-x-icu"), 12709)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "gl-ES-x-icu"), 12710)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "gsw-x-icu"), 12711)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "gsw-CH-x-icu"), 12712)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "gsw-FR-x-icu"), 12713)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "gsw-LI-x-icu"), 12714)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "gu-x-icu"), 12715)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "gu-IN-x-icu"), 12716)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "guz-x-icu"), 12717)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "guz-KE-x-icu"), 12718)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "gv-x-icu"), 12719)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "gv-IM-x-icu"), 12720)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ha-x-icu"), 12721)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ha-GH-x-icu"), 12722)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ha-NE-x-icu"), 12723)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ha-NG-x-icu"), 12724)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "haw-x-icu"), 12725)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "haw-US-x-icu"), 12726)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "he-x-icu"), 12727)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "he-IL-x-icu"), 12728)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "hi-x-icu"), 12729)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "hi-IN-x-icu"), 12730)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "hr-x-icu"), 12731)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "hr-BA-x-icu"), 12732)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "hr-HR-x-icu"), 12733)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "hsb-x-icu"), 12734)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "hsb-DE-x-icu"), 12735)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "hu-x-icu"), 12736)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "hu-HU-x-icu"), 12737)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "hy-x-icu"), 12738)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "hy-AM-x-icu"), 12739)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ia-x-icu"), 27401)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ia-001-x-icu"), 27411)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "id-x-icu"), 27421)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "id-ID-x-icu"), 27431)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ig-x-icu"), 27441)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ig-NG-x-icu"), 27451)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ii-x-icu"), 27461)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ii-CN-x-icu"), 27471)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "is-x-icu"), 27481)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "is-IS-x-icu"), 27491)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "it-x-icu"), 27501)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "it-CH-x-icu"), 27511)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "it-IT-x-icu"), 27521)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "it-SM-x-icu"), 27531)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "it-VA-x-icu"), 27541)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ja-x-icu"), 27551)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ja-JP-x-icu"), 27561)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "jgo-x-icu"), 27571)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "jgo-CM-x-icu"), 27581)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "jmc-x-icu"), 27591)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "jmc-TZ-x-icu"), 27601)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "jv-x-icu"), 27611)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "jv-ID-x-icu"), 27621)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ka-x-icu"), 27631)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ka-GE-x-icu"), 27641)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kab-x-icu"), 27651)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kab-DZ-x-icu"), 27661)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kam-x-icu"), 27671)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kam-KE-x-icu"), 27681)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kde-x-icu"), 27691)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kde-TZ-x-icu"), 27701)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kea-x-icu"), 27711)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kea-CV-x-icu"), 27721)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "khq-x-icu"), 27731)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "khq-ML-x-icu"), 27741)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ki-x-icu"), 27751)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ki-KE-x-icu"), 27761)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kk-x-icu"), 27771)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kk-KZ-x-icu"), 27781)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kkj-x-icu"), 27791)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kkj-CM-x-icu"), 27801)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kl-x-icu"), 27811)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kl-GL-x-icu"), 27821)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kln-x-icu"), 27831)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kln-KE-x-icu"), 27841)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "km-x-icu"), 27851)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "km-KH-x-icu"), 27861)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kn-x-icu"), 27871)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kn-IN-x-icu"), 27881)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ko-x-icu"), 27891)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ko-KP-x-icu"), 27901)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ko-KR-x-icu"), 27911)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kok-x-icu"), 27921)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kok-IN-x-icu"), 27931)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ks-x-icu"), 27941)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ks-Arab-x-icu"), 27951)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ks-Arab-IN-x-icu"), 27961)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ksb-x-icu"), 27971)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ksb-TZ-x-icu"), 27981)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ksf-x-icu"), 27991)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ksf-CM-x-icu"), 28001)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ksh-x-icu"), 28011)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ksh-DE-x-icu"), 28021)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ku-x-icu"), 28031)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ku-TR-x-icu"), 28041)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kw-x-icu"), 28051)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "kw-GB-x-icu"), 28061)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ky-x-icu"), 28071)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ky-KG-x-icu"), 28081)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lag-x-icu"), 28091)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lag-TZ-x-icu"), 28101)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lb-x-icu"), 28111)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lb-LU-x-icu"), 28121)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lg-x-icu"), 28131)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lg-UG-x-icu"), 28141)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lkt-x-icu"), 28151)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lkt-US-x-icu"), 28161)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ln-x-icu"), 28171)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ln-AO-x-icu"), 28181)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ln-CD-x-icu"), 28191)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ln-CF-x-icu"), 28201)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ln-CG-x-icu"), 28211)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lo-x-icu"), 28221)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lo-LA-x-icu"), 28231)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lrc-x-icu"), 28241)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lrc-IQ-x-icu"), 28251)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lrc-IR-x-icu"), 28261)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lt-x-icu"), 28271)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lt-LT-x-icu"), 28281)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lu-x-icu"), 28291)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lu-CD-x-icu"), 28301)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "luo-x-icu"), 28311)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "luo-KE-x-icu"), 28321)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "luy-x-icu"), 28331)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "luy-KE-x-icu"), 28341)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lv-x-icu"), 28351)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "lv-LV-x-icu"), 28361)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mai-x-icu"), 28371)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mai-IN-x-icu"), 28381)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mas-x-icu"), 28391)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mas-KE-x-icu"), 28401)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mas-TZ-x-icu"), 28411)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mer-x-icu"), 28421)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mer-KE-x-icu"), 28431)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mfe-x-icu"), 28441)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mfe-MU-x-icu"), 28451)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mg-x-icu"), 28461)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mg-MG-x-icu"), 28471)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mgh-x-icu"), 28481)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mgh-MZ-x-icu"), 28491)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mgo-x-icu"), 28501)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mgo-CM-x-icu"), 28511)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mi-x-icu"), 28521)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mi-NZ-x-icu"), 28531)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mk-x-icu"), 28541)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mk-MK-x-icu"), 28551)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ml-x-icu"), 28561)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ml-IN-x-icu"), 28571)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mn-x-icu"), 28581)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mn-MN-x-icu"), 28591)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mni-x-icu"), 28601)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mni-Beng-x-icu"), 28611)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mni-Beng-IN-x-icu"), 28621)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mr-x-icu"), 28631)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mr-IN-x-icu"), 28641)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ms-x-icu"), 28651)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ms-BN-x-icu"), 28661)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ms-ID-x-icu"), 28671)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ms-MY-x-icu"), 28681)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ms-SG-x-icu"), 28691)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mt-x-icu"), 28701)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mt-MT-x-icu"), 28711)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mua-x-icu"), 28721)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mua-CM-x-icu"), 28731)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "my-x-icu"), 28741)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "my-MM-x-icu"), 28751)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mzn-x-icu"), 28761)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "mzn-IR-x-icu"), 28771)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "naq-x-icu"), 28781)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "naq-NA-x-icu"), 28791)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nb-x-icu"), 28801)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nb-NO-x-icu"), 28811)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nb-SJ-x-icu"), 28821)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nd-x-icu"), 28831)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nd-ZW-x-icu"), 28841)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nds-x-icu"), 28851)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nds-DE-x-icu"), 28861)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nds-NL-x-icu"), 28871)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ne-x-icu"), 28881)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ne-IN-x-icu"), 28891)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ne-NP-x-icu"), 28901)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nl-x-icu"), 28911)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nl-AW-x-icu"), 28921)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nl-BE-x-icu"), 28931)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nl-BQ-x-icu"), 28941)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nl-CW-x-icu"), 28951)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nl-NL-x-icu"), 28961)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nl-SR-x-icu"), 28971)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nl-SX-x-icu"), 28981)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nmg-x-icu"), 28991)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nmg-CM-x-icu"), 29001)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nn-x-icu"), 29011)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nn-NO-x-icu"), 29021)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nnh-x-icu"), 29031)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nnh-CM-x-icu"), 29041)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nus-x-icu"), 29051)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nus-SS-x-icu"), 29061)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nyn-x-icu"), 29071)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "nyn-UG-x-icu"), 29081)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "om-x-icu"), 29091)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "om-ET-x-icu"), 29101)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "om-KE-x-icu"), 29111)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "or-x-icu"), 29121)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "or-IN-x-icu"), 29131)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "os-x-icu"), 29141)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "os-GE-x-icu"), 29151)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "os-RU-x-icu"), 29161)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pa-x-icu"), 29171)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pa-Arab-x-icu"), 29181)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pa-Arab-PK-x-icu"), 29191)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pa-Guru-x-icu"), 29201)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pa-Guru-IN-x-icu"), 29211)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pcm-x-icu"), 29221)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pcm-NG-x-icu"), 29231)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pl-x-icu"), 29241)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pl-PL-x-icu"), 29251)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ps-x-icu"), 29261)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ps-AF-x-icu"), 29271)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ps-PK-x-icu"), 29281)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pt-x-icu"), 29291)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pt-AO-x-icu"), 29301)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pt-BR-x-icu"), 29311)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pt-CH-x-icu"), 29321)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pt-CV-x-icu"), 29331)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pt-GQ-x-icu"), 29341)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pt-GW-x-icu"), 29351)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pt-LU-x-icu"), 29361)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pt-MO-x-icu"), 29371)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pt-MZ-x-icu"), 29381)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pt-PT-x-icu"), 29391)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pt-ST-x-icu"), 29401)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "pt-TL-x-icu"), 29411)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "qu-x-icu"), 29421)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "qu-BO-x-icu"), 29431)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "qu-EC-x-icu"), 29441)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "qu-PE-x-icu"), 29451)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "rm-x-icu"), 29461)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "rm-CH-x-icu"), 29471)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "rn-x-icu"), 29481)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "rn-BI-x-icu"), 29491)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ro-x-icu"), 29501)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ro-MD-x-icu"), 29511)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ro-RO-x-icu"), 29521)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "rof-x-icu"), 29531)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "rof-TZ-x-icu"), 29541)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ru-x-icu"), 29551)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ru-BY-x-icu"), 29561)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ru-KG-x-icu"), 29571)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ru-KZ-x-icu"), 29581)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ru-MD-x-icu"), 29591)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ru-RU-x-icu"), 29601)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ru-UA-x-icu"), 29611)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "rw-x-icu"), 29621)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "rw-RW-x-icu"), 29631)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "rwk-x-icu"), 29641)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "rwk-TZ-x-icu"), 29651)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sah-x-icu"), 29661)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sah-RU-x-icu"), 29671)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "saq-x-icu"), 29681)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "saq-KE-x-icu"), 29691)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sat-x-icu"), 29701)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sat-Olck-x-icu"), 29711)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sat-Olck-IN-x-icu"), 29721)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sbp-x-icu"), 29731)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sbp-TZ-x-icu"), 29741)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sd-x-icu"), 29751)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sd-Arab-x-icu"), 29761)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sd-Arab-PK-x-icu"), 29771)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sd-Deva-x-icu"), 29781)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sd-Deva-IN-x-icu"), 29791)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "se-x-icu"), 29801)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "se-FI-x-icu"), 29811)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "se-NO-x-icu"), 29821)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "se-SE-x-icu"), 29831)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "seh-x-icu"), 29841)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "seh-MZ-x-icu"), 29851)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ses-x-icu"), 29861)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ses-ML-x-icu"), 29871)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sg-x-icu"), 29881)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sg-CF-x-icu"), 29891)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "shi-x-icu"), 29901)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "shi-Latn-x-icu"), 29911)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "shi-Latn-MA-x-icu"), 29921)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "shi-Tfng-x-icu"), 29931)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "shi-Tfng-MA-x-icu"), 29941)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "si-x-icu"), 29951)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "si-LK-x-icu"), 29961)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sk-x-icu"), 29971)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sk-SK-x-icu"), 29981)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sl-x-icu"), 29991)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sl-SI-x-icu"), 30001)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "smn-x-icu"), 30011)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "smn-FI-x-icu"), 30021)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sn-x-icu"), 30031)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sn-ZW-x-icu"), 30041)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "so-x-icu"), 30051)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "so-DJ-x-icu"), 30061)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "so-ET-x-icu"), 30071)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "so-KE-x-icu"), 30081)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "so-SO-x-icu"), 30091)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sq-x-icu"), 30101)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sq-AL-x-icu"), 30111)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sq-MK-x-icu"), 30121)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sq-XK-x-icu"), 30131)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sr-x-icu"), 30141)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sr-Cyrl-x-icu"), 30151)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sr-Cyrl-BA-x-icu"), 30161)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sr-Cyrl-ME-x-icu"), 30171)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sr-Cyrl-RS-x-icu"), 30181)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sr-Cyrl-XK-x-icu"), 30191)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sr-Latn-x-icu"), 30201)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sr-Latn-BA-x-icu"), 30211)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sr-Latn-ME-x-icu"), 30221)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sr-Latn-RS-x-icu"), 30231)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sr-Latn-XK-x-icu"), 30241)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "su-x-icu"), 30251)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "su-Latn-x-icu"), 30261)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "su-Latn-ID-x-icu"), 30271)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sv-x-icu"), 30281)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sv-AX-x-icu"), 30291)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sv-FI-x-icu"), 30301)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sv-SE-x-icu"), 30311)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sw-x-icu"), 30321)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sw-CD-x-icu"), 30331)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sw-KE-x-icu"), 30341)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sw-TZ-x-icu"), 30351)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "sw-UG-x-icu"), 30361)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ta-x-icu"), 30371)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ta-IN-x-icu"), 30381)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ta-LK-x-icu"), 30391)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ta-MY-x-icu"), 30401)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ta-SG-x-icu"), 30411)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "te-x-icu"), 30421)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "te-IN-x-icu"), 30431)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "teo-x-icu"), 30441)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "teo-KE-x-icu"), 30451)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "teo-UG-x-icu"), 30461)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "tg-x-icu"), 30471)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "tg-TJ-x-icu"), 30481)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "th-x-icu"), 30491)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "th-TH-x-icu"), 30501)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ti-x-icu"), 30511)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ti-ER-x-icu"), 30521)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ti-ET-x-icu"), 30531)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "tk-x-icu"), 30541)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "tk-TM-x-icu"), 30551)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "to-x-icu"), 30561)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "to-TO-x-icu"), 30571)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "tr-x-icu"), 30581)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "tr-CY-x-icu"), 30591)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "tr-TR-x-icu"), 30601)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "tt-x-icu"), 30611)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "tt-RU-x-icu"), 30621)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "twq-x-icu"), 30631)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "twq-NE-x-icu"), 30641)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "tzm-x-icu"), 30651)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "tzm-MA-x-icu"), 30661)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ug-x-icu"), 30671)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ug-CN-x-icu"), 30681)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "uk-x-icu"), 30691)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "uk-UA-x-icu"), 30701)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ur-x-icu"), 30711)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ur-IN-x-icu"), 30721)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "ur-PK-x-icu"), 30731)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "uz-x-icu"), 30741)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "uz-Arab-x-icu"), 30751)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "uz-Arab-AF-x-icu"), 30761)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "uz-Cyrl-x-icu"), 30771)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "uz-Cyrl-UZ-x-icu"), 30781)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "uz-Latn-x-icu"), 30791)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "uz-Latn-UZ-x-icu"), 30801)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "vai-x-icu"), 30811)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "vai-Latn-x-icu"), 30821)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "vai-Latn-LR-x-icu"), 30831)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "vai-Vaii-x-icu"), 30841)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "vai-Vaii-LR-x-icu"), 30851)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "vi-x-icu"), 30861)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "vi-VN-x-icu"), 30871)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "vun-x-icu"), 30881)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "vun-TZ-x-icu"), 30891)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "wae-x-icu"), 30901)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "wae-CH-x-icu"), 30911)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "wo-x-icu"), 30921)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "wo-SN-x-icu"), 30931)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "xh-x-icu"), 30941)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "xh-ZA-x-icu"), 30951)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "xog-x-icu"), 30961)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "xog-UG-x-icu"), 30971)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "yav-x-icu"), 30981)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "yav-CM-x-icu"), 30991)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "yi-x-icu"), 31001)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "yi-001-x-icu"), 31011)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "yo-x-icu"), 31021)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "yo-BJ-x-icu"), 31031)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "yo-NG-x-icu"), 31041)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "yue-x-icu"), 31051)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "yue-Hans-x-icu"), 31061)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "yue-Hans-CN-x-icu"), 31071)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "yue-Hant-x-icu"), 31081)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "yue-Hant-HK-x-icu"), 31091)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "zgh-x-icu"), 31101)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "zgh-MA-x-icu"), 31111)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "zh-x-icu"), 31121)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "zh-Hans-x-icu"), 31131)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "zh-Hans-CN-x-icu"), 31141)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "zh-Hans-HK-x-icu"), 31151)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "zh-Hans-MO-x-icu"), 31161)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "zh-Hans-SG-x-icu"), 31171)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "zh-Hant-x-icu"), 31181)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "zh-Hant-HK-x-icu"), 31191)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "zh-Hant-MO-x-icu"), 31201)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "zh-Hant-TW-x-icu"), 31211)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "zu-x-icu"), 31221)
globalCache.setBuiltIn(NewId(Section_Collation, "pg_catalog", "zu-ZA-x-icu"), 31231)
}
+22
View File
@@ -0,0 +1,22 @@
// 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 id
// This adds all of the built-in databases to the cache.
func init() {
globalCache.setBuiltIn(NewId(Section_Database, "template1"), 1)
globalCache.setBuiltIn(NewId(Section_Database, "template0"), 4)
globalCache.setBuiltIn(NewId(Section_Database, "postgres"), 5)
}
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
// 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 id
// This adds all of the built-in schemas to the cache.
func init() {
globalCache.setBuiltIn(NewId(Section_Namespace, "pg_catalog"), 11)
globalCache.setBuiltIn(NewId(Section_Namespace, "pg_toast"), 99)
globalCache.setBuiltIn(NewId(Section_Namespace, "public"), 2200)
globalCache.setBuiltIn(NewId(Section_Namespace, "information_schema"), 13183)
}
+188
View File
@@ -0,0 +1,188 @@
// 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 id
import "github.com/lib/pq/oid"
// This adds all of the built-in type IDs to the cache.
func init() {
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_abstime"), uint32(oid.T__abstime))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_aclitem"), uint32(oid.T__aclitem))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_bit"), uint32(oid.T__bit))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_bool"), uint32(oid.T__bool))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_box"), uint32(oid.T__box))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_bpchar"), uint32(oid.T__bpchar))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_bytea"), uint32(oid.T__bytea))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_char"), uint32(oid.T__char))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_cid"), uint32(oid.T__cid))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_cidr"), uint32(oid.T__cidr))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_circle"), uint32(oid.T__circle))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_cstring"), uint32(oid.T__cstring))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_date"), uint32(oid.T__date))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_daterange"), uint32(oid.T__daterange))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_float4"), uint32(oid.T__float4))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_float8"), uint32(oid.T__float8))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_gtsvector"), uint32(oid.T__gtsvector))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_inet"), uint32(oid.T__inet))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_int2"), uint32(oid.T__int2))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_int2vector"), uint32(oid.T__int2vector))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_int4"), uint32(oid.T__int4))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_int4range"), uint32(oid.T__int4range))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_int8"), uint32(oid.T__int8))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_int8range"), uint32(oid.T__int8range))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_interval"), uint32(oid.T__interval))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_json"), uint32(oid.T__json))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_jsonb"), uint32(oid.T__jsonb))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_line"), uint32(oid.T__line))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_lseg"), uint32(oid.T__lseg))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_macaddr"), uint32(oid.T__macaddr))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_money"), uint32(oid.T__money))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_name"), uint32(oid.T__name))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_numeric"), uint32(oid.T__numeric))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_numrange"), uint32(oid.T__numrange))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_oid"), uint32(oid.T__oid))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_oidvector"), uint32(oid.T__oidvector))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_path"), uint32(oid.T__path))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_pg_lsn"), uint32(oid.T__pg_lsn))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_point"), uint32(oid.T__point))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_polygon"), uint32(oid.T__polygon))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_record"), uint32(oid.T__record))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_refcursor"), uint32(oid.T__refcursor))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_regclass"), uint32(oid.T__regclass))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_regconfig"), uint32(oid.T__regconfig))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_regdictionary"), uint32(oid.T__regdictionary))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_regnamespace"), uint32(oid.T__regnamespace))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_regoper"), uint32(oid.T__regoper))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_regoperator"), uint32(oid.T__regoperator))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_regproc"), uint32(oid.T__regproc))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_regprocedure"), uint32(oid.T__regprocedure))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_regrole"), uint32(oid.T__regrole))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_regtype"), uint32(oid.T__regtype))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_reltime"), uint32(oid.T__reltime))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_text"), uint32(oid.T__text))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_tid"), uint32(oid.T__tid))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_time"), uint32(oid.T__time))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_timestamp"), uint32(oid.T__timestamp))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_timestamptz"), uint32(oid.T__timestamptz))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_timetz"), uint32(oid.T__timetz))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_tinterval"), uint32(oid.T__tinterval))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_tsquery"), uint32(oid.T__tsquery))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_tsrange"), uint32(oid.T__tsrange))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_tstzrange"), uint32(oid.T__tstzrange))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_tsvector"), uint32(oid.T__tsvector))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_txid_snapshot"), uint32(oid.T__txid_snapshot))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_uuid"), uint32(oid.T__uuid))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_varbit"), uint32(oid.T__varbit))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_varchar"), uint32(oid.T__varchar))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_xid"), uint32(oid.T__xid))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "_xml"), uint32(oid.T__xml))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "abstime"), uint32(oid.T_abstime))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "aclitem"), uint32(oid.T_aclitem))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "any"), uint32(oid.T_any))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "anyarray"), uint32(oid.T_anyarray))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "anyelement"), uint32(oid.T_anyelement))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "anyenum"), uint32(oid.T_anyenum))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "anynonarray"), uint32(oid.T_anynonarray))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "anyrange"), uint32(oid.T_anyrange))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "bit"), uint32(oid.T_bit))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "bool"), uint32(oid.T_bool))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "box"), uint32(oid.T_box))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "bpchar"), uint32(oid.T_bpchar))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "bytea"), uint32(oid.T_bytea))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "char"), uint32(oid.T_char))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "cid"), uint32(oid.T_cid))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "cidr"), uint32(oid.T_cidr))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "circle"), uint32(oid.T_circle))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "cstring"), uint32(oid.T_cstring))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "date"), uint32(oid.T_date))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "daterange"), uint32(oid.T_daterange))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "event_trigger"), uint32(oid.T_event_trigger))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "fdw_handler"), uint32(oid.T_fdw_handler))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "float4"), uint32(oid.T_float4))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "float8"), uint32(oid.T_float8))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "gtsvector"), uint32(oid.T_gtsvector))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "index_am_handler"), uint32(oid.T_index_am_handler))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "inet"), uint32(oid.T_inet))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "int2"), uint32(oid.T_int2))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "int2vector"), uint32(oid.T_int2vector))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "int4"), uint32(oid.T_int4))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "int4range"), uint32(oid.T_int4range))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "int8"), uint32(oid.T_int8))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "int8range"), uint32(oid.T_int8range))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "internal"), uint32(oid.T_internal))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "interval"), uint32(oid.T_interval))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "json"), uint32(oid.T_json))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "jsonb"), uint32(oid.T_jsonb))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "language_handler"), uint32(oid.T_language_handler))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "line"), uint32(oid.T_line))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "lseg"), uint32(oid.T_lseg))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "macaddr"), uint32(oid.T_macaddr))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "money"), uint32(oid.T_money))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "name"), uint32(oid.T_name))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "numeric"), uint32(oid.T_numeric))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "numrange"), uint32(oid.T_numrange))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "oid"), uint32(oid.T_oid))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "oidvector"), uint32(oid.T_oidvector))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "opaque"), uint32(oid.T_opaque))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "path"), uint32(oid.T_path))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "pg_attribute"), uint32(oid.T_pg_attribute))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "pg_auth_members"), uint32(oid.T_pg_auth_members))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "pg_authid"), uint32(oid.T_pg_authid))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "pg_class"), uint32(oid.T_pg_class))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "pg_database"), uint32(oid.T_pg_database))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "pg_ddl_command"), uint32(oid.T_pg_ddl_command))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "pg_lsn"), uint32(oid.T_pg_lsn))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "pg_node_tree"), uint32(oid.T_pg_node_tree))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "pg_proc"), uint32(oid.T_pg_proc))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "pg_shseclabel"), uint32(oid.T_pg_shseclabel))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "pg_type"), uint32(oid.T_pg_type))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "point"), uint32(oid.T_point))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "polygon"), uint32(oid.T_polygon))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "record"), uint32(oid.T_record))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "refcursor"), uint32(oid.T_refcursor))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "regclass"), uint32(oid.T_regclass))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "regconfig"), uint32(oid.T_regconfig))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "regdictionary"), uint32(oid.T_regdictionary))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "regnamespace"), uint32(oid.T_regnamespace))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "regoper"), uint32(oid.T_regoper))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "regoperator"), uint32(oid.T_regoperator))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "regproc"), uint32(oid.T_regproc))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "regprocedure"), uint32(oid.T_regprocedure))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "regrole"), uint32(oid.T_regrole))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "regtype"), uint32(oid.T_regtype))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "reltime"), uint32(oid.T_reltime))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "smgr"), uint32(oid.T_smgr))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "text"), uint32(oid.T_text))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "tid"), uint32(oid.T_tid))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "time"), uint32(oid.T_time))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "timestamp"), uint32(oid.T_timestamp))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "timestamptz"), uint32(oid.T_timestamptz))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "timetz"), uint32(oid.T_timetz))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "tinterval"), uint32(oid.T_tinterval))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "trigger"), uint32(oid.T_trigger))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "tsm_handler"), uint32(oid.T_tsm_handler))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "tsquery"), uint32(oid.T_tsquery))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "tsrange"), uint32(oid.T_tsrange))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "tstzrange"), uint32(oid.T_tstzrange))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "tsvector"), uint32(oid.T_tsvector))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "txid_snapshot"), uint32(oid.T_txid_snapshot))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "unknown"), uint32(oid.T_unknown))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "uuid"), uint32(oid.T_uuid))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "varbit"), uint32(oid.T_varbit))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "varchar"), uint32(oid.T_varchar))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "void"), uint32(oid.T_void))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "xid"), uint32(oid.T_xid))
globalCache.setBuiltIn(NewId(Section_Type, "pg_catalog", "xml"), uint32(oid.T_xml))
}
+260
View File
@@ -0,0 +1,260 @@
// 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 id
import (
"bytes"
"fmt"
"strings"
"unsafe"
)
// Id uses one of two formats. Which format is being used is marked by the upper Section bit being either 0 or 1.
// Often, an ID contains information that will commonly be accessed by the item, so the first format is tailored for
// efficient retrieval of specific segments. If an item is larger than the size limit (255, size is stored as an uint8),
// then we use the second format, which inserts a separator between items. This allows Id to hold any data in case
// the need arises in the future, but in practice we'll only see the first format (since data will usually be
// identifiers or smaller embedded IDs). Id IDs will be accessed far more often than they'll be created, hence the
// focus on efficient retrieval rather than simplicity of storage.
//
// First format (upper bit is 0):
// The first byte is the section
// The second byte contains the number of segments N (up to 255 segments)
// The next N bytes contain the length of each respective segment (up to 255 bytes)
// The remaining bytes are the original string data, stored contiguously
// Second format (upper bit is 1):
// The first byte is the section
// The remaining bytes are the original string data, stored with the separator between each segment
const (
// idSeparator marks the different data sections in an Id. This is the null byte since that byte is invalid in
// all identifiers, so we can guarantee that it's safe to use as a separator. This is used when an individual data
// segment is larger than 254 bytes.
idSeparator = "\x00"
// formatMask is the upper bit that determines whether we're using the first or second format.
formatMask = uint8(0x80)
// Null is an empty, invalid ID.
Null Id = ""
// NullAccessMethod is an empty, invalid ID. This is exactly equivalent to Null.
NullAccessMethod AccessMethod = ""
// NullCast is an empty, invalid ID. This is exactly equivalent to Null.
NullCast Cast = ""
// NullCheck is an empty, invalid ID. This is exactly equivalent to Null.
NullCheck Check = ""
// NullCollation is an empty, invalid ID. This is exactly equivalent to Null.
NullCollation Collation = ""
// NullColumnDefault is an empty, invalid ID. This is exactly equivalent to Null.
NullColumnDefault ColumnDefault = ""
// NullDatabase is an empty, invalid ID. This is exactly equivalent to Null.
NullDatabase Database = ""
// NullEnumLabel is an empty, invalid ID. This is exactly equivalent to Null.
NullEnumLabel EnumLabel = ""
// NullExtension is an empty, invalid ID. This is exactly equivalent to Null.
NullExtension Extension = ""
// NullForeignKey is an empty, invalid ID. This is exactly equivalent to Null.
NullForeignKey ForeignKey = ""
// NullFunction is an empty, invalid ID. This is exactly equivalent to Null.
NullFunction Function = ""
// NullIndex is an empty, invalid ID. This is exactly equivalent to Null.
NullIndex Index = ""
// NullNamespace is an empty, invalid ID. This is exactly equivalent to Null.
NullNamespace Namespace = ""
// NullProcedure is an empty, invalid ID. This is exactly equivalent to Null.
NullProcedure Procedure = ""
// NullSequence is an empty, invalid ID. This is exactly equivalent to Null.
NullSequence Sequence = ""
// NullTable is an empty, invalid ID. This is exactly equivalent to Null.
NullTable Table = ""
// NullTrigger is an empty, invalid ID. This is exactly equivalent to Null.
NullTrigger Trigger = ""
// NullType is an empty, invalid ID. This is exactly equivalent to Null.
NullType Type = ""
// NullView is an empty, invalid ID. This is exactly equivalent to Null.
NullView View = ""
)
// Id is an ID that is used within Doltgres. This ID is never exposed to clients through any normal means, and
// exists solely for internal operations to be able to identify specific items. This functions as an internal
// replacement for Postgres' OIDs.
type Id string
// NewId constructs an Id using the given section and data. In general, you should prefer to use the `NewIDTYPE` that
// matches the Section that's being created, and then convert that to an Id for returning or storage. You almost never
// want to call this function directly.
func NewId(section Section, data ...string) Id {
if section == Section_Null {
// It's easier if there's only one canonical way to represent a null ID, so we'll return our constant instead of
// creating a new string
return Null
}
if len(data) > 255 {
return newIdSecondFormat(section, data)
}
buf := bytes.Buffer{}
buf.WriteByte(uint8(section))
buf.WriteByte(uint8(len(data)))
for _, segment := range data {
segmentLength := len(segment)
if segmentLength > 255 {
return newIdSecondFormat(section, data)
}
buf.WriteByte(uint8(segmentLength))
}
for _, segment := range data {
buf.WriteString(segment)
}
return Id(buf.Bytes())
}
// newIdSecondFormat constructs an Id using the given section and data. This always returns the second format (using the
// separator).
func newIdSecondFormat(section Section, data []string) Id {
buf := bytes.Buffer{}
buf.WriteByte(uint8(section) | formatMask)
for i, segment := range data {
if i > 0 {
buf.WriteString(idSeparator)
}
buf.WriteString(segment)
}
return Id(buf.Bytes())
}
// IsValid returns whether the Id is valid.
func (id Id) IsValid() bool {
// We don't allow setting the section to Section_Null, so we can do a simple length check
return len(id) > 0
}
// Section returns the Section for this Id.
func (id Id) Section() Section {
if len(id) == 0 {
return Section_Null
}
return Section(id[0] & (^formatMask))
}
// Data returns the original data used to create this Id.
func (id Id) Data() []string {
if len(id) <= 1 {
return nil
}
if id[0]&formatMask == formatMask {
// Second format
return strings.Split(string(id[1:]), idSeparator)
} else {
// First format
segmentCount := int(id[1])
data := id[2+segmentCount:] // We skip 2 for the section and count bytes, then the number of segment counts
segments := make([]string, segmentCount)
start := 0
for i := 0; i < segmentCount; i++ {
length := int(id[2+i])
segments[i] = string(data[start : start+length])
start += length
}
return segments
}
}
// SegmentCount returns the number of segments that were in the original data.
func (id Id) SegmentCount() int {
if len(id) <= 1 {
return 0
}
if id[0]&formatMask == formatMask {
// Second format
return len(id.Data())
} else {
// First format
return int(id[1])
}
}
// Segment returns the segment from the given index. An empty string is returned for an index not contained by the ID.
func (id Id) Segment(index int) string {
if index < 0 || len(id) <= 1 {
return ""
}
if id[0]&formatMask == formatMask {
// Second format
data := id.Data()
if index >= len(data) {
return ""
}
return data[index]
} else {
// First format
segmentCount := int(id[1])
data := id[2+segmentCount:] // We skip 2 for the section and count bytes, then the number of segment counts
if index >= segmentCount {
return ""
}
start := 0
currentLength := 0
for i := 0; i <= index; i++ {
start += currentLength
currentLength = int(id[2+i])
}
return string(data[start : start+currentLength])
}
}
// String returns a display-suitable version of the ID. Although the ID is implemented as a string, it should not be
// treated as a string except for the purposes of storage and retrieval.
func (id Id) String() string {
data := id.Data()
if len(data) == 0 {
return fmt.Sprintf(`{%s:[]}`, id.Section().String())
}
return fmt.Sprintf(`{%s:["%s"]}`, id.Section().String(), strings.Join(data, `","`))
}
// CaseString returns a quoted string that may be used to represent this ID in a switch-case.
func (id Id) CaseString() string {
if len(id) == 0 {
return `""`
}
if id[0]&formatMask == formatMask {
// Second format
data := strings.ReplaceAll(string(id[1:]), "\x00", `\x00`)
data = strings.ReplaceAll(data, `"`, `\x22`)
return fmt.Sprintf(`"\x%02x%s"`, id[0], data)
} else {
// First format
sb := strings.Builder{}
sb.Grow(len(id) + 32)
sb.WriteRune('"')
count := int(id[1])
sb.WriteString(fmt.Sprintf(`\x%02x\x%02x`, id[0], count))
for i := 0; i < count; i++ {
sb.WriteString(fmt.Sprintf(`\x%02x`, id[2+i]))
}
sb.WriteString(strings.ReplaceAll(string(id[2+count:]), `"`, `\x22`))
sb.WriteRune('"')
return sb.String()
}
}
// UnderlyingBytes returns the underlying bytes for the ID. These must not be modified, as this is intended solely for
// efficient usage of operations that require byte slices.
func (id Id) UnderlyingBytes() []byte {
return unsafe.Slice(unsafe.StringData(string(id)), len(id))
}
// usesSecondFormat returns whether the separator is used, which is the second format.
func (id Id) usesSecondFormat() bool {
return len(id) > 0 && id[0]&formatMask == formatMask
}
+61
View File
@@ -0,0 +1,61 @@
// 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 id
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
const longString1 = `
0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789`
func TestInternal(t *testing.T) {
tests := []struct {
section Section
data []string
}{
{Section_Table, []string{"exampleschema", "exampletable"}},
{Section_Table, []string{"random$string", longString1, "bogus_data"}},
{Section_Type, []string{`best "type" ever`, "worst type ever?"}},
}
for testIdx, test := range tests {
t.Run(fmt.Sprintf("%d", testIdx), func(t *testing.T) {
id := NewId(test.section, test.data...)
for {
require.True(t, id.IsValid())
require.Equal(t, test.section, id.Section())
data := id.Data()
require.Len(t, data, len(test.data))
for i := range data {
require.Equal(t, test.data[i], data[i])
require.Equal(t, test.data[i], id.Segment(i))
}
// If this is using the first format, then we'll rerun the test using a variant forced to the second format
if !id.usesSecondFormat() {
id = newIdSecondFormat(test.section, test.data)
continue
}
break
}
})
}
}
+587
View File
@@ -0,0 +1,587 @@
// Copyright 2025 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 id
import (
"fmt"
"strconv"
"strings"
)
// AccessMethod is an Id wrapper for access methods. This wrapper must not be returned to the client.
type AccessMethod Id
// Cast is an Id wrapper for casts. This wrapper must not be returned to the client.
type Cast Id
// Check is an Id wrapper for checks. This wrapper must not be returned to the client.
type Check Id
// Collation is an Id wrapper for collations. This wrapper must not be returned to the client.
type Collation Id
// ColumnDefault is an Id wrapper for column defaults. This wrapper must not be returned to the client.
type ColumnDefault Id
// Database is an Id wrapper for databases. This wrapper must not be returned to the client.
type Database Id
// EnumLabel is an Id wrapper for enum labels. This wrapper must not be returned to the client.
type EnumLabel Id
// Extension is an Id wrapper for extensions. This wrapper must not be returned to the client.
type Extension Id
// ForeignKey is an Id wrapper for foreign keys. This wrapper must not be returned to the client.
type ForeignKey Id
// Function is an Id wrapper for functions. This wrapper must not be returned to the client.
type Function Id
// Index is an Id wrapper for indexes. This wrapper must not be returned to the client.
type Index Id
// Namespace is an Id wrapper for schemas/namespaces. This wrapper must not be returned to the client.
type Namespace Id
// Oid is an Id wrapper for OIDs. This wrapper must not be returned to the client.
type Oid Id
// Procedure is an Id wrapper for procedures. This wrapper must not be returned to the client.
type Procedure Id
// Sequence is an Id wrapper for sequences. This wrapper must not be returned to the client.
type Sequence Id
// Table is an Id wrapper for tables. This wrapper must not be returned to the client.
type Table Id
// Trigger is an Id wrapper for triggers. This wrapper must not be returned to the client.
type Trigger Id
// Type is an Id wrapper for types. This wrapper must not be returned to the client.
type Type Id
// View is an Id wrapper for views. This wrapper must not be returned to the client.
type View Id
// NewAccessMethod returns a new AccessMethod. This wrapper must not be returned to the client.
func NewAccessMethod(methodName string) AccessMethod {
if len(methodName) == 0 {
return NullAccessMethod
}
return AccessMethod(NewId(Section_AccessMethod, methodName))
}
// NewCast returns a new Cast. This wrapper must not be returned to the client.
func NewCast(sourceType Type, targetType Type) Cast {
if len(sourceType) == 0 && len(targetType) == 0 {
return NullCast
}
return Cast(NewId(Section_Cast, string(sourceType), string(targetType)))
}
// NewCheck returns a new Check. This wrapper must not be returned to the client.
func NewCheck(schemaName string, tableName string, checkName string) Check {
if len(schemaName) == 0 && len(tableName) == 0 && len(checkName) == 0 {
return NullCheck
}
return Check(NewId(Section_Check, schemaName, tableName, checkName))
}
// NewCollation returns a new Collation. This wrapper must not be returned to the client.
func NewCollation(schemaName string, collationName string) Collation {
if len(schemaName) == 0 && len(collationName) == 0 {
return NullCollation
}
return Collation(NewId(Section_Collation, schemaName, collationName))
}
// NewColumnDefault returns a new ColumnDefault. This wrapper must not be returned to the client.
func NewColumnDefault(schemaName string, tableName string, columnName string) ColumnDefault {
if len(schemaName) == 0 && len(tableName) == 0 && len(columnName) == 0 {
return NullColumnDefault
}
return ColumnDefault(NewId(Section_ColumnDefault, schemaName, tableName, columnName))
}
// NewDatabase returns a new Database. This wrapper must not be returned to the client.
func NewDatabase(dbName string) Database {
if len(dbName) == 0 {
return NullDatabase
}
return Database(NewId(Section_Database, dbName))
}
// NewEnumLabel returns a new EnumLabel. This wrapper must not be returned to the client.
func NewEnumLabel(parent Type, label string) EnumLabel {
if len(parent) == 0 && len(label) == 0 {
return NullEnumLabel
}
return EnumLabel(NewId(Section_EnumLabel, string(parent), label))
}
// NewExtension returns a new Extension. This wrapper must not be returned to the client.
func NewExtension(name string) Extension {
if len(name) == 0 {
return NullExtension
}
return Extension(NewId(Section_Extension, name))
}
// NewForeignKey returns a new ForeignKey. This wrapper must not be returned to the client.
func NewForeignKey(schemaName string, tableName string, fkName string) ForeignKey {
if len(schemaName) == 0 && len(tableName) == 0 && len(fkName) == 0 {
return NullForeignKey
}
return ForeignKey(NewId(Section_ForeignKey, schemaName, tableName, fkName))
}
// NewFunction returns a new Function. This wrapper must not be returned to the client.
func NewFunction(schemaName string, funcName string, params ...Type) Function {
if len(schemaName) == 0 && len(funcName) == 0 && len(params) == 0 {
return NullFunction
}
data := make([]string, len(params)+2)
data[0] = schemaName
data[1] = funcName
for i := range params {
data[2+i] = string(params[i])
}
return Function(NewId(Section_Function, data...))
}
// NewIndex returns a new Index. This wrapper must not be returned to the client.
func NewIndex(schemaName string, tableName string, indexName string) Index {
if len(schemaName) == 0 && len(tableName) == 0 && len(indexName) == 0 {
return NullIndex
}
return Index(NewId(Section_Index, schemaName, tableName, indexName))
}
// NewNamespace returns a new Namespace. This wrapper must not be returned to the client.
func NewNamespace(schemaName string) Namespace {
if len(schemaName) == 0 {
return NullNamespace
}
return Namespace(NewId(Section_Namespace, schemaName))
}
// NewOID returns a new Oid. This wrapper must not be returned to the client.
func NewOID(val uint32) Oid {
return Oid(NewId(Section_OID, strconv.FormatUint(uint64(val), 10)))
}
// NewProcedure returns a new Procedure. This wrapper must not be returned to the client.
func NewProcedure(schemaName string, procName string, params ...Type) Procedure {
if len(schemaName) == 0 && len(procName) == 0 && len(params) == 0 {
return NullProcedure
}
data := make([]string, len(params)+2)
data[0] = schemaName
data[1] = procName
for i := range params {
data[2+i] = string(params[i])
}
return Procedure(NewId(Section_Procedure, data...))
}
// NewSequence returns a new Sequence. This wrapper must not be returned to the client.
func NewSequence(schemaName string, sequenceName string) Sequence {
if len(schemaName) == 0 && len(sequenceName) == 0 {
return NullSequence
}
return Sequence(NewId(Section_Sequence, schemaName, sequenceName))
}
// NewTable returns a new Table. This wrapper must not be returned to the client.
func NewTable(schemaName string, tableName string) Table {
if len(schemaName) == 0 && len(tableName) == 0 {
return NullTable
}
return Table(NewId(Section_Table, schemaName, tableName))
}
// NewTrigger returns a new Trigger. This wrapper must not be returned to the client.
func NewTrigger(schemaName string, tableName string, triggerName string) Trigger {
if len(schemaName) == 0 && len(tableName) == 0 && len(triggerName) == 0 {
return NullTrigger
}
return Trigger(NewId(Section_Trigger, schemaName, tableName, triggerName))
}
// NewType returns a new Type. This wrapper must not be returned to the client.
func NewType(schemaName string, typeName string) Type {
if len(schemaName) == 0 && len(typeName) == 0 {
return NullType
}
return Type(NewId(Section_Type, schemaName, typeName))
}
// NewView returns a new View. This wrapper must not be returned to the client.
func NewView(schemaName string, viewName string) View {
if len(schemaName) == 0 && len(viewName) == 0 {
return NullView
}
return View(NewId(Section_View, schemaName, viewName))
}
// MethodName returns the method's name.
func (id AccessMethod) MethodName() string {
return Id(id).Segment(0)
}
// SourceType returns the source type.
func (id Cast) SourceType() Type {
return Type(Id(id).Segment(0))
}
// TargetType returns the target type.
func (id Cast) TargetType() Type {
return Type(Id(id).Segment(1))
}
// CheckName returns the check's name.
func (id Check) CheckName() string {
return Id(id).Segment(2)
}
// SchemaName returns the schema name of the check.
func (id Check) SchemaName() string {
return Id(id).Segment(0)
}
// TableName returns the name of the table that the check belongs to.
func (id Check) TableName() string {
return Id(id).Segment(1)
}
// CollationName returns the collation's name.
func (id Collation) CollationName() string {
return Id(id).Segment(1)
}
// SchemaName returns the schema name of the collation.
func (id Collation) SchemaName() string {
return Id(id).Segment(0)
}
// ColumnName returns the column's name that the default belongs to.
func (id ColumnDefault) ColumnName() string {
return Id(id).Segment(2)
}
// SchemaName returns the schema name of the column default.
func (id ColumnDefault) SchemaName() string {
return Id(id).Segment(0)
}
// TableName returns the name of the table that the column belongs to.
func (id ColumnDefault) TableName() string {
return Id(id).Segment(1)
}
// DatabaseName returns the database's name.
func (id Database) DatabaseName() string {
return Id(id).Segment(0)
}
// Parent returns the parent ENUM for the label.
func (id EnumLabel) Parent() Type {
return Type(Id(id).Segment(0))
}
// Label returns the name of the label.
func (id EnumLabel) Label() string {
return Id(id).Segment(1)
}
// Name returns the name of the extension.
func (id Extension) Name() string {
return Id(id).Segment(0)
}
// ForeignKeyName returns the foreign key's name.
func (id ForeignKey) ForeignKeyName() string {
return Id(id).Segment(2)
}
// SchemaName returns the schema name of the foreign key.
func (id ForeignKey) SchemaName() string {
return Id(id).Segment(0)
}
// TableName returns the name of the table that the foreign key belongs to.
func (id ForeignKey) TableName() string {
return Id(id).Segment(1)
}
// DisplayString returns the function ID as a suitable display name.
// For example, the output will generally look like: "func_name(param1, param2)"
func (id Function) DisplayString() string {
if !id.IsValid() {
return ""
}
params := make([]string, id.ParameterCount())
for i, paramID := range id.Parameters() {
params[i] = paramID.TypeName()
}
return fmt.Sprintf("%s(%s)", id.FunctionName(), strings.Join(params, ", "))
}
// FunctionName returns the function's name.
func (id Function) FunctionName() string {
return Id(id).Segment(1)
}
// Parameters returns the function's name.
func (id Function) Parameters() []Type {
data := Id(id).Data()[2:]
params := make([]Type, len(data))
for i := range data {
params[i] = Type(data[i])
}
return params
}
// ParameterCount returns the function's name.
func (id Function) ParameterCount() int {
return Id(id).SegmentCount() - 2
}
// SchemaName returns the schema name of the function.
func (id Function) SchemaName() string {
return Id(id).Segment(0)
}
// IndexName returns the index's name.
func (id Index) IndexName() string {
return Id(id).Segment(2)
}
// SchemaName returns the schema name of the index.
func (id Index) SchemaName() string {
return Id(id).Segment(0)
}
// TableName returns the name of the table that the index belongs to.
func (id Index) TableName() string {
return Id(id).Segment(1)
}
// SchemaName returns the schema name.
func (id Namespace) SchemaName() string {
return Id(id).Segment(0)
}
// OID returns the contained uint32 value.
func (id Oid) OID() uint32 {
val, _ := strconv.ParseUint(Id(id).Segment(0), 10, 32)
return uint32(val)
}
// ProcedureName returns the procedure's name.
func (id Procedure) ProcedureName() string {
return Id(id).Segment(1)
}
// Parameters returns the procedure's parameters.
func (id Procedure) Parameters() []Type {
data := Id(id).Data()[2:]
params := make([]Type, len(data))
for i := range data {
params[i] = Type(data[i])
}
return params
}
// ParameterCount returns the procedure's parameter count.
func (id Procedure) ParameterCount() int {
return Id(id).SegmentCount() - 2
}
// SchemaName returns the schema name of the procedure.
func (id Procedure) SchemaName() string {
return Id(id).Segment(0)
}
// SchemaName returns the schema name of the sequence.
func (id Sequence) SchemaName() string {
return Id(id).Segment(0)
}
// SequenceName returns the name of the sequence.
func (id Sequence) SequenceName() string {
return Id(id).Segment(1)
}
// SchemaName returns the schema name of the table.
func (id Table) SchemaName() string {
return Id(id).Segment(0)
}
// TableName returns the table's name.
func (id Table) TableName() string {
return Id(id).Segment(1)
}
// SchemaName returns the schema name of the trigger.
func (id Trigger) SchemaName() string {
return Id(id).Segment(0)
}
// TableName returns the name of the table that the trigger belongs to.
func (id Trigger) TableName() string {
return Id(id).Segment(1)
}
// TriggerName returns the trigger's name.
func (id Trigger) TriggerName() string {
return Id(id).Segment(2)
}
// SchemaName returns the schema name of the type.
func (id Type) SchemaName() string {
return Id(id).Segment(0)
}
// TypeName returns the type's name.
func (id Type) TypeName() string {
return Id(id).Segment(1)
}
// SchemaName returns the schema name of the view.
func (id View) SchemaName() string {
return Id(id).Segment(0)
}
// ViewName returns the view's name.
func (id View) ViewName() string {
return Id(id).Segment(1)
}
// IsValid returns whether the ID is valid.
func (id AccessMethod) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id Cast) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id Check) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id Collation) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id ColumnDefault) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id Database) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id EnumLabel) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id Extension) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id ForeignKey) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id Function) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id Index) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id Namespace) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id Oid) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id Procedure) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id Sequence) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id Table) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id Trigger) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id Type) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id View) IsValid() bool { return Id(id).IsValid() }
// AsId returns the unwrapped ID.
func (id AccessMethod) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id Cast) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id Check) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id Collation) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id ColumnDefault) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id Database) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id EnumLabel) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id Extension) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id ForeignKey) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id Function) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id Index) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id Namespace) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id Oid) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id Procedure) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id Sequence) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id Table) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id Trigger) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id Type) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id View) AsId() Id { return Id(id) }
+103
View File
@@ -0,0 +1,103 @@
// Copyright 2025 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 id
import "github.com/dolthub/go-mysql-server/sql"
// Operation represents an operation that is being performed or validated.
type Operation uint8
const (
Operation_Rename Operation = iota
Operation_Delete
Operation_Delete_Cascade
)
// registry is the implementation of the global registry. This holds all functions that operate or validate a change on
// an ID.
type registry struct {
listeners [][]Listener
}
// globalRegistry is the variable that is referenced for the registry.
var globalRegistry = &registry{
listeners: make([][]Listener, section_count),
}
type Listener interface {
// OperationPerformer is a function that performs the given operation on the original ID. Some operations, such as
// renames, will use the new ID.
OperationPerformer(ctx *sql.Context, operation Operation, databaseName string, originalID Id, newID Id) error
// OperationValidator is a function that validates the given operation on the original ID. Some operations, such as
// renames, will use the new ID. A validator is not required, and is intended for operations that may be relatively
// expensive to perform, but able to check quickly for failures. In addition, validators should not perform any
// modifications. If a validator is not required, then this should just return nil.
OperationValidator(ctx *sql.Context, operation Operation, databaseName string, originalID Id, newID Id) error
}
// RegisterListener registers the given listener for the given sections.
//
// For example, sequences are related to tables. Whenever a table operation is performed that changes its ID, sequences
// will also need to update their IDs that reference the table. This is accomplished by registering a performer that
// accepts a table section, where the performer modifies sequences as needed.
//
// Performers should not register sections that are directly related to themselves. For example, a sequence performer
// should not register itself under the sequence section, as it will be the one broadcasting that section, and therefore
// could cause a loop.
func RegisterListener(listener Listener, sections ...Section) {
for _, section := range sections {
if section == Section_Null {
continue
}
globalRegistry.listeners[section] = append(globalRegistry.listeners[section], listener)
}
}
// PerformOperation calls all registered performers that are associated with the given section. This does not call any
// validators, which should be done using ValidateOperation. This returns the first error that is encountered.
func PerformOperation(ctx *sql.Context, targetSection Section, operation Operation, databaseName string, originalID Id, newID Id) error {
for _, listener := range globalRegistry.listeners[targetSection] {
if err := listener.OperationPerformer(ctx, operation, databaseName, originalID, newID); err != nil {
return err
}
}
// TODO: need to look for tables that store OIDs in their columns and UPDATE them to the new value
// it will be relatively slow, but that's the price a user pays to store OIDs in their tables
return nil
}
// ValidateOperation calls all registered validators that are associated with the given section.
func ValidateOperation(ctx *sql.Context, targetSection Section, operation Operation, databaseName string, originalID Id, newID Id) error {
for _, listener := range globalRegistry.listeners[targetSection] {
if err := listener.OperationValidator(ctx, operation, databaseName, originalID, newID); err != nil {
return err
}
}
return nil
}
// String returns the name of the operation.
func (op Operation) String() string {
switch op {
case Operation_Rename:
return "Rename"
case Operation_Delete:
return "Delete"
case Operation_Delete_Cascade:
return "DeleteCascade"
default:
return "UNKNOWN_OPERATION"
}
}
+152
View File
@@ -0,0 +1,152 @@
// 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 id
// Section represents a specific space that an Internal ID resides in. This makes it relatively simple to find the
// target of the ID, since each searchable space has its own section.
type Section uint8
// All new sections must be given an unused number, as these may be persisted in tables. Changing them would change
// pre-existing table data, potentially corrupting that table data. At most, there can be 127 sections, since the first
// bit is reserved to determine a format's encoding.
const (
Section_Null Section = 0 // Represents a null ID
Section_AccessMethod Section = 1 // Refers to relation access methods
Section_Cast Section = 2 // Refers to casts between types
Section_Check Section = 3 // Refers to checks on tables
Section_Collation Section = 4 // Refers to collations
Section_ColumnDefault Section = 5 // Refers to column defaults on tables
Section_Database Section = 6 // Refers to the database
Section_EnumLabel Section = 7 // Refers to a specific label in an ENUM type
Section_EventTrigger Section = 8 // Refers to event triggers
Section_ExclusionConstraint Section = 9 // Refers to exclusion constraints
Section_Extension Section = 10 // Refers to extensions
Section_ForeignKey Section = 11 // Refers to foreign keys on tables
Section_ForeignDataWrapper Section = 12 // Refers to foreign data wrappers
Section_ForeignServer Section = 13 // Refers to foreign servers
Section_ForeignTable Section = 14 // Refers to foreign tables
Section_Function Section = 15 // Refers to functions
Section_FunctionLanguage Section = 16 // Refers to the programming languages available for writing functions
Section_Index Section = 17 // Refers to indexes on tables
Section_Namespace Section = 18 // Namespaces are the underlying structure of a schema (basically the schema)
Section_OID Section = 19 // Refers to a raw OID that is not actually attached to anything (ONLY used with reg types)
Section_Operator Section = 20 // Refers to operators (+, -, *, etc.)
Section_OperatorClass Section = 21 // Refers to operator classes
Section_OperatorFamily Section = 22 // Refers to operator families
Section_PrimaryKey Section = 23 // Refers to primary keys on tables
Section_Procedure Section = 24 // Refers to stored procedures
Section_Publication Section = 25 // Refers to publications
Section_RowLevelSecurity Section = 26 // Refers to row-level security polices on tables
Section_Sequence Section = 27 // Refers to sequences
Section_Subscription Section = 28 // Refers to logical replication subscriptions
Section_Table Section = 29 // Refers to tables
Section_TextSearchConfig Section = 30 // Refers to text search configuration
Section_TextSearchDictionary Section = 31 // Refers to text search dictionaries
Section_TextSearchParser Section = 32 // Refers to text search parsers
Section_TextSearchTemplate Section = 33 // Refers to text search templates
Section_Trigger Section = 34 // Refers to triggers on tables and views
Section_Type Section = 35 // Refers to types
Section_UniqueKey Section = 36 // Refers to unique keys on tables
Section_User Section = 37 // Refers to users
Section_View Section = 38 // Refers to views
section_count uint8 = 39 // This is the number of sections, and should ALWAYS be kept up-to-date
)
// String returns the name of the Section.
func (section Section) String() string {
switch section {
case Section_Null:
return "Null"
case Section_AccessMethod:
return "AccessMethod"
case Section_Cast:
return "Cast"
case Section_Check:
return "Check"
case Section_Collation:
return "Collation"
case Section_ColumnDefault:
return "ColumnDefault"
case Section_Database:
return "Database"
case Section_EnumLabel:
return "EnumLabel"
case Section_EventTrigger:
return "EventTrigger"
case Section_ExclusionConstraint:
return "ExclusionConstraint"
case Section_Extension:
return "Extension"
case Section_ForeignKey:
return "ForeignKey"
case Section_ForeignDataWrapper:
return "ForeignDataWrapper"
case Section_ForeignServer:
return "ForeignServer"
case Section_ForeignTable:
return "ForeignTable"
case Section_Function:
return "Function"
case Section_FunctionLanguage:
return "FunctionLanguage"
case Section_Index:
return "Index"
case Section_Namespace:
return "Namespace"
case Section_OID:
return "OID"
case Section_Operator:
return "Operator"
case Section_OperatorClass:
return "OperatorClass:"
case Section_OperatorFamily:
return "OperatorFamily"
case Section_PrimaryKey:
return "PrimaryKey"
case Section_Procedure:
return "Procedure"
case Section_Publication:
return "Publication"
case Section_RowLevelSecurity:
return "RowLevelSecurity"
case Section_Sequence:
return "Sequence"
case Section_Subscription:
return "Subscription"
case Section_Table:
return "Table"
case Section_TextSearchConfig:
return "TextSearchConfig"
case Section_TextSearchDictionary:
return "TextSearchDictionary"
case Section_TextSearchParser:
return "TextSearchParser"
case Section_TextSearchTemplate:
return "TextSearchTemplate"
case Section_Trigger:
return "Trigger"
case Section_Type:
return "Type"
case Section_UniqueKey:
return "UniqueKey"
case Section_User:
return "User"
case Section_View:
return "View"
default:
return "UNKNOWN_SECTION"
}
}
+81
View File
@@ -0,0 +1,81 @@
// Copyright 2025 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 id
import "testing"
// TestSectionValue operates as a line of defense to prevent accidental changes to pre-existing Section IDs.
// If this test fails, then a Section was changed that should not have been changed.
func TestSectionValue(t *testing.T) {
ids := []struct {
Section
ID uint8
Name string
}{
{Section_Null, 0, "Null"},
{Section_AccessMethod, 1, "AccessMethod"},
{Section_Cast, 2, "Cast"},
{Section_Check, 3, "Check"},
{Section_Collation, 4, "Collation"},
{Section_ColumnDefault, 5, "ColumnDefault"},
{Section_Database, 6, "Database"},
{Section_EnumLabel, 7, "EnumLabel"},
{Section_EventTrigger, 8, "EventTrigger"},
{Section_ExclusionConstraint, 9, "ExclusionConstraint"},
{Section_Extension, 10, "Extension"},
{Section_ForeignKey, 11, "ForeignKey"},
{Section_ForeignDataWrapper, 12, "ForeignDataWrapper"},
{Section_ForeignServer, 13, "ForeignServer"},
{Section_ForeignTable, 14, "ForeignTable"},
{Section_Function, 15, "Function"},
{Section_FunctionLanguage, 16, "FunctionLanguage"},
{Section_Index, 17, "Index"},
{Section_Namespace, 18, "Namespace"},
{Section_OID, 19, "OID"},
{Section_Operator, 20, "Operator"},
{Section_OperatorClass, 21, "OperatorClass"},
{Section_OperatorFamily, 22, "OperatorFamily"},
{Section_PrimaryKey, 23, "PrimaryKey"},
{Section_Procedure, 24, "Procedure"},
{Section_Publication, 25, "Publication"},
{Section_RowLevelSecurity, 26, "RowLevelSecurity"},
{Section_Sequence, 27, "Sequence"},
{Section_Subscription, 28, "Subscription"},
{Section_Table, 29, "Table"},
{Section_TextSearchConfig, 30, "TextSearchConfig"},
{Section_TextSearchDictionary, 31, "TextSearchDictionary"},
{Section_TextSearchParser, 32, "TextSearchParser"},
{Section_TextSearchTemplate, 33, "TextSearchTemplate"},
{Section_Trigger, 34, "Trigger"},
{Section_Type, 35, "Type"},
{Section_UniqueKey, 36, "UniqueKey"},
{Section_User, 37, "User"},
{Section_View, 38, "View"},
}
allIds := make(map[uint8]string)
for _, id := range ids {
if uint8(id.Section) != id.ID {
t.Logf("Section `%s` has been changed from its permanent value of `%d` to `%d`",
id.Name, id.ID, uint8(id.Section))
t.Fail()
} else if existingName, ok := allIds[id.ID]; ok {
t.Logf("Section `%s` has the same value as `%s`: `%d`",
id.Name, existingName, id.ID)
t.Fail()
} else {
allIds[id.ID] = id.Name
}
}
}
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2025 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 id
import (
"github.com/cockroachdb/errors"
"github.com/dolthub/go-mysql-server/sql"
)
// GetFromTable returns the id from the table.
func GetFromTable(ctx *sql.Context, tbl sql.Table) (Table, bool, error) {
schTbl, ok := tbl.(sql.DatabaseSchemaTable)
if !ok {
return NullTable, false, errors.Newf(`table "%s" does not specify a schema`, tbl.Name())
}
return NewTable(schTbl.DatabaseSchema().SchemaName(), schTbl.Name()), true, nil
}
+31
View File
@@ -0,0 +1,31 @@
// 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 core
// IsValidPostgresIdentifier returns true according to Postgres quoted identifier rules.
// Quoted identifiers can contain any character except the null character (code zero),
// including supplementary Unicode (emoji, code points above U+FFFF) unlike MySQL.
// https://www.postgresql.org/docs/current/sql-syntax-lexical.html
func IsValidPostgresIdentifier(name string) bool {
if len(name) == 0 {
return false
}
for _, c := range name {
if c == 0x0000 {
return false
}
}
return true
}
+50
View File
@@ -0,0 +1,50 @@
// 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 core
import (
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/store/types"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core/conflicts"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/typecollection"
"github.com/dolthub/doltgresql/server/plpgsql"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// Init initializes this package.
func Init() {
doltdb.EmptyRootValue = emptyRootValue
doltdb.NewRootValue = newRootValue
types.DoltgresRootValueHumanReadableStringAtIndentationLevel = rootValueHumanReadableStringAtIndentationLevel
types.DoltgresRootValueWalkAddrs = rootValueWalkAddrs
conflicts.ClearContextValues = ClearContextValues
plpgsql.GetTypesCollectionFromContext = GetTypesCollectionFromContext
id.RegisterListener(sequenceIDListener{}, id.Section_Table)
typecollection.GetSqlTableFromContext = GetSqlTableFromContext
typecollection.GetSchemaName = GetSchemaName
pgtypes.GetTypesCollectionFromContext = func(ctx *sql.Context, database string) (pgtypes.TypeCollection, error) {
return GetTypesCollectionFromContext(ctx, database)
}
pgtypes.GetAssignmentCast = func(ctx *sql.Context, sourceType *pgtypes.DoltgresType, targetType *pgtypes.DoltgresType) (pgtypes.Cast, error) {
castsColl, err := GetCastsCollectionFromContext(ctx, "")
if err != nil {
return nil, err
}
return castsColl.GetAssignmentCast(ctx, sourceType, targetType)
}
}
+111
View File
@@ -0,0 +1,111 @@
// Copyright 2025 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 merge
import (
"context"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/merge"
)
// ResolveMergeValues is a way to handle merging between "our" value and "their" value. This will always take the
// changed value if one side has changed from the "ancestor" while the other has not. If both have changed (or the
// ancestor does not exist), then this defers to a custom resolution function. This function is only called when both
// "our" and "their" values have changed from the ancestor.
func ResolveMergeValues[T comparable](ourVal, theirVal, ancVal T, hasAncestorValue bool, customResolve func(T, T) T) T {
if hasAncestorValue {
if ourVal == ancVal {
return theirVal
}
if theirVal == ancVal {
return ourVal
}
}
if ourVal == theirVal {
return ourVal
}
return customResolve(ourVal, theirVal)
}
// ResolveMergeValuesVariadic is the same as ResolveMergeValues, except that it will take a variadic custom resolution
// function. This is primarily for values that will use one of the variadic utility functions (Min, Max, etc.) as it
// will always receive two inputs. If Go expands how functions interact with generics, then this function can be removed.
func ResolveMergeValuesVariadic[T comparable](ourVal, theirVal, ancVal T, hasAncestorValue bool, customResolve func(...T) T) T {
return ResolveMergeValues(ourVal, theirVal, ancVal, hasAncestorValue, func(t1, t2 T) T {
return customResolve(t1, t2)
})
}
// DiffValues handles common comparisons for diffs. This makes an assumption that "our" and "their" values are always
// valid. Returns true when the diff represents a conflict. When false is returned, the diff's "our" value contains the
// merged value.
func DiffValues[T comparable](diff *objinterface.RootObjectDiff, ourVal, theirVal, ancVal T, hasAncestorValue bool) bool {
return DiffValuesFunc(diff, ourVal, theirVal, ancVal, hasAncestorValue, func(v1 T, v2 T) bool {
return v1 == v2
})
}
// DiffValuesFunc is the same as DiffValues, except that this handles values that are not trivially comparable.
func DiffValuesFunc[T any](diff *objinterface.RootObjectDiff, ourVal, theirVal, ancVal T, hasAncestorValue bool, equals func(T, T) bool) bool {
// Each check is ordered such that the successive checks rely on the failure of the previous checks
if equals(ourVal, theirVal) {
diff.OurValue = ourVal
diff.TheirValue = theirVal
diff.AncestorValue = nil
diff.OurChange = objinterface.RootObjectDiffChange_NoChange
diff.TheirChange = objinterface.RootObjectDiffChange_NoChange
return false
}
if !hasAncestorValue {
diff.OurValue = ourVal
diff.TheirValue = theirVal
diff.AncestorValue = nil
diff.OurChange = objinterface.RootObjectDiffChange_Added
diff.TheirChange = objinterface.RootObjectDiffChange_Added
return true
}
if equals(ourVal, ancVal) {
diff.OurValue = theirVal
diff.TheirValue = theirVal
diff.AncestorValue = ancVal
diff.OurChange = objinterface.RootObjectDiffChange_NoChange
diff.TheirChange = objinterface.RootObjectDiffChange_Modified
return false
}
if equals(theirVal, ancVal) {
diff.OurValue = ourVal
diff.TheirValue = ourVal
diff.AncestorValue = ancVal
diff.OurChange = objinterface.RootObjectDiffChange_Modified
diff.TheirChange = objinterface.RootObjectDiffChange_NoChange
return false
}
diff.OurValue = ourVal
diff.TheirValue = theirVal
diff.AncestorValue = ancVal
diff.OurChange = objinterface.RootObjectDiffChange_Modified
diff.TheirChange = objinterface.RootObjectDiffChange_Modified
return true
}
// CreateConflict handles conflict creation and is declared in a different package. It is assigned here by an Init
// function to get around import cycles.
var CreateConflict = func(ctx context.Context, rightSrc doltdb.Rootish, ours doltdb.RootObject, theirs doltdb.RootObject, ancestor doltdb.RootObject) (doltdb.RootObject, *merge.MergeStats, error) {
return nil, nil, errors.New("CreateConflict was never initialized")
}
+134
View File
@@ -0,0 +1,134 @@
// 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 core
import (
"context"
"fmt"
"strings"
doltserial "github.com/dolthub/dolt/go/gen/fb/serial"
"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/dolt/go/store/types"
flatbuffers "github.com/dolthub/flatbuffers/v23/go"
"github.com/dolthub/doltgresql/core/storage"
"github.com/dolthub/doltgresql/flatbuffers/gen/serial"
)
// emptyRootValue is Doltgres' implementation of doltdb.EmptyRootValue.
func emptyRootValue(ctx context.Context, vrw types.ValueReadWriter, ns tree.NodeStore) (doltdb.RootValue, error) {
builder := flatbuffers.NewBuilder(80)
emptyam, err := prolly.NewEmptyAddressMap(ns)
if err != nil {
return nil, err
}
ambytes := []byte(tree.ValueFromNode(emptyam.Node()).(types.SerialMessage))
tablesoff := builder.CreateByteVector(ambytes)
var empty hash.Hash
fkoff := builder.CreateByteVector(empty[:])
serial.RootValueStart(builder)
serial.RootValueAddFeatureVersion(builder, int64(DoltgresFeatureVersion))
serial.RootValueAddCollation(builder, serial.Collationutf8mb4_0900_bin)
serial.RootValueAddTables(builder, tablesoff)
serial.RootValueAddForeignKeyAddr(builder, fkoff)
bs := doltserial.FinishMessage(builder, serial.RootValueEnd(builder), []byte(doltserial.DoltgresRootValueFileID))
return newRootValue(ctx, vrw, ns, types.SerialMessage(bs))
}
// newRootValue is Doltgres' implementation of doltdb.NewRootValue.
func newRootValue(ctx context.Context, vrw types.ValueReadWriter, ns tree.NodeStore, v types.Value) (doltdb.RootValue, error) {
var st storage.RootStorage
srv, err := serial.TryGetRootAsRootValue([]byte(v.(types.SerialMessage)), doltserial.MessagePrefixSz)
if err != nil {
return nil, err
}
st = storage.RootStorage{SRV: srv}
ver := st.GetFeatureVersion()
if DoltgresFeatureVersion < ver {
return nil, doltdb.ErrClientOutOfDate{
ClientVer: DoltgresFeatureVersion,
RepoVer: ver,
}
}
return &RootValue{
vrw: vrw,
ns: ns,
st: st,
}, nil
}
// rootValueHumanReadableStringAtIndentationLevel is Doltgres' implementation of
// types.DoltgresRootValueHumanReadableStringAtIndentationLevel.
func rootValueHumanReadableStringAtIndentationLevel(sm types.SerialMessage, level int) string {
msg, _ := serial.TryGetRootAsRootValue(sm, doltserial.MessagePrefixSz)
ret := &strings.Builder{}
printWithIndendationLevel(level, ret, "{\n")
printWithIndendationLevel(level, ret, "\tFeatureVersion: %d\n", msg.FeatureVersion())
printWithIndendationLevel(level, ret, "\tForeignKeys: #%s\n", hash.New(msg.ForeignKeyAddrBytes()).String())
printWithIndendationLevel(level, ret, "\tTables: %s\n",
types.SerialMessage(msg.TablesBytes()).HumanReadableStringAtIndentationLevel(level+1))
printWithIndendationLevel(level, ret, "}")
return ret.String()
}
// rootValueWalkAddrs is Doltgres' implementation of types.DoltgresRootValueWalkAddrs.
func rootValueWalkAddrs(sm types.SerialMessage, cb func(addr hash.Hash) error) error {
var msg serial.RootValue
err := serial.InitRootValueRoot(&msg, []byte(sm), doltserial.MessagePrefixSz)
if err != nil {
return err
}
err = types.SerialMessage(msg.TablesBytes()).WalkAddrs(types.Format_DOLT, cb)
if err != nil {
return err
}
addr := hash.New(msg.ForeignKeyAddrBytes())
if !addr.IsEmpty() {
if err = cb(addr); err != nil {
return err
}
}
// Walk all Doltgres root object fields (sequences, types, functions, procedures, triggers, extensions, casts).
// Each field stores a 20-byte hash pointing to a prolly address map in the chunk store. Without walking these,
// backup/restore (SyncRoots/PullChunks) will not copy the root object chunks and they will be absent after restore.
for _, ros := range storage.RootObjectSerializations {
addrBytes := ros.Bytes(&msg)
if len(addrBytes) == 0 {
continue
}
objAddr := hash.New(addrBytes)
if !objAddr.IsEmpty() {
if err = cb(objAddr); err != nil {
return err
}
}
}
return nil
}
// printWithIndendationLevel is a helper for rootValueHumanReadableStringAtIndentationLevel to print at the given
// indentation level.
func printWithIndendationLevel(level int, builder *strings.Builder, format string, a ...any) {
fmt.Fprint(builder, strings.Repeat("\t", level))
fmt.Fprintf(builder, format, a...)
}
+482
View File
@@ -0,0 +1,482 @@
// Copyright 2025 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 procedures
import (
"context"
"fmt"
"maps"
"slices"
"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/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/server/plpgsql"
)
// ParameterMode represents the mode of the given parameter (whether it's IN, OUT, INOUT, or VARIADIC).
type ParameterMode uint8
const (
ParameterMode_IN ParameterMode = 0
ParameterMode_OUT ParameterMode = 1
ParameterMode_INOUT ParameterMode = 2
ParameterMode_VARIADIC ParameterMode = 3
)
// Collection contains a collection of procedures.
type Collection struct {
accessCache map[id.Procedure]Procedure // This cache is used for general access when you know the exact ID
overloadCache map[id.Procedure][]id.Procedure // This cache is used to find overloads if you know the name
idCache []id.Procedure // This cache simply contains the name of every procedure
mapHash hash.Hash // This is cached so that we don't have to calculate the hash every time
underlyingMap prolly.AddressMap
ns tree.NodeStore
}
// Procedure represents a created procedure.
type Procedure struct {
ID id.Procedure
ParameterNames []string
ParameterTypes []id.Type
ParameterModes []ParameterMode
ParameterDefaults []string
Definition string
ExtensionName string // Only used when this is an extension procedure
ExtensionSymbol string // Only used when this is an extension procedure
Operations []plpgsql.InterpreterOperation // Only used when this is a plpgsql language
SQLDefinition string // Only used when this is a sql language
}
var _ objinterface.Collection = (*Collection)(nil)
var _ objinterface.RootObject = Procedure{}
// NewCollection returns a new Collection.
func NewCollection(ctx context.Context, underlyingMap prolly.AddressMap, ns tree.NodeStore) (*Collection, error) {
collection := &Collection{
accessCache: make(map[id.Procedure]Procedure),
overloadCache: make(map[id.Procedure][]id.Procedure),
idCache: nil,
mapHash: hash.Hash{},
underlyingMap: underlyingMap,
ns: ns,
}
return collection, collection.reloadCaches(ctx)
}
// GetProcedure returns the procedure with the given ID. Returns a procedure with an invalid ID if it cannot be found
// (Procedure.ID.IsValid() == false).
func (pgp *Collection) GetProcedure(_ context.Context, procID id.Procedure) (Procedure, error) {
if f, ok := pgp.accessCache[procID]; ok {
return f, nil
}
return Procedure{}, nil
}
// GetProcedureOverloads returns the overloads for the procedure matching the schema and the procedure name. The
// parameter types are ignored when searching for overloads.
func (pgp *Collection) GetProcedureOverloads(_ context.Context, procID id.Procedure) ([]Procedure, error) {
overloads, ok := pgp.overloadCache[id.NewProcedure(procID.SchemaName(), procID.ProcedureName())]
if !ok || len(overloads) == 0 {
return nil, nil
}
procs := make([]Procedure, len(overloads))
for i, overload := range overloads {
procs[i] = pgp.accessCache[overload]
}
return procs, nil
}
// HasProcedure returns whether the procedure is present.
func (pgp *Collection) HasProcedure(_ context.Context, procID id.Procedure) bool {
_, ok := pgp.accessCache[procID]
return ok
}
// AddProcedure adds a new procedure.
func (pgp *Collection) AddProcedure(ctx context.Context, proc Procedure) error {
// First we'll check to see if it exists
if _, ok := pgp.accessCache[proc.ID]; ok {
return errors.Errorf(`procedure "%s" already exists with same argument types`, proc.ID.ProcedureName())
}
// Now we'll add the procedure to our map
data, err := proc.Serialize(ctx)
if err != nil {
return err
}
h, err := pgp.ns.WriteBytes(ctx, data)
if err != nil {
return err
}
mapEditor := pgp.underlyingMap.Editor()
if err = mapEditor.Add(ctx, string(proc.ID), h); err != nil {
return err
}
newMap, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pgp.underlyingMap = newMap
pgp.mapHash = pgp.underlyingMap.HashOf()
return pgp.reloadCaches(ctx)
}
// DropProcedure drops an existing procedure.
func (pgp *Collection) DropProcedure(ctx context.Context, procIDs ...id.Procedure) error {
if len(procIDs) == 0 {
return nil
}
// Check that each name exists before performing any deletions
for _, procID := range procIDs {
if _, ok := pgp.accessCache[procID]; !ok {
return errors.Errorf(`procedure %s does not exist`, procID.ProcedureName())
}
}
// Now we'll remove the procedure from the map
mapEditor := pgp.underlyingMap.Editor()
for _, procID := range procIDs {
err := mapEditor.Delete(ctx, string(procID))
if err != nil {
return err
}
}
newMap, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pgp.underlyingMap = newMap
pgp.mapHash = pgp.underlyingMap.HashOf()
return pgp.reloadCaches(ctx)
}
// resolveName returns the fully resolved name of the given procedure. Returns an error if the name is ambiguous.
//
// The following formats are examples of a formatted name:
// name()
// name(type1, schema.type2)
// name(,,)
func (pgp *Collection) resolveName(_ context.Context, schemaName string, formattedName string) (id.Procedure, error) {
if len(pgp.accessCache) == 0 || len(formattedName) == 0 {
return id.NullProcedure, nil
}
// Extract the actual name from the format
leftParenIndex := strings.IndexByte(formattedName, '(')
if leftParenIndex == -1 {
return id.NullProcedure, nil
}
if formattedName[len(formattedName)-1] != ')' {
return id.NullProcedure, nil
}
procedureName := strings.TrimSpace(formattedName[:leftParenIndex])
var typeIDs []id.Type
typePortion := strings.TrimSpace(formattedName[leftParenIndex+1 : len(formattedName)-1])
if len(typePortion) > 0 {
// If the type portion is just an empty string, then we don't want any type IDs
typeStrings := strings.Split(strings.TrimSpace(formattedName[leftParenIndex+1:len(formattedName)-1]), ",")
typeIDs = make([]id.Type, len(typeStrings))
for i, typeString := range typeStrings {
typeParts := strings.Split(typeString, ".")
switch len(typeParts) {
case 1:
typeIDs[i] = id.NewType("", strings.TrimSpace(typeParts[0]))
case 2:
typeIDs[i] = id.NewType(strings.TrimSpace(typeParts[0]), strings.TrimSpace(typeParts[1]))
default:
return id.NullProcedure, nil
}
}
}
// If there's an exact match, then we return exactly that
fullID := id.NewProcedure(schemaName, procedureName, typeIDs...)
if _, ok := pgp.accessCache[fullID]; ok {
return fullID, nil
}
// Otherwise we'll iterate over all the names
var resolvedID id.Procedure
OuterLoop:
for _, procID := range pgp.idCache {
if !strings.EqualFold(procedureName, procID.ProcedureName()) {
continue
}
if len(schemaName) > 0 && !strings.EqualFold(schemaName, procID.SchemaName()) {
continue
}
if len(typeIDs) > 0 {
if procID.ParameterCount() != len(typeIDs) {
continue
}
for i, param := range procID.Parameters() {
if len(typeIDs[i].TypeName()) > 0 && !strings.EqualFold(typeIDs[i].TypeName(), param.TypeName()) {
continue OuterLoop
}
if len(typeIDs[i].SchemaName()) > 0 && !strings.EqualFold(typeIDs[i].SchemaName(), param.SchemaName()) {
continue OuterLoop
}
}
}
// Everything must have matched to have made it here
if resolvedID.IsValid() {
procTableName := ProcedureIDToTableName(procID)
resolvedTableName := ProcedureIDToTableName(resolvedID)
return id.NullProcedure, fmt.Errorf("`%s.%s` is ambiguous, matches `%s` and `%s`",
schemaName, formattedName, procTableName.String(), resolvedTableName.String())
}
resolvedID = procID
}
return resolvedID, nil
}
// iterateIDs iterates over all procedure IDs in the collection.
func (pgp *Collection) iterateIDs(_ context.Context, callback func(procID id.Procedure) (stop bool, err error)) error {
for _, procID := range pgp.idCache {
stop, err := callback(procID)
if err != nil {
return err
} else if stop {
return nil
}
}
return nil
}
// IterateProcedures iterates over all procedures in the collection.
func (pgp *Collection) IterateProcedures(_ context.Context, callback func(f Procedure) (stop bool, err error)) error {
for _, procID := range pgp.idCache {
stop, err := callback(pgp.accessCache[procID])
if err != nil {
return err
} else if stop {
return nil
}
}
return nil
}
// Clone returns a new *Collection with the same contents as the original.
func (pgp *Collection) Clone(_ context.Context) *Collection {
return &Collection{
accessCache: maps.Clone(pgp.accessCache),
overloadCache: maps.Clone(pgp.overloadCache),
idCache: slices.Clone(pgp.idCache),
mapHash: pgp.mapHash,
underlyingMap: pgp.underlyingMap,
ns: pgp.ns,
}
}
// Map returns the underlying map.
func (pgp *Collection) Map(_ context.Context) (prolly.AddressMap, error) {
return pgp.underlyingMap, nil
}
// 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 (pgp *Collection) DiffersFrom(ctx context.Context, root objinterface.RootValue) bool {
hashOnGivenRoot, err := pgp.LoadCollectionHash(ctx, root)
if err != nil {
return true
}
if pgp.mapHash.Equal(hashOnGivenRoot) {
return false
}
// An empty map should match an uninitialized collection on the root
count, err := pgp.underlyingMap.Count()
if err == nil && count == 0 && hashOnGivenRoot.IsEmpty() {
return false
}
return true
}
// reloadCaches writes the underlying map's contents to the caches.
func (pgp *Collection) reloadCaches(ctx context.Context) error {
count, err := pgp.underlyingMap.Count()
if err != nil {
return err
}
clear(pgp.accessCache)
clear(pgp.overloadCache)
pgp.mapHash = pgp.underlyingMap.HashOf()
pgp.idCache = make([]id.Procedure, 0, count)
return pgp.underlyingMap.IterAll(ctx, func(_ string, h hash.Hash) error {
if h.IsEmpty() {
return nil
}
data, err := pgp.ns.ReadBytes(ctx, h)
if err != nil {
return err
}
f, err := DeserializeProcedure(ctx, data)
if err != nil {
return err
}
pgp.accessCache[f.ID] = f
partialID := id.NewProcedure(f.ID.SchemaName(), f.ID.ProcedureName())
pgp.overloadCache[partialID] = append(pgp.overloadCache[partialID], f.ID)
pgp.idCache = append(pgp.idCache, f.ID)
return 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 (pgp *Collection) tableNameToID(schemaName string, formattedName string) id.Procedure {
leftParenIndex := strings.IndexByte(formattedName, '(')
if leftParenIndex == -1 {
return id.NullProcedure
}
if formattedName[len(formattedName)-1] != ')' {
return id.NullProcedure
}
procedureName := strings.TrimSpace(formattedName[:leftParenIndex])
var typeIDs []id.Type
typePortion := strings.TrimSpace(formattedName[leftParenIndex+1 : len(formattedName)-1])
if len(typePortion) > 0 {
// If the type portion is just an empty string, then we don't want any type IDs
typeStrings := strings.Split(strings.TrimSpace(formattedName[leftParenIndex+1:len(formattedName)-1]), ",")
typeIDs = make([]id.Type, len(typeStrings))
for i, typeString := range typeStrings {
typeParts := strings.Split(typeString, ".")
switch len(typeParts) {
case 1:
typeIDs[i] = id.NewType("", strings.TrimSpace(typeParts[0]))
case 2:
typeIDs[i] = id.NewType(strings.TrimSpace(typeParts[0]), strings.TrimSpace(typeParts[1]))
default:
return id.NullProcedure
}
}
}
return id.NewProcedure(schemaName, procedureName, typeIDs...)
}
// GetID implements the interface objinterface.RootObject.
func (procedure Procedure) GetID() id.Id {
return procedure.ID.AsId()
}
// GetInnerDefinition returns the inner definition inside the CREATE PROCEDURE statement.
func (procedure Procedure) GetInnerDefinition() string {
// TODO: right now we're hardcode searching for $$, which will fail for some definition strings
start := strings.Index(procedure.Definition, "$$")
end := strings.LastIndex(procedure.Definition, "$$")
if start == -1 || end == -1 {
// Return the whole definition for now
return procedure.Definition
}
return strings.TrimSpace(procedure.Definition[start+2 : end])
}
// ReplaceDefinition returns a new definition with the inner portion replaced with the given string.
func (procedure Procedure) ReplaceDefinition(newInner string) string {
return strings.Replace(procedure.Definition, procedure.GetInnerDefinition(), newInner, 1)
}
// GetRootObjectID implements the interface objinterface.RootObject.
func (procedure Procedure) GetRootObjectID() objinterface.RootObjectID {
return objinterface.RootObjectID_Procedures
}
// HashOf implements the interface objinterface.RootObject.
func (procedure Procedure) HashOf(ctx context.Context) (hash.Hash, error) {
data, err := procedure.Serialize(ctx)
if err != nil {
return hash.Hash{}, err
}
return hash.Of(data), nil
}
// Name implements the interface objinterface.RootObject.
func (procedure Procedure) Name() doltdb.TableName {
return ProcedureIDToTableName(procedure.ID)
}
// ParameterModesAsString returns a string that represents the parameter modes. The string may be converted back to a
// slice using ParameterModesFromString.
func (procedure Procedure) ParameterModesAsString() string {
sb := strings.Builder{}
for i, mode := range procedure.ParameterModes {
if i > 0 {
sb.WriteRune(',')
}
switch mode {
case ParameterMode_IN:
sb.WriteString("in")
case ParameterMode_OUT:
sb.WriteString("out")
case ParameterMode_INOUT:
sb.WriteString("inout")
case ParameterMode_VARIADIC:
sb.WriteString("variadic")
default:
panic("unhandled procedure parameter mode")
}
}
return sb.String()
}
// ProcedureIDToTableName returns the ID in a format that's better for user consumption.
func ProcedureIDToTableName(procID id.Procedure) doltdb.TableName {
paramTypes := procID.Parameters()
strTypes := make([]string, len(paramTypes))
for i, paramType := range paramTypes {
if paramType.SchemaName() == "pg_catalog" || paramType.SchemaName() == procID.SchemaName() {
strTypes[i] = paramType.TypeName()
} else {
strTypes[i] = fmt.Sprintf("%s.%s", paramType.SchemaName(), paramType.TypeName())
}
}
return doltdb.TableName{
Name: fmt.Sprintf("%s(%s)", procID.ProcedureName(), strings.Join(strTypes, ",")),
Schema: procID.SchemaName(),
}
}
// ParameterModesFromString returns a ParameterMode slice from the given string. It is assumed that this string was
// originally created using Procedure.ParameterModesAsString.
func ParameterModesFromString(str string) ([]ParameterMode, error) {
if len(str) == 0 {
return nil, nil
}
modeStrings := strings.Split(str, ",")
modes := make([]ParameterMode, len(modeStrings))
for i, modeString := range modeStrings {
switch modeString {
case "in":
modes[i] = ParameterMode_IN
case "out":
modes[i] = ParameterMode_OUT
case "inout":
modes[i] = ParameterMode_INOUT
case "variadic":
modes[i] = ParameterMode_VARIADIC
default:
return nil, errors.Errorf("`%s` is not a valid parameter argmode, it may be one of the following: in, out, inout, variadic", modeString)
}
}
return modes, nil
}
+126
View File
@@ -0,0 +1,126 @@
// Copyright 2025 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 procedures
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/merge"
"github.com/dolthub/dolt/go/store/hash"
"github.com/dolthub/dolt/go/store/prolly"
"github.com/dolthub/doltgresql/core/id"
pgmerge "github.com/dolthub/doltgresql/core/merge"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/flatbuffers/gen/serial"
)
// storage is used to read from and write to the root.
var storage = objinterface.RootObjectSerializer{
Bytes: (*serial.RootValue).ProceduresBytes,
RootValueAdd: serial.RootValueAddProcedures,
}
// HandleMerge implements the interface objinterface.Collection.
func (*Collection) HandleMerge(ctx context.Context, mro merge.MergeRootObject) (doltdb.RootObject, *merge.MergeStats, error) {
ourProc := mro.OurRootObj.(Procedure)
theirProc := mro.TheirRootObj.(Procedure)
// Ensure that they have the same identifier
if ourProc.ID != theirProc.ID {
return nil, nil, errors.Newf("attempted to merge different procedures: `%s` and `%s`",
ourProc.Name().String(), theirProc.Name().String())
}
ourHash, err := ourProc.HashOf(ctx)
if err != nil {
return nil, nil, err
}
theirHash, err := theirProc.HashOf(ctx)
if err != nil {
return nil, nil, err
}
if ourHash.Equal(theirHash) {
return mro.OurRootObj, &merge.MergeStats{
Operation: merge.TableUnmodified,
Adds: 0,
Deletes: 0,
Modifications: 0,
DataConflicts: 0,
SchemaConflicts: 0,
RootObjectConflicts: 0,
ConstraintViolations: 0,
}, nil
}
return pgmerge.CreateConflict(ctx, mro.RightSrc, ourProc, theirProc, mro.AncestorRootObj)
}
// LoadCollection implements the interface objinterface.Collection.
func (*Collection) LoadCollection(ctx context.Context, root objinterface.RootValue) (objinterface.Collection, error) {
return LoadProcedures(ctx, root)
}
// LoadCollectionHash implements the interface objinterface.Collection.
func (*Collection) LoadCollectionHash(ctx context.Context, root objinterface.RootValue) (hash.Hash, error) {
m, ok, err := storage.GetProllyMap(ctx, root)
if err != nil || !ok {
return hash.Hash{}, err
}
return m.HashOf(), nil
}
// LoadProcedures loads the procedures collection from the given root.
func LoadProcedures(ctx context.Context, root objinterface.RootValue) (*Collection, error) {
m, ok, err := storage.GetProllyMap(ctx, root)
if err != nil {
return nil, err
}
if !ok {
m, err = prolly.NewEmptyAddressMap(root.NodeStore())
if err != nil {
return nil, err
}
}
return NewCollection(ctx, m, root.NodeStore())
}
// ResolveNameFromObjects implements the interface objinterface.Collection.
func (*Collection) ResolveNameFromObjects(ctx context.Context, name doltdb.TableName, rootObjects []objinterface.RootObject) (doltdb.TableName, id.Id, error) {
tempCollection := Collection{
accessCache: make(map[id.Procedure]Procedure),
idCache: make([]id.Procedure, 0, len(rootObjects)),
}
for _, rootObject := range rootObjects {
if obj, ok := rootObject.(Procedure); ok {
tempCollection.accessCache[obj.ID] = obj
tempCollection.idCache = append(tempCollection.idCache, obj.ID)
}
}
return tempCollection.ResolveName(ctx, name)
}
// Serializer implements the interface objinterface.Collection.
func (*Collection) Serializer() objinterface.RootObjectSerializer {
return storage
}
// UpdateRoot implements the interface objinterface.Collection.
func (pgp *Collection) UpdateRoot(ctx context.Context, root objinterface.RootValue) (objinterface.RootValue, error) {
m, err := pgp.Map(ctx)
if err != nil {
return nil, err
}
return storage.WriteProllyMap(ctx, root, m)
}
+285
View File
@@ -0,0 +1,285 @@
// Copyright 2025 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 procedures
import (
"context"
"strings"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/doltgresql/core/id"
pgmerge "github.com/dolthub/doltgresql/core/merge"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/server/plpgsql"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
const (
FIELD_NAME_PARAMETER_NAMES = "parameter_names"
FIELD_NAME_PARAMETER_MODES = "parameter_argmodes"
FIELD_NAME_DEFINITION = "definition"
FIELD_NAME_EXTENSION_NAME = "extension_name"
FIELD_NAME_EXTENSION_SYMBOL = "extension_symbol"
FIELD_NAME_SQL_DEFINITION = "sql_definition"
)
// DeserializeRootObject implements the interface objinterface.Collection.
func (pgp *Collection) DeserializeRootObject(ctx context.Context, data []byte) (objinterface.RootObject, error) {
return DeserializeProcedure(ctx, data)
}
// DiffRootObjects implements the interface objinterface.Collection.
func (pgp *Collection) DiffRootObjects(ctx context.Context, fromHash string, o objinterface.RootObject, t objinterface.RootObject, a objinterface.RootObject) ([]objinterface.RootObjectDiff, objinterface.RootObject, error) {
// We ignore many fields when diffing, as differences in these fields would result in a different procedure due to overloading
// For example, "proc_name(text)" and "proc_name(varchar)" cannot produce a conflict as they're different procedures
ours := o.(Procedure)
theirs := t.(Procedure)
ancestor, hasAncestor := a.(Procedure)
var diffs []objinterface.RootObjectDiff
{
ourParamNames := strings.Join(ours.ParameterNames, ",")
theirParamNames := strings.Join(theirs.ParameterNames, ",")
ancParamNames := strings.Join(ancestor.ParameterNames, ",")
diff := objinterface.RootObjectDiff{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: FIELD_NAME_PARAMETER_NAMES,
}
if pgmerge.DiffValues(&diff, ourParamNames, theirParamNames, ancParamNames, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.ParameterNames = strings.Split(diff.OurValue.(string), ",")
}
}
{
ourModes := ours.ParameterModesAsString()
theirModes := theirs.ParameterModesAsString()
ancModes := ancestor.ParameterModesAsString()
diff := objinterface.RootObjectDiff{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: FIELD_NAME_PARAMETER_MODES,
}
if pgmerge.DiffValues(&diff, ourModes, theirModes, ancModes, hasAncestor) {
diffs = append(diffs, diff)
} else {
paramModes, err := ParameterModesFromString(diff.OurValue.(string))
if err != nil {
return nil, nil, err
}
ours.ParameterModes = paramModes
}
}
if ours.Definition != theirs.Definition {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: FIELD_NAME_DEFINITION,
}
if pgmerge.DiffValues(&diff, ours.GetInnerDefinition(), theirs.GetInnerDefinition(), ancestor.GetInnerDefinition(), hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Definition = ours.ReplaceDefinition(diff.OurValue.(string))
}
}
if ours.ExtensionName != theirs.ExtensionName {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: FIELD_NAME_EXTENSION_NAME,
}
if pgmerge.DiffValues(&diff, ours.ExtensionName, theirs.ExtensionName, ancestor.ExtensionName, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.ExtensionName = diff.OurValue.(string)
}
}
if ours.ExtensionSymbol != theirs.ExtensionSymbol {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: FIELD_NAME_EXTENSION_SYMBOL,
}
if pgmerge.DiffValues(&diff, ours.ExtensionSymbol, theirs.ExtensionSymbol, ancestor.ExtensionSymbol, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.ExtensionSymbol = diff.OurValue.(string)
}
}
if ours.SQLDefinition != theirs.SQLDefinition {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: FIELD_NAME_SQL_DEFINITION,
}
if pgmerge.DiffValues(&diff, ours.SQLDefinition, theirs.SQLDefinition, ancestor.SQLDefinition, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.SQLDefinition = diff.OurValue.(string)
}
}
return diffs, ours, nil
}
// DropRootObject implements the interface objinterface.Collection.
func (pgp *Collection) DropRootObject(ctx context.Context, identifier id.Id) error {
if identifier.Section() != id.Section_Procedure {
return errors.Errorf(`procedure %s does not exist`, identifier.String())
}
return pgp.DropProcedure(ctx, id.Procedure(identifier))
}
// GetFieldType implements the interface objinterface.Collection.
func (pgp *Collection) GetFieldType(ctx context.Context, fieldName string) *pgtypes.DoltgresType {
switch fieldName {
case FIELD_NAME_PARAMETER_NAMES:
return pgtypes.Text
case FIELD_NAME_PARAMETER_MODES:
return pgtypes.Text
case FIELD_NAME_DEFINITION:
return pgtypes.Text
case FIELD_NAME_EXTENSION_NAME:
return pgtypes.Text
case FIELD_NAME_EXTENSION_SYMBOL:
return pgtypes.Text
case FIELD_NAME_SQL_DEFINITION:
return pgtypes.Text
default:
return nil
}
}
// GetID implements the interface objinterface.Collection.
func (pgp *Collection) GetID() objinterface.RootObjectID {
return objinterface.RootObjectID_Procedures
}
// GetRootObject implements the interface objinterface.Collection.
func (pgp *Collection) GetRootObject(ctx context.Context, identifier id.Id) (objinterface.RootObject, bool, error) {
if identifier.Section() != id.Section_Procedure {
return nil, false, nil
}
f, err := pgp.GetProcedure(ctx, id.Procedure(identifier))
return f, err == nil && f.ID.IsValid(), err
}
// HasRootObject implements the interface objinterface.Collection.
func (pgp *Collection) HasRootObject(ctx context.Context, identifier id.Id) (bool, error) {
if identifier.Section() != id.Section_Procedure {
return false, nil
}
return pgp.HasProcedure(ctx, id.Procedure(identifier)), nil
}
// IDToTableName implements the interface objinterface.Collection.
func (pgp *Collection) IDToTableName(identifier id.Id) doltdb.TableName {
if identifier.Section() != id.Section_Procedure {
return doltdb.TableName{}
}
return ProcedureIDToTableName(id.Procedure(identifier))
}
// IterAll implements the interface objinterface.Collection.
func (pgp *Collection) IterAll(ctx context.Context, callback func(rootObj objinterface.RootObject) (stop bool, err error)) error {
return pgp.IterateProcedures(ctx, func(f Procedure) (stop bool, err error) {
return callback(f)
})
}
// IterIDs implements the interface objinterface.Collection.
func (pgp *Collection) IterIDs(ctx context.Context, callback func(identifier id.Id) (stop bool, err error)) error {
return pgp.iterateIDs(ctx, func(procID id.Procedure) (stop bool, err error) {
return callback(procID.AsId())
})
}
// PutRootObject implements the interface objinterface.Collection.
func (pgp *Collection) PutRootObject(ctx context.Context, rootObj objinterface.RootObject) error {
f, ok := rootObj.(Procedure)
if !ok {
return errors.Newf("invalid procedure root object: %T", rootObj)
}
return pgp.AddProcedure(ctx, f)
}
// RenameRootObject implements the interface objinterface.Collection.
func (pgp *Collection) RenameRootObject(ctx context.Context, oldName id.Id, newName id.Id) error {
if !oldName.IsValid() || !newName.IsValid() || oldName.Section() != newName.Section() || oldName.Section() != id.Section_Procedure {
return errors.New("cannot rename procedure due to invalid name")
}
oldProcName := id.Procedure(oldName)
newProcName := id.Procedure(newName)
if oldProcName.ParameterCount() != newProcName.ParameterCount() {
return errors.Newf(`old procedure id had "%d" parameters, new procedure id has "%d" parameters`,
oldProcName.ParameterCount(), newProcName.ParameterCount())
}
proc, err := pgp.GetProcedure(ctx, oldProcName)
if err != nil {
return err
}
if err = pgp.DropProcedure(ctx, oldProcName); err != nil {
return err
}
proc.ID = newProcName
return pgp.AddProcedure(ctx, proc)
}
// ResolveName implements the interface objinterface.Collection.
func (pgp *Collection) ResolveName(ctx context.Context, name doltdb.TableName) (doltdb.TableName, id.Id, error) {
rawID, err := pgp.resolveName(ctx, name.Schema, name.Name)
if err != nil || !rawID.IsValid() {
return doltdb.TableName{}, id.Null, err
}
return ProcedureIDToTableName(rawID), rawID.AsId(), nil
}
// TableNameToID implements the interface objinterface.Collection.
func (pgp *Collection) TableNameToID(name doltdb.TableName) id.Id {
return pgp.tableNameToID(name.Schema, name.Name).AsId()
}
// UpdateField implements the interface objinterface.Collection.
func (pgp *Collection) UpdateField(ctx context.Context, rootObject objinterface.RootObject, fieldName string, newValue any) (objinterface.RootObject, error) {
procedure := rootObject.(Procedure)
switch fieldName {
case FIELD_NAME_PARAMETER_NAMES:
procedure.ParameterNames = strings.Split(newValue.(string), ",")
case FIELD_NAME_PARAMETER_MODES:
newModes, err := ParameterModesFromString(newValue.(string))
if err != nil {
return nil, err
}
procedure.ParameterModes = newModes
case FIELD_NAME_DEFINITION:
newDefinition := procedure.ReplaceDefinition(newValue.(string))
parsedBody, err := plpgsql.Parse(newDefinition)
if err != nil {
return nil, err
}
procedure.Definition = newDefinition
procedure.Operations = parsedBody
case FIELD_NAME_EXTENSION_NAME:
procedure.ExtensionName = newValue.(string)
case FIELD_NAME_EXTENSION_SYMBOL:
procedure.ExtensionSymbol = newValue.(string)
case FIELD_NAME_SQL_DEFINITION:
procedure.SQLDefinition = newValue.(string)
default:
return nil, errors.Newf("unknown field name: `%s`", fieldName)
}
return procedure, nil
}
+113
View File
@@ -0,0 +1,113 @@
// Copyright 2025 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 procedures
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/server/plpgsql"
"github.com/dolthub/doltgresql/utils"
)
// Serialize returns the Procedure as a byte slice. If the Procedure is invalid, then this returns a nil slice.
func (procedure Procedure) Serialize(ctx context.Context) ([]byte, error) {
if !procedure.ID.IsValid() {
return nil, nil
}
// Write all of the procedures to the writer
writer := utils.NewWriter(256)
writer.VariableUint(1) // Version
// Write the procedure data
writer.Id(procedure.ID.AsId())
writer.StringSlice(procedure.ParameterNames)
writer.IdTypeSlice(procedure.ParameterTypes)
writer.String(procedure.Definition)
writer.String(procedure.ExtensionName)
writer.String(procedure.ExtensionSymbol)
writer.String(procedure.SQLDefinition)
// Write the parameter modes
writer.VariableUint(uint64(len(procedure.ParameterModes)))
for _, mode := range procedure.ParameterModes {
writer.Uint8(uint8(mode))
}
// Write the operations
writer.VariableUint(uint64(len(procedure.Operations)))
for _, op := range procedure.Operations {
writer.Uint16(uint16(op.OpCode))
writer.String(op.PrimaryData)
writer.StringSlice(op.SecondaryData)
writer.String(op.Target)
writer.Int32(int32(op.Index))
writer.StringMap(op.Options)
}
// Write version 1 data
writer.StringSlice(procedure.ParameterDefaults)
// Returns the data
return writer.Data(), nil
}
// DeserializeProcedure returns the Procedure that was serialized in the byte slice. Returns an empty Procedure (invalid
// ID) if data is nil or empty.
func DeserializeProcedure(ctx context.Context, data []byte) (Procedure, error) {
if len(data) == 0 {
return Procedure{}, nil
}
reader := utils.NewReader(data)
version := reader.VariableUint()
if version > 1 {
return Procedure{}, errors.Errorf("version %d of procedures is not supported, please upgrade the server", version)
}
// Read from the reader
p := Procedure{}
p.ID = id.Procedure(reader.Id())
p.ParameterNames = reader.StringSlice()
p.ParameterTypes = reader.IdTypeSlice()
p.Definition = reader.String()
p.ExtensionName = reader.String()
p.ExtensionSymbol = reader.String()
p.SQLDefinition = reader.String()
// Read the parameter modes
modeCount := reader.VariableUint()
p.ParameterModes = make([]ParameterMode, modeCount)
for modeIdx := uint64(0); modeIdx < modeCount; modeIdx++ {
p.ParameterModes[modeIdx] = ParameterMode(reader.Uint8())
}
// Read the operations
opCount := reader.VariableUint()
p.Operations = make([]plpgsql.InterpreterOperation, opCount)
for opIdx := uint64(0); opIdx < opCount; opIdx++ {
op := plpgsql.InterpreterOperation{}
op.OpCode = plpgsql.OpCode(reader.Uint16())
op.PrimaryData = reader.String()
op.SecondaryData = reader.StringSlice()
op.Target = reader.String()
op.Index = int(reader.Int32())
op.Options = reader.StringMap()
p.Operations[opIdx] = op
}
if version >= 1 {
p.ParameterDefaults = reader.StringSlice()
}
if !reader.IsEmpty() {
return Procedure{}, errors.New("extra data found while deserializing a procedure")
}
// Return the deserialized object
return p, nil
}
+89
View File
@@ -0,0 +1,89 @@
// 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 core
import (
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/sequences"
)
// RelationType states the type of the relation.
type RelationType byte
const (
RelationType_DoesNotExist RelationType = iota
RelationType_Table
RelationType_Sequence
)
// GetRelationType returns whether the working root has the given relation, and what type of relation it is. According
// to the Postgres docs, a relation may be one of: table, sequence, index, view, materialized view, foreign table. This
// may also include composite types and partitions, but this hasn't been confirmed.
func GetRelationType(ctx *sql.Context, schema string, relation string) (RelationType, error) {
// TODO: the schema isn't actually being used
if len(schema) == 0 {
var err error
schema, err = GetCurrentSchema(ctx)
if err != nil {
return RelationType_DoesNotExist, err
}
}
session := dsess.DSessFromSess(ctx.Session)
state, ok, err := session.LookupDbState(ctx, ctx.GetCurrentDatabase())
if err != nil {
return RelationType_DoesNotExist, err
}
if !ok {
return RelationType_DoesNotExist, errors.Errorf("GetRelationType cannot find the database")
}
// Verify relation against temporary tables created this session
dbName := ctx.GetCurrentDatabase()
if _, ok := session.GetTemporaryTable(ctx, dbName, relation); ok {
return RelationType_Table, nil
}
return GetRelationTypeFromRoot(ctx, schema, relation, state.WorkingRoot().(*RootValue))
}
// GetRelationTypeFromRoot performs the same function as GetRelationType, except that it uses the given root rather than
// the working session's root.
func GetRelationTypeFromRoot(ctx *sql.Context, schema string, relation string, root *RootValue) (RelationType, error) {
// Check tables first
ok, err := root.HasTable(ctx, doltdb.TableName{Schema: schema, Name: relation})
if err != nil {
return RelationType_DoesNotExist, err
}
if ok {
return RelationType_Table, nil
}
// Check sequences next
collection, err := sequences.LoadSequences(ctx, root)
if err != nil {
return RelationType_DoesNotExist, err
}
if collection.HasSequence(ctx, id.NewSequence(schema, relation)) {
return RelationType_Sequence, nil
}
// TODO: the rest of the relations
return RelationType_DoesNotExist, nil
}
+576
View File
@@ -0,0 +1,576 @@
// Copyright 2025 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 rootobject
import (
"bytes"
"context"
"fmt"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/merge"
"github.com/dolthub/doltgresql/core/casts"
"github.com/dolthub/doltgresql/core/conflicts"
"github.com/dolthub/doltgresql/core/extensions"
"github.com/dolthub/doltgresql/core/functions"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/procedures"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/core/sequences"
"github.com/dolthub/doltgresql/core/triggers"
"github.com/dolthub/doltgresql/core/typecollection"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
var (
// globalCollections maps each ID to the collection.
globalCollections = []objinterface.Collection{
nil, // Corresponds to RootObjectID_None
&sequences.Collection{},
&typecollection.TypeCollection{},
&functions.Collection{},
&triggers.Collection{},
&extensions.Collection{},
&conflicts.Collection{},
&procedures.Collection{},
&casts.Collection{},
}
)
// CreateConflict creates a conflict on the given root for the two root objects.
func CreateConflict(ctx context.Context, rightSrc doltdb.Rootish, o doltdb.RootObject, t doltdb.RootObject, a doltdb.RootObject) (doltdb.RootObject, *merge.MergeStats, error) {
ours, ok1 := o.(objinterface.RootObject)
theirs, ok2 := t.(objinterface.RootObject)
if !ok1 || !ok2 {
return nil, nil, errors.New("unsupported object found during conflict creation")
}
ancestor, _ := a.(objinterface.RootObject) // If this is nil, then the conversion will also be nil (which is fine)
rightHash, err := rightSrc.HashOf()
if err != nil {
return nil, nil, err
}
if ours.GetID() != theirs.GetID() {
return nil, nil, errors.Errorf(`cannot create a conflict between "%s" and "%s"`,
ours.Name().String(), theirs.Name().String())
}
conflict := conflicts.Conflict{
ID: ours.GetID(),
FromHash: rightHash.String(),
RootObjectID: ours.GetRootObjectID(),
Ours: ours,
Theirs: theirs,
Ancestor: ancestor,
}
diffs, newOurs, err := conflict.Diffs(ctx)
if err != nil {
return nil, nil, err
}
if len(diffs) == 0 {
oldSerialized, err := ours.Serialize(ctx)
if err != nil {
return nil, nil, err
}
newSerialized, err := newOurs.Serialize(ctx)
if err != nil {
return nil, nil, err
}
if bytes.Equal(oldSerialized, newSerialized) {
return newOurs, &merge.MergeStats{
Operation: merge.TableUnmodified,
Adds: 0,
Deletes: 0,
Modifications: 0,
DataConflicts: 0,
SchemaConflicts: 0,
RootObjectConflicts: 0,
ConstraintViolations: 0,
}, nil
} else {
return newOurs, &merge.MergeStats{
Operation: merge.TableModified,
Adds: 0,
Deletes: 0,
Modifications: 1,
DataConflicts: 0,
SchemaConflicts: 0,
RootObjectConflicts: 0,
ConstraintViolations: 0,
}, nil
}
}
conflict.Ours = newOurs
return conflict, &merge.MergeStats{
Operation: merge.TableUnmodified,
Adds: 0,
Deletes: 0,
Modifications: 0,
DataConflicts: 0,
SchemaConflicts: 0,
RootObjectConflicts: len(diffs),
ConstraintViolations: 0,
}, nil
}
// DeserializeRootObject calls the same-named function on the collection that matches the ID that was given.
func DeserializeRootObject(ctx context.Context, rootObjID objinterface.RootObjectID, data []byte) (objinterface.RootObject, error) {
if int64(rootObjID) >= int64(len(globalCollections)) {
return nil, errors.New("unsupported object found, please upgrade the server")
}
collection := globalCollections[rootObjID]
if collection == nil {
return nil, errors.Errorf("invalid root object ID: %d", rootObjID)
}
return collection.DeserializeRootObject(ctx, data)
}
// DiffRootObjects calls the same-named function on the collection that matches the ID that was given.
func DiffRootObjects(ctx context.Context, rootObjID objinterface.RootObjectID, fromHash string, ours, theirs, ancestor objinterface.RootObject) ([]objinterface.RootObjectDiff, objinterface.RootObject, error) {
if int64(rootObjID) >= int64(len(globalCollections)) {
return nil, nil, errors.New("unsupported object found, please upgrade the server")
}
collection := globalCollections[rootObjID]
if collection == nil {
return nil, nil, errors.Errorf("invalid root object ID: %d", rootObjID)
}
if ours == nil && theirs == nil {
return nil, nil, nil
}
if ours == nil {
return []objinterface.RootObjectDiff{{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: objinterface.FIELD_NAME_ROOT_OBJECT,
AncestorValue: objinterface.FIELD_NAME_ANCESTOR,
OurValue: nil,
TheirValue: objinterface.FIELD_NAME_THEIRS,
OurChange: objinterface.RootObjectDiffChange_Deleted,
TheirChange: objinterface.RootObjectDiffChange_Modified,
}}, nil, nil
}
if theirs == nil {
return []objinterface.RootObjectDiff{{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: objinterface.FIELD_NAME_ROOT_OBJECT,
AncestorValue: objinterface.FIELD_NAME_ANCESTOR,
OurValue: objinterface.FIELD_NAME_OURS,
TheirValue: nil,
OurChange: objinterface.RootObjectDiffChange_Modified,
TheirChange: objinterface.RootObjectDiffChange_Deleted,
}}, nil, nil
}
return collection.DiffRootObjects(ctx, fromHash, ours, theirs, ancestor)
}
// GetFieldType calls the same-named function on the collection that matches the ID that was given.
func GetFieldType(ctx context.Context, rootObjID objinterface.RootObjectID, fieldName string) *pgtypes.DoltgresType {
if int64(rootObjID) >= int64(len(globalCollections)) {
return nil
}
collection := globalCollections[rootObjID]
if collection == nil {
return nil
}
if fieldName == objinterface.FIELD_NAME_ROOT_OBJECT {
return pgtypes.Text
}
return collection.GetFieldType(ctx, fieldName)
}
// UpdateField calls the same-named function on the collection that matches the ID that was given.
func UpdateField(ctx context.Context, rootObjID objinterface.RootObjectID, rootObject objinterface.RootObject, fieldName string, newValue any) (objinterface.RootObject, error) {
if int64(rootObjID) >= int64(len(globalCollections)) {
return nil, errors.New("unsupported object found, please upgrade the server")
}
collection := globalCollections[rootObjID]
if collection == nil {
return nil, errors.Errorf("invalid root object ID: %d", rootObjID)
}
// This field should always be handled before this call is made, so it's an error if we see it here
if fieldName == objinterface.FIELD_NAME_ROOT_OBJECT {
return nil, errors.New("cannot set the `root_object` field alongside other fields")
}
return collection.UpdateField(ctx, rootObject, fieldName, newValue)
}
// GetRootObject returns the root object that matches the given name.
func GetRootObject(ctx context.Context, root objinterface.RootValue, tName doltdb.TableName) (objinterface.RootObject, bool, error) {
_, rawID, objID, err := ResolveName(ctx, root, tName)
if err != nil || objID == objinterface.RootObjectID_None {
return nil, false, err
}
coll, _ := globalCollections[objID].LoadCollection(ctx, root)
return coll.GetRootObject(ctx, rawID)
}
// GetRootObjectConflicts returns the conflict root object that matches the given name.
func GetRootObjectConflicts(ctx context.Context, root objinterface.RootValue, tName doltdb.TableName) (conflicts.Conflict, bool, error) {
coll, err := globalCollections[objinterface.RootObjectID_Conflicts].LoadCollection(ctx, root)
if err != nil {
return conflicts.Conflict{}, false, err
}
_, rawID, err := coll.ResolveName(ctx, tName)
if err != nil {
return conflicts.Conflict{}, false, err
}
ro, ok, err := coll.GetRootObject(ctx, rawID)
if err != nil || !ok {
return conflicts.Conflict{}, false, err
}
return ro.(conflicts.Conflict), true, nil
}
// HandleMerge handles merging root objects.
func HandleMerge(ctx context.Context, mro merge.MergeRootObject) (doltdb.RootObject, *merge.MergeStats, error) {
if mro.OurRootObj == nil {
switch {
case mro.TheirRootObj != nil && mro.AncestorRootObj != nil:
theirs := mro.TheirRootObj.(objinterface.RootObject)
ancestor := mro.AncestorRootObj.(objinterface.RootObject)
rightHash, err := mro.RightSrc.HashOf()
if err != nil {
return nil, nil, err
}
theirData, err := theirs.Serialize(ctx)
if err != nil {
return nil, nil, err
}
ancData, err := ancestor.Serialize(ctx)
if err != nil {
return nil, nil, err
}
if bytes.Equal(theirData, ancData) {
return nil, &merge.MergeStats{
Operation: merge.TableRemoved,
Adds: 0,
Deletes: 0,
Modifications: 0,
DataConflicts: 0,
SchemaConflicts: 0,
RootObjectConflicts: 0,
ConstraintViolations: 0,
}, nil
} else {
return conflicts.Conflict{
ID: theirs.GetID(),
FromHash: rightHash.String(),
RootObjectID: theirs.GetRootObjectID(),
Ours: nil,
Theirs: theirs,
Ancestor: ancestor,
}, &merge.MergeStats{
Operation: merge.TableModified,
Adds: 0,
Deletes: 0,
Modifications: 0,
DataConflicts: 0,
SchemaConflicts: 0,
RootObjectConflicts: 1,
ConstraintViolations: 0,
}, nil
}
case mro.TheirRootObj != nil && mro.AncestorRootObj == nil:
return mro.TheirRootObj, &merge.MergeStats{
Operation: merge.TableAdded,
Adds: 0,
Deletes: 0,
Modifications: 0,
DataConflicts: 0,
SchemaConflicts: 0,
RootObjectConflicts: 0,
ConstraintViolations: 0,
}, nil
case mro.TheirRootObj == nil && mro.AncestorRootObj != nil:
return nil, &merge.MergeStats{
Operation: merge.TableRemoved,
Adds: 0,
Deletes: 0,
Modifications: 0,
DataConflicts: 0,
SchemaConflicts: 0,
RootObjectConflicts: 0,
ConstraintViolations: 0,
}, nil
case mro.TheirRootObj == nil && mro.AncestorRootObj == nil:
return nil, &merge.MergeStats{
Operation: merge.TableUnmodified,
Adds: 0,
Deletes: 0,
Modifications: 0,
DataConflicts: 0,
SchemaConflicts: 0,
RootObjectConflicts: 0,
ConstraintViolations: 0,
}, nil
default:
return nil, nil, errors.New("HandleMerge has somehow reached a default case")
}
} else if mro.TheirRootObj == nil {
switch {
case mro.AncestorRootObj != nil:
ours := mro.OurRootObj.(objinterface.RootObject)
ancestor := mro.AncestorRootObj.(objinterface.RootObject)
rightHash, err := mro.RightSrc.HashOf()
if err != nil {
return nil, nil, err
}
ourData, err := ours.Serialize(ctx)
if err != nil {
return nil, nil, err
}
ancData, err := ancestor.Serialize(ctx)
if err != nil {
return nil, nil, err
}
if bytes.Equal(ourData, ancData) {
return nil, &merge.MergeStats{
Operation: merge.TableRemoved,
Adds: 0,
Deletes: 0,
Modifications: 0,
DataConflicts: 0,
SchemaConflicts: 0,
RootObjectConflicts: 0,
ConstraintViolations: 0,
}, nil
} else {
return conflicts.Conflict{
ID: ours.GetID(),
FromHash: rightHash.String(),
RootObjectID: ours.GetRootObjectID(),
Ours: ours,
Theirs: nil,
Ancestor: ancestor,
}, &merge.MergeStats{
Operation: merge.TableModified,
Adds: 0,
Deletes: 0,
Modifications: 0,
DataConflicts: 0,
SchemaConflicts: 0,
RootObjectConflicts: 1,
ConstraintViolations: 0,
}, nil
}
case mro.AncestorRootObj == nil:
return mro.OurRootObj, &merge.MergeStats{
Operation: merge.TableAdded,
Adds: 0,
Deletes: 0,
Modifications: 0,
DataConflicts: 0,
SchemaConflicts: 0,
RootObjectConflicts: 0,
ConstraintViolations: 0,
}, nil
default:
return nil, nil, errors.New("HandleMerge has somehow reached a default case")
}
}
identifier := mro.OurRootObj.(objinterface.RootObject).GetRootObjectID()
if int64(identifier) >= int64(len(globalCollections)) {
return nil, nil, errors.New("unsupported root object found, please upgrade Doltgres to the latest version")
}
coll := globalCollections[identifier]
if coll == nil {
return nil, nil, errors.Newf("invalid root object found, ID: %d", int64(identifier))
}
return coll.HandleMerge(ctx, mro)
}
// LoadAllCollections loads and returns all collections from the root.
func LoadAllCollections(ctx context.Context, root objinterface.RootValue) ([]objinterface.Collection, error) {
colls := make([]objinterface.Collection, 0, len(globalCollections))
for i, emptyColl := range globalCollections {
if emptyColl == nil || i == int(objinterface.RootObjectID_Conflicts) {
continue
}
coll, err := emptyColl.LoadCollection(ctx, root)
if err != nil {
return nil, err
}
colls = append(colls, coll)
}
return colls, nil
}
// LoadCollection loads the collection matching the given ID from the root.
func LoadCollection(ctx context.Context, root objinterface.RootValue, collectionID objinterface.RootObjectID) (objinterface.Collection, error) {
if globalCollections[collectionID] == nil {
return nil, nil
}
return globalCollections[collectionID].LoadCollection(ctx, root)
}
// PutRootObject adds the given root object to the respective Collection in the root, returning the updated root.
func PutRootObject(ctx context.Context, root objinterface.RootValue, tName doltdb.TableName, rootObj objinterface.RootObject) (objinterface.RootValue, error) {
if rootObj == nil {
return root, nil
}
coll, err := LoadCollection(ctx, root, rootObj.GetRootObjectID())
if err != nil {
return nil, err
}
identifier := coll.TableNameToID(tName)
exists, err := coll.HasRootObject(ctx, identifier)
if err != nil {
return nil, err
}
// If this doesn't exist, it may be because the name is slightly different (e.g. missing schema), and we want to resolve it properly
if !exists {
_, resolvedID, err := coll.ResolveName(ctx, tName)
if err != nil {
return nil, err
}
if resolvedID.IsValid() {
identifier = resolvedID
exists = true
}
}
if exists {
if err = coll.DropRootObject(ctx, identifier); err != nil {
return nil, err
}
}
// If this is a conflict, then we only want to put the conflict in the collection if it produces conflict diffs.
// Otherwise, we'll put the merged root object instead.
if conflict, ok := rootObj.(objinterface.Conflict); ok {
diffs, merged, err := conflict.Diffs(ctx)
if err != nil {
return nil, err
}
if len(diffs) == 0 {
// If we deleted from the conflicts collection, then we need to update the collection on the root before returning
if exists {
root, err = coll.UpdateRoot(ctx, root)
if err != nil {
return nil, err
}
}
if merged == nil {
return RemoveRootObjectIfExists(ctx, root, conflict.GetID(), conflict.GetContainedRootObjectID())
} else {
return PutRootObject(ctx, root, tName, merged)
}
} else {
if merged == nil {
root, err = RemoveRootObjectIfExists(ctx, root, conflict.GetID(), conflict.GetContainedRootObjectID())
if err != nil {
return nil, err
}
} else {
root, err = PutRootObject(ctx, root, tName, merged)
if err != nil {
return nil, err
}
}
}
}
if err = coll.PutRootObject(ctx, rootObj); err != nil {
return nil, err
}
return coll.UpdateRoot(ctx, root)
}
// RemoveRootObject removes the matching root object from its respective Collection, returning the updated root.
func RemoveRootObject(ctx context.Context, root objinterface.RootValue, identifier id.Id, rootObjectID objinterface.RootObjectID) (objinterface.RootValue, error) {
coll, err := LoadCollection(ctx, root, rootObjectID)
if err != nil {
return nil, err
}
if err = coll.DropRootObject(ctx, identifier); err != nil {
return nil, err
}
return coll.UpdateRoot(ctx, root)
}
// RemoveRootObjectIfExists removes the matching root object from its respective Collection, returning the updated root.
// If the root object does not exist, then this will not attempt the deletion.
func RemoveRootObjectIfExists(ctx context.Context, root objinterface.RootValue, identifier id.Id, rootObjectID objinterface.RootObjectID) (objinterface.RootValue, error) {
coll, err := LoadCollection(ctx, root, rootObjectID)
if err != nil {
return nil, err
}
exists, err := coll.HasRootObject(ctx, identifier)
if err != nil {
return nil, err
}
if exists {
if err = coll.DropRootObject(ctx, identifier); err != nil {
return nil, err
}
}
return coll.UpdateRoot(ctx, root)
}
// ResolveName returns the fully resolved name of the given item (if the item exists). Also returns the type of the item.
func ResolveName(ctx context.Context, root objinterface.RootValue, name doltdb.TableName) (doltdb.TableName, id.Id, objinterface.RootObjectID, error) {
var resolvedName doltdb.TableName
resolvedRawID := id.Null
resolvedObjID := objinterface.RootObjectID_None
for i, emptyColl := range globalCollections {
if emptyColl == nil || i == int(objinterface.RootObjectID_Conflicts) {
continue
}
coll, err := emptyColl.LoadCollection(ctx, root)
if err != nil {
return doltdb.TableName{}, id.Null, objinterface.RootObjectID_None, err
}
if coll == nil {
continue
}
rName, rID, err := coll.ResolveName(ctx, name)
if err != nil {
return doltdb.TableName{}, id.Null, objinterface.RootObjectID_None, err
}
if rID.IsValid() {
if resolvedObjID != objinterface.RootObjectID_None {
return doltdb.TableName{}, id.Null, objinterface.RootObjectID_None, fmt.Errorf(`"%s" is ambiguous`, name.String())
}
resolvedName = rName
resolvedRawID = rID
resolvedObjID = coll.GetID()
}
}
return resolvedName, resolvedRawID, resolvedObjID, nil
}
// resolveNameFromObjects resolves the given name on all given objects on all global collections (except for conflicts).
func resolveNameFromObjects(ctx context.Context, name doltdb.TableName, rootObjects []objinterface.RootObject) (doltdb.TableName, id.Id, error) {
var resolvedName doltdb.TableName
resolvedRawID := id.Null
for i, emptyColl := range globalCollections {
if emptyColl == nil || i == int(objinterface.RootObjectID_Conflicts) {
continue
}
rName, rID, err := emptyColl.ResolveNameFromObjects(ctx, name, rootObjects)
if err != nil {
return doltdb.TableName{}, id.Null, err
}
if rID.IsValid() {
if resolvedRawID != id.Null {
return doltdb.TableName{}, id.Null, fmt.Errorf(`"%s" is ambiguous`, name.String())
}
resolvedName = rName
resolvedRawID = rID
}
}
return resolvedName, resolvedRawID, nil
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright 2025 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 rootobject
import (
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/merge"
"github.com/dolthub/doltgresql/core/conflicts"
pgmerge "github.com/dolthub/doltgresql/core/merge"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/core/storage"
)
// Init initializes the package
func Init() {
merge.MergeRootObjects = HandleMerge
pgmerge.CreateConflict = CreateConflict
conflicts.DeserializeRootObject = DeserializeRootObject
conflicts.DiffRootObjects = DiffRootObjects
conflicts.GetFieldType = GetFieldType
conflicts.ResolveNameExternal = resolveNameFromObjects
conflicts.UpdateField = UpdateField
doltdb.RootObjectDiffFromRow = objinterface.DiffFromRow
for _, collFuncs := range globalCollections {
if collFuncs == nil {
continue
}
serializer := collFuncs.Serializer()
storage.RootObjectSerializations = append(storage.RootObjectSerializations, storage.RootObjectSerialization{
Bytes: serializer.Bytes,
RootValueAdd: serializer.RootValueAdd,
})
}
}
+113
View File
@@ -0,0 +1,113 @@
// Copyright 2025 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 objinterface
import (
"context"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/merge"
"github.com/dolthub/dolt/go/store/hash"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/storage"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// RootObjectID is an ID that distinguishes names and root objects from one another.
type RootObjectID int64
const (
RootObjectID_None RootObjectID = iota
RootObjectID_Sequences
RootObjectID_Types
RootObjectID_Functions
RootObjectID_Triggers
RootObjectID_Extensions
RootObjectID_Conflicts
RootObjectID_Procedures
RootObjectID_Casts
)
const (
FIELD_NAME_ROOT_OBJECT = "root_object"
FIELD_NAME_ANCESTOR = "ancestor"
FIELD_NAME_OURS = "ours"
FIELD_NAME_THEIRS = "theirs"
)
// Collection is a collection of root objects.
type Collection interface {
// DeserializeRootObject deserializes a root object's data.
DeserializeRootObject(ctx context.Context, data []byte) (RootObject, error)
// DiffRootObjects returns a RootObjectDiff for each change made from the ancestor that is not reflected on both
// "ours" and "theirs".
DiffRootObjects(ctx context.Context, fromHash string, ours RootObject, theirs RootObject, ancestor RootObject) ([]RootObjectDiff, RootObject, error)
// DropRootObject removes the given root object from the collection.
DropRootObject(ctx context.Context, identifier id.Id) error
// GetFieldType returns the type associated with the given diff field name. Returns nil if the name is invalid.
GetFieldType(ctx context.Context, fieldName string) *pgtypes.DoltgresType
// GetID returns the identifying ID for the Collection.
GetID() RootObjectID
// GetRootObject returns the root object matching the given ID. Returns false if it cannot be found.
GetRootObject(ctx context.Context, identifier id.Id) (RootObject, bool, error)
// HasRootObject returns whether a root object exactly matching the given ID was found.
HasRootObject(ctx context.Context, identifier id.Id) (bool, error)
// IDToTableName converts the given ID to a table name. The table name will be empty for invalid IDs.
IDToTableName(identifier id.Id) doltdb.TableName
// IterAll iterates over all root objects in the Collection.
IterAll(ctx context.Context, callback func(rootObj RootObject) (stop bool, err error)) error
// IterIDs iterates over all IDs in the Collection.
IterIDs(ctx context.Context, callback func(identifier id.Id) (stop bool, err error)) error
// PutRootObject updates the Collection with the given root object. This may error if the root object already exists.
PutRootObject(ctx context.Context, rootObj RootObject) error
// RenameRootObject changes the ID for a root object matching the old ID.
RenameRootObject(ctx context.Context, oldID id.Id, newID id.Id) error
// ResolveName finds the closest matching (or exact) ID for the given name. If an exact match is not found, then
// this may error if the name is ambiguous.
ResolveName(ctx context.Context, name doltdb.TableName) (doltdb.TableName, id.Id, error)
// TableNameToID converts the given name to an ID. The ID will be invalid for empty/malformed names.
TableNameToID(name doltdb.TableName) id.Id
// UpdateField updates the field on the given root object with the new value. Returns a new root object with the
// updated field.
UpdateField(ctx context.Context, rootObject RootObject, fieldName string, newValue any) (RootObject, error)
// HandleMerge handles merging of two objects. It is guaranteed that "ours" and "theirs" will not be nil, however
// "ancestor" may or may not be nil.
HandleMerge(ctx context.Context, mro merge.MergeRootObject) (doltdb.RootObject, *merge.MergeStats, error)
// LoadCollection loads the Collection from the given root.
LoadCollection(ctx context.Context, root RootValue) (Collection, error)
// LoadCollectionHash loads the Collection hash from the given root. This does not load the entire collection from
// the root, and is therefore a bit more performant if only the hash is needed.
LoadCollectionHash(ctx context.Context, root RootValue) (hash.Hash, error)
// ResolveNameFromObjects finds the closest matching (or exact) ID for the given name. If an exact match is not
// found, then this may error if the name is ambiguous. This searches through the given root objects rather than the
// ones stored in the collection itself.
ResolveNameFromObjects(ctx context.Context, name doltdb.TableName, rootObjects []RootObject) (doltdb.TableName, id.Id, error)
// Serializer returns the serializer associated with this Collection.
Serializer() RootObjectSerializer
// UpdateRoot updates the Collection in the given root, returning the updated root.
UpdateRoot(ctx context.Context, root RootValue) (RootValue, error)
}
// RootValue is an interface to get around import cycles, since the core package references this package (and is where
// RootValue is defined).
type RootValue interface {
doltdb.RootValue
// GetStorage returns the storage contained in the root.
GetStorage(context.Context) storage.RootStorage
// WithStorage returns an updated RootValue with the given storage.
WithStorage(context.Context, storage.RootStorage) RootValue
}
+208
View File
@@ -0,0 +1,208 @@
// Copyright 2025 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 objinterface
import (
"cmp"
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core/id"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// RootObject is an expanded interface on Dolt's root objects.
type RootObject interface {
doltdb.RootObject
// GetID returns the root object ID.
GetID() id.Id
// GetRootObjectID returns the root object ID.
GetRootObjectID() RootObjectID
// Serialize returns the byte representation of the root object.
Serialize(ctx context.Context) ([]byte, error)
}
// Conflict is an expanded interface on Dolt's conflict root object.
type Conflict interface {
RootObject
doltdb.ConflictRootObject
// GetContainedRootObjectID returns the root object ID of the contained items.
GetContainedRootObjectID() RootObjectID
// Diffs returns the diffs for the conflict, along with the merged root object if there are no diffs.
Diffs(ctx context.Context) ([]RootObjectDiff, RootObject, error)
// FieldType returns the type associated with the given field name. Returns nil if the name does not match a field.
FieldType(ctx context.Context, name string) *pgtypes.DoltgresType
}
// RootObjectDiffChange specifies the type of change that occurred from the ancestor value.
type RootObjectDiffChange uint8
const (
RootObjectDiffChange_Added RootObjectDiffChange = iota
RootObjectDiffChange_Deleted
RootObjectDiffChange_Modified
RootObjectDiffChange_NoChange
)
// RootObjectDiffSchema is the baseline schema that is returned for root object diffs.
var RootObjectDiffSchema = sql.Schema{
{Name: "from_root_ish", Type: pgtypes.Text, Default: nil, Nullable: false},
{Name: "base_value", Type: pgtypes.Text, Default: nil, Nullable: true},
{Name: "our_value", Type: pgtypes.Text, Default: nil, Nullable: true},
{Name: "our_diff_type", Type: pgtypes.Text, Default: nil, Nullable: false},
{Name: "their_value", Type: pgtypes.Text, Default: nil, Nullable: true},
{Name: "their_diff_type", Type: pgtypes.Text, Default: nil, Nullable: false},
{Name: "dolt_conflict_id", Type: pgtypes.Text, Default: nil, Nullable: false},
}
// RootObjectDiff represents a diff between the ancestor value and our/their values. The field name uniquely identifies
// which part of a root object that this diff covers.
type RootObjectDiff struct {
Type *pgtypes.DoltgresType
FromHash string
FieldName string
AncestorValue any
OurValue any
TheirValue any
OurChange RootObjectDiffChange
TheirChange RootObjectDiffChange
}
var _ doltdb.RootObjectDiff = RootObjectDiff{}
// CompareIds implements the interface doltdb.RootObjectDiff.
func (diff RootObjectDiff) CompareIds(ctx context.Context, o doltdb.RootObjectDiff) (int, error) {
other, ok := o.(RootObjectDiff)
if !ok {
return 0, errors.Errorf("root object diff cannot compare with diff of type %T", o)
}
return cmp.Compare(diff.FieldName, other.FieldName), nil
}
// ToRow implements the interface doltdb.RootObjectDiff.
func (diff RootObjectDiff) ToRow(ctx *sql.Context) (_ sql.Row, err error) {
var baseValue any
var ourValue any
var theirValue any
var ourChange any
var theirChange any
if diff.AncestorValue != nil {
baseValue, err = diff.Type.IoOutput(ctx, diff.AncestorValue)
if err != nil {
return nil, err
}
}
if diff.OurValue != nil {
ourValue, err = diff.Type.IoOutput(ctx, diff.OurValue)
if err != nil {
return nil, err
}
}
if diff.TheirValue != nil {
theirValue, err = diff.Type.IoOutput(ctx, diff.TheirValue)
if err != nil {
return nil, err
}
}
switch diff.OurChange {
case RootObjectDiffChange_Added:
ourChange = "added"
case RootObjectDiffChange_Deleted:
ourChange = "deleted"
case RootObjectDiffChange_Modified:
ourChange = "modified"
case RootObjectDiffChange_NoChange:
ourChange = "no_change"
}
switch diff.TheirChange {
case RootObjectDiffChange_Added:
theirChange = "added"
case RootObjectDiffChange_Deleted:
theirChange = "deleted"
case RootObjectDiffChange_Modified:
theirChange = "modified"
case RootObjectDiffChange_NoChange:
ourChange = "no_change"
}
return sql.Row{diff.FromHash, baseValue, ourValue, ourChange, theirValue, theirChange, diff.FieldName}, nil
}
// DiffFromRow converts a row to a conflict diff.
func DiffFromRow(ctx *sql.Context, conflict doltdb.ConflictRootObject, row sql.Row) (_ doltdb.RootObjectDiff, err error) {
if len(row) != len(RootObjectDiffSchema) {
return nil, errors.Newf("expected root object row diff to have %d columns but had %d", len(RootObjectDiffSchema), len(row))
}
fieldName := row[6].(string)
typ := conflict.(Conflict).FieldType(ctx, fieldName)
if typ == nil {
return nil, errors.Newf("cannot find a field named `%s`", fieldName)
}
var baseValue any
var ourValue any
var theirValue any
var ourChange RootObjectDiffChange
var theirChange RootObjectDiffChange
if row[1] != nil {
baseValue, err = typ.IoInput(ctx, row[1].(string))
if err != nil {
return nil, err
}
}
if row[2] != nil {
ourValue, err = typ.IoInput(ctx, row[2].(string))
if err != nil {
return nil, err
}
}
if row[4] != nil {
theirValue, err = typ.IoInput(ctx, row[4].(string))
if err != nil {
return nil, err
}
}
switch row[3].(string) {
case "added":
ourChange = RootObjectDiffChange_Added
case "deleted":
ourChange = RootObjectDiffChange_Deleted
case "modified":
ourChange = RootObjectDiffChange_Modified
case "no_change":
ourChange = RootObjectDiffChange_NoChange
}
switch row[5].(string) {
case "added":
theirChange = RootObjectDiffChange_Added
case "deleted":
theirChange = RootObjectDiffChange_Deleted
case "modified":
theirChange = RootObjectDiffChange_Modified
case "no_change":
ourChange = RootObjectDiffChange_NoChange
}
return RootObjectDiff{
Type: typ,
FromHash: row[0].(string),
FieldName: fieldName,
AncestorValue: baseValue,
OurValue: ourValue,
TheirValue: theirValue,
OurChange: ourChange,
TheirChange: theirChange,
}, nil
}
+102
View File
@@ -0,0 +1,102 @@
// Copyright 2025 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 objinterface
import (
"context"
"fmt"
doltserial "github.com/dolthub/dolt/go/gen/fb/serial"
"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/dolt/go/store/types"
flatbuffers "github.com/dolthub/flatbuffers/v23/go"
"github.com/dolthub/doltgresql/core/storage"
"github.com/dolthub/doltgresql/flatbuffers/gen/serial"
)
// RootObjectSerializer holds function pointers for the serialization of root objects.
type RootObjectSerializer struct {
Bytes func(*serial.RootValue) []byte
RootValueAdd func(builder *flatbuffers.Builder, sequences flatbuffers.UOffsetT)
}
// CreateProllyMap creates and returns a new, empty Prolly map.
func (serializer RootObjectSerializer) CreateProllyMap(ctx context.Context, root RootValue) (prolly.AddressMap, error) {
return prolly.NewEmptyAddressMap(root.NodeStore())
}
// GetProllyMap loads the Prolly map from the given root, using the internal serialization functions.
func (serializer RootObjectSerializer) GetProllyMap(ctx context.Context, root RootValue) (prolly.AddressMap, bool, error) {
val, ok, err := serializer.getValue(ctx, root)
if err != nil || !ok {
return prolly.AddressMap{}, ok, err
}
serialMessage := val.(types.SerialMessage)
node, fileId, err := tree.NodeFromBytes(serialMessage)
if err != nil {
return prolly.AddressMap{}, false, err
}
if fileId != doltserial.AddressMapFileID {
return prolly.AddressMap{}, false, fmt.Errorf("invalid address map identifier, expected %s, got %s", doltserial.AddressMapFileID, fileId)
}
addressMap, err := prolly.NewAddressMap(node, root.NodeStore())
return addressMap, err == nil, err
}
// WriteProllyMap writes the given Prolly map to the root, returning the updated root.
func (serializer RootObjectSerializer) WriteProllyMap(ctx context.Context, root RootValue, val prolly.AddressMap) (RootValue, error) {
return serializer.writeValue(ctx, root, tree.ValueFromNode(val.Node()))
}
// getValue loads the value from the given root, using the internal serialization functions.
func (serializer RootObjectSerializer) getValue(ctx context.Context, root RootValue) (types.Value, bool, error) {
hashBytes := serializer.Bytes(root.GetStorage(ctx).SRV)
if len(hashBytes) == 0 {
return nil, false, nil
}
h := hash.New(hashBytes)
if h.IsEmpty() {
return nil, false, nil
}
val, err := root.VRW().ReadValue(ctx, h)
return val, err == nil && val != nil, err
}
// setHash writes the given hash to storage, returning the updated storage.
func (serializer RootObjectSerializer) setHash(ctx context.Context, st storage.RootStorage, h hash.Hash) (storage.RootStorage, error) {
if len(serializer.Bytes(st.SRV)) > 0 {
ret := st.Clone()
copy(serializer.Bytes(ret.SRV), h[:])
return ret, nil
} else {
return st.Clone(), nil
}
}
// writeValue writes the given value to the root, returning the updated root.
func (serializer RootObjectSerializer) writeValue(ctx context.Context, root RootValue, val types.Value) (RootValue, error) {
ref, err := root.VRW().WriteValue(ctx, val)
if err != nil {
return nil, err
}
newStorage, err := serializer.setHash(ctx, root.GetStorage(ctx), ref.TargetHash())
if err != nil {
return nil, err
}
return root.WithStorage(ctx, newStorage), nil
}
+946
View File
@@ -0,0 +1,946 @@
// 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 core
import (
"bytes"
"context"
"fmt"
"slices"
"sort"
"strconv"
"strings"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/schema"
"github.com/dolthub/dolt/go/store/hash"
"github.com/dolthub/dolt/go/store/prolly/tree"
"github.com/dolthub/dolt/go/store/types"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core/conflicts"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/core/sequences"
"github.com/dolthub/doltgresql/core/storage"
"github.com/dolthub/doltgresql/core/triggers"
)
// DoltgresFeatureVersion is Doltgres' feature version. We use Dolt's feature version added to our own.
var DoltgresFeatureVersion = doltdb.DoltFeatureVersion + 0
// RootValue is Doltgres' implementation of doltdb.RootValue.
type RootValue struct {
vrw types.ValueReadWriter
ns tree.NodeStore
st storage.RootStorage
fkc *doltdb.ForeignKeyCollection // cache the first load
hash hash.Hash // cache the first load
colls []objinterface.Collection // cache the first load
}
var _ doltdb.RootValue = (*RootValue)(nil)
var _ objinterface.RootValue = (*RootValue)(nil)
// CreateDatabaseSchema implements the interface doltdb.RootValue.
func (root *RootValue) CreateDatabaseSchema(ctx context.Context, dbSchema schema.DatabaseSchema) (doltdb.RootValue, error) {
existingSchemas, err := root.st.GetSchemas(ctx)
if err != nil {
return nil, err
}
err = validateSchemaForCreate(existingSchemas, dbSchema)
if err != nil {
return nil, err
}
existingSchemas = append(existingSchemas, dbSchema)
sort.Slice(existingSchemas, func(i, j int) bool {
return existingSchemas[i].Name < existingSchemas[j].Name
})
r, err := root.st.SetSchemas(ctx, existingSchemas)
if err != nil {
return nil, err
}
return root.withStorage(r), nil
}
// DropDatabaseSchema implements the interface doltdb.RootValue.
func (root *RootValue) DropDatabaseSchema(ctx context.Context, dbSchema schema.DatabaseSchema) (doltdb.RootValue, error) {
schemas, err := root.st.GetSchemas(ctx)
if err != nil {
return nil, err
}
found := false
schemaName := dbSchema.Name
for i, s := range schemas {
if strings.EqualFold(s.Name, dbSchema.Name) {
found = true
schemaName = s.Name
// remove this element in the slice
schemas = append(schemas[:i], schemas[i+1:]...)
break
}
}
if !found {
return nil, fmt.Errorf("No schema with the name %s exists", dbSchema.Name)
}
// Check for dangling objects in the schema and reject the drop if there are any
danglingObjects := false
root.IterRootObjects(ctx, func(name doltdb.TableName, table doltdb.RootObject) (stop bool, err error) {
if strings.EqualFold(name.Schema, dbSchema.Name) {
danglingObjects = true
}
return false, nil
})
// check the tables specifically in addition to root objects. This is just an extra paranoid step to avoid
// removing a schema that still has tables.
tableMap, err := root.getTableMap(ctx, schemaName)
if err != nil {
return nil, err
}
tableMap.Iter(ctx, func(name string, addr hash.Hash) (bool, error) {
danglingObjects = true
return true, nil
})
if danglingObjects {
return nil, fmt.Errorf("cannot drop schema %s because other objects depend on it", dbSchema.Name)
}
r, err := root.st.SetSchemas(ctx, schemas)
if err != nil {
return nil, err
}
return root.withStorage(r), nil
}
// validateSchemaForCreate returns an error if a schema with the name given cannot be created
func validateSchemaForCreate(existingSchemas []schema.DatabaseSchema, dbSchema schema.DatabaseSchema) error {
if dbSchema.Name == "" {
return errors.New("Schema name cannot be empty")
}
for _, s := range existingSchemas {
if strings.EqualFold(s.Name, dbSchema.Name) {
return errors.Errorf("A schema with the name %s already exists", dbSchema.Name)
}
}
return nil
}
func (root *RootValue) TableListHash() uint64 {
return 0
}
// DebugString implements the interface doltdb.RootValue.
func (root *RootValue) DebugString(ctx context.Context, transitive bool) string {
var buf bytes.Buffer
buf.WriteString(root.st.DebugString(ctx))
if transitive {
buf.WriteString("\nTables:")
root.IterTables(ctx, func(name doltdb.TableName, table *doltdb.Table, sch schema.Schema) (stop bool, err error) {
buf.WriteString("\nTable ")
buf.WriteString(name.Name)
buf.WriteString(":\n")
buf.WriteString(table.DebugString(ctx, root.ns))
return false, nil
})
buf.WriteString("\nSchemas:")
schemas, err := root.GetDatabaseSchemas(ctx)
if err != nil {
return ""
}
for _, schema := range schemas {
buf.WriteString("\nSchema ")
buf.WriteString(schema.Name)
}
fkc, err := root.GetForeignKeyCollection(ctx)
if err == nil && fkc.Count() > 0 {
buf.WriteString("\nForeign Keys:")
fkc.Iter(func(fk doltdb.ForeignKey) (stop bool, err error) {
buf.WriteString("\n")
buf.WriteString(fk.Name)
buf.WriteString(": ")
buf.WriteString(fk.TableName.String())
buf.WriteString("(")
for i, tag := range fk.ReferencedTableColumns {
if i > 0 {
buf.WriteString(",")
}
buf.WriteString(strconv.Itoa(int(tag)))
}
buf.WriteString(") ON ")
buf.WriteString(fk.ReferencedTableName.String())
buf.WriteString("(")
for i, tag := range fk.ReferencedTableColumns {
if i > 0 {
buf.WriteString(",")
}
buf.WriteString(strconv.Itoa(int(tag)))
}
buf.WriteString(")\n")
return false, nil
})
}
seqs, err := sequences.LoadSequences(ctx, root)
if err != nil {
return "error loading sequences: " + err.Error()
}
seqs.IterateSequences(ctx, func(seq *sequences.Sequence) (stop bool, err error) {
buf.WriteString("Sequence ")
buf.WriteString(seq.Name().String())
buf.WriteString(": ")
buf.WriteString("OwnerColumn: ")
buf.WriteString(seq.OwnerColumn)
buf.WriteString(" OwnerTable: ")
buf.WriteString(seq.OwnerTable.AsId().String())
buf.WriteString(" Increment: ")
buf.WriteString(strconv.FormatInt(seq.Increment, 10))
buf.WriteString(" Current: ")
buf.WriteString(strconv.FormatInt(seq.Current, 10))
buf.WriteString(" Start: ")
buf.WriteString(strconv.FormatInt(seq.Start, 10))
buf.WriteString(" Min: ")
buf.WriteString(strconv.FormatInt(seq.Minimum, 10))
buf.WriteString(" Max: ")
buf.WriteString(strconv.FormatInt(seq.Maximum, 10))
buf.WriteString(" Cache: ")
buf.WriteString(strconv.FormatInt(seq.Cache, 10))
buf.WriteString(" Cycle: ")
buf.WriteString(strconv.FormatBool(seq.Cycle))
buf.WriteString(" DataTypeID: ")
buf.WriteString(seq.DataTypeID.AsId().String())
buf.WriteString(" DataTypeName: ")
buf.WriteString(seq.DataTypeID.AsId().String())
buf.WriteString("\n")
return false, nil
})
}
return buf.String()
}
// FilterRootObjectNames implements the interface doltdb.RootValue.
func (root *RootValue) FilterRootObjectNames(ctx context.Context, names []doltdb.TableName) ([]doltdb.TableName, error) {
var returnNames []doltdb.TableName
for _, name := range names {
_, _, objID, err := rootobject.ResolveName(ctx, root, name)
if err != nil {
return nil, err
}
if objID != objinterface.RootObjectID_None {
returnNames = append(returnNames, name)
}
}
return returnNames, nil
}
// GetAllTableNames implements the interface doltdb.RootValue.
func (root *RootValue) GetAllTableNames(ctx context.Context, includeRootObjects bool) ([]doltdb.TableName, error) {
var names []doltdb.TableName
existingSchemas, err := root.st.GetSchemas(ctx)
if err != nil {
return nil, err
}
for _, existingSchema := range existingSchemas {
tableMap, err := root.getTableMap(ctx, existingSchema.Name)
if err != nil {
return nil, err
}
err = tableMap.Iter(ctx, func(name string, _ hash.Hash) (bool, error) {
names = append(names, doltdb.TableName{
Name: name,
Schema: existingSchema.Name,
})
return false, nil
})
if err != nil {
return nil, err
}
}
if includeRootObjects {
colls, err := rootobject.LoadAllCollections(ctx, root)
if err != nil {
return nil, err
}
for _, coll := range colls {
err = coll.IterIDs(ctx, func(identifier id.Id) (stop bool, err error) {
names = append(names, coll.IDToTableName(identifier))
return false, nil
})
if err != nil {
return nil, err
}
}
}
return names, nil
}
// GetCollation implements the interface doltdb.RootValue.
func (root *RootValue) GetCollation(ctx context.Context) (schema.Collation, error) {
return root.st.GetCollation(ctx)
}
// GetConflictRootObject implements the interface doltdb.RootValue. This function is specifically used to find conflicts,
// as the standard GetRootObject will not look at the conflicts collection.
func (root *RootValue) GetConflictRootObject(ctx context.Context, tName doltdb.TableName) (doltdb.ConflictRootObject, bool, error) {
return rootobject.GetRootObjectConflicts(ctx, root, tName)
}
// GetConflictRootObjects implements the interface doltdb.RootValue.
func (root *RootValue) GetConflictRootObjects(ctx context.Context) ([]doltdb.ConflictRootObject, error) {
collection, err := conflicts.LoadConflicts(ctx, root)
if err != nil {
return nil, err
}
var allConflicts []doltdb.ConflictRootObject
_ = collection.IterAll(ctx, func(rootObj objinterface.RootObject) (stop bool, err error) {
allConflicts = append(allConflicts, rootObj.(doltdb.ConflictRootObject))
return false, nil
})
return allConflicts, nil
}
// GetDatabaseSchemas implements the interface doltdb.RootValue.
func (root *RootValue) GetDatabaseSchemas(ctx context.Context) ([]schema.DatabaseSchema, error) {
existingSchemas, err := root.st.GetSchemas(ctx)
if err != nil {
return nil, err
}
return existingSchemas, nil
}
// GetFeatureVersion implements the interface doltdb.RootValue.
func (root *RootValue) GetFeatureVersion(ctx context.Context) (ver doltdb.FeatureVersion, ok bool, err error) {
return root.st.GetFeatureVersion(), true, nil
}
// GetForeignKeyCollection implements the interface doltdb.RootValue.
func (root *RootValue) GetForeignKeyCollection(ctx context.Context) (*doltdb.ForeignKeyCollection, error) {
if root.fkc == nil {
fkMap, ok, err := root.st.GetForeignKeys(ctx, root.vrw)
if err != nil {
return nil, err
}
if !ok {
return doltdb.NewForeignKeyCollection()
}
root.fkc, err = doltdb.DeserializeForeignKeys(ctx, root.vrw.Format(), fkMap)
if err != nil {
return nil, err
}
}
return root.fkc.Copy(), nil
}
// GetRootObject implements the interface doltdb.RootValue.
func (root *RootValue) GetRootObject(ctx context.Context, tName doltdb.TableName) (doltdb.RootObject, bool, error) {
return rootobject.GetRootObject(ctx, root, tName)
}
// GetStorage returns the underlying storage.
func (root *RootValue) GetStorage(ctx context.Context) storage.RootStorage {
return root.st
}
// GetTable implements the interface doltdb.RootValue.
func (root *RootValue) GetTable(ctx context.Context, tName doltdb.TableName) (*doltdb.Table, bool, error) {
tableMap, err := root.getTableMap(ctx, tName.Schema)
if err != nil {
return nil, false, err
}
addr, err := tableMap.Get(ctx, tName.Name)
if err != nil {
return nil, false, err
}
return doltdb.GetTable(ctx, root, addr)
}
// GetTableHash implements the interface doltdb.RootValue.
func (root *RootValue) GetTableHash(ctx context.Context, tName doltdb.TableName) (hash.Hash, bool, error) {
// Check the tables first
tableMap, err := root.getTableMap(ctx, tName.Schema)
if err != nil {
return hash.Hash{}, false, err
}
tVal, err := tableMap.Get(ctx, tName.Name)
if err != nil {
return hash.Hash{}, false, err
}
if !tVal.IsEmpty() {
return tVal, true, nil
}
// Then check the root objects
_, rawID, objID, err := rootobject.ResolveName(ctx, root, tName)
if err != nil {
return hash.Hash{}, false, err
}
if objID == objinterface.RootObjectID_None {
return hash.Hash{}, false, nil
}
coll, err := rootobject.LoadCollection(ctx, root, objID)
if err != nil {
return hash.Hash{}, false, err
}
obj, ok, err := coll.GetRootObject(ctx, rawID)
if err != nil || !ok {
return hash.Hash{}, false, err
}
h, err := obj.HashOf(ctx)
return h, err == nil && !h.IsEmpty(), err
}
// GetTableNames implements the interface doltdb.RootValue.
func (root *RootValue) GetTableNames(ctx context.Context, schemaName string, includeRootObjects bool) ([]string, error) {
tableMap, err := root.getTableMap(ctx, schemaName)
if err != nil {
return nil, err
}
var names []string
err = tableMap.Iter(ctx, func(name string, _ hash.Hash) (bool, error) {
names = append(names, name)
return false, nil
})
if err != nil {
return nil, err
}
if includeRootObjects {
if root.colls == nil {
root.colls, err = rootobject.LoadAllCollections(ctx, root)
if err != nil {
return nil, err
}
}
for _, coll := range root.colls {
err = coll.IterIDs(ctx, func(identifier id.Id) (stop bool, err error) {
tName := coll.IDToTableName(identifier)
if tName.Schema == schemaName {
names = append(names, tName.Name)
}
return false, nil
})
if err != nil {
return nil, err
}
}
}
return names, nil
}
// GetTableSchemaHash implements the interface doltdb.RootValue.
func (root *RootValue) GetTableSchemaHash(ctx context.Context, tName doltdb.TableName) (hash.Hash, error) {
// TODO: look into faster ways to get the table schema hash without having to deserialize the table first
tab, ok, err := root.GetTable(ctx, tName)
if err != nil {
return hash.Hash{}, err
}
if !ok {
return hash.Hash{}, nil
}
return tab.GetSchemaHash(ctx)
}
// HashOf implements the interface doltdb.RootValue.
func (root *RootValue) HashOf() (hash.Hash, error) {
if root.hash.IsEmpty() {
var err error
root.hash, err = root.st.NomsValue().Hash(root.vrw.Format())
if err != nil {
return hash.Hash{}, nil
}
}
return root.hash, nil
}
// HasTable implements the interface doltdb.RootValue.
func (root *RootValue) HasTable(ctx context.Context, tName doltdb.TableName) (bool, error) {
// Check the tables first
tableMap, err := root.st.GetTablesMap(ctx, root.vrw, root.ns, tName.Schema)
if err != nil {
return false, err
}
a, err := tableMap.Get(ctx, tName.Name)
if err != nil {
return false, err
}
if !a.IsEmpty() {
return true, nil
}
// Then check the root objects
_, _, objID, err := rootobject.ResolveName(ctx, root, tName)
if err != nil {
return false, err
}
return objID != objinterface.RootObjectID_None, nil
}
// IterRootObjects implements the interface doltdb.RootValue.
func (root *RootValue) IterRootObjects(ctx context.Context, cb func(name doltdb.TableName, table doltdb.RootObject) (stop bool, err error)) error {
colls, err := rootobject.LoadAllCollections(ctx, root)
if err != nil {
return err
}
for _, coll := range colls {
err = coll.IterAll(ctx, func(rootObj objinterface.RootObject) (stop bool, err error) {
return cb(rootObj.Name(), rootObj)
})
if err != nil {
return err
}
}
return nil
}
// IterTables implements the interface doltdb.RootValue.
func (root *RootValue) IterTables(ctx context.Context, cb func(name doltdb.TableName, table *doltdb.Table, sch schema.Schema) (stop bool, err error)) error {
schemaNames, err := schemaNames(ctx, root)
if err != nil {
return err
}
for _, schemaName := range schemaNames {
tm, err := root.getTableMap(ctx, schemaName)
if err != nil {
return err
}
err = tm.Iter(ctx, func(name string, addr hash.Hash) (bool, error) {
tbl, exists, err := doltdb.GetTable(ctx, root, addr)
if !exists {
return false, errors.Errorf("cannot find table %s from addr", name)
}
if err != nil {
return true, err
}
sch, err := tbl.GetSchema(ctx)
if err != nil {
return true, err
}
return cb(doltdb.TableName{
Name: name,
Schema: schemaName,
}, tbl, sch)
})
if err != nil {
return err
}
}
return nil
}
// schemaNames returns all names of all schemas which may have tables
func schemaNames(ctx context.Context, root doltdb.RootValue) ([]string, error) {
dbSchemas, err := root.GetDatabaseSchemas(ctx)
if err != nil {
return nil, err
}
schNames := make([]string, len(dbSchemas)+1)
for i, dbSchema := range dbSchemas {
schNames[i] = dbSchema.Name
}
return schNames, nil
}
// NodeStore implements the interface doltdb.RootValue.
func (root *RootValue) NodeStore() tree.NodeStore {
return root.ns
}
// NomsValue implements the interface doltdb.RootValue.
func (root *RootValue) NomsValue() types.Value {
return root.st.NomsValue()
}
// PutForeignKeyCollection implements the interface doltdb.RootValue.
func (root *RootValue) PutForeignKeyCollection(ctx context.Context, fkc *doltdb.ForeignKeyCollection) (doltdb.RootValue, error) {
value, err := doltdb.SerializeForeignKeys(ctx, root.vrw, fkc)
if err != nil {
return nil, err
}
newStorage, err := root.st.SetForeignKeyMap(ctx, root.vrw, value)
if err != nil {
return nil, err
}
return root.withStorage(newStorage), nil
}
// PutRootObject implements the interface doltdb.RootValue.
func (root *RootValue) PutRootObject(ctx context.Context, tName doltdb.TableName, rootObj doltdb.RootObject) (doltdb.RootValue, error) {
if rootObj == nil {
return root, nil
}
return rootobject.PutRootObject(ctx, root, tName, rootObj.(objinterface.RootObject))
}
// PutTable implements the interface doltdb.RootValue.
func (root *RootValue) PutTable(ctx context.Context, tName doltdb.TableName, table *doltdb.Table) (doltdb.RootValue, error) {
// TODO: modify owned sequences based on schema changes
tableRef, err := doltdb.RefFromNomsTable(ctx, table)
if err != nil {
return nil, err
}
return root.putTable(ctx, tName, tableRef)
}
// RemoveTables implements the interface doltdb.RootValue.
func (root *RootValue) RemoveTables(
ctx context.Context,
skipFKHandling bool,
allowDroppingFKReferenced bool,
originalTables ...doltdb.TableName,
) (doltdb.RootValue, error) {
if len(originalTables) == 0 {
return root, nil
}
tableMaps := make(map[string]storage.RootTableMap)
deletedObjIds := make(map[objinterface.RootObjectID]struct{})
var tables []doltdb.TableName
var rootObjNames []struct {
rawID id.Id
objID objinterface.RootObjectID
}
for _, name := range originalTables {
// Split into tables and root objects
tableMap, ok := tableMaps[name.Schema]
if !ok {
var err error
tableMap, err = root.getTableMap(ctx, name.Schema)
if err != nil {
return nil, err
}
tableMaps[name.Schema] = tableMap
}
tableHash, err := tableMap.Get(ctx, name.Name)
if err != nil {
return nil, err
}
if !tableHash.IsEmpty() {
tables = append(tables, name)
continue
}
// Table wasn't in the table map, so we'll check our root objects
_, rawID, objID, err := rootobject.ResolveName(ctx, root, name)
if err != nil {
return nil, err
}
if objID == objinterface.RootObjectID_None {
return nil, errors.Errorf("%w: '%s'", doltdb.ErrTableNotFound, name)
}
rootObjNames = append(rootObjNames, struct {
rawID id.Id
objID objinterface.RootObjectID
}{rawID: rawID, objID: objID})
deletedObjIds[objID] = struct{}{}
}
newRoot := root
// First we'll handle regular table names
if len(tables) > 0 {
edits := make([]storage.TableEdit, len(tables))
for i, name := range tables {
edits[i].Name = name
}
newStorage, err := newRoot.st.EditTablesMap(ctx, newRoot.vrw, newRoot.ns, edits)
if err != nil {
return nil, err
}
newRoot = newRoot.withStorage(newStorage)
// Sequences should be dropped when their owning tables are dropped
seqColl, err := sequences.LoadSequences(ctx, newRoot)
if err != nil {
return nil, err
}
for _, tableName := range tables {
seqs, err := seqColl.GetSequencesWithTable(ctx, tableName)
if err != nil {
return nil, err
}
if len(seqs) > 0 {
for _, seq := range seqs {
deletedObjIds[objinterface.RootObjectID_Sequences] = struct{}{}
if err = seqColl.DropSequence(ctx, seq.Id); err != nil {
return nil, err
}
// If we're deleting sequences here, then we need to ensure that we don't try to delete them later too
rootObjNames = slices.DeleteFunc(rootObjNames, func(s struct {
rawID id.Id
objID objinterface.RootObjectID
}) bool {
return s.rawID == seq.Id.AsId()
})
}
}
}
retRoot, err := seqColl.UpdateRoot(ctx, newRoot)
if err != nil {
return nil, err
}
newRoot = retRoot.(*RootValue)
// Triggers should also be dropped when their target tables are dropped
trigColl, err := triggers.LoadTriggers(ctx, newRoot)
if err != nil {
return nil, err
}
for _, tableName := range tables {
for _, trigID := range trigColl.GetTriggerIDsForTable(ctx, id.NewTable(tableName.Schema, tableName.Name)) {
deletedObjIds[objinterface.RootObjectID_Triggers] = struct{}{}
if err = trigColl.DropTrigger(ctx, trigID); err != nil {
return nil, err
}
// If we're deleting sequences here, then we need to ensure that we don't try to delete them later too
rootObjNames = slices.DeleteFunc(rootObjNames, func(s struct {
rawID id.Id
objID objinterface.RootObjectID
}) bool {
return s.rawID == trigID.AsId()
})
}
}
retRoot, err = trigColl.UpdateRoot(ctx, newRoot)
if err != nil {
return nil, err
}
newRoot = retRoot.(*RootValue)
// Handle foreign keys
if !skipFKHandling {
fkc, err := newRoot.GetForeignKeyCollection(ctx)
if err != nil {
return nil, err
}
if allowDroppingFKReferenced {
err = fkc.RemoveAndUnresolveTables(ctx, newRoot, tables...)
} else {
err = fkc.RemoveTables(ctx, tables...)
}
if err != nil {
return nil, err
}
newRootInterface, err := newRoot.PutForeignKeyCollection(ctx, fkc)
if err != nil {
return nil, err
}
newRoot = newRootInterface.(*RootValue)
}
}
// Then we'll handle root objects
for _, rootObjName := range rootObjNames {
newRootInt, err := rootobject.RemoveRootObject(ctx, newRoot, rootObjName.rawID, rootObjName.objID)
if err != nil {
return nil, err
}
newRoot = newRootInt.(*RootValue)
}
// We're not updating the cached values for the deleted object IDs, so we'll remove them
if sqlCtx, ok := ctx.(*sql.Context); ok && len(deletedObjIds) > 0 {
if cv, err := getContextValues(sqlCtx); err == nil {
for deletedObjId := range deletedObjIds {
cv.clear(deletedObjId)
}
}
}
return newRoot, nil
}
// RenameTable implements the interface doltdb.RootValue.
func (root *RootValue) RenameTable(ctx context.Context, oldName, newName doltdb.TableName) (doltdb.RootValue, error) {
_, rawOldID, objID, err := rootobject.ResolveName(ctx, root, oldName)
if err != nil {
return nil, err
}
if objID == objinterface.RootObjectID_None {
newStorage, err := root.st.EditTablesMap(ctx, root.vrw, root.ns, []storage.TableEdit{{OldName: oldName, Name: newName}})
if err != nil {
return nil, err
}
newRoot := root.withStorage(newStorage)
collection, err := sequences.LoadSequences(ctx, newRoot)
if err != nil {
return nil, err
}
seqs, err := collection.GetSequencesWithTable(ctx, oldName)
if err != nil {
return nil, err
}
for _, seq := range seqs {
seq.OwnerTable = id.NewTable(seq.OwnerTable.SchemaName(), newName.Name)
}
return collection.UpdateRoot(ctx, newRoot)
} else {
coll, err := rootobject.LoadCollection(ctx, root, objID)
if err != nil {
return nil, err
}
rawNewID := coll.TableNameToID(newName)
if err = coll.RenameRootObject(ctx, rawOldID, rawNewID); err != nil {
return nil, err
}
return coll.UpdateRoot(ctx, root)
}
}
// ResolveRootValue implements the interface doltdb.RootValue.
func (root *RootValue) ResolveRootValue(ctx context.Context) (doltdb.RootValue, error) {
return root, nil
}
// ResolveTableName implements the interface doltdb.RootValue.
func (root *RootValue) ResolveTableName(ctx context.Context, tName doltdb.TableName) (string, bool, error) {
// Check the tables first
tableMap, err := root.getTableMap(ctx, tName.Schema)
if err != nil {
return "", false, err
}
a, err := tableMap.Get(ctx, tName.Name)
if err != nil {
return "", false, err
}
if !a.IsEmpty() {
return tName.Name, true, nil
}
found := false
resolvedName := tName.Name
err = tableMap.Iter(ctx, func(name string, addr hash.Hash) (bool, error) {
if !found && strings.EqualFold(tName.Name, name) {
resolvedName = name
found = true
}
return false, nil
})
if err != nil {
return "", false, nil
}
if found {
return resolvedName, true, nil
}
// Then check the root objects
resolvedTableName, _, objID, err := rootobject.ResolveName(ctx, root, tName)
if err != nil {
return "", false, err
}
return resolvedTableName.Name, objID != objinterface.RootObjectID_None, nil
}
// SetCollation implements the interface doltdb.RootValue.
func (root *RootValue) SetCollation(ctx context.Context, collation schema.Collation) (doltdb.RootValue, error) {
newStorage, err := root.st.SetCollation(ctx, collation)
if err != nil {
return nil, err
}
return root.withStorage(newStorage), nil
}
// SetFeatureVersion implements the interface doltdb.RootValue.
func (root *RootValue) SetFeatureVersion(v doltdb.FeatureVersion) (doltdb.RootValue, error) {
newStorage, err := root.st.SetFeatureVersion(v)
if err != nil {
return nil, err
}
return root.withStorage(newStorage), nil
}
// SetTableHash implements the interface doltdb.RootValue.
func (root *RootValue) SetTableHash(ctx context.Context, tName doltdb.TableName, h hash.Hash) (doltdb.RootValue, error) {
// TODO: error for root object tables?
val, err := root.vrw.ReadValue(ctx, h)
if err != nil {
return nil, err
}
ref, err := types.NewRef(val, root.vrw.Format())
if err != nil {
return nil, err
}
return root.putTable(ctx, tName, ref)
}
// VRW implements the interface doltdb.RootValue.
func (root *RootValue) VRW() types.ValueReadWriter {
return root.vrw
}
// WithStorage returns a new root value with the given storage.
func (root *RootValue) WithStorage(ctx context.Context, st storage.RootStorage) objinterface.RootValue {
return root.withStorage(st)
}
// getTableMap returns the tableMap for this root.
func (root *RootValue) getTableMap(ctx context.Context, schemaName string) (storage.RootTableMap, error) {
if schemaName == "" {
schemaName = doltdb.DefaultSchemaName
}
return root.st.GetTablesMap(ctx, root.vrw, root.ns, schemaName)
}
// putTable provides an inner implementation that is called from multiple other functions.
func (root *RootValue) putTable(ctx context.Context, tName doltdb.TableName, ref types.Ref) (doltdb.RootValue, error) {
if !doltdb.IsValidTableName(tName.Name) {
panic("Don't attempt to put a table with a name that fails the IsValidTableName check")
}
newStorage, err := root.st.EditTablesMap(ctx, root.VRW(), root.NodeStore(), []storage.TableEdit{{Name: tName, Ref: &ref}})
if err != nil {
return nil, err
}
return root.withStorage(newStorage), nil
}
// withStorage returns a new root value with the given storage.
func (root *RootValue) withStorage(st storage.RootStorage) *RootValue {
return &RootValue{
vrw: root.vrw,
ns: root.ns,
st: st,
}
}
+49
View File
@@ -0,0 +1,49 @@
// 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 core
import (
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/resolve"
"github.com/dolthub/go-mysql-server/sql"
)
// GetCurrentSchema returns the current schema used by the context. Defaults to "public" if the context does not specify
// a schema.
func GetCurrentSchema(ctx *sql.Context) (string, error) {
_, root, err := GetRootFromContext(ctx)
if err != nil {
return "", nil
}
return resolve.FirstExistingSchemaOnSearchPath(ctx, root)
}
// GetSchemaName returns the schema name if there is any exist.
// If the given schema is not empty, it's returned.
// If it is empty, uses given database to get schema name if it's DatabaseSchema.
// If it's not of DatabaseSchema type or the schema name of it is empty,
// it tries retrieving the current schema used by the context.
// Defaults to "public" if the context does not specify a schema.
func GetSchemaName(ctx *sql.Context, db sql.Database, schemaName string) (string, error) {
if schemaName == "" {
if schema, isSch := db.(sql.DatabaseSchema); isSch {
schemaName = schema.SchemaName()
}
if schemaName == "" {
return GetCurrentSchema(ctx)
}
}
return schemaName, nil
}
+111
View File
@@ -0,0 +1,111 @@
// Copyright 2025 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 core
import (
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core/id"
)
// sequenceIDListener implements the performer and validator functions for sequences.
type sequenceIDListener struct{}
var _ id.Listener = sequenceIDListener{}
// OperationValidator is the internal ID validator for sequences.
func (sequenceIDListener) OperationValidator(ctx *sql.Context, operation id.Operation, databaseName string, originalID id.Id, newID id.Id) error {
switch originalID.Section() {
case id.Section_ColumnDefault, id.Section_Table:
switch operation {
case id.Operation_Rename, id.Operation_Delete, id.Operation_Delete_Cascade:
return nil
default:
return errors.Errorf("sequence validator received unexpected operation `%s`", operation.String())
}
default:
return errors.Errorf("sequence validator received unexpected section `%s`", originalID.Section().String())
}
}
// OperationPerformer is the internal ID performer for sequences, which modifies the sequence collection in response to
// the given operation and section of the original ID.
func (sequenceIDListener) OperationPerformer(ctx *sql.Context, operation id.Operation, databaseName string, originalID id.Id, newID id.Id) error {
switch originalID.Section() {
case id.Section_ColumnDefault:
originalIDCol := id.ColumnDefault(originalID)
switch operation {
case id.Operation_Rename:
return nil
case id.Operation_Delete, id.Operation_Delete_Cascade:
collection, err := GetSequencesCollectionFromContext(ctx, databaseName)
if err != nil {
return err
}
sequences, err := collection.GetSequencesWithTable(ctx, doltdb.TableName{
Name: originalIDCol.TableName(),
Schema: originalIDCol.SchemaName(),
})
if err != nil {
return err
}
for _, sequence := range sequences {
if sequence.OwnerColumn == originalIDCol.ColumnName() {
if err = collection.DropSequence(ctx, sequence.Id); err != nil {
return err
}
}
}
return nil
default:
return errors.Errorf("sequence performer received unexpected operation `%s`", operation.String())
}
case id.Section_Table:
originalIDTable := id.Table(originalID)
switch operation {
case id.Operation_Rename, id.Operation_Delete, id.Operation_Delete_Cascade:
collection, err := GetSequencesCollectionFromContext(ctx, databaseName)
if err != nil {
return err
}
sequences, err := collection.GetSequencesWithTable(ctx, doltdb.TableName{
Name: originalIDTable.TableName(),
Schema: originalIDTable.SchemaName(),
})
if err != nil {
return err
}
for _, sequence := range sequences {
if err = collection.DropSequence(ctx, sequence.Id); err != nil {
return err
}
if operation == id.Operation_Rename {
sequence.OwnerTable = id.Table(newID)
if err = collection.CreateSequence(ctx, sequence); err != nil {
return err
}
}
}
return nil
default:
return errors.Errorf("sequence performer received unexpected operation `%s`", operation.String())
}
default:
return errors.Errorf("sequence performer received unexpected section `%s`", originalID.Section().String())
}
}
+474
View File
@@ -0,0 +1,474 @@
// 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 sequences
import (
"context"
"fmt"
"io"
"math"
"sort"
"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/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
)
// Collection contains a collection of sequences.
type Collection struct {
accessedMap map[id.Sequence]*Sequence // Whenever a sequence is accessed, it is added to the access map for faster retrieval
underlyingMap prolly.AddressMap
ns tree.NodeStore
}
// Persistence controls the persistence of a Sequence.
type Persistence uint8
const (
Persistence_Permanent Persistence = 0
Persistence_Temporary Persistence = 1
Persistence_Unlogged Persistence = 2
)
// Sequence represents a single sequence within the pg_sequence table.
type Sequence struct {
Id id.Sequence
DataTypeID id.Type
Persistence Persistence
Start int64
Current int64
Increment int64
Minimum int64
Maximum int64
Cache int64
Cycle bool
IsAtEnd bool
HasBeenCalled bool
OwnerTable id.Table
OwnerColumn string
}
var _ objinterface.Collection = (*Collection)(nil)
var _ objinterface.RootObject = (*Sequence)(nil)
var _ doltdb.RootObject = (*Sequence)(nil)
// GetSequence returns the sequence with the given schema and name. Returns nil if the sequence cannot be found.
func (pgs *Collection) GetSequence(ctx context.Context, name id.Sequence) (*Sequence, error) {
return pgs.getSequence(ctx, name)
}
// GetSequencesWithTable returns all sequences with the given table as the owner.
func (pgs *Collection) GetSequencesWithTable(ctx context.Context, name doltdb.TableName) ([]*Sequence, error) {
// For now, this function isn't used in a critical path, so we're not too worried about performance
if err := pgs.cacheAllSequences(ctx); err != nil {
return nil, err
}
var seqs []*Sequence
nameID := id.NewTable(name.Schema, name.Name)
for _, seq := range pgs.accessedMap {
if seq.OwnerTable == nameID {
seqs = append(seqs, seq)
}
}
return seqs, nil
}
// GetAllSequences returns a map containing all sequences in the collection, grouped by the schema they're contained in.
// Each sequence array is also sorted by the sequence name.
func (pgs *Collection) GetAllSequences(ctx context.Context) (sequences map[string][]*Sequence, schemaNames []string, totalCount int, err error) {
// For now, this function is only used by the "reg" types, so we're not too worried about performance
if err = pgs.cacheAllSequences(ctx); err != nil {
return nil, nil, 0, err
}
totalCount = len(pgs.accessedMap)
schemaNamesMap := make(map[string]struct{})
sequences = make(map[string][]*Sequence)
for seqID, seq := range pgs.accessedMap {
schemaNamesMap[seqID.SchemaName()] = struct{}{}
sequences[seqID.SchemaName()] = append(sequences[seqID.SchemaName()], seq)
}
// Sort the sequences in the sequence map
for _, seqs := range sequences {
sort.Slice(seqs, func(i, j int) bool {
return seqs[i].Id < seqs[j].Id
})
}
// Create and sort the schema names
schemaNames = make([]string, 0, len(schemaNamesMap))
for name := range schemaNamesMap {
schemaNames = append(schemaNames, name)
}
sort.Slice(schemaNames, func(i, j int) bool {
return schemaNames[i] < schemaNames[j]
})
return
}
// HasSequence returns whether the sequence is present.
func (pgs *Collection) HasSequence(ctx context.Context, name id.Sequence) bool {
// Subsequent loads are cached
if _, ok := pgs.accessedMap[name]; ok {
return true
}
// The initial load is from the internal map
ok, err := pgs.underlyingMap.Has(ctx, string(name))
if err == nil && ok {
return true
}
return false
}
// CreateSequence creates a new sequence.
func (pgs *Collection) CreateSequence(ctx context.Context, seq *Sequence) error {
// Ensure that the sequence does not already exist
if _, ok := pgs.accessedMap[seq.Id]; ok {
return errors.Errorf(`relation "%s" already exists`, seq.Id.SequenceName())
}
if ok, err := pgs.underlyingMap.Has(ctx, string(seq.Id)); err != nil {
return err
} else if ok {
return errors.Errorf(`relation "%s" already exists`, seq.Id.SequenceName())
}
// Add it to our cache, which will be emptied when we do anything permanent
pgs.accessedMap[seq.Id] = seq
return nil
}
// DropSequence drops existing sequences.
func (pgs *Collection) DropSequence(ctx context.Context, names ...id.Sequence) (err error) {
// We need to clear the cache so that we only need to worry about the underlying map
if err = pgs.writeCache(ctx); err != nil {
return err
}
for _, name := range names {
if ok, err := pgs.underlyingMap.Has(ctx, string(name)); err != nil {
return err
} else if !ok {
return errors.Errorf(`sequence "%s" does not exist`, name.SequenceName())
}
}
// Now we'll remove the sequences from the underlying map
mapEditor := pgs.underlyingMap.Editor()
for _, name := range names {
if err = mapEditor.Delete(ctx, string(name)); err != nil {
return err
}
}
flushed, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pgs.underlyingMap = flushed
return nil
}
// resolveName returns the fully resolved name of the given sequence. Returns an error if the name is ambiguous.
func (pgs *Collection) resolveName(ctx context.Context, schemaName string, sequenceName string) (id.Sequence, error) {
if err := pgs.writeCache(ctx); err != nil {
return id.NullSequence, err
}
count, err := pgs.underlyingMap.Count()
if err != nil || count == 0 {
return id.NullSequence, err
}
// First check for an exact match
inputID := id.NewSequence(schemaName, sequenceName)
ok, err := pgs.underlyingMap.Has(ctx, string(inputID))
if err != nil {
return id.NullSequence, err
} else if ok {
return inputID, nil
}
// Now we'll iterate over all the names
var resolvedID id.Sequence
if len(schemaName) > 0 {
err = pgs.underlyingMap.IterAll(ctx, func(k string, _ hash.Hash) error {
seqID := id.Sequence(k)
if strings.EqualFold(sequenceName, seqID.SequenceName()) &&
strings.EqualFold(schemaName, seqID.SchemaName()) {
if resolvedID.IsValid() {
return fmt.Errorf("`%s.%s` is ambiguous, matches `%s.%s` and `%s.%s`",
schemaName, sequenceName, seqID.SchemaName(), seqID.SequenceName(), resolvedID.SchemaName(), resolvedID.SequenceName())
}
resolvedID = seqID
}
return nil
})
if err != nil {
return id.NullSequence, err
}
} else {
err = pgs.underlyingMap.IterAll(ctx, func(k string, _ hash.Hash) error {
seqID := id.Sequence(k)
if strings.EqualFold(sequenceName, seqID.SequenceName()) {
if resolvedID.IsValid() {
return fmt.Errorf("`%s` is ambiguous, matches `%s.%s` and `%s.%s`",
sequenceName, seqID.SchemaName(), seqID.SequenceName(), resolvedID.SchemaName(), resolvedID.SequenceName())
}
resolvedID = seqID
}
return nil
})
if err != nil {
return id.NullSequence, err
}
}
return resolvedID, nil
}
// iterateIDs iterates over all sequence IDs in the collection.
func (pgs *Collection) iterateIDs(ctx context.Context, f func(seqID id.Sequence) (stop bool, err error)) (err error) {
if err = pgs.writeCache(ctx); err != nil {
return err
}
return pgs.underlyingMap.IterAll(ctx, func(k string, _ hash.Hash) error {
seqID := id.Sequence(k)
stop, err := f(seqID)
if err != nil {
return err
} else if stop {
return io.EOF
} else {
return nil
}
})
}
// IterateSequences iterates over all sequences in the collection.
func (pgs *Collection) IterateSequences(ctx context.Context, f func(seq *Sequence) (stop bool, err error)) (err error) {
// For now, this function isn't used in a critical path, so we're not too worried about performance
if err = pgs.cacheAllSequences(ctx); err != nil {
return err
}
for _, seq := range pgs.accessedMap {
if stop, err := f(seq); err != nil {
return err
} else if stop {
break
}
}
return nil
}
// NextVal returns the next value in the sequence.
func (pgs *Collection) NextVal(ctx context.Context, name id.Sequence) (int64, error) {
seq, err := pgs.getSequence(ctx, name)
if err != nil {
return 0, err
}
if seq == nil {
return 0, errors.Errorf(`relation "%s" does not exist`, name.SequenceName())
}
return seq.nextValForSequence()
}
// SetVal sets the sequence to the
func (pgs *Collection) SetVal(ctx context.Context, name id.Sequence, newValue int64, autoAdvance bool) error {
seq, err := pgs.getSequence(ctx, name)
if err != nil {
return err
}
if seq == nil {
return errors.Errorf(`relation "%s" does not exist`, name.SequenceName())
}
if newValue < seq.Minimum || newValue > seq.Maximum {
return errors.Errorf(`setval: value %d is out of bounds for sequence "%s" (%d..%d)`,
newValue, name, seq.Minimum, seq.Maximum)
}
seq.Current = newValue
seq.IsAtEnd = false
seq.HasBeenCalled = false
if autoAdvance {
_, err := seq.nextValForSequence()
return err
}
return nil
}
// Clone returns a new *Collection with the same contents as the original.
func (pgs *Collection) Clone(ctx context.Context) *Collection {
newCollection := &Collection{
accessedMap: make(map[id.Sequence]*Sequence),
underlyingMap: pgs.underlyingMap,
ns: pgs.ns,
}
for seqID, seq := range pgs.accessedMap {
newCollection.accessedMap[seqID] = seq
}
return newCollection
}
// Map writes any cached sequences to the underlying map, and then returns the underlying map.
func (pgs *Collection) Map(ctx context.Context) (prolly.AddressMap, error) {
if err := pgs.writeCache(ctx); err != nil {
return prolly.AddressMap{}, err
}
return pgs.underlyingMap, nil
}
// GetID implements the interface objinterface.RootObject.
func (sequence *Sequence) GetID() id.Id {
return sequence.Id.AsId()
}
// GetRootObjectID implements the interface objinterface.RootObject.
func (sequence *Sequence) GetRootObjectID() objinterface.RootObjectID {
return objinterface.RootObjectID_Sequences
}
// HashOf implements the interface rootobject.RootObject.
func (sequence *Sequence) HashOf(ctx context.Context) (hash.Hash, error) {
data, err := sequence.Serialize(ctx)
if err != nil {
return hash.Hash{}, err
}
return hash.Of(data), nil
}
// Name implements the interface rootobject.RootObject.
func (sequence *Sequence) Name() doltdb.TableName {
return doltdb.TableName{
Name: sequence.Id.SequenceName(),
Schema: sequence.Id.SchemaName(),
}
}
// cacheAllSequences loads every sequence from the Dolt map into our local map. This exists to simplify any iteration
// logic, and shouldn't be used on a performance-critical path.
func (pgs *Collection) cacheAllSequences(ctx context.Context) error {
found := make(map[id.Sequence]struct{})
for seqID := range pgs.accessedMap {
found[seqID] = struct{}{}
}
return pgs.underlyingMap.IterAll(ctx, func(k string, v hash.Hash) error {
seqID := id.Sequence(k)
if _, ok := found[seqID]; ok {
return nil
}
found[seqID] = struct{}{}
data, err := pgs.ns.ReadBytes(ctx, v)
if err != nil {
return err
}
seq, err := DeserializeSequence(ctx, data)
if err != nil {
return err
}
pgs.accessedMap[seq.Id] = seq
return nil
})
}
// getSequence gets the sequence matching the given name.
func (pgs *Collection) getSequence(ctx context.Context, name id.Sequence) (*Sequence, error) {
// Subsequent loads are cached
if seq, ok := pgs.accessedMap[name]; ok {
return seq, nil
}
// The initial load is from the internal map
h, err := pgs.underlyingMap.Get(ctx, string(name))
if err != nil || h.IsEmpty() {
return nil, err
}
data, err := pgs.ns.ReadBytes(ctx, h)
if err != nil {
return nil, err
}
seq, err := DeserializeSequence(ctx, data)
if err != nil {
return nil, err
}
pgs.accessedMap[seq.Id] = seq
return seq, nil
}
// writeCache writes every Sequence in the cache to the underlying map.
func (pgs *Collection) writeCache(ctx context.Context) (err error) {
if len(pgs.accessedMap) == 0 {
return nil
}
mapEditor := pgs.underlyingMap.Editor()
for _, seq := range pgs.accessedMap {
data, err := seq.Serialize(ctx)
if err != nil {
return err
}
h, err := pgs.ns.WriteBytes(ctx, data)
if err != nil {
return err
}
if err = mapEditor.Update(ctx, string(seq.Id), h); err != nil {
return err
}
}
// Assign underlyingMap only after the error check. Flush returns a
// zero AddressMap on failure, which would corrupt the Collection.
flushed, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pgs.underlyingMap = flushed
clear(pgs.accessedMap)
return nil
}
// nextValForSequence increments the calling sequence.
func (sequence *Sequence) nextValForSequence() (int64, error) {
// First we'll check if we've reached the end, and cycle or error as necessary
if sequence.IsAtEnd {
if !sequence.Cycle {
if sequence.Increment > 0 {
return 0, errors.Errorf(`nextval: reached maximum value of sequence "%s" (%d)`, sequence.Id, sequence.Maximum)
} else {
return 0, errors.Errorf(`nextval: reached minimum value of sequence "%s" (%d)`, sequence.Id, sequence.Minimum)
}
}
sequence.IsAtEnd = false
if sequence.Increment > 0 {
sequence.Current = sequence.Minimum
} else {
sequence.Current = sequence.Maximum
}
}
// We'll return the current value, so everything after this sets the value for the next call
sequence.HasBeenCalled = true
valueToReturn := sequence.Current
// Increment the current value
if sequence.Increment > 0 {
// Check for overflow or crossing the maximum, meaning we're at the end
if sequence.Current > math.MaxInt64-sequence.Increment || sequence.Current+sequence.Increment > sequence.Maximum {
sequence.IsAtEnd = true
} else {
sequence.Current += sequence.Increment
}
} else {
// Check for underflow or crossing the minimum, meaning we're at the end
if sequence.Current < math.MinInt64-sequence.Increment || sequence.Current+sequence.Increment < sequence.Minimum {
sequence.IsAtEnd = true
} else {
sequence.Current += sequence.Increment
}
}
return valueToReturn, nil
}
+190
View File
@@ -0,0 +1,190 @@
// Copyright 2025 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 sequences
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/merge"
"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/doltgresql/core/id"
merge2 "github.com/dolthub/doltgresql/core/merge"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/flatbuffers/gen/serial"
pgtypes "github.com/dolthub/doltgresql/server/types"
"github.com/dolthub/doltgresql/utils"
)
// storage is used to read from and write to the root.
var storage = objinterface.RootObjectSerializer{
Bytes: (*serial.RootValue).SequencesBytes,
RootValueAdd: serial.RootValueAddSequences,
}
// HandleMerge implements the interface objinterface.Collection.
func (*Collection) HandleMerge(ctx context.Context, mro merge.MergeRootObject) (doltdb.RootObject, *merge.MergeStats, error) {
ourSeq := mro.OurRootObj.(*Sequence)
theirSeq := mro.TheirRootObj.(*Sequence)
// Ensure that they have the same identifier
if ourSeq.Id != theirSeq.Id {
return nil, nil, errors.Newf("attempted to merge different sequences: `%s` and `%s`",
ourSeq.Name().String(), theirSeq.Name().String())
}
// Check if an ancestor is present
var ancSeq Sequence
hasAncestor := false
if mro.AncestorRootObj != nil {
ancSeq = *(mro.AncestorRootObj.(*Sequence))
hasAncestor = true
}
// Take the min/max of fields that aren't dependent on the increment direction
mergedSeq := *ourSeq
mergedSeq.Minimum = merge2.ResolveMergeValuesVariadic(ourSeq.Minimum, theirSeq.Minimum, ancSeq.Minimum, hasAncestor, utils.Min)
mergedSeq.Maximum = merge2.ResolveMergeValuesVariadic(ourSeq.Maximum, theirSeq.Maximum, ancSeq.Maximum, hasAncestor, utils.Max)
mergedSeq.Cache = merge2.ResolveMergeValuesVariadic(ourSeq.Cache, theirSeq.Cache, ancSeq.Cache, hasAncestor, utils.Min)
mergedSeq.Cycle = merge2.ResolveMergeValues(ourSeq.Cycle, theirSeq.Cycle, ancSeq.Cycle, hasAncestor, func(ourCycle, theirCycle bool) bool {
return ourCycle || theirCycle
})
// Take the largest type specified
mergedSeq.DataTypeID = merge2.ResolveMergeValues(ourSeq.DataTypeID, theirSeq.DataTypeID, ancSeq.DataTypeID, hasAncestor, func(ourID, theirID id.Type) id.Type {
if (ourID == pgtypes.Int16.ID && (theirID == pgtypes.Int32.ID || theirID == pgtypes.Int64.ID)) ||
(ourID == pgtypes.Int32.ID && theirID == pgtypes.Int64.ID) {
return theirID
} else {
return ourID
}
})
// Handle the fields that are dependent on the increment direction.
// We'll always take the increment size that's the smallest for the most granularity, along with the one that
// has progressed the furthest.
// For opposing increment directions, we'll take whatever is in our collection.
mergedSeq.Increment = merge2.ResolveMergeValues(ourSeq.Increment, theirSeq.Increment, ancSeq.Increment, hasAncestor, func(ourIncrement, theirIncrement int64) int64 {
if ourSeq.Increment >= 0 && theirSeq.Increment >= 0 {
return utils.Min(ourIncrement, theirIncrement)
} else if ourSeq.Increment < 0 && theirSeq.Increment < 0 {
return utils.Max(ourIncrement, theirIncrement)
} else {
return ourIncrement
}
})
mergedSeq.Start = merge2.ResolveMergeValues(ourSeq.Start, theirSeq.Start, ancSeq.Start, hasAncestor, func(ourStart, theirStart int64) int64 {
if ourSeq.Increment >= 0 && theirSeq.Increment >= 0 {
return utils.Min(ourStart, theirStart)
} else if ourSeq.Increment < 0 && theirSeq.Increment < 0 {
return utils.Max(ourStart, theirStart)
} else {
return ourStart
}
})
mergedSeq.Current = merge2.ResolveMergeValues(ourSeq.Current, theirSeq.Current, ancSeq.Current, hasAncestor, func(ourCurrent, theirCurrent int64) int64 {
if ourSeq.Increment >= 0 && theirSeq.Increment >= 0 {
return utils.Max(ourCurrent, theirCurrent)
} else if ourSeq.Increment < 0 && theirSeq.Increment < 0 {
return utils.Min(ourCurrent, theirCurrent)
} else {
return ourCurrent
}
})
mergedSeq.HasBeenCalled = merge2.ResolveMergeValues(ourSeq.HasBeenCalled, theirSeq.HasBeenCalled, ancSeq.HasBeenCalled, hasAncestor, func(ourcalled, theirCalled bool) bool {
return ourcalled || theirCalled
})
return &mergedSeq, &merge.MergeStats{
Operation: merge.TableModified,
Adds: 0,
Deletes: 0,
Modifications: 1,
DataConflicts: 0,
SchemaConflicts: 0,
ConstraintViolations: 0,
}, nil
}
// LoadCollection implements the interface objinterface.Collection.
func (*Collection) LoadCollection(ctx context.Context, root objinterface.RootValue) (objinterface.Collection, error) {
return LoadSequences(ctx, root)
}
// LoadCollectionHash implements the interface objinterface.Collection.
func (*Collection) LoadCollectionHash(ctx context.Context, root objinterface.RootValue) (hash.Hash, error) {
m, ok, err := storage.GetProllyMap(ctx, root)
if err != nil || !ok {
return hash.Hash{}, err
}
return m.HashOf(), nil
}
// LoadSequences loads the sequences collection from the given root.
func LoadSequences(ctx context.Context, root objinterface.RootValue) (*Collection, error) {
m, ok, err := storage.GetProllyMap(ctx, root)
if err != nil {
return nil, err
}
if !ok {
m, err = prolly.NewEmptyAddressMap(root.NodeStore())
if err != nil {
return nil, err
}
}
return &Collection{
accessedMap: make(map[id.Sequence]*Sequence),
underlyingMap: m,
ns: root.NodeStore(),
}, nil
}
// ResolveNameFromObjects implements the interface objinterface.Collection.
func (*Collection) ResolveNameFromObjects(ctx context.Context, name doltdb.TableName, rootObjects []objinterface.RootObject) (doltdb.TableName, id.Id, error) {
// First we'll check if there are any objects to search through in the first place
accessedMap := make(map[id.Sequence]*Sequence)
for _, rootObject := range rootObjects {
if obj, ok := rootObject.(*Sequence); ok {
accessedMap[obj.Id] = obj
}
}
if len(accessedMap) == 0 {
return doltdb.TableName{}, id.Null, nil
}
// There are root objects to search through, so we'll create a temporary store
ns := tree.NewTestNodeStore()
addressMap, err := prolly.NewEmptyAddressMap(ns)
if err != nil {
return doltdb.TableName{}, id.Null, err
}
tempCollection := Collection{
accessedMap: accessedMap,
underlyingMap: addressMap,
ns: ns,
}
return tempCollection.ResolveName(ctx, name)
}
// Serializer implements the interface objinterface.Collection.
func (*Collection) Serializer() objinterface.RootObjectSerializer {
return storage
}
// UpdateRoot implements the interface objinterface.Collection.
func (pgs *Collection) UpdateRoot(ctx context.Context, root objinterface.RootValue) (objinterface.RootValue, error) {
m, err := pgs.Map(ctx)
if err != nil {
return nil, err
}
return storage.WriteProllyMap(ctx, root, m)
}
+137
View File
@@ -0,0 +1,137 @@
// 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 sequences
import (
"context"
"errors"
"math"
"testing"
"github.com/dolthub/dolt/go/store/hash"
"github.com/dolthub/dolt/go/store/prolly"
"github.com/dolthub/dolt/go/store/prolly/tree"
"github.com/stretchr/testify/require"
"github.com/dolthub/doltgresql/core/id"
)
// TestMap_RemainsUsableAfterFlushFailure asserts that Collection.Map
// remains usable after a flush returns an error. A subsequent Map call
// against a healthy store must succeed.
func TestMap_RemainsUsableAfterFlushFailure(t *testing.T) {
t.Parallel()
ctx := context.Background()
ns := newCountingFailNodeStore(t)
coll := newTestCollection(t, ns)
coll.accessedMap[id.NewSequence("public", "seq_one")] = newTestSequence("public", "seq_one")
_, err := coll.Map(ctx)
require.NoError(t, err)
ns.failAfter(0)
coll.accessedMap[id.NewSequence("public", "seq_two")] = newTestSequence("public", "seq_two")
_, err = coll.Map(ctx)
require.Error(t, err)
ns.allowAll()
require.NotPanics(t, func() {
_, _ = coll.Map(ctx)
})
}
// TestDropSequence_RemainsUsableAfterFlushFailure asserts the same
// recovery contract for DropSequence.
func TestDropSequence_RemainsUsableAfterFlushFailure(t *testing.T) {
t.Parallel()
ctx := context.Background()
ns := newCountingFailNodeStore(t)
coll := newTestCollection(t, ns)
pending := newTestSequence("public", "pending")
coll.accessedMap[pending.Id] = pending
ns.failAfter(0)
err := coll.DropSequence(ctx, pending.Id)
require.Error(t, err)
ns.allowAll()
require.NotPanics(t, func() {
_ = coll.DropSequence(ctx, id.NewSequence("public", "pending"))
})
}
// newTestCollection returns a Collection backed by |ns| with an empty
// address map.
func newTestCollection(t *testing.T, ns tree.NodeStore) *Collection {
t.Helper()
addrMap, err := prolly.NewEmptyAddressMap(ns)
require.NoError(t, err)
return &Collection{
accessedMap: map[id.Sequence]*Sequence{},
underlyingMap: addrMap,
ns: ns,
}
}
// newTestSequence returns a Sequence with valid bounds that round-trip
// through Serialize and Deserialize. The exact values are not significant.
func newTestSequence(schema, name string) *Sequence {
return &Sequence{
Id: id.NewSequence(schema, name),
Start: 1,
Current: 1,
Increment: 1,
Minimum: 1,
Maximum: math.MaxInt64,
Cache: 1,
}
}
// countingFailNodeStore wraps a real test NodeStore so that callers can
// induce Write failures at chosen points without otherwise altering
// behavior. Used to drive the writeCache flush-failure path.
type countingFailNodeStore struct {
tree.NodeStore
writes int
budget int // -1 means unlimited
}
func newCountingFailNodeStore(t *testing.T) *countingFailNodeStore {
t.Helper()
return &countingFailNodeStore{NodeStore: tree.NewTestNodeStore(), budget: -1}
}
// failAfter permits |allowed| additional Write calls then fails the
// rest, until allowAll is called.
func (f *countingFailNodeStore) failAfter(allowed int) {
f.writes = 0
f.budget = allowed
}
// allowAll restores the wrapper to delegate every Write to the
// underlying NodeStore.
func (f *countingFailNodeStore) allowAll() {
f.budget = -1
}
func (f *countingFailNodeStore) Write(ctx context.Context, nd *tree.Node) (hash.Hash, error) {
if f.budget >= 0 {
f.writes++
if f.writes > f.budget {
return hash.Hash{}, errors.New("induced node store failure")
}
}
return f.NodeStore.Write(ctx, nd)
}
+362
View File
@@ -0,0 +1,362 @@
// Copyright 2025 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 sequences
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/doltgresql/core/id"
pgmerge "github.com/dolthub/doltgresql/core/merge"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
const (
FIELD_NAME_DATA_TYPE = "data_type"
FIELD_NAME_PERSISTENCE = "persistence"
FIELD_NAME_START = "start"
FIELD_NAME_CURRENT = "current"
FIELD_NAME_INCREMENT = "increment"
FIELD_NAME_MINIMUM = "minimum"
FIELD_NAME_MAXIMUM = "maximum"
FIELD_NAME_CACHE = "cache"
FIELD_NAME_CYCLE = "cycle"
FIELD_NAME_IS_AT_END = "is_at_end"
FIELD_NAME_HAS_BEEN_CALLED = "has_been_called"
FIELD_NAME_OWNER_TABLE = "owner_table"
FIELD_NAME_OWNER_COLUMN = "owner_column"
)
// DeserializeRootObject implements the interface objinterface.Collection.
func (pgs *Collection) DeserializeRootObject(ctx context.Context, data []byte) (objinterface.RootObject, error) {
return DeserializeSequence(ctx, data)
}
// DiffRootObjects implements the interface objinterface.Collection.
func (pgs *Collection) DiffRootObjects(ctx context.Context, fromHash string, o objinterface.RootObject, t objinterface.RootObject, a objinterface.RootObject) ([]objinterface.RootObjectDiff, objinterface.RootObject, error) {
ours := o.(*Sequence)
{
copiedOurs := *ours
ours = &copiedOurs
}
theirs := t.(*Sequence)
var ancestor Sequence
hasAncestor := false
if ancestorPtr, ok := a.(*Sequence); ok {
ancestor = *ancestorPtr
hasAncestor = true
}
var diffs []objinterface.RootObjectDiff
if ours.DataTypeID != theirs.DataTypeID {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: FIELD_NAME_DATA_TYPE,
}
if pgmerge.DiffValues(&diff, ours.DataTypeID.TypeName(), theirs.DataTypeID.TypeName(), ancestor.DataTypeID.TypeName(), hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.DataTypeID = id.NewType(ours.DataTypeID.SchemaName(), diff.OurValue.(string))
}
}
if ours.Persistence != theirs.Persistence {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Int32,
FromHash: fromHash,
FieldName: FIELD_NAME_PERSISTENCE,
}
if pgmerge.DiffValues(&diff, int32(ours.Persistence), int32(theirs.Persistence), int32(ancestor.Persistence), hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Persistence = Persistence(diff.OurValue.(int32))
}
}
if ours.Start != theirs.Start {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Int64,
FromHash: fromHash,
FieldName: FIELD_NAME_START,
}
if pgmerge.DiffValues(&diff, ours.Start, theirs.Start, ancestor.Start, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Start = diff.OurValue.(int64)
}
}
if ours.Current != theirs.Current {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Int64,
FromHash: fromHash,
FieldName: FIELD_NAME_CURRENT,
}
if pgmerge.DiffValues(&diff, ours.Current, theirs.Current, ancestor.Current, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Current = diff.OurValue.(int64)
}
}
if ours.HasBeenCalled != theirs.HasBeenCalled {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Bool,
FromHash: fromHash,
FieldName: FIELD_NAME_HAS_BEEN_CALLED,
}
if pgmerge.DiffValues(&diff, ours.HasBeenCalled, theirs.HasBeenCalled, ancestor.HasBeenCalled, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.HasBeenCalled = diff.OurValue.(bool)
}
}
if ours.Increment != theirs.Increment {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Int64,
FromHash: fromHash,
FieldName: FIELD_NAME_INCREMENT,
}
if pgmerge.DiffValues(&diff, ours.Increment, theirs.Increment, ancestor.Increment, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Increment = diff.OurValue.(int64)
}
}
if ours.Minimum != theirs.Minimum {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Int64,
FromHash: fromHash,
FieldName: FIELD_NAME_MINIMUM,
}
if pgmerge.DiffValues(&diff, ours.Minimum, theirs.Minimum, ancestor.Minimum, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Minimum = diff.OurValue.(int64)
}
}
if ours.Maximum != theirs.Maximum {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Int64,
FromHash: fromHash,
FieldName: FIELD_NAME_MAXIMUM,
}
if pgmerge.DiffValues(&diff, ours.Maximum, theirs.Maximum, ancestor.Maximum, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Maximum = diff.OurValue.(int64)
}
}
if ours.Cache != theirs.Cache {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Int64,
FromHash: fromHash,
FieldName: FIELD_NAME_CACHE,
}
if pgmerge.DiffValues(&diff, ours.Cache, theirs.Cache, ancestor.Cache, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Cache = diff.OurValue.(int64)
}
}
if ours.Cycle != theirs.Cycle {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Bool,
FromHash: fromHash,
FieldName: FIELD_NAME_CYCLE,
}
if pgmerge.DiffValues(&diff, ours.Cycle, theirs.Cycle, ancestor.Cycle, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.Cycle = diff.OurValue.(bool)
}
}
if ours.IsAtEnd != theirs.IsAtEnd {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Bool,
FromHash: fromHash,
FieldName: FIELD_NAME_IS_AT_END,
}
if pgmerge.DiffValues(&diff, ours.IsAtEnd, theirs.IsAtEnd, ancestor.IsAtEnd, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.IsAtEnd = diff.OurValue.(bool)
}
}
if ours.OwnerTable != theirs.OwnerTable {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: FIELD_NAME_OWNER_TABLE,
}
if pgmerge.DiffValues(&diff, ours.OwnerTable.TableName(), theirs.OwnerTable.TableName(), ancestor.OwnerTable.TableName(), hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.OwnerTable = id.NewTable(ours.OwnerTable.SchemaName(), diff.OurValue.(string))
}
}
if ours.OwnerColumn != theirs.OwnerColumn {
diff := objinterface.RootObjectDiff{
Type: pgtypes.Text,
FromHash: fromHash,
FieldName: FIELD_NAME_OWNER_COLUMN,
}
if pgmerge.DiffValues(&diff, ours.OwnerColumn, theirs.OwnerColumn, ancestor.OwnerColumn, hasAncestor) {
diffs = append(diffs, diff)
} else {
ours.OwnerColumn = diff.OurValue.(string)
}
}
return diffs, ours, nil
}
// DropRootObject implements the interface objinterface.Collection.
func (pgs *Collection) DropRootObject(ctx context.Context, identifier id.Id) error {
if identifier.Section() != id.Section_Sequence {
return errors.Errorf(`sequence %s does not exist`, identifier.String())
}
return pgs.DropSequence(ctx, id.Sequence(identifier))
}
// GetFieldType implements the interface objinterface.Collection.
func (pgs *Collection) GetFieldType(ctx context.Context, fieldName string) *pgtypes.DoltgresType {
switch fieldName {
case FIELD_NAME_DATA_TYPE:
return pgtypes.Text
case FIELD_NAME_PERSISTENCE:
return pgtypes.Int32
case FIELD_NAME_START:
return pgtypes.Int64
case FIELD_NAME_CURRENT:
return pgtypes.Int64
case FIELD_NAME_INCREMENT:
return pgtypes.Int64
case FIELD_NAME_MINIMUM:
return pgtypes.Int64
case FIELD_NAME_MAXIMUM:
return pgtypes.Int64
case FIELD_NAME_CACHE:
return pgtypes.Int64
case FIELD_NAME_CYCLE:
return pgtypes.Bool
case FIELD_NAME_IS_AT_END:
return pgtypes.Bool
case FIELD_NAME_HAS_BEEN_CALLED:
return pgtypes.Bool
case FIELD_NAME_OWNER_TABLE:
return pgtypes.Text
case FIELD_NAME_OWNER_COLUMN:
return pgtypes.Text
default:
return nil
}
}
// GetID implements the interface objinterface.Collection.
func (pgs *Collection) GetID() objinterface.RootObjectID {
return objinterface.RootObjectID_Sequences
}
// GetRootObject implements the interface objinterface.Collection.
func (pgs *Collection) GetRootObject(ctx context.Context, identifier id.Id) (objinterface.RootObject, bool, error) {
if identifier.Section() != id.Section_Sequence {
return nil, false, nil
}
seq, err := pgs.GetSequence(ctx, id.Sequence(identifier))
return seq, err == nil && seq != nil, err
}
// HasRootObject implements the interface objinterface.Collection.
func (pgs *Collection) HasRootObject(ctx context.Context, identifier id.Id) (bool, error) {
if identifier.Section() != id.Section_Sequence {
return false, nil
}
return pgs.HasSequence(ctx, id.Sequence(identifier)), nil
}
// IDToTableName implements the interface objinterface.Collection.
func (pgs *Collection) IDToTableName(identifier id.Id) doltdb.TableName {
if identifier.Section() != id.Section_Sequence {
return doltdb.TableName{}
}
seqID := id.Sequence(identifier)
return doltdb.TableName{
Name: seqID.SequenceName(),
Schema: seqID.SchemaName(),
}
}
// IterAll implements the interface objinterface.Collection.
func (pgs *Collection) IterAll(ctx context.Context, callback func(rootObj objinterface.RootObject) (stop bool, err error)) error {
return pgs.IterateSequences(ctx, func(seq *Sequence) (stop bool, err error) {
return callback(seq)
})
}
// IterIDs implements the interface objinterface.Collection.
func (pgs *Collection) IterIDs(ctx context.Context, callback func(identifier id.Id) (stop bool, err error)) error {
return pgs.iterateIDs(ctx, func(seqID id.Sequence) (stop bool, err error) {
return callback(seqID.AsId())
})
}
// PutRootObject implements the interface objinterface.Collection.
func (pgs *Collection) PutRootObject(ctx context.Context, rootObj objinterface.RootObject) error {
seq, ok := rootObj.(*Sequence)
if !ok {
return errors.Newf("invalid sequence root object: %T", rootObj)
}
return pgs.CreateSequence(ctx, seq)
}
// RenameRootObject implements the interface objinterface.Collection.
func (pgs *Collection) RenameRootObject(ctx context.Context, oldName id.Id, newName id.Id) error {
if !oldName.IsValid() || !newName.IsValid() || oldName.Section() != newName.Section() || oldName.Section() != id.Section_Sequence {
return errors.New("cannot rename sequence due to invalid name")
}
oldSeqName := id.Sequence(oldName)
newSeqName := id.Sequence(newName)
seq, err := pgs.GetSequence(ctx, oldSeqName)
if err != nil {
return err
}
if err = pgs.DropSequence(ctx, oldSeqName); err != nil {
return err
}
newSeq := *seq
newSeq.Id = newSeqName
return pgs.CreateSequence(ctx, &newSeq)
}
// ResolveName implements the interface objinterface.Collection.
func (pgs *Collection) ResolveName(ctx context.Context, name doltdb.TableName) (doltdb.TableName, id.Id, error) {
rawID, err := pgs.resolveName(ctx, name.Schema, name.Name)
if err != nil || !rawID.IsValid() {
return doltdb.TableName{}, id.Null, err
}
return doltdb.TableName{
Name: rawID.SequenceName(),
Schema: rawID.SchemaName(),
}, rawID.AsId(), nil
}
// TableNameToID implements the interface objinterface.Collection.
func (pgs *Collection) TableNameToID(name doltdb.TableName) id.Id {
return id.NewSequence(name.Schema, name.Name).AsId()
}
// UpdateField implements the interface objinterface.Collection.
func (pgs *Collection) UpdateField(ctx context.Context, rootObject objinterface.RootObject, fieldName string, newValue any) (objinterface.RootObject, error) {
return nil, errors.New("updating through the conflicts table for this object type is not yet supported")
}
+89
View File
@@ -0,0 +1,89 @@
// 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 sequences
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/utils"
)
// Serialize returns the Sequence as a byte slice. If the Sequence is nil, then this returns a nil slice.
func (sequence *Sequence) Serialize(ctx context.Context) ([]byte, error) {
if sequence == nil {
return nil, nil
}
// Create the writer
writer := utils.NewWriter(256)
writer.VariableUint(1) // Version
// Write the sequence data
writer.Id(sequence.Id.AsId())
writer.Id(sequence.DataTypeID.AsId())
writer.Uint8(uint8(sequence.Persistence))
writer.Int64(sequence.Start)
writer.Int64(sequence.Current)
writer.Int64(sequence.Increment)
writer.Int64(sequence.Minimum)
writer.Int64(sequence.Maximum)
writer.Int64(sequence.Cache)
writer.Bool(sequence.Cycle)
writer.Bool(sequence.IsAtEnd)
writer.Bool(sequence.HasBeenCalled)
writer.Id(sequence.OwnerTable.AsId())
writer.String(sequence.OwnerColumn)
// Returns the data
return writer.Data(), nil
}
// DeserializeSequence returns the Sequence that was serialized in the byte slice. Returns an empty Sequence if data is
// nil or empty.
func DeserializeSequence(_ context.Context, data []byte) (*Sequence, error) {
if len(data) == 0 {
return nil, nil
}
reader := utils.NewReader(data)
version := reader.VariableUint()
if version > 1 {
return nil, errors.Errorf("version %d of sequences is not supported, please upgrade the server", version)
}
// Read from the reader
sequence := &Sequence{}
sequence.Id = id.Sequence(reader.Id())
sequence.DataTypeID = id.Type(reader.Id())
sequence.Persistence = Persistence(reader.Uint8())
sequence.Start = reader.Int64()
sequence.Current = reader.Int64()
sequence.Increment = reader.Int64()
sequence.Minimum = reader.Int64()
sequence.Maximum = reader.Int64()
sequence.Cache = reader.Int64()
sequence.Cycle = reader.Bool()
sequence.IsAtEnd = reader.Bool()
if version >= 1 {
sequence.HasBeenCalled = reader.Bool()
}
sequence.OwnerTable = id.Table(reader.Id())
sequence.OwnerColumn = reader.String()
if !reader.IsEmpty() {
return nil, errors.Errorf("extra data found while deserializing a sequence")
}
// Return the deserialized object
return sequence, nil
}
+353
View File
@@ -0,0 +1,353 @@
// 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 storage
import (
"context"
"fmt"
doltserial "github.com/dolthub/dolt/go/gen/fb/serial"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/schema"
"github.com/dolthub/dolt/go/store/hash"
"github.com/dolthub/dolt/go/store/prolly"
"github.com/dolthub/dolt/go/store/prolly/shim"
"github.com/dolthub/dolt/go/store/prolly/tree"
"github.com/dolthub/dolt/go/store/types"
flatbuffers "github.com/dolthub/flatbuffers/v23/go"
"github.com/dolthub/doltgresql/flatbuffers/gen/serial"
)
// RootStorage is the FlatBuffer interface for the storage format.
type RootStorage struct {
SRV *serial.RootValue
}
type TableEdit struct {
Name doltdb.TableName
Ref *types.Ref
// Used for rename.
OldName doltdb.TableName
}
// RootObjectSerialization handles the allocation/preservation of bytes for root objects.
type RootObjectSerialization struct {
Bytes func(*serial.RootValue) []byte
RootValueAdd func(builder *flatbuffers.Builder, sequences flatbuffers.UOffsetT)
}
// RootObjectSerializations contains all root object serializations. This should be set from the global initialization
// function.
var RootObjectSerializations []RootObjectSerialization
// SetForeignKeyMap sets the foreign key and returns a new storage object.
func (r RootStorage) SetForeignKeyMap(ctx context.Context, vrw types.ValueReadWriter, v types.Value) (RootStorage, error) {
var h hash.Hash
isempty, err := doltdb.EmptyForeignKeyCollection(v.(types.SerialMessage))
if err != nil {
return RootStorage{}, err
}
if !isempty {
ref, err := vrw.WriteValue(ctx, v)
if err != nil {
return RootStorage{}, err
}
h = ref.TargetHash()
}
ret := r.Clone()
copy(ret.SRV.ForeignKeyAddrBytes(), h[:])
return ret, nil
}
// SetFeatureVersion sets the feature version and returns a new storage object.
func (r RootStorage) SetFeatureVersion(v doltdb.FeatureVersion) (RootStorage, error) {
ret := r.Clone()
ret.SRV.MutateFeatureVersion(int64(v))
return ret, nil
}
// SetCollation sets the collation and returns a new storage object.
func (r RootStorage) SetCollation(ctx context.Context, collation schema.Collation) (RootStorage, error) {
ret := r.Clone()
ret.SRV.MutateCollation(serial.Collation(collation))
return ret, nil
}
// GetSchemas returns all schemas.
func (r RootStorage) GetSchemas(ctx context.Context) ([]schema.DatabaseSchema, error) {
numSchemas := r.SRV.SchemasLength()
schemas := make([]schema.DatabaseSchema, numSchemas)
for i := 0; i < numSchemas; i++ {
dbSchema := new(serial.DatabaseSchema)
_, err := r.SRV.TrySchemas(dbSchema, i)
if err != nil {
return nil, err
}
schemas[i] = schema.DatabaseSchema{
Name: string(dbSchema.Name()),
}
}
return schemas, nil
}
// SetSchemas sets the given schemas and returns a new storage object.
func (r RootStorage) SetSchemas(ctx context.Context, dbSchemas []schema.DatabaseSchema) (RootStorage, error) {
msg, err := r.serializeRootValue(r.SRV.TablesBytes(), dbSchemas)
if err != nil {
return RootStorage{}, err
}
return RootStorage{msg}, nil
}
// Clone returns a clone of the calling storage.
func (r RootStorage) Clone() RootStorage {
bs := make([]byte, len(r.SRV.Table().Bytes))
copy(bs, r.SRV.Table().Bytes)
var ret serial.RootValue
ret.Init(bs, r.SRV.Table().Pos)
return RootStorage{&ret}
}
// DebugString returns the storage as a printable string.
func (r RootStorage) DebugString(ctx context.Context) string {
return fmt.Sprintf("RootStorage[%d, %s, %s]",
r.SRV.FeatureVersion(),
"...",
hash.New(r.SRV.ForeignKeyAddrBytes()).String())
}
// NomsValue returns the storage as a noms value.
func (r RootStorage) NomsValue() types.Value {
return types.SerialMessage(r.SRV.Table().Bytes)
}
// GetFeatureVersion returns the feature version for this storage object.
func (r RootStorage) GetFeatureVersion() doltdb.FeatureVersion {
return doltdb.FeatureVersion(r.SRV.FeatureVersion())
}
// getAddressMap returns the address map from within this storage object.
func (r RootStorage) getAddressMap(vrw types.ValueReadWriter, ns tree.NodeStore) (prolly.AddressMap, error) {
tbytes := r.SRV.TablesBytes()
node, _, err := shim.NodeFromValue(types.SerialMessage(tbytes))
if err != nil {
return prolly.AddressMap{}, err
}
return prolly.NewAddressMap(node, ns)
}
// GetTablesMap returns the tables map from within this storage object.
func (r RootStorage) GetTablesMap(ctx context.Context, vrw types.ValueReadWriter, ns tree.NodeStore, databaseSchema string) (RootTableMap, error) {
am, err := r.getAddressMap(vrw, ns)
if err != nil {
return RootTableMap{}, err
}
return RootTableMap{AddressMap: am, schemaName: databaseSchema}, nil
}
// GetForeignKeys returns the types.SerialMessage representing the foreign keys.
func (r RootStorage) GetForeignKeys(ctx context.Context, vr types.ValueReader) (types.Value, bool, error) {
addr := hash.New(r.SRV.ForeignKeyAddrBytes())
if addr.IsEmpty() {
return types.SerialMessage{}, false, nil
}
v, err := vr.ReadValue(ctx, addr)
if err != nil {
return types.SerialMessage{}, false, err
}
return v.(types.SerialMessage), true, nil
}
// GetCollation returns the collation declared within storage.
func (r RootStorage) GetCollation(ctx context.Context) (schema.Collation, error) {
collation := r.SRV.Collation()
// Pre-existing repositories will return invalid here
if collation == serial.Collationinvalid {
return schema.Collation_Default, nil
}
return schema.Collation(collation), nil
}
// EditTablesMap edits the table map within storage.
func (r RootStorage) EditTablesMap(ctx context.Context, vrw types.ValueReadWriter, ns tree.NodeStore, edits []TableEdit) (RootStorage, error) {
am, err := r.getAddressMap(vrw, ns)
if err != nil {
return RootStorage{}, err
}
ae := am.Editor()
for _, e := range edits {
if e.OldName.Name != "" {
oldaddr, err := am.Get(ctx, encodeTableNameForAddressMap(e.OldName))
if err != nil {
return RootStorage{}, err
}
newaddr, err := am.Get(ctx, encodeTableNameForAddressMap(e.Name))
if err != nil {
return RootStorage{}, err
}
if oldaddr.IsEmpty() {
return RootStorage{}, doltdb.ErrTableNotFound
}
if !newaddr.IsEmpty() {
return RootStorage{}, doltdb.ErrTableExists
}
err = ae.Delete(ctx, encodeTableNameForAddressMap(e.OldName))
if err != nil {
return RootStorage{}, err
}
err = ae.Update(ctx, encodeTableNameForAddressMap(e.Name), oldaddr)
if err != nil {
return RootStorage{}, err
}
} else {
if e.Ref == nil {
err := ae.Delete(ctx, encodeTableNameForAddressMap(e.Name))
if err != nil {
return RootStorage{}, err
}
} else {
err := ae.Update(ctx, encodeTableNameForAddressMap(e.Name), e.Ref.TargetHash())
if err != nil {
return RootStorage{}, err
}
}
}
}
am, err = ae.Flush(ctx)
if err != nil {
return RootStorage{}, err
}
ambytes := []byte(tree.ValueFromNode(am.Node()).(types.SerialMessage))
dbSchemas, err := r.GetSchemas(ctx)
if err != nil {
return RootStorage{}, err
}
msg, err := r.serializeRootValue(ambytes, dbSchemas)
if err != nil {
return RootStorage{}, err
}
return RootStorage{msg}, nil
}
// serializeRootValue serializes a new serial.RootValue object.
func (r RootStorage) serializeRootValue(addressMapBytes []byte, dbSchemas []schema.DatabaseSchema) (*serial.RootValue, error) {
builder := flatbuffers.NewBuilder(80)
tablesOffset := builder.CreateByteVector(addressMapBytes)
schemasOffset := serializeDatabaseSchemas(builder, dbSchemas)
fkOffset := builder.CreateByteVector(r.SRV.ForeignKeyAddrBytes())
rootObjOffsets := make([]flatbuffers.UOffsetT, len(RootObjectSerializations))
for i := range RootObjectSerializations {
rootObjOffset := RootObjectSerializations[i].Bytes(r.SRV)
if len(rootObjOffset) == 0 {
h := hash.Hash{}
rootObjOffset = h[:]
}
rootObjOffsets[i] = builder.CreateByteVector(rootObjOffset)
}
serial.RootValueStart(builder)
serial.RootValueAddFeatureVersion(builder, r.SRV.FeatureVersion())
serial.RootValueAddCollation(builder, r.SRV.Collation())
serial.RootValueAddTables(builder, tablesOffset)
serial.RootValueAddForeignKeyAddr(builder, fkOffset)
for i := range RootObjectSerializations {
RootObjectSerializations[i].RootValueAdd(builder, rootObjOffsets[i])
}
if schemasOffset > 0 {
serial.RootValueAddSchemas(builder, schemasOffset)
}
bs := doltserial.FinishMessage(builder, serial.RootValueEnd(builder), []byte(doltserial.DoltgresRootValueFileID))
msg, err := serial.TryGetRootAsRootValue(bs, doltserial.MessagePrefixSz)
if err != nil {
return nil, err
}
return msg, nil
}
// serializeDatabaseSchemas serialzes the schemas into an offset within the given builder.
func serializeDatabaseSchemas(b *flatbuffers.Builder, dbSchemas []schema.DatabaseSchema) flatbuffers.UOffsetT {
// if we have no schemas, do not serialize an empty vector
if len(dbSchemas) == 0 {
return 0
}
offsets := make([]flatbuffers.UOffsetT, len(dbSchemas))
for i := len(dbSchemas) - 1; i >= 0; i-- {
dbSchema := dbSchemas[i]
nameOff := b.CreateString(dbSchema.Name)
serial.DatabaseSchemaStart(b)
serial.DatabaseSchemaAddName(b, nameOff)
offsets[i] = serial.DatabaseSchemaEnd(b)
}
serial.RootValueStartSchemasVector(b, len(offsets))
for i := len(offsets) - 1; i >= 0; i-- {
b.PrependUOffsetT(offsets[i])
}
return b.EndVector(len(offsets))
}
// encodeTableNameForAddressMap encodes the given table name for writing into storage.
func encodeTableNameForAddressMap(name doltdb.TableName) string {
if name.Schema == "" {
return name.Name
}
return fmt.Sprintf("\000%s\000%s", name.Schema, name.Name)
}
// decodeTableNameForAddressMap decodes a previously-encoded table name from storage.
func decodeTableNameForAddressMap(encodedName, schemaName string) (string, bool) {
if schemaName == "" && encodedName[0] != 0 {
return encodedName, true
} else if schemaName != "" && encodedName[0] == 0 &&
len(encodedName) > len(schemaName)+2 &&
encodedName[1:len(schemaName)+1] == schemaName {
return encodedName[len(schemaName)+2:], true
}
return "", false
}
// RootTableMap is an address map alongside a schema name.
type RootTableMap struct {
prolly.AddressMap
schemaName string
}
// Get returns the hash of the table with the given case-sensitive name.
func (m RootTableMap) Get(ctx context.Context, name string) (hash.Hash, error) {
return m.AddressMap.Get(ctx, encodeTableNameForAddressMap(doltdb.TableName{Name: name, Schema: m.schemaName}))
}
// Iter calls the given callback for each table and hash contained in the map.
func (m RootTableMap) Iter(ctx context.Context, cb func(string, hash.Hash) (bool, error)) error {
var stop bool
return m.AddressMap.IterAll(ctx, func(n string, a hash.Hash) error {
n, ok := decodeTableNameForAddressMap(n, m.schemaName)
if !stop && ok {
var err error
stop, err = cb(n, a)
return err
}
return nil
})
}
+62
View File
@@ -0,0 +1,62 @@
// Copyright 2025 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 core
import (
"github.com/dolthub/dolt/go/libraries/doltcore/sqle"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/plan"
)
// SQLNodeToDoltTable takes a sql.Node and returns a *sqle.DoltTable if either the node is a Dolt table, or it is a
// wrapper or container that holds a Dolt table. Returns nil if a Dolt table could not be found. If the node is not a
// sql.Table, then this will return nil.
func SQLNodeToDoltTable(n sql.Node) *sqle.DoltTable {
tbl, ok := n.(sql.Table)
if !ok {
return nil
}
return SQLTableToDoltTable(tbl)
}
// SQLTableToDoltTable takes a sql.Table and returns a *sqle.DoltTable if either the table is a Dolt table, or it is a
// wrapper or container that holds a Dolt table. Returns nil if a Dolt table could not be found.
func SQLTableToDoltTable(tbl sql.Table) *sqle.DoltTable {
switch t := tbl.(type) {
case *plan.ResolvedTable:
return SQLTableToDoltTable(t.Table)
case *plan.ProcessTable:
return SQLTableToDoltTable(t.Table)
case *plan.IndexedTableAccess:
return SQLTableToDoltTable(t.Table)
case *plan.ProcedureResolvedTable:
return SQLTableToDoltTable(t.ResolvedTable.Table)
case *sqle.WritableIndexedDoltTable:
return t.WritableDoltTable.DoltTable
case *sqle.IndexedDoltTable:
return t.DoltTable
case *sqle.AlterableDoltTable:
return t.WritableDoltTable.DoltTable
case *sqle.WritableDoltTable:
return t.DoltTable
case *sqle.DoltTable:
return t
default:
if wrapper, ok := tbl.(sql.TableWrapper); ok {
return SQLTableToDoltTable(wrapper.Underlying())
}
return nil
}
}
+385
View File
@@ -0,0 +1,385 @@
// 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 triggers
import (
"context"
"fmt"
"maps"
"slices"
"sort"
"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/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/server/plpgsql"
)
// Collection contains a collection of triggers.
type Collection struct {
accessCache map[id.Trigger]Trigger // This cache is used for general access when you know the exact ID
tableCache map[id.Table][]id.Trigger // This cache is used to find triggers by table
idCache []id.Trigger // This cache simply contains the name of every trigger
mapHash hash.Hash // This is cached so that we don't have to calculate the hash every time
underlyingMap prolly.AddressMap
ns tree.NodeStore
}
// TriggerTiming specifies the timing of the trigger's execution.
type TriggerTiming uint8
const (
TriggerTiming_Before TriggerTiming = 0
TriggerTiming_After TriggerTiming = 1
TriggerTiming_InsteadOf TriggerTiming = 2
)
// TriggerDeferrable specifies whether the trigger is deferrable.
type TriggerDeferrable uint8
const (
TriggerDeferrable_NotDeferrable TriggerDeferrable = 0 // NOT DEFERRABLE
TriggerDeferrable_DeferrableImmediate TriggerDeferrable = 1 // DEFERRABLE INITIALLY IMMEDIATE
TriggerDeferrable_DeferrableDeferred TriggerDeferrable = 2 // DEFERRABLE INITIALLY DEFERRED
)
// TriggerEventType specifies which type of event that the trigger applies to.
type TriggerEventType uint8
const (
TriggerEventType_Insert TriggerEventType = 0
TriggerEventType_Update TriggerEventType = 1
TriggerEventType_Delete TriggerEventType = 2
TriggerEventType_Truncate TriggerEventType = 3
)
// TriggerEvent specifies the event type, along with column information for update events.
type TriggerEvent struct {
Type TriggerEventType
ColumnNames []string
}
// Trigger represents a trigger.
type Trigger struct {
ID id.Trigger
Function id.Function
Timing TriggerTiming
Events []TriggerEvent
ForEachRow bool // When false, represents FOR EACH STATEMENT
When []plpgsql.InterpreterOperation
Deferrable TriggerDeferrable
ReferencedTableName id.Table // FROM referenced_table_name
Constraint bool
OldTransitionName string // REFERENCING OLD TABLE AS transition_relation_name
NewTransitionName string // REFERENCING NEW TABLE AS transition_relation_name
Arguments []string
Definition string
}
var _ objinterface.Collection = (*Collection)(nil)
var _ objinterface.RootObject = Trigger{}
// NewCollection returns a new Collection.
func NewCollection(ctx context.Context, underlyingMap prolly.AddressMap, ns tree.NodeStore) (*Collection, error) {
collection := &Collection{
accessCache: make(map[id.Trigger]Trigger),
tableCache: make(map[id.Table][]id.Trigger),
idCache: nil,
mapHash: hash.Hash{},
underlyingMap: underlyingMap,
ns: ns,
}
return collection, collection.reloadCaches(ctx)
}
// GetTrigger returns the trigger with the given ID. Returns a trigger with an invalid ID if it cannot be found
// (Trigger.ID.IsValid() == false).
func (pgt *Collection) GetTrigger(ctx context.Context, trigID id.Trigger) (Trigger, error) {
if f, ok := pgt.accessCache[trigID]; ok {
return f, nil
}
return Trigger{}, nil
}
// GetTriggerIDsForTable returns the trigger IDs for the given table.
func (pgt *Collection) GetTriggerIDsForTable(ctx context.Context, tableID id.Table) []id.Trigger {
return pgt.tableCache[tableID]
}
// GetTriggersForTable returns the triggers for the given table.
func (pgt *Collection) GetTriggersForTable(ctx context.Context, tableID id.Table) []Trigger {
triggerIDs := pgt.tableCache[tableID]
triggers := make([]Trigger, len(triggerIDs))
for i, trigID := range triggerIDs {
triggers[i] = pgt.accessCache[trigID]
}
return triggers
}
// GetTriggersForTableByTiming returns the triggers for the given table, all matching the given timing. These triggers
// are also sorted by their name ascending.
func (pgt *Collection) GetTriggersForTableByTiming(ctx context.Context, tableID id.Table, timing TriggerTiming) []Trigger {
triggers := pgt.GetTriggersForTable(ctx, tableID)
timingTriggers := make([]Trigger, 0, len(triggers))
for _, trig := range triggers {
if trig.Timing == timing {
timingTriggers = append(timingTriggers, trig)
}
}
sort.Slice(timingTriggers, func(i, j int) bool {
return timingTriggers[i].Name().String() < timingTriggers[j].Name().String()
})
return timingTriggers
}
// HasTrigger returns whether the trigger is present.
func (pgt *Collection) HasTrigger(ctx context.Context, trigID id.Trigger) bool {
_, ok := pgt.accessCache[trigID]
return ok
}
// AddTrigger adds a new trigger.
func (pgt *Collection) AddTrigger(ctx context.Context, t Trigger) error {
// First we'll check to see if it exists
if _, ok := pgt.accessCache[t.ID]; ok {
return errors.Errorf(`trigger "%s" for relation "%s" already exists`, t.ID.TriggerName(), t.ID.TableName())
}
// Now we'll add the trigger to our map
data, err := t.Serialize(ctx)
if err != nil {
return err
}
h, err := pgt.ns.WriteBytes(ctx, data)
if err != nil {
return err
}
mapEditor := pgt.underlyingMap.Editor()
if err = mapEditor.Add(ctx, string(t.ID), h); err != nil {
return err
}
newMap, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pgt.underlyingMap = newMap
pgt.mapHash = pgt.underlyingMap.HashOf()
return pgt.reloadCaches(ctx)
}
// DropTrigger drops an existing trigger.
func (pgt *Collection) DropTrigger(ctx context.Context, trigIDs ...id.Trigger) error {
if len(trigIDs) == 0 {
return nil
}
// Check that each name exists before performing any deletions
for _, trigID := range trigIDs {
if _, ok := pgt.accessCache[trigID]; !ok {
return errors.Errorf(`trigger "%s" for table "%s" does not exist`, trigID.TriggerName(), trigID.TableName())
}
}
// Now we'll remove the triggers from the map
mapEditor := pgt.underlyingMap.Editor()
for _, trigID := range trigIDs {
err := mapEditor.Delete(ctx, string(trigID))
if err != nil {
return err
}
}
newMap, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pgt.underlyingMap = newMap
pgt.mapHash = pgt.underlyingMap.HashOf()
return pgt.reloadCaches(ctx)
}
// resolveName returns the fully resolved name of the given trigger. Returns an error if the name is ambiguous.
func (pgt *Collection) resolveName(ctx context.Context, schemaName string, formattedName string) (id.Trigger, error) {
if len(pgt.accessCache) == 0 || len(formattedName) == 0 {
return id.NullTrigger, nil
}
// Check for an exact match
fullID := pgt.tableNameToID(schemaName, formattedName)
if _, ok := pgt.accessCache[fullID]; ok {
return fullID, nil
}
tableName := fullID.TableName()
triggerName := fullID.TriggerName()
// Otherwise we'll iterate over all the names
var resolvedID id.Trigger
for _, trigID := range pgt.idCache {
if !strings.EqualFold(triggerName, trigID.TriggerName()) || !strings.EqualFold(tableName, trigID.TableName()) {
continue
}
if len(schemaName) > 0 && !strings.EqualFold(schemaName, trigID.SchemaName()) {
continue
}
// The above matches, so this counts as a match
if resolvedID.IsValid() {
trigTableName := TriggerIDToTableName(trigID)
resolvedTableName := TriggerIDToTableName(resolvedID)
return id.NullTrigger, fmt.Errorf("`%s.%s` is ambiguous, matches `%s` and `%s`",
schemaName, formattedName, trigTableName.String(), resolvedTableName.String())
}
resolvedID = trigID
}
return resolvedID, nil
}
// iterateIDs iterates over all trigger IDs in the collection.
func (pgt *Collection) iterateIDs(ctx context.Context, callback func(trigID id.Trigger) (stop bool, err error)) error {
for _, trigID := range pgt.idCache {
stop, err := callback(trigID)
if err != nil {
return err
} else if stop {
return nil
}
}
return nil
}
// IterateTriggers iterates over all triggers in the collection.
func (pgt *Collection) IterateTriggers(ctx context.Context, callback func(t Trigger) (stop bool, err error)) error {
for _, trigID := range pgt.idCache {
stop, err := callback(pgt.accessCache[trigID])
if err != nil {
return err
} else if stop {
return nil
}
}
return nil
}
// Clone returns a new *Collection with the same contents as the original.
func (pgt *Collection) Clone(ctx context.Context) *Collection {
return &Collection{
accessCache: maps.Clone(pgt.accessCache),
tableCache: maps.Clone(pgt.tableCache),
idCache: slices.Clone(pgt.idCache),
underlyingMap: pgt.underlyingMap,
mapHash: pgt.mapHash,
ns: pgt.ns,
}
}
// Map writes any cached sequences to the underlying map, and then returns the underlying map.
func (pgt *Collection) Map(ctx context.Context) (prolly.AddressMap, error) {
return pgt.underlyingMap, nil
}
// 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 (pgt *Collection) DiffersFrom(ctx context.Context, root objinterface.RootValue) bool {
hashOnGivenRoot, err := pgt.LoadCollectionHash(ctx, root)
if err != nil {
return true
}
if pgt.mapHash.Equal(hashOnGivenRoot) {
return false
}
// An empty map should match an uninitialized collection on the root
count, err := pgt.underlyingMap.Count()
if err == nil && count == 0 && hashOnGivenRoot.IsEmpty() {
return false
}
return true
}
// reloadCaches writes the underlying map's contents to the caches.
func (pgt *Collection) reloadCaches(ctx context.Context) error {
count, err := pgt.underlyingMap.Count()
if err != nil {
return err
}
clear(pgt.accessCache)
clear(pgt.tableCache)
pgt.mapHash = pgt.underlyingMap.HashOf()
pgt.idCache = make([]id.Trigger, 0, count)
return pgt.underlyingMap.IterAll(ctx, func(_ string, h hash.Hash) error {
if h.IsEmpty() {
return nil
}
data, err := pgt.ns.ReadBytes(ctx, h)
if err != nil {
return err
}
t, err := DeserializeTrigger(ctx, data)
if err != nil {
return err
}
pgt.accessCache[t.ID] = t
tableID := id.NewTable(t.ID.SchemaName(), t.ID.TableName())
pgt.tableCache[tableID] = append(pgt.tableCache[tableID], t.ID)
pgt.idCache = append(pgt.idCache, t.ID)
return 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 (pgt *Collection) tableNameToID(schemaName string, formattedName string) id.Trigger {
names := strings.Split(formattedName, ".")
if len(names) != 2 {
return id.NullTrigger
}
return id.NewTrigger(schemaName, names[0], names[1])
}
// GetID implements the interface objinterface.RootObject.
func (trigger Trigger) GetID() id.Id {
return trigger.ID.AsId()
}
// GetRootObjectID implements the interface objinterface.RootObject.
func (trigger Trigger) GetRootObjectID() objinterface.RootObjectID {
return objinterface.RootObjectID_Triggers
}
// HashOf implements the interface objinterface.RootObject.
func (trigger Trigger) HashOf(ctx context.Context) (hash.Hash, error) {
data, err := trigger.Serialize(ctx)
if err != nil {
return hash.Hash{}, err
}
return hash.Of(data), nil
}
// Name implements the interface objinterface.RootObject.
func (trigger Trigger) Name() doltdb.TableName {
return TriggerIDToTableName(trigger.ID)
}
// TriggerIDToTableName returns the ID in a format that's better for user consumption.
func TriggerIDToTableName(trigID id.Trigger) doltdb.TableName {
return doltdb.TableName{
Name: fmt.Sprintf("%s.%s", trigID.TableName(), trigID.TriggerName()),
Schema: trigID.SchemaName(),
}
}
+125
View File
@@ -0,0 +1,125 @@
// Copyright 2025 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 triggers
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/merge"
"github.com/dolthub/dolt/go/store/hash"
"github.com/dolthub/dolt/go/store/prolly"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/flatbuffers/gen/serial"
)
// storage is used to read from and write to the root.
var storage = objinterface.RootObjectSerializer{
Bytes: (*serial.RootValue).TriggersBytes,
RootValueAdd: serial.RootValueAddTriggers,
}
// HandleMerge implements the interface objinterface.Collection.
func (*Collection) HandleMerge(ctx context.Context, mro merge.MergeRootObject) (doltdb.RootObject, *merge.MergeStats, error) {
ourTrigger := mro.OurRootObj.(Trigger)
theirTrigger := mro.TheirRootObj.(Trigger)
// Ensure that they have the same identifier
if ourTrigger.ID != theirTrigger.ID {
return nil, nil, errors.Newf("attempted to merge different triggers: `%s` and `%s`",
ourTrigger.Name().String(), theirTrigger.Name().String())
}
ourHash, err := ourTrigger.HashOf(ctx)
if err != nil {
return nil, nil, err
}
theirHash, err := theirTrigger.HashOf(ctx)
if err != nil {
return nil, nil, err
}
if ourHash.Equal(theirHash) {
return mro.OurRootObj, &merge.MergeStats{
Operation: merge.TableUnmodified,
Adds: 0,
Deletes: 0,
Modifications: 0,
DataConflicts: 0,
SchemaConflicts: 0,
ConstraintViolations: 0,
}, nil
}
// TODO: figure out a decent merge strategy
return nil, nil, errors.Errorf("unable to merge `%s`", theirTrigger.Name().String())
}
// LoadCollection implements the interface objinterface.Collection.
func (*Collection) LoadCollection(ctx context.Context, root objinterface.RootValue) (objinterface.Collection, error) {
return LoadTriggers(ctx, root)
}
// LoadCollectionHash implements the interface objinterface.Collection.
func (*Collection) LoadCollectionHash(ctx context.Context, root objinterface.RootValue) (hash.Hash, error) {
m, ok, err := storage.GetProllyMap(ctx, root)
if err != nil || !ok {
return hash.Hash{}, err
}
return m.HashOf(), nil
}
// LoadTriggers loads the triggers collection from the given root.
func LoadTriggers(ctx context.Context, root objinterface.RootValue) (*Collection, error) {
m, ok, err := storage.GetProllyMap(ctx, root)
if err != nil {
return nil, err
}
if !ok {
m, err = prolly.NewEmptyAddressMap(root.NodeStore())
if err != nil {
return nil, err
}
}
return NewCollection(ctx, m, root.NodeStore())
}
// ResolveNameFromObjects implements the interface objinterface.Collection.
func (*Collection) ResolveNameFromObjects(ctx context.Context, name doltdb.TableName, rootObjects []objinterface.RootObject) (doltdb.TableName, id.Id, error) {
tempCollection := Collection{
accessCache: make(map[id.Trigger]Trigger),
idCache: make([]id.Trigger, 0, len(rootObjects)),
}
for _, rootObject := range rootObjects {
if obj, ok := rootObject.(Trigger); ok {
tempCollection.accessCache[obj.ID] = obj
tempCollection.idCache = append(tempCollection.idCache, obj.ID)
}
}
return tempCollection.ResolveName(ctx, name)
}
// Serializer implements the interface objinterface.Collection.
func (*Collection) Serializer() objinterface.RootObjectSerializer {
return storage
}
// UpdateRoot implements the interface objinterface.Collection.
func (pgt *Collection) UpdateRoot(ctx context.Context, root objinterface.RootValue) (objinterface.RootValue, error) {
m, err := pgt.Map(ctx)
if err != nil {
return nil, err
}
return storage.WriteProllyMap(ctx, root, m)
}
+139
View File
@@ -0,0 +1,139 @@
// Copyright 2025 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 triggers
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// DeserializeRootObject implements the interface objinterface.Collection.
func (pgt *Collection) DeserializeRootObject(ctx context.Context, data []byte) (objinterface.RootObject, error) {
return DeserializeTrigger(ctx, data)
}
// DiffRootObjects implements the interface objinterface.Collection.
func (pgt *Collection) DiffRootObjects(ctx context.Context, fromHash string, ours objinterface.RootObject, theirs objinterface.RootObject, ancestor objinterface.RootObject) ([]objinterface.RootObjectDiff, objinterface.RootObject, error) {
return nil, nil, errors.New("trigger conflict detection has not yet been implemented")
}
// DropRootObject implements the interface objinterface.Collection.
func (pgt *Collection) DropRootObject(ctx context.Context, identifier id.Id) error {
if identifier.Section() != id.Section_Trigger {
return errors.Errorf(`trigger %s does not exist`, identifier.String())
}
return pgt.DropTrigger(ctx, id.Trigger(identifier))
}
// GetFieldType implements the interface objinterface.Collection.
func (pgt *Collection) GetFieldType(ctx context.Context, fieldName string) *pgtypes.DoltgresType {
return nil
}
// GetID implements the interface objinterface.Collection.
func (pgt *Collection) GetID() objinterface.RootObjectID {
return objinterface.RootObjectID_Triggers
}
// GetRootObject implements the interface objinterface.Collection.
func (pgt *Collection) GetRootObject(ctx context.Context, identifier id.Id) (objinterface.RootObject, bool, error) {
if identifier.Section() != id.Section_Trigger {
return nil, false, nil
}
f, err := pgt.GetTrigger(ctx, id.Trigger(identifier))
return f, err == nil && f.ID.IsValid(), err
}
// HasRootObject implements the interface objinterface.Collection.
func (pgt *Collection) HasRootObject(ctx context.Context, identifier id.Id) (bool, error) {
if identifier.Section() != id.Section_Trigger {
return false, nil
}
return pgt.HasTrigger(ctx, id.Trigger(identifier)), nil
}
// IDToTableName implements the interface objinterface.Collection.
func (pgt *Collection) IDToTableName(identifier id.Id) doltdb.TableName {
if identifier.Section() != id.Section_Trigger {
return doltdb.TableName{}
}
return TriggerIDToTableName(id.Trigger(identifier))
}
// IterAll implements the interface objinterface.Collection.
func (pgt *Collection) IterAll(ctx context.Context, callback func(rootObj objinterface.RootObject) (stop bool, err error)) error {
return pgt.IterateTriggers(ctx, func(t Trigger) (stop bool, err error) {
return callback(t)
})
}
// IterIDs implements the interface objinterface.Collection.
func (pgt *Collection) IterIDs(ctx context.Context, callback func(identifier id.Id) (stop bool, err error)) error {
return pgt.iterateIDs(ctx, func(trigID id.Trigger) (stop bool, err error) {
return callback(trigID.AsId())
})
}
// PutRootObject implements the interface objinterface.Collection.
func (pgt *Collection) PutRootObject(ctx context.Context, rootObj objinterface.RootObject) error {
t, ok := rootObj.(Trigger)
if !ok {
return errors.Newf("invalid trigger root object: %T", rootObj)
}
return pgt.AddTrigger(ctx, t)
}
// RenameRootObject implements the interface objinterface.Collection.
func (pgt *Collection) RenameRootObject(ctx context.Context, oldName id.Id, newName id.Id) error {
if !oldName.IsValid() || !newName.IsValid() || oldName.Section() != newName.Section() || oldName.Section() != id.Section_Trigger {
return errors.New("cannot rename trigger due to invalid name")
}
oldTriggerName := id.Trigger(oldName)
newTriggerName := id.Trigger(newName)
t, err := pgt.GetTrigger(ctx, oldTriggerName)
if err != nil {
return err
}
if err = pgt.DropTrigger(ctx, newTriggerName); err != nil {
return err
}
t.ID = newTriggerName
return pgt.AddTrigger(ctx, t)
}
// ResolveName implements the interface objinterface.Collection.
func (pgt *Collection) ResolveName(ctx context.Context, name doltdb.TableName) (doltdb.TableName, id.Id, error) {
rawID, err := pgt.resolveName(ctx, name.Schema, name.Name)
if err != nil || !rawID.IsValid() {
return doltdb.TableName{}, id.Null, err
}
return TriggerIDToTableName(rawID), rawID.AsId(), nil
}
// TableNameToID implements the interface objinterface.Collection.
func (pgt *Collection) TableNameToID(name doltdb.TableName) id.Id {
return pgt.tableNameToID(name.Schema, name.Name).AsId()
}
// UpdateField implements the interface objinterface.Collection.
func (pgt *Collection) UpdateField(ctx context.Context, rootObject objinterface.RootObject, fieldName string, newValue any) (objinterface.RootObject, error) {
return nil, errors.New("updating through the conflicts table for this object type is not yet supported")
}
+118
View File
@@ -0,0 +1,118 @@
// 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 triggers
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/server/plpgsql"
"github.com/dolthub/doltgresql/utils"
)
// Serialize returns the Trigger as a byte slice. If the Trigger is invalid, then this returns a nil slice.
func (trigger Trigger) Serialize(ctx context.Context) ([]byte, error) {
if !trigger.ID.IsValid() {
return nil, nil
}
// Initialize the writer and version
writer := utils.NewWriter(256)
writer.VariableUint(0) // Version
// Write the trigger data
writer.Id(trigger.ID.AsId())
writer.Id(trigger.Function.AsId())
writer.Uint8(uint8(trigger.Timing))
writer.Bool(trigger.ForEachRow)
writer.Uint8(uint8(trigger.Deferrable))
writer.Id(trigger.ReferencedTableName.AsId())
writer.Bool(trigger.Constraint)
writer.String(trigger.OldTransitionName)
writer.String(trigger.NewTransitionName)
writer.StringSlice(trigger.Arguments)
writer.String(trigger.Definition)
// Write the WHEN operations
writer.VariableUint(uint64(len(trigger.When)))
for _, op := range trigger.When {
writer.Uint16(uint16(op.OpCode))
writer.String(op.PrimaryData)
writer.StringSlice(op.SecondaryData)
writer.String(op.Target)
writer.Int32(int32(op.Index))
writer.StringMap(op.Options)
}
// Write the events
writer.VariableUint(uint64(len(trigger.Events)))
for _, event := range trigger.Events {
writer.Uint8(uint8(event.Type))
writer.StringSlice(event.ColumnNames)
}
// Returns the data
return writer.Data(), nil
}
// DeserializeTrigger returns the Trigger that was serialized in the byte slice. Returns an empty Trigger (invalid ID)
// if data is nil or empty.
func DeserializeTrigger(ctx context.Context, data []byte) (Trigger, error) {
if len(data) == 0 {
return Trigger{}, nil
}
reader := utils.NewReader(data)
version := reader.VariableUint()
if version != 0 {
return Trigger{}, errors.Errorf("version %d of triggers is not supported, please upgrade the server", version)
}
// Read from the reader
t := Trigger{}
t.ID = id.Trigger(reader.Id())
t.Function = id.Function(reader.Id())
t.Timing = TriggerTiming(reader.Uint8())
t.ForEachRow = reader.Bool()
t.Deferrable = TriggerDeferrable(reader.Uint8())
t.ReferencedTableName = id.Table(reader.Id())
t.Constraint = reader.Bool()
t.OldTransitionName = reader.String()
t.NewTransitionName = reader.String()
t.Arguments = reader.StringSlice()
t.Definition = reader.String()
// Read the WHEN operations
opCount := reader.VariableUint()
t.When = make([]plpgsql.InterpreterOperation, opCount)
for opIdx := uint64(0); opIdx < opCount; opIdx++ {
op := plpgsql.InterpreterOperation{}
op.OpCode = plpgsql.OpCode(reader.Uint16())
op.PrimaryData = reader.String()
op.SecondaryData = reader.StringSlice()
op.Target = reader.String()
op.Index = int(reader.Int32())
op.Options = reader.StringMap()
t.When[opIdx] = op
}
// Read the events
eventCount := reader.VariableUint()
t.Events = make([]TriggerEvent, eventCount)
for eventIdx := uint64(0); eventIdx < eventCount; eventIdx++ {
t.Events[eventIdx].Type = TriggerEventType(reader.Uint8())
t.Events[eventIdx].ColumnNames = reader.StringSlice()
}
if !reader.IsEmpty() {
return Trigger{}, errors.Errorf("extra data found while deserializing a trigger")
}
// Return the deserialized object
return t, nil
}
+188
View File
@@ -0,0 +1,188 @@
// Copyright 2025 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 typecollection
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/merge"
"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/doltgresql/core/id"
merge2 "github.com/dolthub/doltgresql/core/merge"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/flatbuffers/gen/serial"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// storage is used to read from and write to the root.
var storage = objinterface.RootObjectSerializer{
Bytes: (*serial.RootValue).TypesBytes,
RootValueAdd: serial.RootValueAddTypes,
}
// HandleMerge implements the interface objinterface.Collection.
func (*TypeCollection) HandleMerge(ctx context.Context, mro merge.MergeRootObject) (doltdb.RootObject, *merge.MergeStats, error) {
ourType := mro.OurRootObj.(TypeWrapper).Type
theirType := mro.TheirRootObj.(TypeWrapper).Type
// Ensure that they have the same identifier
if ourType.ID != theirType.ID {
return nil, nil, errors.Newf("attempted to merge different types: `%s` and `%s`",
ourType.ID.TypeName(), theirType.ID.TypeName())
}
// Different types with the same name cannot be merged. (e.g.: 'domain' type and 'base' type with the same name)
if ourType.TypType != theirType.TypType {
return nil, nil, errors.Errorf(`cannot merge type "%s" because type types do not match: '%s' and '%s'"`,
theirType.ID.TypeName(), ourType.TypType, theirType.TypType)
}
// Check if an ancestor is present
var ancType *pgtypes.DoltgresType
hasAncestor := false
if mro.AncestorRootObj != nil {
ancType = mro.AncestorRootObj.(TypeWrapper).Type
hasAncestor = true
}
mergedType := ourType.Copy()
switch theirType.TypType {
case pgtypes.TypeType_Domain:
if ourType.BaseTypeType.ID != theirType.BaseTypeType.ID {
// TODO: we can extend on this in the future (e.g.: maybe uses preferred type?)
return nil, nil, errors.Errorf(`base types of domain type "%s" do not match`, theirType.ID.TypeName())
}
var err error
mergedType.Default = merge2.ResolveMergeValues(ourType.Default, theirType.Default, ancType.Default, hasAncestor, func(ourDefault, theirDefault string) string {
if ourType.Default == "" {
return theirDefault
} else if theirType.Default != "" && ourType.Default != theirType.Default {
err = errors.Errorf(`default values of domain type "%s" do not match`, theirType.ID.TypeName())
return ourDefault
} else {
return ourDefault
}
})
if err != nil {
return nil, nil, err
}
// if either of types defined as NOT NULL, take NOT NULL
mergedType.NotNull = merge2.ResolveMergeValues(ourType.NotNull, theirType.NotNull, ancType.NotNull, hasAncestor, func(ourNotNull, theirNotNull bool) bool {
return ourNotNull || theirNotNull
})
if len(theirType.Checks) > 0 {
// TODO: check for duplicate check constraints
ourType.Checks = append(ourType.Checks, theirType.Checks...)
}
return TypeWrapper{Type: mergedType}, &merge.MergeStats{
Operation: merge.TableModified,
Adds: 0,
Deletes: 0,
Modifications: 1,
DataConflicts: 0,
SchemaConflicts: 0,
ConstraintViolations: 0,
}, nil
default:
// TODO: support merge for other types. (base, range, etc.)
return nil, nil, errors.Newf("cannot merge `%s` due to unsupported type", ourType.ID.TypeName())
}
}
// LoadCollection implements the interface objinterface.Collection.
func (*TypeCollection) LoadCollection(ctx context.Context, root objinterface.RootValue) (objinterface.Collection, error) {
return LoadTypes(ctx, root)
}
// LoadCollectionHash implements the interface objinterface.Collection.
func (*TypeCollection) LoadCollectionHash(ctx context.Context, root objinterface.RootValue) (hash.Hash, error) {
m, ok, err := storage.GetProllyMap(ctx, root)
if err != nil || !ok {
return hash.Hash{}, err
}
return m.HashOf(), nil
}
// LoadTypes loads the types collection from the given root.
func LoadTypes(ctx context.Context, root objinterface.RootValue) (*TypeCollection, error) {
m, ok, err := storage.GetProllyMap(ctx, root)
if err != nil {
return nil, err
}
if !ok {
m, err = prolly.NewEmptyAddressMap(root.NodeStore())
if err != nil {
return nil, err
}
}
return &TypeCollection{
accessedMap: make(map[id.Type]*pgtypes.DoltgresType),
initCache: make(map[id.Type]*pgtypes.DoltgresType),
underlyingMap: m,
ns: root.NodeStore(),
}, nil
}
// ResolveNameFromObjects implements the interface objinterface.Collection.
func (*TypeCollection) ResolveNameFromObjects(ctx context.Context, name doltdb.TableName, rootObjects []objinterface.RootObject) (doltdb.TableName, id.Id, error) {
// First we'll check if there are any objects to search through in the first place
accessedMap := make(map[id.Type]*pgtypes.DoltgresType)
for _, rootObject := range rootObjects {
if obj, ok := rootObject.(TypeWrapper); ok && obj.Type != nil {
accessedMap[obj.Type.ID] = obj.Type
}
}
if len(accessedMap) == 0 {
return doltdb.TableName{}, id.Null, nil
}
// There are root objects to search through, so we'll create a temporary store
ns := tree.NewTestNodeStore()
addressMap, err := prolly.NewEmptyAddressMap(ns)
if err != nil {
return doltdb.TableName{}, id.Null, err
}
tempCollection := TypeCollection{
accessedMap: accessedMap,
initCache: make(map[id.Type]*pgtypes.DoltgresType),
underlyingMap: addressMap,
ns: ns,
}
return tempCollection.ResolveName(ctx, name)
}
// Serializer implements the interface objinterface.Collection.
func (*TypeCollection) Serializer() objinterface.RootObjectSerializer {
return storage
}
// UpdateRoot implements the interface objinterface.Collection.
func (pgs *TypeCollection) UpdateRoot(ctx context.Context, root objinterface.RootValue) (objinterface.RootValue, error) {
initialCount, initialCountErr := pgs.underlyingMap.Count()
m, err := pgs.Map(ctx)
if err != nil {
return nil, err
}
if initialCountErr == nil && initialCount == 0 {
if currentCount, currentCountErr := m.Count(); currentCountErr == nil && currentCount == 0 {
// In this specific case, we can guarantee that we haven't updated anything, so we won't write to the root.
// This preserves that the collection is only written when there's a meaningful data change, otherwise
// writing an empty collection will change the hash of a root if it previously had no collection.
return root, nil
}
}
return storage.WriteProllyMap(ctx, root, m)
}
+193
View File
@@ -0,0 +1,193 @@
// Copyright 2025 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 typecollection
import (
"context"
"io"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/store/hash"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// DeserializeRootObject implements the interface objinterface.Collection.
func (pgs *TypeCollection) DeserializeRootObject(ctx context.Context, data []byte) (objinterface.RootObject, error) {
sqlCtx, _ := ctx.(*sql.Context)
t, err := pgtypes.DeserializeType(sqlCtx, data)
if err != nil {
return nil, err
}
return TypeWrapper{
Type: t.(*pgtypes.DoltgresType),
}, nil
}
// DiffRootObjects implements the interface objinterface.Collection.
func (pgs *TypeCollection) DiffRootObjects(ctx context.Context, fromHash string, ours objinterface.RootObject, theirs objinterface.RootObject, ancestor objinterface.RootObject) ([]objinterface.RootObjectDiff, objinterface.RootObject, error) {
return nil, nil, errors.New("type conflict detection has not yet been implemented")
}
// DropRootObject implements the interface objinterface.Collection.
func (pgs *TypeCollection) DropRootObject(ctx context.Context, identifier id.Id) error {
if identifier.Section() != id.Section_Type {
return errors.Errorf(`type %s does not exist`, identifier.String())
}
return pgs.DropType(ctx, id.Type(identifier))
}
// GetFieldType implements the interface objinterface.Collection.
func (pgs *TypeCollection) GetFieldType(ctx context.Context, fieldName string) *pgtypes.DoltgresType {
return nil
}
// GetID implements the interface objinterface.Collection.
func (pgs *TypeCollection) GetID() objinterface.RootObjectID {
return objinterface.RootObjectID_Types
}
// GetRootObject implements the interface objinterface.Collection.
func (pgs *TypeCollection) GetRootObject(ctx context.Context, identifier id.Id) (objinterface.RootObject, bool, error) {
if identifier.Section() != id.Section_Type {
return nil, false, nil
}
typ, err := pgs.GetType(ctx, id.Type(identifier))
return TypeWrapper{Type: typ}, err == nil && typ != nil, err
}
// HasRootObject implements the interface objinterface.Collection.
func (pgs *TypeCollection) HasRootObject(ctx context.Context, identifier id.Id) (bool, error) {
if identifier.Section() != id.Section_Type {
return false, nil
}
return pgs.HasType(ctx, id.Type(identifier)), nil
}
// IDToTableName implements the interface objinterface.Collection.
func (pgs *TypeCollection) IDToTableName(identifier id.Id) doltdb.TableName {
if identifier.Section() != id.Section_Type {
return doltdb.TableName{}
}
typID := id.Type(identifier)
return doltdb.TableName{
Name: typID.TypeName(),
Schema: typID.SchemaName(),
}
}
// IterAll implements the interface objinterface.Collection. As this is specifically used in the root object context, we
// do not iterate built-in types. In all other situations, we should use IterateTypes.
func (pgs *TypeCollection) IterAll(ctx context.Context, callback func(rootObj objinterface.RootObject) (stop bool, err error)) error {
// We write the cache so that we only need to worry about the underlying map
sqlCtx, _ := ctx.(*sql.Context)
if err := pgs.writeCache(ctx); err != nil {
return err
}
err := pgs.underlyingMap.IterAll(ctx, func(_ string, v hash.Hash) error {
data, err := pgs.ns.ReadBytes(ctx, v)
if err != nil {
return err
}
t, err := pgtypes.DeserializeType(sqlCtx, data)
if err != nil {
return err
}
stop, err := callback(TypeWrapper{t.(*pgtypes.DoltgresType)})
if err != nil {
return err
} else if stop {
return io.EOF
} else {
return nil
}
})
return err
}
// IterIDs implements the interface objinterface.Collection. As this is specifically used in the root object context, we
// do not iterate the IDs of built-in types. In all other situations, we should use IterateTypes (even if you only need
// the IDs).
func (pgs *TypeCollection) IterIDs(ctx context.Context, callback func(identifier id.Id) (stop bool, err error)) error {
// We write the cache so that we only need to worry about the underlying map
if err := pgs.writeCache(ctx); err != nil {
return err
}
err := pgs.underlyingMap.IterAll(ctx, func(k string, _ hash.Hash) error {
stop, err := callback(id.Id(k))
if err != nil {
return err
} else if stop {
return io.EOF
} else {
return nil
}
})
return err
}
// PutRootObject implements the interface objinterface.Collection.
func (pgs *TypeCollection) PutRootObject(ctx context.Context, rootObj objinterface.RootObject) error {
typ, ok := rootObj.(TypeWrapper)
if !ok || typ.Type == nil {
return errors.Newf("invalid type root object: %T", rootObj)
}
return pgs.CreateType(ctx, typ.Type)
}
// RenameRootObject implements the interface objinterface.Collection.
func (pgs *TypeCollection) RenameRootObject(ctx context.Context, oldName id.Id, newName id.Id) error {
if !oldName.IsValid() || !newName.IsValid() || oldName.Section() != newName.Section() || oldName.Section() != id.Section_Type {
return errors.New("cannot rename type due to invalid name")
}
oldTypeName := id.Type(oldName)
newTypeName := id.Type(newName)
typ, err := pgs.GetType(ctx, oldTypeName)
if err != nil {
return err
}
if err = pgs.DropType(ctx, oldTypeName); err != nil {
return err
}
newType := typ.Copy()
newType.ID = newTypeName
return pgs.CreateType(ctx, newType)
}
// ResolveName implements the interface objinterface.Collection.
func (pgs *TypeCollection) ResolveName(ctx context.Context, name doltdb.TableName) (doltdb.TableName, id.Id, error) {
rawID, err := pgs.resolveName(ctx, name.Schema, name.Name)
if err != nil || !rawID.IsValid() {
return doltdb.TableName{}, id.Null, err
}
return doltdb.TableName{
Name: rawID.TypeName(),
Schema: rawID.SchemaName(),
}, rawID.AsId(), nil
}
// TableNameToID implements the interface objinterface.Collection.
func (pgs *TypeCollection) TableNameToID(name doltdb.TableName) id.Id {
return id.NewType(name.Schema, name.Name).AsId()
}
// UpdateField implements the interface objinterface.Collection.
func (pgs *TypeCollection) UpdateField(ctx context.Context, rootObject objinterface.RootObject, fieldName string, newValue any) (objinterface.RootObject, error) {
return nil, errors.New("updating through the conflicts table for this object type is not yet supported")
}
+579
View File
@@ -0,0 +1,579 @@
// 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 typecollection
import (
"context"
"io"
"sort"
"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/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
parsertypes "github.com/dolthub/doltgresql/postgres/parser/types"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// anonymousCompositePrefix is the prefix for anonymous composite type names. These types are not stored on
// disk, but instead are created dynamically as needed.
const anonymousCompositePrefix = "table("
// anonymousCompositeSuffix is the suffix for anonymous composite type names.
const anonymousCompositeSuffix = ")"
// TypeCollection is a collection of all types (both built-in and user defined).
type TypeCollection struct {
accessedMap map[id.Type]*pgtypes.DoltgresType
initCache map[id.Type]*pgtypes.DoltgresType // This is only used by the function `WithCachedType`
underlyingMap prolly.AddressMap
ns tree.NodeStore
}
// TypeWrapper is a wrapper around a type that allows it to be used as a root object.
type TypeWrapper struct {
Type *pgtypes.DoltgresType
}
var _ objinterface.Collection = (*TypeCollection)(nil)
var _ objinterface.RootObject = TypeWrapper{}
var _ doltdb.RootObject = TypeWrapper{}
// CreateType creates a new type.
func (pgs *TypeCollection) CreateType(ctx context.Context, typ *pgtypes.DoltgresType) error {
// First we check the built-in types
if _, ok := pgtypes.IDToBuiltInDoltgresType[typ.ID]; ok {
return pgtypes.ErrTypeAlreadyExists.New(typ.Name())
}
// Ensure that the type does not already exist in the cache or underlying map
if _, ok := pgs.accessedMap[typ.ID]; ok {
return pgtypes.ErrTypeAlreadyExists.New(typ.Name())
}
if ok, err := pgs.underlyingMap.Has(ctx, string(typ.ID)); err != nil {
return err
} else if ok {
return pgtypes.ErrTypeAlreadyExists.New(typ.Name())
}
// Add it to our cache, which will be written when we do anything permanent
pgs.accessedMap[typ.ID] = typ
return nil
}
// DropType drops an existing type.
func (pgs *TypeCollection) DropType(ctx context.Context, names ...id.Type) (err error) {
// First we'll check if we're trying to drop a built-in type
for _, name := range names {
if _, ok := pgtypes.IDToBuiltInDoltgresType[name]; ok {
// TODO: investigate why we sometimes attempt to drop built-in types
return nil
}
}
// We need to clear the cache so that we only need to worry about the underlying map
if err = pgs.writeCache(ctx); err != nil {
return err
}
for _, name := range names {
if ok, err := pgs.underlyingMap.Has(ctx, string(name)); err != nil {
return err
} else if !ok {
return pgtypes.ErrTypeDoesNotExist.New(name.TypeName())
}
}
// Now we'll remove the types from the underlying map
mapEditor := pgs.underlyingMap.Editor()
for _, name := range names {
if err = mapEditor.Delete(ctx, string(name)); err != nil {
return err
}
}
pgs.underlyingMap, err = mapEditor.Flush(ctx)
return err
}
// GetAllTypes returns a map containing all types in the collection, grouped by the schema they're contained in.
// Each type array is also sorted by the type name. It includes built-in types.
func (pgs *TypeCollection) GetAllTypes(ctx context.Context) (typeMap map[string][]*pgtypes.DoltgresType, schemaNames []string, totalCount int, err error) {
schemaNamesMap := make(map[string]struct{})
typeMap = make(map[string][]*pgtypes.DoltgresType)
err = pgs.IterateTypes(ctx, func(t *pgtypes.DoltgresType) (stop bool, err error) {
schemaNamesMap[t.ID.SchemaName()] = struct{}{}
typeMap[t.ID.SchemaName()] = append(typeMap[t.ID.SchemaName()], t)
totalCount++
return false, nil
})
if err != nil {
return nil, nil, 0, err
}
// Sort the types in the type map
for _, seqs := range typeMap {
sort.Slice(seqs, func(i, j int) bool {
return seqs[i].ID < seqs[j].ID
})
}
// Create and sort the schema names
schemaNames = make([]string, 0, len(schemaNamesMap))
for name := range schemaNamesMap {
schemaNames = append(schemaNames, name)
}
sort.Slice(schemaNames, func(i, j int) bool {
return schemaNames[i] < schemaNames[j]
})
return
}
// GetDomainType returns a domain type with the given schema and name.
// Returns nil if the type cannot be found. It checks for domain type.
func (pgs *TypeCollection) GetDomainType(ctx context.Context, name id.Type) (*pgtypes.DoltgresType, error) {
t, err := pgs.GetType(ctx, name)
if err != nil || t == nil {
return nil, err
}
if t.TypType == pgtypes.TypeType_Domain {
return t, nil
}
return nil, nil
}
// GetType returns the type with the given schema and name.
// Returns nil if the type cannot be found.
func (pgs *TypeCollection) GetType(ctx context.Context, name id.Type) (*pgtypes.DoltgresType, error) {
// Check the built-in types first
if t, ok := pgtypes.IDToBuiltInDoltgresType[name]; ok {
return t, nil
}
// Subsequent loads are cached
if t, ok := pgs.accessedMap[name]; ok {
return t, nil
}
if t, ok := pgs.initCache[name]; ok {
return t, nil
}
sqlCtx, ok := ctx.(*sql.Context)
if !ok {
return nil, errors.New("type collection requires a SQL context")
}
// The initial load is from the internal map
h, err := pgs.underlyingMap.Get(ctx, string(name))
if err != nil {
return nil, err
}
if h.IsEmpty() {
// If this is an anonymous composite type, create it dynamically
if isAnonymousCompositeType(name) {
return pgs.createAnonymousCompositeType(sqlCtx, name)
}
// Table composite types are computed on the fly from the live table schema rather than
// stored as root objects (storing them would create a naming collision with the actual
// table in Dolt's diff layer, since both map to the same doltdb.TableName).
typeName := name.TypeName()
// A name starting with "_" may be the implicit array type for a table's composite row
// type. Resolve the element type first (which handles the table lookup), then wrap it.
if strings.HasPrefix(typeName, "_") {
elemType, err := pgs.GetType(ctx, id.NewType(name.SchemaName(), typeName[1:]))
if err != nil || elemType == nil {
return nil, err
}
return pgtypes.CreateArrayTypeFromBaseType(elemType), nil
}
tbl, schema, err := pgs.getTable(sqlCtx, name.SchemaName(), typeName)
if err != nil || tbl == nil {
return nil, err
}
return pgs.tableToType(sqlCtx, tbl, schema)
}
data, err := pgs.ns.ReadBytes(ctx, h)
if err != nil {
return nil, err
}
t, err := pgtypes.DeserializeType(sqlCtx, data)
if err != nil {
return nil, err
}
pgt := t.(*pgtypes.DoltgresType)
pgs.accessedMap[pgt.ID] = pgt
return pgt, nil
}
// ResolveType returns the type given if there's an exact match, or the closest matching type if the exact ID cannot be
// found. In general, this should only be used in cases where we are not sure of the schema name, as this is
// significantly slower than GetType. Returns an error if the type cannot be resolved, unlike GetType which returns a
// nil if the type is not found.
func (pgs *TypeCollection) ResolveType(ctx context.Context, name id.Type) (*pgtypes.DoltgresType, error) {
if t, err := pgs.GetType(ctx, name); err != nil {
return nil, err
} else if t != nil && t.IsResolvedType() {
return t, nil
}
resolvedId, err := pgs.resolveName(ctx, name.SchemaName(), name.TypeName())
if err != nil {
return nil, err
}
t, err := pgs.GetType(ctx, resolvedId)
if err != nil {
return nil, err
}
if !t.IsResolvedType() {
return nil, errors.Errorf("unable to resolve type `%s`", name.TypeName())
}
return t, nil
}
// WithCachedType executes the given function while caching the given type, which allows for recursive type
// initialization to reference unfinished types.
func (pgs *TypeCollection) WithCachedType(typeToCache *pgtypes.DoltgresType, f func()) {
pgs.initCache[typeToCache.ID] = typeToCache
defer func() {
delete(pgs.initCache, typeToCache.ID)
}()
f()
}
// isAnonymousCompositeType return true if |returnType| represents an anonymous composite return type
// for a function (i.e. the function was declared as "RETURNS TABLE(...)").
func isAnonymousCompositeType(returnType id.Type) bool {
typeName := returnType.TypeName()
return strings.HasPrefix(typeName, anonymousCompositePrefix) &&
strings.HasSuffix(typeName, anonymousCompositeSuffix)
}
// createAnonymousCompositeType creates a new DoltgresType for the anonymous composite return type for a function,
// as represented by |returnType|.
func (pgs *TypeCollection) createAnonymousCompositeType(ctx *sql.Context, returnType id.Type) (*pgtypes.DoltgresType, error) {
typeName := returnType.TypeName()
attributeTypes := typeName[len(anonymousCompositePrefix) : len(typeName)-len(anonymousCompositeSuffix)]
attributeTypesSlice := strings.Split(attributeTypes, ",")
attrs := make([]pgtypes.CompositeAttribute, len(attributeTypesSlice))
for i, attributeNameAndType := range attributeTypesSlice {
split := strings.Split(attributeNameAndType, ":")
if len(split) != 2 {
return nil, errors.Errorf("unexpected anonymous composite type attribute syntax: %s", attributeNameAndType)
}
// Attribute names may be standard SQL names such as "SMALLINT", so we have to normalize such names
attributeName := split[1]
var attributeType *pgtypes.DoltgresType
var err error
if parserT, ok, _ := parsertypes.TypeForNonKeywordTypeName(strings.ToLower(attributeName)); ok {
typeId := id.Cache().ToInternal(uint32(parserT.Oid()))
if typeId.IsValid() && typeId.Section() == id.Section_Type {
attributeType, err = pgs.ResolveType(ctx, id.Type(typeId))
if err != nil {
return nil, err
}
}
}
if !attributeType.IsResolvedType() {
// We check if a schema is present by the existence of a "."
schemaName := ""
if strings.Contains(attributeName, ".") {
typeSplit := strings.SplitN(attributeName, ".", 2)
schemaName = typeSplit[0]
attributeName = typeSplit[1]
}
attributeType, err = pgs.ResolveType(ctx, id.NewType(schemaName, attributeName))
if err != nil {
return nil, err
}
}
attrs[i] = pgtypes.NewCompositeAttribute(ctx, id.Null, split[0], attributeType, int16(i), "")
}
return pgtypes.NewCompositeType(ctx, id.Null, nil, returnType, attrs), nil
}
// HasType checks if a type exists with given schema and type name.
func (pgs *TypeCollection) HasType(ctx context.Context, name id.Type) bool {
// We can check the built-in types first
if _, ok := pgtypes.IDToBuiltInDoltgresType[name]; ok {
return true
}
// Now we'll check our created types
if _, ok := pgs.accessedMap[name]; ok {
return true
}
ok, err := pgs.underlyingMap.Has(ctx, string(name))
if err == nil && ok {
return true
}
// Table composite types are not stored; check the table as a fallback.
sqlCtx, ok := ctx.(*sql.Context)
if !ok {
return false
}
tbl, _, err := pgs.getTable(sqlCtx, name.SchemaName(), name.TypeName())
return err == nil && tbl != nil
}
// resolveName returns the fully resolved name of the given type. Returns an error if the name is ambiguous.
func (pgs *TypeCollection) resolveName(ctx context.Context, schemaName string, typeName string) (id.Type, error) {
// TODO: this should probably check table names as well since tables create composite types matching their rows
// First check for an exact match in the built-in types
inputID := id.NewType(schemaName, typeName)
if _, ok := pgtypes.IDToBuiltInDoltgresType[inputID]; ok {
return inputID, nil
}
// Iterate over all the built-in names for a relative match
var resolvedID id.Type
for _, typ := range pgtypes.GetAllBuitInTypes() {
if strings.EqualFold(typeName, typ.ID.TypeName()) {
if len(schemaName) > 0 && !strings.EqualFold(schemaName, typ.ID.SchemaName()) {
continue
}
if resolvedID.IsValid() {
return id.NullType, errors.Errorf("`%s.%s` is ambiguous, matches `%s.%s` and `%s.%s`",
schemaName, typeName, typ.ID.SchemaName(), typ.ID.TypeName(), resolvedID.SchemaName(), resolvedID.TypeName())
}
resolvedID = typ.ID
}
}
// Iterate over the initialization cache in case this is during a type initialization loop
for _, typ := range pgs.initCache {
if strings.EqualFold(typeName, typ.ID.TypeName()) {
if len(schemaName) > 0 && !strings.EqualFold(schemaName, typ.ID.SchemaName()) {
continue
}
if resolvedID.IsValid() {
return id.NullType, errors.Errorf("`%s.%s` is ambiguous, matches `%s.%s` and `%s.%s`",
schemaName, typeName, typ.ID.SchemaName(), typ.ID.TypeName(), resolvedID.SchemaName(), resolvedID.TypeName())
}
resolvedID = typ.ID
}
}
// We write the cache so that we only need to worry about the underlying map
if err := pgs.writeCache(ctx); err != nil {
return id.NullType, err
}
// Check for an exact match in the underlying map
ok, err := pgs.underlyingMap.Has(ctx, string(inputID))
if err != nil {
return id.NullType, err
} else if ok {
// We don't bother looking if there's an existing match, since this is an exact match (so no ambiguity)
return inputID, nil
}
// Iterate over all the names in the map
err = pgs.underlyingMap.IterAll(ctx, func(k string, _ hash.Hash) error {
typeID := id.Type(k)
if strings.EqualFold(typeName, typeID.TypeName()) {
if len(schemaName) > 0 && !strings.EqualFold(schemaName, typeID.SchemaName()) {
return nil
}
if resolvedID.IsValid() {
return errors.Errorf("`%s.%s` is ambiguous, matches `%s.%s` and `%s.%s`",
schemaName, typeName, typeID.SchemaName(), typeID.TypeName(), resolvedID.SchemaName(), resolvedID.TypeName())
}
resolvedID = typeID
}
return nil
})
if err != nil {
return id.NullType, err
}
return resolvedID, nil
}
// IterateTypes iterates over all types in the collection.
func (pgs *TypeCollection) IterateTypes(ctx context.Context, f func(typ *pgtypes.DoltgresType) (stop bool, err error)) error {
// TODO: this should probably iterate tables as well since tables create composite types matching their rows
// We can iterate the built-in types first
for _, t := range pgtypes.GetAllBuitInTypes() {
stop, err := f(t)
if err != nil || stop {
return err
}
}
sqlCtx, ok := ctx.(*sql.Context)
if !ok {
return errors.New("type collection requires a SQL context")
}
// We write the cache so that we only need to worry about the underlying map
if err := pgs.writeCache(ctx); err != nil {
return err
}
err := pgs.underlyingMap.IterAll(ctx, func(_ string, v hash.Hash) error {
data, err := pgs.ns.ReadBytes(ctx, v)
if err != nil {
return err
}
t, err := pgtypes.DeserializeType(sqlCtx, data)
if err != nil {
return err
}
stop, err := f(t.(*pgtypes.DoltgresType))
if err != nil {
return err
} else if stop {
return io.EOF
} else {
return nil
}
})
return err
}
// Clone returns a new *TypeCollection with the same contents as the original.
func (pgs *TypeCollection) Clone(ctx context.Context) *TypeCollection {
newCollection := &TypeCollection{
accessedMap: make(map[id.Type]*pgtypes.DoltgresType),
initCache: make(map[id.Type]*pgtypes.DoltgresType),
underlyingMap: pgs.underlyingMap,
ns: pgs.ns,
}
for typeID, t := range pgs.accessedMap {
newCollection.accessedMap[typeID] = t
}
return newCollection
}
// Map writes any cached types to the underlying map, and then returns the underlying map.
func (pgs *TypeCollection) Map(ctx context.Context) (prolly.AddressMap, error) {
if err := pgs.writeCache(ctx); err != nil {
return prolly.AddressMap{}, err
}
return pgs.underlyingMap, nil
}
// GetID implements the interface objinterface.RootObject.
func (t TypeWrapper) GetID() id.Id {
if t.Type != nil {
return t.Type.ID.AsId()
}
return id.Null
}
// GetRootObjectID implements the interface objinterface.RootObject.
func (t TypeWrapper) GetRootObjectID() objinterface.RootObjectID {
return objinterface.RootObjectID_Types
}
// HashOf implements the interface objinterface.RootObject.
func (t TypeWrapper) HashOf(ctx context.Context) (hash.Hash, error) {
if t.Type != nil {
return hash.Of(t.Type.Serialize()), nil
}
return hash.Hash{}, nil
}
// Name implements the interface objinterface.RootObject.
func (t TypeWrapper) Name() doltdb.TableName {
if t.Type != nil {
return doltdb.TableName{
Name: t.Type.ID.TypeName(),
Schema: t.Type.ID.SchemaName(),
}
}
return doltdb.TableName{}
}
// Serialize implements the interface objinterface.RootObject.
func (t TypeWrapper) Serialize(ctx context.Context) ([]byte, error) {
if t.Type != nil {
return t.Type.Serialize(), nil
}
return nil, nil
}
// writeCache writes every type in the cache to the underlying map.
func (pgs *TypeCollection) writeCache(ctx context.Context) (err error) {
if len(pgs.accessedMap) == 0 {
return nil
}
mapEditor := pgs.underlyingMap.Editor()
for _, t := range pgs.accessedMap {
data := t.Serialize()
h, err := pgs.ns.WriteBytes(ctx, data)
if err != nil {
return err
}
if err = mapEditor.Update(ctx, string(t.ID), h); err != nil {
return err
}
}
// Assign underlyingMap only after the error check. Flush returns a
// zero AddressMap on failure, which would corrupt the TypeCollection.
flushed, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pgs.underlyingMap = flushed
clear(pgs.accessedMap)
return nil
}
// getTable returns the SQL table that matches the given schema and table name. Returns a nil table if one is not found.
// This is intended for use with tableToType.
func (*TypeCollection) getTable(ctx *sql.Context, schema string, tblName string) (tbl sql.Table, actualSchema string, err error) {
actualSchema, err = GetSchemaName(ctx, nil, schema)
if err != nil {
return nil, "", err
}
tbl, err = GetSqlTableFromContext(ctx, "", doltdb.TableName{
Name: tblName,
Schema: actualSchema,
})
if err != nil || tbl == nil {
return nil, "", err
}
if schTbl, ok := tbl.(sql.DatabaseSchemaTable); ok {
actualSchema = schTbl.DatabaseSchema().SchemaName()
}
return tbl, actualSchema, nil
}
// tableToType handles type creation related to a table's composite row type.
// https://www.postgresql.org/docs/15/sql-createtable.html
func (pgs *TypeCollection) tableToType(ctx *sql.Context, tbl sql.Table, schema string) (*pgtypes.DoltgresType, error) {
tblName := tbl.Name()
tblSch := tbl.Schema(ctx)
typeID := id.NewType(schema, tblName)
relID := id.NewTable(schema, tblName).AsId()
arrayID := id.NewType(schema, "_"+tblName)
attrs := make([]pgtypes.CompositeAttribute, len(tblSch))
for i, col := range tblSch {
collation := "" // TODO: what should we use for the collation?
colType, ok := col.Type.(*pgtypes.DoltgresType)
if !ok {
// TODO: perhaps we should use a better error message stating that it uses a non-Doltgres type?
return nil, pgtypes.ErrTypeDoesNotExist.New(tblName)
}
attrs[i] = pgtypes.NewCompositeAttribute(ctx, relID, col.Name, colType, int16(i+1), collation)
}
tableType := pgtypes.NewCompositeType(ctx, relID, pgtypes.NewUnresolvedDoltgresTypeFromID(arrayID), typeID, attrs)
_ = pgtypes.CreateArrayTypeFromBaseType(tableType) // This sets the tableType's `Array` field as well
return tableType, nil
}
// GetSqlTableFromContext is a forward declaration to get around import cycles
var GetSqlTableFromContext func(ctx *sql.Context, databaseName string, tableName doltdb.TableName) (sql.Table, error)
// GetSchemaName is a forward declaration to get around import cycles
var GetSchemaName func(ctx *sql.Context, db sql.Database, schemaName string) (string, error)
+106
View File
@@ -0,0 +1,106 @@
// 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 typecollection
import (
"context"
"errors"
"testing"
"github.com/dolthub/dolt/go/store/hash"
"github.com/dolthub/dolt/go/store/prolly"
"github.com/dolthub/dolt/go/store/prolly/tree"
"github.com/stretchr/testify/require"
"github.com/dolthub/doltgresql/core/id"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// TestMap_RemainsUsableAfterFlushFailure asserts that TypeCollection.Map
// remains usable after a flush returns an error. A subsequent Map call
// against a healthy store must succeed.
func TestMap_RemainsUsableAfterFlushFailure(t *testing.T) {
t.Parallel()
ctx := context.Background()
ns := newCountingFailNodeStore(t)
coll := newTestTypeCollection(t, ns)
first := pgtypes.NewUnresolvedDoltgresType("public", "type_one")
coll.accessedMap[first.ID] = first
_, err := coll.Map(ctx)
require.NoError(t, err)
ns.failAfter(0)
second := pgtypes.NewUnresolvedDoltgresType("public", "type_two")
coll.accessedMap[second.ID] = second
_, err = coll.Map(ctx)
require.Error(t, err)
ns.allowAll()
require.NotPanics(t, func() {
_, _ = coll.Map(ctx)
})
}
// newTestTypeCollection returns a TypeCollection backed by |ns| with an
// empty address map.
func newTestTypeCollection(t *testing.T, ns tree.NodeStore) *TypeCollection {
t.Helper()
addrMap, err := prolly.NewEmptyAddressMap(ns)
require.NoError(t, err)
return &TypeCollection{
accessedMap: map[id.Type]*pgtypes.DoltgresType{},
initCache: map[id.Type]*pgtypes.DoltgresType{},
underlyingMap: addrMap,
ns: ns,
}
}
// countingFailNodeStore wraps a real test NodeStore so that callers
// can induce Write failures at chosen points without otherwise altering
// behavior. Used to drive the writeCache flush-failure path.
type countingFailNodeStore struct {
tree.NodeStore
writes int
budget int // -1 means unlimited
}
func newCountingFailNodeStore(t *testing.T) *countingFailNodeStore {
t.Helper()
return &countingFailNodeStore{NodeStore: tree.NewTestNodeStore(), budget: -1}
}
// failAfter permits |allowed| additional Write calls then fails the
// rest, until allowAll is called.
func (f *countingFailNodeStore) failAfter(allowed int) {
f.writes = 0
f.budget = allowed
}
// allowAll restores the wrapper to delegate every Write to the
// underlying NodeStore.
func (f *countingFailNodeStore) allowAll() {
f.budget = -1
}
func (f *countingFailNodeStore) Write(ctx context.Context, nd *tree.Node) (hash.Hash, error) {
if f.budget >= 0 {
f.writes++
if f.writes > f.budget {
return hash.Hash{}, errors.New("induced node store failure")
}
}
return f.NodeStore.Write(ctx, nd)
}