From 2b582387d51c44fa928351ca55f05fc8b8d2986e Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Fri, 4 Aug 2023 06:35:13 +0200 Subject: [PATCH] Fixed #34760 -- Dropped support for SQLite < 3.27. --- .../gis/db/backends/spatialite/schema.py | 6 +- django/db/backends/base/features.py | 3 - django/db/backends/sqlite3/base.py | 2 +- django/db/backends/sqlite3/features.py | 23 +--- django/db/backends/sqlite3/schema.py | 104 +----------------- docs/ref/contrib/gis/install/index.txt | 2 +- docs/ref/databases.txt | 2 +- docs/ref/models/querysets.txt | 2 +- docs/releases/5.0.txt | 2 + tests/backends/sqlite/tests.py | 48 +------- tests/migrations/test_operations.py | 29 ++--- tests/schema/tests.py | 19 +--- 12 files changed, 30 insertions(+), 212 deletions(-) diff --git a/django/contrib/gis/db/backends/spatialite/schema.py b/django/contrib/gis/db/backends/spatialite/schema.py index d75d7671e5..fb2c5690de 100644 --- a/django/contrib/gis/db/backends/spatialite/schema.py +++ b/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: diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py index 79abad82cf..06945732a0 100644 --- a/django/db/backends/base/features.py +++ b/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 diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index a3a382a56b..08de0bad5a 100644 --- a/django/db/backends/sqlite3/base.py +++ b/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 diff --git a/django/db/backends/sqlite3/features.py b/django/db/backends/sqlite3/features.py index f471b72cb2..3ae84f2646 100644 --- a/django/db/backends/sqlite3/features.py +++ b/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 { diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py index 46ba07092d..ec128fd733 100644 --- a/django/db/backends/sqlite3/schema.py +++ b/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 diff --git a/docs/ref/contrib/gis/install/index.txt b/docs/ref/contrib/gis/install/index.txt index b207094dbb..c5d03eee0d 100644 --- a/docs/ref/contrib/gis/install/index.txt +++ b/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 `. 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 diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index aa55446607..6e9cb63fda 100644 --- a/docs/ref/databases.txt +++ b/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 diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 1c73a7f085..106a5e8a17 100644 --- a/docs/ref/models/querysets.txt +++ b/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 diff --git a/docs/releases/5.0.txt b/docs/releases/5.0.txt index b468e19836..030e16a054 100644 --- a/docs/releases/5.0.txt +++ b/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 diff --git a/tests/backends/sqlite/tests.py b/tests/backends/sqlite/tests.py index 88b514270a..2b728f8409 100644 --- a/tests/backends/sqlite/tests.py +++ b/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) diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index 58213ff642..617bd3d7b0 100644 --- a/tests/migrations/test_operations.py +++ b/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): diff --git a/tests/schema/tests.py b/tests/schema/tests.py index aff7b08bd9..9f620331bc 100644 --- a/tests/schema/tests.py +++ b/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")