diff --git a/django/core/management.py b/django/core/management.py index 0b0d356e809..17d6d4ca9a4 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -95,19 +95,12 @@ def _get_sequence_list(): return sequence_list -# If the foreign key points to an AutoField, a PositiveIntegerField or a -# PositiveSmallIntegerField, the foreign key should be an IntegerField, not the -# referred field type. Otherwise, the foreign key should be the same type of -# field as the field to which it points. -get_rel_data_type = lambda f: (f.get_internal_type() in ('AutoField', 'PositiveIntegerField', 'PositiveSmallIntegerField')) and 'IntegerField' or f.get_internal_type() - def get_sql_create(app): "Returns a list of the CREATE TABLE SQL statements for the given app." - from django.db import get_creation_module, models + from django.db import models + from django.conf import settings - data_types = get_creation_module().DATA_TYPES - - if not data_types: + if settings.DATABASE_ENGINE == 'dummy': # This must be the "dummy" database backend, which means the user # hasn't set DATABASE_ENGINE. sys.stderr.write(style.ERROR("Error: Django doesn't know which syntax to use for your SQL statements,\n" + @@ -159,28 +152,19 @@ def _get_sql_model_create(model, known_models=set()): Returns list_of_sql, pending_references_dict """ - from django.db import backend, get_creation_module, models - data_types = get_creation_module().DATA_TYPES + from django.db import backend, models opts = model._meta final_output = [] table_output = [] pending_references = {} for f in opts.fields: - if isinstance(f, (models.ForeignKey, models.OneToOneField)): - rel_field = f.rel.get_related_field() - while isinstance(rel_field, (models.ForeignKey, models.OneToOneField)): - rel_field = rel_field.rel.get_related_field() - data_type = get_rel_data_type(rel_field) - else: - rel_field = f - data_type = f.get_internal_type() - col_type = data_types[data_type] + col_type = f.db_type() tablespace = f.db_tablespace or opts.db_tablespace if col_type is not None: # Make the definition (e.g. 'foo VARCHAR(30)') for this field. field_output = [style.SQL_FIELD(backend.quote_name(f.column)), - style.SQL_COLTYPE(col_type % rel_field.__dict__)] + style.SQL_COLTYPE(col_type)] field_output.append(style.SQL_KEYWORD('%sNULL' % (not f.null and 'NOT ' or ''))) if f.unique and (not f.primary_key or backend.allows_unique_and_pk): field_output.append(style.SQL_KEYWORD('UNIQUE')) @@ -204,7 +188,7 @@ def _get_sql_model_create(model, known_models=set()): table_output.append(' '.join(field_output)) if opts.order_with_respect_to: table_output.append(style.SQL_FIELD(backend.quote_name('_order')) + ' ' + \ - style.SQL_COLTYPE(data_types['IntegerField']) + ' ' + \ + style.SQL_COLTYPE(models.IntegerField().db_type()) + ' ' + \ style.SQL_KEYWORD('NULL')) for field_constraints in opts.unique_together: table_output.append(style.SQL_KEYWORD('UNIQUE') + ' (%s)' % \ @@ -232,9 +216,8 @@ def _get_sql_for_pending_references(model, pending_references): """ Get any ALTER TABLE statements to add constraints after the fact. """ - from django.db import backend, get_creation_module + from django.db import backend from django.db.backends.util import truncate_name - data_types = get_creation_module().DATA_TYPES final_output = [] if backend.supports_constraints: @@ -257,11 +240,9 @@ def _get_sql_for_pending_references(model, pending_references): return final_output def _get_many_to_many_sql_for_model(model): - from django.db import backend, get_creation_module + from django.db import backend, models from django.contrib.contenttypes import generic - data_types = get_creation_module().DATA_TYPES - opts = model._meta final_output = [] for f in opts.many_to_many: @@ -275,19 +256,19 @@ def _get_many_to_many_sql_for_model(model): style.SQL_TABLE(backend.quote_name(f.m2m_db_table())) + ' ('] table_output.append(' %s %s %s%s,' % \ (style.SQL_FIELD(backend.quote_name('id')), - style.SQL_COLTYPE(data_types['AutoField']), + style.SQL_COLTYPE(models.AutoField(primary_key=True).db_type()), style.SQL_KEYWORD('NOT NULL PRIMARY KEY'), tablespace_sql)) table_output.append(' %s %s %s %s (%s)%s,' % \ (style.SQL_FIELD(backend.quote_name(f.m2m_column_name())), - style.SQL_COLTYPE(data_types[get_rel_data_type(opts.pk)] % opts.pk.__dict__), + style.SQL_COLTYPE(models.ForeignKey(model).db_type()), style.SQL_KEYWORD('NOT NULL REFERENCES'), style.SQL_TABLE(backend.quote_name(opts.db_table)), style.SQL_FIELD(backend.quote_name(opts.pk.column)), backend.get_deferrable_sql())) table_output.append(' %s %s %s %s (%s)%s,' % \ (style.SQL_FIELD(backend.quote_name(f.m2m_reverse_name())), - style.SQL_COLTYPE(data_types[get_rel_data_type(f.rel.to._meta.pk)] % f.rel.to._meta.pk.__dict__), + style.SQL_COLTYPE(models.ForeignKey(f.rel.to).db_type()), style.SQL_KEYWORD('NOT NULL REFERENCES'), style.SQL_TABLE(backend.quote_name(f.rel.to._meta.db_table)), style.SQL_FIELD(backend.quote_name(f.rel.to._meta.pk.column)), @@ -517,7 +498,7 @@ def _emit_post_sync_signal(created_models, verbosity, interactive): def syncdb(verbosity=1, interactive=True): "Creates the database tables for all apps in INSTALLED_APPS whose tables haven't already been created." - from django.db import backend, connection, transaction, models, get_creation_module + from django.db import backend, connection, transaction, models from django.conf import settings disable_termcolors() @@ -533,8 +514,6 @@ def syncdb(verbosity=1, interactive=True): except ImportError: pass - data_types = get_creation_module().DATA_TYPES - cursor = connection.cursor() # Get a list of all existing database tables, @@ -1266,8 +1245,7 @@ runserver.args = '[--noreload] [--adminmedia=ADMIN_MEDIA_PATH] [optional port nu def createcachetable(tablename): "Creates the table needed to use the SQL cache backend" - from django.db import backend, connection, transaction, get_creation_module, models - data_types = get_creation_module().DATA_TYPES + from django.db import backend, connection, transaction, models fields = ( # "key" is a reserved word in MySQL, so use "cache_key" instead. models.CharField(name='cache_key', maxlength=255, unique=True, primary_key=True), @@ -1277,7 +1255,7 @@ def createcachetable(tablename): table_output = [] index_output = [] for f in fields: - field_output = [backend.quote_name(f.name), data_types[f.get_internal_type()] % f.__dict__] + field_output = [backend.quote_name(f.name), f.db_type()] field_output.append("%sNULL" % (not f.null and "NOT " or "")) if f.unique: field_output.append("UNIQUE") diff --git a/django/db/backends/ado_mssql/creation.py b/django/db/backends/ado_mssql/creation.py index a1098ea43e7..e7d612817c1 100644 --- a/django/db/backends/ado_mssql/creation.py +++ b/django/db/backends/ado_mssql/creation.py @@ -12,7 +12,6 @@ DATA_TYPES = { 'ImageField': 'varchar(100)', 'IntegerField': 'int', 'IPAddressField': 'char(15)', - 'ManyToManyField': None, 'NullBooleanField': 'bit', 'OneToOneField': 'int', 'PhoneNumberField': 'varchar(20)', diff --git a/django/db/backends/mysql/creation.py b/django/db/backends/mysql/creation.py index 1b23fbff6ed..e4b5eaf2c88 100644 --- a/django/db/backends/mysql/creation.py +++ b/django/db/backends/mysql/creation.py @@ -16,7 +16,6 @@ DATA_TYPES = { 'ImageField': 'varchar(100)', 'IntegerField': 'integer', 'IPAddressField': 'char(15)', - 'ManyToManyField': None, 'NullBooleanField': 'bool', 'OneToOneField': 'integer', 'PhoneNumberField': 'varchar(20)', diff --git a/django/db/backends/mysql_old/creation.py b/django/db/backends/mysql_old/creation.py index 1b23fbff6ed..e4b5eaf2c88 100644 --- a/django/db/backends/mysql_old/creation.py +++ b/django/db/backends/mysql_old/creation.py @@ -16,7 +16,6 @@ DATA_TYPES = { 'ImageField': 'varchar(100)', 'IntegerField': 'integer', 'IPAddressField': 'char(15)', - 'ManyToManyField': None, 'NullBooleanField': 'bool', 'OneToOneField': 'integer', 'PhoneNumberField': 'varchar(20)', diff --git a/django/db/backends/oracle/creation.py b/django/db/backends/oracle/creation.py index f7903bce4f9..420b98bd9e1 100644 --- a/django/db/backends/oracle/creation.py +++ b/django/db/backends/oracle/creation.py @@ -19,7 +19,6 @@ DATA_TYPES = { 'ImageField': 'NVARCHAR2(100)', 'IntegerField': 'NUMBER(11)', 'IPAddressField': 'VARCHAR2(15)', - 'ManyToManyField': None, 'NullBooleanField': 'NUMBER(1) CHECK ((%(column)s IN (0,1)) OR (%(column)s IS NULL))', 'OneToOneField': 'NUMBER(11)', 'PhoneNumberField': 'VARCHAR2(20)', diff --git a/django/db/backends/postgresql/creation.py b/django/db/backends/postgresql/creation.py index 4646b68ab87..fde094de7f1 100644 --- a/django/db/backends/postgresql/creation.py +++ b/django/db/backends/postgresql/creation.py @@ -16,7 +16,6 @@ DATA_TYPES = { 'ImageField': 'varchar(100)', 'IntegerField': 'integer', 'IPAddressField': 'inet', - 'ManyToManyField': None, 'NullBooleanField': 'boolean', 'OneToOneField': 'integer', 'PhoneNumberField': 'varchar(20)', diff --git a/django/db/backends/sqlite3/creation.py b/django/db/backends/sqlite3/creation.py index e63046ab7d8..ab7442252e2 100644 --- a/django/db/backends/sqlite3/creation.py +++ b/django/db/backends/sqlite3/creation.py @@ -15,7 +15,6 @@ DATA_TYPES = { 'ImageField': 'varchar(100)', 'IntegerField': 'integer', 'IPAddressField': 'char(15)', - 'ManyToManyField': None, 'NullBooleanField': 'bool', 'OneToOneField': 'integer', 'PhoneNumberField': 'varchar(20)', diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index bc6d551a4c6..5e3509857b1 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -1,3 +1,4 @@ +from django.db import get_creation_module from django.db.models import signals from django.dispatch import dispatcher from django.conf import settings @@ -117,6 +118,30 @@ class Field(object): """ return value + def db_type(self): + """ + Returns the database column data type for this field, taking into + account the DATABASE_ENGINE setting. + """ + # The default implementation of this method looks at the + # backend-specific DATA_TYPES dictionary, looking up the field by its + # "internal type". + # + # A Field class can implement the get_internal_type() method to specify + # which *preexisting* Django Field class it's most similar to -- i.e., + # an XMLField is represented by a TEXT column type, which is the same + # as the TextField Django field type, which means XMLField's + # get_internal_type() returns 'TextField'. + # + # But the limitation of the get_internal_type() / DATA_TYPES approach + # is that it cannot handle database column types that aren't already + # mapped to one of the built-in Django field types. In this case, you + # can implement db_type() instead of get_internal_type() to specify + # exactly which wacky database column type you want to use. + data_types = get_creation_module().DATA_TYPES + internal_type = self.get_internal_type() + return data_types[internal_type] % self.__dict__ + def validate_full(self, field_data, all_data): """ Returns a list of errors for this field. This is the main interface, diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 152af105458..c4284f04b10 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -1,6 +1,6 @@ from django.db import backend, transaction from django.db.models import signals, get_model -from django.db.models.fields import AutoField, Field, IntegerField, get_ul_class +from django.db.models.fields import AutoField, Field, IntegerField, PositiveIntegerField, PositiveSmallIntegerField, get_ul_class from django.db.models.related import RelatedObject from django.utils.text import capfirst from django.utils.translation import ugettext_lazy, string_concat, ungettext, ugettext as _ @@ -556,6 +556,16 @@ class ForeignKey(RelatedField, Field): defaults.update(kwargs) return super(ForeignKey, self).formfield(**defaults) + def db_type(self): + # The database column type of a ForeignKey is the column type + # of the field to which it points. An exception is if the ForeignKey + # points to an AutoField/PositiveIntegerField/PositiveSmallIntegerField, + # in which case the column type is simply that of an IntegerField. + rel_field = self.rel.get_related_field() + if isinstance(rel_field, (AutoField, PositiveIntegerField, PositiveSmallIntegerField)): + return IntegerField().db_type() + return rel_field.db_type() + class OneToOneField(RelatedField, IntegerField): def __init__(self, to, to_field=None, **kwargs): try: @@ -622,6 +632,16 @@ class OneToOneField(RelatedField, IntegerField): defaults.update(kwargs) return super(OneToOneField, self).formfield(**defaults) + def db_type(self): + # The database column type of a OneToOneField is the column type + # of the field to which it points. An exception is if the OneToOneField + # points to an AutoField/PositiveIntegerField/PositiveSmallIntegerField, + # in which case the column type is simply that of an IntegerField. + rel_field = self.rel.get_related_field() + if isinstance(rel_field, (AutoField, PositiveIntegerField, PositiveSmallIntegerField)): + return IntegerField().db_type() + return rel_field.db_type() + class ManyToManyField(RelatedField, Field): def __init__(self, to, **kwargs): kwargs['verbose_name'] = kwargs.get('verbose_name', None) @@ -745,6 +765,11 @@ class ManyToManyField(RelatedField, Field): defaults['initial'] = [i._get_pk_val() for i in defaults['initial']] return super(ManyToManyField, self).formfield(**defaults) + def db_type(self): + # A ManyToManyField is not represented by a single column, + # so return None. + return None + class ManyToOneRel(object): def __init__(self, to, field_name, num_in_admin=3, min_num_in_admin=None, max_num_in_admin=None, num_extra_on_change=1, edit_inline=False, diff --git a/docs/model-api.txt b/docs/model-api.txt index 914d4868aed..f14aa7352c6 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -981,6 +981,77 @@ See the `One-to-one relationship model example`_ for a full example. .. _One-to-one relationship model example: http://www.djangoproject.com/documentation/models/one_to_one/ +Custom field types +------------------ + +**New in Django development version** + +Django's built-in field types don't cover every possible database column type -- +only the common types, such as ``VARCHAR`` and ``INTEGER``. For more obscure +column types, such as geographic polygons or even user-created types such as +`PostgreSQL custom types`_, you can define your own Django ``Field`` subclasses. + +.. _PostgreSQL custom types: http://www.postgresql.org/docs/8.2/interactive/sql-createtype.html + +.. admonition:: Experimental territory + + This is an area of Django that traditionally has not been documented, but + we're starting to include bits of documentation, one feature at a time. + Please forgive the sparseness of this section. + + If you like living on the edge and are comfortable with the risk of + unstable, undocumented APIs, see the code for the core ``Field`` class + in ``django/db/models/fields/__init__.py`` -- but if/when the innards + change, don't say we didn't warn you. + +To create a custom field type, simply subclass ``django.db.models.Field``. +Here is an incomplete list of the methods you should implement: + +``db_type()`` +~~~~~~~~~~~~~ + +Returns the database column data type for the ``Field``, taking into account +the current ``DATABASE_ENGINE`` setting. + +Say you've created a PostgreSQL custom type called ``mytype``. You can use this +field with Django by subclassing ``Field`` and implementing the ``db_type()`` +method, like so:: + + from django.db import models + + class MytypeField(models.Field): + def db_type(self): + return 'mytype' + +Once you have ``MytypeField``, you can use it in any model, just like any other +``Field`` type:: + + class Person(models.Model): + name = models.CharField(maxlength=80) + gender = models.CharField(maxlength=1) + something_else = MytypeField() + +If you aim to build a database-agnostic application, you should account for +differences in database column types. For example, the date/time column type +in PostgreSQL is called ``timestamp``, while the same column in MySQL is called +``datetime``. The simplest way to handle this in a ``db_type()`` method is to +import the Django settings module and check the ``DATABASE_ENGINE`` setting. +For example:: + + class MyDateField(models.Field): + def db_type(self): + from django.conf import settings + if settings.DATABASE_ENGINE == 'mysql': + return 'datetime' + else: + return 'timestamp' + +The ``db_type()`` method is only called by Django when the framework constructs +the ``CREATE TABLE`` statements for your application -- that is, when you first +create your tables. It's not called at any other time, so it can afford to +execute slightly complex code, such as the ``DATABASE_ENGINE`` check in the +above example. + Meta options ============