alembic 1.12.1__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.
@@ -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
alembic/ddl/impl.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections import namedtuple
3
+ import logging
4
4
  import re
5
5
  from typing import Any
6
6
  from typing import Callable
@@ -8,6 +8,7 @@ from typing import Dict
8
8
  from typing import Iterable
9
9
  from typing import List
10
10
  from typing import Mapping
11
+ from typing import NamedTuple
11
12
  from typing import Optional
12
13
  from typing import Sequence
13
14
  from typing import Set
@@ -20,7 +21,10 @@ from sqlalchemy import cast
20
21
  from sqlalchemy import schema
21
22
  from sqlalchemy import text
22
23
 
24
+ from . import _autogen
23
25
  from . import base
26
+ from ._autogen import _constraint_sig
27
+ from ._autogen import ComparisonResult
24
28
  from .. import util
25
29
  from ..util import sqla_compat
26
30
 
@@ -50,6 +54,8 @@ if TYPE_CHECKING:
50
54
  from ..operations.batch import ApplyBatchImpl
51
55
  from ..operations.batch import BatchOperationsImpl
52
56
 
57
+ log = logging.getLogger(__name__)
58
+
53
59
 
54
60
  class ImplMeta(type):
55
61
  def __init__(
@@ -66,8 +72,6 @@ class ImplMeta(type):
66
72
 
67
73
  _impls: Dict[str, Type[DefaultImpl]] = {}
68
74
 
69
- Params = namedtuple("Params", ["token0", "tokens", "args", "kwargs"])
70
-
71
75
 
72
76
  class DefaultImpl(metaclass=ImplMeta):
73
77
 
@@ -438,6 +442,7 @@ class DefaultImpl(metaclass=ImplMeta):
438
442
  )
439
443
 
440
444
  def _tokenize_column_type(self, column: Column) -> Params:
445
+ definition: str
441
446
  definition = self.dialect.type_compiler.process(column.type).lower()
442
447
 
443
448
  # tokenize the SQLAlchemy-generated version of a type, so that
@@ -452,9 +457,9 @@ class DefaultImpl(metaclass=ImplMeta):
452
457
  # varchar character set utf8
453
458
  #
454
459
 
455
- tokens = re.findall(r"[\w\-_]+|\(.+?\)", definition)
460
+ tokens: List[str] = re.findall(r"[\w\-_]+|\(.+?\)", definition)
456
461
 
457
- term_tokens = []
462
+ term_tokens: List[str] = []
458
463
  paren_term = None
459
464
 
460
465
  for token in tokens:
@@ -466,6 +471,7 @@ class DefaultImpl(metaclass=ImplMeta):
466
471
  params = Params(term_tokens[0], term_tokens[1:], [], {})
467
472
 
468
473
  if paren_term:
474
+ term: str
469
475
  for term in re.findall("[^(),]+", paren_term):
470
476
  if "=" in term:
471
477
  key, val = term.split("=")
@@ -664,15 +670,96 @@ class DefaultImpl(metaclass=ImplMeta):
664
670
  bool(diff) or bool(metadata_identity) != bool(inspector_identity),
665
671
  )
666
672
 
667
- def create_index_sig(self, index: Index) -> Tuple[Any, ...]:
668
- # order of col matters in an index
669
- return tuple(col.name for col in index.columns)
673
+ def _compare_index_unique(
674
+ self, metadata_index: Index, reflected_index: Index
675
+ ) -> Optional[str]:
676
+ conn_unique = bool(reflected_index.unique)
677
+ meta_unique = bool(metadata_index.unique)
678
+ if conn_unique != meta_unique:
679
+ return f"unique={conn_unique} to unique={meta_unique}"
680
+ else:
681
+ return None
682
+
683
+ def _create_metadata_constraint_sig(
684
+ self, constraint: _autogen._C, **opts: Any
685
+ ) -> _constraint_sig[_autogen._C]:
686
+ return _constraint_sig.from_constraint(True, self, constraint, **opts)
687
+
688
+ def _create_reflected_constraint_sig(
689
+ self, constraint: _autogen._C, **opts: Any
690
+ ) -> _constraint_sig[_autogen._C]:
691
+ return _constraint_sig.from_constraint(False, self, constraint, **opts)
692
+
693
+ def compare_indexes(
694
+ self,
695
+ metadata_index: Index,
696
+ reflected_index: Index,
697
+ ) -> ComparisonResult:
698
+ """Compare two indexes by comparing the signature generated by
699
+ ``create_index_sig``.
670
700
 
671
- def create_unique_constraint_sig(
672
- self, const: UniqueConstraint
673
- ) -> Tuple[Any, ...]:
674
- # order of col does not matters in an unique constraint
675
- return tuple(sorted([col.name for col in const.columns]))
701
+ This method returns a ``ComparisonResult``.
702
+ """
703
+ msg: List[str] = []
704
+ unique_msg = self._compare_index_unique(
705
+ metadata_index, reflected_index
706
+ )
707
+ if unique_msg:
708
+ msg.append(unique_msg)
709
+ m_sig = self._create_metadata_constraint_sig(metadata_index)
710
+ r_sig = self._create_reflected_constraint_sig(reflected_index)
711
+
712
+ assert _autogen.is_index_sig(m_sig)
713
+ assert _autogen.is_index_sig(r_sig)
714
+
715
+ # The assumption is that the index have no expression
716
+ for sig in m_sig, r_sig:
717
+ if sig.has_expressions:
718
+ log.warning(
719
+ "Generating approximate signature for index %s. "
720
+ "The dialect "
721
+ "implementation should either skip expression indexes "
722
+ "or provide a custom implementation.",
723
+ sig.const,
724
+ )
725
+
726
+ if m_sig.column_names != r_sig.column_names:
727
+ msg.append(
728
+ f"expression {r_sig.column_names} to {m_sig.column_names}"
729
+ )
730
+
731
+ if msg:
732
+ return ComparisonResult.Different(msg)
733
+ else:
734
+ return ComparisonResult.Equal()
735
+
736
+ def compare_unique_constraint(
737
+ self,
738
+ metadata_constraint: UniqueConstraint,
739
+ reflected_constraint: UniqueConstraint,
740
+ ) -> ComparisonResult:
741
+ """Compare two unique constraints by comparing the two signatures.
742
+
743
+ The arguments are two tuples that contain the unique constraint and
744
+ the signatures generated by ``create_unique_constraint_sig``.
745
+
746
+ This method returns a ``ComparisonResult``.
747
+ """
748
+ metadata_tup = self._create_metadata_constraint_sig(
749
+ metadata_constraint
750
+ )
751
+ reflected_tup = self._create_reflected_constraint_sig(
752
+ reflected_constraint
753
+ )
754
+
755
+ meta_sig = metadata_tup.unnamed
756
+ conn_sig = reflected_tup.unnamed
757
+ if conn_sig != meta_sig:
758
+ return ComparisonResult.Different(
759
+ f"expression {conn_sig} to {meta_sig}"
760
+ )
761
+ else:
762
+ return ComparisonResult.Equal()
676
763
 
677
764
  def _skip_functional_indexes(self, metadata_indexes, conn_indexes):
678
765
  conn_indexes_by_name = {c.name: c for c in conn_indexes}
@@ -697,6 +784,13 @@ class DefaultImpl(metaclass=ImplMeta):
697
784
  return reflected_object.get("dialect_options", {})
698
785
 
699
786
 
787
+ class Params(NamedTuple):
788
+ token0: str
789
+ tokens: List[str]
790
+ args: List[str]
791
+ kwargs: Dict[str, str]
792
+
793
+
700
794
  def _compare_identity_options(
701
795
  metadata_io: Union[schema.Identity, schema.Sequence, None],
702
796
  inspector_io: Union[schema.Identity, schema.Sequence, None],
alembic/ddl/mysql.py CHANGED
@@ -20,7 +20,6 @@ from .base import format_column_name
20
20
  from .base import format_server_default
21
21
  from .impl import DefaultImpl
22
22
  from .. import util
23
- from ..autogenerate import compare
24
23
  from ..util import sqla_compat
25
24
  from ..util.sqla_compat import _is_mariadb
26
25
  from ..util.sqla_compat import _is_type_bound
@@ -272,10 +271,12 @@ class MySQLImpl(DefaultImpl):
272
271
 
273
272
  def correct_for_autogen_foreignkeys(self, conn_fks, metadata_fks):
274
273
  conn_fk_by_sig = {
275
- compare._fk_constraint_sig(fk).sig: fk for fk in conn_fks
274
+ self._create_reflected_constraint_sig(fk).unnamed_no_options: fk
275
+ for fk in conn_fks
276
276
  }
277
277
  metadata_fk_by_sig = {
278
- compare._fk_constraint_sig(fk).sig: fk for fk in metadata_fks
278
+ self._create_metadata_constraint_sig(fk).unnamed_no_options: fk
279
+ for fk in metadata_fks
279
280
  }
280
281
 
281
282
  for sig in set(conn_fk_by_sig).intersection(metadata_fk_by_sig):