naming.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. # sql/naming.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: allow-untyped-defs, allow-untyped-calls
  8. """Establish constraint and index naming conventions."""
  9. from __future__ import annotations
  10. import re
  11. from . import events # noqa
  12. from .base import _NONE_NAME
  13. from .elements import conv as conv
  14. from .schema import CheckConstraint
  15. from .schema import Column
  16. from .schema import Constraint
  17. from .schema import ForeignKeyConstraint
  18. from .schema import Index
  19. from .schema import PrimaryKeyConstraint
  20. from .schema import Table
  21. from .schema import UniqueConstraint
  22. from .. import event
  23. from .. import exc
  24. class ConventionDict:
  25. def __init__(self, const, table, convention):
  26. self.const = const
  27. self._is_fk = isinstance(const, ForeignKeyConstraint)
  28. self.table = table
  29. self.convention = convention
  30. self._const_name = const.name
  31. def _key_table_name(self):
  32. return self.table.name
  33. def _column_X(self, idx, attrname):
  34. if self._is_fk:
  35. try:
  36. fk = self.const.elements[idx]
  37. except IndexError:
  38. return ""
  39. else:
  40. return getattr(fk.parent, attrname)
  41. else:
  42. cols = list(self.const.columns)
  43. try:
  44. col = cols[idx]
  45. except IndexError:
  46. return ""
  47. else:
  48. return getattr(col, attrname)
  49. def _key_constraint_name(self):
  50. if self._const_name in (None, _NONE_NAME):
  51. raise exc.InvalidRequestError(
  52. "Naming convention including "
  53. "%(constraint_name)s token requires that "
  54. "constraint is explicitly named."
  55. )
  56. if not isinstance(self._const_name, conv):
  57. self.const.name = None
  58. return self._const_name
  59. def _key_column_X_key(self, idx):
  60. # note this method was missing before
  61. # [ticket:3989], meaning tokens like ``%(column_0_key)s`` weren't
  62. # working even though documented.
  63. return self._column_X(idx, "key")
  64. def _key_column_X_name(self, idx):
  65. return self._column_X(idx, "name")
  66. def _key_column_X_label(self, idx):
  67. return self._column_X(idx, "_ddl_label")
  68. def _key_referred_table_name(self):
  69. fk = self.const.elements[0]
  70. refs = fk.target_fullname.split(".")
  71. if len(refs) == 3:
  72. refschema, reftable, refcol = refs
  73. else:
  74. reftable, refcol = refs
  75. return reftable
  76. def _key_referred_column_X_name(self, idx):
  77. fk = self.const.elements[idx]
  78. # note that before [ticket:3989], this method was returning
  79. # the specification for the :class:`.ForeignKey` itself, which normally
  80. # would be using the ``.key`` of the column, not the name.
  81. return fk.column.name
  82. def __getitem__(self, key):
  83. if key in self.convention:
  84. return self.convention[key](self.const, self.table)
  85. elif hasattr(self, "_key_%s" % key):
  86. return getattr(self, "_key_%s" % key)()
  87. else:
  88. col_template = re.match(r".*_?column_(\d+)(_?N)?_.+", key)
  89. if col_template:
  90. idx = col_template.group(1)
  91. multiples = col_template.group(2)
  92. if multiples:
  93. if self._is_fk:
  94. elems = self.const.elements
  95. else:
  96. elems = list(self.const.columns)
  97. tokens = []
  98. for idx, elem in enumerate(elems):
  99. attr = "_key_" + key.replace("0" + multiples, "X")
  100. try:
  101. tokens.append(getattr(self, attr)(idx))
  102. except AttributeError:
  103. raise KeyError(key)
  104. sep = "_" if multiples.startswith("_") else ""
  105. return sep.join(tokens)
  106. else:
  107. attr = "_key_" + key.replace(idx, "X")
  108. idx = int(idx)
  109. if hasattr(self, attr):
  110. return getattr(self, attr)(idx)
  111. raise KeyError(key)
  112. _prefix_dict = {
  113. Index: "ix",
  114. PrimaryKeyConstraint: "pk",
  115. CheckConstraint: "ck",
  116. UniqueConstraint: "uq",
  117. ForeignKeyConstraint: "fk",
  118. }
  119. def _get_convention(dict_, key):
  120. for super_ in key.__mro__:
  121. if super_ in _prefix_dict and _prefix_dict[super_] in dict_:
  122. return dict_[_prefix_dict[super_]]
  123. elif super_ in dict_:
  124. return dict_[super_]
  125. else:
  126. return None
  127. def _constraint_name_for_table(const, table):
  128. metadata = table.metadata
  129. convention = _get_convention(metadata.naming_convention, type(const))
  130. if isinstance(const.name, conv):
  131. return const.name
  132. elif (
  133. convention is not None
  134. and not isinstance(const.name, conv)
  135. and (
  136. const.name is None
  137. or "constraint_name" in convention
  138. or const.name is _NONE_NAME
  139. )
  140. ):
  141. return conv(
  142. convention
  143. % ConventionDict(const, table, metadata.naming_convention)
  144. )
  145. elif convention is _NONE_NAME:
  146. return None
  147. @event.listens_for(
  148. PrimaryKeyConstraint, "_sa_event_column_added_to_pk_constraint"
  149. )
  150. def _column_added_to_pk_constraint(pk_constraint, col):
  151. if pk_constraint._implicit_generated:
  152. # only operate upon the "implicit" pk constraint for now,
  153. # as we have to force the name to None to reset it. the
  154. # "implicit" constraint will only have a naming convention name
  155. # if at all.
  156. table = pk_constraint.table
  157. pk_constraint.name = None
  158. newname = _constraint_name_for_table(pk_constraint, table)
  159. if newname:
  160. pk_constraint.name = newname
  161. @event.listens_for(Constraint, "after_parent_attach")
  162. @event.listens_for(Index, "after_parent_attach")
  163. def _constraint_name(const, table):
  164. if isinstance(table, Column):
  165. # this path occurs for a CheckConstraint linked to a Column
  166. # for column-attached constraint, set another event
  167. # to link the column attached to the table as this constraint
  168. # associated with the table.
  169. event.listen(
  170. table,
  171. "after_parent_attach",
  172. lambda col, table: _constraint_name(const, table),
  173. )
  174. elif isinstance(table, Table):
  175. if isinstance(const.name, conv) or const.name is _NONE_NAME:
  176. return
  177. newname = _constraint_name_for_table(const, table)
  178. if newname:
  179. const.name = newname