compare.py 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317
  1. # mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
  2. # mypy: no-warn-return-any, allow-any-generics
  3. from __future__ import annotations
  4. import contextlib
  5. import logging
  6. import re
  7. from typing import Any
  8. from typing import cast
  9. from typing import Dict
  10. from typing import Iterator
  11. from typing import Mapping
  12. from typing import Optional
  13. from typing import Set
  14. from typing import Tuple
  15. from typing import TYPE_CHECKING
  16. from typing import TypeVar
  17. from typing import Union
  18. from sqlalchemy import event
  19. from sqlalchemy import inspect
  20. from sqlalchemy import schema as sa_schema
  21. from sqlalchemy import text
  22. from sqlalchemy import types as sqltypes
  23. from sqlalchemy.sql import expression
  24. from sqlalchemy.sql.schema import ForeignKeyConstraint
  25. from sqlalchemy.sql.schema import Index
  26. from sqlalchemy.sql.schema import UniqueConstraint
  27. from sqlalchemy.util import OrderedSet
  28. from .. import util
  29. from ..ddl._autogen import is_index_sig
  30. from ..ddl._autogen import is_uq_sig
  31. from ..operations import ops
  32. from ..util import sqla_compat
  33. if TYPE_CHECKING:
  34. from typing import Literal
  35. from sqlalchemy.engine.reflection import Inspector
  36. from sqlalchemy.sql.elements import quoted_name
  37. from sqlalchemy.sql.elements import TextClause
  38. from sqlalchemy.sql.schema import Column
  39. from sqlalchemy.sql.schema import Table
  40. from alembic.autogenerate.api import AutogenContext
  41. from alembic.ddl.impl import DefaultImpl
  42. from alembic.operations.ops import AlterColumnOp
  43. from alembic.operations.ops import MigrationScript
  44. from alembic.operations.ops import ModifyTableOps
  45. from alembic.operations.ops import UpgradeOps
  46. from ..ddl._autogen import _constraint_sig
  47. log = logging.getLogger(__name__)
  48. def _populate_migration_script(
  49. autogen_context: AutogenContext, migration_script: MigrationScript
  50. ) -> None:
  51. upgrade_ops = migration_script.upgrade_ops_list[-1]
  52. downgrade_ops = migration_script.downgrade_ops_list[-1]
  53. _produce_net_changes(autogen_context, upgrade_ops)
  54. upgrade_ops.reverse_into(downgrade_ops)
  55. comparators = util.Dispatcher(uselist=True)
  56. def _produce_net_changes(
  57. autogen_context: AutogenContext, upgrade_ops: UpgradeOps
  58. ) -> None:
  59. connection = autogen_context.connection
  60. assert connection is not None
  61. include_schemas = autogen_context.opts.get("include_schemas", False)
  62. inspector: Inspector = inspect(connection)
  63. default_schema = connection.dialect.default_schema_name
  64. schemas: Set[Optional[str]]
  65. if include_schemas:
  66. schemas = set(inspector.get_schema_names())
  67. # replace default schema name with None
  68. schemas.discard("information_schema")
  69. # replace the "default" schema with None
  70. schemas.discard(default_schema)
  71. schemas.add(None)
  72. else:
  73. schemas = {None}
  74. schemas = {
  75. s for s in schemas if autogen_context.run_name_filters(s, "schema", {})
  76. }
  77. assert autogen_context.dialect is not None
  78. comparators.dispatch("schema", autogen_context.dialect.name)(
  79. autogen_context, upgrade_ops, schemas
  80. )
  81. @comparators.dispatch_for("schema")
  82. def _autogen_for_tables(
  83. autogen_context: AutogenContext,
  84. upgrade_ops: UpgradeOps,
  85. schemas: Union[Set[None], Set[Optional[str]]],
  86. ) -> None:
  87. inspector = autogen_context.inspector
  88. conn_table_names: Set[Tuple[Optional[str], str]] = set()
  89. version_table_schema = (
  90. autogen_context.migration_context.version_table_schema
  91. )
  92. version_table = autogen_context.migration_context.version_table
  93. for schema_name in schemas:
  94. tables = set(inspector.get_table_names(schema=schema_name))
  95. if schema_name == version_table_schema:
  96. tables = tables.difference(
  97. [autogen_context.migration_context.version_table]
  98. )
  99. conn_table_names.update(
  100. (schema_name, tname)
  101. for tname in tables
  102. if autogen_context.run_name_filters(
  103. tname, "table", {"schema_name": schema_name}
  104. )
  105. )
  106. metadata_table_names = OrderedSet(
  107. [(table.schema, table.name) for table in autogen_context.sorted_tables]
  108. ).difference([(version_table_schema, version_table)])
  109. _compare_tables(
  110. conn_table_names,
  111. metadata_table_names,
  112. inspector,
  113. upgrade_ops,
  114. autogen_context,
  115. )
  116. def _compare_tables(
  117. conn_table_names: set,
  118. metadata_table_names: set,
  119. inspector: Inspector,
  120. upgrade_ops: UpgradeOps,
  121. autogen_context: AutogenContext,
  122. ) -> None:
  123. default_schema = inspector.bind.dialect.default_schema_name
  124. # tables coming from the connection will not have "schema"
  125. # set if it matches default_schema_name; so we need a list
  126. # of table names from local metadata that also have "None" if schema
  127. # == default_schema_name. Most setups will be like this anyway but
  128. # some are not (see #170)
  129. metadata_table_names_no_dflt_schema = OrderedSet(
  130. [
  131. (schema if schema != default_schema else None, tname)
  132. for schema, tname in metadata_table_names
  133. ]
  134. )
  135. # to adjust for the MetaData collection storing the tables either
  136. # as "schemaname.tablename" or just "tablename", create a new lookup
  137. # which will match the "non-default-schema" keys to the Table object.
  138. tname_to_table = {
  139. no_dflt_schema: autogen_context.table_key_to_table[
  140. sa_schema._get_table_key(tname, schema)
  141. ]
  142. for no_dflt_schema, (schema, tname) in zip(
  143. metadata_table_names_no_dflt_schema, metadata_table_names
  144. )
  145. }
  146. metadata_table_names = metadata_table_names_no_dflt_schema
  147. for s, tname in metadata_table_names.difference(conn_table_names):
  148. name = "%s.%s" % (s, tname) if s else tname
  149. metadata_table = tname_to_table[(s, tname)]
  150. if autogen_context.run_object_filters(
  151. metadata_table, tname, "table", False, None
  152. ):
  153. upgrade_ops.ops.append(
  154. ops.CreateTableOp.from_table(metadata_table)
  155. )
  156. log.info("Detected added table %r", name)
  157. modify_table_ops = ops.ModifyTableOps(tname, [], schema=s)
  158. comparators.dispatch("table")(
  159. autogen_context,
  160. modify_table_ops,
  161. s,
  162. tname,
  163. None,
  164. metadata_table,
  165. )
  166. if not modify_table_ops.is_empty():
  167. upgrade_ops.ops.append(modify_table_ops)
  168. removal_metadata = sa_schema.MetaData()
  169. for s, tname in conn_table_names.difference(metadata_table_names):
  170. name = sa_schema._get_table_key(tname, s)
  171. exists = name in removal_metadata.tables
  172. t = sa_schema.Table(tname, removal_metadata, schema=s)
  173. if not exists:
  174. event.listen(
  175. t,
  176. "column_reflect",
  177. # fmt: off
  178. autogen_context.migration_context.impl.
  179. _compat_autogen_column_reflect
  180. (inspector),
  181. # fmt: on
  182. )
  183. inspector.reflect_table(t, include_columns=None)
  184. if autogen_context.run_object_filters(t, tname, "table", True, None):
  185. modify_table_ops = ops.ModifyTableOps(tname, [], schema=s)
  186. comparators.dispatch("table")(
  187. autogen_context, modify_table_ops, s, tname, t, None
  188. )
  189. if not modify_table_ops.is_empty():
  190. upgrade_ops.ops.append(modify_table_ops)
  191. upgrade_ops.ops.append(ops.DropTableOp.from_table(t))
  192. log.info("Detected removed table %r", name)
  193. existing_tables = conn_table_names.intersection(metadata_table_names)
  194. existing_metadata = sa_schema.MetaData()
  195. conn_column_info = {}
  196. for s, tname in existing_tables:
  197. name = sa_schema._get_table_key(tname, s)
  198. exists = name in existing_metadata.tables
  199. t = sa_schema.Table(tname, existing_metadata, schema=s)
  200. if not exists:
  201. event.listen(
  202. t,
  203. "column_reflect",
  204. # fmt: off
  205. autogen_context.migration_context.impl.
  206. _compat_autogen_column_reflect(inspector),
  207. # fmt: on
  208. )
  209. inspector.reflect_table(t, include_columns=None)
  210. conn_column_info[(s, tname)] = t
  211. for s, tname in sorted(existing_tables, key=lambda x: (x[0] or "", x[1])):
  212. s = s or None
  213. name = "%s.%s" % (s, tname) if s else tname
  214. metadata_table = tname_to_table[(s, tname)]
  215. conn_table = existing_metadata.tables[name]
  216. if autogen_context.run_object_filters(
  217. metadata_table, tname, "table", False, conn_table
  218. ):
  219. modify_table_ops = ops.ModifyTableOps(tname, [], schema=s)
  220. with _compare_columns(
  221. s,
  222. tname,
  223. conn_table,
  224. metadata_table,
  225. modify_table_ops,
  226. autogen_context,
  227. inspector,
  228. ):
  229. comparators.dispatch("table")(
  230. autogen_context,
  231. modify_table_ops,
  232. s,
  233. tname,
  234. conn_table,
  235. metadata_table,
  236. )
  237. if not modify_table_ops.is_empty():
  238. upgrade_ops.ops.append(modify_table_ops)
  239. _IndexColumnSortingOps: Mapping[str, Any] = util.immutabledict(
  240. {
  241. "asc": expression.asc,
  242. "desc": expression.desc,
  243. "nulls_first": expression.nullsfirst,
  244. "nulls_last": expression.nullslast,
  245. "nullsfirst": expression.nullsfirst, # 1_3 name
  246. "nullslast": expression.nullslast, # 1_3 name
  247. }
  248. )
  249. def _make_index(
  250. impl: DefaultImpl, params: Dict[str, Any], conn_table: Table
  251. ) -> Optional[Index]:
  252. exprs: list[Union[Column[Any], TextClause]] = []
  253. sorting = params.get("column_sorting")
  254. for num, col_name in enumerate(params["column_names"]):
  255. item: Union[Column[Any], TextClause]
  256. if col_name is None:
  257. assert "expressions" in params
  258. name = params["expressions"][num]
  259. item = text(name)
  260. else:
  261. name = col_name
  262. item = conn_table.c[col_name]
  263. if sorting and name in sorting:
  264. for operator in sorting[name]:
  265. if operator in _IndexColumnSortingOps:
  266. item = _IndexColumnSortingOps[operator](item)
  267. exprs.append(item)
  268. ix = sa_schema.Index(
  269. params["name"],
  270. *exprs,
  271. unique=params["unique"],
  272. _table=conn_table,
  273. **impl.adjust_reflected_dialect_options(params, "index"),
  274. )
  275. if "duplicates_constraint" in params:
  276. ix.info["duplicates_constraint"] = params["duplicates_constraint"]
  277. return ix
  278. def _make_unique_constraint(
  279. impl: DefaultImpl, params: Dict[str, Any], conn_table: Table
  280. ) -> UniqueConstraint:
  281. uq = sa_schema.UniqueConstraint(
  282. *[conn_table.c[cname] for cname in params["column_names"]],
  283. name=params["name"],
  284. **impl.adjust_reflected_dialect_options(params, "unique_constraint"),
  285. )
  286. if "duplicates_index" in params:
  287. uq.info["duplicates_index"] = params["duplicates_index"]
  288. return uq
  289. def _make_foreign_key(
  290. params: Dict[str, Any], conn_table: Table
  291. ) -> ForeignKeyConstraint:
  292. tname = params["referred_table"]
  293. if params["referred_schema"]:
  294. tname = "%s.%s" % (params["referred_schema"], tname)
  295. options = params.get("options", {})
  296. const = sa_schema.ForeignKeyConstraint(
  297. [conn_table.c[cname] for cname in params["constrained_columns"]],
  298. ["%s.%s" % (tname, n) for n in params["referred_columns"]],
  299. onupdate=options.get("onupdate"),
  300. ondelete=options.get("ondelete"),
  301. deferrable=options.get("deferrable"),
  302. initially=options.get("initially"),
  303. name=params["name"],
  304. )
  305. # needed by 0.7
  306. conn_table.append_constraint(const)
  307. return const
  308. @contextlib.contextmanager
  309. def _compare_columns(
  310. schema: Optional[str],
  311. tname: Union[quoted_name, str],
  312. conn_table: Table,
  313. metadata_table: Table,
  314. modify_table_ops: ModifyTableOps,
  315. autogen_context: AutogenContext,
  316. inspector: Inspector,
  317. ) -> Iterator[None]:
  318. name = "%s.%s" % (schema, tname) if schema else tname
  319. metadata_col_names = OrderedSet(
  320. c.name for c in metadata_table.c if not c.system
  321. )
  322. metadata_cols_by_name = {
  323. c.name: c for c in metadata_table.c if not c.system
  324. }
  325. conn_col_names = {
  326. c.name: c
  327. for c in conn_table.c
  328. if autogen_context.run_name_filters(
  329. c.name, "column", {"table_name": tname, "schema_name": schema}
  330. )
  331. }
  332. for cname in metadata_col_names.difference(conn_col_names):
  333. if autogen_context.run_object_filters(
  334. metadata_cols_by_name[cname], cname, "column", False, None
  335. ):
  336. modify_table_ops.ops.append(
  337. ops.AddColumnOp.from_column_and_tablename(
  338. schema, tname, metadata_cols_by_name[cname]
  339. )
  340. )
  341. log.info("Detected added column '%s.%s'", name, cname)
  342. for colname in metadata_col_names.intersection(conn_col_names):
  343. metadata_col = metadata_cols_by_name[colname]
  344. conn_col = conn_table.c[colname]
  345. if not autogen_context.run_object_filters(
  346. metadata_col, colname, "column", False, conn_col
  347. ):
  348. continue
  349. alter_column_op = ops.AlterColumnOp(tname, colname, schema=schema)
  350. comparators.dispatch("column")(
  351. autogen_context,
  352. alter_column_op,
  353. schema,
  354. tname,
  355. colname,
  356. conn_col,
  357. metadata_col,
  358. )
  359. if alter_column_op.has_changes():
  360. modify_table_ops.ops.append(alter_column_op)
  361. yield
  362. for cname in set(conn_col_names).difference(metadata_col_names):
  363. if autogen_context.run_object_filters(
  364. conn_table.c[cname], cname, "column", True, None
  365. ):
  366. modify_table_ops.ops.append(
  367. ops.DropColumnOp.from_column_and_tablename(
  368. schema, tname, conn_table.c[cname]
  369. )
  370. )
  371. log.info("Detected removed column '%s.%s'", name, cname)
  372. _C = TypeVar("_C", bound=Union[UniqueConstraint, ForeignKeyConstraint, Index])
  373. @comparators.dispatch_for("table")
  374. def _compare_indexes_and_uniques(
  375. autogen_context: AutogenContext,
  376. modify_ops: ModifyTableOps,
  377. schema: Optional[str],
  378. tname: Union[quoted_name, str],
  379. conn_table: Optional[Table],
  380. metadata_table: Optional[Table],
  381. ) -> None:
  382. inspector = autogen_context.inspector
  383. is_create_table = conn_table is None
  384. is_drop_table = metadata_table is None
  385. impl = autogen_context.migration_context.impl
  386. # 1a. get raw indexes and unique constraints from metadata ...
  387. if metadata_table is not None:
  388. metadata_unique_constraints = {
  389. uq
  390. for uq in metadata_table.constraints
  391. if isinstance(uq, sa_schema.UniqueConstraint)
  392. }
  393. metadata_indexes = set(metadata_table.indexes)
  394. else:
  395. metadata_unique_constraints = set()
  396. metadata_indexes = set()
  397. conn_uniques = conn_indexes = frozenset() # type:ignore[var-annotated]
  398. supports_unique_constraints = False
  399. unique_constraints_duplicate_unique_indexes = False
  400. if conn_table is not None:
  401. # 1b. ... and from connection, if the table exists
  402. try:
  403. conn_uniques = inspector.get_unique_constraints( # type:ignore[assignment] # noqa
  404. tname, schema=schema
  405. )
  406. supports_unique_constraints = True
  407. except NotImplementedError:
  408. pass
  409. except TypeError:
  410. # number of arguments is off for the base
  411. # method in SQLAlchemy due to the cache decorator
  412. # not being present
  413. pass
  414. else:
  415. conn_uniques = [ # type:ignore[assignment]
  416. uq
  417. for uq in conn_uniques
  418. if autogen_context.run_name_filters(
  419. uq["name"],
  420. "unique_constraint",
  421. {"table_name": tname, "schema_name": schema},
  422. )
  423. ]
  424. for uq in conn_uniques:
  425. if uq.get("duplicates_index"):
  426. unique_constraints_duplicate_unique_indexes = True
  427. try:
  428. conn_indexes = inspector.get_indexes( # type:ignore[assignment]
  429. tname, schema=schema
  430. )
  431. except NotImplementedError:
  432. pass
  433. else:
  434. conn_indexes = [ # type:ignore[assignment]
  435. ix
  436. for ix in conn_indexes
  437. if autogen_context.run_name_filters(
  438. ix["name"],
  439. "index",
  440. {"table_name": tname, "schema_name": schema},
  441. )
  442. ]
  443. # 2. convert conn-level objects from raw inspector records
  444. # into schema objects
  445. if is_drop_table:
  446. # for DROP TABLE uniques are inline, don't need them
  447. conn_uniques = set() # type:ignore[assignment]
  448. else:
  449. conn_uniques = { # type:ignore[assignment]
  450. _make_unique_constraint(impl, uq_def, conn_table)
  451. for uq_def in conn_uniques
  452. }
  453. conn_indexes = { # type:ignore[assignment]
  454. index
  455. for index in (
  456. _make_index(impl, ix, conn_table) for ix in conn_indexes
  457. )
  458. if index is not None
  459. }
  460. # 2a. if the dialect dupes unique indexes as unique constraints
  461. # (mysql and oracle), correct for that
  462. if unique_constraints_duplicate_unique_indexes:
  463. _correct_for_uq_duplicates_uix(
  464. conn_uniques,
  465. conn_indexes,
  466. metadata_unique_constraints,
  467. metadata_indexes,
  468. autogen_context.dialect,
  469. impl,
  470. )
  471. # 3. give the dialect a chance to omit indexes and constraints that
  472. # we know are either added implicitly by the DB or that the DB
  473. # can't accurately report on
  474. impl.correct_for_autogen_constraints(
  475. conn_uniques, # type: ignore[arg-type]
  476. conn_indexes, # type: ignore[arg-type]
  477. metadata_unique_constraints,
  478. metadata_indexes,
  479. )
  480. # 4. organize the constraints into "signature" collections, the
  481. # _constraint_sig() objects provide a consistent facade over both
  482. # Index and UniqueConstraint so we can easily work with them
  483. # interchangeably
  484. metadata_unique_constraints_sig = {
  485. impl._create_metadata_constraint_sig(uq)
  486. for uq in metadata_unique_constraints
  487. }
  488. metadata_indexes_sig = {
  489. impl._create_metadata_constraint_sig(ix) for ix in metadata_indexes
  490. }
  491. conn_unique_constraints = {
  492. impl._create_reflected_constraint_sig(uq) for uq in conn_uniques
  493. }
  494. conn_indexes_sig = {
  495. impl._create_reflected_constraint_sig(ix) for ix in conn_indexes
  496. }
  497. # 5. index things by name, for those objects that have names
  498. metadata_names = {
  499. cast(str, c.md_name_to_sql_name(autogen_context)): c
  500. for c in metadata_unique_constraints_sig.union(metadata_indexes_sig)
  501. if c.is_named
  502. }
  503. conn_uniques_by_name: Dict[sqla_compat._ConstraintName, _constraint_sig]
  504. conn_indexes_by_name: Dict[sqla_compat._ConstraintName, _constraint_sig]
  505. conn_uniques_by_name = {c.name: c for c in conn_unique_constraints}
  506. conn_indexes_by_name = {c.name: c for c in conn_indexes_sig}
  507. conn_names = {
  508. c.name: c
  509. for c in conn_unique_constraints.union(conn_indexes_sig)
  510. if sqla_compat.constraint_name_string(c.name)
  511. }
  512. doubled_constraints = {
  513. name: (conn_uniques_by_name[name], conn_indexes_by_name[name])
  514. for name in set(conn_uniques_by_name).intersection(
  515. conn_indexes_by_name
  516. )
  517. }
  518. # 6. index things by "column signature", to help with unnamed unique
  519. # constraints.
  520. conn_uniques_by_sig = {uq.unnamed: uq for uq in conn_unique_constraints}
  521. metadata_uniques_by_sig = {
  522. uq.unnamed: uq for uq in metadata_unique_constraints_sig
  523. }
  524. unnamed_metadata_uniques = {
  525. uq.unnamed: uq
  526. for uq in metadata_unique_constraints_sig
  527. if not sqla_compat._constraint_is_named(
  528. uq.const, autogen_context.dialect
  529. )
  530. }
  531. # assumptions:
  532. # 1. a unique constraint or an index from the connection *always*
  533. # has a name.
  534. # 2. an index on the metadata side *always* has a name.
  535. # 3. a unique constraint on the metadata side *might* have a name.
  536. # 4. The backend may double up indexes as unique constraints and
  537. # vice versa (e.g. MySQL, Postgresql)
  538. def obj_added(obj: _constraint_sig):
  539. if is_index_sig(obj):
  540. if autogen_context.run_object_filters(
  541. obj.const, obj.name, "index", False, None
  542. ):
  543. modify_ops.ops.append(ops.CreateIndexOp.from_index(obj.const))
  544. log.info(
  545. "Detected added index '%r' on '%s'",
  546. obj.name,
  547. obj.column_names,
  548. )
  549. elif is_uq_sig(obj):
  550. if not supports_unique_constraints:
  551. # can't report unique indexes as added if we don't
  552. # detect them
  553. return
  554. if is_create_table or is_drop_table:
  555. # unique constraints are created inline with table defs
  556. return
  557. if autogen_context.run_object_filters(
  558. obj.const, obj.name, "unique_constraint", False, None
  559. ):
  560. modify_ops.ops.append(
  561. ops.AddConstraintOp.from_constraint(obj.const)
  562. )
  563. log.info(
  564. "Detected added unique constraint %r on '%s'",
  565. obj.name,
  566. obj.column_names,
  567. )
  568. else:
  569. assert False
  570. def obj_removed(obj: _constraint_sig):
  571. if is_index_sig(obj):
  572. if obj.is_unique and not supports_unique_constraints:
  573. # many databases double up unique constraints
  574. # as unique indexes. without that list we can't
  575. # be sure what we're doing here
  576. return
  577. if autogen_context.run_object_filters(
  578. obj.const, obj.name, "index", True, None
  579. ):
  580. modify_ops.ops.append(ops.DropIndexOp.from_index(obj.const))
  581. log.info("Detected removed index %r on %r", obj.name, tname)
  582. elif is_uq_sig(obj):
  583. if is_create_table or is_drop_table:
  584. # if the whole table is being dropped, we don't need to
  585. # consider unique constraint separately
  586. return
  587. if autogen_context.run_object_filters(
  588. obj.const, obj.name, "unique_constraint", True, None
  589. ):
  590. modify_ops.ops.append(
  591. ops.DropConstraintOp.from_constraint(obj.const)
  592. )
  593. log.info(
  594. "Detected removed unique constraint %r on %r",
  595. obj.name,
  596. tname,
  597. )
  598. else:
  599. assert False
  600. def obj_changed(
  601. old: _constraint_sig,
  602. new: _constraint_sig,
  603. msg: str,
  604. ):
  605. if is_index_sig(old):
  606. assert is_index_sig(new)
  607. if autogen_context.run_object_filters(
  608. new.const, new.name, "index", False, old.const
  609. ):
  610. log.info(
  611. "Detected changed index %r on %r: %s", old.name, tname, msg
  612. )
  613. modify_ops.ops.append(ops.DropIndexOp.from_index(old.const))
  614. modify_ops.ops.append(ops.CreateIndexOp.from_index(new.const))
  615. elif is_uq_sig(old):
  616. assert is_uq_sig(new)
  617. if autogen_context.run_object_filters(
  618. new.const, new.name, "unique_constraint", False, old.const
  619. ):
  620. log.info(
  621. "Detected changed unique constraint %r on %r: %s",
  622. old.name,
  623. tname,
  624. msg,
  625. )
  626. modify_ops.ops.append(
  627. ops.DropConstraintOp.from_constraint(old.const)
  628. )
  629. modify_ops.ops.append(
  630. ops.AddConstraintOp.from_constraint(new.const)
  631. )
  632. else:
  633. assert False
  634. for removed_name in sorted(set(conn_names).difference(metadata_names)):
  635. conn_obj = conn_names[removed_name]
  636. if (
  637. is_uq_sig(conn_obj)
  638. and conn_obj.unnamed in unnamed_metadata_uniques
  639. ):
  640. continue
  641. elif removed_name in doubled_constraints:
  642. conn_uq, conn_idx = doubled_constraints[removed_name]
  643. if (
  644. all(
  645. conn_idx.unnamed != meta_idx.unnamed
  646. for meta_idx in metadata_indexes_sig
  647. )
  648. and conn_uq.unnamed not in metadata_uniques_by_sig
  649. ):
  650. obj_removed(conn_uq)
  651. obj_removed(conn_idx)
  652. else:
  653. obj_removed(conn_obj)
  654. for existing_name in sorted(set(metadata_names).intersection(conn_names)):
  655. metadata_obj = metadata_names[existing_name]
  656. if existing_name in doubled_constraints:
  657. conn_uq, conn_idx = doubled_constraints[existing_name]
  658. if is_index_sig(metadata_obj):
  659. conn_obj = conn_idx
  660. else:
  661. conn_obj = conn_uq
  662. else:
  663. conn_obj = conn_names[existing_name]
  664. if type(conn_obj) != type(metadata_obj):
  665. obj_removed(conn_obj)
  666. obj_added(metadata_obj)
  667. else:
  668. comparison = metadata_obj.compare_to_reflected(conn_obj)
  669. if comparison.is_different:
  670. # constraint are different
  671. obj_changed(conn_obj, metadata_obj, comparison.message)
  672. elif comparison.is_skip:
  673. # constraint cannot be compared, skip them
  674. thing = (
  675. "index" if is_index_sig(conn_obj) else "unique constraint"
  676. )
  677. log.info(
  678. "Cannot compare %s %r, assuming equal and skipping. %s",
  679. thing,
  680. conn_obj.name,
  681. comparison.message,
  682. )
  683. else:
  684. # constraint are equal
  685. assert comparison.is_equal
  686. for added_name in sorted(set(metadata_names).difference(conn_names)):
  687. obj = metadata_names[added_name]
  688. obj_added(obj)
  689. for uq_sig in unnamed_metadata_uniques:
  690. if uq_sig not in conn_uniques_by_sig:
  691. obj_added(unnamed_metadata_uniques[uq_sig])
  692. def _correct_for_uq_duplicates_uix(
  693. conn_unique_constraints,
  694. conn_indexes,
  695. metadata_unique_constraints,
  696. metadata_indexes,
  697. dialect,
  698. impl,
  699. ):
  700. # dedupe unique indexes vs. constraints, since MySQL / Oracle
  701. # doesn't really have unique constraints as a separate construct.
  702. # but look in the metadata and try to maintain constructs
  703. # that already seem to be defined one way or the other
  704. # on that side. This logic was formerly local to MySQL dialect,
  705. # generalized to Oracle and others. See #276
  706. # resolve final rendered name for unique constraints defined in the
  707. # metadata. this includes truncation of long names. naming convention
  708. # names currently should already be set as cons.name, however leave this
  709. # to the sqla_compat to decide.
  710. metadata_cons_names = [
  711. (sqla_compat._get_constraint_final_name(cons, dialect), cons)
  712. for cons in metadata_unique_constraints
  713. ]
  714. metadata_uq_names = {
  715. name for name, cons in metadata_cons_names if name is not None
  716. }
  717. unnamed_metadata_uqs = {
  718. impl._create_metadata_constraint_sig(cons).unnamed
  719. for name, cons in metadata_cons_names
  720. if name is None
  721. }
  722. metadata_ix_names = {
  723. sqla_compat._get_constraint_final_name(cons, dialect)
  724. for cons in metadata_indexes
  725. if cons.unique
  726. }
  727. # for reflection side, names are in their final database form
  728. # already since they're from the database
  729. conn_ix_names = {cons.name: cons for cons in conn_indexes if cons.unique}
  730. uqs_dupe_indexes = {
  731. cons.name: cons
  732. for cons in conn_unique_constraints
  733. if cons.info["duplicates_index"]
  734. }
  735. for overlap in uqs_dupe_indexes:
  736. if overlap not in metadata_uq_names:
  737. if (
  738. impl._create_reflected_constraint_sig(
  739. uqs_dupe_indexes[overlap]
  740. ).unnamed
  741. not in unnamed_metadata_uqs
  742. ):
  743. conn_unique_constraints.discard(uqs_dupe_indexes[overlap])
  744. elif overlap not in metadata_ix_names:
  745. conn_indexes.discard(conn_ix_names[overlap])
  746. @comparators.dispatch_for("column")
  747. def _compare_nullable(
  748. autogen_context: AutogenContext,
  749. alter_column_op: AlterColumnOp,
  750. schema: Optional[str],
  751. tname: Union[quoted_name, str],
  752. cname: Union[quoted_name, str],
  753. conn_col: Column[Any],
  754. metadata_col: Column[Any],
  755. ) -> None:
  756. metadata_col_nullable = metadata_col.nullable
  757. conn_col_nullable = conn_col.nullable
  758. alter_column_op.existing_nullable = conn_col_nullable
  759. if conn_col_nullable is not metadata_col_nullable:
  760. if (
  761. sqla_compat._server_default_is_computed(
  762. metadata_col.server_default, conn_col.server_default
  763. )
  764. and sqla_compat._nullability_might_be_unset(metadata_col)
  765. or (
  766. sqla_compat._server_default_is_identity(
  767. metadata_col.server_default, conn_col.server_default
  768. )
  769. )
  770. ):
  771. log.info(
  772. "Ignoring nullable change on identity column '%s.%s'",
  773. tname,
  774. cname,
  775. )
  776. else:
  777. alter_column_op.modify_nullable = metadata_col_nullable
  778. log.info(
  779. "Detected %s on column '%s.%s'",
  780. "NULL" if metadata_col_nullable else "NOT NULL",
  781. tname,
  782. cname,
  783. )
  784. @comparators.dispatch_for("column")
  785. def _setup_autoincrement(
  786. autogen_context: AutogenContext,
  787. alter_column_op: AlterColumnOp,
  788. schema: Optional[str],
  789. tname: Union[quoted_name, str],
  790. cname: quoted_name,
  791. conn_col: Column[Any],
  792. metadata_col: Column[Any],
  793. ) -> None:
  794. if metadata_col.table._autoincrement_column is metadata_col:
  795. alter_column_op.kw["autoincrement"] = True
  796. elif metadata_col.autoincrement is True:
  797. alter_column_op.kw["autoincrement"] = True
  798. elif metadata_col.autoincrement is False:
  799. alter_column_op.kw["autoincrement"] = False
  800. @comparators.dispatch_for("column")
  801. def _compare_type(
  802. autogen_context: AutogenContext,
  803. alter_column_op: AlterColumnOp,
  804. schema: Optional[str],
  805. tname: Union[quoted_name, str],
  806. cname: Union[quoted_name, str],
  807. conn_col: Column[Any],
  808. metadata_col: Column[Any],
  809. ) -> None:
  810. conn_type = conn_col.type
  811. alter_column_op.existing_type = conn_type
  812. metadata_type = metadata_col.type
  813. if conn_type._type_affinity is sqltypes.NullType:
  814. log.info(
  815. "Couldn't determine database type " "for column '%s.%s'",
  816. tname,
  817. cname,
  818. )
  819. return
  820. if metadata_type._type_affinity is sqltypes.NullType:
  821. log.info(
  822. "Column '%s.%s' has no type within " "the model; can't compare",
  823. tname,
  824. cname,
  825. )
  826. return
  827. isdiff = autogen_context.migration_context._compare_type(
  828. conn_col, metadata_col
  829. )
  830. if isdiff:
  831. alter_column_op.modify_type = metadata_type
  832. log.info(
  833. "Detected type change from %r to %r on '%s.%s'",
  834. conn_type,
  835. metadata_type,
  836. tname,
  837. cname,
  838. )
  839. def _render_server_default_for_compare(
  840. metadata_default: Optional[Any], autogen_context: AutogenContext
  841. ) -> Optional[str]:
  842. if isinstance(metadata_default, sa_schema.DefaultClause):
  843. if isinstance(metadata_default.arg, str):
  844. metadata_default = metadata_default.arg
  845. else:
  846. metadata_default = str(
  847. metadata_default.arg.compile(
  848. dialect=autogen_context.dialect,
  849. compile_kwargs={"literal_binds": True},
  850. )
  851. )
  852. if isinstance(metadata_default, str):
  853. return metadata_default
  854. else:
  855. return None
  856. def _normalize_computed_default(sqltext: str) -> str:
  857. """we want to warn if a computed sql expression has changed. however
  858. we don't want false positives and the warning is not that critical.
  859. so filter out most forms of variability from the SQL text.
  860. """
  861. return re.sub(r"[ \(\)'\"`\[\]\t\r\n]", "", sqltext).lower()
  862. def _compare_computed_default(
  863. autogen_context: AutogenContext,
  864. alter_column_op: AlterColumnOp,
  865. schema: Optional[str],
  866. tname: str,
  867. cname: str,
  868. conn_col: Column[Any],
  869. metadata_col: Column[Any],
  870. ) -> None:
  871. rendered_metadata_default = str(
  872. cast(sa_schema.Computed, metadata_col.server_default).sqltext.compile(
  873. dialect=autogen_context.dialect,
  874. compile_kwargs={"literal_binds": True},
  875. )
  876. )
  877. # since we cannot change computed columns, we do only a crude comparison
  878. # here where we try to eliminate syntactical differences in order to
  879. # get a minimal comparison just to emit a warning.
  880. rendered_metadata_default = _normalize_computed_default(
  881. rendered_metadata_default
  882. )
  883. if isinstance(conn_col.server_default, sa_schema.Computed):
  884. rendered_conn_default = str(
  885. conn_col.server_default.sqltext.compile(
  886. dialect=autogen_context.dialect,
  887. compile_kwargs={"literal_binds": True},
  888. )
  889. )
  890. if rendered_conn_default is None:
  891. rendered_conn_default = ""
  892. else:
  893. rendered_conn_default = _normalize_computed_default(
  894. rendered_conn_default
  895. )
  896. else:
  897. rendered_conn_default = ""
  898. if rendered_metadata_default != rendered_conn_default:
  899. _warn_computed_not_supported(tname, cname)
  900. def _warn_computed_not_supported(tname: str, cname: str) -> None:
  901. util.warn("Computed default on %s.%s cannot be modified" % (tname, cname))
  902. def _compare_identity_default(
  903. autogen_context,
  904. alter_column_op,
  905. schema,
  906. tname,
  907. cname,
  908. conn_col,
  909. metadata_col,
  910. ):
  911. impl = autogen_context.migration_context.impl
  912. diff, ignored_attr, is_alter = impl._compare_identity_default(
  913. metadata_col.server_default, conn_col.server_default
  914. )
  915. return diff, is_alter
  916. @comparators.dispatch_for("column")
  917. def _compare_server_default(
  918. autogen_context: AutogenContext,
  919. alter_column_op: AlterColumnOp,
  920. schema: Optional[str],
  921. tname: Union[quoted_name, str],
  922. cname: Union[quoted_name, str],
  923. conn_col: Column[Any],
  924. metadata_col: Column[Any],
  925. ) -> Optional[bool]:
  926. metadata_default = metadata_col.server_default
  927. conn_col_default = conn_col.server_default
  928. if conn_col_default is None and metadata_default is None:
  929. return False
  930. if sqla_compat._server_default_is_computed(metadata_default):
  931. return _compare_computed_default( # type:ignore[func-returns-value]
  932. autogen_context,
  933. alter_column_op,
  934. schema,
  935. tname,
  936. cname,
  937. conn_col,
  938. metadata_col,
  939. )
  940. if sqla_compat._server_default_is_computed(conn_col_default):
  941. _warn_computed_not_supported(tname, cname)
  942. return False
  943. if sqla_compat._server_default_is_identity(
  944. metadata_default, conn_col_default
  945. ):
  946. alter_column_op.existing_server_default = conn_col_default
  947. diff, is_alter = _compare_identity_default(
  948. autogen_context,
  949. alter_column_op,
  950. schema,
  951. tname,
  952. cname,
  953. conn_col,
  954. metadata_col,
  955. )
  956. if is_alter:
  957. alter_column_op.modify_server_default = metadata_default
  958. if diff:
  959. log.info(
  960. "Detected server default on column '%s.%s': "
  961. "identity options attributes %s",
  962. tname,
  963. cname,
  964. sorted(diff),
  965. )
  966. else:
  967. rendered_metadata_default = _render_server_default_for_compare(
  968. metadata_default, autogen_context
  969. )
  970. rendered_conn_default = (
  971. cast(Any, conn_col_default).arg.text if conn_col_default else None
  972. )
  973. alter_column_op.existing_server_default = conn_col_default
  974. is_diff = autogen_context.migration_context._compare_server_default(
  975. conn_col,
  976. metadata_col,
  977. rendered_metadata_default,
  978. rendered_conn_default,
  979. )
  980. if is_diff:
  981. alter_column_op.modify_server_default = metadata_default
  982. log.info("Detected server default on column '%s.%s'", tname, cname)
  983. return None
  984. @comparators.dispatch_for("column")
  985. def _compare_column_comment(
  986. autogen_context: AutogenContext,
  987. alter_column_op: AlterColumnOp,
  988. schema: Optional[str],
  989. tname: Union[quoted_name, str],
  990. cname: quoted_name,
  991. conn_col: Column[Any],
  992. metadata_col: Column[Any],
  993. ) -> Optional[Literal[False]]:
  994. assert autogen_context.dialect is not None
  995. if not autogen_context.dialect.supports_comments:
  996. return None
  997. metadata_comment = metadata_col.comment
  998. conn_col_comment = conn_col.comment
  999. if conn_col_comment is None and metadata_comment is None:
  1000. return False
  1001. alter_column_op.existing_comment = conn_col_comment
  1002. if conn_col_comment != metadata_comment:
  1003. alter_column_op.modify_comment = metadata_comment
  1004. log.info("Detected column comment '%s.%s'", tname, cname)
  1005. return None
  1006. @comparators.dispatch_for("table")
  1007. def _compare_foreign_keys(
  1008. autogen_context: AutogenContext,
  1009. modify_table_ops: ModifyTableOps,
  1010. schema: Optional[str],
  1011. tname: Union[quoted_name, str],
  1012. conn_table: Table,
  1013. metadata_table: Table,
  1014. ) -> None:
  1015. # if we're doing CREATE TABLE, all FKs are created
  1016. # inline within the table def
  1017. if conn_table is None or metadata_table is None:
  1018. return
  1019. inspector = autogen_context.inspector
  1020. metadata_fks = {
  1021. fk
  1022. for fk in metadata_table.constraints
  1023. if isinstance(fk, sa_schema.ForeignKeyConstraint)
  1024. }
  1025. conn_fks_list = [
  1026. fk
  1027. for fk in inspector.get_foreign_keys(tname, schema=schema)
  1028. if autogen_context.run_name_filters(
  1029. fk["name"],
  1030. "foreign_key_constraint",
  1031. {"table_name": tname, "schema_name": schema},
  1032. )
  1033. ]
  1034. conn_fks = {
  1035. _make_foreign_key(const, conn_table) # type: ignore[arg-type]
  1036. for const in conn_fks_list
  1037. }
  1038. impl = autogen_context.migration_context.impl
  1039. # give the dialect a chance to correct the FKs to match more
  1040. # closely
  1041. autogen_context.migration_context.impl.correct_for_autogen_foreignkeys(
  1042. conn_fks, metadata_fks
  1043. )
  1044. metadata_fks_sig = {
  1045. impl._create_metadata_constraint_sig(fk) for fk in metadata_fks
  1046. }
  1047. conn_fks_sig = {
  1048. impl._create_reflected_constraint_sig(fk) for fk in conn_fks
  1049. }
  1050. # check if reflected FKs include options, indicating the backend
  1051. # can reflect FK options
  1052. if conn_fks_list and "options" in conn_fks_list[0]:
  1053. conn_fks_by_sig = {c.unnamed: c for c in conn_fks_sig}
  1054. metadata_fks_by_sig = {c.unnamed: c for c in metadata_fks_sig}
  1055. else:
  1056. # otherwise compare by sig without options added
  1057. conn_fks_by_sig = {c.unnamed_no_options: c for c in conn_fks_sig}
  1058. metadata_fks_by_sig = {
  1059. c.unnamed_no_options: c for c in metadata_fks_sig
  1060. }
  1061. metadata_fks_by_name = {
  1062. c.name: c for c in metadata_fks_sig if c.name is not None
  1063. }
  1064. conn_fks_by_name = {c.name: c for c in conn_fks_sig if c.name is not None}
  1065. def _add_fk(obj, compare_to):
  1066. if autogen_context.run_object_filters(
  1067. obj.const, obj.name, "foreign_key_constraint", False, compare_to
  1068. ):
  1069. modify_table_ops.ops.append(
  1070. ops.CreateForeignKeyOp.from_constraint(const.const) # type: ignore[has-type] # noqa: E501
  1071. )
  1072. log.info(
  1073. "Detected added foreign key (%s)(%s) on table %s%s",
  1074. ", ".join(obj.source_columns),
  1075. ", ".join(obj.target_columns),
  1076. "%s." % obj.source_schema if obj.source_schema else "",
  1077. obj.source_table,
  1078. )
  1079. def _remove_fk(obj, compare_to):
  1080. if autogen_context.run_object_filters(
  1081. obj.const, obj.name, "foreign_key_constraint", True, compare_to
  1082. ):
  1083. modify_table_ops.ops.append(
  1084. ops.DropConstraintOp.from_constraint(obj.const)
  1085. )
  1086. log.info(
  1087. "Detected removed foreign key (%s)(%s) on table %s%s",
  1088. ", ".join(obj.source_columns),
  1089. ", ".join(obj.target_columns),
  1090. "%s." % obj.source_schema if obj.source_schema else "",
  1091. obj.source_table,
  1092. )
  1093. # so far it appears we don't need to do this by name at all.
  1094. # SQLite doesn't preserve constraint names anyway
  1095. for removed_sig in set(conn_fks_by_sig).difference(metadata_fks_by_sig):
  1096. const = conn_fks_by_sig[removed_sig]
  1097. if removed_sig not in metadata_fks_by_sig:
  1098. compare_to = (
  1099. metadata_fks_by_name[const.name].const
  1100. if const.name in metadata_fks_by_name
  1101. else None
  1102. )
  1103. _remove_fk(const, compare_to)
  1104. for added_sig in set(metadata_fks_by_sig).difference(conn_fks_by_sig):
  1105. const = metadata_fks_by_sig[added_sig]
  1106. if added_sig not in conn_fks_by_sig:
  1107. compare_to = (
  1108. conn_fks_by_name[const.name].const
  1109. if const.name in conn_fks_by_name
  1110. else None
  1111. )
  1112. _add_fk(const, compare_to)
  1113. @comparators.dispatch_for("table")
  1114. def _compare_table_comment(
  1115. autogen_context: AutogenContext,
  1116. modify_table_ops: ModifyTableOps,
  1117. schema: Optional[str],
  1118. tname: Union[quoted_name, str],
  1119. conn_table: Optional[Table],
  1120. metadata_table: Optional[Table],
  1121. ) -> None:
  1122. assert autogen_context.dialect is not None
  1123. if not autogen_context.dialect.supports_comments:
  1124. return
  1125. # if we're doing CREATE TABLE, comments will be created inline
  1126. # with the create_table op.
  1127. if conn_table is None or metadata_table is None:
  1128. return
  1129. if conn_table.comment is None and metadata_table.comment is None:
  1130. return
  1131. if metadata_table.comment is None and conn_table.comment is not None:
  1132. modify_table_ops.ops.append(
  1133. ops.DropTableCommentOp(
  1134. tname, existing_comment=conn_table.comment, schema=schema
  1135. )
  1136. )
  1137. elif metadata_table.comment != conn_table.comment:
  1138. modify_table_ops.ops.append(
  1139. ops.CreateTableCommentOp(
  1140. tname,
  1141. metadata_table.comment,
  1142. existing_comment=conn_table.comment,
  1143. schema=schema,
  1144. )
  1145. )