1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011 MediaGoblin contributors. See AUTHORS.
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 def _simple_printer(string
):
22 Prints a string, but without an auto \n at the end.
24 sys
.stdout
.write(string
)
28 class MigrationManager(object):
30 Migration handling tool.
32 Takes information about a database, lets you update the database
33 to the latest migrations, etc.
36 def __init__(self
, name
, models
, migration_registry
, session
,
37 printer
=_simple_printer
):
40 - name: identifier of this section of the database
41 - session: session we're going to migrate
42 - migration_registry: where we should find all migrations to
47 self
.session
= session
48 self
.migration_registry
= migration_registry
49 self
._sorted
_migrations
= None
50 self
.printer
= printer
53 from mediagoblin
.db
.sql
.models
import MigrationData
55 self
.migration_model
= MigrationData
56 self
.migration_table
= MigrationData
.__table
__
59 def sorted_migrations(self
):
61 Sort migrations if necessary and store in self._sorted_migrations
63 if not self
._sorted
_migrations
:
64 self
._sorted
_migrations
= sorted(
65 self
.migration_registry
.items(),
66 # sort on the key... the migration number
67 key
=lambda migration_tuple
: migration_tuple
[0])
69 return self
._sorted
_migrations
72 def migration_data(self
):
74 Get the migration row associated with this object, if any.
76 return self
.session
.query(
77 self
.migration_model
).filter_by(name
=self
.name
).first()
80 def latest_migration(self
):
82 Return a migration number for the latest migration, or 0 if
83 there are no migrations.
85 if self
.sorted_migrations
:
86 return self
.sorted_migrations
[-1][0]
88 # If no migrations have been set, we start at 0.
92 def database_current_migration(self
):
94 Return the current migration in the database.
96 # If the table doesn't even exist, return None.
97 if not self
.migration_table
.exists(self
.session
.bind
):
100 # Also return None if self.migration_data is None.
101 if self
.migration_data
is None:
104 return self
.migration_data
.version
106 def set_current_migration(self
, migration_number
=None):
108 Set the migration in the database to migration_number
109 (or, the latest available)
111 self
.migration_data
.version
= migration_number
or self
.latest_migration
112 self
.session
.commit()
114 def migrations_to_run(self
):
116 Get a list of migrations to run still, if any.
118 Note that this will fail if there's no migration record for
121 assert self
.database_current_migration
is not None
123 db_current_migration
= self
.database_current_migration
126 (migration_number
, migration_func
)
127 for migration_number
, migration_func
in self
.sorted_migrations
128 if migration_number
> db_current_migration
]
131 def init_tables(self
):
133 Create all tables relative to this package
135 # sanity check before we proceed, none of these should be created
136 for model
in self
.models
:
137 # Maybe in the future just print out a "Yikes!" or something?
138 assert not model
.__table
__.exists(self
.session
.bind
)
140 self
.migration_model
.metadata
.create_all(
142 tables
=[model
.__table
__ for model
in self
.models
])
144 def create_new_migration_record(self
):
146 Create a new migration record for this migration set
148 migration_record
= self
.migration_model(
150 version
=self
.latest_migration
)
151 self
.session
.add(migration_record
)
152 self
.session
.commit()
156 Print out a dry run of what we would have upgraded.
158 if self
.database_current_migration
is None:
160 u
'~> Woulda initialized: %s\n' % self
.name_for_printing())
163 migrations_to_run
= self
.migrations_to_run()
164 if migrations_to_run
:
166 u
'~> Woulda updated %s:\n' % self
.name_for_printing())
168 for migration_number
, migration_func
in migrations_to_run():
170 u
' + Would update %s, "%s"\n' % (
171 migration_number
, migration_func
.func_name
))
175 def name_for_printing(self
):
176 if self
.name
== u
'__main__':
177 return u
"main mediagoblin tables"
179 # TODO: Use the friendlier media manager "human readable" name
180 return u
'media type "%s"' % self
.name
182 def init_or_migrate(self
):
184 Initialize the database or migrate if appropriate.
186 Returns information about whether or not we initialized
187 ('inited'), migrated ('migrated'), or did nothing (None)
189 assure_migrations_table_setup(self
.session
)
191 # Find out what migration number, if any, this database data is at,
192 # and what the latest is.
193 migration_number
= self
.database_current_migration
195 # Is this our first time? Is there even a table entry for
198 # - create all tables
199 # - create record in migrations registry
200 # - print / inform the user
202 if migration_number
is None:
203 self
.printer(u
"-> Initializing %s... " % self
.name_for_printing())
206 # auto-set at latest migration number
207 self
.create_new_migration_record()
209 self
.printer(u
"done.\n")
210 self
.set_current_migration()
213 # Run migrations, if appropriate.
214 migrations_to_run
= self
.migrations_to_run()
215 if migrations_to_run
:
217 u
'-> Updating %s:\n' % self
.name_for_printing())
218 for migration_number
, migration_func
in migrations_to_run
:
220 u
' + Running migration %s, "%s"... ' % (
221 migration_number
, migration_func
.func_name
))
222 migration_func(self
.session
)
223 self
.printer('done.\n')
225 self
.set_current_migration()
228 # Otherwise return None. Well it would do this anyway, but
233 class RegisterMigration(object):
235 Tool for registering migrations
239 @RegisterMigration(33)
240 def update_dwarves(database):
243 This will register your migration with the default migration
244 registry. Alternately, to specify a very specific
245 migration_registry, you can pass in that as the second argument.
247 Note, the number of your migration should NEVER be 0 or less than
248 0. 0 is the default "no migrations" state!
250 def __init__(self
, migration_number
, migration_registry
):
251 assert migration_number
> 0, "Migration number must be > 0!"
252 assert migration_number
not in migration_registry
, \
253 "Duplicate migration numbers detected! That's not allowed!"
255 self
.migration_number
= migration_number
256 self
.migration_registry
= migration_registry
258 def __call__(self
, migration
):
259 self
.migration_registry
[self
.migration_number
] = migration
263 def assure_migrations_table_setup(db
):
265 Make sure the migrations table is set up in the database.
267 from mediagoblin
.db
.sql
.models
import MigrationData
269 if not MigrationData
.__table
__.exists(db
.bind
):
270 MigrationData
.metadata
.create_all(
271 db
.bind
, tables
=[MigrationData
.__table
__])