Author: cito
Date: Thu Feb 4 15:18:08 2016
New Revision: 817
Log:
Support the hstore data type
Added adaptation and typecasting of the hstore type as Python dictionaries.
For the typecasting, a fast parser has been added to the C extension.
Modified:
trunk/docs/contents/changelog.rst
trunk/docs/contents/pg/module.rst
trunk/docs/contents/pgdb/module.rst
trunk/docs/contents/pgdb/types.rst
trunk/pg.py
trunk/pgdb.py
trunk/pgmodule.c
trunk/tests/test_classic_dbwrapper.py
trunk/tests/test_classic_functions.py
trunk/tests/test_dbapi20.py
Modified: trunk/docs/contents/changelog.rst
==============================================================================
--- trunk/docs/contents/changelog.rst Thu Feb 4 07:37:31 2016 (r816)
+++ trunk/docs/contents/changelog.rst Thu Feb 4 15:18:08 2016 (r817)
@@ -6,40 +6,6 @@
- This version now runs on both Python 2 and Python 3.
- The supported versions are Python 2.6 to 2.7, and 3.3 to 3.5.
- PostgreSQL is supported in all versions from 9.0 to 9.5.
-- Changes in the DB-API 2 module (pgdb):
- - The DB-API 2 module now always returns result rows as named tuples
- instead of simply lists as before. The documentation explains how
- you can restore the old behavior or use custom row objects instead.
- - The names of the various classes used by the classic and DB-API 2
- modules have been renamed to become simpler, more intuitive and in
- line with the names used in the DB-API 2 documentation.
- Since the API provides only objects of these types through constructor
- functions, this should not cause any incompatibilities.
- - The DB-API 2 module now supports the callproc() cursor method. Note
- that output parameters are currently not replaced in the return value.
- - The DB-API 2 module now supports copy operations between data streams
- on the client and database tables via the COPY command of PostgreSQL.
- The cursor method copy_from() can be used to copy data from the database
- to the client, and the cursor method copy_to() can be used to copy data
- from the client to the database.
- - The 7-tuples returned by the description attribute of a pgdb cursor
- 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
- passing records as well as making use of the IN syntax.
- - Inversely, when a fetch method of a DB-API 2 cursor returns a PostgreSQL
- array, it is passed to Python as a list, and when it returns a PostgreSQL
- composite type, it is passed to Python as a named tuple. PyGreSQL uses
- a new fast built-in parser to achieve this. Anonymous composite types are
- also supported, but yield only an ordinary tuple containing text strings.
- - A new type helper Interval() has been added.
- Changes in the classic PyGreSQL module (pg):
- The classic interface got two new methods get_as_list() and get_as_dict()
returning a database table as a Python list or dict. The amount of data
@@ -91,9 +57,41 @@
that allows using the format specifications from Python. A flag "inline"
can be set to specify whether parameters should be sent to the database
separately or formatted into the SQL.
- - The methods for adapting and typecasting values pertaining to PostgreSQL
- types have been refactored and swapped out to separate classes.
- A new type helper Bytea() has been added.
+- Changes in the DB-API 2 module (pgdb):
+ - The DB-API 2 module now always returns result rows as named tuples
+ instead of simply lists as before. The documentation explains how
+ you can restore the old behavior or use custom row objects instead.
+ - The names of the various classes used by the classic and DB-API 2
+ modules have been renamed to become simpler, more intuitive and in
+ line with the names used in the DB-API 2 documentation.
+ Since the API provides only objects of these types through constructor
+ functions, this should not cause any incompatibilities.
+ - The DB-API 2 module now supports the callproc() cursor method. Note
+ that output parameters are currently not replaced in the return value.
+ - The DB-API 2 module now supports copy operations between data streams
+ on the client and database tables via the COPY command of PostgreSQL.
+ The cursor method copy_from() can be used to copy data from the database
+ to the client, and the cursor method copy_to() can be used to copy data
+ from the client to the database.
+ - The 7-tuples returned by the description attribute of a pgdb cursor
+ 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
+ passing records as well as making use of the IN syntax.
+ - Inversely, when a fetch method of a DB-API 2 cursor returns a PostgreSQL
+ array, it is passed to Python as a list, and when it returns a PostgreSQL
+ composite type, it is passed to Python as a named tuple. PyGreSQL uses
+ a new fast built-in parser to achieve this. Anonymous composite types are
+ also supported, but yield only an ordinary tuple containing text strings.
+ - A new type helper Interval() has been added.
- Changes concerning both modules:
- The modules now provide get_typecast() and set_typecast() methods
allowing to control the typecasting on the global level. The connection
@@ -106,15 +104,20 @@
library. In earlier versions of PyGreSQL they had been returned as
strings. You can restore the old behavior by deactivating the respective
typecast functions, e.g. set_typecast('date', None).
- - PyGreSQL now supports the JSON and JSONB data types, converting such
+ - PyGreSQL now supports the "hstore" data type, converting such columns
+ automatically to and from Python dictionaries. If you want to insert
+ Python objects as JSON data using DB-API 2, you should wrap them in the
+ new HStore() type constructor as a hint to PyGreSQL.
+ - PyGreSQL now supports the "json" and "jsonb" data types, converting such
columns automatically to and from Python objects. If you want to insert
Python objects as JSON data using DB-API 2, you should wrap them in the
new Json() type constructor as a hint to PyGreSQL.
- - The new type helpers Literal() and Json() have been added.
- - Fast parsers cast_array() and cast_record() for the input and output
- syntax for PostgreSQL arrays and composite types have been added to the
- C extension module. The array parser also allows using multi-dimensional
- arrays with PyGreSQL.
+ - A new type helper Literal() for inserting parameters literally as SQL
+ has been added. This is useful for table names, for instance.
+ - Fast parsers cast_array(), cast_record() and cast_hstore for the input
+ and output syntax for PostgreSQL arrays, composite types and the hstore
+ type have been added to the C extension module. The array parser also
+ allows using multi-dimensional arrays with PyGreSQL.
- The tty parameter and attribute of database connections has been
removed since it is not supported any more since PostgreSQL 7.4.
Modified: trunk/docs/contents/pg/module.rst
==============================================================================
--- trunk/docs/contents/pg/module.rst Thu Feb 4 07:37:31 2016 (r816)
+++ trunk/docs/contents/pg/module.rst Thu Feb 4 15:18:08 2016 (r817)
@@ -715,8 +715,9 @@
------------
The module provides the following type helper functions. You can wrap
-parameters with these functions when passing them to :meth:`DB.query_formatted`
-in order to give PyGreSQL a hint about the type of the parameters.
+parameters with these functions when passing them to :meth:`DB.query`
+or :meth:`DB.query_formatted` in order to give PyGreSQL a hint about the
+type of the parameters, if it cannot be derived from the context.
.. function:: Bytea(bytes)
@@ -724,18 +725,30 @@
.. versionadded:: 5.0
+.. function:: HStore(dict)
+
+ A wrapper for holding an hstore dictionary
+
+.. versionadded:: 5.0
+
.. function:: Json(obj)
A wrapper for holding an object serializable to JSON
.. versionadded:: 5.0
+The following additional type helper is only meaningful when used with
+:meth:`DB.query_formatted`. It marks a parameter as text that shall be
+literally included into the SQL. This is useful for passing table names
+for instance.
+
.. function:: Literal(sql)
A wrapper for holding a literal SQL string
.. versionadded:: 5.0
+
Module constants
----------------
Modified: trunk/docs/contents/pgdb/module.rst
==============================================================================
--- trunk/docs/contents/pgdb/module.rst Thu Feb 4 07:37:31 2016 (r816)
+++ trunk/docs/contents/pgdb/module.rst Thu Feb 4 15:18:08 2016 (r817)
@@ -99,7 +99,6 @@
a global change is picked up by a running connection, you must reopen it or
call :meth:`TypeCache.reset_typecast` on the :attr:`Connection.type_cache`.
-
Module constants
----------------
Modified: trunk/docs/contents/pgdb/types.rst
==============================================================================
--- trunk/docs/contents/pgdb/types.rst Thu Feb 4 07:37:31 2016 (r816)
+++ trunk/docs/contents/pgdb/types.rst Thu Feb 4 15:18:08 2016 (r817)
@@ -66,6 +66,18 @@
.. versionadded:: 5.0
+.. function:: Interval(days, hours=0, minutes=0, seconds=0, microseconds=0)
+
+ Construct an object holding a time interval value
+
+.. versionadded:: 5.0
+
+.. function:: Hstore(dict)
+
+ Construct a wrapper for holding an hstore dictionary
+
+.. versionadded:: 5.0
+
.. function:: Json(obj, [encode])
Construct a wrapper for holding an object serializable to JSON
Modified: trunk/pg.py
==============================================================================
--- trunk/pg.py Thu Feb 4 07:37:31 2016 (r816)
+++ trunk/pg.py Thu Feb 4 15:18:08 2016 (r817)
@@ -179,7 +179,7 @@
' abstime reltime', # these are very old
'float': 'float4 float8',
'int': 'cid int2 int4 int8 oid xid',
- 'json': 'json jsonb',
+ 'hstore': 'hstore', 'json': 'json jsonb',
'num': 'numeric',
'money': 'money',
'text': 'bpchar char name text varchar'}
@@ -226,8 +226,29 @@
return '$%d' % len(self)
-class Literal(str):
- """Wrapper class for marking literal SQL values."""
+class Bytea(bytes):
+ """Wrapper class for marking Bytea values."""
+
+
+class Hstore(dict):
+ """Wrapper class for marking hstore values."""
+
+ _re_quote = regex('^[Nn][Uu][Ll][Ll]$|[ ,=>]')
+
+ @classmethod
+ def _quote(cls, s):
+ if s is None:
+ return 'NULL'
+ if not s:
+ return '""'
+ s = s.replace('"', '\\"')
+ if cls._re_quote.search(s):
+ s = '"%s"' % s
+ return s
+
+ def __str__(self):
+ q = self._quote
+ return ','.join('%s=>%s' % (q(k), q(v)) for k, v in self.items())
class Json:
@@ -237,8 +258,8 @@
self.obj = obj
-class Bytea(bytes):
- """Wrapper class for marking Bytea values."""
+class Literal(str):
+ """Wrapper class for marking literal SQL values."""
class Adapter:
@@ -835,8 +856,8 @@
defaults = {'char': str, 'bpchar': str, 'name': str,
'text': str, 'varchar': str,
'bool': cast_bool, 'bytea': unescape_bytea,
- 'int2': int, 'int4': int, 'serial': int,
- 'int8': long, 'json': cast_json, 'jsonb': cast_json,
+ 'int2': int, 'int4': int, 'serial': int, 'int8': long,
+ 'hstore': cast_hstore, 'json': cast_json, 'jsonb': cast_json,
'oid': long, 'oid8': long,
'float4': float, 'float8': float,
'numeric': cast_num, 'money': cast_money,
Modified: trunk/pgdb.py
==============================================================================
--- trunk/pgdb.py Thu Feb 4 07:37:31 2016 (r816)
+++ trunk/pgdb.py Thu Feb 4 15:18:08 2016 (r817)
@@ -386,8 +386,8 @@
defaults = {'char': str, 'bpchar': str, 'name': str,
'text': str, 'varchar': str,
'bool': cast_bool, 'bytea': unescape_bytea,
- 'int2': int, 'int4': int, 'serial': int,
- 'int8': long, 'json': jsondecode, 'jsonb': jsondecode,
+ 'int2': int, 'int4': int, 'serial': int, 'int8': long,
+ 'hstore': cast_hstore, 'json': jsondecode, 'jsonb': jsondecode,
'oid': long, 'oid8': long,
'float4': float, 'float8': float,
'numeric': Decimal, 'money': cast_money,
@@ -730,7 +730,7 @@
"""Quote value depending on its type."""
if value is None:
return 'NULL'
- if isinstance(value, (datetime, date, time, timedelta, Json)):
+ if isinstance(value, (datetime, date, time, timedelta, Hstore, Json)):
value = str(value)
if isinstance(value, basestring):
if isinstance(value, Binary):
@@ -1559,13 +1559,35 @@
# Additional type helpers for PyGreSQL:
+class Bytea(bytes):
+ """Construct an object capable of holding a bytea value."""
+
+
def Interval(days, hours=0, minutes=0, seconds=0, microseconds=0):
"""Construct an object holding a time inverval value."""
return timedelta(days, hours=hours, minutes=minutes, seconds=seconds,
microseconds=microseconds)
-class Bytea(bytes):
- """Construct an object capable of holding a bytea value."""
+
+class Hstore(dict):
+ """Wrapper class for marking hstore values."""
+
+ _re_quote = regex('^[Nn][Uu][Ll][Ll]$|[ ,=>]')
+
+ @classmethod
+ def _quote(cls, s):
+ if s is None:
+ return 'NULL'
+ if not s:
+ return '""'
+ s = s.replace('"', '\\"')
+ if cls._re_quote.search(s):
+ s = '"%s"' % s
+ return s
+
+ def __str__(self):
+ q = self._quote
+ return ','.join('%s=>%s' % (q(k), q(v)) for k, v in self.items())
class Json:
@@ -1581,8 +1603,6 @@
return obj
return self.encode(obj)
- __pg_repr__ = __str__
-
class Literal:
"""Construct a wrapper for holding a literal SQL string."""
@@ -1595,7 +1615,6 @@
__pg_repr__ = __str__
-
# If run as script, print some information:
if __name__ == '__main__':
Modified: trunk/pgmodule.c
==============================================================================
--- trunk/pgmodule.c Thu Feb 4 07:37:31 2016 (r816)
+++ trunk/pgmodule.c Thu Feb 4 15:18:08 2016 (r817)
@@ -1076,6 +1076,198 @@
return ret;
}
+/* Cast string s with size and encoding to a Python dictionary.
+ using the input and output syntax for hstore values. */
+
+static PyObject *
+cast_hstore(char *s, Py_ssize_t size, int encoding)
+{
+ PyObject *result;
+ char *end = s + size;
+
+ result = PyDict_New();
+
+ /* everything is set up, start parsing the record */
+ while (s != end)
+ {
+ char *key, *val;
+ PyObject *key_obj, *val_obj;
+ Py_ssize_t key_esc = 0, val_esc = 0, size;
+ int quoted;
+
+ while (s != end && *s == ' ') ++s;
+ if (s == end) break;
+ quoted = *s == '"';
+ if (quoted)
+ {
+ key = ++s;
+ while (s != end)
+ {
+ if (*s == '"') break;
+ if (*s == '\\')
+ {
+ if (++s == end) break;
+ ++key_esc;
+ }
+ ++s;
+ }
+ if (s == end)
+ {
+ PyErr_SetString(PyExc_ValueError, "Unterminated
quote");
+ Py_DECREF(result); return NULL;
+ }
+ }
+ else
+ {
+ key = s;
+ while (s != end)
+ {
+ if (*s == '=' || *s == ' ') break;
+ if (*s == '\\')
+ {
+ if (++s == end) break;
+ ++key_esc;
+ }
+ ++s;
+ }
+ if (s == key)
+ {
+ PyErr_SetString(PyExc_ValueError, "Missing
key");
+ Py_DECREF(result); return NULL;
+ }
+ }
+ size = s - key - key_esc;
+ if (key_esc)
+ {
+ char *r = key, *t;
+ key = (char *) PyMem_Malloc(size);
+ if (!key)
+ {
+ Py_DECREF(result); return PyErr_NoMemory();
+ }
+ t = key;
+ while (r != s)
+ {
+ if (*r == '\\')
+ {
+ ++r; if (r == s) break;
+ }
+ *t++ = *r++;
+ }
+ }
+ key_obj = cast_sized_text(key, size, encoding, PYGRES_TEXT);
+ if (key_esc) PyMem_Free(key);
+ if (!key_obj)
+ {
+ Py_DECREF(result); return NULL;
+ }
+ if (quoted) ++s;
+ while (s != end && *s == ' ') ++s;
+ if (s == end || *s++ != '=' || s == end || *s++ != '>')
+ {
+ PyErr_SetString(PyExc_ValueError, "Invalid characters
after key");
+ Py_DECREF(key_obj); Py_DECREF(result); return NULL;
+ }
+ while (s != end && *s == ' ') ++s;
+ quoted = *s == '"';
+ if (quoted)
+ {
+ val = ++s;
+ while (s != end)
+ {
+ if (*s == '"') break;
+ if (*s == '\\')
+ {
+ if (++s == end) break;
+ ++val_esc;
+ }
+ ++s;
+ }
+ if (s == end)
+ {
+ PyErr_SetString(PyExc_ValueError, "Unterminated
quote");
+ Py_DECREF(result); return NULL;
+ }
+ }
+ else
+ {
+ val = s;
+ while (s != end)
+ {
+ if (*s == ',' || *s == ' ') break;
+ if (*s == '\\')
+ {
+ if (++s == end) break;
+ ++val_esc;
+ }
+ ++s;
+ }
+ if (s == val)
+ {
+ PyErr_SetString(PyExc_ValueError, "Missing
value");
+ Py_DECREF(key_obj); Py_DECREF(result); return
NULL;
+ }
+ if (STR_IS_NULL(val, s - val))
+ val = NULL;
+ }
+ if (val)
+ {
+ size = s - val - val_esc;
+ if (val_esc)
+ {
+ char *r = val, *t;
+ val = (char *) PyMem_Malloc(size);
+ if (!val)
+ {
+ Py_DECREF(key_obj); Py_DECREF(result);
+ return PyErr_NoMemory();
+ }
+ t = val;
+ while (r != s)
+ {
+ if (*r == '\\')
+ {
+ ++r; if (r == s) break;
+ }
+ *t++ = *r++;
+ }
+ }
+ val_obj = cast_sized_text(val, size, encoding,
PYGRES_TEXT);
+ if (val_esc) PyMem_Free(val);
+ if (!val_obj)
+ {
+ Py_DECREF(key_obj); Py_DECREF(result); return
NULL;
+ }
+ }
+ else
+ {
+ Py_INCREF(Py_None); val_obj = Py_None;
+ }
+ if (quoted) ++s;
+ while (s != end && *s == ' ') ++s;
+ if (s != end)
+ {
+ if (*s++ != ',')
+ {
+ PyErr_SetString(PyExc_ValueError,
+ "Invalid characters after val");
+ Py_DECREF(key_obj); Py_DECREF(val_obj);
+ Py_DECREF(result); return NULL;
+ }
+ while (s != end && *s == ' ') ++s;
+ if (s == end)
+ {
+ PyErr_SetString(PyExc_ValueError, "Missing
entry");
+ Py_DECREF(key_obj); Py_DECREF(val_obj);
+ Py_DECREF(result); return NULL;
+ }
+ }
+ PyDict_SetItem(result, key_obj, val_obj);
+ Py_DECREF(key_obj); Py_DECREF(val_obj);
+ }
+ return result;
+}
+
/* internal wrapper for the notice receiver callback */
static void
notice_receiver(void *arg, const PGresult *res)
@@ -5420,16 +5612,16 @@
if (PyBytes_Check(string_obj))
{
- encoding = pg_encoding_ascii;
PyBytes_AsStringAndSize(string_obj, &string, &size);
string_obj = NULL;
+ encoding = pg_encoding_ascii;
}
else if (PyUnicode_Check(string_obj))
{
- encoding = pg_encoding_utf8;
- string_obj = get_encoded_string(string_obj, encoding);
+ string_obj = PyUnicode_AsUTF8String(string_obj);
if (!string_obj) return NULL; /* pass the UnicodeEncodeError */
PyBytes_AsStringAndSize(string_obj, &string, &size);
+ encoding = pg_encoding_utf8;
}
else
{
@@ -5478,16 +5670,16 @@
if (PyBytes_Check(string_obj))
{
- encoding = pg_encoding_ascii;
PyBytes_AsStringAndSize(string_obj, &string, &size);
string_obj = NULL;
+ encoding = pg_encoding_ascii;
}
else if (PyUnicode_Check(string_obj))
{
- encoding = pg_encoding_utf8;
- string_obj = get_encoded_string(string_obj, encoding);
+ string_obj = PyUnicode_AsUTF8String(string_obj);
if (!string_obj) return NULL; /* pass the UnicodeEncodeError */
PyBytes_AsStringAndSize(string_obj, &string, &size);
+ encoding = pg_encoding_utf8;
}
else
{
@@ -5527,6 +5719,43 @@
return ret;
}
+/* cast a string with a text representation of an hstore to a dict */
+static char pgCastHStore__doc__[] =
+"cast_hstore(string) -- cast a string as an hstore";
+
+PyObject *
+pgCastHStore(PyObject *self, PyObject *string)
+{
+ PyObject *tmp_obj = NULL, *ret;
+ char *s;
+ Py_ssize_t size;
+ int encoding;
+
+ if (PyBytes_Check(string))
+ {
+ PyBytes_AsStringAndSize(string, &s, &size);
+ encoding = pg_encoding_ascii;
+ }
+ else if (PyUnicode_Check(string))
+ {
+ tmp_obj = PyUnicode_AsUTF8String(string);
+ if (!tmp_obj) return NULL; /* pass the UnicodeEncodeError */
+ PyBytes_AsStringAndSize(tmp_obj, &s, &size);
+ encoding = pg_encoding_utf8;
+ }
+ else
+ {
+ PyErr_SetString(PyExc_TypeError,
+ "Function cast_hstore() expects a string as first
argument");
+ return NULL;
+ }
+
+ ret = cast_hstore(s, size, encoding);
+
+ Py_XDECREF(tmp_obj);
+
+ return ret;
+}
/* List of functions defined in the module */
@@ -5571,6 +5800,7 @@
pgCastArray__doc__},
{"cast_record", (PyCFunction) pgCastRecord, METH_VARARGS|METH_KEYWORDS,
pgCastRecord__doc__},
+ {"cast_hstore", (PyCFunction) pgCastHStore, METH_O,
pgCastHStore__doc__},
#ifdef DEFAULT_VARS
{"get_defhost", pgGetDefHost, METH_NOARGS, pgGetDefHost__doc__},
Modified: trunk/tests/test_classic_dbwrapper.py
==============================================================================
--- trunk/tests/test_classic_dbwrapper.py Thu Feb 4 07:37:31 2016
(r816)
+++ trunk/tests/test_classic_dbwrapper.py Thu Feb 4 15:18:08 2016
(r817)
@@ -3683,6 +3683,23 @@
self.assertIsInstance(r[1], list)
self.assertEqual(r[1][0], dt[1])
+ def testHstore(self):
+ try:
+ self.db.query("select 'k=>v'::hstore")
+ except pg.ProgrammingEror:
+ try:
+ self.db.query("create extension hstore")
+ except pg.ProgrammingError:
+ self.skipTest("hstore extension not enabled")
+ d = {'k': 'v', 'foo': 'bar', 'baz': 'whatever',
+ '1a': 'anything at all', '2=b': 'value = 2', '3>c': 'value > 3',
+ '4"c': 'value " 4', "5'c": "value ' 5", 'hello, world': '"hi!"',
+ 'None': None, 'NULL': 'NULL', 'empty': ''}
+ q = "select $1::hstore"
+ r = self.db.query(q, (pg.Hstore(d),)).getresult()[0][0]
+ self.assertIsInstance(r, dict)
+ self.assertEqual(r, d)
+
def testDbTypesInfo(self):
dbtypes = self.db.dbtypes
self.assertIsInstance(dbtypes, dict)
Modified: trunk/tests/test_classic_functions.py
==============================================================================
--- trunk/tests/test_classic_functions.py Thu Feb 4 07:37:31 2016
(r816)
+++ trunk/tests/test_classic_functions.py Thu Feb 4 15:18:08 2016
(r817)
@@ -616,6 +616,56 @@
self.assertEqual(f(string, cast, b';'), expected)
+class TestParseHStore(unittest.TestCase):
+ """Test the hstore parser."""
+
+ test_strings = [
+ ('', {}),
+ ('=>', ValueError),
+ ('""=>', ValueError),
+ ('=>""', ValueError),
+ ('""=>""', {'': ''}),
+ ('NULL=>NULL', {'NULL': None}),
+ ('null=>null', {'null': None}),
+ ('NULL=>"NULL"', {'NULL': 'NULL'}),
+ ('null=>"null"', {'null': 'null'}),
+ ('k', ValueError),
+ ('k,', ValueError),
+ ('k=', ValueError),
+ ('k=>', ValueError),
+ ('k=>v', {'k': 'v'}),
+ ('k=>v,', ValueError),
+ (' k => v ', {'k': 'v'}),
+ (' k => v ', {'k': 'v'}),
+ ('" k " => " v "', {' k ': ' v '}),
+ ('"k=>v', ValueError),
+ ('k=>"v', ValueError),
+ ('"1-a" => "anything at all"', {'1-a': 'anything at all'}),
+ ('k => v, foo => bar, baz => whatever,'
+ ' "1-a" => "anything at all"',
+ {'k': 'v', 'foo': 'bar', 'baz': 'whatever',
+ '1-a': 'anything at all'}),
+ ('"Hello, World!"=>"Hi!"', {'Hello, World!': 'Hi!'}),
+ ('"Hi!"=>"Hello, World!"', {'Hi!': 'Hello, World!'}),
+ ('"k=>v"=>k\=\>v', {'k=>v': 'k=>v'}),
+ ('k\=\>v=>"k=>v"', {'k=>v': 'k=>v'}),
+ ('a\\,b=>a,b=>a', {'a,b': 'a', 'b': 'a'})]
+
+ def testParser(self):
+ f = pg.cast_hstore
+
+ self.assertRaises(TypeError, f)
+ self.assertRaises(TypeError, f, None)
+ self.assertRaises(TypeError, f, 42)
+ self.assertRaises(TypeError, f, '', None)
+
+ for string, expected in self.test_strings:
+ if expected is ValueError:
+ self.assertRaises(ValueError, f, string)
+ else:
+ self.assertEqual(f(string), expected)
+
+
class TestCastInterval(unittest.TestCase):
"""Test the interval typecast function."""
Modified: trunk/tests/test_dbapi20.py
==============================================================================
--- trunk/tests/test_dbapi20.py Thu Feb 4 07:37:31 2016 (r816)
+++ trunk/tests/test_dbapi20.py Thu Feb 4 15:18:08 2016 (r817)
@@ -590,6 +590,32 @@
finally:
con.close()
+ def test_hstore(self):
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ cur.execute("select 'k=>v'::hstore")
+ except pgdb.ProgrammingError:
+ try:
+ cur.execute("create extension hstore")
+ except pgdb.ProgrammingError:
+ self.skipTest("hstore extension not enabled")
+ finally:
+ con.close()
+ d = {'k': 'v', 'foo': 'bar', 'baz': 'whatever',
+ '1a': 'anything at all', '2=b': 'value = 2', '3>c': 'value > 3',
+ '4"c': 'value " 4', "5'c": "value ' 5", 'hello, world': '"hi!"',
+ 'None': None, 'NULL': 'NULL', 'empty': ''}
+ con = self._connect()
+ try:
+ cur = con.cursor()
+ cur.execute("select %s::hstore", (pgdb.Hstore(d),))
+ result = cur.fetchone()[0]
+ finally:
+ con.close()
+ self.assertIsInstance(result, dict)
+ self.assertEqual(result, d)
+
def test_insert_array(self):
values = [(None, None), ([], []), ([None], [[None], ['null']]),
([1, 2, 3], [['a', 'b'], ['c', 'd']]),
_______________________________________________
PyGreSQL mailing list
[email protected]
https://mail.vex.net/mailman/listinfo.cgi/pygresql