From 95a101a690402076321331d56e67d896e8f6dfbc Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Thu, 8 Dec 2022 05:53:18 +0100 Subject: [PATCH] Fixed #34201 -- Bumped minimum supported SQLite to 3.21.0. --- django/db/backends/sqlite3/base.py | 142 ++++++++----------------- django/db/backends/sqlite3/features.py | 8 +- docs/ref/contrib/gis/install/index.txt | 2 +- docs/ref/databases.txt | 2 +- docs/releases/4.2.txt | 2 + tests/backends/sqlite/tests.py | 4 +- 6 files changed, 54 insertions(+), 106 deletions(-) diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index cc46434f99..a3a382a56b 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -239,103 +239,53 @@ class DatabaseWrapper(BaseDatabaseWrapper): determine if rows with invalid references were entered while constraint checks were off. """ - if self.features.supports_pragma_foreign_key_check: - with self.cursor() as cursor: - if table_names is None: - violations = cursor.execute("PRAGMA foreign_key_check").fetchall() - else: - violations = chain.from_iterable( - cursor.execute( - "PRAGMA foreign_key_check(%s)" - % self.ops.quote_name(table_name) - ).fetchall() - for table_name in table_names - ) - # See https://www.sqlite.org/pragma.html#pragma_foreign_key_check - for ( - table_name, - rowid, - referenced_table_name, - foreign_key_index, - ) in violations: - foreign_key = cursor.execute( - "PRAGMA foreign_key_list(%s)" % self.ops.quote_name(table_name) - ).fetchall()[foreign_key_index] - column_name, referenced_column_name = foreign_key[3:5] - primary_key_column_name = self.introspection.get_primary_key_column( - cursor, table_name - ) - primary_key_value, bad_value = cursor.execute( - "SELECT %s, %s FROM %s WHERE rowid = %%s" - % ( - self.ops.quote_name(primary_key_column_name), - self.ops.quote_name(column_name), - self.ops.quote_name(table_name), - ), - (rowid,), - ).fetchone() - raise IntegrityError( - "The row in table '%s' with primary key '%s' has an " - "invalid foreign key: %s.%s contains a value '%s' that " - "does not have a corresponding value in %s.%s." - % ( - table_name, - primary_key_value, - table_name, - column_name, - bad_value, - referenced_table_name, - referenced_column_name, - ) - ) - else: - with self.cursor() as cursor: - if table_names is None: - table_names = self.introspection.table_names(cursor) - for table_name in table_names: - primary_key_column_name = self.introspection.get_primary_key_column( - cursor, table_name - ) - if not primary_key_column_name: - continue - relations = self.introspection.get_relations(cursor, table_name) - for column_name, ( - referenced_column_name, + with self.cursor() as cursor: + if table_names is None: + violations = cursor.execute("PRAGMA foreign_key_check").fetchall() + else: + violations = chain.from_iterable( + cursor.execute( + "PRAGMA foreign_key_check(%s)" % self.ops.quote_name(table_name) + ).fetchall() + for table_name in table_names + ) + # See https://www.sqlite.org/pragma.html#pragma_foreign_key_check + for ( + table_name, + rowid, + referenced_table_name, + foreign_key_index, + ) in violations: + foreign_key = cursor.execute( + "PRAGMA foreign_key_list(%s)" % self.ops.quote_name(table_name) + ).fetchall()[foreign_key_index] + column_name, referenced_column_name = foreign_key[3:5] + primary_key_column_name = self.introspection.get_primary_key_column( + cursor, table_name + ) + primary_key_value, bad_value = cursor.execute( + "SELECT %s, %s FROM %s WHERE rowid = %%s" + % ( + self.ops.quote_name(primary_key_column_name), + self.ops.quote_name(column_name), + self.ops.quote_name(table_name), + ), + (rowid,), + ).fetchone() + raise IntegrityError( + "The row in table '%s' with primary key '%s' has an " + "invalid foreign key: %s.%s contains a value '%s' that " + "does not have a corresponding value in %s.%s." + % ( + table_name, + primary_key_value, + table_name, + column_name, + bad_value, referenced_table_name, - ) in relations.items(): - cursor.execute( - """ - SELECT REFERRING.`%s`, REFERRING.`%s` FROM `%s` as REFERRING - LEFT JOIN `%s` as REFERRED - ON (REFERRING.`%s` = REFERRED.`%s`) - WHERE REFERRING.`%s` IS NOT NULL AND REFERRED.`%s` IS NULL - """ - % ( - primary_key_column_name, - column_name, - table_name, - referenced_table_name, - column_name, - referenced_column_name, - column_name, - referenced_column_name, - ) - ) - for bad_row in cursor.fetchall(): - raise IntegrityError( - "The row in table '%s' with primary key '%s' has an " - "invalid foreign key: %s.%s contains a value '%s' that " - "does not have a corresponding value in %s.%s." - % ( - table_name, - bad_row[0], - table_name, - column_name, - bad_row[1], - referenced_table_name, - referenced_column_name, - ) - ) + referenced_column_name, + ) + ) def is_usable(self): return True diff --git a/django/db/backends/sqlite3/features.py b/django/db/backends/sqlite3/features.py index 60b9b7d3a0..7dd1c39702 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, 9) + minimum_database_version = (3, 21) test_db_allows_multiple_connections = False supports_unspecified_pk = True supports_timezones = False @@ -31,11 +31,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): # Is "ALTER TABLE ... DROP COLUMN" supported? can_alter_table_drop_column = Database.sqlite_version_info >= (3, 35, 5) supports_parentheses_in_compound = False - # Deferred constraint checks can be emulated on SQLite < 3.20 but not in a - # reasonably performant way. - supports_pragma_foreign_key_check = Database.sqlite_version_info >= (3, 20, 0) - can_defer_constraint_checks = supports_pragma_foreign_key_check - supports_functions_in_partial_indexes = Database.sqlite_version_info >= (3, 15, 0) + can_defer_constraint_checks = True supports_over_clause = Database.sqlite_version_info >= (3, 25, 0) supports_frame_range_fixed_distance = Database.sqlite_version_info >= (3, 28, 0) supports_aggregate_filter_clause = Database.sqlite_version_info >= (3, 30, 1) diff --git a/docs/ref/contrib/gis/install/index.txt b/docs/ref/contrib/gis/install/index.txt index 629a070bb8..625b3aabad 100644 --- a/docs/ref/contrib/gis/install/index.txt +++ b/docs/ref/contrib/gis/install/index.txt @@ -61,7 +61,7 @@ Database Library Requirements Supported Versions Notes PostgreSQL GEOS, GDAL, PROJ, PostGIS 12+ Requires PostGIS. MySQL GEOS, GDAL 8+ :ref:`Limited functionality `. Oracle GEOS, GDAL 19+ XE not supported. -SQLite GEOS, GDAL, PROJ, SpatiaLite 3.9.0+ Requires SpatiaLite 4.3+ +SQLite GEOS, GDAL, PROJ, SpatiaLite 3.21.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 19d1acc776..79b0386e1a 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -730,7 +730,7 @@ appropriate typecasting. SQLite notes ============ -Django supports SQLite 3.9.0 and later. +Django supports SQLite 3.21.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/releases/4.2.txt b/docs/releases/4.2.txt index 77d4c8594c..d378162537 100644 --- a/docs/releases/4.2.txt +++ b/docs/releases/4.2.txt @@ -407,6 +407,8 @@ Miscellaneous * The ``is_summary`` argument of the undocumented ``Query.add_annotation()`` method is removed. +* The minimum supported version of SQLite is increased from 3.9.0 to 3.21.0. + .. _deprecated-features-4.2: Features deprecated in 4.2 diff --git a/tests/backends/sqlite/tests.py b/tests/backends/sqlite/tests.py index 0669d26191..c83cde60dd 100644 --- a/tests/backends/sqlite/tests.py +++ b/tests/backends/sqlite/tests.py @@ -106,9 +106,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, 8)) + @mock.patch.object(connection, "get_database_version", return_value=(3, 20)) def test_check_database_version_supported(self, mocked_get_database_version): - msg = "SQLite 3.9 or later is required (found 3.8)." + msg = "SQLite 3.21 or later is required (found 3.20)." with self.assertRaisesMessage(NotSupportedError, msg): connection.check_database_version_supported() self.assertTrue(mocked_get_database_version.called)