fix: drop+recreate contentless FTS5 index instead of DELETE

The search_index is a contentless FTS5 table (content=''), which
SQLite does not support DELETE on. ClearSearchIndex now drops and
recreates the virtual table. Single-row DeleteSearchIndex becomes
a no-op since contentless FTS5 also cannot delete individual rows;
stale entries are harmless (search JOINs filter them out) and the
index is fully rebuilt during FullRescan.
This commit is contained in:
2026-03-05 10:41:55 -05:00
parent 2439c2a728
commit 8e9a616037
4 changed files with 59 additions and 46 deletions
+6 -16
View File
@@ -860,7 +860,12 @@ func (l *Library) updateAudioFileMetadata(
)
}
// Index in FTS5 search_index (delete old entry, insert new).
// Re-index in FTS5 search_index.
// Contentless FTS5 (content='') does not support DELETE, so we
// cannot remove the old entry. Inserting a new row with the
// same rowid is accepted by FTS5 — the old entry becomes stale
// but harmless (search JOINs against track_metadata filter it).
// The index is fully rebuilt during FullRescan.
tags := result.tags
if tags == nil {
tags = &metadata.TrackMetadata{}
@@ -875,21 +880,6 @@ func (l *Library) updateAudioFileMetadata(
album := tags.Album
// SAFETY: FTS5 virtual table, see search.go:DeleteSearchIndex. Rowid parameterized.
if _, err := tx.ExecContext(
l.ctx,
`DELETE FROM search_index WHERE rowid = ?`,
result.existingFileID,
); err != nil {
l.logger.Warn(
"could not remove old FTS entry",
"id", result.existingFileID,
"err", err,
)
metrics.addWarning(result.absolutePath, "commit", err)
}
// SAFETY: FTS5 virtual table, see search.go:InsertSearchIndex. All values parameterized.
if _, err := tx.ExecContext(
l.ctx,
+9 -10
View File
@@ -160,22 +160,21 @@ func (l *Library) clearLibraryTables() error {
)
}
// Clear FTS5 search index.
// SAFETY: FTS5 virtual table, see search.go:ClearSearchIndex. No parameters; unconditional delete.
if _, err := tx.ExecContext(
l.ctx, `DELETE FROM search_index`,
); err != nil {
return fmt.Errorf(
"could not clear search index: %w", err,
)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf(
"could not commit library clear transaction: %w", err,
)
}
// Clear FTS5 search index AFTER the transaction.
// ClearSearchIndex drops and recreates the contentless FTS5
// virtual table, which cannot run inside a transaction.
if err := l.db.ClearSearchIndex(); err != nil {
return fmt.Errorf(
"could not clear search index: %w", err,
)
}
l.logger.Info("all library tables cleared")
return nil