--- /dev/null
+.. change::
+ :tags: bug, engine
+ :tickets: 5348
+
+ Fixed an issue in the C extension for the :class:`_result.Row` class which
+ could lead to a memory leak in the unlikely case of a :class:`_result.Row`
+ object which referred to an ORM object that then was mutated to refer back
+ to the ``Row`` itself, creating a cycle. The Python C APIs for tracking GC
+ cycles has been added to the native :class:`_result.Row` implementation to
+ accommodate for this case.
+
Py_INCREF(keymap);
self->keymap = keymap;
self->key_style = PyLong_AsLong(key_style);
+
+ // observation: because we have not implemented our own new method,
+ // cPython is apparently already calling PyObject_GC_Track for us.
+ // We assume it also called PyObject_GC_New since prior to #5348 we
+ // were already relying upon it to call PyObject_New, and we have now
+ // set Py_TPFLAGS_HAVE_GC.
+
+ return 0;
+}
+
+static int
+BaseRow_traverse(BaseRow *self, visitproc visit, void *arg)
+{
+ Py_VISIT(self->parent);
+ Py_VISIT(self->row);
+ Py_VISIT(self->keymap);
return 0;
}
static void
BaseRow_dealloc(BaseRow *self)
{
+ PyObject_GC_UnTrack(self);
Py_XDECREF(self->parent);
Py_XDECREF(self->row);
Py_XDECREF(self->keymap);
-#if PY_MAJOR_VERSION >= 3
- Py_TYPE(self)->tp_free((PyObject *)self);
-#else
- self->ob_type->tp_free((PyObject *)self);
-#endif
+ PyObject_GC_Del(self);
+
}
static PyObject *
(getattrofunc)BaseRow_getattro,/* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
- Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
+ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, /* tp_flags */
"BaseRow is a abstract base class for Row", /* tp_doc */
- 0, /* tp_traverse */
+ (traverseproc)BaseRow_traverse, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
from sqlalchemy import testing
from sqlalchemy import Unicode
from sqlalchemy import util
+from sqlalchemy.engine import result
from sqlalchemy.orm import aliased
from sqlalchemy.orm import clear_mappers
from sqlalchemy.orm import configure_mappers
go()
+ @testing.requires.cextensions
+ def test_cycles_in_row(self):
+
+ tup = result.result_tuple(["a", "b", "c"])
+
+ @profile_memory()
+ def go():
+ obj = {"foo": {}}
+ obj["foo"]["bar"] = obj
+
+ row = tup([1, 2, obj])
+
+ obj["foo"]["row"] = row
+
+ del row
+
+ go()
+
def test_ad_hoc_types(self):
"""test storage of bind processors, result processors
in dialect-wide registry."""