diff --git a/django/contrib/gis/db/backend/__init__.py b/django/contrib/gis/db/backend/__init__.py index f3e4e8ecdc..a28a8d1cff 100644 --- a/django/contrib/gis/db/backend/__init__.py +++ b/django/contrib/gis/db/backend/__init__.py @@ -1,15 +1,21 @@ """ - This module provides the backend for spatial SQL construction with Django. + This module provides the backend for spatial SQL construction with Django. - Specifically, this module will import the correct routines and modules - needed for GeoDjango + Specifically, this module will import the correct routines and modules + needed for GeoDjango. + + (1) GeoBackEndField, a base class needed for GeometryField. + (2) GeometryProxy, for lazy-instantiated geometries from the + database output. + (3) GIS_TERMS, a list of acceptable geographic lookup types for + the backend. + (4) The `parse_lookup` function, used for spatial SQL construction by + the GeoQuerySet. + (5) The `create_spatial_db`, `geo_quotename`, and `get_geo_where_clause` + routines (needed by `parse_lookup`. - (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 added for - additional spatial databases (e.g., Oracle, DB2). + Currently only PostGIS is supported, but someday backends will be added for + additional spatial databases (e.g., Oracle, DB2). """ from django.conf import settings from django.db import connection @@ -21,9 +27,11 @@ from django.utils.datastructures import SortedDict ASGML, ASKML, UNION = (False, False, False) if settings.DATABASE_ENGINE == 'postgresql_psycopg2': - # PostGIS is the spatial database, getting the rquired modules, renaming as necessary. + # PostGIS is the spatial database, getting the rquired modules, + # renaming as necessary. from django.contrib.gis.db.backend.postgis import \ PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \ + PostGISProxy as GeometryProxy, \ create_spatial_db, geo_quotename, get_geo_where_clause, \ ASGML, ASKML, UNION else: diff --git a/django/contrib/gis/db/backend/postgis/__init__.py b/django/contrib/gis/db/backend/postgis/__init__.py index 33ef9a43fa..d95bde4370 100644 --- a/django/contrib/gis/db/backend/postgis/__init__.py +++ b/django/contrib/gis/db/backend/postgis/__init__.py @@ -7,6 +7,7 @@ from django.contrib.gis.db.backend.postgis.query import \ MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 from django.contrib.gis.db.backend.postgis.creation import create_spatial_db from django.contrib.gis.db.backend.postgis.field import PostGISField +from django.contrib.gis.db.backend.postgis.proxy import PostGISProxy # Functions used by GeoManager methods, and not via lookup types. if MAJOR_VERSION == 1: diff --git a/django/contrib/gis/db/backend/postgis/field.py b/django/contrib/gis/db/backend/postgis/field.py index c1cfc5e97e..2d941dfd0b 100644 --- a/django/contrib/gis/db/backend/postgis/field.py +++ b/django/contrib/gis/db/backend/postgis/field.py @@ -44,9 +44,11 @@ class PostGISField(Field): return sql def _post_create_sql(self, style, db_table): - """Returns SQL that will be executed after the model has been + """ + Returns SQL that will be executed after the model has been created. Geometry columns must be added after creation with the - PostGIS AddGeometryColumn() function.""" + PostGIS AddGeometryColumn() function. + """ # Getting the AddGeometryColumn() SQL necessary to create a PostGIS # geometry field. @@ -54,9 +56,9 @@ class PostGISField(Field): # 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)) + return (post_sql, self._geom_index(style, db_table)) else: - return post_sql + return (post_sql,) def _post_delete_sql(self, style, db_table): "Drops the geometry column." @@ -66,9 +68,18 @@ class PostGISField(Field): style.SQL_FIELD(quotename(self.column)) + ');' return sql + def db_type(self): + """ + PostGIS geometry columns are added by stored procedures, should be + None. + """ + return None + 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.""" + """ + 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. @@ -101,6 +112,12 @@ class PostGISField(Field): else: raise TypeError('Geometry Proxy should only return GEOSGeometry objects.') + def get_internal_type(self): + """ + Returns NoField because a stored procedure is used by PostGIS to create the + """ + return 'NoField' + def get_placeholder(self, value): """ Provides a proper substitution value for Geometries that are not in the diff --git a/django/contrib/gis/db/backend/postgis/models.py b/django/contrib/gis/db/backend/postgis/models.py new file mode 100644 index 0000000000..4071dc646b --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/models.py @@ -0,0 +1,54 @@ +""" + The GeometryColumns and SpatialRefSys models for the PostGIS backend. +""" +from django.db import models +from django.contrib.gis.models import SpatialRefSysMixin + +# Checking for the presence of GDAL (needed for the SpatialReference object) +from django.contrib.gis.gdal import HAS_GDAL +if HAS_GDAL: + from django.contrib.gis.gdal import SpatialReference + +class GeometryColumns(models.Model): + """ + The 'geometry_columns' table from the PostGIS. See the PostGIS + documentation at Ch. 4.2.2. + """ + f_table_catalog = models.CharField(maxlength=256) + f_table_schema = models.CharField(maxlength=256) + f_table_name = models.CharField(maxlength=256, primary_key=True) + f_geometry_column = models.CharField(maxlength=256) + coord_dimension = models.IntegerField() + srid = models.IntegerField() + type = models.CharField(maxlength=30) + + class Meta: + db_table = 'geometry_columns' + + @classmethod + def table_name(self): + "Class method for returning the table name field for this model." + return 'f_table_name' + + def __unicode__(self): + return "%s.%s - %dD %s field (SRID: %d)" % \ + (self.f_table_name, self.f_geometry_column, + self.coord_dimension, self.type, self.srid) + +class SpatialRefSys(models.Model, SpatialRefSysMixin): + """ + The 'spatial_ref_sys' table from PostGIS. See the PostGIS + documentaiton at Ch. 4.2.1. + """ + srid = models.IntegerField(primary_key=True) + auth_name = models.CharField(maxlength=256) + auth_srid = models.IntegerField() + srtext = models.CharField(maxlength=2048) + proj4text = models.CharField(maxlength=2048) + + class Meta: + db_table = 'spatial_ref_sys' + + @property + def wkt(self): + return self.srtext diff --git a/django/contrib/gis/db/backend/postgis/proxy.py b/django/contrib/gis/db/backend/postgis/proxy.py new file mode 100644 index 0000000000..bb1ee7f280 --- /dev/null +++ b/django/contrib/gis/db/backend/postgis/proxy.py @@ -0,0 +1,62 @@ +""" + The GeometryProxy object, allows for lazy-geometries. The proxy uses + Python descriptors for instantiating and setting GEOS Geometry objects + corresponding to geographic model fields. + + Thanks to Robert Coup for providing this functionality (see #4322). +""" + +from types import NoneType, StringType, UnicodeType + +class PostGISProxy(object): + def __init__(self, klass, field): + """ + Proxy initializes on the given Geometry class (not an instance) and + the GeometryField. + """ + self._field = field + self._klass = klass + + def __get__(self, obj, type=None): + """ + This accessor retrieves the geometry, initializing it using the geometry + class specified during initialization and the HEXEWKB value of the field. + Currently, only GEOS or OGR geometries are supported. + """ + # Getting the value of the field. + geom_value = obj.__dict__[self._field.attname] + + if isinstance(geom_value, self._klass): + geom = geom_value + elif (geom_value is None) or (geom_value==''): + geom = None + else: + # Otherwise, a GEOSGeometry object is built using the field's contents, + # and the model's corresponding attribute is set. + geom = self._klass(geom_value) + setattr(obj, self._field.attname, geom) + return geom + + def __set__(self, obj, value): + """ + This accessor sets the proxied geometry with the geometry class + specified during initialization. Values of None, HEXEWKB, or WKT may + be used to set the geometry as well. + """ + # The OGC Geometry type of the field. + gtype = self._field._geom + + # The geometry type must match that of the field -- unless the + # general GeometryField is used. + if isinstance(value, self._klass) and (str(value.geom_type).upper() == gtype or gtype == 'GEOMETRY'): + # Assigning the SRID to the geometry. + if value.srid is None: value.srid = self._field._srid + elif isinstance(value, (NoneType, StringType, UnicodeType)): + # Set with None, WKT, or HEX + pass + else: + raise TypeError('cannot set %s GeometryProxy with value of type: %s' % (obj.__class__.__name__, type(value))) + + # Setting the objects dictionary with the value, and returning. + obj.__dict__[self._field.attname] = value + return value diff --git a/django/contrib/gis/db/models/fields/__init__.py b/django/contrib/gis/db/models/fields/__init__.py index 8d329fd97c..850cac2007 100644 --- a/django/contrib/gis/db/models/fields/__init__.py +++ b/django/contrib/gis/db/models/fields/__init__.py @@ -1,9 +1,9 @@ -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.conf import settings +from django.contrib.gis.db.backend import GeoBackendField, GeometryProxy # these depend on the spatial database backend. from django.contrib.gis.oldforms import WKTField -from django.utils.functional import curry +from django.contrib.gis.geos import GEOSGeometry -#TODO: Flesh out widgets. +#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies. class GeometryField(GeoBackendField): "The base GIS field -- maps to the OpenGIS Specification Geometry type." @@ -31,26 +31,9 @@ class GeometryField(GeoBackendField): def contribute_to_class(self, cls, name): super(GeometryField, self).contribute_to_class(cls, name) - # setup for lazy-instantiated GEOSGeometry objects - setattr(cls, self.attname, GeometryProxy(self)) - - # Adding needed accessor functions - setattr(cls, 'get_%s_geos' % self.name, curry(cls._get_GEOM_geos, field=self)) - setattr(cls, 'get_%s_ogr' % self.name, curry(cls._get_GEOM_ogr, field=self, srid=self._srid)) - setattr(cls, 'get_%s_srid' % self.name, curry(cls._get_GEOM_srid, srid=self._srid)) - setattr(cls, 'get_%s_srs' % self.name, curry(cls._get_GEOM_srs, srid=self._srid)) - setattr(cls, 'get_%s_wkt' % self.name, curry(cls._get_GEOM_wkt, field=self)) - setattr(cls, 'get_%s_centroid' % self.name, curry(cls._get_GEOM_centroid, field=self)) - setattr(cls, 'get_%s_area' % self.name, curry(cls._get_GEOM_area, field=self)) + # Setup for lazy-instantiated GEOSGeometry object. + setattr(cls, self.attname, GeometryProxy(GEOSGeometry, self)) - def get_internal_type(self): - return "NoField" - - def db_type(self): - # Geometry columns are added by stored procedures, and thus should - # be None. - return None - 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/mixin.py b/django/contrib/gis/db/models/mixin.py index 90640d1787..475a053b8f 100644 --- a/django/contrib/gis/db/models/mixin.py +++ b/django/contrib/gis/db/models/mixin.py @@ -1,54 +1,11 @@ -from warnings import warn - -# GEOS is a requirement, GDAL is not -from django.contrib.gis.geos import GEOSGeometry -from django.contrib.gis.gdal import HAS_GDAL -if HAS_GDAL: - from django.contrib.gis.gdal import OGRGeometry, SpatialReference - # Until model subclassing is a possibility, a mixin class is used to add # the necessary functions that may be contributed for geographic objects. class GeoMixin: - "The Geographic Mixin class provides routines for geographic objects." - - # A subclass of Model is specifically needed so that these geographic - # routines are present for instantiations of the models. - def _get_GEOM_geos(self, field): - "Returns a GEOS Python object for the geometry." - warn("use model.%s" % field.attname, DeprecationWarning) - return getattr(self, field.attname) - - def _get_GEOM_ogr(self, field, srid): - "Returns an OGR Python object for the geometry." - if HAS_GDAL: - return OGRGeometry(getattr(self, field.attname).wkt, - SpatialReference('EPSG:%d' % srid)) - else: - raise Exception, "GDAL is not installed!" - - def _get_GEOM_srid(self, srid): - "Returns the spatial reference identifier (SRID) of the geometry." - warn("use model.geometry_field.srid", DeprecationWarning) - return srid - - def _get_GEOM_srs(self, srid): - "Returns ane OGR Spatial Reference object of the geometry." - if HAS_GDAL: - return SpatialReference('EPSG:%d' % srid) - else: - raise Exception, "GDAL is not installed!" - - def _get_GEOM_wkt(self, field): - "Returns the WKT of the geometry." - warn("use model.%s.centroid.wkt" % field.attname, DeprecationWarning) - return getattr(self, field.attname).wkt - - def _get_GEOM_centroid(self, field): - "Returns the centroid of the geometry, in WKT." - warn("use model.%s.centroid.wkt" % field.attname, DeprecationWarning) - return getattr(self, field.attname).centroid.wkt - - def _get_GEOM_area(self, field): - "Returns the area of the geometry, in projected units." - warn("use model.%s.area" % field.attname, DeprecationWarning) - return getattr(self, field.attname).area + """ + The Geographic Mixin class provides routines for geographic objects, + however, it is no longer necessary, since all of its previous functions + may now be accessed via the GeometryProxy. This mixin is only provided + for backwards-compatibility purposes, and will be eventually removed + (unless the need arises again). + """ + pass diff --git a/django/contrib/gis/db/models/proxy.py b/django/contrib/gis/db/models/proxy.py deleted file mode 100644 index 65c613da26..0000000000 --- a/django/contrib/gis/db/models/proxy.py +++ /dev/null @@ -1,47 +0,0 @@ -""" - The GeometryProxy object, allows for lazy-geometries. The proxy uses - Python descriptors for instantiating and setting GEOS Geometry objects - corresponding to geographic model fields. - - Thanks to Robert Coup for providing this functionality (see #4322). -""" - -from types import NoneType, StringType, UnicodeType -from django.contrib.gis.geos import GEOSGeometry, GEOSException - -# TODO: docstrings -class GeometryProxy(object): - def __init__(self, field): - "Proxy initializes on the given GeometryField." - self._field = field - - def __get__(self, obj, type=None): - # Getting the value of the field. - geom_value = obj.__dict__[self._field.attname] - - if isinstance(geom_value, GEOSGeometry): - # If the value of the field is None, or is already a GEOS Geometry - # no more work is needed. - geom = geom_value - elif (geom_value is None) or (geom_value==''): - geom = None - else: - # Otherwise, a GEOSGeometry object is built using the field's contents, - # and the model's corresponding attribute is set. - geom = GEOSGeometry(geom_value) - setattr(obj, self._field.attname, geom) - return geom - - def __set__(self, obj, value): - if isinstance(value, GEOSGeometry) and (value.geom_type.upper() == self._field._geom): - # Getting set with GEOS Geometry; geom_type must match that of the field. - - # If value's SRID is not set, setting it to the field's SRID. - if value.srid is None: value.srid = self._field._srid - elif isinstance(value, (NoneType, StringType, UnicodeType)): - # Getting set with None, WKT, or HEX - pass - else: - raise TypeError, 'cannot set %s GeometryProxy with value of type: %s' % (self._field._geom, type(value)) - obj.__dict__[self._field.attname] = value - return value diff --git a/django/contrib/gis/models.py b/django/contrib/gis/models.py index 91ca8d7818..5a4e480837 100644 --- a/django/contrib/gis/models.py +++ b/django/contrib/gis/models.py @@ -1,8 +1,9 @@ """ - Models for the PostGIS/OGC database tables. + Imports the SpatialRefSys and GeometryColumns models dependent on the + spatial database backend. """ import re -from django.db import models +from django.conf import settings # Checking for the presence of GDAL (needed for the SpatialReference object) from django.contrib.gis.gdal import HAS_GDAL @@ -15,37 +16,11 @@ if HAS_GDAL: # parameter. spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P.+)\",(?P\d+(\.\d+)?),(?P\d{3}\.\d+),') -# This is the global 'geometry_columns' from PostGIS. -# See PostGIS Documentation at Ch. 4.2.2 -class GeometryColumns(models.Model): - f_table_catalog = models.CharField(maxlength=256) - f_table_schema = models.CharField(maxlength=256) - f_table_name = models.CharField(maxlength=256, primary_key=True) - f_geometry_column = models.CharField(maxlength=256) - coord_dimension = models.IntegerField() - srid = models.IntegerField() - type = models.CharField(maxlength=30) - - class Meta: - db_table = 'geometry_columns' - - def __str__(self): - return "%s.%s - %dD %s field (SRID: %d)" % \ - (self.f_table_name, self.f_geometry_column, - self.coord_dimension, self.type, self.srid) - -# This is the global 'spatial_ref_sys' table from PostGIS. -# See PostGIS Documentation at Ch. 4.2.1 -class SpatialRefSys(models.Model): - srid = models.IntegerField(primary_key=True) - auth_name = models.CharField(maxlength=256) - auth_srid = models.IntegerField() - srtext = models.CharField(maxlength=2048) - proj4text = models.CharField(maxlength=2048) - - class Meta: - db_table = 'spatial_ref_sys' - +class SpatialRefSysMixin(object): + """ + The SpatialRefSysMixin is a class used by the database-dependent + SpatialRefSys objects to reduce redundnant code. + """ @property def srs(self): """ @@ -58,24 +33,17 @@ class SpatialRefSys(models.Model): else: # Attempting to cache a SpatialReference object. - # Trying to get from WKT first + # Trying to get from WKT first. try: - self._srs = SpatialReference(self.srtext, 'wkt') - return self._srs.clone() - except Exception, msg1: + self._srs = SpatialReference(self.wkt) + return self.srs + except Exception, msg: pass - - # Trying the proj4 text next - try: - self._srs = SpatialReference(self.proj4text, 'proj4') - return self._srs.clone() - except Exception, msg2: - pass - - raise Exception, 'Could not get an OSR Spatial Reference:\n\tWKT error: %s\n\tPROJ.4 error: %s' % (msg1, msg2) + + raise Exception('Could not get OSR SpatialReference from WKT: %s\nError:\n%s' % (self.wkt, msg)) else: - raise Exception, 'GDAL is not installed!' - + raise Exception('GDAL is not installed.') + @property def ellipsoid(self): """ @@ -85,7 +53,7 @@ class SpatialRefSys(models.Model): if HAS_GDAL: return self.srs.ellipsoid else: - m = spheroid_regex.match(self.srtext) + m = spheroid_regex.match(self.wkt) if m: return (float(m.group('major')), float(m.group('flattening'))) else: return None @@ -139,10 +107,18 @@ class SpatialRefSys(models.Model): "Returns the name of the angular units." return self.srs.angular_name - def __str__(self): + def __unicode__(self): """ Returns the string representation. If GDAL is installed, it will be 'pretty' OGC WKT. """ - if HAS_GDAL: return str(self.srs) - else: return "%d:%s " % (self.srid, self.auth_name) + try: + return unicode(self.srs) + except: + return unicode(self.srtext) + +# The SpatialRefSys and GeometryColumns models +if settings.DATABASE_ENGINE == 'postgresql_psycopg2': + from django.contrib.gis.db.backend.postgis.models import GeometryColumns, SpatialRefSys +else: + raise NotImplementedError('No SpatialRefSys or GeometryColumns models for backend: %s' % settings.DATABASE_ENGINE) diff --git a/django/contrib/gis/tests/geoapp/models.py b/django/contrib/gis/tests/geoapp/models.py index fa979beead..4315b4247c 100644 --- a/django/contrib/gis/tests/geoapp/models.py +++ b/django/contrib/gis/tests/geoapp/models.py @@ -1,16 +1,21 @@ from django.contrib.gis.db import models -class Country(models.Model, models.GeoMixin): - name = models.CharField(maxlength=30) +class Country(models.Model): + name = models.CharField(max_length=30) mpoly = models.MultiPolygonField() # SRID, by default, is 4326 objects = models.GeoManager() -class City(models.Model, models.GeoMixin): - name = models.CharField(maxlength=30) +class City(models.Model): + name = models.CharField(max_length=30) point = models.PointField() objects = models.GeoManager() -class State(models.Model, models.GeoMixin): - name = models.CharField(maxlength=30) +class State(models.Model): + name = models.CharField(max_length=30) poly = models.PolygonField(null=True) # Allowing NULL geometries here. objects = models.GeoManager() + +class Feature(models.Model): + name = models.CharField(max_length=20) + geom = models.GeometryField() + objects = models.GeoManager() diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index e8127c0827..004e75898e 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -1,6 +1,7 @@ import unittest -from models import Country, City, State -from django.contrib.gis.geos import fromstr, Point, LineString, LinearRing, Polygon +from models import Country, City, State, Feature +from django.contrib.gis.geos import * +from django.contrib.gis import gdal class GeoModelTest(unittest.TestCase): @@ -59,8 +60,17 @@ class GeoModelTest(unittest.TestCase): nullstate = State(name='NullState', poly=ply) self.assertEqual(4326, nullstate.poly.srid) # SRID auto-set from None nullstate.save() - self.assertEqual(ply, State.objects.get(name='NullState').poly) + + ns = State.objects.get(name='NullState') + self.assertEqual(ply, ns.poly) + # Testing the `ogr` and `srs` lazy-geometry properties. + if gdal.HAS_GDAL: + self.assertEqual(True, isinstance(ns.poly.ogr, gdal.OGRGeometry)) + self.assertEqual(ns.poly.wkb, ns.poly.ogr.wkb) + self.assertEqual(True, isinstance(ns.poly.srs, gdal.SpatialReference)) + self.assertEqual('WGS 84', ns.poly.srs.name) + # Changing the interior ring on the poly attribute. new_inner = LinearRing((30, 30), (30, 70), (70, 70), (70, 30), (30, 30)) nullstate.poly[1] = new_inner.clone() @@ -277,6 +287,32 @@ class GeoModelTest(unittest.TestCase): self.assertEqual(True, union.equals_exact(u, 10)) # Going up to 10 digits of precision. qs = City.objects.filter(name='NotACity') self.assertEqual(None, qs.union('point')) + + def test18_geometryfield(self): + "Testing GeometryField." + f1 = Feature(name='Point', geom=Point(1, 1)) + f2 = Feature(name='LineString', geom=LineString((0, 0), (1, 1), (5, 5))) + f3 = Feature(name='Polygon', geom=Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0)))) + f4 = Feature(name='GeometryCollection', + geom=GeometryCollection(Point(2, 2), LineString((0, 0), (2, 2)), + Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0))))) + f1.save() + f2.save() + f3.save() + f4.save() + + f_1 = Feature.objects.get(name='Point') + self.assertEqual(True, isinstance(f_1.geom, Point)) + self.assertEqual((1.0, 1.0), f_1.geom.tuple) + f_2 = Feature.objects.get(name='LineString') + self.assertEqual(True, isinstance(f_2.geom, LineString)) + self.assertEqual(((0.0, 0.0), (1.0, 1.0), (5.0, 5.0)), f_2.geom.tuple) + + f_3 = Feature.objects.get(name='Polygon') + self.assertEqual(True, isinstance(f_3.geom, Polygon)) + f_4 = Feature.objects.get(name='GeometryCollection') + self.assertEqual(True, isinstance(f_4.geom, GeometryCollection)) + self.assertEqual(f_3.geom, f_4.geom[2]) def suite(): s = unittest.TestSuite() diff --git a/django/core/management/sql.py b/django/core/management/sql.py index 1757d4d7a0..d69d83e44e 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -404,7 +404,7 @@ def custom_sql_for_model(model, style): # Post-creation SQL should come before any initial SQL data is loaded. for f in opts.fields: if hasattr(f, '_post_create_sql'): - output.append(f._post_create_sql(style, model._meta.db_table)) + output.extend(f._post_create_sql(style, model._meta.db_table)) # Some backends can't execute more than one SQL statement at a time, # so split into separate statements.