1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 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/>.
17 from mediagoblin
.tools
.common
import simple_printer
18 from sqlalchemy
import Table
19 from sqlalchemy
.sql
import select
21 class TableAlreadyExists(Exception):
25 class MigrationManager(object):
27 Migration handling tool.
29 Takes information about a database, lets you update the database
30 to the latest migrations, etc.
33 def __init__(self
, name
, models
, foundations
, migration_registry
, session
,
34 printer
=simple_printer
):
37 - name: identifier of this section of the database
38 - session: session we're going to migrate
39 - migration_registry: where we should find all migrations to
42 self
.name
= unicode(name
)
44 self
.foundations
= foundations
45 self
.session
= session
46 self
.migration_registry
= migration_registry
47 self
._sorted
_migrations
= None
48 self
.printer
= printer
51 from mediagoblin
.db
.models
import MigrationData
53 self
.migration_model
= MigrationData
54 self
.migration_table
= MigrationData
.__table
__
57 def sorted_migrations(self
):
59 Sort migrations if necessary and store in self._sorted_migrations
61 if not self
._sorted
_migrations
:
62 self
._sorted
_migrations
= sorted(
63 self
.migration_registry
.items(),
64 # sort on the key... the migration number
65 key
=lambda migration_tuple
: migration_tuple
[0])
67 return self
._sorted
_migrations
70 def migration_data(self
):
72 Get the migration row associated with this object, if any.
74 return self
.session
.query(
75 self
.migration_model
).filter_by(name
=self
.name
).first()
78 def latest_migration(self
):
80 Return a migration number for the latest migration, or 0 if
81 there are no migrations.
83 if self
.sorted_migrations
:
84 return self
.sorted_migrations
[-1][0]
86 # If no migrations have been set, we start at 0.
90 def database_current_migration(self
):
92 Return the current migration in the database.
94 # If the table doesn't even exist, return None.
95 if not self
.migration_table
.exists(self
.session
.bind
):
98 # Also return None if self.migration_data is None.
99 if self
.migration_data
is None:
102 return self
.migration_data
.version
104 def set_current_migration(self
, migration_number
=None):
106 Set the migration in the database to migration_number
107 (or, the latest available)
109 self
.migration_data
.version
= migration_number
or self
.latest_migration
110 self
.session
.commit()
112 def migrations_to_run(self
):
114 Get a list of migrations to run still, if any.
116 Note that this will fail if there's no migration record for
119 assert self
.database_current_migration
is not None
121 db_current_migration
= self
.database_current_migration
124 (migration_number
, migration_func
)
125 for migration_number
, migration_func
in self
.sorted_migrations
126 if migration_number
> db_current_migration
]
129 def init_tables(self
):
131 Create all tables relative to this package
133 # sanity check before we proceed, none of these should be created
134 for model
in self
.models
:
135 # Maybe in the future just print out a "Yikes!" or something?
136 if model
.__table
__.exists(self
.session
.bind
):
137 raise TableAlreadyExists(
138 u
"Intended to create table '%s' but it already exists" %
139 model
.__table
__.name
)
141 self
.migration_model
.metadata
.create_all(
143 tables
=[model
.__table
__ for model
in self
.models
])
145 def populate_table_foundations(self
):
147 Create the table foundations (default rows) as layed out in FOUNDATIONS
148 in mediagoblin.db.models
150 for Model
, rows
in self
.foundations
.items():
151 self
.printer(u
' + Laying foundations for %s table\n' %
153 for parameters
in rows
:
154 new_row
= Model(**parameters
)
155 self
.session
.add(new_row
)
157 def create_new_migration_record(self
):
159 Create a new migration record for this migration set
161 migration_record
= self
.migration_model(
163 version
=self
.latest_migration
)
164 self
.session
.add(migration_record
)
165 self
.session
.commit()
169 Print out a dry run of what we would have upgraded.
171 if self
.database_current_migration
is None:
173 u
'~> Woulda initialized: %s\n' % self
.name_for_printing())
176 migrations_to_run
= self
.migrations_to_run()
177 if migrations_to_run
:
179 u
'~> Woulda updated %s:\n' % self
.name_for_printing())
181 for migration_number
, migration_func
in migrations_to_run():
183 u
' + Would update %s, "%s"\n' % (
184 migration_number
, migration_func
.func_name
))
188 def name_for_printing(self
):
189 if self
.name
== u
'__main__':
190 return u
"main mediagoblin tables"
192 return u
'plugin "%s"' % self
.name
194 def init_or_migrate(self
):
196 Initialize the database or migrate if appropriate.
198 Returns information about whether or not we initialized
199 ('inited'), migrated ('migrated'), or did nothing (None)
201 assure_migrations_table_setup(self
.session
)
203 # Find out what migration number, if any, this database data is at,
204 # and what the latest is.
205 migration_number
= self
.database_current_migration
207 # Is this our first time? Is there even a table entry for
210 # - create all tables
211 # - create record in migrations registry
212 # - print / inform the user
214 if migration_number
is None:
215 self
.printer(u
"-> Initializing %s... " % self
.name_for_printing())
218 # auto-set at latest migration number
219 self
.create_new_migration_record()
220 self
.printer(u
"done.\n")
221 self
.populate_table_foundations()
222 self
.set_current_migration()
225 # Run migrations, if appropriate.
226 migrations_to_run
= self
.migrations_to_run()
227 if migrations_to_run
:
229 u
'-> Updating %s:\n' % self
.name_for_printing())
230 for migration_number
, migration_func
in migrations_to_run
:
232 u
' + Running migration %s, "%s"... ' % (
233 migration_number
, migration_func
.func_name
))
234 migration_func(self
.session
)
235 self
.set_current_migration(migration_number
)
236 self
.printer('done.\n')
240 # Otherwise return None. Well it would do this anyway, but
245 class RegisterMigration(object):
247 Tool for registering migrations
251 @RegisterMigration(33)
252 def update_dwarves(database):
255 This will register your migration with the default migration
256 registry. Alternately, to specify a very specific
257 migration_registry, you can pass in that as the second argument.
259 Note, the number of your migration should NEVER be 0 or less than
260 0. 0 is the default "no migrations" state!
262 def __init__(self
, migration_number
, migration_registry
):
263 assert migration_number
> 0, "Migration number must be > 0!"
264 assert migration_number
not in migration_registry
, \
265 "Duplicate migration numbers detected! That's not allowed!"
267 self
.migration_number
= migration_number
268 self
.migration_registry
= migration_registry
270 def __call__(self
, migration
):
271 self
.migration_registry
[self
.migration_number
] = migration
275 def assure_migrations_table_setup(db
):
277 Make sure the migrations table is set up in the database.
279 from mediagoblin
.db
.models
import MigrationData
281 if not MigrationData
.__table
__.exists(db
.bind
):
282 MigrationData
.metadata
.create_all(
283 db
.bind
, tables
=[MigrationData
.__table
__])
286 def inspect_table(metadata
, table_name
):
287 """Simple helper to get a ref to an already existing table"""
288 return Table(table_name
, metadata
, autoload
=True,
289 autoload_with
=metadata
.bind
)
291 def replace_table_hack(db
, old_table
, replacement_table
):
293 A function to fully replace a current table with a new one for migrati-
294 -ons. This is necessary because some changes are made tricky in some situa-
295 -tion, for example, dropping a boolean column in sqlite is impossible w/o
298 :param old_table A ref to the old table, gotten through
301 :param replacement_table A ref to the new table, gotten through
304 Users are encouraged to sqlalchemy-migrate replace table solutions, unless
305 that is not possible... in which case, this solution works,
308 surviving_columns
= replacement_table
.columns
.keys()
309 old_table_name
= old_table
.name
310 for row
in db
.execute(select(
311 [column
for column
in old_table
.columns
312 if column
.name
in surviving_columns
])):
314 db
.execute(replacement_table
.insert().values(**row
))
320 replacement_table
.rename(old_table_name
)