Development of an internal social media platform with personalised dashboards for students
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.

operations.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  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_text
  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. }
  15. cast_data_types = {
  16. 'CharField': 'char(%(max_length)s)',
  17. 'TextField': 'char',
  18. 'IntegerField': 'signed integer',
  19. 'BigIntegerField': 'signed integer',
  20. 'SmallIntegerField': 'signed integer',
  21. 'PositiveIntegerField': 'unsigned integer',
  22. 'PositiveSmallIntegerField': 'unsigned integer',
  23. }
  24. cast_char_field_without_max_length = 'char'
  25. explain_prefix = 'EXPLAIN'
  26. def date_extract_sql(self, lookup_type, field_name):
  27. # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html
  28. if lookup_type == 'week_day':
  29. # DAYOFWEEK() returns an integer, 1-7, Sunday=1.
  30. # Note: WEEKDAY() returns 0-6, Monday=0.
  31. return "DAYOFWEEK(%s)" % field_name
  32. elif lookup_type == 'week':
  33. # Override the value of default_week_format for consistency with
  34. # other database backends.
  35. # Mode 3: Monday, 1-53, with 4 or more days this year.
  36. return "WEEK(%s, 3)" % field_name
  37. else:
  38. # EXTRACT returns 1-53 based on ISO-8601 for the week number.
  39. return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name)
  40. def date_trunc_sql(self, lookup_type, field_name):
  41. fields = {
  42. 'year': '%%Y-01-01',
  43. 'month': '%%Y-%%m-01',
  44. } # Use double percents to escape.
  45. if lookup_type in fields:
  46. format_str = fields[lookup_type]
  47. return "CAST(DATE_FORMAT(%s, '%s') AS DATE)" % (field_name, format_str)
  48. elif lookup_type == 'quarter':
  49. return "MAKEDATE(YEAR(%s), 1) + INTERVAL QUARTER(%s) QUARTER - INTERVAL 1 QUARTER" % (
  50. field_name, field_name
  51. )
  52. elif lookup_type == 'week':
  53. return "DATE_SUB(%s, INTERVAL WEEKDAY(%s) DAY)" % (
  54. field_name, field_name
  55. )
  56. else:
  57. return "DATE(%s)" % (field_name)
  58. def _convert_field_to_tz(self, field_name, tzname):
  59. if settings.USE_TZ:
  60. field_name = "CONVERT_TZ(%s, 'UTC', '%s')" % (field_name, tzname)
  61. return field_name
  62. def datetime_cast_date_sql(self, field_name, tzname):
  63. field_name = self._convert_field_to_tz(field_name, tzname)
  64. return "DATE(%s)" % field_name
  65. def datetime_cast_time_sql(self, field_name, tzname):
  66. field_name = self._convert_field_to_tz(field_name, tzname)
  67. return "TIME(%s)" % field_name
  68. def datetime_extract_sql(self, lookup_type, field_name, tzname):
  69. field_name = self._convert_field_to_tz(field_name, tzname)
  70. return self.date_extract_sql(lookup_type, field_name)
  71. def datetime_trunc_sql(self, lookup_type, field_name, tzname):
  72. field_name = self._convert_field_to_tz(field_name, tzname)
  73. fields = ['year', 'month', 'day', 'hour', 'minute', 'second']
  74. format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape.
  75. format_def = ('0000-', '01', '-01', ' 00:', '00', ':00')
  76. if lookup_type == 'quarter':
  77. return (
  78. "CAST(DATE_FORMAT(MAKEDATE(YEAR({field_name}), 1) + "
  79. "INTERVAL QUARTER({field_name}) QUARTER - " +
  80. "INTERVAL 1 QUARTER, '%%Y-%%m-01 00:00:00') AS DATETIME)"
  81. ).format(field_name=field_name)
  82. if lookup_type == 'week':
  83. return (
  84. "CAST(DATE_FORMAT(DATE_SUB({field_name}, "
  85. "INTERVAL WEEKDAY({field_name}) DAY), "
  86. "'%%Y-%%m-%%d 00:00:00') AS DATETIME)"
  87. ).format(field_name=field_name)
  88. try:
  89. i = fields.index(lookup_type) + 1
  90. except ValueError:
  91. sql = field_name
  92. else:
  93. format_str = ''.join([f for f in format[:i]] + [f for f in format_def[i:]])
  94. sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str)
  95. return sql
  96. def time_trunc_sql(self, lookup_type, field_name):
  97. fields = {
  98. 'hour': '%%H:00:00',
  99. 'minute': '%%H:%%i:00',
  100. 'second': '%%H:%%i:%%s',
  101. } # Use double percents to escape.
  102. if lookup_type in fields:
  103. format_str = fields[lookup_type]
  104. return "CAST(DATE_FORMAT(%s, '%s') AS TIME)" % (field_name, format_str)
  105. else:
  106. return "TIME(%s)" % (field_name)
  107. def date_interval_sql(self, timedelta):
  108. return 'INTERVAL %s MICROSECOND' % duration_microseconds(timedelta)
  109. def format_for_duration_arithmetic(self, sql):
  110. return 'INTERVAL %s MICROSECOND' % sql
  111. def force_no_ordering(self):
  112. """
  113. "ORDER BY NULL" prevents MySQL from implicitly ordering by grouped
  114. columns. If no ordering would otherwise be applied, we don't want any
  115. implicit sorting going on.
  116. """
  117. return [(None, ("NULL", [], False))]
  118. def last_executed_query(self, cursor, sql, params):
  119. # With MySQLdb, cursor objects have an (undocumented) "_last_executed"
  120. # attribute where the exact query sent to the database is saved.
  121. # See MySQLdb/cursors.py in the source distribution.
  122. return force_text(getattr(cursor, '_last_executed', None), errors='replace')
  123. def no_limit_value(self):
  124. # 2**64 - 1, as recommended by the MySQL documentation
  125. return 18446744073709551615
  126. def quote_name(self, name):
  127. if name.startswith("`") and name.endswith("`"):
  128. return name # Quoting once is enough.
  129. return "`%s`" % name
  130. def random_function_sql(self):
  131. return 'RAND()'
  132. def sql_flush(self, style, tables, sequences, allow_cascade=False):
  133. # NB: The generated SQL below is specific to MySQL
  134. # 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements
  135. # to clear all tables of all data
  136. if tables:
  137. sql = ['SET FOREIGN_KEY_CHECKS = 0;']
  138. for table in tables:
  139. sql.append('%s %s;' % (
  140. style.SQL_KEYWORD('TRUNCATE'),
  141. style.SQL_FIELD(self.quote_name(table)),
  142. ))
  143. sql.append('SET FOREIGN_KEY_CHECKS = 1;')
  144. sql.extend(self.sequence_reset_by_name_sql(style, sequences))
  145. return sql
  146. else:
  147. return []
  148. def validate_autopk_value(self, value):
  149. # MySQLism: zero in AUTO_INCREMENT field does not work. Refs #17653.
  150. if value == 0:
  151. raise ValueError('The database backend does not accept 0 as a '
  152. 'value for AutoField.')
  153. return value
  154. def adapt_datetimefield_value(self, value):
  155. if value is None:
  156. return None
  157. # Expression values are adapted by the database.
  158. if hasattr(value, 'resolve_expression'):
  159. return value
  160. # MySQL doesn't support tz-aware datetimes
  161. if timezone.is_aware(value):
  162. if settings.USE_TZ:
  163. value = timezone.make_naive(value, self.connection.timezone)
  164. else:
  165. raise ValueError("MySQL backend does not support timezone-aware datetimes when USE_TZ is False.")
  166. return str(value)
  167. def adapt_timefield_value(self, value):
  168. if value is None:
  169. return None
  170. # Expression values are adapted by the database.
  171. if hasattr(value, 'resolve_expression'):
  172. return value
  173. # MySQL doesn't support tz-aware times
  174. if timezone.is_aware(value):
  175. raise ValueError("MySQL backend does not support timezone-aware times.")
  176. return str(value)
  177. def max_name_length(self):
  178. return 64
  179. def bulk_insert_sql(self, fields, placeholder_rows):
  180. placeholder_rows_sql = (", ".join(row) for row in placeholder_rows)
  181. values_sql = ", ".join("(%s)" % sql for sql in placeholder_rows_sql)
  182. return "VALUES " + values_sql
  183. def combine_expression(self, connector, sub_expressions):
  184. if connector == '^':
  185. return 'POW(%s)' % ','.join(sub_expressions)
  186. # Convert the result to a signed integer since MySQL's binary operators
  187. # return an unsigned integer.
  188. elif connector in ('&', '|', '<<'):
  189. return 'CONVERT(%s, SIGNED)' % connector.join(sub_expressions)
  190. elif connector == '>>':
  191. lhs, rhs = sub_expressions
  192. return 'FLOOR(%(lhs)s / POW(2, %(rhs)s))' % {'lhs': lhs, 'rhs': rhs}
  193. return super().combine_expression(connector, sub_expressions)
  194. def get_db_converters(self, expression):
  195. converters = super().get_db_converters(expression)
  196. internal_type = expression.output_field.get_internal_type()
  197. if internal_type == 'TextField':
  198. converters.append(self.convert_textfield_value)
  199. elif internal_type in ['BooleanField', 'NullBooleanField']:
  200. converters.append(self.convert_booleanfield_value)
  201. elif internal_type == 'DateTimeField':
  202. if settings.USE_TZ:
  203. converters.append(self.convert_datetimefield_value)
  204. elif internal_type == 'UUIDField':
  205. converters.append(self.convert_uuidfield_value)
  206. return converters
  207. def convert_textfield_value(self, value, expression, connection):
  208. if value is not None:
  209. value = force_text(value)
  210. return value
  211. def convert_booleanfield_value(self, value, expression, connection):
  212. if value in (0, 1):
  213. value = bool(value)
  214. return value
  215. def convert_datetimefield_value(self, value, expression, connection):
  216. if value is not None:
  217. value = timezone.make_aware(value, self.connection.timezone)
  218. return value
  219. def convert_uuidfield_value(self, value, expression, connection):
  220. if value is not None:
  221. value = uuid.UUID(value)
  222. return value
  223. def binary_placeholder_sql(self, value):
  224. return '_binary %s' if value is not None and not hasattr(value, 'as_sql') else '%s'
  225. def subtract_temporals(self, internal_type, lhs, rhs):
  226. lhs_sql, lhs_params = lhs
  227. rhs_sql, rhs_params = rhs
  228. if internal_type == 'TimeField':
  229. return (
  230. "((TIME_TO_SEC(%(lhs)s) * 1000000 + MICROSECOND(%(lhs)s)) -"
  231. " (TIME_TO_SEC(%(rhs)s) * 1000000 + MICROSECOND(%(rhs)s)))"
  232. ) % {'lhs': lhs_sql, 'rhs': rhs_sql}, lhs_params * 2 + rhs_params * 2
  233. else:
  234. return "TIMESTAMPDIFF(MICROSECOND, %s, %s)" % (rhs_sql, lhs_sql), rhs_params + lhs_params
  235. def explain_query_prefix(self, format=None, **options):
  236. # Alias MySQL's TRADITIONAL to TEXT for consistency with other backends.
  237. if format and format.upper() == 'TEXT':
  238. format = 'TRADITIONAL'
  239. prefix = super().explain_query_prefix(format, **options)
  240. if format:
  241. prefix += ' FORMAT=%s' % format
  242. if self.connection.features.needs_explain_extended and format is None:
  243. # EXTENDED and FORMAT are mutually exclusive options.
  244. prefix += ' EXTENDED'
  245. return prefix
  246. def regex_lookup(self, lookup_type):
  247. # REGEXP BINARY doesn't work correctly in MySQL 8+ and REGEXP_LIKE
  248. # doesn't exist in MySQL 5.6 or in MariaDB.
  249. if self.connection.mysql_version < (8, 0, 0) or self.connection.mysql_is_mariadb:
  250. if lookup_type == 'regex':
  251. return '%s REGEXP BINARY %s'
  252. return '%s REGEXP %s'
  253. match_option = 'c' if lookup_type == 'regex' else 'i'
  254. return "REGEXP_LIKE(%%s, %%s, '%s')" % match_option