mirror of
https://github.com/postgres/postgres.git
synced 2026-03-16 07:32:54 -04:00
PLy_elog() was not able to handle correctly cases where a SPI called
failed, which would fill in a DETAIL string able to trigger an
assertion. We may want to improve this infrastructure so as it is able
to provide any extra detail information provided by an error stack, but
this is left as a future improvement as it could impact existing error
stacks and any applications that depend on them. For now, the assertion
is removed and a regression test is added to cover the case of a failure
with a detail string.
This problem exists since 2bd78eb8d5, so backpatch all the way down
with tweaks to the regression tests output added where required.
Author: Alexander Lakhin
Discussion: https://postgr.es/m/18070-ab9c171cbf4ebb0f@postgresql.org
Backpatch-through: 11
599 lines
15 KiB
C
599 lines
15 KiB
C
/*
|
|
* reporting Python exceptions as PostgreSQL errors
|
|
*
|
|
* src/pl/plpython/plpy_elog.c
|
|
*/
|
|
|
|
#include "postgres.h"
|
|
|
|
#include "lib/stringinfo.h"
|
|
#include "plpy_elog.h"
|
|
#include "plpy_main.h"
|
|
#include "plpy_procedure.h"
|
|
#include "plpython.h"
|
|
|
|
PyObject *PLy_exc_error = NULL;
|
|
PyObject *PLy_exc_fatal = NULL;
|
|
PyObject *PLy_exc_spi_error = NULL;
|
|
|
|
|
|
static void PLy_traceback(PyObject *e, PyObject *v, PyObject *tb,
|
|
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);
|
|
|
|
/*
|
|
* Emit a PG error or notice, together with any available info about
|
|
* 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 put
|
|
* in the context.
|
|
*/
|
|
void
|
|
PLy_elog_impl(int elevel, const char *fmt,...)
|
|
{
|
|
int save_errno = errno;
|
|
char *xmsg;
|
|
char *tbmsg;
|
|
int tb_depth;
|
|
StringInfoData emsg;
|
|
PyObject *exc,
|
|
*val,
|
|
*tb;
|
|
const char *primary = NULL;
|
|
int sqlerrcode = 0;
|
|
char *detail = NULL;
|
|
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)
|
|
{
|
|
PyErr_NormalizeException(&exc, &val, &tb);
|
|
|
|
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;
|
|
}
|
|
|
|
/* this releases our refcount on tb! */
|
|
PLy_traceback(exc, val, tb,
|
|
&xmsg, &tbmsg, &tb_depth);
|
|
|
|
if (fmt)
|
|
{
|
|
initStringInfo(&emsg);
|
|
for (;;)
|
|
{
|
|
va_list ap;
|
|
int needed;
|
|
|
|
errno = save_errno;
|
|
va_start(ap, fmt);
|
|
needed = appendStringInfoVA(&emsg, dgettext(TEXTDOMAIN, fmt), ap);
|
|
va_end(ap);
|
|
if (needed == 0)
|
|
break;
|
|
enlargeStringInfo(&emsg, needed);
|
|
}
|
|
primary = emsg.data;
|
|
|
|
/* If there's an exception message, it goes in the detail. */
|
|
if (xmsg)
|
|
detail = xmsg;
|
|
}
|
|
else
|
|
{
|
|
if (xmsg)
|
|
primary = xmsg;
|
|
}
|
|
|
|
PG_TRY();
|
|
{
|
|
ereport(elevel,
|
|
(errcode(sqlerrcode ? sqlerrcode : ERRCODE_EXTERNAL_ROUTINE_EXCEPTION),
|
|
errmsg_internal("%s", primary ? primary : "no exception data"),
|
|
(detail) ? errdetail_internal("%s", detail) : 0,
|
|
(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_FINALLY();
|
|
{
|
|
if (fmt)
|
|
pfree(emsg.data);
|
|
if (xmsg)
|
|
pfree(xmsg);
|
|
if (tbmsg)
|
|
pfree(tbmsg);
|
|
Py_XDECREF(exc);
|
|
Py_XDECREF(val);
|
|
}
|
|
PG_END_TRY();
|
|
}
|
|
|
|
/*
|
|
* Extract a Python traceback from the given exception data.
|
|
*
|
|
* The exception error message is returned in xmsg, the traceback in
|
|
* tbmsg (both as palloc'd strings) and the traceback depth in
|
|
* tb_depth.
|
|
*
|
|
* We release refcounts on all the Python objects in the traceback stack,
|
|
* but not on e or v.
|
|
*/
|
|
static void
|
|
PLy_traceback(PyObject *e, PyObject *v, PyObject *tb,
|
|
char **xmsg, char **tbmsg, int *tb_depth)
|
|
{
|
|
PyObject *e_type_o;
|
|
PyObject *e_module_o;
|
|
char *e_type_s = NULL;
|
|
char *e_module_s = NULL;
|
|
PyObject *vob = NULL;
|
|
char *vstr;
|
|
StringInfoData xstr;
|
|
StringInfoData tbstr;
|
|
|
|
/*
|
|
* if no exception, return nulls
|
|
*/
|
|
if (e == NULL)
|
|
{
|
|
*xmsg = NULL;
|
|
*tbmsg = NULL;
|
|
*tb_depth = 0;
|
|
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Format the exception and its value and put it in xmsg.
|
|
*/
|
|
|
|
e_type_o = PyObject_GetAttrString(e, "__name__");
|
|
e_module_o = PyObject_GetAttrString(e, "__module__");
|
|
if (e_type_o)
|
|
e_type_s = PLyUnicode_AsString(e_type_o);
|
|
if (e_type_s)
|
|
e_module_s = PLyUnicode_AsString(e_module_o);
|
|
|
|
if (v && ((vob = PyObject_Str(v)) != NULL))
|
|
vstr = PLyUnicode_AsString(vob);
|
|
else
|
|
vstr = "unknown";
|
|
|
|
initStringInfo(&xstr);
|
|
if (!e_type_s || !e_module_s)
|
|
{
|
|
/* 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)
|
|
appendStringInfoString(&xstr, e_type_s);
|
|
else
|
|
appendStringInfo(&xstr, "%s.%s", e_module_s, e_type_s);
|
|
appendStringInfo(&xstr, ": %s", vstr);
|
|
|
|
*xmsg = xstr.data;
|
|
|
|
/*
|
|
* Now format the traceback and put it in tbmsg.
|
|
*/
|
|
|
|
*tb_depth = 0;
|
|
initStringInfo(&tbstr);
|
|
/* Mimic Python traceback reporting as close as possible. */
|
|
appendStringInfoString(&tbstr, "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;
|
|
PyObject *volatile filename = 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");
|
|
|
|
filename = PyObject_GetAttrString(code, "co_filename");
|
|
if (filename == NULL)
|
|
elog(ERROR, "could not get file name from Python code object");
|
|
}
|
|
PG_CATCH();
|
|
{
|
|
Py_XDECREF(frame);
|
|
Py_XDECREF(code);
|
|
Py_XDECREF(name);
|
|
Py_XDECREF(lineno);
|
|
Py_XDECREF(filename);
|
|
PG_RE_THROW();
|
|
}
|
|
PG_END_TRY();
|
|
|
|
/* The first frame always points at <module>, skip it. */
|
|
if (*tb_depth > 0)
|
|
{
|
|
PLyExecutionContext *exec_ctx = PLy_current_execution_context();
|
|
char *proname;
|
|
char *fname;
|
|
char *line;
|
|
char *plain_filename;
|
|
long plain_lineno;
|
|
|
|
/*
|
|
* The second frame points at the internal function, but to mimic
|
|
* Python error reporting we want to say <module>.
|
|
*/
|
|
if (*tb_depth == 1)
|
|
fname = "<module>";
|
|
else
|
|
fname = PLyUnicode_AsString(name);
|
|
|
|
proname = PLy_procedure_name(exec_ctx->curr_proc);
|
|
plain_filename = PLyUnicode_AsString(filename);
|
|
plain_lineno = PyLong_AsLong(lineno);
|
|
|
|
if (proname == NULL)
|
|
appendStringInfo(&tbstr, "\n PL/Python anonymous code block, line %ld, in %s",
|
|
plain_lineno - 1, fname);
|
|
else
|
|
appendStringInfo(&tbstr, "\n PL/Python function \"%s\", line %ld, in %s",
|
|
proname, plain_lineno - 1, fname);
|
|
|
|
/*
|
|
* function code object was compiled with "<string>" as the
|
|
* filename
|
|
*/
|
|
if (exec_ctx->curr_proc && plain_filename != NULL &&
|
|
strcmp(plain_filename, "<string>") == 0)
|
|
{
|
|
/*
|
|
* If we know the current procedure, append the exact line
|
|
* from the source, again mimicking Python's traceback.py
|
|
* module behavior. We could store the already line-split
|
|
* source to avoid splitting it every time, but producing a
|
|
* traceback is not the most important scenario to optimize
|
|
* for. But we do not go as far as traceback.py in reading
|
|
* the source of imported modules.
|
|
*/
|
|
line = get_source_line(exec_ctx->curr_proc->src, plain_lineno);
|
|
if (line)
|
|
{
|
|
appendStringInfo(&tbstr, "\n %s", line);
|
|
pfree(line);
|
|
}
|
|
}
|
|
}
|
|
|
|
Py_DECREF(frame);
|
|
Py_DECREF(code);
|
|
Py_DECREF(name);
|
|
Py_DECREF(lineno);
|
|
Py_DECREF(filename);
|
|
|
|
/* 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. */
|
|
*tbmsg = tbstr.data;
|
|
|
|
Py_XDECREF(e_type_o);
|
|
Py_XDECREF(e_module_o);
|
|
Py_XDECREF(vob);
|
|
}
|
|
|
|
/*
|
|
* Extract error code from SPIError's sqlstate attribute.
|
|
*/
|
|
static void
|
|
PLy_get_sqlerrcode(PyObject *exc, int *sqlerrcode)
|
|
{
|
|
PyObject *sqlstate;
|
|
char *buffer;
|
|
|
|
sqlstate = PyObject_GetAttrString(exc, "sqlstate");
|
|
if (sqlstate == NULL)
|
|
return;
|
|
|
|
buffer = PLyUnicode_AsString(sqlstate);
|
|
if (strlen(buffer) == 5 &&
|
|
strspn(buffer, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") == 5)
|
|
{
|
|
*sqlerrcode = MAKE_SQLSTATE(buffer[0], buffer[1], buffer[2],
|
|
buffer[3], buffer[4]);
|
|
}
|
|
|
|
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;
|
|
|
|
spidata = PyObject_GetAttrString(exc, "spidata");
|
|
|
|
if (spidata != NULL)
|
|
{
|
|
PyArg_ParseTuple(spidata, "izzzizzzzz",
|
|
sqlerrcode, detail, hint, query, position,
|
|
schema_name, table_name, column_name,
|
|
datatype_name, constraint_name);
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* 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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/*
|
|
* Get the given source line as a palloc'd string
|
|
*/
|
|
static char *
|
|
get_source_line(const char *src, int lineno)
|
|
{
|
|
const char *s = NULL;
|
|
const char *next = src;
|
|
int current = 0;
|
|
|
|
/* sanity check */
|
|
if (lineno <= 0)
|
|
return NULL;
|
|
|
|
while (current < lineno)
|
|
{
|
|
s = next;
|
|
next = strchr(s + 1, '\n');
|
|
current++;
|
|
if (next == NULL)
|
|
break;
|
|
}
|
|
|
|
if (current != lineno)
|
|
return NULL;
|
|
|
|
while (*s && isspace((unsigned char) *s))
|
|
s++;
|
|
|
|
if (next == NULL)
|
|
return pstrdup(s);
|
|
|
|
/*
|
|
* Sanity check, next < s if the line was all-whitespace, which should
|
|
* never happen if Python reported a frame created on that line, but check
|
|
* anyway.
|
|
*/
|
|
if (next < s)
|
|
return NULL;
|
|
|
|
return pnstrdup(s, next - s);
|
|
}
|
|
|
|
|
|
/* call PyErr_SetString with a vprint interface and translation support */
|
|
void
|
|
PLy_exception_set(PyObject *exc, const char *fmt,...)
|
|
{
|
|
char buf[1024];
|
|
va_list ap;
|
|
|
|
va_start(ap, fmt);
|
|
vsnprintf(buf, sizeof(buf), dgettext(TEXTDOMAIN, fmt), ap);
|
|
va_end(ap);
|
|
|
|
PyErr_SetString(exc, buf);
|
|
}
|
|
|
|
/* same, with pluralized message */
|
|
void
|
|
PLy_exception_set_plural(PyObject *exc,
|
|
const char *fmt_singular, const char *fmt_plural,
|
|
unsigned long n,...)
|
|
{
|
|
char buf[1024];
|
|
va_list ap;
|
|
|
|
va_start(ap, n);
|
|
vsnprintf(buf, sizeof(buf),
|
|
dngettext(TEXTDOMAIN, fmt_singular, fmt_plural, n),
|
|
ap);
|
|
va_end(ap);
|
|
|
|
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(PLyUnicode_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 = PLyUnicode_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;
|
|
}
|