Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions django/contrib/gis/db/backends/mysql/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,6 @@ def disallowed_aggregates(self):
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 = {
Expand Down
10 changes: 0 additions & 10 deletions django/contrib/gis/db/backends/mysql/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,6 @@
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))
Expand Down
7 changes: 2 additions & 5 deletions django/db/backends/mysql/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,8 @@ def data_types(self):
_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",
Expand Down
66 changes: 2 additions & 64 deletions django/db/backends/mysql/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def minimum_database_version(self):
if self.connection.mysql_is_mariadb:
return (10, 6)
else:
return (8, 0, 11)
return (8, 4)

@cached_property
def test_collations(self):
Expand Down Expand Up @@ -104,24 +104,6 @@ def django_test_skips(self):
"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(
{
Expand Down Expand Up @@ -186,44 +168,16 @@ def has_zoneinfo_database(self):
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

Expand Down Expand Up @@ -262,24 +216,8 @@ def supports_expression_indexes(self):
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
Expand Down
14 changes: 5 additions & 9 deletions django/db/backends/mysql/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ def explain_query_prefix(self, format=None, **options):
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"
Expand Down Expand Up @@ -407,15 +407,11 @@ def conditional_expression_supported_in_where_clause(self, expression):
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)"
Expand Down
18 changes: 2 additions & 16 deletions django/db/backends/mysql/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,33 +65,19 @@ def skip_default(self, field):
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
# ALTER COLUMN statement.
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)"
Expand Down
2 changes: 1 addition & 1 deletion docs/ref/contrib/gis/install/index.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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+
================== ============================== ================== =========================================
Expand Down
2 changes: 1 addition & 1 deletion docs/ref/databases.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 2 additions & 3 deletions docs/ref/models/indexes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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``
----------
Expand Down
4 changes: 2 additions & 2 deletions docs/ref/models/querysets.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
6 changes: 6 additions & 0 deletions docs/releases/6.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------------

Expand Down
4 changes: 2 additions & 2 deletions tests/backends/mysql/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ def test_check_database_version_supported(self, mocked_get_database_version):
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()
Expand Down
4 changes: 1 addition & 3 deletions tests/queries/test_explain.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,7 @@ def test_mysql_text_to_traditional(self):
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:
Expand Down