Commit 0c5933d0 authored by Peter Eisentraut's avatar Peter Eisentraut

Wrap PL/Python SPI calls into subtransactions

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
parent c73fe72e
...@@ -858,6 +858,9 @@ $$ LANGUAGE plpythonu; ...@@ -858,6 +858,9 @@ $$ LANGUAGE plpythonu;
<literal>plpy.<replaceable>foo</replaceable></literal>. <literal>plpy.<replaceable>foo</replaceable></literal>.
</para> </para>
<sect2>
<title>Database Access Functions</title>
<para> <para>
The <literal>plpy</literal> module provides two The <literal>plpy</literal> module provides two
functions called <function>execute</function> and functions called <function>execute</function> and
...@@ -937,6 +940,33 @@ CREATE FUNCTION usesavedplan() RETURNS trigger AS $$ ...@@ -937,6 +940,33 @@ CREATE FUNCTION usesavedplan() RETURNS trigger AS $$
$$ LANGUAGE plpythonu; $$ LANGUAGE plpythonu;
</programlisting> </programlisting>
</para> </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>
<sect1 id="plpython-util"> <sect1 id="plpython-util">
......
...@@ -32,8 +32,6 @@ CREATE FUNCTION sql_syntax_error() RETURNS text ...@@ -32,8 +32,6 @@ CREATE FUNCTION sql_syntax_error() RETURNS text
'plpy.execute("syntax error")' 'plpy.execute("syntax error")'
LANGUAGE plpythonu; LANGUAGE plpythonu;
SELECT sql_syntax_error(); 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" ERROR: plpy.SPIError: syntax error at or near "syntax"
LINE 1: syntax error LINE 1: syntax error
^ ^
...@@ -56,8 +54,6 @@ CREATE FUNCTION exception_index_invalid_nested() RETURNS text ...@@ -56,8 +54,6 @@ CREATE FUNCTION exception_index_invalid_nested() RETURNS text
return rv[0]' return rv[0]'
LANGUAGE plpythonu; LANGUAGE plpythonu;
SELECT exception_index_invalid_nested(); 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 ERROR: plpy.SPIError: function test5(unknown) does not exist
LINE 1: SELECT test5('foo') LINE 1: SELECT test5('foo')
^ ^
...@@ -78,8 +74,6 @@ return None ...@@ -78,8 +74,6 @@ return None
' '
LANGUAGE plpythonu; LANGUAGE plpythonu;
SELECT invalid_type_uncaught('rick'); 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 ERROR: plpy.SPIError: type "test" does not exist
CONTEXT: PL/Python function "invalid_type_uncaught" CONTEXT: PL/Python function "invalid_type_uncaught"
/* for what it's worth catch the exception generated by /* for what it's worth catch the exception generated by
...@@ -101,8 +95,6 @@ return None ...@@ -101,8 +95,6 @@ return None
' '
LANGUAGE plpythonu; LANGUAGE plpythonu;
SELECT invalid_type_caught('rick'); 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 NOTICE: type "test" does not exist
CONTEXT: PL/Python function "invalid_type_caught" CONTEXT: PL/Python function "invalid_type_caught"
invalid_type_caught invalid_type_caught
...@@ -128,8 +120,6 @@ return None ...@@ -128,8 +120,6 @@ return None
' '
LANGUAGE plpythonu; LANGUAGE plpythonu;
SELECT invalid_type_reraised('rick'); 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 ERROR: plpy.Error: type "test" does not exist
CONTEXT: PL/Python function "invalid_type_reraised" CONTEXT: PL/Python function "invalid_type_reraised"
/* no typo no messing about /* no typo no messing about
...@@ -150,3 +140,25 @@ SELECT valid_type('rick'); ...@@ -150,3 +140,25 @@ SELECT valid_type('rick');
(1 row) (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"
...@@ -32,8 +32,6 @@ CREATE FUNCTION sql_syntax_error() RETURNS text ...@@ -32,8 +32,6 @@ CREATE FUNCTION sql_syntax_error() RETURNS text
'plpy.execute("syntax error")' 'plpy.execute("syntax error")'
LANGUAGE plpythonu; LANGUAGE plpythonu;
SELECT sql_syntax_error(); 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" ERROR: plpy.SPIError: syntax error at or near "syntax"
LINE 1: syntax error LINE 1: syntax error
^ ^
...@@ -56,8 +54,6 @@ CREATE FUNCTION exception_index_invalid_nested() RETURNS text ...@@ -56,8 +54,6 @@ CREATE FUNCTION exception_index_invalid_nested() RETURNS text
return rv[0]' return rv[0]'
LANGUAGE plpythonu; LANGUAGE plpythonu;
SELECT exception_index_invalid_nested(); 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 ERROR: plpy.SPIError: function test5(unknown) does not exist
LINE 1: SELECT test5('foo') LINE 1: SELECT test5('foo')
^ ^
...@@ -78,8 +74,6 @@ return None ...@@ -78,8 +74,6 @@ return None
' '
LANGUAGE plpythonu; LANGUAGE plpythonu;
SELECT invalid_type_uncaught('rick'); 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 ERROR: plpy.SPIError: type "test" does not exist
CONTEXT: PL/Python function "invalid_type_uncaught" CONTEXT: PL/Python function "invalid_type_uncaught"
/* for what it's worth catch the exception generated by /* for what it's worth catch the exception generated by
...@@ -101,8 +95,6 @@ return None ...@@ -101,8 +95,6 @@ return None
' '
LANGUAGE plpythonu; LANGUAGE plpythonu;
SELECT invalid_type_caught('rick'); 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 NOTICE: type "test" does not exist
CONTEXT: PL/Python function "invalid_type_caught" CONTEXT: PL/Python function "invalid_type_caught"
invalid_type_caught invalid_type_caught
...@@ -128,8 +120,6 @@ return None ...@@ -128,8 +120,6 @@ return None
' '
LANGUAGE plpythonu; LANGUAGE plpythonu;
SELECT invalid_type_reraised('rick'); 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 ERROR: plpy.Error: type "test" does not exist
CONTEXT: PL/Python function "invalid_type_reraised" CONTEXT: PL/Python function "invalid_type_reraised"
/* no typo no messing about /* no typo no messing about
...@@ -150,3 +140,25 @@ SELECT valid_type('rick'); ...@@ -150,3 +140,25 @@ SELECT valid_type('rick');
(1 row) (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"
...@@ -101,6 +101,7 @@ typedef int Py_ssize_t; ...@@ -101,6 +101,7 @@ typedef int Py_ssize_t;
#include "nodes/makefuncs.h" #include "nodes/makefuncs.h"
#include "parser/parse_type.h" #include "parser/parse_type.h"
#include "tcop/tcopprot.h" #include "tcop/tcopprot.h"
#include "access/xact.h"
#include "utils/builtins.h" #include "utils/builtins.h"
#include "utils/hsearch.h" #include "utils/hsearch.h"
#include "utils/lsyscache.h" #include "utils/lsyscache.h"
...@@ -2856,6 +2857,7 @@ PLy_spi_prepare(PyObject *self, PyObject *args) ...@@ -2856,6 +2857,7 @@ PLy_spi_prepare(PyObject *self, PyObject *args)
char *query; char *query;
void *tmpplan; void *tmpplan;
volatile MemoryContext oldcontext; volatile MemoryContext oldcontext;
volatile ResourceOwner oldowner;
int nargs; int nargs;
if (!PyArg_ParseTuple(args, "s|O", &query, &list)) if (!PyArg_ParseTuple(args, "s|O", &query, &list))
...@@ -2879,6 +2881,11 @@ PLy_spi_prepare(PyObject *self, PyObject *args) ...@@ -2879,6 +2881,11 @@ PLy_spi_prepare(PyObject *self, PyObject *args)
plan->args = nargs ? PLy_malloc(sizeof(PLyTypeInfo) * nargs) : NULL; plan->args = nargs ? PLy_malloc(sizeof(PLyTypeInfo) * nargs) : NULL;
oldcontext = CurrentMemoryContext; oldcontext = CurrentMemoryContext;
oldowner = CurrentResourceOwner;
BeginInternalSubTransaction(NULL);
MemoryContextSwitchTo(oldcontext);
PG_TRY(); PG_TRY();
{ {
int i; int i;
...@@ -2958,20 +2965,42 @@ PLy_spi_prepare(PyObject *self, PyObject *args) ...@@ -2958,20 +2965,42 @@ PLy_spi_prepare(PyObject *self, PyObject *args)
if (plan->plan == NULL) if (plan->plan == NULL)
elog(ERROR, "SPI_saveplan failed: %s", elog(ERROR, "SPI_saveplan failed: %s",
SPI_result_code_string(SPI_result)); 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(); PG_CATCH();
{ {
ErrorData *edata; ErrorData *edata;
/* Save error info */
MemoryContextSwitchTo(oldcontext); MemoryContextSwitchTo(oldcontext);
edata = CopyErrorData(); edata = CopyErrorData();
FlushErrorState(); FlushErrorState();
Py_DECREF(plan); Py_DECREF(plan);
Py_XDECREF(optr); Py_XDECREF(optr);
if (!PyErr_Occurred())
PLy_exception_set(PLy_exc_spi_error, /* Abort the inner transaction */
"unrecognized error in PLy_spi_prepare"); RollbackAndReleaseCurrentSubTransaction();
PLy_elog(WARNING, NULL); 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); PLy_spi_exception_set(edata);
return NULL; return NULL;
} }
...@@ -3013,6 +3042,7 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit) ...@@ -3013,6 +3042,7 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit)
rv; rv;
PLyPlanObject *plan; PLyPlanObject *plan;
volatile MemoryContext oldcontext; volatile MemoryContext oldcontext;
volatile ResourceOwner oldowner;
PyObject *ret; PyObject *ret;
if (list != NULL) if (list != NULL)
...@@ -3048,6 +3078,12 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit) ...@@ -3048,6 +3078,12 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit)
} }
oldcontext = CurrentMemoryContext; oldcontext = CurrentMemoryContext;
oldowner = CurrentResourceOwner;
BeginInternalSubTransaction(NULL);
/* Want to run inside function's memory context */
MemoryContextSwitchTo(oldcontext);
PG_TRY(); PG_TRY();
{ {
char *nulls; char *nulls;
...@@ -3100,12 +3136,24 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit) ...@@ -3100,12 +3136,24 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit)
if (nargs > 0) if (nargs > 0)
pfree(nulls); 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(); PG_CATCH();
{ {
int k; int k;
ErrorData *edata; ErrorData *edata;
/* Save error info */
MemoryContextSwitchTo(oldcontext); MemoryContextSwitchTo(oldcontext);
edata = CopyErrorData(); edata = CopyErrorData();
FlushErrorState(); FlushErrorState();
...@@ -3123,10 +3171,19 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit) ...@@ -3123,10 +3171,19 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit)
} }
} }
if (!PyErr_Occurred()) /* Abort the inner transaction */
PLy_exception_set(PLy_exc_spi_error, RollbackAndReleaseCurrentSubTransaction();
"unrecognized error in PLy_spi_execute_plan"); MemoryContextSwitchTo(oldcontext);
PLy_elog(WARNING, NULL); 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); PLy_spi_exception_set(edata);
return NULL; return NULL;
} }
...@@ -3142,6 +3199,14 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit) ...@@ -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; return ret;
} }
...@@ -3150,26 +3215,55 @@ PLy_spi_execute_query(char *query, long limit) ...@@ -3150,26 +3215,55 @@ PLy_spi_execute_query(char *query, long limit)
{ {
int rv; int rv;
volatile MemoryContext oldcontext; volatile MemoryContext oldcontext;
volatile ResourceOwner oldowner;
PyObject *ret; PyObject *ret;
oldcontext = CurrentMemoryContext; oldcontext = CurrentMemoryContext;
oldowner = CurrentResourceOwner;
BeginInternalSubTransaction(NULL);
/* Want to run inside function's memory context */
MemoryContextSwitchTo(oldcontext);
PG_TRY(); PG_TRY();
{ {
pg_verifymbstr(query, strlen(query), false); pg_verifymbstr(query, strlen(query), false);
rv = SPI_execute(query, PLy_curr_procedure->fn_readonly, limit); rv = SPI_execute(query, PLy_curr_procedure->fn_readonly, limit);
ret = PLy_spi_execute_fetch_result(SPI_tuptable, SPI_processed, rv); 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(); PG_CATCH();
{ {
ErrorData *edata; ErrorData *edata;
/* Save error info */
MemoryContextSwitchTo(oldcontext); MemoryContextSwitchTo(oldcontext);
edata = CopyErrorData(); edata = CopyErrorData();
FlushErrorState(); FlushErrorState();
if (!PyErr_Occurred())
PLy_exception_set(PLy_exc_spi_error, /* Abort the inner transaction */
"unrecognized error in PLy_spi_execute_query"); RollbackAndReleaseCurrentSubTransaction();
PLy_elog(WARNING, NULL); 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); PLy_spi_exception_set(edata);
return NULL; return NULL;
} }
......
...@@ -130,3 +130,25 @@ return None ...@@ -130,3 +130,25 @@ return None
LANGUAGE plpythonu; LANGUAGE plpythonu;
SELECT valid_type('rick'); 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();
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment