From c87daabbf32779f5421a846dd33a7dd46cc27d54 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Mon, 27 Oct 2025 15:05:23 +0100 Subject: [PATCH] Fixed #36624 -- Dropped support for MySQL < 8.4. --- .../gis/db/backends/mysql/operations.py | 2 - .../contrib/gis/db/backends/mysql/schema.py | 10 --- django/db/backends/mysql/base.py | 7 +- django/db/backends/mysql/features.py | 66 +------------------ django/db/backends/mysql/operations.py | 14 ++-- django/db/backends/mysql/schema.py | 18 +---- docs/ref/contrib/gis/install/index.txt | 2 +- docs/ref/databases.txt | 2 +- docs/ref/models/indexes.txt | 5 +- docs/ref/models/querysets.txt | 4 +- docs/releases/6.1.txt | 6 ++ tests/backends/mysql/tests.py | 4 +- tests/queries/test_explain.py | 4 +- 13 files changed, 26 insertions(+), 118 deletions(-) diff --git a/django/contrib/gis/db/backends/mysql/operations.py b/django/contrib/gis/db/backends/mysql/operations.py index f838a79da6..b82bd16abb 100644 --- a/django/contrib/gis/db/backends/mysql/operations.py +++ b/django/contrib/gis/db/backends/mysql/operations.py @@ -76,8 +76,6 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations): if is_mariadb: if self.connection.mysql_version < (12, 0, 1): disallowed_aggregates.insert(0, models.Collect) - elif self.connection.mysql_version < (8, 0, 24): - disallowed_aggregates.insert(0, models.Collect) return tuple(disallowed_aggregates) function_names = { diff --git a/django/contrib/gis/db/backends/mysql/schema.py b/django/contrib/gis/db/backends/mysql/schema.py index e485c671e5..78e97bb1ca 100644 --- a/django/contrib/gis/db/backends/mysql/schema.py +++ b/django/contrib/gis/db/backends/mysql/schema.py @@ -10,16 +10,6 @@ logger = logging.getLogger("django.contrib.gis") class MySQLGISSchemaEditor(DatabaseSchemaEditor): 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): if isinstance(value, self.connection.ops.Adapter): return super().quote_value(str(value)) diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index e83dc106f7..d4b98971fa 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -144,11 +144,8 @@ class DatabaseWrapper(BaseDatabaseWrapper): _data_types["UUIDField"] = "uuid" return _data_types - # For these data types: - # - MySQL < 8.0.13 doesn't accept default values and implicitly treats them - # as nullable - # - all versions of MySQL and MariaDB don't support full width database - # indexes + # For these data types MySQL and MariaDB don't support full width database + # indexes. _limited_data_types = ( "tinyblob", "blob", diff --git a/django/db/backends/mysql/features.py b/django/db/backends/mysql/features.py index 4be20b92ac..3315db6ae9 100644 --- a/django/db/backends/mysql/features.py +++ b/django/db/backends/mysql/features.py @@ -66,7 +66,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): if self.connection.mysql_is_mariadb: return (10, 6) else: - return (8, 0, 11) + return (8, 4) @cached_property def test_collations(self): @@ -104,24 +104,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): "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: skips.update( { @@ -186,44 +168,16 @@ class DatabaseFeatures(BaseDatabaseFeatures): def is_sql_auto_is_null_enabled(self): 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 def has_select_for_update_of(self): 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 def supported_explain_formats(self): # Alias MySQL's TRADITIONAL to TEXT for consistency with other # backends. formats = {"JSON", "TEXT", "TRADITIONAL"} - if not self.connection.mysql_is_mariadb and self.connection.mysql_version >= ( - 8, - 0, - 16, - ): + if not self.connection.mysql_is_mariadb: formats.add("TREE") return formats @@ -262,24 +216,8 @@ class DatabaseFeatures(BaseDatabaseFeatures): return ( not self.connection.mysql_is_mariadb 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 def has_native_uuid_field(self): is_mariadb = self.connection.mysql_is_mariadb diff --git a/django/db/backends/mysql/operations.py b/django/db/backends/mysql/operations.py index 7dfcd57958..74ba72f316 100644 --- a/django/db/backends/mysql/operations.py +++ b/django/db/backends/mysql/operations.py @@ -349,7 +349,7 @@ class DatabaseOperations(BaseDatabaseOperations): format = "TREE" analyze = options.pop("analyze", False) 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. prefix = ( "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): if on_conflict == OnConflict.UPDATE: conflict_suffix_sql = "ON DUPLICATE KEY UPDATE %(fields)s" - # The use of VALUES() is deprecated in MySQL 8.0.20+. Instead, use - # aliases for the new row and its columns available in MySQL - # 8.0.19+. + # The use of VALUES() is not supported in MySQL. Instead, use + # aliases for the new row and its columns. if not self.connection.mysql_is_mariadb: - if self.connection.mysql_version >= (8, 0, 19): - conflict_suffix_sql = f"AS new {conflict_suffix_sql}" - field_sql = "%(field)s = new.%(field)s" - else: - field_sql = "%(field)s = VALUES(%(field)s)" + conflict_suffix_sql = f"AS new {conflict_suffix_sql}" + field_sql = "%(field)s = new.%(field)s" # Use VALUE() on MariaDB. else: field_sql = "%(field)s = VALUE(%(field)s)" diff --git a/django/db/backends/mysql/schema.py b/django/db/backends/mysql/schema.py index ab388754ed..9eba216256 100644 --- a/django/db/backends/mysql/schema.py +++ b/django/db/backends/mysql/schema.py @@ -65,13 +65,10 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): default_is_empty = self.effective_default(field) in ("", b"") if default_is_empty and self._is_text_or_blob(field): return True - if not self._supports_limited_data_type_defaults: - return self._is_limited_data_type(field) return False def skip_default_on_alter(self, field): - default_is_empty = self.effective_default(field) in ("", b"") - if default_is_empty and self._is_text_or_blob(field): + if self.skip_default(field): return True 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 @@ -79,19 +76,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): return True 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): - if ( - not self.connection.mysql_is_mariadb - and self._supports_limited_data_type_defaults - and self._is_limited_data_type(field) - ): + if not self.connection.mysql_is_mariadb and self._is_limited_data_type(field): # MySQL supports defaults for BLOB and TEXT columns only if the # default value is written as an expression i.e. in parentheses. return "(%s)" diff --git a/docs/ref/contrib/gis/install/index.txt b/docs/ref/contrib/gis/install/index.txt index f127478151..54c0491c65 100644 --- a/docs/ref/contrib/gis/install/index.txt +++ b/docs/ref/contrib/gis/install/index.txt @@ -58,7 +58,7 @@ supported versions, and any notes for each of the supported database backends: Database Library Requirements Supported Versions Notes ================== ============================== ================== ========================================= PostgreSQL GEOS, GDAL, PROJ, PostGIS 15+ Requires PostGIS. -MySQL GEOS, GDAL 8.0.11+ :ref:`Limited functionality `. +MySQL GEOS, GDAL 8.4+ :ref:`Limited functionality `. Oracle GEOS, GDAL 19+ XE not supported. SQLite GEOS, GDAL, PROJ, SpatiaLite 3.37.0+ Requires SpatiaLite 4.3+ ================== ============================== ================== ========================================= diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index cd415e1c00..9ea54297aa 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -434,7 +434,7 @@ MySQL notes 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 contains detailed data on all database schemas. diff --git a/docs/ref/models/indexes.txt b/docs/ref/models/indexes.txt index d2b2430643..6f8dd39fb7 100644 --- a/docs/ref/models/indexes.txt +++ b/docs/ref/models/indexes.txt @@ -63,10 +63,9 @@ and the ``weight`` rounded to the nearest integer. error. This means that functions such as :class:`Concat() ` aren't accepted. -.. admonition:: MySQL and MariaDB +.. admonition:: MariaDB - Functional indexes are ignored with MySQL < 8.0.13 and MariaDB as neither - supports them. + Functional indexes are unsupported and ignored with MariaDB. ``fields`` ---------- diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index d9badb690d..3e7024211e 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -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 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 -there are triggers or if a function is called, even for a ``SELECT`` query. +by MariaDB, MySQL, and PostgreSQL could result in changes to data if there are +triggers or if a function is called, even for a ``SELECT`` query. .. _field-lookups: diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt index 1430cb4f17..670026077a 100644 --- a/docs/releases/6.1.txt +++ b/docs/releases/6.1.txt @@ -319,6 +319,12 @@ Dropped support for PostgreSQL 14 Upstream support for PostgreSQL 14 ends in November 2026. Django 6.1 supports 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 ------------- diff --git a/tests/backends/mysql/tests.py b/tests/backends/mysql/tests.py index e718f9fae4..15228d254f 100644 --- a/tests/backends/mysql/tests.py +++ b/tests/backends/mysql/tests.py @@ -109,8 +109,8 @@ class Tests(TestCase): mocked_get_database_version.return_value = (10, 5) msg = "MariaDB 10.6 or later is required (found 10.5)." else: - mocked_get_database_version.return_value = (8, 0, 4) - msg = "MySQL 8.0.11 or later is required (found 8.0.4)." + mocked_get_database_version.return_value = (8, 0, 31) + msg = "MySQL 8.4 or later is required (found 8.0.31)." with self.assertRaisesMessage(NotSupportedError, msg): connection.check_database_version_supported() diff --git a/tests/queries/test_explain.py b/tests/queries/test_explain.py index 95ca913cfc..59bd0e8d08 100644 --- a/tests/queries/test_explain.py +++ b/tests/queries/test_explain.py @@ -159,9 +159,7 @@ class ExplainTests(TestCase): self.assertEqual(len(captured_queries), 1) self.assertIn("FORMAT=TRADITIONAL", captured_queries[0]["sql"]) - @unittest.skipUnless( - connection.vendor == "mysql", "MariaDB and MySQL >= 8.0.18 specific." - ) + @unittest.skipUnless(connection.vendor == "mysql", "MySQL specific") def test_mysql_analyze(self): qs = Tag.objects.filter(name="test") with CaptureQueriesContext(connection) as captured_queries: