features.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import operator
  2. from django.db.backends.base.features import BaseDatabaseFeatures
  3. from django.utils.functional import cached_property
  4. class DatabaseFeatures(BaseDatabaseFeatures):
  5. empty_fetchmany_value = ()
  6. allows_group_by_pk = True
  7. related_fields_match_type = True
  8. # MySQL doesn't support sliced subqueries with IN/ALL/ANY/SOME.
  9. allow_sliced_subqueries_with_in = False
  10. has_select_for_update = True
  11. supports_forward_references = False
  12. supports_regex_backreferencing = False
  13. supports_date_lookup_using_string = False
  14. supports_timezones = False
  15. requires_explicit_null_ordering_when_grouping = True
  16. can_release_savepoints = True
  17. atomic_transactions = False
  18. can_clone_databases = True
  19. supports_temporal_subtraction = True
  20. supports_select_intersection = False
  21. supports_select_difference = False
  22. supports_slicing_ordering_in_compound = True
  23. supports_index_on_text_field = False
  24. has_case_insensitive_like = False
  25. create_test_procedure_without_params_sql = """
  26. CREATE PROCEDURE test_procedure ()
  27. BEGIN
  28. DECLARE V_I INTEGER;
  29. SET V_I = 1;
  30. END;
  31. """
  32. create_test_procedure_with_int_param_sql = """
  33. CREATE PROCEDURE test_procedure (P_I INTEGER)
  34. BEGIN
  35. DECLARE V_I INTEGER;
  36. SET V_I = P_I;
  37. END;
  38. """
  39. # Neither MySQL nor MariaDB support partial indexes.
  40. supports_partial_indexes = False
  41. # COLLATE must be wrapped in parentheses because MySQL treats COLLATE as an
  42. # indexed expression.
  43. collate_as_index_expression = True
  44. supports_order_by_nulls_modifier = False
  45. order_by_nulls_first = True
  46. @cached_property
  47. def test_collations(self):
  48. charset = 'utf8'
  49. if (
  50. self.connection.mysql_is_mariadb
  51. and self.connection.mysql_version >= (10, 6)
  52. ) or (
  53. not self.connection.mysql_is_mariadb
  54. and self.connection.mysql_version >= (8, 0, 30)
  55. ):
  56. # utf8 is an alias for utf8mb3 in MariaDB 10.6+ and MySQL 8.0.30+.
  57. charset = "utf8mb3"
  58. return {
  59. 'ci': f'{charset}_general_ci',
  60. 'non_default': f'{charset}_esperanto_ci',
  61. 'swedish_ci': f'{charset}_swedish_ci',
  62. }
  63. @cached_property
  64. def django_test_skips(self):
  65. skips = {
  66. "This doesn't work on MySQL.": {
  67. 'db_functions.comparison.test_greatest.GreatestTests.test_coalesce_workaround',
  68. 'db_functions.comparison.test_least.LeastTests.test_coalesce_workaround',
  69. },
  70. 'Running on MySQL requires utf8mb4 encoding (#18392).': {
  71. 'model_fields.test_textfield.TextFieldTests.test_emoji',
  72. 'model_fields.test_charfield.TestCharField.test_emoji',
  73. },
  74. "MySQL doesn't support functional indexes on a function that "
  75. "returns JSON": {
  76. 'schema.tests.SchemaTests.test_func_index_json_key_transform',
  77. },
  78. }
  79. if 'ONLY_FULL_GROUP_BY' in self.connection.sql_mode:
  80. skips.update({
  81. 'GROUP BY optimization does not work properly when '
  82. 'ONLY_FULL_GROUP_BY mode is enabled on MySQL, see #31331.': {
  83. 'aggregation.tests.AggregateTestCase.test_aggregation_subquery_annotation_multivalued',
  84. 'annotations.tests.NonAggregateAnnotationTestCase.test_annotation_aggregate_with_m2o',
  85. },
  86. })
  87. if (
  88. self.connection.mysql_is_mariadb and
  89. (10, 4, 3) < self.connection.mysql_version < (10, 5, 2)
  90. ):
  91. skips.update({
  92. 'https://jira.mariadb.org/browse/MDEV-19598': {
  93. 'schema.tests.SchemaTests.test_alter_not_unique_field_to_primary_key',
  94. },
  95. })
  96. if (
  97. self.connection.mysql_is_mariadb and
  98. (10, 4, 12) < self.connection.mysql_version < (10, 5)
  99. ):
  100. skips.update({
  101. 'https://jira.mariadb.org/browse/MDEV-22775': {
  102. 'schema.tests.SchemaTests.test_alter_pk_with_self_referential_field',
  103. },
  104. })
  105. if not self.supports_explain_analyze:
  106. skips.update({
  107. 'MariaDB and MySQL >= 8.0.18 specific.': {
  108. 'queries.test_explain.ExplainTests.test_mysql_analyze',
  109. },
  110. })
  111. return skips
  112. @cached_property
  113. def _mysql_storage_engine(self):
  114. "Internal method used in Django tests. Don't rely on this from your code"
  115. return self.connection.mysql_server_data['default_storage_engine']
  116. @cached_property
  117. def allows_auto_pk_0(self):
  118. """
  119. Autoincrement primary key can be set to 0 if it doesn't generate new
  120. autoincrement values.
  121. """
  122. return 'NO_AUTO_VALUE_ON_ZERO' in self.connection.sql_mode
  123. @cached_property
  124. def update_can_self_select(self):
  125. return self.connection.mysql_is_mariadb and self.connection.mysql_version >= (10, 3, 2)
  126. @cached_property
  127. def can_introspect_foreign_keys(self):
  128. "Confirm support for introspected foreign keys"
  129. return self._mysql_storage_engine != 'MyISAM'
  130. @cached_property
  131. def introspected_field_types(self):
  132. return {
  133. **super().introspected_field_types,
  134. 'BinaryField': 'TextField',
  135. 'BooleanField': 'IntegerField',
  136. 'DurationField': 'BigIntegerField',
  137. 'GenericIPAddressField': 'CharField',
  138. }
  139. @cached_property
  140. def can_return_columns_from_insert(self):
  141. return self.connection.mysql_is_mariadb and self.connection.mysql_version >= (10, 5, 0)
  142. can_return_rows_from_bulk_insert = property(operator.attrgetter('can_return_columns_from_insert'))
  143. @cached_property
  144. def has_zoneinfo_database(self):
  145. return self.connection.mysql_server_data['has_zoneinfo_database']
  146. @cached_property
  147. def is_sql_auto_is_null_enabled(self):
  148. return self.connection.mysql_server_data['sql_auto_is_null']
  149. @cached_property
  150. def supports_over_clause(self):
  151. if self.connection.mysql_is_mariadb:
  152. return True
  153. return self.connection.mysql_version >= (8, 0, 2)
  154. supports_frame_range_fixed_distance = property(operator.attrgetter('supports_over_clause'))
  155. @cached_property
  156. def supports_column_check_constraints(self):
  157. if self.connection.mysql_is_mariadb:
  158. return self.connection.mysql_version >= (10, 2, 1)
  159. return self.connection.mysql_version >= (8, 0, 16)
  160. supports_table_check_constraints = property(operator.attrgetter('supports_column_check_constraints'))
  161. @cached_property
  162. def can_introspect_check_constraints(self):
  163. if self.connection.mysql_is_mariadb:
  164. version = self.connection.mysql_version
  165. return (version >= (10, 2, 22) and version < (10, 3)) or version >= (10, 3, 10)
  166. return self.connection.mysql_version >= (8, 0, 16)
  167. @cached_property
  168. def has_select_for_update_skip_locked(self):
  169. return not self.connection.mysql_is_mariadb and self.connection.mysql_version >= (8, 0, 1)
  170. @cached_property
  171. def has_select_for_update_nowait(self):
  172. if self.connection.mysql_is_mariadb:
  173. return self.connection.mysql_version >= (10, 3, 0)
  174. return self.connection.mysql_version >= (8, 0, 1)
  175. @cached_property
  176. def has_select_for_update_of(self):
  177. return not self.connection.mysql_is_mariadb and self.connection.mysql_version >= (8, 0, 1)
  178. @cached_property
  179. def supports_explain_analyze(self):
  180. return self.connection.mysql_is_mariadb or self.connection.mysql_version >= (8, 0, 18)
  181. @cached_property
  182. def supported_explain_formats(self):
  183. # Alias MySQL's TRADITIONAL to TEXT for consistency with other
  184. # backends.
  185. formats = {'JSON', 'TEXT', 'TRADITIONAL'}
  186. if not self.connection.mysql_is_mariadb and self.connection.mysql_version >= (8, 0, 16):
  187. formats.add('TREE')
  188. return formats
  189. @cached_property
  190. def supports_transactions(self):
  191. """
  192. All storage engines except MyISAM support transactions.
  193. """
  194. return self._mysql_storage_engine != 'MyISAM'
  195. @cached_property
  196. def ignores_table_name_case(self):
  197. return self.connection.mysql_server_data['lower_case_table_names']
  198. @cached_property
  199. def supports_default_in_lead_lag(self):
  200. # To be added in https://jira.mariadb.org/browse/MDEV-12981.
  201. return not self.connection.mysql_is_mariadb
  202. @cached_property
  203. def supports_json_field(self):
  204. if self.connection.mysql_is_mariadb:
  205. return self.connection.mysql_version >= (10, 2, 7)
  206. return self.connection.mysql_version >= (5, 7, 8)
  207. @cached_property
  208. def can_introspect_json_field(self):
  209. if self.connection.mysql_is_mariadb:
  210. return self.supports_json_field and self.can_introspect_check_constraints
  211. return self.supports_json_field
  212. @cached_property
  213. def supports_index_column_ordering(self):
  214. return (
  215. not self.connection.mysql_is_mariadb and
  216. self.connection.mysql_version >= (8, 0, 1)
  217. )
  218. @cached_property
  219. def supports_expression_indexes(self):
  220. return (
  221. not self.connection.mysql_is_mariadb and
  222. self.connection.mysql_version >= (8, 0, 13)
  223. )