diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index 830447c22e..a3b9e0618f 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -20,7 +20,7 @@ from calibre.db import SPOOL_SIZE, _get_next_series_num_for_list from calibre.db.categories import get_categories from calibre.db.locking import create_locks from calibre.db.errors import NoSuchFormat -from calibre.db.fields import create_field, IDENTITY +from calibre.db.fields import create_field, IDENTITY, InvalidLinkTable from calibre.db.search import Search from calibre.db.tables import VirtualTable from calibre.db.write import get_series_values, uniq @@ -866,10 +866,18 @@ class Cache(object): def search(self, query, restriction='', virtual_fields=None, book_ids=None): return self._search_api(self, query, restriction, virtual_fields=virtual_fields, book_ids=book_ids) - @read_api - def get_categories(self, sort='name', book_ids=None, icon_map=None): - return get_categories(self, sort=sort, book_ids=book_ids, - icon_map=icon_map) + @api + def get_categories(self, sort='name', book_ids=None, icon_map=None, already_fixed=None): + try: + with self.read_lock: + return get_categories(self, sort=sort, book_ids=book_ids, icon_map=icon_map) + except InvalidLinkTable as err: + bad_field = err.field_name + if bad_field == already_fixed: + raise + with self.write_lock: + self.fields[bad_field].table.fix_link_table(self.backend) + return self.get_categories(sort=sort, book_ids=book_ids, icon_map=icon_map, already_fixed=bad_field) @write_api def update_last_modified(self, book_ids, now=None): diff --git a/src/calibre/db/fields.py b/src/calibre/db/fields.py index b59fc58608..fbed097143 100644 --- a/src/calibre/db/fields.py +++ b/src/calibre/db/fields.py @@ -27,6 +27,12 @@ def bool_sort_key(bools_are_tristate): IDENTITY = lambda x: x +class InvalidLinkTable(Exception): + + def __init__(self, name): + Exception.__init__(self, name) + self.field_name = name + class Field(object): is_many = False @@ -144,9 +150,15 @@ class Field(object): ratings = tuple(r for r in (book_rating_map.get(book_id, 0) for book_id in item_book_ids) if r > 0) avg = sum(ratings)/len(ratings) if ratings else 0 - name = self.category_formatter(id_map[item_id]) + try: + name = self.category_formatter(id_map[item_id]) + except KeyError: + # db has entries in the link table without entries in the + # id table, for example, see + # https://bugs.launchpad.net/bugs/1218783 + raise InvalidLinkTable(self.name) sval = (self.category_sort_value(item_id, item_book_ids, lang_map) - if special_sort else name) + if special_sort else name) c = tag_class(name, id=item_id, sort=sval, avg=avg, id_set=item_book_ids, count=len(item_book_ids)) ans.append(c) diff --git a/src/calibre/db/tables.py b/src/calibre/db/tables.py index 00eeaa5a1d..f2ccbe0c39 100644 --- a/src/calibre/db/tables.py +++ b/src/calibre/db/tables.py @@ -64,6 +64,9 @@ class Table(object): def remove_books(self, book_ids, db): return set() + def fix_link_table(self, db): + pass + class VirtualTable(Table): ''' @@ -201,6 +204,17 @@ class ManyToOneTable(Table): cbm[item_id].add(book) bcm[book] = item_id + def fix_link_table(self, db): + linked_item_ids = {item_id for item_id in self.book_col_map.itervalues()} + extra_item_ids = linked_item_ids - set(self.id_map) + if extra_item_ids: + for item_id in extra_item_ids: + book_ids = self.col_book_map.pop(item_id, ()) + for book_id in book_ids: + self.book_col_map.pop(book_id, None) + db.conn.executemany('DELETE FROM {0} WHERE {1}=?'.format( + self.link_table, self.metadata['link_column']), tuple((x,) for x in extra_item_ids)) + def remove_books(self, book_ids, db): clean = set() for book_id in book_ids: @@ -284,6 +298,17 @@ class ManyToManyTable(ManyToOneTable): self.book_col_map = {k:tuple(v) for k, v in bcm.iteritems()} + def fix_link_table(self, db): + linked_item_ids = {item_id for item_ids in self.book_col_map.itervalues() for item_id in item_ids} + extra_item_ids = linked_item_ids - set(self.id_map) + if extra_item_ids: + for item_id in extra_item_ids: + book_ids = self.col_book_map.pop(item_id, ()) + for book_id in book_ids: + self.book_col_map[book_id] = tuple(iid for iid in self.book_col_map.pop(book_id, ()) if iid not in extra_item_ids) + db.conn.executemany('DELETE FROM {0} WHERE {1}=?'.format( + self.link_table, self.metadata['link_column']), tuple((x,) for x in extra_item_ids)) + def remove_books(self, book_ids, db): clean = set() for book_id in book_ids: