From 20c9c1745728a5463746ba89ff0271bc2de4c668 Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Thu, 16 Feb 2017 02:07:05 +0000 Subject: [PATCH 1/5] connection.isolation_level is now writable --- NEWS | 1 + doc/src/connection.rst | 38 +++--- lib/extensions.py | 2 +- psycopg/connection_type.c | 66 ++++++++-- tests/test_connection.py | 257 ++++++++++++++++++++++++++------------ 5 files changed, 256 insertions(+), 108 deletions(-) diff --git a/NEWS b/NEWS index 33461e51..b48e932c 100644 --- a/NEWS +++ b/NEWS @@ -39,6 +39,7 @@ New features: control the session characteristics as it may create problems with external connection pools such as pgbouncer; use :sql:`BEGIN` options instead (:ticket:`#503`). +- `~connection.isolation_level` is now writable. Bug fixes: diff --git a/doc/src/connection.rst b/doc/src/connection.rst index 2adad597..c2a0c5ce 100644 --- a/doc/src/connection.rst +++ b/doc/src/connection.rst @@ -454,6 +454,28 @@ The ``connection`` class .. versionadded:: 2.4.2 + .. attribute:: isolation_level + + Return or set the `transaction isolation level`_ for the current + session. The value is one of the :ref:`isolation-level-constants` + defined in the `psycopg2.extensions` module. On set it is also + possible to use one of the literal values ``READ UNCOMMITTED``, ``READ + COMMITTED``, ``REPEATABLE READ``, ``SERIALIZABLE``, ``DEFAULT``. + + .. versionchanged:: 2.7 + + the property is writable. + + .. versionchanged:: 2.7 + + the default value for `!isolation_level` is + `~psycopg2.extensions.ISOLATION_LEVEL_DEFAULT`; previously the + property would have queried the server and returned the real value + applied. To know this value you can run a query such as :sql:`show + transaction_isolation`. Usually the default value is `READ + COMMITTED`, but this may be changed in the server configuration. + + .. method:: set_isolation_level(level) .. note:: @@ -486,22 +508,6 @@ The ``connection`` class an exception. - .. attribute:: isolation_level - - Read the `transaction isolation level`_ for the current session. The - value is one of the :ref:`isolation-level-constants` defined in the - `psycopg2.extensions` module. - - .. versionchanged:: 2.7 - - the default value for `!isolation_level` is - `~psycopg2.extensions.ISOLATION_LEVEL_DEFAULT`; previously the - property would have queried the server and returned the real value - applied. To know this value you can run a query such as :sql:`show - transaction_isolation`. Usually the default value is `READ - COMMITTED`, but this may be changed in the server configuration. - - .. index:: pair: Client; Encoding diff --git a/lib/extensions.py b/lib/extensions.py index f4dc706f..9fee5510 100644 --- a/lib/extensions.py +++ b/lib/extensions.py @@ -72,7 +72,7 @@ ISOLATION_LEVEL_READ_UNCOMMITTED = 4 ISOLATION_LEVEL_READ_COMMITTED = 1 ISOLATION_LEVEL_REPEATABLE_READ = 2 ISOLATION_LEVEL_SERIALIZABLE = 3 -ISOLATION_LEVEL_DEFAULT = 5 +ISOLATION_LEVEL_DEFAULT = None """psycopg connection status values.""" diff --git a/psycopg/connection_type.c b/psycopg/connection_type.c index 7647056b..b09fdde7 100644 --- a/psycopg/connection_type.c +++ b/psycopg/connection_type.c @@ -455,8 +455,14 @@ _psyco_conn_parse_isolevel(PyObject *pyval) Py_INCREF(pyval); /* for ensure_bytes */ + /* None is default. This is only used when setting the property, because + * set_session() has None used as "don't change" */ + if (pyval == Py_None) { + rv = ISOLATION_LEVEL_DEFAULT; + } + /* parse from one of the level constants */ - if (PyInt_Check(pyval)) { + else if (PyInt_Check(pyval)) { level = PyInt_AsLong(pyval); if (level == -1 && PyErr_Occurred()) { goto exit; } if (level < 1 || level > 4) { @@ -469,7 +475,6 @@ _psyco_conn_parse_isolevel(PyObject *pyval) } /* parse from the string -- this includes "default" */ - else { if (!(pyval = psycopg_ensure_bytes(pyval))) { goto exit; @@ -617,11 +622,11 @@ psyco_conn_autocommit_get(connectionObject *self) } BORROWED static PyObject * -_psyco_conn_autocommit_set_checks(connectionObject *self) +_psyco_set_session_check_setter_wrapper(connectionObject *self) { /* wrapper to use the EXC_IF macros. * return NULL in case of error, else whatever */ - _set_session_checks(self, autocommit); + _set_session_checks(self, set_session); return Py_None; /* borrowed */ } @@ -630,7 +635,7 @@ psyco_conn_autocommit_set(connectionObject *self, PyObject *pyvalue) { int value; - if (!_psyco_conn_autocommit_set_checks(self)) { return -1; } + if (!_psyco_set_session_check_setter_wrapper(self)) { return -1; } if (-1 == (value = PyObject_IsTrue(pyvalue))) { return -1; } if (0 > conn_set_session(self, value, self->isolevel, self->readonly, self->deferrable)) { @@ -643,6 +648,9 @@ psyco_conn_autocommit_set(connectionObject *self, PyObject *pyvalue) /* isolation_level - return the current isolation level */ +#define psyco_conn_isolation_level_doc \ +"Set or return the connection transaction isolation level." + static PyObject * psyco_conn_isolation_level_get(connectionObject *self) { @@ -653,7 +661,29 @@ psyco_conn_isolation_level_get(connectionObject *self) rv = conn_get_isolation_level(self); if (-1 == rv) { return NULL; } - return PyInt_FromLong((long)rv); + if (ISOLATION_LEVEL_DEFAULT == rv) { + Py_RETURN_NONE; + } else { + return PyInt_FromLong((long)rv); + } +} + + +/* isolation_level - set a new isolation level */ + +static int +psyco_conn_isolation_level_set(connectionObject *self, PyObject *pyvalue) +{ + int value; + + if (!_psyco_set_session_check_setter_wrapper(self)) { return -1; } + if (0 > (value = _psyco_conn_parse_isolevel(pyvalue))) { return -1; } + if (0 > conn_set_session(self, self->autocommit, + value, self->readonly, self->deferrable)) { + return -1; + } + + return 0; } @@ -666,15 +696,25 @@ static PyObject * psyco_conn_set_isolation_level(connectionObject *self, PyObject *args) { int level = 1; + PyObject *pyval = NULL; _set_session_checks(self, set_isolation_level); - if (!PyArg_ParseTuple(args, "i", &level)) return NULL; + if (!PyArg_ParseTuple(args, "O", &pyval)) return NULL; - if (level < 0 || level > 5) { - PyErr_SetString(PyExc_ValueError, - "isolation level must be between 0 and 4"); - return NULL; + if (pyval == Py_None) { + level = ISOLATION_LEVEL_DEFAULT; + } + + /* parse from one of the level constants */ + else if (PyInt_Check(pyval)) { + level = PyInt_AsLong(pyval); + + if (level < 0 || level > 4) { + PyErr_SetString(PyExc_ValueError, + "isolation level must be between 0 and 4"); + return NULL; + } } if (level == 0) { @@ -1103,8 +1143,8 @@ static struct PyGetSetDef connectionObject_getsets[] = { psyco_conn_autocommit_doc }, { "isolation_level", (getter)psyco_conn_isolation_level_get, - (setter)NULL, - "The current isolation level." }, + (setter)psyco_conn_isolation_level_set, + psyco_conn_isolation_level_doc }, {NULL} }; #undef EXCEPTION_GETTER diff --git a/tests/test_connection.py b/tests/test_connection.py index 703d8f14..da0a588c 100755 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -222,7 +222,7 @@ class ConnectionTests(ConnectingTestCase): self.conn.set_client_encoding("EUC_JP") # conn.encoding is 'EUCJP' now. cur = self.conn.cursor() - psycopg2.extensions.register_type(psycopg2.extensions.UNICODE, cur) + ext.register_type(ext.UNICODE, cur) cur.execute("select 'foo'::text;") self.assertEqual(cur.fetchone()[0], u'foo') @@ -308,14 +308,14 @@ class ConnectionTests(ConnectingTestCase): # issue #210 conn = self.connect() cur = conn.cursor(cursor_factory=None) - self.assertEqual(type(cur), psycopg2.extensions.cursor) + self.assertEqual(type(cur), ext.cursor) conn = self.connect(cursor_factory=psycopg2.extras.DictCursor) cur = conn.cursor(cursor_factory=None) self.assertEqual(type(cur), psycopg2.extras.DictCursor) def test_failed_init_status(self): - class SubConnection(psycopg2.extensions.connection): + class SubConnection(ext.connection): def __init__(self, dsn): try: super(SubConnection, self).__init__(dsn) @@ -488,23 +488,23 @@ class IsolationLevelsTestCase(ConnectingTestCase): conn = self.connect() self.assertEqual( conn.isolation_level, - psycopg2.extensions.ISOLATION_LEVEL_DEFAULT) + ext.ISOLATION_LEVEL_DEFAULT) def test_encoding(self): conn = self.connect() - self.assert_(conn.encoding in psycopg2.extensions.encodings) + self.assert_(conn.encoding in ext.encodings) def test_set_isolation_level(self): conn = self.connect() curs = conn.cursor() levels = [ - (None, psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT), + (None, ext.ISOLATION_LEVEL_AUTOCOMMIT), ('read uncommitted', - psycopg2.extensions.ISOLATION_LEVEL_READ_UNCOMMITTED), - ('read committed', psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED), - ('repeatable read', psycopg2.extensions.ISOLATION_LEVEL_REPEATABLE_READ), - ('serializable', psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE), + ext.ISOLATION_LEVEL_READ_UNCOMMITTED), + ('read committed', ext.ISOLATION_LEVEL_READ_COMMITTED), + ('repeatable read', ext.ISOLATION_LEVEL_REPEATABLE_READ), + ('serializable', ext.ISOLATION_LEVEL_SERIALIZABLE), ] for name, level in levels: conn.set_isolation_level(level) @@ -512,8 +512,8 @@ class IsolationLevelsTestCase(ConnectingTestCase): # the only values available on prehistoric PG versions if conn.server_version < 80000: if level in ( - psycopg2.extensions.ISOLATION_LEVEL_READ_UNCOMMITTED, - psycopg2.extensions.ISOLATION_LEVEL_REPEATABLE_READ): + ext.ISOLATION_LEVEL_READ_UNCOMMITTED, + ext.ISOLATION_LEVEL_REPEATABLE_READ): name, level = levels[levels.index((name, level)) + 1] self.assertEqual(conn.isolation_level, level) @@ -529,7 +529,7 @@ class IsolationLevelsTestCase(ConnectingTestCase): conn.commit() self.assertRaises(ValueError, conn.set_isolation_level, -1) - self.assertRaises(ValueError, conn.set_isolation_level, 6) + self.assertRaises(ValueError, conn.set_isolation_level, 5) def test_set_isolation_level_default(self): conn = self.connect() @@ -539,14 +539,14 @@ class IsolationLevelsTestCase(ConnectingTestCase): curs.execute("set default_transaction_isolation to 'read committed'") conn.autocommit = False - conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE) + conn.set_isolation_level(ext.ISOLATION_LEVEL_SERIALIZABLE) self.assertEqual(conn.isolation_level, - psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE) + ext.ISOLATION_LEVEL_SERIALIZABLE) curs.execute("show transaction_isolation") self.assertEqual(curs.fetchone()[0], "serializable") conn.rollback() - conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_DEFAULT) + conn.set_isolation_level(ext.ISOLATION_LEVEL_DEFAULT) curs.execute("show transaction_isolation") self.assertEqual(curs.fetchone()[0], "read committed") @@ -554,25 +554,25 @@ class IsolationLevelsTestCase(ConnectingTestCase): conn = self.connect() cur = conn.cursor() - self.assertEqual(psycopg2.extensions.TRANSACTION_STATUS_IDLE, + self.assertEqual(ext.TRANSACTION_STATUS_IDLE, conn.get_transaction_status()) cur.execute("insert into isolevel values (10);") - self.assertEqual(psycopg2.extensions.TRANSACTION_STATUS_INTRANS, + self.assertEqual(ext.TRANSACTION_STATUS_INTRANS, conn.get_transaction_status()) # changed in psycopg 2.7 self.assertRaises(psycopg2.ProgrammingError, conn.set_isolation_level, - psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE) - self.assertEqual(psycopg2.extensions.TRANSACTION_STATUS_INTRANS, + ext.ISOLATION_LEVEL_SERIALIZABLE) + self.assertEqual(ext.TRANSACTION_STATUS_INTRANS, conn.get_transaction_status()) self.assertEqual(conn.isolation_level, - psycopg2.extensions.ISOLATION_LEVEL_DEFAULT) + ext.ISOLATION_LEVEL_DEFAULT) def test_isolation_level_autocommit(self): cnn1 = self.connect() cnn2 = self.connect() - cnn2.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) + cnn2.set_isolation_level(ext.ISOLATION_LEVEL_AUTOCOMMIT) cur1 = cnn1.cursor() cur1.execute("select count(*) from isolevel;") @@ -588,7 +588,7 @@ class IsolationLevelsTestCase(ConnectingTestCase): def test_isolation_level_read_committed(self): cnn1 = self.connect() cnn2 = self.connect() - cnn2.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED) + cnn2.set_isolation_level(ext.ISOLATION_LEVEL_READ_COMMITTED) cur1 = cnn1.cursor() cur1.execute("select count(*) from isolevel;") @@ -614,7 +614,7 @@ class IsolationLevelsTestCase(ConnectingTestCase): def test_isolation_level_serializable(self): cnn1 = self.connect() cnn2 = self.connect() - cnn2.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE) + cnn2.set_isolation_level(ext.ISOLATION_LEVEL_SERIALIZABLE) cur1 = cnn1.cursor() cur1.execute("select count(*) from isolevel;") @@ -650,6 +650,107 @@ class IsolationLevelsTestCase(ConnectingTestCase): self.assertRaises(psycopg2.InterfaceError, cnn.set_isolation_level, 1) + def test_setattr_isolation_level_int(self): + cur = self.conn.cursor() + self.conn.isolation_level = ext.ISOLATION_LEVEL_SERIALIZABLE + self.assertEqual(self.conn.isolation_level, ext.ISOLATION_LEVEL_SERIALIZABLE) + + cur.execute("SHOW transaction_isolation;") + self.assertEqual(cur.fetchone()[0], 'serializable') + self.conn.rollback() + + self.conn.isolation_level = ext.ISOLATION_LEVEL_REPEATABLE_READ + cur.execute("SHOW transaction_isolation;") + if self.conn.server_version > 80000: + self.assertEqual(self.conn.isolation_level, + ext.ISOLATION_LEVEL_REPEATABLE_READ) + self.assertEqual(cur.fetchone()[0], 'repeatable read') + else: + self.assertEqual(self.conn.isolation_level, + ext.ISOLATION_LEVEL_SERIALIZABLE) + self.assertEqual(cur.fetchone()[0], 'serializable') + self.conn.rollback() + + self.conn.isolation_level = ext.ISOLATION_LEVEL_READ_COMMITTED + self.assertEqual(self.conn.isolation_level, + ext.ISOLATION_LEVEL_READ_COMMITTED) + cur.execute("SHOW transaction_isolation;") + self.assertEqual(cur.fetchone()[0], 'read committed') + self.conn.rollback() + + self.conn.isolation_level = ext.ISOLATION_LEVEL_READ_UNCOMMITTED + cur.execute("SHOW transaction_isolation;") + if self.conn.server_version > 80000: + self.assertEqual(self.conn.isolation_level, + ext.ISOLATION_LEVEL_READ_UNCOMMITTED) + self.assertEqual(cur.fetchone()[0], 'read uncommitted') + else: + self.assertEqual(self.conn.isolation_level, + ext.ISOLATION_LEVEL_READ_COMMITTED) + self.assertEqual(cur.fetchone()[0], 'read committed') + self.conn.rollback() + + self.assertEqual(ext.ISOLATION_LEVEL_DEFAULT, None) + self.conn.isolation_level = ext.ISOLATION_LEVEL_DEFAULT + self.assertEqual(self.conn.isolation_level, None) + cur.execute("SHOW transaction_isolation;") + isol = cur.fetchone()[0] + cur.execute("SHOW default_transaction_isolation;") + self.assertEqual(cur.fetchone()[0], isol) + + def test_setattr_isolation_level_str(self): + cur = self.conn.cursor() + self.conn.isolation_level = "serializable" + self.assertEqual(self.conn.isolation_level, ext.ISOLATION_LEVEL_SERIALIZABLE) + + cur.execute("SHOW transaction_isolation;") + self.assertEqual(cur.fetchone()[0], 'serializable') + self.conn.rollback() + + self.conn.isolation_level = "repeatable read" + cur.execute("SHOW transaction_isolation;") + if self.conn.server_version > 80000: + self.assertEqual(self.conn.isolation_level, + ext.ISOLATION_LEVEL_REPEATABLE_READ) + self.assertEqual(cur.fetchone()[0], 'repeatable read') + else: + self.assertEqual(self.conn.isolation_level, + ext.ISOLATION_LEVEL_SERIALIZABLE) + self.assertEqual(cur.fetchone()[0], 'serializable') + self.conn.rollback() + + self.conn.isolation_level = "read committed" + self.assertEqual(self.conn.isolation_level, + ext.ISOLATION_LEVEL_READ_COMMITTED) + cur.execute("SHOW transaction_isolation;") + self.assertEqual(cur.fetchone()[0], 'read committed') + self.conn.rollback() + + self.conn.isolation_level = "read uncommitted" + cur.execute("SHOW transaction_isolation;") + if self.conn.server_version > 80000: + self.assertEqual(self.conn.isolation_level, + ext.ISOLATION_LEVEL_READ_UNCOMMITTED) + self.assertEqual(cur.fetchone()[0], 'read uncommitted') + else: + self.assertEqual(self.conn.isolation_level, + ext.ISOLATION_LEVEL_READ_COMMITTED) + self.assertEqual(cur.fetchone()[0], 'read committed') + self.conn.rollback() + + self.conn.isolation_level = "default" + self.assertEqual(self.conn.isolation_level, None) + cur.execute("SHOW transaction_isolation;") + isol = cur.fetchone()[0] + cur.execute("SHOW default_transaction_isolation;") + self.assertEqual(cur.fetchone()[0], isol) + + def test_setattr_isolation_level_invalid(self): + self.assertRaises(ValueError, setattr, self.conn, 'isolation_level', 0) + self.assertRaises(ValueError, setattr, self.conn, 'isolation_level', -1) + self.assertRaises(ValueError, setattr, self.conn, 'isolation_level', 5) + self.assertRaises(ValueError, setattr, self.conn, 'isolation_level', 'bah') + class ConnectionTwoPhaseTests(ConnectingTestCase): def setUp(self): @@ -716,10 +817,10 @@ class ConnectionTwoPhaseTests(ConnectingTestCase): def test_tpc_commit(self): cnn = self.connect() xid = cnn.xid(1, "gtrid", "bqual") - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(cnn.status, ext.STATUS_READY) cnn.tpc_begin(xid) - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_BEGIN) + self.assertEqual(cnn.status, ext.STATUS_BEGIN) cur = cnn.cursor() cur.execute("insert into test_tpc values ('test_tpc_commit');") @@ -727,22 +828,22 @@ class ConnectionTwoPhaseTests(ConnectingTestCase): self.assertEqual(0, self.count_test_records()) cnn.tpc_prepare() - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_PREPARED) + self.assertEqual(cnn.status, ext.STATUS_PREPARED) self.assertEqual(1, self.count_xacts()) self.assertEqual(0, self.count_test_records()) cnn.tpc_commit() - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(cnn.status, ext.STATUS_READY) self.assertEqual(0, self.count_xacts()) self.assertEqual(1, self.count_test_records()) def test_tpc_commit_one_phase(self): cnn = self.connect() xid = cnn.xid(1, "gtrid", "bqual") - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(cnn.status, ext.STATUS_READY) cnn.tpc_begin(xid) - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_BEGIN) + self.assertEqual(cnn.status, ext.STATUS_BEGIN) cur = cnn.cursor() cur.execute("insert into test_tpc values ('test_tpc_commit_1p');") @@ -750,17 +851,17 @@ class ConnectionTwoPhaseTests(ConnectingTestCase): self.assertEqual(0, self.count_test_records()) cnn.tpc_commit() - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(cnn.status, ext.STATUS_READY) self.assertEqual(0, self.count_xacts()) self.assertEqual(1, self.count_test_records()) def test_tpc_commit_recovered(self): cnn = self.connect() xid = cnn.xid(1, "gtrid", "bqual") - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(cnn.status, ext.STATUS_READY) cnn.tpc_begin(xid) - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_BEGIN) + self.assertEqual(cnn.status, ext.STATUS_BEGIN) cur = cnn.cursor() cur.execute("insert into test_tpc values ('test_tpc_commit_rec');") @@ -776,17 +877,17 @@ class ConnectionTwoPhaseTests(ConnectingTestCase): xid = cnn.xid(1, "gtrid", "bqual") cnn.tpc_commit(xid) - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(cnn.status, ext.STATUS_READY) self.assertEqual(0, self.count_xacts()) self.assertEqual(1, self.count_test_records()) def test_tpc_rollback(self): cnn = self.connect() xid = cnn.xid(1, "gtrid", "bqual") - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(cnn.status, ext.STATUS_READY) cnn.tpc_begin(xid) - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_BEGIN) + self.assertEqual(cnn.status, ext.STATUS_BEGIN) cur = cnn.cursor() cur.execute("insert into test_tpc values ('test_tpc_rollback');") @@ -794,22 +895,22 @@ class ConnectionTwoPhaseTests(ConnectingTestCase): self.assertEqual(0, self.count_test_records()) cnn.tpc_prepare() - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_PREPARED) + self.assertEqual(cnn.status, ext.STATUS_PREPARED) self.assertEqual(1, self.count_xacts()) self.assertEqual(0, self.count_test_records()) cnn.tpc_rollback() - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(cnn.status, ext.STATUS_READY) self.assertEqual(0, self.count_xacts()) self.assertEqual(0, self.count_test_records()) def test_tpc_rollback_one_phase(self): cnn = self.connect() xid = cnn.xid(1, "gtrid", "bqual") - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(cnn.status, ext.STATUS_READY) cnn.tpc_begin(xid) - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_BEGIN) + self.assertEqual(cnn.status, ext.STATUS_BEGIN) cur = cnn.cursor() cur.execute("insert into test_tpc values ('test_tpc_rollback_1p');") @@ -817,17 +918,17 @@ class ConnectionTwoPhaseTests(ConnectingTestCase): self.assertEqual(0, self.count_test_records()) cnn.tpc_rollback() - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(cnn.status, ext.STATUS_READY) self.assertEqual(0, self.count_xacts()) self.assertEqual(0, self.count_test_records()) def test_tpc_rollback_recovered(self): cnn = self.connect() xid = cnn.xid(1, "gtrid", "bqual") - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(cnn.status, ext.STATUS_READY) cnn.tpc_begin(xid) - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_BEGIN) + self.assertEqual(cnn.status, ext.STATUS_BEGIN) cur = cnn.cursor() cur.execute("insert into test_tpc values ('test_tpc_commit_rec');") @@ -843,21 +944,21 @@ class ConnectionTwoPhaseTests(ConnectingTestCase): xid = cnn.xid(1, "gtrid", "bqual") cnn.tpc_rollback(xid) - self.assertEqual(cnn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(cnn.status, ext.STATUS_READY) self.assertEqual(0, self.count_xacts()) self.assertEqual(0, self.count_test_records()) def test_status_after_recover(self): cnn = self.connect() - self.assertEqual(psycopg2.extensions.STATUS_READY, cnn.status) + self.assertEqual(ext.STATUS_READY, cnn.status) cnn.tpc_recover() - self.assertEqual(psycopg2.extensions.STATUS_READY, cnn.status) + self.assertEqual(ext.STATUS_READY, cnn.status) cur = cnn.cursor() cur.execute("select 1") - self.assertEqual(psycopg2.extensions.STATUS_BEGIN, cnn.status) + self.assertEqual(ext.STATUS_BEGIN, cnn.status) cnn.tpc_recover() - self.assertEqual(psycopg2.extensions.STATUS_BEGIN, cnn.status) + self.assertEqual(ext.STATUS_BEGIN, cnn.status) def test_recovered_xids(self): # insert a few test xns @@ -1030,25 +1131,25 @@ class TransactionControlTests(ConnectingTestCase): self.conn.close() self.assertRaises(psycopg2.InterfaceError, self.conn.set_session, - psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE) + ext.ISOLATION_LEVEL_SERIALIZABLE) def test_not_in_transaction(self): cur = self.conn.cursor() cur.execute("select 1") self.assertRaises(psycopg2.ProgrammingError, self.conn.set_session, - psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE) + ext.ISOLATION_LEVEL_SERIALIZABLE) def test_set_isolation_level(self): cur = self.conn.cursor() self.conn.set_session( - psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE) + ext.ISOLATION_LEVEL_SERIALIZABLE) cur.execute("SHOW transaction_isolation;") self.assertEqual(cur.fetchone()[0], 'serializable') self.conn.rollback() self.conn.set_session( - psycopg2.extensions.ISOLATION_LEVEL_REPEATABLE_READ) + ext.ISOLATION_LEVEL_REPEATABLE_READ) cur.execute("SHOW transaction_isolation;") if self.conn.server_version > 80000: self.assertEqual(cur.fetchone()[0], 'repeatable read') @@ -1057,13 +1158,13 @@ class TransactionControlTests(ConnectingTestCase): self.conn.rollback() self.conn.set_session( - isolation_level=psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED) + isolation_level=ext.ISOLATION_LEVEL_READ_COMMITTED) cur.execute("SHOW transaction_isolation;") self.assertEqual(cur.fetchone()[0], 'read committed') self.conn.rollback() self.conn.set_session( - isolation_level=psycopg2.extensions.ISOLATION_LEVEL_READ_UNCOMMITTED) + isolation_level=ext.ISOLATION_LEVEL_READ_UNCOMMITTED) cur.execute("SHOW transaction_isolation;") if self.conn.server_version > 80000: self.assertEqual(cur.fetchone()[0], 'read uncommitted') @@ -1183,44 +1284,44 @@ class AutocommitTests(ConnectingTestCase): def test_default_no_autocommit(self): self.assert_(not self.conn.autocommit) - self.assertEqual(self.conn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(self.conn.status, ext.STATUS_READY) self.assertEqual(self.conn.get_transaction_status(), - psycopg2.extensions.TRANSACTION_STATUS_IDLE) + ext.TRANSACTION_STATUS_IDLE) cur = self.conn.cursor() cur.execute('select 1;') - self.assertEqual(self.conn.status, psycopg2.extensions.STATUS_BEGIN) + self.assertEqual(self.conn.status, ext.STATUS_BEGIN) self.assertEqual(self.conn.get_transaction_status(), - psycopg2.extensions.TRANSACTION_STATUS_INTRANS) + ext.TRANSACTION_STATUS_INTRANS) self.conn.rollback() - self.assertEqual(self.conn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(self.conn.status, ext.STATUS_READY) self.assertEqual(self.conn.get_transaction_status(), - psycopg2.extensions.TRANSACTION_STATUS_IDLE) + ext.TRANSACTION_STATUS_IDLE) def test_set_autocommit(self): self.conn.autocommit = True self.assert_(self.conn.autocommit) - self.assertEqual(self.conn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(self.conn.status, ext.STATUS_READY) self.assertEqual(self.conn.get_transaction_status(), - psycopg2.extensions.TRANSACTION_STATUS_IDLE) + ext.TRANSACTION_STATUS_IDLE) cur = self.conn.cursor() cur.execute('select 1;') - self.assertEqual(self.conn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(self.conn.status, ext.STATUS_READY) self.assertEqual(self.conn.get_transaction_status(), - psycopg2.extensions.TRANSACTION_STATUS_IDLE) + ext.TRANSACTION_STATUS_IDLE) self.conn.autocommit = False self.assert_(not self.conn.autocommit) - self.assertEqual(self.conn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(self.conn.status, ext.STATUS_READY) self.assertEqual(self.conn.get_transaction_status(), - psycopg2.extensions.TRANSACTION_STATUS_IDLE) + ext.TRANSACTION_STATUS_IDLE) cur.execute('select 1;') - self.assertEqual(self.conn.status, psycopg2.extensions.STATUS_BEGIN) + self.assertEqual(self.conn.status, ext.STATUS_BEGIN) self.assertEqual(self.conn.get_transaction_status(), - psycopg2.extensions.TRANSACTION_STATUS_INTRANS) + ext.TRANSACTION_STATUS_INTRANS) def test_set_intrans_error(self): cur = self.conn.cursor() @@ -1231,34 +1332,34 @@ class AutocommitTests(ConnectingTestCase): def test_set_session_autocommit(self): self.conn.set_session(autocommit=True) self.assert_(self.conn.autocommit) - self.assertEqual(self.conn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(self.conn.status, ext.STATUS_READY) self.assertEqual(self.conn.get_transaction_status(), - psycopg2.extensions.TRANSACTION_STATUS_IDLE) + ext.TRANSACTION_STATUS_IDLE) cur = self.conn.cursor() cur.execute('select 1;') - self.assertEqual(self.conn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(self.conn.status, ext.STATUS_READY) self.assertEqual(self.conn.get_transaction_status(), - psycopg2.extensions.TRANSACTION_STATUS_IDLE) + ext.TRANSACTION_STATUS_IDLE) self.conn.set_session(autocommit=False) self.assert_(not self.conn.autocommit) - self.assertEqual(self.conn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(self.conn.status, ext.STATUS_READY) self.assertEqual(self.conn.get_transaction_status(), - psycopg2.extensions.TRANSACTION_STATUS_IDLE) + ext.TRANSACTION_STATUS_IDLE) cur.execute('select 1;') - self.assertEqual(self.conn.status, psycopg2.extensions.STATUS_BEGIN) + self.assertEqual(self.conn.status, ext.STATUS_BEGIN) self.assertEqual(self.conn.get_transaction_status(), - psycopg2.extensions.TRANSACTION_STATUS_INTRANS) + ext.TRANSACTION_STATUS_INTRANS) self.conn.rollback() self.conn.set_session('serializable', readonly=True, autocommit=True) self.assert_(self.conn.autocommit) cur.execute('select 1;') - self.assertEqual(self.conn.status, psycopg2.extensions.STATUS_READY) + self.assertEqual(self.conn.status, ext.STATUS_READY) self.assertEqual(self.conn.get_transaction_status(), - psycopg2.extensions.TRANSACTION_STATUS_IDLE) + ext.TRANSACTION_STATUS_IDLE) cur.execute("SHOW transaction_isolation;") self.assertEqual(cur.fetchone()[0], 'serializable') cur.execute("SHOW transaction_read_only;") From b5d80b609d6f1b0922ed51d6182ff137f1b3ba3e Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Thu, 16 Feb 2017 02:40:21 +0000 Subject: [PATCH 2/5] Revert pre-2.7b1 behaviour of silent rollback on conn.set_isolation_level() Legacy method is legacy. --- NEWS | 2 -- doc/src/connection.rst | 11 +++-------- psycopg/connection_type.c | 8 +++++++- tests/test_connection.py | 32 ++++++++++++++++++++++++++------ 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/NEWS b/NEWS index b48e932c..9e27a445 100644 --- a/NEWS +++ b/NEWS @@ -54,8 +54,6 @@ Other changes: - `~connection.isolation_level` doesn't read from the database but will return `~psycopg2.extensions.ISOLATION_LEVEL_DEFAULT` if no value was set on the connection. -- `~connection.set_isolation_level()` will throw an exception if executed - inside a transaction; previously it would have silently rolled it back. - Empty arrays no more converted into lists if they don't have a type attached (:ticket:`#506`) diff --git a/doc/src/connection.rst b/doc/src/connection.rst index c2a0c5ce..d554ce6d 100644 --- a/doc/src/connection.rst +++ b/doc/src/connection.rst @@ -480,8 +480,9 @@ The ``connection`` class .. note:: - From version 2.4.2, `set_session()` and `autocommit` offer - finer control on the transaction characteristics. + This is a legacy method mixing `~conn.isolation_level` and + `~conn.autocommit`. Using the respective properties is a better + option. Set the `transaction isolation level`_ for the current session. The level defines the different phenomena that can happen in the @@ -501,12 +502,6 @@ The ``connection`` class See also :ref:`transactions-control`. - .. versionchanged:: 2.7 - - the function must be called outside a transaction; previously it - would have executed an implicit :sql:`ROLLBACK`; it will now raise - an exception. - .. index:: pair: Client; Encoding diff --git a/psycopg/connection_type.c b/psycopg/connection_type.c index b09fdde7..df67c802 100644 --- a/psycopg/connection_type.c +++ b/psycopg/connection_type.c @@ -698,7 +698,9 @@ psyco_conn_set_isolation_level(connectionObject *self, PyObject *args) int level = 1; PyObject *pyval = NULL; - _set_session_checks(self, set_isolation_level); + EXC_IF_CONN_CLOSED(self); + EXC_IF_CONN_ASYNC(self, "isolation_level"); + EXC_IF_TPC_PREPARED(self, "isolation_level"); if (!PyArg_ParseTuple(args, "O", &pyval)) return NULL; @@ -717,6 +719,10 @@ psyco_conn_set_isolation_level(connectionObject *self, PyObject *args) } } + if (0 > conn_rollback(self)) { + return NULL; + } + if (level == 0) { if (0 > conn_set_session(self, 1, ISOLATION_LEVEL_DEFAULT, self->readonly, self->deferrable)) { diff --git a/tests/test_connection.py b/tests/test_connection.py index da0a588c..1d11ff16 100755 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -560,14 +560,34 @@ class IsolationLevelsTestCase(ConnectingTestCase): self.assertEqual(ext.TRANSACTION_STATUS_INTRANS, conn.get_transaction_status()) - # changed in psycopg 2.7 - self.assertRaises(psycopg2.ProgrammingError, - conn.set_isolation_level, - ext.ISOLATION_LEVEL_SERIALIZABLE) - self.assertEqual(ext.TRANSACTION_STATUS_INTRANS, + conn.set_isolation_level( + psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE) + self.assertEqual(psycopg2.extensions.TRANSACTION_STATUS_IDLE, conn.get_transaction_status()) + cur.execute("select count(*) from isolevel;") + self.assertEqual(0, cur.fetchone()[0]) + + cur.execute("insert into isolevel values (10);") + self.assertEqual(psycopg2.extensions.TRANSACTION_STATUS_INTRANS, + conn.get_transaction_status()) + conn.set_isolation_level( + psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) + self.assertEqual(psycopg2.extensions.TRANSACTION_STATUS_IDLE, + conn.get_transaction_status()) + cur.execute("select count(*) from isolevel;") + self.assertEqual(0, cur.fetchone()[0]) + + cur.execute("insert into isolevel values (10);") + self.assertEqual(psycopg2.extensions.TRANSACTION_STATUS_IDLE, + conn.get_transaction_status()) + conn.set_isolation_level( + psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED) + self.assertEqual(psycopg2.extensions.TRANSACTION_STATUS_IDLE, + conn.get_transaction_status()) + cur.execute("select count(*) from isolevel;") + self.assertEqual(1, cur.fetchone()[0]) self.assertEqual(conn.isolation_level, - ext.ISOLATION_LEVEL_DEFAULT) + psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED) def test_isolation_level_autocommit(self): cnn1 = self.connect() From d50ed488074a3b809bf98fb1e6971bc295ba31c3 Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Thu, 16 Feb 2017 11:04:02 +0000 Subject: [PATCH 3/5] Added readonly and deferrable attributes --- NEWS | 4 +- doc/src/connection.rst | 35 +++++++-- psycopg/connection.h | 1 - psycopg/connection_int.c | 16 +--- psycopg/connection_type.c | 127 ++++++++++++++++++++++++++++---- tests/test_async.py | 17 +++-- tests/test_async_keyword.py | 4 +- tests/test_connection.py | 141 ++++++++++++++++++++++++++++++------ 8 files changed, 277 insertions(+), 68 deletions(-) diff --git a/NEWS b/NEWS index 9e27a445..566a39bc 100644 --- a/NEWS +++ b/NEWS @@ -39,7 +39,9 @@ New features: control the session characteristics as it may create problems with external connection pools such as pgbouncer; use :sql:`BEGIN` options instead (:ticket:`#503`). -- `~connection.isolation_level` is now writable. +- `~connection.isolation_level` is now writable and entirely separated from + `~connection.autocommit`; added `~connection.readonly`, + `connection.deferrable` writable attributes. Bug fixes: diff --git a/doc/src/connection.rst b/doc/src/connection.rst index d554ce6d..53f908fe 100644 --- a/doc/src/connection.rst +++ b/doc/src/connection.rst @@ -386,12 +386,6 @@ The ``connection`` class The function must be invoked with no transaction in progress. - .. note:: - - There is currently no builtin method to read the current value for - the parameters: use :sql:`SHOW default_transaction_...` to read - the values from the backend. - .. seealso:: |SET TRANSACTION|_ for further details about the behaviour of the transaction parameters in the server. @@ -475,6 +469,35 @@ The ``connection`` class transaction_isolation`. Usually the default value is `READ COMMITTED`, but this may be changed in the server configuration. + This value is now entirely separate from the `autocommit` + property: in previous version, if `!autocommit` was set to `!True` + this property would have returned + `~psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT`; it will now + return the server isolation level. + + + .. attribute:: readonly + + Return or set the read-only status for the current session. Available + values are `!True` (new transactions will be in read-only mode), + `!False` (new transactions will be writable), `!None` (use the default + configured for the server by :sql:`default_transaction_read_only`). + + .. versionadded:: 2.7 + + + .. attribute:: deferrable + + Return or set the `deferrable status`__ for the current session. + Available values are `!True` (new transactions will be in deferrable + mode), `!False` (new transactions will be in non deferrable mode), + `!None` (use the default configured for the server by + :sql:`default_transaction_deferrable`). + + .. __: `SET TRANSACTION`_ + + .. versionadded:: 2.7 + .. method:: set_isolation_level(level) diff --git a/psycopg/connection.h b/psycopg/connection.h index 65efcaf0..6b1f2448 100644 --- a/psycopg/connection.h +++ b/psycopg/connection.h @@ -154,7 +154,6 @@ HIDDEN PyObject *conn_encode(connectionObject *self, PyObject *b); HIDDEN PyObject *conn_decode(connectionObject *self, const char *str, Py_ssize_t len); HIDDEN int conn_get_standard_conforming_strings(PGconn *pgconn); HIDDEN PyObject *conn_pgenc_to_pyenc(const char *encoding, char **clean_encoding); -RAISES_NEG HIDDEN int conn_get_isolation_level(connectionObject *self); HIDDEN int conn_get_protocol_version(PGconn *pgconn); HIDDEN int conn_get_server_version(PGconn *pgconn); HIDDEN void conn_notice_process(connectionObject *self); diff --git a/psycopg/connection_int.c b/psycopg/connection_int.c index e6906eb5..49daae5d 100644 --- a/psycopg/connection_int.c +++ b/psycopg/connection_int.c @@ -568,19 +568,6 @@ exit: } -RAISES_NEG int -conn_get_isolation_level(connectionObject *self) -{ - /* this may get called by async connections too: here's your result */ - if (self->autocommit) { - return ISOLATION_LEVEL_AUTOCOMMIT; - } - else { - return self->isolevel; - } -} - - int conn_get_protocol_version(PGconn *pgconn) { @@ -697,6 +684,9 @@ conn_setup(connectionObject *self, PGconn *pgconn) /* for reset */ self->autocommit = 0; + self->isolevel = ISOLATION_LEVEL_DEFAULT; + self->readonly = STATE_DEFAULT; + self->deferrable = STATE_DEFAULT; /* success */ rv = 0; diff --git a/psycopg/connection_type.c b/psycopg/connection_type.c index df67c802..66ff7050 100644 --- a/psycopg/connection_type.c +++ b/psycopg/connection_type.c @@ -510,7 +510,10 @@ _psyco_conn_parse_onoff(PyObject *pyval) Py_INCREF(pyval); /* for ensure_bytes */ - if (PyUnicode_CheckExact(pyval) || Bytes_CheckExact(pyval)) { + if (pyval == Py_None) { + rv = STATE_DEFAULT; + } + else if (PyUnicode_CheckExact(pyval) || Bytes_CheckExact(pyval)) { if (!(pyval = psycopg_ensure_bytes(pyval))) { goto exit; } @@ -591,7 +594,7 @@ psyco_conn_set_session(connectionObject *self, PyObject *args, PyObject *kwargs) " from PostgreSQL 9.1"); return NULL; } - if (0 > (c_deferrable = _psyco_conn_parse_onoff(readonly))) { + if (0 > (c_deferrable = _psyco_conn_parse_onoff(deferrable))) { return NULL; } } @@ -609,6 +612,8 @@ psyco_conn_set_session(connectionObject *self, PyObject *args, PyObject *kwargs) } +/* autocommit - return or set the current autocommit status */ + #define psyco_conn_autocommit_doc \ "Set or return the autocommit status." @@ -646,7 +651,7 @@ psyco_conn_autocommit_set(connectionObject *self, PyObject *pyvalue) } -/* isolation_level - return the current isolation level */ +/* isolation_level - return or set the current isolation level */ #define psyco_conn_isolation_level_doc \ "Set or return the connection transaction isolation level." @@ -654,23 +659,14 @@ psyco_conn_autocommit_set(connectionObject *self, PyObject *pyvalue) static PyObject * psyco_conn_isolation_level_get(connectionObject *self) { - int rv; - - EXC_IF_CONN_CLOSED(self); - EXC_IF_TPC_PREPARED(self, set_isolation_level); - - rv = conn_get_isolation_level(self); - if (-1 == rv) { return NULL; } - if (ISOLATION_LEVEL_DEFAULT == rv) { + if (self->isolevel == ISOLATION_LEVEL_DEFAULT) { Py_RETURN_NONE; } else { - return PyInt_FromLong((long)rv); + return PyInt_FromLong((long)self->isolevel); } } -/* isolation_level - set a new isolation level */ - static int psyco_conn_isolation_level_set(connectionObject *self, PyObject *pyvalue) { @@ -725,7 +721,7 @@ psyco_conn_set_isolation_level(connectionObject *self, PyObject *args) if (level == 0) { if (0 > conn_set_session(self, 1, - ISOLATION_LEVEL_DEFAULT, self->readonly, self->deferrable)) { + self->isolevel, self->readonly, self->deferrable)) { return NULL; } } @@ -739,6 +735,99 @@ psyco_conn_set_isolation_level(connectionObject *self, PyObject *args) Py_RETURN_NONE; } + +/* readonly - return or set the current read-only status */ + +#define psyco_conn_readonly_doc \ +"Set or return the connection read-only status." + +static PyObject * +psyco_conn_readonly_get(connectionObject *self) +{ + PyObject *rv = NULL; + + switch (self->readonly) { + case STATE_OFF: + rv = Py_False; + break; + case STATE_ON: + rv = Py_True; + break; + case STATE_DEFAULT: + rv = Py_None; + break; + default: + PyErr_Format(InternalError, + "bad internal value for readonly: %d", self->readonly); + break; + } + + return rv; +} + + +static int +psyco_conn_readonly_set(connectionObject *self, PyObject *pyvalue) +{ + int value; + + if (!_psyco_set_session_check_setter_wrapper(self)) { return -1; } + if (0 > (value = _psyco_conn_parse_onoff(pyvalue))) { return -1; } + if (0 > conn_set_session(self, self->autocommit, + self->isolevel, value, self->deferrable)) { + return -1; + } + + return 0; +} + + +/* deferrable - return or set the current deferrable status */ + +#define psyco_conn_deferrable_doc \ +"Set or return the connection deferrable status." + +static PyObject * +psyco_conn_deferrable_get(connectionObject *self) +{ + PyObject *rv = NULL; + + switch (self->deferrable) { + case STATE_OFF: + rv = Py_False; + break; + case STATE_ON: + rv = Py_True; + break; + case STATE_DEFAULT: + rv = Py_None; + break; + default: + PyErr_Format(InternalError, + "bad internal value for deferrable: %d", self->deferrable); + break; + } + + return rv; +} + + +static int +psyco_conn_deferrable_set(connectionObject *self, PyObject *pyvalue) +{ + int value; + + if (!_psyco_set_session_check_setter_wrapper(self)) { return -1; } + if (0 > (value = _psyco_conn_parse_onoff(pyvalue))) { return -1; } + if (0 > conn_set_session(self, self->autocommit, + self->isolevel, self->readonly, value)) { + return -1; + } + + return 0; +} + + /* set_client_encoding method - set client encoding */ #define psyco_conn_set_client_encoding_doc \ @@ -1151,6 +1240,14 @@ static struct PyGetSetDef connectionObject_getsets[] = { (getter)psyco_conn_isolation_level_get, (setter)psyco_conn_isolation_level_set, psyco_conn_isolation_level_doc }, + { "readonly", + (getter)psyco_conn_readonly_get, + (setter)psyco_conn_readonly_set, + psyco_conn_readonly_doc }, + { "deferrable", + (getter)psyco_conn_deferrable_get, + (setter)psyco_conn_deferrable_set, + psyco_conn_deferrable_doc }, {NULL} }; #undef EXCEPTION_GETTER diff --git a/tests/test_async.py b/tests/test_async.py index 63a55138..0a386d3e 100755 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -26,7 +26,7 @@ from testutils import unittest, skip_before_postgres, slow import psycopg2 -from psycopg2 import extensions +from psycopg2 import extensions as ext import time import StringIO @@ -74,13 +74,14 @@ class AsyncTests(ConnectingTestCase): self.assert_(self.conn.async_) self.assert_(not self.sync_conn.async_) - # the async connection should be in isolevel 0 - self.assertEquals(self.conn.isolation_level, 0) + # the async connection should be autocommit + self.assert_(self.conn.autocommit) + self.assertEquals(self.conn.isolation_level, ext.ISOLATION_LEVEL_DEFAULT) # check other properties to be found on the connection self.assert_(self.conn.server_version) self.assert_(self.conn.protocol_version in (2, 3)) - self.assert_(self.conn.encoding in psycopg2.extensions.encodings) + self.assert_(self.conn.encoding in ext.encodings) def test_async_named_cursor(self): self.assertRaises(psycopg2.ProgrammingError, @@ -192,7 +193,7 @@ class AsyncTests(ConnectingTestCase): # getting transaction status works self.assertEquals(self.conn.get_transaction_status(), - extensions.TRANSACTION_STATUS_ACTIVE) + ext.TRANSACTION_STATUS_ACTIVE) self.assertTrue(self.conn.isexecuting()) # setting connection encoding should fail @@ -311,9 +312,9 @@ class AsyncTests(ConnectingTestCase): self.assertEquals(cur.fetchone()[0], "b" * 10000) def test_async_subclass(self): - class MyConn(psycopg2.extensions.connection): + class MyConn(ext.connection): def __init__(self, dsn, async_=0): - psycopg2.extensions.connection.__init__(self, dsn, async_=async_) + ext.connection.__init__(self, dsn, async_=async_) conn = self.connect(connection_factory=MyConn, async_=True) self.assert_(isinstance(conn, MyConn)) @@ -330,7 +331,7 @@ class AsyncTests(ConnectingTestCase): curs.execute("select %s;", ('x' * size,)) self.wait(stub) self.assertEqual(size, len(curs.fetchone()[0])) - if stub.polls.count(psycopg2.extensions.POLL_WRITE) > 1: + if stub.polls.count(ext.POLL_WRITE) > 1: return # This is more a testing glitch than an error: it happens diff --git a/tests/test_async_keyword.py b/tests/test_async_keyword.py index 67dbb039..ff41e1b6 100755 --- a/tests/test_async_keyword.py +++ b/tests/test_async_keyword.py @@ -59,8 +59,8 @@ class AsyncTests(ConnectingTestCase): self.assert_(self.conn.async) self.assert_(not self.sync_conn.async) - # the async connection should be in isolevel 0 - self.assertEquals(self.conn.isolation_level, 0) + # the async connection should be autocommit + self.assert_(self.conn.autocommit) # check other properties to be found on the connection self.assert_(self.conn.server_version) diff --git a/tests/test_connection.py b/tests/test_connection.py index 1d11ff16..a9525b4d 100755 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -89,13 +89,26 @@ class ConnectionTests(ConnectingTestCase): def test_reset(self): conn = self.conn - # switch isolation level, then reset - level = conn.isolation_level - conn.set_isolation_level(0) - self.assertEqual(conn.isolation_level, 0) + # switch session characteristics + conn.autocommit = True + conn.isolation_level = 'serializable' + conn.readonly = True + if self.conn.server_version >= 90100: + conn.deferrable = False + + self.assert_(conn.autocommit) + self.assertEqual(conn.isolation_level, ext.ISOLATION_LEVEL_SERIALIZABLE) + self.assert_(conn.readonly is True) + if self.conn.server_version >= 90100: + self.assert_(conn.deferrable is False) + conn.reset() - # now the isolation level should be equal to saved one - self.assertEqual(conn.isolation_level, level) + # now the session characteristics should be reverted + self.assert_(not conn.autocommit) + self.assertEqual(conn.isolation_level, ext.ISOLATION_LEVEL_DEFAULT) + self.assert_(conn.readonly is None) + if self.conn.server_version >= 90100: + self.assert_(conn.deferrable is None) def test_notices(self): conn = self.conn @@ -499,7 +512,6 @@ class IsolationLevelsTestCase(ConnectingTestCase): curs = conn.cursor() levels = [ - (None, ext.ISOLATION_LEVEL_AUTOCOMMIT), ('read uncommitted', ext.ISOLATION_LEVEL_READ_UNCOMMITTED), ('read committed', ext.ISOLATION_LEVEL_READ_COMMITTED), @@ -521,16 +533,27 @@ class IsolationLevelsTestCase(ConnectingTestCase): curs.execute('show transaction_isolation;') got_name = curs.fetchone()[0] - if name is None: - curs.execute('show transaction_isolation;') - name = curs.fetchone()[0] - self.assertEqual(name, got_name) conn.commit() self.assertRaises(ValueError, conn.set_isolation_level, -1) self.assertRaises(ValueError, conn.set_isolation_level, 5) + def test_set_isolation_level_autocommit(self): + conn = self.connect() + curs = conn.cursor() + + conn.set_isolation_level(ext.ISOLATION_LEVEL_AUTOCOMMIT) + self.assertEqual(conn.isolation_level, ext.ISOLATION_LEVEL_DEFAULT) + self.assert_(conn.autocommit) + + conn.isolation_level = 'serializable' + self.assertEqual(conn.isolation_level, ext.ISOLATION_LEVEL_SERIALIZABLE) + self.assert_(conn.autocommit) + + curs.execute('show transaction_isolation;') + self.assertEqual(curs.fetchone()[0], 'serializable') + def test_set_isolation_level_default(self): conn = self.connect() curs = conn.cursor() @@ -663,8 +686,6 @@ class IsolationLevelsTestCase(ConnectingTestCase): def test_isolation_level_closed(self): cnn = self.connect() cnn.close() - self.assertRaises(psycopg2.InterfaceError, getattr, - cnn, 'isolation_level') self.assertRaises(psycopg2.InterfaceError, cnn.set_isolation_level, 0) self.assertRaises(psycopg2.InterfaceError, @@ -1226,22 +1247,47 @@ class TransactionControlTests(ConnectingTestCase): self.assertRaises(ValueError, self.conn.set_session, 'whatever') def test_set_read_only(self): - cur = self.conn.cursor() - self.conn.set_session(readonly=True) - cur.execute("SHOW transaction_read_only;") - self.assertEqual(cur.fetchone()[0], 'on') - self.conn.rollback() - cur.execute("SHOW transaction_read_only;") - self.assertEqual(cur.fetchone()[0], 'on') - self.conn.rollback() + self.assert_(self.conn.readonly is None) cur = self.conn.cursor() - self.conn.set_session(readonly=None) + self.conn.set_session(readonly=True) + self.assert_(self.conn.readonly is True) + cur.execute("SHOW transaction_read_only;") + self.assertEqual(cur.fetchone()[0], 'on') + self.conn.rollback() cur.execute("SHOW transaction_read_only;") self.assertEqual(cur.fetchone()[0], 'on') self.conn.rollback() self.conn.set_session(readonly=False) + self.assert_(self.conn.readonly is False) + cur.execute("SHOW transaction_read_only;") + self.assertEqual(cur.fetchone()[0], 'off') + self.conn.rollback() + + def test_setattr_read_only(self): + cur = self.conn.cursor() + self.conn.readonly = True + self.assert_(self.conn.readonly is True) + cur.execute("SHOW transaction_read_only;") + self.assertEqual(cur.fetchone()[0], 'on') + self.assertRaises(self.conn.ProgrammingError, + setattr, self.conn, 'readonly', False) + self.assert_(self.conn.readonly is True) + self.conn.rollback() + cur.execute("SHOW transaction_read_only;") + self.assertEqual(cur.fetchone()[0], 'on') + self.conn.rollback() + + cur = self.conn.cursor() + self.conn.readonly = None + self.assert_(self.conn.readonly is None) + cur.execute("SHOW transaction_read_only;") + self.assertEqual(cur.fetchone()[0], 'off') # assume defined by server + self.conn.rollback() + + self.conn.readonly = False + self.assert_(self.conn.readonly is False) cur.execute("SHOW transaction_read_only;") self.assertEqual(cur.fetchone()[0], 'off') self.conn.rollback() @@ -1264,8 +1310,10 @@ class TransactionControlTests(ConnectingTestCase): @skip_before_postgres(9, 1) def test_set_deferrable(self): + self.assert_(self.conn.deferrable is None) cur = self.conn.cursor() self.conn.set_session(readonly=True, deferrable=True) + self.assert_(self.conn.deferrable is True) cur.execute("SHOW transaction_read_only;") self.assertEqual(cur.fetchone()[0], 'on') cur.execute("SHOW transaction_deferrable;") @@ -1276,6 +1324,7 @@ class TransactionControlTests(ConnectingTestCase): self.conn.rollback() self.conn.set_session(deferrable=False) + self.assert_(self.conn.deferrable is False) cur.execute("SHOW transaction_read_only;") self.assertEqual(cur.fetchone()[0], 'on') cur.execute("SHOW transaction_deferrable;") @@ -1286,6 +1335,54 @@ class TransactionControlTests(ConnectingTestCase): def test_set_deferrable_error(self): self.assertRaises(psycopg2.ProgrammingError, self.conn.set_session, readonly=True, deferrable=True) + self.assertRaises(psycopg2.ProgrammingError, + setattr, self.conn, 'deferrable', True) + + @skip_before_postgres(9, 1) + def test_setattr_deferrable(self): + cur = self.conn.cursor() + self.conn.deferrable = True + self.assert_(self.conn.deferrable is True) + cur.execute("SHOW transaction_deferrable;") + self.assertEqual(cur.fetchone()[0], 'on') + self.assertRaises(self.conn.ProgrammingError, + setattr, self.conn, 'deferrable', False) + self.assert_(self.conn.deferrable is True) + self.conn.rollback() + cur.execute("SHOW transaction_deferrable;") + self.assertEqual(cur.fetchone()[0], 'on') + self.conn.rollback() + + cur = self.conn.cursor() + self.conn.deferrable = None + self.assert_(self.conn.deferrable is None) + cur.execute("SHOW transaction_deferrable;") + self.assertEqual(cur.fetchone()[0], 'off') # assume defined by server + self.conn.rollback() + + self.conn.deferrable = False + self.assert_(self.conn.deferrable is False) + cur.execute("SHOW transaction_deferrable;") + self.assertEqual(cur.fetchone()[0], 'off') + self.conn.rollback() + + def test_mixing_session_attribs(self): + cur = self.conn.cursor() + self.conn.autocommit = True + self.conn.readonly = True + + cur.execute("SHOW transaction_read_only;") + self.assertEqual(cur.fetchone()[0], 'on') + + cur.execute("SHOW default_transaction_read_only;") + self.assertEqual(cur.fetchone()[0], 'on') + + self.conn.autocommit = False + cur.execute("SHOW transaction_read_only;") + self.assertEqual(cur.fetchone()[0], 'on') + + cur.execute("SHOW default_transaction_read_only;") + self.assertEqual(cur.fetchone()[0], 'off') class AutocommitTests(ConnectingTestCase): From d7bba865f3ed3b5c9e5dde9154b6a6c091a54deb Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Thu, 16 Feb 2017 13:14:45 +0000 Subject: [PATCH 4/5] Check for deferrable unsupported applied to attribute too --- psycopg/connection_int.c | 9 ++++++++- psycopg/connection_type.c | 6 ------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/psycopg/connection_int.c b/psycopg/connection_int.c index 49daae5d..e0a369e7 100644 --- a/psycopg/connection_int.c +++ b/psycopg/connection_int.c @@ -1189,6 +1189,13 @@ conn_set_session(connectionObject *self, int autocommit, PGresult *pgres = NULL; char *error = NULL; + if (deferrable != self->deferrable && self->server_version < 90100) { + PyErr_SetString(ProgrammingError, + "the 'deferrable' setting is only available" + " from PostgreSQL 9.1"); + goto exit; + } + /* Promote an isolation level to one of the levels supported by the server */ if (self->server_version < 80000) { if (isolevel == ISOLATION_LEVEL_READ_UNCOMMITTED) { @@ -1219,7 +1226,7 @@ conn_set_session(connectionObject *self, int autocommit, goto endlock; } } - if (deferrable != self->deferrable && self->server_version >= 90100) { + if (deferrable != self->deferrable) { if (0 > pq_set_guc_locked(self, "default_transaction_deferrable", srv_state_guc[deferrable], &pgres, &error, &_save)) { diff --git a/psycopg/connection_type.c b/psycopg/connection_type.c index 66ff7050..26100b23 100644 --- a/psycopg/connection_type.c +++ b/psycopg/connection_type.c @@ -588,12 +588,6 @@ psyco_conn_set_session(connectionObject *self, PyObject *args, PyObject *kwargs) } } if (Py_None != deferrable) { - if (self->server_version < 90100) { - PyErr_SetString(ProgrammingError, - "the 'deferrable' setting is only available" - " from PostgreSQL 9.1"); - return NULL; - } if (0 > (c_deferrable = _psyco_conn_parse_onoff(deferrable))) { return NULL; } From 61101888e43d1526b63a23aeb216a9bc2ea2e197 Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Thu, 16 Feb 2017 13:15:14 +0000 Subject: [PATCH 5/5] Revert default_transaction_* to default only if set When moving from autocommit True -> False reset only the server parameters that were actually specified by psycopg to honour the serssion characteristics. --- psycopg/connection_int.c | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/psycopg/connection_int.c b/psycopg/connection_int.c index e0a369e7..e8081b9e 100644 --- a/psycopg/connection_int.c +++ b/psycopg/connection_int.c @@ -1237,17 +1237,21 @@ conn_set_session(connectionObject *self, int autocommit, else if (self->autocommit) { /* we are moving from autocommit to not autocommit, so revert the * characteristics to defaults to let BEGIN do its work */ - if (0 > pq_set_guc_locked(self, - "default_transaction_isolation", "default", - &pgres, &error, &_save)) { - goto endlock; + if (self->isolevel != ISOLATION_LEVEL_DEFAULT) { + if (0 > pq_set_guc_locked(self, + "default_transaction_isolation", "default", + &pgres, &error, &_save)) { + goto endlock; + } } - if (0 > pq_set_guc_locked(self, - "default_transaction_read_only", "default", - &pgres, &error, &_save)) { - goto endlock; + if (self->readonly != STATE_DEFAULT) { + if (0 > pq_set_guc_locked(self, + "default_transaction_read_only", "default", + &pgres, &error, &_save)) { + goto endlock; + } } - if (self->server_version >= 90100) { + if (self->deferrable != STATE_DEFAULT) { if (0 > pq_set_guc_locked(self, "default_transaction_deferrable", "default", &pgres, &error, &_save)) {