]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Allow MetaData as the target for column_reflect event
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 18 Nov 2020 14:57:30 +0000 (09:57 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 18 Nov 2020 15:02:08 +0000 (10:02 -0500)
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

doc/build/changelog/unreleased_14/5712.rst [new file with mode: 0644]
doc/build/orm/mapping_columns.rst
lib/sqlalchemy/engine/reflection.py
lib/sqlalchemy/ext/automap.py
lib/sqlalchemy/sql/events.py
lib/sqlalchemy/sql/schema.py
test/engine/test_reflection.py

diff --git a/doc/build/changelog/unreleased_14/5712.rst b/doc/build/changelog/unreleased_14/5712.rst
new file mode 100644 (file)
index 0000000..3ca6b8f
--- /dev/null
@@ -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
+
+
+
index 74ed9fd28c6a17b2d5021b009211e98d9ae79fb3..948334c731016972cb60907fe66e233c00a2bf47 100644 (file)
@@ -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_<lower_case_name>"
         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_<lower_case_name>"
-            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:
 
index 5770eab8c632fbf35aefcfdc50c8834e5cf53731..142637693f422ad7e3172172ca6220f59ff70dbc 100644 (file)
@@ -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
index 97dff7f4ec0d72d7befd7530e44f66c4450a7820..8fe318dfb09841650bc55e9424dcbecc27e73ada 100644 (file)
@@ -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_<columnname>"``, the event could
+be applied as::
+
+    @event.listens_for(Base.metadata, "column_reflect")
+    def column_reflect(inspector, table, column_info):
+        # set column.key = "attr_<lower_case_name>"
+        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
index 23ea2d8d23918f7a9d025fdd7fcf1dce17e026cc..58d04f7aa380a3408f4adf1111469fe0a533970b 100644 (file)
@@ -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
+
         """
index c1f7ab58ae175b1162b1fa015b0c86bd4afbec5d..b7e5dac313cabd92cfd3f67f920001589ca4cddd 100644 (file)
@@ -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.
index baa84d1fa295fb9c2d9bb56d97862554e3fe9aef..b19836c84241572909e95d53127b686e376fc7ba 100644 (file)
@@ -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):