From e20e1890450011693df845394e0a133a202b5466 Mon Sep 17 00:00:00 2001 From: David Smith Date: Sun, 29 Sep 2024 16:53:46 +0100 Subject: [PATCH] Refs #33783 -- Added IsEmpty GIS database function and __isempty lookup on SpatiaLite. --- .../gis/db/backends/spatialite/operations.py | 3 ++- django/contrib/gis/db/models/functions.py | 4 ++++ docs/ref/contrib/gis/db-api.txt | 4 ++-- docs/ref/contrib/gis/functions.txt | 7 ++++++- docs/ref/contrib/gis/geoquerysets.txt | 7 ++++++- docs/releases/6.1.txt | 4 +++- tests/gis_tests/geoapp/test_functions.py | 14 +++++++++++++- 7 files changed, 36 insertions(+), 7 deletions(-) diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index e9321ee2a3..54ec249f07 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/django/contrib/gis/db/backends/spatialite/operations.py @@ -73,6 +73,7 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations): "ForcePolygonCW": "ST_ForceLHR", "FromWKB": "ST_GeomFromWKB", "FromWKT": "ST_GeomFromText", + "IsEmpty": "ST_IsEmpty", "Length": "ST_Length", "LineLocatePoint": "ST_Line_Locate_Point", "NumPoints": "ST_NPoints", @@ -84,7 +85,7 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations): @cached_property def unsupported_functions(self): - unsupported = {"GeometryDistance", "IsEmpty", "MemSize", "Rotate"} + unsupported = {"GeometryDistance", "MemSize", "Rotate"} if not self.geom_lib_version(): unsupported |= {"Azimuth", "GeoHash", "MakeValid"} if self.spatial_version < (5, 1): diff --git a/django/contrib/gis/db/models/functions.py b/django/contrib/gis/db/models/functions.py index 9e94d0f77a..b6ff35858f 100644 --- a/django/contrib/gis/db/models/functions.py +++ b/django/contrib/gis/db/models/functions.py @@ -451,6 +451,10 @@ class IsEmpty(GeoFuncMixin, Transform): lookup_name = "isempty" output_field = BooleanField() + def as_sqlite(self, compiler, connection, **extra_context): + sql, params = super().as_sql(compiler, connection, **extra_context) + return "NULLIF(%s, -1)" % sql, params + @BaseSpatialField.register_lookup class IsValid(OracleToleranceMixin, GeoFuncMixin, Transform): diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index e23a7667f1..b51001ecd1 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/docs/ref/contrib/gis/db-api.txt @@ -363,7 +363,7 @@ Lookup Type PostGIS Oracle MariaDB MySQL [#]_ :lookup:`exact ` X X X X X B :lookup:`geom_type` X X (≥ 23c) X X X :lookup:`intersects` X X X X X B -:lookup:`isempty` X +:lookup:`isempty` X X :lookup:`isvalid` X X X (≥ 12.0.1) X X :lookup:`overlaps` X X X X X B :lookup:`relate` X X X X C @@ -414,7 +414,7 @@ Function PostGIS Oracle MariaDB MySQL :class:`GeometryDistance` X :class:`GeometryType` X X (≥ 23c) X X X :class:`Intersection` X X X X X -:class:`IsEmpty` X +:class:`IsEmpty` X X :class:`IsValid` X X X (≥ 12.0.1) X X :class:`Length` X X X X X :class:`LineLocatePoint` X X diff --git a/docs/ref/contrib/gis/functions.txt b/docs/ref/contrib/gis/functions.txt index af1cd439dc..ab540627eb 100644 --- a/docs/ref/contrib/gis/functions.txt +++ b/docs/ref/contrib/gis/functions.txt @@ -621,11 +621,16 @@ Miscellaneous .. class:: IsEmpty(expr) -*Availability*: `PostGIS `__ +*Availability*: `PostGIS `__, +SpatiaLite Accepts a geographic field or expression and tests if the value is an empty geometry. Returns ``True`` if its value is empty and ``False`` otherwise. +.. versionchanged:: 6.1 + + SpatiaLite support was added. + ``IsValid`` ----------- diff --git a/docs/ref/contrib/gis/geoquerysets.txt b/docs/ref/contrib/gis/geoquerysets.txt index d6d477fb9c..6109bafb4f 100644 --- a/docs/ref/contrib/gis/geoquerysets.txt +++ b/docs/ref/contrib/gis/geoquerysets.txt @@ -361,7 +361,8 @@ SpatiaLite ``Intersects(poly, geom)`` ``isempty`` ----------- -*Availability*: `PostGIS `__ +*Availability*: `PostGIS `__, +SpatiaLite Tests if the geometry is empty. @@ -369,6 +370,10 @@ Example:: Zipcode.objects.filter(poly__isempty=True) +.. versionchanged:: 6.1 + + SpatiaLite support was added. + .. fieldlookup:: isvalid ``isvalid`` diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt index 309a775e74..be05350d94 100644 --- a/docs/releases/6.1.txt +++ b/docs/releases/6.1.txt @@ -53,7 +53,9 @@ Minor features :mod:`django.contrib.gis` ~~~~~~~~~~~~~~~~~~~~~~~~~ -* ... +* The :lookup:`isempty` lookup and + :class:`IsEmpty() ` + database function are now supported on SpatiaLite. :mod:`django.contrib.messages` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/gis_tests/geoapp/test_functions.py b/tests/gis_tests/geoapp/test_functions.py index 70c462a78e..b1ab4340aa 100644 --- a/tests/gis_tests/geoapp/test_functions.py +++ b/tests/gis_tests/geoapp/test_functions.py @@ -431,7 +431,7 @@ class GISFunctionsTests(FuncTestMixin, TestCase): self.assertIs(c.inter.empty, True) @skipUnlessDBFeature("supports_empty_geometries", "has_IsEmpty_function") - def test_isempty(self): + def test_isempty_geometry_empty(self): empty = City.objects.create(name="Nowhere", point=Point(srid=4326)) City.objects.create(name="Somewhere", point=Point(6.825, 47.1, srid=4326)) self.assertSequenceEqual( @@ -442,6 +442,18 @@ class GISFunctionsTests(FuncTestMixin, TestCase): ) self.assertSequenceEqual(City.objects.filter(point__isempty=True), [empty]) + @skipUnlessDBFeature("has_IsEmpty_function") + def test_isempty_geometry_null(self): + nowhere = State.objects.create(name="Nowhere", poly=None) + qs = State.objects.annotate(isempty=functions.IsEmpty("poly")) + self.assertSequenceEqual(qs.filter(isempty=None), [nowhere]) + self.assertSequenceEqual( + qs.filter(isempty=False).order_by("name").values_list("name", flat=True), + ["Colorado", "Kansas"], + ) + self.assertSequenceEqual(qs.filter(isempty=True), []) + self.assertSequenceEqual(State.objects.filter(poly__isempty=True), []) + @skipUnlessDBFeature("has_IsValid_function") def test_isvalid(self): valid_geom = fromstr("POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))")