Fixed #34760 -- Dropped support for SQLite < 3.27.

This commit is contained in:
Mariusz Felisiak 2023-08-04 06:35:13 +02:00 committed by GitHub
parent f46a6b2816
commit 2b582387d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 30 additions and 212 deletions

View File

@ -134,9 +134,7 @@ class SpatialiteSchemaEditor(DatabaseSchemaEditor):
else:
super().remove_field(model, field)
def alter_db_table(
self, model, old_db_table, new_db_table, disable_constraints=True
):
def alter_db_table(self, model, old_db_table, new_db_table):
from django.contrib.gis.db.models import GeometryField
if old_db_table == new_db_table or (
@ -155,7 +153,7 @@ class SpatialiteSchemaEditor(DatabaseSchemaEditor):
}
)
# Alter table
super().alter_db_table(model, old_db_table, new_db_table, disable_constraints)
super().alter_db_table(model, old_db_table, new_db_table)
# Repoint any straggler names
for geom_table in self.geometry_tables:
try:

View File

@ -174,9 +174,6 @@ class BaseDatabaseFeatures:
schema_editor_uses_clientside_param_binding = False
# Does it support operations requiring references rename in a transaction?
supports_atomic_references_rename = True
# Can we issue more than one ALTER COLUMN clause in an ALTER TABLE?
supports_combined_alters = False

View File

@ -182,7 +182,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
conn.execute("PRAGMA foreign_keys = ON")
# The macOS bundled SQLite defaults legacy_alter_table ON, which
# prevents atomic table renames (feature supports_atomic_references_rename)
# prevents atomic table renames.
conn.execute("PRAGMA legacy_alter_table = OFF")
return conn

View File

@ -9,7 +9,7 @@ from .base import Database
class DatabaseFeatures(BaseDatabaseFeatures):
minimum_database_version = (3, 21)
minimum_database_version = (3, 27)
test_db_allows_multiple_connections = False
supports_unspecified_pk = True
supports_timezones = False
@ -26,13 +26,11 @@ class DatabaseFeatures(BaseDatabaseFeatures):
time_cast_precision = 3
can_release_savepoints = True
has_case_insensitive_like = True
# Is "ALTER TABLE ... RENAME COLUMN" supported?
can_alter_table_rename_column = Database.sqlite_version_info >= (3, 25, 0)
# Is "ALTER TABLE ... DROP COLUMN" supported?
can_alter_table_drop_column = Database.sqlite_version_info >= (3, 35, 5)
supports_parentheses_in_compound = False
can_defer_constraint_checks = True
supports_over_clause = Database.sqlite_version_info >= (3, 25, 0)
supports_over_clause = True
supports_frame_range_fixed_distance = Database.sqlite_version_info >= (3, 28, 0)
supports_aggregate_filter_clause = Database.sqlite_version_info >= (3, 30, 1)
supports_order_by_nulls_modifier = Database.sqlite_version_info >= (3, 30, 0)
@ -40,8 +38,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
requires_compound_order_by_subquery = Database.sqlite_version_info < (3, 30)
order_by_nulls_first = True
supports_json_field_contains = False
supports_update_conflicts = Database.sqlite_version_info >= (3, 24, 0)
supports_update_conflicts_with_target = supports_update_conflicts
supports_update_conflicts = True
supports_update_conflicts_with_target = True
test_collations = {
"ci": "nocase",
"cs": "binary",
@ -88,15 +86,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
"test_integer_with_negative_precision",
},
}
if Database.sqlite_version_info < (3, 27):
skips.update(
{
"Nondeterministic failure on SQLite < 3.27.": {
"expressions_window.tests.WindowFunctionTests."
"test_subquery_row_range_rank",
},
}
)
if self.connection.is_in_memory_db():
skips.update(
{
@ -131,10 +120,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
)
return skips
@cached_property
def supports_atomic_references_rename(self):
return Database.sqlite_version_info >= (3, 26, 0)
@cached_property
def introspected_field_types(self):
return {

View File

@ -7,7 +7,6 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.backends.ddl_references import Statement
from django.db.backends.utils import strip_quotes
from django.db.models import NOT_PROVIDED, UniqueConstraint
from django.db.transaction import atomic
class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
@ -73,105 +72,6 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
def prepare_default(self, value):
return self.quote_value(value)
def _is_referenced_by_fk_constraint(
self, table_name, column_name=None, ignore_self=False
):
"""
Return whether or not the provided table name is referenced by another
one. If `column_name` is specified, only references pointing to that
column are considered. If `ignore_self` is True, self-referential
constraints are ignored.
"""
with self.connection.cursor() as cursor:
for other_table in self.connection.introspection.get_table_list(cursor):
if ignore_self and other_table.name == table_name:
continue
relations = self.connection.introspection.get_relations(
cursor, other_table.name
)
for constraint_column, constraint_table in relations.values():
if constraint_table == table_name and (
column_name is None or constraint_column == column_name
):
return True
return False
def alter_db_table(
self, model, old_db_table, new_db_table, disable_constraints=True
):
if (
not self.connection.features.supports_atomic_references_rename
and disable_constraints
and self._is_referenced_by_fk_constraint(old_db_table)
):
if self.connection.in_atomic_block:
raise NotSupportedError(
(
"Renaming the %r table while in a transaction is not "
"supported on SQLite < 3.26 because it would break referential "
"integrity. Try adding `atomic = False` to the Migration class."
)
% old_db_table
)
self.connection.enable_constraint_checking()
super().alter_db_table(model, old_db_table, new_db_table)
self.connection.disable_constraint_checking()
else:
super().alter_db_table(model, old_db_table, new_db_table)
def alter_field(self, model, old_field, new_field, strict=False):
if not self._field_should_be_altered(old_field, new_field):
return
old_field_name = old_field.name
table_name = model._meta.db_table
_, old_column_name = old_field.get_attname_column()
if (
new_field.name != old_field_name
and not self.connection.features.supports_atomic_references_rename
and self._is_referenced_by_fk_constraint(
table_name, old_column_name, ignore_self=True
)
):
if self.connection.in_atomic_block:
raise NotSupportedError(
(
"Renaming the %r.%r column while in a transaction is not "
"supported on SQLite < 3.26 because it would break referential "
"integrity. Try adding `atomic = False` to the Migration class."
)
% (model._meta.db_table, old_field_name)
)
with atomic(self.connection.alias):
super().alter_field(model, old_field, new_field, strict=strict)
# Follow SQLite's documented procedure for performing changes
# that don't affect the on-disk content.
# https://sqlite.org/lang_altertable.html#otheralter
with self.connection.cursor() as cursor:
schema_version = cursor.execute("PRAGMA schema_version").fetchone()[
0
]
cursor.execute("PRAGMA writable_schema = 1")
references_template = ' REFERENCES "%s" ("%%s") ' % table_name
new_column_name = new_field.get_attname_column()[1]
search = references_template % old_column_name
replacement = references_template % new_column_name
cursor.execute(
"UPDATE sqlite_master SET sql = replace(sql, %s, %s)",
(search, replacement),
)
cursor.execute("PRAGMA schema_version = %d" % (schema_version + 1))
cursor.execute("PRAGMA writable_schema = 0")
# The integrity check will raise an exception and rollback
# the transaction if the sqlite_master updates corrupt the
# database.
cursor.execute("PRAGMA integrity_check")
# Perform a VACUUM to refresh the database representation from
# the sqlite_master table.
with self.connection.cursor() as cursor:
cursor.execute("VACUUM")
else:
super().alter_field(model, old_field, new_field, strict=strict)
def _remake_table(
self, model, create_field=None, delete_field=None, alter_fields=None
):
@ -358,7 +258,6 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
new_model,
new_model._meta.db_table,
model._meta.db_table,
disable_constraints=False,
)
# Run deferred SQL on correct table
@ -458,8 +357,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
# Use "ALTER TABLE ... RENAME COLUMN" if only the column name
# changed and there aren't any constraints.
if (
self.connection.features.can_alter_table_rename_column
and old_field.column != new_field.column
old_field.column != new_field.column
and self.column_sql(model, old_field) == self.column_sql(model, new_field)
and not (
old_field.remote_field

View File

@ -59,7 +59,7 @@ Database Library Requirements Supported Versions Notes
PostgreSQL GEOS, GDAL, PROJ, PostGIS 12+ Requires PostGIS.
MySQL GEOS, GDAL 8.0.11+ :ref:`Limited functionality <mysql-spatial-limitations>`.
Oracle GEOS, GDAL 19+ XE not supported.
SQLite GEOS, GDAL, PROJ, SpatiaLite 3.21.0+ Requires SpatiaLite 4.3+
SQLite GEOS, GDAL, PROJ, SpatiaLite 3.27.0+ Requires SpatiaLite 4.3+
================== ============================== ================== =========================================
See also `this comparison matrix`__ on the OSGeo Wiki for

View File

@ -792,7 +792,7 @@ appropriate typecasting.
SQLite notes
============
Django supports SQLite 3.21.0 and later.
Django supports SQLite 3.27.0 and later.
SQLite_ provides an excellent development alternative for applications that
are predominantly read-only or require a smaller installation footprint. As

View File

@ -2411,7 +2411,7 @@ On databases that support it (all but Oracle), setting the ``ignore_conflicts``
parameter to ``True`` tells the database to ignore failure to insert any rows
that fail constraints such as duplicate unique values.
On databases that support it (all except Oracle and SQLite < 3.24), setting the
On databases that support it (all except Oracle), setting the
``update_conflicts`` parameter to ``True``, tells the database to update
``update_fields`` when a row insertion fails on conflicts. On PostgreSQL and
SQLite, in addition to ``update_fields``, a list of ``unique_fields`` that may

View File

@ -566,6 +566,8 @@ Miscellaneous
* The ``AlreadyRegistered`` and ``NotRegistered`` exceptions are moved from
``django.contrib.admin.sites`` to ``django.contrib.admin.exceptions``.
* The minimum supported version of SQLite is increased from 3.21.0 to 3.27.0.
.. _deprecated-features-5.0:
Features deprecated in 5.0

View File

@ -7,17 +7,12 @@ from pathlib import Path
from unittest import mock
from django.db import NotSupportedError, connection, transaction
from django.db.models import Aggregate, Avg, CharField, StdDev, Sum, Variance
from django.db.models import Aggregate, Avg, StdDev, Sum, Variance
from django.db.utils import ConnectionHandler
from django.test import (
TestCase,
TransactionTestCase,
override_settings,
skipIfDBFeature,
)
from django.test import TestCase, TransactionTestCase, override_settings
from django.test.utils import isolate_apps
from ..models import Author, Item, Object, Square
from ..models import Item, Object, Square
@unittest.skipUnless(connection.vendor == "sqlite", "SQLite tests")
@ -106,9 +101,9 @@ class Tests(TestCase):
connections["default"].close()
self.assertTrue(os.path.isfile(os.path.join(tmp, "test.db")))
@mock.patch.object(connection, "get_database_version", return_value=(3, 20))
@mock.patch.object(connection, "get_database_version", return_value=(3, 26))
def test_check_database_version_supported(self, mocked_get_database_version):
msg = "SQLite 3.21 or later is required (found 3.20)."
msg = "SQLite 3.27 or later is required (found 3.26)."
with self.assertRaisesMessage(NotSupportedError, msg):
connection.check_database_version_supported()
self.assertTrue(mocked_get_database_version.called)
@ -167,39 +162,6 @@ class SchemaTests(TransactionTestCase):
self.assertFalse(constraint_checks_enabled())
self.assertTrue(constraint_checks_enabled())
@skipIfDBFeature("supports_atomic_references_rename")
def test_field_rename_inside_atomic_block(self):
"""
NotImplementedError is raised when a model field rename is attempted
inside an atomic block.
"""
new_field = CharField(max_length=255, unique=True)
new_field.set_attributes_from_name("renamed")
msg = (
"Renaming the 'backends_author'.'name' column while in a "
"transaction is not supported on SQLite < 3.26 because it would "
"break referential integrity. Try adding `atomic = False` to the "
"Migration class."
)
with self.assertRaisesMessage(NotSupportedError, msg):
with connection.schema_editor(atomic=True) as editor:
editor.alter_field(Author, Author._meta.get_field("name"), new_field)
@skipIfDBFeature("supports_atomic_references_rename")
def test_table_rename_inside_atomic_block(self):
"""
NotImplementedError is raised when a table rename is attempted inside
an atomic block.
"""
msg = (
"Renaming the 'backends_author' table while in a transaction is "
"not supported on SQLite < 3.26 because it would break referential "
"integrity. Try adding `atomic = False` to the Migration class."
)
with self.assertRaisesMessage(NotSupportedError, msg):
with connection.schema_editor(atomic=True) as editor:
editor.alter_db_table(Author, "backends_author", "renamed_table")
@unittest.skipUnless(connection.vendor == "sqlite", "Test only for SQLite")
@override_settings(DEBUG=True)

View File

@ -805,10 +805,7 @@ class OperationTests(OperationTestBase):
)
# Migrate forwards
new_state = project_state.clone()
atomic_rename = connection.features.supports_atomic_references_rename
new_state = self.apply_operations(
"test_rnmo", new_state, [operation], atomic=atomic_rename
)
new_state = self.apply_operations("test_rnmo", new_state, [operation])
# Test new state and database
self.assertNotIn(("test_rnmo", "pony"), new_state.models)
self.assertIn(("test_rnmo", "horse"), new_state.models)
@ -828,7 +825,7 @@ class OperationTests(OperationTestBase):
)
# Migrate backwards
original_state = self.unapply_operations(
"test_rnmo", project_state, [operation], atomic=atomic_rename
"test_rnmo", project_state, [operation]
)
# Test original state and database
self.assertIn(("test_rnmo", "pony"), original_state.models)
@ -907,8 +904,7 @@ class OperationTests(OperationTestBase):
self.assertFKNotExists(
"test_rmwsrf_rider", ["friend_id"], ("test_rmwsrf_horserider", "id")
)
atomic_rename = connection.features.supports_atomic_references_rename
with connection.schema_editor(atomic=atomic_rename) as editor:
with connection.schema_editor() as editor:
operation.database_forwards("test_rmwsrf", editor, project_state, new_state)
self.assertTableNotExists("test_rmwsrf_rider")
self.assertTableExists("test_rmwsrf_horserider")
@ -922,7 +918,7 @@ class OperationTests(OperationTestBase):
("test_rmwsrf_horserider", "id"),
)
# And test reversal
with connection.schema_editor(atomic=atomic_rename) as editor:
with connection.schema_editor() as editor:
operation.database_backwards(
"test_rmwsrf", editor, new_state, project_state
)
@ -972,9 +968,7 @@ class OperationTests(OperationTestBase):
self.assertFKNotExists(
"test_rmwsc_rider", ["pony_id"], ("test_rmwsc_shetlandpony", "id")
)
with connection.schema_editor(
atomic=connection.features.supports_atomic_references_rename
) as editor:
with connection.schema_editor() as editor:
operation.database_forwards("test_rmwsc", editor, project_state, new_state)
# Now we have a little horse table, not shetland pony
self.assertTableNotExists("test_rmwsc_shetlandpony")
@ -1031,7 +1025,6 @@ class OperationTests(OperationTestBase):
operations=[
migrations.RenameModel("ReflexivePony", "ReflexivePony2"),
],
atomic=connection.features.supports_atomic_references_rename,
)
Pony = project_state.apps.get_model(app_label, "ReflexivePony2")
pony = Pony.objects.create()
@ -1070,7 +1063,6 @@ class OperationTests(OperationTestBase):
operations=[
migrations.RenameModel("Pony", "Pony2"),
],
atomic=connection.features.supports_atomic_references_rename,
)
Pony = project_state.apps.get_model(app_label, "Pony2")
Rider = project_state.apps.get_model(app_label, "Rider")
@ -1125,7 +1117,6 @@ class OperationTests(OperationTestBase):
app_label_2,
project_state,
operations=[migrations.RenameModel("Rider", "Pony")],
atomic=connection.features.supports_atomic_references_rename,
)
m2m_table = f"{app_label_2}_pony_riders"
@ -1146,7 +1137,6 @@ class OperationTests(OperationTestBase):
app_label_2,
project_state_2,
operations=[migrations.RenameModel("Rider", "Pony")],
atomic=connection.features.supports_atomic_references_rename,
)
m2m_table = f"{app_label_2}_rider_riders"
self.assertColumnExists(m2m_table, "to_rider_id")
@ -1178,7 +1168,6 @@ class OperationTests(OperationTestBase):
app_label,
project_state,
operations=[migrations.RenameModel("Pony", "PinkPony")],
atomic=connection.features.supports_atomic_references_rename,
)
Pony = new_state.apps.get_model(app_label, "PinkPony")
Rider = new_state.apps.get_model(app_label, "Rider")
@ -1219,7 +1208,6 @@ class OperationTests(OperationTestBase):
operations=[
migrations.RenameModel("Rider", "Rider2"),
],
atomic=connection.features.supports_atomic_references_rename,
)
Pony = project_state.apps.get_model(app_label, "Pony")
Rider = project_state.apps.get_model(app_label, "Rider2")
@ -1341,7 +1329,6 @@ class OperationTests(OperationTestBase):
),
migrations.RenameModel(old_name="Rider", new_name="Jockey"),
],
atomic=connection.features.supports_atomic_references_rename,
)
Pony = project_state.apps.get_model(app_label, "Pony")
Jockey = project_state.apps.get_model(app_label, "Jockey")
@ -2042,13 +2029,12 @@ class OperationTests(OperationTestBase):
second_state = first_state.clone()
operation = migrations.AlterModelTable(name="pony", table=None)
operation.state_forwards(app_label, second_state)
atomic_rename = connection.features.supports_atomic_references_rename
with connection.schema_editor(atomic=atomic_rename) as editor:
with connection.schema_editor() as editor:
operation.database_forwards(app_label, editor, first_state, second_state)
self.assertTableExists(new_m2m_table)
self.assertTableNotExists(original_m2m_table)
# And test reversal
with connection.schema_editor(atomic=atomic_rename) as editor:
with connection.schema_editor() as editor:
operation.database_backwards(app_label, editor, second_state, first_state)
self.assertTableExists(original_m2m_table)
self.assertTableNotExists(new_m2m_table)
@ -2988,7 +2974,6 @@ class OperationTests(OperationTestBase):
"Pony", "id", models.CharField(primary_key=True, max_length=99)
),
],
atomic=connection.features.supports_atomic_references_rename,
)
def test_rename_field(self):

View File

@ -2075,9 +2075,7 @@ class SchemaTests(TransactionTestCase):
editor.create_model(Book)
new_field = CharField(max_length=255, unique=True)
new_field.set_attributes_from_name("renamed")
with connection.schema_editor(
atomic=connection.features.supports_atomic_references_rename
) as editor:
with connection.schema_editor() as editor:
editor.alter_field(Author, Author._meta.get_field("name"), new_field)
# Ensure the foreign key reference was updated.
self.assertForeignKeyExists(Book, "author_id", "schema_author", "renamed")
@ -2122,9 +2120,7 @@ class SchemaTests(TransactionTestCase):
new_field = IntegerField(db_default=1985)
new_field.set_attributes_from_name("renamed_year")
new_field.model = AuthorDbDefault
with connection.schema_editor(
atomic=connection.features.supports_atomic_references_rename
) as editor:
with connection.schema_editor() as editor:
editor.alter_field(AuthorDbDefault, old_field, new_field, strict=True)
columns = self.column_classes(AuthorDbDefault)
self.assertEqual(columns["renamed_year"][1].default, "1985")
@ -3550,9 +3546,7 @@ class SchemaTests(TransactionTestCase):
connection.features.introspected_field_types["CharField"],
)
# Alter the table
with connection.schema_editor(
atomic=connection.features.supports_atomic_references_rename
) as editor:
with connection.schema_editor() as editor:
editor.alter_db_table(Author, "schema_author", "schema_otherauthor")
Author._meta.db_table = "schema_otherauthor"
columns = self.column_classes(Author)
@ -3563,9 +3557,7 @@ class SchemaTests(TransactionTestCase):
# Ensure the foreign key reference was updated
self.assertForeignKeyExists(Book, "author_id", "schema_otherauthor")
# Alter the table again
with connection.schema_editor(
atomic=connection.features.supports_atomic_references_rename
) as editor:
with connection.schema_editor() as editor:
editor.alter_db_table(Author, "schema_otherauthor", "schema_author")
# Ensure the table is still there
Author._meta.db_table = "schema_author"
@ -5130,8 +5122,7 @@ class SchemaTests(TransactionTestCase):
editor.add_field(Book, author)
def test_rename_table_renames_deferred_sql_references(self):
atomic_rename = connection.features.supports_atomic_references_rename
with connection.schema_editor(atomic=atomic_rename) as editor:
with connection.schema_editor() as editor:
editor.create_model(Author)
editor.create_model(Book)
editor.alter_db_table(Author, "schema_author", "schema_renamed_author")