You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

base.py 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. """
  2. MySQL database backend for Django.
  3. Requires mysqlclient: https://pypi.org/project/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, 13):
  30. raise ImproperlyConfigured('mysqlclient 1.3.13 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 = {
  35. **conversions,
  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. 'gt': '> %s',
  124. 'gte': '>= %s',
  125. 'lt': '< %s',
  126. 'lte': '<= %s',
  127. 'startswith': 'LIKE BINARY %s',
  128. 'endswith': 'LIKE BINARY %s',
  129. 'istartswith': 'LIKE %s',
  130. 'iendswith': 'LIKE %s',
  131. }
  132. # The patterns below are used to generate SQL pattern lookup clauses when
  133. # the right-hand side of the lookup isn't a raw string (it might be an expression
  134. # or the result of a bilateral transformation).
  135. # In those cases, special characters for LIKE operators (e.g. \, *, _) should be
  136. # escaped on database side.
  137. #
  138. # Note: we use str.format() here for readability as '%' is used as a wildcard for
  139. # the LIKE operator.
  140. pattern_esc = r"REPLACE(REPLACE(REPLACE({}, '\\', '\\\\'), '%%', '\%%'), '_', '\_')"
  141. pattern_ops = {
  142. 'contains': "LIKE BINARY CONCAT('%%', {}, '%%')",
  143. 'icontains': "LIKE CONCAT('%%', {}, '%%')",
  144. 'startswith': "LIKE BINARY CONCAT({}, '%%')",
  145. 'istartswith': "LIKE CONCAT({}, '%%')",
  146. 'endswith': "LIKE BINARY CONCAT('%%', {})",
  147. 'iendswith': "LIKE CONCAT('%%', {})",
  148. }
  149. isolation_levels = {
  150. 'read uncommitted',
  151. 'read committed',
  152. 'repeatable read',
  153. 'serializable',
  154. }
  155. Database = Database
  156. SchemaEditorClass = DatabaseSchemaEditor
  157. # Classes instantiated in __init__().
  158. client_class = DatabaseClient
  159. creation_class = DatabaseCreation
  160. features_class = DatabaseFeatures
  161. introspection_class = DatabaseIntrospection
  162. ops_class = DatabaseOperations
  163. validation_class = DatabaseValidation
  164. def get_connection_params(self):
  165. kwargs = {
  166. 'conv': django_conversions,
  167. 'charset': 'utf8',
  168. }
  169. settings_dict = self.settings_dict
  170. if settings_dict['USER']:
  171. kwargs['user'] = settings_dict['USER']
  172. if settings_dict['NAME']:
  173. kwargs['db'] = settings_dict['NAME']
  174. if settings_dict['PASSWORD']:
  175. kwargs['passwd'] = settings_dict['PASSWORD']
  176. if settings_dict['HOST'].startswith('/'):
  177. kwargs['unix_socket'] = settings_dict['HOST']
  178. elif settings_dict['HOST']:
  179. kwargs['host'] = settings_dict['HOST']
  180. if settings_dict['PORT']:
  181. kwargs['port'] = int(settings_dict['PORT'])
  182. # We need the number of potentially affected rows after an
  183. # "UPDATE", not the number of changed rows.
  184. kwargs['client_flag'] = CLIENT.FOUND_ROWS
  185. # Validate the transaction isolation level, if specified.
  186. options = settings_dict['OPTIONS'].copy()
  187. isolation_level = options.pop('isolation_level', 'read committed')
  188. if isolation_level:
  189. isolation_level = isolation_level.lower()
  190. if isolation_level not in self.isolation_levels:
  191. raise ImproperlyConfigured(
  192. "Invalid transaction isolation level '%s' specified.\n"
  193. "Use one of %s, or None." % (
  194. isolation_level,
  195. ', '.join("'%s'" % s for s in sorted(self.isolation_levels))
  196. ))
  197. self.isolation_level = isolation_level
  198. kwargs.update(options)
  199. return kwargs
  200. def get_new_connection(self, conn_params):
  201. return Database.connect(**conn_params)
  202. def init_connection_state(self):
  203. assignments = []
  204. if self.features.is_sql_auto_is_null_enabled:
  205. # SQL_AUTO_IS_NULL controls whether an AUTO_INCREMENT column on
  206. # a recently inserted row will return when the field is tested
  207. # for NULL. Disabling this brings this aspect of MySQL in line
  208. # with SQL standards.
  209. assignments.append('SET SQL_AUTO_IS_NULL = 0')
  210. if self.isolation_level:
  211. assignments.append('SET SESSION TRANSACTION ISOLATION LEVEL %s' % self.isolation_level.upper())
  212. if assignments:
  213. with self.cursor() as cursor:
  214. cursor.execute('; '.join(assignments))
  215. def create_cursor(self, name=None):
  216. cursor = self.connection.cursor()
  217. return CursorWrapper(cursor)
  218. def _rollback(self):
  219. try:
  220. BaseDatabaseWrapper._rollback(self)
  221. except Database.NotSupportedError:
  222. pass
  223. def _set_autocommit(self, autocommit):
  224. with self.wrap_database_errors:
  225. self.connection.autocommit(autocommit)
  226. def disable_constraint_checking(self):
  227. """
  228. Disable foreign key checks, primarily for use in adding rows with
  229. forward references. Always return True to indicate constraint checks
  230. need to be re-enabled.
  231. """
  232. self.cursor().execute('SET foreign_key_checks=0')
  233. return True
  234. def enable_constraint_checking(self):
  235. """
  236. Re-enable foreign key checks after they have been disabled.
  237. """
  238. # Override needs_rollback in case constraint_checks_disabled is
  239. # nested inside transaction.atomic.
  240. self.needs_rollback, needs_rollback = False, self.needs_rollback
  241. try:
  242. self.cursor().execute('SET foreign_key_checks=1')
  243. finally:
  244. self.needs_rollback = needs_rollback
  245. def check_constraints(self, table_names=None):
  246. """
  247. Check each table name in `table_names` for rows with invalid foreign
  248. key references. This method is intended to be used in conjunction with
  249. `disable_constraint_checking()` and `enable_constraint_checking()`, to
  250. determine if rows with invalid references were entered while constraint
  251. checks were off.
  252. """
  253. with self.cursor() as cursor:
  254. if table_names is None:
  255. table_names = self.introspection.table_names(cursor)
  256. for table_name in table_names:
  257. primary_key_column_name = self.introspection.get_primary_key_column(cursor, table_name)
  258. if not primary_key_column_name:
  259. continue
  260. key_columns = self.introspection.get_key_columns(cursor, table_name)
  261. for column_name, referenced_table_name, referenced_column_name in key_columns:
  262. cursor.execute(
  263. """
  264. SELECT REFERRING.`%s`, REFERRING.`%s` FROM `%s` as REFERRING
  265. LEFT JOIN `%s` as REFERRED
  266. ON (REFERRING.`%s` = REFERRED.`%s`)
  267. WHERE REFERRING.`%s` IS NOT NULL AND REFERRED.`%s` IS NULL
  268. """ % (
  269. primary_key_column_name, column_name, table_name,
  270. referenced_table_name, column_name, referenced_column_name,
  271. column_name, referenced_column_name,
  272. )
  273. )
  274. for bad_row in cursor.fetchall():
  275. raise utils.IntegrityError(
  276. "The row in table '%s' with primary key '%s' has an invalid "
  277. "foreign key: %s.%s contains a value '%s' that does not "
  278. "have a corresponding value in %s.%s."
  279. % (
  280. table_name, bad_row[0], table_name, column_name,
  281. bad_row[1], referenced_table_name, referenced_column_name,
  282. )
  283. )
  284. def is_usable(self):
  285. try:
  286. self.connection.ping()
  287. except Database.Error:
  288. return False
  289. else:
  290. return True
  291. @cached_property
  292. def mysql_server_info(self):
  293. with self.temporary_connection() as cursor:
  294. cursor.execute('SELECT VERSION()')
  295. return cursor.fetchone()[0]
  296. @cached_property
  297. def mysql_version(self):
  298. match = server_version_re.match(self.mysql_server_info)
  299. if not match:
  300. raise Exception('Unable to determine MySQL version from version string %r' % self.mysql_server_info)
  301. return tuple(int(x) for x in match.groups())
  302. @cached_property
  303. def mysql_is_mariadb(self):
  304. # MariaDB isn't officially supported.
  305. return 'mariadb' in self.mysql_server_info.lower()