mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
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:
@@ -425,17 +425,37 @@ type joinedMultiCriterionHandlerBuilder struct {
|
||||
|
||||
func (m *joinedMultiCriterionHandlerBuilder) handler(criterion *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if criterion != nil && len(criterion.Value) > 0 {
|
||||
var args []interface{}
|
||||
for _, tagID := range criterion.Value {
|
||||
args = append(args, tagID)
|
||||
}
|
||||
|
||||
if criterion != nil {
|
||||
joinAlias := m.joinAs
|
||||
if joinAlias == "" {
|
||||
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 := ""
|
||||
havingClause := ""
|
||||
|
||||
@@ -475,7 +495,27 @@ type multiCriterionHandlerBuilder struct {
|
||||
|
||||
func (m *multiCriterionHandlerBuilder) handler(criterion *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
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{}
|
||||
for _, tagID := range criterion.Value {
|
||||
args = append(args, tagID)
|
||||
@@ -637,7 +677,25 @@ func addHierarchicalConditionClauses(f *filterBuilder, criterion *models.Hierarc
|
||||
|
||||
func (m *hierarchicalMultiCriterionHandlerBuilder) handler(criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
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)
|
||||
|
||||
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 {
|
||||
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)
|
||||
|
||||
joinAlias := m.joinAs
|
||||
joinTable := utils.StrFormat(`(
|
||||
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
|
||||
|
||||
@@ -390,7 +390,24 @@ func galleryStudioCriterionHandler(qb *galleryQueryBuilder, studios *models.Hier
|
||||
|
||||
func galleryPerformerTagsCriterionHandler(qb *galleryQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
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)
|
||||
|
||||
f.addWith(`performer_tags AS (
|
||||
|
||||
@@ -825,6 +825,29 @@ func TestGalleryQueryPerformerTags(t *testing.T) {
|
||||
galleries = queryGallery(t, sqb, &galleryFilter, &findFilter)
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -464,7 +464,24 @@ func imageStudioCriterionHandler(qb *imageQueryBuilder, studios *models.Hierarch
|
||||
|
||||
func imagePerformerTagsCriterionHandler(qb *imageQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
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)
|
||||
|
||||
f.addWith(`performer_tags AS (
|
||||
|
||||
@@ -680,11 +680,7 @@ func TestImageQueryPerformers(t *testing.T) {
|
||||
Performers: &performerCriterion,
|
||||
}
|
||||
|
||||
images, _, err := queryImagesWithCount(sqb, &imageFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
|
||||
images := queryImages(t, sqb, &imageFilter, nil)
|
||||
assert.Len(t, images, 2)
|
||||
|
||||
// ensure ids are correct
|
||||
@@ -700,11 +696,7 @@ func TestImageQueryPerformers(t *testing.T) {
|
||||
Modifier: models.CriterionModifierIncludesAll,
|
||||
}
|
||||
|
||||
images, _, err = queryImagesWithCount(sqb, &imageFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
|
||||
images = queryImages(t, sqb, &imageFilter, nil)
|
||||
assert.Len(t, images, 1)
|
||||
assert.Equal(t, imageIDs[imageIdxWithTwoPerformers], images[0].ID)
|
||||
|
||||
@@ -720,10 +712,30 @@ func TestImageQueryPerformers(t *testing.T) {
|
||||
Q: &q,
|
||||
}
|
||||
|
||||
images, _, err = queryImagesWithCount(sqb, &imageFilter, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
images = queryImages(t, sqb, &imageFilter, &findFilter)
|
||||
assert.Len(t, images, 0)
|
||||
|
||||
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)
|
||||
|
||||
return nil
|
||||
@@ -745,11 +757,7 @@ func TestImageQueryTags(t *testing.T) {
|
||||
Tags: &tagCriterion,
|
||||
}
|
||||
|
||||
images, _, err := queryImagesWithCount(sqb, &imageFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
|
||||
images := queryImages(t, sqb, &imageFilter, nil)
|
||||
assert.Len(t, images, 2)
|
||||
|
||||
// ensure ids are correct
|
||||
@@ -765,11 +773,7 @@ func TestImageQueryTags(t *testing.T) {
|
||||
Modifier: models.CriterionModifierIncludesAll,
|
||||
}
|
||||
|
||||
images, _, err = queryImagesWithCount(sqb, &imageFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
|
||||
images = queryImages(t, sqb, &imageFilter, nil)
|
||||
assert.Len(t, images, 1)
|
||||
assert.Equal(t, imageIDs[imageIdxWithTwoTags], images[0].ID)
|
||||
|
||||
@@ -785,10 +789,30 @@ func TestImageQueryTags(t *testing.T) {
|
||||
Q: &q,
|
||||
}
|
||||
|
||||
images, _, err = queryImagesWithCount(sqb, &imageFilter, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
images = queryImages(t, sqb, &imageFilter, &findFilter)
|
||||
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(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)
|
||||
|
||||
return nil
|
||||
@@ -962,6 +986,29 @@ func TestImageQueryPerformerTags(t *testing.T) {
|
||||
images = queryImages(t, sqb, &imageFilter, &findFilter)
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -209,7 +209,24 @@ func movieStudioCriterionHandler(qb *movieQueryBuilder, studios *models.Hierarch
|
||||
|
||||
func moviePerformersCriterionHandler(qb *movieQueryBuilder, performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
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{}
|
||||
for _, arg := range performers.Value {
|
||||
args = append(args, arg)
|
||||
|
||||
@@ -439,19 +439,6 @@ func performerGalleryCountCriterionHandler(qb *performerQueryBuilder, count *mod
|
||||
func performerStudiosCriterionHandler(qb *performerQueryBuilder, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
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{
|
||||
{
|
||||
"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"
|
||||
valuesClause := getHierarchicalValues(qb.tx, studios.Value, studioTable, "", "parent_id", studios.Depth)
|
||||
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))
|
||||
}
|
||||
|
||||
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.addWhere(fmt.Sprintf("%s.performer_id IS %s NULL", derivedPerformerStudioTable, clauseCondition))
|
||||
|
||||
@@ -787,6 +787,34 @@ func TestPerformerQueryStudio(t *testing.T) {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -668,7 +668,24 @@ func sceneMoviesCriterionHandler(qb *sceneQueryBuilder, movies *models.MultiCrit
|
||||
|
||||
func scenePerformerTagsCriterionHandler(qb *sceneQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
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)
|
||||
|
||||
f.addWith(`performer_tags AS (
|
||||
|
||||
@@ -191,7 +191,22 @@ func sceneMarkerTagIDCriterionHandler(qb *sceneMarkerQueryBuilder, tagID *string
|
||||
|
||||
func sceneMarkerTagsCriterionHandler(qb *sceneMarkerQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
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)
|
||||
|
||||
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 {
|
||||
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)
|
||||
|
||||
f.addWith(`scene_tags AS (
|
||||
|
||||
@@ -14,15 +14,17 @@ func TestMarkerFindBySceneID(t *testing.T) {
|
||||
withTxn(func(r models.Repository) error {
|
||||
mqb := r.SceneMarker()
|
||||
|
||||
sceneID := sceneIDs[sceneIdxWithMarker]
|
||||
sceneID := sceneIDs[sceneIdxWithMarkers]
|
||||
markers, err := mqb.FindBySceneID(sceneID)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Error finding markers: %s", err.Error())
|
||||
}
|
||||
|
||||
assert.Len(t, markers, 1)
|
||||
assert.Equal(t, markerIDs[markerIdxWithScene], markers[0].ID)
|
||||
assert.Greater(t, len(markers), 0)
|
||||
for _, marker := range markers {
|
||||
assert.Equal(t, sceneIDs[sceneIdxWithMarkers], int(marker.SceneID.Int64))
|
||||
}
|
||||
|
||||
markers, err = mqb.FindBySceneID(0)
|
||||
|
||||
@@ -40,15 +42,15 @@ func TestMarkerCountByTagID(t *testing.T) {
|
||||
withTxn(func(r models.Repository) error {
|
||||
mqb := r.SceneMarker()
|
||||
|
||||
markerCount, err := mqb.CountByTagID(tagIDs[tagIdxWithPrimaryMarker])
|
||||
markerCount, err := mqb.CountByTagID(tagIDs[tagIdxWithPrimaryMarkers])
|
||||
|
||||
if err != nil {
|
||||
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 {
|
||||
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 Destroy
|
||||
// TODO Find
|
||||
|
||||
@@ -775,7 +775,7 @@ func TestSceneQueryHasMarkers(t *testing.T) {
|
||||
HasMarkers: &hasMarkers,
|
||||
}
|
||||
|
||||
q := getSceneStringValue(sceneIdxWithMarker, titleField)
|
||||
q := getSceneStringValue(sceneIdxWithMarkers, titleField)
|
||||
findFilter := models.FindFilterType{
|
||||
Q: &q,
|
||||
}
|
||||
@@ -783,7 +783,7 @@ func TestSceneQueryHasMarkers(t *testing.T) {
|
||||
scenes := queryScene(t, sqb, &sceneFilter, &findFilter)
|
||||
|
||||
assert.Len(t, scenes, 1)
|
||||
assert.Equal(t, sceneIDs[sceneIdxWithMarker], scenes[0].ID)
|
||||
assert.Equal(t, sceneIDs[sceneIdxWithMarkers], scenes[0].ID)
|
||||
|
||||
hasMarkers = "false"
|
||||
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
|
||||
for _, scene := range scenes {
|
||||
assert.NotEqual(t, sceneIDs[sceneIdxWithMarker], scene.ID)
|
||||
assert.NotEqual(t, sceneIDs[sceneIdxWithMarkers], scene.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1151,6 +1151,29 @@ func TestSceneQueryPerformerTags(t *testing.T) {
|
||||
scenes = queryScene(t, sqb, &sceneFilter, &findFilter)
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,10 +34,11 @@ const (
|
||||
sceneIdxWithTwoPerformers
|
||||
sceneIdxWithTag
|
||||
sceneIdxWithTwoTags
|
||||
sceneIdxWithMarkerAndTag
|
||||
sceneIdxWithStudio
|
||||
sceneIdx1WithStudio
|
||||
sceneIdx2WithStudio
|
||||
sceneIdxWithMarker
|
||||
sceneIdxWithMarkers
|
||||
sceneIdxWithPerformerTag
|
||||
sceneIdxWithPerformerTwoTags
|
||||
sceneIdxWithSpacedName
|
||||
@@ -139,8 +140,9 @@ const (
|
||||
tagIdxWithScene = iota
|
||||
tagIdx1WithScene
|
||||
tagIdx2WithScene
|
||||
tagIdxWithPrimaryMarker
|
||||
tagIdxWithMarker
|
||||
tagIdx3WithScene
|
||||
tagIdxWithPrimaryMarkers
|
||||
tagIdxWithMarkers
|
||||
tagIdxWithCoverImage
|
||||
tagIdxWithImage
|
||||
tagIdx1WithImage
|
||||
@@ -191,6 +193,9 @@ const (
|
||||
|
||||
const (
|
||||
markerIdxWithScene = iota
|
||||
markerIdxWithTag
|
||||
markerIdxWithSceneTag
|
||||
totalMarkers
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -239,6 +244,7 @@ var (
|
||||
{sceneIdxWithTag, tagIdxWithScene},
|
||||
{sceneIdxWithTwoTags, tagIdx1WithScene},
|
||||
{sceneIdxWithTwoTags, tagIdx2WithScene},
|
||||
{sceneIdxWithMarkerAndTag, tagIdx3WithScene},
|
||||
}
|
||||
|
||||
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 (
|
||||
imageGalleryLinks = [][2]int{
|
||||
{imageIdxWithGallery, galleryIdxWithImage},
|
||||
@@ -516,8 +537,10 @@ func populateDB() error {
|
||||
return fmt.Errorf("error linking tags parent: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := createMarker(r.SceneMarker(), sceneIdxWithMarker, tagIdxWithPrimaryMarker, []int{tagIdxWithMarker}); err != nil {
|
||||
return fmt.Errorf("error creating scene marker: %s", err.Error())
|
||||
for _, ms := range markerSpecs {
|
||||
if err := createMarker(r.SceneMarker(), ms); err != nil {
|
||||
return fmt.Errorf("error creating scene marker: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -687,6 +710,7 @@ func createGalleries(gqb models.GalleryReaderWriter, n int) error {
|
||||
for i := 0; i < n; i++ {
|
||||
gallery := models.Gallery{
|
||||
Path: models.NullString(getGalleryStringValue(i, pathField)),
|
||||
Title: models.NullString(getGalleryStringValue(i, titleField)),
|
||||
URL: getGalleryNullStringValue(i, urlField),
|
||||
Checksum: getGalleryStringValue(i, checksumField),
|
||||
Rating: getRating(i),
|
||||
@@ -843,7 +867,7 @@ func getTagStringValue(index int, field string) string {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -851,7 +875,11 @@ func getTagSceneCount(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
|
||||
}
|
||||
|
||||
@@ -1008,28 +1036,30 @@ func createStudios(sqb models.StudioReaderWriter, n int, o int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func createMarker(mqb models.SceneMarkerReaderWriter, sceneIdx, primaryTagIdx int, tagIdxs []int) error {
|
||||
func createMarker(mqb models.SceneMarkerReaderWriter, markerSpec markerSpec) error {
|
||||
marker := models.SceneMarker{
|
||||
SceneID: sql.NullInt64{Int64: int64(sceneIDs[sceneIdx]), Valid: true},
|
||||
PrimaryTagID: tagIDs[primaryTagIdx],
|
||||
SceneID: sql.NullInt64{Int64: int64(sceneIDs[markerSpec.sceneIdx]), Valid: true},
|
||||
PrimaryTagID: tagIDs[markerSpec.primaryTagIdx],
|
||||
}
|
||||
|
||||
created, err := mqb.Create(marker)
|
||||
|
||||
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)
|
||||
|
||||
newTagIDs := []int{}
|
||||
if len(markerSpec.tagIdxs) > 0 {
|
||||
newTagIDs := []int{}
|
||||
|
||||
for _, tagIdx := range tagIdxs {
|
||||
newTagIDs = append(newTagIDs, tagIDs[tagIdx])
|
||||
}
|
||||
for _, tagIdx := range markerSpec.tagIdxs {
|
||||
newTagIDs = append(newTagIDs, tagIDs[tagIdx])
|
||||
}
|
||||
|
||||
if err := mqb.UpdateTags(created.ID, newTagIDs); err != nil {
|
||||
return fmt.Errorf("Error creating marker/tag join: %s", err.Error())
|
||||
if err := mqb.UpdateTags(created.ID, newTagIDs); err != nil {
|
||||
return fmt.Errorf("error creating marker/tag join: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -443,7 +443,23 @@ func tagMarkerCountCriterionHandler(qb *tagQueryBuilder, markerCount *models.Int
|
||||
|
||||
func tagParentsCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
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{}
|
||||
for _, val := range tags.Value {
|
||||
args = append(args, val)
|
||||
@@ -476,7 +492,23 @@ func tagParentsCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalMu
|
||||
|
||||
func tagChildrenCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||
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{}
|
||||
for _, val := range tags.Value {
|
||||
args = append(args, val)
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestMarkerFindBySceneMarkerID(t *testing.T) {
|
||||
withTxn(func(r models.Repository) error {
|
||||
tqb := r.Tag()
|
||||
|
||||
markerID := markerIDs[markerIdxWithScene]
|
||||
markerID := markerIDs[markerIdxWithTag]
|
||||
|
||||
tags, err := tqb.FindBySceneMarkerID(markerID)
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestMarkerFindBySceneMarkerID(t *testing.T) {
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -168,7 +168,7 @@ func TestTagQuerySort(t *testing.T) {
|
||||
|
||||
sortBy = "scene_markers_count"
|
||||
tags = queryTags(t, sqb, nil, findFilter)
|
||||
assert.Equal(tagIDs[tagIdxWithMarker], tags[0].ID)
|
||||
assert.Equal(tagIDs[tagIdxWithMarkers], tags[0].ID)
|
||||
|
||||
sortBy = "images_count"
|
||||
tags = queryTags(t, sqb, nil, findFilter)
|
||||
@@ -613,6 +613,7 @@ func verifyTagChildCount(t *testing.T, sceneCountCriterion models.IntCriterionIn
|
||||
|
||||
func TestTagQueryParent(t *testing.T) {
|
||||
withTxn(func(r models.Repository) error {
|
||||
const nameField = "Name"
|
||||
sqb := r.Tag()
|
||||
tagCriterion := models.HierarchicalMultiCriterionInput{
|
||||
Value: []string{
|
||||
@@ -634,7 +635,7 @@ func TestTagQueryParent(t *testing.T) {
|
||||
|
||||
tagCriterion.Modifier = models.CriterionModifierExcludes
|
||||
|
||||
q := getTagStringValue(tagIdxWithParentTag, titleField)
|
||||
q := getTagStringValue(tagIdxWithParentTag, nameField)
|
||||
findFilter := models.FindFilterType{
|
||||
Q: &q,
|
||||
}
|
||||
@@ -660,12 +661,37 @@ func TestTagQueryParent(t *testing.T) {
|
||||
tags = queryTags(t, sqb, &tagFilter, nil)
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
func TestTagQueryChild(t *testing.T) {
|
||||
withTxn(func(r models.Repository) error {
|
||||
const nameField = "Name"
|
||||
|
||||
sqb := r.Tag()
|
||||
tagCriterion := models.HierarchicalMultiCriterionInput{
|
||||
Value: []string{
|
||||
@@ -687,7 +713,7 @@ func TestTagQueryChild(t *testing.T) {
|
||||
|
||||
tagCriterion.Modifier = models.CriterionModifierExcludes
|
||||
|
||||
q := getTagStringValue(tagIdxWithChildTag, titleField)
|
||||
q := getTagStringValue(tagIdxWithChildTag, nameField)
|
||||
findFilter := models.FindFilterType{
|
||||
Q: &q,
|
||||
}
|
||||
@@ -713,6 +739,29 @@ func TestTagQueryChild(t *testing.T) {
|
||||
tags = queryTags(t, sqb, &tagFilter, nil)
|
||||
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
|
||||
})
|
||||
}
|
||||
@@ -842,8 +891,8 @@ func TestTagMerge(t *testing.T) {
|
||||
srcIdxs := []int{
|
||||
tagIdx1WithScene,
|
||||
tagIdx2WithScene,
|
||||
tagIdxWithPrimaryMarker,
|
||||
tagIdxWithMarker,
|
||||
tagIdxWithPrimaryMarkers,
|
||||
tagIdxWithMarkers,
|
||||
tagIdxWithCoverImage,
|
||||
tagIdxWithImage,
|
||||
tagIdx1WithImage,
|
||||
@@ -893,7 +942,7 @@ func TestTagMerge(t *testing.T) {
|
||||
assert.Contains(sceneTagIDs, destID)
|
||||
|
||||
// ensure marker points to new tag
|
||||
marker, err := r.SceneMarker().Find(markerIDs[markerIdxWithScene])
|
||||
marker, err := r.SceneMarker().Find(markerIDs[markerIdxWithTag])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
### ✨ 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))
|
||||
* 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))
|
||||
|
||||
@@ -655,9 +655,14 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
defaultFilter?.findDefaultFilter
|
||||
) {
|
||||
newFilter.currentPage = 1;
|
||||
newFilter.configureFromQueryParameters(
|
||||
JSON.parse(defaultFilter.findDefaultFilter.filter)
|
||||
);
|
||||
try {
|
||||
newFilter.configureFromQueryParameters(
|
||||
JSON.parse(defaultFilter.findDefaultFilter.filter)
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
// ignore
|
||||
}
|
||||
// #1507 - reset random seed when loaded
|
||||
newFilter.randomSeed = -1;
|
||||
update = true;
|
||||
|
||||
@@ -364,6 +364,8 @@ export class ILabeledIdCriterionOption extends CriterionOption {
|
||||
const modifierOptions = [
|
||||
CriterionModifier.Includes,
|
||||
CriterionModifier.Excludes,
|
||||
CriterionModifier.IsNull,
|
||||
CriterionModifier.NotNull,
|
||||
];
|
||||
|
||||
let defaultModifier = CriterionModifier.Includes;
|
||||
|
||||
Reference in New Issue
Block a user