1
0
mirror of https://github.com/django/django.git synced 2025-01-03 06:55:47 +00:00

Fixed #35479 -- Dropped support for PostgreSQL 13 and PostGIS 3.0.

This commit is contained in:
Mariusz Felisiak 2024-05-24 21:23:50 +02:00 committed by Sarah Boyce
parent bcbc4b9b8a
commit b049bec7cf
15 changed files with 32 additions and 103 deletions

View File

@ -90,7 +90,7 @@ jobs:
continue-on-error: true continue-on-error: true
services: services:
postgres: postgres:
image: postgres:13-alpine image: postgres:14-alpine
env: env:
POSTGRES_DB: django POSTGRES_DB: django
POSTGRES_USER: user POSTGRES_USER: user
@ -163,7 +163,7 @@ jobs:
name: Selenium tests, PostgreSQL name: Selenium tests, PostgreSQL
services: services:
postgres: postgres:
image: postgres:13-alpine image: postgres:14-alpine
env: env:
POSTGRES_DB: django POSTGRES_DB: django
POSTGRES_USER: user POSTGRES_USER: user

View File

@ -43,7 +43,7 @@ jobs:
name: PostgreSQL name: PostgreSQL
services: services:
postgres: postgres:
image: postgres:13-alpine image: postgres:14-alpine
env: env:
POSTGRES_DB: django POSTGRES_DB: django
POSTGRES_USER: user POSTGRES_USER: user

View File

@ -203,7 +203,7 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
raise ImproperlyConfigured( raise ImproperlyConfigured(
'Cannot determine PostGIS version for database "%s" ' 'Cannot determine PostGIS version for database "%s" '
'using command "SELECT postgis_lib_version()". ' 'using command "SELECT postgis_lib_version()". '
"GeoDjango requires at least PostGIS version 3.0. " "GeoDjango requires at least PostGIS version 3.1. "
"Was the database created from a spatial database " "Was the database created from a spatial database "
"template?" % self.connection.settings_dict["NAME"] "template?" % self.connection.settings_dict["NAME"]
) )

View File

@ -1,7 +1,7 @@
from types import NoneType from types import NoneType
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import DEFAULT_DB_ALIAS, NotSupportedError from django.db import DEFAULT_DB_ALIAS
from django.db.backends.ddl_references import Expressions, Statement, Table from django.db.backends.ddl_references import Expressions, Statement, Table
from django.db.models import BaseConstraint, Deferrable, F, Q from django.db.models import BaseConstraint, Deferrable, F, Q
from django.db.models.expressions import Exists, ExpressionList from django.db.models.expressions import Exists, ExpressionList
@ -114,7 +114,6 @@ class ExclusionConstraint(BaseConstraint):
) )
def create_sql(self, model, schema_editor): def create_sql(self, model, schema_editor):
self.check_supported(schema_editor)
return Statement( return Statement(
"ALTER TABLE %(table)s ADD %(constraint)s", "ALTER TABLE %(table)s ADD %(constraint)s",
table=Table(model._meta.db_table, schema_editor.quote_name), table=Table(model._meta.db_table, schema_editor.quote_name),
@ -128,17 +127,6 @@ class ExclusionConstraint(BaseConstraint):
schema_editor.quote_name(self.name), schema_editor.quote_name(self.name),
) )
def check_supported(self, schema_editor):
if (
self.include
and self.index_type.lower() == "spgist"
and not schema_editor.connection.features.supports_covering_spgist_indexes
):
raise NotSupportedError(
"Covering exclusion constraints using an SP-GiST index "
"require PostgreSQL 14+."
)
def deconstruct(self): def deconstruct(self):
path, args, kwargs = super().deconstruct() path, args, kwargs = super().deconstruct()
kwargs["expressions"] = self.expressions kwargs["expressions"] = self.expressions

View File

@ -1,4 +1,3 @@
from django.db import NotSupportedError
from django.db.models import Func, Index from django.db.models import Func, Index
from django.utils.functional import cached_property from django.utils.functional import cached_property
@ -234,13 +233,6 @@ class SpGistIndex(PostgresIndex):
with_params.append("fillfactor = %d" % self.fillfactor) with_params.append("fillfactor = %d" % self.fillfactor)
return with_params return with_params
def check_supported(self, schema_editor):
if (
self.include
and not schema_editor.connection.features.supports_covering_spgist_indexes
):
raise NotSupportedError("Covering SP-GiST indexes require PostgreSQL 14+.")
class OpClass(Func): class OpClass(Func):
template = "%(expressions)s %(name)s" template = "%(expressions)s %(name)s"

View File

@ -7,7 +7,7 @@ from django.utils.functional import cached_property
class DatabaseFeatures(BaseDatabaseFeatures): class DatabaseFeatures(BaseDatabaseFeatures):
minimum_database_version = (13,) minimum_database_version = (14,)
allows_group_by_selected_pks = True allows_group_by_selected_pks = True
can_return_columns_from_insert = True can_return_columns_from_insert = True
can_return_rows_from_bulk_insert = True can_return_rows_from_bulk_insert = True
@ -152,10 +152,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
"PositiveSmallIntegerField": "SmallIntegerField", "PositiveSmallIntegerField": "SmallIntegerField",
} }
@cached_property
def is_postgresql_14(self):
return self.connection.pg_version >= 140000
@cached_property @cached_property
def is_postgresql_15(self): def is_postgresql_15(self):
return self.connection.pg_version >= 150000 return self.connection.pg_version >= 150000
@ -164,8 +160,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
def is_postgresql_16(self): def is_postgresql_16(self):
return self.connection.pg_version >= 160000 return self.connection.pg_version >= 160000
has_bit_xor = property(operator.attrgetter("is_postgresql_14"))
supports_covering_spgist_indexes = property(operator.attrgetter("is_postgresql_14"))
supports_unlimited_charfield = True supports_unlimited_charfield = True
supports_nulls_distinct_unique_constraints = property( supports_nulls_distinct_unique_constraints = property(
operator.attrgetter("is_postgresql_15") operator.attrgetter("is_postgresql_15")

View File

@ -12,7 +12,7 @@ Program Description Required
`PROJ`_ Cartographic Projections library Yes (PostgreSQL and SQLite only) 9.x, 8.x, 7.x, 6.x `PROJ`_ Cartographic Projections library Yes (PostgreSQL and SQLite only) 9.x, 8.x, 7.x, 6.x
:doc:`GDAL <../gdal>` Geospatial Data Abstraction Library Yes 3.8, 3.7, 3.6, 3.5, 3.4, 3.3, 3.2, 3.1, 3.0 :doc:`GDAL <../gdal>` Geospatial Data Abstraction Library Yes 3.8, 3.7, 3.6, 3.5, 3.4, 3.3, 3.2, 3.1, 3.0
:doc:`GeoIP <../geoip2>` IP-based geolocation library No 2 :doc:`GeoIP <../geoip2>` IP-based geolocation library No 2
`PostGIS`__ Spatial extensions for PostgreSQL Yes (PostgreSQL only) 3.4, 3.3, 3.2, 3.1, 3.0 `PostGIS`__ Spatial extensions for PostgreSQL Yes (PostgreSQL only) 3.4, 3.3, 3.2, 3.1
`SpatiaLite`__ Spatial extensions for SQLite Yes (SQLite only) 5.1, 5.0, 4.3 `SpatiaLite`__ Spatial extensions for SQLite Yes (SQLite only) 5.1, 5.0, 4.3
======================== ==================================== ================================ =========================================== ======================== ==================================== ================================ ===========================================
@ -35,7 +35,6 @@ totally fine with GeoDjango. Your mileage may vary.
GDAL 3.6.0 2022-11-03 GDAL 3.6.0 2022-11-03
GDAL 3.7.0 2023-05-10 GDAL 3.7.0 2023-05-10
GDAL 3.8.0 2023-11-13 GDAL 3.8.0 2023-11-13
PostGIS 3.0.0 2019-10-20
PostGIS 3.1.0 2020-12-18 PostGIS 3.1.0 2020-12-18
PostGIS 3.2.0 2021-12-18 PostGIS 3.2.0 2021-12-18
PostGIS 3.3.0 2022-08-27 PostGIS 3.3.0 2022-08-27

View File

@ -56,7 +56,7 @@ supported versions, and any notes for each of the supported database backends:
================== ============================== ================== ========================================= ================== ============================== ================== =========================================
Database Library Requirements Supported Versions Notes Database Library Requirements Supported Versions Notes
================== ============================== ================== ========================================= ================== ============================== ================== =========================================
PostgreSQL GEOS, GDAL, PROJ, PostGIS 13+ Requires PostGIS. PostgreSQL GEOS, GDAL, PROJ, PostGIS 14+ Requires PostGIS.
MySQL GEOS, GDAL 8.0.11+ :ref:`Limited functionality <mysql-spatial-limitations>`. MySQL GEOS, GDAL 8.0.11+ :ref:`Limited functionality <mysql-spatial-limitations>`.
Oracle GEOS, GDAL 19+ XE not supported. Oracle GEOS, GDAL 19+ XE not supported.
SQLite GEOS, GDAL, PROJ, SpatiaLite 3.31.0+ Requires SpatiaLite 4.3+ SQLite GEOS, GDAL, PROJ, SpatiaLite 3.31.0+ Requires SpatiaLite 4.3+
@ -300,7 +300,7 @@ Summary:
.. code-block:: shell .. code-block:: shell
$ sudo port install postgresql13-server $ sudo port install postgresql14-server
$ sudo port install geos $ sudo port install geos
$ sudo port install proj6 $ sudo port install proj6
$ sudo port install postgis3 $ sudo port install postgis3
@ -314,14 +314,14 @@ Summary:
.. code-block:: shell .. code-block:: shell
export PATH=/opt/local/bin:/opt/local/lib/postgresql13/bin export PATH=/opt/local/bin:/opt/local/lib/postgresql14/bin
In addition, add the ``DYLD_FALLBACK_LIBRARY_PATH`` setting so that In addition, add the ``DYLD_FALLBACK_LIBRARY_PATH`` setting so that
the libraries can be found by Python: the libraries can be found by Python:
.. code-block:: shell .. code-block:: shell
export DYLD_FALLBACK_LIBRARY_PATH=/opt/local/lib:/opt/local/lib/postgresql13 export DYLD_FALLBACK_LIBRARY_PATH=/opt/local/lib:/opt/local/lib/postgresql14
__ https://www.macports.org/ __ https://www.macports.org/

View File

@ -14,12 +14,6 @@ All of these functions are available from the
Returns a version 4 UUID. Returns a version 4 UUID.
On PostgreSQL < 13, the `pgcrypto extension`_ must be installed. You can use
the :class:`~django.contrib.postgres.operations.CryptoExtension` migration
operation to install it.
.. _pgcrypto extension: https://www.postgresql.org/docs/current/pgcrypto.html
Usage example: Usage example:
.. code-block:: pycon .. code-block:: pycon

View File

@ -115,7 +115,7 @@ below for information on how to set up your database correctly.
PostgreSQL notes PostgreSQL notes
================ ================
Django supports PostgreSQL 13 and higher. `psycopg`_ 3.1.8+ or `psycopg2`_ Django supports PostgreSQL 14 and higher. `psycopg`_ 3.1.8+ or `psycopg2`_
2.8.4+ is required, though the latest `psycopg`_ 3.1.8+ is recommended. 2.8.4+ is required, though the latest `psycopg`_ 3.1.8+ is recommended.
.. note:: .. note::

View File

@ -238,6 +238,17 @@ backends.
* ... * ...
:mod:`django.contrib.gis`
-------------------------
* Support for PostGIS 3.0 is removed.
Dropped support for PostgreSQL 13
---------------------------------
Upstream support for PostgreSQL 13 ends in November 2025. Django 5.2 supports
PostgreSQL 14 and higher.
Miscellaneous Miscellaneous
------------- -------------

View File

@ -548,12 +548,12 @@ class Tests(TestCase):
def test_get_database_version(self): def test_get_database_version(self):
new_connection = no_pool_connection() new_connection = no_pool_connection()
new_connection.pg_version = 130009 new_connection.pg_version = 140009
self.assertEqual(new_connection.get_database_version(), (13, 9)) self.assertEqual(new_connection.get_database_version(), (14, 9))
@mock.patch.object(connection, "get_database_version", return_value=(12,)) @mock.patch.object(connection, "get_database_version", return_value=(13,))
def test_check_database_version_supported(self, mocked_get_database_version): def test_check_database_version_supported(self, mocked_get_database_version):
msg = "PostgreSQL 13 or later is required (found 12)." msg = "PostgreSQL 14 or later is required (found 13)."
with self.assertRaisesMessage(NotSupportedError, msg): with self.assertRaisesMessage(NotSupportedError, msg):
connection.check_database_version_supported() connection.check_database_version_supported()
self.assertTrue(mocked_get_database_version.called) self.assertTrue(mocked_get_database_version.called)

View File

@ -1,4 +1,4 @@
from django.db import connection, transaction from django.db import transaction
from django.db.models import ( from django.db.models import (
CharField, CharField,
F, F,
@ -13,7 +13,6 @@ from django.db.models import (
) )
from django.db.models.fields.json import KeyTextTransform, KeyTransform from django.db.models.fields.json import KeyTextTransform, KeyTransform
from django.db.models.functions import Cast, Concat, LPad, Substr from django.db.models.functions import Cast, Concat, LPad, Substr
from django.test import skipUnlessDBFeature
from django.test.utils import Approximate from django.test.utils import Approximate
from django.utils import timezone from django.utils import timezone
@ -95,9 +94,8 @@ class TestGeneralAggregate(PostgreSQLTestCase):
BoolOr("boolean_field"), BoolOr("boolean_field"),
JSONBAgg("integer_field"), JSONBAgg("integer_field"),
StringAgg("char_field", delimiter=";"), StringAgg("char_field", delimiter=";"),
BitXor("integer_field"),
] ]
if connection.features.has_bit_xor:
tests.append(BitXor("integer_field"))
for aggregation in tests: for aggregation in tests:
with self.subTest(aggregation=aggregation): with self.subTest(aggregation=aggregation):
# Empty result with non-execution optimization. # Empty result with non-execution optimization.
@ -133,9 +131,8 @@ class TestGeneralAggregate(PostgreSQLTestCase):
StringAgg("char_field", delimiter=";", default=Value("<empty>")), StringAgg("char_field", delimiter=";", default=Value("<empty>")),
"<empty>", "<empty>",
), ),
(BitXor("integer_field", default=0), 0),
] ]
if connection.features.has_bit_xor:
tests.append((BitXor("integer_field", default=0), 0))
for aggregation, expected_result in tests: for aggregation, expected_result in tests:
with self.subTest(aggregation=aggregation): with self.subTest(aggregation=aggregation):
# Empty result with non-execution optimization. # Empty result with non-execution optimization.
@ -348,7 +345,6 @@ class TestGeneralAggregate(PostgreSQLTestCase):
) )
self.assertEqual(values, {"bitor": 0}) self.assertEqual(values, {"bitor": 0})
@skipUnlessDBFeature("has_bit_xor")
def test_bit_xor_general(self): def test_bit_xor_general(self):
AggregateTestModel.objects.create(integer_field=3) AggregateTestModel.objects.create(integer_field=3)
values = AggregateTestModel.objects.filter( values = AggregateTestModel.objects.filter(
@ -356,14 +352,12 @@ class TestGeneralAggregate(PostgreSQLTestCase):
).aggregate(bitxor=BitXor("integer_field")) ).aggregate(bitxor=BitXor("integer_field"))
self.assertEqual(values, {"bitxor": 2}) self.assertEqual(values, {"bitxor": 2})
@skipUnlessDBFeature("has_bit_xor")
def test_bit_xor_on_only_true_values(self): def test_bit_xor_on_only_true_values(self):
values = AggregateTestModel.objects.filter( values = AggregateTestModel.objects.filter(
integer_field=1, integer_field=1,
).aggregate(bitxor=BitXor("integer_field")) ).aggregate(bitxor=BitXor("integer_field"))
self.assertEqual(values, {"bitxor": 1}) self.assertEqual(values, {"bitxor": 1})
@skipUnlessDBFeature("has_bit_xor")
def test_bit_xor_on_only_false_values(self): def test_bit_xor_on_only_false_values(self):
values = AggregateTestModel.objects.filter( values = AggregateTestModel.objects.filter(
integer_field=0, integer_field=0,

View File

@ -4,7 +4,7 @@ from unittest import mock
from django.contrib.postgres.indexes import OpClass from django.contrib.postgres.indexes import OpClass
from django.core.checks import Error from django.core.checks import Error
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import IntegrityError, NotSupportedError, connection, transaction from django.db import IntegrityError, connection, transaction
from django.db.models import ( from django.db.models import (
CASCADE, CASCADE,
CharField, CharField,
@ -997,7 +997,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
RangesModel.objects.create(ints=(10, 19)) RangesModel.objects.create(ints=(10, 19))
RangesModel.objects.create(ints=(51, 60)) RangesModel.objects.create(ints=(51, 60))
@skipUnlessDBFeature("supports_covering_spgist_indexes")
def test_range_adjacent_spgist_include(self): def test_range_adjacent_spgist_include(self):
constraint_name = "ints_adjacent_spgist_include" constraint_name = "ints_adjacent_spgist_include"
self.assertNotIn( self.assertNotIn(
@ -1034,7 +1033,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
editor.add_constraint(RangesModel, constraint) editor.add_constraint(RangesModel, constraint)
self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table)) self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
@skipUnlessDBFeature("supports_covering_spgist_indexes")
def test_range_adjacent_spgist_include_condition(self): def test_range_adjacent_spgist_include_condition(self):
constraint_name = "ints_adjacent_spgist_include_condition" constraint_name = "ints_adjacent_spgist_include_condition"
self.assertNotIn( self.assertNotIn(
@ -1067,7 +1065,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
editor.add_constraint(RangesModel, constraint) editor.add_constraint(RangesModel, constraint)
self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table)) self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
@skipUnlessDBFeature("supports_covering_spgist_indexes")
def test_range_adjacent_spgist_include_deferrable(self): def test_range_adjacent_spgist_include_deferrable(self):
constraint_name = "ints_adjacent_spgist_include_deferrable" constraint_name = "ints_adjacent_spgist_include_deferrable"
self.assertNotIn( self.assertNotIn(
@ -1084,27 +1081,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
editor.add_constraint(RangesModel, constraint) editor.add_constraint(RangesModel, constraint)
self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table)) self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
def test_spgist_include_not_supported(self):
constraint_name = "ints_adjacent_spgist_include_not_supported"
constraint = ExclusionConstraint(
name=constraint_name,
expressions=[("ints", RangeOperators.ADJACENT_TO)],
index_type="spgist",
include=["id"],
)
msg = (
"Covering exclusion constraints using an SP-GiST index require "
"PostgreSQL 14+."
)
with connection.schema_editor() as editor:
with mock.patch(
"django.db.backends.postgresql.features.DatabaseFeatures."
"supports_covering_spgist_indexes",
False,
):
with self.assertRaisesMessage(NotSupportedError, msg):
editor.add_constraint(RangesModel, constraint)
def test_range_adjacent_opclass(self): def test_range_adjacent_opclass(self):
constraint_name = "ints_adjacent_opclass" constraint_name = "ints_adjacent_opclass"
self.assertNotIn( self.assertNotIn(
@ -1187,7 +1163,6 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
editor.add_constraint(RangesModel, constraint) editor.add_constraint(RangesModel, constraint)
self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table)) self.assertIn(constraint_name, self.get_constraints(RangesModel._meta.db_table))
@skipUnlessDBFeature("supports_covering_spgist_indexes")
def test_range_adjacent_spgist_opclass_include(self): def test_range_adjacent_spgist_opclass_include(self):
constraint_name = "ints_adjacent_spgist_opclass_include" constraint_name = "ints_adjacent_spgist_opclass_include"
self.assertNotIn( self.assertNotIn(

View File

@ -1,5 +1,3 @@
from unittest import mock
from django.contrib.postgres.indexes import ( from django.contrib.postgres.indexes import (
BloomIndex, BloomIndex,
BrinIndex, BrinIndex,
@ -11,10 +9,9 @@ from django.contrib.postgres.indexes import (
PostgresIndex, PostgresIndex,
SpGistIndex, SpGistIndex,
) )
from django.db import NotSupportedError, connection from django.db import connection
from django.db.models import CharField, F, Index, Q from django.db.models import CharField, F, Index, Q
from django.db.models.functions import Cast, Collate, Length, Lower from django.db.models.functions import Cast, Collate, Length, Lower
from django.test import skipUnlessDBFeature
from django.test.utils import register_lookup from django.test.utils import register_lookup
from . import PostgreSQLSimpleTestCase, PostgreSQLTestCase from . import PostgreSQLSimpleTestCase, PostgreSQLTestCase
@ -640,7 +637,6 @@ class SchemaTests(PostgreSQLTestCase):
index_name, self.get_constraints(TextFieldModel._meta.db_table) index_name, self.get_constraints(TextFieldModel._meta.db_table)
) )
@skipUnlessDBFeature("supports_covering_spgist_indexes")
def test_spgist_include(self): def test_spgist_include(self):
index_name = "scene_spgist_include_setting" index_name = "scene_spgist_include_setting"
index = SpGistIndex(name=index_name, fields=["scene"], include=["setting"]) index = SpGistIndex(name=index_name, fields=["scene"], include=["setting"])
@ -654,20 +650,6 @@ class SchemaTests(PostgreSQLTestCase):
editor.remove_index(Scene, index) editor.remove_index(Scene, index)
self.assertNotIn(index_name, self.get_constraints(Scene._meta.db_table)) self.assertNotIn(index_name, self.get_constraints(Scene._meta.db_table))
def test_spgist_include_not_supported(self):
index_name = "spgist_include_exception"
index = SpGistIndex(fields=["scene"], name=index_name, include=["setting"])
msg = "Covering SP-GiST indexes require PostgreSQL 14+."
with self.assertRaisesMessage(NotSupportedError, msg):
with mock.patch(
"django.db.backends.postgresql.features.DatabaseFeatures."
"supports_covering_spgist_indexes",
False,
):
with connection.schema_editor() as editor:
editor.add_index(Scene, index)
self.assertNotIn(index_name, self.get_constraints(Scene._meta.db_table))
def test_custom_suffix(self): def test_custom_suffix(self):
class CustomSuffixIndex(PostgresIndex): class CustomSuffixIndex(PostgresIndex):
suffix = "sfx" suffix = "sfx"