summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAhmed Ibrahim <ahmeddark369@gmail.com>2025-07-17 09:50:39 +0300
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2025-07-20 11:18:38 +0200
commita5b0a618c3ff16f9f52a9abdc89c05899ef112b4 (patch)
tree08c83f12bfe69d585b3c40fecb215dceaa52a4b3
parent6aa05fd23205fce523924e307f78ce8d614af7f1 (diff)
Fixed #28696 -- Added GeometryType GIS database function and __geom_type lookup.
Co-Authored-By: Mariusz Felisiak <felisiak.mariusz@gmail.com>
-rw-r--r--django/contrib/gis/db/backends/base/operations.py1
-rw-r--r--django/contrib/gis/db/backends/oracle/operations.py40
-rw-r--r--django/contrib/gis/db/backends/postgis/operations.py1
-rw-r--r--django/contrib/gis/db/models/functions.py24
-rw-r--r--docs/ref/contrib/gis/db-api.txt72
-rw-r--r--docs/ref/contrib/gis/functions.txt12
-rw-r--r--docs/ref/contrib/gis/geoquerysets.txt26
-rw-r--r--docs/releases/6.0.txt4
-rw-r--r--tests/gis_tests/geoapp/test_functions.py66
9 files changed, 192 insertions, 54 deletions
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,
@@ -423,6 +424,29 @@ class Intersection(OracleToleranceMixin, GeomOutputGeoFunc):
@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"
output_field = BooleanField()
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
<spatial-lookup-raster>`: 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 <gis-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 <same_as>` 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 <gis-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 <same_as>` 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 <https://postgis.net/docs/GeometryType.html>`__,
+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 <https://postgis.net/docs/GeometryType.html>`__,
+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() <django.contrib.gis.db.models.functions.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)