Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions server/plpgsql/interpreter_logic.go
Original file line number Diff line number Diff line change
Expand Up @@ -492,11 +492,11 @@ func call(ctx *sql.Context, iFunc InterpretedFunction, stack InterpreterStack) (
if err != nil {
return nil, err
}
stack.InitCursor(operation.Target, schema, rows)
stack.InitCursor(schema, rows)
// The loop reports FOUND when it is left even if the query matched nothing.
stack.MarkScopeLoop(false)
case OpCode_ForQueryNext:
schema, row, ok := stack.AdvanceCursor(operation.PrimaryData)
schema, row, ok := stack.AdvanceCursor()
if !ok {
// Jump forward past the loop body and back-goto, same mechanism as OpCode_If. The loop's
// ScopeEnd is what closes the cursor and reports FOUND, since every way out of the loop
Expand Down Expand Up @@ -609,14 +609,10 @@ func evaluateDynamicUsing(ctx *sql.Context, iFunc InterpretedFunction, operation
// of a Goto leave scopes, so they share this rather than each handling scope depth on its own, which would
// leave whichever of them grew a new responsibility last out of step with the other.
//
// A FOR..IN..SELECT loop's scope owns the cursor the loop iterates, so leaving the scope is what closes it.
// Leaving it is also what reports a FOR loop's FOUND: Postgres sets FOUND when such a loop exits, by
// whichever path, to whether the body ran at all, and leaves it alone while the loop is running. A WHILE or
// plain LOOP does not set FOUND, so only a scope its loop marked reports one.
// Leaving the scope is what reports a FOR loop's FOUND: PostgreSQL sets FOUND when such a loop exits, by
// whichever path, to whether the body ran at all, and leaves it alone while the loop runs. A WHILE or plain
// LOOP never sets FOUND, so only a loop that marked its scope reports one.
func exitScope(ctx *sql.Context, stack InterpreterStack) error {
if cursorName := stack.ScopeCursor(); len(cursorName) > 0 {
stack.CloseCursor(cursorName)
}
if reportsFound, iterated := stack.ScopeLoop(); reportsFound {
if err := stack.SetFound(ctx, iterated); err != nil {
return err
Expand Down
42 changes: 13 additions & 29 deletions server/plpgsql/interpreter_stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ const (
// https://www.postgresql.org/docs/15/plpgsql-statements.html#PLPGSQL-STATEMENTS-DIAGNOSTICS
const FoundVariableName = "found"

// cursorState holds the result set for a FOR record IN query LOOP cursor.
// cursorState holds the result set a loop walks.
type cursorState struct {
Schema sql.Schema
Rows []sql.Row
Expand Down Expand Up @@ -145,10 +145,10 @@ type InterpreterVariableReference struct {
type InterpreterScopeDetails struct {
variables map[string]*interpreterVariable
label string
// cursor names the FOR..IN..SELECT cursor this scope owns, if it is such a loop's scope. The scope
// owning it is what lets the cursor be torn down wherever the loop is left, rather than only where
// the cursor runs out.
cursor string
// cursor holds the result set this scope's loop walks, when the scope is such a loop's. Hanging it
// off the scope is what tears it down wherever the loop is left, rather than only where it runs out,
// and is what keeps nested loops apart: the inner opens its cursor in its own scope, not the outer's.
cursor *cursorState
// reportsFound marks the scope of a loop that sets FOUND when it is left, and iterated records
// whether that loop ever advanced into its body.
reportsFound bool
Expand All @@ -166,8 +166,6 @@ type InterpreterStack struct {

// 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
// unfoldedNames holds the folded names of variables that go through special name resolution for
// compatibility with triggers that were compiled by older version of doltgres. These are names
// that are declared by the trigger itself --- NEW, OLD and TG_ variables. A body compiled before
Expand All @@ -193,7 +191,6 @@ func NewInterpreterStack(runner sql.StatementRunner) InterpreterStack {
outParams: make([]string, 0),
stack: stack,
runner: runner,
cursors: make(map[string]*cursorState),
unfoldedNames: make(map[string]struct{}),
}
}
Expand Down Expand Up @@ -521,40 +518,27 @@ func (is *InterpreterStack) ReturnOutParamResults() any {
return record
}

// InitCursor stores the result set for a FOR record IN query LOOP cursor. The cursor is opened in the
// loop's own scope, which takes ownership of it.
func (is *InterpreterStack) InitCursor(name string, schema sql.Schema, rows []sql.Row) {
is.cursors[name] = &cursorState{
// InitCursor stores the result set a loop walks in the current scope, which is the loop's own.
func (is *InterpreterStack) InitCursor(schema sql.Schema, rows []sql.Row) {
is.stack.Peek().cursor = &cursorState{
Schema: schema,
Rows: rows,
Index: 0,
}
is.stack.Peek().cursor = name
}

// ScopeCursor returns the name of the cursor the current scope owns, or an empty string when the scope is
// not that of a FOR..IN..SELECT loop.
func (is *InterpreterStack) ScopeCursor() string {
return is.stack.Peek().cursor
}

// 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) {
// AdvanceCursor returns the next row of the cursor the current scope owns, and false once it is exhausted.
// The scope is the loop's own, since the operation that advances the cursor sits at the top of the body.
func (is *InterpreterStack) AdvanceCursor() (sql.Schema, sql.Row, bool) {
cs := is.stack.Peek().cursor
if cs == nil || 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)
}

// MarkScopeLoop marks the current scope as a loop's, whose exit reports FOUND, and records whether the loop
// has just advanced into its body. Only the loop itself knows that it advanced, and it cannot record it in
// FOUND, which PostgreSQL leaves alone until the loop is left.
Expand Down
Loading
Loading