diff --git a/pkg/sqlite/filter.go b/pkg/sqlite/filter.go index 40b19fbde..8b29c9a2f 100644 --- a/pkg/sqlite/filter.go +++ b/pkg/sqlite/filter.go @@ -311,13 +311,13 @@ func stringCriterionHandler(c *models.StringCriterionInput, column string) crite f.setError(err) return } - f.addWhere(column+" regexp ?", c.Value) + f.addWhere(fmt.Sprintf("(%s IS NOT NULL AND %[1]s regexp ?)", column), c.Value) case models.CriterionModifierNotMatchesRegex: if _, err := regexp.Compile(c.Value); err != nil { f.setError(err) return } - f.addWhere(column+" NOT regexp ?", c.Value) + f.addWhere(fmt.Sprintf("(%s IS NULL OR %[1]s NOT regexp ?)", column), c.Value) default: clause, count := getSimpleCriterionClause(modifier, "?") diff --git a/pkg/sqlite/filter_internal_test.go b/pkg/sqlite/filter_internal_test.go index 957166eba..5677074d7 100644 --- a/pkg/sqlite/filter_internal_test.go +++ b/pkg/sqlite/filter_internal_test.go @@ -530,7 +530,7 @@ func TestStringCriterionHandlerMatchesRegex(t *testing.T) { }, column)) assert.Len(f.whereClauses, 1) - assert.Equal(fmt.Sprintf("%[1]s regexp ?", column), f.whereClauses[0].sql) + assert.Equal(fmt.Sprintf("(%s IS NOT NULL AND %[1]s regexp ?)", column), f.whereClauses[0].sql) assert.Len(f.whereClauses[0].args, 1) assert.Equal(validValue, f.whereClauses[0].args[0]) @@ -558,7 +558,7 @@ func TestStringCriterionHandlerNotMatchesRegex(t *testing.T) { }, column)) assert.Len(f.whereClauses, 1) - assert.Equal(fmt.Sprintf("%[1]s NOT regexp ?", column), f.whereClauses[0].sql) + assert.Equal(fmt.Sprintf("(%s IS NULL OR %[1]s NOT regexp ?)", column), f.whereClauses[0].sql) assert.Len(f.whereClauses[0].args, 1) assert.Equal(validValue, f.whereClauses[0].args[0]) diff --git a/pkg/sqlite/performer_test.go b/pkg/sqlite/performer_test.go index 283303be1..c5c7dc6cd 100644 --- a/pkg/sqlite/performer_test.go +++ b/pkg/sqlite/performer_test.go @@ -228,6 +228,46 @@ func verifyPerformerAge(t *testing.T, ageCriterion models.IntCriterionInput) { }) } +func TestPerformerQueryCareerLength(t *testing.T) { + const value = "2005" + careerLengthCriterion := models.StringCriterionInput{ + Value: value, + Modifier: models.CriterionModifierEquals, + } + + verifyPerformerCareerLength(t, careerLengthCriterion) + + careerLengthCriterion.Modifier = models.CriterionModifierNotEquals + verifyPerformerCareerLength(t, careerLengthCriterion) + + careerLengthCriterion.Modifier = models.CriterionModifierMatchesRegex + verifyPerformerCareerLength(t, careerLengthCriterion) + + careerLengthCriterion.Modifier = models.CriterionModifierNotMatchesRegex + verifyPerformerCareerLength(t, careerLengthCriterion) +} + +func verifyPerformerCareerLength(t *testing.T, criterion models.StringCriterionInput) { + withTxn(func(r models.Repository) error { + qb := r.Performer() + performerFilter := models.PerformerFilterType{ + CareerLength: &criterion, + } + + performers, _, err := qb.Query(&performerFilter, nil) + if err != nil { + t.Errorf("Error querying performer: %s", err.Error()) + } + + for _, performer := range performers { + cl := performer.CareerLength + verifyNullString(t, cl, criterion) + } + + return nil + }) +} + func queryPerformers(t *testing.T, qb models.PerformerReader, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) []*models.Performer { performers, _, err := qb.Query(performerFilter, findFilter) if err != nil { diff --git a/pkg/sqlite/query.go b/pkg/sqlite/query.go index d0d764337..82a17cf4f 100644 --- a/pkg/sqlite/query.go +++ b/pkg/sqlite/query.go @@ -1,6 +1,7 @@ package sqlite import ( + "fmt" "regexp" "github.com/stashapp/stash/pkg/models" @@ -127,14 +128,14 @@ func (qb *queryBuilder) handleStringCriterionInput(c *models.StringCriterionInpu qb.err = err return } - qb.addWhere(column + " regexp ?") + qb.addWhere(fmt.Sprintf("(%s IS NOT NULL AND %[1]s regexp ?)", column)) qb.addArg(c.Value) case models.CriterionModifierNotMatchesRegex: if _, err := regexp.Compile(c.Value); err != nil { qb.err = err return } - qb.addWhere(column + " NOT regexp ?") + qb.addWhere(fmt.Sprintf("(%s IS NULL OR %[1]s NOT regexp ?)", column)) qb.addArg(c.Value) case models.CriterionModifierIsNull: qb.addWhere("(" + column + " IS NULL OR TRIM(" + column + ") = '')") diff --git a/pkg/sqlite/scene_test.go b/pkg/sqlite/scene_test.go index 153967d1a..ac34403fb 100644 --- a/pkg/sqlite/scene_test.go +++ b/pkg/sqlite/scene_test.go @@ -356,6 +356,17 @@ func verifyNullString(t *testing.T, value sql.NullString, criterion models.Strin if criterion.Modifier == models.CriterionModifierNotEquals { assert.NotEqual(criterion.Value, value.String) } + if criterion.Modifier == models.CriterionModifierMatchesRegex { + assert.True(value.Valid) + assert.Regexp(regexp.MustCompile(criterion.Value), value) + } + if criterion.Modifier == models.CriterionModifierNotMatchesRegex { + if !value.Valid { + // correct + return + } + assert.NotRegexp(regexp.MustCompile(criterion.Value), value) + } } func verifyString(t *testing.T, value string, criterion models.StringCriterionInput) { diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index 910ba6413..323bb113c 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -587,6 +587,15 @@ func getPerformerBirthdate(index int) string { return birthdate.Format("2006-01-02") } +func getPerformerCareerLength(index int) *string { + if index%5 == 0 { + return nil + } + + ret := fmt.Sprintf("20%2d", index) + return &ret +} + //createPerformers creates n performers with plain Name and o performers with camel cased NaMe included func createPerformers(pqb models.PerformerReaderWriter, n int, o int) error { const namePlain = "Name" @@ -613,6 +622,11 @@ func createPerformers(pqb models.PerformerReaderWriter, n int, o int) error { }, } + careerLength := getPerformerCareerLength(i) + if careerLength != nil { + performer.CareerLength = models.NullString(*careerLength) + } + created, err := pqb.Create(performer) if err != nil { diff --git a/ui/v2.5/src/components/Changelog/versions/v060.md b/ui/v2.5/src/components/Changelog/versions/v060.md index 539043e72..d7f4278d6 100644 --- a/ui/v2.5/src/components/Changelog/versions/v060.md +++ b/ui/v2.5/src/components/Changelog/versions/v060.md @@ -20,6 +20,7 @@ * Added Rescan button to scene, image, gallery details overflow button. ### 🐛 Bug fixes +* Fix SQL error when filtering nullable string fields with regex. * Fix incorrect folders being excluded during scanning. * Filter out streaming resolution options that are over the maximum streaming resolution. * Fix `cover.jpg` not being detected as cover image when in sub-directory.