Anonymous avatar Anonymous committed d1a2ce3

Initial South checkin

Comments (0)

Files changed (14)

+"""
+South - Useable migrations for Django apps
+"""
+
+# Establish the common DatabaseOperations instance, which we call 'db'.
+# This code somewhat lifted from django evolution
+from django.conf import settings
+module_name = ['south.db', settings.DATABASE_ENGINE]
+module = __import__('.'.join(module_name),{},{},[''])
+db = module.DatabaseOperations()
+
+from django.db import connection, transaction
+
+class DatabaseOperations(object):
+
+    """
+    Generic SQL implementation of the DatabaseOperations.
+    Some of this code comes from Django Evolution.
+    """
+
+    types = {
+        "varchar": "VARCHAR",
+        "text": "TEXT",
+        "integer": "INT",
+    }
+
+    def __init__(self):
+        self.debug = False
+
+
+    def get_type(self, name, param=None):
+        """
+        Generic type-converting method, to smooth things over.
+        """
+        if name in ["text", "string"]:
+            if param:
+                return "%s(%s)" % (self.types['varchar'], param)
+            else:
+                return self.types['text']
+        else:
+            return self.types[name]
+
+
+    def execute(self, sql, params=[]):
+        """
+        Executes the given SQL statement, with optional parameters.
+        If the instance's debug attribute is True, prints out what it executes.
+        """
+        cursor = connection.cursor()
+        if self.debug:
+            print "   = %s" % sql
+        return cursor.execute(sql, params)
+
+
+    def create_table(self, table_name, columns):
+        """
+        Creates the table 'table_name'. 'columns' is a list of columns
+        in the same format used by add_column (but as a list - think of its
+        positional arguments).
+        """
+        qn = connection.ops.quote_name
+        params = (
+            qn(table_name),
+            ", ".join([
+                self.column_sql(*column)
+                for column in columns
+            ]),
+        )
+        self.execute('CREATE TABLE %s (%s);' % params)
+
+
+    def rename_table(self, old_table_name, table_name):
+        """
+        Renames the table 'old_table_name' to 'table_name'.
+        """
+        if old_table_name == table_name:
+            # No Operation
+            return
+        qn = connection.ops.quote_name
+        params = (qn(old_table_name), qn(table_name))
+        self.execute('ALTER TABLE %s RENAME TO %s;' % params)
+
+
+    def delete_table(self, table_name):
+        """
+        Deletes the table 'table_name'.
+        """
+        qn = connection.ops.quote_name
+        params = (qn(table_name), )
+        self.execute('DROP TABLE %s;' % params)
+
+
+    def add_column(self, table_name, column_name, type_name, type_param=None, unique=False, null=True, related_to=None):
+        """
+        Adds the column 'column_name' to the table 'table_name'.
+        The column will have type 'type_name', which is one of the generic
+        types South offers, such as 'string' or 'integer'.
+        
+        @param table_name: The name of the table to add the column to
+        @param column_name: The name of the column to add
+        @param type_name: The (generic) name of this column's type
+        @param type_param: An optional parameter to the type - e.g., its length
+        @param unique: Whether this column has UNIQUE set. Defaults to False.
+        @param null: If this column will be allowed to contain NULL values. Defaults to True.
+        @param related_to: A tuple of (table_name, column_name) for the column this references if it is a ForeignKey.
+        """
+        qn = connection.ops.quote_name
+        params = (
+            qn(table_name),
+            self.column_sql(column_name, type_name, type_param, unique, null, related_to),
+        )
+        sql = 'ALTER TABLE %s ADD COLUMN %s;' % params
+        self.execute(sql)
+
+
+    def column_sql(self, column_name, type_name, type_param=None, unique=False, null=True, related_to=None):
+        """
+        Creates the SQL snippet for a column. Used by add_column and add_table.
+        """
+        qn = connection.ops.quote_name
+        params = (
+            qn(column_name),
+            self.get_type(type_name, type_param),
+            (unique and "UNIQUE " or "") + (null and "NULL" or ""),
+            related_to and ("REFERENCES %s (%s) %s" % (
+                related_to[0],  # Table name
+                related_to[1],  # Column name
+                connection.ops.deferrable_sql(), # Django knows this
+            )) or "",
+        )
+        return '%s %s %s %s' % params
+
+
+    def delete_column(self, table_name, column_name):
+        """
+        Deletes the column 'column_name' from the table 'table_name'.
+        """
+        qn = connection.ops.quote_name
+        params = (qn(mtable_name), qn(column_name))
+        return ['ALTER TABLE %s DROP COLUMN %s CASCADE;' % params]
+
+
+    def rename_column(self, table_name, old, new):
+        """
+        Renames the column 'old' from the table 'table_name' to 'new'.
+        """
+        raise NotImplementedError("rename_column has no generic SQL syntax")
+
+
+    def commit_transaction(self):
+        """
+        Commits the current transaction.
+        """
+        transaction.commit()
+
+
+    def rollback_transaction(self):
+        """
+        Rolls back the current transaction.
+        """
+        transaction.rollback()

db/postgresql_psycopg2.py

+
+from django.db import connection
+from south.db import generic
+
+class DatabaseOperations(generic.DatabaseOperations):
+
+    """
+    PsycoPG2 implementation of database operations.
+    """
+
+    def rename_column(self, table_name, old, new):
+        if old == new:
+            return []
+        qn = connection.ops.quote_name
+        params = (qn(table_name), qn(old), qn(new))
+        self.execute('ALTER TABLE %s RENAME COLUMN %s TO %s;' % params)
+This is South, a Django application to provide migrations in a sane way.
+
+By sane, we mean that the status of every migration is tracked individually,
+rather than just the number of the top migration reached; this means South
+can detect when you have an unapplied migration that's sitting in the middle
+of a whole load of applied ones, and will let you apply it straight off,
+or let you roll back to it, and apply from there forward.
+
+You'll interact with South in two main ways; via manage.py, and through
+app migrations.
+
+ manage.py
+ ---------
+ 
+ South has three commands in manage.py:
+ 
+  syncdb - This is a modified version of the original Django syncdb, that
+           only does the usual syncdb on apps without migrations (i.e. without
+           a migrations/ python package in their app directory).
+           This means you can still use syncdb's magic creation for things like
+           Django auth, but use migrations for your own app.
+
+  migrate - Allows you to migrate forwards or backwards. With no arguments,
+            will migrate forwards as far as it can (i.e. perform all migrations).
+            If you provide an argument, it will attempt to migrate forwards
+            or backwards until it reaches the state where it would have just
+            performed the named migration (e.g. ./manage.py migrate 0002_test)
+            
+            This argument does not end in .py, and you can use the special
+            argument 'zero' to undo every single migration.
+            
+            If there is a consistency error - that is, South finds a migration
+            which is unapplied, but before a migration that is already applied -
+            it will tell you what migrations are wrong and refuse to continue.
+            
+            In this situation, you need to provide one of these command-line
+            switches:
+            
+             --skip will ignore any warnings and just continue applying
+                    or undoing migrations.
+             --merge will run the missing migrations straight away, and then
+                     continue, if you are trying to progress forwards. If you
+                     are trying to migrate backwards, it behaves like --skip.
+            
+            Generally, a sensible thing to do is either --merge, or perform
+            a rollback to the offending migration and then roll forwards.
+            For example, if 0003_addedlater is the unapplied migration:
+            
+              ./manage.py migrate --skip 0002_test
+              ./manage.py migrate
+            
+            Here, we roll back past where 0003 would have been applied and
+            then roll forwards, applying it in the process.
+
+  startmigration - Provide this command with an app name and optional migration
+                   name and it will create a new stub migration for you.
+                   If the app currently has no migration directory, it will
+                   also create that.
+
+
+ Migrations
+ ----------
+ 
+ Migrations are found in the migrations/ directory under an app. They are always
+ executed in ASCII sort order, so 11_foo.py would be executed before 2_bar.py
+ (you can get around this by naming files like 0002_bar.py; this is what
+ ./manage.py startmigration does).
+ 
+ Inside a migration file, there should be a Migration class with forward() and
+ backward() methods. These will be called when the migration needs to be executed
+ in the respective directions.
+ 
+ You can do anything inside these two functions; you can use the Django ORM
+ to change data, or django.db.connection to execute raw SQL, for example.
+ However, as raw SQL isn't very portable, there's also a thin database actions
+ abstraction layer called south.db.db, which provides database-agnostic
+ methods like create_table, add_column, rename_column, and so on.
+ 
+ Documentation for south.db can be found in its own file, for now.

Empty file added.

Add a comment to this file

management/commands/__init__.py

Empty file added.

management/commands/migrate.py

+from django.core.management.base import BaseCommand
+from django.core.management.color import no_style
+from django.db import models
+from optparse import make_option
+from south import migration
+import sys
+
+class Command(BaseCommand):
+    option_list = BaseCommand.option_list + (
+        make_option('--skip', action='store_true', dest='skip', default=False,
+            help='Will skip over out-of-order missing migrations'),
+        make_option('--merge', action='store_true', dest='merge', default=False,
+            help='Will run out-of-order missing migrations as they are - no rollbacks.'),
+        make_option('--only', action='store_true', dest='only', default=False,
+            help='Only runs or rolls back the migration specified, and none around it.'),
+    )
+    help = "Runs migrations for all apps."
+
+    def handle(self, target=None, skip=False, merge=False, only=False, backwards=False, **options):
+        # Work out what the resolve mode is
+        resolve_mode = merge and "merge" or (skip and "skip" or None)
+        # Turn on db debugging
+        from south.db import db
+        db.debug = True
+        # Migrate each app
+        for app in models.get_apps():
+            migrations = migration.get_migrations(app)
+            if migrations is not None:
+                migration.migrate_app(
+                    migrations,
+                    resolve_mode = resolve_mode,
+                    target_name = target,
+                )
+                continue

management/commands/startmigration.py

+from django.core.management.base import BaseCommand
+from django.core.management.color import no_style
+from django.db import models
+from optparse import make_option
+from south import migration
+import sys
+import os
+import string
+import random
+
+class Command(BaseCommand):
+    option_list = BaseCommand.option_list
+    help = "Creates a new template migration for the given app"
+
+    def handle(self, app=None, name="", **options):
+        if not app:
+            print "Please provide an app in which to create the migration."
+            return
+        # See if the app exists
+        try:
+            app_module = __import__(app, {}, {}, ['migrations'])
+        except ImportError:
+            print "App '%s' doesn't seem to exist." % app
+            return
+        # Make the migrations directory if it's not there
+        migrations_dir = os.path.join(
+            os.path.dirname(app_module.__file__),
+            "migrations",
+        )
+        if not os.path.isdir(migrations_dir):
+            print "Creating migrations directory at '%s'..." % migrations_dir
+            os.mkdir(migrations_dir)
+            # Touch the init py file
+            open(os.path.join(migrations_dir, "__init__.py"), "w").close()
+        # See what filename is next in line. We assume they use numbers.
+        migrations = migration.get_migration_files(app)
+        highest_number = 0
+        for migration_name in migrations:
+            try:
+                number = int(migration_name.split("_")[0])
+                highest_number = max(highest_number, number)
+            except ValueError:
+                pass
+        # Make the new filename
+        new_filename = "%04i%s_%s.py" % (
+            highest_number + 1,
+            "".join([random.choice(string.letters.lower()) for i in range(0)]), # Possible random stuff insertion
+            name,
+        )
+        fp = open(os.path.join(migrations_dir, new_filename), "w")
+        fp.write("""
+from south.db import db
+from %s.models import *
+
+class Migration:
+    
+    def forwards(self):
+        # Write your forwards migration here
+        pass
+    
+    def backwards(self):
+        # Write your backwards migration here
+        pass""" % app)
+        fp.close()

management/commands/syncdb.py

+from django.core.management.base import NoArgsCommand
+from django.core.management.color import no_style
+from optparse import make_option
+from south import migration
+from django.core.management.commands import syncdb
+from django.conf import settings
+import sys
+
+class Command(NoArgsCommand):
+    option_list = NoArgsCommand.option_list + (
+        make_option('--verbosity', action='store', dest='verbosity', default='1',
+            type='choice', choices=['0', '1', '2'],
+            help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'),
+        make_option('--noinput', action='store_false', dest='interactive', default=True,
+            help='Tells Django to NOT prompt the user for input of any kind.'),
+    )
+    help = "Create the database tables for all apps in INSTALLED_APPS whose tables haven't already been created, except those which use migrations."
+
+    def handle_noargs(self, **options):
+        from django.db import models
+        # Work out what uses migrations and so doesn't need syncing
+        apps_needing_sync = []
+        apps_migrated = []
+        for app in models.get_apps():
+            app_name = '.'.join( app.__name__.split('.')[0:-1] )
+            migrations = migration.get_migrations(app)
+            if migrations is None:
+                apps_needing_sync.append(app_name)
+            else:
+                # This is a migrated app, leave it
+                apps_migrated.append(app_name)
+        # Run syncdb on only the ones needed
+        old_installed, settings.INSTALLED_APPS = settings.INSTALLED_APPS, apps_needing_sync
+        syncdb.Command().execute(**options)
+        settings.INSTALLED_APPS = old_installed
+        # Be obvious about what we did
+        print "\nSynced:\n > %s" % "\n > ".join(apps_needing_sync)
+        print "\nNot synced (use migrations):\n - %s" % "\n - ".join(apps_migrated)
+        print "(use ./manage.py migrate to migrate these)"
+
+import datetime
+import os
+import sys
+from django.conf import settings
+from models import MigrationHistory
+
+
+def get_migrations(app):
+    """
+    Returns the list of migration modules for the given app, or None
+    if it does not use migrations.
+    """
+    app_name = app.__name__
+    app_name = '.'.join( app_name.split('.')[0:-1] )
+    mod = __import__(app_name, {}, {}, ['migrations'])
+    if hasattr(mod, 'migrations'):
+        return getattr(mod, 'migrations')
+
+
+def get_migration_files(app):
+    """
+    Returns a list of migration file names for the given app.
+    """
+    return sorted([
+        filename[:-3]
+        for filename in os.listdir(os.path.join(
+            os.path.dirname(__import__(app,{},{},['migrations']).__file__),
+            "migrations",
+        ))
+        if filename.endswith(".py") and filename != "__init__.py"
+    ])
+
+
+def get_migration(app_name, name):
+    """
+    Returns the migration class implied by 'name'.
+    """
+    module = __import__(app_name + ".migrations." + name, locals(), globals())
+    return getattr(module.migrations, name).Migration
+
+
+def run_forwards(app_name, migrations):
+    """
+    Runs the specified migrations forwards, in order.
+    """
+    for migration in migrations:
+        print " > %s" % migration
+        klass = get_migration(app_name, migration)
+        klass().forwards()
+        # Record us as having done this
+        record = MigrationHistory.for_migration(app_name, migration)
+        record.applied = datetime.datetime.utcnow()
+        record.save()
+
+
+def run_backwards(app_name, migrations, ignore=[]):
+    """
+    Runs the specified migrations backwards, in order, skipping those
+    migrations in 'ignore'.
+    """
+    for migration in migrations:
+        if migration not in ignore:
+            print " < %s" % migration
+            klass = get_migration(app_name, migration)
+            klass().backwards()
+            # Record us as having not done this
+            record = MigrationHistory.for_migration(app_name, migration)
+            record.delete()
+
+
+def migrate_app(migration_module, target_name=None, resolve_mode=None):
+    
+    # Work out exactly what we're working on
+    app_name = os.path.splitext(migration_module.__name__)[0]
+    
+    # Find out what delightful migrations we have
+    migrations = sorted([
+        filename[:-3]
+        for filename in os.listdir(os.path.dirname(migration_module.__file__))
+        if filename.endswith(".py") and filename != "__init__.py"
+    ])
+    
+    # Find out what's already applied
+    current_migrations = [m.migration for m in MigrationHistory.objects.filter(
+        app_name = app_name,
+        applied__isnull = False,
+    )]
+    current_migrations.sort()
+    
+    # Say what we're doing
+    print "Running migrations for %s:" % app_name
+    
+    missing = []   # Migrations that should be there but aren't
+    offset = 0     # To keep the lists in sync when missing ones crop up
+    first = None   # The first missing migration (for rollback)
+    current = len(migrations)  # The apparent latest migration.
+    
+    # Work out the missing migrations
+    for i, migration in enumerate(migrations):
+        if i >= len(current_migrations):
+            current = i
+            break
+        elif current_migrations[i-offset] != migration:
+            if not first:
+                first = i
+            missing.append(migration)
+            offset += 1
+    
+    if offset:
+        current += offset
+    
+    # Work out what they want us to go to.
+    # Target (and current) are relative to migrations, not current_migrations
+    if not target_name:
+        target = len(migrations)
+    else:
+        if target_name == "zero":
+            target = 0
+        else:
+            try:
+                target = migrations.index(target_name) + 1
+            except ValueError:
+                print " ! '%s' is not a migration." % target_name
+                return
+    
+    def describe_index(i):
+        if i == 0:
+            return "(no migrations applied)"
+        else:
+            return "(after %s)" % migrations[i-1]
+    
+    def one_before(what):
+        index = migrations.index(what) - 1
+        if index < 0:
+            return "zero"
+        else:
+            return migrations[index]
+    
+    print " - Current migration: %s %s" % (current, describe_index(current))
+    print " - Target migration: %s %s" % (target, describe_index(target))
+    
+    if missing:
+        print " ! These migrations should have been applied already, but aren't:"
+        print "   - " + "\n   - ".join(missing)
+        if resolve_mode is None:
+            print " ! Please re-run migrate with one of these switches:"
+            print "   --skip: Ignore this migration mismatch and keep going"
+            print "   --merge: Just apply the missing migrations out of order"
+            print "   If you want to roll back to the first of these migrations"
+            print "   and then roll forward, do:"
+            print "     ./manage.py migrate --skip %s" % one_before(missing[0])
+            print "     ./manage.py migrate"
+            return
+    
+    # If we're using merge, and going forwards, merge
+    if target >= current and resolve_mode == "merge" and missing:
+        print " - Merging..."
+        run_forwards(app_name, missing)
+    
+    # Now do the right direction.
+    if target == current:
+        print " - No migration needed."
+    elif target < current:
+        # Rollback
+        print " - Rolling back..."
+        run_backwards(app_name, reversed(current_migrations[target:current]))
+    else:
+        print " - Migrating..."
+        run_forwards(app_name, migrations[current:target])
+from django.db import models
+
+class MigrationHistory(models.Model):
+    app_name = models.CharField(max_length=255)
+    migration = models.CharField(max_length=255)
+    applied = models.DateTimeField(blank=True, null=True)
+
+    @classmethod
+    def for_migration(cls, app_name, migration):
+        try:
+            return cls.objects.get(
+                app_name = app_name,
+                migration = migration,
+            )
+        except cls.DoesNotExist:
+            return cls(
+                app_name = app_name,
+                migration = migration,
+            )

tests/__init__.py

+
+from south.tests.db import *
+import unittest
+
+from south.db import db
+from django.db import connection
+
+# Create a list of error classes from the various database libraries
+errors = []
+try:
+    from psycopg2 import ProgrammingError
+    errors.append(ProgrammingError)
+except ImportError:
+    pass
+errors = tuple(errors)
+
+class TestOperations(unittest.TestCase):
+
+    """
+    Tests if the various DB abstraction calls work.
+    Can only test a limited amount due to DB differences.
+    """
+
+    def setUp(self):
+        db.debug = False
+
+    def test_create(self):
+        """
+        Test creation and deletion of tables.
+        """
+        cursor = connection.cursor()
+        # It needs to take at least 2 args
+        self.assertRaises(TypeError, db.create_table)
+        self.assertRaises(TypeError, db.create_table, "test1")
+        # Empty tables (i.e. no columns) are fine
+        db.create_table("test1", [])
+        db.commit_transaction()
+        # And should exist
+        cursor.execute("SELECT * FROM test1")
+        # Make sure we can't do the same query on an empty table
+        try:
+            cursor.execute("SELECT * FROM nottheretest1")
+            self.fail("Non-existent table could be selected!")
+        except:
+            pass
+        # Clear the dirty transaction
+        db.rollback_transaction()
+        # Remove the table
+        db.delete_table("test1")
+        # Make sure it went
+        try:
+            cursor.execute("SELECT * FROM test1")
+            self.fail("Just-deleted table could be selected!")
+        except:
+            pass
+        # Clear the dirty transaction
+        db.rollback_transaction()
+        # Try deleting a nonexistent one
+        try:
+            db.delete_table("nottheretest1")
+            self.fail("Non-existent table could be deleted!")
+        except:
+            pass
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.