Django

Code

root/django/branches/schema-evolution/django/core/management.py

Revision 5822, 86.6 kB (checked in by danderson, 1 year ago)

schema-evolution: update from HEAD (v5821)

Line 
1 # Django management-related functions, including "CREATE TABLE" generation and
2 # development-server initialization.
3
4 import django
5 from django.core.exceptions import ImproperlyConfigured
6 from optparse import OptionParser
7 from django.utils import termcolors
8 from django.conf import settings
9 import os, re, shutil, sys, textwrap
10
11 try:
12     set
13 except NameError:
14     from sets import Set as set   # Python 2.3 fallback
15
16 # For backwards compatibility: get_version() used to be in this module.
17 get_version = django.get_version
18
19 MODULE_TEMPLATE = '''    {%% if perms.%(app)s.%(addperm)s or perms.%(app)s.%(changeperm)s %%}
20     <tr>
21         <th>{%% if perms.%(app)s.%(changeperm)s %%}<a href="%(app)s/%(mod)s/">{%% endif %%}%(name)s{%% if perms.%(app)s.%(changeperm)s %%}</a>{%% endif %%}</th>
22         <td class="x50">{%% if perms.%(app)s.%(addperm)s %%}<a href="%(app)s/%(mod)s/add/" class="addlink">{%% endif %%}Add{%% if perms.%(app)s.%(addperm)s %%}</a>{%% endif %%}</td>
23         <td class="x75">{%% if perms.%(app)s.%(changeperm)s %%}<a href="%(app)s/%(mod)s/" class="changelink">{%% endif %%}Change{%% if perms.%(app)s.%(changeperm)s %%}</a>{%% endif %%}</td>
24     </tr>
25     {%% endif %%}'''
26
27 APP_ARGS = '[appname ...]'
28
29 # Use django.__path__[0] because we don't know which directory django into
30 # which has been installed.
31 PROJECT_TEMPLATE_DIR = os.path.join(django.__path__[0], 'conf', '%s_template')
32
33 INVALID_PROJECT_NAMES = ('django', 'site', 'test')
34
35 # Set up the terminal color scheme.
36 class dummy: pass
37 style = dummy()
38 style.ERROR = termcolors.make_style(fg='red', opts=('bold',))
39 style.ERROR_OUTPUT = termcolors.make_style(fg='red', opts=('bold',))
40 style.NOTICE = termcolors.make_style(fg='red')
41 style.SQL_FIELD = termcolors.make_style(fg='green', opts=('bold',))
42 style.SQL_COLTYPE = termcolors.make_style(fg='green')
43 style.SQL_KEYWORD = termcolors.make_style(fg='yellow')
44 style.SQL_TABLE = termcolors.make_style(opts=('bold',))
45 del dummy
46
47 def disable_termcolors():
48     class dummy:
49         def __getattr__(self, attr):
50             return lambda x: x
51     global style
52     style = dummy()
53
54 # Disable terminal coloring on Windows, Pocket PC, or if somebody's piping the output.
55 if sys.platform == 'win32' or sys.platform == 'Pocket PC' or not sys.stdout.isatty():
56     disable_termcolors()
57
58 def _is_valid_dir_name(s):
59     return bool(re.search(r'^\w+$', s))
60
61 def _get_installed_models(table_list):
62     "Gets a set of all models that are installed, given a list of existing tables"
63     from django.db import backend, models
64     all_models = []
65     for app in models.get_apps():
66         for model in models.get_models(app):
67             all_models.append(model)
68     if backend.uses_case_insensitive_names:
69         converter = str.upper
70     else:
71         converter = lambda x: x
72     return set([m for m in all_models if converter(m._meta.db_table) in map(converter, table_list)])
73
74 def _get_table_list():
75     "Gets a list of all db tables that are physically installed."
76     from django.db import connection, get_introspection_module
77     cursor = connection.cursor()
78     return get_introspection_module().get_table_list(cursor)
79
80 def _get_sequence_list():
81     "Returns a list of information about all DB sequences for all models in all apps"
82     from django.db import models
83
84     apps = models.get_apps()
85     sequence_list = []
86
87     for app in apps:
88         for model in models.get_models(app):
89             for f in model._meta.fields:
90                 if isinstance(f, models.AutoField):
91                     sequence_list.append({'table':model._meta.db_table,'column':f.column,})
92                     break # Only one AutoField is allowed per model, so don't bother continuing.
93
94             for f in model._meta.many_to_many:
95                 sequence_list.append({'table':f.m2m_db_table(),'column':None,})
96
97     return sequence_list
98
99 def get_sql_create(app):
100     "Returns a list of the CREATE TABLE SQL statements for the given app."
101     from django.db import models
102     from django.conf import settings
103
104     if settings.DATABASE_ENGINE == 'dummy':
105         # This must be the "dummy" database backend, which means the user
106         # hasn't set DATABASE_ENGINE.
107         sys.stderr.write(style.ERROR("Error: Django doesn't know which syntax to use for your SQL statements,\n" +
108             "because you haven't specified the DATABASE_ENGINE setting.\n" +
109             "Edit your settings file and change DATABASE_ENGINE to something like 'postgresql' or 'mysql'.\n"))
110         sys.exit(1)
111
112     # Get installed models, so we generate REFERENCES right.
113     # We trim models from the current app so that the sqlreset command does not
114     # generate invalid SQL (leaving models out of known_models is harmless, so
115     # we can be conservative).
116     app_models = models.get_models(app)
117     final_output = []
118     known_models = set([model for model in _get_installed_models(_get_table_list()) if model not in app_models])
119     pending_references = {}
120
121     for model in app_models:
122         output, references = _get_sql_model_create(model, known_models)
123         final_output.extend(output)
124         for refto, refs in references.items():
125             pending_references.setdefault(refto,[]).extend(refs)
126         final_output.extend(_get_sql_for_pending_references(model, pending_references))
127         # Keep track of the fact that we've created the table for this model.
128         known_models.add(model)
129
130     # Create the many-to-many join tables.
131     for model in app_models:
132         final_output.extend(_get_many_to_many_sql_for_model(model))
133
134     # Handle references to tables that are from other apps
135     # but don't exist physically
136     not_installed_models = set(pending_references.keys())
137     if not_installed_models:
138         alter_sql = []
139         for model in not_installed_models:
140             alter_sql.extend(['-- ' + sql for sql in
141                 _get_sql_for_pending_references(model, pending_references)])
142         if alter_sql:
143             final_output.append('-- The following references should be added but depend on non-existent tables:')
144             final_output.extend(alter_sql)
145
146     return final_output
147 get_sql_create.help_doc = "Prints the CREATE TABLE SQL statements for the given app name(s)."
148 get_sql_create.args = APP_ARGS
149
150 def _get_sql_model_create(model, known_models=set()):
151     """
152     Get the SQL required to create a single model.
153
154     Returns list_of_sql, pending_references_dict
155     """
156     from django.db import backend, models
157
158     opts = model._meta
159     final_output = []
160     table_output = []
161     pending_references = {}
162     for f in opts.fields:
163         col_type = f.db_type()
164         tablespace = f.db_tablespace or opts.db_tablespace
165         if col_type is None:
166             # Skip ManyToManyFields, because they're not represented as
167             # database columns in this table.
168             continue
169         # Make the definition (e.g. 'foo VARCHAR(30)') for this field.
170         field_output = [style.SQL_FIELD(backend.quote_name(f.column)),
171             style.SQL_COLTYPE(col_type)]
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)) or (f.primary_key and backend.pk_requires_unique):
174             field_output.append(style.SQL_KEYWORD('UNIQUE'))
175         if f.primary_key:
176             field_output.append(style.SQL_KEYWORD('PRIMARY KEY'))
177         if tablespace and backend.supports_tablespaces and (f.unique or f.primary_key) and backend.autoindexes_primary_keys:
178             # We must specify the index tablespace inline, because we
179             # won't be generating a CREATE INDEX statement for this field.
180             field_output.append(backend.get_tablespace_sql(tablespace, inline=True))
181         if f.rel:
182             if f.rel.to in known_models:
183                 field_output.append(style.SQL_KEYWORD('REFERENCES') + ' ' + \
184                     style.SQL_TABLE(backend.quote_name(f.rel.to._meta.db_table)) + ' (' + \
185                     style.SQL_FIELD(backend.quote_name(f.rel.to._meta.get_field(f.rel.field_name).column)) + ')' +
186                     backend.get_deferrable_sql()
187                 )
188             else:
189                 # We haven't yet created the table to which this field
190                 # is related, so save it for later.
191                 pr = pending_references.setdefault(f.rel.to, []).append((model, f))
192         table_output.append(' '.join(field_output))
193     if opts.order_with_respect_to:
194         table_output.append(style.SQL_FIELD(backend.quote_name('_order')) + ' ' + \
195             style.SQL_COLTYPE(models.IntegerField().db_type()) + ' ' + \
196             style.SQL_KEYWORD('NULL'))
197     for field_constraints in opts.unique_together:
198         table_output.append(style.SQL_KEYWORD('UNIQUE') + ' (%s)' % \
199             ", ".join([backend.quote_name(style.SQL_FIELD(opts.get_field(f).column)) for f in field_constraints]))
200
201     full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + style.SQL_TABLE(backend.quote_name(opts.db_table)) + ' (']
202     for i, line in enumerate(table_output): # Combine and add commas.
203         full_statement.append('    %s%s' % (line, i < len(table_output)-1 and ',' or ''))
204     full_statement.append(')')
205     if opts.db_tablespace and backend.supports_tablespaces:
206         full_statement.append(backend.get_tablespace_sql(opts.db_tablespace))
207     full_statement.append(';')
208     final_output.append('\n'.join(full_statement))
209
210     if opts.has_auto_field and hasattr(backend, 'get_autoinc_sql'):
211         # Add any extra SQL needed to support auto-incrementing primary keys
212         autoinc_sql = backend.get_autoinc_sql(opts.db_table)
213         if autoinc_sql:
214             for stmt in autoinc_sql:
215                 final_output.append(stmt)
216
217     return final_output, pending_references
218
219 def _get_sql_for_pending_references(model, pending_references):
220     """
221     Get any ALTER TABLE statements to add constraints after the fact.
222     """
223     from django.db import backend
224     from django.db.backends.util import truncate_name
225
226     final_output = []
227     if backend.supports_constraints:
228         opts = model._meta
229         if model in pending_references:
230             for rel_class, f in pending_references[model]:
231                 rel_opts = rel_class._meta
232                 r_table = rel_opts.db_table
233                 r_col = f.column
234                 table = opts.db_table
235                 col = opts.get_field(f.rel.field_name).column
236                 # For MySQL, r_name must be unique in the first 64 characters.
237                 # So we are careful with character usage here.
238                 r_name = '%s_refs_%s_%x' % (r_col, col, abs(hash((r_table, table))))
239                 final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % \
240                     (backend.quote_name(r_table), truncate_name(r_name, backend.get_max_name_length()),
241                     backend.quote_name(r_col), backend.quote_name(table), backend.quote_name(col),
242                     backend.get_deferrable_sql()))
243             del pending_references[model]
244     return final_output
245
246 def _get_many_to_many_sql_for_model(model):
247     from django.db import backend, models
248     from django.contrib.contenttypes import generic
249
250     opts = model._meta
251     final_output = []
252     for f in opts.many_to_many:
253         if not isinstance(f.rel, generic.GenericRel):
254             tablespace = f.db_tablespace or opts.db_tablespace
255             if tablespace and backend.supports_tablespaces and backend.autoindexes_primary_keys:
256                 tablespace_sql = ' ' + backend.get_tablespace_sql(tablespace, inline=True)
257             else:
258                 tablespace_sql = ''
259             table_output = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + \
260                 style.SQL_TABLE(backend.quote_name(f.m2m_db_table())) + ' (']
261             table_output.append('    %s %s %s%s,' % \
262                 (style.SQL_FIELD(backend.quote_name('id')),
263                 style.SQL_COLTYPE(models.AutoField(primary_key=True).db_type()),
264                 style.SQL_KEYWORD('NOT NULL PRIMARY KEY'),
265                 tablespace_sql))
266             table_output.append('    %s %s %s %s (%s)%s,' % \
267                 (style.SQL_FIELD(backend.quote_name(f.m2m_column_name())),
268                 style.SQL_COLTYPE(models.ForeignKey(model).db_type()),
269                 style.SQL_KEYWORD('NOT NULL REFERENCES'),
270                 style.SQL_TABLE(backend.quote_name(opts.db_table)),
271                 style.SQL_FIELD(backend.quote_name(opts.pk.column)),
272                 backend.get_deferrable_sql()))
273             table_output.append('    %s %s %s %s (%s)%s,' % \
274                 (style.SQL_FIELD(backend.quote_name(f.m2m_reverse_name())),
275                 style.SQL_COLTYPE(models.ForeignKey(f.rel.to).db_type()),
276                 style.SQL_KEYWORD('NOT NULL REFERENCES'),
277                 style.SQL_TABLE(backend.quote_name(f.rel.to._meta.db_table)),
278                 style.SQL_FIELD(backend.quote_name(f.rel.to._meta.pk.column)),
279                 backend.get_deferrable_sql()))
280             table_output.append('    %s (%s, %s)%s' % \
281                 (style.SQL_KEYWORD('UNIQUE'),
282                 style.SQL_FIELD(backend.quote_name(f.m2m_column_name())),
283                 style.SQL_FIELD(backend.quote_name(f.m2m_reverse_name())),
284                 tablespace_sql))
285             table_output.append(')')
286             if opts.db_tablespace and backend.supports_tablespaces:
287                 # f.db_tablespace is only for indices, so ignore its value here.
288                 table_output.append(backend.get_tablespace_sql(opts.db_tablespace))
289             table_output.append(';')
290             final_output.append('\n'.join(table_output))
291
292             # Add any extra SQL needed to support auto-incrementing PKs
293             autoinc_sql = backend.get_autoinc_sql(f.m2m_db_table())
294             if autoinc_sql:
295                 for stmt in autoinc_sql:
296                     final_output.append(stmt)
297
298     return final_output
299
300 def get_sql_delete(app):
301     "Returns a list of the DROP TABLE SQL statements for the given app."
302     from django.db import backend, connection, models, get_introspection_module
303     from django.db.backends.util import truncate_name
304     introspection = get_introspection_module()
305
306     # This should work even if a connection isn't available
307     try:
308         cursor = connection.cursor()
309     except:
310         cursor = None
311
312     # Figure out which tables already exist
313     if cursor:
314         table_names = introspection.get_table_list(cursor)
315     else:
316         table_names = []
317     if backend.uses_case_insensitive_names:
318         table_name_converter = str.upper
319     else:
320         table_name_converter = lambda x: x
321
322     output = []
323
324     # Output DROP TABLE statements for standard application tables.
325     to_delete = set()
326
327     references_to_delete = {}
328     app_models = models.get_models(app)
329     for model in app_models:
330         if cursor and table_name_converter(model._meta.db_table) in table_names:
331             # The table exists, so it needs to be dropped
332             opts = model._meta
333             for f in opts.fields:
334                 if f.rel and f.rel.to not in to_delete:
335                     references_to_delete.setdefault(f.rel.to, []).append( (model, f) )
336
337             to_delete.add(model)
338
339     for model in app_models:
340         if cursor and table_name_converter(model._meta.db_table) in table_names:
341             # Drop the table now
342             output.append('%s %s;' % (style.SQL_KEYWORD('DROP TABLE'),
343                 style.SQL_TABLE(backend.quote_name(model._meta.db_table))))
344             if backend.supports_constraints and model in references_to_delete:
345                 for rel_class, f in references_to_delete[model]:
346                     table = rel_class._meta.db_table
347                     col = f.column
348                     r_table = model._meta.db_table
349                     r_col = model._meta.get_field(f.rel.field_name).column
350                     r_name = '%s_refs_%s_%x' % (col, r_col, abs(hash((table, r_table))))
351                     output.append('%s %s %s %s;' % \
352                         (style.SQL_KEYWORD('ALTER TABLE'),
353                         style.SQL_TABLE(backend.quote_name(table)),
354                         style.SQL_KEYWORD(backend.get_drop_foreignkey_sql()),
355                         style.SQL_FIELD(truncate_name(r_name, backend.get_max_name_length()))))
356                 del references_to_delete[model]
357             if model._meta.has_auto_field and hasattr(backend, 'get_drop_sequence'):
358                 output.append(backend.get_drop_sequence(model._meta.db_table))
359
360     # Output DROP TABLE statements for many-to-many tables.
361     for model in app_models:
362         opts = model._meta
363         for f in opts.many_to_many:
364             if cursor and table_name_converter(f.m2m_db_table()) in table_names:
365                 output.append("%s %s;" % (style.SQL_KEYWORD('DROP TABLE'),
366                     style.SQL_TABLE(backend.quote_name(f.m2m_db_table()))))
367                 if hasattr(backend, 'get_drop_sequence'):
368                     output.append(backend.get_drop_sequence("%s_%s" % (model._meta.db_table, f.column)))
369
370
371     app_label = app_models[0]._meta.app_label
372
373     # Close database connection explicitly, in case this output is being piped
374     # directly into a database client, to avoid locking issues.
375     if cursor:
376         cursor.close()
377         connection.close()
378
379     return output[::-1] # Reverse it, to deal with table dependencies.
380 get_sql_delete.help_doc = "Prints the DROP TABLE SQL statements for the given app name(s)."
381 get_sql_delete.args = APP_ARGS
382
383 def get_sql_reset(app):
384     "Returns a list of the DROP TABLE SQL, then the CREATE TABLE SQL, for the given module."
385     return get_sql_delete(app) + get_sql_all(app)
386 get_sql_reset.help_doc = "Prints the DROP TABLE SQL, then the CREATE TABLE SQL, for the given app name(s)."
387 get_sql_reset.args = APP_ARGS
388
389 def get_sql_flush():
390     "Returns a list of the SQL statements used to flush the database"
391     from django.db import backend
392     statements = backend.get_sql_flush(style, _get_table_list(), _get_sequence_list())
393     return statements
394 get_sql_flush.help_doc = "Returns a list of the SQL statements required to return all tables in the database to the state they were in just after they were installed."
395 get_sql_flush.args = ''
396
397 def get_custom_sql_for_model(model):
398     from django.db import models
399     from django.conf import settings
400
401     opts = model._meta
402     app_dir = os.path.normpath(os.path.join(os.path.dirname(models.get_app(model._meta.app_label).__file__), 'sql'))
403     output = []
404
405     # Some backends can't execute more than one SQL statement at a time,
406     # so split into separate statements.
407     statements = re.compile(r";[ \t]*$", re.M)
408
409     # Find custom SQL, if it's available.
410     sql_files = [os.path.join(app_dir, "%s.%s.sql" % (opts.object_name.lower(), settings.DATABASE_ENGINE)),
411                  os.path.join(app_dir, "%s.sql" % opts.object_name.lower())]
412     for sql_file in sql_files:
413         if os.path.exists(sql_file):
414             fp = open(sql_file, 'U')
415             for statement in statements.split(fp.read().decode(settings.FILE_CHARSET)):
416                 # Remove any comments from the file
417                 statement = re.sub(ur"--.*[\n\Z]", "", statement)
418                 if statement.strip():
419                     output.append(statement + u";")
420             fp.close()
421
422     return output
423
424 def get_custom_sql(app):
425     "Returns a list of the custom table modifying SQL statements for the given app."
426     from django.db.models import get_models
427     output = []
428
429     app_models = get_models(app)
430     app_dir = os.path.normpath(os.path.join(os.path.dirname(app.__file__), 'sql'))
431
432     for model in app_models:
433         output.extend(get_custom_sql_for_model(model))
434
435     return output
436 get_custom_sql.help_doc = "Prints the custom table modifying SQL statements for the given app name(s)."
437 get_custom_sql.args = APP_ARGS
438
439 def get_sql_initial_data(apps):
440     "Returns a list of the initial INSERT SQL statements for the given app."
441     return style.ERROR("This action has been renamed. Try './manage.py sqlcustom %s'." % ' '.join(apps and apps or ['app1', 'app2']))
442 get_sql_initial_data