codekingpro/portable-devtools
114k
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 