From 1691280d1b202fb0a556026cd5d460e4ae1cd7c9 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Mon, 15 Dec 2025 11:49:30 +1100 Subject: [PATCH] Fix excludes handling in performer studio filter (#6413) --- pkg/sqlite/performer_filter.go | 63 ++++++++++++++++------- pkg/sqlite/performer_test.go | 94 +++++++++++++++++++++++++++++++++- pkg/sqlite/setup_test.go | 39 ++++++++------ 3 files changed, 162 insertions(+), 34 deletions(-) diff --git a/pkg/sqlite/performer_filter.go b/pkg/sqlite/performer_filter.go index 29bc75a74..11d3138bc 100644 --- a/pkg/sqlite/performer_filter.go +++ b/pkg/sqlite/performer_filter.go @@ -447,7 +447,7 @@ func (qb *performerFilterHandler) studiosCriterionHandler(studios *models.Hierar return } - if len(studios.Value) == 0 { + if len(studios.Value) == 0 && len(studios.Excludes) == 0 { return } @@ -464,27 +464,54 @@ func (qb *performerFilterHandler) studiosCriterionHandler(studios *models.Hierar return } - const derivedPerformerStudioTable = "performer_studio" - valuesClause, err := getHierarchicalValues(ctx, studios.Value, studioTable, "", "parent_id", "child_id", studios.Depth) - if err != nil { - f.setError(err) - return - } - f.addWith("studio(root_id, item_id) AS (" + valuesClause + ")") + if len(studios.Value) > 0 { + const derivedPerformerStudioTable = "performer_studio" + valuesClause, err := getHierarchicalValues(ctx, studios.Value, studioTable, "", "parent_id", "child_id", studios.Depth) + if err != nil { + f.setError(err) + return + } + f.addWith("studio(root_id, item_id) AS (" + valuesClause + ")") - templStr := `SELECT performer_id FROM {primaryTable} + templStr := `SELECT performer_id FROM {primaryTable} + INNER JOIN {joinTable} ON {primaryTable}.id = {joinTable}.{primaryFK} + INNER JOIN studio ON {primaryTable}.studio_id = studio.item_id` + + var unions []string + for _, c := range formatMaps { + unions = append(unions, utils.StrFormat(templStr, c)) + } + + f.addWith(fmt.Sprintf("%s AS (%s)", derivedPerformerStudioTable, strings.Join(unions, " UNION "))) + + f.addLeftJoin(derivedPerformerStudioTable, "", fmt.Sprintf("performers.id = %s.performer_id", derivedPerformerStudioTable)) + f.addWhere(fmt.Sprintf("%s.performer_id IS %s NULL", derivedPerformerStudioTable, clauseCondition)) + } + + // #6412 - handle excludes as well + if len(studios.Excludes) > 0 { + excludeValuesClause, err := getHierarchicalValues(ctx, studios.Excludes, studioTable, "", "parent_id", "child_id", studios.Depth) + if err != nil { + f.setError(err) + return + } + f.addWith("exclude_studio(root_id, item_id) AS (" + excludeValuesClause + ")") + + excludeTemplStr := `SELECT performer_id FROM {primaryTable} INNER JOIN {joinTable} ON {primaryTable}.id = {joinTable}.{primaryFK} - INNER JOIN studio ON {primaryTable}.studio_id = studio.item_id` + INNER JOIN exclude_studio ON {primaryTable}.studio_id = exclude_studio.item_id` - var unions []string - for _, c := range formatMaps { - unions = append(unions, utils.StrFormat(templStr, c)) + var unions []string + for _, c := range formatMaps { + unions = append(unions, utils.StrFormat(excludeTemplStr, c)) + } + + const excludePerformerStudioTable = "performer_studio_exclude" + f.addWith(fmt.Sprintf("%s AS (%s)", excludePerformerStudioTable, strings.Join(unions, " UNION "))) + + f.addLeftJoin(excludePerformerStudioTable, "", fmt.Sprintf("performers.id = %s.performer_id", excludePerformerStudioTable)) + f.addWhere(fmt.Sprintf("%s.performer_id IS NULL", excludePerformerStudioTable)) } - - f.addWith(fmt.Sprintf("%s AS (%s)", derivedPerformerStudioTable, strings.Join(unions, " UNION "))) - - f.addLeftJoin(derivedPerformerStudioTable, "", fmt.Sprintf("performers.id = %s.performer_id", derivedPerformerStudioTable)) - f.addWhere(fmt.Sprintf("%s.performer_id IS %s NULL", derivedPerformerStudioTable, clauseCondition)) } } } diff --git a/pkg/sqlite/performer_test.go b/pkg/sqlite/performer_test.go index d5d8ce2fa..190d80e31 100644 --- a/pkg/sqlite/performer_test.go +++ b/pkg/sqlite/performer_test.go @@ -1160,6 +1160,98 @@ func TestPerformerQuery(t *testing.T) { []int{performerIdx1WithScene, performerIdxWithScene}, false, }, + { + "include scene studio", + nil, + &models.PerformerFilterType{ + Studios: &models.HierarchicalMultiCriterionInput{ + Value: []string{strconv.Itoa(studioIDs[studioIdxWithScenePerformer])}, + Modifier: models.CriterionModifierIncludes, + }, + }, + []int{performerIdxWithSceneStudio}, + nil, + false, + }, + { + "include image studio", + nil, + &models.PerformerFilterType{ + Studios: &models.HierarchicalMultiCriterionInput{ + Value: []string{strconv.Itoa(studioIDs[studioIdxWithImagePerformer])}, + Modifier: models.CriterionModifierIncludes, + }, + }, + []int{performerIdxWithImageStudio}, + nil, + false, + }, + { + "include gallery studio", + nil, + &models.PerformerFilterType{ + Studios: &models.HierarchicalMultiCriterionInput{ + Value: []string{strconv.Itoa(studioIDs[studioIdxWithGalleryPerformer])}, + Modifier: models.CriterionModifierIncludes, + }, + }, + []int{performerIdxWithGalleryStudio}, + nil, + false, + }, + { + "exclude scene studio", + nil, + &models.PerformerFilterType{ + Studios: &models.HierarchicalMultiCriterionInput{ + Value: []string{strconv.Itoa(studioIDs[studioIdxWithScenePerformer])}, + Modifier: models.CriterionModifierExcludes, + }, + }, + nil, + []int{performerIdxWithSceneStudio}, + false, + }, + { + "exclude image studio", + nil, + &models.PerformerFilterType{ + Studios: &models.HierarchicalMultiCriterionInput{ + Value: []string{strconv.Itoa(studioIDs[studioIdxWithImagePerformer])}, + Modifier: models.CriterionModifierExcludes, + }, + }, + nil, + []int{performerIdxWithImageStudio}, + false, + }, + { + "exclude gallery studio", + nil, + &models.PerformerFilterType{ + Studios: &models.HierarchicalMultiCriterionInput{ + Value: []string{strconv.Itoa(studioIDs[studioIdxWithGalleryPerformer])}, + Modifier: models.CriterionModifierExcludes, + }, + }, + nil, + []int{performerIdxWithGalleryStudio}, + false, + }, + { + "include and exclude scene studio", + nil, + &models.PerformerFilterType{ + Studios: &models.HierarchicalMultiCriterionInput{ + Value: []string{strconv.Itoa(studioIDs[studioIdx1WithTwoScenePerformer])}, + Modifier: models.CriterionModifierIncludes, + Excludes: []string{strconv.Itoa(studioIDs[studioIdx2WithTwoScenePerformer])}, + }, + }, + nil, + []int{performerIdxWithTwoSceneStudio}, + false, + }, } for _, tt := range tests { @@ -2260,7 +2352,7 @@ func TestPerformerQuerySortScenesCount(t *testing.T) { assert.True(t, len(performers) > 0) lastPerformer := performers[len(performers)-1] - assert.Equal(t, performerIDs[performerIdxWithTag], lastPerformer.ID) + assert.Equal(t, performerIDs[performerIdxWithTwoSceneStudio], lastPerformer.ID) return nil }) diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index 2e95012b5..63c66fd06 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -77,6 +77,8 @@ const ( sceneIdxWithPerformerTwoTags sceneIdxWithSpacedName sceneIdxWithStudioPerformer + sceneIdx1WithTwoStudioPerformer + sceneIdx2WithTwoStudioPerformer sceneIdxWithGrandChildStudio sceneIdxMissingPhash sceneIdxWithPerformerParentTag @@ -138,6 +140,7 @@ const ( performerIdxWithSceneStudio performerIdxWithImageStudio performerIdxWithGalleryStudio + performerIdxWithTwoSceneStudio performerIdxWithParentTag // new indexes above // performers with dup names start from the end @@ -257,6 +260,8 @@ const ( studioIdxWithScenePerformer studioIdxWithImagePerformer studioIdxWithGalleryPerformer + studioIdx1WithTwoScenePerformer + studioIdx2WithTwoScenePerformer studioIdxWithTag studioIdx2WithTag studioIdxWithTwoTags @@ -384,16 +389,18 @@ var ( } scenePerformers = linkMap{ - sceneIdxWithPerformer: {performerIdxWithScene}, - sceneIdxWithTwoPerformers: {performerIdx1WithScene, performerIdx2WithScene}, - sceneIdxWithThreePerformers: {performerIdx1WithScene, performerIdx2WithScene, performerIdx3WithScene}, - sceneIdxWithPerformerTag: {performerIdxWithTag}, - sceneIdxWithTwoPerformerTag: {performerIdxWithTag, performerIdx2WithTag}, - sceneIdxWithPerformerTwoTags: {performerIdxWithTwoTags}, - sceneIdx1WithPerformer: {performerIdxWithTwoScenes}, - sceneIdx2WithPerformer: {performerIdxWithTwoScenes}, - sceneIdxWithStudioPerformer: {performerIdxWithSceneStudio}, - sceneIdxWithPerformerParentTag: {performerIdxWithParentTag}, + sceneIdxWithPerformer: {performerIdxWithScene}, + sceneIdxWithTwoPerformers: {performerIdx1WithScene, performerIdx2WithScene}, + sceneIdxWithThreePerformers: {performerIdx1WithScene, performerIdx2WithScene, performerIdx3WithScene}, + sceneIdxWithPerformerTag: {performerIdxWithTag}, + sceneIdxWithTwoPerformerTag: {performerIdxWithTag, performerIdx2WithTag}, + sceneIdxWithPerformerTwoTags: {performerIdxWithTwoTags}, + sceneIdx1WithPerformer: {performerIdxWithTwoScenes}, + sceneIdx2WithPerformer: {performerIdxWithTwoScenes}, + sceneIdxWithStudioPerformer: {performerIdxWithSceneStudio}, + sceneIdx1WithTwoStudioPerformer: {performerIdxWithTwoSceneStudio}, + sceneIdx2WithTwoStudioPerformer: {performerIdxWithTwoSceneStudio}, + sceneIdxWithPerformerParentTag: {performerIdxWithParentTag}, } sceneGalleries = linkMap{ @@ -406,11 +413,13 @@ var ( } sceneStudios = map[int]int{ - sceneIdxWithStudio: studioIdxWithScene, - sceneIdx1WithStudio: studioIdxWithTwoScenes, - sceneIdx2WithStudio: studioIdxWithTwoScenes, - sceneIdxWithStudioPerformer: studioIdxWithScenePerformer, - sceneIdxWithGrandChildStudio: studioIdxWithGrandParent, + sceneIdxWithStudio: studioIdxWithScene, + sceneIdx1WithStudio: studioIdxWithTwoScenes, + sceneIdx2WithStudio: studioIdxWithTwoScenes, + sceneIdxWithStudioPerformer: studioIdxWithScenePerformer, + sceneIdx1WithTwoStudioPerformer: studioIdx1WithTwoScenePerformer, + sceneIdx2WithTwoStudioPerformer: studioIdx2WithTwoScenePerformer, + sceneIdxWithGrandChildStudio: studioIdxWithGrandParent, } )