mysql.py 16 KB

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