Files
dolthub--doltgresql/server/plpgsql/statements.go
T
2026-07-13 12:32:25 +08:00

506 lines
14 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"
"strings"
"github.com/dolthub/go-mysql-server/sql"
"github.com/cockroachdb/errors"
pg_query "github.com/dolthub/pg_query_go/v6"
)
// Statement represents a PL/pgSQL statement.
type Statement interface {
// OperationSize reports the number of operations that the statement will convert to.
OperationSize() int32
// AppendOperations adds the statement to the operation slice.
AppendOperations(ops *[]InterpreterOperation, stack *InterpreterStack) error
}
// Assignment represents an assignment statement.
type Assignment struct {
VariableName string
Expression string
VariableIndex int32 // TODO: figure out what this is used for, probably to get around shadowed variables?
}
var _ Statement = Assignment{}
// OperationSize implements the interface Statement.
func (Assignment) OperationSize() int32 {
return 1
}
// AppendOperations implements the interface Statement.
func (stmt Assignment) AppendOperations(ops *[]InterpreterOperation, stack *InterpreterStack) error {
expression, referencedVariables, err := substituteVariableReferences(stmt.Expression, stack)
if err != nil {
return err
}
*ops = append(*ops, InterpreterOperation{
OpCode: OpCode_Assign,
PrimaryData: "SELECT " + expression + ";",
SecondaryData: referencedVariables,
Target: stmt.VariableName,
})
return nil
}
// Block contains a collection of statements, alongside the variables that were declared for the block. Only the
// top-level block will contain parameter variables.
type Block struct {
TriggerNew int32 // When non-zero, indicates that the NEW record exists for use with triggers
TriggerOld int32 // When non-zero, indicates that the OLD record exists for use with triggers
Variables []Variable
Records []Record
Body []Statement
Label string
IsLoop bool
}
var _ Statement = Block{}
// OperationSize implements the interface Statement.
func (stmt Block) OperationSize() int32 {
total := int32(2) // We start with 2 since we'll have ScopeBegin and ScopeEnd
for _, variable := range stmt.Variables {
if !variable.IsParameter {
total++
}
}
for _, innerStmt := range stmt.Body {
total += innerStmt.OperationSize()
}
return total
}
// AppendOperations implements the interface Statement.
func (stmt Block) AppendOperations(ops *[]InterpreterOperation, stack *InterpreterStack) error {
stack.PushScope()
stack.SetLabel(stmt.Label) // If the label is empty, then this won't change anything
var loop string
if stmt.IsLoop {
loop = "_"
// All loops need a label, so we'll make an anonymous one if an explicit one hasn't been given
if len(stmt.Label) == 0 {
stack.SetAnonymousLabel()
stmt.Label = stack.GetCurrentLabel()
}
}
*ops = append(*ops, InterpreterOperation{
OpCode: OpCode_ScopeBegin,
PrimaryData: stmt.Label,
Target: loop,
})
for _, variable := range stmt.Variables {
op := InterpreterOperation{
OpCode: OpCode_Declare,
PrimaryData: variable.Type,
Target: variable.Name,
}
var val any
if variable.Default != "" {
op.SecondaryData = []string{variable.Default}
val = variable.Default
}
if !variable.IsParameter {
*ops = append(*ops, op)
}
stack.NewVariableWithValue(variable.Name, nil, val)
}
for _, record := range stmt.Records {
var fakeSch sql.Schema
for _, fieldName := range record.Fields {
fakeSch = append(fakeSch, &sql.Column{Name: fieldName})
}
stack.NewRecord(record.Name, fakeSch, nil)
}
for _, innerStmt := range stmt.Body {
if err := innerStmt.AppendOperations(ops, stack); err != nil {
return err
}
}
*ops = append(*ops, InterpreterOperation{
OpCode: OpCode_ScopeEnd,
})
stack.PopScope()
return nil
}
// ExecuteSQL represents a standard SQL statement's execution (including the INTO syntax).
type ExecuteSQL struct {
Statement string
Target string
}
var _ Statement = ExecuteSQL{}
// OperationSize implements the interface Statement.
func (ExecuteSQL) OperationSize() int32 {
return 1
}
// AppendOperations implements the interface Statement.
func (stmt ExecuteSQL) AppendOperations(ops *[]InterpreterOperation, stack *InterpreterStack) error {
statementStr, referencedVariables, err := substituteVariableReferences(stmt.Statement, stack)
if err != nil {
return err
}
*ops = append(*ops, InterpreterOperation{
OpCode: OpCode_Execute,
PrimaryData: statementStr,
SecondaryData: referencedVariables,
Target: stmt.Target,
})
return nil
}
// DynamicExecute represents a dynamic SQL statement's execution.
type DynamicExecute struct {
Query string
Params []string
Target string
}
var _ Statement = DynamicExecute{}
// OperationSize implements the interface Statement.
func (DynamicExecute) OperationSize() int32 {
return 1
}
// AppendOperations implements the interface Statement.
func (stmt DynamicExecute) AppendOperations(ops *[]InterpreterOperation, stack *InterpreterStack) error {
*ops = append(*ops, InterpreterOperation{
OpCode: OpCode_Execute,
PrimaryData: stmt.Query,
SecondaryData: stmt.Params,
Target: stmt.Target,
})
return nil
}
// ForQueryInit executes a SQL query and stores the result set in a named cursor on the stack.
// It is the first operation emitted for a FOR record IN query LOOP statement.
type ForQueryInit struct {
CursorName string
Query string
}
var _ Statement = ForQueryInit{}
// OperationSize implements the interface Statement.
func (ForQueryInit) OperationSize() int32 {
return 1
}
// AppendOperations implements the interface Statement.
func (stmt ForQueryInit) AppendOperations(ops *[]InterpreterOperation, stack *InterpreterStack) error {
queryStr, referencedVariables, err := substituteVariableReferences(stmt.Query, stack)
if err != nil {
return err
}
*ops = append(*ops, InterpreterOperation{
OpCode: OpCode_ForQueryInit,
PrimaryData: queryStr,
SecondaryData: referencedVariables,
Target: stmt.CursorName,
})
return nil
}
// ForQueryNext fetches the next row from a named cursor and assigns it to a record variable.
// When the cursor is exhausted it jumps forward by GotoOffset (like an If), exiting the loop.
type ForQueryNext struct {
CursorName string
RecordVar string
GotoOffset int32
}
var _ Statement = ForQueryNext{}
// OperationSize implements the interface Statement.
func (ForQueryNext) OperationSize() int32 {
return 1
}
// AppendOperations implements the interface Statement.
func (stmt ForQueryNext) AppendOperations(ops *[]InterpreterOperation, stack *InterpreterStack) error {
*ops = append(*ops, InterpreterOperation{
OpCode: OpCode_ForQueryNext,
PrimaryData: stmt.CursorName,
Target: stmt.RecordVar,
Index: len(*ops) + int(stmt.GotoOffset),
})
return nil
}
// Goto jumps to the counter at the given offset.
type Goto struct {
Offset int32
Label string
NearestScopeOp bool
}
var _ Statement = Goto{}
// OperationSize implements the interface Statement.
func (Goto) OperationSize() int32 {
return 1
}
// AppendOperations implements the interface Statement.
func (stmt Goto) AppendOperations(ops *[]InterpreterOperation, stack *InterpreterStack) error {
if len(stmt.Label) > 0 {
*ops = append(*ops, InterpreterOperation{
OpCode: OpCode_Goto,
PrimaryData: stmt.Label,
Index: int(stmt.Offset),
})
} else if stmt.NearestScopeOp {
label := stack.GetCurrentLabel()
if len(label) == 0 {
if stmt.Offset > 0 {
return errors.New("EXIT cannot be used outside a loop, unless it has a label")
} else {
return errors.New("CONTINUE cannot be used outside a loop")
}
}
*ops = append(*ops, InterpreterOperation{
OpCode: OpCode_Goto,
PrimaryData: label,
Index: int(stmt.Offset),
})
} else {
*ops = append(*ops, InterpreterOperation{
OpCode: OpCode_Goto,
Index: len(*ops) + int(stmt.Offset),
})
}
return nil
}
// If represents an IF condition, alongside its Goto offset if the condition is true.
type If struct {
Condition string
GotoOffset int32
}
var _ Statement = If{}
// OperationSize implements the interface Statement.
func (If) OperationSize() int32 {
return 1
}
// AppendOperations implements the interface Statement.
func (stmt If) AppendOperations(ops *[]InterpreterOperation, stack *InterpreterStack) error {
condition, referencedVariables, err := substituteVariableReferences(stmt.Condition, stack)
if err != nil {
return err
}
*ops = append(*ops, InterpreterOperation{
OpCode: OpCode_If,
PrimaryData: "SELECT " + condition + ";",
SecondaryData: referencedVariables,
Index: len(*ops) + int(stmt.GotoOffset),
})
return nil
}
// Perform represents a PERFORM statement.
type Perform struct {
Statement string
}
var _ Statement = Perform{}
// OperationSize implements the interface Statement.
func (Perform) OperationSize() int32 {
return 1
}
// AppendOperations implements the interface Statement.
func (stmt Perform) AppendOperations(ops *[]InterpreterOperation, stack *InterpreterStack) error {
statementStr, referencedVariables, err := substituteVariableReferences(stmt.Statement, stack)
if err != nil {
return err
}
*ops = append(*ops, InterpreterOperation{
OpCode: OpCode_Perform,
PrimaryData: statementStr,
SecondaryData: referencedVariables,
})
return nil
}
// Raise represents a RAISE statement
type Raise struct {
Level string
Message string
Params []string
Options map[string]string
}
var _ Statement = Raise{}
// OperationSize implements the interface Statement.
func (r Raise) OperationSize() int32 {
return 1
}
// AppendOperations implements the interface Statement.
func (r Raise) AppendOperations(ops *[]InterpreterOperation, _ *InterpreterStack) error {
*ops = append(*ops, InterpreterOperation{
OpCode: OpCode_Raise,
PrimaryData: r.Level,
SecondaryData: append([]string{r.Message}, r.Params...),
Options: r.Options,
})
return nil
}
// Record represents a record (along with known fields for future access). These are exclusively found within Block.
type Record struct {
Name string
Fields []string
}
// ReturnQuery represents a RETURN QUERY statement.
type ReturnQuery struct {
Query string
}
var _ Statement = ReturnQuery{}
// OperationSize implements the interface Statement.
func (r ReturnQuery) OperationSize() int32 {
return 1
}
// AppendOperations implements the interface Statement.
func (r ReturnQuery) AppendOperations(ops *[]InterpreterOperation, stack *InterpreterStack) error {
query, referencedVariables, err := substituteVariableReferences(r.Query, stack)
if err != nil {
return err
}
*ops = append(*ops, InterpreterOperation{
OpCode: OpCode_ReturnQuery,
PrimaryData: query,
SecondaryData: referencedVariables,
})
return nil
}
// Return represents a RETURN statement.
type Return struct {
Expression string
}
var _ Statement = Return{}
// OperationSize implements the interface Statement.
func (Return) OperationSize() int32 {
return 1
}
// AppendOperations implements the interface Statement.
func (stmt Return) AppendOperations(ops *[]InterpreterOperation, stack *InterpreterStack) error {
expression, referencedVariables, err := substituteVariableReferences(stmt.Expression, stack)
if err != nil {
return err
}
if len(expression) > 0 {
expression = "SELECT " + expression + ";"
}
*ops = append(*ops, InterpreterOperation{
OpCode: OpCode_Return,
PrimaryData: expression,
SecondaryData: referencedVariables,
})
return nil
}
// Variable represents a variable. These are exclusively found within Block.
type Variable struct {
Name string
Type string
IsParameter bool
Default string
}
// OperationSizeForStatements returns the sum of OperationSize for every statement.
func OperationSizeForStatements(stmts []Statement) int32 {
total := int32(0)
for _, stmt := range stmts {
total += stmt.OperationSize()
}
return total
}
// substituteVariableReferences parses the specified |expression| and replaces
// any token that matches a variable name in the |stack| with "$N", where N
// indicates which variable in the returned |referenceVars| slice is used.
func substituteVariableReferences(expression string, stack *InterpreterStack) (newExpression string, referencedVars []string, err error) {
scanResult, err := pg_query.Scan(expression)
if err != nil {
return "", nil, err
}
varMap := stack.ListVariables()
for i := 0; i < len(scanResult.Tokens); i++ {
token := scanResult.Tokens[i]
substring := expression[token.Start:token.End]
// varMap lowercases everything, so we'll lowercase our substring to enable case-insensitivity
isAfterDot := i > 0 && scanResult.Tokens[i-1].Token == '.'
if !isAfterDot {
if _, ok := varMap[strings.ToLower(substring)]; ok {
// If there's a '.', then we'll assume this is accessing a record's field (`NEW.val1` for example)
for i+2 < len(scanResult.Tokens) && scanResult.Tokens[i+1].Token == '.' {
nextFieldSubstring := expression[scanResult.Tokens[i+2].Start:scanResult.Tokens[i+2].End]
substring += "." + nextFieldSubstring
i += 2
}
// Variables cannot have a '(' after their name as that would classify them as functions, so we have to
// explicitly check for that. This is because variables and functions can share names, for example:
// SELECT COUNT(*) INTO count FROM table_name;
if i+1 >= len(scanResult.Tokens) || scanResult.Tokens[i+1].Token != '(' {
referencedVars = append(referencedVars, substring)
newExpression += fmt.Sprintf("$%d ", len(referencedVars))
} else {
newExpression += substring + " "
}
} else if _, ok := triggerSpecialVariables[substring]; ok {
referencedVars = append(referencedVars, substring)
newExpression += fmt.Sprintf("$%d ", len(referencedVars))
} else {
newExpression += substring + " "
}
} else {
newExpression += substring + " "
}
}
return newExpression, referencedVars, nil
}