Merge branch 'array-typecast' into devel

This commit is contained in:
Daniele Varrazzo 2011-09-22 20:00:08 +02:00
commit 50b445fa12
11 changed files with 229 additions and 21 deletions

4
NEWS
View File

@ -1,6 +1,9 @@
What's new in psycopg 2.4.3 What's new in psycopg 2.4.3
--------------------------- ---------------------------
- Added 'new_array_type()' function for easy creation of array
typecasters.
- Added support for arrays of hstores and composite types (ticket #66).
- Fixed segfault in case of transaction started with connection lost - Fixed segfault in case of transaction started with connection lost
(and possibly other events). (and possibly other events).
- Rollback connections in transaction or in error before putting them - Rollback connections in transaction or in error before putting them
@ -11,6 +14,7 @@ What's new in psycopg 2.4.3
- Fixed interaction between RealDictCursor and named cursors - Fixed interaction between RealDictCursor and named cursors
(ticket #67). (ticket #67).
- Dropped limit on the columns length in COPY operations (ticket #68). - Dropped limit on the columns length in COPY operations (ticket #68).
- Fixed typecasting of arrays containing consecutive backslashes.
- 'errorcodes' map updated to PostgreSQL 9.1. - 'errorcodes' map updated to PostgreSQL 9.1.

View File

@ -216,6 +216,9 @@ read:
>>> print type(point), point.x, point.y >>> print type(point), point.x, point.y
<class 'Point'> 10.2 20.3 <class 'Point'> 10.2 20.3
A typecaster created by `!new_type()` can be also used with
`~psycopg2.extensions.new_array_type()` to create a typecaster converting a
PostgreSQL array into a Python list.
.. index:: .. index::

View File

@ -290,7 +290,7 @@ details.
.. function:: new_type(oids, name, adapter) .. function:: new_type(oids, name, adapter)
Create a new type caster to convert from a PostgreSQL type to a Python Create a new type caster to convert from a PostgreSQL type to a Python
object. The created object must be registered using object. The object created must be registered using
`register_type()` to be used. `register_type()` to be used.
:param oids: tuple of OIDs of the PostgreSQL type to convert. :param oids: tuple of OIDs of the PostgreSQL type to convert.
@ -309,6 +309,23 @@ details.
See :ref:`type-casting-from-sql-to-python` for an usage example. See :ref:`type-casting-from-sql-to-python` for an usage example.
.. function:: new_array_type(oids, name, base_caster)
Create a new type caster to convert from a PostgreSQL array type to a list
of Python object. The object created must be registered using
`register_type()` to be used.
:param oids: tuple of OIDs of the PostgreSQL type to convert. It should
probably be the oid of the array type (e.g. the ``typarray`` field in
the ``pg_type`` table.
:param name: the name of the new type adapter.
:param base_caster: a Psycopg typecaster, e.g. created using the
`new_type()` function. The caster should be able to parse a single
item of the desired type.
.. versionadded:: 2.4.3
.. function:: register_type(obj [, scope]) .. function:: register_type(obj [, scope])
Register a type caster created using `new_type()`. Register a type caster created using `new_type()`.

View File

@ -57,7 +57,7 @@ except ImportError:
pass pass
from psycopg2._psycopg import adapt, adapters, encodings, connection, cursor, lobject, Xid from psycopg2._psycopg import adapt, adapters, encodings, connection, cursor, lobject, Xid
from psycopg2._psycopg import string_types, binary_types, new_type, register_type from psycopg2._psycopg import string_types, binary_types, new_type, new_array_type, register_type
from psycopg2._psycopg import ISQLQuote, Notify from psycopg2._psycopg import ISQLQuote, Notify
from psycopg2._psycopg import QueryCanceledError, TransactionRollbackError from psycopg2._psycopg import QueryCanceledError, TransactionRollbackError

View File

@ -699,7 +699,8 @@ WHERE typname = 'hstore';
return tuple(rv0), tuple(rv1) return tuple(rv0), tuple(rv1)
def register_hstore(conn_or_curs, globally=False, unicode=False, oid=None): def register_hstore(conn_or_curs, globally=False, unicode=False,
oid=None, array_oid=None):
"""Register adapter and typecaster for `!dict`\-\ |hstore| conversions. """Register adapter and typecaster for `!dict`\-\ |hstore| conversions.
:param conn_or_curs: a connection or cursor: the typecaster will be :param conn_or_curs: a connection or cursor: the typecaster will be
@ -709,14 +710,18 @@ def register_hstore(conn_or_curs, globally=False, unicode=False, oid=None):
will be `!unicode` instead of `!str`. The option is not available on will be `!unicode` instead of `!str`. The option is not available on
Python 3 Python 3
:param oid: the OID of the |hstore| type if known. If not, it will be :param oid: the OID of the |hstore| type if known. If not, it will be
queried on *conn_or_curs* queried on *conn_or_curs*.
:param array_oid: the OID of the |hstore| array type if known. If not, it
will be queried on *conn_or_curs*.
The connection or cursor passed to the function will be used to query the The connection or cursor passed to the function will be used to query the
database and look for the OID of the |hstore| type (which may be different database and look for the OID of the |hstore| type (which may be different
across databases). If querying is not desirable (e.g. with across databases). If querying is not desirable (e.g. with
:ref:`asynchronous connections <async-support>`) you may specify it in the :ref:`asynchronous connections <async-support>`) you may specify it in the
*oid* parameter (it can be found using a query such as :sql:`SELECT *oid* parameter, which can be found using a query such as :sql:`SELECT
'hstore'::regtype::oid;`). 'hstore'::regtype::oid`. Analogously you can obtain a value for *array_oid*
using a query such as :sql:`SELECT 'hstore[]'::regtype::oid`.
Note that, when passing a dictionary from Python to the database, both Note that, when passing a dictionary from Python to the database, both
strings and unicode keys and values are supported. Dictionaries returned strings and unicode keys and values are supported. Dictionaries returned
@ -730,6 +735,10 @@ def register_hstore(conn_or_curs, globally=False, unicode=False, oid=None):
added the *oid* parameter. If not specified, the typecaster is added the *oid* parameter. If not specified, the typecaster is
installed also if |hstore| is not installed in the :sql:`public` installed also if |hstore| is not installed in the :sql:`public`
schema. schema.
.. versionchanged:: 2.4.3
added support for |hstore| array.
""" """
if oid is None: if oid is None:
oid = HstoreAdapter.get_oids(conn_or_curs) oid = HstoreAdapter.get_oids(conn_or_curs)
@ -738,11 +747,18 @@ def register_hstore(conn_or_curs, globally=False, unicode=False, oid=None):
"hstore type not found in the database. " "hstore type not found in the database. "
"please install it from your 'contrib/hstore.sql' file") "please install it from your 'contrib/hstore.sql' file")
else: else:
oid = oid[0] # for the moment we don't have a HSTOREARRAY array_oid = oid[1]
oid = oid[0]
if isinstance(oid, int): if isinstance(oid, int):
oid = (oid,) oid = (oid,)
if array_oid is not None:
if isinstance(array_oid, int):
array_oid = (array_oid,)
else:
array_oid = tuple([x for x in array_oid if x])
# create and register the typecaster # create and register the typecaster
if sys.version_info[0] < 3 and unicode: if sys.version_info[0] < 3 and unicode:
cast = HstoreAdapter.parse_unicode cast = HstoreAdapter.parse_unicode
@ -753,11 +769,18 @@ def register_hstore(conn_or_curs, globally=False, unicode=False, oid=None):
_ext.register_type(HSTORE, not globally and conn_or_curs or None) _ext.register_type(HSTORE, not globally and conn_or_curs or None)
_ext.register_adapter(dict, HstoreAdapter) _ext.register_adapter(dict, HstoreAdapter)
if array_oid:
HSTOREARRAY = _ext.new_array_type(array_oid, "HSTOREARRAY", HSTORE)
_ext.register_type(HSTOREARRAY, not globally and conn_or_curs or None)
class CompositeCaster(object): class CompositeCaster(object):
"""Helps conversion of a PostgreSQL composite type into a Python object. """Helps conversion of a PostgreSQL composite type into a Python object.
The class is usually created by the `register_composite()` function. The class is usually created by the `register_composite()` function.
You may want to create and register manually instances of the class if
querying the database at registration time is not desirable (such as when
using an :ref:`asynchronous connections <async-support>`).
.. attribute:: name .. attribute:: name
@ -767,6 +790,10 @@ class CompositeCaster(object):
The oid of the PostgreSQL type. The oid of the PostgreSQL type.
.. attribute:: array_oid
The oid of the PostgreSQL array type, if available.
.. attribute:: type .. attribute:: type
The type of the Python objects returned. If :py:func:`collections.namedtuple()` The type of the Python objects returned. If :py:func:`collections.namedtuple()`
@ -782,14 +809,20 @@ class CompositeCaster(object):
List of component type oids of the type to be casted. List of component type oids of the type to be casted.
""" """
def __init__(self, name, oid, attrs): def __init__(self, name, oid, attrs, array_oid=None):
self.name = name self.name = name
self.oid = oid self.oid = oid
self.array_oid = array_oid
self.attnames = [ a[0] for a in attrs ] self.attnames = [ a[0] for a in attrs ]
self.atttypes = [ a[1] for a in attrs ] self.atttypes = [ a[1] for a in attrs ]
self._create_type(name, self.attnames) self._create_type(name, self.attnames)
self.typecaster = _ext.new_type((oid,), name, self.parse) self.typecaster = _ext.new_type((oid,), name, self.parse)
if array_oid:
self.array_typecaster = _ext.new_array_type(
(array_oid,), "%sARRAY" % name, self.typecaster)
else:
self.array_typecaster = None
def parse(self, s, curs): def parse(self, s, curs):
if s is None: if s is None:
@ -861,15 +894,18 @@ class CompositeCaster(object):
tname = name tname = name
schema = 'public' schema = 'public'
# column typarray not available before PG 8.3
typarray = conn.server_version >= 80300 and "typarray" or "NULL"
# get the type oid and attributes # get the type oid and attributes
curs.execute("""\ curs.execute("""\
SELECT t.oid, attname, atttypid SELECT t.oid, %s, attname, atttypid
FROM pg_type t FROM pg_type t
JOIN pg_namespace ns ON typnamespace = ns.oid JOIN pg_namespace ns ON typnamespace = ns.oid
JOIN pg_attribute a ON attrelid = typrelid JOIN pg_attribute a ON attrelid = typrelid
WHERE typname = %s and nspname = %s WHERE typname = %%s and nspname = %%s
ORDER BY attnum; ORDER BY attnum;
""", (tname, schema)) """ % typarray, (tname, schema))
recs = curs.fetchall() recs = curs.fetchall()
@ -883,9 +919,11 @@ ORDER BY attnum;
"PostgreSQL type '%s' not found" % name) "PostgreSQL type '%s' not found" % name)
type_oid = recs[0][0] type_oid = recs[0][0]
type_attrs = [ (r[1], r[2]) for r in recs ] array_oid = recs[0][1]
type_attrs = [ (r[2], r[3]) for r in recs ]
return CompositeCaster(tname, type_oid, type_attrs) return CompositeCaster(tname, type_oid, type_attrs,
array_oid=array_oid)
def register_composite(name, conn_or_curs, globally=False): def register_composite(name, conn_or_curs, globally=False):
"""Register a typecaster to convert a composite type into a tuple. """Register a typecaster to convert a composite type into a tuple.
@ -899,10 +937,17 @@ def register_composite(name, conn_or_curs, globally=False):
*conn_or_curs*, otherwise register it globally *conn_or_curs*, otherwise register it globally
:return: the registered `CompositeCaster` instance responsible for the :return: the registered `CompositeCaster` instance responsible for the
conversion conversion
.. versionchanged:: 2.4.3
added support for array of composite types
""" """
caster = CompositeCaster._from_db(name, conn_or_curs) caster = CompositeCaster._from_db(name, conn_or_curs)
_ext.register_type(caster.typecaster, not globally and conn_or_curs or None) _ext.register_type(caster.typecaster, not globally and conn_or_curs or None)
if caster.array_typecaster is not None:
_ext.register_type(caster.array_typecaster, not globally and conn_or_curs or None)
return caster return caster

View File

@ -257,7 +257,7 @@ psyco_connect(PyObject *self, PyObject *args, PyObject *keywds)
" * `conn_or_curs`: A connection, cursor or None" " * `conn_or_curs`: A connection, cursor or None"
#define typecast_from_python_doc \ #define typecast_from_python_doc \
"new_type(oids, name, adapter) -> new type object\n\n" \ "new_type(oids, name, castobj) -> new type object\n\n" \
"Create a new binding object. The object can be used with the\n" \ "Create a new binding object. The object can be used with the\n" \
"`register_type()` function to bind PostgreSQL objects to python objects.\n\n" \ "`register_type()` function to bind PostgreSQL objects to python objects.\n\n" \
":Parameters:\n" \ ":Parameters:\n" \
@ -268,6 +268,15 @@ psyco_connect(PyObject *self, PyObject *args, PyObject *keywds)
" the string representation returned by PostgreSQL (`!None` if ``NULL``)\n" \ " the string representation returned by PostgreSQL (`!None` if ``NULL``)\n" \
" and ``cur`` is the cursor from which data are read." " and ``cur`` is the cursor from which data are read."
#define typecast_array_from_python_doc \
"new_array_type(oids, name, baseobj) -> new type object\n\n" \
"Create a new binding object to parse an array.\n\n" \
"The object can be used with `register_type()`.\n\n" \
":Parameters:\n" \
" * `oids`: Tuple of ``oid`` of the PostgreSQL types to convert.\n" \
" * `name`: Name for the new type\n" \
" * `baseobj`: Adapter to perform type conversion of a single array item."
static void static void
_psyco_register_type_set(PyObject **dict, PyObject *type) _psyco_register_type_set(PyObject **dict, PyObject *type)
{ {
@ -758,6 +767,8 @@ static PyMethodDef psycopgMethods[] = {
METH_VARARGS, psyco_register_type_doc}, METH_VARARGS, psyco_register_type_doc},
{"new_type", (PyCFunction)typecast_from_python, {"new_type", (PyCFunction)typecast_from_python,
METH_VARARGS|METH_KEYWORDS, typecast_from_python_doc}, METH_VARARGS|METH_KEYWORDS, typecast_from_python_doc},
{"new_array_type", (PyCFunction)typecast_array_from_python,
METH_VARARGS|METH_KEYWORDS, typecast_array_from_python_doc},
{"AsIs", (PyCFunction)psyco_AsIs, {"AsIs", (PyCFunction)psyco_AsIs,
METH_VARARGS, psyco_AsIs_doc}, METH_VARARGS, psyco_AsIs_doc},

View File

@ -603,6 +603,29 @@ typecast_from_python(PyObject *self, PyObject *args, PyObject *keywds)
return typecast_new(name, v, cast, base); return typecast_new(name, v, cast, base);
} }
PyObject *
typecast_array_from_python(PyObject *self, PyObject *args, PyObject *keywds)
{
PyObject *values, *name = NULL, *base = NULL;
typecastObject *obj = NULL;
static char *kwlist[] = {"values", "name", "baseobj", NULL};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "O!O!O!", kwlist,
&PyTuple_Type, &values,
&Text_Type, &name,
&typecastType, &base)) {
return NULL;
}
if ((obj = (typecastObject *)typecast_new(name, values, NULL, base))) {
obj->ccast = typecast_GENERIC_ARRAY_cast;
obj->pcast = NULL;
}
return (PyObject *)obj;
}
PyObject * PyObject *
typecast_from_c(typecastObject_initlist *type, PyObject *dict) typecast_from_c(typecastObject_initlist *type, PyObject *dict)
{ {

View File

@ -77,9 +77,11 @@ HIDDEN int typecast_add(PyObject *obj, PyObject *dict, int binary);
/* the C callable typecastObject creator function */ /* the C callable typecastObject creator function */
HIDDEN PyObject *typecast_from_c(typecastObject_initlist *type, PyObject *d); HIDDEN PyObject *typecast_from_c(typecastObject_initlist *type, PyObject *d);
/* the python callable typecast creator function */ /* the python callable typecast creator functions */
HIDDEN PyObject *typecast_from_python( HIDDEN PyObject *typecast_from_python(
PyObject *self, PyObject *args, PyObject *keywds); PyObject *self, PyObject *args, PyObject *keywds);
HIDDEN PyObject *typecast_array_from_python(
PyObject *self, PyObject *args, PyObject *keywds);
/* the function used to dispatch typecasting calls */ /* the function used to dispatch typecasting calls */
HIDDEN PyObject *typecast_cast( HIDDEN PyObject *typecast_cast(

View File

@ -133,7 +133,7 @@ typecast_array_tokenize(const char *str, Py_ssize_t strlength,
} }
if (res == ASCAN_QUOTED) { if (res == ASCAN_QUOTED) {
Py_ssize_t j; const char *j, *jj;
char *buffer = PyMem_Malloc(l+1); char *buffer = PyMem_Malloc(l+1);
if (buffer == NULL) { if (buffer == NULL) {
PyErr_NoMemory(); PyErr_NoMemory();
@ -142,10 +142,9 @@ typecast_array_tokenize(const char *str, Py_ssize_t strlength,
*token = buffer; *token = buffer;
for (j = *pos; j < *pos+l; j++) { for (j = str + *pos, jj = j + l; j < jj; ++j) {
if (str[j] != '\\' if (*j == '\\') { ++j; }
|| (j > *pos && str[j-1] == '\\')) *(buffer++) = *j;
*(buffer++) = str[j];
} }
*buffer = '\0'; *buffer = '\0';

View File

@ -189,6 +189,17 @@ class TypesBasicTests(unittest.TestCase):
s = self.execute("SELECT '{}'::text AS foo") s = self.execute("SELECT '{}'::text AS foo")
self.failUnlessEqual(s, "{}") self.failUnlessEqual(s, "{}")
def testArrayEscape(self):
ss = ['', '\\', '"', '\\\\', '\\"']
for s in ss:
r = self.execute("SELECT %s AS foo", (s,))
self.failUnlessEqual(s, r)
r = self.execute("SELECT %s AS foo", ([s],))
self.failUnlessEqual([s], r)
r = self.execute("SELECT %s AS foo", (ss,))
self.failUnlessEqual(ss, r)
@testutils.skip_from_python(3) @testutils.skip_from_python(3)
def testTypeRoundtripBuffer(self): def testTypeRoundtripBuffer(self):
o1 = buffer("".join(map(chr, range(256)))) o1 = buffer("".join(map(chr, range(256))))
@ -285,6 +296,19 @@ class TypesBasicTests(unittest.TestCase):
l1 = self.execute("select -%s;", (-1L,)) l1 = self.execute("select -%s;", (-1L,))
self.assertEqual(1, l1) self.assertEqual(1, l1)
def testGenericArray(self):
def caster(s, cur):
if s is None: return "nada"
return int(s) * 2
base = psycopg2.extensions.new_type((23,), "INT4", caster)
array = psycopg2.extensions.new_array_type((1007,), "INT4ARRAY", base)
psycopg2.extensions.register_type(array, self.conn)
a = self.execute("select '{1,2,3}'::int4[]")
self.assertEqual(a, [2,4,6])
a = self.execute("select '{1,2,NULL}'::int4[]")
self.assertEqual(a, [2,4,'nada'])
class AdaptSubclassTest(unittest.TestCase): class AdaptSubclassTest(unittest.TestCase):
def test_adapt_subtype(self): def test_adapt_subtype(self):

View File

@ -22,7 +22,7 @@ import re
import sys import sys
from datetime import date from datetime import date
from testutils import unittest, skip_if_no_uuid from testutils import unittest, skip_if_no_uuid, skip_before_postgres
import psycopg2 import psycopg2
import psycopg2.extras import psycopg2.extras
@ -357,6 +357,63 @@ class HstoreTestCase(unittest.TestCase):
finally: finally:
psycopg2.extensions.string_types.pop(oid) psycopg2.extensions.string_types.pop(oid)
@skip_if_no_hstore
@skip_before_postgres(8, 3)
def test_roundtrip_array(self):
from psycopg2.extras import register_hstore
register_hstore(self.conn)
ds = []
ds.append({})
ds.append({'a': 'b', 'c': None})
ab = map(chr, range(32, 128))
ds.append(dict(zip(ab, ab)))
ds.append({''.join(ab): ''.join(ab)})
self.conn.set_client_encoding('latin1')
if sys.version_info[0] < 3:
ab = map(chr, range(32, 127) + range(160, 255))
else:
ab = bytes(range(32, 127) + range(160, 255)).decode('latin1')
ds.append({''.join(ab): ''.join(ab)})
ds.append(dict(zip(ab, ab)))
cur = self.conn.cursor()
cur.execute("select %s", (ds,))
ds1 = cur.fetchone()[0]
self.assertEqual(ds, ds1)
@skip_if_no_hstore
@skip_before_postgres(8, 3)
def test_array_cast(self):
from psycopg2.extras import register_hstore
register_hstore(self.conn)
cur = self.conn.cursor()
cur.execute("select array['a=>1'::hstore, 'b=>2'::hstore];")
a = cur.fetchone()[0]
self.assertEqual(a, [{'a': '1'}, {'b': '2'}])
@skip_if_no_hstore
def test_array_cast_oid(self):
cur = self.conn.cursor()
cur.execute("select 'hstore'::regtype::oid, 'hstore[]'::regtype::oid")
oid, aoid = cur.fetchone()
from psycopg2.extras import register_hstore
register_hstore(None, globally=True, oid=oid, array_oid=aoid)
try:
cur.execute("select null::hstore, ''::hstore, 'a => b'::hstore, '{a=>b}'::hstore[]")
t = cur.fetchone()
self.assert_(t[0] is None)
self.assertEqual(t[1], {})
self.assertEqual(t[2], {'a': 'b'})
self.assertEqual(t[3], [{'a': 'b'}])
finally:
psycopg2.extensions.string_types.pop(oid)
psycopg2.extensions.string_types.pop(aoid)
def skip_if_no_composite(f): def skip_if_no_composite(f):
def skip_if_no_composite_(self): def skip_if_no_composite_(self):
@ -563,6 +620,29 @@ class AdaptTypeTestCase(unittest.TestCase):
curs.execute("select (4,8)::typens.typens_ii") curs.execute("select (4,8)::typens.typens_ii")
self.assertEqual(curs.fetchone()[0], (4,8)) self.assertEqual(curs.fetchone()[0], (4,8))
@skip_if_no_composite
@skip_before_postgres(8, 3)
def test_composite_array(self):
oid = self._create_type("type_isd",
[('anint', 'integer'), ('astring', 'text'), ('adate', 'date')])
t = psycopg2.extras.register_composite("type_isd", self.conn)
curs = self.conn.cursor()
r1 = (10, 'hello', date(2011,1,2))
r2 = (20, 'world', date(2011,1,3))
curs.execute("select %s::type_isd[];", ([r1, r2],))
v = curs.fetchone()[0]
self.assertEqual(len(v), 2)
self.assert_(isinstance(v[0], t.type))
self.assertEqual(v[0][0], 10)
self.assertEqual(v[0][1], "hello")
self.assertEqual(v[0][2], date(2011,1,2))
self.assert_(isinstance(v[1], t.type))
self.assertEqual(v[1][0], 20)
self.assertEqual(v[1][1], "world")
self.assertEqual(v[1][2], date(2011,1,3))
def _create_type(self, name, fields): def _create_type(self, name, fields):
curs = self.conn.cursor() curs = self.conn.cursor()
try: try: