From: Mike Bayer Date: Wed, 18 Nov 2020 14:57:30 +0000 (-0500) Subject: Allow MetaData as the target for column_reflect event X-Git-Tag: rel_1_4_0b2~143 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=57ca85de0e81222a1e1b875cdc1df10a1220a330;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Allow MetaData as the target for column_reflect event The :meth:`_event.DDLEvents.column_reflect` event may now be applied to a :class:`_schema.MetaData` object where it will take effect for the :class:`_schema.Table` objects local to that collection. Fixes: #5712 Change-Id: I6044baa72d096ebd1fd99128270119747d1461b9 --- diff --git a/doc/build/changelog/unreleased_14/5712.rst b/doc/build/changelog/unreleased_14/5712.rst new file mode 100644 index 0000000000..3ca6b8f87f --- /dev/null +++ b/doc/build/changelog/unreleased_14/5712.rst @@ -0,0 +1,18 @@ +.. change:: + :tags: usecase, schema + :tickets: 5712 + + The :meth:`_events.DDLEvents.column_reflect` event may now be applied to a + :class:`_schema.MetaData` object where it will take effect for the + :class:`_schema.Table` objects local to that collection. + + .. seealso:: + + :meth:`_events.DDLEvents.column_reflect` + + :ref:`mapper_automated_reflection_schemes` - in the ORM mapping documentation + + :ref:`automap_intercepting_columns` - in the :ref:`automap_toplevel` documentation + + + diff --git a/doc/build/orm/mapping_columns.rst b/doc/build/orm/mapping_columns.rst index 74ed9fd28c..948334c731 100644 --- a/doc/build/orm/mapping_columns.rst +++ b/doc/build/orm/mapping_columns.rst @@ -63,11 +63,14 @@ a :class:`_schema.Column` explicitly mapped to a class can have a different attr name than the column. But what if we aren't listing out :class:`_schema.Column` objects explicitly, and instead are automating the production of :class:`_schema.Table` objects using reflection (e.g. as described in :ref:`metadata_reflection_toplevel`)? -In this case we can make use of the :meth:`.DDLEvents.column_reflect` event +In this case we can make use of the :meth:`_events.DDLEvents.column_reflect` event to intercept the production of :class:`_schema.Column` objects and provide them -with the :attr:`_schema.Column.key` of our choice:: +with the :attr:`_schema.Column.key` of our choice. The event is most easily +associated with the :class:`_schema.MetaData` object that's in use, +such as below we use the one linked to the :class:`_orm.declarative_base` +instance:: - @event.listens_for(Table, "column_reflect") + @event.listens_for(Base.metadata, "column_reflect") def column_reflect(inspector, table, column_info): # set column.key = "attr_" column_info['key'] = "attr_%s" % column_info['name'].lower() @@ -79,14 +82,14 @@ with our event that adds a new ".key" element, such as in a mapping as below:: __table__ = Table("some_table", Base.metadata, autoload_with=some_engine) -If we want to qualify our event to only react for the specific :class:`_schema.MetaData` -object above, we can check for it in our event:: +The approach also works with the :ref:`automap_toplevel` extension. See +the section :ref:`automap_intercepting_columns` for background. - @event.listens_for(Table, "column_reflect") - def column_reflect(inspector, table, column_info): - if table.metadata is Base.metadata: - # set column.key = "attr_" - column_info['key'] = "attr_%s" % column_info['name'].lower() +.. seealso:: + + :meth:`_events.DDLEvents.column_reflect` + + :ref:`automap_intercepting_columns` - in the :ref:`automap_toplevel` documentation .. _column_prefix: diff --git a/lib/sqlalchemy/engine/reflection.py b/lib/sqlalchemy/engine/reflection.py index 5770eab8c6..142637693f 100644 --- a/lib/sqlalchemy/engine/reflection.py +++ b/lib/sqlalchemy/engine/reflection.py @@ -841,6 +841,7 @@ class Inspector(object): orig_name = col_d["name"] + table.metadata.dispatch.column_reflect(self, table, col_d) table.dispatch.column_reflect(self, table, col_d) # fetch name again as column_reflect is allowed to diff --git a/lib/sqlalchemy/ext/automap.py b/lib/sqlalchemy/ext/automap.py index 97dff7f4ec..8fe318dfb0 100644 --- a/lib/sqlalchemy/ext/automap.py +++ b/lib/sqlalchemy/ext/automap.py @@ -529,6 +529,36 @@ the :meth:`.AutomapBase.prepare` method is required; if not called, the classes we've declared are in an un-mapped state. +.. _automap_intercepting_columns: + +Intercepting Column Definitions +=============================== + +The :class:`_schema.MetaData` and :class:`_schema.Table` objects support an +event hook :meth:`_events.DDLEvents.column_reflect` that may be used to intercept +the information reflected about a database column before the :class:`_schema.Column` +object is constructed. For example if we wanted to map columns using a +naming convention such as ``"attr_"``, the event could +be applied as:: + + @event.listens_for(Base.metadata, "column_reflect") + def column_reflect(inspector, table, column_info): + # set column.key = "attr_" + column_info['key'] = "attr_%s" % column_info['name'].lower() + + # run reflection + Base.prepare(engine, reflect=True) + +.. versionadded:: 1.4.0b2 the :meth:`_events.DDLEvents.column_reflect` event + may be applied to a :class:`_schema.MetaData` object. + +.. seealso:: + + :meth:`_events.DDLEvents.column_reflect` + + :ref:`mapper_automated_reflection_schemes` - in the ORM mapping documentation + + """ # noqa from .declarative import declarative_base as _declarative_base from .. import util diff --git a/lib/sqlalchemy/sql/events.py b/lib/sqlalchemy/sql/events.py index 23ea2d8d23..58d04f7aa3 100644 --- a/lib/sqlalchemy/sql/events.py +++ b/lib/sqlalchemy/sql/events.py @@ -213,8 +213,31 @@ class DDLEvents(event.Events): """Called for each unit of 'column info' retrieved when a :class:`_schema.Table` is being reflected. - Currently, this event may only be applied to the :class:`_schema.Table` - class directly:: + This event is most easily used by applying it to a specific + :class:`_schema.MetaData` instance, where it will take effect for + all :class:`_schema.Table` objects within that + :class:`_schema.MetaData` that undergo reflection:: + + metadata = MetaData() + + @event.listens_for(metadata, 'column_reflect') + def receive_column_reflect(inspector, table, column_info): + # receives for all Table objects that are reflected + # under this MetaData + + + # will use the above event hook + my_table = Table("my_table", metadata, autoload_with=some_engine) + + + .. versionadded:: 1.4.0b2 The :meth:`_events.DDLEvents.column_reflect` + hook may now be applied to a :class:`_schema.MetaData` object as + well as the :class:`_schema.MetaData` class itself where it will + take place for all :class:`_schema.Table` objects associated with + the targeted :class:`_schema.MetaData`. + + It may also be applied to the :class:`_schema.Table` class across + the board:: from sqlalchemy import Table @@ -222,7 +245,8 @@ class DDLEvents(event.Events): def receive_column_reflect(inspector, table, column_info): # receives for all Table objects that are reflected - Or applied using the + It can also be applied to a specific :class:`_schema.Table` at the + point that one is being reflected using the :paramref:`_schema.Table.listeners` parameter:: t1 = Table( @@ -257,14 +281,6 @@ class DDLEvents(event.Events): or :func:`_expression.text` object as well. Is applied to the :paramref:`_schema.Column.server_default` parameter - .. versionchanged:: 1.1.6 - - The :meth:`.DDLEvents.column_reflect` event allows a non - string :class:`.FetchedValue`, - :func:`_expression.text`, or derived object to be - specified as the value of ``default`` in the column - dictionary. - The event is called before any action is taken against this dictionary, and the contents can be modified; the following additional keys may be added to the dictionary to further modify @@ -290,4 +306,12 @@ class DDLEvents(event.Events): i.e. those copies that are generated when :meth:`_schema.Table.to_metadata` is used. + .. seealso:: + + :ref:`mapper_automated_reflection_schemes` - + in the ORM mapping documentation + + :ref:`automap_intercepting_columns` - + in the :ref:`automap_toplevel` documentation + """ diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py index c1f7ab58ae..b7e5dac313 100644 --- a/lib/sqlalchemy/sql/schema.py +++ b/lib/sqlalchemy/sql/schema.py @@ -389,8 +389,10 @@ class Table(DialectKWArgs, SchemaItem, TableClause): which will be passed to :func:`.event.listen` upon construction. This alternate hook to :func:`.event.listen` allows the establishment of a listener function specific to this :class:`_schema.Table` before - the "autoload" process begins. Particularly useful for - the :meth:`.DDLEvents.column_reflect` event:: + the "autoload" process begins. Historically this has been intended + for use with the :meth:`.DDLEvents.column_reflect` event, however + note that this event hook may now be associated with the + :class:`_schema.MetaData` object directly:: def listen_for_reflect(table, column_info): "handle the column reflection event" @@ -403,6 +405,10 @@ class Table(DialectKWArgs, SchemaItem, TableClause): ('column_reflect', listen_for_reflect) ]) + .. seealso:: + + :meth:`_events.DDLEvents.column_reflect` + :param must_exist: When ``True``, indicates that this Table must already be present in the given :class:`_schema.MetaData` collection, else an exception is raised. diff --git a/test/engine/test_reflection.py b/test/engine/test_reflection.py index baa84d1fa2..b19836c842 100644 --- a/test/engine/test_reflection.py +++ b/test/engine/test_reflection.py @@ -3,6 +3,7 @@ import unicodedata import sqlalchemy as sa from sqlalchemy import Computed from sqlalchemy import DefaultClause +from sqlalchemy import event from sqlalchemy import FetchedValue from sqlalchemy import ForeignKey from sqlalchemy import Identity @@ -2268,6 +2269,41 @@ class ColumnEventsTest(fixtures.RemovesEvents, fixtures.TestBase): self._do_test("x", {"default": my_default}, assert_text_of_one) + def test_listen_metadata_obj(self): + m1 = MetaData() + + m2 = MetaData() + + canary = [] + + @event.listens_for(m1, "column_reflect") + def go(insp, table, info): + canary.append(info["name"]) + + Table("related", m1, autoload_with=testing.db) + + Table("related", m2, autoload_with=testing.db) + + eq_(canary, ["q", "x", "y"]) + + def test_listen_metadata_cls(self): + m1 = MetaData() + + m2 = MetaData() + + canary = [] + + def go(insp, table, info): + canary.append(info["name"]) + + self.event_listen(MetaData, "column_reflect", go) + + Table("related", m1, autoload_with=testing.db) + + Table("related", m2, autoload_with=testing.db) + + eq_(canary, ["q", "x", "y", "q", "x", "y"]) + class ComputedColumnTest(fixtures.ComputedReflectionFixtureTest): def check_table_column(self, table, name, text, persisted):