base.py 16 KB

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