OSDN Git Service

PL/Python explicit subtransactions
authorPeter Eisentraut <peter_e@gmx.net>
Sun, 27 Feb 2011 15:09:56 +0000 (17:09 +0200)
committerPeter Eisentraut <peter_e@gmx.net>
Sun, 27 Feb 2011 19:15:35 +0000 (21:15 +0200)
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
src/pl/plpython/Makefile
src/pl/plpython/expected/README
src/pl/plpython/expected/plpython_subtransaction.out [new file with mode: 0644]
src/pl/plpython/expected/plpython_subtransaction_0.out [new file with mode: 0644]
src/pl/plpython/expected/plpython_test.out
src/pl/plpython/plpython.c
src/pl/plpython/sql/plpython_subtransaction.sql [new file with mode: 0644]

index 94d42bf..73203e6 100644 (file)
@@ -955,7 +955,7 @@ $$ LANGUAGE plpythonu;
 
   </sect2>
 
-  <sect2>
+  <sect2 id="plpython-trapping">
    <title>Trapping Errors</title>
 
    <para>
@@ -981,6 +981,136 @@ $$ LANGUAGE plpythonu;
   </sect2>
  </sect1>
 
+ <sect1 id="plpython-subtransaction">
+  <title>Explicit Subtransactions</title>
+
+  <para>
+   Recovering from errors caused by database access as described in
+   <xref linkend="plpython-trapping"> 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.
+  </para>
+
+  <sect2>
+   <title>Subtransaction Context Managers</title>
+
+   <para>
+    Consider a function that implements a transfer between two
+    accounts:
+<programlisting>
+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;
+</programlisting>
+    If the second <literal>UPDATE</literal> statement results in an
+    exception being raised, this function will report the error, but
+    the result of the first <literal>UPDATE</literal> 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.
+   </para>
+
+   <para>
+    To avoid such issues, you can wrap your
+    <literal>plpy.execute</literal> calls in an explicit
+    subtransaction.  The <literal>plpy</literal> module provides a
+    helper object to manage explicit subtransactions that gets created
+    with the <literal>plpy.subtransaction()</literal> function.
+    Objects created by this function implement the
+    <ulink url="http://docs.python.org/library/stdtypes.html#context-manager-types">
+    context manager interface</ulink>.  Using explicit subtransactions
+    we can rewrite our function as:
+<programlisting>
+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;
+</programlisting>
+    Note that the use of <literal>try/catch</literal> 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 <productname>PostgreSQL</productname> error, so that the
+    <literal>operations</literal> 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.
+   </para>
+  </sect2>
+
+  <sect2>
+   <title>Older Python Versions</title>
+
+   <para>
+    Context managers syntax using the <literal>with</literal> 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 <literal>__enter__</literal> and
+    <literal>__exit__</literal> functions using the
+    <literal>enter</literal> and <literal>exit</literal> convenience
+    aliases.  The example function that transfers funds could be
+    written as:
+<programlisting>
+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;
+</programlisting>
+   </para>
+
+   <note>
+    <para>
+     Although context managers were implemented in Python 2.5, to use
+     the <literal>with</literal> syntax in that version you need to
+     use a <ulink
+     url="http://docs.python.org/release/2.5/ref/future.html">future
+     statement</ulink>.  Because of implementation details, however,
+     you cannot use future statements in PL/Python functions.
+    </para>
+   </note>
+  </sect2>
+ </sect1>
+
  <sect1 id="plpython-util">
   <title>Utility Functions</title>
   <para>
index 376c746..c72e1e3 100644 (file)
@@ -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)
index 65e8969..e1b943c 100644 (file)
@@ -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 (file)
index 0000000..01b40a0
--- /dev/null
@@ -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 (file)
index 0000000..c5575fd
--- /dev/null
@@ -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;
index 961f6c0..18eb6d1 100644 (file)
@@ -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
index e583278..4744ee7 100644 (file)
@@ -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
+ * <http://www.python.org/dev/peps/pep-0343/>.
+ */
+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 (file)
index 0000000..a19cad5
--- /dev/null
@@ -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;