506 lines
14 KiB
Go
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
|
|
}
|