chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:32:25 +08:00
commit e014feafe1
2285 changed files with 1131979 additions and 0 deletions
+504
View File
@@ -0,0 +1,504 @@
// 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,
}
+57
View File
@@ -0,0 +1,57 @@
// 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
// OpCode states the operation to be performed. Most operations have a direct analogue to a Pl/pgSQL operation, however
// some exist only in Doltgres (specific to our interpreter implementation).
type OpCode uint16
const (
// New OpCode values MUST be added to the END of this list!
// Function OpCodes are persisted to disk, so these values MUST be stable across Doltgres versions.
OpCode_Alias OpCode = 0 // https://www.postgresql.org/docs/15/plpgsql-declarations.html#PLPGSQL-DECLARATION-ALIAS
OpCode_Assign OpCode = 1 // https://www.postgresql.org/docs/15/plpgsql-statements.html#PLPGSQL-STATEMENTS-ASSIGNMENT
OpCode_Case OpCode = 2 // https://www.postgresql.org/docs/15/plpgsql-control-structures.html#PLPGSQL-CONDITIONALS
OpCode_Declare OpCode = 3 // https://www.postgresql.org/docs/15/plpgsql-declarations.html
OpCode_DeleteInto OpCode = 4 // https://www.postgresql.org/docs/15/plpgsql-statements.html
OpCode_Exception OpCode = 5 // https://www.postgresql.org/docs/15/plpgsql-control-structures.html#PLPGSQL-ERROR-TRAPPING
OpCode_Execute OpCode = 6 // Executing a standard SQL statement (expects no rows returned unless Target is specified)
OpCode_Get OpCode = 7 // https://www.postgresql.org/docs/15/plpgsql-statements.html#PLPGSQL-STATEMENTS-DIAGNOSTICS
OpCode_Goto OpCode = 8 // All control-flow structures can be represented using Goto
OpCode_If OpCode = 9 // https://www.postgresql.org/docs/15/plpgsql-control-structures.html#PLPGSQL-CONDITIONALS
OpCode_InsertInto OpCode = 10 // https://www.postgresql.org/docs/15/plpgsql-statements.html
OpCode_Perform OpCode = 11 // https://www.postgresql.org/docs/15/plpgsql-statements.html
OpCode_Raise OpCode = 12 // https://www.postgresql.org/docs/15/plpgsql-errors-and-messages.html
OpCode_Return OpCode = 13 // https://www.postgresql.org/docs/15/plpgsql-control-structures.html#PLPGSQL-STATEMENTS-RETURNING
OpCode_ScopeBegin OpCode = 14 // This is used for scope control, specific to Doltgres
OpCode_ScopeEnd OpCode = 15 // This is used for scope control, specific to Doltgres
OpCode_SelectInto OpCode = 16 // https://www.postgresql.org/docs/15/plpgsql-statements.html
OpCode_UpdateInto OpCode = 17 // https://www.postgresql.org/docs/15/plpgsql-statements.html
OpCode_ReturnQuery OpCode = 18 // https://www.postgresql.org/docs/current/plpgsql-control-structures.html#PLPGSQL-STATEMENTS-RETURNING-RETURN-NEXT
OpCode_ForQueryInit OpCode = 19 // Initialize a cursor for FOR record IN query LOOP
OpCode_ForQueryNext OpCode = 20 // Advance cursor and assign next row to record, or jump to exit
// New OpCode values MUST be added to the END of this list!
// Function OpCodes are persisted to disk, so these values MUST be stable across Doltgres versions.
)
// InterpreterOperation is an operation that will be performed by the interpreter.
type InterpreterOperation struct {
OpCode OpCode
PrimaryData string // This will represent the "main" data, such as the query for PERFORM, expression for IF, etc.
SecondaryData []string // This represents auxiliary data, such as bindings, strictness, etc.
Target string // This is the variable that will store the results (if applicable)
Index int // This is the index that should be set for operations that move the function counter
Options map[string]string // This is extra data for operations that need it
}
+300
View File
@@ -0,0 +1,300 @@
// 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"
pgtypes "github.com/dolthub/doltgresql/server/types"
"github.com/dolthub/doltgresql/utils"
)
// cursorState holds the result set for a FOR record IN query LOOP cursor.
type cursorState struct {
Schema sql.Schema
Rows []sql.Row
Index int
}
// interpreterVariable is a variable that lives on the stack. This will hold an actual value, but will not be directly
// interacted with. InterpreterVariableReference are, instead, the avenue of interaction as a variable may be an
// aggregate type (such as a record).
type interpreterVariable struct {
Record sql.Schema // TODO: all records carry their type information alongside the value, so this is redundant
Type *pgtypes.DoltgresType
Value any
}
// InterpreterVariableReference is a reference to a variable that lives on the stack. If the type is not null, then it
// is valid to dereference the value for assignment. We make use of references rather than directly interacting with
// the variables as this allows for interacting with sections of aggregate types (such as record) as well as normal
// variable interaction.
type InterpreterVariableReference struct {
Type *pgtypes.DoltgresType
Value *any
}
// InterpreterScopeDetails contains all of the details that are relevant to a particular scope.
type InterpreterScopeDetails struct {
variables map[string]*interpreterVariable
label string
}
// InterpreterStack represents the working information that an interpreter will use during execution. It is not exactly
// the same as a stack in the traditional programming sense, but rather is a loose abstraction that serves the same
// general purpose.
type InterpreterStack struct {
stack *utils.Stack[*InterpreterScopeDetails]
runner sql.StatementRunner
labelID int
// returnQueryBuffer buffers results from RETURN QUERY statements
returnQueryBuffer [][]pgtypes.RecordValue
// cursors holds the active FOR record IN query LOOP result sets
cursors map[string]*cursorState
}
// NewInterpreterStack creates a new InterpreterStack.
func NewInterpreterStack(runner sql.StatementRunner) InterpreterStack {
stack := utils.NewStack[*InterpreterScopeDetails]()
// This first push represents the function base, including parameters
stack.Push(&InterpreterScopeDetails{
variables: make(map[string]*interpreterVariable),
})
return InterpreterStack{
stack: stack,
runner: runner,
cursors: make(map[string]*cursorState),
}
}
// Details returns the details for the current scope.
func (is *InterpreterStack) Details() *InterpreterScopeDetails {
return is.stack.Peek()
}
// Runner returns the runner that is being used for the function's execution.
func (is *InterpreterStack) Runner() sql.StatementRunner {
return is.runner
}
// GetCurrentLabel traverses the stack (starting from the top) returning the first label found. Returns an empty string
// if no labels were set.
func (is *InterpreterStack) GetCurrentLabel() string {
for i := 0; i < is.stack.Len(); i++ {
label := is.stack.PeekDepth(i).label
if len(label) > 0 {
return label
}
}
return ""
}
// GetVariable traverses the stack (starting from the top) to find a variable with a matching name. Returns nil if no
// variable was found.
func (is *InterpreterStack) GetVariable(name string) InterpreterVariableReference {
// TODO: handle nested record access
fieldName := ""
if strings.Count(name, ".") == 1 {
splitName := strings.Split(name, ".")
name = splitName[0]
fieldName = splitName[1]
}
for i := 0; i < is.stack.Len(); i++ {
if iv, ok := is.stack.PeekDepth(i).variables[name]; ok {
if len(fieldName) == 0 {
return InterpreterVariableReference{
Type: iv.Type,
Value: &iv.Value,
}
} else if len(iv.Record) > 0 {
fieldIdx := iv.Record.IndexOf(fieldName, iv.Record[0].Source)
if fieldIdx == -1 {
// TODO: implement this as a proper error for missing record field rather than the generic "variable not found"
return InterpreterVariableReference{}
}
return InterpreterVariableReference{
Type: iv.Record[fieldIdx].Type.(*pgtypes.DoltgresType),
Value: &(iv.Value.(sql.Row)[fieldIdx]),
}
} else if iv.Type.IsCompositeType() {
for fieldIdx := range iv.Type.CompositeAttrs {
if iv.Type.CompositeAttrs[fieldIdx].Name == fieldName {
vals := iv.Value.([]pgtypes.RecordValue)
return InterpreterVariableReference{
Type: vals[fieldIdx].Type.(*pgtypes.DoltgresType),
Value: &(vals[fieldIdx].Value),
}
}
}
// The field could not be found
return InterpreterVariableReference{}
} else {
// Can't access fields on an empty record
return InterpreterVariableReference{}
}
}
}
return InterpreterVariableReference{}
}
// ListVariables returns a map with the names of all variables. The attached slice represents field names for records.
// All names are lowercased.
func (is *InterpreterStack) ListVariables() map[string][]string {
seen := make(map[string][]string)
for i := 0; i < is.stack.Len(); i++ {
for varName, iv := range is.stack.PeekDepth(i).variables {
var fieldNames []string
if len(iv.Record) > 0 {
for _, col := range iv.Record {
fieldNames = append(fieldNames, strings.ToLower(col.Name))
}
}
seen[strings.ToLower(varName)] = fieldNames
}
}
return seen
}
// NewRecord creates a new record in the current scope. If a record with the same name exists in a previous scope, then
// that record will be shadowed until the current scope exits.
func (is *InterpreterStack) NewRecord(name string, sch sql.Schema, val sql.Row) {
// TODO: this is currently implemented only for the specific record types used in triggers: OLD and NEW
var newVal sql.Row
if val != nil {
newVal = make(sql.Row, len(val))
copy(newVal, val)
}
is.stack.Peek().variables[name] = &interpreterVariable{
Record: sch,
Type: pgtypes.Trigger, // TODO: we need to implement the RECORD pseudotype and replace the TRIGGER type here
Value: newVal,
}
}
// NewVariable creates a new variable in the current scope. If a variable with the same name exists in a previous scope,
// then that variable will be shadowed until the current scope exits.
func (is *InterpreterStack) NewVariable(name string, typ *pgtypes.DoltgresType) {
is.NewVariableWithValue(name, typ, typ.Zero())
}
// NewVariableWithValue creates a new variable in the current scope, setting its initial value to the one given.
func (is *InterpreterStack) NewVariableWithValue(name string, typ *pgtypes.DoltgresType, val any) {
is.stack.Peek().variables[name] = &interpreterVariable{
Type: typ,
Value: val,
}
}
// NewVariableAlias creates a new variable alias, named |alias|, in the current frame of this stack,
// pointing to the specified |variable|.
func (is *InterpreterStack) NewVariableAlias(alias string, target string) {
for i := 0; i < is.stack.Len(); i++ {
if iv, ok := is.stack.PeekDepth(i).variables[target]; ok {
// TODO: this won't work for RECORD types
is.stack.Peek().variables[alias] = iv
break
}
}
}
// PushScope creates a new scope.
func (is *InterpreterStack) PushScope() {
is.stack.Push(&InterpreterScopeDetails{
variables: make(map[string]*interpreterVariable),
})
}
// PopScope removes the current scope.
func (is *InterpreterStack) PopScope() {
is.stack.Pop()
}
// SetVariable sets the first variable found, with a matching name, to the value given. This does not ensure that the
// value matches the expectations of the type, so it should be validated before this is called. Returns an error if the
// variable cannot be found.
func (is *InterpreterStack) SetVariable(ctx *sql.Context, name string, val any) error {
iv := is.GetVariable(name)
if iv.Type == nil {
return fmt.Errorf("variable `%s` could not be found", name)
}
*iv.Value = val
return nil
}
// SetLabel sets the label for the current scope.
func (is *InterpreterStack) SetLabel(label string) {
is.stack.Peek().label = label
}
// SetAnonymousLabel sets the label for the current scope to a guaranteed unique value.
func (is *InterpreterStack) SetAnonymousLabel() {
// Postgres labels cannot have a tab character, so we can generate a label with one to guarantee it's unique
is.stack.Peek().label = fmt.Sprintf("\t%d", is.labelID)
is.labelID++
}
// BufferReturnQueryResults buffers |results| from a RETURN QUERY statement so that they can be returned when
// the function exits. If results from a previous RETURN QUERY call have already been buffered, |results| will
// be appended.
func (is *InterpreterStack) BufferReturnQueryResults(results [][]pgtypes.RecordValue) {
is.returnQueryBuffer = append(is.returnQueryBuffer, results...)
}
// ReturnQueryResults returns the buffered results from a RETURN QUERY statement.
func (is *InterpreterStack) ReturnQueryResults() [][]pgtypes.RecordValue {
return is.returnQueryBuffer
}
// InitCursor stores the result set for a FOR record IN query LOOP cursor.
func (is *InterpreterStack) InitCursor(name string, schema sql.Schema, rows []sql.Row) {
is.cursors[name] = &cursorState{
Schema: schema,
Rows: rows,
Index: 0,
}
}
// AdvanceCursor returns the next row for the named cursor and advances its index.
// Returns (schema, row, true) if a row is available, or (nil, nil, false) when exhausted.
func (is *InterpreterStack) AdvanceCursor(name string) (sql.Schema, sql.Row, bool) {
cs, ok := is.cursors[name]
if !ok || cs.Index >= len(cs.Rows) {
return nil, nil, false
}
row := cs.Rows[cs.Index]
cs.Index++
return cs.Schema, row, true
}
// CloseCursor removes the named cursor from the stack.
func (is *InterpreterStack) CloseCursor(name string) {
delete(is.cursors, name)
}
// UpdateRecord finds the named variable and sets its schema and row value.
func (is *InterpreterStack) UpdateRecord(name string, schema sql.Schema, val sql.Row) error {
for i := 0; i < is.stack.Len(); i++ {
if iv, ok := is.stack.PeekDepth(i).variables[name]; ok {
iv.Record = schema
iv.Value = val
return nil
}
}
return fmt.Errorf("record variable `%s` could not be found", name)
}
+795
View File
@@ -0,0 +1,795 @@
// 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"
)
// action exists to match the expected JSON format.
type action struct {
StmtBlock plpgSQL_stmt_block `json:"PLpgSQL_stmt_block"`
}
// cond exists to match the expected JSON format.
type cond struct {
Expression plpgSQL_expr `json:"PLpgSQL_expr"`
}
// datatype exists to match the expected JSON format.
type datatype struct {
Type plpgSQL_type `json:"PLpgSQL_type"`
}
// default_val exists to match the expected JSON format.
type default_val struct {
Var plpgSQL_expr `json:"PLpgSQL_expr"`
}
// datum exists to match the expected JSON format.
type datum struct {
Record *plpgSQL_rec `json:"PLpgSQL_rec"`
RecordField *plpgSQL_recfield `json:"PLpgSQL_recfield"`
Row *plpgSQL_row `json:"PLpgSQL_row"`
Variable *plpgSQL_var `json:"PLpgSQL_var"`
}
// elsif exists to match the expected JSON format.
type elsif struct {
ElseIf plpgSQL_if_elsif `json:"PLpgSQL_if_elsif"`
}
// expr exists to match the expected JSON format.
type expr struct {
Expression plpgSQL_expr `json:"PLpgSQL_expr"`
}
// field exists to match the expected JSON format.
type field struct {
Name string `json:"name"`
VariableNumber int32 `json:"varno"`
}
// function exists to match the expected JSON format.
type function struct {
Function plpgSQL_block `json:"PLpgSQL_function"`
}
// plpgSQL_block exists to match the expected JSON format.
type plpgSQL_block struct {
NewVariableNumber int32 `json:"new_varno"`
OldVariableNumber int32 `json:"old_varno"`
Datums []datum `json:"datums"`
Action action `json:"action"`
}
// plpgSQL_expr exists to match the expected JSON format.
type plpgSQL_expr struct {
Query string `json:"query"`
ParseMode int32 `json:"parseMode"`
}
// plpgSQL_if_elsif exists to match the expected JSON format.
type plpgSQL_if_elsif struct {
Condition cond `json:"cond"`
Then []statement `json:"stmts"`
LineNumber int32 `json:"lineno"`
}
// plpgSQL_rec exists to match the expected JSON format.
type plpgSQL_rec struct {
RefName string `json:"refname"`
DatumNumber int32 `json:"dno"`
}
// plpgSQL_recfield exists to match the expected JSON format.
type plpgSQL_recfield struct {
FieldName string `json:"fieldname"`
RecordParentNumber int32 `json:"recparentno"`
}
// plpgSQL_row exists to match the expected JSON format.
type plpgSQL_row struct {
RefName string `json:"refname"`
Fields []field `json:"fields"`
LineNumber int32 `json:"lineno"`
}
// plpgSQL_stmt_assign exists to match the expected JSON format.
type plpgSQL_stmt_assign struct {
Expression expr `json:"expr"`
VariableNumber int32 `json:"varno"`
LineNumber int32 `json:"lineno"`
}
// plpgSQL_stmt_block exists to match the expected JSON format.
type plpgSQL_stmt_block struct {
Body []statement `json:"body"`
Label string `json:"label"`
LineNumber int32 `json:"lineno"`
}
// plpgSQL_stmt_call exists to match the expected JSON format.
type plpgSQL_stmt_call struct {
LineNumber int32 `json:"lineno"`
Expression expr `json:"expr"`
IsCall bool `json:"is_call"`
Target datum `json:"target"`
}
// plpgSQL_stmt_case exists to match the expected JSON format.
type plpgSQL_stmt_case struct {
LineNumber int32 `json:"lineno"`
Expression expr `json:"t_expr"`
// VarNo indicates the ID for the __Case__Variable_N__ variable that holds the evaluated
// value of the case expression.
VarNo int32 `json:"t_varno"`
WhenList []statement `json:"case_when_list"`
HasElse bool `json:"have_else"`
Else []statement `json:"else_stmts"`
}
// plpgSQL_stmt_dynexecute exists to match the expected JSON format.
type plpgSQL_stmt_dynexecute struct {
LineNumber int32 `json:"lineno"`
Into bool `json:"into"`
Query expr `json:"query"`
Target datum `json:"target"`
Params []sqlstmt `json:"params"`
}
// plpgSQL_case_when exists to match the expected JSON format.
type plpgSQL_case_when struct {
LineNumber int32 `json:"lineno"`
Expression expr `json:"expr"`
Body []statement `json:"stmts"`
}
// plpgSQL_stmt_execsql exists to match the expected JSON format.
type plpgSQL_stmt_execsql struct {
SQLStmt sqlstmt `json:"sqlstmt"`
LineNumber int32 `json:"lineno"`
Into bool `json:"into"`
Target datum `json:"target"`
}
// plpgSQL_stmt_exit exists to match the expected JSON format.
type plpgSQL_stmt_exit struct {
Label string `json:"label"`
IsExit bool `json:"is_exit"`
Condition *expr `json:"cond"`
LineNumber int32 `json:"lineno"`
}
// plpgSQL_stmt_fori exists to match the expected JSON format.
type plpgSQL_stmt_fori struct {
Label string `json:"label"`
Var datum `json:"var"`
Lower *expr `json:"lower"`
Upper *expr `json:"upper"`
Step *expr `json:"step"`
Reverse bool `json:"reverse"`
Body []statement `json:"body"`
LineNumber int32 `json:"lineno"`
}
// plpgSQL_stmt_fors exists to match the expected JSON format.
type plpgSQL_stmt_fors struct {
Label string `json:"label"`
Var datum `json:"var"`
Body []statement `json:"body"`
Query *expr `json:"query"`
LineNumber int32 `json:"lineno"`
}
// plpgSQL_stmt_if exists to match the expected JSON format.
type plpgSQL_stmt_if struct {
Condition cond `json:"cond"`
Then []statement `json:"then_body"`
ElseIf []elsif `json:"elsif_list"`
Else []statement `json:"else_body"`
LineNumber int32 `json:"lineno"`
}
// plpgSQL_stmt_loop exists to match the expected JSON format.
type plpgSQL_stmt_loop struct {
Body []statement `json:"body"`
Label string `json:"label"`
LineNumber int32 `json:"lineno"`
}
// plpgSQL_stmt_perform exists to match the expected JSON format.
type plpgSQL_stmt_perform struct {
Expression expr `json:"expr"`
LineNumber int32 `json:"lineno"`
}
// plpgSQL_stmt_raise exists to match the expected JSON format.
type plpgSQL_stmt_raise struct {
LineNumber int32 `json:"lineno"`
ELogLevel int32 `json:"elog_level"`
Message string `json:"message"`
Params []sqlstmt `json:"params"`
Options []plpgSQL_raise_option_wrapper `json:"options"`
}
// plpgSQL_raise_option_wrapper exists to match the expected JSON format.
type plpgSQL_raise_option_wrapper struct {
Option plpgSQL_raise_option `json:"PLpgSQL_raise_option"`
}
// plpgSQL_raise_option exists to match the expected JSON format.
type plpgSQL_raise_option struct {
OptionType int32 `json:"opt_type"`
Expression sqlstmt `json:"expr"`
}
// plpgSQL_stmt_return exists to match the expected JSON format.
type plpgSQL_stmt_return struct {
Expression expr `json:"expr"`
LineNumber int32 `json:"lineno"`
}
// plpgSQL_stmt_return_query exists to match the expected JSON format.
type plpgSQL_stmt_return_query struct {
Query expr `json:"query"`
LineNumber int32 `json:"lineno"`
}
// plpgSQL_stmt_while exists to match the expected JSON format.
type plpgSQL_stmt_while struct {
Condition cond `json:"cond"`
Body []statement `json:"body"`
Label string `json:"label"`
LineNumber int32 `json:"lineno"`
}
// plpgSQL_type exists to match the expected JSON format.
type plpgSQL_type struct {
Name string `json:"typname"`
}
// plpgSQL_var exists to match the expected JSON format.
type plpgSQL_var struct {
RefName string `json:"refname"`
Type datatype `json:"datatype"`
LineNumber int32 `json:"lineno"`
Default default_val `json:"default_val"`
}
// sqlstmt exists to match the expected JSON format.
type sqlstmt struct {
Expr plpgSQL_expr `json:"PLpgSQL_expr"`
}
// statement exists to match the expected JSON format. Unlike other structs, this is used like a union rather than
// having a singular expected implementation.
type statement struct {
Assignment *plpgSQL_stmt_assign `json:"PLpgSQL_stmt_assign"`
Block *plpgSQL_stmt_block `json:"PLpgSQL_stmt_block"`
Call *plpgSQL_stmt_call `json:"PLpgSQL_stmt_call"`
Case *plpgSQL_stmt_case `json:"PLpgSQL_stmt_case"`
DynExec *plpgSQL_stmt_dynexecute `json:"PLpgSQL_stmt_dynexecute"`
ExecSQL *plpgSQL_stmt_execsql `json:"PLpgSQL_stmt_execsql"`
Exit *plpgSQL_stmt_exit `json:"PLpgSQL_stmt_exit"`
ForILoop *plpgSQL_stmt_fori `json:"PLpgSQL_stmt_fori"`
ForSLoop *plpgSQL_stmt_fors `json:"PLpgSQL_stmt_fors"`
If *plpgSQL_stmt_if `json:"PLpgSQL_stmt_if"`
Loop *plpgSQL_stmt_loop `json:"PLpgSQL_stmt_loop"`
Perform *plpgSQL_stmt_perform `json:"PLpgSQL_stmt_perform"`
Raise *plpgSQL_stmt_raise `json:"PLpgSQL_stmt_raise"`
Return *plpgSQL_stmt_return `json:"PLpgSQL_stmt_return"`
ReturnQuery *plpgSQL_stmt_return_query `json:"PLpgSQL_stmt_return_query"`
When *plpgSQL_case_when `json:"PLpgSQL_case_when"`
While *plpgSQL_stmt_while `json:"PLpgSQL_stmt_while"`
}
// Convert converts the JSON statement into its output form.
func (stmt *plpgSQL_stmt_assign) Convert() (Assignment, error) {
query := stmt.Expression.Expression.Query
varName := ""
if equalsIdx := strings.Index(query, ":="); equalsIdx > 0 {
varName = strings.TrimSpace(query[:equalsIdx])
query = strings.TrimSpace(query[equalsIdx+2:])
} else if equalsIdx = strings.Index(query, "="); equalsIdx > 0 {
varName = strings.TrimSpace(query[:equalsIdx])
query = strings.TrimSpace(query[equalsIdx+1:])
} else {
return Assignment{}, errors.New("PL/pgSQL assignment cannot find `:=` sign")
}
return Assignment{
VariableName: varName,
Expression: query,
VariableIndex: stmt.VariableNumber,
}, nil
}
// Convert converts the JSON statement into its output form.
func (stmt *plpgSQL_stmt_call) Convert() (ExecuteSQL, error) {
var target string
if !stmt.IsCall {
if stmt.Target.Row != nil {
names := make([]string, len(stmt.Target.Row.Fields))
for i, rowField := range stmt.Target.Row.Fields {
names[i] = rowField.Name
}
target = strings.Join(names, ",")
} else if stmt.Target.Variable != nil {
target = stmt.Target.Variable.RefName
} else {
return ExecuteSQL{}, errors.Errorf("unhandled datum type: %T", stmt.Target)
}
}
return ExecuteSQL{
Statement: stmt.Expression.Expression.Query,
Target: target,
}, nil
}
// Convert converts the JSON statement into its output form.
func (stmt *plpgSQL_stmt_case) Convert() (block Block, err error) {
// If the CASE statement has a main expression, start by assigning it to a variable so
// we can evaluate it once and only once.
if stmt.Expression.Expression.Query != "" {
// TODO: pg_query_go creates the definitions for these variables, and
// ideally users shouldn't be able to reference them. We could
// update all the references to them (i.e. declaration, assignment,
// and WHEN block exprs) to change the name to include a \0 char to
// prevent users from referencing them or colliding with them.
block.Body = append(block.Body, Assignment{
VariableName: fmt.Sprintf("__Case__Variable_%d__", stmt.VarNo),
Expression: stmt.Expression.Expression.Query,
})
}
// Record indexes of all the GOTO ops that jump to the very end of the case block so we
// can update them later and plug in the correct offsets after we know the final size.
var gotoEndOpsIndexes []int
// Add operations for each WHEN statement...
for _, stmt := range stmt.WhenList {
when := stmt.When
if when == nil {
return Block{}, fmt.Errorf("case statement WHEN clause is nil")
}
// TODO: The generated expressions from pg_query_go uses double quotes
// around the variable name, which is valid for Postgres, but
// our engine doesn't currently resolve double-quoted strings to
// variables, so for now, we just extract the double quotes.
expressionString := when.Expression.Expression.Query
expressionString = strings.ReplaceAll(expressionString, `"`, "")
convertedWhenBodyStatements, err := jsonConvertStatements(when.Body)
if err != nil {
return Block{}, err
}
block.Body = append(block.Body,
If{
Condition: expressionString,
GotoOffset: 2,
},
Goto{
// This GOTO jumps to the next WHEN block, so step over all the statements
// from this WHEN block, plus 1 for the GOTO op we add at the end of each
// block, and plus 1 more to move to the next statement.
Offset: int32(len(convertedWhenBodyStatements) + 1 + 1),
})
block.Body = append(block.Body, convertedWhenBodyStatements...)
// Add a GOTO op to jump to the end of the entire CASE block, and record its position
// in the statement block so we can update it later.
block.Body = append(block.Body, Goto{})
gotoEndOpsIndexes = append(gotoEndOpsIndexes, len(block.Body)-1)
}
if stmt.HasElse {
convertElseBodyStatements, err := jsonConvertStatements(stmt.Else)
if err != nil {
return Block{}, err
}
block.Body = append(block.Body, convertElseBodyStatements...)
// TODO: If no cases match and there is no ELSE block, then add a RAISE statement
// to return an error.
//} else {
// Sample PostgreSQL error response:
// ERROR: case not found
// HINT: CASE statement is missing ELSE part.
// CONTEXT: PL/pgSQL function interpreted_case(integer) line 5 at CASE
}
// Update all the GOTO ops that jump to the very end of the case block.
for _, gotoEndOpIndex := range gotoEndOpsIndexes {
// Sanity check that we are looking at a GOTO statement
if _, ok := block.Body[gotoEndOpIndex].(Goto); !ok {
return Block{}, fmt.Errorf("expected Goto statement, got %T", block.Body[gotoEndOpIndex])
}
block.Body[gotoEndOpIndex] = Goto{
Offset: int32(len(block.Body) - gotoEndOpIndex),
}
}
return block, nil
}
// Convert converts the JSON statement into its output form.
func (stmt *plpgSQL_stmt_dynexecute) Convert() (DynamicExecute, error) {
var params []string
for _, param := range stmt.Params {
params = append(params, param.Expr.Query)
}
var target string
if stmt.Into {
switch {
case stmt.Target.Row != nil:
names := make([]string, len(stmt.Target.Row.Fields))
for i, rowField := range stmt.Target.Row.Fields {
names[i] = rowField.Name
}
target = strings.Join(names, ",")
case stmt.Target.Variable != nil:
target = stmt.Target.Variable.RefName
default:
return DynamicExecute{}, errors.Errorf("unhandled datum type: %T", stmt.Target)
}
}
query := strings.TrimSuffix(strings.TrimPrefix(stmt.Query.Expression.Query, "'"), "'")
return DynamicExecute{
Query: query,
Params: params,
Target: target,
}, nil
}
// Convert converts the JSON statement into its output form.
func (stmt *plpgSQL_stmt_execsql) Convert() (ExecuteSQL, error) {
var target string
if stmt.Into {
if stmt.Target.Row != nil {
names := make([]string, len(stmt.Target.Row.Fields))
for i, rowField := range stmt.Target.Row.Fields {
names[i] = rowField.Name
}
target = strings.Join(names, ",")
} else if stmt.Target.Variable != nil {
target = stmt.Target.Variable.RefName
} else {
return ExecuteSQL{}, errors.Errorf("unhandled datum type: %T", stmt.Target)
}
}
return ExecuteSQL{
Statement: stmt.SQLStmt.Expr.Query,
Target: target,
}, nil
}
// Convert converts the JSON statement into its output form.
func (stmt *plpgSQL_stmt_exit) Convert() Statement {
offset := int32(-1)
if stmt.IsExit {
offset = 1
}
var gotoStmt Goto
if len(stmt.Label) > 0 {
gotoStmt = Goto{
Offset: offset,
Label: stmt.Label,
}
} else {
gotoStmt = Goto{
Offset: offset,
NearestScopeOp: true,
}
}
if stmt.Condition == nil {
return gotoStmt
} else {
return Block{
Body: []Statement{
If{
Condition: stmt.Condition.Expression.Query,
GotoOffset: 2,
},
Goto{Offset: 2},
gotoStmt,
},
}
}
}
// Convert converts the JSON statement into its output form.
func (stmt *plpgSQL_stmt_fori) Convert() (block Block, err error) {
block.Label = stmt.Label
block.IsLoop = true
if stmt.Var.Variable == nil {
return Block{}, errors.New("for loop variable cannot be nil")
}
varName := stmt.Var.Variable.RefName
// Extract bound and step expressions
lowerExpr := "1"
if stmt.Lower != nil {
lowerExpr = stmt.Lower.Expression.Query
}
upperExpr := "1"
if stmt.Upper != nil {
upperExpr = stmt.Upper.Expression.Query
}
stepExpr := "1"
if stmt.Step != nil {
stepExpr = stmt.Step.Expression.Query
}
// Determine init value, loop condition, and increment expression based on direction.
// In the JSON, Lower is always the starting value and Upper is the ending bound.
var condition, incrExpr string
if stmt.Reverse {
condition = fmt.Sprintf("%s >= (%s)", varName, upperExpr)
incrExpr = fmt.Sprintf("%s - (%s)", varName, stepExpr)
} else {
condition = fmt.Sprintf("%s <= (%s)", varName, upperExpr)
incrExpr = fmt.Sprintf("%s + (%s)", varName, stepExpr)
}
// Convert the loop body.
convertedBody, err := jsonConvertStatements(stmt.Body)
if err != nil {
return Block{}, err
}
bodySize := OperationSizeForStatements(convertedBody)
// Build the loop body:
// [0] InitAssign: varName := lower
// [1] If(condition, GotoOffset:2) → jumps to [3] (first body stmt) when true
// [2] ExitGoto → offset=3+bodySize → jumps to ScopeEnd
// [3..3+N-1] body statements (N = bodySize)
// [3+N] IncrAssign: varName := varName +/- step
// [3+N+1] BackGoto → offset=-(3+bodySize) → jumps back to If at [1]
//
// Because no variables are declared in this block (the loop variable is already
// declared by the caller's DECLARE section), ScopeBegin is at M and the
// InitAssign is at M+1, so all offsets are consistent.
block.Body = []Statement{
Assignment{
VariableName: varName,
Expression: lowerExpr,
},
If{
Condition: condition,
GotoOffset: 2,
},
Goto{
Offset: 3 + bodySize,
},
}
block.Body = append(block.Body, convertedBody...)
block.Body = append(block.Body,
Assignment{
VariableName: varName,
Expression: incrExpr,
},
Goto{
Offset: -(3 + bodySize),
},
)
return block, nil
}
// Convert converts the JSON statement into its output form.
func (stmt *plpgSQL_stmt_fors) Convert() (block Block, err error) {
block.Label = stmt.Label
block.IsLoop = true
if stmt.Query == nil {
return Block{}, errors.New("FOR..IN..SELECT loop must have a query")
}
var varName string
switch {
case stmt.Var.Record != nil:
varName = stmt.Var.Record.RefName
case stmt.Var.Variable != nil:
varName = stmt.Var.Variable.RefName
case stmt.Var.Row != nil:
varName = stmt.Var.Row.RefName
default:
return Block{}, errors.New("FOR..IN..SELECT loop variable must be a record, row, or variable")
}
// Use the line number to keep cursor names unique across multiple ForS loops
// that might use the same variable name.
cursorName := fmt.Sprintf("__cursor_%s_%d__", varName, stmt.LineNumber)
query := stmt.Query.Expression.Query
convertedBody, err := jsonConvertStatements(stmt.Body)
if err != nil {
return Block{}, err
}
bodySize := OperationSizeForStatements(convertedBody)
// Layout inside the block (ScopeBegin/ScopeEnd are added by Block.AppendOperations):
// [0] ForQueryInit execute query, store rows in cursor
// [1] ForQueryNext fetch next row into varName, or jump forward by (bodySize+2) to ScopeEnd
// [2..2+bodySize-1] body statements
// [2+bodySize] Goto back to ForQueryNext: offset = -(1 + bodySize)
block.Body = []Statement{
ForQueryInit{CursorName: cursorName, Query: query},
ForQueryNext{CursorName: cursorName, RecordVar: varName, GotoOffset: bodySize + 2},
}
block.Body = append(block.Body, convertedBody...)
block.Body = append(block.Body, Goto{Offset: -(1 + bodySize)})
return block, nil
}
// Convert converts the JSON statement into its output form.
func (stmt *plpgSQL_stmt_if) Convert() (Block, error) {
// We store all GOTOs that will need to go to the end of the block. Since we can't know that ahead of time, we store
// their indexes and set them at the end of the function.
type gotoEndIndex struct {
BodyIndex int
GotoIndex int32
}
var gotoEndIndexes []gotoEndIndex
returnBlock := Block{
Body: []Statement{
If{
Condition: stmt.Condition.Expression.Query,
GotoOffset: 2, // The operation following the conditional skips the THEN statements, so we're skipping that
},
},
}
// We'll parse our THEN statements, but we won't add them to the block just yet as we need their operation sizes
thenStmts, err := jsonConvertStatements(stmt.Then)
if err != nil {
return Block{}, err
}
// When the condition is false, we want to skip our THEN block, so we do that (plus the GOTO which finishes the THEN block)
returnBlock.Body = append(returnBlock.Body, Goto{Offset: OperationSizeForStatements(thenStmts) + 2})
// Then we'll append our THEN block
returnBlock.Body = append(returnBlock.Body, thenStmts...)
// Then we want to append the GOTO that finishes the THEN block, but we don't know the end just yet, so we'll save
// its index and fill it in later
gotoEndIndexes = append(gotoEndIndexes, gotoEndIndex{
BodyIndex: len(returnBlock.Body),
GotoIndex: OperationSizeForStatements(returnBlock.Body),
})
returnBlock.Body = append(returnBlock.Body, Goto{})
// We repeat the same process for each ELSIF statement (refer to the comments above)
for _, elseIf := range stmt.ElseIf {
returnBlock.Body = append(returnBlock.Body, If{
Condition: elseIf.ElseIf.Condition.Expression.Query,
GotoOffset: 2, // Same rules as skipping our THEN statement above
})
elseIfStmts, err := jsonConvertStatements(elseIf.ElseIf.Then)
if err != nil {
return Block{}, err
}
returnBlock.Body = append(returnBlock.Body, Goto{Offset: OperationSizeForStatements(elseIfStmts) + 2})
returnBlock.Body = append(returnBlock.Body, elseIfStmts...)
gotoEndIndexes = append(gotoEndIndexes, gotoEndIndex{
BodyIndex: len(returnBlock.Body),
GotoIndex: OperationSizeForStatements(returnBlock.Body),
})
returnBlock.Body = append(returnBlock.Body, Goto{})
}
// Finally we handle our ELSE statements. We don't have a condition to check, so we don't have to append any
// additional GOTOs.
elseStmts, err := jsonConvertStatements(stmt.Else)
if err != nil {
return Block{}, err
}
returnBlock.Body = append(returnBlock.Body, elseStmts...)
// Now we'll set all of our GOTOs so that they skip to the end of the block.
// We have to take their index position into account, since we want to skip to the end from their relative position.
for _, idx := range gotoEndIndexes {
returnBlock.Body[idx.BodyIndex] = Goto{Offset: OperationSizeForStatements(returnBlock.Body) - idx.GotoIndex}
}
return returnBlock, nil
}
// Convert converts the JSON statement into its output form.
func (stmt *plpgSQL_stmt_loop) Convert() (block Block, err error) {
// Set the block's label if one was provided
block.Label = stmt.Label
block.IsLoop = true
// Convert the body of the loop first so we can determine the GOTO offset
block.Body, err = jsonConvertStatements(stmt.Body)
if err != nil {
return Block{}, err
}
// The loop returns to the beginning of the loop, skipping the body
block.Body = append(block.Body, Goto{Offset: -OperationSizeForStatements(block.Body)})
return block, nil
}
// Convert converts the JSON statement into its output form.
func (stmt *plpgSQL_stmt_perform) Convert() Perform {
return Perform{
Statement: stmt.Expression.Expression.Query,
}
}
// Convert converts the JSON statement into its output form.
func (stmt *plpgSQL_stmt_raise) Convert() Raise {
var params []string
for _, param := range stmt.Params {
params = append(params, param.Expr.Query)
}
options := make(map[string]string)
for _, option := range stmt.Options {
options[strconv.Itoa(int(option.Option.OptionType))] = option.Option.Expression.Expr.Query
}
return Raise{
Level: NoticeLevel(uint8(stmt.ELogLevel)).String(),
Message: stmt.Message,
Params: params,
Options: options,
}
}
// Convert converts the JSON statement into its output form.
func (stmt *plpgSQL_stmt_return) Convert() Return {
return Return{
Expression: stmt.Expression.Expression.Query,
}
}
// Convert converts the JSON statement into its output form.
func (stmt *plpgSQL_stmt_return_query) Convert() ReturnQuery {
return ReturnQuery{
Query: stmt.Query.Expression.Query,
}
}
// Convert converts the JSON statement into its output form.
func (stmt *plpgSQL_stmt_while) Convert() (block Block, err error) {
// Convert the body of the loop first so we can determine the GOTO offsets
convertedLoopBodyStmts, err := jsonConvertStatements(stmt.Body)
if err != nil {
return Block{}, err
}
block = Block{
Body: []Statement{
If{
Condition: stmt.Condition.Expression.Query,
// Jump forward two statements, so we skip over the GOTO below that exits the WHILE loop.
GotoOffset: 2,
},
Goto{
// Jump forward 1 statement to get to the loop body, then jump over the loop body and the
// GOTO statement that jumps to the start of the WHILE loop.
Offset: 1 + OperationSizeForStatements(convertedLoopBodyStmts) + 1,
},
},
Label: stmt.Label,
IsLoop: true,
}
// Add the converted body of the WHILE loop, and a GOTO statement that jumps backwards past the current
// GOTO statement, and past all the body statements, and past the GOTO statement at the start of the loop.
block.Body = append(block.Body, convertedLoopBodyStmts...)
block.Body = append(block.Body, Goto{Offset: -1 * (OperationSizeForStatements(convertedLoopBodyStmts) + 2)})
return block, nil
}
+140
View File
@@ -0,0 +1,140 @@
// 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 (
"strings"
"github.com/cockroachdb/errors"
)
// jsonConvert handles the conversion from the JSON format into a format that is easier to work with.
func jsonConvert(jsonBlock plpgSQL_block) (Block, error) {
block := Block{
TriggerNew: jsonBlock.NewVariableNumber,
TriggerOld: jsonBlock.OldVariableNumber,
Label: jsonBlock.Action.StmtBlock.Label,
}
lowestRecordNumber := int32(2147483647)
// We do a first loop to determine the offset for the first record
for _, v := range jsonBlock.Datums {
switch {
case v.Record != nil:
if v.Record.DatumNumber < lowestRecordNumber {
lowestRecordNumber = v.Record.DatumNumber
}
}
}
offset := int32(0) - lowestRecordNumber
// Then we do a second loop that actually adds all of the datums to the block
for _, v := range jsonBlock.Datums {
switch {
case v.Record != nil:
// TODO: support normal record types
datumNumber := v.Record.DatumNumber + offset
if int(datumNumber) >= len(block.Records) {
oldRecords := block.Records
block.Records = make([]Record, datumNumber+1)
copy(block.Records, oldRecords)
}
if v.Record.DatumNumber > 0 {
block.Records[datumNumber].Name = v.Record.RefName
}
case v.RecordField != nil:
recordParentNumber := v.RecordField.RecordParentNumber + offset
if int(recordParentNumber) >= len(block.Records) {
return Block{}, errors.New("invalid record parent number")
}
block.Records[recordParentNumber].Fields = append(
block.Records[recordParentNumber].Fields, v.RecordField.FieldName)
case v.Row != nil:
case v.Variable != nil:
block.Variables = append(block.Variables, Variable{
Name: v.Variable.RefName,
Type: strings.ToLower(v.Variable.Type.Type.Name),
IsParameter: v.Variable.LineNumber == 0,
Default: v.Variable.Default.Var.Query,
})
default:
return Block{}, errors.Errorf("unhandled datum type: %T", v)
}
}
var err error
block.Body, err = jsonConvertStatements(jsonBlock.Action.StmtBlock.Body)
if err != nil {
return Block{}, err
}
return block, nil
}
// jsonConvertStatement converts a statement in JSON form to the output form.
func jsonConvertStatement(stmt statement) (Statement, error) {
switch {
case stmt.Assignment != nil:
return stmt.Assignment.Convert()
case stmt.Block != nil:
stmts, err := jsonConvertStatements(stmt.Block.Body)
if err != nil {
return Block{}, err
}
return Block{
Body: stmts,
}, nil
case stmt.Call != nil:
return stmt.Call.Convert()
case stmt.Case != nil:
return stmt.Case.Convert()
case stmt.DynExec != nil:
return stmt.DynExec.Convert()
case stmt.ExecSQL != nil:
return stmt.ExecSQL.Convert()
case stmt.Exit != nil:
return stmt.Exit.Convert(), nil
case stmt.ForILoop != nil:
return stmt.ForILoop.Convert()
case stmt.ForSLoop != nil:
return stmt.ForSLoop.Convert()
case stmt.If != nil:
return stmt.If.Convert()
case stmt.Loop != nil:
return stmt.Loop.Convert()
case stmt.Perform != nil:
return stmt.Perform.Convert(), nil
case stmt.Raise != nil:
return stmt.Raise.Convert(), nil
case stmt.Return != nil:
return stmt.Return.Convert(), nil
case stmt.ReturnQuery != nil:
return stmt.ReturnQuery.Convert(), nil
case stmt.While != nil:
return stmt.While.Convert()
default:
return Block{}, errors.Errorf("unhandled statement type: %T", stmt)
}
}
// jsonConvertStatements converts a collection of statements in JSON form to their output form.
func jsonConvertStatements(stmts []statement) ([]Statement, error) {
newStmts := make([]Statement, len(stmts))
for i, stmt := range stmts {
var err error
newStmts[i], err = jsonConvertStatement(stmt)
if err != nil {
return nil, err
}
}
return newStmts, nil
}
+61
View File
@@ -0,0 +1,61 @@
// 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
// NoticeLevel represents the severity, or level, of a notice created by a RAISE statement.
type NoticeLevel uint8
const (
NoticeLevelDebug NoticeLevel = 14
NoticeLevelLog NoticeLevel = 15
NoticeLevelInfo NoticeLevel = 17
NoticeLevelNotice NoticeLevel = 18
NoticeLevelWarning NoticeLevel = 19
NoticeLevelException NoticeLevel = 21
)
// String returns a string representation of this NoticeLevel.
func (nl NoticeLevel) String() string {
switch nl {
case NoticeLevelDebug:
return "DEBUG"
case NoticeLevelLog:
return "LOG"
case NoticeLevelInfo:
return "INFO"
case NoticeLevelNotice:
return "NOTICE"
case NoticeLevelWarning:
return "WARNING"
case NoticeLevelException:
return "EXCEPTION"
default:
return "UNKNOWN"
}
}
// NoticeOptionType represents the type of option specified for a notice in the USING clause of a RAISE statement.
type NoticeOptionType uint8
const (
NoticeOptionTypeErrCode NoticeOptionType = 0
NoticeOptionTypeMessage NoticeOptionType = 1
NoticeOptionTypeDetail NoticeOptionType = 2
NoticeOptionTypeHint NoticeOptionType = 3
NoticeOptionTypeConstraint NoticeOptionType = 5
NoticeOptionTypeDataType NoticeOptionType = 6
NoticeOptionTypeTable NoticeOptionType = 7
NoticeOptionTypeSchema NoticeOptionType = 8
)
+52
View File
@@ -0,0 +1,52 @@
// 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 (
"encoding/json"
"github.com/cockroachdb/errors"
pg_query "github.com/dolthub/pg_query_go/v6"
)
// Parse parses the given CREATE FUNCTION string (which must be the entire string, not just the body) into a Block
// containing the contents of the body.
func Parse(fullCreateFunctionString string) ([]InterpreterOperation, error) {
var functions []function
parsedBody, err := pg_query.ParsePlPgSqlToJSON(fullCreateFunctionString)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(parsedBody), &functions)
if err != nil {
return nil, err
}
if len(functions) != 1 {
return nil, errors.New("CREATE FUNCTION parsed multiple blocks")
}
block, err := jsonConvert(functions[0].Function)
if err != nil {
return nil, err
}
ops := make([]InterpreterOperation, 0, len(block.Body)+len(block.Variables))
stack := NewInterpreterStack(nil)
if err = block.AppendOperations(&ops, &stack); err != nil {
return nil, err
}
if err = reconcileLabels(ops); err != nil {
return nil, err
}
return ops, nil
}
+90
View File
@@ -0,0 +1,90 @@
// 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 (
"github.com/cockroachdb/errors"
"github.com/dolthub/doltgresql/utils"
)
// labelStackItem is the stack item used while reconciling labels.
type labelStackItem struct {
label string
start int
isLoop bool
}
// reconcileLabels handles all GOTO operations that may point to a label, since labels may be nested/shadowed. It's
// easier to perform this is a final step, rather than trying to reconcile them during the operation conversion step.
func reconcileLabels(ops []InterpreterOperation) error {
labels := utils.NewStack[labelStackItem]()
gotos := make(map[int]*InterpreterOperation)
for opIndex, operation := range ops {
switch operation.OpCode {
case OpCode_Goto:
// When this is true, we have a label
if len(operation.PrimaryData) > 0 {
if operation.Index < 0 {
// This is a CONTINUE, so we already know the index that we need to go to
found := false
for i := 0; i < labels.Len(); i++ {
stackItem := labels.PeekDepth(i)
if stackItem.label == operation.PrimaryData {
if !stackItem.isLoop {
return errors.New("CONTINUE cannot be used outside a loop")
}
found = true
ops[opIndex].Index = stackItem.start
ops[opIndex].PrimaryData = ""
break
}
}
if !found {
return errors.Errorf(`there is no label "%s" attached to any block or loop enclosing this statement`, operation.PrimaryData)
}
} else {
// This is an EXIT, so we'll save it for later
gotos[opIndex] = &ops[opIndex]
}
}
case OpCode_ScopeBegin:
// We'll push the label and loop status to the stack
labels.Push(labelStackItem{
label: operation.PrimaryData,
start: opIndex + 1, // We want to go to the operation after this one, else we'll continually increase the scope
isLoop: len(operation.Target) > 0,
})
// We clear the label and loop status since we only set them for reconciliation
ops[opIndex].PrimaryData = ""
ops[opIndex].Target = ""
case OpCode_ScopeEnd:
stackItem := labels.Pop()
for gotoIdx, gotoOp := range gotos {
if gotoOp.PrimaryData == stackItem.label {
gotoOp.Index = opIndex // We want to go to this operation, as we want to exit the scope
gotoOp.PrimaryData = ""
delete(gotos, gotoIdx)
}
}
}
}
if len(gotos) > 0 {
for _, op := range gotos {
return errors.Errorf(`there is no label "%s" attached to any block or loop enclosing this statement`, op.PrimaryData)
}
}
return nil
}
+505
View File
@@ -0,0 +1,505 @@
// 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
}