impl.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844
  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 logging
  5. import re
  6. from typing import Any
  7. from typing import Callable
  8. from typing import Dict
  9. from typing import Iterable
  10. from typing import List
  11. from typing import Mapping
  12. from typing import NamedTuple
  13. from typing import Optional
  14. from typing import Sequence
  15. from typing import Set
  16. from typing import Tuple
  17. from typing import Type
  18. from typing import TYPE_CHECKING
  19. from typing import Union
  20. from sqlalchemy import cast
  21. from sqlalchemy import schema
  22. from sqlalchemy import text
  23. from . import _autogen
  24. from . import base
  25. from ._autogen import _constraint_sig as _constraint_sig
  26. from ._autogen import ComparisonResult as ComparisonResult
  27. from .. import util
  28. from ..util import sqla_compat
  29. if TYPE_CHECKING:
  30. from typing import Literal
  31. from typing import TextIO
  32. from sqlalchemy.engine import Connection
  33. from sqlalchemy.engine import Dialect
  34. from sqlalchemy.engine.cursor import CursorResult
  35. from sqlalchemy.engine.reflection import Inspector
  36. from sqlalchemy.sql import ClauseElement
  37. from sqlalchemy.sql import Executable
  38. from sqlalchemy.sql.elements import ColumnElement
  39. from sqlalchemy.sql.elements import quoted_name
  40. from sqlalchemy.sql.schema import Column
  41. from sqlalchemy.sql.schema import Constraint
  42. from sqlalchemy.sql.schema import ForeignKeyConstraint
  43. from sqlalchemy.sql.schema import Index
  44. from sqlalchemy.sql.schema import Table
  45. from sqlalchemy.sql.schema import UniqueConstraint
  46. from sqlalchemy.sql.selectable import TableClause
  47. from sqlalchemy.sql.type_api import TypeEngine
  48. from .base import _ServerDefault
  49. from ..autogenerate.api import AutogenContext
  50. from ..operations.batch import ApplyBatchImpl
  51. from ..operations.batch import BatchOperationsImpl
  52. log = logging.getLogger(__name__)
  53. class ImplMeta(type):
  54. def __init__(
  55. cls,
  56. classname: str,
  57. bases: Tuple[Type[DefaultImpl]],
  58. dict_: Dict[str, Any],
  59. ):
  60. newtype = type.__init__(cls, classname, bases, dict_)
  61. if "__dialect__" in dict_:
  62. _impls[dict_["__dialect__"]] = cls # type: ignore[assignment]
  63. return newtype
  64. _impls: Dict[str, Type[DefaultImpl]] = {}
  65. class DefaultImpl(metaclass=ImplMeta):
  66. """Provide the entrypoint for major migration operations,
  67. including database-specific behavioral variances.
  68. While individual SQL/DDL constructs already provide
  69. for database-specific implementations, variances here
  70. allow for entirely different sequences of operations
  71. to take place for a particular migration, such as
  72. SQL Server's special 'IDENTITY INSERT' step for
  73. bulk inserts.
  74. """
  75. __dialect__ = "default"
  76. transactional_ddl = False
  77. command_terminator = ";"
  78. type_synonyms: Tuple[Set[str], ...] = ({"NUMERIC", "DECIMAL"},)
  79. type_arg_extract: Sequence[str] = ()
  80. # These attributes are deprecated in SQLAlchemy via #10247. They need to
  81. # be ignored to support older version that did not use dialect kwargs.
  82. # They only apply to Oracle and are replaced by oracle_order,
  83. # oracle_on_null
  84. identity_attrs_ignore: Tuple[str, ...] = ("order", "on_null")
  85. def __init__(
  86. self,
  87. dialect: Dialect,
  88. connection: Optional[Connection],
  89. as_sql: bool,
  90. transactional_ddl: Optional[bool],
  91. output_buffer: Optional[TextIO],
  92. context_opts: Dict[str, Any],
  93. ) -> None:
  94. self.dialect = dialect
  95. self.connection = connection
  96. self.as_sql = as_sql
  97. self.literal_binds = context_opts.get("literal_binds", False)
  98. self.output_buffer = output_buffer
  99. self.memo: dict = {}
  100. self.context_opts = context_opts
  101. if transactional_ddl is not None:
  102. self.transactional_ddl = transactional_ddl
  103. if self.literal_binds:
  104. if not self.as_sql:
  105. raise util.CommandError(
  106. "Can't use literal_binds setting without as_sql mode"
  107. )
  108. @classmethod
  109. def get_by_dialect(cls, dialect: Dialect) -> Type[DefaultImpl]:
  110. return _impls[dialect.name]
  111. def static_output(self, text: str) -> None:
  112. assert self.output_buffer is not None
  113. self.output_buffer.write(text + "\n\n")
  114. self.output_buffer.flush()
  115. def requires_recreate_in_batch(
  116. self, batch_op: BatchOperationsImpl
  117. ) -> bool:
  118. """Return True if the given :class:`.BatchOperationsImpl`
  119. would need the table to be recreated and copied in order to
  120. proceed.
  121. Normally, only returns True on SQLite when operations other
  122. than add_column are present.
  123. """
  124. return False
  125. def prep_table_for_batch(
  126. self, batch_impl: ApplyBatchImpl, table: Table
  127. ) -> None:
  128. """perform any operations needed on a table before a new
  129. one is created to replace it in batch mode.
  130. the PG dialect uses this to drop constraints on the table
  131. before the new one uses those same names.
  132. """
  133. @property
  134. def bind(self) -> Optional[Connection]:
  135. return self.connection
  136. def _exec(
  137. self,
  138. construct: Union[Executable, str],
  139. execution_options: Optional[dict[str, Any]] = None,
  140. multiparams: Sequence[dict] = (),
  141. params: Dict[str, Any] = util.immutabledict(),
  142. ) -> Optional[CursorResult]:
  143. if isinstance(construct, str):
  144. construct = text(construct)
  145. if self.as_sql:
  146. if multiparams or params:
  147. # TODO: coverage
  148. raise Exception("Execution arguments not allowed with as_sql")
  149. compile_kw: dict[str, Any]
  150. if self.literal_binds and not isinstance(
  151. construct, schema.DDLElement
  152. ):
  153. compile_kw = dict(compile_kwargs={"literal_binds": True})
  154. else:
  155. compile_kw = {}
  156. if TYPE_CHECKING:
  157. assert isinstance(construct, ClauseElement)
  158. compiled = construct.compile(dialect=self.dialect, **compile_kw)
  159. self.static_output(
  160. str(compiled).replace("\t", " ").strip()
  161. + self.command_terminator
  162. )
  163. return None
  164. else:
  165. conn = self.connection
  166. assert conn is not None
  167. if execution_options:
  168. conn = conn.execution_options(**execution_options)
  169. if params:
  170. assert isinstance(multiparams, tuple)
  171. multiparams += (params,)
  172. return conn.execute(construct, multiparams)
  173. def execute(
  174. self,
  175. sql: Union[Executable, str],
  176. execution_options: Optional[dict[str, Any]] = None,
  177. ) -> None:
  178. self._exec(sql, execution_options)
  179. def alter_column(
  180. self,
  181. table_name: str,
  182. column_name: str,
  183. nullable: Optional[bool] = None,
  184. server_default: Union[_ServerDefault, Literal[False]] = False,
  185. name: Optional[str] = None,
  186. type_: Optional[TypeEngine] = None,
  187. schema: Optional[str] = None,
  188. autoincrement: Optional[bool] = None,
  189. comment: Optional[Union[str, Literal[False]]] = False,
  190. existing_comment: Optional[str] = None,
  191. existing_type: Optional[TypeEngine] = None,
  192. existing_server_default: Optional[_ServerDefault] = None,
  193. existing_nullable: Optional[bool] = None,
  194. existing_autoincrement: Optional[bool] = None,
  195. **kw: Any,
  196. ) -> None:
  197. if autoincrement is not None or existing_autoincrement is not None:
  198. util.warn(
  199. "autoincrement and existing_autoincrement "
  200. "only make sense for MySQL",
  201. stacklevel=3,
  202. )
  203. if nullable is not None:
  204. self._exec(
  205. base.ColumnNullable(
  206. table_name,
  207. column_name,
  208. nullable,
  209. schema=schema,
  210. existing_type=existing_type,
  211. existing_server_default=existing_server_default,
  212. existing_nullable=existing_nullable,
  213. existing_comment=existing_comment,
  214. )
  215. )
  216. if server_default is not False:
  217. kw = {}
  218. cls_: Type[
  219. Union[
  220. base.ComputedColumnDefault,
  221. base.IdentityColumnDefault,
  222. base.ColumnDefault,
  223. ]
  224. ]
  225. if sqla_compat._server_default_is_computed(
  226. server_default, existing_server_default
  227. ):
  228. cls_ = base.ComputedColumnDefault
  229. elif sqla_compat._server_default_is_identity(
  230. server_default, existing_server_default
  231. ):
  232. cls_ = base.IdentityColumnDefault
  233. kw["impl"] = self
  234. else:
  235. cls_ = base.ColumnDefault
  236. self._exec(
  237. cls_(
  238. table_name,
  239. column_name,
  240. server_default, # type:ignore[arg-type]
  241. schema=schema,
  242. existing_type=existing_type,
  243. existing_server_default=existing_server_default,
  244. existing_nullable=existing_nullable,
  245. existing_comment=existing_comment,
  246. **kw,
  247. )
  248. )
  249. if type_ is not None:
  250. self._exec(
  251. base.ColumnType(
  252. table_name,
  253. column_name,
  254. type_,
  255. schema=schema,
  256. existing_type=existing_type,
  257. existing_server_default=existing_server_default,
  258. existing_nullable=existing_nullable,
  259. existing_comment=existing_comment,
  260. )
  261. )
  262. if comment is not False:
  263. self._exec(
  264. base.ColumnComment(
  265. table_name,
  266. column_name,
  267. comment,
  268. schema=schema,
  269. existing_type=existing_type,
  270. existing_server_default=existing_server_default,
  271. existing_nullable=existing_nullable,
  272. existing_comment=existing_comment,
  273. )
  274. )
  275. # do the new name last ;)
  276. if name is not None:
  277. self._exec(
  278. base.ColumnName(
  279. table_name,
  280. column_name,
  281. name,
  282. schema=schema,
  283. existing_type=existing_type,
  284. existing_server_default=existing_server_default,
  285. existing_nullable=existing_nullable,
  286. )
  287. )
  288. def add_column(
  289. self,
  290. table_name: str,
  291. column: Column[Any],
  292. schema: Optional[Union[str, quoted_name]] = None,
  293. ) -> None:
  294. self._exec(base.AddColumn(table_name, column, schema=schema))
  295. def drop_column(
  296. self,
  297. table_name: str,
  298. column: Column[Any],
  299. schema: Optional[str] = None,
  300. **kw,
  301. ) -> None:
  302. self._exec(base.DropColumn(table_name, column, schema=schema))
  303. def add_constraint(self, const: Any) -> None:
  304. if const._create_rule is None or const._create_rule(self):
  305. self._exec(schema.AddConstraint(const))
  306. def drop_constraint(self, const: Constraint) -> None:
  307. self._exec(schema.DropConstraint(const))
  308. def rename_table(
  309. self,
  310. old_table_name: str,
  311. new_table_name: Union[str, quoted_name],
  312. schema: Optional[Union[str, quoted_name]] = None,
  313. ) -> None:
  314. self._exec(
  315. base.RenameTable(old_table_name, new_table_name, schema=schema)
  316. )
  317. def create_table(self, table: Table) -> None:
  318. table.dispatch.before_create(
  319. table, self.connection, checkfirst=False, _ddl_runner=self
  320. )
  321. self._exec(schema.CreateTable(table))
  322. table.dispatch.after_create(
  323. table, self.connection, checkfirst=False, _ddl_runner=self
  324. )
  325. for index in table.indexes:
  326. self._exec(schema.CreateIndex(index))
  327. with_comment = (
  328. self.dialect.supports_comments and not self.dialect.inline_comments
  329. )
  330. comment = table.comment
  331. if comment and with_comment:
  332. self.create_table_comment(table)
  333. for column in table.columns:
  334. comment = column.comment
  335. if comment and with_comment:
  336. self.create_column_comment(column)
  337. def drop_table(self, table: Table) -> None:
  338. table.dispatch.before_drop(
  339. table, self.connection, checkfirst=False, _ddl_runner=self
  340. )
  341. self._exec(schema.DropTable(table))
  342. table.dispatch.after_drop(
  343. table, self.connection, checkfirst=False, _ddl_runner=self
  344. )
  345. def create_index(self, index: Index, **kw: Any) -> None:
  346. self._exec(schema.CreateIndex(index, **kw))
  347. def create_table_comment(self, table: Table) -> None:
  348. self._exec(schema.SetTableComment(table))
  349. def drop_table_comment(self, table: Table) -> None:
  350. self._exec(schema.DropTableComment(table))
  351. def create_column_comment(self, column: ColumnElement[Any]) -> None:
  352. self._exec(schema.SetColumnComment(column))
  353. def drop_index(self, index: Index, **kw: Any) -> None:
  354. self._exec(schema.DropIndex(index, **kw))
  355. def bulk_insert(
  356. self,
  357. table: Union[TableClause, Table],
  358. rows: List[dict],
  359. multiinsert: bool = True,
  360. ) -> None:
  361. if not isinstance(rows, list):
  362. raise TypeError("List expected")
  363. elif rows and not isinstance(rows[0], dict):
  364. raise TypeError("List of dictionaries expected")
  365. if self.as_sql:
  366. for row in rows:
  367. self._exec(
  368. sqla_compat._insert_inline(table).values(
  369. **{
  370. k: sqla_compat._literal_bindparam(
  371. k, v, type_=table.c[k].type
  372. )
  373. if not isinstance(
  374. v, sqla_compat._literal_bindparam
  375. )
  376. else v
  377. for k, v in row.items()
  378. }
  379. )
  380. )
  381. else:
  382. if rows:
  383. if multiinsert:
  384. self._exec(
  385. sqla_compat._insert_inline(table), multiparams=rows
  386. )
  387. else:
  388. for row in rows:
  389. self._exec(
  390. sqla_compat._insert_inline(table).values(**row)
  391. )
  392. def _tokenize_column_type(self, column: Column) -> Params:
  393. definition: str
  394. definition = self.dialect.type_compiler.process(column.type).lower()
  395. # tokenize the SQLAlchemy-generated version of a type, so that
  396. # the two can be compared.
  397. #
  398. # examples:
  399. # NUMERIC(10, 5)
  400. # TIMESTAMP WITH TIMEZONE
  401. # INTEGER UNSIGNED
  402. # INTEGER (10) UNSIGNED
  403. # INTEGER(10) UNSIGNED
  404. # varchar character set utf8
  405. #
  406. tokens: List[str] = re.findall(r"[\w\-_]+|\(.+?\)", definition)
  407. term_tokens: List[str] = []
  408. paren_term = None
  409. for token in tokens:
  410. if re.match(r"^\(.*\)$", token):
  411. paren_term = token
  412. else:
  413. term_tokens.append(token)
  414. params = Params(term_tokens[0], term_tokens[1:], [], {})
  415. if paren_term:
  416. term: str
  417. for term in re.findall("[^(),]+", paren_term):
  418. if "=" in term:
  419. key, val = term.split("=")
  420. params.kwargs[key.strip()] = val.strip()
  421. else:
  422. params.args.append(term.strip())
  423. return params
  424. def _column_types_match(
  425. self, inspector_params: Params, metadata_params: Params
  426. ) -> bool:
  427. if inspector_params.token0 == metadata_params.token0:
  428. return True
  429. synonyms = [{t.lower() for t in batch} for batch in self.type_synonyms]
  430. inspector_all_terms = " ".join(
  431. [inspector_params.token0] + inspector_params.tokens
  432. )
  433. metadata_all_terms = " ".join(
  434. [metadata_params.token0] + metadata_params.tokens
  435. )
  436. for batch in synonyms:
  437. if {inspector_all_terms, metadata_all_terms}.issubset(batch) or {
  438. inspector_params.token0,
  439. metadata_params.token0,
  440. }.issubset(batch):
  441. return True
  442. return False
  443. def _column_args_match(
  444. self, inspected_params: Params, meta_params: Params
  445. ) -> bool:
  446. """We want to compare column parameters. However, we only want
  447. to compare parameters that are set. If they both have `collation`,
  448. we want to make sure they are the same. However, if only one
  449. specifies it, dont flag it for being less specific
  450. """
  451. if (
  452. len(meta_params.tokens) == len(inspected_params.tokens)
  453. and meta_params.tokens != inspected_params.tokens
  454. ):
  455. return False
  456. if (
  457. len(meta_params.args) == len(inspected_params.args)
  458. and meta_params.args != inspected_params.args
  459. ):
  460. return False
  461. insp = " ".join(inspected_params.tokens).lower()
  462. meta = " ".join(meta_params.tokens).lower()
  463. for reg in self.type_arg_extract:
  464. mi = re.search(reg, insp)
  465. mm = re.search(reg, meta)
  466. if mi and mm and mi.group(1) != mm.group(1):
  467. return False
  468. return True
  469. def compare_type(
  470. self, inspector_column: Column[Any], metadata_column: Column
  471. ) -> bool:
  472. """Returns True if there ARE differences between the types of the two
  473. columns. Takes impl.type_synonyms into account between retrospected
  474. and metadata types
  475. """
  476. inspector_params = self._tokenize_column_type(inspector_column)
  477. metadata_params = self._tokenize_column_type(metadata_column)
  478. if not self._column_types_match(inspector_params, metadata_params):
  479. return True
  480. if not self._column_args_match(inspector_params, metadata_params):
  481. return True
  482. return False
  483. def compare_server_default(
  484. self,
  485. inspector_column,
  486. metadata_column,
  487. rendered_metadata_default,
  488. rendered_inspector_default,
  489. ):
  490. return rendered_inspector_default != rendered_metadata_default
  491. def correct_for_autogen_constraints(
  492. self,
  493. conn_uniques: Set[UniqueConstraint],
  494. conn_indexes: Set[Index],
  495. metadata_unique_constraints: Set[UniqueConstraint],
  496. metadata_indexes: Set[Index],
  497. ) -> None:
  498. pass
  499. def cast_for_batch_migrate(self, existing, existing_transfer, new_type):
  500. if existing.type._type_affinity is not new_type._type_affinity:
  501. existing_transfer["expr"] = cast(
  502. existing_transfer["expr"], new_type
  503. )
  504. def render_ddl_sql_expr(
  505. self, expr: ClauseElement, is_server_default: bool = False, **kw: Any
  506. ) -> str:
  507. """Render a SQL expression that is typically a server default,
  508. index expression, etc.
  509. """
  510. compile_kw = {"literal_binds": True, "include_table": False}
  511. return str(
  512. expr.compile(dialect=self.dialect, compile_kwargs=compile_kw)
  513. )
  514. def _compat_autogen_column_reflect(self, inspector: Inspector) -> Callable:
  515. return self.autogen_column_reflect
  516. def correct_for_autogen_foreignkeys(
  517. self,
  518. conn_fks: Set[ForeignKeyConstraint],
  519. metadata_fks: Set[ForeignKeyConstraint],
  520. ) -> None:
  521. pass
  522. def autogen_column_reflect(self, inspector, table, column_info):
  523. """A hook that is attached to the 'column_reflect' event for when
  524. a Table is reflected from the database during the autogenerate
  525. process.
  526. Dialects can elect to modify the information gathered here.
  527. """
  528. def start_migrations(self) -> None:
  529. """A hook called when :meth:`.EnvironmentContext.run_migrations`
  530. is called.
  531. Implementations can set up per-migration-run state here.
  532. """
  533. def emit_begin(self) -> None:
  534. """Emit the string ``BEGIN``, or the backend-specific
  535. equivalent, on the current connection context.
  536. This is used in offline mode and typically
  537. via :meth:`.EnvironmentContext.begin_transaction`.
  538. """
  539. self.static_output("BEGIN" + self.command_terminator)
  540. def emit_commit(self) -> None:
  541. """Emit the string ``COMMIT``, or the backend-specific
  542. equivalent, on the current connection context.
  543. This is used in offline mode and typically
  544. via :meth:`.EnvironmentContext.begin_transaction`.
  545. """
  546. self.static_output("COMMIT" + self.command_terminator)
  547. def render_type(
  548. self, type_obj: TypeEngine, autogen_context: AutogenContext
  549. ) -> Union[str, Literal[False]]:
  550. return False
  551. def _compare_identity_default(self, metadata_identity, inspector_identity):
  552. # ignored contains the attributes that were not considered
  553. # because assumed to their default values in the db.
  554. diff, ignored = _compare_identity_options(
  555. metadata_identity,
  556. inspector_identity,
  557. sqla_compat.Identity(),
  558. skip={"always"},
  559. )
  560. meta_always = getattr(metadata_identity, "always", None)
  561. inspector_always = getattr(inspector_identity, "always", None)
  562. # None and False are the same in this comparison
  563. if bool(meta_always) != bool(inspector_always):
  564. diff.add("always")
  565. diff.difference_update(self.identity_attrs_ignore)
  566. # returns 3 values:
  567. return (
  568. # different identity attributes
  569. diff,
  570. # ignored identity attributes
  571. ignored,
  572. # if the two identity should be considered different
  573. bool(diff) or bool(metadata_identity) != bool(inspector_identity),
  574. )
  575. def _compare_index_unique(
  576. self, metadata_index: Index, reflected_index: Index
  577. ) -> Optional[str]:
  578. conn_unique = bool(reflected_index.unique)
  579. meta_unique = bool(metadata_index.unique)
  580. if conn_unique != meta_unique:
  581. return f"unique={conn_unique} to unique={meta_unique}"
  582. else:
  583. return None
  584. def _create_metadata_constraint_sig(
  585. self, constraint: _autogen._C, **opts: Any
  586. ) -> _constraint_sig[_autogen._C]:
  587. return _constraint_sig.from_constraint(True, self, constraint, **opts)
  588. def _create_reflected_constraint_sig(
  589. self, constraint: _autogen._C, **opts: Any
  590. ) -> _constraint_sig[_autogen._C]:
  591. return _constraint_sig.from_constraint(False, self, constraint, **opts)
  592. def compare_indexes(
  593. self,
  594. metadata_index: Index,
  595. reflected_index: Index,
  596. ) -> ComparisonResult:
  597. """Compare two indexes by comparing the signature generated by
  598. ``create_index_sig``.
  599. This method returns a ``ComparisonResult``.
  600. """
  601. msg: List[str] = []
  602. unique_msg = self._compare_index_unique(
  603. metadata_index, reflected_index
  604. )
  605. if unique_msg:
  606. msg.append(unique_msg)
  607. m_sig = self._create_metadata_constraint_sig(metadata_index)
  608. r_sig = self._create_reflected_constraint_sig(reflected_index)
  609. assert _autogen.is_index_sig(m_sig)
  610. assert _autogen.is_index_sig(r_sig)
  611. # The assumption is that the index have no expression
  612. for sig in m_sig, r_sig:
  613. if sig.has_expressions:
  614. log.warning(
  615. "Generating approximate signature for index %s. "
  616. "The dialect "
  617. "implementation should either skip expression indexes "
  618. "or provide a custom implementation.",
  619. sig.const,
  620. )
  621. if m_sig.column_names != r_sig.column_names:
  622. msg.append(
  623. f"expression {r_sig.column_names} to {m_sig.column_names}"
  624. )
  625. if msg:
  626. return ComparisonResult.Different(msg)
  627. else:
  628. return ComparisonResult.Equal()
  629. def compare_unique_constraint(
  630. self,
  631. metadata_constraint: UniqueConstraint,
  632. reflected_constraint: UniqueConstraint,
  633. ) -> ComparisonResult:
  634. """Compare two unique constraints by comparing the two signatures.
  635. The arguments are two tuples that contain the unique constraint and
  636. the signatures generated by ``create_unique_constraint_sig``.
  637. This method returns a ``ComparisonResult``.
  638. """
  639. metadata_tup = self._create_metadata_constraint_sig(
  640. metadata_constraint
  641. )
  642. reflected_tup = self._create_reflected_constraint_sig(
  643. reflected_constraint
  644. )
  645. meta_sig = metadata_tup.unnamed
  646. conn_sig = reflected_tup.unnamed
  647. if conn_sig != meta_sig:
  648. return ComparisonResult.Different(
  649. f"expression {conn_sig} to {meta_sig}"
  650. )
  651. else:
  652. return ComparisonResult.Equal()
  653. def _skip_functional_indexes(self, metadata_indexes, conn_indexes):
  654. conn_indexes_by_name = {c.name: c for c in conn_indexes}
  655. for idx in list(metadata_indexes):
  656. if idx.name in conn_indexes_by_name:
  657. continue
  658. iex = sqla_compat.is_expression_index(idx)
  659. if iex:
  660. util.warn(
  661. "autogenerate skipping metadata-specified "
  662. "expression-based index "
  663. f"{idx.name!r}; dialect {self.__dialect__!r} under "
  664. f"SQLAlchemy {sqla_compat.sqlalchemy_version} can't "
  665. "reflect these indexes so they can't be compared"
  666. )
  667. metadata_indexes.discard(idx)
  668. def adjust_reflected_dialect_options(
  669. self, reflected_object: Dict[str, Any], kind: str
  670. ) -> Dict[str, Any]:
  671. return reflected_object.get("dialect_options", {})
  672. class Params(NamedTuple):
  673. token0: str
  674. tokens: List[str]
  675. args: List[str]
  676. kwargs: Dict[str, str]
  677. def _compare_identity_options(
  678. metadata_io: Union[schema.Identity, schema.Sequence, None],
  679. inspector_io: Union[schema.Identity, schema.Sequence, None],
  680. default_io: Union[schema.Identity, schema.Sequence],
  681. skip: Set[str],
  682. ):
  683. # this can be used for identity or sequence compare.
  684. # default_io is an instance of IdentityOption with all attributes to the
  685. # default value.
  686. meta_d = sqla_compat._get_identity_options_dict(metadata_io)
  687. insp_d = sqla_compat._get_identity_options_dict(inspector_io)
  688. diff = set()
  689. ignored_attr = set()
  690. def check_dicts(
  691. meta_dict: Mapping[str, Any],
  692. insp_dict: Mapping[str, Any],
  693. default_dict: Mapping[str, Any],
  694. attrs: Iterable[str],
  695. ):
  696. for attr in set(attrs).difference(skip):
  697. meta_value = meta_dict.get(attr)
  698. insp_value = insp_dict.get(attr)
  699. if insp_value != meta_value:
  700. default_value = default_dict.get(attr)
  701. if meta_value == default_value:
  702. ignored_attr.add(attr)
  703. else:
  704. diff.add(attr)
  705. check_dicts(
  706. meta_d,
  707. insp_d,
  708. sqla_compat._get_identity_options_dict(default_io),
  709. set(meta_d).union(insp_d),
  710. )
  711. if sqla_compat.identity_has_dialect_kwargs:
  712. # use only the dialect kwargs in inspector_io since metadata_io
  713. # can have options for many backends
  714. check_dicts(
  715. getattr(metadata_io, "dialect_kwargs", {}),
  716. getattr(inspector_io, "dialect_kwargs", {}),
  717. default_io.dialect_kwargs, # type: ignore[union-attr]
  718. getattr(inspector_io, "dialect_kwargs", {}),
  719. )
  720. return diff, ignored_attr