301 lines
10 KiB
Go
301 lines
10 KiB
Go
// Copyright 2025 Dolthub, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package plpgsql
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/dolthub/go-mysql-server/sql"
|
|
|
|
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)
|
|
}
|