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

505 lines
17 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 plpgsql
import (
"fmt"
"strconv"
"strings"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess"
"github.com/dolthub/go-mysql-server/sql"
"github.com/jackc/pgx/v5/pgproto3"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/typecollection"
"github.com/dolthub/doltgresql/postgres/parser/types"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// InterpretedFunction is an interface that essentially mirrors the implementation of InterpretedFunction in the
// framework package.
type InterpretedFunction interface {
ApplyBindings(ctx *sql.Context, stack InterpreterStack, stmt string, bindings []string, enforceType bool) (newStmt string, varFound bool, err error)
GetParameters() []*pgtypes.DoltgresType
GetParameterNames() []string
GetReturn() *pgtypes.DoltgresType
GetStatements() []InterpreterOperation
QueryMultiReturn(ctx *sql.Context, stack InterpreterStack, stmt string, bindings []string) (schema sql.Schema, rows []sql.Row, err error)
QuerySingleReturn(ctx *sql.Context, stack InterpreterStack, stmt string, targetType *pgtypes.DoltgresType, bindings []string) (val any, err error)
// IsSRF returns whether the function is a set returning function, meaning whether the
// function returns one or more rows as a result.
IsSRF() bool
}
// GetTypesCollectionFromContext is declared within the core package, but is assigned to this variable to work around
// import cycles.
var GetTypesCollectionFromContext func(ctx *sql.Context, database string) (*typecollection.TypeCollection, error)
// Call runs the contained operations on the given runner.
func Call(ctx *sql.Context, iFunc InterpretedFunction, runner sql.StatementRunner, paramsAndReturn []*pgtypes.DoltgresType, vals []any) (any, error) {
// Set up the initial state of the function
stack := NewInterpreterStack(runner)
// Add the parameters
parameterTypes := iFunc.GetParameters()
parameterNames := iFunc.GetParameterNames()
if len(vals) != len(parameterTypes) {
return nil, fmt.Errorf("parameter count mismatch: expected %d got %d", len(parameterTypes), len(vals))
}
for i := range vals {
stack.NewVariableWithValue(parameterNames[i], parameterTypes[i], vals[i])
}
return call(ctx, iFunc, stack)
}
// TriggerCall runs the contained trigger operations on the given runner.
func TriggerCall(ctx *sql.Context, iFunc InterpretedFunction, runner sql.StatementRunner, sch sql.Schema, oldRow sql.Row, newRow sql.Row, trigVars map[string]any) (any, error) {
// Set up the initial state of the function
stack := NewInterpreterStack(runner)
// Add the special variables
stack.NewRecord("OLD", sch, oldRow)
stack.NewRecord("NEW", sch, newRow)
for varName, val := range trigVars {
varType, ok := triggerSpecialVariables[varName]
if !ok {
return nil, fmt.Errorf("unknown variable %s for trigger", varName)
}
stack.NewVariableWithValue(varName, varType, val)
}
return call(ctx, iFunc, stack)
}
// call runs the contained operations on the given runner.
func call(ctx *sql.Context, iFunc InterpretedFunction, stack InterpreterStack) (any, error) {
// We increment before accessing, so start at -1
counter := -1
// Run the statements
statements := iFunc.GetStatements()
for {
counter++
if counter >= len(statements) {
break
} else if counter < 0 {
panic("negative function counter")
}
operation := statements[counter]
switch operation.OpCode {
case OpCode_Alias:
iv := stack.GetVariable(operation.PrimaryData)
if iv.Type == nil {
return nil, fmt.Errorf("variable `%s` could not be found", operation.PrimaryData)
}
stack.NewVariableAlias(operation.Target, operation.PrimaryData)
case OpCode_Assign:
iv := stack.GetVariable(operation.Target)
if iv.Type == nil {
return nil, fmt.Errorf("variable `%s` could not be found", operation.Target)
}
retVal, err := iFunc.QuerySingleReturn(ctx, stack, operation.PrimaryData, iv.Type, operation.SecondaryData)
if err != nil {
return nil, err
}
err = stack.SetVariable(ctx, operation.Target, retVal)
if err != nil {
return nil, err
}
case OpCode_Declare:
typeCollection, err := GetTypesCollectionFromContext(ctx, "")
if err != nil {
return nil, err
}
// pg_query_go sets PrimaryData for implicit CASE statement variables to
// `pg_catalog."integer"`, so we remove double-quotes and extract the schema name.
typeName := operation.PrimaryData
typeName = strings.ReplaceAll(typeName, `"`, "")
schemaName := "pg_catalog"
if strings.Contains(typeName, ".") {
parts := strings.Split(typeName, ".")
schemaName = parts[0]
typeName = parts[1]
// Check the NonKeyword type names to see if we're looking at
// an alias of a type if we're in the pg_catalog schema.
// Skip array types (names starting with "_") since their internal
// lookup key uses the "_typename" form, not the "typename[]" form
// that TypeForNonKeywordTypeName returns.
if schemaName == "pg_catalog" && !strings.HasPrefix(typeName, "_") {
typ, ok, _ := types.TypeForNonKeywordTypeName(typeName)
if ok && typ != nil {
typeName = typ.Name()
}
}
}
resolvedType, err := typeCollection.GetType(ctx, id.NewType(schemaName, typeName))
if err != nil {
return nil, err
}
if resolvedType == nil {
return nil, pgtypes.ErrTypeDoesNotExist.New(operation.PrimaryData)
}
if len(operation.SecondaryData) != 0 {
defVal := operation.SecondaryData[0]
// Default value can be a literal value or a reference to parameter
isParam := false
for _, param := range iFunc.GetParameterNames() {
if param == defVal {
isParam = true
break
}
}
if isParam {
ivr := stack.GetVariable(defVal)
if ivr.Value != nil {
stack.NewVariableWithValue(operation.Target, resolvedType, *ivr.Value)
} else {
stack.NewVariable(operation.Target, resolvedType)
}
} else {
val, err := resolvedType.IoInput(ctx, strings.Trim(operation.SecondaryData[0], "'"))
if err != nil {
return nil, err
}
stack.NewVariableWithValue(operation.Target, resolvedType, val)
}
} else {
stack.NewVariable(operation.Target, resolvedType)
}
case OpCode_DeleteInto:
// TODO: implement
case OpCode_Exception:
// TODO: implement
case OpCode_Execute:
if len(operation.Target) > 0 {
if vars := strings.Split(operation.Target, ","); len(vars) > 1 {
// multiple column row result
sch, rows, err := iFunc.QueryMultiReturn(ctx, stack, operation.PrimaryData, operation.SecondaryData)
if err != nil {
return nil, err
}
if len(rows) > 1 {
return nil, errors.New("query returned more than one row")
}
for i, row := range rows {
if len(row) != len(vars) {
return nil, errors.New("number of row values does not match number of schema columns")
}
target := stack.GetVariable(vars[i])
if target.Type == nil {
return nil, fmt.Errorf("variable `%s` could not be found", operation.Target)
}
if sch[i].Type.(*pgtypes.DoltgresType).ID != target.Type.ID {
return nil, fmt.Errorf("variable type `%s` does not match `%s`", sch[i].Type.String(), target.Type.String())
}
err = stack.SetVariable(ctx, vars[i], rows[0][i])
if err != nil {
return nil, err
}
}
} else {
// single column
target := stack.GetVariable(operation.Target)
if target.Type == nil {
return nil, fmt.Errorf("variable `%s` could not be found", operation.Target)
}
retVal, err := iFunc.QuerySingleReturn(ctx, stack, operation.PrimaryData, target.Type, operation.SecondaryData)
if err != nil {
return nil, err
}
err = stack.SetVariable(ctx, operation.Target, retVal)
if err != nil {
return nil, err
}
}
} else {
_, _, err := iFunc.QueryMultiReturn(ctx, stack, operation.PrimaryData, operation.SecondaryData)
if err != nil {
return nil, err
}
}
case OpCode_Get:
// TODO: implement
case OpCode_Goto:
// We must compare to the index - 1, so that the increment hits our target
if counter <= operation.Index {
for ; counter < operation.Index-1; counter++ {
switch statements[counter].OpCode {
case OpCode_ScopeBegin:
stack.PushScope()
case OpCode_ScopeEnd:
stack.PopScope()
}
}
} else {
for ; counter > operation.Index-1; counter-- {
switch statements[counter].OpCode {
case OpCode_ScopeBegin:
stack.PopScope()
case OpCode_ScopeEnd:
stack.PushScope()
}
}
}
case OpCode_If:
retVal, err := iFunc.QuerySingleReturn(ctx, stack, operation.PrimaryData, pgtypes.Bool, operation.SecondaryData)
if err != nil {
return nil, err
}
if retVal.(bool) {
// We're never changing the scope, so we can just assign it directly.
// Also, we must assign to index-1, so that the increment hits our target.
counter = operation.Index - 1
}
case OpCode_InsertInto:
// TODO: implement
case OpCode_Perform:
_, _, err := iFunc.QueryMultiReturn(ctx, stack, operation.PrimaryData, operation.SecondaryData)
if err != nil {
return nil, err
}
case OpCode_Raise:
// TODO: Use the client_min_messages config param to determine which
// notice levels to send to the client.
// https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-CLIENT-MIN-MESSAGES
message, err := evaluteNoticeMessage(ctx, iFunc, operation, stack)
if err != nil {
return nil, err
}
if operation.PrimaryData == "EXCEPTION" {
// TODO: Notices at the EXCEPTION level should also abort the current tx.
return nil, errors.New(message)
} else {
noticeResponse := &pgproto3.NoticeResponse{
Severity: operation.PrimaryData,
Message: message,
}
if err = applyNoticeOptions(ctx, noticeResponse, operation.Options); err != nil {
return nil, err
}
sess := dsess.DSessFromSess(ctx.Session)
sess.Notice(noticeResponse)
}
case OpCode_Return:
// If RETURN QUERY results are being buffered, return those
if len(stack.ReturnQueryResults()) > 0 {
records := stack.ReturnQueryResults()
rows := make([]sql.Row, len(records))
for i, record := range records {
rows[i] = sql.Row{record}
}
return sql.RowsToRowIter(rows...), nil
}
if len(operation.PrimaryData) == 0 {
return nil, nil
}
// TODO: handle record types properly, we'll special case triggers for now
if iFunc.GetReturn().ID == pgtypes.Trigger.ID && len(operation.SecondaryData) == 1 {
normalized := strings.ReplaceAll(strings.ToLower(operation.PrimaryData), " ", "")
if normalized == "select$1;" {
if strings.EqualFold(operation.SecondaryData[0], "new") {
return *stack.GetVariable("NEW").Value, nil
} else if strings.EqualFold(operation.SecondaryData[0], "old") {
return *stack.GetVariable("OLD").Value, nil
}
}
}
val, err := iFunc.QuerySingleReturn(ctx, stack, operation.PrimaryData, iFunc.GetReturn(), operation.SecondaryData)
if err != nil {
return nil, err
}
// If this is a set returning function, then we need to return a RowIter and wrap
// the composite value in a sql.Row.
if iFunc.IsSRF() {
return sql.RowsToRowIter(sql.Row{val}), nil
}
return val, err
case OpCode_ForQueryInit:
schema, rows, err := iFunc.QueryMultiReturn(ctx, stack, operation.PrimaryData, operation.SecondaryData)
if err != nil {
return nil, err
}
stack.InitCursor(operation.Target, schema, rows)
case OpCode_ForQueryNext:
schema, row, ok := stack.AdvanceCursor(operation.PrimaryData)
if !ok {
stack.CloseCursor(operation.PrimaryData)
// Jump forward past the loop body and back-goto, same mechanism as OpCode_If.
counter = operation.Index - 1
} else {
if err := stack.UpdateRecord(operation.Target, schema, row); err != nil {
return nil, err
}
}
case OpCode_ReturnQuery:
schema, rows, err := iFunc.QueryMultiReturn(ctx, stack, operation.PrimaryData, operation.SecondaryData)
if err != nil {
return nil, err
}
records, err := convertRowsToRecords(schema, rows)
if err != nil {
return nil, err
}
stack.BufferReturnQueryResults(records)
case OpCode_ScopeBegin:
stack.PushScope()
case OpCode_ScopeEnd:
stack.PopScope()
case OpCode_SelectInto:
// TODO: implement
case OpCode_UpdateInto:
// TODO: implement
default:
panic("unimplemented opcode")
}
}
return nil, nil
}
// convertRowsToRecords iterates overs |rows| and converts each field in each row
// into a RecordValue. |schema| is specified for type information.
func convertRowsToRecords(schema sql.Schema, rows []sql.Row) ([][]pgtypes.RecordValue, error) {
records := make([][]pgtypes.RecordValue, 0, len(rows))
for _, row := range rows {
record := make([]pgtypes.RecordValue, len(row))
for i, field := range row {
t := schema[i].Type
doltgresType, ok := t.(*pgtypes.DoltgresType)
if !ok {
// non-Doltgres types are still used in analysis, but we only support disk serialization
// for Doltgres types, so we must convert the GMS type to the nearest Doltgres type here.
// TODO: this conversion isn't fully accurate. expression.GMSCast has additional logic in
// its Eval() method to handle types more exactly and also handles converting the
// value to ensure it is well formed for the returned DoltgresType. We can't
// currently use GMSCast directly here though, because of a dependency cycle, so
// that conversion logic needs to be extracted into a package both places can import.
var err error
doltgresType, err = pgtypes.FromGmsTypeToDoltgresType(t)
if err != nil {
return nil, err
}
}
record[i] = pgtypes.RecordValue{
Value: field,
Type: doltgresType,
}
}
records = append(records, record)
}
return records, nil
}
// applyNoticeOptions adds the specified |options| to the |noticeResponse|.
func applyNoticeOptions(ctx *sql.Context, noticeResponse *pgproto3.NoticeResponse, options map[string]string) error {
for key, value := range options {
i, err := strconv.Atoi(key)
if err != nil {
return err
}
switch NoticeOptionType(i) {
case NoticeOptionTypeErrCode:
noticeResponse.Code = value
case NoticeOptionTypeMessage:
noticeResponse.Message = value
case NoticeOptionTypeDetail:
noticeResponse.Detail = value
case NoticeOptionTypeHint:
noticeResponse.Hint = value
case NoticeOptionTypeConstraint:
noticeResponse.ConstraintName = value
case NoticeOptionTypeDataType:
noticeResponse.DataTypeName = value
case NoticeOptionTypeTable:
noticeResponse.TableName = value
case NoticeOptionTypeSchema:
noticeResponse.SchemaName = value
default:
ctx.GetLogger().Warnf("unhandled notice option type: %s", key)
}
}
return nil
}
// evaluteNoticeMessage evaluates the message for a RAISE NOTICE statement, including
// evaluating any specified parameters and plugging them into the message in place of
// the % placeholders.
func evaluteNoticeMessage(ctx *sql.Context, iFunc InterpretedFunction,
operation InterpreterOperation, stack InterpreterStack) (string, error) {
message := operation.SecondaryData[0]
if len(operation.SecondaryData) > 1 {
params := operation.SecondaryData[1:]
currentParamIdx := 0
parts := strings.Split(message, "%%")
for i, part := range parts {
for strings.Contains(part, "%") {
if currentParamIdx >= len(params) {
return "", errors.New("too few parameters specified for RAISE")
}
currentParam := params[currentParamIdx]
currentParamIdx += 1
formattedVar, varFound, err := iFunc.ApplyBindings(ctx, stack, "$1", []string{currentParam}, false)
if varFound {
if err != nil {
return "", err
}
part = strings.Replace(part, "%", formattedVar, 1)
} else {
retVal, err := iFunc.QuerySingleReturn(ctx, stack, fmt.Sprintf("SELECT (%s)::text", currentParam), nil, nil)
if err != nil {
return "", err
}
stringVal := fmt.Sprintf("%v", retVal) // We should always return a string, but this is just a safety net
part = strings.Replace(part, "%", stringVal, 1)
}
}
parts[i] = part
}
if currentParamIdx < len(params) {
return "", errors.New("too many parameters specified for RAISE")
}
message = strings.Join(parts, "%")
}
return message, nil
}
// triggerSpecialVariables are the list of special variables for triggers.
// https://www.postgresql.org/docs/15/plpgsql-trigger.html
// TODO: NEW and OLD variables are handled separately using `InterpreterStack.NewRecord` function.
var triggerSpecialVariables = map[string]*pgtypes.DoltgresType{
//"NEW":
//"OLD":
"TG_NAME": pgtypes.Name,
"TG_WHEN": pgtypes.Text,
"TG_LEVEL": pgtypes.Text,
"TG_OP": pgtypes.Text,
"TG_RELID": pgtypes.Oid,
"TG_RELNAME": pgtypes.Name,
"TG_TABLE_NAME": pgtypes.Name,
"TG_TABLE_SCHEMA": pgtypes.Name,
"TG_NARGS": pgtypes.Int32,
"TG_ARGV[]": pgtypes.TextArray,
}