base.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. """
  2. MySQL database backend for Django.
  3. Requires mysqlclient: https://pypi.python.org/pypi/mysqlclient/
  4. """
  5. import re
  6. from django.core.exceptions import ImproperlyConfigured
  7. from django.db import utils
  8. from django.db.backends import utils as backend_utils
  9. from django.db.backends.base.base import BaseDatabaseWrapper
  10. from django.utils.functional import cached_property
  11. try:
  12. import MySQLdb as Database
  13. except ImportError as err:
  14. raise ImproperlyConfigured(
  15. 'Error loading MySQLdb module.\n'
  16. 'Did you install mysqlclient?'
  17. ) from err
  18. from MySQLdb.constants import CLIENT, FIELD_TYPE # isort:skip
  19. from MySQLdb.converters import conversions # isort:skip
  20. # Some of these import MySQLdb, so import them after checking if it's installed.
  21. from .client import DatabaseClient # isort:skip
  22. from .creation import DatabaseCreation # isort:skip
  23. from .features import DatabaseFeatures # isort:skip
  24. from .introspection import DatabaseIntrospection # isort:skip
  25. from .operations import DatabaseOperations # isort:skip
  26. from .schema import DatabaseSchemaEditor # isort:skip
  27. from .validation import DatabaseValidation # isort:skip
  28. version = Database.version_info
  29. if version < (1, 3, 3):
  30. raise ImproperlyConfigured("mysqlclient 1.3.3 or newer is required; you have %s" % Database.__version__)
  31. # MySQLdb returns TIME columns as timedelta -- they are more like timedelta in
  32. # terms of actual behavior as they are signed and include days -- and Django
  33. # expects time.
  34. django_conversions = conversions.copy()
  35. django_conversions.update({
  36. FIELD_TYPE.TIME: backend_utils.typecast_time,
  37. })
  38. # This should match the numerical portion of the version numbers (we can treat
  39. # versions like 5.0.24 and 5.0.24a as the same).
  40. server_version_re = re.compile(r'(\d{1,2})\.(\d{1,2})\.(\d{1,2})')
  41. class CursorWrapper:
  42. """
  43. A thin wrapper around MySQLdb's normal cursor class that catches particular
  44. exception instances and reraises them with the correct types.
  45. Implemented as a wrapper, rather than a subclass, so that it isn't stuck
  46. to the particular underlying representation returned by Connection.cursor().
  47. """
  48. codes_for_integrityerror = (
  49. 1048, # Column cannot be null
  50. 1690, # BIGINT UNSIGNED value is out of range
  51. )
  52. def __init__(self, cursor):
  53. self.cursor = cursor
  54. def execute(self, query, args=None):
  55. try:
  56. # args is None means no string interpolation
  57. return self.cursor.execute(query, args)
  58. except Database.OperationalError as e:
  59. # Map some error codes to IntegrityError, since they seem to be
  60. # misclassified and Django would prefer the more logical place.
  61. if e.args[0] in self.codes_for_integrityerror:
  62. raise utils.IntegrityError(*tuple(e.args))
  63. raise
  64. def executemany(self, query, args):
  65. try:
  66. return self.cursor.executemany(query, args)
  67. except Database.OperationalError as e:
  68. # Map some error codes to IntegrityError, since they seem to be
  69. # misclassified and Django would prefer the more logical place.
  70. if e.args[0] in self.codes_for_integrityerror:
  71. raise utils.IntegrityError(*tuple(e.args))
  72. raise
  73. def __getattr__(self, attr):
  74. return getattr(self.cursor, attr)
  75. def __iter__(self):
  76. return iter(self.cursor)
  77. class DatabaseWrapper(BaseDatabaseWrapper):
  78. vendor = 'mysql'
  79. display_name = 'MySQL'
  80. # This dictionary maps Field objects to their associated MySQL column
  81. # types, as strings. Column-type strings can contain format strings; they'll
  82. # be interpolated against the values of Field.__dict__ before being output.
  83. # If a column type is set to None, it won't be included in the output.
  84. data_types = {
  85. 'AutoField': 'integer AUTO_INCREMENT',
  86. 'BigAutoField': 'bigint AUTO_INCREMENT',
  87. 'BinaryField': 'longblob',
  88. 'BooleanField': 'bool',
  89. 'CharField': 'varchar(%(max_length)s)',
  90. 'DateField': 'date',
  91. 'DateTimeField': 'datetime(6)',
  92. 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)',
  93. 'DurationField': 'bigint',
  94. 'FileField': 'varchar(%(max_length)s)',
  95. 'FilePathField': 'varchar(%(max_length)s)',
  96. 'FloatField': 'double precision',
  97. 'IntegerField': 'integer',
  98. 'BigIntegerField': 'bigint',
  99. 'IPAddressField': 'char(15)',
  100. 'GenericIPAddressField': 'char(39)',
  101. 'NullBooleanField': 'bool',
  102. 'OneToOneField': 'integer',
  103. 'PositiveIntegerField': 'integer UNSIGNED',
  104. 'PositiveSmallIntegerField': 'smallint UNSIGNED',
  105. 'SlugField': 'varchar(%(max_length)s)',
  106. 'SmallIntegerField': 'smallint',
  107. 'TextField': 'longtext',
  108. 'TimeField': 'time(6)',
  109. 'UUIDField': 'char(32)',
  110. }
  111. # For these columns, MySQL doesn't:
  112. # - accept default values and implicitly treats these columns as nullable
  113. # - support a database index
  114. _limited_data_types = (
  115. 'tinyblob', 'blob', 'mediumblob', 'longblob', 'tinytext', 'text',
  116. 'mediumtext', 'longtext', 'json',
  117. )
  118. operators = {
  119. 'exact': '= %s',
  120. 'iexact': 'LIKE %s',
  121. 'contains': 'LIKE BINARY %s',
  122. 'icontains': 'LIKE %s',
  123. 'regex': 'REGEXP BINARY %s',
  124. 'iregex': 'REGEXP %s',
  125. 'gt': '> %s',
  126. 'gte': '>= %s',
  127. 'lt': '< %s',
  128. 'lte': '<= %s',
  129. 'startswith': 'LIKE BINARY %s',
  130. 'endswith': 'LIKE BINARY %s',
  131. 'istartswith': 'LIKE %s',
  132. 'iendswith': 'LIKE %s',
  133. }
  134. # The patterns below are used to generate SQL pattern lookup clauses when
  135. # the right-hand side of the lookup isn't a raw string (it might be an expression
  136. # or the result of a bilateral transformation).
  137. # In those cases, special characters for LIKE operators (e.g. \, *, _) should be
  138. # escaped on database side.
  139. #
  140. # Note: we use str.format() here for readability as '%' is used as a wildcard for
  141. # the LIKE operator.
  142. pattern_esc = r"REPLACE(REPLACE(REPLACE({}, '\\', '\\\\'), '%%', '\%%'), '_', '\_')"
  143. pattern_ops = {
  144. 'contains': "LIKE BINARY CONCAT('%%', {}, '%%')",
  145. 'icontains': "LIKE CONCAT('%%', {}, '%%')",
  146. 'startswith': "LIKE BINARY CONCAT({}, '%%')",
  147. 'istartswith': "LIKE CONCAT({}, '%%')",
  148. 'endswith': "LIKE BINARY CONCAT('%%', {})",
  149. 'iendswith': "LIKE CONCAT('%%', {})",
  150. }
  151. isolation_levels = {
  152. 'read uncommitted',
  153. 'read committed',
  154. 'repeatable read',
  155. 'serializable',
  156. }
  157. Database = Database
  158. SchemaEditorClass = DatabaseSchemaEditor
  159. # Classes instantiated in __init__().
  160. client_class = DatabaseClient
  161. creation_class = DatabaseCreation
  162. features_class = DatabaseFeatures
  163. introspection_class = DatabaseIntrospection
  164. ops_class = DatabaseOperations
  165. validation_class = DatabaseValidation
  166. def get_connection_params(self):
  167. kwargs = {
  168. 'conv': django_conversions,
  169. 'charset': 'utf8',
  170. }
  171. settings_dict = self.settings_dict
  172. if settings_dict['USER']:
  173. kwargs['user'] = settings_dict['USER']
  174. if settings_dict['NAME']:
  175. kwargs['db'] = settings_dict['NAME']
  176. if settings_dict['PASSWORD']:
  177. kwargs['passwd'] = settings_dict['PASSWORD']
  178. if settings_dict['HOST'].startswith('/'):
  179. kwargs['unix_socket'] = settings_dict['HOST']
  180. elif settings_dict['HOST']:
  181. kwargs['host'] = settings_dict['HOST']
  182. if settings_dict['PORT']:
  183. kwargs['port'] = int(settings_dict['PORT'])
  184. # We need the number of potentially affected rows after an
  185. # "UPDATE", not the number of changed rows.
  186. kwargs['client_flag'] = CLIENT.FOUND_ROWS
  187. # Validate the transaction isolation level, if specified.
  188. options = settings_dict['OPTIONS'].copy()
  189. isolation_level = options.pop('isolation_level', 'read committed')
  190. if isolation_level:
  191. isolation_level = isolation_level.lower()
  192. if isolation_level not in self.isolation_levels:
  193. raise ImproperlyConfigured(
  194. "Invalid transaction isolation level '%s' specified.\n"
  195. "Use one of %s, or None." % (
  196. isolation_level,
  197. ', '.join("'%s'" % s for s in sorted(self.isolation_levels))
  198. ))
  199. self.isolation_level = isolation_level
  200. kwargs.update(options)
  201. return kwargs
  202. def get_new_connection(self, conn_params):
  203. return Database.connect(**conn_params)
  204. def init_connection_state(self):
  205. assignments = []
  206. if self.features.is_sql_auto_is_null_enabled:
  207. # SQL_AUTO_IS_NULL controls whether an AUTO_INCREMENT column on
  208. # a recently inserted row will return when the field is tested
  209. # for NULL. Disabling this brings this aspect of MySQL in line
  210. # with SQL standards.
  211. assignments.append('SET SQL_AUTO_IS_NULL = 0')
  212. if self.isolation_level:
  213. assignments.append('SET SESSION TRANSACTION ISOLATION LEVEL %s' % self.isolation_level.upper())
  214. if assignments:
  215. with self.cursor() as cursor:
  216. cursor.execute('; '.join(assignments))
  217. def create_cursor(self, name=None):
  218. cursor = self.connection.cursor()
  219. return CursorWrapper(cursor)
  220. def _rollback(self):
  221. try:
  222. BaseDatabaseWrapper._rollback(self)
  223. except Database.NotSupportedError:
  224. pass
  225. def _set_autocommit(self, autocommit):
  226. with self.wrap_database_errors:
  227. self.connection.autocommit(autocommit)
  228. def disable_constraint_checking(self):
  229. """
  230. Disable foreign key checks, primarily for use in adding rows with
  231. forward references. Always return True to indicate constraint checks
  232. need to be re-enabled.
  233. """
  234. self.cursor().execute('SET foreign_key_checks=0')
  235. return True
  236. def enable_constraint_checking(self):
  237. """
  238. Re-enable foreign key checks after they have been disabled.
  239. """
  240. # Override needs_rollback in case constraint_checks_disabled is
  241. # nested inside transaction.atomic.
  242. self.needs_rollback, needs_rollback = False, self.needs_rollback
  243. try:
  244. self.cursor().execute('SET foreign_key_checks=1')
  245. finally:
  246. self.needs_rollback = needs_rollback
  247. def check_constraints(self, table_names=None):
  248. """
  249. Check each table name in `table_names` for rows with invalid foreign
  250. key references. This method is intended to be used in conjunction with
  251. `disable_constraint_checking()` and `enable_constraint_checking()`, to
  252. determine if rows with invalid references were entered while constraint
  253. checks were off.
  254. Raise an IntegrityError on the first invalid foreign key reference
  255. encountered (if any) and provide detailed information about the
  256. invalid reference in the error message.
  257. Backends can override this method if they can more directly apply
  258. constraint checking (e.g. via "SET CONSTRAINTS ALL IMMEDIATE")
  259. """
  260. with self.cursor() as cursor:
  261. if table_names is None:
  262. table_names = self.introspection.table_names(cursor)
  263. for table_name in table_names:
  264. primary_key_column_name = self.introspection.get_primary_key_column(cursor, table_name)
  265. if not primary_key_column_name:
  266. continue
  267. key_columns = self.introspection.get_key_columns(cursor, table_name)
  268. for column_name, referenced_table_name, referenced_column_name in key_columns:
  269. cursor.execute(
  270. """
  271. SELECT REFERRING.`%s`, REFERRING.`%s` FROM `%s` as REFERRING
  272. LEFT JOIN `%s` as REFERRED
  273. ON (REFERRING.`%s` = REFERRED.`%s`)
  274. WHERE REFERRING.`%s` IS NOT NULL AND REFERRED.`%s` IS NULL
  275. """ % (
  276. primary_key_column_name, column_name, table_name,
  277. referenced_table_name, column_name, referenced_column_name,
  278. column_name, referenced_column_name,
  279. )
  280. )
  281. for bad_row in cursor.fetchall():
  282. raise utils.IntegrityError(
  283. "The row in table '%s' with primary key '%s' has an invalid "
  284. "foreign key: %s.%s contains a value '%s' that does not "
  285. "have a corresponding value in %s.%s."
  286. % (
  287. table_name, bad_row[0], table_name, column_name,
  288. bad_row[1], referenced_table_name, referenced_column_name,
  289. )
  290. )
  291. def is_usable(self):
  292. try:
  293. self.connection.ping()
  294. except Database.Error:
  295. return False
  296. else:
  297. return True
  298. @cached_property
  299. def mysql_version(self):
  300. with self.temporary_connection() as cursor:
  301. cursor.execute('SELECT VERSION()')
  302. server_info = cursor.fetchone()[0]
  303. match = server_version_re.match(server_info)
  304. if not match:
  305. raise Exception('Unable to determine MySQL version from version string %r' % server_info)
  306. return tuple(int(x) for x in match.groups())