Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit b58fd4a

Browse files
committed
Add a "subtransaction" command to PL/Tcl.
This allows rolling back the effects of some SPI commands without having to fail the entire PL/Tcl function. Victor Wagner, reviewed by Pavel Stehule Discussion: https://postgr.es/m/20170108205750.2dab04a1@wagner.wagner.home
1 parent f9dfa5c commit b58fd4a

File tree

5 files changed

+398
-7
lines changed

5 files changed

+398
-7
lines changed

doc/src/sgml/pltcl.sgml

+106-6
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,20 @@ $$ LANGUAGE pltcl;
476476
</listitem>
477477
</varlistentry>
478478

479+
<varlistentry>
480+
<term><function>subtransaction</function> <replaceable>command</replaceable></term>
481+
<listitem>
482+
<para>
483+
The Tcl script contained in <replaceable>command</replaceable> is
484+
executed within a SQL subtransaction. If the script returns an
485+
error, that entire subtransaction is rolled back before returning the
486+
error out to the surrounding Tcl code.
487+
See <xref linkend="pltcl-subtransactions"> for more details and an
488+
example.
489+
</para>
490+
</listitem>
491+
</varlistentry>
492+
479493
<varlistentry>
480494
<term><function>quote</> <replaceable>string</replaceable></term>
481495
<listitem>
@@ -844,18 +858,22 @@ CREATE EVENT TRIGGER tcl_a_snitch ON ddl_command_start EXECUTE PROCEDURE tclsnit
844858
either by executing some invalid operation or by generating an error
845859
using the Tcl <function>error</function> command or
846860
PL/Tcl's <function>elog</function> command. Such errors can be caught
847-
within Tcl using the Tcl <function>catch</function> command. If they
848-
are not caught but are allowed to propagate out to the top level of
849-
execution of the PL/Tcl function, they turn into database errors.
861+
within Tcl using the Tcl <function>catch</function> command. If an
862+
error is not caught but is allowed to propagate out to the top level of
863+
execution of the PL/Tcl function, it is reported as a SQL error in the
864+
function's calling query.
850865
</para>
851866

852867
<para>
853-
Conversely, database errors that occur within PL/Tcl's
868+
Conversely, SQL errors that occur within PL/Tcl's
854869
<function>spi_exec</function>, <function>spi_prepare</function>,
855870
and <function>spi_execp</function> commands are reported as Tcl errors,
856871
so they are catchable by Tcl's <function>catch</function> command.
857-
Again, if they propagate out to the top level without being caught,
858-
they turn back into database errors.
872+
(Each of these PL/Tcl commands runs its SQL operation in a
873+
subtransaction, which is rolled back on error, so that any
874+
partially-completed operation is automatically cleaned up.)
875+
Again, if an error propagates out to the top level without being caught,
876+
it turns back into a SQL error.
859877
</para>
860878

861879
<para>
@@ -902,6 +920,88 @@ if {[catch { spi_exec $sql_command }]} {
902920
</para>
903921
</sect1>
904922

923+
<sect1 id="pltcl-subtransactions">
924+
<title>Explicit Subtransactions in PL/Tcl</title>
925+
926+
<indexterm>
927+
<primary>subtransactions</primary>
928+
<secondary>in PL/Tcl</secondary>
929+
</indexterm>
930+
931+
<para>
932+
Recovering from errors caused by database access as described in
933+
<xref linkend="pltcl-error-handling"> can lead to an undesirable
934+
situation where some operations succeed before one of them fails,
935+
and after recovering from that error the data is left in an
936+
inconsistent state. PL/Tcl offers a solution to this problem in
937+
the form of explicit subtransactions.
938+
</para>
939+
940+
<para>
941+
Consider a function that implements a transfer between two accounts:
942+
<programlisting>
943+
CREATE FUNCTION transfer_funds() RETURNS void AS $$
944+
if [catch {
945+
spi_exec "UPDATE accounts SET balance = balance - 100 WHERE account_name = 'joe'"
946+
spi_exec "UPDATE accounts SET balance = balance + 100 WHERE account_name = 'mary'"
947+
} errormsg] {
948+
set result [format "error transferring funds: %s" $errormsg]
949+
} else {
950+
set result "funds transferred successfully"
951+
}
952+
spi_exec "INSERT INTO operations (result) VALUES ('[quote $result]')"
953+
$$ LANGUAGE pltcl;
954+
</programlisting>
955+
If the second <command>UPDATE</command> statement results in an
956+
exception being raised, this function will log the failure, but
957+
the result of the first <command>UPDATE</command> will
958+
nevertheless be committed. In other words, the funds will be
959+
withdrawn from Joe's account, but will not be transferred to
960+
Mary's account. This happens because each <function>spi_exec</function>
961+
is a separate subtransaction, and only one of those subtransactions
962+
got rolled back.
963+
</para>
964+
965+
<para>
966+
To handle such cases, you can wrap multiple database operations in an
967+
explicit subtransaction, which will succeed or roll back as a whole.
968+
PL/Tcl provides a <function>subtransaction</function> command to manage
969+
this. We can rewrite our function as:
970+
<programlisting>
971+
CREATE FUNCTION transfer_funds2() RETURNS void AS $$
972+
if [catch {
973+
subtransaction {
974+
spi_exec "UPDATE accounts SET balance = balance - 100 WHERE account_name = 'joe'"
975+
spi_exec "UPDATE accounts SET balance = balance + 100 WHERE account_name = 'mary'"
976+
}
977+
} errormsg] {
978+
set result [format "error transferring funds: %s" $errormsg]
979+
} else {
980+
set result "funds transferred successfully"
981+
}
982+
spi_exec "INSERT INTO operations (result) VALUES ('[quote $result]')"
983+
$$ LANGUAGE pltcl;
984+
</programlisting>
985+
Note that use of <function>catch</function> is still required for this
986+
purpose. Otherwise the error would propagate to the top level of the
987+
function, preventing the desired insertion into
988+
the <structname>operations</structname> table.
989+
The <function>subtransaction</function> command does not trap errors, it
990+
only assures that all database operations executed inside its scope will
991+
be rolled back together when an error is reported.
992+
</para>
993+
994+
<para>
995+
A rollback of an explicit subtransaction occurs on any error reported
996+
by the contained Tcl code, not only errors originating from database
997+
access. Thus a regular Tcl exception raised inside
998+
a <function>subtransaction</function> command will also cause the
999+
subtransaction to be rolled back. However, non-error exits out of the
1000+
contained Tcl code (for instance, due to <function>return</function>) do
1001+
not cause a rollback.
1002+
</para>
1003+
</sect1>
1004+
9051005
<sect1 id="pltcl-config">
9061006
<title>PL/Tcl Configuration</title>
9071007

src/pl/tcl/Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ DATA = pltcl.control pltcl--1.0.sql pltcl--unpackaged--1.0.sql \
2828
pltclu.control pltclu--1.0.sql pltclu--unpackaged--1.0.sql
2929

3030
REGRESS_OPTS = --dbname=$(PL_TESTDB) --load-extension=pltcl
31-
REGRESS = pltcl_setup pltcl_queries pltcl_start_proc pltcl_unicode
31+
REGRESS = pltcl_setup pltcl_queries pltcl_start_proc pltcl_subxact pltcl_unicode
3232

3333
# Tcl on win32 ships with import libraries only for Microsoft Visual C++,
3434
# which are not compatible with mingw gcc. Therefore we need to build a

src/pl/tcl/expected/pltcl_subxact.out

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
--
2+
-- Test explicit subtransactions
3+
--
4+
CREATE TABLE subtransaction_tbl (
5+
i integer
6+
);
7+
--
8+
-- We use this wrapper to catch errors and return errormsg only,
9+
-- because values of $::errorinfo variable contain procedure name which
10+
-- includes OID, so it's not stable
11+
--
12+
CREATE FUNCTION pltcl_wrapper(statement text) RETURNS text
13+
AS $$
14+
if [catch {spi_exec $1} msg] {
15+
return "ERROR: $msg"
16+
} else {
17+
return "SUCCESS: $msg"
18+
}
19+
$$ LANGUAGE pltcl;
20+
-- Test subtransaction successfully committed
21+
CREATE FUNCTION subtransaction_ctx_success() RETURNS void
22+
AS $$
23+
spi_exec "INSERT INTO subtransaction_tbl VALUES(1)"
24+
subtransaction {
25+
spi_exec "INSERT INTO subtransaction_tbl VALUES(2)"
26+
}
27+
$$ LANGUAGE pltcl;
28+
BEGIN;
29+
INSERT INTO subtransaction_tbl VALUES(0);
30+
SELECT subtransaction_ctx_success();
31+
subtransaction_ctx_success
32+
----------------------------
33+
34+
(1 row)
35+
36+
COMMIT;
37+
SELECT * FROM subtransaction_tbl;
38+
i
39+
---
40+
0
41+
1
42+
2
43+
(3 rows)
44+
45+
TRUNCATE subtransaction_tbl;
46+
-- Test subtransaction rollback
47+
CREATE FUNCTION subtransaction_ctx_test(what_error text = NULL) RETURNS void
48+
AS $$
49+
spi_exec "INSERT INTO subtransaction_tbl VALUES (1)"
50+
subtransaction {
51+
spi_exec "INSERT INTO subtransaction_tbl VALUES (2)"
52+
if {$1 == "SPI"} {
53+
spi_exec "INSERT INTO subtransaction_tbl VALUES ('oops')"
54+
} elseif { $1 == "Tcl"} {
55+
elog ERROR "Tcl error"
56+
}
57+
}
58+
$$ LANGUAGE pltcl;
59+
SELECT pltcl_wrapper('SELECT subtransaction_ctx_test()');
60+
pltcl_wrapper
61+
---------------
62+
SUCCESS: 1
63+
(1 row)
64+
65+
SELECT * FROM subtransaction_tbl;
66+
i
67+
---
68+
1
69+
2
70+
(2 rows)
71+
72+
TRUNCATE subtransaction_tbl;
73+
SELECT pltcl_wrapper('SELECT subtransaction_ctx_test(''SPI'')');
74+
pltcl_wrapper
75+
-------------------------------------------------
76+
ERROR: invalid input syntax for integer: "oops"
77+
(1 row)
78+
79+
SELECT * FROM subtransaction_tbl;
80+
i
81+
---
82+
(0 rows)
83+
84+
TRUNCATE subtransaction_tbl;
85+
SELECT pltcl_wrapper('SELECT subtransaction_ctx_test(''Tcl'')');
86+
pltcl_wrapper
87+
------------------
88+
ERROR: Tcl error
89+
(1 row)
90+
91+
SELECT * FROM subtransaction_tbl;
92+
i
93+
---
94+
(0 rows)
95+
96+
TRUNCATE subtransaction_tbl;
97+
-- Nested subtransactions
98+
CREATE FUNCTION subtransaction_nested_test(swallow boolean = 'f') RETURNS text
99+
AS $$
100+
spi_exec "INSERT INTO subtransaction_tbl VALUES (1)"
101+
subtransaction {
102+
spi_exec "INSERT INTO subtransaction_tbl VALUES (2)"
103+
if [catch {
104+
subtransaction {
105+
spi_exec "INSERT INTO subtransaction_tbl VALUES (3)"
106+
spi_exec "error"
107+
}
108+
} errormsg] {
109+
if {$1 != "t"} {
110+
error $errormsg $::errorInfo $::errorCode
111+
}
112+
elog NOTICE "Swallowed $errormsg"
113+
}
114+
}
115+
return "ok"
116+
$$ LANGUAGE pltcl;
117+
SELECT pltcl_wrapper('SELECT subtransaction_nested_test()');
118+
pltcl_wrapper
119+
----------------------------------------
120+
ERROR: syntax error at or near "error"
121+
(1 row)
122+
123+
SELECT * FROM subtransaction_tbl;
124+
i
125+
---
126+
(0 rows)
127+
128+
TRUNCATE subtransaction_tbl;
129+
SELECT pltcl_wrapper('SELECT subtransaction_nested_test(''t'')');
130+
NOTICE: Swallowed syntax error at or near "error"
131+
pltcl_wrapper
132+
---------------
133+
SUCCESS: 1
134+
(1 row)
135+
136+
SELECT * FROM subtransaction_tbl;
137+
i
138+
---
139+
1
140+
2
141+
(2 rows)
142+
143+
TRUNCATE subtransaction_tbl;

src/pl/tcl/pltcl.c

+53
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ static int pltcl_SPI_execute_plan(ClientData cdata, Tcl_Interp *interp,
306306
int objc, Tcl_Obj *const objv[]);
307307
static int pltcl_SPI_lastoid(ClientData cdata, Tcl_Interp *interp,
308308
int objc, Tcl_Obj *const objv[]);
309+
static int pltcl_subtransaction(ClientData cdata, Tcl_Interp *interp,
310+
int objc, Tcl_Obj *const objv[]);
309311

310312
static void pltcl_subtrans_begin(MemoryContext oldcontext,
311313
ResourceOwner oldowner);
@@ -516,6 +518,8 @@ pltcl_init_interp(pltcl_interp_desc *interp_desc, Oid prolang, bool pltrusted)
516518
pltcl_SPI_execute_plan, NULL, NULL);
517519
Tcl_CreateObjCommand(interp, "spi_lastoid",
518520
pltcl_SPI_lastoid, NULL, NULL);
521+
Tcl_CreateObjCommand(interp, "subtransaction",
522+
pltcl_subtransaction, NULL, NULL);
519523

520524
/************************************************************
521525
* Call the appropriate start_proc, if there is one.
@@ -2850,6 +2854,55 @@ pltcl_SPI_lastoid(ClientData cdata, Tcl_Interp *interp,
28502854
}
28512855

28522856

2857+
/**********************************************************************
2858+
* pltcl_subtransaction() - Execute some Tcl code in a subtransaction
2859+
*
2860+
* The subtransaction is aborted if the Tcl code fragment returns TCL_ERROR,
2861+
* otherwise it's subcommitted.
2862+
**********************************************************************/
2863+
static int
2864+
pltcl_subtransaction(ClientData cdata, Tcl_Interp *interp,
2865+
int objc, Tcl_Obj *const objv[])
2866+
{
2867+
MemoryContext oldcontext = CurrentMemoryContext;
2868+
ResourceOwner oldowner = CurrentResourceOwner;
2869+
int retcode;
2870+
2871+
if (objc != 2)
2872+
{
2873+
Tcl_WrongNumArgs(interp, 1, objv, "command");
2874+
return TCL_ERROR;
2875+
}
2876+
2877+
/*
2878+
* Note: we don't use pltcl_subtrans_begin and friends here because we
2879+
* don't want the error handling in pltcl_subtrans_abort. But otherwise
2880+
* the processing should be about the same as in those functions.
2881+
*/
2882+
BeginInternalSubTransaction(NULL);
2883+
MemoryContextSwitchTo(oldcontext);
2884+
2885+
retcode = Tcl_EvalObjEx(interp, objv[1], 0);
2886+
2887+
if (retcode == TCL_ERROR)
2888+
{
2889+
/* Rollback the subtransaction */
2890+
RollbackAndReleaseCurrentSubTransaction();
2891+
}
2892+
else
2893+
{
2894+
/* Commit the subtransaction */
2895+
ReleaseCurrentSubTransaction();
2896+
}
2897+
2898+
/* In either case, restore previous memory context and resource owner */
2899+
MemoryContextSwitchTo(oldcontext);
2900+
CurrentResourceOwner = oldowner;
2901+
2902+
return retcode;
2903+
}
2904+
2905+
28532906
/**********************************************************************
28542907
* pltcl_set_tuple_values() - Set variables for all attributes
28552908
* of a given tuple

0 commit comments

Comments
 (0)