Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions django_mongodb_backend/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ def execute_sql(
except EmptyResultSet:
return iter([]) if result_type == MULTI else None

print(f"Query: {query}")
cursor = query.get_cursor()
if result_type == SINGLE:
try:
Expand Down Expand Up @@ -785,6 +786,7 @@ def explain_query(self):
for option in self.connection.ops.explain_options:
if value := options.get(option):
kwargs[option] = value
print(f"PIPELINE: {pipeline}")
explain = self.connection.database.command(
"explain",
{"aggregate": self.collection_name, "pipeline": pipeline, "cursor": {}},
Expand Down
15 changes: 13 additions & 2 deletions django_mongodb_backend/gis/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
class GISFeatures(BaseSpatialFeatures):
has_spatialrefsys_table = False
supports_transform = False
supports_distance_geodetic = False
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please alphabetize about supports_transform.

has_Distance_function = False
has_Union_function = False

@cached_property
def django_test_expected_failures(self):
Expand Down Expand Up @@ -39,6 +42,11 @@ def django_test_skips(self):
# SouthTexasCity fixture objects use SRID 2278 which is ignored
# by the patched version of loaddata in the Django fork.
"gis_tests.distapp.tests.DistanceTest.test_init",
"gis_tests.distapp.tests.DistanceTest.test_distance_lookups",
"gis_tests.distapp.tests.DistanceTest.test_distance_lookups_with_expression_rhs",
"gis_tests.distapp.tests.DistanceTest.test_distance_annotation_group_by",
"gis_tests.distapp.tests.DistanceFunctionsTests.test_distance_simple",
"gis_tests.distapp.tests.DistanceFunctionsTests.test_distance_order_by",
},
"ImproperlyConfigured isn't raised when using RasterField": {
# Normally RasterField.db_type() raises an error, but MongoDB
Expand All @@ -49,10 +57,13 @@ def django_test_skips(self):
# Error: Index already exists with a different name
"gis_tests.geoapp.test_indexes.SchemaIndexesTests.test_index_name",
},
"GIS lookups not supported.": {
"gis_tests.geoapp.tests.GeoModelTest.test_gis_query_as_string",
"GIS Union not supported.": {
"gis_tests.geoapp.tests.GeoLookupTest.test_gis_lookups_with_complex_expressions",
},
Comment on lines 56 to 62
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self that we should add @skipUnlessDBFeature("supports_Union_function") and send the patch upstream. I'll hold off in case any more similar changes are identified by this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a couple other places where tests not marked as requiring a feature use it. Maybe the authors didn't expect a backend to implement only the slice of operations that we do?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. The skipping is generally just best effort... what's needed for the built-in backends.

"Subqueries not supported.": {
"gis_tests.geoapp.tests.GeoLookupTest.test_subquery_annotation",
"gis_tests.geoapp.tests.GeoQuerySetTest.test_within_subquery",
},
"GeoJSONSerializer doesn't support ObjectId.": {
"gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_fields_option",
"gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_geometry_field_option",
Expand Down
15 changes: 10 additions & 5 deletions django_mongodb_backend/gis/lookups.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from django.contrib.gis.db.models.lookups import GISLookup
from django.db import NotSupportedError
from django.contrib.gis.db.models.lookups import DistanceLookupFromFunction, GISLookup

from django_mongodb_backend.query_utils import process_lhs, process_rhs

def gis_lookup(self, compiler, connection, as_expr=False): # noqa: ARG001
raise NotSupportedError(f"MongoDB does not support the {self.lookup_name} lookup.")

def _gis_lookup(self, compiler, connection, as_expr=False):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding an underscore prefix is not needed.

lhs_mql = process_lhs(self, compiler, connection, as_expr=as_expr)
rhs_mql = process_rhs(self, compiler, connection, as_expr=as_expr)
rhs_op = self.get_rhs_op(connection, rhs_mql)
return rhs_op.as_mql(lhs_mql, rhs_mql, self.rhs_params)


def register_lookups():
GISLookup.as_mql = gis_lookup
GISLookup.as_mql = _gis_lookup
DistanceLookupFromFunction.as_mql = _gis_lookup
125 changes: 122 additions & 3 deletions django_mongodb_backend/gis/operations.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,110 @@
import warnings

from django.contrib.gis import geos
from django.contrib.gis.db import models
from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations
from django.contrib.gis.measure import Distance
from django.db.backends.base.operations import BaseDatabaseOperations

from .adapter import Adapter
from .utils import SpatialOperator


def _gis_within_operator(field, value, op=None, params=None):
print(f"Within value: {value}")
return {
field: {
"$geoWithin": {
"$geometry": {
"type": value["type"],
"coordinates": value["coordinates"],
}
}
}
}


def _gis_intersects_operator(field, value, op=None, params=None):
return {
field: {
"$geoIntersects": {
"$geometry": {
"type": value["type"],
"coordinates": value["coordinates"],
}
}
}
}


def _gis_disjoint_operator(field, value, op=None, params=None):
return {
field: {
"$not": {
"$geoIntersects": {
"$geometry": {
"type": value["type"],
"coordinates": value["coordinates"],
}
}
}
}
}


def _gis_contains_operator(field, value, op=None, params=None):
value_type = value["type"]
if value_type != "Point":
warnings.warn(
"MongoDB does not support strict contains on non-Point query geometries. Results will be for intersection."
)
return {
field: {
"$geoIntersects": {
"$geometry": {
"type": value_type,
"coordinates": value["coordinates"],
}
}
}
}

class GISOperations(BaseSpatialOperations):

def _gis_distance_operator(field, value, op=None, params=None):
print(f"Distance: {params}")
if hasattr(params[0], "m"):
distance = params[0].m
else:
distance = params[0]
if op == "distance_gt" or op == "distance_gte":
cmd = {
field: {
"$not": {
"$geoWithin": {
"$centerSphere": [
value["coordinates"],
distance / 6378100, # radius of earth in meters
],
}
}
}
}
else:
cmd = {
field: {
"$geoWithin": {
"$centerSphere": [
value["coordinates"],
distance / 6378100, # radius of earth in meters
],
}
}
}
print(f"Command: {cmd}")
return cmd


class GISOperations(BaseSpatialOperations, BaseDatabaseOperations):
Adapter = Adapter

disallowed_aggregates = (
Expand All @@ -18,7 +117,16 @@ class GISOperations(BaseSpatialOperations):

@property
def gis_operators(self):
return {}
return {
"contains": SpatialOperator("contains", _gis_contains_operator),
"intersects": SpatialOperator("intersects", _gis_intersects_operator),
"disjoint": SpatialOperator("disjoint", _gis_disjoint_operator),
"within": SpatialOperator("within", _gis_within_operator),
"distance_gt": SpatialOperator("distance_gt", _gis_distance_operator),
"distance_gte": SpatialOperator("distance_gte", _gis_distance_operator),
"distance_lt": SpatialOperator("distance_lt", _gis_distance_operator),
"distance_lte": SpatialOperator("distance_lte", _gis_distance_operator),
}

unsupported_functions = {
"Area",
Expand All @@ -33,7 +141,6 @@ def gis_operators(self):
"Centroid",
"ClosestPoint",
"Difference",
"Distance",
"Envelope",
"ForcePolygonCW",
"FromWKB",
Expand Down Expand Up @@ -95,3 +202,15 @@ def converter(value, expression, connection): # noqa: ARG001
return geom_class(*value["coordinates"], srid=srid)

return converter

def get_distance(self, f, value, lookup_type):
value = value[0]
if isinstance(value, Distance):
if f.geodetic(self.connection):
raise ValueError(
"Only numeric values of degree units are allowed on geodetic distance queries."
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use self.assertRaisesMessage() to test for this.

)
dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
else:
dist_param = value
return [dist_param]
19 changes: 19 additions & 0 deletions django_mongodb_backend/gis/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
A collection of utility routines and classes used by the spatial
backend.
"""

from django.contrib.gis.db.backends.utils import SpatialOperator as _SpatialOperator


class SpatialOperator(_SpatialOperator):
"""
Class encapsulating the behavior specific to a GIS operation (used by lookups).
"""

def __init__(self, op=None, func=None):
self.op = op
self.func = func

def as_mql(self, lhs, rhs, params=None):
return self.func(lhs, rhs, self.op, params)
Loading