PATCH: pick exception type based on SQLSTATE

"James Henstridge" <[email protected]>
Newsgroups gmane.comp.python.db.psycopg.devel
Message-ID <[email protected]>
Currently the pq_raise() method will raise ProgrammingError unless it
thinks that an IntegrityError is appropriate.

This patch extends the SQLSTATE checking to also pick from
NotSupportedError, DataError, InternalError and OperationalError.  I
used the following documents to help with the mapping:

http://www.postgresql.org/docs/current/static/errcodes-appendix.html
http://pyodbc.sourceforge.net/docs.html#errors
http://informixdb.svn.sourceforge.net/viewvc/informixdb/trunk/informixdb/ext/_informixdb.ec?view=markup#3402

If any of the mappings are wrong, it should be pretty easy to fix up.

The patch also changes pq_complete_error() to not force an exception
type when calling pq_raise().  This means that e.g. integrity errors
caught on transaction commit will raise IntegrityError instead of
OperationalError (this required fixing one of the tests).

Does this look okay to commit?

James.

_______________________________________________
Psycopg mailing list
Psycopg-IAPFreCvJWPBWskQ1e/[email protected]
http://lists.initd.org/mailman/listinfo/psycopg
exceptions.patch (text/x-patch, 6 KB)
Index: psycopg/pqpath.c
===================================================================
--- psycopg/pqpath.c	(revision 919)
+++ psycopg/pqpath.c	(working copy)
@@ -56,6 +56,81 @@
         return msg;
 }
 
+/* Returns the Python exception corresponding to an SQLSTATE error
+   code.  A list of error codes can be found at:
+
+   http://www.postgresql.org/docs/current/static/errcodes-appendix.html */
+static PyObject *
+exception_from_sqlstate(const char *sqlstate)
+{
+    switch (sqlstate[0]) {
+    case '0':
+        switch (sqlstate[1]) {
+        case 'A': /* Class 0A - Feature Not Supported */
+            return NotSupportedError;
+        }
+        break;
+    case '2':
+        switch (sqlstate[1]) {
+        case '1': /* Class 21 - Cardinality Violation */
+            return ProgrammingError;
+        case '2': /* Class 22 - Data Exception */
+            return DataError;
+        case '3': /* Class 23 - Integrity Constraint Violation */
+            return IntegrityError;
+        case '4': /* Class 24 - Invalid Cursor State */
+        case '5': /* Class 25 - Invalid Transaction State */
+            return InternalError;
+        case '6': /* Class 26 - Invalid SQL Statement Name */
+        case '7': /* Class 27 - Triggered Data Change Violation */
+        case '8': /* Class 28 - Invalid Authorization Specification */
+            return OperationalError;
+        case 'B': /* Class 2B - Dependent Privilege Descriptors Still Exist */
+        case 'D': /* Class 2D - Invalid Transaction Termination */
+        case 'F': /* Class 2F - SQL Routine Exception */
+            return InternalError;
+        }
+        break;
+    case '3':
+        switch (sqlstate[1]) {
+        case '4': /* Class 34 - Invalid Cursor Name */
+            return OperationalError;
+        case '8': /* Class 38 - External Routine Exception */
+        case '9': /* Class 39 - External Routine Invocation Exception */
+        case 'B': /* Class 3B - Savepoint Exception */
+            return InternalError;
+        case 'D': /* Class 3D - Invalid Catalog Name */
+        case 'F': /* Class 3F - Invalid Schema Name */
+            return ProgrammingError;
+        }
+        break;
+    case '4':
+        switch (sqlstate[1]) {
+        case '0': /* Class 40 - Transaction Rollback */
+            return OperationalError;
+        case '2': /* Class 42 - Syntax Error or Access Rule Violation */
+        case '4': /* Class 44 — WITH CHECK OPTION Violation */
+            return ProgrammingError;
+        }
+        break;
+    case '5':
+        /* Class 53 - Insufficient Resources
+           Class 54 - Program Limit Exceeded
+           Class 55 - Object Not In Prerequisite State
+           Class 57 - Operator Intervention
+           Class 58 - System Error (errors external to PostgreSQL itself) */
+        return OperationalError;
+    case 'F': /* Class F0 - Configuration File Error */
+        return InternalError;
+    case 'P': /* Class P0 - PL/pgSQL Error */
+        return InternalError;
+    case 'X': /* Class XX - Internal Error */
+        return InternalError;
+    }
+    /* return DatabaseError as a fallback */
+    return DatabaseError;
+}
+
 /* pq_raise - raise a python exception of the right kind
 
    This function should be called while holding the GIL. */
@@ -99,20 +174,10 @@
     }
 
     /* if exc is NULL, analyze the message and try to deduce the right
-       exception kind (only if we have a pgres, obviously) */
-    if (exc == NULL) {
-        if (pgres) {
-            if (conn->protocol == 3) {
-#ifdef HAVE_PQPROTOCOL3
-                char *pgstate =
-                    PQresultErrorField(pgres, PG_DIAG_SQLSTATE);
-                if (pgstate != NULL && !strncmp(pgstate, "23", 2))
-                    exc = IntegrityError;
-                else
-                    exc = ProgrammingError;
-#endif
-            }
-        }
+       exception kind (only if we got the SQLSTATE from the pgres,
+       obviously) */
+    if (exc == NULL && code != NULL) {
+        exc = exception_from_sqlstate(code);
     }
 
     /* if exc is still NULL psycopg was not built with HAVE_PQPROTOCOL3 or the
@@ -286,7 +351,7 @@
     Dprintf("pq_complete_error: pgconn = %p, pgres = %p, error = %s",
             conn->pgconn, *pgres, *error ? *error : "(null)");
     if (*pgres != NULL)
-        pq_raise(conn, NULL, *pgres, OperationalError, NULL);
+        pq_raise(conn, NULL, *pgres, NULL, NULL);
     else if (*error != NULL) {
         PyErr_SetString(OperationalError, *error);
         free(*error);
Index: ChangeLog
===================================================================
--- ChangeLog	(revision 919)
+++ ChangeLog	(working copy)
@@ -1,3 +1,17 @@
+2008-01-12  James Henstridge  <[email protected]>
+
+	* tests/test_transaction.py
+	(TransactionTestCase.test_failed_commit): Expect IntegrityError
+	instead of OperationalError.
+
+	* psycopg/pqpath.c (exception_from_sqlstate): new function that
+	converts an SQLSTATE error code to the corresponding exception
+	class.
+	(pq_raise): use exception_from_sqlstate() to pick which exception
+	to use when working with protocol version 3.
+	(pq_complete_error): Let pq_raise() pick an appropriate exception
+	rather than forcing OperationalError.
+
 2008-01-11  James Henstridge  <[email protected]>
 
 	* psycopg/adapter_binary.c (binary_quote): apply Brandon Rhodes'
Index: tests/test_transaction.py
===================================================================
--- tests/test_transaction.py	(revision 919)
+++ tests/test_transaction.py	(working copy)
@@ -62,7 +62,7 @@
         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.assertRaises(psycopg2.IntegrityError, 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')
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.