CALL transaction_test1();
</programlisting>
</para>
-
- <para>
- Transactions cannot be ended when a cursor created by
- <function>spi_query</function> is open.
- </para>
</listitem>
</varlistentry>
</variablelist>
</para>
<para>
- A transaction cannot be ended inside a loop over a query result, nor
- inside a block with exception handlers.
+ Special considerations apply to cursor loops. Consider this example:
+<programlisting>
+CREATE PROCEDURE transaction_test2()
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ r RECORD;
+BEGIN
+ FOR r IN SELECT * FROM test2 ORDER BY x LOOP
+ INSERT INTO test1 (a) VALUES (r.x);
+ COMMIT;
+ END LOOP;
+END;
+$$;
+
+CALL transaction_test2();
+</programlisting>
+ Normally, cursors are automatically closed at transaction commit.
+ However, a cursor created as part of a loop like this is automatically
+ converted to a holdable cursor by the first <command>COMMIT</command> or
+ <command>ROLLBACK</command>. That means that the cursor is fully
+ evaluated at the first <command>COMMIT</command> or
+ <command>ROLLBACK</command> rather than row by row. The cursor is still
+ removed automatically after the loop, so this is mostly invisible to the
+ user.
+ </para>
+
+ <para>
+ Transaction commands are not allowed in cursor loops driven by commands
+ that are not read-only (for example <command>UPDATE
+ ... RETURNING</command>).
+ </para>
+
+ <para>
+ A transaction cannot be ended inside a block with exception handlers.
</para>
</sect1>
</para>
<para>
- Transactions cannot be ended when a cursor created by
- <literal>plpy.cursor</literal> is open or when an explicit subtransaction
- is active.
+ Transactions cannot be ended when an explicit subtransaction is active.
</para>
</sect1>
if (am_walsender)
WalSndErrorCleanup();
+ PortalErrorCleanup();
+
/*
* We can't release replication slots inside AbortTransaction() as we
* need to be able to start and abort transactions while having a slot
}
}
+/*
+ * "Hold" a portal. Prepare it for access by later transactions.
+ */
+static void
+HoldPortal(Portal portal)
+{
+ /*
+ * Note that PersistHoldablePortal() must release all resources
+ * used by the portal that are local to the creating transaction.
+ */
+ PortalCreateHoldStore(portal);
+ PersistHoldablePortal(portal);
+
+ /* drop cached plan reference, if any */
+ PortalReleaseCachedPlan(portal);
+
+ /*
+ * Any resources belonging to the portal will be released in the
+ * upcoming transaction-wide cleanup; the portal will no longer
+ * have its own resources.
+ */
+ portal->resowner = NULL;
+
+ /*
+ * Having successfully exported the holdable cursor, mark it as
+ * not belonging to this transaction.
+ */
+ portal->createSubid = InvalidSubTransactionId;
+ portal->activeSubid = InvalidSubTransactionId;
+}
/*
* Pre-commit processing for portals.
/*
* There should be no pinned portals anymore. Complain if someone
- * leaked one.
+ * leaked one. Auto-held portals are allowed; we assume that whoever
+ * pinned them is managing them.
*/
- if (portal->portalPinned)
+ if (portal->portalPinned && !portal->autoHeld)
elog(ERROR, "cannot commit while a portal is pinned");
/*
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot PREPARE a transaction that has created a cursor WITH HOLD")));
- /*
- * Note that PersistHoldablePortal() must release all resources
- * used by the portal that are local to the creating transaction.
- */
- PortalCreateHoldStore(portal);
- PersistHoldablePortal(portal);
-
- /* drop cached plan reference, if any */
- PortalReleaseCachedPlan(portal);
-
- /*
- * Any resources belonging to the portal will be released in the
- * upcoming transaction-wide cleanup; the portal will no longer
- * have its own resources.
- */
- portal->resowner = NULL;
-
- /*
- * Having successfully exported the holdable cursor, mark it as
- * not belonging to this transaction.
- */
- portal->createSubid = InvalidSubTransactionId;
- portal->activeSubid = InvalidSubTransactionId;
+ HoldPortal(portal);
/* Report we changed state */
result = true;
if (portal->createSubid == InvalidSubTransactionId)
continue;
+ /*
+ * Do nothing to auto-held cursors. This is similar to the case of a
+ * cursor from a previous transaction, but it could also be that the
+ * cursor was auto-held in this transaction, so it wants to live on.
+ */
+ if (portal->autoHeld)
+ continue;
+
/*
* If it was created in the current transaction, we can't do normal
* shutdown on a READY portal either; it might refer to objects
if (portal->status == PORTAL_ACTIVE)
continue;
- /* Do nothing to cursors held over from a previous transaction */
- if (portal->createSubid == InvalidSubTransactionId)
+ /*
+ * Do nothing to cursors held over from a previous transaction or
+ * auto-held ones.
+ */
+ if (portal->createSubid == InvalidSubTransactionId || portal->autoHeld)
{
Assert(portal->status != PORTAL_ACTIVE);
Assert(portal->resowner == NULL);
}
}
+/*
+ * Portal-related cleanup when we return to the main loop on error.
+ *
+ * This is different from the cleanup at transaction abort. Auto-held portals
+ * are cleaned up on error but not on transaction abort.
+ */
+void
+PortalErrorCleanup(void)
+{
+ HASH_SEQ_STATUS status;
+ PortalHashEnt *hentry;
+
+ hash_seq_init(&status, PortalHashTable);
+
+ while ((hentry = (PortalHashEnt *) hash_seq_search(&status)) != NULL)
+ {
+ Portal portal = hentry->portal;
+
+ if (portal->autoHeld)
+ {
+ portal->portalPinned = false;
+ PortalDrop(portal, false);
+ }
+ }
+}
+
/*
* Pre-subcommit processing for portals.
*
return true;
}
-bool
-ThereArePinnedPortals(void)
+/*
+ * Hold all pinned portals.
+ *
+ * A procedural language implementation that uses pinned portals for its
+ * internally generated cursors can call this in its COMMIT command to convert
+ * those cursors to held cursors, so that they survive the transaction end.
+ * We mark those portals as "auto-held" so that exception exit knows to clean
+ * them up. (In normal, non-exception code paths, the PL needs to clean those
+ * portals itself, since transaction end won't do it anymore, but that should
+ * be normal practice anyway.)
+ */
+void
+HoldPinnedPortals(void)
{
HASH_SEQ_STATUS status;
PortalHashEnt *hentry;
{
Portal portal = hentry->portal;
- if (portal->portalPinned)
- return true;
- }
+ if (portal->portalPinned && !portal->autoHeld)
+ {
+ /*
+ * Doing transaction control, especially abort, inside a cursor
+ * loop that is not read-only, for example using UPDATE
+ * ... RETURNING, has weird semantics issues. Also, this
+ * implementation wouldn't work, because such portals cannot be
+ * held. (The core grammar enforces that only SELECT statements
+ * can drive a cursor, but for example PL/pgSQL does not restrict
+ * it.)
+ */
+ if (portal->strategy != PORTAL_ONE_SELECT)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
+ errmsg("cannot perform transaction commands inside a cursor loop that is not read-only")));
- return false;
+ portal->autoHeld = true;
+ HoldPortal(portal);
+ }
+ }
}
/* Status data */
PortalStatus status; /* see above */
bool portalPinned; /* a pinned portal can't be dropped */
+ bool autoHeld; /* was automatically converted from pinned to
+ * held (see HoldPinnedPortals()) */
/* If not NULL, Executor is active; call ExecutorEnd eventually: */
QueryDesc *queryDesc; /* info needed for executor invocation */
extern bool PreCommit_Portals(bool isPrepare);
extern void AtAbort_Portals(void);
extern void AtCleanup_Portals(void);
+extern void PortalErrorCleanup(void);
extern void AtSubCommit_Portals(SubTransactionId mySubid,
SubTransactionId parentSubid,
ResourceOwner parentXactOwner);
extern void PortalCreateHoldStore(Portal portal);
extern void PortalHashTableDeleteAll(void);
extern bool ThereAreNoReadyPortals(void);
-extern bool ThereArePinnedPortals(void);
+extern void HoldPinnedPortals(void);
#endif /* PORTAL_H */
spi_commit();
}
$$;
-ERROR: cannot commit transaction while a cursor is open at line 6.
-CONTEXT: PL/Perl anonymous code block
SELECT * FROM test1;
a | b
---+---
+ 0 |
+ 1 |
+ 2 |
+ 3 |
+ 4 |
+(5 rows)
+
+-- check that this doesn't leak a holdable portal
+SELECT * FROM pg_cursors;
+ name | statement | is_holdable | is_binary | is_scrollable | creation_time
+------+-----------+-------------+-----------+---------------+---------------
+(0 rows)
+
+-- error in cursor loop with commit
+TRUNCATE test1;
+DO LANGUAGE plperl $$
+my $sth = spi_query("SELECT * FROM test2 ORDER BY x");
+my $row;
+while (defined($row = spi_fetchrow($sth))) {
+ spi_exec_query("INSERT INTO test1 (a) VALUES (12/(" . $row->{x} . "-2))");
+ spi_commit();
+}
+$$;
+ERROR: division by zero at line 5.
+CONTEXT: PL/Perl anonymous code block
+SELECT * FROM test1;
+ a | b
+-----+---
+ -6 |
+ -12 |
+(2 rows)
+
+SELECT * FROM pg_cursors;
+ name | statement | is_holdable | is_binary | is_scrollable | creation_time
+------+-----------+-------------+-----------+---------------+---------------
(0 rows)
-- rollback inside cursor loop
spi_rollback();
}
$$;
-ERROR: cannot abort transaction while a cursor is open at line 6.
-CONTEXT: PL/Perl anonymous code block
SELECT * FROM test1;
a | b
---+---
(0 rows)
+SELECT * FROM pg_cursors;
+ name | statement | is_holdable | is_binary | is_scrollable | creation_time
+------+-----------+-------------+-----------+---------------+---------------
+(0 rows)
+
+-- first commit then rollback inside cursor loop
+TRUNCATE test1;
+DO LANGUAGE plperl $$
+my $sth = spi_query("SELECT * FROM test2 ORDER BY x");
+my $row;
+while (defined($row = spi_fetchrow($sth))) {
+ spi_exec_query("INSERT INTO test1 (a) VALUES (" . $row->{x} . ")");
+ if ($row->{x} % 2 == 0) {
+ spi_commit();
+ } else {
+ spi_rollback();
+ }
+}
+$$;
+SELECT * FROM test1;
+ a | b
+---+---
+ 0 |
+ 2 |
+ 4 |
+(3 rows)
+
+SELECT * FROM pg_cursors;
+ name | statement | is_holdable | is_binary | is_scrollable | creation_time
+------+-----------+-------------+-----------+---------------+---------------
+(0 rows)
+
DROP TABLE test1;
DROP TABLE test2;
PG_TRY();
{
- if (ThereArePinnedPortals())
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("cannot commit transaction while a cursor is open")));
+ HoldPinnedPortals();
SPI_commit();
SPI_start_transaction();
PG_TRY();
{
- if (ThereArePinnedPortals())
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
- errmsg("cannot abort transaction while a cursor is open")));
+ HoldPinnedPortals();
SPI_rollback();
SPI_start_transaction();
SELECT * FROM test1;
+-- check that this doesn't leak a holdable portal
+SELECT * FROM pg_cursors;
+
+
+-- error in cursor loop with commit
+TRUNCATE test1;
+
+DO LANGUAGE plperl $$
+my $sth = spi_query("SELECT * FROM test2 ORDER BY x");
+my $row;
+while (defined($row = spi_fetchrow($sth))) {
+ spi_exec_query("INSERT INTO test1 (a) VALUES (12/(" . $row->{x} . "-2))");
+ spi_commit();
+}
+$$;
+
+SELECT * FROM test1;
+
+SELECT * FROM pg_cursors;
+
-- rollback inside cursor loop
TRUNCATE test1;
SELECT * FROM test1;
+SELECT * FROM pg_cursors;
+
+
+-- first commit then rollback inside cursor loop
+TRUNCATE test1;
+
+DO LANGUAGE plperl $$
+my $sth = spi_query("SELECT * FROM test2 ORDER BY x");
+my $row;
+while (defined($row = spi_fetchrow($sth))) {
+ spi_exec_query("INSERT INTO test1 (a) VALUES (" . $row->{x} . ")");
+ if ($row->{x} % 2 == 0) {
+ spi_commit();
+ } else {
+ spi_rollback();
+ }
+}
+$$;
+
+SELECT * FROM test1;
+
+SELECT * FROM pg_cursors;
+
DROP TABLE test1;
DROP TABLE test2;
END LOOP;
END;
$$;
-ERROR: committing inside a cursor loop is not supported
-CONTEXT: PL/pgSQL function inline_code_block line 7 at COMMIT
SELECT * FROM test1;
a | b
---+---
+ 0 |
+ 1 |
+ 2 |
+ 3 |
+ 4 |
+(5 rows)
+
+-- check that this doesn't leak a holdable portal
+SELECT * FROM pg_cursors;
+ name | statement | is_holdable | is_binary | is_scrollable | creation_time
+------+-----------+-------------+-----------+---------------+---------------
+(0 rows)
+
+-- error in cursor loop with commit
+TRUNCATE test1;
+DO LANGUAGE plpgsql $$
+DECLARE
+ r RECORD;
+BEGIN
+ FOR r IN SELECT * FROM test2 ORDER BY x LOOP
+ INSERT INTO test1 (a) VALUES (12/(r.x-2));
+ COMMIT;
+ END LOOP;
+END;
+$$;
+ERROR: division by zero
+CONTEXT: SQL statement "INSERT INTO test1 (a) VALUES (12/(r.x-2))"
+PL/pgSQL function inline_code_block line 6 at SQL statement
+SELECT * FROM test1;
+ a | b
+-----+---
+ -6 |
+ -12 |
+(2 rows)
+
+SELECT * FROM pg_cursors;
+ name | statement | is_holdable | is_binary | is_scrollable | creation_time
+------+-----------+-------------+-----------+---------------+---------------
(0 rows)
-- rollback inside cursor loop
END LOOP;
END;
$$;
-ERROR: cannot abort transaction inside a cursor loop
+SELECT * FROM test1;
+ a | b
+---+---
+(0 rows)
+
+SELECT * FROM pg_cursors;
+ name | statement | is_holdable | is_binary | is_scrollable | creation_time
+------+-----------+-------------+-----------+---------------+---------------
+(0 rows)
+
+-- first commit then rollback inside cursor loop
+TRUNCATE test1;
+DO LANGUAGE plpgsql $$
+DECLARE
+ r RECORD;
+BEGIN
+ FOR r IN SELECT * FROM test2 ORDER BY x LOOP
+ INSERT INTO test1 (a) VALUES (r.x);
+ IF r.x % 2 = 0 THEN
+ COMMIT;
+ ELSE
+ ROLLBACK;
+ END IF;
+ END LOOP;
+END;
+$$;
+SELECT * FROM test1;
+ a | b
+---+---
+ 0 |
+ 2 |
+ 4 |
+(3 rows)
+
+SELECT * FROM pg_cursors;
+ name | statement | is_holdable | is_binary | is_scrollable | creation_time
+------+-----------+-------------+-----------+---------------+---------------
+(0 rows)
+
+-- rollback inside cursor loop
+TRUNCATE test1;
+DO LANGUAGE plpgsql $$
+DECLARE
+ r RECORD;
+BEGIN
+ FOR r IN UPDATE test2 SET x = x * 2 RETURNING x LOOP
+ INSERT INTO test1 (a) VALUES (r.x);
+ ROLLBACK;
+ END LOOP;
+END;
+$$;
+ERROR: cannot perform transaction commands inside a cursor loop that is not read-only
CONTEXT: PL/pgSQL function inline_code_block line 7 at ROLLBACK
SELECT * FROM test1;
a | b
---+---
(0 rows)
+SELECT * FROM test2;
+ x
+---
+ 0
+ 1
+ 2
+ 3
+ 4
+(5 rows)
+
+SELECT * FROM pg_cursors;
+ name | statement | is_holdable | is_binary | is_scrollable | creation_time
+------+-----------+-------------+-----------+---------------+---------------
+(0 rows)
+
-- commit inside block with exception handler
TRUNCATE test1;
DO LANGUAGE plpgsql $$
static int
exec_stmt_commit(PLpgSQL_execstate *estate, PLpgSQL_stmt_commit *stmt)
{
- /*
- * XXX This could be implemented by converting the pinned portals to
- * holdable ones and organizing the cleanup separately.
- */
- if (ThereArePinnedPortals())
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("committing inside a cursor loop is not supported")));
+ HoldPinnedPortals();
SPI_commit();
SPI_start_transaction();
static int
exec_stmt_rollback(PLpgSQL_execstate *estate, PLpgSQL_stmt_rollback *stmt)
{
- /*
- * Unlike the COMMIT case above, this might not make sense at all,
- * especially if the query driving the cursor loop has side effects.
- */
- if (ThereArePinnedPortals())
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
- errmsg("cannot abort transaction inside a cursor loop")));
+ HoldPinnedPortals();
SPI_rollback();
SPI_start_transaction();
SELECT * FROM test1;
+-- check that this doesn't leak a holdable portal
+SELECT * FROM pg_cursors;
+
+
+-- error in cursor loop with commit
+TRUNCATE test1;
+
+DO LANGUAGE plpgsql $$
+DECLARE
+ r RECORD;
+BEGIN
+ FOR r IN SELECT * FROM test2 ORDER BY x LOOP
+ INSERT INTO test1 (a) VALUES (12/(r.x-2));
+ COMMIT;
+ END LOOP;
+END;
+$$;
+
+SELECT * FROM test1;
+
+SELECT * FROM pg_cursors;
+
-- rollback inside cursor loop
TRUNCATE test1;
SELECT * FROM test1;
+SELECT * FROM pg_cursors;
+
+
+-- first commit then rollback inside cursor loop
+TRUNCATE test1;
+
+DO LANGUAGE plpgsql $$
+DECLARE
+ r RECORD;
+BEGIN
+ FOR r IN SELECT * FROM test2 ORDER BY x LOOP
+ INSERT INTO test1 (a) VALUES (r.x);
+ IF r.x % 2 = 0 THEN
+ COMMIT;
+ ELSE
+ ROLLBACK;
+ END IF;
+ END LOOP;
+END;
+$$;
+
+SELECT * FROM test1;
+
+SELECT * FROM pg_cursors;
+
+
+-- rollback inside cursor loop
+TRUNCATE test1;
+
+DO LANGUAGE plpgsql $$
+DECLARE
+ r RECORD;
+BEGIN
+ FOR r IN UPDATE test2 SET x = x * 2 RETURNING x LOOP
+ INSERT INTO test1 (a) VALUES (r.x);
+ ROLLBACK;
+ END LOOP;
+END;
+$$;
+
+SELECT * FROM test1;
+SELECT * FROM test2;
+
+SELECT * FROM pg_cursors;
+
-- commit inside block with exception handler
TRUNCATE test1;
plpy.execute("INSERT INTO test1 (a) VALUES (%s)" % row['x'])
plpy.commit()
$$;
-ERROR: cannot commit transaction while a cursor is open
-CONTEXT: PL/Python anonymous code block
SELECT * FROM test1;
a | b
---+---
+ 0 |
+ 1 |
+ 2 |
+ 3 |
+ 4 |
+(5 rows)
+
+-- check that this doesn't leak a holdable portal
+SELECT * FROM pg_cursors;
+ name | statement | is_holdable | is_binary | is_scrollable | creation_time
+------+-----------+-------------+-----------+---------------+---------------
+(0 rows)
+
+-- error in cursor loop with commit
+TRUNCATE test1;
+DO LANGUAGE plpythonu $$
+for row in plpy.cursor("SELECT * FROM test2 ORDER BY x"):
+ plpy.execute("INSERT INTO test1 (a) VALUES (12/(%s-2))" % row['x'])
+ plpy.commit()
+$$;
+ERROR: spiexceptions.DivisionByZero: division by zero
+CONTEXT: Traceback (most recent call last):
+ PL/Python anonymous code block, line 3, in <module>
+ plpy.execute("INSERT INTO test1 (a) VALUES (12/(%s-2))" % row['x'])
+PL/Python anonymous code block
+SELECT * FROM test1;
+ a | b
+-----+---
+ -6 |
+ -12 |
+(2 rows)
+
+SELECT * FROM pg_cursors;
+ name | statement | is_holdable | is_binary | is_scrollable | creation_time
+------+-----------+-------------+-----------+---------------+---------------
(0 rows)
-- rollback inside cursor loop
plpy.execute("INSERT INTO test1 (a) VALUES (%s)" % row['x'])
plpy.rollback()
$$;
-ERROR: cannot abort transaction while a cursor is open
-CONTEXT: PL/Python anonymous code block
SELECT * FROM test1;
a | b
---+---
(0 rows)
+SELECT * FROM pg_cursors;
+ name | statement | is_holdable | is_binary | is_scrollable | creation_time
+------+-----------+-------------+-----------+---------------+---------------
+(0 rows)
+
+-- first commit then rollback inside cursor loop
+TRUNCATE test1;
+DO LANGUAGE plpythonu $$
+for row in plpy.cursor("SELECT * FROM test2 ORDER BY x"):
+ plpy.execute("INSERT INTO test1 (a) VALUES (%s)" % row['x'])
+ if row['x'] % 2 == 0:
+ plpy.commit()
+ else:
+ plpy.rollback()
+$$;
+SELECT * FROM test1;
+ a | b
+---+---
+ 0 |
+ 2 |
+ 4 |
+(3 rows)
+
+SELECT * FROM pg_cursors;
+ name | statement | is_holdable | is_binary | is_scrollable | creation_time
+------+-----------+-------------+-----------+---------------+---------------
+(0 rows)
+
DROP TABLE test1;
DROP TABLE test2;
{
PLyExecutionContext *exec_ctx = PLy_current_execution_context();
- if (ThereArePinnedPortals())
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("cannot commit transaction while a cursor is open")));
+ HoldPinnedPortals();
SPI_commit();
SPI_start_transaction();
{
PLyExecutionContext *exec_ctx = PLy_current_execution_context();
- if (ThereArePinnedPortals())
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
- errmsg("cannot abort transaction while a cursor is open")));
+ HoldPinnedPortals();
SPI_rollback();
SPI_start_transaction();
SELECT * FROM test1;
+-- check that this doesn't leak a holdable portal
+SELECT * FROM pg_cursors;
+
+
+-- error in cursor loop with commit
+TRUNCATE test1;
+
+DO LANGUAGE plpythonu $$
+for row in plpy.cursor("SELECT * FROM test2 ORDER BY x"):
+ plpy.execute("INSERT INTO test1 (a) VALUES (12/(%s-2))" % row['x'])
+ plpy.commit()
+$$;
+
+SELECT * FROM test1;
+
+SELECT * FROM pg_cursors;
+
-- rollback inside cursor loop
TRUNCATE test1;
SELECT * FROM test1;
+SELECT * FROM pg_cursors;
+
+
+-- first commit then rollback inside cursor loop
+TRUNCATE test1;
+
+DO LANGUAGE plpythonu $$
+for row in plpy.cursor("SELECT * FROM test2 ORDER BY x"):
+ plpy.execute("INSERT INTO test1 (a) VALUES (%s)" % row['x'])
+ if row['x'] % 2 == 0:
+ plpy.commit()
+ else:
+ plpy.rollback()
+$$;
+
+SELECT * FROM test1;
+
+SELECT * FROM pg_cursors;
+
DROP TABLE test1;
DROP TABLE test2;