From 4464b9b9ad9da921f8b50b4e7e26bb4233e05ca0 Mon Sep 17 00:00:00 2001
From: Sergey Fedoseev <fedoseev.sergey@gmail.com>
Date: Wed, 30 Nov 2016 22:22:56 +0600
Subject: [PATCH] Fixed #27556 -- Added Oracle support for IsValid function and
 isvalid lookup.

---
 django/contrib/gis/db/backends/oracle/operations.py |  8 +++++++-
 django/contrib/gis/db/models/functions.py           |  6 +++++-
 django/contrib/gis/db/models/lookups.py             |  7 ++++---
 docs/ref/contrib/gis/db-api.txt                     |  4 ++--
 docs/ref/contrib/gis/functions.txt                  |  4 ++--
 docs/releases/1.11.txt                              |  4 ++++
 tests/gis_tests/geoapp/tests.py                     | 10 ++++++++--
 7 files changed, 32 insertions(+), 11 deletions(-)

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):