</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term>
+ <literal><function>spi_commit()</function></literal>
+ <indexterm>
+ <primary>spi_commit</primary>
+ <secondary>in PL/Perl</secondary>
+ </indexterm>
+ </term>
+ <term>
+ <literal><function>spi_rollback()</function></literal>
+ <indexterm>
+ <primary>spi_rollback</primary>
+ <secondary>in PL/Perl</secondary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Commit or roll back the current transaction. This can only be called
+ in a procedure or anonymous code block (<command>DO</command> command)
+ called from the top level. (Note that it is not possible to run the
+ SQL commands <command>COMMIT</command> or <command>ROLLBACK</command>
+ via <function>spi_exec_query</function> or similar. It has to be done
+ using these functions.) After a transaction is ended, a new
+ transaction is automatically started, so there is no separate function
+ for that.
+ </para>
+
+ <para>
+ Here is an example:
+<programlisting>
+CREATE PROCEDURE transaction_test1()
+LANGUAGE plperl
+AS $$
+foreach my $i (0..9) {
+ spi_exec_query("INSERT INTO test1 (a) VALUES ($i)");
+ if ($i % 2 == 0) {
+ spi_commit();
+ } else {
+ spi_rollback();
+ }
+}
+$$;
+
+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>
</sect2>
</sect1>
+ <sect1 id="plpgsql-transactions">
+ <title>Transaction Management</title>
+
+ <para>
+ In procedures invoked by the <command>CALL</command> command from the top
+ level as well as in anonymous code blocks (<command>DO</command> command)
+ called from the top level, it is possible to end transactions using the
+ commands <command>COMMIT</command> and <command>ROLLBACK</command>. A new
+ transaction is started automatically after a transaction is ended using
+ these commands, so there is no separate <command>START
+ TRANSACTION</command> command. (Note that <command>BEGIN</command> and
+ <command>END</command> have different meanings in PL/pgSQL.)
+ </para>
+
+ <para>
+ Here is a simple example:
+<programlisting>
+CREATE PROCEDURE transaction_test1()
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ FOR i IN 0..9 LOOP
+ INSERT INTO test1 (a) VALUES (i);
+ IF i % 2 = 0 THEN
+ COMMIT;
+ ELSE
+ ROLLBACK;
+ END IF;
+ END LOOP;
+END
+$$;
+
+CALL transaction_test1();
+</programlisting>
+ </para>
+
+ <para>
+ A transaction cannot be ended inside a loop over a query result, nor
+ inside a block with exception handlers.
+ </para>
+ </sect1>
+
<sect1 id="plpgsql-errors-and-messages">
<title>Errors and Messages</title>
<programlisting>
CREATE OR REPLACE PROCEDURE cs_create_job(v_job_id IN INTEGER) IS
a_running_job_count INTEGER;
- PRAGMA AUTONOMOUS_TRANSACTION; -- <co id="co.plpgsql-porting-pragma"/>
BEGIN
- LOCK TABLE cs_jobs IN EXCLUSIVE MODE; -- <co id="co.plpgsql-porting-locktable"/>
+ LOCK TABLE cs_jobs IN EXCLUSIVE MODE;
SELECT count(*) INTO a_running_job_count FROM cs_jobs WHERE end_stamp IS NULL;
IF a_running_job_count > 0 THEN
- COMMIT; -- free lock <co id="co.plpgsql-porting-commit"/>
+ COMMIT; -- free lock
raise_application_error(-20000,
'Unable to create a new job: a job is currently running.');
END IF;
</programlisting>
</para>
- <para>
- Procedures like this can easily be converted into <productname>PostgreSQL</productname>
- functions returning <type>void</type>. This procedure in
- particular is interesting because it can teach us some things:
-
- <calloutlist>
- <callout arearefs="co.plpgsql-porting-pragma">
- <para>
- There is no <literal>PRAGMA</literal> statement in <productname>PostgreSQL</productname>.
- </para>
- </callout>
-
- <callout arearefs="co.plpgsql-porting-locktable">
- <para>
- If you do a <command>LOCK TABLE</command> in <application>PL/pgSQL</application>,
- the lock will not be released until the calling transaction is
- finished.
- </para>
- </callout>
-
- <callout arearefs="co.plpgsql-porting-commit">
- <para>
- You cannot issue <command>COMMIT</command> in a
- <application>PL/pgSQL</application> function. The function is
- running within some outer transaction and so <command>COMMIT</command>
- would imply terminating the function's execution. However, in
- this particular case it is not necessary anyway, because the lock
- obtained by the <command>LOCK TABLE</command> will be released when
- we raise an error.
- </para>
- </callout>
- </calloutlist>
- </para>
-
<para>
This is how we could port this procedure to <application>PL/pgSQL</application>:
<programlisting>
-CREATE OR REPLACE FUNCTION cs_create_job(v_job_id integer) RETURNS void AS $$
+CREATE OR REPLACE PROCEDURE cs_create_job(v_job_id integer) AS $$
DECLARE
a_running_job_count integer;
BEGIN
SELECT count(*) INTO a_running_job_count FROM cs_jobs WHERE end_stamp IS NULL;
IF a_running_job_count > 0 THEN
+ COMMIT; -- free lock
RAISE EXCEPTION 'Unable to create a new job: a job is currently running'; -- <co id="co.plpgsql-porting-raise"/>
END IF;
WHEN unique_violation THEN -- <co id="co.plpgsql-porting-exception"/>
-- don't worry if it already exists
END;
+ COMMIT;
END;
$$ LANGUAGE plpgsql;
</programlisting>
</para>
</callout>
</calloutlist>
-
- The main functional difference between this procedure and the
- Oracle equivalent is that the exclusive lock on the <literal>cs_jobs</literal>
- table will be held until the calling transaction completes. Also, if
- the caller later aborts (for example due to an error), the effects of
- this procedure will be rolled back.
</para>
</example>
</sect2>
</sect2>
</sect1>
+ <sect1 id="plpython-transactions">
+ <title>Transaction Management</title>
+
+ <para>
+ In a procedure called from the top level or an anonymous code block
+ (<command>DO</command> command) called from the top level it is possible to
+ control transactions. To commit the current transaction, call
+ <literal>plpy.commit()</literal>. To roll back the current transaction,
+ call <literal>plpy.rollback()</literal>. (Note that it is not possible to
+ run the SQL commands <command>COMMIT</command> or
+ <command>ROLLBACK</command> via <function>plpy.execute</function> or
+ similar. It has to be done using these functions.) After a transaction is
+ ended, a new transaction is automatically started, so there is no separate
+ function for that.
+ </para>
+
+ <para>
+ Here is an example:
+<programlisting>
+CREATE PROCEDURE transaction_test1()
+LANGUAGE plpythonu
+AS $$
+for i in range(0, 10):
+ plpy.execute("INSERT INTO test1 (a) VALUES (%d)" % i)
+ if i % 2 == 0:
+ plpy.commit()
+ else:
+ plpy.rollback()
+$$;
+
+CALL transaction_test1();
+</programlisting>
+ </para>
+
+ <para>
+ Transactions cannot be ended when a cursor created by
+ <literal>plpy.cursor</literal> is open or when an explicit subtransaction
+ is active.
+ </para>
+ </sect1>
+
<sect1 id="plpython-util">
<title>Utility Functions</title>
<para>
</para>
</sect1>
+ <sect1 id="pltcl-transactions">
+ <title>Transaction Management</title>
+
+ <para>
+ In a procedure called from the top level or an anonymous code block
+ (<command>DO</command> command) called from the top level it is possible
+ to control transactions. To commit the current transaction, call the
+ <literal>commit</literal> command. To roll back the current transaction,
+ call the <literal>rollback</literal> command. (Note that it is not
+ possible to run the SQL commands <command>COMMIT</command> or
+ <command>ROLLBACK</command> via <function>spi_exec</function> or similar.
+ It has to be done using these functions.) After a transaction is ended,
+ a new transaction is automatically started, so there is no separate
+ command for that.
+ </para>
+
+ <para>
+ Here is an example:
+<programlisting>
+CREATE PROCEDURE transaction_test1()
+LANGUAGE pltcl
+AS $$
+for {set i 0} {$i < 10} {incr i} {
+ spi_exec "INSERT INTO test1 (a) VALUES ($i)"
+ if {$i % 2 == 0} {
+ commit
+ } else {
+ rollback
+ }
+}
+$$;
+
+CALL transaction_test1();
+</programlisting>
+ </para>
+
+ <para>
+ Transactions cannot be ended when an explicit subtransaction is active.
+ </para>
+ </sect1>
+
<sect1 id="pltcl-config">
<title>PL/Tcl Configuration</title>
<para>
To call a function (not a procedure), use <command>SELECT</command> instead.
</para>
+
+ <para>
+ If <command>CALL</command> is executed in a transaction block, then the
+ called procedure cannot execute transaction control statements.
+ Transaction control statements are only allowed if <command>CALL</command>
+ is executed in its own transaction.
+ </para>
</refsect1>
<refsect1>
procedure exit, unless the current transaction is rolled back.
</para>
+ <para>
+ If a <literal>SET</literal> clause is attached to a procedure, then
+ that procedure cannot execute transaction control statements (for
+ example, <command>COMMIT</command> and <command>ROLLBACK</command>,
+ depending on the language).
+ </para>
+
<para>
See <xref linkend="sql-set"/> and
<xref linkend="runtime-config"/>
This is the same privilege requirement as for creating a function
in the language.
</para>
+
+ <para>
+ If <command>DO</command> is executed in a transaction block, then the
+ procedure code cannot execute transaction control statements. Transaction
+ control statements are only allowed if <command>DO</command> is executed in
+ its own transaction.
+ </para>
</refsect1>
<refsect1 id="sql-do-examples">
<refentry id="spi-spi-connect">
<indexterm><primary>SPI_connect</primary></indexterm>
+ <indexterm><primary>SPI_connect_ext</primary></indexterm>
<refmeta>
<refentrytitle>SPI_connect</refentrytitle>
<refnamediv>
<refname>SPI_connect</refname>
+ <refname>SPI_connect_ext</refname>
<refpurpose>connect a procedure to the SPI manager</refpurpose>
</refnamediv>
<refsynopsisdiv>
<synopsis>
int SPI_connect(void)
+</synopsis>
+
+ <synopsis>
+int SPI_connect_ext(int <parameter>options</parameter>)
</synopsis>
</refsynopsisdiv>
function if you want to execute commands through SPI. Some utility
SPI functions can be called from unconnected procedures.
</para>
+
+ <para>
+ <function>SPI_connect_ext</function> does the same but has an argument that
+ allows passing option flags. Currently, the following option values are
+ available:
+ <variablelist>
+ <varlistentry>
+ <term><symbol>SPI_OPT_NONATOMIC</symbol></term>
+ <listitem>
+ <para>
+ Sets the SPI connection to be <firstterm>nonatomic</firstterm>, which
+ means that transaction control calls <function>SPI_commit</function>,
+ <function>SPI_rollback</function>, and
+ <function>SPI_start_transaction</function> are allowed. Otherwise,
+ calling these functions will result in an immediate error.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+
+ <para>
+ <literal>SPI_connect()</literal> is equivalent to
+ <literal>SPI_connect_ext(0)</literal>.
+ </para>
</refsect1>
<refsect1>
</sect1>
+ <sect1 id="spi-transaction">
+ <title>Transaction Management</title>
+
+ <para>
+ It is not possible to run transaction control commands such
+ as <command>COMMIT</command> and <command>ROLLBACK</command> through SPI
+ functions such as <function>SPI_execute</function>. There are, however,
+ separate interface functions that allow transaction control through SPI.
+ </para>
+
+ <para>
+ It is not generally safe and sensible to start and end transactions in
+ arbitrary user-defined SQL-callable functions without taking into account
+ the context in which they are called. For example, a transaction boundary
+ in the middle of a function that is part of a complex SQL expression that
+ is part of some SQL command will probably result in obscure internal errors
+ or crashes. The interface functions presented here are primarily intended
+ to be used by procedural language implementations to support transaction
+ management in procedures that are invoked by the <command>CALL</command>
+ command, taking the context of the <command>CALL</command> invocation into
+ account. SPI procedures implemented in C can implement the same logic, but
+ the details of that are beyond the scope of this documentation.
+ </para>
+
+<!-- *********************************************** -->
+
+<refentry id="spi-spi-commit">
+ <indexterm><primary>SPI_commit</primary></indexterm>
+
+ <refmeta>
+ <refentrytitle>SPI_commit</refentrytitle>
+ <manvolnum>3</manvolnum>
+ </refmeta>
+
+ <refnamediv>
+ <refname>SPI_commit</refname>
+ <refpurpose>commit the current transaction</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+void SPI_commit(void)
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+ <title>Description</title>
+
+ <para>
+ <function>SPI_commit</function> commits the current transaction. It is
+ approximately equivalent to running the SQL
+ command <command>COMMIT</command>. After a transaction is committed, a new
+ transaction has to be started
+ using <function>SPI_start_transaction</function> before further database
+ actions can be executed.
+ </para>
+
+ <para>
+ This function can only be executed if the SPI connection has been set as
+ nonatomic in the call to <function>SPI_connect_ext</function>.
+ </para>
+ </refsect1>
+</refentry>
+
+<!-- *********************************************** -->
+
+<refentry id="spi-spi-rollback">
+ <indexterm><primary>SPI_rollback</primary></indexterm>
+
+ <refmeta>
+ <refentrytitle>SPI_rollback</refentrytitle>
+ <manvolnum>3</manvolnum>
+ </refmeta>
+
+ <refnamediv>
+ <refname>SPI_rollback</refname>
+ <refpurpose>abort the current transaction</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+void SPI_rollback(void)
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+ <title>Description</title>
+
+ <para>
+ <function>SPI_rollback</function> rolls back the current transaction. It
+ is approximately equivalent to running the SQL
+ command <command>ROLLBACK</command>. After a transaction is rolled back, a
+ new transaction has to be started
+ using <function>SPI_start_transaction</function> before further database
+ actions can be executed.
+ </para>
+
+ <para>
+ This function can only be executed if the SPI connection has been set as
+ nonatomic in the call to <function>SPI_connect_ext</function>.
+ </para>
+ </refsect1>
+</refentry>
+
+<!-- *********************************************** -->
+
+<refentry id="spi-spi-start-transaction">
+ <indexterm><primary>SPI_start_transaction</primary></indexterm>
+
+ <refmeta>
+ <refentrytitle>SPI_start_transaction</refentrytitle>
+ <manvolnum>3</manvolnum>
+ </refmeta>
+
+ <refnamediv>
+ <refname>SPI_start_transaction</refname>
+ <refpurpose>start a new transaction</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+void SPI_start_transaction(void)
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+ <title>Description</title>
+
+ <para>
+ <function>SPI_start_transaction</function> starts a new transaction. It
+ can only be called after <function>SPI_commit</function>
+ or <function>SPI_rollback</function>, as there is no transaction active at
+ that point. Normally, when an SPI procedure is called, there is already a
+ transaction active, so attempting to start another one before closing out
+ the current one will result in an error.
+ </para>
+
+ <para>
+ This function can only be executed if the SPI connection has been set as
+ nonatomic in the call to <function>SPI_connect_ext</function>.
+ </para>
+ </refsect1>
+</refentry>
+
+ </sect1>
+
<sect1 id="spi-visibility">
<title>Visibility of Data Changes</title>
#include "utils/fmgroids.h"
#include "utils/guc.h"
#include "utils/lsyscache.h"
+#include "utils/memutils.h"
#include "utils/rel.h"
#include "utils/syscache.h"
#include "utils/tqual.h"
/*
* ExecuteDoStmt
* Execute inline procedural-language code
+ *
+ * See at ExecuteCallStmt() about the atomic argument.
*/
void
-ExecuteDoStmt(DoStmt *stmt)
+ExecuteDoStmt(DoStmt *stmt, bool atomic)
{
InlineCodeBlock *codeblock = makeNode(InlineCodeBlock);
ListCell *arg;
codeblock->langOid = HeapTupleGetOid(languageTuple);
languageStruct = (Form_pg_language) GETSTRUCT(languageTuple);
codeblock->langIsTrusted = languageStruct->lanpltrusted;
+ codeblock->atomic = atomic;
if (languageStruct->lanpltrusted)
{
/*
* Execute CALL statement
+ *
+ * Inside a top-level CALL statement, transaction-terminating commands such as
+ * COMMIT or a PL-specific equivalent are allowed. The terminology in the SQL
+ * standard is that CALL establishes a non-atomic execution context. Most
+ * other commands establish an atomic execution context, in which transaction
+ * control actions are not allowed. If there are nested executions of CALL,
+ * we want to track the execution context recursively, so that the nested
+ * CALLs can also do transaction control. Note, however, that for example in
+ * CALL -> SELECT -> CALL, the second call cannot do transaction control,
+ * because the SELECT in between establishes an atomic execution context.
+ *
+ * So when ExecuteCallStmt() is called from the top level, we pass in atomic =
+ * false (recall that that means transactions = yes). We then create a
+ * CallContext node with content atomic = false, which is passed in the
+ * fcinfo->context field to the procedure invocation. The language
+ * implementation should then take appropriate measures to allow or prevent
+ * transaction commands based on that information, e.g., call
+ * SPI_connect_ext(SPI_OPT_NONATOMIC). The language should also pass on the
+ * atomic flag to any nested invocations to CALL.
*/
void
-ExecuteCallStmt(ParseState *pstate, CallStmt *stmt)
+ExecuteCallStmt(ParseState *pstate, CallStmt *stmt, bool atomic)
{
List *targs;
ListCell *lc;
AclResult aclresult;
FmgrInfo flinfo;
FunctionCallInfoData fcinfo;
+ CallContext *callcontext;
+ HeapTuple tp;
targs = NIL;
foreach(lc, stmt->funccall->args)
FUNC_MAX_ARGS,
FUNC_MAX_ARGS)));
+ callcontext = makeNode(CallContext);
+ callcontext->atomic = atomic;
+
+ /*
+ * If proconfig is set we can't allow transaction commands because of the
+ * way the GUC stacking works: The transaction boundary would have to pop
+ * the proconfig setting off the stack. That restriction could be lifted
+ * by redesigning the GUC nesting mechanism a bit.
+ */
+ tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(fexpr->funcid));
+ if (!HeapTupleIsValid(tp))
+ elog(ERROR, "cache lookup failed for function %u", fexpr->funcid);
+ if (!heap_attisnull(tp, Anum_pg_proc_proconfig))
+ callcontext->atomic = true;
+ ReleaseSysCache(tp);
+
fmgr_info(fexpr->funcid, &flinfo);
- InitFunctionCallInfoData(fcinfo, &flinfo, nargs, fexpr->inputcollid, NULL, NULL);
+ InitFunctionCallInfoData(fcinfo, &flinfo, nargs, fexpr->inputcollid, (Node *) callcontext, NULL);
i = 0;
foreach (lc, fexpr->args)
int
SPI_connect(void)
+{
+ return SPI_connect_ext(0);
+}
+
+int
+SPI_connect_ext(int options)
{
int newdepth;
elog(ERROR, "SPI stack corrupted");
newdepth = 16;
_SPI_stack = (_SPI_connection *)
- MemoryContextAlloc(TopTransactionContext,
+ MemoryContextAlloc(TopMemoryContext,
newdepth * sizeof(_SPI_connection));
_SPI_stack_depth = newdepth;
}
_SPI_current->execCxt = NULL;
_SPI_current->connectSubid = GetCurrentSubTransactionId();
_SPI_current->queryEnv = NULL;
+ _SPI_current->atomic = (options & SPI_OPT_NONATOMIC ? false : true);
+ _SPI_current->internal_xact = false;
/*
* Create memory contexts for this procedure
*
- * XXX it would be better to use PortalContext as the parent context, but
- * we may not be inside a portal (consider deferred-trigger execution).
- * Perhaps CurTransactionContext would do? For now it doesn't matter
- * because we clean up explicitly in AtEOSubXact_SPI().
+ * In atomic contexts (the normal case), we use TopTransactionContext,
+ * otherwise PortalContext, so that it lives across transaction
+ * boundaries.
+ *
+ * XXX It could be better to use PortalContext as the parent context in
+ * all cases, but we may not be inside a portal (consider deferred-trigger
+ * execution). Perhaps CurTransactionContext could be an option? For now
+ * it doesn't matter because we clean up explicitly in AtEOSubXact_SPI().
*/
- _SPI_current->procCxt = AllocSetContextCreate(TopTransactionContext,
+ _SPI_current->procCxt = AllocSetContextCreate(_SPI_current->atomic ? TopTransactionContext : PortalContext,
"SPI Proc",
ALLOCSET_DEFAULT_SIZES);
- _SPI_current->execCxt = AllocSetContextCreate(TopTransactionContext,
+ _SPI_current->execCxt = AllocSetContextCreate(_SPI_current->atomic ? TopTransactionContext : _SPI_current->procCxt,
"SPI Exec",
ALLOCSET_DEFAULT_SIZES);
/* ... and switch to procedure's context */
return SPI_OK_FINISH;
}
+void
+SPI_start_transaction(void)
+{
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ StartTransactionCommand();
+ MemoryContextSwitchTo(oldcontext);
+}
+
+void
+SPI_commit(void)
+{
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ if (_SPI_current->atomic)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
+ errmsg("invalid transaction termination")));
+
+ /*
+ * This restriction is required by PLs implemented on top of SPI. They
+ * use subtransactions to establish exception blocks that are supposed to
+ * be rolled back together if there is an error. Terminating the
+ * top-level transaction in such a block violates that idea. A future PL
+ * implementation might have different ideas about this, in which case
+ * this restriction would have to be refined or the check possibly be
+ * moved out of SPI into the PLs.
+ */
+ if (IsSubTransaction())
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
+ errmsg("cannot commit while a subtransaction is active")));
+
+ _SPI_current->internal_xact = true;
+
+ if (ActiveSnapshotSet())
+ PopActiveSnapshot();
+ CommitTransactionCommand();
+ MemoryContextSwitchTo(oldcontext);
+
+ _SPI_current->internal_xact = false;
+}
+
+void
+SPI_rollback(void)
+{
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ if (_SPI_current->atomic)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
+ errmsg("invalid transaction termination")));
+
+ /* see under SPI_commit() */
+ if (IsSubTransaction())
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
+ errmsg("cannot roll back while a subtransaction is active")));
+
+ _SPI_current->internal_xact = true;
+
+ AbortCurrentTransaction();
+ MemoryContextSwitchTo(oldcontext);
+
+ _SPI_current->internal_xact = false;
+}
+
/*
* Clean up SPI state at transaction commit or abort.
*/
void
AtEOXact_SPI(bool isCommit)
{
+ /*
+ * Do nothing if the transaction end was initiated by SPI.
+ */
+ if (_SPI_current && _SPI_current->internal_xact)
+ return;
+
/*
* Note that memory contexts belonging to SPI stack entries will be freed
* automatically, so we can ignore them here. We just need to restore our
if (connection->connectSubid != mySubid)
break; /* couldn't be any underneath it either */
+ if (connection->internal_xact)
+ break;
+
found = true;
/*
break;
case T_DoStmt:
- ExecuteDoStmt((DoStmt *) parsetree);
+ ExecuteDoStmt((DoStmt *) parsetree,
+ (context != PROCESS_UTILITY_TOPLEVEL || IsTransactionBlock()));
break;
case T_CreateTableSpaceStmt:
break;
case T_CallStmt:
- ExecuteCallStmt(pstate, castNode(CallStmt, parsetree));
+ ExecuteCallStmt(pstate, castNode(CallStmt, parsetree),
+ (context != PROCESS_UTILITY_TOPLEVEL || IsTransactionBlock()));
break;
case T_ClusterStmt:
/*
* Abort processing for portals.
*
- * At this point we reset "active" status and run the cleanup hook if
- * present, but we can't release the portal's memory until the cleanup call.
- *
- * The reason we need to reset active is so that we can replace the unnamed
- * portal, else we'll fail to execute ROLLBACK when it arrives.
+ * At this point we run the cleanup hook if present, but we can't release the
+ * portal's memory until the cleanup call.
*/
void
AtAbort_Portals(void)
{
Portal portal = hentry->portal;
- /*
- * See similar code in AtSubAbort_Portals(). This would fire if code
- * orchestrating multiple top-level transactions within a portal, such
- * as VACUUM, caught errors and continued under the same portal with a
- * fresh transaction. No part of core PostgreSQL functions that way.
- * XXX Such code would wish the portal to remain ACTIVE, as in
- * PreCommit_Portals().
- */
- if (portal->status == PORTAL_ACTIVE)
- MarkPortalFailed(portal);
-
/*
* Do nothing else to cursors held over from a previous transaction.
*/
* Although we can't delete the portal data structure proper, we can
* release any memory in subsidiary contexts, such as executor state.
* The cleanup hook was the last thing that might have needed data
- * there.
+ * there. But leave active portals alone.
*/
- MemoryContextDeleteChildren(portal->portalContext);
+ if (portal->status != PORTAL_ACTIVE)
+ MemoryContextDeleteChildren(portal->portalContext);
}
}
{
Portal portal = hentry->portal;
+ /*
+ * Do not touch active portals --- this can only happen in the case of
+ * a multi-transaction command.
+ */
+ if (portal->status == PORTAL_ACTIVE)
+ continue;
+
/* Do nothing to cursors held over from a previous transaction */
if (portal->createSubid == InvalidSubTransactionId)
{
return true;
}
+
+bool
+ThereArePinnedPortals(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->portalPinned)
+ return true;
+ }
+
+ return false;
+}
extern void DropTransformById(Oid transformOid);
extern void IsThereFunctionInNamespace(const char *proname, int pronargs,
oidvector *proargtypes, Oid nspOid);
-extern void ExecuteDoStmt(DoStmt *stmt);
-extern void ExecuteCallStmt(ParseState *pstate, CallStmt *stmt);
+extern void ExecuteDoStmt(DoStmt *stmt, bool atomic);
+extern void ExecuteCallStmt(ParseState *pstate, CallStmt *stmt, bool atomic);
extern Oid get_cast_oid(Oid sourcetypeid, Oid targettypeid, bool missing_ok);
extern Oid get_transform_oid(Oid type_id, Oid lang_id, bool missing_ok);
extern void interpret_function_parameter_list(ParseState *pstate,
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
+#define SPI_OPT_NONATOMIC (1 << 0)
+
/* These used to be functions, now just no-ops for backwards compatibility */
#define SPI_push() ((void) 0)
#define SPI_pop() ((void) 0)
extern PGDLLIMPORT int SPI_result;
extern int SPI_connect(void);
+extern int SPI_connect_ext(int options);
extern int SPI_finish(void);
extern int SPI_execute(const char *src, bool read_only, long tcount);
extern int SPI_execute_plan(SPIPlanPtr plan, Datum *Values, const char *Nulls,
extern int SPI_unregister_relation(const char *name);
extern int SPI_register_trigger_data(TriggerData *tdata);
+extern void SPI_start_transaction(void);
+extern void SPI_commit(void);
+extern void SPI_rollback(void);
+
extern void AtEOXact_SPI(bool isCommit);
extern void AtEOSubXact_SPI(bool isCommit, SubTransactionId mySubid);
MemoryContext savedcxt; /* context of SPI_connect's caller */
SubTransactionId connectSubid; /* ID of connecting subtransaction */
QueryEnvironment *queryEnv; /* query environment setup for SPI level */
+
+ /* transaction management support */
+ bool atomic; /* atomic execution context, does not allow transactions */
+ bool internal_xact; /* SPI-managed transaction boundary, skip cleanup */
} _SPI_connection;
/*
T_FdwRoutine, /* in foreign/fdwapi.h */
T_IndexAmRoutine, /* in access/amapi.h */
T_TsmRoutine, /* in access/tsmapi.h */
- T_ForeignKeyCacheInfo /* in utils/rel.h */
+ T_ForeignKeyCacheInfo, /* in utils/rel.h */
+ T_CallContext /* in nodes/parsenodes.h */
} NodeTag;
/*
char *source_text; /* source text of anonymous code block */
Oid langOid; /* OID of selected language */
bool langIsTrusted; /* trusted property of the language */
+ bool atomic; /* atomic execution context */
} InlineCodeBlock;
/* ----------------------
FuncCall *funccall;
} CallStmt;
+typedef struct CallContext
+{
+ NodeTag type;
+ bool atomic;
+} CallContext;
+
/* ----------------------
* Alter Object Rename Statement
* ----------------------
extern void PortalCreateHoldStore(Portal portal);
extern void PortalHashTableDeleteAll(void);
extern bool ThereAreNoReadyPortals(void);
+extern bool ThereArePinnedPortals(void);
#endif /* PORTAL_H */
SHLIB_LINK = $(perl_embed_ldflags)
REGRESS_OPTS = --dbname=$(PL_TESTDB) --load-extension=plperl --load-extension=plperlu
-REGRESS = plperl plperl_lc plperl_trigger plperl_shared plperl_elog plperl_util plperl_init plperlu plperl_array plperl_call
+REGRESS = plperl plperl_lc plperl_trigger plperl_shared plperl_elog plperl_util plperl_init plperlu plperl_array plperl_call plperl_transaction
# if Perl can support two interpreters in one backend,
# test plperl-and-plperlu cases
ifneq ($(PERL),)
plperl_spi_cursor_close(cursor);
pfree(cursor);
+void
+spi_spi_commit()
+ CODE:
+ plperl_spi_commit();
+
+void
+spi_spi_rollback()
+ CODE:
+ plperl_spi_rollback();
BOOT:
items = 0; /* avoid 'unused variable' warning */
--- /dev/null
+CREATE TABLE test1 (a int, b text);
+CREATE PROCEDURE transaction_test1()
+LANGUAGE plperl
+AS $$
+foreach my $i (0..9) {
+ spi_exec_query("INSERT INTO test1 (a) VALUES ($i)");
+ if ($i % 2 == 0) {
+ spi_commit();
+ } else {
+ spi_rollback();
+ }
+}
+$$;
+CALL transaction_test1();
+SELECT * FROM test1;
+ a | b
+---+---
+ 0 |
+ 2 |
+ 4 |
+ 6 |
+ 8 |
+(5 rows)
+
+TRUNCATE test1;
+DO
+LANGUAGE plperl
+$$
+foreach my $i (0..9) {
+ spi_exec_query("INSERT INTO test1 (a) VALUES ($i)");
+ if ($i % 2 == 0) {
+ spi_commit();
+ } else {
+ spi_rollback();
+ }
+}
+$$;
+SELECT * FROM test1;
+ a | b
+---+---
+ 0 |
+ 2 |
+ 4 |
+ 6 |
+ 8 |
+(5 rows)
+
+TRUNCATE test1;
+-- not allowed in a function
+CREATE FUNCTION transaction_test2() RETURNS int
+LANGUAGE plperl
+AS $$
+foreach my $i (0..9) {
+ spi_exec_query("INSERT INTO test1 (a) VALUES ($i)");
+ if ($i % 2 == 0) {
+ spi_commit();
+ } else {
+ spi_rollback();
+ }
+}
+return 1;
+$$;
+SELECT transaction_test2();
+ERROR: invalid transaction termination at line 5.
+CONTEXT: PL/Perl function "transaction_test2"
+SELECT * FROM test1;
+ a | b
+---+---
+(0 rows)
+
+-- also not allowed if procedure is called from a function
+CREATE FUNCTION transaction_test3() RETURNS int
+LANGUAGE plperl
+AS $$
+spi_exec_query("CALL transaction_test1()");
+return 1;
+$$;
+SELECT transaction_test3();
+ERROR: invalid transaction termination at line 5. at line 2.
+CONTEXT: PL/Perl function "transaction_test3"
+SELECT * FROM test1;
+ a | b
+---+---
+(0 rows)
+
+-- DO block inside function
+CREATE FUNCTION transaction_test4() RETURNS int
+LANGUAGE plperl
+AS $$
+spi_exec_query('DO LANGUAGE plperl $x$ spi_commit(); $x$');
+return 1;
+$$;
+SELECT transaction_test4();
+ERROR: invalid transaction termination at line 1. at line 2.
+CONTEXT: PL/Perl function "transaction_test4"
+-- commit inside cursor loop
+CREATE TABLE test2 (x int);
+INSERT INTO test2 VALUES (0), (1), (2), (3), (4);
+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} . ")");
+ 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 rows)
+
+-- 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} . ")");
+ 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)
+
+DROP TABLE test1;
+DROP TABLE test2;
current_call_data = &this_call_data;
- if (SPI_connect() != SPI_OK_CONNECT)
+ if (SPI_connect_ext(codeblock->atomic ? 0 : SPI_OPT_NONATOMIC) != SPI_OK_CONNECT)
elog(ERROR, "could not connect to SPI manager");
select_perl_context(desc.lanpltrusted);
static Datum
plperl_func_handler(PG_FUNCTION_ARGS)
{
+ bool nonatomic;
plperl_proc_desc *prodesc;
SV *perlret;
Datum retval = 0;
ReturnSetInfo *rsi;
ErrorContextCallback pl_error_context;
- if (SPI_connect() != SPI_OK_CONNECT)
+ nonatomic = fcinfo->context &&
+ IsA(fcinfo->context, CallContext) &&
+ !castNode(CallContext, fcinfo->context)->atomic;
+
+ if (SPI_connect_ext(nonatomic ? SPI_OPT_NONATOMIC : 0) != SPI_OK_CONNECT)
elog(ERROR, "could not connect to SPI manager");
prodesc = compile_plperl_function(fcinfo->flinfo->fn_oid, false, false);
SPI_freeplan(plan);
}
+void
+plperl_spi_commit(void)
+{
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ PG_TRY();
+ {
+ if (ThereArePinnedPortals())
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot commit transaction while a cursor is open")));
+
+ SPI_commit();
+ SPI_start_transaction();
+ }
+ PG_CATCH();
+ {
+ ErrorData *edata;
+
+ /* Save error info */
+ MemoryContextSwitchTo(oldcontext);
+ edata = CopyErrorData();
+ FlushErrorState();
+
+ /* Punt the error to Perl */
+ croak_cstr(edata->message);
+ }
+ PG_END_TRY();
+}
+
+void
+plperl_spi_rollback(void)
+{
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ PG_TRY();
+ {
+ if (ThereArePinnedPortals())
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TRANSACTION_TERMINATION),
+ errmsg("cannot abort transaction while a cursor is open")));
+
+ SPI_rollback();
+ SPI_start_transaction();
+ }
+ PG_CATCH();
+ {
+ ErrorData *edata;
+
+ /* Save error info */
+ MemoryContextSwitchTo(oldcontext);
+ edata = CopyErrorData();
+ FlushErrorState();
+
+ /* Punt the error to Perl */
+ croak_cstr(edata->message);
+ }
+ PG_END_TRY();
+}
+
/*
* Implementation of plperl's elog() function
*
SV *plperl_spi_query_prepared(char *, int, SV **);
void plperl_spi_freeplan(char *);
void plperl_spi_cursor_close(char *);
+void plperl_spi_commit(void);
+void plperl_spi_rollback(void);
char *plperl_sv_to_literal(SV *, char *);
void plperl_util_elog(int level, SV *msg);
--- /dev/null
+CREATE TABLE test1 (a int, b text);
+
+
+CREATE PROCEDURE transaction_test1()
+LANGUAGE plperl
+AS $$
+foreach my $i (0..9) {
+ spi_exec_query("INSERT INTO test1 (a) VALUES ($i)");
+ if ($i % 2 == 0) {
+ spi_commit();
+ } else {
+ spi_rollback();
+ }
+}
+$$;
+
+CALL transaction_test1();
+
+SELECT * FROM test1;
+
+
+TRUNCATE test1;
+
+DO
+LANGUAGE plperl
+$$
+foreach my $i (0..9) {
+ spi_exec_query("INSERT INTO test1 (a) VALUES ($i)");
+ if ($i % 2 == 0) {
+ spi_commit();
+ } else {
+ spi_rollback();
+ }
+}
+$$;
+
+SELECT * FROM test1;
+
+
+TRUNCATE test1;
+
+-- not allowed in a function
+CREATE FUNCTION transaction_test2() RETURNS int
+LANGUAGE plperl
+AS $$
+foreach my $i (0..9) {
+ spi_exec_query("INSERT INTO test1 (a) VALUES ($i)");
+ if ($i % 2 == 0) {
+ spi_commit();
+ } else {
+ spi_rollback();
+ }
+}
+return 1;
+$$;
+
+SELECT transaction_test2();
+
+SELECT * FROM test1;
+
+
+-- also not allowed if procedure is called from a function
+CREATE FUNCTION transaction_test3() RETURNS int
+LANGUAGE plperl
+AS $$
+spi_exec_query("CALL transaction_test1()");
+return 1;
+$$;
+
+SELECT transaction_test3();
+
+SELECT * FROM test1;
+
+
+-- DO block inside function
+CREATE FUNCTION transaction_test4() RETURNS int
+LANGUAGE plperl
+AS $$
+spi_exec_query('DO LANGUAGE plperl $x$ spi_commit(); $x$');
+return 1;
+$$;
+
+SELECT transaction_test4();
+
+
+-- commit inside cursor loop
+CREATE TABLE test2 (x int);
+INSERT INTO test2 VALUES (0), (1), (2), (3), (4);
+
+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} . ")");
+ spi_commit();
+}
+$$;
+
+SELECT * FROM test1;
+
+
+-- 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} . ")");
+ spi_rollback();
+}
+$$;
+
+SELECT * FROM test1;
+
+
+DROP TABLE test1;
+DROP TABLE test2;
REGRESS_OPTS = --dbname=$(PL_TESTDB)
-REGRESS = plpgsql_call plpgsql_control
+REGRESS = plpgsql_call plpgsql_control plpgsql_transaction
all: all-lib
--- /dev/null
+CREATE TABLE test1 (a int, b text);
+CREATE PROCEDURE transaction_test1()
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ FOR i IN 0..9 LOOP
+ INSERT INTO test1 (a) VALUES (i);
+ IF i % 2 = 0 THEN
+ COMMIT;
+ ELSE
+ ROLLBACK;
+ END IF;
+ END LOOP;
+END
+$$;
+CALL transaction_test1();
+SELECT * FROM test1;
+ a | b
+---+---
+ 0 |
+ 2 |
+ 4 |
+ 6 |
+ 8 |
+(5 rows)
+
+TRUNCATE test1;
+DO
+LANGUAGE plpgsql
+$$
+BEGIN
+ FOR i IN 0..9 LOOP
+ INSERT INTO test1 (a) VALUES (i);
+ IF i % 2 = 0 THEN
+ COMMIT;
+ ELSE
+ ROLLBACK;
+ END IF;
+ END LOOP;
+END
+$$;
+SELECT * FROM test1;
+ a | b
+---+---
+ 0 |
+ 2 |
+ 4 |
+ 6 |
+ 8 |
+(5 rows)
+
+-- transaction commands not allowed when called in transaction block
+START TRANSACTION;
+CALL transaction_test1();
+ERROR: invalid transaction termination
+CONTEXT: PL/pgSQL function transaction_test1() line 6 at COMMIT
+COMMIT;
+START TRANSACTION;
+DO LANGUAGE plpgsql $$ BEGIN COMMIT; END $$;
+ERROR: invalid transaction termination
+CONTEXT: PL/pgSQL function inline_code_block line 1 at COMMIT
+COMMIT;
+TRUNCATE test1;
+-- not allowed in a function
+CREATE FUNCTION transaction_test2() RETURNS int
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ FOR i IN 0..9 LOOP
+ INSERT INTO test1 (a) VALUES (i);
+ IF i % 2 = 0 THEN
+ COMMIT;
+ ELSE
+ ROLLBACK;
+ END IF;
+ END LOOP;
+ RETURN 1;
+END
+$$;
+SELECT transaction_test2();
+ERROR: invalid transaction termination
+CONTEXT: PL/pgSQL function transaction_test2() line 6 at COMMIT
+SELECT * FROM test1;
+ a | b
+---+---
+(0 rows)
+
+-- also not allowed if procedure is called from a function
+CREATE FUNCTION transaction_test3() RETURNS int
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ CALL transaction_test1();
+ RETURN 1;
+END;
+$$;
+SELECT transaction_test3();
+ERROR: invalid transaction termination
+CONTEXT: PL/pgSQL function transaction_test1() line 6 at COMMIT
+SQL statement "CALL transaction_test1()"
+PL/pgSQL function transaction_test3() line 3 at SQL statement
+SELECT * FROM test1;
+ a | b
+---+---
+(0 rows)
+
+-- DO block inside function
+CREATE FUNCTION transaction_test4() RETURNS int
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ EXECUTE 'DO LANGUAGE plpgsql $x$ BEGIN COMMIT; END $x$';
+ RETURN 1;
+END;
+$$;
+SELECT transaction_test4();
+ERROR: invalid transaction termination
+CONTEXT: PL/pgSQL function inline_code_block line 1 at COMMIT
+SQL statement "DO LANGUAGE plpgsql $x$ BEGIN COMMIT; END $x$"
+PL/pgSQL function transaction_test4() line 3 at EXECUTE
+-- proconfig settings currently disallow transaction statements
+CREATE PROCEDURE transaction_test5()
+LANGUAGE plpgsql
+SET work_mem = 555
+AS $$
+BEGIN
+ COMMIT;
+END;
+$$;
+CALL transaction_test5();
+ERROR: invalid transaction termination
+CONTEXT: PL/pgSQL function transaction_test5() line 3 at COMMIT
+-- commit inside cursor loop
+CREATE TABLE test2 (x int);
+INSERT INTO test2 VALUES (0), (1), (2), (3), (4);
+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);
+ COMMIT;
+ 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 rows)
+
+-- 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);
+ ROLLBACK;
+ END LOOP;
+END;
+$$;
+ERROR: cannot abort transaction inside a cursor loop
+CONTEXT: PL/pgSQL function inline_code_block line 7 at ROLLBACK
+SELECT * FROM test1;
+ a | b
+---+---
+(0 rows)
+
+-- commit inside block with exception handler
+TRUNCATE test1;
+DO LANGUAGE plpgsql $$
+BEGIN
+ BEGIN
+ INSERT INTO test1 (a) VALUES (1);
+ COMMIT;
+ INSERT INTO test1 (a) VALUES (1/0);
+ COMMIT;
+ EXCEPTION
+ WHEN division_by_zero THEN
+ RAISE NOTICE 'caught division_by_zero';
+ END;
+END;
+$$;
+ERROR: cannot commit while a subtransaction is active
+CONTEXT: PL/pgSQL function inline_code_block line 5 at COMMIT
+SELECT * FROM test1;
+ a | b
+---+---
+(0 rows)
+
+-- rollback inside block with exception handler
+TRUNCATE test1;
+DO LANGUAGE plpgsql $$
+BEGIN
+ BEGIN
+ INSERT INTO test1 (a) VALUES (1);
+ ROLLBACK;
+ INSERT INTO test1 (a) VALUES (1/0);
+ ROLLBACK;
+ EXCEPTION
+ WHEN division_by_zero THEN
+ RAISE NOTICE 'caught division_by_zero';
+ END;
+END;
+$$;
+ERROR: cannot roll back while a subtransaction is active
+CONTEXT: PL/pgSQL function inline_code_block line 5 at ROLLBACK
+SELECT * FROM test1;
+ a | b
+---+---
+(0 rows)
+
+-- COMMIT failures
+DO LANGUAGE plpgsql $$
+BEGIN
+ CREATE TABLE test3 (y int UNIQUE DEFERRABLE INITIALLY DEFERRED);
+ COMMIT;
+ INSERT INTO test3 (y) VALUES (1);
+ COMMIT;
+ INSERT INTO test3 (y) VALUES (1);
+ INSERT INTO test3 (y) VALUES (2);
+ COMMIT;
+ INSERT INTO test3 (y) VALUES (3); -- won't get here
+END;
+$$;
+ERROR: duplicate key value violates unique constraint "test3_y_key"
+DETAIL: Key (y)=(1) already exists.
+CONTEXT: PL/pgSQL function inline_code_block line 9 at COMMIT
+SELECT * FROM test3;
+ y
+---
+ 1
+(1 row)
+
+DROP TABLE test1;
+DROP TABLE test2;
+DROP TABLE test3;
PLpgSQL_stmt_dynexecute *stmt);
static int exec_stmt_dynfors(PLpgSQL_execstate *estate,
PLpgSQL_stmt_dynfors *stmt);
+static int exec_stmt_commit(PLpgSQL_execstate *estate,
+ PLpgSQL_stmt_commit *stmt);
+static int exec_stmt_rollback(PLpgSQL_execstate *estate,
+ PLpgSQL_stmt_rollback *stmt);
static void plpgsql_estate_setup(PLpgSQL_execstate *estate,
PLpgSQL_function *func,
rc = exec_stmt_close(estate, (PLpgSQL_stmt_close *) stmt);
break;
+ case PLPGSQL_STMT_COMMIT:
+ rc = exec_stmt_commit(estate, (PLpgSQL_stmt_commit *) stmt);
+ break;
+
+ case PLPGSQL_STMT_ROLLBACK:
+ rc = exec_stmt_rollback(estate, (PLpgSQL_stmt_rollback *) stmt);
+ break;
+
default:
estate->err_stmt = save_estmt;
elog(ERROR, "unrecognized cmdtype: %d", stmt->cmd_type);
return PLPGSQL_RC_OK;
}
+/*
+ * exec_stmt_commit
+ *
+ * Commit the transaction.
+ */
+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")));
+
+ SPI_commit();
+ SPI_start_transaction();
+
+ estate->simple_eval_estate = NULL;
+ plpgsql_create_econtext(estate);
+
+ return PLPGSQL_RC_OK;
+}
+
+/*
+ * exec_stmt_rollback
+ *
+ * Abort the 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")));
+
+ SPI_rollback();
+ SPI_start_transaction();
+
+ estate->simple_eval_estate = NULL;
+ plpgsql_create_econtext(estate);
+
+ return PLPGSQL_RC_OK;
+}
/* ----------
* exec_assign_expr Put an expression's result into a variable.
*/
if (event == XACT_EVENT_COMMIT || event == XACT_EVENT_PREPARE)
{
- /* Shouldn't be any econtext stack entries left at commit */
- Assert(simple_econtext_stack == NULL);
+ simple_econtext_stack = NULL;
if (shared_simple_eval_estate)
FreeExecutorState(shared_simple_eval_estate);
return "CLOSE";
case PLPGSQL_STMT_PERFORM:
return "PERFORM";
+ case PLPGSQL_STMT_COMMIT:
+ return "COMMIT";
+ case PLPGSQL_STMT_ROLLBACK:
+ return "ROLLBACK";
}
return "unknown";
static void free_fetch(PLpgSQL_stmt_fetch *stmt);
static void free_close(PLpgSQL_stmt_close *stmt);
static void free_perform(PLpgSQL_stmt_perform *stmt);
+static void free_commit(PLpgSQL_stmt_commit *stmt);
+static void free_rollback(PLpgSQL_stmt_rollback *stmt);
static void free_expr(PLpgSQL_expr *expr);
case PLPGSQL_STMT_PERFORM:
free_perform((PLpgSQL_stmt_perform *) stmt);
break;
+ case PLPGSQL_STMT_COMMIT:
+ free_commit((PLpgSQL_stmt_commit *) stmt);
+ break;
+ case PLPGSQL_STMT_ROLLBACK:
+ free_rollback((PLpgSQL_stmt_rollback *) stmt);
+ break;
default:
elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type);
break;
free_expr(stmt->expr);
}
+static void
+free_commit(PLpgSQL_stmt_commit *stmt)
+{
+}
+
+static void
+free_rollback(PLpgSQL_stmt_rollback *stmt)
+{
+}
+
static void
free_exit(PLpgSQL_stmt_exit *stmt)
{
static void dump_cursor_direction(PLpgSQL_stmt_fetch *stmt);
static void dump_close(PLpgSQL_stmt_close *stmt);
static void dump_perform(PLpgSQL_stmt_perform *stmt);
+static void dump_commit(PLpgSQL_stmt_commit *stmt);
+static void dump_rollback(PLpgSQL_stmt_rollback *stmt);
static void dump_expr(PLpgSQL_expr *expr);
case PLPGSQL_STMT_PERFORM:
dump_perform((PLpgSQL_stmt_perform *) stmt);
break;
+ case PLPGSQL_STMT_COMMIT:
+ dump_commit((PLpgSQL_stmt_commit *) stmt);
+ break;
+ case PLPGSQL_STMT_ROLLBACK:
+ dump_rollback((PLpgSQL_stmt_rollback *) stmt);
+ break;
default:
elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type);
break;
printf("\n");
}
+static void
+dump_commit(PLpgSQL_stmt_commit *stmt)
+{
+ dump_ind();
+ printf("COMMIT\n");
+}
+
+static void
+dump_rollback(PLpgSQL_stmt_rollback *stmt)
+{
+ dump_ind();
+ printf("ROLLBACK\n");
+}
+
static void
dump_exit(PLpgSQL_stmt_exit *stmt)
{
%type <stmt> stmt_return stmt_raise stmt_assert stmt_execsql
%type <stmt> stmt_dynexecute stmt_for stmt_perform stmt_getdiag
%type <stmt> stmt_open stmt_fetch stmt_move stmt_close stmt_null
+%type <stmt> stmt_commit stmt_rollback
%type <stmt> stmt_case stmt_foreach_a
%type <list> proc_exceptions
%token <keyword> K_COLLATE
%token <keyword> K_COLUMN
%token <keyword> K_COLUMN_NAME
+%token <keyword> K_COMMIT
%token <keyword> K_CONSTANT
%token <keyword> K_CONSTRAINT
%token <keyword> K_CONSTRAINT_NAME
%token <keyword> K_RETURN
%token <keyword> K_RETURNED_SQLSTATE
%token <keyword> K_REVERSE
+%token <keyword> K_ROLLBACK
%token <keyword> K_ROW_COUNT
%token <keyword> K_ROWTYPE
%token <keyword> K_SCHEMA
{ $$ = $1; }
| stmt_null
{ $$ = $1; }
+ | stmt_commit
+ { $$ = $1; }
+ | stmt_rollback
+ { $$ = $1; }
;
stmt_perform : K_PERFORM expr_until_semi
}
;
+stmt_commit : K_COMMIT ';'
+ {
+ PLpgSQL_stmt_commit *new;
+
+ new = palloc(sizeof(PLpgSQL_stmt_commit));
+ new->cmd_type = PLPGSQL_STMT_COMMIT;
+ new->lineno = plpgsql_location_to_lineno(@1);
+
+ $$ = (PLpgSQL_stmt *)new;
+ }
+ ;
+
+stmt_rollback : K_ROLLBACK ';'
+ {
+ PLpgSQL_stmt_rollback *new;
+
+ new = palloc(sizeof(PLpgSQL_stmt_rollback));
+ new->cmd_type = PLPGSQL_STMT_ROLLBACK;
+ new->lineno = plpgsql_location_to_lineno(@1);
+
+ $$ = (PLpgSQL_stmt *)new;
+ }
+ ;
+
+
cursor_variable : T_DATUM
{
/*
| K_COLLATE
| K_COLUMN
| K_COLUMN_NAME
+ | K_COMMIT
| K_CONSTANT
| K_CONSTRAINT
| K_CONSTRAINT_NAME
| K_RETURN
| K_RETURNED_SQLSTATE
| K_REVERSE
+ | K_ROLLBACK
| K_ROW_COUNT
| K_ROWTYPE
| K_SCHEMA
Datum
plpgsql_call_handler(PG_FUNCTION_ARGS)
{
+ bool nonatomic;
PLpgSQL_function *func;
PLpgSQL_execstate *save_cur_estate;
Datum retval;
int rc;
+ nonatomic = fcinfo->context &&
+ IsA(fcinfo->context, CallContext) &&
+ !castNode(CallContext, fcinfo->context)->atomic;
+
/*
* Connect to SPI manager
*/
- if ((rc = SPI_connect()) != SPI_OK_CONNECT)
+ if ((rc = SPI_connect_ext(nonatomic ? SPI_OPT_NONATOMIC : 0)) != SPI_OK_CONNECT)
elog(ERROR, "SPI_connect failed: %s", SPI_result_code_string(rc));
/* Find or compile the function */
/*
* Connect to SPI manager
*/
- if ((rc = SPI_connect()) != SPI_OK_CONNECT)
+ if ((rc = SPI_connect_ext(codeblock->atomic ? 0 : SPI_OPT_NONATOMIC)) != SPI_OK_CONNECT)
elog(ERROR, "SPI_connect failed: %s", SPI_result_code_string(rc));
/* Compile the anonymous code block */
PG_KEYWORD("collate", K_COLLATE, UNRESERVED_KEYWORD)
PG_KEYWORD("column", K_COLUMN, UNRESERVED_KEYWORD)
PG_KEYWORD("column_name", K_COLUMN_NAME, UNRESERVED_KEYWORD)
+ PG_KEYWORD("commit", K_COMMIT, UNRESERVED_KEYWORD)
PG_KEYWORD("constant", K_CONSTANT, UNRESERVED_KEYWORD)
PG_KEYWORD("constraint", K_CONSTRAINT, UNRESERVED_KEYWORD)
PG_KEYWORD("constraint_name", K_CONSTRAINT_NAME, UNRESERVED_KEYWORD)
PG_KEYWORD("return", K_RETURN, UNRESERVED_KEYWORD)
PG_KEYWORD("returned_sqlstate", K_RETURNED_SQLSTATE, UNRESERVED_KEYWORD)
PG_KEYWORD("reverse", K_REVERSE, UNRESERVED_KEYWORD)
+ PG_KEYWORD("rollback", K_ROLLBACK, UNRESERVED_KEYWORD)
PG_KEYWORD("row_count", K_ROW_COUNT, UNRESERVED_KEYWORD)
PG_KEYWORD("rowtype", K_ROWTYPE, UNRESERVED_KEYWORD)
PG_KEYWORD("schema", K_SCHEMA, UNRESERVED_KEYWORD)
PLPGSQL_STMT_OPEN,
PLPGSQL_STMT_FETCH,
PLPGSQL_STMT_CLOSE,
- PLPGSQL_STMT_PERFORM
+ PLPGSQL_STMT_PERFORM,
+ PLPGSQL_STMT_COMMIT,
+ PLPGSQL_STMT_ROLLBACK
} PLpgSQL_stmt_type;
/*
PLpgSQL_expr *expr;
} PLpgSQL_stmt_perform;
+/*
+ * COMMIT statement
+ */
+typedef struct PLpgSQL_stmt_commit
+{
+ PLpgSQL_stmt_type cmd_type;
+ int lineno;
+} PLpgSQL_stmt_commit;
+
+/*
+ * ROLLBACK statement
+ */
+typedef struct PLpgSQL_stmt_rollback
+{
+ PLpgSQL_stmt_type cmd_type;
+ int lineno;
+} PLpgSQL_stmt_rollback;
+
/*
* GET DIAGNOSTICS item
*/
--- /dev/null
+CREATE TABLE test1 (a int, b text);
+
+
+CREATE PROCEDURE transaction_test1()
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ FOR i IN 0..9 LOOP
+ INSERT INTO test1 (a) VALUES (i);
+ IF i % 2 = 0 THEN
+ COMMIT;
+ ELSE
+ ROLLBACK;
+ END IF;
+ END LOOP;
+END
+$$;
+
+CALL transaction_test1();
+
+SELECT * FROM test1;
+
+
+TRUNCATE test1;
+
+DO
+LANGUAGE plpgsql
+$$
+BEGIN
+ FOR i IN 0..9 LOOP
+ INSERT INTO test1 (a) VALUES (i);
+ IF i % 2 = 0 THEN
+ COMMIT;
+ ELSE
+ ROLLBACK;
+ END IF;
+ END LOOP;
+END
+$$;
+
+SELECT * FROM test1;
+
+
+-- transaction commands not allowed when called in transaction block
+START TRANSACTION;
+CALL transaction_test1();
+COMMIT;
+
+START TRANSACTION;
+DO LANGUAGE plpgsql $$ BEGIN COMMIT; END $$;
+COMMIT;
+
+
+TRUNCATE test1;
+
+-- not allowed in a function
+CREATE FUNCTION transaction_test2() RETURNS int
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ FOR i IN 0..9 LOOP
+ INSERT INTO test1 (a) VALUES (i);
+ IF i % 2 = 0 THEN
+ COMMIT;
+ ELSE
+ ROLLBACK;
+ END IF;
+ END LOOP;
+ RETURN 1;
+END
+$$;
+
+SELECT transaction_test2();
+
+SELECT * FROM test1;
+
+
+-- also not allowed if procedure is called from a function
+CREATE FUNCTION transaction_test3() RETURNS int
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ CALL transaction_test1();
+ RETURN 1;
+END;
+$$;
+
+SELECT transaction_test3();
+
+SELECT * FROM test1;
+
+
+-- DO block inside function
+CREATE FUNCTION transaction_test4() RETURNS int
+LANGUAGE plpgsql
+AS $$
+BEGIN
+ EXECUTE 'DO LANGUAGE plpgsql $x$ BEGIN COMMIT; END $x$';
+ RETURN 1;
+END;
+$$;
+
+SELECT transaction_test4();
+
+
+-- proconfig settings currently disallow transaction statements
+CREATE PROCEDURE transaction_test5()
+LANGUAGE plpgsql
+SET work_mem = 555
+AS $$
+BEGIN
+ COMMIT;
+END;
+$$;
+
+CALL transaction_test5();
+
+
+-- commit inside cursor loop
+CREATE TABLE test2 (x int);
+INSERT INTO test2 VALUES (0), (1), (2), (3), (4);
+
+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);
+ COMMIT;
+ END LOOP;
+END;
+$$;
+
+SELECT * FROM test1;
+
+
+-- 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);
+ ROLLBACK;
+ END LOOP;
+END;
+$$;
+
+SELECT * FROM test1;
+
+
+-- commit inside block with exception handler
+TRUNCATE test1;
+
+DO LANGUAGE plpgsql $$
+BEGIN
+ BEGIN
+ INSERT INTO test1 (a) VALUES (1);
+ COMMIT;
+ INSERT INTO test1 (a) VALUES (1/0);
+ COMMIT;
+ EXCEPTION
+ WHEN division_by_zero THEN
+ RAISE NOTICE 'caught division_by_zero';
+ END;
+END;
+$$;
+
+SELECT * FROM test1;
+
+
+-- rollback inside block with exception handler
+TRUNCATE test1;
+
+DO LANGUAGE plpgsql $$
+BEGIN
+ BEGIN
+ INSERT INTO test1 (a) VALUES (1);
+ ROLLBACK;
+ INSERT INTO test1 (a) VALUES (1/0);
+ ROLLBACK;
+ EXCEPTION
+ WHEN division_by_zero THEN
+ RAISE NOTICE 'caught division_by_zero';
+ END;
+END;
+$$;
+
+SELECT * FROM test1;
+
+
+-- COMMIT failures
+DO LANGUAGE plpgsql $$
+BEGIN
+ CREATE TABLE test3 (y int UNIQUE DEFERRABLE INITIALLY DEFERRED);
+ COMMIT;
+ INSERT INTO test3 (y) VALUES (1);
+ COMMIT;
+ INSERT INTO test3 (y) VALUES (1);
+ INSERT INTO test3 (y) VALUES (2);
+ COMMIT;
+ INSERT INTO test3 (y) VALUES (3); -- won't get here
+END;
+$$;
+
+SELECT * FROM test3;
+
+
+DROP TABLE test1;
+DROP TABLE test2;
+DROP TABLE test3;
plpython_quote \
plpython_composite \
plpython_subtransaction \
+ plpython_transaction \
plpython_drop
REGRESS_PLPYTHON3_MANGLE := $(REGRESS)
Error
Fatal
SPIError
+ commit
cursor
debug
error
quote_ident
quote_literal
quote_nullable
+ rollback
spiexceptions
subtransaction
warning
-(18 rows)
+(20 rows)
CREATE FUNCTION elog_test_basic() RETURNS void
AS $$
--- /dev/null
+CREATE TABLE test1 (a int, b text);
+CREATE PROCEDURE transaction_test1()
+LANGUAGE plpythonu
+AS $$
+for i in range(0, 10):
+ plpy.execute("INSERT INTO test1 (a) VALUES (%d)" % i)
+ if i % 2 == 0:
+ plpy.commit()
+ else:
+ plpy.rollback()
+$$;
+CALL transaction_test1();
+SELECT * FROM test1;
+ a | b
+---+---
+ 0 |
+ 2 |
+ 4 |
+ 6 |
+ 8 |
+(5 rows)
+
+TRUNCATE test1;
+DO
+LANGUAGE plpythonu
+$$
+for i in range(0, 10):
+ plpy.execute("INSERT INTO test1 (a) VALUES (%d)" % i)
+ if i % 2 == 0:
+ plpy.commit()
+ else:
+ plpy.rollback()
+$$;
+SELECT * FROM test1;
+ a | b
+---+---
+ 0 |
+ 2 |
+ 4 |
+ 6 |
+ 8 |
+(5 rows)
+
+TRUNCATE test1;
+-- not allowed in a function
+CREATE FUNCTION transaction_test2() RETURNS int
+LANGUAGE plpythonu
+AS $$
+for i in range(0, 10):
+ plpy.execute("INSERT INTO test1 (a) VALUES (%d)" % i)
+ if i % 2 == 0:
+ plpy.commit()
+ else:
+ plpy.rollback()
+return 1
+$$;
+SELECT transaction_test2();
+ERROR: invalid transaction termination
+CONTEXT: PL/Python function "transaction_test2"
+SELECT * FROM test1;
+ a | b
+---+---
+(0 rows)
+
+-- also not allowed if procedure is called from a function
+CREATE FUNCTION transaction_test3() RETURNS int
+LANGUAGE plpythonu
+AS $$
+plpy.execute("CALL transaction_test1()")
+return 1
+$$;
+SELECT transaction_test3();
+ERROR: spiexceptions.InvalidTransactionTermination: invalid transaction termination
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "transaction_test3", line 2, in <module>
+ plpy.execute("CALL transaction_test1()")
+PL/Python function "transaction_test3"
+SELECT * FROM test1;
+ a | b
+---+---
+(0 rows)
+
+-- DO block inside function
+CREATE FUNCTION transaction_test4() RETURNS int
+LANGUAGE plpythonu
+AS $$
+plpy.execute("DO LANGUAGE plpythonu $x$ plpy.commit() $x$")
+return 1
+$$;
+SELECT transaction_test4();
+ERROR: spiexceptions.InvalidTransactionTermination: invalid transaction termination
+CONTEXT: Traceback (most recent call last):
+ PL/Python function "transaction_test4", line 2, in <module>
+ plpy.execute("DO LANGUAGE plpythonu $x$ plpy.commit() $x$")
+PL/Python function "transaction_test4"
+-- commit inside subtransaction (prohibited)
+DO LANGUAGE plpythonu $$
+with plpy.subtransaction():
+ plpy.commit()
+$$;
+WARNING: forcibly aborting a subtransaction that has not been exited
+ERROR: cannot commit while a subtransaction is active
+CONTEXT: PL/Python anonymous code block
+-- commit inside cursor loop
+CREATE TABLE test2 (x int);
+INSERT INTO test2 VALUES (0), (1), (2), (3), (4);
+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'])
+ plpy.commit()
+$$;
+ERROR: cannot commit transaction while a cursor is open
+CONTEXT: PL/Python anonymous code block
+SELECT * FROM test1;
+ a | b
+---+---
+(0 rows)
+
+-- 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'])
+ plpy.rollback()
+$$;
+ERROR: cannot abort transaction while a cursor is open
+CONTEXT: PL/Python anonymous code block
+SELECT * FROM test1;
+ a | b
+---+---
+(0 rows)
+
+DROP TABLE test1;
+DROP TABLE test2;
static void plpython_inline_error_callback(void *arg);
static void PLy_init_interp(void);
-static PLyExecutionContext *PLy_push_execution_context(void);
+static PLyExecutionContext *PLy_push_execution_context(bool atomic_context);
static void PLy_pop_execution_context(void);
/* static state for Python library conflict detection */
Datum
plpython_call_handler(PG_FUNCTION_ARGS)
{
+ bool nonatomic;
Datum retval;
PLyExecutionContext *exec_ctx;
ErrorContextCallback plerrcontext;
PLy_initialize();
+ nonatomic = fcinfo->context &&
+ IsA(fcinfo->context, CallContext) &&
+ !castNode(CallContext, fcinfo->context)->atomic;
+
/* Note: SPI_finish() happens in plpy_exec.c, which is dubious design */
- if (SPI_connect() != SPI_OK_CONNECT)
+ if (SPI_connect_ext(nonatomic ? SPI_OPT_NONATOMIC : 0) != SPI_OK_CONNECT)
elog(ERROR, "SPI_connect failed");
/*
* here and the PG_TRY. (plpython_error_callback expects the stack entry
* to be there, so we have to make the context first.)
*/
- exec_ctx = PLy_push_execution_context();
+ exec_ctx = PLy_push_execution_context(!nonatomic);
/*
* Setup error traceback support for ereport()
PLy_initialize();
/* Note: SPI_finish() happens in plpy_exec.c, which is dubious design */
- if (SPI_connect() != SPI_OK_CONNECT)
+ if (SPI_connect_ext(codeblock->atomic ? 0 : SPI_OPT_NONATOMIC) != SPI_OK_CONNECT)
elog(ERROR, "SPI_connect failed");
MemSet(&fake_fcinfo, 0, sizeof(fake_fcinfo));
* need the stack entry, but for consistency with plpython_call_handler we
* do it in this order.)
*/
- exec_ctx = PLy_push_execution_context();
+ exec_ctx = PLy_push_execution_context(codeblock->atomic);
/*
* Setup error traceback support for ereport()
}
static PLyExecutionContext *
-PLy_push_execution_context(void)
+PLy_push_execution_context(bool atomic_context)
{
PLyExecutionContext *context;
+ /* Pick a memory context similar to what SPI uses. */
context = (PLyExecutionContext *)
- MemoryContextAlloc(TopTransactionContext, sizeof(PLyExecutionContext));
+ MemoryContextAlloc(atomic_context ? TopTransactionContext : PortalContext,
+ sizeof(PLyExecutionContext));
context->curr_proc = NULL;
context->scratch_ctx = NULL;
context->next = PLy_execution_contexts;
#include "postgres.h"
+#include "access/xact.h"
#include "mb/pg_wchar.h"
#include "utils/builtins.h"
+#include "utils/snapmgr.h"
#include "plpython.h"
#include "plpy_cursorobject.h"
#include "plpy_elog.h"
+#include "plpy_main.h"
#include "plpy_planobject.h"
#include "plpy_resultobject.h"
#include "plpy_spi.h"
static PyObject *PLy_quote_literal(PyObject *self, PyObject *args);
static PyObject *PLy_quote_nullable(PyObject *self, PyObject *args);
static PyObject *PLy_quote_ident(PyObject *self, PyObject *args);
+static PyObject *PLy_commit(PyObject *self, PyObject *args);
+static PyObject *PLy_rollback(PyObject *self, PyObject *args);
/* A list of all known exceptions, generated from backend/utils/errcodes.txt */
*/
{"cursor", PLy_cursor, METH_VARARGS, NULL},
+ /*
+ * transaction control
+ */
+ {"commit", PLy_commit, METH_NOARGS, NULL},
+ {"rollback", PLy_rollback, METH_NOARGS, NULL},
+
{NULL, NULL, 0, NULL}
};
*/
Py_RETURN_NONE;
}
+
+static PyObject *
+PLy_commit(PyObject *self, PyObject *args)
+{
+ 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")));
+
+ SPI_commit();
+ SPI_start_transaction();
+
+ /* was cleared at transaction end, reset pointer */
+ exec_ctx->scratch_ctx = NULL;
+
+ Py_RETURN_NONE;
+}
+
+static PyObject *
+PLy_rollback(PyObject *self, PyObject *args)
+{
+ 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")));
+
+ SPI_rollback();
+ SPI_start_transaction();
+
+ /* was cleared at transaction end, reset pointer */
+ exec_ctx->scratch_ctx = NULL;
+
+ Py_RETURN_NONE;
+}
--- /dev/null
+CREATE TABLE test1 (a int, b text);
+
+
+CREATE PROCEDURE transaction_test1()
+LANGUAGE plpythonu
+AS $$
+for i in range(0, 10):
+ plpy.execute("INSERT INTO test1 (a) VALUES (%d)" % i)
+ if i % 2 == 0:
+ plpy.commit()
+ else:
+ plpy.rollback()
+$$;
+
+CALL transaction_test1();
+
+SELECT * FROM test1;
+
+
+TRUNCATE test1;
+
+DO
+LANGUAGE plpythonu
+$$
+for i in range(0, 10):
+ plpy.execute("INSERT INTO test1 (a) VALUES (%d)" % i)
+ if i % 2 == 0:
+ plpy.commit()
+ else:
+ plpy.rollback()
+$$;
+
+SELECT * FROM test1;
+
+
+TRUNCATE test1;
+
+-- not allowed in a function
+CREATE FUNCTION transaction_test2() RETURNS int
+LANGUAGE plpythonu
+AS $$
+for i in range(0, 10):
+ plpy.execute("INSERT INTO test1 (a) VALUES (%d)" % i)
+ if i % 2 == 0:
+ plpy.commit()
+ else:
+ plpy.rollback()
+return 1
+$$;
+
+SELECT transaction_test2();
+
+SELECT * FROM test1;
+
+
+-- also not allowed if procedure is called from a function
+CREATE FUNCTION transaction_test3() RETURNS int
+LANGUAGE plpythonu
+AS $$
+plpy.execute("CALL transaction_test1()")
+return 1
+$$;
+
+SELECT transaction_test3();
+
+SELECT * FROM test1;
+
+
+-- DO block inside function
+CREATE FUNCTION transaction_test4() RETURNS int
+LANGUAGE plpythonu
+AS $$
+plpy.execute("DO LANGUAGE plpythonu $x$ plpy.commit() $x$")
+return 1
+$$;
+
+SELECT transaction_test4();
+
+
+-- commit inside subtransaction (prohibited)
+DO LANGUAGE plpythonu $$
+with plpy.subtransaction():
+ plpy.commit()
+$$;
+
+
+-- commit inside cursor loop
+CREATE TABLE test2 (x int);
+INSERT INTO test2 VALUES (0), (1), (2), (3), (4);
+
+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'])
+ plpy.commit()
+$$;
+
+SELECT * FROM test1;
+
+
+-- 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'])
+ plpy.rollback()
+$$;
+
+SELECT * FROM test1;
+
+
+DROP TABLE test1;
+DROP TABLE test2;
pltclu.control pltclu--1.0.sql pltclu--unpackaged--1.0.sql
REGRESS_OPTS = --dbname=$(PL_TESTDB) --load-extension=pltcl
-REGRESS = pltcl_setup pltcl_queries pltcl_call pltcl_start_proc pltcl_subxact pltcl_unicode
+REGRESS = pltcl_setup pltcl_queries pltcl_call pltcl_start_proc pltcl_subxact pltcl_unicode pltcl_transaction
# Tcl on win32 ships with import libraries only for Microsoft Visual C++,
# which are not compatible with mingw gcc. Therefore we need to build a
--- /dev/null
+-- suppress CONTEXT so that function OIDs aren't in output
+\set VERBOSITY terse
+CREATE TABLE test1 (a int, b text);
+CREATE PROCEDURE transaction_test1()
+LANGUAGE pltcl
+AS $$
+for {set i 0} {$i < 10} {incr i} {
+ spi_exec "INSERT INTO test1 (a) VALUES ($i)"
+ if {$i % 2 == 0} {
+ commit
+ } else {
+ rollback
+ }
+}
+$$;
+CALL transaction_test1();
+SELECT * FROM test1;
+ a | b
+---+---
+ 0 |
+ 2 |
+ 4 |
+ 6 |
+ 8 |
+(5 rows)
+
+TRUNCATE test1;
+-- not allowed in a function
+CREATE FUNCTION transaction_test2() RETURNS int
+LANGUAGE pltcl
+AS $$
+for {set i 0} {$i < 10} {incr i} {
+ spi_exec "INSERT INTO test1 (a) VALUES ($i)"
+ if {$i % 2 == 0} {
+ commit
+ } else {
+ rollback
+ }
+}
+return 1
+$$;
+SELECT transaction_test2();
+ERROR: invalid transaction termination
+SELECT * FROM test1;
+ a | b
+---+---
+(0 rows)
+
+-- also not allowed if procedure is called from a function
+CREATE FUNCTION transaction_test3() RETURNS int
+LANGUAGE pltcl
+AS $$
+spi_exec "CALL transaction_test1()"
+return 1
+$$;
+SELECT transaction_test3();
+ERROR: invalid transaction termination
+SELECT * FROM test1;
+ a | b
+---+---
+(0 rows)
+
+-- commit inside cursor loop
+CREATE TABLE test2 (x int);
+INSERT INTO test2 VALUES (0), (1), (2), (3), (4);
+TRUNCATE test1;
+CREATE PROCEDURE transaction_test4a()
+LANGUAGE pltcl
+AS $$
+spi_exec -array row "SELECT * FROM test2 ORDER BY x" {
+ spi_exec "INSERT INTO test1 (a) VALUES ($row(x))"
+ commit
+}
+$$;
+CALL transaction_test4a();
+ERROR: cannot commit while a subtransaction is active
+SELECT * FROM test1;
+ a | b
+---+---
+(0 rows)
+
+-- rollback inside cursor loop
+TRUNCATE test1;
+CREATE PROCEDURE transaction_test4b()
+LANGUAGE pltcl
+AS $$
+spi_exec -array row "SELECT * FROM test2 ORDER BY x" {
+ spi_exec "INSERT INTO test1 (a) VALUES ($row(x))"
+ rollback
+}
+$$;
+CALL transaction_test4b();
+ERROR: cannot roll back while a subtransaction is active
+SELECT * FROM test1;
+ a | b
+---+---
+(0 rows)
+
+DROP TABLE test1;
+DROP TABLE test2;
int objc, Tcl_Obj *const objv[]);
static int pltcl_subtransaction(ClientData cdata, Tcl_Interp *interp,
int objc, Tcl_Obj *const objv[]);
+static int pltcl_commit(ClientData cdata, Tcl_Interp *interp,
+ int objc, Tcl_Obj *const objv[]);
+static int pltcl_rollback(ClientData cdata, Tcl_Interp *interp,
+ int objc, Tcl_Obj *const objv[]);
static void pltcl_subtrans_begin(MemoryContext oldcontext,
ResourceOwner oldowner);
pltcl_SPI_lastoid, NULL, NULL);
Tcl_CreateObjCommand(interp, "subtransaction",
pltcl_subtransaction, NULL, NULL);
+ Tcl_CreateObjCommand(interp, "commit",
+ pltcl_commit, NULL, NULL);
+ Tcl_CreateObjCommand(interp, "rollback",
+ pltcl_rollback, NULL, NULL);
/************************************************************
* Call the appropriate start_proc, if there is one.
pltcl_func_handler(PG_FUNCTION_ARGS, pltcl_call_state *call_state,
bool pltrusted)
{
+ bool nonatomic;
pltcl_proc_desc *prodesc;
Tcl_Interp *volatile interp;
Tcl_Obj *tcl_cmd;
int tcl_rc;
Datum retval;
+ nonatomic = fcinfo->context &&
+ IsA(fcinfo->context, CallContext) &&
+ !castNode(CallContext, fcinfo->context)->atomic;
+
/* Connect to SPI manager */
- if (SPI_connect() != SPI_OK_CONNECT)
+ if (SPI_connect_ext(nonatomic ? SPI_OPT_NONATOMIC : 0) != SPI_OK_CONNECT)
elog(ERROR, "could not connect to SPI manager");
/* Find or compile the function */
}
+/**********************************************************************
+ * pltcl_commit()
+ *
+ * Commit the transaction and start a new one.
+ **********************************************************************/
+static int
+pltcl_commit(ClientData cdata, Tcl_Interp *interp,
+ int objc, Tcl_Obj *const objv[])
+{
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ PG_TRY();
+ {
+ SPI_commit();
+ SPI_start_transaction();
+ }
+ PG_CATCH();
+ {
+ ErrorData *edata;
+
+ /* Save error info */
+ MemoryContextSwitchTo(oldcontext);
+ edata = CopyErrorData();
+ FlushErrorState();
+
+ /* Pass the error data to Tcl */
+ pltcl_construct_errorCode(interp, edata);
+ UTF_BEGIN;
+ Tcl_SetObjResult(interp, Tcl_NewStringObj(UTF_E2U(edata->message), -1));
+ UTF_END;
+ FreeErrorData(edata);
+
+ return TCL_ERROR;
+ }
+ PG_END_TRY();
+
+ return TCL_OK;
+}
+
+
+/**********************************************************************
+ * pltcl_rollback()
+ *
+ * Abort the transaction and start a new one.
+ **********************************************************************/
+static int
+pltcl_rollback(ClientData cdata, Tcl_Interp *interp,
+ int objc, Tcl_Obj *const objv[])
+{
+ MemoryContext oldcontext = CurrentMemoryContext;
+
+ PG_TRY();
+ {
+ SPI_rollback();
+ SPI_start_transaction();
+ }
+ PG_CATCH();
+ {
+ ErrorData *edata;
+
+ /* Save error info */
+ MemoryContextSwitchTo(oldcontext);
+ edata = CopyErrorData();
+ FlushErrorState();
+
+ /* Pass the error data to Tcl */
+ pltcl_construct_errorCode(interp, edata);
+ UTF_BEGIN;
+ Tcl_SetObjResult(interp, Tcl_NewStringObj(UTF_E2U(edata->message), -1));
+ UTF_END;
+ FreeErrorData(edata);
+
+ return TCL_ERROR;
+ }
+ PG_END_TRY();
+
+ return TCL_OK;
+}
+
+
/**********************************************************************
* pltcl_set_tuple_values() - Set variables for all attributes
* of a given tuple
--- /dev/null
+-- suppress CONTEXT so that function OIDs aren't in output
+\set VERBOSITY terse
+
+CREATE TABLE test1 (a int, b text);
+
+
+CREATE PROCEDURE transaction_test1()
+LANGUAGE pltcl
+AS $$
+for {set i 0} {$i < 10} {incr i} {
+ spi_exec "INSERT INTO test1 (a) VALUES ($i)"
+ if {$i % 2 == 0} {
+ commit
+ } else {
+ rollback
+ }
+}
+$$;
+
+CALL transaction_test1();
+
+SELECT * FROM test1;
+
+
+TRUNCATE test1;
+
+-- not allowed in a function
+CREATE FUNCTION transaction_test2() RETURNS int
+LANGUAGE pltcl
+AS $$
+for {set i 0} {$i < 10} {incr i} {
+ spi_exec "INSERT INTO test1 (a) VALUES ($i)"
+ if {$i % 2 == 0} {
+ commit
+ } else {
+ rollback
+ }
+}
+return 1
+$$;
+
+SELECT transaction_test2();
+
+SELECT * FROM test1;
+
+
+-- also not allowed if procedure is called from a function
+CREATE FUNCTION transaction_test3() RETURNS int
+LANGUAGE pltcl
+AS $$
+spi_exec "CALL transaction_test1()"
+return 1
+$$;
+
+SELECT transaction_test3();
+
+SELECT * FROM test1;
+
+
+-- commit inside cursor loop
+CREATE TABLE test2 (x int);
+INSERT INTO test2 VALUES (0), (1), (2), (3), (4);
+
+TRUNCATE test1;
+
+CREATE PROCEDURE transaction_test4a()
+LANGUAGE pltcl
+AS $$
+spi_exec -array row "SELECT * FROM test2 ORDER BY x" {
+ spi_exec "INSERT INTO test1 (a) VALUES ($row(x))"
+ commit
+}
+$$;
+
+CALL transaction_test4a();
+
+SELECT * FROM test1;
+
+
+-- rollback inside cursor loop
+TRUNCATE test1;
+
+CREATE PROCEDURE transaction_test4b()
+LANGUAGE pltcl
+AS $$
+spi_exec -array row "SELECT * FROM test2 ORDER BY x" {
+ spi_exec "INSERT INTO test1 (a) VALUES ($row(x))"
+ rollback
+}
+$$;
+
+CALL transaction_test4b();
+
+SELECT * FROM test1;
+
+
+DROP TABLE test1;
+DROP TABLE test2;