Added CREATE OPERATOR and CREATE AGGREGATE

This commit is contained in:
Daylon Wilkins
2026-08-14 03:00:26 -07:00
parent 52e16087d5
commit d5ed6f85c0
74 changed files with 55185 additions and 49620 deletions
+249
View File
@@ -0,0 +1,249 @@
// 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 aggregates
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/doltgresql/core/functions"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
)
// Collection contains a collection of aggregate functions.
type Collection struct {
objinterface.RootObjectMap
}
// Aggregate represents a created aggregate function.
type Aggregate struct {
ID id.Function
ReturnType id.Type
SFunc id.Function // State transition function
SType id.Type // Internal state type
FinalFunc id.Function // Final calculation function
CombineFunc id.Function
InitCond string
HasInitCond bool
}
var _ objinterface.Collection = (*Collection)(nil)
var _ objinterface.RootObject = Aggregate{}
// NewCollection returns a new Collection.
func NewCollection(ctx context.Context, rom objinterface.RootObjectMap) *Collection {
return &Collection{RootObjectMap: rom}
}
// GetAggregate returns the aggregate with the given ID. Returns an Aggregate with an invalid ID if it cannot be found
// (Aggregate.ID.IsValid() == false).
func (pga *Collection) GetAggregate(ctx context.Context, aggregateID id.Function) (Aggregate, error) {
h, err := pga.Contents().Get(ctx, string(aggregateID))
if err != nil || h.IsEmpty() {
return Aggregate{}, err
}
data, err := pga.NodeStore().ReadBytes(ctx, h)
if err != nil {
return Aggregate{}, err
}
return DeserializeAggregate(ctx, data)
}
// GetAggregateOverloads returns every aggregate that shares the given aggregate's schema and name.
func (pga *Collection) GetAggregateOverloads(ctx context.Context, aggregateID id.Function) ([]Aggregate, error) {
var overloads []Aggregate
err := pga.IterateAggregates(ctx, func(a Aggregate) (stop bool, err error) {
if a.ID.SchemaName() == aggregateID.SchemaName() && a.ID.FunctionName() == aggregateID.FunctionName() {
overloads = append(overloads, a)
}
return false, nil
})
return overloads, err
}
// HasAggregate returns whether the given aggregate exists.
func (pga *Collection) HasAggregate(ctx context.Context, aggregateID id.Function) bool {
ok, err := pga.Contents().Has(ctx, string(aggregateID))
return err == nil && ok
}
// HasAggregateName returns whether an aggregate with the given name exists in any schema.
func (pga *Collection) HasAggregateName(ctx context.Context, name string) (bool, error) {
found := false
err := pga.Contents().IterAll(ctx, func(k string, _ hash.Hash) error {
if id.Function(k).FunctionName() == name {
found = true
return io.EOF
}
return nil
})
if err != nil && err != io.EOF {
return false, err
}
return found, nil
}
// AddAggregate adds a new aggregate.
func (pga *Collection) AddAggregate(ctx context.Context, a Aggregate) error {
if pga.HasAggregate(ctx, a.ID) {
return errors.Errorf(`aggregate "%s" already exists with same argument types`, a.ID.FunctionName())
}
data, err := a.Serialize(ctx)
if err != nil {
return err
}
h, err := pga.NodeStore().WriteBytes(ctx, data)
if err != nil {
return err
}
mapEditor := pga.Contents().Editor()
if err = mapEditor.Add(ctx, string(a.ID), h); err != nil {
return err
}
newMap, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pga.SetContents(newMap)
return nil
}
// DropAggregate drops an existing aggregate.
func (pga *Collection) DropAggregate(ctx context.Context, aggregateIDs ...id.Function) error {
if len(aggregateIDs) == 0 {
return nil
}
for _, aggregateID := range aggregateIDs {
if ok, err := pga.Contents().Has(ctx, string(aggregateID)); err != nil {
return err
} else if !ok {
return errors.Errorf(`aggregate %s does not exist`, aggregateID.DisplayString())
}
}
mapEditor := pga.Contents().Editor()
for _, aggregateID := range aggregateIDs {
if err := mapEditor.Delete(ctx, string(aggregateID)); err != nil {
return err
}
}
newMap, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pga.SetContents(newMap)
return nil
}
// resolveName returns the fully resolved name of the given aggregate. Returns an error if the name is ambiguous.
func (pga *Collection) resolveName(ctx context.Context, schemaName string, formattedName string) (id.Function, error) {
partialID := functions.TableNameToFunctionID(schemaName, formattedName)
if !partialID.IsValid() {
return id.NullFunction, nil
}
// Check for an exact match
if pga.HasAggregate(ctx, partialID) {
return partialID, nil
}
// Otherwise we'll iterate over all the names
var resolvedID id.Function
partialParams := partialID.Parameters()
err := pga.IterateAggregates(ctx, func(a Aggregate) (stop bool, err error) {
if !strings.EqualFold(a.ID.FunctionName(), partialID.FunctionName()) {
return false, nil
}
if len(partialID.SchemaName()) > 0 && !strings.EqualFold(a.ID.SchemaName(), partialID.SchemaName()) {
return false, nil
}
if len(partialParams) > 0 {
if a.ID.ParameterCount() != len(partialParams) {
return false, nil
}
for i, param := range a.ID.Parameters() {
if len(partialParams[i].TypeName()) > 0 && !strings.EqualFold(partialParams[i].TypeName(), param.TypeName()) {
return false, nil
}
if len(partialParams[i].SchemaName()) > 0 && !strings.EqualFold(partialParams[i].SchemaName(), param.SchemaName()) {
return false, nil
}
}
}
// Everything must have matched to have made it here
if resolvedID.IsValid() {
aggregateTableName := functions.FunctionIDToTableName(a.ID)
resolvedTableName := functions.FunctionIDToTableName(resolvedID)
return true, fmt.Errorf("`%s` is ambiguous, matches `%s` and `%s`",
formattedName, aggregateTableName.String(), resolvedTableName.String())
}
resolvedID = a.ID
return false, nil
})
return resolvedID, err
}
// IterateAggregates iterates over all aggregates in the collection.
func (pga *Collection) IterateAggregates(ctx context.Context, callback func(a Aggregate) (stop bool, err error)) error {
return pga.Contents().IterAll(ctx, func(_ string, v hash.Hash) error {
data, err := pga.NodeStore().ReadBytes(ctx, v)
if err != nil {
return err
}
a, err := DeserializeAggregate(ctx, data)
if err != nil {
return err
}
stop, err := callback(a)
if err != nil {
return err
} else if stop {
return io.EOF
} else {
return nil
}
})
}
// GetID implements the interface objinterface.RootObject.
func (aggregate Aggregate) GetID() id.Id {
return aggregate.ID.AsId()
}
// GetRootObjectID implements the interface objinterface.RootObject.
func (aggregate Aggregate) GetRootObjectID() objinterface.RootObjectID {
return objinterface.RootObjectID_Aggregates
}
// HashOf implements the interface objinterface.RootObject.
func (aggregate Aggregate) HashOf(ctx context.Context) (hash.Hash, error) {
data, err := aggregate.Serialize(ctx)
if err != nil {
return hash.Hash{}, err
}
return hash.Of(data), nil
}
// Name implements the interface objinterface.RootObject.
func (aggregate Aggregate) Name() doltdb.TableName {
return functions.FunctionIDToTableName(aggregate.ID)
}
+102
View File
@@ -0,0 +1,102 @@
// 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 aggregates
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/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).AggregatesBytes,
RootValueAdd: serial.RootValueAddAggregates,
}
// HandleMerge implements the interface objinterface.Collection.
func (*Collection) HandleMerge(ctx context.Context, mro merge.MergeRootObject) (doltdb.RootObject, *merge.MergeStats, error) {
ourAggregate := mro.OurRootObj.(Aggregate)
theirAggregate := mro.TheirRootObj.(Aggregate)
// Ensure that they have the same identifier
if ourAggregate.ID != theirAggregate.ID {
return nil, nil, errors.Newf("attempted to merge different aggregates: `%s` and `%s`",
ourAggregate.Name().String(), theirAggregate.Name().String())
}
ourHash, err := ourAggregate.HashOf(ctx)
if err != nil {
return nil, nil, err
}
theirHash, err := theirAggregate.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`", theirAggregate.Name().String())
}
// LoadCollection implements the interface objinterface.Collection.
func (*Collection) LoadCollection(ctx context.Context, root objinterface.RootValue) (objinterface.Collection, error) {
return LoadAggregates(ctx, root)
}
// LoadAggregates loads the aggregates collection from the given root.
func LoadAggregates(ctx context.Context, root objinterface.RootValue) (*Collection, error) {
rom, err := objinterface.NewRootObjectMap(ctx, storage, root)
if err != nil {
return nil, err
}
return NewCollection(ctx, rom), nil
}
// ResolveNameFromObjects implements the interface objinterface.Collection.
func (*Collection) ResolveNameFromObjects(ctx context.Context, name doltdb.TableName, rootObjects []objinterface.RootObject) (doltdb.TableName, id.Id, error) {
rom, err := objinterface.NewDetachedRootObjectMap(storage, tree.NewTestNodeStore())
if err != nil {
return doltdb.TableName{}, id.Null, err
}
tempCollection := NewCollection(ctx, rom)
for _, rootObject := range rootObjects {
if a, ok := rootObject.(Aggregate); ok {
if err = tempCollection.AddAggregate(ctx, a); 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
}
+147
View File
@@ -0,0 +1,147 @@
// 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 aggregates
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/functions"
"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 (pga *Collection) DeserializeRootObject(ctx context.Context, data []byte) (objinterface.RootObject, error) {
return DeserializeAggregate(ctx, data)
}
// DiffRootObjects implements the interface objinterface.Collection.
func (pga *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("aggregate conflict detection has not yet been implemented")
}
// DropRootObject implements the interface objinterface.Collection.
func (pga *Collection) DropRootObject(ctx context.Context, identifier id.Id) error {
if identifier.Section() != id.Section_Function {
return errors.Errorf(`aggregate %s does not exist`, identifier.String())
}
return pga.DropAggregate(ctx, id.Function(identifier))
}
// GetFieldType implements the interface objinterface.Collection.
func (pga *Collection) GetFieldType(ctx context.Context, fieldName string) *pgtypes.DoltgresType {
return nil
}
// GetID implements the interface objinterface.Collection.
func (pga *Collection) GetID() objinterface.RootObjectID {
return objinterface.RootObjectID_Aggregates
}
// GetRootObject implements the interface objinterface.Collection.
func (pga *Collection) GetRootObject(ctx context.Context, identifier id.Id) (objinterface.RootObject, bool, error) {
if identifier.Section() != id.Section_Function {
return nil, false, nil
}
a, err := pga.GetAggregate(ctx, id.Function(identifier))
return a, err == nil && a.ID.IsValid(), err
}
// HasRootObject implements the interface objinterface.Collection.
func (pga *Collection) HasRootObject(ctx context.Context, identifier id.Id) (bool, error) {
if identifier.Section() != id.Section_Function {
return false, nil
}
return pga.HasAggregate(ctx, id.Function(identifier)), nil
}
// IDToTableName implements the interface objinterface.Collection.
func (pga *Collection) IDToTableName(identifier id.Id) doltdb.TableName {
if identifier.Section() != id.Section_Function {
return doltdb.TableName{}
}
return functions.FunctionIDToTableName(id.Function(identifier))
}
// IterAll implements the interface objinterface.Collection.
func (pga *Collection) IterAll(ctx context.Context, callback func(rootObj objinterface.RootObject) (stop bool, err error)) error {
return pga.IterateAggregates(ctx, func(a Aggregate) (stop bool, err error) {
return callback(a)
})
}
// IterIDs implements the interface objinterface.Collection.
func (pga *Collection) IterIDs(ctx context.Context, callback func(identifier id.Id) (stop bool, err error)) error {
return pga.Contents().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
}
})
}
// PutRootObject implements the interface objinterface.Collection.
func (pga *Collection) PutRootObject(ctx context.Context, rootObj objinterface.RootObject) error {
a, ok := rootObj.(Aggregate)
if !ok {
return errors.Newf("invalid aggregate root object: %T", rootObj)
}
return pga.AddAggregate(ctx, a)
}
// RenameRootObject implements the interface objinterface.Collection.
func (pga *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 aggregate due to invalid id")
}
a, err := pga.GetAggregate(ctx, id.Function(oldName))
if err != nil {
return err
}
if err = pga.DropAggregate(ctx, id.Function(oldName)); err != nil {
return err
}
a.ID = id.Function(newName)
return pga.AddAggregate(ctx, a)
}
// ResolveName implements the interface objinterface.Collection.
func (pga *Collection) ResolveName(ctx context.Context, name doltdb.TableName) (doltdb.TableName, id.Id, error) {
rawID, err := pga.resolveName(ctx, name.Schema, name.Name)
if err != nil || !rawID.IsValid() {
return doltdb.TableName{}, id.Null, err
}
return functions.FunctionIDToTableName(rawID), rawID.AsId(), nil
}
// TableNameToID implements the interface objinterface.Collection.
func (pga *Collection) TableNameToID(name doltdb.TableName) id.Id {
return functions.TableNameToFunctionID(name.Schema, name.Name).AsId()
}
// UpdateField implements the interface objinterface.Collection.
func (pga *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")
}
+75
View File
@@ -0,0 +1,75 @@
// 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 aggregates
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/utils"
)
// Serialize returns the Aggregate as a byte slice. If the Aggregate is invalid, then this returns a nil slice.
func (aggregate Aggregate) Serialize(ctx context.Context) ([]byte, error) {
if !aggregate.ID.IsValid() {
return nil, nil
}
// Initialize the writer and version
writer := utils.NewWriter(256)
writer.VariableUint(0) // Version
// Write the aggregate data
writer.Id(aggregate.ID.AsId())
writer.Id(aggregate.ReturnType.AsId())
writer.Id(aggregate.SFunc.AsId())
writer.Id(aggregate.SType.AsId())
writer.Id(aggregate.FinalFunc.AsId())
writer.Id(aggregate.CombineFunc.AsId())
writer.String(aggregate.InitCond)
writer.Bool(aggregate.HasInitCond)
// Returns the data
return writer.Data(), nil
}
// DeserializeAggregate returns the Aggregate that was serialized in the byte slice. Returns an empty Aggregate
// (invalid ID) if data is nil or empty.
func DeserializeAggregate(ctx context.Context, data []byte) (Aggregate, error) {
if len(data) == 0 {
return Aggregate{}, nil
}
reader := utils.NewReader(data)
version := reader.VariableUint()
if version != 0 {
return Aggregate{}, errors.Errorf("version %d of aggregates is not supported, please upgrade the server", version)
}
// Read from the reader
a := Aggregate{}
a.ID = id.Function(reader.Id())
a.ReturnType = id.Type(reader.Id())
a.SFunc = id.Function(reader.Id())
a.SType = id.Type(reader.Id())
a.FinalFunc = id.Function(reader.Id())
a.CombineFunc = id.Function(reader.Id())
a.InitCond = reader.String()
a.HasInitCond = reader.Bool()
if !reader.IsEmpty() {
return Aggregate{}, errors.Errorf("extra data found while deserializing an aggregate")
}
// Return the deserialized object
return a, nil
}
+22
View File
@@ -27,9 +27,11 @@ import (
"github.com/dolthub/dolt/go/store/types"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core/aggregates"
"github.com/dolthub/doltgresql/core/casts"
"github.com/dolthub/doltgresql/core/extensions"
"github.com/dolthub/doltgresql/core/functions"
"github.com/dolthub/doltgresql/core/operators"
"github.com/dolthub/doltgresql/core/procedures"
"github.com/dolthub/doltgresql/core/rootobject"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
@@ -354,6 +356,26 @@ func GetCastsCollectionFromContext(ctx *sql.Context, database string) (*casts.Co
return coll.(*casts.Collection), nil
}
// GetAggregatesCollectionFromContext returns the given aggregates collection from the context.
// Will always return a collection if no error is returned.
func GetAggregatesCollectionFromContext(ctx *sql.Context, database string) (*aggregates.Collection, error) {
coll, err := collectionFromContext(ctx, database, objinterface.RootObjectID_Aggregates)
if err != nil {
return nil, err
}
return coll.(*aggregates.Collection), nil
}
// GetOperatorsCollectionFromContext returns the given operators collection from the context.
// Will always return a collection if no error is returned.
func GetOperatorsCollectionFromContext(ctx *sql.Context, database string) (*operators.Collection, error) {
coll, err := collectionFromContext(ctx, database, objinterface.RootObjectID_Operators)
if err != nil {
return nil, err
}
return coll.(*operators.Collection), 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) {
+32 -32
View File
@@ -288,38 +288,6 @@ func (pgf *Collection) reloadCaches(ctx context.Context) error {
})
}
// 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()
@@ -372,6 +340,38 @@ func (function Function) Name() doltdb.TableName {
return FunctionIDToTableName(function.ID)
}
// TableNameToFunctionID returns the ID that was encoded via the Name() call, as the returned TableName contains
// additional information (which this is able to process).
func TableNameToFunctionID(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...)
}
// FunctionIDToTableName returns the ID in a format that's better for user consumption.
func FunctionIDToTableName(funcID id.Function) doltdb.TableName {
paramTypes := funcID.Parameters()
+1 -1
View File
@@ -291,7 +291,7 @@ func (pgf *Collection) ResolveName(ctx context.Context, name doltdb.TableName) (
// TableNameToID implements the interface objinterface.Collection.
func (pgf *Collection) TableNameToID(name doltdb.TableName) id.Id {
return pgf.tableNameToID(name.Schema, name.Name).AsId()
return TableNameToFunctionID(name.Schema, name.Name).AsId()
}
// UpdateField implements the interface objinterface.Collection.
+2
View File
@@ -73,6 +73,8 @@ const (
NullIndex Index = ""
// NullNamespace is an empty, invalid ID. This is exactly equivalent to Null.
NullNamespace Namespace = ""
// NullOperator is an empty, invalid ID. This is exactly equivalent to Null.
NullOperator Operator = ""
// 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.
+37
View File
@@ -62,6 +62,9 @@ type Namespace Id
// Oid is an Id wrapper for OIDs. This wrapper must not be returned to the client.
type Oid Id
// Operator is an Id wrapper for operators. This wrapper must not be returned to the client.
type Operator Id
// Procedure is an Id wrapper for procedures. This wrapper must not be returned to the client.
type Procedure Id
@@ -198,6 +201,14 @@ func NewOID(val uint32) Oid {
return Oid(NewId(Section_OID, strconv.FormatUint(uint64(val), 10)))
}
// NewOperator returns a new Operator. This wrapper must not be returned to the client.
func NewOperator(schemaName string, symbol string, leftType Type, rightType Type) Operator {
if len(symbol) == 0 {
return NullOperator
}
return Operator(NewId(Section_Operator, schemaName, symbol, string(leftType), string(rightType)))
}
// 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 {
@@ -419,6 +430,26 @@ func (id Oid) OID() uint32 {
return uint32(val)
}
// LeftType returns the type of the operator's left operand.
func (id Operator) LeftType() Type {
return Type(Id(id).Segment(2))
}
// RightType returns the type of the operator's right operand.
func (id Operator) RightType() Type {
return Type(Id(id).Segment(3))
}
// SchemaName returns the name of the schema that the operator belongs to.
func (id Operator) SchemaName() string {
return Id(id).Segment(0)
}
// Symbol returns the operator's symbol.
func (id Operator) Symbol() string {
return Id(id).Segment(1)
}
// ProcedureName returns the procedure's name.
func (id Procedure) ProcedureName() string {
return Id(id).Segment(1)
@@ -546,6 +577,9 @@ 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 Operator) IsValid() bool { return Id(id).IsValid() }
// IsValid returns whether the ID is valid.
func (id Procedure) IsValid() bool { return Id(id).IsValid() }
@@ -609,6 +643,9 @@ 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 Operator) AsId() Id { return Id(id) }
// AsId returns the unwrapped ID.
func (id Procedure) AsId() Id { return Id(id) }
+271
View File
@@ -0,0 +1,271 @@
// 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 operators
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/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// Collection contains a collection of operators.
type Collection struct {
objinterface.RootObjectMap
}
// Operator represents a user-defined operator.
type Operator struct {
ID id.Operator
Function id.Function
ReturnType id.Type
Commutator string
Negator string
Hashes bool
Merges bool
}
var _ objinterface.Collection = (*Collection)(nil)
var _ objinterface.RootObject = Operator{}
// NewCollection returns a new Collection.
func NewCollection(rom objinterface.RootObjectMap) *Collection {
return &Collection{RootObjectMap: rom}
}
// GetOperator returns the operator with the given ID. Returns an Operator with an invalid ID if it cannot be found
// (Operator.ID.IsValid() == false).
func (pgo *Collection) GetOperator(ctx context.Context, operatorID id.Operator) (Operator, error) {
h, err := pgo.Contents().Get(ctx, string(operatorID))
if err != nil || h.IsEmpty() {
return Operator{}, err
}
data, err := pgo.NodeStore().ReadBytes(ctx, h)
if err != nil {
return Operator{}, err
}
return DeserializeOperator(ctx, data)
}
// ResolveOperator returns the operator matching the given symbol and operand types, searching the given schemas in
// order and allowing unknown operand types to match any type. Returns false if no operator matches.
func (pgo *Collection) ResolveOperator(ctx context.Context, schemaNames []string, symbol string, leftType id.Type, rightType id.Type) (Operator, bool, error) {
for _, schemaName := range schemaNames {
o, err := pgo.GetOperator(ctx, id.NewOperator(schemaName, symbol, leftType, rightType))
if err != nil {
return Operator{}, false, err
}
if o.ID.IsValid() {
return o, true, nil
}
}
leftUnknown := leftType == pgtypes.Unknown.ID
rightUnknown := rightType == pgtypes.Unknown.ID
if !leftUnknown && !rightUnknown {
return Operator{}, false, nil
}
for _, schemaName := range schemaNames {
var resolved Operator
err := pgo.IterateOperators(ctx, func(o Operator) (stop bool, err error) {
if o.ID.SchemaName() != schemaName || o.ID.Symbol() != symbol {
return false, nil
}
if (!leftUnknown && o.ID.LeftType() != leftType) || (!rightUnknown && o.ID.RightType() != rightType) {
return false, nil
}
if resolved.ID.IsValid() {
return true, errors.Errorf("operator is not unique: %s", symbol)
}
resolved = o
return false, nil
})
if err != nil {
return Operator{}, false, err
}
if resolved.ID.IsValid() {
return resolved, true, nil
}
}
return Operator{}, false, nil
}
// HasOperator returns whether the given operator exists.
func (pgo *Collection) HasOperator(ctx context.Context, operatorID id.Operator) bool {
ok, err := pgo.Contents().Has(ctx, string(operatorID))
return err == nil && ok
}
// AddOperator adds a new operator.
func (pgo *Collection) AddOperator(ctx context.Context, o Operator) error {
if pgo.HasOperator(ctx, o.ID) {
return errors.Errorf(`operator %s already exists`, o.ID.Symbol())
}
data, err := o.Serialize(ctx)
if err != nil {
return err
}
h, err := pgo.NodeStore().WriteBytes(ctx, data)
if err != nil {
return err
}
mapEditor := pgo.Contents().Editor()
if err = mapEditor.Add(ctx, string(o.ID), h); err != nil {
return err
}
newMap, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pgo.SetContents(newMap)
return nil
}
// DropOperator drops an existing operator.
func (pgo *Collection) DropOperator(ctx context.Context, operatorIDs ...id.Operator) error {
if len(operatorIDs) == 0 {
return nil
}
for _, operatorID := range operatorIDs {
if ok, err := pgo.Contents().Has(ctx, string(operatorID)); err != nil {
return err
} else if !ok {
return errors.Errorf(`operator does not exist: %s %s %s`,
operatorID.LeftType().TypeName(), operatorID.Symbol(), operatorID.RightType().TypeName())
}
}
mapEditor := pgo.Contents().Editor()
for _, operatorID := range operatorIDs {
if err := mapEditor.Delete(ctx, string(operatorID)); err != nil {
return err
}
}
newMap, err := mapEditor.Flush(ctx)
if err != nil {
return err
}
pgo.SetContents(newMap)
return nil
}
// resolveName returns the fully resolved name of the given operator. Returns an error if the name is ambiguous.
func (pgo *Collection) resolveName(ctx context.Context, schemaName string, formattedName string) (id.Operator, error) {
if len(formattedName) == 0 {
return id.NullOperator, nil
}
// Check for an exact match
fullID := pgo.tableNameToID(schemaName, formattedName)
if pgo.HasOperator(ctx, fullID) {
return fullID, nil
}
// Otherwise we'll iterate over all the names
var resolvedID id.Operator
err := pgo.IterateOperators(ctx, func(o Operator) (stop bool, err error) {
if !strings.EqualFold(string(o.ID), string(fullID)) {
return false, nil
}
// The above matches, so this counts as a match
if resolvedID.IsValid() {
operatorTableName := OperatorIDToTableName(o.ID)
resolvedTableName := OperatorIDToTableName(resolvedID)
return true, fmt.Errorf("`%s` is ambiguous, matches `%s` and `%s`",
formattedName, operatorTableName.String(), resolvedTableName.String())
}
resolvedID = o.ID
return false, nil
})
return resolvedID, err
}
// IterateOperators iterates over all operators in the collection.
func (pgo *Collection) IterateOperators(ctx context.Context, callback func(o Operator) (stop bool, err error)) error {
return pgo.Contents().IterAll(ctx, func(_ string, v hash.Hash) error {
data, err := pgo.NodeStore().ReadBytes(ctx, v)
if err != nil {
return err
}
o, err := DeserializeOperator(ctx, data)
if err != nil {
return err
}
stop, err := callback(o)
if err != nil {
return err
} else if stop {
return io.EOF
} else {
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 (pgo *Collection) tableNameToID(schema string, formattedName string) id.Operator {
sections := strings.Split(strings.TrimSuffix(strings.TrimPrefix(formattedName, "("), ")"), ")|(")
if len(sections) != 5 {
return id.NullOperator
}
return id.NewOperator(schema, sections[0],
id.NewType(sections[1], sections[2]), id.NewType(sections[3], sections[4]))
}
// GetID implements the interface objinterface.RootObject.
func (operator Operator) GetID() id.Id {
return operator.ID.AsId()
}
// GetRootObjectID implements the interface objinterface.RootObject.
func (operator Operator) GetRootObjectID() objinterface.RootObjectID {
return objinterface.RootObjectID_Operators
}
// HashOf implements the interface objinterface.RootObject.
func (operator Operator) HashOf(ctx context.Context) (hash.Hash, error) {
data, err := operator.Serialize(ctx)
if err != nil {
return hash.Hash{}, err
}
return hash.Of(data), nil
}
// Name implements the interface objinterface.RootObject.
func (operator Operator) Name() doltdb.TableName {
return OperatorIDToTableName(operator.ID)
}
// OperatorIDToTableName returns the ID in a format that's better for user consumption.
func OperatorIDToTableName(operatorID id.Operator) doltdb.TableName {
name := fmt.Sprintf(`(%s)|(%s)|(%s)|(%s)|(%s)`,
operatorID.Symbol(),
operatorID.LeftType().SchemaName(),
operatorID.LeftType().TypeName(),
operatorID.RightType().SchemaName(),
operatorID.RightType().TypeName())
return doltdb.TableName{
Name: name,
Schema: operatorID.SchemaName(),
}
}
+103
View File
@@ -0,0 +1,103 @@
// 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 operators
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/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).OperatorsBytes,
RootValueAdd: serial.RootValueAddOperators,
}
// HandleMerge implements the interface objinterface.Collection.
func (*Collection) HandleMerge(ctx context.Context, mro merge.MergeRootObject) (doltdb.RootObject, *merge.MergeStats, error) {
ourOperator := mro.OurRootObj.(Operator)
theirOperator := mro.TheirRootObj.(Operator)
// Ensure that they have the same identifier
if ourOperator.ID != theirOperator.ID {
return nil, nil, errors.Newf("attempted to merge different operators: `%s` and `%s`",
ourOperator.Name().String(), theirOperator.Name().String())
}
ourHash, err := ourOperator.HashOf(ctx)
if err != nil {
return nil, nil, err
}
theirHash, err := theirOperator.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`", theirOperator.Name().String())
}
// LoadCollection implements the interface objinterface.Collection.
func (*Collection) LoadCollection(ctx context.Context, root objinterface.RootValue) (objinterface.Collection, error) {
return LoadOperators(ctx, root)
}
// LoadOperators loads the operators collection from the given root.
func LoadOperators(ctx context.Context, root objinterface.RootValue) (*Collection, error) {
rom, err := objinterface.NewRootObjectMap(ctx, storage, root)
if err != nil {
return nil, err
}
return NewCollection(rom), nil
}
// 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
rom, err := objinterface.NewDetachedRootObjectMap(storage, tree.NewTestNodeStore())
if err != nil {
return doltdb.TableName{}, id.Null, err
}
tempCollection := NewCollection(rom)
for _, rootObject := range rootObjects {
if o, ok := rootObject.(Operator); ok {
if err = tempCollection.AddOperator(ctx, o); 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
}
+146
View File
@@ -0,0 +1,146 @@
// 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 operators
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 (pgo *Collection) DeserializeRootObject(ctx context.Context, data []byte) (objinterface.RootObject, error) {
return DeserializeOperator(ctx, data)
}
// DiffRootObjects implements the interface objinterface.Collection.
func (pgo *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("operator conflict detection has not yet been implemented")
}
// DropRootObject implements the interface objinterface.Collection.
func (pgo *Collection) DropRootObject(ctx context.Context, identifier id.Id) error {
if identifier.Section() != id.Section_Operator {
return errors.Errorf(`operator %s does not exist`, identifier.String())
}
return pgo.DropOperator(ctx, id.Operator(identifier))
}
// GetFieldType implements the interface objinterface.Collection.
func (pgo *Collection) GetFieldType(ctx context.Context, fieldName string) *pgtypes.DoltgresType {
return nil
}
// GetID implements the interface objinterface.Collection.
func (pgo *Collection) GetID() objinterface.RootObjectID {
return objinterface.RootObjectID_Operators
}
// GetRootObject implements the interface objinterface.Collection.
func (pgo *Collection) GetRootObject(ctx context.Context, identifier id.Id) (objinterface.RootObject, bool, error) {
if identifier.Section() != id.Section_Operator {
return nil, false, nil
}
o, err := pgo.GetOperator(ctx, id.Operator(identifier))
return o, err == nil && o.ID.IsValid(), err
}
// HasRootObject implements the interface objinterface.Collection.
func (pgo *Collection) HasRootObject(ctx context.Context, identifier id.Id) (bool, error) {
if identifier.Section() != id.Section_Operator {
return false, nil
}
return pgo.HasOperator(ctx, id.Operator(identifier)), nil
}
// IDToTableName implements the interface objinterface.Collection.
func (pgo *Collection) IDToTableName(identifier id.Id) doltdb.TableName {
if identifier.Section() != id.Section_Operator {
return doltdb.TableName{}
}
return OperatorIDToTableName(id.Operator(identifier))
}
// IterAll implements the interface objinterface.Collection.
func (pgo *Collection) IterAll(ctx context.Context, callback func(rootObj objinterface.RootObject) (stop bool, err error)) error {
return pgo.IterateOperators(ctx, func(o Operator) (stop bool, err error) {
return callback(o)
})
}
// IterIDs implements the interface objinterface.Collection.
func (pgo *Collection) IterIDs(ctx context.Context, callback func(identifier id.Id) (stop bool, err error)) error {
return pgo.Contents().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
}
})
}
// PutRootObject implements the interface objinterface.Collection.
func (pgo *Collection) PutRootObject(ctx context.Context, rootObj objinterface.RootObject) error {
o, ok := rootObj.(Operator)
if !ok {
return errors.Newf("invalid operator root object: %T", rootObj)
}
return pgo.AddOperator(ctx, o)
}
// RenameRootObject implements the interface objinterface.Collection.
func (pgo *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_Operator {
return errors.New("cannot rename operator due to invalid id")
}
o, err := pgo.GetOperator(ctx, id.Operator(oldName))
if err != nil {
return err
}
if err = pgo.DropOperator(ctx, id.Operator(oldName)); err != nil {
return err
}
o.ID = id.Operator(newName)
return pgo.AddOperator(ctx, o)
}
// ResolveName implements the interface objinterface.Collection.
func (pgo *Collection) ResolveName(ctx context.Context, name doltdb.TableName) (doltdb.TableName, id.Id, error) {
rawID, err := pgo.resolveName(ctx, name.Schema, name.Name)
if err != nil || !rawID.IsValid() {
return doltdb.TableName{}, id.Null, err
}
return OperatorIDToTableName(rawID), rawID.AsId(), nil
}
// TableNameToID implements the interface objinterface.Collection.
func (pgo *Collection) TableNameToID(name doltdb.TableName) id.Id {
return pgo.tableNameToID(name.Schema, name.Name).AsId()
}
// UpdateField implements the interface objinterface.Collection.
func (pgo *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")
}
+73
View File
@@ -0,0 +1,73 @@
// 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 operators
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/utils"
)
// Serialize returns the Operator as a byte slice. If the Operator is invalid, then this returns a nil slice.
func (operator Operator) Serialize(ctx context.Context) ([]byte, error) {
if !operator.ID.IsValid() {
return nil, nil
}
// Initialize the writer and version
writer := utils.NewWriter(256)
writer.VariableUint(0) // Version
// Write the operator data
writer.Id(operator.ID.AsId())
writer.Id(operator.Function.AsId())
writer.Id(operator.ReturnType.AsId())
writer.String(operator.Commutator)
writer.String(operator.Negator)
writer.Bool(operator.Hashes)
writer.Bool(operator.Merges)
// Returns the data
return writer.Data(), nil
}
// DeserializeOperator returns the Operator that was serialized in the byte slice. Returns an empty Operator (invalid
// ID) if data is nil or empty.
func DeserializeOperator(ctx context.Context, data []byte) (Operator, error) {
if len(data) == 0 {
return Operator{}, nil
}
reader := utils.NewReader(data)
version := reader.VariableUint()
if version != 0 {
return Operator{}, errors.Errorf("version %d of operators is not supported, please upgrade the server", version)
}
// Read from the reader
o := Operator{}
o.ID = id.Operator(reader.Id())
o.Function = id.Function(reader.Id())
o.ReturnType = id.Type(reader.Id())
o.Commutator = reader.String()
o.Negator = reader.String()
o.Hashes = reader.Bool()
o.Merges = reader.Bool()
if !reader.IsEmpty() {
return Operator{}, errors.Errorf("extra data found while deserializing an operator")
}
// Return the deserialized object
return o, nil
}
+4
View File
@@ -23,11 +23,13 @@ import (
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/merge"
"github.com/dolthub/doltgresql/core/aggregates"
"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/operators"
"github.com/dolthub/doltgresql/core/procedures"
"github.com/dolthub/doltgresql/core/rootobject/objinterface"
"github.com/dolthub/doltgresql/core/sequences"
@@ -48,6 +50,8 @@ var (
&conflicts.Collection{},
&procedures.Collection{},
&casts.Collection{},
&operators.Collection{},
&aggregates.Collection{},
}
)
@@ -38,6 +38,8 @@ const (
RootObjectID_Conflicts
RootObjectID_Procedures
RootObjectID_Casts
RootObjectID_Operators
RootObjectID_Aggregates
RootObjectID_Count // This must always be last since it represents the count
)
+2 -1
View File
@@ -25,6 +25,7 @@ import (
"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"
)
@@ -98,7 +99,7 @@ func (serializer RootObjectSerializer) WriteProllyMap(ctx context.Context, root
}
h = ref.TargetHash()
}
newStorage, err := root.GetStorage(ctx).SetRootObjectHash(ctx, serializer.Bytes, h)
newStorage, err := root.GetStorage(ctx).SetRootObjectHash(ctx, storage.RootObjectSerialization(serializer), h)
if err != nil {
return nil, err
}
+39 -7
View File
@@ -19,10 +19,12 @@ import (
"os"
"testing"
doltserial "github.com/dolthub/dolt/go/gen/fb/serial"
"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"
flatbuffers "github.com/dolthub/flatbuffers/v23/go"
"github.com/stretchr/testify/require"
"github.com/dolthub/doltgresql/core/casts"
@@ -30,6 +32,8 @@ import (
"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/flatbuffers/gen/serial"
)
func TestMain(m *testing.M) {
@@ -48,8 +52,8 @@ func TestLoadAndSaveEmptyCollectionsPreserveRoot(t *testing.T) {
root: newTestRoot,
},
{
name: "root with empty root object fields",
root: newTestRootWithFields,
name: "legacy root with zero-hash root object fields",
root: newLegacyTestRoot,
},
} {
t.Run(test.name, func(t *testing.T) {
@@ -81,7 +85,7 @@ func TestLoadAndSaveEmptyCollectionsPreserveRoot(t *testing.T) {
func TestEmptiedCollectionMatchesUnwrittenCollection(t *testing.T) {
t.Parallel()
ctx := context.Background()
root := newTestRootWithFields(t, ctx)
root := newTestRootWithSchema(t, ctx)
startHash, err := root.HashOf()
require.NoError(t, err)
@@ -113,7 +117,7 @@ func TestEmptiedCollectionMatchesUnwrittenCollection(t *testing.T) {
func TestCollectionStaleness(t *testing.T) {
t.Parallel()
ctx := context.Background()
root := newTestRootWithFields(t, ctx)
root := newTestRootWithSchema(t, ctx)
ours, err := sequences.LoadSequences(ctx, root)
require.NoError(t, err)
@@ -142,7 +146,7 @@ func TestCollectionStaleness(t *testing.T) {
func TestResolutionCollectionsAreReused(t *testing.T) {
t.Parallel()
ctx := context.Background()
root := newTestRootWithFields(t, ctx)
root := newTestRootWithSchema(t, ctx)
first, err := root.ReadOnlyCollections(ctx)
require.NoError(t, err)
@@ -191,11 +195,39 @@ func newTestRoot(t testing.TB, ctx context.Context) *RootValue {
return root.(*RootValue)
}
// newTestRootWithFields returns an empty in-memory root that has every root object field written as empty.
func newTestRootWithFields(t testing.TB, ctx context.Context) *RootValue {
// newTestRootWithSchema returns an empty in-memory root that has a database schema and no root object fields.
func newTestRootWithSchema(t testing.TB, ctx context.Context) *RootValue {
t.Helper()
root, err := newTestRoot(t, ctx).CreateDatabaseSchema(ctx, schema.DatabaseSchema{Name: "public"})
require.NoError(t, err)
require.Empty(t, root.(*RootValue).st.SRV.CastsBytes())
return root.(*RootValue)
}
// newLegacyTestRoot returns an empty in-memory root with every root object field written as a zero hash, matching
// the roots that the previous version serialized.
func newLegacyTestRoot(t testing.TB, ctx context.Context) *RootValue {
t.Helper()
base := newTestRoot(t, ctx)
builder := flatbuffers.NewBuilder(80)
tablesOffset := builder.CreateByteVector(base.st.SRV.TablesBytes())
fkOffset := builder.CreateByteVector(base.st.SRV.ForeignKeyAddrBytes())
var empty hash.Hash
rootObjOffsets := make([]flatbuffers.UOffsetT, len(storage.RootObjectSerializations))
for i := range storage.RootObjectSerializations {
rootObjOffsets[i] = builder.CreateByteVector(empty[:])
}
serial.RootValueStart(builder)
serial.RootValueAddFeatureVersion(builder, base.st.SRV.FeatureVersion())
serial.RootValueAddCollation(builder, base.st.SRV.Collation())
serial.RootValueAddTables(builder, tablesOffset)
serial.RootValueAddForeignKeyAddr(builder, fkOffset)
for i := range storage.RootObjectSerializations {
storage.RootObjectSerializations[i].RootValueAdd(builder, rootObjOffsets[i])
}
bs := doltserial.FinishMessage(builder, serial.RootValueEnd(builder), []byte(doltserial.DoltgresRootValueFileID))
root, err := newRootValue(ctx, base.vrw, base.ns, types.SerialMessage(bs))
require.NoError(t, err)
require.Len(t, root.(*RootValue).st.SRV.CastsBytes(), hash.ByteLen)
return root.(*RootValue)
}
+41 -22
View File
@@ -87,22 +87,33 @@ func (r RootStorage) SetCollation(ctx context.Context, collation schema.Collatio
return ret, nil
}
// SetRootObjectHash sets the hash of the root object collection read by the given accessor, returning a new storage
// object. Roots written before a root object existed have no field for it, so the root value is rebuilt first.
func (r RootStorage) SetRootObjectHash(ctx context.Context, bytes func(*serial.RootValue) []byte, h hash.Hash) (RootStorage, error) {
// SetRootObjectHash sets the hash of the root object collection handled by the given serialization, returning a new
// storage object.
func (r RootStorage) SetRootObjectHash(ctx context.Context, serialization RootObjectSerialization, h hash.Hash) (RootStorage, error) {
ret := r.Clone()
if len(bytes(ret.SRV)) == 0 {
dbSchemas, err := r.GetSchemas(ctx)
if err != nil {
return RootStorage{}, err
fieldBytes := serialization.Bytes(ret.SRV)
if len(fieldBytes) == hash.ByteLen {
copy(fieldBytes, h[:])
if !h.IsEmpty() {
return ret, nil
}
msg, err := r.serializeRootValue(r.SRV.TablesBytes(), dbSchemas)
if err != nil {
return RootStorage{}, err
}
ret = RootStorage{msg}
} else if h.IsEmpty() {
return ret, nil
}
copy(bytes(ret.SRV), h[:])
dbSchemas, err := ret.GetSchemas(ctx)
if err != nil {
return RootStorage{}, err
}
var newRootObjAdd func(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT)
if !h.IsEmpty() {
newRootObjAdd = serialization.RootValueAdd
}
msg, err := ret.serializeRootValue(ret.SRV.TablesBytes(), dbSchemas, newRootObjAdd)
if err != nil {
return RootStorage{}, err
}
ret = RootStorage{msg}
copy(serialization.Bytes(ret.SRV), h[:])
return ret, nil
}
@@ -127,7 +138,7 @@ func (r RootStorage) GetSchemas(ctx context.Context) ([]schema.DatabaseSchema, e
// 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)
msg, err := r.serializeRootValue(r.SRV.TablesBytes(), dbSchemas, nil)
if err != nil {
return RootStorage{}, err
}
@@ -259,7 +270,7 @@ func (r RootStorage) EditTablesMap(ctx context.Context, vrw types.ValueReadWrite
return RootStorage{}, err
}
msg, err := r.serializeRootValue(ambytes, dbSchemas)
msg, err := r.serializeRootValue(ambytes, dbSchemas, nil)
if err != nil {
return RootStorage{}, err
}
@@ -267,19 +278,22 @@ func (r RootStorage) EditTablesMap(ctx context.Context, vrw types.ValueReadWrite
}
// serializeRootValue serializes a new serial.RootValue object.
func (r RootStorage) serializeRootValue(addressMapBytes []byte, dbSchemas []schema.DatabaseSchema) (*serial.RootValue, error) {
func (r RootStorage) serializeRootValue(addressMapBytes []byte, dbSchemas []schema.DatabaseSchema, newRootObjAdd func(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT)) (*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[:]
fieldBytes := RootObjectSerializations[i].Bytes(r.SRV)
if len(fieldBytes) != hash.ByteLen || hash.New(fieldBytes).IsEmpty() {
continue
}
rootObjOffsets[i] = builder.CreateByteVector(rootObjOffset)
rootObjOffsets[i] = builder.CreateByteVector(fieldBytes)
}
var newRootObjOffset flatbuffers.UOffsetT
if newRootObjAdd != nil {
newRootObjOffset = builder.CreateByteVector(make([]byte, hash.ByteLen))
}
serial.RootValueStart(builder)
@@ -288,7 +302,12 @@ func (r RootStorage) serializeRootValue(addressMapBytes []byte, dbSchemas []sche
serial.RootValueAddTables(builder, tablesOffset)
serial.RootValueAddForeignKeyAddr(builder, fkOffset)
for i := range RootObjectSerializations {
RootObjectSerializations[i].RootValueAdd(builder, rootObjOffsets[i])
if rootObjOffsets[i] > 0 {
RootObjectSerializations[i].RootValueAdd(builder, rootObjOffsets[i])
}
}
if newRootObjAdd != nil {
newRootObjAdd(builder, newRootObjOffset)
}
if schemasOffset > 0 {
serial.RootValueAddSchemas(builder, schemasOffset)
+81 -1
View File
@@ -463,7 +463,75 @@ func (rcv *RootValue) MutateCasts(j int, n byte) bool {
return false
}
const RootValueNumFields = 13
func (rcv *RootValue) Operators(j int) byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(30))
if o != 0 {
a := rcv._tab.Vector(o)
return rcv._tab.GetByte(a + flatbuffers.UOffsetT(j*1))
}
return 0
}
func (rcv *RootValue) OperatorsLength() int {
o := flatbuffers.UOffsetT(rcv._tab.Offset(30))
if o != 0 {
return rcv._tab.VectorLen(o)
}
return 0
}
func (rcv *RootValue) OperatorsBytes() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(30))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *RootValue) MutateOperators(j int, n byte) bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(30))
if o != 0 {
a := rcv._tab.Vector(o)
return rcv._tab.MutateByte(a+flatbuffers.UOffsetT(j*1), n)
}
return false
}
func (rcv *RootValue) Aggregates(j int) byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(32))
if o != 0 {
a := rcv._tab.Vector(o)
return rcv._tab.GetByte(a + flatbuffers.UOffsetT(j*1))
}
return 0
}
func (rcv *RootValue) AggregatesLength() int {
o := flatbuffers.UOffsetT(rcv._tab.Offset(32))
if o != 0 {
return rcv._tab.VectorLen(o)
}
return 0
}
func (rcv *RootValue) AggregatesBytes() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(32))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *RootValue) MutateAggregates(j int, n byte) bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(32))
if o != 0 {
a := rcv._tab.Vector(o)
return rcv._tab.MutateByte(a+flatbuffers.UOffsetT(j*1), n)
}
return false
}
const RootValueNumFields = 15
func RootValueStart(builder *flatbuffers.Builder) {
builder.StartObject(RootValueNumFields)
@@ -540,6 +608,18 @@ func RootValueAddCasts(builder *flatbuffers.Builder, casts flatbuffers.UOffsetT)
func RootValueStartCastsVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
return builder.StartVector(1, numElems, 1)
}
func RootValueAddOperators(builder *flatbuffers.Builder, operators flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(13, flatbuffers.UOffsetT(operators), 0)
}
func RootValueStartOperatorsVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
return builder.StartVector(1, numElems, 1)
}
func RootValueAddAggregates(builder *flatbuffers.Builder, aggregates flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(14, flatbuffers.UOffsetT(aggregates), 0)
}
func RootValueStartAggregatesVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
return builder.StartVector(1, numElems, 1)
}
func RootValueEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+4
View File
@@ -43,6 +43,10 @@ table RootValue {
procedures:[ubyte]; // Serialized AddressMap.
casts:[ubyte]; // Serialized AddressMap.
operators:[ubyte]; // Serialized AddressMap.
aggregates:[ubyte]; // Serialized AddressMap.
}
table DatabaseSchema {
+24
View File
@@ -83,6 +83,7 @@ var KeywordsCategories = map[string]string{
"comments": "U",
"commit": "U",
"committed": "U",
"commutator": "U",
"compact": "U",
"complete": "U",
"compression": "U",
@@ -221,6 +222,7 @@ var KeywordsCategories = map[string]string{
"groups": "U",
"handler": "U",
"hash": "U",
"hashes": "U",
"having": "R",
"header": "U",
"high": "U",
@@ -294,6 +296,7 @@ var KeywordsCategories = map[string]string{
"lease": "U",
"least": "C",
"left": "T",
"leftarg": "U",
"less": "U",
"level": "U",
"like": "T",
@@ -315,6 +318,7 @@ var KeywordsCategories = map[string]string{
"materialized": "U",
"maxvalue": "U",
"merge": "U",
"merges": "U",
"method": "U",
"mfinalfunc": "U",
"mfinalfunc_extra": "U",
@@ -347,6 +351,7 @@ var KeywordsCategories = map[string]string{
"names": "U",
"nan": "U",
"natural": "T",
"negator": "U",
"never": "U",
"new": "U",
"next": "U",
@@ -478,6 +483,7 @@ var KeywordsCategories = map[string]string{
"revision_history": "U",
"revoke": "U",
"right": "T",
"rightarg": "U",
"role": "U",
"roles": "U",
"rollback": "U",
@@ -723,6 +729,7 @@ var KeywordNames = []string{
"comments",
"commit",
"committed",
"commutator",
"compact",
"complete",
"compression",
@@ -861,6 +868,7 @@ var KeywordNames = []string{
"groups",
"handler",
"hash",
"hashes",
"having",
"header",
"high",
@@ -934,6 +942,7 @@ var KeywordNames = []string{
"lease",
"least",
"left",
"leftarg",
"less",
"level",
"like",
@@ -955,6 +964,7 @@ var KeywordNames = []string{
"materialized",
"maxvalue",
"merge",
"merges",
"method",
"mfinalfunc",
"mfinalfunc_extra",
@@ -987,6 +997,7 @@ var KeywordNames = []string{
"names",
"nan",
"natural",
"negator",
"never",
"new",
"next",
@@ -1118,6 +1129,7 @@ var KeywordNames = []string{
"revision_history",
"revoke",
"right",
"rightarg",
"role",
"roles",
"rollback",
@@ -1448,6 +1460,8 @@ func GetKeywordID(k string) int32 {
return COMMIT
case "committed":
return COMMITTED
case "commutator":
return COMMUTATOR
case "compact":
return COMPACT
case "complete":
@@ -1724,6 +1738,8 @@ func GetKeywordID(k string) int32 {
return HANDLER
case "hash":
return HASH
case "hashes":
return HASHES
case "having":
return HAVING
case "header":
@@ -1870,6 +1886,8 @@ func GetKeywordID(k string) int32 {
return LEAST
case "left":
return LEFT
case "leftarg":
return LEFTARG
case "less":
return LESS
case "level":
@@ -1912,6 +1930,8 @@ func GetKeywordID(k string) int32 {
return MAXVALUE
case "merge":
return MERGE
case "merges":
return MERGES
case "method":
return METHOD
case "mfinalfunc":
@@ -1976,6 +1996,8 @@ func GetKeywordID(k string) int32 {
return NAN
case "natural":
return NATURAL
case "negator":
return NEGATOR
case "never":
return NEVER
case "new":
@@ -2238,6 +2260,8 @@ func GetKeywordID(k string) int32 {
return REVOKE
case "right":
return RIGHT
case "rightarg":
return RIGHTARG
case "role":
return ROLE
case "roles":
+693 -681
View File
File diff suppressed because it is too large Load Diff
+433 -403
View File
File diff suppressed because it is too large Load Diff
+37 -1
View File
@@ -272,7 +272,13 @@ func (s *scanner) scan(lval *sqlSymType) {
s.pos++
lval.id = NOT_EQUALS
return
case '=': // <=
case '=':
if s.peekN(1) == '>' { // <=>
s.pos += 2
lval.id = COSINE_DISTANCE
return
}
// <=
s.pos++
lval.id = LESS_EQUALS
return
@@ -280,6 +286,36 @@ func (s *scanner) scan(lval *sqlSymType) {
s.pos++
lval.id = CONTAINED_BY
return
case '-':
if s.peekN(1) == '>' { // <->
s.pos += 2
lval.id = L2_DISTANCE
return
}
case '+':
if s.peekN(1) == '>' { // <+>
s.pos += 2
lval.id = L1_DISTANCE
return
}
case '#':
if s.peekN(1) == '>' { // <#>
s.pos += 2
lval.id = NEG_INNER_PRODUCT
return
}
case '%':
if s.peekN(1) == '>' { // <%>
s.pos += 2
lval.id = JACCARD_DISTANCE
return
}
case '~':
if s.peekN(1) == '>' { // <~>
s.pos += 2
lval.id = HAMMING_DISTANCE
return
}
}
return
+28612 -27849
View File
File diff suppressed because it is too large Load Diff
+143 -8
View File
@@ -681,6 +681,18 @@ func (u *sqlSymUnion) createAggOptions() []tree.CreateAggOption {
func (u *sqlSymUnion) aggregatesToDrop() []tree.AggregateToDrop {
return u.val.([]tree.AggregateToDrop)
}
func (u *sqlSymUnion) createOperatorOption() tree.CreateOperatorOption {
return u.val.(tree.CreateOperatorOption)
}
func (u *sqlSymUnion) createOperatorOptions() []tree.CreateOperatorOption {
return u.val.([]tree.CreateOperatorOption)
}
func (u *sqlSymUnion) operatorToDrop() tree.OperatorToDrop {
return u.val.(tree.OperatorToDrop)
}
func (u *sqlSymUnion) operatorsToDrop() []tree.OperatorToDrop {
return u.val.([]tree.OperatorToDrop)
}
func (u *sqlSymUnion) vacuumOptions() tree.VacuumOptions {
return u.val.(tree.VacuumOptions)
}
@@ -705,6 +717,7 @@ func (u *sqlSymUnion) vacuumTableAndColsList() tree.VacuumTableAndColsList {
%token <str> TYPECAST TYPEANNOTATE DOT_DOT
%token <str> LESS_EQUALS GREATER_EQUALS NOT_EQUALS
%token <str> NOT_REGMATCH REGIMATCH NOT_REGIMATCH
%token <str> L1_DISTANCE L2_DISTANCE COSINE_DISTANCE NEG_INNER_PRODUCT JACCARD_DISTANCE HAMMING_DISTANCE
%token <str> TEXTSEARCHMATCH
%token <str> ERROR
@@ -726,7 +739,7 @@ func (u *sqlSymUnion) vacuumTableAndColsList() tree.VacuumTableAndColsList {
%token <str> CHANGEFEED BPCHAR CHAR CHARACTER CHARACTERISTICS CHECK CHECK_OPTION CLASS CLOSE
%token <str> CLUSTER COALESCE COLLATABLE COLLATE COLLATION COLLATION_VERSION COLUMN COLUMNS COMBINEFUNC COMMENT COMMENTS
%token <str> BLOCK_COMMENT HINT
%token <str> COMMIT COMMITTED COMPACT COMPLETE COMPRESSION CONCAT CONCURRENTLY CONFIGURATION CONFIGURATIONS CONFIGURE
%token <str> COMMIT COMMITTED COMMUTATOR COMPACT COMPLETE COMPRESSION CONCAT CONCURRENTLY CONFIGURATION CONFIGURATIONS CONFIGURE
%token <str> CONFLICT CONNECT CONNECTION CONSTRAINT CONSTRAINTS CONTAINS CONTROLCHANGEFEED
%token <str> CONTROLJOB CONVERSION CONVERT COPY COST CREATE CREATEDB CREATELOGIN CREATEROLE
%token <str> CROSS CUBE CURRENT CURRENT_CATALOG CURRENT_DATE CURRENT_SCHEMA
@@ -751,7 +764,7 @@ func (u *sqlSymUnion) vacuumTableAndColsList() tree.VacuumTableAndColsList {
%token <str> GEOMETRYCOLLECTION GEOMETRYCOLLECTIONM GEOMETRYCOLLECTIONZ GEOMETRYCOLLECTIONZM
%token <str> GLOBAL GRANT GRANTED GRANTS GREATEST GROUP GROUPING GROUPS
%token <str> HANDLER HASH HAVING HIGH HISTOGRAM HOUR HYPOTHETICAL
%token <str> HANDLER HASH HASHES HAVING HIGH HISTOGRAM HOUR HYPOTHETICAL
%token <str> ICU_LOCALE ICU_RULES IDENTITY
%token <str> IF IFERROR IFNULL IGNORE_FOREIGN_KEYS ILIKE IMMEDIATE IMPLICIT IMMUTABLE IMPORT
@@ -765,16 +778,16 @@ func (u *sqlSymUnion) vacuumTableAndColsList() tree.VacuumTableAndColsList {
%token <str> KEY KEYS KMS KV
%token <str> LANGUAGE LARGE LAST LATERAL LATEST LC_CTYPE LC_COLLATE
%token <str> LEADING LEAKPROOF LEASE LEAST LEFT LESS LEVEL LIKE LIMIT
%token <str> LEADING LEAKPROOF LEASE LEAST LEFT LEFTARG LESS LEVEL LIKE LIMIT
%token <str> LINESTRING LINESTRINGM LINESTRINGZ LINESTRINGZM LIST
%token <str> LOCAL LOCALE LOCALE_PROVIDER LOCALTIME LOCALTIMESTAMP LOCKED LOGGED LOGIN LOOKUP LOW LSHIFT
%token <str> MAIN MATCH MATERIALIZED MAXVALUE MERGE METHOD MFINALFUNC MFINALFUNC_EXTRA MFINALFUNC_MODIFY
%token <str> MAIN MATCH MATERIALIZED MAXVALUE MERGE MERGES METHOD MFINALFUNC MFINALFUNC_EXTRA MFINALFUNC_MODIFY
%token <str> MINITCOND MINUTE MINVALUE MINVFUNC MODIFYCLUSTERSETTING MODULUS MONTH MSFUNC MSPACE MSSPACE MSTYPE
%token <str> MULTILINESTRING MULTILINESTRINGM MULTILINESTRINGZ MULTILINESTRINGZM MULTIPOINT MULTIPOINTM
%token <str> MULTIPOINTZ MULTIPOINTZM MULTIPOLYGON MULTIPOLYGONM MULTIPOLYGONZ MULTIPOLYGONZM MULTIRANGE_TYPE_NAME
%token <str> NAN NAME NAMES NATURAL NEVER NEW NEXT NO NOCANCELQUERY NOCONTROLCHANGEFEED NOCONTROLJOB
%token <str> NAN NAME NAMES NATURAL NEGATOR NEVER NEW NEXT NO NOCANCELQUERY NOCONTROLCHANGEFEED NOCONTROLJOB
%token <str> NOBYPASSRLS NOCREATEDB NOCREATELOGIN NOCREATEROLE NOINHERIT NOLOGIN NOMODIFYCLUSTERSETTING NOREPLICATION NOSUPERUSER NO_INDEX_JOIN
%token <str> NONE NORMAL NOT NOTHING NOTNULL NOVIEWACTIVITY NOWAIT NULL NULLIF NULLS NUMERIC YES
@@ -791,7 +804,7 @@ func (u *sqlSymUnion) vacuumTableAndColsList() tree.VacuumTableAndColsList {
%token <str> RANGE RANGES READ READ_ONLY READ_WRITE REAL RECEIVE RECURSIVE RECURRING REF REFERENCES REFERENCING REFRESH
%token <str> REGCLASS REGPROC REGPROCEDURE REGNAMESPACE REGTYPE REINDEX RELEASE REMAINDER
%token <str> REMOVE_PATH RENAME REPEATABLE REPLACE REPLICA REPLICATION RESET RESTART RESTORE RESTRICT RESTRICTED RESUME
%token <str> RETRY RETURN RETURNING RETURNS REVISION_HISTORY REVOKE RIGHT
%token <str> RETRY RETURN RETURNING RETURNS REVISION_HISTORY REVOKE RIGHT RIGHTARG
%token <str> ROLE ROLES ROUTINE ROUTINES ROLLBACK ROLLUP ROW ROWS RSHIFT RULE RUNNING
%token <str> SAFE SAVEPOINT SCATTER SCHEDULE SCHEDULES SCHEMA SCHEMAS SCRUB SEARCH SECOND SECURITY
@@ -947,6 +960,9 @@ func (u *sqlSymUnion) vacuumTableAndColsList() tree.VacuumTableAndColsList {
%type <tree.Statement> create_aggregate_order_by_args_stmt
%type <tree.Statement> create_aggregate_old_syntax_stmt
%type <tree.Statement> create_operator_stmt
%type <tree.Statement> drop_operator_stmt
%type <tree.Statement> create_stats_stmt
%type <*tree.CreateStatsOptions> opt_create_stats_options
%type <*tree.CreateStatsOptions> create_stats_option_list
@@ -1110,6 +1126,10 @@ func (u *sqlSymUnion) vacuumTableAndColsList() tree.VacuumTableAndColsList {
%type <tree.CreateAggOption> create_agg_args_only_option create_agg_order_by_args_option
%type <tree.CreateAggOption> create_agg_old_syntax_option create_agg_common_option create_agg_parallel_option
%type <[]tree.CreateAggOption> create_agg_args_only_option_list create_agg_order_by_args_option_list create_agg_old_syntax_option_list
%type <tree.CreateOperatorOption> create_operator_option
%type <[]tree.CreateOperatorOption> create_operator_option_list
%type <tree.OperatorToDrop> operator_to_drop
%type <[]tree.OperatorToDrop> drop_operators
%type <[]tree.AggregateToDrop> drop_aggregates
%type <tree.CreateCastScope> create_cast_scope_opt
@@ -1441,6 +1461,7 @@ func (u *sqlSymUnion) vacuumTableAndColsList() tree.VacuumTableAndColsList {
%nonassoc UNBOUNDED // ideally should have same precedence as IDENT
%nonassoc IDENT NULL PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP
%left CONCAT FETCHVAL FETCHTEXT FETCHVAL_PATH FETCHTEXT_PATH REMOVE_PATH // multi-character ops
%left L1_DISTANCE L2_DISTANCE COSINE_DISTANCE NEG_INNER_PRODUCT JACCARD_DISTANCE HAMMING_DISTANCE
%left '|'
%left '#'
%left '&'
@@ -4060,6 +4081,7 @@ create_stmt:
| create_language_stmt // EXTEND WITH HELP: CREATE LANGUAGE
| create_aggregate_stmt // EXTEND WITH HELP: CREATE AGGREGATE
| create_cast_stmt // EXTEND WITH HELP: CREATE CAST
| create_operator_stmt // EXTEND WITH HELP: CREATE OPERATOR
| create_unsupported {}
| CREATE error // SHOW HELP: CREATE
@@ -4067,7 +4089,8 @@ create_unsupported:
CREATE CONVERSION error { return unimplemented(sqllex, "create conversion") }
| CREATE DEFAULT CONVERSION error { return unimplemented(sqllex, "create def conv") }
| CREATE FOREIGN TABLE error { return unimplemented(sqllex, "create foreign table") }
| CREATE OPERATOR error { return unimplemented(sqllex, "create operator") }
| CREATE OPERATOR CLASS error { return unimplemented(sqllex, "create operator class") }
| CREATE OPERATOR FAMILY error { return unimplemented(sqllex, "create operator family") }
| CREATE PUBLICATION error { return unimplemented(sqllex, "create publication") }
| CREATE opt_or_replace RULE error { return unimplemented(sqllex, "create rule") }
| CREATE SERVER error { return unimplemented(sqllex, "create server") }
@@ -4190,6 +4213,49 @@ create_agg_parallel_option:
| PARALLEL '=' UNSAFE
{ $$.val = tree.CreateAggOption{Option: tree.AggOptTypeParallel, Parallel: tree.ParallelSafe} }
// %Help: CREATE OPERATOR - define a new operator
// %Category: DDL
// %Text: CREATE OPERATOR name (
// {FUNCTION|PROCEDURE} = function_name
// [, LEFTARG = left_type ] [, RIGHTARG = right_type ]
// [, COMMUTATOR = com_op ] [, NEGATOR = neg_op ]
// [, RESTRICT = res_proc ] [, JOIN = join_proc ]
// [, HASHES ] [, MERGES ]
// )
// %SeeAlso: WEBDOCS/sql-createoperator.html
create_operator_stmt:
CREATE OPERATOR operator '(' create_operator_option_list ')'
{ $$.val = &tree.CreateOperator{Name: $3.op(), Options: $5.createOperatorOptions()} }
| CREATE OPERATOR error // SHOW HELP: CREATE OPERATOR
create_operator_option_list:
create_operator_option
{ $$.val = []tree.CreateOperatorOption{$1.createOperatorOption()} }
| create_operator_option_list ',' create_operator_option
{ $$.val = append($1.createOperatorOptions(), $3.createOperatorOption()) }
create_operator_option:
FUNCTION '=' routine_name
{ $$.val = tree.CreateOperatorOption{Option: tree.OperatorOptTypeFunction, FuncName: $3.unresolvedObjectName()} }
| PROCEDURE '=' routine_name
{ $$.val = tree.CreateOperatorOption{Option: tree.OperatorOptTypeFunction, FuncName: $3.unresolvedObjectName()} }
| LEFTARG '=' type_name
{ $$.val = tree.CreateOperatorOption{Option: tree.OperatorOptTypeLeftArg, TypeVal: $3.typeReference()} }
| RIGHTARG '=' type_name
{ $$.val = tree.CreateOperatorOption{Option: tree.OperatorOptTypeRightArg, TypeVal: $3.typeReference()} }
| COMMUTATOR '=' operator
{ $$.val = tree.CreateOperatorOption{Option: tree.OperatorOptTypeCommutator, OpVal: $3.op()} }
| NEGATOR '=' operator
{ $$.val = tree.CreateOperatorOption{Option: tree.OperatorOptTypeNegator, OpVal: $3.op()} }
| RESTRICT '=' routine_name
{ $$.val = tree.CreateOperatorOption{Option: tree.OperatorOptTypeRestrict, FuncName: $3.unresolvedObjectName()} }
| JOIN '=' routine_name
{ $$.val = tree.CreateOperatorOption{Option: tree.OperatorOptTypeJoin, FuncName: $3.unresolvedObjectName()} }
| HASHES
{ $$.val = tree.CreateOperatorOption{Option: tree.OperatorOptTypeHashes} }
| MERGES
{ $$.val = tree.CreateOperatorOption{Option: tree.OperatorOptTypeMerges} }
// %Help: CREATE CAST - define a new cast
// %Category: DDL
// %Text: CREATE CAST (source_type AS target_type) WITH FUNCTION function_name [ (argument_type [, ...]) ] [ AS ASSIGNMENT | AS IMPLICIT ]
@@ -4742,7 +4808,8 @@ drop_unsupported:
| DROP CONVERSION error { return unimplemented(sqllex, "drop conversion") }
| DROP FOREIGN TABLE error { return unimplemented(sqllex, "drop foreign table") }
| DROP FOREIGN DATA error { return unimplemented(sqllex, "drop fdw") }
| DROP OPERATOR error { return unimplemented(sqllex, "drop operator") }
| DROP OPERATOR CLASS error { return unimplemented(sqllex, "drop operator class") }
| DROP OPERATOR FAMILY error { return unimplemented(sqllex, "drop operator family") }
| DROP PUBLICATION error { return unimplemented(sqllex, "drop publication") }
| DROP RULE error { return unimplemented(sqllex, "drop rule") }
| DROP SERVER error { return unimplemented(sqllex, "drop server") }
@@ -4769,6 +4836,37 @@ drop_aggregates:
$$.val = append($1.aggregatesToDrop(), tree.AggregateToDrop{Name: $3.unresolvedObjectName(), AggSig: $5.aggregateSignature()})
}
// %Help: DROP OPERATOR - remove an operator
// %Category: DDL
// %Text: DROP OPERATOR [ IF EXISTS ] name ( { left_type | NONE } , right_type ) [, ...] [ CASCADE | RESTRICT ]
// %SeeAlso: WEBDOCS/sql-dropoperator.html
drop_operator_stmt:
DROP OPERATOR drop_operators opt_drop_behavior
{
$$.val = &tree.DropOperator{Operators: $3.operatorsToDrop(), DropBehavior: $4.dropBehavior()}
}
| DROP OPERATOR IF EXISTS drop_operators opt_drop_behavior
{
$$.val = &tree.DropOperator{Operators: $5.operatorsToDrop(), IfExists: true, DropBehavior: $6.dropBehavior()}
}
| DROP OPERATOR error // SHOW HELP: DROP OPERATOR
drop_operators:
operator_to_drop
{
$$.val = []tree.OperatorToDrop{$1.operatorToDrop()}
}
| drop_operators ',' operator_to_drop
{
$$.val = append($1.operatorsToDrop(), $3.operatorToDrop())
}
operator_to_drop:
operator '(' typename ',' typename ')'
{
$$.val = tree.OperatorToDrop{Op: $1.op(), Left: tree.OperatorArgType($3.typeReference()), Right: tree.OperatorArgType($5.typeReference())}
}
drop_domain_stmt:
DROP DOMAIN table_name_list opt_drop_behavior
{
@@ -5047,6 +5145,7 @@ drop_stmt:
| drop_extension_stmt // EXTEND WITH HELP: DROP EXTENSION
| drop_language_stmt // EXTEND WITH HELP: DROP LANGUAGE
| drop_aggregate_stmt // EXTEND WITH HELP: DROP AGGREGATE
| drop_operator_stmt // EXTEND WITH HELP: DROP OPERATOR
| drop_unsupported {}
| DROP error // SHOW HELP: DROP
@@ -12625,6 +12724,30 @@ a_expr:
{
$$.val = &tree.FuncExpr{Func: tree.WrapFunction("json_remove_path"), Exprs: tree.Exprs{$1.expr(), $3.expr()}}
}
| a_expr L2_DISTANCE a_expr
{
$$.val = &tree.BinaryExpr{Operator: tree.L2Distance, Left: $1.expr(), Right: $3.expr()}
}
| a_expr L1_DISTANCE a_expr
{
$$.val = &tree.BinaryExpr{Operator: tree.L1Distance, Left: $1.expr(), Right: $3.expr()}
}
| a_expr COSINE_DISTANCE a_expr
{
$$.val = &tree.BinaryExpr{Operator: tree.CosineDistance, Left: $1.expr(), Right: $3.expr()}
}
| a_expr NEG_INNER_PRODUCT a_expr
{
$$.val = &tree.BinaryExpr{Operator: tree.NegInnerProduct, Left: $1.expr(), Right: $3.expr()}
}
| a_expr JACCARD_DISTANCE a_expr
{
$$.val = &tree.BinaryExpr{Operator: tree.JaccardDistance, Left: $1.expr(), Right: $3.expr()}
}
| a_expr HAMMING_DISTANCE a_expr
{
$$.val = &tree.BinaryExpr{Operator: tree.HammingDistance, Left: $1.expr(), Right: $3.expr()}
}
| a_expr INET_CONTAINED_BY_OR_EQUALS a_expr
{
$$.val = &tree.FuncExpr{Func: tree.WrapFunction("inet_contained_by_or_equals"), Exprs: tree.Exprs{$1.expr(), $3.expr()}}
@@ -13878,6 +14001,12 @@ operator:
| FETCHTEXT_PATH { $$.val = tree.JSONFetchTextPath }
| AND_AND { $$.val = tree.Overlaps }
| TEXTSEARCHMATCH { $$.val = tree.TextSearchMatch }
| L2_DISTANCE { $$.val = tree.L2Distance }
| L1_DISTANCE { $$.val = tree.L1Distance }
| COSINE_DISTANCE { $$.val = tree.CosineDistance }
| NEG_INNER_PRODUCT { $$.val = tree.NegInnerProduct }
| JACCARD_DISTANCE { $$.val = tree.JaccardDistance }
| HAMMING_DISTANCE { $$.val = tree.HammingDistance }
math_op:
'+' { $$.val = tree.Plus }
@@ -14863,6 +14992,7 @@ unreserved_keyword:
| COMMENTS
| COMMIT
| COMMITTED
| COMMUTATOR
| COMPACT
| COMPLETE
| COMPRESSION
@@ -14960,6 +15090,7 @@ unreserved_keyword:
| GROUPS
| HANDLER
| HASH
| HASHES
| HEADER
| HIGH
| HISTOGRAM
@@ -15009,6 +15140,7 @@ unreserved_keyword:
| LC_CTYPE
| LEAKPROOF
| LEASE
| LEFTARG
| LESS
| LEVEL
| LINESTRING
@@ -15026,6 +15158,7 @@ unreserved_keyword:
| MATERIALIZED
| MAXVALUE
| MERGE
| MERGES
| METHOD
| MFINALFUNC
| MFINALFUNC_EXTRA
@@ -15057,6 +15190,7 @@ unreserved_keyword:
| NAME
| NAMES
| NAN
| NEGATOR
| NEVER
| NEW
| NEXT
@@ -15162,6 +15296,7 @@ unreserved_keyword:
| RETURNS
| REVISION_HISTORY
| REVOKE
| RIGHTARG
| ROLE
| ROLES
| ROLLBACK
+1 -1
View File
@@ -57,7 +57,7 @@ func (node *CreateAggregate) Format(ctx *FmtCtx) {
ctx.WriteString("SFUNC = ")
ctx.FormatNode(node.SFunc)
ctx.WriteString(" , STYPE = ")
ctx.WriteString(node.BaseType.SQLString())
ctx.WriteString(node.SType.SQLString())
if node.AggOptions != nil {
ctx.FormatNode(&node.AggOptions)
}
+108
View File
@@ -0,0 +1,108 @@
// 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 tree
var _ Statement = &CreateOperator{}
// CreateOperator represents a CREATE OPERATOR statement.
type CreateOperator struct {
Name Operator
Options []CreateOperatorOption
}
// Format implements the NodeFormatter interface.
func (node *CreateOperator) Format(ctx *FmtCtx) {
ctx.WriteString("CREATE OPERATOR ")
ctx.WriteString(OperatorSymbol(node.Name))
ctx.WriteString(" ( ")
for i := range node.Options {
if i > 0 {
ctx.WriteString(" , ")
}
ctx.FormatNode(&node.Options[i])
}
ctx.WriteString(" )")
}
// CreateOperatorOption represents a single option of a CREATE OPERATOR statement.
type CreateOperatorOption struct {
Option CreateOperatorOptionType
// FuncName is used for Function, Restrict, and Join
FuncName *UnresolvedObjectName
// TypeVal is used for LeftArg and RightArg
TypeVal ResolvableTypeReference
// OpVal is used for Commutator and Negator
OpVal Operator
// Hashes and Merges do not define any stored value.
}
// Format implements the NodeFormatter interface.
func (node *CreateOperatorOption) Format(ctx *FmtCtx) {
switch node.Option {
case OperatorOptTypeFunction:
ctx.WriteString("FUNCTION = ")
ctx.FormatNode(node.FuncName)
case OperatorOptTypeLeftArg:
ctx.WriteString("LEFTARG = ")
ctx.WriteString(node.TypeVal.SQLString())
case OperatorOptTypeRightArg:
ctx.WriteString("RIGHTARG = ")
ctx.WriteString(node.TypeVal.SQLString())
case OperatorOptTypeCommutator:
ctx.WriteString("COMMUTATOR = ")
ctx.WriteString(OperatorSymbol(node.OpVal))
case OperatorOptTypeNegator:
ctx.WriteString("NEGATOR = ")
ctx.WriteString(OperatorSymbol(node.OpVal))
case OperatorOptTypeRestrict:
ctx.WriteString("RESTRICT = ")
ctx.FormatNode(node.FuncName)
case OperatorOptTypeJoin:
ctx.WriteString("JOIN = ")
ctx.FormatNode(node.FuncName)
case OperatorOptTypeHashes:
ctx.WriteString("HASHES")
case OperatorOptTypeMerges:
ctx.WriteString("MERGES")
}
}
// OperatorSymbol returns the operator's symbol.
func OperatorSymbol(op Operator) string {
switch op := op.(type) {
case UnaryOperator:
return op.String()
case BinaryOperator:
return op.String()
case ComparisonOperator:
return op.String()
}
return ""
}
// CreateOperatorOptionType represents the type of a CREATE OPERATOR option.
type CreateOperatorOptionType int
const (
OperatorOptTypeFunction CreateOperatorOptionType = iota
OperatorOptTypeLeftArg
OperatorOptTypeRightArg
OperatorOptTypeCommutator
OperatorOptTypeNegator
OperatorOptTypeRestrict
OperatorOptTypeJoin
OperatorOptTypeHashes
OperatorOptTypeMerges
)
+59
View File
@@ -90,6 +90,65 @@ func (node *DropAggregate) Format(ctx *FmtCtx) {
}
}
var _ Statement = &DropOperator{}
// DropOperator represents a DROP OPERATOR statement.
type DropOperator struct {
Operators []OperatorToDrop
IfExists bool
DropBehavior DropBehavior
}
// OperatorToDrop is a single operator listed in a DROP OPERATOR statement.
type OperatorToDrop struct {
Op Operator
// Left is nil when the statement writes NONE, denoting a prefix operator
Left ResolvableTypeReference
Right ResolvableTypeReference
}
// OperatorArgType returns the given operator operand type, converting the NONE keyword (which parses as a type named
// "none") to nil.
func OperatorArgType(typ ResolvableTypeReference) ResolvableTypeReference {
if uon, ok := typ.(*UnresolvedObjectName); ok && uon.NumParts == 1 && uon.Parts[0] == "none" {
return nil
}
return typ
}
// Format implements the NodeFormatter interface.
func (node *DropOperator) Format(ctx *FmtCtx) {
ctx.WriteString("DROP OPERATOR ")
if node.IfExists {
ctx.WriteString("IF EXISTS ")
}
for i, op := range node.Operators {
if i != 0 {
ctx.WriteString(" , ")
}
ctx.WriteString(OperatorSymbol(op.Op))
ctx.WriteString(" ( ")
if op.Left != nil {
ctx.WriteString(op.Left.SQLString())
} else {
ctx.WriteString("NONE")
}
ctx.WriteString(" , ")
if op.Right != nil {
ctx.WriteString(op.Right.SQLString())
} else {
ctx.WriteString("NONE")
}
ctx.WriteString(" )")
}
switch node.DropBehavior {
case DropDefault:
default:
ctx.WriteByte(' ')
ctx.WriteString(dropBehaviorName[node.DropBehavior])
}
}
// DropCast represents a DROP CAST statement.
type DropCast struct {
Source ResolvableTypeReference
+14
View File
@@ -1101,6 +1101,12 @@ const (
JSONFetchText
JSONFetchValPath
JSONFetchTextPath
L2Distance
L1Distance
CosineDistance
NegInnerProduct
JaccardDistance
HammingDistance
NumBinaryOperators
)
@@ -1125,6 +1131,12 @@ var binaryOpName = [...]string{
JSONFetchText: "->>",
JSONFetchValPath: "#>",
JSONFetchTextPath: "#>>",
L2Distance: "<->",
L1Distance: "<+>",
CosineDistance: "<=>",
NegInnerProduct: "<#>",
JaccardDistance: "<%>",
HammingDistance: "<~>",
}
// binaryOpPrio follows the precedence order in the grammar. Used for pretty-printing.
@@ -1137,6 +1149,8 @@ var binaryOpPrio = [...]int{
Bitxor: 6,
Bitor: 7,
Concat: 8, JSONFetchVal: 8, JSONFetchText: 8, JSONFetchValPath: 8, JSONFetchTextPath: 8,
L2Distance: 8, L1Distance: 8, CosineDistance: 8, NegInnerProduct: 8, JaccardDistance: 8,
HammingDistance: 8,
}
// binaryOpFullyAssoc indicates whether an operator is fully associative.
+14
View File
@@ -525,6 +525,12 @@ func (*CreateMaterializedView) StatementType() StatementType { return DDL }
// StatementTag returns a short string identifying the type of statement.
func (*CreateMaterializedView) StatementTag() string { return "CREATE MATERIALIZED VIEW" }
// StatementType implements the Statement interface.
func (*CreateOperator) StatementType() StatementType { return DDL }
// StatementTag returns a short string identifying the type of statement.
func (*CreateOperator) StatementTag() string { return "CREATE OPERATOR" }
// StatementType implements the Statement interface.
func (*CreateProcedure) StatementType() StatementType { return DDL }
@@ -670,6 +676,12 @@ func (*DropLanguage) StatementType() StatementType { return DDL }
// StatementTag returns a short string identifying the type of statement.
func (*DropLanguage) StatementTag() string { return "DROP LANGUAGE" }
// StatementType implements the Statement interface.
func (*DropOperator) StatementType() StatementType { return DDL }
// StatementTag returns a short string identifying the type of statement.
func (*DropOperator) StatementTag() string { return "DROP OPERATOR" }
// StatementType implements the Statement interface.
func (*DropProcedure) StatementType() StatementType { return DDL }
@@ -1238,6 +1250,7 @@ func (n *CreateFunction) String() string { return AsString(n) }
func (n *CreateIndex) String() string { return AsString(n) }
func (n *CreateLanguage) String() string { return AsString(n) }
func (n *CreateMaterializedView) String() string { return AsString(n) }
func (n *CreateOperator) String() string { return AsString(n) }
func (n *CreateProcedure) String() string { return AsString(n) }
func (n *CreateRole) String() string { return AsString(n) }
func (n *CreateTable) String() string { return AsString(n) }
@@ -1257,6 +1270,7 @@ func (n *DropExtension) String() string { return AsString(n) }
func (n *DropFunction) String() string { return AsString(n) }
func (n *DropIndex) String() string { return AsString(n) }
func (n *DropLanguage) String() string { return AsString(n) }
func (n *DropOperator) String() string { return AsString(n) }
func (n *DropProcedure) String() string { return AsString(n) }
func (n *DropSchema) String() string { return AsString(n) }
func (n *DropTable) String() string { return AsString(n) }
+19 -4
View File
@@ -21,6 +21,7 @@ import (
"github.com/dolthub/go-mysql-server/sql/plan"
"github.com/dolthub/go-mysql-server/sql/planbuilder"
"github.com/dolthub/doltgresql/core"
pgexpression "github.com/dolthub/doltgresql/server/expression"
)
@@ -157,23 +158,37 @@ var postgresOnlyWindowFuncNames = map[string]bool{
}
// IsAggregateFunc checks if the given function name is an aggregate function. This is the entire set supported by
// MySQL plus some postgres specific ones.
// MySQL plus some postgres specific ones, along with every user-defined aggregate.
func IsAggregateFunc(ctx *sql.Context, name string) (bool, error) {
isAggregate, err := planbuilder.IsMySQLAggregateFuncName(ctx, name)
if err != nil {
return false, err
}
return isAggregate || postgresOnlyAggregateFuncNames[name], nil
if isAggregate || postgresOnlyAggregateFuncNames[name] {
return true, nil
}
collection, err := core.GetAggregatesCollectionFromContext(ctx, "")
if err != nil {
return false, err
}
return collection.HasAggregateName(ctx, name)
}
// IsWindowFunc checks if the given function name is a window function. This is the entire set supported by
// MySQL plus some postgres specific ones.
// MySQL plus some postgres specific ones, along with every user-defined aggregate.
func IsWindowFunc(ctx *sql.Context, name string) (bool, error) {
isWindow, err := planbuilder.IsMySQLWindowFuncName(ctx, name)
if err != nil {
return false, err
}
return isWindow || postgresOnlyAggregateFuncNames[name] || postgresOnlyWindowFuncNames[name], nil
if isWindow || postgresOnlyAggregateFuncNames[name] || postgresOnlyWindowFuncNames[name] {
return true, nil
}
collection, err := core.GetAggregatesCollectionFromContext(ctx, "")
if err != nil {
return false, err
}
return collection.HasAggregateName(ctx, name)
}
// insertAnalyzerRules inserts the given rule(s) before or after the given analyzer.RuleId, returning an updated slice.
+72
View File
@@ -65,6 +65,26 @@ func ResolveTypeForNodes(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node,
col.Type = dt
}
return node, same, nil
case *pgnodes.CreateAggregate:
if !n.SType.IsResolvedType() {
sType, err := resolveType(ctx, db, n.SType)
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
n.SType = sType
}
for i, argType := range n.ArgTypes {
if !argType.IsResolvedType() {
dt, err := resolveType(ctx, db, argType)
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
n.ArgTypes[i] = dt
}
}
return node, same, nil
case *pgnodes.CreateCast:
if !n.Source.IsResolvedType() {
source, err := resolveType(ctx, db, n.Source)
@@ -113,6 +133,24 @@ func ResolveTypeForNodes(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node,
}
}
return node, same, nil
case *pgnodes.CreateOperator:
if n.Left != nil && !n.Left.IsResolvedType() {
left, err := resolveType(ctx, db, n.Left)
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
n.Left = left
}
if !n.Right.IsResolvedType() {
right, err := resolveType(ctx, db, n.Right)
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
n.Right = right
}
return node, same, nil
case *pgnodes.CreateProcedure:
for i := range n.Parameters {
var err error
@@ -161,6 +199,20 @@ func ResolveTypeForNodes(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node,
}
}
return node, same, nil
case *pgnodes.DropAggregate:
for _, agg := range n.Aggregates {
for i, argType := range agg.ArgTypes {
if !argType.IsResolvedType() {
dt, err := resolveType(ctx, db, argType)
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
agg.ArgTypes[i] = dt
}
}
}
return node, same, nil
case *pgnodes.DropCast:
if !n.Source.IsResolvedType() {
source, err := resolveType(ctx, db, n.Source)
@@ -194,6 +246,26 @@ func ResolveTypeForNodes(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node,
}
}
return node, same, nil
case *pgnodes.DropOperator:
for _, op := range n.Operators {
if op.Left != nil && !op.Left.IsResolvedType() {
left, err := resolveType(ctx, db, op.Left)
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
op.Left = left
}
if !op.Right.IsResolvedType() {
right, err := resolveType(ctx, db, op.Right)
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
op.Right = right
}
}
return node, same, nil
case *pgnodes.DropProcedure:
for _, r := range n.RoutinesWithArgs {
for j, arg := range r.Args {
+4
View File
@@ -109,6 +109,8 @@ func Convert(postgresStmt parser.Statement) (vitess.Statement, error) {
return nodeCreateIndex(ctx, stmt)
case *tree.CreateMaterializedView:
return nodeCreateMaterializedView(ctx, stmt)
case *tree.CreateOperator:
return nodeCreateOperator(ctx, stmt)
case *tree.CreateProcedure:
return nodeCreateProcedure(ctx, stmt)
case *tree.CreateRole:
@@ -147,6 +149,8 @@ func Convert(postgresStmt parser.Statement) (vitess.Statement, error) {
return nodeDropFunction(ctx, stmt)
case *tree.DropIndex:
return nodeDropIndex(ctx, stmt)
case *tree.DropOperator:
return nodeDropOperator(ctx, stmt)
case *tree.DropProcedure:
return nodeDropProcedure(ctx, stmt)
case *tree.DropRole:
+87 -7
View File
@@ -20,6 +20,9 @@ import (
vitess "github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/dolthub/doltgresql/postgres/parser/sem/tree"
"github.com/dolthub/doltgresql/server/auth"
pgnodes "github.com/dolthub/doltgresql/server/node"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// nodeCreateAggregate handles *tree.CreateAggregate nodes.
@@ -27,14 +30,91 @@ func nodeCreateAggregate(ctx *Context, node *tree.CreateAggregate) (vitess.State
if node == nil {
return nil, nil
}
if !ignoreUnsupportedStatements {
if err := validateAggArgMode(ctx, node.Args, node.OrderByArgs); err != nil {
if err := validateAggArgMode(ctx, node.Args, node.OrderByArgs); err != nil {
return nil, err
}
if len(node.OrderByArgs) > 0 {
return NotYetSupportedError("ordered-set aggregates are not yet supported")
}
var argTypeRefs []tree.ResolvableTypeReference
if node.Args != nil {
for _, arg := range node.Args {
if arg.Mode == tree.RoutineArgModeVariadic {
return NotYetSupportedError("VARIADIC aggregates are not yet supported")
}
argTypeRefs = append(argTypeRefs, arg.Type)
}
} else {
argTypeRefs = append(argTypeRefs, node.BaseType)
}
argTypes := make([]*pgtypes.DoltgresType, len(argTypeRefs))
for i, argTypeRef := range argTypeRefs {
_, argType, err := nodeResolvableTypeReference(ctx, argTypeRef, false)
if err != nil {
return nil, err
}
argTypes[i] = argType
}
return NotYetSupportedError("CREATE AGGREGATE is not yet supported")
_, sType, err := nodeResolvableTypeReference(ctx, node.SType, false)
if err != nil {
return nil, err
}
var finalFunc, combineFunc *tree.UnresolvedObjectName
var initCond string
var hasInitCond bool
for _, option := range node.AggOptions {
switch option.Option {
case tree.AggOptTypeFinalFunc:
finalFunc = option.FuncName
case tree.AggOptTypeCombineFunc:
combineFunc = option.FuncName
case tree.AggOptTypeInitCond:
if strVal, ok := option.CondVal.(*tree.StrVal); ok {
initCond = strVal.RawString()
} else {
initCond = tree.AsString(option.CondVal)
}
hasInitCond = true
default:
return NotYetSupportedError("the given aggregate option is not yet supported")
}
}
var finalFuncSchema, finalFuncName string
if finalFunc != nil {
finalFuncTableName := finalFunc.ToTableName()
finalFuncSchema = finalFuncTableName.Schema()
finalFuncName = finalFuncTableName.Table()
}
var combineFuncSchema, combineFuncName string
if combineFunc != nil {
combineFuncTableName := combineFunc.ToTableName()
combineFuncSchema = combineFuncTableName.Schema()
combineFuncName = combineFuncTableName.Table()
}
name := node.Name.ToTableName()
sFuncName := node.SFunc.ToTableName()
return vitess.InjectedStatement{
Statement: pgnodes.NewCreateAggregate(
name.Schema(),
name.Table(),
node.Replace,
argTypes,
sType,
sFuncName.Schema(),
sFuncName.Table(),
finalFuncSchema,
finalFuncName,
combineFuncSchema,
combineFuncName,
initCond,
hasInitCond,
),
Auth: vitess.AuthInformation{
AuthType: auth.AuthType_CREATE,
TargetType: auth.AuthTargetType_TODO,
TargetNames: []string{},
},
}, nil
}
// validateAggArgMode checks routine arguments for `OUT` and `INOUT` modes,
@@ -42,12 +122,12 @@ func nodeCreateAggregate(ctx *Context, node *tree.CreateAggregate) (vitess.State
func validateAggArgMode(ctx *Context, args, orderByArgs tree.RoutineArgs) error {
for _, sig := range args {
if sig.Mode == tree.RoutineArgModeOut || sig.Mode == tree.RoutineArgModeInout {
return errors.Errorf("aggregate functions do not support OUT or INOUT arguments")
return errors.Errorf("aggregates cannot have output arguments")
}
}
for _, sig := range orderByArgs {
if sig.Mode == tree.RoutineArgModeOut || sig.Mode == tree.RoutineArgModeInout {
return errors.Errorf("aggregate functions do not support OUT or INOUT arguments")
return errors.Errorf("aggregates cannot have output arguments")
}
}
return nil
+99
View File
@@ -0,0 +1,99 @@
// 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 ast
import (
"github.com/cockroachdb/errors"
vitess "github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/dolthub/doltgresql/postgres/parser/sem/tree"
"github.com/dolthub/doltgresql/server/auth"
pgnodes "github.com/dolthub/doltgresql/server/node"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// nodeCreateOperator handles *tree.CreateOperator nodes.
func nodeCreateOperator(ctx *Context, node *tree.CreateOperator) (vitess.Statement, error) {
if node == nil {
return nil, nil
}
var function *tree.UnresolvedObjectName
var leftArg, rightArg tree.ResolvableTypeReference
var commutator, negator string
var hashes, merges bool
for _, option := range node.Options {
switch option.Option {
case tree.OperatorOptTypeFunction:
function = option.FuncName
case tree.OperatorOptTypeLeftArg:
leftArg = option.TypeVal
case tree.OperatorOptTypeRightArg:
rightArg = option.TypeVal
case tree.OperatorOptTypeCommutator:
commutator = tree.OperatorSymbol(option.OpVal)
case tree.OperatorOptTypeNegator:
negator = tree.OperatorSymbol(option.OpVal)
case tree.OperatorOptTypeRestrict:
return NotYetSupportedError("RESTRICT is not yet supported")
case tree.OperatorOptTypeJoin:
return NotYetSupportedError("JOIN is not yet supported")
case tree.OperatorOptTypeHashes:
hashes = true
case tree.OperatorOptTypeMerges:
merges = true
}
}
if function == nil {
return nil, errors.New("operator function must be specified")
}
if leftArg == nil && rightArg == nil {
return nil, errors.New("operator argument types must be specified")
}
if rightArg == nil {
return nil, errors.New("operator right argument type must be specified")
}
var err error
var leftType *pgtypes.DoltgresType
if leftArg != nil {
_, leftType, err = nodeResolvableTypeReference(ctx, leftArg, false)
if err != nil {
return nil, err
}
}
_, rightType, err := nodeResolvableTypeReference(ctx, rightArg, false)
if err != nil {
return nil, err
}
funcName := function.ToTableName()
return vitess.InjectedStatement{
Statement: pgnodes.NewCreateOperator(
tree.OperatorSymbol(node.Name),
leftType,
rightType,
funcName.Schema(),
funcName.Table(),
commutator,
negator,
hashes,
merges,
),
Auth: vitess.AuthInformation{
AuthType: auth.AuthType_CREATE,
TargetType: auth.AuthTargetType_TODO,
TargetNames: []string{},
},
}, nil
}
+33 -6
View File
@@ -15,9 +15,13 @@
package ast
import (
"fmt"
vitess "github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/dolthub/doltgresql/postgres/parser/sem/tree"
pgnodes "github.com/dolthub/doltgresql/server/node"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// nodeDropAggregate handles *tree.DropAggregate nodes.
@@ -25,14 +29,37 @@ func nodeDropAggregate(ctx *Context, node *tree.DropAggregate) (vitess.Statement
if node == nil {
return nil, nil
}
if !ignoreUnsupportedStatements {
for _, agg := range node.Aggregates {
if err := validateAggArgMode(ctx, agg.AggSig.Args, agg.AggSig.OrderByArgs); err != nil {
if node.DropBehavior == tree.DropCascade {
return nil, fmt.Errorf("DROP AGGREGATE with CASCADE is not supported yet")
}
aggs := make([]*pgnodes.AggregateToDrop, len(node.Aggregates))
for i, agg := range node.Aggregates {
if err := validateAggArgMode(ctx, agg.AggSig.Args, agg.AggSig.OrderByArgs); err != nil {
return nil, err
}
if agg.AggSig.All {
return NotYetSupportedError("DROP AGGREGATE with a * signature is not yet supported")
}
if len(agg.AggSig.OrderByArgs) > 0 {
return NotYetSupportedError("ordered-set aggregates are not yet supported")
}
argTypes := make([]*pgtypes.DoltgresType, len(agg.AggSig.Args))
for j, arg := range agg.AggSig.Args {
_, argType, err := nodeResolvableTypeReference(ctx, arg.Type, false)
if err != nil {
return nil, err
}
argTypes[j] = argType
}
name := agg.Name.ToTableName()
aggs[i] = &pgnodes.AggregateToDrop{
SchemaName: name.Schema(),
Name: name.Table(),
ArgTypes: argTypes,
}
}
return NotYetSupportedError("DROP AGGREGATE is not yet supported")
return vitess.InjectedStatement{
Statement: pgnodes.NewDropAggregate(node.IfExists, aggs),
Children: nil,
}, nil
}
+63
View File
@@ -0,0 +1,63 @@
// 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 ast
import (
"fmt"
"github.com/cockroachdb/errors"
vitess "github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/dolthub/doltgresql/postgres/parser/sem/tree"
pgnodes "github.com/dolthub/doltgresql/server/node"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// nodeDropOperator handles *tree.DropOperator nodes.
func nodeDropOperator(ctx *Context, node *tree.DropOperator) (vitess.Statement, error) {
if node == nil {
return nil, nil
}
if node.DropBehavior == tree.DropCascade {
return nil, fmt.Errorf("DROP OPERATOR with CASCADE is not supported yet")
}
ops := make([]*pgnodes.OperatorToDrop, len(node.Operators))
for i, op := range node.Operators {
if op.Right == nil {
return nil, errors.New("postfix operators are not supported")
}
var err error
var leftType *pgtypes.DoltgresType
if op.Left != nil {
_, leftType, err = nodeResolvableTypeReference(ctx, op.Left, false)
if err != nil {
return nil, err
}
}
_, rightType, err := nodeResolvableTypeReference(ctx, op.Right, false)
if err != nil {
return nil, err
}
ops[i] = &pgnodes.OperatorToDrop{
Symbol: tree.OperatorSymbol(op.Op),
Left: leftType,
Right: rightType,
}
}
return vitess.InjectedStatement{
Statement: pgnodes.NewDropOperator(node.IfExists, ops),
Children: nil,
}, nil
}
+12
View File
@@ -195,6 +195,18 @@ func nodeExpr(ctx *Context, node tree.Expr) (vitess.Expr, error) {
operator = framework.Operator_BinaryJSONExtractPathJson
case tree.JSONFetchTextPath:
operator = framework.Operator_BinaryJSONExtractPathText
case tree.L2Distance:
operator = framework.Operator_BinaryL2Distance
case tree.L1Distance:
operator = framework.Operator_BinaryL1Distance
case tree.CosineDistance:
operator = framework.Operator_BinaryCosineDistance
case tree.NegInnerProduct:
operator = framework.Operator_BinaryNegInnerProduct
case tree.JaccardDistance:
operator = framework.Operator_BinaryJaccardDistance
case tree.HammingDistance:
operator = framework.Operator_BinaryHammingDistance
default:
return nil, errors.Errorf("the binary operator used is not yet supported")
}
+64 -1
View File
@@ -21,9 +21,12 @@ import (
"github.com/cockroachdb/errors"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/expression"
"github.com/dolthub/go-mysql-server/sql/procedures"
vitess "github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/server/functions/framework"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// BinaryOperator represents a VALUE OPERATOR VALUE expression.
@@ -37,6 +40,7 @@ var _ sql.Expression = (*BinaryOperator)(nil)
var _ expression.BinaryExpression = (*BinaryOperator)(nil)
var _ expression.Equality = (*BinaryOperator)(nil)
var _ sql.IndexComparisonExpression = (*BinaryOperator)(nil)
var _ procedures.InterpreterExpr = (*BinaryOperator)(nil)
// NewBinaryOperator returns a new *BinaryOperator.
func NewBinaryOperator(operator framework.Operator) *BinaryOperator {
@@ -68,6 +72,18 @@ func (b *BinaryOperator) Resolved() bool {
return b.compiledFunc.Resolved()
}
// SetStatementRunner implements the procedures.InterpreterExpr interface.
func (b *BinaryOperator) SetStatementRunner(ctx *sql.Context, runner sql.StatementRunner) sql.Expression {
interpreterExpr, ok := b.compiledFunc.(procedures.InterpreterExpr)
if !ok {
return b
}
return &BinaryOperator{
operator: b.operator,
compiledFunc: interpreterExpr.SetStatementRunner(ctx, runner).(framework.Function),
}
}
// String implements the sql.Expression interface.
func (b *BinaryOperator) String() string {
if b.compiledFunc == nil {
@@ -144,7 +160,21 @@ func (b *BinaryOperator) WithResolvedChildren(ctx context.Context, children []an
return nil, errors.Errorf("expected vitess child to be an expression but has type `%T`", children[1])
}
funcName := "internal_binary_operator_func_" + b.operator.String()
compiledFunc := framework.GetBinaryFunction(b.operator).Compile(sqlCtx, funcName, left, right)
var compiledFunc framework.Function
builtInFunc := framework.GetBinaryFunction(b.operator).Compile(sqlCtx, funcName, left, right)
if builtInFunc != nil && builtInFunc.StashedError() == nil {
compiledFunc = builtInFunc
} else {
userFunc, err := getUserDefinedOperator(sqlCtx, b.operator, left, right)
if err != nil {
return nil, err
}
if userFunc != nil {
compiledFunc = userFunc
} else if builtInFunc != nil {
compiledFunc = builtInFunc
}
}
if compiledFunc == nil {
return nil, errors.Errorf("operator does not exist: %s %s %s",
left.Type(sqlCtx).String(), b.operator.String(), right.Type(sqlCtx).String())
@@ -219,3 +249,36 @@ func unwrapIndexScanTarget(expr sql.Expression) sql.Expression {
}
return expr
}
// getUserDefinedOperator returns the function for the user-defined operator matching the given operands. Returns nil if
// an operator is not found.
func getUserDefinedOperator(ctx *sql.Context, operator framework.Operator, left sql.Expression, right sql.Expression) (framework.Function, error) {
leftType, ok := left.Type(ctx).(*pgtypes.DoltgresType)
if !ok {
return nil, nil
}
rightType, ok := right.Type(ctx).(*pgtypes.DoltgresType)
if !ok {
return nil, nil
}
operatorCollection, err := core.GetOperatorsCollectionFromContext(ctx, "")
if err != nil {
return nil, err
}
searchPath, err := core.SearchPath(ctx)
if err != nil {
return nil, err
}
op, ok, err := operatorCollection.ResolveOperator(ctx, searchPath, operator.String(), leftType.ID, rightType.ID)
if err != nil || !ok {
return nil, err
}
userFunc, err := framework.GetUserFunction(ctx, op.Function.SchemaName(), op.Function.FunctionName(), left, right)
if err != nil {
return nil, err
}
if userFunc == nil {
return nil, sql.ErrFunctionNotFound.New(op.Function.FunctionName())
}
return userFunc, nil
}
+56 -3
View File
@@ -16,6 +16,9 @@ package extdef
import (
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core/casts"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// Function is the Go implementation of a single function that an extension provides.
@@ -35,9 +38,26 @@ type Control struct {
// Extension is a Postgres extension that Doltgres emulates.
type Extension struct {
Name string
Control Control
Routines []Routine
Name string
Control Control
Types []Type
Routines []Routine
Operators []Operator
Casts []Cast
Aggregates []Aggregate
}
// Type is a base type that an extension provides. Definition carries every option except the support functions, which
// are named here.
type Type struct {
Name string
Definition pgtypes.BaseTypeDefinition
Input string
Output string
Receive string
Send string
ModIn string
ModOut string
}
// Routine is a function that an extension provides. Symbol is its C link symbol, which is unique within the extension.
@@ -55,3 +75,36 @@ type Parameter struct {
Name string
Type string
}
// Operator is an operator that an extension provides.
type Operator struct {
Symbol string
Left string
Right string
Routine string
Commutator string
Negator string
Hashes bool
Merges bool
}
// Cast is a cast that an extension provides.
type Cast struct {
Source string
Target string
Routine string
CastType casts.CastType
}
// Aggregate is an aggregate function that an extension provides.
type Aggregate struct {
Name string
Parameters []Parameter
Returns string
StateType string
Transition string
Final string
Combine string
InitCond string
HasInitCond bool
}
+3 -3
View File
@@ -29,11 +29,11 @@ var implementations = map[string]map[string]extdef.Function{}
// Init adds every emulated extension to the registry, making them installable through CREATE EXTENSION.
func Init() {
register(uuid_ossp.Extension())
Register(uuid_ossp.Extension())
}
// register adds the given extension to the registry.
func register(ext *extdef.Extension) {
// Register adds the given extension to the registry.
func Register(ext *extdef.Extension) {
if _, ok := registry[ext.Name]; ok {
panic(errors.Errorf(`extension "%s" has already been registered`, ext.Name))
}
+3 -3
View File
@@ -29,7 +29,7 @@ func testFunction(ctx *sql.Context, args ...any) (any, error) {
}
func TestRegistry(t *testing.T) {
register(&extdef.Extension{
Register(&extdef.Extension{
Name: "doltgres_test",
Control: extdef.Control{DefaultVersion: "2.5", Comment: "a test extension", Relocatable: true},
Routines: []extdef.Routine{{Name: "alpha", Symbol: "alpha", Returns: "uuid", Impl: testFunction}},
@@ -61,11 +61,11 @@ func TestRegistry(t *testing.T) {
// Registering the same extension twice would silently replace the first, so it panics instead
require.Panics(t, func() {
register(&extdef.Extension{Name: "doltgres_test", Control: extdef.Control{DefaultVersion: "2.5"}})
Register(&extdef.Extension{Name: "doltgres_test", Control: extdef.Control{DefaultVersion: "2.5"}})
})
// A symbol may only back one routine, since it is what dispatches a call to its implementation
require.Panics(t, func() {
register(&extdef.Extension{
Register(&extdef.Extension{
Name: "doltgres_test_symbols",
Routines: []extdef.Routine{
{Name: "alpha", Symbol: "alpha", Impl: testFunction},
+13 -1
View File
@@ -58,7 +58,10 @@ func (d dummyExpression) WithChildren(ctx *sql.Context, children ...sql.Expressi
// getQuickFunctionForTypes is used by the types package to load quick functions. This is declared here to work around
// import cycles. Returns nil if a QuickFunction could not be constructed.
func getQuickFunctionForTypes(ctx *sql.Context, functionName string, params []*pgtypes.DoltgresType) any {
func getQuickFunctionForTypes(ctx *sql.Context, schemaName string, functionName string, params []*pgtypes.DoltgresType) any {
if schemaName != "pg_catalog" {
return getQuickFunctionFromProvider(ctx, schemaName, functionName, params)
}
exprs := make([]sql.Expression, len(params))
for i := range params {
exprs[i] = dummyExpression{t: params[i]}
@@ -69,3 +72,12 @@ func getQuickFunctionForTypes(ctx *sql.Context, functionName string, params []*p
}
return cf.GetQuickFunction(ctx)
}
// getQuickFunctionFromProvider resolves a user-defined function. Returns nil if it could not be resolved.
func getQuickFunctionFromProvider(ctx *sql.Context, schemaName string, functionName string, params []*pgtypes.DoltgresType) any {
call := NewUserFunctionCall(ctx, schemaName, functionName, params)
if call == nil {
return nil
}
return &quickUserFunction{call: call}
}
@@ -469,7 +469,11 @@ func (c *CompiledFunction) Eval(ctx *sql.Context, row sql.Row) (interface{}, err
args = c.overload.params.coalesceVariadicValues(args)
// Call the function
return c.callFunction(ctx, args)
}
// callFunction invokes the resolved overload with the given argument values.
func (c *CompiledFunction) callFunction(ctx *sql.Context, args []any) (interface{}, error) {
switch f := c.overload.Function().(type) {
case Function0:
return f.Callable(ctx)
+30
View File
@@ -50,6 +50,12 @@ const (
Operator_BinaryJSONTopLevel // ?
Operator_BinaryJSONTopLevelAny // ?|
Operator_BinaryJSONTopLevelAll // ?&
Operator_BinaryL2Distance // <->
Operator_BinaryL1Distance // <+>
Operator_BinaryCosineDistance // <=>
Operator_BinaryNegInnerProduct // <#>
Operator_BinaryJaccardDistance // <%>
Operator_BinaryHammingDistance // <~>
Operator_UnaryPlus // +
Operator_UnaryMinus // -
// NOTE: Any new operator should also be added to Operator.String() and GetOperatorFromString() functions.
@@ -199,6 +205,18 @@ func (o Operator) String() string {
return "?|"
case Operator_BinaryJSONTopLevelAll:
return "?&"
case Operator_BinaryL2Distance:
return "<->"
case Operator_BinaryL1Distance:
return "<+>"
case Operator_BinaryCosineDistance:
return "<=>"
case Operator_BinaryNegInnerProduct:
return "<#>"
case Operator_BinaryJaccardDistance:
return "<%>"
case Operator_BinaryHammingDistance:
return "<~>"
default:
return "unknown operator"
}
@@ -276,6 +294,18 @@ func GetOperatorFromString(op string) (Operator, error) {
return Operator_BinaryJSONTopLevelAny, nil
case "?&":
return Operator_BinaryJSONTopLevelAll, nil
case "<->":
return Operator_BinaryL2Distance, nil
case "<+>":
return Operator_BinaryL1Distance, nil
case "<=>":
return Operator_BinaryCosineDistance, nil
case "<#>":
return Operator_BinaryNegInnerProduct, nil
case "<%>":
return Operator_BinaryJaccardDistance, nil
case "<~>":
return Operator_BinaryHammingDistance, nil
default:
return 0, errors.Errorf("unhandled Operator `%s`", op)
}
+76 -2
View File
@@ -42,13 +42,21 @@ func (fp *FunctionProvider) Function(ctx *sql.Context, schema, name string) (sql
if err != nil {
return nil, false
}
aggCollection, err := core.GetAggregatesCollectionFromContext(ctx, "")
if err != nil {
return nil, false
}
// TODO: this should search all schemas in the search path, but the search path doesn't handle pg_catalog yet
funcName := id.NewFunction("pg_catalog", name)
overloads, err := funcCollection.GetFunctionOverloads(ctx, funcName)
if err != nil {
return nil, false
}
if len(overloads) == 0 {
aggOverloads, err := aggCollection.GetAggregateOverloads(ctx, funcName)
if err != nil {
return nil, false
}
if len(overloads) == 0 && len(aggOverloads) == 0 {
if schema == "" {
currentSchema, err := core.GetCurrentSchema(ctx)
if err != nil {
@@ -61,7 +69,11 @@ func (fp *FunctionProvider) Function(ctx *sql.Context, schema, name string) (sql
if err != nil {
return nil, false
}
if len(overloads) == 0 {
aggOverloads, err = aggCollection.GetAggregateOverloads(ctx, funcName)
if err != nil {
return nil, false
}
if len(overloads) == 0 && len(aggOverloads) == 0 {
return nil, false
}
}
@@ -124,6 +136,46 @@ func (fp *FunctionProvider) Function(ctx *sql.Context, schema, name string) (sql
}
}
}
for _, aggOverload := range aggOverloads {
stateType, err := typesCollection.GetType(ctx, aggOverload.SType)
if err != nil || stateType == nil {
return nil, false
}
returnType, err := typesCollection.GetType(ctx, aggOverload.ReturnType)
if err != nil || returnType == nil {
return nil, false
}
paramTypes := make([]*pgtypes.DoltgresType, aggOverload.ID.ParameterCount())
for i, param := range aggOverload.ID.Parameters() {
paramTypes[i], err = typesCollection.GetType(ctx, param)
if err != nil || paramTypes[i] == nil {
return nil, false
}
}
if err = overloadTree.Add(UserAggregate{
ID: aggOverload.ID,
ReturnType: returnType,
ParameterTypes: paramTypes,
StateType: stateType,
SFunc: aggOverload.SFunc,
FinalFunc: aggOverload.FinalFunc,
InitCond: aggOverload.InitCond,
HasInitCond: aggOverload.HasInitCond,
}); err != nil {
return nil, false
}
}
if err = addBuiltInOverloads(overloadTree, name); err != nil {
return nil, false
}
if len(aggOverloads) > 0 || len(AggregateCatalog[name]) > 0 {
return sql.FunctionN{
Name: name,
Fn: func(ctx *sql.Context, params ...sql.Expression) (sql.Expression, error) {
return NewCompiledAggregateFunction(ctx, name, params, overloadTree), nil
},
}, true
}
return sql.FunctionN{
Name: name,
Fn: func(ctx *sql.Context, params ...sql.Expression) (sql.Expression, error) {
@@ -131,3 +183,25 @@ func (fp *FunctionProvider) Function(ctx *sql.Context, schema, name string) (sql
},
}, true
}
// addBuiltInOverloads adds the built-in overloads of the given name to the tree, skipping any signature that a
// user-defined overload has already taken.
func addBuiltInOverloads(overloadTree *Overloads, name string) error {
for _, builtIn := range Catalog[name] {
if _, ok := overloadTree.ByParamType[keyForParamTypes(builtIn.GetInputParameterTypes())]; ok {
continue
}
if err := overloadTree.Add(builtIn); err != nil {
return err
}
}
for _, builtIn := range AggregateCatalog[name] {
if _, ok := overloadTree.ByParamType[keyForParamTypes(builtIn.GetInputParameterTypes())]; ok {
continue
}
if err := overloadTree.Add(builtIn); err != nil {
return err
}
}
return nil
}
@@ -439,3 +439,27 @@ func (q *QuickFunction3) WithChildren(ctx *sql.Context, children ...sql.Expressi
// specificFuncImpl implements the interface sql.Expression.
func (*QuickFunction3) specificFuncImpl() {}
// quickUserFunction adapts a user-defined function to the QuickFunction interface.
type quickUserFunction struct {
call *UserFunctionCall
}
var _ pgtypes.QuickFunction = (*quickUserFunction)(nil)
// CallVariadic implements the interface pgtypes.QuickFunction.
func (q *quickUserFunction) CallVariadic(ctx *sql.Context, args ...any) (interface{}, error) {
return q.call.Call(ctx, args...)
}
// ResolvedTypes implements the interface pgtypes.QuickFunction.
func (q *quickUserFunction) ResolvedTypes() []*pgtypes.DoltgresType {
return q.call.compiled.callResolved
}
// WithResolvedTypes implements the interface pgtypes.QuickFunction.
func (q *quickUserFunction) WithResolvedTypes(newTypes []*pgtypes.DoltgresType) any {
newCompiled := *q.call.compiled
newCompiled.callResolved = newTypes
return &quickUserFunction{call: &UserFunctionCall{compiled: &newCompiled, strict: q.call.strict}}
}
@@ -0,0 +1,190 @@
// 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 framework
import (
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core/id"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// UserAggregate is the implementation of a user-defined aggregate.
type UserAggregate struct {
ID id.Function
ReturnType *pgtypes.DoltgresType
ParameterTypes []*pgtypes.DoltgresType
StateType *pgtypes.DoltgresType
SFunc id.Function
FinalFunc id.Function
InitCond string
HasInitCond bool
}
var _ AggregateFunctionInterface = UserAggregate{}
// GetExpectedParameterCount implements the interface FunctionInterface.
func (agg UserAggregate) GetExpectedParameterCount() int {
return len(agg.ParameterTypes)
}
// GetName implements the interface FunctionInterface.
func (agg UserAggregate) GetName() string {
return agg.ID.FunctionName()
}
// GetOutParameters implements the interface FunctionInterface.
func (agg UserAggregate) GetOutParameters() sql.Schema {
return nil
}
// GetInputParameterTypes implements the interface FunctionInterface.
func (agg UserAggregate) GetInputParameterTypes() []*pgtypes.DoltgresType {
return agg.ParameterTypes
}
// GetReturn implements the interface FunctionInterface.
func (agg UserAggregate) GetReturn() *pgtypes.DoltgresType {
return agg.ReturnType
}
// InternalID implements the interface FunctionInterface.
func (agg UserAggregate) InternalID() id.Id {
return agg.ID.AsId()
}
// IsCVariadic implements the interface FunctionInterface.
func (agg UserAggregate) IsCVariadic() bool {
return false
}
// IsSRF implements the interface FunctionInterface.
func (agg UserAggregate) IsSRF() bool {
return false
}
// IsStrict implements the interface FunctionInterface.
func (agg UserAggregate) IsStrict() bool {
return false
}
// NonDeterministic implements the interface FunctionInterface.
func (agg UserAggregate) NonDeterministic() bool {
return false
}
// VariadicIndex implements the interface FunctionInterface.
func (agg UserAggregate) VariadicIndex() int {
return -1
}
// NewBuffer implements the interface AggregateFunctionInterface.
func (agg UserAggregate) NewBuffer(exprs []sql.Expression) (sql.AggregationBuffer, error) {
return &userAggregateBuffer{aggregate: agg, arguments: exprs}, nil
}
// NewWindowFunc implements the interface AggregateFunctionInterface.
func (agg UserAggregate) NewWindowFunc() NewWindowFunctionFn {
//TODO: support user-defined aggregates within an OVER(...) clause
return nil
}
// enforceInterfaceInheritance implements the interface FunctionInterface.
func (agg UserAggregate) enforceInterfaceInheritance(error) {}
// userAggregateBuffer accumulates the transition state of a UserAggregate over the rows of a group.
type userAggregateBuffer struct {
aggregate UserAggregate
arguments []sql.Expression
sFunc *UserFunctionCall
finalFunc *UserFunctionCall
state any
stateExists bool
}
var _ sql.AggregationBuffer = (*userAggregateBuffer)(nil)
// Dispose implements the interface sql.AggregationBuffer.
func (b *userAggregateBuffer) Dispose(ctx *sql.Context) {}
// Eval implements the interface sql.AggregationBuffer.
func (b *userAggregateBuffer) Eval(ctx *sql.Context) (interface{}, error) {
if err := b.resolve(ctx); err != nil {
return nil, err
}
if b.finalFunc == nil {
return b.state, nil
}
return b.finalFunc.Call(ctx, b.state)
}
// Update implements the interface sql.AggregationBuffer.
func (b *userAggregateBuffer) Update(ctx *sql.Context, row sql.Row) error {
if err := b.resolve(ctx); err != nil {
return err
}
args := make([]any, len(b.arguments)+1)
for i, argument := range b.arguments {
val, err := argument.Eval(ctx, row)
if err != nil {
return err
}
if val == nil && b.sFunc.strict {
return nil
}
args[i+1] = val
}
if b.sFunc.strict && !b.stateExists {
b.state = args[1]
b.stateExists = true
return nil
}
args[0] = b.state
newState, err := b.sFunc.Call(ctx, args...)
if err != nil {
return err
}
b.state = newState
b.stateExists = true
return nil
}
// resolve loads the transition and final functions.
func (b *userAggregateBuffer) resolve(ctx *sql.Context) error {
if b.sFunc != nil {
return nil
}
sFuncParams := append([]*pgtypes.DoltgresType{b.aggregate.StateType}, b.aggregate.ParameterTypes...)
b.sFunc = NewUserFunctionCall(ctx, b.aggregate.SFunc.SchemaName(), b.aggregate.SFunc.FunctionName(), sFuncParams)
if b.sFunc == nil {
return ErrFunctionDoesNotExist.New(b.aggregate.SFunc.DisplayString())
}
if b.aggregate.FinalFunc.IsValid() {
b.finalFunc = NewUserFunctionCall(ctx, b.aggregate.FinalFunc.SchemaName(), b.aggregate.FinalFunc.FunctionName(),
[]*pgtypes.DoltgresType{b.aggregate.StateType})
if b.finalFunc == nil {
return ErrFunctionDoesNotExist.New(b.aggregate.FinalFunc.DisplayString())
}
}
if b.aggregate.HasInitCond {
initCond, err := b.aggregate.StateType.IoInput(ctx, b.aggregate.InitCond)
if err != nil {
return err
}
b.state = initCond
b.stateExists = true
}
return nil
}
@@ -0,0 +1,84 @@
// 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 framework
import (
"github.com/cockroachdb/errors"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// UserFunctionCall is a resolved call to a user-defined function that takes argument values rather than argument
// expressions.
type UserFunctionCall struct {
compiled *CompiledFunction
strict bool
}
// GetUserFunction returns the compiled call to the named function with the given argument expressions. Returns nil if
// the function does not exist.
func GetUserFunction(ctx *sql.Context, schemaName string, functionName string, args ...sql.Expression) (Function, error) {
sqlFunc, ok := (&FunctionProvider{}).Function(ctx, schemaName, functionName)
if !ok {
return nil, nil
}
functionN, ok := sqlFunc.(sql.FunctionN)
if !ok {
return nil, nil
}
expr, err := functionN.Fn(ctx, args...)
if err != nil {
return nil, err
}
f, ok := expr.(Function)
if !ok {
return nil, errors.Errorf("function `%s` has an unexpected type: %T", functionName, expr)
}
return f, nil
}
// NewUserFunctionCall resolves the named function for the given parameter types. Returns nil if no overload matches.
func NewUserFunctionCall(ctx *sql.Context, schemaName string, functionName string, paramTypes []*pgtypes.DoltgresType) *UserFunctionCall {
exprs := make([]sql.Expression, len(paramTypes))
for i := range paramTypes {
exprs[i] = dummyExpression{t: paramTypes[i]}
}
f, err := GetUserFunction(ctx, schemaName, functionName, exprs...)
if err != nil {
return nil
}
compiled, ok := f.(*CompiledFunction)
if !ok || !compiled.Resolved() || !compiled.overload.Valid() {
return nil
}
if runner, err := core.GetRunnerFromContext(ctx); err == nil && runner != nil {
compiled = compiled.SetStatementRunner(ctx, runner).(*CompiledFunction)
}
return &UserFunctionCall{compiled: compiled, strict: compiled.IsStrict()}
}
// Call invokes the function with the given argument values.
func (u *UserFunctionCall) Call(ctx *sql.Context, args ...any) (any, error) {
if u.strict {
for _, arg := range args {
if arg == nil {
return nil, nil
}
}
}
return u.compiled.callFunction(ctx, args)
}
+226
View File
@@ -0,0 +1,226 @@
// 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 node
import (
"context"
"fmt"
"strings"
"github.com/cockroachdb/errors"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/plan"
vitess "github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/core/aggregates"
"github.com/dolthub/doltgresql/core/functions"
"github.com/dolthub/doltgresql/core/id"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// CreateAggregate implements CREATE AGGREGATE.
type CreateAggregate struct {
SchemaName string
Name string
Replace bool
ArgTypes []*pgtypes.DoltgresType
SType *pgtypes.DoltgresType
SFuncSchema string
SFuncName string
FinalFuncSchema string
FinalFuncName string
CombineFuncSchema string
CombineFuncName string
InitCond string
HasInitCond bool
}
var _ sql.ExecSourceRel = (*CreateAggregate)(nil)
var _ vitess.Injectable = (*CreateAggregate)(nil)
// NewCreateAggregate returns a new *CreateAggregate.
func NewCreateAggregate(
schemaName, name string,
replace bool,
argTypes []*pgtypes.DoltgresType,
sType *pgtypes.DoltgresType,
sFuncSchema, sFuncName string,
finalFuncSchema, finalFuncName string,
combineFuncSchema, combineFuncName string,
initCond string,
hasInitCond bool) *CreateAggregate {
return &CreateAggregate{
SchemaName: schemaName,
Name: name,
Replace: replace,
ArgTypes: argTypes,
SType: sType,
SFuncSchema: sFuncSchema,
SFuncName: sFuncName,
FinalFuncSchema: finalFuncSchema,
FinalFuncName: finalFuncName,
CombineFuncSchema: combineFuncSchema,
CombineFuncName: combineFuncName,
InitCond: initCond,
HasInitCond: hasInitCond,
}
}
// Children implements the interface sql.ExecSourceRel.
func (c *CreateAggregate) Children() []sql.Node {
return nil
}
// IsReadOnly implements the interface sql.ExecSourceRel.
func (c *CreateAggregate) IsReadOnly() bool {
return false
}
// Resolved implements the interface sql.ExecSourceRel.
func (c *CreateAggregate) Resolved() bool {
return true
}
// RowIter implements the interface sql.ExecSourceRel.
func (c *CreateAggregate) RowIter(ctx *sql.Context, r sql.Row) (sql.RowIter, error) {
funcCollection, err := core.GetFunctionsCollectionFromContext(ctx, "")
if err != nil {
return nil, err
}
argIDs := make([]id.Type, len(c.ArgTypes))
argNames := make([]string, len(c.ArgTypes))
for i, argType := range c.ArgTypes {
argIDs[i] = argType.ID
argNames[i] = argType.String()
}
sFuncID, err := c.lookupFunction(ctx, funcCollection, c.SFuncSchema, c.SFuncName,
append([]id.Type{c.SType.ID}, argIDs...), append([]string{c.SType.String()}, argNames...))
if err != nil {
return nil, err
}
sFunc, err := funcCollection.GetFunction(ctx, sFuncID)
if err != nil {
return nil, err
}
if sFunc.ReturnType != c.SType.ID {
return nil, errors.Errorf("return type of transition function %s is not %s", c.SFuncName, c.SType.String())
}
returnType := c.SType.ID
finalFuncID := id.NullFunction
if c.FinalFuncName != "" {
finalFuncID, err = c.lookupFunction(ctx, funcCollection, c.FinalFuncSchema, c.FinalFuncName,
[]id.Type{c.SType.ID}, []string{c.SType.String()})
if err != nil {
return nil, err
}
finalFunc, err := funcCollection.GetFunction(ctx, finalFuncID)
if err != nil {
return nil, err
}
returnType = finalFunc.ReturnType
}
combineFuncID := id.NullFunction
if c.CombineFuncName != "" {
combineFuncID, err = c.lookupFunction(ctx, funcCollection, c.CombineFuncSchema, c.CombineFuncName,
[]id.Type{c.SType.ID, c.SType.ID}, []string{c.SType.String(), c.SType.String()})
if err != nil {
return nil, err
}
combineFunc, err := funcCollection.GetFunction(ctx, combineFuncID)
if err != nil {
return nil, err
}
if combineFunc.ReturnType != c.SType.ID {
return nil, errors.Errorf("return type of combine function %s is not %s", c.CombineFuncName, c.SType.String())
}
}
schemaName, err := core.GetSchemaName(ctx, nil, c.SchemaName)
if err != nil {
return nil, err
}
aggCollection, err := core.GetAggregatesCollectionFromContext(ctx, "")
if err != nil {
return nil, err
}
aggregateID := id.NewFunction(schemaName, c.Name, argIDs...)
if funcCollection.HasFunction(ctx, aggregateID) {
return nil, errors.Errorf(`function "%s" already exists with same argument types`, c.Name)
}
if aggCollection.HasAggregate(ctx, aggregateID) {
if !c.Replace {
return nil, errors.Errorf(`function "%s" already exists with same argument types`, c.Name)
}
if err = aggCollection.DropAggregate(ctx, aggregateID); err != nil {
return nil, err
}
}
err = aggCollection.AddAggregate(ctx, aggregates.Aggregate{
ID: aggregateID,
ReturnType: returnType,
SFunc: sFuncID,
SType: c.SType.ID,
FinalFunc: finalFuncID,
CombineFunc: combineFuncID,
InitCond: c.InitCond,
HasInitCond: c.HasInitCond,
})
if err != nil {
return nil, err
}
return sql.RowsToRowIter(), nil
}
// lookupFunction returns the ID of the function with the given name and parameter types. Returns an error if no
// function was found.
func (c *CreateAggregate) lookupFunction(
ctx *sql.Context,
funcCollection *functions.Collection,
schema string, name string,
paramTypes []id.Type,
paramNames []string) (id.Function, error) {
funcSchema, err := core.GetSchemaName(ctx, nil, schema)
if err != nil {
return id.NullFunction, err
}
funcID := id.NewFunction(funcSchema, name, paramTypes...)
if !funcCollection.HasFunction(ctx, funcID) {
return id.NullFunction, errors.Errorf("function %s(%s) does not exist", name, strings.Join(paramNames, ", "))
}
return funcID, nil
}
// Schema implements the interface sql.ExecSourceRel.
func (c *CreateAggregate) Schema(ctx *sql.Context) sql.Schema {
return nil
}
// String implements the interface sql.ExecSourceRel.
func (c *CreateAggregate) String() string {
return fmt.Sprintf("CREATE AGGREGATE %s", c.Name)
}
// WithChildren implements the interface sql.ExecSourceRel.
func (c *CreateAggregate) WithChildren(ctx *sql.Context, children ...sql.Node) (sql.Node, error) {
return plan.NillaryWithChildren(c, children...)
}
// WithResolvedChildren implements the interface vitess.Injectable.
func (c *CreateAggregate) WithResolvedChildren(ctx context.Context, children []any) (any, error) {
if len(children) != 0 {
return nil, ErrVitessChildCount.New(0, len(children))
}
return c, nil
}
+1 -1
View File
@@ -139,7 +139,7 @@ func (c *CreateDomain) Schema(ctx *sql.Context) sql.Schema {
// String implements the interface sql.ExecSourceRel.
func (c *CreateDomain) String() string {
return "CREATE DOMAIN"
return fmt.Sprintf("CREATE DOMAIN %s", c.Name)
}
// WithChildren implements the interface sql.ExecSourceRel.
+243 -9
View File
@@ -16,6 +16,7 @@ package node
import (
"context"
"fmt"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
@@ -24,9 +25,12 @@ import (
vitess "github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/core/aggregates"
"github.com/dolthub/doltgresql/core/casts"
coreextensions "github.com/dolthub/doltgresql/core/extensions"
"github.com/dolthub/doltgresql/core/functions"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/operators"
"github.com/dolthub/doltgresql/core/procedures"
"github.com/dolthub/doltgresql/core/typecollection"
"github.com/dolthub/doltgresql/server/extensions"
@@ -98,7 +102,7 @@ func (c *CreateExtension) RowIter(ctx *sql.Context, r sql.Row) (sql.RowIter, err
if err != nil {
return nil, err
}
if err = (extensionObjects{ext: ext, schemaName: schemaName}).materialize(ctx, typColl); err != nil {
if err = (extensionObjects{ext: ext, schemaName: schemaName, typColl: typColl}).materialize(ctx); err != nil {
return nil, err
}
err = extCollection.AddLoadedExtension(ctx, coreextensions.Extension{
@@ -120,7 +124,7 @@ func (c *CreateExtension) Schema(ctx *sql.Context) sql.Schema {
// String implements the interface sql.ExecSourceRel.
func (c *CreateExtension) String() string {
return "CREATE EXTENSION"
return fmt.Sprintf("CREATE EXTENSION %s", c.Name)
}
// WithChildren implements the interface sql.ExecSourceRel.
@@ -140,10 +144,62 @@ func (c *CreateExtension) WithResolvedChildren(ctx context.Context, children []a
type extensionObjects struct {
ext *extdef.Extension
schemaName string
typColl *typecollection.TypeCollection
}
// materialize writes every object that the extension declares.
func (e extensionObjects) materialize(ctx *sql.Context, typColl *typecollection.TypeCollection) error {
func (e extensionObjects) materialize(ctx *sql.Context) error {
if err := e.materializeTypes(ctx); err != nil {
return err
}
if err := e.materializeRoutines(ctx); err != nil {
return err
}
if err := e.materializeOperators(ctx); err != nil {
return err
}
if err := e.materializeCasts(ctx); err != nil {
return err
}
return e.materializeAggregates(ctx)
}
// materializeTypes writes the declared types into the types collection.
func (e extensionObjects) materializeTypes(ctx *sql.Context) error {
for _, declared := range e.ext.Types {
var err error
def := declared.Definition
if def.InputFunc, err = e.supportFuncID(ctx, declared.Input); err != nil {
return err
}
if def.OutputFunc, err = e.supportFuncID(ctx, declared.Output); err != nil {
return err
}
if def.ReceiveFunc, err = e.supportFuncID(ctx, declared.Receive); err != nil {
return err
}
if def.SendFunc, err = e.supportFuncID(ctx, declared.Send); err != nil {
return err
}
if def.ModInFunc, err = e.supportFuncID(ctx, declared.ModIn); err != nil {
return err
}
if def.ModOutFunc, err = e.supportFuncID(ctx, declared.ModOut); err != nil {
return err
}
newType := types.NewBaseType(ctx, id.NewType(e.schemaName, declared.Name), def)
if err = e.typColl.CreateType(ctx, newType); err != nil {
return err
}
if err = e.typColl.CreateType(ctx, types.CreateArrayTypeFromBaseType(newType)); err != nil {
return err
}
}
return nil
}
// materializeRoutines writes the declared routines into the functions collection.
func (e extensionObjects) materializeRoutines(ctx *sql.Context) error {
if len(e.ext.Routines) == 0 {
return nil
}
@@ -152,11 +208,11 @@ func (e extensionObjects) materialize(ctx *sql.Context, typColl *typecollection.
return err
}
for _, routine := range e.ext.Routines {
returnType, err := e.typeID(ctx, typColl, routine.Returns)
returnType, err := e.typeID(ctx, routine.Returns)
if err != nil {
return err
}
paramTypes, err := e.parameterTypes(ctx, typColl, routine.Parameters)
paramTypes, err := e.parameterTypes(ctx, routine.Parameters)
if err != nil {
return err
}
@@ -180,9 +236,146 @@ func (e extensionObjects) materialize(ctx *sql.Context, typColl *typecollection.
return nil
}
// materializeOperators writes the declared operators into the operators collection.
func (e extensionObjects) materializeOperators(ctx *sql.Context) error {
if len(e.ext.Operators) == 0 {
return nil
}
opCollection, err := core.GetOperatorsCollectionFromContext(ctx, "")
if err != nil {
return err
}
for _, declared := range e.ext.Operators {
leftType, err := e.typeID(ctx, declared.Left)
if err != nil {
return err
}
rightType, err := e.typeID(ctx, declared.Right)
if err != nil {
return err
}
routine, err := e.routine(declared.Routine)
if err != nil {
return err
}
returnType, err := e.typeID(ctx, routine.Returns)
if err != nil {
return err
}
funcID, err := e.routineID(ctx, routine)
if err != nil {
return err
}
err = opCollection.AddOperator(ctx, operators.Operator{
ID: id.NewOperator(e.schemaName, declared.Symbol, leftType, rightType),
Function: funcID,
ReturnType: returnType,
Commutator: declared.Commutator,
Negator: declared.Negator,
Hashes: declared.Hashes,
Merges: declared.Merges,
})
if err != nil {
return err
}
}
return nil
}
// materializeCasts writes the declared casts into the casts collection.
func (e extensionObjects) materializeCasts(ctx *sql.Context) error {
if len(e.ext.Casts) == 0 {
return nil
}
castCollection, err := core.GetCastsCollectionFromContext(ctx, "")
if err != nil {
return err
}
for _, declared := range e.ext.Casts {
sourceType, err := e.typeID(ctx, declared.Source)
if err != nil {
return err
}
targetType, err := e.typeID(ctx, declared.Target)
if err != nil {
return err
}
funcID, err := e.optionalRoutineID(ctx, declared.Routine)
if err != nil {
return err
}
err = castCollection.AddCast(ctx, casts.Cast{
ID: id.NewCast(sourceType, targetType),
CastType: declared.CastType,
Function: funcID,
UseInOut: !funcID.IsValid(),
})
if err != nil {
return err
}
}
return nil
}
// materializeAggregates writes the declared aggregates into the aggregates collection.
func (e extensionObjects) materializeAggregates(ctx *sql.Context) error {
if len(e.ext.Aggregates) == 0 {
return nil
}
aggCollection, err := core.GetAggregatesCollectionFromContext(ctx, "")
if err != nil {
return err
}
for _, declared := range e.ext.Aggregates {
returnType, err := e.typeID(ctx, declared.Returns)
if err != nil {
return err
}
stateType, err := e.typeID(ctx, declared.StateType)
if err != nil {
return err
}
paramTypes, err := e.parameterTypes(ctx, declared.Parameters)
if err != nil {
return err
}
transitionFunc, err := e.optionalRoutineID(ctx, declared.Transition)
if err != nil {
return err
}
finalFunc, err := e.optionalRoutineID(ctx, declared.Final)
if err != nil {
return err
}
combineFunc, err := e.optionalRoutineID(ctx, declared.Combine)
if err != nil {
return err
}
err = aggCollection.AddAggregate(ctx, aggregates.Aggregate{
ID: id.NewFunction(e.schemaName, declared.Name, paramTypes...),
ReturnType: returnType,
SFunc: transitionFunc,
SType: stateType,
FinalFunc: finalFunc,
CombineFunc: combineFunc,
InitCond: declared.InitCond,
HasInitCond: declared.HasInitCond,
})
if err != nil {
return err
}
}
return nil
}
// typeID returns the type ID matching the given name.
func (e extensionObjects) typeID(ctx *sql.Context, typColl *typecollection.TypeCollection, name string) (id.Type, error) {
_, typeID, err := typColl.ResolveName(ctx, doltdb.TableName{Name: name})
func (e extensionObjects) typeID(ctx *sql.Context, name string) (id.Type, error) {
for _, declared := range e.ext.Types {
if declared.Name == name {
return id.NewType(e.schemaName, name), nil
}
}
_, typeID, err := e.typColl.ResolveName(ctx, doltdb.TableName{Name: name})
if err != nil {
return id.NullType, err
}
@@ -193,10 +386,10 @@ func (e extensionObjects) typeID(ctx *sql.Context, typColl *typecollection.TypeC
}
// parameterTypes returns the type IDs of the given parameters.
func (e extensionObjects) parameterTypes(ctx *sql.Context, typColl *typecollection.TypeCollection, params []extdef.Parameter) ([]id.Type, error) {
func (e extensionObjects) parameterTypes(ctx *sql.Context, params []extdef.Parameter) ([]id.Type, error) {
paramTypes := make([]id.Type, len(params))
for i, param := range params {
paramType, err := e.typeID(ctx, typColl, param.Type)
paramType, err := e.typeID(ctx, param.Type)
if err != nil {
return nil, err
}
@@ -204,3 +397,44 @@ func (e extensionObjects) parameterTypes(ctx *sql.Context, typColl *typecollecti
}
return paramTypes, nil
}
// routine returns the routine that the extension declares under the given symbol.
func (e extensionObjects) routine(symbol string) (extdef.Routine, error) {
for _, routine := range e.ext.Routines {
if routine.Symbol == symbol {
return routine, nil
}
}
return extdef.Routine{}, errors.Errorf(`extension "%s" does not declare the function "%s"`, e.ext.Name, symbol)
}
// routineID returns the ID that the given routine is materialized under.
func (e extensionObjects) routineID(ctx *sql.Context, routine extdef.Routine) (id.Function, error) {
paramTypes, err := e.parameterTypes(ctx, routine.Parameters)
if err != nil {
return id.NullFunction, err
}
return id.NewFunction(e.schemaName, routine.Name, paramTypes...), nil
}
// optionalRoutineID returns the ID of the routine with the given symbol, or a null ID when the declaration omitted it.
func (e extensionObjects) optionalRoutineID(ctx *sql.Context, symbol string) (id.Function, error) {
if len(symbol) == 0 {
return id.NullFunction, nil
}
routine, err := e.routine(symbol)
if err != nil {
return id.NullFunction, err
}
return e.routineID(ctx, routine)
}
// supportFuncID returns the function registry ID of the routine with the given symbol, or zero when the type omitted
// it.
func (e extensionObjects) supportFuncID(ctx *sql.Context, symbol string) (uint32, error) {
funcID, err := e.optionalRoutineID(ctx, symbol)
if err != nil || !funcID.IsValid() {
return 0, err
}
return types.ToFuncID(funcID), nil
}
+1 -1
View File
@@ -178,7 +178,7 @@ func (c *CreateFunction) Schema(ctx *sql.Context) sql.Schema {
// String implements the interface sql.ExecSourceRel.
func (c *CreateFunction) String() string {
// TODO: fully implement this
return "CREATE FUNCTION"
return fmt.Sprintf("CREATE FUNCTION %s", c.FunctionName)
}
// WithChildren implements the interface sql.ExecSourceRel.
+234
View File
@@ -0,0 +1,234 @@
// 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 node
import (
"context"
"fmt"
"strings"
"github.com/cockroachdb/errors"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/plan"
vitess "github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/operators"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// CreateOperator implements CREATE OPERATOR.
type CreateOperator struct {
Symbol string
Left *pgtypes.DoltgresType // This is nil for prefix operators
Right *pgtypes.DoltgresType
FuncSchema string
FuncName string
Commutator string
Negator string
Hashes bool
Merges bool
}
var _ sql.ExecSourceRel = (*CreateOperator)(nil)
var _ vitess.Injectable = (*CreateOperator)(nil)
// NewCreateOperator returns a new *CreateOperator.
func NewCreateOperator(
symbol string,
left, right *pgtypes.DoltgresType,
funcSchema, funcName string,
commutator, negator string,
hashes, merges bool) *CreateOperator {
return &CreateOperator{
Symbol: symbol,
Left: left,
Right: right,
FuncSchema: funcSchema,
FuncName: funcName,
Commutator: commutator,
Negator: negator,
Hashes: hashes,
Merges: merges,
}
}
// Children implements the interface sql.ExecSourceRel.
func (c *CreateOperator) Children() []sql.Node {
return nil
}
// IsReadOnly implements the interface sql.ExecSourceRel.
func (c *CreateOperator) IsReadOnly() bool {
return false
}
// Resolved implements the interface sql.ExecSourceRel.
func (c *CreateOperator) Resolved() bool {
return true
}
// RowIter implements the interface sql.ExecSourceRel.
func (c *CreateOperator) RowIter(ctx *sql.Context, r sql.Row) (sql.RowIter, error) {
if c.Left == nil {
if c.Commutator != "" {
return nil, errors.New("only binary operators can have commutators")
}
if c.Hashes {
return nil, errors.New("only binary operators can hash")
}
if c.Merges {
return nil, errors.New("only binary operators can merge join")
}
}
funcSchema, err := core.GetSchemaName(ctx, nil, c.FuncSchema)
if err != nil {
return nil, err
}
var paramTypes []id.Type
var paramNames []string
if c.Left != nil {
paramTypes = append(paramTypes, c.Left.ID)
paramNames = append(paramNames, c.Left.String())
}
paramTypes = append(paramTypes, c.Right.ID)
paramNames = append(paramNames, c.Right.String())
funcID := id.NewFunction(funcSchema, c.FuncName, paramTypes...)
funcCollection, err := core.GetFunctionsCollectionFromContext(ctx, "")
if err != nil {
return nil, err
}
if !funcCollection.HasFunction(ctx, funcID) {
return nil, errors.Errorf("function %s(%s) does not exist", c.FuncName, strings.Join(paramNames, ", "))
}
f, err := funcCollection.GetFunction(ctx, funcID)
if err != nil {
return nil, err
}
if f.ReturnType != pgtypes.Bool.ID {
if c.Negator != "" {
return nil, errors.New("only boolean operators can have negators")
}
if c.Hashes {
return nil, errors.New("only boolean operators can hash")
}
if c.Merges {
return nil, errors.New("only boolean operators can merge join")
}
}
schemaName, err := core.GetSchemaName(ctx, nil, "")
if err != nil {
return nil, err
}
leftTypeID := id.NullType
if c.Left != nil {
leftTypeID = c.Left.ID
}
opCollection, err := core.GetOperatorsCollectionFromContext(ctx, "")
if err != nil {
return nil, err
}
operatorID := id.NewOperator(schemaName, c.Symbol, leftTypeID, c.Right.ID)
if opCollection.HasOperator(ctx, operatorID) {
return nil, errors.Errorf("operator %s already exists", c.Symbol)
}
err = opCollection.AddOperator(ctx, operators.Operator{
ID: operatorID,
Function: funcID,
ReturnType: f.ReturnType,
Commutator: c.Commutator,
Negator: c.Negator,
Hashes: c.Hashes,
Merges: c.Merges,
})
if err != nil {
return nil, err
}
if c.Commutator != "" {
commutatorID := id.NewOperator(schemaName, c.Commutator, c.Right.ID, leftTypeID)
if commutatorID != operatorID {
if err = setCommutatorBackLink(ctx, opCollection, commutatorID, c.Symbol); err != nil {
return nil, err
}
}
}
if c.Negator != "" {
negatorID := id.NewOperator(schemaName, c.Negator, leftTypeID, c.Right.ID)
if negatorID != operatorID {
if err = setNegatorBackLink(ctx, opCollection, negatorID, c.Symbol); err != nil {
return nil, err
}
}
}
return sql.RowsToRowIter(), nil
}
// setCommutatorBackLink sets the commutator of the given operator to the given symbol if the operator exists and has
// no commutator.
func setCommutatorBackLink(ctx *sql.Context, opCollection *operators.Collection, operatorID id.Operator, symbol string) error {
if !opCollection.HasOperator(ctx, operatorID) {
return nil
}
op, err := opCollection.GetOperator(ctx, operatorID)
if err != nil || op.Commutator != "" {
return err
}
op.Commutator = symbol
if err = opCollection.DropOperator(ctx, operatorID); err != nil {
return err
}
return opCollection.AddOperator(ctx, op)
}
// setNegatorBackLink sets the negator of the given operator to the given symbol if the operator exists and has no
// negator.
func setNegatorBackLink(ctx *sql.Context, opCollection *operators.Collection, operatorID id.Operator, symbol string) error {
if !opCollection.HasOperator(ctx, operatorID) {
return nil
}
op, err := opCollection.GetOperator(ctx, operatorID)
if err != nil || op.Negator != "" {
return err
}
op.Negator = symbol
if err = opCollection.DropOperator(ctx, operatorID); err != nil {
return err
}
return opCollection.AddOperator(ctx, op)
}
// Schema implements the interface sql.ExecSourceRel.
func (c *CreateOperator) Schema(ctx *sql.Context) sql.Schema {
return nil
}
// String implements the interface sql.ExecSourceRel.
func (c *CreateOperator) String() string {
return fmt.Sprintf("CREATE OPERATOR %s", c.Symbol)
}
// WithChildren implements the interface sql.ExecSourceRel.
func (c *CreateOperator) WithChildren(ctx *sql.Context, children ...sql.Node) (sql.Node, error) {
return plan.NillaryWithChildren(c, children...)
}
// WithResolvedChildren implements the interface vitess.Injectable.
func (c *CreateOperator) WithResolvedChildren(ctx context.Context, children []any) (any, error) {
if len(children) != 0 {
return nil, ErrVitessChildCount.New(0, len(children))
}
return c, nil
}
+2 -1
View File
@@ -16,6 +16,7 @@ package node
import (
"context"
"fmt"
"github.com/cockroachdb/errors"
"github.com/dolthub/go-mysql-server/sql"
@@ -152,7 +153,7 @@ func (c *CreateProcedure) Schema(ctx *sql.Context) sql.Schema {
// String implements the interface sql.ExecSourceRel.
func (c *CreateProcedure) String() string {
return "CREATE PROCEDURE"
return fmt.Sprintf("CREATE PROCEDURE %s", c.ProcedureName)
}
// WithChildren implements the interface sql.ExecSourceRel.
+2 -1
View File
@@ -16,6 +16,7 @@ package node
import (
"context"
"fmt"
"github.com/cockroachdb/errors"
"github.com/dolthub/go-mysql-server/sql"
@@ -148,7 +149,7 @@ func (c *CreateTrigger) Schema(ctx *sql.Context) sql.Schema {
// String implements the interface sql.ExecSourceRel.
func (c *CreateTrigger) String() string {
return "CREATE TRIGGER"
return fmt.Sprintf("CREATE TRIGGER %s", c.Name)
}
// WithChildren implements the interface sql.ExecSourceRel.
+2 -1
View File
@@ -16,6 +16,7 @@ package node
import (
"context"
"fmt"
"github.com/cockroachdb/errors"
"github.com/dolthub/go-mysql-server/sql"
@@ -167,7 +168,7 @@ func (c *CreateType) Schema(ctx *sql.Context) sql.Schema {
// String implements the interface sql.ExecSourceRel.
func (c *CreateType) String() string {
return "CREATE TYPE"
return fmt.Sprintf("CREATE TYPE %s", c.Name)
}
// WithChildren implements the interface sql.ExecSourceRel.
+129
View File
@@ -0,0 +1,129 @@
// 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 node
import (
"context"
"strings"
"github.com/cockroachdb/errors"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/plan"
vitess "github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/core/id"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// AggregateToDrop represents an aggregate in DROP AGGREGATE.
type AggregateToDrop struct {
SchemaName string
Name string
ArgTypes []*pgtypes.DoltgresType
}
// DropAggregate implements DROP AGGREGATE.
type DropAggregate struct {
Aggregates []*AggregateToDrop
IfExists bool
}
var _ sql.ExecSourceRel = (*DropAggregate)(nil)
var _ vitess.Injectable = (*DropAggregate)(nil)
// NewDropAggregate returns a new *DropAggregate.
func NewDropAggregate(ifExists bool, aggregates []*AggregateToDrop) *DropAggregate {
return &DropAggregate{
IfExists: ifExists,
Aggregates: aggregates,
}
}
// Children implements the interface sql.ExecSourceRel.
func (d *DropAggregate) Children() []sql.Node {
return nil
}
// IsReadOnly implements the interface sql.ExecSourceRel.
func (d *DropAggregate) IsReadOnly() bool {
return false
}
// Resolved implements the interface sql.ExecSourceRel.
func (d *DropAggregate) Resolved() bool {
return true
}
// RowIter implements the interface sql.ExecSourceRel.
func (d *DropAggregate) RowIter(ctx *sql.Context, r sql.Row) (sql.RowIter, error) {
aggCollection, err := core.GetAggregatesCollectionFromContext(ctx, "")
if err != nil {
return nil, err
}
funcCollection, err := core.GetFunctionsCollectionFromContext(ctx, "")
if err != nil {
return nil, err
}
for _, agg := range d.Aggregates {
schemaName, err := core.GetSchemaName(ctx, nil, agg.SchemaName)
if err != nil {
return nil, err
}
argIDs := make([]id.Type, len(agg.ArgTypes))
argNames := make([]string, len(agg.ArgTypes))
for i, argType := range agg.ArgTypes {
argIDs[i] = argType.ID
argNames[i] = argType.String()
}
aggregateID := id.NewFunction(schemaName, agg.Name, argIDs...)
if !aggCollection.HasAggregate(ctx, aggregateID) {
if funcCollection.HasFunction(ctx, aggregateID) {
return nil, errors.Errorf("function %s(%s) is not an aggregate", agg.Name, strings.Join(argNames, ", "))
}
if d.IfExists {
continue
}
return nil, errors.Errorf("aggregate %s(%s) does not exist", agg.Name, strings.Join(argNames, ", "))
}
if err = aggCollection.DropAggregate(ctx, aggregateID); err != nil {
return nil, err
}
}
return sql.RowsToRowIter(), nil
}
// Schema implements the interface sql.ExecSourceRel.
func (d *DropAggregate) Schema(ctx *sql.Context) sql.Schema {
return nil
}
// String implements the interface sql.ExecSourceRel.
func (d *DropAggregate) String() string {
return "DROP AGGREGATE"
}
// WithChildren implements the interface sql.ExecSourceRel.
func (d *DropAggregate) WithChildren(ctx *sql.Context, children ...sql.Node) (sql.Node, error) {
return plan.NillaryWithChildren(d, children...)
}
// WithResolvedChildren implements the interface vitess.Injectable.
func (d *DropAggregate) WithResolvedChildren(ctx context.Context, children []any) (any, error) {
if len(children) != 0 {
return nil, ErrVitessChildCount.New(0, len(children))
}
return d, nil
}
+125
View File
@@ -0,0 +1,125 @@
// 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 node
import (
"context"
"github.com/cockroachdb/errors"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/plan"
vitess "github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/core/id"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// OperatorToDrop represents an operator in DROP OPERATOR.
type OperatorToDrop struct {
Symbol string
Left *pgtypes.DoltgresType // This is nil for prefix operators
Right *pgtypes.DoltgresType
}
// DropOperator implements DROP OPERATOR.
type DropOperator struct {
Operators []*OperatorToDrop
IfExists bool
}
var _ sql.ExecSourceRel = (*DropOperator)(nil)
var _ vitess.Injectable = (*DropOperator)(nil)
// NewDropOperator returns a new *DropOperator.
func NewDropOperator(ifExists bool, operators []*OperatorToDrop) *DropOperator {
return &DropOperator{
IfExists: ifExists,
Operators: operators,
}
}
// Children implements the interface sql.ExecSourceRel.
func (d *DropOperator) Children() []sql.Node {
return nil
}
// IsReadOnly implements the interface sql.ExecSourceRel.
func (d *DropOperator) IsReadOnly() bool {
return false
}
// Resolved implements the interface sql.ExecSourceRel.
func (d *DropOperator) Resolved() bool {
return true
}
// RowIter implements the interface sql.ExecSourceRel.
func (d *DropOperator) RowIter(ctx *sql.Context, r sql.Row) (sql.RowIter, error) {
opCollection, err := core.GetOperatorsCollectionFromContext(ctx, "")
if err != nil {
return nil, err
}
searchPath, err := core.SearchPath(ctx)
if err != nil {
return nil, err
}
for _, op := range d.Operators {
leftTypeID := id.NullType
if op.Left != nil {
leftTypeID = op.Left.ID
}
operator, found, err := opCollection.ResolveOperator(ctx, searchPath, op.Symbol, leftTypeID, op.Right.ID)
if err != nil {
return nil, err
}
if !found {
if d.IfExists {
continue
}
if op.Left == nil {
return nil, errors.Errorf("operator does not exist: %s %s", op.Symbol, op.Right.String())
}
return nil, errors.Errorf("operator does not exist: %s %s %s", op.Left.String(), op.Symbol, op.Right.String())
}
if err = opCollection.DropOperator(ctx, operator.ID); err != nil {
return nil, err
}
}
return sql.RowsToRowIter(), nil
}
// Schema implements the interface sql.ExecSourceRel.
func (d *DropOperator) Schema(ctx *sql.Context) sql.Schema {
return nil
}
// String implements the interface sql.ExecSourceRel.
func (d *DropOperator) String() string {
return "DROP OPERATOR"
}
// WithChildren implements the interface sql.ExecSourceRel.
func (d *DropOperator) WithChildren(ctx *sql.Context, children ...sql.Node) (sql.Node, error) {
return plan.NillaryWithChildren(d, children...)
}
// WithResolvedChildren implements the interface vitess.Injectable.
func (d *DropOperator) WithResolvedChildren(ctx context.Context, children []any) (any, error) {
if len(children) != 0 {
return nil, ErrVitessChildCount.New(0, len(children))
}
return d, nil
}
+53 -5
View File
@@ -19,6 +19,9 @@ import (
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/core/aggregates"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/server/tables"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
@@ -43,9 +46,20 @@ func (p PgAggregateHandler) Name() string {
// RowIter implements the interface tables.Handler.
func (p PgAggregateHandler) RowIter(ctx *sql.Context, partition sql.Partition) (sql.RowIter, error) {
// pg_aggregate is currently empty, since built-in aggregate functions do not yet have pg_proc entries.
// TODO: fill this in alongside built-in function entries in pg_proc
return emptyRowIter()
aggregateCollection, err := core.GetAggregatesCollectionFromContext(ctx, "")
if err != nil {
return nil, err
}
var aggs []aggregates.Aggregate
err = aggregateCollection.IterateAggregates(ctx, func(a aggregates.Aggregate) (stop bool, err error) {
aggs = append(aggs, a)
return false, nil
})
if err != nil {
return nil, err
}
return &pgAggregateRowIter{aggs: aggs}, nil
}
// PkSchema implements the interface tables.Handler.
@@ -71,8 +85,8 @@ var pgAggregateSchema = sql.Schema{
{Name: "aggmfinalfn", Type: pgtypes.Regproc, Default: nil, Nullable: false, Source: PgAggregateName},
{Name: "aggfinalextra", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgAggregateName},
{Name: "aggmfinalextra", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgAggregateName},
{Name: "aggfinalmodify", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgAggregateName},
{Name: "aggmfinalmodify", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgAggregateName},
{Name: "aggfinalmodify", Type: pgtypes.InternalChar, Default: nil, Nullable: false, Source: PgAggregateName},
{Name: "aggmfinalmodify", Type: pgtypes.InternalChar, Default: nil, Nullable: false, Source: PgAggregateName},
{Name: "aggsortop", Type: pgtypes.Oid, Default: nil, Nullable: false, Source: PgAggregateName},
{Name: "aggtranstype", Type: pgtypes.Oid, Default: nil, Nullable: false, Source: PgAggregateName},
{Name: "aggtransspace", Type: pgtypes.Int32, Default: nil, Nullable: false, Source: PgAggregateName},
@@ -84,13 +98,47 @@ var pgAggregateSchema = sql.Schema{
// pgAggregateRowIter is the sql.RowIter for the pg_aggregate table.
type pgAggregateRowIter struct {
aggs []aggregates.Aggregate
idx int
}
var _ sql.RowIter = (*pgAggregateRowIter)(nil)
// Next implements the interface sql.RowIter.
func (iter *pgAggregateRowIter) Next(ctx *sql.Context) (sql.Row, error) {
return nil, io.EOF
if iter.idx >= len(iter.aggs) {
return nil, io.EOF
}
agg := iter.aggs[iter.idx]
iter.idx++
var initVal any
if agg.HasInitCond {
initVal = agg.InitCond
}
return sql.Row{
agg.ID.AsId(), // aggfnoid
"n", // aggkind
int16(0), // aggnumdirectargs
agg.SFunc.AsId(), // aggtransfn
agg.FinalFunc.AsId(), // aggfinalfn
agg.CombineFunc.AsId(), // aggcombinefn
id.Null, // aggserialfn
id.Null, // aggdeserialfn
id.Null, // aggmtransfn
id.Null, // aggminvtransfn
id.Null, // aggmfinalfn
false, // aggfinalextra
false, // aggmfinalextra
"r", // aggfinalmodify
"r", // aggmfinalmodify
id.Null, // aggsortop
agg.SType.AsId(), // aggtranstype
int32(0), // aggtransspace
id.Null, // aggmtranstype
int32(0), // aggmtransspace
initVal, // agginitval
nil, // aggminitval
}, nil
}
// Close implements the interface sql.RowIter.
+48 -3
View File
@@ -19,6 +19,9 @@ import (
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/operators"
"github.com/dolthub/doltgresql/server/tables"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
@@ -43,9 +46,20 @@ func (p PgOperatorHandler) Name() string {
// RowIter implements the interface tables.Handler.
func (p PgOperatorHandler) RowIter(ctx *sql.Context, partition sql.Partition) (sql.RowIter, error) {
// pg_operator is currently empty, since built-in operators are not yet cataloged with stable OIDs.
// TODO: fill this in from the operator framework's built-in operators
return emptyRowIter()
operatorCollection, err := core.GetOperatorsCollectionFromContext(ctx, "")
if err != nil {
return nil, err
}
var ops []operators.Operator
err = operatorCollection.IterateOperators(ctx, func(o operators.Operator) (stop bool, err error) {
ops = append(ops, o)
return false, nil
})
if err != nil {
return nil, err
}
return &pgOperatorRowIter{ops: ops}, nil
}
// PkSchema implements the interface tables.Handler.
@@ -77,13 +91,44 @@ var pgOperatorSchema = sql.Schema{
// pgOperatorRowIter is the sql.RowIter for the pg_operator table.
type pgOperatorRowIter struct {
ops []operators.Operator
idx int
}
var _ sql.RowIter = (*pgOperatorRowIter)(nil)
// Next implements the interface sql.RowIter.
func (iter *pgOperatorRowIter) Next(ctx *sql.Context) (sql.Row, error) {
return nil, io.EOF
if iter.idx >= len(iter.ops) {
return nil, io.EOF
}
op := iter.ops[iter.idx]
iter.idx++
commutator := id.Null
if len(op.Commutator) > 0 {
commutator = id.NewOperator(op.ID.SchemaName(), op.Commutator, op.ID.RightType(), op.ID.LeftType()).AsId()
}
negator := id.Null
if len(op.Negator) > 0 {
negator = id.NewOperator(op.ID.SchemaName(), op.Negator, op.ID.LeftType(), op.ID.RightType()).AsId()
}
return sql.Row{
op.ID.AsId(), // oid
op.ID.Symbol(), // oprname
id.NewNamespace(op.ID.SchemaName()).AsId(), // oprnamespace
id.Null, // oprowner
"b", // oprkind
op.Merges, // oprcanmerge
op.Hashes, // oprcanhash
op.ID.LeftType().AsId(), // oprleft
op.ID.RightType().AsId(), // oprright
op.ReturnType.AsId(), // oprresult
commutator, // oprcom
negator, // oprnegate
op.Function.AsId(), // oprcode
id.Null, // oprrest
id.Null, // oprjoin
}, nil
}
// Close implements the interface sql.RowIter.
+34
View File
@@ -19,6 +19,8 @@ import (
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/core/aggregates"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/procedures"
"github.com/dolthub/doltgresql/server/functions"
@@ -229,6 +231,38 @@ func cachePgProcs(ctx *sql.Context, pgCatalogCache *pgCatalogCache) error {
return err
}
aggCollection, err := core.GetAggregatesCollectionFromContext(ctx, "")
if err != nil {
return err
}
err = aggCollection.IterateAggregates(ctx, func(a aggregates.Aggregate) (stop bool, err error) {
var argTypes any
params := a.ID.Parameters()
if len(params) > 0 {
types := make([]any, len(params))
for i, param := range params {
types[i] = param.AsId()
}
argTypes = types
}
pprocs = append(pprocs, &pgProc{
oid: a.ID.AsId(),
name: a.ID.FunctionName(),
schemaOid: id.NewNamespace(a.ID.SchemaName()).AsId(),
variadic: id.Null,
kind: "a",
volatile: "i",
nArgs: int16(len(params)),
retTyp: a.ReturnType.AsId(),
argTypes: argTypes,
src: "aggregate_dummy",
})
return false, nil
})
if err != nil {
return err
}
pgCatalogCache.procs = pprocs
return nil
}
+100
View File
@@ -0,0 +1,100 @@
// 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 types
import (
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core/id"
)
// BaseTypeDefinition describes a base type, with every support function already resolved to its registry ID.
type BaseTypeDefinition struct {
InputFunc uint32
OutputFunc uint32
ReceiveFunc uint32
SendFunc uint32
ModInFunc uint32
ModOutFunc uint32
TypLength int16
PassedByVal bool
Align TypeAlignment
Storage TypeStorage
TypCategory TypeCategory
IsPreferred bool
Default string
Elem *DoltgresType
Delimiter string
Collatable bool
}
// NewBaseTypeDefinition returns the definition of a base type whose options have all been omitted.
func NewBaseTypeDefinition() BaseTypeDefinition {
return BaseTypeDefinition{
TypLength: -1,
Align: TypeAlignment_Int,
Storage: TypeStorage_Plain,
TypCategory: TypeCategory_UserDefinedTypes,
Delimiter: ",",
}
}
// NewBaseType returns a new base type from the given definition.
func NewBaseType(ctx *sql.Context, typeID id.Type, def BaseTypeDefinition) *DoltgresType {
elem := internalNullType
if def.Elem != nil {
elem = def.Elem
}
collation := id.NullCollation
if def.Collatable {
collation = id.NewCollation("pg_catalog", "default")
}
return &DoltgresType{
ID: typeID,
TypLength: def.TypLength,
PassedByVal: def.PassedByVal,
TypType: TypeType_Base,
TypCategory: def.TypCategory,
IsPreferred: def.IsPreferred,
IsDefined: true,
Delimiter: def.Delimiter,
RelID: id.Null,
SubscriptFunc: toFuncID("-"),
Elem: elem,
Array: internalNullType,
InputFunc: def.InputFunc,
OutputFunc: def.OutputFunc,
ReceiveFunc: def.ReceiveFunc,
SendFunc: def.SendFunc,
ModInFunc: def.ModInFunc,
ModOutFunc: def.ModOutFunc,
AnalyzeFunc: toFuncID("-"),
Align: def.Align,
Storage: def.Storage,
NotNull: false,
BaseTypeType: internalNullType,
TypMod: -1,
NDims: 0,
TypCollation: collation,
DefaulBin: "",
Default: def.Default,
Acl: nil,
Checks: nil,
attTypMod: -1,
CompareFunc: toFuncID("-"),
SerializationFunc: nil,
DeserializationFunc: nil,
}
}
+45 -18
View File
@@ -30,14 +30,13 @@ type QuickFunction interface {
WithResolvedTypes(newTypes []*DoltgresType) any
}
// LoadFunctionFromCatalog returns the function matching the given name and parameter types. This is intended solely for
// functions that are used for types, as the returned functions are not valid using the Eval function.
var LoadFunctionFromCatalog func(ctx *sql.Context, funcName string, parameterTypes []*DoltgresType) any
// LoadFunctionFromCatalog returns the function matching the given schema, name and parameter types. This is intended
// solely for functions that are used for types, as the returned functions are not valid using the Eval function.
var LoadFunctionFromCatalog func(ctx *sql.Context, schemaName string, funcName string, parameterTypes []*DoltgresType) any
// functionRegistry is a local registry that holds a mapping from ID to QuickFunction. This is done as types are now
// passed by struct, meaning that we need to cache the loading of functions somewhere. In addition, we don't yet support
// deleting built-in functions, so we can make a global cache. This makes a hard assumption that all functions being
// referenced actually exist, which should be true until built-in function deletion is implemented.
// passed by struct, meaning that we need to cache the loading of functions somewhere. Only the functions in pg_catalog
// are cached, since a user-defined function may be replaced or dropped, and it may differ between databases.
//
// In a way, one can view this as associated an OID to a function. With a proper OID system, this would not need to
// exist. It should be removed once OIDs are figured out.
@@ -46,7 +45,7 @@ type functionRegistry struct {
counter uint32
mapping map[id.Function]uint32
revMapping map[uint32]id.Function
functions [256]QuickFunction // Arbitrary number, big enough for now to fit every function in it
functions []QuickFunction
}
// globalFunctionRegistry is the global functionRegistry. Only one needs to exist since we do not yet allow deleting
@@ -56,6 +55,7 @@ var globalFunctionRegistry = functionRegistry{
counter: 1,
mapping: map[id.Function]uint32{id.NullFunction: 0},
revMapping: map[uint32]id.Function{0: id.NullFunction},
functions: make([]QuickFunction, 1, 256),
}
// InternalToRegistryID returns an ID for the given Internal ID.
@@ -65,11 +65,9 @@ func (r *functionRegistry) InternalToRegistryID(functionID id.Function) uint32 {
if registryID, ok := r.mapping[functionID]; ok {
return registryID
}
if r.counter >= uint32(len(r.functions)) {
panic("max function count reached in static array")
}
r.mapping[functionID] = r.counter
r.revMapping[r.counter] = functionID
r.functions = append(r.functions, nil)
r.counter++
return r.counter - 1
}
@@ -122,13 +120,18 @@ func (r *functionRegistry) loadFunction(ctx *sql.Context, id uint32) QuickFuncti
if !functionID.IsValid() {
return nil
}
funcName, types := r.toFuncSignature(functionID)
potentialFunction := LoadFunctionFromCatalog(ctx, funcName, types)
funcName, types, ok := r.toFuncSignature(ctx, functionID)
if !ok {
return nil
}
potentialFunction := LoadFunctionFromCatalog(ctx, functionID.SchemaName(), funcName, types)
if potentialFunction == nil {
return nil
}
f = potentialFunction.(QuickFunction)
r.functions[id] = f
if functionID.SchemaName() == "pg_catalog" {
r.functions[id] = f
}
return f
}
@@ -140,14 +143,33 @@ func (*functionRegistry) nameWithoutParams(functionID id.Function) string {
return functionID.FunctionName()
}
// toFuncSignature returns a function signature for the given Internal ID.
func (*functionRegistry) toFuncSignature(functionID id.Function) (string, []*DoltgresType) {
// toFuncSignature returns a function signature for the given Internal ID. Returns false when a parameter names a type
// that cannot be resolved, which may happen when a user-defined type has been dropped.
func (*functionRegistry) toFuncSignature(ctx *sql.Context, functionID id.Function) (string, []*DoltgresType, bool) {
internalParams := functionID.Parameters()
params := make([]*DoltgresType, len(internalParams))
var collection TypeCollection
for i, internalParam := range internalParams {
params[i] = IDToBuiltInDoltgresType[internalParam]
if builtIn, ok := IDToBuiltInDoltgresType[internalParam]; ok {
params[i] = builtIn
continue
}
if collection == nil {
if GetTypesCollectionFromContext == nil {
return "", nil, false
}
var err error
if collection, err = GetTypesCollectionFromContext(ctx, ""); err != nil {
return "", nil, false
}
}
param, err := collection.GetType(ctx, internalParam)
if err != nil || param == nil {
return "", nil, false
}
params[i] = param
}
return functionID.FunctionName(), params
return functionID.FunctionName(), params, true
}
// toFuncID creates a valid function string for the given name and parameters, then registers the name with the
@@ -156,7 +178,12 @@ func toFuncID(functionName string, params ...id.Type) uint32 {
if functionName == "-" || len(functionName) == 0 {
return 0
}
functionID := id.NewFunction("pg_catalog", functionName, params...)
return ToFuncID(id.NewFunction("pg_catalog", functionName, params...))
}
// ToFuncID registers the given function with the global function registry, and returns the ID it was given. Primarily
// used by extensions.
func ToFuncID(functionID id.Function) uint32 {
return globalFunctionRegistry.InternalToRegistryID(functionID)
}
@@ -34,6 +34,7 @@ var GlobalCustomVariables = map[string]utils.StatementGenerator{
"collation": customDefinition(`en_US`),
"column_definition": customDefinition(`v1 INTEGER`),
"column_number": customDefinition(`1`),
"com_op": customDefinition(`<=>`),
"connlimit": customDefinition(`-1`),
"cycle_mark_default": customDefinition(`'cycle_mark_default'`),
"cycle_mark_value": customDefinition(`'cycle_mark_value'`),
@@ -55,6 +56,7 @@ var GlobalCustomVariables = map[string]utils.StatementGenerator{
"maxvalue": customDefinition(`1`),
"minvalue": customDefinition(`1`),
"mstate_data_size": customDefinition(`16`),
"neg_op": customDefinition(`<~>`),
"neighbor_enum_value": customDefinition(`'1'`),
"new_enum_value": customDefinition(`'1'`),
"numeric_literal": customDefinition(`1`),
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
// Copyright 2023 Dolthub, Inc.
// 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.
@@ -18,42 +18,42 @@ import "testing"
func TestDropOperator(t *testing.T) {
tests := []QueryParses{
Unimplemented("DROP OPERATOR @@ ( left_type , right_type )"),
Unimplemented("DROP OPERATOR IF EXISTS @@ ( left_type , right_type )"),
Unimplemented("DROP OPERATOR @@ ( NONE , right_type )"),
Unimplemented("DROP OPERATOR IF EXISTS @@ ( NONE , right_type )"),
Unimplemented("DROP OPERATOR @@ ( left_type , right_type ) , @@ ( left_type , right_type )"),
Unimplemented("DROP OPERATOR IF EXISTS @@ ( left_type , right_type ) , @@ ( left_type , right_type )"),
Unimplemented("DROP OPERATOR @@ ( NONE , right_type ) , @@ ( left_type , right_type )"),
Unimplemented("DROP OPERATOR IF EXISTS @@ ( NONE , right_type ) , @@ ( left_type , right_type )"),
Unimplemented("DROP OPERATOR @@ ( left_type , right_type ) , @@ ( NONE , right_type )"),
Unimplemented("DROP OPERATOR IF EXISTS @@ ( left_type , right_type ) , @@ ( NONE , right_type )"),
Unimplemented("DROP OPERATOR @@ ( NONE , right_type ) , @@ ( NONE , right_type )"),
Unimplemented("DROP OPERATOR IF EXISTS @@ ( NONE , right_type ) , @@ ( NONE , right_type )"),
Unimplemented("DROP OPERATOR @@ ( left_type , right_type ) CASCADE"),
Unimplemented("DROP OPERATOR IF EXISTS @@ ( left_type , right_type ) CASCADE"),
Unimplemented("DROP OPERATOR @@ ( NONE , right_type ) CASCADE"),
Unimplemented("DROP OPERATOR IF EXISTS @@ ( NONE , right_type ) CASCADE"),
Unimplemented("DROP OPERATOR @@ ( left_type , right_type ) , @@ ( left_type , right_type ) CASCADE"),
Unimplemented("DROP OPERATOR IF EXISTS @@ ( left_type , right_type ) , @@ ( left_type , right_type ) CASCADE"),
Unimplemented("DROP OPERATOR @@ ( NONE , right_type ) , @@ ( left_type , right_type ) CASCADE"),
Unimplemented("DROP OPERATOR IF EXISTS @@ ( NONE , right_type ) , @@ ( left_type , right_type ) CASCADE"),
Unimplemented("DROP OPERATOR @@ ( left_type , right_type ) , @@ ( NONE , right_type ) CASCADE"),
Unimplemented("DROP OPERATOR IF EXISTS @@ ( left_type , right_type ) , @@ ( NONE , right_type ) CASCADE"),
Unimplemented("DROP OPERATOR @@ ( NONE , right_type ) , @@ ( NONE , right_type ) CASCADE"),
Unimplemented("DROP OPERATOR IF EXISTS @@ ( NONE , right_type ) , @@ ( NONE , right_type ) CASCADE"),
Unimplemented("DROP OPERATOR @@ ( left_type , right_type ) RESTRICT"),
Unimplemented("DROP OPERATOR IF EXISTS @@ ( left_type , right_type ) RESTRICT"),
Unimplemented("DROP OPERATOR @@ ( NONE , right_type ) RESTRICT"),
Unimplemented("DROP OPERATOR IF EXISTS @@ ( NONE , right_type ) RESTRICT"),
Unimplemented("DROP OPERATOR @@ ( left_type , right_type ) , @@ ( left_type , right_type ) RESTRICT"),
Unimplemented("DROP OPERATOR IF EXISTS @@ ( left_type , right_type ) , @@ ( left_type , right_type ) RESTRICT"),
Unimplemented("DROP OPERATOR @@ ( NONE , right_type ) , @@ ( left_type , right_type ) RESTRICT"),
Unimplemented("DROP OPERATOR IF EXISTS @@ ( NONE , right_type ) , @@ ( left_type , right_type ) RESTRICT"),
Unimplemented("DROP OPERATOR @@ ( left_type , right_type ) , @@ ( NONE , right_type ) RESTRICT"),
Unimplemented("DROP OPERATOR IF EXISTS @@ ( left_type , right_type ) , @@ ( NONE , right_type ) RESTRICT"),
Unimplemented("DROP OPERATOR @@ ( NONE , right_type ) , @@ ( NONE , right_type ) RESTRICT"),
Unimplemented("DROP OPERATOR IF EXISTS @@ ( NONE , right_type ) , @@ ( NONE , right_type ) RESTRICT"),
Converts("DROP OPERATOR @@ ( left_type , right_type )"),
Converts("DROP OPERATOR IF EXISTS @@ ( left_type , right_type )"),
Converts("DROP OPERATOR @@ ( NONE , right_type )"),
Converts("DROP OPERATOR IF EXISTS @@ ( NONE , right_type )"),
Converts("DROP OPERATOR @@ ( left_type , right_type ) , @@ ( left_type , right_type )"),
Converts("DROP OPERATOR IF EXISTS @@ ( left_type , right_type ) , @@ ( left_type , right_type )"),
Converts("DROP OPERATOR @@ ( NONE , right_type ) , @@ ( left_type , right_type )"),
Converts("DROP OPERATOR IF EXISTS @@ ( NONE , right_type ) , @@ ( left_type , right_type )"),
Converts("DROP OPERATOR @@ ( left_type , right_type ) , @@ ( NONE , right_type )"),
Converts("DROP OPERATOR IF EXISTS @@ ( left_type , right_type ) , @@ ( NONE , right_type )"),
Converts("DROP OPERATOR @@ ( NONE , right_type ) , @@ ( NONE , right_type )"),
Converts("DROP OPERATOR IF EXISTS @@ ( NONE , right_type ) , @@ ( NONE , right_type )"),
Parses("DROP OPERATOR @@ ( left_type , right_type ) CASCADE"),
Parses("DROP OPERATOR IF EXISTS @@ ( left_type , right_type ) CASCADE"),
Parses("DROP OPERATOR @@ ( NONE , right_type ) CASCADE"),
Parses("DROP OPERATOR IF EXISTS @@ ( NONE , right_type ) CASCADE"),
Parses("DROP OPERATOR @@ ( left_type , right_type ) , @@ ( left_type , right_type ) CASCADE"),
Parses("DROP OPERATOR IF EXISTS @@ ( left_type , right_type ) , @@ ( left_type , right_type ) CASCADE"),
Parses("DROP OPERATOR @@ ( NONE , right_type ) , @@ ( left_type , right_type ) CASCADE"),
Parses("DROP OPERATOR IF EXISTS @@ ( NONE , right_type ) , @@ ( left_type , right_type ) CASCADE"),
Parses("DROP OPERATOR @@ ( left_type , right_type ) , @@ ( NONE , right_type ) CASCADE"),
Parses("DROP OPERATOR IF EXISTS @@ ( left_type , right_type ) , @@ ( NONE , right_type ) CASCADE"),
Parses("DROP OPERATOR @@ ( NONE , right_type ) , @@ ( NONE , right_type ) CASCADE"),
Parses("DROP OPERATOR IF EXISTS @@ ( NONE , right_type ) , @@ ( NONE , right_type ) CASCADE"),
Converts("DROP OPERATOR @@ ( left_type , right_type ) RESTRICT"),
Converts("DROP OPERATOR IF EXISTS @@ ( left_type , right_type ) RESTRICT"),
Converts("DROP OPERATOR @@ ( NONE , right_type ) RESTRICT"),
Converts("DROP OPERATOR IF EXISTS @@ ( NONE , right_type ) RESTRICT"),
Converts("DROP OPERATOR @@ ( left_type , right_type ) , @@ ( left_type , right_type ) RESTRICT"),
Converts("DROP OPERATOR IF EXISTS @@ ( left_type , right_type ) , @@ ( left_type , right_type ) RESTRICT"),
Converts("DROP OPERATOR @@ ( NONE , right_type ) , @@ ( left_type , right_type ) RESTRICT"),
Converts("DROP OPERATOR IF EXISTS @@ ( NONE , right_type ) , @@ ( left_type , right_type ) RESTRICT"),
Converts("DROP OPERATOR @@ ( left_type , right_type ) , @@ ( NONE , right_type ) RESTRICT"),
Converts("DROP OPERATOR IF EXISTS @@ ( left_type , right_type ) , @@ ( NONE , right_type ) RESTRICT"),
Converts("DROP OPERATOR @@ ( NONE , right_type ) , @@ ( NONE , right_type ) RESTRICT"),
Converts("DROP OPERATOR IF EXISTS @@ ( NONE , right_type ) , @@ ( NONE , right_type ) RESTRICT"),
}
RunTests(t, tests)
}
+303
View File
@@ -0,0 +1,303 @@
// 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 _go
import (
"testing"
"github.com/dolthub/go-mysql-server/sql"
)
func TestCreateAggregate(t *testing.T) {
RunScripts(t, []ScriptTest{
{
Name: "CREATE AGGREGATE with SFUNC and STYPE",
SetUpScript: []string{
`CREATE FUNCTION agg_sum_step(state int4, val int4) RETURNS int4
AS $$ SELECT state + val $$ LANGUAGE SQL;`,
`CREATE TABLE agg_sum_vals (pk int4 PRIMARY KEY, grp text, v int4);`,
`INSERT INTO agg_sum_vals VALUES (1, 'a', 10), (2, 'a', 20), (3, 'b', 5), (4, 'b', NULL);`,
},
Assertions: []ScriptTestAssertion{
{
Query: `CREATE AGGREGATE agg_sum (int4) (SFUNC = agg_sum_step, STYPE = int4, INITCOND = '0');`,
Expected: []sql.Row{},
},
{
Query: `SELECT agg_sum(v) FROM agg_sum_vals WHERE grp = 'a';`,
Expected: []sql.Row{{30}},
},
{ // The transition function is not STRICT, so the NULL in group 'b' nulls the state
Query: `SELECT grp, agg_sum(v) FROM agg_sum_vals GROUP BY grp ORDER BY grp;`,
Expected: []sql.Row{{"a", 30}, {"b", nil}},
},
{ // The initial condition is the state when the aggregate sees no rows at all
Query: `SELECT agg_sum(v) FROM agg_sum_vals WHERE pk = 0;`,
Expected: []sql.Row{{0}},
},
{
Query: `SELECT aggkind, agginitval, aggfinalfn::text, aggcombinefn::text FROM pg_aggregate WHERE aggtransfn::text = 'agg_sum_step';`,
Expected: []sql.Row{{"n", "0", "-", "-"}},
},
{
Query: `SELECT proname, prokind FROM pg_proc WHERE proname = 'agg_sum';`,
Expected: []sql.Row{{"agg_sum", "a"}},
},
},
},
{
Name: "CREATE AGGREGATE with STRICT transition function and no INITCOND",
SetUpScript: []string{
`CREATE FUNCTION agg_larger_step(state int4, val int4) RETURNS int4
AS $$ SELECT CASE WHEN state > val THEN state ELSE val END $$ LANGUAGE SQL STRICT;`,
`CREATE TABLE agg_larger_vals (pk int4 PRIMARY KEY, v int4);`,
`INSERT INTO agg_larger_vals VALUES (1, 3), (2, NULL), (3, 8), (4, 5);`,
},
Assertions: []ScriptTestAssertion{
{
Query: `CREATE AGGREGATE agg_larger (int4) (SFUNC = agg_larger_step, STYPE = int4);`,
Expected: []sql.Row{},
},
{ // A STRICT transition function skips NULL inputs, and the first non-NULL value seeds the state
Query: `SELECT agg_larger(v) FROM agg_larger_vals;`,
Expected: []sql.Row{{8}},
},
{
Query: `SELECT agg_larger(v) FROM agg_larger_vals WHERE pk = 2;`,
Expected: []sql.Row{{nil}},
},
{
Query: `SELECT agg_larger(v) FROM agg_larger_vals WHERE pk = 0;`,
Expected: []sql.Row{{nil}},
},
{
Query: `SELECT agginitval IS NULL FROM pg_aggregate WHERE aggtransfn::text = 'agg_larger_step';`,
Expected: []sql.Row{{"t"}},
},
},
},
{
Name: "CREATE AGGREGATE with FINALFUNC",
SetUpScript: []string{
`CREATE FUNCTION agg_charcount_step(state int4, val text) RETURNS int4
AS $$ SELECT state + length(val) $$ LANGUAGE SQL;`,
`CREATE FUNCTION agg_charcount_final(state int4) RETURNS text
AS $$ SELECT 'chars: ' || state $$ LANGUAGE SQL;`,
`CREATE TABLE agg_charcount_vals (pk int4 PRIMARY KEY, v text);`,
`INSERT INTO agg_charcount_vals VALUES (1, 'ab'), (2, 'cde'), (3, 'f');`,
},
Assertions: []ScriptTestAssertion{
{
Query: `CREATE AGGREGATE agg_charcount (text) (SFUNC = agg_charcount_step, STYPE = int4,
FINALFUNC = agg_charcount_final, INITCOND = '0');`,
Expected: []sql.Row{},
},
{
Query: `SELECT agg_charcount(v) FROM agg_charcount_vals;`,
Expected: []sql.Row{{"chars: 6"}},
},
{ // The final function still runs when the aggregate sees no rows
Query: `SELECT agg_charcount(v) FROM agg_charcount_vals WHERE pk = 0;`,
Expected: []sql.Row{{"chars: 0"}},
},
{
Query: `SELECT aggfinalfn::text FROM pg_aggregate WHERE aggtransfn::text = 'agg_charcount_step';`,
Expected: []sql.Row{{"agg_charcount_final"}},
},
},
},
{
Name: "CREATE AGGREGATE with COMBINEFUNC",
SetUpScript: []string{
`CREATE FUNCTION agg_combined_step(state int8, val int4) RETURNS int8
AS $$ SELECT state + val $$ LANGUAGE SQL;`,
`CREATE FUNCTION agg_combined_merge(s1 int8, s2 int8) RETURNS int8
AS $$ SELECT s1 + s2 $$ LANGUAGE SQL;`,
`CREATE TABLE agg_combined_vals (pk int4 PRIMARY KEY, v int4);`,
`INSERT INTO agg_combined_vals VALUES (1, 10), (2, 20), (3, 5);`,
},
Assertions: []ScriptTestAssertion{
{
Query: `CREATE AGGREGATE agg_combined (int4) (SFUNC = agg_combined_step, STYPE = int8,
COMBINEFUNC = agg_combined_merge, INITCOND = '0');`,
Expected: []sql.Row{},
},
{
Query: `SELECT agg_combined(v) FROM agg_combined_vals;`,
Expected: []sql.Row{{35}},
},
{
Query: `SELECT aggcombinefn::text FROM pg_aggregate WHERE aggtransfn::text = 'agg_combined_step';`,
Expected: []sql.Row{{"agg_combined_merge"}},
},
},
},
{
Name: "CREATE AGGREGATE with multiple arguments",
SetUpScript: []string{
`CREATE FUNCTION agg_wsum_step(state int8, val int4, weight int4) RETURNS int8
AS $$ SELECT state + (val * weight) $$ LANGUAGE SQL;`,
`CREATE TABLE agg_wsum_vals (pk int4 PRIMARY KEY, v int4, w int4);`,
`INSERT INTO agg_wsum_vals VALUES (1, 10, 1), (2, 20, 2), (3, 30, 3);`,
},
Assertions: []ScriptTestAssertion{
{
Query: `CREATE AGGREGATE agg_wsum (int4, int4) (SFUNC = agg_wsum_step, STYPE = int8, INITCOND = '0');`,
Expected: []sql.Row{},
},
{
Query: `SELECT agg_wsum(v, w) FROM agg_wsum_vals;`,
Expected: []sql.Row{{140}},
},
},
},
{
Name: "CREATE AGGREGATE in a custom schema",
SetUpScript: []string{
`CREATE SCHEMA agg_nsp;`,
`CREATE FUNCTION agg_nsp_step(state int4, val int4) RETURNS int4
AS $$ SELECT state + 1 $$ LANGUAGE SQL;`,
`CREATE TABLE agg_nsp_vals (pk int4 PRIMARY KEY, v int4);`,
`INSERT INTO agg_nsp_vals VALUES (1, 10), (2, 20), (3, NULL);`,
},
Assertions: []ScriptTestAssertion{
{
Query: `CREATE AGGREGATE agg_nsp.agg_rows (int4) (SFUNC = agg_nsp_step, STYPE = int4, INITCOND = '0');`,
Expected: []sql.Row{},
},
{
Query: `SELECT agg_nsp.agg_rows(v) FROM agg_nsp_vals;`,
Expected: []sql.Row{{3}},
},
{
Query: `SELECT n.nspname FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE p.proname = 'agg_rows';`,
Expected: []sql.Row{{"agg_nsp"}},
},
{
Query: `SELECT agg_rows(v) FROM agg_nsp_vals;`,
ExpectedErr: "function: 'agg_rows' not found",
},
},
},
{
Name: "CREATE OR REPLACE AGGREGATE",
SetUpScript: []string{
`CREATE FUNCTION agg_replace_step(state int4, val int4) RETURNS int4
AS $$ SELECT state + val $$ LANGUAGE SQL;`,
`CREATE TABLE agg_replace_vals (pk int4 PRIMARY KEY, v int4);`,
`INSERT INTO agg_replace_vals VALUES (1, 10), (2, 20);`,
`CREATE AGGREGATE agg_replace (int4) (SFUNC = agg_replace_step, STYPE = int4, INITCOND = '0');`,
},
Assertions: []ScriptTestAssertion{
{
Query: `SELECT agg_replace(v) FROM agg_replace_vals;`,
Expected: []sql.Row{{30}},
},
{
Query: `CREATE OR REPLACE AGGREGATE agg_replace (int4) (SFUNC = agg_replace_step, STYPE = int4, INITCOND = '100');`,
Expected: []sql.Row{},
},
{
Query: `SELECT agg_replace(v) FROM agg_replace_vals;`,
Expected: []sql.Row{{130}},
},
{
Query: `SELECT agginitval FROM pg_aggregate WHERE aggtransfn::text = 'agg_replace_step';`,
Expected: []sql.Row{{"100"}},
},
},
},
{
Name: "DROP AGGREGATE smoke test",
SetUpScript: []string{
`CREATE FUNCTION agg_drop_step(state int4, val int4) RETURNS int4
AS $$ SELECT state + val $$ LANGUAGE SQL;`,
`CREATE TABLE agg_drop_vals (pk int4 PRIMARY KEY, v int4);`,
`INSERT INTO agg_drop_vals VALUES (1, 10), (2, 20);`,
`CREATE AGGREGATE agg_drop (int4) (SFUNC = agg_drop_step, STYPE = int4, INITCOND = '0');`,
},
Assertions: []ScriptTestAssertion{
{
Query: `SELECT agg_drop(v) FROM agg_drop_vals;`,
Expected: []sql.Row{{30}},
},
{
Query: `DROP AGGREGATE agg_drop(int4);`,
Expected: []sql.Row{},
},
{
Query: `SELECT agg_drop(v) FROM agg_drop_vals;`,
ExpectedErr: "function: 'agg_drop' not found",
},
{
Query: `SELECT EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'agg_drop');`,
Expected: []sql.Row{{"f"}},
},
{
Query: `CREATE AGGREGATE agg_drop (int4) (SFUNC = agg_drop_step, STYPE = int4, INITCOND = '0');`,
Expected: []sql.Row{},
},
{
Query: `SELECT agg_drop(v) FROM agg_drop_vals;`,
Expected: []sql.Row{{30}},
},
},
},
{
Name: "CREATE AGGREGATE validation",
SetUpScript: []string{
`CREATE FUNCTION agg_valid_step(state int4, val int4) RETURNS int4
AS $$ SELECT state + val $$ LANGUAGE SQL;`,
`CREATE FUNCTION agg_one_arg(state int4) RETURNS int4
AS $$ SELECT state $$ LANGUAGE SQL;`,
`CREATE FUNCTION agg_wrong_ret(state int4, val int4) RETURNS text
AS $$ SELECT 'x' $$ LANGUAGE SQL;`,
`CREATE FUNCTION agg_taken(a int4) RETURNS int4
AS $$ SELECT a $$ LANGUAGE SQL;`,
`CREATE AGGREGATE agg_valid (int4) (SFUNC = agg_valid_step, STYPE = int4);`,
},
Assertions: []ScriptTestAssertion{
{
Query: `CREATE AGGREGATE agg_bad1 (int4) (SFUNC = agg_missing_step, STYPE = int4);`,
ExpectedErr: "function agg_missing_step(integer, integer) does not exist",
},
{
Query: `CREATE AGGREGATE agg_bad2 (int4) (SFUNC = agg_one_arg, STYPE = int4);`,
ExpectedErr: "function agg_one_arg(integer, integer) does not exist",
},
{
Query: `CREATE AGGREGATE agg_bad3 (int4) (SFUNC = agg_wrong_ret, STYPE = int4);`,
ExpectedErr: "return type of transition function agg_wrong_ret is not integer",
},
{
Query: `CREATE AGGREGATE agg_bad4 (int4) (SFUNC = agg_valid_step, STYPE = int4, FINALFUNC = agg_missing_final);`,
ExpectedErr: "function agg_missing_final(integer) does not exist",
},
{
Query: `CREATE AGGREGATE agg_valid (int4) (SFUNC = agg_valid_step, STYPE = int4);`,
ExpectedErr: `function "agg_valid" already exists with same argument types`,
},
{
Query: `CREATE AGGREGATE agg_taken (int4) (SFUNC = agg_valid_step, STYPE = int4);`,
ExpectedErr: `function "agg_taken" already exists with same argument types`,
},
{
Query: `CREATE AGGREGATE agg_bad5 (OUT x int4) (SFUNC = agg_valid_step, STYPE = int4);`,
ExpectedErr: "aggregates cannot have output arguments",
},
},
},
})
}
+265
View File
@@ -0,0 +1,265 @@
// 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 _go
import (
"testing"
"github.com/dolthub/go-mysql-server/sql"
)
func TestCreateOperator(t *testing.T) {
RunScripts(t, []ScriptTest{
{
Name: "CREATE OPERATOR with LEFTARG, RIGHTARG, and FUNCTION",
SetUpScript: []string{
`CREATE FUNCTION op_int_dist(a int4, b int4) RETURNS int4
AS $$ SELECT abs(a - b) $$ LANGUAGE SQL;`,
`CREATE TABLE op_points (pk int4 PRIMARY KEY, v int4);`,
`INSERT INTO op_points VALUES (1, 4), (2, 9), (3, 15);`,
},
Assertions: []ScriptTestAssertion{
{
Query: `CREATE OPERATOR <-> (LEFTARG = int4, RIGHTARG = int4, FUNCTION = op_int_dist);`,
Expected: []sql.Row{},
},
{
Query: `SELECT 3 <-> 10;`,
Expected: []sql.Row{{7}},
},
{
Query: `SELECT 10 <-> 3;`,
Expected: []sql.Row{{7}},
},
{ // NULL operands are passed through to the non-STRICT backing function
Query: `SELECT (NULL::int4 <-> 3) IS NULL;`,
Expected: []sql.Row{{"t"}},
},
{
Query: `SELECT pk, v <-> 10 FROM op_points ORDER BY pk;`,
Expected: []sql.Row{{1, 6}, {2, 1}, {3, 5}},
},
{
Query: `SELECT pk FROM op_points WHERE v <-> 10 < 3 ORDER BY pk;`,
Expected: []sql.Row{{2}},
},
{
Query: `SELECT oprname, oprkind, oprcanhash, oprcanmerge, oprleft::regtype::text, oprright::regtype::text, oprresult::regtype::text
FROM pg_operator WHERE oprcode::text = 'op_int_dist';`,
Expected: []sql.Row{{"<->", "b", "f", "f", "integer", "integer", "integer"}},
},
{
Query: `SELECT n.nspname FROM pg_operator o JOIN pg_namespace n ON n.oid = o.oprnamespace WHERE o.oprcode::text = 'op_int_dist';`,
Expected: []sql.Row{{"public"}},
},
},
},
{
Name: "CREATE OPERATOR with COMMUTATOR and NEGATOR",
SetUpScript: []string{
`CREATE FUNCTION op_len_eq(a text, b text) RETURNS boolean
AS $$ SELECT length(a) = length(b) $$ LANGUAGE SQL;`,
`CREATE FUNCTION op_len_ne(a text, b text) RETURNS boolean
AS $$ SELECT length(a) <> length(b) $$ LANGUAGE SQL;`,
},
Assertions: []ScriptTestAssertion{
{
Query: `CREATE OPERATOR <=> (LEFTARG = text, RIGHTARG = text, FUNCTION = op_len_eq, COMMUTATOR = <=>);`,
Expected: []sql.Row{},
},
{
Query: `CREATE OPERATOR <~> (LEFTARG = text, RIGHTARG = text, FUNCTION = op_len_ne, COMMUTATOR = <~>, NEGATOR = <=>);`,
Expected: []sql.Row{},
},
{
Query: `SELECT 'abc' <=> 'xyz';`,
Expected: []sql.Row{{"t"}},
},
{
Query: `SELECT 'abc' <=> 'wxyz';`,
Expected: []sql.Row{{"f"}},
},
{
Query: `SELECT 'abc' <~> 'wxyz';`,
Expected: []sql.Row{{"t"}},
},
{
Query: `SELECT c.oprname FROM pg_operator o JOIN pg_operator c ON c.oid = o.oprcom WHERE o.oprcode::text = 'op_len_eq';`,
Expected: []sql.Row{{"<=>"}},
},
{ // Declaring the negator on one operator links both directions
Query: `SELECT n.oprname FROM pg_operator o JOIN pg_operator n ON n.oid = o.oprnegate WHERE o.oprcode::text = 'op_len_ne';`,
Expected: []sql.Row{{"<=>"}},
},
{
Query: `SELECT n.oprname FROM pg_operator o JOIN pg_operator n ON n.oid = o.oprnegate WHERE o.oprcode::text = 'op_len_eq';`,
Expected: []sql.Row{{"<~>"}},
},
},
},
{
Name: "CREATE OPERATOR with HASHES and MERGES",
SetUpScript: []string{
`CREATE FUNCTION op_ci_eq(a text, b text) RETURNS boolean
AS $$ SELECT lower(a) = lower(b) $$ LANGUAGE SQL;`,
},
Assertions: []ScriptTestAssertion{
{ // We're not testing HASHES or MERGES, just that they parse since they're ignored options
Query: `CREATE OPERATOR <%> (LEFTARG = text, RIGHTARG = text, FUNCTION = op_ci_eq, COMMUTATOR = <%>, HASHES, MERGES);`,
Expected: []sql.Row{},
},
{
Query: `SELECT 'ABC' <%> 'abc';`,
Expected: []sql.Row{{"t"}},
},
{
Query: `SELECT oprcanhash, oprcanmerge FROM pg_operator WHERE oprcode::text = 'op_ci_eq';`,
Expected: []sql.Row{{"t", "t"}},
},
},
},
{
Name: "CREATE OPERATOR with composite operands",
SetUpScript: []string{
`CREATE TABLE op_pair (x int4, y int4);`,
`CREATE FUNCTION op_pair_add(a op_pair, b op_pair) RETURNS op_pair
AS $$ SELECT ROW((a).x + (b).x, (a).y + (b).y)::op_pair $$ LANGUAGE SQL;`,
},
Assertions: []ScriptTestAssertion{
{
Query: `CREATE OPERATOR <+> (LEFTARG = op_pair, RIGHTARG = op_pair, FUNCTION = op_pair_add);`,
Expected: []sql.Row{},
},
{
Query: `SELECT ROW(1, 2)::op_pair <+> ROW(3, 4)::op_pair;`,
Expected: []sql.Row{{"(4,6)"}},
},
{
Query: `SELECT oprleft::regtype::text, oprright::regtype::text, oprresult::regtype::text
FROM pg_operator WHERE oprcode::text = 'op_pair_add';`,
Expected: []sql.Row{{"op_pair", "op_pair", "op_pair"}},
},
},
},
{
Name: "CREATE OPERATOR with composite operands on table rows in aggregate",
SetUpScript: []string{
`CREATE TABLE op_pair (x int4, y int4);`,
`CREATE FUNCTION op_pair_add(a op_pair, b op_pair) RETURNS op_pair
AS $$ SELECT ROW((a).x + (b).x, (a).y + (b).y)::op_pair $$ LANGUAGE SQL;`,
`CREATE OPERATOR <+> (LEFTARG = op_pair, RIGHTARG = op_pair, FUNCTION = op_pair_add);`,
`INSERT INTO op_pair VALUES (1, 2), (3, 4), (5, 6);`,
},
Assertions: []ScriptTestAssertion{
{
Query: `SELECT sum(((p <+> ROW(10, 20)::op_pair)).x), sum(((p <+> ROW(10, 20)::op_pair)).y)
FROM op_pair p;`,
Expected: []sql.Row{{39, 72}},
},
},
},
{
Name: "CREATE OPERATOR with mixed operand types",
SetUpScript: []string{
`CREATE FUNCTION op_repeat(a text, b int4) RETURNS text
AS $$ SELECT repeat(a, b) $$ LANGUAGE SQL;`,
},
Assertions: []ScriptTestAssertion{
{
Query: `CREATE OPERATOR <#> (LEFTARG = text, RIGHTARG = int4, FUNCTION = op_repeat);`,
Expected: []sql.Row{},
},
{
Query: `SELECT 'ab' <#> 3;`,
Expected: []sql.Row{{"ababab"}},
},
{
Query: `SELECT 3 <#> 'ab';`,
ExpectedErr: "operator does not exist",
},
},
},
{
Name: "DROP OPERATOR smoke test",
SetUpScript: []string{
`CREATE FUNCTION op_drop_dist(a int4, b int4) RETURNS int4
AS $$ SELECT abs(a - b) $$ LANGUAGE SQL;`,
`CREATE OPERATOR <-> (LEFTARG = int4, RIGHTARG = int4, FUNCTION = op_drop_dist);`,
},
Assertions: []ScriptTestAssertion{
{
Query: `SELECT 3 <-> 10;`,
Expected: []sql.Row{{7}},
},
{
Query: `DROP OPERATOR <-> (int4, int4);`,
Expected: []sql.Row{},
},
{
Query: `SELECT 3 <-> 10;`,
ExpectedErr: "operator does not exist: integer <-> integer",
},
{
Query: `SELECT EXISTS (SELECT 1 FROM pg_operator WHERE oprcode::text = 'op_drop_dist');`,
Expected: []sql.Row{{"f"}},
},
{
Query: `CREATE OPERATOR <-> (LEFTARG = int4, RIGHTARG = int4, FUNCTION = op_drop_dist);`,
Expected: []sql.Row{},
},
{
Query: `SELECT 3 <-> 10;`,
Expected: []sql.Row{{7}},
},
},
},
{
Name: "CREATE OPERATOR validation",
SetUpScript: []string{
`CREATE FUNCTION op_valid_dist(a int4, b int4) RETURNS int4
AS $$ SELECT abs(a - b) $$ LANGUAGE SQL;`,
`CREATE FUNCTION op_one_arg(a int4) RETURNS int4
AS $$ SELECT a $$ LANGUAGE SQL;`,
`CREATE OPERATOR <-> (LEFTARG = int4, RIGHTARG = int4, FUNCTION = op_valid_dist);`,
},
Assertions: []ScriptTestAssertion{
{
Query: `CREATE OPERATOR <=> (LEFTARG = int4, RIGHTARG = int4);`,
ExpectedErr: "operator function must be specified",
},
{
Query: `CREATE OPERATOR <=> (FUNCTION = op_valid_dist);`,
ExpectedErr: "operator argument types must be specified",
},
{
Query: `CREATE OPERATOR <=> (LEFTARG = int4, FUNCTION = op_valid_dist);`,
ExpectedErr: "operator right argument type must be specified",
},
{
Query: `CREATE OPERATOR <=> (LEFTARG = int4, RIGHTARG = int4, FUNCTION = op_missing_fn);`,
ExpectedErr: "function op_missing_fn(integer, integer) does not exist",
},
{
Query: `CREATE OPERATOR <=> (LEFTARG = int4, RIGHTARG = int4, FUNCTION = op_one_arg);`,
ExpectedErr: "function op_one_arg(integer, integer) does not exist",
},
{
Query: `CREATE OPERATOR <-> (LEFTARG = int4, RIGHTARG = int4, FUNCTION = op_valid_dist);`,
ExpectedErr: "operator <-> already exists",
},
},
},
})
}
+316
View File
@@ -0,0 +1,316 @@
// 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 _go
import (
"strconv"
"strings"
"sync"
"testing"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core/casts"
"github.com/dolthub/doltgresql/server/extensions"
"github.com/dolthub/doltgresql/server/extensions/extdef"
pgtypes "github.com/dolthub/doltgresql/server/types"
"github.com/dolthub/doltgresql/utils"
)
func TestExtensionEmulation(t *testing.T) {
registerTestExtension()
RunScripts(t, []ScriptTest{
{
Name: "Declared types are created with their array type",
SetUpScript: []string{
"CREATE EXTENSION doltgres_test;",
},
Assertions: []ScriptTestAssertion{
{
Query: `SELECT typname, typtype, typcategory, typlen, typinput::text, typoutput::text FROM pg_type WHERE typname = 'dgtest_upper';`,
Expected: []sql.Row{
{"dgtest_upper", "b", "U", -1, "dgtest_upper_in", "dgtest_upper_out"},
},
},
{
Query: `SELECT typname, typtype, typcategory FROM pg_type WHERE typname = '_dgtest_upper';`,
Expected: []sql.Row{{"_dgtest_upper", "b", "A"}},
},
{
Query: "SELECT 'abc'::dgtest_upper;",
Expected: []sql.Row{{"ABC"}},
},
{
Query: "SELECT 'MiXeD'::dgtest_upper;",
Expected: []sql.Row{{"MIXED"}},
},
},
},
{
Name: "Declared operators resolve for their operand types",
SetUpScript: []string{
"CREATE EXTENSION doltgres_test;",
},
Assertions: []ScriptTestAssertion{
{
Query: "SELECT 'abc'::dgtest_upper = 'ABC'::dgtest_upper;",
Expected: []sql.Row{{"t"}},
},
{
Query: "SELECT 'abc'::dgtest_upper = 'xyz'::dgtest_upper;",
Expected: []sql.Row{{"f"}},
},
{
Query: `SELECT oprname, oprkind, oprcanhash, oprcanmerge FROM pg_operator WHERE oprcode::text = 'dgtest_upper_eq';`,
Expected: []sql.Row{{"=", "b", "t", "t"}},
},
{ // A symbol that no built-in operator resolves through the same path
Query: "SELECT 'abc'::dgtest_upper <-> 'wxyz'::dgtest_upper;",
Expected: []sql.Row{{1}},
},
{
Query: "SELECT 'abc'::dgtest_upper <-> 'xyz'::dgtest_upper;",
Expected: []sql.Row{{0}},
},
},
},
{
Name: "Declared casts convert to their target type",
SetUpScript: []string{
"CREATE EXTENSION doltgres_test;",
},
Assertions: []ScriptTestAssertion{
{
Query: "SELECT ('abc'::dgtest_upper)::text || 'def';",
Expected: []sql.Row{{"ABCdef"}},
},
{
Query: "SELECT length(('abcd'::dgtest_upper)::text);",
Expected: []sql.Row{{4}},
},
},
},
{
Name: "Declared aggregates accumulate through their transition and final functions",
SetUpScript: []string{
"CREATE EXTENSION doltgres_test;",
"CREATE TABLE t1 (pk INTEGER PRIMARY KEY, v1 TEXT);",
"INSERT INTO t1 VALUES (1, 'ab'), (2, 'cde'), (3, 'f');",
},
Assertions: []ScriptTestAssertion{
{
Query: "SELECT dgtest_charcount(v1) FROM t1;",
Expected: []sql.Row{{"6"}},
},
{
Query: "SELECT dgtest_charcount(v1) FROM t1 WHERE pk = 2;",
Expected: []sql.Row{{"3"}},
},
{ // The initial condition is the state when the aggregate sees no rows at all
Query: "SELECT dgtest_charcount(v1) FROM t1 WHERE pk = 0;",
Expected: []sql.Row{{"0"}},
},
{
Query: `SELECT aggkind, agginitval, aggcombinefn::text, aggfinalfn::text FROM pg_aggregate WHERE aggtransfn::text = 'dgtest_charcount_transition';`,
Expected: []sql.Row{{"n", "0", "dgtest_charcount_combine", "dgtest_charcount_final"}},
},
},
},
{
Name: "Objects are created in the extension's target schema",
SetUpScript: []string{
"CREATE EXTENSION doltgres_test;",
},
Assertions: []ScriptTestAssertion{
{
Query: `SELECT n.nspname FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace WHERE t.typname = 'dgtest_upper';`,
Expected: []sql.Row{{"public"}},
},
{
Query: "SELECT public.dgtest_upper_text('abc'::public.dgtest_upper);",
Expected: []sql.Row{{"ABC"}},
},
},
},
{
Name: "Extension objects are not available before the extension is created",
Assertions: []ScriptTestAssertion{
{
Query: "SELECT 'abc'::dgtest_upper;",
ExpectedErr: "unable to resolve type",
},
{
Query: "SELECT dgtest_charcount('abc');",
ExpectedErr: "not found",
},
{
Query: "CREATE EXTENSION doltgres_test;",
Expected: []sql.Row{},
},
{
Query: "SELECT dgtest_charcount('abc');",
Expected: []sql.Row{{"3"}},
},
},
},
})
}
// registerExtensionOnce ensures that registerTestExtension only runs once.
var registerExtensionOnce sync.Once
// registerTestExtension registers a dummy extension for testing, which declares one of every kind of object. Every test
// whose expected output includes the dummy extension must call this first.
func registerTestExtension() {
registerExtensionOnce.Do(func() {
extensions.Register(&extdef.Extension{
Name: "doltgres_test",
Control: extdef.Control{
DefaultVersion: "1.0",
Comment: "test extension to ensure that emulated extensions behave properly",
Relocatable: true,
},
Types: []extdef.Type{
{
Name: "dgtest_upper",
Definition: pgtypes.NewBaseTypeDefinition(),
Input: "dgtest_upper_in",
Output: "dgtest_upper_out",
},
},
Routines: []extdef.Routine{
{
Name: "dgtest_upper_in",
Symbol: "dgtest_upper_in",
Parameters: []extdef.Parameter{{Type: "cstring"}},
Returns: "dgtest_upper",
Strict: true,
Impl: func(ctx *sql.Context, args ...any) (any, error) {
return strings.ToUpper(args[0].(string)), nil
},
},
{
Name: "dgtest_upper_out",
Symbol: "dgtest_upper_out",
Parameters: []extdef.Parameter{{Type: "dgtest_upper"}},
Returns: "cstring",
Strict: true,
Impl: func(ctx *sql.Context, args ...any) (any, error) {
return args[0].(string), nil
},
},
{
Name: "dgtest_upper_eq",
Symbol: "dgtest_upper_eq",
Parameters: []extdef.Parameter{{Type: "dgtest_upper"}, {Type: "dgtest_upper"}},
Returns: "bool",
Strict: true,
Impl: func(ctx *sql.Context, args ...any) (any, error) {
return args[0].(string) == args[1].(string), nil
},
},
{
Name: "dgtest_upper_text",
Symbol: "dgtest_upper_text",
Parameters: []extdef.Parameter{{Type: "dgtest_upper"}},
Returns: "text",
Strict: true,
Impl: func(ctx *sql.Context, args ...any) (any, error) {
return args[0].(string), nil
},
},
{
Name: "dgtest_upper_distance",
Symbol: "dgtest_upper_distance",
Parameters: []extdef.Parameter{{Type: "dgtest_upper"}, {Type: "dgtest_upper"}},
Returns: "int4",
Strict: true,
Impl: func(ctx *sql.Context, args ...any) (any, error) {
return int32(utils.Abs(len(args[0].(string)) - len(args[1].(string)))), nil
},
},
{
Name: "dgtest_charcount_transition",
Symbol: "dgtest_charcount_transition",
Parameters: []extdef.Parameter{{Type: "int4"}, {Type: "text"}},
Returns: "int4",
Strict: true,
Impl: func(ctx *sql.Context, args ...any) (any, error) {
return args[0].(int32) + int32(len([]rune(args[1].(string)))), nil
},
},
{
Name: "dgtest_charcount_combine",
Symbol: "dgtest_charcount_combine",
Parameters: []extdef.Parameter{{Type: "int4"}, {Type: "int4"}},
Returns: "int4",
Strict: true,
Impl: func(ctx *sql.Context, args ...any) (any, error) {
return args[0].(int32) + args[1].(int32), nil
},
},
{
Name: "dgtest_charcount_final",
Symbol: "dgtest_charcount_final",
Parameters: []extdef.Parameter{{Type: "int4"}},
Returns: "text",
Strict: true,
Impl: func(ctx *sql.Context, args ...any) (any, error) {
return strconv.Itoa(int(args[0].(int32))), nil
},
},
},
Operators: []extdef.Operator{
{
Symbol: "=",
Left: "dgtest_upper",
Right: "dgtest_upper",
Routine: "dgtest_upper_eq",
Commutator: "=",
Hashes: true,
Merges: true,
},
{
Symbol: "<->",
Left: "dgtest_upper",
Right: "dgtest_upper",
Routine: "dgtest_upper_distance",
Commutator: "<->",
},
},
Casts: []extdef.Cast{
{
Source: "dgtest_upper",
Target: "text",
Routine: "dgtest_upper_text",
CastType: casts.CastType_Assignment,
},
},
Aggregates: []extdef.Aggregate{
{
Name: "dgtest_charcount",
Parameters: []extdef.Parameter{{Type: "text"}},
Returns: "text",
StateType: "int4",
Transition: "dgtest_charcount_transition",
Final: "dgtest_charcount_final",
Combine: "dgtest_charcount_combine",
InitCond: "0",
HasInitCond: true,
},
},
})
})
}
+4
View File
@@ -435,6 +435,7 @@ func TestPgAuthid(t *testing.T) {
}
func TestPgAvailableExtensionVersions(t *testing.T) {
registerTestExtension()
RunScripts(t, []ScriptTest{
{
Name: "pg_available_extension_versions",
@@ -447,6 +448,7 @@ func TestPgAvailableExtensionVersions(t *testing.T) {
{
Query: `SELECT name, version, installed, superuser, trusted, relocatable, schema, requires, comment FROM "pg_catalog"."pg_available_extension_versions" ORDER BY name;`,
Expected: []sql.Row{
{"doltgres_test", "1.0", "f", "f", "f", "t", nil, nil, "test extension to ensure that emulated extensions behave properly"},
{"uuid-ossp", "1.1", "f", "t", "t", "t", nil, nil, "generate universally unique identifiers (UUIDs)"},
},
},
@@ -484,6 +486,7 @@ func TestPgAvailableExtensionVersions(t *testing.T) {
}
func TestPgAvailableExtensions(t *testing.T) {
registerTestExtension()
RunScripts(t, []ScriptTest{
{
Name: "pg_available_extensions",
@@ -496,6 +499,7 @@ func TestPgAvailableExtensions(t *testing.T) {
{
Query: `SELECT name, default_version, installed_version, comment FROM "pg_catalog"."pg_available_extensions" ORDER BY name;`,
Expected: []sql.Row{
{"doltgres_test", "1.0", nil, "test extension to ensure that emulated extensions behave properly"},
{"uuid-ossp", "1.1", nil, "generate universally unique identifiers (UUIDs)"},
},
},