From 5c3c3cd0a3046339597a03bc708cb5530dc07059 Mon Sep 17 00:00:00 2001 From: Teodor Sigaev Date: Fri, 8 Apr 2016 18:30:25 +0300 Subject: [PATCH] Enhanced custom error in PLPythonu Patch adds a new, more rich, way to emit error message or exception from PL/Pythonu code. Author: Pavel Stehule Reviewers: Catalin Iacob, Peter Eisentraut, Jim Nasby --- doc/src/sgml/plpython.sgml | 66 +++++-- src/pl/plpython/expected/plpython_test.out | 193 ++++++++++++++++++- src/pl/plpython/plpy_elog.c | 167 ++++++++++++++++- src/pl/plpython/plpy_elog.h | 2 + src/pl/plpython/plpy_plpymodule.c | 206 ++++++++++++++++----- src/pl/plpython/plpy_spi.c | 11 +- src/pl/plpython/sql/plpython_test.sql | 137 +++++++++++++- 7 files changed, 701 insertions(+), 81 deletions(-) diff --git a/doc/src/sgml/plpython.sgml b/doc/src/sgml/plpython.sgml index 015bbad8dd..cff66a2838 100644 --- a/doc/src/sgml/plpython.sgml +++ b/doc/src/sgml/plpython.sgml @@ -1341,24 +1341,23 @@ $$ LANGUAGE plpythonu; Utility Functions The plpy module also provides the functions - plpy.debug(msg), - plpy.log(msg), - plpy.info(msg), - plpy.notice(msg), - plpy.warning(msg), - plpy.error(msg), and - plpy.fatal(msg).elogin PL/Python - plpy.error and - plpy.fatal actually raise a Python exception - which, if uncaught, propagates out to the calling query, causing - the current transaction or subtransaction to be aborted. - raise plpy.Error(msg) and + plpy.debug(msg, **kwargs), + plpy.log(msg, **kwargs), + plpy.info(msg, **kwargs), + plpy.notice(msg, **kwargs), + plpy.warning(msg, **kwargs), + plpy.error(msg, **kwargs), and + plpy.fatal(msg, **kwargs). + elogin PL/Python + plpy.error and plpy.fatal + actually raise a Python exception which, if uncaught, propagates out to + the calling query, causing the current transaction or subtransaction to + be aborted. raise plpy.Error(msg) and raise plpy.Fatal(msg) are - equivalent to calling - plpy.error and - plpy.fatal, respectively. - The other functions only generate messages of different - priority levels. + equivalent to calling plpy.error(msg) and + plpy.fatal(msg), respectively but + the raise form does not allow passing keyword arguments. + The other functions only generate messages of different priority levels. Whether messages of a particular priority are reported to the client, written to the server log, or both is controlled by the and @@ -1366,6 +1365,39 @@ $$ LANGUAGE plpythonu; variables. See for more information. + + + The msg argument is given as a positional argument. For + backward compatibility, more than one positional argument can be given. In + that case, the string representation of the tuple of positional arguments + becomes the message reported to the client. + The following keyword-only arguments are accepted: + + detail, hint, + sqlstate, schema, + table, column, + datatype , constraint + . + The string representation of the objects passed as keyword-only arguments + is used to enrich the messages reported to the client. For example: + + +CREATE FUNCTION raise_custom_exception() RETURNS void AS $$ +plpy.error("custom exception message", detail = "some info about exception", hint = "hint for users") +$$ LANGUAGE plpythonu; + +postgres=# select raise_custom_exception(); +ERROR: XX000: plpy.Error: custom exception message +DETAIL: some info about exception +HINT: hint for users +CONTEXT: Traceback (most recent call last): + PL/Python function "raise_custom_exception", line 2, in <module> + plpy.error("custom exception message", detail = "some info about exception", hint = "hint for users") +PL/Python function "raise_custom_exception" +LOCATION: PLy_elog, plpy_elog.c:132 + + + Another set of utility functions are plpy.quote_literal(string), diff --git a/src/pl/plpython/expected/plpython_test.out b/src/pl/plpython/expected/plpython_test.out index f8270a7c5a..05caba1787 100644 --- a/src/pl/plpython/expected/plpython_test.out +++ b/src/pl/plpython/expected/plpython_test.out @@ -48,7 +48,7 @@ select module_contents(); Error, Fatal, SPIError, cursor, debug, error, execute, fatal, info, log, notice, prepare, quote_ident, quote_literal, quote_nullable, spiexceptions, subtransaction, warning (1 row) -CREATE FUNCTION elog_test() RETURNS void +CREATE FUNCTION elog_test_basic() RETURNS void AS $$ plpy.debug('debug') plpy.log('log') @@ -60,7 +60,7 @@ plpy.notice('notice') plpy.warning('warning') plpy.error('error') $$ LANGUAGE plpythonu; -SELECT elog_test(); +SELECT elog_test_basic(); INFO: info INFO: 37 INFO: () @@ -69,6 +69,193 @@ NOTICE: notice WARNING: warning ERROR: plpy.Error: error CONTEXT: Traceback (most recent call last): - PL/Python function "elog_test", line 10, in + PL/Python function "elog_test_basic", line 10, in plpy.error('error') +PL/Python function "elog_test_basic" +CREATE FUNCTION elog_test() RETURNS void +AS $$ +plpy.debug('debug', detail = 'some detail') +plpy.log('log', detail = 'some detail') +plpy.info('info', detail = 'some detail') +plpy.info() +plpy.info('the question', detail = 42); +plpy.info('This is message text.', + detail = 'This is detail text', + hint = 'This is hint text.', + sqlstate = 'XX000', + schema = 'any info about schema', + table = 'any info about table', + column = 'any info about column', + datatype = 'any info about datatype', + constraint = 'any info about constraint') +plpy.notice('notice', detail = 'some detail') +plpy.warning('warning', detail = 'some detail') +plpy.error('stop on error', detail = 'some detail', hint = 'some hint') +$$ LANGUAGE plpythonu; +SELECT elog_test(); +INFO: info +DETAIL: some detail +INFO: () +INFO: the question +DETAIL: 42 +INFO: This is message text. +DETAIL: This is detail text +HINT: This is hint text. +NOTICE: notice +DETAIL: some detail +WARNING: warning +DETAIL: some detail +ERROR: plpy.Error: stop on error +DETAIL: some detail +HINT: some hint +CONTEXT: Traceback (most recent call last): + PL/Python function "elog_test", line 18, in + plpy.error('stop on error', detail = 'some detail', hint = 'some hint') PL/Python function "elog_test" +do $$ plpy.info('other types', detail = (10,20)) $$ LANGUAGE plpythonu; +INFO: other types +DETAIL: (10, 20) +do $$ +import time; +from datetime import date +plpy.info('other types', detail = date(2016,2,26)) +$$ LANGUAGE plpythonu; +INFO: other types +DETAIL: 2016-02-26 +do $$ +basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana'] +plpy.info('other types', detail = basket) +$$ LANGUAGE plpythonu; +INFO: other types +DETAIL: ['apple', 'orange', 'apple', 'pear', 'orange', 'banana'] +-- should fail +do $$ plpy.info('wrong sqlstate', sqlstate='54444A') $$ LANGUAGE plpythonu; +ERROR: invalid SQLSTATE code +CONTEXT: PL/Python anonymous code block +do $$ plpy.info('unsupported argument', blabla='fooboo') $$ LANGUAGE plpythonu; +ERROR: 'blabla' is an invalid keyword argument for this function +CONTEXT: PL/Python anonymous code block +do $$ plpy.info('first message', message='second message') $$ LANGUAGE plpythonu; +ERROR: the message is already specified +CONTEXT: PL/Python anonymous code block +do $$ plpy.info('first message', 'second message', message='third message') $$ LANGUAGE plpythonu; +ERROR: the message is already specified +CONTEXT: PL/Python anonymous code block +-- raise exception in python, handle exception in plgsql +CREATE OR REPLACE FUNCTION raise_exception(_message text, _detail text DEFAULT NULL, _hint text DEFAULT NULL, + _sqlstate text DEFAULT NULL, + _schema text DEFAULT NULL, _table text DEFAULT NULL, _column text DEFAULT NULL, + _datatype text DEFAULT NULL, _constraint text DEFAULT NULL) +RETURNS void AS $$ +kwargs = { "message":_message, "detail":_detail, "hint":_hint, + "sqlstate":_sqlstate, "schema":_schema, "table":_table, + "column":_column, "datatype":_datatype, "constraint":_constraint } +# ignore None values +plpy.error(**dict((k, v) for k, v in iter(kwargs.items()) if v)) +$$ LANGUAGE plpythonu; +SELECT raise_exception('hello', 'world'); +ERROR: plpy.Error: hello +DETAIL: world +CONTEXT: Traceback (most recent call last): + PL/Python function "raise_exception", line 6, in + plpy.error(**dict((k, v) for k, v in iter(kwargs.items()) if v)) +PL/Python function "raise_exception" +SELECT raise_exception('message text', 'detail text', _sqlstate => 'YY333'); +ERROR: plpy.Error: message text +DETAIL: detail text +CONTEXT: Traceback (most recent call last): + PL/Python function "raise_exception", line 6, in + plpy.error(**dict((k, v) for k, v in iter(kwargs.items()) if v)) +PL/Python function "raise_exception" +SELECT raise_exception(_message => 'message text', + _detail => 'detail text', + _hint => 'hint text', + _sqlstate => 'XX555', + _schema => 'schema text', + _table => 'table text', + _column => 'column text', + _datatype => 'datatype text', + _constraint => 'constraint text'); +ERROR: plpy.Error: message text +DETAIL: detail text +HINT: hint text +CONTEXT: Traceback (most recent call last): + PL/Python function "raise_exception", line 6, in + plpy.error(**dict((k, v) for k, v in iter(kwargs.items()) if v)) +PL/Python function "raise_exception" +SELECT raise_exception(_message => 'message text', + _hint => 'hint text', + _schema => 'schema text', + _column => 'column text', + _constraint => 'constraint text'); +ERROR: plpy.Error: message text +HINT: hint text +CONTEXT: Traceback (most recent call last): + PL/Python function "raise_exception", line 6, in + plpy.error(**dict((k, v) for k, v in iter(kwargs.items()) if v)) +PL/Python function "raise_exception" +DO $$ +DECLARE + __message text; + __detail text; + __hint text; + __sqlstate text; + __schema_name text; + __table_name text; + __column_name text; + __datatype text; + __constraint text; +BEGIN + BEGIN + PERFORM raise_exception(_message => 'message text', + _detail => 'detail text', + _hint => 'hint text', + _sqlstate => 'XX555', + _schema => 'schema text', + _table => 'table text', + _column => 'column text', + _datatype => 'datatype text', + _constraint => 'constraint text'); + EXCEPTION WHEN SQLSTATE 'XX555' THEN + GET STACKED DIAGNOSTICS __message = MESSAGE_TEXT, + __detail = PG_EXCEPTION_DETAIL, + __hint = PG_EXCEPTION_HINT, + __sqlstate = RETURNED_SQLSTATE, + __schema_name = SCHEMA_NAME, + __table_name = TABLE_NAME, + __column_name = COLUMN_NAME, + __datatype = PG_DATATYPE_NAME, + __constraint = CONSTRAINT_NAME; + RAISE NOTICE 'handled exception' + USING DETAIL = format('message:(%s), detail:(%s), hint: (%s), sqlstate: (%s), ' + 'schema:(%s), table:(%s), column:(%s), datatype:(%s), constraint:(%s)', + __message, __detail, __hint, __sqlstate, __schema_name, + __table_name, __column_name, __datatype, __constraint); + END; +END; +$$; +NOTICE: handled exception +DETAIL: message:(plpy.Error: message text), detail:(detail text), hint: (hint text), sqlstate: (XX555), schema:(schema text), table:(table text), column:(column text), datatype:(datatype text), constraint:(constraint text) +-- the displayed context is different between Python2 and Python3, +-- but that's not important for this test +\set SHOW_CONTEXT never +do $$ +try: + plpy.execute("select raise_exception(_message => 'my message', _sqlstate => 'XX987', _hint => 'some hint', _table=> 'users_tab', _datatype => 'user_type')") +except Exception, e: + plpy.info(e.spidata) + raise e +$$ LANGUAGE plpythonu; +INFO: (119577128, None, 'some hint', None, 0, None, 'users_tab', None, 'user_type', None) +ERROR: plpy.SPIError: plpy.Error: my message +HINT: some hint +do $$ +try: + plpy.error(message = 'my message', sqlstate = 'XX987', hint = 'some hint', table = 'users_tab', datatype = 'user_type') +except Exception, e: + plpy.info('sqlstate: %s, hint: %s, tablename: %s, datatype: %s' % (e.sqlstate, e.hint, e.table_name, e.datatype_name)) + raise e +$$ LANGUAGE plpythonu; +INFO: sqlstate: XX987, hint: some hint, tablename: users_tab, datatype: user_type +ERROR: plpy.Error: my message +HINT: some hint diff --git a/src/pl/plpython/plpy_elog.c b/src/pl/plpython/plpy_elog.c index 15406d60b9..8e8db5d9ac 100644 --- a/src/pl/plpython/plpy_elog.c +++ b/src/pl/plpython/plpy_elog.c @@ -23,9 +23,16 @@ PyObject *PLy_exc_spi_error = NULL; static void PLy_traceback(char **xmsg, char **tbmsg, int *tb_depth); static void PLy_get_spi_error_data(PyObject *exc, int *sqlerrcode, char **detail, - char **hint, char **query, int *position); + char **hint, char **query, int *position, + char **schema_name, char **table_name, char **column_name, + char **datatype_name, char **constraint_name); +static void PLy_get_error_data(PyObject *exc, int *sqlerrcode, char **detail, + char **hint, char **schema_name, char **table_name, char **column_name, + char **datatype_name, char **constraint_name); static char *get_source_line(const char *src, int lineno); +static void get_string_attr(PyObject *obj, char *attrname, char **str); +static bool set_string_attr(PyObject *obj, char *attrname, char *str); /* * Emit a PG error or notice, together with any available info about @@ -51,12 +58,23 @@ PLy_elog(int elevel, const char *fmt,...) char *hint = NULL; char *query = NULL; int position = 0; + char *schema_name = NULL; + char *table_name = NULL; + char *column_name = NULL; + char *datatype_name = NULL; + char *constraint_name = NULL; PyErr_Fetch(&exc, &val, &tb); if (exc != NULL) { if (PyErr_GivenExceptionMatches(val, PLy_exc_spi_error)) - PLy_get_spi_error_data(val, &sqlerrcode, &detail, &hint, &query, &position); + PLy_get_spi_error_data(val, &sqlerrcode, &detail, &hint, &query, &position, + &schema_name, &table_name, &column_name, + &datatype_name, &constraint_name); + else if (PyErr_GivenExceptionMatches(val, PLy_exc_error)) + PLy_get_error_data(val, &sqlerrcode, &detail, &hint, + &schema_name, &table_name, &column_name, + &datatype_name, &constraint_name); else if (PyErr_GivenExceptionMatches(val, PLy_exc_fatal)) elevel = FATAL; } @@ -103,7 +121,12 @@ PLy_elog(int elevel, const char *fmt,...) (tb_depth > 0 && tbmsg) ? errcontext("%s", tbmsg) : 0, (hint) ? errhint("%s", hint) : 0, (query) ? internalerrquery(query) : 0, - (position) ? internalerrposition(position) : 0)); + (position) ? internalerrposition(position) : 0, + (schema_name) ? err_generic_string(PG_DIAG_SCHEMA_NAME, schema_name) : 0, + (table_name) ? err_generic_string(PG_DIAG_TABLE_NAME, table_name) : 0, + (column_name) ? err_generic_string(PG_DIAG_COLUMN_NAME, column_name) : 0, + (datatype_name) ? err_generic_string(PG_DIAG_DATATYPE_NAME, datatype_name) : 0, + (constraint_name) ? err_generic_string(PG_DIAG_CONSTRAINT_NAME, constraint_name) : 0)); } PG_CATCH(); { @@ -340,7 +363,7 @@ PLy_traceback(char **xmsg, char **tbmsg, int *tb_depth) * Extract error code from SPIError's sqlstate attribute. */ static void -PLy_get_spi_sqlerrcode(PyObject *exc, int *sqlerrcode) +PLy_get_sqlerrcode(PyObject *exc, int *sqlerrcode) { PyObject *sqlstate; char *buffer; @@ -360,12 +383,14 @@ PLy_get_spi_sqlerrcode(PyObject *exc, int *sqlerrcode) Py_DECREF(sqlstate); } - /* * Extract the error data from a SPIError */ static void -PLy_get_spi_error_data(PyObject *exc, int *sqlerrcode, char **detail, char **hint, char **query, int *position) +PLy_get_spi_error_data(PyObject *exc, int *sqlerrcode, char **detail, + char **hint, char **query, int *position, + char **schema_name, char **table_name, char **column_name, + char **datatype_name, char **constraint_name) { PyObject *spidata = NULL; @@ -373,7 +398,9 @@ PLy_get_spi_error_data(PyObject *exc, int *sqlerrcode, char **detail, char **hin if (spidata != NULL) { - PyArg_ParseTuple(spidata, "izzzi", sqlerrcode, detail, hint, query, position); + PyArg_ParseTuple(spidata, "izzzizzzzz", sqlerrcode, detail, hint, query, position, + schema_name, table_name, column_name, + datatype_name, constraint_name); } else { @@ -381,7 +408,7 @@ PLy_get_spi_error_data(PyObject *exc, int *sqlerrcode, char **detail, char **hin * If there's no spidata, at least set the sqlerrcode. This can happen * if someone explicitly raises a SPI exception from Python code. */ - PLy_get_spi_sqlerrcode(exc, sqlerrcode); + PLy_get_sqlerrcode(exc, sqlerrcode); } PyErr_Clear(); @@ -389,6 +416,30 @@ PLy_get_spi_error_data(PyObject *exc, int *sqlerrcode, char **detail, char **hin Py_XDECREF(spidata); } +/* + * Extract the error data from an Error. + * Note: position and query attributes are never set for Error so, unlike + * PLy_get_spi_error_data, this function doesn't return them. + */ +static void +PLy_get_error_data(PyObject *exc, int *sqlerrcode, char **detail, char **hint, + char **schema_name, char **table_name, char **column_name, + char **datatype_name, char **constraint_name) +{ + PLy_get_sqlerrcode(exc, sqlerrcode); + + get_string_attr(exc, "detail", detail); + get_string_attr(exc, "hint", hint); + get_string_attr(exc, "schema_name", schema_name); + get_string_attr(exc, "table_name", table_name); + get_string_attr(exc, "column_name", column_name); + get_string_attr(exc, "datatype_name", datatype_name); + get_string_attr(exc, "constraint_name", constraint_name); + + PyErr_Clear(); + /* no elog here, we simply won't report the errhint, errposition etc */ +} + /* * Get the given source line as a palloc'd string */ @@ -464,3 +515,103 @@ PLy_exception_set_plural(PyObject *exc, PyErr_SetString(exc, buf); } + +/* set attributes of the given exception to details from ErrorData */ +void +PLy_exception_set_with_details(PyObject *excclass, ErrorData *edata) +{ + PyObject *args = NULL; + PyObject *error = NULL; + + args = Py_BuildValue("(s)", edata->message); + if (!args) + goto failure; + + /* create a new exception with the error message as the parameter */ + error = PyObject_CallObject(excclass, args); + if (!error) + goto failure; + + if (!set_string_attr(error, "sqlstate", + unpack_sql_state(edata->sqlerrcode))) + goto failure; + + if (!set_string_attr(error, "detail", edata->detail)) + goto failure; + + if (!set_string_attr(error, "hint", edata->hint)) + goto failure; + + if (!set_string_attr(error, "query", edata->internalquery)) + goto failure; + + if (!set_string_attr(error, "schema_name", edata->schema_name)) + goto failure; + + if (!set_string_attr(error, "table_name", edata->table_name)) + goto failure; + + if (!set_string_attr(error, "column_name", edata->column_name)) + goto failure; + + if (!set_string_attr(error, "datatype_name", edata->datatype_name)) + goto failure; + + if (!set_string_attr(error, "constraint_name", edata->constraint_name)) + goto failure; + + PyErr_SetObject(excclass, error); + + Py_DECREF(args); + Py_DECREF(error); + + return; + +failure: + Py_XDECREF(args); + Py_XDECREF(error); + + elog(ERROR, "could not convert error to Python exception"); +} + +/* get string value of an object attribute */ +static void +get_string_attr(PyObject *obj, char *attrname, char **str) +{ + PyObject *val; + + val = PyObject_GetAttrString(obj, attrname); + if (val != NULL && val != Py_None) + { + *str = pstrdup(PyString_AsString(val)); + } + Py_XDECREF(val); +} + +/* set an object attribute to a string value, returns true when the set was + * successful + */ +static bool +set_string_attr(PyObject *obj, char *attrname, char *str) +{ + int result; + PyObject *val; + + if (str != NULL) + { + val = PyString_FromString(str); + if (!val) + return false; + } + else + { + val = Py_None; + Py_INCREF(Py_None); + } + + result = PyObject_SetAttrString(obj, attrname, val); + Py_DECREF(val); + + return result != -1; +} + diff --git a/src/pl/plpython/plpy_elog.h b/src/pl/plpython/plpy_elog.h index 94725c2a2e..5dd4ef7a14 100644 --- a/src/pl/plpython/plpy_elog.h +++ b/src/pl/plpython/plpy_elog.h @@ -17,4 +17,6 @@ extern void PLy_exception_set(PyObject *exc, const char *fmt,...) pg_attribute_p extern void PLy_exception_set_plural(PyObject *exc, const char *fmt_singular, const char *fmt_plural, unsigned long n,...) pg_attribute_printf(2, 5) pg_attribute_printf(3, 5); +extern void PLy_exception_set_with_details(PyObject *excclass, ErrorData *edata); + #endif /* PLPY_ELOG_H */ diff --git a/src/pl/plpython/plpy_plpymodule.c b/src/pl/plpython/plpy_plpymodule.c index a44b7fbe9a..f136e8ece8 100644 --- a/src/pl/plpython/plpy_plpymodule.c +++ b/src/pl/plpython/plpy_plpymodule.c @@ -28,13 +28,13 @@ static void PLy_add_exceptions(PyObject *plpy); static void PLy_generate_spi_exceptions(PyObject *mod, PyObject *base); /* module functions */ -static PyObject *PLy_debug(PyObject *self, PyObject *args); -static PyObject *PLy_log(PyObject *self, PyObject *args); -static PyObject *PLy_info(PyObject *self, PyObject *args); -static PyObject *PLy_notice(PyObject *self, PyObject *args); -static PyObject *PLy_warning(PyObject *self, PyObject *args); -static PyObject *PLy_error(PyObject *self, PyObject *args); -static PyObject *PLy_fatal(PyObject *self, PyObject *args); +static PyObject *PLy_debug(PyObject *self, PyObject *args, PyObject *kw); +static PyObject *PLy_log(PyObject *self, PyObject *args, PyObject *kw); +static PyObject *PLy_info(PyObject *self, PyObject *args, PyObject *kw); +static PyObject *PLy_notice(PyObject *self, PyObject *args, PyObject *kw); +static PyObject *PLy_warning(PyObject *self, PyObject *args, PyObject *kw); +static PyObject *PLy_error(PyObject *self, PyObject *args, PyObject *kw); +static PyObject *PLy_fatal(PyObject *self, PyObject *args, PyObject *kw); static PyObject *PLy_quote_literal(PyObject *self, PyObject *args); static PyObject *PLy_quote_nullable(PyObject *self, PyObject *args); static PyObject *PLy_quote_ident(PyObject *self, PyObject *args); @@ -57,13 +57,13 @@ static PyMethodDef PLy_methods[] = { /* * logging methods */ - {"debug", PLy_debug, METH_VARARGS, NULL}, - {"log", PLy_log, METH_VARARGS, NULL}, - {"info", PLy_info, METH_VARARGS, NULL}, - {"notice", PLy_notice, METH_VARARGS, NULL}, - {"warning", PLy_warning, METH_VARARGS, NULL}, - {"error", PLy_error, METH_VARARGS, NULL}, - {"fatal", PLy_fatal, METH_VARARGS, NULL}, + {"debug", (PyCFunction) PLy_debug, METH_VARARGS|METH_KEYWORDS, NULL}, + {"log", (PyCFunction) PLy_log, METH_VARARGS|METH_KEYWORDS, NULL}, + {"info", (PyCFunction) PLy_info, METH_VARARGS|METH_KEYWORDS, NULL}, + {"notice", (PyCFunction) PLy_notice, METH_VARARGS|METH_KEYWORDS, NULL}, + {"warning", (PyCFunction) PLy_warning, METH_VARARGS|METH_KEYWORDS, NULL}, + {"error", (PyCFunction) PLy_error, METH_VARARGS|METH_KEYWORDS, NULL}, + {"fatal", (PyCFunction) PLy_fatal, METH_VARARGS|METH_KEYWORDS, NULL}, /* * create a stored plan @@ -271,48 +271,49 @@ PLy_generate_spi_exceptions(PyObject *mod, PyObject *base) * the python interface to the elog function * don't confuse these with PLy_elog */ -static PyObject *PLy_output(volatile int, PyObject *, PyObject *); +static PyObject *PLy_output(volatile int level, PyObject *self, + PyObject *args, PyObject *kw); static PyObject * -PLy_debug(PyObject *self, PyObject *args) +PLy_debug(PyObject *self, PyObject *args, PyObject *kw) { - return PLy_output(DEBUG2, self, args); + return PLy_output(DEBUG2, self, args, kw); } static PyObject * -PLy_log(PyObject *self, PyObject *args) +PLy_log(PyObject *self, PyObject *args, PyObject *kw) { - return PLy_output(LOG, self, args); + return PLy_output(LOG, self, args, kw); } static PyObject * -PLy_info(PyObject *self, PyObject *args) +PLy_info(PyObject *self, PyObject *args, PyObject *kw) { - return PLy_output(INFO, self, args); + return PLy_output(INFO, self, args, kw); } static PyObject * -PLy_notice(PyObject *self, PyObject *args) +PLy_notice(PyObject *self, PyObject *args, PyObject *kw) { - return PLy_output(NOTICE, self, args); + return PLy_output(NOTICE, self, args, kw); } static PyObject * -PLy_warning(PyObject *self, PyObject *args) +PLy_warning(PyObject *self, PyObject *args, PyObject *kw) { - return PLy_output(WARNING, self, args); + return PLy_output(WARNING, self, args, kw); } static PyObject * -PLy_error(PyObject *self, PyObject *args) +PLy_error(PyObject *self, PyObject *args, PyObject *kw) { - return PLy_output(ERROR, self, args); + return PLy_output(ERROR, self, args, kw); } static PyObject * -PLy_fatal(PyObject *self, PyObject *args) +PLy_fatal(PyObject *self, PyObject *args, PyObject *kw) { - return PLy_output(FATAL, self, args); + return PLy_output(FATAL, self, args, kw); } static PyObject * @@ -368,12 +369,45 @@ PLy_quote_ident(PyObject *self, PyObject *args) return ret; } +/* enforce cast of object to string */ +static char * +object_to_string(PyObject *obj) +{ + if (obj) + { + PyObject *so = PyObject_Str(obj); + + if (so != NULL) + { + char *str; + + str = pstrdup(PyString_AsString(so)); + Py_DECREF(so); + + return str; + } + } + + return NULL; +} + static PyObject * -PLy_output(volatile int level, PyObject *self, PyObject *args) +PLy_output(volatile int level, PyObject *self, PyObject *args, PyObject *kw) { - PyObject *volatile so; - char *volatile sv; - volatile MemoryContext oldcontext; + int sqlstate = 0; + char *volatile sqlstatestr = NULL; + char *volatile message = NULL; + char *volatile detail = NULL; + char *volatile hint = NULL; + char *volatile column = NULL; + char *volatile constraint = NULL; + char *volatile datatype = NULL; + char *volatile table = NULL; + char *volatile schema = NULL; + MemoryContext oldcontext ; + PyObject *key, *value; + PyObject *volatile so; + Py_ssize_t pos = 0; if (PyTuple_Size(args) == 1) { @@ -389,40 +423,118 @@ PLy_output(volatile int level, PyObject *self, PyObject *args) } else so = PyObject_Str(args); - if (so == NULL || ((sv = PyString_AsString(so)) == NULL)) + + if (so == NULL || ((message = pstrdup(PyString_AsString(so))) == NULL)) { level = ERROR; - sv = dgettext(TEXTDOMAIN, "could not parse error message in plpy.elog"); + message = dgettext(TEXTDOMAIN, "could not parse error message in plpy.elog"); + } + + Py_XDECREF(so); + + if (kw != NULL) + { + while (PyDict_Next(kw, &pos, &key, &value)) + { + char *keyword = PyString_AsString(key); + + if (strcmp(keyword, "message") == 0) + { + /* the message should not be overwriten */ + if (PyTuple_Size(args) != 0) + PLy_elog(ERROR, "the message is already specified"); + + pfree(message); + message = object_to_string(value); + } + else if (strcmp(keyword, "detail") == 0) + detail = object_to_string(value); + else if (strcmp(keyword, "hint") == 0) + hint = object_to_string(value); + else if (strcmp(keyword, "sqlstate") == 0) + sqlstatestr = object_to_string(value); + else if (strcmp(keyword, "schema") == 0) + schema = object_to_string(value); + else if (strcmp(keyword, "table") == 0) + table = object_to_string(value); + else if (strcmp(keyword, "column") == 0) + column = object_to_string(value); + else if (strcmp(keyword, "datatype") == 0) + datatype = object_to_string(value); + else if (strcmp(keyword, "constraint") == 0) + constraint = object_to_string(value); + else + PLy_elog(ERROR, "'%s' is an invalid keyword argument for this function", + keyword); + } + } + + if (sqlstatestr != NULL) + { + if (strlen(sqlstatestr) != 5) + PLy_elog(ERROR, "invalid SQLSTATE code"); + + if (strspn(sqlstatestr, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != 5) + PLy_elog(ERROR, "invalid SQLSTATE code"); + + sqlstate = MAKE_SQLSTATE(sqlstatestr[0], + sqlstatestr[1], + sqlstatestr[2], + sqlstatestr[3], + sqlstatestr[4]); } oldcontext = CurrentMemoryContext; PG_TRY(); { - pg_verifymbstr(sv, strlen(sv), false); - elog(level, "%s", sv); + if (message != NULL) + pg_verifymbstr(message, strlen(message), false); + if (detail != NULL) + pg_verifymbstr(detail, strlen(detail), false); + if (hint != NULL) + pg_verifymbstr(hint, strlen(hint), false); + if (schema != NULL) + pg_verifymbstr(schema, strlen(schema), false); + if (table != NULL) + pg_verifymbstr(table, strlen(table), false); + if (column != NULL) + pg_verifymbstr(column, strlen(column), false); + if (datatype != NULL) + pg_verifymbstr(datatype, strlen(datatype), false); + if (constraint != NULL) + pg_verifymbstr(constraint, strlen(constraint), false); + + ereport(level, + ((sqlstate != 0) ? errcode(sqlstate) : 0, + (message != NULL) ? errmsg_internal("%s", message) : 0, + (detail != NULL) ? errdetail_internal("%s", detail) : 0, + (hint != NULL) ? errhint("%s", hint) : 0, + (column != NULL) ? + err_generic_string(PG_DIAG_COLUMN_NAME, column) : 0, + (constraint != NULL) ? + err_generic_string(PG_DIAG_CONSTRAINT_NAME, constraint) : 0, + (datatype != NULL) ? + err_generic_string(PG_DIAG_DATATYPE_NAME, datatype) : 0, + (table != NULL) ? + err_generic_string(PG_DIAG_TABLE_NAME, table) : 0, + (schema != NULL) ? + err_generic_string(PG_DIAG_SCHEMA_NAME, schema) : 0)); } PG_CATCH(); { - ErrorData *edata; + ErrorData *edata; MemoryContextSwitchTo(oldcontext); edata = CopyErrorData(); FlushErrorState(); - /* - * Note: If sv came from PyString_AsString(), it points into storage - * owned by so. So free so after using sv. - */ - Py_XDECREF(so); + PLy_exception_set_with_details(PLy_exc_error, edata); + FreeErrorData(edata); - /* Make Python raise the exception */ - PLy_exception_set(PLy_exc_error, "%s", edata->message); return NULL; } PG_END_TRY(); - Py_XDECREF(so); - /* * return a legal object so the interpreter will continue on its merry way */ diff --git a/src/pl/plpython/plpy_spi.c b/src/pl/plpython/plpy_spi.c index 7d84629f48..f50d6545e0 100644 --- a/src/pl/plpython/plpy_spi.c +++ b/src/pl/plpython/plpy_spi.c @@ -554,8 +554,9 @@ PLy_spi_subtransaction_abort(MemoryContext oldcontext, ResourceOwner oldowner) /* Look up the correct exception */ entry = hash_search(PLy_spi_exceptions, &(edata->sqlerrcode), HASH_FIND, NULL); - /* We really should find it, but just in case have a fallback */ - Assert(entry != NULL); + /* This could be a custom error code, if that's the case fallback to + * SPIError + */ exc = entry ? entry->exc : PLy_exc_spi_error; /* Make Python raise the exception */ PLy_spi_exception_set(exc, edata); @@ -582,8 +583,10 @@ PLy_spi_exception_set(PyObject *excclass, ErrorData *edata) if (!spierror) goto failure; - spidata = Py_BuildValue("(izzzi)", edata->sqlerrcode, edata->detail, edata->hint, - edata->internalquery, edata->internalpos); + spidata= Py_BuildValue("(izzzizzzzz)", edata->sqlerrcode, edata->detail, edata->hint, + edata->internalquery, edata->internalpos, + edata->schema_name, edata->table_name, edata->column_name, + edata->datatype_name, edata->constraint_name); if (!spidata) goto failure; diff --git a/src/pl/plpython/sql/plpython_test.sql b/src/pl/plpython/sql/plpython_test.sql index 3a761047a0..6e5b535ceb 100644 --- a/src/pl/plpython/sql/plpython_test.sql +++ b/src/pl/plpython/sql/plpython_test.sql @@ -36,8 +36,7 @@ $$ LANGUAGE plpythonu; select module_contents(); - -CREATE FUNCTION elog_test() RETURNS void +CREATE FUNCTION elog_test_basic() RETURNS void AS $$ plpy.debug('debug') plpy.log('log') @@ -50,4 +49,138 @@ plpy.warning('warning') plpy.error('error') $$ LANGUAGE plpythonu; +SELECT elog_test_basic(); + +CREATE FUNCTION elog_test() RETURNS void +AS $$ +plpy.debug('debug', detail = 'some detail') +plpy.log('log', detail = 'some detail') +plpy.info('info', detail = 'some detail') +plpy.info() +plpy.info('the question', detail = 42); +plpy.info('This is message text.', + detail = 'This is detail text', + hint = 'This is hint text.', + sqlstate = 'XX000', + schema = 'any info about schema', + table = 'any info about table', + column = 'any info about column', + datatype = 'any info about datatype', + constraint = 'any info about constraint') +plpy.notice('notice', detail = 'some detail') +plpy.warning('warning', detail = 'some detail') +plpy.error('stop on error', detail = 'some detail', hint = 'some hint') +$$ LANGUAGE plpythonu; + SELECT elog_test(); + +do $$ plpy.info('other types', detail = (10,20)) $$ LANGUAGE plpythonu; + +do $$ +import time; +from datetime import date +plpy.info('other types', detail = date(2016,2,26)) +$$ LANGUAGE plpythonu; + +do $$ +basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana'] +plpy.info('other types', detail = basket) +$$ LANGUAGE plpythonu; + +-- should fail +do $$ plpy.info('wrong sqlstate', sqlstate='54444A') $$ LANGUAGE plpythonu; +do $$ plpy.info('unsupported argument', blabla='fooboo') $$ LANGUAGE plpythonu; +do $$ plpy.info('first message', message='second message') $$ LANGUAGE plpythonu; +do $$ plpy.info('first message', 'second message', message='third message') $$ LANGUAGE plpythonu; + +-- raise exception in python, handle exception in plgsql +CREATE OR REPLACE FUNCTION raise_exception(_message text, _detail text DEFAULT NULL, _hint text DEFAULT NULL, + _sqlstate text DEFAULT NULL, + _schema text DEFAULT NULL, _table text DEFAULT NULL, _column text DEFAULT NULL, + _datatype text DEFAULT NULL, _constraint text DEFAULT NULL) +RETURNS void AS $$ +kwargs = { "message":_message, "detail":_detail, "hint":_hint, + "sqlstate":_sqlstate, "schema":_schema, "table":_table, + "column":_column, "datatype":_datatype, "constraint":_constraint } +# ignore None values +plpy.error(**dict((k, v) for k, v in iter(kwargs.items()) if v)) +$$ LANGUAGE plpythonu; + +SELECT raise_exception('hello', 'world'); +SELECT raise_exception('message text', 'detail text', _sqlstate => 'YY333'); +SELECT raise_exception(_message => 'message text', + _detail => 'detail text', + _hint => 'hint text', + _sqlstate => 'XX555', + _schema => 'schema text', + _table => 'table text', + _column => 'column text', + _datatype => 'datatype text', + _constraint => 'constraint text'); + +SELECT raise_exception(_message => 'message text', + _hint => 'hint text', + _schema => 'schema text', + _column => 'column text', + _constraint => 'constraint text'); + +DO $$ +DECLARE + __message text; + __detail text; + __hint text; + __sqlstate text; + __schema_name text; + __table_name text; + __column_name text; + __datatype text; + __constraint text; +BEGIN + BEGIN + PERFORM raise_exception(_message => 'message text', + _detail => 'detail text', + _hint => 'hint text', + _sqlstate => 'XX555', + _schema => 'schema text', + _table => 'table text', + _column => 'column text', + _datatype => 'datatype text', + _constraint => 'constraint text'); + EXCEPTION WHEN SQLSTATE 'XX555' THEN + GET STACKED DIAGNOSTICS __message = MESSAGE_TEXT, + __detail = PG_EXCEPTION_DETAIL, + __hint = PG_EXCEPTION_HINT, + __sqlstate = RETURNED_SQLSTATE, + __schema_name = SCHEMA_NAME, + __table_name = TABLE_NAME, + __column_name = COLUMN_NAME, + __datatype = PG_DATATYPE_NAME, + __constraint = CONSTRAINT_NAME; + RAISE NOTICE 'handled exception' + USING DETAIL = format('message:(%s), detail:(%s), hint: (%s), sqlstate: (%s), ' + 'schema:(%s), table:(%s), column:(%s), datatype:(%s), constraint:(%s)', + __message, __detail, __hint, __sqlstate, __schema_name, + __table_name, __column_name, __datatype, __constraint); + END; +END; +$$; + +-- the displayed context is different between Python2 and Python3, +-- but that's not important for this test +\set SHOW_CONTEXT never + +do $$ +try: + plpy.execute("select raise_exception(_message => 'my message', _sqlstate => 'XX987', _hint => 'some hint', _table=> 'users_tab', _datatype => 'user_type')") +except Exception, e: + plpy.info(e.spidata) + raise e +$$ LANGUAGE plpythonu; + +do $$ +try: + plpy.error(message = 'my message', sqlstate = 'XX987', hint = 'some hint', table = 'users_tab', datatype = 'user_type') +except Exception, e: + plpy.info('sqlstate: %s, hint: %s, tablename: %s, datatype: %s' % (e.sqlstate, e.hint, e.table_name, e.datatype_name)) + raise e +$$ LANGUAGE plpythonu; -- 2.40.0