Subject: Re: Use Python "Limited API" in PL/Python

From: Peter Eisentraut <[email-masked]>
Date: Tue, 7 Jan 2025 08:34:39 +0100

On 02.12.24 09:51, Peter Eisentraut wrote:
> This patch changes PL/Python to use the Python "limited API". This API 
> has stronger ABI stability guarantees.[0] This means, you can build PL/ 
> Python against any Python 3.x version and use any other Python 3.x 
> version at run time.
> 
> This is especially useful for binary packages where the operating system 
> does not come with a fixed suitable version of Python. For example, 
> Postgres.app (for macOS) would prefer to link against the Python version 
> supplied by python.org (Python.app). But that has a 3.x version that 
> changes over time. So instead they bundle a Python version inside 
> Postgres.app. The Windows installer used to also bundle Python but as of 
> PG17 you have to get it yourself, but you have to get a very specific 
> version [1], which is unsatisfactory. This patch fixes that: You can use 
> any Python version independent of what PL/Python was built against. 
> (There is a mechanism to say "at least 3.N", but for this patch, we 
> don't need that, we can stick with the current minimum of 3.2.)
> 
> (I have only tested the macOS side of this, not the Windows side. In 
> fact, the patch currently doesn't build on Windows on CI. I haven't 
> figured out why.)

A bit more exploration of that Windows build failure:

This patch changes it so that, on Windows, plpython is linked against a 
library called "python3.lib" instead of previously "python311.lib" or 
similar.  The build failure is [0]

     LINK : fatal error LNK1104: cannot open file 'python3.lib'

[0]: https://cirrus-ci.com/task/5738515637469184

which suggests that the file isn't there.  But we have tested this 
internally and the build succeeded locally.  I also don't see anything 
in the Python installer code or documentation that suggests that there 
is an option not to install that file or something like that.  It should 
be installed right next to the python3XX.lib file.

How could one debug this?  Is there a way to log into the CI image or 
get a directory dump or something like that?

---

From: Jakob Egger <[email-masked]>
Date: Tue, 14 Jan 2025 16:51:05 +0100

On 07.01.2025, at 08:34, Peter Eisentraut <[email-masked]> wrote:
>> This patch changes PL/Python to use the Python "limited API". This API has stronger ABI stability guarantees.[0] This means, you can build PL/ Python against any Python 3.x version and use any other Python 3.x version at run time.
>> This is especially useful for binary packages where the operating system does not come with a fixed suitable version of Python. For example, Postgres.app (for macOS) would prefer to link against the Python version supplied by python.org (Python.app). But that has a 3.x version that changes over time. So instead they bundle a Python version inside Postgres.app. The Windows installer used to also bundle Python but as of PG17 you have to get it yourself, but you have to get a very specific version [1], which is unsatisfactory. This patch fixes that: You can use any Python version independent of what PL/Python was built against. (There is a mechanism to say "at least 3.N", but for this patch, we don't need that, we can stick with the current minimum of 3.2.)
>> (I have only tested the macOS side of this, not the Windows side. In fact, the patch currently doesn't build on Windows on CI. I haven't figured out why.)
> 
> This patch changes it so that, on Windows, plpython is linked against a library called "python3.lib" instead of previously "python311.lib" or similar.  The build failure is [0]
> 
> which suggests that the file isn't there.  But we have tested this internally and the build succeeded locally.  I also don't see anything in the Python installer code or documentation that suggests that there is an option not to install that file or something like that.  It should be installed right next to the python3XX.lib file.

I think the problem is that meson doesn't know we want to use the limited API.
I found this related issue on Github: https://github.com/mesonbuild/meson/issues/13824

Someone suggests to change the Python dependency in the meson.build file like this:

py_dep = declare_dependency(
    dependencies: py_dep,
    compile_args: '-DPy_LIMITED_API='0x030100',
)

I've tried to create a patch with this change. I'm attaching it to this message so that cfbot picks it up. (I was unable to reproduce the issue locally)

Best regards,
Jakob

￼￼￼

---

From: Jakob Egger <[email-masked]>
Date: Wed, 15 Jan 2025 12:28:22 +0100

> I've tried to create a patch with this change. I'm attaching it to this message so that cfbot picks it up. (I was unable to reproduce the issue locally)

Apologies, please disregard my last patch. It does not work.

It looks like meson just can't build libraries that link with Python that use the limited API.

My understanding of Meson is limited, so I am unsure how to best work around this limitation.

I've spent two days trying to figure it out; I'm at the end of my wits.

Best regards,
Jakob

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 15 Jan 2025 23:20:58 +0100

On 15.01.25 12:28, Jakob Egger wrote:
>> I've tried to create a patch with this change. I'm attaching it to 
>> this message so that cfbot picks it up. (I was unable to reproduce the 
>> issue locally)
> 

Thanks for checking.  I've also been poking at this, without success.

What we could do is just not activate the limited API on Windows 
(actually MSVC, since it works on MinGW).  If someone later finds a way 
to make it work on Windows/MSVC, then it's easy to change.

So here is an updated patch set with that change.  It should pass cfbot 
like this.
From 1a981b3979391c75c1b5a9cd686e7e0c14cd58b6 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Mon, 2 Dec 2024 08:53:42 +0100
Subject: [PATCH v2 1/3] Remove obsolete Python version check

The checked version is already the current minimum supported version
(3.2).

Discussion: https://www.postgresql.org/message-id/flat/[email-masked]
---
 src/pl/plpython/plpy_exec.c | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/src/pl/plpython/plpy_exec.c b/src/pl/plpython/plpy_exec.c
index 0e84bb90829..00747bb811b 100644
--- a/src/pl/plpython/plpy_exec.c
+++ b/src/pl/plpython/plpy_exec.c
@@ -1066,13 +1066,7 @@ PLy_procedure_call(PLyProcedure *proc, const char *kargs, PyObject *vargs)
 
 	PG_TRY();
 	{
-#if PY_VERSION_HEX >= 0x03020000
-		rv = PyEval_EvalCode(proc->code,
-							 proc->globals, proc->globals);
-#else
-		rv = PyEval_EvalCode((PyCodeObject *) proc->code,
-							 proc->globals, proc->globals);
-#endif
+		rv = PyEval_EvalCode(proc->code, proc->globals, proc->globals);
 
 		/*
 		 * Since plpy will only let you close subtransactions that you

base-commit: d5221c49a302ee7f3964449cbc730b01ce4e7d40
-- 
2.47.1

From c921ee3dcae83c489d2963537fcf9e1965004312 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Wed, 15 Jan 2025 22:09:29 +0100
Subject: [PATCH v2 2/3] Prepare for Python "Limited API" in PL/Python

Using the Python Limited API would allow building PL/Python against
any Python 3.x version and using another Python 3.x version at run
time.  This commit does not activate that, but it prepares the code to
only use APIs supported by the Limited API.

Implementation details:

- Convert static types to heap types
  (https://docs.python.org/3/howto/isolating-extensions.html#heap-types).

- Replace PyRun_String() with component functions.

- Replace PyList_SET_ITEM() with PyList_SetItem().

Discussion: https://www.postgresql.org/message-id/flat/[email-masked]
---
 src/pl/plpython/plpy_cursorobject.c  | 71 +++++++++++++-------
 src/pl/plpython/plpy_planobject.c    | 61 +++++++++++------
 src/pl/plpython/plpy_procedure.c     |  5 +-
 src/pl/plpython/plpy_resultobject.c  | 98 +++++++++++++++++-----------
 src/pl/plpython/plpy_subxactobject.c | 41 +++++++-----
 src/pl/plpython/plpy_typeio.c        |  6 +-
 6 files changed, 177 insertions(+), 105 deletions(-)

diff --git a/src/pl/plpython/plpy_cursorobject.c b/src/pl/plpython/plpy_cursorobject.c
index bb3fa8a3909..2a370157f90 100644
--- a/src/pl/plpython/plpy_cursorobject.c
+++ b/src/pl/plpython/plpy_cursorobject.c
@@ -20,7 +20,7 @@
 #include "utils/memutils.h"
 
 static PyObject *PLy_cursor_query(const char *query);
-static void PLy_cursor_dealloc(PyObject *arg);
+static void PLy_cursor_dealloc(PLyCursorObject *self);
 static PyObject *PLy_cursor_iternext(PyObject *self);
 static PyObject *PLy_cursor_fetch(PyObject *self, PyObject *args);
 static PyObject *PLy_cursor_close(PyObject *self, PyObject *unused);
@@ -33,22 +33,43 @@ static PyMethodDef PLy_cursor_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_CursorType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLyCursor",
-	.tp_basicsize = sizeof(PLyCursorObject),
-	.tp_dealloc = PLy_cursor_dealloc,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_cursor_doc,
-	.tp_iter = PyObject_SelfIter,
-	.tp_iternext = PLy_cursor_iternext,
-	.tp_methods = PLy_cursor_methods,
+static PyType_Slot PLyCursor_slots[] =
+{
+	{
+		Py_tp_dealloc, PLy_cursor_dealloc
+	},
+	{
+		Py_tp_doc, (char *) PLy_cursor_doc
+	},
+	{
+		Py_tp_iter, PyObject_SelfIter
+	},
+	{
+		Py_tp_iternext, PLy_cursor_iternext
+	},
+	{
+		Py_tp_methods, PLy_cursor_methods
+	},
+	{
+		0, NULL
+	}
 };
 
+static PyType_Spec PLyCursor_spec =
+{
+	.name = "PLyCursor",
+		.basicsize = sizeof(PLyCursorObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLyCursor_slots,
+};
+
+static PyTypeObject *PLy_CursorType;
+
 void
 PLy_cursor_init_type(void)
 {
-	if (PyType_Ready(&PLy_CursorType) < 0)
+	PLy_CursorType = (PyTypeObject *) PyType_FromSpec(&PLyCursor_spec);
+	if (!PLy_CursorType)
 		elog(ERROR, "could not initialize PLy_CursorType");
 }
 
@@ -80,7 +101,7 @@ PLy_cursor_query(const char *query)
 	volatile MemoryContext oldcontext;
 	volatile ResourceOwner oldowner;
 
-	if ((cursor = PyObject_New(PLyCursorObject, &PLy_CursorType)) == NULL)
+	if ((cursor = PyObject_New(PLyCursorObject, PLy_CursorType)) == NULL)
 		return NULL;
 	cursor->portalname = NULL;
 	cursor->closed = false;
@@ -177,7 +198,7 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 		return NULL;
 	}
 
-	if ((cursor = PyObject_New(PLyCursorObject, &PLy_CursorType)) == NULL)
+	if ((cursor = PyObject_New(PLyCursorObject, PLy_CursorType)) == NULL)
 		return NULL;
 	cursor->portalname = NULL;
 	cursor->closed = false;
@@ -272,30 +293,30 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 }
 
 static void
-PLy_cursor_dealloc(PyObject *arg)
+PLy_cursor_dealloc(PLyCursorObject *self)
 {
-	PLyCursorObject *cursor;
+	PyTypeObject *tp = Py_TYPE(self);
 	Portal		portal;
 
-	cursor = (PLyCursorObject *) arg;
-
-	if (!cursor->closed)
+	if (!self->closed)
 	{
-		portal = GetPortalByName(cursor->portalname);
+		portal = GetPortalByName(self->portalname);
 
 		if (PortalIsValid(portal))
 		{
 			UnpinPortal(portal);
 			SPI_cursor_close(portal);
 		}
-		cursor->closed = true;
+		self->closed = true;
 	}
-	if (cursor->mcxt)
+	if (self->mcxt)
 	{
-		MemoryContextDelete(cursor->mcxt);
-		cursor->mcxt = NULL;
+		MemoryContextDelete(self->mcxt);
+		self->mcxt = NULL;
 	}
-	arg->ob_type->tp_free(arg);
+
+	PyObject_Free(self);
+	Py_DECREF(tp);
 }
 
 static PyObject *
diff --git a/src/pl/plpython/plpy_planobject.c b/src/pl/plpython/plpy_planobject.c
index 9427674d2f4..1b97b5cbd2a 100644
--- a/src/pl/plpython/plpy_planobject.c
+++ b/src/pl/plpython/plpy_planobject.c
@@ -12,7 +12,7 @@
 #include "plpython.h"
 #include "utils/memutils.h"
 
-static void PLy_plan_dealloc(PyObject *arg);
+static void PLy_plan_dealloc(PLyPlanObject *self);
 static PyObject *PLy_plan_cursor(PyObject *self, PyObject *args);
 static PyObject *PLy_plan_execute(PyObject *self, PyObject *args);
 static PyObject *PLy_plan_status(PyObject *self, PyObject *args);
@@ -26,20 +26,37 @@ static PyMethodDef PLy_plan_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_PlanType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLyPlan",
-	.tp_basicsize = sizeof(PLyPlanObject),
-	.tp_dealloc = PLy_plan_dealloc,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_plan_doc,
-	.tp_methods = PLy_plan_methods,
+static PyType_Slot PLyPlan_slots[] =
+{
+	{
+		Py_tp_dealloc, PLy_plan_dealloc
+	},
+	{
+		Py_tp_doc, (char *) PLy_plan_doc
+	},
+	{
+		Py_tp_methods, PLy_plan_methods
+	},
+	{
+		0, NULL
+	}
 };
 
+static PyType_Spec PLyPlan_spec =
+{
+	.name = "PLyPlan",
+		.basicsize = sizeof(PLyPlanObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLyPlan_slots,
+};
+
+static PyTypeObject *PLy_PlanType;
+
 void
 PLy_plan_init_type(void)
 {
-	if (PyType_Ready(&PLy_PlanType) < 0)
+	PLy_PlanType = (PyTypeObject *) PyType_FromSpec(&PLyPlan_spec);
+	if (!PLy_PlanType)
 		elog(ERROR, "could not initialize PLy_PlanType");
 }
 
@@ -48,7 +65,7 @@ PLy_plan_new(void)
 {
 	PLyPlanObject *ob;
 
-	if ((ob = PyObject_New(PLyPlanObject, &PLy_PlanType)) == NULL)
+	if ((ob = PyObject_New(PLyPlanObject, PLy_PlanType)) == NULL)
 		return NULL;
 
 	ob->plan = NULL;
@@ -63,25 +80,27 @@ PLy_plan_new(void)
 bool
 is_PLyPlanObject(PyObject *ob)
 {
-	return ob->ob_type == &PLy_PlanType;
+	return ob->ob_type == PLy_PlanType;
 }
 
 static void
-PLy_plan_dealloc(PyObject *arg)
+PLy_plan_dealloc(PLyPlanObject *self)
 {
-	PLyPlanObject *ob = (PLyPlanObject *) arg;
+	PyTypeObject *tp = Py_TYPE(self);
 
-	if (ob->plan)
+	if (self->plan)
 	{
-		SPI_freeplan(ob->plan);
-		ob->plan = NULL;
+		SPI_freeplan(self->plan);
+		self->plan = NULL;
 	}
-	if (ob->mcxt)
+	if (self->mcxt)
 	{
-		MemoryContextDelete(ob->mcxt);
-		ob->mcxt = NULL;
+		MemoryContextDelete(self->mcxt);
+		self->mcxt = NULL;
 	}
-	arg->ob_type->tp_free(arg);
+
+	PyObject_Free(self);
+	Py_DECREF(tp);
 }

diff --git a/src/pl/plpython/plpy_procedure.c b/src/pl/plpython/plpy_procedure.c
index c35a3b801ab..b494eeb474f 100644
--- a/src/pl/plpython/plpy_procedure.c
+++ b/src/pl/plpython/plpy_procedure.c
@@ -350,6 +350,7 @@ PLy_procedure_compile(PLyProcedure *proc, const char *src)
 {
 	PyObject   *crv = NULL;
 	char	   *msrc;
+	PyObject   *code0;
 
 	proc->globals = PyDict_Copy(PLy_interp_globals);
 
@@ -368,7 +369,9 @@ PLy_procedure_compile(PLyProcedure *proc, const char *src)
 	msrc = PLy_procedure_munge_source(proc->pyname, src);
 	/* Save the mangled source for later inclusion in tracebacks */
 	proc->src = MemoryContextStrdup(proc->mcxt, msrc);
-	crv = PyRun_String(msrc, Py_file_input, proc->globals, NULL);
+	code0 = Py_CompileString(msrc, "<string>", Py_file_input);
+	if (code0)
+		crv = PyEval_EvalCode(code0, proc->globals, NULL);
 	pfree(msrc);
 
 	if (crv != NULL)
diff --git a/src/pl/plpython/plpy_resultobject.c b/src/pl/plpython/plpy_resultobject.c
index 95acce65493..f2628205669 100644
--- a/src/pl/plpython/plpy_resultobject.c
+++ b/src/pl/plpython/plpy_resultobject.c
@@ -10,7 +10,7 @@
 #include "plpy_resultobject.h"
 #include "plpython.h"
 
-static void PLy_result_dealloc(PyObject *arg);
+static void PLy_result_dealloc(PLyResultObject *self);
 static PyObject *PLy_result_colnames(PyObject *self, PyObject *unused);
 static PyObject *PLy_result_coltypes(PyObject *self, PyObject *unused);
 static PyObject *PLy_result_coltypmods(PyObject *self, PyObject *unused);
@@ -24,17 +24,6 @@ static int	PLy_result_ass_subscript(PyObject *arg, PyObject *item, PyObject *val
 
 static char PLy_result_doc[] = "Results of a PostgreSQL query";
 
-static PySequenceMethods PLy_result_as_sequence = {
-	.sq_length = PLy_result_length,
-	.sq_item = PLy_result_item,
-};
-
-static PyMappingMethods PLy_result_as_mapping = {
-	.mp_length = PLy_result_length,
-	.mp_subscript = PLy_result_subscript,
-	.mp_ass_subscript = PLy_result_ass_subscript,
-};
-
 static PyMethodDef PLy_result_methods[] = {
 	{"colnames", PLy_result_colnames, METH_NOARGS, NULL},
 	{"coltypes", PLy_result_coltypes, METH_NOARGS, NULL},
@@ -44,23 +33,55 @@ static PyMethodDef PLy_result_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_ResultType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLyResult",
-	.tp_basicsize = sizeof(PLyResultObject),
-	.tp_dealloc = PLy_result_dealloc,
-	.tp_as_sequence = &PLy_result_as_sequence,
-	.tp_as_mapping = &PLy_result_as_mapping,
-	.tp_str = &PLy_result_str,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_result_doc,
-	.tp_methods = PLy_result_methods,
+static PyType_Slot PLyResult_slots[] =
+{
+	{
+		Py_tp_dealloc, PLy_result_dealloc
+	},
+	{
+		Py_sq_length, PLy_result_length
+	},
+	{
+		Py_sq_item, PLy_result_item
+	},
+	{
+		Py_mp_length, PLy_result_length
+	},
+	{
+		Py_mp_subscript, PLy_result_subscript
+	},
+	{
+		Py_mp_ass_subscript, PLy_result_ass_subscript
+	},
+	{
+		Py_tp_str, PLy_result_str
+	},
+	{
+		Py_tp_doc, (char *) PLy_result_doc
+	},
+	{
+		Py_tp_methods, PLy_result_methods
+	},
+	{
+		0, NULL
+	}
 };
 
+static PyType_Spec PLyResult_spec =
+{
+	.name = "PLyResult",
+		.basicsize = sizeof(PLyResultObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLyResult_slots,
+};
+
+static PyTypeObject *PLy_ResultType;
+
 void
 PLy_result_init_type(void)
 {
-	if (PyType_Ready(&PLy_ResultType) < 0)
+	PLy_ResultType = (PyTypeObject *) PyType_FromSpec(&PLyResult_spec);
+	if (!PLy_ResultType)
 		elog(ERROR, "could not initialize PLy_ResultType");
 }
 
@@ -69,7 +90,7 @@ PLy_result_new(void)
 {
 	PLyResultObject *ob;
 
-	if ((ob = PyObject_New(PLyResultObject, &PLy_ResultType)) == NULL)
+	if ((ob = PyObject_New(PLyResultObject, PLy_ResultType)) == NULL)
 		return NULL;
 
 	/* ob->tuples = NULL; */
@@ -89,20 +110,21 @@ PLy_result_new(void)
 }
 
 static void
-PLy_result_dealloc(PyObject *arg)
+PLy_result_dealloc(PLyResultObject *self)
 {
-	PLyResultObject *ob = (PLyResultObject *) arg;
+	PyTypeObject *tp = Py_TYPE(self);
 
-	Py_XDECREF(ob->nrows);
-	Py_XDECREF(ob->rows);
-	Py_XDECREF(ob->status);
-	if (ob->tupdesc)
+	Py_XDECREF(self->nrows);
+	Py_XDECREF(self->rows);
+	Py_XDECREF(self->status);
+	if (self->tupdesc)
 	{
-		FreeTupleDesc(ob->tupdesc);
-		ob->tupdesc = NULL;
+		FreeTupleDesc(self->tupdesc);
+		self->tupdesc = NULL;
 	}
 
-	arg->ob_type->tp_free(arg);
+	PyObject_Free(self);
+	Py_DECREF(tp);
 }
 
 static PyObject *
@@ -125,7 +147,7 @@ PLy_result_colnames(PyObject *self, PyObject *unused)
 	{
 		Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);
 
-		PyList_SET_ITEM(list, i, PLyUnicode_FromString(NameStr(attr->attname)));
+		PyList_SetItem(list, i, PLyUnicode_FromString(NameStr(attr->attname)));
 	}
 
 	return list;
@@ -151,7 +173,7 @@ PLy_result_coltypes(PyObject *self, PyObject *unused)
 	{
 		Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);
 
-		PyList_SET_ITEM(list, i, PyLong_FromLong(attr->atttypid));
+		PyList_SetItem(list, i, PyLong_FromLong(attr->atttypid));
 	}
 
 	return list;
@@ -177,7 +199,7 @@ PLy_result_coltypmods(PyObject *self, PyObject *unused)
 	{
 		Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);
 
-		PyList_SET_ITEM(list, i, PyLong_FromLong(attr->atttypmod));
+		PyList_SetItem(list, i, PyLong_FromLong(attr->atttypmod));
 	}
 
 	return list;
@@ -227,7 +249,7 @@ PLy_result_str(PyObject *arg)
 	PLyResultObject *ob = (PLyResultObject *) arg;
 
 	return PyUnicode_FromFormat("<%s status=%S nrows=%S rows=%S>",
-								Py_TYPE(ob)->tp_name,
+								"PLyResult",
 								ob->status,
 								ob->nrows,
 								ob->rows);
diff --git a/src/pl/plpython/plpy_subxactobject.c b/src/pl/plpython/plpy_subxactobject.c
index 5c92a0e089a..cc7ff3f9df7 100644
--- a/src/pl/plpython/plpy_subxactobject.c
+++ b/src/pl/plpython/plpy_subxactobject.c
@@ -15,7 +15,6 @@
 List	   *explicit_subtransactions = NIL;

-static void PLy_subtransaction_dealloc(PyObject *subxact);
 static PyObject *PLy_subtransaction_enter(PyObject *self, PyObject *unused);
 static PyObject *PLy_subtransaction_exit(PyObject *self, PyObject *args);
 
@@ -31,21 +30,35 @@ static PyMethodDef PLy_subtransaction_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_SubtransactionType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLySubtransaction",
-	.tp_basicsize = sizeof(PLySubtransactionObject),
-	.tp_dealloc = PLy_subtransaction_dealloc,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_subtransaction_doc,
-	.tp_methods = PLy_subtransaction_methods,
+static PyType_Slot PLySubtransaction_slots[] =
+{
+	{
+		Py_tp_doc, (char *) PLy_subtransaction_doc
+	},
+	{
+		Py_tp_methods, PLy_subtransaction_methods
+	},
+	{
+		0, NULL
+	}
+};
+
+static PyType_Spec PLySubtransaction_spec =
+{
+	.name = "PLySubtransaction",
+		.basicsize = sizeof(PLySubtransactionObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLySubtransaction_slots,
 };
 
+static PyTypeObject *PLy_SubtransactionType;
+
 
 void
 PLy_subtransaction_init_type(void)
 {
-	if (PyType_Ready(&PLy_SubtransactionType) < 0)
+	PLy_SubtransactionType = (PyTypeObject *) PyType_FromSpec(&PLySubtransaction_spec);
+	if (!PLy_SubtransactionType)
 		elog(ERROR, "could not initialize PLy_SubtransactionType");
 }
 
@@ -55,7 +68,7 @@ PLy_subtransaction_new(PyObject *self, PyObject *unused)
 {
 	PLySubtransactionObject *ob;
 
-	ob = PyObject_New(PLySubtransactionObject, &PLy_SubtransactionType);
+	ob = PyObject_New(PLySubtransactionObject, PLy_SubtransactionType);
 
 	if (ob == NULL)
 		return NULL;
@@ -66,12 +79,6 @@ PLy_subtransaction_new(PyObject *self, PyObject *unused)
 	return (PyObject *) ob;
 }
 
-/* Python requires a dealloc function to be defined */
-static void
-PLy_subtransaction_dealloc(PyObject *subxact)
-{
-}
-
 /*
  * subxact.__enter__() or subxact.enter()
  *
diff --git a/src/pl/plpython/plpy_typeio.c b/src/pl/plpython/plpy_typeio.c
index db14c5f8dae..27d9b2f1af6 100644
--- a/src/pl/plpython/plpy_typeio.c
+++ b/src/pl/plpython/plpy_typeio.c
@@ -723,7 +723,7 @@ PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
 
 			sublist = PLyList_FromArray_recurse(elm, dims, ndim, dim + 1,
 												dataptr_p, bitmap_p, bitmask_p);
-			PyList_SET_ITEM(list, i, sublist);
+			PyList_SetItem(list, i, sublist);
 		}
 	}
 	else
@@ -742,14 +742,14 @@ PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
 			if (bitmap && (*bitmap & bitmask) == 0)
 			{
 				Py_INCREF(Py_None);
-				PyList_SET_ITEM(list, i, Py_None);
+				PyList_SetItem(list, i, Py_None);
 			}
 			else
 			{
 				Datum		itemvalue;
 
 				itemvalue = fetch_att(dataptr, elm->typbyval, elm->typlen);
-				PyList_SET_ITEM(list, i, elm->func(elm, itemvalue));
+				PyList_SetItem(list, i, elm->func(elm, itemvalue));
 				dataptr = att_addlength_pointer(dataptr, elm->typlen, dataptr);
 				dataptr = (char *) att_align_nominal(dataptr, elm->typalign);
 			}
-- 
2.47.1

From e059a1653a47f1ca9155a84c8f9a9f122a970ace Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Wed, 15 Jan 2025 22:22:59 +0100
Subject: [PATCH v2 3/3] Activate Python "Limited API" in PL/Python

This allows building PL/Python against any Python 3.x version and
using another Python 3.x version at run time.

Discussion: https://www.postgresql.org/message-id/flat/[email-masked]
---
 src/pl/plpython/plpython.h | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/src/pl/plpython/plpython.h b/src/pl/plpython/plpython.h
index ec822577260..06fc1a5440f 100644
--- a/src/pl/plpython/plpython.h
+++ b/src/pl/plpython/plpython.h
@@ -19,6 +19,15 @@
 #error Python.h must be included via plpython.h
 #endif
 
+/*
+ * Enable Python Limited API
+ *
+ * XXX currently not enabled on MSVC because of build failures
+ */
+#if !defined(_MSC_VER)
+#define Py_LIMITED_API 0x03020000
+#endif
+
 /*
  * Pull in Python headers via a wrapper header, to control the scope of
  * the system_header pragma therein.
-- 
2.47.1

---

From: Peter Eisentraut <[email-masked]>
Date: Mon, 3 Mar 2025 11:17:48 +0100

On 15.01.25 23:20, Peter Eisentraut wrote:
>>> this message so that cfbot picks it up. (I was unable to reproduce 
>>> the issue locally)
>>

Update for the hackers list: This patch set was briefly committed but 
had to be reverted because it crashed on some older Python versions; see 
[0].

I have locally reproduced the problem with Python 3.6 and 3.7; Python 
3.8 is ok.  This matches the results from the buildfarm.

I have poked at this a bit more but haven't gotten a good idea where or 
how to fix it so far.  Help welcome.

[0]: 
https://www.postgresql.org/message-id/flat/E1tnJNu-000CZW-0X%40gemulon.postgresql.org

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 5 Mar 2025 17:40:22 +0100

> how to fix it so far.  Help welcome.

I have figured this out.  There was a Python API change/bugfix between 
3.7 and 3.8 that directly affects this patch.  The relevant commit is 
<https://github.com/python/cpython/commit/364f0b0f19c>.  I have applied 
the workarounds described there to my patch set, and now it works for 
3.6 and 3.7 as well.

From d28601d504dff57c49089f3adcaf0dce83893d83 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Wed, 5 Mar 2025 17:38:06 +0100
Subject: [PATCH v3 1/2] Prepare for Python "Limited API" in PL/Python

Implementation details:

Discussion: https://www.postgresql.org/message-id/flat/[email-masked]
---
 src/pl/plpython/plpy_cursorobject.c  |  84 +++++++++++++++------
 src/pl/plpython/plpy_planobject.c    |  70 +++++++++++------
 src/pl/plpython/plpy_procedure.c     |   5 +-
 src/pl/plpython/plpy_resultobject.c  | 109 +++++++++++++++++----------
 src/pl/plpython/plpy_subxactobject.c |  46 ++++++-----
 src/pl/plpython/plpy_typeio.c        |   6 +-
 6 files changed, 213 insertions(+), 107 deletions(-)

index bb3fa8a3909..1c6be756120 100644

 
 
@@ -80,8 +101,12 @@ PLy_cursor_query(const char *query)
 	volatile MemoryContext oldcontext;
 	volatile ResourceOwner oldowner;
 
-	if ((cursor = PyObject_New(PLyCursorObject, &PLy_CursorType)) == NULL)
+	if ((cursor = PyObject_New(PLyCursorObject, PLy_CursorType)) == NULL)
 		return NULL;
+#if PY_VERSION_HEX < 0x03080000
+	/* Workaround for Python issue 35810; no longer necessary in Python 3.8 */
+	Py_INCREF(PLy_CursorType);
+#endif
 	cursor->portalname = NULL;
 	cursor->closed = false;
 	cursor->mcxt = AllocSetContextCreate(TopMemoryContext,
@@ -177,8 +202,12 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 		return NULL;
 	}
 
-	if ((cursor = PyObject_New(PLyCursorObject, &PLy_CursorType)) == NULL)
+	if ((cursor = PyObject_New(PLyCursorObject, PLy_CursorType)) == NULL)
 		return NULL;
+#if PY_VERSION_HEX < 0x03080000
+	/* Workaround for Python issue 35810; no longer necessary in Python 3.8 */
+	Py_INCREF(PLy_CursorType);
+#endif
 	cursor->portalname = NULL;
 	cursor->closed = false;
 	cursor->mcxt = AllocSetContextCreate(TopMemoryContext,
@@ -272,30 +301,35 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 }
 
 static void
+#if PY_VERSION_HEX >= 0x03080000

 		if (PortalIsValid(portal))
 		{
 			UnpinPortal(portal);
 			SPI_cursor_close(portal);
 		}
-		cursor->closed = true;
+		self->closed = true;
 	}
-	if (cursor->mcxt)
+	if (self->mcxt)
 	{
-		MemoryContextDelete(cursor->mcxt);
-		cursor->mcxt = NULL;
+		MemoryContextDelete(self->mcxt);
+		self->mcxt = NULL;
 	}
-	arg->ob_type->tp_free(arg);
+
+	PyObject_Free(self);
+#if PY_VERSION_HEX >= 0x03080000
+	/* This was not needed before Python 3.8 (Python issue 35810) */
+	Py_DECREF(tp);
+#endif
 }
 
 static PyObject *
index 9427674d2f4..3e385555e5e 100644

 
 
@@ -48,8 +65,12 @@ PLy_plan_new(void)
 {
 	PLyPlanObject *ob;
 
-	if ((ob = PyObject_New(PLyPlanObject, &PLy_PlanType)) == NULL)
+	if ((ob = PyObject_New(PLyPlanObject, PLy_PlanType)) == NULL)
 		return NULL;
+#if PY_VERSION_HEX < 0x03080000
+	/* Workaround for Python issue 35810; no longer necessary in Python 3.8 */
+	Py_INCREF(PLy_PlanType);
+#endif
 
 	ob->plan = NULL;
 	ob->nargs = 0;
@@ -63,25 +84,32 @@ PLy_plan_new(void)
 bool
 is_PLyPlanObject(PyObject *ob)
 {
-	return ob->ob_type == &PLy_PlanType;
+	return ob->ob_type == PLy_PlanType;
 }
 
 static void
+#if PY_VERSION_HEX >= 0x03080000
 
-	if (ob->plan)
+	if (self->plan)
 	{
-		SPI_freeplan(ob->plan);
-		ob->plan = NULL;
+		SPI_freeplan(self->plan);
+		self->plan = NULL;
 	}
-	if (ob->mcxt)
+	if (self->mcxt)
 	{
-		MemoryContextDelete(ob->mcxt);
-		ob->mcxt = NULL;
+		MemoryContextDelete(self->mcxt);
+		self->mcxt = NULL;
 	}
-	arg->ob_type->tp_free(arg);
+
+	PyObject_Free(self);
+#if PY_VERSION_HEX >= 0x03080000
+	/* This was not needed before Python 3.8 (Python issue 35810) */
+	Py_DECREF(tp);
+#endif
 }

 
 
 	if (crv != NULL)
index 95acce65493..80fa1d7accc 100644

 

 
@@ -69,8 +90,12 @@ PLy_result_new(void)
 {
 	PLyResultObject *ob;
 
-	if ((ob = PyObject_New(PLyResultObject, &PLy_ResultType)) == NULL)
+	if ((ob = PyObject_New(PLyResultObject, PLy_ResultType)) == NULL)
 		return NULL;
+#if PY_VERSION_HEX < 0x03080000
+	/* Workaround for Python issue 35810; no longer necessary in Python 3.8 */
+	Py_INCREF(PLy_ResultType);
+#endif
 
 	/* ob->tuples = NULL; */
 
@@ -89,20 +114,26 @@ PLy_result_new(void)
 }
 
 static void
+#if PY_VERSION_HEX >= 0x03080000
 
-	arg->ob_type->tp_free(arg);
+	PyObject_Free(self);
+#if PY_VERSION_HEX >= 0x03080000
+	/* This was not needed before Python 3.8 (Python issue 35810) */
+	Py_DECREF(tp);
+#endif
 }
 
 static PyObject *
@@ -125,7 +156,7 @@ PLy_result_colnames(PyObject *self, PyObject *unused)
 	{
 		Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);

 	return list;
@@ -151,7 +182,7 @@ PLy_result_coltypes(PyObject *self, PyObject *unused)
 	{
 		Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);

 	return list;
@@ -177,7 +208,7 @@ PLy_result_coltypmods(PyObject *self, PyObject *unused)
 	{
 		Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);

 	return list;
@@ -227,7 +258,7 @@ PLy_result_str(PyObject *arg)
 	PLyResultObject *ob = (PLyResultObject *) arg;
 
index 5c92a0e089a..ad24a9fc4b8 100644

 

 void
 
@@ -55,10 +68,13 @@ PLy_subtransaction_new(PyObject *self, PyObject *unused)
 {
 	PLySubtransactionObject *ob;
 
-	ob = PyObject_New(PLySubtransactionObject, &PLy_SubtransactionType);
-
+	ob = PyObject_New(PLySubtransactionObject, PLy_SubtransactionType);
 	if (ob == NULL)
 		return NULL;
+#if PY_VERSION_HEX < 0x03080000
+	/* Workaround for Python issue 35810; no longer necessary in Python 3.8 */
+	Py_INCREF(PLy_SubtransactionType);
+#endif
 
 	ob->started = false;
 	ob->exited = false;
@@ -66,12 +82,6 @@ PLy_subtransaction_new(PyObject *self, PyObject *unused)
 	return (PyObject *) ob;
 }
 
index 51e1d610259..1d127ae3ffe 100644

base-commit: ad40644eb8582fc1f537330b76ae7893cacf7b21
-- 
2.48.1

From dd07e62e041dfae67c1182226b772fe59a71f78e Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Wed, 5 Mar 2025 17:38:06 +0100
Subject: [PATCH v3 2/2] Activate Python "Limited API" in PL/Python

 
+/*
2.48.1

---

From: Peter Eisentraut <[email-masked]>
Date: Fri, 14 Mar 2025 09:41:29 +0100

On 05.03.25 17:40, Peter Eisentraut wrote:
>> had to be reverted because it crashed on some older Python versions; 
>> see [0].
>>
>> I have poked at this a bit more but haven't gotten a good idea where 
>> or how to fix it so far.  Help welcome.
> 

This has all been committed now.

---

From: vignesh C <[email-masked]>
Date: Mon, 17 Mar 2025 11:38:08 +0530

> This has all been committed now.

I felt we can mark this commitfest entry at [1] to commited now.
[1] - https://commitfest.postgresql.org/patch/5416/

Regards,
Vignesh

---

From: Bryan Green <[email-masked]>
Date: Fri, 2 Jan 2026 11:40:11 -0600

> I've spent two days trying to figure it out; I'm at the end of my wits.
Hi Jakob and Peter,

I've been working on enabling the Python Limited API on MSVC, which was
disabled in the recent commit due to build failures.

The issue is that Meson doesn't automatically link against python3.lib
when using the Limited API. Meson's Python dependency always returns the
version-specific library (python3XX.lib) regardless of whether
limited api is defined. This appears to be a known Meson limitation:
https://github.com/mesonbuild/meson/issues/13824

Initially, I thought that maybe the library wasn't being installed, but
that is not the case and the patch to our CI Windows MSVC image is not
needed.

I have a patch that works around this by manually constructing the
Python dependency on MSVC to use python3.lib instead of python3XX.lib,
using cc.find_library('python3') and building the dependency with the
appropriate include directory.  It passes CI when I push.  I'll see what
cfbot thinks.

Still testing, but wanted to share progress and get feedback on the
approach.

-- 
Bryan Green
EDB: https://www.enterprisedb.com

From 52efe8af1377e8adbac3533537f15ad20f7af1cf Mon Sep 17 00:00:00 2001
From: Bryan Green <[email-masked]>
Date: Fri, 2 Jan 2026 10:48:54 -0600
Subject: [PATCH v1] Enable Python Limited API for PL/Python on MSVC

Previously, the Python Limited API was disabled on MSVC due to build
failures caused by Meson not knowing to link against python3.lib
instead of python3XX.lib when using the Limited API.

This commit works around the Meson limitation by explicitly finding
and linking against python3.lib on MSVC, and removes the preprocessor
guard that was disabling the Limited API on MSVC in plpython.h.

This requires python3.lib to be present in the Python installation,
which is included when Python is installed.

Discussion: https://www.postgresql.org/message-id/flat/ee410de1-1e0b-4770-b125-eeefd4726a24%40eisentraut.org
---
 meson.build                | 12 +++++++++++-
 src/pl/plpython/plpython.h |  4 ----
 2 files changed, 11 insertions(+), 5 deletions(-)

diff --git a/meson.build b/meson.build
index d7c5193d4c..e4ecd78a7a 100644
--- a/meson.build
+++ b/meson.build
@@ -1329,7 +1329,17 @@ if not pyopt.disabled()
   pm = import('python')
   python3_inst = pm.find_installation(python.full_path(), required: pyopt)
   if python3_inst.found()
-    python3_dep = python3_inst.dependency(embed: true, required: pyopt)
+    # For Limited API on MSVC, link against python3.lib instead of python3XX.lib
+    if host_system == 'windows' and cc.get_id() == 'msvc'
+      python3_libdir = python3_inst.get_variable('prefix') / 'libs'
+      python3_lib = cc.find_library('python3', dirs: python3_libdir, required: pyopt)
+      python3_dep = declare_dependency(
+        include_directories: include_directories(python3_inst.get_variable('prefix') / 'include'),
+        dependencies: python3_lib,
+      )
+    else
+      python3_dep = python3_inst.dependency(embed: true, required: pyopt)
+    endif
     # Remove this check after we depend on Meson >= 1.1.0
     if not cc.check_header('Python.h', dependencies: python3_dep, required: pyopt, include_directories: postgres_inc)
       python3_dep = not_found_dep
diff --git a/src/pl/plpython/plpython.h b/src/pl/plpython/plpython.h
index 118b310084..445f7bc31a 100644
--- a/src/pl/plpython/plpython.h
+++ b/src/pl/plpython/plpython.h
@@ -25,12 +25,8 @@
 
 /*
  * Enable Python Limited API
- *
- * XXX currently not enabled on MSVC because of build failures
  */
-#if !defined(_MSC_VER)
 #define Py_LIMITED_API 0x03020000
-#endif
 
 /*
2.52.0.windows.1

---

From: Bryan Green <[email-masked]>
Date: Fri, 2 Jan 2026 12:12:22 -0600

> Still testing, but wanted to share progress and get feedback on the

cfbot seems to be happy:
https://cirrus-ci.com/github/postgresql-cfbot/postgresql/cf%2F6369
-- 
Bryan Green
EDB: https://www.enterprisedb.com

---

From: Peter Eisentraut <[email-masked]>
Date: Mon, 5 Jan 2026 08:53:33 +0100

> Still testing, but wanted to share progress and get feedback on the

This approach seems fine to me.  I suppose one question would be whether 
it would be worth using that same approach on all platforms rather than 
just on windows/msvc.  That way we only have one code path to maintain. 
But I'm not sure how easy that would be to achieve.

---

From: Peter Eisentraut <[email-masked]>
Date: Mon, 5 Jan 2026 08:55:08 +0100

> https://cirrus-ci.com/github/postgresql-cfbot/postgresql/cf%2F6369

Another thing you could test is whether this actually makes the ABI 
stable.  So for example build PL/Python against Python 3.13 and then 
install Python 3.14 (and uninstall 3.13 just to be sure) and then start 
a server and run some PL/Python stuff.

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 14 Jan 2026 13:56:46 +0100

> https://cirrus-ci.com/github/postgresql-cfbot/postgresql/cf%2F6369

I have committed this.  Thanks.