11
11
from django.db.backends.postgresql_psycopg2.base import DatabaseOperations
12
12
from django.db.utils import DatabaseError
13
13
from django.utils import six
14
from django.utils.functional import cached_property
16
from .models import GeometryColumns, SpatialRefSys
15
19
#### Classes used in constructing PostGIS spatial SQL ####
16
20
class PostGISOperator(SpatialOperation):
62
66
compiler_module = 'django.contrib.gis.db.models.sql.compiler'
69
geom_func_prefix = 'ST_'
65
70
version_regex = re.compile(r'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)')
66
71
valid_aggregates = dict([(k, None) for k in
67
72
('Collect', 'Extent', 'Extent3D', 'MakeLine', 'Union')])
72
77
def __init__(self, connection):
73
78
super(PostGISOperations, self).__init__(connection)
75
# Trying to get the PostGIS version because the function
76
# signatures will depend on the version used. The cost
77
# here is a database query to determine the version, which
78
# can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple
79
# comprising user-supplied values for the major, minor, and
80
# subminor revision of PostGIS.
82
if hasattr(settings, 'POSTGIS_VERSION'):
83
vtup = settings.POSTGIS_VERSION
85
# The user-supplied PostGIS version.
88
# This was the old documented way, but it's stupid to
92
vtup = self.postgis_version_tuple()
95
# Getting the prefix -- even though we don't officially support
96
# PostGIS 1.2 anymore, keeping it anyway in case a prefix change
97
# for something else is necessary.
98
if version >= (1, 2, 2):
103
self.geom_func_prefix = prefix
104
self.spatial_version = version
105
except DatabaseError:
106
raise ImproperlyConfigured(
107
'Cannot determine PostGIS version for database "%s". '
108
'GeoDjango requires at least PostGIS version 1.3. '
109
'Was the database created from a spatial database '
110
'template?' % self.connection.settings_dict['NAME']
112
# TODO: Raise helpful exceptions as they become known.
80
prefix = self.geom_func_prefix
114
81
# PostGIS-specific operators. The commented descriptions of these
115
82
# operators come from Section 7.6 of the PostGIS 1.4 documentation.
116
83
self.geometry_operators = {
188
155
self.geometry_functions.update(self.distance_functions)
190
157
# Only PostGIS versions 1.3.4+ have GeoJSON serialization support.
191
if version < (1, 3, 4):
158
if self.spatial_version < (1, 3, 4):
194
161
GEOJSON = prefix + 'AsGeoJson'
196
163
# ST_ContainsProperly ST_MakeLine, and ST_GeoHash added in 1.4.
197
if version >= (1, 4, 0):
164
if self.spatial_version >= (1, 4, 0):
198
165
GEOHASH = 'ST_GeoHash'
199
166
BOUNDINGCIRCLE = 'ST_MinimumBoundingCircle'
200
167
self.geometry_functions['contains_properly'] = PostGISFunction(prefix, 'ContainsProperly')
202
169
GEOHASH, BOUNDINGCIRCLE = False, False
204
171
# Geography type support added in 1.5.
205
if version >= (1, 5, 0):
172
if self.spatial_version >= (1, 5, 0):
206
173
self.geography = True
207
174
# Only a subset of the operators and functions are available
208
175
# for the geography type.
209
176
self.geography_functions = self.distance_functions.copy()
210
177
self.geography_functions.update({
211
'coveredby' : self.geometry_functions['coveredby'],
212
'covers' : self.geometry_functions['covers'],
213
'intersects' : self.geometry_functions['intersects'],
178
'coveredby': self.geometry_functions['coveredby'],
179
'covers': self.geometry_functions['covers'],
180
'intersects': self.geometry_functions['intersects'],
215
182
self.geography_operators = {
216
'bboverlaps' : PostGISOperator('&&'),
183
'bboverlaps': PostGISOperator('&&'),
219
186
# Native geometry type support added in PostGIS 2.0.
220
if version >= (2, 0, 0):
187
if self.spatial_version >= (2, 0, 0):
221
188
self.geometry = True
223
190
# Creating a dictionary lookup of all GIS terms for PostGIS.
224
gis_terms = ['isnull']
225
gis_terms += list(self.geometry_operators)
226
gis_terms += list(self.geometry_functions)
227
self.gis_terms = dict([(term, None) for term in gis_terms])
191
self.gis_terms = set(['isnull'])
192
self.gis_terms.update(self.geometry_operators)
193
self.gis_terms.update(self.geometry_functions)
229
195
self.area = prefix + 'Area'
230
196
self.bounding_circle = BOUNDINGCIRCLE
247
213
self.makeline = prefix + 'MakeLine'
248
214
self.mem_size = prefix + 'mem_size'
249
215
self.num_geom = prefix + 'NumGeometries'
250
self.num_points =prefix + 'npoints'
216
self.num_points = prefix + 'npoints'
251
217
self.perimeter = prefix + 'Perimeter'
252
218
self.point_on_surface = prefix + 'PointOnSurface'
253
219
self.polygonize = prefix + 'Polygonize'
261
227
self.union = prefix + 'Union'
262
228
self.unionagg = prefix + 'Union'
264
if version >= (2, 0, 0):
230
if self.spatial_version >= (2, 0, 0):
265
231
self.extent3d = prefix + '3DExtent'
266
232
self.length3d = prefix + '3DLength'
267
233
self.perimeter3d = prefix + '3DPerimeter'
270
236
self.length3d = prefix + 'Length3D'
271
237
self.perimeter3d = prefix + 'Perimeter3D'
240
def spatial_version(self):
241
"""Determine the version of the PostGIS library."""
242
# Trying to get the PostGIS version because the function
243
# signatures will depend on the version used. The cost
244
# here is a database query to determine the version, which
245
# can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple
246
# comprising user-supplied values for the major, minor, and
247
# subminor revision of PostGIS.
248
if hasattr(settings, 'POSTGIS_VERSION'):
249
version = settings.POSTGIS_VERSION
252
vtup = self.postgis_version_tuple()
253
except DatabaseError:
254
raise ImproperlyConfigured(
255
'Cannot determine PostGIS version for database "%s". '
256
'GeoDjango requires at least PostGIS version 1.3. '
257
'Was the database created from a spatial database '
258
'template?' % self.connection.settings_dict['NAME']
273
263
def check_aggregate_support(self, aggregate):
275
265
Checks if the given aggregate name is supported (that is, if it's
324
314
raise NotImplementedError('PostGIS 1.5 supports geography columns '
325
315
'only with an SRID of 4326.')
327
return 'geography(%s,%d)'% (f.geom_type, f.srid)
317
return 'geography(%s,%d)' % (f.geom_type, f.srid)
328
318
elif self.geometry:
329
319
# Postgis 2.0 supports type-based geometries.
330
320
# TODO: Support 'M' extension.
403
393
Helper routine for calling PostGIS functions and returning their result.
405
cursor = self.connection._cursor()
408
cursor.execute('SELECT %s()' % func)
409
row = cursor.fetchone()
411
# Responsibility of callers to perform error handling.
414
# Close out the connection. See #9437.
415
self.connection.close()
395
# Close out the connection. See #9437.
396
with self.connection.temporary_connection() as cursor:
397
cursor.execute('SELECT %s()' % func)
398
return cursor.fetchone()[0]
418
400
def postgis_geos_version(self):
419
401
"Returns the version of the GEOS library used with PostGIS."
561
543
elif lookup_type == 'isnull':
562
544
# Handling 'isnull' lookup type
563
return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
545
return "%s IS %sNULL" % (geo_col, ('' if value else 'NOT ')), []
565
547
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
573
555
if not self.check_aggregate_support(agg):
574
556
raise NotImplementedError('%s spatial aggregate is not implmented for this backend.' % agg_name)
575
557
agg_name = agg_name.lower()
576
if agg_name == 'union': agg_name += 'agg'
558
if agg_name == 'union':
577
560
sql_template = '%(function)s(%(field)s)'
578
561
sql_function = getattr(self, agg_name)
579
562
return sql_template, sql_function
581
564
# Routines for getting the OGC-compliant models.
582
565
def geometry_columns(self):
583
from django.contrib.gis.db.backends.postgis.models import GeometryColumns
584
566
return GeometryColumns
586
568
def spatial_ref_sys(self):
587
from django.contrib.gis.db.backends.postgis.models import SpatialRefSys
588
569
return SpatialRefSys