From: Mike Bayer Date: Wed, 23 Dec 2020 15:43:51 +0000 (-0500) Subject: Add ORMExecuteState mapper accessors X-Git-Tag: rel_1_4_0b2~81 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=0cc8a08262f6b92746a280387282d55beb24fa9d;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Add ORMExecuteState mapper accessors Added :attr:`_orm.ORMExecuteState.bind_mapper` and :attr:`_orm.ORMExecuteState.all_mappers` accessors to :class:`_orm.ORMExecuteState` event object, so that handlers can respond to the target mapper and/or mapped class or classes involved in an ORM statement execution. Change-Id: I2cfe3d422ce5df2559105d53a51135a583359bd9 --- diff --git a/doc/build/changelog/unreleased_14/orm_exec_mapper.rst b/doc/build/changelog/unreleased_14/orm_exec_mapper.rst new file mode 100644 index 0000000000..709e79a4af --- /dev/null +++ b/doc/build/changelog/unreleased_14/orm_exec_mapper.rst @@ -0,0 +1,8 @@ +.. change:: + :tags: usecase, orm + + Added :attr:`_orm.ORMExecuteState.bind_mapper` and + :attr:`_orm.ORMExecuteState.all_mappers` accessors to + :class:`_orm.ORMExecuteState` event object, so that handlers can respond to + the target mapper and/or mapped class or classes involved in an ORM + statement execution. diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 7b5fa2c733..1ec63fa40a 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -230,6 +230,71 @@ class ORMExecuteState(util.MemoizedSlots): _parent_execute_state=self, ) + @property + def bind_mapper(self): + """Return the :class:`_orm.Mapper` that is the primary "bind" mapper. + + For an :class:`_orm.ORMExecuteState` object invoking an ORM + statement, that is, the :attr:`_orm.ORMExecuteState.is_orm_statement` + attribute is ``True``, this attribute will return the + :class:`_orm.Mapper` that is considered to be the "primary" mapper + of the statement. The term "bind mapper" refers to the fact that + a :class:`_orm.Session` object may be "bound" to multiple + :class:`_engine.Engine` objects keyed to mapped classes, and the + "bind mapper" determines which of those :class:`_engine.Engine` objects + would be selected. + + For a statement that is invoked against a single mapped class, + :attr:`_orm.ORMExecuteState.bind_mapper` is intended to be a reliable + way of getting this mapper. + + .. versionadded:: 1.4.0b2 + + .. seealso:: + + :attr:`_orm.ORMExecuteState.all_mappers` + + + """ + return self.bind_arguments.get("mapper", None) + + @property + def all_mappers(self): + """Return a sequence of all :class:`_orm.Mapper` objects that are + involved at the top level of this statement. + + By "top level" we mean those :class:`_orm.Mapper` objects that would + be represented in the result set rows for a :func:`_sql.select` + query, or for a :func:`_dml.update` or :func:`_dml.delete` query, + the mapper that is the main subject of the UPDATE or DELETE. + + .. versionadded:: 1.4.0b2 + + .. seealso:: + + :attr:`_orm.ORMExecuteState.bind_mapper` + + + + """ + if not self.is_orm_statement: + return [] + elif self.is_select: + result = [] + seen = set() + for d in self.statement.column_descriptions: + ent = d["entity"] + if ent: + insp = inspect(ent, raiseerr=False) + if insp and insp.mapper and insp.mapper not in seen: + seen.add(insp.mapper) + result.append(insp.mapper) + return result + elif self.is_update or self.is_delete: + return [self.bind_mapper] + else: + return [] + @property def is_orm_statement(self): """return True if the operation is an ORM statement. diff --git a/test/orm/test_events.py b/test/orm/test_events.py index a046ba34c7..fb05f6601c 100644 --- a/test/orm/test_events.py +++ b/test/orm/test_events.py @@ -2,6 +2,7 @@ import sqlalchemy as sa from sqlalchemy import delete from sqlalchemy import event from sqlalchemy import ForeignKey +from sqlalchemy import inspect from sqlalchemy import Integer from sqlalchemy import literal_column from sqlalchemy import select @@ -184,6 +185,8 @@ class ORMExecuteTest(_RemoveListeners, _fixtures.FixtureTest): ) canary.options( + bind_mapper=ctx.bind_mapper, + all_mappers=ctx.all_mappers, is_select=ctx.is_select, is_update=ctx.is_update, is_delete=ctx.is_delete, @@ -197,6 +200,87 @@ class ORMExecuteTest(_RemoveListeners, _fixtures.FixtureTest): return canary + def test_all_mappers_accessor_one(self): + User, Address = self.classes("User", "Address") + + sess = Session(testing.db, future=True) + + canary = self._flag_fixture(sess) + + sess.execute( + select(User.id, Address.email_address, User.name) + .join(Address) + .filter_by(id=7) + ) + + eq_( + canary.mock_calls, + [ + call.options( + bind_mapper=inspect(User), + all_mappers=[inspect(User), inspect(Address)], + is_select=True, + is_update=False, + is_delete=False, + is_orm_statement=True, + is_relationship_load=False, + is_column_load=False, + lazy_loaded_from=None, + ) + ], + ) + + def test_all_mappers_accessor_two(self): + + sess = Session(testing.db, future=True) + + canary = self._flag_fixture(sess) + + sess.execute(select(self.tables.users).filter_by(id=7)) + + eq_( + canary.mock_calls, + [ + call.options( + bind_mapper=None, + all_mappers=[], + is_select=True, + is_update=False, + is_delete=False, + is_orm_statement=False, + is_relationship_load=False, + is_column_load=False, + lazy_loaded_from=None, + ) + ], + ) + + def test_all_mappers_accessor_three(self): + User, Address = self.classes("User", "Address") + + sess = Session(testing.db, future=True) + + canary = self._flag_fixture(sess) + + sess.execute(select(User).join(Address).filter_by(id=7)) + + eq_( + canary.mock_calls, + [ + call.options( + bind_mapper=inspect(User), + all_mappers=[inspect(User)], # Address not in results + is_select=True, + is_update=False, + is_delete=False, + is_orm_statement=True, + is_relationship_load=False, + is_column_load=False, + lazy_loaded_from=None, + ) + ], + ) + def test_select_flags(self): User, Address = self.classes("User", "Address") @@ -214,6 +298,8 @@ class ORMExecuteTest(_RemoveListeners, _fixtures.FixtureTest): canary.mock_calls, [ call.options( + bind_mapper=inspect(User), + all_mappers=[inspect(User)], is_select=True, is_update=False, is_delete=False, @@ -223,6 +309,8 @@ class ORMExecuteTest(_RemoveListeners, _fixtures.FixtureTest): lazy_loaded_from=None, ), call.options( + bind_mapper=inspect(User), + all_mappers=[inspect(User)], is_select=True, is_update=False, is_delete=False, @@ -249,6 +337,8 @@ class ORMExecuteTest(_RemoveListeners, _fixtures.FixtureTest): canary.mock_calls, [ call.options( + bind_mapper=inspect(User), + all_mappers=[inspect(User)], is_select=True, is_update=False, is_delete=False, @@ -258,6 +348,8 @@ class ORMExecuteTest(_RemoveListeners, _fixtures.FixtureTest): lazy_loaded_from=None, ), call.options( + bind_mapper=inspect(Address), + all_mappers=[inspect(Address)], is_select=True, is_update=False, is_delete=False, @@ -286,6 +378,8 @@ class ORMExecuteTest(_RemoveListeners, _fixtures.FixtureTest): canary.mock_calls, [ call.options( + bind_mapper=inspect(User), + all_mappers=[inspect(User)], is_select=True, is_update=False, is_delete=False, @@ -295,6 +389,8 @@ class ORMExecuteTest(_RemoveListeners, _fixtures.FixtureTest): lazy_loaded_from=None, ), call.options( + bind_mapper=inspect(Address), + all_mappers=[inspect(Address)], is_select=True, is_update=False, is_delete=False, @@ -323,6 +419,8 @@ class ORMExecuteTest(_RemoveListeners, _fixtures.FixtureTest): canary.mock_calls, [ call.options( + bind_mapper=inspect(User), + all_mappers=[inspect(User)], is_select=True, is_update=False, is_delete=False, @@ -332,6 +430,8 @@ class ORMExecuteTest(_RemoveListeners, _fixtures.FixtureTest): lazy_loaded_from=None, ), call.options( + bind_mapper=inspect(Address), + all_mappers=[inspect(Address), inspect(User)], is_select=True, is_update=False, is_delete=False, @@ -357,6 +457,8 @@ class ORMExecuteTest(_RemoveListeners, _fixtures.FixtureTest): canary.mock_calls, [ call.options( + bind_mapper=inspect(User), + all_mappers=[inspect(User)], is_select=False, is_update=False, is_delete=True, @@ -366,6 +468,8 @@ class ORMExecuteTest(_RemoveListeners, _fixtures.FixtureTest): lazy_loaded_from=None, ), call.options( + bind_mapper=inspect(User), + all_mappers=[inspect(User)], is_select=False, is_update=True, is_delete=False,