Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

schema-evolution:

added new "pk_requires_unique" option to the backend, because sqlite3 requires "UNIQUE" when creating PKs in order to 
_actually_ create the constraint.
fixed "get_known_column_flags" introspection for sqlite3
implemented "get_drop_column_sql" for sqlite3 to work around sqlite's lack of DROP COLUMN support
added partial of the sqlite3 unit tests



git-svn-id: http://code.djangoproject.com/svn/django/branches/schema-evolution@5785 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 38c1cd721dc78b17088257caff78f668315d9b25 1 parent 0b4c2c7
Derek Anderson authored August 02, 2007
20  django/core/management.py
@@ -170,7 +170,7 @@ def _get_sql_model_create(model, known_models=set()):
170 170
         field_output = [style.SQL_FIELD(backend.quote_name(f.column)),
171 171
             style.SQL_COLTYPE(col_type)]
172 172
         field_output.append(style.SQL_KEYWORD('%sNULL' % (not f.null and 'NOT ' or '')))
173  
-        if f.unique and (not f.primary_key or backend.allows_unique_and_pk):
  173
+        if (f.unique and (not f.primary_key or backend.allows_unique_and_pk)) or (f.primary_key and backend.pk_requires_unique):
174 174
             field_output.append(style.SQL_KEYWORD('UNIQUE'))
175 175
         if f.primary_key:
176 176
             field_output.append(style.SQL_KEYWORD('PRIMARY KEY'))
@@ -569,18 +569,6 @@ def get_sql_evolution_check_for_new_fields(klass, new_table_name):
569 569
             data_type = f.get_internal_type()
570 570
             col_type = data_types[data_type]
571 571
             if col_type is not None:
572  
-#                field_output = []
573  
-#                field_output.append('ALTER TABLE')
574  
-#                field_output.append(db_table)
575  
-#                field_output.append('ADD COLUMN')
576  
-#                field_output.append(backend.quote_name(f.column))
577  
-#                field_output.append(style.SQL_COLTYPE(col_type % rel_field.__dict__))
578  
-#                field_output.append(style.SQL_KEYWORD('%sNULL' % (not f.null and 'NOT ' or '')))
579  
-#                if f.unique:
580  
-#                    field_output.append(style.SQL_KEYWORD('UNIQUE'))
581  
-#                if f.primary_key:
582  
-#                    field_output.append(style.SQL_KEYWORD('PRIMARY KEY'))
583  
-#                output.append(' '.join(field_output) + ';')
584 572
                 output.append( backend.get_add_column_sql( db_table, f.column, style.SQL_COLTYPE(col_type % rel_field.__dict__), f.null, f.unique, f.primary_key ) )
585 573
     return output
586 574
 
@@ -664,7 +652,7 @@ def get_sql_evolution_check_for_changed_field_flags(klass, new_table_name):
664 652
                     ( column_flags['unique']!=f.unique and ( settings.DATABASE_ENGINE!='postgresql' or not f.primary_key ) ) or \
665 653
                     column_flags['primary_key']!=f.primary_key:
666 654
                     #column_flags['foreign_key']!=f.foreign_key:
667  
-#                print 
  655
+#                print 'need to change'
668 656
 #                print db_table, f.column, column_flags
669 657
 #                print "column_flags['allow_null']!=f.null", column_flags['allow_null']!=f.null
670 658
 #                print "not f.primary_key and isinstance(f, CharField) and column_flags['maxlength']!=str(f.maxlength)", not f.primary_key and isinstance(f, CharField) and column_flags['maxlength']!=str(f.maxlength)
@@ -703,9 +691,9 @@ def get_sql_evolution_check_for_dead_fields(klass, new_table_name):
703 691
         suspect_fields.discard(f.aka)
704 692
         if f.aka: suspect_fields.difference_update(f.aka)
705 693
     if len(suspect_fields)>0:
706  
-        output.append( '-- warning: as the following may cause data loss, it/they must be run manually' )
  694
+        output.append( '-- warning: the following may cause data loss' )
707 695
         for suspect_field in suspect_fields:
708  
-            output.append( '-- '+ backend.get_drop_column_sql( db_table, suspect_field ) )
  696
+            output.extend( backend.get_drop_column_sql( db_table, suspect_field ) )
709 697
         output.append( '-- end warning' )
710 698
     return output
711 699
 
1  django/db/backends/ado_mssql/base.py
@@ -91,6 +91,7 @@ def close(self):
91 91
 
92 92
 allows_group_by_ordinal = True
93 93
 allows_unique_and_pk = True
  94
+pk_requires_unique = False
94 95
 autoindexes_primary_keys = True
95 96
 needs_datetime_string_cast = True
96 97
 needs_upper_for_iops = False
3  django/db/backends/mysql/base.py
@@ -136,6 +136,7 @@ def get_server_version(self):
136 136
 
137 137
 allows_group_by_ordinal = True
138 138
 allows_unique_and_pk = True
  139
+pk_requires_unique = False
139 140
 autoindexes_primary_keys = False
140 141
 needs_datetime_string_cast = True     # MySQLdb requires a typecast for dates
141 142
 needs_upper_for_iops = False
@@ -284,7 +285,7 @@ def get_add_column_sql( table_name, col_name, col_type, null, unique, primary_ke
284 285
 def get_drop_column_sql( table_name, col_name ):
285 286
     output = []
286 287
     output.append( 'ALTER TABLE '+ quote_name(table_name) +' DROP COLUMN '+ quote_name(col_name) + ';' )
287  
-    return '\n'.join(output)
  288
+    return output
288 289
     
289 290
     
290 291
 OPERATOR_MAPPING = {
1  django/db/backends/mysql_old/base.py
@@ -151,6 +151,7 @@ def get_server_version(self):
151 151
 
152 152
 allows_group_by_ordinal = True
153 153
 allows_unique_and_pk = True
  154
+pk_requires_unique = False
154 155
 autoindexes_primary_keys = False
155 156
 needs_datetime_string_cast = True     # MySQLdb requires a typecast for dates
156 157
 needs_upper_for_iops = False
3  django/db/backends/postgresql/base.py
@@ -117,6 +117,7 @@ def close(self):
117 117
 
118 118
 allows_group_by_ordinal = True
119 119
 allows_unique_and_pk = True
  120
+pk_requires_unique = False
120 121
 autoindexes_primary_keys = True
121 122
 needs_datetime_string_cast = True
122 123
 needs_upper_for_iops = False
@@ -321,7 +322,7 @@ def get_add_column_sql( table_name, col_name, col_type, null, unique, primary_ke
321 322
 def get_drop_column_sql( table_name, col_name ):
322 323
     output = []
323 324
     output.append( 'ALTER TABLE '+ quote_name(table_name) +' DROP COLUMN '+ quote_name(col_name) + ';' )
324  
-    return '\n'.join(output)
  325
+    return output
325 326
 
326 327
 # Register these custom typecasts, because Django expects dates/times to be
327 328
 # in Python's native (standard-library) datetime/time format, whereas psycopg
4  django/db/backends/postgresql/introspection.py
@@ -81,8 +81,8 @@ def get_known_column_flags( cursor, table_name, column_name ):
81 81
     dict['foreign_key'] = False
82 82
     dict['unique'] = False
83 83
     dict['default'] = ''
84  
-            
85  
-#    dict['allow_null'] = False
  84
+    dict['allow_null'] = False
  85
+
86 86
     for row in cursor.fetchall():
87 87
         if row[0] == column_name:
88 88
 
1  django/db/backends/postgresql_psycopg2/base.py
@@ -79,6 +79,7 @@ def close(self):
79 79
 
80 80
 allows_group_by_ordinal = True
81 81
 allows_unique_and_pk = True
  82
+pk_requires_unique = False
82 83
 autoindexes_primary_keys = True
83 84
 needs_datetime_string_cast = False
84 85
 needs_upper_for_iops = False
38  django/db/backends/sqlite3/base.py
@@ -2,6 +2,7 @@
2 2
 SQLite3 backend for django.  Requires pysqlite2 (http://pysqlite.org/).
3 3
 """
4 4
 
  5
+from django.core import management
5 6
 from django.db.backends import util
6 7
 try:
7 8
     try:
@@ -102,6 +103,7 @@ def convert_query(self, query, num_params):
102 103
 
103 104
 allows_group_by_ordinal = True
104 105
 allows_unique_and_pk = True
  106
+pk_requires_unique = True # or else the constraint is never created
105 107
 autoindexes_primary_keys = True
106 108
 needs_datetime_string_cast = True
107 109
 needs_upper_for_iops = False
@@ -227,13 +229,16 @@ def get_change_column_name_sql( table_name, indexes, old_col_name, new_col_name,
227 229
     output.append( 'ALTER TABLE '+ quote_name(table_name) +' ADD COLUMN '+ quote_name(new_col_name) +' '+ col_def + ';' )
228 230
     output.append( 'UPDATE '+ quote_name(table_name) +' SET '+ new_col_name +' = '+ old_col_name +' WHERE '+ pk_name +'=(select '+ pk_name +' from '+ table_name +');' )
229 231
     output.append( '-- FYI: sqlite does not support deleting columns, so  '+ quote_name(old_col_name) +' remains as cruft' )
230  
-    # use the following when sqlite gets drop support
231  
-    #output.append( 'ALTER TABLE '+ quote_name(table_name) +' DROP COLUMN '+ quote_name(old_col_name) )
232 232
     return '\n'.join(output)
233 233
 
234  
-def get_change_column_def_sql( table_name, col_name, col_def ):
  234
+def get_change_column_def_sql( table_name, col_name, col_type, null, unique, primary_key ):
235 235
     # sqlite doesn't support column modifications, so we fake it
236 236
     output = []
  237
+    col_def = col_type +' '+ ('%sNULL' % (not null and 'NOT ' or ''))
  238
+    if unique or primary_key:
  239
+        col_def += ' '+ 'UNIQUE'
  240
+    if primary_key:
  241
+        col_def += ' '+ 'PRIMARY KEY'
237 242
     # TODO: fake via renaming the table, building a new one and deleting the old
238 243
     output.append('-- sqlite does not support column modifications '+ quote_name(table_name) +'.'+ quote_name(col_name) +' to '+ col_def)
239 244
     return '\n'.join(output)
@@ -247,7 +252,7 @@ def get_add_column_sql( table_name, col_name, col_type, null, unique, primary_ke
247 252
     field_output.append(quote_name(col_name))
248 253
     field_output.append(col_type)
249 254
     field_output.append(('%sNULL' % (not null and 'NOT ' or '')))
250  
-    if unique:
  255
+    if unique or primary_key:
251 256
         field_output.append(('UNIQUE'))
252 257
     if primary_key:
253 258
         field_output.append(('PRIMARY KEY'))
@@ -255,11 +260,28 @@ def get_add_column_sql( table_name, col_name, col_type, null, unique, primary_ke
255 260
     return '\n'.join(output)
256 261
 
257 262
 def get_drop_column_sql( table_name, col_name ):
  263
+    model = get_model_from_table_name(table_name)
258 264
     output = []
259  
-    output.append( '-- FYI: sqlite does not support deleting columns, so  '+ quote_name(old_col_name) +' remains as cruft' )
260  
-    # use the following when sqlite gets drop support
261  
-    # output.append( '-- ALTER TABLE '+ quote_name(table_name) +' DROP COLUMN '+ quote_name(col_name) )
262  
-    return '\n'.join(output)
  265
+    output.append( '-- FYI: sqlite does not support deleting columns, so we create a new '+ quote_name(col_name) +' and delete the old  (ie, this could take a while)' )
  266
+    tmp_table_name = table_name + '_1337_TMP' # unlikely to produce a namespace conflict
  267
+    output.append( get_change_table_name_sql( tmp_table_name, table_name ) )
  268
+    output.extend( management._get_sql_model_create(model, set())[0] )
  269
+    new_cols = []
  270
+    for f in model._meta.fields:
  271
+        new_cols.append( quote_name(f.column) )
  272
+    output.append( 'INSERT INTO '+ quote_name(table_name) +' SELECT '+ ','.join(new_cols) +' FROM '+ quote_name(tmp_table_name) +';' )
  273
+    output.append( 'DROP TABLE '+ quote_name(tmp_table_name) +';' )
  274
+    return output
  275
+
  276
+def get_model_from_table_name(table_name):
  277
+    from django.db import models
  278
+    for app in models.get_apps():
  279
+        app_name = app.__name__.split('.')[-2]
  280
+        if app_name == table_name.split('_')[0] or app_name == '_'.join(table_name.split('_')[0:1]) or app_name == '_'.join(table_name.split('_')[0:2]):
  281
+            for model in models.get_models(app):
  282
+                if model._meta.db_table == table_name:
  283
+                    return model
  284
+    return None
263 285
     
264 286
 
265 287
 # SQLite requires LIKE statements to include an ESCAPE clause if the value
33  django/db/backends/sqlite3/introspection.py
@@ -53,17 +53,38 @@ def get_columns(cursor, table_name):
53 53
 def get_known_column_flags( cursor, table_name, column_name ):
54 54
     cursor.execute("PRAGMA table_info(%s)" % quote_name(table_name))
55 55
     dict = {}
  56
+    dict['primary_key'] = False
  57
+    dict['foreign_key'] = False
  58
+    dict['unique'] = False
  59
+    dict['default'] = ''
  60
+    dict['allow_null'] = True
  61
+
56 62
     for row in cursor.fetchall():
  63
+#        print row
57 64
         if row[1] == column_name:
  65
+            col_type = row[2]
58 66
 
59 67
             # maxlength check goes here
60 68
             if row[2][0:7]=='varchar':
61 69
                 dict['maxlength'] = row[2][8:len(row[2])-1]
62 70
             
63 71
             # default flag check goes here
64  
-            #if row[2]=='YES': dict['allow_null'] = True
65  
-            #else: dict['allow_null'] = False
  72
+            dict['allow_null'] = row[3]==0
66 73
             
  74
+            # default value check goes here
  75
+            dict['default'] = row[4]
  76
+
  77
+    cursor.execute("PRAGMA index_list(%s)" % quote_name(table_name))
  78
+    index_names = []
  79
+    for row in cursor.fetchall():
  80
+        index_names.append(row[1])
  81
+    for index_name in index_names:
  82
+        cursor.execute("PRAGMA index_info(%s)" % quote_name(index_name))
  83
+        for row in cursor.fetchall():
  84
+            if row[2]==column_name:
  85
+                if col_type=='integer': dict['primary_key'] = True  # sqlite3 does not distinguish between unique and pk; all 
  86
+                else: dict['unique'] = True                         # unique integer columns are treated as part of the pk.
  87
+
67 88
             # primary/foreign/unique key flag check goes here
68 89
             #if row[3]=='PRI': dict['primary_key'] = True
69 90
             #else: dict['primary_key'] = False
@@ -72,12 +93,8 @@ def get_known_column_flags( cursor, table_name, column_name ):
72 93
             #if row[3]=='UNI': dict['unique'] = True
73 94
             #else: dict['unique'] = False
74 95
             
75  
-            # default value check goes here
76  
-            # if row[4]=='NULL': dict['default'] = None
77  
-            # else: dict['default'] = row[4]
78  
-            #dict['default'] = row[4]
79  
-            
80  
-    print table_name, column_name, dict
  96
+
  97
+#    print dict
81 98
     return dict
82 99
     
83 100
 def _table_info(cursor, name):
2  django/test/simple.py
@@ -98,4 +98,4 @@ def run_tests(module_list, verbosity=1, extra_tests=[]):
98 98
     teardown_test_environment()
99 99
     
100 100
     return len(result.failures) + len(result.errors)
101  
-    
  101
+    
66  tests/modeltests/schema_evolution/models.py
@@ -141,3 +141,69 @@ class Meta:
141 141
 
142 142
 """
143 143
 
  144
+if settings.DATABASE_ENGINE == 'sqlite3':
  145
+    __test__['API_TESTS'] += """
  146
+# the table as it is supposed to be
  147
+>>> create_table_sql = management.get_sql_all(app)
  148
+>>> print create_table_sql
  149
+['CREATE TABLE "schema_evolution_person" (\\n    "id" integer NOT NULL UNIQUE PRIMARY KEY,\\n    "name" varchar(20) NOT NULL,\\n    "gender" varchar(1) NOT NULL,\\n    "gender2" varchar(1) NOT NULL\\n)\\n;']
  150
+
  151
+# make sure we don't evolve an unedited table
  152
+>>> management.get_sql_evolution(app)
  153
+[]
  154
+
  155
+# delete a column, so it looks like we've recently added a field
  156
+>>> cursor.execute( 'DROP TABLE "schema_evolution_person";' ).__class__
  157
+<class 'django.db.backends.sqlite3.base.SQLiteCursorWrapper'>
  158
+>>> cursor.execute( 'CREATE TABLE "schema_evolution_person" ( "id" integer NOT NULL UNIQUE PRIMARY KEY, "name" varchar(20) NOT NULL, "gender" varchar(1) NOT NULL );' ).__class__
  159
+<class 'django.db.backends.sqlite3.base.SQLiteCursorWrapper'>
  160
+>>> management.get_sql_evolution(app)
  161
+['ALTER TABLE "schema_evolution_person" ADD COLUMN "gender2" varchar(1) NOT NULL;']
  162
+
  163
+# reset the db
  164
+>>> cursor.execute('DROP TABLE schema_evolution_person;').__class__
  165
+<class 'django.db.backends.sqlite3.base.SQLiteCursorWrapper'>
  166
+>>> cursor.execute(create_table_sql[0]).__class__
  167
+<class 'django.db.backends.sqlite3.base.SQLiteCursorWrapper'>
  168
+
  169
+# add a column, so it looks like we've recently deleted a field
  170
+>>> cursor.execute( 'DROP TABLE "schema_evolution_person";' ).__class__
  171
+<class 'django.db.backends.sqlite3.base.SQLiteCursorWrapper'>
  172
+>>> cursor.execute( 'CREATE TABLE "schema_evolution_person" ( "id" integer NOT NULL UNIQUE PRIMARY KEY, "name" varchar(20) NOT NULL, "gender" varchar(1) NOT NULL, "gender2" varchar(1) NOT NULL, "gender_new" varchar(1) NOT NULL );' ).__class__
  173
+<class 'django.db.backends.sqlite3.base.SQLiteCursorWrapper'>
  174
+>>> management.get_sql_evolution(app)
  175
+['-- warning: the following may cause data loss', u'-- FYI: sqlite does not support deleting columns, so we create a new "gender_new" and delete the old  (ie, this could take a while)', 'ALTER TABLE "schema_evolution_person" RENAME TO "schema_evolution_person_1337_TMP";', 'CREATE TABLE "schema_evolution_person" (\\n    "id" integer NOT NULL UNIQUE PRIMARY KEY,\\n    "name" varchar(20) NOT NULL,\\n    "gender" varchar(1) NOT NULL,\\n    "gender2" varchar(1) NOT NULL\\n)\\n;', 'INSERT INTO "schema_evolution_person" SELECT "id","name","gender","gender2" FROM "schema_evolution_person_1337_TMP";', 'DROP TABLE "schema_evolution_person_1337_TMP";', '-- end warning']
  176
+
  177
+# reset the db
  178
+>>> cursor.execute('DROP TABLE schema_evolution_person;').__class__
  179
+<class 'django.db.backends.sqlite3.base.SQLiteCursorWrapper'>
  180
+>>> cursor.execute(create_table_sql[0]).__class__
  181
+<class 'django.db.backends.sqlite3.base.SQLiteCursorWrapper'>
  182
+
  183
+"""
  184
+
  185
+crap = """
  186
+
  187
+# rename column, so it looks like we've recently renamed a field
  188
+>>> cursor.execute( backend.get_change_column_name_sql( 'schema_evolution_person', {}, 'gender2', 'gender_old', 'varchar(1)' ) )
  189
+>>> management.get_sql_evolution(app)
  190
+['ALTER TABLE "schema_evolution_person" RENAME COLUMN "gender_old" TO "gender2";']
  191
+
  192
+# reset the db
  193
+>>> cursor.execute('DROP TABLE schema_evolution_person;'); cursor.execute(create_table_sql[0])
  194
+
  195
+# rename table, so it looks like we've recently renamed a model
  196
+>>> cursor.execute( backend.get_change_table_name_sql( 'schema_evolution_personold', 'schema_evolution_person' ) )
  197
+>>> management.get_sql_evolution(app)
  198
+['ALTER TABLE "schema_evolution_personold" RENAME TO "schema_evolution_person";']
  199
+
  200
+# reset the db
  201
+>>> cursor.execute(create_table_sql[0])
  202
+
  203
+# change column flags, so it looks like we've recently changed a column flag
  204
+>>> cursor.execute( backend.get_change_column_def_sql( 'schema_evolution_person', 'name', 'varchar(10)', True, False, False ) )
  205
+>>> management.get_sql_evolution(app)
  206
+['ALTER TABLE "schema_evolution_person" ADD COLUMN "name_tmp" varchar(20);\\nUPDATE "schema_evolution_person" SET "name_tmp" = "name";\\nALTER TABLE "schema_evolution_person" DROP COLUMN "name";\\nALTER TABLE "schema_evolution_person" RENAME COLUMN "name_tmp" TO "name";\\nALTER TABLE "schema_evolution_person" ALTER COLUMN "name" SET NOT NULL;']
  207
+
  208
+"""
  209
+

0 notes on commit 38c1cd7

Please sign in to comment.
Something went wrong with that request. Please try again.