perf(database): skip the per-launch duplicate scan and keep playlist payloads last (#1883)

deduplicateXtreamCache no longer scans the whole catalog on every launch once both unique indexes exist (7.8 s cold on a 3.9M-title library). The playlists table keeps payload as its last column (playlists-table.ts); ensurePlaylistsPayloadLast rebuilds an existing table once, keeping its indexes and triggers, foreign keys off, committing only if references referencing playlists gain no violations. On a full copy of that profile initDatabase went from 5.2-8.1 s to 39 ms and the playlist metadata read from 1.5-2 s to 22-34 ms; the one-time rebuild costs about 4 s.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 authored and GitHub committed 2026-10-11 09:02:17 +02:00
1 parent 05fc8f9a2e
commit 37df7aceff
9 files changed
+741 -34

No files matched your search

+31 -1
View File
@@ -65,6 +65,34 @@ When changing initialization:
install, and repeated startup. If a release changes migration ordering, cover
each distinct affected historical schema.
Two startup costs found on a 4 GB profile (3.9M catalog titles, 182
playlists) are guarded in `runMigrations()`:
- `deduplicateXtreamCache()` returns at once when both unique indexes
(`src/lib/xtream-cache-unique-indexes.ts`:
`categories_playlist_type_xtream_unique`,
`content_category_type_xtream_unique`) exist: they make duplicates
impossible, and its two `GROUP BY ... HAVING COUNT(*) > 1` scans cost a full
pass over `content` on every launch otherwise (7.8 s cold on that profile).
A profile whose unique index could not be created yet still gets the repair.
- `payload` must be the last column of `playlists` (`PLAYLISTS_COLUMNS` in
`src/lib/playlists-table.ts`): SQLite
reads a row's columns in order and walks the overflow pages of every column
before the one it needs, so a column after an M3U payload costs the whole
payload on each read (`getAppPlaylistMetas` took 4-22 s for 70 MB of
payloads). Tables created before 0.19 got `payload` appended by `ALTER TABLE`
with the EPG URL columns after it, and fresh installs up to 0.24 created
`last_usage` after it; `ensurePlaylistsPayloadLast()` rebuilds such a table
once (new table, copy, drop, rename, with foreign keys off) and is a no-op
afterwards. It commits only if the tables referencing playlists hold no more
foreign key violations than before; rows a legacy profile already orphaned
do not block it. `src/lib/playlists-table.spec.ts` runs the rebuild and its
rollback on real SQLite. The rebuild rewrites every payload, about 4 s on
that profile; later starts open the database in about 40 ms instead of
5-8 s. Never append a column after `payload`; add it to `PLAYLISTS_COLUMNS`
before `payload` and to
`COLUMN_MIGRATION_STATEMENTS`, and the rebuild moves it on the next start.
The #1580 index-ordering fix is included in 0.24 through PR #1550.
`src/lib/connection-upgrades.spec.ts` exercises `initDatabase()` with real
SQLite under the Electron runtime, using fresh-install schema snapshots from
@@ -72,7 +100,9 @@ tags 0.19–0.23 and a fresh current database. The `epg_channel_id` column is ab
in 0.19, present from 0.20, and indexed from 0.23. Each case checks current Drizzle
tables, columns/types, and named indexes/uniqueness, as well as preserved user
rows, foreign keys, database integrity, index availability, and repeated startup;
an existing EPG index must keep its definition and root page. Snapshots live in
an existing EPG index must keep its definition and root page, and `payload` must
end up as the last `playlists` column (the 0.19 snapshot reaches the current
schema through the ALTER list, so it reproduces an upgraded profile's order). Snapshots live in
`src/lib/testing/fixtures/` and are independent of the current schema, so moving
the index ahead of its column migration makes the 0.19 case fail again.