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