Browse Source

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

Mariusz Felisiak 1 year ago
parent
commit
2b582387d5

+ 2 - 4
django/contrib/gis/db/backends/spatialite/schema.py

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

+ 0 - 3
django/db/backends/base/features.py

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

+ 1 - 1
django/db/backends/sqlite3/base.py

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

+ 4 - 19
django/db/backends/sqlite3/features.py

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

+ 1 - 103
django/db/backends/sqlite3/schema.py

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

+ 1 - 1
docs/ref/contrib/gis/install/index.txt

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

+ 1 - 1
docs/ref/databases.txt

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

+ 1 - 1
docs/ref/models/querysets.txt

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

+ 2 - 0
docs/releases/5.0.txt

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

+ 5 - 43
tests/backends/sqlite/tests.py

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

+ 7 - 22
tests/migrations/test_operations.py

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

+ 5 - 14
tests/schema/tests.py

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