Changeset 791


Ignore:
Timestamp:
Jan 27, 2016, 6:09:18 PM (4 years ago)
Author:
cito
Message:

Add support for composite types

Added a fast parser for the composite type input/output syntax, which is
similar to the already existing parser for the array input/output syntax.

The pgdb module now makes use of this parser, converting in both directions
between PostgreSQL records (composite types) and Python (named) tuples.

Location:
trunk
Files:
5 edited

Legend:

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

    r782 r791  
    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.
     28- If you pass a Python list as one of the parameters to a DB-API 2 cursor,
     29  it is now automatically bound as a PostgreSQL array. If you pass a Python
     30  tuple, it is bound as a PostgreSQL composite type. Inversely, if a query
     31  returns a PostgreSQL array, it is passed to Python as a list, and if it
     32  returns a PostgreSQL composite type, it is passed to Python as a (named)
     33  tuple. PyGreSQL uses the special input and output syntax for PostgreSQL
     34  arrays and composite types in all of these cases. Anonymous composite
     35  types are returned as ordinary (unnamed) tuples with string values.
    3136- Re-activated the shortcut methods of the DB-API connection since they
    3237  can be handy when doing experiments or writing quick scripts. We keep
  • trunk/pgdb.py

    r788 r791  
    137137
    138138
    139 _cast = {'bool': _cast_bool, 'bytea': unescape_bytea,
     139_cast = {'char': str, 'bpchar': str, 'name': str,
     140    'text': str, 'varchar': str,
     141    'bool': _cast_bool, 'bytea': unescape_bytea,
    140142    'int2': int, 'int4': int, 'serial': int,
    141143    'int8': long, 'json': jsondecode, 'jsonb': jsondecode,
    142144    'oid': long, 'oid8': long,
    143145    'float4': float, 'float8': float,
    144     'numeric': Decimal, 'money': _cast_money}
     146    'numeric': Decimal, 'money': _cast_money,
     147    'record': cast_record}
    145148
    146149
     
    184187                key = '"%s"' % key
    185188            oid = "'%s'::regtype" % self._escape_string(key)
    186         self._src.execute("SELECT oid, typname,"
    187              " typlen, typtype, typcategory, typdelim, typrelid"
    188             " FROM pg_type WHERE oid=%s" % oid)
    189         res = self._src.fetch(1)
     189        try:
     190            self._src.execute("SELECT oid, typname,"
     191                 " typlen, typtype, typcategory, typdelim, typrelid"
     192                " FROM pg_type WHERE oid=%s" % oid)
     193        except ProgrammingError:
     194            res = None
     195        else:
     196            res = self._src.fetch(1)
    190197        if not res:
    191198            raise KeyError('Type %s could not be found' % key)
     
    198205        return res
    199206
     207    def get(self, key, default=None):
     208        """Get the type even if it is not cached."""
     209        try:
     210            return self[key]
     211        except KeyError:
     212            return default
     213
    200214    def columns(self, key):
    201215        """Get the names and types of the columns of composite types."""
    202         typ = self[key]
     216        try:
     217            typ = self[key]
     218        except KeyError:
     219            return None  # this type is not known
    203220        if typ.type != 'c' or not typ.relid:
    204             return []  # this type is not composite
     221            return None  # this type is not composite
    205222        self._src.execute("SELECT attname, atttypid"
    206223            " FROM pg_attribute WHERE attrelid=%s AND attnum>0"
     
    209226            for name, oid in self._src.fetch(-1)]
    210227
    211     @staticmethod
    212     def typecast(typ, value):
     228    def typecast(self, typ, value):
    213229        """Cast value according to database type."""
    214230        if value is None:
     
    216232            return None
    217233        cast = _cast.get(typ)
     234        if cast is str:
     235            return value  # no typecast necessary
    218236        if cast is None:
    219237            if typ.startswith('_'):
     
    221239                cast = _cast.get(typ[1:])
    222240                return cast_array(value, cast)
    223             # no typecast available or necessary
    224             return value
     241            # check whether this is a composite type
     242            cols = self.columns(typ)
     243            if cols:
     244                getcast = self.getcast
     245                cast = [getcast(col.type) for col in cols]
     246                value = cast_record(value, cast)
     247                fields = [col.name for col in cols]
     248                record = namedtuple(typ, fields)
     249                return record(*value)
     250            return value  # no typecast available or necessary
    225251        else:
    226252            return cast(value)
    227253
    228 
    229 _re_array_escape = regex(r'(["\\])')
     254    def getcast(self, key):
     255        """Get a cast function for the given database type."""
     256        if isinstance(key, int):
     257            try:
     258                typ = self[key].name
     259            except KeyError:
     260                return None
     261        else:
     262            typ = key
     263        typecast = self.typecast
     264        return lambda value: typecast(typ, value)
     265
     266
    230267_re_array_quote = regex(r'[{},"\\\s]|^[Nn][Uu][Ll][Ll]$')
     268_re_record_quote = regex(r'[(,"\\]')
     269_re_array_escape = _re_record_escape = regex(r'(["\\])')
    231270
    232271
     
    300339            return "'%s'" % self._quote_array(val)
    301340        if isinstance(val, tuple):
    302             q = self._quote
    303             return 'ROW(%s)' % ','.join(str(q(v)) for v in val)
     341            return "'%s'" % self._quote_record(val)
    304342        try:
    305343            return val.__pg_repr__()
     
    310348    def _quote_array(self, val):
    311349        """Quote value as a literal constant for an array."""
    312         # We could also cast to an array constructor here, but that is more
    313         # verbose and you need to know the base type to build emtpy arrays.
     350        q = self._quote_array_element
     351        return '{%s}' % ','.join(q(v) for v in val)
     352
     353    def _quote_array_element(self, val):
     354        """Quote value using the output syntax for arrays."""
    314355        if isinstance(val, list):
    315             return '{%s}' % ','.join(self._quote_array(v) for v in val)
     356            return self._quote_array(val)
    316357        if val is None:
    317358            return 'null'
     
    320361        if isinstance(val, bool):
    321362            return 't' if val else 'f'
     363        if isinstance(val, tuple):
     364            val = self._quote_record(val)
    322365        if isinstance(val, basestring):
    323366            if not val:
     
    326369                return '"%s"' % _re_array_escape.sub(r'\\\1', val)
    327370            return val
    328         try:
    329             return val.__pg_repr__()
    330         except AttributeError:
    331             raise InterfaceError(
    332                 'do not know how to handle type %s' % type(val))
     371        raise InterfaceError(
     372            'do not know how to handle base type %s' % type(val))
     373
     374    def _quote_record(self, val):
     375        """Quote value as a literal constant for a record."""
     376        q = self._quote_record_element
     377        return '(%s)' % ','.join(q(v) for v in val)
     378
     379    def _quote_record_element(self, val):
     380        """Quote value using the output syntax for records."""
     381        if val is None:
     382            return ''
     383        if isinstance(val, (int, long, float)):
     384            return str(val)
     385        if isinstance(val, bool):
     386            return 't' if val else 'f'
     387        if isinstance(val, list):
     388            val = self._quote_array(val)
     389        if isinstance(val, basestring):
     390            if not val:
     391                return '""'
     392            if _re_record_quote.search(val):
     393                return '"%s"' % _re_record_escape.sub(r'\\\1', val)
     394            return val
     395        raise InterfaceError(
     396            'do not know how to handle component type %s' % type(val))
    333397
    334398    def _quoteparams(self, string, parameters):
  • trunk/pgmodule.c

    r785 r791  
    556556                        buf[j] = '\0'; s = buf;
    557557                        /* FALLTHROUGH */ /* no break here */
    558        
     558
    559559                case PYGRES_DECIMAL:
    560560                        if (decimal)
     
    598598        && (s[3] == 'l' || s[3] == 'L'))
    599599
    600 /* Cast string s with size and encoding to a Python list.
    601    Use cast function if specified or basetype to cast elements.
     600/* Cast string s with size and encoding to a Python list,
     601   using the input and output syntax for arrays.
     602   Use internal type or cast function to cast elements.
    602603   The parameter delim specifies the delimiter for the elements,
    603604   since some types do not use the default delimiter of a comma. */
     
    615616                if (!type) type = PYGRES_TEXT;
    616617        }
    617         if (!delim) delim = ',';
     618        if (!delim)
     619                delim = ',';
     620        else if (delim == '{' || delim =='}' || delim=='\\')
     621        {
     622                PyErr_SetString(PyExc_ValueError, "Invalid array delimiter");
     623                return NULL;
     624        }
    618625
    619626        /* strip blanks at the beginning */
     
    654661        {
    655662                PyErr_SetString(PyExc_ValueError,
    656                         "Array must start with an opening brace");
     663                        "Array must start with a left brace");
    657664                return NULL;
    658665        }
     
    690697                                        PyErr_SetString(PyExc_ValueError,
    691698                                                "Subarray expected but not found");
    692                                         return NULL;
     699                                        Py_DECREF(result); return NULL;
    693700                                }
    694701                        }
     
    696703                        subresult = result;
    697704                        result = stack[--level];
    698                         if (PyList_Append(result, subresult)) return NULL;
     705                        if (PyList_Append(result, subresult))
     706                        {
     707                                Py_DECREF(result); return NULL;
     708                        }
    699709                }
    700710                else if (level == depth) /* we expect elements at this level */
     
    709719                                PyErr_SetString(PyExc_ValueError,
    710720                                        "Subarray found where not expected");
    711                                 return NULL;
     721                                Py_DECREF(result); return NULL;
    712722                        }
    713723                        if (*s == '"') /* quoted element */
     
    753763                                if (escaped)
    754764                                {
    755                                         char   *r;
    756                                         int             i;
     765                                        char       *r;
     766                                        Py_ssize_t      i;
    757767
    758768                                        /* create unescaped string */
    759769                                        t = estr;
    760770                                        estr = (char *) PyMem_Malloc(esize);
    761                                         if (!estr) return PyErr_NoMemory();
     771                                        if (!estr)
     772                                        {
     773                                                Py_DECREF(result); return PyErr_NoMemory();
     774                                        }
    762775                                        for (i = 0, r = estr; i < esize; ++i)
    763776                                        {
     
    789802                                }
    790803                                if (escaped) PyMem_Free(estr);
    791                                 if (!element) return NULL;
     804                                if (!element)
     805                                {
     806                                        Py_DECREF(result); return NULL;
     807                                }
    792808                        }
    793809                        else
    794810                        {
    795                                 Py_INCREF(Py_None);
    796                                 element = Py_None;
     811                                Py_INCREF(Py_None); element = Py_None;
    797812                        }
    798                         if (PyList_Append(result, element)) return NULL;
     813                        if (PyList_Append(result, element))
     814                        {
     815                                Py_DECREF(element); Py_DECREF(result); return NULL;
     816                        }
     817                        Py_DECREF(element);
    799818                        if (*s == delim)
    800819                        {
     
    809828                        {
    810829                                PyErr_SetString(PyExc_ValueError,
    811                                         "Subarray must start with an opening brace");
    812                                 return NULL;
     830                                        "Subarray must start with a left brace");
     831                                Py_DECREF(result); return NULL;
    813832                        }
    814833                        do ++s; while (s != end && *s == ' ');
     
    822841                PyErr_SetString(PyExc_ValueError,
    823842                        "Unexpected end of array");
    824                 return NULL;
     843                Py_DECREF(result); return NULL;
    825844        }
    826845        do ++s; while (s != end && *s == ' ');
     
    829848                PyErr_SetString(PyExc_ValueError,
    830849                        "Unexpected characters after end of array");
    831                 return NULL;
     850                Py_DECREF(result); return NULL;
    832851        }
    833852        return result;
     853}
     854
     855/* Cast string s with size and encoding to a Python tuple.
     856   using the input and output syntax for composite types.
     857   Use array of internal types or cast function or sequence of cast
     858   functions to cast elements. The parameter len is the record size.
     859   The parameter delim can specify a delimiter for the elements,
     860   although composite types always use a comma as delimiter. */
     861
     862static PyObject *
     863cast_record(char *s, Py_ssize_t size, int encoding,
     864         int *type, PyObject *cast, Py_ssize_t len, char delim)
     865{
     866        PyObject   *result, *ret;
     867        char       *end = s + size, *t;
     868        Py_ssize_t      i;
     869
     870        if (!delim)
     871                delim = ',';
     872        else if (delim == '(' || delim ==')' || delim=='\\')
     873        {
     874                PyErr_SetString(PyExc_ValueError, "Invalid record delimiter");
     875                return NULL;
     876        }
     877
     878        /* strip blanks at the beginning */
     879        while (s != end && *s == ' ') ++s;
     880        if (s == end || *s != '(')
     881        {
     882                PyErr_SetString(PyExc_ValueError,
     883                        "Record must start with a left parenthesis");
     884                return NULL;
     885        }
     886        result = PyList_New(0);
     887        if (!result) return NULL;
     888        i = 0;
     889        /* everything is set up, start parsing the record */
     890        while (++s != end)
     891        {
     892                PyObject   *element;
     893
     894                if (*s == ')' || *s == delim)
     895                {
     896                        Py_INCREF(Py_None); element = Py_None;
     897                }
     898                else
     899                {
     900                        char       *estr;
     901                        Py_ssize_t      esize;
     902                        int quoted = 0, escaped =0;
     903
     904                        estr = s;
     905                        quoted = *s == '"';
     906                        if (quoted) ++s;
     907                        esize = 0;
     908                        while (s != end)
     909                        {
     910                                if (!quoted && (*s == ')' || *s == delim))
     911                                        break;
     912                                if (*s == '"')
     913                                {
     914                                        ++s; if (s == end) break;
     915                                        if (!(quoted && *s == '"'))
     916                                        {
     917                                                quoted = !quoted; continue;
     918                                        }
     919                                }
     920                                if (*s == '\\')
     921                                {
     922                                        ++s; if (s == end) break;
     923                                }
     924                                ++s, ++esize;
     925                        }
     926                        if (s == end) break; /* error */
     927                        if (estr + esize != s)
     928                        {
     929                                char       *r;
     930
     931                                escaped = 1;
     932                                /* create unescaped string */
     933                                t = estr;
     934                                estr = (char *) PyMem_Malloc(esize);
     935                                if (!estr)
     936                                {
     937                                        Py_DECREF(result); return PyErr_NoMemory();
     938                                }
     939                                quoted = 0;
     940                                r = estr;
     941                                while (t != s)
     942                                {
     943                                        if (*t == '"')
     944                                        {
     945                                                ++t;
     946                                                if (!(quoted && *t == '"'))
     947                                                {
     948                                                        quoted = !quoted; continue;
     949                                                }
     950                                        }
     951                                        if (*t == '\\') ++t;
     952                                        *r++ = *t++;
     953                                }
     954                        }
     955                        if (type) /* internal casting of element type */
     956                        {
     957                                int etype = type[i];
     958
     959                                if (etype & PYGRES_ARRAY)
     960                                        element = cast_array(
     961                                                estr, esize, encoding, etype, NULL, 0);
     962                                else if (etype & PYGRES_TEXT)
     963                                        element = cast_sized_text(estr, esize, encoding, etype);
     964                                else
     965                                        element = cast_sized_simple(estr, esize, etype);
     966                        }
     967                        else /* external casting of base type */
     968                        {
     969#if IS_PY3
     970                                element = encoding == pg_encoding_ascii ? NULL :
     971                                        get_decoded_string(estr, esize, encoding);
     972                                if (!element) /* no decoding necessary or possible */
     973#endif
     974                                element = PyBytes_FromStringAndSize(estr, esize);
     975                                if (element && cast)
     976                                {
     977                                        if (len)
     978                                        {
     979                                                PyObject *ecast = PySequence_GetItem(cast, i);
     980
     981                                                if (ecast)
     982                                                {
     983                                                        if (ecast != Py_None)
     984                                                                element = PyObject_CallFunctionObjArgs(
     985                                                                        ecast, element, NULL);
     986                                                }
     987                                                else
     988                                                {
     989                                                        Py_DECREF(element); element = NULL;
     990                                                }
     991                                        }
     992                                        else
     993                                                element = PyObject_CallFunctionObjArgs(
     994                                                        cast, element, NULL);
     995                                }
     996                        }
     997                        if (escaped) PyMem_Free(estr);
     998                        if (!element)
     999                        {
     1000                                Py_DECREF(result); return NULL;
     1001                        }
     1002                }
     1003                if (PyList_Append(result, element))
     1004                {
     1005                        Py_DECREF(element); Py_DECREF(result); return NULL;
     1006                }
     1007                Py_DECREF(element);
     1008                if (len) ++i;
     1009                if (*s != delim) break; /* no next record */
     1010                if (len && i >= len)
     1011                {
     1012                        PyErr_SetString(PyExc_ValueError, "Too many columns");
     1013                        Py_DECREF(result); return NULL;
     1014                }
     1015        }
     1016        if (s == end || *s != ')')
     1017        {
     1018                PyErr_SetString(PyExc_ValueError, "Unexpected end of record");
     1019                Py_DECREF(result); return NULL;
     1020        }
     1021        do ++s; while (s != end && *s == ' ');
     1022        if (s != end)
     1023        {
     1024                PyErr_SetString(PyExc_ValueError,
     1025                        "Unexpected characters after end of record");
     1026                Py_DECREF(result); return NULL;
     1027        }
     1028        if (len && i < len)
     1029        {
     1030                PyErr_SetString(PyExc_ValueError, "Too few columns");
     1031                Py_DECREF(result); return NULL;
     1032        }
     1033
     1034        ret = PyList_AsTuple(result);
     1035        Py_DECREF(result);
     1036        return ret;
    8341037}
    8351038
     
    51785381{
    51795382        static const char *kwlist[] = {"string", "cast", "delim", NULL};
    5180         PyObject   *string_obj, *cast_obj = NULL;
    5181         char       *string;
     5383        PyObject   *string_obj, *cast_obj = NULL, *ret;
     5384        char       *string, delim = ',';
    51825385        Py_ssize_t      size;
    51835386        int                     encoding;
    5184         char            delim = ',';
    51855387
    51865388        if (!PyArg_ParseTupleAndKeywords(args, dict, "O|Oc",
     
    52085410
    52095411        if (!cast_obj || cast_obj == Py_None)
    5210                 cast_obj = NULL;
     5412        {
     5413                if (cast_obj)
     5414                {
     5415                        Py_DECREF(cast_obj); cast_obj = NULL;
     5416                }
     5417        }
    52115418        else if (!PyCallable_Check(cast_obj))
    52125419        {
     
    52155422        }
    52165423
    5217         return cast_array(string, size, encoding, 0, cast_obj, delim);
     5424        ret = cast_array(string, size, encoding, 0, cast_obj, delim);
     5425
     5426        Py_XDECREF(string_obj);
     5427
     5428        return ret;
     5429}
     5430
     5431/* cast a string with a text representation of a record to a tuple */
     5432static char pgCastRecord__doc__[] =
     5433"cast_record(string, cast=None, delim=',') -- cast a string as a record";
     5434
     5435PyObject *
     5436pgCastRecord(PyObject *self, PyObject *args, PyObject *dict)
     5437{
     5438        static const char *kwlist[] = {"string", "cast", "delim", NULL};
     5439        PyObject   *string_obj, *cast_obj = NULL, *ret;
     5440        char       *string, delim = ',';
     5441        Py_ssize_t      size, len;
     5442        int                     encoding;
     5443
     5444        if (!PyArg_ParseTupleAndKeywords(args, dict, "O|Oc",
     5445                        (char **) kwlist, &string_obj, &cast_obj, &delim))
     5446                return NULL;
     5447
     5448        if (PyBytes_Check(string_obj))
     5449        {
     5450                encoding = pg_encoding_ascii;
     5451                PyBytes_AsStringAndSize(string_obj, &string, &size);
     5452                string_obj = NULL;
     5453        }
     5454        else if (PyUnicode_Check(string_obj))
     5455        {
     5456                encoding = pg_encoding_utf8;
     5457                string_obj = get_encoded_string(string_obj, encoding);
     5458                if (!string_obj) return NULL; /* pass the UnicodeEncodeError */
     5459                PyBytes_AsStringAndSize(string_obj, &string, &size);
     5460        }
     5461        else
     5462        {
     5463                PyErr_SetString(PyExc_TypeError, "cast_record() expects a string");
     5464                return NULL;
     5465        }
     5466
     5467        if (!cast_obj || PyCallable_Check(cast_obj))
     5468        {
     5469                len = 0;
     5470        }
     5471        else if (cast_obj == Py_None)
     5472        {
     5473                Py_DECREF(cast_obj); cast_obj = NULL; len = 0;
     5474        }
     5475        else if (PyTuple_Check(cast_obj) || PyList_Check(cast_obj))
     5476        {
     5477                len = PySequence_Size(cast_obj);
     5478                if (!len)
     5479                {
     5480                        Py_DECREF(cast_obj); cast_obj = NULL;
     5481                }
     5482        }
     5483        else
     5484        {
     5485                PyErr_SetString(PyExc_TypeError,
     5486                        "The cast argument must be callable or a tuple or list of such");
     5487                return NULL;
     5488        }
     5489
     5490        ret = cast_record(string, size, encoding, 0, cast_obj, len, delim);
     5491
     5492        Py_XDECREF(string_obj);
     5493
     5494        return ret;
    52185495}
    52195496
     
    52505527        {"cast_array", (PyCFunction) pgCastArray, METH_VARARGS|METH_KEYWORDS,
    52515528                        pgCastArray__doc__},
     5529        {"cast_record", (PyCFunction) pgCastRecord, METH_VARARGS|METH_KEYWORDS,
     5530                        pgCastRecord__doc__},
    52525531
    52535532#ifdef DEFAULT_VARS
  • trunk/tests/test_classic_functions.py

    r785 r791  
    117117    """Test the array parser."""
    118118
    119     array_expressions = [
     119    test_strings = [
    120120        ('', str, ValueError),
    121121        ('{}', None, []),
     
    156156        (r'{\a\b\c}', str, ['abc']),
    157157        (r'{"\a\b\c"}', str, ['abc']),
     158        (r'{"a"b"}', str, ValueError),
     159        (r'{"a""b"}', str, ValueError),
     160        (r'{"a\"b"}', str, ['a"b']),
    158161        ('{"{}"}', str, ['{}']),
    159162        (r'{\{\}}', str, ['{}']),
     
    218221        self.assertRaises(TypeError, f, '{}', None, 1)
    219222        self.assertRaises(TypeError, f, '{}', None, '')
     223        self.assertRaises(ValueError, f, '{}', None, '\\')
     224        self.assertRaises(ValueError, f, '{}', None, '{')
     225        self.assertRaises(ValueError, f, '{}', None, '}')
    220226        self.assertRaises(TypeError, f, '{}', None, ',;')
    221227        self.assertEqual(f('{}'), [])
     
    300306    def testParserWithData(self):
    301307        f = pg.cast_array
    302         for expression, cast, expected in self.array_expressions:
     308        for string, cast, expected in self.test_strings:
    303309            if expected is ValueError:
    304                 self.assertRaises(ValueError, f, expression, cast)
    305             else:
    306                 self.assertEqual(f(expression, cast), expected)
     310                self.assertRaises(ValueError, f, string, cast)
     311            else:
     312                self.assertEqual(f(string, cast), expected)
    307313
    308314    def testParserWithoutCast(self):
    309315        f = pg.cast_array
    310316
    311         for expression, cast, expected in self.array_expressions:
     317        for string, cast, expected in self.test_strings:
    312318            if cast is not str:
    313319                continue
    314320            if expected is ValueError:
    315                 self.assertRaises(ValueError, f, expression)
    316             else:
    317                 self.assertEqual(f(expression), expected)
     321                self.assertRaises(ValueError, f, string)
     322            else:
     323                self.assertEqual(f(string), expected)
    318324
    319325    def testParserWithDifferentDelimiter(self):
     
    328334                return value
    329335
    330         for expression, cast, expected in self.array_expressions:
    331             expression = replace_comma(expression)
     336        for string, cast, expected in self.test_strings:
     337            string = replace_comma(string)
    332338            if expected is ValueError:
    333                 self.assertRaises(ValueError, f, expression, cast)
     339                self.assertRaises(ValueError, f, string, cast)
    334340            else:
    335341                expected = replace_comma(expected)
    336                 self.assertEqual(f(expression, cast, b';'), expected)
     342                self.assertEqual(f(string, cast, b';'), expected)
     343
     344
     345class TestParseRecord(unittest.TestCase):
     346    """Test the record parser."""
     347
     348    test_strings = [
     349        ('', None, ValueError),
     350        ('', str, ValueError),
     351        ('(', None, ValueError),
     352        ('(', str, ValueError),
     353        ('()', None, (None,)),
     354        ('()', str, (None,)),
     355        ('()', int, (None,)),
     356        ('(,)', str, (None, None)),
     357        ('( , )', str, (' ', ' ')),
     358        ('(")', None, ValueError),
     359        ('("")', None, ('',)),
     360        ('("")', str, ('',)),
     361        ('("")', int, ValueError),
     362        ('("" )', None, (' ',)),
     363        ('("" )', str, (' ',)),
     364        ('("" )', int, ValueError),
     365        ('    ()    ', None, (None,)),
     366        ('   (   )   ', None, ('   ',)),
     367        ('(', str, ValueError),
     368        ('(()', str, ('(',)),
     369        ('(())', str, ValueError),
     370        ('()(', str, ValueError),
     371        ('()()', str, ValueError),
     372        ('[]', str, ValueError),
     373        ('{}', str, ValueError),
     374        ('([])', str, ('[]',)),
     375        ('(hello)', int, ValueError),
     376        ('(42)', int, (42,)),
     377        ('( 42 )', int, (42,)),
     378        ('(  42)', int, (42,)),
     379        ('(42)', str, ('42',)),
     380        ('( 42 )', str, (' 42 ',)),
     381        ('(  42)', str, ('  42',)),
     382        ('(42', int, ValueError),
     383        ('( 42 ', int, ValueError),
     384        ('(hello)', str, ('hello',)),
     385        ('( hello )', str, (' hello ',)),
     386        ('(hello))', str, ValueError),
     387        ('   (hello)   ', str, ('hello',)),
     388        ('   (hello)   )', str, ValueError),
     389        ('(hello)?', str, ValueError),
     390        ('(null)', str, ('null',)),
     391        ('(null)', int, ValueError),
     392        (' ( NULL ) ', str, (' NULL ',)),
     393        ('   (   NULL   )   ', str, ('   NULL   ',)),
     394        (' ( null null ) ', str, (' null null ',)),
     395        (' ("null") ', str, ('null',)),
     396        (' ("NULL") ', str, ('NULL',)),
     397        ('(Hi!)', str, ('Hi!',)),
     398        ('("Hi!")', str, ('Hi!',)),
     399        ("('Hi!')", str, ("'Hi!'",)),
     400        ('(" Hi! ")', str, (' Hi! ',)),
     401        ('("Hi!" )', str, ('Hi! ',)),
     402        ('( "Hi!")', str, (' Hi!',)),
     403        ('( "Hi!" )', str, (' Hi! ',)),
     404        ('( ""Hi!"" )', str, (' Hi! ',)),
     405        ('( """Hi!""" )', str, (' "Hi!" ',)),
     406        ('(a")', str, ValueError),
     407        ('("b)', str, ValueError),
     408        ('("a" "b)', str, ValueError),
     409        ('("a" "b")', str, ('a b',)),
     410        ('( "a" "b" "c" )', str, (' a b c ',)),
     411        ('(  "a"  "b"  "c"  )', str, ('  a  b  c  ',)),
     412        ('(  "a,b"  "c,d"  )', str, ('  a,b  c,d  ',)),
     413        ('( "(a,b,c)" d, e, "f,g")', str, (' (a,b,c) d', ' e', ' f,g')),
     414        ('(a",b,c",d,"e,f")', str, ('a,b,c', 'd', 'e,f')),
     415        ('( """a,b""", ""c,d"", "e,f", "g", ""h"", """i""")', str,
     416            (' "a,b"', ' c', 'd', ' e,f', ' g', ' h', ' "i"')),
     417        ('(a",b)",c"),(d,e)",f,g)', str, ('a,b)', 'c),(d,e)', 'f', 'g')),
     418        ('(a"b)', str, ValueError),
     419        (r'(a\"b)', str, ('a"b',)),
     420        ('(a""b)', str, ('ab',)),
     421        ('("a""b")', str, ('a"b',)),
     422        (r'(a\,b)', str, ('a,b',)),
     423        (r'(a\bc)', str, ('abc',)),
     424        (r'("a\bc")', str, ('abc',)),
     425        (r'(\a\b\c)', str, ('abc',)),
     426        (r'("\a\b\c")', str, ('abc',)),
     427        ('("()")', str, ('()',)),
     428        (r'(\,)', str, (',',)),
     429        (r'(\(\))', str, ('()',)),
     430        (r'(\)\()', str, (')(',)),
     431        ('("(a,b,c)")', str, ('(a,b,c)',)),
     432        ("('abc')", str, ("'abc'",)),
     433        ('("abc")', str, ('abc',)),
     434        (r'(\"abc\")', str, ('"abc"',)),
     435        (r"(\'abc\')", str, ("'abc'",)),
     436        ('(Hello World!)', str, ('Hello World!',)),
     437        ('(Hello, World!)', str, ('Hello', ' World!',)),
     438        ('(Hello,\ World!)', str, ('Hello', ' World!',)),
     439        ('(Hello\, World!)', str, ('Hello, World!',)),
     440        ('("Hello World!")', str, ('Hello World!',)),
     441        ("(this,shouldn't,be,null)", str, ('this', "shouldn't", 'be', 'null')),
     442        ('(null,should,be,)', str, ('null', 'should', 'be', None)),
     443        ('(abcABC0123!?+-*/=&%$\\\\\'\\"{[]}"""":;\\,,)', str,
     444            ('abcABC0123!?+-*/=&%$\\\'"{[]}":;,', None)),
     445        ('(3, 2, 1,)', int, (3, 2, 1, None)),
     446        ('(3, 2, 1, )', int, ValueError),
     447        ('(, 1, 2, 3)', int, (None, 1, 2, 3)),
     448        ('( , 1, 2, 3)', int, ValueError),
     449        ('(,1,,2,,3,)', int, (None, 1, None, 2, None, 3, None)),
     450        ('(3,17,51)', int, (3, 17, 51)),
     451        (' ( 3 , 17 , 51 ) ', int, (3, 17, 51)),
     452        ('(3,17,51)', str, ('3', '17', '51')),
     453        (' ( 3 , 17 , 51 ) ', str, (' 3 ', ' 17 ', ' 51 ')),
     454        ('(1,"2",abc,"def")', str, ('1', '2', 'abc', 'def')),
     455        ('(())', str, ValueError),
     456        ('()))', str, ValueError),
     457        ('()()', str, ValueError),
     458        ('((()', str, ('((',)),
     459        ('(())', int, ValueError),
     460        ('((),())', str, ValueError),
     461        ('("()","()")', str, ('()', '()')),
     462        ('( " () , () , () " )', str, ('  () , () , ()  ',)),
     463        ('(20000, 25000, 25000, 25000)', int, (20000, 25000, 25000, 25000)),
     464        ('("breakfast","consulting","meeting","lunch")', str,
     465            ('breakfast', 'consulting', 'meeting', 'lunch')),
     466        ('("breakfast","consulting","meeting","lunch")',
     467            (str, str, str), ValueError),
     468        ('("breakfast","consulting","meeting","lunch")', (str, str, str, str),
     469            ('breakfast', 'consulting', 'meeting', 'lunch')),
     470        ('("breakfast","consulting","meeting","lunch")',
     471            (str, str, str, str, str), ValueError),
     472        ('("fuzzy dice",42,1.9375)', None, ('fuzzy dice', '42', '1.9375')),
     473        ('("fuzzy dice",42,1.9375)', str, ('fuzzy dice', '42', '1.9375')),
     474        ('("fuzzy dice",42,1.9375)', int, ValueError),
     475        ('("fuzzy dice",42,1.9375)', (str, int, float),
     476            ('fuzzy dice', 42, 1.9375)),
     477        ('("fuzzy dice",42,1.9375)', (str, int), ValueError),
     478        ('("fuzzy dice",42,1.9375)', (str, int, float, str), ValueError),
     479        ('("fuzzy dice",42,)', (str, int, float), ('fuzzy dice', 42, None)),
     480        ('("fuzzy dice",42,)', (str, int), ValueError),
     481        ('("",42,)', (str, int, float), ('', 42, None)),
     482        ('("fuzzy dice","",1.9375)', (str, int, float), ValueError),
     483        ('(fuzzy dice,"42","1.9375")', (str, int, float),
     484            ('fuzzy dice', 42, 1.9375))]
     485
     486    def testParserParams(self):
     487        f = pg.cast_record
     488        self.assertRaises(TypeError, f)
     489        self.assertRaises(TypeError, f, None)
     490        self.assertRaises(TypeError, f, '()', 1)
     491        self.assertRaises(TypeError, f, '()', ',',)
     492        self.assertRaises(TypeError, f, '()', None, None)
     493        self.assertRaises(TypeError, f, '()', None, 1)
     494        self.assertRaises(TypeError, f, '()', None, '')
     495        self.assertRaises(ValueError, f, '()', None, '\\')
     496        self.assertRaises(ValueError, f, '()', None, '(')
     497        self.assertRaises(ValueError, f, '()', None, ')')
     498        self.assertRaises(TypeError, f, '{}', None, ',;')
     499        self.assertEqual(f('()'), (None,))
     500        self.assertEqual(f('()', None), (None,))
     501        self.assertEqual(f('()', None, b';'), (None,))
     502        self.assertEqual(f('()', str), (None,))
     503        self.assertEqual(f('()', str, b';'), (None,))
     504
     505    def testParserSimple(self):
     506        r = pg.cast_record('(a,b,c)')
     507        self.assertIsInstance(r, tuple)
     508        self.assertEqual(len(r), 3)
     509        self.assertEqual(r, ('a', 'b', 'c'))
     510
     511    def testParserNested(self):
     512        f = pg.cast_record
     513        self.assertRaises(ValueError, f, '((a,b,c))')
     514        self.assertRaises(ValueError, f, '((a,b),(c,d))')
     515        self.assertRaises(ValueError, f, '((a),(b),(c))')
     516        self.assertRaises(ValueError, f, '(((((((abc)))))))')
     517
     518    def testParserManyElements(self):
     519        f = pg.cast_record
     520        for n in 3, 5, 9, 12, 16, 32, 64, 256:
     521            r = '(%s)' % ','.join(map(str, range(n)))
     522            r = f(r, int)
     523            self.assertEqual(r, tuple(range(n)))
     524
     525    def testParserCastUniform(self):
     526        f = pg.cast_record
     527        self.assertEqual(f('(1)'), ('1',))
     528        self.assertEqual(f('(1)', None), ('1',))
     529        self.assertEqual(f('(1)', int), (1,))
     530        self.assertEqual(f('(1)', str), ('1',))
     531        self.assertEqual(f('(a)'), ('a',))
     532        self.assertEqual(f('(a)', None), ('a',))
     533        self.assertRaises(ValueError, f, '(a)', int)
     534        self.assertEqual(f('(a)', str), ('a',))
     535        cast = lambda s: '%s is ok' % s
     536        self.assertEqual(f('(a)', cast), ('a is ok',))
     537
     538    def testParserCastNonUniform(self):
     539        f = pg.cast_record
     540        self.assertEqual(f('(1)', []), ('1',))
     541        self.assertEqual(f('(1)', [None]), ('1',))
     542        self.assertEqual(f('(1)', [str]), ('1',))
     543        self.assertEqual(f('(1)', [int]), (1,))
     544        self.assertRaises(ValueError, f, '(1)', [None, None])
     545        self.assertRaises(ValueError, f, '(1)', [str, str])
     546        self.assertRaises(ValueError, f, '(1)', [int, int])
     547        self.assertEqual(f('(a)', [None]), ('a',))
     548        self.assertEqual(f('(a)', [str]), ('a',))
     549        self.assertRaises(ValueError, f, '(a)', [int])
     550        self.assertEqual(f('(1,a)', [int, str]), (1, 'a'))
     551        self.assertRaises(ValueError, f, '(1,a)', [str, int])
     552        self.assertEqual(f('(a,1)', [str, int]), ('a', 1))
     553        self.assertRaises(ValueError, f, '(a,1)', [int, str])
     554        self.assertEqual(f('(1,a,2,b,3,c)',
     555            [int, str, int, str, int, str]), (1, 'a', 2, 'b', 3, 'c'))
     556        self.assertEqual(f('(1,a,2,b,3,c)',
     557            (int, str, int, str, int, str)), (1, 'a', 2, 'b', 3, 'c'))
     558        cast1 = lambda s: '%s is ok' % s
     559        self.assertEqual(f('(a)', [cast1]), ('a is ok',))
     560        cast2 = lambda s: 'and %s is ok, too' % s
     561        self.assertEqual(f('(a,b)', [cast1, cast2]),
     562            ('a is ok', 'and b is ok, too'))
     563        self.assertRaises(ValueError, f, '(a)', [cast1, cast2])
     564        self.assertRaises(ValueError, f, '(a,b,c)', [cast1, cast2])
     565        self.assertEqual(f('(1,2,3,4,5,6)',
     566            [int, float, str, None, cast1, cast2]),
     567            (1, 2.0, '3', '4', '5 is ok', 'and 6 is ok, too'))
     568
     569    def testParserDelim(self):
     570        f = pg.cast_record
     571        self.assertEqual(f('(1,2)'), ('1', '2'))
     572        self.assertEqual(f('(1,2)', delim=b','), ('1', '2'))
     573        self.assertEqual(f('(1;2)'), ('1;2',))
     574        self.assertEqual(f('(1;2)', delim=b';'), ('1', '2'))
     575        self.assertEqual(f('(1,2)', delim=b';'), ('1,2',))
     576
     577    def testParserWithData(self):
     578        f = pg.cast_record
     579        for string, cast, expected in self.test_strings:
     580            if expected is ValueError:
     581                self.assertRaises(ValueError, f, string, cast)
     582            else:
     583                self.assertEqual(f(string, cast), expected)
     584
     585    def testParserWithoutCast(self):
     586        f = pg.cast_record
     587
     588        for string, cast, expected in self.test_strings:
     589            if cast is not str:
     590                continue
     591            if expected is ValueError:
     592                self.assertRaises(ValueError, f, string)
     593            else:
     594                self.assertEqual(f(string), expected)
     595
     596    def testParserWithDifferentDelimiter(self):
     597        f = pg.cast_record
     598
     599        def replace_comma(value):
     600            if isinstance(value, str):
     601                return value.replace(';', '@').replace(
     602                    ',', ';').replace('@', ',')
     603            elif isinstance(value, tuple):
     604                return tuple(replace_comma(v) for v in value)
     605            else:
     606                return value
     607
     608        for string, cast, expected in self.test_strings:
     609            string = replace_comma(string)
     610            if expected is ValueError:
     611                self.assertRaises(ValueError, f, string, cast)
     612            else:
     613                expected = replace_comma(expected)
     614                self.assertEqual(f(string, cast, b';'), expected)
    337615
    338616
  • trunk/tests/test_dbapi20.py

    r788 r791  
    294294        con = self._connect()
    295295        cur = con.cursor()
    296         type_cache = cur.type_cache
     296        type_cache = con.type_cache
     297        self.assertNotIn('numeric', type_cache)
    297298        type_info = type_cache['numeric']
     299        self.assertIn('numeric', type_cache)
    298300        self.assertEqual(type_info.oid, 1700)
    299301        self.assertEqual(type_info.name, 'numeric')
     
    301303        self.assertEqual(type_info.category, 'N')  # numeric
    302304        self.assertEqual(type_info.delim, ',')
    303         self.assertIs(cur.type_cache[1700], type_info)
     305        self.assertIs(con.type_cache[1700], type_info)
     306        self.assertNotIn('pg_type', type_cache)
    304307        type_info = type_cache['pg_type']
     308        self.assertIn('numeric', type_cache)
    305309        self.assertEqual(type_info.type, 'c')  # composite
    306310        self.assertEqual(type_info.category, 'C')  # composite
     
    316320        self.assertEqual(typlen.type, 'b')  # base
    317321        self.assertEqual(typlen.category, 'N')  # numeric
     322        cur.close()
     323        cur = con.cursor()
     324        type_cache = con.type_cache
     325        self.assertIn('numeric', type_cache)
     326        cur.close()
     327        con.close()
     328        con = self._connect()
     329        cur = con.cursor()
     330        type_cache = con.type_cache
     331        self.assertNotIn('pg_type', type_cache)
     332        self.assertEqual(type_cache.get('pg_type'), type_info)
     333        self.assertIn('pg_type', type_cache)
     334        self.assertIsNone(type_cache.get(
     335            self.table_prefix + '_surely_does_not_exist'))
     336        cur.close()
     337        con.close()
    318338
    319339    def test_cursor_iteration(self):
     
    451471            self.assertEqual(inval, outval)
    452472
    453     def test_roundtrip_with_list(self):
     473    def test_insert_array(self):
    454474        values = [(None, None), ([], []), ([None], [[None], ['null']]),
    455475            ([1, 2, 3], [['a', 'b'], ['c', 'd']]),
     
    464484                " (n smallint, i int[], t text[][])" % table)
    465485            params = [(n, v[0], v[1]) for n, v in enumerate(values)]
    466             cur.execute("insert into %s values (%%d,%%s,%%s)" % table, params)
     486            cur.executemany(
     487                "insert into %s values (%%d,%%s,%%s)" % table, params)
    467488            cur.execute("select i, t from %s order by n" % table)
    468489            self.assertEqual(cur.description[0].type_code, pgdb.ARRAY)
     
    476497        self.assertEqual(rows, values)
    477498
    478     def test_tuple_binds_as_row(self):
    479         values = [(1, 2.5, 'this is a test')]
    480         output = '(1,2.5,"this is a test")'
    481         con = self._connect()
    482         try:
    483             cur = con.cursor()
    484             cur.execute("select %s", values)
    485             outval = cur.fetchone()[0]
    486         finally:
    487             con.close()
    488         self.assertEqual(outval, output)
     499    def test_select_array(self):
     500        values = ([1, 2, 3, None], ['a', 'b', 'c', None])
     501        con = self._connect()
     502        try:
     503            cur = con.cursor()
     504            cur.execute("select %s::int[], %s::text[]", values)
     505            row = cur.fetchone()
     506        finally:
     507            con.close()
     508        self.assertEqual(row, values)
     509
     510    def test_insert_record(self):
     511        values = [('John', 61), ('Jane', 63),
     512                  ('Fred', None), ('Wilma', None),
     513                  (None, 42), (None, None)]
     514        table = self.table_prefix + 'booze'
     515        record = self.table_prefix + 'munch'
     516        con = self._connect()
     517        try:
     518            cur = con.cursor()
     519            cur.execute("create type %s as (name varchar, age int)" % record)
     520            cur.execute("create table %s (n smallint, r %s)" % (table, record))
     521            params = enumerate(values)
     522            cur.executemany("insert into %s values (%%d,%%s)" % table, params)
     523            cur.execute("select r from %s order by n" % table)
     524            type_code = cur.description[0].type_code
     525            self.assertEqual(type_code, record)
     526            columns = con.type_cache.columns(type_code)
     527            self.assertEqual(columns[0].name, 'name')
     528            self.assertEqual(columns[1].name, 'age')
     529            self.assertEqual(con.type_cache[columns[0].type].name, 'varchar')
     530            self.assertEqual(con.type_cache[columns[1].type].name, 'int4')
     531            rows = cur.fetchall()
     532        finally:
     533            cur.execute('drop table %s' % table)
     534            cur.execute('drop type %s' % record)
     535            con.close()
     536        self.assertEqual(len(rows), len(values))
     537        rows = [row[0] for row in rows]
     538        self.assertEqual(rows, values)
     539        self.assertEqual(rows[0].name, 'John')
     540        self.assertEqual(rows[0].age, 61)
     541
     542    def test_select_record(self):
     543        values = (1, 25000, 2.5, 'hello', 'Hello World!', 'Hello, World!',
     544            '(test)', '(x,y)', ' x y ', 'null', None)
     545        con = self._connect()
     546        try:
     547            cur = con.cursor()
     548            # Note that %s::record does not work on input unfortunately
     549            # ("input of anonymous composite types is not implemented").
     550            # so we need to resort to a row constructor instead.
     551            row = ','.join(["%s"] * len(values))
     552            cur.execute("select ROW(%s) as test_record" % row, values)
     553            self.assertEqual(cur.description[0].name, 'test_record')
     554            self.assertEqual(cur.description[0].type_code, 'record')
     555            row = cur.fetchone()[0]
     556        finally:
     557            con.close()
     558        # Note that the element types get lost since we created an
     559        # untyped record (an anonymous composite type). For the same
     560        # reason this is also a normal tuple, not a named tuple.
     561        text_values = tuple(None if v is None else str(v) for v in values)
     562        self.assertEqual(row, text_values)
    489563
    490564    def test_custom_type(self):
Note: See TracChangeset for help on using the changeset viewer.