1
0
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:
Mariusz Felisiak
2025-10-27 15:05:23 +01:00
committed by GitHub
parent 4744e9939b
commit c87daabbf3
13 changed files with 26 additions and 118 deletions

View File

@@ -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 = {

View File

@@ -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))

View File

@@ -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",

View File

@@ -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

View File

@@ -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)"

View File

@@ -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)"

View File

@@ -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+
================== ============================== ================== =========================================

View File

@@ -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.

View File

@@ -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``
----------

View File

@@ -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:

View File

@@ -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
-------------

View File

@@ -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()

View File

@@ -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: