Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/sqlalchemy/ext/declarative/extensions.py: 31%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

134 statements  

1# ext/declarative/extensions.py 

2# Copyright (C) 2005-2025 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 

54 class Employee(ConcreteBase, Base): 

55 __tablename__ = "employee" 

56 employee_id = Column(Integer, primary_key=True) 

57 name = Column(String(50)) 

58 __mapper_args__ = { 

59 "polymorphic_identity": "employee", 

60 "concrete": True, 

61 } 

62 

63 

64 class Manager(Employee): 

65 __tablename__ = "manager" 

66 employee_id = Column(Integer, primary_key=True) 

67 name = Column(String(50)) 

68 manager_data = Column(String(40)) 

69 __mapper_args__ = { 

70 "polymorphic_identity": "manager", 

71 "concrete": True, 

72 } 

73 

74 The name of the discriminator column used by :func:`.polymorphic_union` 

75 defaults to the name ``type``. To suit the use case of a mapping where an 

76 actual column in a mapped table is already named ``type``, the 

77 discriminator name can be configured by setting the 

78 ``_concrete_discriminator_name`` attribute:: 

79 

80 class Employee(ConcreteBase, Base): 

81 _concrete_discriminator_name = "_concrete_discriminator" 

82 

83 .. versionadded:: 1.3.19 Added the ``_concrete_discriminator_name`` 

84 attribute to :class:`_declarative.ConcreteBase` so that the 

85 virtual discriminator column name can be customized. 

86 

87 .. versionchanged:: 1.4.2 The ``_concrete_discriminator_name`` attribute 

88 need only be placed on the basemost class to take correct effect for 

89 all subclasses. An explicit error message is now raised if the 

90 mapped column names conflict with the discriminator name, whereas 

91 in the 1.3.x series there would be some warnings and then a non-useful 

92 query would be generated. 

93 

94 .. seealso:: 

95 

96 :class:`.AbstractConcreteBase` 

97 

98 :ref:`concrete_inheritance` 

99 

100 

101 """ 

102 

103 @classmethod 

104 def _create_polymorphic_union(cls, mappers, discriminator_name): 

105 return polymorphic_union( 

106 OrderedDict( 

107 (mp.polymorphic_identity, mp.local_table) for mp in mappers 

108 ), 

109 discriminator_name, 

110 "pjoin", 

111 ) 

112 

113 @classmethod 

114 def __declare_first__(cls): 

115 m = cls.__mapper__ 

116 if m.with_polymorphic: 

117 return 

118 

119 discriminator_name = ( 

120 getattr(cls, "_concrete_discriminator_name", None) or "type" 

121 ) 

122 

123 mappers = list(m.self_and_descendants) 

124 pjoin = cls._create_polymorphic_union(mappers, discriminator_name) 

125 m._set_with_polymorphic(("*", pjoin)) 

126 m._set_polymorphic_on(pjoin.c[discriminator_name]) 

127 

128 

129class AbstractConcreteBase(ConcreteBase): 

130 """A helper class for 'concrete' declarative mappings. 

131 

132 :class:`.AbstractConcreteBase` will use the :func:`.polymorphic_union` 

133 function automatically, against all tables mapped as a subclass 

134 to this class. The function is called via the 

135 ``__declare_first__()`` function, which is essentially 

136 a hook for the :meth:`.before_configured` event. 

137 

138 :class:`.AbstractConcreteBase` applies :class:`_orm.Mapper` for its 

139 immediately inheriting class, as would occur for any other 

140 declarative mapped class. However, the :class:`_orm.Mapper` is not 

141 mapped to any particular :class:`.Table` object. Instead, it's 

142 mapped directly to the "polymorphic" selectable produced by 

143 :func:`.polymorphic_union`, and performs no persistence operations on its 

144 own. Compare to :class:`.ConcreteBase`, which maps its 

145 immediately inheriting class to an actual 

146 :class:`.Table` that stores rows directly. 

147 

148 .. note:: 

149 

150 The :class:`.AbstractConcreteBase` delays the mapper creation of the 

151 base class until all the subclasses have been defined, 

152 as it needs to create a mapping against a selectable that will include 

153 all subclass tables. In order to achieve this, it waits for the 

154 **mapper configuration event** to occur, at which point it scans 

155 through all the configured subclasses and sets up a mapping that will 

156 query against all subclasses at once. 

157 

158 While this event is normally invoked automatically, in the case of 

159 :class:`.AbstractConcreteBase`, it may be necessary to invoke it 

160 explicitly after **all** subclass mappings are defined, if the first 

161 operation is to be a query against this base class. To do so, once all 

162 the desired classes have been configured, the 

163 :meth:`_orm.registry.configure` method on the :class:`_orm.registry` 

164 in use can be invoked, which is available in relation to a particular 

165 declarative base class:: 

166 

167 Base.registry.configure() 

168 

169 Example:: 

170 

171 from sqlalchemy.orm import DeclarativeBase 

172 from sqlalchemy.ext.declarative import AbstractConcreteBase 

173 

174 

175 class Base(DeclarativeBase): 

176 pass 

177 

178 

179 class Employee(AbstractConcreteBase, Base): 

180 pass 

181 

182 

183 class Manager(Employee): 

184 __tablename__ = "manager" 

185 employee_id = Column(Integer, primary_key=True) 

186 name = Column(String(50)) 

187 manager_data = Column(String(40)) 

188 

189 __mapper_args__ = { 

190 "polymorphic_identity": "manager", 

191 "concrete": True, 

192 } 

193 

194 

195 Base.registry.configure() 

196 

197 The abstract base class is handled by declarative in a special way; 

198 at class configuration time, it behaves like a declarative mixin 

199 or an ``__abstract__`` base class. Once classes are configured 

200 and mappings are produced, it then gets mapped itself, but 

201 after all of its descendants. This is a very unique system of mapping 

202 not found in any other SQLAlchemy API feature. 

203 

204 Using this approach, we can specify columns and properties 

205 that will take place on mapped subclasses, in the way that 

206 we normally do as in :ref:`declarative_mixins`:: 

207 

208 from sqlalchemy.ext.declarative import AbstractConcreteBase 

209 

210 

211 class Company(Base): 

212 __tablename__ = "company" 

213 id = Column(Integer, primary_key=True) 

214 

215 

216 class Employee(AbstractConcreteBase, Base): 

217 strict_attrs = True 

218 

219 employee_id = Column(Integer, primary_key=True) 

220 

221 @declared_attr 

222 def company_id(cls): 

223 return Column(ForeignKey("company.id")) 

224 

225 @declared_attr 

226 def company(cls): 

227 return relationship("Company") 

228 

229 

230 class Manager(Employee): 

231 __tablename__ = "manager" 

232 

233 name = Column(String(50)) 

234 manager_data = Column(String(40)) 

235 

236 __mapper_args__ = { 

237 "polymorphic_identity": "manager", 

238 "concrete": True, 

239 } 

240 

241 

242 Base.registry.configure() 

243 

244 When we make use of our mappings however, both ``Manager`` and 

245 ``Employee`` will have an independently usable ``.company`` attribute:: 

246 

247 session.execute(select(Employee).filter(Employee.company.has(id=5))) 

248 

249 :param strict_attrs: when specified on the base class, "strict" attribute 

250 mode is enabled which attempts to limit ORM mapped attributes on the 

251 base class to only those that are immediately present, while still 

252 preserving "polymorphic" loading behavior. 

253 

254 .. versionadded:: 2.0 

255 

256 .. seealso:: 

257 

258 :class:`.ConcreteBase` 

259 

260 :ref:`concrete_inheritance` 

261 

262 :ref:`abstract_concrete_base` 

263 

264 """ 

265 

266 __no_table__ = True 

267 

268 @classmethod 

269 def __declare_first__(cls): 

270 cls._sa_decl_prepare_nocascade() 

271 

272 @classmethod 

273 def _sa_decl_prepare_nocascade(cls): 

274 if getattr(cls, "__mapper__", None): 

275 return 

276 

277 to_map = _DeferredMapperConfig.config_for_cls(cls) 

278 

279 # can't rely on 'self_and_descendants' here 

280 # since technically an immediate subclass 

281 # might not be mapped, but a subclass 

282 # may be. 

283 mappers = [] 

284 stack = list(cls.__subclasses__()) 

285 while stack: 

286 klass = stack.pop() 

287 stack.extend(klass.__subclasses__()) 

288 mn = _mapper_or_none(klass) 

289 if mn is not None: 

290 mappers.append(mn) 

291 

292 discriminator_name = ( 

293 getattr(cls, "_concrete_discriminator_name", None) or "type" 

294 ) 

295 pjoin = cls._create_polymorphic_union(mappers, discriminator_name) 

296 

297 # For columns that were declared on the class, these 

298 # are normally ignored with the "__no_table__" mapping, 

299 # unless they have a different attribute key vs. col name 

300 # and are in the properties argument. 

301 # In that case, ensure we update the properties entry 

302 # to the correct column from the pjoin target table. 

303 declared_cols = set(to_map.declared_columns) 

304 declared_col_keys = {c.key for c in declared_cols} 

305 for k, v in list(to_map.properties.items()): 

306 if v in declared_cols: 

307 to_map.properties[k] = pjoin.c[v.key] 

308 declared_col_keys.remove(v.key) 

309 

310 to_map.local_table = pjoin 

311 

312 strict_attrs = cls.__dict__.get("strict_attrs", False) 

313 

314 m_args = to_map.mapper_args_fn or dict 

315 

316 def mapper_args(): 

317 args = m_args() 

318 args["polymorphic_on"] = pjoin.c[discriminator_name] 

319 args["polymorphic_abstract"] = True 

320 if strict_attrs: 

321 args["include_properties"] = ( 

322 set(pjoin.primary_key) 

323 | declared_col_keys 

324 | {discriminator_name} 

325 ) 

326 args["with_polymorphic"] = ("*", pjoin) 

327 return args 

328 

329 to_map.mapper_args_fn = mapper_args 

330 

331 to_map.map() 

332 

333 stack = [cls] 

334 while stack: 

335 scls = stack.pop(0) 

336 stack.extend(scls.__subclasses__()) 

337 sm = _mapper_or_none(scls) 

338 if sm and sm.concrete and sm.inherits is None: 

339 for sup_ in scls.__mro__[1:]: 

340 sup_sm = _mapper_or_none(sup_) 

341 if sup_sm: 

342 sm._set_concrete_base(sup_sm) 

343 break 

344 

345 @classmethod 

346 def _sa_raise_deferred_config(cls): 

347 raise orm_exc.UnmappedClassError( 

348 cls, 

349 msg="Class %s is a subclass of AbstractConcreteBase and " 

350 "has a mapping pending until all subclasses are defined. " 

351 "Call the sqlalchemy.orm.configure_mappers() function after " 

352 "all subclasses have been defined to " 

353 "complete the mapping of this class." 

354 % orm_exc._safe_cls_name(cls), 

355 ) 

356 

357 

358class DeferredReflection: 

359 """A helper class for construction of mappings based on 

360 a deferred reflection step. 

361 

362 Normally, declarative can be used with reflection by 

363 setting a :class:`_schema.Table` object using autoload_with=engine 

364 as the ``__table__`` attribute on a declarative class. 

365 The caveat is that the :class:`_schema.Table` must be fully 

366 reflected, or at the very least have a primary key column, 

367 at the point at which a normal declarative mapping is 

368 constructed, meaning the :class:`_engine.Engine` must be available 

369 at class declaration time. 

370 

371 The :class:`.DeferredReflection` mixin moves the construction 

372 of mappers to be at a later point, after a specific 

373 method is called which first reflects all :class:`_schema.Table` 

374 objects created so far. Classes can define it as such:: 

375 

376 from sqlalchemy.ext.declarative import declarative_base 

377 from sqlalchemy.ext.declarative import DeferredReflection 

378 

379 Base = declarative_base() 

380 

381 

382 class MyClass(DeferredReflection, Base): 

383 __tablename__ = "mytable" 

384 

385 Above, ``MyClass`` is not yet mapped. After a series of 

386 classes have been defined in the above fashion, all tables 

387 can be reflected and mappings created using 

388 :meth:`.prepare`:: 

389 

390 engine = create_engine("someengine://...") 

391 DeferredReflection.prepare(engine) 

392 

393 The :class:`.DeferredReflection` mixin can be applied to individual 

394 classes, used as the base for the declarative base itself, 

395 or used in a custom abstract class. Using an abstract base 

396 allows that only a subset of classes to be prepared for a 

397 particular prepare step, which is necessary for applications 

398 that use more than one engine. For example, if an application 

399 has two engines, you might use two bases, and prepare each 

400 separately, e.g.:: 

401 

402 class ReflectedOne(DeferredReflection, Base): 

403 __abstract__ = True 

404 

405 

406 class ReflectedTwo(DeferredReflection, Base): 

407 __abstract__ = True 

408 

409 

410 class MyClass(ReflectedOne): 

411 __tablename__ = "mytable" 

412 

413 

414 class MyOtherClass(ReflectedOne): 

415 __tablename__ = "myothertable" 

416 

417 

418 class YetAnotherClass(ReflectedTwo): 

419 __tablename__ = "yetanothertable" 

420 

421 

422 # ... etc. 

423 

424 Above, the class hierarchies for ``ReflectedOne`` and 

425 ``ReflectedTwo`` can be configured separately:: 

426 

427 ReflectedOne.prepare(engine_one) 

428 ReflectedTwo.prepare(engine_two) 

429 

430 .. seealso:: 

431 

432 :ref:`orm_declarative_reflected_deferred_reflection` - in the 

433 :ref:`orm_declarative_table_config_toplevel` section. 

434 

435 """ 

436 

437 @classmethod 

438 def prepare( 

439 cls, bind: Union[Engine, Connection], **reflect_kw: Any 

440 ) -> None: 

441 r"""Reflect all :class:`_schema.Table` objects for all current 

442 :class:`.DeferredReflection` subclasses 

443 

444 :param bind: :class:`_engine.Engine` or :class:`_engine.Connection` 

445 instance 

446 

447 ..versionchanged:: 2.0.16 a :class:`_engine.Connection` is also 

448 accepted. 

449 

450 :param \**reflect_kw: additional keyword arguments passed to 

451 :meth:`_schema.MetaData.reflect`, such as 

452 :paramref:`_schema.MetaData.reflect.views`. 

453 

454 .. versionadded:: 2.0.16 

455 

456 """ 

457 

458 to_map = _DeferredMapperConfig.classes_for_base(cls) 

459 

460 metadata_to_table = collections.defaultdict(set) 

461 

462 # first collect the primary __table__ for each class into a 

463 # collection of metadata/schemaname -> table names 

464 for thingy in to_map: 

465 if thingy.local_table is not None: 

466 metadata_to_table[ 

467 (thingy.local_table.metadata, thingy.local_table.schema) 

468 ].add(thingy.local_table.name) 

469 

470 # then reflect all those tables into their metadatas 

471 

472 if isinstance(bind, Connection): 

473 conn = bind 

474 ctx = contextlib.nullcontext(enter_result=conn) 

475 elif isinstance(bind, Engine): 

476 ctx = bind.connect() 

477 else: 

478 raise sa_exc.ArgumentError( 

479 f"Expected Engine or Connection, got {bind!r}" 

480 ) 

481 

482 with ctx as conn: 

483 for (metadata, schema), table_names in metadata_to_table.items(): 

484 metadata.reflect( 

485 conn, 

486 only=table_names, 

487 schema=schema, 

488 extend_existing=True, 

489 autoload_replace=False, 

490 **reflect_kw, 

491 ) 

492 

493 metadata_to_table.clear() 

494 

495 # .map() each class, then go through relationships and look 

496 # for secondary 

497 for thingy in to_map: 

498 thingy.map() 

499 

500 mapper = thingy.cls.__mapper__ 

501 metadata = mapper.class_.metadata 

502 

503 for rel in mapper._props.values(): 

504 if ( 

505 isinstance(rel, relationships.RelationshipProperty) 

506 and rel._init_args.secondary._is_populated() 

507 ): 

508 secondary_arg = rel._init_args.secondary 

509 

510 if isinstance(secondary_arg.argument, Table): 

511 secondary_table = secondary_arg.argument 

512 metadata_to_table[ 

513 ( 

514 secondary_table.metadata, 

515 secondary_table.schema, 

516 ) 

517 ].add(secondary_table.name) 

518 elif isinstance(secondary_arg.argument, str): 

519 _, resolve_arg = _resolver(rel.parent.class_, rel) 

520 

521 resolver = resolve_arg( 

522 secondary_arg.argument, True 

523 ) 

524 metadata_to_table[ 

525 (metadata, thingy.local_table.schema) 

526 ].add(secondary_arg.argument) 

527 

528 resolver._resolvers += ( 

529 cls._sa_deferred_table_resolver(metadata), 

530 ) 

531 

532 secondary_arg.argument = resolver() 

533 

534 for (metadata, schema), table_names in metadata_to_table.items(): 

535 metadata.reflect( 

536 conn, 

537 only=table_names, 

538 schema=schema, 

539 extend_existing=True, 

540 autoload_replace=False, 

541 ) 

542 

543 @classmethod 

544 def _sa_deferred_table_resolver( 

545 cls, metadata: MetaData 

546 ) -> Callable[[str], Table]: 

547 def _resolve(key: str) -> Table: 

548 # reflection has already occurred so this Table would have 

549 # its contents already 

550 return Table(key, metadata) 

551 

552 return _resolve 

553 

554 _sa_decl_prepare = True 

555 

556 @classmethod 

557 def _sa_raise_deferred_config(cls): 

558 raise orm_exc.UnmappedClassError( 

559 cls, 

560 msg="Class %s is a subclass of DeferredReflection. " 

561 "Mappings are not produced until the .prepare() " 

562 "method is called on the class hierarchy." 

563 % orm_exc._safe_cls_name(cls), 

564 )