diff --git a/django/contrib/gis/db/backends/mysql/base.py b/django/contrib/gis/db/backends/mysql/base.py index b2cc851b55..7f0fc4815b 100644 --- a/django/contrib/gis/db/backends/mysql/base.py +++ b/django/contrib/gis/db/backends/mysql/base.py @@ -1,6 +1,7 @@ from django.db.backends.mysql.base import * from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper from django.contrib.gis.db.backends.mysql.creation import MySQLCreation +from django.contrib.gis.db.backends.mysql.introspection import MySQLIntrospection from django.contrib.gis.db.backends.mysql.operations import MySQLOperations class DatabaseWrapper(MySQLDatabaseWrapper): @@ -9,3 +10,4 @@ class DatabaseWrapper(MySQLDatabaseWrapper): super(DatabaseWrapper, self).__init__(*args, **kwargs) self.creation = MySQLCreation(self) self.ops = MySQLOperations() + self.introspection = MySQLIntrospection(self) diff --git a/django/contrib/gis/db/backends/mysql/introspection.py b/django/contrib/gis/db/backends/mysql/introspection.py new file mode 100644 index 0000000000..59d0f627ee --- /dev/null +++ b/django/contrib/gis/db/backends/mysql/introspection.py @@ -0,0 +1,32 @@ +from MySQLdb.constants import FIELD_TYPE + +from django.contrib.gis.gdal import OGRGeomType +from django.db.backends.mysql.introspection import DatabaseIntrospection + +class MySQLIntrospection(DatabaseIntrospection): + # Updating the data_types_reverse dictionary with the appropriate + # type for Geometry fields. + data_types_reverse = DatabaseIntrospection.data_types_reverse.copy() + data_types_reverse[FIELD_TYPE.GEOMETRY] = 'GeometryField' + + def get_geometry_type(self, table_name, geo_col): + cursor = self.connection.cursor() + try: + # In order to get the specific geometry type of the field, + # we introspect on the table definition using `DESCRIBE`. + cursor.execute('DESCRIBE %s' % + self.connection.ops.quote_name(table_name)) + # Increment over description info until we get to the geometry + # column. + for column, typ, null, key, default, extra in cursor.fetchall(): + if column == geo_col: + # Using OGRGeomType to convert from OGC name to Django field. + # MySQL does not support 3D or SRIDs, so the field params + # are empty. + field_type = OGRGeomType(typ).django + field_params = {} + break + finally: + cursor.close() + + return field_type, field_params diff --git a/django/contrib/gis/db/backends/oracle/base.py b/django/contrib/gis/db/backends/oracle/base.py index b39cb555a2..398b3d3b76 100644 --- a/django/contrib/gis/db/backends/oracle/base.py +++ b/django/contrib/gis/db/backends/oracle/base.py @@ -1,10 +1,12 @@ from django.db.backends.oracle.base import * from django.db.backends.oracle.base import DatabaseWrapper as OracleDatabaseWrapper from django.contrib.gis.db.backends.oracle.creation import OracleCreation +from django.contrib.gis.db.backends.oracle.introspection import OracleIntrospection from django.contrib.gis.db.backends.oracle.operations import OracleOperations class DatabaseWrapper(OracleDatabaseWrapper): def __init__(self, *args, **kwargs): super(DatabaseWrapper, self).__init__(*args, **kwargs) - self.creation = OracleCreation(self) self.ops = OracleOperations(self) + self.creation = OracleCreation(self) + self.introspection = OracleIntrospection(self) diff --git a/django/contrib/gis/db/backends/oracle/introspection.py b/django/contrib/gis/db/backends/oracle/introspection.py new file mode 100644 index 0000000000..58dd3f39b8 --- /dev/null +++ b/django/contrib/gis/db/backends/oracle/introspection.py @@ -0,0 +1,39 @@ +import cx_Oracle +from django.db.backends.oracle.introspection import DatabaseIntrospection + +class OracleIntrospection(DatabaseIntrospection): + # Associating any OBJECTVAR instances with GeometryField. Of course, + # this won't work right on Oracle objects that aren't MDSYS.SDO_GEOMETRY, + # but it is the only object type supported within Django anyways. + data_types_reverse = DatabaseIntrospection.data_types_reverse.copy() + data_types_reverse[cx_Oracle.OBJECT] = 'GeometryField' + + def get_geometry_type(self, table_name, geo_col): + cursor = self.connection.cursor() + try: + # Querying USER_SDO_GEOM_METADATA to get the SRID and dimension information. + try: + cursor.execute('SELECT "DIMINFO", "SRID" FROM "USER_SDO_GEOM_METADATA" WHERE "TABLE_NAME"=%s AND "COLUMN_NAME"=%s', + (table_name.upper(), geo_col.upper())) + row = cursor.fetchone() + except Exception, msg: + raise Exception('Could not find entry in USER_SDO_GEOM_METADATA corresponding to "%s"."%s"\n' + 'Error message: %s.' % (table_name, geo_col, msg)) + + # TODO: Research way to find a more specific geometry field type for + # the column's contents. + field_type = 'GeometryField' + + # Getting the field parameters. + field_params = {} + dim, srid = row + if srid != 4326: + field_params['srid'] = srid + # Length of object array ( SDO_DIM_ARRAY ) is number of dimensions. + dim = len(dim) + if dim != 2: + field_params['dim'] = dim + finally: + cursor.close() + + return field_type, field_params diff --git a/django/contrib/gis/db/backends/postgis/base.py b/django/contrib/gis/db/backends/postgis/base.py index e77186ed2b..634a7d5890 100644 --- a/django/contrib/gis/db/backends/postgis/base.py +++ b/django/contrib/gis/db/backends/postgis/base.py @@ -1,6 +1,7 @@ from django.db.backends.postgresql_psycopg2.base import * from django.db.backends.postgresql_psycopg2.base import DatabaseWrapper as Psycopg2DatabaseWrapper from django.contrib.gis.db.backends.postgis.creation import PostGISCreation +from django.contrib.gis.db.backends.postgis.introspection import PostGISIntrospection from django.contrib.gis.db.backends.postgis.operations import PostGISOperations class DatabaseWrapper(Psycopg2DatabaseWrapper): @@ -8,3 +9,4 @@ class DatabaseWrapper(Psycopg2DatabaseWrapper): super(DatabaseWrapper, self).__init__(*args, **kwargs) self.creation = PostGISCreation(self) self.ops = PostGISOperations(self) + self.introspection = PostGISIntrospection(self) diff --git a/django/contrib/gis/db/backends/postgis/introspection.py b/django/contrib/gis/db/backends/postgis/introspection.py new file mode 100644 index 0000000000..7962d19ff9 --- /dev/null +++ b/django/contrib/gis/db/backends/postgis/introspection.py @@ -0,0 +1,95 @@ +from django.db.backends.postgresql_psycopg2.introspection import DatabaseIntrospection +from django.contrib.gis.gdal import OGRGeomType + +class GeoIntrospectionError(Exception): + pass + +class PostGISIntrospection(DatabaseIntrospection): + # Reverse dictionary for PostGIS geometry types not populated until + # introspection is actually performed. + postgis_types_reverse = {} + + def get_postgis_types(self): + """ + Returns a dictionary with keys that are the PostgreSQL object + identification integers for the PostGIS geometry and/or + geography types (if supported). + """ + cursor = self.connection.cursor() + # The OID integers associated with the geometry type may + # be different across versions; hence, this is why we have + # to query the PostgreSQL pg_type table corresponding to the + # PostGIS custom data types. + oid_sql = 'SELECT "oid" FROM "pg_type" WHERE "typname" = %s' + try: + cursor.execute(oid_sql, ('geometry',)) + GEOM_TYPE = cursor.fetchone()[0] + postgis_types = { GEOM_TYPE : 'GeometryField' } + if self.connection.ops.geography: + cursor.execute(oid_sql, ('geography',)) + GEOG_TYPE = cursor.fetchone()[0] + # The value for the geography type is actually a tuple + # to pass in the `geography=True` keyword to the field + # definition. + postgis_types[GEOG_TYPE] = ('GeometryField', {'geography' : True}) + finally: + cursor.close() + + return postgis_types + + def get_field_type(self, data_type, description): + if not self.postgis_types_reverse: + # If the PostGIS types reverse dictionary is not populated, do so + # now. In order to prevent unnecessary requests upon connection + # intialization, the `data_types_reverse` dictionary is not updated + # with the PostGIS custom types until introspection is actually + # performed -- in other words, when this function is called. + self.postgis_types_reverse = self.get_postgis_types() + self.data_types_reverse.update(self.postgis_types_reverse) + return super(PostGISIntrospection, self).get_field_type(data_type, description) + + def get_geometry_type(self, table_name, geo_col): + """ + The geometry type OID used by PostGIS does not indicate the particular + type of field that a geometry column is (e.g., whether it's a + PointField or a PolygonField). Thus, this routine queries the PostGIS + metadata tables to determine the geometry type, + """ + cursor = self.connection.cursor() + try: + try: + # First seeing if this geometry column is in the `geometry_columns` + cursor.execute('SELECT "coord_dimension", "srid", "type" ' + 'FROM "geometry_columns" ' + 'WHERE "f_table_name"=%s AND "f_geometry_column"=%s', + (table_name, geo_col)) + row = cursor.fetchone() + if not row: raise GeoIntrospectionError + except GeoIntrospectionError: + if self.connection.ops.geography: + cursor.execute('SELECT "coord_dimension", "srid", "type" ' + 'FROM "geography_columns" ' + 'WHERE "f_table_name"=%s AND "f_geography_column"=%s', + (table_name, geo_col)) + row = cursor.fetchone() + + if not row: + raise Exception('Could not find a geometry or geography column for "%s"."%s"' % + (table_name, geo_col)) + + # OGRGeomType does not require GDAL and makes it easy to convert + # from OGC geom type name to Django field. + field_type = OGRGeomType(row[2]).django + + # Getting any GeometryField keyword arguments that are not the default. + dim = row[0] + srid = row[1] + field_params = {} + if srid != 4326: + field_params['srid'] = srid + if dim != 2: + field_params['dim'] = dim + finally: + cursor.close() + + return field_type, field_params diff --git a/django/contrib/gis/db/backends/spatialite/base.py b/django/contrib/gis/db/backends/spatialite/base.py index 971447688b..a2fc47e7de 100644 --- a/django/contrib/gis/db/backends/spatialite/base.py +++ b/django/contrib/gis/db/backends/spatialite/base.py @@ -7,6 +7,7 @@ from django.db.backends.sqlite3.base import DatabaseWrapper as SqliteDatabaseWra _sqlite_extract, _sqlite_date_trunc, _sqlite_regexp from django.contrib.gis.db.backends.spatialite.client import SpatiaLiteClient from django.contrib.gis.db.backends.spatialite.creation import SpatiaLiteCreation +from django.contrib.gis.db.backends.spatialite.introspection import SpatiaLiteIntrospection from django.contrib.gis.db.backends.spatialite.operations import SpatiaLiteOperations class DatabaseWrapper(SqliteDatabaseWrapper): @@ -32,6 +33,7 @@ class DatabaseWrapper(SqliteDatabaseWrapper): self.ops = SpatiaLiteOperations(self) self.client = SpatiaLiteClient(self) self.creation = SpatiaLiteCreation(self) + self.introspection = SpatiaLiteIntrospection(self) def _cursor(self): if self.connection is None: diff --git a/django/contrib/gis/db/backends/spatialite/introspection.py b/django/contrib/gis/db/backends/spatialite/introspection.py new file mode 100644 index 0000000000..1b5952ceac --- /dev/null +++ b/django/contrib/gis/db/backends/spatialite/introspection.py @@ -0,0 +1,51 @@ +from django.contrib.gis.gdal import OGRGeomType +from django.db.backends.sqlite3.introspection import DatabaseIntrospection, FlexibleFieldLookupDict + +class GeoFlexibleFieldLookupDict(FlexibleFieldLookupDict): + """ + Sublcass that includes updates the `base_data_types_reverse` dict + for geometry field types. + """ + base_data_types_reverse = FlexibleFieldLookupDict.base_data_types_reverse.copy() + base_data_types_reverse.update( + {'point' : 'GeometryField', + 'linestring' : 'GeometryField', + 'polygon' : 'GeometryField', + 'multipoint' : 'GeometryField', + 'multilinestring' : 'GeometryField', + 'multipolygon' : 'GeometryField', + 'geometrycollection' : 'GeometryField', + }) + +class SpatiaLiteIntrospection(DatabaseIntrospection): + data_types_reverse = GeoFlexibleFieldLookupDict() + + def get_geometry_type(self, table_name, geo_col): + cursor = self.connection.cursor() + try: + # Querying the `geometry_columns` table to get additional metadata. + cursor.execute('SELECT "coord_dimension", "srid", "type" ' + 'FROM "geometry_columns" ' + 'WHERE "f_table_name"=%s AND "f_geometry_column"=%s', + (table_name, geo_col)) + row = cursor.fetchone() + if not row: + raise Exception('Could not find a geometry column for "%s"."%s"' % + (table_name, geo_col)) + + # OGRGeomType does not require GDAL and makes it easy to convert + # from OGC geom type name to Django field. + field_type = OGRGeomType(row[2]).django + + # Getting any GeometryField keyword arguments that are not the default. + dim = row[0] + srid = row[1] + field_params = {} + if srid != 4326: + field_params['srid'] = srid + if isinstance(dim, basestring) and 'Z' in dim: + field_params['dim'] = 3 + finally: + cursor.close() + + return field_type, field_params diff --git a/django/contrib/gis/management/commands/inspectdb.py b/django/contrib/gis/management/commands/inspectdb.py index 365bb24063..937bb8eb33 100644 --- a/django/contrib/gis/management/commands/inspectdb.py +++ b/django/contrib/gis/management/commands/inspectdb.py @@ -1,188 +1,32 @@ -""" - This overrides the traditional `inspectdb` command so that geographic databases - may be introspected. -""" +from optparse import make_option -from django.core.management.commands.inspectdb import Command as InspectCommand -from django.contrib.gis.db.backend import SpatialBackend +from django.core.management.base import CommandError +from django.core.management.commands.inspectdb import Command as InspectDBCommand -class Command(InspectCommand): +class Command(InspectDBCommand): + db_module = 'django.contrib.gis.db' + gis_tables = {} - # Mapping from lower-case OGC type to the corresponding GeoDjango field. - geofield_mapping = {'point' : 'PointField', - 'linestring' : 'LineStringField', - 'polygon' : 'PolygonField', - 'multipoint' : 'MultiPointField', - 'multilinestring' : 'MultiLineStringField', - 'multipolygon' : 'MultiPolygonField', - 'geometrycollection' : 'GeometryCollectionField', - 'geometry' : 'GeometryField', - } - - def geometry_columns(self): - """ - Returns a datastructure of metadata information associated with the - `geometry_columns` (or equivalent) table. - """ - # The `geo_cols` is a dictionary data structure that holds information - # about any geographic columns in the database. - geo_cols = {} - def add_col(table, column, coldata): - if table in geo_cols: - # If table already has a geometry column. - geo_cols[table][column] = coldata + def get_field_type(self, connection, table_name, row): + field_type, field_params, field_notes = super(Command, self).get_field_type(connection, table_name, row) + if field_type == 'GeometryField': + geo_col = row[0] + # Getting a more specific field type and any additional parameters + # from the `get_geometry_type` routine for the spatial backend. + field_type, geo_params = connection.introspection.get_geometry_type(table_name, geo_col) + field_params.update(geo_params) + # Adding the table name and column to the `gis_tables` dictionary, this + # allows us to track which tables need a GeoManager. + if table_name in self.gis_tables: + self.gis_tables[table_name].append(geo_col) else: - # Otherwise, create a dictionary indexed by column. - geo_cols[table] = { column : coldata } + self.gis_tables[table_name] = [geo_col] + return field_type, field_params, field_notes - if SpatialBackend.name == 'postgis': - # PostGIS holds all geographic column information in the `geometry_columns` table. - from django.contrib.gis.models import GeometryColumns - for geo_col in GeometryColumns.objects.all(): - table = geo_col.f_table_name - column = geo_col.f_geometry_column - coldata = {'type' : geo_col.type, 'srid' : geo_col.srid, 'dim' : geo_col.coord_dimension} - add_col(table, column, coldata) - return geo_cols - elif SpatialBackend.name == 'mysql': - # On MySQL have to get all table metadata before hand; this means walking through - # each table and seeing if any column types are spatial. Can't detect this with - # `cursor.description` (what the introspection module does) because all spatial types - # have the same integer type (255 for GEOMETRY). - from django.db import connection - cursor = connection.cursor() - cursor.execute('SHOW TABLES') - tables = cursor.fetchall(); - for table_tup in tables: - table = table_tup[0] - table_desc = cursor.execute('DESCRIBE `%s`' % table) - col_info = cursor.fetchall() - for column, typ, null, key, default, extra in col_info: - if typ in self.geofield_mapping: add_col(table, column, {'type' : typ}) - return geo_cols - else: - # TODO: Oracle (has incomplete `geometry_columns` -- have to parse - # SDO SQL to get specific type, SRID, and other information). - raise NotImplementedError('Geographic database inspection not available.') - - def handle_inspection(self): - "Overloaded from Django's version to handle geographic database tables." - from django.db import connection - import keyword - - geo_cols = self.geometry_columns() - - table2model = lambda table_name: table_name.title().replace('_', '') - - cursor = connection.cursor() - yield "# This is an auto-generated Django model module." - yield "# You'll have to do the following manually to clean this up:" - yield "# * Rearrange models' order" - yield "# * Make sure each model has one field with primary_key=True" - yield "# Feel free to rename the models, but don't rename db_table values or field names." - yield "#" - yield "# Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]'" - yield "# into your database." - yield '' - yield 'from django.contrib.gis.db import models' - yield '' - for table_name in connection.introspection.get_table_list(cursor): - # Getting the geographic table dictionary. - geo_table = geo_cols.get(table_name, {}) - - yield 'class %s(models.Model):' % table2model(table_name) - try: - relations = connection.introspection.get_relations(cursor, table_name) - except NotImplementedError: - relations = {} - try: - indexes = connection.introspection.get_indexes(cursor, table_name) - except NotImplementedError: - indexes = {} - for i, row in enumerate(connection.introspection.get_table_description(cursor, table_name)): - att_name, iatt_name = row[0].lower(), row[0] - comment_notes = [] # Holds Field notes, to be displayed in a Python comment. - extra_params = {} # Holds Field parameters such as 'db_column'. - - if ' ' in att_name: - extra_params['db_column'] = att_name - att_name = att_name.replace(' ', '') - comment_notes.append('Field renamed to remove spaces.') - if keyword.iskeyword(att_name): - extra_params['db_column'] = att_name - att_name += '_field' - comment_notes.append('Field renamed because it was a Python reserved word.') - - if i in relations: - rel_to = relations[i][1] == table_name and "'self'" or table2model(relations[i][1]) - field_type = 'ForeignKey(%s' % rel_to - if att_name.endswith('_id'): - att_name = att_name[:-3] - else: - extra_params['db_column'] = att_name - else: - if iatt_name in geo_table: - ## Customization for Geographic Columns ## - geo_col = geo_table[iatt_name] - field_type = self.geofield_mapping[geo_col['type'].lower()] - # Adding extra keyword arguments for the SRID and dimension (if not defaults). - dim, srid = geo_col.get('dim', 2), geo_col.get('srid', 4326) - if dim != 2: extra_params['dim'] = dim - if srid != 4326: extra_params['srid'] = srid - else: - try: - field_type = connection.introspection.get_field_type(row[1], row) - except KeyError: - field_type = 'TextField' - comment_notes.append('This field type is a guess.') - - # This is a hook for data_types_reverse to return a tuple of - # (field_type, extra_params_dict). - if type(field_type) is tuple: - field_type, new_params = field_type - extra_params.update(new_params) - - # Add max_length for all CharFields. - if field_type == 'CharField' and row[3]: - extra_params['max_length'] = row[3] - - if field_type == 'DecimalField': - extra_params['max_digits'] = row[4] - extra_params['decimal_places'] = row[5] - - # Add primary_key and unique, if necessary. - column_name = extra_params.get('db_column', att_name) - if column_name in indexes: - if indexes[column_name]['primary_key']: - extra_params['primary_key'] = True - elif indexes[column_name]['unique']: - extra_params['unique'] = True - - field_type += '(' - - # Don't output 'id = meta.AutoField(primary_key=True)', because - # that's assumed if it doesn't exist. - if att_name == 'id' and field_type == 'AutoField(' and extra_params == {'primary_key': True}: - continue - - # Add 'null' and 'blank', if the 'null_ok' flag was present in the - # table description. - if row[6]: # If it's NULL... - extra_params['blank'] = True - if not field_type in ('TextField(', 'CharField('): - extra_params['null'] = True - - field_desc = '%s = models.%s' % (att_name, field_type) - if extra_params: - if not field_desc.endswith('('): - field_desc += ', ' - field_desc += ', '.join(['%s=%r' % (k, v) for k, v in extra_params.items()]) - field_desc += ')' - if comment_notes: - field_desc += ' # ' + ' '.join(comment_notes) - yield ' %s' % field_desc - if table_name in geo_cols: - yield ' objects = models.GeoManager()' - yield ' class Meta:' - yield ' db_table = %r' % table_name - yield '' + def get_meta(self, table_name): + meta_lines = super(Command, self).get_meta(table_name) + if table_name in self.gis_tables: + # If the table is a geographic one, then we need make + # GeoManager the default manager for the model. + meta_lines.insert(0, ' objects = models.GeoManager()') + return meta_lines diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py index c95c6400fb..e45f22c287 100644 --- a/django/core/management/commands/inspectdb.py +++ b/django/core/management/commands/inspectdb.py @@ -15,6 +15,8 @@ class Command(NoArgsCommand): requires_model_validation = False + db_module = 'django.db' + def handle_noargs(self, **options): try: for line in self.handle_inspection(options): @@ -37,7 +39,7 @@ class Command(NoArgsCommand): yield "# Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]'" yield "# into your database." yield '' - yield 'from django.db import models' + yield 'from %s import models' % self.db_module yield '' for table_name in connection.introspection.get_table_list(cursor): yield 'class %s(models.Model):' % table2model(table_name) @@ -81,25 +83,11 @@ class Command(NoArgsCommand): else: extra_params['db_column'] = column_name else: - try: - field_type = connection.introspection.get_field_type(row[1], row) - except KeyError: - field_type = 'TextField' - comment_notes.append('This field type is a guess.') - - # This is a hook for DATA_TYPES_REVERSE to return a tuple of - # (field_type, extra_params_dict). - if type(field_type) is tuple: - field_type, new_params = field_type - extra_params.update(new_params) - - # Add max_length for all CharFields. - if field_type == 'CharField' and row[3]: - extra_params['max_length'] = row[3] - - if field_type == 'DecimalField': - extra_params['max_digits'] = row[4] - extra_params['decimal_places'] = row[5] + # Calling `get_field_type` to get the field type string and any + # additional paramters and notes. + field_type, field_params, field_notes = self.get_field_type(connection, table_name, row) + extra_params.update(field_params) + comment_notes.extend(field_notes) # Add primary_key and unique, if necessary. if column_name in indexes: @@ -131,6 +119,46 @@ class Command(NoArgsCommand): if comment_notes: field_desc += ' # ' + ' '.join(comment_notes) yield ' %s' % field_desc - yield ' class Meta:' - yield ' db_table = %r' % table_name - yield '' + for meta_line in self.get_meta(table_name): + yield meta_line + + def get_field_type(self, connection, table_name, row): + """ + Given the database connection, the table name, and the cursor row + description, this routine will return the given field type name, as + well as any additional keyword parameters and notes for the field. + """ + field_params = {} + field_notes = [] + + try: + field_type = connection.introspection.get_field_type(row[1], row) + except KeyError: + field_type = 'TextField' + field_notes.append('This field type is a guess.') + + # This is a hook for DATA_TYPES_REVERSE to return a tuple of + # (field_type, field_params_dict). + if type(field_type) is tuple: + field_type, new_params = field_type + field_params.update(new_params) + + # Add max_length for all CharFields. + if field_type == 'CharField' and row[3]: + field_params['max_length'] = row[3] + + if field_type == 'DecimalField': + field_params['max_digits'] = row[4] + field_params['decimal_places'] = row[5] + + return field_type, field_params, field_notes + + def get_meta(self, table_name): + """ + Return a sequence comprising the lines of code necessary + to construct the inner Meta class for the model corresponding + to the given database table name. + """ + return [' class Meta:', + ' db_table = %r' % table_name, + '']