From 9295655d19fe5cbae5a46bccdf9c8be1b0a57cfd Mon Sep 17 00:00:00 2001 From: notsafeforgit Date: Fri, 13 Mar 2026 18:39:34 -0700 Subject: [PATCH] fix: resolve image duplicate finder issues - Wrap FindDuplicateImages query in r.withReadTxn() to ensure a database transaction in context. - Use queryFunc instead of queryStruct for fetching multiple hashes, preventing runtime errors. - Fix N+1 query issue in duplicate grouping by using qb.FindMany() instead of qb.Find() for each duplicate image. - Revert searchColumns array to exclude "images.details" which was from another PR and remove related failing test. --- internal/api/resolver_query_find_image.go | 11 ++++++-- pkg/sqlite/image.go | 31 ++++++++++++----------- pkg/sqlite/image_test.go | 14 ---------- 3 files changed, 25 insertions(+), 31 deletions(-) diff --git a/internal/api/resolver_query_find_image.go b/internal/api/resolver_query_find_image.go index a09ca768e..f547151b1 100644 --- a/internal/api/resolver_query_find_image.go +++ b/internal/api/resolver_query_find_image.go @@ -135,6 +135,13 @@ func (r *queryResolver) AllImages(ctx context.Context) (ret []*models.Image, err return ret, nil } -func (r *queryResolver) FindDuplicateImages(ctx context.Context, distance int) ([][]*models.Image, error) { - return r.repository.Image.FindDuplicates(ctx, distance) +func (r *queryResolver) FindDuplicateImages(ctx context.Context, distance int) (ret [][]*models.Image, err error) { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + ret, err = r.repository.Image.FindDuplicates(ctx, distance) + return err + }); err != nil { + return nil, err + } + + return ret, nil } diff --git a/pkg/sqlite/image.go b/pkg/sqlite/image.go index 1e271cf73..780979270 100644 --- a/pkg/sqlite/image.go +++ b/pkg/sqlite/image.go @@ -838,7 +838,7 @@ func (qb *ImageStore) makeQuery(ctx context.Context, imageFilter *models.ImageFi ) filepathColumn := "folders.path || '" + string(filepath.Separator) + "' || files.basename" - searchColumns := []string{"images.title", "images.details", filepathColumn, "files_fingerprints.fingerprint"} + searchColumns := []string{"images.title", filepathColumn, "files_fingerprints.fingerprint"} query.parseQueryString(searchColumns, *q) } @@ -1104,29 +1104,30 @@ func (qb *ImageStore) FindDuplicates(ctx context.Context, distance int) ([][]*mo WHERE files_fingerprints.type = 'phash'` var hashes []*utils.Phash - err := imageRepository.queryStruct(ctx, query, nil, &hashes) - if err != nil { - return nil, err - } + if err := imageRepository.queryFunc(ctx, query, nil, false, func(rows *sqlx.Rows) error { + phash := utils.Phash{ + Bucket: -1, + Duration: -1, + } + if err := rows.StructScan(&phash); err != nil { + return err + } - for _, h := range hashes { - h.Bucket = -1 + hashes = append(hashes, &phash) + return nil + }); err != nil { + return nil, err } dupeIds := utils.FindDuplicates(hashes, distance, -1) var result [][]*models.Image for _, comp := range dupeIds { - var group []*models.Image - for _, id := range comp { - img, err := qb.Find(ctx, id) - if err == nil && img != nil { - group = append(group, img) + if images, err := qb.FindMany(ctx, comp); err == nil { + if len(images) > 1 { + result = append(result, images) } } - if len(group) > 1 { - result = append(result, group) - } } return result, nil diff --git a/pkg/sqlite/image_test.go b/pkg/sqlite/image_test.go index 85337c911..3bad40b3b 100644 --- a/pkg/sqlite/image_test.go +++ b/pkg/sqlite/image_test.go @@ -1596,20 +1596,6 @@ func TestImageQueryQ(t *testing.T) { }) } -func TestImageQueryQ_Details(t *testing.T) { - withTxn(func(ctx context.Context) error { - const imageIdx = 3 - - q := getImageStringValue(imageIdx, detailsField) - - sqb := db.Image - - imageQueryQ(ctx, t, sqb, q, imageIdx) - - return nil - }) -} - func queryImagesWithCount(ctx context.Context, sqb models.ImageReader, imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) ([]*models.Image, int, error) { result, err := sqb.Query(ctx, models.ImageQueryOptions{ QueryOptions: models.QueryOptions{