From 5fdfbaa7f1307738ffd0803ef5f48d149f39610c Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Mon, 21 Jun 2021 15:48:28 +1000 Subject: [PATCH] Query bug fixes (#1510) * Fix joins being dropped * Fix missing scene stash_id criterion * Refactor criterion handlers * Add tag alias filter * Remove handleCriterionFunc --- pkg/sqlite/filter.go | 28 +++---- pkg/sqlite/filter_internal_test.go | 49 ++++++++--- pkg/sqlite/gallery.go | 28 +++---- pkg/sqlite/image.go | 26 +++--- pkg/sqlite/movies.go | 6 +- pkg/sqlite/performer.go | 58 ++++++------- pkg/sqlite/scene.go | 83 +++++++------------ pkg/sqlite/stash_id_test.go | 2 + pkg/sqlite/tag.go | 14 ++-- .../src/components/Changelog/versions/v080.md | 1 + ui/v2.5/src/models/list-filter/tags.ts | 6 +- 11 files changed, 150 insertions(+), 151 deletions(-) diff --git a/pkg/sqlite/filter.go b/pkg/sqlite/filter.go index ebe10fd8d..dce1b11d3 100644 --- a/pkg/sqlite/filter.go +++ b/pkg/sqlite/filter.go @@ -28,6 +28,10 @@ type criterionHandler interface { type criterionHandlerFunc func(f *filterBuilder) +func (h criterionHandlerFunc) handle(f *filterBuilder) { + h(f) +} + type join struct { table string as string @@ -62,13 +66,17 @@ type joins []join func (j *joins) add(newJoins ...join) { // only add if not already joined for _, newJoin := range newJoins { + found := false for _, jj := range *j { if jj.equals(newJoin) { - return + found = true + break } } - *j = append(*j, newJoin) + if !found { + *j = append(*j, newJoin) + } } } @@ -284,15 +292,7 @@ func (f *filterBuilder) getError() error { // handleCriterion calls the handle function on the provided criterionHandler, // providing itself. func (f *filterBuilder) handleCriterion(handler criterionHandler) { - f.handleCriterionFunc(func(h *filterBuilder) { - handler.handle(h) - }) -} - -// handleCriterionFunc calls the provided criterion handler function providing -// itself. -func (f *filterBuilder) handleCriterionFunc(handler criterionHandlerFunc) { - handler(f) + handler.handle(f) } func (f *filterBuilder) setError(e error) { @@ -519,15 +519,9 @@ type stringListCriterionHandlerBuilder struct { func (m *stringListCriterionHandlerBuilder) handler(criterion *models.StringCriterionInput) criterionHandlerFunc { return func(f *filterBuilder) { if criterion != nil && len(criterion.Value) > 0 { - var args []interface{} - for _, tagID := range criterion.Value { - args = append(args, tagID) - } - m.addJoinTable(f) stringCriterionHandler(criterion, m.joinTable+"."+m.stringColumn)(f) - } } } diff --git a/pkg/sqlite/filter_internal_test.go b/pkg/sqlite/filter_internal_test.go index 20ca571e1..e7a39521d 100644 --- a/pkg/sqlite/filter_internal_test.go +++ b/pkg/sqlite/filter_internal_test.go @@ -9,6 +9,31 @@ import ( "github.com/stretchr/testify/assert" ) +func TestJoinsAddJoin(t *testing.T) { + var joins joins + + // add a single join + joins.add(join{table: "test"}) + + assert := assert.New(t) + + // ensure join was added + assert.Len(joins, 1) + + // add the same join and another + joins.add([]join{ + { + table: "test", + }, + { + table: "foo", + }, + }...) + + // should have added a single join + assert.Len(joins, 2) +} + func TestFilterBuilderAnd(t *testing.T) { assert := assert.New(t) @@ -437,7 +462,7 @@ func TestStringCriterionHandlerIncludes(t *testing.T) { const quotedValue = `"two words"` f := &filterBuilder{} - f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{ + f.handleCriterion(stringCriterionHandler(&models.StringCriterionInput{ Modifier: models.CriterionModifierIncludes, Value: value1, }, column)) @@ -449,7 +474,7 @@ func TestStringCriterionHandlerIncludes(t *testing.T) { assert.Equal("%words%", f.whereClauses[0].args[1]) f = &filterBuilder{} - f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{ + f.handleCriterion(stringCriterionHandler(&models.StringCriterionInput{ Modifier: models.CriterionModifierIncludes, Value: quotedValue, }, column)) @@ -468,7 +493,7 @@ func TestStringCriterionHandlerExcludes(t *testing.T) { const quotedValue = `"two words"` f := &filterBuilder{} - f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{ + f.handleCriterion(stringCriterionHandler(&models.StringCriterionInput{ Modifier: models.CriterionModifierExcludes, Value: value1, }, column)) @@ -480,7 +505,7 @@ func TestStringCriterionHandlerExcludes(t *testing.T) { assert.Equal("%words%", f.whereClauses[0].args[1]) f = &filterBuilder{} - f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{ + f.handleCriterion(stringCriterionHandler(&models.StringCriterionInput{ Modifier: models.CriterionModifierExcludes, Value: quotedValue, }, column)) @@ -498,7 +523,7 @@ func TestStringCriterionHandlerEquals(t *testing.T) { const value1 = "two words" f := &filterBuilder{} - f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{ + f.handleCriterion(stringCriterionHandler(&models.StringCriterionInput{ Modifier: models.CriterionModifierEquals, Value: value1, }, column)) @@ -516,7 +541,7 @@ func TestStringCriterionHandlerNotEquals(t *testing.T) { const value1 = "two words" f := &filterBuilder{} - f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{ + f.handleCriterion(stringCriterionHandler(&models.StringCriterionInput{ Modifier: models.CriterionModifierNotEquals, Value: value1, }, column)) @@ -535,7 +560,7 @@ func TestStringCriterionHandlerMatchesRegex(t *testing.T) { const invalidValue = "*two words" f := &filterBuilder{} - f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{ + f.handleCriterion(stringCriterionHandler(&models.StringCriterionInput{ Modifier: models.CriterionModifierMatchesRegex, Value: validValue, }, column)) @@ -547,7 +572,7 @@ func TestStringCriterionHandlerMatchesRegex(t *testing.T) { // ensure invalid regex sets error state f = &filterBuilder{} - f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{ + f.handleCriterion(stringCriterionHandler(&models.StringCriterionInput{ Modifier: models.CriterionModifierMatchesRegex, Value: invalidValue, }, column)) @@ -563,7 +588,7 @@ func TestStringCriterionHandlerNotMatchesRegex(t *testing.T) { const invalidValue = "*two words" f := &filterBuilder{} - f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{ + f.handleCriterion(stringCriterionHandler(&models.StringCriterionInput{ Modifier: models.CriterionModifierNotMatchesRegex, Value: validValue, }, column)) @@ -575,7 +600,7 @@ func TestStringCriterionHandlerNotMatchesRegex(t *testing.T) { // ensure invalid regex sets error state f = &filterBuilder{} - f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{ + f.handleCriterion(stringCriterionHandler(&models.StringCriterionInput{ Modifier: models.CriterionModifierNotMatchesRegex, Value: invalidValue, }, column)) @@ -589,7 +614,7 @@ func TestStringCriterionHandlerIsNull(t *testing.T) { const column = "column" f := &filterBuilder{} - f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{ + f.handleCriterion(stringCriterionHandler(&models.StringCriterionInput{ Modifier: models.CriterionModifierIsNull, }, column)) @@ -604,7 +629,7 @@ func TestStringCriterionHandlerNotNull(t *testing.T) { const column = "column" f := &filterBuilder{} - f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{ + f.handleCriterion(stringCriterionHandler(&models.StringCriterionInput{ Modifier: models.CriterionModifierNotNull, }, column)) diff --git a/pkg/sqlite/gallery.go b/pkg/sqlite/gallery.go index f16b9eebc..cfa894cb8 100644 --- a/pkg/sqlite/gallery.go +++ b/pkg/sqlite/gallery.go @@ -203,20 +203,20 @@ func (qb *galleryQueryBuilder) makeFilter(galleryFilter *models.GalleryFilterTyp query.not(qb.makeFilter(galleryFilter.Not)) } - query.handleCriterionFunc(boolCriterionHandler(galleryFilter.IsZip, "galleries.zip")) - query.handleCriterionFunc(stringCriterionHandler(galleryFilter.Path, "galleries.path")) - query.handleCriterionFunc(intCriterionHandler(galleryFilter.Rating, "galleries.rating")) - query.handleCriterionFunc(stringCriterionHandler(galleryFilter.URL, "galleries.url")) - query.handleCriterionFunc(boolCriterionHandler(galleryFilter.Organized, "galleries.organized")) - query.handleCriterionFunc(galleryIsMissingCriterionHandler(qb, galleryFilter.IsMissing)) - query.handleCriterionFunc(galleryTagsCriterionHandler(qb, galleryFilter.Tags)) - query.handleCriterionFunc(galleryTagCountCriterionHandler(qb, galleryFilter.TagCount)) - query.handleCriterionFunc(galleryPerformersCriterionHandler(qb, galleryFilter.Performers)) - query.handleCriterionFunc(galleryPerformerCountCriterionHandler(qb, galleryFilter.PerformerCount)) - query.handleCriterionFunc(galleryStudioCriterionHandler(qb, galleryFilter.Studios)) - query.handleCriterionFunc(galleryPerformerTagsCriterionHandler(qb, galleryFilter.PerformerTags)) - query.handleCriterionFunc(galleryAverageResolutionCriterionHandler(qb, galleryFilter.AverageResolution)) - query.handleCriterionFunc(galleryImageCountCriterionHandler(qb, galleryFilter.ImageCount)) + query.handleCriterion(boolCriterionHandler(galleryFilter.IsZip, "galleries.zip")) + query.handleCriterion(stringCriterionHandler(galleryFilter.Path, "galleries.path")) + query.handleCriterion(intCriterionHandler(galleryFilter.Rating, "galleries.rating")) + query.handleCriterion(stringCriterionHandler(galleryFilter.URL, "galleries.url")) + query.handleCriterion(boolCriterionHandler(galleryFilter.Organized, "galleries.organized")) + query.handleCriterion(galleryIsMissingCriterionHandler(qb, galleryFilter.IsMissing)) + query.handleCriterion(galleryTagsCriterionHandler(qb, galleryFilter.Tags)) + query.handleCriterion(galleryTagCountCriterionHandler(qb, galleryFilter.TagCount)) + query.handleCriterion(galleryPerformersCriterionHandler(qb, galleryFilter.Performers)) + query.handleCriterion(galleryPerformerCountCriterionHandler(qb, galleryFilter.PerformerCount)) + query.handleCriterion(galleryStudioCriterionHandler(qb, galleryFilter.Studios)) + query.handleCriterion(galleryPerformerTagsCriterionHandler(qb, galleryFilter.PerformerTags)) + query.handleCriterion(galleryAverageResolutionCriterionHandler(qb, galleryFilter.AverageResolution)) + query.handleCriterion(galleryImageCountCriterionHandler(qb, galleryFilter.ImageCount)) return query } diff --git a/pkg/sqlite/image.go b/pkg/sqlite/image.go index 254008f9c..b0933c93a 100644 --- a/pkg/sqlite/image.go +++ b/pkg/sqlite/image.go @@ -231,20 +231,20 @@ func (qb *imageQueryBuilder) makeFilter(imageFilter *models.ImageFilterType) *fi query.not(qb.makeFilter(imageFilter.Not)) } - query.handleCriterionFunc(stringCriterionHandler(imageFilter.Path, "images.path")) - query.handleCriterionFunc(intCriterionHandler(imageFilter.Rating, "images.rating")) - query.handleCriterionFunc(intCriterionHandler(imageFilter.OCounter, "images.o_counter")) - query.handleCriterionFunc(boolCriterionHandler(imageFilter.Organized, "images.organized")) - query.handleCriterionFunc(resolutionCriterionHandler(imageFilter.Resolution, "images.height", "images.width")) - query.handleCriterionFunc(imageIsMissingCriterionHandler(qb, imageFilter.IsMissing)) + query.handleCriterion(stringCriterionHandler(imageFilter.Path, "images.path")) + query.handleCriterion(intCriterionHandler(imageFilter.Rating, "images.rating")) + query.handleCriterion(intCriterionHandler(imageFilter.OCounter, "images.o_counter")) + query.handleCriterion(boolCriterionHandler(imageFilter.Organized, "images.organized")) + query.handleCriterion(resolutionCriterionHandler(imageFilter.Resolution, "images.height", "images.width")) + query.handleCriterion(imageIsMissingCriterionHandler(qb, imageFilter.IsMissing)) - query.handleCriterionFunc(imageTagsCriterionHandler(qb, imageFilter.Tags)) - query.handleCriterionFunc(imageTagCountCriterionHandler(qb, imageFilter.TagCount)) - query.handleCriterionFunc(imageGalleriesCriterionHandler(qb, imageFilter.Galleries)) - query.handleCriterionFunc(imagePerformersCriterionHandler(qb, imageFilter.Performers)) - query.handleCriterionFunc(imagePerformerCountCriterionHandler(qb, imageFilter.PerformerCount)) - query.handleCriterionFunc(imageStudioCriterionHandler(qb, imageFilter.Studios)) - query.handleCriterionFunc(imagePerformerTagsCriterionHandler(qb, imageFilter.PerformerTags)) + query.handleCriterion(imageTagsCriterionHandler(qb, imageFilter.Tags)) + query.handleCriterion(imageTagCountCriterionHandler(qb, imageFilter.TagCount)) + query.handleCriterion(imageGalleriesCriterionHandler(qb, imageFilter.Galleries)) + query.handleCriterion(imagePerformersCriterionHandler(qb, imageFilter.Performers)) + query.handleCriterion(imagePerformerCountCriterionHandler(qb, imageFilter.PerformerCount)) + query.handleCriterion(imageStudioCriterionHandler(qb, imageFilter.Studios)) + query.handleCriterion(imagePerformerTagsCriterionHandler(qb, imageFilter.PerformerTags)) return query } diff --git a/pkg/sqlite/movies.go b/pkg/sqlite/movies.go index 91f286945..e6887b47a 100644 --- a/pkg/sqlite/movies.go +++ b/pkg/sqlite/movies.go @@ -118,9 +118,9 @@ func (qb *movieQueryBuilder) All() ([]*models.Movie, error) { func (qb *movieQueryBuilder) makeFilter(movieFilter *models.MovieFilterType) *filterBuilder { query := &filterBuilder{} - query.handleCriterionFunc(movieIsMissingCriterionHandler(qb, movieFilter.IsMissing)) - query.handleCriterionFunc(stringCriterionHandler(movieFilter.URL, "movies.url")) - query.handleCriterionFunc(movieStudioCriterionHandler(qb, movieFilter.Studios)) + query.handleCriterion(movieIsMissingCriterionHandler(qb, movieFilter.IsMissing)) + query.handleCriterion(stringCriterionHandler(movieFilter.URL, "movies.url")) + query.handleCriterion(movieStudioCriterionHandler(qb, movieFilter.Studios)) return query } diff --git a/pkg/sqlite/performer.go b/pkg/sqlite/performer.go index f04a6689f..a7828159b 100644 --- a/pkg/sqlite/performer.go +++ b/pkg/sqlite/performer.go @@ -239,51 +239,51 @@ func (qb *performerQueryBuilder) makeFilter(filter *models.PerformerFilterType) } const tableName = performerTable - query.handleCriterionFunc(boolCriterionHandler(filter.FilterFavorites, tableName+".favorite")) + query.handleCriterion(boolCriterionHandler(filter.FilterFavorites, tableName+".favorite")) - query.handleCriterionFunc(yearFilterCriterionHandler(filter.BirthYear, tableName+".birthdate")) - query.handleCriterionFunc(yearFilterCriterionHandler(filter.DeathYear, tableName+".death_date")) + query.handleCriterion(yearFilterCriterionHandler(filter.BirthYear, tableName+".birthdate")) + query.handleCriterion(yearFilterCriterionHandler(filter.DeathYear, tableName+".death_date")) - query.handleCriterionFunc(performerAgeFilterCriterionHandler(filter.Age)) + query.handleCriterion(performerAgeFilterCriterionHandler(filter.Age)) - query.handleCriterionFunc(func(f *filterBuilder) { + query.handleCriterion(criterionHandlerFunc(func(f *filterBuilder) { if gender := filter.Gender; gender != nil { f.addWhere(tableName+".gender = ?", gender.Value.String()) } - }) + })) - query.handleCriterionFunc(performerIsMissingCriterionHandler(qb, filter.IsMissing)) - query.handleCriterionFunc(stringCriterionHandler(filter.Ethnicity, tableName+".ethnicity")) - query.handleCriterionFunc(stringCriterionHandler(filter.Country, tableName+".country")) - query.handleCriterionFunc(stringCriterionHandler(filter.EyeColor, tableName+".eye_color")) - query.handleCriterionFunc(stringCriterionHandler(filter.Height, tableName+".height")) - query.handleCriterionFunc(stringCriterionHandler(filter.Measurements, tableName+".measurements")) - query.handleCriterionFunc(stringCriterionHandler(filter.FakeTits, tableName+".fake_tits")) - query.handleCriterionFunc(stringCriterionHandler(filter.CareerLength, tableName+".career_length")) - query.handleCriterionFunc(stringCriterionHandler(filter.Tattoos, tableName+".tattoos")) - query.handleCriterionFunc(stringCriterionHandler(filter.Piercings, tableName+".piercings")) - query.handleCriterionFunc(intCriterionHandler(filter.Rating, tableName+".rating")) - query.handleCriterionFunc(stringCriterionHandler(filter.HairColor, tableName+".hair_color")) - query.handleCriterionFunc(stringCriterionHandler(filter.URL, tableName+".url")) - query.handleCriterionFunc(intCriterionHandler(filter.Weight, tableName+".weight")) - query.handleCriterionFunc(func(f *filterBuilder) { + query.handleCriterion(performerIsMissingCriterionHandler(qb, filter.IsMissing)) + query.handleCriterion(stringCriterionHandler(filter.Ethnicity, tableName+".ethnicity")) + query.handleCriterion(stringCriterionHandler(filter.Country, tableName+".country")) + query.handleCriterion(stringCriterionHandler(filter.EyeColor, tableName+".eye_color")) + query.handleCriterion(stringCriterionHandler(filter.Height, tableName+".height")) + query.handleCriterion(stringCriterionHandler(filter.Measurements, tableName+".measurements")) + query.handleCriterion(stringCriterionHandler(filter.FakeTits, tableName+".fake_tits")) + query.handleCriterion(stringCriterionHandler(filter.CareerLength, tableName+".career_length")) + query.handleCriterion(stringCriterionHandler(filter.Tattoos, tableName+".tattoos")) + query.handleCriterion(stringCriterionHandler(filter.Piercings, tableName+".piercings")) + query.handleCriterion(intCriterionHandler(filter.Rating, tableName+".rating")) + query.handleCriterion(stringCriterionHandler(filter.HairColor, tableName+".hair_color")) + query.handleCriterion(stringCriterionHandler(filter.URL, tableName+".url")) + query.handleCriterion(intCriterionHandler(filter.Weight, tableName+".weight")) + query.handleCriterion(criterionHandlerFunc(func(f *filterBuilder) { if filter.StashID != nil { qb.stashIDRepository().join(f, "performer_stash_ids", "performers.id") stringCriterionHandler(filter.StashID, "performer_stash_ids.stash_id")(f) } - }) + })) // TODO - need better handling of aliases - query.handleCriterionFunc(stringCriterionHandler(filter.Aliases, tableName+".aliases")) + query.handleCriterion(stringCriterionHandler(filter.Aliases, tableName+".aliases")) - query.handleCriterionFunc(performerTagsCriterionHandler(qb, filter.Tags)) + query.handleCriterion(performerTagsCriterionHandler(qb, filter.Tags)) - query.handleCriterionFunc(performerStudiosCriterionHandler(filter.Studios)) + query.handleCriterion(performerStudiosCriterionHandler(filter.Studios)) - query.handleCriterionFunc(performerTagCountCriterionHandler(qb, filter.TagCount)) - query.handleCriterionFunc(performerSceneCountCriterionHandler(qb, filter.SceneCount)) - query.handleCriterionFunc(performerImageCountCriterionHandler(qb, filter.ImageCount)) - query.handleCriterionFunc(performerGalleryCountCriterionHandler(qb, filter.GalleryCount)) + query.handleCriterion(performerTagCountCriterionHandler(qb, filter.TagCount)) + query.handleCriterion(performerSceneCountCriterionHandler(qb, filter.SceneCount)) + query.handleCriterion(performerImageCountCriterionHandler(qb, filter.ImageCount)) + query.handleCriterion(performerGalleryCountCriterionHandler(qb, filter.GalleryCount)) return query } diff --git a/pkg/sqlite/scene.go b/pkg/sqlite/scene.go index 8997837af..511e1580c 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -353,25 +353,32 @@ func (qb *sceneQueryBuilder) makeFilter(sceneFilter *models.SceneFilterType) *fi query.not(qb.makeFilter(sceneFilter.Not)) } - query.handleCriterionFunc(stringCriterionHandler(sceneFilter.Path, "scenes.path")) - query.handleCriterionFunc(intCriterionHandler(sceneFilter.Rating, "scenes.rating")) - query.handleCriterionFunc(intCriterionHandler(sceneFilter.OCounter, "scenes.o_counter")) - query.handleCriterionFunc(boolCriterionHandler(sceneFilter.Organized, "scenes.organized")) - query.handleCriterionFunc(durationCriterionHandler(sceneFilter.Duration, "scenes.duration")) - query.handleCriterionFunc(resolutionCriterionHandler(sceneFilter.Resolution, "scenes.height", "scenes.width")) - query.handleCriterionFunc(hasMarkersCriterionHandler(sceneFilter.HasMarkers)) - query.handleCriterionFunc(sceneIsMissingCriterionHandler(qb, sceneFilter.IsMissing)) - query.handleCriterionFunc(stringCriterionHandler(sceneFilter.URL, "scenes.url")) - query.handleCriterionFunc(stringCriterionHandler(sceneFilter.StashID, "scene_stash_ids.stash_id")) - query.handleCriterionFunc(boolCriterionHandler(sceneFilter.Interactive, "scenes.interactive")) + query.handleCriterion(stringCriterionHandler(sceneFilter.Path, "scenes.path")) + query.handleCriterion(intCriterionHandler(sceneFilter.Rating, "scenes.rating")) + query.handleCriterion(intCriterionHandler(sceneFilter.OCounter, "scenes.o_counter")) + query.handleCriterion(boolCriterionHandler(sceneFilter.Organized, "scenes.organized")) + query.handleCriterion(durationCriterionHandler(sceneFilter.Duration, "scenes.duration")) + query.handleCriterion(resolutionCriterionHandler(sceneFilter.Resolution, "scenes.height", "scenes.width")) + query.handleCriterion(hasMarkersCriterionHandler(sceneFilter.HasMarkers)) + query.handleCriterion(sceneIsMissingCriterionHandler(qb, sceneFilter.IsMissing)) + query.handleCriterion(stringCriterionHandler(sceneFilter.URL, "scenes.url")) - query.handleCriterionFunc(sceneTagsCriterionHandler(qb, sceneFilter.Tags)) - query.handleCriterionFunc(sceneTagCountCriterionHandler(qb, sceneFilter.TagCount)) - query.handleCriterionFunc(scenePerformersCriterionHandler(qb, sceneFilter.Performers)) - query.handleCriterionFunc(scenePerformerCountCriterionHandler(qb, sceneFilter.PerformerCount)) - query.handleCriterionFunc(sceneStudioCriterionHandler(qb, sceneFilter.Studios)) - query.handleCriterionFunc(sceneMoviesCriterionHandler(qb, sceneFilter.Movies)) - query.handleCriterionFunc(scenePerformerTagsCriterionHandler(qb, sceneFilter.PerformerTags)) + query.handleCriterion(criterionHandlerFunc(func(f *filterBuilder) { + if sceneFilter.StashID != nil { + qb.stashIDRepository().join(f, "scene_stash_ids", "scenes.id") + stringCriterionHandler(sceneFilter.StashID, "scene_stash_ids.stash_id")(f) + } + })) + + query.handleCriterion(boolCriterionHandler(sceneFilter.Interactive, "scenes.interactive")) + + query.handleCriterion(sceneTagsCriterionHandler(qb, sceneFilter.Tags)) + query.handleCriterion(sceneTagCountCriterionHandler(qb, sceneFilter.TagCount)) + query.handleCriterion(scenePerformersCriterionHandler(qb, sceneFilter.Performers)) + query.handleCriterion(scenePerformerCountCriterionHandler(qb, sceneFilter.PerformerCount)) + query.handleCriterion(sceneStudioCriterionHandler(qb, sceneFilter.Studios)) + query.handleCriterion(sceneMoviesCriterionHandler(qb, sceneFilter.Movies)) + query.handleCriterion(scenePerformerTagsCriterionHandler(qb, sceneFilter.PerformerTags)) return query } @@ -401,10 +408,6 @@ func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilt } filter := qb.makeFilter(sceneFilter) - if sceneFilter.StashID != nil { - qb.stashIDRepository().join(filter, "scene_stash_ids", "scenes.id") - } - query.addFilter(filter) qb.setSceneSort(&query, findFilter) @@ -427,14 +430,6 @@ func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilt return scenes, countResult, nil } -func appendClause(clauses []string, clause string) []string { - if clause != "" { - return append(clauses, clause) - } - - return clauses -} - func durationCriterionHandler(durationFilter *models.IntCriterionInput, column string) criterionHandlerFunc { return func(f *filterBuilder) { if durationFilter != nil { @@ -524,6 +519,9 @@ func sceneIsMissingCriterionHandler(qb *sceneQueryBuilder, isMissing *string) cr case "tags": qb.tagsRepository().join(f, "tags_join", "scenes.id") f.addWhere("tags_join.scene_id IS NULL") + case "stash_id": + qb.stashIDRepository().join(f, "scene_stash_ids", "scenes.id") + f.addWhere("scene_stash_ids.scene_id IS NULL") default: f.addWhere("(scenes." + *isMissing + " IS NULL OR TRIM(scenes." + *isMissing + ") = '')") } @@ -638,31 +636,6 @@ func scenePerformerTagsCriterionHandler(qb *sceneQueryBuilder, performerTagsFilt } } -func handleScenePerformerTagsCriterion(query *queryBuilder, performerTagsFilter *models.MultiCriterionInput) { - if performerTagsFilter != nil && len(performerTagsFilter.Value) > 0 { - for _, tagID := range performerTagsFilter.Value { - query.addArg(tagID) - } - - query.body += " LEFT JOIN performers_tags AS performer_tags_join on performers_join.performer_id = performer_tags_join.performer_id" - - if performerTagsFilter.Modifier == models.CriterionModifierIncludes { - // includes any of the provided ids - query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value))) - } else if performerTagsFilter.Modifier == models.CriterionModifierIncludesAll { - // includes all of the provided ids - query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value))) - query.addHaving(fmt.Sprintf("count(distinct performer_tags_join.tag_id) IS %d", len(performerTagsFilter.Value))) - } else if performerTagsFilter.Modifier == models.CriterionModifierExcludes { - query.addWhere(fmt.Sprintf(`not exists - (select performers_scenes.performer_id from performers_scenes - left join performers_tags on performers_tags.performer_id = performers_scenes.performer_id where - performers_scenes.scene_id = scenes.id AND - performers_tags.tag_id in %s)`, getInBinding(len(performerTagsFilter.Value)))) - } - } -} - func (qb *sceneQueryBuilder) getDefaultSceneSort() string { return " ORDER BY scenes.path, scenes.date ASC " } diff --git a/pkg/sqlite/stash_id_test.go b/pkg/sqlite/stash_id_test.go index 943abc9c0..5c9b44f4c 100644 --- a/pkg/sqlite/stash_id_test.go +++ b/pkg/sqlite/stash_id_test.go @@ -1,3 +1,5 @@ +// +build integration + package sqlite_test import ( diff --git a/pkg/sqlite/tag.go b/pkg/sqlite/tag.go index d05fb4c9b..444577a0a 100644 --- a/pkg/sqlite/tag.go +++ b/pkg/sqlite/tag.go @@ -279,14 +279,14 @@ func (qb *tagQueryBuilder) makeFilter(tagFilter *models.TagFilterType) *filterBu // } // } - query.handleCriterionFunc(stringCriterionHandler(tagFilter.Name, tagTable+".name")) - query.handleCriterionFunc(tagAliasCriterionHandler(qb, tagFilter.Aliases)) + query.handleCriterion(stringCriterionHandler(tagFilter.Name, tagTable+".name")) + query.handleCriterion(tagAliasCriterionHandler(qb, tagFilter.Aliases)) - query.handleCriterionFunc(tagIsMissingCriterionHandler(qb, tagFilter.IsMissing)) - query.handleCriterionFunc(tagSceneCountCriterionHandler(qb, tagFilter.SceneCount)) - query.handleCriterionFunc(tagImageCountCriterionHandler(qb, tagFilter.ImageCount)) - query.handleCriterionFunc(tagGalleryCountCriterionHandler(qb, tagFilter.GalleryCount)) - query.handleCriterionFunc(tagPerformerCountCriterionHandler(qb, tagFilter.PerformerCount)) + query.handleCriterion(tagIsMissingCriterionHandler(qb, tagFilter.IsMissing)) + query.handleCriterion(tagSceneCountCriterionHandler(qb, tagFilter.SceneCount)) + query.handleCriterion(tagImageCountCriterionHandler(qb, tagFilter.ImageCount)) + query.handleCriterion(tagGalleryCountCriterionHandler(qb, tagFilter.GalleryCount)) + query.handleCriterion(tagPerformerCountCriterionHandler(qb, tagFilter.PerformerCount)) return query } diff --git a/ui/v2.5/src/components/Changelog/versions/v080.md b/ui/v2.5/src/components/Changelog/versions/v080.md index 5811aa850..bc9e32dca 100644 --- a/ui/v2.5/src/components/Changelog/versions/v080.md +++ b/ui/v2.5/src/components/Changelog/versions/v080.md @@ -26,6 +26,7 @@ * Add button to remove studio stash ID. ([#1378](https://github.com/stashapp/stash/pull/1378)) ### 🐛 Bug fixes +* Fix query with multiple table joins causing invalid query SQL. ([#1510](https://github.com/stashapp/stash/pull/1510)) * Fix file move detection when case of filename is changed on case-insensitive file systems. ([#1426](https://github.com/stashapp/stash/issues/1426)) * Fix auto-tagger not tagging scenes with no whitespace in name. ([#1488](https://github.com/stashapp/stash/pull/1488)) * Fix click/drag to select scenes. ([#1476](https://github.com/stashapp/stash/pull/1476)) diff --git a/ui/v2.5/src/models/list-filter/tags.ts b/ui/v2.5/src/models/list-filter/tags.ts index fd533d14b..17b8d4107 100644 --- a/ui/v2.5/src/models/list-filter/tags.ts +++ b/ui/v2.5/src/models/list-filter/tags.ts @@ -1,4 +1,7 @@ -import { createMandatoryNumberCriterionOption } from "./criteria/criterion"; +import { + createMandatoryNumberCriterionOption, + createStringCriterionOption, +} from "./criteria/criterion"; import { TagIsMissingCriterionOption } from "./criteria/is-missing"; import { ListFilterOptions } from "./filter-options"; import { DisplayMode } from "./types"; @@ -34,6 +37,7 @@ const sortByOptions = [ const displayModeOptions = [DisplayMode.Grid, DisplayMode.List]; const criterionOptions = [ TagIsMissingCriterionOption, + createStringCriterionOption("aliases"), createMandatoryNumberCriterionOption("scene_count"), createMandatoryNumberCriterionOption("image_count"), createMandatoryNumberCriterionOption("gallery_count"),