Skip to content

Commit 8352299

Browse files
author
James William Pye
committed
Implement Transactions as independent objects.
This migrates the APIs away from the more restrictive transaction-depth based management model. The problem with the model was that it was functionally inconsistent with SQL's concept of savepoints, and offered no obvious means for implementing the second commit for prepared transactions.
1 parent 84722f0 commit 8352299

4 files changed

Lines changed: 462 additions & 327 deletions

File tree

postgresql/api.py

Lines changed: 154 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
PG-API
99
======
1010
11-
``postgresql.api`` is a Python API to the PostgreSQL RDBMS. It is designed to take
11+
``postgresql.api`` is a Python API to the PostgreSQL DBMS. It is designed to take
1212
full advantage of PostgreSQL's features to provide the Python programmer with
1313
substantial convenience.
1414
1515
This module is used to define the PG-API. It creates a set of ABCs
1616
that makes up the basic interfaces used to work with a PostgreSQL. PG-API is an
17-
extension of the ``py-sqlapi`` ABCs.
17+
extension of ``py-database``'s database.api.sql APIs.
1818
1919
The `InterfaceElement` is the common ABC, the methods and attributes defined
2020
within that class can, should, be mostly ignored while extracting information.
@@ -75,15 +75,14 @@ class InterfaceElement(metaclass = ABCMeta):
7575
7676
<Python Traceback>
7777
postgresql.exceptions.Error: <message>
78-
DRIVER: postgresql.driver.pq3
79-
CONNECTOR: pq://user@localhost:5432/database
80-
CONNECTION: <connection_title> <backend_id> <socket information>
81-
<settings, transaction state, connection state>
82-
QUERY: <query_title> <statement_id> <parameter info>
83-
<query body>
8478
CURSOR: <cursor_id>
8579
<parameters>
86-
80+
QUERY: <statement_id> <parameter info>
81+
<query body>
82+
CONNECTION: <connection_title> <backend_id> <socket information>
83+
<settings, transaction state, connection state>
84+
CONNECTOR: pq://user@localhost:5432/database
85+
DRIVER: postgresql.driver.pq3
8786
8887
Receptors
8988
---------
@@ -99,8 +98,8 @@ class InterfaceElement(metaclass = ABCMeta):
9998
-------
10099
101100
Many of these APIs are used to support features that users are *not*
102-
expected to use. Almost everything on `InterfaceElement` is subject to
103-
deprecation.
101+
expected to use directly. Almost everything on `InterfaceElement` is subject
102+
to deprecation.
104103
"""
105104
ife_object_title = "<untitled>"
106105

@@ -566,7 +565,7 @@ def statement_id(self) -> str:
566565

567566
@propertydoc
568567
@abstractproperty
569-
def string(self) -> str:
568+
def string(self) -> object:
570569
"""
571570
The SQL string of the prepared statement.
572571
@@ -594,7 +593,7 @@ def __call__(self,
594593
595594
>>> p=db.prepare("SELECT column FROM ttable WHERE key = $1")
596595
>>> p('identifier')
597-
<`Cursor` instance>
596+
<postgresql.api.Cursor>
598597
"""
599598

600599
@abstractmethod
@@ -679,148 +678,193 @@ def __call__(self, *args, **kw) -> (object, Cursor, collections.Iterable):
679678
SRF returns a composite(OUT parameters), it *should* return a `Cursor`.
680679
"""
681680

682-
class TransactionManager(
683-
InterfaceElement
684-
):
681+
##
682+
# Arguably, it would be wiser to isolate blocks, prepared transactions, and
683+
# savepoints, but the utility of the separation is not significant. It's really
684+
# more interesting as a formality that the user may explicitly state the
685+
# type of the transaction. However, this capability is not completely absent
686+
# from the current interface as the configuration parameters, or lack thereof,
687+
# help imply the expectations.
688+
class Transaction(InterfaceElement):
685689
"""
686-
A `TranactionManager` is the `Connection`'s transaction manager.
687-
`TransactionManager` compliant instances *must* exist on a `Connection`
688-
instance's `xact` attribute.
690+
A `Tranaction` is an element that represents a transaction in the session.
691+
Once created, it's ready to be started, and subsequently committed or
692+
rolled back.
689693
690-
Normal usage would entail the use of the with-statement::
694+
Read-only transaction:
691695
692-
with db.xact:
693-
...
694-
695-
Or, in cases where two-phase commit is desired::
696+
>>> with db.xact(mode = 'read only'):
697+
... ...
696698
697-
with db.xact(gid = 'gid'):
698-
...
699+
Read committed isolation:
700+
701+
>>> with db.xact(isolation = 'READ COMMITTED'):
702+
... ...
703+
704+
Savepoints are created if inside a transaction block:
705+
706+
>>> with db.xact():
707+
... with db.xact():
708+
... ...
709+
710+
Or, in cases where two-phase commit is desired:
711+
712+
>>> with db.xact(gid = 'gid') as gxact:
713+
... with gxact:
714+
... # phase 1 block
715+
... ...
716+
>>> # fully committed at this point
717+
718+
Considering that transactions decide what's saved and what's not saved, it is
719+
important that they are used properly. In most situations, when an action is
720+
performed where state of the transaction is unexpected, an exception should
721+
occur.
699722
"""
700723
ife_label = 'XACT'
701724

702725
@propertydoc
703726
@abstractproperty
704-
def failed(self) -> (bool, None):
727+
def mode(self) -> (None, str):
705728
"""
706-
bool stating if the current transaction has failed due to an error.
707-
`None` if not in a transaction block.
729+
The mode of the transaction block:
730+
731+
START TRANSACTION [ISOLATION] <mode>;
732+
733+
The `mode` property is a string and will be directly interpolated into the
734+
START TRANSACTION statement.
708735
"""
709736

710737
@propertydoc
711738
@abstractproperty
712-
def depth(self) -> int:
739+
def isolation(self) -> (None, str):
713740
"""
714-
`int` stating the current transaction depth.
741+
The isolation level of the transaction block:
715742
716-
The depth starts at zero, indicating no transactions have been started.
717-
For each call to `start`, this is incremented by one.
718-
For each call to `abort` or `commit`, this is decremented by one.
743+
START TRANSACTION <isolation> [MODE];
719744
720-
Implementation must protect against negative levels.
745+
The `isolation` property is a string and will be directly interpolated into
746+
the START TRANSACTION statement.
721747
"""
722748

723-
@abstractmethod
724-
def start(self) -> None:
725-
"""
726-
Start a transaction block. If a transaction block has already been
727-
started, make a savepoint.
728-
``start``, ``begin``, and ``__enter__`` are synonyms.
749+
@propertydoc
750+
@abstractproperty
751+
def gid(self) -> (None, str):
729752
"""
730-
__enter__ = begin = start
753+
The global identifier of the transaction block:
731754
732-
def __context__(self):
733-
return self
755+
PREPARE TRANSACTION <gid>;
756+
757+
The `gid` property is a string that indicates that the block is a prepared
758+
transaction.
759+
"""
734760

735761
@abstractmethod
736-
def __exit__(self, typ, obj, tb):
762+
def start(self) -> None:
737763
"""
738-
Commit the transaction, or abort if the given exception is not `None`.
739-
If the transaction level is greater than one, then the savepoint
740-
corresponding to the current level will be released or rolled back in
741-
cases of an exception.
764+
Start the transaction.
765+
766+
If the database is in a transaction block, the transaction should be
767+
configured as a savepoint. If any transaction block configuration was
768+
applied to the transaction, raise a postgresql.exceptions.OperationError.
742769
743-
If an exception was raised, then the return value must indicate the need
744-
to further raise the exception, unless the exception is an
745-
`postgresql.exceptions.AbortTransaction`. In which case, the transaction
746-
will be rolled back accordingly, but the no exception will be raised.
770+
If the database is not in a transaction block, start one using the
771+
configuration where:
772+
773+
`self.isolation` specifies the ``ISOLATION LEVEL``. Normally, ``READ
774+
COMMITTED``, ``SERIALIZABLE``, or ``READ UNCOMMITTED``.
775+
776+
`self.mode` specifies the mode of the transaction. Normally, ``READ
777+
ONLY`` or ``READ WRITE``.
778+
779+
If the transaction is open, do nothing.
747780
"""
781+
begin = start
748782

749783
@abstractmethod
750784
def commit(self) -> None:
751785
"""
752-
Commit the transaction block, release a savepoint, or prepare the
753-
transaction for commit. If the number of running transactions is greater
754-
than one, then the corresponding savepoint is released. If no savepoints
755-
are set and the transaction is configured with a 'gid', then the
756-
transaction is prepared instead of committed, otherwise the transaction
757-
is simply committed.
758-
"""
786+
Commit the transaction.
759787
760-
@abstractmethod
761-
def rollback(self) -> None:
762-
"""
763-
Abort the current transaction or rollback to the last started savepoint.
764-
`rollback` and `abort` are synonyms.
788+
If the transaction is configured with a `gid` and it has not been
789+
prepared, issue a PREPARE TRANSACTION statement with the configured `gid`.
790+
791+
If the transaction is configured with a `gid` and has already been
792+
prepared, issue a COMMIT PREPARED statement with the configured `gid`.
793+
794+
If the transaction was started inside a transaction block, it should be
795+
identified as a savepoint, and the savepoint should be released.
796+
797+
If the transaction has already been committed, do nothing.
765798
"""
766-
abort = rollback
767799

768800
@abstractmethod
769-
def __call__(self, gid = None, isolation = None, read_only = None):
801+
def rollback(self) -> None:
770802
"""
771-
Initialize the transaction using parameters and return self to support a
772-
convenient with-statement syntax.
803+
Abort the transaction.
773804
774-
The configuration only applies to transaction blocks as savepoints have
775-
no parameters to be configured.
805+
If the transaction is configured with a `gid` *and* has been prepared, issue
806+
a ROLLBACK PREPARE statement with the configured `gid`.
776807
777-
If the `gid`, the first keyword parameter, is configured, the
778-
transaction manager will issue a ``PREPARE TRANSACTION`` with the
779-
specified identifier instead of a ``COMMIT``.
808+
If the transaction is a savepoint, ROLLBACK TO the savepoint identifier.
780809
781-
If `isolation` is specified, the ``START TRANSACTION`` will include it
782-
as the ``ISOLATION LEVEL``. This must be a character string.
810+
If the transaction is a transaction block, issue an ABORT.
783811
784-
If the `read_only` parameter is specified, the transaction block will be
785-
started in the ``READ ONLY`` mode if True, and ``READ WRITE`` mode if
786-
False. If `None`, neither ``READ ONLY`` or ``READ WRITE`` will be
787-
specified.
788-
789-
Read-only transaction::
812+
If the transaction has already been aborted, do nothing.
813+
"""
814+
abort = rollback
790815

791-
>>> with db.xact(read_only = True):
792-
...
816+
@abstractmethod
817+
def recover(self) -> None:
818+
"""
819+
If the transaction is assigned a `gid`, recover may be used to identify
820+
the transaction as prepared and ready for committing or aborting.
793821
794-
Read committed isolation::
822+
This method is used in recovery procedures where a prepared transaction
823+
needs to be committed or rolled back.
795824
796-
>>> with pg_con.xact(isolation = 'READ COMMITTED'):
797-
...
825+
If no prepared transaction with the configured `gid` exists, a
826+
`postgresql.exceptions.UndefinedObjectError` must be raised.
827+
[This is consistent with the error raised by ROLLBACK/COMMIT PREPARED]
798828
799-
Database configured defaults apply to all `TransactionManager`
800-
operations.
829+
Once this method has been ran, it should identify the transaction as being
830+
prepared so that subsequent invocations to `commit` or `rollback` should
831+
cause the appropriate ROLLBACK PREPARED or COMMIT PREPARED statements to
832+
be executed.
801833
"""
802834

803835
@abstractmethod
804-
def commit_prepared(self, gid : str):
836+
def prepare(self) -> None:
805837
"""
806-
Commit the prepared transaction with the given `gid`.
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`.
807842
"""
808843

809844
@abstractmethod
810-
def rollback_prepared(self, *gids : str):
845+
def __enter__(self):
811846
"""
812-
Rollback the prepared transactions with the given `gid`.
847+
Synonym for `start` returning self.
813848
"""
814849

815-
@propertydoc
816-
@abstractproperty
817-
def prepared(self) -> "sequence of prepared transaction identifiers":
850+
def __context__(self):
851+
'Return self'
852+
return self
853+
854+
@abstractmethod
855+
def __exit__(self, typ, obj, tb):
818856
"""
819-
A sequence of available prepared transactions for the current user on
820-
the current database. This is intended to be more relavent for the
821-
current context than selecting the contents of ``pg_prepared_xacts``.
822-
So, the view *must* be limited to those of the current database, and
823-
those which the user can commit.
857+
If an exception is indicated by the parameters, run the transaction's
858+
`rollback` method iff the database is still available(not closed), and
859+
return a `False` value.
860+
861+
If an exception is not indicated, but the database's transaction state is
862+
in error, run the transaction's `rollback` method and raise a
863+
`postgresql.exceptions.InFailedTransactionError`. If the database is
864+
unavailable, the `rollback` method should cause a
865+
`postgresql.exceptions.ConnectionDoesNotExistError` exception to occur.
866+
867+
Otherwise, run the transaction's `commit` method.
824868
"""
825869

826870
class Settings(
@@ -994,9 +1038,14 @@ def client_port(self) -> (int, None):
9941038

9951039
@propertydoc
9961040
@abstractproperty
997-
def xact(self) -> TransactionManager:
1041+
def xact(self,
1042+
gid : "global identifier to configure" = None,
1043+
isolation : "ISOLATION LEVEL to use with the transaction" = None,
1044+
mode : "Mode of the transaction, READ ONLY or READ WRITE" = None,
1045+
) -> Transaction:
9981046
"""
999-
A `TransactionManager` instance bound to the `Database`.
1047+
Create a `Transaction` object using the given keyword arguments as its
1048+
configuration.
10001049
"""
10011050

10021051
@propertydoc
@@ -1091,14 +1140,12 @@ def reset(self) -> None:
10911140
10921141
Issues a ``RESET ALL`` to the database. If the database supports
10931142
removing temporary tables created in the session, then remove them.
1094-
Reapply initial configuration settings such as path. If inside a
1095-
transaction block when called, reset the transaction state using the
1096-
`reset` method on the connection's transaction manager, `xact`.
1143+
Reapply initial configuration settings such as path.
10971144
10981145
The purpose behind this method is to provide a soft-reconnect method
10991146
that re-initializes the connection into its original state. One
11001147
obvious use of this would be in a connection pool where the connection
1101-
is done being used.
1148+
is being recycled.
11021149
"""
11031150

11041151
class Connector(InterfaceElement):
@@ -1120,7 +1167,6 @@ class Connector(InterfaceElement):
11201167
def Connection(self) -> "`Connection`":
11211168
"""
11221169
The default `Connection` class that is used.
1123-
This *should* be available on the type object.
11241170
"""
11251171

11261172
@propertydoc

0 commit comments

Comments
 (0)