3
# Miro - an RSS based video player application
4
# Copyright (C) 2005-2010 Participatory Culture Foundation
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
20
# In addition, as a special exception, the copyright holders give
21
# permission to link the code of portions of this program with the OpenSSL
24
# You must obey the GNU General Public License in all respects for all of
25
# the code used other than OpenSSL. If you modify file(s) with this
26
# exception, you may extend this exception to your version of the file(s),
27
# but you are not obligated to do so. If you do not wish to do so, delete
28
# this exception statement from your version. If you delete this exception
29
# statement from all source files in the program, then also delete it here.
36
from miro import signals
38
class DatabaseConstraintError(Exception):
39
"""Raised when a DDBObject fails its constraint checking during
44
class DatabaseConsistencyError(Exception):
45
"""Raised when the database encounters an internal consistency
50
class DatabaseThreadError(Exception):
51
"""Raised when the database encounters an internal consistency
56
class DatabaseVersionError(StandardError):
57
"""Raised when an attempt is made to restore a database newer than
62
class ObjectNotFoundError(StandardError):
63
"""Raised when an attempt is made to lookup an object that doesn't
68
class TooManyObjects(StandardError):
69
"""Raised when an attempt is made to lookup a singleton and
70
multiple rows match the query.
74
class NotRootDBError(StandardError):
75
"""Raised when an attempt is made to call a function that's only
76
allowed to be called from the root database.
80
class NoValue(object):
81
"""Used as a dummy value so that "None" can be treated as a valid
86
# begin* and end* no longer actually lock the database. Instead
87
# confirm_db_thread prints a warning if it's run from any thread that
88
# isn't the main thread. This can be removed from releases for speed
92
def set_thread(thread):
94
if event_thread is None:
97
def confirm_db_thread():
98
if event_thread is None or event_thread != threading.currentThread():
99
if event_thread is None:
100
error_string = "Database event thread not set"
102
error_string = "Database called from %s" % threading.currentThread()
103
traceback.print_stack()
104
raise DatabaseThreadError, error_string
107
def __init__(self, klass, where, values, order_by, joins, limit):
111
self.order_by = order_by
116
return app.db.query(self.klass, self.where, self.values,
117
self.order_by, self.joins, self.limit)
120
return app.db.query_count(self.klass, self.where, self.values,
121
self.joins, self.limit)
123
def get_singleton(self):
125
if len(results) == 1:
127
elif len(results) == 0:
128
raise ObjectNotFoundError("Can't find singleton")
130
raise TooManyObjects("Too many results returned")
132
def make_tracker(self):
133
if self.limit is not None:
134
raise ValueError("tracking views with limits not supported")
135
return ViewTracker(self.klass, self.where, self.values, self.joins)
137
class ViewTrackerManager(object):
139
# maps table_name to trackers
140
self.table_to_tracker = {}
141
# maps joined tables to trackers
142
self.joined_table_to_tracker = {}
144
def trackers_for_table(self, table_name):
146
return self.table_to_tracker[table_name]
148
self.table_to_tracker[table_name] = set()
149
return self.table_to_tracker[table_name]
151
def trackers_for_ddb_class(self, klass):
152
return self.trackers_for_table(app.db.table_name(klass))
154
def update_view_trackers(self, obj):
155
"""Update view trackers based on an object change."""
157
for tracker in self.trackers_for_ddb_class(obj.__class__):
158
tracker.object_changed(obj)
160
def bulk_update_view_trackers(self, table_name):
161
for tracker in self.trackers_for_table(table_name):
162
tracker.check_all_objects()
164
def bulk_remove_from_view_trackers(self, table_name, objects):
165
for tracker in self.trackers_for_table(table_name):
166
tracker.remove_objects(objects)
168
def remove_from_view_trackers(self, obj):
169
"""Update view trackers based on an object change."""
171
for tracker in self.trackers_for_ddb_class(obj.__class__):
172
tracker.remove_object(obj)
174
class ViewTracker(signals.SignalEmitter):
175
def __init__(self, klass, where, values, joins):
176
signals.SignalEmitter.__init__(self, 'added', 'removed', 'changed')
179
if isinstance(values, list):
180
raise TypeError("values must be a tuple")
183
self.current_ids = set(app.db.query_ids(klass, where, values,
185
self.table_name = app.db.table_name(klass)
186
vt_manager = app.view_tracker_manager
187
vt_manager.trackers_for_table(self.table_name).add(self)
190
vt_manager = app.view_tracker_manager
191
vt_manager.trackers_for_table(self.table_name).discard(self)
193
def _obj_in_view(self, obj):
194
where = '%s.id = ?' % (self.table_name,)
196
where += ' AND (%s)' % (self.where,)
198
values = (obj.id,) + self.values
199
return app.db.query_count(self.klass, where, values, self.joins) > 0
201
def object_changed(self, obj):
202
self.check_object(obj)
204
def remove_object(self, obj):
205
if obj.id in self.current_ids:
206
self.current_ids.remove(obj.id)
207
self.emit('removed', obj)
209
def remove_objects(self, objects):
210
object_map = dict((o.id, o) for o in objects)
211
object_ids = set(object_map.keys())
212
for removed_id in self.current_ids.intersection(object_ids):
213
self.current_ids.remove(removed_id)
214
self.emit('removed', object_map[removed_id])
216
def check_object(self, obj):
217
before = (obj.id in self.current_ids)
218
now = self._obj_in_view(obj)
219
if before and not now:
220
self.current_ids.remove(obj.id)
221
self.emit('removed', obj)
222
elif now and not before:
223
self.current_ids.add(obj.id)
224
self.emit('added', obj)
226
self.emit('changed', obj)
228
def check_all_objects(self):
229
new_ids = set(app.db.query_ids(self.klass, self.where,
230
self.values, joins=self.joins))
231
old_ids = self.current_ids
232
self.current_ids = new_ids
233
for id_ in new_ids.difference(old_ids):
234
self.emit('added', app.db.get_obj_by_id(id_))
235
for id_ in old_ids.difference(new_ids):
236
self.emit('removed', app.db.get_obj_by_id(id_))
237
for id_ in old_ids.intersection(new_ids):
238
# XXX this hits all the IDs, but there doesn't seem to be
239
# a way to check if the objects have actually been
240
# changed. luckily, this isn't called very often.
241
self.emit('changed', app.db.get_obj_by_id(id_))
244
return len(self.current_ids)
246
class BulkSQLManager(object):
251
self.pending_inserts = set()
255
raise ValueError("BulkSQLManager.start() called twice")
260
raise ValueError("BulkSQLManager.finish() called twice")
266
to_insert = self.to_insert
267
to_remove = self.to_remove
270
self._commit_sql(to_insert, to_remove)
271
self._update_view_trackers(to_insert, to_remove)
272
if len(self.to_insert) == len(self.to_remove) == 0:
274
# inside _commit_sql() or _update_view_trackers(), we were asked
275
# to insert or remove more items, repeat the proccess again
277
raise AssertionError("Called _commit_sql 100 times and still "
278
"have items to commit. Are we in a circular loop?")
281
self.pending_inserts = set()
283
def _commit_sql(self, to_insert, to_remove):
284
for table_name, objects in to_insert.items():
285
logging.debug('bulk insert: %s %s', table_name, len(objects))
286
app.db.bulk_insert(objects)
288
obj.inserted_into_db()
290
for table_name, objects in to_remove.items():
291
logging.debug('bulk remove: %s %s', table_name, len(objects))
292
app.db.bulk_remove(objects)
294
obj.removed_from_db()
296
def _update_view_trackers(self, to_insert, to_remove):
297
for table_name in to_insert:
298
app.view_tracker_manager.bulk_update_view_trackers(table_name)
300
for table_name, objects in to_remove.items():
301
if table_name in to_insert:
302
# already updated the view above
304
app.view_tracker_manager.bulk_remove_from_view_trackers(
307
def add_insert(self, obj):
308
table_name = app.db.table_name(obj.__class__)
310
inserts_for_table = self.to_insert[table_name]
312
inserts_for_table = []
313
self.to_insert[table_name] = inserts_for_table
314
inserts_for_table.append(obj)
315
self.pending_inserts.add(obj)
317
def will_insert(self, obj):
318
return obj in self.pending_inserts
320
def add_remove(self, obj):
321
table_name = app.db.table_name(obj.__class__)
322
if self.will_insert(obj):
323
self.to_insert[table_name].remove(obj)
324
self.pending_inserts.remove(obj)
327
removes_for_table = self.to_remove[table_name]
329
removes_for_table = []
330
self.to_remove[table_name] = removes_for_table
331
removes_for_table.append(obj)
333
class AttributeUpdateTracker(object):
334
"""Used by DDBObject to track changes to attributes."""
336
def __init__(self, name):
339
# Simple implementation of the python descriptor protocol. We
340
# just want to update changed_attributes when attributes are set.
342
def __get__(self, instance, owner):
344
return instance.__dict__[self.name]
346
raise AttributeError(self.name)
347
except AttributeError:
349
raise AttributeError(
350
"Can't access '%s' as a class attribute" % self.name)
354
def __set__(self, instance, value):
355
if instance.__dict__.get(self.name, "BOGUS VALUE FOO") != value:
356
instance.changed_attributes.add(self.name)
357
instance.__dict__[self.name] = value
359
class DDBObject(signals.SignalEmitter):
360
"""Dynamic Database object
362
#The last ID used in this class
365
def __init__(self, *args, **kwargs):
366
self.in_db_init = True
367
signals.SignalEmitter.__init__(self, 'removed')
368
self.changed_attributes = set()
370
if len(args) == 0 and kwargs.keys() == ['restored_data']:
376
self.__dict__.update(kwargs['restored_data'])
377
app.db.remember_object(self)
378
self.setup_restored()
379
# handle setup_restored() calling remove()
380
if not self.id_exists():
383
self.id = DDBObject.lastID = DDBObject.lastID + 1
384
# call remember_object so that id_exists will return True
385
# when setup_new() is being run
386
app.db.remember_object(self)
387
self.setup_new(*args, **kwargs)
388
# handle setup_new() calling remove()
389
if not self.id_exists():
392
self.in_db_init = False
395
self._insert_into_db()
397
def _insert_into_db(self):
398
if not app.bulk_sql_manager.active:
399
app.db.insert_obj(self)
400
self.inserted_into_db()
401
app.view_tracker_manager.update_view_trackers(self)
403
app.bulk_sql_manager.add_insert(self)
405
def inserted_into_db(self):
406
self.check_constraints()
410
def make_view(cls, where=None, values=None, order_by=None, joins=None,
414
return View(cls, where, values, order_by, joins, limit)
417
def get_by_id(cls, id_):
419
# try memory first before going to sqlite.
420
obj = app.db.get_obj_by_id(id_)
421
if app.db.object_from_class_table(obj, cls):
424
raise ObjectNotFoundError(id_)
426
return cls.make_view('id=?', (id_,)).get_singleton()
429
def delete(cls, where, values=None):
430
return app.db.delete(cls, where, values)
433
def select(cls, columns, where=None, values=None, convert=True):
434
return app.db.select(cls, columns, where, values, convert)
437
"""Initialize a newly created object."""
440
def setup_restored(self):
441
"""Initialize an object restored from disk."""
444
def on_db_insert(self):
445
"""Called after an object has been inserted into the db."""
449
def track_attribute_changes(cls, name):
450
"""Set up tracking when attributes get set.
452
Call this on a DDBObject subclass to track changes to certain
453
attributes. Each DDBObject has a changed_attributes set,
454
which contains the attributes that have changed.
456
This is used by the SQLite storage layer to track which
457
attributes are changed between SQL UPDATE statements.
461
>> MyDDBObjectSubclass.track_attribute_changes('foo')
462
>> MyDDBObjectSubclass.track_attribute_changes('bar')
463
>>> obj = MyDDBObjectSubclass()
464
>>> print obj.changed_attributes
466
>> obj.foo = obj.bar = obj.baz = 3
467
>>> print obj.changed_attributes
470
# The AttributeUpdateTracker class does all the work
471
setattr(cls, name, AttributeUpdateTracker(name))
473
def reset_changed_attributes(self):
474
self.changed_attributes = set()
477
"""Returns unique integer assocaited with this object
483
self.get_by_id(self.id)
484
except ObjectNotFoundError:
490
"""Call this after you've removed all references to the object
492
if not app.bulk_sql_manager.active:
493
app.db.remove_obj(self)
494
self.removed_from_db()
495
app.view_tracker_manager.remove_from_view_trackers(self)
497
app.bulk_sql_manager.add_remove(self)
499
def removed_from_db(self):
502
def confirm_db_thread(self):
503
"""Call this before you grab data from an object
507
view.confirm_db_thread()
512
def check_constraints(self):
513
"""Subclasses can override this method to do constraint
514
checking before they get saved to disk. They should raise a
515
DatabaseConstraintError on problems.
519
def signal_change(self, needs_save=True):
520
"""Call this after you change the object
523
# signal_change called while we were setting up a object,
526
if not self.id_exists():
527
msg = ("signal_change() called on non-existant object (id is %s)" \
529
raise DatabaseConstraintError, msg
530
self.on_signal_change()
531
self.check_constraints()
532
if app.bulk_sql_manager.will_insert(self):
533
# Don't need to send an UPDATE SQL command, or check the view
534
# trackers in this case. Both will be done when the
535
# BulkSQLManager.finish() is called.
538
app.db.update_obj(self)
539
app.view_tracker_manager.update_view_trackers(self)
541
def on_signal_change(self):
544
def update_last_id():
545
DDBObject.lastID = app.db.get_last_id()
547
def setup_managers():
548
app.view_tracker_manager = ViewTrackerManager()
549
app.bulk_sql_manager = BulkSQLManager()