@@ -414,6 +414,108 @@ func (db *IndexDB) UpdateCacheSize(cacheSizeMB int) error {
414414 return nil
415415}
416416
417+ // GetFilesBySize retrieves all files with a specific size, optionally filtered by path prefix.
418+ // Used for duplicate detection - returns files ordered by name for efficient grouping.
419+ // Returns empty slice on database busy/lock errors (non-fatal).
420+ func (db * IndexDB ) GetFilesBySize (size int64 , pathPrefix string ) ([]* iteminfo.FileInfo , error ) {
421+ var query string
422+ var args []interface {}
423+
424+ if pathPrefix != "" {
425+ query = `
426+ SELECT path, name, size, mod_time, type, is_dir, is_hidden, has_preview
427+ FROM index_items
428+ WHERE size = ? AND is_dir = 0 AND path GLOB ?
429+ ORDER BY name
430+ `
431+ args = []interface {}{size , pathPrefix + "*" }
432+ } else {
433+ query = `
434+ SELECT path, name, size, mod_time, type, is_dir, is_hidden, has_preview
435+ FROM index_items
436+ WHERE size = ? AND is_dir = 0
437+ ORDER BY name
438+ `
439+ args = []interface {}{size }
440+ }
441+
442+ rows , err := db .Query (query , args ... )
443+ if err != nil {
444+ // Soft failure: DB is busy or locked, return empty slice
445+ if isBusyError (err ) || isTransactionError (err ) {
446+ return []* iteminfo.FileInfo {}, nil
447+ }
448+ return nil , err
449+ }
450+ defer rows .Close ()
451+
452+ var files []* iteminfo.FileInfo
453+ for rows .Next () {
454+ item , err := scanRow (rows )
455+ if err != nil {
456+ return nil , err
457+ }
458+ files = append (files , item )
459+ }
460+
461+ return files , rows .Err ()
462+ }
463+
464+ // GetSizeGroupsForDuplicates queries for all size groups that have 2+ files.
465+ // Returns sizes in descending order (largest first) and a count map.
466+ // Optionally filters by path prefix for scoped searches.
467+ // Returns empty results on database busy/lock errors (non-fatal).
468+ func (db * IndexDB ) GetSizeGroupsForDuplicates (minSize int64 , pathPrefix string ) ([]int64 , map [int64 ]int , error ) {
469+ var query string
470+ var args []interface {}
471+
472+ if pathPrefix != "" {
473+ query = `
474+ SELECT size, COUNT(*) as count
475+ FROM index_items
476+ WHERE size >= ? AND is_dir = 0 AND path GLOB ?
477+ GROUP BY size
478+ HAVING COUNT(*) >= 2
479+ ORDER BY size DESC
480+ `
481+ args = []interface {}{minSize , pathPrefix + "*" }
482+ } else {
483+ query = `
484+ SELECT size, COUNT(*) as count
485+ FROM index_items
486+ WHERE size >= ? AND is_dir = 0
487+ GROUP BY size
488+ HAVING COUNT(*) >= 2
489+ ORDER BY size DESC
490+ `
491+ args = []interface {}{minSize }
492+ }
493+
494+ rows , err := db .Query (query , args ... )
495+ if err != nil {
496+ // Soft failure: DB is busy or locked, return empty results
497+ if isBusyError (err ) || isTransactionError (err ) {
498+ return []int64 {}, make (map [int64 ]int ), nil
499+ }
500+ return nil , nil , err
501+ }
502+ defer rows .Close ()
503+
504+ var sizes []int64
505+ sizeCounts := make (map [int64 ]int )
506+ for rows .Next () {
507+ var size int64
508+ var count int
509+ if err := rows .Scan (& size , & count ); err != nil {
510+ return nil , nil , err
511+ }
512+ sizes = append (sizes , size )
513+ sizeCounts [size ] = count
514+ }
515+
516+ return sizes , sizeCounts , rows .Err ()
517+ }
518+
417519// Helper functions
418520
419521func getParentPath (path string ) string {
0 commit comments