diff --git a/ChangeLog b/ChangeLog index 0e721f3f..43d990db 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2010-11-18 Daniele Varrazzo + + * psycopg/connection.h: Added enum with possilbe isolation level states. + Also, general isolation levels cleanup and tests added. + 2010-11-17 Daniele Varrazzo * psycopg/connection_type.c: don't clobber exception if diff --git a/psycopg/connection.h b/psycopg/connection.h index 21069d85..96536876 100644 --- a/psycopg/connection.h +++ b/psycopg/connection.h @@ -66,6 +66,13 @@ extern "C" { #define psyco_datestyle "SET DATESTYLE TO 'ISO'" #define psyco_transaction_isolation "SHOW default_transaction_isolation" +/* possible values for isolation_level */ +typedef enum { + ISOLATION_LEVEL_AUTOCOMMIT = 0, + ISOLATION_LEVEL_READ_COMMITTED = 1, + ISOLATION_LEVEL_SERIALIZABLE = 2, +} conn_isolation_level_t; + extern HIDDEN PyTypeObject connectionType; struct connectionObject_notice { diff --git a/psycopg/connection_int.c b/psycopg/connection_int.c index 8516b2bf..34bcef6f 100644 --- a/psycopg/connection_int.c +++ b/psycopg/connection_int.c @@ -265,11 +265,11 @@ conn_get_isolation_level(PGresult *pgres) char *isolation_level = PQgetvalue(pgres, 0, 0); - if ((strncmp(lvl1a, isolation_level, strlen(isolation_level)) == 0) - || (strncmp(lvl1b, isolation_level, strlen(isolation_level)) == 0)) - rv = 1; + if ((strcmp(lvl1b, isolation_level) == 0) /* most likely */ + || (strcmp(lvl1a, isolation_level) == 0)) + rv = ISOLATION_LEVEL_READ_COMMITTED; else /* if it's not one of the lower ones, it's SERIALIZABLE */ - rv = 2; + rv = ISOLATION_LEVEL_SERIALIZABLE; CLEARPGRES(pgres); @@ -659,7 +659,7 @@ _conn_poll_setup_async(connectionObject *self) * expected to manage the transactions himself, by sending * (asynchronously) BEGIN and COMMIT statements. */ - self->isolation_level = 0; + self->isolation_level = ISOLATION_LEVEL_AUTOCOMMIT; /* If the datestyle is ISO or anything else good, * we can skip the CONN_STATUS_DATESTYLE step. */ @@ -830,20 +830,21 @@ conn_switch_isolation_level(connectionObject *self, int level) char *error = NULL; int res = 0; - /* if the current isolation level is equal to the requested one don't switch */ - if (self->isolation_level == level) return 0; - Py_BEGIN_ALLOW_THREADS; pthread_mutex_lock(&self->lock); - /* if the current isolation level is > 0 we need to abort the current - transaction before changing; that all folks! */ - if (self->isolation_level != level && self->isolation_level > 0) { - res = pq_abort_locked(self, &pgres, &error, &_save); - } - self->isolation_level = level; + /* if the current isolation level is equal to the requested one don't switch */ + if (self->isolation_level != level) { - Dprintf("conn_switch_isolation_level: switched to level %d", level); + /* if the current isolation level is > 0 we need to abort the current + transaction before changing; that all folks! */ + if (self->isolation_level != ISOLATION_LEVEL_AUTOCOMMIT) { + res = pq_abort_locked(self, &pgres, &error, &_save); + } + self->isolation_level = level; + + Dprintf("conn_switch_isolation_level: switched to level %d", level); + } pthread_mutex_unlock(&self->lock); Py_END_ALLOW_THREADS; diff --git a/psycopg/connection_type.c b/psycopg/connection_type.c index f6781090..19945ce4 100644 --- a/psycopg/connection_type.c +++ b/psycopg/connection_type.c @@ -209,7 +209,7 @@ psyco_conn_tpc_begin(connectionObject *self, PyObject *args) } /* two phase commit and autocommit make no point */ - if (self->isolation_level == 0) { + if (self->isolation_level == ISOLATION_LEVEL_AUTOCOMMIT) { PyErr_SetString(ProgrammingError, "tpc_begin can't be called in autocommit mode"); goto exit; @@ -410,7 +410,7 @@ psyco_conn_set_isolation_level(connectionObject *self, PyObject *args) if (level < 0 || level > 2) { PyErr_SetString(PyExc_ValueError, - "isolation level out of bounds (0,3)"); + "isolation level must be between 0 and 2"); return NULL; } diff --git a/psycopg/cursor_type.c b/psycopg/cursor_type.c index d13b234a..488a5cd3 100644 --- a/psycopg/cursor_type.c +++ b/psycopg/cursor_type.c @@ -476,7 +476,7 @@ psyco_curs_execute(cursorObject *self, PyObject *args, PyObject *kwargs) NULL, NULL); return NULL; } - if (self->conn->isolation_level == 0) { + if (self->conn->isolation_level == ISOLATION_LEVEL_AUTOCOMMIT) { psyco_set_error(ProgrammingError, (PyObject*)self, "can't use a named cursor outside of transactions", NULL, NULL); return NULL; diff --git a/psycopg/lobject_int.c b/psycopg/lobject_int.c index 9186c609..bac2afb6 100644 --- a/psycopg/lobject_int.c +++ b/psycopg/lobject_int.c @@ -129,7 +129,7 @@ lobject_close_locked(lobjectObject *self, char **error) { int retvalue; - if (self->conn->isolation_level == 0 || + if (self->conn->isolation_level == ISOLATION_LEVEL_AUTOCOMMIT || self->conn->mark != self->mark || self->fd == -1) return 0; diff --git a/psycopg/lobject_type.c b/psycopg/lobject_type.c index 67c0104d..8aefa74e 100644 --- a/psycopg/lobject_type.c +++ b/psycopg/lobject_type.c @@ -55,7 +55,7 @@ psyco_lobj_close(lobjectObject *self, PyObject *args) closing the current transaction is equivalent to close all the opened large objects */ if (!lobject_is_closed(self) - && self->conn->isolation_level > 0 + && self->conn->isolation_level != ISOLATION_LEVEL_AUTOCOMMIT && self->conn->mark == self->mark) { Dprintf("psyco_lobj_close: closing lobject at %p", self); @@ -300,7 +300,7 @@ lobject_setup(lobjectObject *self, connectionObject *conn, { Dprintf("lobject_setup: init lobject object at %p", self); - if (conn->isolation_level == 0) { + if (conn->isolation_level == ISOLATION_LEVEL_AUTOCOMMIT) { psyco_set_error(ProgrammingError, (PyObject*)self, "can't use a lobject outside of transactions", NULL, NULL); return -1; diff --git a/psycopg/pqpath.c b/psycopg/pqpath.c index 7db71963..ca975658 100644 --- a/psycopg/pqpath.c +++ b/psycopg/pqpath.c @@ -409,7 +409,8 @@ pq_begin_locked(connectionObject *conn, PGresult **pgres, char **error, Dprintf("pq_begin_locked: pgconn = %p, isolevel = %ld, status = %d", conn->pgconn, conn->isolation_level, conn->status); - if (conn->isolation_level == 0 || conn->status != CONN_STATUS_READY) { + if (conn->isolation_level == ISOLATION_LEVEL_AUTOCOMMIT + || conn->status != CONN_STATUS_READY) { Dprintf("pq_begin_locked: transaction in progress"); return 0; } @@ -438,7 +439,8 @@ pq_commit(connectionObject *conn) Dprintf("pq_commit: pgconn = %p, isolevel = %ld, status = %d", conn->pgconn, conn->isolation_level, conn->status); - if (conn->isolation_level == 0 || conn->status != CONN_STATUS_BEGIN) { + if (conn->isolation_level == ISOLATION_LEVEL_AUTOCOMMIT + || conn->status != CONN_STATUS_BEGIN) { Dprintf("pq_commit: no transaction to commit"); return 0; } @@ -473,7 +475,8 @@ pq_abort_locked(connectionObject *conn, PGresult **pgres, char **error, Dprintf("pq_abort_locked: pgconn = %p, isolevel = %ld, status = %d", conn->pgconn, conn->isolation_level, conn->status); - if (conn->isolation_level == 0 || conn->status != CONN_STATUS_BEGIN) { + if (conn->isolation_level == ISOLATION_LEVEL_AUTOCOMMIT + || conn->status != CONN_STATUS_BEGIN) { Dprintf("pq_abort_locked: no transaction to abort"); return 0; } @@ -501,7 +504,8 @@ pq_abort(connectionObject *conn) Dprintf("pq_abort: pgconn = %p, isolevel = %ld, status = %d", conn->pgconn, conn->isolation_level, conn->status); - if (conn->isolation_level == 0 || conn->status != CONN_STATUS_BEGIN) { + if (conn->isolation_level == ISOLATION_LEVEL_AUTOCOMMIT + || conn->status != CONN_STATUS_BEGIN) { Dprintf("pq_abort: no transaction to abort"); return 0; } @@ -542,7 +546,8 @@ pq_reset_locked(connectionObject *conn, PGresult **pgres, char **error, conn->mark += 1; - if (conn->isolation_level > 0 && conn->status == CONN_STATUS_BEGIN) { + if (conn->isolation_level != ISOLATION_LEVEL_AUTOCOMMIT + && conn->status == CONN_STATUS_BEGIN) { retvalue = pq_execute_command_locked(conn, "ABORT", pgres, error, tstate); if (retvalue != 0) return retvalue; } diff --git a/tests/test_connection.py b/tests/test_connection.py index 338d016a..36ce6406 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -82,6 +82,20 @@ class ConnectionTests(unittest.TestCase): conn = self.connect() self.assert_(conn.protocol_version in (2,3), conn.protocol_version) + +class IsolationLevelsTestCase(unittest.TestCase): + + def setUp(self): + conn = self.connect() + cur = conn.cursor() + cur.execute("drop table if exists isolevel;") + cur.execute("create table isolevel (id integer);") + conn.commit() + conn.close() + + def connect(self): + return psycopg2.connect(tests.dsn) + def test_isolation_level(self): conn = self.connect() self.assertEqual( @@ -92,22 +106,78 @@ class ConnectionTests(unittest.TestCase): conn = self.connect() self.assert_(conn.encoding in psycopg2.extensions.encodings) + def test_set_isolation_level(self): + conn = self.connect() + + conn.set_isolation_level( + psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) + self.assertEqual(conn.isolation_level, + psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) + + conn.set_isolation_level( + psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED) + self.assertEqual(conn.isolation_level, + psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED) + + conn.set_isolation_level( + psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE) + self.assertEqual(conn.isolation_level, + psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE) + + self.assertRaises(ValueError, conn.set_isolation_level, -1) + self.assertRaises(ValueError, conn.set_isolation_level, 3) + + def test_set_isolation_level_abort(self): + conn = self.connect() + cur = conn.cursor() + + self.assertEqual(psycopg2.extensions.TRANSACTION_STATUS_IDLE, + conn.get_transaction_status()) + 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_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]) + def test_isolation_level_autocommit(self): cnn1 = self.connect() cnn2 = self.connect() cnn2.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) cur1 = cnn1.cursor() - cur1.execute("drop table if exists isolevel;") - cur1.execute("create table isolevel (id integer);") cur1.execute("select count(*) from isolevel;") self.assertEqual(0, cur1.fetchone()[0]) cnn1.commit() cur2 = cnn2.cursor() cur2.execute("insert into isolevel values (10);") - cur1.execute("select count(*) from isolevel;") + cur1.execute("select count(*) from isolevel;") self.assertEqual(1, cur1.fetchone()[0]) def test_isolation_level_read_committed(self): @@ -116,47 +186,52 @@ class ConnectionTests(unittest.TestCase): cnn2.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED) cur1 = cnn1.cursor() - cur1.execute("drop table if exists isolevel;") - cur1.execute("create table isolevel (id integer);") cur1.execute("select count(*) from isolevel;") self.assertEqual(0, cur1.fetchone()[0]) cnn1.commit() cur2 = cnn2.cursor() cur2.execute("insert into isolevel values (10);") + cur1.execute("insert into isolevel values (20);") + cur2.execute("select count(*) from isolevel;") self.assertEqual(1, cur2.fetchone()[0]) - - cur1.execute("insert into isolevel values (20);") cnn1.commit() - cur2.execute("select count(*) from isolevel;") self.assertEqual(2, cur2.fetchone()[0]) + cur1.execute("select count(*) from isolevel;") + self.assertEqual(1, cur1.fetchone()[0]) + cnn2.commit() + cur1.execute("select count(*) from isolevel;") + self.assertEqual(2, cur1.fetchone()[0]) + def test_isolation_level_serializable(self): cnn1 = self.connect() cnn2 = self.connect() cnn2.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE) cur1 = cnn1.cursor() - cur1.execute("drop table if exists isolevel;") - cur1.execute("create table isolevel (id integer);") cur1.execute("select count(*) from isolevel;") self.assertEqual(0, cur1.fetchone()[0]) cnn1.commit() cur2 = cnn2.cursor() cur2.execute("insert into isolevel values (10);") - cur2.execute("select count(*) from isolevel;") - self.assertEqual(1, cur2.fetchone()[0]) - cur1.execute("insert into isolevel values (20);") - cnn1.commit() + cur2.execute("select count(*) from isolevel;") + self.assertEqual(1, cur2.fetchone()[0]) + cnn1.commit() cur2.execute("select count(*) from isolevel;") self.assertEqual(1, cur2.fetchone()[0]) + cur1.execute("select count(*) from isolevel;") + self.assertEqual(1, cur1.fetchone()[0]) cnn2.commit() + cur1.execute("select count(*) from isolevel;") + self.assertEqual(2, cur1.fetchone()[0]) + cur2.execute("select count(*) from isolevel;") self.assertEqual(2, cur2.fetchone()[0])