Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Merge pull request #376 from andrewgodwin/schema-alteration

Schema alteration
  • Loading branch information...
commit 9aa358cedd1ad93c0f4c20700db7016651dc0598 2 parents 57c82f9 + 5569b0b
Marc Tamlyn authored

Showing 111 changed files with 6,334 additions and 403 deletions. Show diff stats Hide diff stats

  1. 7  django/conf/global_settings.py
  2. 6  django/contrib/auth/management/__init__.py
  3. 4  django/contrib/contenttypes/management.py
  4. 2  django/contrib/gis/db/backends/spatialite/creation.py
  5. 2  django/contrib/gis/tests/layermap/tests.py
  6. 4  django/contrib/sites/management.py
  7. 2  django/core/management/commands/createcachetable.py
  8. 2  django/core/management/commands/dumpdata.py
  9. 24  django/core/management/commands/flush.py
  10. 2  django/core/management/commands/loaddata.py
  11. 84  django/core/management/commands/makemigrations.py
  12. 245  django/core/management/commands/migrate.py
  13. 150  django/core/management/commands/syncdb.py
  14. 16  django/core/management/sql.py
  15. 50  django/db/backends/__init__.py
  16. 13  django/db/backends/creation.py
  17. 8  django/db/backends/mysql/base.py
  18. 70  django/db/backends/mysql/introspection.py
  19. 26  django/db/backends/mysql/schema.py
  20. 10  django/db/backends/oracle/base.py
  21. 15  django/db/backends/oracle/creation.py
  22. 140  django/db/backends/oracle/introspection.py
  23. 103  django/db/backends/oracle/schema.py
  24. 7  django/db/backends/postgresql_psycopg2/base.py
  25. 9  django/db/backends/postgresql_psycopg2/creation.py
  26. 97  django/db/backends/postgresql_psycopg2/introspection.py
  27. 5  django/db/backends/postgresql_psycopg2/schema.py
  28. 729  django/db/backends/schema.py
  29. 7  django/db/backends/sqlite3/base.py
  30. 45  django/db/backends/sqlite3/introspection.py
  31. 155  django/db/backends/sqlite3/schema.py
  32. 2  django/db/migrations/__init__.py
  33. 440  django/db/migrations/autodetector.py
  34. 90  django/db/migrations/executor.py
  35. 152  django/db/migrations/graph.py
  36. 167  django/db/migrations/loader.py
  37. 101  django/db/migrations/migration.py
  38. 2  django/db/migrations/operations/__init__.py
  39. 62  django/db/migrations/operations/base.py
  40. 132  django/db/migrations/operations/fields.py
  41. 157  django/db/migrations/operations/models.py
  42. 69  django/db/migrations/recorder.py
  43. 142  django/db/migrations/state.py
  44. 180  django/db/migrations/writer.py
  45. 8  django/db/models/base.py
  46. 26  django/db/models/fields/__init__.py
  47. 24  django/db/models/fields/related.py
  48. 63  django/db/models/loading.py
  49. 18  django/db/models/options.py
  50. 6  django/db/models/signals.py
  51. 7  django/db/utils.py
  52. 11  django/test/testcases.py
  53. 31  django/utils/datastructures.py
  54. 5  django/utils/functional.py
  55. 12  django/utils/termcolors.py
  56. 4  docs/howto/legacy-databases.txt
  57. 3  docs/index.txt
  58. 4  docs/internals/contributing/writing-documentation.txt
  59. 11  docs/internals/deprecation.txt
  60. 7  docs/intro/overview.txt
  61. 2  docs/intro/reusable-apps.txt
  62. 9  docs/man/django-admin.1
  63. 2  docs/ref/contrib/comments/index.txt
  64. 2  docs/ref/contrib/contenttypes.txt
  65. 4  docs/ref/contrib/flatpages.txt
  66. 2  docs/ref/contrib/index.txt
  67. 4  docs/ref/contrib/redirects.txt
  68. 4  docs/ref/contrib/sites.txt
  69. 6  docs/ref/databases.txt
  70. 104  docs/ref/django-admin.txt
  71. 12  docs/ref/models/options.txt
  72. 82  docs/ref/signals.txt
  73. 74  docs/releases/1.7.txt
  74. 14  docs/topics/auth/customizing.txt
  75. 10  docs/topics/auth/default.txt
  76. 2  docs/topics/auth/index.txt
  77. 4  docs/topics/cache.txt
  78. 7  docs/topics/db/models.txt
  79. 21  docs/topics/db/multi-db.txt
  80. 2  docs/topics/http/sessions.txt
  81. 1  docs/topics/index.txt
  82. 6  docs/topics/install.txt
  83. 297  docs/topics/migrations.txt
  84. 2  docs/topics/serialization.txt
  85. 14  docs/topics/testing/advanced.txt
  86. 4  docs/topics/testing/overview.txt
  87. 0  tests/app_cache/__init__.py
  88. 17  tests/app_cache/models.py
  89. 44  tests/app_cache/tests.py
  90. 2  tests/cache/tests.py
  91. 0  tests/migrations/__init__.py
  92. 0  tests/migrations/models.py
  93. 274  tests/migrations/test_autodetector.py
  94. 41  tests/migrations/test_base.py
  95. 37  tests/migrations/test_commands.py
  96. 77  tests/migrations/test_executor.py
  97. 135  tests/migrations/test_graph.py
  98. 79  tests/migrations/test_loader.py
  99. 27  tests/migrations/test_migrations/0001_initial.py
  100. 24  tests/migrations/test_migrations/0002_second.py
  101. 0  tests/migrations/test_migrations/__init__.py
  102. 21  tests/migrations/test_migrations_2/0001_initial.py
  103. 0  tests/migrations/test_migrations_2/__init__.py
  104. 327  tests/migrations/test_operations.py
  105. 77  tests/migrations/test_state.py
  106. 84  tests/migrations/test_writer.py
  107. 46  tests/multiple_database/tests.py
  108. 0  tests/schema/__init__.py
  109. 97  tests/schema/models.py
  110. 650  tests/schema/tests.py
  111. 12  tests/utils_tests/test_functional.py
7  django/conf/global_settings.py
@@ -609,3 +609,10 @@
609 609
     'django.contrib.staticfiles.finders.AppDirectoriesFinder',
610 610
 #    'django.contrib.staticfiles.finders.DefaultStorageFinder',
611 611
 )
  612
+
  613
+##############
  614
+# MIGRATIONS #
  615
+##############
  616
+
  617
+# Migration module overrides for apps, by app label.
  618
+MIGRATION_MODULES = {}
6  django/contrib/auth/management/__init__.py
@@ -65,7 +65,7 @@ def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kw
65 65
     except UnavailableApp:
66 66
         return
67 67
 
68  
-    if not router.allow_syncdb(db, auth_app.Permission):
  68
+    if not router.allow_migrate(db, auth_app.Permission):
69 69
         return
70 70
 
71 71
     from django.contrib.contenttypes.models import ContentType
@@ -188,7 +188,7 @@ def get_default_username(check_db=True):
188 188
             return ''
189 189
     return default_username
190 190
 
191  
-signals.post_syncdb.connect(create_permissions,
  191
+signals.post_migrate.connect(create_permissions,
192 192
     dispatch_uid="django.contrib.auth.management.create_permissions")
193  
-signals.post_syncdb.connect(create_superuser,
  193
+signals.post_migrate.connect(create_superuser,
194 194
     sender=auth_app, dispatch_uid="django.contrib.auth.management.create_superuser")
4  django/contrib/contenttypes/management.py
@@ -16,7 +16,7 @@ def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, *
16 16
     except UnavailableApp:
17 17
         return
18 18
 
19  
-    if not router.allow_syncdb(db, ContentType):
  19
+    if not router.allow_migrate(db, ContentType):
20 20
         return
21 21
 
22 22
     ContentType.objects.clear_cache()
@@ -88,7 +88,7 @@ def update_all_contenttypes(verbosity=2, **kwargs):
88 88
     for app in get_apps():
89 89
         update_contenttypes(app, None, verbosity, **kwargs)
90 90
 
91  
-signals.post_syncdb.connect(update_contenttypes)
  91
+signals.post_migrate.connect(update_contenttypes)
92 92
 
93 93
 if __name__ == "__main__":
94 94
     update_all_contenttypes()
2  django/contrib/gis/db/backends/spatialite/creation.py
@@ -47,7 +47,7 @@ def create_test_db(self, verbosity=1, autoclobber=False):
47 47
 
48 48
         # We need to then do a flush to ensure that any data installed by
49 49
         # custom SQL has been removed. The only test data should come from
50  
-        # test fixtures, or autogenerated from post_syncdb triggers.
  50
+        # test fixtures, or autogenerated from post_migrate triggers.
51 51
         # This has the side effect of loading initial data (which was
52 52
         # intentionally skipped in the syncdb).
53 53
         call_command('flush',
2  django/contrib/gis/tests/layermap/tests.py
@@ -311,7 +311,7 @@ def db_for_write(self, model, **hints):
311 311
     def allow_relation(self, obj1, obj2, **hints):
312 312
         return None
313 313
 
314  
-    def allow_syncdb(self, db, model):
  314
+    def allow_migrate(self, db, model):
315 315
         return True
316 316
 
317 317
 
4  django/contrib/sites/management.py
@@ -11,7 +11,7 @@
11 11
 
12 12
 def create_default_site(app, created_models, verbosity, db, **kwargs):
13 13
     # Only create the default sites in databases where Django created the table
14  
-    if Site in created_models and router.allow_syncdb(db, Site) :
  14
+    if Site in created_models and router.allow_migrate(db, Site) :
15 15
         # The default settings set SITE_ID = 1, and some tests in Django's test
16 16
         # suite rely on this value. However, if database sequences are reused
17 17
         # (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):
33 33
 
34 34
     Site.objects.clear_cache()
35 35
 
36  
-signals.post_syncdb.connect(create_default_site, sender=site_app)
  36
+signals.post_migrate.connect(create_default_site, sender=site_app)
2  django/core/management/commands/createcachetable.py
@@ -24,7 +24,7 @@ class Command(LabelCommand):
24 24
     def handle_label(self, tablename, **options):
25 25
         db = options.get('database')
26 26
         cache = BaseDatabaseCache(tablename, {})
27  
-        if not router.allow_syncdb(db, cache.cache_model_class):
  27
+        if not router.allow_migrate(db, cache.cache_model_class):
28 28
             return
29 29
         connection = connections[db]
30 30
         fields = (
2  django/core/management/commands/dumpdata.py
@@ -118,7 +118,7 @@ def get_objects():
118 118
             for model in sort_dependencies(app_list.items()):
119 119
                 if model in excluded_models:
120 120
                     continue
121  
-                if not model._meta.proxy and router.allow_syncdb(using, model):
  121
+                if not model._meta.proxy and router.allow_migrate(using, model):
122 122
                     if use_base_manager:
123 123
                         objects = model._base_manager
124 124
                     else:
24  django/core/management/commands/flush.py
@@ -7,7 +7,7 @@
7 7
 from django.core.management import call_command
8 8
 from django.core.management.base import NoArgsCommand, CommandError
9 9
 from django.core.management.color import no_style
10  
-from django.core.management.sql import sql_flush, emit_post_sync_signal
  10
+from django.core.management.sql import sql_flush, emit_post_migrate_signal
11 11
 from django.utils.six.moves import input
12 12
 from django.utils import six
13 13
 
@@ -23,8 +23,8 @@ class Command(NoArgsCommand):
23 23
             help='Tells Django not to load any initial data after database synchronization.'),
24 24
     )
25 25
     help = ('Returns the database to the state it was in immediately after '
26  
-           'syncdb was executed. This means that all data will be removed '
27  
-           'from the database, any post-synchronization handlers will be '
  26
+           'migrate was first executed. This means that all data will be removed '
  27
+           'from the database, any post-migration handlers will be '
28 28
            're-executed, and the initial_data fixture will be re-installed.')
29 29
 
30 30
     def handle_noargs(self, **options):
@@ -35,7 +35,7 @@ def handle_noargs(self, **options):
35 35
         # The following are stealth options used by Django's internals.
36 36
         reset_sequences = options.get('reset_sequences', True)
37 37
         allow_cascade = options.get('allow_cascade', False)
38  
-        inhibit_post_syncdb = options.get('inhibit_post_syncdb', False)
  38
+        inhibit_post_migrate = options.get('inhibit_post_migrate', False)
39 39
 
40 40
         self.style = no_style()
41 41
 
@@ -54,7 +54,7 @@ def handle_noargs(self, **options):
54 54
         if interactive:
55 55
             confirm = input("""You have requested a flush of the database.
56 56
 This will IRREVERSIBLY DESTROY all data currently in the %r database,
57  
-and return each table to the state it was in after syncdb.
  57
+and return each table to a fresh state.
58 58
 Are you sure you want to do this?
59 59
 
60 60
     Type 'yes' to continue, or 'no' to cancel: """ % connection.settings_dict['NAME'])
@@ -77,8 +77,8 @@ def handle_noargs(self, **options):
77 77
                     "The full error: %s") % (connection.settings_dict['NAME'], e)
78 78
                 six.reraise(CommandError, CommandError(new_msg), sys.exc_info()[2])
79 79
 
80  
-            if not inhibit_post_syncdb:
81  
-                self.emit_post_syncdb(verbosity, interactive, db)
  80
+            if not inhibit_post_migrate:
  81
+                self.emit_post_migrate(verbosity, interactive, db)
82 82
 
83 83
             # Reinstall the initial_data fixture.
84 84
             if options.get('load_initial_data'):
@@ -89,13 +89,13 @@ def handle_noargs(self, **options):
89 89
             self.stdout.write("Flush cancelled.\n")
90 90
 
91 91
     @staticmethod
92  
-    def emit_post_syncdb(verbosity, interactive, database):
93  
-        # Emit the post sync signal. This allows individual applications to
94  
-        # respond as if the database had been sync'd from scratch.
  92
+    def emit_post_migrate(verbosity, interactive, database):
  93
+        # Emit the post migrate signal. This allows individual applications to
  94
+        # respond as if the database had been migrated from scratch.
95 95
         all_models = []
96 96
         for app in models.get_apps():
97 97
             all_models.extend([
98 98
                 m for m in models.get_models(app, include_auto_created=True)
99  
-                if router.allow_syncdb(database, m)
  99
+                if router.allow_migrate(database, m)
100 100
             ])
101  
-        emit_post_sync_signal(set(all_models), verbosity, interactive, database)
  101
+        emit_post_migrate_signal(set(all_models), verbosity, interactive, database)
2  django/core/management/commands/loaddata.py
@@ -134,7 +134,7 @@ def load_label(self, fixture_label):
134 134
 
135 135
                 for obj in objects:
136 136
                     objects_in_fixture += 1
137  
-                    if router.allow_syncdb(self.using, obj.object.__class__):
  137
+                    if router.allow_migrate(self.using, obj.object.__class__):
138 138
                         loaded_objects_in_fixture += 1
139 139
                         self.models.add(obj.object.__class__)
140 140
                         try:
84  django/core/management/commands/makemigrations.py
... ...
@@ -0,0 +1,84 @@
  1
+import sys
  2
+import os
  3
+from optparse import make_option
  4
+
  5
+from django.core.management.base import BaseCommand
  6
+from django.core.exceptions import ImproperlyConfigured
  7
+from django.db import connections, DEFAULT_DB_ALIAS
  8
+from django.db.migrations.loader import MigrationLoader
  9
+from django.db.migrations.autodetector import MigrationAutodetector, InteractiveMigrationQuestioner
  10
+from django.db.migrations.state import ProjectState
  11
+from django.db.migrations.writer import MigrationWriter
  12
+from django.db.models.loading import cache
  13
+
  14
+
  15
+class Command(BaseCommand):
  16
+    option_list = BaseCommand.option_list + (
  17
+        make_option('--empty', action='store_true', dest='empty', default=False,
  18
+            help='Make a blank migration.'),
  19
+    )
  20
+
  21
+    help = "Creates new migration(s) for apps."
  22
+    usage_str = "Usage: ./manage.py makemigrations [--empty] [app [app ...]]"
  23
+
  24
+    def handle(self, *app_labels, **options):
  25
+
  26
+        self.verbosity = int(options.get('verbosity'))
  27
+        self.interactive = options.get('interactive')
  28
+
  29
+        # Make sure the app they asked for exists
  30
+        app_labels = set(app_labels)
  31
+        bad_app_labels = set()
  32
+        for app_label in app_labels:
  33
+            try:
  34
+                cache.get_app(app_label)
  35
+            except ImproperlyConfigured:
  36
+                bad_app_labels.add(app_label)
  37
+        if bad_app_labels:
  38
+            for app_label in bad_app_labels:
  39
+                self.stderr.write("App '%s' could not be found. Is it in INSTALLED_APPS?" % app_label)
  40
+            sys.exit(2)
  41
+
  42
+        # Load the current graph state. Takes a connection, but it's not used
  43
+        # (makemigrations doesn't look at the database state).
  44
+        loader = MigrationLoader(connections[DEFAULT_DB_ALIAS])
  45
+
  46
+        # Detect changes
  47
+        autodetector = MigrationAutodetector(
  48
+            loader.graph.project_state(),
  49
+            ProjectState.from_app_cache(cache),
  50
+            InteractiveMigrationQuestioner(specified_apps=app_labels),
  51
+        )
  52
+        changes = autodetector.changes(graph=loader.graph, trim_to_apps=app_labels or None)
  53
+
  54
+        # No changes? Tell them.
  55
+        if not changes:
  56
+            if len(app_labels) == 1:
  57
+                self.stdout.write("No changes detected in app '%s'" % app_labels.pop())
  58
+            elif len(app_labels) > 1:
  59
+                self.stdout.write("No changes detected in apps '%s'" % ("', '".join(app_labels)))
  60
+            else:
  61
+                self.stdout.write("No changes detected")
  62
+            return
  63
+
  64
+        directory_created = {}
  65
+        for app_label, migrations in changes.items():
  66
+            self.stdout.write(self.style.MIGRATE_HEADING("Migrations for '%s':" % app_label) + "\n")
  67
+            for migration in migrations:
  68
+                # Describe the migration
  69
+                writer = MigrationWriter(migration)
  70
+                self.stdout.write("  %s:\n" % (self.style.MIGRATE_LABEL(writer.filename),))
  71
+                for operation in migration.operations:
  72
+                    self.stdout.write("    - %s\n" % operation.describe())
  73
+                # Write it
  74
+                migrations_directory = os.path.dirname(writer.path)
  75
+                if not directory_created.get(app_label, False):
  76
+                    if not os.path.isdir(migrations_directory):
  77
+                        os.mkdir(migrations_directory)
  78
+                    init_path = os.path.join(migrations_directory, "__init__.py")
  79
+                    if not os.path.isfile(init_path):
  80
+                        open(init_path, "w").close()
  81
+                    # We just do this once per app
  82
+                    directory_created[app_label] = True
  83
+                with open(writer.path, "w") as fh:
  84
+                    fh.write(writer.as_string())
245  django/core/management/commands/migrate.py
... ...
@@ -0,0 +1,245 @@
  1
+from optparse import make_option
  2
+from collections import OrderedDict
  3
+from importlib import import_module
  4
+import itertools
  5
+import traceback
  6
+
  7
+from django.conf import settings
  8
+from django.core.management import call_command
  9
+from django.core.management.base import BaseCommand, CommandError
  10
+from django.core.management.color import no_style
  11
+from django.core.management.sql import custom_sql_for_model, emit_post_migrate_signal, emit_pre_migrate_signal
  12
+from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS
  13
+from django.db.migrations.executor import MigrationExecutor
  14
+from django.db.migrations.loader import AmbiguityError
  15
+from django.utils.module_loading import module_has_submodule
  16
+
  17
+
  18
+class Command(BaseCommand):
  19
+    option_list = BaseCommand.option_list + (
  20
+        make_option('--noinput', action='store_false', dest='interactive', default=True,
  21
+            help='Tells Django to NOT prompt the user for input of any kind.'),
  22
+        make_option('--no-initial-data', action='store_false', dest='load_initial_data', default=True,
  23
+            help='Tells Django not to load any initial data after database synchronization.'),
  24
+        make_option('--database', action='store', dest='database',
  25
+            default=DEFAULT_DB_ALIAS, help='Nominates a database to synchronize. '
  26
+                'Defaults to the "default" database.'),
  27
+        make_option('--fake', action='store_true', dest='fake', default=False,
  28
+            help='Mark migrations as run without actually running them'),
  29
+    )
  30
+
  31
+    help = "Updates database schema. Manages both apps with migrations and those without."
  32
+
  33
+    def handle(self, *args, **options):
  34
+
  35
+        self.verbosity = int(options.get('verbosity'))
  36
+        self.interactive = options.get('interactive')
  37
+        self.show_traceback = options.get('traceback')
  38
+        self.load_initial_data = options.get('load_initial_data')
  39
+        self.test_database = options.get('test_database', False)
  40
+
  41
+        # Import the 'management' module within each installed app, to register
  42
+        # dispatcher events.
  43
+        for app_name in settings.INSTALLED_APPS:
  44
+            if module_has_submodule(import_module(app_name), "management"):
  45
+                import_module('.management', app_name)
  46
+
  47
+        # Get the database we're operating from
  48
+        db = options.get('database')
  49
+        connection = connections[db]
  50
+
  51
+        # Work out which apps have migrations and which do not
  52
+        executor = MigrationExecutor(connection, self.migration_progress_callback)
  53
+
  54
+        # If they supplied command line arguments, work out what they mean.
  55
+        run_syncdb = False
  56
+        target_app_labels_only = True
  57
+        if len(args) > 2:
  58
+            raise CommandError("Too many command-line arguments (expecting 'appname' or 'appname migrationname')")
  59
+        elif len(args) == 2:
  60
+            app_label, migration_name = args
  61
+            if app_label not in executor.loader.migrated_apps:
  62
+                raise CommandError("App '%s' does not have migrations (you cannot selectively sync unmigrated apps)" % app_label)
  63
+            if migration_name == "zero":
  64
+                targets = [(app_label, None)]
  65
+            else:
  66
+                try:
  67
+                    migration = executor.loader.get_migration_by_prefix(app_label, migration_name)
  68
+                except AmbiguityError:
  69
+                    raise CommandError("More than one migration matches '%s' in app '%s'. Please be more specific." % (app_label, migration_name))
  70
+                except KeyError:
  71
+                    raise CommandError("Cannot find a migration matching '%s' from app '%s'. Is it in INSTALLED_APPS?" % (app_label, migration_name))
  72
+                targets = [(app_label, migration.name)]
  73
+            target_app_labels_only = False
  74
+        elif len(args) == 1:
  75
+            app_label = args[0]
  76
+            if app_label not in executor.loader.migrated_apps:
  77
+                raise CommandError("App '%s' does not have migrations (you cannot selectively sync unmigrated apps)" % app_label)
  78
+            targets = [key for key in executor.loader.graph.leaf_nodes() if key[0] == app_label]
  79
+        else:
  80
+            targets = executor.loader.graph.leaf_nodes()
  81
+            run_syncdb = True
  82
+
  83
+        plan = executor.migration_plan(targets)
  84
+
  85
+        # Print some useful info
  86
+        if self.verbosity >= 1:
  87
+            self.stdout.write(self.style.MIGRATE_HEADING("Operations to perform:"))
  88
+            if run_syncdb:
  89
+                self.stdout.write(self.style.MIGRATE_LABEL("  Synchronize unmigrated apps: ") + (", ".join(executor.loader.unmigrated_apps) or "(none)"))
  90
+            if target_app_labels_only:
  91
+                self.stdout.write(self.style.MIGRATE_LABEL("  Apply all migrations: ") + (", ".join(set(a for a, n in targets)) or "(none)"))
  92
+            else:
  93
+                if targets[0][1] is None:
  94
+                    self.stdout.write(self.style.MIGRATE_LABEL("  Unapply all migrations: ") + "%s" % (targets[0][0], ))
  95
+                else:
  96
+                    self.stdout.write(self.style.MIGRATE_LABEL("  Target specific migration: ") + "%s, from %s" % (targets[0][1], targets[0][0]))
  97
+
  98
+        # Run the syncdb phase.
  99
+        # If you ever manage to get rid of this, I owe you many, many drinks.
  100
+        # Note that pre_migrate is called from inside here, as it needs
  101
+        # the list of models about to be installed.
  102
+        if run_syncdb:
  103
+            if self.verbosity >= 1:
  104
+                self.stdout.write(self.style.MIGRATE_HEADING("Synchronizing apps without migrations:"))
  105
+            created_models = self.sync_apps(connection, executor.loader.unmigrated_apps)
  106
+        else:
  107
+            created_models = []
  108
+
  109
+        # Migrate!
  110
+        if self.verbosity >= 1:
  111
+            self.stdout.write(self.style.MIGRATE_HEADING("Running migrations:"))
  112
+        if not plan:
  113
+            if self.verbosity >= 1:
  114
+                self.stdout.write("  No migrations needed.")
  115
+        else:
  116
+            executor.migrate(targets, plan, fake=options.get("fake", False))
  117
+
  118
+        # Send the post_migrate signal, so individual apps can do whatever they need
  119
+        # to do at this point.
  120
+        emit_post_migrate_signal(created_models, self.verbosity, self.interactive, connection.alias)
  121
+
  122
+    def migration_progress_callback(self, action, migration):
  123
+        if self.verbosity >= 1:
  124
+            if action == "apply_start":
  125
+                self.stdout.write("  Applying %s..." % migration, ending="")
  126
+                self.stdout.flush()
  127
+            elif action == "apply_success":
  128
+                self.stdout.write(self.style.MIGRATE_SUCCESS(" OK"))
  129
+            elif action == "unapply_start":
  130
+                self.stdout.write("  Unapplying %s..." % migration, ending="")
  131
+                self.stdout.flush()
  132
+            elif action == "unapply_success":
  133
+                self.stdout.write(self.style.MIGRATE_SUCCESS(" OK"))
  134
+
  135
+    def sync_apps(self, connection, apps):
  136
+        "Runs the old syncdb-style operation on a list of apps."
  137
+        cursor = connection.cursor()
  138
+
  139
+        # Get a list of already installed *models* so that references work right.
  140
+        tables = connection.introspection.table_names()
  141
+        seen_models = connection.introspection.installed_models(tables)
  142
+        created_models = set()
  143
+        pending_references = {}
  144
+
  145
+        # Build the manifest of apps and models that are to be synchronized
  146
+        all_models = [
  147
+            (app.__name__.split('.')[-2],
  148
+                [
  149
+                    m for m in models.get_models(app, include_auto_created=True)
  150
+                    if router.allow_migrate(connection.alias, m)
  151
+                ])
  152
+            for app in models.get_apps() if app.__name__.split('.')[-2] in apps
  153
+        ]
  154
+
  155
+        def model_installed(model):
  156
+            opts = model._meta
  157
+            converter = connection.introspection.table_name_converter
  158
+            # Note that if a model is unmanaged we short-circuit and never try to install it
  159
+            return not ((converter(opts.db_table) in tables) or
  160
+                (opts.auto_created and converter(opts.auto_created._meta.db_table) in tables))
  161
+
  162
+        manifest = OrderedDict(
  163
+            (app_name, list(filter(model_installed, model_list)))
  164
+            for app_name, model_list in all_models
  165
+        )
  166
+
  167
+        create_models = set([x for x in itertools.chain(*manifest.values())])
  168
+        emit_pre_migrate_signal(create_models, self.verbosity, self.interactive, connection.alias)
  169
+
  170
+        # Create the tables for each model
  171
+        if self.verbosity >= 1:
  172
+            self.stdout.write("  Creating tables...\n")
  173
+        with transaction.atomic(using=connection.alias, savepoint=False):
  174
+            for app_name, model_list in manifest.items():
  175
+                for model in model_list:
  176
+                    # Create the model's database table, if it doesn't already exist.
  177
+                    if self.verbosity >= 3:
  178
+                        self.stdout.write("    Processing %s.%s model\n" % (app_name, model._meta.object_name))
  179
+                    sql, references = connection.creation.sql_create_model(model, no_style(), seen_models)
  180
+                    seen_models.add(model)
  181
+                    created_models.add(model)
  182
+                    for refto, refs in references.items():
  183
+                        pending_references.setdefault(refto, []).extend(refs)
  184
+                        if refto in seen_models:
  185
+                            sql.extend(connection.creation.sql_for_pending_references(refto, no_style(), pending_references))
  186
+                    sql.extend(connection.creation.sql_for_pending_references(model, no_style(), pending_references))
  187
+                    if self.verbosity >= 1 and sql:
  188
+                        self.stdout.write("    Creating table %s\n" % model._meta.db_table)
  189
+                    for statement in sql:
  190
+                        cursor.execute(statement)
  191
+                    tables.append(connection.introspection.table_name_converter(model._meta.db_table))
  192
+
  193
+        # We force a commit here, as that was the previous behaviour.
  194
+        # If you can prove we don't need this, remove it.
  195
+        transaction.set_dirty(using=connection.alias)
  196
+
  197
+        # The connection may have been closed by a syncdb handler.
  198
+        cursor = connection.cursor()
  199
+
  200
+        # Install custom SQL for the app (but only if this
  201
+        # is a model we've just created)
  202
+        if self.verbosity >= 1:
  203
+            self.stdout.write("  Installing custom SQL...\n")
  204
+        for app_name, model_list in manifest.items():
  205
+            for model in model_list:
  206
+                if model in created_models:
  207
+                    custom_sql = custom_sql_for_model(model, no_style(), connection)
  208
+                    if custom_sql:
  209
+                        if self.verbosity >= 2:
  210
+                            self.stdout.write("    Installing custom SQL for %s.%s model\n" % (app_name, model._meta.object_name))
  211
+                        try:
  212
+                            with transaction.commit_on_success_unless_managed(using=connection.alias):
  213
+                                for sql in custom_sql:
  214
+                                    cursor.execute(sql)
  215
+                        except Exception as e:
  216
+                            self.stderr.write("    Failed to install custom SQL for %s.%s model: %s\n" % (app_name, model._meta.object_name, e))
  217
+                            if self.show_traceback:
  218
+                                traceback.print_exc()
  219
+                    else:
  220
+                        if self.verbosity >= 3:
  221
+                            self.stdout.write("    No custom SQL for %s.%s model\n" % (app_name, model._meta.object_name))
  222
+
  223
+        if self.verbosity >= 1:
  224
+            self.stdout.write("  Installing indexes...\n")
  225
+
  226
+        # Install SQL indices for all newly created models
  227
+        for app_name, model_list in manifest.items():
  228
+            for model in model_list:
  229
+                if model in created_models:
  230
+                    index_sql = connection.creation.sql_indexes_for_model(model, no_style())
  231
+                    if index_sql:
  232
+                        if self.verbosity >= 2:
  233
+                            self.stdout.write("    Installing index for %s.%s model\n" % (app_name, model._meta.object_name))
  234
+                        try:
  235
+                            with transaction.commit_on_success_unless_managed(using=connection.alias):
  236
+                                for sql in index_sql:
  237
+                                    cursor.execute(sql)
  238
+                        except Exception as e:
  239
+                            self.stderr.write("    Failed to install index for %s.%s model: %s\n" % (app_name, model._meta.object_name, e))
  240
+
  241
+        # Load initial_data fixtures (unless that has been disabled)
  242
+        if self.load_initial_data:
  243
+            call_command('loaddata', 'initial_data', verbosity=self.verbosity, database=connection.alias, skip_validation=True)
  244
+
  245
+        return created_models
150  django/core/management/commands/syncdb.py
... ...
@@ -1,15 +1,8 @@
1  
-from collections import OrderedDict
2  
-from importlib import import_module
  1
+import warnings
3 2
 from optparse import make_option
4  
-import itertools
5  
-import traceback
6  
-
7  
-from django.conf import settings
  3
+from django.db import DEFAULT_DB_ALIAS
8 4
 from django.core.management import call_command
9 5
 from django.core.management.base import NoArgsCommand
10  
-from django.core.management.color import no_style
11  
-from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal, emit_pre_sync_signal
12  
-from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS
13 6
 
14 7
 
15 8
 class Command(NoArgsCommand):
@@ -22,141 +15,8 @@ class Command(NoArgsCommand):
22 15
             default=DEFAULT_DB_ALIAS, help='Nominates a database to synchronize. '
23 16
                 'Defaults to the "default" database.'),
24 17
     )
25  
-    help = "Create the database tables for all apps in INSTALLED_APPS whose tables haven't already been created."
  18
+    help = "Deprecated - use 'migrate' instead."
26 19
 
27 20
     def handle_noargs(self, **options):
28  
-
29  
-        verbosity = int(options.get('verbosity'))
30  
-        interactive = options.get('interactive')
31  
-        show_traceback = options.get('traceback')
32  
-        load_initial_data = options.get('load_initial_data')
33  
-
34  
-        self.style = no_style()
35  
-
36  
-        # Import the 'management' module within each installed app, to register
37  
-        # dispatcher events.
38  
-        for app_name in settings.INSTALLED_APPS:
39  
-            try:
40  
-                import_module('.management', app_name)
41  
-            except ImportError as exc:
42  
-                # This is slightly hackish. We want to ignore ImportErrors
43  
-                # if the "management" module itself is missing -- but we don't
44  
-                # want to ignore the exception if the management module exists
45  
-                # but raises an ImportError for some reason. The only way we
46  
-                # can do this is to check the text of the exception. Note that
47  
-                # we're a bit broad in how we check the text, because different
48  
-                # Python implementations may not use the same text.
49  
-                # CPython uses the text "No module named management"
50  
-                # PyPy uses "No module named myproject.myapp.management"
51  
-                msg = exc.args[0]
52  
-                if not msg.startswith('No module named') or 'management' not in msg:
53  
-                    raise
54  
-
55  
-        db = options.get('database')
56  
-        connection = connections[db]
57  
-        cursor = connection.cursor()
58  
-
59  
-        # Get a list of already installed *models* so that references work right.
60  
-        tables = connection.introspection.table_names()
61  
-        seen_models = connection.introspection.installed_models(tables)
62  
-        created_models = set()
63  
-        pending_references = {}
64  
-
65  
-        # Build the manifest of apps and models that are to be synchronized
66  
-        all_models = [
67  
-            (app.__name__.split('.')[-2],
68  
-                [m for m in models.get_models(app, include_auto_created=True)
69  
-                if router.allow_syncdb(db, m)])
70  
-            for app in models.get_apps()
71  
-        ]
72  
-
73  
-        def model_installed(model):
74  
-            opts = model._meta
75  
-            converter = connection.introspection.table_name_converter
76  
-            return not ((converter(opts.db_table) in tables) or
77  
-                (opts.auto_created and converter(opts.auto_created._meta.db_table) in tables))
78  
-
79  
-        manifest = OrderedDict(
80  
-            (app_name, list(filter(model_installed, model_list)))
81  
-            for app_name, model_list in all_models
82  
-        )
83  
-
84  
-        create_models = set([x for x in itertools.chain(*manifest.values())])
85  
-        emit_pre_sync_signal(create_models, verbosity, interactive, db)
86  
-
87  
-        # Create the tables for each model
88  
-        if verbosity >= 1:
89  
-            self.stdout.write("Creating tables ...\n")
90  
-        with transaction.commit_on_success_unless_managed(using=db):
91  
-            for app_name, model_list in manifest.items():
92  
-                for model in model_list:
93  
-                    # Create the model's database table, if it doesn't already exist.
94  
-                    if verbosity >= 3:
95  
-                        self.stdout.write("Processing %s.%s model\n" % (app_name, model._meta.object_name))
96  
-                    sql, references = connection.creation.sql_create_model(model, self.style, seen_models)
97  
-                    seen_models.add(model)
98  
-                    created_models.add(model)
99  
-                    for refto, refs in references.items():
100  
-                        pending_references.setdefault(refto, []).extend(refs)
101  
-                        if refto in seen_models:
102  
-                            sql.extend(connection.creation.sql_for_pending_references(refto, self.style, pending_references))
103  
-                    sql.extend(connection.creation.sql_for_pending_references(model, self.style, pending_references))
104  
-                    if verbosity >= 1 and sql:
105  
-                        self.stdout.write("Creating table %s\n" % model._meta.db_table)
106  
-                    for statement in sql:
107  
-                        cursor.execute(statement)
108  
-                    tables.append(connection.introspection.table_name_converter(model._meta.db_table))
109  
-
110  
-        # Send the post_syncdb signal, so individual apps can do whatever they need
111  
-        # to do at this point.
112  
-        emit_post_sync_signal(created_models, verbosity, interactive, db)
113  
-
114  
-        # The connection may have been closed by a syncdb handler.
115  
-        cursor = connection.cursor()
116  
-
117  
-        # Install custom SQL for the app (but only if this
118  
-        # is a model we've just created)
119  
-        if verbosity >= 1:
120  
-            self.stdout.write("Installing custom SQL ...\n")
121  
-        for app_name, model_list in manifest.items():
122  
-            for model in model_list:
123  
-                if model in created_models:
124  
-                    custom_sql = custom_sql_for_model(model, self.style, connection)
125  
-                    if custom_sql:
126  
-                        if verbosity >= 2:
127  
-                            self.stdout.write("Installing custom SQL for %s.%s model\n" % (app_name, model._meta.object_name))
128  
-                        try:
129  
-                            with transaction.commit_on_success_unless_managed(using=db):
130  
-                                for sql in custom_sql:
131  
-                                    cursor.execute(sql)
132  
-                        except Exception as e:
133  
-                            self.stderr.write("Failed to install custom SQL for %s.%s model: %s\n" % \
134  
-                                                (app_name, model._meta.object_name, e))
135  
-                            if show_traceback:
136  
-                                traceback.print_exc()
137  
-                    else:
138  
-                        if verbosity >= 3:
139  
-                            self.stdout.write("No custom SQL for %s.%s model\n" % (app_name, model._meta.object_name))
140  
-
141  
-        if verbosity >= 1:
142  
-            self.stdout.write("Installing indexes ...\n")
143  
-        # Install SQL indices for all newly created models
144  
-        for app_name, model_list in manifest.items():
145  
-            for model in model_list:
146  
-                if model in created_models:
147  
-                    index_sql = connection.creation.sql_indexes_for_model(model, self.style)
148  
-                    if index_sql:
149  
-                        if verbosity >= 2:
150  
-                            self.stdout.write("Installing index for %s.%s model\n" % (app_name, model._meta.object_name))
151  
-                        try:
152  
-                            with transaction.commit_on_success_unless_managed(using=db):
153  
-                                for sql in index_sql:
154  
-                                    cursor.execute(sql)
155  
-                        except Exception as e:
156  
-                            self.stderr.write("Failed to install index for %s.%s model: %s\n" % \
157  
-                                                (app_name, model._meta.object_name, e))
158  
-
159  
-        # Load initial_data fixtures (unless that has been disabled)
160  
-        if load_initial_data:
161  
-            call_command('loaddata', 'initial_data', verbosity=verbosity,
162  
-                         database=db, skip_validation=True)
  21
+        warnings.warn("The syncdb command will be removed in Django 1.9", PendingDeprecationWarning)
  22
+        call_command("migrate", **options)
16  django/core/management/sql.py
@@ -206,25 +206,25 @@ def custom_sql_for_model(model, style, connection):
206 206
     return output
207 207
 
208 208
 
209  
-def emit_pre_sync_signal(create_models, verbosity, interactive, db):
210  
-    # Emit the pre_sync signal for every application.
  209
+def emit_pre_migrate_signal(create_models, verbosity, interactive, db):
  210
+    # Emit the pre_migrate signal for every application.
211 211
     for app in models.get_apps():
212 212
         app_name = app.__name__.split('.')[-2]
213 213
         if verbosity >= 2:
214  
-            print("Running pre-sync handlers for application %s" % app_name)
215  
-        models.signals.pre_syncdb.send(sender=app, app=app,
  214
+            print("Running pre-migrate handlers for application %s" % app_name)
  215
+        models.signals.pre_migrate.send(sender=app, app=app,
216 216
                                        create_models=create_models,
217 217
                                        verbosity=verbosity,
218 218
                                        interactive=interactive,
219 219
                                        db=db)
220 220
 
221 221
 
222  
-def emit_post_sync_signal(created_models, verbosity, interactive, db):
223  
-    # Emit the post_sync signal for every application.
  222
+def emit_post_migrate_signal(created_models, verbosity, interactive, db):
  223
+    # Emit the post_migrate signal for every application.
224 224
     for app in models.get_apps():
225 225
         app_name = app.__name__.split('.')[-2]
226 226
         if verbosity >= 2:
227  
-            print("Running post-sync handlers for application %s" % app_name)
228  
-        models.signals.post_syncdb.send(sender=app, app=app,
  227
+            print("Running post-migrate handlers for application %s" % app_name)
  228
+        models.signals.post_migrate.send(sender=app, app=app,
229 229
             created_models=created_models, verbosity=verbosity,
230 230
             interactive=interactive, db=db)
50  django/db/backends/__init__.py
@@ -521,6 +521,10 @@ def _start_transaction_under_autocommit(self):
521 521
         """
522 522
         raise NotImplementedError
523 523
 
  524
+    def schema_editor(self):
  525
+        "Returns a new instance of this backend's SchemaEditor"
  526
+        raise NotImplementedError()
  527
+
524 528
 
525 529
 class BaseDatabaseFeatures(object):
526 530
     allows_group_by_pk = False
@@ -630,11 +634,32 @@ class BaseDatabaseFeatures(object):
630 634
     # when autocommit is disabled? http://bugs.python.org/issue8145#msg109965
631 635
     autocommits_when_autocommit_is_off = False
632 636
 
  637
+    # Can we roll back DDL in a transaction?
  638
+    can_rollback_ddl = False
  639
+
  640
+    # Can we issue more than one ALTER COLUMN clause in an ALTER TABLE?
  641
+    supports_combined_alters = False
  642
+
  643
+    # What's the maximum length for index names?
  644
+    max_index_name_length = 63
  645
+
  646
+    # Does it support foreign keys?
  647
+    supports_foreign_keys = True
  648
+
  649
+    # Does it support CHECK constraints?
  650
+    supports_check_constraints = True
  651
+
633 652
     # Does the backend support 'pyformat' style ("... %(name)s ...", {'name': value})
634 653
     # parameter passing? Note this can be provided by the backend even if not
635 654
     # supported by the Python driver
636 655
     supports_paramstyle_pyformat = True
637 656
 
  657
+    # Does the backend require literal defaults, rather than parameterised ones?
  658
+    requires_literal_defaults = False
  659
+
  660
+    # Does the backend require a connection reset after each material schema change?
  661
+    connection_persists_old_columns = False
  662
+
638 663
     def __init__(self, connection):
639 664
         self.connection = connection
640 665
 
@@ -1227,7 +1252,7 @@ def django_table_names(self, only_existing=False):
1227 1252
             for model in models.get_models(app):
1228 1253
                 if not model._meta.managed:
1229 1254
                     continue
1230  
-                if not router.allow_syncdb(self.connection.alias, model):
  1255
+                if not router.allow_migrate(self.connection.alias, model):
1231 1256
                     continue
1232 1257
                 tables.add(model._meta.db_table)
1233 1258
                 tables.update([f.m2m_db_table() for f in model._meta.local_many_to_many])
@@ -1247,7 +1272,7 @@ def installed_models(self, tables):
1247 1272
         all_models = []
1248 1273
         for app in models.get_apps():
1249 1274
             for model in models.get_models(app):
1250  
-                if router.allow_syncdb(self.connection.alias, model):
  1275
+                if router.allow_migrate(self.connection.alias, model):
1251 1276
                     all_models.append(model)
1252 1277
         tables = list(map(self.table_name_converter, tables))
1253 1278
         return set([
@@ -1268,7 +1293,7 @@ def sequence_list(self):
1268 1293
                     continue
1269 1294
                 if model._meta.swapped:
1270 1295
                     continue
1271  
-                if not router.allow_syncdb(self.connection.alias, model):
  1296
+                if not router.allow_migrate(self.connection.alias, model):
1272 1297
                     continue
1273 1298
                 for f in model._meta.local_fields:
1274 1299
                     if isinstance(f, models.AutoField):
@@ -1310,6 +1335,25 @@ def get_indexes(self, cursor, table_name):
1310 1335
         """
1311 1336
         raise NotImplementedError
1312 1337
 
  1338
+    def get_constraints(self, cursor, table_name):
  1339
+        """
  1340
+        Retrieves any constraints or keys (unique, pk, fk, check, index)
  1341
+        across one or more columns.
  1342
+
  1343
+        Returns a dict mapping constraint names to their attributes,
  1344
+        where attributes is a dict with keys:
  1345
+         * columns: List of columns this covers
  1346
+         * primary_key: True if primary key, False otherwise
  1347
+         * unique: True if this is a unique constraint, False otherwise
  1348
+         * foreign_key: (table, column) of target, or None
  1349
+         * check: True if check constraint, False otherwise
  1350
+         * index: True if index, False otherwise.
  1351
+
  1352
+        Some backends may return special constraint names that don't exist
  1353
+        if they don't name constraints of a certain type (e.g. SQLite)
  1354
+        """
  1355
+        raise NotImplementedError
  1356
+
1313 1357
 
1314 1358
 class BaseDatabaseClient(object):
1315 1359
     """
13  django/db/backends/creation.py
@@ -23,11 +23,13 @@ class BaseDatabaseCreation(object):
23 23
     destruction of test databases.
24 24
     """
25 25
     data_types = {}
  26
+    data_type_check_constraints = {}
26 27
 
27 28
     def __init__(self, connection):
28 29
         self.connection = connection
29 30
 
30  
-    def _digest(self, *args):
  31
+    @classmethod
  32
+    def _digest(cls, *args):
31 33
         """
32 34
         Generates a 32-bit digest of a set of arguments that can be used to
33 35
         shorten identifying names.
@@ -330,18 +332,19 @@ def create_test_db(self, verbosity=1, autoclobber=False):
330 332
         settings.DATABASES[self.connection.alias]["NAME"] = test_database_name
331 333
         self.connection.settings_dict["NAME"] = test_database_name
332 334
 
333  
-        # Report syncdb messages at one level lower than that requested.
  335
+        # Report migrate messages at one level lower than that requested.
334 336
         # This ensures we don't get flooded with messages during testing
335 337
         # (unless you really ask to be flooded)
336  
-        call_command('syncdb',
  338
+        call_command('migrate',
337 339
             verbosity=max(verbosity - 1, 0),
338 340
             interactive=False,
339 341
             database=self.connection.alias,
340  
-            load_initial_data=False)
  342
+            load_initial_data=False,
  343
+            test_database=True)
341 344
 
342 345
         # We need to then do a flush to ensure that any data installed by
343 346
         # custom SQL has been removed. The only test data should come from
344  
-        # test fixtures, or autogenerated from post_syncdb triggers.
  347
+        # test fixtures, or autogenerated from post_migrate triggers.
345 348
         # This has the side effect of loading initial data (which was
346 349
         # intentionally skipped in the syncdb).
347 350
         call_command('flush',
8  django/db/backends/mysql/base.py
@@ -44,6 +44,9 @@
44 44
 from django.db.backends.mysql.introspection import DatabaseIntrospection
45 45
 from django.db.backends.mysql.validation import DatabaseValidation
46 46
 from django.utils.encoding import force_str, force_text
  47
+from django.db.backends.mysql.schema import DatabaseSchemaEditor
  48
+from django.utils.encoding import force_str
  49
+from django.utils.functional import cached_property
47 50
 from django.utils.safestring import SafeBytes, SafeText
48 51
 from django.utils import six
49 52
 from django.utils import timezone
@@ -171,6 +174,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
171 174
     requires_explicit_null_ordering_when_grouping = True
172 175
     allows_primary_key_0 = False
173 176
     uses_savepoints = True
  177
+    supports_check_constraints = False
174 178
 
175 179
     def __init__(self, connection):
176 180
         super(DatabaseFeatures, self).__init__(connection)
@@ -514,6 +518,10 @@ def check_constraints(self, table_names=None):
514 518
                         table_name, column_name, bad_row[1],
515 519
                         referenced_table_name, referenced_column_name))
516 520
 
  521
+    def schema_editor(self):
  522
+        "Returns a new instance of this backend's SchemaEditor"
  523
+        return DatabaseSchemaEditor(self)
  524
+
517 525
     def is_usable(self):
518 526
         try:
519 527
             self.connection.ping()
70  django/db/backends/mysql/introspection.py
... ...
@@ -1,6 +1,6 @@
1 1
 import re
2 2
 from .base import FIELD_TYPE
3  
-
  3
+from django.utils.datastructures import OrderedSet
4 4
 from django.db.backends import BaseDatabaseIntrospection, FieldInfo
5 5
 from django.utils.encoding import force_text
6 6
 
@@ -115,5 +115,71 @@ def get_indexes(self, cursor, table_name):
115 115
         for row in rows:
116 116
             if row[2] in multicol_indexes:
117 117
                 continue
118  
-            indexes[row[4]] = {'primary_key': (row[2] == 'PRIMARY'), 'unique': not bool(row[1])}
  118
+            if row[4] not in indexes:
  119
+                indexes[row[4]] = {'primary_key': False, 'unique': False}
  120
+            # It's possible to have the unique and PK constraints in separate indexes.
  121
+            if row[2] == 'PRIMARY':
  122
+                indexes[row[4]]['primary_key'] = True
  123
+            if not bool(row[1]):
  124
+                indexes[row[4]]['unique'] = True
119 125
         return indexes
  126
+
  127
+    def get_constraints(self, cursor, table_name):
  128
+        """
  129
+        Retrieves any constraints or keys (unique, pk, fk, check, index) across one or more columns.
  130
+        """
  131
+        constraints = {}
  132
+        # Get the actual constraint names and columns
  133
+        name_query = """
  134
+            SELECT kc.`constraint_name`, kc.`column_name`,
  135
+                kc.`referenced_table_name`, kc.`referenced_column_name`
  136
+            FROM information_schema.key_column_usage AS kc
  137
+            WHERE
  138
+                kc.table_schema = %s AND
  139
+                kc.table_name = %s
  140
+        """
  141
+        cursor.execute(name_query, [self.connection.settings_dict['NAME'], table_name])
  142
+        for constraint, column, ref_table, ref_column in cursor.fetchall():
  143
+            if constraint not in constraints:
  144
+                constraints[constraint] = {
  145
+                    'columns': OrderedSet(),
  146
+                    'primary_key': False,
  147
+                    'unique': False,
  148
+                    'index': False,
  149
+                    'check': False,
  150
+                    'foreign_key': (ref_table, ref_column) if ref_column else None,
  151
+                }
  152
+            constraints[constraint]['columns'].add(column)
  153
+        # Now get the constraint types
  154
+        type_query = """
  155
+            SELECT c.constraint_name, c.constraint_type
  156
+            FROM information_schema.table_constraints AS c
  157
+            WHERE
  158
+                c.table_schema = %s AND
  159
+                c.table_name = %s
  160
+        """
  161
+        cursor.execute(type_query, [self.connection.settings_dict['NAME'], table_name])
  162
+        for constraint, kind in cursor.fetchall():
  163
+            if kind.lower() == "primary key":
  164
+                constraints[constraint]['primary_key'] = True
  165
+                constraints[constraint]['unique'] = True
  166
+            elif kind.lower() == "unique":
  167
+                constraints[constraint]['unique'] = True
  168
+        # Now add in the indexes
  169
+        cursor.execute("SHOW INDEX FROM %s" % self.connection.ops.quote_name(table_name))
  170
+        for table, non_unique, index, colseq, column in [x[:5] for x in cursor.fetchall()]:
  171
+            if index not in constraints:
  172
+                constraints[index] = {
  173
+                    'columns': OrderedSet(),
  174
+                    'primary_key': False,
  175
+                    'unique': False,
  176
+                    'index': True,
  177
+                    'check': False,
  178
+                    'foreign_key': None,
  179
+                }
  180
+            constraints[index]['index'] = True
  181
+            constraints[index]['columns'].add(column)
  182
+        # Convert the sorted sets to lists
  183
+        for constraint in constraints.values():
  184
+            constraint['columns'] = list(constraint['columns'])
  185
+        return constraints
26  django/db/backends/mysql/schema.py
... ...
@@ -0,0 +1,26 @@
  1
+from django.db.backends.schema import BaseDatabaseSchemaEditor
  2
+
  3
+
  4
+class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
  5
+
  6
+    sql_rename_table = "RENAME TABLE %(old_table)s TO %(new_table)s"
  7
+
  8
+    sql_alter_column_null = "MODIFY %(column)s %(type)s NULL"
  9
+    sql_alter_column_not_null = "MODIFY %(column)s %(type)s NOT NULL"
  10
+    sql_alter_column_type = "MODIFY %(column)s %(type)s"
  11
+    sql_rename_column = "ALTER TABLE %(table)s CHANGE %(old_column)s %(new_column)s %(type)s"
  12
+
  13
+    sql_delete_unique = "ALTER TABLE %(table)s DROP INDEX %(name)s"
  14
+
  15
+    sql_create_fk = "ALTER TABLE %(table)s ADD CONSTRAINT %(name)s FOREIGN KEY (%(column)s) REFERENCES %(to_table)s (%(to_column)s)"
  16
+    sql_delete_fk = "ALTER TABLE %(table)s DROP FOREIGN KEY %(name)s"
  17
+
  18
+    sql_delete_index = "DROP INDEX %(name)s ON %(table)s"
  19
+
  20
+    sql_delete_pk = "ALTER TABLE %(table)s DROP PRIMARY KEY"
  21
+
  22
+    alter_string_set_null = 'MODIFY %(column)s %(type)s NULL;'
  23
+    alter_string_drop_null = 'MODIFY %(column)s %(type)s NOT NULL;'
  24
+
  25
+    sql_create_pk = "ALTER TABLE %(table)s ADD CONSTRAINT %(name)s PRIMARY KEY (%(columns)s)"
  26
+    sql_delete_pk = "ALTER TABLE %(table)s DROP PRIMARY KEY"
10  django/db/backends/oracle/base.py
@@ -55,6 +55,7 @@ def _setup_environment(environ):
55 55
 from django.db.backends.oracle.client import DatabaseClient
56 56
 from django.db.backends.oracle.creation import DatabaseCreation
57 57
 from django.db.backends.oracle.introspection import DatabaseIntrospection
  58
+from django.db.backends.oracle.schema import DatabaseSchemaEditor
58 59
 from django.utils.encoding import force_bytes, force_text
59 60
 
60 61
 
@@ -90,6 +91,11 @@ class DatabaseFeatures(BaseDatabaseFeatures):
90 91
     has_bulk_insert = True
91 92
     supports_tablespaces = True
92 93
     supports_sequence_reset = False
  94
+    supports_combined_alters = False
  95
+    max_index_name_length = 30
  96
+    nulls_order_largest = True
  97
+    requires_literal_defaults = True
  98
+    connection_persists_old_columns = True
93 99
     nulls_order_largest = True
94 100
 
95 101
 
@@ -621,6 +627,10 @@ def _commit(self):
621 627
                    and x.code == 2091 and 'ORA-02291' in x.message:
622 628
                     six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
623 629
                 raise
  630
+    
  631
+    def schema_editor(self):
  632
+        "Returns a new instance of this backend's SchemaEditor"
  633
+        return DatabaseSchemaEditor(self)
624 634
 
625 635
     # Oracle doesn't support savepoint commits.  Ignore them.
626 636
     def _savepoint_commit(self, sid):
15  django/db/backends/oracle/creation.py
@@ -22,7 +22,7 @@ class DatabaseCreation(BaseDatabaseCreation):
22 22
     data_types = {
23 23
         'AutoField': 'NUMBER(11)',
24 24
         'BinaryField': 'BLOB',
25  
-        'BooleanField': 'NUMBER(1) CHECK (%(qn_column)s IN (0,1))',
  25
+        'BooleanField': 'NUMBER(1)',
26 26
         'CharField': 'NVARCHAR2(%(max_length)s)',