diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index a36e53c175..119e166c49 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -265,8 +265,10 @@ class Cache(object): for name, field in self.fields.iteritems(): if name[0] == '#' and name.endswith('_index'): field.series_field = self.fields[name[:-len('_index')]] + self.fields[name[:-len('_index')]].index_field = field elif name == 'series_index': field.series_field = self.fields['series'] + self.fields['series'].index_field = field elif name == 'authors': field.author_sort_field = self.fields['author_sort'] elif name == 'title': @@ -1179,6 +1181,18 @@ class Cache(object): else: table.remove_books(book_ids, self.backend) + @write_api + def remove_items(self, field, item_ids): + ''' Delete all items in the specified field with the specified ids. Returns the set of affected book ids. ''' + field = self.fields[field] + affected_books = field.table.remove_items(item_ids, self.backend) + if affected_books: + if hasattr(field, 'index_field'): + self._set_field(field.index_field.name, {bid:1.0 for bid in affected_books}) + else: + self._mark_as_dirty(affected_books) + return affected_books + @write_api def add_custom_book_data(self, name, val_map, delete_first=False): ''' Add data for name where val_map is a map of book_ids to values. If diff --git a/src/calibre/db/tables.py b/src/calibre/db/tables.py index 19c4ade10c..7715f6abef 100644 --- a/src/calibre/db/tables.py +++ b/src/calibre/db/tables.py @@ -204,6 +204,21 @@ class ManyToOneTable(Table): [(x,) for x in clean]) return clean + def remove_items(self, item_ids, db): + affected_books = set() + for item_id in item_ids: + val = self.id_map.pop(item_id, null) + if val is null: + continue + book_ids = self.col_book_map.pop(item_id, set()) + for book_id in book_ids: + self.book_col_map.pop(book_id, None) + affected_books.update(book_ids) + item_ids = tuple((x,) for x in item_ids) + db.conn.executemany('DELETE FROM {0} WHERE {1}=?'.format(self.link_table, self.metadata['link_column']), item_ids) + db.conn.executemany('DELETE FROM {0} WHERE id=?'.format(self.metadata['table']), item_ids) + return affected_books + class ManyToManyTable(ManyToOneTable): ''' @@ -250,6 +265,21 @@ class ManyToManyTable(ManyToOneTable): [(x,) for x in clean]) return clean + def remove_items(self, item_ids, db): + affected_books = set() + for item_id in item_ids: + val = self.id_map.pop(item_id, null) + if val is null: + continue + book_ids = self.col_book_map.pop(item_id, set()) + for book_id in book_ids: + self.book_col_map[book_id] = tuple(x for x in self.book_col_map.get(book_id, ()) if x != item_id) + affected_books.update(book_ids) + item_ids = tuple((x,) for x in item_ids) + db.conn.executemany('DELETE FROM {0} WHERE {1}=?'.format(self.link_table, self.metadata['link_column']), item_ids) + db.conn.executemany('DELETE FROM {0} WHERE id=?'.format(self.metadata['table']), item_ids) + return affected_books + class AuthorsTable(ManyToManyTable): def read_id_maps(self, db): @@ -274,6 +304,9 @@ class AuthorsTable(ManyToManyTable): self.asort_map.pop(item_id, None) return clean + def remove_items(self, item_ids, db): + raise ValueError('Direct removal of authors is not allowed') + class FormatsTable(ManyToManyTable): do_clean_on_remove = False @@ -331,6 +364,9 @@ class FormatsTable(ManyToManyTable): return {book_id:zero_max(book_id) for book_id in formats_map} + def remove_items(self, item_ids, db): + raise NotImplementedError('Cannot delete a format directly') + def update_fmt(self, book_id, fmt, fname, size, db): fmts = list(self.book_col_map.get(book_id, [])) try: @@ -381,4 +417,6 @@ class IdentifiersTable(ManyToManyTable): clean.add(item_id) return clean + def remove_items(self, item_ids, db): + raise NotImplementedError('Direct deletion of identifiers is not implemented') diff --git a/src/calibre/db/tests/legacy.py b/src/calibre/db/tests/legacy.py index bda3401107..7182a6ef06 100644 --- a/src/calibre/db/tests/legacy.py +++ b/src/calibre/db/tests/legacy.py @@ -293,7 +293,7 @@ class LegacyTest(BaseTest): 'clean_user_categories', 'cleanup_tags', 'books_list_filter', 'conn', 'connect', 'construct_file_name', 'construct_path_name', 'clear_dirtied', 'commit_dirty_cache', 'initialize_database', 'initialize_dynamic', 'run_import_plugins', 'vacuum', 'set_path', 'row', 'row_factory', 'rows', 'rmtree', 'series_index_pat', - 'import_old_database', 'dirtied_lock', 'dirtied_cache', 'dirty_queue_length', + 'import_old_database', 'dirtied_lock', 'dirtied_cache', 'dirty_queue_length', 'dirty_books_referencing', } SKIP_ARGSPEC = { '__init__', 'get_next_series_num_for', 'has_book', 'author_sort_from_authors', diff --git a/src/calibre/db/tests/writing.py b/src/calibre/db/tests/writing.py index 36b6d3d2a3..c4918b4c4b 100644 --- a/src/calibre/db/tests/writing.py +++ b/src/calibre/db/tests/writing.py @@ -436,3 +436,41 @@ class WritingTest(BaseTest): self.assertFalse(cache.has_conversion_options(all_ids)) # }}} + def test_remove_items(self): # {{{ + ' Test removal of many-(many,one) items ' + cache = self.init_cache() + tmap = cache.get_id_map('tags') + self.assertEqual(cache.remove_items('tags', tmap), {1, 2}) + tmap = cache.get_id_map('#tags') + t = {v:k for k, v in tmap.iteritems()}['My Tag Two'] + self.assertEqual(cache.remove_items('#tags', (t,)), {1, 2}) + + smap = cache.get_id_map('series') + self.assertEqual(cache.remove_items('series', smap), {1, 2}) + smap = cache.get_id_map('#series') + s = {v:k for k, v in smap.iteritems()}['My Series Two'] + self.assertEqual(cache.remove_items('#series', (s,)), {1}) + + for c in (cache, self.init_cache()): + self.assertFalse(c.get_id_map('tags')) + self.assertFalse(c.all_field_names('tags')) + for bid in c.all_book_ids(): + self.assertFalse(c.field_for('tags', bid)) + + self.assertEqual(len(c.get_id_map('#tags')), 1) + self.assertEqual(c.all_field_names('#tags'), {'My Tag One'}) + for bid in c.all_book_ids(): + self.assertIn(c.field_for('#tags', bid), ((), ('My Tag One',))) + + for bid in (1, 2): + self.assertEqual(c.field_for('series_index', bid), 1.0) + self.assertFalse(c.get_id_map('series')) + self.assertFalse(c.all_field_names('series')) + for bid in c.all_book_ids(): + self.assertFalse(c.field_for('series', bid)) + + self.assertEqual(c.field_for('series_index', 1), 1.0) + self.assertEqual(c.all_field_names('#series'), {'My Series One'}) + for bid in c.all_book_ids(): + self.assertIn(c.field_for('#series', bid), (None, 'My Series One')) + # }}}