operations.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. import uuid
  2. from django.conf import settings
  3. from django.db.backends.base.operations import BaseDatabaseOperations
  4. from django.utils import timezone
  5. from django.utils.duration import duration_microseconds
  6. from django.utils.encoding import force_str
  7. class DatabaseOperations(BaseDatabaseOperations):
  8. compiler_module = "django.db.backends.mysql.compiler"
  9. # MySQL stores positive fields as UNSIGNED ints.
  10. integer_field_ranges = {
  11. **BaseDatabaseOperations.integer_field_ranges,
  12. 'PositiveSmallIntegerField': (0, 65535),
  13. 'PositiveIntegerField': (0, 4294967295),
  14. 'PositiveBigIntegerField': (0, 18446744073709551615),
  15. }
  16. cast_data_types = {
  17. 'AutoField': 'signed integer',
  18. 'BigAutoField': 'signed integer',
  19. 'SmallAutoField': 'signed integer',
  20. 'CharField': 'char(%(max_length)s)',
  21. 'DecimalField': 'decimal(%(max_digits)s, %(decimal_places)s)',
  22. 'TextField': 'char',
  23. 'IntegerField': 'signed integer',
  24. 'BigIntegerField': 'signed integer',
  25. 'SmallIntegerField': 'signed integer',
  26. 'PositiveBigIntegerField': 'unsigned integer',
  27. 'PositiveIntegerField': 'unsigned integer',
  28. 'PositiveSmallIntegerField': 'unsigned integer',
  29. }
  30. cast_char_field_without_max_length = 'char'
  31. explain_prefix = 'EXPLAIN'
  32. def date_extract_sql(self, lookup_type, field_name):
  33. # https://dev.mysql.com/doc/mysql/en/date-and-time-functions.html
  34. if lookup_type == 'week_day':
  35. # DAYOFWEEK() returns an integer, 1-7, Sunday=1.
  36. return "DAYOFWEEK(%s)" % field_name
  37. elif lookup_type == 'iso_week_day':
  38. # WEEKDAY() returns an integer, 0-6, Monday=0.
  39. return "WEEKDAY(%s) + 1" % field_name
  40. elif lookup_type == 'week':
  41. # Override the value of default_week_format for consistency with
  42. # other database backends.
  43. # Mode 3: Monday, 1-53, with 4 or more days this year.
  44. return "WEEK(%s, 3)" % field_name
  45. elif lookup_type == 'iso_year':
  46. # Get the year part from the YEARWEEK function, which returns a
  47. # number as year * 100 + week.
  48. return "TRUNCATE(YEARWEEK(%s, 3), -2) / 100" % field_name
  49. else:
  50. # EXTRACT returns 1-53 based on ISO-8601 for the week number.
  51. return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name)
  52. def date_trunc_sql(self, lookup_type, field_name):
  53. fields = {
  54. 'year': '%%Y-01-01',
  55. 'month': '%%Y-%%m-01',
  56. } # Use double percents to escape.
  57. if lookup_type in fields:
  58. format_str = fields[lookup_type]
  59. return "CAST(DATE_FORMAT(%s, '%s') AS DATE)" % (field_name, format_str)
  60. elif lookup_type == 'quarter':
  61. return "MAKEDATE(YEAR(%s), 1) + INTERVAL QUARTER(%s) QUARTER - INTERVAL 1 QUARTER" % (
  62. field_name, field_name
  63. )
  64. elif lookup_type == 'week':
  65. return "DATE_SUB(%s, INTERVAL WEEKDAY(%s) DAY)" % (
  66. field_name, field_name
  67. )
  68. else:
  69. return "DATE(%s)" % (field_name)
  70. def _prepare_tzname_delta(self, tzname):
  71. if '+' in tzname:
  72. return tzname[tzname.find('+'):]
  73. elif '-' in tzname:
  74. return tzname[tzname.find('-'):]
  75. return tzname
  76. def _convert_field_to_tz(self, field_name, tzname):
  77. if settings.USE_TZ and self.connection.timezone_name != tzname:
  78. field_name = "CONVERT_TZ(%s, '%s', '%s')" % (
  79. field_name,
  80. self.connection.timezone_name,
  81. self._prepare_tzname_delta(tzname),
  82. )
  83. return field_name
  84. def datetime_cast_date_sql(self, field_name, tzname):
  85. field_name = self._convert_field_to_tz(field_name, tzname)
  86. return "DATE(%s)" % field_name
  87. def datetime_cast_time_sql(self, field_name, tzname):
  88. field_name = self._convert_field_to_tz(field_name, tzname)
  89. return "TIME(%s)" % field_name
  90. def datetime_extract_sql(self, lookup_type, field_name, tzname):
  91. field_name = self._convert_field_to_tz(field_name, tzname)
  92. return self.date_extract_sql(lookup_type, field_name)
  93. def datetime_trunc_sql(self, lookup_type, field_name, tzname):
  94. field_name = self._convert_field_to_tz(field_name, tzname)
  95. fields = ['year', 'month', 'day', 'hour', 'minute', 'second']
  96. format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape.
  97. format_def = ('0000-', '01', '-01', ' 00:', '00', ':00')
  98. if lookup_type == 'quarter':
  99. return (
  100. "CAST(DATE_FORMAT(MAKEDATE(YEAR({field_name}), 1) + "
  101. "INTERVAL QUARTER({field_name}) QUARTER - " +
  102. "INTERVAL 1 QUARTER, '%%Y-%%m-01 00:00:00') AS DATETIME)"
  103. ).format(field_name=field_name)
  104. if lookup_type == 'week':
  105. return (
  106. "CAST(DATE_FORMAT(DATE_SUB({field_name}, "
  107. "INTERVAL WEEKDAY({field_name}) DAY), "
  108. "'%%Y-%%m-%%d 00:00:00') AS DATETIME)"
  109. ).format(field_name=field_name)
  110. try:
  111. i = fields.index(lookup_type) + 1
  112. except ValueError:
  113. sql = field_name
  114. else:
  115. format_str = ''.join(format[:i] + format_def[i:])
  116. sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str)
  117. return sql
  118. def time_trunc_sql(self, lookup_type, field_name):
  119. fields = {
  120. 'hour': '%%H:00:00',
  121. 'minute': '%%H:%%i:00',
  122. 'second': '%%H:%%i:%%s',
  123. } # Use double percents to escape.
  124. if lookup_type in fields:
  125. format_str = fields[lookup_type]
  126. return "CAST(DATE_FORMAT(%s, '%s') AS TIME)" % (field_name, format_str)
  127. else:
  128. return "TIME(%s)" % (field_name)
  129. def date_interval_sql(self, timedelta):
  130. return 'INTERVAL %s MICROSECOND' % duration_microseconds(timedelta)
  131. def fetch_returned_insert_rows(self, cursor):
  132. """
  133. Given a cursor object that has just performed an INSERT...RETURNING
  134. statement into a table, return the tuple of returned data.
  135. """
  136. return cursor.fetchall()
  137. def format_for_duration_arithmetic(self, sql):
  138. return 'INTERVAL %s MICROSECOND' % sql
  139. def force_no_ordering(self):
  140. """
  141. "ORDER BY NULL" prevents MySQL from implicitly ordering by grouped
  142. columns. If no ordering would otherwise be applied, we don't want any
  143. implicit sorting going on.
  144. """
  145. return [(None, ("NULL", [], False))]
  146. def last_executed_query(self, cursor, sql, params):
  147. # With MySQLdb, cursor objects have an (undocumented) "_executed"
  148. # attribute where the exact query sent to the database is saved.
  149. # See MySQLdb/cursors.py in the source distribution.
  150. # MySQLdb returns string, PyMySQL bytes.
  151. return force_str(getattr(cursor, '_executed', None), errors='replace')
  152. def no_limit_value(self):
  153. # 2**64 - 1, as recommended by the MySQL documentation
  154. return 18446744073709551615
  155. def quote_name(self, name):
  156. if name.startswith("`") and name.endswith("`"):
  157. return name # Quoting once is enough.
  158. return "`%s`" % name
  159. def random_function_sql(self):
  160. return 'RAND()'
  161. def return_insert_columns(self, fields):
  162. # MySQL and MariaDB < 10.5.0 don't support an INSERT...RETURNING
  163. # statement.
  164. if not fields:
  165. return '', ()
  166. columns = [
  167. '%s.%s' % (
  168. self.quote_name(field.model._meta.db_table),
  169. self.quote_name(field.column),
  170. ) for field in fields
  171. ]
  172. return 'RETURNING %s' % ', '.join(columns), ()
  173. def sql_flush(self, style, tables, sequences, allow_cascade=False):
  174. # NB: The generated SQL below is specific to MySQL
  175. # 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements
  176. # to clear all tables of all data
  177. if tables:
  178. sql = ['SET FOREIGN_KEY_CHECKS = 0;']
  179. for table in tables:
  180. sql.append('%s %s;' % (
  181. style.SQL_KEYWORD('TRUNCATE'),
  182. style.SQL_FIELD(self.quote_name(table)),
  183. ))
  184. sql.append('SET FOREIGN_KEY_CHECKS = 1;')
  185. sql.extend(self.sequence_reset_by_name_sql(style, sequences))
  186. return sql
  187. else:
  188. return []
  189. def validate_autopk_value(self, value):
  190. # MySQLism: zero in AUTO_INCREMENT field does not work. Refs #17653.
  191. if value == 0:
  192. raise ValueError('The database backend does not accept 0 as a '
  193. 'value for AutoField.')
  194. return value
  195. def adapt_datetimefield_value(self, value):
  196. if value is None:
  197. return None
  198. # Expression values are adapted by the database.
  199. if hasattr(value, 'resolve_expression'):
  200. return value
  201. # MySQL doesn't support tz-aware datetimes
  202. if timezone.is_aware(value):
  203. if settings.USE_TZ:
  204. value = timezone.make_naive(value, self.connection.timezone)
  205. else:
  206. raise ValueError("MySQL backend does not support timezone-aware datetimes when USE_TZ is False.")
  207. return str(value)
  208. def adapt_timefield_value(self, value):
  209. if value is None:
  210. return None
  211. # Expression values are adapted by the database.
  212. if hasattr(value, 'resolve_expression'):
  213. return value
  214. # MySQL doesn't support tz-aware times
  215. if timezone.is_aware(value):
  216. raise ValueError("MySQL backend does not support timezone-aware times.")
  217. return str(value)
  218. def max_name_length(self):
  219. return 64
  220. def bulk_insert_sql(self, fields, placeholder_rows):
  221. placeholder_rows_sql = (", ".join(row) for row in placeholder_rows)
  222. values_sql = ", ".join("(%s)" % sql for sql in placeholder_rows_sql)
  223. return "VALUES " + values_sql
  224. def combine_expression(self, connector, sub_expressions):
  225. if connector == '^':
  226. return 'POW(%s)' % ','.join(sub_expressions)
  227. # Convert the result to a signed integer since MySQL's binary operators
  228. # return an unsigned integer.
  229. elif connector in ('&', '|', '<<', '#'):
  230. connector = '^' if connector == '#' else connector
  231. return 'CONVERT(%s, SIGNED)' % connector.join(sub_expressions)
  232. elif connector == '>>':
  233. lhs, rhs = sub_expressions
  234. return 'FLOOR(%(lhs)s / POW(2, %(rhs)s))' % {'lhs': lhs, 'rhs': rhs}
  235. return super().combine_expression(connector, sub_expressions)
  236. def get_db_converters(self, expression):
  237. converters = super().get_db_converters(expression)
  238. internal_type = expression.output_field.get_internal_type()
  239. if internal_type in ['BooleanField', 'NullBooleanField']:
  240. converters.append(self.convert_booleanfield_value)
  241. elif internal_type == 'DateTimeField':
  242. if settings.USE_TZ:
  243. converters.append(self.convert_datetimefield_value)
  244. elif internal_type == 'UUIDField':
  245. converters.append(self.convert_uuidfield_value)
  246. return converters
  247. def convert_booleanfield_value(self, value, expression, connection):
  248. if value in (0, 1):
  249. value = bool(value)
  250. return value
  251. def convert_datetimefield_value(self, value, expression, connection):
  252. if value is not None:
  253. value = timezone.make_aware(value, self.connection.timezone)
  254. return value
  255. def convert_uuidfield_value(self, value, expression, connection):
  256. if value is not None:
  257. value = uuid.UUID(value)
  258. return value
  259. def binary_placeholder_sql(self, value):
  260. return '_binary %s' if value is not None and not hasattr(value, 'as_sql') else '%s'
  261. def subtract_temporals(self, internal_type, lhs, rhs):
  262. lhs_sql, lhs_params = lhs
  263. rhs_sql, rhs_params = rhs
  264. if internal_type == 'TimeField':
  265. if self.connection.mysql_is_mariadb:
  266. # MariaDB includes the microsecond component in TIME_TO_SEC as
  267. # a decimal. MySQL returns an integer without microseconds.
  268. return 'CAST((TIME_TO_SEC(%(lhs)s) - TIME_TO_SEC(%(rhs)s)) * 1000000 AS SIGNED)' % {
  269. 'lhs': lhs_sql, 'rhs': rhs_sql
  270. }, (*lhs_params, *rhs_params)
  271. return (
  272. "((TIME_TO_SEC(%(lhs)s) * 1000000 + MICROSECOND(%(lhs)s)) -"
  273. " (TIME_TO_SEC(%(rhs)s) * 1000000 + MICROSECOND(%(rhs)s)))"
  274. ) % {'lhs': lhs_sql, 'rhs': rhs_sql}, tuple(lhs_params) * 2 + tuple(rhs_params) * 2
  275. params = (*rhs_params, *lhs_params)
  276. return "TIMESTAMPDIFF(MICROSECOND, %s, %s)" % (rhs_sql, lhs_sql), params
  277. def explain_query_prefix(self, format=None, **options):
  278. # Alias MySQL's TRADITIONAL to TEXT for consistency with other backends.
  279. if format and format.upper() == 'TEXT':
  280. format = 'TRADITIONAL'
  281. elif not format and 'TREE' in self.connection.features.supported_explain_formats:
  282. # Use TREE by default (if supported) as it's more informative.
  283. format = 'TREE'
  284. analyze = options.pop('analyze', False)
  285. prefix = super().explain_query_prefix(format, **options)
  286. if analyze and self.connection.features.supports_explain_analyze:
  287. # MariaDB uses ANALYZE instead of EXPLAIN ANALYZE.
  288. prefix = 'ANALYZE' if self.connection.mysql_is_mariadb else prefix + ' ANALYZE'
  289. if format and not (analyze and not self.connection.mysql_is_mariadb):
  290. # Only MariaDB supports the analyze option with formats.
  291. prefix += ' FORMAT=%s' % format
  292. if self.connection.features.needs_explain_extended and not analyze and format is None:
  293. # ANALYZE, EXTENDED, and FORMAT are mutually exclusive options.
  294. prefix += ' EXTENDED'
  295. return prefix
  296. def regex_lookup(self, lookup_type):
  297. # REGEXP BINARY doesn't work correctly in MySQL 8+ and REGEXP_LIKE
  298. # doesn't exist in MySQL 5.6 or in MariaDB.
  299. if self.connection.mysql_version < (8, 0, 0) or self.connection.mysql_is_mariadb:
  300. if lookup_type == 'regex':
  301. return '%s REGEXP BINARY %s'
  302. return '%s REGEXP %s'
  303. match_option = 'c' if lookup_type == 'regex' else 'i'
  304. return "REGEXP_LIKE(%%s, %%s, '%s')" % match_option
  305. def insert_statement(self, ignore_conflicts=False):
  306. return 'INSERT IGNORE INTO' if ignore_conflicts else super().insert_statement(ignore_conflicts)