Author: cito
Date: Tue May  8 05:18:10 2012
New Revision: 434

Log:
Add sqlstate attribute to DatabaseError instances.

Modified:
   trunk/docs/changelog.txt
   trunk/docs/pg.txt
   trunk/module/TEST_PyGreSQL_classic.py
   trunk/module/TEST_PyGreSQL_dbapi20.py
   trunk/module/pg.py
   trunk/module/pgdb.py
   trunk/module/pgmodule.c
   trunk/module/test_pg.py

Modified: trunk/docs/changelog.txt
==============================================================================
--- trunk/docs/changelog.txt    Sat May  5 14:49:43 2012        (r433)
+++ trunk/docs/changelog.txt    Tue May  8 05:18:10 2012        (r434)
@@ -9,6 +9,7 @@
   (as suggested by Adam Frederick).
 - Binary objects are now automatically escaped and unescaped.
 - Bug in money quoting fixed.  Amounts of $0.00 handled correctly.
+- All DatabaseError instances now have a sqlstate attribute.
 
 Version 4.0 (2009-01-01)
 ------------------------

Modified: trunk/docs/pg.txt
==============================================================================
--- trunk/docs/pg.txt   Sat May  5 14:49:43 2012        (r433)
+++ trunk/docs/pg.txt   Tue May  8 05:18:10 2012        (r434)
@@ -74,7 +74,7 @@
   :pgobject: If successful, the `pgobject` handling the connection
 
 Exceptions raised:
-  :TypeError: bad argument type, or too many arguments
+  :Type: bad argument type, or too many arguments
   :SyntaxError: duplicate argument definition
   :pg.InternalError: some error occurred during pg connection definition
 
@@ -448,6 +448,10 @@
   method returns a `pgqueryobject` that can be accessed via the `getresult()`
   or `dictresult()` method or simply printed. Otherwise, it returns `None`.
 
+  When the database could not process the query, a `pg.ProgrammingError` or
+  a `pg.InternalError` is raised. You can check the "SQLSTATE" code of this
+  error by reading its `sqlstate` attribute.
+
 reset - resets the connection
 -----------------------------
 Syntax::
@@ -1103,7 +1107,7 @@
 
 Exceptions raised:
   :TypeError: too many parameters
-  :pg.InternalError: invalid previous result
+  :MemoryError: internal memory error
 
 Description:
   This method returns the list of the values returned by the query.
@@ -1124,7 +1128,7 @@
 
 Exceptions raised:
   :TypeError: too many parameters
-  :pg.InternalError: invalid previous result
+  :MemoryError: internal memory error
 
 Description:
   This method returns the list of the values returned by the query
@@ -1146,7 +1150,6 @@
 
 Exceptions raised:
   :TypeError: too many parameters
-  :pg.InternalError: invalid previous result, or lost connection
 
 Description:
   This method returns the list of names of the fields defined for the
@@ -1167,7 +1170,6 @@
 Exceptions raised:
   :TypeError: invalid connection, bad parameter type, or too many parameters
   :ValueError: invalid field number
-  :pg.InternalError: invalid previous result, or lost connection
 
 Description:
   This method allows to find a field name from its rank number. It can be
@@ -1187,7 +1189,6 @@
 Exceptions raised:
   :TypeError: invalid connection, bad parameter type, or too many parameters
   :ValueError: unknown field name
-  :pg.InternalError: invalid previous result, or lost connection
 
 Description:
   This method returns a field number from its name. It can be used to
@@ -1225,7 +1226,7 @@
 Dereferencing the initial `pgobject` is not a problem since Python won't
 deallocate it before the `pglarge` object dereference it.
 All functions return a generic error message on call error, whatever the
-exact error was. The `error` attribute of the object allow to get the exact
+exact error was. The `error` attribute of the object allows to get the exact
 error message.
 
 See also the PostgreSQL programmer's guide for more information about the
@@ -1408,7 +1409,7 @@
 Exceptions raised:
   :TypeError: invalid connection or invalid object,
     bad parameter type, or too many parameters
-  :IOError:   object is not closed, or export error
+  :IOError: object is not closed, or export error
 
 Description:
   This methods allows to dump the content of a large object in a very simple

Modified: trunk/module/TEST_PyGreSQL_classic.py
==============================================================================
--- trunk/module/TEST_PyGreSQL_classic.py       Sat May  5 14:49:43 2012        
(r433)
+++ trunk/module/TEST_PyGreSQL_classic.py       Tue May  8 05:18:10 2012        
(r434)
@@ -21,10 +21,11 @@
 db.query("SET DEFAULT_WITH_OIDS=TRUE")
 db.query("SET STANDARD_CONFORMING_STRINGS=FALSE")
 
-class utility_test(unittest.TestCase):
+
+class UtilityTest(unittest.TestCase):
 
     def setUp(self):
-        # create test tables if they don't exist
+        """Setup test tables or empty them if they already exist."""
         for t in ('_test1', '_test2'):
             try:
                 db.query("CREATE SCHEMA " + t)
@@ -34,12 +35,12 @@
                 db.query("CREATE TABLE %s._test_schema "
                     "(%s int PRIMARY KEY)" % (t, t))
             except Exception:
-                pass
+                db.query("DELETE FROM %s._test_schema" % t)
         try:
             db.query("CREATE TABLE _test_schema "
                 "(_test int PRIMARY KEY, _i interval, dvar int DEFAULT 999)")
         except Exception:
-            pass
+            db.query("DELETE FROM _test_schema")
         try:
             db.query("CREATE VIEW _test_vschema AS "
                 "SELECT _test, 'abc'::text AS _test2  FROM _test_schema")
@@ -48,28 +49,23 @@
 
     def test_invalidname(self):
         """Make sure that invalid table names are caught"""
-
         self.failUnlessRaises(ProgrammingError, db.get_attnames, 'x.y.z')
 
     def test_schema(self):
         """Does it differentiate the same table name in different schemas"""
-
         # see if they differentiate the table names properly
         self.assertEqual(
             db.get_attnames('_test_schema'),
             {'_test': 'int', 'oid': 'int', '_i': 'date', 'dvar': 'int'}
         )
-
         self.assertEqual(
             db.get_attnames('public._test_schema'),
             {'_test': 'int', 'oid': 'int', '_i': 'date', 'dvar': 'int'}
         )
-
         self.assertEqual(
             db.get_attnames('_test1._test_schema'),
             {'_test1': 'int', 'oid': 'int'}
         )
-
         self.assertEqual(
             db.get_attnames('_test2._test_schema'),
             {'_test2': 'int', 'oid': 'int'}
@@ -87,41 +83,39 @@
         self.assertEqual(db.pkey('public.test1'), 'a')
 
     def test_get(self):
-        try:
-            db.query("INSERT INTO _test_schema VALUES (1234)")
-        except Exception:
-            pass # OK if it already exists
-
+        db.query("INSERT INTO _test_schema VALUES (1234)")
         db.get('_test_schema', 1234)
-        db.get('_test_schema', 1234, keyname = '_test')
+        db.get('_test_schema', 1234, keyname='_test')
         self.failUnlessRaises(ProgrammingError, db.get, '_test_vschema', 1234)
-        db.get('_test_vschema', 1234, keyname = '_test')
+        db.get('_test_vschema', 1234, keyname='_test')
 
     def test_insert(self):
-        db.query("DELETE FROM _test_schema")
-
-        d = dict(_test = 1234)
+        d = dict(_test=1234)
         db.insert('_test_schema', d)
         self.assertEqual(d['dvar'], 999)
-
-        db.insert('_test_schema', _test = 1235)
+        db.insert('_test_schema', _test=1235)
         self.assertEqual(d['dvar'], 999)
 
+    def test_sqlstate(self):
+        db.query("INSERT INTO _test_schema VALUES (1234)")
+        try:
+            db.query("INSERT INTO _test_schema VALUES (1234)")
+        except DatabaseError, error:
+            # currently PyGreSQL does not support IntegrityError
+            self.assert_(isinstance(error, ProgrammingError))
+            # the SQLSTATE error code for unique violation is 23505
+            self.assertEqual(error.sqlstate, '23505')
+
     def test_mixed_case(self):
         try:
             db.query('CREATE TABLE _test_mc ("_Test" int PRIMARY KEY)')
         except Exception:
-            pass
-
-        db.query("DELETE FROM _test_mc")
-        d = dict(_Test = 1234)
+            db.query("DELETE FROM _test_mc")
+        d = dict(_Test=1234)
         db.insert('_test_mc', d)
 
     def test_update(self):
-        try:
-            db.query("INSERT INTO _test_schema VALUES (1234)")
-        except Exception:
-            pass # OK if it already exists
+        db.query("INSERT INTO _test_schema VALUES (1234)")
 
         r = db.get('_test_schema', 1234)
         r['dvar'] = 123
@@ -130,54 +124,54 @@
         self.assertEqual(r['dvar'], 123)
 
         r = db.get('_test_schema', 1234)
-        db.update('_test_schema', _test = 1234, dvar = 456)
+        db.update('_test_schema', _test=1234, dvar=456)
         r = db.get('_test_schema', 1234)
         self.assertEqual(r['dvar'], 456)
 
         r = db.get('_test_schema', 1234)
-        db.update('_test_schema', r, dvar = 456)
+        db.update('_test_schema', r, dvar=456)
         r = db.get('_test_schema', 1234)
         self.assertEqual(r['dvar'], 456)
 
     def test_quote(self):
-        _quote = db._quote
-        self.assertEqual(_quote(0, 'int'), "0")
-        self.assertEqual(_quote(0, 'num'), "0")
-        self.assertEqual(_quote('0', 'int'), "0")
-        self.assertEqual(_quote('0', 'num'), "0")
-        self.assertEqual(_quote(1, 'int'), "1")
-        self.assertEqual(_quote(1, 'text'), "'1'")
-        self.assertEqual(_quote(1, 'num'), "1")
-        self.assertEqual(_quote('1', 'int'), "1")
-        self.assertEqual(_quote('1', 'text'), "'1'")
-        self.assertEqual(_quote('1', 'num'), "1")
-        self.assertEqual(_quote(None, 'int'), "NULL")
-        self.assertEqual(_quote(1, 'money'), "'1.00'")
-        self.assertEqual(_quote('1', 'money'), "'1.00'")
-        self.assertEqual(_quote(1.234, 'money'), "'1.23'")
-        self.assertEqual(_quote('1.234', 'money'), "'1.23'")
-        self.assertEqual(_quote(0, 'money'), "'0.00'")
-        self.assertEqual(_quote(0.00, 'money'), "'0.00'")
-        self.assertEqual(_quote(Decimal('0.00'), 'money'), "'0.00'")
-        self.assertEqual(_quote(None, 'money'), "NULL")
-        self.assertEqual(_quote('', 'money'), "NULL")
-        self.assertEqual(_quote(0, 'bool'), "'f'")
-        self.assertEqual(_quote('', 'bool'), "NULL")
-        self.assertEqual(_quote('f', 'bool'), "'f'")
-        self.assertEqual(_quote('off', 'bool'), "'f'")
-        self.assertEqual(_quote('no', 'bool'), "'f'")
-        self.assertEqual(_quote(1, 'bool'), "'t'")
-        self.assertEqual(_quote('1', 'bool'), "'t'")
-        self.assertEqual(_quote('t', 'bool'), "'t'")
-        self.assertEqual(_quote('on', 'bool'), "'t'")
-        self.assertEqual(_quote('yes', 'bool'), "'t'")
-        self.assertEqual(_quote('true', 'bool'), "'t'")
-        self.assertEqual(_quote('y', 'bool'), "'t'")
-        self.assertEqual(_quote('', 'date'), "NULL")
-        self.assertEqual(_quote('date', 'date'), "'date'")
-        self.assertEqual(_quote('', 'text'), "''")
-        self.assertEqual(_quote("'", 'text'), "''''")
-        self.assertEqual(_quote("\\", 'text'), "'\\\\'")
+        q = db._quote
+        self.assertEqual(q(0, 'int'), "0")
+        self.assertEqual(q(0, 'num'), "0")
+        self.assertEqual(q('0', 'int'), "0")
+        self.assertEqual(q('0', 'num'), "0")
+        self.assertEqual(q(1, 'int'), "1")
+        self.assertEqual(q(1, 'text'), "'1'")
+        self.assertEqual(q(1, 'num'), "1")
+        self.assertEqual(q('1', 'int'), "1")
+        self.assertEqual(q('1', 'text'), "'1'")
+        self.assertEqual(q('1', 'num'), "1")
+        self.assertEqual(q(None, 'int'), "NULL")
+        self.assertEqual(q(1, 'money'), "'1.00'")
+        self.assertEqual(q('1', 'money'), "'1.00'")
+        self.assertEqual(q(1.234, 'money'), "'1.23'")
+        self.assertEqual(q('1.234', 'money'), "'1.23'")
+        self.assertEqual(q(0, 'money'), "'0.00'")
+        self.assertEqual(q(0.00, 'money'), "'0.00'")
+        self.assertEqual(q(Decimal('0.00'), 'money'), "'0.00'")
+        self.assertEqual(q(None, 'money'), "NULL")
+        self.assertEqual(q('', 'money'), "NULL")
+        self.assertEqual(q(0, 'bool'), "'f'")
+        self.assertEqual(q('', 'bool'), "NULL")
+        self.assertEqual(q('f', 'bool'), "'f'")
+        self.assertEqual(q('off', 'bool'), "'f'")
+        self.assertEqual(q('no', 'bool'), "'f'")
+        self.assertEqual(q(1, 'bool'), "'t'")
+        self.assertEqual(q('1', 'bool'), "'t'")
+        self.assertEqual(q('t', 'bool'), "'t'")
+        self.assertEqual(q('on', 'bool'), "'t'")
+        self.assertEqual(q('yes', 'bool'), "'t'")
+        self.assertEqual(q('true', 'bool'), "'t'")
+        self.assertEqual(q('y', 'bool'), "'t'")
+        self.assertEqual(q('', 'date'), "NULL")
+        self.assertEqual(q('date', 'date'), "'date'")
+        self.assertEqual(q('', 'text'), "''")
+        self.assertEqual(q("'", 'text'), "''''")
+        self.assertEqual(q("\\", 'text'), "'\\\\'")
 
 
 if __name__ == '__main__':

Modified: trunk/module/TEST_PyGreSQL_dbapi20.py
==============================================================================
--- trunk/module/TEST_PyGreSQL_dbapi20.py       Sat May  5 14:49:43 2012        
(r433)
+++ trunk/module/TEST_PyGreSQL_dbapi20.py       Tue May  8 05:18:10 2012        
(r434)
@@ -56,7 +56,7 @@
         con = self._connect()
         curs = con.cursor()
         curs.execute("select 1 union select 2 union select 3")
-        self.assertEqual([r[0] for r in curs], [1,2,3])
+        self.assertEqual([r[0] for r in curs], [1, 2, 3])
 
     def test_fetch_2_rows(self):
         Decimal = pgdb.decimal_type()
@@ -96,6 +96,16 @@
         finally:
             con.close()
 
+    def test_sqlstate(self):
+        con = self._connect()
+        cur = con.cursor()
+        try:
+            cur.execute("select 1/0")
+        except pgdb.DatabaseError, error:
+            self.assert_(isinstance(error, pgdb.ProgrammingError))
+            # the SQLSTATE error code for division by zero is 22012
+            self.assertEqual(error.sqlstate, '22012')
+
     def test_float(self):
         from math import pi, e
         try:

Modified: trunk/module/pg.py
==============================================================================
--- trunk/module/pg.py  Sat May  5 14:49:43 2012        (r433)
+++ trunk/module/pg.py  Tue May  8 05:18:10 2012        (r434)
@@ -90,6 +90,20 @@
     """Build oid key from qualified class name."""
     return 'oid(%s)' % qcl
 
+def _db_error(msg, cls=DatabaseError):
+    """Returns DatabaseError with empty sqlstate attribute."""
+    error = cls(msg)
+    error.sqlstate = None
+    return error
+
+def _int_error(msg):
+    """Returns InternalError."""
+    return _db_error(msg, InternalError)
+
+def _prg_error(msg):
+    """Returns ProgrammingError."""
+    return _db_error(msg, ProgrammingError)
+
 
 # The PostGreSQL database connection interface:
 
@@ -139,7 +153,7 @@
         if self.db:
             return getattr(self.db, name)
         else:
-            raise InternalError('Connection is not valid')
+            raise _int_error('Connection is not valid')
 
     # Auxiliary methods
 
@@ -222,7 +236,7 @@
         if len(s) > 1: # name already qualfied?
             # should be database.schema.table or schema.table
             if len(s) > 3:
-                raise ProgrammingError('Too many dots in class name %s' % cl)
+                raise _prg_error('Too many dots in class name %s' % cl)
             schema, cl = s[-2:]
         else:
             cl = s[0]
@@ -266,7 +280,7 @@
                 self.db.close()
                 self.db = None
             else:
-                raise InternalError('Connection already closed')
+                raise _int_error('Connection already closed')
 
     def reset(self):
         """Reset connection with current parameters.
@@ -308,7 +322,7 @@
         """
         # Wraps shared library function for debugging.
         if not self.db:
-            raise InternalError('Connection is not valid')
+            raise _int_error('Connection is not valid')
         self._do_debug(qstr)
         return self.db.query(qstr)
 
@@ -407,15 +421,14 @@
             self._attnames = newattnames
             return
         elif newattnames:
-            raise ProgrammingError(
-                'If supplied, newattnames must be a dictionary')
+            raise _prg_error('If supplied, newattnames must be a dictionary')
         cl = self._split_schema(cl) # split into schema and class
         qcl = _join_parts(cl) # build fully qualified name
         # May as well cache them:
         if qcl in self._attnames:
             return self._attnames[qcl]
         if qcl not in self.get_relations('rv'):
-            raise ProgrammingError('Class %s does not exist' % qcl)
+            raise _prg_error('Class %s does not exist' % qcl)
         t = {}
         for att, typ in self.db.query("SELECT pg_attribute.attname,"
             " pg_type.typname FROM pg_class"
@@ -487,14 +500,14 @@
         if not keyname:
             # use the primary key by default
             try:
-               keyname = self.pkey(qcl)
+                keyname = self.pkey(qcl)
             except KeyError:
-               raise ProgrammingError('Class %s has no primary key' % qcl)
+                raise _prg_error('Class %s has no primary key' % qcl)
         # We want the oid for later updates if that isn't the key
         if keyname == 'oid':
             if isinstance(arg, dict):
                 if qoid not in arg:
-                    raise ProgrammingError('%s not in arg' % qoid)
+                    raise _db_error('%s not in arg' % qoid)
             else:
                 arg = {qoid: arg}
             where = 'oid = %s' % arg[qoid]
@@ -505,7 +518,7 @@
                 keyname = (keyname,)
             if not isinstance(arg, dict):
                 if len(keyname) > 1:
-                    raise ProgrammingError('Composite key needs dict as arg')
+                    raise _prg_error('Composite key needs dict as arg')
                 arg = dict([(k, arg) for k in keyname])
             where = ' AND '.join(['%s = %s'
                 % (k, self._quote(arg[k], attnames[k])) for k in keyname])
@@ -514,7 +527,7 @@
         self._do_debug(q)
         res = self.db.query(q).dictresult()
         if not res:
-            raise DatabaseError('No such record in %s where %s' % (qcl, where))
+            raise _db_error('No such record in %s where %s' % (qcl, where))
         for att, value in res[0].iteritems():
             arg[att == 'oid' and qoid or att] = value
         return arg
@@ -600,14 +613,14 @@
             try:
                 keyname = self.pkey(qcl)
             except KeyError:
-                raise ProgrammingError('Class %s has no primary key' % qcl)
+                raise _prg_error('Class %s has no primary key' % qcl)
             if isinstance(keyname, basestring):
                 keyname = (keyname,)
             try:
                 where = ' AND '.join(['%s = %s'
                     % (k, self._quote(d[k], attnames[k])) for k in keyname])
             except KeyError:
-                raise ProgrammingError('Update needs primary key or oid.')
+                raise _prg_error('Update needs primary key or oid.')
         values = []
         for n in attnames:
             if n in d and n not in keyname:
@@ -688,7 +701,7 @@
             try:
                 keyname = self.pkey(qcl)
             except KeyError:
-                raise ProgrammingError('Class %s has no primary key' % qcl)
+                raise _prg_error('Class %s has no primary key' % qcl)
             if isinstance(keyname, basestring):
                 keyname = (keyname,)
             attnames = self.get_attnames(qcl)
@@ -696,7 +709,7 @@
                 where = ' AND '.join(['%s = %s'
                     % (k, self._quote(d[k], attnames[k])) for k in keyname])
             except KeyError:
-                raise ProgrammingError('Delete needs primary key or oid.')
+                raise _prg_error('Delete needs primary key or oid.')
         q = 'DELETE FROM %s WHERE %s' % (qcl, where)
         self._do_debug(q)
         return int(self.db.query(q))

Modified: trunk/module/pgdb.py
==============================================================================
--- trunk/module/pgdb.py        Sat May  5 14:49:43 2012        (r433)
+++ trunk/module/pgdb.py        Tue May  8 05:18:10 2012        (r434)
@@ -145,6 +145,18 @@
     'numeric': Decimal, 'money': _cast_money}
 
 
+def _db_error(msg, cls=DatabaseError):
+    """Returns DatabaseError with empty sqlstate attribute."""
+    error = cls(msg)
+    error.sqlstate = None
+    return error
+
+
+def _op_error(msg):
+    """Returns OperationalError."""
+    return _db_error(msg, OperationalError)
+
+
 class pgdbTypeCache(dict):
     """Cache for database types."""
 
@@ -219,7 +231,7 @@
         if isinstance(val, (datetime, date, time, timedelta)):
             val = str(val)
         elif isinstance(val, unicode):
-            val = val.encode( 'utf8' )
+            val = val.encode('utf8')
         if isinstance(val, str):
             if isinstance(val, Binary):
                 val = self._cnx.escape_bytea(val)
@@ -311,8 +323,10 @@
             if not self._dbcnx._tnx:
                 try:
                     self._cnx.source().execute(sql)
+                except DatabaseError:
+                    raise
                 except Exception:
-                    raise OperationalError("can't start transaction")
+                    raise _op_error("can't start transaction")
                 self._dbcnx._tnx = True
             for params in param_seq:
                 if params:
@@ -324,12 +338,12 @@
                     totrows += rows
                 else:
                     self.rowcount = -1
-        except Error:
-            msg = sys.exc_info()[1]
-            raise DatabaseError("error '%s' in '%s'" % (msg, sql))
-        except Exception:
-            err = sys.exc_info()[1]
-            raise OperationalError("internal error in '%s': %s" % (sql, err))
+        except DatabaseError:
+            raise
+        except Error, err:
+            raise _db_error("error '%s' in '%s'" % (err, sql))
+        except Exception, err:
+            raise _op_error("internal error in '%s': %s" % (sql, err))
         # then initialize result raw count and description
         if self._src.resulttype == RESULT_DQL:
             self.rowcount = self._src.ntuples
@@ -372,9 +386,10 @@
             self.arraysize = size
         try:
             result = self._src.fetch(size)
-        except Error:
-            err = sys.exc_info()[1]
-            raise DatabaseError(str(err))
+        except DatabaseError:
+            raise
+        except Error, err:
+            raise _db_error(str(err))
         row_factory = self.row_factory
         typecast = self._type_cache.typecast
         coltypes = [desc[1] for desc in self.description]
@@ -429,7 +444,7 @@
         try:
             self._cnx.source()
         except Exception:
-            raise OperationalError("invalid connection")
+            raise _op_error("invalid connection")
 
     def close(self):
         """Close the connection object."""
@@ -437,7 +452,7 @@
             self._cnx.close()
             self._cnx = None
         else:
-            raise OperationalError("connection has been closed")
+            raise _op_error("connection has been closed")
 
     def commit(self):
         """Commit any pending transaction to the database."""
@@ -446,10 +461,12 @@
                 self._tnx = False
                 try:
                     self._cnx.source().execute("COMMIT")
+                except DatabaseError:
+                    raise
                 except Exception:
-                    raise OperationalError("can't commit")
+                    raise _op_error("can't commit")
         else:
-            raise OperationalError("connection has been closed")
+            raise _op_error("connection has been closed")
 
     def rollback(self):
         """Roll back to the start of any pending transaction."""
@@ -458,10 +475,12 @@
                 self._tnx = False
                 try:
                     self._cnx.source().execute("ROLLBACK")
+                except DatabaseError:
+                    raise
                 except Exception:
-                    raise OperationalError("can't rollback")
+                    raise _op_error("can't rollback")
         else:
-            raise OperationalError("connection has been closed")
+            raise _op_error("connection has been closed")
 
     def cursor(self):
         """Return a new Cursor Object using the connection."""
@@ -469,9 +488,9 @@
             try:
                 return pgdbCursor(self)
             except Exception:
-                raise OperationalError("invalid connection")
+                raise _op_error("invalid connection")
         else:
-            raise OperationalError("connection has been closed")
+            raise _op_error("connection has been closed")
 
 
 ### Module Interface

Modified: trunk/module/pgmodule.c
==============================================================================
--- trunk/module/pgmodule.c     Sat May  5 14:49:43 2012        (r433)
+++ trunk/module/pgmodule.c     Tue May  8 05:18:10 2012        (r434)
@@ -217,6 +217,44 @@
 /* --------------------------------------------------------------------- */
 /* INTERNAL FUNCTIONS */
 
+/* sets database error with sqlstate attribute */
+/* This should be used when raising a subclass of DatabaseError */
+static void
+set_dberror(PyObject *type, const char *msg, PGresult *result)
+{
+       PyObject *err = NULL;
+       PyObject *str;
+
+       str = PyString_FromString(msg);
+       if (str)
+       {
+               err = PyObject_CallFunctionObjArgs(type, str, NULL);
+               Py_DECREF(str);
+       }
+       else
+               err = NULL;
+       if (err)
+       {
+               if (result) {
+                       char *sqlstate = PQresultErrorField(result, 
PG_DIAG_SQLSTATE);
+                       str = sqlstate ? PyString_FromStringAndSize(sqlstate, 
5) : NULL;
+               }
+               else
+                       str = NULL;
+               if (!str)
+               {
+                       Py_INCREF(Py_None);
+                       str = Py_None;
+               }
+               PyObject_SetAttrString(err, "sqlstate", str);
+               Py_DECREF(str);
+               PyErr_SetObject(type, err);
+               Py_DECREF(err);
+       }
+       else
+               PyErr_SetString(type, msg);
+}
+
 
 /* checks connection validity */
 static int
@@ -224,7 +262,7 @@
 {
        if (!self->valid)
        {
-               PyErr_SetString(IntegrityError, "connection has been closed.");
+               set_dberror(OperationalError, "connection has been closed.", 
NULL);
                return 0;
        }
        return 1;
@@ -240,7 +278,7 @@
 
        if (!self->lo_oid)
        {
-               PyErr_SetString(IntegrityError, "object is not valid (null 
oid).");
+               set_dberror(IntegrityError, "object is not valid (null oid).", 
NULL);
                return 0;
        }
 
@@ -272,19 +310,20 @@
 {
        if (!self->valid)
        {
-               PyErr_SetString(IntegrityError, "object has been closed");
+               set_dberror(OperationalError, "object has been closed", NULL);
                return 0;
        }
 
        if ((level & CHECK_RESULT) && self->result == NULL)
        {
-               PyErr_SetString(DatabaseError, "no result.");
+               set_dberror(DatabaseError, "no result.", NULL);
                return 0;
        }
 
        if ((level & CHECK_DQL) && self->result_type != RESULT_DQL)
        {
-               PyErr_SetString(DatabaseError, "last query did not return 
tuples.");
+               set_dberror(DatabaseError,
+                       "last query did not return tuples.", self->result);
                return 0;
        }
 
@@ -662,11 +701,12 @@
                case PGRES_BAD_RESPONSE:
                case PGRES_FATAL_ERROR:
                case PGRES_NONFATAL_ERROR:
-                       PyErr_SetString(ProgrammingError, 
PQerrorMessage(self->pgcnx->cnx));
+                       set_dberror(ProgrammingError,
+                               PQerrorMessage(self->pgcnx->cnx), self->result);
                        break;
                default:
-                       PyErr_SetString(InternalError, "internal error: "
-                               "unknown result status.");
+                       set_dberror(InternalError, "internal error: "
+                               "unknown result status.", self->result);
                        break;
        }
 
@@ -1278,7 +1318,7 @@
        /* gets arguments */
        if (!PyArg_ParseTuple(args, "i", &size))
        {
-               PyErr_SetString(PyExc_TypeError, "read(size), wih size 
(integer).");
+               PyErr_SetString(PyExc_TypeError, "read(size), with size 
(integer).");
                return NULL;
        }
 
@@ -1716,7 +1756,7 @@
 
        if (PQstatus(npgobj->cnx) == CONNECTION_BAD)
        {
-               PyErr_SetString(InternalError, PQerrorMessage(npgobj->cnx));
+               set_dberror(InternalError, PQerrorMessage(npgobj->cnx), NULL);
                Py_XDECREF(npgobj);
                return NULL;
        }
@@ -1787,7 +1827,7 @@
        /* connection object cannot already be closed */
        if (!self->cnx)
        {
-               PyErr_SetString(InternalError, "Connection already closed");
+               set_dberror(InternalError, "Connection already closed", NULL);
                return NULL;
        }
 
@@ -2428,7 +2468,8 @@
                        case PGRES_BAD_RESPONSE:
                        case PGRES_FATAL_ERROR:
                        case PGRES_NONFATAL_ERROR:
-                               PyErr_SetString(ProgrammingError, 
PQerrorMessage(self->cnx));
+                               set_dberror(ProgrammingError,
+                                       PQerrorMessage(self->cnx), result);
                                break;
                        case PGRES_COMMAND_OK:
                                {                                               
/* INSERT, UPDATE, DELETE */
@@ -2455,8 +2496,8 @@
                                Py_INCREF(Py_None);
                                return Py_None;
                        default:
-                               PyErr_SetString(InternalError, "internal error: 
"
-                                       "unknown result status.");
+                               set_dberror(InternalError,
+                                       "internal error: unknown result 
status.", result);
                                break;
                }
 
@@ -2997,7 +3038,7 @@
        lo_oid = lo_creat(self->cnx, mode);
        if (lo_oid == 0)
        {
-               PyErr_SetString(OperationalError, "can't create large object.");
+               set_dberror(OperationalError, "can't create large object.", 
NULL);
                return NULL;
        }
 
@@ -3059,7 +3100,7 @@
        lo_oid = lo_import(self->cnx, name);
        if (lo_oid == 0)
        {
-               PyErr_SetString(OperationalError, "can't create large object.");
+               set_dberror(OperationalError, "can't create large object.", 
NULL);
                return NULL;
        }
 

Modified: trunk/module/test_pg.py
==============================================================================
--- trunk/module/test_pg.py     Sat May  5 14:49:43 2012        (r433)
+++ trunk/module/test_pg.py     Tue May  8 05:18:10 2012        (r434)
@@ -909,6 +909,12 @@
     def testMethodQuery(self):
         self.db.query("select 1+1")
 
+    def testMethodQueryProgrammingError(self):
+        try:
+            self.db.query("select 1/0")
+        except pg.ProgrammingError, error:
+            self.assertEqual(error.sqlstate, '22012')
+
     def testMethodEndcopy(self):
         try:
             self.db.endcopy()
@@ -1123,6 +1129,12 @@
         self.assert_(isinstance(r, str))
         self.assertEqual(r, '5')
 
+    def testQueryProgrammingError(self):
+        try:
+            self.db.query("select 1/0")
+        except pg.ProgrammingError, error:
+            self.assertEqual(error.sqlstate, '22012')
+
     def testPkey(self):
         smart_ddl(self.db, "drop table pkeytest0")
         smart_ddl(self.db, "create table pkeytest0 ("
_______________________________________________
PyGreSQL mailing list
[email protected]
https://mail.vex.net/mailman/listinfo.cgi/pygresql

Reply via email to