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

Lines changed: 106 additions & 6 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 143 additions & 0 deletions
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

Lines changed: 53 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy