Fix joined hierarchical filtering (#3775)

* Fix joined hierarchical filtering
* Fix scene performer tag filter
* Generalise performer tag handler
* Add unit tests
* Add equals handling
* Make performer tags equals/not equals unsupported
* Make tags not equals unsupported
* Make not equals unsupported for performers criterion
* Support equals/not equals for studio criterion
* Fix marker scene tags equals filter
* Fix scene performer tag filter
* Make equals/not equals unsupported for hierarchical criterion
* Use existing studio handler in movie
* Hide unsupported tag modifier options
* Use existing performer tags logic where possible
* Restore old parent/child filter logic
* Disable sub-tags in equals modifier for tags criterion
This commit is contained in:
WithoutPants
2023-06-06 13:01:50 +10:00
committed by GitHub
parent 4acf843229
commit 256e0a11ea
19 changed files with 2153 additions and 938 deletions

View File

@@ -135,6 +135,17 @@ type HierarchicalMultiCriterionInput struct {
Excludes []string `json:"excludes"` Excludes []string `json:"excludes"`
} }
func (i HierarchicalMultiCriterionInput) CombineExcludes() HierarchicalMultiCriterionInput {
ii := i
if ii.Modifier == CriterionModifierExcludes {
ii.Modifier = CriterionModifierIncludesAll
ii.Excludes = append(ii.Excludes, ii.Value...)
ii.Value = nil
}
return ii
}
type MultiCriterionInput struct { type MultiCriterionInput struct {
Value []string `json:"value"` Value []string `json:"value"`
Modifier CriterionModifier `json:"modifier"` Modifier CriterionModifier `json:"modifier"`

View File

@@ -9,7 +9,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/utils" "github.com/stashapp/stash/pkg/utils"
"github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models"
@@ -694,6 +693,8 @@ func (m *joinedMultiCriterionHandlerBuilder) handler(c *models.MultiCriterionInp
}) })
havingClause = fmt.Sprintf("count(distinct %s.%s) IS %d", joinAlias, m.foreignFK, len(criterion.Value)) havingClause = fmt.Sprintf("count(distinct %s.%s) IS %d", joinAlias, m.foreignFK, len(criterion.Value))
args = append(args, len(criterion.Value)) args = append(args, len(criterion.Value))
case models.CriterionModifierNotEquals:
f.setError(fmt.Errorf("not equals modifier is not supported for multi criterion input"))
case models.CriterionModifierIncludesAll: case models.CriterionModifierIncludesAll:
// includes all of the provided ids // includes all of the provided ids
m.addJoinTable(f) m.addJoinTable(f)
@@ -830,6 +831,33 @@ func (m *stringListCriterionHandlerBuilder) handler(criterion *models.StringCrit
} }
} }
func studioCriterionHandler(primaryTable string, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if studios == nil {
return
}
studiosCopy := *studios
switch studiosCopy.Modifier {
case models.CriterionModifierEquals:
studiosCopy.Modifier = models.CriterionModifierIncludesAll
case models.CriterionModifierNotEquals:
studiosCopy.Modifier = models.CriterionModifierExcludes
}
hh := hierarchicalMultiCriterionHandlerBuilder{
tx: dbWrapper{},
primaryTable: primaryTable,
foreignTable: studioTable,
foreignFK: studioIDColumn,
parentFK: "parent_id",
}
hh.handler(&studiosCopy)(ctx, f)
}
}
type hierarchicalMultiCriterionHandlerBuilder struct { type hierarchicalMultiCriterionHandlerBuilder struct {
tx dbWrapper tx dbWrapper
@@ -838,12 +866,20 @@ type hierarchicalMultiCriterionHandlerBuilder struct {
foreignFK string foreignFK string
parentFK string parentFK string
childFK string
relationsTable string relationsTable string
} }
func getHierarchicalValues(ctx context.Context, tx dbWrapper, values []string, table, relationsTable, parentFK string, depth *int) string { func getHierarchicalValues(ctx context.Context, tx dbWrapper, values []string, table, relationsTable, parentFK string, childFK string, depth *int) (string, error) {
var args []interface{} var args []interface{}
if parentFK == "" {
parentFK = "parent_id"
}
if childFK == "" {
childFK = "child_id"
}
depthVal := 0 depthVal := 0
if depth != nil { if depth != nil {
depthVal = *depth depthVal = *depth
@@ -865,7 +901,7 @@ func getHierarchicalValues(ctx context.Context, tx dbWrapper, values []string, t
} }
if valid { if valid {
return "VALUES" + strings.Join(valuesClauses, ",") return "VALUES" + strings.Join(valuesClauses, ","), nil
} }
} }
@@ -885,13 +921,14 @@ func getHierarchicalValues(ctx context.Context, tx dbWrapper, values []string, t
"inBinding": getInBinding(inCount), "inBinding": getInBinding(inCount),
"recursiveSelect": "", "recursiveSelect": "",
"parentFK": parentFK, "parentFK": parentFK,
"childFK": childFK,
"depthCondition": depthCondition, "depthCondition": depthCondition,
"unionClause": "", "unionClause": "",
} }
if relationsTable != "" { if relationsTable != "" {
withClauseMap["recursiveSelect"] = utils.StrFormat(`SELECT p.root_id, c.child_id, depth + 1 FROM {relationsTable} AS c withClauseMap["recursiveSelect"] = utils.StrFormat(`SELECT p.root_id, c.{childFK}, depth + 1 FROM {relationsTable} AS c
INNER JOIN items as p ON c.parent_id = p.item_id INNER JOIN items as p ON c.{parentFK} = p.item_id
`, withClauseMap) `, withClauseMap)
} else { } else {
withClauseMap["recursiveSelect"] = utils.StrFormat(`SELECT p.root_id, c.id, depth + 1 FROM {table} as c withClauseMap["recursiveSelect"] = utils.StrFormat(`SELECT p.root_id, c.id, depth + 1 FROM {table} as c
@@ -916,12 +953,10 @@ WHERE id in {inBinding}
var valuesClause string var valuesClause string
err := tx.Get(ctx, &valuesClause, query, args...) err := tx.Get(ctx, &valuesClause, query, args...)
if err != nil { if err != nil {
logger.Error(err) return "", fmt.Errorf("failed to get hierarchical values: %w", err)
// return record which never matches so we don't have to handle error here
return "VALUES(NULL, NULL)"
} }
return valuesClause return valuesClause, nil
} }
func addHierarchicalConditionClauses(f *filterBuilder, criterion models.HierarchicalMultiCriterionInput, table, idColumn string) { func addHierarchicalConditionClauses(f *filterBuilder, criterion models.HierarchicalMultiCriterionInput, table, idColumn string) {
@@ -942,6 +977,12 @@ func (m *hierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hierarchica
// make a copy so we don't modify the original // make a copy so we don't modify the original
criterion := *c criterion := *c
// don't support equals/not equals
if criterion.Modifier == models.CriterionModifierEquals || criterion.Modifier == models.CriterionModifierNotEquals {
f.setError(fmt.Errorf("modifier %s is not supported for hierarchical multi criterion", criterion.Modifier))
return
}
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull { if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
var notClause string var notClause string
if criterion.Modifier == models.CriterionModifierNotNull { if criterion.Modifier == models.CriterionModifierNotNull {
@@ -968,7 +1009,11 @@ func (m *hierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hierarchica
} }
if len(criterion.Value) > 0 { if len(criterion.Value) > 0 {
valuesClause := getHierarchicalValues(ctx, m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, criterion.Depth) valuesClause, err := getHierarchicalValues(ctx, m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, m.childFK, criterion.Depth)
if err != nil {
f.setError(err)
return
}
switch criterion.Modifier { switch criterion.Modifier {
case models.CriterionModifierIncludes: case models.CriterionModifierIncludes:
@@ -980,7 +1025,11 @@ func (m *hierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hierarchica
} }
if len(criterion.Excludes) > 0 { if len(criterion.Excludes) > 0 {
valuesClause := getHierarchicalValues(ctx, m.tx, criterion.Excludes, m.foreignTable, m.relationsTable, m.parentFK, criterion.Depth) valuesClause, err := getHierarchicalValues(ctx, m.tx, criterion.Excludes, m.foreignTable, m.relationsTable, m.parentFK, m.childFK, criterion.Depth)
if err != nil {
f.setError(err)
return
}
f.addWhere(fmt.Sprintf("%s.%s NOT IN (SELECT column2 FROM (%s)) OR %[1]s.%[2]s IS NULL", m.primaryTable, m.foreignFK, valuesClause)) f.addWhere(fmt.Sprintf("%s.%s NOT IN (SELECT column2 FROM (%s)) OR %[1]s.%[2]s IS NULL", m.primaryTable, m.foreignFK, valuesClause))
} }
@@ -992,10 +1041,12 @@ type joinedHierarchicalMultiCriterionHandlerBuilder struct {
tx dbWrapper tx dbWrapper
primaryTable string primaryTable string
primaryKey string
foreignTable string foreignTable string
foreignFK string foreignFK string
parentFK string parentFK string
childFK string
relationsTable string relationsTable string
joinAs string joinAs string
@@ -1004,16 +1055,25 @@ type joinedHierarchicalMultiCriterionHandlerBuilder struct {
} }
func (m *joinedHierarchicalMultiCriterionHandlerBuilder) addHierarchicalConditionClauses(f *filterBuilder, criterion models.HierarchicalMultiCriterionInput, table, idColumn string) { func (m *joinedHierarchicalMultiCriterionHandlerBuilder) addHierarchicalConditionClauses(f *filterBuilder, criterion models.HierarchicalMultiCriterionInput, table, idColumn string) {
if criterion.Modifier == models.CriterionModifierEquals { primaryKey := m.primaryKey
if primaryKey == "" {
primaryKey = "id"
}
switch criterion.Modifier {
case models.CriterionModifierEquals:
// includes only the provided ids // includes only the provided ids
f.addWhere(fmt.Sprintf("%s.%s IS NOT NULL", table, idColumn)) f.addWhere(fmt.Sprintf("%s.%s IS NOT NULL", table, idColumn))
f.addHaving(fmt.Sprintf("count(distinct %s.%s) IS %d", table, idColumn, len(criterion.Value))) f.addHaving(fmt.Sprintf("count(distinct %s.%s) IS %d", table, idColumn, len(criterion.Value)))
f.addWhere(utils.StrFormat("(SELECT COUNT(*) FROM {joinTable} s WHERE s.{primaryFK} = {primaryTable}.id) = ?", utils.StrFormatMap{ f.addWhere(utils.StrFormat("(SELECT COUNT(*) FROM {joinTable} s WHERE s.{primaryFK} = {primaryTable}.{primaryKey}) = ?", utils.StrFormatMap{
"joinTable": m.joinTable, "joinTable": m.joinTable,
"primaryFK": m.primaryFK, "primaryFK": m.primaryFK,
"primaryTable": m.primaryTable, "primaryTable": m.primaryTable,
"primaryKey": primaryKey,
}), len(criterion.Value)) }), len(criterion.Value))
} else { case models.CriterionModifierNotEquals:
f.setError(fmt.Errorf("not equals modifier is not supported for hierarchical multi criterion input"))
default:
addHierarchicalConditionClauses(f, criterion, table, idColumn) addHierarchicalConditionClauses(f, criterion, table, idColumn)
} }
} }
@@ -1024,6 +1084,15 @@ func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hiera
// make a copy so we don't modify the original // make a copy so we don't modify the original
criterion := *c criterion := *c
joinAlias := m.joinAs joinAlias := m.joinAs
primaryKey := m.primaryKey
if primaryKey == "" {
primaryKey = "id"
}
if criterion.Modifier == models.CriterionModifierEquals && criterion.Depth != nil && *criterion.Depth != 0 {
f.setError(fmt.Errorf("depth is not supported for equals modifier in hierarchical multi criterion input"))
return
}
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull { if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
var notClause string var notClause string
@@ -1031,7 +1100,7 @@ func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hiera
notClause = "NOT" notClause = "NOT"
} }
f.addLeftJoin(m.joinTable, joinAlias, fmt.Sprintf("%s.%s = %s.id", joinAlias, m.primaryFK, m.primaryTable)) f.addLeftJoin(m.joinTable, joinAlias, fmt.Sprintf("%s.%s = %s.%s", joinAlias, m.primaryFK, m.primaryTable, primaryKey))
f.addWhere(utils.StrFormat("{table}.{column} IS {not} NULL", utils.StrFormatMap{ f.addWhere(utils.StrFormat("{table}.{column} IS {not} NULL", utils.StrFormatMap{
"table": joinAlias, "table": joinAlias,
@@ -1053,7 +1122,11 @@ func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hiera
} }
if len(criterion.Value) > 0 { if len(criterion.Value) > 0 {
valuesClause := getHierarchicalValues(ctx, m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, criterion.Depth) valuesClause, err := getHierarchicalValues(ctx, m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, m.childFK, criterion.Depth)
if err != nil {
f.setError(err)
return
}
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
@@ -1065,13 +1138,17 @@ func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hiera
"valuesClause": valuesClause, "valuesClause": valuesClause,
}) })
f.addLeftJoin(joinTable, joinAlias, fmt.Sprintf("%s.%s = %s.id", joinAlias, m.primaryFK, m.primaryTable)) f.addLeftJoin(joinTable, joinAlias, fmt.Sprintf("%s.%s = %s.%s", joinAlias, m.primaryFK, m.primaryTable, primaryKey))
m.addHierarchicalConditionClauses(f, criterion, joinAlias, "root_id") m.addHierarchicalConditionClauses(f, criterion, joinAlias, "root_id")
} }
if len(criterion.Excludes) > 0 { if len(criterion.Excludes) > 0 {
valuesClause := getHierarchicalValues(ctx, m.tx, criterion.Excludes, m.foreignTable, m.relationsTable, m.parentFK, criterion.Depth) valuesClause, err := getHierarchicalValues(ctx, m.tx, criterion.Excludes, m.foreignTable, m.relationsTable, m.parentFK, m.childFK, criterion.Depth)
if err != nil {
f.setError(err)
return
}
joinTable := utils.StrFormat(`( joinTable := utils.StrFormat(`(
SELECT j2.*, e.column1 AS root_id, e.column2 AS item_id FROM {joinTable} AS j2 SELECT j2.*, e.column1 AS root_id, e.column2 AS item_id FROM {joinTable} AS j2
@@ -1085,7 +1162,7 @@ func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hiera
joinAlias2 := joinAlias + "2" joinAlias2 := joinAlias + "2"
f.addLeftJoin(joinTable, joinAlias2, fmt.Sprintf("%s.%s = %s.id", joinAlias2, m.primaryFK, m.primaryTable)) f.addLeftJoin(joinTable, joinAlias2, fmt.Sprintf("%s.%s = %s.%s", joinAlias2, m.primaryFK, m.primaryTable, primaryKey))
// modify for exclusion // modify for exclusion
criterionCopy := criterion criterionCopy := criterion
@@ -1098,6 +1175,83 @@ func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hiera
} }
} }
type joinedPerformerTagsHandler struct {
criterion *models.HierarchicalMultiCriterionInput
primaryTable string // eg scenes
joinTable string // eg performers_scenes
joinPrimaryKey string // eg scene_id
}
func (h *joinedPerformerTagsHandler) handle(ctx context.Context, f *filterBuilder) {
tags := h.criterion
if tags != nil {
criterion := tags.CombineExcludes()
// validate the modifier
switch criterion.Modifier {
case models.CriterionModifierIncludesAll, models.CriterionModifierIncludes, models.CriterionModifierExcludes, models.CriterionModifierIsNull, models.CriterionModifierNotNull:
// valid
default:
f.setError(fmt.Errorf("invalid modifier %s for performer tags", criterion.Modifier))
}
strFormatMap := utils.StrFormatMap{
"primaryTable": h.primaryTable,
"joinTable": h.joinTable,
"joinPrimaryKey": h.joinPrimaryKey,
"inBinding": getInBinding(len(criterion.Value)),
}
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
var notClause string
if criterion.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addLeftJoin(h.joinTable, "", utils.StrFormat("{primaryTable}.id = {joinTable}.{joinPrimaryKey}", strFormatMap))
f.addLeftJoin("performers_tags", "", utils.StrFormat("{joinTable}.performer_id = performers_tags.performer_id", strFormatMap))
f.addWhere(fmt.Sprintf("performers_tags.tag_id IS %s NULL", notClause))
return
}
if len(criterion.Value) == 0 && len(criterion.Excludes) == 0 {
return
}
if len(criterion.Value) > 0 {
valuesClause, err := getHierarchicalValues(ctx, dbWrapper{}, criterion.Value, tagTable, "tags_relations", "", "", criterion.Depth)
if err != nil {
f.setError(err)
return
}
f.addWith(utils.StrFormat(`performer_tags AS (
SELECT ps.{joinPrimaryKey} as primaryID, t.column1 AS root_tag_id FROM {joinTable} ps
INNER JOIN performers_tags pt ON pt.performer_id = ps.performer_id
INNER JOIN (`+valuesClause+`) t ON t.column2 = pt.tag_id
)`, strFormatMap))
f.addLeftJoin("performer_tags", "", utils.StrFormat("performer_tags.primaryID = {primaryTable}.id", strFormatMap))
addHierarchicalConditionClauses(f, criterion, "performer_tags", "root_tag_id")
}
if len(criterion.Excludes) > 0 {
valuesClause, err := getHierarchicalValues(ctx, dbWrapper{}, criterion.Excludes, tagTable, "tags_relations", "", "", criterion.Depth)
if err != nil {
f.setError(err)
return
}
clause := utils.StrFormat("{primaryTable}.id NOT IN (SELECT {joinTable}.{joinPrimaryKey} FROM {joinTable} INNER JOIN performers_tags ON {joinTable}.performer_id = performers_tags.performer_id WHERE performers_tags.tag_id IN (SELECT column2 FROM (%s)))", strFormatMap)
f.addWhere(fmt.Sprintf(clause, valuesClause))
}
}
}
type stashIDCriterionHandler struct { type stashIDCriterionHandler struct {
c *models.StashIDCriterionInput c *models.StashIDCriterionInput
stashIDRepository *stashIDRepository stashIDRepository *stashIDRepository

View File

@@ -670,7 +670,7 @@ func (qb *GalleryStore) makeFilter(ctx context.Context, galleryFilter *models.Ga
query.handleCriterion(ctx, galleryPerformersCriterionHandler(qb, galleryFilter.Performers)) query.handleCriterion(ctx, galleryPerformersCriterionHandler(qb, galleryFilter.Performers))
query.handleCriterion(ctx, galleryPerformerCountCriterionHandler(qb, galleryFilter.PerformerCount)) query.handleCriterion(ctx, galleryPerformerCountCriterionHandler(qb, galleryFilter.PerformerCount))
query.handleCriterion(ctx, hasChaptersCriterionHandler(galleryFilter.HasChapters)) query.handleCriterion(ctx, hasChaptersCriterionHandler(galleryFilter.HasChapters))
query.handleCriterion(ctx, galleryStudioCriterionHandler(qb, galleryFilter.Studios)) query.handleCriterion(ctx, studioCriterionHandler(galleryTable, galleryFilter.Studios))
query.handleCriterion(ctx, galleryPerformerTagsCriterionHandler(qb, galleryFilter.PerformerTags)) query.handleCriterion(ctx, galleryPerformerTagsCriterionHandler(qb, galleryFilter.PerformerTags))
query.handleCriterion(ctx, galleryAverageResolutionCriterionHandler(qb, galleryFilter.AverageResolution)) query.handleCriterion(ctx, galleryAverageResolutionCriterionHandler(qb, galleryFilter.AverageResolution))
query.handleCriterion(ctx, galleryImageCountCriterionHandler(qb, galleryFilter.ImageCount)) query.handleCriterion(ctx, galleryImageCountCriterionHandler(qb, galleryFilter.ImageCount))
@@ -968,51 +968,12 @@ func hasChaptersCriterionHandler(hasChapters *string) criterionHandlerFunc {
} }
} }
func galleryStudioCriterionHandler(qb *GalleryStore, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { func galleryPerformerTagsCriterionHandler(qb *GalleryStore, tags *models.HierarchicalMultiCriterionInput) criterionHandler {
h := hierarchicalMultiCriterionHandlerBuilder{ return &joinedPerformerTagsHandler{
tx: qb.tx, criterion: tags,
primaryTable: galleryTable, primaryTable: galleryTable,
foreignTable: studioTable, joinTable: performersGalleriesTable,
foreignFK: studioIDColumn, joinPrimaryKey: galleryIDColumn,
parentFK: "parent_id",
}
return h.handler(studios)
}
func galleryPerformerTagsCriterionHandler(qb *GalleryStore, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if tags != nil {
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
var notClause string
if tags.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addLeftJoin("performers_galleries", "", "galleries.id = performers_galleries.gallery_id")
f.addLeftJoin("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(ctx, qb.tx, tags.Value, tagTable, "tags_relations", "", tags.Depth)
f.addWith(`performer_tags AS (
SELECT pg.gallery_id, t.column1 AS root_tag_id FROM performers_galleries pg
INNER JOIN performers_tags pt ON pt.performer_id = pg.performer_id
INNER JOIN (` + valuesClause + `) t ON t.column2 = pt.tag_id
)`)
f.addLeftJoin("performer_tags", "", "performer_tags.gallery_id = galleries.id")
addHierarchicalConditionClauses(f, *tags, "performer_tags", "root_tag_id")
}
} }
} }

View File

@@ -1945,155 +1945,370 @@ func TestGalleryQueryIsMissingDate(t *testing.T) {
} }
func TestGalleryQueryPerformers(t *testing.T) { func TestGalleryQueryPerformers(t *testing.T) {
withTxn(func(ctx context.Context) error { tests := []struct {
sqb := db.Gallery name string
performerCriterion := models.MultiCriterionInput{ filter models.MultiCriterionInput
includeIdxs []int
excludeIdxs []int
wantErr bool
}{
{
"includes",
models.MultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(performerIDs[performerIdxWithGallery]), strconv.Itoa(performerIDs[performerIdxWithGallery]),
strconv.Itoa(performerIDs[performerIdx1WithGallery]), strconv.Itoa(performerIDs[performerIdx1WithGallery]),
}, },
Modifier: models.CriterionModifierIncludes, Modifier: models.CriterionModifierIncludes,
} },
[]int{
galleryFilter := models.GalleryFilterType{ galleryIdxWithPerformer,
Performers: &performerCriterion, galleryIdxWithTwoPerformers,
} },
[]int{
galleries := queryGallery(ctx, t, sqb, &galleryFilter, nil) galleryIdxWithImage,
},
assert.Len(t, galleries, 2) false,
},
// ensure ids are correct {
for _, gallery := range galleries { "includes all",
assert.True(t, gallery.ID == galleryIDs[galleryIdxWithPerformer] || gallery.ID == galleryIDs[galleryIdxWithTwoPerformers]) models.MultiCriterionInput{
}
performerCriterion = models.MultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(performerIDs[performerIdx1WithGallery]), strconv.Itoa(performerIDs[performerIdx1WithGallery]),
strconv.Itoa(performerIDs[performerIdx2WithGallery]), strconv.Itoa(performerIDs[performerIdx2WithGallery]),
}, },
Modifier: models.CriterionModifierIncludesAll, Modifier: models.CriterionModifierIncludesAll,
}
galleries = queryGallery(ctx, t, sqb, &galleryFilter, nil)
assert.Len(t, galleries, 1)
assert.Equal(t, galleryIDs[galleryIdxWithTwoPerformers], galleries[0].ID)
performerCriterion = models.MultiCriterionInput{
Value: []string{
strconv.Itoa(performerIDs[performerIdx1WithGallery]),
}, },
[]int{
galleryIdxWithTwoPerformers,
},
[]int{
galleryIdxWithPerformer,
},
false,
},
{
"excludes",
models.MultiCriterionInput{
Modifier: models.CriterionModifierExcludes, Modifier: models.CriterionModifierExcludes,
Value: []string{strconv.Itoa(tagIDs[performerIdx1WithGallery])},
},
nil,
[]int{galleryIdxWithTwoPerformers},
false,
},
{
"is null",
models.MultiCriterionInput{
Modifier: models.CriterionModifierIsNull,
},
[]int{galleryIdxWithTag},
[]int{
galleryIdxWithPerformer,
galleryIdxWithTwoPerformers,
galleryIdxWithPerformerTwoTags,
},
false,
},
{
"not null",
models.MultiCriterionInput{
Modifier: models.CriterionModifierNotNull,
},
[]int{
galleryIdxWithPerformer,
galleryIdxWithTwoPerformers,
galleryIdxWithPerformerTwoTags,
},
[]int{galleryIdxWithTag},
false,
},
{
"equals",
models.MultiCriterionInput{
Modifier: models.CriterionModifierEquals,
Value: []string{
strconv.Itoa(tagIDs[performerIdx1WithGallery]),
strconv.Itoa(tagIDs[performerIdx2WithGallery]),
},
},
[]int{galleryIdxWithTwoPerformers},
[]int{
galleryIdxWithThreePerformers,
},
false,
},
{
"not equals",
models.MultiCriterionInput{
Modifier: models.CriterionModifierNotEquals,
Value: []string{
strconv.Itoa(tagIDs[performerIdx1WithGallery]),
strconv.Itoa(tagIDs[performerIdx2WithGallery]),
},
},
nil,
nil,
true,
},
} }
q := getGalleryStringValue(galleryIdxWithTwoPerformers, titleField) for _, tt := range tests {
findFilter := models.FindFilterType{ runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
Q: &q, assert := assert.New(t)
results, _, err := db.Gallery.Query(ctx, &models.GalleryFilterType{
Performers: &tt.filter,
}, nil)
if (err != nil) != tt.wantErr {
t.Errorf("GalleryStore.Query() error = %v, wantErr %v", err, tt.wantErr)
return
} }
galleries = queryGallery(ctx, t, sqb, &galleryFilter, &findFilter) ids := galleriesToIDs(results)
assert.Len(t, galleries, 0)
return nil include := indexesToIDs(galleryIDs, tt.includeIdxs)
exclude := indexesToIDs(galleryIDs, tt.excludeIdxs)
for _, i := range include {
assert.Contains(ids, i)
}
for _, e := range exclude {
assert.NotContains(ids, e)
}
}) })
} }
}
func TestGalleryQueryTags(t *testing.T) { func TestGalleryQueryTags(t *testing.T) {
withTxn(func(ctx context.Context) error { tests := []struct {
sqb := db.Gallery name string
tagCriterion := models.HierarchicalMultiCriterionInput{ filter models.HierarchicalMultiCriterionInput
includeIdxs []int
excludeIdxs []int
wantErr bool
}{
{
"includes",
models.HierarchicalMultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(tagIDs[tagIdxWithGallery]), strconv.Itoa(tagIDs[tagIdxWithGallery]),
strconv.Itoa(tagIDs[tagIdx1WithGallery]), strconv.Itoa(tagIDs[tagIdx1WithGallery]),
}, },
Modifier: models.CriterionModifierIncludes, Modifier: models.CriterionModifierIncludes,
} },
[]int{
galleryFilter := models.GalleryFilterType{ galleryIdxWithTag,
Tags: &tagCriterion, galleryIdxWithTwoTags,
} },
[]int{
galleries := queryGallery(ctx, t, sqb, &galleryFilter, nil) galleryIdxWithImage,
assert.Len(t, galleries, 2) },
false,
// ensure ids are correct },
for _, gallery := range galleries { {
assert.True(t, gallery.ID == galleryIDs[galleryIdxWithTag] || gallery.ID == galleryIDs[galleryIdxWithTwoTags]) "includes all",
} models.HierarchicalMultiCriterionInput{
tagCriterion = models.HierarchicalMultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithGallery]), strconv.Itoa(tagIDs[tagIdx1WithGallery]),
strconv.Itoa(tagIDs[tagIdx2WithGallery]), strconv.Itoa(tagIDs[tagIdx2WithGallery]),
}, },
Modifier: models.CriterionModifierIncludesAll, Modifier: models.CriterionModifierIncludesAll,
} },
[]int{
galleries = queryGallery(ctx, t, sqb, &galleryFilter, nil) galleryIdxWithTwoTags,
},
assert.Len(t, galleries, 1) []int{
assert.Equal(t, galleryIDs[galleryIdxWithTwoTags], galleries[0].ID) galleryIdxWithTag,
},
tagCriterion = models.HierarchicalMultiCriterionInput{ false,
},
{
"excludes",
models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierExcludes,
Value: []string{strconv.Itoa(tagIDs[tagIdx1WithGallery])},
},
nil,
[]int{galleryIdxWithTwoTags},
false,
},
{
"is null",
models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIsNull,
},
[]int{galleryIdx1WithPerformer},
[]int{
galleryIdxWithTag,
galleryIdxWithTwoTags,
galleryIdxWithThreeTags,
},
false,
},
{
"not null",
models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierNotNull,
},
[]int{
galleryIdxWithTag,
galleryIdxWithTwoTags,
galleryIdxWithThreeTags,
},
[]int{galleryIdx1WithPerformer},
false,
},
{
"equals",
models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierEquals,
Value: []string{ Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithGallery]), strconv.Itoa(tagIDs[tagIdx1WithGallery]),
strconv.Itoa(tagIDs[tagIdx2WithGallery]),
},
},
[]int{galleryIdxWithTwoTags},
[]int{
galleryIdxWithThreeTags,
},
false,
},
{
"not equals",
models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierNotEquals,
Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithGallery]),
strconv.Itoa(tagIDs[tagIdx2WithGallery]),
},
},
nil,
nil,
true,
}, },
Modifier: models.CriterionModifierExcludes,
} }
q := getGalleryStringValue(galleryIdxWithTwoTags, titleField) for _, tt := range tests {
findFilter := models.FindFilterType{ runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
Q: &q, assert := assert.New(t)
results, _, err := db.Gallery.Query(ctx, &models.GalleryFilterType{
Tags: &tt.filter,
}, nil)
if (err != nil) != tt.wantErr {
t.Errorf("GalleryStore.Query() error = %v, wantErr %v", err, tt.wantErr)
return
} }
galleries = queryGallery(ctx, t, sqb, &galleryFilter, &findFilter) ids := galleriesToIDs(results)
assert.Len(t, galleries, 0)
return nil include := indexesToIDs(imageIDs, tt.includeIdxs)
exclude := indexesToIDs(imageIDs, tt.excludeIdxs)
for _, i := range include {
assert.Contains(ids, i)
}
for _, e := range exclude {
assert.NotContains(ids, e)
}
}) })
} }
}
func TestGalleryQueryStudio(t *testing.T) { func TestGalleryQueryStudio(t *testing.T) {
withTxn(func(ctx context.Context) error { tests := []struct {
sqb := db.Gallery name string
studioCriterion := models.HierarchicalMultiCriterionInput{ q string
studioCriterion models.HierarchicalMultiCriterionInput
expectedIDs []int
wantErr bool
}{
{
"includes",
"",
models.HierarchicalMultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(studioIDs[studioIdxWithGallery]), strconv.Itoa(studioIDs[studioIdxWithGallery]),
}, },
Modifier: models.CriterionModifierIncludes, Modifier: models.CriterionModifierIncludes,
},
[]int{galleryIDs[galleryIdxWithStudio]},
false,
},
{
"excludes",
getGalleryStringValue(galleryIdxWithStudio, titleField),
models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(studioIDs[studioIdxWithGallery]),
},
Modifier: models.CriterionModifierExcludes,
},
[]int{},
false,
},
{
"excludes includes null",
getGalleryStringValue(galleryIdxWithImage, titleField),
models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(studioIDs[studioIdxWithGallery]),
},
Modifier: models.CriterionModifierExcludes,
},
[]int{galleryIDs[galleryIdxWithImage]},
false,
},
{
"equals",
"",
models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(studioIDs[studioIdxWithGallery]),
},
Modifier: models.CriterionModifierEquals,
},
[]int{galleryIDs[galleryIdxWithStudio]},
false,
},
{
"not equals",
getGalleryStringValue(galleryIdxWithStudio, titleField),
models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(studioIDs[studioIdxWithGallery]),
},
Modifier: models.CriterionModifierNotEquals,
},
[]int{},
false,
},
} }
qb := db.Gallery
for _, tt := range tests {
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
studioCriterion := tt.studioCriterion
galleryFilter := models.GalleryFilterType{ galleryFilter := models.GalleryFilterType{
Studios: &studioCriterion, Studios: &studioCriterion,
} }
galleries := queryGallery(ctx, t, sqb, &galleryFilter, nil) var findFilter *models.FindFilterType
if tt.q != "" {
assert.Len(t, galleries, 1) findFilter = &models.FindFilterType{
Q: &tt.q,
// ensure id is correct }
assert.Equal(t, galleryIDs[galleryIdxWithStudio], galleries[0].ID)
studioCriterion = models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(studioIDs[studioIdxWithGallery]),
},
Modifier: models.CriterionModifierExcludes,
} }
q := getGalleryStringValue(galleryIdxWithStudio, titleField) gallerys := queryGallery(ctx, t, qb, &galleryFilter, findFilter)
findFilter := models.FindFilterType{
Q: &q,
}
galleries = queryGallery(ctx, t, sqb, &galleryFilter, &findFilter) assert.ElementsMatch(t, galleriesToIDs(gallerys), tt.expectedIDs)
assert.Len(t, galleries, 0)
return nil
}) })
} }
}
func TestGalleryQueryStudioDepth(t *testing.T) { func TestGalleryQueryStudioDepth(t *testing.T) {
withTxn(func(ctx context.Context) error { withTxn(func(ctx context.Context) error {
@@ -2157,82 +2372,199 @@ func TestGalleryQueryStudioDepth(t *testing.T) {
} }
func TestGalleryQueryPerformerTags(t *testing.T) { func TestGalleryQueryPerformerTags(t *testing.T) {
withTxn(func(ctx context.Context) error { allDepth := -1
sqb := db.Gallery
tagCriterion := models.HierarchicalMultiCriterionInput{ tests := []struct {
name string
findFilter *models.FindFilterType
filter *models.GalleryFilterType
includeIdxs []int
excludeIdxs []int
wantErr bool
}{
{
"includes",
nil,
&models.GalleryFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(tagIDs[tagIdxWithPerformer]), strconv.Itoa(tagIDs[tagIdxWithPerformer]),
strconv.Itoa(tagIDs[tagIdx1WithPerformer]), strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
}, },
Modifier: models.CriterionModifierIncludes, Modifier: models.CriterionModifierIncludes,
} },
},
galleryFilter := models.GalleryFilterType{ []int{
PerformerTags: &tagCriterion, galleryIdxWithPerformerTag,
} galleryIdxWithPerformerTwoTags,
galleryIdxWithTwoPerformerTag,
galleries := queryGallery(ctx, t, sqb, &galleryFilter, nil) },
assert.Len(t, galleries, 2) []int{
galleryIdxWithPerformer,
// ensure ids are correct },
for _, gallery := range galleries { false,
assert.True(t, gallery.ID == galleryIDs[galleryIdxWithPerformerTag] || gallery.ID == galleryIDs[galleryIdxWithPerformerTwoTags]) },
} {
"includes sub-tags",
tagCriterion = models.HierarchicalMultiCriterionInput{ nil,
&models.GalleryFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(tagIDs[tagIdxWithParentAndChild]),
},
Depth: &allDepth,
Modifier: models.CriterionModifierIncludes,
},
},
[]int{
galleryIdxWithPerformerParentTag,
},
[]int{
galleryIdxWithPerformer,
galleryIdxWithPerformerTag,
galleryIdxWithPerformerTwoTags,
galleryIdxWithTwoPerformerTag,
},
false,
},
{
"includes all",
nil,
&models.GalleryFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithPerformer]), strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
strconv.Itoa(tagIDs[tagIdx2WithPerformer]), strconv.Itoa(tagIDs[tagIdx2WithPerformer]),
}, },
Modifier: models.CriterionModifierIncludesAll, Modifier: models.CriterionModifierIncludesAll,
}
galleries = queryGallery(ctx, t, sqb, &galleryFilter, nil)
assert.Len(t, galleries, 1)
assert.Equal(t, galleryIDs[galleryIdxWithPerformerTwoTags], galleries[0].ID)
tagCriterion = models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
}, },
},
[]int{
galleryIdxWithPerformerTwoTags,
},
[]int{
galleryIdxWithPerformer,
galleryIdxWithPerformerTag,
galleryIdxWithTwoPerformerTag,
},
false,
},
{
"excludes performer tag tagIdx2WithPerformer",
nil,
&models.GalleryFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierExcludes, Modifier: models.CriterionModifierExcludes,
} Value: []string{strconv.Itoa(tagIDs[tagIdx2WithPerformer])},
},
q := getGalleryStringValue(galleryIdxWithPerformerTwoTags, titleField) },
findFilter := models.FindFilterType{ nil,
Q: &q, []int{galleryIdxWithTwoPerformerTag},
} false,
},
galleries = queryGallery(ctx, t, sqb, &galleryFilter, &findFilter) {
assert.Len(t, galleries, 0) "excludes sub-tags",
nil,
tagCriterion = models.HierarchicalMultiCriterionInput{ &models.GalleryFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(tagIDs[tagIdxWithParentAndChild]),
},
Depth: &allDepth,
Modifier: models.CriterionModifierExcludes,
},
},
[]int{
galleryIdxWithPerformer,
galleryIdxWithPerformerTag,
galleryIdxWithPerformerTwoTags,
galleryIdxWithTwoPerformerTag,
},
[]int{
galleryIdxWithPerformerParentTag,
},
false,
},
{
"is null",
nil,
&models.GalleryFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIsNull, Modifier: models.CriterionModifierIsNull,
},
},
[]int{galleryIdx1WithImage},
[]int{galleryIdxWithPerformerTag},
false,
},
{
"not null",
nil,
&models.GalleryFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierNotNull,
},
},
[]int{galleryIdxWithPerformerTag},
[]int{galleryIdx1WithImage},
false,
},
{
"equals",
nil,
&models.GalleryFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierEquals,
Value: []string{
strconv.Itoa(tagIDs[tagIdx2WithPerformer]),
},
},
},
nil,
nil,
true,
},
{
"not equals",
nil,
&models.GalleryFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierNotEquals,
Value: []string{
strconv.Itoa(tagIDs[tagIdx2WithPerformer]),
},
},
},
nil,
nil,
true,
},
} }
q = getGalleryStringValue(galleryIdx1WithImage, titleField)
galleries = queryGallery(ctx, t, sqb, &galleryFilter, &findFilter) for _, tt := range tests {
assert.Len(t, galleries, 1) runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
assert.Equal(t, galleryIDs[galleryIdx1WithImage], galleries[0].ID) assert := assert.New(t)
q = getGalleryStringValue(galleryIdxWithPerformerTag, titleField) results, _, err := db.Gallery.Query(ctx, tt.filter, tt.findFilter)
galleries = queryGallery(ctx, t, sqb, &galleryFilter, &findFilter) if (err != nil) != tt.wantErr {
assert.Len(t, galleries, 0) t.Errorf("ImageStore.Query() error = %v, wantErr %v", err, tt.wantErr)
return
}
tagCriterion.Modifier = models.CriterionModifierNotNull ids := galleriesToIDs(results)
galleries = queryGallery(ctx, t, sqb, &galleryFilter, &findFilter) include := indexesToIDs(galleryIDs, tt.includeIdxs)
assert.Len(t, galleries, 1) exclude := indexesToIDs(galleryIDs, tt.excludeIdxs)
assert.Equal(t, galleryIDs[galleryIdxWithPerformerTag], galleries[0].ID)
q = getGalleryStringValue(galleryIdx1WithImage, titleField) for _, i := range include {
galleries = queryGallery(ctx, t, sqb, &galleryFilter, &findFilter) assert.Contains(ids, i)
assert.Len(t, galleries, 0) }
for _, e := range exclude {
return nil assert.NotContains(ids, e)
}
}) })
} }
}
func TestGalleryQueryTagCount(t *testing.T) { func TestGalleryQueryTagCount(t *testing.T) {
const tagCount = 1 const tagCount = 1

View File

@@ -669,7 +669,7 @@ func (qb *ImageStore) makeFilter(ctx context.Context, imageFilter *models.ImageF
query.handleCriterion(ctx, imageGalleriesCriterionHandler(qb, imageFilter.Galleries)) query.handleCriterion(ctx, imageGalleriesCriterionHandler(qb, imageFilter.Galleries))
query.handleCriterion(ctx, imagePerformersCriterionHandler(qb, imageFilter.Performers)) query.handleCriterion(ctx, imagePerformersCriterionHandler(qb, imageFilter.Performers))
query.handleCriterion(ctx, imagePerformerCountCriterionHandler(qb, imageFilter.PerformerCount)) query.handleCriterion(ctx, imagePerformerCountCriterionHandler(qb, imageFilter.PerformerCount))
query.handleCriterion(ctx, imageStudioCriterionHandler(qb, imageFilter.Studios)) query.handleCriterion(ctx, studioCriterionHandler(imageTable, imageFilter.Studios))
query.handleCriterion(ctx, imagePerformerTagsCriterionHandler(qb, imageFilter.PerformerTags)) query.handleCriterion(ctx, imagePerformerTagsCriterionHandler(qb, imageFilter.PerformerTags))
query.handleCriterion(ctx, imagePerformerFavoriteCriterionHandler(imageFilter.PerformerFavorite)) query.handleCriterion(ctx, imagePerformerFavoriteCriterionHandler(imageFilter.PerformerFavorite))
query.handleCriterion(ctx, timestampCriterionHandler(imageFilter.CreatedAt, "images.created_at")) query.handleCriterion(ctx, timestampCriterionHandler(imageFilter.CreatedAt, "images.created_at"))
@@ -946,51 +946,12 @@ GROUP BY performers_images.image_id HAVING SUM(performers.favorite) = 0)`, "nofa
} }
} }
func imageStudioCriterionHandler(qb *ImageStore, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { func imagePerformerTagsCriterionHandler(qb *ImageStore, tags *models.HierarchicalMultiCriterionInput) criterionHandler {
h := hierarchicalMultiCriterionHandlerBuilder{ return &joinedPerformerTagsHandler{
tx: qb.tx, criterion: tags,
primaryTable: imageTable, primaryTable: imageTable,
foreignTable: studioTable, joinTable: performersImagesTable,
foreignFK: studioIDColumn, joinPrimaryKey: imageIDColumn,
parentFK: "parent_id",
}
return h.handler(studios)
}
func imagePerformerTagsCriterionHandler(qb *ImageStore, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if tags != nil {
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
var notClause string
if tags.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addLeftJoin("performers_images", "", "images.id = performers_images.image_id")
f.addLeftJoin("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(ctx, qb.tx, tags.Value, tagTable, "tags_relations", "", tags.Depth)
f.addWith(`performer_tags AS (
SELECT pi.image_id, t.column1 AS root_tag_id FROM performers_images pi
INNER JOIN performers_tags pt ON pt.performer_id = pi.performer_id
INNER JOIN (` + valuesClause + `) t ON t.column2 = pt.tag_id
)`)
f.addLeftJoin("performer_tags", "", "performer_tags.image_id = images.id")
addHierarchicalConditionClauses(f, *tags, "performer_tags", "root_tag_id")
}
} }
} }

View File

@@ -2124,204 +2124,370 @@ func TestImageQueryGallery(t *testing.T) {
} }
func TestImageQueryPerformers(t *testing.T) { func TestImageQueryPerformers(t *testing.T) {
withTxn(func(ctx context.Context) error { tests := []struct {
sqb := db.Image name string
performerCriterion := models.MultiCriterionInput{ filter models.MultiCriterionInput
includeIdxs []int
excludeIdxs []int
wantErr bool
}{
{
"includes",
models.MultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(performerIDs[performerIdxWithImage]), strconv.Itoa(performerIDs[performerIdxWithImage]),
strconv.Itoa(performerIDs[performerIdx1WithImage]), strconv.Itoa(performerIDs[performerIdx1WithImage]),
}, },
Modifier: models.CriterionModifierIncludes, Modifier: models.CriterionModifierIncludes,
} },
[]int{
imageFilter := models.ImageFilterType{ imageIdxWithPerformer,
Performers: &performerCriterion, imageIdxWithTwoPerformers,
} },
[]int{
images := queryImages(ctx, t, sqb, &imageFilter, nil) imageIdxWithGallery,
assert.Len(t, images, 2) },
false,
// ensure ids are correct },
for _, image := range images { {
assert.True(t, image.ID == imageIDs[imageIdxWithPerformer] || image.ID == imageIDs[imageIdxWithTwoPerformers]) "includes all",
} models.MultiCriterionInput{
performerCriterion = models.MultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(performerIDs[performerIdx1WithImage]), strconv.Itoa(performerIDs[performerIdx1WithImage]),
strconv.Itoa(performerIDs[performerIdx2WithImage]), strconv.Itoa(performerIDs[performerIdx2WithImage]),
}, },
Modifier: models.CriterionModifierIncludesAll, Modifier: models.CriterionModifierIncludesAll,
}
images = queryImages(ctx, t, sqb, &imageFilter, nil)
assert.Len(t, images, 1)
assert.Equal(t, imageIDs[imageIdxWithTwoPerformers], images[0].ID)
performerCriterion = models.MultiCriterionInput{
Value: []string{
strconv.Itoa(performerIDs[performerIdx1WithImage]),
}, },
[]int{
imageIdxWithTwoPerformers,
},
[]int{
imageIdxWithPerformer,
},
false,
},
{
"excludes",
models.MultiCriterionInput{
Modifier: models.CriterionModifierExcludes, Modifier: models.CriterionModifierExcludes,
} Value: []string{strconv.Itoa(tagIDs[performerIdx1WithImage])},
},
q := getImageStringValue(imageIdxWithTwoPerformers, titleField) nil,
findFilter := models.FindFilterType{ []int{imageIdxWithTwoPerformers},
Q: &q, false,
} },
{
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter) "is null",
assert.Len(t, images, 0) models.MultiCriterionInput{
performerCriterion = models.MultiCriterionInput{
Modifier: models.CriterionModifierIsNull, Modifier: models.CriterionModifierIsNull,
},
[]int{imageIdxWithTag},
[]int{
imageIdxWithPerformer,
imageIdxWithTwoPerformers,
imageIdxWithPerformerTwoTags,
},
false,
},
{
"not null",
models.MultiCriterionInput{
Modifier: models.CriterionModifierNotNull,
},
[]int{
imageIdxWithPerformer,
imageIdxWithTwoPerformers,
imageIdxWithPerformerTwoTags,
},
[]int{imageIdxWithTag},
false,
},
{
"equals",
models.MultiCriterionInput{
Modifier: models.CriterionModifierEquals,
Value: []string{
strconv.Itoa(tagIDs[performerIdx1WithImage]),
strconv.Itoa(tagIDs[performerIdx2WithImage]),
},
},
[]int{imageIdxWithTwoPerformers},
[]int{
imageIdxWithThreePerformers,
},
false,
},
{
"not equals",
models.MultiCriterionInput{
Modifier: models.CriterionModifierNotEquals,
Value: []string{
strconv.Itoa(tagIDs[performerIdx1WithImage]),
strconv.Itoa(tagIDs[performerIdx2WithImage]),
},
},
nil,
nil,
true,
},
} }
q = getImageStringValue(imageIdxWithGallery, titleField)
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter) for _, tt := range tests {
assert.Len(t, images, 1) runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
assert.Equal(t, imageIDs[imageIdxWithGallery], images[0].ID) assert := assert.New(t)
q = getImageStringValue(imageIdxWithPerformerTag, titleField) results, err := db.Image.Query(ctx, models.ImageQueryOptions{
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter) ImageFilter: &models.ImageFilterType{
assert.Len(t, images, 0) Performers: &tt.filter,
},
performerCriterion.Modifier = models.CriterionModifierNotNull
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 1)
assert.Equal(t, imageIDs[imageIdxWithPerformerTag], images[0].ID)
q = getImageStringValue(imageIdxWithGallery, titleField)
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 0)
return nil
}) })
if (err != nil) != tt.wantErr {
t.Errorf("ImageStore.Query() error = %v, wantErr %v", err, tt.wantErr)
return
}
include := indexesToIDs(imageIDs, tt.includeIdxs)
exclude := indexesToIDs(imageIDs, tt.excludeIdxs)
for _, i := range include {
assert.Contains(results.IDs, i)
}
for _, e := range exclude {
assert.NotContains(results.IDs, e)
}
})
}
} }
func TestImageQueryTags(t *testing.T) { func TestImageQueryTags(t *testing.T) {
withTxn(func(ctx context.Context) error { tests := []struct {
sqb := db.Image name string
tagCriterion := models.HierarchicalMultiCriterionInput{ filter models.HierarchicalMultiCriterionInput
includeIdxs []int
excludeIdxs []int
wantErr bool
}{
{
"includes",
models.HierarchicalMultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(tagIDs[tagIdxWithImage]), strconv.Itoa(tagIDs[tagIdxWithImage]),
strconv.Itoa(tagIDs[tagIdx1WithImage]), strconv.Itoa(tagIDs[tagIdx1WithImage]),
}, },
Modifier: models.CriterionModifierIncludes, Modifier: models.CriterionModifierIncludes,
} },
[]int{
imageFilter := models.ImageFilterType{ imageIdxWithTag,
Tags: &tagCriterion, imageIdxWithTwoTags,
} },
[]int{
images := queryImages(ctx, t, sqb, &imageFilter, nil) imageIdxWithGallery,
assert.Len(t, images, 2) },
false,
// ensure ids are correct },
for _, image := range images { {
assert.True(t, image.ID == imageIDs[imageIdxWithTag] || image.ID == imageIDs[imageIdxWithTwoTags]) "includes all",
} models.HierarchicalMultiCriterionInput{
tagCriterion = models.HierarchicalMultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithImage]), strconv.Itoa(tagIDs[tagIdx1WithImage]),
strconv.Itoa(tagIDs[tagIdx2WithImage]), strconv.Itoa(tagIDs[tagIdx2WithImage]),
}, },
Modifier: models.CriterionModifierIncludesAll, Modifier: models.CriterionModifierIncludesAll,
} },
[]int{
images = queryImages(ctx, t, sqb, &imageFilter, nil) imageIdxWithTwoTags,
assert.Len(t, images, 1) },
assert.Equal(t, imageIDs[imageIdxWithTwoTags], images[0].ID) []int{
imageIdxWithTag,
tagCriterion = models.HierarchicalMultiCriterionInput{ },
false,
},
{
"excludes",
models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierExcludes,
Value: []string{strconv.Itoa(tagIDs[tagIdx1WithImage])},
},
nil,
[]int{imageIdxWithTwoTags},
false,
},
{
"is null",
models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIsNull,
},
[]int{imageIdx1WithPerformer},
[]int{
imageIdxWithTag,
imageIdxWithTwoTags,
imageIdxWithThreeTags,
},
false,
},
{
"not null",
models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierNotNull,
},
[]int{
imageIdxWithTag,
imageIdxWithTwoTags,
imageIdxWithThreeTags,
},
[]int{imageIdx1WithPerformer},
false,
},
{
"equals",
models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierEquals,
Value: []string{ Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithImage]), strconv.Itoa(tagIDs[tagIdx1WithImage]),
strconv.Itoa(tagIDs[tagIdx2WithImage]),
},
},
[]int{imageIdxWithTwoTags},
[]int{
imageIdxWithThreeTags,
},
false,
},
{
"not equals",
models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierNotEquals,
Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithImage]),
strconv.Itoa(tagIDs[tagIdx2WithImage]),
},
},
nil,
nil,
true,
}, },
Modifier: models.CriterionModifierExcludes,
} }
q := getImageStringValue(imageIdxWithTwoTags, titleField) for _, tt := range tests {
findFilter := models.FindFilterType{ runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
Q: &q, assert := assert.New(t)
}
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter) results, err := db.Image.Query(ctx, models.ImageQueryOptions{
assert.Len(t, images, 0) ImageFilter: &models.ImageFilterType{
Tags: &tt.filter,
tagCriterion = models.HierarchicalMultiCriterionInput{ },
Modifier: models.CriterionModifierIsNull,
}
q = getImageStringValue(imageIdxWithGallery, titleField)
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 1)
assert.Equal(t, imageIDs[imageIdxWithGallery], images[0].ID)
q = getImageStringValue(imageIdxWithTag, titleField)
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 0)
tagCriterion.Modifier = models.CriterionModifierNotNull
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 1)
assert.Equal(t, imageIDs[imageIdxWithTag], images[0].ID)
q = getImageStringValue(imageIdxWithGallery, titleField)
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 0)
return nil
}) })
if (err != nil) != tt.wantErr {
t.Errorf("ImageStore.Query() error = %v, wantErr %v", err, tt.wantErr)
return
}
include := indexesToIDs(imageIDs, tt.includeIdxs)
exclude := indexesToIDs(imageIDs, tt.excludeIdxs)
for _, i := range include {
assert.Contains(results.IDs, i)
}
for _, e := range exclude {
assert.NotContains(results.IDs, e)
}
})
}
} }
func TestImageQueryStudio(t *testing.T) { func TestImageQueryStudio(t *testing.T) {
withTxn(func(ctx context.Context) error { tests := []struct {
sqb := db.Image name string
studioCriterion := models.HierarchicalMultiCriterionInput{ q string
studioCriterion models.HierarchicalMultiCriterionInput
expectedIDs []int
wantErr bool
}{
{
"includes",
"",
models.HierarchicalMultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(studioIDs[studioIdxWithImage]), strconv.Itoa(studioIDs[studioIdxWithImage]),
}, },
Modifier: models.CriterionModifierIncludes, Modifier: models.CriterionModifierIncludes,
},
[]int{imageIDs[imageIdxWithStudio]},
false,
},
{
"excludes",
getImageStringValue(imageIdxWithStudio, titleField),
models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(studioIDs[studioIdxWithImage]),
},
Modifier: models.CriterionModifierExcludes,
},
[]int{},
false,
},
{
"excludes includes null",
getImageStringValue(imageIdxWithGallery, titleField),
models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(studioIDs[studioIdxWithImage]),
},
Modifier: models.CriterionModifierExcludes,
},
[]int{imageIDs[imageIdxWithGallery]},
false,
},
{
"equals",
"",
models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(studioIDs[studioIdxWithImage]),
},
Modifier: models.CriterionModifierEquals,
},
[]int{imageIDs[imageIdxWithStudio]},
false,
},
{
"not equals",
getImageStringValue(imageIdxWithStudio, titleField),
models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(studioIDs[studioIdxWithImage]),
},
Modifier: models.CriterionModifierNotEquals,
},
[]int{},
false,
},
} }
qb := db.Image
for _, tt := range tests {
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
studioCriterion := tt.studioCriterion
imageFilter := models.ImageFilterType{ imageFilter := models.ImageFilterType{
Studios: &studioCriterion, Studios: &studioCriterion,
} }
images, _, err := queryImagesWithCount(ctx, sqb, &imageFilter, nil) var findFilter *models.FindFilterType
if err != nil { if tt.q != "" {
t.Errorf("Error querying image: %s", err.Error()) findFilter = &models.FindFilterType{
Q: &tt.q,
}
} }
assert.Len(t, images, 1) images := queryImages(ctx, t, qb, &imageFilter, findFilter)
// ensure id is correct assert.ElementsMatch(t, imagesToIDs(images), tt.expectedIDs)
assert.Equal(t, imageIDs[imageIdxWithStudio], images[0].ID)
studioCriterion = models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(studioIDs[studioIdxWithImage]),
},
Modifier: models.CriterionModifierExcludes,
}
q := getImageStringValue(imageIdxWithStudio, titleField)
findFilter := models.FindFilterType{
Q: &q,
}
images, _, err = queryImagesWithCount(ctx, sqb, &imageFilter, &findFilter)
if err != nil {
t.Errorf("Error querying image: %s", err.Error())
}
assert.Len(t, images, 0)
return nil
}) })
} }
}
func TestImageQueryStudioDepth(t *testing.T) { func TestImageQueryStudioDepth(t *testing.T) {
withTxn(func(ctx context.Context) error { withTxn(func(ctx context.Context) error {
@@ -2394,81 +2560,201 @@ func queryImages(ctx context.Context, t *testing.T, sqb models.ImageReader, imag
} }
func TestImageQueryPerformerTags(t *testing.T) { func TestImageQueryPerformerTags(t *testing.T) {
withTxn(func(ctx context.Context) error { allDepth := -1
sqb := db.Image
tagCriterion := models.HierarchicalMultiCriterionInput{ tests := []struct {
name string
findFilter *models.FindFilterType
filter *models.ImageFilterType
includeIdxs []int
excludeIdxs []int
wantErr bool
}{
{
"includes",
nil,
&models.ImageFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(tagIDs[tagIdxWithPerformer]), strconv.Itoa(tagIDs[tagIdxWithPerformer]),
strconv.Itoa(tagIDs[tagIdx1WithPerformer]), strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
}, },
Modifier: models.CriterionModifierIncludes, Modifier: models.CriterionModifierIncludes,
} },
},
imageFilter := models.ImageFilterType{ []int{
PerformerTags: &tagCriterion, imageIdxWithPerformerTag,
} imageIdxWithPerformerTwoTags,
imageIdxWithTwoPerformerTag,
images := queryImages(ctx, t, sqb, &imageFilter, nil) },
assert.Len(t, images, 2) []int{
imageIdxWithPerformer,
// ensure ids are correct },
for _, image := range images { false,
assert.True(t, image.ID == imageIDs[imageIdxWithPerformerTag] || image.ID == imageIDs[imageIdxWithPerformerTwoTags]) },
} {
"includes sub-tags",
tagCriterion = models.HierarchicalMultiCriterionInput{ nil,
&models.ImageFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(tagIDs[tagIdxWithParentAndChild]),
},
Depth: &allDepth,
Modifier: models.CriterionModifierIncludes,
},
},
[]int{
imageIdxWithPerformerParentTag,
},
[]int{
imageIdxWithPerformer,
imageIdxWithPerformerTag,
imageIdxWithPerformerTwoTags,
imageIdxWithTwoPerformerTag,
},
false,
},
{
"includes all",
nil,
&models.ImageFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithPerformer]), strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
strconv.Itoa(tagIDs[tagIdx2WithPerformer]), strconv.Itoa(tagIDs[tagIdx2WithPerformer]),
}, },
Modifier: models.CriterionModifierIncludesAll, Modifier: models.CriterionModifierIncludesAll,
}
images = queryImages(ctx, t, sqb, &imageFilter, nil)
assert.Len(t, images, 1)
assert.Equal(t, imageIDs[imageIdxWithPerformerTwoTags], images[0].ID)
tagCriterion = models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
}, },
},
[]int{
imageIdxWithPerformerTwoTags,
},
[]int{
imageIdxWithPerformer,
imageIdxWithPerformerTag,
imageIdxWithTwoPerformerTag,
},
false,
},
{
"excludes performer tag tagIdx2WithPerformer",
nil,
&models.ImageFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierExcludes, Modifier: models.CriterionModifierExcludes,
} Value: []string{strconv.Itoa(tagIDs[tagIdx2WithPerformer])},
},
q := getImageStringValue(imageIdxWithPerformerTwoTags, titleField) },
findFilter := models.FindFilterType{ nil,
Q: &q, []int{imageIdxWithTwoPerformerTag},
} false,
},
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter) {
assert.Len(t, images, 0) "excludes sub-tags",
nil,
tagCriterion = models.HierarchicalMultiCriterionInput{ &models.ImageFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(tagIDs[tagIdxWithParentAndChild]),
},
Depth: &allDepth,
Modifier: models.CriterionModifierExcludes,
},
},
[]int{
imageIdxWithPerformer,
imageIdxWithPerformerTag,
imageIdxWithPerformerTwoTags,
imageIdxWithTwoPerformerTag,
},
[]int{
imageIdxWithPerformerParentTag,
},
false,
},
{
"is null",
nil,
&models.ImageFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIsNull, Modifier: models.CriterionModifierIsNull,
},
},
[]int{imageIdxWithGallery},
[]int{imageIdxWithPerformerTag},
false,
},
{
"not null",
nil,
&models.ImageFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierNotNull,
},
},
[]int{imageIdxWithPerformerTag},
[]int{imageIdxWithGallery},
false,
},
{
"equals",
nil,
&models.ImageFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierEquals,
Value: []string{
strconv.Itoa(tagIDs[tagIdx2WithPerformer]),
},
},
},
nil,
nil,
true,
},
{
"not equals",
nil,
&models.ImageFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierNotEquals,
Value: []string{
strconv.Itoa(tagIDs[tagIdx2WithPerformer]),
},
},
},
nil,
nil,
true,
},
} }
q = getImageStringValue(imageIdxWithGallery, titleField)
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter) for _, tt := range tests {
assert.Len(t, images, 1) runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
assert.Equal(t, imageIDs[imageIdxWithGallery], images[0].ID) assert := assert.New(t)
q = getImageStringValue(imageIdxWithPerformerTag, titleField) results, err := db.Image.Query(ctx, models.ImageQueryOptions{
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter) ImageFilter: tt.filter,
assert.Len(t, images, 0) QueryOptions: models.QueryOptions{
FindFilter: tt.findFilter,
tagCriterion.Modifier = models.CriterionModifierNotNull },
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 1)
assert.Equal(t, imageIDs[imageIdxWithPerformerTag], images[0].ID)
q = getImageStringValue(imageIdxWithGallery, titleField)
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter)
assert.Len(t, images, 0)
return nil
}) })
if (err != nil) != tt.wantErr {
t.Errorf("ImageStore.Query() error = %v, wantErr %v", err, tt.wantErr)
return
}
include := indexesToIDs(imageIDs, tt.includeIdxs)
exclude := indexesToIDs(imageIDs, tt.excludeIdxs)
for _, i := range include {
assert.Contains(results.IDs, i)
}
for _, e := range exclude {
assert.NotContains(results.IDs, e)
}
})
}
} }
func TestImageQueryTagCount(t *testing.T) { func TestImageQueryTagCount(t *testing.T) {
@@ -2587,7 +2873,7 @@ func TestImageQuerySorting(t *testing.T) {
"date", "date",
models.SortDirectionEnumDesc, models.SortDirectionEnumDesc,
imageIdxWithTwoGalleries, imageIdxWithTwoGalleries,
imageIdxWithGrandChildStudio, imageIdxWithPerformerParentTag,
}, },
} }

View File

@@ -176,7 +176,7 @@ func (qb *movieQueryBuilder) makeFilter(ctx context.Context, movieFilter *models
query.handleCriterion(ctx, floatIntCriterionHandler(movieFilter.Duration, "movies.duration", nil)) query.handleCriterion(ctx, floatIntCriterionHandler(movieFilter.Duration, "movies.duration", nil))
query.handleCriterion(ctx, movieIsMissingCriterionHandler(qb, movieFilter.IsMissing)) query.handleCriterion(ctx, movieIsMissingCriterionHandler(qb, movieFilter.IsMissing))
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.URL, "movies.url")) query.handleCriterion(ctx, stringCriterionHandler(movieFilter.URL, "movies.url"))
query.handleCriterion(ctx, movieStudioCriterionHandler(qb, movieFilter.Studios)) query.handleCriterion(ctx, studioCriterionHandler(movieTable, movieFilter.Studios))
query.handleCriterion(ctx, moviePerformersCriterionHandler(qb, movieFilter.Performers)) query.handleCriterion(ctx, moviePerformersCriterionHandler(qb, movieFilter.Performers))
query.handleCriterion(ctx, dateCriterionHandler(movieFilter.Date, "movies.date")) query.handleCriterion(ctx, dateCriterionHandler(movieFilter.Date, "movies.date"))
query.handleCriterion(ctx, timestampCriterionHandler(movieFilter.CreatedAt, "movies.created_at")) query.handleCriterion(ctx, timestampCriterionHandler(movieFilter.CreatedAt, "movies.created_at"))
@@ -239,19 +239,6 @@ func movieIsMissingCriterionHandler(qb *movieQueryBuilder, isMissing *string) cr
} }
} }
func movieStudioCriterionHandler(qb *movieQueryBuilder, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
h := hierarchicalMultiCriterionHandlerBuilder{
tx: qb.tx,
primaryTable: movieTable,
foreignTable: studioTable,
foreignFK: studioIDColumn,
parentFK: "parent_id",
}
return h.handler(studios)
}
func moviePerformersCriterionHandler(qb *movieQueryBuilder, performers *models.MultiCriterionInput) criterionHandlerFunc { func moviePerformersCriterionHandler(qb *movieQueryBuilder, performers *models.MultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) { return func(ctx context.Context, f *filterBuilder) {
if performers != nil { if performers != nil {

View File

@@ -908,7 +908,11 @@ func performerStudiosCriterionHandler(qb *PerformerStore, studios *models.Hierar
} }
const derivedPerformerStudioTable = "performer_studio" const derivedPerformerStudioTable = "performer_studio"
valuesClause := getHierarchicalValues(ctx, qb.tx, studios.Value, studioTable, "", "parent_id", studios.Depth) valuesClause, err := getHierarchicalValues(ctx, qb.tx, studios.Value, studioTable, "", "parent_id", "child_id", studios.Depth)
if err != nil {
f.setError(err)
return
}
f.addWith("studio(root_id, item_id) AS (" + valuesClause + ")") f.addWith("studio(root_id, item_id) AS (" + valuesClause + ")")
templStr := `SELECT performer_id FROM {primaryTable} templStr := `SELECT performer_id FROM {primaryTable}

View File

@@ -515,10 +515,11 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
models.Performer{ models.Performer{
ID: performerIDs[performerIdxWithTwoTags], ID: performerIDs[performerIdxWithTwoTags],
Name: getPerformerStringValue(performerIdxWithTwoTags, "Name"), Name: getPerformerStringValue(performerIdxWithTwoTags, "Name"),
Favorite: true, Favorite: getPerformerBoolValue(performerIdxWithTwoTags),
Aliases: models.NewRelatedStrings([]string{}), Aliases: models.NewRelatedStrings([]string{}),
TagIDs: models.NewRelatedIDs([]int{}), TagIDs: models.NewRelatedIDs([]int{}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{}), StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
IgnoreAutoTag: getIgnoreAutoTag(performerIdxWithTwoTags),
}, },
false, false,
}, },
@@ -1904,10 +1905,10 @@ func TestPerformerQuerySortScenesCount(t *testing.T) {
assert.True(t, len(performers) > 0) assert.True(t, len(performers) > 0)
// first performer should be performerIdxWithTwoScenes // first performer should be performerIdx1WithScene
firstPerformer := performers[0] firstPerformer := performers[0]
assert.Equal(t, performerIDs[performerIdxWithTwoScenes], firstPerformer.ID) assert.Equal(t, performerIDs[performerIdx1WithScene], firstPerformer.ID)
// sort in ascending order // sort in ascending order
direction = models.SortDirectionEnumAsc direction = models.SortDirectionEnumAsc
@@ -1920,7 +1921,7 @@ func TestPerformerQuerySortScenesCount(t *testing.T) {
assert.True(t, len(performers) > 0) assert.True(t, len(performers) > 0)
lastPerformer := performers[len(performers)-1] lastPerformer := performers[len(performers)-1]
assert.Equal(t, performerIDs[performerIdxWithTwoScenes], lastPerformer.ID) assert.Equal(t, performerIDs[performerIdxWithTag], lastPerformer.ID)
return nil return nil
}) })
@@ -2060,7 +2061,7 @@ func TestPerformerStore_FindByStashIDStatus(t *testing.T) {
name: "!hasStashID", name: "!hasStashID",
hasStashID: false, hasStashID: false,
stashboxEndpoint: getPerformerStringValue(performerIdxWithScene, "endpoint"), stashboxEndpoint: getPerformerStringValue(performerIdxWithScene, "endpoint"),
include: []int{performerIdxWithImage}, include: []int{performerIdxWithTwoScenes},
exclude: []int{performerIdx2WithScene}, exclude: []int{performerIdx2WithScene},
wantErr: false, wantErr: false,
}, },

View File

@@ -959,7 +959,7 @@ func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneF
query.handleCriterion(ctx, sceneTagCountCriterionHandler(qb, sceneFilter.TagCount)) query.handleCriterion(ctx, sceneTagCountCriterionHandler(qb, sceneFilter.TagCount))
query.handleCriterion(ctx, scenePerformersCriterionHandler(qb, sceneFilter.Performers)) query.handleCriterion(ctx, scenePerformersCriterionHandler(qb, sceneFilter.Performers))
query.handleCriterion(ctx, scenePerformerCountCriterionHandler(qb, sceneFilter.PerformerCount)) query.handleCriterion(ctx, scenePerformerCountCriterionHandler(qb, sceneFilter.PerformerCount))
query.handleCriterion(ctx, sceneStudioCriterionHandler(qb, sceneFilter.Studios)) query.handleCriterion(ctx, studioCriterionHandler(sceneTable, sceneFilter.Studios))
query.handleCriterion(ctx, sceneMoviesCriterionHandler(qb, sceneFilter.Movies)) query.handleCriterion(ctx, sceneMoviesCriterionHandler(qb, sceneFilter.Movies))
query.handleCriterion(ctx, scenePerformerTagsCriterionHandler(qb, sceneFilter.PerformerTags)) query.handleCriterion(ctx, scenePerformerTagsCriterionHandler(qb, sceneFilter.PerformerTags))
query.handleCriterion(ctx, scenePerformerFavoriteCriterionHandler(sceneFilter.PerformerFavorite)) query.handleCriterion(ctx, scenePerformerFavoriteCriterionHandler(sceneFilter.PerformerFavorite))
@@ -1352,19 +1352,6 @@ func scenePerformerAgeCriterionHandler(performerAge *models.IntCriterionInput) c
} }
} }
func sceneStudioCriterionHandler(qb *SceneStore, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
h := hierarchicalMultiCriterionHandlerBuilder{
tx: qb.tx,
primaryTable: sceneTable,
foreignTable: studioTable,
foreignFK: studioIDColumn,
parentFK: "parent_id",
}
return h.handler(studios)
}
func sceneMoviesCriterionHandler(qb *SceneStore, movies *models.MultiCriterionInput) criterionHandlerFunc { func sceneMoviesCriterionHandler(qb *SceneStore, movies *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) { addJoinsFunc := func(f *filterBuilder) {
qb.moviesRepository().join(f, "", "scenes.id") qb.moviesRepository().join(f, "", "scenes.id")
@@ -1374,38 +1361,12 @@ func sceneMoviesCriterionHandler(qb *SceneStore, movies *models.MultiCriterionIn
return h.handler(movies) return h.handler(movies)
} }
func scenePerformerTagsCriterionHandler(qb *SceneStore, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { func scenePerformerTagsCriterionHandler(qb *SceneStore, tags *models.HierarchicalMultiCriterionInput) criterionHandler {
return func(ctx context.Context, f *filterBuilder) { return &joinedPerformerTagsHandler{
if tags != nil { criterion: tags,
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull { primaryTable: sceneTable,
var notClause string joinTable: performersScenesTable,
if tags.Modifier == models.CriterionModifierNotNull { joinPrimaryKey: sceneIDColumn,
notClause = "NOT"
}
f.addLeftJoin("performers_scenes", "", "scenes.id = performers_scenes.scene_id")
f.addLeftJoin("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(ctx, qb.tx, tags.Value, tagTable, "tags_relations", "", tags.Depth)
f.addWith(`performer_tags AS (
SELECT ps.scene_id, t.column1 AS root_tag_id FROM performers_scenes ps
INNER JOIN performers_tags pt ON pt.performer_id = ps.performer_id
INNER JOIN (` + valuesClause + `) t ON t.column2 = pt.tag_id
)`)
f.addLeftJoin("performer_tags", "", "performer_tags.scene_id = scenes.id")
addHierarchicalConditionClauses(f, *tags, "performer_tags", "root_tag_id")
}
} }
} }

View File

@@ -209,7 +209,11 @@ func sceneMarkerTagsCriterionHandler(qb *sceneMarkerQueryBuilder, tags *models.H
if len(tags.Value) == 0 { if len(tags.Value) == 0 {
return return
} }
valuesClause := getHierarchicalValues(ctx, qb.tx, tags.Value, tagTable, "tags_relations", "", tags.Depth) valuesClause, err := getHierarchicalValues(ctx, qb.tx, tags.Value, tagTable, "tags_relations", "parent_id", "child_id", tags.Depth)
if err != nil {
f.setError(err)
return
}
f.addWith(`marker_tags AS ( f.addWith(`marker_tags AS (
SELECT mt.scene_marker_id, t.column1 AS root_tag_id FROM scene_markers_tags mt SELECT mt.scene_marker_id, t.column1 AS root_tag_id FROM scene_markers_tags mt
@@ -229,32 +233,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(ctx context.Context, f *filterBuilder) { return func(ctx context.Context, f *filterBuilder) {
if tags != nil { if tags != nil {
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
var notClause string
if tags.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addLeftJoin("scenes_tags", "", "scene_markers.scene_id = scenes_tags.scene_id") f.addLeftJoin("scenes_tags", "", "scene_markers.scene_id = scenes_tags.scene_id")
f.addWhere(fmt.Sprintf("scenes_tags.tag_id IS %s NULL", notClause)) h := joinedHierarchicalMultiCriterionHandlerBuilder{
return tx: qb.tx,
primaryTable: "scene_markers",
primaryKey: sceneIDColumn,
foreignTable: tagTable,
foreignFK: tagIDColumn,
relationsTable: "tags_relations",
joinTable: "scenes_tags",
joinAs: "marker_scenes_tags",
primaryFK: sceneIDColumn,
} }
if len(tags.Value) == 0 { h.handler(tags).handle(ctx, f)
return
}
valuesClause := getHierarchicalValues(ctx, qb.tx, tags.Value, tagTable, "tags_relations", "", tags.Depth)
f.addWith(`scene_tags AS (
SELECT st.scene_id, t.column1 AS root_tag_id FROM scenes_tags st
INNER JOIN (` + valuesClause + `) t ON t.column2 = st.tag_id
)`)
f.addLeftJoin("scene_tags", "", "scene_tags.scene_id = scene_markers.scene_id")
addHierarchicalConditionClauses(f, *tags, "scene_tags", "root_tag_id")
} }
} }
} }

View File

@@ -5,9 +5,12 @@ package sqlite_test
import ( import (
"context" "context"
"strconv"
"testing" "testing"
"github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
"github.com/stashapp/stash/pkg/sqlite" "github.com/stashapp/stash/pkg/sqlite"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -50,7 +53,7 @@ func TestMarkerCountByTagID(t *testing.T) {
t.Errorf("error calling CountByTagID: %s", err.Error()) t.Errorf("error calling CountByTagID: %s", err.Error())
} }
assert.Equal(t, 3, markerCount) assert.Equal(t, 4, markerCount)
markerCount, err = mqb.CountByTagID(ctx, tagIDs[tagIdxWithMarkers]) markerCount, err = mqb.CountByTagID(ctx, tagIDs[tagIdxWithMarkers])
@@ -151,7 +154,7 @@ func TestMarkerQuerySceneTags(t *testing.T) {
} }
withTxn(func(ctx context.Context) error { withTxn(func(ctx context.Context) error {
testTags := func(m *models.SceneMarker, markerFilter *models.SceneMarkerFilterType) { testTags := func(t *testing.T, m *models.SceneMarker, markerFilter *models.SceneMarkerFilterType) {
s, err := db.Scene.Find(ctx, int(m.SceneID.Int64)) s, err := db.Scene.Find(ctx, int(m.SceneID.Int64))
if err != nil { if err != nil {
t.Errorf("error getting marker tag ids: %v", err) t.Errorf("error getting marker tag ids: %v", err)
@@ -164,12 +167,41 @@ func TestMarkerQuerySceneTags(t *testing.T) {
} }
tagIDs := s.TagIDs.List() tagIDs := s.TagIDs.List()
if markerFilter.SceneTags.Modifier == models.CriterionModifierIsNull && len(tagIDs) > 0 { values, _ := stringslice.StringSliceToIntSlice(markerFilter.SceneTags.Value)
switch markerFilter.SceneTags.Modifier {
case models.CriterionModifierIsNull:
if len(tagIDs) > 0 {
t.Errorf("expected marker %d to have no scene tags - found %d", m.ID, len(tagIDs)) 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 { case models.CriterionModifierNotNull:
if len(tagIDs) == 0 {
t.Errorf("expected marker %d to have scene tags - found 0", m.ID) t.Errorf("expected marker %d to have scene tags - found 0", m.ID)
} }
case models.CriterionModifierIncludes:
for _, v := range values {
assert.Contains(t, tagIDs, v)
}
case models.CriterionModifierExcludes:
for _, v := range values {
assert.NotContains(t, tagIDs, v)
}
case models.CriterionModifierEquals:
for _, v := range values {
assert.Contains(t, tagIDs, v)
}
assert.Len(t, tagIDs, len(values))
case models.CriterionModifierNotEquals:
foundAll := true
for _, v := range values {
if !intslice.IntInclude(tagIDs, v) {
foundAll = false
break
}
}
if foundAll && len(tagIDs) == len(values) {
t.Errorf("expected marker %d to have scene tags not equal to %v - found %v", m.ID, values, tagIDs)
}
}
} }
cases := []test{ cases := []test{
@@ -191,6 +223,70 @@ func TestMarkerQuerySceneTags(t *testing.T) {
}, },
nil, nil,
}, },
{
"includes",
&models.SceneMarkerFilterType{
SceneTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIncludes,
Value: []string{
strconv.Itoa(tagIDs[tagIdx3WithScene]),
},
},
},
nil,
},
{
"includes all",
&models.SceneMarkerFilterType{
SceneTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIncludesAll,
Value: []string{
strconv.Itoa(tagIDs[tagIdx2WithScene]),
strconv.Itoa(tagIDs[tagIdx3WithScene]),
},
},
},
nil,
},
{
"equals",
&models.SceneMarkerFilterType{
SceneTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierEquals,
Value: []string{
strconv.Itoa(tagIDs[tagIdx2WithScene]),
strconv.Itoa(tagIDs[tagIdx3WithScene]),
},
},
},
nil,
},
// not equals not supported
// {
// "not equals",
// &models.SceneMarkerFilterType{
// SceneTags: &models.HierarchicalMultiCriterionInput{
// Modifier: models.CriterionModifierNotEquals,
// Value: []string{
// strconv.Itoa(tagIDs[tagIdx2WithScene]),
// strconv.Itoa(tagIDs[tagIdx3WithScene]),
// },
// },
// },
// nil,
// },
{
"excludes",
&models.SceneMarkerFilterType{
SceneTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIncludes,
Value: []string{
strconv.Itoa(tagIDs[tagIdx2WithScene]),
},
},
},
nil,
},
} }
for _, tc := range cases { for _, tc := range cases {
@@ -198,7 +294,7 @@ func TestMarkerQuerySceneTags(t *testing.T) {
markers := queryMarkers(ctx, t, sqlite.SceneMarkerReaderWriter, tc.markerFilter, tc.findFilter) markers := queryMarkers(ctx, t, sqlite.SceneMarkerReaderWriter, tc.markerFilter, tc.findFilter)
assert.Greater(t, len(markers), 0) assert.Greater(t, len(markers), 0)
for _, m := range markers { for _, m := range markers {
testTags(m, tc.markerFilter) testTags(t, m, tc.markerFilter)
} }
}) })
} }

View File

@@ -669,6 +669,7 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
clearScenePartial(), clearScenePartial(),
models.Scene{ models.Scene{
ID: sceneIDs[sceneIdxWithSpacedName], ID: sceneIDs[sceneIdxWithSpacedName],
OCounter: getOCounter(sceneIdxWithSpacedName),
Files: models.NewRelatedVideoFiles([]*file.VideoFile{ Files: models.NewRelatedVideoFiles([]*file.VideoFile{
makeSceneFile(sceneIdxWithSpacedName), makeSceneFile(sceneIdxWithSpacedName),
}), }),
@@ -677,6 +678,10 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
PerformerIDs: models.NewRelatedIDs([]int{}), PerformerIDs: models.NewRelatedIDs([]int{}),
Movies: models.NewRelatedMovies([]models.MoviesScenes{}), Movies: models.NewRelatedMovies([]models.MoviesScenes{}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{}), StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
PlayCount: getScenePlayCount(sceneIdxWithSpacedName),
PlayDuration: getScenePlayDuration(sceneIdxWithSpacedName),
LastPlayedAt: getSceneLastPlayed(sceneIdxWithSpacedName),
ResumeTime: getSceneResumeTime(sceneIdxWithSpacedName),
}, },
false, false,
}, },
@@ -2101,6 +2106,8 @@ func sceneQueryQ(ctx context.Context, t *testing.T, sqb models.SceneReader, q st
// no Q should return all results // no Q should return all results
filter.Q = nil filter.Q = nil
pp := totalScenes
filter.PerPage = &pp
scenes = queryScene(ctx, t, sqb, nil, &filter) scenes = queryScene(ctx, t, sqb, nil, &filter)
assert.Len(t, scenes, totalScenes) assert.Len(t, scenes, totalScenes)
@@ -2230,8 +2237,8 @@ func TestSceneQuery(t *testing.T) {
return return
} }
include := indexesToIDs(performerIDs, tt.includeIdxs) include := indexesToIDs(sceneIDs, tt.includeIdxs)
exclude := indexesToIDs(performerIDs, tt.excludeIdxs) exclude := indexesToIDs(sceneIDs, tt.excludeIdxs)
for _, i := range include { for _, i := range include {
assert.Contains(results.IDs, i) assert.Contains(results.IDs, i)
@@ -3057,7 +3064,13 @@ func queryScenes(ctx context.Context, t *testing.T, queryBuilder models.SceneRea
}, },
} }
return queryScene(ctx, t, queryBuilder, &sceneFilter, nil) // needed so that we don't hit the default limit of 25 scenes
pp := 1000
findFilter := &models.FindFilterType{
PerPage: &pp,
}
return queryScene(ctx, t, queryBuilder, &sceneFilter, findFilter)
} }
func createScene(ctx context.Context, width int, height int) (*models.Scene, error) { func createScene(ctx context.Context, width int, height int) (*models.Scene, error) {
@@ -3329,192 +3342,473 @@ func TestSceneQueryIsMissingPhash(t *testing.T) {
} }
func TestSceneQueryPerformers(t *testing.T) { func TestSceneQueryPerformers(t *testing.T) {
withTxn(func(ctx context.Context) error { tests := []struct {
sqb := db.Scene name string
performerCriterion := models.MultiCriterionInput{ filter models.MultiCriterionInput
includeIdxs []int
excludeIdxs []int
wantErr bool
}{
{
"includes",
models.MultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(performerIDs[performerIdxWithScene]), strconv.Itoa(performerIDs[performerIdxWithScene]),
strconv.Itoa(performerIDs[performerIdx1WithScene]), strconv.Itoa(performerIDs[performerIdx1WithScene]),
}, },
Modifier: models.CriterionModifierIncludes, Modifier: models.CriterionModifierIncludes,
} },
[]int{
sceneFilter := models.SceneFilterType{ sceneIdxWithPerformer,
Performers: &performerCriterion, sceneIdxWithTwoPerformers,
} },
[]int{
scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) sceneIdxWithGallery,
},
assert.Len(t, scenes, 2) false,
},
// ensure ids are correct {
for _, scene := range scenes { "includes all",
assert.True(t, scene.ID == sceneIDs[sceneIdxWithPerformer] || scene.ID == sceneIDs[sceneIdxWithTwoPerformers]) models.MultiCriterionInput{
}
performerCriterion = models.MultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(performerIDs[performerIdx1WithScene]), strconv.Itoa(performerIDs[performerIdx1WithScene]),
strconv.Itoa(performerIDs[performerIdx2WithScene]), strconv.Itoa(performerIDs[performerIdx2WithScene]),
}, },
Modifier: models.CriterionModifierIncludesAll, Modifier: models.CriterionModifierIncludesAll,
}
scenes = queryScene(ctx, t, sqb, &sceneFilter, nil)
assert.Len(t, scenes, 1)
assert.Equal(t, sceneIDs[sceneIdxWithTwoPerformers], scenes[0].ID)
performerCriterion = models.MultiCriterionInput{
Value: []string{
strconv.Itoa(performerIDs[performerIdx1WithScene]),
}, },
[]int{
sceneIdxWithTwoPerformers,
},
[]int{
sceneIdxWithPerformer,
},
false,
},
{
"excludes",
models.MultiCriterionInput{
Modifier: models.CriterionModifierExcludes, Modifier: models.CriterionModifierExcludes,
Value: []string{strconv.Itoa(tagIDs[performerIdx1WithScene])},
},
nil,
[]int{sceneIdxWithTwoPerformers},
false,
},
{
"is null",
models.MultiCriterionInput{
Modifier: models.CriterionModifierIsNull,
},
[]int{sceneIdxWithTag},
[]int{
sceneIdxWithPerformer,
sceneIdxWithTwoPerformers,
sceneIdxWithPerformerTwoTags,
},
false,
},
{
"not null",
models.MultiCriterionInput{
Modifier: models.CriterionModifierNotNull,
},
[]int{
sceneIdxWithPerformer,
sceneIdxWithTwoPerformers,
sceneIdxWithPerformerTwoTags,
},
[]int{sceneIdxWithTag},
false,
},
{
"equals",
models.MultiCriterionInput{
Modifier: models.CriterionModifierEquals,
Value: []string{
strconv.Itoa(tagIDs[performerIdx1WithScene]),
strconv.Itoa(tagIDs[performerIdx2WithScene]),
},
},
[]int{sceneIdxWithTwoPerformers},
[]int{
sceneIdxWithThreePerformers,
},
false,
},
{
"not equals",
models.MultiCriterionInput{
Modifier: models.CriterionModifierNotEquals,
Value: []string{
strconv.Itoa(tagIDs[performerIdx1WithScene]),
strconv.Itoa(tagIDs[performerIdx2WithScene]),
},
},
nil,
nil,
true,
},
} }
q := getSceneStringValue(sceneIdxWithTwoPerformers, titleField) for _, tt := range tests {
findFilter := models.FindFilterType{ runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
Q: &q, assert := assert.New(t)
}
scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter) results, err := db.Scene.Query(ctx, models.SceneQueryOptions{
assert.Len(t, scenes, 0) SceneFilter: &models.SceneFilterType{
Performers: &tt.filter,
return nil },
}) })
if (err != nil) != tt.wantErr {
t.Errorf("SceneStore.Query() error = %v, wantErr %v", err, tt.wantErr)
return
}
include := indexesToIDs(sceneIDs, tt.includeIdxs)
exclude := indexesToIDs(sceneIDs, tt.excludeIdxs)
for _, i := range include {
assert.Contains(results.IDs, i)
}
for _, e := range exclude {
assert.NotContains(results.IDs, e)
}
})
}
} }
func TestSceneQueryTags(t *testing.T) { func TestSceneQueryTags(t *testing.T) {
withTxn(func(ctx context.Context) error { tests := []struct {
sqb := db.Scene name string
tagCriterion := models.HierarchicalMultiCriterionInput{ filter models.HierarchicalMultiCriterionInput
includeIdxs []int
excludeIdxs []int
wantErr bool
}{
{
"includes",
models.HierarchicalMultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(tagIDs[tagIdxWithScene]), strconv.Itoa(tagIDs[tagIdxWithScene]),
strconv.Itoa(tagIDs[tagIdx1WithScene]), strconv.Itoa(tagIDs[tagIdx1WithScene]),
}, },
Modifier: models.CriterionModifierIncludes, Modifier: models.CriterionModifierIncludes,
} },
[]int{
sceneFilter := models.SceneFilterType{ sceneIdxWithTag,
Tags: &tagCriterion, sceneIdxWithTwoTags,
} },
[]int{
scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) sceneIdxWithGallery,
assert.Len(t, scenes, 2) },
false,
// ensure ids are correct },
for _, scene := range scenes { {
assert.True(t, scene.ID == sceneIDs[sceneIdxWithTag] || scene.ID == sceneIDs[sceneIdxWithTwoTags]) "includes all",
} models.HierarchicalMultiCriterionInput{
tagCriterion = models.HierarchicalMultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithScene]), strconv.Itoa(tagIDs[tagIdx1WithScene]),
strconv.Itoa(tagIDs[tagIdx2WithScene]), strconv.Itoa(tagIDs[tagIdx2WithScene]),
}, },
Modifier: models.CriterionModifierIncludesAll, Modifier: models.CriterionModifierIncludesAll,
} },
[]int{
scenes = queryScene(ctx, t, sqb, &sceneFilter, nil) sceneIdxWithTwoTags,
},
assert.Len(t, scenes, 1) []int{
assert.Equal(t, sceneIDs[sceneIdxWithTwoTags], scenes[0].ID) sceneIdxWithTag,
},
tagCriterion = models.HierarchicalMultiCriterionInput{ false,
},
{
"excludes",
models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierExcludes,
Value: []string{strconv.Itoa(tagIDs[tagIdx1WithScene])},
},
nil,
[]int{sceneIdxWithTwoTags},
false,
},
{
"is null",
models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIsNull,
},
[]int{sceneIdx1WithPerformer},
[]int{
sceneIdxWithTag,
sceneIdxWithTwoTags,
sceneIdxWithMarkerAndTag,
},
false,
},
{
"not null",
models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierNotNull,
},
[]int{
sceneIdxWithTag,
sceneIdxWithTwoTags,
sceneIdxWithMarkerAndTag,
},
[]int{sceneIdx1WithPerformer},
false,
},
{
"equals",
models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierEquals,
Value: []string{ Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithScene]), strconv.Itoa(tagIDs[tagIdx1WithScene]),
strconv.Itoa(tagIDs[tagIdx2WithScene]),
},
},
[]int{sceneIdxWithTwoTags},
[]int{
sceneIdxWithThreeTags,
},
false,
},
{
"not equals",
models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierNotEquals,
Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithScene]),
strconv.Itoa(tagIDs[tagIdx2WithScene]),
},
},
nil,
nil,
true,
}, },
Modifier: models.CriterionModifierExcludes,
} }
q := getSceneStringValue(sceneIdxWithTwoTags, titleField) for _, tt := range tests {
findFilter := models.FindFilterType{ runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
Q: &q, assert := assert.New(t)
}
scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter) results, err := db.Scene.Query(ctx, models.SceneQueryOptions{
assert.Len(t, scenes, 0) SceneFilter: &models.SceneFilterType{
Tags: &tt.filter,
return nil },
}) })
if (err != nil) != tt.wantErr {
t.Errorf("SceneStore.Query() error = %v, wantErr %v", err, tt.wantErr)
return
}
include := indexesToIDs(sceneIDs, tt.includeIdxs)
exclude := indexesToIDs(sceneIDs, tt.excludeIdxs)
for _, i := range include {
assert.Contains(results.IDs, i)
}
for _, e := range exclude {
assert.NotContains(results.IDs, e)
}
})
}
} }
func TestSceneQueryPerformerTags(t *testing.T) { func TestSceneQueryPerformerTags(t *testing.T) {
withTxn(func(ctx context.Context) error { allDepth := -1
sqb := db.Scene
tagCriterion := models.HierarchicalMultiCriterionInput{ tests := []struct {
name string
findFilter *models.FindFilterType
filter *models.SceneFilterType
includeIdxs []int
excludeIdxs []int
wantErr bool
}{
{
"includes",
nil,
&models.SceneFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(tagIDs[tagIdxWithPerformer]), strconv.Itoa(tagIDs[tagIdxWithPerformer]),
strconv.Itoa(tagIDs[tagIdx1WithPerformer]), strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
}, },
Modifier: models.CriterionModifierIncludes, Modifier: models.CriterionModifierIncludes,
} },
},
sceneFilter := models.SceneFilterType{ []int{
PerformerTags: &tagCriterion, sceneIdxWithPerformerTag,
} sceneIdxWithPerformerTwoTags,
sceneIdxWithTwoPerformerTag,
scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) },
assert.Len(t, scenes, 2) []int{
sceneIdxWithPerformer,
// ensure ids are correct },
for _, scene := range scenes { false,
assert.True(t, scene.ID == sceneIDs[sceneIdxWithPerformerTag] || scene.ID == sceneIDs[sceneIdxWithPerformerTwoTags]) },
} {
"includes sub-tags",
tagCriterion = models.HierarchicalMultiCriterionInput{ nil,
&models.SceneFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(tagIDs[tagIdxWithParentAndChild]),
},
Depth: &allDepth,
Modifier: models.CriterionModifierIncludes,
},
},
[]int{
sceneIdxWithPerformerParentTag,
},
[]int{
sceneIdxWithPerformer,
sceneIdxWithPerformerTag,
sceneIdxWithPerformerTwoTags,
sceneIdxWithTwoPerformerTag,
},
false,
},
{
"includes all",
nil,
&models.SceneFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Value: []string{ Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithPerformer]), strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
strconv.Itoa(tagIDs[tagIdx2WithPerformer]), strconv.Itoa(tagIDs[tagIdx2WithPerformer]),
}, },
Modifier: models.CriterionModifierIncludesAll, Modifier: models.CriterionModifierIncludesAll,
}
scenes = queryScene(ctx, t, sqb, &sceneFilter, nil)
assert.Len(t, scenes, 1)
assert.Equal(t, sceneIDs[sceneIdxWithPerformerTwoTags], scenes[0].ID)
tagCriterion = models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
}, },
},
[]int{
sceneIdxWithPerformerTwoTags,
},
[]int{
sceneIdxWithPerformer,
sceneIdxWithPerformerTag,
sceneIdxWithTwoPerformerTag,
},
false,
},
{
"excludes performer tag tagIdx2WithPerformer",
nil,
&models.SceneFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierExcludes, Modifier: models.CriterionModifierExcludes,
} Value: []string{strconv.Itoa(tagIDs[tagIdx2WithPerformer])},
},
q := getSceneStringValue(sceneIdxWithPerformerTwoTags, titleField) },
findFilter := models.FindFilterType{ nil,
Q: &q, []int{sceneIdxWithTwoPerformerTag},
} false,
},
scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter) {
assert.Len(t, scenes, 0) "excludes sub-tags",
nil,
tagCriterion = models.HierarchicalMultiCriterionInput{ &models.SceneFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(tagIDs[tagIdxWithParentAndChild]),
},
Depth: &allDepth,
Modifier: models.CriterionModifierExcludes,
},
},
[]int{
sceneIdxWithPerformer,
sceneIdxWithPerformerTag,
sceneIdxWithPerformerTwoTags,
sceneIdxWithTwoPerformerTag,
},
[]int{
sceneIdxWithPerformerParentTag,
},
false,
},
{
"is null",
nil,
&models.SceneFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIsNull, Modifier: models.CriterionModifierIsNull,
},
},
[]int{sceneIdx1WithPerformer},
[]int{sceneIdxWithPerformerTag},
false,
},
{
"not null",
nil,
&models.SceneFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierNotNull,
},
},
[]int{sceneIdxWithPerformerTag},
[]int{sceneIdx1WithPerformer},
false,
},
{
"equals",
nil,
&models.SceneFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierEquals,
Value: []string{
strconv.Itoa(tagIDs[tagIdx2WithPerformer]),
},
},
},
nil,
nil,
true,
},
{
"not equals",
nil,
&models.SceneFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierNotEquals,
Value: []string{
strconv.Itoa(tagIDs[tagIdx2WithPerformer]),
},
},
},
nil,
nil,
true,
},
} }
q = getSceneStringValue(sceneIdx1WithPerformer, titleField)
scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter) for _, tt := range tests {
assert.Len(t, scenes, 1) runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
assert.Equal(t, sceneIDs[sceneIdx1WithPerformer], scenes[0].ID) assert := assert.New(t)
q = getSceneStringValue(sceneIdxWithPerformerTag, titleField) results, err := db.Scene.Query(ctx, models.SceneQueryOptions{
scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter) SceneFilter: tt.filter,
assert.Len(t, scenes, 0) QueryOptions: models.QueryOptions{
FindFilter: tt.findFilter,
tagCriterion.Modifier = models.CriterionModifierNotNull },
scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter)
assert.Len(t, scenes, 1)
assert.Equal(t, sceneIDs[sceneIdxWithPerformerTag], scenes[0].ID)
q = getSceneStringValue(sceneIdx1WithPerformer, titleField)
scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter)
assert.Len(t, scenes, 0)
return nil
}) })
if (err != nil) != tt.wantErr {
t.Errorf("SceneStore.Query() error = %v, wantErr %v", err, tt.wantErr)
return
}
include := indexesToIDs(sceneIDs, tt.includeIdxs)
exclude := indexesToIDs(sceneIDs, tt.excludeIdxs)
for _, i := range include {
assert.Contains(results.IDs, i)
}
for _, e := range exclude {
assert.NotContains(results.IDs, e)
}
})
}
} }
func TestSceneQueryStudio(t *testing.T) { func TestSceneQueryStudio(t *testing.T) {
@@ -3561,6 +3855,30 @@ func TestSceneQueryStudio(t *testing.T) {
[]int{sceneIDs[sceneIdxWithGallery]}, []int{sceneIDs[sceneIdxWithGallery]},
false, false,
}, },
{
"equals",
"",
models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(studioIDs[studioIdxWithScene]),
},
Modifier: models.CriterionModifierEquals,
},
[]int{sceneIDs[sceneIdxWithStudio]},
false,
},
{
"not equals",
getSceneStringValue(sceneIdxWithStudio, titleField),
models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(studioIDs[studioIdxWithScene]),
},
Modifier: models.CriterionModifierNotEquals,
},
[]int{},
false,
},
} }
qb := db.Scene qb := db.Scene

View File

@@ -60,19 +60,24 @@ const (
sceneIdx1WithPerformer sceneIdx1WithPerformer
sceneIdx2WithPerformer sceneIdx2WithPerformer
sceneIdxWithTwoPerformers sceneIdxWithTwoPerformers
sceneIdxWithThreePerformers
sceneIdxWithTag sceneIdxWithTag
sceneIdxWithTwoTags sceneIdxWithTwoTags
sceneIdxWithThreeTags
sceneIdxWithMarkerAndTag sceneIdxWithMarkerAndTag
sceneIdxWithMarkerTwoTags
sceneIdxWithStudio sceneIdxWithStudio
sceneIdx1WithStudio sceneIdx1WithStudio
sceneIdx2WithStudio sceneIdx2WithStudio
sceneIdxWithMarkers sceneIdxWithMarkers
sceneIdxWithPerformerTag sceneIdxWithPerformerTag
sceneIdxWithTwoPerformerTag
sceneIdxWithPerformerTwoTags sceneIdxWithPerformerTwoTags
sceneIdxWithSpacedName sceneIdxWithSpacedName
sceneIdxWithStudioPerformer sceneIdxWithStudioPerformer
sceneIdxWithGrandChildStudio sceneIdxWithGrandChildStudio
sceneIdxMissingPhash sceneIdxMissingPhash
sceneIdxWithPerformerParentTag
// new indexes above // new indexes above
lastSceneIdx lastSceneIdx
@@ -90,16 +95,20 @@ const (
imageIdx1WithPerformer imageIdx1WithPerformer
imageIdx2WithPerformer imageIdx2WithPerformer
imageIdxWithTwoPerformers imageIdxWithTwoPerformers
imageIdxWithThreePerformers
imageIdxWithTag imageIdxWithTag
imageIdxWithTwoTags imageIdxWithTwoTags
imageIdxWithThreeTags
imageIdxWithStudio imageIdxWithStudio
imageIdx1WithStudio imageIdx1WithStudio
imageIdx2WithStudio imageIdx2WithStudio
imageIdxWithStudioPerformer imageIdxWithStudioPerformer
imageIdxInZip imageIdxInZip
imageIdxWithPerformerTag imageIdxWithPerformerTag
imageIdxWithTwoPerformerTag
imageIdxWithPerformerTwoTags imageIdxWithPerformerTwoTags
imageIdxWithGrandChildStudio imageIdxWithGrandChildStudio
imageIdxWithPerformerParentTag
// new indexes above // new indexes above
totalImages totalImages
) )
@@ -108,20 +117,25 @@ const (
performerIdxWithScene = iota performerIdxWithScene = iota
performerIdx1WithScene performerIdx1WithScene
performerIdx2WithScene performerIdx2WithScene
performerIdx3WithScene
performerIdxWithTwoScenes performerIdxWithTwoScenes
performerIdxWithImage performerIdxWithImage
performerIdxWithTwoImages performerIdxWithTwoImages
performerIdx1WithImage performerIdx1WithImage
performerIdx2WithImage performerIdx2WithImage
performerIdx3WithImage
performerIdxWithTag performerIdxWithTag
performerIdx2WithTag
performerIdxWithTwoTags performerIdxWithTwoTags
performerIdxWithGallery performerIdxWithGallery
performerIdxWithTwoGalleries performerIdxWithTwoGalleries
performerIdx1WithGallery performerIdx1WithGallery
performerIdx2WithGallery performerIdx2WithGallery
performerIdx3WithGallery
performerIdxWithSceneStudio performerIdxWithSceneStudio
performerIdxWithImageStudio performerIdxWithImageStudio
performerIdxWithGalleryStudio performerIdxWithGalleryStudio
performerIdxWithParentTag
// new indexes above // new indexes above
// performers with dup names start from the end // performers with dup names start from the end
performerIdx1WithDupName performerIdx1WithDupName
@@ -155,16 +169,20 @@ const (
galleryIdx1WithPerformer galleryIdx1WithPerformer
galleryIdx2WithPerformer galleryIdx2WithPerformer
galleryIdxWithTwoPerformers galleryIdxWithTwoPerformers
galleryIdxWithThreePerformers
galleryIdxWithTag galleryIdxWithTag
galleryIdxWithTwoTags galleryIdxWithTwoTags
galleryIdxWithThreeTags
galleryIdxWithStudio galleryIdxWithStudio
galleryIdx1WithStudio galleryIdx1WithStudio
galleryIdx2WithStudio galleryIdx2WithStudio
galleryIdxWithPerformerTag galleryIdxWithPerformerTag
galleryIdxWithTwoPerformerTag
galleryIdxWithPerformerTwoTags galleryIdxWithPerformerTwoTags
galleryIdxWithStudioPerformer galleryIdxWithStudioPerformer
galleryIdxWithGrandChildStudio galleryIdxWithGrandChildStudio
galleryIdxWithoutFile galleryIdxWithoutFile
galleryIdxWithPerformerParentTag
// new indexes above // new indexes above
lastGalleryIdx lastGalleryIdx
@@ -182,12 +200,14 @@ const (
tagIdxWithImage tagIdxWithImage
tagIdx1WithImage tagIdx1WithImage
tagIdx2WithImage tagIdx2WithImage
tagIdx3WithImage
tagIdxWithPerformer tagIdxWithPerformer
tagIdx1WithPerformer tagIdx1WithPerformer
tagIdx2WithPerformer tagIdx2WithPerformer
tagIdxWithGallery tagIdxWithGallery
tagIdx1WithGallery tagIdx1WithGallery
tagIdx2WithGallery tagIdx2WithGallery
tagIdx3WithGallery
tagIdxWithChildTag tagIdxWithChildTag
tagIdxWithParentTag tagIdxWithParentTag
tagIdxWithGrandChild tagIdxWithGrandChild
@@ -334,17 +354,22 @@ var (
sceneTags = linkMap{ sceneTags = linkMap{
sceneIdxWithTag: {tagIdxWithScene}, sceneIdxWithTag: {tagIdxWithScene},
sceneIdxWithTwoTags: {tagIdx1WithScene, tagIdx2WithScene}, sceneIdxWithTwoTags: {tagIdx1WithScene, tagIdx2WithScene},
sceneIdxWithThreeTags: {tagIdx1WithScene, tagIdx2WithScene, tagIdx3WithScene},
sceneIdxWithMarkerAndTag: {tagIdx3WithScene}, sceneIdxWithMarkerAndTag: {tagIdx3WithScene},
sceneIdxWithMarkerTwoTags: {tagIdx2WithScene, tagIdx3WithScene},
} }
scenePerformers = linkMap{ scenePerformers = linkMap{
sceneIdxWithPerformer: {performerIdxWithScene}, sceneIdxWithPerformer: {performerIdxWithScene},
sceneIdxWithTwoPerformers: {performerIdx1WithScene, performerIdx2WithScene}, sceneIdxWithTwoPerformers: {performerIdx1WithScene, performerIdx2WithScene},
sceneIdxWithThreePerformers: {performerIdx1WithScene, performerIdx2WithScene, performerIdx3WithScene},
sceneIdxWithPerformerTag: {performerIdxWithTag}, sceneIdxWithPerformerTag: {performerIdxWithTag},
sceneIdxWithTwoPerformerTag: {performerIdxWithTag, performerIdx2WithTag},
sceneIdxWithPerformerTwoTags: {performerIdxWithTwoTags}, sceneIdxWithPerformerTwoTags: {performerIdxWithTwoTags},
sceneIdx1WithPerformer: {performerIdxWithTwoScenes}, sceneIdx1WithPerformer: {performerIdxWithTwoScenes},
sceneIdx2WithPerformer: {performerIdxWithTwoScenes}, sceneIdx2WithPerformer: {performerIdxWithTwoScenes},
sceneIdxWithStudioPerformer: {performerIdxWithSceneStudio}, sceneIdxWithStudioPerformer: {performerIdxWithSceneStudio},
sceneIdxWithPerformerParentTag: {performerIdxWithParentTag},
} }
sceneGalleries = linkMap{ sceneGalleries = linkMap{
@@ -376,6 +401,7 @@ var (
{sceneIdxWithMarkers, tagIdxWithPrimaryMarkers, nil}, {sceneIdxWithMarkers, tagIdxWithPrimaryMarkers, nil},
{sceneIdxWithMarkers, tagIdxWithPrimaryMarkers, []int{tagIdxWithMarkers}}, {sceneIdxWithMarkers, tagIdxWithPrimaryMarkers, []int{tagIdxWithMarkers}},
{sceneIdxWithMarkerAndTag, tagIdxWithPrimaryMarkers, nil}, {sceneIdxWithMarkerAndTag, tagIdxWithPrimaryMarkers, nil},
{sceneIdxWithMarkerTwoTags, tagIdxWithPrimaryMarkers, nil},
} }
) )
@@ -409,15 +435,19 @@ var (
imageTags = linkMap{ imageTags = linkMap{
imageIdxWithTag: {tagIdxWithImage}, imageIdxWithTag: {tagIdxWithImage},
imageIdxWithTwoTags: {tagIdx1WithImage, tagIdx2WithImage}, imageIdxWithTwoTags: {tagIdx1WithImage, tagIdx2WithImage},
imageIdxWithThreeTags: {tagIdx1WithImage, tagIdx2WithImage, tagIdx3WithImage},
} }
imagePerformers = linkMap{ imagePerformers = linkMap{
imageIdxWithPerformer: {performerIdxWithImage}, imageIdxWithPerformer: {performerIdxWithImage},
imageIdxWithTwoPerformers: {performerIdx1WithImage, performerIdx2WithImage}, imageIdxWithTwoPerformers: {performerIdx1WithImage, performerIdx2WithImage},
imageIdxWithThreePerformers: {performerIdx1WithImage, performerIdx2WithImage, performerIdx3WithImage},
imageIdxWithPerformerTag: {performerIdxWithTag}, imageIdxWithPerformerTag: {performerIdxWithTag},
imageIdxWithTwoPerformerTag: {performerIdxWithTag, performerIdx2WithTag},
imageIdxWithPerformerTwoTags: {performerIdxWithTwoTags}, imageIdxWithPerformerTwoTags: {performerIdxWithTwoTags},
imageIdx1WithPerformer: {performerIdxWithTwoImages}, imageIdx1WithPerformer: {performerIdxWithTwoImages},
imageIdx2WithPerformer: {performerIdxWithTwoImages}, imageIdx2WithPerformer: {performerIdxWithTwoImages},
imageIdxWithStudioPerformer: {performerIdxWithImageStudio}, imageIdxWithStudioPerformer: {performerIdxWithImageStudio},
imageIdxWithPerformerParentTag: {performerIdxWithParentTag},
} }
) )
@@ -425,11 +455,14 @@ var (
galleryPerformers = linkMap{ galleryPerformers = linkMap{
galleryIdxWithPerformer: {performerIdxWithGallery}, galleryIdxWithPerformer: {performerIdxWithGallery},
galleryIdxWithTwoPerformers: {performerIdx1WithGallery, performerIdx2WithGallery}, galleryIdxWithTwoPerformers: {performerIdx1WithGallery, performerIdx2WithGallery},
galleryIdxWithThreePerformers: {performerIdx1WithGallery, performerIdx2WithGallery, performerIdx3WithGallery},
galleryIdxWithPerformerTag: {performerIdxWithTag}, galleryIdxWithPerformerTag: {performerIdxWithTag},
galleryIdxWithTwoPerformerTag: {performerIdxWithTag, performerIdx2WithTag},
galleryIdxWithPerformerTwoTags: {performerIdxWithTwoTags}, galleryIdxWithPerformerTwoTags: {performerIdxWithTwoTags},
galleryIdx1WithPerformer: {performerIdxWithTwoGalleries}, galleryIdx1WithPerformer: {performerIdxWithTwoGalleries},
galleryIdx2WithPerformer: {performerIdxWithTwoGalleries}, galleryIdx2WithPerformer: {performerIdxWithTwoGalleries},
galleryIdxWithStudioPerformer: {performerIdxWithGalleryStudio}, galleryIdxWithStudioPerformer: {performerIdxWithGalleryStudio},
galleryIdxWithPerformerParentTag: {performerIdxWithParentTag},
} }
galleryStudios = map[int]int{ galleryStudios = map[int]int{
@@ -443,6 +476,7 @@ var (
galleryTags = linkMap{ galleryTags = linkMap{
galleryIdxWithTag: {tagIdxWithGallery}, galleryIdxWithTag: {tagIdxWithGallery},
galleryIdxWithTwoTags: {tagIdx1WithGallery, tagIdx2WithGallery}, galleryIdxWithTwoTags: {tagIdx1WithGallery, tagIdx2WithGallery},
galleryIdxWithThreeTags: {tagIdx1WithGallery, tagIdx2WithGallery, tagIdx3WithGallery},
} }
) )
@@ -463,7 +497,9 @@ var (
var ( var (
performerTags = linkMap{ performerTags = linkMap{
performerIdxWithTag: {tagIdxWithPerformer}, performerIdxWithTag: {tagIdxWithPerformer},
performerIdx2WithTag: {tagIdx2WithPerformer},
performerIdxWithTwoTags: {tagIdx1WithPerformer, tagIdx2WithPerformer}, performerIdxWithTwoTags: {tagIdx1WithPerformer, tagIdx2WithPerformer},
performerIdxWithParentTag: {tagIdxWithParentAndChild},
} }
) )
@@ -484,6 +520,16 @@ func indexesToIDs(ids []int, indexes []int) []int {
return ret return ret
} }
func indexFromID(ids []int, id int) int {
for i, v := range ids {
if v == id {
return i
}
}
return -1
}
var db *sqlite.Database var db *sqlite.Database
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@@ -1431,11 +1477,8 @@ 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] || id == tagIDs[tagIdx3WithScene] { idx := indexFromID(tagIDs, id)
return 1 return len(sceneTags.reverseLookup(idx))
}
return 0
} }
func getTagMarkerCount(id int) int { func getTagMarkerCount(id int) int {
@@ -1451,27 +1494,18 @@ func getTagMarkerCount(id int) int {
} }
func getTagImageCount(id int) int { func getTagImageCount(id int) int {
if id == tagIDs[tagIdx1WithImage] || id == tagIDs[tagIdx2WithImage] || id == tagIDs[tagIdxWithImage] { idx := indexFromID(tagIDs, id)
return 1 return len(imageTags.reverseLookup(idx))
}
return 0
} }
func getTagGalleryCount(id int) int { func getTagGalleryCount(id int) int {
if id == tagIDs[tagIdx1WithGallery] || id == tagIDs[tagIdx2WithGallery] || id == tagIDs[tagIdxWithGallery] { idx := indexFromID(tagIDs, id)
return 1 return len(galleryTags.reverseLookup(idx))
}
return 0
} }
func getTagPerformerCount(id int) int { func getTagPerformerCount(id int) int {
if id == tagIDs[tagIdx1WithPerformer] || id == tagIDs[tagIdx2WithPerformer] || id == tagIDs[tagIdxWithPerformer] { idx := indexFromID(tagIDs, id)
return 1 return len(performerTags.reverseLookup(idx))
}
return 0
} }
func getTagParentCount(id int) int { func getTagParentCount(id int) int {

View File

@@ -474,9 +474,19 @@ func tagMarkerCountCriterionHandler(qb *tagQueryBuilder, markerCount *models.Int
} }
} }
func tagParentsCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { func tagParentsCriterionHandler(qb *tagQueryBuilder, criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) { return func(ctx context.Context, f *filterBuilder) {
if tags != nil { if criterion != nil {
tags := criterion.CombineExcludes()
// validate the modifier
switch tags.Modifier {
case models.CriterionModifierIncludesAll, models.CriterionModifierIncludes, models.CriterionModifierExcludes, models.CriterionModifierIsNull, models.CriterionModifierNotNull:
// valid
default:
f.setError(fmt.Errorf("invalid modifier %s for tag parent/children", criterion.Modifier))
}
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull { if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
var notClause string var notClause string
if tags.Modifier == models.CriterionModifierNotNull { if tags.Modifier == models.CriterionModifierNotNull {
@@ -489,10 +499,11 @@ func tagParentsCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalMu
return return
} }
if len(tags.Value) == 0 { if len(tags.Value) == 0 && len(tags.Excludes) == 0 {
return return
} }
if len(tags.Value) > 0 {
var args []interface{} var args []interface{}
for _, val := range tags.Value { for _, val := range tags.Value {
args = append(args, val) args = append(args, val)
@@ -518,14 +529,58 @@ func tagParentsCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalMu
f.addLeftJoin("parents", "", "parents.item_id = tags.id") f.addLeftJoin("parents", "", "parents.item_id = tags.id")
addHierarchicalConditionClauses(f, *tags, "parents", "root_id") addHierarchicalConditionClauses(f, tags, "parents", "root_id")
}
if len(tags.Excludes) > 0 {
var args []interface{}
for _, val := range tags.Excludes {
args = append(args, val)
}
depthVal := 0
if tags.Depth != nil {
depthVal = *tags.Depth
}
var depthCondition string
if depthVal != -1 {
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
}
query := `parents2 AS (
SELECT parent_id AS root_id, child_id AS item_id, 0 AS depth FROM tags_relations WHERE parent_id IN` + getInBinding(len(tags.Excludes)) + `
UNION
SELECT root_id, child_id, depth + 1 FROM tags_relations INNER JOIN parents2 ON item_id = parent_id ` + depthCondition + `
)`
f.addRecursiveWith(query, args...)
f.addLeftJoin("parents2", "", "parents2.item_id = tags.id")
addHierarchicalConditionClauses(f, models.HierarchicalMultiCriterionInput{
Value: tags.Excludes,
Depth: tags.Depth,
Modifier: models.CriterionModifierExcludes,
}, "parents2", "root_id")
}
} }
} }
} }
func tagChildrenCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc { func tagChildrenCriterionHandler(qb *tagQueryBuilder, criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) { return func(ctx context.Context, f *filterBuilder) {
if tags != nil { if criterion != nil {
tags := criterion.CombineExcludes()
// validate the modifier
switch tags.Modifier {
case models.CriterionModifierIncludesAll, models.CriterionModifierIncludes, models.CriterionModifierExcludes, models.CriterionModifierIsNull, models.CriterionModifierNotNull:
// valid
default:
f.setError(fmt.Errorf("invalid modifier %s for tag parent/children", criterion.Modifier))
}
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull { if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
var notClause string var notClause string
if tags.Modifier == models.CriterionModifierNotNull { if tags.Modifier == models.CriterionModifierNotNull {
@@ -538,10 +593,11 @@ func tagChildrenCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalM
return return
} }
if len(tags.Value) == 0 { if len(tags.Value) == 0 && len(tags.Excludes) == 0 {
return return
} }
if len(tags.Value) > 0 {
var args []interface{} var args []interface{}
for _, val := range tags.Value { for _, val := range tags.Value {
args = append(args, val) args = append(args, val)
@@ -567,7 +623,41 @@ func tagChildrenCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalM
f.addLeftJoin("children", "", "children.item_id = tags.id") f.addLeftJoin("children", "", "children.item_id = tags.id")
addHierarchicalConditionClauses(f, *tags, "children", "root_id") addHierarchicalConditionClauses(f, tags, "children", "root_id")
}
if len(tags.Excludes) > 0 {
var args []interface{}
for _, val := range tags.Excludes {
args = append(args, val)
}
depthVal := 0
if tags.Depth != nil {
depthVal = *tags.Depth
}
var depthCondition string
if depthVal != -1 {
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
}
query := `children2 AS (
SELECT child_id AS root_id, parent_id AS item_id, 0 AS depth FROM tags_relations WHERE child_id IN` + getInBinding(len(tags.Excludes)) + `
UNION
SELECT root_id, parent_id, depth + 1 FROM tags_relations INNER JOIN children2 ON item_id = child_id ` + depthCondition + `
)`
f.addRecursiveWith(query, args...)
f.addLeftJoin("children2", "", "children2.item_id = tags.id")
addHierarchicalConditionClauses(f, models.HierarchicalMultiCriterionInput{
Value: tags.Excludes,
Depth: tags.Depth,
Modifier: models.CriterionModifierExcludes,
}, "children2", "root_id")
}
} }
} }
} }

View File

@@ -187,7 +187,7 @@ func TestTagQuerySort(t *testing.T) {
tags := queryTags(ctx, t, sqb, nil, findFilter) tags := queryTags(ctx, t, sqb, nil, findFilter)
assert := assert.New(t) assert := assert.New(t)
assert.Equal(tagIDs[tagIdxWithScene], tags[0].ID) assert.Equal(tagIDs[tagIdx2WithScene], tags[0].ID)
sortBy = "scene_markers_count" sortBy = "scene_markers_count"
tags = queryTags(ctx, t, sqb, nil, findFilter) tags = queryTags(ctx, t, sqb, nil, findFilter)
@@ -195,15 +195,15 @@ func TestTagQuerySort(t *testing.T) {
sortBy = "images_count" sortBy = "images_count"
tags = queryTags(ctx, t, sqb, nil, findFilter) tags = queryTags(ctx, t, sqb, nil, findFilter)
assert.Equal(tagIDs[tagIdxWithImage], tags[0].ID) assert.Equal(tagIDs[tagIdx1WithImage], tags[0].ID)
sortBy = "galleries_count" sortBy = "galleries_count"
tags = queryTags(ctx, t, sqb, nil, findFilter) tags = queryTags(ctx, t, sqb, nil, findFilter)
assert.Equal(tagIDs[tagIdxWithGallery], tags[0].ID) assert.Equal(tagIDs[tagIdx1WithGallery], tags[0].ID)
sortBy = "performers_count" sortBy = "performers_count"
tags = queryTags(ctx, t, sqb, nil, findFilter) tags = queryTags(ctx, t, sqb, nil, findFilter)
assert.Equal(tagIDs[tagIdxWithPerformer], tags[0].ID) assert.Equal(tagIDs[tagIdx2WithPerformer], tags[0].ID)
return nil return nil
}) })

View File

@@ -309,14 +309,18 @@ export const HierarchicalObjectsFilter = <
return ( return (
<Form> <Form>
{criterion.modifier !== CriterionModifier.Equals && (
<Form.Group> <Form.Group>
<Form.Check <Form.Check
id={criterionOptionTypeToIncludeID()} id={criterionOptionTypeToIncludeID()}
checked={criterion.value.depth !== 0} checked={criterion.value.depth !== 0}
label={intl.formatMessage(criterionOptionTypeToIncludeUIString())} label={intl.formatMessage(criterionOptionTypeToIncludeUIString())}
onChange={() => onDepthChanged(criterion.value.depth !== 0 ? 0 : -1)} onChange={() =>
onDepthChanged(criterion.value.depth !== 0 ? 0 : -1)
}
/> />
</Form.Group> </Form.Group>
)}
{criterion.value.depth !== 0 && ( {criterion.value.depth !== 0 && (
<Form.Group> <Form.Group>

View File

@@ -567,6 +567,11 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
protected toCriterionInput(): HierarchicalMultiCriterionInput { protected toCriterionInput(): HierarchicalMultiCriterionInput {
let excludes: string[] = []; let excludes: string[] = [];
// if modifier is equals, depth must be 0
const depth =
this.modifier === CriterionModifier.Equals ? 0 : this.value.depth;
if (this.value.excluded) { if (this.value.excluded) {
excludes = this.value.excluded.map((v) => v.id); excludes = this.value.excluded.map((v) => v.id);
} }
@@ -574,7 +579,7 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
value: this.value.items.map((v) => v.id), value: this.value.items.map((v) => v.id),
excludes: excludes, excludes: excludes,
modifier: this.modifier, modifier: this.modifier,
depth: this.value.depth, depth,
}; };
} }

View File

@@ -4,14 +4,24 @@ import { CriterionOption, IHierarchicalLabeledIdCriterion } from "./criterion";
export class TagsCriterion extends IHierarchicalLabeledIdCriterion {} export class TagsCriterion extends IHierarchicalLabeledIdCriterion {}
class tagsCriterionOption extends CriterionOption { const tagsModifierOptions = [
constructor(messageID: string, value: CriterionType, parameterName: string) {
const modifierOptions = [
CriterionModifier.Includes, CriterionModifier.Includes,
CriterionModifier.IncludesAll, CriterionModifier.IncludesAll,
CriterionModifier.Equals, CriterionModifier.Equals,
]; ];
const withoutEqualsModifierOptions = [
CriterionModifier.Includes,
CriterionModifier.IncludesAll,
];
class tagsCriterionOption extends CriterionOption {
constructor(
messageID: string,
value: CriterionType,
parameterName: string,
modifierOptions: CriterionModifier[]
) {
let defaultModifier = CriterionModifier.IncludesAll; let defaultModifier = CriterionModifier.IncludesAll;
super({ super({
@@ -27,25 +37,30 @@ class tagsCriterionOption extends CriterionOption {
export const TagsCriterionOption = new tagsCriterionOption( export const TagsCriterionOption = new tagsCriterionOption(
"tags", "tags",
"tags", "tags",
"tags" "tags",
tagsModifierOptions
); );
export const SceneTagsCriterionOption = new tagsCriterionOption( export const SceneTagsCriterionOption = new tagsCriterionOption(
"sceneTags", "sceneTags",
"sceneTags", "sceneTags",
"scene_tags" "scene_tags",
tagsModifierOptions
); );
export const PerformerTagsCriterionOption = new tagsCriterionOption( export const PerformerTagsCriterionOption = new tagsCriterionOption(
"performerTags", "performerTags",
"performerTags", "performerTags",
"performer_tags" "performer_tags",
withoutEqualsModifierOptions
); );
export const ParentTagsCriterionOption = new tagsCriterionOption( export const ParentTagsCriterionOption = new tagsCriterionOption(
"parent_tags", "parent_tags",
"parentTags", "parentTags",
"parents" "parents",
withoutEqualsModifierOptions
); );
export const ChildTagsCriterionOption = new tagsCriterionOption( export const ChildTagsCriterionOption = new tagsCriterionOption(
"sub_tags", "sub_tags",
"childTags", "childTags",
"children" "children",
withoutEqualsModifierOptions
); );