Skip to content

perf(delete): avoid loading every linked book when deleting/editing a book - #1665

Open
manut999 wants to merge 1 commit into
crocodilestick:mainfrom
manut999:fix/fast-orphan-check-on-delete
Open

manut999 wants to merge 1 commit into
crocodilestick:mainfrom
manut999:fix/fast-orphan-check-on-delete

Conversation

@manut999

@manut999 manut999 commented Nov 9, 2026

Copy link
Copy Markdown

Problem

Deleting a book is very slow on large libraries. On mine (~208k books) every single delete took ~40 s, and since it runs in the web process the whole UI stops responding for that time (healthcheck timeouts, unhealthy). Resolving 19 duplicate groups took 785 s.

delete_whole_book() calls modify_database_object() for authors, tags, series, languages and publishers, which ends in remove_objects():

db_book_object.remove(del_element)
if len(del_element.books) == 0:
    db_session.delete(del_element)

len(del_element.books) loads every book linked to that element just to see whether the list is empty. Because all Books relationships are lazy='selectin', each of those books also pulls its authors, tags, data, comments, identifiers, … For the language deu that is 189,034 books in my library — on every delete.

Confirmed with py-spy dump during a duplicate resolution run:

LazyLoader._emit_lazyload (sqlalchemy/orm/strategies.py:1152)
...
InstrumentedAttribute.__get__ (sqlalchemy/orm/attributes.py:569)
remove_objects (cps/editbooks.py:1979)
modify_database_object (cps/editbooks.py:2074)
delete_whole_book (cps/editbooks.py:1321)      # languages
auto_resolve_duplicates (cps/duplicates.py:1749)

The same applies to editing a book's metadata whenever a widely used tag/language/publisher is removed from it, and to deletes from the book page.

Fix

Flush the removed link, then ask the database whether any book still references the element:

db_session.flush()
still_used = (db_session.query(db.Books.id)
              .with_parent(del_element, type(del_element).books)
              .limit(1)
              .first())
if still_used is None:
    db_session.delete(del_element)

with_parent uses the existing books backref, so it works for authors, tags, series, languages, publishers and the multi-value custom column classes alike, without per-type code. The semantics are unchanged: the element is deleted exactly when no book links to it anymore.

Testing

On a copy of my metadata.db (190k books), using the real cps.db models and remove_objects():

case before after
remove language deu from a book ~40 s 0.00 s, deu kept
remove a tag that only this book had — 0.00 s, tag deleted

Running patched in my instance since; duplicate resolution and manual deletes are now instant and the UI stays responsive.

🤖 Generated with Claude Code

…l books

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant