about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/sqlalchemy/ext/automap.py
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/sqlalchemy/ext/automap.py
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/sqlalchemy/ext/automap.py')
-rw-r--r--.venv/lib/python3.12/site-packages/sqlalchemy/ext/automap.py1701
1 files changed, 1701 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/sqlalchemy/ext/automap.py b/.venv/lib/python3.12/site-packages/sqlalchemy/ext/automap.py
new file mode 100644
index 00000000..817f91d2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sqlalchemy/ext/automap.py
@@ -0,0 +1,1701 @@
+# ext/automap.py
+# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+r"""Define an extension to the :mod:`sqlalchemy.ext.declarative` system
+which automatically generates mapped classes and relationships from a database
+schema, typically though not necessarily one which is reflected.
+
+It is hoped that the :class:`.AutomapBase` system provides a quick
+and modernized solution to the problem that the very famous
+`SQLSoup <https://pypi.org/project/sqlsoup/>`_
+also tries to solve, that of generating a quick and rudimentary object
+model from an existing database on the fly.  By addressing the issue strictly
+at the mapper configuration level, and integrating fully with existing
+Declarative class techniques, :class:`.AutomapBase` seeks to provide
+a well-integrated approach to the issue of expediently auto-generating ad-hoc
+mappings.
+
+.. tip:: The :ref:`automap_toplevel` extension is geared towards a
+   "zero declaration" approach, where a complete ORM model including classes
+   and pre-named relationships can be generated on the fly from a database
+   schema. For applications that still want to use explicit class declarations
+   including explicit relationship definitions in conjunction with reflection
+   of tables, the :class:`.DeferredReflection` class, described at
+   :ref:`orm_declarative_reflected_deferred_reflection`, is a better choice.
+
+.. _automap_basic_use:
+
+Basic Use
+=========
+
+The simplest usage is to reflect an existing database into a new model.
+We create a new :class:`.AutomapBase` class in a similar manner as to how
+we create a declarative base class, using :func:`.automap_base`.
+We then call :meth:`.AutomapBase.prepare` on the resulting base class,
+asking it to reflect the schema and produce mappings::
+
+    from sqlalchemy.ext.automap import automap_base
+    from sqlalchemy.orm import Session
+    from sqlalchemy import create_engine
+
+    Base = automap_base()
+
+    # engine, suppose it has two tables 'user' and 'address' set up
+    engine = create_engine("sqlite:///mydatabase.db")
+
+    # reflect the tables
+    Base.prepare(autoload_with=engine)
+
+    # mapped classes are now created with names by default
+    # matching that of the table name.
+    User = Base.classes.user
+    Address = Base.classes.address
+
+    session = Session(engine)
+
+    # rudimentary relationships are produced
+    session.add(Address(email_address="foo@bar.com", user=User(name="foo")))
+    session.commit()
+
+    # collection-based relationships are by default named
+    # "<classname>_collection"
+    u1 = session.query(User).first()
+    print(u1.address_collection)
+
+Above, calling :meth:`.AutomapBase.prepare` while passing along the
+:paramref:`.AutomapBase.prepare.reflect` parameter indicates that the
+:meth:`_schema.MetaData.reflect`
+method will be called on this declarative base
+classes' :class:`_schema.MetaData` collection; then, each **viable**
+:class:`_schema.Table` within the :class:`_schema.MetaData`
+will get a new mapped class
+generated automatically.  The :class:`_schema.ForeignKeyConstraint`
+objects which
+link the various tables together will be used to produce new, bidirectional
+:func:`_orm.relationship` objects between classes.
+The classes and relationships
+follow along a default naming scheme that we can customize.  At this point,
+our basic mapping consisting of related ``User`` and ``Address`` classes is
+ready to use in the traditional way.
+
+.. note:: By **viable**, we mean that for a table to be mapped, it must
+   specify a primary key.  Additionally, if the table is detected as being
+   a pure association table between two other tables, it will not be directly
+   mapped and will instead be configured as a many-to-many table between
+   the mappings for the two referring tables.
+
+Generating Mappings from an Existing MetaData
+=============================================
+
+We can pass a pre-declared :class:`_schema.MetaData` object to
+:func:`.automap_base`.
+This object can be constructed in any way, including programmatically, from
+a serialized file, or from itself being reflected using
+:meth:`_schema.MetaData.reflect`.
+Below we illustrate a combination of reflection and
+explicit table declaration::
+
+    from sqlalchemy import create_engine, MetaData, Table, Column, ForeignKey
+    from sqlalchemy.ext.automap import automap_base
+
+    engine = create_engine("sqlite:///mydatabase.db")
+
+    # produce our own MetaData object
+    metadata = MetaData()
+
+    # we can reflect it ourselves from a database, using options
+    # such as 'only' to limit what tables we look at...
+    metadata.reflect(engine, only=["user", "address"])
+
+    # ... or just define our own Table objects with it (or combine both)
+    Table(
+        "user_order",
+        metadata,
+        Column("id", Integer, primary_key=True),
+        Column("user_id", ForeignKey("user.id")),
+    )
+
+    # we can then produce a set of mappings from this MetaData.
+    Base = automap_base(metadata=metadata)
+
+    # calling prepare() just sets up mapped classes and relationships.
+    Base.prepare()
+
+    # mapped classes are ready
+    User = Base.classes.user
+    Address = Base.classes.address
+    Order = Base.classes.user_order
+
+.. _automap_by_module:
+
+Generating Mappings from Multiple Schemas
+=========================================
+
+The :meth:`.AutomapBase.prepare` method when used with reflection may reflect
+tables from one schema at a time at most, using the
+:paramref:`.AutomapBase.prepare.schema` parameter to indicate the name of a
+schema to be reflected from. In order to populate the :class:`.AutomapBase`
+with tables from multiple schemas, :meth:`.AutomapBase.prepare` may be invoked
+multiple times, each time passing a different name to the
+:paramref:`.AutomapBase.prepare.schema` parameter. The
+:meth:`.AutomapBase.prepare` method keeps an internal list of
+:class:`_schema.Table` objects that have already been mapped, and will add new
+mappings only for those :class:`_schema.Table` objects that are new since the
+last time :meth:`.AutomapBase.prepare` was run::
+
+    e = create_engine("postgresql://scott:tiger@localhost/test")
+
+    Base.metadata.create_all(e)
+
+    Base = automap_base()
+
+    Base.prepare(e)
+    Base.prepare(e, schema="test_schema")
+    Base.prepare(e, schema="test_schema_2")
+
+.. versionadded:: 2.0  The :meth:`.AutomapBase.prepare` method may be called
+   any number of times; only newly added tables will be mapped
+   on each run.   Previously in version 1.4 and earlier, multiple calls would
+   cause errors as it would attempt to re-map an already mapped class.
+   The previous workaround approach of invoking
+   :meth:`_schema.MetaData.reflect` directly remains available as well.
+
+Automapping same-named tables across multiple schemas
+-----------------------------------------------------
+
+For the common case where multiple schemas may have same-named tables and
+therefore would generate same-named classes, conflicts can be resolved either
+through use of the :paramref:`.AutomapBase.prepare.classname_for_table` hook to
+apply different classnames on a per-schema basis, or by using the
+:paramref:`.AutomapBase.prepare.modulename_for_table` hook, which allows
+disambiguation of same-named classes by changing their effective ``__module__``
+attribute. In the example below, this hook is used to create a ``__module__``
+attribute for all classes that is of the form ``mymodule.<schemaname>``, where
+the schema name ``default`` is used if no schema is present::
+
+    e = create_engine("postgresql://scott:tiger@localhost/test")
+
+    Base.metadata.create_all(e)
+
+
+    def module_name_for_table(cls, tablename, table):
+        if table.schema is not None:
+            return f"mymodule.{table.schema}"
+        else:
+            return f"mymodule.default"
+
+
+    Base = automap_base()
+
+    Base.prepare(e, modulename_for_table=module_name_for_table)
+    Base.prepare(
+        e, schema="test_schema", modulename_for_table=module_name_for_table
+    )
+    Base.prepare(
+        e, schema="test_schema_2", modulename_for_table=module_name_for_table
+    )
+
+The same named-classes are organized into a hierarchical collection available
+at :attr:`.AutomapBase.by_module`.  This collection is traversed using the
+dot-separated name of a particular package/module down into the desired
+class name.
+
+.. note:: When using the :paramref:`.AutomapBase.prepare.modulename_for_table`
+   hook to return a new ``__module__`` that is not ``None``, the class is
+   **not** placed into the :attr:`.AutomapBase.classes` collection; only
+   classes that were not given an explicit modulename are placed here, as the
+   collection cannot represent same-named classes individually.
+
+In the example above, if the database contained a table named ``accounts`` in
+all three of the default schema, the ``test_schema`` schema, and the
+``test_schema_2`` schema, three separate classes will be available as::
+
+    Base.by_module.mymodule.default.accounts
+    Base.by_module.mymodule.test_schema.accounts
+    Base.by_module.mymodule.test_schema_2.accounts
+
+The default module namespace generated for all :class:`.AutomapBase` classes is
+``sqlalchemy.ext.automap``. If no
+:paramref:`.AutomapBase.prepare.modulename_for_table` hook is used, the
+contents of :attr:`.AutomapBase.by_module` will be entirely within the
+``sqlalchemy.ext.automap`` namespace (e.g.
+``MyBase.by_module.sqlalchemy.ext.automap.<classname>``), which would contain
+the same series of classes as what would be seen in
+:attr:`.AutomapBase.classes`. Therefore it's generally only necessary to use
+:attr:`.AutomapBase.by_module` when explicit ``__module__`` conventions are
+present.
+
+.. versionadded: 2.0
+
+    Added the :attr:`.AutomapBase.by_module` collection, which stores
+    classes within a named hierarchy based on dot-separated module names,
+    as well as the :paramref:`.Automap.prepare.modulename_for_table` parameter
+    which allows for custom ``__module__`` schemes for automapped
+    classes.
+
+
+
+Specifying Classes Explicitly
+=============================
+
+.. tip:: If explicit classes are expected to be prominent in an application,
+   consider using :class:`.DeferredReflection` instead.
+
+The :mod:`.sqlalchemy.ext.automap` extension allows classes to be defined
+explicitly, in a way similar to that of the :class:`.DeferredReflection` class.
+Classes that extend from :class:`.AutomapBase` act like regular declarative
+classes, but are not immediately mapped after their construction, and are
+instead mapped when we call :meth:`.AutomapBase.prepare`.  The
+:meth:`.AutomapBase.prepare` method will make use of the classes we've
+established based on the table name we use.  If our schema contains tables
+``user`` and ``address``, we can define one or both of the classes to be used::
+
+    from sqlalchemy.ext.automap import automap_base
+    from sqlalchemy import create_engine
+
+    # automap base
+    Base = automap_base()
+
+
+    # pre-declare User for the 'user' table
+    class User(Base):
+        __tablename__ = "user"
+
+        # override schema elements like Columns
+        user_name = Column("name", String)
+
+        # override relationships too, if desired.
+        # we must use the same name that automap would use for the
+        # relationship, and also must refer to the class name that automap will
+        # generate for "address"
+        address_collection = relationship("address", collection_class=set)
+
+
+    # reflect
+    engine = create_engine("sqlite:///mydatabase.db")
+    Base.prepare(autoload_with=engine)
+
+    # we still have Address generated from the tablename "address",
+    # but User is the same as Base.classes.User now
+
+    Address = Base.classes.address
+
+    u1 = session.query(User).first()
+    print(u1.address_collection)
+
+    # the backref is still there:
+    a1 = session.query(Address).first()
+    print(a1.user)
+
+Above, one of the more intricate details is that we illustrated overriding
+one of the :func:`_orm.relationship` objects that automap would have created.
+To do this, we needed to make sure the names match up with what automap
+would normally generate, in that the relationship name would be
+``User.address_collection`` and the name of the class referred to, from
+automap's perspective, is called ``address``, even though we are referring to
+it as ``Address`` within our usage of this class.
+
+Overriding Naming Schemes
+=========================
+
+:mod:`.sqlalchemy.ext.automap` is tasked with producing mapped classes and
+relationship names based on a schema, which means it has decision points in how
+these names are determined.  These three decision points are provided using
+functions which can be passed to the :meth:`.AutomapBase.prepare` method, and
+are known as :func:`.classname_for_table`,
+:func:`.name_for_scalar_relationship`,
+and :func:`.name_for_collection_relationship`.  Any or all of these
+functions are provided as in the example below, where we use a "camel case"
+scheme for class names and a "pluralizer" for collection names using the
+`Inflect <https://pypi.org/project/inflect>`_ package::
+
+    import re
+    import inflect
+
+
+    def camelize_classname(base, tablename, table):
+        "Produce a 'camelized' class name, e.g."
+        "'words_and_underscores' -> 'WordsAndUnderscores'"
+
+        return str(
+            tablename[0].upper()
+            + re.sub(
+                r"_([a-z])",
+                lambda m: m.group(1).upper(),
+                tablename[1:],
+            )
+        )
+
+
+    _pluralizer = inflect.engine()
+
+
+    def pluralize_collection(base, local_cls, referred_cls, constraint):
+        "Produce an 'uncamelized', 'pluralized' class name, e.g."
+        "'SomeTerm' -> 'some_terms'"
+
+        referred_name = referred_cls.__name__
+        uncamelized = re.sub(
+            r"[A-Z]",
+            lambda m: "_%s" % m.group(0).lower(),
+            referred_name,
+        )[1:]
+        pluralized = _pluralizer.plural(uncamelized)
+        return pluralized
+
+
+    from sqlalchemy.ext.automap import automap_base
+
+    Base = automap_base()
+
+    engine = create_engine("sqlite:///mydatabase.db")
+
+    Base.prepare(
+        autoload_with=engine,
+        classname_for_table=camelize_classname,
+        name_for_collection_relationship=pluralize_collection,
+    )
+
+From the above mapping, we would now have classes ``User`` and ``Address``,
+where the collection from ``User`` to ``Address`` is called
+``User.addresses``::
+
+    User, Address = Base.classes.User, Base.classes.Address
+
+    u1 = User(addresses=[Address(email="foo@bar.com")])
+
+Relationship Detection
+======================
+
+The vast majority of what automap accomplishes is the generation of
+:func:`_orm.relationship` structures based on foreign keys.  The mechanism
+by which this works for many-to-one and one-to-many relationships is as
+follows:
+
+1. A given :class:`_schema.Table`, known to be mapped to a particular class,
+   is examined for :class:`_schema.ForeignKeyConstraint` objects.
+
+2. From each :class:`_schema.ForeignKeyConstraint`, the remote
+   :class:`_schema.Table`
+   object present is matched up to the class to which it is to be mapped,
+   if any, else it is skipped.
+
+3. As the :class:`_schema.ForeignKeyConstraint`
+   we are examining corresponds to a
+   reference from the immediate mapped class,  the relationship will be set up
+   as a many-to-one referring to the referred class; a corresponding
+   one-to-many backref will be created on the referred class referring
+   to this class.
+
+4. If any of the columns that are part of the
+   :class:`_schema.ForeignKeyConstraint`
+   are not nullable (e.g. ``nullable=False``), a
+   :paramref:`_orm.relationship.cascade` keyword argument
+   of ``all, delete-orphan`` will be added to the keyword arguments to
+   be passed to the relationship or backref.  If the
+   :class:`_schema.ForeignKeyConstraint` reports that
+   :paramref:`_schema.ForeignKeyConstraint.ondelete`
+   is set to ``CASCADE`` for a not null or ``SET NULL`` for a nullable
+   set of columns, the option :paramref:`_orm.relationship.passive_deletes`
+   flag is set to ``True`` in the set of relationship keyword arguments.
+   Note that not all backends support reflection of ON DELETE.
+
+5. The names of the relationships are determined using the
+   :paramref:`.AutomapBase.prepare.name_for_scalar_relationship` and
+   :paramref:`.AutomapBase.prepare.name_for_collection_relationship`
+   callable functions.  It is important to note that the default relationship
+   naming derives the name from the **the actual class name**.  If you've
+   given a particular class an explicit name by declaring it, or specified an
+   alternate class naming scheme, that's the name from which the relationship
+   name will be derived.
+
+6. The classes are inspected for an existing mapped property matching these
+   names.  If one is detected on one side, but none on the other side,
+   :class:`.AutomapBase` attempts to create a relationship on the missing side,
+   then uses the :paramref:`_orm.relationship.back_populates`
+   parameter in order to
+   point the new relationship to the other side.
+
+7. In the usual case where no relationship is on either side,
+   :meth:`.AutomapBase.prepare` produces a :func:`_orm.relationship` on the
+   "many-to-one" side and matches it to the other using the
+   :paramref:`_orm.relationship.backref` parameter.
+
+8. Production of the :func:`_orm.relationship` and optionally the
+   :func:`.backref`
+   is handed off to the :paramref:`.AutomapBase.prepare.generate_relationship`
+   function, which can be supplied by the end-user in order to augment
+   the arguments passed to :func:`_orm.relationship` or :func:`.backref` or to
+   make use of custom implementations of these functions.
+
+Custom Relationship Arguments
+-----------------------------
+
+The :paramref:`.AutomapBase.prepare.generate_relationship` hook can be used
+to add parameters to relationships.  For most cases, we can make use of the
+existing :func:`.automap.generate_relationship` function to return
+the object, after augmenting the given keyword dictionary with our own
+arguments.
+
+Below is an illustration of how to send
+:paramref:`_orm.relationship.cascade` and
+:paramref:`_orm.relationship.passive_deletes`
+options along to all one-to-many relationships::
+
+    from sqlalchemy.ext.automap import generate_relationship
+    from sqlalchemy.orm import interfaces
+
+
+    def _gen_relationship(
+        base, direction, return_fn, attrname, local_cls, referred_cls, **kw
+    ):
+        if direction is interfaces.ONETOMANY:
+            kw["cascade"] = "all, delete-orphan"
+            kw["passive_deletes"] = True
+        # make use of the built-in function to actually return
+        # the result.
+        return generate_relationship(
+            base, direction, return_fn, attrname, local_cls, referred_cls, **kw
+        )
+
+
+    from sqlalchemy.ext.automap import automap_base
+    from sqlalchemy import create_engine
+
+    # automap base
+    Base = automap_base()
+
+    engine = create_engine("sqlite:///mydatabase.db")
+    Base.prepare(autoload_with=engine, generate_relationship=_gen_relationship)
+
+Many-to-Many relationships
+--------------------------
+
+:mod:`.sqlalchemy.ext.automap` will generate many-to-many relationships, e.g.
+those which contain a ``secondary`` argument.  The process for producing these
+is as follows:
+
+1. A given :class:`_schema.Table` is examined for
+   :class:`_schema.ForeignKeyConstraint`
+   objects, before any mapped class has been assigned to it.
+
+2. If the table contains two and exactly two
+   :class:`_schema.ForeignKeyConstraint`
+   objects, and all columns within this table are members of these two
+   :class:`_schema.ForeignKeyConstraint` objects, the table is assumed to be a
+   "secondary" table, and will **not be mapped directly**.
+
+3. The two (or one, for self-referential) external tables to which the
+   :class:`_schema.Table`
+   refers to are matched to the classes to which they will be
+   mapped, if any.
+
+4. If mapped classes for both sides are located, a many-to-many bi-directional
+   :func:`_orm.relationship` / :func:`.backref`
+   pair is created between the two
+   classes.
+
+5. The override logic for many-to-many works the same as that of one-to-many/
+   many-to-one; the :func:`.generate_relationship` function is called upon
+   to generate the structures and existing attributes will be maintained.
+
+Relationships with Inheritance
+------------------------------
+
+:mod:`.sqlalchemy.ext.automap` will not generate any relationships between
+two classes that are in an inheritance relationship.   That is, with two
+classes given as follows::
+
+    class Employee(Base):
+        __tablename__ = "employee"
+        id = Column(Integer, primary_key=True)
+        type = Column(String(50))
+        __mapper_args__ = {
+            "polymorphic_identity": "employee",
+            "polymorphic_on": type,
+        }
+
+
+    class Engineer(Employee):
+        __tablename__ = "engineer"
+        id = Column(Integer, ForeignKey("employee.id"), primary_key=True)
+        __mapper_args__ = {
+            "polymorphic_identity": "engineer",
+        }
+
+The foreign key from ``Engineer`` to ``Employee`` is used not for a
+relationship, but to establish joined inheritance between the two classes.
+
+Note that this means automap will not generate *any* relationships
+for foreign keys that link from a subclass to a superclass.  If a mapping
+has actual relationships from subclass to superclass as well, those
+need to be explicit.  Below, as we have two separate foreign keys
+from ``Engineer`` to ``Employee``, we need to set up both the relationship
+we want as well as the ``inherit_condition``, as these are not things
+SQLAlchemy can guess::
+
+    class Employee(Base):
+        __tablename__ = "employee"
+        id = Column(Integer, primary_key=True)
+        type = Column(String(50))
+
+        __mapper_args__ = {
+            "polymorphic_identity": "employee",
+            "polymorphic_on": type,
+        }
+
+
+    class Engineer(Employee):
+        __tablename__ = "engineer"
+        id = Column(Integer, ForeignKey("employee.id"), primary_key=True)
+        favorite_employee_id = Column(Integer, ForeignKey("employee.id"))
+
+        favorite_employee = relationship(
+            Employee, foreign_keys=favorite_employee_id
+        )
+
+        __mapper_args__ = {
+            "polymorphic_identity": "engineer",
+            "inherit_condition": id == Employee.id,
+        }
+
+Handling Simple Naming Conflicts
+--------------------------------
+
+In the case of naming conflicts during mapping, override any of
+:func:`.classname_for_table`, :func:`.name_for_scalar_relationship`,
+and :func:`.name_for_collection_relationship` as needed.  For example, if
+automap is attempting to name a many-to-one relationship the same as an
+existing column, an alternate convention can be conditionally selected.  Given
+a schema:
+
+.. sourcecode:: sql
+
+    CREATE TABLE table_a (
+        id INTEGER PRIMARY KEY
+    );
+
+    CREATE TABLE table_b (
+        id INTEGER PRIMARY KEY,
+        table_a INTEGER,
+        FOREIGN KEY(table_a) REFERENCES table_a(id)
+    );
+
+The above schema will first automap the ``table_a`` table as a class named
+``table_a``; it will then automap a relationship onto the class for ``table_b``
+with the same name as this related class, e.g. ``table_a``.  This
+relationship name conflicts with the mapping column ``table_b.table_a``,
+and will emit an error on mapping.
+
+We can resolve this conflict by using an underscore as follows::
+
+    def name_for_scalar_relationship(
+        base, local_cls, referred_cls, constraint
+    ):
+        name = referred_cls.__name__.lower()
+        local_table = local_cls.__table__
+        if name in local_table.columns:
+            newname = name + "_"
+            warnings.warn(
+                "Already detected name %s present.  using %s" % (name, newname)
+            )
+            return newname
+        return name
+
+
+    Base.prepare(
+        autoload_with=engine,
+        name_for_scalar_relationship=name_for_scalar_relationship,
+    )
+
+Alternatively, we can change the name on the column side.   The columns
+that are mapped can be modified using the technique described at
+:ref:`mapper_column_distinct_names`, by assigning the column explicitly
+to a new name::
+
+    Base = automap_base()
+
+
+    class TableB(Base):
+        __tablename__ = "table_b"
+        _table_a = Column("table_a", ForeignKey("table_a.id"))
+
+
+    Base.prepare(autoload_with=engine)
+
+Using Automap with Explicit Declarations
+========================================
+
+As noted previously, automap has no dependency on reflection, and can make
+use of any collection of :class:`_schema.Table` objects within a
+:class:`_schema.MetaData`
+collection.  From this, it follows that automap can also be used
+generate missing relationships given an otherwise complete model that fully
+defines table metadata::
+
+    from sqlalchemy.ext.automap import automap_base
+    from sqlalchemy import Column, Integer, String, ForeignKey
+
+    Base = automap_base()
+
+
+    class User(Base):
+        __tablename__ = "user"
+
+        id = Column(Integer, primary_key=True)
+        name = Column(String)
+
+
+    class Address(Base):
+        __tablename__ = "address"
+
+        id = Column(Integer, primary_key=True)
+        email = Column(String)
+        user_id = Column(ForeignKey("user.id"))
+
+
+    # produce relationships
+    Base.prepare()
+
+    # mapping is complete, with "address_collection" and
+    # "user" relationships
+    a1 = Address(email="u1")
+    a2 = Address(email="u2")
+    u1 = User(address_collection=[a1, a2])
+    assert a1.user is u1
+
+Above, given mostly complete ``User`` and ``Address`` mappings, the
+:class:`_schema.ForeignKey` which we defined on ``Address.user_id`` allowed a
+bidirectional relationship pair ``Address.user`` and
+``User.address_collection`` to be generated on the mapped classes.
+
+Note that when subclassing :class:`.AutomapBase`,
+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(autoload_with=engine)
+
+.. 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 __future__ import annotations
+
+import dataclasses
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import ClassVar
+from typing import Dict
+from typing import List
+from typing import NoReturn
+from typing import Optional
+from typing import overload
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from .. import util
+from ..orm import backref
+from ..orm import declarative_base as _declarative_base
+from ..orm import exc as orm_exc
+from ..orm import interfaces
+from ..orm import relationship
+from ..orm.decl_base import _DeferredMapperConfig
+from ..orm.mapper import _CONFIGURE_MUTEX
+from ..schema import ForeignKeyConstraint
+from ..sql import and_
+from ..util import Properties
+from ..util.typing import Protocol
+
+if TYPE_CHECKING:
+    from ..engine.base import Engine
+    from ..orm.base import RelationshipDirection
+    from ..orm.relationships import ORMBackrefArgument
+    from ..orm.relationships import Relationship
+    from ..sql.schema import Column
+    from ..sql.schema import MetaData
+    from ..sql.schema import Table
+    from ..util import immutabledict
+
+
+_KT = TypeVar("_KT", bound=Any)
+_VT = TypeVar("_VT", bound=Any)
+
+
+class PythonNameForTableType(Protocol):
+    def __call__(
+        self, base: Type[Any], tablename: str, table: Table
+    ) -> str: ...
+
+
+def classname_for_table(
+    base: Type[Any],
+    tablename: str,
+    table: Table,
+) -> str:
+    """Return the class name that should be used, given the name
+    of a table.
+
+    The default implementation is::
+
+        return str(tablename)
+
+    Alternate implementations can be specified using the
+    :paramref:`.AutomapBase.prepare.classname_for_table`
+    parameter.
+
+    :param base: the :class:`.AutomapBase` class doing the prepare.
+
+    :param tablename: string name of the :class:`_schema.Table`.
+
+    :param table: the :class:`_schema.Table` object itself.
+
+    :return: a string class name.
+
+     .. note::
+
+        In Python 2, the string used for the class name **must** be a
+        non-Unicode object, e.g. a ``str()`` object.  The ``.name`` attribute
+        of :class:`_schema.Table` is typically a Python unicode subclass,
+        so the
+        ``str()`` function should be applied to this name, after accounting for
+        any non-ASCII characters.
+
+    """
+    return str(tablename)
+
+
+class NameForScalarRelationshipType(Protocol):
+    def __call__(
+        self,
+        base: Type[Any],
+        local_cls: Type[Any],
+        referred_cls: Type[Any],
+        constraint: ForeignKeyConstraint,
+    ) -> str: ...
+
+
+def name_for_scalar_relationship(
+    base: Type[Any],
+    local_cls: Type[Any],
+    referred_cls: Type[Any],
+    constraint: ForeignKeyConstraint,
+) -> str:
+    """Return the attribute name that should be used to refer from one
+    class to another, for a scalar object reference.
+
+    The default implementation is::
+
+        return referred_cls.__name__.lower()
+
+    Alternate implementations can be specified using the
+    :paramref:`.AutomapBase.prepare.name_for_scalar_relationship`
+    parameter.
+
+    :param base: the :class:`.AutomapBase` class doing the prepare.
+
+    :param local_cls: the class to be mapped on the local side.
+
+    :param referred_cls: the class to be mapped on the referring side.
+
+    :param constraint: the :class:`_schema.ForeignKeyConstraint` that is being
+     inspected to produce this relationship.
+
+    """
+    return referred_cls.__name__.lower()
+
+
+class NameForCollectionRelationshipType(Protocol):
+    def __call__(
+        self,
+        base: Type[Any],
+        local_cls: Type[Any],
+        referred_cls: Type[Any],
+        constraint: ForeignKeyConstraint,
+    ) -> str: ...
+
+
+def name_for_collection_relationship(
+    base: Type[Any],
+    local_cls: Type[Any],
+    referred_cls: Type[Any],
+    constraint: ForeignKeyConstraint,
+) -> str:
+    """Return the attribute name that should be used to refer from one
+    class to another, for a collection reference.
+
+    The default implementation is::
+
+        return referred_cls.__name__.lower() + "_collection"
+
+    Alternate implementations
+    can be specified using the
+    :paramref:`.AutomapBase.prepare.name_for_collection_relationship`
+    parameter.
+
+    :param base: the :class:`.AutomapBase` class doing the prepare.
+
+    :param local_cls: the class to be mapped on the local side.
+
+    :param referred_cls: the class to be mapped on the referring side.
+
+    :param constraint: the :class:`_schema.ForeignKeyConstraint` that is being
+     inspected to produce this relationship.
+
+    """
+    return referred_cls.__name__.lower() + "_collection"
+
+
+class GenerateRelationshipType(Protocol):
+    @overload
+    def __call__(
+        self,
+        base: Type[Any],
+        direction: RelationshipDirection,
+        return_fn: Callable[..., Relationship[Any]],
+        attrname: str,
+        local_cls: Type[Any],
+        referred_cls: Type[Any],
+        **kw: Any,
+    ) -> Relationship[Any]: ...
+
+    @overload
+    def __call__(
+        self,
+        base: Type[Any],
+        direction: RelationshipDirection,
+        return_fn: Callable[..., ORMBackrefArgument],
+        attrname: str,
+        local_cls: Type[Any],
+        referred_cls: Type[Any],
+        **kw: Any,
+    ) -> ORMBackrefArgument: ...
+
+    def __call__(
+        self,
+        base: Type[Any],
+        direction: RelationshipDirection,
+        return_fn: Union[
+            Callable[..., Relationship[Any]], Callable[..., ORMBackrefArgument]
+        ],
+        attrname: str,
+        local_cls: Type[Any],
+        referred_cls: Type[Any],
+        **kw: Any,
+    ) -> Union[ORMBackrefArgument, Relationship[Any]]: ...
+
+
+@overload
+def generate_relationship(
+    base: Type[Any],
+    direction: RelationshipDirection,
+    return_fn: Callable[..., Relationship[Any]],
+    attrname: str,
+    local_cls: Type[Any],
+    referred_cls: Type[Any],
+    **kw: Any,
+) -> Relationship[Any]: ...
+
+
+@overload
+def generate_relationship(
+    base: Type[Any],
+    direction: RelationshipDirection,
+    return_fn: Callable[..., ORMBackrefArgument],
+    attrname: str,
+    local_cls: Type[Any],
+    referred_cls: Type[Any],
+    **kw: Any,
+) -> ORMBackrefArgument: ...
+
+
+def generate_relationship(
+    base: Type[Any],
+    direction: RelationshipDirection,
+    return_fn: Union[
+        Callable[..., Relationship[Any]], Callable[..., ORMBackrefArgument]
+    ],
+    attrname: str,
+    local_cls: Type[Any],
+    referred_cls: Type[Any],
+    **kw: Any,
+) -> Union[Relationship[Any], ORMBackrefArgument]:
+    r"""Generate a :func:`_orm.relationship` or :func:`.backref`
+    on behalf of two
+    mapped classes.
+
+    An alternate implementation of this function can be specified using the
+    :paramref:`.AutomapBase.prepare.generate_relationship` parameter.
+
+    The default implementation of this function is as follows::
+
+        if return_fn is backref:
+            return return_fn(attrname, **kw)
+        elif return_fn is relationship:
+            return return_fn(referred_cls, **kw)
+        else:
+            raise TypeError("Unknown relationship function: %s" % return_fn)
+
+    :param base: the :class:`.AutomapBase` class doing the prepare.
+
+    :param direction: indicate the "direction" of the relationship; this will
+     be one of :data:`.ONETOMANY`, :data:`.MANYTOONE`, :data:`.MANYTOMANY`.
+
+    :param return_fn: the function that is used by default to create the
+     relationship.  This will be either :func:`_orm.relationship` or
+     :func:`.backref`.  The :func:`.backref` function's result will be used to
+     produce a new :func:`_orm.relationship` in a second step,
+     so it is critical
+     that user-defined implementations correctly differentiate between the two
+     functions, if a custom relationship function is being used.
+
+    :param attrname: the attribute name to which this relationship is being
+     assigned. If the value of :paramref:`.generate_relationship.return_fn` is
+     the :func:`.backref` function, then this name is the name that is being
+     assigned to the backref.
+
+    :param local_cls: the "local" class to which this relationship or backref
+     will be locally present.
+
+    :param referred_cls: the "referred" class to which the relationship or
+     backref refers to.
+
+    :param \**kw: all additional keyword arguments are passed along to the
+     function.
+
+    :return: a :func:`_orm.relationship` or :func:`.backref` construct,
+     as dictated
+     by the :paramref:`.generate_relationship.return_fn` parameter.
+
+    """
+
+    if return_fn is backref:
+        return return_fn(attrname, **kw)
+    elif return_fn is relationship:
+        return return_fn(referred_cls, **kw)
+    else:
+        raise TypeError("Unknown relationship function: %s" % return_fn)
+
+
+ByModuleProperties = Properties[Union["ByModuleProperties", Type[Any]]]
+
+
+class AutomapBase:
+    """Base class for an "automap" schema.
+
+    The :class:`.AutomapBase` class can be compared to the "declarative base"
+    class that is produced by the :func:`.declarative.declarative_base`
+    function.  In practice, the :class:`.AutomapBase` class is always used
+    as a mixin along with an actual declarative base.
+
+    A new subclassable :class:`.AutomapBase` is typically instantiated
+    using the :func:`.automap_base` function.
+
+    .. seealso::
+
+        :ref:`automap_toplevel`
+
+    """
+
+    __abstract__ = True
+
+    classes: ClassVar[Properties[Type[Any]]]
+    """An instance of :class:`.util.Properties` containing classes.
+
+    This object behaves much like the ``.c`` collection on a table.  Classes
+    are present under the name they were given, e.g.::
+
+        Base = automap_base()
+        Base.prepare(autoload_with=some_engine)
+
+        User, Address = Base.classes.User, Base.classes.Address
+
+    For class names that overlap with a method name of
+    :class:`.util.Properties`, such as ``items()``, the getitem form
+    is also supported::
+
+        Item = Base.classes["items"]
+
+    """
+
+    by_module: ClassVar[ByModuleProperties]
+    """An instance of :class:`.util.Properties` containing a hierarchal
+    structure of dot-separated module names linked to classes.
+
+    This collection is an alternative to the :attr:`.AutomapBase.classes`
+    collection that is useful when making use of the
+    :paramref:`.AutomapBase.prepare.modulename_for_table` parameter, which will
+    apply distinct ``__module__`` attributes to generated classes.
+
+    The default ``__module__`` an automap-generated class is
+    ``sqlalchemy.ext.automap``; to access this namespace using
+    :attr:`.AutomapBase.by_module` looks like::
+
+        User = Base.by_module.sqlalchemy.ext.automap.User
+
+    If a class had a ``__module__`` of ``mymodule.account``, accessing
+    this namespace looks like::
+
+        MyClass = Base.by_module.mymodule.account.MyClass
+
+    .. versionadded:: 2.0
+
+    .. seealso::
+
+        :ref:`automap_by_module`
+
+    """
+
+    metadata: ClassVar[MetaData]
+    """Refers to the :class:`_schema.MetaData` collection that will be used
+    for new :class:`_schema.Table` objects.
+
+    .. seealso::
+
+        :ref:`orm_declarative_metadata`
+
+    """
+
+    _sa_automapbase_bookkeeping: ClassVar[_Bookkeeping]
+
+    @classmethod
+    @util.deprecated_params(
+        engine=(
+            "2.0",
+            "The :paramref:`_automap.AutomapBase.prepare.engine` parameter "
+            "is deprecated and will be removed in a future release.  "
+            "Please use the "
+            ":paramref:`_automap.AutomapBase.prepare.autoload_with` "
+            "parameter.",
+        ),
+        reflect=(
+            "2.0",
+            "The :paramref:`_automap.AutomapBase.prepare.reflect` "
+            "parameter is deprecated and will be removed in a future "
+            "release.  Reflection is enabled when "
+            ":paramref:`_automap.AutomapBase.prepare.autoload_with` "
+            "is passed.",
+        ),
+    )
+    def prepare(
+        cls: Type[AutomapBase],
+        autoload_with: Optional[Engine] = None,
+        engine: Optional[Any] = None,
+        reflect: bool = False,
+        schema: Optional[str] = None,
+        classname_for_table: Optional[PythonNameForTableType] = None,
+        modulename_for_table: Optional[PythonNameForTableType] = None,
+        collection_class: Optional[Any] = None,
+        name_for_scalar_relationship: Optional[
+            NameForScalarRelationshipType
+        ] = None,
+        name_for_collection_relationship: Optional[
+            NameForCollectionRelationshipType
+        ] = None,
+        generate_relationship: Optional[GenerateRelationshipType] = None,
+        reflection_options: Union[
+            Dict[_KT, _VT], immutabledict[_KT, _VT]
+        ] = util.EMPTY_DICT,
+    ) -> None:
+        """Extract mapped classes and relationships from the
+        :class:`_schema.MetaData` and perform mappings.
+
+        For full documentation and examples see
+        :ref:`automap_basic_use`.
+
+        :param autoload_with: an :class:`_engine.Engine` or
+         :class:`_engine.Connection` with which
+         to perform schema reflection; when specified, the
+         :meth:`_schema.MetaData.reflect` method will be invoked within
+         the scope of this method.
+
+        :param engine: legacy; use :paramref:`.AutomapBase.autoload_with`.
+         Used to indicate the :class:`_engine.Engine` or
+         :class:`_engine.Connection` with which to reflect tables with,
+         if :paramref:`.AutomapBase.reflect` is True.
+
+        :param reflect: legacy; use :paramref:`.AutomapBase.autoload_with`.
+         Indicates that :meth:`_schema.MetaData.reflect` should be invoked.
+
+        :param classname_for_table: callable function which will be used to
+         produce new class names, given a table name.  Defaults to
+         :func:`.classname_for_table`.
+
+        :param modulename_for_table: callable function which will be used to
+         produce the effective ``__module__`` for an internally generated
+         class, to allow for multiple classes of the same name in a single
+         automap base which would be in different "modules".
+
+         Defaults to ``None``, which will indicate that ``__module__`` will not
+         be set explicitly; the Python runtime will use the value
+         ``sqlalchemy.ext.automap`` for these classes.
+
+         When assigning ``__module__`` to generated classes, they can be
+         accessed based on dot-separated module names using the
+         :attr:`.AutomapBase.by_module` collection.   Classes that have
+         an explicit ``__module_`` assigned using this hook do **not** get
+         placed into the :attr:`.AutomapBase.classes` collection, only
+         into :attr:`.AutomapBase.by_module`.
+
+         .. versionadded:: 2.0
+
+         .. seealso::
+
+            :ref:`automap_by_module`
+
+        :param name_for_scalar_relationship: callable function which will be
+         used to produce relationship names for scalar relationships.  Defaults
+         to :func:`.name_for_scalar_relationship`.
+
+        :param name_for_collection_relationship: callable function which will
+         be used to produce relationship names for collection-oriented
+         relationships.  Defaults to :func:`.name_for_collection_relationship`.
+
+        :param generate_relationship: callable function which will be used to
+         actually generate :func:`_orm.relationship` and :func:`.backref`
+         constructs.  Defaults to :func:`.generate_relationship`.
+
+        :param collection_class: the Python collection class that will be used
+         when a new :func:`_orm.relationship`
+         object is created that represents a
+         collection.  Defaults to ``list``.
+
+        :param schema: Schema name to reflect when reflecting tables using
+         the :paramref:`.AutomapBase.prepare.autoload_with` parameter. The name
+         is passed to the :paramref:`_schema.MetaData.reflect.schema` parameter
+         of :meth:`_schema.MetaData.reflect`. When omitted, the default schema
+         in use by the database connection is used.
+
+         .. note:: The :paramref:`.AutomapBase.prepare.schema`
+            parameter supports reflection of a single schema at a time.
+            In order to include tables from many schemas, use
+            multiple calls to :meth:`.AutomapBase.prepare`.
+
+            For an overview of multiple-schema automap including the use
+            of additional naming conventions to resolve table name
+            conflicts, see the section :ref:`automap_by_module`.
+
+            .. versionadded:: 2.0 :meth:`.AutomapBase.prepare` supports being
+               directly invoked any number of times, keeping track of tables
+               that have already been processed to avoid processing them
+               a second time.
+
+        :param reflection_options: When present, this dictionary of options
+         will be passed to :meth:`_schema.MetaData.reflect`
+         to supply general reflection-specific options like ``only`` and/or
+         dialect-specific options like ``oracle_resolve_synonyms``.
+
+         .. versionadded:: 1.4
+
+        """
+
+        for mr in cls.__mro__:
+            if "_sa_automapbase_bookkeeping" in mr.__dict__:
+                automap_base = cast("Type[AutomapBase]", mr)
+                break
+        else:
+            assert False, "Can't locate automap base in class hierarchy"
+
+        glbls = globals()
+        if classname_for_table is None:
+            classname_for_table = glbls["classname_for_table"]
+        if name_for_scalar_relationship is None:
+            name_for_scalar_relationship = glbls[
+                "name_for_scalar_relationship"
+            ]
+        if name_for_collection_relationship is None:
+            name_for_collection_relationship = glbls[
+                "name_for_collection_relationship"
+            ]
+        if generate_relationship is None:
+            generate_relationship = glbls["generate_relationship"]
+        if collection_class is None:
+            collection_class = list
+
+        if autoload_with:
+            reflect = True
+
+        if engine:
+            autoload_with = engine
+
+        if reflect:
+            assert autoload_with
+            opts = dict(
+                schema=schema,
+                extend_existing=True,
+                autoload_replace=False,
+            )
+            if reflection_options:
+                opts.update(reflection_options)
+            cls.metadata.reflect(autoload_with, **opts)  # type: ignore[arg-type]  # noqa: E501
+
+        with _CONFIGURE_MUTEX:
+            table_to_map_config: Union[
+                Dict[Optional[Table], _DeferredMapperConfig],
+                Dict[Table, _DeferredMapperConfig],
+            ] = {
+                cast("Table", m.local_table): m
+                for m in _DeferredMapperConfig.classes_for_base(
+                    cls, sort=False
+                )
+            }
+
+            many_to_many: List[
+                Tuple[Table, Table, List[ForeignKeyConstraint], Table]
+            ]
+            many_to_many = []
+
+            bookkeeping = automap_base._sa_automapbase_bookkeeping
+            metadata_tables = cls.metadata.tables
+
+            for table_key in set(metadata_tables).difference(
+                bookkeeping.table_keys
+            ):
+                table = metadata_tables[table_key]
+                bookkeeping.table_keys.add(table_key)
+
+                lcl_m2m, rem_m2m, m2m_const = _is_many_to_many(cls, table)
+                if lcl_m2m is not None:
+                    assert rem_m2m is not None
+                    assert m2m_const is not None
+                    many_to_many.append((lcl_m2m, rem_m2m, m2m_const, table))
+                elif not table.primary_key:
+                    continue
+                elif table not in table_to_map_config:
+                    clsdict: Dict[str, Any] = {"__table__": table}
+                    if modulename_for_table is not None:
+                        new_module = modulename_for_table(
+                            cls, table.name, table
+                        )
+                        if new_module is not None:
+                            clsdict["__module__"] = new_module
+                    else:
+                        new_module = None
+
+                    newname = classname_for_table(cls, table.name, table)
+                    if new_module is None and newname in cls.classes:
+                        util.warn(
+                            "Ignoring duplicate class name "
+                            f"'{newname}' "
+                            "received in automap base for table "
+                            f"{table.key} without "
+                            "``__module__`` being set; consider using the "
+                            "``modulename_for_table`` hook"
+                        )
+                        continue
+
+                    mapped_cls = type(
+                        newname,
+                        (automap_base,),
+                        clsdict,
+                    )
+                    map_config = _DeferredMapperConfig.config_for_cls(
+                        mapped_cls
+                    )
+                    assert map_config.cls.__name__ == newname
+                    if new_module is None:
+                        cls.classes[newname] = mapped_cls
+
+                    by_module_properties: ByModuleProperties = cls.by_module
+                    for token in map_config.cls.__module__.split("."):
+                        if token not in by_module_properties:
+                            by_module_properties[token] = util.Properties({})
+
+                        props = by_module_properties[token]
+
+                        # we can assert this because the clsregistry
+                        # module would have raised if there was a mismatch
+                        # between modules/classes already.
+                        # see test_cls_schema_name_conflict
+                        assert isinstance(props, Properties)
+                        by_module_properties = props
+
+                    by_module_properties[map_config.cls.__name__] = mapped_cls
+
+                    table_to_map_config[table] = map_config
+
+            for map_config in table_to_map_config.values():
+                _relationships_for_fks(
+                    automap_base,
+                    map_config,
+                    table_to_map_config,
+                    collection_class,
+                    name_for_scalar_relationship,
+                    name_for_collection_relationship,
+                    generate_relationship,
+                )
+
+            for lcl_m2m, rem_m2m, m2m_const, table in many_to_many:
+                _m2m_relationship(
+                    automap_base,
+                    lcl_m2m,
+                    rem_m2m,
+                    m2m_const,
+                    table,
+                    table_to_map_config,
+                    collection_class,
+                    name_for_scalar_relationship,
+                    name_for_collection_relationship,
+                    generate_relationship,
+                )
+
+            for map_config in _DeferredMapperConfig.classes_for_base(
+                automap_base
+            ):
+                map_config.map()
+
+    _sa_decl_prepare = True
+    """Indicate that the mapping of classes should be deferred.
+
+    The presence of this attribute name indicates to declarative
+    that the call to mapper() should not occur immediately; instead,
+    information about the table and attributes to be mapped are gathered
+    into an internal structure called _DeferredMapperConfig.  These
+    objects can be collected later using classes_for_base(), additional
+    mapping decisions can be made, and then the map() method will actually
+    apply the mapping.
+
+    The only real reason this deferral of the whole
+    thing is needed is to support primary key columns that aren't reflected
+    yet when the class is declared; everything else can theoretically be
+    added to the mapper later.  However, the _DeferredMapperConfig is a
+    nice interface in any case which exists at that not usually exposed point
+    at which declarative has the class and the Table but hasn't called
+    mapper() yet.
+
+    """
+
+    @classmethod
+    def _sa_raise_deferred_config(cls) -> NoReturn:
+        raise orm_exc.UnmappedClassError(
+            cls,
+            msg="Class %s is a subclass of AutomapBase.  "
+            "Mappings are not produced until the .prepare() "
+            "method is called on the class hierarchy."
+            % orm_exc._safe_cls_name(cls),
+        )
+
+
+@dataclasses.dataclass
+class _Bookkeeping:
+    __slots__ = ("table_keys",)
+
+    table_keys: Set[str]
+
+
+def automap_base(
+    declarative_base: Optional[Type[Any]] = None, **kw: Any
+) -> Any:
+    r"""Produce a declarative automap base.
+
+    This function produces a new base class that is a product of the
+    :class:`.AutomapBase` class as well a declarative base produced by
+    :func:`.declarative.declarative_base`.
+
+    All parameters other than ``declarative_base`` are keyword arguments
+    that are passed directly to the :func:`.declarative.declarative_base`
+    function.
+
+    :param declarative_base: an existing class produced by
+     :func:`.declarative.declarative_base`.  When this is passed, the function
+     no longer invokes :func:`.declarative.declarative_base` itself, and all
+     other keyword arguments are ignored.
+
+    :param \**kw: keyword arguments are passed along to
+     :func:`.declarative.declarative_base`.
+
+    """
+    if declarative_base is None:
+        Base = _declarative_base(**kw)
+    else:
+        Base = declarative_base
+
+    return type(
+        Base.__name__,
+        (AutomapBase, Base),
+        {
+            "__abstract__": True,
+            "classes": util.Properties({}),
+            "by_module": util.Properties({}),
+            "_sa_automapbase_bookkeeping": _Bookkeeping(set()),
+        },
+    )
+
+
+def _is_many_to_many(
+    automap_base: Type[Any], table: Table
+) -> Tuple[
+    Optional[Table], Optional[Table], Optional[list[ForeignKeyConstraint]]
+]:
+    fk_constraints = [
+        const
+        for const in table.constraints
+        if isinstance(const, ForeignKeyConstraint)
+    ]
+    if len(fk_constraints) != 2:
+        return None, None, None
+
+    cols: List[Column[Any]] = sum(
+        [
+            [fk.parent for fk in fk_constraint.elements]
+            for fk_constraint in fk_constraints
+        ],
+        [],
+    )
+
+    if set(cols) != set(table.c):
+        return None, None, None
+
+    return (
+        fk_constraints[0].elements[0].column.table,
+        fk_constraints[1].elements[0].column.table,
+        fk_constraints,
+    )
+
+
+def _relationships_for_fks(
+    automap_base: Type[Any],
+    map_config: _DeferredMapperConfig,
+    table_to_map_config: Union[
+        Dict[Optional[Table], _DeferredMapperConfig],
+        Dict[Table, _DeferredMapperConfig],
+    ],
+    collection_class: type,
+    name_for_scalar_relationship: NameForScalarRelationshipType,
+    name_for_collection_relationship: NameForCollectionRelationshipType,
+    generate_relationship: GenerateRelationshipType,
+) -> None:
+    local_table = cast("Optional[Table]", map_config.local_table)
+    local_cls = cast(
+        "Optional[Type[Any]]", map_config.cls
+    )  # derived from a weakref, may be None
+
+    if local_table is None or local_cls is None:
+        return
+    for constraint in local_table.constraints:
+        if isinstance(constraint, ForeignKeyConstraint):
+            fks = constraint.elements
+            referred_table = fks[0].column.table
+            referred_cfg = table_to_map_config.get(referred_table, None)
+            if referred_cfg is None:
+                continue
+            referred_cls = referred_cfg.cls
+
+            if local_cls is not referred_cls and issubclass(
+                local_cls, referred_cls
+            ):
+                continue
+
+            relationship_name = name_for_scalar_relationship(
+                automap_base, local_cls, referred_cls, constraint
+            )
+            backref_name = name_for_collection_relationship(
+                automap_base, referred_cls, local_cls, constraint
+            )
+
+            o2m_kws: Dict[str, Union[str, bool]] = {}
+            nullable = False not in {fk.parent.nullable for fk in fks}
+            if not nullable:
+                o2m_kws["cascade"] = "all, delete-orphan"
+
+                if (
+                    constraint.ondelete
+                    and constraint.ondelete.lower() == "cascade"
+                ):
+                    o2m_kws["passive_deletes"] = True
+            else:
+                if (
+                    constraint.ondelete
+                    and constraint.ondelete.lower() == "set null"
+                ):
+                    o2m_kws["passive_deletes"] = True
+
+            create_backref = backref_name not in referred_cfg.properties
+
+            if relationship_name not in map_config.properties:
+                if create_backref:
+                    backref_obj = generate_relationship(
+                        automap_base,
+                        interfaces.ONETOMANY,
+                        backref,
+                        backref_name,
+                        referred_cls,
+                        local_cls,
+                        collection_class=collection_class,
+                        **o2m_kws,
+                    )
+                else:
+                    backref_obj = None
+                rel = generate_relationship(
+                    automap_base,
+                    interfaces.MANYTOONE,
+                    relationship,
+                    relationship_name,
+                    local_cls,
+                    referred_cls,
+                    foreign_keys=[fk.parent for fk in constraint.elements],
+                    backref=backref_obj,
+                    remote_side=[fk.column for fk in constraint.elements],
+                )
+                if rel is not None:
+                    map_config.properties[relationship_name] = rel
+                    if not create_backref:
+                        referred_cfg.properties[
+                            backref_name
+                        ].back_populates = relationship_name  # type: ignore[union-attr] # noqa: E501
+            elif create_backref:
+                rel = generate_relationship(
+                    automap_base,
+                    interfaces.ONETOMANY,
+                    relationship,
+                    backref_name,
+                    referred_cls,
+                    local_cls,
+                    foreign_keys=[fk.parent for fk in constraint.elements],
+                    back_populates=relationship_name,
+                    collection_class=collection_class,
+                    **o2m_kws,
+                )
+                if rel is not None:
+                    referred_cfg.properties[backref_name] = rel
+                    map_config.properties[
+                        relationship_name
+                    ].back_populates = backref_name  # type: ignore[union-attr]
+
+
+def _m2m_relationship(
+    automap_base: Type[Any],
+    lcl_m2m: Table,
+    rem_m2m: Table,
+    m2m_const: List[ForeignKeyConstraint],
+    table: Table,
+    table_to_map_config: Union[
+        Dict[Optional[Table], _DeferredMapperConfig],
+        Dict[Table, _DeferredMapperConfig],
+    ],
+    collection_class: type,
+    name_for_scalar_relationship: NameForCollectionRelationshipType,
+    name_for_collection_relationship: NameForCollectionRelationshipType,
+    generate_relationship: GenerateRelationshipType,
+) -> None:
+    map_config = table_to_map_config.get(lcl_m2m, None)
+    referred_cfg = table_to_map_config.get(rem_m2m, None)
+    if map_config is None or referred_cfg is None:
+        return
+
+    local_cls = map_config.cls
+    referred_cls = referred_cfg.cls
+
+    relationship_name = name_for_collection_relationship(
+        automap_base, local_cls, referred_cls, m2m_const[0]
+    )
+    backref_name = name_for_collection_relationship(
+        automap_base, referred_cls, local_cls, m2m_const[1]
+    )
+
+    create_backref = backref_name not in referred_cfg.properties
+
+    if table in table_to_map_config:
+        overlaps = "__*"
+    else:
+        overlaps = None
+
+    if relationship_name not in map_config.properties:
+        if create_backref:
+            backref_obj = generate_relationship(
+                automap_base,
+                interfaces.MANYTOMANY,
+                backref,
+                backref_name,
+                referred_cls,
+                local_cls,
+                collection_class=collection_class,
+                overlaps=overlaps,
+            )
+        else:
+            backref_obj = None
+
+        rel = generate_relationship(
+            automap_base,
+            interfaces.MANYTOMANY,
+            relationship,
+            relationship_name,
+            local_cls,
+            referred_cls,
+            overlaps=overlaps,
+            secondary=table,
+            primaryjoin=and_(
+                fk.column == fk.parent for fk in m2m_const[0].elements
+            ),  # type: ignore [arg-type]
+            secondaryjoin=and_(
+                fk.column == fk.parent for fk in m2m_const[1].elements
+            ),  # type: ignore [arg-type]
+            backref=backref_obj,
+            collection_class=collection_class,
+        )
+        if rel is not None:
+            map_config.properties[relationship_name] = rel
+
+            if not create_backref:
+                referred_cfg.properties[
+                    backref_name
+                ].back_populates = relationship_name  # type: ignore[union-attr] # noqa: E501
+    elif create_backref:
+        rel = generate_relationship(
+            automap_base,
+            interfaces.MANYTOMANY,
+            relationship,
+            backref_name,
+            referred_cls,
+            local_cls,
+            overlaps=overlaps,
+            secondary=table,
+            primaryjoin=and_(
+                fk.column == fk.parent for fk in m2m_const[1].elements
+            ),  # type: ignore [arg-type]
+            secondaryjoin=and_(
+                fk.column == fk.parent for fk in m2m_const[0].elements
+            ),  # type: ignore [arg-type]
+            back_populates=relationship_name,
+            collection_class=collection_class,
+        )
+        if rel is not None:
+            referred_cfg.properties[backref_name] = rel
+            map_config.properties[
+                relationship_name
+            ].back_populates = backref_name  # type: ignore[union-attr]