Permalink
Browse files

Merge pull request #376 from andrewgodwin/schema-alteration

Schema alteration
  • Loading branch information...
2 parents 57c82f9 + 5569b0b commit 9aa358cedd1ad93c0f4c20700db7016651dc0598 @mjtamlyn mjtamlyn committed Aug 23, 2013
Showing with 6,334 additions and 403 deletions.
  1. +7 −0 django/conf/global_settings.py
  2. +3 −3 django/contrib/auth/management/__init__.py
  3. +2 −2 django/contrib/contenttypes/management.py
  4. +1 −1 django/contrib/gis/db/backends/spatialite/creation.py
  5. +1 −1 django/contrib/gis/tests/layermap/tests.py
  6. +2 −2 django/contrib/sites/management.py
  7. +1 −1 django/core/management/commands/createcachetable.py
  8. +1 −1 django/core/management/commands/dumpdata.py
  9. +12 −12 django/core/management/commands/flush.py
  10. +1 −1 django/core/management/commands/loaddata.py
  11. +84 −0 django/core/management/commands/makemigrations.py
  12. +245 −0 django/core/management/commands/migrate.py
  13. +5 −145 django/core/management/commands/syncdb.py
  14. +8 −8 django/core/management/sql.py
  15. +47 −3 django/db/backends/__init__.py
  16. +8 −5 django/db/backends/creation.py
  17. +8 −0 django/db/backends/mysql/base.py
  18. +68 −2 django/db/backends/mysql/introspection.py
  19. +26 −0 django/db/backends/mysql/schema.py
  20. +10 −0 django/db/backends/oracle/base.py
  21. +11 −4 django/db/backends/oracle/creation.py
  22. +140 −0 django/db/backends/oracle/introspection.py
  23. +103 −0 django/db/backends/oracle/schema.py
  24. +7 −0 django/db/backends/postgresql_psycopg2/base.py
  25. +7 −2 django/db/backends/postgresql_psycopg2/creation.py
  26. +96 −1 django/db/backends/postgresql_psycopg2/introspection.py
  27. +5 −0 django/db/backends/postgresql_psycopg2/schema.py
  28. +729 −0 django/db/backends/schema.py
  29. +7 −0 django/db/backends/sqlite3/base.py
  30. +43 −2 django/db/backends/sqlite3/introspection.py
  31. +155 −0 django/db/backends/sqlite3/schema.py
  32. +2 −0 django/db/migrations/__init__.py
  33. +440 −0 django/db/migrations/autodetector.py
  34. +90 −0 django/db/migrations/executor.py
  35. +152 −0 django/db/migrations/graph.py
  36. +167 −0 django/db/migrations/loader.py
  37. +101 −0 django/db/migrations/migration.py
  38. +2 −0 django/db/migrations/operations/__init__.py
  39. +62 −0 django/db/migrations/operations/base.py
  40. +132 −0 django/db/migrations/operations/fields.py
  41. +157 −0 django/db/migrations/operations/models.py
  42. +69 −0 django/db/migrations/recorder.py
  43. +142 −0 django/db/migrations/state.py
  44. +180 −0 django/db/migrations/writer.py
  45. +4 −4 django/db/models/base.py
  46. +23 −3 django/db/models/fields/__init__.py
  47. +18 −6 django/db/models/fields/related.py
  48. +53 −10 django/db/models/loading.py
  49. +13 −5 django/db/models/options.py
  50. +4 −2 django/db/models/signals.py
  51. +5 −2 django/db/utils.py
  52. +5 −6 django/test/testcases.py
  53. +31 −0 django/utils/datastructures.py
  54. +5 −0 django/utils/functional.py
  55. +12 −0 django/utils/termcolors.py
  56. +2 −2 docs/howto/legacy-databases.txt
  57. +3 −0 docs/index.txt
  58. +2 −2 docs/internals/contributing/writing-documentation.txt
  59. +11 −0 docs/internals/deprecation.txt
  60. +4 −3 docs/intro/overview.txt
  61. +1 −1 docs/intro/reusable-apps.txt
  62. +4 −5 docs/man/django-admin.1
  63. +1 −1 docs/ref/contrib/comments/index.txt
  64. +1 −1 docs/ref/contrib/contenttypes.txt
  65. +2 −2 docs/ref/contrib/flatpages.txt
  66. +1 −1 docs/ref/contrib/index.txt
  67. +2 −2 docs/ref/contrib/redirects.txt
  68. +2 −2 docs/ref/contrib/sites.txt
  69. +3 −3 docs/ref/databases.txt
  70. +58 −46 docs/ref/django-admin.txt
  71. +6 −6 docs/ref/models/options.txt
  72. +54 −28 docs/ref/signals.txt
  73. +74 −0 docs/releases/1.7.txt
  74. +6 −8 docs/topics/auth/customizing.txt
  75. +5 −5 docs/topics/auth/default.txt
  76. +1 −1 docs/topics/auth/index.txt
  77. +2 −2 docs/topics/cache.txt
  78. +4 −3 docs/topics/db/models.txt
  79. +15 −6 docs/topics/db/multi-db.txt
  80. +1 −1 docs/topics/http/sessions.txt
  81. +1 −0 docs/topics/index.txt
  82. +2 −4 docs/topics/install.txt
  83. +297 −0 docs/topics/migrations.txt
  84. +1 −1 docs/topics/serialization.txt
  85. +7 −7 docs/topics/testing/advanced.txt
  86. +2 −2 docs/topics/testing/overview.txt
  87. 0 tests/app_cache/__init__.py
  88. +17 −0 tests/app_cache/models.py
  89. +44 −0 tests/app_cache/tests.py
  90. +1 −1 tests/cache/tests.py
  91. 0 tests/migrations/__init__.py
  92. 0 tests/migrations/models.py
  93. +274 −0 tests/migrations/test_autodetector.py
  94. +41 −0 tests/migrations/test_base.py
  95. +37 −0 tests/migrations/test_commands.py
  96. +77 −0 tests/migrations/test_executor.py
  97. +135 −0 tests/migrations/test_graph.py
  98. +79 −0 tests/migrations/test_loader.py
  99. +27 −0 tests/migrations/test_migrations/0001_initial.py
  100. +24 −0 tests/migrations/test_migrations/0002_second.py
  101. 0 tests/migrations/test_migrations/__init__.py
  102. +21 −0 tests/migrations/test_migrations_2/0001_initial.py
  103. 0 tests/migrations/test_migrations_2/__init__.py
  104. +327 −0 tests/migrations/test_operations.py
  105. +77 −0 tests/migrations/test_state.py
  106. +84 −0 tests/migrations/test_writer.py
  107. +23 −23 tests/multiple_database/tests.py
  108. 0 tests/schema/__init__.py
  109. +97 −0 tests/schema/models.py
  110. +650 −0 tests/schema/tests.py
  111. +12 −0 tests/utils_tests/test_functional.py
@@ -609,3 +609,10 @@
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
)
+
+##############
+# MIGRATIONS #
+##############
+
+# Migration module overrides for apps, by app label.
+MIGRATION_MODULES = {}
@@ -65,7 +65,7 @@ def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kw
except UnavailableApp:
return
- if not router.allow_syncdb(db, auth_app.Permission):
+ if not router.allow_migrate(db, auth_app.Permission):
return
from django.contrib.contenttypes.models import ContentType
@@ -188,7 +188,7 @@ def get_default_username(check_db=True):
return ''
return default_username
-signals.post_syncdb.connect(create_permissions,
+signals.post_migrate.connect(create_permissions,
dispatch_uid="django.contrib.auth.management.create_permissions")
-signals.post_syncdb.connect(create_superuser,
+signals.post_migrate.connect(create_superuser,
sender=auth_app, dispatch_uid="django.contrib.auth.management.create_superuser")
@@ -16,7 +16,7 @@ def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, *
except UnavailableApp:
return
- if not router.allow_syncdb(db, ContentType):
+ if not router.allow_migrate(db, ContentType):
return
ContentType.objects.clear_cache()
@@ -88,7 +88,7 @@ def update_all_contenttypes(verbosity=2, **kwargs):
for app in get_apps():
update_contenttypes(app, None, verbosity, **kwargs)
-signals.post_syncdb.connect(update_contenttypes)
+signals.post_migrate.connect(update_contenttypes)
if __name__ == "__main__":
update_all_contenttypes()
@@ -47,7 +47,7 @@ def create_test_db(self, verbosity=1, autoclobber=False):
# We need to then do a flush to ensure that any data installed by
# custom SQL has been removed. The only test data should come from
- # test fixtures, or autogenerated from post_syncdb triggers.
+ # test fixtures, or autogenerated from post_migrate triggers.
# This has the side effect of loading initial data (which was
# intentionally skipped in the syncdb).
call_command('flush',
@@ -311,7 +311,7 @@ def db_for_write(self, model, **hints):
def allow_relation(self, obj1, obj2, **hints):
return None
- def allow_syncdb(self, db, model):
+ def allow_migrate(self, db, model):
return True
@@ -11,7 +11,7 @@
def create_default_site(app, created_models, verbosity, db, **kwargs):
# Only create the default sites in databases where Django created the table
- if Site in created_models and router.allow_syncdb(db, Site) :
+ if Site in created_models and router.allow_migrate(db, Site) :
# The default settings set SITE_ID = 1, and some tests in Django's test
# suite rely on this value. However, if database sequences are reused
# (e.g. in the test suite after flush/syncdb), it isn't guaranteed that
@@ -33,4 +33,4 @@ def create_default_site(app, created_models, verbosity, db, **kwargs):
Site.objects.clear_cache()
-signals.post_syncdb.connect(create_default_site, sender=site_app)
+signals.post_migrate.connect(create_default_site, sender=site_app)
@@ -24,7 +24,7 @@ class Command(LabelCommand):
def handle_label(self, tablename, **options):
db = options.get('database')
cache = BaseDatabaseCache(tablename, {})
- if not router.allow_syncdb(db, cache.cache_model_class):
+ if not router.allow_migrate(db, cache.cache_model_class):
return
connection = connections[db]
fields = (
@@ -118,7 +118,7 @@ def get_objects():
for model in sort_dependencies(app_list.items()):
if model in excluded_models:
continue
- if not model._meta.proxy and router.allow_syncdb(using, model):
+ if not model._meta.proxy and router.allow_migrate(using, model):
if use_base_manager:
objects = model._base_manager
else:
@@ -7,7 +7,7 @@
from django.core.management import call_command
from django.core.management.base import NoArgsCommand, CommandError
from django.core.management.color import no_style
-from django.core.management.sql import sql_flush, emit_post_sync_signal
+from django.core.management.sql import sql_flush, emit_post_migrate_signal
from django.utils.six.moves import input
from django.utils import six
@@ -23,8 +23,8 @@ class Command(NoArgsCommand):
help='Tells Django not to load any initial data after database synchronization.'),
)
help = ('Returns the database to the state it was in immediately after '
- 'syncdb was executed. This means that all data will be removed '
- 'from the database, any post-synchronization handlers will be '
+ 'migrate was first executed. This means that all data will be removed '
+ 'from the database, any post-migration handlers will be '
're-executed, and the initial_data fixture will be re-installed.')
def handle_noargs(self, **options):
@@ -35,7 +35,7 @@ def handle_noargs(self, **options):
# The following are stealth options used by Django's internals.
reset_sequences = options.get('reset_sequences', True)
allow_cascade = options.get('allow_cascade', False)
- inhibit_post_syncdb = options.get('inhibit_post_syncdb', False)
+ inhibit_post_migrate = options.get('inhibit_post_migrate', False)
self.style = no_style()
@@ -54,7 +54,7 @@ def handle_noargs(self, **options):
if interactive:
confirm = input("""You have requested a flush of the database.
This will IRREVERSIBLY DESTROY all data currently in the %r database,
-and return each table to the state it was in after syncdb.
+and return each table to a fresh state.
Are you sure you want to do this?
Type 'yes' to continue, or 'no' to cancel: """ % connection.settings_dict['NAME'])
@@ -77,8 +77,8 @@ def handle_noargs(self, **options):
"The full error: %s") % (connection.settings_dict['NAME'], e)
six.reraise(CommandError, CommandError(new_msg), sys.exc_info()[2])
- if not inhibit_post_syncdb:
- self.emit_post_syncdb(verbosity, interactive, db)
+ if not inhibit_post_migrate:
+ self.emit_post_migrate(verbosity, interactive, db)
# Reinstall the initial_data fixture.
if options.get('load_initial_data'):
@@ -89,13 +89,13 @@ def handle_noargs(self, **options):
self.stdout.write("Flush cancelled.\n")
@staticmethod
- def emit_post_syncdb(verbosity, interactive, database):
- # Emit the post sync signal. This allows individual applications to
- # respond as if the database had been sync'd from scratch.
+ def emit_post_migrate(verbosity, interactive, database):
+ # Emit the post migrate signal. This allows individual applications to
+ # respond as if the database had been migrated from scratch.
all_models = []
for app in models.get_apps():
all_models.extend([
m for m in models.get_models(app, include_auto_created=True)
- if router.allow_syncdb(database, m)
+ if router.allow_migrate(database, m)
])
- emit_post_sync_signal(set(all_models), verbosity, interactive, database)
+ emit_post_migrate_signal(set(all_models), verbosity, interactive, database)
@@ -134,7 +134,7 @@ def load_label(self, fixture_label):
for obj in objects:
objects_in_fixture += 1
- if router.allow_syncdb(self.using, obj.object.__class__):
+ if router.allow_migrate(self.using, obj.object.__class__):
loaded_objects_in_fixture += 1
self.models.add(obj.object.__class__)
try:
@@ -0,0 +1,84 @@
+import sys
+import os
+from optparse import make_option
+
+from django.core.management.base import BaseCommand
+from django.core.exceptions import ImproperlyConfigured
+from django.db import connections, DEFAULT_DB_ALIAS
+from django.db.migrations.loader import MigrationLoader
+from django.db.migrations.autodetector import MigrationAutodetector, InteractiveMigrationQuestioner
+from django.db.migrations.state import ProjectState
+from django.db.migrations.writer import MigrationWriter
+from django.db.models.loading import cache
+
+
+class Command(BaseCommand):
+ option_list = BaseCommand.option_list + (
+ make_option('--empty', action='store_true', dest='empty', default=False,
+ help='Make a blank migration.'),
+ )
+
+ help = "Creates new migration(s) for apps."
+ usage_str = "Usage: ./manage.py makemigrations [--empty] [app [app ...]]"
+
+ def handle(self, *app_labels, **options):
+
+ self.verbosity = int(options.get('verbosity'))
+ self.interactive = options.get('interactive')
+
+ # Make sure the app they asked for exists
+ app_labels = set(app_labels)
+ bad_app_labels = set()
+ for app_label in app_labels:
+ try:
+ cache.get_app(app_label)
+ except ImproperlyConfigured:
+ bad_app_labels.add(app_label)
+ if bad_app_labels:
+ for app_label in bad_app_labels:
+ self.stderr.write("App '%s' could not be found. Is it in INSTALLED_APPS?" % app_label)
+ sys.exit(2)
+
+ # Load the current graph state. Takes a connection, but it's not used
+ # (makemigrations doesn't look at the database state).
+ loader = MigrationLoader(connections[DEFAULT_DB_ALIAS])
+
+ # Detect changes
+ autodetector = MigrationAutodetector(
+ loader.graph.project_state(),
+ ProjectState.from_app_cache(cache),
+ InteractiveMigrationQuestioner(specified_apps=app_labels),
+ )
+ changes = autodetector.changes(graph=loader.graph, trim_to_apps=app_labels or None)
+
+ # No changes? Tell them.
+ if not changes:
+ if len(app_labels) == 1:
+ self.stdout.write("No changes detected in app '%s'" % app_labels.pop())
+ elif len(app_labels) > 1:
+ self.stdout.write("No changes detected in apps '%s'" % ("', '".join(app_labels)))
+ else:
+ self.stdout.write("No changes detected")
+ return
+
+ directory_created = {}
+ for app_label, migrations in changes.items():
+ self.stdout.write(self.style.MIGRATE_HEADING("Migrations for '%s':" % app_label) + "\n")
+ for migration in migrations:
+ # Describe the migration
+ writer = MigrationWriter(migration)
+ self.stdout.write(" %s:\n" % (self.style.MIGRATE_LABEL(writer.filename),))
+ for operation in migration.operations:
+ self.stdout.write(" - %s\n" % operation.describe())
+ # Write it
+ migrations_directory = os.path.dirname(writer.path)
+ if not directory_created.get(app_label, False):
+ if not os.path.isdir(migrations_directory):
+ os.mkdir(migrations_directory)
+ init_path = os.path.join(migrations_directory, "__init__.py")
+ if not os.path.isfile(init_path):
+ open(init_path, "w").close()
+ # We just do this once per app
+ directory_created[app_label] = True
+ with open(writer.path, "w") as fh:
+ fh.write(writer.as_string())
Oops, something went wrong.

0 comments on commit 9aa358c

Please sign in to comment.