mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
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:
@@ -135,6 +135,17 @@ type HierarchicalMultiCriterionInput struct {
|
|||||||
Excludes []string `json:"excludes"`
|
Excludes []string `json:"excludes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i HierarchicalMultiCriterionInput) CombineExcludes() HierarchicalMultiCriterionInput {
|
||||||
|
ii := i
|
||||||
|
if ii.Modifier == CriterionModifierExcludes {
|
||||||
|
ii.Modifier = CriterionModifierIncludesAll
|
||||||
|
ii.Excludes = append(ii.Excludes, ii.Value...)
|
||||||
|
ii.Value = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ii
|
||||||
|
}
|
||||||
|
|
||||||
type MultiCriterionInput struct {
|
type MultiCriterionInput struct {
|
||||||
Value []string `json:"value"`
|
Value []string `json:"value"`
|
||||||
Modifier CriterionModifier `json:"modifier"`
|
Modifier CriterionModifier `json:"modifier"`
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
@@ -694,6 +693,8 @@ func (m *joinedMultiCriterionHandlerBuilder) handler(c *models.MultiCriterionInp
|
|||||||
})
|
})
|
||||||
havingClause = fmt.Sprintf("count(distinct %s.%s) IS %d", joinAlias, m.foreignFK, len(criterion.Value))
|
havingClause = fmt.Sprintf("count(distinct %s.%s) IS %d", joinAlias, m.foreignFK, len(criterion.Value))
|
||||||
args = append(args, len(criterion.Value))
|
args = append(args, len(criterion.Value))
|
||||||
|
case models.CriterionModifierNotEquals:
|
||||||
|
f.setError(fmt.Errorf("not equals modifier is not supported for multi criterion input"))
|
||||||
case models.CriterionModifierIncludesAll:
|
case models.CriterionModifierIncludesAll:
|
||||||
// includes all of the provided ids
|
// includes all of the provided ids
|
||||||
m.addJoinTable(f)
|
m.addJoinTable(f)
|
||||||
@@ -830,6 +831,33 @@ func (m *stringListCriterionHandlerBuilder) handler(criterion *models.StringCrit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func studioCriterionHandler(primaryTable string, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||||
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
|
if studios == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
studiosCopy := *studios
|
||||||
|
switch studiosCopy.Modifier {
|
||||||
|
case models.CriterionModifierEquals:
|
||||||
|
studiosCopy.Modifier = models.CriterionModifierIncludesAll
|
||||||
|
case models.CriterionModifierNotEquals:
|
||||||
|
studiosCopy.Modifier = models.CriterionModifierExcludes
|
||||||
|
}
|
||||||
|
|
||||||
|
hh := hierarchicalMultiCriterionHandlerBuilder{
|
||||||
|
tx: dbWrapper{},
|
||||||
|
|
||||||
|
primaryTable: primaryTable,
|
||||||
|
foreignTable: studioTable,
|
||||||
|
foreignFK: studioIDColumn,
|
||||||
|
parentFK: "parent_id",
|
||||||
|
}
|
||||||
|
|
||||||
|
hh.handler(&studiosCopy)(ctx, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type hierarchicalMultiCriterionHandlerBuilder struct {
|
type hierarchicalMultiCriterionHandlerBuilder struct {
|
||||||
tx dbWrapper
|
tx dbWrapper
|
||||||
|
|
||||||
@@ -838,12 +866,20 @@ type hierarchicalMultiCriterionHandlerBuilder struct {
|
|||||||
foreignFK string
|
foreignFK string
|
||||||
|
|
||||||
parentFK string
|
parentFK string
|
||||||
|
childFK string
|
||||||
relationsTable string
|
relationsTable string
|
||||||
}
|
}
|
||||||
|
|
||||||
func getHierarchicalValues(ctx context.Context, tx dbWrapper, values []string, table, relationsTable, parentFK string, depth *int) string {
|
func getHierarchicalValues(ctx context.Context, tx dbWrapper, values []string, table, relationsTable, parentFK string, childFK string, depth *int) (string, error) {
|
||||||
var args []interface{}
|
var args []interface{}
|
||||||
|
|
||||||
|
if parentFK == "" {
|
||||||
|
parentFK = "parent_id"
|
||||||
|
}
|
||||||
|
if childFK == "" {
|
||||||
|
childFK = "child_id"
|
||||||
|
}
|
||||||
|
|
||||||
depthVal := 0
|
depthVal := 0
|
||||||
if depth != nil {
|
if depth != nil {
|
||||||
depthVal = *depth
|
depthVal = *depth
|
||||||
@@ -865,7 +901,7 @@ func getHierarchicalValues(ctx context.Context, tx dbWrapper, values []string, t
|
|||||||
}
|
}
|
||||||
|
|
||||||
if valid {
|
if valid {
|
||||||
return "VALUES" + strings.Join(valuesClauses, ",")
|
return "VALUES" + strings.Join(valuesClauses, ","), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -885,13 +921,14 @@ func getHierarchicalValues(ctx context.Context, tx dbWrapper, values []string, t
|
|||||||
"inBinding": getInBinding(inCount),
|
"inBinding": getInBinding(inCount),
|
||||||
"recursiveSelect": "",
|
"recursiveSelect": "",
|
||||||
"parentFK": parentFK,
|
"parentFK": parentFK,
|
||||||
|
"childFK": childFK,
|
||||||
"depthCondition": depthCondition,
|
"depthCondition": depthCondition,
|
||||||
"unionClause": "",
|
"unionClause": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if relationsTable != "" {
|
if relationsTable != "" {
|
||||||
withClauseMap["recursiveSelect"] = utils.StrFormat(`SELECT p.root_id, c.child_id, depth + 1 FROM {relationsTable} AS c
|
withClauseMap["recursiveSelect"] = utils.StrFormat(`SELECT p.root_id, c.{childFK}, depth + 1 FROM {relationsTable} AS c
|
||||||
INNER JOIN items as p ON c.parent_id = p.item_id
|
INNER JOIN items as p ON c.{parentFK} = p.item_id
|
||||||
`, withClauseMap)
|
`, withClauseMap)
|
||||||
} else {
|
} else {
|
||||||
withClauseMap["recursiveSelect"] = utils.StrFormat(`SELECT p.root_id, c.id, depth + 1 FROM {table} as c
|
withClauseMap["recursiveSelect"] = utils.StrFormat(`SELECT p.root_id, c.id, depth + 1 FROM {table} as c
|
||||||
@@ -916,12 +953,10 @@ WHERE id in {inBinding}
|
|||||||
var valuesClause string
|
var valuesClause string
|
||||||
err := tx.Get(ctx, &valuesClause, query, args...)
|
err := tx.Get(ctx, &valuesClause, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(err)
|
return "", fmt.Errorf("failed to get hierarchical values: %w", err)
|
||||||
// return record which never matches so we don't have to handle error here
|
|
||||||
return "VALUES(NULL, NULL)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return valuesClause
|
return valuesClause, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func addHierarchicalConditionClauses(f *filterBuilder, criterion models.HierarchicalMultiCriterionInput, table, idColumn string) {
|
func addHierarchicalConditionClauses(f *filterBuilder, criterion models.HierarchicalMultiCriterionInput, table, idColumn string) {
|
||||||
@@ -942,6 +977,12 @@ func (m *hierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hierarchica
|
|||||||
// make a copy so we don't modify the original
|
// make a copy so we don't modify the original
|
||||||
criterion := *c
|
criterion := *c
|
||||||
|
|
||||||
|
// don't support equals/not equals
|
||||||
|
if criterion.Modifier == models.CriterionModifierEquals || criterion.Modifier == models.CriterionModifierNotEquals {
|
||||||
|
f.setError(fmt.Errorf("modifier %s is not supported for hierarchical multi criterion", criterion.Modifier))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
|
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
|
||||||
var notClause string
|
var notClause string
|
||||||
if criterion.Modifier == models.CriterionModifierNotNull {
|
if criterion.Modifier == models.CriterionModifierNotNull {
|
||||||
@@ -968,7 +1009,11 @@ func (m *hierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hierarchica
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(criterion.Value) > 0 {
|
if len(criterion.Value) > 0 {
|
||||||
valuesClause := getHierarchicalValues(ctx, m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, criterion.Depth)
|
valuesClause, err := getHierarchicalValues(ctx, m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, m.childFK, criterion.Depth)
|
||||||
|
if err != nil {
|
||||||
|
f.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch criterion.Modifier {
|
switch criterion.Modifier {
|
||||||
case models.CriterionModifierIncludes:
|
case models.CriterionModifierIncludes:
|
||||||
@@ -980,7 +1025,11 @@ func (m *hierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hierarchica
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(criterion.Excludes) > 0 {
|
if len(criterion.Excludes) > 0 {
|
||||||
valuesClause := getHierarchicalValues(ctx, m.tx, criterion.Excludes, m.foreignTable, m.relationsTable, m.parentFK, criterion.Depth)
|
valuesClause, err := getHierarchicalValues(ctx, m.tx, criterion.Excludes, m.foreignTable, m.relationsTable, m.parentFK, m.childFK, criterion.Depth)
|
||||||
|
if err != nil {
|
||||||
|
f.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
f.addWhere(fmt.Sprintf("%s.%s NOT IN (SELECT column2 FROM (%s)) OR %[1]s.%[2]s IS NULL", m.primaryTable, m.foreignFK, valuesClause))
|
f.addWhere(fmt.Sprintf("%s.%s NOT IN (SELECT column2 FROM (%s)) OR %[1]s.%[2]s IS NULL", m.primaryTable, m.foreignFK, valuesClause))
|
||||||
}
|
}
|
||||||
@@ -992,10 +1041,12 @@ type joinedHierarchicalMultiCriterionHandlerBuilder struct {
|
|||||||
tx dbWrapper
|
tx dbWrapper
|
||||||
|
|
||||||
primaryTable string
|
primaryTable string
|
||||||
|
primaryKey string
|
||||||
foreignTable string
|
foreignTable string
|
||||||
foreignFK string
|
foreignFK string
|
||||||
|
|
||||||
parentFK string
|
parentFK string
|
||||||
|
childFK string
|
||||||
relationsTable string
|
relationsTable string
|
||||||
|
|
||||||
joinAs string
|
joinAs string
|
||||||
@@ -1004,16 +1055,25 @@ type joinedHierarchicalMultiCriterionHandlerBuilder struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *joinedHierarchicalMultiCriterionHandlerBuilder) addHierarchicalConditionClauses(f *filterBuilder, criterion models.HierarchicalMultiCriterionInput, table, idColumn string) {
|
func (m *joinedHierarchicalMultiCriterionHandlerBuilder) addHierarchicalConditionClauses(f *filterBuilder, criterion models.HierarchicalMultiCriterionInput, table, idColumn string) {
|
||||||
if criterion.Modifier == models.CriterionModifierEquals {
|
primaryKey := m.primaryKey
|
||||||
|
if primaryKey == "" {
|
||||||
|
primaryKey = "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch criterion.Modifier {
|
||||||
|
case models.CriterionModifierEquals:
|
||||||
// includes only the provided ids
|
// includes only the provided ids
|
||||||
f.addWhere(fmt.Sprintf("%s.%s IS NOT NULL", table, idColumn))
|
f.addWhere(fmt.Sprintf("%s.%s IS NOT NULL", table, idColumn))
|
||||||
f.addHaving(fmt.Sprintf("count(distinct %s.%s) IS %d", table, idColumn, len(criterion.Value)))
|
f.addHaving(fmt.Sprintf("count(distinct %s.%s) IS %d", table, idColumn, len(criterion.Value)))
|
||||||
f.addWhere(utils.StrFormat("(SELECT COUNT(*) FROM {joinTable} s WHERE s.{primaryFK} = {primaryTable}.id) = ?", utils.StrFormatMap{
|
f.addWhere(utils.StrFormat("(SELECT COUNT(*) FROM {joinTable} s WHERE s.{primaryFK} = {primaryTable}.{primaryKey}) = ?", utils.StrFormatMap{
|
||||||
"joinTable": m.joinTable,
|
"joinTable": m.joinTable,
|
||||||
"primaryFK": m.primaryFK,
|
"primaryFK": m.primaryFK,
|
||||||
"primaryTable": m.primaryTable,
|
"primaryTable": m.primaryTable,
|
||||||
|
"primaryKey": primaryKey,
|
||||||
}), len(criterion.Value))
|
}), len(criterion.Value))
|
||||||
} else {
|
case models.CriterionModifierNotEquals:
|
||||||
|
f.setError(fmt.Errorf("not equals modifier is not supported for hierarchical multi criterion input"))
|
||||||
|
default:
|
||||||
addHierarchicalConditionClauses(f, criterion, table, idColumn)
|
addHierarchicalConditionClauses(f, criterion, table, idColumn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1024,6 +1084,15 @@ func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hiera
|
|||||||
// make a copy so we don't modify the original
|
// make a copy so we don't modify the original
|
||||||
criterion := *c
|
criterion := *c
|
||||||
joinAlias := m.joinAs
|
joinAlias := m.joinAs
|
||||||
|
primaryKey := m.primaryKey
|
||||||
|
if primaryKey == "" {
|
||||||
|
primaryKey = "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
if criterion.Modifier == models.CriterionModifierEquals && criterion.Depth != nil && *criterion.Depth != 0 {
|
||||||
|
f.setError(fmt.Errorf("depth is not supported for equals modifier in hierarchical multi criterion input"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
|
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
|
||||||
var notClause string
|
var notClause string
|
||||||
@@ -1031,7 +1100,7 @@ func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hiera
|
|||||||
notClause = "NOT"
|
notClause = "NOT"
|
||||||
}
|
}
|
||||||
|
|
||||||
f.addLeftJoin(m.joinTable, joinAlias, fmt.Sprintf("%s.%s = %s.id", joinAlias, m.primaryFK, m.primaryTable))
|
f.addLeftJoin(m.joinTable, joinAlias, fmt.Sprintf("%s.%s = %s.%s", joinAlias, m.primaryFK, m.primaryTable, primaryKey))
|
||||||
|
|
||||||
f.addWhere(utils.StrFormat("{table}.{column} IS {not} NULL", utils.StrFormatMap{
|
f.addWhere(utils.StrFormat("{table}.{column} IS {not} NULL", utils.StrFormatMap{
|
||||||
"table": joinAlias,
|
"table": joinAlias,
|
||||||
@@ -1053,7 +1122,11 @@ func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hiera
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(criterion.Value) > 0 {
|
if len(criterion.Value) > 0 {
|
||||||
valuesClause := getHierarchicalValues(ctx, m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, criterion.Depth)
|
valuesClause, err := getHierarchicalValues(ctx, m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, m.childFK, criterion.Depth)
|
||||||
|
if err != nil {
|
||||||
|
f.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
joinTable := utils.StrFormat(`(
|
joinTable := utils.StrFormat(`(
|
||||||
SELECT j.*, d.column1 AS root_id, d.column2 AS item_id FROM {joinTable} AS j
|
SELECT j.*, d.column1 AS root_id, d.column2 AS item_id FROM {joinTable} AS j
|
||||||
@@ -1065,13 +1138,17 @@ func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hiera
|
|||||||
"valuesClause": valuesClause,
|
"valuesClause": valuesClause,
|
||||||
})
|
})
|
||||||
|
|
||||||
f.addLeftJoin(joinTable, joinAlias, fmt.Sprintf("%s.%s = %s.id", joinAlias, m.primaryFK, m.primaryTable))
|
f.addLeftJoin(joinTable, joinAlias, fmt.Sprintf("%s.%s = %s.%s", joinAlias, m.primaryFK, m.primaryTable, primaryKey))
|
||||||
|
|
||||||
m.addHierarchicalConditionClauses(f, criterion, joinAlias, "root_id")
|
m.addHierarchicalConditionClauses(f, criterion, joinAlias, "root_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(criterion.Excludes) > 0 {
|
if len(criterion.Excludes) > 0 {
|
||||||
valuesClause := getHierarchicalValues(ctx, m.tx, criterion.Excludes, m.foreignTable, m.relationsTable, m.parentFK, criterion.Depth)
|
valuesClause, err := getHierarchicalValues(ctx, m.tx, criterion.Excludes, m.foreignTable, m.relationsTable, m.parentFK, m.childFK, criterion.Depth)
|
||||||
|
if err != nil {
|
||||||
|
f.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
joinTable := utils.StrFormat(`(
|
joinTable := utils.StrFormat(`(
|
||||||
SELECT j2.*, e.column1 AS root_id, e.column2 AS item_id FROM {joinTable} AS j2
|
SELECT j2.*, e.column1 AS root_id, e.column2 AS item_id FROM {joinTable} AS j2
|
||||||
@@ -1085,7 +1162,7 @@ func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hiera
|
|||||||
|
|
||||||
joinAlias2 := joinAlias + "2"
|
joinAlias2 := joinAlias + "2"
|
||||||
|
|
||||||
f.addLeftJoin(joinTable, joinAlias2, fmt.Sprintf("%s.%s = %s.id", joinAlias2, m.primaryFK, m.primaryTable))
|
f.addLeftJoin(joinTable, joinAlias2, fmt.Sprintf("%s.%s = %s.%s", joinAlias2, m.primaryFK, m.primaryTable, primaryKey))
|
||||||
|
|
||||||
// modify for exclusion
|
// modify for exclusion
|
||||||
criterionCopy := criterion
|
criterionCopy := criterion
|
||||||
@@ -1098,6 +1175,83 @@ func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(c *models.Hiera
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type joinedPerformerTagsHandler struct {
|
||||||
|
criterion *models.HierarchicalMultiCriterionInput
|
||||||
|
|
||||||
|
primaryTable string // eg scenes
|
||||||
|
joinTable string // eg performers_scenes
|
||||||
|
joinPrimaryKey string // eg scene_id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *joinedPerformerTagsHandler) handle(ctx context.Context, f *filterBuilder) {
|
||||||
|
tags := h.criterion
|
||||||
|
|
||||||
|
if tags != nil {
|
||||||
|
criterion := tags.CombineExcludes()
|
||||||
|
|
||||||
|
// validate the modifier
|
||||||
|
switch criterion.Modifier {
|
||||||
|
case models.CriterionModifierIncludesAll, models.CriterionModifierIncludes, models.CriterionModifierExcludes, models.CriterionModifierIsNull, models.CriterionModifierNotNull:
|
||||||
|
// valid
|
||||||
|
default:
|
||||||
|
f.setError(fmt.Errorf("invalid modifier %s for performer tags", criterion.Modifier))
|
||||||
|
}
|
||||||
|
|
||||||
|
strFormatMap := utils.StrFormatMap{
|
||||||
|
"primaryTable": h.primaryTable,
|
||||||
|
"joinTable": h.joinTable,
|
||||||
|
"joinPrimaryKey": h.joinPrimaryKey,
|
||||||
|
"inBinding": getInBinding(len(criterion.Value)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
|
||||||
|
var notClause string
|
||||||
|
if criterion.Modifier == models.CriterionModifierNotNull {
|
||||||
|
notClause = "NOT"
|
||||||
|
}
|
||||||
|
|
||||||
|
f.addLeftJoin(h.joinTable, "", utils.StrFormat("{primaryTable}.id = {joinTable}.{joinPrimaryKey}", strFormatMap))
|
||||||
|
f.addLeftJoin("performers_tags", "", utils.StrFormat("{joinTable}.performer_id = performers_tags.performer_id", strFormatMap))
|
||||||
|
|
||||||
|
f.addWhere(fmt.Sprintf("performers_tags.tag_id IS %s NULL", notClause))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(criterion.Value) == 0 && len(criterion.Excludes) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(criterion.Value) > 0 {
|
||||||
|
valuesClause, err := getHierarchicalValues(ctx, dbWrapper{}, criterion.Value, tagTable, "tags_relations", "", "", criterion.Depth)
|
||||||
|
if err != nil {
|
||||||
|
f.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f.addWith(utils.StrFormat(`performer_tags AS (
|
||||||
|
SELECT ps.{joinPrimaryKey} as primaryID, t.column1 AS root_tag_id FROM {joinTable} ps
|
||||||
|
INNER JOIN performers_tags pt ON pt.performer_id = ps.performer_id
|
||||||
|
INNER JOIN (`+valuesClause+`) t ON t.column2 = pt.tag_id
|
||||||
|
)`, strFormatMap))
|
||||||
|
|
||||||
|
f.addLeftJoin("performer_tags", "", utils.StrFormat("performer_tags.primaryID = {primaryTable}.id", strFormatMap))
|
||||||
|
|
||||||
|
addHierarchicalConditionClauses(f, criterion, "performer_tags", "root_tag_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(criterion.Excludes) > 0 {
|
||||||
|
valuesClause, err := getHierarchicalValues(ctx, dbWrapper{}, criterion.Excludes, tagTable, "tags_relations", "", "", criterion.Depth)
|
||||||
|
if err != nil {
|
||||||
|
f.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clause := utils.StrFormat("{primaryTable}.id NOT IN (SELECT {joinTable}.{joinPrimaryKey} FROM {joinTable} INNER JOIN performers_tags ON {joinTable}.performer_id = performers_tags.performer_id WHERE performers_tags.tag_id IN (SELECT column2 FROM (%s)))", strFormatMap)
|
||||||
|
f.addWhere(fmt.Sprintf(clause, valuesClause))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type stashIDCriterionHandler struct {
|
type stashIDCriterionHandler struct {
|
||||||
c *models.StashIDCriterionInput
|
c *models.StashIDCriterionInput
|
||||||
stashIDRepository *stashIDRepository
|
stashIDRepository *stashIDRepository
|
||||||
|
|||||||
@@ -670,7 +670,7 @@ func (qb *GalleryStore) makeFilter(ctx context.Context, galleryFilter *models.Ga
|
|||||||
query.handleCriterion(ctx, galleryPerformersCriterionHandler(qb, galleryFilter.Performers))
|
query.handleCriterion(ctx, galleryPerformersCriterionHandler(qb, galleryFilter.Performers))
|
||||||
query.handleCriterion(ctx, galleryPerformerCountCriterionHandler(qb, galleryFilter.PerformerCount))
|
query.handleCriterion(ctx, galleryPerformerCountCriterionHandler(qb, galleryFilter.PerformerCount))
|
||||||
query.handleCriterion(ctx, hasChaptersCriterionHandler(galleryFilter.HasChapters))
|
query.handleCriterion(ctx, hasChaptersCriterionHandler(galleryFilter.HasChapters))
|
||||||
query.handleCriterion(ctx, galleryStudioCriterionHandler(qb, galleryFilter.Studios))
|
query.handleCriterion(ctx, studioCriterionHandler(galleryTable, galleryFilter.Studios))
|
||||||
query.handleCriterion(ctx, galleryPerformerTagsCriterionHandler(qb, galleryFilter.PerformerTags))
|
query.handleCriterion(ctx, galleryPerformerTagsCriterionHandler(qb, galleryFilter.PerformerTags))
|
||||||
query.handleCriterion(ctx, galleryAverageResolutionCriterionHandler(qb, galleryFilter.AverageResolution))
|
query.handleCriterion(ctx, galleryAverageResolutionCriterionHandler(qb, galleryFilter.AverageResolution))
|
||||||
query.handleCriterion(ctx, galleryImageCountCriterionHandler(qb, galleryFilter.ImageCount))
|
query.handleCriterion(ctx, galleryImageCountCriterionHandler(qb, galleryFilter.ImageCount))
|
||||||
@@ -968,51 +968,12 @@ func hasChaptersCriterionHandler(hasChapters *string) criterionHandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func galleryStudioCriterionHandler(qb *GalleryStore, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
func galleryPerformerTagsCriterionHandler(qb *GalleryStore, tags *models.HierarchicalMultiCriterionInput) criterionHandler {
|
||||||
h := hierarchicalMultiCriterionHandlerBuilder{
|
return &joinedPerformerTagsHandler{
|
||||||
tx: qb.tx,
|
criterion: tags,
|
||||||
|
primaryTable: galleryTable,
|
||||||
primaryTable: galleryTable,
|
joinTable: performersGalleriesTable,
|
||||||
foreignTable: studioTable,
|
joinPrimaryKey: galleryIDColumn,
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1945,154 +1945,369 @@ func TestGalleryQueryIsMissingDate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGalleryQueryPerformers(t *testing.T) {
|
func TestGalleryQueryPerformers(t *testing.T) {
|
||||||
withTxn(func(ctx context.Context) error {
|
tests := []struct {
|
||||||
sqb := db.Gallery
|
name string
|
||||||
performerCriterion := models.MultiCriterionInput{
|
filter models.MultiCriterionInput
|
||||||
Value: []string{
|
includeIdxs []int
|
||||||
strconv.Itoa(performerIDs[performerIdxWithGallery]),
|
excludeIdxs []int
|
||||||
strconv.Itoa(performerIDs[performerIdx1WithGallery]),
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"includes",
|
||||||
|
models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(performerIDs[performerIdxWithGallery]),
|
||||||
|
strconv.Itoa(performerIDs[performerIdx1WithGallery]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludes,
|
[]int{
|
||||||
}
|
galleryIdxWithPerformer,
|
||||||
|
galleryIdxWithTwoPerformers,
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludesAll,
|
[]int{
|
||||||
}
|
galleryIdxWithImage,
|
||||||
|
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
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)
|
for _, tt := range tests {
|
||||||
findFilter := models.FindFilterType{
|
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||||
Q: &q,
|
assert := assert.New(t)
|
||||||
}
|
|
||||||
|
|
||||||
galleries = queryGallery(ctx, t, sqb, &galleryFilter, &findFilter)
|
results, _, err := db.Gallery.Query(ctx, &models.GalleryFilterType{
|
||||||
assert.Len(t, galleries, 0)
|
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) {
|
func TestGalleryQueryTags(t *testing.T) {
|
||||||
withTxn(func(ctx context.Context) error {
|
tests := []struct {
|
||||||
sqb := db.Gallery
|
name string
|
||||||
tagCriterion := models.HierarchicalMultiCriterionInput{
|
filter models.HierarchicalMultiCriterionInput
|
||||||
Value: []string{
|
includeIdxs []int
|
||||||
strconv.Itoa(tagIDs[tagIdxWithGallery]),
|
excludeIdxs []int
|
||||||
strconv.Itoa(tagIDs[tagIdx1WithGallery]),
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"includes",
|
||||||
|
models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdxWithGallery]),
|
||||||
|
strconv.Itoa(tagIDs[tagIdx1WithGallery]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludes,
|
[]int{
|
||||||
}
|
galleryIdxWithTag,
|
||||||
|
galleryIdxWithTwoTags,
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludesAll,
|
[]int{
|
||||||
}
|
galleryIdxWithImage,
|
||||||
|
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
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)
|
for _, tt := range tests {
|
||||||
findFilter := models.FindFilterType{
|
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||||
Q: &q,
|
assert := assert.New(t)
|
||||||
}
|
|
||||||
|
|
||||||
galleries = queryGallery(ctx, t, sqb, &galleryFilter, &findFilter)
|
results, _, err := db.Gallery.Query(ctx, &models.GalleryFilterType{
|
||||||
assert.Len(t, galleries, 0)
|
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) {
|
func TestGalleryQueryStudio(t *testing.T) {
|
||||||
withTxn(func(ctx context.Context) error {
|
tests := []struct {
|
||||||
sqb := db.Gallery
|
name string
|
||||||
studioCriterion := models.HierarchicalMultiCriterionInput{
|
q string
|
||||||
Value: []string{
|
studioCriterion models.HierarchicalMultiCriterionInput
|
||||||
strconv.Itoa(studioIDs[studioIdxWithGallery]),
|
expectedIDs []int
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"includes",
|
||||||
|
"",
|
||||||
|
models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(studioIDs[studioIdxWithGallery]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludes,
|
[]int{galleryIDs[galleryIdxWithStudio]},
|
||||||
}
|
false,
|
||||||
|
},
|
||||||
galleryFilter := models.GalleryFilterType{
|
{
|
||||||
Studios: &studioCriterion,
|
"excludes",
|
||||||
}
|
getGalleryStringValue(galleryIdxWithStudio, titleField),
|
||||||
|
models.HierarchicalMultiCriterionInput{
|
||||||
galleries := queryGallery(ctx, t, sqb, &galleryFilter, nil)
|
Value: []string{
|
||||||
|
strconv.Itoa(studioIDs[studioIdxWithGallery]),
|
||||||
assert.Len(t, galleries, 1)
|
},
|
||||||
|
Modifier: models.CriterionModifierExcludes,
|
||||||
// ensure id is correct
|
|
||||||
assert.Equal(t, galleryIDs[galleryIdxWithStudio], galleries[0].ID)
|
|
||||||
|
|
||||||
studioCriterion = models.HierarchicalMultiCriterionInput{
|
|
||||||
Value: []string{
|
|
||||||
strconv.Itoa(studioIDs[studioIdxWithGallery]),
|
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierExcludes,
|
[]int{},
|
||||||
}
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"excludes includes null",
|
||||||
|
getGalleryStringValue(galleryIdxWithImage, titleField),
|
||||||
|
models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(studioIDs[studioIdxWithGallery]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierExcludes,
|
||||||
|
},
|
||||||
|
[]int{galleryIDs[galleryIdxWithImage]},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"equals",
|
||||||
|
"",
|
||||||
|
models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(studioIDs[studioIdxWithGallery]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
},
|
||||||
|
[]int{galleryIDs[galleryIdxWithStudio]},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not equals",
|
||||||
|
getGalleryStringValue(galleryIdxWithStudio, titleField),
|
||||||
|
models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(studioIDs[studioIdxWithGallery]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierNotEquals,
|
||||||
|
},
|
||||||
|
[]int{},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
q := getGalleryStringValue(galleryIdxWithStudio, titleField)
|
qb := db.Gallery
|
||||||
findFilter := models.FindFilterType{
|
|
||||||
Q: &q,
|
|
||||||
}
|
|
||||||
|
|
||||||
galleries = queryGallery(ctx, t, sqb, &galleryFilter, &findFilter)
|
for _, tt := range tests {
|
||||||
assert.Len(t, galleries, 0)
|
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) {
|
func TestGalleryQueryStudioDepth(t *testing.T) {
|
||||||
@@ -2157,81 +2372,198 @@ func TestGalleryQueryStudioDepth(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGalleryQueryPerformerTags(t *testing.T) {
|
func TestGalleryQueryPerformerTags(t *testing.T) {
|
||||||
withTxn(func(ctx context.Context) error {
|
allDepth := -1
|
||||||
sqb := db.Gallery
|
|
||||||
tagCriterion := models.HierarchicalMultiCriterionInput{
|
tests := []struct {
|
||||||
Value: []string{
|
name string
|
||||||
strconv.Itoa(tagIDs[tagIdxWithPerformer]),
|
findFilter *models.FindFilterType
|
||||||
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
|
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,
|
[]int{
|
||||||
}
|
galleryIdxWithPerformerTag,
|
||||||
|
galleryIdxWithPerformerTwoTags,
|
||||||
galleryFilter := models.GalleryFilterType{
|
galleryIdxWithTwoPerformerTag,
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludesAll,
|
[]int{
|
||||||
}
|
galleryIdxWithPerformer,
|
||||||
|
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
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)
|
for _, tt := range tests {
|
||||||
findFilter := models.FindFilterType{
|
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||||
Q: &q,
|
assert := assert.New(t)
|
||||||
}
|
|
||||||
|
|
||||||
galleries = queryGallery(ctx, t, sqb, &galleryFilter, &findFilter)
|
results, _, err := db.Gallery.Query(ctx, tt.filter, tt.findFilter)
|
||||||
assert.Len(t, galleries, 0)
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ImageStore.Query() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
tagCriterion = models.HierarchicalMultiCriterionInput{
|
ids := galleriesToIDs(results)
|
||||||
Modifier: models.CriterionModifierIsNull,
|
|
||||||
}
|
|
||||||
q = getGalleryStringValue(galleryIdx1WithImage, titleField)
|
|
||||||
|
|
||||||
galleries = queryGallery(ctx, t, sqb, &galleryFilter, &findFilter)
|
include := indexesToIDs(galleryIDs, tt.includeIdxs)
|
||||||
assert.Len(t, galleries, 1)
|
exclude := indexesToIDs(galleryIDs, tt.excludeIdxs)
|
||||||
assert.Equal(t, galleryIDs[galleryIdx1WithImage], galleries[0].ID)
|
|
||||||
|
|
||||||
q = getGalleryStringValue(galleryIdxWithPerformerTag, titleField)
|
for _, i := range include {
|
||||||
galleries = queryGallery(ctx, t, sqb, &galleryFilter, &findFilter)
|
assert.Contains(ids, i)
|
||||||
assert.Len(t, galleries, 0)
|
}
|
||||||
|
for _, e := range exclude {
|
||||||
tagCriterion.Modifier = models.CriterionModifierNotNull
|
assert.NotContains(ids, e)
|
||||||
|
}
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGalleryQueryTagCount(t *testing.T) {
|
func TestGalleryQueryTagCount(t *testing.T) {
|
||||||
|
|||||||
@@ -669,7 +669,7 @@ func (qb *ImageStore) makeFilter(ctx context.Context, imageFilter *models.ImageF
|
|||||||
query.handleCriterion(ctx, imageGalleriesCriterionHandler(qb, imageFilter.Galleries))
|
query.handleCriterion(ctx, imageGalleriesCriterionHandler(qb, imageFilter.Galleries))
|
||||||
query.handleCriterion(ctx, imagePerformersCriterionHandler(qb, imageFilter.Performers))
|
query.handleCriterion(ctx, imagePerformersCriterionHandler(qb, imageFilter.Performers))
|
||||||
query.handleCriterion(ctx, imagePerformerCountCriterionHandler(qb, imageFilter.PerformerCount))
|
query.handleCriterion(ctx, imagePerformerCountCriterionHandler(qb, imageFilter.PerformerCount))
|
||||||
query.handleCriterion(ctx, imageStudioCriterionHandler(qb, imageFilter.Studios))
|
query.handleCriterion(ctx, studioCriterionHandler(imageTable, imageFilter.Studios))
|
||||||
query.handleCriterion(ctx, imagePerformerTagsCriterionHandler(qb, imageFilter.PerformerTags))
|
query.handleCriterion(ctx, imagePerformerTagsCriterionHandler(qb, imageFilter.PerformerTags))
|
||||||
query.handleCriterion(ctx, imagePerformerFavoriteCriterionHandler(imageFilter.PerformerFavorite))
|
query.handleCriterion(ctx, imagePerformerFavoriteCriterionHandler(imageFilter.PerformerFavorite))
|
||||||
query.handleCriterion(ctx, timestampCriterionHandler(imageFilter.CreatedAt, "images.created_at"))
|
query.handleCriterion(ctx, timestampCriterionHandler(imageFilter.CreatedAt, "images.created_at"))
|
||||||
@@ -946,51 +946,12 @@ GROUP BY performers_images.image_id HAVING SUM(performers.favorite) = 0)`, "nofa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageStudioCriterionHandler(qb *ImageStore, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
func imagePerformerTagsCriterionHandler(qb *ImageStore, tags *models.HierarchicalMultiCriterionInput) criterionHandler {
|
||||||
h := hierarchicalMultiCriterionHandlerBuilder{
|
return &joinedPerformerTagsHandler{
|
||||||
tx: qb.tx,
|
criterion: tags,
|
||||||
|
primaryTable: imageTable,
|
||||||
primaryTable: imageTable,
|
joinTable: performersImagesTable,
|
||||||
foreignTable: studioTable,
|
joinPrimaryKey: imageIDColumn,
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2124,203 +2124,369 @@ func TestImageQueryGallery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestImageQueryPerformers(t *testing.T) {
|
func TestImageQueryPerformers(t *testing.T) {
|
||||||
withTxn(func(ctx context.Context) error {
|
tests := []struct {
|
||||||
sqb := db.Image
|
name string
|
||||||
performerCriterion := models.MultiCriterionInput{
|
filter models.MultiCriterionInput
|
||||||
Value: []string{
|
includeIdxs []int
|
||||||
strconv.Itoa(performerIDs[performerIdxWithImage]),
|
excludeIdxs []int
|
||||||
strconv.Itoa(performerIDs[performerIdx1WithImage]),
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"includes",
|
||||||
|
models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(performerIDs[performerIdxWithImage]),
|
||||||
|
strconv.Itoa(performerIDs[performerIdx1WithImage]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludes,
|
[]int{
|
||||||
}
|
imageIdxWithPerformer,
|
||||||
|
imageIdxWithTwoPerformers,
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludesAll,
|
[]int{
|
||||||
}
|
imageIdxWithGallery,
|
||||||
|
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
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)
|
for _, tt := range tests {
|
||||||
findFilter := models.FindFilterType{
|
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||||
Q: &q,
|
assert := assert.New(t)
|
||||||
}
|
|
||||||
|
|
||||||
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter)
|
results, err := db.Image.Query(ctx, models.ImageQueryOptions{
|
||||||
assert.Len(t, images, 0)
|
ImageFilter: &models.ImageFilterType{
|
||||||
|
Performers: &tt.filter,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ImageStore.Query() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
performerCriterion = models.MultiCriterionInput{
|
include := indexesToIDs(imageIDs, tt.includeIdxs)
|
||||||
Modifier: models.CriterionModifierIsNull,
|
exclude := indexesToIDs(imageIDs, tt.excludeIdxs)
|
||||||
}
|
|
||||||
q = getImageStringValue(imageIdxWithGallery, titleField)
|
|
||||||
|
|
||||||
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter)
|
for _, i := range include {
|
||||||
assert.Len(t, images, 1)
|
assert.Contains(results.IDs, i)
|
||||||
assert.Equal(t, imageIDs[imageIdxWithGallery], images[0].ID)
|
}
|
||||||
|
for _, e := range exclude {
|
||||||
q = getImageStringValue(imageIdxWithPerformerTag, titleField)
|
assert.NotContains(results.IDs, e)
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestImageQueryTags(t *testing.T) {
|
func TestImageQueryTags(t *testing.T) {
|
||||||
withTxn(func(ctx context.Context) error {
|
tests := []struct {
|
||||||
sqb := db.Image
|
name string
|
||||||
tagCriterion := models.HierarchicalMultiCriterionInput{
|
filter models.HierarchicalMultiCriterionInput
|
||||||
Value: []string{
|
includeIdxs []int
|
||||||
strconv.Itoa(tagIDs[tagIdxWithImage]),
|
excludeIdxs []int
|
||||||
strconv.Itoa(tagIDs[tagIdx1WithImage]),
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"includes",
|
||||||
|
models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdxWithImage]),
|
||||||
|
strconv.Itoa(tagIDs[tagIdx1WithImage]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludes,
|
[]int{
|
||||||
}
|
imageIdxWithTag,
|
||||||
|
imageIdxWithTwoTags,
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludesAll,
|
[]int{
|
||||||
}
|
imageIdxWithGallery,
|
||||||
|
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
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)
|
for _, tt := range tests {
|
||||||
findFilter := models.FindFilterType{
|
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||||
Q: &q,
|
assert := assert.New(t)
|
||||||
}
|
|
||||||
|
|
||||||
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter)
|
results, err := db.Image.Query(ctx, models.ImageQueryOptions{
|
||||||
assert.Len(t, images, 0)
|
ImageFilter: &models.ImageFilterType{
|
||||||
|
Tags: &tt.filter,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ImageStore.Query() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
tagCriterion = models.HierarchicalMultiCriterionInput{
|
include := indexesToIDs(imageIDs, tt.includeIdxs)
|
||||||
Modifier: models.CriterionModifierIsNull,
|
exclude := indexesToIDs(imageIDs, tt.excludeIdxs)
|
||||||
}
|
|
||||||
q = getImageStringValue(imageIdxWithGallery, titleField)
|
|
||||||
|
|
||||||
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter)
|
for _, i := range include {
|
||||||
assert.Len(t, images, 1)
|
assert.Contains(results.IDs, i)
|
||||||
assert.Equal(t, imageIDs[imageIdxWithGallery], images[0].ID)
|
}
|
||||||
|
for _, e := range exclude {
|
||||||
q = getImageStringValue(imageIdxWithTag, titleField)
|
assert.NotContains(results.IDs, e)
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestImageQueryStudio(t *testing.T) {
|
func TestImageQueryStudio(t *testing.T) {
|
||||||
withTxn(func(ctx context.Context) error {
|
tests := []struct {
|
||||||
sqb := db.Image
|
name string
|
||||||
studioCriterion := models.HierarchicalMultiCriterionInput{
|
q string
|
||||||
Value: []string{
|
studioCriterion models.HierarchicalMultiCriterionInput
|
||||||
strconv.Itoa(studioIDs[studioIdxWithImage]),
|
expectedIDs []int
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"includes",
|
||||||
|
"",
|
||||||
|
models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(studioIDs[studioIdxWithImage]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludes,
|
[]int{imageIDs[imageIdxWithStudio]},
|
||||||
}
|
false,
|
||||||
|
},
|
||||||
imageFilter := models.ImageFilterType{
|
{
|
||||||
Studios: &studioCriterion,
|
"excludes",
|
||||||
}
|
getImageStringValue(imageIdxWithStudio, titleField),
|
||||||
|
models.HierarchicalMultiCriterionInput{
|
||||||
images, _, err := queryImagesWithCount(ctx, sqb, &imageFilter, nil)
|
Value: []string{
|
||||||
if err != nil {
|
strconv.Itoa(studioIDs[studioIdxWithImage]),
|
||||||
t.Errorf("Error querying image: %s", err.Error())
|
},
|
||||||
}
|
Modifier: models.CriterionModifierExcludes,
|
||||||
|
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
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)
|
qb := db.Image
|
||||||
findFilter := models.FindFilterType{
|
|
||||||
Q: &q,
|
|
||||||
}
|
|
||||||
|
|
||||||
images, _, err = queryImagesWithCount(ctx, sqb, &imageFilter, &findFilter)
|
for _, tt := range tests {
|
||||||
if err != nil {
|
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||||
t.Errorf("Error querying image: %s", err.Error())
|
studioCriterion := tt.studioCriterion
|
||||||
}
|
|
||||||
assert.Len(t, images, 0)
|
|
||||||
|
|
||||||
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) {
|
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) {
|
func TestImageQueryPerformerTags(t *testing.T) {
|
||||||
withTxn(func(ctx context.Context) error {
|
allDepth := -1
|
||||||
sqb := db.Image
|
|
||||||
tagCriterion := models.HierarchicalMultiCriterionInput{
|
tests := []struct {
|
||||||
Value: []string{
|
name string
|
||||||
strconv.Itoa(tagIDs[tagIdxWithPerformer]),
|
findFilter *models.FindFilterType
|
||||||
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
|
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,
|
[]int{
|
||||||
}
|
imageIdxWithPerformerTag,
|
||||||
|
imageIdxWithPerformerTwoTags,
|
||||||
imageFilter := models.ImageFilterType{
|
imageIdxWithTwoPerformerTag,
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludesAll,
|
[]int{
|
||||||
}
|
imageIdxWithPerformer,
|
||||||
|
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
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)
|
for _, tt := range tests {
|
||||||
findFilter := models.FindFilterType{
|
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||||
Q: &q,
|
assert := assert.New(t)
|
||||||
}
|
|
||||||
|
|
||||||
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter)
|
results, err := db.Image.Query(ctx, models.ImageQueryOptions{
|
||||||
assert.Len(t, images, 0)
|
ImageFilter: 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{
|
include := indexesToIDs(imageIDs, tt.includeIdxs)
|
||||||
Modifier: models.CriterionModifierIsNull,
|
exclude := indexesToIDs(imageIDs, tt.excludeIdxs)
|
||||||
}
|
|
||||||
q = getImageStringValue(imageIdxWithGallery, titleField)
|
|
||||||
|
|
||||||
images = queryImages(ctx, t, sqb, &imageFilter, &findFilter)
|
for _, i := range include {
|
||||||
assert.Len(t, images, 1)
|
assert.Contains(results.IDs, i)
|
||||||
assert.Equal(t, imageIDs[imageIdxWithGallery], images[0].ID)
|
}
|
||||||
|
for _, e := range exclude {
|
||||||
q = getImageStringValue(imageIdxWithPerformerTag, titleField)
|
assert.NotContains(results.IDs, e)
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestImageQueryTagCount(t *testing.T) {
|
func TestImageQueryTagCount(t *testing.T) {
|
||||||
@@ -2587,7 +2873,7 @@ func TestImageQuerySorting(t *testing.T) {
|
|||||||
"date",
|
"date",
|
||||||
models.SortDirectionEnumDesc,
|
models.SortDirectionEnumDesc,
|
||||||
imageIdxWithTwoGalleries,
|
imageIdxWithTwoGalleries,
|
||||||
imageIdxWithGrandChildStudio,
|
imageIdxWithPerformerParentTag,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ func (qb *movieQueryBuilder) makeFilter(ctx context.Context, movieFilter *models
|
|||||||
query.handleCriterion(ctx, floatIntCriterionHandler(movieFilter.Duration, "movies.duration", nil))
|
query.handleCriterion(ctx, floatIntCriterionHandler(movieFilter.Duration, "movies.duration", nil))
|
||||||
query.handleCriterion(ctx, movieIsMissingCriterionHandler(qb, movieFilter.IsMissing))
|
query.handleCriterion(ctx, movieIsMissingCriterionHandler(qb, movieFilter.IsMissing))
|
||||||
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.URL, "movies.url"))
|
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.URL, "movies.url"))
|
||||||
query.handleCriterion(ctx, movieStudioCriterionHandler(qb, movieFilter.Studios))
|
query.handleCriterion(ctx, studioCriterionHandler(movieTable, movieFilter.Studios))
|
||||||
query.handleCriterion(ctx, moviePerformersCriterionHandler(qb, movieFilter.Performers))
|
query.handleCriterion(ctx, moviePerformersCriterionHandler(qb, movieFilter.Performers))
|
||||||
query.handleCriterion(ctx, dateCriterionHandler(movieFilter.Date, "movies.date"))
|
query.handleCriterion(ctx, dateCriterionHandler(movieFilter.Date, "movies.date"))
|
||||||
query.handleCriterion(ctx, timestampCriterionHandler(movieFilter.CreatedAt, "movies.created_at"))
|
query.handleCriterion(ctx, timestampCriterionHandler(movieFilter.CreatedAt, "movies.created_at"))
|
||||||
@@ -239,19 +239,6 @@ func movieIsMissingCriterionHandler(qb *movieQueryBuilder, isMissing *string) cr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func movieStudioCriterionHandler(qb *movieQueryBuilder, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
|
||||||
h := hierarchicalMultiCriterionHandlerBuilder{
|
|
||||||
tx: qb.tx,
|
|
||||||
|
|
||||||
primaryTable: movieTable,
|
|
||||||
foreignTable: studioTable,
|
|
||||||
foreignFK: studioIDColumn,
|
|
||||||
parentFK: "parent_id",
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.handler(studios)
|
|
||||||
}
|
|
||||||
|
|
||||||
func moviePerformersCriterionHandler(qb *movieQueryBuilder, performers *models.MultiCriterionInput) criterionHandlerFunc {
|
func moviePerformersCriterionHandler(qb *movieQueryBuilder, performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if performers != nil {
|
if performers != nil {
|
||||||
|
|||||||
@@ -908,7 +908,11 @@ func performerStudiosCriterionHandler(qb *PerformerStore, studios *models.Hierar
|
|||||||
}
|
}
|
||||||
|
|
||||||
const derivedPerformerStudioTable = "performer_studio"
|
const derivedPerformerStudioTable = "performer_studio"
|
||||||
valuesClause := getHierarchicalValues(ctx, qb.tx, studios.Value, studioTable, "", "parent_id", studios.Depth)
|
valuesClause, err := getHierarchicalValues(ctx, qb.tx, studios.Value, studioTable, "", "parent_id", "child_id", studios.Depth)
|
||||||
|
if err != nil {
|
||||||
|
f.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
f.addWith("studio(root_id, item_id) AS (" + valuesClause + ")")
|
f.addWith("studio(root_id, item_id) AS (" + valuesClause + ")")
|
||||||
|
|
||||||
templStr := `SELECT performer_id FROM {primaryTable}
|
templStr := `SELECT performer_id FROM {primaryTable}
|
||||||
|
|||||||
@@ -513,12 +513,13 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
|
|||||||
performerIDs[performerIdxWithTwoTags],
|
performerIDs[performerIdxWithTwoTags],
|
||||||
clearPerformerPartial(),
|
clearPerformerPartial(),
|
||||||
models.Performer{
|
models.Performer{
|
||||||
ID: performerIDs[performerIdxWithTwoTags],
|
ID: performerIDs[performerIdxWithTwoTags],
|
||||||
Name: getPerformerStringValue(performerIdxWithTwoTags, "Name"),
|
Name: getPerformerStringValue(performerIdxWithTwoTags, "Name"),
|
||||||
Favorite: true,
|
Favorite: getPerformerBoolValue(performerIdxWithTwoTags),
|
||||||
Aliases: models.NewRelatedStrings([]string{}),
|
Aliases: models.NewRelatedStrings([]string{}),
|
||||||
TagIDs: models.NewRelatedIDs([]int{}),
|
TagIDs: models.NewRelatedIDs([]int{}),
|
||||||
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
|
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
|
||||||
|
IgnoreAutoTag: getIgnoreAutoTag(performerIdxWithTwoTags),
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
@@ -1904,10 +1905,10 @@ func TestPerformerQuerySortScenesCount(t *testing.T) {
|
|||||||
|
|
||||||
assert.True(t, len(performers) > 0)
|
assert.True(t, len(performers) > 0)
|
||||||
|
|
||||||
// first performer should be performerIdxWithTwoScenes
|
// first performer should be performerIdx1WithScene
|
||||||
firstPerformer := performers[0]
|
firstPerformer := performers[0]
|
||||||
|
|
||||||
assert.Equal(t, performerIDs[performerIdxWithTwoScenes], firstPerformer.ID)
|
assert.Equal(t, performerIDs[performerIdx1WithScene], firstPerformer.ID)
|
||||||
|
|
||||||
// sort in ascending order
|
// sort in ascending order
|
||||||
direction = models.SortDirectionEnumAsc
|
direction = models.SortDirectionEnumAsc
|
||||||
@@ -1920,7 +1921,7 @@ func TestPerformerQuerySortScenesCount(t *testing.T) {
|
|||||||
assert.True(t, len(performers) > 0)
|
assert.True(t, len(performers) > 0)
|
||||||
lastPerformer := performers[len(performers)-1]
|
lastPerformer := performers[len(performers)-1]
|
||||||
|
|
||||||
assert.Equal(t, performerIDs[performerIdxWithTwoScenes], lastPerformer.ID)
|
assert.Equal(t, performerIDs[performerIdxWithTag], lastPerformer.ID)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -2060,7 +2061,7 @@ func TestPerformerStore_FindByStashIDStatus(t *testing.T) {
|
|||||||
name: "!hasStashID",
|
name: "!hasStashID",
|
||||||
hasStashID: false,
|
hasStashID: false,
|
||||||
stashboxEndpoint: getPerformerStringValue(performerIdxWithScene, "endpoint"),
|
stashboxEndpoint: getPerformerStringValue(performerIdxWithScene, "endpoint"),
|
||||||
include: []int{performerIdxWithImage},
|
include: []int{performerIdxWithTwoScenes},
|
||||||
exclude: []int{performerIdx2WithScene},
|
exclude: []int{performerIdx2WithScene},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -959,7 +959,7 @@ func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneF
|
|||||||
query.handleCriterion(ctx, sceneTagCountCriterionHandler(qb, sceneFilter.TagCount))
|
query.handleCriterion(ctx, sceneTagCountCriterionHandler(qb, sceneFilter.TagCount))
|
||||||
query.handleCriterion(ctx, scenePerformersCriterionHandler(qb, sceneFilter.Performers))
|
query.handleCriterion(ctx, scenePerformersCriterionHandler(qb, sceneFilter.Performers))
|
||||||
query.handleCriterion(ctx, scenePerformerCountCriterionHandler(qb, sceneFilter.PerformerCount))
|
query.handleCriterion(ctx, scenePerformerCountCriterionHandler(qb, sceneFilter.PerformerCount))
|
||||||
query.handleCriterion(ctx, sceneStudioCriterionHandler(qb, sceneFilter.Studios))
|
query.handleCriterion(ctx, studioCriterionHandler(sceneTable, sceneFilter.Studios))
|
||||||
query.handleCriterion(ctx, sceneMoviesCriterionHandler(qb, sceneFilter.Movies))
|
query.handleCriterion(ctx, sceneMoviesCriterionHandler(qb, sceneFilter.Movies))
|
||||||
query.handleCriterion(ctx, scenePerformerTagsCriterionHandler(qb, sceneFilter.PerformerTags))
|
query.handleCriterion(ctx, scenePerformerTagsCriterionHandler(qb, sceneFilter.PerformerTags))
|
||||||
query.handleCriterion(ctx, scenePerformerFavoriteCriterionHandler(sceneFilter.PerformerFavorite))
|
query.handleCriterion(ctx, scenePerformerFavoriteCriterionHandler(sceneFilter.PerformerFavorite))
|
||||||
@@ -1352,19 +1352,6 @@ func scenePerformerAgeCriterionHandler(performerAge *models.IntCriterionInput) c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sceneStudioCriterionHandler(qb *SceneStore, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
|
||||||
h := hierarchicalMultiCriterionHandlerBuilder{
|
|
||||||
tx: qb.tx,
|
|
||||||
|
|
||||||
primaryTable: sceneTable,
|
|
||||||
foreignTable: studioTable,
|
|
||||||
foreignFK: studioIDColumn,
|
|
||||||
parentFK: "parent_id",
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.handler(studios)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sceneMoviesCriterionHandler(qb *SceneStore, movies *models.MultiCriterionInput) criterionHandlerFunc {
|
func sceneMoviesCriterionHandler(qb *SceneStore, movies *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
addJoinsFunc := func(f *filterBuilder) {
|
addJoinsFunc := func(f *filterBuilder) {
|
||||||
qb.moviesRepository().join(f, "", "scenes.id")
|
qb.moviesRepository().join(f, "", "scenes.id")
|
||||||
@@ -1374,38 +1361,12 @@ func sceneMoviesCriterionHandler(qb *SceneStore, movies *models.MultiCriterionIn
|
|||||||
return h.handler(movies)
|
return h.handler(movies)
|
||||||
}
|
}
|
||||||
|
|
||||||
func scenePerformerTagsCriterionHandler(qb *SceneStore, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
func scenePerformerTagsCriterionHandler(qb *SceneStore, tags *models.HierarchicalMultiCriterionInput) criterionHandler {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return &joinedPerformerTagsHandler{
|
||||||
if tags != nil {
|
criterion: tags,
|
||||||
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
|
primaryTable: sceneTable,
|
||||||
var notClause string
|
joinTable: performersScenesTable,
|
||||||
if tags.Modifier == models.CriterionModifierNotNull {
|
joinPrimaryKey: sceneIDColumn,
|
||||||
notClause = "NOT"
|
|
||||||
}
|
|
||||||
|
|
||||||
f.addLeftJoin("performers_scenes", "", "scenes.id = performers_scenes.scene_id")
|
|
||||||
f.addLeftJoin("performers_tags", "", "performers_scenes.performer_id = performers_tags.performer_id")
|
|
||||||
|
|
||||||
f.addWhere(fmt.Sprintf("performers_tags.tag_id IS %s NULL", notClause))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tags.Value) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
valuesClause := getHierarchicalValues(ctx, qb.tx, tags.Value, tagTable, "tags_relations", "", tags.Depth)
|
|
||||||
|
|
||||||
f.addWith(`performer_tags AS (
|
|
||||||
SELECT ps.scene_id, t.column1 AS root_tag_id FROM performers_scenes ps
|
|
||||||
INNER JOIN performers_tags pt ON pt.performer_id = ps.performer_id
|
|
||||||
INNER JOIN (` + valuesClause + `) t ON t.column2 = pt.tag_id
|
|
||||||
)`)
|
|
||||||
|
|
||||||
f.addLeftJoin("performer_tags", "", "performer_tags.scene_id = scenes.id")
|
|
||||||
|
|
||||||
addHierarchicalConditionClauses(f, *tags, "performer_tags", "root_tag_id")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -209,7 +209,11 @@ func sceneMarkerTagsCriterionHandler(qb *sceneMarkerQueryBuilder, tags *models.H
|
|||||||
if len(tags.Value) == 0 {
|
if len(tags.Value) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
valuesClause := getHierarchicalValues(ctx, qb.tx, tags.Value, tagTable, "tags_relations", "", tags.Depth)
|
valuesClause, err := getHierarchicalValues(ctx, qb.tx, tags.Value, tagTable, "tags_relations", "parent_id", "child_id", tags.Depth)
|
||||||
|
if err != nil {
|
||||||
|
f.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
f.addWith(`marker_tags AS (
|
f.addWith(`marker_tags AS (
|
||||||
SELECT mt.scene_marker_id, t.column1 AS root_tag_id FROM scene_markers_tags mt
|
SELECT mt.scene_marker_id, t.column1 AS root_tag_id FROM scene_markers_tags mt
|
||||||
@@ -229,32 +233,23 @@ INNER JOIN (` + valuesClause + `) t ON t.column2 = m.primary_tag_id
|
|||||||
func sceneMarkerSceneTagsCriterionHandler(qb *sceneMarkerQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
func sceneMarkerSceneTagsCriterionHandler(qb *sceneMarkerQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if tags != nil {
|
if tags != nil {
|
||||||
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
|
f.addLeftJoin("scenes_tags", "", "scene_markers.scene_id = scenes_tags.scene_id")
|
||||||
var notClause string
|
|
||||||
if tags.Modifier == models.CriterionModifierNotNull {
|
|
||||||
notClause = "NOT"
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
primaryTable: "scene_markers",
|
||||||
return
|
primaryKey: sceneIDColumn,
|
||||||
|
foreignTable: tagTable,
|
||||||
|
foreignFK: tagIDColumn,
|
||||||
|
|
||||||
|
relationsTable: "tags_relations",
|
||||||
|
joinTable: "scenes_tags",
|
||||||
|
joinAs: "marker_scenes_tags",
|
||||||
|
primaryFK: sceneIDColumn,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tags.Value) == 0 {
|
h.handler(tags).handle(ctx, f)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
valuesClause := getHierarchicalValues(ctx, qb.tx, tags.Value, tagTable, "tags_relations", "", tags.Depth)
|
|
||||||
|
|
||||||
f.addWith(`scene_tags AS (
|
|
||||||
SELECT st.scene_id, t.column1 AS root_tag_id FROM scenes_tags st
|
|
||||||
INNER JOIN (` + valuesClause + `) t ON t.column2 = st.tag_id
|
|
||||||
)`)
|
|
||||||
|
|
||||||
f.addLeftJoin("scene_tags", "", "scene_tags.scene_id = scene_markers.scene_id")
|
|
||||||
|
|
||||||
addHierarchicalConditionClauses(f, *tags, "scene_tags", "root_tag_id")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ package sqlite_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
||||||
|
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||||
"github.com/stashapp/stash/pkg/sqlite"
|
"github.com/stashapp/stash/pkg/sqlite"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -50,7 +53,7 @@ func TestMarkerCountByTagID(t *testing.T) {
|
|||||||
t.Errorf("error calling CountByTagID: %s", err.Error())
|
t.Errorf("error calling CountByTagID: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, 3, markerCount)
|
assert.Equal(t, 4, markerCount)
|
||||||
|
|
||||||
markerCount, err = mqb.CountByTagID(ctx, tagIDs[tagIdxWithMarkers])
|
markerCount, err = mqb.CountByTagID(ctx, tagIDs[tagIdxWithMarkers])
|
||||||
|
|
||||||
@@ -151,7 +154,7 @@ func TestMarkerQuerySceneTags(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
withTxn(func(ctx context.Context) error {
|
withTxn(func(ctx context.Context) error {
|
||||||
testTags := func(m *models.SceneMarker, markerFilter *models.SceneMarkerFilterType) {
|
testTags := func(t *testing.T, m *models.SceneMarker, markerFilter *models.SceneMarkerFilterType) {
|
||||||
s, err := db.Scene.Find(ctx, int(m.SceneID.Int64))
|
s, err := db.Scene.Find(ctx, int(m.SceneID.Int64))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error getting marker tag ids: %v", err)
|
t.Errorf("error getting marker tag ids: %v", err)
|
||||||
@@ -164,11 +167,40 @@ func TestMarkerQuerySceneTags(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tagIDs := s.TagIDs.List()
|
tagIDs := s.TagIDs.List()
|
||||||
if markerFilter.SceneTags.Modifier == models.CriterionModifierIsNull && len(tagIDs) > 0 {
|
values, _ := stringslice.StringSliceToIntSlice(markerFilter.SceneTags.Value)
|
||||||
t.Errorf("expected marker %d to have no scene tags - found %d", m.ID, len(tagIDs))
|
switch markerFilter.SceneTags.Modifier {
|
||||||
}
|
case models.CriterionModifierIsNull:
|
||||||
if markerFilter.SceneTags.Modifier == models.CriterionModifierNotNull && len(tagIDs) == 0 {
|
if len(tagIDs) > 0 {
|
||||||
t.Errorf("expected marker %d to have scene tags - found 0", m.ID)
|
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,
|
nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"includes",
|
||||||
|
&models.SceneMarkerFilterType{
|
||||||
|
SceneTags: &models.HierarchicalMultiCriterionInput{
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdx3WithScene]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"includes all",
|
||||||
|
&models.SceneMarkerFilterType{
|
||||||
|
SceneTags: &models.HierarchicalMultiCriterionInput{
|
||||||
|
Modifier: models.CriterionModifierIncludesAll,
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdx2WithScene]),
|
||||||
|
strconv.Itoa(tagIDs[tagIdx3WithScene]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"equals",
|
||||||
|
&models.SceneMarkerFilterType{
|
||||||
|
SceneTags: &models.HierarchicalMultiCriterionInput{
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdx2WithScene]),
|
||||||
|
strconv.Itoa(tagIDs[tagIdx3WithScene]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
// not equals not supported
|
||||||
|
// {
|
||||||
|
// "not equals",
|
||||||
|
// &models.SceneMarkerFilterType{
|
||||||
|
// SceneTags: &models.HierarchicalMultiCriterionInput{
|
||||||
|
// Modifier: models.CriterionModifierNotEquals,
|
||||||
|
// Value: []string{
|
||||||
|
// strconv.Itoa(tagIDs[tagIdx2WithScene]),
|
||||||
|
// strconv.Itoa(tagIDs[tagIdx3WithScene]),
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// nil,
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
"excludes",
|
||||||
|
&models.SceneMarkerFilterType{
|
||||||
|
SceneTags: &models.HierarchicalMultiCriterionInput{
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdx2WithScene]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
@@ -198,7 +294,7 @@ func TestMarkerQuerySceneTags(t *testing.T) {
|
|||||||
markers := queryMarkers(ctx, t, sqlite.SceneMarkerReaderWriter, tc.markerFilter, tc.findFilter)
|
markers := queryMarkers(ctx, t, sqlite.SceneMarkerReaderWriter, tc.markerFilter, tc.findFilter)
|
||||||
assert.Greater(t, len(markers), 0)
|
assert.Greater(t, len(markers), 0)
|
||||||
for _, m := range markers {
|
for _, m := range markers {
|
||||||
testTags(m, tc.markerFilter)
|
testTags(t, m, tc.markerFilter)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -668,7 +668,8 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
|
|||||||
sceneIDs[sceneIdxWithSpacedName],
|
sceneIDs[sceneIdxWithSpacedName],
|
||||||
clearScenePartial(),
|
clearScenePartial(),
|
||||||
models.Scene{
|
models.Scene{
|
||||||
ID: sceneIDs[sceneIdxWithSpacedName],
|
ID: sceneIDs[sceneIdxWithSpacedName],
|
||||||
|
OCounter: getOCounter(sceneIdxWithSpacedName),
|
||||||
Files: models.NewRelatedVideoFiles([]*file.VideoFile{
|
Files: models.NewRelatedVideoFiles([]*file.VideoFile{
|
||||||
makeSceneFile(sceneIdxWithSpacedName),
|
makeSceneFile(sceneIdxWithSpacedName),
|
||||||
}),
|
}),
|
||||||
@@ -677,6 +678,10 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
|
|||||||
PerformerIDs: models.NewRelatedIDs([]int{}),
|
PerformerIDs: models.NewRelatedIDs([]int{}),
|
||||||
Movies: models.NewRelatedMovies([]models.MoviesScenes{}),
|
Movies: models.NewRelatedMovies([]models.MoviesScenes{}),
|
||||||
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
|
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
|
||||||
|
PlayCount: getScenePlayCount(sceneIdxWithSpacedName),
|
||||||
|
PlayDuration: getScenePlayDuration(sceneIdxWithSpacedName),
|
||||||
|
LastPlayedAt: getSceneLastPlayed(sceneIdxWithSpacedName),
|
||||||
|
ResumeTime: getSceneResumeTime(sceneIdxWithSpacedName),
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
@@ -2101,6 +2106,8 @@ func sceneQueryQ(ctx context.Context, t *testing.T, sqb models.SceneReader, q st
|
|||||||
|
|
||||||
// no Q should return all results
|
// no Q should return all results
|
||||||
filter.Q = nil
|
filter.Q = nil
|
||||||
|
pp := totalScenes
|
||||||
|
filter.PerPage = &pp
|
||||||
scenes = queryScene(ctx, t, sqb, nil, &filter)
|
scenes = queryScene(ctx, t, sqb, nil, &filter)
|
||||||
|
|
||||||
assert.Len(t, scenes, totalScenes)
|
assert.Len(t, scenes, totalScenes)
|
||||||
@@ -2230,8 +2237,8 @@ func TestSceneQuery(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
include := indexesToIDs(performerIDs, tt.includeIdxs)
|
include := indexesToIDs(sceneIDs, tt.includeIdxs)
|
||||||
exclude := indexesToIDs(performerIDs, tt.excludeIdxs)
|
exclude := indexesToIDs(sceneIDs, tt.excludeIdxs)
|
||||||
|
|
||||||
for _, i := range include {
|
for _, i := range include {
|
||||||
assert.Contains(results.IDs, i)
|
assert.Contains(results.IDs, i)
|
||||||
@@ -3057,7 +3064,13 @@ func queryScenes(ctx context.Context, t *testing.T, queryBuilder models.SceneRea
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return queryScene(ctx, t, queryBuilder, &sceneFilter, nil)
|
// needed so that we don't hit the default limit of 25 scenes
|
||||||
|
pp := 1000
|
||||||
|
findFilter := &models.FindFilterType{
|
||||||
|
PerPage: &pp,
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryScene(ctx, t, queryBuilder, &sceneFilter, findFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createScene(ctx context.Context, width int, height int) (*models.Scene, error) {
|
func createScene(ctx context.Context, width int, height int) (*models.Scene, error) {
|
||||||
@@ -3329,192 +3342,473 @@ func TestSceneQueryIsMissingPhash(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSceneQueryPerformers(t *testing.T) {
|
func TestSceneQueryPerformers(t *testing.T) {
|
||||||
withTxn(func(ctx context.Context) error {
|
tests := []struct {
|
||||||
sqb := db.Scene
|
name string
|
||||||
performerCriterion := models.MultiCriterionInput{
|
filter models.MultiCriterionInput
|
||||||
Value: []string{
|
includeIdxs []int
|
||||||
strconv.Itoa(performerIDs[performerIdxWithScene]),
|
excludeIdxs []int
|
||||||
strconv.Itoa(performerIDs[performerIdx1WithScene]),
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"includes",
|
||||||
|
models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(performerIDs[performerIdxWithScene]),
|
||||||
|
strconv.Itoa(performerIDs[performerIdx1WithScene]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludes,
|
[]int{
|
||||||
}
|
sceneIdxWithPerformer,
|
||||||
|
sceneIdxWithTwoPerformers,
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludesAll,
|
[]int{
|
||||||
}
|
sceneIdxWithGallery,
|
||||||
|
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
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)
|
for _, tt := range tests {
|
||||||
findFilter := models.FindFilterType{
|
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||||
Q: &q,
|
assert := assert.New(t)
|
||||||
}
|
|
||||||
|
|
||||||
scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter)
|
results, err := db.Scene.Query(ctx, models.SceneQueryOptions{
|
||||||
assert.Len(t, scenes, 0)
|
SceneFilter: &models.SceneFilterType{
|
||||||
|
Performers: &tt.filter,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
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) {
|
func TestSceneQueryTags(t *testing.T) {
|
||||||
withTxn(func(ctx context.Context) error {
|
tests := []struct {
|
||||||
sqb := db.Scene
|
name string
|
||||||
tagCriterion := models.HierarchicalMultiCriterionInput{
|
filter models.HierarchicalMultiCriterionInput
|
||||||
Value: []string{
|
includeIdxs []int
|
||||||
strconv.Itoa(tagIDs[tagIdxWithScene]),
|
excludeIdxs []int
|
||||||
strconv.Itoa(tagIDs[tagIdx1WithScene]),
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"includes",
|
||||||
|
models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdxWithScene]),
|
||||||
|
strconv.Itoa(tagIDs[tagIdx1WithScene]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludes,
|
[]int{
|
||||||
}
|
sceneIdxWithTag,
|
||||||
|
sceneIdxWithTwoTags,
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludesAll,
|
[]int{
|
||||||
}
|
sceneIdxWithGallery,
|
||||||
|
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
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)
|
for _, tt := range tests {
|
||||||
findFilter := models.FindFilterType{
|
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||||
Q: &q,
|
assert := assert.New(t)
|
||||||
}
|
|
||||||
|
|
||||||
scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter)
|
results, err := db.Scene.Query(ctx, models.SceneQueryOptions{
|
||||||
assert.Len(t, scenes, 0)
|
SceneFilter: &models.SceneFilterType{
|
||||||
|
Tags: &tt.filter,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
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) {
|
func TestSceneQueryPerformerTags(t *testing.T) {
|
||||||
withTxn(func(ctx context.Context) error {
|
allDepth := -1
|
||||||
sqb := db.Scene
|
|
||||||
tagCriterion := models.HierarchicalMultiCriterionInput{
|
tests := []struct {
|
||||||
Value: []string{
|
name string
|
||||||
strconv.Itoa(tagIDs[tagIdxWithPerformer]),
|
findFilter *models.FindFilterType
|
||||||
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
|
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,
|
[]int{
|
||||||
}
|
sceneIdxWithPerformerTag,
|
||||||
|
sceneIdxWithPerformerTwoTags,
|
||||||
sceneFilter := models.SceneFilterType{
|
sceneIdxWithTwoPerformerTag,
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludesAll,
|
[]int{
|
||||||
}
|
sceneIdxWithPerformer,
|
||||||
|
|
||||||
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]),
|
|
||||||
},
|
},
|
||||||
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)
|
for _, tt := range tests {
|
||||||
findFilter := models.FindFilterType{
|
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||||
Q: &q,
|
assert := assert.New(t)
|
||||||
}
|
|
||||||
|
|
||||||
scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter)
|
results, err := db.Scene.Query(ctx, models.SceneQueryOptions{
|
||||||
assert.Len(t, scenes, 0)
|
SceneFilter: 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{
|
include := indexesToIDs(sceneIDs, tt.includeIdxs)
|
||||||
Modifier: models.CriterionModifierIsNull,
|
exclude := indexesToIDs(sceneIDs, tt.excludeIdxs)
|
||||||
}
|
|
||||||
q = getSceneStringValue(sceneIdx1WithPerformer, titleField)
|
|
||||||
|
|
||||||
scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter)
|
for _, i := range include {
|
||||||
assert.Len(t, scenes, 1)
|
assert.Contains(results.IDs, i)
|
||||||
assert.Equal(t, sceneIDs[sceneIdx1WithPerformer], scenes[0].ID)
|
}
|
||||||
|
for _, e := range exclude {
|
||||||
q = getSceneStringValue(sceneIdxWithPerformerTag, titleField)
|
assert.NotContains(results.IDs, e)
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSceneQueryStudio(t *testing.T) {
|
func TestSceneQueryStudio(t *testing.T) {
|
||||||
@@ -3561,6 +3855,30 @@ func TestSceneQueryStudio(t *testing.T) {
|
|||||||
[]int{sceneIDs[sceneIdxWithGallery]},
|
[]int{sceneIDs[sceneIdxWithGallery]},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"equals",
|
||||||
|
"",
|
||||||
|
models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(studioIDs[studioIdxWithScene]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
},
|
||||||
|
[]int{sceneIDs[sceneIdxWithStudio]},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not equals",
|
||||||
|
getSceneStringValue(sceneIdxWithStudio, titleField),
|
||||||
|
models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(studioIDs[studioIdxWithScene]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierNotEquals,
|
||||||
|
},
|
||||||
|
[]int{},
|
||||||
|
false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
qb := db.Scene
|
qb := db.Scene
|
||||||
|
|||||||
@@ -60,19 +60,24 @@ const (
|
|||||||
sceneIdx1WithPerformer
|
sceneIdx1WithPerformer
|
||||||
sceneIdx2WithPerformer
|
sceneIdx2WithPerformer
|
||||||
sceneIdxWithTwoPerformers
|
sceneIdxWithTwoPerformers
|
||||||
|
sceneIdxWithThreePerformers
|
||||||
sceneIdxWithTag
|
sceneIdxWithTag
|
||||||
sceneIdxWithTwoTags
|
sceneIdxWithTwoTags
|
||||||
|
sceneIdxWithThreeTags
|
||||||
sceneIdxWithMarkerAndTag
|
sceneIdxWithMarkerAndTag
|
||||||
|
sceneIdxWithMarkerTwoTags
|
||||||
sceneIdxWithStudio
|
sceneIdxWithStudio
|
||||||
sceneIdx1WithStudio
|
sceneIdx1WithStudio
|
||||||
sceneIdx2WithStudio
|
sceneIdx2WithStudio
|
||||||
sceneIdxWithMarkers
|
sceneIdxWithMarkers
|
||||||
sceneIdxWithPerformerTag
|
sceneIdxWithPerformerTag
|
||||||
|
sceneIdxWithTwoPerformerTag
|
||||||
sceneIdxWithPerformerTwoTags
|
sceneIdxWithPerformerTwoTags
|
||||||
sceneIdxWithSpacedName
|
sceneIdxWithSpacedName
|
||||||
sceneIdxWithStudioPerformer
|
sceneIdxWithStudioPerformer
|
||||||
sceneIdxWithGrandChildStudio
|
sceneIdxWithGrandChildStudio
|
||||||
sceneIdxMissingPhash
|
sceneIdxMissingPhash
|
||||||
|
sceneIdxWithPerformerParentTag
|
||||||
// new indexes above
|
// new indexes above
|
||||||
lastSceneIdx
|
lastSceneIdx
|
||||||
|
|
||||||
@@ -90,16 +95,20 @@ const (
|
|||||||
imageIdx1WithPerformer
|
imageIdx1WithPerformer
|
||||||
imageIdx2WithPerformer
|
imageIdx2WithPerformer
|
||||||
imageIdxWithTwoPerformers
|
imageIdxWithTwoPerformers
|
||||||
|
imageIdxWithThreePerformers
|
||||||
imageIdxWithTag
|
imageIdxWithTag
|
||||||
imageIdxWithTwoTags
|
imageIdxWithTwoTags
|
||||||
|
imageIdxWithThreeTags
|
||||||
imageIdxWithStudio
|
imageIdxWithStudio
|
||||||
imageIdx1WithStudio
|
imageIdx1WithStudio
|
||||||
imageIdx2WithStudio
|
imageIdx2WithStudio
|
||||||
imageIdxWithStudioPerformer
|
imageIdxWithStudioPerformer
|
||||||
imageIdxInZip
|
imageIdxInZip
|
||||||
imageIdxWithPerformerTag
|
imageIdxWithPerformerTag
|
||||||
|
imageIdxWithTwoPerformerTag
|
||||||
imageIdxWithPerformerTwoTags
|
imageIdxWithPerformerTwoTags
|
||||||
imageIdxWithGrandChildStudio
|
imageIdxWithGrandChildStudio
|
||||||
|
imageIdxWithPerformerParentTag
|
||||||
// new indexes above
|
// new indexes above
|
||||||
totalImages
|
totalImages
|
||||||
)
|
)
|
||||||
@@ -108,20 +117,25 @@ const (
|
|||||||
performerIdxWithScene = iota
|
performerIdxWithScene = iota
|
||||||
performerIdx1WithScene
|
performerIdx1WithScene
|
||||||
performerIdx2WithScene
|
performerIdx2WithScene
|
||||||
|
performerIdx3WithScene
|
||||||
performerIdxWithTwoScenes
|
performerIdxWithTwoScenes
|
||||||
performerIdxWithImage
|
performerIdxWithImage
|
||||||
performerIdxWithTwoImages
|
performerIdxWithTwoImages
|
||||||
performerIdx1WithImage
|
performerIdx1WithImage
|
||||||
performerIdx2WithImage
|
performerIdx2WithImage
|
||||||
|
performerIdx3WithImage
|
||||||
performerIdxWithTag
|
performerIdxWithTag
|
||||||
|
performerIdx2WithTag
|
||||||
performerIdxWithTwoTags
|
performerIdxWithTwoTags
|
||||||
performerIdxWithGallery
|
performerIdxWithGallery
|
||||||
performerIdxWithTwoGalleries
|
performerIdxWithTwoGalleries
|
||||||
performerIdx1WithGallery
|
performerIdx1WithGallery
|
||||||
performerIdx2WithGallery
|
performerIdx2WithGallery
|
||||||
|
performerIdx3WithGallery
|
||||||
performerIdxWithSceneStudio
|
performerIdxWithSceneStudio
|
||||||
performerIdxWithImageStudio
|
performerIdxWithImageStudio
|
||||||
performerIdxWithGalleryStudio
|
performerIdxWithGalleryStudio
|
||||||
|
performerIdxWithParentTag
|
||||||
// new indexes above
|
// new indexes above
|
||||||
// performers with dup names start from the end
|
// performers with dup names start from the end
|
||||||
performerIdx1WithDupName
|
performerIdx1WithDupName
|
||||||
@@ -155,16 +169,20 @@ const (
|
|||||||
galleryIdx1WithPerformer
|
galleryIdx1WithPerformer
|
||||||
galleryIdx2WithPerformer
|
galleryIdx2WithPerformer
|
||||||
galleryIdxWithTwoPerformers
|
galleryIdxWithTwoPerformers
|
||||||
|
galleryIdxWithThreePerformers
|
||||||
galleryIdxWithTag
|
galleryIdxWithTag
|
||||||
galleryIdxWithTwoTags
|
galleryIdxWithTwoTags
|
||||||
|
galleryIdxWithThreeTags
|
||||||
galleryIdxWithStudio
|
galleryIdxWithStudio
|
||||||
galleryIdx1WithStudio
|
galleryIdx1WithStudio
|
||||||
galleryIdx2WithStudio
|
galleryIdx2WithStudio
|
||||||
galleryIdxWithPerformerTag
|
galleryIdxWithPerformerTag
|
||||||
|
galleryIdxWithTwoPerformerTag
|
||||||
galleryIdxWithPerformerTwoTags
|
galleryIdxWithPerformerTwoTags
|
||||||
galleryIdxWithStudioPerformer
|
galleryIdxWithStudioPerformer
|
||||||
galleryIdxWithGrandChildStudio
|
galleryIdxWithGrandChildStudio
|
||||||
galleryIdxWithoutFile
|
galleryIdxWithoutFile
|
||||||
|
galleryIdxWithPerformerParentTag
|
||||||
// new indexes above
|
// new indexes above
|
||||||
lastGalleryIdx
|
lastGalleryIdx
|
||||||
|
|
||||||
@@ -182,12 +200,14 @@ const (
|
|||||||
tagIdxWithImage
|
tagIdxWithImage
|
||||||
tagIdx1WithImage
|
tagIdx1WithImage
|
||||||
tagIdx2WithImage
|
tagIdx2WithImage
|
||||||
|
tagIdx3WithImage
|
||||||
tagIdxWithPerformer
|
tagIdxWithPerformer
|
||||||
tagIdx1WithPerformer
|
tagIdx1WithPerformer
|
||||||
tagIdx2WithPerformer
|
tagIdx2WithPerformer
|
||||||
tagIdxWithGallery
|
tagIdxWithGallery
|
||||||
tagIdx1WithGallery
|
tagIdx1WithGallery
|
||||||
tagIdx2WithGallery
|
tagIdx2WithGallery
|
||||||
|
tagIdx3WithGallery
|
||||||
tagIdxWithChildTag
|
tagIdxWithChildTag
|
||||||
tagIdxWithParentTag
|
tagIdxWithParentTag
|
||||||
tagIdxWithGrandChild
|
tagIdxWithGrandChild
|
||||||
@@ -332,19 +352,24 @@ var (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
sceneTags = linkMap{
|
sceneTags = linkMap{
|
||||||
sceneIdxWithTag: {tagIdxWithScene},
|
sceneIdxWithTag: {tagIdxWithScene},
|
||||||
sceneIdxWithTwoTags: {tagIdx1WithScene, tagIdx2WithScene},
|
sceneIdxWithTwoTags: {tagIdx1WithScene, tagIdx2WithScene},
|
||||||
sceneIdxWithMarkerAndTag: {tagIdx3WithScene},
|
sceneIdxWithThreeTags: {tagIdx1WithScene, tagIdx2WithScene, tagIdx3WithScene},
|
||||||
|
sceneIdxWithMarkerAndTag: {tagIdx3WithScene},
|
||||||
|
sceneIdxWithMarkerTwoTags: {tagIdx2WithScene, tagIdx3WithScene},
|
||||||
}
|
}
|
||||||
|
|
||||||
scenePerformers = linkMap{
|
scenePerformers = linkMap{
|
||||||
sceneIdxWithPerformer: {performerIdxWithScene},
|
sceneIdxWithPerformer: {performerIdxWithScene},
|
||||||
sceneIdxWithTwoPerformers: {performerIdx1WithScene, performerIdx2WithScene},
|
sceneIdxWithTwoPerformers: {performerIdx1WithScene, performerIdx2WithScene},
|
||||||
sceneIdxWithPerformerTag: {performerIdxWithTag},
|
sceneIdxWithThreePerformers: {performerIdx1WithScene, performerIdx2WithScene, performerIdx3WithScene},
|
||||||
sceneIdxWithPerformerTwoTags: {performerIdxWithTwoTags},
|
sceneIdxWithPerformerTag: {performerIdxWithTag},
|
||||||
sceneIdx1WithPerformer: {performerIdxWithTwoScenes},
|
sceneIdxWithTwoPerformerTag: {performerIdxWithTag, performerIdx2WithTag},
|
||||||
sceneIdx2WithPerformer: {performerIdxWithTwoScenes},
|
sceneIdxWithPerformerTwoTags: {performerIdxWithTwoTags},
|
||||||
sceneIdxWithStudioPerformer: {performerIdxWithSceneStudio},
|
sceneIdx1WithPerformer: {performerIdxWithTwoScenes},
|
||||||
|
sceneIdx2WithPerformer: {performerIdxWithTwoScenes},
|
||||||
|
sceneIdxWithStudioPerformer: {performerIdxWithSceneStudio},
|
||||||
|
sceneIdxWithPerformerParentTag: {performerIdxWithParentTag},
|
||||||
}
|
}
|
||||||
|
|
||||||
sceneGalleries = linkMap{
|
sceneGalleries = linkMap{
|
||||||
@@ -376,6 +401,7 @@ var (
|
|||||||
{sceneIdxWithMarkers, tagIdxWithPrimaryMarkers, nil},
|
{sceneIdxWithMarkers, tagIdxWithPrimaryMarkers, nil},
|
||||||
{sceneIdxWithMarkers, tagIdxWithPrimaryMarkers, []int{tagIdxWithMarkers}},
|
{sceneIdxWithMarkers, tagIdxWithPrimaryMarkers, []int{tagIdxWithMarkers}},
|
||||||
{sceneIdxWithMarkerAndTag, tagIdxWithPrimaryMarkers, nil},
|
{sceneIdxWithMarkerAndTag, tagIdxWithPrimaryMarkers, nil},
|
||||||
|
{sceneIdxWithMarkerTwoTags, tagIdxWithPrimaryMarkers, nil},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -407,29 +433,36 @@ var (
|
|||||||
imageIdxWithGrandChildStudio: studioIdxWithGrandParent,
|
imageIdxWithGrandChildStudio: studioIdxWithGrandParent,
|
||||||
}
|
}
|
||||||
imageTags = linkMap{
|
imageTags = linkMap{
|
||||||
imageIdxWithTag: {tagIdxWithImage},
|
imageIdxWithTag: {tagIdxWithImage},
|
||||||
imageIdxWithTwoTags: {tagIdx1WithImage, tagIdx2WithImage},
|
imageIdxWithTwoTags: {tagIdx1WithImage, tagIdx2WithImage},
|
||||||
|
imageIdxWithThreeTags: {tagIdx1WithImage, tagIdx2WithImage, tagIdx3WithImage},
|
||||||
}
|
}
|
||||||
imagePerformers = linkMap{
|
imagePerformers = linkMap{
|
||||||
imageIdxWithPerformer: {performerIdxWithImage},
|
imageIdxWithPerformer: {performerIdxWithImage},
|
||||||
imageIdxWithTwoPerformers: {performerIdx1WithImage, performerIdx2WithImage},
|
imageIdxWithTwoPerformers: {performerIdx1WithImage, performerIdx2WithImage},
|
||||||
imageIdxWithPerformerTag: {performerIdxWithTag},
|
imageIdxWithThreePerformers: {performerIdx1WithImage, performerIdx2WithImage, performerIdx3WithImage},
|
||||||
imageIdxWithPerformerTwoTags: {performerIdxWithTwoTags},
|
imageIdxWithPerformerTag: {performerIdxWithTag},
|
||||||
imageIdx1WithPerformer: {performerIdxWithTwoImages},
|
imageIdxWithTwoPerformerTag: {performerIdxWithTag, performerIdx2WithTag},
|
||||||
imageIdx2WithPerformer: {performerIdxWithTwoImages},
|
imageIdxWithPerformerTwoTags: {performerIdxWithTwoTags},
|
||||||
imageIdxWithStudioPerformer: {performerIdxWithImageStudio},
|
imageIdx1WithPerformer: {performerIdxWithTwoImages},
|
||||||
|
imageIdx2WithPerformer: {performerIdxWithTwoImages},
|
||||||
|
imageIdxWithStudioPerformer: {performerIdxWithImageStudio},
|
||||||
|
imageIdxWithPerformerParentTag: {performerIdxWithParentTag},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
galleryPerformers = linkMap{
|
galleryPerformers = linkMap{
|
||||||
galleryIdxWithPerformer: {performerIdxWithGallery},
|
galleryIdxWithPerformer: {performerIdxWithGallery},
|
||||||
galleryIdxWithTwoPerformers: {performerIdx1WithGallery, performerIdx2WithGallery},
|
galleryIdxWithTwoPerformers: {performerIdx1WithGallery, performerIdx2WithGallery},
|
||||||
galleryIdxWithPerformerTag: {performerIdxWithTag},
|
galleryIdxWithThreePerformers: {performerIdx1WithGallery, performerIdx2WithGallery, performerIdx3WithGallery},
|
||||||
galleryIdxWithPerformerTwoTags: {performerIdxWithTwoTags},
|
galleryIdxWithPerformerTag: {performerIdxWithTag},
|
||||||
galleryIdx1WithPerformer: {performerIdxWithTwoGalleries},
|
galleryIdxWithTwoPerformerTag: {performerIdxWithTag, performerIdx2WithTag},
|
||||||
galleryIdx2WithPerformer: {performerIdxWithTwoGalleries},
|
galleryIdxWithPerformerTwoTags: {performerIdxWithTwoTags},
|
||||||
galleryIdxWithStudioPerformer: {performerIdxWithGalleryStudio},
|
galleryIdx1WithPerformer: {performerIdxWithTwoGalleries},
|
||||||
|
galleryIdx2WithPerformer: {performerIdxWithTwoGalleries},
|
||||||
|
galleryIdxWithStudioPerformer: {performerIdxWithGalleryStudio},
|
||||||
|
galleryIdxWithPerformerParentTag: {performerIdxWithParentTag},
|
||||||
}
|
}
|
||||||
|
|
||||||
galleryStudios = map[int]int{
|
galleryStudios = map[int]int{
|
||||||
@@ -441,8 +474,9 @@ var (
|
|||||||
}
|
}
|
||||||
|
|
||||||
galleryTags = linkMap{
|
galleryTags = linkMap{
|
||||||
galleryIdxWithTag: {tagIdxWithGallery},
|
galleryIdxWithTag: {tagIdxWithGallery},
|
||||||
galleryIdxWithTwoTags: {tagIdx1WithGallery, tagIdx2WithGallery},
|
galleryIdxWithTwoTags: {tagIdx1WithGallery, tagIdx2WithGallery},
|
||||||
|
galleryIdxWithThreeTags: {tagIdx1WithGallery, tagIdx2WithGallery, tagIdx3WithGallery},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -462,8 +496,10 @@ var (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
performerTags = linkMap{
|
performerTags = linkMap{
|
||||||
performerIdxWithTag: {tagIdxWithPerformer},
|
performerIdxWithTag: {tagIdxWithPerformer},
|
||||||
performerIdxWithTwoTags: {tagIdx1WithPerformer, tagIdx2WithPerformer},
|
performerIdx2WithTag: {tagIdx2WithPerformer},
|
||||||
|
performerIdxWithTwoTags: {tagIdx1WithPerformer, tagIdx2WithPerformer},
|
||||||
|
performerIdxWithParentTag: {tagIdxWithParentAndChild},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -484,6 +520,16 @@ func indexesToIDs(ids []int, indexes []int) []int {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func indexFromID(ids []int, id int) int {
|
||||||
|
for i, v := range ids {
|
||||||
|
if v == id {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
var db *sqlite.Database
|
var db *sqlite.Database
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
@@ -1431,11 +1477,8 @@ func getTagStringValue(index int, field string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getTagSceneCount(id int) int {
|
func getTagSceneCount(id int) int {
|
||||||
if id == tagIDs[tagIdx1WithScene] || id == tagIDs[tagIdx2WithScene] || id == tagIDs[tagIdxWithScene] || id == tagIDs[tagIdx3WithScene] {
|
idx := indexFromID(tagIDs, id)
|
||||||
return 1
|
return len(sceneTags.reverseLookup(idx))
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTagMarkerCount(id int) int {
|
func getTagMarkerCount(id int) int {
|
||||||
@@ -1451,27 +1494,18 @@ func getTagMarkerCount(id int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getTagImageCount(id int) int {
|
func getTagImageCount(id int) int {
|
||||||
if id == tagIDs[tagIdx1WithImage] || id == tagIDs[tagIdx2WithImage] || id == tagIDs[tagIdxWithImage] {
|
idx := indexFromID(tagIDs, id)
|
||||||
return 1
|
return len(imageTags.reverseLookup(idx))
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTagGalleryCount(id int) int {
|
func getTagGalleryCount(id int) int {
|
||||||
if id == tagIDs[tagIdx1WithGallery] || id == tagIDs[tagIdx2WithGallery] || id == tagIDs[tagIdxWithGallery] {
|
idx := indexFromID(tagIDs, id)
|
||||||
return 1
|
return len(galleryTags.reverseLookup(idx))
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTagPerformerCount(id int) int {
|
func getTagPerformerCount(id int) int {
|
||||||
if id == tagIDs[tagIdx1WithPerformer] || id == tagIDs[tagIdx2WithPerformer] || id == tagIDs[tagIdxWithPerformer] {
|
idx := indexFromID(tagIDs, id)
|
||||||
return 1
|
return len(performerTags.reverseLookup(idx))
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTagParentCount(id int) int {
|
func getTagParentCount(id int) int {
|
||||||
|
|||||||
@@ -474,9 +474,19 @@ func tagMarkerCountCriterionHandler(qb *tagQueryBuilder, markerCount *models.Int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func tagParentsCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
func tagParentsCriterionHandler(qb *tagQueryBuilder, criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if tags != nil {
|
if criterion != nil {
|
||||||
|
tags := criterion.CombineExcludes()
|
||||||
|
|
||||||
|
// validate the modifier
|
||||||
|
switch tags.Modifier {
|
||||||
|
case models.CriterionModifierIncludesAll, models.CriterionModifierIncludes, models.CriterionModifierExcludes, models.CriterionModifierIsNull, models.CriterionModifierNotNull:
|
||||||
|
// valid
|
||||||
|
default:
|
||||||
|
f.setError(fmt.Errorf("invalid modifier %s for tag parent/children", criterion.Modifier))
|
||||||
|
}
|
||||||
|
|
||||||
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
|
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
|
||||||
var notClause string
|
var notClause string
|
||||||
if tags.Modifier == models.CriterionModifierNotNull {
|
if tags.Modifier == models.CriterionModifierNotNull {
|
||||||
@@ -489,43 +499,88 @@ func tagParentsCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalMu
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tags.Value) == 0 {
|
if len(tags.Value) == 0 && len(tags.Excludes) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var args []interface{}
|
if len(tags.Value) > 0 {
|
||||||
for _, val := range tags.Value {
|
var args []interface{}
|
||||||
args = append(args, val)
|
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 len(tags.Excludes) > 0 {
|
||||||
if tags.Depth != nil {
|
var args []interface{}
|
||||||
depthVal = *tags.Depth
|
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) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if tags != nil {
|
if criterion != nil {
|
||||||
|
tags := criterion.CombineExcludes()
|
||||||
|
|
||||||
|
// validate the modifier
|
||||||
|
switch tags.Modifier {
|
||||||
|
case models.CriterionModifierIncludesAll, models.CriterionModifierIncludes, models.CriterionModifierExcludes, models.CriterionModifierIsNull, models.CriterionModifierNotNull:
|
||||||
|
// valid
|
||||||
|
default:
|
||||||
|
f.setError(fmt.Errorf("invalid modifier %s for tag parent/children", criterion.Modifier))
|
||||||
|
}
|
||||||
|
|
||||||
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
|
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
|
||||||
var notClause string
|
var notClause string
|
||||||
if tags.Modifier == models.CriterionModifierNotNull {
|
if tags.Modifier == models.CriterionModifierNotNull {
|
||||||
@@ -538,36 +593,71 @@ func tagChildrenCriterionHandler(qb *tagQueryBuilder, tags *models.HierarchicalM
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tags.Value) == 0 {
|
if len(tags.Value) == 0 && len(tags.Excludes) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var args []interface{}
|
if len(tags.Value) > 0 {
|
||||||
for _, val := range tags.Value {
|
var args []interface{}
|
||||||
args = append(args, val)
|
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 len(tags.Excludes) > 0 {
|
||||||
if tags.Depth != nil {
|
var args []interface{}
|
||||||
depthVal = *tags.Depth
|
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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ func TestTagQuerySort(t *testing.T) {
|
|||||||
|
|
||||||
tags := queryTags(ctx, t, sqb, nil, findFilter)
|
tags := queryTags(ctx, t, sqb, nil, findFilter)
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
assert.Equal(tagIDs[tagIdxWithScene], tags[0].ID)
|
assert.Equal(tagIDs[tagIdx2WithScene], tags[0].ID)
|
||||||
|
|
||||||
sortBy = "scene_markers_count"
|
sortBy = "scene_markers_count"
|
||||||
tags = queryTags(ctx, t, sqb, nil, findFilter)
|
tags = queryTags(ctx, t, sqb, nil, findFilter)
|
||||||
@@ -195,15 +195,15 @@ func TestTagQuerySort(t *testing.T) {
|
|||||||
|
|
||||||
sortBy = "images_count"
|
sortBy = "images_count"
|
||||||
tags = queryTags(ctx, t, sqb, nil, findFilter)
|
tags = queryTags(ctx, t, sqb, nil, findFilter)
|
||||||
assert.Equal(tagIDs[tagIdxWithImage], tags[0].ID)
|
assert.Equal(tagIDs[tagIdx1WithImage], tags[0].ID)
|
||||||
|
|
||||||
sortBy = "galleries_count"
|
sortBy = "galleries_count"
|
||||||
tags = queryTags(ctx, t, sqb, nil, findFilter)
|
tags = queryTags(ctx, t, sqb, nil, findFilter)
|
||||||
assert.Equal(tagIDs[tagIdxWithGallery], tags[0].ID)
|
assert.Equal(tagIDs[tagIdx1WithGallery], tags[0].ID)
|
||||||
|
|
||||||
sortBy = "performers_count"
|
sortBy = "performers_count"
|
||||||
tags = queryTags(ctx, t, sqb, nil, findFilter)
|
tags = queryTags(ctx, t, sqb, nil, findFilter)
|
||||||
assert.Equal(tagIDs[tagIdxWithPerformer], tags[0].ID)
|
assert.Equal(tagIDs[tagIdx2WithPerformer], tags[0].ID)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -309,14 +309,18 @@ export const HierarchicalObjectsFilter = <
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Group>
|
{criterion.modifier !== CriterionModifier.Equals && (
|
||||||
<Form.Check
|
<Form.Group>
|
||||||
id={criterionOptionTypeToIncludeID()}
|
<Form.Check
|
||||||
checked={criterion.value.depth !== 0}
|
id={criterionOptionTypeToIncludeID()}
|
||||||
label={intl.formatMessage(criterionOptionTypeToIncludeUIString())}
|
checked={criterion.value.depth !== 0}
|
||||||
onChange={() => onDepthChanged(criterion.value.depth !== 0 ? 0 : -1)}
|
label={intl.formatMessage(criterionOptionTypeToIncludeUIString())}
|
||||||
/>
|
onChange={() =>
|
||||||
</Form.Group>
|
onDepthChanged(criterion.value.depth !== 0 ? 0 : -1)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
)}
|
||||||
|
|
||||||
{criterion.value.depth !== 0 && (
|
{criterion.value.depth !== 0 && (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
|
|||||||
@@ -567,6 +567,11 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
|
|||||||
|
|
||||||
protected toCriterionInput(): HierarchicalMultiCriterionInput {
|
protected toCriterionInput(): HierarchicalMultiCriterionInput {
|
||||||
let excludes: string[] = [];
|
let excludes: string[] = [];
|
||||||
|
|
||||||
|
// if modifier is equals, depth must be 0
|
||||||
|
const depth =
|
||||||
|
this.modifier === CriterionModifier.Equals ? 0 : this.value.depth;
|
||||||
|
|
||||||
if (this.value.excluded) {
|
if (this.value.excluded) {
|
||||||
excludes = this.value.excluded.map((v) => v.id);
|
excludes = this.value.excluded.map((v) => v.id);
|
||||||
}
|
}
|
||||||
@@ -574,7 +579,7 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
|
|||||||
value: this.value.items.map((v) => v.id),
|
value: this.value.items.map((v) => v.id),
|
||||||
excludes: excludes,
|
excludes: excludes,
|
||||||
modifier: this.modifier,
|
modifier: this.modifier,
|
||||||
depth: this.value.depth,
|
depth,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,24 @@ import { CriterionOption, IHierarchicalLabeledIdCriterion } from "./criterion";
|
|||||||
|
|
||||||
export class TagsCriterion extends IHierarchicalLabeledIdCriterion {}
|
export class TagsCriterion extends IHierarchicalLabeledIdCriterion {}
|
||||||
|
|
||||||
class tagsCriterionOption extends CriterionOption {
|
const tagsModifierOptions = [
|
||||||
constructor(messageID: string, value: CriterionType, parameterName: string) {
|
CriterionModifier.Includes,
|
||||||
const modifierOptions = [
|
CriterionModifier.IncludesAll,
|
||||||
CriterionModifier.Includes,
|
CriterionModifier.Equals,
|
||||||
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;
|
let defaultModifier = CriterionModifier.IncludesAll;
|
||||||
|
|
||||||
super({
|
super({
|
||||||
@@ -27,25 +37,30 @@ class tagsCriterionOption extends CriterionOption {
|
|||||||
export const TagsCriterionOption = new tagsCriterionOption(
|
export const TagsCriterionOption = new tagsCriterionOption(
|
||||||
"tags",
|
"tags",
|
||||||
"tags",
|
"tags",
|
||||||
"tags"
|
"tags",
|
||||||
|
tagsModifierOptions
|
||||||
);
|
);
|
||||||
export const SceneTagsCriterionOption = new tagsCriterionOption(
|
export const SceneTagsCriterionOption = new tagsCriterionOption(
|
||||||
"sceneTags",
|
"sceneTags",
|
||||||
"sceneTags",
|
"sceneTags",
|
||||||
"scene_tags"
|
"scene_tags",
|
||||||
|
tagsModifierOptions
|
||||||
);
|
);
|
||||||
export const PerformerTagsCriterionOption = new tagsCriterionOption(
|
export const PerformerTagsCriterionOption = new tagsCriterionOption(
|
||||||
"performerTags",
|
"performerTags",
|
||||||
"performerTags",
|
"performerTags",
|
||||||
"performer_tags"
|
"performer_tags",
|
||||||
|
withoutEqualsModifierOptions
|
||||||
);
|
);
|
||||||
export const ParentTagsCriterionOption = new tagsCriterionOption(
|
export const ParentTagsCriterionOption = new tagsCriterionOption(
|
||||||
"parent_tags",
|
"parent_tags",
|
||||||
"parentTags",
|
"parentTags",
|
||||||
"parents"
|
"parents",
|
||||||
|
withoutEqualsModifierOptions
|
||||||
);
|
);
|
||||||
export const ChildTagsCriterionOption = new tagsCriterionOption(
|
export const ChildTagsCriterionOption = new tagsCriterionOption(
|
||||||
"sub_tags",
|
"sub_tags",
|
||||||
"childTags",
|
"childTags",
|
||||||
"children"
|
"children",
|
||||||
|
withoutEqualsModifierOptions
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user