From d12fe78d5f62922a3663eecd745f3d39b40adaed Mon Sep 17 00:00:00 2001 From: Drew Kimball Date: Tue, 26 Mar 2024 03:53:25 -0600 Subject: [PATCH 1/3] sql: defer tail-call identification until execbuilding This commit changes the way routine tail-calls are handled. Before, only PL/pgSQL sub-routines were considered as tail-calls, and this was determined by a `TailCall` property that was set during optbuilding. This approach was fragile, did not work for explicit tail-calls, and did not work well with nested routine calls in general. Now, tail-calls are determined after optimization, during execbuilding. This will allow explicit (user-specified) tail calls to be optimized. It also prevents inlining rules from causing correctness bugs, since the old `TailCall` property only applied to the original calling routine. See `ExtractTailCalls` for further details. The next commit will add additional testing. Informs #120916 Release note: None --- pkg/sql/opt/exec/execbuilder/builder.go | 5 ++ pkg/sql/opt/exec/execbuilder/relational.go | 8 +-- pkg/sql/opt/exec/execbuilder/scalar.go | 26 +++++++- pkg/sql/opt/memo/expr.go | 3 + pkg/sql/opt/memo/extract.go | 75 ++++++++++++++++++++++ pkg/sql/opt/norm/decorrelate_funcs.go | 10 ++- pkg/sql/opt/ops/scalar.opt | 5 -- pkg/sql/opt/optbuilder/plpgsql.go | 4 +- pkg/sql/opt/optbuilder/routine.go | 1 + 9 files changed, 121 insertions(+), 16 deletions(-) diff --git a/pkg/sql/opt/exec/execbuilder/builder.go b/pkg/sql/opt/exec/execbuilder/builder.go index 7f5c6c7c5fea..612619899955 100644 --- a/pkg/sql/opt/exec/execbuilder/builder.go +++ b/pkg/sql/opt/exec/execbuilder/builder.go @@ -111,6 +111,11 @@ type Builder struct { // subqueries for statements inside a UDF. planLazySubqueries bool + // tailCalls is used when building the last body statement of a routine. It + // identifies nested routines that are in tail-call position. This information + // is used to determine whether tail-call optimization is applicable. + tailCalls map[*memo.UDFCallExpr]struct{} + // -- output -- // flags tracks various properties of the plan accumulated while building. diff --git a/pkg/sql/opt/exec/execbuilder/relational.go b/pkg/sql/opt/exec/execbuilder/relational.go index 189236be45fd..043f85e18e71 100644 --- a/pkg/sql/opt/exec/execbuilder/relational.go +++ b/pkg/sql/opt/exec/execbuilder/relational.go @@ -3390,10 +3390,10 @@ func (b *Builder) buildCall(c *memo.CallExpr) (_ execPlan, outputCols colOrdMap, udf.Def.CalledOnNullInput, udf.Def.MultiColDataSource, udf.Def.SetReturning, - udf.TailCall, - true, /* procedure */ - nil, /* blockState */ - nil, /* cursorDeclaration */ + false, /* tailCall */ + true, /* procedure */ + nil, /* blockState */ + nil, /* cursorDeclaration */ ) var ep execPlan diff --git a/pkg/sql/opt/exec/execbuilder/scalar.go b/pkg/sql/opt/exec/execbuilder/scalar.go index aeeced100cce..1c7fcc2f635a 100644 --- a/pkg/sql/opt/exec/execbuilder/scalar.go +++ b/pkg/sql/opt/exec/execbuilder/scalar.go @@ -953,6 +953,14 @@ func (b *Builder) buildUDF(ctx *buildScalarCtx, scalar opt.ScalarExpr) (tree.Typ b.initRoutineExceptionHandler(blockState, udf.Def.ExceptionBlock) } + // Execution expects there to be more than one body statement if a cursor is + // opened. + if udf.Def.CursorDeclaration != nil && len(udf.Def.Body) <= 1 { + panic(errors.AssertionFailedf( + "expected more than one body statement for a routine that opens a cursor", + )) + } + // Create a tree.RoutinePlanFn that can plan the statements in the UDF body. // TODO(mgartner): Add support for WITH expressions inside UDF bodies. planGen := b.buildRoutinePlanGenerator( @@ -969,6 +977,10 @@ func (b *Builder) buildUDF(ctx *buildScalarCtx, scalar opt.ScalarExpr) (tree.Typ // statements. enableStepping := udf.Def.Volatility == volatility.Volatile + // The calling routine, if any, will have already determined whether this + // routine is in tail-call position. + _, tailCall := b.tailCalls[udf] + return tree.NewTypedRoutineExpr( udf.Def.Name, args, @@ -978,7 +990,7 @@ func (b *Builder) buildUDF(ctx *buildScalarCtx, scalar opt.ScalarExpr) (tree.Typ udf.Def.CalledOnNullInput, udf.Def.MultiColDataSource, udf.Def.SetReturning, - udf.TailCall, + tailCall, false, /* procedure */ blockState, udf.Def.CursorDeclaration, @@ -1173,12 +1185,23 @@ func (b *Builder) buildRoutinePlanGenerator( return err } + // Identify nested routines that are in tail-call position, and cache them + // in the Builder. When a nested routine is evaluated, this information + // may be used to enable tail-call optimization. + isFinalPlan := i == len(stmts)-1 + var tailCalls map[*memo.UDFCallExpr]struct{} + if isFinalPlan { + tailCalls = make(map[*memo.UDFCallExpr]struct{}) + memo.ExtractTailCalls(optimizedExpr, tailCalls) + } + // Build the memo into a plan. ef := ref.(exec.Factory) eb := New(ctx, ef, &o, f.Memo(), b.catalog, optimizedExpr, b.semaCtx, b.evalCtx, false /* allowAutoCommit */, b.IsANSIDML) eb.withExprs = withExprs eb.disableTelemetry = true eb.planLazySubqueries = true + eb.tailCalls = tailCalls plan, err := eb.Build() if err != nil { if errors.IsAssertionFailure(err) { @@ -1194,7 +1217,6 @@ func (b *Builder) buildRoutinePlanGenerator( if len(eb.subqueries) > 0 { return expectedLazyRoutineError("subquery") } - isFinalPlan := i == len(stmts)-1 var stmtForDistSQLDiagram string if i < len(stmtStr) { stmtForDistSQLDiagram = stmtStr[i] diff --git a/pkg/sql/opt/memo/expr.go b/pkg/sql/opt/memo/expr.go index 82e74d5f8b91..c085c5febb65 100644 --- a/pkg/sql/opt/memo/expr.go +++ b/pkg/sql/opt/memo/expr.go @@ -717,6 +717,9 @@ type UDFDefinition struct { // builtin function. RoutineType tree.RoutineType + // RoutineLang indicates the language of the routine (SQL or PL/pgSQL). + RoutineLang tree.RoutineLanguage + // Params is the list of columns representing parameters of the function. The // i-th column in the list corresponds to the i-th parameter of the function. // During execution of the UDF, these columns are replaced with the arguments diff --git a/pkg/sql/opt/memo/extract.go b/pkg/sql/opt/memo/extract.go index b23410562838..04feb5a66a53 100644 --- a/pkg/sql/opt/memo/extract.go +++ b/pkg/sql/opt/memo/extract.go @@ -450,3 +450,78 @@ func ExtractValueForConstColumn( } return nil } + +// ExtractTailCalls traverses the given expression tree, searching for routines +// that are in tail-call position relative to the (assumed) calling routine. +// ExtractTailCalls assumes that the given expression is the last body statement +// of the calling routine, and that the map is already initialized. +// +// In order for a nested routine to qualify as a tail-call, the following +// condition must be true: If the nested routine is evaluated, then the calling +// routine must return the result of the nested routine without further +// modification. This means even simple expressions like CAST are not allowed. +// +// ExtractTailCalls is best-effort, but is sufficient to identify the tail-calls +// produced among PL/pgSQL sub-routines. +// +// NOTE: ExtractTailCalls does not take into account whether the calling routine +// has an exception handler. The execution engine must take this into account +// before applying tail-call optimization. +func ExtractTailCalls(expr opt.Expr, tailCalls map[*UDFCallExpr]struct{}) { + switch t := expr.(type) { + case *ProjectExpr: + // * The cardinality cannot be greater than one: Otherwise, a nested routine + // will be evaluated more than once, and all evaluations other than the last + // are not tail-calls. + // + // * There must be a single projection: the execution does not provide + // guarantees about order of evaluation for projections (though it may in + // the future). + // + // * The passthrough set must be empty: Otherwise, the result of the nested + // routine cannot directly be used as the result of the calling routine. + // + // * No routine in the input of the project can be a tail-call, since the + // Project will perform work after the nested routine evaluates. + // Note: this condition is enforced by simply not calling ExtractTailCalls + // on the input of the Project. + if t.Relational().Cardinality.IsZeroOrOne() && + len(t.Projections) == 1 && t.Passthrough.Empty() { + ExtractTailCalls(t.Projections[0].Element, tailCalls) + } + + case *ValuesExpr: + // Allow only the case where the Values expression contains only a single + // expression. Note: it may be possible to make an explicit guarantee that + // expressions in a row are evaluated in order, in which case it would be + // sufficient to ensure that the nested routine is in the last column. + if len(t.Rows) == 1 && len(t.Rows[0].(*TupleExpr).Elems) == 1 { + ExtractTailCalls(t.Rows[0].(*TupleExpr).Elems[0], tailCalls) + } + + case *SubqueryExpr: + // A subquery within a routine is lazily evaluated and passes through a + // single input row. Similar to Project, we require that the input have only + // one row and one column, since otherwise work may happen after the nested + // routine evaluates. + if t.Input.Relational().Cardinality.IsZeroOrOne() && + t.Input.Relational().OutputCols.Len() == 1 { + ExtractTailCalls(t.Input, tailCalls) + } + + case *CaseExpr: + // Case expressions guarantee that exactly one branch is evaluated, and pass + // through the result of the chosen branch. Therefore, a routine within a + // CASE branch can be a tail-call. + for i := range t.Whens { + ExtractTailCalls(t.Whens[i].(*WhenExpr).Value, tailCalls) + } + ExtractTailCalls(t.OrElse, tailCalls) + + case *UDFCallExpr: + // If we reached a scalar UDFCall expression, it is a tail call. + if !t.Def.SetReturning { + tailCalls[t] = struct{}{} + } + } +} diff --git a/pkg/sql/opt/norm/decorrelate_funcs.go b/pkg/sql/opt/norm/decorrelate_funcs.go index 8359e1b640c4..588747a83b74 100644 --- a/pkg/sql/opt/norm/decorrelate_funcs.go +++ b/pkg/sql/opt/norm/decorrelate_funcs.go @@ -14,6 +14,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/opt" "github.com/cockroachdb/cockroach/pkg/sql/opt/memo" "github.com/cockroachdb/cockroach/pkg/sql/opt/props" + "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" "github.com/cockroachdb/cockroach/pkg/sql/types" "github.com/cockroachdb/errors" "github.com/cockroachdb/redact" @@ -65,9 +66,12 @@ func (c *CustomFuncs) deriveHasUnhoistableExpr(expr opt.Expr) bool { // cannot be reordered with other expressions. return true case *memo.UDFCallExpr: - if t.TailCall { - // A routine with the "tail-call" property cannot be reordered with other - // expressions, since it may then no longer be in tail-call position. + if t.Def.RoutineLang == tree.RoutineLangPLpgSQL { + // Hoisting a PL/pgSQL sub-routine could move it out of tail-call + // position, forcing inefficient nested execution. + // + // TODO(#119956): consider relaxing this for routines which aren't already + // in tail-call position. return true } } diff --git a/pkg/sql/opt/ops/scalar.opt b/pkg/sql/opt/ops/scalar.opt index 4d62fbfd16db..88c715ba7999 100644 --- a/pkg/sql/opt/ops/scalar.opt +++ b/pkg/sql/opt/ops/scalar.opt @@ -1272,11 +1272,6 @@ define UDFCall { define UDFCallPrivate { # Def points to the UDF SQL body. Def UDFDefinition - - # TailCall indicates whether the UDF is in tail-call position, meaning that - # it is nested in a parent routine which will not perform any additional - # processing once this call is evaluated. - TailCall bool } # TxnControl allows PL/pgSQL stored procedures to pause their execution, commit diff --git a/pkg/sql/opt/optbuilder/plpgsql.go b/pkg/sql/opt/optbuilder/plpgsql.go index ff14ad16938b..513860ab0079 100644 --- a/pkg/sql/opt/optbuilder/plpgsql.go +++ b/pkg/sql/opt/optbuilder/plpgsql.go @@ -1620,6 +1620,7 @@ func (b *plpgsqlBuilder) makeContinuation(conName string) continuation { CalledOnNullInput: true, BlockState: b.block().state, RoutineType: tree.UDFRoutine, + RoutineLang: tree.RoutineLangPLpgSQL, }, typ: continuationDefault, s: s, @@ -1671,9 +1672,8 @@ func (b *plpgsqlBuilder) callContinuation(con *continuation, s *scope) *scope { if con == nil { return b.handleEndOfFunction(s) } - // PLpgSQL continuation routines are always in tail-call position. args := b.makeContinuationArgs(con, s) - call := b.ob.factory.ConstructUDFCall(args, &memo.UDFCallPrivate{Def: con.def, TailCall: true}) + call := b.ob.factory.ConstructUDFCall(args, &memo.UDFCallPrivate{Def: con.def}) b.addBarrierIfVolatile(s, call) returnColName := scopeColName("").WithMetadataName(con.def.Name) diff --git a/pkg/sql/opt/optbuilder/routine.go b/pkg/sql/opt/optbuilder/routine.go index a9bab6e48169..129bdb1b8187 100644 --- a/pkg/sql/opt/optbuilder/routine.go +++ b/pkg/sql/opt/optbuilder/routine.go @@ -393,6 +393,7 @@ func (b *Builder) buildRoutine( CalledOnNullInput: o.CalledOnNullInput, MultiColDataSource: isMultiColDataSource, RoutineType: o.Type, + RoutineLang: o.Language, Body: body, BodyProps: bodyProps, BodyStmts: bodyStmts, From 15a74f42002b79a320a66293df822443956889e7 Mon Sep 17 00:00:00 2001 From: Drew Kimball Date: Tue, 26 Mar 2024 04:06:12 -0600 Subject: [PATCH 2/3] sql: add tests for tail-call property This commit adds tests for the `ExtractTailCalls` function from the previous commit, and adds a `tail-call` field to `UDFCall` expressions that are in tail-call position in another routine. It also adds a regression test for #120916. Informs #120916 Release note: None --- .../testdata/logic_test/explain_call_plpgsql | 1 + .../testdata/logic_test/nested_routines | 27 + .../tests/3node-tenant/generated_test.go | 7 + .../tests/fakedist-disk/BUILD.bazel | 2 +- .../tests/fakedist-disk/generated_test.go | 7 + .../tests/fakedist-vec-off/BUILD.bazel | 2 +- .../tests/fakedist-vec-off/generated_test.go | 7 + .../logictestccl/tests/fakedist/BUILD.bazel | 2 +- .../tests/fakedist/generated_test.go | 7 + .../local-legacy-schema-changer/BUILD.bazel | 2 +- .../generated_test.go | 7 + .../tests/local-read-committed/BUILD.bazel | 2 +- .../local-read-committed/generated_test.go | 7 + .../tests/local-vec-off/BUILD.bazel | 2 +- .../tests/local-vec-off/generated_test.go | 7 + pkg/ccl/logictestccl/tests/local/BUILD.bazel | 2 +- .../tests/local/generated_test.go | 7 + pkg/sql/opt/memo/expr_format.go | 21 +- pkg/sql/opt/memo/testdata/logprops/tail-calls | 1053 +++++++++++++++++ pkg/sql/opt/norm/testdata/rules/routine | 2 + .../opt/optbuilder/testdata/procedure_plpgsql | 13 + pkg/sql/opt/optbuilder/testdata/udf_plpgsql | 235 ++++ 22 files changed, 1411 insertions(+), 11 deletions(-) create mode 100644 pkg/ccl/logictestccl/testdata/logic_test/nested_routines create mode 100644 pkg/sql/opt/memo/testdata/logprops/tail-calls diff --git a/pkg/ccl/logictestccl/testdata/logic_test/explain_call_plpgsql b/pkg/ccl/logictestccl/testdata/logic_test/explain_call_plpgsql index 5ea1be3c5785..4ee7658021cb 100644 --- a/pkg/ccl/logictestccl/testdata/logic_test/explain_call_plpgsql +++ b/pkg/ccl/logictestccl/testdata/logic_test/explain_call_plpgsql @@ -85,6 +85,7 @@ call ├── fd: ()-->(5) └── tuple [type=tuple{void}] └── udf: _stmt_raise_1 [type=void] + ├── tail-call ├── args │ └── variable: x:1 [type=int] ├── params: x:2(int) diff --git a/pkg/ccl/logictestccl/testdata/logic_test/nested_routines b/pkg/ccl/logictestccl/testdata/logic_test/nested_routines new file mode 100644 index 000000000000..01a8d86bf100 --- /dev/null +++ b/pkg/ccl/logictestccl/testdata/logic_test/nested_routines @@ -0,0 +1,27 @@ +# LogicTest: !local-mixed-23.1 !local-mixed-23.2 + +# Regression test for #120916 - the nested routine is not in tail-call position, +# and so cannot be a target for TCO. +statement ok +CREATE FUNCTION f_nested(x INT) RETURNS INT AS $$ + BEGIN + x := x * 2; + RETURN x; + END +$$ LANGUAGE PLpgSQL; + +statement ok +CREATE FUNCTION f() RETURNS RECORD AS $$ + DECLARE + a INT := -2; + BEGIN + a := f_nested(a); + RAISE NOTICE 'here'; + RETURN (a, -a); + END +$$ LANGUAGE PLpgSQL; + +query II +SELECT * FROM f() AS g(x INT, y INT); +---- +-4 4 diff --git a/pkg/ccl/logictestccl/tests/3node-tenant/generated_test.go b/pkg/ccl/logictestccl/tests/3node-tenant/generated_test.go index ea18c1bdb19d..d1625e0835e2 100644 --- a/pkg/ccl/logictestccl/tests/3node-tenant/generated_test.go +++ b/pkg/ccl/logictestccl/tests/3node-tenant/generated_test.go @@ -2608,6 +2608,13 @@ func TestTenantLogicCCL_hash_sharded_index_read_committed( runCCLLogicTest(t, "hash_sharded_index_read_committed") } +func TestTenantLogicCCL_nested_routines( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runCCLLogicTest(t, "nested_routines") +} + func TestTenantLogicCCL_new_schema_changer( t *testing.T, ) { diff --git a/pkg/ccl/logictestccl/tests/fakedist-disk/BUILD.bazel b/pkg/ccl/logictestccl/tests/fakedist-disk/BUILD.bazel index 9222a647bb1b..2348d79ae0d5 100644 --- a/pkg/ccl/logictestccl/tests/fakedist-disk/BUILD.bazel +++ b/pkg/ccl/logictestccl/tests/fakedist-disk/BUILD.bazel @@ -12,7 +12,7 @@ go_test( "//build/toolchains:is_heavy": {"test.Pool": "heavy"}, "//conditions:default": {"test.Pool": "large"}, }), - shard_count = 26, + shard_count = 27, tags = [ "ccl_test", "cpu:2", diff --git a/pkg/ccl/logictestccl/tests/fakedist-disk/generated_test.go b/pkg/ccl/logictestccl/tests/fakedist-disk/generated_test.go index efb8333f643f..6efe18f36e0e 100644 --- a/pkg/ccl/logictestccl/tests/fakedist-disk/generated_test.go +++ b/pkg/ccl/logictestccl/tests/fakedist-disk/generated_test.go @@ -99,6 +99,13 @@ func TestCCLLogic_hash_sharded_index_read_committed( runCCLLogicTest(t, "hash_sharded_index_read_committed") } +func TestCCLLogic_nested_routines( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runCCLLogicTest(t, "nested_routines") +} + func TestCCLLogic_new_schema_changer( t *testing.T, ) { diff --git a/pkg/ccl/logictestccl/tests/fakedist-vec-off/BUILD.bazel b/pkg/ccl/logictestccl/tests/fakedist-vec-off/BUILD.bazel index 474815813dbd..d20bad5b2b00 100644 --- a/pkg/ccl/logictestccl/tests/fakedist-vec-off/BUILD.bazel +++ b/pkg/ccl/logictestccl/tests/fakedist-vec-off/BUILD.bazel @@ -12,7 +12,7 @@ go_test( "//build/toolchains:is_heavy": {"test.Pool": "heavy"}, "//conditions:default": {"test.Pool": "large"}, }), - shard_count = 26, + shard_count = 27, tags = [ "ccl_test", "cpu:2", diff --git a/pkg/ccl/logictestccl/tests/fakedist-vec-off/generated_test.go b/pkg/ccl/logictestccl/tests/fakedist-vec-off/generated_test.go index b490824c5ef2..db8eb2e17bd8 100644 --- a/pkg/ccl/logictestccl/tests/fakedist-vec-off/generated_test.go +++ b/pkg/ccl/logictestccl/tests/fakedist-vec-off/generated_test.go @@ -99,6 +99,13 @@ func TestCCLLogic_hash_sharded_index_read_committed( runCCLLogicTest(t, "hash_sharded_index_read_committed") } +func TestCCLLogic_nested_routines( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runCCLLogicTest(t, "nested_routines") +} + func TestCCLLogic_new_schema_changer( t *testing.T, ) { diff --git a/pkg/ccl/logictestccl/tests/fakedist/BUILD.bazel b/pkg/ccl/logictestccl/tests/fakedist/BUILD.bazel index cf89a1265ecd..940d98f323c5 100644 --- a/pkg/ccl/logictestccl/tests/fakedist/BUILD.bazel +++ b/pkg/ccl/logictestccl/tests/fakedist/BUILD.bazel @@ -12,7 +12,7 @@ go_test( "//build/toolchains:is_heavy": {"test.Pool": "heavy"}, "//conditions:default": {"test.Pool": "large"}, }), - shard_count = 27, + shard_count = 28, tags = [ "ccl_test", "cpu:2", diff --git a/pkg/ccl/logictestccl/tests/fakedist/generated_test.go b/pkg/ccl/logictestccl/tests/fakedist/generated_test.go index 5d7bc7fc5cda..d752e9deffbb 100644 --- a/pkg/ccl/logictestccl/tests/fakedist/generated_test.go +++ b/pkg/ccl/logictestccl/tests/fakedist/generated_test.go @@ -99,6 +99,13 @@ func TestCCLLogic_hash_sharded_index_read_committed( runCCLLogicTest(t, "hash_sharded_index_read_committed") } +func TestCCLLogic_nested_routines( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runCCLLogicTest(t, "nested_routines") +} + func TestCCLLogic_new_schema_changer( t *testing.T, ) { diff --git a/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/BUILD.bazel b/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/BUILD.bazel index 23d00a43d2b4..b10d882d5978 100644 --- a/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/BUILD.bazel +++ b/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/BUILD.bazel @@ -9,7 +9,7 @@ go_test( "//pkg/ccl/logictestccl:testdata", # keep ], exec_properties = {"test.Pool": "large"}, - shard_count = 26, + shard_count = 27, tags = [ "ccl_test", "cpu:1", diff --git a/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/generated_test.go b/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/generated_test.go index 7f6fc7dad431..98c042c77ea7 100644 --- a/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/generated_test.go +++ b/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/generated_test.go @@ -99,6 +99,13 @@ func TestCCLLogic_hash_sharded_index_read_committed( runCCLLogicTest(t, "hash_sharded_index_read_committed") } +func TestCCLLogic_nested_routines( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runCCLLogicTest(t, "nested_routines") +} + func TestCCLLogic_new_schema_changer( t *testing.T, ) { diff --git a/pkg/ccl/logictestccl/tests/local-read-committed/BUILD.bazel b/pkg/ccl/logictestccl/tests/local-read-committed/BUILD.bazel index 4798e6cd5b8e..6f0f26ec3a11 100644 --- a/pkg/ccl/logictestccl/tests/local-read-committed/BUILD.bazel +++ b/pkg/ccl/logictestccl/tests/local-read-committed/BUILD.bazel @@ -10,7 +10,7 @@ go_test( "//pkg/sql/opt/exec/execbuilder:testdata", # keep ], exec_properties = {"test.Pool": "large"}, - shard_count = 33, + shard_count = 34, tags = [ "ccl_test", "cpu:1", diff --git a/pkg/ccl/logictestccl/tests/local-read-committed/generated_test.go b/pkg/ccl/logictestccl/tests/local-read-committed/generated_test.go index 914ad76fe16d..3c4d8e428c8e 100644 --- a/pkg/ccl/logictestccl/tests/local-read-committed/generated_test.go +++ b/pkg/ccl/logictestccl/tests/local-read-committed/generated_test.go @@ -126,6 +126,13 @@ func TestReadCommittedLogicCCL_hash_sharded_index_read_committed( runCCLLogicTest(t, "hash_sharded_index_read_committed") } +func TestReadCommittedLogicCCL_nested_routines( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runCCLLogicTest(t, "nested_routines") +} + func TestReadCommittedLogicCCL_new_schema_changer( t *testing.T, ) { diff --git a/pkg/ccl/logictestccl/tests/local-vec-off/BUILD.bazel b/pkg/ccl/logictestccl/tests/local-vec-off/BUILD.bazel index d7c874c807b5..1087a077a893 100644 --- a/pkg/ccl/logictestccl/tests/local-vec-off/BUILD.bazel +++ b/pkg/ccl/logictestccl/tests/local-vec-off/BUILD.bazel @@ -9,7 +9,7 @@ go_test( "//pkg/ccl/logictestccl:testdata", # keep ], exec_properties = {"test.Pool": "large"}, - shard_count = 26, + shard_count = 27, tags = [ "ccl_test", "cpu:1", diff --git a/pkg/ccl/logictestccl/tests/local-vec-off/generated_test.go b/pkg/ccl/logictestccl/tests/local-vec-off/generated_test.go index 39736076be8b..9f2a448b36b3 100644 --- a/pkg/ccl/logictestccl/tests/local-vec-off/generated_test.go +++ b/pkg/ccl/logictestccl/tests/local-vec-off/generated_test.go @@ -99,6 +99,13 @@ func TestCCLLogic_hash_sharded_index_read_committed( runCCLLogicTest(t, "hash_sharded_index_read_committed") } +func TestCCLLogic_nested_routines( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runCCLLogicTest(t, "nested_routines") +} + func TestCCLLogic_new_schema_changer( t *testing.T, ) { diff --git a/pkg/ccl/logictestccl/tests/local/BUILD.bazel b/pkg/ccl/logictestccl/tests/local/BUILD.bazel index 3524b606c516..54c58be6bcbc 100644 --- a/pkg/ccl/logictestccl/tests/local/BUILD.bazel +++ b/pkg/ccl/logictestccl/tests/local/BUILD.bazel @@ -9,7 +9,7 @@ go_test( "//pkg/ccl/logictestccl:testdata", # keep ], exec_properties = {"test.Pool": "large"}, - shard_count = 42, + shard_count = 43, tags = [ "ccl_test", "cpu:1", diff --git a/pkg/ccl/logictestccl/tests/local/generated_test.go b/pkg/ccl/logictestccl/tests/local/generated_test.go index 0bc32da7f3f0..6635dee8eb4a 100644 --- a/pkg/ccl/logictestccl/tests/local/generated_test.go +++ b/pkg/ccl/logictestccl/tests/local/generated_test.go @@ -155,6 +155,13 @@ func TestCCLLogic_hash_sharded_index_read_committed( runCCLLogicTest(t, "hash_sharded_index_read_committed") } +func TestCCLLogic_nested_routines( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runCCLLogicTest(t, "nested_routines") +} + func TestCCLLogic_new_schema_changer( t *testing.T, ) { diff --git a/pkg/sql/opt/memo/expr_format.go b/pkg/sql/opt/memo/expr_format.go index 83122d85b35e..6a88236a2f45 100644 --- a/pkg/sql/opt/memo/expr_format.go +++ b/pkg/sql/opt/memo/expr_format.go @@ -171,6 +171,10 @@ type ExprFmtCtx struct { // seenUDFs is used to ensure that formatting of recursive UDFs does not // infinitely recurse. seenUDFs map[*UDFDefinition]struct{} + + // tailCalls allows for quick lookup of all the routines in tail-call position + // when the last body statement of a routine is formatted. + tailCalls map[*UDFCallExpr]struct{} } // makeExprFmtCtxForString creates an expression formatting context from a new @@ -957,13 +961,18 @@ func (f *ExprFmtCtx) formatScalarWithLabel( } n := tp.Child("body") for i := range def.Body { + stmtNode := n if i == 0 && def.CursorDeclaration != nil { // The first statement is opening a cursor. - cur := n.Child("open-cursor") - f.formatExpr(def.Body[i], cur) - continue + stmtNode = n.Child("open-cursor") + } + prevTailCalls := f.tailCalls + if i == len(def.Body)-1 { + f.tailCalls = make(map[*UDFCallExpr]struct{}) + ExtractTailCalls(def.Body[i], f.tailCalls) } - f.formatExpr(def.Body[i], n) + f.formatExpr(def.Body[i], stmtNode) + f.tailCalls = prevTailCalls } delete(f.seenUDFs, def) } else { @@ -998,6 +1007,10 @@ func (f *ExprFmtCtx) formatScalarWithLabel( if !udf.Def.CalledOnNullInput { tp.Child("strict") } + if _, tailCall := f.tailCalls[udf]; tailCall { + // This routine is in tail-call position in the parent routine. + tp.Child("tail-call") + } formatRoutineArgs(udf.Args, tp) formatUDFDefinition(udf.Def, tp) } diff --git a/pkg/sql/opt/memo/testdata/logprops/tail-calls b/pkg/sql/opt/memo/testdata/logprops/tail-calls new file mode 100644 index 000000000000..14adda5af6f2 --- /dev/null +++ b/pkg/sql/opt/memo/testdata/logprops/tail-calls @@ -0,0 +1,1053 @@ +exec-ddl +CREATE FUNCTION nested() RETURNS INT AS $$ + SELECT 1; +$$ LANGUAGE SQL; +---- + +exec-ddl +CREATE FUNCTION nested_arg(x INT) RETURNS INT AS $$ + SELECT x; +$$ LANGUAGE SQL; +---- + +exec-ddl +CREATE FUNCTION generator() RETURNS SETOF INT AS $$ + VALUES (1), (2), (3); +$$ LANGUAGE SQL; +---- + +exec-ddl +CREATE TABLE t (a INT); +---- + +# ============================================================================== +# Test explicit tail-calls with a SQL routine as the parent. +# ============================================================================== + +# Basic tail call. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + SELECT * FROM t; + SELECT nested(); +$$ LANGUAGE SQL; +---- + +norm format=(hide-all,show-scalars) +VALUES (f()); +---- +values + └── tuple + └── udf: f + └── body + ├── scan t + └── values + └── tuple + └── udf: nested + ├── tail-call + └── body + └── values + └── tuple + └── const: 1 + +# Not a tail-call because it isn't the last body statement. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + SELECT * FROM t; + SELECT nested(); + SELECT 1; +$$ LANGUAGE SQL; +---- + +norm format=(hide-all,show-scalars) +VALUES (f()); +---- +values + └── tuple + └── udf: f + └── body + ├── scan t + ├── values + │ └── tuple + │ └── udf: nested + │ └── body + │ └── values + │ └── tuple + │ └── const: 1 + └── values + └── tuple + └── const: 1 + +# Tail-call with a named result column. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + SELECT * FROM t; + SELECT nested() AS foo; +$$ LANGUAGE SQL; +---- + +norm format=(hide-all,show-scalars) +VALUES (f()); +---- +values + └── tuple + └── udf: f + └── body + ├── scan t + └── values + └── tuple + └── udf: nested + ├── tail-call + └── body + └── values + └── tuple + └── const: 1 + +# Nested routine cannot be a tail-call because it's a data source. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + SELECT * FROM t; + SELECT * FROM nested(); +$$ LANGUAGE SQL; +---- + +norm format=(hide-all,show-scalars) +VALUES (f()); +---- +values + └── tuple + └── udf: f + └── body + ├── scan t + └── limit + ├── project-set + │ ├── values + │ │ └── tuple + │ └── zip + │ └── udf: nested + │ └── body + │ └── values + │ └── tuple + │ └── const: 1 + └── const: 1 + +# Case with a nonempty input with one row. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + SELECT * FROM t; + SELECT nested() FROM (VALUES (1)); +$$ LANGUAGE SQL; +---- + +norm format=(hide-all,show-scalars) disable=(MergeProjectWithValues,PruneValuesCols) +VALUES (f()); +---- +values + └── tuple + └── udf: f + └── body + ├── scan t + └── project + ├── values + │ └── tuple + │ └── const: 1 + └── projections + └── udf: nested + ├── tail-call + └── body + └── project + ├── values + │ └── tuple + └── projections + └── const: 1 + +# Case with a nonempty input with more than one row. The nested routine can +# still be considered a tail-call because the UDF enforces a LIMIT 1. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + SELECT * FROM t; + SELECT nested() FROM (VALUES (1), (2)); +$$ LANGUAGE SQL; +---- + +norm format=(hide-all,show-scalars) disable=PruneValuesCols +VALUES (f()); +---- +values + └── tuple + └── udf: f + └── body + ├── scan t + └── project + ├── limit + │ ├── values + │ │ ├── tuple + │ │ │ └── const: 1 + │ │ └── tuple + │ │ └── const: 2 + │ └── const: 1 + └── projections + └── udf: nested + ├── tail-call + └── body + └── values + └── tuple + └── const: 1 + +# Case with a nonempty input with more than one row, which disqualifies the +# routine from being a tail-call. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS SETOF INT AS $$ + SELECT * FROM t; + SELECT nested() FROM (VALUES (1), (2)); +$$ LANGUAGE SQL; +---- + +norm format=(hide-all,show-scalars) disable=PruneValuesCols +SELECT f(); +---- +project-set + ├── values + │ └── tuple + └── zip + └── udf: f + └── body + ├── scan t + └── project + ├── values + │ ├── tuple + │ │ └── const: 1 + │ └── tuple + │ └── const: 2 + └── projections + └── udf: nested + └── body + └── values + └── tuple + └── const: 1 + +# A generator function cannot be considered a tail-call. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS SETOF INT AS $$ + SELECT * FROM t; + SELECT generator(); +$$ LANGUAGE SQL; +---- + +norm format=(hide-all,show-scalars) disable=PruneValuesCols +SELECT f(); +---- +project-set + ├── values + │ └── tuple + └── zip + └── udf: f + └── body + ├── scan t + └── project-set + ├── values + │ └── tuple + └── zip + └── udf: generator + └── body + └── values + ├── tuple + │ └── const: 1 + ├── tuple + │ └── const: 2 + └── tuple + └── const: 3 + +# ============================================================================== +# Test explicit tail-calls with a PL/pgSQL routine as the parent. These tests +# also demonstrate that PL/pgSQL sub-routines are tail-calls, as do the +# optbuilder tests. +# ============================================================================== + +# Basic tail call. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE NOTICE 'foo'; + RETURN nested(); + END +$$ LANGUAGE PLpgSQL; +---- + +norm format=(hide-all,show-scalars) +VALUES (f()); +---- +values + └── tuple + └── udf: f + └── body + └── values + └── tuple + └── udf: _stmt_raise_1 + ├── tail-call + └── body + ├── values + │ └── tuple + │ └── function: crdb_internal.plpgsql_raise + │ ├── const: 'NOTICE' + │ ├── const: 'foo' + │ ├── const: '' + │ ├── const: '' + │ └── const: '00000' + └── values + └── tuple + └── udf: nested + ├── tail-call + └── body + └── values + └── tuple + └── const: 1 + +# Not a tail-call because the result is not used by the parent function. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE NOTICE 'foo'; + SELECT nested(); + RETURN 0; + END +$$ LANGUAGE PLpgSQL; +---- + +norm format=(hide-all,show-scalars) +VALUES (f()); +---- +values + └── tuple + └── udf: f + └── body + └── values + └── tuple + └── udf: _stmt_raise_1 + ├── tail-call + └── body + ├── values + │ └── tuple + │ └── function: crdb_internal.plpgsql_raise + │ ├── const: 'NOTICE' + │ ├── const: 'foo' + │ ├── const: '' + │ ├── const: '' + │ └── const: '00000' + └── values + └── tuple + └── udf: _stmt_exec_3 + ├── tail-call + └── body + ├── values + │ └── tuple + │ └── udf: nested + │ └── body + │ └── values + │ └── tuple + │ └── const: 1 + └── values + └── tuple + └── const: 0 + +# Tail-call mediated through a variable assignment. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + x INT; + BEGIN + RAISE NOTICE 'foo'; + x := nested(); + RETURN x; + END +$$ LANGUAGE PLpgSQL; +---- + +norm format=(hide-all,show-scalars) +VALUES (f()); +---- +values + └── tuple + └── udf: f + └── body + └── project + ├── barrier + │ └── values + │ └── tuple + │ └── null + └── projections + └── udf: _stmt_raise_1 + ├── tail-call + ├── args + │ └── variable: x + ├── params: x + └── body + ├── values + │ └── tuple + │ └── function: crdb_internal.plpgsql_raise + │ ├── const: 'NOTICE' + │ ├── const: 'foo' + │ ├── const: '' + │ ├── const: '' + │ └── const: '00000' + └── values + └── tuple + └── udf: nested + ├── tail-call + └── body + └── values + └── tuple + └── const: 1 + +# Not a tail-call because of the second RAISE statement. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + x INT; + BEGIN + RAISE NOTICE 'foo'; + x := nested(); + RAISE NOTICE 'bar'; + RETURN x; + END +$$ LANGUAGE PLpgSQL; +---- + +norm format=(hide-all,show-scalars) +VALUES (f()); +---- +values + └── tuple + └── udf: f + └── body + └── project + ├── barrier + │ └── values + │ └── tuple + │ └── null + └── projections + └── udf: _stmt_raise_1 + ├── tail-call + ├── args + │ └── variable: x + ├── params: x + └── body + ├── values + │ └── tuple + │ └── function: crdb_internal.plpgsql_raise + │ ├── const: 'NOTICE' + │ ├── const: 'foo' + │ ├── const: '' + │ ├── const: '' + │ └── const: '00000' + └── project + ├── barrier + │ └── values + │ └── tuple + │ └── udf: nested + │ └── body + │ └── values + │ └── tuple + │ └── const: 1 + └── projections + └── udf: _stmt_raise_3 + ├── tail-call + ├── args + │ └── variable: x + ├── params: x + └── body + ├── values + │ └── tuple + │ └── function: crdb_internal.plpgsql_raise + │ ├── const: 'NOTICE' + │ ├── const: 'bar' + │ ├── const: '' + │ ├── const: '' + │ └── const: '00000' + └── values + └── tuple + └── variable: x + +# Tail-call with an argument. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + x INT; + BEGIN + RAISE NOTICE 'foo'; + SELECT a INTO x FROM t LIMIT 1; + RETURN nested_arg(x); + END +$$ LANGUAGE PLpgSQL; +---- + +norm format=(hide-all,show-scalars) +VALUES (f()); +---- +values + └── tuple + └── udf: f + └── body + └── project + ├── barrier + │ └── values + │ └── tuple + │ └── null + └── projections + └── udf: _stmt_raise_1 + ├── tail-call + ├── args + │ └── variable: x + ├── params: x + └── body + ├── values + │ └── tuple + │ └── function: crdb_internal.plpgsql_raise + │ ├── const: 'NOTICE' + │ ├── const: 'foo' + │ ├── const: '' + │ ├── const: '' + │ └── const: '00000' + └── values + └── tuple + └── udf: _stmt_exec_3 + ├── tail-call + ├── args + │ └── variable: x + ├── params: x + └── body + └── project + ├── barrier + │ └── project + │ ├── left-join (cross) + │ │ ├── values + │ │ │ └── tuple + │ │ ├── limit + │ │ │ ├── scan t + │ │ │ └── const: 1 + │ │ └── filters (true) + │ └── projections + │ └── variable: a + └── projections + └── udf: _stmt_exec_ret_4 + ├── tail-call + ├── args + │ └── variable: x + ├── params: x + └── body + └── values + └── tuple + └── udf: nested_arg + ├── tail-call + ├── args + │ └── variable: x + ├── params: x + └── body + └── values + └── tuple + └── variable: x + +# Tail-calls within an IF statement. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + x INT := 10; + BEGIN + IF random() > 0.5 THEN + RETURN nested(); + ELSE + RETURN nested_arg(x); + END IF; + END +$$ LANGUAGE PLpgSQL; +---- + +norm format=(hide-all,show-scalars) +VALUES (f()); +---- +values + └── tuple + └── udf: f + └── body + └── project + ├── barrier + │ └── values + │ └── tuple + │ └── const: 10 + └── projections + └── case + ├── true + ├── when + │ ├── gt + │ │ ├── function: random + │ │ └── const: 0.5 + │ └── subquery + │ └── values + │ └── tuple + │ └── udf: nested + │ ├── tail-call + │ └── body + │ └── values + │ └── tuple + │ └── const: 1 + └── subquery + └── values + └── tuple + └── udf: nested_arg + ├── tail-call + ├── args + │ └── variable: x + ├── params: x + └── body + └── values + └── tuple + └── variable: x + +# Tail-call reachable from both branches of an IF statement. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + x INT; + BEGIN + IF random() > 0.5 THEN + x := 100; + ELSE + x := 200; + END IF; + RETURN nested_arg(x); + END +$$ LANGUAGE PLpgSQL; +---- + +norm format=(hide-all,show-scalars) +VALUES (f()); +---- +values + └── tuple + └── udf: f + └── body + └── project + ├── barrier + │ └── values + │ └── tuple + │ └── null + └── projections + └── case + ├── true + ├── when + │ ├── gt + │ │ ├── function: random + │ │ └── const: 0.5 + │ └── subquery + │ └── project + │ ├── barrier + │ │ └── values + │ │ └── tuple + │ │ └── const: 100 + │ └── projections + │ └── udf: stmt_if_1 + │ ├── tail-call + │ ├── args + │ │ └── variable: x + │ ├── params: x + │ └── body + │ └── values + │ └── tuple + │ └── udf: nested_arg + │ ├── tail-call + │ ├── args + │ │ └── variable: x + │ ├── params: x + │ └── body + │ └── values + │ └── tuple + │ └── variable: x + └── subquery + └── project + ├── barrier + │ └── values + │ └── tuple + │ └── const: 200 + └── projections + └── udf: stmt_if_1 + ├── tail-call + ├── args + │ └── variable: x + ├── params: x + └── body + └── values + └── tuple + └── udf: nested_arg + ├── tail-call + ├── args + │ └── variable: x + ├── params: x + └── body + └── values + └── tuple + └── variable: x + +# Tail-call within a loop. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + i INT := 0; + BEGIN + WHILE i < 10 LOOP + IF i = 5 THEN + RETURN nested(); + END IF; + i := i + 1; + END LOOP; + END +$$ LANGUAGE PLpgSQL; +---- + +norm format=(hide-all,show-scalars) +VALUES (f()); +---- +values + └── tuple + └── udf: f + └── body + └── project + ├── barrier + │ └── values + │ └── tuple + │ └── const: 0 + └── projections + └── udf: stmt_loop_5 + ├── tail-call + ├── args + │ └── variable: i + ├── params: i + └── body + └── values + └── tuple + └── case + ├── true + ├── when + │ ├── lt + │ │ ├── variable: i + │ │ └── const: 10 + │ └── subquery + │ └── values + │ └── tuple + │ └── case + │ ├── true + │ ├── when + │ │ ├── eq + │ │ │ ├── variable: i + │ │ │ └── const: 5 + │ │ └── subquery + │ │ └── values + │ │ └── tuple + │ │ └── udf: nested + │ │ ├── tail-call + │ │ └── body + │ │ └── values + │ │ └── tuple + │ │ └── const: 1 + │ └── subquery + │ └── values + │ └── tuple + │ └── subquery + │ └── project + │ ├── values + │ │ └── tuple + │ │ └── plus + │ │ ├── variable: i + │ │ └── const: 1 + │ └── projections + │ └── subquery + │ └── values + │ └── tuple + │ └── udf: stmt_loop_5 + │ ├── tail-call + │ ├── args + │ │ └── variable: i + │ └── recursive-call + └── subquery + └── values + └── tuple + └── udf: loop_exit_1 + ├── tail-call + ├── args + │ └── variable: i + ├── params: i + └── body + └── values + └── tuple + └── udf: _end_of_function_2 + ├── tail-call + ├── args + │ └── variable: i + ├── params: i + └── body + ├── values + │ └── tuple + │ └── function: crdb_internal.plpgsql_raise + │ ├── const: 'ERROR' + │ ├── const: 'control reached end of function without RETURN' + │ ├── const: '' + │ ├── const: '' + │ └── const: '2F005' + └── values + └── tuple + └── null + +# Tail-call within nested PL/pgSQL blocks. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + BEGIN + RAISE NOTICE 'foo'; + If random() < 0.5 THEN + RETURN nested(); + END IF; + BEGIN + RAISE NOTICE 'bar'; + RETURN nested_arg(100); + END; + END; + END +$$ LANGUAGE PLpgSQL; +---- + +norm format=(hide-all,show-scalars) +VALUES (f()); +---- +values + └── tuple + └── udf: f + └── body + └── values + └── tuple + └── udf: _stmt_raise_5 + ├── tail-call + └── body + ├── values + │ └── tuple + │ └── function: crdb_internal.plpgsql_raise + │ ├── const: 'NOTICE' + │ ├── const: 'foo' + │ ├── const: '' + │ ├── const: '' + │ └── const: '00000' + └── values + └── tuple + └── case + ├── true + ├── when + │ ├── lt + │ │ ├── function: random + │ │ └── const: 0.5 + │ └── subquery + │ └── values + │ └── tuple + │ └── udf: nested + │ ├── tail-call + │ └── body + │ └── values + │ └── tuple + │ └── const: 1 + └── subquery + └── values + └── tuple + └── udf: stmt_if_7 + ├── tail-call + └── body + └── values + └── tuple + └── udf: _stmt_raise_9 + ├── tail-call + └── body + ├── values + │ └── tuple + │ └── function: crdb_internal.plpgsql_raise + │ ├── const: 'NOTICE' + │ ├── const: 'bar' + │ ├── const: '' + │ ├── const: '' + │ └── const: '00000' + └── values + └── tuple + └── udf: nested_arg + ├── tail-call + ├── args + │ └── const: 100 + ├── params: x + └── body + └── values + └── tuple + └── variable: x + +# Tail-call within an exception handler. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RETURN nested_arg(50); + EXCEPTION WHEN division_by_zero THEN + RAISE NOTICE 'oops'; + RETURN nested(); + END +$$ LANGUAGE PLpgSQL; +---- + +norm format=(hide-all,show-scalars) +VALUES (f()); +---- +values + └── tuple + └── udf: f + └── body + └── values + └── tuple + └── udf: exception_block_5 + ├── tail-call + ├── body + │ └── values + │ └── tuple + │ └── udf: nested_arg + │ ├── tail-call + │ ├── args + │ │ └── const: 50 + │ ├── params: x + │ └── body + │ └── values + │ └── tuple + │ └── variable: x + └── exception-handler + └── SQLSTATE '22012' + └── values + └── tuple + └── udf: _stmt_raise_2 + └── body + ├── values + │ └── tuple + │ └── function: crdb_internal.plpgsql_raise + │ ├── const: 'NOTICE' + │ ├── const: 'oops' + │ ├── const: '' + │ ├── const: '' + │ └── const: '00000' + └── values + └── tuple + └── udf: nested + ├── tail-call + └── body + └── values + └── tuple + └── const: 1 + +# Nested routine cannot be a tail-call because it's a data source. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + x INT; + BEGIN + SELECT * INTO x FROM nested(); + RETURN x; + END +$$ LANGUAGE PLpgSQL; +---- + +norm format=(hide-all,show-scalars) +VALUES (f()); +---- +values + └── tuple + └── udf: f + └── body + └── project + ├── barrier + │ └── values + │ └── tuple + │ └── null + └── projections + └── udf: _stmt_exec_1 + ├── tail-call + ├── args + │ └── variable: x + ├── params: x + └── body + └── project + ├── left-join (cross) + │ ├── values + │ │ └── tuple + │ ├── limit + │ │ ├── project-set + │ │ │ ├── values + │ │ │ │ └── tuple + │ │ │ └── zip + │ │ │ └── udf: nested + │ │ │ └── body + │ │ │ └── values + │ │ │ └── tuple + │ │ │ └── const: 1 + │ │ └── const: 1 + │ └── filters (true) + └── projections + └── variable: nested + +# A generator function cannot be considered a tail-call. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RETURN (SELECT generator()); + END +$$ LANGUAGE PLpgSQL; +---- + +norm format=(hide-all,show-scalars) disable=PruneValuesCols +SELECT f(); +---- +values + └── tuple + └── udf: f + └── body + └── values + └── tuple + └── subquery + └── max1-row + └── project-set + ├── values + │ └── tuple + └── zip + └── udf: generator + └── body + └── values + ├── tuple + │ └── const: 1 + ├── tuple + │ └── const: 2 + └── tuple + └── const: 3 + +# TODO(121105): the set-returning UDF call should be built into a project-set. +# Until then, just make sure it isn't considered a tail-call. +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RETURN generator(); + END +$$ LANGUAGE PLpgSQL; +---- + +norm format=(hide-all,show-scalars) disable=PruneValuesCols +SELECT f(); +---- +values + └── tuple + └── udf: f + └── body + └── values + └── tuple + └── udf: generator + └── body + └── values + ├── tuple + │ └── const: 1 + ├── tuple + │ └── const: 2 + └── tuple + └── const: 3 diff --git a/pkg/sql/opt/norm/testdata/rules/routine b/pkg/sql/opt/norm/testdata/rules/routine index fbd3986f9f44..ba00dd625b81 100644 --- a/pkg/sql/opt/norm/testdata/rules/routine +++ b/pkg/sql/opt/norm/testdata/rules/routine @@ -49,6 +49,7 @@ call │ ├── fd: ()-->(7) │ └── tuple │ └── udf: _stmt_raise_2 + │ ├── tail-call │ ├── args │ │ └── variable: x:1 │ ├── params: x:4 @@ -97,6 +98,7 @@ call ├── fd: ()-->(11) └── tuple └── udf: _stmt_exec_4 + ├── tail-call ├── args │ └── variable: x:1 ├── params: x:8 diff --git a/pkg/sql/opt/optbuilder/testdata/procedure_plpgsql b/pkg/sql/opt/optbuilder/testdata/procedure_plpgsql index af2d758664a9..4cc850497c90 100644 --- a/pkg/sql/opt/optbuilder/testdata/procedure_plpgsql +++ b/pkg/sql/opt/optbuilder/testdata/procedure_plpgsql @@ -85,6 +85,7 @@ call │ │ └── variable: count_rows:14 [as=c:57] │ └── projections │ └── udf: _stmt_exec_ret_2 [as="_stmt_exec_ret_2":58] + │ ├── tail-call │ ├── args │ │ ├── variable: arg_k:5 │ │ ├── variable: new_i:6 @@ -110,6 +111,7 @@ call │ │ │ └── tuple │ │ └── projections │ │ └── udf: _stmt_exec_4 [as="_stmt_exec_4":41] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: arg_k:15 │ │ │ ├── variable: new_i:16 @@ -142,6 +144,7 @@ call │ │ │ └── tuple │ │ └── projections │ │ └── udf: stmt_if_3 [as=stmt_if_3:40] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: arg_k:24 │ │ │ ├── variable: new_i:25 @@ -163,6 +166,7 @@ call │ │ └── tuple │ └── projections │ └── udf: _stmt_exec_5 [as="_stmt_exec_5":55] + │ ├── tail-call │ ├── args │ │ ├── variable: arg_k:15 │ │ ├── variable: new_i:16 @@ -188,6 +192,7 @@ call │ │ └── tuple │ └── projections │ └── udf: stmt_if_3 [as=stmt_if_3:54] + │ ├── tail-call │ ├── args │ │ ├── variable: arg_k:42 │ │ ├── variable: new_i:43 @@ -323,6 +328,7 @@ call │ │ └── const: 1 │ └── projections │ └── udf: _stmt_raise_6 [as="_stmt_raise_6":26] + │ ├── tail-call │ ├── args │ │ ├── variable: x:18 │ │ ├── variable: y:19 @@ -367,6 +373,7 @@ call │ │ └── tuple │ └── projections │ └── udf: nested_block_3 [as=nested_block_3:25] + │ ├── tail-call │ ├── args │ │ ├── variable: x:21 │ │ └── variable: y:22 @@ -392,6 +399,7 @@ call │ │ └── const: 1 │ └── projections │ └── udf: _stmt_raise_4 [as="_stmt_raise_4":16] + │ ├── tail-call │ ├── args │ │ ├── variable: x:10 │ │ └── variable: y:11 @@ -499,6 +507,7 @@ call │ │ └── tuple │ └── projections │ └── udf: exception_block_7 [as=exception_block_7:17] + │ ├── tail-call │ ├── args │ │ └── variable: x:2 │ ├── params: x:12 @@ -509,6 +518,7 @@ call │ │ │ └── tuple │ │ └── projections │ │ └── udf: _stmt_exec_8 [as="_stmt_exec_8":16] + │ │ ├── tail-call │ │ ├── args │ │ │ └── variable: x:12 │ │ ├── params: x:13 @@ -527,6 +537,7 @@ call │ │ │ └── tuple │ │ └── projections │ │ └── udf: nested_block_3 [as=nested_block_3:15] + │ │ ├── tail-call │ │ ├── args │ │ │ └── variable: x:13 │ │ ├── params: x:4 @@ -537,6 +548,7 @@ call │ │ │ └── tuple │ │ └── projections │ │ └── udf: _stmt_raise_4 [as="_stmt_raise_4":8] + │ │ ├── tail-call │ │ ├── args │ │ │ └── variable: x:4 │ │ ├── params: x:5 @@ -590,6 +602,7 @@ call │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_4 [as="_stmt_raise_4":8] + │ ├── tail-call │ ├── args │ │ └── variable: x:4 │ ├── params: x:5 diff --git a/pkg/sql/opt/optbuilder/testdata/udf_plpgsql b/pkg/sql/opt/optbuilder/testdata/udf_plpgsql index 8f82dd8570bc..934149db89a9 100644 --- a/pkg/sql/opt/optbuilder/testdata/udf_plpgsql +++ b/pkg/sql/opt/optbuilder/testdata/udf_plpgsql @@ -725,6 +725,7 @@ project │ │ └── tuple │ └── projections │ └── udf: stmt_if_6 [as=stmt_if_6:21] + │ ├── tail-call │ ├── args │ │ ├── variable: a:13 │ │ ├── variable: b:14 @@ -804,6 +805,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: loop_exit_1 [as=loop_exit_1:22] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: a:8 │ │ │ ├── variable: b:9 @@ -823,6 +825,7 @@ project │ │ └── tuple │ └── projections │ └── udf: stmt_if_4 [as=stmt_if_4:23] + │ ├── tail-call │ ├── args │ │ ├── variable: a:8 │ │ ├── variable: b:9 @@ -854,6 +857,7 @@ project │ │ └── tuple │ └── projections │ └── udf: stmt_if_5 [as=stmt_if_5:20] + │ ├── tail-call │ ├── args │ │ ├── variable: a:11 │ │ ├── variable: b:12 @@ -872,6 +876,7 @@ project │ │ └── const: 1 │ └── projections │ └── udf: stmt_loop_3 [as=stmt_loop_3:18] + │ ├── tail-call │ ├── args │ │ ├── variable: a:14 │ │ ├── variable: b:15 @@ -943,6 +948,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: loop_exit_1 [as=loop_exit_1:16] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: a:8 │ │ │ ├── variable: b:9 @@ -962,6 +968,7 @@ project │ │ └── tuple │ └── projections │ └── udf: stmt_if_4 [as=stmt_if_4:17] + │ ├── tail-call │ ├── args │ │ ├── variable: a:8 │ │ ├── variable: b:9 @@ -980,6 +987,7 @@ project │ │ └── const: 1 │ └── projections │ └── udf: stmt_loop_3 [as=stmt_loop_3:15] + │ ├── tail-call │ ├── args │ │ ├── variable: a:11 │ │ ├── variable: b:12 @@ -1076,6 +1084,7 @@ project │ │ │ │ └── tuple │ │ │ └── projections │ │ │ └── udf: loop_exit_3 [as=loop_exit_3:26] + │ │ │ ├── tail-call │ │ │ ├── args │ │ │ │ ├── variable: a:15 │ │ │ │ ├── variable: b:16 @@ -1089,6 +1098,7 @@ project │ │ │ │ └── tuple │ │ │ └── projections │ │ │ └── udf: stmt_if_1 [as=stmt_if_1:14] + │ │ │ ├── tail-call │ │ │ ├── args │ │ │ │ ├── variable: a:10 │ │ │ │ ├── variable: b:11 @@ -1109,6 +1119,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: stmt_if_5 [as=stmt_if_5:27] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: a:15 │ │ │ ├── variable: b:16 @@ -1134,6 +1145,7 @@ project │ │ │ └── const: 1 │ │ └── projections │ │ └── udf: stmt_loop_4 [as=stmt_loop_4:25] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: a:19 │ │ │ ├── variable: b:20 @@ -1237,6 +1249,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: loop_exit_1 [as=loop_exit_1:29] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: a:10 │ │ │ ├── variable: b:11 @@ -1257,6 +1270,7 @@ project │ │ └── tuple │ └── projections │ └── udf: stmt_if_4 [as=stmt_if_4:30] + │ ├── tail-call │ ├── args │ │ ├── variable: a:10 │ │ ├── variable: b:11 @@ -1288,6 +1302,7 @@ project │ │ │ └── const: 1 │ │ └── projections │ │ └── udf: stmt_loop_3 [as=stmt_loop_3:26] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: a:14 │ │ │ ├── variable: b:15 @@ -1301,6 +1316,7 @@ project │ │ └── tuple │ └── projections │ └── udf: stmt_if_5 [as=stmt_if_5:27] + │ ├── tail-call │ ├── args │ │ ├── variable: a:14 │ │ ├── variable: b:15 @@ -1326,6 +1342,7 @@ project │ │ └── const: 1 │ └── projections │ └── udf: stmt_loop_3 [as=stmt_loop_3:24] + │ ├── tail-call │ ├── args │ │ ├── variable: a:18 │ │ ├── variable: b:19 @@ -1417,6 +1434,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: loop_exit_1 [as=loop_exit_1:47] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: a:12 │ │ │ ├── variable: b:13 @@ -1438,6 +1456,7 @@ project │ │ └── tuple │ └── projections │ └── udf: stmt_if_4 [as=stmt_if_4:48] + │ ├── tail-call │ ├── args │ │ ├── variable: a:12 │ │ ├── variable: b:13 @@ -1456,6 +1475,7 @@ project │ │ └── const: 0 [as=j:22] │ └── projections │ └── udf: stmt_loop_6 [as=stmt_loop_6:46] + │ ├── tail-call │ ├── args │ │ ├── variable: a:17 │ │ ├── variable: b:18 @@ -1482,6 +1502,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: loop_exit_5 [as=loop_exit_5:43] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: a:30 │ │ │ ├── variable: b:31 @@ -1502,6 +1523,7 @@ project │ │ │ └── const: 1 │ │ └── projections │ │ └── udf: stmt_loop_3 [as=stmt_loop_3:29] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: a:23 │ │ │ ├── variable: b:24 @@ -1516,6 +1538,7 @@ project │ │ └── tuple │ └── projections │ └── udf: stmt_if_7 [as=stmt_if_7:44] + │ ├── tail-call │ ├── args │ │ ├── variable: a:30 │ │ ├── variable: b:31 @@ -1542,6 +1565,7 @@ project │ │ └── const: 1 │ └── projections │ └── udf: stmt_loop_6 [as=stmt_loop_6:42] + │ ├── tail-call │ ├── args │ │ ├── variable: a:35 │ │ ├── variable: b:36 @@ -1631,6 +1655,7 @@ project │ │ │ └── const: 1 │ │ └── projections │ │ └── udf: stmt_if_4 [as=stmt_if_4:17] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: n:8 │ │ │ ├── variable: sum:15 @@ -1643,6 +1668,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: stmt_loop_3 [as=stmt_loop_3:14] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: n:11 │ │ │ ├── variable: sum:12 @@ -1655,6 +1681,7 @@ project │ │ └── tuple │ └── projections │ └── udf: loop_exit_1 [as=loop_exit_1:18] + │ ├── tail-call │ ├── args │ │ ├── variable: n:8 │ │ ├── variable: sum:9 @@ -1738,6 +1765,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: loop_exit_1 [as=loop_exit_1:17] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: n:8 │ │ │ ├── variable: sum:9 @@ -1757,6 +1785,7 @@ project │ │ └── tuple │ └── projections │ └── udf: stmt_if_4 [as=stmt_if_4:18] + │ ├── tail-call │ ├── args │ │ ├── variable: n:8 │ │ ├── variable: sum:9 @@ -1781,6 +1810,7 @@ project │ │ └── const: 1 │ └── projections │ └── udf: stmt_loop_3 [as=stmt_loop_3:16] + │ ├── tail-call │ ├── args │ │ ├── variable: n:11 │ │ ├── variable: sum:14 @@ -1859,6 +1889,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: stmt_loop_3 [as=stmt_loop_3:17] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: n:8 │ │ │ ├── variable: sum:9 @@ -1871,6 +1902,7 @@ project │ │ └── tuple │ └── projections │ └── udf: stmt_if_4 [as=stmt_if_4:18] + │ ├── tail-call │ ├── args │ │ ├── variable: n:8 │ │ ├── variable: sum:9 @@ -1895,6 +1927,7 @@ project │ │ └── const: 1 │ └── projections │ └── udf: stmt_loop_3 [as=stmt_loop_3:16] + │ ├── tail-call │ ├── args │ │ ├── variable: n:11 │ │ ├── variable: sum:14 @@ -1952,6 +1985,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_3 [as="_stmt_raise_3":10] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_4:2 @@ -1970,6 +2004,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_5 [as="_stmt_raise_5":9] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_6:3 @@ -1988,6 +2023,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_7 [as="_stmt_raise_7":8] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_8:4 @@ -2006,6 +2042,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_9 [as="_stmt_raise_9":7] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_10:5 @@ -2082,6 +2119,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_3 [as="_stmt_raise_3":10] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_4:2 @@ -2121,6 +2159,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_5 [as="_stmt_raise_5":9] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_6:3 @@ -2143,6 +2182,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_7 [as="_stmt_raise_7":8] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_8:4 @@ -2172,6 +2212,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_9 [as="_stmt_raise_9":7] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_10:5 @@ -2293,6 +2334,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_3 [as="_stmt_raise_3":16] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_4:2 @@ -2311,6 +2353,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_5 [as="_stmt_raise_5":15] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_6:3 @@ -2329,6 +2372,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_7 [as="_stmt_raise_7":14] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_8:4 @@ -2347,6 +2391,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_9 [as="_stmt_raise_9":13] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_10:5 @@ -2365,6 +2410,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_11 [as="_stmt_raise_11":12] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_12:6 @@ -2383,6 +2429,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_13 [as="_stmt_raise_13":11] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_14:7 @@ -2401,6 +2448,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_15 [as="_stmt_raise_15":10] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_16:8 @@ -2472,6 +2520,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_3 [as="_stmt_raise_3":12] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_4:2 @@ -2493,6 +2542,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_5 [as="_stmt_raise_5":11] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_6:3 @@ -2511,6 +2561,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_7 [as="_stmt_raise_7":10] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_8:4 @@ -2529,6 +2580,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_9 [as="_stmt_raise_9":9] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_10:5 @@ -2547,6 +2599,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_11 [as="_stmt_raise_11":8] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_12:6 @@ -2641,6 +2694,7 @@ project │ │ └── const: 100 [as=i:4] │ └── projections │ └── udf: _stmt_raise_3 [as="_stmt_raise_3":27] + │ ├── tail-call │ ├── args │ │ └── variable: i:4 │ ├── params: i:5 @@ -2684,6 +2738,7 @@ project │ │ └── count-rows [as=count_rows:12] │ └── projections │ └── udf: _stmt_raise_5 [as="_stmt_raise_5":26] + │ ├── tail-call │ ├── args │ │ └── variable: i:13 │ ├── params: i:14 @@ -2712,6 +2767,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_7 [as="_stmt_raise_7":25] + │ ├── tail-call │ ├── args │ │ └── variable: i:14 │ ├── params: i:16 @@ -2815,6 +2871,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: loop_exit_1 [as=loop_exit_1:14] + │ │ ├── tail-call │ │ ├── args │ │ │ └── variable: i:7 │ │ ├── params: i:2 @@ -2825,6 +2882,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: _stmt_raise_2 [as="_stmt_raise_2":6] + │ │ ├── tail-call │ │ ├── args │ │ │ └── variable: i:2 │ │ ├── params: i:3 @@ -2860,6 +2918,7 @@ project │ │ └── tuple │ └── projections │ └── udf: stmt_if_6 [as=stmt_if_6:15] + │ ├── tail-call │ ├── args │ │ └── variable: i:7 │ ├── params: i:8 @@ -2870,6 +2929,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_7 [as="_stmt_raise_7":13] + │ ├── tail-call │ ├── args │ │ └── variable: i:8 │ ├── params: i:9 @@ -2904,6 +2964,7 @@ project │ │ └── const: 1 │ └── projections │ └── udf: stmt_loop_5 [as=stmt_loop_5:12] + │ ├── tail-call │ ├── args │ │ └── variable: i:11 │ └── recursive-call @@ -3116,6 +3177,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: loop_exit_1 [as=loop_exit_1:21] + │ │ ├── tail-call │ │ ├── args │ │ │ └── variable: i:7 │ │ ├── params: i:2 @@ -3126,6 +3188,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: _stmt_raise_2 [as="_stmt_raise_2":6] + │ │ ├── tail-call │ │ ├── args │ │ │ └── variable: i:2 │ │ ├── params: i:3 @@ -3161,6 +3224,7 @@ project │ │ └── tuple │ └── projections │ └── udf: stmt_if_6 [as=stmt_if_6:22] + │ ├── tail-call │ ├── args │ │ └── variable: i:7 │ ├── params: i:8 @@ -3183,6 +3247,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: _stmt_raise_10 [as="_stmt_raise_10":18] + │ │ ├── tail-call │ │ ├── args │ │ │ └── variable: i:8 │ │ ├── params: i:15 @@ -3211,6 +3276,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: stmt_if_7 [as=stmt_if_7:17] + │ │ ├── tail-call │ │ ├── args │ │ │ └── variable: i:15 │ │ ├── params: i:9 @@ -3221,6 +3287,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: _stmt_raise_8 [as="_stmt_raise_8":14] + │ │ ├── tail-call │ │ ├── args │ │ │ └── variable: i:9 │ │ ├── params: i:10 @@ -3255,6 +3322,7 @@ project │ │ │ └── const: 1 │ │ └── projections │ │ └── udf: stmt_loop_5 [as=stmt_loop_5:13] + │ │ ├── tail-call │ │ ├── args │ │ │ └── variable: i:12 │ │ └── recursive-call @@ -3265,6 +3333,7 @@ project │ │ └── tuple │ └── projections │ └── udf: stmt_if_7 [as=stmt_if_7:19] + │ ├── tail-call │ ├── args │ │ └── variable: i:8 │ ├── params: i:9 @@ -3275,6 +3344,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_8 [as="_stmt_raise_8":14] + │ ├── tail-call │ ├── args │ │ └── variable: i:9 │ ├── params: i:10 @@ -3309,6 +3379,7 @@ project │ │ └── const: 1 │ └── projections │ └── udf: stmt_loop_5 [as=stmt_loop_5:13] + │ ├── tail-call │ ├── args │ │ └── variable: i:12 │ └── recursive-call @@ -3903,6 +3974,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: _stmt_raise_8 [as="_stmt_raise_8":6] + │ │ ├── tail-call │ │ └── body │ │ ├── project │ │ │ ├── columns: stmt_raise_9:4 @@ -4076,6 +4148,7 @@ project │ │ │ └── variable: i:10 │ │ └── projections │ │ └── udf: assign_exception_block_4 [as=assign_exception_block_4:32] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: i:10 │ │ │ ├── variable: j:11 @@ -4097,6 +4170,7 @@ project │ │ │ └── variable: j:16 │ │ └── projections │ │ └── udf: assign_exception_block_5 [as=assign_exception_block_5:31] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: i:15 │ │ │ ├── variable: j:16 @@ -4118,6 +4192,7 @@ project │ │ │ └── variable: k:22 │ │ └── projections │ │ └── udf: assign_exception_block_6 [as=assign_exception_block_6:30] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: i:20 │ │ │ ├── variable: j:21 @@ -4218,6 +4293,7 @@ project │ │ │ │ └── const: 100 [as=x:11] │ │ │ └── projections │ │ │ └── udf: assign_exception_block_6 [as=assign_exception_block_6:19] + │ │ │ ├── tail-call │ │ │ ├── args │ │ │ │ ├── variable: i:6 │ │ │ │ └── variable: x:11 @@ -4229,6 +4305,7 @@ project │ │ │ │ └── tuple │ │ │ └── projections │ │ │ └── udf: _stmt_raise_7 [as="_stmt_raise_7":18] + │ │ │ ├── tail-call │ │ │ ├── args │ │ │ │ ├── variable: i:12 │ │ │ │ └── variable: x:13 @@ -4251,6 +4328,7 @@ project │ │ │ │ └── tuple │ │ │ └── projections │ │ │ └── udf: stmt_if_4 [as=stmt_if_4:17] + │ │ │ ├── tail-call │ │ │ ├── args │ │ │ │ ├── variable: i:14 │ │ │ │ └── variable: x:15 @@ -4275,6 +4353,7 @@ project │ │ │ └── const: 200 [as=x:20] │ │ └── projections │ │ └── udf: assign_exception_block_9 [as=assign_exception_block_9:28] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: i:6 │ │ │ └── variable: x:20 @@ -4286,6 +4365,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: _stmt_raise_10 [as="_stmt_raise_10":27] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: i:21 │ │ │ └── variable: x:22 @@ -4308,6 +4388,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: stmt_if_4 [as=stmt_if_4:26] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: i:23 │ │ │ └── variable: x:24 @@ -4393,6 +4474,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: stmt_loop_6 [as=stmt_loop_6:43] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: n:10 │ │ │ ├── variable: a:11 @@ -4418,6 +4500,7 @@ project │ │ │ │ └── tuple │ │ │ └── projections │ │ │ └── udf: loop_exit_4 [as=loop_exit_4:40] + │ │ │ ├── tail-call │ │ │ ├── args │ │ │ │ ├── variable: n:19 │ │ │ │ ├── variable: a:20 @@ -4438,6 +4521,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: stmt_if_7 [as=stmt_if_7:41] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: n:19 │ │ │ ├── variable: a:20 @@ -4461,6 +4545,7 @@ project │ │ │ └── variable: a:24 │ │ └── projections │ │ └── udf: assign_exception_block_8 [as=assign_exception_block_8:39] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: n:23 │ │ │ ├── variable: a:24 @@ -4482,6 +4567,7 @@ project │ │ │ └── const: 1 │ │ └── projections │ │ └── udf: assign_exception_block_9 [as=assign_exception_block_9:38] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: n:28 │ │ │ ├── variable: a:29 @@ -4495,6 +4581,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: stmt_loop_6 [as=stmt_loop_6:37] + │ │ ├── tail-call │ │ ├── args │ │ │ ├── variable: n:33 │ │ │ ├── variable: a:34 @@ -4560,6 +4647,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_exec_2 [as="_stmt_exec_2":27] + │ ├── tail-call │ └── body │ ├── delete kv │ │ ├── columns: @@ -4578,6 +4666,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_exec_3 [as="_stmt_exec_3":26] + │ ├── tail-call │ └── body │ ├── insert kv │ │ ├── columns: @@ -4677,6 +4766,7 @@ project │ │ └── variable: y:6 [as=j:14] │ └── projections │ └── udf: _stmt_exec_ret_2 [as="_stmt_exec_ret_2":15] + │ ├── tail-call │ ├── args │ │ ├── variable: i:13 │ │ └── variable: j:14 @@ -4760,6 +4850,7 @@ project │ │ └── variable: x:3 [as=i:26] │ └── projections │ └── udf: _stmt_exec_ret_2 [as="_stmt_exec_ret_2":27] + │ ├── tail-call │ ├── args │ │ └── variable: i:26 │ ├── params: i:8 @@ -4770,6 +4861,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_3 [as="_stmt_raise_3":25] + │ ├── tail-call │ ├── args │ │ └── variable: i:8 │ ├── params: i:9 @@ -4798,6 +4890,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_exec_5 [as="_stmt_exec_5":24] + │ ├── tail-call │ ├── args │ │ └── variable: i:9 │ ├── params: i:11 @@ -4831,6 +4924,7 @@ project │ │ └── variable: x:12 [as=i:22] │ └── projections │ └── udf: _stmt_exec_ret_6 [as="_stmt_exec_ret_6":23] + │ ├── tail-call │ ├── args │ │ └── variable: i:22 │ ├── params: i:17 @@ -4841,6 +4935,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_7 [as="_stmt_raise_7":21] + │ ├── tail-call │ ├── args │ │ └── variable: i:17 │ ├── params: i:18 @@ -4924,6 +5019,7 @@ project │ │ └── variable: curs:5 │ └── projections │ └── udf: _stmt_open_1 [as="_stmt_open_1":7] + │ ├── tail-call │ ├── args │ │ └── variable: curs:6 │ ├── params: curs:2 @@ -5001,6 +5097,7 @@ project │ │ └── variable: curs:12 │ └── projections │ └── udf: _stmt_open_1 [as="_stmt_open_1":14] + │ ├── tail-call │ ├── args │ │ ├── variable: i:11 │ │ └── variable: curs:13 @@ -5091,6 +5188,7 @@ project │ │ └── variable: curs:29 │ └── projections │ └── udf: _stmt_open_1 [as="_stmt_open_1":33] + │ ├── tail-call │ ├── args │ │ ├── variable: curs:32 │ │ ├── variable: curs2:30 @@ -5110,6 +5208,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _gen_cursor_name_6 [as="_gen_cursor_name_6":28] + │ ├── tail-call │ ├── args │ │ ├── variable: curs:4 │ │ ├── variable: curs2:5 @@ -5129,6 +5228,7 @@ project │ │ └── variable: curs2:24 │ └── projections │ └── udf: _stmt_open_2 [as="_stmt_open_2":27] + │ ├── tail-call │ ├── args │ │ ├── variable: curs:23 │ │ ├── variable: curs2:26 @@ -5148,6 +5248,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _gen_cursor_name_5 [as="_gen_cursor_name_5":22] + │ ├── tail-call │ ├── args │ │ ├── variable: curs:8 │ │ ├── variable: curs2:9 @@ -5167,6 +5268,7 @@ project │ │ └── variable: curs3:19 │ └── projections │ └── udf: _stmt_open_3 [as="_stmt_open_3":21] + │ ├── tail-call │ ├── args │ │ ├── variable: curs:17 │ │ ├── variable: curs2:18 @@ -5295,6 +5397,7 @@ project │ │ └── variable: curs:8 [type=refcursor] │ └── projections │ └── udf: _stmt_open_1 [as="_stmt_open_1":10, type=int, outer=(9), volatile, udf] + │ ├── tail-call │ ├── args │ │ └── variable: curs:9 [type=refcursor] │ ├── params: curs:2(refcursor) @@ -5328,6 +5431,7 @@ project │ │ └── tuple [type=tuple] │ └── projections │ └── udf: _stmt_close_2 [as="_stmt_close_2":7, type=int, outer=(2), volatile, udf] + │ ├── tail-call │ ├── args │ │ └── variable: curs:2 [type=refcursor] │ ├── params: curs:4(refcursor) @@ -5413,6 +5517,7 @@ project │ │ │ └── tuple │ │ └── projections │ │ └── udf: _gen_cursor_name_8 [as="_gen_cursor_name_8":17] + │ │ ├── tail-call │ │ ├── args │ │ │ └── variable: curs:10 │ │ ├── params: curs:14 @@ -5430,6 +5535,7 @@ project │ │ │ └── variable: curs:14 │ │ └── projections │ │ └── udf: _stmt_open_6 [as="_stmt_open_6":16] + │ │ ├── tail-call │ │ ├── args │ │ │ └── variable: curs:15 │ │ ├── params: curs:11 @@ -5474,6 +5580,7 @@ project │ │ └── variable: curs:6 │ └── projections │ └── udf: _stmt_open_2 [as="_stmt_open_2":8] + │ ├── tail-call │ ├── args │ │ └── variable: curs:7 │ ├── params: curs:3 @@ -5614,6 +5721,7 @@ project │ │ └── variable: curs:19 [type=refcursor] │ └── projections │ └── udf: _stmt_open_1 [as="_stmt_open_1":22, type=int, outer=(20,21), volatile, udf] + │ ├── tail-call │ ├── args │ │ ├── variable: curs:21 [type=refcursor] │ │ └── variable: x:20 [type=int] @@ -5644,6 +5752,7 @@ project │ │ └── tuple [type=tuple] │ └── projections │ └── udf: _stmt_fetch_2 [as="_stmt_fetch_2":18, type=int, outer=(3,4), volatile, udf] + │ ├── tail-call │ ├── args │ │ ├── variable: curs:3 [type=refcursor] │ │ └── variable: x:4 [type=int] @@ -5692,6 +5801,7 @@ project │ │ └── variable: stmt_fetch_3:12 [type=tuple{int}] │ └── projections │ └── udf: _stmt_exec_ret_4 [as="_stmt_exec_ret_4":17, type=int, outer=(10,13), udf] + │ ├── tail-call │ ├── args │ │ ├── variable: curs:10 [type=refcursor] │ │ └── variable: x:13 [type=int] @@ -5766,6 +5876,7 @@ project │ │ └── variable: curs:12 │ └── projections │ └── udf: _stmt_open_1 [as="_stmt_open_1":14] + │ ├── tail-call │ ├── args │ │ └── variable: curs:13 │ ├── params: curs:2 @@ -5781,6 +5892,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_fetch_2 [as="_stmt_fetch_2":11] + │ ├── tail-call │ ├── args │ │ └── variable: curs:2 │ ├── params: curs:8 @@ -5980,6 +6092,7 @@ project │ │ │ └── const: 1 [type=int] │ │ └── projections │ │ └── udf: stmt_if_6 [as=stmt_if_6:11, type=int] + │ │ ├── tail-call │ │ ├── args │ │ │ └── variable: i:10 [type=int] │ │ ├── params: i:8(int) @@ -5990,6 +6103,7 @@ project │ │ │ └── tuple [type=tuple] │ │ └── projections │ │ └── udf: stmt_loop_5 [as=stmt_loop_5:9, type=int] + │ │ ├── tail-call │ │ ├── args │ │ │ └── variable: i:8 [type=int] │ │ └── recursive-call @@ -6000,6 +6114,7 @@ project │ │ └── tuple [type=tuple] │ └── projections │ └── udf: loop_exit_1 [as=loop_exit_1:12, type=int] + │ ├── tail-call │ ├── args │ │ └── variable: i:7 [type=int] │ ├── params: i:2(int) @@ -6010,6 +6125,7 @@ project │ │ └── tuple [type=tuple] │ └── projections │ └── udf: _end_of_function_2 [as="_end_of_function_2":6, type=int] + │ ├── tail-call │ ├── args │ │ └── variable: i:2 [type=int] │ ├── params: i:3(int) @@ -6130,6 +6246,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_exec_3 [as="_stmt_exec_3":33] + │ ├── tail-call │ ├── args │ │ └── variable: found:2 │ ├── params: found:4 @@ -6162,6 +6279,7 @@ project │ │ └── variable: a:5 [as=found:31] │ └── projections │ └── udf: _stmt_exec_ret_4 [as="_stmt_exec_ret_4":32] + │ ├── tail-call │ ├── args │ │ └── variable: found:31 │ ├── params: found:9 @@ -6172,6 +6290,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_5 [as="_stmt_raise_5":30] + │ ├── tail-call │ ├── args │ │ └── variable: found:9 │ ├── params: found:10 @@ -6193,6 +6312,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_exec_7 [as="_stmt_exec_7":29] + │ ├── tail-call │ ├── args │ │ └── variable: found:10 │ ├── params: found:12 @@ -6233,6 +6353,7 @@ project │ │ └── variable: a:13 [as=found:27] │ └── projections │ └── udf: _stmt_exec_ret_8 [as="_stmt_exec_ret_8":28] + │ ├── tail-call │ ├── args │ │ └── variable: found:27 │ ├── params: found:22 @@ -6243,6 +6364,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_9 [as="_stmt_raise_9":26] + │ ├── tail-call │ ├── args │ │ └── variable: found:22 │ ├── params: found:23 @@ -6354,6 +6476,7 @@ project │ │ └── const: 80 [as=inner_quantity:10] │ └── projections │ └── udf: _stmt_raise_7 [as="_stmt_raise_7":19] + │ ├── tail-call │ ├── args │ │ ├── variable: outer_quantity:4 │ │ └── variable: inner_quantity:10 @@ -6383,6 +6506,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_9 [as="_stmt_raise_9":18] + │ ├── tail-call │ ├── args │ │ ├── variable: outer_quantity:11 │ │ └── variable: inner_quantity:12 @@ -6412,6 +6536,7 @@ project │ │ └── tuple │ └── projections │ └── udf: nested_block_3 [as=nested_block_3:17] + │ ├── tail-call │ ├── args │ │ └── variable: outer_quantity:14 │ ├── params: outer_quantity:5 @@ -6422,6 +6547,7 @@ project │ │ └── tuple │ └── projections │ └── udf: _stmt_raise_4 [as="_stmt_raise_4":9] + │ ├── tail-call │ ├── args │ │ └── variable: outer_quantity:5 │ ├── params: outer_quantity:6 @@ -6508,6 +6634,7 @@ call │ │ └── tuple │ └── projections │ └── udf: _stmt_exec_3 [as="_stmt_exec_3":22] + │ ├── tail-call │ └── body │ ├── insert txn_timestamps │ │ ├── columns: @@ -6536,6 +6663,7 @@ call │ │ └── tuple │ └── projections │ └── udf: _stmt_exec_5 [as="_stmt_exec_5":20] + │ ├── tail-call │ └── body │ ├── insert txn_timestamps │ │ ├── columns: @@ -6635,6 +6763,7 @@ project │ │ └── tuple [type=tuple] │ └── projections │ └── udf: stmt_if_1 [as=stmt_if_1:5, type=tuple{int, unknown, decimal}] + │ ├── tail-call │ └── body │ └── project │ ├── columns: "_end_of_function_2":3(tuple{int, unknown, decimal}) @@ -6642,6 +6771,7 @@ project │ │ └── tuple [type=tuple] │ └── projections │ └── udf: _end_of_function_2 [as="_end_of_function_2":3, type=tuple{int, unknown, decimal}] + │ ├── tail-call │ └── body │ ├── project │ │ ├── columns: stmt_raise_3:1(int) @@ -6661,3 +6791,108 @@ project │ └── projections │ └── null [as=end_of_function_4:2, type=tuple{int, unknown, decimal}] └── const: 1 [type=int] + +# Regression test for #120916 - the nested call should not have the "tail-call" +# property, because it isn't a terminal statement. +exec-ddl +CREATE OR REPLACE FUNCTION f_nested(x INT) RETURNS INT AS $$ + BEGIN + x := x * 2; + RETURN x; + END +$$ LANGUAGE PLpgSQL; +---- + +exec-ddl +CREATE OR REPLACE FUNCTION f() RETURNS RECORD AS $$ + DECLARE + a INT := -2; + BEGIN + a := f_nested(a); + RAISE NOTICE 'here'; + RETURN (a, -a); + END +$$ LANGUAGE PLpgSQL; +---- + +build format=show-scalars +SELECT * FROM f() AS g(x INT, y INT); +---- +project-set + ├── columns: x:12 y:13 + ├── values + │ └── tuple + └── zip + └── udf: f + └── body + └── project + ├── columns: column10:10 column11:11 + ├── limit + │ ├── columns: "_stmt_raise_1":9 + │ ├── project + │ │ ├── columns: "_stmt_raise_1":9 + │ │ ├── barrier + │ │ │ ├── columns: a:5 + │ │ │ └── project + │ │ │ ├── columns: a:5 + │ │ │ ├── barrier + │ │ │ │ ├── columns: a:1!null + │ │ │ │ └── project + │ │ │ │ ├── columns: a:1!null + │ │ │ │ ├── values + │ │ │ │ │ └── tuple + │ │ │ │ └── projections + │ │ │ │ └── const: -2 [as=a:1] + │ │ │ └── projections + │ │ │ └── udf: f_nested [as=a:5] + │ │ │ ├── args + │ │ │ │ └── variable: a:1 + │ │ │ ├── params: x:2 + │ │ │ └── body + │ │ │ └── limit + │ │ │ ├── columns: stmt_return_1:4 + │ │ │ ├── project + │ │ │ │ ├── columns: stmt_return_1:4 + │ │ │ │ ├── project + │ │ │ │ │ ├── columns: x:3 + │ │ │ │ │ ├── values + │ │ │ │ │ │ └── tuple + │ │ │ │ │ └── projections + │ │ │ │ │ └── mult [as=x:3] + │ │ │ │ │ ├── variable: x:2 + │ │ │ │ │ └── const: 2 + │ │ │ │ └── projections + │ │ │ │ └── variable: x:3 [as=stmt_return_1:4] + │ │ │ └── const: 1 + │ │ └── projections + │ │ └── udf: _stmt_raise_1 [as="_stmt_raise_1":9] + │ │ ├── args + │ │ │ └── variable: a:5 + │ │ ├── params: a:6 + │ │ └── body + │ │ ├── project + │ │ │ ├── columns: stmt_raise_2:7 + │ │ │ ├── values + │ │ │ │ └── tuple + │ │ │ └── projections + │ │ │ └── function: crdb_internal.plpgsql_raise [as=stmt_raise_2:7] + │ │ │ ├── const: 'NOTICE' + │ │ │ ├── const: 'here' + │ │ │ ├── const: '' + │ │ │ ├── const: '' + │ │ │ └── const: '00000' + │ │ └── project + │ │ ├── columns: stmt_return_3:8 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── tuple [as=stmt_return_3:8] + │ │ ├── variable: a:6 + │ │ └── unary-minus + │ │ └── variable: a:6 + │ └── const: 1 + └── projections + ├── column-access: 0 [as=column10:10] + │ └── variable: "_stmt_raise_1":9 + └── column-access: 1 [as=column11:11] + └── variable: "_stmt_raise_1":9 From 332d347c3982117a1434e27246494c1ba731cc7a Mon Sep 17 00:00:00 2001 From: Drew Kimball Date: Tue, 26 Mar 2024 04:14:11 -0600 Subject: [PATCH 3/3] sql: check exception handler before applying TCO This commit finishes the tail-call optimization fix begun by the previous commits, by preventing TCO when it would lose the reference to the calling routine's exception handler. PL/pgSQL sub-routines always maintain a reference to their parent's exception handler, so this isn't a problem for them. However, explicit (user-specified) nested routines do not track the calling routine's exception handler. There is no release note because this bug hasn't appeared in any release. Fixes #120916 Release note: None --- .../testdata/logic_test/nested_routines | 28 +++++++++++++++++ pkg/sql/routine.go | 31 ++++++++++++++++--- pkg/sql/sem/eval/deps.go | 6 +++- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/pkg/ccl/logictestccl/testdata/logic_test/nested_routines b/pkg/ccl/logictestccl/testdata/logic_test/nested_routines index 01a8d86bf100..db1b04994a6e 100644 --- a/pkg/ccl/logictestccl/testdata/logic_test/nested_routines +++ b/pkg/ccl/logictestccl/testdata/logic_test/nested_routines @@ -25,3 +25,31 @@ query II SELECT * FROM f() AS g(x INT, y INT); ---- -4 4 + +# Case with an exception handler on the parent routine. This prevents TCO, +# since executing the child routine in the parent's context would lose track +# of the exception handler. +statement ok +DROP FUNCTION f; +DROP FUNCTION f_nested; + +statement ok +CREATE FUNCTION f_nested() RETURNS INT AS $$ + BEGIN + RETURN 1//0; + END +$$ LANGUAGE PLpgSQL; + +statement ok +CREATE FUNCTION f() RETURNS INT AS $$ + BEGIN + RETURN f_nested(); + EXCEPTION WHEN division_by_zero THEN + RETURN -1; + END +$$ LANGUAGE PLpgSQL; + +query I +SELECT f(); +---- +-1 diff --git a/pkg/sql/routine.go b/pkg/sql/routine.go index cc1be441562c..de687b48d686 100644 --- a/pkg/sql/routine.go +++ b/pkg/sql/routine.go @@ -114,9 +114,10 @@ func (p *planner) EvalRoutineExpr( return expr.CachedResult, nil } - if expr.TailCall && !expr.Generator && p.EvalContext().RoutineSender != nil { + if tailCallOptimizationEnabled && expr.TailCall && !expr.Generator { // This is a nested routine in tail-call position. - if tailCallOptimizationEnabled { + sender := p.EvalContext().RoutineSender + if sender != nil && sender.CanOptimizeTailCall(expr) { // Tail-call optimizations are enabled. Send the information needed to // evaluate this routine to the parent routine, then return. It is safe to // return NULL here because the parent is guaranteed not to perform any @@ -484,8 +485,30 @@ var tailCallOptimizationEnabled = util.ConstantWithMetamorphicTestBool( true, ) -func (g *routineGenerator) SendDeferredRoutine(routine *tree.RoutineExpr, args tree.Datums) { - g.deferredRoutine.expr = routine +func (g *routineGenerator) CanOptimizeTailCall(nestedRoutine *tree.RoutineExpr) bool { + // Tail-call optimization is allowed only if the current routine will not + // perform any work after its body statements finish executing. + // + // Note: cursors are opened after the first body statement, and there is + // always more than one body statement if a cursor is opened. This is enforced + // during exec-building. For this reason, we only have to check for an + // exception handler. + if g.expr.BlockState != nil { + // If the current routine has an exception handler (which is the case when + // BlockState is non-nil), the nested routine must either be part of the + // same PL/pgSQL block, or a child block. Otherwise, enabling TCO could + // cause execution to skip the exception handler. + childBlock := nestedRoutine.BlockState + if childBlock == nil { + return false + } + return childBlock == g.expr.BlockState || childBlock.Parent == g.expr.BlockState + } + return true +} + +func (g *routineGenerator) SendDeferredRoutine(nestedRoutine *tree.RoutineExpr, args tree.Datums) { + g.deferredRoutine.expr = nestedRoutine g.deferredRoutine.args = args } diff --git a/pkg/sql/sem/eval/deps.go b/pkg/sql/sem/eval/deps.go index b5c20e9ddae4..9425bd484e5d 100644 --- a/pkg/sql/sem/eval/deps.go +++ b/pkg/sql/sem/eval/deps.go @@ -555,9 +555,13 @@ type ClientNoticeSender interface { // for its own evaluation to a parent routine. This is used to defer execution // for tail-call optimization. It can only be used during local execution. type DeferredRoutineSender interface { + // CanOptimizeTailCall determines whether a nested routine in tail-call + // position can be executed in its parent's context. + CanOptimizeTailCall(nestedRoutine *tree.RoutineExpr) bool + // SendDeferredRoutine sends a local nested routine and its arguments to its // parent routine. - SendDeferredRoutine(expr *tree.RoutineExpr, args tree.Datums) + SendDeferredRoutine(nestedRoutine *tree.RoutineExpr, args tree.Datums) } // PrivilegedAccessor gives access to certain queries that would otherwise