package entity import ( "fmt" "strings" "time" "github.com/dustin/go-humanize/english" "github.com/jinzhu/gorm" "github.com/photoprism/photoprism/internal/mutex" ) type LabelPhotoCount struct { LabelID int PhotoCount int } type LabelPhotoCounts []LabelPhotoCount // LabelCounts returns the number of photos for each label ID. func LabelCounts() LabelPhotoCounts { result := LabelPhotoCounts{} if err := UnscopedDb().Raw(` SELECT label_id, SUM(photo_count) AS photo_count FROM ( SELECT l.id AS label_id, COUNT(*) AS photo_count FROM labels l JOIN photos_labels pl ON pl.label_id = l.id JOIN photos ph ON pl.photo_id = ph.id WHERE pl.uncertainty < 100 AND ph.photo_quality >= 0 AND ph.photo_private = 0 AND ph.deleted_at IS NULL GROUP BY l.id UNION ALL SELECT l.id AS label_id, COUNT(*) AS photo_count FROM labels l JOIN categories c ON c.category_id = l.id JOIN photos_labels pl ON pl.label_id = c.label_id JOIN photos ph ON pl.photo_id = ph.id WHERE pl.uncertainty < 100 AND ph.photo_quality >= 0 AND ph.photo_private = 0 AND ph.deleted_at IS NULL GROUP BY l.id) counts GROUP BY label_id `).Scan(&result).Error; err != nil { log.Errorf("label-count: %s", err.Error()) } return result } // UpdatePlacesCounts updates the places photo counts. func UpdatePlacesCounts() (err error) { mutex.IndexUpdate.Lock() defer mutex.IndexUpdate.Unlock() start := time.Now() // Update places. res := Db().Table("places"). UpdateColumn("photo_count", gorm.Expr("(SELECT COUNT(*) FROM photos p "+ "WHERE places.id = p.place_id "+ "AND p.photo_quality >= 0 "+ "AND p.photo_private = 0 "+ "AND p.deleted_at IS NULL)")) if res.Error != nil { return res.Error } log.Debugf("counts: updated %s [%s]", english.Plural(int(res.RowsAffected), "place", "places"), time.Since(start)) return nil } // UpdateSubjectCounts updates the subject file counts. func UpdateSubjectCounts() (err error) { mutex.IndexUpdate.Lock() defer mutex.IndexUpdate.Unlock() start := time.Now() var res *gorm.DB subjTable := Subject{}.TableName() filesTable := File{}.TableName() markerTable := Marker{}.TableName() condition := gorm.Expr("subj_type = ?", SubjPerson) switch DbDialect() { case MySQL: res = Db().Exec(`UPDATE ? LEFT JOIN ( SELECT m.subj_uid, COUNT(DISTINCT f.id) AS subj_files, COUNT(DISTINCT f.photo_id) AS subj_photos FROM ? f JOIN ? m ON f.file_uid = m.file_uid AND m.subj_uid IS NOT NULL AND m.subj_uid <> '' AND m.subj_uid IS NOT NULL WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL GROUP BY m.subj_uid ) b ON b.subj_uid = subjects.subj_uid SET subjects.file_count = CASE WHEN b.subj_files IS NULL THEN 0 ELSE b.subj_files END, subjects.photo_count = CASE WHEN b.subj_photos IS NULL THEN 0 ELSE b.subj_photos END WHERE ?`, gorm.Expr(subjTable), gorm.Expr(filesTable), gorm.Expr(markerTable), condition) case SQLite: // Update files count. res = Db().Table(subjTable). UpdateColumn("file_count", gorm.Expr("(SELECT COUNT(DISTINCT f.id) FROM files f "+ fmt.Sprintf("JOIN %s m ON f.file_uid = m.file_uid AND m.subj_uid = %s.subj_uid ", markerTable, subjTable)+" WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL) WHERE ?", condition)) // Update photo count. if res.Error != nil { return res.Error } else { photosRes := Db().Table(subjTable). UpdateColumn("photo_count", gorm.Expr("(SELECT COUNT(DISTINCT f.photo_id) FROM files f "+ fmt.Sprintf("JOIN %s m ON f.file_uid = m.file_uid AND m.subj_uid = %s.subj_uid ", markerTable, subjTable)+" WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL) WHERE ?", condition)) res.RowsAffected += photosRes.RowsAffected } default: return fmt.Errorf("sql: unsupported dialect %s", DbDialect()) } if res.Error != nil { return res.Error } log.Debugf("counts: updated %s [%s]", english.Plural(int(res.RowsAffected), "subject", "subjects"), time.Since(start)) return nil } // UpdateLabelCounts updates the label photo counts. func UpdateLabelCounts() (err error) { mutex.IndexUpdate.Lock() defer mutex.IndexUpdate.Unlock() start := time.Now() var res *gorm.DB if IsDialect(MySQL) { res = Db().Exec(`UPDATE labels LEFT JOIN ( SELECT p2.label_id, COUNT(DISTINCT photo_id) AS label_photos FROM ( SELECT pl.label_id as label_id, p.id AS photo_id FROM photos p JOIN photos_labels pl ON pl.photo_id = p.id AND pl.uncertainty < 100 WHERE p.photo_quality > -1 AND p.photo_private = 0 AND p.deleted_at IS NULL UNION SELECT c.category_id as label_id, p.id AS photo_id FROM photos p JOIN photos_labels pl ON pl.photo_id = p.id AND pl.uncertainty < 100 JOIN categories c ON c.label_id = pl.label_id WHERE p.photo_quality > -1 AND p.photo_private = 0 AND p.deleted_at IS NULL ) p2 GROUP BY p2.label_id ) b ON b.label_id = labels.id SET photo_count = CASE WHEN b.label_photos IS NULL THEN 0 ELSE b.label_photos END`) } else if IsDialect(SQLite) { res = Db(). Table("labels"). UpdateColumn("photo_count", gorm.Expr(`(SELECT photo_count FROM (SELECT label_id, SUM(photo_count) AS photo_count FROM ( SELECT l.id AS label_id, COUNT(*) AS photo_count FROM labels l JOIN photos_labels pl ON pl.label_id = l.id JOIN photos ph ON pl.photo_id = ph.id WHERE pl.uncertainty < 100 AND ph.photo_quality >= 0 AND ph.photo_private = 0 AND ph.deleted_at IS NULL GROUP BY l.id UNION ALL SELECT l.id AS label_id, COUNT(*) AS photo_count FROM labels l JOIN categories c ON c.category_id = l.id JOIN photos_labels pl ON pl.label_id = c.label_id JOIN photos ph ON pl.photo_id = ph.id WHERE pl.uncertainty < 100 AND ph.photo_quality >= 0 AND ph.photo_private = 0 AND ph.deleted_at IS NULL GROUP BY l.id) counts GROUP BY label_id) label_counts WHERE label_id = labels.id)`)) } else { return fmt.Errorf("sql: unsupported dialect %s", DbDialect()) } if res.Error != nil { return res.Error } log.Debugf("counts: updated %s [%s]", english.Plural(int(res.RowsAffected), "label", "labels"), time.Since(start)) return nil } // UpdateCounts updates precalculated photo and file counts. func UpdateCounts() (err error) { log.Info("index: updating counts") if err = UpdatePlacesCounts(); err != nil { if strings.Contains(err.Error(), "Error 1054") { log.Errorf("counts: failed updating places, potentially incompatible database version") log.Errorf("%s see https://jira.mariadb.org/browse/MDEV-25362", err) return nil } return err } if err = UpdateSubjectCounts(); err != nil { if strings.Contains(err.Error(), "Error 1054") { log.Errorf("counts: failed updating subjects, potentially incompatible database version") log.Errorf("%s see https://jira.mariadb.org/browse/MDEV-25362", err) return nil } return err } if err = UpdateLabelCounts(); err != nil { return err } /* TODO: Slow with many photos due to missing index. start = time.Now() // Update calendar album visibility. switch DbDialect() { default: if err = UnscopedDb().Exec(`UPDATE albums SET deleted_at = ? WHERE album_type=? AND id NOT IN ( SELECT a.id FROM albums a JOIN photos p ON a.album_month = MONTH(p.taken_at) AND a.album_year = YEAR(p.taken_at) AND p.deleted_at IS NULL AND p.photo_quality > -1 AND p.photo_private = 0 WHERE album_type=? GROUP BY a.id)`, TimeStamp(), AlbumMonth, AlbumMonth).Error; err != nil { return err } if err = UnscopedDb().Exec(`UPDATE albums SET deleted_at = NULL WHERE album_type=? AND id IN ( SELECT a.id FROM albums a JOIN photos p ON a.album_month = MONTH(p.taken_at) AND a.album_year = YEAR(p.taken_at) AND p.deleted_at IS NULL AND p.photo_quality > -1 AND p.photo_private = 0 WHERE album_type=? GROUP BY a.id)`, AlbumMonth, AlbumMonth).Error; err != nil { return err } } log.Debugf("calendar: updating visibility completed [%s]", time.Since(start)) */ return nil }