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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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