Author: cito
Date: Mon Jan 25 15:44:52 2016
New Revision: 781
Log:
Add full support for PostgreSQL array types
At the core of this patch is a fast parser for the peculiar syntax of
literal array expressions in PostgreSQL that was added to the C module.
This is not trivial, because PostgreSQL arrays can be multidimensional
and the syntax is different from Python and SQL expressions.
The Python pg and pgdb modules make use of this parser so that they can
return database columns containing PostgreSQL arrays to Python as lists.
Also added quoting methods that allow passing PostgreSQL arrays as lists
to insert()/update() and execute/executemany(). These methods are simpler
and were implemented in Python but needed support from the regex module.
The patch also adds makes getresult() in pg automatically return bytea
values in unescaped form as bytes strings. Before, it was necessary to
call unescape_bytea manually. The pgdb module did this already.
The patch includes some more refactorings and simplifications regarding
the quoting and casting in pg and pgdb.
Some references to antique PostgreSQL types that are not used any more
in the supported PostgreSQL versions have been removed.
Also added documentation and tests for the new features.
Modified:
trunk/docs/contents/changelog.rst
trunk/docs/contents/pg/db_wrapper.rst
trunk/docs/contents/pg/module.rst
trunk/docs/contents/pg/query.rst
trunk/pg.py
trunk/pgdb.py
trunk/pgmodule.c
trunk/pgtypes.h
trunk/tests/test_classic_connection.py
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 Sat Jan 23 08:34:22 2016 (r780)
+++ trunk/docs/contents/changelog.rst Mon Jan 25 15:44:52 2016 (r781)
@@ -39,12 +39,18 @@
- A method upsert() has been added to the DB wrapper class that exploits the
"upsert" feature that is new in PostgreSQL 9.5. The new method nicely
complements the existing get/insert/update/delete() methods.
+- You can now insert() PostgreSQL arrays as lists in the classic module.
+- A fast parser for PostgreSQL array output syntax has been added to the
+ C module. Data in an array type column is now returned as a Python list,
+ which can be nested if the array has more than one dimension.
- 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 pkey() method of the classic interface now returns tuples instead
of frozenset. The order of the tuples is like in the primary key index.
+- The classic module now also returns bytea columns fetched from the database
+ as byte strings, you don't need to call unescape_bytea() any more.
- The table name that is affixed to the name of the OID column returned
by the get() method of the classic interface will not automatically
be fully qualified any more. This reduces overhead from the interface,
Modified: trunk/docs/contents/pg/db_wrapper.rst
==============================================================================
--- trunk/docs/contents/pg/db_wrapper.rst Sat Jan 23 08:34:22 2016
(r780)
+++ trunk/docs/contents/pg/db_wrapper.rst Mon Jan 25 15:44:52 2016
(r781)
@@ -310,6 +310,9 @@
in order to allow the caller to work with multiple tables, it is
munged as ``oid(table)`` using the actual name of the table.
+Note that since PyGreSQL 5.0 this will return the value of an array
+type column as a Python list.
+
insert -- insert a row into a database table
--------------------------------------------
@@ -332,6 +335,9 @@
The dictionary is then reloaded with the values actually inserted in order
to pick up values modified by rules, triggers, etc.
+Note that since PyGreSQL 5.0 it is possible to insert a value for an
+array type column by passing it as Python list.
+
update -- update a row in a database table
------------------------------------------
@@ -666,11 +672,6 @@
unescape_bytea -- unescape data retrieved from the database
-----------------------------------------------------------
-The following method unescapes binary ``bytea`` data strings that
-have been retrieved from the database. You don't need to use this
-method on the data returned by :meth:`DB.get` and similar, only if
-you query the database directly with :meth:`DB.query`.
-
.. method:: DB.unescape_bytea(string)
Unescape ``bytea`` data that has been retrieved as text
@@ -679,7 +680,10 @@
:returns: byte string containing the binary data
:rtype: bytes
-See the module function :func:`pg.unescape_bytea` with the same name.
+Converts an escaped string representation of binary data stored as ``bytea``
+into the raw byte string representing the binary data -- this is the reverse
+of :meth:`DB.escape_bytea`. Since the :class:`Query` results will already
+return unescaped byte strings, you normally don't have to use this method.
encode/decode_json -- encode and decode JSON data
-------------------------------------------------
Modified: trunk/docs/contents/pg/module.rst
==============================================================================
--- trunk/docs/contents/pg/module.rst Sat Jan 23 08:34:22 2016 (r780)
+++ trunk/docs/contents/pg/module.rst Mon Jan 25 15:44:52 2016 (r781)
@@ -278,6 +278,7 @@
Escapes binary data for use within an SQL command with the type ``bytea``.
As with :func:`escape_string`, this is only used when inserting data directly
into an SQL command string.
+
Note that there is also a :class:`Connection` method with the same name
which takes connection properties into account.
@@ -299,16 +300,13 @@
:rtype: bytes
:raises TypeError: bad argument type, or too many arguments
-Converts an escaped string representation of binary data into binary
-data -- the reverse of :func:`escape_bytea`. This is needed when retrieving
-``bytea`` data with one of the :meth:`Query.getresult`,
-:meth:`Query.dictresult` or :meth:`Query.namedresult` methods.
-
-Example::
+Converts an escaped string representation of binary data stored as ``bytea``
+into the raw byte string representing the binary data -- this is the reverse
+of :func:`escape_bytea`. Since the :class:`Query` results will already
+return unescaped byte strings, you normally don't have to use this method.
- picture = unescape_bytea(con.query(
- "select img from pictures where name='Garfield'").getresult[0][0])
- open('garfield.gif', 'wb').write(picture)
+Note that there is also a :class:`DB` method with the same name
+which does exactly the same.
get/set_decimal -- decimal type to be used for numeric values
-------------------------------------------------------------
Modified: trunk/docs/contents/pg/query.rst
==============================================================================
--- trunk/docs/contents/pg/query.rst Sat Jan 23 08:34:22 2016 (r780)
+++ trunk/docs/contents/pg/query.rst Mon Jan 25 15:44:52 2016 (r781)
@@ -26,6 +26,9 @@
:meth:`Query.listfields`, :meth:`Query.fieldname`
and :meth:`Query.fieldnum` methods.
+Note that since PyGreSQL 5.0 this will return the values of array type
+columns as Python lists.
+
dictresult -- get query values as list of dictionaries
------------------------------------------------------
@@ -42,6 +45,9 @@
with each tuple returned as a dictionary with the field names
used as the dictionary index.
+Note that since PyGreSQL 5.0 this will return the values of array type
+columns as Python lists.
+
namedresult -- get query values as list of named tuples
-------------------------------------------------------
@@ -58,6 +64,9 @@
This method returns the list of the values returned by the query
with each row returned as a named tuple with proper field names.
+Note that since PyGreSQL 5.0 this will return the values of array type
+columns as Python lists.
+
.. versionadded:: 4.1
listfields -- list fields names of previous query result
Modified: trunk/pg.py
==============================================================================
--- trunk/pg.py Sat Jan 23 08:34:22 2016 (r780)
+++ trunk/pg.py Mon Jan 25 15:44:52 2016 (r781)
@@ -38,6 +38,7 @@
from collections import namedtuple
from functools import partial
from operator import itemgetter
+from re import compile as regex
from json import loads as jsondecode, dumps as jsonencode
try:
@@ -131,32 +132,42 @@
raise TypeError('This object is read-only')
-# Auxiliary functions that are independent from a DB connection:
+# Auxiliary classes and functions that are independent from a DB connection:
def _oid_key(table):
"""Build oid key from a table name."""
return 'oid(%s)' % table
-def _simpletype(typ):
- """Determine a simplified name a pg_type name."""
- if typ.startswith('bool'):
- return 'bool'
- if typ.startswith(('abstime', 'date', 'interval', 'timestamp')):
- return 'date'
- if typ.startswith(('cid', 'oid', 'int', 'xid')):
- return 'int'
- if typ.startswith('float'):
- return 'float'
- if typ.startswith('numeric'):
- return 'num'
- if typ.startswith('money'):
- return 'money'
- if typ.startswith('bytea'):
- return 'bytea'
- if typ.startswith('json'):
- return 'json'
- return 'text'
+class _SimpleType(dict):
+ """Dictionary mapping pg_type names to simple type names."""
+
+ _types = {'bool': 'bool',
+ 'bytea': 'bytea',
+ 'date': 'date interval time timetz timestamp timestamptz'
+ ' abstime reltime', # these are very old
+ 'float': 'float4 float8',
+ 'int': 'cid int2 int4 int8 oid xid',
+ 'json': 'json jsonb',
+ 'num': 'numeric',
+ 'money': 'money',
+ 'text': 'bpchar char name text varchar'}
+
+ def __init__(self):
+ for typ, keys in self._types.items():
+ for key in keys.split():
+ self[key] = typ
+ self['_%s' % key] = '%s[]' % typ
+
+ @staticmethod
+ def __missing__(key):
+ return 'text'
+
+_simpletype = _SimpleType()
+
+
+class _Literal(str):
+ """Wrapper class for literal SQL."""
def _namedresult(q):
@@ -413,7 +424,7 @@
def _do_debug(self, *args):
"""Print a debug message"""
if self.debug:
- s = '\n'.join(args)
+ s = '\n'.join(str(arg) for arg in args)
if isinstance(self.debug, basestring):
print(self.debug % s)
elif hasattr(self.debug, 'write'):
@@ -458,38 +469,117 @@
if not d:
return None
if isinstance(d, basestring) and d.lower() in self._date_literals:
- raise ValueError
+ return _Literal(d)
return d
_num_types = frozenset('int float num money'
' int2 int4 int8 float4 float8 numeric money'.split())
- def _prepare_num(self, d):
+ @staticmethod
+ def _prepare_num(d):
"""Prepare a numeric parameter."""
if not d and d != 0:
return None
return d
+ _prepare_int = _prepare_float = _prepare_money = _prepare_num
+
def _prepare_bytea(self, d):
"""Prepare a bytea parameter."""
return self.escape_bytea(d)
def _prepare_json(self, d):
"""Prepare a json parameter."""
+ if not d:
+ return None
+ if isinstance(d, basestring):
+ return d
return self.encode_json(d)
- _prepare_funcs = dict( # quote methods for each type
- bool=_prepare_bool, date=_prepare_date,
- int=_prepare_num, num=_prepare_num, float=_prepare_num,
- money=_prepare_num, bytea=_prepare_bytea, json=_prepare_json)
+ _re_array_escape = regex(r'(["\\])')
+ _re_array_quote = regex(r'[{},"\\\s]|^[Nn][Uu][Ll][Ll]$')
+
+ def _prepare_bool_array(self, d):
+ """Prepare a bool array parameter."""
+ if isinstance(d, list):
+ return '{%s}' % ','.join(self._prepare_bool_array(v) for v in d)
+ if d is None:
+ return 'null'
+ if isinstance(d, basestring):
+ if not d:
+ return 'null'
+ d = d.lower() in self._bool_true_values
+ return 't' if d else 'f'
+
+ def _prepare_num_array(self, d):
+ """Prepare a numeric array parameter."""
+ if isinstance(d, list):
+ return '{%s}' % ','.join(self._prepare_num_array(v) for v in d)
+ if not d and d != 0:
+ return 'null'
+ return str(d)
+
+ _prepare_int_array = _prepare_float_array = _prepare_money_array = \
+ _prepare_num_array
+
+ def _prepare_text_array(self, d):
+ """Prepare a text array parameter."""
+ if isinstance(d, list):
+ return '{%s}' % ','.join(self._prepare_text_array(v) for v in d)
+ if d is None:
+ return 'null'
+ if not d:
+ return '""'
+ d = str(d)
+ if self._re_array_quote.search(d):
+ d = '"%s"' % self._re_array_escape.sub(r'\\\1', d)
+ return d
+
+ def _prepare_bytea_array(self, d):
+ """Prepare a bytea array parameter."""
+ if isinstance(d, list):
+ return '{%s}' % ','.join(self._prepare_bytea_array(v) for v in d)
+ if d is None:
+ return 'null'
+ return self.escape_bytea(d).replace('\\', '\\\\')
+
+ def _prepare_json_array(self, d):
+ """Prepare a json array parameter."""
+ if isinstance(d, list):
+ return '{%s}' % ','.join(self._prepare_json_array(v) for v in d)
+ if not d:
+ return 'null'
+ if not isinstance(d, basestring):
+ d = self.encode_json(d)
+ if self._re_array_quote.search(d):
+ d = '"%s"' % self._re_array_escape.sub(r'\\\1', d)
+ return d
def _prepare_param(self, value, typ, params):
"""Prepare and add a parameter to the list."""
+ if isinstance(value, _Literal):
+ return value
if value is not None and typ != 'text':
- prepare = self._prepare_funcs[typ]
- try:
- value = prepare(self, value)
- except ValueError:
+ if typ.endswith('[]'):
+ if isinstance(value, list):
+ prepare = getattr(self, '_prepare_%s_array' % typ[:-2])
+ value = prepare(value)
+ elif isinstance(value, basestring):
+ value = value.strip()
+ if not value.startswith('{') or not value.endswith('}'):
+ if value[:5].lower() == 'array':
+ value = value[5:].lstrip()
+ if value.startswith('[') and value.endswith(']'):
+ value = _Literal('ARRAY%s' % value)
+ else:
+ raise ValueError(
+ 'Invalid array expression: %s' % value)
+ else:
+ raise ValueError('Invalid array parameter: %s' % value)
+ else:
+ prepare = getattr(self, '_prepare_%s' % typ)
+ value = prepare(value)
+ if isinstance(value, _Literal):
return value
params.append(value)
return '$%d' % len(params)
@@ -725,7 +815,7 @@
self._do_debug(q)
self.db.query(q)
- def query(self, qstr, *args):
+ def query(self, command, *args):
"""Execute a SQL command string.
This method simply sends a SQL query to the database. If the query is
@@ -747,8 +837,11 @@
# Wraps shared library function for debugging.
if not self.db:
raise _int_error('Connection is not valid')
- self._do_debug(qstr)
- return self.db.query(qstr, args)
+ if args:
+ self._do_debug(command, args)
+ return self.db.query(command, args)
+ self._do_debug(command)
+ return self.db.query(command)
def pkey(self, table, composite=False, flush=False):
"""Get or set the primary key of a table.
@@ -849,7 +942,7 @@
self._prepare_qualified_param(table, 1))
names = self.db.query(q, (table,)).getresult()
if not self._regtypes:
- names = ((name, _simpletype(typ)) for name, typ in names)
+ names = ((name, _simpletype[typ]) for name, typ in names)
names = AttrDict(names)
attnames[table] = names # cache it
return names
@@ -950,8 +1043,6 @@
for n, value in res[0].items():
if qoid and n == 'oid':
n = qoid
- elif value is not None and attnames.get(n) == 'bytea':
- value = self.unescape_bytea(value)
row[n] = value
return row
@@ -985,6 +1076,8 @@
if n in row:
names.append(col(n))
values.append(param(row[n], attnames[n]))
+ if not names:
+ raise _prg_error('No column found that can be inserted')
names, values = ', '.join(names), ', '.join(values)
ret = 'oid, *' if qoid else '*'
q = 'INSERT INTO %s (%s) VALUES (%s) RETURNING %s' % (
@@ -996,8 +1089,6 @@
for n, value in res[0].items():
if qoid and n == 'oid':
n = qoid
- elif value is not None and attnames.get(n) == 'bytea':
- value = self.unescape_bytea(value)
row[n] = value
return row
@@ -1065,8 +1156,6 @@
for n, value in res[0].items():
if qoid and n == 'oid':
n = qoid
- elif value is not None and attnames.get(n) == 'bytea':
- value = self.unescape_bytea(value)
row[n] = value
return row
@@ -1168,8 +1257,6 @@
for n, value in res[0].items():
if qoid and n == 'oid':
n = qoid
- elif value is not None and attnames.get(n) == 'bytea':
- value = self.unescape_bytea(value)
row[n] = value
else:
self.get(table, row)
Modified: trunk/pgdb.py
==============================================================================
--- trunk/pgdb.py Sat Jan 23 08:34:22 2016 (r780)
+++ trunk/pgdb.py Mon Jan 25 15:44:52 2016 (r781)
@@ -72,6 +72,7 @@
from decimal import Decimal
from math import isnan, isinf
from collections import namedtuple
+from re import compile as regex
from json import loads as jsondecode, dumps as jsonencode
try:
@@ -165,12 +166,16 @@
@staticmethod
def typecast(typ, value):
- """Cast value to database type."""
+ """Cast value according to database type."""
if value is None:
# for NULL values, no typecast is necessary
return None
cast = _cast.get(typ)
if cast is None:
+ if typ.startswith('_'):
+ # cast as an array type
+ cast = _cast.get(typ[1:])
+ return cast_array(value, cast)
# no typecast available or necessary
return value
else:
@@ -192,6 +197,9 @@
return res
+_re_array_escape = regex(r'(["\\])')
+_re_array_quote = regex(r'[{},"\\\s]|^[Nn][Uu][Ll][Ll]$')
+
class _quotedict(dict):
"""Dictionary with auto quoting of its items.
@@ -240,6 +248,8 @@
def _quote(self, val):
"""Quote value depending on its type."""
+ if val is None:
+ return 'NULL'
if isinstance(val, (datetime, date, time, timedelta, Json)):
val = str(val)
if isinstance(val, basestring):
@@ -249,30 +259,49 @@
val = val.decode('ascii')
else:
val = self._cnx.escape_string(val)
- val = "'%s'" % val
- elif isinstance(val, (int, long)):
- pass
- elif isinstance(val, float):
+ return "'%s'" % val
+ if isinstance(val, float):
if isinf(val):
return "'-Infinity'" if val < 0 else "'Infinity'"
- elif isnan(val):
+ if isnan(val):
return "'NaN'"
- elif val is None:
- val = 'NULL'
- elif isinstance(val, list):
+ return val
+ if isinstance(val, (int, long, Decimal)):
+ return val
+ if isinstance(val, list):
+ return "'%s'" % self._quote_array(val)
+ if isinstance(val, tuple):
q = self._quote
- val = 'ARRAY[%s]' % ','.join(str(q(v)) for v in val)
- elif isinstance(val, tuple):
- q = self._quote
- val = 'ROW(%s)' % ','.join(str(q(v)) for v in val)
- elif Decimal is not float and isinstance(val, Decimal):
- pass
- elif hasattr(val, '__pg_repr__'):
- val = val.__pg_repr__()
- else:
+ return 'ROW(%s)' % ','.join(str(q(v)) for v in val)
+ try:
+ return val.__pg_repr__()
+ except AttributeError:
+ raise InterfaceError(
+ 'do not know how to handle type %s' % type(val))
+
+ def _quote_array(self, val):
+ """Quote value as a literal constant for an array."""
+ # We could also cast to an array constructor here, but that is more
+ # verbose and you need to know the base type to build emtpy arrays.
+ if isinstance(val, list):
+ return '{%s}' % ','.join(self._quote_array(v) for v in val)
+ if val is None:
+ return 'null'
+ if isinstance(val, (int, long, float)):
+ return str(val)
+ if isinstance(val, bool):
+ return 't' if val else 'f'
+ if isinstance(val, basestring):
+ if not val:
+ return '""'
+ if _re_array_quote.search(val):
+ return '"%s"' % _re_array_escape.sub(r'\\\1', val)
+ return val
+ try:
+ return val.__pg_repr__()
+ except AttributeError:
raise InterfaceError(
'do not know how to handle type %s' % type(val))
- return val
def _quoteparams(self, string, parameters):
"""Quote parameters.
@@ -901,25 +930,45 @@
def __eq__(self, other):
if isinstance(other, basestring):
+ if other.startswith('_'):
+ other = other[1:]
return other in self
else:
return super(Type, self).__eq__(other)
def __ne__(self, other):
if isinstance(other, basestring):
+ if other.startswith('_'):
+ other = other[1:]
return other not in self
else:
return super(Type, self).__ne__(other)
+class ArrayType:
+ """Type class for PostgreSQL array types."""
+
+ def __eq__(self, other):
+ if isinstance(other, basestring):
+ return other.startswith('_')
+ else:
+ return isinstance(other, ArrayType)
+
+ def __ne__(self, other):
+ if isinstance(other, basestring):
+ return not other.startswith('_')
+ else:
+ return not isinstance(other, ArrayType)
+
+
# Mandatory type objects defined by DB-API 2 specs:
STRING = Type('char bpchar name text varchar')
BINARY = Type('bytea')
NUMBER = Type('int2 int4 serial int8 float4 float8 numeric money')
-DATETIME = Type('date time timetz timestamp timestamptz datetime abstime'
- ' interval tinterval timespan reltime')
-ROWID = Type('oid oid8')
+DATETIME = Type('date time timetz timestamp timestamptz interval'
+ ' abstime reltime') # these are very old
+ROWID = Type('oid')
# Additional type objects (more specific):
@@ -933,10 +982,14 @@
MONEY = Type('money')
DATE = Type('date')
TIME = Type('time timetz')
-TIMESTAMP = Type('timestamp timestamptz datetime abstime')
-INTERVAL = Type('interval tinterval timespan reltime')
+TIMESTAMP = Type('timestamp timestamptz')
+INTERVAL = Type('interval')
JSON = Type('json jsonb')
+# Type object for arrays (also equate to their base types):
+
+ARRAY = ArrayType()
+
# Mandatory type helpers defined by DB-API 2 specs:
Modified: trunk/pgmodule.c
==============================================================================
--- trunk/pgmodule.c Sat Jan 23 08:34:22 2016 (r780)
+++ trunk/pgmodule.c Mon Jan 25 15:44:52 2016 (r781)
@@ -53,7 +53,7 @@
#endif
/* default values */
-#define PG_ARRAYSIZE 1
+#define PG_ARRAYSIZE 1
/* flags for object validity checks */
#define CHECK_OPEN 1
@@ -79,6 +79,7 @@
{"", "movefirst", "movelast", "movenext", "moveprev"};
#define MAX_BUFFER_SIZE 8192 /* maximum transaction size */
+#define MAX_ARRAY_DEPTH 16 /* maximum allowed depth of an array */
/* MODULE GLOBAL VARIABLES */
@@ -187,16 +188,22 @@
#define is_largeObject(v) (PyType(v) == &largeType)
#endif /* LARGE_OBJECTS */
-/* define internal types */
+/* PyGreSQL internal types */
-#define PYGRES_DEFAULT 0
+/* simple types */
#define PYGRES_INT 1
#define PYGRES_LONG 2
#define PYGRES_FLOAT 3
#define PYGRES_DECIMAL 4
#define PYGRES_MONEY 5
#define PYGRES_BOOL 6
-#define PYGRES_JSON 7
+/* text based types */
+#define PYGRES_TEXT 8
+#define PYGRES_BYTEA 9
+#define PYGRES_JSON 10
+#define PYGRES_EXTERNAL 11
+/* array types */
+#define PYGRES_ARRAY 16
/* --------------------------------------------------------------------- */
/* Internal Functions
*/
@@ -204,7 +211,7 @@
/* shared function for encoding and decoding strings */
-PyObject *
+static PyObject *
get_decoded_string(char *str, Py_ssize_t size, int encoding)
{
if (encoding == pg_encoding_utf8)
@@ -218,7 +225,7 @@
pg_encoding_to_char(encoding), "strict");
}
-PyObject *
+static PyObject *
get_encoded_string(PyObject *unicode_obj, int encoding)
{
if (encoding == pg_encoding_utf8)
@@ -232,90 +239,294 @@
pg_encoding_to_char(encoding), "strict");
}
-/* shared functions for converting PG types to Python types */
-static int *
-get_type_array(PGresult *result, int nfields)
+/* helper functions */
+
+/* get PyGreSQL internal types for a PostgreSQL type */
+static int
+get_type(Oid pgtype)
{
- int *array, *a;
- int j;
+ int t;
- if (!(array = PyMem_Malloc(sizeof(int) * nfields)))
+ switch (pgtype)
{
- PyErr_SetString(PyExc_MemoryError, "Memory error in
getresult()");
- return NULL;
- }
+ /* simple types */
- for (j = 0, a=array; j < nfields; j++)
- {
- switch (PQftype(result, j))
- {
- case INT2OID:
- case INT4OID:
- case OIDOID:
- *a++ = PYGRES_INT;
- break;
+ case INT2OID:
+ case INT4OID:
+ case CIDOID:
+ case OIDOID:
+ case XIDOID:
+ t = PYGRES_INT;
+ break;
- case INT8OID:
- *a++ = PYGRES_LONG;
- break;
+ case INT8OID:
+ t = PYGRES_LONG;
+ break;
- case FLOAT4OID:
- case FLOAT8OID:
- *a++ = PYGRES_FLOAT;
- break;
+ case FLOAT4OID:
+ case FLOAT8OID:
+ t = PYGRES_FLOAT;
+ break;
- case NUMERICOID:
- *a++ = PYGRES_DECIMAL;
- break;
+ case NUMERICOID:
+ t = PYGRES_DECIMAL;
+ break;
- case CASHOID:
- *a++ = decimal_point ? PYGRES_MONEY :
PYGRES_DEFAULT;
- break;
+ case CASHOID:
+ t = decimal_point ? PYGRES_MONEY : PYGRES_TEXT;
+ break;
- case BOOLOID:
- *a++ = PYGRES_BOOL;
- break;
+ case BOOLOID:
+ t = PYGRES_BOOL;
+ break;
- case JSONOID:
- case JSONBOID:
- *a++ = jsondecode ? PYGRES_JSON :
PYGRES_DEFAULT;
- break;
+ case BYTEAOID:
+ t = PYGRES_BYTEA;
+ break;
- default:
- *a++ = PYGRES_DEFAULT;
- }
+ case JSONOID:
+ case JSONBOID:
+ t = jsondecode ? PYGRES_JSON : PYGRES_TEXT;
+ break;
+
+ /* array types */
+
+ case INT2ARRAYOID:
+ case INT4ARRAYOID:
+ case CIDARRAYOID:
+ case OIDARRAYOID:
+ case XIDARRAYOID:
+ t = PYGRES_INT | PYGRES_ARRAY;
+ break;
+
+ case INT8ARRAYOID:
+ t = PYGRES_LONG | PYGRES_ARRAY;
+ break;
+
+ case FLOAT4ARRAYOID:
+ case FLOAT8ARRAYOID:
+ t = PYGRES_FLOAT | PYGRES_ARRAY;
+ break;
+
+ case NUMERICARRAYOID:
+ t = PYGRES_DECIMAL | PYGRES_ARRAY;
+ break;
+
+ case CASHARRAYOID:
+ t = (decimal_point ?
+ PYGRES_MONEY : PYGRES_TEXT) | PYGRES_ARRAY;
+ break;
+
+ case BOOLARRAYOID:
+ t = PYGRES_BOOL | PYGRES_ARRAY;
+ break;
+
+ case BYTEAARRAYOID:
+ t = PYGRES_BYTEA | PYGRES_ARRAY;
+ break;
+
+ case JSONARRAYOID:
+ case JSONBARRAYOID:
+ t = (jsondecode ? PYGRES_JSON : PYGRES_TEXT) |
PYGRES_ARRAY;
+ break;
+
+ case ANYARRAYOID:
+ case BPCHARARRAYOID:
+ case CHARARRAYOID:
+ case TEXTARRAYOID:
+ case VARCHARARRAYOID:
+ case DATEARRAYOID:
+ case INTERVALARRAYOID:
+ case TIMEARRAYOID:
+ case TIMETZARRAYOID:
+ case TIMESTAMPARRAYOID:
+ case TIMESTAMPTZARRAYOID:
+ t = PYGRES_TEXT | PYGRES_ARRAY;
+ break;
+
+ default:
+ t = PYGRES_TEXT;
}
- return array;
+ return t;
}
-/* cast string s with type, size and encoding to a Python object */
-PyObject *
-cast_value(char *s, int type, Py_ssize_t size, int encoding)
+/* get PyGreSQL column types for all result columns */
+static int *
+get_col_types(PGresult *result, int nfields)
+{
+ int *types, *t, j;
+
+ if (!(types = PyMem_Malloc(sizeof(int) * nfields)))
+ return (int *)PyErr_NoMemory();
+
+ for (j = 0, t=types; j < nfields; ++j)
+ *t++ = get_type(PQftype(result, j));
+
+ return types;
+}
+
+/* Cast a bytea encoded text based type to a Python object.
+ This assumes the text is null-terminated character string. */
+static PyObject *
+cast_bytea_text(char *s)
+{
+ PyObject *obj;
+ char *tmp_str;
+ size_t str_len;
+
+ tmp_str = (char *)PQunescapeBytea((unsigned char*)s, &str_len);
+ obj = PyBytes_FromStringAndSize(tmp_str, str_len);
+ if (tmp_str)
+ PQfreemem(tmp_str);
+ return obj;
+}
+
+/* Cast a text based type to a Python object.
+ This needs the character string, size and encoding. */
+static PyObject *
+cast_sized_text(char *s, Py_ssize_t size, int encoding, int type)
{
PyObject *obj, *tmp_obj;
- char cashbuf[64];
- int k;
+ char *tmp_str;
+ size_t str_len;
- switch (type)
+ switch (type) /* this must be the PyGreSQL internal type */
{
+ case PYGRES_BYTEA:
+ /* we need to add a null byte */
+ tmp_str = (char *) PyMem_Malloc(size + 1);
+ if (!tmp_str) return PyErr_NoMemory();
+ memcpy(tmp_str, s, size);
+ s = tmp_str; *(s + size) = '\0';
+ tmp_str = (char *)PQunescapeBytea((unsigned char*)s,
&str_len);
+ PyMem_Free(s);
+ if (!tmp_str) return PyErr_NoMemory();
+ obj = PyBytes_FromStringAndSize(tmp_str, str_len);
+ if (tmp_str)
+ PQfreemem(tmp_str);
+ break;
+
case PYGRES_JSON:
/* this type should only be passed when jsondecode is
set */
- if (!jsondecode)
+ obj = get_decoded_string(s, size, encoding);
+ if (obj && jsondecode) /* was able to decode */
{
- PyErr_SetString(PyExc_ValueError, "JSON decoder
is not set");
- return NULL;
+ tmp_obj = Py_BuildValue("(O)", obj);
+ obj = PyObject_CallObject(jsondecode, tmp_obj);
+ Py_DECREF(tmp_obj);
}
+ break;
+ default: /* PYGRES_TEXT */
+#if IS_PY3
obj = get_decoded_string(s, size, encoding);
- if (obj) /* was able to decode */
+ if (!obj) /* cannot decode */
+#endif
+ obj = PyBytes_FromStringAndSize(s, size);
+ }
+
+ return obj;
+}
+
+/* Cast a simple type to a Python object.
+ This needs a character string representation with a given size. */
+static PyObject *
+cast_sized_simple(char *s, Py_ssize_t size, int type)
+{
+ PyObject *obj, *tmp_obj;
+ char buf[64], *t;
+ int i, j, n;
+
+ switch (type) /* this must be the PyGreSQL internal type */
+ {
+ case PYGRES_INT:
+ n = sizeof(buf)/sizeof(buf[0]) - 1;
+ if (size < n) n = size;
+ for (i = 0, t = buf; i < n; ++i) *t++ = *s++;
+ *t = '\0';
+ obj = PyInt_FromString(buf, NULL, 10);
+ break;
+
+ case PYGRES_LONG:
+ n = sizeof(buf)/sizeof(buf[0]) - 1;
+ if (size < n) n = size;
+ for (i = 0, t = buf; i < n; ++i) *t++ = *s++;
+ *t = '\0';
+ obj = PyLong_FromString(buf, NULL, 10);
+ break;
+
+ case PYGRES_FLOAT:
+ tmp_obj = PyStr_FromStringAndSize(s, size);
+ obj = PyFloat_FromString(tmp_obj);
+ Py_DECREF(tmp_obj);
+ break;
+
+ case PYGRES_MONEY:
+ /* this type should only be passed when decimal_point
is set */
+ n = sizeof(buf)/sizeof(buf[0]) - 1;
+ for (i = 0, j = 0; i < size && j < n; ++i, ++s)
{
- tmp_obj = Py_BuildValue("(O)", obj);
- obj = PyObject_CallObject(jsondecode, tmp_obj);
+ if (*s >= '0' && *s <= '9')
+ buf[j++] = *s;
+ else if (*s == decimal_point)
+ buf[j++] = '.';
+ else if (*s == '(' || *s == '-')
+ buf[j++] = '-';
+ }
+ if (decimal)
+ {
+ buf[j] = '\0';
+ obj = PyObject_CallFunction(decimal, "(s)",
buf);
+ }
+ else
+ {
+ tmp_obj = PyStr_FromString(buf);
+ obj = PyFloat_FromString(tmp_obj);
Py_DECREF(tmp_obj);
+
}
break;
+ case PYGRES_DECIMAL:
+ tmp_obj = PyStr_FromStringAndSize(s, size);
+ obj = decimal ? PyObject_CallFunctionObjArgs(
+ decimal, tmp_obj, NULL) :
PyFloat_FromString(tmp_obj);
+ Py_DECREF(tmp_obj);
+ break;
+
+ case PYGRES_BOOL:
+ /* convert to bool only if use_bool is set */
+ if (use_bool)
+ {
+ obj = *s == 't' ? Py_True : Py_False;
+ Py_INCREF(obj);
+ }
+ else
+ {
+ obj = PyStr_FromString(*s == 't' ? "t" : "f");
+ }
+ break;
+
+ default:
+ /* other types should never be passed, use
cast_sized_text */
+ obj = PyStr_FromStringAndSize(s, size);
+ }
+
+ return obj;
+}
+
+/* Cast a simple type to a Python object.
+ This needs a null-terminated character string representation. */
+static PyObject *
+cast_unsized_simple(char *s, int type)
+{
+ PyObject *obj, *tmp_obj;
+ char buf[64];
+ int j, n;
+
+ switch (type) /* this must be the PyGreSQL internal type */
+ {
case PYGRES_INT:
obj = PyInt_FromString(s, NULL, 10);
break;
@@ -331,40 +542,31 @@
break;
case PYGRES_MONEY:
- /* type should only be passed when decimal_point is set
*/
- if (!decimal_point)
- {
- PyErr_SetString(PyExc_ValueError, "Decimal
point is not set");
- return NULL;
- }
-
- for (k = 0;
- *s && k < sizeof(cashbuf)/sizeof(cashbuf[0]) -
1;
- s++)
+ /* this type should only be passed when decimal_point
is set */
+ n = sizeof(buf)/sizeof(buf[0]) - 1;
+ for (j = 0; *s && j < n; ++s)
{
if (*s >= '0' && *s <= '9')
- cashbuf[k++] = *s;
+ buf[j++] = *s;
else if (*s == decimal_point)
- cashbuf[k++] = '.';
+ buf[j++] = '.';
else if (*s == '(' || *s == '-')
- cashbuf[k++] = '-';
+ buf[j++] = '-';
}
- cashbuf[k] = '\0';
- s = cashbuf;
+ buf[j] = '\0'; s = buf;
/* FALLTHROUGH */ /* no break here */
case PYGRES_DECIMAL:
if (decimal)
{
- tmp_obj = Py_BuildValue("(s)", s);
- obj = PyEval_CallObject(decimal, tmp_obj);
+ obj = PyObject_CallFunction(decimal, "(s)", s);
}
else
{
tmp_obj = PyStr_FromString(s);
obj = PyFloat_FromString(tmp_obj);
+ Py_DECREF(tmp_obj);
}
- Py_DECREF(tmp_obj);
break;
case PYGRES_BOOL:
@@ -381,19 +583,260 @@
break;
default:
-#if IS_PY3
- obj = get_decoded_string(s, size, encoding);
- if (!obj) /* cannot decode */
-#endif
- obj = PyBytes_FromStringAndSize(s, size);
+ /* other types should never be passed, use
cast_sized_text */
+ obj = PyStr_FromString(s);
}
return obj;
}
+/* quick case insensitive check if given sized string is null */
+#define STR_IS_NULL(s, n) (n == 4 \
+ && (s[0] == 'n' || s[0] == 'N') \
+ && (s[1] == 'u' || s[1] == 'U') \
+ && (s[2] == 'l' || s[2] == 'L') \
+ && (s[3] == 'l' || s[3] == 'L'))
+
+/* Cast string s with size and encoding to a Python list.
+ Use cast function if specified or basetype to cast elements.
+ The parameter delim specifies the delimiter for the elements,
+ since some types do not use the default delimiter of a comma. */
+static PyObject *
+cast_array(char *s, Py_ssize_t size, int encoding,
+ int type, PyObject *cast, char delim)
+{
+ PyObject *result, *stack[MAX_ARRAY_DEPTH];
+ char *end = s + size, *t;
+ int depth, ranges = 0, level = 0;
+
+ if (type)
+ {
+ type &= ~PYGRES_ARRAY; /* get the base type */
+ if (!type) type = PYGRES_TEXT;
+ }
+ if (!delim) delim = ',';
+
+ /* strip blanks at the beginning */
+ while (s != end && *s == ' ') ++s;
+ if (*s == '[') /* dimension ranges */
+ {
+ int valid;
+
+ for (valid = 0; !valid;)
+ {
+ if (s == end || *s++ != '[') break;
+ while (s != end && *s == ' ') ++s;
+ if (s != end && (*s == '+' || *s == '-')) ++s;
+ if (s == end || *s <= '0' || *s >= '9') break;
+ while (s != end && *s >= '0' && *s <= '9') ++s;
+ if (s == end || *s++ != ':') break;
+ if (s != end && (*s == '+' || *s == '-')) ++s;
+ if (s == end || *s <= '0' || *s >= '9') break;
+ while (s != end && *s >= '0' && *s <= '9') ++s;
+ if (s == end || *s++ != ']') break;
+ while (s != end && *s == ' ') ++s;
+ ++ranges;
+ if (s != end && *s == '=')
+ {
+ do ++s; while (s != end && *s == ' ');
+ valid = 1;
+ }
+ }
+ if (!valid)
+ {
+ PyErr_SetString(PyExc_ValueError, "Invalid array
dimensions");
+ return NULL;
+ }
+ }
+ for (t = s, depth = 0; t != end && (*t == '{' || *t == ' '); ++t)
+ if (*t == '{') ++depth;
+ if (!depth)
+ {
+ PyErr_SetString(PyExc_ValueError,
+ "Array must start with an opening brace");
+ return NULL;
+ }
+ if (ranges && depth != ranges)
+ {
+ PyErr_SetString(PyExc_ValueError,
+ "Array dimensions do not match content");
+ return NULL;
+ }
+ if (depth > MAX_ARRAY_DEPTH)
+ {
+ PyErr_SetString(PyExc_ValueError, "Array is too deeply nested");
+ return NULL;
+ }
+ depth--; /* next level of parsing */
+ result = PyList_New(0);
+ if (!result) return NULL;
+ do ++s; while (s != end && *s == ' ');
+ /* everything is set up, start parsing the array */
+ while (s != end)
+ {
+ if (*s == '}')
+ {
+ PyObject *subresult;
+
+ if (!level) break; /* top level array ended */
+ do ++s; while (s != end && *s == ' ');
+ if (s == end) break; /* error */
+ if (*s == delim)
+ {
+ do ++s; while (s != end && *s == ' ');
+ if (s == end) break; /* error */
+ if (*s != '{')
+ {
+ PyErr_SetString(PyExc_ValueError,
+ "Subarray expected but not
found");
+ return NULL;
+ }
+ }
+ else if (*s != '}') break; /* error */
+ subresult = result;
+ result = stack[--level];
+ if (PyList_Append(result, subresult)) return NULL;
+ }
+ else if (level == depth) /* we expect elements at this level */
+ {
+ PyObject *element;
+ char *estr;
+ Py_ssize_t esize;
+ int escaped = 0;
+
+ if (*s == '{')
+ {
+ PyErr_SetString(PyExc_ValueError,
+ "Subarray found where not expected");
+ return NULL;
+ }
+ if (*s == '"') /* quoted element */
+ {
+ estr = ++s;
+ while (s != end && *s != '"')
+ {
+ if (*s == '\\')
+ {
+ ++s; if (s == end) break;
+ escaped = 1;
+ }
+ ++s;
+ }
+ esize = s - estr;
+ do ++s; while (s != end && *s == ' ');
+ }
+ else /* unquoted element */
+ {
+ estr = s;
+ /* can contain blanks inside */
+ while (s != end && *s != '"' &&
+ *s != '{' && *s != '}' && *s != delim)
+ {
+ if (*s == '\\')
+ {
+ ++s; if (s == end) break;
+ escaped = 1;
+ }
+ ++s;
+ }
+ t = s; while (t > estr && *(t - 1) == ' ') --t;
+ if (!(esize = t - estr))
+ {
+ s = end; break; /* error */
+ }
+ if (STR_IS_NULL(estr, esize)) /* NULL gives
None */
+ estr = NULL;
+ }
+ if (s == end) break; /* error */
+ if (estr)
+ {
+ if (escaped)
+ {
+ char *r;
+ int i;
+
+ /* create unescaped string */
+ t = estr;
+ estr = (char *) PyMem_Malloc(esize);
+ if (!estr) return PyErr_NoMemory();
+ for (i = 0, r = estr; i < esize; ++i)
+ {
+ if (*t == '\\') ++t, ++i;
+ *r++ = *t++;
+ }
+ esize = r - estr;
+ }
+ if (type) /* internal casting of base type */
+ {
+ if (type & PYGRES_TEXT)
+ element = cast_sized_text(estr,
esize, encoding, type);
+ else
+ element =
cast_sized_simple(estr, esize, type);
+ }
+ else /* external casting of base type */
+ {
+#if IS_PY3
+ element = encoding == pg_encoding_ascii
? NULL :
+ get_decoded_string(estr, esize,
encoding);
+ if (!element) /* no decoding necessary
or possible */
+#else
+ element =
PyBytes_FromStringAndSize(estr, esize);
+#endif
+ if (element && cast)
+ {
+ element =
PyObject_CallFunctionObjArgs(
+ cast, element, NULL);
+ }
+ }
+ if (escaped) PyMem_Free(estr);
+ if (!element) return NULL;
+ }
+ else
+ {
+ Py_INCREF(Py_None);
+ element = Py_None;
+ }
+ if (PyList_Append(result, element)) return NULL;
+ if (*s == delim)
+ {
+ do ++s; while (s != end && *s == ' ');
+ if (s == end) break; /* error */
+ }
+ else if (*s != '}') break; /* error */
+ }
+ else /* we expect arrays at this level */
+ {
+ if (*s != '{')
+ {
+ PyErr_SetString(PyExc_ValueError,
+ "Subarray must start with an opening
brace");
+ return NULL;
+ }
+ do ++s; while (s != end && *s == ' ');
+ if (s == end) break; /* error */
+ stack[level++] = result;
+ if (!(result = PyList_New(0))) return NULL;
+ }
+ }
+ if (s == end || *s != '}')
+ {
+ PyErr_SetString(PyExc_ValueError,
+ "Unexpected end of array");
+ return NULL;
+ }
+ do ++s; while (s != end && *s == ' ');
+ if (s != end)
+ {
+ PyErr_SetString(PyExc_ValueError,
+ "Unexpected characters after end of array");
+ return NULL;
+ }
+ return result;
+}
/* internal wrapper for the notice receiver callback */
-static void notice_receiver(void *arg, const PGresult *res)
+static void
+notice_receiver(void *arg, const PGresult *res)
{
PyGILState_STATE gstate = PyGILState_Ensure();
connObject *self = (connObject*) arg;
@@ -401,7 +844,7 @@
if (proc && PyCallable_Check(proc))
{
noticeObject *notice = PyObject_NEW(noticeObject, ¬iceType);
- PyObject *args, *ret;
+ PyObject *ret;
if (notice)
{
notice->pgcnx = arg;
@@ -412,10 +855,8 @@
Py_INCREF(Py_None);
notice = (noticeObject *)(void *)Py_None;
}
- args = Py_BuildValue("(O)", notice);
- ret = PyObject_CallObject(proc, args);
+ ret = PyObject_CallFunction(proc, "(O)", notice);
Py_XDECREF(ret);
- Py_DECREF(args);
}
PyGILState_Release(gstate);
}
@@ -481,159 +922,147 @@
{
const int n = PQnfields(res);
- if (n > 0)
+ if (n <= 0)
+ return PyStr_FromString("(nothing selected)");
+
+ char * const aligns = (char *) PyMem_Malloc(n * sizeof(char));
+ int * const sizes = (int *) PyMem_Malloc(n * sizeof(int));
+
+ if (!aligns || !sizes)
{
- char * const aligns = (char *) PyMem_Malloc(n * sizeof(char));
- int * const sizes = (int *) PyMem_Malloc(n * sizeof(int));
+ PyMem_Free(aligns); PyMem_Free(sizes); return PyErr_NoMemory();
+ }
- if (aligns && sizes)
- {
- const int m = PQntuples(res);
- int i, j;
- size_t size;
- char *buffer;
+ const int m = PQntuples(res);
+ int i, j;
+ size_t size;
+ char *buffer;
- /* calculate sizes and alignments */
- for (j = 0; j < n; j++)
- {
- const char * const s = PQfname(res, j);
- const int format = PQfformat(res, j);
+ /* calculate sizes and alignments */
+ for (j = 0; j < n; ++j)
+ {
+ const char * const s = PQfname(res, j);
+ const int format = PQfformat(res, j);
- sizes[j] = s ? (int)strlen(s) : 0;
- if (format)
- {
- aligns[j] = '\0';
- if (m && sizes[j] < 8)
- /* "<binary>" must fit */
- sizes[j] = 8;
- }
- else
- {
- const Oid ftype = PQftype(res, j);
+ sizes[j] = s ? (int)strlen(s) : 0;
+ if (format)
+ {
+ aligns[j] = '\0';
+ if (m && sizes[j] < 8)
+ /* "<binary>" must fit */
+ sizes[j] = 8;
+ }
+ else
+ {
+ const Oid ftype = PQftype(res, j);
- switch (ftype)
- {
- case INT2OID:
- case INT4OID:
- case INT8OID:
- case FLOAT4OID:
- case FLOAT8OID:
- case NUMERICOID:
- case OIDOID:
- case XIDOID:
- case CIDOID:
- case CASHOID:
- aligns[j] = 'r';
- break;
- default:
- aligns[j] = 'l';
- }
- }
+ switch (ftype)
+ {
+ case INT2OID:
+ case INT4OID:
+ case INT8OID:
+ case FLOAT4OID:
+ case FLOAT8OID:
+ case NUMERICOID:
+ case OIDOID:
+ case XIDOID:
+ case CIDOID:
+ case CASHOID:
+ aligns[j] = 'r';
+ break;
+ default:
+ aligns[j] = 'l';
}
- for (i = 0; i < m; i++)
+ }
+ }
+ for (i = 0; i < m; ++i)
+ {
+ for (j = 0; j < n; ++j)
+ {
+ if (aligns[j])
{
- for (j = 0; j < n; j++)
- {
- if (aligns[j])
- {
- const int k = PQgetlength(res,
i, j);
+ const int k = PQgetlength(res, i, j);
- if (sizes[j] < k)
- /* value must fit */
- sizes[j] = k;
- }
- }
+ if (sizes[j] < k)
+ /* value must fit */
+ sizes[j] = k;
}
- size = 0;
- /* size of one row */
- for (j = 0; j < n; j++) size += sizes[j] + 1;
- /* times number of rows incl. heading */
- size *= (m + 2);
- /* plus size of footer */
- size += 40;
- /* is the buffer size that needs to be allocated */
- buffer = (char *) PyMem_Malloc(size);
- if (buffer)
- {
- char *p = buffer;
- PyObject *result;
+ }
+ }
+ size = 0;
+ /* size of one row */
+ for (j = 0; j < n; ++j) size += sizes[j] + 1;
+ /* times number of rows incl. heading */
+ size *= (m + 2);
+ /* plus size of footer */
+ size += 40;
+ /* is the buffer size that needs to be allocated */
+ buffer = (char *) PyMem_Malloc(size);
+ if (!buffer)
+ {
+ PyMem_Free(aligns); PyMem_Free(sizes); return PyErr_NoMemory();
+ }
+ char *p = buffer;
+ PyObject *result;
- /* create the header */
- for (j = 0; j < n; j++)
- {
- const char * const s = PQfname(res, j);
- const int k = sizes[j];
- const int h = (k - (int)strlen(s)) / 2;
-
- sprintf(p, "%*s", h, "");
- sprintf(p + h, "%-*s", k - h, s);
- p += k;
- if (j + 1 < n)
- *p++ = '|';
- }
- *p++ = '\n';
- for (j = 0; j < n; j++)
- {
- int k = sizes[j];
+ /* create the header */
+ for (j = 0; j < n; ++j)
+ {
+ const char * const s = PQfname(res, j);
+ const int k = sizes[j];
+ const int h = (k - (int)strlen(s)) / 2;
- while (k--)
- *p++ = '-';
- if (j + 1 < n)
- *p++ = '+';
- }
- *p++ = '\n';
- /* create the body */
- for (i = 0; i < m; i++)
- {
- for (j = 0; j < n; j++)
- {
- const char align = aligns[j];
- const int k = sizes[j];
+ sprintf(p, "%*s", h, "");
+ sprintf(p + h, "%-*s", k - h, s);
+ p += k;
+ if (j + 1 < n)
+ *p++ = '|';
+ }
+ *p++ = '\n';
+ for (j = 0; j < n; ++j)
+ {
+ int k = sizes[j];
- if (align)
- {
- sprintf(p, align == 'r'
?
- "%*s" : "%-*s",
k,
- PQgetvalue(res,
i, j));
- }
- else
- {
- sprintf(p, "%-*s", k,
-
PQgetisnull(res, i, j) ?
- "" :
"<binary>");
- }
- p += k;
- if (j + 1 < n)
- *p++ = '|';
- }
- *p++ = '\n';
- }
- /* free memory */
- PyMem_Free(aligns);
- PyMem_Free(sizes);
- /* create the footer */
- sprintf(p, "(%d row%s)", m, m == 1 ? "" : "s");
- /* return the result */
- result = PyStr_FromString(buffer);
- PyMem_Free(buffer);
- return result;
+ while (k--)
+ *p++ = '-';
+ if (j + 1 < n)
+ *p++ = '+';
+ }
+ *p++ = '\n';
+ /* create the body */
+ for (i = 0; i < m; ++i)
+ {
+ for (j = 0; j < n; ++j)
+ {
+ const char align = aligns[j];
+ const int k = sizes[j];
+
+ if (align)
+ {
+ sprintf(p, align == 'r' ?
+ "%*s" : "%-*s", k,
+ PQgetvalue(res, i, j));
}
else
{
- PyErr_SetString(PyExc_MemoryError,
- "Not enough memory for formatting the
query result");
- return NULL;
+ sprintf(p, "%-*s", k,
+ PQgetisnull(res, i, j) ?
+ "" : "<binary>");
}
- } else {
- PyMem_Free(aligns);
- PyMem_Free(sizes);
- PyErr_SetString(PyExc_MemoryError,
- "Not enough memory for formatting the query
result");
- return NULL;
- }
- }
- else
- return PyStr_FromString("(nothing selected)");
+ p += k;
+ if (j + 1 < n)
+ *p++ = '|';
+ }
+ *p++ = '\n';
+ }
+ /* free memory */
+ PyMem_Free(aligns); PyMem_Free(sizes);
+ /* create the footer */
+ sprintf(p, "(%d row%s)", m, m == 1 ? "" : "s");
+ /* return the result */
+ result = PyStr_FromString(buffer);
+ PyMem_Free(buffer);
+ return result;
}
/* --------------------------------------------------------------------- */
@@ -1294,19 +1723,17 @@
str = (PyObject **)PyMem_Malloc(nparms * sizeof(*str));
parms = (char **)PyMem_Malloc(nparms * sizeof(*parms));
- if (!str || !parms) {
- PyMem_Free(parms);
- PyMem_Free(str);
- Py_XDECREF(query_obj);
- Py_XDECREF(param_obj);
- PyErr_SetString(PyExc_MemoryError, "Memory error in
query()");
- return NULL;
+ if (!str || !parms)
+ {
+ PyMem_Free(parms); PyMem_Free(str);
+ Py_XDECREF(query_obj); Py_XDECREF(param_obj);
+ return PyErr_NoMemory();
}
/* convert optional args to a list of strings -- this allows
* the caller to pass whatever they like, and prevents us
* from having to map types to OIDs */
- for (i = 0, s=str, p=parms; i < nparms; i++, p++)
+ for (i = 0, s=str, p=parms; i < nparms; ++i, ++p)
{
PyObject *obj = PySequence_Fast_GET_ITEM(param_obj, i);
@@ -1427,10 +1854,7 @@
}
if (!(npgobj = PyObject_NEW(queryObject, &queryType)))
- {
- PyErr_SetString(PyExc_MemoryError, "Can't create query object");
- return NULL;
- }
+ return PyErr_NoMemory();
/* stores result and returns object */
npgobj->result = result;
@@ -1613,11 +2037,7 @@
/* allocate buffer */
if (!(buffer = PyMem_Malloc(MAX_BUFFER_SIZE)))
- {
- PyErr_SetString(PyExc_MemoryError,
- "Can't allocate insert buffer");
- return NULL;
- }
+ return PyErr_NoMemory();
/* starts query */
sprintf(buffer, "copy %s from stdin", table);
@@ -1640,7 +2060,7 @@
n = 0; /* not strictly necessary but avoids warning */
/* feed table */
- for (i = 0; i < m; i++)
+ for (i = 0; i < m; ++i)
{
sublist = getitem(list, i);
if (PyTuple_Check(sublist))
@@ -1678,7 +2098,7 @@
bufpt = buffer;
bufsiz = MAX_BUFFER_SIZE - 1;
- for (j = 0; j < n; j++)
+ for (j = 0; j < n; ++j)
{
if (j)
{
@@ -1759,10 +2179,7 @@
if (bufsiz <= 0)
{
- PyMem_Free(buffer);
- PyErr_SetString(PyExc_MemoryError,
- "Insert buffer overflow");
- return NULL;
+ PyMem_Free(buffer); return PyErr_NoMemory();
}
}
@@ -2819,26 +3236,24 @@
size = self->max_row - self->current_row;
/* allocate list for result */
- if (!(reslist = PyList_New(0)))
- return NULL;
+ if (!(reslist = PyList_New(0))) return NULL;
#if IS_PY3
encoding = self->encoding;
#endif
/* builds result */
- for (i = 0, k = self->current_row; i < size; i++, k++)
+ for (i = 0, k = self->current_row; i < size; ++i, ++k)
{
PyObject *rowtuple;
int j;
if (!(rowtuple = PyTuple_New(self->num_fields)))
{
- Py_DECREF(reslist);
- return NULL;
+ Py_DECREF(reslist); return NULL;
}
- for (j = 0; j < self->num_fields; j++)
+ for (j = 0; j < self->num_fields; ++j)
{
PyObject *str;
@@ -2847,7 +3262,8 @@
Py_INCREF(Py_None);
str = Py_None;
}
- else {
+ else
+ {
char *s = PQgetvalue(self->result, k, j);
Py_ssize_t size = PQgetlength(self->result, k,
j);
#if IS_PY3
@@ -2864,7 +3280,10 @@
PyTuple_SET_ITEM(rowtuple, j, str);
}
- PyList_Append(reslist, rowtuple);
+ if (PyList_Append(reslist, rowtuple))
+ {
+ Py_DECREF(rowtuple); Py_DECREF(reslist); return NULL;
+ }
Py_DECREF(rowtuple);
}
@@ -2901,7 +3320,7 @@
break;
case QUERY_MOVENEXT:
if (self->current_row != self->max_row)
- self->current_row++;
+ ++self->current_row;
break;
case QUERY_MOVEPREV:
if (self->current_row > 0)
@@ -2978,7 +3397,8 @@
if (!PyArg_ParseTuple(args, "O", &buffer_obj))
return NULL;
- if (buffer_obj == Py_None) {
+ if (buffer_obj == Py_None)
+ {
/* pass None for terminating the operation */
buffer = errormsg = NULL;
buffer_obj = NULL;
@@ -3242,7 +3662,7 @@
if (!(result = PyTuple_New(self->num_fields)))
return NULL;
- for (i = 0; i < self->num_fields; i++)
+ for (i = 0; i < self->num_fields; ++i)
{
info = pgsource_buildinfo(self, i);
if (!info)
@@ -3605,7 +4025,7 @@
n = PQnfields(self->result);
fieldstuple = PyTuple_New(n);
- for (i = 0; i < n; i++)
+ for (i = 0; i < n; ++i)
{
name = PQfname(self->result, i);
str = PyStr_FromString(name);
@@ -3682,10 +4102,7 @@
queryGetResult(queryObject *self, PyObject *args)
{
PyObject *reslist;
- int i,
- m,
- n,
- *coltypes;
+ int i, m, n, *col_types;
int encoding = self->encoding;
/* checks args (args == NULL for an internal call) */
@@ -3699,12 +4116,11 @@
/* stores result in tuple */
m = PQntuples(self->result);
n = PQnfields(self->result);
- if (!(reslist = PyList_New(m)))
- return NULL;
+ if (!(reslist = PyList_New(m))) return NULL;
- coltypes = get_type_array(self->result, n);
+ if (!(col_types = get_col_types(self->result, n))) return NULL;
- for (i = 0; i < m; i++)
+ for (i = 0; i < m; ++i)
{
PyObject *rowtuple;
int j;
@@ -3716,7 +4132,7 @@
goto exit;
}
- for (j = 0; j < n; j++)
+ for (j = 0; j < n; ++j)
{
PyObject * val;
@@ -3727,17 +4143,22 @@
}
else /* not null */
{
- char *s = PQgetvalue(self->result, i, j);
- Py_ssize_t size =
PQgetlength(self->result, i, j);;
-
- if (PQfformat(self->result, j) == 0) /* text */
- {
- val = cast_value(s, coltypes[j], size,
encoding);
- }
- else /* not text */
- {
- val = PyBytes_FromStringAndSize(s,
size);
- }
+ /* get the string representation of the value */
+ /* note: this is always null-terminated text
format */
+ char *s = PQgetvalue(self->result, i, j);
+ /* get the PyGreSQL type of the column */
+ int type = col_types[j];
+
+ if (type & PYGRES_ARRAY)
+ val = cast_array(s,
PQgetlength(self->result, i, j),
+ encoding, type, NULL, 0);
+ else if (type == PYGRES_BYTEA)
+ val = cast_bytea_text(s);
+ else if (type & PYGRES_TEXT)
+ val = cast_sized_text(s,
PQgetlength(self->result, i, j),
+ encoding, type);
+ else
+ val = cast_unsized_simple(s, type);
}
if (!val)
@@ -3755,7 +4176,7 @@
}
exit:
- PyMem_Free(coltypes);
+ PyMem_Free(col_types);
/* returns list */
return reslist;
@@ -3774,7 +4195,7 @@
int i,
m,
n,
- *coltypes;
+ *col_types;
int encoding = self->encoding;
/* checks args (args == NULL for an internal call) */
@@ -3788,12 +4209,11 @@
/* stores result in list */
m = PQntuples(self->result);
n = PQnfields(self->result);
- if (!(reslist = PyList_New(m)))
- return NULL;
+ if (!(reslist = PyList_New(m))) return NULL;
- coltypes = get_type_array(self->result, n);
+ if (!(col_types = get_col_types(self->result, n))) return NULL;
- for (i = 0; i < m; i++)
+ for (i = 0; i < m; ++i)
{
PyObject *dict;
int j;
@@ -3805,7 +4225,7 @@
goto exit;
}
- for (j = 0; j < n; j++)
+ for (j = 0; j < n; ++j)
{
PyObject * val;
@@ -3816,17 +4236,22 @@
}
else /* not null */
{
- char *s = PQgetvalue(self->result, i, j);
- Py_ssize_t size =
PQgetlength(self->result, i, j);;
-
- if (PQfformat(self->result, j) == 0) /* text */
- {
- val = cast_value(s, coltypes[j], size,
encoding);
- }
- else /* not text */
- {
- val = PyBytes_FromStringAndSize(s,
size);
- }
+ /* get the string representation of the value */
+ /* note: this is always null-terminated text
format */
+ char *s = PQgetvalue(self->result, i, j);
+ /* get the PyGreSQL type of the column */
+ int type = col_types[j];
+
+ if (type & PYGRES_ARRAY)
+ val = cast_array(s,
PQgetlength(self->result, i, j),
+ encoding, type, NULL, 0);
+ else if (type == PYGRES_BYTEA)
+ val = cast_bytea_text(s);
+ else if (type & PYGRES_TEXT)
+ val = cast_sized_text(s,
PQgetlength(self->result, i, j),
+ encoding, type);
+ else
+ val = cast_unsized_simple(s, type);
}
if (!val)
@@ -3845,7 +4270,7 @@
}
exit:
- PyMem_Free(coltypes);
+ PyMem_Free(col_types);
/* returns list */
return reslist;
@@ -3860,8 +4285,7 @@
static PyObject *
queryNamedResult(queryObject *self, PyObject *args)
{
- PyObject *arglist,
- *ret;
+ PyObject *ret;
if (namedresult)
{
@@ -3873,9 +4297,7 @@
return NULL;
}
- arglist = Py_BuildValue("(O)", self);
- ret = PyObject_CallObject(namedresult, arglist);
- Py_DECREF(arglist);
+ ret = PyObject_CallFunction(namedresult, "(O)", self);
if (ret == NULL)
return NULL;
@@ -4203,9 +4625,11 @@
Py_XDECREF(from_obj);
+ if (!to) return PyErr_NoMemory();
+
to_obj = PyBytes_FromStringAndSize(to, to_length);
- if (to)
- PQfreemem(to);
+ PQfreemem(to);
+
return to_obj;
}
@@ -4225,7 +4649,9 @@
{
s[0] = decimal_point; s[1] = '\0';
ret = PyStr_FromString(s);
- } else {
+ }
+ else
+ {
Py_INCREF(Py_None); ret = Py_None;
}
}
@@ -4261,7 +4687,9 @@
{
decimal_point = *s;
Py_INCREF(Py_None); ret = Py_None;
- } else {
+ }
+ else
+ {
PyErr_SetString(PyExc_TypeError,
"set_decimal_point() expects a decimal mark character");
}
@@ -4738,6 +5166,55 @@
}
#endif /* DEFAULT_VARS */
+/* cast a string with a text representation of an array to a list */
+static char pgCastArray__doc__[] =
+"cast_array(string, cast=None, delim=',') -- cast a string as an array";
+
+PyObject *
+pgCastArray(PyObject *self, PyObject *args, PyObject *dict)
+{
+ static const char *kwlist[] = {"string", "cast", "delim", NULL};
+ PyObject *string_obj, *cast_obj = NULL;
+ char *string;
+ Py_ssize_t size;
+ int encoding;
+ char delim = ',';
+
+ if (!PyArg_ParseTupleAndKeywords(args, dict, "O|Oc",
+ (char **) kwlist, &string_obj, &cast_obj, &delim))
+ return NULL;
+
+ if (PyBytes_Check(string_obj))
+ {
+ encoding = pg_encoding_ascii;
+ PyBytes_AsStringAndSize(string_obj, &string, &size);
+ string_obj = NULL;
+ }
+ else if (PyUnicode_Check(string_obj))
+ {
+ encoding = pg_encoding_utf8;
+ string_obj = get_encoded_string(string_obj, encoding);
+ if (!string_obj) return NULL; /* pass the UnicodeEncodeError */
+ PyBytes_AsStringAndSize(string_obj, &string, &size);
+ }
+ else
+ {
+ PyErr_SetString(PyExc_TypeError, "cast_array() expects a
string");
+ return NULL;
+ }
+
+ if (!cast_obj || cast_obj == Py_None)
+ cast_obj = NULL;
+ else if (!PyCallable_Check(cast_obj))
+ {
+ PyErr_SetString(PyExc_TypeError, "The cast argument must be
callable");
+ return NULL;
+ }
+
+ return cast_array(string, size, encoding, 0, cast_obj, delim);
+}
+
+
/* List of functions defined in the module */
static struct PyMethodDef pgMethods[] = {
@@ -4767,6 +5244,8 @@
pgGetJsondecode__doc__},
{"set_jsondecode", (PyCFunction) pgSetJsondecode, METH_VARARGS,
pgSetJsondecode__doc__},
+ {"cast_array", (PyCFunction) pgCastArray, METH_VARARGS|METH_KEYWORDS,
+ pgCastArray__doc__},
#ifdef DEFAULT_VARS
{"get_defhost", pgGetDefHost, METH_VARARGS, pgGetDefHost__doc__},
Modified: trunk/pgtypes.h
==============================================================================
--- trunk/pgtypes.h Sat Jan 23 08:34:22 2016 (r780)
+++ trunk/pgtypes.h Mon Jan 25 15:44:52 2016 (r781)
@@ -100,4 +100,28 @@
#define TSM_HANDLEROID 3310
#define ANYRANGEOID 3831
+/* more types */
+
+#define JSONARRAYOID 199
+#define CASHARRAYOID 791
+#define BOOLARRAYOID 1000
+#define BYTEAARRAYOID 1001
+#define CHARARRAYOID 1002
+#define XIDARRAYOID 1011
+#define CIDARRAYOID 1012
+#define BPCHARARRAYOID 1014
+#define VARCHARARRAYOID 1015
+#define INT8ARRAYOID 1016
+#define FLOAT8ARRAYOID 1022
+#define ABSTIMEARRAYOID 1023
+#define RELTIMEARRAYOID 1024
+#define TIMESTAMPARRAYOID 1115
+#define DATEARRAYOID 1182
+#define TIMEARRAYOID 1183
+#define TIMESTAMPTZARRAYOID 1185
+#define INTERVALARRAYOID 1187
+#define NUMERICARRAYOID 1231
+#define TIMETZARRAYOID 1270
+#define JSONBARRAYOID 3807
+
#endif /* PG_TYPE_H */
Modified: trunk/tests/test_classic_connection.py
==============================================================================
--- trunk/tests/test_classic_connection.py Sat Jan 23 08:34:22 2016
(r780)
+++ trunk/tests/test_classic_connection.py Mon Jan 25 15:44:52 2016
(r781)
@@ -866,6 +866,81 @@
).dictresult(), [{'garbage': garbage}])
+class TestQueryResultTypes(unittest.TestCase):
+ """Test proper result types via a basic pg connection."""
+
+ def setUp(self):
+ self.c = connect()
+ self.c.query('set client_encoding=utf8')
+ self.c.query("set datestyle='ISO,YMD'")
+
+ def tearDown(self):
+ self.c.close()
+
+ def assert_proper_cast(self, value, pgtype, pytype):
+ q = 'select $1::%s' % (pgtype,)
+ r = self.c.query(q, (value,)).getresult()[0][0]
+ self.assertIsInstance(r, pytype)
+ if isinstance(value, (bytes, str)):
+ if not value or '{':
+ value = '"%s"' % value
+ value = '{%s}' % value
+ r = self.c.query(q + '[]', (value,)).getresult()[0][0]
+ self.assertIsInstance(r, list)
+ self.assertEqual(len(r), 1)
+ self.assertIsInstance(r[0], pytype)
+
+ def testInt(self):
+ self.assert_proper_cast(0, 'int', int)
+ self.assert_proper_cast(0, 'smallint', int)
+ self.assert_proper_cast(0, 'oid', int)
+ self.assert_proper_cast(0, 'cid', int)
+ self.assert_proper_cast(0, 'xid', int)
+
+ def testLong(self):
+ self.assert_proper_cast(0, 'bigint', long)
+
+ def testFloat(self):
+ self.assert_proper_cast(0, 'float', float)
+ self.assert_proper_cast(0, 'real', float)
+ self.assert_proper_cast(0, 'double', float)
+ self.assert_proper_cast(0, 'double precision', float)
+ self.assert_proper_cast('infinity', 'float', float)
+
+ def testFloat(self):
+ decimal = pg.get_decimal()
+ self.assert_proper_cast(decimal(0), 'numeric', decimal)
+ self.assert_proper_cast(decimal(0), 'decimal', decimal)
+
+ def testMoney(self):
+ decimal = pg.get_decimal()
+ self.assert_proper_cast(decimal('0'), 'money', decimal)
+
+ def testBool(self):
+ bool_type = bool if pg.get_bool() else str
+ self.assert_proper_cast('f', 'bool', bool_type)
+
+ def testDate(self):
+ self.assert_proper_cast('1956-01-31', 'date', str)
+ self.assert_proper_cast('0', 'interval', str)
+ self.assert_proper_cast('08:42', 'time', str)
+ self.assert_proper_cast('08:42', 'timetz', str)
+ self.assert_proper_cast('1956-01-31 08:42', 'timestamp', str)
+ self.assert_proper_cast('1956-01-31 08:42', 'timestamptz', str)
+
+ def testText(self):
+ self.assert_proper_cast('', 'text', str)
+ self.assert_proper_cast('', 'char', str)
+ self.assert_proper_cast('', 'bpchar', str)
+ self.assert_proper_cast('', 'varchar', str)
+
+ def testBytea(self):
+ self.assert_proper_cast('', 'bytea', bytes)
+
+ def testJson(self):
+ self.assert_proper_cast('{}', 'json', dict)
+
+
class TestInserttable(unittest.TestCase):
"""Test inserttable method."""
Modified: trunk/tests/test_classic_dbwrapper.py
==============================================================================
--- trunk/tests/test_classic_dbwrapper.py Sat Jan 23 08:34:22 2016
(r780)
+++ trunk/tests/test_classic_dbwrapper.py Mon Jan 25 15:44:52 2016
(r781)
@@ -375,6 +375,8 @@
class TestDBClass(unittest.TestCase):
"""Test the methods of the DB class wrapped pg connection."""
+ maxDiff = 80 * 20
+
cls_set_up = False
@classmethod
@@ -1437,6 +1439,7 @@
insert = self.db.insert
query = self.db.query
self.createTable('test_table', 'n int', oids=True)
+ self.assertRaises(pg.ProgrammingError, insert, 'test_table', m=1)
r = insert('test_table', n=1)
self.assertIsInstance(r, dict)
self.assertEqual(r['n'], 1)
@@ -1497,7 +1500,6 @@
r = insert('test_table', r)
self.assertIsInstance(r, dict)
self.assertEqual(r['n'], 6)
- r = query(q).getresult()
r = ' '.join(str(row[0]) for row in query(q).getresult())
self.assertEqual(r, '6 7')
@@ -2737,15 +2739,13 @@
self.createTable('bytea_test', 'n smallint primary key, data bytea')
s = b"It's all \\ kinds \x00 of\r nasty \xff stuff!\n"
r = self.db.escape_bytea(s)
- query('insert into bytea_test values(3,$1)', (r,))
+ query('insert into bytea_test values(3, $1)', (r,))
r = query('select * from bytea_test where n=3').getresult()
self.assertEqual(len(r), 1)
r = r[0]
self.assertEqual(len(r), 2)
self.assertEqual(r[0], 3)
r = r[1]
- self.assertIsInstance(r, str)
- r = self.db.unescape_bytea(r)
self.assertIsInstance(r, bytes)
self.assertEqual(r, s)
@@ -2796,8 +2796,6 @@
self.assertEqual(len(r), 2)
self.assertEqual(r[0], 5)
r = r[1]
- self.assertIsInstance(r, str)
- r = self.db.unescape_bytea(r)
self.assertIsInstance(r, bytes)
self.assertEqual(r, s)
r = self.db.get('bytea_test', dict(n=5))
@@ -2894,6 +2892,13 @@
self.assertIsInstance(r['new'], bool)
self.assertIsInstance(r['tags'], list)
self.assertIsInstance(r['stock'], dict)
+ # insert JSON object as text
+ self.db.insert('json_test', n=2, data=json.dumps(data))
+ q = "select data from json_test where n in (1, 2) order by n"
+ r = self.db.query(q).getresult()
+ self.assertEqual(len(r), 2)
+ self.assertIsInstance(r[0][0], str if jsondecode is None else dict)
+ self.assertEqual(r[0][0], r[1][0])
def testInsertGetJsonb(self):
try:
@@ -2958,6 +2963,209 @@
self.assertIsInstance(r['tags'], list)
self.assertIsInstance(r['stock'], dict)
+ def testArray(self):
+ self.createTable('arraytest',
+ 'id smallint, i2 smallint[], i4 integer[], i8 bigint[],'
+ ' d numeric[], f4 real[], f8 double precision[], m money[],'
+ ' b bool[], v4 varchar(4)[], c4 char(4)[], t text[]')
+ r = self.db.get_attnames('arraytest')
+ self.assertEqual(r, dict(id='int', i2='int[]', i4='int[]', i8='int[]',
+ d='num[]', f4='float[]', f8='float[]', m='money[]',
+ b='bool[]', v4='text[]', c4='text[]', t='text[]'))
+ decimal = pg.get_decimal()
+ if decimal is Decimal:
+ long_decimal = decimal('123456789.123456789')
+ odd_money = decimal('1234567891234567.89')
+ else:
+ long_decimal = decimal('12345671234.5')
+ odd_money = decimal('1234567123.25')
+ t, f = (True, False) if pg.get_bool() else ('t', 'f')
+ data = dict(id=42, i2=[42, 1234, None, 0, -1],
+ i4=[42, 123456789, None, 0, 1, -1],
+ i8=[long(42), long(123456789123456789), None,
+ long(0), long(1), long(-1)],
+ d=[decimal(42), long_decimal, None,
+ decimal(0), decimal(1), decimal(-1), -long_decimal],
+ f4=[42.0, 1234.5, None, 0.0, 1.0, -1.0,
+ float('inf'), float('-inf')],
+ f8=[42.0, 12345671234.5, None, 0.0, 1.0, -1.0,
+ float('inf'), float('-inf')],
+ m=[decimal('42.00'), odd_money, None,
+ decimal('0.00'), decimal('1.00'), decimal('-1.00'), -odd_money],
+ b=[t, f, t, None, f, t, None, None, t],
+ v4=['abc', '"Hi"', '', None], c4=['abc ', '"Hi"', ' ', None],
+ t=['abc', 'Hello, World!', '"Hello, World!"', '', None])
+ r = data.copy()
+ self.db.insert('arraytest', r)
+ self.assertEqual(r, data)
+ self.db.insert('arraytest', r)
+ r = self.db.get('arraytest', 42, 'id')
+ self.assertEqual(r, data)
+ r = self.db.query('select * from arraytest limit 1').dictresult()[0]
+ self.assertEqual(r, data)
+
+ def testArrayInput(self):
+ insert = self.db.insert
+ self.createTable('arraytest', 'i int[], t text[]', oids=True)
+ r = dict(i=[1, 2, 3], t=['a', 'b', 'c'])
+ insert('arraytest', r)
+ self.assertEqual(r['i'], [1, 2, 3])
+ self.assertEqual(r['t'], ['a', 'b', 'c'])
+ r = dict(i='{1,2,3}', t='{a,b,c}')
+ self.db.insert('arraytest', r)
+ self.assertEqual(r['i'], [1, 2, 3])
+ self.assertEqual(r['t'], ['a', 'b', 'c'])
+ r = dict(i="[1, 2, 3]", t="['a', 'b', 'c']")
+ self.db.insert('arraytest', r)
+ self.assertEqual(r['i'], [1, 2, 3])
+ self.assertEqual(r['t'], ['a', 'b', 'c'])
+ r = dict(i="array[1, 2, 3]", t="array['a', 'b', 'c']")
+ self.db.insert('arraytest', r)
+ self.assertEqual(r['i'], [1, 2, 3])
+ self.assertEqual(r['t'], ['a', 'b', 'c'])
+ r = dict(i="ARRAY[1, 2, 3]", t="ARRAY['a', 'b', 'c']")
+ self.db.insert('arraytest', r)
+ self.assertEqual(r['i'], [1, 2, 3])
+ self.assertEqual(r['t'], ['a', 'b', 'c'])
+ r = dict(i="1, 2, 3", t="'a', 'b', 'c'")
+ self.assertRaises(ValueError, self.db.insert, 'arraytest', r)
+
+ def testArrayOfIds(self):
+ self.createTable('arraytest', 'c cid[], o oid[], x xid[]', oids=True)
+ r = self.db.get_attnames('arraytest')
+ self.assertEqual(r, dict(oid='int', c='int[]', o='int[]', x='int[]'))
+ data = dict(c=[11, 12, 13], o=[21, 22, 23], x=[31, 32, 33])
+ r = data.copy()
+ self.db.insert('arraytest', r)
+ qoid = 'oid(arraytest)'
+ oid = r.pop(qoid)
+ self.assertEqual(r, data)
+ r = {qoid: oid}
+ self.db.get('arraytest', r)
+ self.assertEqual(oid, r.pop(qoid))
+ self.assertEqual(r, data)
+
+ def testArrayOfText(self):
+ self.createTable('arraytest', 'data text[]', oids=True)
+ r = self.db.get_attnames('arraytest')
+ self.assertEqual(r['data'], 'text[]')
+ data = ['Hello, World!', '', None, '{a,b,c}', '"Hi!"',
+ 'null', 'NULL', 'Null', 'nulL',
+ "It's all \\ kinds of\r nasty stuff!\n"]
+ r = dict(data=data)
+ self.db.insert('arraytest', r)
+ self.assertEqual(r['data'], data)
+ self.assertIsInstance(r['data'][1], str)
+ self.assertIsNone(r['data'][2])
+ r['data'] = None
+ self.db.get('arraytest', r)
+ self.assertEqual(r['data'], data)
+ self.assertIsInstance(r['data'][1], str)
+ self.assertIsNone(r['data'][2])
+
+ def testArrayOfBytea(self):
+ self.createTable('arraytest', 'data bytea[]', oids=True)
+ r = self.db.get_attnames('arraytest')
+ self.assertEqual(r['data'], 'bytea[]')
+ data = [b'Hello, World!', b'', None, b'{a,b,c}', b'"Hi!"',
+ b"It's all \\ kinds \x00 of\r nasty \xff stuff!\n"]
+ r = dict(data=data)
+ self.db.insert('arraytest', r)
+ self.assertEqual(r['data'], data)
+ self.assertIsInstance(r['data'][1], bytes)
+ self.assertIsNone(r['data'][2])
+ r['data'] = None
+ self.db.get('arraytest', r)
+ self.assertEqual(r['data'], data)
+ self.assertIsInstance(r['data'][1], bytes)
+ self.assertIsNone(r['data'][2])
+
+ def testArrayOfJson(self):
+ try:
+ self.createTable('arraytest', 'data json[]', oids=True)
+ except pg.ProgrammingError as error:
+ if self.db.server_version < 90200:
+ self.skipTest('database does not support json')
+ self.fail(str(error))
+ r = self.db.get_attnames('arraytest')
+ self.assertEqual(r['data'], 'json[]')
+ data = [dict(id=815, name='John Doe'), dict(id=816, name='Jane Roe')]
+ jsondecode = pg.get_jsondecode()
+ r = dict(data=data)
+ self.db.insert('arraytest', r)
+ if jsondecode is None:
+ r['data'] = [json.loads(d) for d in r['data']]
+ self.assertEqual(r['data'], data)
+ r['data'] = None
+ self.db.get('arraytest', r)
+ if jsondecode is None:
+ r['data'] = [json.loads(d) for d in r['data']]
+ self.assertEqual(r['data'], data)
+ r = dict(data=[json.dumps(d) for d in data])
+ self.db.insert('arraytest', r)
+ if jsondecode is None:
+ r['data'] = [json.loads(d) for d in r['data']]
+ self.assertEqual(r['data'], data)
+ r['data'] = None
+ self.db.get('arraytest', r)
+ # insert empty json values
+ r = dict(data=['', None])
+ self.db.insert('arraytest', r)
+ r = r['data']
+ self.assertIsInstance(r, list)
+ self.assertEqual(len(r), 2)
+ self.assertIsNone(r[0])
+ self.assertIsNone(r[1])
+
+ def testArrayOfJsonb(self):
+ try:
+ self.createTable('arraytest', 'data jsonb[]', oids=True)
+ except pg.ProgrammingError as error:
+ if self.db.server_version < 90400:
+ self.skipTest('database does not support jsonb')
+ self.fail(str(error))
+ r = self.db.get_attnames('arraytest')
+ self.assertEqual(r['data'], 'json[]')
+ data = [dict(id=815, name='John Doe'), dict(id=816, name='Jane Roe')]
+ jsondecode = pg.get_jsondecode()
+ r = dict(data=data)
+ self.db.insert('arraytest', r)
+ if jsondecode is None:
+ r['data'] = [json.loads(d) for d in r['data']]
+ self.assertEqual(r['data'], data)
+ r['data'] = None
+ self.db.get('arraytest', r)
+ if jsondecode is None:
+ r['data'] = [json.loads(d) for d in r['data']]
+ self.assertEqual(r['data'], data)
+ r = dict(data=[json.dumps(d) for d in data])
+ self.db.insert('arraytest', r)
+ if jsondecode is None:
+ r['data'] = [json.loads(d) for d in r['data']]
+ self.assertEqual(r['data'], data)
+ r['data'] = None
+ self.db.get('arraytest', r)
+ # insert empty json values
+ r = dict(data=['', None])
+ self.db.insert('arraytest', r)
+ r = r['data']
+ self.assertIsInstance(r, list)
+ self.assertEqual(len(r), 2)
+ self.assertIsNone(r[0])
+ self.assertIsNone(r[1])
+
+ def testDeepArray(self):
+ self.createTable('arraytest', 'data text[][][]', oids=True)
+ r = self.db.get_attnames('arraytest')
+ self.assertEqual(r['data'], 'text[]')
+ data = [[['Hello, World!', '{a,b,c}', 'back\\slash']]]
+ r = dict(data=data)
+ self.db.insert('arraytest', r)
+ self.assertEqual(r['data'], data)
+ r['data'] = None
+ self.db.get('arraytest', r)
+ self.assertEqual(r['data'], data)
+
def testNotificationHandler(self):
# the notification handler itself is tested separately
f = self.db.notification_handler
@@ -3249,6 +3457,14 @@
self.assertEqual(output, ["select 1", "select 2"])
self.assertEqual(self.get_output(), "")
+ def testDebugMultipleArgs(self):
+ output = []
+ self.db.debug = output.append
+ args = ['Error', 42, {1: 'a', 2: 'b'}, [3, 5, 7]]
+ self.db._do_debug(*args)
+ self.assertEqual(output, ['\n'.join(str(arg) for arg in args)])
+ self.assertEqual(self.get_output(), "")
+
if __name__ == '__main__':
unittest.main()
Modified: trunk/tests/test_classic_functions.py
==============================================================================
--- trunk/tests/test_classic_functions.py Sat Jan 23 08:34:22 2016
(r780)
+++ trunk/tests/test_classic_functions.py Mon Jan 25 15:44:52 2016
(r781)
@@ -113,6 +113,229 @@
self.assertEqual(pg.get_defbase(), d0)
+class TestParseArray(unittest.TestCase):
+ """Test the array parser."""
+
+ array_expressions = [
+ ('', str, ValueError),
+ ('{}', None, []),
+ ('{}', str, []),
+ (' { } ', None, []),
+ ('{', str, ValueError),
+ ('{{}', str, ValueError),
+ ('{}{', str, ValueError),
+ ('[]', str, ValueError),
+ ('()', str, ValueError),
+ ('{[]}', str, ['[]']),
+ ('{hello}', int, ValueError),
+ ('{42}', int, [42]),
+ ('{ 42 }', int, [42]),
+ ('{42', int, ValueError),
+ ('{ 42 ', int, ValueError),
+ ('{hello}', str, ['hello']),
+ ('{ hello }', str, ['hello']),
+ ('{hi} ', str, ['hi']),
+ ('{hi} ?', str, ValueError),
+ ('{null}', str, [None]),
+ (' { NULL } ', str, [None]),
+ (' { NULL } ', str, [None]),
+ (' { not null } ', str, ['not null']),
+ (' { not NULL } ', str, ['not NULL']),
+ (' {"null"} ', str, ['null']),
+ (' {"NULL"} ', str, ['NULL']),
+ ('{Hi!}', str, ['Hi!']),
+ ('{"Hi!"}', str, ['Hi!']),
+ ('{" Hi! "}', str, [' Hi! ']),
+ ('{a"}', str, ValueError),
+ ('{"b}', str, ValueError),
+ ('{a"b}', str, ValueError),
+ (r'{a\"b}', str, ['a"b']),
+ (r'{a\,b}', str, ['a,b']),
+ (r'{a\bc}', str, ['abc']),
+ (r'{"a\bc"}', str, ['abc']),
+ (r'{\a\b\c}', str, ['abc']),
+ (r'{"\a\b\c"}', str, ['abc']),
+ ('{"{}"}', str, ['{}']),
+ (r'{\{\}}', str, ['{}']),
+ ('{"{a,b,c}"}', str, ['{a,b,c}']),
+ ("{'abc'}", str, ["'abc'"]),
+ ('{"abc"}', str, ['abc']),
+ (r'{\"abc\"}', str, ['"abc"']),
+ (r"{\'abc\'}", str, ["'abc'"]),
+ (r"{abc,d,efg}", str, ['abc', 'd', 'efg']),
+ ('{Hello World!}', str, ['Hello World!']),
+ ('{Hello, World!}', str, ['Hello', 'World!']),
+ ('{Hello,\ World!}', str, ['Hello', ' World!']),
+ ('{Hello\, World!}', str, ['Hello, World!']),
+ ('{"Hello World!"}', str, ['Hello World!']),
+ ('{this, should, be, null}', str, ['this', 'should', 'be', None]),
+ ('{This, should, be, NULL}', str, ['This', 'should', 'be', None]),
+ ('{3, 2, 1, null}', int, [3, 2, 1, None]),
+ ('{3, 2, 1, NULL}', int, [3, 2, 1, None]),
+ ('{3,17,51}', int, [3, 17, 51]),
+ (' { 3 , 17 , 51 } ', int, [3, 17, 51]),
+ ('{3,17,51}', str, ['3', '17', '51']),
+ (' { 3 , 17 , 51 } ', str, ['3', '17', '51']),
+ ('{1,"2",abc,"def"}', str, ['1', '2', 'abc', 'def']),
+ ('{{}}', int, [[]]),
+ ('{{},{}}', int, [[], []]),
+ ('{ {} , {} , {} }', int, [[], [], []]),
+ ('{ {} , {} , {} , }', int, ValueError),
+ ('{{{1,2,3},{4,5,6}}}', int, [[[1, 2, 3], [4, 5, 6]]]),
+ ('{{1,2,3},{4,5,6},{7,8,9}}', int, [[1, 2, 3], [4, 5, 6], [7, 8, 9]]),
+ ('{20000, 25000, 25000, 25000}', int, [20000, 25000, 25000, 25000]),
+ ('{{{17,18,19},{14,15,16},{11,12,13}},'
+ '{{27,28,29},{24,25,26},{21,22,23}},'
+ '{{37,38,39},{34,35,36},{31,32,33}}}', int,
+ [[[17, 18, 19], [14, 15, 16], [11, 12, 13]],
+ [[27, 28, 29], [24, 25, 26], [21, 22, 23]],
+ [[37, 38, 39], [34, 35, 36], [31, 32, 33]]]),
+ ('{{"breakfast", "consulting"}, {"meeting", "lunch"}}', str,
+ [['breakfast', 'consulting'], ['meeting', 'lunch']]),
+ ('[1:3]={1,2,3}', int, [1, 2, 3]),
+ ('[-1:1]={1,2,3}', int, [1, 2, 3]),
+ ('[-1:+1]={1,2,3}', int, [1, 2, 3]),
+ ('[-3:-1]={1,2,3}', int, [1, 2, 3]),
+ ('[+1:+3]={1,2,3}', int, [1, 2, 3]),
+ ('[]={1,2,3}', int, ValueError),
+ ('[1:]={1,2,3}', int, ValueError),
+ ('[:3]={1,2,3}', int, ValueError),
+ ('[1:1][-2:-1][3:5]={{{1,2,3},{4,5,6}}}',
+ int, [[[1, 2, 3], [4, 5, 6]]]),
+ (' [1:1] [-2:-1] [3:5] = { { { 1 , 2 , 3 }, {4 , 5 , 6 } } }',
+ int, [[[1, 2, 3], [4, 5, 6]]]),
+ ('[1:1][3:5]={{1,2,3},{4,5,6}}', int, [[1, 2, 3], [4, 5, 6]]),
+ ('[3:5]={{1,2,3},{4,5,6}}', int, ValueError),
+ ('[1:1][-2:-1][3:5]={{1,2,3},{4,5,6}}', int, ValueError)]
+
+ def testParserParams(self):
+ f = pg.cast_array
+ self.assertRaises(TypeError, f)
+ self.assertRaises(TypeError, f, None)
+ self.assertRaises(TypeError, f, '{}', 1)
+ self.assertRaises(TypeError, f, '{}', ',',)
+ self.assertRaises(TypeError, f, '{}', None, None)
+ self.assertRaises(TypeError, f, '{}', None, 1)
+ self.assertRaises(TypeError, f, '{}', None, '')
+ self.assertRaises(TypeError, f, '{}', None, ',;')
+ self.assertEqual(f('{}'), [])
+ self.assertEqual(f('{}', None), [])
+ self.assertEqual(f('{}', None, ';'), [])
+ self.assertEqual(f('{}', str), [])
+ self.assertEqual(f('{}', str, ';'), [])
+
+ def testParserSimple(self):
+ r = pg.cast_array('{a,b,c}')
+ self.assertIsInstance(r, list)
+ self.assertEqual(len(r), 3)
+ self.assertEqual(r, ['a', 'b', 'c'])
+
+ def testParserNested(self):
+ f = pg.cast_array
+ r = f('{{a,b,c}}')
+ self.assertIsInstance(r, list)
+ self.assertEqual(len(r), 1)
+ r = r[0]
+ self.assertIsInstance(r, list)
+ self.assertEqual(len(r), 3)
+ self.assertEqual(r, ['a', 'b', 'c'])
+ self.assertRaises(ValueError, f, '{a,{b,c}}')
+ r = f('{{a,b},{c,d}}')
+ self.assertIsInstance(r, list)
+ self.assertEqual(len(r), 2)
+ r = r[1]
+ self.assertIsInstance(r, list)
+ self.assertEqual(len(r), 2)
+ self.assertEqual(r, ['c', 'd'])
+ r = f('{{a},{b},{c}}')
+ self.assertIsInstance(r, list)
+ self.assertEqual(len(r), 3)
+ r = r[1]
+ self.assertIsInstance(r, list)
+ self.assertEqual(len(r), 1)
+ self.assertEqual(r[0], 'b')
+ r = f('{{{{{{{abc}}}}}}}')
+ for i in range(7):
+ self.assertIsInstance(r, list)
+ self.assertEqual(len(r), 1)
+ r = r[0]
+ self.assertEqual(r, 'abc')
+
+ def testParserTooDeeplyNested(self):
+ f = pg.cast_array
+ for n in 3, 5, 9, 12, 16, 32, 64, 256:
+ r = '%sa,b,c%s' % ('{' * n, '}' * n)
+ if n > 16: # hard coded maximum depth
+ self.assertRaises(ValueError, f, r)
+ else:
+ r = f(r)
+ for i in range(n - 1):
+ self.assertIsInstance(r, list)
+ self.assertEqual(len(r), 1)
+ r = r[0]
+ self.assertEqual(len(r), 3)
+ self.assertEqual(r, ['a', 'b', 'c'])
+
+ def testParserCast(self):
+ f = pg.cast_array
+ self.assertEqual(f('{1}'), ['1'])
+ self.assertEqual(f('{1}', None), ['1'])
+ self.assertEqual(f('{1}', int), [1])
+ self.assertEqual(f('{1}', str), ['1'])
+ self.assertEqual(f('{a}'), ['a'])
+ self.assertEqual(f('{a}', None), ['a'])
+ self.assertRaises(ValueError, f, '{a}', int)
+ self.assertEqual(f('{a}', str), ['a'])
+ cast = lambda s: '%s is ok' % s
+ self.assertEqual(f('{a}', cast), ['a is ok'])
+
+ def testParserDelim(self):
+ f = pg.cast_array
+ self.assertEqual(f('{1,2}'), ['1', '2'])
+ self.assertEqual(f('{1,2}', delim=','), ['1', '2'])
+ self.assertEqual(f('{1;2}'), ['1;2'])
+ self.assertEqual(f('{1;2}', delim=';'), ['1', '2'])
+ self.assertEqual(f('{1,2}', delim=';'), ['1,2'])
+
+ def testParserWithData(self):
+ f = pg.cast_array
+ for expression, cast, expected in self.array_expressions:
+ if expected is ValueError:
+ self.assertRaises(ValueError, f, expression, cast)
+ else:
+ self.assertEqual(f(expression, cast), expected)
+
+ def testParserWithoutCast(self):
+ f = pg.cast_array
+
+ for expression, cast, expected in self.array_expressions:
+ if cast is not str:
+ continue
+ if expected is ValueError:
+ self.assertRaises(ValueError, f, expression)
+ else:
+ self.assertEqual(f(expression), expected)
+
+ def testParserWithDifferentDelimiter(self):
+ f = pg.cast_array
+
+ def replace_comma(value):
+ if isinstance(value, basestring):
+ return value.replace(',', ';')
+ elif isinstance(value, list):
+ return [replace_comma(v) for v in value]
+ else:
+ return value
+
+ for expression, cast, expected in self.array_expressions:
+ expression = replace_comma(expression)
+ if expected is ValueError:
+ self.assertRaises(ValueError, f, expression, cast)
+ else:
+ expected = replace_comma(expected)
+ self.assertEqual(f(expression, cast, ';'), expected)
+
+
class TestEscapeFunctions(unittest.TestCase):
"""Test pg escape and unescape functions.
Modified: trunk/tests/test_dbapi20.py
==============================================================================
--- trunk/tests/test_dbapi20.py Sat Jan 23 08:34:22 2016 (r780)
+++ trunk/tests/test_dbapi20.py Mon Jan 25 15:44:52 2016 (r781)
@@ -361,14 +361,15 @@
cur.execute(
"create table %s (n smallint, floattest float)" % table)
params = enumerate(values)
- cur.executemany("insert into %s values (%%s,%%s)" % table, params)
- cur.execute("select * from %s order by 1" % table)
+ cur.executemany("insert into %s values (%%d,%%s)" % table, params)
+ cur.execute("select floattest from %s order by n" % table)
rows = cur.fetchall()
- self.assertEqual(cur.description[1].type_code, pgdb.FLOAT)
+ self.assertEqual(cur.description[0].type_code, pgdb.FLOAT)
+ self.assertNotEqual(cur.description[0].type_code, pgdb.ARRAY)
finally:
con.close()
self.assertEqual(len(rows), len(values))
- rows = [row[1] for row in rows]
+ rows = [row[0] for row in rows]
for inval, outval in zip(values, rows):
if inval in ('inf', 'Infinity'):
inval = inf
@@ -397,43 +398,52 @@
cur.execute(
"create table %s (n smallint, ts timestamp)" % table)
params = enumerate(values)
- cur.executemany("insert into %s values (%%s,%%s)" % table, params)
- cur.execute("select * from %s order by 1" % table)
+ cur.executemany("insert into %s values (%%d,%%s)" % table, params)
+ cur.execute("select ts from %s order by n" % table)
rows = cur.fetchall()
- self.assertEqual(cur.description[1].type_code, pgdb.DATETIME)
+ self.assertEqual(cur.description[0].type_code, pgdb.DATETIME)
+ self.assertNotEqual(cur.description[0].type_code, pgdb.ARRAY)
finally:
con.close()
self.assertEqual(len(rows), len(values))
- rows = [row[1] for row in rows]
+ rows = [row[0] for row in rows]
for inval, outval in zip(values, rows):
if isinstance(inval, datetime):
inval = inval.strftime('%Y-%m-%d %H:%M:%S')
self.assertEqual(inval, outval)
- def test_list_binds_as_array(self):
- values = ([20000, 25000, 25000, 30000],
- [['breakfast', 'consulting'], ['meeting', 'lunch']])
- output = ('{20000,25000,25000,30000}',
- '{{breakfast,consulting},{meeting,lunch}}')
+ def test_roundtrip_with_list(self):
+ values = [(None, None), ([], []), ([None], [[None], ['null']]),
+ ([1, 2, 3], [['a', 'b'], ['c', 'd']]),
+ ([20000, 25000, 25000, 30000],
+ [['breakfast', 'consulting'], ['meeting', 'lunch']]),
+ ([0, 1, -1], [['Hello, World!', '"Hi!"'], ['{x,y}', ' x y ']])]
table = self.table_prefix + 'booze'
con = self._connect()
try:
cur = con.cursor()
- cur.execute("create table %s (i int[], t text[][])" % table)
- cur.execute("insert into %s values (%%s,%%s)" % table, values)
- cur.execute("select * from %s" % table)
- row = cur.fetchone()
+ cur.execute("create table %s"
+ " (n smallint, i int[], t text[][])" % table)
+ params = [(n, v[0], v[1]) for n, v in enumerate(values)]
+ cur.execute("insert into %s values (%%d,%%s,%%s)" % table, params)
+ cur.execute("select i, t from %s order by n" % table)
+ self.assertEqual(cur.description[0].type_code, pgdb.ARRAY)
+ self.assertEqual(cur.description[0].type_code, pgdb.NUMBER)
+ self.assertEqual(cur.description[0].type_code, pgdb.INTEGER)
+ self.assertEqual(cur.description[1].type_code, pgdb.ARRAY)
+ self.assertEqual(cur.description[1].type_code, pgdb.STRING)
+ rows = cur.fetchall()
finally:
con.close()
- self.assertEqual(row, output)
+ self.assertEqual(rows, values)
def test_tuple_binds_as_row(self):
- values = (1, 2.5, 'this is a test')
+ values = [(1, 2.5, 'this is a test')]
output = '(1,2.5,"this is a test")'
con = self._connect()
try:
cur = con.cursor()
- cur.execute("select %s", [values])
+ cur.execute("select %s", values)
outval = cur.fetchone()[0]
finally:
con.close()
@@ -450,7 +460,7 @@
cur.execute(
'create table "%s" (n smallint, b bit varying(7))' % table)
cur.executemany("insert into %s values (%%s,%%s)" % table, params)
- cur.execute("select * from %s order by 1" % table)
+ cur.execute("select * from %s" % table)
rows = cur.fetchall()
finally:
con.close()
@@ -563,12 +573,12 @@
"create table %s (n smallint, booltest bool)" % table)
params = enumerate(values)
cur.executemany("insert into %s values (%%s,%%s)" % table, params)
- cur.execute("select * from %s order by 1" % table)
+ cur.execute("select booltest from %s order by n" % table)
rows = cur.fetchall()
- self.assertEqual(cur.description[1].type_code, pgdb.BOOL)
+ self.assertEqual(cur.description[0].type_code, pgdb.BOOL)
finally:
con.close()
- rows = [row[1] for row in rows]
+ rows = [row[0] for row in rows]
values[3] = values[5] = True
values[4] = values[6] = False
self.assertEqual(rows, values)
@@ -586,7 +596,7 @@
self.skipTest('database does not support json')
params = (pgdb.Json(inval),)
cur.execute("insert into %s values (%%s)" % table, params)
- cur.execute("select * from %s" % table)
+ cur.execute("select jsontest from %s" % table)
outval = cur.fetchone()[0]
self.assertEqual(cur.description[0].type_code, pgdb.JSON)
finally:
@@ -606,7 +616,7 @@
self.skipTest('database does not support jsonb')
params = (pgdb.Json(inval),)
cur.execute("insert into %s values (%%s)" % table, params)
- cur.execute("select * from %s" % table)
+ cur.execute("select jsonbtest from %s" % table)
outval = cur.fetchone()[0]
self.assertEqual(cur.description[0].type_code, pgdb.JSON)
finally:
@@ -754,6 +764,10 @@
self.assertTrue(pgdb.NUMBER >= pgdb.INTEGER)
self.assertTrue(pgdb.TIME <= pgdb.DATETIME)
self.assertTrue(pgdb.DATETIME >= pgdb.DATE)
+ self.assertEqual(pgdb.ARRAY, pgdb.ARRAY)
+ self.assertNotEqual(pgdb.ARRAY, pgdb.STRING)
+ self.assertEqual('_char', pgdb.ARRAY)
+ self.assertNotEqual('char', pgdb.ARRAY)
if __name__ == '__main__':
_______________________________________________
PyGreSQL mailing list
[email protected]
https://mail.vex.net/mailman/listinfo.cgi/pygresql