Wrap PL/Python SPI calls into subtransactions
authorPeter Eisentraut <[email protected]>
Wed, 2 Feb 2011 20:06:10 +0000 (22:06 +0200)
committerPeter Eisentraut <[email protected]>
Wed, 2 Feb 2011 20:06:10 +0000 (22:06 +0200)
This allows the language-specific try/catch construct to catch and
handle exceptions arising from SPI calls, matching the behavior of
other PLs.

As an additional bonus you no longer get all the ugly "unrecognized
error in PLy_spi_execute_query" errors.

Jan UrbaƄski, reviewed by Steve Singer

doc/src/sgml/plpython.sgml
src/pl/plpython/expected/plpython_error.out
src/pl/plpython/expected/plpython_error_0.out
src/pl/plpython/plpython.c
src/pl/plpython/sql/plpython_error.sql

index befd6afb15e812d52aa5602271dce4e20a38df38..e05c2937b1f9fe68963f68a625f061ce1b895ff8 100644 (file)
@@ -858,6 +858,9 @@ $$ LANGUAGE plpythonu;
    <literal>plpy.<replaceable>foo</replaceable></literal>.
   </para>
 
+  <sect2>
+    <title>Database Access Functions</title>
+
   <para>
    The <literal>plpy</literal> module provides two
    functions called <function>execute</function> and
@@ -937,6 +940,33 @@ CREATE FUNCTION usesavedplan() RETURNS trigger AS $$
 $$ LANGUAGE plpythonu;
 </programlisting>
   </para>
+
+  </sect2>
+
+  <sect2>
+   <title>Trapping Errors</title>
+
+   <para>
+    Functions accessing the database might encounter errors, which
+    will cause them to abort and raise an exception.  Both
+    <function>plpy.execute</function> and
+    <function>plpy.prepare</function> can raise an instance of
+    <literal>plpy.SPIError</literal>, which by default will terminate
+    the function.  This error can be handled just like any other
+    Python exception, by using the <literal>try/except</literal>
+    construct.  For example:
+<programlisting>
+CREATE FUNCTION try_adding_joe() RETURNS text AS $$
+    try:
+        plpy.execute("INSERT INTO users(username) VALUES ('joe')")
+    except plpy.SPIError:
+        return "something went wrong"
+    else:
+        return "Joe added"
+$$ LANGUAGE plpythonu;
+</programlisting>
+   </para>
+  </sect2>
  </sect1>
 
  <sect1 id="plpython-util">
index 2b6141c376ee9d58787f9015148195a599eadb37..7597ca73f127a3e7434874b2c3b284302ad1dde6 100644 (file)
@@ -32,8 +32,6 @@ CREATE FUNCTION sql_syntax_error() RETURNS text
 'plpy.execute("syntax error")'
         LANGUAGE plpythonu;
 SELECT sql_syntax_error();
-WARNING:  plpy.SPIError: unrecognized error in PLy_spi_execute_query
-CONTEXT:  PL/Python function "sql_syntax_error"
 ERROR:  plpy.SPIError: syntax error at or near "syntax"
 LINE 1: syntax error
         ^
@@ -56,8 +54,6 @@ CREATE FUNCTION exception_index_invalid_nested() RETURNS text
 return rv[0]'
    LANGUAGE plpythonu;
 SELECT exception_index_invalid_nested();
-WARNING:  plpy.SPIError: unrecognized error in PLy_spi_execute_query
-CONTEXT:  PL/Python function "exception_index_invalid_nested"
 ERROR:  plpy.SPIError: function test5(unknown) does not exist
 LINE 1: SELECT test5('foo')
                ^
@@ -78,8 +74,6 @@ return None
 '
    LANGUAGE plpythonu;
 SELECT invalid_type_uncaught('rick');
-WARNING:  plpy.SPIError: unrecognized error in PLy_spi_prepare
-CONTEXT:  PL/Python function "invalid_type_uncaught"
 ERROR:  plpy.SPIError: type "test" does not exist
 CONTEXT:  PL/Python function "invalid_type_uncaught"
 /* for what it's worth catch the exception generated by
@@ -101,8 +95,6 @@ return None
 '
    LANGUAGE plpythonu;
 SELECT invalid_type_caught('rick');
-WARNING:  plpy.SPIError: unrecognized error in PLy_spi_prepare
-CONTEXT:  PL/Python function "invalid_type_caught"
 NOTICE:  type "test" does not exist
 CONTEXT:  PL/Python function "invalid_type_caught"
  invalid_type_caught 
@@ -128,8 +120,6 @@ return None
 '
    LANGUAGE plpythonu;
 SELECT invalid_type_reraised('rick');
-WARNING:  plpy.SPIError: unrecognized error in PLy_spi_prepare
-CONTEXT:  PL/Python function "invalid_type_reraised"
 ERROR:  plpy.Error: type "test" does not exist
 CONTEXT:  PL/Python function "invalid_type_reraised"
 /* no typo no messing about
@@ -150,3 +140,25 @@ SELECT valid_type('rick');
  
 (1 row)
 
+/* manually starting subtransactions - a bad idea
+ */
+CREATE FUNCTION manual_subxact() RETURNS void AS $$
+plpy.execute("savepoint save")
+plpy.execute("create table foo(x integer)")
+plpy.execute("rollback to save")
+$$ LANGUAGE plpythonu;
+SELECT manual_subxact();
+ERROR:  plpy.SPIError: SPI_execute failed: SPI_ERROR_TRANSACTION
+CONTEXT:  PL/Python function "manual_subxact"
+/* same for prepared plans
+ */
+CREATE FUNCTION manual_subxact_prepared() RETURNS void AS $$
+save = plpy.prepare("savepoint save")
+rollback = plpy.prepare("rollback to save")
+plpy.execute(save)
+plpy.execute("create table foo(x integer)")
+plpy.execute(rollback)
+$$ LANGUAGE plpythonu;
+SELECT manual_subxact_prepared();
+ERROR:  plpy.SPIError: SPI_execute_plan failed: SPI_ERROR_TRANSACTION
+CONTEXT:  PL/Python function "manual_subxact_prepared"
index 3842d8fb4a1acfeda27a24eb926425eeab03ca08..42e41196306339de681c452b45365f7140781965 100644 (file)
@@ -32,8 +32,6 @@ CREATE FUNCTION sql_syntax_error() RETURNS text
 'plpy.execute("syntax error")'
         LANGUAGE plpythonu;
 SELECT sql_syntax_error();
-WARNING:  plpy.SPIError: unrecognized error in PLy_spi_execute_query
-CONTEXT:  PL/Python function "sql_syntax_error"
 ERROR:  plpy.SPIError: syntax error at or near "syntax"
 LINE 1: syntax error
         ^
@@ -56,8 +54,6 @@ CREATE FUNCTION exception_index_invalid_nested() RETURNS text
 return rv[0]'
    LANGUAGE plpythonu;
 SELECT exception_index_invalid_nested();
-WARNING:  plpy.SPIError: unrecognized error in PLy_spi_execute_query
-CONTEXT:  PL/Python function "exception_index_invalid_nested"
 ERROR:  plpy.SPIError: function test5(unknown) does not exist
 LINE 1: SELECT test5('foo')
                ^
@@ -78,8 +74,6 @@ return None
 '
    LANGUAGE plpythonu;
 SELECT invalid_type_uncaught('rick');
-WARNING:  plpy.SPIError: unrecognized error in PLy_spi_prepare
-CONTEXT:  PL/Python function "invalid_type_uncaught"
 ERROR:  plpy.SPIError: type "test" does not exist
 CONTEXT:  PL/Python function "invalid_type_uncaught"
 /* for what it's worth catch the exception generated by
@@ -101,8 +95,6 @@ return None
 '
    LANGUAGE plpythonu;
 SELECT invalid_type_caught('rick');
-WARNING:  plpy.SPIError: unrecognized error in PLy_spi_prepare
-CONTEXT:  PL/Python function "invalid_type_caught"
 NOTICE:  type "test" does not exist
 CONTEXT:  PL/Python function "invalid_type_caught"
  invalid_type_caught 
@@ -128,8 +120,6 @@ return None
 '
    LANGUAGE plpythonu;
 SELECT invalid_type_reraised('rick');
-WARNING:  plpy.SPIError: unrecognized error in PLy_spi_prepare
-CONTEXT:  PL/Python function "invalid_type_reraised"
 ERROR:  plpy.Error: type "test" does not exist
 CONTEXT:  PL/Python function "invalid_type_reraised"
 /* no typo no messing about
@@ -150,3 +140,25 @@ SELECT valid_type('rick');
  
 (1 row)
 
+/* manually starting subtransactions - a bad idea
+ */
+CREATE FUNCTION manual_subxact() RETURNS void AS $$
+plpy.execute("savepoint save")
+plpy.execute("create table foo(x integer)")
+plpy.execute("rollback to save")
+$$ LANGUAGE plpythonu;
+SELECT manual_subxact();
+ERROR:  plpy.SPIError: SPI_execute failed: SPI_ERROR_TRANSACTION
+CONTEXT:  PL/Python function "manual_subxact"
+/* same for prepared plans
+ */
+CREATE FUNCTION manual_subxact_prepared() RETURNS void AS $$
+save = plpy.prepare("savepoint save")
+rollback = plpy.prepare("rollback to save")
+plpy.execute(save)
+plpy.execute("create table foo(x integer)")
+plpy.execute(rollback)
+$$ LANGUAGE plpythonu;
+SELECT manual_subxact_prepared();
+ERROR:  plpy.SPIError: SPI_execute_plan failed: SPI_ERROR_TRANSACTION
+CONTEXT:  PL/Python function "manual_subxact_prepared"
index fbfeb2c9f16029cc682f4c557e4ba2990fbc06c3..fff7de767435e1204f7c0c21339950999d9a131b 100644 (file)
@@ -101,6 +101,7 @@ typedef int Py_ssize_t;
 #include "nodes/makefuncs.h"
 #include "parser/parse_type.h"
 #include "tcop/tcopprot.h"
+#include "access/xact.h"
 #include "utils/builtins.h"
 #include "utils/hsearch.h"
 #include "utils/lsyscache.h"
@@ -2856,6 +2857,7 @@ PLy_spi_prepare(PyObject *self, PyObject *args)
    char       *query;
    void       *tmpplan;
    volatile MemoryContext oldcontext;
+   volatile ResourceOwner oldowner;
    int         nargs;
 
    if (!PyArg_ParseTuple(args, "s|O", &query, &list))
@@ -2879,6 +2881,11 @@ PLy_spi_prepare(PyObject *self, PyObject *args)
    plan->args = nargs ? PLy_malloc(sizeof(PLyTypeInfo) * nargs) : NULL;
 
    oldcontext = CurrentMemoryContext;
+   oldowner = CurrentResourceOwner;
+
+   BeginInternalSubTransaction(NULL);
+   MemoryContextSwitchTo(oldcontext);
+
    PG_TRY();
    {
        int i;
@@ -2958,20 +2965,42 @@ PLy_spi_prepare(PyObject *self, PyObject *args)
        if (plan->plan == NULL)
            elog(ERROR, "SPI_saveplan failed: %s",
                 SPI_result_code_string(SPI_result));
+
+       /* Commit the inner transaction, return to outer xact context */
+       ReleaseCurrentSubTransaction();
+       MemoryContextSwitchTo(oldcontext);
+       CurrentResourceOwner = oldowner;
+
+       /*
+        * AtEOSubXact_SPI() should not have popped any SPI context, but just
+        * in case it did, make sure we remain connected.
+        */
+       SPI_restore_connection();
    }
    PG_CATCH();
    {
        ErrorData   *edata;
 
+       /* Save error info */
        MemoryContextSwitchTo(oldcontext);
        edata = CopyErrorData();
        FlushErrorState();
        Py_DECREF(plan);
        Py_XDECREF(optr);
-       if (!PyErr_Occurred())
-           PLy_exception_set(PLy_exc_spi_error,
-                             "unrecognized error in PLy_spi_prepare");
-       PLy_elog(WARNING, NULL);
+
+       /* Abort the inner transaction */
+       RollbackAndReleaseCurrentSubTransaction();
+       MemoryContextSwitchTo(oldcontext);
+       CurrentResourceOwner = oldowner;
+
+       /*
+        * If AtEOSubXact_SPI() popped any SPI context of the subxact, it
+        * will have left us in a disconnected state.  We need this hack to
+        * return to connected state.
+        */
+       SPI_restore_connection();
+
+       /* Make Python raise the exception */
        PLy_spi_exception_set(edata);
        return NULL;
    }
@@ -3013,6 +3042,7 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit)
                rv;
    PLyPlanObject *plan;
    volatile MemoryContext oldcontext;
+   volatile ResourceOwner oldowner;
    PyObject   *ret;
 
    if (list != NULL)
@@ -3048,6 +3078,12 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit)
    }
 
    oldcontext = CurrentMemoryContext;
+   oldowner = CurrentResourceOwner;
+
+   BeginInternalSubTransaction(NULL);
+   /* Want to run inside function's memory context */
+   MemoryContextSwitchTo(oldcontext);
+
    PG_TRY();
    {
        char       *nulls;
@@ -3100,12 +3136,24 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit)
 
        if (nargs > 0)
            pfree(nulls);
+
+       /* Commit the inner transaction, return to outer xact context */
+       ReleaseCurrentSubTransaction();
+       MemoryContextSwitchTo(oldcontext);
+       CurrentResourceOwner = oldowner;
+
+       /*
+        * AtEOSubXact_SPI() should not have popped any SPI context, but just
+        * in case it did, make sure we remain connected.
+        */
+       SPI_restore_connection();
    }
    PG_CATCH();
    {
        int         k;
        ErrorData   *edata;
 
+       /* Save error info */
        MemoryContextSwitchTo(oldcontext);
        edata = CopyErrorData();
        FlushErrorState();
@@ -3123,10 +3171,19 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit)
            }
        }
 
-       if (!PyErr_Occurred())
-           PLy_exception_set(PLy_exc_spi_error,
-                             "unrecognized error in PLy_spi_execute_plan");
-       PLy_elog(WARNING, NULL);
+       /* Abort the inner transaction */
+       RollbackAndReleaseCurrentSubTransaction();
+       MemoryContextSwitchTo(oldcontext);
+       CurrentResourceOwner = oldowner;
+
+       /*
+        * If AtEOSubXact_SPI() popped any SPI context of the subxact, it
+        * will have left us in a disconnected state.  We need this hack to
+        * return to connected state.
+        */
+       SPI_restore_connection();
+
+       /* Make Python raise the exception */
        PLy_spi_exception_set(edata);
        return NULL;
    }
@@ -3142,6 +3199,14 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit)
        }
    }
 
+   if (rv < 0)
+   {
+       PLy_exception_set(PLy_exc_spi_error,
+                         "SPI_execute_plan failed: %s",
+                         SPI_result_code_string(rv));
+       return NULL;
+   }
+
    return ret;
 }
 
@@ -3150,26 +3215,55 @@ PLy_spi_execute_query(char *query, long limit)
 {
    int         rv;
    volatile MemoryContext oldcontext;
+   volatile ResourceOwner oldowner;
    PyObject   *ret;
 
    oldcontext = CurrentMemoryContext;
+   oldowner = CurrentResourceOwner;
+
+   BeginInternalSubTransaction(NULL);
+   /* Want to run inside function's memory context */
+   MemoryContextSwitchTo(oldcontext);
+
    PG_TRY();
    {
        pg_verifymbstr(query, strlen(query), false);
        rv = SPI_execute(query, PLy_curr_procedure->fn_readonly, limit);
        ret = PLy_spi_execute_fetch_result(SPI_tuptable, SPI_processed, rv);
+
+       /* Commit the inner transaction, return to outer xact context */
+       ReleaseCurrentSubTransaction();
+       MemoryContextSwitchTo(oldcontext);
+       CurrentResourceOwner = oldowner;
+
+       /*
+        * AtEOSubXact_SPI() should not have popped any SPI context, but just
+        * in case it did, make sure we remain connected.
+        */
+       SPI_restore_connection();
    }
    PG_CATCH();
    {
        ErrorData   *edata;
 
+       /* Save error info */
        MemoryContextSwitchTo(oldcontext);
        edata = CopyErrorData();
        FlushErrorState();
-       if (!PyErr_Occurred())
-           PLy_exception_set(PLy_exc_spi_error,
-                             "unrecognized error in PLy_spi_execute_query");
-       PLy_elog(WARNING, NULL);
+
+       /* Abort the inner transaction */
+       RollbackAndReleaseCurrentSubTransaction();
+       MemoryContextSwitchTo(oldcontext);
+       CurrentResourceOwner = oldowner;
+
+       /*
+        * If AtEOSubXact_SPI() popped any SPI context of the subxact, it
+        * will have left us in a disconnected state.  We need this hack to
+        * return to connected state.
+        */
+       SPI_restore_connection();
+
+       /* Make Python raise the exception */
        PLy_spi_exception_set(edata);
        return NULL;
    }
index 6509257b24deb355d6599f6ca651544b44694211..7861cd61a2fa675ae50a45e56d4a02f1652355f1 100644 (file)
@@ -130,3 +130,25 @@ return None
    LANGUAGE plpythonu;
 
 SELECT valid_type('rick');
+
+/* manually starting subtransactions - a bad idea
+ */
+CREATE FUNCTION manual_subxact() RETURNS void AS $$
+plpy.execute("savepoint save")
+plpy.execute("create table foo(x integer)")
+plpy.execute("rollback to save")
+$$ LANGUAGE plpythonu;
+
+SELECT manual_subxact();
+
+/* same for prepared plans
+ */
+CREATE FUNCTION manual_subxact_prepared() RETURNS void AS $$
+save = plpy.prepare("savepoint save")
+rollback = plpy.prepare("rollback to save")
+plpy.execute(save)
+plpy.execute("create table foo(x integer)")
+plpy.execute(rollback)
+$$ LANGUAGE plpythonu;
+
+SELECT manual_subxact_prepared();