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