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
alembic/ddl/impl.py CHANGED
@@ -1,11 +1,14 @@
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
7
7
  from typing import Dict
8
+ from typing import Iterable
8
9
  from typing import List
10
+ from typing import Mapping
11
+ from typing import NamedTuple
9
12
  from typing import Optional
10
13
  from typing import Sequence
11
14
  from typing import Set
@@ -18,7 +21,10 @@ from sqlalchemy import cast
18
21
  from sqlalchemy import schema
19
22
  from sqlalchemy import text
20
23
 
24
+ from . import _autogen
21
25
  from . import base
26
+ from ._autogen import _constraint_sig
27
+ from ._autogen import ComparisonResult
22
28
  from .. import util
23
29
  from ..util import sqla_compat
24
30
 
@@ -30,7 +36,8 @@ if TYPE_CHECKING:
30
36
  from sqlalchemy.engine import Dialect
31
37
  from sqlalchemy.engine.cursor import CursorResult
32
38
  from sqlalchemy.engine.reflection import Inspector
33
- from sqlalchemy.sql.elements import ClauseElement
39
+ from sqlalchemy.sql import ClauseElement
40
+ from sqlalchemy.sql import Executable
34
41
  from sqlalchemy.sql.elements import ColumnElement
35
42
  from sqlalchemy.sql.elements import quoted_name
36
43
  from sqlalchemy.sql.schema import Column
@@ -47,6 +54,8 @@ if TYPE_CHECKING:
47
54
  from ..operations.batch import ApplyBatchImpl
48
55
  from ..operations.batch import BatchOperationsImpl
49
56
 
57
+ log = logging.getLogger(__name__)
58
+
50
59
 
51
60
  class ImplMeta(type):
52
61
  def __init__(
@@ -63,8 +72,6 @@ class ImplMeta(type):
63
72
 
64
73
  _impls: Dict[str, Type[DefaultImpl]] = {}
65
74
 
66
- Params = namedtuple("Params", ["token0", "tokens", "args", "kwargs"])
67
-
68
75
 
69
76
  class DefaultImpl(metaclass=ImplMeta):
70
77
 
@@ -86,8 +93,11 @@ class DefaultImpl(metaclass=ImplMeta):
86
93
  command_terminator = ";"
87
94
  type_synonyms: Tuple[Set[str], ...] = ({"NUMERIC", "DECIMAL"},)
88
95
  type_arg_extract: Sequence[str] = ()
89
- # on_null is known to be supported only by oracle
90
- identity_attrs_ignore: Tuple[str, ...] = ("on_null",)
96
+ # These attributes are deprecated in SQLAlchemy via #10247. They need to
97
+ # be ignored to support older version that did not use dialect kwargs.
98
+ # They only apply to Oracle and are replaced by oracle_order,
99
+ # oracle_on_null
100
+ identity_attrs_ignore: Tuple[str, ...] = ("order", "on_null")
91
101
 
92
102
  def __init__(
93
103
  self,
@@ -154,7 +164,7 @@ class DefaultImpl(metaclass=ImplMeta):
154
164
 
155
165
  def _exec(
156
166
  self,
157
- construct: Union[ClauseElement, str],
167
+ construct: Union[Executable, str],
158
168
  execution_options: Optional[dict[str, Any]] = None,
159
169
  multiparams: Sequence[dict] = (),
160
170
  params: Dict[str, Any] = util.immutabledict(),
@@ -166,6 +176,7 @@ class DefaultImpl(metaclass=ImplMeta):
166
176
  # TODO: coverage
167
177
  raise Exception("Execution arguments not allowed with as_sql")
168
178
 
179
+ compile_kw: dict[str, Any]
169
180
  if self.literal_binds and not isinstance(
170
181
  construct, schema.DDLElement
171
182
  ):
@@ -173,9 +184,9 @@ class DefaultImpl(metaclass=ImplMeta):
173
184
  else:
174
185
  compile_kw = {}
175
186
 
176
- compiled = construct.compile(
177
- dialect=self.dialect, **compile_kw # type: ignore[arg-type]
178
- )
187
+ if TYPE_CHECKING:
188
+ assert isinstance(construct, ClauseElement)
189
+ compiled = construct.compile(dialect=self.dialect, **compile_kw)
179
190
  self.static_output(
180
191
  str(compiled).replace("\t", " ").strip()
181
192
  + self.command_terminator
@@ -190,13 +201,11 @@ class DefaultImpl(metaclass=ImplMeta):
190
201
  assert isinstance(multiparams, tuple)
191
202
  multiparams += (params,)
192
203
 
193
- return conn.execute( # type: ignore[call-overload]
194
- construct, multiparams
195
- )
204
+ return conn.execute(construct, multiparams)
196
205
 
197
206
  def execute(
198
207
  self,
199
- sql: Union[ClauseElement, str],
208
+ sql: Union[Executable, str],
200
209
  execution_options: Optional[dict[str, Any]] = None,
201
210
  ) -> None:
202
211
  self._exec(sql, execution_options)
@@ -433,6 +442,7 @@ class DefaultImpl(metaclass=ImplMeta):
433
442
  )
434
443
 
435
444
  def _tokenize_column_type(self, column: Column) -> Params:
445
+ definition: str
436
446
  definition = self.dialect.type_compiler.process(column.type).lower()
437
447
 
438
448
  # tokenize the SQLAlchemy-generated version of a type, so that
@@ -447,9 +457,9 @@ class DefaultImpl(metaclass=ImplMeta):
447
457
  # varchar character set utf8
448
458
  #
449
459
 
450
- tokens = re.findall(r"[\w\-_]+|\(.+?\)", definition)
460
+ tokens: List[str] = re.findall(r"[\w\-_]+|\(.+?\)", definition)
451
461
 
452
- term_tokens = []
462
+ term_tokens: List[str] = []
453
463
  paren_term = None
454
464
 
455
465
  for token in tokens:
@@ -461,6 +471,7 @@ class DefaultImpl(metaclass=ImplMeta):
461
471
  params = Params(term_tokens[0], term_tokens[1:], [], {})
462
472
 
463
473
  if paren_term:
474
+ term: str
464
475
  for term in re.findall("[^(),]+", paren_term):
465
476
  if "=" in term:
466
477
  key, val = term.split("=")
@@ -573,13 +584,10 @@ class DefaultImpl(metaclass=ImplMeta):
573
584
 
574
585
  """
575
586
 
576
- compile_kw = {
577
- "compile_kwargs": {"literal_binds": True, "include_table": False}
578
- }
587
+ compile_kw = {"literal_binds": True, "include_table": False}
588
+
579
589
  return str(
580
- expr.compile(
581
- dialect=self.dialect, **compile_kw # type: ignore[arg-type]
582
- )
590
+ expr.compile(dialect=self.dialect, compile_kwargs=compile_kw)
583
591
  )
584
592
 
585
593
  def _compat_autogen_column_reflect(self, inspector: Inspector) -> Callable:
@@ -638,10 +646,10 @@ class DefaultImpl(metaclass=ImplMeta):
638
646
  # ignored contains the attributes that were not considered
639
647
  # because assumed to their default values in the db.
640
648
  diff, ignored = _compare_identity_options(
641
- sqla_compat._identity_attrs,
642
649
  metadata_identity,
643
650
  inspector_identity,
644
651
  sqla_compat.Identity(),
652
+ skip={"always"},
645
653
  )
646
654
 
647
655
  meta_always = getattr(metadata_identity, "always", None)
@@ -662,15 +670,96 @@ class DefaultImpl(metaclass=ImplMeta):
662
670
  bool(diff) or bool(metadata_identity) != bool(inspector_identity),
663
671
  )
664
672
 
665
- def create_index_sig(self, index: Index) -> Tuple[Any, ...]:
666
- # order of col matters in an index
667
- 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``.
700
+
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
+ )
668
754
 
669
- def create_unique_constraint_sig(
670
- self, const: UniqueConstraint
671
- ) -> Tuple[Any, ...]:
672
- # order of col does not matters in an unique constraint
673
- return tuple(sorted([col.name for col in const.columns]))
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()
674
763
 
675
764
  def _skip_functional_indexes(self, metadata_indexes, conn_indexes):
676
765
  conn_indexes_by_name = {c.name: c for c in conn_indexes}
@@ -695,21 +784,58 @@ class DefaultImpl(metaclass=ImplMeta):
695
784
  return reflected_object.get("dialect_options", {})
696
785
 
697
786
 
787
+ class Params(NamedTuple):
788
+ token0: str
789
+ tokens: List[str]
790
+ args: List[str]
791
+ kwargs: Dict[str, str]
792
+
793
+
698
794
  def _compare_identity_options(
699
- attributes, metadata_io, inspector_io, default_io
795
+ metadata_io: Union[schema.Identity, schema.Sequence, None],
796
+ inspector_io: Union[schema.Identity, schema.Sequence, None],
797
+ default_io: Union[schema.Identity, schema.Sequence],
798
+ skip: Set[str],
700
799
  ):
701
800
  # this can be used for identity or sequence compare.
702
801
  # default_io is an instance of IdentityOption with all attributes to the
703
802
  # default value.
803
+ meta_d = sqla_compat._get_identity_options_dict(metadata_io)
804
+ insp_d = sqla_compat._get_identity_options_dict(inspector_io)
805
+
704
806
  diff = set()
705
807
  ignored_attr = set()
706
- for attr in attributes:
707
- meta_value = getattr(metadata_io, attr, None)
708
- default_value = getattr(default_io, attr, None)
709
- conn_value = getattr(inspector_io, attr, None)
710
- if conn_value != meta_value:
711
- if meta_value == default_value:
712
- ignored_attr.add(attr)
713
- else:
714
- diff.add(attr)
808
+
809
+ def check_dicts(
810
+ meta_dict: Mapping[str, Any],
811
+ insp_dict: Mapping[str, Any],
812
+ default_dict: Mapping[str, Any],
813
+ attrs: Iterable[str],
814
+ ):
815
+ for attr in set(attrs).difference(skip):
816
+ meta_value = meta_dict.get(attr)
817
+ insp_value = insp_dict.get(attr)
818
+ if insp_value != meta_value:
819
+ default_value = default_dict.get(attr)
820
+ if meta_value == default_value:
821
+ ignored_attr.add(attr)
822
+ else:
823
+ diff.add(attr)
824
+
825
+ check_dicts(
826
+ meta_d,
827
+ insp_d,
828
+ sqla_compat._get_identity_options_dict(default_io),
829
+ set(meta_d).union(insp_d),
830
+ )
831
+ if sqla_compat.identity_has_dialect_kwargs:
832
+ # use only the dialect kwargs in inspector_io since metadata_io
833
+ # can have options for many backends
834
+ check_dicts(
835
+ getattr(metadata_io, "dialect_kwargs", {}),
836
+ getattr(inspector_io, "dialect_kwargs", {}),
837
+ default_io.dialect_kwargs, # type: ignore[union-attr]
838
+ getattr(inspector_io, "dialect_kwargs", {}),
839
+ )
840
+
715
841
  return diff, ignored_attr
alembic/ddl/mssql.py CHANGED
@@ -51,16 +51,13 @@ class MSSQLImpl(DefaultImpl):
51
51
  batch_separator = "GO"
52
52
 
53
53
  type_synonyms = DefaultImpl.type_synonyms + ({"VARCHAR", "NVARCHAR"},)
54
- identity_attrs_ignore = (
54
+ identity_attrs_ignore = DefaultImpl.identity_attrs_ignore + (
55
55
  "minvalue",
56
56
  "maxvalue",
57
57
  "nominvalue",
58
58
  "nomaxvalue",
59
59
  "cycle",
60
60
  "cache",
61
- "order",
62
- "on_null",
63
- "order",
64
61
  )
65
62
 
66
63
  def __init__(self, *arg, **kw) -> 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):