diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:17:33 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:17:33 +0000 |
commit | 5e45211a64149b3c659b90ff2de6fa982a5a93ed (patch) | |
tree | 739caf8c461053357daa9f162bef34516c7bf452 /src/pl/plpython/plpy_elog.c | |
parent | Initial commit. (diff) | |
download | postgresql-15-5e45211a64149b3c659b90ff2de6fa982a5a93ed.tar.xz postgresql-15-5e45211a64149b3c659b90ff2de6fa982a5a93ed.zip |
Adding upstream version 15.5.upstream/15.5
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/pl/plpython/plpy_elog.c')
-rw-r--r-- | src/pl/plpython/plpy_elog.c | 599 |
1 files changed, 599 insertions, 0 deletions
diff --git a/src/pl/plpython/plpy_elog.c b/src/pl/plpython/plpy_elog.c new file mode 100644 index 0000000..70de5ba --- /dev/null +++ b/src/pl/plpython/plpy_elog.c @@ -0,0 +1,599 @@ +/* + * 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; +} |