Support Is (not) null for all multi criterions (#1785)

* Support Is (not) null for all multi criterions

Add support for the Is null and Is not null modifiers for all cases of
the MultiCriterionInput and HierarchicalMultiCriterionInput. This
partially overlaps the "X Count" filter which sometimes is available
(because it would be the same as "X Count equals 0" and "X Count greater
than 0") but this also enables it for other criterions like the "Parent
Studio" filter for studios or just the "Studios" filter for scenes /
images / galleries, the "Movies" filter for scenes etc.

* Don't crash UI on bad saved filter
* Add missing code for tag parent/child

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
gitgiggety
2021-11-06 23:34:33 +01:00
committed by GitHub
parent e961ba4459
commit 25274e2596
18 changed files with 659 additions and 95 deletions

View File

@@ -425,17 +425,37 @@ type joinedMultiCriterionHandlerBuilder struct {
func (m *joinedMultiCriterionHandlerBuilder) handler(criterion *models.MultiCriterionInput) criterionHandlerFunc { func (m *joinedMultiCriterionHandlerBuilder) handler(criterion *models.MultiCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) { return func(f *filterBuilder) {
if criterion != nil && len(criterion.Value) > 0 { if criterion != nil {
var args []interface{}
for _, tagID := range criterion.Value {
args = append(args, tagID)
}
joinAlias := m.joinAs joinAlias := m.joinAs
if joinAlias == "" { if joinAlias == "" {
joinAlias = m.joinTable joinAlias = m.joinTable
} }
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
var notClause string
if criterion.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
m.addJoinTable(f)
f.addWhere(utils.StrFormat("{table}.{column} IS {not} NULL", utils.StrFormatMap{
"table": joinAlias,
"column": m.foreignFK,
"not": notClause,
}))
return
}
if len(criterion.Value) == 0 {
return
}
var args []interface{}
for _, tagID := range criterion.Value {
args = append(args, tagID)
}
whereClause := "" whereClause := ""
havingClause := "" havingClause := ""
@@ -475,7 +495,27 @@ type multiCriterionHandlerBuilder struct {
func (m *multiCriterionHandlerBuilder) handler(criterion *models.MultiCriterionInput) criterionHandlerFunc { func (m *multiCriterionHandlerBuilder) handler(criterion *models.MultiCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) { return func(f *filterBuilder) {
if criterion != nil && len(criterion.Value) > 0 { if criterion != nil {
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
var notClause string
if criterion.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
table := m.primaryTable
if m.joinTable != "" {
table = m.joinTable
f.addJoin(table, "", fmt.Sprintf("%s.%s = %s.id", table, m.primaryFK, m.primaryTable))
}
f.addWhere(fmt.Sprintf("%s.%s IS %s NULL", table, m.foreignFK, notClause))
return
}
if len(criterion.Value) == 0 {
return
}
var args []interface{} var args []interface{}
for _, tagID := range criterion.Value { for _, tagID := range criterion.Value {
args = append(args, tagID) args = append(args, tagID)
@@ -637,7 +677,25 @@ func addHierarchicalConditionClauses(f *filterBuilder, criterion *models.Hierarc
func (m *hierarchicalMultiCriterionHandlerBuilder) handler(criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { func (m *hierarchicalMultiCriterionHandlerBuilder) handler(criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) { return func(f *filterBuilder) {
if criterion != nil && len(criterion.Value) > 0 { if criterion != nil {
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
var notClause string
if criterion.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addWhere(utils.StrFormat("{table}.{column} IS {not} NULL", utils.StrFormatMap{
"table": m.primaryTable,
"column": m.foreignFK,
"not": notClause,
}))
return
}
if len(criterion.Value) == 0 {
return
}
valuesClause := getHierarchicalValues(m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, criterion.Depth) valuesClause := getHierarchicalValues(m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, criterion.Depth)
f.addJoin("(SELECT column1 AS root_id, column2 AS item_id FROM ("+valuesClause+"))", m.derivedTable, fmt.Sprintf("%s.item_id = %s.%s", m.derivedTable, m.primaryTable, m.foreignFK)) f.addJoin("(SELECT column1 AS root_id, column2 AS item_id FROM ("+valuesClause+"))", m.derivedTable, fmt.Sprintf("%s.item_id = %s.%s", m.derivedTable, m.primaryTable, m.foreignFK))
@@ -664,10 +722,31 @@ type joinedHierarchicalMultiCriterionHandlerBuilder struct {
func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) { return func(f *filterBuilder) {
if criterion != nil && len(criterion.Value) > 0 { if criterion != nil {
joinAlias := m.joinAs
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
var notClause string
if criterion.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addJoin(m.joinTable, joinAlias, fmt.Sprintf("%s.%s = %s.id", joinAlias, m.primaryFK, m.primaryTable))
f.addWhere(utils.StrFormat("{table}.{column} IS {not} NULL", utils.StrFormatMap{
"table": joinAlias,
"column": m.foreignFK,
"not": notClause,
}))
return
}
if len(criterion.Value) == 0 {
return
}
valuesClause := getHierarchicalValues(m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, criterion.Depth) valuesClause := getHierarchicalValues(m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, criterion.Depth)
joinAlias := m.joinAs
joinTable := utils.StrFormat(`( joinTable := utils.StrFormat(`(
SELECT j.*, d.column1 AS root_id, d.column2 AS item_id FROM {joinTable} AS j SELECT j.*, d.column1 AS root_id, d.column2 AS item_id FROM {joinTable} AS j
INNER JOIN ({valuesClause}) AS d ON j.{foreignFK} = d.column2 INNER JOIN ({valuesClause}) AS d ON j.{foreignFK} = d.column2

View File

@@ -390,7 +390,24 @@ func galleryStudioCriterionHandler(qb *galleryQueryBuilder, studios *models.Hier
func galleryPerformerTagsCriterionHandler(qb *galleryQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { func galleryPerformerTagsCriterionHandler(qb *galleryQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) { return func(f *filterBuilder) {
if tags != nil && len(tags.Value) > 0 { if tags != nil {
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
var notClause string
if tags.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addJoin("performers_galleries", "", "galleries.id = performers_galleries.gallery_id")
f.addJoin("performers_tags", "", "performers_galleries.performer_id = performers_tags.performer_id")
f.addWhere(fmt.Sprintf("performers_tags.tag_id IS %s NULL", notClause))
return
}
if len(tags.Value) == 0 {
return
}
valuesClause := getHierarchicalValues(qb.tx, tags.Value, tagTable, "tags_relations", "", tags.Depth) valuesClause := getHierarchicalValues(qb.tx, tags.Value, tagTable, "tags_relations", "", tags.Depth)
f.addWith(`performer_tags AS ( f.addWith(`performer_tags AS (

View File

@@ -825,6 +825,29 @@ func TestGalleryQueryPerformerTags(t *testing.T) {
galleries = queryGallery(t, sqb, &galleryFilter, &findFilter) galleries = queryGallery(t, sqb, &galleryFilter, &findFilter)
assert.Len(t, galleries, 0) assert.Len(t, galleries, 0)
tagCriterion = models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIsNull,
}
q = getGalleryStringValue(galleryIdx1WithImage, titleField)
galleries = queryGallery(t, sqb, &galleryFilter, &findFilter)
assert.Len(t, galleries, 1)
assert.Equal(t, galleryIDs[galleryIdx1WithImage], galleries[0].ID)
q = getGalleryStringValue(galleryIdxWithPerformerTag, titleField)
galleries = queryGallery(t, sqb, &galleryFilter, &findFilter)
assert.Len(t, galleries, 0)
tagCriterion.Modifier = models.CriterionModifierNotNull
galleries = queryGallery(t, sqb, &galleryFilter, &findFilter)
assert.Len(t, galleries, 1)
assert.Equal(t, galleryIDs[galleryIdxWithPerformerTag], galleries[0].ID)
q = getGalleryStringValue(galleryIdx1WithImage, titleField)
galleries = queryGallery(t, sqb, &galleryFilter, &findFilter)
assert.Len(t, galleries, 0)
return nil return nil
}) })
} }

View File

@@ -464,7 +464,24 @@ func imageStudioCriterionHandler(qb *imageQueryBuilder, studios *models.Hierarch
func imagePerformerTagsCriterionHandler(qb *imageQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { func imagePerformerTagsCriterionHandler(qb *imageQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) { return func(f *filterBuilder) {
if tags != nil && len(tags.Value) > 0 { if tags != nil {
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
var notClause string
if tags.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addJoin("performers_images", "", "images.id = performers_images.image_id")
f.addJoin("performers_tags", "", "performers_images.performer_id = performers_tags.performer_id")
f.addWhere(fmt.Sprintf("performers_tags.tag_id IS %s NULL", notClause))
return
}
if len(tags.Value) == 0 {
return
}
valuesClause := getHierarchicalValues(qb.tx, tags.Value, tagTable, "tags_relations", "", tags.Depth) valuesClause := getHierarchicalValues(qb.tx, tags.Value, tagTable, "tags_relations", "", tags.Depth)
f.addWith(`performer_tags AS ( f.addWith(`performer_tags AS (

View File

@@ -680,11 +680,7 @@ func TestImageQueryPerformers(t *testing.T) {
Performers: &performerCriterion, Performers: &performerCriterion,
} }
images, _, err := queryImagesWithCount(sqb, &imageFilter, nil) images := queryImages(t, sqb, &imageFilter, nil)
if err != nil {
t.Errorf("Error querying image: %s", err.Error())
}
assert.Len(t, images, 2) assert.Len(t, images, 2)
// ensure ids are correct // ensure ids are correct
@@ -700,11 +696,7 @@ func TestImageQueryPerformers(t *testing.T) {
Modifier: models.CriterionModifierIncludesAll, Modifier: models.CriterionModifierIncludesAll,
} }
images, _, err = queryImagesWithCount(sqb, &imageFilter, nil) images = queryImages(t, sqb, &imageFilter, nil)
if err != nil {
t.Errorf("Error querying image: %s", err.Error())
}
assert.Len(t, images, 1) assert.Len(t, images, 1)
assert.Equal(t, imageIDs[imageIdxWithTwoPerformers], images[0].ID) assert.Equal(t, imageIDs[imageIdxWithTwoPerformers], images[0].ID)
@@ -720,10 +712,30 @@ func TestImageQueryPerformers(t *testing.T) {
Q: &q, Q: &q,
} }
images, _, err = queryImagesWithCount(sqb, &imageFilter, &findFilter) images = queryImages(t, sqb, &imageFilter, &findFilter)
if err != nil { assert.Len(t, images, 0)
t.Errorf("Error querying image: %s", err.Error())
performerCriterion = models.MultiCriterionInput{
Modifier: models.CriterionModifierIsNull,
} }
q = getImageStringValue(imageIdxWithGallery, titleField)
images = queryImages(t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 1)
assert.Equal(t, imageIDs[imageIdxWithGallery], images[0].ID)
q = getImageStringValue(imageIdxWithPerformerTag, titleField)
images = queryImages(t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 0)
performerCriterion.Modifier = models.CriterionModifierNotNull
images = queryImages(t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 1)
assert.Equal(t, imageIDs[imageIdxWithPerformerTag], images[0].ID)
q = getImageStringValue(imageIdxWithGallery, titleField)
images = queryImages(t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 0) assert.Len(t, images, 0)
return nil return nil
@@ -745,11 +757,7 @@ func TestImageQueryTags(t *testing.T) {
Tags: &tagCriterion, Tags: &tagCriterion,
} }
images, _, err := queryImagesWithCount(sqb, &imageFilter, nil) images := queryImages(t, sqb, &imageFilter, nil)
if err != nil {
t.Errorf("Error querying image: %s", err.Error())
}
assert.Len(t, images, 2) assert.Len(t, images, 2)
// ensure ids are correct // ensure ids are correct
@@ -765,11 +773,7 @@ func TestImageQueryTags(t *testing.T) {
Modifier: models.CriterionModifierIncludesAll, Modifier: models.CriterionModifierIncludesAll,
} }
images, _, err = queryImagesWithCount(sqb, &imageFilter, nil) images = queryImages(t, sqb, &imageFilter, nil)
if err != nil {
t.Errorf("Error querying image: %s", err.Error())
}
assert.Len(t, images, 1) assert.Len(t, images, 1)
assert.Equal(t, imageIDs[imageIdxWithTwoTags], images[0].ID) assert.Equal(t, imageIDs[imageIdxWithTwoTags], images[0].ID)
@@ -785,10 +789,30 @@ func TestImageQueryTags(t *testing.T) {
Q: &q, Q: &q,
} }
images, _, err = queryImagesWithCount(sqb, &imageFilter, &findFilter) images = queryImages(t, sqb, &imageFilter, &findFilter)
if err != nil { assert.Len(t, images, 0)
t.Errorf("Error querying image: %s", err.Error())
tagCriterion = models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIsNull,
} }
q = getImageStringValue(imageIdxWithGallery, titleField)
images = queryImages(t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 1)
assert.Equal(t, imageIDs[imageIdxWithGallery], images[0].ID)
q = getImageStringValue(imageIdxWithTag, titleField)
images = queryImages(t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 0)
tagCriterion.Modifier = models.CriterionModifierNotNull
images = queryImages(t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 1)
assert.Equal(t, imageIDs[imageIdxWithTag], images[0].ID)
q = getImageStringValue(imageIdxWithGallery, titleField)
images = queryImages(t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 0) assert.Len(t, images, 0)
return nil return nil
@@ -962,6 +986,29 @@ func TestImageQueryPerformerTags(t *testing.T) {
images = queryImages(t, sqb, &imageFilter, &findFilter) images = queryImages(t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 0) assert.Len(t, images, 0)
tagCriterion = models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIsNull,
}
q = getImageStringValue(imageIdxWithGallery, titleField)
images = queryImages(t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 1)
assert.Equal(t, imageIDs[imageIdxWithGallery], images[0].ID)
q = getImageStringValue(imageIdxWithPerformerTag, titleField)
images = queryImages(t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 0)
tagCriterion.Modifier = models.CriterionModifierNotNull
images = queryImages(t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 1)
assert.Equal(t, imageIDs[imageIdxWithPerformerTag], images[0].ID)
q = getImageStringValue(imageIdxWithGallery, titleField)
images = queryImages(t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 0)
return nil return nil
}) })
} }

View File

@@ -209,7 +209,24 @@ func movieStudioCriterionHandler(qb *movieQueryBuilder, studios *models.Hierarch
func moviePerformersCriterionHandler(qb *movieQueryBuilder, performers *models.MultiCriterionInput) criterionHandlerFunc { func moviePerformersCriterionHandler(qb *movieQueryBuilder, performers *models.MultiCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) { return func(f *filterBuilder) {
if performers != nil && len(performers.Value) > 0 { if performers != nil {
if performers.Modifier == models.CriterionModifierIsNull || performers.Modifier == models.CriterionModifierNotNull {
var notClause string
if performers.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addJoin("movies_scenes", "", "movies.id = movies_scenes.movie_id")
f.addJoin("performers_scenes", "", "movies_scenes.scene_id = performers_scenes.scene_id")
f.addWhere(fmt.Sprintf("performers_scenes.performer_id IS %s NULL", notClause))
return
}
if len(performers.Value) == 0 {
return
}
var args []interface{} var args []interface{}
for _, arg := range performers.Value { for _, arg := range performers.Value {
args = append(args, arg) args = append(args, arg)

View File

@@ -439,19 +439,6 @@ func performerGalleryCountCriterionHandler(qb *performerQueryBuilder, count *mod
func performerStudiosCriterionHandler(qb *performerQueryBuilder, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { func performerStudiosCriterionHandler(qb *performerQueryBuilder, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) { return func(f *filterBuilder) {
if studios != nil { if studios != nil {
var clauseCondition string
switch studios.Modifier {
case models.CriterionModifierIncludes:
// return performers who appear in scenes/images/galleries with any of the given studios
clauseCondition = "NOT"
case models.CriterionModifierExcludes:
// exclude performers who appear in scenes/images/galleries with any of the given studios
clauseCondition = ""
default:
return
}
formatMaps := []utils.StrFormatMap{ formatMaps := []utils.StrFormatMap{
{ {
"primaryTable": sceneTable, "primaryTable": sceneTable,
@@ -470,6 +457,41 @@ func performerStudiosCriterionHandler(qb *performerQueryBuilder, studios *models
}, },
} }
if studios.Modifier == models.CriterionModifierIsNull || studios.Modifier == models.CriterionModifierNotNull {
var notClause string
if studios.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
var conditions []string
for _, c := range formatMaps {
f.addJoin(c["joinTable"].(string), "", fmt.Sprintf("%s.performer_id = performers.id", c["joinTable"]))
f.addJoin(c["primaryTable"].(string), "", fmt.Sprintf("%s.%s = %s.id", c["joinTable"], c["primaryFK"], c["primaryTable"]))
conditions = append(conditions, fmt.Sprintf("%s.studio_id IS NULL", c["primaryTable"]))
}
f.addWhere(fmt.Sprintf("%s (%s)", notClause, strings.Join(conditions, " AND ")))
return
}
if len(studios.Value) == 0 {
return
}
var clauseCondition string
switch studios.Modifier {
case models.CriterionModifierIncludes:
// return performers who appear in scenes/images/galleries with any of the given studios
clauseCondition = "NOT"
case models.CriterionModifierExcludes:
// exclude performers who appear in scenes/images/galleries with any of the given studios
clauseCondition = ""
default:
return
}
const derivedPerformerStudioTable = "performer_studio" const derivedPerformerStudioTable = "performer_studio"
valuesClause := getHierarchicalValues(qb.tx, studios.Value, studioTable, "", "parent_id", studios.Depth) valuesClause := getHierarchicalValues(qb.tx, studios.Value, studioTable, "", "parent_id", studios.Depth)
f.addWith("studio(root_id, item_id) AS (" + valuesClause + ")") f.addWith("studio(root_id, item_id) AS (" + valuesClause + ")")
@@ -483,7 +505,7 @@ func performerStudiosCriterionHandler(qb *performerQueryBuilder, studios *models
unions = append(unions, utils.StrFormat(templStr, c)) unions = append(unions, utils.StrFormat(templStr, c))
} }
f.addWith(fmt.Sprintf("%s AS (%s)", "performer_studio", strings.Join(unions, " UNION "))) f.addWith(fmt.Sprintf("%s AS (%s)", derivedPerformerStudioTable, strings.Join(unions, " UNION ")))
f.addJoin(derivedPerformerStudioTable, "", fmt.Sprintf("performers.id = %s.performer_id", derivedPerformerStudioTable)) f.addJoin(derivedPerformerStudioTable, "", fmt.Sprintf("performers.id = %s.performer_id", derivedPerformerStudioTable))
f.addWhere(fmt.Sprintf("%s.performer_id IS %s NULL", derivedPerformerStudioTable, clauseCondition)) f.addWhere(fmt.Sprintf("%s.performer_id IS %s NULL", derivedPerformerStudioTable, clauseCondition))

View File

@@ -787,6 +787,34 @@ func TestPerformerQueryStudio(t *testing.T) {
assert.Len(t, performers, 0) assert.Len(t, performers, 0)
} }
// test NULL/not NULL
q := getPerformerStringValue(performerIdx1WithImage, "Name")
performerFilter := &models.PerformerFilterType{
Studios: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIsNull,
},
}
findFilter := &models.FindFilterType{
Q: &q,
}
performers := queryPerformers(t, sqb, performerFilter, findFilter)
assert.Len(t, performers, 1)
assert.Equal(t, imageIDs[performerIdx1WithImage], performers[0].ID)
q = getPerformerStringValue(performerIdxWithSceneStudio, "Name")
performers = queryPerformers(t, sqb, performerFilter, findFilter)
assert.Len(t, performers, 0)
performerFilter.Studios.Modifier = models.CriterionModifierNotNull
performers = queryPerformers(t, sqb, performerFilter, findFilter)
assert.Len(t, performers, 1)
assert.Equal(t, imageIDs[performerIdxWithSceneStudio], performers[0].ID)
q = getPerformerStringValue(performerIdx1WithImage, "Name")
performers = queryPerformers(t, sqb, performerFilter, findFilter)
assert.Len(t, performers, 0)
return nil return nil
}) })
} }

View File

@@ -668,7 +668,24 @@ func sceneMoviesCriterionHandler(qb *sceneQueryBuilder, movies *models.MultiCrit
func scenePerformerTagsCriterionHandler(qb *sceneQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { func scenePerformerTagsCriterionHandler(qb *sceneQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) { return func(f *filterBuilder) {
if tags != nil && len(tags.Value) > 0 { if tags != nil {
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
var notClause string
if tags.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addJoin("performers_scenes", "", "scenes.id = performers_scenes.scene_id")
f.addJoin("performers_tags", "", "performers_scenes.performer_id = performers_tags.performer_id")
f.addWhere(fmt.Sprintf("performers_tags.tag_id IS %s NULL", notClause))
return
}
if len(tags.Value) == 0 {
return
}
valuesClause := getHierarchicalValues(qb.tx, tags.Value, tagTable, "tags_relations", "", tags.Depth) valuesClause := getHierarchicalValues(qb.tx, tags.Value, tagTable, "tags_relations", "", tags.Depth)
f.addWith(`performer_tags AS ( f.addWith(`performer_tags AS (

View File

@@ -191,7 +191,22 @@ func sceneMarkerTagIDCriterionHandler(qb *sceneMarkerQueryBuilder, tagID *string
func sceneMarkerTagsCriterionHandler(qb *sceneMarkerQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { func sceneMarkerTagsCriterionHandler(qb *sceneMarkerQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) { return func(f *filterBuilder) {
if tags != nil && len(tags.Value) > 0 { if tags != nil {
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
var notClause string
if tags.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addJoin("scene_markers_tags", "", "scene_markers.id = scene_markers_tags.scene_marker_id")
f.addWhere(fmt.Sprintf("%s scene_markers_tags.tag_id IS NULL", notClause))
return
}
if len(tags.Value) == 0 {
return
}
valuesClause := getHierarchicalValues(qb.tx, tags.Value, tagTable, "tags_relations", "", tags.Depth) valuesClause := getHierarchicalValues(qb.tx, tags.Value, tagTable, "tags_relations", "", tags.Depth)
f.addWith(`marker_tags AS ( f.addWith(`marker_tags AS (
@@ -211,7 +226,23 @@ INNER JOIN (` + valuesClause + `) t ON t.column2 = m.primary_tag_id
func sceneMarkerSceneTagsCriterionHandler(qb *sceneMarkerQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { func sceneMarkerSceneTagsCriterionHandler(qb *sceneMarkerQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) { return func(f *filterBuilder) {
if tags != nil && len(tags.Value) > 0 { if tags != nil {
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
var notClause string
if tags.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addJoin("scenes_tags", "", "scene_markers.scene_id = scenes_tags.scene_id")
f.addWhere(fmt.Sprintf("scenes_tags.tag_id IS %s NULL", notClause))
return
}
if len(tags.Value) == 0 {
return
}
valuesClause := getHierarchicalValues(qb.tx, tags.Value, tagTable, "tags_relations", "", tags.Depth) valuesClause := getHierarchicalValues(qb.tx, tags.Value, tagTable, "tags_relations", "", tags.Depth)
f.addWith(`scene_tags AS ( f.addWith(`scene_tags AS (

View File

@@ -14,15 +14,17 @@ func TestMarkerFindBySceneID(t *testing.T) {
withTxn(func(r models.Repository) error { withTxn(func(r models.Repository) error {
mqb := r.SceneMarker() mqb := r.SceneMarker()
sceneID := sceneIDs[sceneIdxWithMarker] sceneID := sceneIDs[sceneIdxWithMarkers]
markers, err := mqb.FindBySceneID(sceneID) markers, err := mqb.FindBySceneID(sceneID)
if err != nil { if err != nil {
t.Errorf("Error finding markers: %s", err.Error()) t.Errorf("Error finding markers: %s", err.Error())
} }
assert.Len(t, markers, 1) assert.Greater(t, len(markers), 0)
assert.Equal(t, markerIDs[markerIdxWithScene], markers[0].ID) for _, marker := range markers {
assert.Equal(t, sceneIDs[sceneIdxWithMarkers], int(marker.SceneID.Int64))
}
markers, err = mqb.FindBySceneID(0) markers, err = mqb.FindBySceneID(0)
@@ -40,15 +42,15 @@ func TestMarkerCountByTagID(t *testing.T) {
withTxn(func(r models.Repository) error { withTxn(func(r models.Repository) error {
mqb := r.SceneMarker() mqb := r.SceneMarker()
markerCount, err := mqb.CountByTagID(tagIDs[tagIdxWithPrimaryMarker]) markerCount, err := mqb.CountByTagID(tagIDs[tagIdxWithPrimaryMarkers])
if err != nil { if err != nil {
t.Errorf("error calling CountByTagID: %s", err.Error()) t.Errorf("error calling CountByTagID: %s", err.Error())
} }
assert.Equal(t, 1, markerCount) assert.Equal(t, 3, markerCount)
markerCount, err = mqb.CountByTagID(tagIDs[tagIdxWithMarker]) markerCount, err = mqb.CountByTagID(tagIDs[tagIdxWithMarkers])
if err != nil { if err != nil {
t.Errorf("error calling CountByTagID: %s", err.Error()) t.Errorf("error calling CountByTagID: %s", err.Error())
@@ -83,6 +85,128 @@ func TestMarkerQuerySortBySceneUpdated(t *testing.T) {
}) })
} }
func TestMarkerQueryTags(t *testing.T) {
type test struct {
name string
markerFilter *models.SceneMarkerFilterType
findFilter *models.FindFilterType
}
withTxn(func(r models.Repository) error {
testTags := func(m *models.SceneMarker, markerFilter *models.SceneMarkerFilterType) {
tagIDs, err := r.SceneMarker().GetTagIDs(m.ID)
if err != nil {
t.Errorf("error getting marker tag ids: %v", err)
}
if markerFilter.Tags.Modifier == models.CriterionModifierIsNull && len(tagIDs) > 0 {
t.Errorf("expected marker %d to have no tags - found %d", m.ID, len(tagIDs))
}
if markerFilter.Tags.Modifier == models.CriterionModifierNotNull && len(tagIDs) == 0 {
t.Errorf("expected marker %d to have tags - found 0", m.ID)
}
}
cases := []test{
{
"is null",
&models.SceneMarkerFilterType{
Tags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIsNull,
},
},
nil,
},
{
"not null",
&models.SceneMarkerFilterType{
Tags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierNotNull,
},
},
nil,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
markers := queryMarkers(t, r.SceneMarker(), tc.markerFilter, tc.findFilter)
assert.Greater(t, len(markers), 0)
for _, m := range markers {
testTags(m, tc.markerFilter)
}
})
}
return nil
})
}
func TestMarkerQuerySceneTags(t *testing.T) {
type test struct {
name string
markerFilter *models.SceneMarkerFilterType
findFilter *models.FindFilterType
}
withTxn(func(r models.Repository) error {
testTags := func(m *models.SceneMarker, markerFilter *models.SceneMarkerFilterType) {
tagIDs, err := r.Scene().GetTagIDs(int(m.SceneID.Int64))
if err != nil {
t.Errorf("error getting marker tag ids: %v", err)
}
if markerFilter.SceneTags.Modifier == models.CriterionModifierIsNull && len(tagIDs) > 0 {
t.Errorf("expected marker %d to have no scene tags - found %d", m.ID, len(tagIDs))
}
if markerFilter.SceneTags.Modifier == models.CriterionModifierNotNull && len(tagIDs) == 0 {
t.Errorf("expected marker %d to have scene tags - found 0", m.ID)
}
}
cases := []test{
{
"is null",
&models.SceneMarkerFilterType{
SceneTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIsNull,
},
},
nil,
},
{
"not null",
&models.SceneMarkerFilterType{
SceneTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierNotNull,
},
},
nil,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
markers := queryMarkers(t, r.SceneMarker(), tc.markerFilter, tc.findFilter)
assert.Greater(t, len(markers), 0)
for _, m := range markers {
testTags(m, tc.markerFilter)
}
})
}
return nil
})
}
func queryMarkers(t *testing.T, sqb models.SceneMarkerReader, markerFilter *models.SceneMarkerFilterType, findFilter *models.FindFilterType) []*models.SceneMarker {
t.Helper()
result, _, err := sqb.Query(markerFilter, findFilter)
if err != nil {
t.Errorf("Error querying markers: %v", err)
}
return result
}
// TODO Update // TODO Update
// TODO Destroy // TODO Destroy
// TODO Find // TODO Find

View File

@@ -775,7 +775,7 @@ func TestSceneQueryHasMarkers(t *testing.T) {
HasMarkers: &hasMarkers, HasMarkers: &hasMarkers,
} }
q := getSceneStringValue(sceneIdxWithMarker, titleField) q := getSceneStringValue(sceneIdxWithMarkers, titleField)
findFilter := models.FindFilterType{ findFilter := models.FindFilterType{
Q: &q, Q: &q,
} }
@@ -783,7 +783,7 @@ func TestSceneQueryHasMarkers(t *testing.T) {
scenes := queryScene(t, sqb, &sceneFilter, &findFilter) scenes := queryScene(t, sqb, &sceneFilter, &findFilter)
assert.Len(t, scenes, 1) assert.Len(t, scenes, 1)
assert.Equal(t, sceneIDs[sceneIdxWithMarker], scenes[0].ID) assert.Equal(t, sceneIDs[sceneIdxWithMarkers], scenes[0].ID)
hasMarkers = "false" hasMarkers = "false"
scenes = queryScene(t, sqb, &sceneFilter, &findFilter) scenes = queryScene(t, sqb, &sceneFilter, &findFilter)
@@ -796,7 +796,7 @@ func TestSceneQueryHasMarkers(t *testing.T) {
// ensure non of the ids equal the one with gallery // ensure non of the ids equal the one with gallery
for _, scene := range scenes { for _, scene := range scenes {
assert.NotEqual(t, sceneIDs[sceneIdxWithMarker], scene.ID) assert.NotEqual(t, sceneIDs[sceneIdxWithMarkers], scene.ID)
} }
return nil return nil
@@ -1151,6 +1151,29 @@ func TestSceneQueryPerformerTags(t *testing.T) {
scenes = queryScene(t, sqb, &sceneFilter, &findFilter) scenes = queryScene(t, sqb, &sceneFilter, &findFilter)
assert.Len(t, scenes, 0) assert.Len(t, scenes, 0)
tagCriterion = models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIsNull,
}
q = getSceneStringValue(sceneIdx1WithPerformer, titleField)
scenes = queryScene(t, sqb, &sceneFilter, &findFilter)
assert.Len(t, scenes, 1)
assert.Equal(t, sceneIDs[sceneIdx1WithPerformer], scenes[0].ID)
q = getSceneStringValue(sceneIdxWithPerformerTag, titleField)
scenes = queryScene(t, sqb, &sceneFilter, &findFilter)
assert.Len(t, scenes, 0)
tagCriterion.Modifier = models.CriterionModifierNotNull
scenes = queryScene(t, sqb, &sceneFilter, &findFilter)
assert.Len(t, scenes, 1)
assert.Equal(t, sceneIDs[sceneIdxWithPerformerTag], scenes[0].ID)
q = getSceneStringValue(sceneIdx1WithPerformer, titleField)
scenes = queryScene(t, sqb, &sceneFilter, &findFilter)
assert.Len(t, scenes, 0)
return nil return nil
}) })
} }

View File

@@ -34,10 +34,11 @@ const (
sceneIdxWithTwoPerformers sceneIdxWithTwoPerformers
sceneIdxWithTag sceneIdxWithTag
sceneIdxWithTwoTags sceneIdxWithTwoTags
sceneIdxWithMarkerAndTag
sceneIdxWithStudio sceneIdxWithStudio
sceneIdx1WithStudio sceneIdx1WithStudio
sceneIdx2WithStudio sceneIdx2WithStudio
sceneIdxWithMarker sceneIdxWithMarkers
sceneIdxWithPerformerTag sceneIdxWithPerformerTag
sceneIdxWithPerformerTwoTags sceneIdxWithPerformerTwoTags
sceneIdxWithSpacedName sceneIdxWithSpacedName
@@ -139,8 +140,9 @@ const (
tagIdxWithScene = iota tagIdxWithScene = iota
tagIdx1WithScene tagIdx1WithScene
tagIdx2WithScene tagIdx2WithScene
tagIdxWithPrimaryMarker tagIdx3WithScene
tagIdxWithMarker tagIdxWithPrimaryMarkers
tagIdxWithMarkers
tagIdxWithCoverImage tagIdxWithCoverImage
tagIdxWithImage tagIdxWithImage
tagIdx1WithImage tagIdx1WithImage
@@ -191,6 +193,9 @@ const (
const ( const (
markerIdxWithScene = iota markerIdxWithScene = iota
markerIdxWithTag
markerIdxWithSceneTag
totalMarkers
) )
const ( const (
@@ -239,6 +244,7 @@ var (
{sceneIdxWithTag, tagIdxWithScene}, {sceneIdxWithTag, tagIdxWithScene},
{sceneIdxWithTwoTags, tagIdx1WithScene}, {sceneIdxWithTwoTags, tagIdx1WithScene},
{sceneIdxWithTwoTags, tagIdx2WithScene}, {sceneIdxWithTwoTags, tagIdx2WithScene},
{sceneIdxWithMarkerAndTag, tagIdx3WithScene},
} }
scenePerformerLinks = [][2]int{ scenePerformerLinks = [][2]int{
@@ -269,6 +275,21 @@ var (
} }
) )
type markerSpec struct {
sceneIdx int
primaryTagIdx int
tagIdxs []int
}
var (
// indexed by marker
markerSpecs = []markerSpec{
{sceneIdxWithMarkers, tagIdxWithPrimaryMarkers, nil},
{sceneIdxWithMarkers, tagIdxWithPrimaryMarkers, []int{tagIdxWithMarkers}},
{sceneIdxWithMarkerAndTag, tagIdxWithPrimaryMarkers, nil},
}
)
var ( var (
imageGalleryLinks = [][2]int{ imageGalleryLinks = [][2]int{
{imageIdxWithGallery, galleryIdxWithImage}, {imageIdxWithGallery, galleryIdxWithImage},
@@ -516,8 +537,10 @@ func populateDB() error {
return fmt.Errorf("error linking tags parent: %s", err.Error()) return fmt.Errorf("error linking tags parent: %s", err.Error())
} }
if err := createMarker(r.SceneMarker(), sceneIdxWithMarker, tagIdxWithPrimaryMarker, []int{tagIdxWithMarker}); err != nil { for _, ms := range markerSpecs {
return fmt.Errorf("error creating scene marker: %s", err.Error()) if err := createMarker(r.SceneMarker(), ms); err != nil {
return fmt.Errorf("error creating scene marker: %s", err.Error())
}
} }
return nil return nil
@@ -687,6 +710,7 @@ func createGalleries(gqb models.GalleryReaderWriter, n int) error {
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
gallery := models.Gallery{ gallery := models.Gallery{
Path: models.NullString(getGalleryStringValue(i, pathField)), Path: models.NullString(getGalleryStringValue(i, pathField)),
Title: models.NullString(getGalleryStringValue(i, titleField)),
URL: getGalleryNullStringValue(i, urlField), URL: getGalleryNullStringValue(i, urlField),
Checksum: getGalleryStringValue(i, checksumField), Checksum: getGalleryStringValue(i, checksumField),
Rating: getRating(i), Rating: getRating(i),
@@ -843,7 +867,7 @@ func getTagStringValue(index int, field string) string {
} }
func getTagSceneCount(id int) int { func getTagSceneCount(id int) int {
if id == tagIDs[tagIdx1WithScene] || id == tagIDs[tagIdx2WithScene] || id == tagIDs[tagIdxWithScene] { if id == tagIDs[tagIdx1WithScene] || id == tagIDs[tagIdx2WithScene] || id == tagIDs[tagIdxWithScene] || id == tagIDs[tagIdx3WithScene] {
return 1 return 1
} }
@@ -851,7 +875,11 @@ func getTagSceneCount(id int) int {
} }
func getTagMarkerCount(id int) int { func getTagMarkerCount(id int) int {
if id == tagIDs[tagIdxWithMarker] || id == tagIDs[tagIdxWithPrimaryMarker] { if id == tagIDs[tagIdxWithPrimaryMarkers] {
return 3
}
if id == tagIDs[tagIdxWithMarkers] {
return 1 return 1
} }
@@ -1008,28 +1036,30 @@ func createStudios(sqb models.StudioReaderWriter, n int, o int) error {
return nil return nil
} }
func createMarker(mqb models.SceneMarkerReaderWriter, sceneIdx, primaryTagIdx int, tagIdxs []int) error { func createMarker(mqb models.SceneMarkerReaderWriter, markerSpec markerSpec) error {
marker := models.SceneMarker{ marker := models.SceneMarker{
SceneID: sql.NullInt64{Int64: int64(sceneIDs[sceneIdx]), Valid: true}, SceneID: sql.NullInt64{Int64: int64(sceneIDs[markerSpec.sceneIdx]), Valid: true},
PrimaryTagID: tagIDs[primaryTagIdx], PrimaryTagID: tagIDs[markerSpec.primaryTagIdx],
} }
created, err := mqb.Create(marker) created, err := mqb.Create(marker)
if err != nil { if err != nil {
return fmt.Errorf("Error creating marker %v+: %s", marker, err.Error()) return fmt.Errorf("error creating marker %v+: %w", marker, err)
} }
markerIDs = append(markerIDs, created.ID) markerIDs = append(markerIDs, created.ID)
newTagIDs := []int{} if len(markerSpec.tagIdxs) > 0 {
newTagIDs := []int{}
for _, tagIdx := range tagIdxs { for _, tagIdx := range markerSpec.tagIdxs {
newTagIDs = append(newTagIDs, tagIDs[tagIdx]) newTagIDs = append(newTagIDs, tagIDs[tagIdx])
} }
if err := mqb.UpdateTags(created.ID, newTagIDs); err != nil { if err := mqb.UpdateTags(created.ID, newTagIDs); err != nil {
return fmt.Errorf("Error creating marker/tag join: %s", err.Error()) return fmt.Errorf("error creating marker/tag join: %w", err)
}
} }
return nil return nil

View File

@@ -443,7 +443,23 @@ func tagMarkerCountCriterionHandler(qb *tagQueryBuilder, markerCount *models.Int
func tagParentsCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { func tagParentsCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) { return func(f *filterBuilder) {
if tags != nil && len(tags.Value) > 0 { if tags != nil {
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
var notClause string
if tags.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addJoin("tags_relations", "parent_relations", "tags.id = parent_relations.child_id")
f.addWhere(fmt.Sprintf("parent_relations.parent_id IS %s NULL", notClause))
return
}
if len(tags.Value) == 0 {
return
}
var args []interface{} var args []interface{}
for _, val := range tags.Value { for _, val := range tags.Value {
args = append(args, val) args = append(args, val)
@@ -476,7 +492,23 @@ func tagParentsCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalMu
func tagChildrenCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { func tagChildrenCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) { return func(f *filterBuilder) {
if tags != nil && len(tags.Value) > 0 { if tags != nil {
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
var notClause string
if tags.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addJoin("tags_relations", "child_relations", "tags.id = child_relations.parent_id")
f.addWhere(fmt.Sprintf("child_relations.child_id IS %s NULL", notClause))
return
}
if len(tags.Value) == 0 {
return
}
var args []interface{} var args []interface{}
for _, val := range tags.Value { for _, val := range tags.Value {
args = append(args, val) args = append(args, val)

View File

@@ -18,7 +18,7 @@ func TestMarkerFindBySceneMarkerID(t *testing.T) {
withTxn(func(r models.Repository) error { withTxn(func(r models.Repository) error {
tqb := r.Tag() tqb := r.Tag()
markerID := markerIDs[markerIdxWithScene] markerID := markerIDs[markerIdxWithTag]
tags, err := tqb.FindBySceneMarkerID(markerID) tags, err := tqb.FindBySceneMarkerID(markerID)
@@ -27,7 +27,7 @@ func TestMarkerFindBySceneMarkerID(t *testing.T) {
} }
assert.Len(t, tags, 1) assert.Len(t, tags, 1)
assert.Equal(t, tagIDs[tagIdxWithMarker], tags[0].ID) assert.Equal(t, tagIDs[tagIdxWithMarkers], tags[0].ID)
tags, err = tqb.FindBySceneMarkerID(0) tags, err = tqb.FindBySceneMarkerID(0)
@@ -168,7 +168,7 @@ func TestTagQuerySort(t *testing.T) {
sortBy = "scene_markers_count" sortBy = "scene_markers_count"
tags = queryTags(t, sqb, nil, findFilter) tags = queryTags(t, sqb, nil, findFilter)
assert.Equal(tagIDs[tagIdxWithMarker], tags[0].ID) assert.Equal(tagIDs[tagIdxWithMarkers], tags[0].ID)
sortBy = "images_count" sortBy = "images_count"
tags = queryTags(t, sqb, nil, findFilter) tags = queryTags(t, sqb, nil, findFilter)
@@ -613,6 +613,7 @@ func verifyTagChildCount(t *testing.T, sceneCountCriterion models.IntCriterionIn
func TestTagQueryParent(t *testing.T) { func TestTagQueryParent(t *testing.T) {
withTxn(func(r models.Repository) error { withTxn(func(r models.Repository) error {
const nameField = "Name"
sqb := r.Tag() sqb := r.Tag()
tagCriterion := models.HierarchicalMultiCriterionInput{ tagCriterion := models.HierarchicalMultiCriterionInput{
Value: []string{ Value: []string{
@@ -634,7 +635,7 @@ func TestTagQueryParent(t *testing.T) {
tagCriterion.Modifier = models.CriterionModifierExcludes tagCriterion.Modifier = models.CriterionModifierExcludes
q := getTagStringValue(tagIdxWithParentTag, titleField) q := getTagStringValue(tagIdxWithParentTag, nameField)
findFilter := models.FindFilterType{ findFilter := models.FindFilterType{
Q: &q, Q: &q,
} }
@@ -660,12 +661,37 @@ func TestTagQueryParent(t *testing.T) {
tags = queryTags(t, sqb, &tagFilter, nil) tags = queryTags(t, sqb, &tagFilter, nil)
assert.Len(t, tags, 2) assert.Len(t, tags, 2)
tagCriterion = models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIsNull,
}
q = getTagStringValue(tagIdxWithGallery, nameField)
tags = queryTags(t, sqb, &tagFilter, &findFilter)
assert.Len(t, tags, 1)
assert.Equal(t, tagIDs[tagIdxWithGallery], tags[0].ID)
q = getTagStringValue(tagIdxWithParentTag, nameField)
tags = queryTags(t, sqb, &tagFilter, &findFilter)
assert.Len(t, tags, 0)
tagCriterion.Modifier = models.CriterionModifierNotNull
tags = queryTags(t, sqb, &tagFilter, &findFilter)
assert.Len(t, tags, 1)
assert.Equal(t, tagIDs[tagIdxWithParentTag], tags[0].ID)
q = getTagStringValue(tagIdxWithGallery, nameField)
tags = queryTags(t, sqb, &tagFilter, &findFilter)
assert.Len(t, tags, 0)
return nil return nil
}) })
} }
func TestTagQueryChild(t *testing.T) { func TestTagQueryChild(t *testing.T) {
withTxn(func(r models.Repository) error { withTxn(func(r models.Repository) error {
const nameField = "Name"
sqb := r.Tag() sqb := r.Tag()
tagCriterion := models.HierarchicalMultiCriterionInput{ tagCriterion := models.HierarchicalMultiCriterionInput{
Value: []string{ Value: []string{
@@ -687,7 +713,7 @@ func TestTagQueryChild(t *testing.T) {
tagCriterion.Modifier = models.CriterionModifierExcludes tagCriterion.Modifier = models.CriterionModifierExcludes
q := getTagStringValue(tagIdxWithChildTag, titleField) q := getTagStringValue(tagIdxWithChildTag, nameField)
findFilter := models.FindFilterType{ findFilter := models.FindFilterType{
Q: &q, Q: &q,
} }
@@ -713,6 +739,29 @@ func TestTagQueryChild(t *testing.T) {
tags = queryTags(t, sqb, &tagFilter, nil) tags = queryTags(t, sqb, &tagFilter, nil)
assert.Len(t, tags, 2) assert.Len(t, tags, 2)
tagCriterion = models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIsNull,
}
q = getTagStringValue(tagIdxWithGallery, nameField)
tags = queryTags(t, sqb, &tagFilter, &findFilter)
assert.Len(t, tags, 1)
assert.Equal(t, tagIDs[tagIdxWithGallery], tags[0].ID)
q = getTagStringValue(tagIdxWithChildTag, nameField)
tags = queryTags(t, sqb, &tagFilter, &findFilter)
assert.Len(t, tags, 0)
tagCriterion.Modifier = models.CriterionModifierNotNull
tags = queryTags(t, sqb, &tagFilter, &findFilter)
assert.Len(t, tags, 1)
assert.Equal(t, tagIDs[tagIdxWithChildTag], tags[0].ID)
q = getTagStringValue(tagIdxWithGallery, nameField)
tags = queryTags(t, sqb, &tagFilter, &findFilter)
assert.Len(t, tags, 0)
return nil return nil
}) })
} }
@@ -842,8 +891,8 @@ func TestTagMerge(t *testing.T) {
srcIdxs := []int{ srcIdxs := []int{
tagIdx1WithScene, tagIdx1WithScene,
tagIdx2WithScene, tagIdx2WithScene,
tagIdxWithPrimaryMarker, tagIdxWithPrimaryMarkers,
tagIdxWithMarker, tagIdxWithMarkers,
tagIdxWithCoverImage, tagIdxWithCoverImage,
tagIdxWithImage, tagIdxWithImage,
tagIdx1WithImage, tagIdx1WithImage,
@@ -893,7 +942,7 @@ func TestTagMerge(t *testing.T) {
assert.Contains(sceneTagIDs, destID) assert.Contains(sceneTagIDs, destID)
// ensure marker points to new tag // ensure marker points to new tag
marker, err := r.SceneMarker().Find(markerIDs[markerIdxWithScene]) marker, err := r.SceneMarker().Find(markerIDs[markerIdxWithTag])
if err != nil { if err != nil {
return err return err
} }

View File

@@ -1,4 +1,5 @@
### ✨ New Features ### ✨ New Features
* Support is (not) null for multi-relational filter criteria. ([#1785](https://github.com/stashapp/stash/pull/1785))
* Optionally open browser on startup (enabled by default for new systems). ([#1832](https://github.com/stashapp/stash/pull/1832)) * Optionally open browser on startup (enabled by default for new systems). ([#1832](https://github.com/stashapp/stash/pull/1832))
* Support setting defaults for Delete File and Delete Generated Files in the Interface Settings. ([#1852](https://github.com/stashapp/stash/pull/1852)) * Support setting defaults for Delete File and Delete Generated Files in the Interface Settings. ([#1852](https://github.com/stashapp/stash/pull/1852))
* Added Identify task to automatically identify scenes from stash-box/scraper sources. See manual entry for details. ([#1839](https://github.com/stashapp/stash/pull/1839)) * Added Identify task to automatically identify scenes from stash-box/scraper sources. See manual entry for details. ([#1839](https://github.com/stashapp/stash/pull/1839))

View File

@@ -655,9 +655,14 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
defaultFilter?.findDefaultFilter defaultFilter?.findDefaultFilter
) { ) {
newFilter.currentPage = 1; newFilter.currentPage = 1;
newFilter.configureFromQueryParameters( try {
JSON.parse(defaultFilter.findDefaultFilter.filter) newFilter.configureFromQueryParameters(
); JSON.parse(defaultFilter.findDefaultFilter.filter)
);
} catch (err) {
console.log(err);
// ignore
}
// #1507 - reset random seed when loaded // #1507 - reset random seed when loaded
newFilter.randomSeed = -1; newFilter.randomSeed = -1;
update = true; update = true;

View File

@@ -364,6 +364,8 @@ export class ILabeledIdCriterionOption extends CriterionOption {
const modifierOptions = [ const modifierOptions = [
CriterionModifier.Includes, CriterionModifier.Includes,
CriterionModifier.Excludes, CriterionModifier.Excludes,
CriterionModifier.IsNull,
CriterionModifier.NotNull,
]; ];
let defaultModifier = CriterionModifier.Includes; let defaultModifier = CriterionModifier.Includes;