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