From d2d965681d0076bbca5694aa10cff0ebdebfa3fa Mon Sep 17 00:00:00 2001 From: Drew Kimball Date: Wed, 27 Sep 2023 00:31:14 -0600 Subject: [PATCH] plpgsql: add support for unnamed cursors This patch adds support for opening an "unnamed" cursor in a PLpgSQL routine. A PLpgSQL cursor is unnamed when the value for the cursor variable is `NULL`. When an unnamed cursor is opened, a name will be generated for it like `` that is guaranteed not to conflict with an existing cursor or portal name. The PLpgSQL variable that represents the cursor's name is updated to reflect the generated name. Informs #109709 Release note (sql change): Added support for unnamed PLpgSQL cursors, which generate a unique name when no cursor name was specified. --- .../testdata/logic_test/plpgsql_cursor | 274 ++++++++++++++---- pkg/sql/opt/optbuilder/plpgsql.go | 55 +++- pkg/sql/opt/optbuilder/testdata/udf_plpgsql | 273 ++++++++++++----- pkg/sql/routine.go | 13 +- 4 files changed, 478 insertions(+), 137 deletions(-) diff --git a/pkg/sql/logictest/testdata/logic_test/plpgsql_cursor b/pkg/sql/logictest/testdata/logic_test/plpgsql_cursor index 7968d918a0ad..42f88f00c31e 100644 --- a/pkg/sql/logictest/testdata/logic_test/plpgsql_cursor +++ b/pkg/sql/logictest/testdata/logic_test/plpgsql_cursor @@ -138,25 +138,6 @@ FETCH FORWARD 3 FROM foo; ---- 1 -# Cursor with empty-string name. -statement ok -ABORT; -CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ - DECLARE - curs STRING := ''; - BEGIN - OPEN curs FOR SELECT 1; - RETURN 0; - END -$$ LANGUAGE PLpgSQL; -BEGIN; -SELECT f(); - -query I -FETCH FORWARD 3 FROM ""; ----- -1 - # Multiple cursors. statement ok ABORT; @@ -232,14 +213,28 @@ SELECT * FROM xy; 1 2 3 4 +# The empty string conflicts with the unnamed portal, which always exists. statement ok ABORT; +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + curs STRING := ''; + BEGIN + OPEN curs FOR SELECT 1; + RETURN 0; + END +$$ LANGUAGE PLpgSQL; +BEGIN; + +statement error pgcode 42P03 pq: cursor \"\" already in use +SELECT f(); # It is possible to use the OPEN statement in an implicit transaction, but the # cursor is closed at the end of the transaction when the statement execution # finishes. So, until FETCH is implemented, we can't actually read from the # cursor. statement ok +ABORT; CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ DECLARE curs STRING := 'foo'; @@ -307,23 +302,6 @@ CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ END $$ LANGUAGE PLpgSQL; -statement ok -CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ - DECLARE - curs STRING; - BEGIN - OPEN curs FOR SELECT 1; - RETURN 0; - END -$$ LANGUAGE PLpgSQL; -BEGIN; - -statement error pgcode 0A000 pq: unimplemented: opening an unnamed cursor is not yet supported -SELECT f(); - -statement ok -ABORT; - statement error pgcode 0A000 pq: unimplemented: opening a cursor in a routine with an exception block is not yet supported CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ DECLARE @@ -636,23 +614,6 @@ SELECT count(*) FROM pg_cursors; ---- 0 -# It is currently necessary to assign the cursor a name, since one is not -# automatically generated. -statement ok -ABORT; -CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ - DECLARE - curs CURSOR FOR SELECT 1; - BEGIN - OPEN curs; - RETURN 0; - END -$$ LANGUAGE PLpgSQL; -BEGIN; - -statement error pgcode 0A000 pq: unimplemented: opening an unnamed cursor is not yet supported -SELECT f(); - statement ok ABORT; @@ -691,3 +652,210 @@ CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ RETURN 0; END $$ LANGUAGE PLpgSQL; + +statement ok +DELETE FROM xy WHERE x <> 1 AND x <> 3; + +# Testing unnamed cursors. +statement ok +DROP FUNCTION f(); +CREATE OR REPLACE FUNCTION f() RETURNS STRING AS $$ + DECLARE + curs STRING; + BEGIN + OPEN curs FOR SELECT 1; + RETURN curs; + END +$$ LANGUAGE PLpgSQL; + +statement ok +BEGIN; + +query T rowsort +SELECT name FROM pg_cursors; +---- + +query T +SELECT f(); +---- + + +query I +FETCH FORWARD 3 FROM ""; +---- +1 + +query T rowsort +SELECT name FROM pg_cursors; +---- + + +query T +SELECT f(); +---- + + +query I +FETCH FORWARD 3 FROM ""; +---- +1 + +query T +SELECT f(); +---- + + +query I +FETCH FORWARD 3 FROM ""; +---- +1 + +query T rowsort +SELECT name FROM pg_cursors; +---- + + + + +# The generated name does not "fill in gaps". +statement ok +CLOSE ""; +CLOSE ""; + +query T +SELECT f(); +---- + + +query T +SELECT f(); +---- + + +query T rowsort +SELECT name FROM pg_cursors; +---- + + + + +statement ok +ABORT; +BEGIN; + +query T +SELECT f(); +---- + + +# The counter for the generated name keeps incrementing as long as the session +# is open. +query T rowsort +SELECT name FROM pg_cursors; +---- + + +# The generated name will not conflict with manually created cursors. +statement ok +DECLARE "" CURSOR FOR SELECT 1; +DECLARE "" CURSOR FOR SELECT 1; + +query T rowsort +SELECT name FROM pg_cursors; +---- + + + + +query T +SELECT f(); +---- + + +query T rowsort +SELECT name FROM pg_cursors; +---- + + + + + +# Do not generate a new name if one was supplied. +statement ok +ABORT; +CREATE OR REPLACE FUNCTION f() RETURNS STRING AS $$ + DECLARE + curs STRING := 'foo'; + BEGIN + OPEN curs FOR SELECT 1; + RETURN curs; + END +$$ LANGUAGE PLpgSQL; +BEGIN; + +query T rowsort +SELECT name FROM pg_cursors; +---- + +query T +SELECT f(); +---- +foo + +query T rowsort +SELECT name FROM pg_cursors; +---- +foo + +# The unnamed portal counter shouldn't have incremented for the named cursor, +# since no name was generated. +statement ok +CREATE OR REPLACE FUNCTION f_unnamed() RETURNS STRING AS $$ + DECLARE + curs STRING; + BEGIN + OPEN curs FOR SELECT 2; + RETURN curs; + END +$$ LANGUAGE PLpgSQL; + +query T +SELECT f_unnamed(); +---- + + +query T rowsort +SELECT name FROM pg_cursors; +---- + +foo + +query I +FETCH FORWARD 3 FROM ""; +---- +2 + +# A bound, unnamed cursor. +statement ok +ABORT; +DROP FUNCTION f(); +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + curs CURSOR FOR SELECT 100; + BEGIN + OPEN curs; + RETURN 0; + END +$$ LANGUAGE PLpgSQL; +BEGIN; + +statement ok +SELECT f(); + +query I +FETCH FORWARD 3 FROM ""; +---- +100 + +statement ok +ABORT; diff --git a/pkg/sql/opt/optbuilder/plpgsql.go b/pkg/sql/opt/optbuilder/plpgsql.go index 577a0b23dd32..73c43b0a1a7e 100644 --- a/pkg/sql/opt/optbuilder/plpgsql.go +++ b/pkg/sql/opt/optbuilder/plpgsql.go @@ -539,6 +539,12 @@ func (b *plpgsqlBuilder) buildPLpgSQLStatements(stmts []ast.Statement, s *scope) } panic(err) } + // TODO(drewk): this should check REFCURSOR. + if !source.(*scopeColumn).typ.Equivalent(types.String) { + panic(pgerror.Newf(pgcode.DatatypeMismatch, + "variable \"%s\" must be of type cursor or refcursor", t.CurVar, + )) + } // Initialize the routine with the information needed to pipe the first // body statement into a cursor. query := b.resolveOpenQuery(t) @@ -558,7 +564,16 @@ func (b *plpgsqlBuilder) buildPLpgSQLStatements(stmts []ast.Statement, s *scope) } b.appendBodyStmt(&openCon, openScope) b.appendPlpgSQLStmts(&openCon, stmts[i+1:]) - return b.callContinuation(&openCon, s) + + // Build a statement to generate a unique name for the cursor if one + // was not supplied. Add this to its own volatile routine to ensure that + // the name generation isn't reordered with other operations. Use the + // resulting projected column as input to the OPEN continuation. + nameCon := b.makeContinuation("_gen_cursor_name") + nameCon.def.Volatility = volatility.Volatile + nameScope := b.buildCursorNameGen(&nameCon, t.CurVar) + b.appendBodyStmt(&nameCon, b.callContinuation(&openCon, nameScope)) + return b.callContinuation(&nameCon, s) default: panic(unimplemented.New( @@ -609,6 +624,44 @@ func (b *plpgsqlBuilder) resolveOpenQuery(open *ast.Open) tree.Statement { return stmt } +// buildCursorNameGen builds a statement that generates a unique name for the +// cursor if the variable containing the name is unset. The unique name +// generation is implemented by the crdb_internal.plpgsql_gen_cursor_name +// builtin function. +func (b *plpgsqlBuilder) buildCursorNameGen(nameCon *continuation, nameVar ast.Variable) *scope { + _, source, _, _ := nameCon.s.FindSourceProvidingColumn(b.ob.ctx, nameVar) + const nameFnName = "crdb_internal.plpgsql_gen_cursor_name" + props, overloads := builtinsregistry.GetBuiltinProperties(nameFnName) + if len(overloads) != 1 { + panic(errors.AssertionFailedf("expected one overload for %s", nameFnName)) + } + nameCall := b.ob.factory.ConstructFunction( + memo.ScalarListExpr{b.ob.factory.ConstructVariable(source.(*scopeColumn).id)}, + &memo.FunctionPrivate{ + Name: nameFnName, + Typ: types.String, + Properties: props, + Overload: &overloads[0], + }, + ) + // Build an expression that calls the builtin function if the name is unset. + scalar := b.ob.factory.ConstructCase(memo.TrueSingleton, + memo.ScalarListExpr{ + b.ob.factory.ConstructWhen( + b.ob.factory.ConstructIs( + b.ob.factory.ConstructVariable(source.(*scopeColumn).id), memo.NullSingleton, + ), + nameCall, + ), + }, + b.ob.factory.ConstructVariable(source.(*scopeColumn).id), + ) + nameScope := nameCon.s.push() + b.ob.synthesizeColumn(nameScope, scopeColName(nameVar), types.String, nil /* expr */, scalar) + b.ob.constructProjectForScope(nameCon.s, nameScope) + return nameScope +} + // addPLpgSQLAssign adds a PL/pgSQL assignment to the current scope as a // new column with the variable name that projects the assigned expression. // If there is a column with the same name in the previous scope, it will be diff --git a/pkg/sql/opt/optbuilder/testdata/udf_plpgsql b/pkg/sql/opt/optbuilder/testdata/udf_plpgsql index 4c21e18981c3..fc492ab99f5b 100644 --- a/pkg/sql/opt/optbuilder/testdata/udf_plpgsql +++ b/pkg/sql/opt/optbuilder/testdata/udf_plpgsql @@ -4620,16 +4620,16 @@ build format=show-scalars SELECT f(); ---- project - ├── columns: f:6 + ├── columns: f:9 ├── values │ └── tuple └── projections - └── udf: f [as=f:6] + └── udf: f [as=f:9] └── body └── limit - ├── columns: "_stmt_open_1":5 + ├── columns: "_gen_cursor_name_3":8 ├── project - │ ├── columns: "_stmt_open_1":5 + │ ├── columns: "_gen_cursor_name_3":8 │ ├── project │ │ ├── columns: curs:1!null │ │ ├── values @@ -4637,24 +4637,46 @@ project │ │ └── projections │ │ └── const: 'foo' [as=curs:1] │ └── projections - │ └── udf: _stmt_open_1 [as="_stmt_open_1":5] + │ └── udf: _gen_cursor_name_3 [as="_gen_cursor_name_3":8] │ ├── args │ │ └── variable: curs:1 - │ ├── params: curs:2 + │ ├── params: curs:5 │ └── body - │ ├── open-cursor - │ │ └── project - │ │ ├── columns: "?column?":3!null - │ │ ├── values - │ │ │ └── tuple - │ │ └── projections - │ │ └── const: 1 [as="?column?":3] │ └── project - │ ├── columns: stmt_return_2:4!null - │ ├── values - │ │ └── tuple + │ ├── columns: "_stmt_open_1":7 + │ ├── project + │ │ ├── columns: curs:6 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── case [as=curs:6] + │ │ ├── true + │ │ ├── when + │ │ │ ├── is + │ │ │ │ ├── variable: curs:5 + │ │ │ │ └── null + │ │ │ └── function: crdb_internal.plpgsql_gen_cursor_name + │ │ │ └── variable: curs:5 + │ │ └── variable: curs:5 │ └── projections - │ └── const: 0 [as=stmt_return_2:4] + │ └── udf: _stmt_open_1 [as="_stmt_open_1":7] + │ ├── args + │ │ └── variable: curs:6 + │ ├── params: curs:2 + │ └── body + │ ├── open-cursor + │ │ └── project + │ │ ├── columns: "?column?":3!null + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── const: 1 [as="?column?":3] + │ └── project + │ ├── columns: stmt_return_2:4!null + │ ├── values + │ │ └── tuple + │ └── projections + │ └── const: 0 [as=stmt_return_2:4] └── const: 1 exec-ddl @@ -4673,16 +4695,16 @@ build format=show-scalars SELECT f(); ---- project - ├── columns: f:12 + ├── columns: f:16 ├── values │ └── tuple └── projections - └── udf: f [as=f:12] + └── udf: f [as=f:16] └── body └── limit - ├── columns: "_stmt_open_1":11 + ├── columns: "_gen_cursor_name_3":15 ├── project - │ ├── columns: "_stmt_open_1":11 + │ ├── columns: "_gen_cursor_name_3":15 │ ├── project │ │ ├── columns: curs:2!null i:1!null │ │ ├── project @@ -4694,29 +4716,52 @@ project │ │ └── projections │ │ └── const: 'foo' [as=curs:2] │ └── projections - │ └── udf: _stmt_open_1 [as="_stmt_open_1":11] + │ └── udf: _gen_cursor_name_3 [as="_gen_cursor_name_3":15] │ ├── args │ │ ├── variable: i:1 │ │ └── variable: curs:2 - │ ├── params: i:3 curs:4 + │ ├── params: i:11 curs:12 │ └── body - │ ├── open-cursor - │ │ └── project - │ │ ├── columns: x:5!null y:6 - │ │ └── select - │ │ ├── columns: x:5!null y:6 rowid:7!null crdb_internal_mvcc_timestamp:8 tableoid:9 - │ │ ├── scan xy - │ │ │ └── columns: x:5 y:6 rowid:7!null crdb_internal_mvcc_timestamp:8 tableoid:9 - │ │ └── filters - │ │ └── eq - │ │ ├── variable: x:5 - │ │ └── variable: i:3 │ └── project - │ ├── columns: stmt_return_2:10!null - │ ├── values - │ │ └── tuple + │ ├── columns: "_stmt_open_1":14 + │ ├── project + │ │ ├── columns: curs:13 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── case [as=curs:13] + │ │ ├── true + │ │ ├── when + │ │ │ ├── is + │ │ │ │ ├── variable: curs:12 + │ │ │ │ └── null + │ │ │ └── function: crdb_internal.plpgsql_gen_cursor_name + │ │ │ └── variable: curs:12 + │ │ └── variable: curs:12 │ └── projections - │ └── const: 0 [as=stmt_return_2:10] + │ └── udf: _stmt_open_1 [as="_stmt_open_1":14] + │ ├── args + │ │ ├── variable: i:11 + │ │ └── variable: curs:13 + │ ├── params: i:3 curs:4 + │ └── body + │ ├── open-cursor + │ │ └── project + │ │ ├── columns: x:5!null y:6 + │ │ └── select + │ │ ├── columns: x:5!null y:6 rowid:7!null crdb_internal_mvcc_timestamp:8 tableoid:9 + │ │ ├── scan xy + │ │ │ └── columns: x:5 y:6 rowid:7!null crdb_internal_mvcc_timestamp:8 tableoid:9 + │ │ └── filters + │ │ └── eq + │ │ ├── variable: x:5 + │ │ └── variable: i:3 + │ └── project + │ ├── columns: stmt_return_2:10!null + │ ├── values + │ │ └── tuple + │ └── projections + │ └── const: 0 [as=stmt_return_2:10] └── const: 1 exec-ddl @@ -4738,16 +4783,16 @@ build format=show-scalars SELECT f(); ---- project - ├── columns: f:20 + ├── columns: f:35 ├── values │ └── tuple └── projections - └── udf: f [as=f:20] + └── udf: f [as=f:35] └── body └── limit - ├── columns: "_stmt_open_1":19 + ├── columns: "_gen_cursor_name_7":34 ├── project - │ ├── columns: "_stmt_open_1":19 + │ ├── columns: "_gen_cursor_name_7":34 │ ├── project │ │ ├── columns: curs3:3!null curs:1!null curs2:2!null │ │ ├── project @@ -4763,62 +4808,134 @@ project │ │ └── projections │ │ └── const: 'baz' [as=curs3:3] │ └── projections - │ └── udf: _stmt_open_1 [as="_stmt_open_1":19] + │ └── udf: _gen_cursor_name_7 [as="_gen_cursor_name_7":34] │ ├── args │ │ ├── variable: curs:1 │ │ ├── variable: curs2:2 │ │ └── variable: curs3:3 - │ ├── params: curs:4 curs2:5 curs3:6 + │ ├── params: curs:29 curs2:30 curs3:31 │ └── body - │ ├── open-cursor - │ │ └── project - │ │ ├── columns: "?column?":7!null - │ │ ├── values - │ │ │ └── tuple - │ │ └── projections - │ │ └── const: 1 [as="?column?":7] │ └── project - │ ├── columns: "_stmt_open_2":18 - │ ├── values - │ │ └── tuple + │ ├── columns: "_stmt_open_1":33 + │ ├── project + │ │ ├── columns: curs:32 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── case [as=curs:32] + │ │ ├── true + │ │ ├── when + │ │ │ ├── is + │ │ │ │ ├── variable: curs:29 + │ │ │ │ └── null + │ │ │ └── function: crdb_internal.plpgsql_gen_cursor_name + │ │ │ └── variable: curs:29 + │ │ └── variable: curs:29 │ └── projections - │ └── udf: _stmt_open_2 [as="_stmt_open_2":18] + │ └── udf: _stmt_open_1 [as="_stmt_open_1":33] │ ├── args - │ │ ├── variable: curs:4 - │ │ ├── variable: curs2:5 - │ │ └── variable: curs3:6 - │ ├── params: curs:8 curs2:9 curs3:10 + │ │ ├── variable: curs:32 + │ │ ├── variable: curs2:30 + │ │ └── variable: curs3:31 + │ ├── params: curs:4 curs2:5 curs3:6 │ └── body │ ├── open-cursor │ │ └── project - │ │ ├── columns: "?column?":11!null + │ │ ├── columns: "?column?":7!null │ │ ├── values │ │ │ └── tuple │ │ └── projections - │ │ └── const: 2 [as="?column?":11] + │ │ └── const: 1 [as="?column?":7] │ └── project - │ ├── columns: "_stmt_open_3":17 + │ ├── columns: "_gen_cursor_name_6":28 │ ├── values │ │ └── tuple │ └── projections - │ └── udf: _stmt_open_3 [as="_stmt_open_3":17] + │ └── udf: _gen_cursor_name_6 [as="_gen_cursor_name_6":28] │ ├── args - │ │ ├── variable: curs:8 - │ │ ├── variable: curs2:9 - │ │ └── variable: curs3:10 - │ ├── params: curs:12 curs2:13 curs3:14 + │ │ ├── variable: curs:4 + │ │ ├── variable: curs2:5 + │ │ └── variable: curs3:6 + │ ├── params: curs:23 curs2:24 curs3:25 │ └── body - │ ├── open-cursor - │ │ └── project - │ │ ├── columns: "?column?":15!null - │ │ ├── values - │ │ │ └── tuple - │ │ └── projections - │ │ └── const: 3 [as="?column?":15] │ └── project - │ ├── columns: stmt_return_4:16!null - │ ├── values - │ │ └── tuple + │ ├── columns: "_stmt_open_2":27 + │ ├── project + │ │ ├── columns: curs2:26 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── case [as=curs2:26] + │ │ ├── true + │ │ ├── when + │ │ │ ├── is + │ │ │ │ ├── variable: curs2:24 + │ │ │ │ └── null + │ │ │ └── function: crdb_internal.plpgsql_gen_cursor_name + │ │ │ └── variable: curs2:24 + │ │ └── variable: curs2:24 │ └── projections - │ └── const: 0 [as=stmt_return_4:16] + │ └── udf: _stmt_open_2 [as="_stmt_open_2":27] + │ ├── args + │ │ ├── variable: curs:23 + │ │ ├── variable: curs2:26 + │ │ └── variable: curs3:25 + │ ├── params: curs:8 curs2:9 curs3:10 + │ └── body + │ ├── open-cursor + │ │ └── project + │ │ ├── columns: "?column?":11!null + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── const: 2 [as="?column?":11] + │ └── project + │ ├── columns: "_gen_cursor_name_5":22 + │ ├── values + │ │ └── tuple + │ └── projections + │ └── udf: _gen_cursor_name_5 [as="_gen_cursor_name_5":22] + │ ├── args + │ │ ├── variable: curs:8 + │ │ ├── variable: curs2:9 + │ │ └── variable: curs3:10 + │ ├── params: curs:17 curs2:18 curs3:19 + │ └── body + │ └── project + │ ├── columns: "_stmt_open_3":21 + │ ├── project + │ │ ├── columns: curs3:20 + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── case [as=curs3:20] + │ │ ├── true + │ │ ├── when + │ │ │ ├── is + │ │ │ │ ├── variable: curs3:19 + │ │ │ │ └── null + │ │ │ └── function: crdb_internal.plpgsql_gen_cursor_name + │ │ │ └── variable: curs3:19 + │ │ └── variable: curs3:19 + │ └── projections + │ └── udf: _stmt_open_3 [as="_stmt_open_3":21] + │ ├── args + │ │ ├── variable: curs:17 + │ │ ├── variable: curs2:18 + │ │ └── variable: curs3:20 + │ ├── params: curs:12 curs2:13 curs3:14 + │ └── body + │ ├── open-cursor + │ │ └── project + │ │ ├── columns: "?column?":15!null + │ │ ├── values + │ │ │ └── tuple + │ │ └── projections + │ │ └── const: 3 [as="?column?":15] + │ └── project + │ ├── columns: stmt_return_4:16!null + │ ├── values + │ │ └── tuple + │ └── projections + │ └── const: 0 [as=stmt_return_4:16] └── const: 1 diff --git a/pkg/sql/routine.go b/pkg/sql/routine.go index 91e5a73145a2..2a67b9f8fb0d 100644 --- a/pkg/sql/routine.go +++ b/pkg/sql/routine.go @@ -24,7 +24,6 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" "github.com/cockroachdb/cockroach/pkg/sql/types" "github.com/cockroachdb/cockroach/pkg/util" - "github.com/cockroachdb/cockroach/pkg/util/errorutil/unimplemented" "github.com/cockroachdb/cockroach/pkg/util/timeutil" "github.com/cockroachdb/cockroach/pkg/util/tracing" "github.com/cockroachdb/errors" @@ -383,16 +382,20 @@ func (g *routineGenerator) newCursorHelper(plan *planComponents) (*plpgsqlCursor panic(errors.AssertionFailedf("unexpected name argument index: %d", open.NameArgIdx)) } if g.args[open.NameArgIdx] == tree.DNull { - return nil, unimplemented.New("unnamed cursor", - "opening an unnamed cursor is not yet supported", - ) + return nil, errors.AssertionFailedf("expected non-null cursor name") + } + cursorName := tree.Name(tree.MustBeDString(g.args[open.NameArgIdx])) + if cursorName == "" { + // Specifying the empty string as a cursor name conflicts with the + // "unnamed" portal, which always exists. + return nil, pgerror.Newf(pgcode.DuplicateCursor, "cursor \"\" already in use") } // Use context.Background(), since the cursor can outlive the context in which // it was created. planCols := plan.main.planColumns() cursorHelper := &plpgsqlCursorHelper{ ctx: context.Background(), - cursorName: tree.Name(tree.MustBeDString(g.args[open.NameArgIdx])), + cursorName: cursorName, resultCols: make(colinfo.ResultColumns, len(planCols)), } copy(cursorHelper.resultCols, planCols)