diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py index 639634e8e3..b4b04622c6 100644 --- a/django/contrib/gis/db/backends/oracle/operations.py +++ b/django/contrib/gis/db/backends/oracle/operations.py @@ -52,6 +52,10 @@ class SDORelate(SpatialOperator): return super(SDORelate, self).as_sql(connection, lookup, template_params, sql_params) +class SDOIsValid(SpatialOperator): + sql_template = "%%(func)s(%%(lhs)s, %s) = 'TRUE'" % DEFAULT_TOLERANCE + + class OracleOperations(BaseSpatialOperations, DatabaseOperations): name = 'oracle' @@ -85,6 +89,7 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations): 'Difference': 'SDO_GEOM.SDO_DIFFERENCE', 'Distance': 'SDO_GEOM.SDO_DISTANCE', 'Intersection': 'SDO_GEOM.SDO_INTERSECTION', + 'IsValid': 'SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT', 'Length': 'SDO_GEOM.SDO_LENGTH', 'NumGeometries': 'SDO_UTIL.GETNUMELEM', 'NumPoints': 'SDO_UTIL.GETNUMVERTICES', @@ -109,6 +114,7 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations): 'covers': SDOOperator(func='SDO_COVERS'), 'disjoint': SDODisjoint(), 'intersects': SDOOperator(func='SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()? + 'isvalid': SDOIsValid(func='SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT'), 'equals': SDOOperator(func='SDO_EQUAL'), 'exact': SDOOperator(func='SDO_EQUAL'), 'overlaps': SDOOperator(func='SDO_OVERLAPS'), @@ -128,7 +134,7 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations): unsupported_functions = { 'AsGeoJSON', 'AsGML', 'AsKML', 'AsSVG', 'BoundingCircle', 'Envelope', - 'ForceRHR', 'GeoHash', 'IsValid', 'MakeValid', 'MemSize', 'Scale', + 'ForceRHR', 'GeoHash', 'MakeValid', 'MemSize', 'Scale', 'SnapToGrid', 'Translate', } diff --git a/django/contrib/gis/db/models/functions.py b/django/contrib/gis/db/models/functions.py index 1929b2f7a1..1cec34d2bf 100644 --- a/django/contrib/gis/db/models/functions.py +++ b/django/contrib/gis/db/models/functions.py @@ -285,9 +285,13 @@ class Intersection(OracleToleranceMixin, GeoFuncWithGeoParam): arity = 2 -class IsValid(GeoFunc): +class IsValid(OracleToleranceMixin, GeoFunc): output_field_class = BooleanField + def as_oracle(self, compiler, connection, **extra_context): + sql, params = super(IsValid, self).as_oracle(compiler, connection, **extra_context) + return "CASE %s WHEN 'TRUE' THEN 1 ELSE 0 END" % sql, params + class Length(DistanceResultMixin, OracleToleranceMixin, GeoFunc): output_field_class = FloatField diff --git a/django/contrib/gis/db/models/lookups.py b/django/contrib/gis/db/models/lookups.py index 19a1b70e4e..8d4e8229e7 100644 --- a/django/contrib/gis/db/models/lookups.py +++ b/django/contrib/gis/db/models/lookups.py @@ -5,7 +5,7 @@ import re from django.core.exceptions import FieldDoesNotExist from django.db.models.constants import LOOKUP_SEP from django.db.models.expressions import Col, Expression -from django.db.models.lookups import BuiltinLookup, Lookup, Transform +from django.db.models.lookups import Lookup, Transform from django.db.models.sql.query import Query from django.utils import six @@ -352,15 +352,16 @@ class IntersectsLookup(GISLookup): gis_lookups['intersects'] = IntersectsLookup -class IsValidLookup(BuiltinLookup): +class IsValidLookup(GISLookup): lookup_name = 'isvalid' + sql_template = '%(func)s(%(lhs)s)' def as_sql(self, compiler, connection): if self.lhs.field.geom_type == 'RASTER': raise ValueError('The isvalid lookup is only available on geometry fields.') gis_op = connection.ops.gis_operators[self.lookup_name] sql, params = self.process_lhs(compiler, connection) - sql = '%(func)s(%(lhs)s)' % {'func': gis_op.func, 'lhs': sql} + sql, params = gis_op.as_sql(connection, self, {'func': gis_op.func, 'lhs': sql}, params) if not self.rhs: sql = 'NOT ' + sql return sql, params diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index 2b2e02ac16..670b23cd78 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/docs/ref/contrib/gis/db-api.txt @@ -347,7 +347,7 @@ Lookup Type PostGIS Oracle MySQL [#]_ SpatiaLite :lookup:`equals` X X X X C :lookup:`exact` X X X X B :lookup:`intersects` X X X X B -:lookup:`isvalid` X X (LWGEOM) +:lookup:`isvalid` X X X (LWGEOM) :lookup:`overlaps` X X X X B :lookup:`relate` X X X C :lookup:`same_as` X X X X B @@ -390,7 +390,7 @@ Function PostGIS Oracle MySQL SpatiaLite :class:`ForceRHR` X :class:`GeoHash` X X (LWGEOM) :class:`Intersection` X X X (≥ 5.6.1) X -:class:`IsValid` X X (LWGEOM) +:class:`IsValid` X X X (LWGEOM) :class:`Length` X X X X :class:`MakeValid` X X (LWGEOM) :class:`MemSize` X diff --git a/docs/ref/contrib/gis/functions.txt b/docs/ref/contrib/gis/functions.txt index b3f989126b..6a1bf10434 100644 --- a/docs/ref/contrib/gis/functions.txt +++ b/docs/ref/contrib/gis/functions.txt @@ -298,14 +298,14 @@ intersection between them. .. versionadded:: 1.10 -*Availability*: PostGIS, SpatiaLite (LWGEOM) +*Availability*: PostGIS, Oracle, SpatiaLite (LWGEOM) Accepts a geographic field or expression and tests if the value is well formed. Returns ``True`` if its value is a valid geometry and ``False`` otherwise. .. versionchanged:: 1.11 - SpatiaLite support was added. + SpatiaLite and Oracle support was added. ``Length`` ========== diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index acc2fe10e2..7bf206c678 100644 --- a/docs/releases/1.11.txt +++ b/docs/releases/1.11.txt @@ -159,6 +159,10 @@ Minor features :class:`~django.contrib.gis.db.models.functions.MakeValid` function, and :lookup:`isvalid` lookup. +* Added Oracle support for the + :class:`~django.contrib.gis.db.models.functions.IsValid` function and + :lookup:`isvalid` lookup. + :mod:`django.contrib.messages` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/gis_tests/geoapp/tests.py b/tests/gis_tests/geoapp/tests.py index 074162fe14..aede5950cd 100644 --- a/tests/gis_tests/geoapp/tests.py +++ b/tests/gis_tests/geoapp/tests.py @@ -295,8 +295,14 @@ class GeoLookupTest(TestCase): def test_isvalid_lookup(self): invalid_geom = fromstr('POLYGON((0 0, 0 1, 1 1, 1 0, 1 1, 1 0, 0 0))') State.objects.create(name='invalid', poly=invalid_geom) - self.assertEqual(State.objects.filter(poly__isvalid=False).count(), 1) - self.assertEqual(State.objects.filter(poly__isvalid=True).count(), State.objects.count() - 1) + qs = State.objects.all() + if oracle: + # Kansas has adjacent vertices with distance 6.99244813842e-12 + # which is smaller than the default Oracle tolerance. + qs = qs.exclude(name='Kansas') + self.assertEqual(State.objects.filter(name='Kansas', poly__isvalid=False).count(), 1) + self.assertEqual(qs.filter(poly__isvalid=False).count(), 1) + self.assertEqual(qs.filter(poly__isvalid=True).count(), qs.count() - 1) @skipUnlessDBFeature("supports_left_right_lookups") def test_left_right_lookups(self):