summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>2018-10-11 04:39:14 +0100
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>2018-10-11 04:39:14 +0100
commit7619c91d62793c4424603cb8bd7116fb8cdbef20 (patch)
tree96b08aa485ce8ce47a6b8d1f7adecc051bb0735b
parente00c4e2a7fdc98ea016ebfcf2c9012b04a75a5fa (diff)
parent6b3d3604bfdf7986f4bf4bfc6ba061f5cd4451bf (diff)
downloadpsycopg2-7619c91d62793c4424603cb8bd7116fb8cdbef20.tar.gz
Merge branch 'description-extra-attrs'
-rw-r--r--NEWS3
-rw-r--r--doc/src/cursor.rst73
-rw-r--r--doc/src/extensions.rst74
-rw-r--r--psycopg/column.h48
-rw-r--r--psycopg/column_type.c375
-rw-r--r--psycopg/pqpath.c63
-rw-r--r--psycopg/psycopgmodule.c67
-rw-r--r--setup.py4
-rwxr-xr-xtests/test_cursor.py23
9 files changed, 597 insertions, 133 deletions
diff --git a/NEWS b/NEWS
index e475b9e..21db9dd 100644
--- a/NEWS
+++ b/NEWS
@@ -7,6 +7,9 @@ What's new in psycopg 2.8
New features:
- Added `~psycopg2.extensions.encrypt_password()` function (:ticket:`#576`).
+- Added `~psycopg2.extensions.Column.table_oid` and
+ `~psycopg2.extensions.Column.table_column` attributes on `cursor.description`
+ items (:ticket:`#661`).
- Added `connection.host` property (:ticket:`#726`).
- `~psycopg2.sql.Identifier` can represent qualified names in SQL composition
(:ticket:`#732`).
diff --git a/doc/src/cursor.rst b/doc/src/cursor.rst
index 057ccbf..1d7098f 100644
--- a/doc/src/cursor.rst
+++ b/doc/src/cursor.rst
@@ -37,46 +37,49 @@ The ``cursor`` class
.. attribute:: description
- This read-only attribute is a sequence of 7-item sequences.
-
- Each of these sequences is a named tuple (a regular tuple if
- :func:`collections.namedtuple` is not available) containing information
- describing one result column:
-
- 0. `!name`: the name of the column returned.
- 1. `!type_code`: the PostgreSQL OID of the column. You can use the
- |pg_type|_ system table to get more informations about the type.
- This is the value used by Psycopg to decide what Python type use
- to represent the value. See also
- :ref:`type-casting-from-sql-to-python`.
- 2. `!display_size`: the actual length of the column in bytes.
- Obtaining this value is computationally intensive, so it is
- always `!None` unless the :envvar:`PSYCOPG_DISPLAY_SIZE` parameter
- is set at compile time. See also PQgetlength_.
- 3. `!internal_size`: the size in bytes of the column associated to
- this column on the server. Set to a negative value for
- variable-size types See also PQfsize_.
- 4. `!precision`: total number of significant digits in columns of
- type |NUMERIC|_. `!None` for other types.
- 5. `!scale`: count of decimal digits in the fractional part in
- columns of type |NUMERIC|. `!None` for other types.
- 6. `!null_ok`: always `!None` as not easy to retrieve from the libpq.
-
- This attribute will be `!None` for operations that do not return rows
- or if the cursor has not had an operation invoked via the
- |execute*|_ methods yet.
-
- .. |pg_type| replace:: :sql:`pg_type`
- .. _pg_type: https://www.postgresql.org/docs/current/static/catalog-pg-type.html
- .. _PQgetlength: https://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQGETLENGTH
- .. _PQfsize: https://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQFSIZE
- .. _NUMERIC: https://www.postgresql.org/docs/current/static/datatype-numeric.html#DATATYPE-NUMERIC-DECIMAL
- .. |NUMERIC| replace:: :sql:`NUMERIC`
+ Read-only attribute describing the result of a query. It is a
+ sequence of `~psycopg2.extensions.Column` instances, each one
+ describing one result column in order. The attribute is `!None` for
+ operations that do not return rows or if the cursor has not had an
+ operation invoked via the |execute*|_ methods yet.
+
+ For compatibility with the DB-API, every object can be unpacked as a
+ 7-items sequence: the attributes retuned this way are the following.
+ For further details and other attributes available check the
+ `~psycopg2.extensions.Column` documentation.
+
+ 0. `~psycopg2.extensions.Column.name`: the name of the column returned.
+
+ 1. `~psycopg2.extensions.Column.type_code`: the PostgreSQL OID of the
+ column.
+
+ 2. `~psycopg2.extensions.Column.display_size`: the actual length of
+ the column in bytes.
+
+ 3. `~psycopg2.extensions.Column.internal_size`: the size in bytes of
+ the column associated to this column on the server.
+
+ 4. `~psycopg2.extensions.Column.precision`: total number of
+ significant digits in columns of type |NUMERIC|. `!None`
+ for other types.
+
+ 5. `~psycopg2.extensions.Column.scale`: count of decimal digits in
+ the fractional part in columns of type |NUMERIC|. `!None`
+ for other types.
+
+ 6. `~psycopg2.extensions.Column.null_ok`: always `!None` as not easy
+ to retrieve from the libpq.
.. versionchanged:: 2.4
if possible, columns descriptions are named tuple instead of
regular tuples.
+ .. versionchanged:: 2.8
+ columns descriptions are instances of `!Column`, exposing extra
+ attributes.
+
+ .. |NUMERIC| replace:: :sql:`NUMERIC`
+
.. method:: close()
Close the cursor now (rather than whenever `del` is executed).
diff --git a/doc/src/extensions.rst b/doc/src/extensions.rst
index 5bcc26f..159774b 100644
--- a/doc/src/extensions.rst
+++ b/doc/src/extensions.rst
@@ -154,6 +154,80 @@ introspection etc.
Close the object and remove it from the database.
+.. class:: Column
+
+ Description of one result column, exposed as items of the
+ `cursor.description` sequence.
+
+ .. versionadded:: 2.8
+
+ in previous version the `!description` attribute was a sequence of
+ simple tuples or namedtuples.
+
+ .. attribute:: name
+
+ The name of the column returned.
+
+ .. attribute:: type_code
+
+ The PostgreSQL OID of the column. You can use the |pg_type|_ system
+ table to get more informations about the type. This is the value used
+ by Psycopg to decide what Python type use to represent the value. See
+ also :ref:`type-casting-from-sql-to-python`.
+
+ .. attribute:: display_size
+
+ The actual length of the column in bytes. Obtaining this value is
+ computationally intensive, so it is always `!None` unless the
+ :envvar:`PSYCOPG_DISPLAY_SIZE` parameter is set at compile time. See
+ also PQgetlength_.
+
+ .. attribute:: internal_size
+
+ The size in bytes of the column associated to this column on the
+ server. Set to a negative value for variable-size types See also
+ PQfsize_.
+
+ .. attribute:: precision
+
+ Total number of significant digits in columns of type |NUMERIC|_.
+ `!None` for other types.
+
+ .. attribute:: scale
+
+ Count of decimal digits in the fractional part in columns of type
+ |NUMERIC|. `!None` for other types.
+
+ .. attribute:: null_ok
+
+ Always `!None` as not easy to retrieve from the libpq.
+
+ .. attribute:: table_oid
+
+ The oid of the table from which the column was fetched (matching
+ :sql:`pg_class.oid`). `!None` if the column is not a simple reference
+ to a table column. See also PQftable_.
+
+ .. versionadded:: 2.8
+
+ .. attribute:: table_column
+
+ The number of the column (within its table) making up the result
+ (matching :sql:`pg_attribute.attnum`, so it will start from 1).
+ `!None` if the column is not a simple reference to a table column. See
+ also PQftablecol_.
+
+ .. versionadded:: 2.8
+
+ .. |pg_type| replace:: :sql:`pg_type`
+ .. _pg_type: https://www.postgresql.org/docs/current/static/catalog-pg-type.html
+ .. _PQgetlength: https://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQGETLENGTH
+ .. _PQfsize: https://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQFSIZE
+ .. _PQftable: https://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQFTABLE
+ .. _PQftablecol: https://www.postgresql.org/docs/current/static/libpq-exec.html#LIBPQ-PQFTABLECOL
+ .. _NUMERIC: https://www.postgresql.org/docs/current/static/datatype-numeric.html#DATATYPE-NUMERIC-DECIMAL
+ .. |NUMERIC| replace:: :sql:`NUMERIC`
+
.. autoclass:: Notify(pid, channel, payload='')
:members: pid, channel, payload
diff --git a/psycopg/column.h b/psycopg/column.h
new file mode 100644
index 0000000..59e9d9c
--- /dev/null
+++ b/psycopg/column.h
@@ -0,0 +1,48 @@
+/* column.h - definition for a column in cursor.description type
+ *
+ * Copyright (C) 2018 Daniele Varrazzo <daniele.varrazzo@gmail.com>
+ *
+ * This file is part of psycopg.
+ *
+ * psycopg2 is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link this program with the OpenSSL library (or with
+ * modified versions of OpenSSL that use the same license as OpenSSL),
+ * and distribute linked combinations including the two.
+ *
+ * You must obey the GNU Lesser General Public License in all respects for
+ * all of the code used other than OpenSSL.
+ *
+ * psycopg2 is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
+ * License for more details.
+ */
+
+#ifndef PSYCOPG_COLUMN_H
+#define PSYCOPG_COLUMN_H 1
+
+extern HIDDEN PyTypeObject columnType;
+
+typedef struct {
+ PyObject_HEAD
+
+ PyObject *name;
+ PyObject *type_code;
+ PyObject *display_size;
+ PyObject *internal_size;
+ PyObject *precision;
+ PyObject *scale;
+ PyObject *null_ok;
+
+ /* Extensions to the DBAPI */
+ PyObject *table_oid;
+ PyObject *table_column;
+
+} columnObject;
+
+#endif /* PSYCOPG_COLUMN_H */
diff --git a/psycopg/column_type.c b/psycopg/column_type.c
new file mode 100644
index 0000000..8ee7d4c
--- /dev/null
+++ b/psycopg/column_type.c
@@ -0,0 +1,375 @@
+/* column_type.c - python interface to cursor.description objects
+ *
+ * Copyright (C) 2018 Daniele Varrazzo <daniele.varrazzo@gmail.com>
+ *
+ * This file is part of psycopg.
+ *
+ * psycopg2 is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link this program with the OpenSSL library (or with
+ * modified versions of OpenSSL that use the same license as OpenSSL),
+ * and distribute linked combinations including the two.
+ *
+ * You must obey the GNU Lesser General Public License in all respects for
+ * all of the code used other than OpenSSL.
+ *
+ * psycopg2 is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
+ * License for more details.
+ */
+
+#define PSYCOPG_MODULE
+#include "psycopg/psycopg.h"
+
+#include "psycopg/column.h"
+
+
+static const char column_doc[] =
+ "Description of a column returned by a query.\n\n"
+ "The DBAPI demands this object to be a 7-items sequence. This object\n"
+ "respects this interface, but adds names for the exposed attributes\n"
+ "and adds attribute not requested by the DBAPI.";
+
+static const char name_doc[] =
+ "The name of the column returned.";
+
+static const char type_code_doc[] =
+ "The PostgreSQL OID of the column.\n\n"
+ "You can use the pg_type system table to get more informations about the\n"
+ "type. This is the value used by Psycopg to decide what Python type use\n"
+ "to represent the value";
+
+static const char display_size_doc[] =
+ "The actual length of the column in bytes.\n\n"
+ "Obtaining this value is computationally intensive, so it is always None\n"
+ "unless the PSYCOPG_DISPLAY_SIZE parameter is set at compile time.";
+
+static const char internal_size_doc[] =
+ "The size in bytes of the column associated to this column on the server.\n\n"
+ "Set to a negative value for variable-size types.";
+
+static const char precision_doc[] =
+ "Total number of significant digits in columns of type NUMERIC.\n\n"
+ "None for other types.";
+
+static const char scale_doc[] =
+ "Count of decimal digits in the fractional part in columns of type NUMERIC.\n\n"
+ "None for other types.";
+
+static const char null_ok_doc[] =
+ "Always none.";
+
+static const char table_oid_doc[] =
+ "The OID of the table from which the column was fetched.\n\n"
+ "None if not available";
+
+static const char table_column_doc[] =
+ "The number (within its table) of the column making up the result\n\n"
+ "None if not available. Note that PostgreSQL column numbers start at 1";
+
+
+static PyMemberDef column_members[] = {
+ { "name", T_OBJECT, offsetof(columnObject, name), READONLY, (char *)name_doc },
+ { "type_code", T_OBJECT, offsetof(columnObject, type_code), READONLY, (char *)type_code_doc },
+ { "display_size", T_OBJECT, offsetof(columnObject, display_size), READONLY, (char *)display_size_doc },
+ { "internal_size", T_OBJECT, offsetof(columnObject, internal_size), READONLY, (char *)internal_size_doc },
+ { "precision", T_OBJECT, offsetof(columnObject, precision), READONLY, (char *)precision_doc },
+ { "scale", T_OBJECT, offsetof(columnObject, scale), READONLY, (char *)scale_doc },
+ { "null_ok", T_OBJECT, offsetof(columnObject, null_ok), READONLY, (char *)null_ok_doc },
+ { "table_oid", T_OBJECT, offsetof(columnObject, table_oid), READONLY, (char *)table_oid_doc },
+ { "table_column", T_OBJECT, offsetof(columnObject, table_column), READONLY, (char *)table_column_doc },
+ { NULL }
+};
+
+
+static PyObject *
+column_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
+{
+ return type->tp_alloc(type, 0);
+}
+
+
+static int
+column_init(columnObject *self, PyObject *args, PyObject *kwargs)
+{
+ static char *kwlist[] = {
+ "name", "type_code", "display_size", "internal_size",
+ "precision", "scale", "null_ok", "table_oid", "table_column", NULL};
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|OOOOOOOOO", kwlist,
+ &self->name, &self->type_code, &self->display_size,
+ &self->internal_size, &self->precision, &self->scale,
+ &self->null_ok, &self->table_oid, &self->table_column)) {
+ return -1;
+ }
+
+ return 0;
+}
+
+
+static void
+column_dealloc(columnObject *self)
+{
+ Py_CLEAR(self->name);
+ Py_CLEAR(self->type_code);
+ Py_CLEAR(self->display_size);
+ Py_CLEAR(self->internal_size);
+ Py_CLEAR(self->precision);
+ Py_CLEAR(self->scale);
+ Py_CLEAR(self->null_ok);
+ Py_CLEAR(self->table_oid);
+ Py_CLEAR(self->table_column);
+
+ Py_TYPE(self)->tp_free((PyObject *)self);
+}
+
+
+static PyObject*
+column_repr(columnObject *self)
+{
+ PyObject *rv = NULL;
+ PyObject *format = NULL;
+ PyObject *args = NULL;
+ PyObject *tmp;
+
+ if (!(format = Text_FromUTF8("Column(name=%r, type_code=%r)"))) {
+ goto exit;
+ }
+
+ if (!(args = PyTuple_New(2))) { goto exit; }
+
+ tmp = self->name ? self->name : Py_None;
+ Py_INCREF(tmp);
+ PyTuple_SET_ITEM(args, 0, tmp);
+
+ tmp = self->type_code ? self->type_code : Py_None;
+ Py_INCREF(tmp);
+ PyTuple_SET_ITEM(args, 1, tmp);
+
+ rv = Text_Format(format, args);
+
+exit:
+ Py_XDECREF(args);
+ Py_XDECREF(format);
+
+ return rv;
+}
+
+
+static PyObject *
+column_richcompare(columnObject *self, PyObject *other, int op)
+{
+ PyObject *rv = NULL;
+ PyObject *tself = NULL;
+
+ if (!(tself = PyObject_CallFunctionObjArgs(
+ (PyObject *)&PyTuple_Type, (PyObject *)self, NULL))) {
+ goto exit;
+ }
+
+ rv = PyObject_RichCompare(tself, other, op);
+
+exit:
+ Py_XDECREF(tself);
+ return rv;
+}
+
+
+/* column description can be accessed as a 7 items tuple for DBAPI compatibility */
+
+static Py_ssize_t
+column_len(columnObject *self)
+{
+ return 7;
+}
+
+
+static PyObject *
+column_getitem(columnObject *self, Py_ssize_t item)
+{
+ PyObject *rv = NULL;
+
+ if (item < 0)
+ item += 7;
+
+ switch (item) {
+ case 0:
+ rv = self->name;
+ break;
+ case 1:
+ rv = self->type_code;
+ break;
+ case 2:
+ rv = self->display_size;
+ break;
+ case 3:
+ rv = self->internal_size;
+ break;
+ case 4:
+ rv = self->precision;
+ break;
+ case 5:
+ rv = self->scale;
+ break;
+ case 6:
+ rv = self->null_ok;
+ break;
+ default:
+ PyErr_SetString(PyExc_IndexError, "index out of range");
+ return NULL;
+ }
+
+ if (!rv) {
+ rv = Py_None;
+ }
+
+ Py_INCREF(rv);
+ return rv;
+}
+
+
+static PySequenceMethods column_sequence = {
+ (lenfunc)column_len, /* sq_length */
+ 0, /* sq_concat */
+ 0, /* sq_repeat */
+ (ssizeargfunc)column_getitem, /* sq_item */
+ 0, /* sq_slice */
+ 0, /* sq_ass_item */
+ 0, /* sq_ass_slice */
+ 0, /* sq_contains */
+ 0, /* sq_inplace_concat */
+ 0, /* sq_inplace_repeat */
+};
+
+
+static PyObject *
+column_getstate(columnObject *self)
+{
+ return PyObject_CallFunctionObjArgs(
+ (PyObject *)&PyTuple_Type, (PyObject *)self, NULL);
+}
+
+
+PyObject *
+column_setstate(columnObject *self, PyObject *state)
+{
+ Py_ssize_t size;
+ PyObject *rv = NULL;
+
+ if (state == Py_None) {
+ goto exit;
+ }
+ if (!PyTuple_Check(state)) {
+ PyErr_SetString(PyExc_TypeError, "state is not a tuple");
+ goto error;
+ }
+
+ size = PyTuple_GET_SIZE(state);
+
+ if (size > 0) {
+ Py_CLEAR(self->name);
+ self->name = PyTuple_GET_ITEM(state, 0);
+ Py_INCREF(self->name);
+ }
+ if (size > 1) {
+ Py_CLEAR(self->type_code);
+ self->type_code = PyTuple_GET_ITEM(state, 1);
+ Py_INCREF(self->type_code);
+ }
+ if (size > 2) {
+ Py_CLEAR(self->display_size);
+ self->display_size = PyTuple_GET_ITEM(state, 2);
+ Py_INCREF(self->display_size);
+ }
+ if (size > 3) {
+ Py_CLEAR(self->internal_size);
+ self->internal_size = PyTuple_GET_ITEM(state, 3);
+ Py_INCREF(self->internal_size);
+ }
+ if (size > 4) {
+ Py_CLEAR(self->precision);
+ self->precision = PyTuple_GET_ITEM(state, 4);
+ Py_INCREF(self->precision);
+ }
+ if (size > 5) {
+ Py_CLEAR(self->scale);
+ self->scale = PyTuple_GET_ITEM(state, 5);
+ Py_INCREF(self->scale);
+ }
+ if (size > 6) {
+ Py_CLEAR(self->null_ok);
+ self->null_ok = PyTuple_GET_ITEM(state, 6);
+ Py_INCREF(self->null_ok);
+ }
+ if (size > 7) {
+ Py_CLEAR(self->table_oid);
+ self->table_oid = PyTuple_GET_ITEM(state, 7);
+ Py_INCREF(self->table_oid);
+ }
+ if (size > 8) {
+ Py_CLEAR(self->table_column);
+ self->table_column = PyTuple_GET_ITEM(state, 8);
+ Py_INCREF(self->table_column);
+ }
+
+exit:
+ rv = Py_None;
+ Py_INCREF(rv);
+
+error:
+ return rv;
+}
+
+
+static PyMethodDef column_methods[] = {
+ /* Make Column picklable. */
+ {"__getstate__", (PyCFunction)column_getstate, METH_NOARGS },
+ {"__setstate__", (PyCFunction)column_setstate, METH_O },
+ {NULL}
+};
+
+
+PyTypeObject columnType = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ "psycopg2.extensions.Column",
+ sizeof(columnObject), 0,
+ (destructor)column_dealloc, /* tp_dealloc */
+ 0, /*tp_print*/
+ 0, /*tp_getattr*/
+ 0, /*tp_setattr*/
+ 0, /*tp_compare*/
+ (reprfunc)column_repr, /*tp_repr*/
+ 0, /*tp_as_number*/
+ &column_sequence, /*tp_as_sequence*/
+ 0, /*tp_as_mapping*/
+ 0, /*tp_hash */
+ 0, /*tp_call*/
+ 0, /*tp_str*/
+ 0, /*tp_getattro*/
+ 0, /*tp_setattro*/
+ 0, /*tp_as_buffer*/
+ Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /*tp_flags*/
+ column_doc, /*tp_doc*/
+ 0, /*tp_traverse*/
+ 0, /*tp_clear*/
+ (richcmpfunc)column_richcompare, /*tp_richcompare*/
+ 0, /*tp_weaklistoffset*/
+ 0, /*tp_iter*/
+ 0, /*tp_iternext*/
+ column_methods, /*tp_methods*/
+ column_members, /*tp_members*/
+ 0, /*tp_getset*/
+ 0, /*tp_base*/
+ 0, /*tp_dict*/
+ 0, /*tp_descr_get*/
+ 0, /*tp_descr_set*/
+ 0, /*tp_dictoffset*/
+ (initproc)column_init, /*tp_init*/
+ 0, /*tp_alloc*/
+ column_new, /*tp_new*/
+};
diff --git a/psycopg/pqpath.c b/psycopg/pqpath.c
index 287262c..2893739 100644
--- a/psycopg/pqpath.c
+++ b/psycopg/pqpath.c
@@ -41,6 +41,7 @@
#include "psycopg/typecast.h"
#include "psycopg/pgtypes.h"
#include "psycopg/error.h"
+#include "psycopg/column.h"
#include "psycopg/libpq_support.h"
#include "libpq-fe.h"
@@ -1206,12 +1207,17 @@ _pq_fetch_tuples(cursorObject *curs)
Oid ftype = PQftype(curs->pgres, i);
int fsize = PQfsize(curs->pgres, i);
int fmod = PQfmod(curs->pgres, i);
+ Oid ftable = PQftable(curs->pgres, i);
+ int ftablecol = PQftablecol(curs->pgres, i);
- PyObject *dtitem = NULL;
+ columnObject *column = NULL;
PyObject *type = NULL;
PyObject *cast = NULL;
- if (!(dtitem = PyTuple_New(7))) { goto exit; }
+ if (!(column = (columnObject *)PyObject_CallObject(
+ (PyObject *)&columnType, NULL))) {
+ goto exit;
+ }
/* fill the right cast function by accessing three different dictionaries:
- the per-cursor dictionary, if available (can be NULL or None)
@@ -1248,20 +1254,16 @@ _pq_fetch_tuples(cursorObject *curs)
curs->conn, PQfname(curs->pgres, i)))) {
goto err_for;
}
- PyTuple_SET_ITEM(dtitem, 0, tmp);
+ column->name = tmp;
}
- PyTuple_SET_ITEM(dtitem, 1, type);
+ column->type_code = type;
type = NULL;
/* 2/ display size is the maximum size of this field result tuples. */
if (dsize && dsize[i] >= 0) {
PyObject *tmp;
if (!(tmp = PyInt_FromLong(dsize[i]))) { goto err_for; }
- PyTuple_SET_ITEM(dtitem, 2, tmp);
- }
- else {
- Py_INCREF(Py_None);
- PyTuple_SET_ITEM(dtitem, 2, Py_None);
+ column->display_size = tmp;
}
/* 3/ size on the backend */
@@ -1270,18 +1272,18 @@ _pq_fetch_tuples(cursorObject *curs)
if (ftype == NUMERICOID) {
PyObject *tmp;
if (!(tmp = PyInt_FromLong((fmod >> 16)))) { goto err_for; }
- PyTuple_SET_ITEM(dtitem, 3, tmp);
+ column->internal_size = tmp;
}
else { /* If variable length record, return maximum size */
PyObject *tmp;
if (!(tmp = PyInt_FromLong(fmod))) { goto err_for; }
- PyTuple_SET_ITEM(dtitem, 3, tmp);
+ column->internal_size = tmp;
}
}
else {
PyObject *tmp;
if (!(tmp = PyInt_FromLong(fsize))) { goto err_for; }
- PyTuple_SET_ITEM(dtitem, 3, tmp);
+ column->internal_size = tmp;
}
/* 4,5/ scale and precision */
@@ -1291,40 +1293,35 @@ _pq_fetch_tuples(cursorObject *curs)
if (!(tmp = PyInt_FromLong((fmod >> 16) & 0xFFFF))) {
goto err_for;
}
- PyTuple_SET_ITEM(dtitem, 4, tmp);
+ column->precision = tmp;
if (!(tmp = PyInt_FromLong(fmod & 0xFFFF))) {
- PyTuple_SET_ITEM(dtitem, 5, tmp);
+ goto err_for;
}
- PyTuple_SET_ITEM(dtitem, 5, tmp);
- }
- else {
- Py_INCREF(Py_None);
- PyTuple_SET_ITEM(dtitem, 4, Py_None);
- Py_INCREF(Py_None);
- PyTuple_SET_ITEM(dtitem, 5, Py_None);
+ column->scale = tmp;
}
- /* 6/ FIXME: null_ok??? */
- Py_INCREF(Py_None);
- PyTuple_SET_ITEM(dtitem, 6, Py_None);
+ /* table_oid, table_column */
+ if (ftable != InvalidOid) {
+ PyObject *tmp;
+ if (!(tmp = PyInt_FromLong((long)ftable))) { goto err_for; }
+ column->table_oid = tmp;
+ }
- /* Convert into a namedtuple if available */
- if (Py_None != psyco_DescriptionType) {
- PyObject *tmp = dtitem;
- dtitem = PyObject_CallObject(psyco_DescriptionType, tmp);
- Py_DECREF(tmp);
- if (NULL == dtitem) { goto err_for; }
+ if (ftablecol > 0) {
+ PyObject *tmp;
+ if (!(tmp = PyInt_FromLong((long)ftablecol))) { goto err_for; }
+ column->table_column = tmp;
}
- PyTuple_SET_ITEM(description, i, dtitem);
- dtitem = NULL;
+ PyTuple_SET_ITEM(description, i, (PyObject *)column);
+ column = NULL;
continue;
err_for:
Py_XDECREF(type);
- Py_XDECREF(dtitem);
+ Py_XDECREF(column);
goto exit;
}
diff --git a/psycopg/psycopgmodule.c b/psycopg/psycopgmodule.c
index 23e648d..ff04b25 100644
--- a/psycopg/psycopgmodule.c
+++ b/psycopg/psycopgmodule.c
@@ -32,6 +32,7 @@
#include "psycopg/replication_cursor.h"
#include "psycopg/replication_message.h"
#include "psycopg/green.h"
+#include "psycopg/column.h"
#include "psycopg/lobject.h"
#include "psycopg/notify.h"
#include "psycopg/xid.h"
@@ -69,9 +70,6 @@ HIDDEN int psycopg_debug_enabled = 0;
/* Python representation of SQL NULL */
HIDDEN PyObject *psyco_null = NULL;
-/* The type of the cursor.description items */
-HIDDEN PyObject *psyco_DescriptionType = NULL;
-
/* macro trick to stringify a macro expansion */
#define xstr(s) str(s)
#define str(s) #s
@@ -839,63 +837,6 @@ psyco_GetDecimalType(void)
}
-/* Create a namedtuple for cursor.description items
- *
- * Return None in case of expected errors (e.g. namedtuples not available)
- * NULL in case of errors to propagate.
- */
-static PyObject *
-psyco_make_description_type(void)
-{
- PyObject *coll = NULL;
- PyObject *nt = NULL;
- PyTypeObject *t = NULL;
- PyObject *s = NULL;
- PyObject *rv = NULL;
-
- /* Try to import collections.namedtuple */
- if (!(coll = PyImport_ImportModule("collections"))) {
- Dprintf("psyco_make_description_type: collections import failed");
- goto error;
- }
- if (!(nt = PyObject_GetAttrString(coll, "namedtuple"))) {
- Dprintf("psyco_make_description_type: no collections.namedtuple");
- goto error;
- }
-
- /* Build the namedtuple */
- if(!(t = (PyTypeObject *)PyObject_CallFunction(nt, "ss", "Column",
- "name type_code display_size internal_size precision scale null_ok"))) {
- goto exit;
- }
-
- /* Export the tuple on the extensions module
- * Required to guarantee picklability on Py > 3.3 (see Python issue 21374)
- * for previous Py version the module is psycopg2 anyway but for consistency
- * we'd rather expose it from the extensions module. */
- if (!(s = Text_FromUTF8("psycopg2.extensions"))) { goto exit; }
- if (0 > PyDict_SetItemString(t->tp_dict, "__module__", s)) { goto exit; }
-
- rv = (PyObject *)t;
- t = NULL;
-
-exit:
- Py_XDECREF(coll);
- Py_XDECREF(nt);
- Py_XDECREF((PyObject *)t);
- Py_XDECREF(s);
-
- return rv;
-
-error:
- /* controlled error: we will fall back to regular tuples. Return None. */
- PyErr_Clear();
- rv = Py_None;
- Py_INCREF(rv);
- goto exit;
-}
-
-
/** method table and module initialization **/
static PyMethodDef psycopgMethods[] = {
@@ -1041,6 +982,9 @@ INIT_MODULE(_psycopg)(void)
Py_TYPE(&chunkType) = &PyType_Type;
if (PyType_Ready(&chunkType) == -1) goto exit;
+ Py_TYPE(&columnType) = &PyType_Type;
+ if (PyType_Ready(&columnType) == -1) goto exit;
+
Py_TYPE(&notifyType) = &PyType_Type;
if (PyType_Ready(&notifyType) == -1) goto exit;
@@ -1119,7 +1063,6 @@ INIT_MODULE(_psycopg)(void)
if (!(psycoEncodings = PyDict_New())) { goto exit; }
if (0 != psyco_encodings_fill(psycoEncodings)) { goto exit; }
psyco_null = Bytes_FromString("NULL");
- if (!(psyco_DescriptionType = psyco_make_description_type())) { goto exit; }
/* set some module's parameters */
PyModule_AddStringConstant(module, "__version__", xstr(PSYCOPG_VERSION));
@@ -1138,6 +1081,7 @@ INIT_MODULE(_psycopg)(void)
PyModule_AddObject(module, "ReplicationCursor", (PyObject*)&replicationCursorType);
PyModule_AddObject(module, "ReplicationMessage", (PyObject*)&replicationMessageType);
PyModule_AddObject(module, "ISQLQuote", (PyObject*)&isqlquoteType);
+ PyModule_AddObject(module, "Column", (PyObject*)&columnType);
PyModule_AddObject(module, "Notify", (PyObject*)&notifyType);
PyModule_AddObject(module, "Xid", (PyObject*)&xidType);
PyModule_AddObject(module, "Diagnostics", (PyObject*)&diagnosticsType);
@@ -1150,7 +1094,6 @@ INIT_MODULE(_psycopg)(void)
PyModule_AddObject(module, "List", (PyObject*)&listType);
PyModule_AddObject(module, "QuotedString", (PyObject*)&qstringType);
PyModule_AddObject(module, "lobject", (PyObject*)&lobjectType);
- PyModule_AddObject(module, "Column", psyco_DescriptionType);
/* encodings dictionary in module dictionary */
PyModule_AddObject(module, "encodings", psycoEncodings);
diff --git a/setup.py b/setup.py
index db85fdb..ded5a05 100644
--- a/setup.py
+++ b/setup.py
@@ -486,7 +486,7 @@ sources = [
'libpq_support.c', 'win32_support.c', 'solaris_support.c',
'connection_int.c', 'connection_type.c',
- 'cursor_int.c', 'cursor_type.c',
+ 'cursor_int.c', 'cursor_type.c', 'column_type.c',
'replication_connection_type.c',
'replication_cursor_type.c',
'replication_message_type.c',
@@ -508,7 +508,7 @@ depends = [
'replication_connection.h',
'replication_cursor.h',
'replication_message.h',
- 'notify.h', 'pqpath.h', 'xid.h',
+ 'notify.h', 'pqpath.h', 'xid.h', 'column.h',
'libpq_support.h', 'win32_support.h',
'adapter_asis.h', 'adapter_binary.h', 'adapter_datetime.h',
diff --git a/tests/test_cursor.py b/tests/test_cursor.py
index 37110db..d048f3e 100755
--- a/tests/test_cursor.py
+++ b/tests/test_cursor.py
@@ -377,7 +377,7 @@ class CursorTests(ConnectingTestCase):
for i, rec in enumerate(curs):
self.assertEqual(i + 1, curs.rownumber)
- def test_namedtuple_description(self):
+ def test_description_attribs(self):
curs = self.conn.cursor()
curs.execute("""select
3.14::decimal(10,2) as pi,
@@ -412,6 +412,27 @@ class CursorTests(ConnectingTestCase):
self.assertEqual(c.precision, None)
self.assertEqual(c.scale, None)
+ def test_description_extra_attribs(self):
+ curs = self.conn.cursor()
+ curs.execute("""
+ create table testcol (
+ pi decimal(10,2),
+ hi text)
+ """)
+ curs.execute("select oid from pg_class where relname = %s", ('testcol',))
+ oid = curs.fetchone()[0]
+
+ curs.execute("insert into testcol values (3.14, 'hello')")
+ curs.execute("select hi, pi, 42 from testcol")
+ self.assertEqual(curs.description[0].table_oid, oid)
+ self.assertEqual(curs.description[0].table_column, 2)
+
+ self.assertEqual(curs.description[1].table_oid, oid)
+ self.assertEqual(curs.description[1].table_column, 1)
+
+ self.assertEqual(curs.description[2].table_oid, None)
+ self.assertEqual(curs.description[2].table_column, None)
+
def test_pickle_description(self):
curs = self.conn.cursor()
curs.execute('SELECT 1 AS foo')