mirror of
https://git.postgresql.org/git/postgresql.git
synced 2024-10-01 14:52:08 +02:00
9b7e24a2cb
Since 19252e8ec9
we reject Python 2 during build configuration. Now that the
dust on the buildfarm has settled, remove Python 2 specific code, including
the "Python 2/3 porting layer".
The code to detect conflicts between plpython using Python 2 and 3 is not
removed, in case somebody creates an out-of-tree version adding back support
for Python 2.
Reviewed-By: Peter Eisentraut <peter@eisentraut.org>
Reviewed-By: Tom Lane <tgl@sss.pgh.pa.us>
Discussion: https://postgr.es/m/20211031184548.g4sxfe47n2kyi55r@alap3.anarazel.de
603 lines
15 KiB
C
603 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;
|
|
|
|
/* Since we have a format string, we cannot have a SPI detail. */
|
|
Assert(detail == NULL);
|
|
|
|
/* 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;
|
|
}
|