Skip to content

Commit c5f84e9

Browse files
author
James William Pye
committed
Require prepare() before commit().
This does not effect __exit__'s functionality; it will call prepare when configured with a gid and self.state != 'prepared'.
1 parent b6e602e commit c5f84e9

3 files changed

Lines changed: 34 additions & 13 deletions

File tree

postgresql/api.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -765,7 +765,7 @@ def start(self) -> None:
765765
766766
If the database is in a transaction block, the transaction should be
767767
configured as a savepoint. If any transaction block configuration was
768-
applied to the transaction, raise a postgresql.exceptions.OperationError.
768+
applied to the transaction, raise a `postgresql.exceptions.OperationError`.
769769
770770
If the database is not in a transaction block, start one using the
771771
configuration where:
@@ -776,7 +776,10 @@ def start(self) -> None:
776776
`self.mode` specifies the mode of the transaction. Normally, ``READ
777777
ONLY`` or ``READ WRITE``.
778778
779-
If the transaction is open, do nothing.
779+
If the transaction is open--started or prepared, do nothing.
780+
781+
If the transaction has been committed or aborted, raise an
782+
`postgresql.exceptions.OperationError`.
780783
"""
781784
begin = start
782785

@@ -786,7 +789,7 @@ def commit(self) -> None:
786789
Commit the transaction.
787790
788791
If the transaction is configured with a `gid` and it has not been
789-
prepared, issue a PREPARE TRANSACTION statement with the configured `gid`.
792+
prepared, raise a `postgresql.exceptions.OperationError`.
790793
791794
If the transaction is configured with a `gid` and has already been
792795
prepared, issue a COMMIT PREPARED statement with the configured `gid`.
@@ -835,10 +838,11 @@ def recover(self) -> None:
835838
@abstractmethod
836839
def prepare(self) -> None:
837840
"""
838-
Explicitly prepare the transaction with the configured `gid`.
839-
Commit will automatically call this method if the transaction has a
840-
configured `gid`, so it is primarily provided for isolating the
841-
functionality that will be used by `commit`.
841+
Explicitly prepare the transaction with the configured `gid` by issuing a
842+
PREPARE TRANSACTION statement with the configured `gid`.
843+
This *must* be called for the first phase of the commit.
844+
845+
If the transaction is already prepared, do nothing.
842846
"""
843847

844848
@abstractmethod
@@ -864,6 +868,9 @@ def __exit__(self, typ, obj, tb):
864868
unavailable, the `rollback` method should cause a
865869
`postgresql.exceptions.ConnectionDoesNotExistError` exception to occur.
866870
871+
If the transaction is configured with a `gid` and the transaction has not
872+
been prepared, run the `prepare` method.
873+
867874
Otherwise, run the transaction's `commit` method.
868875
"""
869876

postgresql/driver/pq3.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,7 +1583,10 @@ def __exit__(self, typ, value, tb):
15831583
err.raise_exception()
15841584
else:
15851585
# No exception, and no error state. Everything is good.
1586-
self.commit()
1586+
if self.gid is not None and self.state == 'open':
1587+
self.prepare()
1588+
else:
1589+
self.commit()
15871590
else:
15881591
# There's an exception, so only rollback if the connection
15891592
# exists. If the rollback() was called here, it would just
@@ -1659,7 +1662,6 @@ def start(self):
16591662
self.ife_descend(err)
16601663
err.raise_exception()
16611664
q = self._savepoint_xact_string(hex(id(self)))
1662-
self.state = 'starting'
16631665
self.database.execute(q)
16641666
self.state = 'open'
16651667
begin = start
@@ -1690,7 +1692,6 @@ def prepare(self):
16901692
self.ife_descend(err)
16911693
err.raise_exception()
16921694
q = self._prepare_string(self.gid)
1693-
self.state = 'preparing'
16941695
self.database.execute(q)
16951696
self.state = 'prepared'
16961697

@@ -1713,6 +1714,8 @@ def recover(self):
17131714
err.raise_exception()
17141715

17151716
def commit(self):
1717+
if self.state == 'committed':
1718+
return
17161719
if self.state not in ('prepared', 'open'):
17171720
err = pg_exc.OperationError(
17181721
"commit attempted on transaction with unexpected state",
@@ -1728,7 +1731,14 @@ def commit(self):
17281731
if self.state == 'prepared':
17291732
q = "COMMIT PREPARED '" + self.gid.replace("'", "''") + "';"
17301733
else:
1731-
return self.prepare()
1734+
err = pg_exc.OperationError(
1735+
"cannot commit un-prepared two-phase commit transaction",
1736+
details = {
1737+
'hint': "Run the prepare() method before commit()."
1738+
}
1739+
)
1740+
self.ife_descend(err)
1741+
err.raise_exception()
17321742
else:
17331743
q = 'COMMIT'
17341744
else:
@@ -1743,7 +1753,6 @@ def commit(self):
17431753
self.ife_descend(err)
17441754
err.raise_exception()
17451755
q = self._release_string(hex(id(self)))
1746-
self.state = 'committing'
17471756
self.database.execute(q)
17481757
self.state = 'committed'
17491758

@@ -1773,7 +1782,6 @@ def rollback(self):
17731782
q = self._rollback_to_string(hex(id(self)))
17741783
else:
17751784
raise RuntimeError("unknown transaction type " + repr(self.type))
1776-
self.state = 'aborting'
17771785
self.database.execute(q)
17781786
self.state = 'aborted'
17791787
abort = rollback

postgresql/test/test_driver.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,12 @@ def testPreparedTransactionCommit(self):
511511
'foo',
512512
)
513513

514+
def testUnPreparedTransactionCommit(self):
515+
x = self.db.xact(gid = 'never_prepared')
516+
x.start()
517+
self.failUnlessRaises(pg_exc.OperationError, x.commit)
518+
self.failUnlessRaises(pg_exc.OperationError, x.commit)
519+
514520
def testPreparedTransactionRollback(self):
515521
x = self.db.xact(gid = 'rollback_gid')
516522
with x:

0 commit comments

Comments
 (0)