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/postgresql.py CHANGED
@@ -21,10 +21,8 @@ from sqlalchemy.dialects.postgresql import BIGINT
21
21
  from sqlalchemy.dialects.postgresql import ExcludeConstraint
22
22
  from sqlalchemy.dialects.postgresql import INTEGER
23
23
  from sqlalchemy.schema import CreateIndex
24
- from sqlalchemy.sql import operators
25
24
  from sqlalchemy.sql.elements import ColumnClause
26
25
  from sqlalchemy.sql.elements import TextClause
27
- from sqlalchemy.sql.elements import UnaryExpression
28
26
  from sqlalchemy.sql.functions import FunctionElement
29
27
  from sqlalchemy.types import NULLTYPE
30
28
 
@@ -38,6 +36,7 @@ from .base import format_table_name
38
36
  from .base import format_type
39
37
  from .base import IdentityColumnDefault
40
38
  from .base import RenameTable
39
+ from .impl import ComparisonResult
41
40
  from .impl import DefaultImpl
42
41
  from .. import util
43
42
  from ..autogenerate import render
@@ -57,8 +56,8 @@ if TYPE_CHECKING:
57
56
  from sqlalchemy.dialects.postgresql.hstore import HSTORE
58
57
  from sqlalchemy.dialects.postgresql.json import JSON
59
58
  from sqlalchemy.dialects.postgresql.json import JSONB
60
- from sqlalchemy.sql.elements import BinaryExpression
61
59
  from sqlalchemy.sql.elements import ClauseElement
60
+ from sqlalchemy.sql.elements import ColumnElement
62
61
  from sqlalchemy.sql.elements import quoted_name
63
62
  from sqlalchemy.sql.schema import MetaData
64
63
  from sqlalchemy.sql.schema import Table
@@ -79,7 +78,6 @@ class PostgresqlImpl(DefaultImpl):
79
78
  type_synonyms = DefaultImpl.type_synonyms + (
80
79
  {"FLOAT", "DOUBLE PRECISION"},
81
80
  )
82
- identity_attrs_ignore = ("on_null", "order")
83
81
 
84
82
  def create_index(self, index: Index, **kw: Any) -> None:
85
83
  # this likely defaults to None if not present, so get()
@@ -253,62 +251,60 @@ class PostgresqlImpl(DefaultImpl):
253
251
  if not sqla_compat.sqla_2:
254
252
  self._skip_functional_indexes(metadata_indexes, conn_indexes)
255
253
 
256
- def _cleanup_index_expr(
257
- self, index: Index, expr: str, remove_suffix: str
258
- ) -> str:
259
- # start = expr
254
+ # pg behavior regarding modifiers
255
+ # | # | compiled sql | returned sql | regexp. group is removed |
256
+ # | - | ---------------- | -----------------| ------------------------ |
257
+ # | 1 | nulls first | nulls first | - |
258
+ # | 2 | nulls last | | (?<! desc)( nulls last)$ |
259
+ # | 3 | asc | | ( asc)$ |
260
+ # | 4 | asc nulls first | nulls first | ( asc) nulls first$ |
261
+ # | 5 | asc nulls last | | ( asc nulls last)$ |
262
+ # | 6 | desc | desc | - |
263
+ # | 7 | desc nulls first | desc | desc( nulls first)$ |
264
+ # | 8 | desc nulls last | desc nulls last | - |
265
+ _default_modifiers_re = ( # order of case 2 and 5 matters
266
+ re.compile("( asc nulls last)$"), # case 5
267
+ re.compile("(?<! desc)( nulls last)$"), # case 2
268
+ re.compile("( asc)$"), # case 3
269
+ re.compile("( asc) nulls first$"), # case 4
270
+ re.compile(" desc( nulls first)$"), # case 7
271
+ )
272
+
273
+ def _cleanup_index_expr(self, index: Index, expr: str) -> str:
260
274
  expr = expr.lower().replace('"', "").replace("'", "")
261
275
  if index.table is not None:
262
276
  # should not be needed, since include_table=False is in compile
263
277
  expr = expr.replace(f"{index.table.name.lower()}.", "")
264
278
 
265
- while expr and expr[0] == "(" and expr[-1] == ")":
266
- expr = expr[1:-1]
267
279
  if "::" in expr:
268
280
  # strip :: cast. types can have spaces in them
269
281
  expr = re.sub(r"(::[\w ]+\w)", "", expr)
270
282
 
271
- if remove_suffix and expr.endswith(remove_suffix):
272
- expr = expr[: -len(remove_suffix)]
273
-
274
- # print(f"START: {start} END: {expr}")
275
- return expr
283
+ while expr and expr[0] == "(" and expr[-1] == ")":
284
+ expr = expr[1:-1]
276
285
 
277
- def _default_modifiers(self, exp: ClauseElement) -> str:
278
- to_remove = ""
279
- while isinstance(exp, UnaryExpression):
280
- if exp.modifier is None:
281
- exp = exp.element
282
- else:
283
- op = exp.modifier
284
- if isinstance(exp.element, UnaryExpression):
285
- inner_op = exp.element.modifier
286
- else:
287
- inner_op = None
288
- if inner_op is None:
289
- if op == operators.asc_op:
290
- # default is asc
291
- to_remove = " asc"
292
- elif op == operators.nullslast_op:
293
- # default is nulls last
294
- to_remove = " nulls last"
295
- else:
296
- if (
297
- inner_op == operators.asc_op
298
- and op == operators.nullslast_op
299
- ):
300
- # default is asc nulls last
301
- to_remove = " asc nulls last"
302
- elif (
303
- inner_op == operators.desc_op
304
- and op == operators.nullsfirst_op
305
- ):
306
- # default for desc is nulls first
307
- to_remove = " nulls first"
286
+ # NOTE: when parsing the connection expression this cleanup could
287
+ # be skipped
288
+ for rs in self._default_modifiers_re:
289
+ if match := rs.search(expr):
290
+ start, end = match.span(1)
291
+ expr = expr[:start] + expr[end:]
308
292
  break
309
- return to_remove
310
293
 
311
- def _dialect_sig(
294
+ while expr and expr[0] == "(" and expr[-1] == ")":
295
+ expr = expr[1:-1]
296
+
297
+ # strip casts
298
+ cast_re = re.compile(r"cast\s*\(")
299
+ if cast_re.match(expr):
300
+ expr = cast_re.sub("", expr)
301
+ # remove the as type
302
+ expr = re.sub(r"as\s+[^)]+\)", "", expr)
303
+ # remove spaces
304
+ expr = expr.replace(" ", "")
305
+ return expr
306
+
307
+ def _dialect_options(
312
308
  self, item: Union[Index, UniqueConstraint]
313
309
  ) -> Tuple[Any, ...]:
314
310
  # only the positive case is returned by sqlalchemy reflection so
@@ -317,25 +313,93 @@ class PostgresqlImpl(DefaultImpl):
317
313
  return ("nulls_not_distinct",)
318
314
  return ()
319
315
 
320
- def create_index_sig(self, index: Index) -> Tuple[Any, ...]:
321
- return tuple(
322
- self._cleanup_index_expr(
323
- index,
324
- *(
325
- (e, "")
326
- if isinstance(e, str)
327
- else (self._compile_element(e), self._default_modifiers(e))
328
- ),
316
+ def compare_indexes(
317
+ self,
318
+ metadata_index: Index,
319
+ reflected_index: Index,
320
+ ) -> ComparisonResult:
321
+ msg = []
322
+ unique_msg = self._compare_index_unique(
323
+ metadata_index, reflected_index
324
+ )
325
+ if unique_msg:
326
+ msg.append(unique_msg)
327
+ m_exprs = metadata_index.expressions
328
+ r_exprs = reflected_index.expressions
329
+ if len(m_exprs) != len(r_exprs):
330
+ msg.append(f"expression number {len(r_exprs)} to {len(m_exprs)}")
331
+ if msg:
332
+ # no point going further, return early
333
+ return ComparisonResult.Different(msg)
334
+ skip = []
335
+ for pos, (m_e, r_e) in enumerate(zip(m_exprs, r_exprs), 1):
336
+ m_compile = self._compile_element(m_e)
337
+ m_text = self._cleanup_index_expr(metadata_index, m_compile)
338
+ # print(f"META ORIG: {m_compile!r} CLEANUP: {m_text!r}")
339
+ r_compile = self._compile_element(r_e)
340
+ r_text = self._cleanup_index_expr(metadata_index, r_compile)
341
+ # print(f"CONN ORIG: {r_compile!r} CLEANUP: {r_text!r}")
342
+ if m_text == r_text:
343
+ continue # expressions these are equal
344
+ elif m_compile.strip().endswith("_ops") and (
345
+ " " in m_compile or ")" in m_compile # is an expression
346
+ ):
347
+ skip.append(
348
+ f"expression #{pos} {m_compile!r} detected "
349
+ "as including operator clause."
350
+ )
351
+ util.warn(
352
+ f"Expression #{pos} {m_compile!r} in index "
353
+ f"{reflected_index.name!r} detected to include "
354
+ "an operator clause. Expression compare cannot proceed. "
355
+ "Please move the operator clause to the "
356
+ "``postgresql_ops`` dict to enable proper compare "
357
+ "of the index expressions: "
358
+ "https://docs.sqlalchemy.org/en/latest/dialects/postgresql.html#operator-classes", # noqa: E501
359
+ )
360
+ else:
361
+ msg.append(f"expression #{pos} {r_compile!r} to {m_compile!r}")
362
+
363
+ m_options = self._dialect_options(metadata_index)
364
+ r_options = self._dialect_options(reflected_index)
365
+ if m_options != r_options:
366
+ msg.extend(f"options {r_options} to {m_options}")
367
+
368
+ if msg:
369
+ return ComparisonResult.Different(msg)
370
+ elif skip:
371
+ # if there are other changes detected don't skip the index
372
+ return ComparisonResult.Skip(skip)
373
+ else:
374
+ return ComparisonResult.Equal()
375
+
376
+ def compare_unique_constraint(
377
+ self,
378
+ metadata_constraint: UniqueConstraint,
379
+ reflected_constraint: UniqueConstraint,
380
+ ) -> ComparisonResult:
381
+ metadata_tup = self._create_metadata_constraint_sig(
382
+ metadata_constraint
383
+ )
384
+ reflected_tup = self._create_reflected_constraint_sig(
385
+ reflected_constraint
386
+ )
387
+
388
+ meta_sig = metadata_tup.unnamed
389
+ conn_sig = reflected_tup.unnamed
390
+ if conn_sig != meta_sig:
391
+ return ComparisonResult.Different(
392
+ f"expression {conn_sig} to {meta_sig}"
329
393
  )
330
- for e in index.expressions
331
- ) + self._dialect_sig(index)
332
394
 
333
- def create_unique_constraint_sig(
334
- self, const: UniqueConstraint
335
- ) -> Tuple[Any, ...]:
336
- return tuple(
337
- sorted([col.name for col in const.columns])
338
- ) + self._dialect_sig(const)
395
+ metadata_do = self._dialect_options(metadata_tup.const)
396
+ conn_do = self._dialect_options(reflected_tup.const)
397
+ if metadata_do != conn_do:
398
+ return ComparisonResult.Different(
399
+ f"expression {conn_do} to {metadata_do}"
400
+ )
401
+
402
+ return ComparisonResult.Equal()
339
403
 
340
404
  def adjust_reflected_dialect_options(
341
405
  self, reflected_options: Dict[str, Any], kind: str
@@ -346,12 +410,37 @@ class PostgresqlImpl(DefaultImpl):
346
410
  options.pop("postgresql_include", None)
347
411
  return options
348
412
 
349
- def _compile_element(self, element: ClauseElement) -> str:
413
+ def _compile_element(self, element: Union[ClauseElement, str]) -> str:
414
+ if isinstance(element, str):
415
+ return element
350
416
  return element.compile(
351
417
  dialect=self.dialect,
352
418
  compile_kwargs={"literal_binds": True, "include_table": False},
353
419
  ).string
354
420
 
421
+ def render_ddl_sql_expr(
422
+ self,
423
+ expr: ClauseElement,
424
+ is_server_default: bool = False,
425
+ is_index: bool = False,
426
+ **kw: Any,
427
+ ) -> str:
428
+ """Render a SQL expression that is typically a server default,
429
+ index expression, etc.
430
+
431
+ """
432
+
433
+ # apply self_group to index expressions;
434
+ # see https://github.com/sqlalchemy/sqlalchemy/blob/
435
+ # 82fa95cfce070fab401d020c6e6e4a6a96cc2578/
436
+ # lib/sqlalchemy/dialects/postgresql/base.py#L2261
437
+ if is_index and not isinstance(expr, ColumnClause):
438
+ expr = expr.self_group()
439
+
440
+ return super().render_ddl_sql_expr(
441
+ expr, is_server_default=is_server_default, is_index=is_index, **kw
442
+ )
443
+
355
444
  def render_type(
356
445
  self, type_: TypeEngine, autogen_context: AutogenContext
357
446
  ) -> Union[str, Literal[False]]:
@@ -513,7 +602,7 @@ class CreateExcludeConstraintOp(ops.AddConstraintOp):
513
602
  Sequence[Tuple[str, str]],
514
603
  Sequence[Tuple[ColumnClause[Any], str]],
515
604
  ],
516
- where: Optional[Union[BinaryExpression, str]] = None,
605
+ where: Optional[Union[ColumnElement[bool], str]] = None,
517
606
  schema: Optional[str] = None,
518
607
  _orig_constraint: Optional[ExcludeConstraint] = None,
519
608
  **kw,
@@ -538,9 +627,7 @@ class CreateExcludeConstraintOp(ops.AddConstraintOp):
538
627
  (expr, op)
539
628
  for expr, name, op in constraint._render_exprs # type:ignore[attr-defined] # noqa
540
629
  ],
541
- where=cast(
542
- "Optional[Union[BinaryExpression, str]]", constraint.where
543
- ),
630
+ where=cast("ColumnElement[bool] | None", constraint.where),
544
631
  schema=constraint_table.schema,
545
632
  _orig_constraint=constraint,
546
633
  deferrable=constraint.deferrable,
alembic/op.pyi CHANGED
@@ -19,14 +19,13 @@ from typing import TYPE_CHECKING
19
19
  from typing import TypeVar
20
20
  from typing import Union
21
21
 
22
- from sqlalchemy.sql.expression import TableClause
23
- from sqlalchemy.sql.expression import Update
24
-
25
22
  if TYPE_CHECKING:
26
23
  from sqlalchemy.engine import Connection
27
- from sqlalchemy.sql.elements import BinaryExpression
24
+ from sqlalchemy.sql import Executable
25
+ from sqlalchemy.sql.elements import ColumnElement
28
26
  from sqlalchemy.sql.elements import conv
29
27
  from sqlalchemy.sql.elements import TextClause
28
+ from sqlalchemy.sql.expression import TableClause
30
29
  from sqlalchemy.sql.functions import Function
31
30
  from sqlalchemy.sql.schema import Column
32
31
  from sqlalchemy.sql.schema import Computed
@@ -197,7 +196,7 @@ def alter_column(
197
196
  don't otherwise specify a new type, as well as for
198
197
  when nullability is being changed on a SQL Server
199
198
  column. It is also used if the type is a so-called
200
- SQLlchemy "schema" type which may define a constraint (i.e.
199
+ SQLAlchemy "schema" type which may define a constraint (i.e.
201
200
  :class:`~sqlalchemy.types.Boolean`,
202
201
  :class:`~sqlalchemy.types.Enum`),
203
202
  so that the constraint can be dropped.
@@ -481,7 +480,7 @@ def bulk_insert(
481
480
  def create_check_constraint(
482
481
  constraint_name: Optional[str],
483
482
  table_name: str,
484
- condition: Union[str, BinaryExpression, TextClause],
483
+ condition: Union[str, ColumnElement[bool], TextClause],
485
484
  *,
486
485
  schema: Optional[str] = None,
487
486
  **kw: Any,
@@ -1024,7 +1023,7 @@ def drop_table_comment(
1024
1023
  """
1025
1024
 
1026
1025
  def execute(
1027
- sqltext: Union[str, TextClause, Update],
1026
+ sqltext: Union[Executable, str],
1028
1027
  *,
1029
1028
  execution_options: Optional[dict[str, Any]] = None,
1030
1029
  ) -> None:
@@ -1080,7 +1079,7 @@ def execute(
1080
1079
  )
1081
1080
 
1082
1081
  Additionally, when passing the statement as a plain string, it is first
1083
- coerceed into a :func:`sqlalchemy.sql.expression.text` construct
1082
+ coerced into a :func:`sqlalchemy.sql.expression.text` construct
1084
1083
  before being passed along. In the less likely case that the
1085
1084
  literal SQL string contains a colon, it must be escaped with a
1086
1085
  backslash, as::
@@ -1093,9 +1092,8 @@ def execute(
1093
1092
  * a string
1094
1093
  * a :func:`sqlalchemy.sql.expression.text` construct.
1095
1094
  * a :func:`sqlalchemy.sql.expression.insert` construct.
1096
- * a :func:`sqlalchemy.sql.expression.update`,
1097
- :func:`sqlalchemy.sql.expression.insert`,
1098
- or :func:`sqlalchemy.sql.expression.delete` construct.
1095
+ * a :func:`sqlalchemy.sql.expression.update` construct.
1096
+ * a :func:`sqlalchemy.sql.expression.delete` construct.
1099
1097
  * Any "executable" described in SQLAlchemy Core documentation,
1100
1098
  noting that no result set is returned.
1101
1099
 
@@ -3,6 +3,7 @@ from .base import AbstractOperations
3
3
  from .base import BatchOperations
4
4
  from .base import Operations
5
5
  from .ops import MigrateOperation
6
+ from .ops import MigrationScript
6
7
 
7
8
 
8
9
  __all__ = [
@@ -10,4 +11,5 @@ __all__ = [
10
11
  "Operations",
11
12
  "BatchOperations",
12
13
  "MigrateOperation",
14
+ "MigrationScript",
13
15
  ]
@@ -35,10 +35,10 @@ if TYPE_CHECKING:
35
35
 
36
36
  from sqlalchemy import Table
37
37
  from sqlalchemy.engine import Connection
38
- from sqlalchemy.sql.expression import BinaryExpression
38
+ from sqlalchemy.sql import Executable
39
+ from sqlalchemy.sql.expression import ColumnElement
39
40
  from sqlalchemy.sql.expression import TableClause
40
41
  from sqlalchemy.sql.expression import TextClause
41
- from sqlalchemy.sql.expression import Update
42
42
  from sqlalchemy.sql.functions import Function
43
43
  from sqlalchemy.sql.schema import Column
44
44
  from sqlalchemy.sql.schema import Computed
@@ -723,7 +723,7 @@ class Operations(AbstractOperations):
723
723
  don't otherwise specify a new type, as well as for
724
724
  when nullability is being changed on a SQL Server
725
725
  column. It is also used if the type is a so-called
726
- SQLlchemy "schema" type which may define a constraint (i.e.
726
+ SQLAlchemy "schema" type which may define a constraint (i.e.
727
727
  :class:`~sqlalchemy.types.Boolean`,
728
728
  :class:`~sqlalchemy.types.Enum`),
729
729
  so that the constraint can be dropped.
@@ -861,7 +861,7 @@ class Operations(AbstractOperations):
861
861
  self,
862
862
  constraint_name: Optional[str],
863
863
  table_name: str,
864
- condition: Union[str, BinaryExpression, TextClause],
864
+ condition: Union[str, ColumnElement[bool], TextClause],
865
865
  *,
866
866
  schema: Optional[str] = None,
867
867
  **kw: Any,
@@ -1433,7 +1433,7 @@ class Operations(AbstractOperations):
1433
1433
 
1434
1434
  def execute(
1435
1435
  self,
1436
- sqltext: Union[str, TextClause, Update],
1436
+ sqltext: Union[Executable, str],
1437
1437
  *,
1438
1438
  execution_options: Optional[dict[str, Any]] = None,
1439
1439
  ) -> None:
@@ -1489,7 +1489,7 @@ class Operations(AbstractOperations):
1489
1489
  )
1490
1490
 
1491
1491
  Additionally, when passing the statement as a plain string, it is first
1492
- coerceed into a :func:`sqlalchemy.sql.expression.text` construct
1492
+ coerced into a :func:`sqlalchemy.sql.expression.text` construct
1493
1493
  before being passed along. In the less likely case that the
1494
1494
  literal SQL string contains a colon, it must be escaped with a
1495
1495
  backslash, as::
@@ -1502,9 +1502,8 @@ class Operations(AbstractOperations):
1502
1502
  * a string
1503
1503
  * a :func:`sqlalchemy.sql.expression.text` construct.
1504
1504
  * a :func:`sqlalchemy.sql.expression.insert` construct.
1505
- * a :func:`sqlalchemy.sql.expression.update`,
1506
- :func:`sqlalchemy.sql.expression.insert`,
1507
- or :func:`sqlalchemy.sql.expression.delete` construct.
1505
+ * a :func:`sqlalchemy.sql.expression.update` construct.
1506
+ * a :func:`sqlalchemy.sql.expression.delete` construct.
1508
1507
  * Any "executable" described in SQLAlchemy Core documentation,
1509
1508
  noting that no result set is returned.
1510
1509
 
@@ -1635,7 +1634,7 @@ class BatchOperations(AbstractOperations):
1635
1634
  def create_check_constraint(
1636
1635
  self,
1637
1636
  constraint_name: str,
1638
- condition: Union[str, BinaryExpression, TextClause],
1637
+ condition: Union[str, ColumnElement[bool], TextClause],
1639
1638
  **kw: Any,
1640
1639
  ) -> None:
1641
1640
  """Issue a "create check constraint" instruction using the
@@ -1822,7 +1821,7 @@ class BatchOperations(AbstractOperations):
1822
1821
 
1823
1822
  def execute(
1824
1823
  self,
1825
- sqltext: Union[str, TextClause, Update],
1824
+ sqltext: Union[Executable, str],
1826
1825
  *,
1827
1826
  execution_options: Optional[dict[str, Any]] = None,
1828
1827
  ) -> None:
alembic/operations/ops.py CHANGED
@@ -28,9 +28,7 @@ from ..util import sqla_compat
28
28
  if TYPE_CHECKING:
29
29
  from typing import Literal
30
30
 
31
- from sqlalchemy.sql.dml import Insert
32
- from sqlalchemy.sql.dml import Update
33
- from sqlalchemy.sql.elements import BinaryExpression
31
+ from sqlalchemy.sql import Executable
34
32
  from sqlalchemy.sql.elements import ColumnElement
35
33
  from sqlalchemy.sql.elements import conv
36
34
  from sqlalchemy.sql.elements import quoted_name
@@ -788,7 +786,7 @@ class CreateCheckConstraintOp(AddConstraintOp):
788
786
  operations: Operations,
789
787
  constraint_name: Optional[str],
790
788
  table_name: str,
791
- condition: Union[str, BinaryExpression, TextClause],
789
+ condition: Union[str, ColumnElement[bool], TextClause],
792
790
  *,
793
791
  schema: Optional[str] = None,
794
792
  **kw: Any,
@@ -841,7 +839,7 @@ class CreateCheckConstraintOp(AddConstraintOp):
841
839
  cls,
842
840
  operations: BatchOperations,
843
841
  constraint_name: str,
844
- condition: Union[str, BinaryExpression, TextClause],
842
+ condition: Union[str, ColumnElement[bool], TextClause],
845
843
  **kw: Any,
846
844
  ) -> None:
847
845
  """Issue a "create check constraint" instruction using the
@@ -901,7 +899,7 @@ class CreateIndexOp(MigrateOperation):
901
899
  return cls(
902
900
  index.name, # type: ignore[arg-type]
903
901
  index.table.name,
904
- sqla_compat._get_index_expressions(index),
902
+ index.expressions,
905
903
  schema=index.table.schema,
906
904
  unique=index.unique,
907
905
  **index.kwargs,
@@ -1883,7 +1881,7 @@ class AlterColumnOp(AlterTableOp):
1883
1881
  don't otherwise specify a new type, as well as for
1884
1882
  when nullability is being changed on a SQL Server
1885
1883
  column. It is also used if the type is a so-called
1886
- SQLlchemy "schema" type which may define a constraint (i.e.
1884
+ SQLAlchemy "schema" type which may define a constraint (i.e.
1887
1885
  :class:`~sqlalchemy.types.Boolean`,
1888
1886
  :class:`~sqlalchemy.types.Enum`),
1889
1887
  so that the constraint can be dropped.
@@ -2424,7 +2422,7 @@ class ExecuteSQLOp(MigrateOperation):
2424
2422
 
2425
2423
  def __init__(
2426
2424
  self,
2427
- sqltext: Union[Update, str, Insert, TextClause],
2425
+ sqltext: Union[Executable, str],
2428
2426
  *,
2429
2427
  execution_options: Optional[dict[str, Any]] = None,
2430
2428
  ) -> None:
@@ -2435,7 +2433,7 @@ class ExecuteSQLOp(MigrateOperation):
2435
2433
  def execute(
2436
2434
  cls,
2437
2435
  operations: Operations,
2438
- sqltext: Union[str, TextClause, Update],
2436
+ sqltext: Union[Executable, str],
2439
2437
  *,
2440
2438
  execution_options: Optional[dict[str, Any]] = None,
2441
2439
  ) -> None:
@@ -2491,7 +2489,7 @@ class ExecuteSQLOp(MigrateOperation):
2491
2489
  )
2492
2490
 
2493
2491
  Additionally, when passing the statement as a plain string, it is first
2494
- coerceed into a :func:`sqlalchemy.sql.expression.text` construct
2492
+ coerced into a :func:`sqlalchemy.sql.expression.text` construct
2495
2493
  before being passed along. In the less likely case that the
2496
2494
  literal SQL string contains a colon, it must be escaped with a
2497
2495
  backslash, as::
@@ -2504,9 +2502,8 @@ class ExecuteSQLOp(MigrateOperation):
2504
2502
  * a string
2505
2503
  * a :func:`sqlalchemy.sql.expression.text` construct.
2506
2504
  * a :func:`sqlalchemy.sql.expression.insert` construct.
2507
- * a :func:`sqlalchemy.sql.expression.update`,
2508
- :func:`sqlalchemy.sql.expression.insert`,
2509
- or :func:`sqlalchemy.sql.expression.delete` construct.
2505
+ * a :func:`sqlalchemy.sql.expression.update` construct.
2506
+ * a :func:`sqlalchemy.sql.expression.delete` construct.
2510
2507
  * Any "executable" described in SQLAlchemy Core documentation,
2511
2508
  noting that no result set is returned.
2512
2509
 
@@ -2527,7 +2524,7 @@ class ExecuteSQLOp(MigrateOperation):
2527
2524
  def batch_execute(
2528
2525
  cls,
2529
2526
  operations: Operations,
2530
- sqltext: Union[str, TextClause, Update],
2527
+ sqltext: Union[Executable, str],
2531
2528
  *,
2532
2529
  execution_options: Optional[dict[str, Any]] = None,
2533
2530
  ) -> None:
@@ -2542,6 +2539,9 @@ class ExecuteSQLOp(MigrateOperation):
2542
2539
  operations, sqltext, execution_options=execution_options
2543
2540
  )
2544
2541
 
2542
+ def to_diff_tuple(self) -> Tuple[str, Union[Executable, str]]:
2543
+ return ("execute", self.sqltext)
2544
+
2545
2545
 
2546
2546
  class OpContainer(MigrateOperation):
2547
2547
  """Represent a sequence of operations operation."""
@@ -5,7 +5,7 @@ from sqlalchemy import schema as sa_schema
5
5
  from . import ops
6
6
  from .base import Operations
7
7
  from ..util.sqla_compat import _copy
8
- from ..util.sqla_compat import sqla_2
8
+ from ..util.sqla_compat import sqla_14
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from sqlalchemy.sql.schema import Table
@@ -98,8 +98,8 @@ def create_index(
98
98
  idx = operation.to_index(operations.migration_context)
99
99
  kw = {}
100
100
  if operation.if_not_exists is not None:
101
- if not sqla_2:
102
- raise NotImplementedError("SQLAlchemy 2.0+ required")
101
+ if not sqla_14:
102
+ raise NotImplementedError("SQLAlchemy 1.4+ required")
103
103
 
104
104
  kw["if_not_exists"] = operation.if_not_exists
105
105
  operations.impl.create_index(idx, **kw)
@@ -109,8 +109,8 @@ def create_index(
109
109
  def drop_index(operations: "Operations", operation: "ops.DropIndexOp") -> None:
110
110
  kw = {}
111
111
  if operation.if_exists is not None:
112
- if not sqla_2:
113
- raise NotImplementedError("SQLAlchemy 2.0+ required")
112
+ if not sqla_14:
113
+ raise NotImplementedError("SQLAlchemy 1.4+ required")
114
114
 
115
115
  kw["if_exists"] = operation.if_exists
116
116
 
@@ -10,6 +10,7 @@ from typing import Mapping
10
10
  from typing import MutableMapping
11
11
  from typing import Optional
12
12
  from typing import overload
13
+ from typing import Sequence
13
14
  from typing import TextIO
14
15
  from typing import Tuple
15
16
  from typing import TYPE_CHECKING
@@ -23,11 +24,12 @@ from .migration import _ProxyTransaction
23
24
  from .migration import MigrationContext
24
25
  from .. import util
25
26
  from ..operations import Operations
27
+ from ..script.revision import _GetRevArg
26
28
 
27
29
  if TYPE_CHECKING:
28
30
  from sqlalchemy.engine import URL
29
31
  from sqlalchemy.engine.base import Connection
30
- from sqlalchemy.sql.elements import ClauseElement
32
+ from sqlalchemy.sql import Executable
31
33
  from sqlalchemy.sql.schema import MetaData
32
34
  from sqlalchemy.sql.schema import SchemaItem
33
35
  from sqlalchemy.sql.type_api import TypeEngine
@@ -36,13 +38,13 @@ if TYPE_CHECKING:
36
38
  from ..autogenerate.api import AutogenContext
37
39
  from ..config import Config
38
40
  from ..ddl import DefaultImpl
39
- from ..operations.ops import MigrateOperation
41
+ from ..operations.ops import MigrationScript
40
42
  from ..script.base import ScriptDirectory
41
43
 
42
44
  _RevNumber = Optional[Union[str, Tuple[str, ...]]]
43
45
 
44
46
  ProcessRevisionDirectiveFn = Callable[
45
- [MigrationContext, Tuple[str, str], List["MigrateOperation"]], None
47
+ [MigrationContext, _GetRevArg, List["MigrationScript"]], None
46
48
  ]
47
49
 
48
50
  RenderItemFn = Callable[
@@ -415,7 +417,7 @@ class EnvironmentContext(util.ModuleClsProxy):
415
417
  tag: Optional[str] = None,
416
418
  template_args: Optional[Dict[str, Any]] = None,
417
419
  render_as_batch: bool = False,
418
- target_metadata: Optional[MetaData] = None,
420
+ target_metadata: Union[MetaData, Sequence[MetaData], None] = None,
419
421
  include_name: Optional[IncludeNameFn] = None,
420
422
  include_object: Optional[IncludeObjectFn] = None,
421
423
  include_schemas: bool = False,
@@ -938,7 +940,7 @@ class EnvironmentContext(util.ModuleClsProxy):
938
940
 
939
941
  def execute(
940
942
  self,
941
- sql: Union[ClauseElement, str],
943
+ sql: Union[Executable, str],
942
944
  execution_options: Optional[dict] = None,
943
945
  ) -> None:
944
946
  """Execute the given SQL using the current change context.