2016-02-25 7:06 GMT+01:00 Catalin Iacob <iacobcata...@gmail.com>: > On Fri, Feb 19, 2016 at 9:41 PM, Pavel Stehule <pavel.steh...@gmail.com> > wrote: > > It looks like good idea. Last version are not breaking compatibility - > and > > I think so it can works. > > > > I wrote the code, that works on Python2 and Python3 > > Hi, > > I've attached a patch on top of yours with some documentation > improvements, feel free to fold it in your next version. >
merged > > I think the concept is good. We're down to code level changes. Most of > them are cosmetical, misleading comments and so on but there are some > bugs in there as far as I see. > > > In object_to_string you don't need to test for Py_None. PyObject_Str > will return NULL on failure and whatever str() returns on the > underlying object. No need to special case None. > > In object_to_string, when you're already in a (so != NULL) block, you > can use Py_DECREF instead of XDECREF. > fixed > > object_to_string seems buggy to me: it returns the pointer returned by > PyString_AsString which points to the internal buffer of the Python > object but it also XDECREFs that object. You seem to be returning a > pointer to freed space. > fixed > > get_string_attr seems to have the same issue as object_to_string. > use pstrdup, but the test on Py_None is necessary PyObject_GetAttrString can returns Py_None, and 3.4's PyString_AsString produce a error "ERROR: could not convert Python Unicode object to bytes" when object is None. So minimally in "get_string_attr" the test on Py_None is necessary > > In PLy_get_error_data query and position will never be set for > plpy.Error since you don't offer them to Python and therefore don't > set them in PLy_output_kw. So I think you should remove the code > related to them, including the char **query, int *position parameters > for PLy_get_error_data. Removing position also allows removing > get_int_attr and the need to exercise this function in the tests. > has sense, removed > > You're using PLy_get_spi_sqlerrcode in PLy_get_error_data which makes > the spi in the name unsuitable. I would rename it to just > PLy_get_sqlerrcode. At least the comment at the top of > PLy_get_spi_sqlerrcode needs to change since it now also extracts info > from Error not just SPIError. > renamed > > /* set value of string pointer on object field */ comments are weird > for a function that gets a value. But those functions need to change > anyway since they're buggy (see above). > fixed > > The only change in plpy_plpymodule.h is the removal of an empty line > unrelated to this patch, probably from previous versions. You should > undo it to leave plpy_plpymodule.h untouched. > fixed > > Why rename PLy_output to PLy_output_kw? It only makes the patch bigger > without being an improvement. Maybe you also have this from previous > versions. > renamed to PLy_output only > > In PLy_output_kw you don't need to check message for NULL, just as sv > wasn't checked before. > It is not necessary, but I am thinking so it better - it is maybe too defensive - but the message can be taken from more sources than in old code, and one check on NULL is correct > In PLy_output_kw you added a FreeErrorData(edata) which didn't exist > before. I'm not familiar with that API but I'm wondering if it's > needed or not, good to have it or not etc. > The previous code used Python memory for message. Now, I used PostgreSQL memory (via pstrdup), so now I have to call FreeErrorData. > > In PLy_output_kw you didn't update the "Note: If sv came from > PyString_AsString(), it points into storage..." comment which still > refers to sv which is now deleted. > I moved Py_XDECREF(so) up, and removed comment > > In PLy_output_kw you removed the "return a legal object so the > interpreter will continue on its merry way" comment which might not be > the most valuable comment in the world but removing it is still > unrelated to this patch. > returned > > In PLy_output_kw you test for kw != NULL && kw != Py_None. The kw != > NULL is definitely needed but I'm quite sure Python will never pass > Py_None into it so the != Py_None isn't needed. Can't find a reference > to prove this at the moment. > cleaned > > > Some more tests could be added to exercise more parts of the patch: > - check that SPIError is enhanced with all the new things: > schema_name, table_name etc. > - in the plpy.error call use every keyword argument not just detail > and hint: it proves you save and restore every field correctly from > the Error fields since that's not exercised by the info call above > which does use every argument > > - use a wrong keyword argument to see it gets rejected with you error > message > - try some other types than str (like detail=42) for error as well > since error goes on another code path than info > - a test exercising the "invalid sqlstate" code > done Sending updated version Regards Pavel
diff --git a/doc/src/sgml/plpython.sgml b/doc/src/sgml/plpython.sgml new file mode 100644 index 015bbad..4ed34d6 *** a/doc/src/sgml/plpython.sgml --- b/doc/src/sgml/plpython.sgml *************** $$ LANGUAGE plpythonu; *** 1341,1353 **** <title>Utility Functions</title> <para> The <literal>plpy</literal> module also provides the functions ! <literal>plpy.debug(<replaceable>msg</>)</literal>, ! <literal>plpy.log(<replaceable>msg</>)</literal>, ! <literal>plpy.info(<replaceable>msg</>)</literal>, ! <literal>plpy.notice(<replaceable>msg</>)</literal>, ! <literal>plpy.warning(<replaceable>msg</>)</literal>, ! <literal>plpy.error(<replaceable>msg</>)</literal>, and ! <literal>plpy.fatal(<replaceable>msg</>)</literal>.<indexterm><primary>elog</><secondary>in PL/Python</></indexterm> <function>plpy.error</function> and <function>plpy.fatal</function> actually raise a Python exception which, if uncaught, propagates out to the calling query, causing --- 1341,1353 ---- <title>Utility Functions</title> <para> The <literal>plpy</literal> module also provides the functions ! <literal>plpy.debug(<replaceable>msg, **kwargs</>)</literal>, ! <literal>plpy.log(<replaceable>msg, **kwargs</>)</literal>, ! <literal>plpy.info(<replaceable>msg, **kwargs</>)</literal>, ! <literal>plpy.notice(<replaceable>msg, **kwargs</>)</literal>, ! <literal>plpy.warning(<replaceable>msg, **kwargs</>)</literal>, ! <literal>plpy.error(<replaceable>msg, **kwargs</>)</literal>, and ! <literal>plpy.fatal(<replaceable>msg, **kwargs</>)</literal>.<indexterm><primary>elog</><secondary>in PL/Python</></indexterm> <function>plpy.error</function> and <function>plpy.fatal</function> actually raise a Python exception which, if uncaught, propagates out to the calling query, causing *************** $$ LANGUAGE plpythonu; *** 1355,1362 **** <literal>raise plpy.Error(<replaceable>msg</>)</literal> and <literal>raise plpy.Fatal(<replaceable>msg</>)</literal> are equivalent to calling ! <function>plpy.error</function> and ! <function>plpy.fatal</function>, respectively. The other functions only generate messages of different priority levels. Whether messages of a particular priority are reported to the client, --- 1355,1363 ---- <literal>raise plpy.Error(<replaceable>msg</>)</literal> and <literal>raise plpy.Fatal(<replaceable>msg</>)</literal> are equivalent to calling ! <literal>plpy.error(<replaceable>msg</>)</literal> and ! <literal>plpy.fatal(<replaceable>msg</>)</literal>, respectively but ! the <literal>raise</literal> 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, *************** $$ LANGUAGE plpythonu; *** 1367,1372 **** --- 1368,1401 ---- </para> <para> + + The <replaceable>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: + <literal><replaceable>detail</replaceable>, <replaceable>hint</replaceable>, <replaceable>sqlstate</replaceable>, <replaceable>schema</replaceable>, <replaceable>table</replaceable>, <replaceable>column</replaceable>, <replaceable>datatype</replaceable> , <replaceable>constraint</replaceable></literal>. + The string representation of the objects passed as keyword-only arguments + is used to enrich the messages reported to the client. For example: + + <programlisting> + 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 + </programlisting> + </para> + + <para> Another set of utility functions are <literal>plpy.quote_literal(<replaceable>string</>)</literal>, <literal>plpy.quote_nullable(<replaceable>string</>)</literal>, and diff --git a/src/pl/plpython/expected/plpython_test.out b/src/pl/plpython/expected/plpython_test.out new file mode 100644 index f8270a7..c385256 *** a/src/pl/plpython/expected/plpython_test.out --- b/src/pl/plpython/expected/plpython_test.out *************** select module_contents(); *** 48,54 **** 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 AS $$ plpy.debug('debug') plpy.log('log') --- 48,54 ---- 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_basic() RETURNS void AS $$ plpy.debug('debug') plpy.log('log') *************** plpy.notice('notice') *** 60,66 **** plpy.warning('warning') plpy.error('error') $$ LANGUAGE plpythonu; ! SELECT elog_test(); INFO: info INFO: 37 INFO: () --- 60,66 ---- plpy.warning('warning') plpy.error('error') $$ LANGUAGE plpythonu; ! SELECT elog_test_basic(); INFO: info INFO: 37 INFO: () *************** NOTICE: notice *** 69,74 **** WARNING: warning ERROR: plpy.Error: error CONTEXT: Traceback (most recent call last): ! PL/Python function "elog_test", line 10, in <module> plpy.error('error') PL/Python function "elog_test" --- 69,255 ---- WARNING: warning ERROR: plpy.Error: error CONTEXT: Traceback (most recent call last): ! PL/Python function "elog_test_basic", line 10, in <module> 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 <module> + 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 + -- raise exception i n 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 <module> + 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 <module> + 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 <module> + 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 <module> + 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 for this test is not important. + \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 as 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 as e: + plpy.info('sqlstate: {0}, hint: {1}, tablename: {2}, datatype: {3}'.format(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 new file mode 100644 index 15406d6..7554840 *** a/src/pl/plpython/plpy_elog.c --- b/src/pl/plpython/plpy_elog.c *************** PyObject *PLy_exc_spi_error = NULL; *** 23,31 **** 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); static char *get_source_line(const char *src, int lineno); /* * Emit a PG error or notice, together with any available info about --- 23,39 ---- 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 **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); + static bool set_int_attr(PyObject *obj, char *attrname, int iv); /* * Emit a PG error or notice, together with any available info about *************** PLy_elog(int elevel, const char *fmt,... *** 51,62 **** char *hint = NULL; char *query = NULL; int position = 0; 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); else if (PyErr_GivenExceptionMatches(val, PLy_exc_fatal)) elevel = FATAL; } --- 59,81 ---- 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, ! &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; } *************** PLy_elog(int elevel, const char *fmt,... *** 103,109 **** (tb_depth > 0 && tbmsg) ? errcontext("%s", tbmsg) : 0, (hint) ? errhint("%s", hint) : 0, (query) ? internalerrquery(query) : 0, ! (position) ? internalerrposition(position) : 0)); } PG_CATCH(); { --- 122,133 ---- (tb_depth > 0 && tbmsg) ? errcontext("%s", tbmsg) : 0, (hint) ? errhint("%s", hint) : 0, (query) ? internalerrquery(query) : 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(); { *************** PLy_traceback(char **xmsg, char **tbmsg, *** 340,346 **** * Extract error code from SPIError's sqlstate attribute. */ static void ! PLy_get_spi_sqlerrcode(PyObject *exc, int *sqlerrcode) { PyObject *sqlstate; char *buffer; --- 364,370 ---- * Extract error code from SPIError's sqlstate attribute. */ static void ! PLy_get_sqlerrcode(PyObject *exc, int *sqlerrcode) { PyObject *sqlstate; char *buffer; *************** PLy_get_spi_sqlerrcode(PyObject *exc, in *** 360,371 **** 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) { PyObject *spidata = NULL; --- 384,397 ---- 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, ! char **schema_name, char **table_name, char **column_name, ! char **datatype_name, char **constraint_name) { PyObject *spidata = NULL; *************** PLy_get_spi_error_data(PyObject *exc, in *** 373,379 **** if (spidata != NULL) { ! PyArg_ParseTuple(spidata, "izzzi", sqlerrcode, detail, hint, query, position); } else { --- 399,407 ---- if (spidata != NULL) { ! PyArg_ParseTuple(spidata, "izzzizzzzz", sqlerrcode, detail, hint, query, position, ! schema_name, table_name, column_name, ! datatype_name, constraint_name); } else { *************** PLy_get_spi_error_data(PyObject *exc, in *** 381,387 **** * 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); } PyErr_Clear(); --- 409,415 ---- * 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_sqlerrcode(exc, sqlerrcode); } PyErr_Clear(); *************** PLy_get_spi_error_data(PyObject *exc, in *** 390,395 **** --- 418,446 ---- } /* + * Extract the error data from an Error. + * Note: position and query fields are not used in Python Error exception ever. + */ + 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 */ static char * *************** PLy_exception_set_plural(PyObject *exc, *** 464,466 **** --- 515,634 ---- PyErr_SetString(exc, buf); } + + /* set exceptions from an 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_int_attr(error, "position", edata->internalpos)) + 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 object field */ + 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 a string field of object, returns true when the change was success */ + 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; + } + + /* same as previous for int */ + static bool + set_int_attr(PyObject *obj, char *attrname, int iv) + { + int result; + PyObject *val; + + val = PyInt_FromLong((long) iv); + if (!val) + return false; + + 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 new file mode 100644 index 94725c2..5dd4ef7 *** a/src/pl/plpython/plpy_elog.h --- b/src/pl/plpython/plpy_elog.h *************** extern void PLy_exception_set(PyObject * *** 17,20 **** --- 17,22 ---- 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 new file mode 100644 index a44b7fb..0ad8611 *** a/src/pl/plpython/plpy_plpymodule.c --- b/src/pl/plpython/plpy_plpymodule.c *************** static void PLy_add_exceptions(PyObject *** 28,40 **** 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_quote_literal(PyObject *self, PyObject *args); static PyObject *PLy_quote_nullable(PyObject *self, PyObject *args); static PyObject *PLy_quote_ident(PyObject *self, PyObject *args); --- 28,40 ---- static void PLy_generate_spi_exceptions(PyObject *mod, PyObject *base); /* module functions */ ! 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); *************** static PyMethodDef PLy_methods[] = { *** 57,69 **** /* * 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}, /* * create a stored plan --- 57,69 ---- /* * logging methods */ ! {"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 *************** PLy_generate_spi_exceptions(PyObject *mo *** 271,318 **** * 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_debug(PyObject *self, PyObject *args) { ! return PLy_output(DEBUG2, self, args); } static PyObject * ! PLy_log(PyObject *self, PyObject *args) { ! return PLy_output(LOG, self, args); } static PyObject * ! PLy_info(PyObject *self, PyObject *args) { ! return PLy_output(INFO, self, args); } static PyObject * ! PLy_notice(PyObject *self, PyObject *args) { ! return PLy_output(NOTICE, self, args); } static PyObject * ! PLy_warning(PyObject *self, PyObject *args) { ! return PLy_output(WARNING, self, args); } static PyObject * ! PLy_error(PyObject *self, PyObject *args) { ! return PLy_output(ERROR, self, args); } static PyObject * ! PLy_fatal(PyObject *self, PyObject *args) { ! return PLy_output(FATAL, self, args); } static PyObject * --- 271,319 ---- * the python interface to the elog function * don't confuse these with PLy_elog */ ! static PyObject *PLy_output(volatile int level, PyObject *self, ! PyObject *args, PyObject *kw); static PyObject * ! PLy_debug(PyObject *self, PyObject *args, PyObject *kw) { ! return PLy_output(DEBUG2, self, args, kw); } static PyObject * ! PLy_log(PyObject *self, PyObject *args, PyObject *kw) { ! return PLy_output(LOG, self, args, kw); } static PyObject * ! PLy_info(PyObject *self, PyObject *args, PyObject *kw) { ! return PLy_output(INFO, self, args, kw); } static PyObject * ! PLy_notice(PyObject *self, PyObject *args, PyObject *kw) { ! return PLy_output(NOTICE, self, args, kw); } static PyObject * ! PLy_warning(PyObject *self, PyObject *args, PyObject *kw) { ! return PLy_output(WARNING, self, args, kw); } static PyObject * ! PLy_error(PyObject *self, PyObject *args, PyObject *kw) { ! return PLy_output(ERROR, self, args, kw); } static PyObject * ! PLy_fatal(PyObject *self, PyObject *args, PyObject *kw) { ! return PLy_output(FATAL, self, args, kw); } static PyObject * *************** PLy_quote_ident(PyObject *self, PyObject *** 368,379 **** return ret; } static PyObject * ! PLy_output(volatile int level, PyObject *self, PyObject *args) { ! PyObject *volatile so; ! char *volatile sv; ! volatile MemoryContext oldcontext; if (PyTuple_Size(args) == 1) { --- 369,413 ---- 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, PyObject *kw) { ! 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) { *************** PLy_output(volatile int level, PyObject *** 389,428 **** } else so = PyObject_Str(args); ! if (so == NULL || ((sv = PyString_AsString(so)) == NULL)) { level = ERROR; ! sv = dgettext(TEXTDOMAIN, "could not parse error message in plpy.elog"); } oldcontext = CurrentMemoryContext; PG_TRY(); { ! pg_verifymbstr(sv, strlen(sv), false); ! elog(level, "%s", sv); } PG_CATCH(); { ! 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); - /* 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 */ --- 423,533 ---- } else so = PyObject_Str(args); ! ! if (so == NULL || ((message = pstrdup(PyString_AsString(so))) == NULL)) { level = ERROR; ! 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) ! 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(); { ! 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; MemoryContextSwitchTo(oldcontext); edata = CopyErrorData(); FlushErrorState(); ! PLy_exception_set_with_details(PLy_exc_error, edata); ! FreeErrorData(edata); return NULL; } PG_END_TRY(); /* * 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 new file mode 100644 index 58e78ec..6cdbd23 *** a/src/pl/plpython/plpy_spi.c --- b/src/pl/plpython/plpy_spi.c *************** PLy_spi_subtransaction_abort(MemoryConte *** 536,543 **** /* 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); exc = entry ? entry->exc : PLy_exc_spi_error; /* Make Python raise the exception */ PLy_spi_exception_set(exc, edata); --- 536,542 ---- /* Look up the correct exception */ entry = hash_search(PLy_spi_exceptions, &(edata->sqlerrcode), HASH_FIND, NULL); ! /* Now, the exception with custom code can come */ exc = entry ? entry->exc : PLy_exc_spi_error; /* Make Python raise the exception */ PLy_spi_exception_set(exc, edata); *************** PLy_spi_exception_set(PyObject *excclass *** 564,571 **** if (!spierror) goto failure; ! spidata = Py_BuildValue("(izzzi)", edata->sqlerrcode, edata->detail, edata->hint, ! edata->internalquery, edata->internalpos); if (!spidata) goto failure; --- 563,572 ---- if (!spierror) goto failure; ! 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 new file mode 100644 index 3a76104..6f59cab *** a/src/pl/plpython/sql/plpython_test.sql --- b/src/pl/plpython/sql/plpython_test.sql *************** $$ LANGUAGE plpythonu; *** 36,43 **** select module_contents(); ! ! CREATE FUNCTION elog_test() RETURNS void AS $$ plpy.debug('debug') plpy.log('log') --- 36,42 ---- select module_contents(); ! CREATE FUNCTION elog_test_basic() RETURNS void AS $$ plpy.debug('debug') plpy.log('log') *************** plpy.warning('warning') *** 50,53 **** --- 49,184 ---- 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; + + -- raise exception i n 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 for this test is not important. + \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 as 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 as e: + plpy.info('sqlstate: {0}, hint: {1}, tablename: {2}, datatype: {3}'.format(e.sqlstate, e.hint, e.table_name, e.datatype_name)) + raise e + $$ LANGUAGE plpythonu;
-- Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-hackers