From f3119a6c388ce2c5b2ed074c7d34214b267bf2a1 Mon Sep 17 00:00:00 2001 From: gitgiggety <79809426+gitgiggety@users.noreply.github.com> Date: Thu, 16 Sep 2021 04:09:23 +0200 Subject: [PATCH] Add AND, OR and NOT support to studio filter (#1726) --- graphql/schema/types/filters.graphql | 4 + pkg/sqlite/performer_test.go | 2 +- pkg/sqlite/studio.go | 44 +++++++++ pkg/sqlite/studio_test.go | 137 +++++++++++++++++++++++++++ 4 files changed, 186 insertions(+), 1 deletion(-) diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index 4ea425f93..36f7b93bc 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -181,6 +181,10 @@ input MovieFilterType { } input StudioFilterType { + AND: StudioFilterType + OR: StudioFilterType + NOT: StudioFilterType + name: StringCriterionInput details: StringCriterionInput """Filter to only include studios with this parent studio""" diff --git a/pkg/sqlite/performer_test.go b/pkg/sqlite/performer_test.go index 4ce990c35..dd61c6f99 100644 --- a/pkg/sqlite/performer_test.go +++ b/pkg/sqlite/performer_test.go @@ -165,7 +165,7 @@ func TestPerformerQueryEthnicityAndRating(t *testing.T) { }) } -func TestPerformerQueryPathNotRating(t *testing.T) { +func TestPerformerQueryEthnicityNotRating(t *testing.T) { const performerIdx = 1 performerRating := getRating(performerIdx) diff --git a/pkg/sqlite/studio.go b/pkg/sqlite/studio.go index 584dd2557..c2960a95e 100644 --- a/pkg/sqlite/studio.go +++ b/pkg/sqlite/studio.go @@ -147,9 +147,50 @@ func (qb *studioQueryBuilder) QueryForAutoTag(words []string) ([]*models.Studio, return qb.queryStudios(query+" WHERE "+where, args) } +func (qb *studioQueryBuilder) validateFilter(filter *models.StudioFilterType) error { + const and = "AND" + const or = "OR" + const not = "NOT" + + if filter.And != nil { + if filter.Or != nil { + return illegalFilterCombination(and, or) + } + if filter.Not != nil { + return illegalFilterCombination(and, not) + } + + return qb.validateFilter(filter.And) + } + + if filter.Or != nil { + if filter.Not != nil { + return illegalFilterCombination(or, not) + } + + return qb.validateFilter(filter.Or) + } + + if filter.Not != nil { + return qb.validateFilter(filter.Not) + } + + return nil +} + func (qb *studioQueryBuilder) makeFilter(studioFilter *models.StudioFilterType) *filterBuilder { query := &filterBuilder{} + if studioFilter.And != nil { + query.and(qb.makeFilter(studioFilter.And)) + } + if studioFilter.Or != nil { + query.or(qb.makeFilter(studioFilter.Or)) + } + if studioFilter.Not != nil { + query.not(qb.makeFilter(studioFilter.Not)) + } + query.handleCriterion(stringCriterionHandler(studioFilter.Name, studioTable+".name")) query.handleCriterion(stringCriterionHandler(studioFilter.Details, studioTable+".details")) query.handleCriterion(stringCriterionHandler(studioFilter.URL, studioTable+".url")) @@ -193,6 +234,9 @@ func (qb *studioQueryBuilder) Query(studioFilter *models.StudioFilterType, findF query.addArg(thisArgs...) } + if err := qb.validateFilter(studioFilter); err != nil { + return nil, 0, err + } filter := qb.makeFilter(studioFilter) query.addFilter(filter) diff --git a/pkg/sqlite/studio_test.go b/pkg/sqlite/studio_test.go index 08c0d835c..8ac08d603 100644 --- a/pkg/sqlite/studio_test.go +++ b/pkg/sqlite/studio_test.go @@ -46,6 +46,143 @@ func TestStudioFindByName(t *testing.T) { }) } +func TestStudioQueryNameOr(t *testing.T) { + const studio1Idx = 1 + const studio2Idx = 2 + + studio1Name := getStudioStringValue(studio1Idx, "Name") + studio2Name := getStudioStringValue(studio2Idx, "Name") + + studioFilter := models.StudioFilterType{ + Name: &models.StringCriterionInput{ + Value: studio1Name, + Modifier: models.CriterionModifierEquals, + }, + Or: &models.StudioFilterType{ + Name: &models.StringCriterionInput{ + Value: studio2Name, + Modifier: models.CriterionModifierEquals, + }, + }, + } + + withTxn(func(r models.Repository) error { + sqb := r.Studio() + + studios := queryStudio(t, sqb, &studioFilter, nil) + + assert.Len(t, studios, 2) + assert.Equal(t, studio1Name, studios[0].Name.String) + assert.Equal(t, studio2Name, studios[1].Name.String) + + return nil + }) +} + +func TestStudioQueryNameAndUrl(t *testing.T) { + const studioIdx = 1 + studioName := getStudioStringValue(studioIdx, "Name") + studioUrl := getStudioNullStringValue(studioIdx, urlField) + + studioFilter := models.StudioFilterType{ + Name: &models.StringCriterionInput{ + Value: studioName, + Modifier: models.CriterionModifierEquals, + }, + And: &models.StudioFilterType{ + URL: &models.StringCriterionInput{ + Value: studioUrl.String, + Modifier: models.CriterionModifierEquals, + }, + }, + } + + withTxn(func(r models.Repository) error { + sqb := r.Studio() + + studios := queryStudio(t, sqb, &studioFilter, nil) + + assert.Len(t, studios, 1) + assert.Equal(t, studioName, studios[0].Name.String) + assert.Equal(t, studioUrl.String, studios[0].URL.String) + + return nil + }) +} + +func TestStudioQueryNameNotUrl(t *testing.T) { + const studioIdx = 1 + + studioUrl := getStudioNullStringValue(studioIdx, urlField) + + nameCriterion := models.StringCriterionInput{ + Value: "studio_.*1_Name", + Modifier: models.CriterionModifierMatchesRegex, + } + + urlCriterion := models.StringCriterionInput{ + Value: studioUrl.String, + Modifier: models.CriterionModifierEquals, + } + + studioFilter := models.StudioFilterType{ + Name: &nameCriterion, + Not: &models.StudioFilterType{ + URL: &urlCriterion, + }, + } + + withTxn(func(r models.Repository) error { + sqb := r.Studio() + + studios := queryStudio(t, sqb, &studioFilter, nil) + + for _, studio := range studios { + verifyString(t, studio.Name.String, nameCriterion) + urlCriterion.Modifier = models.CriterionModifierNotEquals + verifyNullString(t, studio.URL, urlCriterion) + } + + return nil + }) +} + +func TestStudioIllegalQuery(t *testing.T) { + assert := assert.New(t) + + const studioIdx = 1 + subFilter := models.StudioFilterType{ + Name: &models.StringCriterionInput{ + Value: getStudioStringValue(studioIdx, "Name"), + Modifier: models.CriterionModifierEquals, + }, + } + + studioFilter := &models.StudioFilterType{ + And: &subFilter, + Or: &subFilter, + } + + withTxn(func(r models.Repository) error { + sqb := r.Studio() + + _, _, err := sqb.Query(studioFilter, nil) + assert.NotNil(err) + + studioFilter.Or = nil + studioFilter.Not = &subFilter + _, _, err = sqb.Query(studioFilter, nil) + assert.NotNil(err) + + studioFilter.And = nil + studioFilter.Or = &subFilter + _, _, err = sqb.Query(studioFilter, nil) + assert.NotNil(err) + + return nil + }) +} + func TestStudioQueryForAutoTag(t *testing.T) { withTxn(func(r models.Repository) error { tqb := r.Studio()