2017cfc0b429b47497353d9b7c938b4fa6026c96
[mediagoblin.git] / mediagoblin / db / util.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
3 #
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.
8 #
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.
13 #
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/>.
16 import sys
17 from mediagoblin.db.base import Session
18 from mediagoblin.db.models import MediaEntry, Tag, MediaTag, Collection
19
20 from mediagoblin.tools.common import simple_printer
21
22
23 class MigrationManager(object):
24 """
25 Migration handling tool.
26
27 Takes information about a database, lets you update the database
28 to the latest migrations, etc.
29 """
30
31 def __init__(self, name, models, migration_registry, session,
32 printer=simple_printer):
33 """
34 Args:
35 - name: identifier of this section of the database
36 - session: session we're going to migrate
37 - migration_registry: where we should find all migrations to
38 run
39 """
40 self.name = unicode(name)
41 self.models = models
42 self.session = session
43 self.migration_registry = migration_registry
44 self._sorted_migrations = None
45 self.printer = printer
46
47 # For convenience
48 from mediagoblin.db.models import MigrationData
49
50 self.migration_model = MigrationData
51 self.migration_table = MigrationData.__table__
52
53 @property
54 def sorted_migrations(self):
55 """
56 Sort migrations if necessary and store in self._sorted_migrations
57 """
58 if not self._sorted_migrations:
59 self._sorted_migrations = sorted(
60 self.migration_registry.items(),
61 # sort on the key... the migration number
62 key=lambda migration_tuple: migration_tuple[0])
63
64 return self._sorted_migrations
65
66 @property
67 def migration_data(self):
68 """
69 Get the migration row associated with this object, if any.
70 """
71 return self.session.query(
72 self.migration_model).filter_by(name=self.name).first()
73
74 @property
75 def latest_migration(self):
76 """
77 Return a migration number for the latest migration, or 0 if
78 there are no migrations.
79 """
80 if self.sorted_migrations:
81 return self.sorted_migrations[-1][0]
82 else:
83 # If no migrations have been set, we start at 0.
84 return 0
85
86 @property
87 def database_current_migration(self):
88 """
89 Return the current migration in the database.
90 """
91 # If the table doesn't even exist, return None.
92 if not self.migration_table.exists(self.session.bind):
93 return None
94
95 # Also return None if self.migration_data is None.
96 if self.migration_data is None:
97 return None
98
99 return self.migration_data.version
100
101 def set_current_migration(self, migration_number=None):
102 """
103 Set the migration in the database to migration_number
104 (or, the latest available)
105 """
106 self.migration_data.version = migration_number or self.latest_migration
107 self.session.commit()
108
109 def migrations_to_run(self):
110 """
111 Get a list of migrations to run still, if any.
112
113 Note that this will fail if there's no migration record for
114 this class!
115 """
116 assert self.database_current_migration is not None
117
118 db_current_migration = self.database_current_migration
119
120 return [
121 (migration_number, migration_func)
122 for migration_number, migration_func in self.sorted_migrations
123 if migration_number > db_current_migration]
124
125
126 def init_tables(self):
127 """
128 Create all tables relative to this package
129 """
130 # sanity check before we proceed, none of these should be created
131 for model in self.models:
132 # Maybe in the future just print out a "Yikes!" or something?
133 assert not model.__table__.exists(self.session.bind)
134
135 self.migration_model.metadata.create_all(
136 self.session.bind,
137 tables=[model.__table__ for model in self.models])
138
139 def create_new_migration_record(self):
140 """
141 Create a new migration record for this migration set
142 """
143 migration_record = self.migration_model(
144 name=self.name,
145 version=self.latest_migration)
146 self.session.add(migration_record)
147 self.session.commit()
148
149 def dry_run(self):
150 """
151 Print out a dry run of what we would have upgraded.
152 """
153 if self.database_current_migration is None:
154 self.printer(
155 u'~> Woulda initialized: %s\n' % self.name_for_printing())
156 return u'inited'
157
158 migrations_to_run = self.migrations_to_run()
159 if migrations_to_run:
160 self.printer(
161 u'~> Woulda updated %s:\n' % self.name_for_printing())
162
163 for migration_number, migration_func in migrations_to_run():
164 self.printer(
165 u' + Would update %s, "%s"\n' % (
166 migration_number, migration_func.func_name))
167
168 return u'migrated'
169
170 def name_for_printing(self):
171 if self.name == u'__main__':
172 return u"main mediagoblin tables"
173 else:
174 # TODO: Use the friendlier media manager "human readable" name
175 return u'media type "%s"' % self.name
176
177 def init_or_migrate(self):
178 """
179 Initialize the database or migrate if appropriate.
180
181 Returns information about whether or not we initialized
182 ('inited'), migrated ('migrated'), or did nothing (None)
183 """
184 assure_migrations_table_setup(self.session)
185
186 # Find out what migration number, if any, this database data is at,
187 # and what the latest is.
188 migration_number = self.database_current_migration
189
190 # Is this our first time? Is there even a table entry for
191 # this identifier?
192 # If so:
193 # - create all tables
194 # - create record in migrations registry
195 # - print / inform the user
196 # - return 'inited'
197 if migration_number is None:
198 self.printer(u"-> Initializing %s... " % self.name_for_printing())
199
200 self.init_tables()
201 # auto-set at latest migration number
202 self.create_new_migration_record()
203
204 self.printer(u"done.\n")
205 self.set_current_migration()
206 return u'inited'
207
208 # Run migrations, if appropriate.
209 migrations_to_run = self.migrations_to_run()
210 if migrations_to_run:
211 self.printer(
212 u'-> Updating %s:\n' % self.name_for_printing())
213 for migration_number, migration_func in migrations_to_run:
214 self.printer(
215 u' + Running migration %s, "%s"... ' % (
216 migration_number, migration_func.func_name))
217 migration_func(self.session)
218 self.set_current_migration(migration_number)
219 self.printer('done.\n')
220
221 return u'migrated'
222
223 # Otherwise return None. Well it would do this anyway, but
224 # for clarity... ;)
225 return None
226
227
228 class RegisterMigration(object):
229 """
230 Tool for registering migrations
231
232 Call like:
233
234 @RegisterMigration(33)
235 def update_dwarves(database):
236 [...]
237
238 This will register your migration with the default migration
239 registry. Alternately, to specify a very specific
240 migration_registry, you can pass in that as the second argument.
241
242 Note, the number of your migration should NEVER be 0 or less than
243 0. 0 is the default "no migrations" state!
244 """
245 def __init__(self, migration_number, migration_registry):
246 assert migration_number > 0, "Migration number must be > 0!"
247 assert migration_number not in migration_registry, \
248 "Duplicate migration numbers detected! That's not allowed!"
249
250 self.migration_number = migration_number
251 self.migration_registry = migration_registry
252
253 def __call__(self, migration):
254 self.migration_registry[self.migration_number] = migration
255 return migration
256
257
258 def assure_migrations_table_setup(db):
259 """
260 Make sure the migrations table is set up in the database.
261 """
262 from mediagoblin.db.models import MigrationData
263
264 if not MigrationData.__table__.exists(db.bind):
265 MigrationData.metadata.create_all(
266 db.bind, tables=[MigrationData.__table__])
267
268
269 ##########################
270 # Random utility functions
271 ##########################
272
273
274 def atomic_update(table, query_dict, update_values):
275 table.find(query_dict).update(update_values,
276 synchronize_session=False)
277 Session.commit()
278
279
280 def check_media_slug_used(dummy_db, uploader_id, slug, ignore_m_id):
281 filt = (MediaEntry.uploader == uploader_id) \
282 & (MediaEntry.slug == slug)
283 if ignore_m_id is not None:
284 filt = filt & (MediaEntry.id != ignore_m_id)
285 does_exist = Session.query(MediaEntry.id).filter(filt).first() is not None
286 return does_exist
287
288
289 def media_entries_for_tag_slug(dummy_db, tag_slug):
290 return MediaEntry.query \
291 .join(MediaEntry.tags_helper) \
292 .join(MediaTag.tag_helper) \
293 .filter(
294 (MediaEntry.state == u'processed')
295 & (Tag.slug == tag_slug))
296
297
298 def clean_orphan_tags(commit=True):
299 """Search for unused MediaTags and delete them"""
300 q1 = Session.query(Tag).outerjoin(MediaTag).filter(MediaTag.id==None)
301 for t in q1:
302 Session.delete(t)
303 # The "let the db do all the work" version:
304 # q1 = Session.query(Tag.id).outerjoin(MediaTag).filter(MediaTag.id==None)
305 # q2 = Session.query(Tag).filter(Tag.id.in_(q1))
306 # q2.delete(synchronize_session = False)
307 if commit:
308 Session.commit()
309
310
311 def check_collection_slug_used(dummy_db, creator_id, slug, ignore_c_id):
312 filt = (Collection.creator == creator_id) \
313 & (Collection.slug == slug)
314 if ignore_c_id is not None:
315 filt = filt & (Collection.id != ignore_c_id)
316 does_exist = Session.query(Collection.id).filter(filt).first() is not None
317 return does_exist
318
319
320 if __name__ == '__main__':
321 from mediagoblin.db.open import setup_connection_and_db_from_config
322
323 db = setup_connection_and_db_from_config({'sql_engine':'sqlite:///mediagoblin.db'})
324
325 clean_orphan_tags()