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

Reply via email to