From c9e701baa972f9ea104134b9a7e971be80f84d1f Mon Sep 17 00:00:00 2001 From: Federico Di Gregorio Date: Sat, 8 Sep 2007 08:54:30 +0000 Subject: [PATCH] Fixed bug #194 (and added nice MD project not that C/C++ is supported.) --- ChangeLog | 8 +++++ psycopg/connection_int.c | 10 ++++-- psycopg/pqpath.c | 26 ++++++++++++-- tests/__init__.py | 21 +++++++++++ tests/test_transaction.py | 76 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_transaction.py diff --git a/ChangeLog b/ChangeLog index ed3e0ccc..8a48e926 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,11 @@ +2007-09-08 Federico Di Gregorio + + * Added MonoDevelop project, yahi! + +2007-09-06 Federico Di Gregorio + + * Fixed bug #194. + 2007-09-01 Federico Di Gregorio * Added "name" parameter to all .cursor() calls in extras.py. diff --git a/psycopg/connection_int.c b/psycopg/connection_int.c index be534727..6995451f 100644 --- a/psycopg/connection_int.c +++ b/psycopg/connection_int.c @@ -40,8 +40,10 @@ conn_notice_callback(void *args, const char *message) Dprintf("conn_notice_callback: %s", message); /* unfortunately the old protocl return COPY FROM errors only as notices, - so we need to filter them looking for such errors */ - if (strncmp(message, "ERROR", 5) == 0) + so we need to filter them looking for such errors (but we do it + only if the protocol if <3, else we don't need that */ + + if (self->protocol < 3 && strncmp(message, "ERROR", 5) == 0) pq_set_critical(self, message); else PyList_Append(self->notice_list, PyString_FromString(message)); @@ -208,6 +210,8 @@ conn_commit(connectionObject *self) pthread_mutex_unlock(&self->lock); Py_END_ALLOW_THREADS; + if (res == -1) + pq_resolve_critical(self, 0); return res; } @@ -227,6 +231,8 @@ conn_rollback(connectionObject *self) pthread_mutex_unlock(&self->lock); Py_END_ALLOW_THREADS; + if (res == -1) + pq_resolve_critical(self, 0); return res; } diff --git a/psycopg/pqpath.c b/psycopg/pqpath.c index c568c694..9f5d6f8e 100644 --- a/psycopg/pqpath.c +++ b/psycopg/pqpath.c @@ -148,10 +148,27 @@ pq_set_critical(connectionObject *conn, const char *msg) if (msg == NULL) msg = PQerrorMessage(conn->pgconn); if (conn->critical) free(conn->critical); + Dprintf("pq_set_critical: setting %s", msg); if (msg && msg[0] != '\0') conn->critical = strdup(msg); else conn->critical = NULL; } +void +pq_clear_critical(connectionObject *conn) +{ + /* sometimes we know that the notice analizer set a critical that + was not really as such (like when raising an error for a delayed + contraint violation. it would be better to analyze the notice + or avoid the set-error-on-notice stuff at all but given that we + can't, some functions at least clear the critical status after + operations they know would result in a wrong critical to be set */ + Dprintf("pq_clear_critical: clearing %s", conn->critical); + if (conn->critical) { + free(conn->critical); + conn->critical = NULL; + } +} + PyObject * pq_resolve_critical(connectionObject *conn, int close) { @@ -167,6 +184,9 @@ pq_resolve_critical(connectionObject *conn, int close) /* we don't want to destroy this connection but just close it */ if (close == 1) conn_close(conn); + + /* remember to clear the critical! */ + pq_clear_critical(conn); } return NULL; } @@ -268,6 +288,9 @@ pq_commit(connectionObject *conn) pgstatus = PQresultStatus(pgres); if (pgstatus != PGRES_COMMAND_OK ) { Dprintf("pq_commit: result is NOT OK"); + /* if the result is not OK the transaction has been rolled back + so we set the status to CONN_STATUS_READY anyway */ + conn->status = CONN_STATUS_READY; pq_set_critical(conn, NULL); goto cleanup; } @@ -400,8 +423,7 @@ pq_execute(cursorObject *curs, const char *query, int async) if (pq_begin(curs->conn) < 0) { pthread_mutex_unlock(&(curs->conn->lock)); Py_BLOCK_THREADS; - PyErr_SetString(OperationalError, - PQerrorMessage(curs->conn->pgconn)); + pq_resolve_critical(curs->conn, 0); return -1; } diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..20553ccc --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,21 @@ +import os +import unittest + +dbname = os.environ.get('PSYCOPG2_TESTDB', 'test') + +import test_psycopg2_dbapi20 +import test_transaction +import types_basic +import extras_dictcursor + +def test_suite(): + suite = unittest.TestSuite() + suite.addTest(test_psycopg2_dbapi20.test_suite()) + suite.addTest(test_transaction.test_suite()) + suite.addTest(types_basic.test_suite()) + suite.addTest(extras_dictcursor.test_suite()) + return suite + +if __name__ == "__main__": + unittest.main() + diff --git a/tests/test_transaction.py b/tests/test_transaction.py new file mode 100644 index 00000000..b6f6880f --- /dev/null +++ b/tests/test_transaction.py @@ -0,0 +1,76 @@ +import psycopg2 +import unittest +import tests + +from psycopg2.extensions import ( + ISOLATION_LEVEL_SERIALIZABLE, STATUS_BEGIN, STATUS_READY) + +class TransactionTestCase(unittest.TestCase): + + def setUp(self): + self.conn = psycopg2.connect("dbname=%s" % tests.dbname) + self.conn.set_isolation_level(ISOLATION_LEVEL_SERIALIZABLE) + curs = self.conn.cursor() + curs.execute(''' + CREATE TEMPORARY TABLE table1 ( + id int PRIMARY KEY + )''') + # The constraint is set to deferrable for the commit_failed test + curs.execute(''' + CREATE TEMPORARY TABLE table2 ( + id int PRIMARY KEY, + table1_id int, + CONSTRAINT table2__table1_id__fk + FOREIGN KEY (table1_id) REFERENCES table1(id) DEFERRABLE)''') + curs.execute('INSERT INTO table1 VALUES (1)') + curs.execute('INSERT INTO table2 VALUES (1, 1)') + self.conn.commit() + + def tearDown(self): + self.conn.close() + + def test_rollback(self): + # Test that rollback undoes changes + curs = self.conn.cursor() + curs.execute('INSERT INTO table2 VALUES (2, 1)') + # Rollback takes us from BEGIN state to READY state + self.assertEqual(self.conn.status, STATUS_BEGIN) + self.conn.rollback() + self.assertEqual(self.conn.status, STATUS_READY) + curs.execute('SELECT id, table1_id FROM table2 WHERE id = 2') + self.assertEqual(curs.fetchall(), []) + + def test_commit(self): + # Test that commit stores changes + curs = self.conn.cursor() + curs.execute('INSERT INTO table2 VALUES (2, 1)') + # Rollback takes us from BEGIN state to READY state + self.assertEqual(self.conn.status, STATUS_BEGIN) + self.conn.commit() + self.assertEqual(self.conn.status, STATUS_READY) + # Now rollback and show that the new record is still there: + self.conn.rollback() + curs.execute('SELECT id, table1_id FROM table2 WHERE id = 2') + self.assertEqual(curs.fetchall(), [(2, 1)]) + + def test_failed_commit(self): + # Test that we can recover from a failed commit. + # We use a deferred constraint to cause a failure on commit. + curs = self.conn.cursor() + curs.execute('SET CONSTRAINTS table2__table1_id__fk DEFERRED') + curs.execute('INSERT INTO table2 VALUES (2, 42)') + # The commit should fail, and move the cursor back to READY state + self.assertEqual(self.conn.status, STATUS_BEGIN) + self.assertRaises(psycopg2.OperationalError, self.conn.commit) + self.assertEqual(self.conn.status, STATUS_READY) + # The connection should be ready to use for the next transaction: + curs.execute('SELECT 1') + self.assertEqual(curs.fetchone()[0], 1) + + +def test_suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + +if __name__ == "__main__": + unittest.main() +