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

@@ -37,6 +37,38 @@ function seed(sqlite: Database.Database) {
if (columns.some(({ name }) => name === 'epg_channel_id')) {
sqlite.exec("UPDATE content SET epg_channel_id = 'retained-epg-id'");
}
// An M3U source with a channel payload and every playlist column the
// schema has: the payload-last rebuild of `playlists` must carry them all
// across unchanged.
const playlistColumns = new Set(
(sqlite.pragma('table_info(playlists)') as { name: string }[]).map(
({ name }) => name
)
);
if (playlistColumns.has('payload')) {
const values: Record<string, string | number> = {
id: 'm3u',
name: 'Saved list',
type: 'm3u-url',
url: 'https://list.invalid/a.m3u',
payload: '{"channels":[{"name":"One"}]}',
epg_urls: '["https://epg.invalid/a.xml"]',
detected_epg_urls: '["https://epg.invalid/b.xml"]',
favorites: '["one"]',
recently_viewed: '["one"]',
last_usage: '2026-01-01T00:00:00.000Z',
count: 1,
};
const present = Object.keys(values).filter((name) =>
playlistColumns.has(name)
);
sqlite
.prepare(
`INSERT INTO playlists (${present.join(', ')})
VALUES (${present.map(() => '?').join(', ')})`
)
.run(...present.map((name) => values[name]));
}
}
/**
@@ -145,6 +177,15 @@ async function main() {
const sqlite = new Database(databasePath, { readonly: true });
try {
verifyCurrentSchema(sqlite);
assert.equal(
(
sqlite.pragma('table_info(playlists)') as {
name: string;
}[]
).at(-1)?.name,
'payload',
'payload must be the last playlists column'
);
for (const { query, rows } of before) {
assert.deepEqual(sqlite.prepare(query).all(), rows, query);
}