Skip to content

Commit

Permalink
Fixed #34760 -- Dropped support for SQLite < 3.27.
Browse files Browse the repository at this point in the history
  • Loading branch information
felixxm authored Aug 4, 2023
1 parent f46a6b2 commit 2b58238
Show file tree
Hide file tree
Showing 12 changed files with 30 additions and 212 deletions.
6 changes: 2 additions & 4 deletions django/contrib/gis/db/backends/spatialite/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,7 @@ def remove_field(self, model, field):
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 (
Expand All @@ -155,7 +153,7 @@ def alter_db_table(
}
)
# 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:
Expand Down
3 changes: 0 additions & 3 deletions django/db/backends/base/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion django/db/backends/sqlite3/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def get_new_connection(self, conn_params):

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

Expand Down
23 changes: 4 additions & 19 deletions django/db/backends/sqlite3/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@


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
Expand All @@ -26,22 +26,20 @@ 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)
# NULLS LAST/FIRST emulation on < 3.30 requires subquery wrapping.
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",
Expand Down Expand Up @@ -88,15 +86,6 @@ def django_test_skips(self):
"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(
{
Expand Down Expand Up @@ -131,10 +120,6 @@ def django_test_skips(self):
)
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 {
Expand Down
104 changes: 1 addition & 103 deletions django/db/backends/sqlite3/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
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):
Expand Down Expand Up @@ -73,105 +72,6 @@ def quote_value(self, value):
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
):
Expand Down Expand Up @@ -358,7 +258,6 @@ def is_self_referential(f):
new_model,
new_model._meta.db_table,
model._meta.db_table,
disable_constraints=False,
)

# Run deferred SQL on correct table
Expand Down Expand Up @@ -458,8 +357,7 @@ def _alter_field(
# 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
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 @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/ref/databases.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/ref/models/querysets.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/releases/5.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 5 additions & 43 deletions tests/backends/sqlite/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,12 @@
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")
Expand Down Expand Up @@ -106,9 +101,9 @@ def test_pathlib_name(self):
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)
Expand Down Expand Up @@ -167,39 +162,6 @@ def constraint_checks_enabled():
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)
Expand Down
Loading

0 comments on commit 2b58238

Please sign in to comment.