From ffcc38ebe819fe8f46dd4a0941d202fcde5adb1d Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Sun, 29 Jul 2007 19:58:24 +0000 Subject: gis: created backend module, that will (in the future) allow for support of different spatial databases; improved postgis-specific code in preparation for PostGIS 1.2.2 (and beyond). git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@5776 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/gis/db/backend/__init__.py | 268 +++++++++++++++++ django/contrib/gis/db/backend/postgis/__init__.py | 7 + django/contrib/gis/db/backend/postgis/creation.py | 190 ++++++++++++ django/contrib/gis/db/backend/postgis/field.py | 107 +++++++ .../contrib/gis/db/backend/postgis/management.py | 36 +++ django/contrib/gis/db/backend/postgis/query.py | 127 ++++++++ django/contrib/gis/db/models/fields/__init__.py | 111 +------ django/contrib/gis/db/models/postgis.py | 331 --------------------- django/contrib/gis/db/models/query.py | 2 +- django/contrib/gis/tests/__init__.py | 2 +- django/contrib/gis/tests/geoapp/tests.py | 2 +- django/contrib/gis/utils/spatial_db.py | 190 ------------ 12 files changed, 740 insertions(+), 633 deletions(-) create mode 100644 django/contrib/gis/db/backend/__init__.py create mode 100644 django/contrib/gis/db/backend/postgis/__init__.py create mode 100644 django/contrib/gis/db/backend/postgis/creation.py create mode 100644 django/contrib/gis/db/backend/postgis/field.py create mode 100644 django/contrib/gis/db/backend/postgis/management.py create mode 100644 django/contrib/gis/db/backend/postgis/query.py delete mode 100644 django/contrib/gis/db/models/postgis.py delete mode 100644 django/contrib/gis/utils/spatial_db.py diff --git a/django/contrib/gis/db/backend/__init__.py b/django/contrib/gis/db/backend/__init__.py new file mode 100644 index 0000000000..afda586dea --- /dev/null +++ b/django/contrib/gis/db/backend/__init__.py @@ -0,0 +1,268 @@ +""" + This module provides the backend for spatial SQL construction with Django. + + Specifically, this module will import the correct routines and modules + needed for GeoDjango + + (1) GeoBackEndField, a base class needed for GeometryField. + (2) The parse_lookup() function, used for spatial SQL construction by + the GeoQuerySet. + + Currently only PostGIS is supported, but someday backends will be aded for + additional spatial databases. +""" +from django.conf import settings +from django.db import backend +from django.db.models.query import LOOKUP_SEPARATOR, field_choices, find_field, FieldFound, QUERY_TERMS, get_where_clause +from django.utils.datastructures import SortedDict + +if settings.DATABASE_ENGINE == 'postgresql_psycopg2': + # PostGIS is the spatial database, getting the rquired modules, renaming as necessary. + from postgis import \ + PostGISField as GeoBackendField, \ + POSTGIS_TERMS as GIS_TERMS, \ + create_spatial_db, get_geo_where_clause +else: + raise NotImplementedError, 'No Geographic Backend exists for %s' % settings.DATABASE_NAME + +#### query.py overloaded functions #### +# parse_lookup() and lookup_inner() are modified from their django/db/models/query.py +# counterparts to support constructing SQL for geographic queries. +# +# Status: Synced with r5609. +# +def parse_lookup(kwarg_items, opts): + # Helper function that handles converting API kwargs + # (e.g. "name__exact": "tom") to SQL. + # Returns a tuple of (joins, where, params). + + # 'joins' is a sorted dictionary describing the tables that must be joined + # to complete the query. The dictionary is sorted because creation order + # is significant; it is a dictionary to ensure uniqueness of alias names. + # + # Each key-value pair follows the form + # alias: (table, join_type, condition) + # where + # alias is the AS alias for the joined table + # table is the actual table name to be joined + # join_type is the type of join (INNER JOIN, LEFT OUTER JOIN, etc) + # condition is the where-like statement over which narrows the join. + # alias will be derived from the lookup list name. + # + # At present, this method only every returns INNER JOINs; the option is + # there for others to implement custom Q()s, etc that return other join + # types. + joins, where, params = SortedDict(), [], [] + + for kwarg, value in kwarg_items: + path = kwarg.split(LOOKUP_SEPARATOR) + # Extract the last elements of the kwarg. + # The very-last is the lookup_type (equals, like, etc). + # The second-last is the table column on which the lookup_type is + # to be performed. If this name is 'pk', it will be substituted with + # the name of the primary key. + # If there is only one part, or the last part is not a query + # term, assume that the query is an __exact + lookup_type = path.pop() + if lookup_type == 'pk': + lookup_type = 'exact' + path.append(None) + elif len(path) == 0 or not ((lookup_type in QUERY_TERMS) or (lookup_type in GIS_TERMS)): + path.append(lookup_type) + lookup_type = 'exact' + + if len(path) < 1: + raise TypeError, "Cannot parse keyword query %r" % kwarg + + if value is None: + # Interpret '__exact=None' as the sql '= NULL'; otherwise, reject + # all uses of None as a query value. + if lookup_type != 'exact': + raise ValueError, "Cannot use None as a query value" + elif callable(value): + value = value() + + joins2, where2, params2 = lookup_inner(path, lookup_type, value, opts, opts.db_table, None) + joins.update(joins2) + where.extend(where2) + params.extend(params2) + return joins, where, params + +def lookup_inner(path, lookup_type, value, opts, table, column): + qn = backend.quote_name + joins, where, params = SortedDict(), [], [] + current_opts = opts + current_table = table + current_column = column + intermediate_table = None + join_required = False + + name = path.pop(0) + # Has the primary key been requested? If so, expand it out + # to be the name of the current class' primary key + if name is None or name == 'pk': + name = current_opts.pk.name + + # Try to find the name in the fields associated with the current class + try: + # Does the name belong to a defined many-to-many field? + field = find_field(name, current_opts.many_to_many, False) + if field: + new_table = current_table + '__' + name + new_opts = field.rel.to._meta + new_column = new_opts.pk.column + + # Need to create an intermediate table join over the m2m table + # This process hijacks current_table/column to point to the + # intermediate table. + current_table = "m2m_" + new_table + intermediate_table = field.m2m_db_table() + join_column = field.m2m_reverse_name() + intermediate_column = field.m2m_column_name() + + raise FieldFound + + # Does the name belong to a reverse defined many-to-many field? + field = find_field(name, current_opts.get_all_related_many_to_many_objects(), True) + if field: + new_table = current_table + '__' + name + new_opts = field.opts + new_column = new_opts.pk.column + + # Need to create an intermediate table join over the m2m table. + # This process hijacks current_table/column to point to the + # intermediate table. + current_table = "m2m_" + new_table + intermediate_table = field.field.m2m_db_table() + join_column = field.field.m2m_column_name() + intermediate_column = field.field.m2m_reverse_name() + + raise FieldFound + + # Does the name belong to a one-to-many field? + field = find_field(name, current_opts.get_all_related_objects(), True) + if field: + new_table = table + '__' + name + new_opts = field.opts + new_column = field.field.column + join_column = opts.pk.column + + # 1-N fields MUST be joined, regardless of any other conditions. + join_required = True + + raise FieldFound + + # Does the name belong to a one-to-one, many-to-one, or regular field? + field = find_field(name, current_opts.fields, False) + if field: + if field.rel: # One-to-One/Many-to-one field + new_table = current_table + '__' + name + new_opts = field.rel.to._meta + new_column = new_opts.pk.column + join_column = field.column + raise FieldFound + elif path: + # For regular fields, if there are still items on the path, + # an error has been made. We munge "name" so that the error + # properly identifies the cause of the problem. + name += LOOKUP_SEPARATOR + path[0] + else: + raise FieldFound + + except FieldFound: # Match found, loop has been shortcut. + pass + else: # No match found. + choices = field_choices(current_opts.many_to_many, False) + \ + field_choices(current_opts.get_all_related_many_to_many_objects(), True) + \ + field_choices(current_opts.get_all_related_objects(), True) + \ + field_choices(current_opts.fields, False) + raise TypeError, "Cannot resolve keyword '%s' into field. Choices are: %s" % (name, ", ".join(choices)) + + # Check whether an intermediate join is required between current_table + # and new_table. + if intermediate_table: + joins[qn(current_table)] = ( + qn(intermediate_table), "LEFT OUTER JOIN", + "%s.%s = %s.%s" % (qn(table), qn(current_opts.pk.column), qn(current_table), qn(intermediate_column)) + ) + + if path: + # There are elements left in the path. More joins are required. + if len(path) == 1 and path[0] in (new_opts.pk.name, None) \ + and lookup_type in ('exact', 'isnull') and not join_required: + # If the next and final name query is for a primary key, + # and the search is for isnull/exact, then the current + # (for N-1) or intermediate (for N-N) table can be used + # for the search. No need to join an extra table just + # to check the primary key. + new_table = current_table + else: + # There are 1 or more name queries pending, and we have ruled out + # any shortcuts; therefore, a join is required. + joins[qn(new_table)] = ( + qn(new_opts.db_table), "INNER JOIN", + "%s.%s = %s.%s" % (qn(current_table), qn(join_column), qn(new_table), qn(new_column)) + ) + # If we have made the join, we don't need to tell subsequent + # recursive calls about the column name we joined on. + join_column = None + + # There are name queries remaining. Recurse deeper. + joins2, where2, params2 = lookup_inner(path, lookup_type, value, new_opts, new_table, join_column) + + joins.update(joins2) + where.extend(where2) + params.extend(params2) + else: + # No elements left in path. Current element is the element on which + # the search is being performed. + + if join_required: + # Last query term is a RelatedObject + if field.field.rel.multiple: + # RelatedObject is from a 1-N relation. + # Join is required; query operates on joined table. + column = new_opts.pk.name + joins[qn(new_table)] = ( + qn(new_opts.db_table), "INNER JOIN", + "%s.%s = %s.%s" % (qn(current_table), qn(join_column), qn(new_table), qn(new_column)) + ) + current_table = new_table + else: + # RelatedObject is from a 1-1 relation, + # No need to join; get the pk value from the related object, + # and compare using that. + column = current_opts.pk.name + elif intermediate_table: + # Last query term is a related object from an N-N relation. + # Join from intermediate table is sufficient. + column = join_column + elif name == current_opts.pk.name and lookup_type in ('exact', 'isnull') and current_column: + # Last query term is for a primary key. If previous iterations + # introduced a current/intermediate table that can be used to + # optimize the query, then use that table and column name. + column = current_column + else: + # Last query term was a normal field. + column = field.column + + # If the field is a geometry field, then the WHERE clause will need to be obtained + # with the get_geo_where_clause() + if hasattr(field, '_geom'): + # Getting the geographic where clause. + gwc = get_geo_where_clause(lookup_type, current_table + '.', column, value) + + # Getting the geographic parameters from the field. + geo_params = field.get_db_prep_lookup(lookup_type, value) + + # If a dictionary was passed back from the field modify the where clause. + if isinstance(geo_params, dict): + gwc = gwc % geo_params['where'] + geo_params = geo_params['params'] + where.append(gwc) + params.extend(geo_params) + else: + where.append(get_where_clause(lookup_type, current_table + '.', column, value)) + params.extend(field.get_db_prep_lookup(lookup_type, value)) + + return joins, where, params diff --git a/django/contrib/gis/db/backend/postgis/__init__.py b/django/contrib/gis/db/backend/postgis/__init__.py new file mode 100644 index 0000000000..0564ad7348 --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/__init__.py @@ -0,0 +1,7 @@ +""" + The PostGIS spatial database backend module. +""" +from query import get_geo_where_clause, GEOM_FUNC_PREFIX, POSTGIS_TERMS +from creation import create_spatial_db +from field import PostGISField + diff --git a/django/contrib/gis/db/backend/postgis/creation.py b/django/contrib/gis/db/backend/postgis/creation.py new file mode 100644 index 0000000000..0fa618b4df --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/creation.py @@ -0,0 +1,190 @@ +from django.conf import settings +from django.core.management import syncdb +from django.db import connection, backend +from django.test.utils import _set_autocommit, TEST_DATABASE_PREFIX +from commands import getstatusoutput +import os, re, sys + +def create_lang(db_name, verbosity=1): + "This sets up the pl/pgsql language on the given database." + + # Getting the command-line options for the shell command + options = get_cmd_options(db_name) + + # Constructing the 'createlang' command. + createlang_cmd = 'createlang %splpgsql' % options + if verbosity >= 1: print createlang_cmd + + # Must have database super-user privileges to execute createlang -- it must + # also be in your path. + status, output = getstatusoutput(createlang_cmd) + + # Checking the status of the command, 0 => execution successful + if status != 0: + raise Exception, "Error executing 'plpgsql' command: %s\n" % output + + +def _create_with_cursor(db_name, verbosity=1, autoclobber=False): + "Creates database with psycopg2 cursor." + + # Constructing the necessary SQL to create the database (the DATABASE_USER + # must possess the privileges to create a database) + create_sql = 'CREATE DATABASE %s OWNER %s' % (backend.quote_name(db_name), + settings.DATABASE_USER) + cursor = connection.cursor() + _set_autocommit(connection) + + try: + # Trying to create the database first. + cursor.execute(create_sql) + except Exception, e: + # Drop and recreate, if necessary. + if not autoclobber: + confirm = raw_input("\nIt appears the database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % db_name) + if autoclobber or confirm == 'yes': + if verbosity >= 1: print 'Destroying old spatial database...' + drop_db(db_name) + if verbosity >= 1: print 'Creating new spatial database...' + cursor.execute(create_sql) + else: + raise Exception, 'Spatial Database Creation canceled.' + +created_regex = re.compile(r'^createdb: database creation failed: ERROR: database ".+" already exists') +def _create_with_shell(db_name, verbosity=1, autoclobber=False): + """If no spatial database already exists, then using a cursor will not work. Thus, a + `createdb` command will be issued through the shell to bootstrap the database.""" + + # Getting the command-line options for the shell command + options = get_cmd_options(False) + create_cmd = 'createdb -O %s %s%s' % (settings.DATABASE_USER, options, db_name) + if verbosity >= 1: print create_cmd + + # Attempting to create the database. + status, output = getstatusoutput(create_cmd) + if status != 0: + if created_regex.match(output): + if not autoclobber: + confirm = raw_input("\nIt appears the database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % db_name) + if autoclobber or confirm == 'yes': + if verbosity >= 1: print 'Destroying old spatial database...' + drop_cmd = 'dropdb %s%s' % (options, db_name) + status, output = getstatusoutput(drop_cmd) + if status != 0: + raise Exception, 'Could not drop database %s: %s' % (db_name, output) + if verbosity >= 1: print 'Creating new spatial database...' + status, output = getstatusoutput(create_cmd) + if status != 0: + raise Exception, 'Could not create database after dropping: %s' % output + else: + raise Exception, 'Spatial Database Creation canceled.' + else: + raise Exception, 'Unknown error occurred in creating database: %s' % output + +def create_spatial_db(test=False, verbosity=1, autoclobber=False, interactive=False): + "This Python routine creates a spatial database based on settings.py." + + # Making sure we're using PostgreSQL and psycopg2 + if settings.DATABASE_ENGINE != 'postgresql_psycopg2': + raise Exception, 'Spatial database creation only supported postgresql_psycopg2 platform.' + + # This routine depends on getstatusoutput(), which does not work on Windows. + # TODO: Consider executing shell commands with popen for Windows compatibility + if os.name == 'nt': + raise Exception, 'Automatic spatial database creation only supported on *NIX platforms.' + + # Getting the spatial database name + if test: + db_name = get_spatial_db(test=True) + _create_with_cursor(db_name, verbosity=verbosity, autoclobber=autoclobber) + else: + db_name = get_spatial_db() + _create_with_shell(db_name, verbosity=verbosity, autoclobber=autoclobber) + + # Creating the db language. + create_lang(db_name, verbosity=verbosity) + + # Now adding in the PostGIS routines. + load_postgis_sql(db_name, verbosity=verbosity) + + if verbosity >= 1: print 'Creation of spatial database %s successful.' % db_name + + # Closing the connection + connection.close() + settings.DATABASE_NAME = db_name + + # Syncing the database + syncdb(verbosity, interactive=interactive) + + # Get a cursor (even though we don't need one yet). This has + # the side effect of initializing the test database. + cursor = connection.cursor() + +def drop_db(db_name=False, test=False): + "Using the cursor, drops the given database. All exceptions will be propagated up." + if not db_name: db_name = get_spatial_db(test=test) + cursor = connection.cursor() + cursor.execute("DROP DATABASE %s" % backend.quote_name(db_name)) + +def get_cmd_options(db_name): + "Obtains the command-line PostgreSQL connection options for shell commands." + # The db_name parameter is optional + if db_name: + options = '-d %s -U %s ' % (db_name, settings.DATABASE_USER) + else: + options = '-U %s ' % settings.DATABASE_USER + if settings.DATABASE_HOST: + options += '-h %s ' % settings.DATABASE_HOST + if settings.DATABASE_PORT: + options += '-p %s ' % settings.DATABASE_PORT + return options + +def get_spatial_db(test=False): + """This routine returns the name of the spatial database. + Set the 'test' keyword for the test spatial database name.""" + if test: + if settings.TEST_DATABASE_NAME: + test_db_name = settings.TEST_DATABASE_NAME + else: + test_db_name = TEST_DATABASE_PREFIX + settings.DATABASE_NAME + return test_db_name + else: + if not settings.DATABASE_NAME: + raise Exception, 'must configure DATABASE_NAME in settings.py' + return settings.DATABASE_NAME + +def load_postgis_sql(db_name, verbosity=1): + "This routine loads up the PostGIS SQL files lwpostgis.sql and spatial_ref_sys.sql." + + # Getting the path to the PostGIS SQL + try: + # POSTGIS_SQL_PATH may be placed in settings to tell GeoDjango where the + # PostGIS SQL files are located + sql_path = settings.POSTGIS_SQL_PATH + except AttributeError: + sql_path = '/usr/local/share' + + # The PostGIS SQL post-creation files. + lwpostgis_file = os.path.join(sql_path, 'lwpostgis.sql') + srefsys_file = os.path.join(sql_path, 'spatial_ref_sys.sql') + if not os.path.isfile(lwpostgis_file): + raise Exception, 'Could not find PostGIS function definitions in %s' % lwpostgis_file + if not os.path.isfile(srefsys_file): + raise Exception, 'Could not find PostGIS spatial reference system definitions in %s' % srefsys_file + + # Getting the psql command-line options. + options = get_cmd_options(db_name) + + # Now trying to load up the PostGIS functions + cmd = 'psql %s-f %s' % (options, lwpostgis_file) + if verbosity >= 1: print cmd + status, output = getstatusoutput(cmd) + if status != 0: + raise Exception, 'Error in loading PostGIS lwgeometry routines.' + + # Now trying to load up the Spatial Reference System table + cmd = 'psql %s-f %s' % (options, srefsys_file) + if verbosity >= 1: print cmd + status, output = getstatusoutput(cmd) + if status !=0: + raise Exception, 'Error in loading PostGIS spatial_ref_sys table.' + diff --git a/django/contrib/gis/db/backend/postgis/field.py b/django/contrib/gis/db/backend/postgis/field.py new file mode 100644 index 0000000000..09bc4e642b --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/field.py @@ -0,0 +1,107 @@ +from django.db.models.fields import Field # Django base Field class +from django.contrib.gis.geos import GEOSGeometry, GEOSException +from types import StringType +from query import POSTGIS_TERMS, quotename + +class PostGISField(Field): + def _add_geom(self, style, db_table): + """Constructs the addition of the geometry to the table using the + AddGeometryColumn(...) PostGIS (and OGC standard) stored procedure. + + Takes the style object (provides syntax highlighting) and the + database table as parameters. + """ + sql = style.SQL_KEYWORD('SELECT ') + \ + style.SQL_TABLE('AddGeometryColumn') + '(' + \ + style.SQL_TABLE(quotename(db_table)) + ', ' + \ + style.SQL_FIELD(quotename(self.column)) + ', ' + \ + style.SQL_FIELD(str(self._srid)) + ', ' + \ + style.SQL_COLTYPE(quotename(self._geom)) + ', ' + \ + style.SQL_KEYWORD(str(self._dim)) + ');' + + if not self.null: + # Add a NOT NULL constraint to the field + sql += '\n' + \ + style.SQL_KEYWORD('ALTER TABLE ') + \ + style.SQL_TABLE(quotename(db_table, dbl=True)) + \ + style.SQL_KEYWORD(' ALTER ') + \ + style.SQL_FIELD(quotename(self.column, dbl=True)) + \ + style.SQL_KEYWORD(' SET NOT NULL') + ';' + return sql + + def _geom_index(self, style, db_table, + index_type='GIST', index_opts='GIST_GEOMETRY_OPS'): + "Creates a GiST index for this geometry field." + sql = style.SQL_KEYWORD('CREATE INDEX ') + \ + style.SQL_TABLE(quotename('%s_%s_id' % (db_table, self.column), dbl=True)) + \ + style.SQL_KEYWORD(' ON ') + \ + style.SQL_TABLE(quotename(db_table, dbl=True)) + \ + style.SQL_KEYWORD(' USING ') + \ + style.SQL_COLTYPE(index_type) + ' ( ' + \ + style.SQL_FIELD(quotename(self.column, dbl=True)) + ' ' + \ + style.SQL_KEYWORD(index_opts) + ' );' + return sql + + def _post_create_sql(self, style, db_table): + """Returns SQL that will be executed after the model has been + created. Geometry columns must be added after creation with the + PostGIS AddGeometryColumn() function.""" + + # Getting the AddGeometryColumn() SQL necessary to create a PostGIS + # geometry field. + post_sql = self._add_geom(style, db_table) + + # If the user wants to index this data, then get the indexing SQL as well. + if self._index: + return '%s\n%s' % (post_sql, self._geom_index(style, db_table)) + else: + return post_sql + + def _post_delete_sql(self, style, db_table): + "Drops the geometry column." + sql = style.SQL_KEYWORD('SELECT ') + \ + style.SQL_KEYWORD('DropGeometryColumn') + '(' + \ + style.SQL_TABLE(quotename(db_table)) + ', ' + \ + style.SQL_FIELD(quotename(self.column)) + ');' + return sql + + def get_db_prep_lookup(self, lookup_type, value): + "Returns field's value prepared for database lookup, accepts WKT and GEOS Geometries for the value." + if lookup_type in POSTGIS_TERMS: + if lookup_type == 'isnull': return [value] # special case for NULL geometries. + if not bool(value): return [None] # If invalid value passed in. + if isinstance(value, GEOSGeometry): + # GEOSGeometry instance passed in. + if value.srid != self._srid: + # Returning a dictionary instructs the parse_lookup() to add what's in the 'where' key + # to the where parameters, since we need to transform the geometry in the query. + return {'where' : "Transform(%s,%s)", + 'params' : [value, self._srid] + } + else: + # Just return the GEOSGeometry, it has its own psycopg2 adaptor. + return [value] + elif isinstance(value, StringType): + # String instance passed in, assuming WKT. + # TODO: Any validation needed here to prevent SQL injection? + return ["SRID=%d;%s" % (self._srid, value)] + else: + raise TypeError("Invalid type (%s) used for field lookup value." % str(type(value))) + else: + raise TypeError("Field has invalid lookup: %s" % lookup_type) + + def get_db_prep_save(self, value): + "Prepares the value for saving in the database." + if not bool(value): return None + if isinstance(value, GEOSGeometry): + return value + else: + return ("SRID=%d;%s" % (self._srid, wkt)) + + def get_placeholder(self, value): + "Provides a proper substitution value for " + if isinstance(value, GEOSGeometry) and value.srid != self._srid: + # Adding Transform() to the SQL placeholder. + return 'Transform(%%s, %s)' % self._srid + else: + return '%s' diff --git a/django/contrib/gis/db/backend/postgis/management.py b/django/contrib/gis/db/backend/postgis/management.py new file mode 100644 index 0000000000..33ff44066f --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/management.py @@ -0,0 +1,36 @@ +""" + This utility module is for obtaining information about the PostGIS installation. + + See PostGIS docs at Ch. 6.2.1 for more information on these functions. +""" + +def _get_postgis_func(func): + "Helper routine for calling PostGIS functions and returning their result." + from django.db import connection + cursor = connection.cursor() + cursor.execute('SELECT %s()' % func) + row = cursor.fetchone() + cursor.close() + return row[0] + +def postgis_geos_version(): + "Returns the version of the GEOS library used with PostGIS." + return _get_postgis_func('postgis_geos_version') + +def postgis_lib_version(): + "Returns the version number of the PostGIS library used with PostgreSQL." + return _get_postgis_func('postgis_lib_version') + +def postgis_proj_version(): + "Returns the version of the PROJ.4 library used with PostGIS." + return _get_postgis_func('postgis_proj_version') + +def postgis_version(): + "Returns PostGIS version number and compile-time options." + return _get_postgis_func('postgis_version') + +def postgis_full_version(): + "Returns PostGIS version number and compile-time options." + return _get_postgis_func('postgis_full_version') + + diff --git a/django/contrib/gis/db/backend/postgis/query.py b/django/contrib/gis/db/backend/postgis/query.py new file mode 100644 index 0000000000..3e80d285f8 --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/query.py @@ -0,0 +1,127 @@ +""" + This module contains the spatial lookup types, and the get_geo_where_clause() + routine for PostGIS. +""" +from django.db import backend +from management import postgis_lib_version + +# Getting the PostGIS version +POSTGIS_VERSION = postgis_lib_version() +MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = map(int, POSTGIS_VERSION.split('.')) + +# The supported PostGIS versions. +# TODO: Confirm tests with PostGIS versions 1.1.x -- should work. Versions <= 1.0.x didn't use GEOS C API. +if MAJOR_VERSION != 1 or MINOR_VERSION1 <= 1: + raise Exception, 'PostGIS version %s not supported.' % POSTGIS_VERSION + +# PostGIS-specific operators. The commented descriptions of these +# operators come from Section 6.2.2 of the official PostGIS documentation. +POSTGIS_OPERATORS = { + # The "&<" operator returns true if A's bounding box overlaps or is to the left of B's bounding box. + 'overlaps_left' : '&<', + # The "&>" operator returns true if A's bounding box overlaps or is to the right of B's bounding box. + 'overlaps_right' : '&>', + # The "<<" operator returns true if A's bounding box is strictly to the left of B's bounding box. + 'left' : '<<', + # The ">>" operator returns true if A's bounding box is strictly to the right of B's bounding box. + 'right' : '>>', + # The "&<|" operator returns true if A's bounding box overlaps or is below B's bounding box. + 'overlaps_below' : '&<|', + # The "|&>" operator returns true if A's bounding box overlaps or is above B's bounding box. + 'overlaps_above' : '|&>', + # The "<<|" operator returns true if A's bounding box is strictly below B's bounding box. + 'strictly_below' : '<<|', + # The "|>>" operator returns true if A's bounding box is strictly above B's bounding box. + 'strictly_above' : '|>>', + # The "~=" operator is the "same as" operator. It tests actual geometric equality of two features. So if + # A and B are the same feature, vertex-by-vertex, the operator returns true. + 'same_as' : '~=', + 'exact' : '~=', + # The "@" operator returns true if A's bounding box is completely contained by B's bounding box. + 'contained' : '@', + # The "~" operator returns true if A's bounding box completely contains B's bounding box. + 'bbcontains' : '~', + # The "&&" operator is the "overlaps" operator. If A's bounding boux overlaps B's bounding box the + # operator returns true. + 'bboverlaps' : '&&', + } + +# PostGIS Geometry Relationship Functions -- most of these use GEOS. +# +# For PostGIS >= 1.2.2 these routines will do a bounding box query first before calling +# the more expensive GEOS routines (called 'inline index magic'). +# +POSTGIS_GEOMETRY_FUNCTIONS = { + 'equals' : '%sEquals', + 'disjoint' : '%sDisjoint', + 'touches' : '%sTouches', + 'crosses' : '%sCrosses', + 'within' : '%sWithin', + 'overlaps' : '%sOverlaps', + 'contains' : '%sContains', + 'intersects' : '%sIntersects', + 'relate' : ('%sRelate', str), + } + +# Versions of PostGIS >= 1.2.2 changed their naming convention to be 'SQL-MM-centric'. +# Practically, this means that 'ST_' is appended to geometry function names. +if MINOR_VERSION1 >= 2 and MINOR_VERSION2 >= 2: + # The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2. + POSTGIS_GEOMETRY_FUNCTIONS.update( + {'dwithin' : ('%sDWithin', float), + 'coveredby' : '%sCoveredBy', + 'covers' : '%sCovers', + } + ) + GEOM_FUNC_PREFIX = 'ST_' +else: + GEOM_FUNC_PREFIX = '' + +# Updating with the geometry function prefix. +for k, v in POSTGIS_GEOMETRY_FUNCTIONS.items(): + if isinstance(v, tuple): + v = list(v) + v[0] = v[0] % GEOM_FUNC_PREFIX + v = tuple(v) + else: + v = v % GEOM_FUNC_PREFIX + POSTGIS_GEOMETRY_FUNCTIONS[k] = v + +# Any other lookup types that do not require a mapping. +MISC_TERMS = ['isnull'] + +# The quotation used for postgis (uses single quotes). +def quotename(value, dbl=False): + if dbl: return '"%s"' % value + else: return "'%s'" % value + +# These are the PostGIS-customized QUERY_TERMS -- a list of the lookup types +# allowed for geographic queries. +POSTGIS_TERMS = list(POSTGIS_OPERATORS.keys()) # Getting the operators first +POSTGIS_TERMS += list(POSTGIS_GEOMETRY_FUNCTIONS.keys()) # Adding on the Geometry Functions +POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull') +POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable + +def get_geo_where_clause(lookup_type, table_prefix, field_name, value): + "Returns the SQL WHERE clause for use in PostGIS SQL construction." + if table_prefix.endswith('.'): + table_prefix = backend.quote_name(table_prefix[:-1])+'.' + field_name = backend.quote_name(field_name) + + # See if a PostGIS operator matches the lookup type first + try: + return '%s%s %s %%s' % (table_prefix, field_name, POSTGIS_OPERATORS[lookup_type]) + except KeyError: + pass + + # See if a PostGIS Geometry function matches the lookup type next + try: + return '%s(%s%s, %%s)' % (POSTGIS_GEOMETRY_FUNCTIONS[lookup_type], table_prefix, field_name) + except KeyError: + pass + + # Handling 'isnull' lookup type + if lookup_type == 'isnull': + return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or '')) + + raise TypeError, "Got invalid lookup_type: %s" % repr(lookup_type) diff --git a/django/contrib/gis/db/models/fields/__init__.py b/django/contrib/gis/db/models/fields/__init__.py index d27208dffe..e004d3bbea 100644 --- a/django/contrib/gis/db/models/fields/__init__.py +++ b/django/contrib/gis/db/models/fields/__init__.py @@ -1,15 +1,10 @@ -# The Django base Field class. -from django.db.models.fields import Field +from django.contrib.gis.db.backend import GeoBackendField # depends on the spatial database backend. from django.contrib.gis.db.models.proxy import GeometryProxy -from django.contrib.gis.db.models.postgis import POSTGIS_TERMS, quotename from django.contrib.gis.oldforms import WKTField from django.utils.functional import curry -from django.contrib.gis.geos import GEOSGeometry, GEOSException -from types import StringType #TODO: Flesh out widgets. - -class GeometryField(Field): +class GeometryField(GeoBackendField): "The base GIS field -- maps to the OpenGIS Specification Geometry type." # The OpenGIS Geometry name. @@ -33,67 +28,6 @@ class GeometryField(Field): self._dim = dim super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function - def _add_geom(self, style, db_table): - """Constructs the addition of the geometry to the table using the - AddGeometryColumn(...) PostGIS (and OGC standard) stored procedure. - - Takes the style object (provides syntax highlighting) and the - database table as parameters. - """ - sql = style.SQL_KEYWORD('SELECT ') + \ - style.SQL_TABLE('AddGeometryColumn') + '(' + \ - style.SQL_TABLE(quotename(db_table)) + ', ' + \ - style.SQL_FIELD(quotename(self.column)) + ', ' + \ - style.SQL_FIELD(str(self._srid)) + ', ' + \ - style.SQL_COLTYPE(quotename(self._geom)) + ', ' + \ - style.SQL_KEYWORD(str(self._dim)) + ');' - - if not self.null: - # Add a NOT NULL constraint to the field - sql += '\n' + \ - style.SQL_KEYWORD('ALTER TABLE ') + \ - style.SQL_TABLE(quotename(db_table, dbl=True)) + \ - style.SQL_KEYWORD(' ALTER ') + \ - style.SQL_FIELD(quotename(self.column, dbl=True)) + \ - style.SQL_KEYWORD(' SET NOT NULL') + ';' - return sql - - def _geom_index(self, style, db_table, - index_type='GIST', index_opts='GIST_GEOMETRY_OPS'): - "Creates a GiST index for this geometry field." - sql = style.SQL_KEYWORD('CREATE INDEX ') + \ - style.SQL_TABLE(quotename('%s_%s_id' % (db_table, self.column), dbl=True)) + \ - style.SQL_KEYWORD(' ON ') + \ - style.SQL_TABLE(quotename(db_table, dbl=True)) + \ - style.SQL_KEYWORD(' USING ') + \ - style.SQL_COLTYPE(index_type) + ' ( ' + \ - style.SQL_FIELD(quotename(self.column, dbl=True)) + ' ' + \ - style.SQL_KEYWORD(index_opts) + ' );' - return sql - - def _post_create_sql(self, style, db_table): - """Returns SQL that will be executed after the model has been - created. Geometry columns must be added after creation with the - PostGIS AddGeometryColumn() function.""" - - # Getting the AddGeometryColumn() SQL necessary to create a PostGIS - # geometry field. - post_sql = self._add_geom(style, db_table) - - # If the user wants to index this data, then get the indexing SQL as well. - if self._index: - return '%s\n%s' % (post_sql, self._geom_index(style, db_table)) - else: - return post_sql - - def _post_delete_sql(self, style, db_table): - "Drops the geometry column." - sql = style.SQL_KEYWORD('SELECT ') + \ - style.SQL_KEYWORD('DropGeometryColumn') + '(' + \ - style.SQL_TABLE(quotename(db_table)) + ', ' + \ - style.SQL_FIELD(quotename(self.column)) + ');' - return sql - def contribute_to_class(self, cls, name): super(GeometryField, self).contribute_to_class(cls, name) @@ -112,47 +46,6 @@ class GeometryField(Field): def get_internal_type(self): return "NoField" - def get_db_prep_lookup(self, lookup_type, value): - "Returns field's value prepared for database lookup, accepts WKT and GEOS Geometries for the value." - if lookup_type in POSTGIS_TERMS: - if lookup_type == 'isnull': return [value] # special case for NULL geometries. - if not bool(value): return [None] # If invalid value passed in. - if isinstance(value, GEOSGeometry): - # GEOSGeometry instance passed in. - if value.srid != self._srid: - # Returning a dictionary instructs the parse_lookup() to add what's in the 'where' key - # to the where parameters, since we need to transform the geometry in the query. - return {'where' : "Transform(%s,%s)", - 'params' : [value, self._srid] - } - else: - # Just return the GEOSGeometry, it has its own psycopg2 adaptor. - return [value] - elif isinstance(value, StringType): - # String instance passed in, assuming WKT. - # TODO: Any validation needed here to prevent SQL injection? - return ["SRID=%d;%s" % (self._srid, value)] - else: - raise TypeError("Invalid type (%s) used for field lookup value." % str(type(value))) - else: - raise TypeError("Field has invalid lookup: %s" % lookup_type) - - def get_db_prep_save(self, value): - "Prepares the value for saving in the database." - if not bool(value): return None - if isinstance(value, GEOSGeometry): - return value - else: - return ("SRID=%d;%s" % (self._srid, wkt)) - - def get_placeholder(self, value): - "Provides a proper substitution value for " - if isinstance(value, GEOSGeometry) and value.srid != self._srid: - # Adding Transform() to the SQL placeholder. - return 'Transform(%%s, %s)' % self._srid - else: - return '%s' - def get_manipulator_field_objs(self): "Using the WKTField (defined above) to be our manipulator." return [WKTField] diff --git a/django/contrib/gis/db/models/postgis.py b/django/contrib/gis/db/models/postgis.py deleted file mode 100644 index fe17b12499..0000000000 --- a/django/contrib/gis/db/models/postgis.py +++ /dev/null @@ -1,331 +0,0 @@ -# This module is meant to re-define the helper routines used by the -# django.db.models.query objects to be customized for PostGIS. -from django.db import backend -from django.db.models.query import LOOKUP_SEPARATOR, field_choices, find_field, FieldFound, QUERY_TERMS, get_where_clause -from django.utils.datastructures import SortedDict - -# PostGIS-specific operators. The commented descriptions of these -# operators come from Section 6.2.2 of the official PostGIS documentation. -POSTGIS_OPERATORS = { - # The "&<" operator returns true if A's bounding box overlaps or is to the left of B's bounding box. - 'overlaps_left' : '&< %s', - # The "&>" operator returns true if A's bounding box overlaps or is to the right of B's bounding box. - 'overlaps_right' : '&> %s', - # The "<<" operator returns true if A's bounding box is strictly to the left of B's bounding box. - 'left' : '<< %s', - # The ">>" operator returns true if A's bounding box is strictly to the right of B's bounding box. - 'right' : '>> %s', - # The "&<|" operator returns true if A's bounding box overlaps or is below B's bounding box. - 'overlaps_below' : '&<| %s', - # The "|&>" operator returns true if A's bounding box overlaps or is above B's bounding box. - 'overlaps_above' : '|&> %s', - # The "<<|" operator returns true if A's bounding box is strictly below B's bounding box. - 'strictly_below' : '<<| %s', - # The "|>>" operator returns true if A's bounding box is strictly above B's bounding box. - 'strictly_above' : '|>> %s', - # The "~=" operator is the "same as" operator. It tests actual geometric equality of two features. So if - # A and B are the same feature, vertex-by-vertex, the operator returns true. - 'same_as' : '~= %s', - 'exact' : '~= %s', - # The "@" operator returns true if A's bounding box is completely contained by B's bounding box. - 'contained' : '@ %s', - # The "~" operator returns true if A's bounding box completely contains B's bounding box. - 'bbcontains' : '~ %s', - # The "&&" operator is the "overlaps" operator. If A's bounding boux overlaps B's bounding box the - # operator returns true. - 'bboverlaps' : '&& %s', - } - -# PostGIS Geometry Functions -- most of these use GEOS. -POSTGIS_GEOMETRY_FUNCTIONS = { - #'distance' : 'Distance', -- doesn't work right now. - 'equals' : 'Equals', - 'disjoint' : 'Disjoint', - 'touches' : 'Touches', - 'crosses' : 'Crosses', - 'within' : 'Within', - 'overlaps' : 'Overlaps', - 'contains' : 'Contains', - 'intersects' : 'Intersects', - 'relate' : 'Relate', - } - -# Any other lookup types that do not require a mapping. -MISC_TERMS = ['isnull'] - -# The quotation used for postgis (uses single quotes). -def quotename(value, dbl=False): - if dbl: return '"%s"' % value - else: return "'%s'" % value - -# These are the PostGIS-customized QUERY_TERMS, combines both the operators -# and the geometry functions. -POSTGIS_TERMS = list(POSTGIS_OPERATORS.keys()) # Getting the operators first -POSTGIS_TERMS += list(POSTGIS_GEOMETRY_FUNCTIONS.keys()) # Adding on the Geometry Functions -POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull') -POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable - -def get_geo_where_clause(lookup_type, table_prefix, field_name, value): - if table_prefix.endswith('.'): - table_prefix = backend.quote_name(table_prefix[:-1])+'.' - field_name = backend.quote_name(field_name) - - # See if a PostGIS operator matches the lookup type first - try: - return '%s%s %s' % (table_prefix, field_name, (POSTGIS_OPERATORS[lookup_type] % '%s')) - except KeyError: - pass - - # See if a PostGIS Geometry function matches the lookup type next - try: - return '%s(%s%s, %%s)' % (POSTGIS_GEOMETRY_FUNCTIONS[lookup_type], table_prefix, field_name) - except KeyError: - pass - - # For any other lookup type - if lookup_type == 'isnull': - return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or '')) - - raise TypeError, "Got invalid lookup_type: %s" % repr(lookup_type) - -#### query.py overloaded functions #### -# parse_lookup() and lookup_inner() are modified from their django/db/models/query.py -# counterparts to support constructing SQL for geographic queries. -# -# Status: Synced with r5609. -# -def parse_lookup(kwarg_items, opts): - # Helper function that handles converting API kwargs - # (e.g. "name__exact": "tom") to SQL. - # Returns a tuple of (joins, where, params). - - # 'joins' is a sorted dictionary describing the tables that must be joined - # to complete the query. The dictionary is sorted because creation order - # is significant; it is a dictionary to ensure uniqueness of alias names. - # - # Each key-value pair follows the form - # alias: (table, join_type, condition) - # where - # alias is the AS alias for the joined table - # table is the actual table name to be joined - # join_type is the type of join (INNER JOIN, LEFT OUTER JOIN, etc) - # condition is the where-like statement over which narrows the join. - # alias will be derived from the lookup list name. - # - # At present, this method only every returns INNER JOINs; the option is - # there for others to implement custom Q()s, etc that return other join - # types. - joins, where, params = SortedDict(), [], [] - - for kwarg, value in kwarg_items: - path = kwarg.split(LOOKUP_SEPARATOR) - # Extract the last elements of the kwarg. - # The very-last is the lookup_type (equals, like, etc). - # The second-last is the table column on which the lookup_type is - # to be performed. If this name is 'pk', it will be substituted with - # the name of the primary key. - # If there is only one part, or the last part is not a query - # term, assume that the query is an __exact - lookup_type = path.pop() - if lookup_type == 'pk': - lookup_type = 'exact' - path.append(None) - elif len(path) == 0 or not ((lookup_type in QUERY_TERMS) or (lookup_type in POSTGIS_TERMS)): - path.append(lookup_type) - lookup_type = 'exact' - - if len(path) < 1: - raise TypeError, "Cannot parse keyword query %r" % kwarg - - if value is None: - # Interpret '__exact=None' as the sql '= NULL'; otherwise, reject - # all uses of None as a query value. - if lookup_type != 'exact': - raise ValueError, "Cannot use None as a query value" - elif callable(value): - value = value() - - joins2, where2, params2 = lookup_inner(path, lookup_type, value, opts, opts.db_table, None) - joins.update(joins2) - where.extend(where2) - params.extend(params2) - return joins, where, params - -def lookup_inner(path, lookup_type, value, opts, table, column): - qn = backend.quote_name - joins, where, params = SortedDict(), [], [] - current_opts = opts - current_table = table - current_column = column - intermediate_table = None - join_required = False - - name = path.pop(0) - # Has the primary key been requested? If so, expand it out - # to be the name of the current class' primary key - if name is None or name == 'pk': - name = current_opts.pk.name - - # Try to find the name in the fields associated with the current class - try: - # Does the name belong to a defined many-to-many field? - field = find_field(name, current_opts.many_to_many, False) - if field: - new_table = current_table + '__' + name - new_opts = field.rel.to._meta - new_column = new_opts.pk.column - - # Need to create an intermediate table join over the m2m table - # This process hijacks current_table/column to point to the - # intermediate table. - current_table = "m2m_" + new_table - intermediate_table = field.m2m_db_table() - join_column = field.m2m_reverse_name() - intermediate_column = field.m2m_column_name() - - raise FieldFound - - # Does the name belong to a reverse defined many-to-many field? - field = find_field(name, current_opts.get_all_related_many_to_many_objects(), True) - if field: - new_table = current_table + '__' + name - new_opts = field.opts - new_column = new_opts.pk.column - - # Need to create an intermediate table join over the m2m table. - # This process hijacks current_table/column to point to the - # intermediate table. - current_table = "m2m_" + new_table - intermediate_table = field.field.m2m_db_table() - join_column = field.field.m2m_column_name() - intermediate_column = field.field.m2m_reverse_name() - - raise FieldFound - - # Does the name belong to a one-to-many field? - field = find_field(name, current_opts.get_all_related_objects(), True) - if field: - new_table = table + '__' + name - new_opts = field.opts - new_column = field.field.column - join_column = opts.pk.column - - # 1-N fields MUST be joined, regardless of any other conditions. - join_required = True - - raise FieldFound - - # Does the name belong to a one-to-one, many-to-one, or regular field? - field = find_field(name, current_opts.fields, False) - if field: - if field.rel: # One-to-One/Many-to-one field - new_table = current_table + '__' + name - new_opts = field.rel.to._meta - new_column = new_opts.pk.column - join_column = field.column - raise FieldFound - elif path: - # For regular fields, if there are still items on the path, - # an error has been made. We munge "name" so that the error - # properly identifies the cause of the problem. - name += LOOKUP_SEPARATOR + path[0] - else: - raise FieldFound - - except FieldFound: # Match found, loop has been shortcut. - pass - else: # No match found. - choices = field_choices(current_opts.many_to_many, False) + \ - field_choices(current_opts.get_all_related_many_to_many_objects(), True) + \ - field_choices(current_opts.get_all_related_objects(), True) + \ - field_choices(current_opts.fields, False) - raise TypeError, "Cannot resolve keyword '%s' into field. Choices are: %s" % (name, ", ".join(choices)) - - # Check whether an intermediate join is required between current_table - # and new_table. - if intermediate_table: - joins[qn(current_table)] = ( - qn(intermediate_table), "LEFT OUTER JOIN", - "%s.%s = %s.%s" % (qn(table), qn(current_opts.pk.column), qn(current_table), qn(intermediate_column)) - ) - - if path: - # There are elements left in the path. More joins are required. - if len(path) == 1 and path[0] in (new_opts.pk.name, None) \ - and lookup_type in ('exact', 'isnull') and not join_required: - # If the next and final name query is for a primary key, - # and the search is for isnull/exact, then the current - # (for N-1) or intermediate (for N-N) table can be used - # for the search. No need to join an extra table just - # to check the primary key. - new_table = current_table - else: - # There are 1 or more name queries pending, and we have ruled out - # any shortcuts; therefore, a join is required. - joins[qn(new_table)] = ( - qn(new_opts.db_table), "INNER JOIN", - "%s.%s = %s.%s" % (qn(current_table), qn(join_column), qn(new_table), qn(new_column)) - ) - # If we have made the join, we don't need to tell subsequent - # recursive calls about the column name we joined on. - join_column = None - - # There are name queries remaining. Recurse deeper. - joins2, where2, params2 = lookup_inner(path, lookup_type, value, new_opts, new_table, join_column) - - joins.update(joins2) - where.extend(where2) - params.extend(params2) - else: - # No elements left in path. Current element is the element on which - # the search is being performed. - - if join_required: - # Last query term is a RelatedObject - if field.field.rel.multiple: - # RelatedObject is from a 1-N relation. - # Join is required; query operates on joined table. - column = new_opts.pk.name - joins[qn(new_table)] = ( - qn(new_opts.db_table), "INNER JOIN", - "%s.%s = %s.%s" % (qn(current_table), qn(join_column), qn(new_table), qn(new_column)) - ) - current_table = new_table - else: - # RelatedObject is from a 1-1 relation, - # No need to join; get the pk value from the related object, - # and compare using that. - column = current_opts.pk.name - elif intermediate_table: - # Last query term is a related object from an N-N relation. - # Join from intermediate table is sufficient. - column = join_column - elif name == current_opts.pk.name and lookup_type in ('exact', 'isnull') and current_column: - # Last query term is for a primary key. If previous iterations - # introduced a current/intermediate table that can be used to - # optimize the query, then use that table and column name. - column = current_column - else: - # Last query term was a normal field. - column = field.column - - # If the field is a geometry field, then the WHERE clause will need to be obtained - # with the get_geo_where_clause() - if hasattr(field, '_geom'): - # Getting the geographic where clause. - gwc = get_geo_where_clause(lookup_type, current_table + '.', column, value) - - # Getting the geographic parameters from the field. - geo_params = field.get_db_prep_lookup(lookup_type, value) - - # If a dictionary was passed back from the field modify the where clause. - if isinstance(geo_params, dict): - gwc = gwc % geo_params['where'] - geo_params = geo_params['params'] - where.append(gwc) - params.extend(geo_params) - else: - where.append(get_where_clause(lookup_type, current_table + '.', column, value)) - params.extend(field.get_db_prep_lookup(lookup_type, value)) - - return joins, where, params diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index 6619e54c9a..dab1f04eb6 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -1,7 +1,7 @@ from django.db.models.query import Q, QuerySet from django.db import backend from django.contrib.gis.db.models.fields import GeometryField -from django.contrib.gis.db.models.postgis import parse_lookup +from django.contrib.gis.db.backend import parse_lookup # parse_lookup depends on the spatial database backend. from django.db.models.fields import FieldDoesNotExist import operator diff --git a/django/contrib/gis/tests/__init__.py b/django/contrib/gis/tests/__init__.py index fb4167fe22..fc9bd6cb03 100644 --- a/django/contrib/gis/tests/__init__.py +++ b/django/contrib/gis/tests/__init__.py @@ -1,6 +1,6 @@ from copy import copy from unittest import TestSuite, TextTestRunner -from django.contrib.gis.utils import create_spatial_db +from django.contrib.gis.db.backend import create_spatial_db from django.db import connection from django.test.utils import destroy_test_db diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index 50a21aa227..dcc5e681ff 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -91,7 +91,7 @@ class GeoModelTest(unittest.TestCase): nmi.save() def test005_left_right(self): - "Testing the left ('<<') right ('>>') operators." + "Testing the 'left' and 'right' lookup types." # Left: A << B => true if xmax(A) < xmin(B) # Right: A >> B => true if xmin(A) > xmax(B) diff --git a/django/contrib/gis/utils/spatial_db.py b/django/contrib/gis/utils/spatial_db.py deleted file mode 100644 index 0fa618b4df..0000000000 --- a/django/contrib/gis/utils/spatial_db.py +++ /dev/null @@ -1,190 +0,0 @@ -from django.conf import settings -from django.core.management import syncdb -from django.db import connection, backend -from django.test.utils import _set_autocommit, TEST_DATABASE_PREFIX -from commands import getstatusoutput -import os, re, sys - -def create_lang(db_name, verbosity=1): - "This sets up the pl/pgsql language on the given database." - - # Getting the command-line options for the shell command - options = get_cmd_options(db_name) - - # Constructing the 'createlang' command. - createlang_cmd = 'createlang %splpgsql' % options - if verbosity >= 1: print createlang_cmd - - # Must have database super-user privileges to execute createlang -- it must - # also be in your path. - status, output = getstatusoutput(createlang_cmd) - - # Checking the status of the command, 0 => execution successful - if status != 0: - raise Exception, "Error executing 'plpgsql' command: %s\n" % output - - -def _create_with_cursor(db_name, verbosity=1, autoclobber=False): - "Creates database with psycopg2 cursor." - - # Constructing the necessary SQL to create the database (the DATABASE_USER - # must possess the privileges to create a database) - create_sql = 'CREATE DATABASE %s OWNER %s' % (backend.quote_name(db_name), - settings.DATABASE_USER) - cursor = connection.cursor() - _set_autocommit(connection) - - try: - # Trying to create the database first. - cursor.execute(create_sql) - except Exception, e: - # Drop and recreate, if necessary. - if not autoclobber: - confirm = raw_input("\nIt appears the database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % db_name) - if autoclobber or confirm == 'yes': - if verbosity >= 1: print 'Destroying old spatial database...' - drop_db(db_name) - if verbosity >= 1: print 'Creating new spatial database...' - cursor.execute(create_sql) - else: - raise Exception, 'Spatial Database Creation canceled.' - -created_regex = re.compile(r'^createdb: database creation failed: ERROR: database ".+" already exists') -def _create_with_shell(db_name, verbosity=1, autoclobber=False): - """If no spatial database already exists, then using a cursor will not work. Thus, a - `createdb` command will be issued through the shell to bootstrap the database.""" - - # Getting the command-line options for the shell command - options = get_cmd_options(False) - create_cmd = 'createdb -O %s %s%s' % (settings.DATABASE_USER, options, db_name) - if verbosity >= 1: print create_cmd - - # Attempting to create the database. - status, output = getstatusoutput(create_cmd) - if status != 0: - if created_regex.match(output): - if not autoclobber: - confirm = raw_input("\nIt appears the database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % db_name) - if autoclobber or confirm == 'yes': - if verbosity >= 1: print 'Destroying old spatial database...' - drop_cmd = 'dropdb %s%s' % (options, db_name) - status, output = getstatusoutput(drop_cmd) - if status != 0: - raise Exception, 'Could not drop database %s: %s' % (db_name, output) - if verbosity >= 1: print 'Creating new spatial database...' - status, output = getstatusoutput(create_cmd) - if status != 0: - raise Exception, 'Could not create database after dropping: %s' % output - else: - raise Exception, 'Spatial Database Creation canceled.' - else: - raise Exception, 'Unknown error occurred in creating database: %s' % output - -def create_spatial_db(test=False, verbosity=1, autoclobber=False, interactive=False): - "This Python routine creates a spatial database based on settings.py." - - # Making sure we're using PostgreSQL and psycopg2 - if settings.DATABASE_ENGINE != 'postgresql_psycopg2': - raise Exception, 'Spatial database creation only supported postgresql_psycopg2 platform.' - - # This routine depends on getstatusoutput(), which does not work on Windows. - # TODO: Consider executing shell commands with popen for Windows compatibility - if os.name == 'nt': - raise Exception, 'Automatic spatial database creation only supported on *NIX platforms.' - - # Getting the spatial database name - if test: - db_name = get_spatial_db(test=True) - _create_with_cursor(db_name, verbosity=verbosity, autoclobber=autoclobber) - else: - db_name = get_spatial_db() - _create_with_shell(db_name, verbosity=verbosity, autoclobber=autoclobber) - - # Creating the db language. - create_lang(db_name, verbosity=verbosity) - - # Now adding in the PostGIS routines. - load_postgis_sql(db_name, verbosity=verbosity) - - if verbosity >= 1: print 'Creation of spatial database %s successful.' % db_name - - # Closing the connection - connection.close() - settings.DATABASE_NAME = db_name - - # Syncing the database - syncdb(verbosity, interactive=interactive) - - # Get a cursor (even though we don't need one yet). This has - # the side effect of initializing the test database. - cursor = connection.cursor() - -def drop_db(db_name=False, test=False): - "Using the cursor, drops the given database. All exceptions will be propagated up." - if not db_name: db_name = get_spatial_db(test=test) - cursor = connection.cursor() - cursor.execute("DROP DATABASE %s" % backend.quote_name(db_name)) - -def get_cmd_options(db_name): - "Obtains the command-line PostgreSQL connection options for shell commands." - # The db_name parameter is optional - if db_name: - options = '-d %s -U %s ' % (db_name, settings.DATABASE_USER) - else: - options = '-U %s ' % settings.DATABASE_USER - if settings.DATABASE_HOST: - options += '-h %s ' % settings.DATABASE_HOST - if settings.DATABASE_PORT: - options += '-p %s ' % settings.DATABASE_PORT - return options - -def get_spatial_db(test=False): - """This routine returns the name of the spatial database. - Set the 'test' keyword for the test spatial database name.""" - if test: - if settings.TEST_DATABASE_NAME: - test_db_name = settings.TEST_DATABASE_NAME - else: - test_db_name = TEST_DATABASE_PREFIX + settings.DATABASE_NAME - return test_db_name - else: - if not settings.DATABASE_NAME: - raise Exception, 'must configure DATABASE_NAME in settings.py' - return settings.DATABASE_NAME - -def load_postgis_sql(db_name, verbosity=1): - "This routine loads up the PostGIS SQL files lwpostgis.sql and spatial_ref_sys.sql." - - # Getting the path to the PostGIS SQL - try: - # POSTGIS_SQL_PATH may be placed in settings to tell GeoDjango where the - # PostGIS SQL files are located - sql_path = settings.POSTGIS_SQL_PATH - except AttributeError: - sql_path = '/usr/local/share' - - # The PostGIS SQL post-creation files. - lwpostgis_file = os.path.join(sql_path, 'lwpostgis.sql') - srefsys_file = os.path.join(sql_path, 'spatial_ref_sys.sql') - if not os.path.isfile(lwpostgis_file): - raise Exception, 'Could not find PostGIS function definitions in %s' % lwpostgis_file - if not os.path.isfile(srefsys_file): - raise Exception, 'Could not find PostGIS spatial reference system definitions in %s' % srefsys_file - - # Getting the psql command-line options. - options = get_cmd_options(db_name) - - # Now trying to load up the PostGIS functions - cmd = 'psql %s-f %s' % (options, lwpostgis_file) - if verbosity >= 1: print cmd - status, output = getstatusoutput(cmd) - if status != 0: - raise Exception, 'Error in loading PostGIS lwgeometry routines.' - - # Now trying to load up the Spatial Reference System table - cmd = 'psql %s-f %s' % (options, srefsys_file) - if verbosity >= 1: print cmd - status, output = getstatusoutput(cmd) - if status !=0: - raise Exception, 'Error in loading PostGIS spatial_ref_sys table.' - -- cgit v1.3