mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #36624 -- Dropped support for MySQL < 8.4.
This commit is contained in:
		| @@ -76,8 +76,6 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations): | |||||||
|         if is_mariadb: |         if is_mariadb: | ||||||
|             if self.connection.mysql_version < (12, 0, 1): |             if self.connection.mysql_version < (12, 0, 1): | ||||||
|                 disallowed_aggregates.insert(0, models.Collect) |                 disallowed_aggregates.insert(0, models.Collect) | ||||||
|         elif self.connection.mysql_version < (8, 0, 24): |  | ||||||
|             disallowed_aggregates.insert(0, models.Collect) |  | ||||||
|         return tuple(disallowed_aggregates) |         return tuple(disallowed_aggregates) | ||||||
|  |  | ||||||
|     function_names = { |     function_names = { | ||||||
|   | |||||||
| @@ -10,16 +10,6 @@ logger = logging.getLogger("django.contrib.gis") | |||||||
| class MySQLGISSchemaEditor(DatabaseSchemaEditor): | class MySQLGISSchemaEditor(DatabaseSchemaEditor): | ||||||
|     sql_add_spatial_index = "CREATE SPATIAL INDEX %(index)s ON %(table)s(%(column)s)" |     sql_add_spatial_index = "CREATE SPATIAL INDEX %(index)s ON %(table)s(%(column)s)" | ||||||
|  |  | ||||||
|     def skip_default(self, field): |  | ||||||
|         # Geometry fields are stored as BLOB/TEXT, for which MySQL < 8.0.13 |  | ||||||
|         # doesn't support defaults. |  | ||||||
|         if ( |  | ||||||
|             isinstance(field, GeometryField) |  | ||||||
|             and not self._supports_limited_data_type_defaults |  | ||||||
|         ): |  | ||||||
|             return True |  | ||||||
|         return super().skip_default(field) |  | ||||||
|  |  | ||||||
|     def quote_value(self, value): |     def quote_value(self, value): | ||||||
|         if isinstance(value, self.connection.ops.Adapter): |         if isinstance(value, self.connection.ops.Adapter): | ||||||
|             return super().quote_value(str(value)) |             return super().quote_value(str(value)) | ||||||
|   | |||||||
| @@ -144,11 +144,8 @@ class DatabaseWrapper(BaseDatabaseWrapper): | |||||||
|             _data_types["UUIDField"] = "uuid" |             _data_types["UUIDField"] = "uuid" | ||||||
|         return _data_types |         return _data_types | ||||||
|  |  | ||||||
|     # For these data types: |     # For these data types MySQL and MariaDB don't support full width database | ||||||
|     # - MySQL < 8.0.13 doesn't accept default values and implicitly treats them |     # indexes. | ||||||
|     #   as nullable |  | ||||||
|     # - all versions of MySQL and MariaDB don't support full width database |  | ||||||
|     #   indexes |  | ||||||
|     _limited_data_types = ( |     _limited_data_types = ( | ||||||
|         "tinyblob", |         "tinyblob", | ||||||
|         "blob", |         "blob", | ||||||
|   | |||||||
| @@ -66,7 +66,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): | |||||||
|         if self.connection.mysql_is_mariadb: |         if self.connection.mysql_is_mariadb: | ||||||
|             return (10, 6) |             return (10, 6) | ||||||
|         else: |         else: | ||||||
|             return (8, 0, 11) |             return (8, 4) | ||||||
|  |  | ||||||
|     @cached_property |     @cached_property | ||||||
|     def test_collations(self): |     def test_collations(self): | ||||||
| @@ -104,24 +104,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): | |||||||
|                 "update.tests.AdvancedTests.test_update_ordered_by_m2m_annotation_desc", |                 "update.tests.AdvancedTests.test_update_ordered_by_m2m_annotation_desc", | ||||||
|             }, |             }, | ||||||
|         } |         } | ||||||
|         if not self.supports_explain_analyze: |  | ||||||
|             skips.update( |  | ||||||
|                 { |  | ||||||
|                     "MariaDB and MySQL >= 8.0.18 specific.": { |  | ||||||
|                         "queries.test_explain.ExplainTests.test_mysql_analyze", |  | ||||||
|                     }, |  | ||||||
|                 } |  | ||||||
|             ) |  | ||||||
|         if self.connection.mysql_version < (8, 0, 31): |  | ||||||
|             skips.update( |  | ||||||
|                 { |  | ||||||
|                     "Nesting of UNIONs at the right-hand side is not supported on " |  | ||||||
|                     "MySQL < 8.0.31": { |  | ||||||
|                         "queries.test_qs_combinators.QuerySetSetOperationTests." |  | ||||||
|                         "test_union_nested" |  | ||||||
|                     }, |  | ||||||
|                 } |  | ||||||
|             ) |  | ||||||
|         if not self.connection.mysql_is_mariadb: |         if not self.connection.mysql_is_mariadb: | ||||||
|             skips.update( |             skips.update( | ||||||
|                 { |                 { | ||||||
| @@ -186,44 +168,16 @@ class DatabaseFeatures(BaseDatabaseFeatures): | |||||||
|     def is_sql_auto_is_null_enabled(self): |     def is_sql_auto_is_null_enabled(self): | ||||||
|         return self.connection.mysql_server_data["sql_auto_is_null"] |         return self.connection.mysql_server_data["sql_auto_is_null"] | ||||||
|  |  | ||||||
|     @cached_property |  | ||||||
|     def supports_column_check_constraints(self): |  | ||||||
|         if self.connection.mysql_is_mariadb: |  | ||||||
|             return True |  | ||||||
|         return self.connection.mysql_version >= (8, 0, 16) |  | ||||||
|  |  | ||||||
|     supports_table_check_constraints = property( |  | ||||||
|         operator.attrgetter("supports_column_check_constraints") |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     @cached_property |  | ||||||
|     def can_introspect_check_constraints(self): |  | ||||||
|         if self.connection.mysql_is_mariadb: |  | ||||||
|             return True |  | ||||||
|         return self.connection.mysql_version >= (8, 0, 16) |  | ||||||
|  |  | ||||||
|     @cached_property |     @cached_property | ||||||
|     def has_select_for_update_of(self): |     def has_select_for_update_of(self): | ||||||
|         return not self.connection.mysql_is_mariadb |         return not self.connection.mysql_is_mariadb | ||||||
|  |  | ||||||
|     @cached_property |  | ||||||
|     def supports_explain_analyze(self): |  | ||||||
|         return self.connection.mysql_is_mariadb or self.connection.mysql_version >= ( |  | ||||||
|             8, |  | ||||||
|             0, |  | ||||||
|             18, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     @cached_property |     @cached_property | ||||||
|     def supported_explain_formats(self): |     def supported_explain_formats(self): | ||||||
|         # Alias MySQL's TRADITIONAL to TEXT for consistency with other |         # Alias MySQL's TRADITIONAL to TEXT for consistency with other | ||||||
|         # backends. |         # backends. | ||||||
|         formats = {"JSON", "TEXT", "TRADITIONAL"} |         formats = {"JSON", "TEXT", "TRADITIONAL"} | ||||||
|         if not self.connection.mysql_is_mariadb and self.connection.mysql_version >= ( |         if not self.connection.mysql_is_mariadb: | ||||||
|             8, |  | ||||||
|             0, |  | ||||||
|             16, |  | ||||||
|         ): |  | ||||||
|             formats.add("TREE") |             formats.add("TREE") | ||||||
|         return formats |         return formats | ||||||
|  |  | ||||||
| @@ -262,24 +216,8 @@ class DatabaseFeatures(BaseDatabaseFeatures): | |||||||
|         return ( |         return ( | ||||||
|             not self.connection.mysql_is_mariadb |             not self.connection.mysql_is_mariadb | ||||||
|             and self._mysql_storage_engine != "MyISAM" |             and self._mysql_storage_engine != "MyISAM" | ||||||
|             and self.connection.mysql_version >= (8, 0, 13) |  | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @cached_property |  | ||||||
|     def supports_select_intersection(self): |  | ||||||
|         is_mariadb = self.connection.mysql_is_mariadb |  | ||||||
|         return is_mariadb or self.connection.mysql_version >= (8, 0, 31) |  | ||||||
|  |  | ||||||
|     supports_select_difference = property( |  | ||||||
|         operator.attrgetter("supports_select_intersection") |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     @cached_property |  | ||||||
|     def supports_expression_defaults(self): |  | ||||||
|         if self.connection.mysql_is_mariadb: |  | ||||||
|             return True |  | ||||||
|         return self.connection.mysql_version >= (8, 0, 13) |  | ||||||
|  |  | ||||||
|     @cached_property |     @cached_property | ||||||
|     def has_native_uuid_field(self): |     def has_native_uuid_field(self): | ||||||
|         is_mariadb = self.connection.mysql_is_mariadb |         is_mariadb = self.connection.mysql_is_mariadb | ||||||
|   | |||||||
| @@ -349,7 +349,7 @@ class DatabaseOperations(BaseDatabaseOperations): | |||||||
|             format = "TREE" |             format = "TREE" | ||||||
|         analyze = options.pop("analyze", False) |         analyze = options.pop("analyze", False) | ||||||
|         prefix = super().explain_query_prefix(format, **options) |         prefix = super().explain_query_prefix(format, **options) | ||||||
|         if analyze and self.connection.features.supports_explain_analyze: |         if analyze: | ||||||
|             # MariaDB uses ANALYZE instead of EXPLAIN ANALYZE. |             # MariaDB uses ANALYZE instead of EXPLAIN ANALYZE. | ||||||
|             prefix = ( |             prefix = ( | ||||||
|                 "ANALYZE" if self.connection.mysql_is_mariadb else prefix + " ANALYZE" |                 "ANALYZE" if self.connection.mysql_is_mariadb else prefix + " ANALYZE" | ||||||
| @@ -407,15 +407,11 @@ class DatabaseOperations(BaseDatabaseOperations): | |||||||
|     def on_conflict_suffix_sql(self, fields, on_conflict, update_fields, unique_fields): |     def on_conflict_suffix_sql(self, fields, on_conflict, update_fields, unique_fields): | ||||||
|         if on_conflict == OnConflict.UPDATE: |         if on_conflict == OnConflict.UPDATE: | ||||||
|             conflict_suffix_sql = "ON DUPLICATE KEY UPDATE %(fields)s" |             conflict_suffix_sql = "ON DUPLICATE KEY UPDATE %(fields)s" | ||||||
|             # The use of VALUES() is deprecated in MySQL 8.0.20+. Instead, use |             # The use of VALUES() is not supported in MySQL. Instead, use | ||||||
|             # aliases for the new row and its columns available in MySQL |             # aliases for the new row and its columns. | ||||||
|             # 8.0.19+. |  | ||||||
|             if not self.connection.mysql_is_mariadb: |             if not self.connection.mysql_is_mariadb: | ||||||
|                 if self.connection.mysql_version >= (8, 0, 19): |  | ||||||
|                 conflict_suffix_sql = f"AS new {conflict_suffix_sql}" |                 conflict_suffix_sql = f"AS new {conflict_suffix_sql}" | ||||||
|                 field_sql = "%(field)s = new.%(field)s" |                 field_sql = "%(field)s = new.%(field)s" | ||||||
|                 else: |  | ||||||
|                     field_sql = "%(field)s = VALUES(%(field)s)" |  | ||||||
|             # Use VALUE() on MariaDB. |             # Use VALUE() on MariaDB. | ||||||
|             else: |             else: | ||||||
|                 field_sql = "%(field)s = VALUE(%(field)s)" |                 field_sql = "%(field)s = VALUE(%(field)s)" | ||||||
|   | |||||||
| @@ -65,13 +65,10 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): | |||||||
|         default_is_empty = self.effective_default(field) in ("", b"") |         default_is_empty = self.effective_default(field) in ("", b"") | ||||||
|         if default_is_empty and self._is_text_or_blob(field): |         if default_is_empty and self._is_text_or_blob(field): | ||||||
|             return True |             return True | ||||||
|         if not self._supports_limited_data_type_defaults: |  | ||||||
|             return self._is_limited_data_type(field) |  | ||||||
|         return False |         return False | ||||||
|  |  | ||||||
|     def skip_default_on_alter(self, field): |     def skip_default_on_alter(self, field): | ||||||
|         default_is_empty = self.effective_default(field) in ("", b"") |         if self.skip_default(field): | ||||||
|         if default_is_empty and self._is_text_or_blob(field): |  | ||||||
|             return True |             return True | ||||||
|         if self._is_limited_data_type(field) and not self.connection.mysql_is_mariadb: |         if self._is_limited_data_type(field) and not self.connection.mysql_is_mariadb: | ||||||
|             # MySQL doesn't support defaults for BLOB and TEXT in the |             # MySQL doesn't support defaults for BLOB and TEXT in the | ||||||
| @@ -79,19 +76,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): | |||||||
|             return True |             return True | ||||||
|         return False |         return False | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def _supports_limited_data_type_defaults(self): |  | ||||||
|         # MariaDB and MySQL >= 8.0.13 support defaults for BLOB and TEXT. |  | ||||||
|         if self.connection.mysql_is_mariadb: |  | ||||||
|             return True |  | ||||||
|         return self.connection.mysql_version >= (8, 0, 13) |  | ||||||
|  |  | ||||||
|     def _column_default_sql(self, field): |     def _column_default_sql(self, field): | ||||||
|         if ( |         if not self.connection.mysql_is_mariadb and self._is_limited_data_type(field): | ||||||
|             not self.connection.mysql_is_mariadb |  | ||||||
|             and self._supports_limited_data_type_defaults |  | ||||||
|             and self._is_limited_data_type(field) |  | ||||||
|         ): |  | ||||||
|             # MySQL supports defaults for BLOB and TEXT columns only if the |             # MySQL supports defaults for BLOB and TEXT columns only if the | ||||||
|             # default value is written as an expression i.e. in parentheses. |             # default value is written as an expression i.e. in parentheses. | ||||||
|             return "(%s)" |             return "(%s)" | ||||||
|   | |||||||
| @@ -58,7 +58,7 @@ supported versions, and any notes for each of the supported database backends: | |||||||
| Database            Library Requirements            Supported Versions  Notes | Database            Library Requirements            Supported Versions  Notes | ||||||
| ==================  ==============================  ==================  ========================================= | ==================  ==============================  ==================  ========================================= | ||||||
| PostgreSQL          GEOS, GDAL, PROJ, PostGIS       15+                 Requires PostGIS. | PostgreSQL          GEOS, GDAL, PROJ, PostGIS       15+                 Requires PostGIS. | ||||||
| MySQL               GEOS, GDAL                      8.0.11+             :ref:`Limited functionality <mysql-spatial-limitations>`. | MySQL               GEOS, GDAL                      8.4+                :ref:`Limited functionality <mysql-spatial-limitations>`. | ||||||
| Oracle              GEOS, GDAL                      19+                 XE not supported. | Oracle              GEOS, GDAL                      19+                 XE not supported. | ||||||
| SQLite              GEOS, GDAL, PROJ, SpatiaLite    3.37.0+             Requires SpatiaLite 4.3+ | SQLite              GEOS, GDAL, PROJ, SpatiaLite    3.37.0+             Requires SpatiaLite 4.3+ | ||||||
| ==================  ==============================  ==================  ========================================= | ==================  ==============================  ==================  ========================================= | ||||||
|   | |||||||
| @@ -434,7 +434,7 @@ MySQL notes | |||||||
| Version support | Version support | ||||||
| --------------- | --------------- | ||||||
|  |  | ||||||
| Django supports MySQL 8.0.11 and higher. | Django supports MySQL 8.4 and higher. | ||||||
|  |  | ||||||
| Django's ``inspectdb`` feature uses the ``information_schema`` database, which | Django's ``inspectdb`` feature uses the ``information_schema`` database, which | ||||||
| contains detailed data on all database schemas. | contains detailed data on all database schemas. | ||||||
|   | |||||||
| @@ -63,10 +63,9 @@ and the ``weight`` rounded to the nearest integer. | |||||||
|     error. This means that functions such as |     error. This means that functions such as | ||||||
|     :class:`Concat() <django.db.models.functions.Concat>` aren't accepted. |     :class:`Concat() <django.db.models.functions.Concat>` aren't accepted. | ||||||
|  |  | ||||||
| .. admonition:: MySQL and MariaDB | .. admonition:: MariaDB | ||||||
|  |  | ||||||
|     Functional indexes are ignored with MySQL < 8.0.13 and MariaDB as neither |     Functional indexes are unsupported and ignored with MariaDB. | ||||||
|     supports them. |  | ||||||
|  |  | ||||||
| ``fields`` | ``fields`` | ||||||
| ---------- | ---------- | ||||||
|   | |||||||
| @@ -3161,8 +3161,8 @@ Pass these flags as keyword arguments. For example, when using PostgreSQL: | |||||||
|  |  | ||||||
| On some databases, flags may cause the query to be executed which could have | On some databases, flags may cause the query to be executed which could have | ||||||
| adverse effects on your database. For example, the ``ANALYZE`` flag supported | adverse effects on your database. For example, the ``ANALYZE`` flag supported | ||||||
| by MariaDB, MySQL 8.0.18+, and PostgreSQL could result in changes to data if | by MariaDB, MySQL, and PostgreSQL could result in changes to data if there are | ||||||
| there are triggers or if a function is called, even for a ``SELECT`` query. | triggers or if a function is called, even for a ``SELECT`` query. | ||||||
|  |  | ||||||
| .. _field-lookups: | .. _field-lookups: | ||||||
|  |  | ||||||
|   | |||||||
| @@ -319,6 +319,12 @@ Dropped support for PostgreSQL 14 | |||||||
| Upstream support for PostgreSQL 14 ends in November 2026. Django 6.1 supports | Upstream support for PostgreSQL 14 ends in November 2026. Django 6.1 supports | ||||||
| PostgreSQL 15 and higher. | PostgreSQL 15 and higher. | ||||||
|  |  | ||||||
|  | Dropped support for MySQL < 8.4 | ||||||
|  | ------------------------------- | ||||||
|  |  | ||||||
|  | Upstream support for MySQL 8.0 ends in April 2026, and MySQL 8.1-8.3 are | ||||||
|  | short-term innovation releases. Django 6.1 supports MySQL 8.4 and higher. | ||||||
|  |  | ||||||
| Miscellaneous | Miscellaneous | ||||||
| ------------- | ------------- | ||||||
|  |  | ||||||
|   | |||||||
| @@ -109,8 +109,8 @@ class Tests(TestCase): | |||||||
|             mocked_get_database_version.return_value = (10, 5) |             mocked_get_database_version.return_value = (10, 5) | ||||||
|             msg = "MariaDB 10.6 or later is required (found 10.5)." |             msg = "MariaDB 10.6 or later is required (found 10.5)." | ||||||
|         else: |         else: | ||||||
|             mocked_get_database_version.return_value = (8, 0, 4) |             mocked_get_database_version.return_value = (8, 0, 31) | ||||||
|             msg = "MySQL 8.0.11 or later is required (found 8.0.4)." |             msg = "MySQL 8.4 or later is required (found 8.0.31)." | ||||||
|  |  | ||||||
|         with self.assertRaisesMessage(NotSupportedError, msg): |         with self.assertRaisesMessage(NotSupportedError, msg): | ||||||
|             connection.check_database_version_supported() |             connection.check_database_version_supported() | ||||||
|   | |||||||
| @@ -159,9 +159,7 @@ class ExplainTests(TestCase): | |||||||
|         self.assertEqual(len(captured_queries), 1) |         self.assertEqual(len(captured_queries), 1) | ||||||
|         self.assertIn("FORMAT=TRADITIONAL", captured_queries[0]["sql"]) |         self.assertIn("FORMAT=TRADITIONAL", captured_queries[0]["sql"]) | ||||||
|  |  | ||||||
|     @unittest.skipUnless( |     @unittest.skipUnless(connection.vendor == "mysql", "MySQL specific") | ||||||
|         connection.vendor == "mysql", "MariaDB and MySQL >= 8.0.18 specific." |  | ||||||
|     ) |  | ||||||
|     def test_mysql_analyze(self): |     def test_mysql_analyze(self): | ||||||
|         qs = Tag.objects.filter(name="test") |         qs = Tag.objects.filter(name="test") | ||||||
|         with CaptureQueriesContext(connection) as captured_queries: |         with CaptureQueriesContext(connection) as captured_queries: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user