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.
- alembic/__init__.py +1 -3
- alembic/autogenerate/api.py +14 -6
- alembic/autogenerate/compare.py +129 -195
- alembic/autogenerate/render.py +42 -32
- alembic/autogenerate/rewriter.py +19 -19
- alembic/command.py +11 -9
- alembic/config.py +1 -1
- alembic/context.pyi +12 -5
- alembic/ddl/_autogen.py +323 -0
- alembic/ddl/impl.py +167 -41
- alembic/ddl/mssql.py +1 -4
- alembic/ddl/mysql.py +4 -3
- alembic/ddl/postgresql.py +157 -70
- alembic/op.pyi +9 -11
- alembic/operations/__init__.py +2 -0
- alembic/operations/base.py +10 -11
- alembic/operations/ops.py +14 -14
- alembic/operations/toimpl.py +5 -5
- alembic/runtime/environment.py +7 -5
- alembic/runtime/migration.py +4 -4
- alembic/script/base.py +25 -17
- alembic/script/revision.py +30 -25
- alembic/templates/async/alembic.ini.mako +3 -3
- alembic/templates/generic/alembic.ini.mako +3 -3
- alembic/templates/multidb/alembic.ini.mako +3 -3
- alembic/testing/requirements.py +12 -4
- alembic/testing/schemacompare.py +9 -0
- alembic/testing/suite/test_autogen_identity.py +23 -38
- alembic/util/compat.py +0 -1
- alembic/util/sqla_compat.py +58 -30
- {alembic-1.12.0.dist-info → alembic-1.13.0.dist-info}/METADATA +8 -6
- {alembic-1.12.0.dist-info → alembic-1.13.0.dist-info}/RECORD +36 -35
- {alembic-1.12.0.dist-info → alembic-1.13.0.dist-info}/WHEEL +1 -1
- {alembic-1.12.0.dist-info → alembic-1.13.0.dist-info}/LICENSE +0 -0
- {alembic-1.12.0.dist-info → alembic-1.13.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
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
|
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
|
-
#
|
90
|
-
|
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[
|
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
|
-
|
177
|
-
|
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(
|
194
|
-
construct, multiparams
|
195
|
-
)
|
204
|
+
return conn.execute(construct, multiparams)
|
196
205
|
|
197
206
|
def execute(
|
198
207
|
self,
|
199
|
-
sql: Union[
|
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
|
-
|
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
|
666
|
-
|
667
|
-
|
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
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
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
|
-
|
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
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
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
|
-
|
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
|
-
|
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):
|