summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>2017-02-16 11:04:02 +0000
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>2017-02-16 12:46:35 +0000
commitd50ed488074a3b809bf98fb1e6971bc295ba31c3 (patch)
tree333d66d7ddf85b5704ccd64a99283862aebbc9f8
parentb5d80b609d6f1b0922ed51d6182ff137f1b3ba3e (diff)
downloadpsycopg2-d50ed488074a3b809bf98fb1e6971bc295ba31c3.tar.gz
Added readonly and deferrable attributes
-rw-r--r--NEWS4
-rw-r--r--doc/src/connection.rst35
-rw-r--r--psycopg/connection.h1
-rw-r--r--psycopg/connection_int.c16
-rw-r--r--psycopg/connection_type.c127
-rwxr-xr-xtests/test_async.py17
-rwxr-xr-xtests/test_async_keyword.py4
-rwxr-xr-xtests/test_connection.py127
8 files changed, 270 insertions, 61 deletions
diff --git a/NEWS b/NEWS
index 9e27a44..566a39b 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 d554ce6..53f908f 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 65efcaf..6b1f244 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 e6906eb..49daae5 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 df67c80..66ff705 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 63a5513..0a386d3 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 67dbb03..ff41e1b 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 1d11ff1..a9525b4 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,8 +1247,11 @@ class TransactionControlTests(ConnectingTestCase):
self.assertRaises(ValueError, self.conn.set_session, 'whatever')
def test_set_read_only(self):
+ self.assert_(self.conn.readonly is None)
+
cur = self.conn.cursor()
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()
@@ -1235,13 +1259,35 @@ class TransactionControlTests(ConnectingTestCase):
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.set_session(readonly=None)
+ 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()
- self.conn.set_session(readonly=False)
+ 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):