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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 2153 additions and 938 deletions

View file

@ -135,6 +135,17 @@ type HierarchicalMultiCriterionInput struct {
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 {
Value []string `json:"value"`
Modifier CriterionModifier `json:"modifier"`

View file

@ -9,7 +9,6 @@ import (
"strconv"
"strings"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/utils"
"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))
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:
// includes all of the provided ids
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 {
tx dbWrapper
@ -838,12 +866,20 @@ type hierarchicalMultiCriterionHandlerBuilder struct {
foreignFK string
parentFK string
childFK 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{}
if parentFK == "" {
parentFK = "parent_id"
}
if childFK == "" {
childFK = "child_id"
}
depthVal := 0
if depth != nil {
depthVal = *depth
@ -865,7 +901,7 @@ func getHierarchicalValues(ctx context.Context, tx dbWrapper, values []string, t
}
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),
"recursiveSelect": "",
"parentFK": parentFK,
"childFK": childFK,
"depthCondition": depthCondition,
"unionClause": "",
}
if relationsTable != "" {
withClauseMap["recursiveSelect"] = utils.StrFormat(`SELECT p.root_id, c.child_id, depth + 1 FROM {relationsTable} AS c
INNER JOIN items as p ON c.parent_id = p.item_id
withClauseMap["recursiveSelect"] = utils.StrFormat(`SELECT p.root_id, c.{childFK}, depth + 1 FROM {relationsTable} AS c
INNER JOIN items as p ON c.{parentFK} = p.item_id
`, withClauseMap)
} else {
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
err := tx.Get(ctx, &valuesClause, query, args...)
if err != nil {
logger.Error(err)
// return record which never matches so we don't have to handle error here
return "VALUES(NULL, NULL)"
return "", fmt.Errorf("failed to get hierarchical values: %w", err)
}
return valuesClause
return valuesClause, nil
}
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
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 {
var notClause string
if criterion.Modifier == models.CriterionModifierNotNull {
@ -968,7 +1009,11 @@ func (m *hierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hierarchica
}
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 {
case models.CriterionModifierIncludes:
@ -980,7 +1025,11 @@ func (m *hierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hierarchica
}
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))
}
@ -992,10 +1041,12 @@ type joinedHierarchicalMultiCriterionHandlerBuilder struct {
tx dbWrapper
primaryTable string
primaryKey string
foreignTable string
foreignFK string
parentFK string
childFK string
relationsTable string
joinAs string
@ -1004,16 +1055,25 @@ type joinedHierarchicalMultiCriterionHandlerBuilder struct {
}
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
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.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,
"primaryFK": m.primaryFK,
"primaryTable": m.primaryTable,
"primaryKey": primaryKey,
}), 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)
}
}
@ -1024,6 +1084,15 @@ func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hiera
// make a copy so we don't modify the original
criterion := *c
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 {
var notClause string
@ -1031,7 +1100,7 @@ func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hiera
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{
"table": joinAlias,
@ -1053,7 +1122,11 @@ func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hiera
}
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(`(
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,
})
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")
}
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(`(
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"
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
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 {
c *models.StashIDCriterionInput
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, galleryPerformerCountCriterionHandler(qb, galleryFilter.PerformerCount))
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, galleryAverageResolutionCriterionHandler(qb, galleryFilter.AverageResolution))
query.handleCriterion(ctx, galleryImageCountCriterionHandler(qb, galleryFilter.ImageCount))
@ -968,51 +968,12 @@ func hasChaptersCriterionHandler(hasChapters *string) criterionHandlerFunc {
}
}
func galleryStudioCriterionHandler(qb *GalleryStore, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
h := hierarchicalMultiCriterionHandlerBuilder{
tx: qb.tx,
primaryTable: galleryTable,
foreignTable: studioTable,
foreignFK: studioIDColumn,
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")
}
func galleryPerformerTagsCriterionHandler(qb *GalleryStore, tags *models.HierarchicalMultiCriterionInput) criterionHandler {
return &joinedPerformerTagsHandler{
criterion: tags,
primaryTable: galleryTable,
joinTable: performersGalleriesTable,
joinPrimaryKey: galleryIDColumn,
}
}

View file

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

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, imagePerformersCriterionHandler(qb, imageFilter.Performers))
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, imagePerformerFavoriteCriterionHandler(imageFilter.PerformerFavorite))
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 {
h := hierarchicalMultiCriterionHandlerBuilder{
tx: qb.tx,
primaryTable: imageTable,
foreignTable: studioTable,
foreignFK: studioIDColumn,
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")
}
func imagePerformerTagsCriterionHandler(qb *ImageStore, tags *models.HierarchicalMultiCriterionInput) criterionHandler {
return &joinedPerformerTagsHandler{
criterion: tags,
primaryTable: imageTable,
joinTable: performersImagesTable,
joinPrimaryKey: imageIDColumn,
}
}

View file

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

View file

@ -908,7 +908,11 @@ func performerStudiosCriterionHandler(qb *PerformerStore, studios *models.Hierar
}
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 + ")")
templStr := `SELECT performer_id FROM {primaryTable}

View file

@ -513,12 +513,13 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
performerIDs[performerIdxWithTwoTags],
clearPerformerPartial(),
models.Performer{
ID: performerIDs[performerIdxWithTwoTags],
Name: getPerformerStringValue(performerIdxWithTwoTags, "Name"),
Favorite: true,
Aliases: models.NewRelatedStrings([]string{}),
TagIDs: models.NewRelatedIDs([]int{}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
ID: performerIDs[performerIdxWithTwoTags],
Name: getPerformerStringValue(performerIdxWithTwoTags, "Name"),
Favorite: getPerformerBoolValue(performerIdxWithTwoTags),
Aliases: models.NewRelatedStrings([]string{}),
TagIDs: models.NewRelatedIDs([]int{}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
IgnoreAutoTag: getIgnoreAutoTag(performerIdxWithTwoTags),
},
false,
},
@ -1904,10 +1905,10 @@ func TestPerformerQuerySortScenesCount(t *testing.T) {
assert.True(t, len(performers) > 0)
// first performer should be performerIdxWithTwoScenes
// first performer should be performerIdx1WithScene
firstPerformer := performers[0]
assert.Equal(t, performerIDs[performerIdxWithTwoScenes], firstPerformer.ID)
assert.Equal(t, performerIDs[performerIdx1WithScene], firstPerformer.ID)
// sort in ascending order
direction = models.SortDirectionEnumAsc
@ -1920,7 +1921,7 @@ func TestPerformerQuerySortScenesCount(t *testing.T) {
assert.True(t, len(performers) > 0)
lastPerformer := performers[len(performers)-1]
assert.Equal(t, performerIDs[performerIdxWithTwoScenes], lastPerformer.ID)
assert.Equal(t, performerIDs[performerIdxWithTag], lastPerformer.ID)
return nil
})
@ -2060,7 +2061,7 @@ func TestPerformerStore_FindByStashIDStatus(t *testing.T) {
name: "!hasStashID",
hasStashID: false,
stashboxEndpoint: getPerformerStringValue(performerIdxWithScene, "endpoint"),
include: []int{performerIdxWithImage},
include: []int{performerIdxWithTwoScenes},
exclude: []int{performerIdx2WithScene},
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, scenePerformersCriterionHandler(qb, sceneFilter.Performers))
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, scenePerformerTagsCriterionHandler(qb, sceneFilter.PerformerTags))
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 {
addJoinsFunc := func(f *filterBuilder) {
qb.moviesRepository().join(f, "", "scenes.id")
@ -1374,38 +1361,12 @@ func sceneMoviesCriterionHandler(qb *SceneStore, movies *models.MultiCriterionIn
return h.handler(movies)
}
func scenePerformerTagsCriterionHandler(qb *SceneStore, 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_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")
}
func scenePerformerTagsCriterionHandler(qb *SceneStore, tags *models.HierarchicalMultiCriterionInput) criterionHandler {
return &joinedPerformerTagsHandler{
criterion: tags,
primaryTable: sceneTable,
joinTable: performersScenesTable,
joinPrimaryKey: sceneIDColumn,
}
}

View file

@ -209,7 +209,11 @@ func sceneMarkerTagsCriterionHandler(qb *sceneMarkerQueryBuilder, tags *models.H
if len(tags.Value) == 0 {
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 (
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 {
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("scenes_tags", "", "scene_markers.scene_id = scenes_tags.scene_id")
f.addLeftJoin("scenes_tags", "", "scene_markers.scene_id = scenes_tags.scene_id")
h := joinedHierarchicalMultiCriterionHandlerBuilder{
tx: qb.tx,
f.addWhere(fmt.Sprintf("scenes_tags.tag_id IS %s NULL", notClause))
return
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 {
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")
h.handler(tags).handle(ctx, f)
}
}
}

View file

@ -5,9 +5,12 @@ package sqlite_test
import (
"context"
"strconv"
"testing"
"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/stretchr/testify/assert"
)
@ -50,7 +53,7 @@ func TestMarkerCountByTagID(t *testing.T) {
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])
@ -151,7 +154,7 @@ func TestMarkerQuerySceneTags(t *testing.T) {
}
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))
if err != nil {
t.Errorf("error getting marker tag ids: %v", err)
@ -164,11 +167,40 @@ func TestMarkerQuerySceneTags(t *testing.T) {
}
tagIDs := s.TagIDs.List()
if markerFilter.SceneTags.Modifier == models.CriterionModifierIsNull && len(tagIDs) > 0 {
t.Errorf("expected marker %d to have no scene tags - found %d", m.ID, len(tagIDs))
}
if markerFilter.SceneTags.Modifier == models.CriterionModifierNotNull && len(tagIDs) == 0 {
t.Errorf("expected marker %d to have scene tags - found 0", m.ID)
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))
}
case models.CriterionModifierNotNull:
if len(tagIDs) == 0 {
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)
}
}
}
@ -191,6 +223,70 @@ func TestMarkerQuerySceneTags(t *testing.T) {
},
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 {
@ -198,7 +294,7 @@ func TestMarkerQuerySceneTags(t *testing.T) {
markers := queryMarkers(ctx, t, sqlite.SceneMarkerReaderWriter, tc.markerFilter, tc.findFilter)
assert.Greater(t, len(markers), 0)
for _, m := range markers {
testTags(m, tc.markerFilter)
testTags(t, m, tc.markerFilter)
}
})
}

View file

@ -668,7 +668,8 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
sceneIDs[sceneIdxWithSpacedName],
clearScenePartial(),
models.Scene{
ID: sceneIDs[sceneIdxWithSpacedName],
ID: sceneIDs[sceneIdxWithSpacedName],
OCounter: getOCounter(sceneIdxWithSpacedName),
Files: models.NewRelatedVideoFiles([]*file.VideoFile{
makeSceneFile(sceneIdxWithSpacedName),
}),
@ -677,6 +678,10 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
PerformerIDs: models.NewRelatedIDs([]int{}),
Movies: models.NewRelatedMovies([]models.MoviesScenes{}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
PlayCount: getScenePlayCount(sceneIdxWithSpacedName),
PlayDuration: getScenePlayDuration(sceneIdxWithSpacedName),
LastPlayedAt: getSceneLastPlayed(sceneIdxWithSpacedName),
ResumeTime: getSceneResumeTime(sceneIdxWithSpacedName),
},
false,
},
@ -2101,6 +2106,8 @@ func sceneQueryQ(ctx context.Context, t *testing.T, sqb models.SceneReader, q st
// no Q should return all results
filter.Q = nil
pp := totalScenes
filter.PerPage = &pp
scenes = queryScene(ctx, t, sqb, nil, &filter)
assert.Len(t, scenes, totalScenes)
@ -2230,8 +2237,8 @@ func TestSceneQuery(t *testing.T) {
return
}
include := indexesToIDs(performerIDs, tt.includeIdxs)
exclude := indexesToIDs(performerIDs, tt.excludeIdxs)
include := indexesToIDs(sceneIDs, tt.includeIdxs)
exclude := indexesToIDs(sceneIDs, tt.excludeIdxs)
for _, i := range include {
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) {
@ -3329,192 +3342,473 @@ func TestSceneQueryIsMissingPhash(t *testing.T) {
}
func TestSceneQueryPerformers(t *testing.T) {
withTxn(func(ctx context.Context) error {
sqb := db.Scene
performerCriterion := models.MultiCriterionInput{
Value: []string{
strconv.Itoa(performerIDs[performerIdxWithScene]),
strconv.Itoa(performerIDs[performerIdx1WithScene]),
tests := []struct {
name string
filter models.MultiCriterionInput
includeIdxs []int
excludeIdxs []int
wantErr bool
}{
{
"includes",
models.MultiCriterionInput{
Value: []string{
strconv.Itoa(performerIDs[performerIdxWithScene]),
strconv.Itoa(performerIDs[performerIdx1WithScene]),
},
Modifier: models.CriterionModifierIncludes,
},
Modifier: models.CriterionModifierIncludes,
}
sceneFilter := models.SceneFilterType{
Performers: &performerCriterion,
}
scenes := queryScene(ctx, t, sqb, &sceneFilter, nil)
assert.Len(t, scenes, 2)
// ensure ids are correct
for _, scene := range scenes {
assert.True(t, scene.ID == sceneIDs[sceneIdxWithPerformer] || scene.ID == sceneIDs[sceneIdxWithTwoPerformers])
}
performerCriterion = models.MultiCriterionInput{
Value: []string{
strconv.Itoa(performerIDs[performerIdx1WithScene]),
strconv.Itoa(performerIDs[performerIdx2WithScene]),
[]int{
sceneIdxWithPerformer,
sceneIdxWithTwoPerformers,
},
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{
sceneIdxWithGallery,
},
Modifier: models.CriterionModifierExcludes,
}
false,
},
{
"includes all",
models.MultiCriterionInput{
Value: []string{
strconv.Itoa(performerIDs[performerIdx1WithScene]),
strconv.Itoa(performerIDs[performerIdx2WithScene]),
},
Modifier: models.CriterionModifierIncludesAll,
},
[]int{
sceneIdxWithTwoPerformers,
},
[]int{
sceneIdxWithPerformer,
},
false,
},
{
"excludes",
models.MultiCriterionInput{
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)
findFilter := models.FindFilterType{
Q: &q,
}
for _, tt := range tests {
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
assert := assert.New(t)
scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter)
assert.Len(t, scenes, 0)
results, err := db.Scene.Query(ctx, models.SceneQueryOptions{
SceneFilter: &models.SceneFilterType{
Performers: &tt.filter,
},
})
if (err != nil) != tt.wantErr {
t.Errorf("SceneStore.Query() error = %v, wantErr %v", err, tt.wantErr)
return
}
return nil
})
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) {
withTxn(func(ctx context.Context) error {
sqb := db.Scene
tagCriterion := models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(tagIDs[tagIdxWithScene]),
strconv.Itoa(tagIDs[tagIdx1WithScene]),
tests := []struct {
name string
filter models.HierarchicalMultiCriterionInput
includeIdxs []int
excludeIdxs []int
wantErr bool
}{
{
"includes",
models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(tagIDs[tagIdxWithScene]),
strconv.Itoa(tagIDs[tagIdx1WithScene]),
},
Modifier: models.CriterionModifierIncludes,
},
Modifier: models.CriterionModifierIncludes,
}
sceneFilter := models.SceneFilterType{
Tags: &tagCriterion,
}
scenes := queryScene(ctx, t, sqb, &sceneFilter, nil)
assert.Len(t, scenes, 2)
// ensure ids are correct
for _, scene := range scenes {
assert.True(t, scene.ID == sceneIDs[sceneIdxWithTag] || scene.ID == sceneIDs[sceneIdxWithTwoTags])
}
tagCriterion = models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithScene]),
strconv.Itoa(tagIDs[tagIdx2WithScene]),
[]int{
sceneIdxWithTag,
sceneIdxWithTwoTags,
},
Modifier: models.CriterionModifierIncludesAll,
}
scenes = queryScene(ctx, t, sqb, &sceneFilter, nil)
assert.Len(t, scenes, 1)
assert.Equal(t, sceneIDs[sceneIdxWithTwoTags], scenes[0].ID)
tagCriterion = models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithScene]),
[]int{
sceneIdxWithGallery,
},
Modifier: models.CriterionModifierExcludes,
}
false,
},
{
"includes all",
models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithScene]),
strconv.Itoa(tagIDs[tagIdx2WithScene]),
},
Modifier: models.CriterionModifierIncludesAll,
},
[]int{
sceneIdxWithTwoTags,
},
[]int{
sceneIdxWithTag,
},
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{
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,
},
}
q := getSceneStringValue(sceneIdxWithTwoTags, titleField)
findFilter := models.FindFilterType{
Q: &q,
}
for _, tt := range tests {
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
assert := assert.New(t)
scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter)
assert.Len(t, scenes, 0)
results, err := db.Scene.Query(ctx, models.SceneQueryOptions{
SceneFilter: &models.SceneFilterType{
Tags: &tt.filter,
},
})
if (err != nil) != tt.wantErr {
t.Errorf("SceneStore.Query() error = %v, wantErr %v", err, tt.wantErr)
return
}
return nil
})
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) {
withTxn(func(ctx context.Context) error {
sqb := db.Scene
tagCriterion := models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(tagIDs[tagIdxWithPerformer]),
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
allDepth := -1
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{
strconv.Itoa(tagIDs[tagIdxWithPerformer]),
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
},
Modifier: models.CriterionModifierIncludes,
},
},
Modifier: models.CriterionModifierIncludes,
}
sceneFilter := models.SceneFilterType{
PerformerTags: &tagCriterion,
}
scenes := queryScene(ctx, t, sqb, &sceneFilter, nil)
assert.Len(t, scenes, 2)
// ensure ids are correct
for _, scene := range scenes {
assert.True(t, scene.ID == sceneIDs[sceneIdxWithPerformerTag] || scene.ID == sceneIDs[sceneIdxWithPerformerTwoTags])
}
tagCriterion = models.HierarchicalMultiCriterionInput{
Value: []string{
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
strconv.Itoa(tagIDs[tagIdx2WithPerformer]),
[]int{
sceneIdxWithPerformerTag,
sceneIdxWithPerformerTwoTags,
sceneIdxWithTwoPerformerTag,
},
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{
sceneIdxWithPerformer,
},
Modifier: models.CriterionModifierExcludes,
}
false,
},
{
"includes sub-tags",
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{
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
strconv.Itoa(tagIDs[tagIdx2WithPerformer]),
},
Modifier: models.CriterionModifierIncludesAll,
},
},
[]int{
sceneIdxWithPerformerTwoTags,
},
[]int{
sceneIdxWithPerformer,
sceneIdxWithPerformerTag,
sceneIdxWithTwoPerformerTag,
},
false,
},
{
"excludes performer tag tagIdx2WithPerformer",
nil,
&models.SceneFilterType{
PerformerTags: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierExcludes,
Value: []string{strconv.Itoa(tagIDs[tagIdx2WithPerformer])},
},
},
nil,
[]int{sceneIdxWithTwoPerformerTag},
false,
},
{
"excludes sub-tags",
nil,
&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,
},
},
[]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(sceneIdxWithPerformerTwoTags, titleField)
findFilter := models.FindFilterType{
Q: &q,
}
for _, tt := range tests {
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
assert := assert.New(t)
scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter)
assert.Len(t, scenes, 0)
results, err := db.Scene.Query(ctx, models.SceneQueryOptions{
SceneFilter: tt.filter,
QueryOptions: models.QueryOptions{
FindFilter: tt.findFilter,
},
})
if (err != nil) != tt.wantErr {
t.Errorf("SceneStore.Query() error = %v, wantErr %v", err, tt.wantErr)
return
}
tagCriterion = models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIsNull,
}
q = getSceneStringValue(sceneIdx1WithPerformer, titleField)
include := indexesToIDs(sceneIDs, tt.includeIdxs)
exclude := indexesToIDs(sceneIDs, tt.excludeIdxs)
scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter)
assert.Len(t, scenes, 1)
assert.Equal(t, sceneIDs[sceneIdx1WithPerformer], scenes[0].ID)
q = getSceneStringValue(sceneIdxWithPerformerTag, titleField)
scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter)
assert.Len(t, scenes, 0)
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
})
for _, i := range include {
assert.Contains(results.IDs, i)
}
for _, e := range exclude {
assert.NotContains(results.IDs, e)
}
})
}
}
func TestSceneQueryStudio(t *testing.T) {
@ -3561,6 +3855,30 @@ func TestSceneQueryStudio(t *testing.T) {
[]int{sceneIDs[sceneIdxWithGallery]},
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

View file

@ -60,19 +60,24 @@ const (
sceneIdx1WithPerformer
sceneIdx2WithPerformer
sceneIdxWithTwoPerformers
sceneIdxWithThreePerformers
sceneIdxWithTag
sceneIdxWithTwoTags
sceneIdxWithThreeTags
sceneIdxWithMarkerAndTag
sceneIdxWithMarkerTwoTags
sceneIdxWithStudio
sceneIdx1WithStudio
sceneIdx2WithStudio
sceneIdxWithMarkers
sceneIdxWithPerformerTag
sceneIdxWithTwoPerformerTag
sceneIdxWithPerformerTwoTags
sceneIdxWithSpacedName
sceneIdxWithStudioPerformer
sceneIdxWithGrandChildStudio
sceneIdxMissingPhash
sceneIdxWithPerformerParentTag
// new indexes above
lastSceneIdx
@ -90,16 +95,20 @@ const (
imageIdx1WithPerformer
imageIdx2WithPerformer
imageIdxWithTwoPerformers
imageIdxWithThreePerformers
imageIdxWithTag
imageIdxWithTwoTags
imageIdxWithThreeTags
imageIdxWithStudio
imageIdx1WithStudio
imageIdx2WithStudio
imageIdxWithStudioPerformer
imageIdxInZip
imageIdxWithPerformerTag
imageIdxWithTwoPerformerTag
imageIdxWithPerformerTwoTags
imageIdxWithGrandChildStudio
imageIdxWithPerformerParentTag
// new indexes above
totalImages
)
@ -108,20 +117,25 @@ const (
performerIdxWithScene = iota
performerIdx1WithScene
performerIdx2WithScene
performerIdx3WithScene
performerIdxWithTwoScenes
performerIdxWithImage
performerIdxWithTwoImages
performerIdx1WithImage
performerIdx2WithImage
performerIdx3WithImage
performerIdxWithTag
performerIdx2WithTag
performerIdxWithTwoTags
performerIdxWithGallery
performerIdxWithTwoGalleries
performerIdx1WithGallery
performerIdx2WithGallery
performerIdx3WithGallery
performerIdxWithSceneStudio
performerIdxWithImageStudio
performerIdxWithGalleryStudio
performerIdxWithParentTag
// new indexes above
// performers with dup names start from the end
performerIdx1WithDupName
@ -155,16 +169,20 @@ const (
galleryIdx1WithPerformer
galleryIdx2WithPerformer
galleryIdxWithTwoPerformers
galleryIdxWithThreePerformers
galleryIdxWithTag
galleryIdxWithTwoTags
galleryIdxWithThreeTags
galleryIdxWithStudio
galleryIdx1WithStudio
galleryIdx2WithStudio
galleryIdxWithPerformerTag
galleryIdxWithTwoPerformerTag
galleryIdxWithPerformerTwoTags
galleryIdxWithStudioPerformer
galleryIdxWithGrandChildStudio
galleryIdxWithoutFile
galleryIdxWithPerformerParentTag
// new indexes above
lastGalleryIdx
@ -182,12 +200,14 @@ const (
tagIdxWithImage
tagIdx1WithImage
tagIdx2WithImage
tagIdx3WithImage
tagIdxWithPerformer
tagIdx1WithPerformer
tagIdx2WithPerformer
tagIdxWithGallery
tagIdx1WithGallery
tagIdx2WithGallery
tagIdx3WithGallery
tagIdxWithChildTag
tagIdxWithParentTag
tagIdxWithGrandChild
@ -332,19 +352,24 @@ var (
var (
sceneTags = linkMap{
sceneIdxWithTag: {tagIdxWithScene},
sceneIdxWithTwoTags: {tagIdx1WithScene, tagIdx2WithScene},
sceneIdxWithMarkerAndTag: {tagIdx3WithScene},
sceneIdxWithTag: {tagIdxWithScene},
sceneIdxWithTwoTags: {tagIdx1WithScene, tagIdx2WithScene},
sceneIdxWithThreeTags: {tagIdx1WithScene, tagIdx2WithScene, tagIdx3WithScene},
sceneIdxWithMarkerAndTag: {tagIdx3WithScene},
sceneIdxWithMarkerTwoTags: {tagIdx2WithScene, tagIdx3WithScene},
}
scenePerformers = linkMap{
sceneIdxWithPerformer: {performerIdxWithScene},
sceneIdxWithTwoPerformers: {performerIdx1WithScene, performerIdx2WithScene},
sceneIdxWithPerformerTag: {performerIdxWithTag},
sceneIdxWithPerformerTwoTags: {performerIdxWithTwoTags},
sceneIdx1WithPerformer: {performerIdxWithTwoScenes},
sceneIdx2WithPerformer: {performerIdxWithTwoScenes},
sceneIdxWithStudioPerformer: {performerIdxWithSceneStudio},
sceneIdxWithPerformer: {performerIdxWithScene},
sceneIdxWithTwoPerformers: {performerIdx1WithScene, performerIdx2WithScene},
sceneIdxWithThreePerformers: {performerIdx1WithScene, performerIdx2WithScene, performerIdx3WithScene},
sceneIdxWithPerformerTag: {performerIdxWithTag},
sceneIdxWithTwoPerformerTag: {performerIdxWithTag, performerIdx2WithTag},
sceneIdxWithPerformerTwoTags: {performerIdxWithTwoTags},
sceneIdx1WithPerformer: {performerIdxWithTwoScenes},
sceneIdx2WithPerformer: {performerIdxWithTwoScenes},
sceneIdxWithStudioPerformer: {performerIdxWithSceneStudio},
sceneIdxWithPerformerParentTag: {performerIdxWithParentTag},
}
sceneGalleries = linkMap{
@ -376,6 +401,7 @@ var (
{sceneIdxWithMarkers, tagIdxWithPrimaryMarkers, nil},
{sceneIdxWithMarkers, tagIdxWithPrimaryMarkers, []int{tagIdxWithMarkers}},
{sceneIdxWithMarkerAndTag, tagIdxWithPrimaryMarkers, nil},
{sceneIdxWithMarkerTwoTags, tagIdxWithPrimaryMarkers, nil},
}
)
@ -407,29 +433,36 @@ var (
imageIdxWithGrandChildStudio: studioIdxWithGrandParent,
}
imageTags = linkMap{
imageIdxWithTag: {tagIdxWithImage},
imageIdxWithTwoTags: {tagIdx1WithImage, tagIdx2WithImage},
imageIdxWithTag: {tagIdxWithImage},
imageIdxWithTwoTags: {tagIdx1WithImage, tagIdx2WithImage},
imageIdxWithThreeTags: {tagIdx1WithImage, tagIdx2WithImage, tagIdx3WithImage},
}
imagePerformers = linkMap{
imageIdxWithPerformer: {performerIdxWithImage},
imageIdxWithTwoPerformers: {performerIdx1WithImage, performerIdx2WithImage},
imageIdxWithPerformerTag: {performerIdxWithTag},
imageIdxWithPerformerTwoTags: {performerIdxWithTwoTags},
imageIdx1WithPerformer: {performerIdxWithTwoImages},
imageIdx2WithPerformer: {performerIdxWithTwoImages},
imageIdxWithStudioPerformer: {performerIdxWithImageStudio},
imageIdxWithPerformer: {performerIdxWithImage},
imageIdxWithTwoPerformers: {performerIdx1WithImage, performerIdx2WithImage},
imageIdxWithThreePerformers: {performerIdx1WithImage, performerIdx2WithImage, performerIdx3WithImage},
imageIdxWithPerformerTag: {performerIdxWithTag},
imageIdxWithTwoPerformerTag: {performerIdxWithTag, performerIdx2WithTag},
imageIdxWithPerformerTwoTags: {performerIdxWithTwoTags},
imageIdx1WithPerformer: {performerIdxWithTwoImages},
imageIdx2WithPerformer: {performerIdxWithTwoImages},
imageIdxWithStudioPerformer: {performerIdxWithImageStudio},
imageIdxWithPerformerParentTag: {performerIdxWithParentTag},
}
)
var (
galleryPerformers = linkMap{
galleryIdxWithPerformer: {performerIdxWithGallery},
galleryIdxWithTwoPerformers: {performerIdx1WithGallery, performerIdx2WithGallery},
galleryIdxWithPerformerTag: {performerIdxWithTag},
galleryIdxWithPerformerTwoTags: {performerIdxWithTwoTags},
galleryIdx1WithPerformer: {performerIdxWithTwoGalleries},
galleryIdx2WithPerformer: {performerIdxWithTwoGalleries},
galleryIdxWithStudioPerformer: {performerIdxWithGalleryStudio},
galleryIdxWithPerformer: {performerIdxWithGallery},
galleryIdxWithTwoPerformers: {performerIdx1WithGallery, performerIdx2WithGallery},
galleryIdxWithThreePerformers: {performerIdx1WithGallery, performerIdx2WithGallery, performerIdx3WithGallery},
galleryIdxWithPerformerTag: {performerIdxWithTag},
galleryIdxWithTwoPerformerTag: {performerIdxWithTag, performerIdx2WithTag},
galleryIdxWithPerformerTwoTags: {performerIdxWithTwoTags},
galleryIdx1WithPerformer: {performerIdxWithTwoGalleries},
galleryIdx2WithPerformer: {performerIdxWithTwoGalleries},
galleryIdxWithStudioPerformer: {performerIdxWithGalleryStudio},
galleryIdxWithPerformerParentTag: {performerIdxWithParentTag},
}
galleryStudios = map[int]int{
@ -441,8 +474,9 @@ var (
}
galleryTags = linkMap{
galleryIdxWithTag: {tagIdxWithGallery},
galleryIdxWithTwoTags: {tagIdx1WithGallery, tagIdx2WithGallery},
galleryIdxWithTag: {tagIdxWithGallery},
galleryIdxWithTwoTags: {tagIdx1WithGallery, tagIdx2WithGallery},
galleryIdxWithThreeTags: {tagIdx1WithGallery, tagIdx2WithGallery, tagIdx3WithGallery},
}
)
@ -462,8 +496,10 @@ var (
var (
performerTags = linkMap{
performerIdxWithTag: {tagIdxWithPerformer},
performerIdxWithTwoTags: {tagIdx1WithPerformer, tagIdx2WithPerformer},
performerIdxWithTag: {tagIdxWithPerformer},
performerIdx2WithTag: {tagIdx2WithPerformer},
performerIdxWithTwoTags: {tagIdx1WithPerformer, tagIdx2WithPerformer},
performerIdxWithParentTag: {tagIdxWithParentAndChild},
}
)
@ -484,6 +520,16 @@ func indexesToIDs(ids []int, indexes []int) []int {
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
func TestMain(m *testing.M) {
@ -1431,11 +1477,8 @@ func getTagStringValue(index int, field string) string {
}
func getTagSceneCount(id int) int {
if id == tagIDs[tagIdx1WithScene] || id == tagIDs[tagIdx2WithScene] || id == tagIDs[tagIdxWithScene] || id == tagIDs[tagIdx3WithScene] {
return 1
}
return 0
idx := indexFromID(tagIDs, id)
return len(sceneTags.reverseLookup(idx))
}
func getTagMarkerCount(id int) int {
@ -1451,27 +1494,18 @@ func getTagMarkerCount(id int) int {
}
func getTagImageCount(id int) int {
if id == tagIDs[tagIdx1WithImage] || id == tagIDs[tagIdx2WithImage] || id == tagIDs[tagIdxWithImage] {
return 1
}
return 0
idx := indexFromID(tagIDs, id)
return len(imageTags.reverseLookup(idx))
}
func getTagGalleryCount(id int) int {
if id == tagIDs[tagIdx1WithGallery] || id == tagIDs[tagIdx2WithGallery] || id == tagIDs[tagIdxWithGallery] {
return 1
}
return 0
idx := indexFromID(tagIDs, id)
return len(galleryTags.reverseLookup(idx))
}
func getTagPerformerCount(id int) int {
if id == tagIDs[tagIdx1WithPerformer] || id == tagIDs[tagIdx2WithPerformer] || id == tagIDs[tagIdxWithPerformer] {
return 1
}
return 0
idx := indexFromID(tagIDs, id)
return len(performerTags.reverseLookup(idx))
}
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) {
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 {
var notClause string
if tags.Modifier == models.CriterionModifierNotNull {
@ -489,43 +499,88 @@ func tagParentsCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalMu
return
}
if len(tags.Value) == 0 {
if len(tags.Value) == 0 && len(tags.Excludes) == 0 {
return
}
var args []interface{}
for _, val := range tags.Value {
args = append(args, val)
if len(tags.Value) > 0 {
var args []interface{}
for _, val := range tags.Value {
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 := `parents 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.Value)) + `
UNION
SELECT root_id, child_id, depth + 1 FROM tags_relations INNER JOIN parents ON item_id = parent_id ` + depthCondition + `
)`
f.addRecursiveWith(query, args...)
f.addLeftJoin("parents", "", "parents.item_id = tags.id")
addHierarchicalConditionClauses(f, tags, "parents", "root_id")
}
depthVal := 0
if tags.Depth != nil {
depthVal = *tags.Depth
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")
}
var depthCondition string
if depthVal != -1 {
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
}
query := `parents 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.Value)) + `
UNION
SELECT root_id, child_id, depth + 1 FROM tags_relations INNER JOIN parents ON item_id = parent_id ` + depthCondition + `
)`
f.addRecursiveWith(query, args...)
f.addLeftJoin("parents", "", "parents.item_id = tags.id")
addHierarchicalConditionClauses(f, *tags, "parents", "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) {
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 {
var notClause string
if tags.Modifier == models.CriterionModifierNotNull {
@ -538,36 +593,71 @@ func tagChildrenCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalM
return
}
if len(tags.Value) == 0 {
if len(tags.Value) == 0 && len(tags.Excludes) == 0 {
return
}
var args []interface{}
for _, val := range tags.Value {
args = append(args, val)
if len(tags.Value) > 0 {
var args []interface{}
for _, val := range tags.Value {
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 := `children 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.Value)) + `
UNION
SELECT root_id, parent_id, depth + 1 FROM tags_relations INNER JOIN children ON item_id = child_id ` + depthCondition + `
)`
f.addRecursiveWith(query, args...)
f.addLeftJoin("children", "", "children.item_id = tags.id")
addHierarchicalConditionClauses(f, tags, "children", "root_id")
}
depthVal := 0
if tags.Depth != nil {
depthVal = *tags.Depth
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")
}
var depthCondition string
if depthVal != -1 {
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
}
query := `children 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.Value)) + `
UNION
SELECT root_id, parent_id, depth + 1 FROM tags_relations INNER JOIN children ON item_id = child_id ` + depthCondition + `
)`
f.addRecursiveWith(query, args...)
f.addLeftJoin("children", "", "children.item_id = tags.id")
addHierarchicalConditionClauses(f, *tags, "children", "root_id")
}
}
}

View file

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

View file

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

View file

@ -567,6 +567,11 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
protected toCriterionInput(): HierarchicalMultiCriterionInput {
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) {
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),
excludes: excludes,
modifier: this.modifier,
depth: this.value.depth,
depth,
};
}

View file

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