summaryrefslogtreecommitdiffstats
path: root/src/pl/plpython/plpy_elog.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:17:33 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:17:33 +0000
commit5e45211a64149b3c659b90ff2de6fa982a5a93ed (patch)
tree739caf8c461053357daa9f162bef34516c7bf452 /src/pl/plpython/plpy_elog.c
parentInitial commit. (diff)
downloadpostgresql-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.c599
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;
+}