diff --git a/doc/src/sgml/plpython.sgml b/doc/src/sgml/plpython.sgml index befd6afb15e..e05c2937b1f 100644 --- a/doc/src/sgml/plpython.sgml +++ b/doc/src/sgml/plpython.sgml @@ -858,6 +858,9 @@ $$ LANGUAGE plpythonu; plpy.foo. + + Database Access Functions + The plpy module provides two functions called execute and @@ -937,6 +940,33 @@ CREATE FUNCTION usesavedplan() RETURNS trigger AS $$ $$ LANGUAGE plpythonu; + + + + + Trapping Errors + + + Functions accessing the database might encounter errors, which + will cause them to abort and raise an exception. Both + plpy.execute and + plpy.prepare can raise an instance of + plpy.SPIError, which by default will terminate + the function. This error can be handled just like any other + Python exception, by using the try/except + construct. For example: + +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; + + + diff --git a/src/pl/plpython/expected/plpython_error.out b/src/pl/plpython/expected/plpython_error.out index 2b6141c376e..7597ca73f12 100644 --- a/src/pl/plpython/expected/plpython_error.out +++ b/src/pl/plpython/expected/plpython_error.out @@ -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" diff --git a/src/pl/plpython/expected/plpython_error_0.out b/src/pl/plpython/expected/plpython_error_0.out index 3842d8fb4a1..42e41196306 100644 --- a/src/pl/plpython/expected/plpython_error_0.out +++ b/src/pl/plpython/expected/plpython_error_0.out @@ -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" diff --git a/src/pl/plpython/plpython.c b/src/pl/plpython/plpython.c index fbfeb2c9f16..fff7de76743 100644 --- a/src/pl/plpython/plpython.c +++ b/src/pl/plpython/plpython.c @@ -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; } diff --git a/src/pl/plpython/sql/plpython_error.sql b/src/pl/plpython/sql/plpython_error.sql index 6509257b24d..7861cd61a2f 100644 --- a/src/pl/plpython/sql/plpython_error.sql +++ b/src/pl/plpython/sql/plpython_error.sql @@ -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();