Skip to content

Commit c97a4fe

Browse files
committed
Initial support
1 parent 0e0cdb0 commit c97a4fe

File tree

5 files changed

+166
-10
lines changed

5 files changed

+166
-10
lines changed

django_mongodb_backend/compiler.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ def execute_sql(
359359
except EmptyResultSet:
360360
return iter([]) if result_type == MULTI else None
361361

362+
print(f"Query: {query}")
362363
cursor = query.get_cursor()
363364
if result_type == SINGLE:
364365
try:
@@ -785,6 +786,7 @@ def explain_query(self):
785786
for option in self.connection.ops.explain_options:
786787
if value := options.get(option):
787788
kwargs[option] = value
789+
print(f"PIPELINE: {pipeline}")
788790
explain = self.connection.database.command(
789791
"explain",
790792
{"aggregate": self.collection_name, "pipeline": pipeline, "cursor": {}},

django_mongodb_backend/gis/features.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
class GISFeatures(BaseSpatialFeatures):
66
has_spatialrefsys_table = False
77
supports_transform = False
8+
supports_distance_geodetic = False
9+
has_Distance_function = False
10+
has_Union_function = False
811

912
@cached_property
1013
def django_test_expected_failures(self):
@@ -39,6 +42,11 @@ def django_test_skips(self):
3942
# SouthTexasCity fixture objects use SRID 2278 which is ignored
4043
# by the patched version of loaddata in the Django fork.
4144
"gis_tests.distapp.tests.DistanceTest.test_init",
45+
"gis_tests.distapp.tests.DistanceTest.test_distance_lookups",
46+
"gis_tests.distapp.tests.DistanceTest.test_distance_lookups_with_expression_rhs",
47+
"gis_tests.distapp.tests.DistanceTest.test_distance_annotation_group_by",
48+
"gis_tests.distapp.tests.DistanceFunctionsTests.test_distance_simple",
49+
"gis_tests.distapp.tests.DistanceFunctionsTests.test_distance_order_by",
4250
},
4351
"ImproperlyConfigured isn't raised when using RasterField": {
4452
# Normally RasterField.db_type() raises an error, but MongoDB
@@ -49,10 +57,13 @@ def django_test_skips(self):
4957
# Error: Index already exists with a different name
5058
"gis_tests.geoapp.test_indexes.SchemaIndexesTests.test_index_name",
5159
},
52-
"GIS lookups not supported.": {
53-
"gis_tests.geoapp.tests.GeoModelTest.test_gis_query_as_string",
60+
"GIS Union not supported.": {
5461
"gis_tests.geoapp.tests.GeoLookupTest.test_gis_lookups_with_complex_expressions",
5562
},
63+
"Subqueries not supported.": {
64+
"gis_tests.geoapp.tests.GeoLookupTest.test_subquery_annotation",
65+
"gis_tests.geoapp.tests.GeoQuerySetTest.test_within_subquery",
66+
},
5667
"GeoJSONSerializer doesn't support ObjectId.": {
5768
"gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_fields_option",
5869
"gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_geometry_field_option",
Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
from django.contrib.gis.db.models.lookups import GISLookup
2-
from django.db import NotSupportedError
1+
from django.contrib.gis.db.models.lookups import DistanceLookupFromFunction, GISLookup
32

3+
from django_mongodb_backend.query_utils import process_lhs, process_rhs
44

5-
def gis_lookup(self, compiler, connection, as_expr=False): # noqa: ARG001
6-
raise NotSupportedError(f"MongoDB does not support the {self.lookup_name} lookup.")
5+
6+
def _gis_lookup(self, compiler, connection, as_expr=False):
7+
lhs_mql = process_lhs(self, compiler, connection, as_expr=as_expr)
8+
rhs_mql = process_rhs(self, compiler, connection, as_expr=as_expr)
9+
rhs_op = self.get_rhs_op(connection, rhs_mql)
10+
return rhs_op.as_mql(lhs_mql, rhs_mql, self.rhs_params)
711

812

913
def register_lookups():
10-
GISLookup.as_mql = gis_lookup
14+
GISLookup.as_mql = _gis_lookup
15+
DistanceLookupFromFunction.as_mql = _gis_lookup

django_mongodb_backend/gis/operations.py

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,110 @@
1+
import warnings
2+
13
from django.contrib.gis import geos
24
from django.contrib.gis.db import models
35
from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations
6+
from django.contrib.gis.measure import Distance
7+
from django.db.backends.base.operations import BaseDatabaseOperations
48

59
from .adapter import Adapter
10+
from .utils import SpatialOperator
11+
12+
13+
def _gis_within_operator(field, value, op=None, params=None):
14+
print(f"Within value: {value}")
15+
return {
16+
field: {
17+
"$geoWithin": {
18+
"$geometry": {
19+
"type": value["type"],
20+
"coordinates": value["coordinates"],
21+
}
22+
}
23+
}
24+
}
25+
26+
27+
def _gis_intersects_operator(field, value, op=None, params=None):
28+
return {
29+
field: {
30+
"$geoIntersects": {
31+
"$geometry": {
32+
"type": value["type"],
33+
"coordinates": value["coordinates"],
34+
}
35+
}
36+
}
37+
}
38+
39+
40+
def _gis_disjoint_operator(field, value, op=None, params=None):
41+
return {
42+
field: {
43+
"$not": {
44+
"$geoIntersects": {
45+
"$geometry": {
46+
"type": value["type"],
47+
"coordinates": value["coordinates"],
48+
}
49+
}
50+
}
51+
}
52+
}
53+
654

55+
def _gis_contains_operator(field, value, op=None, params=None):
56+
value_type = value["type"]
57+
if value_type != "Point":
58+
warnings.warn(
59+
"MongoDB does not support strict contains on non-Point query geometries. Results will be for intersection."
60+
)
61+
return {
62+
field: {
63+
"$geoIntersects": {
64+
"$geometry": {
65+
"type": value_type,
66+
"coordinates": value["coordinates"],
67+
}
68+
}
69+
}
70+
}
771

8-
class GISOperations(BaseSpatialOperations):
72+
73+
def _gis_distance_operator(field, value, op=None, params=None):
74+
print(f"Distance: {params}")
75+
if hasattr(params[0], "m"):
76+
distance = params[0].m
77+
else:
78+
distance = params[0]
79+
if op == "distance_gt" or op == "distance_gte":
80+
cmd = {
81+
field: {
82+
"$not": {
83+
"$geoWithin": {
84+
"$centerSphere": [
85+
value["coordinates"],
86+
distance / 6378100, # radius of earth in meters
87+
],
88+
}
89+
}
90+
}
91+
}
92+
else:
93+
cmd = {
94+
field: {
95+
"$geoWithin": {
96+
"$centerSphere": [
97+
value["coordinates"],
98+
distance / 6378100, # radius of earth in meters
99+
],
100+
}
101+
}
102+
}
103+
print(f"Command: {cmd}")
104+
return cmd
105+
106+
107+
class GISOperations(BaseSpatialOperations, BaseDatabaseOperations):
9108
Adapter = Adapter
10109

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

19118
@property
20119
def gis_operators(self):
21-
return {}
120+
return {
121+
"contains": SpatialOperator("contains", _gis_contains_operator),
122+
"intersects": SpatialOperator("intersects", _gis_intersects_operator),
123+
"disjoint": SpatialOperator("disjoint", _gis_disjoint_operator),
124+
"within": SpatialOperator("within", _gis_within_operator),
125+
"distance_gt": SpatialOperator("distance_gt", _gis_distance_operator),
126+
"distance_gte": SpatialOperator("distance_gte", _gis_distance_operator),
127+
"distance_lt": SpatialOperator("distance_lt", _gis_distance_operator),
128+
"distance_lte": SpatialOperator("distance_lte", _gis_distance_operator),
129+
}
22130

23131
unsupported_functions = {
24132
"Area",
@@ -33,7 +141,6 @@ def gis_operators(self):
33141
"Centroid",
34142
"ClosestPoint",
35143
"Difference",
36-
"Distance",
37144
"Envelope",
38145
"ForcePolygonCW",
39146
"FromWKB",
@@ -95,3 +202,15 @@ def converter(value, expression, connection): # noqa: ARG001
95202
return geom_class(*value["coordinates"], srid=srid)
96203

97204
return converter
205+
206+
def get_distance(self, f, value, lookup_type):
207+
value = value[0]
208+
if isinstance(value, Distance):
209+
if f.geodetic(self.connection):
210+
raise ValueError(
211+
"Only numeric values of degree units are allowed on geodetic distance queries."
212+
)
213+
dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
214+
else:
215+
dist_param = value
216+
return [dist_param]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""
2+
A collection of utility routines and classes used by the spatial
3+
backend.
4+
"""
5+
6+
from django.contrib.gis.db.backends.utils import SpatialOperator as _SpatialOperator
7+
8+
9+
class SpatialOperator(_SpatialOperator):
10+
"""
11+
Class encapsulating the behavior specific to a GIS operation (used by lookups).
12+
"""
13+
14+
def __init__(self, op=None, func=None):
15+
self.op = op
16+
self.func = func
17+
18+
def as_mql(self, lhs, rhs, params=None):
19+
return self.func(lhs, rhs, self.op, params)

0 commit comments

Comments
 (0)