mysql.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  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 re
  5. from typing import Any
  6. from typing import Optional
  7. from typing import TYPE_CHECKING
  8. from typing import Union
  9. from sqlalchemy import schema
  10. from sqlalchemy import types as sqltypes
  11. from sqlalchemy.sql import elements
  12. from sqlalchemy.sql import functions
  13. from sqlalchemy.sql import operators
  14. from .base import alter_table
  15. from .base import AlterColumn
  16. from .base import ColumnDefault
  17. from .base import ColumnName
  18. from .base import ColumnNullable
  19. from .base import ColumnType
  20. from .base import format_column_name
  21. from .base import format_server_default
  22. from .impl import DefaultImpl
  23. from .. import util
  24. from ..util import sqla_compat
  25. from ..util.sqla_compat import _is_type_bound
  26. from ..util.sqla_compat import compiles
  27. if TYPE_CHECKING:
  28. from typing import Literal
  29. from sqlalchemy.dialects.mysql.base import MySQLDDLCompiler
  30. from sqlalchemy.sql.ddl import DropConstraint
  31. from sqlalchemy.sql.elements import ClauseElement
  32. from sqlalchemy.sql.schema import Constraint
  33. from sqlalchemy.sql.type_api import TypeEngine
  34. from .base import _ServerDefault
  35. class MySQLImpl(DefaultImpl):
  36. __dialect__ = "mysql"
  37. transactional_ddl = False
  38. type_synonyms = DefaultImpl.type_synonyms + (
  39. {"BOOL", "TINYINT"},
  40. {"JSON", "LONGTEXT"},
  41. )
  42. type_arg_extract = [r"character set ([\w\-_]+)", r"collate ([\w\-_]+)"]
  43. def render_ddl_sql_expr(
  44. self,
  45. expr: ClauseElement,
  46. is_server_default: bool = False,
  47. is_index: bool = False,
  48. **kw: Any,
  49. ) -> str:
  50. # apply Grouping to index expressions;
  51. # see https://github.com/sqlalchemy/sqlalchemy/blob/
  52. # 36da2eaf3e23269f2cf28420ae73674beafd0661/
  53. # lib/sqlalchemy/dialects/mysql/base.py#L2191
  54. if is_index and (
  55. isinstance(expr, elements.BinaryExpression)
  56. or (
  57. isinstance(expr, elements.UnaryExpression)
  58. and expr.modifier not in (operators.desc_op, operators.asc_op)
  59. )
  60. or isinstance(expr, functions.FunctionElement)
  61. ):
  62. expr = elements.Grouping(expr)
  63. return super().render_ddl_sql_expr(
  64. expr, is_server_default=is_server_default, is_index=is_index, **kw
  65. )
  66. def alter_column(
  67. self,
  68. table_name: str,
  69. column_name: str,
  70. *,
  71. nullable: Optional[bool] = None,
  72. server_default: Optional[
  73. Union[_ServerDefault, Literal[False]]
  74. ] = False,
  75. name: Optional[str] = None,
  76. type_: Optional[TypeEngine] = None,
  77. schema: Optional[str] = None,
  78. existing_type: Optional[TypeEngine] = None,
  79. existing_server_default: Optional[_ServerDefault] = None,
  80. existing_nullable: Optional[bool] = None,
  81. autoincrement: Optional[bool] = None,
  82. existing_autoincrement: Optional[bool] = None,
  83. comment: Optional[Union[str, Literal[False]]] = False,
  84. existing_comment: Optional[str] = None,
  85. **kw: Any,
  86. ) -> None:
  87. if sqla_compat._server_default_is_identity(
  88. server_default, existing_server_default
  89. ) or sqla_compat._server_default_is_computed(
  90. server_default, existing_server_default
  91. ):
  92. # modifying computed or identity columns is not supported
  93. # the default will raise
  94. super().alter_column(
  95. table_name,
  96. column_name,
  97. nullable=nullable,
  98. type_=type_,
  99. schema=schema,
  100. existing_type=existing_type,
  101. existing_nullable=existing_nullable,
  102. server_default=server_default,
  103. existing_server_default=existing_server_default,
  104. **kw,
  105. )
  106. if name is not None or self._is_mysql_allowed_functional_default(
  107. type_ if type_ is not None else existing_type, server_default
  108. ):
  109. self._exec(
  110. MySQLChangeColumn(
  111. table_name,
  112. column_name,
  113. schema=schema,
  114. newname=name if name is not None else column_name,
  115. nullable=(
  116. nullable
  117. if nullable is not None
  118. else (
  119. existing_nullable
  120. if existing_nullable is not None
  121. else True
  122. )
  123. ),
  124. type_=type_ if type_ is not None else existing_type,
  125. default=(
  126. server_default
  127. if server_default is not False
  128. else existing_server_default
  129. ),
  130. autoincrement=(
  131. autoincrement
  132. if autoincrement is not None
  133. else existing_autoincrement
  134. ),
  135. comment=(
  136. comment if comment is not False else existing_comment
  137. ),
  138. )
  139. )
  140. elif (
  141. nullable is not None
  142. or type_ is not None
  143. or autoincrement is not None
  144. or comment is not False
  145. ):
  146. self._exec(
  147. MySQLModifyColumn(
  148. table_name,
  149. column_name,
  150. schema=schema,
  151. newname=name if name is not None else column_name,
  152. nullable=(
  153. nullable
  154. if nullable is not None
  155. else (
  156. existing_nullable
  157. if existing_nullable is not None
  158. else True
  159. )
  160. ),
  161. type_=type_ if type_ is not None else existing_type,
  162. default=(
  163. server_default
  164. if server_default is not False
  165. else existing_server_default
  166. ),
  167. autoincrement=(
  168. autoincrement
  169. if autoincrement is not None
  170. else existing_autoincrement
  171. ),
  172. comment=(
  173. comment if comment is not False else existing_comment
  174. ),
  175. )
  176. )
  177. elif server_default is not False:
  178. self._exec(
  179. MySQLAlterDefault(
  180. table_name, column_name, server_default, schema=schema
  181. )
  182. )
  183. def drop_constraint(
  184. self,
  185. const: Constraint,
  186. **kw: Any,
  187. ) -> None:
  188. if isinstance(const, schema.CheckConstraint) and _is_type_bound(const):
  189. return
  190. super().drop_constraint(const)
  191. def _is_mysql_allowed_functional_default(
  192. self,
  193. type_: Optional[TypeEngine],
  194. server_default: Optional[Union[_ServerDefault, Literal[False]]],
  195. ) -> bool:
  196. return (
  197. type_ is not None
  198. and type_._type_affinity is sqltypes.DateTime
  199. and server_default is not None
  200. )
  201. def compare_server_default(
  202. self,
  203. inspector_column,
  204. metadata_column,
  205. rendered_metadata_default,
  206. rendered_inspector_default,
  207. ):
  208. # partially a workaround for SQLAlchemy issue #3023; if the
  209. # column were created without "NOT NULL", MySQL may have added
  210. # an implicit default of '0' which we need to skip
  211. # TODO: this is not really covered anymore ?
  212. if (
  213. metadata_column.type._type_affinity is sqltypes.Integer
  214. and inspector_column.primary_key
  215. and not inspector_column.autoincrement
  216. and not rendered_metadata_default
  217. and rendered_inspector_default == "'0'"
  218. ):
  219. return False
  220. elif (
  221. rendered_inspector_default
  222. and inspector_column.type._type_affinity is sqltypes.Integer
  223. ):
  224. rendered_inspector_default = (
  225. re.sub(r"^'|'$", "", rendered_inspector_default)
  226. if rendered_inspector_default is not None
  227. else None
  228. )
  229. return rendered_inspector_default != rendered_metadata_default
  230. elif (
  231. rendered_metadata_default
  232. and metadata_column.type._type_affinity is sqltypes.String
  233. ):
  234. metadata_default = re.sub(r"^'|'$", "", rendered_metadata_default)
  235. return rendered_inspector_default != f"'{metadata_default}'"
  236. elif rendered_inspector_default and rendered_metadata_default:
  237. # adjust for "function()" vs. "FUNCTION" as can occur particularly
  238. # for the CURRENT_TIMESTAMP function on newer MariaDB versions
  239. # SQLAlchemy MySQL dialect bundles ON UPDATE into the server
  240. # default; adjust for this possibly being present.
  241. onupdate_ins = re.match(
  242. r"(.*) (on update.*?)(?:\(\))?$",
  243. rendered_inspector_default.lower(),
  244. )
  245. onupdate_met = re.match(
  246. r"(.*) (on update.*?)(?:\(\))?$",
  247. rendered_metadata_default.lower(),
  248. )
  249. if onupdate_ins:
  250. if not onupdate_met:
  251. return True
  252. elif onupdate_ins.group(2) != onupdate_met.group(2):
  253. return True
  254. rendered_inspector_default = onupdate_ins.group(1)
  255. rendered_metadata_default = onupdate_met.group(1)
  256. return re.sub(
  257. r"(.*?)(?:\(\))?$", r"\1", rendered_inspector_default.lower()
  258. ) != re.sub(
  259. r"(.*?)(?:\(\))?$", r"\1", rendered_metadata_default.lower()
  260. )
  261. else:
  262. return rendered_inspector_default != rendered_metadata_default
  263. def correct_for_autogen_constraints(
  264. self,
  265. conn_unique_constraints,
  266. conn_indexes,
  267. metadata_unique_constraints,
  268. metadata_indexes,
  269. ):
  270. # TODO: if SQLA 1.0, make use of "duplicates_index"
  271. # metadata
  272. removed = set()
  273. for idx in list(conn_indexes):
  274. if idx.unique:
  275. continue
  276. # MySQL puts implicit indexes on FK columns, even if
  277. # composite and even if MyISAM, so can't check this too easily.
  278. # the name of the index may be the column name or it may
  279. # be the name of the FK constraint.
  280. for col in idx.columns:
  281. if idx.name == col.name:
  282. conn_indexes.remove(idx)
  283. removed.add(idx.name)
  284. break
  285. for fk in col.foreign_keys:
  286. if fk.name == idx.name:
  287. conn_indexes.remove(idx)
  288. removed.add(idx.name)
  289. break
  290. if idx.name in removed:
  291. break
  292. # then remove indexes from the "metadata_indexes"
  293. # that we've removed from reflected, otherwise they come out
  294. # as adds (see #202)
  295. for idx in list(metadata_indexes):
  296. if idx.name in removed:
  297. metadata_indexes.remove(idx)
  298. def correct_for_autogen_foreignkeys(self, conn_fks, metadata_fks):
  299. conn_fk_by_sig = {
  300. self._create_reflected_constraint_sig(fk).unnamed_no_options: fk
  301. for fk in conn_fks
  302. }
  303. metadata_fk_by_sig = {
  304. self._create_metadata_constraint_sig(fk).unnamed_no_options: fk
  305. for fk in metadata_fks
  306. }
  307. for sig in set(conn_fk_by_sig).intersection(metadata_fk_by_sig):
  308. mdfk = metadata_fk_by_sig[sig]
  309. cnfk = conn_fk_by_sig[sig]
  310. # MySQL considers RESTRICT to be the default and doesn't
  311. # report on it. if the model has explicit RESTRICT and
  312. # the conn FK has None, set it to RESTRICT
  313. if (
  314. mdfk.ondelete is not None
  315. and mdfk.ondelete.lower() == "restrict"
  316. and cnfk.ondelete is None
  317. ):
  318. cnfk.ondelete = "RESTRICT"
  319. if (
  320. mdfk.onupdate is not None
  321. and mdfk.onupdate.lower() == "restrict"
  322. and cnfk.onupdate is None
  323. ):
  324. cnfk.onupdate = "RESTRICT"
  325. class MariaDBImpl(MySQLImpl):
  326. __dialect__ = "mariadb"
  327. class MySQLAlterDefault(AlterColumn):
  328. def __init__(
  329. self,
  330. name: str,
  331. column_name: str,
  332. default: Optional[_ServerDefault],
  333. schema: Optional[str] = None,
  334. ) -> None:
  335. super(AlterColumn, self).__init__(name, schema=schema)
  336. self.column_name = column_name
  337. self.default = default
  338. class MySQLChangeColumn(AlterColumn):
  339. def __init__(
  340. self,
  341. name: str,
  342. column_name: str,
  343. schema: Optional[str] = None,
  344. newname: Optional[str] = None,
  345. type_: Optional[TypeEngine] = None,
  346. nullable: Optional[bool] = None,
  347. default: Optional[Union[_ServerDefault, Literal[False]]] = False,
  348. autoincrement: Optional[bool] = None,
  349. comment: Optional[Union[str, Literal[False]]] = False,
  350. ) -> None:
  351. super(AlterColumn, self).__init__(name, schema=schema)
  352. self.column_name = column_name
  353. self.nullable = nullable
  354. self.newname = newname
  355. self.default = default
  356. self.autoincrement = autoincrement
  357. self.comment = comment
  358. if type_ is None:
  359. raise util.CommandError(
  360. "All MySQL CHANGE/MODIFY COLUMN operations "
  361. "require the existing type."
  362. )
  363. self.type_ = sqltypes.to_instance(type_)
  364. class MySQLModifyColumn(MySQLChangeColumn):
  365. pass
  366. @compiles(ColumnNullable, "mysql", "mariadb")
  367. @compiles(ColumnName, "mysql", "mariadb")
  368. @compiles(ColumnDefault, "mysql", "mariadb")
  369. @compiles(ColumnType, "mysql", "mariadb")
  370. def _mysql_doesnt_support_individual(element, compiler, **kw):
  371. raise NotImplementedError(
  372. "Individual alter column constructs not supported by MySQL"
  373. )
  374. @compiles(MySQLAlterDefault, "mysql", "mariadb")
  375. def _mysql_alter_default(
  376. element: MySQLAlterDefault, compiler: MySQLDDLCompiler, **kw
  377. ) -> str:
  378. return "%s ALTER COLUMN %s %s" % (
  379. alter_table(compiler, element.table_name, element.schema),
  380. format_column_name(compiler, element.column_name),
  381. (
  382. "SET DEFAULT %s" % format_server_default(compiler, element.default)
  383. if element.default is not None
  384. else "DROP DEFAULT"
  385. ),
  386. )
  387. @compiles(MySQLModifyColumn, "mysql", "mariadb")
  388. def _mysql_modify_column(
  389. element: MySQLModifyColumn, compiler: MySQLDDLCompiler, **kw
  390. ) -> str:
  391. return "%s MODIFY %s %s" % (
  392. alter_table(compiler, element.table_name, element.schema),
  393. format_column_name(compiler, element.column_name),
  394. _mysql_colspec(
  395. compiler,
  396. nullable=element.nullable,
  397. server_default=element.default,
  398. type_=element.type_,
  399. autoincrement=element.autoincrement,
  400. comment=element.comment,
  401. ),
  402. )
  403. @compiles(MySQLChangeColumn, "mysql", "mariadb")
  404. def _mysql_change_column(
  405. element: MySQLChangeColumn, compiler: MySQLDDLCompiler, **kw
  406. ) -> str:
  407. return "%s CHANGE %s %s %s" % (
  408. alter_table(compiler, element.table_name, element.schema),
  409. format_column_name(compiler, element.column_name),
  410. format_column_name(compiler, element.newname),
  411. _mysql_colspec(
  412. compiler,
  413. nullable=element.nullable,
  414. server_default=element.default,
  415. type_=element.type_,
  416. autoincrement=element.autoincrement,
  417. comment=element.comment,
  418. ),
  419. )
  420. def _mysql_colspec(
  421. compiler: MySQLDDLCompiler,
  422. nullable: Optional[bool],
  423. server_default: Optional[Union[_ServerDefault, Literal[False]]],
  424. type_: TypeEngine,
  425. autoincrement: Optional[bool],
  426. comment: Optional[Union[str, Literal[False]]],
  427. ) -> str:
  428. spec = "%s %s" % (
  429. compiler.dialect.type_compiler.process(type_),
  430. "NULL" if nullable else "NOT NULL",
  431. )
  432. if autoincrement:
  433. spec += " AUTO_INCREMENT"
  434. if server_default is not False and server_default is not None:
  435. spec += " DEFAULT %s" % format_server_default(compiler, server_default)
  436. if comment:
  437. spec += " COMMENT %s" % compiler.sql_compiler.render_literal_value(
  438. comment, sqltypes.String()
  439. )
  440. return spec
  441. @compiles(schema.DropConstraint, "mysql", "mariadb")
  442. def _mysql_drop_constraint(
  443. element: DropConstraint, compiler: MySQLDDLCompiler, **kw
  444. ) -> str:
  445. """Redefine SQLAlchemy's drop constraint to
  446. raise errors for invalid constraint type."""
  447. constraint = element.element
  448. if isinstance(
  449. constraint,
  450. (
  451. schema.ForeignKeyConstraint,
  452. schema.PrimaryKeyConstraint,
  453. schema.UniqueConstraint,
  454. ),
  455. ):
  456. assert not kw
  457. return compiler.visit_drop_constraint(element)
  458. elif isinstance(constraint, schema.CheckConstraint):
  459. # note that SQLAlchemy as of 1.2 does not yet support
  460. # DROP CONSTRAINT for MySQL/MariaDB, so we implement fully
  461. # here.
  462. if compiler.dialect.is_mariadb:
  463. return "ALTER TABLE %s DROP CONSTRAINT %s" % (
  464. compiler.preparer.format_table(constraint.table),
  465. compiler.preparer.format_constraint(constraint),
  466. )
  467. else:
  468. return "ALTER TABLE %s DROP CHECK %s" % (
  469. compiler.preparer.format_table(constraint.table),
  470. compiler.preparer.format_constraint(constraint),
  471. )
  472. else:
  473. raise NotImplementedError(
  474. "No generic 'DROP CONSTRAINT' in MySQL - "
  475. "please specify constraint type"
  476. )