Batch: Pre-create new albums/labels to improve performance #271 #5324

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2025-11-20 16:56:14 +01:00
parent ad2470ca04
commit 117c8db73b
5 changed files with 435 additions and 230 deletions

View file

@ -84,7 +84,7 @@ func NewPhotosFormWithEntities(photos search.PhotoResults, preloaded map[string]
return nil
}
// Populate Albums and Labels from selected photos (no raw SQL; use preload helpers)
// Populate Albums and Labels from selected photos (no raw SQL; use preload helpers).
total := len(photos)
if total > 0 {
type albumAgg struct {
@ -120,7 +120,7 @@ func NewPhotosFormWithEntities(photos search.PhotoResults, preloaded map[string]
albumCount[a.AlbumUID] = v
}
// Labels on this photo (only visible ones: uncertainty < 100)
// Labels on this photo (only visible ones: uncertainty < 100).
for _, pl := range p.Labels {
if pl.Uncertainty >= 100 || pl.Label == nil || !pl.Label.HasID() {
continue
@ -136,7 +136,7 @@ func NewPhotosFormWithEntities(photos search.PhotoResults, preloaded map[string]
}
}
// Build Albums items
// Build Albums items.
frm.Albums.Items = make([]Item, 0, len(albumCount))
anyAlbumMixed := false
@ -148,7 +148,7 @@ func NewPhotosFormWithEntities(photos search.PhotoResults, preloaded map[string]
frm.Albums.Items = append(frm.Albums.Items, Item{Value: uid, Title: agg.title, Mixed: mixed, Action: ActionNone})
}
// Sort shared-first (Mixed=false), then by Title alphabetically
// Sort shared-first (Mixed=false), then by Title alphabetically.
sort.Slice(frm.Albums.Items, func(i, j int) bool {
if frm.Albums.Items[i].Mixed != frm.Albums.Items[j].Mixed {
return !frm.Albums.Items[i].Mixed && frm.Albums.Items[j].Mixed
@ -159,7 +159,7 @@ func NewPhotosFormWithEntities(photos search.PhotoResults, preloaded map[string]
frm.Albums.Mixed = anyAlbumMixed
frm.Albums.Action = ActionNone
// Build Labels items
// Build Labels items.
frm.Labels.Items = make([]Item, 0, len(labelCount))
anyLabelMixed := false
for uid, agg := range labelCount {
@ -170,7 +170,7 @@ func NewPhotosFormWithEntities(photos search.PhotoResults, preloaded map[string]
frm.Labels.Items = append(frm.Labels.Items, Item{Value: uid, Title: agg.name, Mixed: mixed, Action: ActionNone})
}
// Sort shared-first (Mixed=false), then by Title alphabetically
// Sort shared-first (Mixed=false), then by Title alphabetically.
sort.Slice(frm.Labels.Items, func(i, j int) bool {
if frm.Labels.Items[i].Mixed != frm.Labels.Items[j].Mixed {
return !frm.Labels.Items[i].Mixed && frm.Labels.Items[j].Mixed

View file

@ -2,7 +2,6 @@ package batch
import (
"fmt"
"strings"
"time"
"github.com/photoprism/photoprism/internal/entity"
@ -62,6 +61,9 @@ func PreparePhotoSaveRequests(photos search.PhotoResults, preloaded map[string]*
return nil, preloaded, MutationStats{}
}
// Pre-create any album/label add targets so Apply* can reuse resolved UIDs per photo.
resolveBatchItemValues(values)
if preloaded == nil {
preloaded = map[string]*entity.Photo{}
}
@ -224,226 +226,3 @@ func SavePhotos(requests []*PhotoSaveRequest) ([]bool, error) {
return results, nil
}
// savePhoto applies the batched form values to a single entity.Photo and writes
// only the changed columns back to the database.
func savePhoto(req *PhotoSaveRequest) (bool, error) {
if req == nil || req.Photo == nil || req.Form == nil || req.Values == nil {
return false, fmt.Errorf("batch: invalid save request")
}
p := req.Photo
formValues := req.Values
formData := req.Form
if !p.HasID() {
return false, fmt.Errorf("batch: photo has no id")
}
original := *p
details := p.GetDetails()
origDetails := *details
if shouldUpdateString(formValues.PhotoTitle) {
p.PhotoTitle = formData.PhotoTitle
p.TitleSrc = formData.TitleSrc
}
if shouldUpdateString(formValues.PhotoCaption) {
p.PhotoCaption = formData.PhotoCaption
p.CaptionSrc = formData.CaptionSrc
}
if formValues.PhotoType.Action == ActionUpdate {
p.PhotoType = formData.PhotoType
p.TypeSrc = formData.TypeSrc
}
if formValues.PhotoFavorite.Action == ActionUpdate {
p.PhotoFavorite = formValues.PhotoFavorite.Value
}
if formValues.PhotoPrivate.Action == ActionUpdate {
p.PhotoPrivate = formValues.PhotoPrivate.Value
}
if formValues.PhotoScan.Action == ActionUpdate {
p.PhotoScan = formValues.PhotoScan.Value
}
if formValues.PhotoPanorama.Action == ActionUpdate {
p.PhotoPanorama = formValues.PhotoPanorama.Value
}
timeChanged := formValues.PhotoDay.Action == ActionUpdate ||
formValues.PhotoMonth.Action == ActionUpdate ||
formValues.PhotoYear.Action == ActionUpdate ||
formValues.TimeZone.Action == ActionUpdate
if timeChanged {
p.PhotoYear = formData.PhotoYear
p.PhotoMonth = formData.PhotoMonth
p.PhotoDay = formData.PhotoDay
if formValues.TimeZone.Action == ActionUpdate {
p.TimeZone = formData.TimeZone
}
p.TakenAtLocal = formData.TakenAtLocal
p.TakenSrc = formData.TakenSrc
p.NormalizeValues()
if p.TimeZoneUTC() {
p.TakenAt = p.TakenAtLocal
} else {
p.TakenAt = p.GetTakenAt()
}
p.UpdateDateFields()
}
locationChanged := formValues.PhotoLat.Action == ActionUpdate ||
formValues.PhotoLng.Action == ActionUpdate ||
formValues.PhotoCountry.Action == ActionUpdate ||
formValues.PhotoAltitude.Action == ActionUpdate
if formValues.PhotoLat.Action == ActionUpdate {
p.PhotoLat = formValues.PhotoLat.Value
}
if formValues.PhotoLng.Action == ActionUpdate {
p.PhotoLng = formValues.PhotoLng.Value
}
if formValues.PhotoAltitude.Action == ActionUpdate {
p.PhotoAltitude = formValues.PhotoAltitude.Value
}
if formValues.PhotoCountry.Action == ActionUpdate {
p.PhotoCountry = formValues.PhotoCountry.Value
}
if locationChanged {
p.PlaceSrc = entity.SrcBatch
locKeywords, locLabels := p.UpdateLocation()
if len(locLabels) > 0 {
p.AddLabels(locLabels)
}
if len(locKeywords) > 0 {
words := txt.UniqueWords(txt.Words(details.Keywords))
words = append(words, locKeywords...)
details.Keywords = strings.Join(txt.UniqueWords(words), ", ")
}
}
if formValues.DetailsSubject.Action == ActionUpdate || formValues.DetailsSubject.Action == ActionRemove {
details.Subject = formData.Details.Subject
details.SubjectSrc = formData.Details.SubjectSrc
}
if formValues.DetailsArtist.Action == ActionUpdate || formValues.DetailsArtist.Action == ActionRemove {
details.Artist = formData.Details.Artist
details.ArtistSrc = formData.Details.ArtistSrc
}
if formValues.DetailsCopyright.Action == ActionUpdate || formValues.DetailsCopyright.Action == ActionRemove {
details.Copyright = formData.Details.Copyright
details.CopyrightSrc = formData.Details.CopyrightSrc
}
if formValues.DetailsLicense.Action == ActionUpdate || formValues.DetailsLicense.Action == ActionRemove {
details.License = formData.Details.License
details.LicenseSrc = formData.Details.LicenseSrc
}
updates := entity.Values{}
addUpdate := func(column string, changed bool, value interface{}) {
if changed {
updates[column] = value
}
}
addUpdate("photo_title", p.PhotoTitle != original.PhotoTitle, p.PhotoTitle)
addUpdate("title_src", p.TitleSrc != original.TitleSrc, p.TitleSrc)
addUpdate("photo_caption", p.PhotoCaption != original.PhotoCaption, p.PhotoCaption)
addUpdate("caption_src", p.CaptionSrc != original.CaptionSrc, p.CaptionSrc)
addUpdate("photo_type", p.PhotoType != original.PhotoType, p.PhotoType)
addUpdate("type_src", p.TypeSrc != original.TypeSrc, p.TypeSrc)
addUpdate("photo_favorite", p.PhotoFavorite != original.PhotoFavorite, p.PhotoFavorite)
addUpdate("photo_private", p.PhotoPrivate != original.PhotoPrivate, p.PhotoPrivate)
addUpdate("photo_scan", p.PhotoScan != original.PhotoScan, p.PhotoScan)
addUpdate("photo_panorama", p.PhotoPanorama != original.PhotoPanorama, p.PhotoPanorama)
addUpdate("photo_year", p.PhotoYear != original.PhotoYear, p.PhotoYear)
addUpdate("photo_month", p.PhotoMonth != original.PhotoMonth, p.PhotoMonth)
addUpdate("photo_day", p.PhotoDay != original.PhotoDay, p.PhotoDay)
addUpdate("time_zone", p.TimeZone != original.TimeZone, p.TimeZone)
addUpdate("taken_src", p.TakenSrc != original.TakenSrc, p.TakenSrc)
addUpdate("taken_at", !p.TakenAt.Equal(original.TakenAt), p.TakenAt)
addUpdate("taken_at_local", !p.TakenAtLocal.Equal(original.TakenAtLocal), p.TakenAtLocal)
addUpdate("photo_lat", p.PhotoLat != original.PhotoLat, p.PhotoLat)
addUpdate("photo_lng", p.PhotoLng != original.PhotoLng, p.PhotoLng)
addUpdate("photo_altitude", p.PhotoAltitude != original.PhotoAltitude, p.PhotoAltitude)
addUpdate("photo_country", p.PhotoCountry != original.PhotoCountry, p.PhotoCountry)
addUpdate("place_id", p.PlaceID != original.PlaceID, p.PlaceID)
addUpdate("cell_id", p.CellID != original.CellID, p.CellID)
addUpdate("place_src", p.PlaceSrc != original.PlaceSrc, p.PlaceSrc)
detailUpdates := entity.Values{}
if details.Subject != origDetails.Subject {
detailUpdates["subject"] = details.Subject
}
if details.SubjectSrc != origDetails.SubjectSrc {
detailUpdates["subject_src"] = details.SubjectSrc
}
if details.Artist != origDetails.Artist {
detailUpdates["artist"] = details.Artist
}
if details.ArtistSrc != origDetails.ArtistSrc {
detailUpdates["artist_src"] = details.ArtistSrc
}
if details.Copyright != origDetails.Copyright {
detailUpdates["copyright"] = details.Copyright
}
if details.CopyrightSrc != origDetails.CopyrightSrc {
detailUpdates["copyright_src"] = details.CopyrightSrc
}
if details.License != origDetails.License {
detailUpdates["license"] = details.License
}
if details.LicenseSrc != origDetails.LicenseSrc {
detailUpdates["license_src"] = details.LicenseSrc
}
if details.Keywords != origDetails.Keywords {
detailUpdates["keywords"] = details.Keywords
}
if len(updates) == 0 && len(detailUpdates) == 0 {
return false, nil
}
log.Debugf("batch: saving photo %s with updates=%v details=%v", p.PhotoUID, updates, detailUpdates)
edited := entity.Now()
updates["edited_at"] = edited
p.EditedAt = &edited
updates["updated_at"] = edited
p.UpdatedAt = edited
updates["checked_at"] = nil
p.CheckedAt = nil
if err := p.Updates(updates); err != nil {
return false, err
}
if len(detailUpdates) > 0 {
if err := details.Updates(detailUpdates); err != nil {
return false, err
}
}
return true, nil
}
// shouldUpdateString reports whether the provided field requests an update or removal.
func shouldUpdateString(v String) bool {
return v.Action == ActionUpdate || v.Action == ActionRemove
}

View file

@ -0,0 +1,232 @@
package batch
import (
"fmt"
"strings"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/pkg/txt"
)
// savePhoto applies the batched form values to a single entity.Photo and writes
// only the changed columns back to the database.
func savePhoto(req *PhotoSaveRequest) (bool, error) {
if req == nil || req.Photo == nil || req.Form == nil || req.Values == nil {
return false, fmt.Errorf("batch: invalid save request")
}
p := req.Photo
formValues := req.Values
formData := req.Form
if !p.HasID() {
return false, fmt.Errorf("batch: photo has no id")
}
original := *p
details := p.GetDetails()
origDetails := *details
if shouldUpdateString(formValues.PhotoTitle) {
p.PhotoTitle = formData.PhotoTitle
p.TitleSrc = formData.TitleSrc
}
if shouldUpdateString(formValues.PhotoCaption) {
p.PhotoCaption = formData.PhotoCaption
p.CaptionSrc = formData.CaptionSrc
}
if formValues.PhotoType.Action == ActionUpdate {
p.PhotoType = formData.PhotoType
p.TypeSrc = formData.TypeSrc
}
if formValues.PhotoFavorite.Action == ActionUpdate {
p.PhotoFavorite = formValues.PhotoFavorite.Value
}
if formValues.PhotoPrivate.Action == ActionUpdate {
p.PhotoPrivate = formValues.PhotoPrivate.Value
}
if formValues.PhotoScan.Action == ActionUpdate {
p.PhotoScan = formValues.PhotoScan.Value
}
if formValues.PhotoPanorama.Action == ActionUpdate {
p.PhotoPanorama = formValues.PhotoPanorama.Value
}
timeChanged := formValues.PhotoDay.Action == ActionUpdate ||
formValues.PhotoMonth.Action == ActionUpdate ||
formValues.PhotoYear.Action == ActionUpdate ||
formValues.TimeZone.Action == ActionUpdate
if timeChanged {
p.PhotoYear = formData.PhotoYear
p.PhotoMonth = formData.PhotoMonth
p.PhotoDay = formData.PhotoDay
if formValues.TimeZone.Action == ActionUpdate {
p.TimeZone = formData.TimeZone
}
p.TakenAtLocal = formData.TakenAtLocal
p.TakenSrc = formData.TakenSrc
p.NormalizeValues()
if p.TimeZoneUTC() {
p.TakenAt = p.TakenAtLocal
} else {
p.TakenAt = p.GetTakenAt()
}
p.UpdateDateFields()
}
locationChanged := formValues.PhotoLat.Action == ActionUpdate ||
formValues.PhotoLng.Action == ActionUpdate ||
formValues.PhotoCountry.Action == ActionUpdate ||
formValues.PhotoAltitude.Action == ActionUpdate
if formValues.PhotoLat.Action == ActionUpdate {
p.PhotoLat = formValues.PhotoLat.Value
}
if formValues.PhotoLng.Action == ActionUpdate {
p.PhotoLng = formValues.PhotoLng.Value
}
if formValues.PhotoAltitude.Action == ActionUpdate {
p.PhotoAltitude = formValues.PhotoAltitude.Value
}
if formValues.PhotoCountry.Action == ActionUpdate {
p.PhotoCountry = formValues.PhotoCountry.Value
}
if locationChanged {
p.PlaceSrc = entity.SrcBatch
locKeywords, locLabels := p.UpdateLocation()
if len(locLabels) > 0 {
p.AddLabels(locLabels)
}
if len(locKeywords) > 0 {
words := txt.UniqueWords(txt.Words(details.Keywords))
words = append(words, locKeywords...)
details.Keywords = strings.Join(txt.UniqueWords(words), ", ")
}
}
if formValues.DetailsSubject.Action == ActionUpdate || formValues.DetailsSubject.Action == ActionRemove {
details.Subject = formData.Details.Subject
details.SubjectSrc = formData.Details.SubjectSrc
}
if formValues.DetailsArtist.Action == ActionUpdate || formValues.DetailsArtist.Action == ActionRemove {
details.Artist = formData.Details.Artist
details.ArtistSrc = formData.Details.ArtistSrc
}
if formValues.DetailsCopyright.Action == ActionUpdate || formValues.DetailsCopyright.Action == ActionRemove {
details.Copyright = formData.Details.Copyright
details.CopyrightSrc = formData.Details.CopyrightSrc
}
if formValues.DetailsLicense.Action == ActionUpdate || formValues.DetailsLicense.Action == ActionRemove {
details.License = formData.Details.License
details.LicenseSrc = formData.Details.LicenseSrc
}
updates := entity.Values{}
addUpdate := func(column string, changed bool, value interface{}) {
if changed {
updates[column] = value
}
}
addUpdate("photo_title", p.PhotoTitle != original.PhotoTitle, p.PhotoTitle)
addUpdate("title_src", p.TitleSrc != original.TitleSrc, p.TitleSrc)
addUpdate("photo_caption", p.PhotoCaption != original.PhotoCaption, p.PhotoCaption)
addUpdate("caption_src", p.CaptionSrc != original.CaptionSrc, p.CaptionSrc)
addUpdate("photo_type", p.PhotoType != original.PhotoType, p.PhotoType)
addUpdate("type_src", p.TypeSrc != original.TypeSrc, p.TypeSrc)
addUpdate("photo_favorite", p.PhotoFavorite != original.PhotoFavorite, p.PhotoFavorite)
addUpdate("photo_private", p.PhotoPrivate != original.PhotoPrivate, p.PhotoPrivate)
addUpdate("photo_scan", p.PhotoScan != original.PhotoScan, p.PhotoScan)
addUpdate("photo_panorama", p.PhotoPanorama != original.PhotoPanorama, p.PhotoPanorama)
addUpdate("photo_year", p.PhotoYear != original.PhotoYear, p.PhotoYear)
addUpdate("photo_month", p.PhotoMonth != original.PhotoMonth, p.PhotoMonth)
addUpdate("photo_day", p.PhotoDay != original.PhotoDay, p.PhotoDay)
addUpdate("time_zone", p.TimeZone != original.TimeZone, p.TimeZone)
addUpdate("taken_src", p.TakenSrc != original.TakenSrc, p.TakenSrc)
addUpdate("taken_at", !p.TakenAt.Equal(original.TakenAt), p.TakenAt)
addUpdate("taken_at_local", !p.TakenAtLocal.Equal(original.TakenAtLocal), p.TakenAtLocal)
addUpdate("photo_lat", p.PhotoLat != original.PhotoLat, p.PhotoLat)
addUpdate("photo_lng", p.PhotoLng != original.PhotoLng, p.PhotoLng)
addUpdate("photo_altitude", p.PhotoAltitude != original.PhotoAltitude, p.PhotoAltitude)
addUpdate("photo_country", p.PhotoCountry != original.PhotoCountry, p.PhotoCountry)
addUpdate("place_id", p.PlaceID != original.PlaceID, p.PlaceID)
addUpdate("cell_id", p.CellID != original.CellID, p.CellID)
addUpdate("place_src", p.PlaceSrc != original.PlaceSrc, p.PlaceSrc)
detailUpdates := entity.Values{}
if details.Subject != origDetails.Subject {
detailUpdates["subject"] = details.Subject
}
if details.SubjectSrc != origDetails.SubjectSrc {
detailUpdates["subject_src"] = details.SubjectSrc
}
if details.Artist != origDetails.Artist {
detailUpdates["artist"] = details.Artist
}
if details.ArtistSrc != origDetails.ArtistSrc {
detailUpdates["artist_src"] = details.ArtistSrc
}
if details.Copyright != origDetails.Copyright {
detailUpdates["copyright"] = details.Copyright
}
if details.CopyrightSrc != origDetails.CopyrightSrc {
detailUpdates["copyright_src"] = details.CopyrightSrc
}
if details.License != origDetails.License {
detailUpdates["license"] = details.License
}
if details.LicenseSrc != origDetails.LicenseSrc {
detailUpdates["license_src"] = details.LicenseSrc
}
if details.Keywords != origDetails.Keywords {
detailUpdates["keywords"] = details.Keywords
}
if len(updates) == 0 && len(detailUpdates) == 0 {
return false, nil
}
log.Debugf("batch: saving photo %s with updates=%v details=%v", p.PhotoUID, updates, detailUpdates)
edited := entity.Now()
updates["edited_at"] = edited
p.EditedAt = &edited
updates["updated_at"] = edited
p.UpdatedAt = edited
updates["checked_at"] = nil
p.CheckedAt = nil
if err := p.Updates(updates); err != nil {
return false, err
}
if len(detailUpdates) > 0 {
if err := details.Updates(detailUpdates); err != nil {
return false, err
}
}
return true, nil
}
// shouldUpdateString reports whether the provided field requests an update or removal.
func shouldUpdateString(v String) bool {
return v.Action == ActionUpdate || v.Action == ActionRemove
}

View file

@ -0,0 +1,105 @@
package batch
import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/pkg/clean"
)
// resolveBatchItemValues pre-creates album and label references so later per-photo work only
// performs relation changes instead of issuing duplicate lookup/create queries for every photo.
func resolveBatchItemValues(values *PhotosForm) {
if values == nil {
return
}
resolveAlbumValues(&values.Albums)
resolveLabelValues(&values.Labels)
}
// resolveAlbumValues pre-creates album UIDs for add actions so ApplyAlbums only performs
// membership changes instead of repeated title lookups.
func resolveAlbumValues(items *Items) {
if items == nil || items.Action != ActionUpdate {
return
}
items.ResolveValuesByTitle(func(title, action string) string {
if action != ActionAdd || title == "" {
return ""
}
return ensureAlbumUID(title)
})
}
// resolveLabelValues pre-creates label UIDs for add actions to avoid recreating the same
// labels per photo when batch updates run.
func resolveLabelValues(items *Items) {
if items == nil || items.Action != ActionUpdate {
return
}
items.ResolveValuesByTitle(func(title, action string) string {
if action != ActionAdd || title == "" {
return ""
}
return ensureLabelUID(title)
})
}
// ensureAlbumUID returns the UID of the album with the provided title, creating or restoring
// the album on demand when it does not already exist. Returns an empty string on failure.
func ensureAlbumUID(title string) string {
if title == "" {
return ""
}
album := entity.NewUserAlbum(title, entity.AlbumManual, entity.DefaultOrderAlbum, entity.OwnerUnknown)
if existing := album.Find(); existing != nil && existing.HasID() {
if existing.Deleted() {
if err := existing.Restore(); err != nil {
log.Errorf("batch: failed to restore album %s: %s", clean.Log(title), err)
return ""
}
}
return existing.AlbumUID
}
if err := album.Create(); err != nil {
log.Errorf("batch: failed to create album %s: %s", clean.Log(title), err)
return ""
}
return album.AlbumUID
}
// ensureLabelUID resolves or creates a label for the given title and returns its UID,
// restoring deleted labels when necessary.
func ensureLabelUID(title string) string {
if title == "" {
return ""
}
label, err := entity.FindLabel(title, true)
if err != nil || label == nil || !label.HasUID() {
label = entity.FirstOrCreateLabel(entity.NewLabel(title, 0))
err = nil
}
if label == nil || !label.HasUID() {
log.Errorf("batch: failed to resolve label %s", clean.Log(title))
return ""
}
if label.Deleted() {
if restoreErr := label.Restore(); restoreErr != nil {
log.Errorf("batch: failed to restore label %s: %s", clean.Log(title), restoreErr)
return ""
}
}
return label.LabelUID
}

View file

@ -0,0 +1,89 @@
package batch
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/entity/search"
"github.com/photoprism/photoprism/pkg/rnd"
)
func TestPreparePhotoSaveRequestsResolvesItemTitles(t *testing.T) {
albumTitle := fmt.Sprintf("Batch Album %d", time.Now().UnixNano())
labelTitle := fmt.Sprintf("Batch Label %d", time.Now().UnixNano())
values := &PhotosForm{
Albums: Items{
Action: ActionUpdate,
Items: []Item{
{Title: albumTitle, Action: ActionAdd},
{Title: albumTitle, Action: ActionAdd},
{Title: albumTitle, Action: ActionRemove},
},
},
Labels: Items{
Action: ActionUpdate,
Items: []Item{
{Title: labelTitle, Action: ActionAdd},
{Title: labelTitle, Action: ActionRemove},
},
},
}
requests, _, _ := PreparePhotoSaveRequests(search.PhotoResults{}, nil, values)
require.Len(t, requests, 0)
albumUID := values.Albums.Items[0].Value
labelUID := values.Labels.Items[0].Value
require.True(t, rnd.IsUID(albumUID, entity.AlbumUID))
require.True(t, rnd.IsUID(labelUID, entity.LabelUID))
require.Equal(t, albumUID, values.Albums.Items[1].Value)
require.Equal(t, "", values.Albums.Items[2].Value)
require.Equal(t, "", values.Labels.Items[1].Value)
t.Cleanup(func() {
if albumUID != "" {
if album := entity.FindAlbum(entity.Album{AlbumUID: albumUID}); album != nil {
_ = album.Delete()
}
}
if labelUID != "" {
var label entity.Label
if err := entity.Db().Where("label_uid = ?", labelUID).First(&label).Error; err == nil {
_ = label.Delete()
}
}
})
}
func TestEnsureAlbumUIDCreatesAndReuses(t *testing.T) {
title := fmt.Sprintf("Resolver Album %d", time.Now().UnixNano())
uid := ensureAlbumUID(title)
require.True(t, rnd.IsUID(uid, entity.AlbumUID))
assert.Equal(t, uid, ensureAlbumUID(title))
if album := entity.FindAlbum(entity.Album{AlbumUID: uid}); album != nil {
t.Cleanup(func() { _ = album.Delete() })
}
}
func TestEnsureLabelUIDCreatesAndReuses(t *testing.T) {
title := fmt.Sprintf("Resolver Label %d", time.Now().UnixNano())
uid := ensureLabelUID(title)
require.True(t, rnd.IsUID(uid, entity.LabelUID))
assert.Equal(t, uid, ensureLabelUID(title))
t.Cleanup(func() {
var label entity.Label
if err := entity.Db().Where("label_uid = ?", uid).First(&label).Error; err == nil {
_ = label.Delete()
}
})
}