From 22690719ea5a89ccbcd04deb58c83d8d5f138df8 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Sun, 27 Feb 2011 17:09:56 +0200 Subject: [PATCH] PL/Python explicit subtransactions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a context manager, obtainable by plpy.subtransaction(), to run a group of statements in a subtransaction. Jan UrbaƄski, reviewed by Steve Singer, additional scribbling by me --- doc/src/sgml/plpython.sgml | 132 +++++- src/pl/plpython/Makefile | 1 + src/pl/plpython/expected/README | 2 + .../expected/plpython_subtransaction.out | 378 ++++++++++++++++++ .../expected/plpython_subtransaction_0.out | 366 +++++++++++++++++ src/pl/plpython/expected/plpython_test.out | 6 +- src/pl/plpython/plpython.c | 283 ++++++++++++- .../plpython/sql/plpython_subtransaction.sql | 244 +++++++++++ 8 files changed, 1404 insertions(+), 8 deletions(-) create mode 100644 src/pl/plpython/expected/plpython_subtransaction.out create mode 100644 src/pl/plpython/expected/plpython_subtransaction_0.out create mode 100644 src/pl/plpython/sql/plpython_subtransaction.sql diff --git a/doc/src/sgml/plpython.sgml b/doc/src/sgml/plpython.sgml index 94d42bfaaa..73203e6251 100644 --- a/doc/src/sgml/plpython.sgml +++ b/doc/src/sgml/plpython.sgml @@ -955,7 +955,7 @@ $$ LANGUAGE plpythonu; - + Trapping Errors @@ -981,6 +981,136 @@ $$ LANGUAGE plpythonu; + + Explicit Subtransactions + + + Recovering from errors caused by database access as described in + can lead to an undesirable + situation where some operations succeed before one of them fails, + and after recovering from that error the data is left in an + inconsistent state. PL/Python offers a solution to this problem in + the form of explicit subtransactions. + + + + Subtransaction Context Managers + + + Consider a function that implements a transfer between two + accounts: + +CREATE FUNCTION transfer_funds() RETURNS void AS $$ +try: + plpy.execute("UPDATE accounts SET balance = balance - 100 WHERE account_name = 'joe'") + plpy.execute("UPDATE accounts SET balance = balance + 100 WHERE account_name = 'mary'") +except plpy.SPIError, e: + result = "error transferring funds: %s" % e.args +else: + result = "funds transferred correctly" +plan = plpy.prepare("INSERT INTO operations (result) VALUES ($1)", ["text"]) +plpy.execute(plan, [result]) +$$ LANGUAGE plpythonu; + + If the second UPDATE statement results in an + exception being raised, this function will report the error, but + the result of the first UPDATE will + nevertheless be committed. In other words, the funds will be + withdrawn from Joe's account, but will not be transferred to + Mary's account. + + + + To avoid such issues, you can wrap your + plpy.execute calls in an explicit + subtransaction. The plpy module provides a + helper object to manage explicit subtransactions that gets created + with the plpy.subtransaction() function. + Objects created by this function implement the + + context manager interface. Using explicit subtransactions + we can rewrite our function as: + +CREATE FUNCTION transfer_funds2() RETURNS void AS $$ +try: + with plpy.subtransaction(): + plpy.execute("UPDATE accounts SET balance = balance - 100 WHERE account_name = 'joe'") + plpy.execute("UPDATE accounts SET balance = balance + 100 WHERE account_name = 'mary'") +except plpy.SPIError, e: + result = "error transferring funds: %s" % e.args +else: + result = "funds transferred correctly" +plan = plpy.prepare("INSERT INTO operations (result) VALUES ($1)", ["text"]) +plpy.execute(plan, [result]) +$$ LANGUAGE plpythonu; + + Note that the use of try/catch is still + required. Otherwise the exception would propagate to the top of + the Python stack and would cause the whole function to abort with + a PostgreSQL error, so that the + operations table would not have any row + inserted into it. The subtransaction context manager does not + trap errors, it only assures that all database operations executed + inside its scope will be atomically committed or rolled back. A + rollback of the subtransaction block occurrs on any kind of + exception exit, not only ones caused by errors originating from + database access. A regular Python exception raised inside an + explicit subtransaction block would also cause the subtransaction + to be rolled back. + + + + + Older Python Versions + + + Context managers syntax using the with keyword + is available by default in Python 2.6. If using PL/Python with an + older Python version, it is still possible to use explicit + subtransactions, although not as transparently. You can call the + subtransaction manager's __enter__ and + __exit__ functions using the + enter and exit convenience + aliases. The example function that transfers funds could be + written as: + +CREATE FUNCTION transfer_funds_old() RETURNS void AS $$ +try: + subxact = plpy.subtransaction() + subxact.enter() + try: + plpy.execute("UPDATE accounts SET balance = balance - 100 WHERE account_name = 'joe'") + plpy.execute("UPDATE accounts SET balance = balance + 100 WHERE account_name = 'mary'") + except: + import sys + subxact.exit(*sys.exc_info()) + raise + else: + subxact.exit(None, None, None) +except plpy.SPIError, e: + result = "error transferring funds: %s" % e.args +else: + result = "funds transferred correctly" + +plan = plpy.prepare("INSERT INTO operations (result) VALUES ($1)", ["text"]) +plpy.execute(plan, [result]) +$$ LANGUAGE plpythonu; + + + + + + Although context managers were implemented in Python 2.5, to use + the with syntax in that version you need to + use a future + statement. Because of implementation details, however, + you cannot use future statements in PL/Python functions. + + + + + Utility Functions diff --git a/src/pl/plpython/Makefile b/src/pl/plpython/Makefile index 376c7467c7..c72e1e300c 100644 --- a/src/pl/plpython/Makefile +++ b/src/pl/plpython/Makefile @@ -81,6 +81,7 @@ REGRESS = \ plpython_unicode \ plpython_quote \ plpython_composite \ + plpython_subtransaction \ plpython_drop # where to find psql for running the tests PSQLDIR = $(bindir) diff --git a/src/pl/plpython/expected/README b/src/pl/plpython/expected/README index 65e8969e86..e1b943ccf1 100644 --- a/src/pl/plpython/expected/README +++ b/src/pl/plpython/expected/README @@ -6,6 +6,8 @@ plpython_unicode.out server encoding != SQL_ASCII and client encoding == UTF8; plpython_unicode_0.out server encoding != SQL_ASCII and client encoding != UTF8; else ... plpython_unicode_3.out server encoding == SQL_ASCII +plpython_subtransaction_0.out Python 2.5 and older (without with statement) + plpython_types_3.out Python 3.x Note: Building with Python 2.2 is supported, but there are no expected diff --git a/src/pl/plpython/expected/plpython_subtransaction.out b/src/pl/plpython/expected/plpython_subtransaction.out new file mode 100644 index 0000000000..01b40a09a7 --- /dev/null +++ b/src/pl/plpython/expected/plpython_subtransaction.out @@ -0,0 +1,378 @@ +-- +-- Test explicit subtransactions +-- +-- Test table to see if transactions get properly rolled back +CREATE TABLE subtransaction_tbl ( + i integer +); +-- Explicit case for Python <2.6 +CREATE FUNCTION subtransaction_test(what_error text = NULL) RETURNS text +AS $$ +import sys +subxact = plpy.subtransaction() +subxact.__enter__() +exc = True +try: + try: + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + plpy.execute("INSERT INTO subtransaction_tbl VALUES (2)") + if what_error == "SPI": + plpy.execute("INSERT INTO subtransaction_tbl VALUES ('oops')") + elif what_error == "Python": + plpy.attribute_error + except: + exc = False + subxact.__exit__(*sys.exc_info()) + raise +finally: + if exc: + subxact.__exit__(None, None, None) +$$ LANGUAGE plpythonu; +SELECT subtransaction_test(); + subtransaction_test +--------------------- + +(1 row) + +SELECT * FROM subtransaction_tbl; + i +--- + 1 + 2 +(2 rows) + +TRUNCATE subtransaction_tbl; +SELECT subtransaction_test('SPI'); +ERROR: plpy.SPIError: invalid input syntax for integer: "oops" +LINE 1: INSERT INTO subtransaction_tbl VALUES ('oops') + ^ +QUERY: INSERT INTO subtransaction_tbl VALUES ('oops') +CONTEXT: PL/Python function "subtransaction_test" +SELECT * FROM subtransaction_tbl; + i +--- +(0 rows) + +TRUNCATE subtransaction_tbl; +SELECT subtransaction_test('Python'); +ERROR: AttributeError: 'module' object has no attribute 'attribute_error' +CONTEXT: PL/Python function "subtransaction_test" +SELECT * FROM subtransaction_tbl; + i +--- +(0 rows) + +TRUNCATE subtransaction_tbl; +-- Context manager case for Python >=2.6 +CREATE FUNCTION subtransaction_ctx_test(what_error text = NULL) RETURNS text +AS $$ +with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + plpy.execute("INSERT INTO subtransaction_tbl VALUES (2)") + if what_error == "SPI": + plpy.execute("INSERT INTO subtransaction_tbl VALUES ('oops')") + elif what_error == "Python": + plpy.attribute_error +$$ LANGUAGE plpythonu; +SELECT subtransaction_ctx_test(); + subtransaction_ctx_test +------------------------- + +(1 row) + +SELECT * FROM subtransaction_tbl; + i +--- + 1 + 2 +(2 rows) + +TRUNCATE subtransaction_tbl; +SELECT subtransaction_ctx_test('SPI'); +ERROR: plpy.SPIError: invalid input syntax for integer: "oops" +LINE 1: INSERT INTO subtransaction_tbl VALUES ('oops') + ^ +QUERY: INSERT INTO subtransaction_tbl VALUES ('oops') +CONTEXT: PL/Python function "subtransaction_ctx_test" +SELECT * FROM subtransaction_tbl; + i +--- +(0 rows) + +TRUNCATE subtransaction_tbl; +SELECT subtransaction_ctx_test('Python'); +ERROR: AttributeError: 'module' object has no attribute 'attribute_error' +CONTEXT: PL/Python function "subtransaction_ctx_test" +SELECT * FROM subtransaction_tbl; + i +--- +(0 rows) + +TRUNCATE subtransaction_tbl; +-- Nested subtransactions +CREATE FUNCTION subtransaction_nested_test(swallow boolean = 'f') RETURNS text +AS $$ +plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") +with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (2)") + try: + with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (3)") + plpy.execute("error") + except plpy.SPIError, e: + if not swallow: + raise + plpy.notice("Swallowed %r" % e) +return "ok" +$$ LANGUAGE plpythonu; +SELECT subtransaction_nested_test(); +ERROR: plpy.SPIError: syntax error at or near "error" +LINE 1: error + ^ +QUERY: error +CONTEXT: PL/Python function "subtransaction_nested_test" +SELECT * FROM subtransaction_tbl; + i +--- +(0 rows) + +TRUNCATE subtransaction_tbl; +SELECT subtransaction_nested_test('t'); +NOTICE: Swallowed SPIError('syntax error at or near "error"',) +CONTEXT: PL/Python function "subtransaction_nested_test" + subtransaction_nested_test +---------------------------- + ok +(1 row) + +SELECT * FROM subtransaction_tbl; + i +--- + 1 + 2 +(2 rows) + +TRUNCATE subtransaction_tbl; +-- Nested subtransactions that recursively call code dealing with +-- subtransactions +CREATE FUNCTION subtransaction_deeply_nested_test() RETURNS text +AS $$ +plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") +with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (2)") + plpy.execute("SELECT subtransaction_nested_test('t')") +return "ok" +$$ LANGUAGE plpythonu; +SELECT subtransaction_deeply_nested_test(); +NOTICE: Swallowed SPIError('syntax error at or near "error"',) +CONTEXT: PL/Python function "subtransaction_nested_test" +SQL statement "SELECT subtransaction_nested_test('t')" +PL/Python function "subtransaction_nested_test" + subtransaction_deeply_nested_test +----------------------------------- + ok +(1 row) + +SELECT * FROM subtransaction_tbl; + i +--- + 1 + 2 + 1 + 2 +(4 rows) + +TRUNCATE subtransaction_tbl; +-- Error conditions from not opening/closing subtransactions +CREATE FUNCTION subtransaction_exit_without_enter() RETURNS void +AS $$ +plpy.subtransaction().__exit__(None, None, None) +$$ LANGUAGE plpythonu; +CREATE FUNCTION subtransaction_enter_without_exit() RETURNS void +AS $$ +plpy.subtransaction().__enter__() +$$ LANGUAGE plpythonu; +CREATE FUNCTION subtransaction_exit_twice() RETURNS void +AS $$ +plpy.subtransaction().__enter__() +plpy.subtransaction().__exit__(None, None, None) +plpy.subtransaction().__exit__(None, None, None) +$$ LANGUAGE plpythonu; +CREATE FUNCTION subtransaction_enter_twice() RETURNS void +AS $$ +plpy.subtransaction().__enter__() +plpy.subtransaction().__enter__() +$$ LANGUAGE plpythonu; +CREATE FUNCTION subtransaction_exit_same_subtransaction_twice() RETURNS void +AS $$ +s = plpy.subtransaction() +s.__enter__() +s.__exit__(None, None, None) +s.__exit__(None, None, None) +$$ LANGUAGE plpythonu; +CREATE FUNCTION subtransaction_enter_same_subtransaction_twice() RETURNS void +AS $$ +s = plpy.subtransaction() +s.__enter__() +s.__enter__() +s.__exit__(None, None, None) +$$ LANGUAGE plpythonu; +-- No warnings here, as the subtransaction gets indeed closed +CREATE FUNCTION subtransaction_enter_subtransaction_in_with() RETURNS void +AS $$ +with plpy.subtransaction() as s: + s.__enter__() +$$ LANGUAGE plpythonu; +CREATE FUNCTION subtransaction_exit_subtransaction_in_with() RETURNS void +AS $$ +with plpy.subtransaction() as s: + s.__exit__(None, None, None) +$$ LANGUAGE plpythonu; +SELECT subtransaction_exit_without_enter(); +ERROR: ValueError: this subtransaction has not been entered +CONTEXT: PL/Python function "subtransaction_exit_without_enter" +SELECT subtransaction_enter_without_exit(); +WARNING: forcibly aborting a subtransaction that has not been exited +CONTEXT: PL/Python function "subtransaction_enter_without_exit" + subtransaction_enter_without_exit +----------------------------------- + +(1 row) + +SELECT subtransaction_exit_twice(); +WARNING: forcibly aborting a subtransaction that has not been exited +CONTEXT: PL/Python function "subtransaction_exit_twice" +ERROR: ValueError: this subtransaction has not been entered +CONTEXT: PL/Python function "subtransaction_exit_twice" +SELECT subtransaction_enter_twice(); +WARNING: forcibly aborting a subtransaction that has not been exited +CONTEXT: PL/Python function "subtransaction_enter_twice" +WARNING: forcibly aborting a subtransaction that has not been exited +CONTEXT: PL/Python function "subtransaction_enter_twice" + subtransaction_enter_twice +---------------------------- + +(1 row) + +SELECT subtransaction_exit_same_subtransaction_twice(); +ERROR: ValueError: this subtransaction has already been exited +CONTEXT: PL/Python function "subtransaction_exit_same_subtransaction_twice" +SELECT subtransaction_enter_same_subtransaction_twice(); +WARNING: forcibly aborting a subtransaction that has not been exited +CONTEXT: PL/Python function "subtransaction_enter_same_subtransaction_twice" +ERROR: ValueError: this subtransaction has already been entered +CONTEXT: PL/Python function "subtransaction_enter_same_subtransaction_twice" +SELECT subtransaction_enter_subtransaction_in_with(); +ERROR: ValueError: this subtransaction has already been entered +CONTEXT: PL/Python function "subtransaction_enter_subtransaction_in_with" +SELECT subtransaction_exit_subtransaction_in_with(); +ERROR: ValueError: this subtransaction has already been exited +CONTEXT: PL/Python function "subtransaction_exit_subtransaction_in_with" +-- Make sure we don't get a "current transaction is aborted" error +SELECT 1 as test; + test +------ + 1 +(1 row) + +-- Mix explicit subtransactions and normal SPI calls +CREATE FUNCTION subtransaction_mix_explicit_and_implicit() RETURNS void +AS $$ +p = plpy.prepare("INSERT INTO subtransaction_tbl VALUES ($1)", ["integer"]) +try: + with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + plpy.execute(p, [2]) + plpy.execute(p, ["wrong"]) +except plpy.SPIError: + plpy.warning("Caught a SPI error from an explicit subtransaction") + +try: + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + plpy.execute(p, [2]) + plpy.execute(p, ["wrong"]) +except plpy.SPIError: + plpy.warning("Caught a SPI error") +$$ LANGUAGE plpythonu; +SELECT subtransaction_mix_explicit_and_implicit(); +WARNING: Caught a SPI error from an explicit subtransaction +CONTEXT: PL/Python function "subtransaction_mix_explicit_and_implicit" +WARNING: Caught a SPI error +CONTEXT: PL/Python function "subtransaction_mix_explicit_and_implicit" + subtransaction_mix_explicit_and_implicit +------------------------------------------ + +(1 row) + +SELECT * FROM subtransaction_tbl; + i +--- + 1 + 2 +(2 rows) + +TRUNCATE subtransaction_tbl; +-- Alternative method names for Python <2.6 +CREATE FUNCTION subtransaction_alternative_names() RETURNS void +AS $$ +s = plpy.subtransaction() +s.enter() +s.exit(None, None, None) +$$ LANGUAGE plpythonu; +SELECT subtransaction_alternative_names(); + subtransaction_alternative_names +---------------------------------- + +(1 row) + +-- try/catch inside a subtransaction block +CREATE FUNCTION try_catch_inside_subtransaction() RETURNS void +AS $$ +with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + try: + plpy.execute("INSERT INTO subtransaction_tbl VALUES ('a')") + except plpy.SPIError: + plpy.notice("caught") +$$ LANGUAGE plpythonu; +SELECT try_catch_inside_subtransaction(); +NOTICE: caught +CONTEXT: PL/Python function "try_catch_inside_subtransaction" + try_catch_inside_subtransaction +--------------------------------- + +(1 row) + +SELECT * FROM subtransaction_tbl; + i +--- + 1 +(1 row) + +TRUNCATE subtransaction_tbl; +ALTER TABLE subtransaction_tbl ADD PRIMARY KEY (i); +NOTICE: ALTER TABLE / ADD PRIMARY KEY will create implicit index "subtransaction_tbl_pkey" for table "subtransaction_tbl" +CREATE FUNCTION pk_violation_inside_subtransaction() RETURNS void +AS $$ +with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + try: + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + except plpy.SPIError: + plpy.notice("caught") +$$ LANGUAGE plpythonu; +SELECT pk_violation_inside_subtransaction(); +NOTICE: caught +CONTEXT: PL/Python function "pk_violation_inside_subtransaction" + pk_violation_inside_subtransaction +------------------------------------ + +(1 row) + +SELECT * FROM subtransaction_tbl; + i +--- + 1 +(1 row) + +DROP TABLE subtransaction_tbl; diff --git a/src/pl/plpython/expected/plpython_subtransaction_0.out b/src/pl/plpython/expected/plpython_subtransaction_0.out new file mode 100644 index 0000000000..c5575fd715 --- /dev/null +++ b/src/pl/plpython/expected/plpython_subtransaction_0.out @@ -0,0 +1,366 @@ +-- +-- Test explicit subtransactions +-- +-- Test table to see if transactions get properly rolled back +CREATE TABLE subtransaction_tbl ( + i integer +); +-- Explicit case for Python <2.6 +CREATE FUNCTION subtransaction_test(what_error text = NULL) RETURNS text +AS $$ +import sys +subxact = plpy.subtransaction() +subxact.__enter__() +exc = True +try: + try: + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + plpy.execute("INSERT INTO subtransaction_tbl VALUES (2)") + if what_error == "SPI": + plpy.execute("INSERT INTO subtransaction_tbl VALUES ('oops')") + elif what_error == "Python": + plpy.attribute_error + except: + exc = False + subxact.__exit__(*sys.exc_info()) + raise +finally: + if exc: + subxact.__exit__(None, None, None) +$$ LANGUAGE plpythonu; +SELECT subtransaction_test(); + subtransaction_test +--------------------- + +(1 row) + +SELECT * FROM subtransaction_tbl; + i +--- + 1 + 2 +(2 rows) + +TRUNCATE subtransaction_tbl; +SELECT subtransaction_test('SPI'); +ERROR: plpy.SPIError: invalid input syntax for integer: "oops" +LINE 1: INSERT INTO subtransaction_tbl VALUES ('oops') + ^ +QUERY: INSERT INTO subtransaction_tbl VALUES ('oops') +CONTEXT: PL/Python function "subtransaction_test" +SELECT * FROM subtransaction_tbl; + i +--- +(0 rows) + +TRUNCATE subtransaction_tbl; +SELECT subtransaction_test('Python'); +ERROR: AttributeError: 'module' object has no attribute 'attribute_error' +CONTEXT: PL/Python function "subtransaction_test" +SELECT * FROM subtransaction_tbl; + i +--- +(0 rows) + +TRUNCATE subtransaction_tbl; +-- Context manager case for Python >=2.6 +CREATE FUNCTION subtransaction_ctx_test(what_error text = NULL) RETURNS text +AS $$ +with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + plpy.execute("INSERT INTO subtransaction_tbl VALUES (2)") + if what_error == "SPI": + plpy.execute("INSERT INTO subtransaction_tbl VALUES ('oops')") + elif what_error == "Python": + plpy.attribute_error +$$ LANGUAGE plpythonu; +ERROR: could not compile PL/Python function "subtransaction_ctx_test" +DETAIL: SyntaxError: invalid syntax (line 3) +SELECT subtransaction_ctx_test(); +ERROR: function subtransaction_ctx_test() does not exist +LINE 1: SELECT subtransaction_ctx_test(); + ^ +HINT: No function matches the given name and argument types. You might need to add explicit type casts. +SELECT * FROM subtransaction_tbl; + i +--- +(0 rows) + +TRUNCATE subtransaction_tbl; +SELECT subtransaction_ctx_test('SPI'); +ERROR: function subtransaction_ctx_test(unknown) does not exist +LINE 1: SELECT subtransaction_ctx_test('SPI'); + ^ +HINT: No function matches the given name and argument types. You might need to add explicit type casts. +SELECT * FROM subtransaction_tbl; + i +--- +(0 rows) + +TRUNCATE subtransaction_tbl; +SELECT subtransaction_ctx_test('Python'); +ERROR: function subtransaction_ctx_test(unknown) does not exist +LINE 1: SELECT subtransaction_ctx_test('Python'); + ^ +HINT: No function matches the given name and argument types. You might need to add explicit type casts. +SELECT * FROM subtransaction_tbl; + i +--- +(0 rows) + +TRUNCATE subtransaction_tbl; +-- Nested subtransactions +CREATE FUNCTION subtransaction_nested_test(swallow boolean = 'f') RETURNS text +AS $$ +plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") +with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (2)") + try: + with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (3)") + plpy.execute("error") + except plpy.SPIError, e: + if not swallow: + raise + plpy.notice("Swallowed %r" % e) +return "ok" +$$ LANGUAGE plpythonu; +ERROR: could not compile PL/Python function "subtransaction_nested_test" +DETAIL: SyntaxError: invalid syntax (line 4) +SELECT subtransaction_nested_test(); +ERROR: function subtransaction_nested_test() does not exist +LINE 1: SELECT subtransaction_nested_test(); + ^ +HINT: No function matches the given name and argument types. You might need to add explicit type casts. +SELECT * FROM subtransaction_tbl; + i +--- +(0 rows) + +TRUNCATE subtransaction_tbl; +SELECT subtransaction_nested_test('t'); +ERROR: function subtransaction_nested_test(unknown) does not exist +LINE 1: SELECT subtransaction_nested_test('t'); + ^ +HINT: No function matches the given name and argument types. You might need to add explicit type casts. +SELECT * FROM subtransaction_tbl; + i +--- +(0 rows) + +TRUNCATE subtransaction_tbl; +-- Nested subtransactions that recursively call code dealing with +-- subtransactions +CREATE FUNCTION subtransaction_deeply_nested_test() RETURNS text +AS $$ +plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") +with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (2)") + plpy.execute("SELECT subtransaction_nested_test('t')") +return "ok" +$$ LANGUAGE plpythonu; +ERROR: could not compile PL/Python function "subtransaction_deeply_nested_test" +DETAIL: SyntaxError: invalid syntax (line 4) +SELECT subtransaction_deeply_nested_test(); +ERROR: function subtransaction_deeply_nested_test() does not exist +LINE 1: SELECT subtransaction_deeply_nested_test(); + ^ +HINT: No function matches the given name and argument types. You might need to add explicit type casts. +SELECT * FROM subtransaction_tbl; + i +--- +(0 rows) + +TRUNCATE subtransaction_tbl; +-- Error conditions from not opening/closing subtransactions +CREATE FUNCTION subtransaction_exit_without_enter() RETURNS void +AS $$ +plpy.subtransaction().__exit__(None, None, None) +$$ LANGUAGE plpythonu; +CREATE FUNCTION subtransaction_enter_without_exit() RETURNS void +AS $$ +plpy.subtransaction().__enter__() +$$ LANGUAGE plpythonu; +CREATE FUNCTION subtransaction_exit_twice() RETURNS void +AS $$ +plpy.subtransaction().__enter__() +plpy.subtransaction().__exit__(None, None, None) +plpy.subtransaction().__exit__(None, None, None) +$$ LANGUAGE plpythonu; +CREATE FUNCTION subtransaction_enter_twice() RETURNS void +AS $$ +plpy.subtransaction().__enter__() +plpy.subtransaction().__enter__() +$$ LANGUAGE plpythonu; +CREATE FUNCTION subtransaction_exit_same_subtransaction_twice() RETURNS void +AS $$ +s = plpy.subtransaction() +s.__enter__() +s.__exit__(None, None, None) +s.__exit__(None, None, None) +$$ LANGUAGE plpythonu; +CREATE FUNCTION subtransaction_enter_same_subtransaction_twice() RETURNS void +AS $$ +s = plpy.subtransaction() +s.__enter__() +s.__enter__() +s.__exit__(None, None, None) +$$ LANGUAGE plpythonu; +-- No warnings here, as the subtransaction gets indeed closed +CREATE FUNCTION subtransaction_enter_subtransaction_in_with() RETURNS void +AS $$ +with plpy.subtransaction() as s: + s.__enter__() +$$ LANGUAGE plpythonu; +ERROR: could not compile PL/Python function "subtransaction_enter_subtransaction_in_with" +DETAIL: SyntaxError: invalid syntax (line 3) +CREATE FUNCTION subtransaction_exit_subtransaction_in_with() RETURNS void +AS $$ +with plpy.subtransaction() as s: + s.__exit__(None, None, None) +$$ LANGUAGE plpythonu; +ERROR: could not compile PL/Python function "subtransaction_exit_subtransaction_in_with" +DETAIL: SyntaxError: invalid syntax (line 3) +SELECT subtransaction_exit_without_enter(); +ERROR: ValueError: this subtransaction has not been entered +CONTEXT: PL/Python function "subtransaction_exit_without_enter" +SELECT subtransaction_enter_without_exit(); +WARNING: forcibly aborting a subtransaction that has not been exited +CONTEXT: PL/Python function "subtransaction_enter_without_exit" + subtransaction_enter_without_exit +----------------------------------- + +(1 row) + +SELECT subtransaction_exit_twice(); +WARNING: forcibly aborting a subtransaction that has not been exited +CONTEXT: PL/Python function "subtransaction_exit_twice" +ERROR: ValueError: this subtransaction has not been entered +CONTEXT: PL/Python function "subtransaction_exit_twice" +SELECT subtransaction_enter_twice(); +WARNING: forcibly aborting a subtransaction that has not been exited +CONTEXT: PL/Python function "subtransaction_enter_twice" +WARNING: forcibly aborting a subtransaction that has not been exited +CONTEXT: PL/Python function "subtransaction_enter_twice" + subtransaction_enter_twice +---------------------------- + +(1 row) + +SELECT subtransaction_exit_same_subtransaction_twice(); +ERROR: ValueError: this subtransaction has already been exited +CONTEXT: PL/Python function "subtransaction_exit_same_subtransaction_twice" +SELECT subtransaction_enter_same_subtransaction_twice(); +WARNING: forcibly aborting a subtransaction that has not been exited +CONTEXT: PL/Python function "subtransaction_enter_same_subtransaction_twice" +ERROR: ValueError: this subtransaction has already been entered +CONTEXT: PL/Python function "subtransaction_enter_same_subtransaction_twice" +SELECT subtransaction_enter_subtransaction_in_with(); +ERROR: function subtransaction_enter_subtransaction_in_with() does not exist +LINE 1: SELECT subtransaction_enter_subtransaction_in_with(); + ^ +HINT: No function matches the given name and argument types. You might need to add explicit type casts. +SELECT subtransaction_exit_subtransaction_in_with(); +ERROR: function subtransaction_exit_subtransaction_in_with() does not exist +LINE 1: SELECT subtransaction_exit_subtransaction_in_with(); + ^ +HINT: No function matches the given name and argument types. You might need to add explicit type casts. +-- Make sure we don't get a "current transaction is aborted" error +SELECT 1 as test; + test +------ + 1 +(1 row) + +-- Mix explicit subtransactions and normal SPI calls +CREATE FUNCTION subtransaction_mix_explicit_and_implicit() RETURNS void +AS $$ +p = plpy.prepare("INSERT INTO subtransaction_tbl VALUES ($1)", ["integer"]) +try: + with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + plpy.execute(p, [2]) + plpy.execute(p, ["wrong"]) +except plpy.SPIError: + plpy.warning("Caught a SPI error from an explicit subtransaction") + +try: + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + plpy.execute(p, [2]) + plpy.execute(p, ["wrong"]) +except plpy.SPIError: + plpy.warning("Caught a SPI error") +$$ LANGUAGE plpythonu; +ERROR: could not compile PL/Python function "subtransaction_mix_explicit_and_implicit" +DETAIL: SyntaxError: invalid syntax (line 5) +SELECT subtransaction_mix_explicit_and_implicit(); +ERROR: function subtransaction_mix_explicit_and_implicit() does not exist +LINE 1: SELECT subtransaction_mix_explicit_and_implicit(); + ^ +HINT: No function matches the given name and argument types. You might need to add explicit type casts. +SELECT * FROM subtransaction_tbl; + i +--- +(0 rows) + +TRUNCATE subtransaction_tbl; +-- Alternative method names for Python <2.6 +CREATE FUNCTION subtransaction_alternative_names() RETURNS void +AS $$ +s = plpy.subtransaction() +s.enter() +s.exit(None, None, None) +$$ LANGUAGE plpythonu; +SELECT subtransaction_alternative_names(); + subtransaction_alternative_names +---------------------------------- + +(1 row) + +-- try/catch inside a subtransaction block +CREATE FUNCTION try_catch_inside_subtransaction() RETURNS void +AS $$ +with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + try: + plpy.execute("INSERT INTO subtransaction_tbl VALUES ('a')") + except plpy.SPIError: + plpy.notice("caught") +$$ LANGUAGE plpythonu; +ERROR: could not compile PL/Python function "try_catch_inside_subtransaction" +DETAIL: SyntaxError: invalid syntax (line 3) +SELECT try_catch_inside_subtransaction(); +ERROR: function try_catch_inside_subtransaction() does not exist +LINE 1: SELECT try_catch_inside_subtransaction(); + ^ +HINT: No function matches the given name and argument types. You might need to add explicit type casts. +SELECT * FROM subtransaction_tbl; + i +--- +(0 rows) + +TRUNCATE subtransaction_tbl; +ALTER TABLE subtransaction_tbl ADD PRIMARY KEY (i); +NOTICE: ALTER TABLE / ADD PRIMARY KEY will create implicit index "subtransaction_tbl_pkey" for table "subtransaction_tbl" +CREATE FUNCTION pk_violation_inside_subtransaction() RETURNS void +AS $$ +with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + try: + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + except plpy.SPIError: + plpy.notice("caught") +$$ LANGUAGE plpythonu; +ERROR: could not compile PL/Python function "pk_violation_inside_subtransaction" +DETAIL: SyntaxError: invalid syntax (line 3) +SELECT pk_violation_inside_subtransaction(); +ERROR: function pk_violation_inside_subtransaction() does not exist +LINE 1: SELECT pk_violation_inside_subtransaction(); + ^ +HINT: No function matches the given name and argument types. You might need to add explicit type casts. +SELECT * FROM subtransaction_tbl; + i +--- +(0 rows) + +DROP TABLE subtransaction_tbl; diff --git a/src/pl/plpython/expected/plpython_test.out b/src/pl/plpython/expected/plpython_test.out index 961f6c0d18..18eb6d1c28 100644 --- a/src/pl/plpython/expected/plpython_test.out +++ b/src/pl/plpython/expected/plpython_test.out @@ -43,9 +43,9 @@ contents.sort() return ", ".join(contents) $$ LANGUAGE plpythonu; select module_contents(); - module_contents ---------------------------------------------------------------------------------------------------------------------------------------- - Error, Fatal, SPIError, debug, error, execute, fatal, info, log, notice, prepare, quote_ident, quote_literal, quote_nullable, warning + module_contents +------------------------------------------------------------------------------------------------------------------------------------------------------- + Error, Fatal, SPIError, debug, error, execute, fatal, info, log, notice, prepare, quote_ident, quote_literal, quote_nullable, subtransaction, warning (1 row) CREATE FUNCTION elog_test() RETURNS void diff --git a/src/pl/plpython/plpython.c b/src/pl/plpython/plpython.c index e5832782fb..4744ee7beb 100644 --- a/src/pl/plpython/plpython.c +++ b/src/pl/plpython/plpython.c @@ -233,6 +233,13 @@ typedef struct PLyProcedureEntry PLyProcedure *proc; } PLyProcedureEntry; +/* explicit subtransaction data */ +typedef struct PLySubtransactionData +{ + MemoryContext oldcontext; + ResourceOwner oldowner; +} PLySubtransactionData; + /* Python objects */ typedef struct PLyPlanObject @@ -254,6 +261,13 @@ typedef struct PLyResultObject PyObject *status; /* query status, SPI_OK_*, or SPI_ERR_* */ } PLyResultObject; +typedef struct PLySubtransactionObject +{ + PyObject_HEAD + bool started; + bool exited; +} PLySubtransactionObject; + /* function declarations */ @@ -382,6 +396,9 @@ static HeapTuple PLyGenericObject_ToTuple(PLyTypeInfo *, TupleDesc, PyObject *); */ static PLyProcedure *PLy_curr_procedure = NULL; +/* list of explicit subtransaction data */ +static List *explicit_subtransactions = NIL; + static PyObject *PLy_interp_globals = NULL; static PyObject *PLy_interp_safe_globals = NULL; static HTAB *PLy_procedure_cache = NULL; @@ -401,6 +418,10 @@ static char PLy_result_doc[] = { "Results of a PostgreSQL query" }; +static char PLy_subtransaction_doc[] = { + "PostgreSQL subtransaction context manager" +}; + /* * the function definitions @@ -1226,20 +1247,71 @@ PLy_function_handler(FunctionCallInfo fcinfo, PLyProcedure *proc) return rv; } +/* + * Abort lingering subtransactions that have been explicitly started + * by plpy.subtransaction().start() and not properly closed. + */ +static void +PLy_abort_open_subtransactions(int save_subxact_level) +{ + Assert(save_subxact_level >= 0); + + while (list_length(explicit_subtransactions) > save_subxact_level) + { + PLySubtransactionData *subtransactiondata; + + Assert(explicit_subtransactions != NIL); + + ereport(WARNING, + (errmsg("forcibly aborting a subtransaction that has not been exited"))); + + RollbackAndReleaseCurrentSubTransaction(); + + SPI_restore_connection(); + + subtransactiondata = (PLySubtransactionData *) linitial(explicit_subtransactions); + explicit_subtransactions = list_delete_first(explicit_subtransactions); + + MemoryContextSwitchTo(subtransactiondata->oldcontext); + CurrentResourceOwner = subtransactiondata->oldowner; + PLy_free(subtransactiondata); + } +} + static PyObject * PLy_procedure_call(PLyProcedure *proc, char *kargs, PyObject *vargs) { PyObject *rv; + int volatile save_subxact_level = list_length(explicit_subtransactions); PyDict_SetItemString(proc->globals, kargs, vargs); + + PG_TRY(); + { #if PY_VERSION_HEX >= 0x03020000 - rv = PyEval_EvalCode(proc->code, - proc->globals, proc->globals); + rv = PyEval_EvalCode(proc->code, + proc->globals, proc->globals); #else - rv = PyEval_EvalCode((PyCodeObject *) proc->code, - proc->globals, proc->globals); + rv = PyEval_EvalCode((PyCodeObject *) proc->code, + proc->globals, proc->globals); #endif + /* + * Since plpy will only let you close subtransactions that + * you started, you cannot *unnest* subtransactions, only + * *nest* them without closing. + */ + Assert(list_length(explicit_subtransactions) >= save_subxact_level); + } + PG_CATCH(); + { + PLy_abort_open_subtransactions(save_subxact_level); + PG_RE_THROW(); + } + PG_END_TRY(); + + PLy_abort_open_subtransactions(save_subxact_level); + /* If the Python code returned an error, propagate it */ if (rv == NULL) PLy_elog(ERROR, NULL); @@ -2762,6 +2834,12 @@ 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_subtransaction(PyObject *, PyObject *); +static PyObject *PLy_subtransaction_new(void); +static void PLy_subtransaction_dealloc(PyObject *); +static PyObject *PLy_subtransaction_enter(PyObject *, PyObject *); +static PyObject *PLy_subtransaction_exit(PyObject *, PyObject *); + static PyMethodDef PLy_plan_methods[] = { {"status", PLy_plan_status, METH_VARARGS, NULL}, @@ -2854,6 +2932,50 @@ static PyTypeObject PLy_ResultType = { PLy_result_methods, /* tp_tpmethods */ }; +static PyMethodDef PLy_subtransaction_methods[] = { + {"__enter__", PLy_subtransaction_enter, METH_VARARGS, NULL}, + {"__exit__", PLy_subtransaction_exit, METH_VARARGS, NULL}, + /* user-friendly names for Python <2.6 */ + {"enter", PLy_subtransaction_enter, METH_VARARGS, NULL}, + {"exit", PLy_subtransaction_exit, METH_VARARGS, NULL}, + {NULL, NULL, 0, NULL} +}; + +static PyTypeObject PLy_SubtransactionType = { + PyVarObject_HEAD_INIT(NULL, 0) + "PLySubtransaction", /* tp_name */ + sizeof(PLySubtransactionObject), /* tp_size */ + 0, /* tp_itemsize */ + + /* + * methods + */ + PLy_subtransaction_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ + PLy_subtransaction_doc, /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + PLy_subtransaction_methods, /* tp_tpmethods */ +}; + static PyMethodDef PLy_methods[] = { /* * logging methods @@ -2883,6 +3005,11 @@ static PyMethodDef PLy_methods[] = { {"quote_nullable", PLy_quote_nullable, METH_VARARGS, NULL}, {"quote_ident", PLy_quote_ident, METH_VARARGS, NULL}, + /* + * create the subtransaction context manager + */ + {"subtransaction", PLy_subtransaction, METH_NOARGS, NULL}, + {NULL, NULL, 0, NULL} }; @@ -3553,6 +3680,150 @@ PLy_spi_execute_fetch_result(SPITupleTable *tuptable, int rows, int status) return (PyObject *) result; } +/* s = plpy.subtransaction() */ +static PyObject * +PLy_subtransaction(PyObject *self, PyObject *unused) +{ + return PLy_subtransaction_new(); +} + +/* Allocate and initialize a PLySubtransactionObject */ +static PyObject * +PLy_subtransaction_new(void) +{ + PLySubtransactionObject *ob; + + ob = PyObject_New(PLySubtransactionObject, &PLy_SubtransactionType); + + if (ob == NULL) + return NULL; + + ob->started = false; + ob->exited = false; + + return (PyObject *) ob; +} + +/* Python requires a dealloc function to be defined */ +static void +PLy_subtransaction_dealloc(PyObject *subxact) +{ +} + +/* + * subxact.__enter__() or subxact.enter() + * + * Start an explicit subtransaction. SPI calls within an explicit + * subtransaction will not start another one, so you can atomically + * execute many SPI calls and still get a controllable exception if + * one of them fails. + */ +static PyObject * +PLy_subtransaction_enter(PyObject *self, PyObject *unused) +{ + PLySubtransactionData *subxactdata; + MemoryContext oldcontext; + PLySubtransactionObject *subxact = (PLySubtransactionObject *) self; + + if (subxact->started) + { + PLy_exception_set(PyExc_ValueError, "this subtransaction has already been entered"); + return NULL; + } + + if (subxact->exited) + { + PLy_exception_set(PyExc_ValueError, "this subtransaction has already been exited"); + return NULL; + } + + subxact->started = true; + oldcontext = CurrentMemoryContext; + + subxactdata = PLy_malloc(sizeof(*subxactdata)); + subxactdata->oldcontext = oldcontext; + subxactdata->oldowner = CurrentResourceOwner; + + BeginInternalSubTransaction(NULL); + /* Do not want to leave the previous memory context */ + MemoryContextSwitchTo(oldcontext); + + explicit_subtransactions = lcons(subxactdata, explicit_subtransactions); + + Py_INCREF(self); + return self; +} + +/* + * subxact.__exit__(exc_type, exc, tb) or subxact.exit(exc_type, exc, tb) + * + * Exit an explicit subtransaction. exc_type is an exception type, exc + * is the exception object, tb is the traceback. If exc_type is None, + * commit the subtransactiony, if not abort it. + * + * The method signature is chosen to allow subtransaction objects to + * be used as context managers as described in + * . + */ +static PyObject * +PLy_subtransaction_exit(PyObject *self, PyObject *args) +{ + PyObject *type; + PyObject *value; + PyObject *traceback; + PLySubtransactionData *subxactdata; + PLySubtransactionObject *subxact = (PLySubtransactionObject *) self; + + if (!PyArg_ParseTuple(args, "OOO", &type, &value, &traceback)) + return NULL; + + if (!subxact->started) + { + PLy_exception_set(PyExc_ValueError, "this subtransaction has not been entered"); + return NULL; + } + + if (subxact->exited) + { + PLy_exception_set(PyExc_ValueError, "this subtransaction has already been exited"); + return NULL; + } + + if (explicit_subtransactions == NIL) + { + PLy_exception_set(PyExc_ValueError, "there is no subtransaction to exit from"); + return NULL; + } + + subxact->exited = true; + + if (type != Py_None) + { + /* Abort the inner transaction */ + RollbackAndReleaseCurrentSubTransaction(); + } + else + { + ReleaseCurrentSubTransaction(); + } + + subxactdata = (PLySubtransactionData *) linitial(explicit_subtransactions); + explicit_subtransactions = list_delete_first(explicit_subtransactions); + + MemoryContextSwitchTo(subxactdata->oldcontext); + CurrentResourceOwner = subxactdata->oldowner; + PLy_free(subxactdata); + + /* + * AtEOSubXact_SPI() should not have popped any SPI context, but + * just in case it did, make sure we remain connected. + */ + SPI_restore_connection(); + + Py_INCREF(Py_None); + return Py_None; +} + /* * language handler and interpreter initialization @@ -3653,6 +3924,8 @@ _PG_init(void) PLy_trigger_cache = hash_create("PL/Python triggers", 32, &hash_ctl, HASH_ELEM | HASH_FUNCTION); + explicit_subtransactions = NIL; + inited = true; } @@ -3688,6 +3961,8 @@ PLy_init_plpy(void) elog(ERROR, "could not initialize PLy_PlanType"); if (PyType_Ready(&PLy_ResultType) < 0) elog(ERROR, "could not initialize PLy_ResultType"); + if (PyType_Ready(&PLy_SubtransactionType) < 0) + elog(ERROR, "could not initialize PLy_SubtransactionType"); #if PY_MAJOR_VERSION >= 3 plpy = PyModule_Create(&PLy_module); diff --git a/src/pl/plpython/sql/plpython_subtransaction.sql b/src/pl/plpython/sql/plpython_subtransaction.sql new file mode 100644 index 0000000000..a19cad5104 --- /dev/null +++ b/src/pl/plpython/sql/plpython_subtransaction.sql @@ -0,0 +1,244 @@ +-- +-- Test explicit subtransactions +-- + +-- Test table to see if transactions get properly rolled back + +CREATE TABLE subtransaction_tbl ( + i integer +); + +-- Explicit case for Python <2.6 + +CREATE FUNCTION subtransaction_test(what_error text = NULL) RETURNS text +AS $$ +import sys +subxact = plpy.subtransaction() +subxact.__enter__() +exc = True +try: + try: + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + plpy.execute("INSERT INTO subtransaction_tbl VALUES (2)") + if what_error == "SPI": + plpy.execute("INSERT INTO subtransaction_tbl VALUES ('oops')") + elif what_error == "Python": + plpy.attribute_error + except: + exc = False + subxact.__exit__(*sys.exc_info()) + raise +finally: + if exc: + subxact.__exit__(None, None, None) +$$ LANGUAGE plpythonu; + +SELECT subtransaction_test(); +SELECT * FROM subtransaction_tbl; +TRUNCATE subtransaction_tbl; +SELECT subtransaction_test('SPI'); +SELECT * FROM subtransaction_tbl; +TRUNCATE subtransaction_tbl; +SELECT subtransaction_test('Python'); +SELECT * FROM subtransaction_tbl; +TRUNCATE subtransaction_tbl; + +-- Context manager case for Python >=2.6 + +CREATE FUNCTION subtransaction_ctx_test(what_error text = NULL) RETURNS text +AS $$ +with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + plpy.execute("INSERT INTO subtransaction_tbl VALUES (2)") + if what_error == "SPI": + plpy.execute("INSERT INTO subtransaction_tbl VALUES ('oops')") + elif what_error == "Python": + plpy.attribute_error +$$ LANGUAGE plpythonu; + +SELECT subtransaction_ctx_test(); +SELECT * FROM subtransaction_tbl; +TRUNCATE subtransaction_tbl; +SELECT subtransaction_ctx_test('SPI'); +SELECT * FROM subtransaction_tbl; +TRUNCATE subtransaction_tbl; +SELECT subtransaction_ctx_test('Python'); +SELECT * FROM subtransaction_tbl; +TRUNCATE subtransaction_tbl; + +-- Nested subtransactions + +CREATE FUNCTION subtransaction_nested_test(swallow boolean = 'f') RETURNS text +AS $$ +plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") +with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (2)") + try: + with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (3)") + plpy.execute("error") + except plpy.SPIError, e: + if not swallow: + raise + plpy.notice("Swallowed %r" % e) +return "ok" +$$ LANGUAGE plpythonu; + +SELECT subtransaction_nested_test(); +SELECT * FROM subtransaction_tbl; +TRUNCATE subtransaction_tbl; + +SELECT subtransaction_nested_test('t'); +SELECT * FROM subtransaction_tbl; +TRUNCATE subtransaction_tbl; + +-- Nested subtransactions that recursively call code dealing with +-- subtransactions + +CREATE FUNCTION subtransaction_deeply_nested_test() RETURNS text +AS $$ +plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") +with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (2)") + plpy.execute("SELECT subtransaction_nested_test('t')") +return "ok" +$$ LANGUAGE plpythonu; + +SELECT subtransaction_deeply_nested_test(); +SELECT * FROM subtransaction_tbl; +TRUNCATE subtransaction_tbl; + +-- Error conditions from not opening/closing subtransactions + +CREATE FUNCTION subtransaction_exit_without_enter() RETURNS void +AS $$ +plpy.subtransaction().__exit__(None, None, None) +$$ LANGUAGE plpythonu; + +CREATE FUNCTION subtransaction_enter_without_exit() RETURNS void +AS $$ +plpy.subtransaction().__enter__() +$$ LANGUAGE plpythonu; + +CREATE FUNCTION subtransaction_exit_twice() RETURNS void +AS $$ +plpy.subtransaction().__enter__() +plpy.subtransaction().__exit__(None, None, None) +plpy.subtransaction().__exit__(None, None, None) +$$ LANGUAGE plpythonu; + +CREATE FUNCTION subtransaction_enter_twice() RETURNS void +AS $$ +plpy.subtransaction().__enter__() +plpy.subtransaction().__enter__() +$$ LANGUAGE plpythonu; + +CREATE FUNCTION subtransaction_exit_same_subtransaction_twice() RETURNS void +AS $$ +s = plpy.subtransaction() +s.__enter__() +s.__exit__(None, None, None) +s.__exit__(None, None, None) +$$ LANGUAGE plpythonu; + +CREATE FUNCTION subtransaction_enter_same_subtransaction_twice() RETURNS void +AS $$ +s = plpy.subtransaction() +s.__enter__() +s.__enter__() +s.__exit__(None, None, None) +$$ LANGUAGE plpythonu; + +-- No warnings here, as the subtransaction gets indeed closed +CREATE FUNCTION subtransaction_enter_subtransaction_in_with() RETURNS void +AS $$ +with plpy.subtransaction() as s: + s.__enter__() +$$ LANGUAGE plpythonu; + +CREATE FUNCTION subtransaction_exit_subtransaction_in_with() RETURNS void +AS $$ +with plpy.subtransaction() as s: + s.__exit__(None, None, None) +$$ LANGUAGE plpythonu; + +SELECT subtransaction_exit_without_enter(); +SELECT subtransaction_enter_without_exit(); +SELECT subtransaction_exit_twice(); +SELECT subtransaction_enter_twice(); +SELECT subtransaction_exit_same_subtransaction_twice(); +SELECT subtransaction_enter_same_subtransaction_twice(); +SELECT subtransaction_enter_subtransaction_in_with(); +SELECT subtransaction_exit_subtransaction_in_with(); + +-- Make sure we don't get a "current transaction is aborted" error +SELECT 1 as test; + +-- Mix explicit subtransactions and normal SPI calls + +CREATE FUNCTION subtransaction_mix_explicit_and_implicit() RETURNS void +AS $$ +p = plpy.prepare("INSERT INTO subtransaction_tbl VALUES ($1)", ["integer"]) +try: + with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + plpy.execute(p, [2]) + plpy.execute(p, ["wrong"]) +except plpy.SPIError: + plpy.warning("Caught a SPI error from an explicit subtransaction") + +try: + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + plpy.execute(p, [2]) + plpy.execute(p, ["wrong"]) +except plpy.SPIError: + plpy.warning("Caught a SPI error") +$$ LANGUAGE plpythonu; + +SELECT subtransaction_mix_explicit_and_implicit(); +SELECT * FROM subtransaction_tbl; +TRUNCATE subtransaction_tbl; + +-- Alternative method names for Python <2.6 + +CREATE FUNCTION subtransaction_alternative_names() RETURNS void +AS $$ +s = plpy.subtransaction() +s.enter() +s.exit(None, None, None) +$$ LANGUAGE plpythonu; + +SELECT subtransaction_alternative_names(); + +-- try/catch inside a subtransaction block + +CREATE FUNCTION try_catch_inside_subtransaction() RETURNS void +AS $$ +with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + try: + plpy.execute("INSERT INTO subtransaction_tbl VALUES ('a')") + except plpy.SPIError: + plpy.notice("caught") +$$ LANGUAGE plpythonu; + +SELECT try_catch_inside_subtransaction(); +SELECT * FROM subtransaction_tbl; +TRUNCATE subtransaction_tbl; + +ALTER TABLE subtransaction_tbl ADD PRIMARY KEY (i); + +CREATE FUNCTION pk_violation_inside_subtransaction() RETURNS void +AS $$ +with plpy.subtransaction(): + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + try: + plpy.execute("INSERT INTO subtransaction_tbl VALUES (1)") + except plpy.SPIError: + plpy.notice("caught") +$$ LANGUAGE plpythonu; + +SELECT pk_violation_inside_subtransaction(); +SELECT * FROM subtransaction_tbl; + +DROP TABLE subtransaction_tbl;