Skip to content

Commit ac116ff

Browse files
author
James William Pye
committed
Correct merge removing deprecated 2pc feature.
(Add it back in.)
1 parent 843aea0 commit ac116ff

2 files changed

Lines changed: 187 additions & 9 deletions

File tree

postgresql/driver/pq3.py

Lines changed: 89 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2043,14 +2043,19 @@ class Transaction(pg_api.Transaction):
20432043

20442044
mode = None
20452045
isolation = None
2046+
gid = None
20462047

2047-
_e_factors = ('database', 'isolation', 'mode')
2048+
_e_factors = ('database', 'gid', 'isolation', 'mode')
20482049

20492050
def _e_metas(self):
20502051
yield (None, self.state)
20512052

2052-
def __init__(self, database, isolation = None, mode = None):
2053+
def __init__(self, database, isolation = None, mode = None, gid = None):
20532054
self.database = database
2055+
self.gid = gid
2056+
if gid is not None:
2057+
# XXX: remove in 1.1
2058+
warnings.warn("two phase interfaces will not exist in 1.1; do not use the 'gid' parameter", DeprecationWarning, stacklevel=3)
20542059
self.isolation = isolation
20552060
self.mode = mode
20562061
self.state = 'initialized'
@@ -2081,10 +2086,26 @@ def __exit__(self, typ, value, tb):
20812086
# If an error occurs, clean up the transaction state
20822087
# and raise as needed.
20832088
except pg_exc.ActiveTransactionError as err:
2084-
if not self.database.closed:
2089+
##
2090+
# Failed COMMIT PREPARED <gid>?
2091+
# Likely cases:
2092+
# - User exited block without preparing the transaction.
2093+
##
2094+
if not self.database.closed and self.gid is not None:
20852095
# adjust the state so rollback will do the right thing and abort.
20862096
self.state = 'open'
20872097
self.rollback()
2098+
##
2099+
# The other exception that *can* occur is
2100+
# UndefinedObjectError in which:
2101+
# - User issued C/RB P <gid> before exit, but not via xact methods.
2102+
# - User adjusted gid after prepare().
2103+
#
2104+
# But the occurrence of this exception means it's not in an active
2105+
# transaction, which means no cleanup other than raise is necessary.
2106+
err.details['cause'] = \
2107+
"The prepared transaction was not " \
2108+
"prepared prior to the block's exit."
20882109
raise
20892110
elif issubclass(typ, Exception):
20902111
# There's an exception, so only rollback if the connection
@@ -2130,7 +2151,7 @@ def start(self):
21302151
)
21312152
else:
21322153
self.type = 'savepoint'
2133-
if (self.isolation, self.mode) != (None,None):
2154+
if (self.gid, self.isolation, self.mode) != (None,None,None):
21342155
em = element.ClientError((
21352156
(b'S', 'ERROR'),
21362157
(b'C', '--OPE'),
@@ -2143,15 +2164,60 @@ def start(self):
21432164
self.state = 'open'
21442165
begin = start
21452166

2167+
@staticmethod
2168+
def _prepare_string(id):
2169+
"2pc prepared transaction 'gid'"
2170+
return "PREPARE TRANSACTION '" + id.replace("'", "''") + "';"
2171+
21462172
@staticmethod
21472173
def _release_string(id):
21482174
'release "";'
21492175
return 'RELEASE "xact(' + id.replace('"', '""') + ')";'
21502176

2177+
def prepare(self):
2178+
if self.state == 'prepared':
2179+
return
2180+
if self.state != 'open':
2181+
em = element.ClientError((
2182+
(b'S', 'ERROR'),
2183+
(b'C', '--OPE'),
2184+
(b'M', "transaction state must be 'open' in order to prepare"),
2185+
))
2186+
self.database.typio.raise_client_error(em, creator = self)
2187+
if self.type != 'block':
2188+
em = element.ClientError((
2189+
(b'S', 'ERROR'),
2190+
(b'C', '--OPE'),
2191+
(b'M', "improper transaction type to prepare"),
2192+
))
2193+
self.database.typio.raise_client_error(em, creator = self)
2194+
q = self._prepare_string(self.gid)
2195+
self.database.execute(q)
2196+
self.state = 'prepared'
2197+
2198+
def recover(self):
2199+
if self.state != 'initialized':
2200+
em = element.ClientError((
2201+
(b'S', 'ERROR'),
2202+
(b'C', '--OPE'),
2203+
(b'M', "improper state for prepared transaction recovery"),
2204+
))
2205+
self.database.typio.raise_client_error(em, creator = self)
2206+
if self.database.sys.xact_is_prepared(self.gid):
2207+
self.state = 'prepared'
2208+
self.type = 'block'
2209+
else:
2210+
em = element.ClientError((
2211+
(b'S', 'ERROR'),
2212+
(b'C', '42704'), # UndefinedObjectError
2213+
(b'M', "prepared transaction does not exist"),
2214+
))
2215+
self.database.typio.raise_client_error(em, creator = self)
2216+
21512217
def commit(self):
21522218
if self.state == 'committed':
21532219
return
2154-
if self.state != 'open':
2220+
if self.state not in ('prepared', 'open'):
21552221
em = element.ClientError((
21562222
(b'S', 'ERROR'),
21572223
(b'C', '--OPE'),
@@ -2160,8 +2226,19 @@ def commit(self):
21602226
self.database.typio.raise_client_error(em, creator = self)
21612227

21622228
if self.type == 'block':
2163-
q = 'COMMIT'
2229+
if self.gid is not None:
2230+
# User better have prepared it.
2231+
q = "COMMIT PREPARED '" + self.gid.replace("'", "''") + "';"
2232+
else:
2233+
q = 'COMMIT'
21642234
else:
2235+
if self.gid is not None:
2236+
em = element.ClientError((
2237+
(b'S', 'ERROR'),
2238+
(b'C', '--OPE'),
2239+
(b'M', "savepoint configured with global identifier"),
2240+
))
2241+
self.database.typio.raise_client_error(em, creator = self)
21652242
q = self._release_string(hex(id(self)))
21662243
self.database.execute(q)
21672244
self.state = 'committed'
@@ -2182,7 +2259,10 @@ def rollback(self):
21822259
self.database.typio.raise_client_error(em, creator = self)
21832260

21842261
if self.type == 'block':
2185-
q = 'ABORT;'
2262+
if self.state == 'prepared':
2263+
q = "ROLLBACK PREPARED '" + self.gid.replace("'", "''") + "'"
2264+
else:
2265+
q = 'ABORT;'
21862266
elif self.type == 'savepoint':
21872267
q = self._rollback_to_string(hex(id(self)))
21882268
else:
@@ -2264,8 +2344,8 @@ def do(self, language : str, source : str,
22642344
sql = "DO " + qlit(source) + " LANGUAGE " + qid(language) + ";"
22652345
self.execute(sql)
22662346

2267-
def xact(self, isolation = None, mode = None):
2268-
x = Transaction(self, isolation = isolation, mode = mode)
2347+
def xact(self, gid = None, isolation = None, mode = None):
2348+
x = Transaction(self, gid = gid, isolation = isolation, mode = mode)
22692349
return x
22702350

22712351
def prepare(self,

postgresql/test/test_driver.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,6 +1524,104 @@ def testCloseInSubTransactionBlock(self):
15241524
except pg_exc.ConnectionDoesNotExistError:
15251525
pass
15261526

1527+
@pg_tmp
1528+
def testPreparedTransactionCommit(self):
1529+
with db.xact(gid='commit_gid') as x:
1530+
db.execute("create table commit_gidtable as select 'foo'::text as t;")
1531+
x.prepare()
1532+
# not committed yet, so it better fail.
1533+
self.assertRaises(pg_exc.UndefinedTableError,
1534+
db.execute, "select * from commit_gidtable"
1535+
)
1536+
# now it's committed.
1537+
self.assertEqual(
1538+
db.prepare("select * FROM commit_gidtable").first(),
1539+
'foo',
1540+
)
1541+
db.execute('drop table commit_gidtable;')
1542+
1543+
@pg_tmp
1544+
def testWithUnpreparedTransaction(self):
1545+
try:
1546+
with db.xact(gid='not-gonna-prepare-it') as x:
1547+
pass
1548+
except pg_exc.ActiveTransactionError:
1549+
# *must* be okay to query again.
1550+
self.assertEqual(db.prepare('select 1').first(), 1)
1551+
else:
1552+
self.fail("commit with gid succeeded unprepared..")
1553+
1554+
@pg_tmp
1555+
def testWithPreparedException(self):
1556+
class TheFailure(Exception):
1557+
pass
1558+
try:
1559+
with db.xact(gid='yeah,weprepare') as x:
1560+
x.prepare()
1561+
raise TheFailure()
1562+
except TheFailure as err:
1563+
# __exit__ should have issued ROLLBACK PREPARED, so let's find out.
1564+
# *must* be okay to query again.
1565+
self.assertEqual(db.prepare('select 1').first(), 1)
1566+
x = db.xact(gid='yeah,weprepare')
1567+
self.assertRaises(pg_exc.UndefinedObjectError, x.recover)
1568+
else:
1569+
self.fail("failure exception was not raised")
1570+
1571+
@pg_tmp
1572+
def testUnPreparedTransactionCommit(self):
1573+
x = db.xact(gid='never_prepared')
1574+
x.start()
1575+
self.assertRaises(pg_exc.ActiveTransactionError, x.commit)
1576+
self.assertRaises(pg_exc.InFailedTransactionError, x.commit)
1577+
1578+
@pg_tmp
1579+
def testPreparedTransactionRollback(self):
1580+
x = db.xact(gid='rollback_gid')
1581+
x.start()
1582+
db.execute("create table gidtable as select 'foo'::text as t;")
1583+
x.prepare()
1584+
x.rollback()
1585+
self.assertRaises(
1586+
pg_exc.UndefinedTableError,
1587+
db.execute, "select * from gidtable"
1588+
)
1589+
1590+
@pg_tmp
1591+
def testPreparedTransactionRecovery(self):
1592+
x = db.xact(gid='recover dis')
1593+
x.start()
1594+
db.execute("create table distable (i int);")
1595+
x.prepare()
1596+
del x
1597+
x = db.xact(gid='recover dis')
1598+
x.recover()
1599+
x.commit()
1600+
db.execute("drop table distable;")
1601+
1602+
@pg_tmp
1603+
def testPreparedTransactionRecoveryAbort(self):
1604+
x = db.xact(gid='recover dis abort')
1605+
x.start()
1606+
db.execute("create table distableabort (i int);")
1607+
x.prepare()
1608+
del x
1609+
x = db.xact(gid='recover dis abort')
1610+
x.recover()
1611+
x.rollback()
1612+
self.assertRaises(
1613+
pg_exc.UndefinedTableError,
1614+
db.execute, "select * from distableabort"
1615+
)
1616+
1617+
@pg_tmp
1618+
def testPreparedTransactionFailedRecovery(self):
1619+
x = db.xact(gid="NO XACT HERE")
1620+
self.assertRaises(
1621+
pg_exc.UndefinedObjectError,
1622+
x.recover
1623+
)
1624+
15271625
@pg_tmp
15281626
def testSettingsCM(self):
15291627
orig = db.settings['search_path']

0 commit comments

Comments
 (0)