1
0
mirror of https://github.com/django/django.git synced 2025-08-21 17:29:13 +00:00

Fixed #28696 -- Added GeometryType GIS database function and __geom_type lookup.

Co-Authored-By: Mariusz Felisiak <felisiak.mariusz@gmail.com>
This commit is contained in:
Ahmed Ibrahim 2025-07-17 09:50:39 +03:00 committed by Mariusz Felisiak
parent 6aa05fd232
commit a5b0a618c3
9 changed files with 192 additions and 54 deletions

View File

@ -54,6 +54,7 @@ class BaseSpatialOperations:
"FromWKT", "FromWKT",
"GeoHash", "GeoHash",
"GeometryDistance", "GeometryDistance",
"GeometryType",
"Intersection", "Intersection",
"IsEmpty", "IsEmpty",
"IsValid", "IsValid",

View File

@ -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.geos.prototypes.io import wkb_r
from django.contrib.gis.measure import Distance from django.contrib.gis.measure import Distance
from django.db.backends.oracle.operations import DatabaseOperations from django.db.backends.oracle.operations import DatabaseOperations
from django.utils.functional import cached_property
DEFAULT_TOLERANCE = "0.05" DEFAULT_TOLERANCE = "0.05"
@ -117,23 +118,28 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
"dwithin": SDODWithin(), "dwithin": SDODWithin(),
} }
unsupported_functions = { @cached_property
"AsKML", def unsupported_functions(self):
"AsSVG", unsupported = {
"Azimuth", "AsKML",
"ClosestPoint", "AsSVG",
"ForcePolygonCW", "Azimuth",
"GeoHash", "ClosestPoint",
"GeometryDistance", "ForcePolygonCW",
"IsEmpty", "GeoHash",
"LineLocatePoint", "GeometryDistance",
"MakeValid", "IsEmpty",
"MemSize", "LineLocatePoint",
"Rotate", "MakeValid",
"Scale", "MemSize",
"SnapToGrid", "Rotate",
"Translate", "Scale",
} "SnapToGrid",
"Translate",
}
if self.connection.oracle_version < (23,):
unsupported.add("GeometryType")
return unsupported
def geo_quote_name(self, name): def geo_quote_name(self, name):
return super().geo_quote_name(name).upper() return super().geo_quote_name(name).upper()

View File

@ -177,6 +177,7 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
"FromWKB": "ST_GeomFromWKB", "FromWKB": "ST_GeomFromWKB",
"FromWKT": "ST_GeomFromText", "FromWKT": "ST_GeomFromText",
"NumPoints": "ST_NPoints", "NumPoints": "ST_NPoints",
"GeometryType": "GeometryType",
} }
return function_names return function_names

View File

@ -9,6 +9,7 @@ from django.db import NotSupportedError
from django.db.models import ( from django.db.models import (
BinaryField, BinaryField,
BooleanField, BooleanField,
CharField,
FloatField, FloatField,
Func, Func,
IntegerField, IntegerField,
@ -422,6 +423,29 @@ class Intersection(OracleToleranceMixin, GeomOutputGeoFunc):
geom_param_pos = (0, 1) 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 @BaseSpatialField.register_lookup
class IsEmpty(GeoFuncMixin, Transform): class IsEmpty(GeoFuncMixin, Transform):
lookup_name = "isempty" lookup_name = "isempty"

View File

@ -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``, <spatial-lookup-raster>`: native support ``N``, bilateral native support ``B``,
and geometry conversion support ``C``. and geometry conversion support ``C``.
================================= ========= ======== ============ ============ ========== ======== ================================= ========= ========= ============ ============ ========== ========
Lookup Type PostGIS Oracle MariaDB MySQL [#]_ SpatiaLite PGRaster Lookup Type PostGIS Oracle MariaDB MySQL [#]_ SpatiaLite PGRaster
================================= ========= ======== ============ ============ ========== ======== ================================= ========= ========= ============ ============ ========== ========
:lookup:`bbcontains` X X X X N :lookup:`bbcontains` X X X X N
:lookup:`bboverlaps` X X X X N :lookup:`bboverlaps` X X X X N
:lookup:`contained` X X X X N :lookup:`contained` X X X X N
:lookup:`contains <gis-contains>` X X X X X B :lookup:`contains <gis-contains>` X X X X X B
:lookup:`contains_properly` X B :lookup:`contains_properly` X B
:lookup:`coveredby` X X X (≥ 12.0.1) X X B :lookup:`coveredby` X X X (≥ 12.0.1) X X B
:lookup:`covers` X X X X B :lookup:`covers` X X X X B
:lookup:`crosses` X X X X C :lookup:`crosses` X X X X C
:lookup:`disjoint` X X X X X B :lookup:`disjoint` X X X X X B
:lookup:`distance_gt` X X X X X N :lookup:`distance_gt` X X X X X N
:lookup:`distance_gte` 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_lt` X X X X X N
:lookup:`distance_lte` X X X X X N :lookup:`distance_lte` X X X X X N
:lookup:`dwithin` X X X B :lookup:`dwithin` X X X B
:lookup:`equals` X X X X X C :lookup:`equals` X X X X X C
:lookup:`exact <same_as>` X X X X X B :lookup:`exact <same_as>` X X X X X B
:lookup:`intersects` 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
:lookup:`isvalid` X X X (≥ 12.0.1) X X :lookup:`isvalid` X X X (≥ 12.0.1) X X
:lookup:`overlaps` X X X X X B :lookup:`overlaps` X X X X X B
:lookup:`relate` X X X X C :lookup:`relate` X X X X C
:lookup:`same_as` X X X X X B :lookup:`same_as` X X X X X B
:lookup:`touches` X X X X X B :lookup:`touches` X X X X X B
:lookup:`within` X X X X X B :lookup:`within` X X X X X B
:lookup:`left` X C :lookup:`left` X C
:lookup:`right` X C :lookup:`right` X C
:lookup:`overlaps_left` X B :lookup:`overlaps_left` X B
:lookup:`overlaps_right` X B :lookup:`overlaps_right` X B
:lookup:`overlaps_above` X C :lookup:`overlaps_above` X C
:lookup:`overlaps_below` X C :lookup:`overlaps_below` X C
:lookup:`strictly_above` X C :lookup:`strictly_above` X C
:lookup:`strictly_below` X C :lookup:`strictly_below` X C
================================= ========= ======== ============ ============ ========== ======== ================================= ========= ========= ============ ============ ========== ========
.. _database-functions-compatibility: .. _database-functions-compatibility:
@ -408,6 +409,7 @@ Function PostGIS Oracle MariaDB MySQL
:class:`FromWKT` X X X X X :class:`FromWKT` X X X X X
:class:`GeoHash` X X (≥ 12.0.1) X X (LWGEOM/RTTOPO) :class:`GeoHash` X X (≥ 12.0.1) X X (LWGEOM/RTTOPO)
:class:`GeometryDistance` X :class:`GeometryDistance` X
:class:`GeometryType` X X (≥ 23c) X X X
:class:`Intersection` X X X X X :class:`Intersection` X X X X X
:class:`IsEmpty` X :class:`IsEmpty` X
:class:`IsValid` X X X (≥ 12.0.1) X X :class:`IsValid` X X X (≥ 12.0.1) X X

View File

@ -635,6 +635,18 @@ Returns ``True`` if its value is a valid geometry and ``False`` otherwise.
MariaDB 12.0.1+ support was added. 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`` ``MemSize``
----------- -----------

View File

@ -399,6 +399,32 @@ Oracle ``SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT(p
MariaDB 12.0.1+ support was added. 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 .. fieldlookup:: overlaps
``overlaps`` ``overlaps``

View File

@ -119,6 +119,10 @@ Minor features
:class:`~django.contrib.gis.db.models.functions.IsValid` database functions :class:`~django.contrib.gis.db.models.functions.IsValid` database functions
are now supported on MariaDB 12.0.1+. 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` :mod:`django.contrib.messages`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -4,14 +4,31 @@ import re
from decimal import Decimal from decimal import Decimal
from django.contrib.gis.db.models import GeometryField, PolygonField, functions 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.contrib.gis.measure import Area
from django.db import NotSupportedError, connection from django.db import NotSupportedError, connection
from django.db.models import IntegerField, Sum, Value from django.db.models import IntegerField, Sum, Value
from django.test import TestCase, skipUnlessDBFeature from django.test import TestCase, skipUnlessDBFeature
from ..utils import FuncTestMixin 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): class GISFunctionsTests(FuncTestMixin, TestCase):
@ -880,3 +897,48 @@ class GISFunctionsTests(FuncTestMixin, TestCase):
City.objects.annotate(union=functions.GeoFunc(1, "point")).get( City.objects.annotate(union=functions.GeoFunc(1, "point")).get(
name="Dallas" 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)