On 23/12/10 14:56, Jan Urbański wrote: > Here's a patch implementing traceback support for PL/Python mentioned in > http://archives.postgresql.org/pgsql-hackers/2010-12/msg01991.php. It's > an incremental patch on top of the plpython-refactor patch sent eariler.
Updated to master.
diff --git a/src/pl/plpython/expected/plpython_do.out b/src/pl/plpython/expected/plpython_do.out index a21b088..fb0f0e5 100644 *** a/src/pl/plpython/expected/plpython_do.out --- b/src/pl/plpython/expected/plpython_do.out *************** NOTICE: This is plpythonu. *** 3,6 **** --- 3,9 ---- CONTEXT: PL/Python anonymous code block DO $$ nonsense $$ LANGUAGE plpythonu; ERROR: NameError: global name 'nonsense' is not defined + DETAIL: Traceback (most recent call last): + PL/Python anonymous code block, line 1, in <module> + nonsense CONTEXT: PL/Python anonymous code block diff --git a/src/pl/plpython/expected/plpython_error.out b/src/pl/plpython/expected/plpython_error.out index ea4a54c..1e6295e 100644 *** a/src/pl/plpython/expected/plpython_error.out --- b/src/pl/plpython/expected/plpython_error.out *************** CONTEXT: PL/Python function "sql_syntax *** 13,18 **** --- 13,21 ---- ERROR: plpy.SPIError: syntax error at or near "syntax" LINE 1: syntax error ^ + DETAIL: Traceback (most recent call last): + PL/Python function "sql_syntax_error", line 1, in <module> + plpy.execute("syntax error") QUERY: syntax error CONTEXT: PL/Python function "sql_syntax_error" /* check the handling of uncaught python exceptions *************** CREATE FUNCTION exception_index_invalid( *** 23,28 **** --- 26,34 ---- LANGUAGE plpythonu; SELECT exception_index_invalid('test'); ERROR: IndexError: list index out of range + DETAIL: Traceback (most recent call last): + PL/Python function "exception_index_invalid", line 1, in <module> + return args[1] CONTEXT: PL/Python function "exception_index_invalid" /* check handling of nested exceptions */ *************** CONTEXT: PL/Python function "exception_ *** 37,42 **** --- 43,51 ---- ERROR: plpy.SPIError: function test5(unknown) does not exist LINE 1: SELECT test5('foo') ^ + DETAIL: Traceback (most recent call last): + PL/Python function "exception_index_invalid_nested", line 1, in <module> + rv = plpy.execute("SELECT test5('foo')") HINT: No function matches the given name and argument types. You might need to add explicit type casts. QUERY: SELECT test5('foo') CONTEXT: PL/Python function "exception_index_invalid_nested" *************** SELECT invalid_type_uncaught('rick'); *** 57,62 **** --- 66,74 ---- WARNING: plpy.SPIError: unrecognized error in PLy_spi_prepare CONTEXT: PL/Python function "invalid_type_uncaught" ERROR: plpy.SPIError: type "test" does not exist + DETAIL: Traceback (most recent call last): + PL/Python function "invalid_type_uncaught", line 3, in <module> + SD["plan"] = plpy.prepare(q, [ "test" ]) CONTEXT: PL/Python function "invalid_type_uncaught" /* for what it's worth catch the exception generated by * the typo, and return None *************** SELECT invalid_type_reraised('rick'); *** 107,112 **** --- 119,127 ---- WARNING: plpy.SPIError: unrecognized error in PLy_spi_prepare CONTEXT: PL/Python function "invalid_type_reraised" ERROR: plpy.Error: type "test" does not exist + DETAIL: Traceback (most recent call last): + PL/Python function "invalid_type_reraised", line 6, in <module> + plpy.error(str(ex)) CONTEXT: PL/Python function "invalid_type_reraised" /* no typo no messing about */ *************** SELECT valid_type('rick'); *** 126,128 **** --- 141,238 ---- (1 row) + /* error in nested functions to get a traceback + */ + CREATE FUNCTION nested_error() RETURNS text + AS + 'def fun1(): + plpy.error("boom") + + def fun2(): + fun1() + + def fun3(): + fun2() + + fun3() + return "not reached" + ' + LANGUAGE plpythonu; + SELECT nested_error(); + ERROR: plpy.Error: boom + DETAIL: Traceback (most recent call last): + PL/Python function "nested_error", line 10, in <module> + fun3() + PL/Python function "nested_error", line 8, in fun3 + fun2() + PL/Python function "nested_error", line 5, in fun2 + fun1() + PL/Python function "nested_error", line 2, in fun1 + plpy.error("boom") + CONTEXT: PL/Python function "nested_error" + /* raising plpy.Error is just like calling plpy.error + */ + CREATE FUNCTION nested_error_raise() RETURNS text + AS + 'def fun1(): + raise plpy.Error("boom") + + def fun2(): + fun1() + + def fun3(): + fun2() + + fun3() + return "not reached" + ' + LANGUAGE plpythonu; + SELECT nested_error_raise(); + ERROR: plpy.Error: boom + DETAIL: Traceback (most recent call last): + PL/Python function "nested_error_raise", line 10, in <module> + fun3() + PL/Python function "nested_error_raise", line 8, in fun3 + fun2() + PL/Python function "nested_error_raise", line 5, in fun2 + fun1() + PL/Python function "nested_error_raise", line 2, in fun1 + raise plpy.Error("boom") + CONTEXT: PL/Python function "nested_error_raise" + /* using plpy.warning should not produce a traceback + */ + CREATE FUNCTION nested_warning() RETURNS text + AS + 'def fun1(): + plpy.warning("boom") + + def fun2(): + fun1() + + def fun3(): + fun2() + + fun3() + return "you''ve been warned" + ' + LANGUAGE plpythonu; + SELECT nested_warning(); + WARNING: boom + CONTEXT: PL/Python function "nested_warning" + nested_warning + -------------------- + you've been warned + (1 row) + + /* AttirbuteError at toplevel used to give segfaults with the traceback + */ + CREATE FUNCTION toplevel_attribute_error() RETURNS void AS + $$ + plpy.nonexistent + $$ LANGUAGE plpythonu; + SELECT toplevel_attribute_error(); + ERROR: AttributeError: 'module' object has no attribute 'nonexistent' + DETAIL: Traceback (most recent call last): + PL/Python function "toplevel_attribute_error", line 2, in <module> + plpy.nonexistent + CONTEXT: PL/Python function "toplevel_attribute_error" diff --git a/src/pl/plpython/expected/plpython_test.out b/src/pl/plpython/expected/plpython_test.out index d92c987..f3bdb3b 100644 *** a/src/pl/plpython/expected/plpython_test.out --- b/src/pl/plpython/expected/plpython_test.out *************** CONTEXT: PL/Python function "elog_test" *** 74,77 **** --- 74,80 ---- WARNING: warning CONTEXT: PL/Python function "elog_test" ERROR: plpy.Error: error + DETAIL: Traceback (most recent call last): + PL/Python function "elog_test", line 10, in <module> + plpy.error('error') CONTEXT: PL/Python function "elog_test" diff --git a/src/pl/plpython/plpython.c b/src/pl/plpython/plpython.c index aafe556..f2d8bdc 100644 *** a/src/pl/plpython/plpython.c --- b/src/pl/plpython/plpython.c *************** typedef int Py_ssize_t; *** 71,76 **** --- 71,77 ---- */ #if PY_MAJOR_VERSION >= 3 #define PyInt_FromLong(x) PyLong_FromLong(x) + #define PyInt_AsLong(x) PyLong_AsLong(x) #endif /* *************** typedef struct PLyProcedure *** 209,214 **** --- 210,216 ---- * type */ bool is_setof; /* true, if procedure returns result set */ PyObject *setof; /* contents of result set. */ + char *src; /* textual procedure code, after mangling */ char **argnames; /* Argument names */ PLyTypeInfo args[FUNC_MAX_ARGS]; int nargs; *************** static void *** 295,301 **** PLy_elog(int, const char *,...) __attribute__((format(printf, 2, 3))); static void PLy_get_spi_error_data(PyObject *exc, char **detail, char **hint, char **query, int *position); ! static char *PLy_traceback(int *); static void *PLy_malloc(size_t); static void *PLy_malloc0(size_t); --- 297,303 ---- PLy_elog(int, const char *,...) __attribute__((format(printf, 2, 3))); static void PLy_get_spi_error_data(PyObject *exc, char **detail, char **hint, char **query, int *position); ! static void PLy_traceback(char **, char **, int *); static void *PLy_malloc(size_t); static void *PLy_malloc0(size_t); *************** PLy_function_handler(FunctionCallInfo fc *** 1064,1072 **** PLy_function_delete_args(proc); if (has_error) ! ereport(ERROR, ! (errcode(ERRCODE_DATA_EXCEPTION), ! errmsg("error fetching next item from iterator"))); /* Disconnect from the SPI manager before returning */ if (SPI_finish() != SPI_OK_FINISH) --- 1066,1072 ---- PLy_function_delete_args(proc); if (has_error) ! PLy_elog(ERROR, "error fetching next item from iterator"); /* Disconnect from the SPI manager before returning */ if (SPI_finish() != SPI_OK_FINISH) *************** PLy_procedure_create(HeapTuple procTup, *** 1400,1405 **** --- 1400,1406 ---- proc->is_setof = procStruct->proretset; proc->setof = NULL; proc->argnames = NULL; + proc->src = NULL; PG_TRY(); { *************** PLy_procedure_compile(PLyProcedure *proc *** 1580,1585 **** --- 1581,1588 ---- * insert the function code into the interpreter */ msrc = PLy_procedure_munge_source(proc->pyname, src); + /* Save the mangled source for later inclusion in tracebacks */ + proc->src = PLy_strdup(msrc); crv = PyRun_String(msrc, Py_file_input, proc->globals, NULL); pfree(msrc); *************** PLy_procedure_delete(PLyProcedure *proc) *** 1677,1682 **** --- 1680,1687 ---- if (proc->argnames && proc->argnames[i]) PLy_free(proc->argnames[i]); } + if (proc->src) + PLy_free(proc->src); if (proc->argnames) PLy_free(proc->argnames); } *************** PLy_spi_execute_plan(PyObject *ob, PyObj *** 2999,3006 **** PLy_elog(ERROR, "could not execute plan"); sv = PyString_AsString(so); PLy_exception_set_plural(PyExc_TypeError, ! "Expected sequence of %d argument, got %d: %s", ! "Expected sequence of %d arguments, got %d: %s", plan->nargs, plan->nargs, nargs, sv); Py_DECREF(so); --- 3004,3011 ---- PLy_elog(ERROR, "could not execute plan"); sv = PyString_AsString(so); PLy_exception_set_plural(PyExc_TypeError, ! "Expected sequence of %d argument, got %d: %s", ! "Expected sequence of %d arguments, got %d: %s", plan->nargs, plan->nargs, nargs, sv); Py_DECREF(so); *************** failure: *** 3573,3645 **** * the current Python error, previously set by PLy_exception_set(). * This should be used to propagate Python errors into PG. If fmt is * NULL, the Python error becomes the primary error message, otherwise ! * it becomes the detail. */ static void PLy_elog(int elevel, const char *fmt,...) { - char *xmsg; - int xlevel; - StringInfoData emsg; PyObject *exc, *val, *tb; char *detail = NULL; char *hint = NULL; char *query = NULL; int position = 0; PyErr_Fetch(&exc, &val, &tb); if (exc != NULL && PyErr_GivenExceptionMatches(val, PLy_exc_spi_error)) PLy_get_spi_error_data(val, &detail, &hint, &query, &position); PyErr_Restore(exc, val, tb); ! xmsg = PLy_traceback(&xlevel); if (fmt) { ! initStringInfo(&emsg); for (;;) { va_list ap; bool success; va_start(ap, fmt); ! success = appendStringInfoVA(&emsg, dgettext(TEXTDOMAIN, fmt), ap); va_end(ap); if (success) break; ! enlargeStringInfo(&emsg, emsg.maxlen); } } PG_TRY(); { if (fmt) ! ereport(elevel, ! (errmsg("%s", emsg.data), ! (xmsg) ? errdetail("%s", xmsg) : 0, ! (hint) ? errhint("%s", hint) : 0, ! (query) ? internalerrquery(query) : 0, ! (position) ? internalerrposition(position) : 0)); else ! ereport(elevel, ! (errmsg("%s", xmsg), ! (detail) ? errdetail("%s", detail) : 0, ! (hint) ? errhint("%s", hint) : 0, ! (query) ? internalerrquery(query) : 0, ! (position) ? internalerrposition(position) : 0)); } PG_CATCH(); { ! if (fmt) ! pfree(emsg.data); if (xmsg) pfree(xmsg); PG_RE_THROW(); } PG_END_TRY(); ! if (fmt) ! pfree(emsg.data); if (xmsg) pfree(xmsg); } --- 3578,3693 ---- * the current Python error, previously set by PLy_exception_set(). * This should be used to propagate Python errors into PG. If fmt is * NULL, the Python error becomes the primary error message, otherwise ! * it becomes the detail. If there is a Python traceback, it is also put ! * in the detail. */ static void PLy_elog(int elevel, const char *fmt,...) { PyObject *exc, *val, *tb; char *detail = NULL; char *hint = NULL; char *query = NULL; int position = 0; + char *fmsg = NULL; + char *emsg = NULL; + char *xmsg = NULL; + int tb_depth = 0; PyErr_Fetch(&exc, &val, &tb); if (exc != NULL && PyErr_GivenExceptionMatches(val, PLy_exc_spi_error)) PLy_get_spi_error_data(val, &detail, &hint, &query, &position); PyErr_Restore(exc, val, tb); ! /* this is a no-op if there is no current Python exception */ ! PLy_traceback(&emsg, &xmsg, &tb_depth); if (fmt) { ! StringInfoData si; ! initStringInfo(&si); for (;;) { va_list ap; bool success; va_start(ap, fmt); ! success = appendStringInfoVA(&si, dgettext(TEXTDOMAIN, fmt), ap); va_end(ap); if (success) break; ! enlargeStringInfo(&si, si.maxlen); } + fmsg = si.data; } PG_TRY(); { + /* If we have a format string, it should be the main error message and + * the emsg + traceback the detailed message. + * + * If we don't have fmsg, we should use emsg as the main error message + * (and failing that just say "no exception data") and put the + * traceback in the detail. + * + * The traceback is present if tb_depth > 0. + */ if (fmt) ! { ! if (tb_depth > 0) ! { ! ereport(elevel, ! (errmsg("%s", fmsg), ! (emsg) ? errdetail("%s\n%s", emsg, xmsg) : 0, ! (hint) ? errhint("%s", hint) : 0, ! (query) ? internalerrquery(query) : 0, ! (position) ? internalerrposition(position) : 0)); ! } ! else ! { ! ereport(elevel, ! (errmsg("%s", fmsg), ! (emsg) ? errdetail("%s", emsg) : 0, ! (hint) ? errhint("%s", hint) : 0, ! (query) ? internalerrquery(query) : 0, ! (position) ? internalerrposition(position) : 0)); ! } ! } else ! { ! if (tb_depth > 0) ! { ! ereport(elevel, ! (errmsg("%s", emsg ? emsg : "no exception data"), ! (detail) ? errdetail("%s\n%s", detail, xmsg) : errdetail("%s", xmsg), ! (hint) ? errhint("%s", hint) : 0, ! (query) ? internalerrquery(query) : 0, ! (position) ? internalerrposition(position) : 0)); ! } ! else ! { ! ereport(elevel, ! (errmsg("%s", emsg ? emsg : "no exception data"), ! (detail) ? errdetail("%s", detail) : 0, ! (hint) ? errhint("%s", hint) : 0, ! (query) ? internalerrquery(query) : 0, ! (position) ? internalerrposition(position) : 0)); ! } ! } } PG_CATCH(); { ! if (fmsg) ! pfree(fmsg); ! if (emsg) ! pfree(emsg); if (xmsg) pfree(xmsg); PG_RE_THROW(); } PG_END_TRY(); ! pfree(emsg); if (xmsg) pfree(xmsg); } *************** cleanup: *** 3666,3673 **** } static char * ! PLy_traceback(int *xlevel) { PyObject *e, *v, --- 3714,3757 ---- } + /* Get the given source line as a palloc'd string */ static char * ! get_source_line(char *src, int lineno) ! { ! char *s; ! char *next; ! int current = 0; ! ! next = src; ! while (current != lineno) ! { ! s = next; ! next = strchr(s + 1, '\n'); ! current++; ! if (next == NULL) ! break; ! } ! ! if (current != lineno) ! return NULL; ! ! while (s && isspace(*s)) ! s++; ! ! if (next == NULL) ! return pstrdup(s); ! ! return pnstrdup(s, next - s); ! } ! ! /* ! * Extract a Python traceback from the current exception. ! * ! * The exception error message is returned in emsg, the traceback in xmsg (both ! * as palloc's strings) and the traceback depth in tb_depth. ! */ ! static void ! PLy_traceback(char **emsg, char **xmsg, int *tb_depth) { PyObject *e, *v, *************** PLy_traceback(int *xlevel) *** 3678,3683 **** --- 3762,3768 ---- char *e_module_s = NULL; PyObject *vob = NULL; char *vstr; + StringInfoData estr; StringInfoData xstr; /* *************** PLy_traceback(int *xlevel) *** 3689,3701 **** * oops, no exception, return */ if (e == NULL) ! { ! *xlevel = WARNING; ! return NULL; ! } PyErr_NormalizeException(&e, &v, &tb); ! Py_XDECREF(tb); e_type_o = PyObject_GetAttrString(e, "__name__"); e_module_o = PyObject_GetAttrString(e, "__module__"); --- 3774,3784 ---- * oops, no exception, return */ if (e == NULL) ! return; PyErr_NormalizeException(&e, &v, &tb); ! ! /* format the exception and its value and put it in emsg */ e_type_o = PyObject_GetAttrString(e, "__name__"); e_module_o = PyObject_GetAttrString(e, "__module__"); *************** PLy_traceback(int *xlevel) *** 3709,3750 **** else vstr = "unknown"; ! initStringInfo(&xstr); if (!e_type_s || !e_module_s) { if (PyString_Check(e)) /* deprecated string exceptions */ ! appendStringInfoString(&xstr, PyString_AsString(e)); else /* shouldn't happen */ ! appendStringInfoString(&xstr, "unrecognized exception"); } /* mimics behavior of traceback.format_exception_only */ else if (strcmp(e_module_s, "builtins") == 0 || strcmp(e_module_s, "__main__") == 0 || strcmp(e_module_s, "exceptions") == 0) ! appendStringInfo(&xstr, "%s", e_type_s); else ! appendStringInfo(&xstr, "%s.%s", e_module_s, e_type_s); ! appendStringInfo(&xstr, ": %s", vstr); Py_XDECREF(e_type_o); Py_XDECREF(e_module_o); Py_XDECREF(vob); Py_XDECREF(v); - - /* - * intuit an appropriate error level based on the exception type - */ - if (PLy_exc_error && PyErr_GivenExceptionMatches(e, PLy_exc_error)) - *xlevel = ERROR; - else if (PLy_exc_fatal && PyErr_GivenExceptionMatches(e, PLy_exc_fatal)) - *xlevel = FATAL; - else - *xlevel = ERROR; - Py_DECREF(e); - return xstr.data; } /* python module code */ --- 3792,3929 ---- else vstr = "unknown"; ! initStringInfo(&estr); if (!e_type_s || !e_module_s) { if (PyString_Check(e)) /* deprecated string exceptions */ ! appendStringInfoString(&estr, PyString_AsString(e)); else /* shouldn't happen */ ! appendStringInfoString(&estr, "unrecognized exception"); } /* mimics behavior of traceback.format_exception_only */ else if (strcmp(e_module_s, "builtins") == 0 || strcmp(e_module_s, "__main__") == 0 || strcmp(e_module_s, "exceptions") == 0) ! appendStringInfo(&estr, "%s", e_type_s); else ! appendStringInfo(&estr, "%s.%s", e_module_s, e_type_s); ! appendStringInfo(&estr, ": %s", vstr); ! ! *emsg = estr.data; ! ! /* now format the traceback and put it in xmsg */ ! *tb_depth = 0; ! initStringInfo(&xstr); ! /* Mimick Python traceback reporting as close as possible */ ! appendStringInfoString(&xstr, "Traceback (most recent call last):"); ! while (tb != NULL && tb != Py_None) ! { ! PyObject *volatile tb_prev = NULL; ! PyObject *volatile frame = NULL; ! PyObject *volatile code = NULL; ! PyObject *volatile name = NULL; ! PyObject *volatile lineno = NULL; ! ! PG_TRY(); ! { ! lineno = PyObject_GetAttrString(tb, "tb_lineno"); ! if (lineno == NULL) ! elog(ERROR, "could not get line number from Python traceback"); ! ! frame = PyObject_GetAttrString(tb, "tb_frame"); ! if (frame == NULL) ! elog(ERROR, "could not get frame from Python traceback"); ! ! code = PyObject_GetAttrString(frame, "f_code"); ! if (code == NULL) ! elog(ERROR, "could not get code object from Python frame"); ! ! name = PyObject_GetAttrString(code, "co_name"); ! if (name == NULL) ! elog(ERROR, "could not get function name from Python code object"); ! } ! PG_CATCH(); ! { ! Py_XDECREF(frame); ! Py_XDECREF(code); ! Py_XDECREF(name); ! Py_XDECREF(lineno); ! PG_RE_THROW(); ! } ! PG_END_TRY(); ! ! /* The first frame always points at <module>, skip it */ ! if (*tb_depth > 0) ! { ! char *proname; ! char *fname; ! char *line; ! long plain_lineno; ! ! /* ! * The second frame points at the internal function, but to mimick ! * Python error reporting we want to say <module> ! */ ! if (*tb_depth == 1) ! fname = "<module>"; ! else ! fname = PyString_AsString(name); ! ! proname = PLy_procedure_name(PLy_curr_procedure); ! plain_lineno = PyInt_AsLong(lineno); ! ! if (proname == NULL) ! appendStringInfo( ! &xstr, "\n PL/Python anonymous code block, line %ld, in %s", ! plain_lineno - 1, fname); ! else ! appendStringInfo( ! &xstr, "\n PL/Python function \"%s\", line %ld, in %s", ! proname, plain_lineno - 1, fname); ! ! if (PLy_curr_procedure) ! { ! /* ! * If we know the current procedure, append the exact line from ! * the source, again mimicking Python's traceback.py module ! * behaviour. We could store the already line-splitted source ! * to avoid splitting it every time, but producing a traceback ! * is not the most important scenario to optimise for. ! */ ! line = get_source_line(PLy_curr_procedure->src, plain_lineno); ! if (line != NULL) ! { ! appendStringInfo(&xstr, "\n %s", line); ! pfree(line); ! } ! } ! } ! ! Py_DECREF(frame); ! Py_DECREF(code); ! Py_DECREF(name); ! Py_DECREF(lineno); ! ! /* Release the current frame and go to the next one */ ! tb_prev = tb; ! tb = PyObject_GetAttrString(tb, "tb_next"); ! Assert(tb_prev != Py_None); ! Py_DECREF(tb_prev); ! if (tb == NULL) ! elog(ERROR, "could not traverse Python traceback"); ! (*tb_depth)++; ! } ! ! /* Return the traceback */ ! *xmsg = xstr.data; Py_XDECREF(e_type_o); Py_XDECREF(e_module_o); Py_XDECREF(vob); Py_XDECREF(v); Py_DECREF(e); } /* python module code */ diff --git a/src/pl/plpython/sql/plpython_error.sql b/src/pl/plpython/sql/plpython_error.sql index 5ca6849..e9b819c 100644 *** a/src/pl/plpython/sql/plpython_error.sql --- b/src/pl/plpython/sql/plpython_error.sql *************** return None *** 107,109 **** --- 107,178 ---- LANGUAGE plpythonu; SELECT valid_type('rick'); + + /* error in nested functions to get a traceback + */ + CREATE FUNCTION nested_error() RETURNS text + AS + 'def fun1(): + plpy.error("boom") + + def fun2(): + fun1() + + def fun3(): + fun2() + + fun3() + return "not reached" + ' + LANGUAGE plpythonu; + + SELECT nested_error(); + + /* raising plpy.Error is just like calling plpy.error + */ + CREATE FUNCTION nested_error_raise() RETURNS text + AS + 'def fun1(): + raise plpy.Error("boom") + + def fun2(): + fun1() + + def fun3(): + fun2() + + fun3() + return "not reached" + ' + LANGUAGE plpythonu; + + SELECT nested_error_raise(); + + /* using plpy.warning should not produce a traceback + */ + CREATE FUNCTION nested_warning() RETURNS text + AS + 'def fun1(): + plpy.warning("boom") + + def fun2(): + fun1() + + def fun3(): + fun2() + + fun3() + return "you''ve been warned" + ' + LANGUAGE plpythonu; + + SELECT nested_warning(); + + /* AttirbuteError at toplevel used to give segfaults with the traceback + */ + CREATE FUNCTION toplevel_attribute_error() RETURNS void AS + $$ + plpy.nonexistent + $$ LANGUAGE plpythonu; + + SELECT toplevel_attribute_error();
-- Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-hackers