features.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  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. related_fields_match_type = True
  7. # MySQL doesn't support sliced subqueries with IN/ALL/ANY/SOME.
  8. allow_sliced_subqueries_with_in = False
  9. has_select_for_update = True
  10. has_select_for_update_nowait = True
  11. has_select_for_update_skip_locked = True
  12. supports_forward_references = False
  13. supports_regex_backreferencing = False
  14. supports_date_lookup_using_string = False
  15. supports_timezones = False
  16. requires_explicit_null_ordering_when_grouping = True
  17. atomic_transactions = False
  18. can_clone_databases = True
  19. supports_comments = True
  20. supports_comments_inline = True
  21. supports_temporal_subtraction = True
  22. supports_slicing_ordering_in_compound = True
  23. supports_index_on_text_field = False
  24. supports_over_clause = True
  25. supports_frame_range_fixed_distance = True
  26. supports_update_conflicts = True
  27. can_rename_index = True
  28. delete_can_self_reference_subquery = False
  29. create_test_procedure_without_params_sql = """
  30. CREATE PROCEDURE test_procedure ()
  31. BEGIN
  32. DECLARE V_I INTEGER;
  33. SET V_I = 1;
  34. END;
  35. """
  36. create_test_procedure_with_int_param_sql = """
  37. CREATE PROCEDURE test_procedure (P_I INTEGER)
  38. BEGIN
  39. DECLARE V_I INTEGER;
  40. SET V_I = P_I;
  41. END;
  42. """
  43. create_test_table_with_composite_primary_key = """
  44. CREATE TABLE test_table_composite_pk (
  45. column_1 INTEGER NOT NULL,
  46. column_2 INTEGER NOT NULL,
  47. PRIMARY KEY(column_1, column_2)
  48. )
  49. """
  50. # Neither MySQL nor MariaDB support partial indexes.
  51. supports_partial_indexes = False
  52. # COLLATE must be wrapped in parentheses because MySQL treats COLLATE as an
  53. # indexed expression.
  54. collate_as_index_expression = True
  55. insert_test_table_with_defaults = "INSERT INTO {} () VALUES ()"
  56. supports_order_by_nulls_modifier = False
  57. order_by_nulls_first = True
  58. supports_logical_xor = True
  59. supports_stored_generated_columns = True
  60. supports_virtual_generated_columns = True
  61. @cached_property
  62. def minimum_database_version(self):
  63. if self.connection.mysql_is_mariadb:
  64. return (10, 6)
  65. else:
  66. return (8, 0, 11)
  67. @cached_property
  68. def test_collations(self):
  69. return {
  70. "ci": "utf8mb4_general_ci",
  71. "non_default": "utf8mb4_esperanto_ci",
  72. "swedish_ci": "utf8mb4_swedish_ci",
  73. "virtual": "utf8mb4_esperanto_ci",
  74. }
  75. test_now_utc_template = "UTC_TIMESTAMP(6)"
  76. @cached_property
  77. def django_test_skips(self):
  78. skips = {
  79. "This doesn't work on MySQL.": {
  80. "db_functions.comparison.test_greatest.GreatestTests."
  81. "test_coalesce_workaround",
  82. "db_functions.comparison.test_least.LeastTests."
  83. "test_coalesce_workaround",
  84. },
  85. "MySQL doesn't support functional indexes on a function that "
  86. "returns JSON": {
  87. "schema.tests.SchemaTests.test_func_index_json_key_transform",
  88. },
  89. "MySQL supports multiplying and dividing DurationFields by a "
  90. "scalar value but it's not implemented (#25287).": {
  91. "expressions.tests.FTimeDeltaTests.test_durationfield_multiply_divide",
  92. },
  93. "UPDATE ... ORDER BY syntax on MySQL/MariaDB does not support ordering by"
  94. "related fields.": {
  95. "update.tests.AdvancedTests."
  96. "test_update_ordered_by_inline_m2m_annotation",
  97. "update.tests.AdvancedTests.test_update_ordered_by_m2m_annotation",
  98. "update.tests.AdvancedTests.test_update_ordered_by_m2m_annotation_desc",
  99. },
  100. }
  101. if not self.supports_explain_analyze:
  102. skips.update(
  103. {
  104. "MariaDB and MySQL >= 8.0.18 specific.": {
  105. "queries.test_explain.ExplainTests.test_mysql_analyze",
  106. },
  107. }
  108. )
  109. if "ONLY_FULL_GROUP_BY" in self.connection.sql_mode:
  110. skips.update(
  111. {
  112. "GROUP BY cannot contain nonaggregated column when "
  113. "ONLY_FULL_GROUP_BY mode is enabled on MySQL, see #34262.": {
  114. "aggregation.tests.AggregateTestCase."
  115. "test_group_by_nested_expression_with_params",
  116. },
  117. }
  118. )
  119. if self.connection.mysql_version < (8, 0, 31):
  120. skips.update(
  121. {
  122. "Nesting of UNIONs at the right-hand side is not supported on "
  123. "MySQL < 8.0.31": {
  124. "queries.test_qs_combinators.QuerySetSetOperationTests."
  125. "test_union_nested"
  126. },
  127. }
  128. )
  129. if not self.connection.mysql_is_mariadb:
  130. skips.update(
  131. {
  132. "MySQL doesn't allow renaming columns referenced by generated "
  133. "columns": {
  134. "migrations.test_operations.OperationTests."
  135. "test_invalid_generated_field_changes_on_rename_stored",
  136. "migrations.test_operations.OperationTests."
  137. "test_invalid_generated_field_changes_on_rename_virtual",
  138. },
  139. }
  140. )
  141. return skips
  142. @cached_property
  143. def _mysql_storage_engine(self):
  144. "Internal method used in Django tests. Don't rely on this from your code"
  145. return self.connection.mysql_server_data["default_storage_engine"]
  146. @cached_property
  147. def allows_auto_pk_0(self):
  148. """
  149. Autoincrement primary key can be set to 0 if it doesn't generate new
  150. autoincrement values.
  151. """
  152. return "NO_AUTO_VALUE_ON_ZERO" in self.connection.sql_mode
  153. @cached_property
  154. def update_can_self_select(self):
  155. return self.connection.mysql_is_mariadb
  156. @cached_property
  157. def can_introspect_foreign_keys(self):
  158. "Confirm support for introspected foreign keys"
  159. return self._mysql_storage_engine != "MyISAM"
  160. @cached_property
  161. def introspected_field_types(self):
  162. return {
  163. **super().introspected_field_types,
  164. "BinaryField": "TextField",
  165. "BooleanField": "IntegerField",
  166. "DurationField": "BigIntegerField",
  167. "GenericIPAddressField": "CharField",
  168. }
  169. @cached_property
  170. def can_return_columns_from_insert(self):
  171. return self.connection.mysql_is_mariadb
  172. can_return_rows_from_bulk_insert = property(
  173. operator.attrgetter("can_return_columns_from_insert")
  174. )
  175. @cached_property
  176. def has_zoneinfo_database(self):
  177. return self.connection.mysql_server_data["has_zoneinfo_database"]
  178. @cached_property
  179. def is_sql_auto_is_null_enabled(self):
  180. return self.connection.mysql_server_data["sql_auto_is_null"]
  181. @cached_property
  182. def supports_column_check_constraints(self):
  183. if self.connection.mysql_is_mariadb:
  184. return True
  185. return self.connection.mysql_version >= (8, 0, 16)
  186. supports_table_check_constraints = property(
  187. operator.attrgetter("supports_column_check_constraints")
  188. )
  189. @cached_property
  190. def can_introspect_check_constraints(self):
  191. if self.connection.mysql_is_mariadb:
  192. return True
  193. return self.connection.mysql_version >= (8, 0, 16)
  194. @cached_property
  195. def has_select_for_update_of(self):
  196. return not self.connection.mysql_is_mariadb
  197. @cached_property
  198. def supports_explain_analyze(self):
  199. return self.connection.mysql_is_mariadb or self.connection.mysql_version >= (
  200. 8,
  201. 0,
  202. 18,
  203. )
  204. @cached_property
  205. def supported_explain_formats(self):
  206. # Alias MySQL's TRADITIONAL to TEXT for consistency with other
  207. # backends.
  208. formats = {"JSON", "TEXT", "TRADITIONAL"}
  209. if not self.connection.mysql_is_mariadb and self.connection.mysql_version >= (
  210. 8,
  211. 0,
  212. 16,
  213. ):
  214. formats.add("TREE")
  215. return formats
  216. @cached_property
  217. def supports_transactions(self):
  218. """
  219. All storage engines except MyISAM support transactions.
  220. """
  221. return self._mysql_storage_engine != "MyISAM"
  222. @cached_property
  223. def ignores_table_name_case(self):
  224. return self.connection.mysql_server_data["lower_case_table_names"]
  225. @cached_property
  226. def supports_default_in_lead_lag(self):
  227. # To be added in https://jira.mariadb.org/browse/MDEV-12981.
  228. return not self.connection.mysql_is_mariadb
  229. @cached_property
  230. def can_introspect_json_field(self):
  231. if self.connection.mysql_is_mariadb:
  232. return self.can_introspect_check_constraints
  233. return True
  234. @cached_property
  235. def supports_index_column_ordering(self):
  236. if self._mysql_storage_engine != "InnoDB":
  237. return False
  238. if self.connection.mysql_is_mariadb:
  239. return self.connection.mysql_version >= (10, 8)
  240. return True
  241. @cached_property
  242. def supports_expression_indexes(self):
  243. return (
  244. not self.connection.mysql_is_mariadb
  245. and self._mysql_storage_engine != "MyISAM"
  246. and self.connection.mysql_version >= (8, 0, 13)
  247. )
  248. @cached_property
  249. def supports_select_intersection(self):
  250. is_mariadb = self.connection.mysql_is_mariadb
  251. return is_mariadb or self.connection.mysql_version >= (8, 0, 31)
  252. supports_select_difference = property(
  253. operator.attrgetter("supports_select_intersection")
  254. )
  255. @cached_property
  256. def supports_expression_defaults(self):
  257. if self.connection.mysql_is_mariadb:
  258. return True
  259. return self.connection.mysql_version >= (8, 0, 13)
  260. @cached_property
  261. def has_native_uuid_field(self):
  262. is_mariadb = self.connection.mysql_is_mariadb
  263. return is_mariadb and self.connection.mysql_version >= (10, 7)
  264. @cached_property
  265. def allows_group_by_selected_pks(self):
  266. if self.connection.mysql_is_mariadb:
  267. return "ONLY_FULL_GROUP_BY" not in self.connection.sql_mode
  268. return True