alembic 1.12.0__py3-none-any.whl → 1.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. alembic/__init__.py +1 -3
  2. alembic/autogenerate/api.py +14 -6
  3. alembic/autogenerate/compare.py +129 -195
  4. alembic/autogenerate/render.py +42 -32
  5. alembic/autogenerate/rewriter.py +19 -19
  6. alembic/command.py +11 -9
  7. alembic/config.py +1 -1
  8. alembic/context.pyi +12 -5
  9. alembic/ddl/_autogen.py +323 -0
  10. alembic/ddl/impl.py +167 -41
  11. alembic/ddl/mssql.py +1 -4
  12. alembic/ddl/mysql.py +4 -3
  13. alembic/ddl/postgresql.py +157 -70
  14. alembic/op.pyi +9 -11
  15. alembic/operations/__init__.py +2 -0
  16. alembic/operations/base.py +10 -11
  17. alembic/operations/ops.py +14 -14
  18. alembic/operations/toimpl.py +5 -5
  19. alembic/runtime/environment.py +7 -5
  20. alembic/runtime/migration.py +4 -4
  21. alembic/script/base.py +25 -17
  22. alembic/script/revision.py +30 -25
  23. alembic/templates/async/alembic.ini.mako +3 -3
  24. alembic/templates/generic/alembic.ini.mako +3 -3
  25. alembic/templates/multidb/alembic.ini.mako +3 -3
  26. alembic/testing/requirements.py +12 -4
  27. alembic/testing/schemacompare.py +9 -0
  28. alembic/testing/suite/test_autogen_identity.py +23 -38
  29. alembic/util/compat.py +0 -1
  30. alembic/util/sqla_compat.py +58 -30
  31. {alembic-1.12.0.dist-info → alembic-1.13.0.dist-info}/METADATA +8 -6
  32. {alembic-1.12.0.dist-info → alembic-1.13.0.dist-info}/RECORD +36 -35
  33. {alembic-1.12.0.dist-info → alembic-1.13.0.dist-info}/WHEEL +1 -1
  34. {alembic-1.12.0.dist-info → alembic-1.13.0.dist-info}/LICENSE +0 -0
  35. {alembic-1.12.0.dist-info → alembic-1.13.0.dist-info}/entry_points.txt +0 -0
  36. {alembic-1.12.0.dist-info → alembic-1.13.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections import OrderedDict
4
3
  from io import StringIO
5
4
  import re
6
5
  from typing import Any
@@ -165,14 +164,22 @@ def _render_modify_table(
165
164
  def _render_create_table_comment(
166
165
  autogen_context: AutogenContext, op: ops.CreateTableCommentOp
167
166
  ) -> str:
168
- templ = (
169
- "{prefix}create_table_comment(\n"
170
- "{indent}'{tname}',\n"
171
- "{indent}{comment},\n"
172
- "{indent}existing_comment={existing},\n"
173
- "{indent}schema={schema}\n"
174
- ")"
175
- )
167
+ if autogen_context._has_batch:
168
+ templ = (
169
+ "{prefix}create_table_comment(\n"
170
+ "{indent}{comment},\n"
171
+ "{indent}existing_comment={existing}\n"
172
+ ")"
173
+ )
174
+ else:
175
+ templ = (
176
+ "{prefix}create_table_comment(\n"
177
+ "{indent}'{tname}',\n"
178
+ "{indent}{comment},\n"
179
+ "{indent}existing_comment={existing},\n"
180
+ "{indent}schema={schema}\n"
181
+ ")"
182
+ )
176
183
  return templ.format(
177
184
  prefix=_alembic_autogenerate_prefix(autogen_context),
178
185
  tname=op.table_name,
@@ -189,13 +196,20 @@ def _render_create_table_comment(
189
196
  def _render_drop_table_comment(
190
197
  autogen_context: AutogenContext, op: ops.DropTableCommentOp
191
198
  ) -> str:
192
- templ = (
193
- "{prefix}drop_table_comment(\n"
194
- "{indent}'{tname}',\n"
195
- "{indent}existing_comment={existing},\n"
196
- "{indent}schema={schema}\n"
197
- ")"
198
- )
199
+ if autogen_context._has_batch:
200
+ templ = (
201
+ "{prefix}drop_table_comment(\n"
202
+ "{indent}existing_comment={existing}\n"
203
+ ")"
204
+ )
205
+ else:
206
+ templ = (
207
+ "{prefix}drop_table_comment(\n"
208
+ "{indent}'{tname}',\n"
209
+ "{indent}existing_comment={existing},\n"
210
+ "{indent}schema={schema}\n"
211
+ ")"
212
+ )
199
213
  return templ.format(
200
214
  prefix=_alembic_autogenerate_prefix(autogen_context),
201
215
  tname=op.table_name,
@@ -246,6 +260,11 @@ def _add_table(autogen_context: AutogenContext, op: ops.CreateTableOp) -> str:
246
260
  comment = table.comment
247
261
  if comment:
248
262
  text += ",\ncomment=%r" % _ident(comment)
263
+
264
+ info = table.info
265
+ if info:
266
+ text += f",\ninfo={info!r}"
267
+
249
268
  for k in sorted(op.kw):
250
269
  text += ",\n%s=%r" % (k.replace(" ", "_"), op.kw[k])
251
270
 
@@ -544,8 +563,10 @@ def _ident(name: Optional[Union[quoted_name, str]]) -> Optional[str]:
544
563
  def _render_potential_expr(
545
564
  value: Any,
546
565
  autogen_context: AutogenContext,
566
+ *,
547
567
  wrap_in_text: bool = True,
548
568
  is_server_default: bool = False,
569
+ is_index: bool = False,
549
570
  ) -> str:
550
571
  if isinstance(value, sql.ClauseElement):
551
572
  if wrap_in_text:
@@ -556,7 +577,7 @@ def _render_potential_expr(
556
577
  return template % {
557
578
  "prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
558
579
  "sql": autogen_context.migration_context.impl.render_ddl_sql_expr(
559
- value, is_server_default=is_server_default
580
+ value, is_server_default=is_server_default, is_index=is_index
560
581
  ),
561
582
  }
562
583
 
@@ -570,7 +591,7 @@ def _get_index_rendered_expressions(
570
591
  return [
571
592
  repr(_ident(getattr(exp, "name", None)))
572
593
  if isinstance(exp, sa_schema.Column)
573
- else _render_potential_expr(exp, autogen_context)
594
+ else _render_potential_expr(exp, autogen_context, is_index=True)
574
595
  for exp in idx.expressions
575
596
  ]
576
597
 
@@ -760,11 +781,9 @@ def _render_computed(
760
781
  def _render_identity(
761
782
  identity: Identity, autogen_context: AutogenContext
762
783
  ) -> str:
763
- # always=None means something different than always=False
764
- kwargs = OrderedDict(always=identity.always)
765
- if identity.on_null is not None:
766
- kwargs["on_null"] = identity.on_null
767
- kwargs.update(_get_identity_options(identity))
784
+ kwargs = sqla_compat._get_identity_options_dict(
785
+ identity, dialect_kwargs=True
786
+ )
768
787
 
769
788
  return "%(prefix)sIdentity(%(kwargs)s)" % {
770
789
  "prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
@@ -772,15 +791,6 @@ def _render_identity(
772
791
  }
773
792
 
774
793
 
775
- def _get_identity_options(identity_options: Identity) -> OrderedDict:
776
- kwargs = OrderedDict()
777
- for attr in sqla_compat._identity_options_attrs:
778
- value = getattr(identity_options, attr, None)
779
- if value is not None:
780
- kwargs[attr] = value
781
- return kwargs
782
-
783
-
784
794
  def _repr_type(
785
795
  type_: TypeEngine,
786
796
  autogen_context: AutogenContext,
@@ -9,19 +9,19 @@ from typing import Type
9
9
  from typing import TYPE_CHECKING
10
10
  from typing import Union
11
11
 
12
- from alembic import util
13
- from alembic.operations import ops
12
+ from .. import util
13
+ from ..operations import ops
14
14
 
15
15
  if TYPE_CHECKING:
16
- from alembic.operations.ops import AddColumnOp
17
- from alembic.operations.ops import AlterColumnOp
18
- from alembic.operations.ops import CreateTableOp
19
- from alembic.operations.ops import MigrateOperation
20
- from alembic.operations.ops import MigrationScript
21
- from alembic.operations.ops import ModifyTableOps
22
- from alembic.operations.ops import OpContainer
23
- from alembic.runtime.migration import MigrationContext
24
- from alembic.script.revision import Revision
16
+ from ..operations.ops import AddColumnOp
17
+ from ..operations.ops import AlterColumnOp
18
+ from ..operations.ops import CreateTableOp
19
+ from ..operations.ops import MigrateOperation
20
+ from ..operations.ops import MigrationScript
21
+ from ..operations.ops import ModifyTableOps
22
+ from ..operations.ops import OpContainer
23
+ from ..runtime.environment import _GetRevArg
24
+ from ..runtime.migration import MigrationContext
25
25
 
26
26
 
27
27
  class Rewriter:
@@ -119,7 +119,7 @@ class Rewriter:
119
119
  def _rewrite(
120
120
  self,
121
121
  context: MigrationContext,
122
- revision: Revision,
122
+ revision: _GetRevArg,
123
123
  directive: MigrateOperation,
124
124
  ) -> Iterator[MigrateOperation]:
125
125
  try:
@@ -142,7 +142,7 @@ class Rewriter:
142
142
  def __call__(
143
143
  self,
144
144
  context: MigrationContext,
145
- revision: Revision,
145
+ revision: _GetRevArg,
146
146
  directives: List[MigrationScript],
147
147
  ) -> None:
148
148
  self.process_revision_directives(context, revision, directives)
@@ -153,7 +153,7 @@ class Rewriter:
153
153
  def _traverse_script(
154
154
  self,
155
155
  context: MigrationContext,
156
- revision: Revision,
156
+ revision: _GetRevArg,
157
157
  directive: MigrationScript,
158
158
  ) -> None:
159
159
  upgrade_ops_list = []
@@ -180,7 +180,7 @@ class Rewriter:
180
180
  def _traverse_op_container(
181
181
  self,
182
182
  context: MigrationContext,
183
- revision: Revision,
183
+ revision: _GetRevArg,
184
184
  directive: OpContainer,
185
185
  ) -> None:
186
186
  self._traverse_list(context, revision, directive.ops)
@@ -189,7 +189,7 @@ class Rewriter:
189
189
  def _traverse_any_directive(
190
190
  self,
191
191
  context: MigrationContext,
192
- revision: Revision,
192
+ revision: _GetRevArg,
193
193
  directive: MigrateOperation,
194
194
  ) -> None:
195
195
  pass
@@ -197,7 +197,7 @@ class Rewriter:
197
197
  def _traverse_for(
198
198
  self,
199
199
  context: MigrationContext,
200
- revision: Revision,
200
+ revision: _GetRevArg,
201
201
  directive: MigrateOperation,
202
202
  ) -> Any:
203
203
  directives = list(self._rewrite(context, revision, directive))
@@ -209,7 +209,7 @@ class Rewriter:
209
209
  def _traverse_list(
210
210
  self,
211
211
  context: MigrationContext,
212
- revision: Revision,
212
+ revision: _GetRevArg,
213
213
  directives: Any,
214
214
  ) -> None:
215
215
  dest = []
@@ -221,7 +221,7 @@ class Rewriter:
221
221
  def process_revision_directives(
222
222
  self,
223
223
  context: MigrationContext,
224
- revision: Revision,
224
+ revision: _GetRevArg,
225
225
  directives: List[MigrationScript],
226
226
  ) -> None:
227
227
  self._traverse_list(context, revision, directives)
alembic/command.py CHANGED
@@ -14,6 +14,7 @@ from .script import ScriptDirectory
14
14
  if TYPE_CHECKING:
15
15
  from alembic.config import Config
16
16
  from alembic.script.base import Script
17
+ from alembic.script.revision import _RevIdType
17
18
  from .runtime.environment import ProcessRevisionDirectiveFn
18
19
 
19
20
 
@@ -105,7 +106,7 @@ def init(
105
106
  os.path.join(os.path.abspath(directory), "__init__.py"),
106
107
  os.path.join(os.path.abspath(versions), "__init__.py"),
107
108
  ]:
108
- with util.status("Adding {path!r}", **config.messaging_opts):
109
+ with util.status(f"Adding {path!r}", **config.messaging_opts):
109
110
  with open(path, "w"):
110
111
  pass
111
112
 
@@ -124,7 +125,7 @@ def revision(
124
125
  sql: bool = False,
125
126
  head: str = "head",
126
127
  splice: bool = False,
127
- branch_label: Optional[str] = None,
128
+ branch_label: Optional[_RevIdType] = None,
128
129
  version_path: Optional[str] = None,
129
130
  rev_id: Optional[str] = None,
130
131
  depends_on: Optional[str] = None,
@@ -244,9 +245,7 @@ def revision(
244
245
  return scripts
245
246
 
246
247
 
247
- def check(
248
- config: "Config",
249
- ) -> None:
248
+ def check(config: "Config") -> None:
250
249
  """Check if revision command with autogenerate has pending upgrade ops.
251
250
 
252
251
  :param config: a :class:`.Config` object.
@@ -291,7 +290,10 @@ def check(
291
290
  # the revision_context now has MigrationScript structure(s) present.
292
291
 
293
292
  migration_script = revision_context.generated_revisions[-1]
294
- diffs = migration_script.upgrade_ops.as_diffs()
293
+ diffs = []
294
+ for upgrade_ops in migration_script.upgrade_ops_list:
295
+ diffs.extend(upgrade_ops.as_diffs())
296
+
295
297
  if diffs:
296
298
  raise util.AutogenerateDiffsDetected(
297
299
  f"New upgrade operations detected: {diffs}"
@@ -302,9 +304,9 @@ def check(
302
304
 
303
305
  def merge(
304
306
  config: Config,
305
- revisions: str,
307
+ revisions: _RevIdType,
306
308
  message: Optional[str] = None,
307
- branch_label: Optional[str] = None,
309
+ branch_label: Optional[_RevIdType] = None,
308
310
  rev_id: Optional[str] = None,
309
311
  ) -> Optional[Script]:
310
312
  """Merge two revisions together. Creates a new migration file.
@@ -623,7 +625,7 @@ def current(config: Config, verbose: bool = False) -> None:
623
625
 
624
626
  def stamp(
625
627
  config: Config,
626
- revision: str,
628
+ revision: _RevIdType,
627
629
  sql: bool = False,
628
630
  tag: Optional[str] = None,
629
631
  purge: bool = False,
alembic/config.py CHANGED
@@ -34,7 +34,7 @@ class Config:
34
34
 
35
35
  some_param = context.config.get_main_option("my option")
36
36
 
37
- When invoking Alembic programatically, a new
37
+ When invoking Alembic programmatically, a new
38
38
  :class:`.Config` can be created by passing
39
39
  the name of an .ini file to the constructor::
40
40
 
alembic/context.pyi CHANGED
@@ -7,12 +7,14 @@ from typing import Callable
7
7
  from typing import Collection
8
8
  from typing import ContextManager
9
9
  from typing import Dict
10
+ from typing import Iterable
10
11
  from typing import List
11
12
  from typing import Literal
12
13
  from typing import Mapping
13
14
  from typing import MutableMapping
14
15
  from typing import Optional
15
16
  from typing import overload
17
+ from typing import Sequence
16
18
  from typing import TextIO
17
19
  from typing import Tuple
18
20
  from typing import TYPE_CHECKING
@@ -21,7 +23,7 @@ from typing import Union
21
23
  if TYPE_CHECKING:
22
24
  from sqlalchemy.engine.base import Connection
23
25
  from sqlalchemy.engine.url import URL
24
- from sqlalchemy.sql.elements import ClauseElement
26
+ from sqlalchemy.sql import Executable
25
27
  from sqlalchemy.sql.schema import Column
26
28
  from sqlalchemy.sql.schema import FetchedValue
27
29
  from sqlalchemy.sql.schema import MetaData
@@ -30,7 +32,7 @@ if TYPE_CHECKING:
30
32
 
31
33
  from .autogenerate.api import AutogenContext
32
34
  from .config import Config
33
- from .operations.ops import MigrateOperation
35
+ from .operations.ops import MigrationScript
34
36
  from .runtime.migration import _ProxyTransaction
35
37
  from .runtime.migration import MigrationContext
36
38
  from .runtime.migration import MigrationInfo
@@ -96,7 +98,7 @@ def configure(
96
98
  tag: Optional[str] = None,
97
99
  template_args: Optional[Dict[str, Any]] = None,
98
100
  render_as_batch: bool = False,
99
- target_metadata: Optional[MetaData] = None,
101
+ target_metadata: Union[MetaData, Sequence[MetaData], None] = None,
100
102
  include_name: Optional[
101
103
  Callable[
102
104
  [
@@ -143,7 +145,12 @@ def configure(
143
145
  include_schemas: bool = False,
144
146
  process_revision_directives: Optional[
145
147
  Callable[
146
- [MigrationContext, Tuple[str, str], List[MigrateOperation]], None
148
+ [
149
+ MigrationContext,
150
+ Union[str, Iterable[Optional[str]], Iterable[str]],
151
+ List[MigrationScript],
152
+ ],
153
+ None,
147
154
  ]
148
155
  ] = None,
149
156
  compare_type: Union[
@@ -629,7 +636,7 @@ def configure(
629
636
  """
630
637
 
631
638
  def execute(
632
- sql: Union[ClauseElement, str], execution_options: Optional[dict] = None
639
+ sql: Union[Executable, str], execution_options: Optional[dict] = None
633
640
  ) -> None:
634
641
  """Execute the given SQL using the current change context.
635
642
 
@@ -0,0 +1,323 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ from typing import ClassVar
5
+ from typing import Dict
6
+ from typing import Generic
7
+ from typing import NamedTuple
8
+ from typing import Optional
9
+ from typing import Sequence
10
+ from typing import Tuple
11
+ from typing import Type
12
+ from typing import TYPE_CHECKING
13
+ from typing import TypeVar
14
+ from typing import Union
15
+
16
+ from sqlalchemy.sql.schema import Constraint
17
+ from sqlalchemy.sql.schema import ForeignKeyConstraint
18
+ from sqlalchemy.sql.schema import Index
19
+ from sqlalchemy.sql.schema import UniqueConstraint
20
+ from typing_extensions import TypeGuard
21
+
22
+ from alembic.ddl.base import _fk_spec
23
+ from .. import util
24
+ from ..util import sqla_compat
25
+
26
+ if TYPE_CHECKING:
27
+ from typing import Literal
28
+
29
+ from alembic.autogenerate.api import AutogenContext
30
+ from alembic.ddl.impl import DefaultImpl
31
+
32
+ CompareConstraintType = Union[Constraint, Index]
33
+
34
+ _C = TypeVar("_C", bound=CompareConstraintType)
35
+
36
+ _clsreg: Dict[str, Type[_constraint_sig]] = {}
37
+
38
+
39
+ class ComparisonResult(NamedTuple):
40
+ status: Literal["equal", "different", "skip"]
41
+ message: str
42
+
43
+ @property
44
+ def is_equal(self) -> bool:
45
+ return self.status == "equal"
46
+
47
+ @property
48
+ def is_different(self) -> bool:
49
+ return self.status == "different"
50
+
51
+ @property
52
+ def is_skip(self) -> bool:
53
+ return self.status == "skip"
54
+
55
+ @classmethod
56
+ def Equal(cls) -> ComparisonResult:
57
+ """the constraints are equal."""
58
+ return cls("equal", "The two constraints are equal")
59
+
60
+ @classmethod
61
+ def Different(cls, reason: Union[str, Sequence[str]]) -> ComparisonResult:
62
+ """the constraints are different for the provided reason(s)."""
63
+ return cls("different", ", ".join(util.to_list(reason)))
64
+
65
+ @classmethod
66
+ def Skip(cls, reason: Union[str, Sequence[str]]) -> ComparisonResult:
67
+ """the constraint cannot be compared for the provided reason(s).
68
+
69
+ The message is logged, but the constraints will be otherwise
70
+ considered equal, meaning that no migration command will be
71
+ generated.
72
+ """
73
+ return cls("skip", ", ".join(util.to_list(reason)))
74
+
75
+
76
+ class _constraint_sig(Generic[_C]):
77
+ const: _C
78
+
79
+ _sig: Tuple[Any, ...]
80
+ name: Optional[sqla_compat._ConstraintNameDefined]
81
+
82
+ impl: DefaultImpl
83
+
84
+ _is_index: ClassVar[bool] = False
85
+ _is_fk: ClassVar[bool] = False
86
+ _is_uq: ClassVar[bool] = False
87
+
88
+ _is_metadata: bool
89
+
90
+ def __init_subclass__(cls) -> None:
91
+ cls._register()
92
+
93
+ @classmethod
94
+ def _register(cls):
95
+ raise NotImplementedError()
96
+
97
+ def __init__(
98
+ self, is_metadata: bool, impl: DefaultImpl, const: _C
99
+ ) -> None:
100
+ raise NotImplementedError()
101
+
102
+ def compare_to_reflected(
103
+ self, other: _constraint_sig[Any]
104
+ ) -> ComparisonResult:
105
+ assert self.impl is other.impl
106
+ assert self._is_metadata
107
+ assert not other._is_metadata
108
+
109
+ return self._compare_to_reflected(other)
110
+
111
+ def _compare_to_reflected(
112
+ self, other: _constraint_sig[_C]
113
+ ) -> ComparisonResult:
114
+ raise NotImplementedError()
115
+
116
+ @classmethod
117
+ def from_constraint(
118
+ cls, is_metadata: bool, impl: DefaultImpl, constraint: _C
119
+ ) -> _constraint_sig[_C]:
120
+ # these could be cached by constraint/impl, however, if the
121
+ # constraint is modified in place, then the sig is wrong. the mysql
122
+ # impl currently does this, and if we fixed that we can't be sure
123
+ # someone else might do it too, so play it safe.
124
+ sig = _clsreg[constraint.__visit_name__](is_metadata, impl, constraint)
125
+ return sig
126
+
127
+ def md_name_to_sql_name(self, context: AutogenContext) -> Optional[str]:
128
+ return sqla_compat._get_constraint_final_name(
129
+ self.const, context.dialect
130
+ )
131
+
132
+ @util.memoized_property
133
+ def is_named(self):
134
+ return sqla_compat._constraint_is_named(self.const, self.impl.dialect)
135
+
136
+ @util.memoized_property
137
+ def unnamed(self) -> Tuple[Any, ...]:
138
+ return self._sig
139
+
140
+ @util.memoized_property
141
+ def unnamed_no_options(self) -> Tuple[Any, ...]:
142
+ raise NotImplementedError()
143
+
144
+ @util.memoized_property
145
+ def _full_sig(self) -> Tuple[Any, ...]:
146
+ return (self.name,) + self.unnamed
147
+
148
+ def __eq__(self, other) -> bool:
149
+ return self._full_sig == other._full_sig
150
+
151
+ def __ne__(self, other) -> bool:
152
+ return self._full_sig != other._full_sig
153
+
154
+ def __hash__(self) -> int:
155
+ return hash(self._full_sig)
156
+
157
+
158
+ class _uq_constraint_sig(_constraint_sig[UniqueConstraint]):
159
+ _is_uq = True
160
+
161
+ @classmethod
162
+ def _register(cls) -> None:
163
+ _clsreg["unique_constraint"] = cls
164
+
165
+ is_unique = True
166
+
167
+ def __init__(
168
+ self,
169
+ is_metadata: bool,
170
+ impl: DefaultImpl,
171
+ const: UniqueConstraint,
172
+ ) -> None:
173
+ self.impl = impl
174
+ self.const = const
175
+ self.name = sqla_compat.constraint_name_or_none(const.name)
176
+ self._sig = tuple(sorted([col.name for col in const.columns]))
177
+ self._is_metadata = is_metadata
178
+
179
+ @property
180
+ def column_names(self) -> Tuple[str, ...]:
181
+ return tuple([col.name for col in self.const.columns])
182
+
183
+ def _compare_to_reflected(
184
+ self, other: _constraint_sig[_C]
185
+ ) -> ComparisonResult:
186
+ assert self._is_metadata
187
+ metadata_obj = self
188
+ conn_obj = other
189
+
190
+ assert is_uq_sig(conn_obj)
191
+ return self.impl.compare_unique_constraint(
192
+ metadata_obj.const, conn_obj.const
193
+ )
194
+
195
+
196
+ class _ix_constraint_sig(_constraint_sig[Index]):
197
+ _is_index = True
198
+
199
+ name: sqla_compat._ConstraintName
200
+
201
+ @classmethod
202
+ def _register(cls) -> None:
203
+ _clsreg["index"] = cls
204
+
205
+ def __init__(
206
+ self, is_metadata: bool, impl: DefaultImpl, const: Index
207
+ ) -> None:
208
+ self.impl = impl
209
+ self.const = const
210
+ self.name = const.name
211
+ self.is_unique = bool(const.unique)
212
+ self._is_metadata = is_metadata
213
+
214
+ def _compare_to_reflected(
215
+ self, other: _constraint_sig[_C]
216
+ ) -> ComparisonResult:
217
+ assert self._is_metadata
218
+ metadata_obj = self
219
+ conn_obj = other
220
+
221
+ assert is_index_sig(conn_obj)
222
+ return self.impl.compare_indexes(metadata_obj.const, conn_obj.const)
223
+
224
+ @util.memoized_property
225
+ def has_expressions(self):
226
+ return sqla_compat.is_expression_index(self.const)
227
+
228
+ @util.memoized_property
229
+ def column_names(self) -> Tuple[str, ...]:
230
+ return tuple([col.name for col in self.const.columns])
231
+
232
+ @util.memoized_property
233
+ def column_names_optional(self) -> Tuple[Optional[str], ...]:
234
+ return tuple(
235
+ [getattr(col, "name", None) for col in self.const.expressions]
236
+ )
237
+
238
+ @util.memoized_property
239
+ def is_named(self):
240
+ return True
241
+
242
+ @util.memoized_property
243
+ def unnamed(self):
244
+ return (self.is_unique,) + self.column_names_optional
245
+
246
+
247
+ class _fk_constraint_sig(_constraint_sig[ForeignKeyConstraint]):
248
+ _is_fk = True
249
+
250
+ @classmethod
251
+ def _register(cls) -> None:
252
+ _clsreg["foreign_key_constraint"] = cls
253
+
254
+ def __init__(
255
+ self,
256
+ is_metadata: bool,
257
+ impl: DefaultImpl,
258
+ const: ForeignKeyConstraint,
259
+ ) -> None:
260
+ self._is_metadata = is_metadata
261
+
262
+ self.impl = impl
263
+ self.const = const
264
+
265
+ self.name = sqla_compat.constraint_name_or_none(const.name)
266
+
267
+ (
268
+ self.source_schema,
269
+ self.source_table,
270
+ self.source_columns,
271
+ self.target_schema,
272
+ self.target_table,
273
+ self.target_columns,
274
+ onupdate,
275
+ ondelete,
276
+ deferrable,
277
+ initially,
278
+ ) = _fk_spec(const)
279
+
280
+ self._sig: Tuple[Any, ...] = (
281
+ self.source_schema,
282
+ self.source_table,
283
+ tuple(self.source_columns),
284
+ self.target_schema,
285
+ self.target_table,
286
+ tuple(self.target_columns),
287
+ ) + (
288
+ (None if onupdate.lower() == "no action" else onupdate.lower())
289
+ if onupdate
290
+ else None,
291
+ (None if ondelete.lower() == "no action" else ondelete.lower())
292
+ if ondelete
293
+ else None,
294
+ # convert initially + deferrable into one three-state value
295
+ "initially_deferrable"
296
+ if initially and initially.lower() == "deferred"
297
+ else "deferrable"
298
+ if deferrable
299
+ else "not deferrable",
300
+ )
301
+
302
+ @util.memoized_property
303
+ def unnamed_no_options(self):
304
+ return (
305
+ self.source_schema,
306
+ self.source_table,
307
+ tuple(self.source_columns),
308
+ self.target_schema,
309
+ self.target_table,
310
+ tuple(self.target_columns),
311
+ )
312
+
313
+
314
+ def is_index_sig(sig: _constraint_sig) -> TypeGuard[_ix_constraint_sig]:
315
+ return sig._is_index
316
+
317
+
318
+ def is_uq_sig(sig: _constraint_sig) -> TypeGuard[_uq_constraint_sig]:
319
+ return sig._is_uq
320
+
321
+
322
+ def is_fk_sig(sig: _constraint_sig) -> TypeGuard[_fk_constraint_sig]:
323
+ return sig._is_fk