mysql.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. from sqlalchemy.ext.compiler import compiles
  2. from sqlalchemy import types as sqltypes
  3. from sqlalchemy import schema
  4. from ..util.compat import string_types
  5. from .. import util
  6. from .impl import DefaultImpl
  7. from .base import ColumnNullable, ColumnName, ColumnDefault, \
  8. ColumnType, AlterColumn, format_column_name, \
  9. format_server_default
  10. from .base import alter_table
  11. from ..autogenerate import compare
  12. from ..util.sqla_compat import _is_type_bound, sqla_100
  13. class MySQLImpl(DefaultImpl):
  14. __dialect__ = 'mysql'
  15. transactional_ddl = False
  16. def alter_column(self, table_name, column_name,
  17. nullable=None,
  18. server_default=False,
  19. name=None,
  20. type_=None,
  21. schema=None,
  22. existing_type=None,
  23. existing_server_default=None,
  24. existing_nullable=None,
  25. autoincrement=None,
  26. existing_autoincrement=None,
  27. **kw
  28. ):
  29. if name is not None:
  30. self._exec(
  31. MySQLChangeColumn(
  32. table_name, column_name,
  33. schema=schema,
  34. newname=name,
  35. nullable=nullable if nullable is not None else
  36. existing_nullable
  37. if existing_nullable is not None
  38. else True,
  39. type_=type_ if type_ is not None else existing_type,
  40. default=server_default if server_default is not False
  41. else existing_server_default,
  42. autoincrement=autoincrement if autoincrement is not None
  43. else existing_autoincrement
  44. )
  45. )
  46. elif nullable is not None or \
  47. type_ is not None or \
  48. autoincrement is not None:
  49. self._exec(
  50. MySQLModifyColumn(
  51. table_name, column_name,
  52. schema=schema,
  53. newname=name if name is not None else column_name,
  54. nullable=nullable if nullable is not None else
  55. existing_nullable
  56. if existing_nullable is not None
  57. else True,
  58. type_=type_ if type_ is not None else existing_type,
  59. default=server_default if server_default is not False
  60. else existing_server_default,
  61. autoincrement=autoincrement if autoincrement is not None
  62. else existing_autoincrement
  63. )
  64. )
  65. elif server_default is not False:
  66. self._exec(
  67. MySQLAlterDefault(
  68. table_name, column_name, server_default,
  69. schema=schema,
  70. )
  71. )
  72. def drop_constraint(self, const):
  73. if isinstance(const, schema.CheckConstraint) and _is_type_bound(const):
  74. return
  75. super(MySQLImpl, self).drop_constraint(const)
  76. def compare_server_default(self, inspector_column,
  77. metadata_column,
  78. rendered_metadata_default,
  79. rendered_inspector_default):
  80. # partially a workaround for SQLAlchemy issue #3023; if the
  81. # column were created without "NOT NULL", MySQL may have added
  82. # an implicit default of '0' which we need to skip
  83. if metadata_column.type._type_affinity is sqltypes.Integer and \
  84. inspector_column.primary_key and \
  85. not inspector_column.autoincrement and \
  86. not rendered_metadata_default and \
  87. rendered_inspector_default == "'0'":
  88. return False
  89. else:
  90. return rendered_inspector_default != rendered_metadata_default
  91. def correct_for_autogen_constraints(self, conn_unique_constraints,
  92. conn_indexes,
  93. metadata_unique_constraints,
  94. metadata_indexes):
  95. # TODO: if SQLA 1.0, make use of "duplicates_index"
  96. # metadata
  97. removed = set()
  98. for idx in list(conn_indexes):
  99. if idx.unique:
  100. continue
  101. # MySQL puts implicit indexes on FK columns, even if
  102. # composite and even if MyISAM, so can't check this too easily.
  103. # the name of the index may be the column name or it may
  104. # be the name of the FK constraint.
  105. for col in idx.columns:
  106. if idx.name == col.name:
  107. conn_indexes.remove(idx)
  108. removed.add(idx.name)
  109. break
  110. for fk in col.foreign_keys:
  111. if fk.name == idx.name:
  112. conn_indexes.remove(idx)
  113. removed.add(idx.name)
  114. break
  115. if idx.name in removed:
  116. break
  117. # then remove indexes from the "metadata_indexes"
  118. # that we've removed from reflected, otherwise they come out
  119. # as adds (see #202)
  120. for idx in list(metadata_indexes):
  121. if idx.name in removed:
  122. metadata_indexes.remove(idx)
  123. if not sqla_100:
  124. self._legacy_correct_for_dupe_uq_uix(
  125. conn_unique_constraints,
  126. conn_indexes,
  127. metadata_unique_constraints,
  128. metadata_indexes
  129. )
  130. def _legacy_correct_for_dupe_uq_uix(self, conn_unique_constraints,
  131. conn_indexes,
  132. metadata_unique_constraints,
  133. metadata_indexes):
  134. # then dedupe unique indexes vs. constraints, since MySQL
  135. # doesn't really have unique constraints as a separate construct.
  136. # but look in the metadata and try to maintain constructs
  137. # that already seem to be defined one way or the other
  138. # on that side. See #276
  139. metadata_uq_names = set([
  140. cons.name for cons in metadata_unique_constraints
  141. if cons.name is not None])
  142. unnamed_metadata_uqs = set([
  143. compare._uq_constraint_sig(cons).sig
  144. for cons in metadata_unique_constraints
  145. if cons.name is None
  146. ])
  147. metadata_ix_names = set([
  148. cons.name for cons in metadata_indexes if cons.unique])
  149. conn_uq_names = dict(
  150. (cons.name, cons) for cons in conn_unique_constraints
  151. )
  152. conn_ix_names = dict(
  153. (cons.name, cons) for cons in conn_indexes if cons.unique
  154. )
  155. for overlap in set(conn_uq_names).intersection(conn_ix_names):
  156. if overlap not in metadata_uq_names:
  157. if compare._uq_constraint_sig(conn_uq_names[overlap]).sig \
  158. not in unnamed_metadata_uqs:
  159. conn_unique_constraints.discard(conn_uq_names[overlap])
  160. elif overlap not in metadata_ix_names:
  161. conn_indexes.discard(conn_ix_names[overlap])
  162. def correct_for_autogen_foreignkeys(self, conn_fks, metadata_fks):
  163. conn_fk_by_sig = dict(
  164. (compare._fk_constraint_sig(fk).sig, fk) for fk in conn_fks
  165. )
  166. metadata_fk_by_sig = dict(
  167. (compare._fk_constraint_sig(fk).sig, fk) for fk in metadata_fks
  168. )
  169. for sig in set(conn_fk_by_sig).intersection(metadata_fk_by_sig):
  170. mdfk = metadata_fk_by_sig[sig]
  171. cnfk = conn_fk_by_sig[sig]
  172. # MySQL considers RESTRICT to be the default and doesn't
  173. # report on it. if the model has explicit RESTRICT and
  174. # the conn FK has None, set it to RESTRICT
  175. if mdfk.ondelete is not None and \
  176. mdfk.ondelete.lower() == 'restrict' and \
  177. cnfk.ondelete is None:
  178. cnfk.ondelete = 'RESTRICT'
  179. if mdfk.onupdate is not None and \
  180. mdfk.onupdate.lower() == 'restrict' and \
  181. cnfk.onupdate is None:
  182. cnfk.onupdate = 'RESTRICT'
  183. class MySQLAlterDefault(AlterColumn):
  184. def __init__(self, name, column_name, default, schema=None):
  185. super(AlterColumn, self).__init__(name, schema=schema)
  186. self.column_name = column_name
  187. self.default = default
  188. class MySQLChangeColumn(AlterColumn):
  189. def __init__(self, name, column_name, schema=None,
  190. newname=None,
  191. type_=None,
  192. nullable=None,
  193. default=False,
  194. autoincrement=None):
  195. super(AlterColumn, self).__init__(name, schema=schema)
  196. self.column_name = column_name
  197. self.nullable = nullable
  198. self.newname = newname
  199. self.default = default
  200. self.autoincrement = autoincrement
  201. if type_ is None:
  202. raise util.CommandError(
  203. "All MySQL CHANGE/MODIFY COLUMN operations "
  204. "require the existing type."
  205. )
  206. self.type_ = sqltypes.to_instance(type_)
  207. class MySQLModifyColumn(MySQLChangeColumn):
  208. pass
  209. @compiles(ColumnNullable, 'mysql')
  210. @compiles(ColumnName, 'mysql')
  211. @compiles(ColumnDefault, 'mysql')
  212. @compiles(ColumnType, 'mysql')
  213. def _mysql_doesnt_support_individual(element, compiler, **kw):
  214. raise NotImplementedError(
  215. "Individual alter column constructs not supported by MySQL"
  216. )
  217. @compiles(MySQLAlterDefault, "mysql")
  218. def _mysql_alter_default(element, compiler, **kw):
  219. return "%s ALTER COLUMN %s %s" % (
  220. alter_table(compiler, element.table_name, element.schema),
  221. format_column_name(compiler, element.column_name),
  222. "SET DEFAULT %s" % format_server_default(compiler, element.default)
  223. if element.default is not None
  224. else "DROP DEFAULT"
  225. )
  226. @compiles(MySQLModifyColumn, "mysql")
  227. def _mysql_modify_column(element, compiler, **kw):
  228. return "%s MODIFY %s %s" % (
  229. alter_table(compiler, element.table_name, element.schema),
  230. format_column_name(compiler, element.column_name),
  231. _mysql_colspec(
  232. compiler,
  233. nullable=element.nullable,
  234. server_default=element.default,
  235. type_=element.type_,
  236. autoincrement=element.autoincrement
  237. ),
  238. )
  239. @compiles(MySQLChangeColumn, "mysql")
  240. def _mysql_change_column(element, compiler, **kw):
  241. return "%s CHANGE %s %s %s" % (
  242. alter_table(compiler, element.table_name, element.schema),
  243. format_column_name(compiler, element.column_name),
  244. format_column_name(compiler, element.newname),
  245. _mysql_colspec(
  246. compiler,
  247. nullable=element.nullable,
  248. server_default=element.default,
  249. type_=element.type_,
  250. autoincrement=element.autoincrement
  251. ),
  252. )
  253. def _render_value(compiler, expr):
  254. if isinstance(expr, string_types):
  255. return "'%s'" % expr
  256. else:
  257. return compiler.sql_compiler.process(expr)
  258. def _mysql_colspec(compiler, nullable, server_default, type_,
  259. autoincrement):
  260. spec = "%s %s" % (
  261. compiler.dialect.type_compiler.process(type_),
  262. "NULL" if nullable else "NOT NULL"
  263. )
  264. if autoincrement:
  265. spec += " AUTO_INCREMENT"
  266. if server_default is not False and server_default is not None:
  267. spec += " DEFAULT %s" % _render_value(compiler, server_default)
  268. return spec
  269. @compiles(schema.DropConstraint, "mysql")
  270. def _mysql_drop_constraint(element, compiler, **kw):
  271. """Redefine SQLAlchemy's drop constraint to
  272. raise errors for invalid constraint type."""
  273. constraint = element.element
  274. if isinstance(constraint, (schema.ForeignKeyConstraint,
  275. schema.PrimaryKeyConstraint,
  276. schema.UniqueConstraint)
  277. ):
  278. return compiler.visit_drop_constraint(element, **kw)
  279. elif isinstance(constraint, schema.CheckConstraint):
  280. raise NotImplementedError(
  281. "MySQL does not support CHECK constraints.")
  282. else:
  283. raise NotImplementedError(
  284. "No generic 'DROP CONSTRAINT' in MySQL - "
  285. "please specify constraint type")