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

* Support Is (not) null for all multi criterions

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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