Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
extensions.py549 linesDownload Raw Back to declarative
1# ext/declarative/extensions.py
2# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7# mypy: ignore-errors
8
9
10"""Public API functions and helpers for declarative."""
11from __future__ import annotations
12
13import collections
14import contextlib
15from typing import Any
16from typing import Callable
17from typing import TYPE_CHECKING
18from typing import Union
19
20from ... import exc as sa_exc
21from ...engine import Connection
22from ...engine import Engine
23from ...orm import exc as orm_exc
24from ...orm import relationships
25from ...orm.base import _mapper_or_none
26from ...orm.clsregistry import _resolver
27from ...orm.decl_base import _DeferredMapperConfig
28from ...orm.util import polymorphic_union
29from ...schema import Table
30from ...util import OrderedDict
31
32if TYPE_CHECKING:
33    from ...sql.schema import MetaData
34
35
36class ConcreteBase:
37    """A helper class for 'concrete' declarative mappings.
38
39    :class:`.ConcreteBase` will use the :func:`.polymorphic_union`
40    function automatically, against all tables mapped as a subclass
41    to this class.   The function is called via the
42    ``__declare_last__()`` function, which is essentially
43    a hook for the :meth:`.after_configured` event.
44
45    :class:`.ConcreteBase` produces a mapped
46    table for the class itself.  Compare to :class:`.AbstractConcreteBase`,
47    which does not.
48
49    Example::
50
51        from sqlalchemy.ext.declarative import ConcreteBase
52
53        class Employee(ConcreteBase, Base):
54            __tablename__ = 'employee'
55            employee_id = Column(Integer, primary_key=True)
56            name = Column(String(50))
57            __mapper_args__ = {
58                            'polymorphic_identity':'employee',
59                            'concrete':True}
60
61        class Manager(Employee):
62            __tablename__ = 'manager'
63            employee_id = Column(Integer, primary_key=True)
64            name = Column(String(50))
65            manager_data = Column(String(40))
66            __mapper_args__ = {
67                            'polymorphic_identity':'manager',
68                            'concrete':True}
69
70
71    The name of the discriminator column used by :func:`.polymorphic_union`
72    defaults to the name ``type``.  To suit the use case of a mapping where an
73    actual column in a mapped table is already named ``type``, the
74    discriminator name can be configured by setting the
75    ``_concrete_discriminator_name`` attribute::
76
77        class Employee(ConcreteBase, Base):
78            _concrete_discriminator_name = '_concrete_discriminator'
79
80    .. versionadded:: 1.3.19 Added the ``_concrete_discriminator_name``
81       attribute to :class:`_declarative.ConcreteBase` so that the
82       virtual discriminator column name can be customized.
83
84    .. versionchanged:: 1.4.2 The ``_concrete_discriminator_name`` attribute
85       need only be placed on the basemost class to take correct effect for
86       all subclasses.   An explicit error message is now raised if the
87       mapped column names conflict with the discriminator name, whereas
88       in the 1.3.x series there would be some warnings and then a non-useful
89       query would be generated.
90
91    .. seealso::
92
93        :class:`.AbstractConcreteBase`
94
95        :ref:`concrete_inheritance`
96
97
98    """
99
100    @classmethod
101    def _create_polymorphic_union(cls, mappers, discriminator_name):
102        return polymorphic_union(
103            OrderedDict(
104                (mp.polymorphic_identity, mp.local_table) for mp in mappers
105            ),
106            discriminator_name,
107            "pjoin",
108        )
109
110    @classmethod
111    def __declare_first__(cls):
112        m = cls.__mapper__
113        if m.with_polymorphic:
114            return
115
116        discriminator_name = (
117            getattr(cls, "_concrete_discriminator_name", None) or "type"
118        )
119
120        mappers = list(m.self_and_descendants)
121        pjoin = cls._create_polymorphic_union(mappers, discriminator_name)
122        m._set_with_polymorphic(("*", pjoin))
123        m._set_polymorphic_on(pjoin.c[discriminator_name])
124
125
126class AbstractConcreteBase(ConcreteBase):
127    """A helper class for 'concrete' declarative mappings.
128
129    :class:`.AbstractConcreteBase` will use the :func:`.polymorphic_union`
130    function automatically, against all tables mapped as a subclass
131    to this class.   The function is called via the
132    ``__declare_first__()`` function, which is essentially
133    a hook for the :meth:`.before_configured` event.
134
135    :class:`.AbstractConcreteBase` applies :class:`_orm.Mapper` for its
136    immediately inheriting class, as would occur for any other
137    declarative mapped class. However, the :class:`_orm.Mapper` is not
138    mapped to any particular :class:`.Table` object.  Instead, it's
139    mapped directly to the "polymorphic" selectable produced by
140    :func:`.polymorphic_union`, and performs no persistence operations on its
141    own.  Compare to :class:`.ConcreteBase`, which maps its
142    immediately inheriting class to an actual
143    :class:`.Table` that stores rows directly.
144
145    .. note::
146
147        The :class:`.AbstractConcreteBase` delays the mapper creation of the
148        base class until all the subclasses have been defined,
149        as it needs to create a mapping against a selectable that will include
150        all subclass tables.  In order to achieve this, it waits for the
151        **mapper configuration event** to occur, at which point it scans
152        through all the configured subclasses and sets up a mapping that will
153        query against all subclasses at once.
154
155        While this event is normally invoked automatically, in the case of
156        :class:`.AbstractConcreteBase`, it may be necessary to invoke it
157        explicitly after **all** subclass mappings are defined, if the first
158        operation is to be a query against this base class. To do so, once all
159        the desired classes have been configured, the
160        :meth:`_orm.registry.configure` method on the :class:`_orm.registry`
161        in use can be invoked, which is available in relation to a particular
162        declarative base class::
163
164            Base.registry.configure()
165
166    Example::
167
168        from sqlalchemy.orm import DeclarativeBase
169        from sqlalchemy.ext.declarative import AbstractConcreteBase
170
171        class Base(DeclarativeBase):
172            pass
173
174        class Employee(AbstractConcreteBase, Base):
175            pass
176
177        class Manager(Employee):
178            __tablename__ = 'manager'
179            employee_id = Column(Integer, primary_key=True)
180            name = Column(String(50))
181            manager_data = Column(String(40))
182
183            __mapper_args__ = {
184                'polymorphic_identity':'manager',
185                'concrete':True
186            }
187
188        Base.registry.configure()
189
190    The abstract base class is handled by declarative in a special way;
191    at class configuration time, it behaves like a declarative mixin
192    or an ``__abstract__`` base class.   Once classes are configured
193    and mappings are produced, it then gets mapped itself, but
194    after all of its descendants.  This is a very unique system of mapping
195    not found in any other SQLAlchemy API feature.
196
197    Using this approach, we can specify columns and properties
198    that will take place on mapped subclasses, in the way that
199    we normally do as in :ref:`declarative_mixins`::
200
201        from sqlalchemy.ext.declarative import AbstractConcreteBase
202
203        class Company(Base):
204            __tablename__ = 'company'
205            id = Column(Integer, primary_key=True)
206
207        class Employee(AbstractConcreteBase, Base):
208            strict_attrs = True
209
210            employee_id = Column(Integer, primary_key=True)
211
212            @declared_attr
213            def company_id(cls):
214                return Column(ForeignKey('company.id'))
215
216            @declared_attr
217            def company(cls):
218                return relationship("Company")
219
220        class Manager(Employee):
221            __tablename__ = 'manager'
222
223            name = Column(String(50))
224            manager_data = Column(String(40))
225
226            __mapper_args__ = {
227                'polymorphic_identity':'manager',
228                'concrete':True
229            }
230
231        Base.registry.configure()
232
233    When we make use of our mappings however, both ``Manager`` and
234    ``Employee`` will have an independently usable ``.company`` attribute::
235
236        session.execute(
237            select(Employee).filter(Employee.company.has(id=5))
238        )
239
240    :param strict_attrs: when specified on the base class, "strict" attribute
241     mode is enabled which attempts to limit ORM mapped attributes on the
242     base class to only those that are immediately present, while still
243     preserving "polymorphic" loading behavior.
244
245     .. versionadded:: 2.0
246
247    .. seealso::
248
249        :class:`.ConcreteBase`
250
251        :ref:`concrete_inheritance`
252
253        :ref:`abstract_concrete_base`
254
255    """
256
257    __no_table__ = True
258
259    @classmethod
260    def __declare_first__(cls):
261        cls._sa_decl_prepare_nocascade()
262
263    @classmethod
264    def _sa_decl_prepare_nocascade(cls):
265        if getattr(cls, "__mapper__", None):
266            return
267
268        to_map = _DeferredMapperConfig.config_for_cls(cls)
269
270        # can't rely on 'self_and_descendants' here
271        # since technically an immediate subclass
272        # might not be mapped, but a subclass
273        # may be.
274        mappers = []
275        stack = list(cls.__subclasses__())
276        while stack:
277            klass = stack.pop()
278            stack.extend(klass.__subclasses__())
279            mn = _mapper_or_none(klass)
280            if mn is not None:
281                mappers.append(mn)
282
283        discriminator_name = (
284            getattr(cls, "_concrete_discriminator_name", None) or "type"
285        )
286        pjoin = cls._create_polymorphic_union(mappers, discriminator_name)
287
288        # For columns that were declared on the class, these
289        # are normally ignored with the "__no_table__" mapping,
290        # unless they have a different attribute key vs. col name
291        # and are in the properties argument.
292        # In that case, ensure we update the properties entry
293        # to the correct column from the pjoin target table.
294        declared_cols = set(to_map.declared_columns)
295        declared_col_keys = {c.key for c in declared_cols}
296        for k, v in list(to_map.properties.items()):
297            if v in declared_cols:
298                to_map.properties[k] = pjoin.c[v.key]
299                declared_col_keys.remove(v.key)
300
301        to_map.local_table = pjoin
302
303        strict_attrs = cls.__dict__.get("strict_attrs", False)
304
305        m_args = to_map.mapper_args_fn or dict
306
307        def mapper_args():
308            args = m_args()
309            args["polymorphic_on"] = pjoin.c[discriminator_name]
310            args["polymorphic_abstract"] = True
311            if strict_attrs:
312                args["include_properties"] = (
313                    set(pjoin.primary_key)
314                    | declared_col_keys
315                    | {discriminator_name}
316                )
317                args["with_polymorphic"] = ("*", pjoin)
318            return args
319
320        to_map.mapper_args_fn = mapper_args
321
322        to_map.map()
323
324        stack = [cls]
325        while stack:
326            scls = stack.pop(0)
327            stack.extend(scls.__subclasses__())
328            sm = _mapper_or_none(scls)
329            if sm and sm.concrete and sm.inherits is None:
330                for sup_ in scls.__mro__[1:]:
331                    sup_sm = _mapper_or_none(sup_)
332                    if sup_sm:
333                        sm._set_concrete_base(sup_sm)
334                        break
335
336    @classmethod
337    def _sa_raise_deferred_config(cls):
338        raise orm_exc.UnmappedClassError(
339            cls,
340            msg="Class %s is a subclass of AbstractConcreteBase and "
341            "has a mapping pending until all subclasses are defined. "
342            "Call the sqlalchemy.orm.configure_mappers() function after "
343            "all subclasses have been defined to "
344            "complete the mapping of this class."
345            % orm_exc._safe_cls_name(cls),
346        )
347
348
349class DeferredReflection:
350    """A helper class for construction of mappings based on
351    a deferred reflection step.
352
353    Normally, declarative can be used with reflection by
354    setting a :class:`_schema.Table` object using autoload_with=engine
355    as the ``__table__`` attribute on a declarative class.
356    The caveat is that the :class:`_schema.Table` must be fully
357    reflected, or at the very least have a primary key column,
358    at the point at which a normal declarative mapping is
359    constructed, meaning the :class:`_engine.Engine` must be available
360    at class declaration time.
361
362    The :class:`.DeferredReflection` mixin moves the construction
363    of mappers to be at a later point, after a specific
364    method is called which first reflects all :class:`_schema.Table`
365    objects created so far.   Classes can define it as such::
366
367        from sqlalchemy.ext.declarative import declarative_base
368        from sqlalchemy.ext.declarative import DeferredReflection
369        Base = declarative_base()
370
371        class MyClass(DeferredReflection, Base):
372            __tablename__ = 'mytable'
373
374    Above, ``MyClass`` is not yet mapped.   After a series of
375    classes have been defined in the above fashion, all tables
376    can be reflected and mappings created using
377    :meth:`.prepare`::
378
379        engine = create_engine("someengine://...")
380        DeferredReflection.prepare(engine)
381
382    The :class:`.DeferredReflection` mixin can be applied to individual
383    classes, used as the base for the declarative base itself,
384    or used in a custom abstract class.   Using an abstract base
385    allows that only a subset of classes to be prepared for a
386    particular prepare step, which is necessary for applications
387    that use more than one engine.  For example, if an application
388    has two engines, you might use two bases, and prepare each
389    separately, e.g.::
390
391        class ReflectedOne(DeferredReflection, Base):
392            __abstract__ = True
393
394        class ReflectedTwo(DeferredReflection, Base):
395            __abstract__ = True
396
397        class MyClass(ReflectedOne):
398            __tablename__ = 'mytable'
399
400        class MyOtherClass(ReflectedOne):
401            __tablename__ = 'myothertable'
402
403        class YetAnotherClass(ReflectedTwo):
404            __tablename__ = 'yetanothertable'
405
406        # ... etc.
407
408    Above, the class hierarchies for ``ReflectedOne`` and
409    ``ReflectedTwo`` can be configured separately::
410
411        ReflectedOne.prepare(engine_one)
412        ReflectedTwo.prepare(engine_two)
413
414    .. seealso::
415
416        :ref:`orm_declarative_reflected_deferred_reflection` - in the
417        :ref:`orm_declarative_table_config_toplevel` section.
418
419    """
420
421    @classmethod
422    def prepare(
423        cls, bind: Union[Engine, Connection], **reflect_kw: Any
424    ) -> None:
425        r"""Reflect all :class:`_schema.Table` objects for all current
426        :class:`.DeferredReflection` subclasses
427
428        :param bind: :class:`_engine.Engine` or :class:`_engine.Connection`
429         instance
430
431         ..versionchanged:: 2.0.16 a :class:`_engine.Connection` is also
432         accepted.
433
434        :param \**reflect_kw: additional keyword arguments passed to
435         :meth:`_schema.MetaData.reflect`, such as
436         :paramref:`_schema.MetaData.reflect.views`.
437
438         .. versionadded:: 2.0.16
439
440        """
441
442        to_map = _DeferredMapperConfig.classes_for_base(cls)
443
444        metadata_to_table = collections.defaultdict(set)
445
446        # first collect the primary __table__ for each class into a
447        # collection of metadata/schemaname -> table names
448        for thingy in to_map:
449            if thingy.local_table is not None:
450                metadata_to_table[
451                    (thingy.local_table.metadata, thingy.local_table.schema)
452                ].add(thingy.local_table.name)
453
454        # then reflect all those tables into their metadatas
455
456        if isinstance(bind, Connection):
457            conn = bind
458            ctx = contextlib.nullcontext(enter_result=conn)
459        elif isinstance(bind, Engine):
460            ctx = bind.connect()
461        else:
462            raise sa_exc.ArgumentError(
463                f"Expected Engine or Connection, got {bind!r}"
464            )
465
466        with ctx as conn:
467            for (metadata, schema), table_names in metadata_to_table.items():
468                metadata.reflect(
469                    conn,
470                    only=table_names,
471                    schema=schema,
472                    extend_existing=True,
473                    autoload_replace=False,
474                    **reflect_kw,
475                )
476
477            metadata_to_table.clear()
478
479            # .map() each class, then go through relationships and look
480            # for secondary
481            for thingy in to_map:
482                thingy.map()
483
484                mapper = thingy.cls.__mapper__
485                metadata = mapper.class_.metadata
486
487                for rel in mapper._props.values():
488                    if (
489                        isinstance(rel, relationships.RelationshipProperty)
490                        and rel._init_args.secondary._is_populated()
491                    ):
492                        secondary_arg = rel._init_args.secondary
493
494                        if isinstance(secondary_arg.argument, Table):
495                            secondary_table = secondary_arg.argument
496                            metadata_to_table[
497                                (
498                                    secondary_table.metadata,
499                                    secondary_table.schema,
500                                )
501                            ].add(secondary_table.name)
502                        elif isinstance(secondary_arg.argument, str):
503                            _, resolve_arg = _resolver(rel.parent.class_, rel)
504
505                            resolver = resolve_arg(
506                                secondary_arg.argument, True
507                            )
508                            metadata_to_table[
509                                (metadata, thingy.local_table.schema)
510                            ].add(secondary_arg.argument)
511
512                            resolver._resolvers += (
513                                cls._sa_deferred_table_resolver(metadata),
514                            )
515
516                            secondary_arg.argument = resolver()
517
518            for (metadata, schema), table_names in metadata_to_table.items():
519                metadata.reflect(
520                    conn,
521                    only=table_names,
522                    schema=schema,
523                    extend_existing=True,
524                    autoload_replace=False,
525                )
526
527    @classmethod
528    def _sa_deferred_table_resolver(
529        cls, metadata: MetaData
530    ) -> Callable[[str], Table]:
531        def _resolve(key: str) -> Table:
532            # reflection has already occurred so this Table would have
533            # its contents already
534            return Table(key, metadata)
535
536        return _resolve
537
538    _sa_decl_prepare = True
539
540    @classmethod
541    def _sa_raise_deferred_config(cls):
542        raise orm_exc.UnmappedClassError(
543            cls,
544            msg="Class %s is a subclass of DeferredReflection.  "
545            "Mappings are not produced until the .prepare() "
546            "method is called on the class hierarchy."
547            % orm_exc._safe_cls_name(cls),
548        )
549 
codekingpro/portable-devtools · Team Ai