Join count filter criteria (#1254)

Co-authored-by: mrbrdo <mrbrdo@gmail.com>
Co-authored-by: peolic <66393006+peolic@users.noreply.github.com>
This commit is contained in:
WithoutPants
2021-04-09 18:46:00 +10:00
committed by GitHub
parent 6a0c73b3a1
commit a2582047ca
17 changed files with 743 additions and 29 deletions

View File

@@ -61,6 +61,14 @@ input PerformerFilterType {
is_missing: String
"""Filter to only include performers with these tags"""
tags: MultiCriterionInput
"""Filter by tag count"""
tag_count: IntCriterionInput
"""Filter by scene count"""
scene_count: IntCriterionInput
"""Filter by image count"""
image_count: IntCriterionInput
"""Filter by gallery count"""
gallery_count: IntCriterionInput
"""Filter by StashID"""
stash_id: String
"""Filter by url"""
@@ -105,10 +113,14 @@ input SceneFilterType {
movies: MultiCriterionInput
"""Filter to only include scenes with these tags"""
tags: MultiCriterionInput
"""Filter by tag count"""
tag_count: IntCriterionInput
"""Filter to only include scenes with performers with these tags"""
performer_tags: MultiCriterionInput
"""Filter to only include scenes with these performers"""
performers: MultiCriterionInput
"""Filter by performer count"""
performer_count: IntCriterionInput
"""Filter by StashID"""
stash_id: String
"""Filter by url"""
@@ -152,10 +164,14 @@ input GalleryFilterType {
studios: MultiCriterionInput
"""Filter to only include galleries with these tags"""
tags: MultiCriterionInput
"""Filter by tag count"""
tag_count: IntCriterionInput
"""Filter to only include galleries with performers with these tags"""
performer_tags: MultiCriterionInput
"""Filter to only include galleries with these performers"""
performers: MultiCriterionInput
"""Filter by performer count"""
performer_count: IntCriterionInput
"""Filter by number of images in this gallery"""
image_count: IntCriterionInput
"""Filter by url"""
@@ -203,10 +219,14 @@ input ImageFilterType {
studios: MultiCriterionInput
"""Filter to only include images with these tags"""
tags: MultiCriterionInput
"""Filter by tag count"""
tag_count: IntCriterionInput
"""Filter to only include images with performers with these tags"""
performer_tags: MultiCriterionInput
"""Filter to only include images with these performers"""
performers: MultiCriterionInput
"""Filter by performer count"""
performer_count: IntCriterionInput
"""Filter to only include images with these galleries"""
galleries: MultiCriterionInput
}

View File

@@ -405,3 +405,23 @@ func (m *multiCriterionHandlerBuilder) handler(criterion *models.MultiCriterionI
}
}
}
type countCriterionHandlerBuilder struct {
primaryTable string
joinTable string
primaryFK string
}
func (m *countCriterionHandlerBuilder) handler(criterion *models.IntCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) {
if criterion != nil {
clause, count := getCountCriterionClause(m.primaryTable, m.joinTable, m.primaryFK, *criterion)
if count == 1 {
f.addWhere(clause, criterion.Value)
} else {
f.addWhere(clause)
}
}
}
}

View File

@@ -239,6 +239,16 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi
query.addHaving(havingClause)
}
if tagCountFilter := galleryFilter.TagCount; tagCountFilter != nil {
clause, count := getCountCriterionClause(galleryTable, galleriesTagsTable, galleryIDColumn, *tagCountFilter)
if count == 1 {
query.addArg(tagCountFilter.Value)
}
query.addWhere(clause)
}
if performersFilter := galleryFilter.Performers; performersFilter != nil && len(performersFilter.Value) > 0 {
for _, performerID := range performersFilter.Value {
query.addArg(performerID)
@@ -250,6 +260,16 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi
query.addHaving(havingClause)
}
if performerCountFilter := galleryFilter.PerformerCount; performerCountFilter != nil {
clause, count := getCountCriterionClause(galleryTable, performersGalleriesTable, galleryIDColumn, *performerCountFilter)
if count == 1 {
query.addArg(performerCountFilter.Value)
}
query.addWhere(clause)
}
if studiosFilter := galleryFilter.Studios; studiosFilter != nil && len(studiosFilter.Value) > 0 {
for _, studioID := range studiosFilter.Value {
query.addArg(studioID)
@@ -382,7 +402,15 @@ func (qb *galleryQueryBuilder) getGallerySort(findFilter *models.FindFilterType)
sort = findFilter.GetSort("path")
direction = findFilter.GetDirection()
}
switch sort {
case "tag_count":
return getCountSort(galleryTable, galleriesTagsTable, galleryIDColumn, direction)
case "performer_count":
return getCountSort(galleryTable, performersGalleriesTable, galleryIDColumn, direction)
default:
return getSort(sort, direction, "galleries")
}
}
func (qb *galleryQueryBuilder) queryGallery(query string, args []interface{}) (*models.Gallery, error) {

View File

@@ -630,6 +630,88 @@ func TestGalleryQueryPerformerTags(t *testing.T) {
})
}
func TestGalleryQueryTagCount(t *testing.T) {
const tagCount = 1
tagCountCriterion := models.IntCriterionInput{
Value: tagCount,
Modifier: models.CriterionModifierEquals,
}
verifyGalleriesTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyGalleriesTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyGalleriesTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierLessThan
verifyGalleriesTagCount(t, tagCountCriterion)
}
func verifyGalleriesTagCount(t *testing.T, tagCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Gallery()
galleryFilter := models.GalleryFilterType{
TagCount: &tagCountCriterion,
}
galleries := queryGallery(t, sqb, &galleryFilter, nil)
assert.Greater(t, len(galleries), 0)
for _, gallery := range galleries {
ids, err := sqb.GetTagIDs(gallery.ID)
if err != nil {
return err
}
verifyInt(t, len(ids), tagCountCriterion)
}
return nil
})
}
func TestGalleryQueryPerformerCount(t *testing.T) {
const performerCount = 1
performerCountCriterion := models.IntCriterionInput{
Value: performerCount,
Modifier: models.CriterionModifierEquals,
}
verifyGalleriesPerformerCount(t, performerCountCriterion)
performerCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyGalleriesPerformerCount(t, performerCountCriterion)
performerCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyGalleriesPerformerCount(t, performerCountCriterion)
performerCountCriterion.Modifier = models.CriterionModifierLessThan
verifyGalleriesPerformerCount(t, performerCountCriterion)
}
func verifyGalleriesPerformerCount(t *testing.T, performerCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Gallery()
galleryFilter := models.GalleryFilterType{
PerformerCount: &performerCountCriterion,
}
galleries := queryGallery(t, sqb, &galleryFilter, nil)
assert.Greater(t, len(galleries), 0)
for _, gallery := range galleries {
ids, err := sqb.GetPerformerIDs(gallery.ID)
if err != nil {
return err
}
verifyInt(t, len(ids), performerCountCriterion)
}
return nil
})
}
// TODO Count
// TODO All
// TODO Query

View File

@@ -328,6 +328,16 @@ func (qb *imageQueryBuilder) Query(imageFilter *models.ImageFilterType, findFilt
query.addHaving(havingClause)
}
if tagCountFilter := imageFilter.TagCount; tagCountFilter != nil {
clause, count := getCountCriterionClause(imageTable, imagesTagsTable, imageIDColumn, *tagCountFilter)
if count == 1 {
query.addArg(tagCountFilter.Value)
}
query.addWhere(clause)
}
if galleriesFilter := imageFilter.Galleries; galleriesFilter != nil && len(galleriesFilter.Value) > 0 {
for _, galleryID := range galleriesFilter.Value {
query.addArg(galleryID)
@@ -350,6 +360,16 @@ func (qb *imageQueryBuilder) Query(imageFilter *models.ImageFilterType, findFilt
query.addHaving(havingClause)
}
if performerCountFilter := imageFilter.PerformerCount; performerCountFilter != nil {
clause, count := getCountCriterionClause(imageTable, performersImagesTable, imageIDColumn, *performerCountFilter)
if count == 1 {
query.addArg(performerCountFilter.Value)
}
query.addWhere(clause)
}
if studiosFilter := imageFilter.Studios; studiosFilter != nil && len(studiosFilter.Value) > 0 {
for _, studioID := range studiosFilter.Value {
query.addArg(studioID)
@@ -412,7 +432,15 @@ func (qb *imageQueryBuilder) getImageSort(findFilter *models.FindFilterType) str
}
sort := findFilter.GetSort("title")
direction := findFilter.GetDirection()
switch sort {
case "tag_count":
return getCountSort(imageTable, imagesTagsTable, imageIDColumn, direction)
case "performer_count":
return getCountSort(imageTable, performersImagesTable, imageIDColumn, direction)
default:
return getSort(sort, direction, "images")
}
}
func (qb *imageQueryBuilder) queryImage(query string, args []interface{}) (*models.Image, error) {

View File

@@ -683,6 +683,88 @@ func TestImageQueryPerformerTags(t *testing.T) {
})
}
func TestImageQueryTagCount(t *testing.T) {
const tagCount = 1
tagCountCriterion := models.IntCriterionInput{
Value: tagCount,
Modifier: models.CriterionModifierEquals,
}
verifyImagesTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyImagesTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyImagesTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierLessThan
verifyImagesTagCount(t, tagCountCriterion)
}
func verifyImagesTagCount(t *testing.T, tagCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Image()
imageFilter := models.ImageFilterType{
TagCount: &tagCountCriterion,
}
images := queryImages(t, sqb, &imageFilter, nil)
assert.Greater(t, len(images), 0)
for _, image := range images {
ids, err := sqb.GetTagIDs(image.ID)
if err != nil {
return err
}
verifyInt(t, len(ids), tagCountCriterion)
}
return nil
})
}
func TestImageQueryPerformerCount(t *testing.T) {
const performerCount = 1
performerCountCriterion := models.IntCriterionInput{
Value: performerCount,
Modifier: models.CriterionModifierEquals,
}
verifyImagesPerformerCount(t, performerCountCriterion)
performerCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyImagesPerformerCount(t, performerCountCriterion)
performerCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyImagesPerformerCount(t, performerCountCriterion)
performerCountCriterion.Modifier = models.CriterionModifierLessThan
verifyImagesPerformerCount(t, performerCountCriterion)
}
func verifyImagesPerformerCount(t *testing.T, performerCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Image()
imageFilter := models.ImageFilterType{
PerformerCount: &performerCountCriterion,
}
images := queryImages(t, sqb, &imageFilter, nil)
assert.Greater(t, len(images), 0)
for _, image := range images {
ids, err := sqb.GetPerformerIDs(image.ID)
if err != nil {
return err
}
verifyInt(t, len(ids), performerCountCriterion)
}
return nil
})
}
func TestImageQuerySorting(t *testing.T) {
withTxn(func(r models.Repository) error {
sort := titleField

View File

@@ -271,6 +271,11 @@ func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterTy
query.addHaving(havingClause)
}
query.handleCountCriterion(performerFilter.TagCount, performerTable, performersTagsTable, performerIDColumn)
query.handleCountCriterion(performerFilter.SceneCount, performerTable, performersScenesTable, performerIDColumn)
query.handleCountCriterion(performerFilter.ImageCount, performerTable, performersImagesTable, performerIDColumn)
query.handleCountCriterion(performerFilter.GalleryCount, performerTable, performersGalleriesTable, performerIDColumn)
query.sortAndPagination = qb.getPerformerSort(findFilter) + getPagination(findFilter)
idsResult, countResult, err := query.executeFind()
if err != nil {
@@ -370,6 +375,11 @@ func (qb *performerQueryBuilder) getPerformerSort(findFilter *models.FindFilterT
sort = findFilter.GetSort("name")
direction = findFilter.GetDirection()
}
if sort == "tag_count" {
return getCountSort(performerTable, performersTagsTable, performerIDColumn, direction)
}
return getSort(sort, direction, "performers")
}

View File

@@ -387,6 +387,188 @@ func TestPerformerQueryTags(t *testing.T) {
})
}
func TestPerformerQueryTagCount(t *testing.T) {
const tagCount = 1
tagCountCriterion := models.IntCriterionInput{
Value: tagCount,
Modifier: models.CriterionModifierEquals,
}
verifyPerformersTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyPerformersTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyPerformersTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierLessThan
verifyPerformersTagCount(t, tagCountCriterion)
}
func verifyPerformersTagCount(t *testing.T, tagCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Performer()
performerFilter := models.PerformerFilterType{
TagCount: &tagCountCriterion,
}
performers := queryPerformers(t, sqb, &performerFilter, nil)
assert.Greater(t, len(performers), 0)
for _, performer := range performers {
ids, err := sqb.GetTagIDs(performer.ID)
if err != nil {
return err
}
verifyInt(t, len(ids), tagCountCriterion)
}
return nil
})
}
func TestPerformerQuerySceneCount(t *testing.T) {
const sceneCount = 1
sceneCountCriterion := models.IntCriterionInput{
Value: sceneCount,
Modifier: models.CriterionModifierEquals,
}
verifyPerformersSceneCount(t, sceneCountCriterion)
sceneCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyPerformersSceneCount(t, sceneCountCriterion)
sceneCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyPerformersSceneCount(t, sceneCountCriterion)
sceneCountCriterion.Modifier = models.CriterionModifierLessThan
verifyPerformersSceneCount(t, sceneCountCriterion)
}
func verifyPerformersSceneCount(t *testing.T, sceneCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Performer()
performerFilter := models.PerformerFilterType{
SceneCount: &sceneCountCriterion,
}
performers := queryPerformers(t, sqb, &performerFilter, nil)
assert.Greater(t, len(performers), 0)
for _, performer := range performers {
ids, err := r.Scene().FindByPerformerID(performer.ID)
if err != nil {
return err
}
verifyInt(t, len(ids), sceneCountCriterion)
}
return nil
})
}
func TestPerformerQueryImageCount(t *testing.T) {
const imageCount = 1
imageCountCriterion := models.IntCriterionInput{
Value: imageCount,
Modifier: models.CriterionModifierEquals,
}
verifyPerformersImageCount(t, imageCountCriterion)
imageCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyPerformersImageCount(t, imageCountCriterion)
imageCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyPerformersImageCount(t, imageCountCriterion)
imageCountCriterion.Modifier = models.CriterionModifierLessThan
verifyPerformersImageCount(t, imageCountCriterion)
}
func verifyPerformersImageCount(t *testing.T, imageCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Performer()
performerFilter := models.PerformerFilterType{
ImageCount: &imageCountCriterion,
}
performers := queryPerformers(t, sqb, &performerFilter, nil)
assert.Greater(t, len(performers), 0)
for _, performer := range performers {
pp := 0
_, count, err := r.Image().Query(&models.ImageFilterType{
Performers: &models.MultiCriterionInput{
Value: []string{strconv.Itoa(performer.ID)},
Modifier: models.CriterionModifierIncludes,
},
}, &models.FindFilterType{
PerPage: &pp,
})
if err != nil {
return err
}
verifyInt(t, count, imageCountCriterion)
}
return nil
})
}
func TestPerformerQueryGalleryCount(t *testing.T) {
const galleryCount = 1
galleryCountCriterion := models.IntCriterionInput{
Value: galleryCount,
Modifier: models.CriterionModifierEquals,
}
verifyPerformersGalleryCount(t, galleryCountCriterion)
galleryCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyPerformersGalleryCount(t, galleryCountCriterion)
galleryCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyPerformersGalleryCount(t, galleryCountCriterion)
galleryCountCriterion.Modifier = models.CriterionModifierLessThan
verifyPerformersGalleryCount(t, galleryCountCriterion)
}
func verifyPerformersGalleryCount(t *testing.T, galleryCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Performer()
performerFilter := models.PerformerFilterType{
GalleryCount: &galleryCountCriterion,
}
performers := queryPerformers(t, sqb, &performerFilter, nil)
assert.Greater(t, len(performers), 0)
for _, performer := range performers {
pp := 0
_, count, err := r.Gallery().Query(&models.GalleryFilterType{
Performers: &models.MultiCriterionInput{
Value: []string{strconv.Itoa(performer.ID)},
Modifier: models.CriterionModifierIncludes,
},
}, &models.FindFilterType{
PerPage: &pp,
})
if err != nil {
return err
}
verifyInt(t, count, galleryCountCriterion)
}
return nil
})
}
func TestPerformerStashIDs(t *testing.T) {
if err := withTxn(func(r models.Repository) error {
qb := r.Performer()

View File

@@ -151,3 +151,15 @@ func (qb *queryBuilder) handleStringCriterionInput(c *models.StringCriterionInpu
}
}
}
func (qb *queryBuilder) handleCountCriterion(countFilter *models.IntCriterionInput, primaryTable, joinTable, primaryFK string) {
if countFilter != nil {
clause, count := getCountCriterionClause(primaryTable, joinTable, primaryFK, *countFilter)
if count == 1 {
qb.addArg(countFilter.Value)
}
qb.addWhere(clause)
}
}

View File

@@ -286,7 +286,7 @@ func (qb *sceneQueryBuilder) Wall(q *string) ([]*models.Scene, error) {
}
func (qb *sceneQueryBuilder) All() ([]*models.Scene, error) {
return qb.queryScenes(selectAll(sceneTable)+qb.getSceneSort(nil), nil)
return qb.queryScenes(selectAll(sceneTable)+qb.getDefaultSceneSort(), nil)
}
func illegalFilterCombination(type1, type2 string) error {
@@ -348,7 +348,9 @@ func (qb *sceneQueryBuilder) makeFilter(sceneFilter *models.SceneFilterType) *fi
query.handleCriterionFunc(stringCriterionHandler(sceneFilter.URL, "scenes.url"))
query.handleCriterionFunc(sceneTagsCriterionHandler(qb, sceneFilter.Tags))
query.handleCriterionFunc(sceneTagCountCriterionHandler(qb, sceneFilter.TagCount))
query.handleCriterionFunc(scenePerformersCriterionHandler(qb, sceneFilter.Performers))
query.handleCriterionFunc(scenePerformerCountCriterionHandler(qb, sceneFilter.PerformerCount))
query.handleCriterionFunc(sceneStudioCriterionHandler(qb, sceneFilter.Studios))
query.handleCriterionFunc(sceneMoviesCriterionHandler(qb, sceneFilter.Movies))
query.handleCriterionFunc(sceneStashIDsHandler(qb, sceneFilter.StashID))
@@ -384,7 +386,8 @@ func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilt
query.addFilter(filter)
query.sortAndPagination = qb.getSceneSort(findFilter) + getPagination(findFilter)
qb.setSceneSort(&query, findFilter)
query.sortAndPagination += getPagination(findFilter)
idsResult, countResult, err := query.executeFind()
if err != nil {
@@ -520,6 +523,7 @@ func (qb *sceneQueryBuilder) getMultiCriterionHandlerBuilder(foreignTable, joinT
addJoinsFunc: addJoinsFunc,
}
}
func sceneTagsCriterionHandler(qb *sceneQueryBuilder, tags *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) {
qb.tagsRepository().join(f, "tags_join", "scenes.id")
@@ -530,6 +534,16 @@ func sceneTagsCriterionHandler(qb *sceneQueryBuilder, tags *models.MultiCriterio
return h.handler(tags)
}
func sceneTagCountCriterionHandler(qb *sceneQueryBuilder, tagCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: scenesTagsTable,
primaryFK: sceneIDColumn,
}
return h.handler(tagCount)
}
func scenePerformersCriterionHandler(qb *sceneQueryBuilder, performers *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) {
qb.performersRepository().join(f, "performers_join", "scenes.id")
@@ -540,6 +554,16 @@ func scenePerformersCriterionHandler(qb *sceneQueryBuilder, performers *models.M
return h.handler(performers)
}
func scenePerformerCountCriterionHandler(qb *sceneQueryBuilder, performerCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: performersScenesTable,
primaryFK: sceneIDColumn,
}
return h.handler(performerCount)
}
func sceneStudioCriterionHandler(qb *sceneQueryBuilder, studios *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) {
f.addJoin("studios", "studio", "studio.id = scenes.studio_id")
@@ -621,13 +645,25 @@ func handleScenePerformerTagsCriterion(query *queryBuilder, performerTagsFilter
}
}
func (qb *sceneQueryBuilder) getSceneSort(findFilter *models.FindFilterType) string {
if findFilter == nil {
func (qb *sceneQueryBuilder) getDefaultSceneSort() string {
return " ORDER BY scenes.path, scenes.date ASC "
}
func (qb *sceneQueryBuilder) setSceneSort(query *queryBuilder, findFilter *models.FindFilterType) {
if findFilter == nil {
query.sortAndPagination += qb.getDefaultSceneSort()
return
}
sort := findFilter.GetSort("title")
direction := findFilter.GetDirection()
return getSort(sort, direction, "scenes")
switch sort {
case "tag_count":
query.sortAndPagination += getCountSort(sceneTable, scenesTagsTable, sceneIDColumn, direction)
case "performer_count":
query.sortAndPagination += getCountSort(sceneTable, performersScenesTable, sceneIDColumn, direction)
default:
query.sortAndPagination += getSort(sort, direction, "scenes")
}
}
func (qb *sceneQueryBuilder) queryScene(query string, args []interface{}) (*models.Scene, error) {

View File

@@ -1214,6 +1214,88 @@ func TestSceneQueryPagination(t *testing.T) {
})
}
func TestSceneQueryTagCount(t *testing.T) {
const tagCount = 1
tagCountCriterion := models.IntCriterionInput{
Value: tagCount,
Modifier: models.CriterionModifierEquals,
}
verifyScenesTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyScenesTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyScenesTagCount(t, tagCountCriterion)
tagCountCriterion.Modifier = models.CriterionModifierLessThan
verifyScenesTagCount(t, tagCountCriterion)
}
func verifyScenesTagCount(t *testing.T, tagCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Scene()
sceneFilter := models.SceneFilterType{
TagCount: &tagCountCriterion,
}
scenes := queryScene(t, sqb, &sceneFilter, nil)
assert.Greater(t, len(scenes), 0)
for _, scene := range scenes {
ids, err := sqb.GetTagIDs(scene.ID)
if err != nil {
return err
}
verifyInt(t, len(ids), tagCountCriterion)
}
return nil
})
}
func TestSceneQueryPerformerCount(t *testing.T) {
const performerCount = 1
performerCountCriterion := models.IntCriterionInput{
Value: performerCount,
Modifier: models.CriterionModifierEquals,
}
verifyScenesPerformerCount(t, performerCountCriterion)
performerCountCriterion.Modifier = models.CriterionModifierNotEquals
verifyScenesPerformerCount(t, performerCountCriterion)
performerCountCriterion.Modifier = models.CriterionModifierGreaterThan
verifyScenesPerformerCount(t, performerCountCriterion)
performerCountCriterion.Modifier = models.CriterionModifierLessThan
verifyScenesPerformerCount(t, performerCountCriterion)
}
func verifyScenesPerformerCount(t *testing.T, performerCountCriterion models.IntCriterionInput) {
withTxn(func(r models.Repository) error {
sqb := r.Scene()
sceneFilter := models.SceneFilterType{
PerformerCount: &performerCountCriterion,
}
scenes := queryScene(t, sqb, &sceneFilter, nil)
assert.Greater(t, len(scenes), 0)
for _, scene := range scenes {
ids, err := sqb.GetPerformerIDs(scene.ID)
if err != nil {
return err
}
verifyInt(t, len(ids), performerCountCriterion)
}
return nil
})
}
func TestSceneCountByTagID(t *testing.T) {
withTxn(func(r models.Repository) error {
sqb := r.Scene()

View File

@@ -24,6 +24,8 @@ const (
sceneIdxWithMovie = iota
sceneIdxWithGallery
sceneIdxWithPerformer
sceneIdx1WithPerformer
sceneIdx2WithPerformer
sceneIdxWithTwoPerformers
sceneIdxWithTag
sceneIdxWithTwoTags
@@ -40,6 +42,8 @@ const (
const (
imageIdxWithGallery = iota
imageIdxWithPerformer
imageIdx1WithPerformer
imageIdx2WithPerformer
imageIdxWithTwoPerformers
imageIdxWithTag
imageIdxWithTwoTags
@@ -55,12 +59,15 @@ const (
performerIdxWithScene = iota
performerIdx1WithScene
performerIdx2WithScene
performerIdxWithTwoScenes
performerIdxWithImage
performerIdxWithTwoImages
performerIdx1WithImage
performerIdx2WithImage
performerIdxWithTag
performerIdxWithTwoTags
performerIdxWithGallery
performerIdxWithTwoGalleries
performerIdx1WithGallery
performerIdx2WithGallery
// new indexes above
@@ -87,6 +94,8 @@ const (
galleryIdxWithScene = iota
galleryIdxWithImage
galleryIdxWithPerformer
galleryIdx1WithPerformer
galleryIdx2WithPerformer
galleryIdxWithTwoPerformers
galleryIdxWithTag
galleryIdxWithTwoTags
@@ -185,6 +194,8 @@ var (
{sceneIdxWithTwoPerformers, performerIdx2WithScene},
{sceneIdxWithPerformerTag, performerIdxWithTag},
{sceneIdxWithPerformerTwoTags, performerIdxWithTwoTags},
{sceneIdx1WithPerformer, performerIdxWithTwoScenes},
{sceneIdx2WithPerformer, performerIdxWithTwoScenes},
}
sceneGalleryLinks = [][2]int{
@@ -218,6 +229,8 @@ var (
{imageIdxWithTwoPerformers, performerIdx2WithImage},
{imageIdxWithPerformerTag, performerIdxWithTag},
{imageIdxWithPerformerTwoTags, performerIdxWithTwoTags},
{imageIdx1WithPerformer, performerIdxWithTwoImages},
{imageIdx2WithPerformer, performerIdxWithTwoImages},
}
)
@@ -228,6 +241,8 @@ var (
{galleryIdxWithTwoPerformers, performerIdx2WithGallery},
{galleryIdxWithPerformerTag, performerIdxWithTag},
{galleryIdxWithPerformerTwoTags, performerIdxWithTwoTags},
{galleryIdx1WithPerformer, performerIdxWithTwoGalleries},
{galleryIdx2WithPerformer, performerIdxWithTwoGalleries},
}
galleryTagLinks = [][2]int{

View File

@@ -2,6 +2,7 @@ package sqlite
import (
"database/sql"
"fmt"
"math/rand"
"strconv"
"strings"
@@ -44,10 +45,15 @@ func getPaginationSQL(page int, perPage int) string {
return " LIMIT " + strconv.Itoa(perPage) + " OFFSET " + strconv.Itoa(page) + " "
}
func getSort(sort string, direction string, tableName string) string {
func getSortDirection(direction string) string {
if direction != "ASC" && direction != "DESC" {
direction = "ASC"
return "ASC"
} else {
return direction
}
}
func getSort(sort string, direction string, tableName string) string {
direction = getSortDirection(direction)
const randomSeedPrefix = "random_"
@@ -96,6 +102,10 @@ func getRandomSort(tableName string, direction string, seed float64) string {
return " ORDER BY " + "(substr(" + colName + " * " + randomSortString + ", length(" + colName + ") + 2))" + " " + direction
}
func getCountSort(primaryTable, joinTable, primaryFK, direction string) string {
return fmt.Sprintf(" ORDER BY (SELECT COUNT(*) FROM %s WHERE %s = %s.id) %s", joinTable, primaryFK, primaryTable, getSortDirection(direction))
}
func getSearchBinding(columns []string, q string, not bool) (string, []interface{}) {
var likeClauses []string
var args []interface{}
@@ -213,6 +223,11 @@ func getMultiCriterionClause(primaryTable, foreignTable, joinTable, primaryFK, f
return whereClause, havingClause
}
func getCountCriterionClause(primaryTable, joinTable, primaryFK string, criterion models.IntCriterionInput) (string, int) {
lhs := fmt.Sprintf("(SELECT COUNT(*) FROM %s s WHERE s.%s = %s.id)", joinTable, primaryFK, primaryTable)
return getIntCriterionWhereClause(lhs, criterion)
}
func ensureTx(tx *sqlx.Tx) {
if tx == nil {
panic("must use a transaction")

View File

@@ -3,6 +3,7 @@
* Added scene queue.
### 🎨 Improvements
* Add various `count` filter criteria and sort options.
* Scroll to top when changing page number.
* Add URL filter criteria for scenes, galleries, movies, performers and studios.
* Add HTTP endpoint for health checking at `/healthz`.

View File

@@ -25,6 +25,7 @@ export type CriterionType =
| "tags"
| "sceneTags"
| "performerTags"
| "tag_count"
| "performers"
| "studios"
| "movies"
@@ -90,6 +91,8 @@ export abstract class Criterion {
return "Scene Tags";
case "performerTags":
return "Performer Tags";
case "tag_count":
return "Tag Count";
case "performers":
return "Performers";
case "studios":
@@ -358,6 +361,15 @@ export class NumberCriterion extends Criterion {
}
}
export class MandatoryNumberCriterion extends NumberCriterion {
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Equals),
Criterion.getModifierOption(CriterionModifier.NotEquals),
Criterion.getModifierOption(CriterionModifier.GreaterThan),
Criterion.getModifierOption(CriterionModifier.LessThan),
];
}
export class DurationCriterion extends Criterion {
public type: CriterionType;
public parameterName: string;

View File

@@ -1,12 +1,11 @@
/* eslint-disable consistent-return, default-case */
import { CriterionModifier } from "src/core/generated-graphql";
import {
Criterion,
CriterionType,
StringCriterion,
NumberCriterion,
DurationCriterion,
MandatoryStringCriterion,
MandatoryNumberCriterion,
} from "./criterion";
import { OrganizedCriterion } from "./organized";
import { FavoriteCriterion } from "./favorite";
@@ -46,7 +45,8 @@ export function makeCriteria(type: CriterionType = "none") {
case "image_count":
case "gallery_count":
case "performer_count":
return new NumberCriterion(type, type);
case "tag_count":
return new MandatoryNumberCriterion(type, type);
case "resolution":
return new ResolutionCriterion();
case "average_resolution":
@@ -89,17 +89,8 @@ export function makeCriteria(type: CriterionType = "none") {
return new GalleriesCriterion();
case "birth_year":
return new NumberCriterion(type, type);
case "age": {
const ret = new NumberCriterion(type, type);
// null/not null doesn't make sense for these criteria
ret.modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Equals),
Criterion.getModifierOption(CriterionModifier.NotEquals),
Criterion.getModifierOption(CriterionModifier.GreaterThan),
Criterion.getModifierOption(CriterionModifier.LessThan),
];
return ret;
}
case "age":
return new MandatoryNumberCriterion(type, type);
case "gender":
return new GenderCriterion();
case "ethnicity":

View File

@@ -128,6 +128,8 @@ export class ListFilterModel {
"duration",
"framerate",
"bitrate",
"tag_count",
"performer_count",
"random",
];
this.displayModeOptions = [
@@ -147,8 +149,10 @@ export class ListFilterModel {
new HasMarkersCriterionOption(),
new SceneIsMissingCriterionOption(),
new TagsCriterionOption(),
ListFilterModel.createCriterionOption("tag_count"),
new PerformerTagsCriterionOption(),
new PerformersCriterionOption(),
ListFilterModel.createCriterionOption("performer_count"),
new StudiosCriterionOption(),
new MoviesCriterionOption(),
ListFilterModel.createCriterionOption("url"),
@@ -163,6 +167,8 @@ export class ListFilterModel {
"o_counter",
"filesize",
"file_mod_time",
"tag_count",
"performer_count",
"random",
];
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall];
@@ -175,8 +181,10 @@ export class ListFilterModel {
new ResolutionCriterionOption(),
new ImageIsMissingCriterionOption(),
new TagsCriterionOption(),
ListFilterModel.createCriterionOption("tag_count"),
new PerformerTagsCriterionOption(),
new PerformersCriterionOption(),
ListFilterModel.createCriterionOption("performer_count"),
new StudiosCriterionOption(),
];
break;
@@ -187,6 +195,7 @@ export class ListFilterModel {
"height",
"birthdate",
"scenes_count",
"tag_count",
"random",
];
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
@@ -212,6 +221,10 @@ export class ListFilterModel {
new PerformerIsMissingCriterionOption(),
new TagsCriterionOption(),
ListFilterModel.createCriterionOption("url"),
ListFilterModel.createCriterionOption("tag_count"),
ListFilterModel.createCriterionOption("scene_count"),
ListFilterModel.createCriterionOption("image_count"),
ListFilterModel.createCriterionOption("gallery_count"),
...numberCriteria
.concat(stringCriteria)
.map((c) => ListFilterModel.createCriterionOption(c)),
@@ -247,6 +260,8 @@ export class ListFilterModel {
"path",
"file_mod_time",
"images_count",
"tag_count",
"performer_count",
"random",
];
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
@@ -258,8 +273,10 @@ export class ListFilterModel {
new AverageResolutionCriterionOption(),
new GalleryIsMissingCriterionOption(),
new TagsCriterionOption(),
ListFilterModel.createCriterionOption("tag_count"),
new PerformerTagsCriterionOption(),
new PerformersCriterionOption(),
ListFilterModel.createCriterionOption("performer_count"),
new StudiosCriterionOption(),
ListFilterModel.createCriterionOption("url"),
];
@@ -563,6 +580,14 @@ export class ListFilterModel {
};
break;
}
case "tag_count": {
const tagCountCrit = criterion as NumberCriterion;
result.tag_count = {
value: tagCountCrit.value,
modifier: tagCountCrit.modifier,
};
break;
}
case "performers": {
const perfCrit = criterion as PerformersCriterion;
result.performers = {
@@ -571,6 +596,14 @@ export class ListFilterModel {
};
break;
}
case "performer_count": {
const performerCountCrit = criterion as NumberCriterion;
result.performer_count = {
value: performerCountCrit.value,
modifier: performerCountCrit.modifier,
};
break;
}
case "studios": {
const studCrit = criterion as StudiosCriterion;
result.studios = {
@@ -711,9 +744,42 @@ export class ListFilterModel {
};
break;
}
case "tag_count": {
const tagCountCrit = criterion as NumberCriterion;
result.tag_count = {
value: tagCountCrit.value,
modifier: tagCountCrit.modifier,
};
break;
}
case "scene_count": {
const countCrit = criterion as NumberCriterion;
result.scene_count = {
value: countCrit.value,
modifier: countCrit.modifier,
};
break;
}
case "image_count": {
const countCrit = criterion as NumberCriterion;
result.image_count = {
value: countCrit.value,
modifier: countCrit.modifier,
};
break;
}
case "gallery_count": {
const countCrit = criterion as NumberCriterion;
result.gallery_count = {
value: countCrit.value,
modifier: countCrit.modifier,
};
break;
}
// no default
}
});
return result;
}
@@ -839,6 +905,14 @@ export class ListFilterModel {
};
break;
}
case "tag_count": {
const tagCountCrit = criterion as NumberCriterion;
result.tag_count = {
value: tagCountCrit.value,
modifier: tagCountCrit.modifier,
};
break;
}
case "performerTags": {
const performerTagsCrit = criterion as TagsCriterion;
result.performer_tags = {
@@ -855,6 +929,14 @@ export class ListFilterModel {
};
break;
}
case "performer_count": {
const countCrit = criterion as NumberCriterion;
result.performer_count = {
value: countCrit.value,
modifier: countCrit.modifier,
};
break;
}
case "studios": {
const studCrit = criterion as StudiosCriterion;
result.studios = {
@@ -1014,6 +1096,14 @@ export class ListFilterModel {
};
break;
}
case "tag_count": {
const tagCountCrit = criterion as NumberCriterion;
result.tag_count = {
value: tagCountCrit.value,
modifier: tagCountCrit.modifier,
};
break;
}
case "performerTags": {
const performerTagsCrit = criterion as TagsCriterion;
result.performer_tags = {
@@ -1030,6 +1120,14 @@ export class ListFilterModel {
};
break;
}
case "performer_count": {
const countCrit = criterion as NumberCriterion;
result.performer_count = {
value: countCrit.value,
modifier: countCrit.modifier,
};
break;
}
case "studios": {
const studCrit = criterion as StudiosCriterion;
result.studios = {