Files
2026-07-13 12:32:25 +08:00

241 lines
8.7 KiB
Go

// Copyright 2025 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package framework
import (
"fmt"
"strconv"
"strings"
"github.com/cockroachdb/errors"
"github.com/dolthub/go-mysql-server/sql"
"github.com/lib/pq"
"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/server/plpgsql"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// InterpretedFunction is the implementation of functions created using PL/pgSQL. The created functions are converted to
// a collection of operations, and an interpreter iterates over those operations to handle the logic.
type InterpretedFunction struct {
ID id.Function
ReturnType *pgtypes.DoltgresType
ParameterNames []string
ParameterTypes []*pgtypes.DoltgresType
Variadic bool
IsNonDeterministic bool
Strict bool
SRF bool
Statements []plpgsql.InterpreterOperation
}
var _ FunctionInterface = InterpretedFunction{}
var _ plpgsql.InterpretedFunction = InterpretedFunction{}
// GetExpectedParameterCount implements the interface FunctionInterface.
func (iFunc InterpretedFunction) GetExpectedParameterCount() int {
return len(iFunc.ParameterTypes)
}
// GetName implements the interface FunctionInterface.
func (iFunc InterpretedFunction) GetName() string {
return iFunc.ID.FunctionName()
}
// GetParameters implements the interface FunctionInterface.
func (iFunc InterpretedFunction) GetParameters() []*pgtypes.DoltgresType {
return iFunc.ParameterTypes
}
// GetParameterNames returns the names of all parameters.
func (iFunc InterpretedFunction) GetParameterNames() []string {
return iFunc.ParameterNames
}
// GetReturn implements the interface FunctionInterface.
func (iFunc InterpretedFunction) GetReturn() *pgtypes.DoltgresType {
return iFunc.ReturnType
}
// GetStatements returns the contained statements.
func (iFunc InterpretedFunction) GetStatements() []plpgsql.InterpreterOperation {
return iFunc.Statements
}
// InternalID implements the interface FunctionInterface.
func (iFunc InterpretedFunction) InternalID() id.Id {
return iFunc.ID.AsId()
}
// IsStrict implements the interface FunctionInterface.
func (iFunc InterpretedFunction) IsStrict() bool {
return iFunc.Strict
}
// IsSRF implements the interface FunctionInterface.
func (iFunc InterpretedFunction) IsSRF() bool {
return iFunc.SRF
}
// NonDeterministic implements the interface FunctionInterface.
func (iFunc InterpretedFunction) NonDeterministic() bool {
return iFunc.IsNonDeterministic
}
// IsCVariadic implements the FunctionInterface interface.
func (iFunc InterpretedFunction) IsCVariadic() bool {
return false
}
// VariadicIndex implements the interface FunctionInterface.
func (iFunc InterpretedFunction) VariadicIndex() int {
// TODO: implement variadic
return -1
}
// QuerySingleReturn handles queries that are supposed to return a single value.
func (iFunc InterpretedFunction) QuerySingleReturn(ctx *sql.Context, stack plpgsql.InterpreterStack, stmt string, targetType *pgtypes.DoltgresType, bindings []string) (val any, err error) {
stmt, _, err = iFunc.ApplyBindings(ctx, stack, stmt, bindings, true)
if err != nil {
return nil, err
}
return sql.RunInterpreted(ctx, func(subCtx *sql.Context) (any, error) {
sch, rowIter, _, err := stack.Runner().QueryWithBindings(subCtx, stmt, nil, nil, nil)
if err != nil {
return nil, err
}
rows, err := sql.RowIterToRows(subCtx, rowIter)
if err != nil {
return nil, err
}
if len(sch) != 1 {
return nil, errors.New("expression does not result in a single value")
}
if len(rows) != 1 {
return nil, errors.New("expression returned multiple result sets")
}
if len(rows[0]) != 1 {
return nil, errors.New("expression returned multiple results")
}
if targetType == nil {
return rows[0][0], nil
}
if rows[0][0] == nil {
return nil, nil
}
sourceType, ok := sch[0].Type.(*pgtypes.DoltgresType)
if !ok {
// TODO: We ensure we have a DoltgresType, but we should also convert the value to
// ensure it's in the correct form for the DoltgresType. This logic lives in
// pgexpressions.GMSCast, but need to be extracted to avoid a dependency cycle
// so it can be used here and from server.plpgsql.
sourceType, err = pgtypes.FromGmsTypeToDoltgresType(sch[0].Type)
if err != nil {
return nil, err
}
}
castsColl, err := core.GetCastsCollectionFromContext(ctx, "")
if err != nil {
return nil, err
}
cast, err := castsColl.GetAssignmentCast(ctx, sourceType, targetType)
if err != nil {
return nil, err
}
if !cast.ID.IsValid() {
// TODO: We're using assignment casting, but for some reason we have to use I/O casting here, which is incorrect?
// We need to dig into this and figure out exactly what's happening, as this is "wrong" according to what
// I understand. This lines up more with explicit casting, but it's supposed to be assignment.
// Maybe there are specific rules for pgsql?
if sourceType.TypCategory == pgtypes.TypeCategory_StringTypes {
cast.ID = id.NewCast(sourceType.ID, targetType.ID)
cast.UseInOut = true
} else {
return nil, errors.New("no valid cast for return value")
}
}
return cast.Eval(subCtx, rows[0][0], sourceType, targetType)
})
}
// QueryMultiReturn handles queries that may return multiple values over multiple rows.
func (iFunc InterpretedFunction) QueryMultiReturn(ctx *sql.Context, stack plpgsql.InterpreterStack, stmt string, bindings []string) (schema sql.Schema, rows []sql.Row, err error) {
stmt, _, err = iFunc.ApplyBindings(ctx, stack, stmt, bindings, true)
if err != nil {
return nil, nil, err
}
rows, err = sql.RunInterpreted(ctx, func(subCtx *sql.Context) (rows []sql.Row, err error) {
var rowIter sql.RowIter
schema, rowIter, _, err = stack.Runner().QueryWithBindings(subCtx, stmt, nil, nil, nil)
if err != nil {
return nil, err
}
// TODO: we should come up with a good way of carrying the RowIter out of the function without needing to wrap
// each call to QueryMultiReturn with RunInterpreted. For now, we don't check the returned rows, so this is
// fine.
return sql.RowIterToRows(subCtx, rowIter)
})
return schema, rows, err
}
// ApplyBindings applies the given bindings to the statement. If `varFound` is false, then the error will state that
// the variable was not found (which means the error may be ignored if you're only concerned with finding a variable).
// If `varFound` is true, then the error is related to formatting the variable. `enforceType` adds casting and quotes to
// ensure that the value is correctly represented in the string.
func (InterpretedFunction) ApplyBindings(ctx *sql.Context, stack plpgsql.InterpreterStack, stmt string, bindings []string, enforceType bool) (newStmt string, varFound bool, err error) {
if len(bindings) == 0 {
return stmt, false, nil
}
newStmt = stmt
for i, bindingName := range bindings {
variable := stack.GetVariable(bindingName)
if variable.Type == nil {
return newStmt, false, fmt.Errorf("variable `%s` could not be found", bindingName)
}
var formattedVar string
if *variable.Value != nil {
formattedVar, err = variable.Type.FormatValueWithContext(ctx, *variable.Value)
if err != nil {
return newStmt, true, err
}
if enforceType {
switch variable.Type.TypCategory {
case pgtypes.TypeCategory_ArrayTypes, pgtypes.TypeCategory_CompositeTypes, pgtypes.TypeCategory_DateTimeTypes, pgtypes.TypeCategory_StringTypes, pgtypes.TypeCategory_UserDefinedTypes:
formattedVar = pq.QuoteLiteral(formattedVar)
}
}
} else {
formattedVar = "NULL"
}
if enforceType {
if variable.Type.TypCategory == pgtypes.TypeCategory_CompositeTypes {
newStmt = strings.Replace(newStmt, "$"+strconv.Itoa(i+1), fmt.Sprintf(`(%s::%s)`, formattedVar, variable.Type.String()), 1)
} else {
newStmt = strings.Replace(newStmt, "$"+strconv.Itoa(i+1), fmt.Sprintf(`((%s)::%s)`, formattedVar, variable.Type.String()), 1)
}
} else {
newStmt = strings.Replace(newStmt, "$"+strconv.Itoa(i+1), formattedVar, 1)
}
}
return newStmt, true, nil
}
// enforceInterfaceInheritance implements the interface FunctionInterface.
func (iFunc InterpretedFunction) enforceInterfaceInheritance(error) {}