Hello,

I've read the "Handling exceptions" topic on this mailing list
(http://www.mail-archive.com/sqlobject-discuss%40lists.sourceforge.net/msg00975.html)
and decided to take a step further and try to add a database exception hierarchy to SQLObject, as described in PEP249 (http://www.python.org/dev/peps/pep-0249/).

I've discovered that the Database API Specification v2.0 is followed by both MySQL and SQLite Python modules, so all that need to be done in this case was a one-to-one mapping between their exceptions and the SQLObject defined ones. However I have no great experience in other database APIs (Postgres, Sybase, etc), so we should see if they also respect the PEP249 specification,
or if there's a way to map their exceptions according to it.

The error message attached to the SQLObject database exception is basically a string, but it also contains the error code (if any), and the originating module and exception,
in case these would need later inspection.

We could also add more specific exceptions derived from the base ones, such as DatabaseConnectionError(OperationalError), or DuplicateEntryError(IntegrityError). In my implementation I found it useful to have a DuplicateEntryError exception, to know exactly if an IntegrityError was raised when a duplicate key constraint violation has occured.

I've attached the patch (diffed against svn revision 1827), please tell me your opinion about it.

Index: __init__.py
===================================================================
--- __init__.py (revision 1827)
+++ __init__.py (working copy)
@@ -5,6 +5,7 @@
 from joins import *
 from index import *
 from dbconnection import connectionForURI
+import dberrors
 
 ## Each of these imports allows the driver to install itself
 ## Then we set up some backward compatibility
Index: sqlite/sqliteconnection.py
===================================================================
--- sqlite/sqliteconnection.py  (revision 1827)
+++ sqlite/sqliteconnection.py  (working copy)
@@ -1,11 +1,20 @@
 from sqlobject.dbconnection import DBAPI
 from sqlobject.col import popKey
+from sqlobject.dberrors import *
 import thread
 
 sqlite = None
 using_sqlite2 = False
 sqlite2_Binary = None
 
+class ErrorMessage(str):
+    def __new__(cls, e):
+        obj = str.__new__(cls, e[0])
+        obj.code = None
+        obj.module = e.__module__
+        obj.exception = e.__class__.__name__
+        return obj
+
 class SQLiteConnection(DBAPI):
 
     supportTransactions = True
@@ -154,6 +163,36 @@
             return self._memoryConn
         return sqlite.connect(self.filename, **self._connOptions)
 
+    def _executeRetry(self, conn, cursor, query):
+        if self.debug:
+            self.printDebug(conn, query, 'QueryR')
+        try:
+            return cursor.execute(query)
+        except self.module.OperationalError, e:
+            raise OperationalError(ErrorMessage(e))
+        except self.module.IntegrityError, e:
+            msg = ErrorMessage(e)
+            if msg.startswith('column') and msg.endswith('not unique'):
+                raise DuplicateEntryError(msg)
+            else:
+                raise IntegrityError(msg)
+        except self.module.InternalError, e:
+            raise InternalError(ErrorMessage(e))
+        except self.module.ProgrammingError, e:
+            raise ProgrammingError(ErrorMessage(e))
+        except self.module.DataError, e:
+            raise DataError(ErrorMessage(e))
+        except self.module.NotSupportedError, e:
+            raise NotSupportedError(ErrorMessage(e))
+        except self.module.DatabaseError, e:
+            raise DatabaseError(ErrorMessage(e))
+        except self.module.InterfaceError, e:
+            raise InterfaceError(ErrorMessage(e))
+        except self.module.Warning, e:
+            raise Warning(ErrorMessage(e))
+        except self.module.Error, e:
+            raise Error(ErrorMessage(e))
+
     def _queryInsertID(self, conn, soInstance, id, names, values):
         table = soInstance.sqlmeta.table
         idName = soInstance.sqlmeta.idName
@@ -164,7 +203,7 @@
         q = self._insertSQL(table, names, values)
         if self.debug:
             self.printDebug(conn, q, 'QueryIns')
-        c.execute(q)
+        self._executeRetry(conn, c, q)
         # lastrowid is a DB-API extension from "PEP 0249":
         if id is None:
             id = int(c.lastrowid)
Index: mysql/mysqlconnection.py
===================================================================
--- mysql/mysqlconnection.py    (revision 1827)
+++ mysql/mysqlconnection.py    (working copy)
@@ -1,7 +1,16 @@
 from sqlobject.dbconnection import DBAPI
+from sqlobject.dberrors import *
 from sqlobject import col
 MySQLdb = None
 
+class ErrorMessage(str):
+    def __new__(cls, e):
+        obj = str.__new__(cls, e[1])
+        obj.code = int(e[0])
+        obj.module = e.__module__
+        obj.exception = e.__class__.__name__
+        return obj
+
 class MySQLConnection(DBAPI):
 
     supportTransactions = False
@@ -44,9 +53,9 @@
             conn = self.module.connect(host=self.host, port=self.port,
                 db=self.db, user=self.user, passwd=self.password, **self.kw)
         except self.module.OperationalError, e:
-            raise self.module.OperationalError(
+            raise OperationalError(
                 "%s; used connection string: host=%s, port=%s, db=%s, user=%s, 
pwd=%s" % (
-                e, self.host, self.port, self.db, self.user, self.password)
+                e, self.host, self.port, self.db, self.user, self.password)    
            
             )
 
         if hasattr(conn, 'autocommit'):
@@ -61,15 +70,39 @@
             conn.autocommit(auto)
 
     def _executeRetry(self, conn, cursor, query):
+        if self.debug:
+            self.printDebug(conn, query, 'QueryR')
         while 1:
             try:
                 return cursor.execute(query)
             except MySQLdb.OperationalError, e:
-                if e.args[0] == 2013: # SERVER_LOST error
+                if e.args[0] in (2006, 20013): # SERVER_GONE or SERVER_LOST 
error
                     if self.debug:
                         self.printDebug(conn, str(e), 'ERROR')
                 else:
-                    raise
+                    raise OperationalError(ErrorMessage(e))
+            except MySQLdb.IntegrityError, e:
+                msg = ErrorMessage(e)
+                if e.args[0] == 1062:
+                    raise DuplicateEntryError(msg)
+                else:
+                    raise IntegrityError(msg)
+            except MySQLdb.InternalError, e:
+                raise InternalError(ErrorMessage(e))
+            except MySQLdb.ProgrammingError, e:
+                raise ProgrammingError(ErrorMessage(e))
+            except MySQLdb.DataError, e:
+                raise DataError(ErrorMessage(e))
+            except MySQLdb.NotSupportedError, e:
+                raise NotSupportedError(ErrorMessage(e))
+            except MySQLdb.DatabaseError, e:
+                raise DatabaseError(ErrorMessage(e))
+            except MySQLdb.InterfaceError, e:
+                raise InterfaceError(ErrorMessage(e))
+            except MySQLdb.Warning, e:
+                raise Warning(ErrorMessage(e))
+            except MySQLdb.Error, e:
+                raise Error(ErrorMessage(e))
 
     def _queryInsertID(self, conn, soInstance, id, names, values):
         table = soInstance.sqlmeta.table
@@ -122,8 +155,8 @@
             # which is not always True (for an embedded application, e.g.)
             self.query('DESCRIBE %s' % (tableName))
             return True
-        except MySQLdb.ProgrammingError, e:
-            if e.args[0] == 1146: # ER_NO_SUCH_TABLE
+        except ProgrammingError, e:
+            if e.code == 1146: # ER_NO_SUCH_TABLE
                 return False
             raise
 
Index: dberrors.py
===================================================================
--- dberrors.py (revision 0)
+++ dberrors.py (revision 0)
@@ -0,0 +1,21 @@
+"""dberrors: database exception classes for SQLObject.
+
+   These classes are dictated by the DB API v2.0:
+   
+   http://www.python.org/topics/database/DatabaseAPI-2.0.html
+"""
+
+class Error(StandardError): pass
+class Warning(StandardError): pass
+
+class InterfaceError(Error): pass
+class DatabaseError(Error): pass
+
+class InternalError(DatabaseError): pass
+class OperationalError(DatabaseError): pass
+class ProgrammingError(DatabaseError): pass
+class IntegrityError(DatabaseError): pass
+class DataError(DatabaseError): pass
+class NotSupportedError(DatabaseError): pass
+
+class DuplicateEntryError(IntegrityError): pass
-------------------------------------------------------------------------
Take Surveys. Earn Cash. Influence the Future of IT
Join SourceForge.net's Techsay panel and you'll get the chance to share your
opinions on IT & business topics through brief surveys -- and earn cash
http://www.techsay.com/default.php?page=join.php&p=sourceforge&CID=DEVDEV
_______________________________________________
sqlobject-discuss mailing list
sqlobject-discuss@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/sqlobject-discuss

Reply via email to