summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog8
-rw-r--r--psycopg/connection_int.c10
-rw-r--r--psycopg/pqpath.c26
-rw-r--r--tests/__init__.py21
-rw-r--r--tests/test_transaction.py76
5 files changed, 137 insertions, 4 deletions
diff --git a/ChangeLog b/ChangeLog
index ed3e0cc..8a48e92 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,11 @@
+2007-09-08 Federico Di Gregorio <fog@initd.org>
+
+ * Added MonoDevelop project, yahi!
+
+2007-09-06 Federico Di Gregorio <fog@initd.org>
+
+ * Fixed bug #194.
+
2007-09-01 Federico Di Gregorio <fog@initd.org>
* Added "name" parameter to all .cursor() calls in extras.py.
diff --git a/psycopg/connection_int.c b/psycopg/connection_int.c
index be53472..6995451 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 c568c69..9f5d6f8 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 0000000..20553cc
--- /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 0000000..b6f6880
--- /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()
+