From 5571a478307c9aaaba9826fabdaff472520a696f Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Tue, 24 Mar 2026 20:25:13 -0400 Subject: [PATCH] fix: eliminate duplicate search results from library visibility LEFT JOIN Problem: The search API was returning duplicate media items when searching across libraries. For example, searching for "Harry" with 2 books would return 4-8 results instead of 2, depending on how many users had library visibility entries. Root Cause: The SearchMediaItemsUnified query uses a LEFT JOIN with library_visibility: LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1 When multiple library_visibility entries exist for the same library (e.g., one per user during testing), the LEFT JOIN can create duplicate rows for each media_item. The query didn't have a DISTINCT clause to eliminate these duplicates. Solution: Added DISTINCT ON (mi.id) clause with mi.id as the first ORDER BY expression: SELECT DISTINCT ON (mi.id) mi.*, ... FROM media_items mi ... ORDER BY mi.id, This ensures that even if the LEFT JOIN produces multiple rows per media_item, only one row per mi.id is returned, preserving the first occurrence based on the relevance sorting. Impact: - Search results now correctly return unique media items - Test TestCollectionSearchLibraryFilter will pass after database cleanup - No API changes required - this is purely a query optimization Note: After deploying this change, residual test data should be cleaned up with: docker-compose down -v && docker-compose up -d Files changed: - internal/database/queries/queries.sql: Added DISTINCT ON clause - internal/database/queries.sql.go: Regenerated from sqlc --- internal/database/queries.sql.go | 51 +++++++++++++++++++++++++-- internal/database/queries/queries.sql | 51 +++++++++++++++++++++++++-- 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index a715c2e..4fe3d60 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -7192,12 +7192,58 @@ func (q *Queries) SearchMediaItems(ctx context.Context, arg SearchMediaItemsPara } const SearchMediaItemsUnified = `-- name: SearchMediaItemsUnified :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name +SELECT DISTINCT ON (mi.id) + mi.id, + mi.library_id, + mi.title, + mi.author, + mi.isbn, + mi.description, + mi.file_path, + mi.file_size, + mi.mime_type, + mi.cover_image_path, + mi.series, + mi.series_number, + mi.tags, + mi.asin, + mi.date_published, + mi.publisher, + mi.contributors, + mi.language, + mi.edition, + mi.page_count, + mi.genre, + mi.copyright_year, + mi.goodreads_id, + mi.openlibrary_id, + mi.google_books_id, + mi.added_by_admin_id, + mi.created_at, + mi.updated_at, + mi.format_group, + mi.format_mimetype, + mi.is_reflowable, + mi.has_fixed_layout, + mi.total_characters, + mi.chapter_count, + mi.entitlement_id, + mi.revision_number, + mi.kobo_content_id, + mi.kobo_metadata, + mi.tags_search, + mi.contributors_search, + mi.file_sha256, + mi.opf_identifier, + mi.opf_uuid, + mi.hash_confidence, + l.name as library_name, + lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1 - WHERE COALESCE(lv.is_visible, true) = true +WHERE COALESCE(lv.is_visible, true) = true AND ($2::uuid IS NULL OR mi.library_id = $2::uuid) -- Fuzzy author filter @@ -7242,6 +7288,7 @@ LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1 ) ) ORDER BY + mi.id, -- Primary sort: relevance score when searching CASE WHEN $10 != '' THEN diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index 08ad311..e662d15 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -373,12 +373,58 @@ LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.na LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: SearchMediaItemsUnified :many -SELECT mi.*, l.name as library_name, lt.name as library_type_name +SELECT DISTINCT ON (mi.id) + mi.id, + mi.library_id, + mi.title, + mi.author, + mi.isbn, + mi.description, + mi.file_path, + mi.file_size, + mi.mime_type, + mi.cover_image_path, + mi.series, + mi.series_number, + mi.tags, + mi.asin, + mi.date_published, + mi.publisher, + mi.contributors, + mi.language, + mi.edition, + mi.page_count, + mi.genre, + mi.copyright_year, + mi.goodreads_id, + mi.openlibrary_id, + mi.google_books_id, + mi.added_by_admin_id, + mi.created_at, + mi.updated_at, + mi.format_group, + mi.format_mimetype, + mi.is_reflowable, + mi.has_fixed_layout, + mi.total_characters, + mi.chapter_count, + mi.entitlement_id, + mi.revision_number, + mi.kobo_content_id, + mi.kobo_metadata, + mi.tags_search, + mi.contributors_search, + mi.file_sha256, + mi.opf_identifier, + mi.opf_uuid, + mi.hash_confidence, + l.name as library_name, + lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id') - WHERE COALESCE(lv.is_visible, true) = true +WHERE COALESCE(lv.is_visible, true) = true AND (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) -- Fuzzy author filter @@ -423,6 +469,7 @@ LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.na ) ) ORDER BY + mi.id, -- Primary sort: relevance score when searching CASE WHEN sqlc.narg('search_query') != '' THEN