mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-16 13:48:09 -07:00
Guard the database update against duplicates and damaged indexes
(cherry picked from commit ea2b73ad78)
This commit is contained in:
@@ -14,6 +14,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
|
||||
### Fixed
|
||||
- Fix long floating point numbers showing up in Comick progress ([@MajorTanya](https://github.com/MajorTanya)) ([#4090](https://github.com/mihonapp/mihon/pull/4090))
|
||||
- Fixed updating from v0.20.x failing when the database had duplicate history or tracking entries, or damaged indexes ([@AntsyLich](https://github.com/AntsyLich))
|
||||
|
||||
## [v0.21.1] - 2026-10-12
|
||||
### Fixed
|
||||
|
||||
@@ -4,6 +4,64 @@ import kotlin.String;
|
||||
import kotlin.collections.List;
|
||||
import kotlinx.serialization.json.JsonObject;
|
||||
|
||||
-- Every view reads `mangas`, so they are dropped here and recreated against the new tables at the end
|
||||
DROP VIEW historyView;
|
||||
DROP VIEW libraryView;
|
||||
DROP VIEW updatesView;
|
||||
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
-- damaged indexes
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- Some databases hold rows their unique constraints should have rejected, which a damaged index explains. A
|
||||
-- write to a table also writes its indexes and fails on a damaged one, and a read through one can miss rows,
|
||||
-- so each table changed in place below is first copied from its rows alone, leaving its indexes behind.
|
||||
CREATE TABLE mangas_copy AS
|
||||
SELECT
|
||||
_id, source, url, artist, author, description, genre, title, status, thumbnail_url, last_update, next_update,
|
||||
initialized, viewer, chapter_flags, cover_last_modified, update_strategy, calculate_interval, notes, memo,
|
||||
favorite_at
|
||||
FROM mangas NOT INDEXED;
|
||||
DROP TABLE mangas;
|
||||
ALTER TABLE mangas_copy RENAME TO mangas;
|
||||
|
||||
CREATE TABLE chapters_copy AS
|
||||
SELECT
|
||||
_id, manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch,
|
||||
date_upload, memo
|
||||
FROM chapters NOT INDEXED;
|
||||
DROP TABLE chapters;
|
||||
ALTER TABLE chapters_copy RENAME TO chapters;
|
||||
|
||||
CREATE TABLE history_copy AS
|
||||
SELECT _id, chapter_id, last_read, time_read
|
||||
FROM history NOT INDEXED;
|
||||
DROP TABLE history;
|
||||
ALTER TABLE history_copy RENAME TO history;
|
||||
|
||||
CREATE TABLE mangas_categories_copy AS
|
||||
SELECT _id, manga_id, category_id
|
||||
FROM mangas_categories NOT INDEXED;
|
||||
DROP TABLE mangas_categories;
|
||||
ALTER TABLE mangas_categories_copy RENAME TO mangas_categories;
|
||||
|
||||
CREATE TABLE manga_sync_copy AS
|
||||
SELECT
|
||||
_id, manga_id, sync_id, remote_id, library_id, title, last_chapter_read, total_chapters, status, score,
|
||||
remote_url, start_date, finish_date, private
|
||||
FROM manga_sync NOT INDEXED;
|
||||
DROP TABLE manga_sync;
|
||||
ALTER TABLE manga_sync_copy RENAME TO manga_sync;
|
||||
|
||||
CREATE TABLE excluded_scanlators_copy AS
|
||||
SELECT manga_id, scanlator
|
||||
FROM excluded_scanlators NOT INDEXED;
|
||||
DROP TABLE excluded_scanlators;
|
||||
ALTER TABLE excluded_scanlators_copy RENAME TO excluded_scanlators;
|
||||
|
||||
-- Merging duplicate chapters looks their history up by chapter
|
||||
CREATE INDEX history_chapter_id_index ON history(chapter_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
-- orphans
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
@@ -21,11 +79,6 @@ DELETE FROM excluded_scanlators WHERE manga_id NOT IN (SELECT _id FROM mangas);
|
||||
-- table - manga
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- Every view reads `mangas`, so they are dropped here and recreated against the new table below
|
||||
DROP VIEW historyView;
|
||||
DROP VIEW libraryView;
|
||||
DROP VIEW updatesView;
|
||||
|
||||
-- Entries sharing a source and url are merged into one, since the new table won't hold both. The copies are
|
||||
-- ranked with the ones in the library first, since that is the entry the user has, then by age. The first
|
||||
-- one stays, takes each detail it is missing from the first copy that has it, and takes over everything
|
||||
@@ -326,8 +379,7 @@ CREATE INDEX manga_category_category_id_index ON manga_category(category_id);
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- `last_read` is when the chapter was last read and `time_read` is how long it has been read for, so
|
||||
-- both are renamed to say so. The chapter it belongs to is already unique, which leaves
|
||||
-- `history_history_chapter_id_index` a prefix of that constraint.
|
||||
-- both are renamed to say so. The chapter it belongs to is already unique, so the constraint indexes it.
|
||||
-- The table keeps its name, so the original is renamed aside and the new one built under
|
||||
-- the real name; renaming a table into place makes SQLite rewrite its stored definition,
|
||||
-- leaving it spelled differently from the same table on a fresh install. The views were
|
||||
@@ -349,11 +401,14 @@ CREATE TABLE history(
|
||||
-- Reaching the entry a row belongs to meant going through its chapter; it names the entry
|
||||
-- itself now, taken from that same chapter. A row whose chapter is already gone cannot name
|
||||
-- one and is dropped, which is what the foreign key would do to it once it is enforced again.
|
||||
-- Some databases hold more than one row for a chapter despite the old unique constraint, and the new one
|
||||
-- rejects them, so they merge the way the history of duplicate chapters does above.
|
||||
INSERT INTO history(id, manga_id, chapter_id, read_at, read_duration)
|
||||
SELECT H._id, C.manga_id, H.chapter_id, H.last_read, H.time_read
|
||||
SELECT min(H._id), C.manga_id, H.chapter_id, max(H.last_read), sum(H.time_read)
|
||||
FROM history_temp H
|
||||
JOIN chapter C
|
||||
ON C.id = H.chapter_id;
|
||||
ON C.id = H.chapter_id
|
||||
GROUP BY H.chapter_id;
|
||||
|
||||
DROP TABLE history_temp;
|
||||
|
||||
@@ -388,19 +443,20 @@ DROP TABLE excluded_scanlators;
|
||||
-- ---------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- `manga_sync` is what tracking was called when it was still called syncing; `sync_id` is the tracker.
|
||||
-- An entry that merged can leave two rows for the same tracker on the survivor, which the unique constraint
|
||||
-- rejects, so the one to keep is chosen first: one on a copy in the library, then the one furthest read, then
|
||||
-- the one on the copy ranked first, then the oldest. A track is taken whole rather than merged field by field,
|
||||
-- because two rows on the same tracker can point at different remote entries.
|
||||
-- An entry that merged can leave two rows for the same tracker on the survivor, and an entry that didn't can
|
||||
-- still hold two despite the old unique constraint. The new one rejects both, so the one to keep is chosen
|
||||
-- first: one on a copy in the library, then the one furthest read, then the one on the copy ranked first, then
|
||||
-- the oldest. A track is taken whole rather than merged field by field, because two rows on the same tracker
|
||||
-- can point at different remote entries.
|
||||
CREATE TEMP TABLE duplicate_tracks AS
|
||||
SELECT
|
||||
T._id,
|
||||
first_value(T._id) OVER (
|
||||
PARTITION BY D.keep_id, T.sync_id
|
||||
PARTITION BY coalesce(D.keep_id, T.manga_id), T.sync_id
|
||||
ORDER BY D.favorite DESC, T.last_chapter_read DESC, D.precedence ASC, T._id ASC
|
||||
) AS keep_track_id
|
||||
FROM manga_sync T
|
||||
JOIN duplicate_mangas D
|
||||
LEFT JOIN duplicate_mangas D
|
||||
ON D._id = T.manga_id;
|
||||
|
||||
DELETE FROM manga_sync
|
||||
|
||||
Reference in New Issue
Block a user