summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMariusz Felisiak <felisiak.mariusz@gmail.com>2025-10-27 15:05:23 +0100
committerGitHub <noreply@github.com>2025-10-27 15:05:23 +0100
commitc87daabbf32779f5421a846dd33a7dd46cc27d54 (patch)
treefca1e4216a722899bfe41ee1cd82d7dc71d35a41
parent4744e9939b65d168c531e5e23d1ac8a4445ac7f9 (diff)
Fixed #36624 -- Dropped support for MySQL < 8.4.
-rw-r--r--django/contrib/gis/db/backends/mysql/operations.py2
-rw-r--r--django/contrib/gis/db/backends/mysql/schema.py10
-rw-r--r--django/db/backends/mysql/base.py7
-rw-r--r--django/db/backends/mysql/features.py66
-rw-r--r--django/db/backends/mysql/operations.py14
-rw-r--r--django/db/backends/mysql/schema.py18
-rw-r--r--docs/ref/contrib/gis/install/index.txt2
-rw-r--r--docs/ref/databases.txt2
-rw-r--r--docs/ref/models/indexes.txt5
-rw-r--r--docs/ref/models/querysets.txt4
-rw-r--r--docs/releases/6.1.txt6
-rw-r--r--tests/backends/mysql/tests.py4
-rw-r--r--tests/queries/test_explain.py4
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(
{
@@ -187,43 +169,15 @@ class DatabaseFeatures(BaseDatabaseFeatures):
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,25 +216,9 @@ 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
return is_mariadb and self.connection.mysql_version >= (10, 7)
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-spatial-limitations>`.
+MySQL GEOS, GDAL 8.4+ :ref:`Limited functionality <mysql-spatial-limitations>`.
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() <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
- 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: