diff --git a/django/contrib/gis/db/backends/base/operations.py b/django/contrib/gis/db/backends/base/operations.py index 8351e71fbd..23e7fd2c25 100644 --- a/django/contrib/gis/db/backends/base/operations.py +++ b/django/contrib/gis/db/backends/base/operations.py @@ -54,6 +54,7 @@ class BaseSpatialOperations: "FromWKT", "GeoHash", "GeometryDistance", + "GeometryType", "Intersection", "IsEmpty", "IsValid", diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py index b8618ae593..3d5dcc7294 100644 --- a/django/contrib/gis/db/backends/oracle/operations.py +++ b/django/contrib/gis/db/backends/oracle/operations.py @@ -18,6 +18,7 @@ from django.contrib.gis.geos.geometry import GEOSGeometry, GEOSGeometryBase from django.contrib.gis.geos.prototypes.io import wkb_r from django.contrib.gis.measure import Distance from django.db.backends.oracle.operations import DatabaseOperations +from django.utils.functional import cached_property DEFAULT_TOLERANCE = "0.05" @@ -117,23 +118,28 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations): "dwithin": SDODWithin(), } - unsupported_functions = { - "AsKML", - "AsSVG", - "Azimuth", - "ClosestPoint", - "ForcePolygonCW", - "GeoHash", - "GeometryDistance", - "IsEmpty", - "LineLocatePoint", - "MakeValid", - "MemSize", - "Rotate", - "Scale", - "SnapToGrid", - "Translate", - } + @cached_property + def unsupported_functions(self): + unsupported = { + "AsKML", + "AsSVG", + "Azimuth", + "ClosestPoint", + "ForcePolygonCW", + "GeoHash", + "GeometryDistance", + "IsEmpty", + "LineLocatePoint", + "MakeValid", + "MemSize", + "Rotate", + "Scale", + "SnapToGrid", + "Translate", + } + if self.connection.oracle_version < (23,): + unsupported.add("GeometryType") + return unsupported def geo_quote_name(self, name): return super().geo_quote_name(name).upper() diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py index 7ae22fd00b..bd1e79a6d6 100644 --- a/django/contrib/gis/db/backends/postgis/operations.py +++ b/django/contrib/gis/db/backends/postgis/operations.py @@ -177,6 +177,7 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations): "FromWKB": "ST_GeomFromWKB", "FromWKT": "ST_GeomFromText", "NumPoints": "ST_NPoints", + "GeometryType": "GeometryType", } return function_names diff --git a/django/contrib/gis/db/models/functions.py b/django/contrib/gis/db/models/functions.py index f26e9ae3e0..cafcd32e3b 100644 --- a/django/contrib/gis/db/models/functions.py +++ b/django/contrib/gis/db/models/functions.py @@ -9,6 +9,7 @@ from django.db import NotSupportedError from django.db.models import ( BinaryField, BooleanField, + CharField, FloatField, Func, IntegerField, @@ -422,6 +423,29 @@ class Intersection(OracleToleranceMixin, GeomOutputGeoFunc): geom_param_pos = (0, 1) +@BaseSpatialField.register_lookup +class GeometryType(GeoFuncMixin, Transform): + output_field = CharField() + lookup_name = "geom_type" + + def as_oracle(self, compiler, connection, **extra_context): + lhs, params = compiler.compile(self.lhs) + sql = ( + "(SELECT DECODE(" + f"SDO_GEOMETRY.GET_GTYPE({lhs})," + "1, 'POINT'," + "2, 'LINESTRING'," + "3, 'POLYGON'," + "4, 'COLLECTION'," + "5, 'MULTIPOINT'," + "6, 'MULTILINESTRING'," + "7, 'MULTIPOLYGON'," + "8, 'SOLID'," + "'UNKNOWN'))" + ) + return sql, params + + @BaseSpatialField.register_lookup class IsEmpty(GeoFuncMixin, Transform): lookup_name = "isempty" diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index 5ce009e9bb..be7e5440b4 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/docs/ref/contrib/gis/db-api.txt @@ -339,42 +339,43 @@ divided into the three categories described in the :ref:`raster lookup details `: native support ``N``, bilateral native support ``B``, and geometry conversion support ``C``. -================================= ========= ======== ============ ============ ========== ======== -Lookup Type PostGIS Oracle MariaDB MySQL [#]_ SpatiaLite PGRaster -================================= ========= ======== ============ ============ ========== ======== -:lookup:`bbcontains` X X X X N -:lookup:`bboverlaps` X X X X N -:lookup:`contained` X X X X N -:lookup:`contains ` X X X X X B -:lookup:`contains_properly` X B -:lookup:`coveredby` X X X (≥ 12.0.1) X X B -:lookup:`covers` X X X X B -:lookup:`crosses` X X X X C -:lookup:`disjoint` X X X X X B -:lookup:`distance_gt` X X X X X N -:lookup:`distance_gte` X X X X X N -:lookup:`distance_lt` X X X X X N -:lookup:`distance_lte` X X X X X N -:lookup:`dwithin` X X X B -:lookup:`equals` X X X X X C -:lookup:`exact ` X X X X X B -:lookup:`intersects` X X X X X B +================================= ========= ========= ============ ============ ========== ======== +Lookup Type PostGIS Oracle MariaDB MySQL [#]_ SpatiaLite PGRaster +================================= ========= ========= ============ ============ ========== ======== +:lookup:`bbcontains` X X X X N +:lookup:`bboverlaps` X X X X N +:lookup:`contained` X X X X N +:lookup:`contains ` X X X X X B +:lookup:`contains_properly` X B +:lookup:`coveredby` X X X (≥ 12.0.1) X X B +:lookup:`covers` X X X X B +:lookup:`crosses` X X X X C +:lookup:`disjoint` X X X X X B +:lookup:`distance_gt` X X X X X N +:lookup:`distance_gte` X X X X X N +:lookup:`distance_lt` X X X X X N +:lookup:`distance_lte` X X X X X N +:lookup:`dwithin` X X X B +:lookup:`equals` X X X X X C +: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:`isvalid` X X X (≥ 12.0.1) X X -:lookup:`overlaps` X X X X X B -:lookup:`relate` X X X X C -:lookup:`same_as` X X X X X B -:lookup:`touches` X X X X X B -:lookup:`within` X X X X X B -:lookup:`left` X C -:lookup:`right` X C -:lookup:`overlaps_left` X B -:lookup:`overlaps_right` X B -:lookup:`overlaps_above` X C -:lookup:`overlaps_below` X C -:lookup:`strictly_above` X C -:lookup:`strictly_below` X C -================================= ========= ======== ============ ============ ========== ======== +: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 +:lookup:`same_as` X X X X X B +:lookup:`touches` X X X X X B +:lookup:`within` X X X X X B +:lookup:`left` X C +:lookup:`right` X C +:lookup:`overlaps_left` X B +:lookup:`overlaps_right` X B +:lookup:`overlaps_above` X C +:lookup:`overlaps_below` X C +:lookup:`strictly_above` X C +:lookup:`strictly_below` X C +================================= ========= ========= ============ ============ ========== ======== .. _database-functions-compatibility: @@ -408,6 +409,7 @@ Function PostGIS Oracle MariaDB MySQL :class:`FromWKT` X X X X X :class:`GeoHash` X X (≥ 12.0.1) X X (LWGEOM/RTTOPO) :class:`GeometryDistance` X +:class:`GeometryType` X X (≥ 23c) X X X :class:`Intersection` X X X X X :class:`IsEmpty` X :class:`IsValid` X X X (≥ 12.0.1) X X diff --git a/docs/ref/contrib/gis/functions.txt b/docs/ref/contrib/gis/functions.txt index f9939c5e63..8c9c1cf82f 100644 --- a/docs/ref/contrib/gis/functions.txt +++ b/docs/ref/contrib/gis/functions.txt @@ -635,6 +635,18 @@ Returns ``True`` if its value is a valid geometry and ``False`` otherwise. MariaDB 12.0.1+ support was added. +``GeometryType`` +---------------- + +.. versionadded:: 6.0 + +.. class:: GeometryType(expr) + +*Availability*: `PostGIS `__, +Oracle 23c+, MariaDB, MySQL, SpatiaLite + +Accepts a geographic field or expression and returns its geometry type. + ``MemSize`` ----------- diff --git a/docs/ref/contrib/gis/geoquerysets.txt b/docs/ref/contrib/gis/geoquerysets.txt index 0231ce6af8..1ab0f7c309 100644 --- a/docs/ref/contrib/gis/geoquerysets.txt +++ b/docs/ref/contrib/gis/geoquerysets.txt @@ -399,6 +399,32 @@ Oracle ``SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT(p MariaDB 12.0.1+ support was added. +.. fieldlookup:: geom_type + +``geom_type`` +------------- + +.. versionadded:: 6.0 + +*Availability*: `PostGIS `__, +Oracle 23c+, MariaDB, MySQL, SpatiaLite + +Returns the geometry type of the geometry field. + +Example:: + + Zipcode.objects.filter(poly__geom_type="POLYGON") + +========== ========================== +Backend SQL Equivalent +========== ========================== +PostGIS ``GeometryType(geom)`` +MariaDB ``ST_GeometryType(geom)`` +MySQL ``ST_GeometryType(geom)`` +Oracle ``SDO_GEOMETRY.GET_GTYPE(geom)`` +SpatiaLite ``GeometryType(geom)`` +========== ========================== + .. fieldlookup:: overlaps ``overlaps`` diff --git a/docs/releases/6.0.txt b/docs/releases/6.0.txt index 3f4ce44a75..c5fa1bac63 100644 --- a/docs/releases/6.0.txt +++ b/docs/releases/6.0.txt @@ -119,6 +119,10 @@ Minor features :class:`~django.contrib.gis.db.models.functions.IsValid` database functions are now supported on MariaDB 12.0.1+. +* The new :lookup:`geom_type` lookup and + :class:`GeometryType() ` + database function allow filtering geometries by their types. + :mod:`django.contrib.messages` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/gis_tests/geoapp/test_functions.py b/tests/gis_tests/geoapp/test_functions.py index 9844905200..989f6069cd 100644 --- a/tests/gis_tests/geoapp/test_functions.py +++ b/tests/gis_tests/geoapp/test_functions.py @@ -4,14 +4,31 @@ import re from decimal import Decimal from django.contrib.gis.db.models import GeometryField, PolygonField, functions -from django.contrib.gis.geos import GEOSGeometry, LineString, Point, Polygon, fromstr +from django.contrib.gis.geos import ( + GEOSGeometry, + LineString, + MultiLineString, + MultiPoint, + MultiPolygon, + Point, + Polygon, + fromstr, +) from django.contrib.gis.measure import Area from django.db import NotSupportedError, connection from django.db.models import IntegerField, Sum, Value from django.test import TestCase, skipUnlessDBFeature from ..utils import FuncTestMixin -from .models import City, Country, CountryWebMercator, ManyPointModel, State, Track +from .models import ( + City, + Country, + CountryWebMercator, + Feature, + ManyPointModel, + State, + Track, +) class GISFunctionsTests(FuncTestMixin, TestCase): @@ -880,3 +897,48 @@ class GISFunctionsTests(FuncTestMixin, TestCase): City.objects.annotate(union=functions.GeoFunc(1, "point")).get( name="Dallas" ) + + @skipUnlessDBFeature("has_GeometryType_function") + def test_geometry_type(self): + Feature.objects.bulk_create( + [ + Feature(name="Point", geom=Point(0, 0)), + Feature(name="LineString", geom=LineString((0, 0), (1, 1))), + Feature(name="Polygon", geom=Polygon(((0, 0), (1, 0), (1, 1), (0, 0)))), + Feature(name="MultiPoint", geom=MultiPoint(Point(0, 0), Point(1, 1))), + Feature( + name="MultiLineString", + geom=MultiLineString( + LineString((0, 0), (1, 1)), LineString((1, 1), (2, 2)) + ), + ), + Feature( + name="MultiPolygon", + geom=MultiPolygon( + Polygon(((0, 0), (1, 0), (1, 1), (0, 0))), + Polygon(((1, 1), (2, 1), (2, 2), (1, 1))), + ), + ), + ] + ) + + expected_results = { + ("POINT", Point), + ("LINESTRING", LineString), + ("POLYGON", Polygon), + ("MULTIPOINT", MultiPoint), + ("MULTILINESTRING", MultiLineString), + ("MULTIPOLYGON", MultiPolygon), + } + + for geom_type, geom_class in expected_results: + with self.subTest(geom_type=geom_type): + obj = ( + Feature.objects.annotate( + geometry_type=functions.GeometryType("geom") + ) + .filter(geom__geom_type=geom_type) + .get() + ) + self.assertIsInstance(obj.geom, geom_class) + self.assertEqual(obj.geometry_type, geom_type)