Skip to content

Commit f9efd1f

Browse files
committed
plpgsql: reference hidden variables only by ordinal
Hidden variables cannot be referenced by the user, and are used internally to implement FOR LOOP bounds and counters. Previously, hidden variables were referenced via metadata name. This was fragile, and in fact had a bug because different loops used the same hidden variables names, leading to incorrect results for nested loops. This commit fixes the bug by tracking hidden variables by their ordinal position among all variables in the current scope. The name of a hidden variable is now only used for display. This ensures that a hidden variable can always be uniquely identified, even within nested PL/pgSQL constructs. Fixes #144321 Release note (bug fix): Fixed a bug existing since v24.3 that could cause PL/pgSQL FOR loops to terminate early or show incorrect values for the counter variable. The bug occurred when two or more FOR loops were nested within one another.
1 parent 3693f3f commit f9efd1f

File tree

4 files changed

+123
-92
lines changed

4 files changed

+123
-92
lines changed

pkg/ccl/logictestccl/testdata/logic_test/udf_plpgsql

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3373,3 +3373,39 @@ SELECT f143887(100, 200);
33733373
300
33743374

33753375
subtest end
3376+
3377+
# Nested FOR loops should not interfere with one another.
3378+
subtest regression_144321
3379+
3380+
statement ok
3381+
CREATE FUNCTION f144321() RETURNS INT LANGUAGE PLpgSQL AS $$
3382+
DECLARE
3383+
outer_counter INT := 0;
3384+
inner_counter INT := 0;
3385+
BEGIN
3386+
FOR i IN 1..4 LOOP
3387+
FOR j IN 1..2 LOOP
3388+
RAISE NOTICE 'i: % j: %', i, j;
3389+
inner_counter := inner_counter + 1;
3390+
END LOOP;
3391+
outer_counter := outer_counter + 1;
3392+
END LOOP;
3393+
RAISE NOTICE 'outer_counter: % inner_counter: %', outer_counter, inner_counter;
3394+
RETURN 0;
3395+
END
3396+
$$;
3397+
3398+
query T noticetrace
3399+
SELECT f144321();
3400+
----
3401+
NOTICE: i: 1 j: 1
3402+
NOTICE: i: 1 j: 2
3403+
NOTICE: i: 2 j: 1
3404+
NOTICE: i: 2 j: 2
3405+
NOTICE: i: 3 j: 1
3406+
NOTICE: i: 3 j: 2
3407+
NOTICE: i: 4 j: 1
3408+
NOTICE: i: 4 j: 2
3409+
NOTICE: outer_counter: 4 inner_counter: 8
3410+
3411+
subtest end

pkg/sql/opt/optbuilder/plpgsql.go

Lines changed: 77 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -363,10 +363,11 @@ type plBlock struct {
363363
// cursor before it is opened.
364364
cursors map[ast.Variable]ast.CursorDeclaration
365365

366-
// hiddenVars is an ordered list of *hidden* variables that were not declared
367-
// by the user, but are used internally by the builder. Hidden variables are
368-
// not visible to the user, and are identified by their metadata name. They
369-
// can only be assigned to by directly calling assignToHiddenVariable().
366+
// hiddenVars lists the names of *hidden* variables that were not declared by
367+
// the user, but are used internally by the builder. Hidden variables are
368+
// not visible to the user, and are identified only by their ordinal position.
369+
// They can only be assigned to by directly calling assignToHiddenVariable().
370+
// The name of a hidden variable is only used for display purposes.
370371
//
371372
// As an example, the internal counter variable for a FOR loop is a
372373
// hidden variable.
@@ -376,7 +377,9 @@ type plBlock struct {
376377
hiddenVars []string
377378

378379
// hiddenVarTypes maps from each hidden variable in the scope to its type.
379-
hiddenVarTypes map[string]*types.T
380+
// It is a list instead of a map because hidden variables are only identified
381+
// by their ordinal position.
382+
hiddenVarTypes []*types.T
380383

381384
// hasExceptionHandler tracks whether this block has an exception handler.
382385
hasExceptionHandler bool
@@ -1358,27 +1361,22 @@ func (b *plpgsqlBuilder) handleIntForLoop(
13581361
stepName = "_loop_step"
13591362
counterName = "_loop_counter"
13601363
)
1361-
b.addHiddenVariable(lowerName, types.Int)
1362-
b.addHiddenVariable(upperName, types.Int)
1363-
b.addHiddenVariable(stepName, types.Int)
1364-
b.addHiddenVariable(counterName, types.Int)
1364+
// User-visible variable must be declared before hidden variables in a block.
13651365
b.addVariable(forLoop.Target[0], types.Int)
1366+
lowerOrd := b.addHiddenVariable(lowerName, types.Int)
1367+
upperOrd := b.addHiddenVariable(upperName, types.Int)
1368+
stepOrd := b.addHiddenVariable(stepName, types.Int)
1369+
counterOrd := b.addHiddenVariable(counterName, types.Int)
13661370

13671371
// Initialize the constant bounds and step size.
13681372
stepSize := control.Step
13691373
if stepSize == nil {
13701374
// The default step size is 1.
13711375
stepSize = tree.NewDInt(1)
13721376
}
1373-
s = b.assignToHiddenVariable(s, lowerName, control.Lower)
1374-
s = b.assignToHiddenVariable(s, upperName, control.Upper)
1375-
s = b.assignToHiddenVariable(s, stepName, stepSize)
1376-
1377-
// When referencing a hidden variable, make sure to check the correct scope,
1378-
// as different columns can represent the variable depending on context.
1379-
refHiddenVar := func(s *scope, name string) *scopeColumn {
1380-
return s.findAnonymousColumnWithMetadataName(name)
1381-
}
1377+
s = b.assignToHiddenVariable(s, lowerOrd, control.Lower)
1378+
s = b.assignToHiddenVariable(s, upperOrd, control.Upper)
1379+
s = b.assignToHiddenVariable(s, stepOrd, stepSize)
13821380

13831381
// Add runtime checks for the bounds and step size.
13841382
branches := make(memo.ScalarListExpr, 0, 4)
@@ -1393,21 +1391,21 @@ func (b *plpgsqlBuilder) handleIntForLoop(
13931391
message := fmt.Sprintf("%s of FOR loop cannot be null", context)
13941392
addCheck(message, pgcode.NullValueNotAllowed.String(), checkCond)
13951393
}
1396-
addNullCheck("lower bound" /* context */, refHiddenVar(s, lowerName))
1397-
addNullCheck("upper bound" /* context */, refHiddenVar(s, upperName))
1398-
addNullCheck("BY value" /* context */, refHiddenVar(s, stepName))
1394+
addNullCheck("lower bound" /* context */, s.findFuncArgCol(lowerOrd))
1395+
addNullCheck("upper bound" /* context */, s.findFuncArgCol(upperOrd))
1396+
addNullCheck("BY value" /* context */, s.findFuncArgCol(stepOrd))
13991397
addCheck("BY value of FOR loop must be greater than zero", /* message */
14001398
pgcode.InvalidParameterValue.String(),
14011399
b.buildSQLExpr(&tree.ComparisonExpr{
14021400
Operator: treecmp.MakeComparisonOperator(treecmp.LE),
1403-
Left: refHiddenVar(s, stepName),
1401+
Left: s.findFuncArgCol(stepOrd),
14041402
Right: tree.DZero,
14051403
}, types.Bool, s))
14061404
b.addRuntimeCheck(s, branches, raiseErrArgs)
14071405

14081406
// Initialize the loop counter target variables with the lower bound.
1409-
s = b.assignToHiddenVariable(s, counterName, refHiddenVar(s, lowerName))
1410-
s = b.addPLpgSQLAssign(s, forLoop.Target[0], refHiddenVar(s, lowerName), noIndirection)
1407+
s = b.assignToHiddenVariable(s, counterOrd, s.findFuncArgCol(lowerOrd))
1408+
s = b.addPLpgSQLAssign(s, forLoop.Target[0], s.findFuncArgCol(lowerOrd), noIndirection)
14111409

14121410
// The looping will be implemented by two continuations: one to execute the
14131411
// loop body, and one to increment the counter variable. The loop body and
@@ -1429,8 +1427,8 @@ func (b *plpgsqlBuilder) handleIntForLoop(
14291427
}
14301428
cond := &tree.ComparisonExpr{
14311429
Operator: cmpOp,
1432-
Left: refHiddenVar(loopCon.s, counterName),
1433-
Right: refHiddenVar(loopCon.s, upperName),
1430+
Left: loopCon.s.findFuncArgCol(counterOrd),
1431+
Right: loopCon.s.findFuncArgCol(upperOrd),
14341432
}
14351433
ifStmt := &ast.If{Condition: cond, ThenBody: forLoop.Body, ElseBody: []ast.Statement{&ast.Exit{}}}
14361434
b.appendPlpgSQLStmts(&loopCon, []ast.Statement{ifStmt})
@@ -1449,12 +1447,12 @@ func (b *plpgsqlBuilder) handleIntForLoop(
14491447
}
14501448
inc := &tree.BinaryExpr{
14511449
Operator: binOp,
1452-
Left: refHiddenVar(incScope, counterName),
1453-
Right: refHiddenVar(incScope, stepName),
1450+
Left: incScope.findFuncArgCol(counterOrd),
1451+
Right: incScope.findFuncArgCol(stepOrd),
14541452
}
1455-
incScope = b.assignToHiddenVariable(incScope, counterName, inc)
1453+
incScope = b.assignToHiddenVariable(incScope, counterOrd, inc)
14561454
incScope = b.addPLpgSQLAssign(
1457-
incScope, forLoop.Target[0], refHiddenVar(incScope, counterName), noIndirection,
1455+
incScope, forLoop.Target[0], incScope.findFuncArgCol(counterOrd), noIndirection,
14581456
)
14591457
// Call recursively into the loop body continuation.
14601458
incScope = b.callContinuation(&loopCon, incScope)
@@ -1578,20 +1576,20 @@ func (b *plpgsqlBuilder) addPLpgSQLAssign(
15781576

15791577
// assignToHiddenVariable is similar to addPLpgSQLAssign, but it assigns to a
15801578
// hidden variable that is not visible to the user.
1581-
func (b *plpgsqlBuilder) assignToHiddenVariable(inScope *scope, name string, val ast.Expr) *scope {
1582-
typ, ord := b.resolveHiddenVariableForAssign(name)
1579+
func (b *plpgsqlBuilder) assignToHiddenVariable(inScope *scope, ord int, val ast.Expr) *scope {
1580+
typ, name := b.resolveVariableForAssignByOrd(ord)
15831581
assignScope := inScope.push()
15841582
for i := range inScope.cols {
15851583
col := &inScope.cols[i]
1586-
if col.name.MetadataName() == name {
1584+
if col.getParamOrd() == ord {
15871585
// Allow the assignment to shadow previous values for this column.
15881586
continue
15891587
}
15901588
// If the column is not an outer column, add the column as a pass-through
15911589
// column from the previous scope.
15921590
assignScope.appendColumn(col)
15931591
}
1594-
colName := scopeColName("").WithMetadataName(name)
1592+
colName := scopeColName("").WithMetadataName(string(name))
15951593
scalar := b.buildSQLExpr(val, typ, inScope)
15961594
b.addBarrierIfVolatile(inScope, scalar)
15971595
col := b.ob.synthesizeColumn(assignScope, colName, typ, nil, scalar)
@@ -2240,10 +2238,10 @@ func (b *plpgsqlBuilder) makeContinuation(conName string) continuation {
22402238
for _, name := range block.vars {
22412239
addParam(scopeColName(name), block.varTypes[name])
22422240
}
2243-
for _, name := range block.hiddenVars {
2241+
for varIdx, name := range block.hiddenVars {
22442242
// Do not give the column constructed for a hidden variable a reference
22452243
// name, since hidden variables cannot be referenced by the user.
2246-
addParam(scopeColName("").WithMetadataName(name), block.hiddenVarTypes[name])
2244+
addParam(scopeColName("").WithMetadataName(name), block.hiddenVarTypes[varIdx])
22472245
}
22482246
}
22492247
b.ensureScopeHasExpr(s)
@@ -2378,7 +2376,7 @@ func (b *plpgsqlBuilder) makeContinuationArgs(con *continuation, s *scope) memo.
23782376
args = append(args, b.ob.factory.ConstructVariable(source.(*scopeColumn).id))
23792377
}
23802378
for _, name := range block.hiddenVars {
2381-
col := s.findAnonymousColumnWithMetadataName(name)
2379+
col := s.findFuncArgCol(len(args))
23822380
if col == nil {
23832381
panic(errors.AssertionFailedf("hidden variable %s not found", name))
23842382
}
@@ -2496,12 +2494,12 @@ func (b *plpgsqlBuilder) coerceType(scalar opt.ScalarExpr, typ *types.T) opt.Sca
24962494
// the given name, as well as its ordinal position within the set of all
24972495
// variables in the current scope. It throws an error if no such variable
24982496
// exists.
2499-
func (b *plpgsqlBuilder) resolveVariableForAssign(name ast.Variable) (varTyp *types.T, varIdx int) {
2497+
func (b *plpgsqlBuilder) resolveVariableForAssign(name ast.Variable) (typ *types.T, ord int) {
25002498
// Search the blocks in reverse order to ensure that more recent declarations
25012499
// are encountered first.
25022500
for i := len(b.blocks) - 1; i >= 0; i-- {
25032501
block := &b.blocks[i]
2504-
typ, ok := block.varTypes[name]
2502+
varTyp, ok := block.varTypes[name]
25052503
if !ok {
25062504
continue
25072505
}
@@ -2512,42 +2510,49 @@ func (b *plpgsqlBuilder) resolveVariableForAssign(name ast.Variable) (varTyp *ty
25122510
}
25132511
// Get the ordinal position of the variable within the set of variables in
25142512
// the current scope.
2515-
varIdx = b.variableCount(i)
2513+
ord = b.variableCount(i)
25162514
for j := range block.vars {
25172515
if block.vars[j] == name {
2518-
varIdx += j
2516+
ord += j
25192517
break
25202518
}
25212519
}
2522-
return typ, varIdx
2520+
return varTyp, ord
25232521
}
25242522
panic(pgerror.Newf(pgcode.Syntax, "\"%s\" is not a known variable", name))
25252523
}
25262524

2527-
// resolveHiddenVariableForAssign is similar to resolveVariableForAssign, but
2528-
// applies to hidden variables, which are identified only by their name in the
2529-
// query's metadata. It panics if the hidden variable is not found.
2530-
func (b *plpgsqlBuilder) resolveHiddenVariableForAssign(name string) (varTyp *types.T, varIdx int) {
2531-
// Search the blocks in reverse order to ensure that more recent declarations
2532-
// are encountered first.
2533-
for i := len(b.blocks) - 1; i >= 0; i-- {
2525+
// resolveVariableForAssignByOrd is similar to resolveVariableForAssign, but
2526+
// resolves a variable by its ordinal position in the list of all variables in
2527+
// the current scope. It returns the name and type of the variable. It panics if
2528+
// the variable is not found.
2529+
//
2530+
// NOTE: unlike resolveVariableForAssign, resolveVariableForAssignByOrd is able
2531+
// to resolve hidden variables.
2532+
func (b *plpgsqlBuilder) resolveVariableForAssignByOrd(ord int) (typ *types.T, name ast.Variable) {
2533+
originalOrd := ord
2534+
for i := range b.blocks {
25342535
block := &b.blocks[i]
2535-
typ, ok := block.hiddenVarTypes[name]
2536-
if !ok {
2537-
continue
2538-
}
2539-
// Get the ordinal position of the variable within the set of variables in
2540-
// the current scope.
2541-
varIdx = b.variableCount(i) + len(block.vars)
2542-
for j := range block.hiddenVars {
2543-
if block.hiddenVars[j] == name {
2544-
varIdx += j
2545-
break
2536+
if ord < len(block.vars) {
2537+
// This is a user-visible variable.
2538+
name = block.vars[ord]
2539+
if block.constants != nil {
2540+
if _, ok := block.constants[name]; ok {
2541+
panic(pgerror.Newf(pgcode.ErrorInAssignment, "variable \"%s\" is declared CONSTANT", name))
2542+
}
25462543
}
2544+
return block.varTypes[name], name
2545+
}
2546+
ord -= len(block.vars)
2547+
if ord < len(block.hiddenVars) {
2548+
// This is a hidden variable.
2549+
return block.hiddenVarTypes[ord], ast.Variable(block.hiddenVars[ord])
25472550
}
2548-
return typ, varIdx
2551+
ord -= len(block.hiddenVars)
25492552
}
2550-
panic(errors.AssertionFailedf("hidden variable %s not found", name))
2553+
// Increment the ordinal for the error message, since the placeholder syntax
2554+
// is 1-based.
2555+
panic(pgerror.Newf(pgcode.Syntax, "\"$%d\" is not a known variable", originalOrd+1))
25512556
}
25522557

25532558
// projectTupleAsIntoTarget maps from the elements of a tuple column to the
@@ -2739,6 +2744,9 @@ func (b *plpgsqlBuilder) getContinuation(
27392744
// PL/pgSQL block scope.
27402745
func (b *plpgsqlBuilder) addVariable(name ast.Variable, typ *types.T) {
27412746
curBlock := b.block()
2747+
if len(curBlock.hiddenVars) > 0 {
2748+
panic(errors.AssertionFailedf("hidden variables must be declared after all visible variables"))
2749+
}
27422750
if _, ok := curBlock.varTypes[name]; ok {
27432751
panic(pgerror.Newf(pgcode.Syntax, "duplicate declaration at or near \"%s\"", name))
27442752
}
@@ -2756,14 +2764,15 @@ func (b *plpgsqlBuilder) addVariable(name ast.Variable, typ *types.T) {
27562764
}
27572765

27582766
// addHiddenVariable adds a hidden variable with the given (metadata) name and
2759-
// type to the current PL/pgSQL block scope.
2760-
func (b *plpgsqlBuilder) addHiddenVariable(metadataName string, typ *types.T) {
2767+
// type to the current PL/pgSQL block scope. It returns the ordinal position of
2768+
// the variable within the set of all variables in the current scope. This will
2769+
// be used to uniquely identify the hidden variable going forward.
2770+
func (b *plpgsqlBuilder) addHiddenVariable(metadataName string, typ *types.T) (ord int) {
2771+
ord = b.variableCount(len(b.blocks))
27612772
curBlock := b.block()
27622773
curBlock.hiddenVars = append(curBlock.hiddenVars, metadataName)
2763-
if curBlock.hiddenVarTypes == nil {
2764-
curBlock.hiddenVarTypes = make(map[string]*types.T)
2765-
}
2766-
curBlock.hiddenVarTypes[metadataName] = typ
2774+
curBlock.hiddenVarTypes = append(curBlock.hiddenVarTypes, typ)
2775+
return ord
27672776
}
27682777

27692778
// block returns the block for the current PL/pgSQL block.

pkg/sql/opt/optbuilder/scope.go

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -673,33 +673,19 @@ func (s *scope) findExistingCol(expr tree.TypedExpr, allowSideEffects bool) *sco
673673
}
674674

675675
// findFuncArgCol returns the column that represents a function argument and has
676-
// an ordinal matching the given placeholder index. If such a column is not
677-
// found in the current scope, ancestor scopes are successively searched. If no
678-
// matching function argument column is found, nil is returned.
679-
func (s *scope) findFuncArgCol(idx tree.PlaceholderIdx) *scopeColumn {
676+
// an ordinal matching the given 0-based ordinal position. If such a column is
677+
// not found in the current scope, ancestor scopes are successively searched.
678+
// If no matching function argument column is found, nil is returned.
679+
func (s *scope) findFuncArgCol(ord int) *scopeColumn {
680680
for ; s != nil; s = s.parent {
681-
if s.checkMaxParamOrd && int(idx) > (s.maxParamOrd-1) {
681+
if s.checkMaxParamOrd && ord > (s.maxParamOrd-1) {
682682
// Referencing this function parameter by ordinal is not allowed. Subtract
683683
// 1 from maxParamOrd to convert it to a 0-based ordinal.
684684
return nil
685685
}
686686
for i := range s.cols {
687687
col := &s.cols[i]
688-
if col.funcParamReferencedBy(idx) {
689-
return col
690-
}
691-
}
692-
}
693-
return nil
694-
}
695-
696-
// findAnonymousColumnWithMetadataName returns the first anonymous column that
697-
// has the given name in the query metadata.
698-
func (s *scope) findAnonymousColumnWithMetadataName(metadataName string) *scopeColumn {
699-
for ; s != nil; s = s.parent {
700-
for i := range s.cols {
701-
col := &s.cols[i]
702-
if col.name.refName == "" && col.name.metadataName == metadataName {
688+
if col.funcParamReferencedBy(ord) {
703689
return col
704690
}
705691
}
@@ -1120,7 +1106,7 @@ func (s *scope) VisitPre(expr tree.Expr) (recurse bool, newExpr tree.Expr) {
11201106
// NOTE: This likely won't work if we want to allow PREPARE statements
11211107
// within user-defined function bodies. We'll need to avoid replacing
11221108
// placeholders that are prepared statement parameters.
1123-
if col := s.findFuncArgCol(t.Idx); col != nil {
1109+
if col := s.findFuncArgCol(int(t.Idx)); col != nil {
11241110
return false, col
11251111
}
11261112

pkg/sql/opt/optbuilder/scope_column.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,9 @@ func (c *scopeColumn) getParamOrd() int {
145145
}
146146

147147
// funcParamReferencedBy returns true if the scopeColumn is a function parameter
148-
// column that can be referenced by the given placeholder.
149-
func (c *scopeColumn) funcParamReferencedBy(idx tree.PlaceholderIdx) bool {
150-
return c.paramOrd > 0 && tree.PlaceholderIdx(c.paramOrd-1) == idx
148+
// column that can be referenced by the given 0-based ordinal.
149+
func (c *scopeColumn) funcParamReferencedBy(ord int) bool {
150+
return c.paramOrd > 0 && int(c.paramOrd-1) == ord
151151
}
152152

153153
// clearName sets the empty table and column name. This is used to make the

0 commit comments

Comments
 (0)