diff --git a/NEWS b/NEWS index c5e57797..26731e6d 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,8 @@ What's new in psycopg 2.5 - Added support for Python 3.3. - 'connection' and 'cursor' objects can be used in 'with' statements as context managers as specified by recent DBAPI extension. + - Added Diagnostics object to get extended info from a database error. + Many thanks to Matthew Woodcraft for the implementation (ticket #149). - Added support for backward scrollable cursors. Thanks to Jon Nelson for the initial patch (ticket #108). - Added a simple way to customize casting of composite types into Python diff --git a/doc/src/extensions.rst b/doc/src/extensions.rst index b0a68ce4..c810ae81 100644 --- a/doc/src/extensions.rst +++ b/doc/src/extensions.rst @@ -139,6 +139,37 @@ functionalities defined by the |DBAPI|_. .. automethod:: from_string(s) +.. autoclass:: Diagnostics(exception) + + .. versionadded:: 2.5 + + The attributes currently available are: + + .. attribute:: + column_name + constraint_name + context + datatype_name + internal_position + internal_query + message_detail + message_hint + message_primary + schema_name + severity + source_file + source_function + source_line + sqlstate + statement_position + table_name + + A string with the error field if available; `!None` if not available. + The attribute value is available only if the error sent by the server + includes the specified field and should remain available until the + cursor that generated the exception executes another query. + + .. autofunction:: set_wait_callback(f) .. versionadded:: 2.2.0 diff --git a/doc/src/module.rst b/doc/src/module.rst index b7436762..a998015a 100644 --- a/doc/src/module.rst +++ b/doc/src/module.rst @@ -157,10 +157,27 @@ available through the following exceptions: The cursor the exception was raised from; `None` if not applicable. + .. attribute:: diag + + A `~psycopg2.extensions.Diagnostics` object containing further + information about the error. :: + + >>> try: + ... cur.execute("SELECT * FROM barf") + ... except Exception, e: + ... pass + + >>> e.diag.severity + 'ERROR' + >>> e.diag.message_primary + 'relation "barf" does not exist' + + .. versionadded:: 2.5 + .. extension:: - The `~Error.pgerror`, `~Error.pgcode`, and `~Error.cursor` attributes - are Psycopg extensions. + The `~Error.pgerror`, `~Error.pgcode`, `~Error.cursor`, and + `~Error.diag` attributes are Psycopg extensions. .. exception:: InterfaceError diff --git a/lib/extensions.py b/lib/extensions.py index ca467d5a..8159a228 100644 --- a/lib/extensions.py +++ b/lib/extensions.py @@ -58,7 +58,7 @@ except ImportError: from psycopg2._psycopg import adapt, adapters, encodings, connection, cursor, lobject, Xid from psycopg2._psycopg import string_types, binary_types, new_type, new_array_type, register_type -from psycopg2._psycopg import ISQLQuote, Notify +from psycopg2._psycopg import ISQLQuote, Notify, Diagnostics from psycopg2._psycopg import QueryCanceledError, TransactionRollbackError diff --git a/psycopg/diagnostics.h b/psycopg/diagnostics.h new file mode 100644 index 00000000..adaf7580 --- /dev/null +++ b/psycopg/diagnostics.h @@ -0,0 +1,38 @@ +/* diagnostics.c - definition for the psycopg Diagnostics type + * + * Copyright (C) 2013 Matthew Woodcraft + * + * 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_DIAGNOSTICS_H +#define PSYCOPG_DIAGNOSTICS_H 1 + +extern HIDDEN PyTypeObject diagnosticsType; + +typedef struct { + PyObject_HEAD + + PyObject *err; /* exception to retrieve the diagnostics from */ + +} diagnosticsObject; + +#endif /* PSYCOPG_DIAGNOSTICS_H */ diff --git a/psycopg/diagnostics_type.c b/psycopg/diagnostics_type.c new file mode 100644 index 00000000..30ef2d46 --- /dev/null +++ b/psycopg/diagnostics_type.c @@ -0,0 +1,235 @@ +/* diagnostics.c - present information from libpq error responses + * + * Copyright (C) 2013 Matthew Woodcraft + * + * 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/diagnostics.h" +#include "psycopg/cursor.h" + +/* These are new in PostgreSQL 9.3. Defining them here so that psycopg2 can + * use them with a 9.3+ server even if compiled against pre-9.3 headers. */ +#ifndef PG_DIAG_SCHEMA_NAME +#define PG_DIAG_SCHEMA_NAME 's' +#endif +#ifndef PG_DIAG_TABLE_NAME +#define PG_DIAG_TABLE_NAME 't' +#endif +#ifndef PG_DIAG_COLUMN_NAME +#define PG_DIAG_COLUMN_NAME 'c' +#endif +#ifndef PG_DIAG_DATATYPE_NAME +#define PG_DIAG_DATATYPE_NAME 'd' +#endif +#ifndef PG_DIAG_CONSTRAINT_NAME +#define PG_DIAG_CONSTRAINT_NAME 'n' +#endif + + +/* Retrieve an error string from the exception's cursor. + * + * If the cursor or its result isn't available, return None. + */ +static PyObject * +psyco_diagnostics_get_field(diagnosticsObject *self, void *closure) +{ + // closure contains the field code. + PyObject *rv = NULL; + PyObject *curs = NULL; + const char* errortext; + if (!(curs = PyObject_GetAttrString(self->err, "cursor")) || + !PyObject_TypeCheck(curs, &cursorType) || + ((cursorObject *)curs)->pgres == NULL) { + goto exit; + } + errortext = PQresultErrorField( + ((cursorObject *)curs)->pgres, (Py_intptr_t) closure); + if (errortext) { + rv = conn_text_from_chars(((cursorObject *)curs)->conn, errortext); + } +exit: + if (!rv) { + rv = Py_None; + Py_INCREF(rv); + } + Py_XDECREF(curs); + return rv; +} + + +/* object calculated member list */ +static struct PyGetSetDef diagnosticsObject_getsets[] = { + { "severity", (getter)psyco_diagnostics_get_field, NULL, + NULL, (void*) PG_DIAG_SEVERITY }, + { "sqlstate", (getter)psyco_diagnostics_get_field, NULL, + NULL, (void*) PG_DIAG_SQLSTATE }, + { "message_primary", (getter)psyco_diagnostics_get_field, NULL, + NULL, (void*) PG_DIAG_MESSAGE_PRIMARY }, + { "message_detail", (getter)psyco_diagnostics_get_field, NULL, + NULL, (void*) PG_DIAG_MESSAGE_DETAIL }, + { "message_hint", (getter)psyco_diagnostics_get_field, NULL, + NULL, (void*) PG_DIAG_MESSAGE_HINT }, + { "statement_position", (getter)psyco_diagnostics_get_field, NULL, + NULL, (void*) PG_DIAG_STATEMENT_POSITION }, + { "internal_position", (getter)psyco_diagnostics_get_field, NULL, + NULL, (void*) PG_DIAG_INTERNAL_POSITION }, + { "internal_query", (getter)psyco_diagnostics_get_field, NULL, + NULL, (void*) PG_DIAG_INTERNAL_QUERY }, + { "context", (getter)psyco_diagnostics_get_field, NULL, + NULL, (void*) PG_DIAG_CONTEXT }, + { "schema_name", (getter)psyco_diagnostics_get_field, NULL, + NULL, (void*) PG_DIAG_SCHEMA_NAME }, + { "table_name", (getter)psyco_diagnostics_get_field, NULL, + NULL, (void*) PG_DIAG_TABLE_NAME }, + { "column_name", (getter)psyco_diagnostics_get_field, NULL, + NULL, (void*) PG_DIAG_COLUMN_NAME }, + { "datatype_name", (getter)psyco_diagnostics_get_field, NULL, + NULL, (void*) PG_DIAG_DATATYPE_NAME }, + { "constraint_name", (getter)psyco_diagnostics_get_field, NULL, + NULL, (void*) PG_DIAG_CONSTRAINT_NAME }, + { "source_file", (getter)psyco_diagnostics_get_field, NULL, + NULL, (void*) PG_DIAG_SOURCE_FILE }, + { "source_line", (getter)psyco_diagnostics_get_field, NULL, + NULL, (void*) PG_DIAG_SOURCE_LINE }, + { "source_function", (getter)psyco_diagnostics_get_field, NULL, + NULL, (void*) PG_DIAG_SOURCE_FUNCTION }, + {NULL} +}; + +/* initialization and finalization methods */ + +static PyObject * +diagnostics_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + return type->tp_alloc(type, 0); +} + +static int +diagnostics_init(diagnosticsObject *self, PyObject *args, PyObject *kwds) +{ + PyObject *err = NULL; + + if (!PyArg_ParseTuple(args, "O", &err)) + return -1; + + Py_INCREF(err); + self->err = err; + return 0; +} + +static int +diagnostics_traverse(diagnosticsObject *self, visitproc visit, void *arg) +{ + Py_VISIT(self->err); + return 0; +} + +static void +diagnostics_dealloc(diagnosticsObject* self) +{ + Py_CLEAR(self->err); + Py_TYPE(self)->tp_free((PyObject *)self); +} + +static void +diagnostics_del(PyObject* self) +{ + PyObject_GC_Del(self); +} + + +/* object type */ + +static const char diagnosticsType_doc[] = + "Details from a database error report.\n\n" + "The object is returned by the `~psycopg2.Error.diag` attribute of the\n" + "`!Error` object.\n" + "All the information available from the |PQresultErrorField|_ function\n" + "are exposed as attributes by the object, e.g. the `!severity` attribute\n" + "returns the `!PG_DIAG_SEVERITY` code. " + "Please refer to the `PostgreSQL documentation`__ for the meaning of all" + " the attributes.\n\n" + ".. |PQresultErrorField| replace:: `!PQresultErrorField()`\n" + ".. _PQresultErrorField: http://www.postgresql.org/docs/current/static/" + "libpq-exec.html#LIBPQ-PQRESULTERRORFIELD\n" + ".. __: PQresultErrorField_\n"; + +PyTypeObject diagnosticsType = { + PyVarObject_HEAD_INIT(NULL, 0) + "psycopg2._psycopg.Diagnostics", + sizeof(diagnosticsObject), + 0, + (destructor)diagnostics_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*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|Py_TPFLAGS_HAVE_GC, /*tp_flags*/ + diagnosticsType_doc, /*tp_doc*/ + + (traverseproc)diagnostics_traverse, /*tp_traverse*/ + 0, /*tp_clear*/ + + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + + /* Attribute descriptor and subclassing stuff */ + + 0, /*tp_methods*/ + 0, /*tp_members*/ + diagnosticsObject_getsets, /*tp_getset*/ + 0, /*tp_base*/ + 0, /*tp_dict*/ + + 0, /*tp_descr_get*/ + 0, /*tp_descr_set*/ + 0, /*tp_dictoffset*/ + + (initproc)diagnostics_init, /*tp_init*/ + 0, /*tp_alloc will be set to PyType_GenericAlloc in module init*/ + diagnostics_new, /*tp_new*/ + (freefunc)diagnostics_del, /*tp_free Low-level free-memory routine */ + 0, /*tp_is_gc For PyObject_IS_GC */ + 0, /*tp_bases*/ + 0, /*tp_mro method resolution order */ + 0, /*tp_cache*/ + 0, /*tp_subclasses*/ + 0 /*tp_weaklist*/ +}; diff --git a/psycopg/pqpath.c b/psycopg/pqpath.c index 641a5af6..62b8c157 100644 --- a/psycopg/pqpath.c +++ b/psycopg/pqpath.c @@ -1509,7 +1509,8 @@ pq_fetch(cursorObject *curs, int no_result) default: Dprintf("pq_fetch: uh-oh, something FAILED: pgconn = %p", curs->conn); pq_raise(curs->conn, curs, NULL); - IFCLEARPGRES(curs->pgres); + /* don't clear curs->pgres, because it contains detailed error + information */ ex = -1; break; } diff --git a/psycopg/psycopgmodule.c b/psycopg/psycopgmodule.c index b1b4979f..34985254 100644 --- a/psycopg/psycopgmodule.c +++ b/psycopg/psycopgmodule.c @@ -35,6 +35,7 @@ #include "psycopg/typecast.h" #include "psycopg/microprotocols.h" #include "psycopg/microprotocols_proto.h" +#include "psycopg/diagnostics.h" #include "psycopg/adapter_qstring.h" #include "psycopg/adapter_binary.h" @@ -500,6 +501,7 @@ psyco_errors_init(void) PyObject *base; PyObject *str = NULL; PyObject *descr = NULL; + PyObject *diag_property = NULL; int rv = -1; #if PY_VERSION_HEX >= 0x02050000 @@ -541,6 +543,12 @@ psyco_errors_init(void) PyObject_SetAttrString(Error, "pgcode", Py_None); PyObject_SetAttrString(Error, "cursor", Py_None); + if (!(diag_property = PyObject_CallFunctionObjArgs( + (PyObject *) &PyProperty_Type, &diagnosticsType, NULL))) { + goto exit; + } + PyObject_SetAttrString(Error, "diag", diag_property); + /* install __reduce_ex__ on Error to make all the subclasses picklable. * * Don't install it on Py 2.4: it is not used by the pickle @@ -560,6 +568,7 @@ psyco_errors_init(void) rv = 0; exit: + Py_XDECREF(diag_property); Py_XDECREF(descr); Py_XDECREF(str); Py_XDECREF(dict); @@ -882,6 +891,7 @@ INIT_MODULE(_psycopg)(void) Py_TYPE(&chunkType) = &PyType_Type; Py_TYPE(&NotifyType) = &PyType_Type; Py_TYPE(&XidType) = &PyType_Type; + Py_TYPE(&diagnosticsType) = &PyType_Type; if (PyType_Ready(&connectionType) == -1) goto exit; if (PyType_Ready(&cursorType) == -1) goto exit; @@ -898,6 +908,7 @@ INIT_MODULE(_psycopg)(void) if (PyType_Ready(&chunkType) == -1) goto exit; if (PyType_Ready(&NotifyType) == -1) goto exit; if (PyType_Ready(&XidType) == -1) goto exit; + if (PyType_Ready(&diagnosticsType) == -1) goto exit; #ifdef PSYCOPG_EXTENSIONS Py_TYPE(&lobjectType) = &PyType_Type; @@ -987,6 +998,7 @@ INIT_MODULE(_psycopg)(void) PyModule_AddObject(module, "ISQLQuote", (PyObject*)&isqlquoteType); PyModule_AddObject(module, "Notify", (PyObject*)&NotifyType); PyModule_AddObject(module, "Xid", (PyObject*)&XidType); + PyModule_AddObject(module, "Diagnostics", (PyObject*)&diagnosticsType); #ifdef PSYCOPG_EXTENSIONS PyModule_AddObject(module, "lobject", (PyObject*)&lobjectType); #endif @@ -1031,6 +1043,7 @@ INIT_MODULE(_psycopg)(void) pydatetimeType.tp_alloc = PyType_GenericAlloc; NotifyType.tp_alloc = PyType_GenericAlloc; XidType.tp_alloc = PyType_GenericAlloc; + diagnosticsType.tp_alloc = PyType_GenericAlloc; #ifdef PSYCOPG_EXTENSIONS lobjectType.tp_alloc = PyType_GenericAlloc; diff --git a/setup.py b/setup.py index 96cd2379..41eac4ca 100644 --- a/setup.py +++ b/setup.py @@ -428,6 +428,7 @@ sources = [ 'connection_int.c', 'connection_type.c', 'cursor_int.c', 'cursor_type.c', + 'diagnostics_type.c', 'lobject_int.c', 'lobject_type.c', 'notify_type.c', 'xid_type.c', @@ -441,7 +442,7 @@ sources = [ depends = [ # headers 'config.h', 'pgtypes.h', 'psycopg.h', 'python.h', - 'connection.h', 'cursor.h', 'green.h', 'lobject.h', + 'connection.h', 'cursor.h', 'diagnostics.h', 'green.h', 'lobject.h', 'notify.h', 'pqpath.h', 'xid.h', 'adapter_asis.h', 'adapter_binary.h', 'adapter_datetime.h', diff --git a/tests/test_module.py b/tests/test_module.py index 4083c368..1acdb84e 100755 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -22,7 +22,8 @@ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public # License for more details. -from testutils import unittest, skip_before_python +from testutils import unittest, skip_before_python, skip_before_postgres + from testconfig import dsn import psycopg2 @@ -154,6 +155,90 @@ class ExceptionsTestCase(unittest.TestCase): self.assert_(e.pgerror) self.assert_(e.cursor is cur) + def test_diagnostics_attributes(self): + cur = self.conn.cursor() + try: + cur.execute("select * from nonexist") + except psycopg2.Error, exc: + e = exc + + diag = e.diag + self.assert_(isinstance(diag, psycopg2.extensions.Diagnostics)) + for attr in [ + 'column_name', 'constraint_name', 'context', 'datatype_name', + 'internal_position', 'internal_query', 'message_detail', + 'message_hint', 'message_primary', 'schema_name', 'severity', + 'source_file', 'source_function', 'source_line', 'sqlstate', + 'statement_position', 'table_name', ]: + v = getattr(diag, attr) + if v is not None: + self.assert_(isinstance(v, str)) + + def test_diagnostics_values(self): + cur = self.conn.cursor() + try: + cur.execute("select * from nonexist") + except psycopg2.Error, exc: + e = exc + + self.assertEqual(e.diag.sqlstate, '42P01') + self.assertEqual(e.diag.severity, 'ERROR') + self.assertEqual(e.diag.statement_position, '15') + + def test_diagnostics_life(self): + import gc + from weakref import ref + + def tmp(): + cur = self.conn.cursor() + try: + cur.execute("select * from nonexist") + except psycopg2.Error, exc: + return cur, exc + + cur, e = tmp() + diag = e.diag + w = ref(cur) + + del e, cur + gc.collect() + assert(w() is not None) + + self.assertEqual(diag.sqlstate, '42P01') + + del diag + gc.collect() + assert(w() is None) + + def test_diagnostics_copy(self): + from StringIO import StringIO + f = StringIO() + cur = self.conn.cursor() + try: + cur.copy_to(f, 'nonexist') + except psycopg2.Error, exc: + diag = exc.diag + + self.assertEqual(diag.sqlstate, '42P01') + + @skip_before_postgres(9, 3) + def test_9_3_diagnostics(self): + cur = self.conn.cursor() + cur.execute(""" + create temp table test_exc ( + data int constraint chk_eq1 check (data = 1) + )""") + try: + cur.execute("insert into test_exc values(2)") + except psycopg2.Error, exc: + e = exc + self.assertEqual(e.pgcode, '23514') + self.assertEqual(e.diag.schema_name[:7], "pg_temp") + self.assertEqual(e.diag.table_name, "test_exc") + self.assertEqual(e.diag.column_name, None) + self.assertEqual(e.diag.constraint_name, "chk_eq1") + self.assertEqual(e.diag.datatype_name, None) + @skip_before_python(2, 5) def test_pickle(self): import pickle