DeadWisdom / Migratory

Migratory (pronounced as a pythoner: 'MY GRAY ER EE') is a migration contrib app for Django. It both generates custom migration files for you and is a system for executing them. What's more, it works great in a vcs environment.

Clone this repository (size: 102.2 KB): HTTPS / SSH
$ hg clone http://bitbucket.org/DeadWisdom/migratory/

Changed (Δ8.5 KB):

raw changeset »

README (4 lines added, 0 lines removed)

migratory/management/commands/migrate.py (161 lines added, 110 lines removed)

migratory/manager/base.py (196 lines added, 42 lines removed)

migratory/manager/sqlite3.py (14 lines added, 8 lines removed)

migratory/matchmaker.py (16 lines added, 64 lines removed)

migratory/migrate.py (29 lines added, 6 lines removed)

migratory/models.py (4 lines added, 1 lines removed)

migratory/snapshot.py (48 lines added, 6 lines removed)

Up to file-list README:

@@ -13,3 +13,7 @@ And then run the tests:
13
13
14
14
    cd test_project
15
15
    ./test
16
    
17
== App Layout ==
18
The main entry points to the app are events hooked in migratory/models.py and 
19
the ./manage.py command 'migrate' in migratory/management/commands/migrate.py

Up to file-list migratory/management/commands/migrate.py:

1
"""
2
Automation of the creation of migration scripts.
3
"""
4
1
5
import os, os.path
2
6
from datetime import date
3
7
@@ -8,33 +12,138 @@ from django.core.management.color import
8
12
from django.contrib.migratory import manager, snapshot
9
13
from django.contrib.migratory.matchmaker import matchmaker, compare_strings
10
14
15
class Command(BaseCommand):
16
    help = "Creates a migration file based on the delta of last snapshot to the current models, adds it to the migration manifest."
17
    args = '<app-name migration-name>'
18
    
19
    def handle(self, *args, **options):
20
        try:
21
            app_name, mig_name = args
22
        except:
23
            raise CommandError("Specify the name of an app, and a slug-formatted name for the migration.")
24
        
25
        self.manager = manager.DatabaseManager()
26
        
27
        try:
28
            if app_name not in self.manager:
29
                raise CommandError("Unable to find previous snapshot of app %r." % app_name)
30
        except:
31
            # No snapshots yet, so we kick out.
32
            return
33
        
34
        app = get_new_app(app_name)
35
        new_models = get_new_models(app)
36
        old_models = self.manager.get_models(app_name)
37
        
38
        # The order of old_models then new_models is important here, as 
39
        # compareModels weights so that all the models in old are also in new.  
40
        # If new has a bunch of new ones, it still makes a perfect match.
41
        matches, drop, add = matchmaker(old_models, new_models, compareModels, .4)
42
        
43
        changeset = Changeset(app)
44
        
45
        for model in drop:
46
            changeset.drop_model(model)
47
            
48
        for model in add:
49
            changeset.add_model(model)
50
        
51
        for old_model, new_model in matches:
52
            old_opts, new_opts = old_model._meta, new_model._meta
53
            
54
            fields, drop, add = matchmaker(
55
                        old_opts.local_fields + old_opts.local_many_to_many, 
56
                        new_opts.local_fields + new_opts.local_many_to_many,
57
                        compareFields,
58
                        .5)
59
            
60
            changeset.change_model(old_model)
61
            
62
            if (old_model._meta.object_name != new_model._meta.object_name):
63
                changeset.rename_model(new_model)
64
            
65
            for field in drop:
66
                changeset.drop_field(field)
67
            
68
            for field in add:
69
                changeset.add_field(field)
70
            
71
            for old, new in fields:
72
                old_snap = snapshot.field_snap(old, old_model)
73
                new_snap = snapshot.field_snap(new, new_model)
74
                
75
                # Strip the 'migration-' prefix for the app on the 
76
                # related-field.
77
                if old_snap.get('__rel__'):
78
                    old_snap['__rel__'] = (
79
                        old_snap['__rel__'][0].replace('migration-', ''),
80
                        old_snap['__rel__'][1]
81
                    )
82
                
83
                if (old.name != new.name):
84
                    changeset.change_field(old, new)
85
                elif (old_snap != new_snap):
86
                    changeset.change_field(new)
87
            
88
            changeset.end_model()
89
        
90
        today = date.today()
91
        if mig_name.endswith('.py') or mig_name.endswith('.sql'):
92
            filename = mig_name
93
        else:
94
            filename = "%d-%d-%d-%s.py" % (
95
                today.year, today.month, today.day, mig_name
96
            )
97
            
98
        changeset.save(filename)  
99
    
100
        print changeset.report()
101
        
102
        if (changeset):
103
            print "\nPlease look over the changes. When you feel confident they are correct, issue a 'syncdb':\n ./manage.py syncdb"
104
105
106
### Helper Functions / Classes ###
11
107
def compareModels(a, b):
12
    name_coef = compare_strings(a._meta.object_name, b._meta.object_name)
13
    name_coef *= .5
108
    """
109
    Comparison func that returns a coefficient of similarity (0.0 to 1.0) 
110
    for the given models.
111
    """
112
    name = compare_strings(a._meta.object_name, b._meta.object_name)
14
113
    
15
    fields_a = set(f.name for f in a._meta.local_fields + a._meta.local_many_to_many)
16
    fields_b = set(f.name for f in b._meta.local_fields + b._meta.local_many_to_many)
114
    fields_a = set(
115
        f.name for f in a._meta.local_fields + a._meta.local_many_to_many)
116
    fields_b = set(
117
        f.name for f in b._meta.local_fields + b._meta.local_many_to_many)
17
118
    
18
    similarity = len(fields_a.intersection(fields_b)) / float( len(fields_a) ) * .5
19
    coef = name_coef + similarity
20
    return coef
119
    fields = len( fields_a.intersection(fields_b) ) / float( len(fields_a) )
120
    
121
    return ( name * .5 ) + ( fields * .5 )
21
122
22
123
def compareFields(a, b):
23
    coef = compare_strings(a.name, b.name)
24
    coef *= .5
124
    """
125
    Compare two given fields.
126
    """
127
    name = compare_strings(a.name, b.name) * .5
25
128
    
26
129
    if a.__class__ == b.__class__:
27
        coef += .5
130
        cls = .5
28
131
    else:
29
        coef += compare_strings(a.__class__.__name__, b.__class__.__name__) * .5
30
    return coef
132
        cls = compare_strings(a.__class__.__name__, b.__class__.__name__)
133
        
134
    return ( name * .5 ) + ( cls * .5 )
31
135
32
136
class Changeset(object):
137
    """
138
    Records changes to a model, and then is able to produce a script for those 
139
    changes and/or print out verbiage for those changes.
140
    """
33
141
    def __init__(self, app):
34
142
        self.changes = []
35
143
        self.app = app
36
144
    
37
145
    def __bool__(self):
146
        """Are there any changes?"""
38
147
        return bool(self.changes)
39
148
    
40
149
    def drop_model(self, model):
@@ -50,10 +159,17 @@ class Changeset(object):
50
159
        ))
51
160
    
52
161
    def change_model(self, model):
162
        """
163
        Marks the beginning of changes to the model, end those changes with 
164
        end_model()
165
        """
53
166
        self.model = model
54
167
        self._changes, self.changes = self.changes, []
55
168
    
56
169
    def end_model(self):
170
        """
171
        Marks the end of model changes.
172
        """
57
173
        model_changes = self.changes
58
174
        self.changes = self._changes
59
175
        
@@ -66,34 +182,42 @@ class Changeset(object):
66
182
    def rename_model(self, new):
67
183
        self.changes.append((
68
184
             "Rename to %r." % new._meta.object_name,
69
             "database.rename_model(%r, %r)" % (self.model._meta.object_name, new._meta.object_name)
185
             "database.rename_model(%r, %r)" % (
186
                self.model._meta.object_name, new._meta.object_name)
70
187
        ))
71
188
    
72
189
    def drop_field(self, field):
73
190
        self.changes.append((
74
191
            "Drop the field %r." % field.name,
75
            "database.drop_field(%r, %r)" % (self.model._meta.object_name, field.name)
192
            "database.drop_field(%r, %r)" % (
193
                self.model._meta.object_name, field.name)
76
194
        ))
77
195
        
78
196
    def add_field(self, field):
79
197
        self.changes.append((
80
198
            "Add the new field %r." % field.name,
81
            "database.add_field(%r, %r)" % (self.model._meta.object_name, field.name)
199
            "database.add_field(%r, %r)" % (
200
                self.model._meta.object_name, field.name)
82
201
        ))
83
202
        
84
203
    def change_field(self, old, new=None):
85
204
        if (new):
86
205
            self.changes.append((
87
206
                "Change to %r, renaming it %r." % (old.name, new.name),
88
                "database.change_field(%r, %r, %r)" % (self.model._meta.object_name, old.name, new.name)
207
                "database.change_field(%r, %r, %r)" % (
208
                    self.model._meta.object_name, old.name, new.name)
89
209
            ))
90
210
        else:
91
211
            self.changes.append((
92
212
                "Change the field %r." % old.name,
93
                "database.change_field(%r, %r)" % (self.model._meta.object_name, old.name)
213
                "database.change_field(%r, %r)" % (
214
                    self.model._meta.object_name, old.name)
94
215
            ))
95
216
        
96
217
    def report(self):
218
        """
219
        Returns a natural language report of the changes as a big string.
220
        """
97
221
        gather = []
98
222
        
99
223
        for report, sub in self.changes:
@@ -108,6 +232,10 @@ class Changeset(object):
108
232
        return '\n'.join(gather)
109
233
    
110
234
    def up(self):
235
        """
236
        Produces the python source-code for the up() function that will be 
237
        used in the migration script.
238
        """
111
239
        gather = []
112
240
        
113
241
        for _, code in self.changes:
@@ -123,12 +251,20 @@ class Changeset(object):
123
251
        return "def up(database):\n    %s\n" % '\n    '.join(gather) 
124
252
        
125
253
    def get_migrations_dir(self):
254
        """
255
        Gets or creates the migration directory on the filesystem.
256
        """
126
257
        path = os.path.join(os.path.dirname(self.app.__file__), 'migrations')
127
258
        if not os.path.isdir(path):
128
259
            os.mkdir(path)
129
260
        return path
130
261
        
131
262
    def get_manifest(self):
263
        """
264
        get_manifest() -> (path, src)
265
        
266
        Returns the manifest (__manifest__.py) path and source.
267
        """
132
268
        migrations_dir = self.get_migrations_dir()
133
269
        path = os.path.join(migrations_dir, '__manifest__.py')
134
270
@@ -145,6 +281,10 @@ class Changeset(object):
145
281
        return path, src
146
282
        
147
283
    def save(self, filename, comment = ''):
284
        """
285
        Saves the changeset to a new migration file, and appends to the 
286
        manifest (__manifest__.py).
287
        """
148
288
        path, src = self.get_manifest()
149
289
        manifests = eval(src)
150
290
        if (filename in manifests):
@@ -154,8 +294,9 @@ class Changeset(object):
154
294
        
155
295
        spaces = '    '
156
296
        if len(lines) > 2:
297
            # Get the padding of the previous line.
157
298
            prev = lines[-2]
158
            spaces = prev[: len( prev ) - len( prev.lstrip() ) ]    # Get the padding of the previous line.
299
            spaces = prev[: len( prev ) - len( prev.lstrip() ) ]    
159
300
        
160
301
        line = spaces + repr(filename) + ','
161
302
        
@@ -181,97 +322,7 @@ class Changeset(object):
181
322
        print "Writing migration to: %s" % path
182
323
        o = open(path, 'w')
183
324
        if path.endswith('.py'):
184
            o.write("\"\"\"\nDjango Migration %s\n%s\n\"\"\"\n\n" % (filename, comment))
325
            o.write("\"\"\"\nDjango Migration %s\n%s\n\"\"\"\n\n" % \
326
                (filename, comment))
185
327
            o.write(self.up())
186
328
        o.close()
187
188
class Command(BaseCommand):
189
    """
190
    A management command which takes one or more arbitrary arguments
191
    (labels) on the command line, and does something with each of
192
    them.
193
194
    Rather than implementing ``handle()``, subclasses must implement
195
    ``handle_label()``, which will be called once for each label.
196
197
    If the arguments should be names of installed applications, use
198
    ``AppCommand`` instead.
199
    
200
    """
201
    help = "Creates a migration file based on the delta of last snapshot to the current models, adds it to the migration manifest."
202
    args = '<app-name migration-name>'
203
    
204
    def handle(self, *args, **options):
205
        try:
206
            app_name, mig_name = args
207
        except:
208
            raise CommandError("Specify the name of an app, and a slug-formatted name for the migration.")
209
        
210
        self.manager = manager.DatabaseManager()
211
        
212
        try:
213
            if app_name not in self.manager:
214
                raise CommandError("Unable to find previous snapshot of app %r." % app_name)
215
        except:
216
            # No snapshots yet, so we kick out.
217
            return
218
        
219
        app = get_new_app(app_name)
220
        new_models = get_new_models(app)
221
        old_models = self.manager.get_models(app_name)
222
        
223
        # The order of old_models then new_models is important here, as compareModels 
224
        # weights so that all the models in old are also in new.  If new has a bunch 
225
        # of new ones, it still makes a perfect match.
226
        matches, drop, add = matchmaker(old_models, new_models, compareModels, .4)
227
        
228
        changeset = Changeset(app)
229
        
230
        for model in drop:
231
            changeset.drop_model(model)
232
            
233
        for model in add:
234
            changeset.add_model(model)
235
        
236
        for old_model, new_model in matches:
237
            fields, drop, add = matchmaker(old_model._meta.local_fields + old_model._meta.local_many_to_many, 
238
                                           new_model._meta.local_fields + new_model._meta.local_many_to_many,
239
                                           compareFields,
240
                                           .5)
241
            
242
            changeset.change_model(old_model)
243
            
244
            if (old_model._meta.object_name != new_model._meta.object_name):
245
                changeset.rename_model(new_model)
246
            
247
            for field in drop:
248
                changeset.drop_field(field)
249
            
250
            for field in add:
251
                changeset.add_field(field)
252
            
253
            for old, new in fields:
254
                old_snap, new_snap = snapshot.field_snap(old, old_model), snapshot.field_snap(new, new_model)
255
                
256
                if old_snap.get('__rel__'):
257
                    old_snap['__rel__'] = (old_snap['__rel__'][0].replace('migration-', ''), old_snap['__rel__'][1])
258
                
259
                if (old.name != new.name):
260
                    changeset.change_field(old, new)
261
                elif (old_snap != new_snap):
262
                    changeset.change_field(new)
263
            
264
            changeset.end_model()
265
        
266
        today = date.today()
267
        if mig_name.endswith('.py') or mig_name.endswith('.sql'):
268
            filename = mig_name
269
        else:
270
            filename = "%d-%d-%d-%s.py" % (today.year, today.month, today.day, mig_name)
271
            
272
        changeset.save(filename)  
273
    
274
        print changeset.report()
275
        
276
        if (changeset):
277
            print "\nPlease look over the changes. When you feel confident they are correct issue a 'syncdb':\n ./manage.py syncdb"

Up to file-list migratory/manager/base.py:

1
"""
2
DatabaseManagerBase acts as the base for all database managers.
3
4
__init__.py in this same folder imports the correct manager for backend
5
specified in settings.DATABASE_ENGINE.  Those manager classes extend
6
DatabaseManagerBase.
7
8
"""
9
1
10
from django.core.management.color import no_style
2
11
from django.utils import simplejson
3
12
from django.db import models
4
13
from django.db import connection
5
14
from django.core.exceptions import ImproperlyConfigured
6
qn = connection.ops.quote_name
7
15
8
16
from django.contrib.migratory.models import Migration, Snapshot
9
17
18
# Are we allowed to do this as a module level?  I am for now.
19
qn = connection.ops.quote_name  
20
10
21
class DatabaseManagerBase(object):
11
    def __init__(self, app_name=None, verbosity=0, test=False, style=None, created_models=[]):
22
    """
23
    The database manager handles both the building of old snapshots into 
24
    usable models, as well as the methods used to adjust the database 
25
    representation.
26
    
27
    The database manager allows access of models through a __getitem__ method, 
28
    so you can do:
29
        
30
        database = DatabaseManager()
31
        print database['app.Model'].objects.all()
32
    """
33
    def __init__(self, app_name=None, 
34
                       verbosity=0, 
35
                       test=False, 
36
                       style=None, 
37
                       created_models=[]):
38
        """
39
        app_name: 
40
            The default application name, allows you to say database['Model], 
41
            instead of database['app.Model']
42
        
43
        verbosity:
44
            The ./manage.py verbosity.
45
        
46
        test:
47
            Set to true, and we won't actually execute the sql.
48
            
49
        style:
50
            The ./manage.py style.
51
        
52
        created_models:
53
            In the case of a syncdb, these are the models that were just 
54
            created.
55
        """
12
56
        self.app_name = app_name    # Default app
13
57
        self.verbosity = verbosity
14
58
        self.test = test
@@ -20,6 +64,7 @@ class DatabaseManagerBase(object):
20
64
        self._rel_fields = []
21
65
                
22
66
    def parse_sig(self, sig):
67
        """Turns 'app.Model' to ('app', 'model')."""
23
68
        if '.' not in sig:
24
69
            if self.app_name:
25
70
                return self.app_name, sig
@@ -29,9 +74,11 @@ class DatabaseManagerBase(object):
29
74
            return sig.split('.')
30
75
    
31
76
    def __getitem__(self, sig):
77
        """Return the model matching the sig from the latest snapshot."""
32
78
        return self.get_model(*self.parse_sig(sig))
33
79
34
80
    def get_model(self, app_name, model_name):
81
        """Return the request model from the latest snapshot."""
35
82
        if app_name not in self._apps:
36
83
            self.build_app(app_name)
37
84
            self.build_rel_fields()
@@ -41,6 +88,7 @@ class DatabaseManagerBase(object):
41
88
            models.get_model(app_name, model_name)
42
89
    
43
90
    def get_models(self, app_name=None):
91
        """Return an iterator of models in the latest snapshot."""
44
92
        if app_name not in self._apps:
45
93
            self.build_app(app_name)
46
94
            self.build_rel_fields()
@@ -51,9 +99,14 @@ class DatabaseManagerBase(object):
51
99
                return iter( models.values() )
52
100
    
53
101
    def __contains__(self, app_name):
102
        """Does the latest snapshot include this app?"""
54
103
        return Snapshot.objects.filter(app=app_name).count() > 0
55
104
    
56
105
    def build_app(self, app_name):
106
        """
107
        Build a dict of models for the given application name from the latest 
108
        snapshot
109
        """
57
110
        if (app_name in self._apps):
58
111
            return
59
112
        try:
@@ -71,10 +124,13 @@ class DatabaseManagerBase(object):
71
124
            self.build_model(app_name, dct)
72
125
    
73
126
    def build_model(self, app_name, dct):
127
        """
128
        Builds a model with the given app_name and snapshot-dict.
129
        """
74
130
        attrs = {}
75
131
        app_name = app_name.encode()
76
132
        
77
        attrs.update( self.build_fields( app_name, dct['name'], dct['fields'], dct['meta'] ) )
133
        attrs.update( self.build_fields(app_name, dct['name'], dct['meta'], dct['fields']) )
78
134
        attrs['Meta'] = type('Meta', (), dct['meta'])
79
135
        attrs['Meta'].app_label = 'migration-' + app_name
80
136
        attrs['__module__'] = dct['module'].encode()
@@ -82,7 +138,15 @@ class DatabaseManagerBase(object):
82
138
        model = type(str(dct['name'].encode()), (models.Model,), attrs)
83
139
        self._apps[app_name][model._meta.object_name] = model
84
140
    
85
    def build_fields(self, app_name, model_name, dicts, model_meta):
141
    def build_fields(self, app_name, model_name, model_meta, dicts):
142
        """
143
        Builds a field for each dict given.
144
        
145
        This will also populate self._rel_fields with any related fields, and
146
        self._required with required apps.  Only after all the required apps 
147
        and models are built will the relative fields be added with 
148
        self.build_rel_fields() below.
149
        """
86
150
        gather = {}
87
151
        
88
152
        for dct in dicts:
@@ -99,79 +163,129 @@ class DatabaseManagerBase(object):
99
163
                if model_meta.get('order_with_respect_to') == name:
100
164
                    del model_meta['order_with_respect_to']
101
165
                    attrs['__order_with_respect_to'] = True
102
                self._rel_fields.append( (app_name, model_name, cls, name, to_app_name, to_model_name, attrs) )
166
                self._rel_fields.append( (app_name,
167
                                          model_name, 
168
                                          cls, 
169
                                          name, 
170
                                          to_app_name, 
171
                                          to_model_name, 
172
                                          attrs) )
103
173
            else:
104
174
                gather[name] = cls(**attrs)
105
175
        
106
176
        return gather
107
177
    
108
178
    def build_rel_fields(self):
179
        """
180
        Only once all the required apps and models are built, do we
181
        actually add the relative fields.
182
        """
109
183
        while self._required:
110
184
            self.build_app( self._required.pop() )
111
185
        
112
        for app_name, model_name, cls, name, to_app_name, to_model_name, attrs in self._rel_fields:
113
            order_with_respect_to = attrs.pop('__order_with_respect_to', False)
186
        for (app_name,
187
             model_name,
188
             cls,
189
             name,
190
             to_app_name,
191
             to_model_name,
192
             attrs) in self._rel_fields:
193
            
194
            order = attrs.pop('__order_with_respect_to', False)
114
195
            
115
196
            model = self.get_model(app_name, model_name)
116
197
            to_model = self.get_model(to_app_name, to_model_name)
117
198
            field = cls(to_model, **attrs)
118
199
            model.add_to_class(name, field)
119
200
            
120
            if order_with_respect_to:
201
            if order:
121
202
                model._meta.order_with_respect_to = field.name
122
203
                model._meta._prepare(model)
123
204
        
124
205
        self._rel_fields = []
125
206
    
126
207
    def _field_sql(self, model, field):
208
        """
209
        Generate the sql needed to create a field.
210
        """
127
211
        style = no_style()
128
212
        opts = model._meta
129
213
        qn = connection.ops.quote_name
130
214
        
131
215
        col_type = field.db_type()
132
216
        tablespace = field.db_tablespace or opts.db_tablespace
217
        
218
        null = 'NULL'
219
        
220
        if (not field.null):
221
            null = 'NOT NULL'
222
        
223
        sql = [
224
            style.SQL_FIELD( qn(field.column) ),
225
            style.SQL_COLTYPE( col_type ),
226
            style.SQL_KEYWORD( null ),
227
        ]
228
        
229
        if field.primary_key:
230
            sql += [
231
                style.SQL_KEYWORD('PRIMARY KEY')
232
            ]
233
        elif field.unique:
234
            sql += [
235
                style.SQL_KEYWORD('UNIQUE')
236
            ]
133
237
            
134
        # Make the definition (e.g. 'foo VARCHAR(30)') for this field.
135
        field_output = [style.SQL_FIELD(qn(field.column)), style.SQL_COLTYPE(col_type)]
136
        field_output.append(style.SQL_KEYWORD('%sNULL' % (not field.null and 'NOT ' or '')))
137
        if field.primary_key:
138
            field_output.append(style.SQL_KEYWORD('PRIMARY KEY'))
139
        elif field.unique:
140
            field_output.append(style.SQL_KEYWORD('UNIQUE'))
141
238
        if tablespace and field.unique:
142
239
            # We must specify the index tablespace inline, because we
143
240
            # won't be generating a CREATE INDEX statement for this field.
144
            field_output.append(connection.ops.tablespace_sql(tablespace, inline=True))
241
            sql += [
242
                connection.ops.tablespace_sql(tablespace, inline=True)
243
            ]
244
            
145
245
        if field.rel:
146
            part = [
246
            column = field.rel.to._meta.get_field(field.rel.field_name).column
247
            
248
            sql += [
147
249
                style.SQL_KEYWORD('REFERENCES'),
148
                style.SQL_TABLE(qn(field.rel.to._meta.db_table)),
149
                '(%s)' % style.SQL_FIELD(qn(field.rel.to._meta.get_field(field.rel.field_name).column)),
250
                style.SQL_TABLE( qn(field.rel.to._meta.db_table) ),
251
                '(%s)' % style.SQL_FIELD( qn(column) ),
150
252
            ]
253
            
151
254
            deferrable = connection.ops.deferrable_sql()
152
255
            if (deferrable):
153
                part.append(deferrable)
154
                
155
            field_output.append(' '.join(part))
256
                sql += [ 
257
                    defferable
258
                ]
156
259
        
157
        return " ".join(field_output)
260
        return " ".join(sql)
158
261
    
159
262
    def _order_with_respect_field_sql(self):
263
        """
264
        Generates the sql for the order_with_respect_to field.
265
        """
160
266
        style = self.style
161
267
        return " ".join([
162
            style.SQL_FIELD(qn('_order')),
163
            style.SQL_COLTYPE(models.IntegerField().db_type()),
268
            style.SQL_FIELD( qn('_order') ),
269
            style.SQL_COLTYPE( models.IntegerField().db_type() ),
164
270
            style.SQL_KEYWORD('NULL'),
165
271
        ])
166
272
    
167
273
    def _unique_statement_sql(self, opts, field_constraints):
274
        """
275
        Generates the sql for specifying unique fields.
276
        """
168
277
        style = self.style
278
        
169
279
        return '%s (%s)' % (
170
280
            style.SQL_KEYWORD('UNIQUE'),
171
281
            ", ".join([style.SQL_FIELD(qn(opts.get_field(f).column)) for f in field_constraints]),
172
282
        )
173
283
    
174
284
    def _model_sql(self, model):
285
        """
286
        Generates the sql for the model.
287
        """
288
        
175
289
        from django.db import models
176
290
        style = no_style()
177
291
        opts = model._meta
@@ -182,16 +296,16 @@ class DatabaseManagerBase(object):
182
296
            fields.append(self._order_with_respect_field_sql())
183
297
        
184
298
        for field_constraints in opts.unique_together:
185
            fields.append( self._unique_statement_sql(opts, field_constraints) )
299
            fields.append(self._unique_statement_sql(opts, field_constraints))
186
300
        
187
301
        sql = [
188
302
            style.SQL_KEYWORD('CREATE TABLE'),
189
303
            style.SQL_TABLE(qn(opts.db_table)),
190
304
            '(\n    %s\n)' % ',\n    '.join(fields),
191
305
        ]
192
        
193
        if opts.has_auto_field:
194
            # Add any extra SQL needed to support auto-incrementing primary keys.
306
307
        # Add any extra SQL needed to support auto-incrementing primary keys.
308
        if opts.has_auto_field:            
195
309
            auto_column = opts.auto_field.db_column or opts.auto_field.name
196
310
            autoinc_sql = connection.ops.autoinc_sql(opts.db_table, auto_column)
197
311
            if autoinc_sql:
@@ -201,15 +315,18 @@ class DatabaseManagerBase(object):
201
315
        return " ".join(sql)
202
316
    
203
317
    def _m2m_sql(self, model, field):
204
        "Return the CREATE TABLE statements for a single m2m field"
318
        """
319
        Generates the sql for a many-to-many field.
320
        """
205
321
        opts = model._meta
206
322
        style = self.style
207
323
        
208
        tablespace_sql = connection.ops.tablespace_sql(field.db_tablespace or opts.db_tablespace, inline=True)
324
        tablespace = field.db_tablespace or opts.db_tablespace
325
        tablespace_sql = connection.ops.tablespace_sql(tablespace,inline=True)
209
326
        
210
327
        auto_sql = [
211
            style.SQL_FIELD(qn('id')),
212
            style.SQL_COLTYPE(models.AutoField(primary_key=True).db_type()),
328
            style.SQL_FIELD( qn('id') ),
329
            style.SQL_COLTYPE( models.AutoField(primary_key=True).db_type() ),
213
330
            style.SQL_KEYWORD('NOT NULL PRIMARY KEY'),
214
331
        ]
215
332
        if (tablespace_sql): auto_sql += [ tablespace_sql ]
@@ -266,6 +383,9 @@ class DatabaseManagerBase(object):
266
383
        return sql
267
384
        
268
385
    def _drop_m2m_sql(self, model, field):
386
        """
387
        Generates the sql to drop a many-to-many table.
388
        """
269
389
        style = self.style
270
390
        opts = model._meta
271
391
        
@@ -277,6 +397,9 @@ class DatabaseManagerBase(object):
277
397
        return " ".join(sql)
278
398
        
279
399
    def _rename_m2m_sql(self, model, new_field, old_field):
400
        """
401
        Generates the sql to rename a many-to-many table.
402
        """
280
403
        style = self.style
281
404
        
282
405
        rename_sql = [
@@ -285,9 +408,21 @@ class DatabaseManagerBase(object):
285
408
            style.SQL_KEYWORD('RENAME TO'),
286
409
            style.SQL_TABLE( qn(new_field.m2m_db_table()) )
287
410
        ]
411
        
288
412
        return " ".join(sql)
289
413
    
290
414
    def execute(self, description, *sql_statements):
415
        """
416
        Executes the sql statements and prints the description to the user, 
417
        unless we are in test mode (see __init__), where the description will 
418
        print but the sql statements won't actually be executed.
419
        
420
        Don't worry about adding a ';' to the end of each statement, it will 
421
        be tacked on.
422
        
423
        Also, each statement can be a list, in which case they are joined by a 
424
        space.
425
        """
291
426
        cursor = connection.cursor()
292
427
293
428
        if self.verbosity >= 1:
@@ -314,8 +449,9 @@ class DatabaseManagerBase(object):
314
449
        
315
450
        The field should be defined in the current models.py
316
451
        
317
        TODO: This won't work, because we might be in a long-ago migration refering 
318
        to models.py that doesn't have the field, or model for that matter. Big problem.
452
        TODO: This won't work, because we might be in a long-ago migration 
453
        refering to models.py that doesn't have the field, or model for that 
454
        matter. Big problem.
319
455
        """
320
456
        style = self.style
321
457
        
@@ -342,7 +478,7 @@ class DatabaseManagerBase(object):
342
478
        """
343
479
        Change a field to the new representation.
344
480
        
345
        To rename a field, specify a new_field_name, otherwise leave it None.
481
        To rename a field, specify a new_field_name, otherwise ignore it.
346
482
        """
347
483
        style = self.style
348
484
        
@@ -367,7 +503,11 @@ class DatabaseManagerBase(object):
367
503
                self.drop_field(model_sig, old_field_name)
368
504
                self.execute("Adding many-to-many table for field %s.%s" % (model_sig, new_field_name), self._m2m_sql(model, field))
369
505
                return
370
            raise NotImplimentedError("Need to rename the many-to-many field.")
506
            if (old.m2m_db_table() != new.m2m_db_table()):
507
                self.execute(
508
                    "Renaming the many-to-many table for field %s.%s" % (model_sig, new_field_name), 
509
                    self._rename_m2m_sql(model, old, field))
510
                return
371
511
        
372
512
        opts.local_fields = [f for f in opts.local_fields if f != old]
373
513
        model.add_to_class(new_field_name, field)
@@ -417,6 +557,11 @@ class DatabaseManagerBase(object):
417
557
        self.execute("Dropping field for %s.%s" % (model_sig, field.name), sql)
418
558
    
419
559
    def add_model(self, model_sig):
560
        """
561
        Adds the model.
562
        
563
        Unless it shows up in self.created_models (see __init__).
564
        """
420
565
        model = models.get_model(*self.parse_sig(model_sig))
421
566
        
422
567
        if (model in self.created_models):
@@ -430,6 +575,9 @@ class DatabaseManagerBase(object):
430
575
            self.execute("Adding many-to-many table for field %s.%s" % (model_sig, field.name), self._m2m_sql(model, field))
431
576
    
432
577
    def rename_model(self, model_sig, new_sig):
578
        """
579
        Rename a model.
580
        """
433
581
        old_model = self[model_sig]
434
582
        
435
583
        app_name, model_name = self.parse_sig(new_sig)
@@ -438,23 +586,29 @@ class DatabaseManagerBase(object):
438
586
        
439
587
        style = self.style
440
588
        
441
        # Have to drop the one created by the sync db.  TODO: Add ManyToMany support.
442
        drop_sql = [
589
        # Have to drop the one created by the sync db.  
590
        # TODO: Add ManyToMany support.
591
        # TODO: Check through self.created_models before doing this.
592
        sql = [
443
593
            style.SQL_KEYWORD('DROP TABLE'),
444
594
            style.SQL_TABLE(qn(new_model._meta.db_table))
445
595
        ]
446
596
        
447
        rename_sql = [
597
        self.execute('Dropping eroneously created table for %s.' % new_sig, sql)
598
        
599
        sql = [
448
600
            style.SQL_KEYWORD('ALTER TABLE'),
449
601
            style.SQL_TABLE(qn(old_model._meta.db_table)),
450
602
            style.SQL_KEYWORD('RENAME TO'),
451
603
            style.SQL_TABLE(qn(new_model._meta.db_table))
452
604
        ]
453
605
        
454
        self.execute('Dropping eroneously created table for %s.' % new_sig, drop_sql)
455
        self.execute("Renaming model %s to %s." % (model_sig, new_sig), rename_sql)
606
        self.execute("Renaming model %s to %s." % (model_sig, new_sig), sql)
456
607
    
457
608
    def drop_model(self, model_sig):
609
        """
610
        Drops a model.
611
        """
458
612
        model = self[model_sig]
459
613
        style = self.style
460
614
        

Up to file-list migratory/manager/sqlite3.py:

@@ -4,12 +4,16 @@ from django.db import connection, models
4
4
qn = connection.ops.quote_name
5
5
6
6
class DatabaseManager(DatabaseManagerBase):
7
    """
8
    SQLite //seems// like a good idea, until here.  Since it does not support
9
    propper ALTER TABLE syntax, we must fuss around a lot.
10
    """
11
    
7
12
    def add_field(self, model_sig, name):
8
13
        """
9
            SQLite3 doesn't support a general ALTER TABLE syntax.  So we have to
10
            create a temporary table, move the values into that, then create
11
            a new table with our added field, and move the values back to it.
12
            Hurray!
14
        We haveto create a temporary table, move the values into that, then 
15
        create a new table with our added field, and move the values back 
16
        to it. Hurray!
13
17
        """
14
18
        
15
19
        app_name, model_name = self.parse_sig(model_sig)
@@ -53,11 +57,13 @@ class DatabaseManager(DatabaseManagerBas
53
57
        default = 'NULL'
54
58
        if (field.has_default()):
55
59
            if (field.rel):
56
                default = repr( field.get_db_prep_save(field.default._get_pk_val()) )   # TODO: repr is wrong, what do we do here?
60
                # TODO: repr is wrong, what do we do here?
61
                val = field.get_db_prep_save(field.default._get_pk_val())
62
                default = repr( val )
57
63
            else:
58
64
                default = field.get_db_prep_save(field.get_default())
59
65
        
60
        # Figure out the new column order, and add the default to the new hole.
66
        # Figure out new column order, and add the default to the new hole.
61
67
        fieldset = set(fields)
62
68
        columns = []
63
69
        for f in opts.local_fields:
@@ -90,8 +96,8 @@ class DatabaseManager(DatabaseManagerBas
90
96
91
97
    def change_field(self, model_sig, old_field_name, new_field_name=None):
92
98
        """
93
            SQLite3 doesn't support a general ALTER TABLE syntax.  So we have to do
94
            what we do above for add_field and drop_field.
99
            SQLite3 doesn't support a general ALTER TABLE syntax.  So we 
100
            have to do what we do above for add_field and drop_field.
95
101
        """
96
102
97
103
        if not new_field_name:

Up to file-list migratory/matchmaker.py:

1
"""
2
Defines ways of matching up to sets of objects using an arbitrary comparison 
3
function.  
4
5
matchmaker() and compare_strings() are the functions of note here.
6
"""
7
1
8
from array import array
2
9
from bisect import insort
3
10
try:
@@ -9,10 +16,13 @@ def levenshtein(a, b):
9
16
    """
10
17
    Levenshtein string distance algorithm from:
11
18
    http://en.wikibooks.org/wiki/Algorithm_implementation/Strings/Levenshtein_distance
12
    Well, this is the Lisp implimentation, the python one was amazingly elegant, and
13
    so, shreaded the stack.  This one uses the table algorithm rather than recursion.
19
    
20
    Well, this is the Lisp implimentation, the python one was amazingly 
21
    elegant, and so, shreaded the stack.  This one uses the table version of 
22
    this algorithm rather than the recursive one.
14
23
15
    It computes the number of insertions or deletions needed to get from a to b.
24
    It computes the number of insertions or deletions needed to get from
25
    strings a to b.
16
26
    """
17
27
18
28
    len_a, len_b = len(a), len(b)
@@ -52,9 +62,9 @@ def compare_strings(a, b):
52
62
def matchmaker(a, b, compareFunc, threshold=.25):
53
63
    """ 
54
64
    Creates a list of matches for the two lists, using the compareFunc as the
55
    test. It does this by going through and comparing each element in a to each
56
    element in b, and then deciding based on the coefficient returned from
57
    compareFunc, which elements in a 'match up' with those in 'b'.
65
    test. It does this by going through and comparing each element in a to 
66
    each element in b, and then deciding based on the coefficient returned 
67
    from compareFunc, which elements in a 'match up' with those in 'b'.
58
68
59
69
    In terms of people, imagine a list of heterosexual males and females,
60
70
    compareFunc spits out a percentage likelihood that the any given
@@ -93,61 +103,3 @@ def matchmaker(a, b, compareFunc, thresh
93
103
        males.remove(male)
94
104
    
95
105
    return matches, list(females), list(males)
96
97
def test_random(males, females):
98
    import random
99
    random.seed(1)
100
    def comparison(a, b):
101
        return random.random()
102
    
103
    matches, lonely, _ = matchmaker(males, females, comparison, .75)
104
    matches = dict(matches)
105
    assert matches['christopher'] == 'linda'
106
    assert matches['thomas'] == 'maria'
107
    assert matches['william'] == 'susan'
108
    assert 'michael' in lonely
109
    assert 'daniel' in lonely
110
    print "Passed."
111
112
def test_levenshtein(males, females):
113
    print matchmaker(males, females, compare_strings, .25)
114
115
if __name__ == '__main__':
116
    males = [   
117
        'james',
118
        'john',
119
        'robert',
120
        'michael',
121
        'william',
122
        'david',
123
        'richard',
124
        'charles',
125
        'joseph',
126
        'thomas',
127
        'christopher',
128
        'daniel',
129
    ]
130
    
131
    females = [
132
        'mary',
133
        'patricia',
134
        'linda',
135
        'barbara',
136
        'elizabeth',
137
        'jennifer',
138
        'maria',
139
        'susan',
140
        'margaret',
141
        'dorothy',
142
        'lisa',
143
        'nancy',
144
        'karen',
145
    ]
146
    
147
    test_random(males, females)
148
    
149
    import cProfile
150
    def go():
151
        test_levenshtein(males, females)
152
    
153
    cProfile.run('go()')

Up to file-list migratory/migrate.py:

1
"""
2
Performs any new migrations.
3
4
This is not to be confused with the ./manage.py command which creates 
5
migration files rather than actually performing them.
6
"""
1
7
import os
2
from django.contrib.migratory.models import Snapshot, Migration
3
8
4
def do_migrations(sender=None, app=None, created_models=None, verbosity=None, interactive=None, signal=None, **_):
9
def do_migrations(sender=None, 
10
                  app=None, 
11
                  created_models=None, 
12
                  verbosity=None, 
13
                  interactive=None, 
14
                  signal=None, 
15
                  **_):
16
    """
17
    Performs all new migrations after ./manage.py syncdb.
18
    
19
    This is connected to the post_syncdb in the models.py file.
20
    """
5
21
    from django.contrib.migratory.manager import DatabaseManager
22
    from django.contrib.migratory.models import Snapshot, Migration
6
23
    
7
24
    if (verbosity > 1):
8
25
        print "\tChecking for migrations...",
@@ -13,7 +30,9 @@ def do_migrations(sender=None, app=None,
13
30
    if (verbosity > 1): print "%s found." % len(migrations.keys())
14
31
    
15
32
    if migrations:
16
        database_manager = DatabaseManager(app_name, verbosity=verbosity, created_models=created_models)
33
        database_manager = DatabaseManager(app_name, 
34
                                           verbosity=verbosity,     
35
                                           created_models=created_models)
17
36
        
18
37
        for name, path in migrations.items():
19
38
            if (verbosity >= 1): print "Executing migration %r..." % name
@@ -22,7 +41,8 @@ def do_migrations(sender=None, app=None,
22
41
23
42
def run_migration(path, database_manager, verbosity):
24
43
    if not os.path.exists(path):
25
        raise RuntimeError("Unable to find migration at the given path: %r" % path)
44
        raise RuntimeError(
45
            "Unable to find migration at the given path: %r" % path)
26
46
    
27
47
    o = open(path)
28
48
    src = o.read()
@@ -59,6 +79,8 @@ def run_migration(path, database_manager
59
79
        print "Nevermind, no up() defined."
60
80
61
81
def get_migrations(app, verbosity):
82
    from django.contrib.migratory.models import Snapshot, Migration
83
    
62
84
    app_name = app.__name__.split('.')[-2]
63
85
    
64
86
    folder = os.path.join(os.path.dirname(app.__file__), 'migrations')
@@ -68,7 +90,8 @@ def get_migrations(app, verbosity):
68
90
    manifest = eval(open(manifest).read())
69
91
    
70
92
    if Snapshot.objects.filter(app=app_name).count() == 0:
71
        # Since we don't have a snapshot, the app is being created, and so all migrations should be marked as done.
93
        # Since we don't have a snapshot, the app is being created, and so all 
94
        # migrations should be marked as done.
72
95
        for name in manifest:
73
96
            if (verbosity >= 1): print "Assuming migrating %r..." % name
74
97
            Migration.objects.get_or_create(name=name)
@@ -80,7 +103,7 @@ def get_migrations(app, verbosity):
80
103
        if Migration.objects.filter(name=migration).count() > 0:
81
104
            continue
82
105
        if not os.path.exists(path):
83
            raise RuntimeError("Cannot find a migration present in the __manifest__.py file.")
106
            raise RuntimeError( "Cannot find a migration present in the __manifest__.py file.")
84
107
        gather[migration] = path
85
108
    
86
109
    return gather

Up to file-list migratory/models.py:

@@ -7,7 +7,10 @@ class Snapshot(models.Model):
7
7
    app = models.CharField(max_length=255)
8
8
    
9
9
    def __unicode__(self):
10
        return "Snapshot of %r on %s" % (str(self.app), DateFormat(self.created).format("F jS"))
10
        return "Snapshot of %r on %s" % (
11
            str(self.app),
12
            DateFormat(self.created).format("F jS")
13
        )
11
14
        
12
15
class Migration(models.Model):
13
16
    name = models.CharField(max_length=255)

Up to file-list migratory/snapshot.py:

1
"""
2
Handling of 'snapshots' or representations of application models that are 
3
saved in the database.
4
"""
5
1
6
from django.utils import simplejson
2
7
from django.db import models
3
8
from django.db.models.fields import NOT_PROVIDED
4
9
5
from django.contrib.migratory.models import Snapshot
6
10
7
def create_snapshots(sender=None, app=None, created_models=None, verbosity=None, interactive=None, signal=None, **_):
11
def create_snapshots(sender=None, 
12
                     app=None, 
13
                     created_models=None,
14
                     verbosity=None,
15
                     interactive=None,
16
                     signal=None, 
17
                     **_):
18
    """
19
    Creates snapshots of the all models in the triggered app.
20
    """
21
    
22
    from django.contrib.migratory.models import Snapshot
23
                     
8
24
    app_name = app.__name__.split('.')[-2]
9
25
    
10
26
    if (verbosity > 1):
@@ -22,6 +38,17 @@ def create_snapshots(sender=None, app=No
22
38
    snapshot.save()
23
39
24
40
def model_snap(model):
41
    """
42
    Create a snapshot (at this point just a dictionary) of the model by 
43
    analyzing its fields and attributes.
44
    
45
    If we could edit the django source, it would be best for the model to 
46
    create a snapshot of itself.  At initialization, it could record all of 
47
    its options and save them away.  As it stands, we have to manually go 
48
    through and infer options by attributes.
49
    
50
    TODO: Handle 'proxy' values.
51
    """
25
52
    opts = model._meta
26
53
27
54
    if (opts.order_with_respect_to):
@@ -29,6 +56,8 @@ def model_snap(model):
29
56
    else:
30
57
        order_with_respect_to = None
31
58
59
    all_fields = opts.local_fields + opts.many_to_many
60
32
61
    return {
33
62
        'name': model._meta.object_name,
34
63
        'module': model.__module__,
@@ -41,15 +70,27 @@ def model_snap(model):
41
70
            'ordering': opts.ordering,
42
71
            'permissions': opts.permissions,
43
72
            'verbose_name': opts.verbose_name_raw,
44
            #'verbose_name_plural': opts.verbose_name_plural,  # __proxy__ wha?
73
74
            # __proxy__ wha?
75
            #'verbose_name_plural': opts.verbose_name_plural,  
45
76
        },
46
        'fields': [field_snap(f, model) for f in opts.fields + opts.many_to_many]
77
        'fields': [field_snap(f, model) for f in all_fields]
47
78
    }
48
79
        
49
80
def field_snap(field, model):
81
    """
82
    Grabs a dictionary snapshot of the field.
83
    
84
    This one is particularly hard, because we can't know for sure what options 
85
    were given to the field on init.  The best we can do is go through a list 
86
    of known options, defined below as 'FIELD_OPTIONS'.
87
    
88
    The Django field base should really provide a better mechanism for this.  
89
    It wouldn't be too hard, and might help codify the system.
90
    """
50
91
    gather = {}
51
92
    
52
    for k in field_attrs:
93
    for k in FIELD_OPTIONS:
53
94
        if hasattr(field, k):
54
95
            v = getattr(field, k)
55
96
            if hasattr(v, '__promise__'):
@@ -66,7 +107,8 @@ def field_snap(field, model):
66
107
    gather['__class__'] = field.__class__.__name__
67
108
    return gather
68
109
69
field_attrs = [
110
# Known field attributes that can be regarded as options in the constructor.
111
FIELD_OPTIONS = [
70
112
    # Base
71
113
    'name',
72
114
    'null',