Changeset 817 for trunk


Ignore:
Timestamp:
Feb 4, 2016, 3:18:08 PM (4 years ago)
Author:
cito
Message:

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.

Location:
trunk
Files:
10 edited

Legend:

Unmodified
Added
Removed
  • trunk/docs/contents/changelog.rst

    r814 r817  
    77- The supported versions are Python 2.6 to 2.7, and 3.3 to 3.5.
    88- PostgreSQL is supported in all versions from 9.0 to 9.5.
    9 - Changes in the DB-API 2 module (pgdb):
    10     - The DB-API 2 module now always returns result rows as named tuples
    11       instead of simply lists as before. The documentation explains how
    12       you can restore the old behavior or use custom row objects instead.
    13     - The names of the various classes used by the classic and DB-API 2
    14       modules have been renamed to become simpler, more intuitive and in
    15       line with the names used in the DB-API 2 documentation.
    16       Since the API provides only objects of these types through constructor
    17       functions, this should not cause any incompatibilities.
    18     - The DB-API 2 module now supports the callproc() cursor method. Note
    19       that output parameters are currently not replaced in the return value.
    20     - The DB-API 2 module now supports copy operations between data streams
    21       on the client and database tables via the COPY command of PostgreSQL.
    22       The cursor method copy_from() can be used to copy data from the database
    23       to the client, and the cursor method copy_to() can be used to copy data
    24       from the client to the database.
    25     - The 7-tuples returned by the description attribute of a pgdb cursor
    26       are now named tuples, i.e. their elements can be also accessed by name.
    27       The column names and types can now also be requested through the
    28       colnames and coltypes attributes, which are not part of DB-API 2 though.
    29       The type_code provided by the description attribute is still equal to
    30       the PostgreSQL internal type name, but now carries some more information
    31       in additional attributes. The size, precision and scale information that
    32       is part of the description is now properly set for numeric types.
    33     - If you pass a Python list as one of the parameters to a DB-API 2 cursor,
    34       it is now automatically bound using an ARRAY constructor. If you pass a
    35       Python tuple, it is bound using a ROW constructor. This is useful for
    36       passing records as well as making use of the IN syntax.
    37     - Inversely, when a fetch method of a DB-API 2 cursor returns a PostgreSQL
    38       array, it is passed to Python as a list, and when it returns a PostgreSQL
    39       composite type, it is passed to Python as a named tuple. PyGreSQL uses
    40       a new fast built-in parser to achieve this. Anonymous composite types are
    41       also supported, but yield only an ordinary tuple containing text strings.
    42     - A new type helper Interval() has been added.
    439- Changes in the classic PyGreSQL module (pg):
    4410    - The classic interface got two new methods get_as_list() and get_as_dict()
     
    9258      can be set to specify whether parameters should be sent to the database
    9359      separately or formatted into the SQL.
    94     - The methods for adapting and typecasting values pertaining to PostgreSQL
    95       types have been refactored and swapped out to separate classes.
    9660    - A new type helper Bytea() has been added.
     61- Changes in the DB-API 2 module (pgdb):
     62    - The DB-API 2 module now always returns result rows as named tuples
     63      instead of simply lists as before. The documentation explains how
     64      you can restore the old behavior or use custom row objects instead.
     65    - The names of the various classes used by the classic and DB-API 2
     66      modules have been renamed to become simpler, more intuitive and in
     67      line with the names used in the DB-API 2 documentation.
     68      Since the API provides only objects of these types through constructor
     69      functions, this should not cause any incompatibilities.
     70    - The DB-API 2 module now supports the callproc() cursor method. Note
     71      that output parameters are currently not replaced in the return value.
     72    - The DB-API 2 module now supports copy operations between data streams
     73      on the client and database tables via the COPY command of PostgreSQL.
     74      The cursor method copy_from() can be used to copy data from the database
     75      to the client, and the cursor method copy_to() can be used to copy data
     76      from the client to the database.
     77    - The 7-tuples returned by the description attribute of a pgdb cursor
     78      are now named tuples, i.e. their elements can be also accessed by name.
     79      The column names and types can now also be requested through the
     80      colnames and coltypes attributes, which are not part of DB-API 2 though.
     81      The type_code provided by the description attribute is still equal to
     82      the PostgreSQL internal type name, but now carries some more information
     83      in additional attributes. The size, precision and scale information that
     84      is part of the description is now properly set for numeric types.
     85    - If you pass a Python list as one of the parameters to a DB-API 2 cursor,
     86      it is now automatically bound using an ARRAY constructor. If you pass a
     87      Python tuple, it is bound using a ROW constructor. This is useful for
     88      passing records as well as making use of the IN syntax.
     89    - Inversely, when a fetch method of a DB-API 2 cursor returns a PostgreSQL
     90      array, it is passed to Python as a list, and when it returns a PostgreSQL
     91      composite type, it is passed to Python as a named tuple. PyGreSQL uses
     92      a new fast built-in parser to achieve this. Anonymous composite types are
     93      also supported, but yield only an ordinary tuple containing text strings.
     94    - A new type helper Interval() has been added.
    9795- Changes concerning both modules:
    9896    - The modules now provide get_typecast() and set_typecast() methods
     
    107105      strings.  You can restore the old behavior by deactivating the respective
    108106      typecast functions, e.g. set_typecast('date', None).
    109     - PyGreSQL now supports the JSON and JSONB data types, converting such
     107    - PyGreSQL now supports the "hstore" data type, converting such columns
     108      automatically to and from Python dictionaries.  If you want to insert
     109      Python objects as JSON data using DB-API 2, you should wrap them in the
     110      new HStore() type constructor as a hint to PyGreSQL.
     111    - PyGreSQL now supports the "json" and "jsonb" data types, converting such
    110112      columns automatically to and from Python objects. If you want to insert
    111113      Python objects as JSON data using DB-API 2, you should wrap them in the
    112114      new Json() type constructor as a hint to PyGreSQL.
    113     - The new type helpers Literal() and Json() have been added.
    114     - Fast parsers cast_array() and cast_record() for the input and output
    115       syntax for PostgreSQL arrays and composite types have been added to the
    116       C extension module. The array parser also allows using multi-dimensional
    117       arrays with PyGreSQL.
     115    - A new type helper Literal() for inserting parameters literally as SQL
     116      has been added.  This is useful for table names, for instance.
     117    - Fast parsers cast_array(), cast_record() and cast_hstore for the input
     118      and output syntax for PostgreSQL arrays, composite types and the hstore
     119      type have been added to the C extension module. The array parser also
     120      allows using multi-dimensional arrays with PyGreSQL.
    118121    - The tty parameter and attribute of database connections has been
    119122      removed since it is not supported any more since PostgreSQL 7.4.
  • trunk/docs/contents/pg/module.rst

    r814 r817  
    716716
    717717The module provides the following type helper functions.  You can wrap
    718 parameters with these functions when passing them to :meth:`DB.query_formatted`
    719 in order to give PyGreSQL a hint about the type of the parameters.
     718parameters with these functions when passing them to :meth:`DB.query`
     719or :meth:`DB.query_formatted` in order to give PyGreSQL a hint about the
     720type of the parameters, if it cannot be derived from the context.
    720721
    721722.. function:: Bytea(bytes)
     
    725726.. versionadded:: 5.0
    726727
     728.. function:: HStore(dict)
     729
     730    A wrapper for holding an hstore dictionary
     731
     732.. versionadded:: 5.0
     733
    727734.. function:: Json(obj)
    728735
     
    731738.. versionadded:: 5.0
    732739
     740The following additional type helper is only meaningful when used with
     741:meth:`DB.query_formatted`.  It marks a parameter as text that shall be
     742literally included into the SQL.  This is useful for passing table names
     743for instance.
     744
    733745.. function:: Literal(sql)
    734746
     
    736748
    737749.. versionadded:: 5.0
     750
    738751
    739752Module constants
  • trunk/docs/contents/pgdb/module.rst

    r814 r817  
    100100call :meth:`TypeCache.reset_typecast` on the :attr:`Connection.type_cache`.
    101101
    102 
    103102Module constants
    104103----------------
  • trunk/docs/contents/pgdb/types.rst

    r814 r817  
    6464
    6565    Construct an object capable of holding a bytea value
     66
     67.. versionadded:: 5.0
     68
     69.. function:: Interval(days, hours=0, minutes=0, seconds=0, microseconds=0)
     70
     71    Construct an object holding a time interval value
     72
     73.. versionadded:: 5.0
     74
     75.. function:: Hstore(dict)
     76
     77    Construct a wrapper for holding an hstore dictionary
    6678
    6779.. versionadded:: 5.0
  • trunk/pg.py

    r815 r817  
    180180        'float': 'float4 float8',
    181181        'int': 'cid int2 int4 int8 oid xid',
    182         'json': 'json jsonb',
     182        'hstore': 'hstore', 'json': 'json jsonb',
    183183        'num': 'numeric',
    184184        'money': 'money',
     
    227227
    228228
     229class Bytea(bytes):
     230    """Wrapper class for marking Bytea values."""
     231
     232
     233class Hstore(dict):
     234    """Wrapper class for marking hstore values."""
     235
     236    _re_quote = regex('^[Nn][Uu][Ll][Ll]$|[ ,=>]')
     237
     238    @classmethod
     239    def _quote(cls, s):
     240        if s is None:
     241            return 'NULL'
     242        if not s:
     243            return '""'
     244        s = s.replace('"', '\\"')
     245        if cls._re_quote.search(s):
     246            s = '"%s"' % s
     247        return s
     248
     249    def __str__(self):
     250        q = self._quote
     251        return ','.join('%s=>%s' % (q(k), q(v)) for k, v in self.items())
     252
     253
     254class Json:
     255    """Wrapper class for marking Json values."""
     256
     257    def __init__(self, obj):
     258        self.obj = obj
     259
     260
    229261class Literal(str):
    230262    """Wrapper class for marking literal SQL values."""
    231 
    232 
    233 class Json:
    234     """Wrapper class for marking Json values."""
    235 
    236     def __init__(self, obj):
    237         self.obj = obj
    238 
    239 
    240 class Bytea(bytes):
    241     """Wrapper class for marking Bytea values."""
    242263
    243264
     
    836857        'text': str, 'varchar': str,
    837858        'bool': cast_bool, 'bytea': unescape_bytea,
    838         'int2': int, 'int4': int, 'serial': int,
    839         'int8': long, 'json': cast_json, 'jsonb': cast_json,
     859        'int2': int, 'int4': int, 'serial': int, 'int8': long,
     860        'hstore': cast_hstore, 'json': cast_json, 'jsonb': cast_json,
    840861        'oid': long, 'oid8': long,
    841862        'float4': float, 'float8': float,
  • trunk/pgdb.py

    r815 r817  
    387387        'text': str, 'varchar': str,
    388388        'bool': cast_bool, 'bytea': unescape_bytea,
    389         'int2': int, 'int4': int, 'serial': int,
    390         'int8': long, 'json': jsondecode, 'jsonb': jsondecode,
     389        'int2': int, 'int4': int, 'serial': int, 'int8': long,
     390        'hstore': cast_hstore, 'json': jsondecode, 'jsonb': jsondecode,
    391391        'oid': long, 'oid8': long,
    392392        'float4': float, 'float8': float,
     
    731731        if value is None:
    732732            return 'NULL'
    733         if isinstance(value, (datetime, date, time, timedelta, Json)):
     733        if isinstance(value, (datetime, date, time, timedelta, Hstore, Json)):
    734734            value = str(value)
    735735        if isinstance(value, basestring):
     
    15601560# Additional type helpers for PyGreSQL:
    15611561
     1562class Bytea(bytes):
     1563    """Construct an object capable of holding a bytea value."""
     1564
     1565
    15621566def Interval(days, hours=0, minutes=0, seconds=0, microseconds=0):
    15631567    """Construct an object holding a time inverval value."""
     
    15651569        microseconds=microseconds)
    15661570
    1567 class Bytea(bytes):
    1568     """Construct an object capable of holding a bytea value."""
     1571
     1572class Hstore(dict):
     1573    """Wrapper class for marking hstore values."""
     1574
     1575    _re_quote = regex('^[Nn][Uu][Ll][Ll]$|[ ,=>]')
     1576
     1577    @classmethod
     1578    def _quote(cls, s):
     1579        if s is None:
     1580            return 'NULL'
     1581        if not s:
     1582            return '""'
     1583        s = s.replace('"', '\\"')
     1584        if cls._re_quote.search(s):
     1585            s = '"%s"' % s
     1586        return s
     1587
     1588    def __str__(self):
     1589        q = self._quote
     1590        return ','.join('%s=>%s' % (q(k), q(v)) for k, v in self.items())
    15691591
    15701592
     
    15821604        return self.encode(obj)
    15831605
    1584     __pg_repr__ = __str__
    1585 
    15861606
    15871607class Literal:
     
    15951615
    15961616    __pg_repr__ = __str__
    1597 
    15981617
    15991618# If run as script, print some information:
  • trunk/pgmodule.c

    r814 r817  
    10771077}
    10781078
     1079/* Cast string s with size and encoding to a Python dictionary.
     1080   using the input and output syntax for hstore values. */
     1081
     1082static PyObject *
     1083cast_hstore(char *s, Py_ssize_t size, int encoding)
     1084{
     1085        PyObject   *result;
     1086        char       *end = s + size;
     1087
     1088    result = PyDict_New();
     1089
     1090        /* everything is set up, start parsing the record */
     1091        while (s != end)
     1092        {
     1093                char       *key, *val;
     1094                PyObject   *key_obj, *val_obj;
     1095                Py_ssize_t      key_esc = 0, val_esc = 0, size;
     1096                int                     quoted;
     1097
     1098                while (s != end && *s == ' ') ++s;
     1099                if (s == end) break;
     1100                quoted = *s == '"';
     1101                if (quoted)
     1102                {
     1103                        key = ++s;
     1104                        while (s != end)
     1105                        {
     1106                                if (*s == '"') break;
     1107                                if (*s == '\\')
     1108                                {
     1109                                        if (++s == end) break;
     1110                                        ++key_esc;
     1111                                }
     1112                                ++s;
     1113                        }
     1114                        if (s == end)
     1115                        {
     1116                                PyErr_SetString(PyExc_ValueError, "Unterminated quote");
     1117                                Py_DECREF(result); return NULL;
     1118                        }
     1119                }
     1120                else
     1121                {
     1122                        key = s;
     1123                        while (s != end)
     1124                        {
     1125                                if (*s == '=' || *s == ' ') break;
     1126                                if (*s == '\\')
     1127                                {
     1128                                        if (++s == end) break;
     1129                                        ++key_esc;
     1130                                }
     1131                                ++s;
     1132                        }
     1133                        if (s == key)
     1134                        {
     1135                                PyErr_SetString(PyExc_ValueError, "Missing key");
     1136                                Py_DECREF(result); return NULL;
     1137                        }
     1138                }
     1139                size = s - key - key_esc;
     1140                if (key_esc)
     1141                {
     1142                        char *r = key, *t;
     1143                        key = (char *) PyMem_Malloc(size);
     1144                        if (!key)
     1145                        {
     1146                                Py_DECREF(result); return PyErr_NoMemory();
     1147                        }
     1148                        t = key;
     1149                        while (r != s)
     1150                        {
     1151                                if (*r == '\\')
     1152                                {
     1153                                        ++r; if (r == s) break;
     1154                                }
     1155                                *t++ = *r++;
     1156                        }
     1157                }
     1158                key_obj = cast_sized_text(key, size, encoding, PYGRES_TEXT);
     1159                if (key_esc) PyMem_Free(key);
     1160                if (!key_obj)
     1161                {
     1162                        Py_DECREF(result); return NULL;
     1163                }
     1164                if (quoted) ++s;
     1165                while (s != end && *s == ' ') ++s;
     1166                if (s == end || *s++ != '=' || s == end || *s++ != '>')
     1167                {
     1168                        PyErr_SetString(PyExc_ValueError, "Invalid characters after key");
     1169                        Py_DECREF(key_obj); Py_DECREF(result); return NULL;
     1170                }
     1171                while (s != end && *s == ' ') ++s;
     1172                quoted = *s == '"';
     1173                if (quoted)
     1174                {
     1175                        val = ++s;
     1176                        while (s != end)
     1177                        {
     1178                                if (*s == '"') break;
     1179                                if (*s == '\\')
     1180                                {
     1181                                        if (++s == end) break;
     1182                                        ++val_esc;
     1183                                }
     1184                                ++s;
     1185                        }
     1186                        if (s == end)
     1187                        {
     1188                                PyErr_SetString(PyExc_ValueError, "Unterminated quote");
     1189                                Py_DECREF(result); return NULL;
     1190                        }
     1191                }
     1192                else
     1193                {
     1194                        val = s;
     1195                        while (s != end)
     1196                        {
     1197                                if (*s == ',' || *s == ' ') break;
     1198                                if (*s == '\\')
     1199                                {
     1200                                        if (++s == end) break;
     1201                                        ++val_esc;
     1202                                }
     1203                                ++s;
     1204                        }
     1205                        if (s == val)
     1206                        {
     1207                                PyErr_SetString(PyExc_ValueError, "Missing value");
     1208                                Py_DECREF(key_obj); Py_DECREF(result); return NULL;
     1209                        }
     1210                        if (STR_IS_NULL(val, s - val))
     1211                                val = NULL;
     1212                }
     1213                if (val)
     1214                {
     1215                        size = s - val - val_esc;
     1216                        if (val_esc)
     1217                        {
     1218                                char *r = val, *t;
     1219                                val = (char *) PyMem_Malloc(size);
     1220                                if (!val)
     1221                                {
     1222                                        Py_DECREF(key_obj); Py_DECREF(result);
     1223                                        return PyErr_NoMemory();
     1224                                }
     1225                                t = val;
     1226                                while (r != s)
     1227                                {
     1228                                        if (*r == '\\')
     1229                                        {
     1230                                                ++r; if (r == s) break;
     1231                                        }
     1232                                        *t++ = *r++;
     1233                                }
     1234                        }
     1235                        val_obj = cast_sized_text(val, size, encoding, PYGRES_TEXT);
     1236                        if (val_esc) PyMem_Free(val);
     1237                        if (!val_obj)
     1238                        {
     1239                                Py_DECREF(key_obj); Py_DECREF(result); return NULL;
     1240                        }
     1241                }
     1242                else
     1243                {
     1244                        Py_INCREF(Py_None); val_obj = Py_None;
     1245                }
     1246                if (quoted) ++s;
     1247                while (s != end && *s == ' ') ++s;
     1248                if (s != end)
     1249                {
     1250                        if (*s++ != ',')
     1251                        {
     1252                                PyErr_SetString(PyExc_ValueError,
     1253                                        "Invalid characters after val");
     1254                                Py_DECREF(key_obj); Py_DECREF(val_obj);
     1255                                Py_DECREF(result); return NULL;
     1256                        }
     1257                        while (s != end && *s == ' ') ++s;
     1258                        if (s == end)
     1259                        {
     1260                                PyErr_SetString(PyExc_ValueError, "Missing entry");
     1261                                Py_DECREF(key_obj); Py_DECREF(val_obj);
     1262                                Py_DECREF(result); return NULL;
     1263                        }
     1264                }
     1265                PyDict_SetItem(result, key_obj, val_obj);
     1266                Py_DECREF(key_obj); Py_DECREF(val_obj);
     1267        }
     1268        return result;
     1269}
     1270
    10791271/* internal wrapper for the notice receiver callback */
    10801272static void
     
    54215613        if (PyBytes_Check(string_obj))
    54225614        {
    5423                 encoding = pg_encoding_ascii;
    54245615                PyBytes_AsStringAndSize(string_obj, &string, &size);
    54255616                string_obj = NULL;
     5617                encoding = pg_encoding_ascii;
    54265618        }
    54275619        else if (PyUnicode_Check(string_obj))
    54285620        {
    5429                 encoding = pg_encoding_utf8;
    5430                 string_obj = get_encoded_string(string_obj, encoding);
     5621                string_obj = PyUnicode_AsUTF8String(string_obj);
    54315622                if (!string_obj) return NULL; /* pass the UnicodeEncodeError */
    54325623                PyBytes_AsStringAndSize(string_obj, &string, &size);
     5624                encoding = pg_encoding_utf8;
    54335625        }
    54345626        else
     
    54795671        if (PyBytes_Check(string_obj))
    54805672        {
    5481                 encoding = pg_encoding_ascii;
    54825673                PyBytes_AsStringAndSize(string_obj, &string, &size);
    54835674                string_obj = NULL;
     5675                encoding = pg_encoding_ascii;
    54845676        }
    54855677        else if (PyUnicode_Check(string_obj))
    54865678        {
    5487                 encoding = pg_encoding_utf8;
    5488                 string_obj = get_encoded_string(string_obj, encoding);
     5679                string_obj = PyUnicode_AsUTF8String(string_obj);
    54895680                if (!string_obj) return NULL; /* pass the UnicodeEncodeError */
    54905681                PyBytes_AsStringAndSize(string_obj, &string, &size);
     5682                encoding = pg_encoding_utf8;
    54915683        }
    54925684        else
     
    55285720}
    55295721
     5722/* cast a string with a text representation of an hstore to a dict */
     5723static char pgCastHStore__doc__[] =
     5724"cast_hstore(string) -- cast a string as an hstore";
     5725
     5726PyObject *
     5727pgCastHStore(PyObject *self, PyObject *string)
     5728{
     5729        PyObject   *tmp_obj = NULL, *ret;
     5730        char       *s;
     5731        Py_ssize_t      size;
     5732        int                     encoding;
     5733
     5734        if (PyBytes_Check(string))
     5735        {
     5736                PyBytes_AsStringAndSize(string, &s, &size);
     5737                encoding = pg_encoding_ascii;
     5738        }
     5739        else if (PyUnicode_Check(string))
     5740        {
     5741                tmp_obj = PyUnicode_AsUTF8String(string);
     5742                if (!tmp_obj) return NULL; /* pass the UnicodeEncodeError */
     5743                PyBytes_AsStringAndSize(tmp_obj, &s, &size);
     5744                encoding = pg_encoding_utf8;
     5745        }
     5746        else
     5747        {
     5748                PyErr_SetString(PyExc_TypeError,
     5749                        "Function cast_hstore() expects a string as first argument");
     5750                return NULL;
     5751        }
     5752
     5753        ret = cast_hstore(s, size, encoding);
     5754
     5755        Py_XDECREF(tmp_obj);
     5756
     5757        return ret;
     5758}
    55305759
    55315760/* List of functions defined in the module */
     
    55725801        {"cast_record", (PyCFunction) pgCastRecord, METH_VARARGS|METH_KEYWORDS,
    55735802                        pgCastRecord__doc__},
     5803        {"cast_hstore", (PyCFunction) pgCastHStore, METH_O, pgCastHStore__doc__},
    55745804
    55755805#ifdef DEFAULT_VARS
  • trunk/tests/test_classic_dbwrapper.py

    r816 r817  
    36843684        self.assertEqual(r[1][0], dt[1])
    36853685
     3686    def testHstore(self):
     3687        try:
     3688            self.db.query("select 'k=>v'::hstore")
     3689        except pg.ProgrammingEror:
     3690            try:
     3691                self.db.query("create extension hstore")
     3692            except pg.ProgrammingError:
     3693                self.skipTest("hstore extension not enabled")
     3694        d = {'k': 'v', 'foo': 'bar', 'baz': 'whatever',
     3695            '1a': 'anything at all', '2=b': 'value = 2', '3>c': 'value > 3',
     3696            '4"c': 'value " 4', "5'c": "value ' 5", 'hello, world': '"hi!"',
     3697            'None': None, 'NULL': 'NULL', 'empty': ''}
     3698        q = "select $1::hstore"
     3699        r = self.db.query(q, (pg.Hstore(d),)).getresult()[0][0]
     3700        self.assertIsInstance(r, dict)
     3701        self.assertEqual(r, d)
     3702
    36863703    def testDbTypesInfo(self):
    36873704        dbtypes = self.db.dbtypes
  • trunk/tests/test_classic_functions.py

    r814 r817  
    615615                expected = replace_comma(expected)
    616616                self.assertEqual(f(string, cast, b';'), expected)
     617
     618
     619class TestParseHStore(unittest.TestCase):
     620    """Test the hstore parser."""
     621
     622    test_strings = [
     623        ('', {}),
     624        ('=>', ValueError),
     625        ('""=>', ValueError),
     626        ('=>""', ValueError),
     627        ('""=>""', {'': ''}),
     628        ('NULL=>NULL', {'NULL': None}),
     629        ('null=>null', {'null': None}),
     630        ('NULL=>"NULL"', {'NULL': 'NULL'}),
     631        ('null=>"null"', {'null': 'null'}),
     632        ('k', ValueError),
     633        ('k,', ValueError),
     634        ('k=', ValueError),
     635        ('k=>', ValueError),
     636        ('k=>v', {'k': 'v'}),
     637        ('k=>v,', ValueError),
     638        (' k => v ', {'k': 'v'}),
     639        ('   k   =>   v   ', {'k': 'v'}),
     640        ('" k " => " v "', {' k ': ' v '}),
     641        ('"k=>v', ValueError),
     642        ('k=>"v', ValueError),
     643        ('"1-a" => "anything at all"', {'1-a': 'anything at all'}),
     644        ('k => v, foo => bar, baz => whatever,'
     645                ' "1-a" => "anything at all"',
     646            {'k': 'v', 'foo': 'bar', 'baz': 'whatever',
     647            '1-a': 'anything at all'}),
     648        ('"Hello, World!"=>"Hi!"', {'Hello, World!': 'Hi!'}),
     649        ('"Hi!"=>"Hello, World!"', {'Hi!': 'Hello, World!'}),
     650        ('"k=>v"=>k\=\>v', {'k=>v': 'k=>v'}),
     651        ('k\=\>v=>"k=>v"', {'k=>v': 'k=>v'}),
     652        ('a\\,b=>a,b=>a', {'a,b': 'a', 'b': 'a'})]
     653
     654    def testParser(self):
     655        f = pg.cast_hstore
     656
     657        self.assertRaises(TypeError, f)
     658        self.assertRaises(TypeError, f, None)
     659        self.assertRaises(TypeError, f, 42)
     660        self.assertRaises(TypeError, f, '', None)
     661
     662        for string, expected in self.test_strings:
     663            if expected is ValueError:
     664                self.assertRaises(ValueError, f, string)
     665            else:
     666                self.assertEqual(f(string), expected)
    617667
    618668
  • trunk/tests/test_dbapi20.py

    r816 r817  
    590590        finally:
    591591            con.close()
     592
     593    def test_hstore(self):
     594        con = self._connect()
     595        try:
     596            cur = con.cursor()
     597            cur.execute("select 'k=>v'::hstore")
     598        except pgdb.ProgrammingError:
     599            try:
     600                cur.execute("create extension hstore")
     601            except pgdb.ProgrammingError:
     602                self.skipTest("hstore extension not enabled")
     603        finally:
     604            con.close()
     605        d = {'k': 'v', 'foo': 'bar', 'baz': 'whatever',
     606            '1a': 'anything at all', '2=b': 'value = 2', '3>c': 'value > 3',
     607            '4"c': 'value " 4', "5'c": "value ' 5", 'hello, world': '"hi!"',
     608            'None': None, 'NULL': 'NULL', 'empty': ''}
     609        con = self._connect()
     610        try:
     611            cur = con.cursor()
     612            cur.execute("select %s::hstore", (pgdb.Hstore(d),))
     613            result = cur.fetchone()[0]
     614        finally:
     615            con.close()
     616        self.assertIsInstance(result, dict)
     617        self.assertEqual(result, d)
    592618
    593619    def test_insert_array(self):
Note: See TracChangeset for help on using the changeset viewer.