Enhanced custom error in PLPythonu
authorTeodor Sigaev
Fri, 8 Apr 2016 15:30:25 +0000 (18:30 +0300)
committerTeodor Sigaev
Fri, 8 Apr 2016 15:33:06 +0000 (18:33 +0300)
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
src/pl/plpython/expected/plpython_test.out
src/pl/plpython/plpy_elog.c
src/pl/plpython/plpy_elog.h
src/pl/plpython/plpy_plpymodule.c
src/pl/plpython/plpy_spi.c
src/pl/plpython/sql/plpython_test.sql

index 015bbad8ddaa976888c957837646e02265313cbb..cff66a2838ad9c2a2fecead665372ff0f4f4f195 100644 (file)
@@ -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
-   <function>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).
+   <indexterm>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:
+   
+   detailhint,
+   sqlstateschema,
+   tablecolumn,
+   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),
index f8270a7c5adfcd703f17034cde977db11f58934c..05caba17870077a873632a916b6c015f5ac6a04b 100644 (file)
@@ -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
index 15406d60b9012a5e48ae99e802b8c10d567fe46c..8e8db5d9acd0a4c4a022ea1ce5d570c7d618fde9 100644 (file)
@@ -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;
+}
+
index 94725c2a2e421239e86af1bf3832bd12bd240230..5dd4ef7a1465a585673053fedec91bb93aae55f0 100644 (file)
@@ -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 */
index a44b7fbe9a397fc55649b9d874e1b6e288391360..f136e8ece8bef416c55e95e61353ee8b0ebe677a 100644 (file)
@@ -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
     */
index 7d84629f48fcebaa1aaa98fcc7a3317704c97550..f50d6545e01849f971e0001d443b6e8f878dbe51 100644 (file)
@@ -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;
 
index 3a761047a09119ce60f9d35f3a68cb5b60bbed44..6e5b535ceb7fc22756371abadeae501409e3f45a 100644 (file)
@@ -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;