Skip to content

Commit

Permalink
implemented schema evolution
Browse files Browse the repository at this point in the history
git-svn-id: http://code.djangoproject.com/svn/django/branches/schema-evolution@3646 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
keredson committed Aug 22, 2006
1 parent 96d4d69 commit ffac835
Show file tree
Hide file tree
Showing 9 changed files with 614 additions and 100 deletions.
427 changes: 341 additions & 86 deletions django/core/management.py

Large diffs are not rendered by default.

44 changes: 43 additions & 1 deletion django/db/backends/mysql/base.py
Expand Up @@ -40,7 +40,7 @@ def execute(self, sql, params=()):
def executemany(self, sql, param_list):
try:
return self.cursor.executemany(sql, param_list)
except Database.Warning:
except Database.Warning, w:
self.cursor.execute("SHOW WARNINGS")
raise Database.Warning, "%s: %s" % (w, self.cursor.fetchall())

Expand Down Expand Up @@ -161,6 +161,48 @@ def get_drop_foreignkey_sql():
def get_pk_default_value():
return "DEFAULT"

def get_change_column_name_sql( table_name, indexes, old_col_name, new_col_name, col_def ):
# mysql doesn't support column renames (AFAIK), so we fake it
# TODO: only supports a single primary key so far
pk_name = None
for key in indexes.keys():
if indexes[key]['primary_key']: pk_name = key
output = []
output.append( 'ALTER TABLE '+ quote_name(table_name) +' CHANGE COLUMN '+ quote_name(old_col_name) +' '+ quote_name(new_col_name) +' '+ col_def + ';' )
return '\n'.join(output)

def get_change_column_def_sql( table_name, col_name, col_type, null, unique, primary_key ):
output = []
col_def = col_type +' '+ ('%sNULL' % (not null and 'NOT ' or ''))
if unique:
col_def += ' '+ 'UNIQUE'
if primary_key:
col_def += ' '+ 'PRIMARY KEY'
output.append( 'ALTER TABLE '+ quote_name(table_name) +' MODIFY COLUMN '+ quote_name(col_name) +' '+ col_def + ';' )
return '\n'.join(output)

def get_add_column_sql( table_name, col_name, col_type, null, unique, primary_key ):
output = []
field_output = []
field_output.append('ALTER TABLE')
field_output.append(quote_name(table_name))
field_output.append('ADD COLUMN')
field_output.append(quote_name(col_name))
field_output.append(col_type)
field_output.append(('%sNULL' % (not null and 'NOT ' or '')))
if unique:
field_output.append(('UNIQUE'))
if primary_key:
field_output.append(('PRIMARY KEY'))
output.append(' '.join(field_output) + ';')
return '\n'.join(output)

def get_drop_column_sql( table_name, col_name ):
output = []
output.append( '-- ALTER TABLE '+ quote_name(table_name) +' DROP COLUMN '+ quote_name(col_name) + ';' )
return '\n'.join(output)


OPERATOR_MAPPING = {
'exact': '= %s',
'iexact': 'LIKE %s',
Expand Down
38 changes: 37 additions & 1 deletion django/db/backends/mysql/introspection.py
@@ -1,4 +1,3 @@
from django.db import transaction
from django.db.backends.mysql.base import quote_name
from MySQLdb import ProgrammingError, OperationalError
from MySQLdb.constants import FIELD_TYPE
Expand Down Expand Up @@ -73,6 +72,43 @@ def get_indexes(cursor, table_name):
indexes[row[4]] = {'primary_key': (row[2] == 'PRIMARY'), 'unique': not bool(row[1])}
return indexes

def get_columns(cursor, table_name):
try:
cursor.execute("describe %s" % quote_name(table_name))
return [row[0] for row in cursor.fetchall()]
except:
return []

def get_known_column_flags( cursor, table_name, column_name ):
cursor.execute("describe %s" % quote_name(table_name))
dict = {}
for row in cursor.fetchall():
if row[0] == column_name:

# maxlength check goes here
if row[1][0:7]=='varchar':
dict['maxlength'] = row[1][8:len(row[1])-1]

# default flag check goes here
if row[2]=='YES': dict['allow_null'] = True
else: dict['allow_null'] = False

# primary/foreign/unique key flag check goes here
if row[3]=='PRI': dict['primary_key'] = True
else: dict['primary_key'] = False
if row[3]=='FOR': dict['foreign_key'] = True
else: dict['foreign_key'] = False
if row[3]=='UNI': dict['unique'] = True
else: dict['unique'] = False

# default value check goes here
# if row[4]=='NULL': dict['default'] = None
# else: dict['default'] = row[4]
dict['default'] = row[4]

# print table_name, column_name, dict
return dict

DATA_TYPES_REVERSE = {
FIELD_TYPE.BLOB: 'TextField',
FIELD_TYPE.CHAR: 'CharField',
Expand Down
36 changes: 36 additions & 0 deletions django/db/backends/postgresql/base.py
Expand Up @@ -111,6 +111,42 @@ def get_drop_foreignkey_sql():
def get_pk_default_value():
return "DEFAULT"

def get_change_column_name_sql( table_name, indexes, old_col_name, new_col_name, col_def ):
# TODO: only supports a single primary key so far
pk_name = None
for key in indexes.keys():
if indexes[key]['primary_key']: pk_name = key
output = []
output.append( 'ALTER TABLE '+ quote_name(table_name) +' RENAME COLUMN '+ quote_name(old_col_name) +' TO '+ quote_name(new_col_name) +';' )
return '\n'.join(output)

def get_change_column_def_sql( table_name, col_name, col_type, null, unique, primary_key ):
output = []
output.append( 'ALTER TABLE '+ quote_name(table_name) +' ADD COLUMN '+ quote_name(col_name+'_tmp') +' '+ col_type + ';' )
output.append( 'UPDATE '+ quote_name(table_name) +' SET '+ quote_name(col_name+'_tmp') +' = '+ quote_name(col_name) + ';' )
output.append( 'ALTER TABLE '+ quote_name(table_name) +' DROP COLUMN '+ quote_name(col_name) +';' )
output.append( 'ALTER TABLE '+ quote_name(table_name) +' RENAME COLUMN '+ quote_name(col_name+'_tmp') +' TO '+ quote_name(col_name) + ';' )
if not null:
output.append( 'ALTER TABLE '+ quote_name(table_name) +' ALTER COLUMN '+ quote_name(col_name) +' SET NOT NULL;' )
if unique:
output.append( 'ALTER TABLE '+ quote_name(table_name) +' ADD CONSTRAINT '+ table_name +'_'+ col_name +'_unique_constraint UNIQUE('+ col_name +');' )

return '\n'.join(output)

def get_add_column_sql( table_name, col_name, col_type, null, unique, primary_key ):
output = []
output.append( 'ALTER TABLE '+ quote_name(table_name) +' ADD COLUMN '+ quote_name(col_name) +' '+ col_type + ';' )
if not null:
output.append( 'ALTER TABLE '+ quote_name(table_name) +' ALTER COLUMN '+ quote_name(col_name) +' SET NOT NULL;' )
if unique:
output.append( 'ALTER TABLE '+ quote_name(table_name) +' ADD CONSTRAINT '+ table_name +'_'+ col_name +'_unique_constraint UNIQUE('+ col_name +');' )
return '\n'.join(output)

def get_drop_column_sql( table_name, col_name ):
output = []
output.append( '-- ALTER TABLE '+ quote_name(table_name) +' DROP COLUMN '+ quote_name(col_name) + ';' )
return '\n'.join(output)

# Register these custom typecasts, because Django expects dates/times to be
# in Python's native (standard-library) datetime/time format, whereas psycopg
# use mx.DateTime by default.
Expand Down
53 changes: 52 additions & 1 deletion django/db/backends/postgresql/introspection.py
@@ -1,4 +1,3 @@
from django.db import transaction
from django.db.backends.postgresql.base import quote_name

def get_table_list(cursor):
Expand Down Expand Up @@ -67,6 +66,58 @@ def get_indexes(cursor, table_name):
indexes[row[0]] = {'primary_key': row[3], 'unique': row[2]}
return indexes

def get_columns(cursor, table_name):
try:
cursor.execute("SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), (SELECT substring(d.adsrc for 128) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef), a.attnotnull, a.attnum, pg_catalog.col_description(a.attrelid, a.attnum) FROM pg_catalog.pg_attribute a WHERE a.attrelid = (SELECT c.oid from pg_catalog.pg_class c where c.relname ~ '^%s$') AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum" % table_name)
return [row[0] for row in cursor.fetchall()]
except:
return []

def get_known_column_flags( cursor, table_name, column_name ):
# print "SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), (SELECT substring(d.adsrc for 128) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef), a.attnotnull, a.attnum, pg_catalog.col_description(a.attrelid, a.attnum) FROM pg_catalog.pg_attribute a WHERE a.attrelid = (SELECT c.oid from pg_catalog.pg_class c where c.relname ~ '^%s$') AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum" % table_name
cursor.execute("SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), (SELECT substring(d.adsrc for 128) FROM pg_catalog.pg_attrdef d WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef), a.attnotnull, a.attnum, pg_catalog.col_description(a.attrelid, a.attnum) FROM pg_catalog.pg_attribute a WHERE a.attrelid = (SELECT c.oid from pg_catalog.pg_class c where c.relname ~ '^%s$') AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum" % table_name)
dict = {}
dict['primary_key'] = False
dict['foreign_key'] = False
dict['unique'] = False
dict['default'] = ''

# dict['allow_null'] = False
for row in cursor.fetchall():
if row[0] == column_name:

# maxlength check goes here
if row[1][0:17]=='character varying':
dict['maxlength'] = row[1][18:len(row[1])-1]

# null flag check goes here
dict['allow_null'] = not row[3]

# pk, fk and unique checks go here
# print "select pg_constraint.conname, pg_constraint.contype, pg_attribute.attname from pg_constraint, pg_attribute where pg_constraint.conrelid=pg_attribute.attrelid and pg_attribute.attnum=any(pg_constraint.conkey) and pg_constraint.conname~'^%s'" % table_name
unique_conname = None
shared_unique_connames = set()
cursor.execute("select pg_constraint.conname, pg_constraint.contype, pg_attribute.attname from pg_constraint, pg_attribute where pg_constraint.conrelid=pg_attribute.attrelid and pg_attribute.attnum=any(pg_constraint.conkey) and pg_constraint.conname~'^%s'" % table_name )
for row in cursor.fetchall():
if row[2] == column_name:
if row[1]=='p': dict['primary_key'] = True
if row[1]=='f': dict['foreign_key'] = True
if row[1]=='u': unique_conname = row[0]
else:
if row[1]=='u': shared_unique_connames.add( row[0] )
if unique_conname and unique_conname not in shared_unique_connames:
dict['unique'] = True

# default value check goes here
cursor.execute("select pg_attribute.attname, adsrc from pg_attrdef, pg_attribute WHERE pg_attrdef.adrelid=pg_attribute.attrelid and pg_attribute.attnum=pg_attrdef.adnum and pg_attrdef.adrelid = (SELECT c.oid from pg_catalog.pg_class c where c.relname ~ '^%s$')" % table_name )
for row in cursor.fetchall():
if row[0] == column_name:
if row[1][0:7] == 'nextval': continue
dict['default'] = row[1][1:row[1].index("'",1)]

# print table_name, column_name, dict
return dict

# Maps type codes to Django Field types.
DATA_TYPES_REVERSE = {
16: 'BooleanField',
Expand Down
45 changes: 45 additions & 0 deletions django/db/backends/sqlite3/base.py
Expand Up @@ -145,6 +145,51 @@ def _sqlite_date_trunc(lookup_type, dt):
elif lookup_type == 'day':
return "%i-%02i-%02i 00:00:00" % (dt.year, dt.month, dt.day)

def get_change_column_name_sql( table_name, indexes, old_col_name, new_col_name, col_def ):
# sqlite doesn't support column renames, so we fake it
# TODO: only supports a single primary key so far
pk_name = None
for key in indexes.keys():
if indexes[key]['primary_key']: pk_name = key
output = []
output.append( 'ALTER TABLE '+ quote_name(table_name) +' ADD COLUMN '+ quote_name(new_col_name) +' '+ col_def + ';' )
output.append( 'UPDATE '+ quote_name(table_name) +' SET '+ new_col_name +' = '+ old_col_name +' WHERE '+ pk_name +'=(select '+ pk_name +' from '+ table_name +');' )
output.append( '-- FYI: sqlite does not support deleting columns, so '+ quote_name(old_col_name) +' remains as cruft' )
# use the following when sqlite gets drop support
#output.append( 'ALTER TABLE '+ quote_name(table_name) +' DROP COLUMN '+ quote_name(old_col_name) )
return '\n'.join(output)

def get_change_column_def_sql( table_name, col_name, col_def ):
# sqlite doesn't support column modifications, so we fake it
output = []
# TODO: fake via renaming the table, building a new one and deleting the old
output.append('-- sqlite does not support column modifications '+ quote_name(table_name) +'.'+ quote_name(col_name) +' to '+ col_def)
return '\n'.join(output)

def get_add_column_sql( table_name, col_name, col_type, null, unique, primary_key ):
output = []
field_output = []
field_output.append('ALTER TABLE')
field_output.append(quote_name(table_name))
field_output.append('ADD COLUMN')
field_output.append(quote_name(col_name))
field_output.append(col_type)
field_output.append(('%sNULL' % (not null and 'NOT ' or '')))
if unique:
field_output.append(('UNIQUE'))
if primary_key:
field_output.append(('PRIMARY KEY'))
output.append(' '.join(field_output) + ';')
return '\n'.join(output)

def get_drop_column_sql( table_name, col_name ):
output = []
output.append( '-- FYI: sqlite does not support deleting columns, so '+ quote_name(old_col_name) +' remains as cruft' )
# use the following when sqlite gets drop support
# output.append( '-- ALTER TABLE '+ quote_name(table_name) +' DROP COLUMN '+ quote_name(col_name) )
return '\n'.join(output)


# SQLite requires LIKE statements to include an ESCAPE clause if the value
# being escaped has a percent or underscore in it.
# See http://www.sqlite.org/lang_expr.html for an explanation.
Expand Down
38 changes: 37 additions & 1 deletion django/db/backends/sqlite3/introspection.py
@@ -1,4 +1,3 @@
from django.db import transaction
from django.db.backends.sqlite3.base import quote_name

def get_table_list(cursor):
Expand Down Expand Up @@ -44,6 +43,43 @@ def get_indexes(cursor, table_name):
indexes[name]['unique'] = True
return indexes

def get_columns(cursor, table_name):
try:
cursor.execute("PRAGMA table_info(%s)" % quote_name(table_name))
return [row[1] for row in cursor.fetchall()]
except:
return []

def get_known_column_flags( cursor, table_name, column_name ):
cursor.execute("PRAGMA table_info(%s)" % quote_name(table_name))
dict = {}
for row in cursor.fetchall():
if row[1] == column_name:

# maxlength check goes here
if row[2][0:7]=='varchar':
dict['maxlength'] = row[2][8:len(row[2])-1]

# default flag check goes here
#if row[2]=='YES': dict['allow_null'] = True
#else: dict['allow_null'] = False

# primary/foreign/unique key flag check goes here
#if row[3]=='PRI': dict['primary_key'] = True
#else: dict['primary_key'] = False
#if row[3]=='FOR': dict['foreign_key'] = True
#else: dict['foreign_key'] = False
#if row[3]=='UNI': dict['unique'] = True
#else: dict['unique'] = False

# default value check goes here
# if row[4]=='NULL': dict['default'] = None
# else: dict['default'] = row[4]
#dict['default'] = row[4]

print table_name, column_name, dict
return dict

def _table_info(cursor, name):
cursor.execute('PRAGMA table_info(%s)' % quote_name(name))
# cid, name, type, notnull, dflt_value, pk
Expand Down
22 changes: 13 additions & 9 deletions django/db/models/fields/__init__.py
Expand Up @@ -4,7 +4,7 @@
from django.core import validators
from django import forms
from django.core.exceptions import ObjectDoesNotExist
from django.utils.functional import curry, lazy
from django.utils.functional import curry
from django.utils.text import capfirst
from django.utils.translation import gettext, gettext_lazy
import datetime, os, time
Expand All @@ -20,7 +20,7 @@ class NOT_PROVIDED:
BLANK_CHOICE_NONE = [("", "None")]

# prepares a value for use in a LIKE query
prep_for_like_query = lambda x: str(x).replace("%", "\%").replace("_", "\_")
prep_for_like_query = lambda x: str(x).replace("\\", "\\\\").replace("%", "\%").replace("_", "\_")

# returns the <ul> class for a given radio_admin value
get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '')
Expand Down Expand Up @@ -68,7 +68,7 @@ def __init__(self, verbose_name=None, name=None, primary_key=False,
core=False, rel=None, default=NOT_PROVIDED, editable=True,
prepopulate_from=None, unique_for_date=None, unique_for_month=None,
unique_for_year=None, validator_list=None, choices=None, radio_admin=None,
help_text='', db_column=None):
help_text='', db_column=None, aka=None):
self.name = name
self.verbose_name = verbose_name
self.primary_key = primary_key
Expand All @@ -84,6 +84,7 @@ def __init__(self, verbose_name=None, name=None, primary_key=False,
self.radio_admin = radio_admin
self.help_text = help_text
self.db_column = db_column
self.aka = aka

# Set db_index to True if the field has a relationship and doesn't explicitly set db_index.
self.db_index = db_index
Expand Down Expand Up @@ -247,9 +248,9 @@ def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=
params['is_required'] = not self.blank and not self.primary_key and not rel

# BooleanFields (CheckboxFields) are a special case. They don't take
# is_required or validator_list.
# is_required.
if isinstance(self, BooleanField):
del params['validator_list'], params['is_required']
del params['is_required']

# If this field is in a related context, check whether any other fields
# in the related object have core=True. If so, add a validator --
Expand Down Expand Up @@ -289,8 +290,11 @@ def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH):
if self.choices:
return first_choice + list(self.choices)
rel_model = self.rel.to
return first_choice + [(x._get_pk_val(), str(x))
for x in rel_model._default_manager.complex_filter(self.rel.limit_choices_to)]
if hasattr(self.rel, 'get_related_field'):
lst = [(getattr(x, self.rel.get_related_field().attname), str(x)) for x in rel_model._default_manager.complex_filter(self.rel.limit_choices_to)]
else:
lst = [(x._get_pk_val(), str(x)) for x in rel_model._default_manager.complex_filter(self.rel.limit_choices_to)]
return first_choice + lst

def get_choices_default(self):
if self.radio_admin:
Expand Down Expand Up @@ -364,8 +368,8 @@ def __init__(self, *args, **kwargs):

def to_python(self, value):
if value in (True, False): return value
if value is 't': return True
if value is 'f': return False
if value in ('t', 'True'): return True
if value in ('f', 'False'): return False
raise validators.ValidationError, gettext("This value must be either True or False.")

def get_manipulator_field_objs(self):
Expand Down

0 comments on commit ffac835

Please sign in to comment.