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