Changeset 774


Ignore:
Timestamp:
Jan 21, 2016, 1:49:28 PM (4 years ago)
Author:
cito
Message:

Add support for JSON and JSONB to pg and pgdb

This adds all necessary functions to make PyGreSQL automatically
convert between JSON columns and Python objects representing them.

The documentation has also been updated, see there for the details.

Also, tuples automatically bind to ROW expressions in pgdb now.

Location:
trunk
Files:
11 edited

Legend:

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

    r772 r774  
    2626  The column names and types can now also be requested through the
    2727  colnames and coltypes attributes, which are not part of DB-API 2 though.
     28- If you pass a list as one of the parameters to a DB-API 2 cursor, it is
     29  now automatically bound as PostgreSQL ARRAY. If you pass a tuple, then
     30  it will be bound as a PostgreSQL ROW expression.
    2831- Re-activated the shortcut methods of the DB-API connection since they
    2932  can be handy when doing experiments or writing quick scripts. We keep
     
    3134- The tty parameter and attribute of database connections has been
    3235  removed since it is not supported any more since PostgreSQL 7.4.
     36- The classic interface got two new methods get_as_list() and get_as_dict()
     37  returning a database table as a Python list or dict. The amount of data
     38  returned can be controlled with various parameters.
     39- A method upsert() has been added to the DB wrapper class that exploits the
     40  "upsert" feature that is new in PostgreSQL 9.5. The new method nicely
     41  complements the existing get/insert/update/delete() methods.
     42- PyGreSQL now supports the JSON and JSONB data types, converting such
     43  columns automatically to and from Python objects. If you want to insert
     44  Python objects as JSON data using DB-API 2, you should wrap them in the
     45  new Json() type constructor as a hint to PyGreSQL.
    3346- The pkey() method of the classic interface now returns tuples instead
    3447  of frozenset. The order of the tuples is like in the primary key index.
     
    4659  DB wrapper methods has been reduced and security has been improved by
    4760  passing the values to libpq separately as parameters instead of inline.
    48 - The classic interface got two new methods get_as_list() and get_as_dict()
    49   returning a database table as a Python list or dict. The amount of data
    50   returned can be controlled with various parameters.
    5161
    5262Version 4.2
  • trunk/docs/contents/pg/db_wrapper.rst

    r770 r774  
    600600ordering.  In this case the returned dictionary will be an ordinary one.
    601601
    602 escape_literal -- escape a literal string for use within SQL
    603 ------------------------------------------------------------
     602escape_literal/identifier/string/bytea -- escape for SQL
     603--------------------------------------------------------
     604
     605The following methods escape text or binary strings so that they can be
     606inserted directly into an SQL command.  Except for :meth:`DB.escape_byte`,
     607you don't need to call these methods for the strings passed as parameters
     608to :meth:`DB.query`.  You also don't need to call any of these methods
     609when storing data using :meth:`DB.insert` and similar.
    604610
    605611.. method:: DB.escape_literal(string)
     
    617623
    618624.. versionadded:: 4.1
    619 
    620 escape_identifier -- escape an identifier string for use within SQL
    621 -------------------------------------------------------------------
    622625
    623626.. method:: DB.escape_identifier(string)
     
    637640.. versionadded:: 4.1
    638641
    639 escape_string -- escape a string for use within SQL
    640 ---------------------------------------------------
    641 
    642642.. method:: DB.escape_string(string)
    643643
     
    648648    :rtype: str
    649649
    650 Similar to the module function with the same name, but the
    651 behavior of this method is adjusted depending on the connection properties
    652 (such as character encoding).
    653 
    654 escape_bytea -- escape binary data for use within SQL
    655 -----------------------------------------------------
     650Similar to the module function :func:`pg.escape_string` with the same name,
     651but the behavior of this method is adjusted depending on the connection
     652properties (such as character encoding).
    656653
    657654.. method:: DB.escape_bytea(datastring)
     
    663660    :rtype: str
    664661
    665 Similar to the module function with the same name, but the
    666 behavior of this method is adjusted depending on the connection properties
    667 (in particular, whether standard-conforming strings are enabled).
    668 
    669 unescape_bytea -- unescape data that has been retrieved as text
    670 ---------------------------------------------------------------
     662Similar to the module function :func:`pg.escape_bytea` with the same name,
     663but the behavior of this method is adjusted depending on the connection
     664properties (in particular, whether standard-conforming strings are enabled).
     665
     666unescape_bytea -- unescape data retrieved from the database
     667-----------------------------------------------------------
     668
     669The following method unescapes binary ``bytea`` data strings that
     670have been retrieved from the database.  You don't need to use this
     671method on the data returned by :meth:`DB.get` and similar, only if
     672you query the database directly with :meth:`DB.query`.
    671673
    672674.. method:: DB.unescape_bytea(string)
     
    678680    :rtype: bytes
    679681
    680 See the module function with the same name.
     682See the module function :func:`pg.unescape_bytea` with the same name.
     683
     684encode/decode_json -- encode and decode JSON data
     685-------------------------------------------------
     686
     687The following methods can be used to encode end decode data in
     688`JSON <http://www.json.org/>`_ format.
     689
     690.. method:: DB.encode_json(obj)
     691
     692    Encode a Python object for use within SQL as type ``json`` or ``jsonb``
     693
     694    :param obj: Python object that shall be encoded to JSON format
     695    :type obj: dict, list or None
     696    :returns: string representation of the Python object in JSON format
     697    :rtype: str
     698
     699This method serializes a Python object into a JSON formatted string that can
     700be used within SQL.  You don't need to use this method on the data stored
     701with :meth:`DB.insert` and similar, only if you store the data directly as
     702part of an SQL command or parameter with :meth:`DB.query`.  This is the same
     703as the :func:`json.dumps` function from the standard library.
     704
     705.. versionadded:: 5.0
     706
     707.. method:: DB.decode_json(string)
     708
     709    Decode ``json`` or ``jsonb`` data that has been retrieved as text
     710
     711    :param string: JSON formatted string shall be decoded into a Python object
     712    :type string: str
     713    :returns: Python object representing the JSON formatted string
     714    :rtype: dict, list or None
     715
     716This method deserializes a JSON formatted string retrieved as text from the
     717database to a Python object.  You normally don't need to use this method as
     718JSON data is automatically decoded by PyGreSQL.  If you don't want the data
     719to be decoded, then you can cast ``json`` or ``jsonb`` columns to ``text``
     720in PostgreSQL or you can set the decoding function to *None* or a different
     721function using :func:`pg.set_jsondecode`.  By default this is the same as
     722the :func:`json.dumps` function from the standard library.
     723
     724.. versionadded:: 5.0
    681725
    682726use_regtypes -- determine use of regular type names
  • trunk/docs/contents/pg/module.rst

    r710 r774  
    414414    Get the function that converts to named tuples
    415415
    416 This function returns the function used by PyGreSQL to construct the
    417 result of the :meth:`Query.namedresult` method.
     416This returns the function used by PyGreSQL to construct the result of the
     417:meth:`Query.namedresult` method.
     418
     419.. versionadded:: 4.1
    418420
    419421.. function:: set_namedresult(func)
     
    424426
    425427You can use this if you want to create different kinds of named tuples
    426 returned by the :meth:`Query.namedresult` method.
     428returned by the :meth:`Query.namedresult` method.  If you set this function
     429to *None*, then it will become equal to :meth:`Query.getresult`.
     430
     431.. versionadded:: 4.1
     432
     433get/set_jsondecode -- decoding JSON format
     434------------------------------------------
     435
     436.. function:: get_jsondecode()
     437
     438    Get the function that deserializes JSON formatted strings
     439
     440This returns the function used by PyGreSQL to construct Python objects
     441from JSON formatted strings.
     442
     443.. function:: set_jsondecode(func)
     444
     445    Set a function that will deserialize JSON formatted strings
     446
     447    :param func: the function to be used for deserializing JSON strings
     448
     449You can use this if you do not want to deserialize JSON strings coming
     450in from the database, or if want to use a different function than the
     451standard function :meth:`json.loads` or if you want to use it with parameters
     452different from the default ones.  If you set this function to *None*, then
     453the automatic deserialization of JSON strings will be deactivated.
     454
     455.. versionadded:: 5.0
    427456
    428457
  • trunk/docs/contents/pgdb/types.rst

    r710 r774  
    44.. py:currentmodule:: pgdb
    55
    6 .. class:: Type
     6Type constructors
     7-----------------
    78
    8 The :attr:`Cursor.description` attribute returns information about each
    9 of the result columns of a query. The *type_code* must compare equal to one
    10 of the :class:`Type` objects defined below. Type objects can be equal to
    11 more than one type code (e.g. :class:`DATETIME` is equal to the type codes
    12 for date, time and timestamp columns).
     9For binding to an operation's input parameters, PostgreSQL needs to have
     10the input in a particular format.  However, from the parameters to the
     11:meth:`Cursor.execute` and :meth:`Cursor.executemany` methods it is not
     12always obvious as which PostgreSQL data types they shall be bound.
     13For instance, a Python string could be bound as a simple ``char`` value,
     14or also as a ``date`` or a ``time``.  Or a list could be bound as a
     15``array`` or a ``json`` object.  To make the intention clear in such cases,
     16you can wrap the parameters in type helper objects.  PyGreSQL provides the
     17constructors defined below to create such objects that can hold special values.
     18When passed to the cursor methods, PyGreSQL can then detect the proper type
     19of the input parameter and bind it accordingly.
    1320
    14 The :mod:`pgdb` module exports the following constructors and singletons:
     21The :mod:`pgdb` module exports the following constructors that as part of
     22the DB-API 2 standard:
    1523
    1624.. function:: Date(year, month, day)
     
    4250    Construct an object capable of holding a (long) binary string value
    4351
    44 .. class:: STRING
     52Additionally, PyGreSQL provides the following constructors for PostgreSQL
     53specific data types:
     54
     55.. function:: Json(obj, [encode])
     56
     57    Construct a wrapper for holding an object serializable to JSON.
     58
     59    You can pass an optional serialization function as a parameter.
     60    By default, PyGreSQL uses :func:`json.dumps` to serialize it.
     61
     62Example for using a type constructor::
     63
     64    >>> cursor.execute("create table jsondata (data jsonb)")
     65    >>> data = {'id': 1, 'name': 'John Doe', 'kids': ['Johnnie', 'Janie']}
     66    >>> cursor.execute("insert into jsondata values (%s)", [Json(data)])
     67
     68.. note::
     69
     70    SQL NULL values are always represented by the Python *None* singleton
     71    on input and output.
     72
     73Type objects
     74------------
     75
     76.. class:: Type
     77
     78The :attr:`Cursor.description` attribute returns information about each
     79of the result columns of a query. The *type_code* must compare equal to one
     80of the :class:`Type` objects defined below.  Type objects can be equal to
     81more than one type code (e.g. :class:`DATETIME` is equal to the type codes
     82for ``date``, ``time`` and ``timestamp`` columns).
     83
     84The pgdb module exports the following :class:`Type` objects as part of the
     85DB-API 2 standard:
     86
     87.. object:: STRING
    4588
    4689    Used to describe columns that are string-based (e.g. ``char``, ``varchar``, ``text``)
    4790
    48 .. class:: BINARY type
     91.. object:: BINARY
    4992
    5093    Used to describe (long) binary columns (``bytea``)
    5194
    52 .. class:: NUMBER
     95.. object:: NUMBER
    5396
    5497    Used to describe numeric columns (e.g. ``int``, ``float``, ``numeric``, ``money``)
    5598
    56 .. class:: DATETIME
     99.. object:: DATETIME
    57100
    58101    Used to describe date/time columns (e.g. ``date``, ``time``, ``timestamp``, ``interval``)
    59102
    60 .. class:: ROWID
     103.. object:: ROWID
    61104
    62105    Used to describe the ``oid`` column of PostgreSQL database tables
     
    66109  The following more specific types are not part of the DB-API 2 standard.
    67110
    68 .. class:: BOOL
     111.. object:: BOOL
    69112
    70113    Used to describe ``boolean`` columns
    71114
    72 .. class:: SMALLINT
     115.. object:: SMALLINT
    73116
    74117    Used to describe ``smallint`` columns
    75118
    76 .. class:: INTEGER
     119.. object:: INTEGER
    77120
    78121    Used to describe ``integer`` columns
    79122
    80 .. class:: LONG
     123.. object:: LONG
    81124
    82125    Used to describe ``bigint`` columns
    83126
    84 .. class:: FLOAT
     127.. object:: FLOAT
    85128
    86129    Used to describe ``float`` columns
    87130
    88 .. class:: NUMERIC
     131.. object:: NUMERIC
    89132
    90133    Used to describe ``numeric`` columns
    91134
    92 .. class:: MONEY
     135.. object:: MONEY
    93136
    94137    Used to describe ``money`` columns
    95138
    96 .. class:: DATE
     139.. object:: DATE
    97140
    98141    Used to describe ``date`` columns
    99142
    100 .. class:: TIME
     143.. object:: TIME
    101144
    102145    Used to describe ``time`` columns
    103146
    104 .. class:: TIMESTAMP
     147.. object:: TIMESTAMP
    105148
    106149    Used to describe ``timestamp`` columns
    107150
    108 .. class:: INTERVAL
     151.. object:: INTERVAL
    109152
    110153    Used to describe date and time ``interval`` columns
     154
     155.. object:: JSON
     156
     157    Used to describe ``json`` and ``jsonb`` columns
     158
     159Example for using some type objects::
     160
     161    >>> cursor = con.cursor()
     162    >>> cursor.execute("create table jsondata (created date, data jsonb)")
     163    >>> cursor.execute("select * from jsondata")
     164    >>> (created, data) = (d.type_code for d in cursor.description)
     165    >>> created == DATE
     166    True
     167    >>> created == DATETIME
     168    True
     169    >>> created == TIME
     170    False
     171    >>> data == JSON
     172    True
     173    >>> data == STRING
     174    False
  • trunk/pg.py

    r770 r774  
    3939from functools import partial
    4040from operator import itemgetter
     41from json import loads as jsondecode, dumps as jsonencode
    4142
    4243try:
     
    4445except NameError:  # Python >= 3.0
    4546    basestring = (str, bytes)
    46 
    47 set_decimal(Decimal)
    4847
    4948try:
     
    131130        def _read_only_error(*args, **kw):
    132131            raise TypeError('This object is read-only')
    133 
    134132
    135133
     
    157155    if typ.startswith('bytea'):
    158156        return 'bytea'
     157    if typ.startswith('json'):
     158        return 'json'
    159159    return 'text'
    160160
     
    164164    row = namedtuple('Row', q.listfields())
    165165    return [row(*r) for r in q.getresult()]
    166 
    167 set_namedresult(_namedresult)
    168166
    169167
     
    201199    return _db_error(msg, ProgrammingError)
    202200
     201
     202# Initialize the C module
     203
     204set_namedresult(_namedresult)
     205set_decimal(Decimal)
     206set_jsondecode(jsondecode)
     207
     208
     209# The notification handler
    203210
    204211class NotificationHandler(object):
     
    468475        return self.escape_bytea(d)
    469476
     477    def _prepare_json(self, d):
     478        """Prepare a json parameter."""
     479        return self.encode_json(d)
     480
    470481    _prepare_funcs = dict(  # quote methods for each type
    471482        bool=_prepare_bool, date=_prepare_date,
    472483        int=_prepare_num, num=_prepare_num, float=_prepare_num,
    473         money=_prepare_num, bytea=_prepare_bytea)
     484        money=_prepare_num, bytea=_prepare_bytea, json=_prepare_json)
    474485
    475486    def _prepare_param(self, value, typ, params):
     
    509520    # so we define unescape_bytea as a method as well
    510521    unescape_bytea = staticmethod(unescape_bytea)
     522
     523    def decode_json(self, s):
     524        """Decode a JSON string coming from the database."""
     525        return (get_jsondecode() or jsondecode)(s)
     526
     527    def encode_json(self, d):
     528        """Encode a JSON string for use within SQL."""
     529        return jsonencode(d)
    511530
    512531    def close(self):
     
    14421461        if keytuple or rowtuple:
    14431462            namedresult = get_namedresult()
    1444             if keytuple:
    1445                 keys = namedresult(_MemoryQuery(keys, keyname))
    1446             if rowtuple:
    1447                 fields = [f for f in fields if f not in keyset]
    1448                 rows = namedresult(_MemoryQuery(rows, fields))
     1463            if namedresult:
     1464                if keytuple:
     1465                    keys = namedresult(_MemoryQuery(keys, keyname))
     1466                if rowtuple:
     1467                    fields = [f for f in fields if f not in keyset]
     1468                    rows = namedresult(_MemoryQuery(rows, fields))
    14491469        return cls(zip(keys, rows))
    14501470
  • trunk/pgdb.py

    r773 r774  
    7373from math import isnan, isinf
    7474from collections import namedtuple
     75from json import loads as jsondecode, dumps as jsonencode
    7576
    7677try:
     
    135136
    136137
    137 def _cast_bytea(value):
    138     return unescape_bytea(value)
    139 
    140 
    141 def _cast_float(value):
    142     return float(value)  # this also works with NaN and Infinity
    143 
    144 
    145 _cast = {'bool': _cast_bool, 'bytea': _cast_bytea,
     138_cast = {'bool': _cast_bool, 'bytea': unescape_bytea,
    146139    'int2': int, 'int4': int, 'serial': int,
    147     'int8': long, 'oid': long, 'oid8': long,
    148     'float4': _cast_float, 'float8': _cast_float,
     140    'int8': long, 'json': jsondecode, 'jsonb': jsondecode,
     141    'oid': long, 'oid8': long,
     142    'float4': float, 'float8': float,
    149143    'numeric': Decimal, 'money': _cast_money}
    150144
     
    247241    def _quote(self, val):
    248242        """Quote value depending on its type."""
    249         if isinstance(val, (datetime, date, time, timedelta)):
     243        if isinstance(val, (datetime, date, time, timedelta, Json)):
    250244            val = str(val)
    251245        if isinstance(val, basestring):
     
    266260        elif val is None:
    267261            val = 'NULL'
    268         elif isinstance(val, (list, tuple)):
     262        elif isinstance(val, list):
    269263            q = self._quote
    270264            val = 'ARRAY[%s]' % ','.join(str(q(v)) for v in val)
     265        elif isinstance(val, tuple):
     266            q = self._quote
     267            val = 'ROW(%s)' % ','.join(str(q(v)) for v in val)
    271268        elif Decimal is not float and isinstance(val, Decimal):
    272269            pass
     
    300297    def execute(self, operation, parameters=None):
    301298        """Prepare and execute a database operation (query or command)."""
    302 
    303         # The parameters may also be specified as list of
    304         # tuples to e.g. insert multiple rows in a single
    305         # operation, but this kind of usage is deprecated:
    306         if (parameters and isinstance(parameters, list) and
    307                 isinstance(parameters[0], tuple)):
     299        # The parameters may also be specified as list of tuples to e.g.
     300        # insert multiple rows in a single operation, but this kind of
     301        # usage is deprecated.  We make several plausibility checks because
     302        # tuples can also be passed with the meaning of ROW constructors.
     303        if (parameters and isinstance(parameters, list)
     304                and len(parameters) > 1
     305                and all(isinstance(p, tuple) for p in parameters)
     306                and all(len(p) == len(parameters[0]) for p in parameters[1:])):
    308307            return self.executemany(operation, parameters)
    309308        else:
     
    332331                self._dbcnx._tnx = True
    333332            for parameters in seq_of_parameters:
     333                sql = operation
    334334                if parameters:
    335                     sql = self._quoteparams(operation, parameters)
    336                 else:
    337                     sql = operation
     335                    sql = self._quoteparams(sql, parameters)
    338336                rows = self._src.execute(sql)
    339337                if rows:  # true if not DML
     
    938936TIMESTAMP = Type('timestamp timestamptz datetime abstime')
    939937INTERVAL = Type('interval tinterval timespan reltime')
     938JSON = Type('json jsonb')
    940939
    941940
     
    953952
    954953def Timestamp(year, month, day, hour=0, minute=0, second=0, microsecond=0):
    955     """construct an object holding a time stamp valu."""
     954    """Construct an object holding a time stamp value."""
    956955    return datetime(year, month, day, hour, minute, second, microsecond)
    957956
     
    963962
    964963def TimeFromTicks(ticks):
    965     """construct an object holding a time value from the given ticks value."""
     964    """Construct an object holding a time value from the given ticks value."""
    966965    return Time(*localtime(ticks)[3:6])
    967966
    968967
    969968def TimestampFromTicks(ticks):
    970     """construct an object holding a time stamp from the given ticks value."""
     969    """Construct an object holding a time stamp from the given ticks value."""
    971970    return Timestamp(*localtime(ticks)[:6])
    972971
    973972
    974973class Binary(bytes):
    975     """construct an object capable of holding a binary (long) string value."""
     974    """Construct an object capable of holding a binary (long) string value."""
     975
     976
     977# Additional type helpers for PyGreSQL:
     978
     979class Json:
     980    """Construct a wrapper for holding an object serializable to JSON."""
     981
     982    def __init__(self, obj, encode=None):
     983        self.obj = obj
     984        self.encode = encode or jsonencode
     985
     986    def __str__(self):
     987        obj = self.obj
     988        if isinstance(obj, basestring):
     989            return obj
     990        return self.encode(obj)
     991
     992    __pg_repr__ = __str__
    976993
    977994
  • trunk/pgmodule.c

    r740 r774  
    9393
    9494static PyObject *decimal = NULL, /* decimal type */
    95                                 *namedresult = NULL; /* function for getting named results */
     95                                *namedresult = NULL, /* function for getting named results */
     96                                *jsondecode = NULL; /* function for decoding json strings */
    9697static char decimal_point = '.'; /* decimal point used in money values */
    9798static int use_bool = 0; /* whether or not bool objects shall be returned */
     
    189190/* define internal types */
    190191
     192#define PYGRES_DEFAULT 0
    191193#define PYGRES_INT 1
    192194#define PYGRES_LONG 2
     
    195197#define PYGRES_MONEY 5
    196198#define PYGRES_BOOL 6
    197 #define PYGRES_DEFAULT 7
     199#define PYGRES_JSON 7
    198200
    199201/* --------------------------------------------------------------------- */
     
    235237get_type_array(PGresult *result, int nfields)
    236238{
    237         int *typ;
     239        int *array, *a;
    238240        int j;
    239241
    240         if (!(typ = PyMem_Malloc(sizeof(int) * nfields)))
     242        if (!(array = PyMem_Malloc(sizeof(int) * nfields)))
    241243        {
    242244                PyErr_SetString(PyExc_MemoryError, "Memory error in getresult()");
     
    244246        }
    245247
    246         for (j = 0; j < nfields; j++)
     248        for (j = 0, a=array; j < nfields; j++)
    247249        {
    248250                switch (PQftype(result, j))
     
    251253                        case INT4OID:
    252254                        case OIDOID:
    253                                 typ[j] = PYGRES_INT;
     255                                *a++ = PYGRES_INT;
    254256                                break;
    255257
    256258                        case INT8OID:
    257                                 typ[j] = PYGRES_LONG;
     259                                *a++ = PYGRES_LONG;
    258260                                break;
    259261
    260262                        case FLOAT4OID:
    261263                        case FLOAT8OID:
    262                                 typ[j] = PYGRES_FLOAT;
     264                                *a++ = PYGRES_FLOAT;
    263265                                break;
    264266
    265267                        case NUMERICOID:
    266                                 typ[j] = PYGRES_DECIMAL;
     268                                *a++ = PYGRES_DECIMAL;
    267269                                break;
    268270
    269271                        case CASHOID:
    270                                 typ[j] = PYGRES_MONEY;
     272                                *a++ = PYGRES_MONEY;
    271273                                break;
    272274
    273275                        case BOOLOID:
    274                                 typ[j] = PYGRES_BOOL;
     276                                *a++ = PYGRES_BOOL;
    275277                                break;
    276278
     279                        case JSONOID:
     280                        case JSONBOID:
     281                                *a++ = PYGRES_JSON;
     282                                break;
     283
    277284                        default:
    278                                 typ[j] = PYGRES_DEFAULT;
     285                                *a++ = PYGRES_DEFAULT;
    279286                }
    280287        }
    281288
    282         return typ;
     289        return array;
    283290}
    284291
     
    36323639                                switch (coltypes[j])
    36333640                                {
     3641                                        case PYGRES_JSON:
     3642                                                if (!jsondecode || /* no JSON decoder available */
     3643                                                        PQfformat(self->result, j) != 0) /* not text */
     3644                                                        goto default_case;
     3645                                                size = PQgetlength(self->result, i, j);
     3646#if IS_PY3
     3647                                                val = get_decoded_string(s, size, encoding);
     3648#else
     3649                                                val = get_decoded_string(s, size, self->encoding);
     3650#endif
     3651                                                if (val) /* was able to decode */
     3652                                                {
     3653                                                        tmp_obj = Py_BuildValue("(O)", val);
     3654                                                        val = PyObject_CallObject(jsondecode, tmp_obj);
     3655                                                        Py_DECREF(tmp_obj);
     3656                                                }
     3657                                                break;
     3658
    36343659                                        case PYGRES_INT:
    36353660                                                val = PyInt_FromString(s, NULL, 10);
     
    38033828                                switch (coltypes[j])
    38043829                                {
     3830                                        case PYGRES_JSON:
     3831                                                if (!jsondecode || /* no JSON decoder available */
     3832                                                        PQfformat(self->result, j) != 0) /* not text */
     3833                                                        goto default_case;
     3834                                                size = PQgetlength(self->result, i, j);
     3835#if IS_PY3
     3836                                                val = get_decoded_string(s, size, encoding);
     3837#else
     3838                                                val = get_decoded_string(s, size, self->encoding);
     3839#endif
     3840                                                if (val) /* was able to decode */
     3841                                                {
     3842                                                        tmp_obj = Py_BuildValue("(O)", val);
     3843                                                        val = PyObject_CallObject(jsondecode, tmp_obj);
     3844                                                        Py_DECREF(tmp_obj);
     3845                                                }
     3846                                                break;
     3847
    38053848                                        case PYGRES_INT:
    38063849                                                val = PyInt_FromString(s, NULL, 10);
     
    39183961                           *ret;
    39193962
    3920         /* checks args (args == NULL for an internal call) */
    3921         if (args && !PyArg_ParseTuple(args, ""))
    3922         {
    3923                 PyErr_SetString(PyExc_TypeError,
    3924                         "Method namedresult() takes no parameters");
    3925                 return NULL;
    3926         }
    3927 
    3928         if (!namedresult)
    3929         {
    3930                 PyErr_SetString(PyExc_TypeError,
    3931                         "Named tuples are not supported");
    3932                 return NULL;
    3933         }
    3934 
    3935         arglist = Py_BuildValue("(O)", self);
    3936         ret = PyObject_CallObject(namedresult, arglist);
    3937         Py_DECREF(arglist);
    3938 
    3939         if (ret == NULL)
    3940                 return NULL;
     3963        if (namedresult)
     3964        {
     3965                /* checks args (args == NULL for an internal call) */
     3966                if (args && !PyArg_ParseTuple(args, ""))
     3967                {
     3968                        PyErr_SetString(PyExc_TypeError,
     3969                                "Method namedresult() takes no parameters");
     3970                        return NULL;
     3971                }
     3972
     3973                arglist = Py_BuildValue("(O)", self);
     3974                ret = PyObject_CallObject(namedresult, arglist);
     3975                Py_DECREF(arglist);
     3976
     3977                if (ret == NULL)
     3978                        return NULL;
     3979                }
     3980        else
     3981        {
     3982                ret = queryGetResult(self, args);
     3983        }
    39413984
    39423985        return ret;
     
    44404483        if (PyArg_ParseTuple(args, "O", &func))
    44414484        {
    4442                 if (PyCallable_Check(func))
     4485                if (func == Py_None)
     4486                {
     4487                        Py_XDECREF(namedresult); namedresult = NULL;
     4488                        Py_INCREF(Py_None); ret = Py_None;
     4489                }
     4490                else if (PyCallable_Check(func))
    44434491                {
    44444492                        Py_XINCREF(func); Py_XDECREF(namedresult); namedresult = func;
     4493                        Py_INCREF(Py_None); ret = Py_None;
     4494                }
     4495                else
     4496                        PyErr_SetString(PyExc_TypeError, "Parameter must be callable");
     4497        }
     4498
     4499        return ret;
     4500}
     4501
     4502/* get json decode function */
     4503static char pgGetJsondecode__doc__[] =
     4504"get_jsondecode(cls) -- get the function used for decoding json results";
     4505
     4506static PyObject *
     4507pgGetJsondecode(PyObject *self, PyObject *args)
     4508{
     4509        PyObject *ret = NULL;
     4510
     4511        if (PyArg_ParseTuple(args, ""))
     4512        {
     4513                ret = jsondecode ? jsondecode : Py_None;
     4514                Py_INCREF(ret);
     4515        }
     4516
     4517        return ret;
     4518}
     4519
     4520/* set json decode function */
     4521static char pgSetJsondecode__doc__[] =
     4522"set_jsondecode(cls) -- set a function to be used for decoding json results";
     4523
     4524static PyObject *
     4525pgSetJsondecode(PyObject *self, PyObject *args)
     4526{
     4527        PyObject *ret = NULL;
     4528        PyObject *func;
     4529
     4530        if (PyArg_ParseTuple(args, "O", &func))
     4531        {
     4532                if (func == Py_None)
     4533                {
     4534                        Py_XDECREF(jsondecode); jsondecode = NULL;
     4535                        Py_INCREF(Py_None); ret = Py_None;
     4536                }
     4537                else if (PyCallable_Check(func))
     4538                {
     4539                        Py_XINCREF(func); Py_XDECREF(jsondecode); jsondecode = func;
    44454540                        Py_INCREF(Py_None); ret = Py_None;
    44464541                }
     
    47664861        {"set_namedresult", (PyCFunction) pgSetNamedresult, METH_VARARGS,
    47674862                        pgSetNamedresult__doc__},
     4863        {"get_jsondecode", (PyCFunction) pgGetJsondecode, METH_VARARGS,
     4864                        pgGetJsondecode__doc__},
     4865        {"set_jsondecode", (PyCFunction) pgSetJsondecode, METH_VARARGS,
     4866                        pgSetJsondecode__doc__},
    47684867
    47694868#ifdef DEFAULT_VARS
  • trunk/pgtypes.h

    r715 r774  
    22        pgtypes - PostgreSQL type definitions
    33
    4         These are the standard PostgreSQL built-in types,
    5         extracted from server/catalog/pg_type.h Revision 1.212,
     4        These are the standard PostgreSQL 9.5 built-in types,
     5        extracted from src/include/catalog/pg_type.h,
    66        because that header file is sometimes not available
    77        or needs other header files to get properly included.
     
    2626#define CIDOID 29
    2727#define OIDVECTOROID 30
     28#define JSONOID 114
    2829#define XMLOID 142
     30#define PGNODETREEOID 194
     31#define PGDDLCOMMANDOID 32
    2932#define POINTOID 600
    3033#define LSEGOID 601
     
    4447#define INETOID 869
    4548#define CIDROID 650
     49#define INT2ARRAYOID 1005
    4650#define INT4ARRAYOID 1007
    4751#define TEXTARRAYOID 1009
     52#define OIDARRAYOID 1028
    4853#define FLOAT4ARRAYOID 1021
    4954#define ACLITEMOID 1033
     
    6671#define REGCLASSOID 2205
    6772#define REGTYPEOID 2206
     73#define REGROLEOID 4096
     74#define REGNAMESPACEOID 4089
    6875#define REGTYPEARRAYOID 2211
     76#define UUIDOID 2950
     77#define LSNOID 3220
    6978#define TSVECTOROID 3614
    7079#define GTSVECTOROID 3642
     
    7281#define REGCONFIGOID 3734
    7382#define REGDICTIONARYOID 3769
     83#define JSONBOID 3802
     84#define INT4RANGEOID 3904
    7485#define RECORDOID 2249
    7586#define RECORDARRAYOID 2287
     
    7990#define VOIDOID 2278
    8091#define TRIGGEROID 2279
     92#define EVTTRIGGEROID 3838
    8193#define LANGUAGE_HANDLEROID 2280
    8294#define INTERNALOID 2281
     
    8597#define ANYNONARRAYOID 2776
    8698#define ANYENUMOID 3500
     99#define FDW_HANDLEROID 3115
     100#define TSM_HANDLEROID 3310
     101#define ANYRANGEOID 3831
    87102
    88103#endif /* PG_TYPE_H */
  • trunk/tests/test_classic_dbwrapper.py

    r770 r774  
    1919import sys
    2020import tempfile
     21import json
    2122
    2223import pg  # the module under test
     
    179180            'begin',
    180181            'cancel', 'clear', 'close', 'commit',
    181             'db', 'dbname', 'debug', 'delete',
    182             'end', 'endcopy', 'error',
     182            'db', 'dbname', 'debug', 'decode_json', 'delete',
     183            'encode_json', 'end', 'endcopy', 'error',
    183184            'escape_bytea', 'escape_identifier',
    184185            'escape_literal', 'escape_string',
     
    284285        self.assertEqual(self.db.unescape_bytea(''), b'')
    285286
     287    def testMethodDecodeJson(self):
     288        self.assertEqual(self.db.decode_json('{}'), {})
     289
     290    def testMethodEncodeJson(self):
     291        self.assertEqual(self.db.encode_json({}), '{}')
     292
    286293    def testMethodQuery(self):
    287294        query = self.db.query
     
    532539            b'\\x746861742773206be47365')
    533540        self.assertEqual(f(r'\\x4f007073ff21'), b'\\x4f007073ff21')
     541
     542    def testDecodeJson(self):
     543        f = self.db.decode_json
     544        self.assertIsNone(f('null'))
     545        data = {
     546          "id": 1, "name": "Foo", "price": 1234.5,
     547          "new": True, "note": None,
     548          "tags": ["Bar", "Eek"],
     549          "stock": {"warehouse": 300, "retail": 20}}
     550        text = json.dumps(data)
     551        r = f(text)
     552        self.assertIsInstance(r, dict)
     553        self.assertEqual(r, data)
     554        self.assertIsInstance(r['id'], int)
     555        self.assertIsInstance(r['name'], unicode)
     556        self.assertIsInstance(r['price'], float)
     557        self.assertIsInstance(r['new'], bool)
     558        self.assertIsInstance(r['tags'], list)
     559        self.assertIsInstance(r['stock'], dict)
     560
     561    def testEncodeJson(self):
     562        f = self.db.encode_json
     563        self.assertEqual(f(None), 'null')
     564        data = {
     565          "id": 1, "name": "Foo", "price": 1234.5,
     566          "new": True, "note": None,
     567          "tags": ["Bar", "Eek"],
     568          "stock": {"warehouse": 300, "retail": 20}}
     569        text = json.dumps(data)
     570        r = f(data)
     571        self.assertIsInstance(r, str)
     572        self.assertEqual(r, text)
    534573
    535574    def testGetParameter(self):
     
    27722811
    27732812    def testUpsertBytea(self):
    2774         query = self.db.query
    27752813        self.createTable('bytea_test', 'n smallint primary key, data bytea')
    27762814        s = b"It's all \\ kinds \x00 of\r nasty \xff stuff!\n"
     
    27952833        self.assertIn('data', r)
    27962834        self.assertIsNone(r['data'], bytes)
     2835
     2836    def testInsertGetJson(self):
     2837        try:
     2838            self.createTable('json_test', 'n smallint primary key, data json')
     2839        except pg.ProgrammingError as error:
     2840            if self.db.server_version < 90200:
     2841                self.skipTest('database does not support json')
     2842            self.fail(str(error))
     2843        jsondecode = pg.get_jsondecode()
     2844        # insert null value
     2845        r = self.db.insert('json_test', n=0, data=None)
     2846        self.assertIsInstance(r, dict)
     2847        self.assertIn('n', r)
     2848        self.assertEqual(r['n'], 0)
     2849        self.assertIn('data', r)
     2850        self.assertIsNone(r['data'])
     2851        r = self.db.get('json_test', 0)
     2852        self.assertIsInstance(r, dict)
     2853        self.assertIn('n', r)
     2854        self.assertEqual(r['n'], 0)
     2855        self.assertIn('data', r)
     2856        self.assertIsNone(r['data'])
     2857        # insert JSON object
     2858        data = {
     2859          "id": 1, "name": "Foo", "price": 1234.5,
     2860          "new": True, "note": None,
     2861          "tags": ["Bar", "Eek"],
     2862          "stock": {"warehouse": 300, "retail": 20}}
     2863        r = self.db.insert('json_test', n=1, data=data)
     2864        self.assertIsInstance(r, dict)
     2865        self.assertIn('n', r)
     2866        self.assertEqual(r['n'], 1)
     2867        self.assertIn('data', r)
     2868        r = r['data']
     2869        if jsondecode is None:
     2870            self.assertIsInstance(r, str)
     2871            r = json.loads(r)
     2872        self.assertIsInstance(r, dict)
     2873        self.assertEqual(r, data)
     2874        self.assertIsInstance(r['id'], int)
     2875        self.assertIsInstance(r['name'], unicode)
     2876        self.assertIsInstance(r['price'], float)
     2877        self.assertIsInstance(r['new'], bool)
     2878        self.assertIsInstance(r['tags'], list)
     2879        self.assertIsInstance(r['stock'], dict)
     2880        r = self.db.get('json_test', 1)
     2881        self.assertIsInstance(r, dict)
     2882        self.assertIn('n', r)
     2883        self.assertEqual(r['n'], 1)
     2884        self.assertIn('data', r)
     2885        r = r['data']
     2886        if jsondecode is None:
     2887            self.assertIsInstance(r, str)
     2888            r = json.loads(r)
     2889        self.assertIsInstance(r, dict)
     2890        self.assertEqual(r, data)
     2891        self.assertIsInstance(r['id'], int)
     2892        self.assertIsInstance(r['name'], unicode)
     2893        self.assertIsInstance(r['price'], float)
     2894        self.assertIsInstance(r['new'], bool)
     2895        self.assertIsInstance(r['tags'], list)
     2896        self.assertIsInstance(r['stock'], dict)
     2897
     2898    def testInsertGetJsonb(self):
     2899        try:
     2900            self.createTable('jsonb_test',
     2901                'n smallint primary key, data jsonb')
     2902        except pg.ProgrammingError as error:
     2903            if self.db.server_version < 90400:
     2904                self.skipTest('database does not support jsonb')
     2905            self.fail(str(error))
     2906        jsondecode = pg.get_jsondecode()
     2907        # insert null value
     2908        r = self.db.insert('jsonb_test', n=0, data=None)
     2909        self.assertIsInstance(r, dict)
     2910        self.assertIn('n', r)
     2911        self.assertEqual(r['n'], 0)
     2912        self.assertIn('data', r)
     2913        self.assertIsNone(r['data'])
     2914        r = self.db.get('jsonb_test', 0)
     2915        self.assertIsInstance(r, dict)
     2916        self.assertIn('n', r)
     2917        self.assertEqual(r['n'], 0)
     2918        self.assertIn('data', r)
     2919        self.assertIsNone(r['data'])
     2920        # insert JSON object
     2921        data = {
     2922          "id": 1, "name": "Foo", "price": 1234.5,
     2923          "new": True, "note": None,
     2924          "tags": ["Bar", "Eek"],
     2925          "stock": {"warehouse": 300, "retail": 20}}
     2926        r = self.db.insert('jsonb_test', n=1, data=data)
     2927        self.assertIsInstance(r, dict)
     2928        self.assertIn('n', r)
     2929        self.assertEqual(r['n'], 1)
     2930        self.assertIn('data', r)
     2931        r = r['data']
     2932        if jsondecode is None:
     2933            self.assertIsInstance(r, str)
     2934            r = json.loads(r)
     2935        self.assertIsInstance(r, dict)
     2936        self.assertEqual(r, data)
     2937        self.assertIsInstance(r['id'], int)
     2938        self.assertIsInstance(r['name'], unicode)
     2939        self.assertIsInstance(r['price'], float)
     2940        self.assertIsInstance(r['new'], bool)
     2941        self.assertIsInstance(r['tags'], list)
     2942        self.assertIsInstance(r['stock'], dict)
     2943        r = self.db.get('jsonb_test', 1)
     2944        self.assertIsInstance(r, dict)
     2945        self.assertIn('n', r)
     2946        self.assertEqual(r['n'], 1)
     2947        self.assertIn('data', r)
     2948        r = r['data']
     2949        if jsondecode is None:
     2950            self.assertIsInstance(r, str)
     2951            r = json.loads(r)
     2952        self.assertIsInstance(r, dict)
     2953        self.assertEqual(r, data)
     2954        self.assertIsInstance(r['id'], int)
     2955        self.assertIsInstance(r['name'], unicode)
     2956        self.assertIsInstance(r['price'], float)
     2957        self.assertIsInstance(r['new'], bool)
     2958        self.assertIsInstance(r['tags'], list)
     2959        self.assertIsInstance(r['stock'], dict)
    27972960
    27982961    def testNotificationHandler(self):
     
    28833046        not_bool = not pg.get_bool()
    28843047        cls.set_option('bool', not_bool)
    2885         unnamed_result = lambda q: q.getresult()
    2886         cls.set_option('namedresult', unnamed_result)
     3048        cls.set_option('namedresult', None)
     3049        cls.set_option('jsondecode', None)
    28873050        super(TestDBClassNonStdOpts, cls).setUpClass()
    28883051
     
    28903053    def tearDownClass(cls):
    28913054        super(TestDBClassNonStdOpts, cls).tearDownClass()
     3055        cls.reset_option('jsondecode')
    28923056        cls.reset_option('namedresult')
    28933057        cls.reset_option('bool')
  • trunk/tests/test_classic_functions.py

    r770 r774  
    1616    import unittest
    1717
     18import json
    1819import re
    1920
     
    182183    def testSetDecimalPoint(self):
    183184        point = pg.get_decimal_point()
    184         pg.set_decimal_point('*')
    185         r = pg.get_decimal_point()
    186         pg.set_decimal_point(point)
    187         self.assertIsInstance(r, str)
    188         self.assertEqual(r, '*')
     185        try:
     186            pg.set_decimal_point('*')
     187            r = pg.get_decimal_point()
     188            self.assertIsInstance(r, str)
     189            self.assertEqual(r, '*')
     190        finally:
     191            pg.set_decimal_point(point)
    189192        r = pg.get_decimal_point()
    190193        self.assertIsInstance(r, str)
     
    197200    def testSetDecimal(self):
    198201        decimal_class = pg.Decimal
    199         pg.set_decimal(int)
    200         r = pg.get_decimal()
    201         pg.set_decimal(decimal_class)
    202         self.assertIs(r, int)
     202        try:
     203            pg.set_decimal(int)
     204            r = pg.get_decimal()
     205            self.assertIs(r, int)
     206        finally:
     207            pg.set_decimal(decimal_class)
    203208        r = pg.get_decimal()
    204209        self.assertIs(r, decimal_class)
     
    211216    def testSetBool(self):
    212217        use_bool = pg.get_bool()
    213         pg.set_bool(True)
    214         r = pg.get_bool()
    215         pg.set_bool(use_bool)
    216         self.assertIsInstance(r, bool)
    217         self.assertIs(r, True)
    218         pg.set_bool(False)
    219         r = pg.get_bool()
    220         pg.set_bool(use_bool)
    221         self.assertIsInstance(r, bool)
    222         self.assertIs(r, False)
     218        try:
     219            pg.set_bool(True)
     220            r = pg.get_bool()
     221            pg.set_bool(use_bool)
     222            self.assertIsInstance(r, bool)
     223            self.assertIs(r, True)
     224            pg.set_bool(False)
     225            r = pg.get_bool()
     226            self.assertIsInstance(r, bool)
     227            self.assertIs(r, False)
     228        finally:
     229            pg.set_bool(use_bool)
    223230        r = pg.get_bool()
    224231        self.assertIsInstance(r, bool)
     
    232239    def testSetNamedresult(self):
    233240        namedresult = pg.get_namedresult()
    234         f = lambda q: q.getresult()
    235         pg.set_namedresult(f)
    236         r = pg.get_namedresult()
    237         pg.set_namedresult(namedresult)
    238         self.assertIs(r, f)
     241        try:
     242            pg.set_namedresult(None)
     243            r = pg.get_namedresult()
     244            self.assertIsNone(r)
     245            f = lambda q: q.getresult()
     246            pg.set_namedresult(f)
     247            r = pg.get_namedresult()
     248            self.assertIs(r, f)
     249            self.assertRaises(TypeError, pg.set_namedresult, 'invalid')
     250        finally:
     251            pg.set_namedresult(namedresult)
    239252        r = pg.get_namedresult()
    240253        self.assertIs(r, namedresult)
     254
     255    def testGetJsondecode(self):
     256        r = pg.get_jsondecode()
     257        self.assertTrue(callable(r))
     258        self.assertIs(r, json.loads)
     259
     260    def testSetJsondecode(self):
     261        jsondecode = pg.get_jsondecode()
     262        try:
     263            pg.set_jsondecode(None)
     264            r = pg.get_jsondecode()
     265            self.assertIsNone(r)
     266            pg.set_jsondecode(str)
     267            r = pg.get_jsondecode()
     268            self.assertIs(r, str)
     269            self.assertRaises(TypeError, pg.set_jsondecode, 'invalid')
     270        finally:
     271            pg.set_jsondecode(jsondecode)
     272        r = pg.get_jsondecode()
     273        self.assertIs(r, jsondecode)
    241274
    242275
  • trunk/tests/test_dbapi20.py

    r772 r774  
    365365            cur.execute("select * from %s order by 1" % table)
    366366            rows = cur.fetchall()
     367            self.assertEqual(cur.description[1].type_code, pgdb.FLOAT)
    367368        finally:
    368369            con.close()
     
    400401            cur.execute("select * from %s order by 1" % table)
    401402            rows = cur.fetchall()
     403            self.assertEqual(cur.description[1].type_code, pgdb.DATETIME)
    402404        finally:
    403405            con.close()
     
    409411            self.assertEqual(inval, outval)
    410412
    411     def test_array(self):
     413    def test_list_binds_as_array(self):
    412414        values = ([20000, 25000, 25000, 30000],
    413415            [['breakfast', 'consulting'], ['meeting', 'lunch']])
     
    425427            con.close()
    426428        self.assertEqual(row, output)
     429
     430    def test_tuple_binds_as_row(self):
     431        values = (1, 2.5, 'this is a test')
     432        output = '(1,2.5,"this is a test")'
     433        con = self._connect()
     434        try:
     435            cur = con.cursor()
     436            cur.execute("select %s", [values])
     437            outval = cur.fetchone()[0]
     438        finally:
     439            con.close()
     440        self.assertEqual(outval, output)
    427441
    428442    def test_custom_type(self):
     
    459473            self.assertTrue(pgdb.decimal_type(int) is int)
    460474            cur.execute('select 42')
     475            self.assertEqual(cur.description[0].type_code, pgdb.INTEGER)
    461476            value = cur.fetchone()[0]
    462477            self.assertTrue(isinstance(value, int))
     
    464479            self.assertTrue(pgdb.decimal_type(float) is float)
    465480            cur.execute('select 4.25')
     481            self.assertEqual(cur.description[0].type_code, pgdb.NUMBER)
    466482            value = cur.fetchone()[0]
    467483            self.assertTrue(isinstance(value, float))
     
    550566            cur.execute("select * from %s order by 1" % table)
    551567            rows = cur.fetchall()
     568            self.assertEqual(cur.description[1].type_code, pgdb.BOOL)
    552569        finally:
    553570            con.close()
     
    557574        self.assertEqual(rows, values)
    558575
     576    def test_json(self):
     577        inval = {"employees":
     578            [{"firstName": "John", "lastName": "Doe", "age": 61}]}
     579        table = self.table_prefix + 'booze'
     580        con = self._connect()
     581        try:
     582            cur = con.cursor()
     583            try:
     584                cur.execute("create table %s (jsontest json)" % table)
     585            except pgdb.ProgrammingError:
     586                self.skipTest('database does not support json')
     587            params = (pgdb.Json(inval),)
     588            cur.execute("insert into %s values (%%s)" % table, params)
     589            cur.execute("select * from %s" % table)
     590            outval = cur.fetchone()[0]
     591            self.assertEqual(cur.description[0].type_code, pgdb.JSON)
     592        finally:
     593            con.close()
     594        self.assertEqual(inval, outval)
     595
     596    def test_jsonb(self):
     597        inval = {"employees":
     598            [{"firstName": "John", "lastName": "Doe", "age": 61}]}
     599        table = self.table_prefix + 'booze'
     600        con = self._connect()
     601        try:
     602            cur = con.cursor()
     603            try:
     604                cur.execute("create table %s (jsonbtest jsonb)" % table)
     605            except pgdb.ProgrammingError:
     606                self.skipTest('database does not support jsonb')
     607            params = (pgdb.Json(inval),)
     608            cur.execute("insert into %s values (%%s)" % table, params)
     609            cur.execute("select * from %s" % table)
     610            outval = cur.fetchone()[0]
     611            self.assertEqual(cur.description[0].type_code, pgdb.JSON)
     612        finally:
     613            con.close()
     614        self.assertEqual(inval, outval)
     615
    559616    def test_execute_edge_cases(self):
    560617        con = self._connect()
     
    564621            cur.executemany(sql, [])
    565622            sql = 'select %d + 1'
    566             cur.execute(sql, [(1,)])  # deprecated use of execute()
    567             self.assertEqual(cur.fetchone()[0], 2)
     623            cur.execute(sql, [(1,), (2,)])  # deprecated use of execute()
     624            self.assertEqual(cur.fetchone()[0], 3)
    568625            sql = 'select 1/0'  # cannot be executed
    569626            self.assertRaises(pgdb.ProgrammingError, cur.execute, sql)
Note: See TracChangeset for help on using the changeset viewer.