Author: cito
Date: Thu Jan 28 16:25:36 2016
New Revision: 796

Log:
Enrich  type_code strings in the DB API 2

The type codes now carry e.g. the information whether a type is a record.
This allows to provide a RECORD type object that compares equal to all
kinds of records, similar to the already existing ARRAY type object.

Modified:
   trunk/docs/contents/changelog.rst
   trunk/docs/contents/pgdb/connection.rst
   trunk/docs/contents/pgdb/types.rst
   trunk/pgdb.py
   trunk/tests/test_dbapi20.py

Modified: trunk/docs/contents/changelog.rst
==============================================================================
--- trunk/docs/contents/changelog.rst   Thu Jan 28 15:08:51 2016        (r795)
+++ trunk/docs/contents/changelog.rst   Thu Jan 28 16:25:36 2016        (r796)
@@ -26,6 +26,10 @@
     are now named tuples, i.e. their elements can be also accessed by name.
     The column names and types can now also be requested through the
     colnames and coltypes attributes, which are not part of DB-API 2 though.
+    The type_code provided by the description attribute is still equal to
+    the PostgreSQL internal type name, but now carries some more information
+    in additional attributes. The size, precision and scale information that
+    is part of the description is now properly set for numeric types.
   - If you pass a Python list as one of the parameters to a DB-API 2 cursor,
     it is now automatically bound using an ARRAY constructor. If you pass a
     Python tuple, it is bound using a ROW constructor. This is useful for

Modified: trunk/docs/contents/pgdb/connection.rst
==============================================================================
--- trunk/docs/contents/pgdb/connection.rst     Thu Jan 28 15:08:51 2016        
(r795)
+++ trunk/docs/contents/pgdb/connection.rst     Thu Jan 28 16:25:36 2016        
(r796)
@@ -81,14 +81,14 @@
 
 .. attribute:: Connection.type_cache
 
-    A dictionary with type information on the PostgreSQL types
+    A dictionary with the various type codes for the PostgreSQL types
 
-You can request the dictionary either via type names or type OIDs.
+You can request the dictionary either via a PostgreSQL type name (which
+(is equal to the DB-API 2 *type_code*) or via a PostgreSQL type OIDs.
 
-The values are named tuples containing the following fields:
+The values are *type_code* strings carrying additional attributes:
 
         - *oid* -- the OID of the type
-        - *name*  -- the type's name
         - *len*  -- the internal size
         - *type*  -- ``'b'`` = base, ``'c'`` = composite, ...
         - *category*  -- ``'A'`` = Array, ``'B'`` = Boolean, ...

Modified: trunk/docs/contents/pgdb/types.rst
==============================================================================
--- trunk/docs/contents/pgdb/types.rst  Thu Jan 28 15:08:51 2016        (r795)
+++ trunk/docs/contents/pgdb/types.rst  Thu Jan 28 16:25:36 2016        (r796)
@@ -156,6 +156,14 @@
 
     Used to describe ``json`` and ``jsonb`` columns
 
+.. object:: ARRAY
+
+    Used to describe columns containing PostgreSQL arrays
+
+.. object:: RECORD
+
+    Used to describe columns containing PostgreSQL records
+
 Example for using some type objects::
 
     >>> cursor = con.cursor()

Modified: trunk/pgdb.py
==============================================================================
--- trunk/pgdb.py       Thu Jan 28 15:08:51 2016        (r795)
+++ trunk/pgdb.py       Thu Jan 28 16:25:36 2016        (r796)
@@ -158,8 +158,24 @@
     return _db_error(msg, OperationalError)
 
 
-TypeInfo = namedtuple('TypeInfo',
-    ['oid', 'name', 'len', 'type', 'category', 'delim', 'relid'])
+class TypeCode(str):
+    """Class representing the type_code used by the DB-API 2.0.
+
+    TypeCode objects are strings equal to the PostgreSQL type name,
+    but carry some additional information.
+    """
+
+    @classmethod
+    def create(cls, oid, name, len, type, category, delim, relid):
+        """Create a type code for a PostgreSQL data type."""
+        self = cls(name)
+        self.oid = oid
+        self.len = len
+        self.type = type
+        self.category = category
+        self.delim = delim
+        self.relid = relid
+        return self
 
 ColumnInfo = namedtuple('ColumnInfo', ['name', 'type'])
 
@@ -167,7 +183,7 @@
 class TypeCache(dict):
     """Cache for database types.
 
-    This cache maps type OIDs and names to TypeInfo tuples containing
+    This cache maps type OIDs and names to TypeCode strings containing
     important information on the associated database type.
     """
 
@@ -195,13 +211,11 @@
             res = self._src.fetch(1)
         if not res:
             raise KeyError('Type %s could not be found' % key)
-        res = list(res[0])
-        res[0] = int(res[0])
-        res[2] = int(res[2])
-        res[6] = int(res[6])
-        res = TypeInfo(*res)
-        self[res.oid] = self[res.name] = res
-        return res
+        res = res[0]
+        type_code = TypeCode.create(int(res[0]), res[1],
+            int(res[2]), res[3], res[4], res[5], int(res[6]))
+        self[type_code.oid] = self[str(type_code)] = type_code
+        return type_code
 
     def get(self, key, default=None):
         """Get the type even if it is not cached."""
@@ -254,7 +268,7 @@
         """Get a cast function for the given database type."""
         if isinstance(key, int):
             try:
-                typ = self[key].name
+                typ = self[key]
             except KeyError:
                 return None
         else:
@@ -367,8 +381,7 @@
     def _make_description(self, info):
         """Make the description tuple for the given field info."""
         name, typ, size, mod = info[1:]
-        type_info = self.type_cache[typ]
-        type_code = type_info.name
+        type_code = self.type_cache[typ]
         if mod > 0:
             mod -= 4
         if type_code == 'numeric':
@@ -1042,6 +1055,26 @@
             return not isinstance(other, ArrayType)
 
 
+class RecordType:
+    """Type class for PostgreSQL record types."""
+
+    def __eq__(self, other):
+        if isinstance(other, TypeCode):
+            return other.type == 'c'
+        elif isinstance(other, basestring):
+            return other == 'record'
+        else:
+            return isinstance(other, RecordType)
+
+    def __ne__(self, other):
+        if isinstance(other, TypeCode):
+            return other.type != 'c'
+        elif isinstance(other, basestring):
+            return other != 'record'
+        else:
+            return not isinstance(other, RecordType)
+
+
 # Mandatory type objects defined by DB-API 2 specs:
 
 STRING = Type('char bpchar name text varchar')
@@ -1071,6 +1104,10 @@
 
 ARRAY = ArrayType()
 
+# Type object for records (encompassing all composite types):
+
+RECORD = RecordType()
+
 
 # Mandatory type helpers defined by DB-API 2 specs:
 

Modified: trunk/tests/test_dbapi20.py
==============================================================================
--- trunk/tests/test_dbapi20.py Thu Jan 28 15:08:51 2016        (r795)
+++ trunk/tests/test_dbapi20.py Thu Jan 28 16:25:36 2016        (r796)
@@ -297,8 +297,8 @@
         self.assertNotIn('numeric', type_cache)
         type_info = type_cache['numeric']
         self.assertIn('numeric', type_cache)
+        self.assertEqual(type_info, 'numeric')
         self.assertEqual(type_info.oid, 1700)
-        self.assertEqual(type_info.name, 'numeric')
         self.assertEqual(type_info.type, 'b')  # base
         self.assertEqual(type_info.category, 'N')  # numeric
         self.assertEqual(type_info.delim, ',')
@@ -311,12 +311,12 @@
         cols = type_cache.columns('pg_type')
         self.assertEqual(cols[0].name, 'typname')
         typname = type_cache[cols[0].type]
-        self.assertEqual(typname.name, 'name')
+        self.assertEqual(typname, 'name')
         self.assertEqual(typname.type, 'b')  # base
         self.assertEqual(typname.category, 'S')  # string
         self.assertEqual(cols[3].name, 'typlen')
         typlen = type_cache[cols[3].type]
-        self.assertEqual(typlen.name, 'int2')
+        self.assertEqual(typlen, 'int2')
         self.assertEqual(typlen.type, 'b')  # base
         self.assertEqual(typlen.category, 'N')  # numeric
         cur.close()
@@ -424,6 +424,7 @@
             rows = cur.fetchall()
             self.assertEqual(cur.description[0].type_code, pgdb.FLOAT)
             self.assertNotEqual(cur.description[0].type_code, pgdb.ARRAY)
+            self.assertNotEqual(cur.description[0].type_code, pgdb.RECORD)
         finally:
             con.close()
         self.assertEqual(len(rows), len(values))
@@ -461,6 +462,7 @@
             rows = cur.fetchall()
             self.assertEqual(cur.description[0].type_code, pgdb.DATETIME)
             self.assertNotEqual(cur.description[0].type_code, pgdb.ARRAY)
+            self.assertNotEqual(cur.description[0].type_code, pgdb.RECORD)
         finally:
             con.close()
         self.assertEqual(len(rows), len(values))
@@ -488,11 +490,14 @@
             cur.executemany("insert into %s values"
                 " (%%d,%%s::int[],%%s::text[][])" % table, params)
             cur.execute("select i, t from %s order by n" % table)
-            self.assertEqual(cur.description[0].type_code, pgdb.ARRAY)
-            self.assertEqual(cur.description[0].type_code, pgdb.NUMBER)
-            self.assertEqual(cur.description[0].type_code, pgdb.INTEGER)
-            self.assertEqual(cur.description[1].type_code, pgdb.ARRAY)
-            self.assertEqual(cur.description[1].type_code, pgdb.STRING)
+            d = cur.description
+            self.assertEqual(d[0].type_code, pgdb.ARRAY)
+            self.assertNotEqual(d[0].type_code, pgdb.RECORD)
+            self.assertEqual(d[0].type_code, pgdb.NUMBER)
+            self.assertEqual(d[0].type_code, pgdb.INTEGER)
+            self.assertEqual(d[1].type_code, pgdb.ARRAY)
+            self.assertNotEqual(d[1].type_code, pgdb.RECORD)
+            self.assertEqual(d[1].type_code, pgdb.STRING)
             rows = cur.fetchall()
         finally:
             con.close()
@@ -525,11 +530,13 @@
             cur.execute("select r from %s order by n" % table)
             type_code = cur.description[0].type_code
             self.assertEqual(type_code, record)
+            self.assertEqual(type_code, pgdb.RECORD)
+            self.assertNotEqual(type_code, pgdb.ARRAY)
             columns = con.type_cache.columns(type_code)
             self.assertEqual(columns[0].name, 'name')
             self.assertEqual(columns[1].name, 'age')
-            self.assertEqual(con.type_cache[columns[0].type].name, 'varchar')
-            self.assertEqual(con.type_cache[columns[1].type].name, 'int4')
+            self.assertEqual(con.type_cache[columns[0].type], 'varchar')
+            self.assertEqual(con.type_cache[columns[1].type], 'int4')
             rows = cur.fetchall()
         finally:
             cur.execute('drop table %s' % table)
@@ -878,6 +885,11 @@
         self.assertNotEqual(pgdb.ARRAY, pgdb.STRING)
         self.assertEqual('_char', pgdb.ARRAY)
         self.assertNotEqual('char', pgdb.ARRAY)
+        self.assertEqual(pgdb.RECORD, pgdb.RECORD)
+        self.assertNotEqual(pgdb.RECORD, pgdb.STRING)
+        self.assertNotEqual(pgdb.RECORD, pgdb.ARRAY)
+        self.assertEqual('record', pgdb.RECORD)
+        self.assertNotEqual('_record', pgdb.RECORD)
 
 
 if __name__ == '__main__':
_______________________________________________
PyGreSQL mailing list
[email protected]
https://mail.vex.net/mailman/listinfo.cgi/pygresql

Reply via email to