chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user