~ubuntu-branches/ubuntu/natty/miro/natty

« back to all changes in this revision

Viewing changes to portable/database.py

  • Committer: Bazaar Package Importer
  • Author(s): Bryce Harrington
  • Date: 2011-01-22 02:46:33 UTC
  • mfrom: (1.4.10 upstream) (1.7.5 experimental)
  • Revision ID: james.westby@ubuntu.com-20110122024633-kjme8u93y2il5nmf
Tags: 3.5.1-1ubuntu1
* Merge from debian.  Remaining ubuntu changes:
  - Use python 2.7 instead of python 2.6
  - Relax dependency on python-dbus to >= 0.83.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- mode: python -*-
2
 
 
3
 
# Miro - an RSS based video player application
4
 
# Copyright (C) 2005-2010 Participatory Culture Foundation
5
 
#
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.
10
 
#
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.
15
 
#
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
19
 
#
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
22
 
# library.
23
 
#
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.
30
 
 
31
 
import logging
32
 
import traceback
33
 
import threading
34
 
 
35
 
from miro import app
36
 
from miro import signals
37
 
 
38
 
class DatabaseConstraintError(Exception):
39
 
    """Raised when a DDBObject fails its constraint checking during
40
 
    signal_change().
41
 
    """
42
 
    pass
43
 
 
44
 
class DatabaseConsistencyError(Exception):
45
 
    """Raised when the database encounters an internal consistency
46
 
    issue.
47
 
    """
48
 
    pass
49
 
 
50
 
class DatabaseThreadError(Exception):
51
 
    """Raised when the database encounters an internal consistency
52
 
    issue.
53
 
    """
54
 
    pass
55
 
 
56
 
class DatabaseVersionError(StandardError):
57
 
    """Raised when an attempt is made to restore a database newer than
58
 
    the one we support
59
 
    """
60
 
    pass
61
 
 
62
 
class ObjectNotFoundError(StandardError):
63
 
    """Raised when an attempt is made to lookup an object that doesn't
64
 
    exist
65
 
    """
66
 
    pass
67
 
 
68
 
class TooManyObjects(StandardError):
69
 
    """Raised when an attempt is made to lookup a singleton and
70
 
    multiple rows match the query.
71
 
    """
72
 
    pass
73
 
 
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.
77
 
    """
78
 
    pass
79
 
 
80
 
class NoValue(object):
81
 
    """Used as a dummy value so that "None" can be treated as a valid
82
 
    value.
83
 
    """
84
 
    pass
85
 
 
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
89
 
# purposes.
90
 
 
91
 
event_thread = None
92
 
def set_thread(thread):
93
 
    global event_thread
94
 
    if event_thread is None:
95
 
        event_thread = thread
96
 
 
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"
101
 
        else:
102
 
            error_string = "Database called from %s" % threading.currentThread()
103
 
        traceback.print_stack()
104
 
        raise DatabaseThreadError, error_string
105
 
 
106
 
class View(object):
107
 
    def __init__(self, klass, where, values, order_by, joins, limit):
108
 
        self.klass = klass
109
 
        self.where = where
110
 
        self.values = values
111
 
        self.order_by = order_by
112
 
        self.joins = joins
113
 
        self.limit = limit
114
 
 
115
 
    def __iter__(self):
116
 
        return app.db.query(self.klass, self.where, self.values,
117
 
                            self.order_by, self.joins, self.limit)
118
 
 
119
 
    def count(self):
120
 
        return app.db.query_count(self.klass, self.where, self.values,
121
 
                                  self.joins, self.limit)
122
 
 
123
 
    def get_singleton(self):
124
 
        results = list(self)
125
 
        if len(results) == 1:
126
 
            return results[0]
127
 
        elif len(results) == 0:
128
 
            raise ObjectNotFoundError("Can't find singleton")
129
 
        else:
130
 
            raise TooManyObjects("Too many results returned")
131
 
 
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)
136
 
 
137
 
class ViewTrackerManager(object):
138
 
    def __init__(self):
139
 
        # maps table_name to trackers
140
 
        self.table_to_tracker = {}
141
 
        # maps joined tables to trackers
142
 
        self.joined_table_to_tracker = {}
143
 
 
144
 
    def trackers_for_table(self, table_name):
145
 
        try:
146
 
            return self.table_to_tracker[table_name]
147
 
        except KeyError:
148
 
            self.table_to_tracker[table_name] = set()
149
 
            return self.table_to_tracker[table_name]
150
 
 
151
 
    def trackers_for_ddb_class(self, klass):
152
 
        return self.trackers_for_table(app.db.table_name(klass))
153
 
 
154
 
    def update_view_trackers(self, obj):
155
 
        """Update view trackers based on an object change."""
156
 
 
157
 
        for tracker in self.trackers_for_ddb_class(obj.__class__):
158
 
            tracker.object_changed(obj)
159
 
 
160
 
    def bulk_update_view_trackers(self, table_name):
161
 
        for tracker in self.trackers_for_table(table_name):
162
 
            tracker.check_all_objects()
163
 
 
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)
167
 
 
168
 
    def remove_from_view_trackers(self, obj):
169
 
        """Update view trackers based on an object change."""
170
 
 
171
 
        for tracker in self.trackers_for_ddb_class(obj.__class__):
172
 
            tracker.remove_object(obj)
173
 
 
174
 
class ViewTracker(signals.SignalEmitter):
175
 
    def __init__(self, klass, where, values, joins):
176
 
        signals.SignalEmitter.__init__(self, 'added', 'removed', 'changed')
177
 
        self.klass = klass
178
 
        self.where = where
179
 
        if isinstance(values, list):
180
 
            raise TypeError("values must be a tuple")
181
 
        self.values = values
182
 
        self.joins = joins
183
 
        self.current_ids = set(app.db.query_ids(klass, where, values,
184
 
            joins=joins))
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)
188
 
 
189
 
    def unlink(self):
190
 
        vt_manager = app.view_tracker_manager
191
 
        vt_manager.trackers_for_table(self.table_name).discard(self)
192
 
 
193
 
    def _obj_in_view(self, obj):
194
 
        where = '%s.id = ?' % (self.table_name,)
195
 
        if self.where:
196
 
            where += ' AND (%s)' % (self.where,)
197
 
 
198
 
        values = (obj.id,) + self.values
199
 
        return app.db.query_count(self.klass, where, values, self.joins) > 0
200
 
 
201
 
    def object_changed(self, obj):
202
 
        self.check_object(obj)
203
 
 
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)
208
 
 
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])
215
 
 
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)
225
 
        elif before and now:
226
 
            self.emit('changed', obj)
227
 
 
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_))
242
 
 
243
 
    def __len__(self):
244
 
        return len(self.current_ids)
245
 
 
246
 
class BulkSQLManager(object):
247
 
    def __init__(self):
248
 
        self.active = False
249
 
        self.to_insert = {}
250
 
        self.to_remove = {}
251
 
        self.pending_inserts = set()
252
 
 
253
 
    def start(self):
254
 
        if self.active:
255
 
            raise ValueError("BulkSQLManager.start() called twice")
256
 
        self.active = True
257
 
 
258
 
    def finish(self):
259
 
        if not self.active:
260
 
            raise ValueError("BulkSQLManager.finish() called twice")
261
 
        self.commit()
262
 
        self.active = False
263
 
 
264
 
    def commit(self):
265
 
        for x in range(100):
266
 
            to_insert = self.to_insert
267
 
            to_remove = self.to_remove
268
 
            self.to_insert = {}
269
 
            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:
273
 
                break
274
 
            # inside _commit_sql() or _update_view_trackers(), we were asked
275
 
            # to insert or remove more items, repeat the proccess again
276
 
        else:
277
 
            raise AssertionError("Called _commit_sql 100 times and still "
278
 
                    "have items to commit.  Are we in a circular loop?")
279
 
        self.to_insert = {}
280
 
        self.to_remove = {}
281
 
        self.pending_inserts = set()
282
 
 
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)
287
 
            for obj in objects:
288
 
                obj.inserted_into_db()
289
 
 
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)
293
 
            for obj in objects:
294
 
                obj.removed_from_db()
295
 
 
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)
299
 
 
300
 
        for table_name, objects in to_remove.items():
301
 
            if table_name in to_insert:
302
 
                # already updated the view above
303
 
                continue
304
 
            app.view_tracker_manager.bulk_remove_from_view_trackers(
305
 
                    table_name, objects)
306
 
 
307
 
    def add_insert(self, obj):
308
 
        table_name = app.db.table_name(obj.__class__)
309
 
        try:
310
 
            inserts_for_table = self.to_insert[table_name]
311
 
        except KeyError:
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)
316
 
 
317
 
    def will_insert(self, obj):
318
 
        return obj in self.pending_inserts
319
 
 
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)
325
 
            return
326
 
        try:
327
 
            removes_for_table = self.to_remove[table_name]
328
 
        except KeyError:
329
 
            removes_for_table = []
330
 
            self.to_remove[table_name] = removes_for_table
331
 
        removes_for_table.append(obj)
332
 
 
333
 
class AttributeUpdateTracker(object):
334
 
    """Used by DDBObject to track changes to attributes."""
335
 
 
336
 
    def __init__(self, name):
337
 
        self.name = name
338
 
 
339
 
    # Simple implementation of the python descriptor protocol.  We
340
 
    # just want to update changed_attributes when attributes are set.
341
 
 
342
 
    def __get__(self, instance, owner):
343
 
        try:
344
 
            return instance.__dict__[self.name]
345
 
        except KeyError:
346
 
            raise AttributeError(self.name)
347
 
        except AttributeError:
348
 
            if instance is None:
349
 
                raise AttributeError(
350
 
                    "Can't access '%s' as a class attribute" % self.name)
351
 
            else:
352
 
                raise
353
 
 
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
358
 
 
359
 
class DDBObject(signals.SignalEmitter):
360
 
    """Dynamic Database object
361
 
    """
362
 
    #The last ID used in this class
363
 
    lastID = 0
364
 
 
365
 
    def __init__(self, *args, **kwargs):
366
 
        self.in_db_init = True
367
 
        signals.SignalEmitter.__init__(self, 'removed')
368
 
        self.changed_attributes = set()
369
 
 
370
 
        if len(args) == 0 and kwargs.keys() == ['restored_data']:
371
 
            restoring = True
372
 
        else:
373
 
            restoring = False
374
 
 
375
 
        if restoring:
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():
381
 
                return
382
 
        else:
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():
390
 
                return
391
 
 
392
 
        self.in_db_init = False
393
 
 
394
 
        if not restoring:
395
 
            self._insert_into_db()
396
 
 
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)
402
 
        else:
403
 
            app.bulk_sql_manager.add_insert(self)
404
 
 
405
 
    def inserted_into_db(self):
406
 
        self.check_constraints()
407
 
        self.on_db_insert()
408
 
 
409
 
    @classmethod
410
 
    def make_view(cls, where=None, values=None, order_by=None, joins=None,
411
 
            limit=None):
412
 
        if values is None:
413
 
            values = ()
414
 
        return View(cls, where, values, order_by, joins, limit)
415
 
 
416
 
    @classmethod
417
 
    def get_by_id(cls, id_):
418
 
        try:
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):
422
 
                return obj
423
 
            else:
424
 
                raise ObjectNotFoundError(id_)
425
 
        except KeyError:
426
 
            return cls.make_view('id=?', (id_,)).get_singleton()
427
 
 
428
 
    @classmethod
429
 
    def delete(cls, where, values=None):
430
 
        return app.db.delete(cls, where, values)
431
 
 
432
 
    @classmethod
433
 
    def select(cls, columns, where=None, values=None, convert=True):
434
 
        return app.db.select(cls, columns, where, values, convert)
435
 
 
436
 
    def setup_new(self):
437
 
        """Initialize a newly created object."""
438
 
        pass
439
 
 
440
 
    def setup_restored(self):
441
 
        """Initialize an object restored from disk."""
442
 
        pass
443
 
 
444
 
    def on_db_insert(self):
445
 
        """Called after an object has been inserted into the db."""
446
 
        pass
447
 
 
448
 
    @classmethod
449
 
    def track_attribute_changes(cls, name):
450
 
        """Set up tracking when attributes get set.
451
 
 
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.
455
 
 
456
 
        This is used by the SQLite storage layer to track which
457
 
        attributes are changed between SQL UPDATE statements.
458
 
 
459
 
        For example:
460
 
 
461
 
        >> MyDDBObjectSubclass.track_attribute_changes('foo')
462
 
        >> MyDDBObjectSubclass.track_attribute_changes('bar')
463
 
        >>> obj = MyDDBObjectSubclass()
464
 
        >>> print obj.changed_attributes
465
 
        set([])
466
 
        >> obj.foo = obj.bar = obj.baz = 3
467
 
        >>> print obj.changed_attributes
468
 
        set(['foo', 'bar'])
469
 
        """
470
 
        # The AttributeUpdateTracker class does all the work
471
 
        setattr(cls, name, AttributeUpdateTracker(name))
472
 
 
473
 
    def reset_changed_attributes(self):
474
 
        self.changed_attributes = set()
475
 
 
476
 
    def get_id(self):
477
 
        """Returns unique integer assocaited with this object
478
 
        """
479
 
        return self.id
480
 
 
481
 
    def id_exists(self):
482
 
        try:
483
 
            self.get_by_id(self.id)
484
 
        except ObjectNotFoundError:
485
 
            return False
486
 
        else:
487
 
            return True
488
 
 
489
 
    def remove(self):
490
 
        """Call this after you've removed all references to the object
491
 
        """
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)
496
 
        else:
497
 
            app.bulk_sql_manager.add_remove(self)
498
 
 
499
 
    def removed_from_db(self):
500
 
        self.emit('removed')
501
 
 
502
 
    def confirm_db_thread(self):
503
 
        """Call this before you grab data from an object
504
 
 
505
 
        Usage::
506
 
 
507
 
            view.confirm_db_thread()
508
 
            ...
509
 
        """
510
 
        confirm_db_thread()
511
 
 
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.
516
 
        """
517
 
        pass
518
 
 
519
 
    def signal_change(self, needs_save=True):
520
 
        """Call this after you change the object
521
 
        """
522
 
        if self.in_db_init:
523
 
            # signal_change called while we were setting up a object,
524
 
            # just ignore it.
525
 
            return
526
 
        if not self.id_exists():
527
 
            msg = ("signal_change() called on non-existant object (id is %s)" \
528
 
                       % self.id)
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.
536
 
            return
537
 
        if needs_save:
538
 
            app.db.update_obj(self)
539
 
        app.view_tracker_manager.update_view_trackers(self)
540
 
 
541
 
    def on_signal_change(self):
542
 
        pass
543
 
 
544
 
def update_last_id():
545
 
    DDBObject.lastID = app.db.get_last_id()
546
 
 
547
 
def setup_managers():
548
 
    app.view_tracker_manager = ViewTrackerManager()
549
 
    app.bulk_sql_manager = BulkSQLManager()
550
 
 
551
 
def initialize():
552
 
    update_last_id()
553
 
    setup_managers()