Add related object filter criteria to various filter types in graphql schema (#4861)

* Move filter criterion handlers into separate file
* Add related filters for image filter
* Add related filters for scene filter
* Add related filters to gallery filter
* Add related filters to movie filter
* Add related filters to performer filter
* Add related filters to studio filter
* Add related filters to tag filter
* Add scene filter to scene marker filter
This commit is contained in:
WithoutPants
2024-06-11 11:34:38 +10:00
committed by GitHub
parent ff23d4e20b
commit e843c890fb
41 changed files with 4562 additions and 3805 deletions

View File

@@ -170,6 +170,14 @@ input PerformerFilterType {
birthdate: DateCriterionInput
"Filter by death date"
death_date: DateCriterionInput
"Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType
"Filter by related images that meet this criteria"
images_filter: ImageFilterType
"Filter by related galleries that meet this criteria"
galleries_filter: GalleryFilterType
"Filter by related tags that meet this criteria"
tags_filter: TagFilterType
"Filter by creation time"
created_at: TimestampCriterionInput
"Filter by last update time"
@@ -193,6 +201,8 @@ input SceneMarkerFilterType {
scene_created_at: TimestampCriterionInput
"Filter by lscene ast update time"
scene_updated_at: TimestampCriterionInput
"Filter by related scenes that meet this criteria"
scene_filter: SceneFilterType
}
input SceneFilterType {
@@ -288,9 +298,26 @@ input SceneFilterType {
created_at: TimestampCriterionInput
"Filter by last update time"
updated_at: TimestampCriterionInput
"Filter by related galleries that meet this criteria"
galleries_filter: GalleryFilterType
"Filter by related performers that meet this criteria"
performers_filter: PerformerFilterType
"Filter by related studios that meet this criteria"
studios_filter: StudioFilterType
"Filter by related tags that meet this criteria"
tags_filter: TagFilterType
"Filter by related movies that meet this criteria"
movies_filter: MovieFilterType
"Filter by related markers that meet this criteria"
markers_filter: SceneMarkerFilterType
}
input MovieFilterType {
AND: MovieFilterType
OR: MovieFilterType
NOT: MovieFilterType
name: StringCriterionInput
director: StringCriterionInput
synopsis: StringCriterionInput
@@ -313,6 +340,11 @@ input MovieFilterType {
created_at: TimestampCriterionInput
"Filter by last update time"
updated_at: TimestampCriterionInput
"Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType
"Filter by related studios that meet this criteria"
studios_filter: StudioFilterType
}
input StudioFilterType {
@@ -346,6 +378,12 @@ input StudioFilterType {
child_count: IntCriterionInput
"Filter by autotag ignore value"
ignore_auto_tag: Boolean
"Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType
"Filter by related images that meet this criteria"
images_filter: ImageFilterType
"Filter by related galleries that meet this criteria"
galleries_filter: GalleryFilterType
"Filter by creation time"
created_at: TimestampCriterionInput
"Filter by last update time"
@@ -411,6 +449,17 @@ input GalleryFilterType {
code: StringCriterionInput
"Filter by photographer"
photographer: StringCriterionInput
"Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType
"Filter by related images that meet this criteria"
images_filter: ImageFilterType
"Filter by related performers that meet this criteria"
performers_filter: PerformerFilterType
"Filter by related studios that meet this criteria"
studios_filter: StudioFilterType
"Filter by related tags that meet this criteria"
tags_filter: TagFilterType
}
input TagFilterType {
@@ -463,6 +512,13 @@ input TagFilterType {
"Filter by autotag ignore value"
ignore_auto_tag: Boolean
"Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType
"Filter by related images that meet this criteria"
images_filter: ImageFilterType
"Filter by related galleries that meet this criteria"
galleries_filter: GalleryFilterType
"Filter by creation time"
created_at: TimestampCriterionInput
@@ -528,6 +584,15 @@ input ImageFilterType {
code: StringCriterionInput
"Filter by photographer"
photographer: StringCriterionInput
"Filter by related galleries that meet this criteria"
galleries_filter: GalleryFilterType
"Filter by related performers that meet this criteria"
performers_filter: PerformerFilterType
"Filter by related studios that meet this criteria"
studios_filter: StudioFilterType
"Filter by related tags that meet this criteria"
tags_filter: TagFilterType
}
enum CriterionModifier {

View File

@@ -98,10 +98,12 @@ func getSingleLetterTags(ctx context.Context, c *Cache, reader models.TagAutoTag
Value: singleFirstCharacterRegex,
Modifier: models.CriterionModifierMatchesRegex,
},
Or: &models.TagFilterType{
Aliases: &models.StringCriterionInput{
Value: singleFirstCharacterRegex,
Modifier: models.CriterionModifierMatchesRegex,
OperatorFilter: models.OperatorFilter[models.TagFilterType]{
Or: &models.TagFilterType{
Aliases: &models.StringCriterionInput{
Value: singleFirstCharacterRegex,
Modifier: models.CriterionModifierMatchesRegex,
},
},
},
}, &models.FindFilterType{

View File

@@ -6,6 +6,27 @@ import (
"strconv"
)
type OperatorFilter[T any] struct {
And *T `json:"AND"`
Or *T `json:"OR"`
Not *T `json:"NOT"`
}
// SubFilter returns the subfilter of the operator filter.
// Only one of And, Or, or Not should be set, so it returns the first of these that are not nil.
func (f *OperatorFilter[T]) SubFilter() *T {
if f.And != nil {
return f.And
}
if f.Or != nil {
return f.Or
}
if f.Not != nil {
return f.Not
}
return nil
}
type CriterionModifier string
const (

View File

@@ -1,9 +1,7 @@
package models
type GalleryFilterType struct {
And *GalleryFilterType `json:"AND"`
Or *GalleryFilterType `json:"OR"`
Not *GalleryFilterType `json:"NOT"`
OperatorFilter[GalleryFilterType]
ID *IntCriterionInput `json:"id"`
Title *StringCriterionInput `json:"title"`
Code *StringCriterionInput `json:"code"`
@@ -51,6 +49,16 @@ type GalleryFilterType struct {
URL *StringCriterionInput `json:"url"`
// Filter by date
Date *DateCriterionInput `json:"date"`
// Filter by related scenes that meet this criteria
ScenesFilter *SceneFilterType `json:"scenes_filter"`
// Filter by related images that meet this criteria
ImagesFilter *ImageFilterType `json:"images_filter"`
// Filter by related performers that meet this criteria
PerformersFilter *PerformerFilterType `json:"performers_filter"`
// Filter by related studios that meet this criteria
StudiosFilter *StudioFilterType `json:"studios_filter"`
// Filter by related tags that meet this criteria
TagsFilter *TagFilterType `json:"tags_filter"`
// Filter by created at
CreatedAt *TimestampCriterionInput `json:"created_at"`
// Filter by updated at

View File

@@ -3,9 +3,7 @@ package models
import "context"
type ImageFilterType struct {
And *ImageFilterType `json:"AND"`
Or *ImageFilterType `json:"OR"`
Not *ImageFilterType `json:"NOT"`
OperatorFilter[ImageFilterType]
ID *IntCriterionInput `json:"id"`
Title *StringCriterionInput `json:"title"`
Code *StringCriterionInput `json:"code"`
@@ -51,6 +49,14 @@ type ImageFilterType struct {
PerformerAge *IntCriterionInput `json:"performer_age"`
// Filter to only include images with these galleries
Galleries *MultiCriterionInput `json:"galleries"`
// Filter by related galleries that meet this criteria
GalleriesFilter *GalleryFilterType `json:"galleries_filter"`
// Filter by related performers that meet this criteria
PerformersFilter *PerformerFilterType `json:"performers_filter"`
// Filter by related studios that meet this criteria
StudiosFilter *StudioFilterType `json:"studios_filter"`
// Filter by related tags that meet this criteria
TagsFilter *TagFilterType `json:"tags_filter"`
// Filter by created at
CreatedAt *TimestampCriterionInput `json:"created_at"`
// Filter by updated at

View File

@@ -1,6 +1,7 @@
package models
type MovieFilterType struct {
OperatorFilter[MovieFilterType]
Name *StringCriterionInput `json:"name"`
Director *StringCriterionInput `json:"director"`
Synopsis *StringCriterionInput `json:"synopsis"`
@@ -18,6 +19,10 @@ type MovieFilterType struct {
Performers *MultiCriterionInput `json:"performers"`
// Filter by date
Date *DateCriterionInput `json:"date"`
// Filter by related scenes that meet this criteria
ScenesFilter *SceneFilterType `json:"scenes_filter"`
// Filter by related studios that meet this criteria
StudiosFilter *StudioFilterType `json:"studios_filter"`
// Filter by created at
CreatedAt *TimestampCriterionInput `json:"created_at"`
// Filter by updated at

View File

@@ -108,9 +108,7 @@ type CircumcisionCriterionInput struct {
}
type PerformerFilterType struct {
And *PerformerFilterType `json:"AND"`
Or *PerformerFilterType `json:"OR"`
Not *PerformerFilterType `json:"NOT"`
OperatorFilter[PerformerFilterType]
Name *StringCriterionInput `json:"name"`
Disambiguation *StringCriterionInput `json:"disambiguation"`
Details *StringCriterionInput `json:"details"`
@@ -188,6 +186,14 @@ type PerformerFilterType struct {
Birthdate *DateCriterionInput `json:"birth_date"`
// Filter by death date
DeathDate *DateCriterionInput `json:"death_date"`
// Filter by related scenes that meet this criteria
ScenesFilter *SceneFilterType `json:"scenes_filter"`
// Filter by related images that meet this criteria
ImagesFilter *ImageFilterType `json:"images_filter"`
// Filter by related galleries that meet this criteria
GalleriesFilter *GalleryFilterType `json:"galleries_filter"`
// Filter by related tags that meet this criteria
TagsFilter *TagFilterType `json:"tags_filter"`
// Filter by created at
CreatedAt *TimestampCriterionInput `json:"created_at"`
// Filter by updated at

View File

@@ -9,9 +9,7 @@ type PHashDuplicationCriterionInput struct {
}
type SceneFilterType struct {
And *SceneFilterType `json:"AND"`
Or *SceneFilterType `json:"OR"`
Not *SceneFilterType `json:"NOT"`
OperatorFilter[SceneFilterType]
ID *IntCriterionInput `json:"id"`
Title *StringCriterionInput `json:"title"`
Code *StringCriterionInput `json:"code"`
@@ -97,6 +95,18 @@ type SceneFilterType struct {
LastPlayedAt *TimestampCriterionInput `json:"last_played_at"`
// Filter by date
Date *DateCriterionInput `json:"date"`
// Filter by related galleries that meet this criteria
GalleriesFilter *GalleryFilterType `json:"galleries_filter"`
// Filter by related performers that meet this criteria
PerformersFilter *PerformerFilterType `json:"performers_filter"`
// Filter by related studios that meet this criteria
StudiosFilter *StudioFilterType `json:"studios_filter"`
// Filter by related tags that meet this criteria
TagsFilter *TagFilterType `json:"tags_filter"`
// Filter by related movies that meet this criteria
MoviesFilter *MovieFilterType `json:"movies_filter"`
// Filter by related markers that meet this criteria
MarkersFilter *SceneMarkerFilterType `json:"markers_filter"`
// Filter by created at
CreatedAt *TimestampCriterionInput `json:"created_at"`
// Filter by updated at

View File

@@ -19,6 +19,8 @@ type SceneMarkerFilterType struct {
SceneCreatedAt *TimestampCriterionInput `json:"scene_created_at"`
// Filter by scenes updated at
SceneUpdatedAt *TimestampCriterionInput `json:"scene_updated_at"`
// Filter by related scenes that meet this criteria
SceneFilter *SceneFilterType `json:"scene_filter"`
}
type MarkerStringsResultType struct {

View File

@@ -1,9 +1,7 @@
package models
type StudioFilterType struct {
And *StudioFilterType `json:"AND"`
Or *StudioFilterType `json:"OR"`
Not *StudioFilterType `json:"NOT"`
OperatorFilter[StudioFilterType]
Name *StringCriterionInput `json:"name"`
Details *StringCriterionInput `json:"details"`
// Filter to only include studios with this parent studio
@@ -32,6 +30,12 @@ type StudioFilterType struct {
ChildCount *IntCriterionInput `json:"child_count"`
// Filter by autotag ignore value
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
// Filter by related scenes that meet this criteria
ScenesFilter *SceneFilterType `json:"scenes_filter"`
// Filter by related images that meet this criteria
ImagesFilter *ImageFilterType `json:"images_filter"`
// Filter by related galleries that meet this criteria
GalleriesFilter *GalleryFilterType `json:"galleries_filter"`
// Filter by created at
CreatedAt *TimestampCriterionInput `json:"created_at"`
// Filter by updated at

View File

@@ -1,9 +1,7 @@
package models
type TagFilterType struct {
And *TagFilterType `json:"AND"`
Or *TagFilterType `json:"OR"`
Not *TagFilterType `json:"NOT"`
OperatorFilter[TagFilterType]
// Filter by tag name
Name *StringCriterionInput `json:"name"`
// Filter by tag aliases
@@ -34,6 +32,12 @@ type TagFilterType struct {
ChildCount *IntCriterionInput `json:"child_count"`
// Filter by autotag ignore value
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
// Filter by related scenes that meet this criteria
ScenesFilter *SceneFilterType `json:"scenes_filter"`
// Filter by related images that meet this criteria
ImagesFilter *ImageFilterType `json:"images_filter"`
// Filter by related galleries that meet this criteria
GalleriesFilter *GalleryFilterType `json:"galleries_filter"`
// Filter by created at
CreatedAt *TimestampCriterionInput `json:"created_at"`
// Filter by updated at

View File

@@ -346,8 +346,8 @@ func (qb *BlobStore) delete(ctx context.Context, checksum string) error {
}
type blobJoinQueryBuilder struct {
repository
blobStore *BlobStore
repository repository
blobStore *BlobStore
joinTable string
}
@@ -381,7 +381,7 @@ func (qb *blobJoinQueryBuilder) UpdateImage(ctx context.Context, id int, blobCol
}
sqlQuery := fmt.Sprintf("UPDATE %s SET %s = ? WHERE id = ?", qb.joinTable, blobCol)
if _, err := qb.tx.Exec(ctx, sqlQuery, checksum, id); err != nil {
if _, err := dbWrapper.Exec(ctx, sqlQuery, checksum, id); err != nil {
return err
}
@@ -428,7 +428,7 @@ func (qb *blobJoinQueryBuilder) DestroyImage(ctx context.Context, id int, blobCo
}
updateQuery := fmt.Sprintf("UPDATE %s SET %s = NULL WHERE id = ?", qb.joinTable, blobCol)
if _, err = qb.tx.Exec(ctx, updateQuery, id); err != nil {
if _, err = dbWrapper.Exec(ctx, updateQuery, id); err != nil {
return err
}
@@ -441,7 +441,7 @@ func (qb *blobJoinQueryBuilder) HasImage(ctx context.Context, id int, blobCol st
"joinCol": blobCol,
})
c, err := qb.runCountQuery(ctx, stmt, []interface{}{id})
c, err := qb.repository.runCountQuery(ctx, stmt, []interface{}{id})
if err != nil {
return false, err
}

File diff suppressed because it is too large Load Diff

View File

@@ -61,7 +61,7 @@ func (e *MismatchedSchemaVersionError) Error() string {
return fmt.Sprintf("schema version %d is incompatible with required schema version %d", e.CurrentSchemaVersion, e.RequiredSchemaVersion)
}
type Database struct {
type storeRepository struct {
Blobs *BlobStore
File *FileStore
Folder *FolderStore
@@ -75,6 +75,10 @@ type Database struct {
Studio *StudioStore
Tag *TagStore
Movie *MovieStore
}
type Database struct {
*storeRepository
db *sqlx.DB
dbPath string
@@ -87,23 +91,32 @@ type Database struct {
func NewDatabase() *Database {
fileStore := NewFileStore()
folderStore := NewFolderStore()
galleryStore := NewGalleryStore(fileStore, folderStore)
blobStore := NewBlobStore(BlobStoreOptions{})
performerStore := NewPerformerStore(blobStore)
studioStore := NewStudioStore(blobStore)
tagStore := NewTagStore(blobStore)
ret := &Database{
r := &storeRepository{}
*r = storeRepository{
Blobs: blobStore,
File: fileStore,
Folder: folderStore,
Scene: NewSceneStore(fileStore, blobStore),
Scene: NewSceneStore(r, blobStore),
SceneMarker: NewSceneMarkerStore(),
Image: NewImageStore(fileStore),
Gallery: NewGalleryStore(fileStore, folderStore),
Image: NewImageStore(r),
Gallery: galleryStore,
GalleryChapter: NewGalleryChapterStore(),
Performer: NewPerformerStore(blobStore),
Studio: NewStudioStore(blobStore),
Tag: NewTagStore(blobStore),
Performer: performerStore,
Studio: studioStore,
Tag: tagStore,
Movie: NewMovieStore(blobStore),
SavedFilter: NewSavedFilterStore(),
lockChan: make(chan struct{}, 1),
}
ret := &Database{
storeRepository: r,
lockChan: make(chan struct{}, 1),
}
return ret
@@ -370,7 +383,7 @@ func (db *Database) Analyze(ctx context.Context) error {
}
func (db *Database) ExecSQL(ctx context.Context, query string, args []interface{}) (*int64, *int64, error) {
wrapper := dbWrapper{}
wrapper := dbWrapperType{}
result, err := wrapper.Exec(ctx, query, args...)
if err != nil {
@@ -393,7 +406,7 @@ func (db *Database) ExecSQL(ctx context.Context, query string, args []interface{
}
func (db *Database) QuerySQL(ctx context.Context, query string, args []interface{}) ([]string, [][]interface{}, error) {
wrapper := dbWrapper{}
wrapper := dbWrapperType{}
rows, err := wrapper.QueryxContext(ctx, query, args...)
if err != nil && !errors.Is(err, sql.ErrNoRows) {

View File

@@ -947,7 +947,6 @@ func (qb *FileStore) setQuerySort(query *queryBuilder, findFilter *models.FindFi
func (qb *FileStore) captionRepository() *captionRepository {
return &captionRepository{
repository: repository{
tx: qb.tx,
tableName: videoCaptionsTable,
idColumn: fileIDColumn,
},

View File

@@ -2,19 +2,55 @@ package sqlite
import (
"context"
"database/sql"
"errors"
"fmt"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/stashapp/stash/pkg/utils"
"github.com/stashapp/stash/pkg/models"
)
func illegalFilterCombination(type1, type2 string) error {
return fmt.Errorf("cannot have %s and %s in the same filter", type1, type2)
}
func validateFilterCombination[T any](sf models.OperatorFilter[T]) error {
const and = "AND"
const or = "OR"
const not = "NOT"
if sf.And != nil {
if sf.Or != nil {
return illegalFilterCombination(and, or)
}
if sf.Not != nil {
return illegalFilterCombination(and, not)
}
}
if sf.Or != nil {
if sf.Not != nil {
return illegalFilterCombination(or, not)
}
}
return nil
}
func handleSubFilter[T any](ctx context.Context, handler criterionHandler, f *filterBuilder, subFilter models.OperatorFilter[T]) {
subQuery := &filterBuilder{}
handler.handle(ctx, subQuery)
if subFilter.And != nil {
f.and(subQuery)
}
if subFilter.Or != nil {
f.or(subQuery)
}
if subFilter.Not != nil {
f.not(subQuery)
}
}
type sqlClause struct {
sql string
args []interface{}
@@ -54,16 +90,6 @@ func andClauses(clauses ...sqlClause) sqlClause {
return joinClauses("AND", clauses...)
}
type criterionHandler interface {
handle(ctx context.Context, f *filterBuilder)
}
type criterionHandlerFunc func(ctx context.Context, f *filterBuilder)
func (h criterionHandlerFunc) handle(ctx context.Context, f *filterBuilder) {
h(ctx, f)
}
type join struct {
table string
as string
@@ -143,6 +169,16 @@ type filterBuilder struct {
err error
}
func (f *filterBuilder) empty() bool {
return f == nil || (len(f.whereClauses) == 0 && len(f.joins) == 0 && len(f.havingClauses) == 0 && f.subFilter == nil)
}
func filterBuilderFromHandler(ctx context.Context, handler criterionHandler) *filterBuilder {
f := &filterBuilder{}
handler.handle(ctx, f)
return f
}
var errSubFilterAlreadySet = errors.New(`sub-filter already set`)
// sub-filter operator values
@@ -388,876 +424,3 @@ func (f *filterBuilder) andClauses(input []sqlClause) (string, []interface{}) {
return "", nil
}
func stringCriterionHandler(c *models.StringCriterionInput, column string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if c != nil {
if modifier := c.Modifier; c.Modifier.IsValid() {
switch modifier {
case models.CriterionModifierIncludes:
f.whereClauses = append(f.whereClauses, getStringSearchClause([]string{column}, c.Value, false))
case models.CriterionModifierExcludes:
f.whereClauses = append(f.whereClauses, getStringSearchClause([]string{column}, c.Value, true))
case models.CriterionModifierEquals:
f.addWhere(column+" LIKE ?", c.Value)
case models.CriterionModifierNotEquals:
f.addWhere(column+" NOT LIKE ?", c.Value)
case models.CriterionModifierMatchesRegex:
if _, err := regexp.Compile(c.Value); err != nil {
f.setError(err)
return
}
f.addWhere(fmt.Sprintf("(%s IS NOT NULL AND %[1]s regexp ?)", column), c.Value)
case models.CriterionModifierNotMatchesRegex:
if _, err := regexp.Compile(c.Value); err != nil {
f.setError(err)
return
}
f.addWhere(fmt.Sprintf("(%s IS NULL OR %[1]s NOT regexp ?)", column), c.Value)
case models.CriterionModifierIsNull:
f.addWhere("(" + column + " IS NULL OR TRIM(" + column + ") = '')")
case models.CriterionModifierNotNull:
f.addWhere("(" + column + " IS NOT NULL AND TRIM(" + column + ") != '')")
default:
panic("unsupported string filter modifier")
}
}
}
}
}
func enumCriterionHandler(modifier models.CriterionModifier, values []string, column string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if modifier.IsValid() {
switch modifier {
case models.CriterionModifierIncludes, models.CriterionModifierEquals:
if len(values) > 0 {
f.whereClauses = append(f.whereClauses, getEnumSearchClause(column, values, false))
}
case models.CriterionModifierExcludes, models.CriterionModifierNotEquals:
if len(values) > 0 {
f.whereClauses = append(f.whereClauses, getEnumSearchClause(column, values, true))
}
case models.CriterionModifierIsNull:
f.addWhere("(" + column + " IS NULL OR TRIM(" + column + ") = '')")
case models.CriterionModifierNotNull:
f.addWhere("(" + column + " IS NOT NULL AND TRIM(" + column + ") != '')")
default:
panic("unsupported string filter modifier")
}
}
}
}
func pathCriterionHandler(c *models.StringCriterionInput, pathColumn string, basenameColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if c != nil {
if addJoinFn != nil {
addJoinFn(f)
}
addWildcards := true
not := false
if modifier := c.Modifier; c.Modifier.IsValid() {
switch modifier {
case models.CriterionModifierIncludes:
f.whereClauses = append(f.whereClauses, getPathSearchClauseMany(pathColumn, basenameColumn, c.Value, addWildcards, not))
case models.CriterionModifierExcludes:
not = true
f.whereClauses = append(f.whereClauses, getPathSearchClauseMany(pathColumn, basenameColumn, c.Value, addWildcards, not))
case models.CriterionModifierEquals:
addWildcards = false
f.whereClauses = append(f.whereClauses, getPathSearchClause(pathColumn, basenameColumn, c.Value, addWildcards, not))
case models.CriterionModifierNotEquals:
addWildcards = false
not = true
f.whereClauses = append(f.whereClauses, getPathSearchClause(pathColumn, basenameColumn, c.Value, addWildcards, not))
case models.CriterionModifierMatchesRegex:
if _, err := regexp.Compile(c.Value); err != nil {
f.setError(err)
return
}
filepathColumn := fmt.Sprintf("%s || '%s' || %s", pathColumn, string(filepath.Separator), basenameColumn)
f.addWhere(fmt.Sprintf("%s IS NOT NULL AND %s IS NOT NULL AND %s regexp ?", pathColumn, basenameColumn, filepathColumn), c.Value)
case models.CriterionModifierNotMatchesRegex:
if _, err := regexp.Compile(c.Value); err != nil {
f.setError(err)
return
}
filepathColumn := fmt.Sprintf("%s || '%s' || %s", pathColumn, string(filepath.Separator), basenameColumn)
f.addWhere(fmt.Sprintf("%s IS NULL OR %s IS NULL OR %s NOT regexp ?", pathColumn, basenameColumn, filepathColumn), c.Value)
case models.CriterionModifierIsNull:
f.addWhere(fmt.Sprintf("%s IS NULL OR TRIM(%[1]s) = '' OR %s IS NULL OR TRIM(%[2]s) = ''", pathColumn, basenameColumn))
case models.CriterionModifierNotNull:
f.addWhere(fmt.Sprintf("%s IS NOT NULL AND TRIM(%[1]s) != '' AND %s IS NOT NULL AND TRIM(%[2]s) != ''", pathColumn, basenameColumn))
default:
panic("unsupported string filter modifier")
}
}
}
}
}
func getPathSearchClause(pathColumn, basenameColumn, p string, addWildcards, not bool) sqlClause {
if addWildcards {
p = "%" + p + "%"
}
filepathColumn := fmt.Sprintf("%s || '%s' || %s", pathColumn, string(filepath.Separator), basenameColumn)
ret := makeClause(fmt.Sprintf("%s LIKE ?", filepathColumn), p)
if not {
ret = ret.not()
}
return ret
}
// getPathSearchClauseMany splits the query string p on whitespace
// Used for backwards compatibility for the includes/excludes modifiers
func getPathSearchClauseMany(pathColumn, basenameColumn, p string, addWildcards, not bool) sqlClause {
q := strings.TrimSpace(p)
trimmedQuery := strings.Trim(q, "\"")
if trimmedQuery == q {
q = regexp.MustCompile(`\s+`).ReplaceAllString(q, " ")
queryWords := strings.Split(q, " ")
var ret []sqlClause
// Search for any word
for _, word := range queryWords {
ret = append(ret, getPathSearchClause(pathColumn, basenameColumn, word, addWildcards, not))
}
if !not {
return orClauses(ret...)
}
return andClauses(ret...)
}
return getPathSearchClause(pathColumn, basenameColumn, trimmedQuery, addWildcards, not)
}
func intCriterionHandler(c *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if c != nil {
if addJoinFn != nil {
addJoinFn(f)
}
clause, args := getIntCriterionWhereClause(column, *c)
f.addWhere(clause, args...)
}
}
}
func floatCriterionHandler(c *models.FloatCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if c != nil {
if addJoinFn != nil {
addJoinFn(f)
}
clause, args := getFloatCriterionWhereClause(column, *c)
f.addWhere(clause, args...)
}
}
}
func boolCriterionHandler(c *bool, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if c != nil {
if addJoinFn != nil {
addJoinFn(f)
}
var v string
if *c {
v = "1"
} else {
v = "0"
}
f.addWhere(column + " = " + v)
}
}
}
func dateCriterionHandler(c *models.DateCriterionInput, column string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if c != nil {
clause, args := getDateCriterionWhereClause(column, *c)
f.addWhere(clause, args...)
}
}
}
func timestampCriterionHandler(c *models.TimestampCriterionInput, column string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if c != nil {
clause, args := getTimestampCriterionWhereClause(column, *c)
f.addWhere(clause, args...)
}
}
}
// handle for MultiCriterion where there is a join table between the new
// objects
type joinedMultiCriterionHandlerBuilder struct {
// table containing the primary objects
primaryTable string
// table joining primary and foreign objects
joinTable string
// alias for join table, if required
joinAs string
// foreign key of the primary object on the join table
primaryFK string
// foreign key of the foreign object on the join table
foreignFK string
addJoinTable func(f *filterBuilder)
}
func (m *joinedMultiCriterionHandlerBuilder) handler(c *models.MultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if c != nil {
// make local copy so we can modify it
criterion := *c
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 && len(criterion.Excludes) == 0 {
return
}
// combine excludes if excludes modifier is selected
if criterion.Modifier == models.CriterionModifierExcludes {
criterion.Modifier = models.CriterionModifierIncludesAll
criterion.Excludes = append(criterion.Excludes, criterion.Value...)
criterion.Value = nil
}
if len(criterion.Value) > 0 {
whereClause := ""
havingClause := ""
var args []interface{}
for _, tagID := range criterion.Value {
args = append(args, tagID)
}
switch criterion.Modifier {
case models.CriterionModifierIncludes:
// includes any of the provided ids
m.addJoinTable(f)
whereClause = fmt.Sprintf("%s.%s IN %s", joinAlias, m.foreignFK, getInBinding(len(criterion.Value)))
case models.CriterionModifierEquals:
// includes only the provided ids
m.addJoinTable(f)
whereClause = utils.StrFormat("{joinAlias}.{foreignFK} IN {inBinding} AND (SELECT COUNT(*) FROM {joinTable} s WHERE s.{primaryFK} = {primaryTable}.id) = ?", utils.StrFormatMap{
"joinAlias": joinAlias,
"foreignFK": m.foreignFK,
"inBinding": getInBinding(len(criterion.Value)),
"joinTable": m.joinTable,
"primaryFK": m.primaryFK,
"primaryTable": m.primaryTable,
})
havingClause = fmt.Sprintf("count(distinct %s.%s) IS %d", joinAlias, m.foreignFK, len(criterion.Value))
args = append(args, len(criterion.Value))
case models.CriterionModifierNotEquals:
f.setError(fmt.Errorf("not equals modifier is not supported for multi criterion input"))
case models.CriterionModifierIncludesAll:
// includes all of the provided ids
m.addJoinTable(f)
whereClause = fmt.Sprintf("%s.%s IN %s", joinAlias, m.foreignFK, getInBinding(len(criterion.Value)))
havingClause = fmt.Sprintf("count(distinct %s.%s) IS %d", joinAlias, m.foreignFK, len(criterion.Value))
}
f.addWhere(whereClause, args...)
f.addHaving(havingClause)
}
if len(criterion.Excludes) > 0 {
var args []interface{}
for _, tagID := range criterion.Excludes {
args = append(args, tagID)
}
// excludes all of the provided ids
// need to use actual join table name for this
// <primaryTable>.id NOT IN (select <joinTable>.<primaryFK> from <joinTable> where <joinTable>.<foreignFK> in <values>)
whereClause := fmt.Sprintf("%[1]s.id NOT IN (SELECT %[3]s.%[2]s from %[3]s where %[3]s.%[4]s in %[5]s)", m.primaryTable, m.primaryFK, m.joinTable, m.foreignFK, getInBinding(len(criterion.Excludes)))
f.addWhere(whereClause, args...)
}
}
}
}
type multiCriterionHandlerBuilder struct {
primaryTable string
foreignTable string
joinTable string
primaryFK string
foreignFK string
// function that will be called to perform any necessary joins
addJoinsFunc func(f *filterBuilder)
}
func (m *multiCriterionHandlerBuilder) handler(criterion *models.MultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
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.addLeftJoin(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)
}
if m.addJoinsFunc != nil {
m.addJoinsFunc(f)
}
whereClause, havingClause := getMultiCriterionClause(m.primaryTable, m.foreignTable, m.joinTable, m.primaryFK, m.foreignFK, criterion)
f.addWhere(whereClause, args...)
f.addHaving(havingClause)
}
}
}
type countCriterionHandlerBuilder struct {
primaryTable string
joinTable string
primaryFK string
}
func (m *countCriterionHandlerBuilder) handler(criterion *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if criterion != nil {
clause, args := getCountCriterionClause(m.primaryTable, m.joinTable, m.primaryFK, *criterion)
f.addWhere(clause, args...)
}
}
}
// handler for StringCriterion for string list fields
type stringListCriterionHandlerBuilder struct {
// table joining primary and foreign objects
joinTable string
// string field on the join table
stringColumn string
addJoinTable func(f *filterBuilder)
}
func (m *stringListCriterionHandlerBuilder) handler(criterion *models.StringCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if criterion != nil {
m.addJoinTable(f)
stringCriterionHandler(criterion, m.joinTable+"."+m.stringColumn)(ctx, f)
}
}
}
func studioCriterionHandler(primaryTable string, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if studios == nil {
return
}
studiosCopy := *studios
switch studiosCopy.Modifier {
case models.CriterionModifierEquals:
studiosCopy.Modifier = models.CriterionModifierIncludesAll
case models.CriterionModifierNotEquals:
studiosCopy.Modifier = models.CriterionModifierExcludes
}
hh := hierarchicalMultiCriterionHandlerBuilder{
tx: dbWrapper{},
primaryTable: primaryTable,
foreignTable: studioTable,
foreignFK: studioIDColumn,
parentFK: "parent_id",
}
hh.handler(&studiosCopy)(ctx, f)
}
}
type hierarchicalMultiCriterionHandlerBuilder struct {
tx dbWrapper
primaryTable string
foreignTable string
foreignFK string
parentFK string
childFK string
relationsTable string
}
func getHierarchicalValues(ctx context.Context, tx dbWrapper, values []string, table, relationsTable, parentFK string, childFK string, depth *int) (string, error) {
var args []interface{}
if parentFK == "" {
parentFK = "parent_id"
}
if childFK == "" {
childFK = "child_id"
}
depthVal := 0
if depth != nil {
depthVal = *depth
}
if depthVal == 0 {
valid := true
var valuesClauses []string
for _, value := range values {
id, err := strconv.Atoi(value)
// In case of invalid value just run the query.
// Building VALUES() based on provided values just saves a query when depth is 0.
if err != nil {
valid = false
break
}
valuesClauses = append(valuesClauses, fmt.Sprintf("(%d,%d)", id, id))
}
if valid {
return "VALUES" + strings.Join(valuesClauses, ","), nil
}
}
for _, value := range values {
args = append(args, value)
}
inCount := len(args)
var depthCondition string
if depthVal != -1 {
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
}
withClauseMap := utils.StrFormatMap{
"table": table,
"relationsTable": relationsTable,
"inBinding": getInBinding(inCount),
"recursiveSelect": "",
"parentFK": parentFK,
"childFK": childFK,
"depthCondition": depthCondition,
"unionClause": "",
}
if relationsTable != "" {
withClauseMap["recursiveSelect"] = utils.StrFormat(`SELECT p.root_id, c.{childFK}, depth + 1 FROM {relationsTable} AS c
INNER JOIN items as p ON c.{parentFK} = p.item_id
`, withClauseMap)
} else {
withClauseMap["recursiveSelect"] = utils.StrFormat(`SELECT p.root_id, c.id, depth + 1 FROM {table} as c
INNER JOIN items as p ON c.{parentFK} = p.item_id
`, withClauseMap)
}
if depthVal != 0 {
withClauseMap["unionClause"] = utils.StrFormat(`
UNION {recursiveSelect} {depthCondition}
`, withClauseMap)
}
withClause := utils.StrFormat(`items AS (
SELECT id as root_id, id as item_id, 0 as depth FROM {table}
WHERE id in {inBinding}
{unionClause})
`, withClauseMap)
query := fmt.Sprintf("WITH RECURSIVE %s SELECT 'VALUES' || GROUP_CONCAT('(' || root_id || ', ' || item_id || ')') AS val FROM items", withClause)
var valuesClause sql.NullString
err := tx.Get(ctx, &valuesClause, query, args...)
if err != nil {
return "", fmt.Errorf("failed to get hierarchical values: %w", err)
}
// if no values are found, just return a values string with the values only
if !valuesClause.Valid {
for i, value := range values {
values[i] = fmt.Sprintf("(%s, %s)", value, value)
}
valuesClause.String = "VALUES" + strings.Join(values, ",")
}
return valuesClause.String, nil
}
func addHierarchicalConditionClauses(f *filterBuilder, criterion models.HierarchicalMultiCriterionInput, table, idColumn string) {
switch criterion.Modifier {
case models.CriterionModifierIncludes:
f.addWhere(fmt.Sprintf("%s.%s IS NOT NULL", table, idColumn))
case models.CriterionModifierIncludesAll:
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)))
case models.CriterionModifierExcludes:
f.addWhere(fmt.Sprintf("%s.%s IS NULL", table, idColumn))
}
}
func (m *hierarchicalMultiCriterionHandlerBuilder) handler(c *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if c != nil {
// make a copy so we don't modify the original
criterion := *c
// don't support equals/not equals
if criterion.Modifier == models.CriterionModifierEquals || criterion.Modifier == models.CriterionModifierNotEquals {
f.setError(fmt.Errorf("modifier %s is not supported for hierarchical multi criterion", criterion.Modifier))
return
}
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
var notClause string
if criterion.Modifier == models.CriterionModifierNotNull {
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 && len(criterion.Excludes) == 0 {
return
}
// combine excludes if excludes modifier is selected
if criterion.Modifier == models.CriterionModifierExcludes {
criterion.Modifier = models.CriterionModifierIncludesAll
criterion.Excludes = append(criterion.Excludes, criterion.Value...)
criterion.Value = nil
}
if len(criterion.Value) > 0 {
valuesClause, err := getHierarchicalValues(ctx, m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, m.childFK, criterion.Depth)
if err != nil {
f.setError(err)
return
}
switch criterion.Modifier {
case models.CriterionModifierIncludes:
f.addWhere(fmt.Sprintf("%s.%s IN (SELECT column2 FROM (%s))", m.primaryTable, m.foreignFK, valuesClause))
case models.CriterionModifierIncludesAll:
f.addWhere(fmt.Sprintf("%s.%s IN (SELECT column2 FROM (%s))", m.primaryTable, m.foreignFK, valuesClause))
f.addHaving(fmt.Sprintf("count(distinct %s.%s) IS %d", m.primaryTable, m.foreignFK, len(criterion.Value)))
}
}
if len(criterion.Excludes) > 0 {
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))
}
}
}
}
type joinedHierarchicalMultiCriterionHandlerBuilder struct {
tx dbWrapper
primaryTable string
primaryKey string
foreignTable string
foreignFK string
parentFK string
childFK string
relationsTable string
joinAs string
joinTable string
primaryFK string
}
func (m *joinedHierarchicalMultiCriterionHandlerBuilder) addHierarchicalConditionClauses(f *filterBuilder, criterion models.HierarchicalMultiCriterionInput, table, idColumn string) {
primaryKey := m.primaryKey
if primaryKey == "" {
primaryKey = "id"
}
switch criterion.Modifier {
case models.CriterionModifierEquals:
// includes only the provided ids
f.addWhere(fmt.Sprintf("%s.%s IS NOT NULL", table, idColumn))
f.addHaving(fmt.Sprintf("count(distinct %s.%s) IS %d", table, idColumn, len(criterion.Value)))
f.addWhere(utils.StrFormat("(SELECT COUNT(*) FROM {joinTable} s WHERE s.{primaryFK} = {primaryTable}.{primaryKey}) = ?", utils.StrFormatMap{
"joinTable": m.joinTable,
"primaryFK": m.primaryFK,
"primaryTable": m.primaryTable,
"primaryKey": primaryKey,
}), len(criterion.Value))
case models.CriterionModifierNotEquals:
f.setError(fmt.Errorf("not equals modifier is not supported for hierarchical multi criterion input"))
default:
addHierarchicalConditionClauses(f, criterion, table, idColumn)
}
}
func (m *joinedHierarchicalMultiCriterionHandlerBuilder) handler(c *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if c != nil {
// make a copy so we don't modify the original
criterion := *c
joinAlias := m.joinAs
primaryKey := m.primaryKey
if primaryKey == "" {
primaryKey = "id"
}
if criterion.Modifier == models.CriterionModifierEquals && criterion.Depth != nil && *criterion.Depth != 0 {
f.setError(fmt.Errorf("depth is not supported for equals modifier in hierarchical multi criterion input"))
return
}
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
var notClause string
if criterion.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addLeftJoin(m.joinTable, joinAlias, fmt.Sprintf("%s.%s = %s.%s", joinAlias, m.primaryFK, m.primaryTable, primaryKey))
f.addWhere(utils.StrFormat("{table}.{column} IS {not} NULL", utils.StrFormatMap{
"table": joinAlias,
"column": m.foreignFK,
"not": notClause,
}))
return
}
// combine excludes if excludes modifier is selected
if criterion.Modifier == models.CriterionModifierExcludes {
criterion.Modifier = models.CriterionModifierIncludesAll
criterion.Excludes = append(criterion.Excludes, criterion.Value...)
criterion.Value = nil
}
if len(criterion.Value) == 0 && len(criterion.Excludes) == 0 {
return
}
if len(criterion.Value) > 0 {
valuesClause, err := getHierarchicalValues(ctx, m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, m.childFK, criterion.Depth)
if err != nil {
f.setError(err)
return
}
joinTable := utils.StrFormat(`(
SELECT j.*, d.column1 AS root_id, d.column2 AS item_id FROM {joinTable} AS j
INNER JOIN ({valuesClause}) AS d ON j.{foreignFK} = d.column2
)
`, utils.StrFormatMap{
"joinTable": m.joinTable,
"foreignFK": m.foreignFK,
"valuesClause": valuesClause,
})
f.addLeftJoin(joinTable, joinAlias, fmt.Sprintf("%s.%s = %s.%s", joinAlias, m.primaryFK, m.primaryTable, primaryKey))
m.addHierarchicalConditionClauses(f, criterion, joinAlias, "root_id")
}
if len(criterion.Excludes) > 0 {
valuesClause, err := getHierarchicalValues(ctx, m.tx, criterion.Excludes, m.foreignTable, m.relationsTable, m.parentFK, m.childFK, criterion.Depth)
if err != nil {
f.setError(err)
return
}
joinTable := utils.StrFormat(`(
SELECT j2.*, e.column1 AS root_id, e.column2 AS item_id FROM {joinTable} AS j2
INNER JOIN ({valuesClause}) AS e ON j2.{foreignFK} = e.column2
)
`, utils.StrFormatMap{
"joinTable": m.joinTable,
"foreignFK": m.foreignFK,
"valuesClause": valuesClause,
})
joinAlias2 := joinAlias + "2"
f.addLeftJoin(joinTable, joinAlias2, fmt.Sprintf("%s.%s = %s.%s", joinAlias2, m.primaryFK, m.primaryTable, primaryKey))
// modify for exclusion
criterionCopy := criterion
criterionCopy.Modifier = models.CriterionModifierExcludes
criterionCopy.Value = c.Excludes
m.addHierarchicalConditionClauses(f, criterionCopy, joinAlias2, "root_id")
}
}
}
}
type joinedPerformerTagsHandler struct {
criterion *models.HierarchicalMultiCriterionInput
primaryTable string // eg scenes
joinTable string // eg performers_scenes
joinPrimaryKey string // eg scene_id
}
func (h *joinedPerformerTagsHandler) handle(ctx context.Context, f *filterBuilder) {
tags := h.criterion
if tags != nil {
criterion := tags.CombineExcludes()
// validate the modifier
switch criterion.Modifier {
case models.CriterionModifierIncludesAll, models.CriterionModifierIncludes, models.CriterionModifierExcludes, models.CriterionModifierIsNull, models.CriterionModifierNotNull:
// valid
default:
f.setError(fmt.Errorf("invalid modifier %s for performer tags", criterion.Modifier))
}
strFormatMap := utils.StrFormatMap{
"primaryTable": h.primaryTable,
"joinTable": h.joinTable,
"joinPrimaryKey": h.joinPrimaryKey,
"inBinding": getInBinding(len(criterion.Value)),
}
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
var notClause string
if criterion.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addLeftJoin(h.joinTable, "", utils.StrFormat("{primaryTable}.id = {joinTable}.{joinPrimaryKey}", strFormatMap))
f.addLeftJoin("performers_tags", "", utils.StrFormat("{joinTable}.performer_id = performers_tags.performer_id", strFormatMap))
f.addWhere(fmt.Sprintf("performers_tags.tag_id IS %s NULL", notClause))
return
}
if len(criterion.Value) == 0 && len(criterion.Excludes) == 0 {
return
}
if len(criterion.Value) > 0 {
valuesClause, err := getHierarchicalValues(ctx, dbWrapper{}, criterion.Value, tagTable, "tags_relations", "", "", criterion.Depth)
if err != nil {
f.setError(err)
return
}
f.addWith(utils.StrFormat(`performer_tags AS (
SELECT ps.{joinPrimaryKey} as primaryID, t.column1 AS root_tag_id FROM {joinTable} ps
INNER JOIN performers_tags pt ON pt.performer_id = ps.performer_id
INNER JOIN (`+valuesClause+`) t ON t.column2 = pt.tag_id
)`, strFormatMap))
f.addLeftJoin("performer_tags", "", utils.StrFormat("performer_tags.primaryID = {primaryTable}.id", strFormatMap))
addHierarchicalConditionClauses(f, criterion, "performer_tags", "root_tag_id")
}
if len(criterion.Excludes) > 0 {
valuesClause, err := getHierarchicalValues(ctx, dbWrapper{}, criterion.Excludes, tagTable, "tags_relations", "", "", criterion.Depth)
if err != nil {
f.setError(err)
return
}
clause := utils.StrFormat("{primaryTable}.id NOT IN (SELECT {joinTable}.{joinPrimaryKey} FROM {joinTable} INNER JOIN performers_tags ON {joinTable}.performer_id = performers_tags.performer_id WHERE performers_tags.tag_id IN (SELECT column2 FROM (%s)))", strFormatMap)
f.addWhere(fmt.Sprintf(clause, valuesClause))
}
}
}
type stashIDCriterionHandler struct {
c *models.StashIDCriterionInput
stashIDRepository *stashIDRepository
stashIDTableAs string
parentIDCol string
}
func (h *stashIDCriterionHandler) handle(ctx context.Context, f *filterBuilder) {
if h.c == nil {
return
}
stashIDRepo := h.stashIDRepository
t := stashIDRepo.tableName
if h.stashIDTableAs != "" {
t = h.stashIDTableAs
}
joinClause := fmt.Sprintf("%s.%s = %s", t, stashIDRepo.idColumn, h.parentIDCol)
if h.c.Endpoint != nil && *h.c.Endpoint != "" {
joinClause += fmt.Sprintf(" AND %s.endpoint = '%s'", t, *h.c.Endpoint)
}
f.addLeftJoin(stashIDRepo.tableName, h.stashIDTableAs, joinClause)
v := ""
if h.c.StashID != nil {
v = *h.c.StashID
}
stringCriterionHandler(&models.StringCriterionInput{
Value: v,
Modifier: h.c.Modifier,
}, t+".stash_id")(ctx, f)
}

View File

@@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"path/filepath"
"regexp"
"github.com/doug-martin/goqu/v9"
"github.com/doug-martin/goqu/v9/exp"
@@ -113,9 +112,75 @@ func (r *galleryRowRecord) fromPartial(o models.GalleryPartial) {
r.setTimestamp("updated_at", o.UpdatedAt)
}
type GalleryStore struct {
type galleryRepositoryType struct {
repository
performers joinRepository
images joinRepository
tags joinRepository
scenes joinRepository
files filesRepository
}
func (r *galleryRepositoryType) addGalleriesFilesTable(f *filterBuilder) {
f.addLeftJoin(galleriesFilesTable, "", "galleries_files.gallery_id = galleries.id")
}
func (r *galleryRepositoryType) addFilesTable(f *filterBuilder) {
r.addGalleriesFilesTable(f)
f.addLeftJoin(fileTable, "", "galleries_files.file_id = files.id")
}
func (r *galleryRepositoryType) addFoldersTable(f *filterBuilder) {
r.addFilesTable(f)
f.addLeftJoin(folderTable, "", "files.parent_folder_id = folders.id")
}
var (
galleryRepository = galleryRepositoryType{
repository: repository{
tableName: galleryTable,
idColumn: idColumn,
},
performers: joinRepository{
repository: repository{
tableName: performersGalleriesTable,
idColumn: galleryIDColumn,
},
fkColumn: "performer_id",
},
tags: joinRepository{
repository: repository{
tableName: galleriesTagsTable,
idColumn: galleryIDColumn,
},
fkColumn: "tag_id",
foreignTable: tagTable,
orderBy: "tags.name ASC",
},
images: joinRepository{
repository: repository{
tableName: galleriesImagesTable,
idColumn: galleryIDColumn,
},
fkColumn: "image_id",
},
scenes: joinRepository{
repository: repository{
tableName: galleriesScenesTable,
idColumn: galleryIDColumn,
},
fkColumn: sceneIDColumn,
},
files: filesRepository{
repository: repository{
tableName: galleriesFilesTable,
idColumn: galleryIDColumn,
},
},
}
)
type GalleryStore struct {
tableMgr *table
fileStore *FileStore
@@ -124,10 +189,6 @@ type GalleryStore struct {
func NewGalleryStore(fileStore *FileStore, folderStore *FolderStore) *GalleryStore {
return &GalleryStore{
repository: repository{
tableName: galleryTable,
idColumn: idColumn,
},
tableMgr: galleryTableMgr,
fileStore: fileStore,
folderStore: folderStore,
@@ -309,7 +370,7 @@ func (qb *GalleryStore) Destroy(ctx context.Context, id int) error {
}
func (qb *GalleryStore) GetFiles(ctx context.Context, id int) ([]models.File, error) {
fileIDs, err := qb.filesRepository().get(ctx, id)
fileIDs, err := galleryRepository.files.get(ctx, id)
if err != nil {
return nil, err
}
@@ -328,7 +389,7 @@ func (qb *GalleryStore) GetFiles(ctx context.Context, id int) ([]models.File, er
func (qb *GalleryStore) GetManyFileIDs(ctx context.Context, ids []int) ([][]models.FileID, error) {
const primaryOnly = false
return qb.filesRepository().getMany(ctx, ids, primaryOnly)
return galleryRepository.files.getMany(ctx, ids, primaryOnly)
}
// returns nil, nil if not found
@@ -617,116 +678,6 @@ func (qb *GalleryStore) All(ctx context.Context) ([]*models.Gallery, error) {
return qb.getMany(ctx, qb.selectDataset())
}
func (qb *GalleryStore) validateFilter(galleryFilter *models.GalleryFilterType) error {
const and = "AND"
const or = "OR"
const not = "NOT"
if galleryFilter.And != nil {
if galleryFilter.Or != nil {
return illegalFilterCombination(and, or)
}
if galleryFilter.Not != nil {
return illegalFilterCombination(and, not)
}
return qb.validateFilter(galleryFilter.And)
}
if galleryFilter.Or != nil {
if galleryFilter.Not != nil {
return illegalFilterCombination(or, not)
}
return qb.validateFilter(galleryFilter.Or)
}
if galleryFilter.Not != nil {
return qb.validateFilter(galleryFilter.Not)
}
return nil
}
func (qb *GalleryStore) makeFilter(ctx context.Context, galleryFilter *models.GalleryFilterType) *filterBuilder {
query := &filterBuilder{}
if galleryFilter.And != nil {
query.and(qb.makeFilter(ctx, galleryFilter.And))
}
if galleryFilter.Or != nil {
query.or(qb.makeFilter(ctx, galleryFilter.Or))
}
if galleryFilter.Not != nil {
query.not(qb.makeFilter(ctx, galleryFilter.Not))
}
query.handleCriterion(ctx, intCriterionHandler(galleryFilter.ID, "galleries.id", nil))
query.handleCriterion(ctx, stringCriterionHandler(galleryFilter.Title, "galleries.title"))
query.handleCriterion(ctx, stringCriterionHandler(galleryFilter.Code, "galleries.code"))
query.handleCriterion(ctx, stringCriterionHandler(galleryFilter.Details, "galleries.details"))
query.handleCriterion(ctx, stringCriterionHandler(galleryFilter.Photographer, "galleries.photographer"))
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if galleryFilter.Checksum != nil {
qb.addGalleriesFilesTable(f)
f.addLeftJoin(fingerprintTable, "fingerprints_md5", "galleries_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'")
}
stringCriterionHandler(galleryFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
}))
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if galleryFilter.IsZip != nil {
qb.addGalleriesFilesTable(f)
if *galleryFilter.IsZip {
f.addWhere("galleries_files.file_id IS NOT NULL")
} else {
f.addWhere("galleries_files.file_id IS NULL")
}
}
}))
query.handleCriterion(ctx, qb.galleryPathCriterionHandler(galleryFilter.Path))
query.handleCriterion(ctx, galleryFileCountCriterionHandler(qb, galleryFilter.FileCount))
query.handleCriterion(ctx, intCriterionHandler(galleryFilter.Rating100, "galleries.rating", nil))
query.handleCriterion(ctx, galleryURLsCriterionHandler(galleryFilter.URL))
query.handleCriterion(ctx, boolCriterionHandler(galleryFilter.Organized, "galleries.organized", nil))
query.handleCriterion(ctx, galleryIsMissingCriterionHandler(qb, galleryFilter.IsMissing))
query.handleCriterion(ctx, galleryTagsCriterionHandler(qb, galleryFilter.Tags))
query.handleCriterion(ctx, galleryTagCountCriterionHandler(qb, galleryFilter.TagCount))
query.handleCriterion(ctx, galleryPerformersCriterionHandler(qb, galleryFilter.Performers))
query.handleCriterion(ctx, galleryPerformerCountCriterionHandler(qb, galleryFilter.PerformerCount))
query.handleCriterion(ctx, hasChaptersCriterionHandler(galleryFilter.HasChapters))
query.handleCriterion(ctx, galleryScenesCriterionHandler(qb, galleryFilter.Scenes))
query.handleCriterion(ctx, studioCriterionHandler(galleryTable, galleryFilter.Studios))
query.handleCriterion(ctx, galleryPerformerTagsCriterionHandler(qb, galleryFilter.PerformerTags))
query.handleCriterion(ctx, galleryAverageResolutionCriterionHandler(qb, galleryFilter.AverageResolution))
query.handleCriterion(ctx, galleryImageCountCriterionHandler(qb, galleryFilter.ImageCount))
query.handleCriterion(ctx, galleryPerformerFavoriteCriterionHandler(galleryFilter.PerformerFavorite))
query.handleCriterion(ctx, galleryPerformerAgeCriterionHandler(galleryFilter.PerformerAge))
query.handleCriterion(ctx, dateCriterionHandler(galleryFilter.Date, "galleries.date"))
query.handleCriterion(ctx, timestampCriterionHandler(galleryFilter.CreatedAt, "galleries.created_at"))
query.handleCriterion(ctx, timestampCriterionHandler(galleryFilter.UpdatedAt, "galleries.updated_at"))
return query
}
func (qb *GalleryStore) addGalleriesFilesTable(f *filterBuilder) {
f.addLeftJoin(galleriesFilesTable, "", "galleries_files.gallery_id = galleries.id")
}
func (qb *GalleryStore) addFilesTable(f *filterBuilder) {
qb.addGalleriesFilesTable(f)
f.addLeftJoin(fileTable, "", "galleries_files.file_id = files.id")
}
func (qb *GalleryStore) addFoldersTable(f *filterBuilder) {
qb.addFilesTable(f)
f.addLeftJoin(folderTable, "", "files.parent_folder_id = folders.id")
}
func (qb *GalleryStore) makeQuery(ctx context.Context, galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
if galleryFilter == nil {
galleryFilter = &models.GalleryFilterType{}
@@ -735,7 +686,7 @@ func (qb *GalleryStore) makeQuery(ctx context.Context, galleryFilter *models.Gal
findFilter = &models.FindFilterType{}
}
query := qb.newQuery()
query := galleryRepository.newQuery()
distinctIDs(&query, galleryTable)
if q := findFilter.Q; q != nil && *q != "" {
@@ -773,10 +724,9 @@ func (qb *GalleryStore) makeQuery(ctx context.Context, galleryFilter *models.Gal
query.parseQueryString(searchColumns, *q)
}
if err := qb.validateFilter(galleryFilter); err != nil {
return nil, err
}
filter := qb.makeFilter(ctx, galleryFilter)
filter := filterBuilderFromHandler(ctx, &galleryFilterHandler{
galleryFilter: galleryFilter,
})
if err := query.addFilter(filter); err != nil {
return nil, err
@@ -818,290 +768,6 @@ func (qb *GalleryStore) QueryCount(ctx context.Context, galleryFilter *models.Ga
return query.executeCount(ctx)
}
func galleryURLsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{
joinTable: galleriesURLsTable,
stringColumn: galleriesURLColumn,
addJoinTable: func(f *filterBuilder) {
galleriesURLsTableMgr.join(f, "", "galleries.id")
},
}
return h.handler(url)
}
func (qb *GalleryStore) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
return multiCriterionHandlerBuilder{
primaryTable: galleryTable,
foreignTable: foreignTable,
joinTable: joinTable,
primaryFK: galleryIDColumn,
foreignFK: foreignFK,
addJoinsFunc: addJoinsFunc,
}
}
func (qb *GalleryStore) galleryPathCriterionHandler(c *models.StringCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if c != nil {
qb.addFoldersTable(f)
f.addLeftJoin(folderTable, "gallery_folder", "galleries.folder_id = gallery_folder.id")
const pathColumn = "folders.path"
const basenameColumn = "files.basename"
const folderPathColumn = "gallery_folder.path"
addWildcards := true
not := false
if modifier := c.Modifier; c.Modifier.IsValid() {
switch modifier {
case models.CriterionModifierIncludes:
clause := getPathSearchClauseMany(pathColumn, basenameColumn, c.Value, addWildcards, not)
clause2 := getStringSearchClause([]string{folderPathColumn}, c.Value, false)
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
case models.CriterionModifierExcludes:
not = true
clause := getPathSearchClauseMany(pathColumn, basenameColumn, c.Value, addWildcards, not)
clause2 := getStringSearchClause([]string{folderPathColumn}, c.Value, true)
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
case models.CriterionModifierEquals:
addWildcards = false
clause := getPathSearchClause(pathColumn, basenameColumn, c.Value, addWildcards, not)
clause2 := makeClause(folderPathColumn+" LIKE ?", c.Value)
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
case models.CriterionModifierNotEquals:
addWildcards = false
not = true
clause := getPathSearchClause(pathColumn, basenameColumn, c.Value, addWildcards, not)
clause2 := makeClause(folderPathColumn+" NOT LIKE ?", c.Value)
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
case models.CriterionModifierMatchesRegex:
if _, err := regexp.Compile(c.Value); err != nil {
f.setError(err)
return
}
filepathColumn := fmt.Sprintf("%s || '%s' || %s", pathColumn, string(filepath.Separator), basenameColumn)
clause := makeClause(fmt.Sprintf("%s IS NOT NULL AND %s IS NOT NULL AND %s regexp ?", pathColumn, basenameColumn, filepathColumn), c.Value)
clause2 := makeClause(fmt.Sprintf("%s IS NOT NULL AND %[1]s regexp ?", folderPathColumn), c.Value)
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
case models.CriterionModifierNotMatchesRegex:
if _, err := regexp.Compile(c.Value); err != nil {
f.setError(err)
return
}
filepathColumn := fmt.Sprintf("%s || '%s' || %s", pathColumn, string(filepath.Separator), basenameColumn)
f.addWhere(fmt.Sprintf("%s IS NULL OR %s IS NULL OR %s NOT regexp ?", pathColumn, basenameColumn, filepathColumn), c.Value)
f.addWhere(fmt.Sprintf("%s IS NULL OR %[1]s NOT regexp ?", folderPathColumn), c.Value)
case models.CriterionModifierIsNull:
f.addWhere(fmt.Sprintf("%s IS NULL OR TRIM(%[1]s) = '' OR %s IS NULL OR TRIM(%[2]s) = ''", pathColumn, basenameColumn))
f.addWhere(fmt.Sprintf("%s IS NULL OR TRIM(%[1]s) = ''", folderPathColumn))
case models.CriterionModifierNotNull:
clause := makeClause(fmt.Sprintf("%s IS NOT NULL AND TRIM(%[1]s) != '' AND %s IS NOT NULL AND TRIM(%[2]s) != ''", pathColumn, basenameColumn))
clause2 := makeClause(fmt.Sprintf("%s IS NOT NULL AND TRIM(%[1]s) != ''", folderPathColumn))
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
default:
panic("unsupported string filter modifier")
}
}
}
}
}
func galleryFileCountCriterionHandler(qb *GalleryStore, fileCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: galleryTable,
joinTable: galleriesFilesTable,
primaryFK: galleryIDColumn,
}
return h.handler(fileCount)
}
func galleryIsMissingCriterionHandler(qb *GalleryStore, isMissing *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if isMissing != nil && *isMissing != "" {
switch *isMissing {
case "url":
galleriesURLsTableMgr.join(f, "", "galleries.id")
f.addWhere("gallery_urls.url IS NULL")
case "scenes":
f.addLeftJoin("scenes_galleries", "scenes_join", "scenes_join.gallery_id = galleries.id")
f.addWhere("scenes_join.gallery_id IS NULL")
case "studio":
f.addWhere("galleries.studio_id IS NULL")
case "performers":
qb.performersRepository().join(f, "performers_join", "galleries.id")
f.addWhere("performers_join.gallery_id IS NULL")
case "date":
f.addWhere("galleries.date IS NULL OR galleries.date IS \"\"")
case "tags":
qb.tagsRepository().join(f, "tags_join", "galleries.id")
f.addWhere("tags_join.gallery_id IS NULL")
default:
f.addWhere("(galleries." + *isMissing + " IS NULL OR TRIM(galleries." + *isMissing + ") = '')")
}
}
}
}
func galleryTagsCriterionHandler(qb *GalleryStore, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
h := joinedHierarchicalMultiCriterionHandlerBuilder{
tx: qb.tx,
primaryTable: galleryTable,
foreignTable: tagTable,
foreignFK: "tag_id",
relationsTable: "tags_relations",
joinAs: "image_tag",
joinTable: galleriesTagsTable,
primaryFK: galleryIDColumn,
}
return h.handler(tags)
}
func galleryTagCountCriterionHandler(qb *GalleryStore, tagCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: galleryTable,
joinTable: galleriesTagsTable,
primaryFK: galleryIDColumn,
}
return h.handler(tagCount)
}
func galleryScenesCriterionHandler(qb *GalleryStore, scenes *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) {
qb.scenesRepository().join(f, "", "galleries.id")
f.addLeftJoin("scenes", "", "scenes_galleries.scene_id = scenes.id")
}
h := qb.getMultiCriterionHandlerBuilder(sceneTable, galleriesScenesTable, "scene_id", addJoinsFunc)
return h.handler(scenes)
}
func galleryPerformersCriterionHandler(qb *GalleryStore, performers *models.MultiCriterionInput) criterionHandlerFunc {
h := joinedMultiCriterionHandlerBuilder{
primaryTable: galleryTable,
joinTable: performersGalleriesTable,
joinAs: "performers_join",
primaryFK: galleryIDColumn,
foreignFK: performerIDColumn,
addJoinTable: func(f *filterBuilder) {
qb.performersRepository().join(f, "performers_join", "galleries.id")
},
}
return h.handler(performers)
}
func galleryPerformerCountCriterionHandler(qb *GalleryStore, performerCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: galleryTable,
joinTable: performersGalleriesTable,
primaryFK: galleryIDColumn,
}
return h.handler(performerCount)
}
func galleryImageCountCriterionHandler(qb *GalleryStore, imageCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: galleryTable,
joinTable: galleriesImagesTable,
primaryFK: galleryIDColumn,
}
return h.handler(imageCount)
}
func hasChaptersCriterionHandler(hasChapters *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if hasChapters != nil {
f.addLeftJoin("galleries_chapters", "", "galleries_chapters.gallery_id = galleries.id")
if *hasChapters == "true" {
f.addHaving("count(galleries_chapters.gallery_id) > 0")
} else {
f.addWhere("galleries_chapters.id IS NULL")
}
}
}
}
func galleryPerformerTagsCriterionHandler(qb *GalleryStore, tags *models.HierarchicalMultiCriterionInput) criterionHandler {
return &joinedPerformerTagsHandler{
criterion: tags,
primaryTable: galleryTable,
joinTable: performersGalleriesTable,
joinPrimaryKey: galleryIDColumn,
}
}
func galleryPerformerFavoriteCriterionHandler(performerfavorite *bool) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if performerfavorite != nil {
f.addLeftJoin("performers_galleries", "", "galleries.id = performers_galleries.gallery_id")
if *performerfavorite {
// contains at least one favorite
f.addLeftJoin("performers", "", "performers.id = performers_galleries.performer_id")
f.addWhere("performers.favorite = 1")
} else {
// contains zero favorites
f.addLeftJoin(`(SELECT performers_galleries.gallery_id as id FROM performers_galleries
JOIN performers ON performers.id = performers_galleries.performer_id
GROUP BY performers_galleries.gallery_id HAVING SUM(performers.favorite) = 0)`, "nofaves", "galleries.id = nofaves.id")
f.addWhere("performers_galleries.gallery_id IS NULL OR nofaves.id IS NOT NULL")
}
}
}
}
func galleryPerformerAgeCriterionHandler(performerAge *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if performerAge != nil {
f.addInnerJoin("performers_galleries", "", "galleries.id = performers_galleries.gallery_id")
f.addInnerJoin("performers", "", "performers_galleries.performer_id = performers.id")
f.addWhere("galleries.date != '' AND performers.birthdate != ''")
f.addWhere("galleries.date IS NOT NULL AND performers.birthdate IS NOT NULL")
ageCalc := "cast(strftime('%Y.%m%d', galleries.date) - strftime('%Y.%m%d', performers.birthdate) as int)"
whereClause, args := getIntWhereClause(ageCalc, performerAge.Modifier, performerAge.Value, performerAge.Value2)
f.addWhere(whereClause, args...)
}
}
}
func galleryAverageResolutionCriterionHandler(qb *GalleryStore, resolution *models.ResolutionCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if resolution != nil && resolution.Value.IsValid() {
qb.imagesRepository().join(f, "images_join", "galleries.id")
f.addLeftJoin("images", "", "images_join.image_id = images.id")
f.addLeftJoin("images_files", "", "images.id = images_files.image_id")
f.addLeftJoin("image_files", "", "images_files.file_id = image_files.file_id")
min := resolution.Value.GetMinResolution()
max := resolution.Value.GetMaxResolution()
const widthHeight = "avg(MIN(image_files.width, image_files.height))"
switch resolution.Modifier {
case models.CriterionModifierEquals:
f.addHaving(fmt.Sprintf("%s BETWEEN %d AND %d", widthHeight, min, max))
case models.CriterionModifierNotEquals:
f.addHaving(fmt.Sprintf("%s NOT BETWEEN %d AND %d", widthHeight, min, max))
case models.CriterionModifierLessThan:
f.addHaving(fmt.Sprintf("%s < %d", widthHeight, min))
case models.CriterionModifierGreaterThan:
f.addHaving(fmt.Sprintf("%s > %d", widthHeight, max))
}
}
}
}
var gallerySortOptions = sortOptions{
"created_at",
"date",
@@ -1194,92 +860,36 @@ func (qb *GalleryStore) GetURLs(ctx context.Context, galleryID int) ([]string, e
return galleriesURLsTableMgr.get(ctx, galleryID)
}
func (qb *GalleryStore) filesRepository() *filesRepository {
return &filesRepository{
repository: repository{
tx: qb.tx,
tableName: galleriesFilesTable,
idColumn: galleryIDColumn,
},
}
}
func (qb *GalleryStore) AddFileID(ctx context.Context, id int, fileID models.FileID) error {
const firstPrimary = false
return galleriesFilesTableMgr.insertJoins(ctx, id, firstPrimary, []models.FileID{fileID})
}
func (qb *GalleryStore) performersRepository() *joinRepository {
return &joinRepository{
repository: repository{
tx: qb.tx,
tableName: performersGalleriesTable,
idColumn: galleryIDColumn,
},
fkColumn: "performer_id",
}
}
func (qb *GalleryStore) GetPerformerIDs(ctx context.Context, id int) ([]int, error) {
return qb.performersRepository().getIDs(ctx, id)
}
func (qb *GalleryStore) tagsRepository() *joinRepository {
return &joinRepository{
repository: repository{
tx: qb.tx,
tableName: galleriesTagsTable,
idColumn: galleryIDColumn,
},
fkColumn: "tag_id",
foreignTable: tagTable,
orderBy: "tags.name ASC",
}
return galleryRepository.performers.getIDs(ctx, id)
}
func (qb *GalleryStore) GetTagIDs(ctx context.Context, id int) ([]int, error) {
return qb.tagsRepository().getIDs(ctx, id)
}
func (qb *GalleryStore) imagesRepository() *joinRepository {
return &joinRepository{
repository: repository{
tx: qb.tx,
tableName: galleriesImagesTable,
idColumn: galleryIDColumn,
},
fkColumn: "image_id",
}
return galleryRepository.tags.getIDs(ctx, id)
}
func (qb *GalleryStore) GetImageIDs(ctx context.Context, galleryID int) ([]int, error) {
return qb.imagesRepository().getIDs(ctx, galleryID)
return galleryRepository.images.getIDs(ctx, galleryID)
}
func (qb *GalleryStore) AddImages(ctx context.Context, galleryID int, imageIDs ...int) error {
return qb.imagesRepository().insertOrIgnore(ctx, galleryID, imageIDs...)
return galleryRepository.images.insertOrIgnore(ctx, galleryID, imageIDs...)
}
func (qb *GalleryStore) RemoveImages(ctx context.Context, galleryID int, imageIDs ...int) error {
return qb.imagesRepository().destroyJoins(ctx, galleryID, imageIDs...)
return galleryRepository.images.destroyJoins(ctx, galleryID, imageIDs...)
}
func (qb *GalleryStore) UpdateImages(ctx context.Context, galleryID int, imageIDs []int) error {
// Delete the existing joins and then create new ones
return qb.imagesRepository().replace(ctx, galleryID, imageIDs)
}
func (qb *GalleryStore) scenesRepository() *joinRepository {
return &joinRepository{
repository: repository{
tx: qb.tx,
tableName: galleriesScenesTable,
idColumn: galleryIDColumn,
},
fkColumn: sceneIDColumn,
}
return galleryRepository.images.replace(ctx, galleryID, imageIDs)
}
func (qb *GalleryStore) GetSceneIDs(ctx context.Context, id int) ([]int, error) {
return qb.scenesRepository().getIDs(ctx, id)
return galleryRepository.scenes.getIDs(ctx, id)
}

View File

@@ -0,0 +1,432 @@
package sqlite
import (
"context"
"fmt"
"path/filepath"
"regexp"
"github.com/stashapp/stash/pkg/models"
)
type galleryFilterHandler struct {
galleryFilter *models.GalleryFilterType
}
func (qb *galleryFilterHandler) validate() error {
galleryFilter := qb.galleryFilter
if galleryFilter == nil {
return nil
}
if err := validateFilterCombination(galleryFilter.OperatorFilter); err != nil {
return err
}
if subFilter := galleryFilter.SubFilter(); subFilter != nil {
sqb := &galleryFilterHandler{galleryFilter: subFilter}
if err := sqb.validate(); err != nil {
return err
}
}
return nil
}
func (qb *galleryFilterHandler) handle(ctx context.Context, f *filterBuilder) {
galleryFilter := qb.galleryFilter
if galleryFilter == nil {
return
}
if err := qb.validate(); err != nil {
f.setError(err)
return
}
sf := galleryFilter.SubFilter()
if sf != nil {
sub := &galleryFilterHandler{sf}
handleSubFilter(ctx, sub, f, galleryFilter.OperatorFilter)
}
f.handleCriterion(ctx, qb.criterionHandler())
}
func (qb *galleryFilterHandler) criterionHandler() criterionHandler {
filter := qb.galleryFilter
return compoundHandler{
intCriterionHandler(filter.ID, "galleries.id", nil),
stringCriterionHandler(filter.Title, "galleries.title"),
stringCriterionHandler(filter.Code, "galleries.code"),
stringCriterionHandler(filter.Details, "galleries.details"),
stringCriterionHandler(filter.Photographer, "galleries.photographer"),
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if filter.Checksum != nil {
galleryRepository.addGalleriesFilesTable(f)
f.addLeftJoin(fingerprintTable, "fingerprints_md5", "galleries_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'")
}
stringCriterionHandler(filter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
}),
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if filter.IsZip != nil {
galleryRepository.addGalleriesFilesTable(f)
if *filter.IsZip {
f.addWhere("galleries_files.file_id IS NOT NULL")
} else {
f.addWhere("galleries_files.file_id IS NULL")
}
}
}),
qb.pathCriterionHandler(filter.Path),
qb.fileCountCriterionHandler(filter.FileCount),
intCriterionHandler(filter.Rating100, "galleries.rating", nil),
qb.urlsCriterionHandler(filter.URL),
boolCriterionHandler(filter.Organized, "galleries.organized", nil),
qb.missingCriterionHandler(filter.IsMissing),
qb.tagsCriterionHandler(filter.Tags),
qb.tagCountCriterionHandler(filter.TagCount),
qb.performersCriterionHandler(filter.Performers),
qb.performerCountCriterionHandler(filter.PerformerCount),
qb.scenesCriterionHandler(filter.Scenes),
qb.hasChaptersCriterionHandler(filter.HasChapters),
studioCriterionHandler(galleryTable, filter.Studios),
qb.performerTagsCriterionHandler(filter.PerformerTags),
qb.averageResolutionCriterionHandler(filter.AverageResolution),
qb.imageCountCriterionHandler(filter.ImageCount),
qb.performerFavoriteCriterionHandler(filter.PerformerFavorite),
qb.performerAgeCriterionHandler(filter.PerformerAge),
&dateCriterionHandler{filter.Date, "galleries.date", nil},
&timestampCriterionHandler{filter.CreatedAt, "galleries.created_at", nil},
&timestampCriterionHandler{filter.UpdatedAt, "galleries.updated_at", nil},
&relatedFilterHandler{
relatedIDCol: "scenes_galleries.scene_id",
relatedRepo: sceneRepository.repository,
relatedHandler: &sceneFilterHandler{filter.ScenesFilter},
joinFn: func(f *filterBuilder) {
galleryRepository.scenes.innerJoin(f, "", "galleries.id")
},
},
&relatedFilterHandler{
relatedIDCol: "galleries_images.image_id",
relatedRepo: imageRepository.repository,
relatedHandler: &imageFilterHandler{filter.ImagesFilter},
joinFn: func(f *filterBuilder) {
galleryRepository.images.innerJoin(f, "", "galleries.id")
},
},
&relatedFilterHandler{
relatedIDCol: "performers_join.performer_id",
relatedRepo: performerRepository.repository,
relatedHandler: &performerFilterHandler{filter.PerformersFilter},
joinFn: func(f *filterBuilder) {
galleryRepository.performers.innerJoin(f, "performers_join", "galleries.id")
},
},
&relatedFilterHandler{
relatedIDCol: "galleries.studio_id",
relatedRepo: studioRepository.repository,
relatedHandler: &studioFilterHandler{filter.StudiosFilter},
},
&relatedFilterHandler{
relatedIDCol: "gallery_tag.tag_id",
relatedRepo: tagRepository.repository,
relatedHandler: &tagFilterHandler{filter.TagsFilter},
joinFn: func(f *filterBuilder) {
galleryRepository.tags.innerJoin(f, "gallery_tag", "galleries.id")
},
},
}
}
func (qb *galleryFilterHandler) urlsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{
joinTable: galleriesURLsTable,
stringColumn: galleriesURLColumn,
addJoinTable: func(f *filterBuilder) {
galleriesURLsTableMgr.join(f, "", "galleries.id")
},
}
return h.handler(url)
}
func (qb *galleryFilterHandler) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
return multiCriterionHandlerBuilder{
primaryTable: galleryTable,
foreignTable: foreignTable,
joinTable: joinTable,
primaryFK: galleryIDColumn,
foreignFK: foreignFK,
addJoinsFunc: addJoinsFunc,
}
}
func (qb *galleryFilterHandler) pathCriterionHandler(c *models.StringCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if c != nil {
galleryRepository.addFoldersTable(f)
f.addLeftJoin(folderTable, "gallery_folder", "galleries.folder_id = gallery_folder.id")
const pathColumn = "folders.path"
const basenameColumn = "files.basename"
const folderPathColumn = "gallery_folder.path"
addWildcards := true
not := false
if modifier := c.Modifier; c.Modifier.IsValid() {
switch modifier {
case models.CriterionModifierIncludes:
clause := getPathSearchClauseMany(pathColumn, basenameColumn, c.Value, addWildcards, not)
clause2 := getStringSearchClause([]string{folderPathColumn}, c.Value, false)
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
case models.CriterionModifierExcludes:
not = true
clause := getPathSearchClauseMany(pathColumn, basenameColumn, c.Value, addWildcards, not)
clause2 := getStringSearchClause([]string{folderPathColumn}, c.Value, true)
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
case models.CriterionModifierEquals:
addWildcards = false
clause := getPathSearchClause(pathColumn, basenameColumn, c.Value, addWildcards, not)
clause2 := makeClause(folderPathColumn+" LIKE ?", c.Value)
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
case models.CriterionModifierNotEquals:
addWildcards = false
not = true
clause := getPathSearchClause(pathColumn, basenameColumn, c.Value, addWildcards, not)
clause2 := makeClause(folderPathColumn+" NOT LIKE ?", c.Value)
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
case models.CriterionModifierMatchesRegex:
if _, err := regexp.Compile(c.Value); err != nil {
f.setError(err)
return
}
filepathColumn := fmt.Sprintf("%s || '%s' || %s", pathColumn, string(filepath.Separator), basenameColumn)
clause := makeClause(fmt.Sprintf("%s IS NOT NULL AND %s IS NOT NULL AND %s regexp ?", pathColumn, basenameColumn, filepathColumn), c.Value)
clause2 := makeClause(fmt.Sprintf("%s IS NOT NULL AND %[1]s regexp ?", folderPathColumn), c.Value)
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
case models.CriterionModifierNotMatchesRegex:
if _, err := regexp.Compile(c.Value); err != nil {
f.setError(err)
return
}
filepathColumn := fmt.Sprintf("%s || '%s' || %s", pathColumn, string(filepath.Separator), basenameColumn)
f.addWhere(fmt.Sprintf("%s IS NULL OR %s IS NULL OR %s NOT regexp ?", pathColumn, basenameColumn, filepathColumn), c.Value)
f.addWhere(fmt.Sprintf("%s IS NULL OR %[1]s NOT regexp ?", folderPathColumn), c.Value)
case models.CriterionModifierIsNull:
f.addWhere(fmt.Sprintf("%s IS NULL OR TRIM(%[1]s) = '' OR %s IS NULL OR TRIM(%[2]s) = ''", pathColumn, basenameColumn))
f.addWhere(fmt.Sprintf("%s IS NULL OR TRIM(%[1]s) = ''", folderPathColumn))
case models.CriterionModifierNotNull:
clause := makeClause(fmt.Sprintf("%s IS NOT NULL AND TRIM(%[1]s) != '' AND %s IS NOT NULL AND TRIM(%[2]s) != ''", pathColumn, basenameColumn))
clause2 := makeClause(fmt.Sprintf("%s IS NOT NULL AND TRIM(%[1]s) != ''", folderPathColumn))
f.whereClauses = append(f.whereClauses, orClauses(clause, clause2))
default:
panic("unsupported string filter modifier")
}
}
}
}
}
func (qb *galleryFilterHandler) fileCountCriterionHandler(fileCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: galleryTable,
joinTable: galleriesFilesTable,
primaryFK: galleryIDColumn,
}
return h.handler(fileCount)
}
func (qb *galleryFilterHandler) missingCriterionHandler(isMissing *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if isMissing != nil && *isMissing != "" {
switch *isMissing {
case "url":
galleriesURLsTableMgr.join(f, "", "galleries.id")
f.addWhere("gallery_urls.url IS NULL")
case "scenes":
f.addLeftJoin("scenes_galleries", "scenes_join", "scenes_join.gallery_id = galleries.id")
f.addWhere("scenes_join.gallery_id IS NULL")
case "studio":
f.addWhere("galleries.studio_id IS NULL")
case "performers":
galleryRepository.performers.join(f, "performers_join", "galleries.id")
f.addWhere("performers_join.gallery_id IS NULL")
case "date":
f.addWhere("galleries.date IS NULL OR galleries.date IS \"\"")
case "tags":
galleryRepository.tags.join(f, "tags_join", "galleries.id")
f.addWhere("tags_join.gallery_id IS NULL")
default:
f.addWhere("(galleries." + *isMissing + " IS NULL OR TRIM(galleries." + *isMissing + ") = '')")
}
}
}
}
func (qb *galleryFilterHandler) tagsCriterionHandler(tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
h := joinedHierarchicalMultiCriterionHandlerBuilder{
primaryTable: galleryTable,
foreignTable: tagTable,
foreignFK: "tag_id",
relationsTable: "tags_relations",
joinAs: "gallery_tag",
joinTable: galleriesTagsTable,
primaryFK: galleryIDColumn,
}
return h.handler(tags)
}
func (qb *galleryFilterHandler) tagCountCriterionHandler(tagCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: galleryTable,
joinTable: galleriesTagsTable,
primaryFK: galleryIDColumn,
}
return h.handler(tagCount)
}
func (qb *galleryFilterHandler) scenesCriterionHandler(scenes *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) {
galleryRepository.scenes.join(f, "", "galleries.id")
f.addLeftJoin("scenes", "", "scenes_galleries.scene_id = scenes.id")
}
h := qb.getMultiCriterionHandlerBuilder(sceneTable, galleriesScenesTable, "scene_id", addJoinsFunc)
return h.handler(scenes)
}
func (qb *galleryFilterHandler) performersCriterionHandler(performers *models.MultiCriterionInput) criterionHandlerFunc {
h := joinedMultiCriterionHandlerBuilder{
primaryTable: galleryTable,
joinTable: performersGalleriesTable,
joinAs: "performers_join",
primaryFK: galleryIDColumn,
foreignFK: performerIDColumn,
addJoinTable: func(f *filterBuilder) {
galleryRepository.performers.join(f, "performers_join", "galleries.id")
},
}
return h.handler(performers)
}
func (qb *galleryFilterHandler) performerCountCriterionHandler(performerCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: galleryTable,
joinTable: performersGalleriesTable,
primaryFK: galleryIDColumn,
}
return h.handler(performerCount)
}
func (qb *galleryFilterHandler) imageCountCriterionHandler(imageCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: galleryTable,
joinTable: galleriesImagesTable,
primaryFK: galleryIDColumn,
}
return h.handler(imageCount)
}
func (qb *galleryFilterHandler) hasChaptersCriterionHandler(hasChapters *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if hasChapters != nil {
f.addLeftJoin("galleries_chapters", "", "galleries_chapters.gallery_id = galleries.id")
if *hasChapters == "true" {
f.addHaving("count(galleries_chapters.gallery_id) > 0")
} else {
f.addWhere("galleries_chapters.id IS NULL")
}
}
}
}
func (qb *galleryFilterHandler) performerTagsCriterionHandler(tags *models.HierarchicalMultiCriterionInput) criterionHandler {
return &joinedPerformerTagsHandler{
criterion: tags,
primaryTable: galleryTable,
joinTable: performersGalleriesTable,
joinPrimaryKey: galleryIDColumn,
}
}
func (qb *galleryFilterHandler) performerFavoriteCriterionHandler(performerfavorite *bool) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if performerfavorite != nil {
f.addLeftJoin("performers_galleries", "", "galleries.id = performers_galleries.gallery_id")
if *performerfavorite {
// contains at least one favorite
f.addLeftJoin("performers", "", "performers.id = performers_galleries.performer_id")
f.addWhere("performers.favorite = 1")
} else {
// contains zero favorites
f.addLeftJoin(`(SELECT performers_galleries.gallery_id as id FROM performers_galleries
JOIN performers ON performers.id = performers_galleries.performer_id
GROUP BY performers_galleries.gallery_id HAVING SUM(performers.favorite) = 0)`, "nofaves", "galleries.id = nofaves.id")
f.addWhere("performers_galleries.gallery_id IS NULL OR nofaves.id IS NOT NULL")
}
}
}
}
func (qb *galleryFilterHandler) performerAgeCriterionHandler(performerAge *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if performerAge != nil {
f.addInnerJoin("performers_galleries", "", "galleries.id = performers_galleries.gallery_id")
f.addInnerJoin("performers", "", "performers_galleries.performer_id = performers.id")
f.addWhere("galleries.date != '' AND performers.birthdate != ''")
f.addWhere("galleries.date IS NOT NULL AND performers.birthdate IS NOT NULL")
ageCalc := "cast(strftime('%Y.%m%d', galleries.date) - strftime('%Y.%m%d', performers.birthdate) as int)"
whereClause, args := getIntWhereClause(ageCalc, performerAge.Modifier, performerAge.Value, performerAge.Value2)
f.addWhere(whereClause, args...)
}
}
}
func (qb *galleryFilterHandler) averageResolutionCriterionHandler(resolution *models.ResolutionCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if resolution != nil && resolution.Value.IsValid() {
galleryRepository.images.join(f, "images_join", "galleries.id")
f.addLeftJoin("images", "", "images_join.image_id = images.id")
f.addLeftJoin("images_files", "", "images.id = images_files.image_id")
f.addLeftJoin("image_files", "", "images_files.file_id = image_files.file_id")
min := resolution.Value.GetMinResolution()
max := resolution.Value.GetMaxResolution()
const widthHeight = "avg(MIN(image_files.width, image_files.height))"
switch resolution.Modifier {
case models.CriterionModifierEquals:
f.addHaving(fmt.Sprintf("%s BETWEEN %d AND %d", widthHeight, min, max))
case models.CriterionModifierNotEquals:
f.addHaving(fmt.Sprintf("%s NOT BETWEEN %d AND %d", widthHeight, min, max))
case models.CriterionModifierLessThan:
f.addHaving(fmt.Sprintf("%s < %d", widthHeight, min))
case models.CriterionModifierGreaterThan:
f.addHaving(fmt.Sprintf("%s > %d", widthHeight, max))
}
}
}
}

View File

@@ -1534,10 +1534,12 @@ func TestGalleryQueryPathOr(t *testing.T) {
Value: gallery1Path,
Modifier: models.CriterionModifierEquals,
},
Or: &models.GalleryFilterType{
Path: &models.StringCriterionInput{
Value: gallery2Path,
Modifier: models.CriterionModifierEquals,
OperatorFilter: models.OperatorFilter[models.GalleryFilterType]{
Or: &models.GalleryFilterType{
Path: &models.StringCriterionInput{
Value: gallery2Path,
Modifier: models.CriterionModifierEquals,
},
},
},
}
@@ -1568,10 +1570,12 @@ func TestGalleryQueryPathAndRating(t *testing.T) {
Value: galleryPath,
Modifier: models.CriterionModifierEquals,
},
And: &models.GalleryFilterType{
Rating100: &models.IntCriterionInput{
Value: *galleryRating,
Modifier: models.CriterionModifierEquals,
OperatorFilter: models.OperatorFilter[models.GalleryFilterType]{
And: &models.GalleryFilterType{
Rating100: &models.IntCriterionInput{
Value: *galleryRating,
Modifier: models.CriterionModifierEquals,
},
},
},
}
@@ -1609,8 +1613,10 @@ func TestGalleryQueryPathNotRating(t *testing.T) {
galleryFilter := models.GalleryFilterType{
Path: &pathCriterion,
Not: &models.GalleryFilterType{
Rating100: &ratingCriterion,
OperatorFilter: models.OperatorFilter[models.GalleryFilterType]{
Not: &models.GalleryFilterType{
Rating100: &ratingCriterion,
},
},
}
@@ -1641,8 +1647,10 @@ func TestGalleryIllegalQuery(t *testing.T) {
}
galleryFilter := &models.GalleryFilterType{
And: &subFilter,
Or: &subFilter,
OperatorFilter: models.OperatorFilter[models.GalleryFilterType]{
And: &subFilter,
Or: &subFilter,
},
}
withTxn(func(ctx context.Context) error {

View File

@@ -112,24 +112,87 @@ func (r *imageRowRecord) fromPartial(i models.ImagePartial) {
r.setTimestamp("updated_at", i.UpdatedAt)
}
type ImageStore struct {
type imageRepositoryType struct {
repository
tableMgr *table
oCounterManager
fileStore *FileStore
performers joinRepository
galleries joinRepository
tags joinRepository
files filesRepository
}
func NewImageStore(fileStore *FileStore) *ImageStore {
return &ImageStore{
func (r *imageRepositoryType) addImagesFilesTable(f *filterBuilder) {
f.addLeftJoin(imagesFilesTable, "", "images_files.image_id = images.id")
}
func (r *imageRepositoryType) addFilesTable(f *filterBuilder) {
r.addImagesFilesTable(f)
f.addLeftJoin(fileTable, "", "images_files.file_id = files.id")
}
func (r *imageRepositoryType) addFoldersTable(f *filterBuilder) {
r.addFilesTable(f)
f.addLeftJoin(folderTable, "", "files.parent_folder_id = folders.id")
}
func (r *imageRepositoryType) addImageFilesTable(f *filterBuilder) {
r.addImagesFilesTable(f)
f.addLeftJoin(imageFileTable, "", "image_files.file_id = images_files.file_id")
}
var (
imageRepository = imageRepositoryType{
repository: repository{
tableName: imageTable,
idColumn: idColumn,
},
performers: joinRepository{
repository: repository{
tableName: performersImagesTable,
idColumn: imageIDColumn,
},
fkColumn: performerIDColumn,
},
galleries: joinRepository{
repository: repository{
tableName: galleriesImagesTable,
idColumn: imageIDColumn,
},
fkColumn: galleryIDColumn,
},
files: filesRepository{
repository: repository{
tableName: imagesFilesTable,
idColumn: imageIDColumn,
},
},
tags: joinRepository{
repository: repository{
tableName: imagesTagsTable,
idColumn: imageIDColumn,
},
fkColumn: tagIDColumn,
foreignTable: tagTable,
orderBy: "tags.name ASC",
},
}
)
type ImageStore struct {
tableMgr *table
oCounterManager
repo *storeRepository
}
func NewImageStore(r *storeRepository) *ImageStore {
return &ImageStore{
tableMgr: imageTableMgr,
oCounterManager: oCounterManager{imageTableMgr},
fileStore: fileStore,
repo: r,
}
}
@@ -418,13 +481,13 @@ func (qb *ImageStore) getMany(ctx context.Context, q *goqu.SelectDataset) ([]*mo
}
func (qb *ImageStore) GetFiles(ctx context.Context, id int) ([]models.File, error) {
fileIDs, err := qb.filesRepository().get(ctx, id)
fileIDs, err := imageRepository.files.get(ctx, id)
if err != nil {
return nil, err
}
// use fileStore to load files
files, err := qb.fileStore.Find(ctx, fileIDs...)
files, err := qb.repo.File.Find(ctx, fileIDs...)
if err != nil {
return nil, err
}
@@ -434,7 +497,7 @@ func (qb *ImageStore) GetFiles(ctx context.Context, id int) ([]models.File, erro
func (qb *ImageStore) GetManyFileIDs(ctx context.Context, ids []int) ([][]models.FileID, error) {
const primaryOnly = false
return qb.filesRepository().getMany(ctx, ids, primaryOnly)
return imageRepository.files.getMany(ctx, ids, primaryOnly)
}
func (qb *ImageStore) FindByFileID(ctx context.Context, fileID models.FileID) ([]*models.Image, error) {
@@ -642,110 +705,6 @@ func (qb *ImageStore) All(ctx context.Context) ([]*models.Image, error) {
return qb.getMany(ctx, qb.selectDataset())
}
func (qb *ImageStore) validateFilter(imageFilter *models.ImageFilterType) error {
const and = "AND"
const or = "OR"
const not = "NOT"
if imageFilter.And != nil {
if imageFilter.Or != nil {
return illegalFilterCombination(and, or)
}
if imageFilter.Not != nil {
return illegalFilterCombination(and, not)
}
return qb.validateFilter(imageFilter.And)
}
if imageFilter.Or != nil {
if imageFilter.Not != nil {
return illegalFilterCombination(or, not)
}
return qb.validateFilter(imageFilter.Or)
}
if imageFilter.Not != nil {
return qb.validateFilter(imageFilter.Not)
}
return nil
}
func (qb *ImageStore) makeFilter(ctx context.Context, imageFilter *models.ImageFilterType) *filterBuilder {
query := &filterBuilder{}
if imageFilter.And != nil {
query.and(qb.makeFilter(ctx, imageFilter.And))
}
if imageFilter.Or != nil {
query.or(qb.makeFilter(ctx, imageFilter.Or))
}
if imageFilter.Not != nil {
query.not(qb.makeFilter(ctx, imageFilter.Not))
}
query.handleCriterion(ctx, intCriterionHandler(imageFilter.ID, "images.id", nil))
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if imageFilter.Checksum != nil {
qb.addImagesFilesTable(f)
f.addInnerJoin(fingerprintTable, "fingerprints_md5", "images_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'")
}
stringCriterionHandler(imageFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
}))
query.handleCriterion(ctx, stringCriterionHandler(imageFilter.Title, "images.title"))
query.handleCriterion(ctx, stringCriterionHandler(imageFilter.Code, "images.code"))
query.handleCriterion(ctx, stringCriterionHandler(imageFilter.Details, "images.details"))
query.handleCriterion(ctx, stringCriterionHandler(imageFilter.Photographer, "images.photographer"))
query.handleCriterion(ctx, pathCriterionHandler(imageFilter.Path, "folders.path", "files.basename", qb.addFoldersTable))
query.handleCriterion(ctx, imageFileCountCriterionHandler(qb, imageFilter.FileCount))
query.handleCriterion(ctx, intCriterionHandler(imageFilter.Rating100, "images.rating", nil))
query.handleCriterion(ctx, intCriterionHandler(imageFilter.OCounter, "images.o_counter", nil))
query.handleCriterion(ctx, boolCriterionHandler(imageFilter.Organized, "images.organized", nil))
query.handleCriterion(ctx, dateCriterionHandler(imageFilter.Date, "images.date"))
query.handleCriterion(ctx, imageURLsCriterionHandler(imageFilter.URL))
query.handleCriterion(ctx, resolutionCriterionHandler(imageFilter.Resolution, "image_files.height", "image_files.width", qb.addImageFilesTable))
query.handleCriterion(ctx, orientationCriterionHandler(imageFilter.Orientation, "image_files.height", "image_files.width", qb.addImageFilesTable))
query.handleCriterion(ctx, imageIsMissingCriterionHandler(qb, imageFilter.IsMissing))
query.handleCriterion(ctx, imageTagsCriterionHandler(qb, imageFilter.Tags))
query.handleCriterion(ctx, imageTagCountCriterionHandler(qb, imageFilter.TagCount))
query.handleCriterion(ctx, imageGalleriesCriterionHandler(qb, imageFilter.Galleries))
query.handleCriterion(ctx, imagePerformersCriterionHandler(qb, imageFilter.Performers))
query.handleCriterion(ctx, imagePerformerCountCriterionHandler(qb, imageFilter.PerformerCount))
query.handleCriterion(ctx, studioCriterionHandler(imageTable, imageFilter.Studios))
query.handleCriterion(ctx, imagePerformerTagsCriterionHandler(qb, imageFilter.PerformerTags))
query.handleCriterion(ctx, imagePerformerFavoriteCriterionHandler(imageFilter.PerformerFavorite))
query.handleCriterion(ctx, imagePerformerAgeCriterionHandler(imageFilter.PerformerAge))
query.handleCriterion(ctx, timestampCriterionHandler(imageFilter.CreatedAt, "images.created_at"))
query.handleCriterion(ctx, timestampCriterionHandler(imageFilter.UpdatedAt, "images.updated_at"))
return query
}
func (qb *ImageStore) addImagesFilesTable(f *filterBuilder) {
f.addLeftJoin(imagesFilesTable, "", "images_files.image_id = images.id")
}
func (qb *ImageStore) addFilesTable(f *filterBuilder) {
qb.addImagesFilesTable(f)
f.addLeftJoin(fileTable, "", "images_files.file_id = files.id")
}
func (qb *ImageStore) addFoldersTable(f *filterBuilder) {
qb.addFilesTable(f)
f.addLeftJoin(folderTable, "", "files.parent_folder_id = folders.id")
}
func (qb *ImageStore) addImageFilesTable(f *filterBuilder) {
qb.addImagesFilesTable(f)
f.addLeftJoin(imageFileTable, "", "image_files.file_id = images_files.file_id")
}
func (qb *ImageStore) makeQuery(ctx context.Context, imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
if imageFilter == nil {
imageFilter = &models.ImageFilterType{}
@@ -754,7 +713,7 @@ func (qb *ImageStore) makeQuery(ctx context.Context, imageFilter *models.ImageFi
findFilter = &models.FindFilterType{}
}
query := qb.newQuery()
query := imageRepository.newQuery()
distinctIDs(&query, imageTable)
if q := findFilter.Q; q != nil && *q != "" {
@@ -782,10 +741,9 @@ func (qb *ImageStore) makeQuery(ctx context.Context, imageFilter *models.ImageFi
query.parseQueryString(searchColumns, *q)
}
if err := qb.validateFilter(imageFilter); err != nil {
return nil, err
}
filter := qb.makeFilter(ctx, imageFilter)
filter := filterBuilderFromHandler(ctx, &imageFilterHandler{
imageFilter: imageFilter,
})
if err := query.addFilter(filter); err != nil {
return nil, err
@@ -824,7 +782,7 @@ func (qb *ImageStore) queryGroupedFields(ctx context.Context, options models.Ima
return models.NewImageQueryResult(qb), nil
}
aggregateQuery := qb.newQuery()
aggregateQuery := imageRepository.newQuery()
if options.Count {
aggregateQuery.addColumn("COUNT(DISTINCT temp.id) as total")
@@ -868,7 +826,7 @@ func (qb *ImageStore) queryGroupedFields(ctx context.Context, options models.Ima
Megapixels null.Float
Size null.Float
}{}
if err := qb.repository.queryStruct(ctx, aggregateQuery.toSQL(includeSortPagination), query.args, &out); err != nil {
if err := imageRepository.queryStruct(ctx, aggregateQuery.toSQL(includeSortPagination), query.args, &out); err != nil {
return nil, err
}
@@ -888,171 +846,6 @@ func (qb *ImageStore) QueryCount(ctx context.Context, imageFilter *models.ImageF
return query.executeCount(ctx)
}
func imageFileCountCriterionHandler(qb *ImageStore, fileCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: imageTable,
joinTable: imagesFilesTable,
primaryFK: imageIDColumn,
}
return h.handler(fileCount)
}
func imageIsMissingCriterionHandler(qb *ImageStore, isMissing *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if isMissing != nil && *isMissing != "" {
switch *isMissing {
case "studio":
f.addWhere("images.studio_id IS NULL")
case "performers":
qb.performersRepository().join(f, "performers_join", "images.id")
f.addWhere("performers_join.image_id IS NULL")
case "galleries":
qb.galleriesRepository().join(f, "galleries_join", "images.id")
f.addWhere("galleries_join.image_id IS NULL")
case "tags":
qb.tagsRepository().join(f, "tags_join", "images.id")
f.addWhere("tags_join.image_id IS NULL")
default:
f.addWhere("(images." + *isMissing + " IS NULL OR TRIM(images." + *isMissing + ") = '')")
}
}
}
}
func imageURLsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{
joinTable: imagesURLsTable,
stringColumn: imageURLColumn,
addJoinTable: func(f *filterBuilder) {
imagesURLsTableMgr.join(f, "", "images.id")
},
}
return h.handler(url)
}
func (qb *ImageStore) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
return multiCriterionHandlerBuilder{
primaryTable: imageTable,
foreignTable: foreignTable,
joinTable: joinTable,
primaryFK: imageIDColumn,
foreignFK: foreignFK,
addJoinsFunc: addJoinsFunc,
}
}
func imageTagsCriterionHandler(qb *ImageStore, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
h := joinedHierarchicalMultiCriterionHandlerBuilder{
tx: qb.tx,
primaryTable: imageTable,
foreignTable: tagTable,
foreignFK: "tag_id",
relationsTable: "tags_relations",
joinAs: "image_tag",
joinTable: imagesTagsTable,
primaryFK: imageIDColumn,
}
return h.handler(tags)
}
func imageTagCountCriterionHandler(qb *ImageStore, tagCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: imageTable,
joinTable: imagesTagsTable,
primaryFK: imageIDColumn,
}
return h.handler(tagCount)
}
func imageGalleriesCriterionHandler(qb *ImageStore, galleries *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) {
if galleries.Modifier == models.CriterionModifierIncludes || galleries.Modifier == models.CriterionModifierIncludesAll {
f.addInnerJoin(galleriesImagesTable, "", "galleries_images.image_id = images.id")
f.addInnerJoin(galleryTable, "", "galleries_images.gallery_id = galleries.id")
}
}
h := qb.getMultiCriterionHandlerBuilder(galleryTable, galleriesImagesTable, galleryIDColumn, addJoinsFunc)
return h.handler(galleries)
}
func imagePerformersCriterionHandler(qb *ImageStore, performers *models.MultiCriterionInput) criterionHandlerFunc {
h := joinedMultiCriterionHandlerBuilder{
primaryTable: imageTable,
joinTable: performersImagesTable,
joinAs: "performers_join",
primaryFK: imageIDColumn,
foreignFK: performerIDColumn,
addJoinTable: func(f *filterBuilder) {
qb.performersRepository().join(f, "performers_join", "images.id")
},
}
return h.handler(performers)
}
func imagePerformerCountCriterionHandler(qb *ImageStore, performerCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: imageTable,
joinTable: performersImagesTable,
primaryFK: imageIDColumn,
}
return h.handler(performerCount)
}
func imagePerformerFavoriteCriterionHandler(performerfavorite *bool) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if performerfavorite != nil {
f.addLeftJoin("performers_images", "", "images.id = performers_images.image_id")
if *performerfavorite {
// contains at least one favorite
f.addLeftJoin("performers", "", "performers.id = performers_images.performer_id")
f.addWhere("performers.favorite = 1")
} else {
// contains zero favorites
f.addLeftJoin(`(SELECT performers_images.image_id as id FROM performers_images
JOIN performers ON performers.id = performers_images.performer_id
GROUP BY performers_images.image_id HAVING SUM(performers.favorite) = 0)`, "nofaves", "images.id = nofaves.id")
f.addWhere("performers_images.image_id IS NULL OR nofaves.id IS NOT NULL")
}
}
}
}
func imagePerformerAgeCriterionHandler(performerAge *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if performerAge != nil {
f.addInnerJoin("performers_images", "", "images.id = performers_images.image_id")
f.addInnerJoin("performers", "", "performers_images.performer_id = performers.id")
f.addWhere("images.date != '' AND performers.birthdate != ''")
f.addWhere("images.date IS NOT NULL AND performers.birthdate IS NOT NULL")
ageCalc := "cast(strftime('%Y.%m%d', images.date) - strftime('%Y.%m%d', performers.birthdate) as int)"
whereClause, args := getIntWhereClause(ageCalc, performerAge.Modifier, performerAge.Value, performerAge.Value2)
f.addWhere(whereClause, args...)
}
}
}
func imagePerformerTagsCriterionHandler(qb *ImageStore, tags *models.HierarchicalMultiCriterionInput) criterionHandler {
return &joinedPerformerTagsHandler{
criterion: tags,
primaryTable: imageTable,
joinTable: performersImagesTable,
joinPrimaryKey: imageIDColumn,
}
}
var imageSortOptions = sortOptions{
"created_at",
"date",
@@ -1138,34 +931,13 @@ func (qb *ImageStore) setImageSortAndPagination(q *queryBuilder, findFilter *mod
return nil
}
func (qb *ImageStore) galleriesRepository() *joinRepository {
return &joinRepository{
repository: repository{
tx: qb.tx,
tableName: galleriesImagesTable,
idColumn: imageIDColumn,
},
fkColumn: galleryIDColumn,
}
}
func (qb *ImageStore) filesRepository() *filesRepository {
return &filesRepository{
repository: repository{
tx: qb.tx,
tableName: imagesFilesTable,
idColumn: imageIDColumn,
},
}
}
func (qb *ImageStore) AddFileID(ctx context.Context, id int, fileID models.FileID) error {
const firstPrimary = false
return imagesFilesTableMgr.insertJoins(ctx, id, firstPrimary, []models.FileID{fileID})
}
func (qb *ImageStore) GetGalleryIDs(ctx context.Context, imageID int) ([]int, error) {
return qb.galleriesRepository().getIDs(ctx, imageID)
return imageRepository.galleries.getIDs(ctx, imageID)
}
// func (qb *imageQueryBuilder) UpdateGalleries(ctx context.Context, imageID int, galleryIDs []int) error {
@@ -1173,46 +945,22 @@ func (qb *ImageStore) GetGalleryIDs(ctx context.Context, imageID int) ([]int, er
// return qb.galleriesRepository().replace(ctx, imageID, galleryIDs)
// }
func (qb *ImageStore) performersRepository() *joinRepository {
return &joinRepository{
repository: repository{
tx: qb.tx,
tableName: performersImagesTable,
idColumn: imageIDColumn,
},
fkColumn: performerIDColumn,
}
}
func (qb *ImageStore) GetPerformerIDs(ctx context.Context, imageID int) ([]int, error) {
return qb.performersRepository().getIDs(ctx, imageID)
return imageRepository.performers.getIDs(ctx, imageID)
}
func (qb *ImageStore) UpdatePerformers(ctx context.Context, imageID int, performerIDs []int) error {
// Delete the existing joins and then create new ones
return qb.performersRepository().replace(ctx, imageID, performerIDs)
}
func (qb *ImageStore) tagsRepository() *joinRepository {
return &joinRepository{
repository: repository{
tx: qb.tx,
tableName: imagesTagsTable,
idColumn: imageIDColumn,
},
fkColumn: tagIDColumn,
foreignTable: tagTable,
orderBy: "tags.name ASC",
}
return imageRepository.performers.replace(ctx, imageID, performerIDs)
}
func (qb *ImageStore) GetTagIDs(ctx context.Context, imageID int) ([]int, error) {
return qb.tagsRepository().getIDs(ctx, imageID)
return imageRepository.tags.getIDs(ctx, imageID)
}
func (qb *ImageStore) UpdateTags(ctx context.Context, imageID int, tagIDs []int) error {
// Delete the existing joins and then create new ones
return qb.tagsRepository().replace(ctx, imageID, tagIDs)
return imageRepository.tags.replace(ctx, imageID, tagIDs)
}
func (qb *ImageStore) GetURLs(ctx context.Context, imageID int) ([]string, error) {

290
pkg/sqlite/image_filter.go Normal file
View File

@@ -0,0 +1,290 @@
package sqlite
import (
"context"
"github.com/stashapp/stash/pkg/models"
)
type imageFilterHandler struct {
imageFilter *models.ImageFilterType
}
func (qb *imageFilterHandler) validate() error {
imageFilter := qb.imageFilter
if imageFilter == nil {
return nil
}
if err := validateFilterCombination(imageFilter.OperatorFilter); err != nil {
return err
}
if subFilter := imageFilter.SubFilter(); subFilter != nil {
sqb := &imageFilterHandler{imageFilter: subFilter}
if err := sqb.validate(); err != nil {
return err
}
}
return nil
}
func (qb *imageFilterHandler) handle(ctx context.Context, f *filterBuilder) {
imageFilter := qb.imageFilter
if imageFilter == nil {
return
}
if err := qb.validate(); err != nil {
f.setError(err)
return
}
sf := imageFilter.SubFilter()
if sf != nil {
sub := &imageFilterHandler{sf}
handleSubFilter(ctx, sub, f, imageFilter.OperatorFilter)
}
f.handleCriterion(ctx, qb.criterionHandler())
}
func (qb *imageFilterHandler) criterionHandler() criterionHandler {
imageFilter := qb.imageFilter
return compoundHandler{
intCriterionHandler(imageFilter.ID, "images.id", nil),
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if imageFilter.Checksum != nil {
imageRepository.addImagesFilesTable(f)
f.addInnerJoin(fingerprintTable, "fingerprints_md5", "images_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'")
}
stringCriterionHandler(imageFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
}),
stringCriterionHandler(imageFilter.Title, "images.title"),
stringCriterionHandler(imageFilter.Code, "images.code"),
stringCriterionHandler(imageFilter.Details, "images.details"),
stringCriterionHandler(imageFilter.Photographer, "images.photographer"),
pathCriterionHandler(imageFilter.Path, "folders.path", "files.basename", imageRepository.addFoldersTable),
qb.fileCountCriterionHandler(imageFilter.FileCount),
intCriterionHandler(imageFilter.Rating100, "images.rating", nil),
intCriterionHandler(imageFilter.OCounter, "images.o_counter", nil),
boolCriterionHandler(imageFilter.Organized, "images.organized", nil),
&dateCriterionHandler{imageFilter.Date, "images.date", nil},
qb.urlsCriterionHandler(imageFilter.URL),
resolutionCriterionHandler(imageFilter.Resolution, "image_files.height", "image_files.width", imageRepository.addImageFilesTable),
orientationCriterionHandler(imageFilter.Orientation, "image_files.height", "image_files.width", imageRepository.addImageFilesTable),
qb.missingCriterionHandler(imageFilter.IsMissing),
qb.tagsCriterionHandler(imageFilter.Tags),
qb.tagCountCriterionHandler(imageFilter.TagCount),
qb.galleriesCriterionHandler(imageFilter.Galleries),
qb.performersCriterionHandler(imageFilter.Performers),
qb.performerCountCriterionHandler(imageFilter.PerformerCount),
studioCriterionHandler(imageTable, imageFilter.Studios),
qb.performerTagsCriterionHandler(imageFilter.PerformerTags),
qb.performerFavoriteCriterionHandler(imageFilter.PerformerFavorite),
qb.performerAgeCriterionHandler(imageFilter.PerformerAge),
&timestampCriterionHandler{imageFilter.CreatedAt, "images.created_at", nil},
&timestampCriterionHandler{imageFilter.UpdatedAt, "images.updated_at", nil},
&relatedFilterHandler{
relatedIDCol: "galleries_images.gallery_id",
relatedRepo: galleryRepository.repository,
relatedHandler: &galleryFilterHandler{imageFilter.GalleriesFilter},
joinFn: func(f *filterBuilder) {
imageRepository.galleries.innerJoin(f, "", "images.id")
},
},
&relatedFilterHandler{
relatedIDCol: "performers_join.performer_id",
relatedRepo: performerRepository.repository,
relatedHandler: &performerFilterHandler{imageFilter.PerformersFilter},
joinFn: func(f *filterBuilder) {
imageRepository.performers.innerJoin(f, "performers_join", "images.id")
},
},
&relatedFilterHandler{
relatedIDCol: "images.studio_id",
relatedRepo: studioRepository.repository,
relatedHandler: &studioFilterHandler{imageFilter.StudiosFilter},
},
&relatedFilterHandler{
relatedIDCol: "image_tag.tag_id",
relatedRepo: tagRepository.repository,
relatedHandler: &tagFilterHandler{imageFilter.TagsFilter},
joinFn: func(f *filterBuilder) {
imageRepository.tags.innerJoin(f, "image_tag", "images.id")
},
},
}
}
func (qb *imageFilterHandler) fileCountCriterionHandler(fileCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: imageTable,
joinTable: imagesFilesTable,
primaryFK: imageIDColumn,
}
return h.handler(fileCount)
}
func (qb *imageFilterHandler) missingCriterionHandler(isMissing *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if isMissing != nil && *isMissing != "" {
switch *isMissing {
case "studio":
f.addWhere("images.studio_id IS NULL")
case "performers":
imageRepository.performers.join(f, "performers_join", "images.id")
f.addWhere("performers_join.image_id IS NULL")
case "galleries":
imageRepository.galleries.join(f, "galleries_join", "images.id")
f.addWhere("galleries_join.image_id IS NULL")
case "tags":
imageRepository.tags.join(f, "tags_join", "images.id")
f.addWhere("tags_join.image_id IS NULL")
default:
f.addWhere("(images." + *isMissing + " IS NULL OR TRIM(images." + *isMissing + ") = '')")
}
}
}
}
func (qb *imageFilterHandler) urlsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{
joinTable: imagesURLsTable,
stringColumn: imageURLColumn,
addJoinTable: func(f *filterBuilder) {
imagesURLsTableMgr.join(f, "", "images.id")
},
}
return h.handler(url)
}
func (qb *imageFilterHandler) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
return multiCriterionHandlerBuilder{
primaryTable: imageTable,
foreignTable: foreignTable,
joinTable: joinTable,
primaryFK: imageIDColumn,
foreignFK: foreignFK,
addJoinsFunc: addJoinsFunc,
}
}
func (qb *imageFilterHandler) tagsCriterionHandler(tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
h := joinedHierarchicalMultiCriterionHandlerBuilder{
primaryTable: imageTable,
foreignTable: tagTable,
foreignFK: "tag_id",
relationsTable: "tags_relations",
joinAs: "image_tag",
joinTable: imagesTagsTable,
primaryFK: imageIDColumn,
}
return h.handler(tags)
}
func (qb *imageFilterHandler) tagCountCriterionHandler(tagCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: imageTable,
joinTable: imagesTagsTable,
primaryFK: imageIDColumn,
}
return h.handler(tagCount)
}
func (qb *imageFilterHandler) galleriesCriterionHandler(galleries *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) {
if galleries.Modifier == models.CriterionModifierIncludes || galleries.Modifier == models.CriterionModifierIncludesAll {
f.addInnerJoin(galleriesImagesTable, "", "galleries_images.image_id = images.id")
f.addInnerJoin(galleryTable, "", "galleries_images.gallery_id = galleries.id")
}
}
h := qb.getMultiCriterionHandlerBuilder(galleryTable, galleriesImagesTable, galleryIDColumn, addJoinsFunc)
return h.handler(galleries)
}
func (qb *imageFilterHandler) performersCriterionHandler(performers *models.MultiCriterionInput) criterionHandlerFunc {
h := joinedMultiCriterionHandlerBuilder{
primaryTable: imageTable,
joinTable: performersImagesTable,
joinAs: "performers_join",
primaryFK: imageIDColumn,
foreignFK: performerIDColumn,
addJoinTable: func(f *filterBuilder) {
imageRepository.performers.join(f, "performers_join", "images.id")
},
}
return h.handler(performers)
}
func (qb *imageFilterHandler) performerCountCriterionHandler(performerCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: imageTable,
joinTable: performersImagesTable,
primaryFK: imageIDColumn,
}
return h.handler(performerCount)
}
func (qb *imageFilterHandler) performerFavoriteCriterionHandler(performerfavorite *bool) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if performerfavorite != nil {
f.addLeftJoin("performers_images", "", "images.id = performers_images.image_id")
if *performerfavorite {
// contains at least one favorite
f.addLeftJoin("performers", "", "performers.id = performers_images.performer_id")
f.addWhere("performers.favorite = 1")
} else {
// contains zero favorites
f.addLeftJoin(`(SELECT performers_images.image_id as id FROM performers_images
JOIN performers ON performers.id = performers_images.performer_id
GROUP BY performers_images.image_id HAVING SUM(performers.favorite) = 0)`, "nofaves", "images.id = nofaves.id")
f.addWhere("performers_images.image_id IS NULL OR nofaves.id IS NOT NULL")
}
}
}
}
func (qb *imageFilterHandler) performerAgeCriterionHandler(performerAge *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if performerAge != nil {
f.addInnerJoin("performers_images", "", "images.id = performers_images.image_id")
f.addInnerJoin("performers", "", "performers_images.performer_id = performers.id")
f.addWhere("images.date != '' AND performers.birthdate != ''")
f.addWhere("images.date IS NOT NULL AND performers.birthdate IS NOT NULL")
ageCalc := "cast(strftime('%Y.%m%d', images.date) - strftime('%Y.%m%d', performers.birthdate) as int)"
whereClause, args := getIntWhereClause(ageCalc, performerAge.Modifier, performerAge.Value, performerAge.Value2)
f.addWhere(whereClause, args...)
}
}
}
func (qb *imageFilterHandler) performerTagsCriterionHandler(tags *models.HierarchicalMultiCriterionInput) criterionHandler {
return &joinedPerformerTagsHandler{
criterion: tags,
primaryTable: imageTable,
joinTable: performersImagesTable,
joinPrimaryKey: imageIDColumn,
}
}

View File

@@ -1668,10 +1668,12 @@ func TestImageQueryPathOr(t *testing.T) {
Value: image1Path,
Modifier: models.CriterionModifierEquals,
},
Or: &models.ImageFilterType{
Path: &models.StringCriterionInput{
Value: image2Path,
Modifier: models.CriterionModifierEquals,
OperatorFilter: models.OperatorFilter[models.ImageFilterType]{
Or: &models.ImageFilterType{
Path: &models.StringCriterionInput{
Value: image2Path,
Modifier: models.CriterionModifierEquals,
},
},
},
}
@@ -1702,10 +1704,12 @@ func TestImageQueryPathAndRating(t *testing.T) {
Value: imagePath,
Modifier: models.CriterionModifierEquals,
},
And: &models.ImageFilterType{
Rating100: &models.IntCriterionInput{
Value: int(imageRating.Int64),
Modifier: models.CriterionModifierEquals,
OperatorFilter: models.OperatorFilter[models.ImageFilterType]{
And: &models.ImageFilterType{
Rating100: &models.IntCriterionInput{
Value: int(imageRating.Int64),
Modifier: models.CriterionModifierEquals,
},
},
},
}
@@ -1743,8 +1747,10 @@ func TestImageQueryPathNotRating(t *testing.T) {
imageFilter := models.ImageFilterType{
Path: &pathCriterion,
Not: &models.ImageFilterType{
Rating100: &ratingCriterion,
OperatorFilter: models.OperatorFilter[models.ImageFilterType]{
Not: &models.ImageFilterType{
Rating100: &ratingCriterion,
},
},
}
@@ -1775,8 +1781,10 @@ func TestImageIllegalQuery(t *testing.T) {
}
imageFilter := &models.ImageFilterType{
And: &subFilter,
Or: &subFilter,
OperatorFilter: models.OperatorFilter[models.ImageFilterType]{
And: &subFilter,
Or: &subFilter,
},
}
withTxn(func(ctx context.Context) error {

View File

@@ -96,8 +96,25 @@ func (r *movieRowRecord) fromPartial(o models.MoviePartial) {
r.setTimestamp("updated_at", o.UpdatedAt)
}
type MovieStore struct {
type movieRepositoryType struct {
repository
scenes repository
}
var (
movieRepository = movieRepositoryType{
repository: repository{
tableName: movieTable,
idColumn: idColumn,
},
scenes: repository{
tableName: moviesScenesTable,
idColumn: movieIDColumn,
},
}
)
type MovieStore struct {
blobJoinQueryBuilder
tableMgr *table
@@ -105,10 +122,6 @@ type MovieStore struct {
func NewMovieStore(blobStore *BlobStore) *MovieStore {
return &MovieStore{
repository: repository{
tableName: movieTable,
idColumn: idColumn,
},
blobJoinQueryBuilder: blobJoinQueryBuilder{
blobStore: blobStore,
joinTable: movieTable,
@@ -180,7 +193,7 @@ func (qb *MovieStore) Destroy(ctx context.Context, id int) error {
return err
}
return qb.destroyExisting(ctx, []int{id})
return movieRepository.destroyExisting(ctx, []int{id})
}
// returns nil, nil if not found
@@ -327,25 +340,6 @@ func (qb *MovieStore) All(ctx context.Context) ([]*models.Movie, error) {
))
}
func (qb *MovieStore) makeFilter(ctx context.Context, movieFilter *models.MovieFilterType) *filterBuilder {
query := &filterBuilder{}
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Name, "movies.name"))
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Director, "movies.director"))
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Synopsis, "movies.synopsis"))
query.handleCriterion(ctx, intCriterionHandler(movieFilter.Rating100, "movies.rating", nil))
query.handleCriterion(ctx, floatIntCriterionHandler(movieFilter.Duration, "movies.duration", nil))
query.handleCriterion(ctx, movieIsMissingCriterionHandler(qb, movieFilter.IsMissing))
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.URL, "movies.url"))
query.handleCriterion(ctx, studioCriterionHandler(movieTable, movieFilter.Studios))
query.handleCriterion(ctx, moviePerformersCriterionHandler(qb, movieFilter.Performers))
query.handleCriterion(ctx, dateCriterionHandler(movieFilter.Date, "movies.date"))
query.handleCriterion(ctx, timestampCriterionHandler(movieFilter.CreatedAt, "movies.created_at"))
query.handleCriterion(ctx, timestampCriterionHandler(movieFilter.UpdatedAt, "movies.updated_at"))
return query
}
func (qb *MovieStore) makeQuery(ctx context.Context, movieFilter *models.MovieFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
if findFilter == nil {
findFilter = &models.FindFilterType{}
@@ -354,7 +348,7 @@ func (qb *MovieStore) makeQuery(ctx context.Context, movieFilter *models.MovieFi
movieFilter = &models.MovieFilterType{}
}
query := qb.newQuery()
query := movieRepository.newQuery()
distinctIDs(&query, movieTable)
if q := findFilter.Q; q != nil && *q != "" {
@@ -362,7 +356,9 @@ func (qb *MovieStore) makeQuery(ctx context.Context, movieFilter *models.MovieFi
query.parseQueryString(searchColumns, *q)
}
filter := qb.makeFilter(ctx, movieFilter)
filter := filterBuilderFromHandler(ctx, &movieFilterHandler{
movieFilter: movieFilter,
})
if err := query.addFilter(filter); err != nil {
return nil, err
@@ -407,71 +403,6 @@ func (qb *MovieStore) QueryCount(ctx context.Context, movieFilter *models.MovieF
return query.executeCount(ctx)
}
func movieIsMissingCriterionHandler(qb *MovieStore, isMissing *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if isMissing != nil && *isMissing != "" {
switch *isMissing {
case "front_image":
f.addWhere("movies.front_image_blob IS NULL")
case "back_image":
f.addWhere("movies.back_image_blob IS NULL")
case "scenes":
f.addLeftJoin("movies_scenes", "", "movies_scenes.movie_id = movies.id")
f.addWhere("movies_scenes.scene_id IS NULL")
default:
f.addWhere("(movies." + *isMissing + " IS NULL OR TRIM(movies." + *isMissing + ") = '')")
}
}
}
}
func moviePerformersCriterionHandler(qb *MovieStore, performers *models.MultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if performers != nil {
if performers.Modifier == models.CriterionModifierIsNull || performers.Modifier == models.CriterionModifierNotNull {
var notClause string
if performers.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addLeftJoin("movies_scenes", "", "movies.id = movies_scenes.movie_id")
f.addLeftJoin("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)
}
// Hack, can't apply args to join, nor inner join on a left join, so use CTE instead
f.addWith(`movies_performers AS (
SELECT movies_scenes.movie_id, performers_scenes.performer_id
FROM movies_scenes
INNER JOIN performers_scenes ON movies_scenes.scene_id = performers_scenes.scene_id
WHERE performers_scenes.performer_id IN`+getInBinding(len(performers.Value))+`
)`, args...)
f.addLeftJoin("movies_performers", "", "movies.id = movies_performers.movie_id")
switch performers.Modifier {
case models.CriterionModifierIncludes:
f.addWhere("movies_performers.performer_id IS NOT NULL")
case models.CriterionModifierIncludesAll:
f.addWhere("movies_performers.performer_id IS NOT NULL")
f.addHaving("COUNT(DISTINCT movies_performers.performer_id) = ?", len(performers.Value))
case models.CriterionModifierExcludes:
f.addWhere("movies_performers.performer_id IS NULL")
}
}
}
}
var movieSortOptions = sortOptions{
"created_at",
"date",
@@ -516,7 +447,7 @@ func (qb *MovieStore) getMovieSort(findFilter *models.FindFilterType) (string, e
func (qb *MovieStore) queryMovies(ctx context.Context, query string, args []interface{}) ([]*models.Movie, error) {
const single = false
var ret []*models.Movie
if err := qb.queryFunc(ctx, query, args, single, func(r *sqlx.Rows) error {
if err := movieRepository.queryFunc(ctx, query, args, single, func(r *sqlx.Rows) error {
var f movieRow
if err := r.StructScan(&f); err != nil {
return err
@@ -586,7 +517,7 @@ INNER JOIN performers_scenes ON performers_scenes.scene_id = movies_scenes.scene
WHERE performers_scenes.performer_id = ?
`
args := []interface{}{performerID}
return qb.runCountQuery(ctx, query, args)
return movieRepository.runCountQuery(ctx, query, args)
}
func (qb *MovieStore) FindByStudioID(ctx context.Context, studioID int) ([]*models.Movie, error) {
@@ -604,5 +535,5 @@ FROM movies
WHERE movies.studio_id = ?
`
args := []interface{}{studioID}
return qb.runCountQuery(ctx, query, args)
return movieRepository.runCountQuery(ctx, query, args)
}

150
pkg/sqlite/movies_filter.go Normal file
View File

@@ -0,0 +1,150 @@
package sqlite
import (
"context"
"fmt"
"github.com/stashapp/stash/pkg/models"
)
type movieFilterHandler struct {
movieFilter *models.MovieFilterType
}
func (qb *movieFilterHandler) validate() error {
movieFilter := qb.movieFilter
if movieFilter == nil {
return nil
}
if err := validateFilterCombination(movieFilter.OperatorFilter); err != nil {
return err
}
if subFilter := movieFilter.SubFilter(); subFilter != nil {
sqb := &movieFilterHandler{movieFilter: subFilter}
if err := sqb.validate(); err != nil {
return err
}
}
return nil
}
func (qb *movieFilterHandler) handle(ctx context.Context, f *filterBuilder) {
movieFilter := qb.movieFilter
if movieFilter == nil {
return
}
if err := qb.validate(); err != nil {
f.setError(err)
return
}
sf := movieFilter.SubFilter()
if sf != nil {
sub := &movieFilterHandler{sf}
handleSubFilter(ctx, sub, f, movieFilter.OperatorFilter)
}
f.handleCriterion(ctx, qb.criterionHandler())
}
func (qb *movieFilterHandler) criterionHandler() criterionHandler {
movieFilter := qb.movieFilter
return compoundHandler{
stringCriterionHandler(movieFilter.Name, "movies.name"),
stringCriterionHandler(movieFilter.Director, "movies.director"),
stringCriterionHandler(movieFilter.Synopsis, "movies.synopsis"),
intCriterionHandler(movieFilter.Rating100, "movies.rating", nil),
floatIntCriterionHandler(movieFilter.Duration, "movies.duration", nil),
qb.missingCriterionHandler(movieFilter.IsMissing),
stringCriterionHandler(movieFilter.URL, "movies.url"),
studioCriterionHandler(movieTable, movieFilter.Studios),
qb.performersCriterionHandler(movieFilter.Performers),
&dateCriterionHandler{movieFilter.Date, "movies.date", nil},
&timestampCriterionHandler{movieFilter.CreatedAt, "movies.created_at", nil},
&timestampCriterionHandler{movieFilter.UpdatedAt, "movies.updated_at", nil},
&relatedFilterHandler{
relatedIDCol: "movies_scenes.scene_id",
relatedRepo: sceneRepository.repository,
relatedHandler: &sceneFilterHandler{movieFilter.ScenesFilter},
joinFn: func(f *filterBuilder) {
movieRepository.scenes.innerJoin(f, "", "movies.id")
},
},
&relatedFilterHandler{
relatedIDCol: "movies.studio_id",
relatedRepo: studioRepository.repository,
relatedHandler: &studioFilterHandler{movieFilter.StudiosFilter},
},
}
}
func (qb *movieFilterHandler) missingCriterionHandler(isMissing *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if isMissing != nil && *isMissing != "" {
switch *isMissing {
case "front_image":
f.addWhere("movies.front_image_blob IS NULL")
case "back_image":
f.addWhere("movies.back_image_blob IS NULL")
case "scenes":
f.addLeftJoin("movies_scenes", "", "movies_scenes.movie_id = movies.id")
f.addWhere("movies_scenes.scene_id IS NULL")
default:
f.addWhere("(movies." + *isMissing + " IS NULL OR TRIM(movies." + *isMissing + ") = '')")
}
}
}
}
func (qb *movieFilterHandler) performersCriterionHandler(performers *models.MultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if performers != nil {
if performers.Modifier == models.CriterionModifierIsNull || performers.Modifier == models.CriterionModifierNotNull {
var notClause string
if performers.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addLeftJoin("movies_scenes", "", "movies.id = movies_scenes.movie_id")
f.addLeftJoin("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)
}
// Hack, can't apply args to join, nor inner join on a left join, so use CTE instead
f.addWith(`movies_performers AS (
SELECT movies_scenes.movie_id, performers_scenes.performer_id
FROM movies_scenes
INNER JOIN performers_scenes ON movies_scenes.scene_id = performers_scenes.scene_id
WHERE performers_scenes.performer_id IN`+getInBinding(len(performers.Value))+`
)`, args...)
f.addLeftJoin("movies_performers", "", "movies.id = movies_performers.movie_id")
switch performers.Modifier {
case models.CriterionModifierIncludes:
f.addWhere("movies_performers.performer_id IS NOT NULL")
case models.CriterionModifierIncludesAll:
f.addWhere("movies_performers.performer_id IS NOT NULL")
f.addHaving("COUNT(DISTINCT movies_performers.performer_id) = ?", len(performers.Value))
case models.CriterionModifierExcludes:
f.addWhere("movies_performers.performer_id IS NULL")
}
}
}
}

View File

@@ -5,8 +5,6 @@ import (
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
"github.com/doug-martin/goqu/v9"
"github.com/doug-martin/goqu/v9/exp"
@@ -176,8 +174,66 @@ func (r *performerRowRecord) fromPartial(o models.PerformerPartial) {
r.setBool("ignore_auto_tag", o.IgnoreAutoTag)
}
type PerformerStore struct {
type performerRepositoryType struct {
repository
tags joinRepository
stashIDs stashIDRepository
scenes joinRepository
images joinRepository
galleries joinRepository
}
var (
performerRepository = performerRepositoryType{
repository: repository{
tableName: performerTable,
idColumn: idColumn,
},
tags: joinRepository{
repository: repository{
tableName: performersTagsTable,
idColumn: performerIDColumn,
},
fkColumn: tagIDColumn,
foreignTable: tagTable,
orderBy: "tags.name ASC",
},
stashIDs: stashIDRepository{
repository{
tableName: "performer_stash_ids",
idColumn: performerIDColumn,
},
},
scenes: joinRepository{
repository: repository{
tableName: performersScenesTable,
idColumn: performerIDColumn,
},
fkColumn: sceneIDColumn,
foreignTable: sceneTable,
},
images: joinRepository{
repository: repository{
tableName: performersImagesTable,
idColumn: performerIDColumn,
},
fkColumn: imageIDColumn,
foreignTable: imageTable,
},
galleries: joinRepository{
repository: repository{
tableName: performersGalleriesTable,
idColumn: performerIDColumn,
},
fkColumn: galleryIDColumn,
foreignTable: galleryTable,
},
}
)
type PerformerStore struct {
blobJoinQueryBuilder
tableMgr *table
@@ -185,10 +241,6 @@ type PerformerStore struct {
func NewPerformerStore(blobStore *BlobStore) *PerformerStore {
return &PerformerStore{
repository: repository{
tableName: performerTable,
idColumn: idColumn,
},
blobJoinQueryBuilder: blobJoinQueryBuilder{
blobStore: blobStore,
joinTable: performerTable,
@@ -312,7 +364,7 @@ func (qb *PerformerStore) Destroy(ctx context.Context, id int) error {
return err
}
return qb.destroyExisting(ctx, []int{id})
return performerRepository.destroyExisting(ctx, []int{id})
}
// returns nil, nil if not found
@@ -525,161 +577,6 @@ func (qb *PerformerStore) QueryForAutoTag(ctx context.Context, words []string) (
return ret, nil
}
func (qb *PerformerStore) validateFilter(filter *models.PerformerFilterType) error {
const and = "AND"
const or = "OR"
const not = "NOT"
if filter.And != nil {
if filter.Or != nil {
return illegalFilterCombination(and, or)
}
if filter.Not != nil {
return illegalFilterCombination(and, not)
}
return qb.validateFilter(filter.And)
}
if filter.Or != nil {
if filter.Not != nil {
return illegalFilterCombination(or, not)
}
return qb.validateFilter(filter.Or)
}
if filter.Not != nil {
return qb.validateFilter(filter.Not)
}
// if legacy height filter used, ensure only supported modifiers are used
if filter.Height != nil {
// treat as an int filter
intCrit := &models.IntCriterionInput{
Modifier: filter.Height.Modifier,
}
if !intCrit.ValidModifier() {
return fmt.Errorf("invalid height modifier: %s", filter.Height.Modifier)
}
// ensure value is a valid number
if _, err := strconv.Atoi(filter.Height.Value); err != nil {
return fmt.Errorf("invalid height value: %s", filter.Height.Value)
}
}
return nil
}
func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.PerformerFilterType) *filterBuilder {
query := &filterBuilder{}
if filter.And != nil {
query.and(qb.makeFilter(ctx, filter.And))
}
if filter.Or != nil {
query.or(qb.makeFilter(ctx, filter.Or))
}
if filter.Not != nil {
query.not(qb.makeFilter(ctx, filter.Not))
}
const tableName = performerTable
query.handleCriterion(ctx, stringCriterionHandler(filter.Name, tableName+".name"))
query.handleCriterion(ctx, stringCriterionHandler(filter.Disambiguation, tableName+".disambiguation"))
query.handleCriterion(ctx, stringCriterionHandler(filter.Details, tableName+".details"))
query.handleCriterion(ctx, boolCriterionHandler(filter.FilterFavorites, tableName+".favorite", nil))
query.handleCriterion(ctx, boolCriterionHandler(filter.IgnoreAutoTag, tableName+".ignore_auto_tag", nil))
query.handleCriterion(ctx, yearFilterCriterionHandler(filter.BirthYear, tableName+".birthdate"))
query.handleCriterion(ctx, yearFilterCriterionHandler(filter.DeathYear, tableName+".death_date"))
query.handleCriterion(ctx, performerAgeFilterCriterionHandler(filter.Age))
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if gender := filter.Gender; gender != nil {
genderCopy := *gender
if genderCopy.Value.IsValid() && len(genderCopy.ValueList) == 0 {
genderCopy.ValueList = []models.GenderEnum{genderCopy.Value}
}
v := utils.StringerSliceToStringSlice(genderCopy.ValueList)
enumCriterionHandler(genderCopy.Modifier, v, tableName+".gender")(ctx, f)
}
}))
query.handleCriterion(ctx, performerIsMissingCriterionHandler(qb, filter.IsMissing))
query.handleCriterion(ctx, stringCriterionHandler(filter.Ethnicity, tableName+".ethnicity"))
query.handleCriterion(ctx, stringCriterionHandler(filter.Country, tableName+".country"))
query.handleCriterion(ctx, stringCriterionHandler(filter.EyeColor, tableName+".eye_color"))
// special handler for legacy height filter
heightCmCrit := filter.HeightCm
if heightCmCrit == nil && filter.Height != nil {
heightCm, _ := strconv.Atoi(filter.Height.Value) // already validated
heightCmCrit = &models.IntCriterionInput{
Value: heightCm,
Modifier: filter.Height.Modifier,
}
}
query.handleCriterion(ctx, intCriterionHandler(heightCmCrit, tableName+".height", nil))
query.handleCriterion(ctx, stringCriterionHandler(filter.Measurements, tableName+".measurements"))
query.handleCriterion(ctx, stringCriterionHandler(filter.FakeTits, tableName+".fake_tits"))
query.handleCriterion(ctx, floatCriterionHandler(filter.PenisLength, tableName+".penis_length", nil))
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if circumcised := filter.Circumcised; circumcised != nil {
v := utils.StringerSliceToStringSlice(circumcised.Value)
enumCriterionHandler(circumcised.Modifier, v, tableName+".circumcised")(ctx, f)
}
}))
query.handleCriterion(ctx, stringCriterionHandler(filter.CareerLength, tableName+".career_length"))
query.handleCriterion(ctx, stringCriterionHandler(filter.Tattoos, tableName+".tattoos"))
query.handleCriterion(ctx, stringCriterionHandler(filter.Piercings, tableName+".piercings"))
query.handleCriterion(ctx, intCriterionHandler(filter.Rating100, tableName+".rating", nil))
query.handleCriterion(ctx, stringCriterionHandler(filter.HairColor, tableName+".hair_color"))
query.handleCriterion(ctx, stringCriterionHandler(filter.URL, tableName+".url"))
query.handleCriterion(ctx, intCriterionHandler(filter.Weight, tableName+".weight", nil))
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if filter.StashID != nil {
qb.stashIDRepository().join(f, "performer_stash_ids", "performers.id")
stringCriterionHandler(filter.StashID, "performer_stash_ids.stash_id")(ctx, f)
}
}))
query.handleCriterion(ctx, &stashIDCriterionHandler{
c: filter.StashIDEndpoint,
stashIDRepository: qb.stashIDRepository(),
stashIDTableAs: "performer_stash_ids",
parentIDCol: "performers.id",
})
query.handleCriterion(ctx, performerAliasCriterionHandler(qb, filter.Aliases))
query.handleCriterion(ctx, performerTagsCriterionHandler(qb, filter.Tags))
query.handleCriterion(ctx, performerStudiosCriterionHandler(qb, filter.Studios))
query.handleCriterion(ctx, performerAppearsWithCriterionHandler(qb, filter.Performers))
query.handleCriterion(ctx, performerTagCountCriterionHandler(qb, filter.TagCount))
query.handleCriterion(ctx, performerSceneCountCriterionHandler(qb, filter.SceneCount))
query.handleCriterion(ctx, performerImageCountCriterionHandler(qb, filter.ImageCount))
query.handleCriterion(ctx, performerGalleryCountCriterionHandler(qb, filter.GalleryCount))
query.handleCriterion(ctx, performerPlayCounterCriterionHandler(qb, filter.PlayCount))
query.handleCriterion(ctx, performerOCounterCriterionHandler(qb, filter.OCounter))
query.handleCriterion(ctx, dateCriterionHandler(filter.Birthdate, tableName+".birthdate"))
query.handleCriterion(ctx, dateCriterionHandler(filter.DeathDate, tableName+".death_date"))
query.handleCriterion(ctx, timestampCriterionHandler(filter.CreatedAt, tableName+".created_at"))
query.handleCriterion(ctx, timestampCriterionHandler(filter.UpdatedAt, tableName+".updated_at"))
return query
}
func (qb *PerformerStore) makeQuery(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
if performerFilter == nil {
performerFilter = &models.PerformerFilterType{}
@@ -688,7 +585,7 @@ func (qb *PerformerStore) makeQuery(ctx context.Context, performerFilter *models
findFilter = &models.FindFilterType{}
}
query := qb.newQuery()
query := performerRepository.newQuery()
distinctIDs(&query, performerTable)
if q := findFilter.Q; q != nil && *q != "" {
@@ -697,10 +594,9 @@ func (qb *PerformerStore) makeQuery(ctx context.Context, performerFilter *models
query.parseQueryString(searchColumns, *q)
}
if err := qb.validateFilter(performerFilter); err != nil {
return nil, err
}
filter := qb.makeFilter(ctx, performerFilter)
filter := filterBuilderFromHandler(ctx, &performerFilterHandler{
performerFilter: performerFilter,
})
if err := query.addFilter(filter); err != nil {
return nil, err
@@ -744,165 +640,16 @@ func (qb *PerformerStore) QueryCount(ctx context.Context, performerFilter *model
return query.executeCount(ctx)
}
// TODO - we need to provide a whitelist of possible values
func performerIsMissingCriterionHandler(qb *PerformerStore, isMissing *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if isMissing != nil && *isMissing != "" {
switch *isMissing {
case "scenes": // Deprecated: use `scene_count == 0` filter instead
f.addLeftJoin(performersScenesTable, "scenes_join", "scenes_join.performer_id = performers.id")
f.addWhere("scenes_join.scene_id IS NULL")
case "image":
f.addWhere("performers.image_blob IS NULL")
case "stash_id":
performersStashIDsTableMgr.join(f, "performer_stash_ids", "performers.id")
f.addWhere("performer_stash_ids.performer_id IS NULL")
case "aliases":
performersAliasesTableMgr.join(f, "", "performers.id")
f.addWhere("performer_aliases.alias IS NULL")
default:
f.addWhere("(performers." + *isMissing + " IS NULL OR TRIM(performers." + *isMissing + ") = '')")
}
}
}
func (qb *PerformerStore) sortByOCounter(direction string) string {
// need to sum the o_counter from scenes and images
return " ORDER BY (" + selectPerformerOCountSQL + ") " + direction
}
func yearFilterCriterionHandler(year *models.IntCriterionInput, col string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if year != nil && year.Modifier.IsValid() {
clause, args := getIntCriterionWhereClause("cast(strftime('%Y', "+col+") as int)", *year)
f.addWhere(clause, args...)
}
}
func (qb *PerformerStore) sortByPlayCount(direction string) string {
// need to sum the o_counter from scenes and images
return " ORDER BY (" + selectPerformerPlayCountSQL + ") " + direction
}
func performerAgeFilterCriterionHandler(age *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if age != nil && age.Modifier.IsValid() {
clause, args := getIntCriterionWhereClause(
"cast(IFNULL(strftime('%Y.%m%d', performers.death_date), strftime('%Y.%m%d', 'now')) - strftime('%Y.%m%d', performers.birthdate) as int)",
*age,
)
f.addWhere(clause, args...)
}
}
}
func performerAliasCriterionHandler(qb *PerformerStore, alias *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{
joinTable: performersAliasesTable,
stringColumn: performerAliasColumn,
addJoinTable: func(f *filterBuilder) {
performersAliasesTableMgr.join(f, "", "performers.id")
},
}
return h.handler(alias)
}
func performerTagsCriterionHandler(qb *PerformerStore, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
h := joinedHierarchicalMultiCriterionHandlerBuilder{
tx: qb.tx,
primaryTable: performerTable,
foreignTable: tagTable,
foreignFK: "tag_id",
relationsTable: "tags_relations",
joinAs: "image_tag",
joinTable: performersTagsTable,
primaryFK: performerIDColumn,
}
return h.handler(tags)
}
func performerTagCountCriterionHandler(qb *PerformerStore, count *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: performerTable,
joinTable: performersTagsTable,
primaryFK: performerIDColumn,
}
return h.handler(count)
}
func performerSceneCountCriterionHandler(qb *PerformerStore, count *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: performerTable,
joinTable: performersScenesTable,
primaryFK: performerIDColumn,
}
return h.handler(count)
}
func performerImageCountCriterionHandler(qb *PerformerStore, count *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: performerTable,
joinTable: performersImagesTable,
primaryFK: performerIDColumn,
}
return h.handler(count)
}
func performerGalleryCountCriterionHandler(qb *PerformerStore, count *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: performerTable,
joinTable: performersGalleriesTable,
primaryFK: performerIDColumn,
}
return h.handler(count)
}
// used for sorting and filtering on performer o-count
var selectPerformerOCountSQL = utils.StrFormat(
"SELECT SUM(o_counter) "+
"FROM ("+
"SELECT SUM(o_counter) as o_counter from {performers_images} s "+
"LEFT JOIN {images} ON {images}.id = s.{images_id} "+
"WHERE s.{performer_id} = {performers}.id "+
"UNION ALL "+
"SELECT COUNT({scenes_o_dates}.{o_date}) as o_counter from {performers_scenes} s "+
"LEFT JOIN {scenes} ON {scenes}.id = s.{scene_id} "+
"LEFT JOIN {scenes_o_dates} ON {scenes_o_dates}.{scene_id} = {scenes}.id "+
"WHERE s.{performer_id} = {performers}.id "+
")",
map[string]interface{}{
"performers_images": performersImagesTable,
"images": imageTable,
"performer_id": performerIDColumn,
"images_id": imageIDColumn,
"performers": performerTable,
"performers_scenes": performersScenesTable,
"scenes": sceneTable,
"scene_id": sceneIDColumn,
"scenes_o_dates": scenesODatesTable,
"o_date": sceneODateColumn,
},
)
// used for sorting and filtering play count on performer view count
var selectPerformerPlayCountSQL = utils.StrFormat(
"SELECT COUNT(DISTINCT {view_date}) FROM ("+
"SELECT {view_date} FROM {performers_scenes} s "+
"LEFT JOIN {scenes} ON {scenes}.id = s.{scene_id} "+
"LEFT JOIN {scenes_view_dates} ON {scenes_view_dates}.{scene_id} = {scenes}.id "+
"WHERE s.{performer_id} = {performers}.id"+
")",
map[string]interface{}{
"performer_id": performerIDColumn,
"performers": performerTable,
"performers_scenes": performersScenesTable,
"scenes": sceneTable,
"scene_id": sceneIDColumn,
"scenes_view_dates": scenesViewDatesTable,
"view_date": sceneViewDateColumn,
},
)
// used for sorting on performer last o_date
var selectPerformerLastOAtSQL = utils.StrFormat(
"SELECT MAX(o_date) FROM ("+
@@ -922,6 +669,11 @@ var selectPerformerLastOAtSQL = utils.StrFormat(
},
)
func (qb *PerformerStore) sortByLastOAt(direction string) string {
// need to get the o_dates from scenes
return " ORDER BY (" + selectPerformerLastOAtSQL + ") " + direction
}
// used for sorting on performer last view_date
var selectPerformerLastPlayedAtSQL = utils.StrFormat(
"SELECT MAX(view_date) FROM ("+
@@ -941,182 +693,6 @@ var selectPerformerLastPlayedAtSQL = utils.StrFormat(
},
)
func performerOCounterCriterionHandler(qb *PerformerStore, count *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if count == nil {
return
}
lhs := "(" + selectPerformerOCountSQL + ")"
clause, args := getIntCriterionWhereClause(lhs, *count)
f.addWhere(clause, args...)
}
}
func performerPlayCounterCriterionHandler(qb *PerformerStore, count *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if count == nil {
return
}
lhs := "(" + selectPerformerPlayCountSQL + ")"
clause, args := getIntCriterionWhereClause(lhs, *count)
f.addWhere(clause, args...)
}
}
func performerStudiosCriterionHandler(qb *PerformerStore, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if studios != nil {
formatMaps := []utils.StrFormatMap{
{
"primaryTable": sceneTable,
"joinTable": performersScenesTable,
"primaryFK": sceneIDColumn,
},
{
"primaryTable": imageTable,
"joinTable": performersImagesTable,
"primaryFK": imageIDColumn,
},
{
"primaryTable": galleryTable,
"joinTable": performersGalleriesTable,
"primaryFK": galleryIDColumn,
},
}
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.addLeftJoin(c["joinTable"].(string), "", fmt.Sprintf("%s.performer_id = performers.id", c["joinTable"]))
f.addLeftJoin(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, err := getHierarchicalValues(ctx, qb.tx, studios.Value, studioTable, "", "parent_id", "child_id", studios.Depth)
if err != nil {
f.setError(err)
return
}
f.addWith("studio(root_id, item_id) AS (" + valuesClause + ")")
templStr := `SELECT performer_id FROM {primaryTable}
INNER JOIN {joinTable} ON {primaryTable}.id = {joinTable}.{primaryFK}
INNER JOIN studio ON {primaryTable}.studio_id = studio.item_id`
var unions []string
for _, c := range formatMaps {
unions = append(unions, utils.StrFormat(templStr, c))
}
f.addWith(fmt.Sprintf("%s AS (%s)", derivedPerformerStudioTable, strings.Join(unions, " UNION ")))
f.addLeftJoin(derivedPerformerStudioTable, "", fmt.Sprintf("performers.id = %s.performer_id", derivedPerformerStudioTable))
f.addWhere(fmt.Sprintf("%s.performer_id IS %s NULL", derivedPerformerStudioTable, clauseCondition))
}
}
}
func performerAppearsWithCriterionHandler(qb *PerformerStore, performers *models.MultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if performers != nil {
formatMaps := []utils.StrFormatMap{
{
"primaryTable": performersScenesTable,
"joinTable": performersScenesTable,
"primaryFK": sceneIDColumn,
},
{
"primaryTable": performersImagesTable,
"joinTable": performersImagesTable,
"primaryFK": imageIDColumn,
},
{
"primaryTable": performersGalleriesTable,
"joinTable": performersGalleriesTable,
"primaryFK": galleryIDColumn,
},
}
if len(performers.Value) == '0' {
return
}
const derivedPerformerPerformersTable = "performer_performers"
valuesClause := strings.Join(performers.Value, "),(")
f.addWith("performer(id) AS (VALUES(" + valuesClause + "))")
templStr := `SELECT {primaryTable}2.performer_id FROM {primaryTable}
INNER JOIN {primaryTable} AS {primaryTable}2 ON {primaryTable}.{primaryFK} = {primaryTable}2.{primaryFK}
INNER JOIN performer ON {primaryTable}.performer_id = performer.id
WHERE {primaryTable}2.performer_id != performer.id`
if performers.Modifier == models.CriterionModifierIncludesAll && len(performers.Value) > 1 {
templStr += `
GROUP BY {primaryTable}2.performer_id
HAVING(count(distinct {primaryTable}.performer_id) IS ` + strconv.Itoa(len(performers.Value)) + `)`
}
var unions []string
for _, c := range formatMaps {
unions = append(unions, utils.StrFormat(templStr, c))
}
f.addWith(fmt.Sprintf("%s AS (%s)", derivedPerformerPerformersTable, strings.Join(unions, " UNION ")))
f.addInnerJoin(derivedPerformerPerformersTable, "", fmt.Sprintf("performers.id = %s.performer_id", derivedPerformerPerformersTable))
}
}
}
func (qb *PerformerStore) sortByOCounter(direction string) string {
// need to sum the o_counter from scenes and images
return " ORDER BY (" + selectPerformerOCountSQL + ") " + direction
}
func (qb *PerformerStore) sortByPlayCount(direction string) string {
// need to sum the o_counter from scenes and images
return " ORDER BY (" + selectPerformerPlayCountSQL + ") " + direction
}
func (qb *PerformerStore) sortByLastOAt(direction string) string {
// need to get the o_dates from scenes
return " ORDER BY (" + selectPerformerLastOAtSQL + ") " + direction
}
func (qb *PerformerStore) sortByLastPlayedAt(direction string) string {
// need to get the view_dates from scenes
return " ORDER BY (" + selectPerformerLastPlayedAtSQL + ") " + direction
@@ -1185,21 +761,8 @@ func (qb *PerformerStore) getPerformerSort(findFilter *models.FindFilterType) (s
return sortQuery, nil
}
func (qb *PerformerStore) tagsRepository() *joinRepository {
return &joinRepository{
repository: repository{
tx: qb.tx,
tableName: performersTagsTable,
idColumn: performerIDColumn,
},
fkColumn: tagIDColumn,
foreignTable: tagTable,
orderBy: "tags.name ASC",
}
}
func (qb *PerformerStore) GetTagIDs(ctx context.Context, id int) ([]int, error) {
return qb.tagsRepository().getIDs(ctx, id)
return performerRepository.tags.getIDs(ctx, id)
}
func (qb *PerformerStore) GetImage(ctx context.Context, performerID int) ([]byte, error) {
@@ -1218,16 +781,6 @@ func (qb *PerformerStore) destroyImage(ctx context.Context, performerID int) err
return qb.blobJoinQueryBuilder.DestroyImage(ctx, performerID, performerImageBlobColumn)
}
func (qb *PerformerStore) stashIDRepository() *stashIDRepository {
return &stashIDRepository{
repository{
tx: qb.tx,
tableName: "performer_stash_ids",
idColumn: performerIDColumn,
},
}
}
func (qb *PerformerStore) GetAliases(ctx context.Context, performerID int) ([]string, error) {
return performersAliasesTableMgr.get(ctx, performerID)
}

View File

@@ -0,0 +1,516 @@
package sqlite
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
type performerFilterHandler struct {
performerFilter *models.PerformerFilterType
}
func (qb *performerFilterHandler) validate() error {
filter := qb.performerFilter
if filter == nil {
return nil
}
if err := validateFilterCombination(filter.OperatorFilter); err != nil {
return err
}
if subFilter := filter.SubFilter(); subFilter != nil {
sqb := &performerFilterHandler{performerFilter: subFilter}
if err := sqb.validate(); err != nil {
return err
}
}
// if legacy height filter used, ensure only supported modifiers are used
if filter.Height != nil {
// treat as an int filter
intCrit := &models.IntCriterionInput{
Modifier: filter.Height.Modifier,
}
if !intCrit.ValidModifier() {
return fmt.Errorf("invalid height modifier: %s", filter.Height.Modifier)
}
// ensure value is a valid number
if _, err := strconv.Atoi(filter.Height.Value); err != nil {
return fmt.Errorf("invalid height value: %s", filter.Height.Value)
}
}
return nil
}
func (qb *performerFilterHandler) handle(ctx context.Context, f *filterBuilder) {
filter := qb.performerFilter
if filter == nil {
return
}
if err := qb.validate(); err != nil {
f.setError(err)
return
}
sf := filter.SubFilter()
if sf != nil {
sub := &performerFilterHandler{sf}
handleSubFilter(ctx, sub, f, filter.OperatorFilter)
}
f.handleCriterion(ctx, qb.criterionHandler())
}
func (qb *performerFilterHandler) criterionHandler() criterionHandler {
filter := qb.performerFilter
const tableName = performerTable
heightCmCrit := filter.HeightCm
return compoundHandler{
stringCriterionHandler(filter.Name, tableName+".name"),
stringCriterionHandler(filter.Disambiguation, tableName+".disambiguation"),
stringCriterionHandler(filter.Details, tableName+".details"),
boolCriterionHandler(filter.FilterFavorites, tableName+".favorite", nil),
boolCriterionHandler(filter.IgnoreAutoTag, tableName+".ignore_auto_tag", nil),
yearFilterCriterionHandler(filter.BirthYear, tableName+".birthdate"),
yearFilterCriterionHandler(filter.DeathYear, tableName+".death_date"),
qb.performerAgeFilterCriterionHandler(filter.Age),
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if gender := filter.Gender; gender != nil {
genderCopy := *gender
if genderCopy.Value.IsValid() && len(genderCopy.ValueList) == 0 {
genderCopy.ValueList = []models.GenderEnum{genderCopy.Value}
}
v := utils.StringerSliceToStringSlice(genderCopy.ValueList)
enumCriterionHandler(genderCopy.Modifier, v, tableName+".gender")(ctx, f)
}
}),
qb.performerIsMissingCriterionHandler(filter.IsMissing),
stringCriterionHandler(filter.Ethnicity, tableName+".ethnicity"),
stringCriterionHandler(filter.Country, tableName+".country"),
stringCriterionHandler(filter.EyeColor, tableName+".eye_color"),
// special handler for legacy height filter
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if heightCmCrit == nil && filter.Height != nil {
heightCm, _ := strconv.Atoi(filter.Height.Value) // already validated
heightCmCrit = &models.IntCriterionInput{
Value: heightCm,
Modifier: filter.Height.Modifier,
}
}
}),
intCriterionHandler(heightCmCrit, tableName+".height", nil),
stringCriterionHandler(filter.Measurements, tableName+".measurements"),
stringCriterionHandler(filter.FakeTits, tableName+".fake_tits"),
floatCriterionHandler(filter.PenisLength, tableName+".penis_length", nil),
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if circumcised := filter.Circumcised; circumcised != nil {
v := utils.StringerSliceToStringSlice(circumcised.Value)
enumCriterionHandler(circumcised.Modifier, v, tableName+".circumcised")(ctx, f)
}
}),
stringCriterionHandler(filter.CareerLength, tableName+".career_length"),
stringCriterionHandler(filter.Tattoos, tableName+".tattoos"),
stringCriterionHandler(filter.Piercings, tableName+".piercings"),
intCriterionHandler(filter.Rating100, tableName+".rating", nil),
stringCriterionHandler(filter.HairColor, tableName+".hair_color"),
stringCriterionHandler(filter.URL, tableName+".url"),
intCriterionHandler(filter.Weight, tableName+".weight", nil),
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if filter.StashID != nil {
performerRepository.stashIDs.join(f, "performer_stash_ids", "performers.id")
stringCriterionHandler(filter.StashID, "performer_stash_ids.stash_id")(ctx, f)
}
}),
&stashIDCriterionHandler{
c: filter.StashIDEndpoint,
stashIDRepository: &performerRepository.stashIDs,
stashIDTableAs: "performer_stash_ids",
parentIDCol: "performers.id",
},
qb.aliasCriterionHandler(filter.Aliases),
qb.tagsCriterionHandler(filter.Tags),
qb.studiosCriterionHandler(filter.Studios),
qb.appearsWithCriterionHandler(filter.Performers),
qb.tagCountCriterionHandler(filter.TagCount),
qb.sceneCountCriterionHandler(filter.SceneCount),
qb.imageCountCriterionHandler(filter.ImageCount),
qb.galleryCountCriterionHandler(filter.GalleryCount),
qb.playCounterCriterionHandler(filter.PlayCount),
qb.oCounterCriterionHandler(filter.OCounter),
&dateCriterionHandler{filter.Birthdate, tableName + ".birthdate", nil},
&dateCriterionHandler{filter.DeathDate, tableName + ".death_date", nil},
&timestampCriterionHandler{filter.CreatedAt, tableName + ".created_at", nil},
&timestampCriterionHandler{filter.UpdatedAt, tableName + ".updated_at", nil},
&relatedFilterHandler{
relatedIDCol: "performers_scenes.scene_id",
relatedRepo: sceneRepository.repository,
relatedHandler: &sceneFilterHandler{filter.ScenesFilter},
joinFn: func(f *filterBuilder) {
performerRepository.scenes.innerJoin(f, "", "performers.id")
},
},
&relatedFilterHandler{
relatedIDCol: "performers_images.image_id",
relatedRepo: imageRepository.repository,
relatedHandler: &imageFilterHandler{filter.ImagesFilter},
joinFn: func(f *filterBuilder) {
performerRepository.images.innerJoin(f, "", "performers.id")
},
},
&relatedFilterHandler{
relatedIDCol: "performers_galleries.gallery_id",
relatedRepo: galleryRepository.repository,
relatedHandler: &galleryFilterHandler{filter.GalleriesFilter},
joinFn: func(f *filterBuilder) {
performerRepository.galleries.innerJoin(f, "", "performers.id")
},
},
&relatedFilterHandler{
relatedIDCol: "performer_tag.tag_id",
relatedRepo: tagRepository.repository,
relatedHandler: &tagFilterHandler{filter.TagsFilter},
joinFn: func(f *filterBuilder) {
performerRepository.tags.innerJoin(f, "performer_tag", "performers.id")
},
},
}
}
// TODO - we need to provide a whitelist of possible values
func (qb *performerFilterHandler) performerIsMissingCriterionHandler(isMissing *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if isMissing != nil && *isMissing != "" {
switch *isMissing {
case "scenes": // Deprecated: use `scene_count == 0` filter instead
f.addLeftJoin(performersScenesTable, "scenes_join", "scenes_join.performer_id = performers.id")
f.addWhere("scenes_join.scene_id IS NULL")
case "image":
f.addWhere("performers.image_blob IS NULL")
case "stash_id":
performersStashIDsTableMgr.join(f, "performer_stash_ids", "performers.id")
f.addWhere("performer_stash_ids.performer_id IS NULL")
case "aliases":
performersAliasesTableMgr.join(f, "", "performers.id")
f.addWhere("performer_aliases.alias IS NULL")
default:
f.addWhere("(performers." + *isMissing + " IS NULL OR TRIM(performers." + *isMissing + ") = '')")
}
}
}
}
func (qb *performerFilterHandler) performerAgeFilterCriterionHandler(age *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if age != nil && age.Modifier.IsValid() {
clause, args := getIntCriterionWhereClause(
"cast(IFNULL(strftime('%Y.%m%d', performers.death_date), strftime('%Y.%m%d', 'now')) - strftime('%Y.%m%d', performers.birthdate) as int)",
*age,
)
f.addWhere(clause, args...)
}
}
}
func (qb *performerFilterHandler) aliasCriterionHandler(alias *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{
joinTable: performersAliasesTable,
stringColumn: performerAliasColumn,
addJoinTable: func(f *filterBuilder) {
performersAliasesTableMgr.join(f, "", "performers.id")
},
}
return h.handler(alias)
}
func (qb *performerFilterHandler) tagsCriterionHandler(tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
h := joinedHierarchicalMultiCriterionHandlerBuilder{
primaryTable: performerTable,
foreignTable: tagTable,
foreignFK: "tag_id",
relationsTable: "tags_relations",
joinAs: "performer_tag",
joinTable: performersTagsTable,
primaryFK: performerIDColumn,
}
return h.handler(tags)
}
func (qb *performerFilterHandler) tagCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: performerTable,
joinTable: performersTagsTable,
primaryFK: performerIDColumn,
}
return h.handler(count)
}
func (qb *performerFilterHandler) sceneCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: performerTable,
joinTable: performersScenesTable,
primaryFK: performerIDColumn,
}
return h.handler(count)
}
func (qb *performerFilterHandler) imageCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: performerTable,
joinTable: performersImagesTable,
primaryFK: performerIDColumn,
}
return h.handler(count)
}
func (qb *performerFilterHandler) galleryCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: performerTable,
joinTable: performersGalleriesTable,
primaryFK: performerIDColumn,
}
return h.handler(count)
}
// used for sorting and filtering on performer o-count
var selectPerformerOCountSQL = utils.StrFormat(
"SELECT SUM(o_counter) "+
"FROM ("+
"SELECT SUM(o_counter) as o_counter from {performers_images} s "+
"LEFT JOIN {images} ON {images}.id = s.{images_id} "+
"WHERE s.{performer_id} = {performers}.id "+
"UNION ALL "+
"SELECT COUNT({scenes_o_dates}.{o_date}) as o_counter from {performers_scenes} s "+
"LEFT JOIN {scenes} ON {scenes}.id = s.{scene_id} "+
"LEFT JOIN {scenes_o_dates} ON {scenes_o_dates}.{scene_id} = {scenes}.id "+
"WHERE s.{performer_id} = {performers}.id "+
")",
map[string]interface{}{
"performers_images": performersImagesTable,
"images": imageTable,
"performer_id": performerIDColumn,
"images_id": imageIDColumn,
"performers": performerTable,
"performers_scenes": performersScenesTable,
"scenes": sceneTable,
"scene_id": sceneIDColumn,
"scenes_o_dates": scenesODatesTable,
"o_date": sceneODateColumn,
},
)
// used for sorting and filtering play count on performer view count
var selectPerformerPlayCountSQL = utils.StrFormat(
"SELECT COUNT(DISTINCT {view_date}) FROM ("+
"SELECT {view_date} FROM {performers_scenes} s "+
"LEFT JOIN {scenes} ON {scenes}.id = s.{scene_id} "+
"LEFT JOIN {scenes_view_dates} ON {scenes_view_dates}.{scene_id} = {scenes}.id "+
"WHERE s.{performer_id} = {performers}.id"+
")",
map[string]interface{}{
"performer_id": performerIDColumn,
"performers": performerTable,
"performers_scenes": performersScenesTable,
"scenes": sceneTable,
"scene_id": sceneIDColumn,
"scenes_view_dates": scenesViewDatesTable,
"view_date": sceneViewDateColumn,
},
)
func (qb *performerFilterHandler) oCounterCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if count == nil {
return
}
lhs := "(" + selectPerformerOCountSQL + ")"
clause, args := getIntCriterionWhereClause(lhs, *count)
f.addWhere(clause, args...)
}
}
func (qb *performerFilterHandler) playCounterCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if count == nil {
return
}
lhs := "(" + selectPerformerPlayCountSQL + ")"
clause, args := getIntCriterionWhereClause(lhs, *count)
f.addWhere(clause, args...)
}
}
func (qb *performerFilterHandler) studiosCriterionHandler(studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if studios != nil {
formatMaps := []utils.StrFormatMap{
{
"primaryTable": sceneTable,
"joinTable": performersScenesTable,
"primaryFK": sceneIDColumn,
},
{
"primaryTable": imageTable,
"joinTable": performersImagesTable,
"primaryFK": imageIDColumn,
},
{
"primaryTable": galleryTable,
"joinTable": performersGalleriesTable,
"primaryFK": galleryIDColumn,
},
}
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.addLeftJoin(c["joinTable"].(string), "", fmt.Sprintf("%s.performer_id = performers.id", c["joinTable"]))
f.addLeftJoin(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, err := getHierarchicalValues(ctx, studios.Value, studioTable, "", "parent_id", "child_id", studios.Depth)
if err != nil {
f.setError(err)
return
}
f.addWith("studio(root_id, item_id) AS (" + valuesClause + ")")
templStr := `SELECT performer_id FROM {primaryTable}
INNER JOIN {joinTable} ON {primaryTable}.id = {joinTable}.{primaryFK}
INNER JOIN studio ON {primaryTable}.studio_id = studio.item_id`
var unions []string
for _, c := range formatMaps {
unions = append(unions, utils.StrFormat(templStr, c))
}
f.addWith(fmt.Sprintf("%s AS (%s)", derivedPerformerStudioTable, strings.Join(unions, " UNION ")))
f.addLeftJoin(derivedPerformerStudioTable, "", fmt.Sprintf("performers.id = %s.performer_id", derivedPerformerStudioTable))
f.addWhere(fmt.Sprintf("%s.performer_id IS %s NULL", derivedPerformerStudioTable, clauseCondition))
}
}
}
func (qb *performerFilterHandler) appearsWithCriterionHandler(performers *models.MultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if performers != nil {
formatMaps := []utils.StrFormatMap{
{
"primaryTable": performersScenesTable,
"joinTable": performersScenesTable,
"primaryFK": sceneIDColumn,
},
{
"primaryTable": performersImagesTable,
"joinTable": performersImagesTable,
"primaryFK": imageIDColumn,
},
{
"primaryTable": performersGalleriesTable,
"joinTable": performersGalleriesTable,
"primaryFK": galleryIDColumn,
},
}
if len(performers.Value) == '0' {
return
}
const derivedPerformerPerformersTable = "performer_performers"
valuesClause := strings.Join(performers.Value, "),(")
f.addWith("performer(id) AS (VALUES(" + valuesClause + "))")
templStr := `SELECT {primaryTable}2.performer_id FROM {primaryTable}
INNER JOIN {primaryTable} AS {primaryTable}2 ON {primaryTable}.{primaryFK} = {primaryTable}2.{primaryFK}
INNER JOIN performer ON {primaryTable}.performer_id = performer.id
WHERE {primaryTable}2.performer_id != performer.id`
if performers.Modifier == models.CriterionModifierIncludesAll && len(performers.Value) > 1 {
templStr += `
GROUP BY {primaryTable}2.performer_id
HAVING(count(distinct {primaryTable}.performer_id) IS ` + strconv.Itoa(len(performers.Value)) + `)`
}
var unions []string
for _, c := range formatMaps {
unions = append(unions, utils.StrFormat(templStr, c))
}
f.addWith(fmt.Sprintf("%s AS (%s)", derivedPerformerPerformersTable, strings.Join(unions, " UNION ")))
f.addInnerJoin(derivedPerformerPerformersTable, "", fmt.Sprintf("performers.id = %s.performer_id", derivedPerformerPerformersTable))
}
}
}

View File

@@ -731,10 +731,12 @@ func TestPerformerQueryEthnicityOr(t *testing.T) {
Value: performer1Eth,
Modifier: models.CriterionModifierEquals,
},
Or: &models.PerformerFilterType{
Ethnicity: &models.StringCriterionInput{
Value: performer2Eth,
Modifier: models.CriterionModifierEquals,
OperatorFilter: models.OperatorFilter[models.PerformerFilterType]{
Or: &models.PerformerFilterType{
Ethnicity: &models.StringCriterionInput{
Value: performer2Eth,
Modifier: models.CriterionModifierEquals,
},
},
},
}
@@ -760,10 +762,12 @@ func TestPerformerQueryEthnicityAndRating(t *testing.T) {
Value: performerEth,
Modifier: models.CriterionModifierEquals,
},
And: &models.PerformerFilterType{
Rating100: &models.IntCriterionInput{
Value: performerRating,
Modifier: models.CriterionModifierEquals,
OperatorFilter: models.OperatorFilter[models.PerformerFilterType]{
And: &models.PerformerFilterType{
Rating100: &models.IntCriterionInput{
Value: performerRating,
Modifier: models.CriterionModifierEquals,
},
},
},
}
@@ -801,8 +805,10 @@ func TestPerformerQueryEthnicityNotRating(t *testing.T) {
performerFilter := models.PerformerFilterType{
Ethnicity: &ethCriterion,
Not: &models.PerformerFilterType{
Rating100: &ratingCriterion,
OperatorFilter: models.OperatorFilter[models.PerformerFilterType]{
Not: &models.PerformerFilterType{
Rating100: &ratingCriterion,
},
},
}
@@ -838,24 +844,30 @@ func TestPerformerIllegalQuery(t *testing.T) {
// And and Or in the same filter
"AndOr",
models.PerformerFilterType{
And: &subFilter,
Or: &subFilter,
OperatorFilter: models.OperatorFilter[models.PerformerFilterType]{
And: &subFilter,
Or: &subFilter,
},
},
},
{
// And and Not in the same filter
"AndNot",
models.PerformerFilterType{
And: &subFilter,
Not: &subFilter,
OperatorFilter: models.OperatorFilter[models.PerformerFilterType]{
And: &subFilter,
Not: &subFilter,
},
},
},
{
// Or and Not in the same filter
"OrNot",
models.PerformerFilterType{
Or: &subFilter,
Not: &subFilter,
OperatorFilter: models.OperatorFilter[models.PerformerFilterType]{
Or: &subFilter,
Not: &subFilter,
},
},
},
{

View File

@@ -20,7 +20,6 @@ type objectList interface {
}
type repository struct {
tx dbWrapper
tableName string
idColumn string
}
@@ -48,7 +47,7 @@ func (r *repository) destroyExisting(ctx context.Context, ids []int) error {
func (r *repository) destroy(ctx context.Context, ids []int) error {
for _, id := range ids {
stmt := fmt.Sprintf("DELETE FROM %s WHERE %s = ?", r.tableName, r.idColumn)
if _, err := r.tx.Exec(ctx, stmt, id); err != nil {
if _, err := dbWrapper.Exec(ctx, stmt, id); err != nil {
return err
}
}
@@ -78,7 +77,7 @@ func (r *repository) runCountQuery(ctx context.Context, query string, args []int
}{0}
// Perform query and fetch result
if err := r.tx.Get(ctx, &result, query, args...); err != nil && !errors.Is(err, sql.ErrNoRows) {
if err := dbWrapper.Get(ctx, &result, query, args...); err != nil && !errors.Is(err, sql.ErrNoRows) {
return 0, err
}
@@ -90,7 +89,7 @@ func (r *repository) runIdsQuery(ctx context.Context, query string, args []inter
Int int `db:"id"`
}
if err := r.tx.Select(ctx, &result, query, args...); err != nil && !errors.Is(err, sql.ErrNoRows) {
if err := dbWrapper.Select(ctx, &result, query, args...); err != nil && !errors.Is(err, sql.ErrNoRows) {
return []int{}, fmt.Errorf("running query: %s [%v]: %w", query, args, err)
}
@@ -102,7 +101,7 @@ func (r *repository) runIdsQuery(ctx context.Context, query string, args []inter
}
func (r *repository) queryFunc(ctx context.Context, query string, args []interface{}, single bool, f func(rows *sqlx.Rows) error) error {
rows, err := r.tx.Queryx(ctx, query, args...)
rows, err := dbWrapper.Queryx(ctx, query, args...)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return err
@@ -150,7 +149,7 @@ func (r *repository) queryStruct(ctx context.Context, query string, args []inter
}
func (r *repository) querySimple(ctx context.Context, query string, args []interface{}, out interface{}) error {
rows, err := r.tx.Queryx(ctx, query, args...)
rows, err := dbWrapper.Queryx(ctx, query, args...)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return err
@@ -230,7 +229,6 @@ func (r *repository) join(j joiner, as string, parentIDCol string) {
j.addLeftJoin(r.tableName, as, fmt.Sprintf("%s.%s = %s", t, r.idColumn, parentIDCol))
}
//nolint:golint,unused
func (r *repository) innerJoin(j joiner, as string, parentIDCol string) {
t := r.tableName
if as != "" {
@@ -269,7 +267,7 @@ func (r *joinRepository) getIDs(ctx context.Context, id int) ([]int, error) {
}
func (r *joinRepository) insert(ctx context.Context, id int, foreignIDs ...int) error {
stmt, err := r.tx.Prepare(ctx, fmt.Sprintf("INSERT INTO %s (%s, %s) VALUES (?, ?)", r.tableName, r.idColumn, r.fkColumn))
stmt, err := dbWrapper.Prepare(ctx, fmt.Sprintf("INSERT INTO %s (%s, %s) VALUES (?, ?)", r.tableName, r.idColumn, r.fkColumn))
if err != nil {
return err
}
@@ -277,7 +275,7 @@ func (r *joinRepository) insert(ctx context.Context, id int, foreignIDs ...int)
defer stmt.Close()
for _, fk := range foreignIDs {
if _, err := r.tx.ExecStmt(ctx, stmt, id, fk); err != nil {
if _, err := dbWrapper.ExecStmt(ctx, stmt, id, fk); err != nil {
return err
}
}
@@ -286,7 +284,7 @@ func (r *joinRepository) insert(ctx context.Context, id int, foreignIDs ...int)
// insertOrIgnore inserts a join into the table, silently failing in the event that a conflict occurs (ie when the join already exists)
func (r *joinRepository) insertOrIgnore(ctx context.Context, id int, foreignIDs ...int) error {
stmt, err := r.tx.Prepare(ctx, fmt.Sprintf("INSERT INTO %s (%s, %s) VALUES (?, ?) ON CONFLICT (%[2]s, %s) DO NOTHING", r.tableName, r.idColumn, r.fkColumn))
stmt, err := dbWrapper.Prepare(ctx, fmt.Sprintf("INSERT INTO %s (%s, %s) VALUES (?, ?) ON CONFLICT (%[2]s, %s) DO NOTHING", r.tableName, r.idColumn, r.fkColumn))
if err != nil {
return err
}
@@ -294,7 +292,7 @@ func (r *joinRepository) insertOrIgnore(ctx context.Context, id int, foreignIDs
defer stmt.Close()
for _, fk := range foreignIDs {
if _, err := r.tx.ExecStmt(ctx, stmt, id, fk); err != nil {
if _, err := dbWrapper.ExecStmt(ctx, stmt, id, fk); err != nil {
return err
}
}
@@ -310,7 +308,7 @@ func (r *joinRepository) destroyJoins(ctx context.Context, id int, foreignIDs ..
args[i+1] = v
}
if _, err := r.tx.Exec(ctx, stmt, args...); err != nil {
if _, err := dbWrapper.Exec(ctx, stmt, args...); err != nil {
return err
}
@@ -360,7 +358,7 @@ func (r *captionRepository) get(ctx context.Context, id models.FileID) ([]*model
func (r *captionRepository) insert(ctx context.Context, id models.FileID, caption *models.VideoCaption) (sql.Result, error) {
stmt := fmt.Sprintf("INSERT INTO %s (%s, %s, %s, %s) VALUES (?, ?, ?, ?)", r.tableName, r.idColumn, captionCodeColumn, captionFilenameColumn, captionTypeColumn)
return r.tx.Exec(ctx, stmt, id, caption.LanguageCode, caption.Filename, caption.CaptionType)
return dbWrapper.Exec(ctx, stmt, id, caption.LanguageCode, caption.Filename, caption.CaptionType)
}
func (r *captionRepository) replace(ctx context.Context, id models.FileID, captions []*models.VideoCaption) error {
@@ -399,7 +397,7 @@ func (r *stringRepository) get(ctx context.Context, id int) ([]string, error) {
func (r *stringRepository) insert(ctx context.Context, id int, s string) (sql.Result, error) {
stmt := fmt.Sprintf("INSERT INTO %s (%s, %s) VALUES (?, ?)", r.tableName, r.idColumn, r.stringColumn)
return r.tx.Exec(ctx, stmt, id, s)
return dbWrapper.Exec(ctx, stmt, id, s)
}
func (r *stringRepository) replace(ctx context.Context, id int, newStrings []string) error {

View File

@@ -168,23 +168,78 @@ func (r *sceneRowRecord) fromPartial(o models.ScenePartial) {
r.setFloat64("play_duration", o.PlayDuration)
}
type SceneStore struct {
type sceneRepositoryType struct {
repository
galleries joinRepository
tags joinRepository
performers joinRepository
movies repository
files filesRepository
stashIDs stashIDRepository
}
var (
sceneRepository = sceneRepositoryType{
repository: repository{
tableName: sceneTable,
idColumn: idColumn,
},
galleries: joinRepository{
repository: repository{
tableName: scenesGalleriesTable,
idColumn: sceneIDColumn,
},
fkColumn: galleryIDColumn,
},
tags: joinRepository{
repository: repository{
tableName: scenesTagsTable,
idColumn: sceneIDColumn,
},
fkColumn: tagIDColumn,
foreignTable: tagTable,
orderBy: "tags.name ASC",
},
performers: joinRepository{
repository: repository{
tableName: performersScenesTable,
idColumn: sceneIDColumn,
},
fkColumn: performerIDColumn,
},
movies: repository{
tableName: moviesScenesTable,
idColumn: sceneIDColumn,
},
files: filesRepository{
repository: repository{
tableName: scenesFilesTable,
idColumn: sceneIDColumn,
},
},
stashIDs: stashIDRepository{
repository{
tableName: "scene_stash_ids",
idColumn: sceneIDColumn,
},
},
}
)
type SceneStore struct {
blobJoinQueryBuilder
tableMgr *table
oDateManager
viewDateManager
fileStore *FileStore
repo *storeRepository
}
func NewSceneStore(fileStore *FileStore, blobStore *BlobStore) *SceneStore {
func NewSceneStore(r *storeRepository, blobStore *BlobStore) *SceneStore {
return &SceneStore{
repository: repository{
tableName: sceneTable,
idColumn: idColumn,
},
blobJoinQueryBuilder: blobJoinQueryBuilder{
blobStore: blobStore,
joinTable: sceneTable,
@@ -193,7 +248,7 @@ func NewSceneStore(fileStore *FileStore, blobStore *BlobStore) *SceneStore {
tableMgr: sceneTableMgr,
viewDateManager: viewDateManager{scenesViewTableMgr},
oDateManager: oDateManager{scenesOTableMgr},
fileStore: fileStore,
repo: r,
}
}
@@ -531,13 +586,13 @@ func (qb *SceneStore) getMany(ctx context.Context, q *goqu.SelectDataset) ([]*mo
}
func (qb *SceneStore) GetFiles(ctx context.Context, id int) ([]*models.VideoFile, error) {
fileIDs, err := qb.filesRepository().get(ctx, id)
fileIDs, err := sceneRepository.files.get(ctx, id)
if err != nil {
return nil, err
}
// use fileStore to load files
files, err := qb.fileStore.Find(ctx, fileIDs...)
files, err := qb.repo.File.Find(ctx, fileIDs...)
if err != nil {
return nil, err
}
@@ -556,7 +611,7 @@ func (qb *SceneStore) GetFiles(ctx context.Context, id int) ([]*models.VideoFile
func (qb *SceneStore) GetManyFileIDs(ctx context.Context, ids []int) ([][]models.FileID, error) {
const primaryOnly = false
return qb.filesRepository().getMany(ctx, ids, primaryOnly)
return sceneRepository.files.getMany(ctx, ids, primaryOnly)
}
func (qb *SceneStore) FindByFileID(ctx context.Context, fileID models.FileID) ([]*models.Scene, error) {
@@ -864,176 +919,6 @@ func (qb *SceneStore) All(ctx context.Context) ([]*models.Scene, error) {
))
}
func illegalFilterCombination(type1, type2 string) error {
return fmt.Errorf("cannot have %s and %s in the same filter", type1, type2)
}
func (qb *SceneStore) validateFilter(sceneFilter *models.SceneFilterType) error {
const and = "AND"
const or = "OR"
const not = "NOT"
if sceneFilter.And != nil {
if sceneFilter.Or != nil {
return illegalFilterCombination(and, or)
}
if sceneFilter.Not != nil {
return illegalFilterCombination(and, not)
}
return qb.validateFilter(sceneFilter.And)
}
if sceneFilter.Or != nil {
if sceneFilter.Not != nil {
return illegalFilterCombination(or, not)
}
return qb.validateFilter(sceneFilter.Or)
}
if sceneFilter.Not != nil {
return qb.validateFilter(sceneFilter.Not)
}
return nil
}
func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneFilterType) *filterBuilder {
query := &filterBuilder{}
if sceneFilter.And != nil {
query.and(qb.makeFilter(ctx, sceneFilter.And))
}
if sceneFilter.Or != nil {
query.or(qb.makeFilter(ctx, sceneFilter.Or))
}
if sceneFilter.Not != nil {
query.not(qb.makeFilter(ctx, sceneFilter.Not))
}
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.ID, "scenes.id", nil))
query.handleCriterion(ctx, pathCriterionHandler(sceneFilter.Path, "folders.path", "files.basename", qb.addFoldersTable))
query.handleCriterion(ctx, sceneFileCountCriterionHandler(qb, sceneFilter.FileCount))
query.handleCriterion(ctx, stringCriterionHandler(sceneFilter.Title, "scenes.title"))
query.handleCriterion(ctx, stringCriterionHandler(sceneFilter.Code, "scenes.code"))
query.handleCriterion(ctx, stringCriterionHandler(sceneFilter.Details, "scenes.details"))
query.handleCriterion(ctx, stringCriterionHandler(sceneFilter.Director, "scenes.director"))
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if sceneFilter.Oshash != nil {
qb.addSceneFilesTable(f)
f.addLeftJoin(fingerprintTable, "fingerprints_oshash", "scenes_files.file_id = fingerprints_oshash.file_id AND fingerprints_oshash.type = 'oshash'")
}
stringCriterionHandler(sceneFilter.Oshash, "fingerprints_oshash.fingerprint")(ctx, f)
}))
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if sceneFilter.Checksum != nil {
qb.addSceneFilesTable(f)
f.addLeftJoin(fingerprintTable, "fingerprints_md5", "scenes_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'")
}
stringCriterionHandler(sceneFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
}))
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if sceneFilter.Phash != nil {
// backwards compatibility
scenePhashDistanceCriterionHandler(qb, &models.PhashDistanceCriterionInput{
Value: sceneFilter.Phash.Value,
Modifier: sceneFilter.Phash.Modifier,
})(ctx, f)
}
}))
query.handleCriterion(ctx, scenePhashDistanceCriterionHandler(qb, sceneFilter.PhashDistance))
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.Rating100, "scenes.rating", nil))
query.handleCriterion(ctx, sceneOCountCriterionHandler(sceneFilter.OCounter))
query.handleCriterion(ctx, boolCriterionHandler(sceneFilter.Organized, "scenes.organized", nil))
query.handleCriterion(ctx, floatIntCriterionHandler(sceneFilter.Duration, "video_files.duration", qb.addVideoFilesTable))
query.handleCriterion(ctx, resolutionCriterionHandler(sceneFilter.Resolution, "video_files.height", "video_files.width", qb.addVideoFilesTable))
query.handleCriterion(ctx, orientationCriterionHandler(sceneFilter.Orientation, "video_files.height", "video_files.width", qb.addVideoFilesTable))
query.handleCriterion(ctx, floatIntCriterionHandler(sceneFilter.Framerate, "ROUND(video_files.frame_rate)", qb.addVideoFilesTable))
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.Bitrate, "video_files.bit_rate", qb.addVideoFilesTable))
query.handleCriterion(ctx, codecCriterionHandler(sceneFilter.VideoCodec, "video_files.video_codec", qb.addVideoFilesTable))
query.handleCriterion(ctx, codecCriterionHandler(sceneFilter.AudioCodec, "video_files.audio_codec", qb.addVideoFilesTable))
query.handleCriterion(ctx, hasMarkersCriterionHandler(sceneFilter.HasMarkers))
query.handleCriterion(ctx, sceneIsMissingCriterionHandler(qb, sceneFilter.IsMissing))
query.handleCriterion(ctx, sceneURLsCriterionHandler(sceneFilter.URL))
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if sceneFilter.StashID != nil {
qb.stashIDRepository().join(f, "scene_stash_ids", "scenes.id")
stringCriterionHandler(sceneFilter.StashID, "scene_stash_ids.stash_id")(ctx, f)
}
}))
query.handleCriterion(ctx, &stashIDCriterionHandler{
c: sceneFilter.StashIDEndpoint,
stashIDRepository: qb.stashIDRepository(),
stashIDTableAs: "scene_stash_ids",
parentIDCol: "scenes.id",
})
query.handleCriterion(ctx, boolCriterionHandler(sceneFilter.Interactive, "video_files.interactive", qb.addVideoFilesTable))
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.InteractiveSpeed, "video_files.interactive_speed", qb.addVideoFilesTable))
query.handleCriterion(ctx, sceneCaptionCriterionHandler(qb, sceneFilter.Captions))
query.handleCriterion(ctx, floatIntCriterionHandler(sceneFilter.ResumeTime, "scenes.resume_time", nil))
query.handleCriterion(ctx, floatIntCriterionHandler(sceneFilter.PlayDuration, "scenes.play_duration", nil))
query.handleCriterion(ctx, scenePlayCountCriterionHandler(sceneFilter.PlayCount))
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if sceneFilter.LastPlayedAt != nil {
f.addLeftJoin(
fmt.Sprintf("(SELECT %s, MAX(%s) as last_played_at FROM %s GROUP BY %s)", sceneIDColumn, sceneViewDateColumn, scenesViewDatesTable, sceneIDColumn),
"scene_last_view",
fmt.Sprintf("scene_last_view.%s = scenes.id", sceneIDColumn),
)
timestampCriterionHandler(sceneFilter.LastPlayedAt, "IFNULL(last_played_at, datetime(0))")(ctx, f)
}
}))
query.handleCriterion(ctx, sceneTagsCriterionHandler(qb, sceneFilter.Tags))
query.handleCriterion(ctx, sceneTagCountCriterionHandler(qb, sceneFilter.TagCount))
query.handleCriterion(ctx, scenePerformersCriterionHandler(qb, sceneFilter.Performers))
query.handleCriterion(ctx, scenePerformerCountCriterionHandler(qb, sceneFilter.PerformerCount))
query.handleCriterion(ctx, studioCriterionHandler(sceneTable, sceneFilter.Studios))
query.handleCriterion(ctx, sceneMoviesCriterionHandler(qb, sceneFilter.Movies))
query.handleCriterion(ctx, sceneGalleriesCriterionHandler(qb, sceneFilter.Galleries))
query.handleCriterion(ctx, scenePerformerTagsCriterionHandler(qb, sceneFilter.PerformerTags))
query.handleCriterion(ctx, scenePerformerFavoriteCriterionHandler(sceneFilter.PerformerFavorite))
query.handleCriterion(ctx, scenePerformerAgeCriterionHandler(sceneFilter.PerformerAge))
query.handleCriterion(ctx, scenePhashDuplicatedCriterionHandler(sceneFilter.Duplicated, qb.addSceneFilesTable))
query.handleCriterion(ctx, dateCriterionHandler(sceneFilter.Date, "scenes.date"))
query.handleCriterion(ctx, timestampCriterionHandler(sceneFilter.CreatedAt, "scenes.created_at"))
query.handleCriterion(ctx, timestampCriterionHandler(sceneFilter.UpdatedAt, "scenes.updated_at"))
return query
}
func (qb *SceneStore) addSceneFilesTable(f *filterBuilder) {
f.addLeftJoin(scenesFilesTable, "", "scenes_files.scene_id = scenes.id")
}
func (qb *SceneStore) addFilesTable(f *filterBuilder) {
qb.addSceneFilesTable(f)
f.addLeftJoin(fileTable, "", "scenes_files.file_id = files.id")
}
func (qb *SceneStore) addFoldersTable(f *filterBuilder) {
qb.addFilesTable(f)
f.addLeftJoin(folderTable, "", "files.parent_folder_id = folders.id")
}
func (qb *SceneStore) addVideoFilesTable(f *filterBuilder) {
qb.addSceneFilesTable(f)
f.addLeftJoin(videoFileTable, "", "video_files.file_id = scenes_files.file_id")
}
func (qb *SceneStore) makeQuery(ctx context.Context, sceneFilter *models.SceneFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
if sceneFilter == nil {
sceneFilter = &models.SceneFilterType{}
@@ -1042,7 +927,7 @@ func (qb *SceneStore) makeQuery(ctx context.Context, sceneFilter *models.SceneFi
findFilter = &models.FindFilterType{}
}
query := qb.newQuery()
query := sceneRepository.newQuery()
distinctIDs(&query, sceneTable)
if q := findFilter.Q; q != nil && *q != "" {
@@ -1074,10 +959,9 @@ func (qb *SceneStore) makeQuery(ctx context.Context, sceneFilter *models.SceneFi
query.parseQueryString(searchColumns, *q)
}
if err := qb.validateFilter(sceneFilter); err != nil {
return nil, err
}
filter := qb.makeFilter(ctx, sceneFilter)
filter := filterBuilderFromHandler(ctx, &sceneFilterHandler{
sceneFilter: sceneFilter,
})
if err := query.addFilter(filter); err != nil {
return nil, err
@@ -1117,7 +1001,7 @@ func (qb *SceneStore) queryGroupedFields(ctx context.Context, options models.Sce
return models.NewSceneQueryResult(qb), nil
}
aggregateQuery := qb.newQuery()
aggregateQuery := sceneRepository.newQuery()
if options.Count {
aggregateQuery.addColumn("COUNT(DISTINCT temp.id) as total")
@@ -1161,7 +1045,7 @@ func (qb *SceneStore) queryGroupedFields(ctx context.Context, options models.Sce
Duration null.Float
Size null.Float
}{}
if err := qb.repository.queryStruct(ctx, aggregateQuery.toSQL(includeSortPagination), query.args, &out); err != nil {
if err := sceneRepository.queryStruct(ctx, aggregateQuery.toSQL(includeSortPagination), query.args, &out); err != nil {
return nil, err
}
@@ -1181,349 +1065,6 @@ func (qb *SceneStore) QueryCount(ctx context.Context, sceneFilter *models.SceneF
return query.executeCount(ctx)
}
func scenePlayCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: scenesViewDatesTable,
primaryFK: sceneIDColumn,
}
return h.handler(count)
}
func sceneOCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: scenesODatesTable,
primaryFK: sceneIDColumn,
}
return h.handler(count)
}
func sceneFileCountCriterionHandler(qb *SceneStore, fileCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: scenesFilesTable,
primaryFK: sceneIDColumn,
}
return h.handler(fileCount)
}
func scenePhashDuplicatedCriterionHandler(duplicatedFilter *models.PHashDuplicationCriterionInput, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
// TODO: Wishlist item: Implement Distance matching
if duplicatedFilter != nil {
if addJoinFn != nil {
addJoinFn(f)
}
var v string
if *duplicatedFilter.Duplicated {
v = ">"
} else {
v = "="
}
f.addInnerJoin("(SELECT file_id FROM files_fingerprints INNER JOIN (SELECT fingerprint FROM files_fingerprints WHERE type = 'phash' GROUP BY fingerprint HAVING COUNT (fingerprint) "+v+" 1) dupes on files_fingerprints.fingerprint = dupes.fingerprint)", "scph", "scenes_files.file_id = scph.file_id")
}
}
}
func floatIntCriterionHandler(durationFilter *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if durationFilter != nil {
if addJoinFn != nil {
addJoinFn(f)
}
clause, args := getIntCriterionWhereClause("cast("+column+" as int)", *durationFilter)
f.addWhere(clause, args...)
}
}
}
func resolutionCriterionHandler(resolution *models.ResolutionCriterionInput, heightColumn string, widthColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if resolution != nil && resolution.Value.IsValid() {
if addJoinFn != nil {
addJoinFn(f)
}
min := resolution.Value.GetMinResolution()
max := resolution.Value.GetMaxResolution()
widthHeight := fmt.Sprintf("MIN(%s, %s)", widthColumn, heightColumn)
switch resolution.Modifier {
case models.CriterionModifierEquals:
f.addWhere(fmt.Sprintf("%s BETWEEN %d AND %d", widthHeight, min, max))
case models.CriterionModifierNotEquals:
f.addWhere(fmt.Sprintf("%s NOT BETWEEN %d AND %d", widthHeight, min, max))
case models.CriterionModifierLessThan:
f.addWhere(fmt.Sprintf("%s < %d", widthHeight, min))
case models.CriterionModifierGreaterThan:
f.addWhere(fmt.Sprintf("%s > %d", widthHeight, max))
}
}
}
}
func codecCriterionHandler(codec *models.StringCriterionInput, codecColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if codec != nil {
if addJoinFn != nil {
addJoinFn(f)
}
stringCriterionHandler(codec, codecColumn)(ctx, f)
}
}
}
func hasMarkersCriterionHandler(hasMarkers *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if hasMarkers != nil {
f.addLeftJoin("scene_markers", "", "scene_markers.scene_id = scenes.id")
if *hasMarkers == "true" {
f.addHaving("count(scene_markers.scene_id) > 0")
} else {
f.addWhere("scene_markers.id IS NULL")
}
}
}
}
func sceneIsMissingCriterionHandler(qb *SceneStore, isMissing *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if isMissing != nil && *isMissing != "" {
switch *isMissing {
case "url":
scenesURLsTableMgr.join(f, "", "scenes.id")
f.addWhere("scene_urls.url IS NULL")
case "galleries":
qb.galleriesRepository().join(f, "galleries_join", "scenes.id")
f.addWhere("galleries_join.scene_id IS NULL")
case "studio":
f.addWhere("scenes.studio_id IS NULL")
case "movie":
qb.moviesRepository().join(f, "movies_join", "scenes.id")
f.addWhere("movies_join.scene_id IS NULL")
case "performers":
qb.performersRepository().join(f, "performers_join", "scenes.id")
f.addWhere("performers_join.scene_id IS NULL")
case "date":
f.addWhere(`scenes.date IS NULL OR scenes.date IS ""`)
case "tags":
qb.tagsRepository().join(f, "tags_join", "scenes.id")
f.addWhere("tags_join.scene_id IS NULL")
case "stash_id":
qb.stashIDRepository().join(f, "scene_stash_ids", "scenes.id")
f.addWhere("scene_stash_ids.scene_id IS NULL")
case "phash":
qb.addSceneFilesTable(f)
f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
f.addWhere("fingerprints_phash.fingerprint IS NULL")
case "cover":
f.addWhere("scenes.cover_blob IS NULL")
default:
f.addWhere("(scenes." + *isMissing + " IS NULL OR TRIM(scenes." + *isMissing + ") = '')")
}
}
}
}
func sceneURLsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{
joinTable: scenesURLsTable,
stringColumn: sceneURLColumn,
addJoinTable: func(f *filterBuilder) {
scenesURLsTableMgr.join(f, "", "scenes.id")
},
}
return h.handler(url)
}
func (qb *SceneStore) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
return multiCriterionHandlerBuilder{
primaryTable: sceneTable,
foreignTable: foreignTable,
joinTable: joinTable,
primaryFK: sceneIDColumn,
foreignFK: foreignFK,
addJoinsFunc: addJoinsFunc,
}
}
func sceneCaptionCriterionHandler(qb *SceneStore, captions *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{
joinTable: videoCaptionsTable,
stringColumn: captionCodeColumn,
addJoinTable: func(f *filterBuilder) {
qb.addSceneFilesTable(f)
f.addLeftJoin(videoCaptionsTable, "", "video_captions.file_id = scenes_files.file_id")
},
}
return h.handler(captions)
}
func sceneTagsCriterionHandler(qb *SceneStore, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
h := joinedHierarchicalMultiCriterionHandlerBuilder{
tx: qb.tx,
primaryTable: sceneTable,
foreignTable: tagTable,
foreignFK: "tag_id",
relationsTable: "tags_relations",
joinAs: "scene_tag",
joinTable: scenesTagsTable,
primaryFK: sceneIDColumn,
}
return h.handler(tags)
}
func sceneTagCountCriterionHandler(qb *SceneStore, tagCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: scenesTagsTable,
primaryFK: sceneIDColumn,
}
return h.handler(tagCount)
}
func scenePerformersCriterionHandler(qb *SceneStore, performers *models.MultiCriterionInput) criterionHandlerFunc {
h := joinedMultiCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: performersScenesTable,
joinAs: "performers_join",
primaryFK: sceneIDColumn,
foreignFK: performerIDColumn,
addJoinTable: func(f *filterBuilder) {
qb.performersRepository().join(f, "performers_join", "scenes.id")
},
}
return h.handler(performers)
}
func scenePerformerCountCriterionHandler(qb *SceneStore, performerCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: performersScenesTable,
primaryFK: sceneIDColumn,
}
return h.handler(performerCount)
}
func scenePerformerFavoriteCriterionHandler(performerfavorite *bool) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if performerfavorite != nil {
f.addLeftJoin("performers_scenes", "", "scenes.id = performers_scenes.scene_id")
if *performerfavorite {
// contains at least one favorite
f.addLeftJoin("performers", "", "performers.id = performers_scenes.performer_id")
f.addWhere("performers.favorite = 1")
} else {
// contains zero favorites
f.addLeftJoin(`(SELECT performers_scenes.scene_id as id FROM performers_scenes
JOIN performers ON performers.id = performers_scenes.performer_id
GROUP BY performers_scenes.scene_id HAVING SUM(performers.favorite) = 0)`, "nofaves", "scenes.id = nofaves.id")
f.addWhere("performers_scenes.scene_id IS NULL OR nofaves.id IS NOT NULL")
}
}
}
}
func scenePerformerAgeCriterionHandler(performerAge *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if performerAge != nil {
f.addInnerJoin("performers_scenes", "", "scenes.id = performers_scenes.scene_id")
f.addInnerJoin("performers", "", "performers_scenes.performer_id = performers.id")
f.addWhere("scenes.date != '' AND performers.birthdate != ''")
f.addWhere("scenes.date IS NOT NULL AND performers.birthdate IS NOT NULL")
ageCalc := "cast(strftime('%Y.%m%d', scenes.date) - strftime('%Y.%m%d', performers.birthdate) as int)"
whereClause, args := getIntWhereClause(ageCalc, performerAge.Modifier, performerAge.Value, performerAge.Value2)
f.addWhere(whereClause, args...)
}
}
}
func sceneMoviesCriterionHandler(qb *SceneStore, movies *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) {
qb.moviesRepository().join(f, "", "scenes.id")
f.addLeftJoin("movies", "", "movies_scenes.movie_id = movies.id")
}
h := qb.getMultiCriterionHandlerBuilder(movieTable, moviesScenesTable, "movie_id", addJoinsFunc)
return h.handler(movies)
}
func sceneGalleriesCriterionHandler(qb *SceneStore, galleries *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) {
qb.galleriesRepository().join(f, "", "scenes.id")
f.addLeftJoin("galleries", "", "scenes_galleries.gallery_id = galleries.id")
}
h := qb.getMultiCriterionHandlerBuilder(galleryTable, scenesGalleriesTable, "gallery_id", addJoinsFunc)
return h.handler(galleries)
}
func scenePerformerTagsCriterionHandler(qb *SceneStore, tags *models.HierarchicalMultiCriterionInput) criterionHandler {
return &joinedPerformerTagsHandler{
criterion: tags,
primaryTable: sceneTable,
joinTable: performersScenesTable,
joinPrimaryKey: sceneIDColumn,
}
}
func scenePhashDistanceCriterionHandler(qb *SceneStore, phashDistance *models.PhashDistanceCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if phashDistance != nil {
qb.addSceneFilesTable(f)
f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
value, _ := utils.StringToPhash(phashDistance.Value)
distance := 0
if phashDistance.Distance != nil {
distance = *phashDistance.Distance
}
if distance == 0 {
// use the default handler
intCriterionHandler(&models.IntCriterionInput{
Value: int(value),
Modifier: phashDistance.Modifier,
}, "fingerprints_phash.fingerprint", nil)(ctx, f)
}
switch {
case phashDistance.Modifier == models.CriterionModifierEquals && distance > 0:
// needed to avoid a type mismatch
f.addWhere("typeof(fingerprints_phash.fingerprint) = 'integer'")
f.addWhere("phash_distance(fingerprints_phash.fingerprint, ?) < ?", value, distance)
case phashDistance.Modifier == models.CriterionModifierNotEquals && distance > 0:
// needed to avoid a type mismatch
f.addWhere("typeof(fingerprints_phash.fingerprint) = 'integer'")
f.addWhere("phash_distance(fingerprints_phash.fingerprint, ?) > ?", value, distance)
default:
intCriterionHandler(&models.IntCriterionInput{
Value: int(value),
Modifier: phashDistance.Modifier,
}, "fingerprints_phash.fingerprint", nil)(ctx, f)
}
}
}
}
var sceneSortOptions = sortOptions{
"bitrate",
"created_at",
@@ -1719,7 +1260,7 @@ func (qb *SceneStore) AssignFiles(ctx context.Context, sceneID int, fileIDs []mo
}
// assign primary only if destination has no files
existingFileIDs, err := qb.filesRepository().get(ctx, sceneID)
existingFileIDs, err := sceneRepository.files.get(ctx, sceneID)
if err != nil {
return err
}
@@ -1728,18 +1269,10 @@ func (qb *SceneStore) AssignFiles(ctx context.Context, sceneID int, fileIDs []mo
return scenesFilesTableMgr.insertJoins(ctx, sceneID, firstPrimary, fileIDs)
}
func (qb *SceneStore) moviesRepository() *repository {
return &repository{
tx: qb.tx,
tableName: moviesScenesTable,
idColumn: sceneIDColumn,
}
}
func (qb *SceneStore) GetMovies(ctx context.Context, id int) (ret []models.MoviesScenes, err error) {
ret = []models.MoviesScenes{}
if err := qb.moviesRepository().getAll(ctx, id, func(rows *sqlx.Rows) error {
if err := sceneRepository.movies.getAll(ctx, id, func(rows *sqlx.Rows) error {
var ms moviesScenesRow
if err := rows.StructScan(&ms); err != nil {
return err
@@ -1754,91 +1287,36 @@ func (qb *SceneStore) GetMovies(ctx context.Context, id int) (ret []models.Movie
return ret, nil
}
func (qb *SceneStore) filesRepository() *filesRepository {
return &filesRepository{
repository: repository{
tx: qb.tx,
tableName: scenesFilesTable,
idColumn: sceneIDColumn,
},
}
}
func (qb *SceneStore) AddFileID(ctx context.Context, id int, fileID models.FileID) error {
const firstPrimary = false
return scenesFilesTableMgr.insertJoins(ctx, id, firstPrimary, []models.FileID{fileID})
}
func (qb *SceneStore) performersRepository() *joinRepository {
return &joinRepository{
repository: repository{
tx: qb.tx,
tableName: performersScenesTable,
idColumn: sceneIDColumn,
},
fkColumn: performerIDColumn,
}
}
func (qb *SceneStore) GetPerformerIDs(ctx context.Context, id int) ([]int, error) {
return qb.performersRepository().getIDs(ctx, id)
}
func (qb *SceneStore) tagsRepository() *joinRepository {
return &joinRepository{
repository: repository{
tx: qb.tx,
tableName: scenesTagsTable,
idColumn: sceneIDColumn,
},
fkColumn: tagIDColumn,
foreignTable: tagTable,
orderBy: "tags.name ASC",
}
return sceneRepository.performers.getIDs(ctx, id)
}
func (qb *SceneStore) GetTagIDs(ctx context.Context, id int) ([]int, error) {
return qb.tagsRepository().getIDs(ctx, id)
}
func (qb *SceneStore) galleriesRepository() *joinRepository {
return &joinRepository{
repository: repository{
tx: qb.tx,
tableName: scenesGalleriesTable,
idColumn: sceneIDColumn,
},
fkColumn: galleryIDColumn,
}
return sceneRepository.tags.getIDs(ctx, id)
}
func (qb *SceneStore) GetGalleryIDs(ctx context.Context, id int) ([]int, error) {
return qb.galleriesRepository().getIDs(ctx, id)
return sceneRepository.galleries.getIDs(ctx, id)
}
func (qb *SceneStore) AddGalleryIDs(ctx context.Context, sceneID int, galleryIDs []int) error {
return scenesGalleriesTableMgr.addJoins(ctx, sceneID, galleryIDs)
}
func (qb *SceneStore) stashIDRepository() *stashIDRepository {
return &stashIDRepository{
repository{
tx: qb.tx,
tableName: "scene_stash_ids",
idColumn: sceneIDColumn,
},
}
}
func (qb *SceneStore) GetStashIDs(ctx context.Context, sceneID int) ([]models.StashID, error) {
return qb.stashIDRepository().get(ctx, sceneID)
return sceneRepository.stashIDs.get(ctx, sceneID)
}
func (qb *SceneStore) FindDuplicates(ctx context.Context, distance int, durationDiff float64) ([][]*models.Scene, error) {
var dupeIds [][]int
if distance == 0 {
var ids []string
if err := qb.tx.Select(ctx, &ids, findExactDuplicateQuery, durationDiff); err != nil {
if err := dbWrapper.Select(ctx, &ids, findExactDuplicateQuery, durationDiff); err != nil {
return nil, err
}
@@ -1858,7 +1336,7 @@ func (qb *SceneStore) FindDuplicates(ctx context.Context, distance int, duration
} else {
var hashes []*utils.Phash
if err := qb.queryFunc(ctx, findAllPhashesQuery, nil, false, func(rows *sqlx.Rows) error {
if err := sceneRepository.queryFunc(ctx, findAllPhashesQuery, nil, false, func(rows *sqlx.Rows) error {
phash := utils.Phash{
Bucket: -1,
Duration: -1,

533
pkg/sqlite/scene_filter.go Normal file
View File

@@ -0,0 +1,533 @@
package sqlite
import (
"context"
"fmt"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
type sceneFilterHandler struct {
sceneFilter *models.SceneFilterType
}
func (qb *sceneFilterHandler) validate() error {
sceneFilter := qb.sceneFilter
if sceneFilter == nil {
return nil
}
if err := validateFilterCombination(sceneFilter.OperatorFilter); err != nil {
return err
}
if subFilter := sceneFilter.SubFilter(); subFilter != nil {
sqb := &sceneFilterHandler{sceneFilter: subFilter}
if err := sqb.validate(); err != nil {
return err
}
}
return nil
}
func (qb *sceneFilterHandler) handle(ctx context.Context, f *filterBuilder) {
sceneFilter := qb.sceneFilter
if sceneFilter == nil {
return
}
if err := qb.validate(); err != nil {
f.setError(err)
return
}
sf := sceneFilter.SubFilter()
if sf != nil {
sub := &sceneFilterHandler{sf}
handleSubFilter(ctx, sub, f, sceneFilter.OperatorFilter)
}
f.handleCriterion(ctx, qb.criterionHandler())
}
func (qb *sceneFilterHandler) criterionHandler() criterionHandler {
sceneFilter := qb.sceneFilter
return compoundHandler{
intCriterionHandler(sceneFilter.ID, "scenes.id", nil),
pathCriterionHandler(sceneFilter.Path, "folders.path", "files.basename", qb.addFoldersTable),
qb.fileCountCriterionHandler(sceneFilter.FileCount),
stringCriterionHandler(sceneFilter.Title, "scenes.title"),
stringCriterionHandler(sceneFilter.Code, "scenes.code"),
stringCriterionHandler(sceneFilter.Details, "scenes.details"),
stringCriterionHandler(sceneFilter.Director, "scenes.director"),
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if sceneFilter.Oshash != nil {
qb.addSceneFilesTable(f)
f.addLeftJoin(fingerprintTable, "fingerprints_oshash", "scenes_files.file_id = fingerprints_oshash.file_id AND fingerprints_oshash.type = 'oshash'")
}
stringCriterionHandler(sceneFilter.Oshash, "fingerprints_oshash.fingerprint")(ctx, f)
}),
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if sceneFilter.Checksum != nil {
qb.addSceneFilesTable(f)
f.addLeftJoin(fingerprintTable, "fingerprints_md5", "scenes_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'")
}
stringCriterionHandler(sceneFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
}),
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if sceneFilter.Phash != nil {
// backwards compatibility
qb.phashDistanceCriterionHandler(&models.PhashDistanceCriterionInput{
Value: sceneFilter.Phash.Value,
Modifier: sceneFilter.Phash.Modifier,
})(ctx, f)
}
}),
qb.phashDistanceCriterionHandler(sceneFilter.PhashDistance),
intCriterionHandler(sceneFilter.Rating100, "scenes.rating", nil),
qb.oCountCriterionHandler(sceneFilter.OCounter),
boolCriterionHandler(sceneFilter.Organized, "scenes.organized", nil),
floatIntCriterionHandler(sceneFilter.Duration, "video_files.duration", qb.addVideoFilesTable),
resolutionCriterionHandler(sceneFilter.Resolution, "video_files.height", "video_files.width", qb.addVideoFilesTable),
orientationCriterionHandler(sceneFilter.Orientation, "video_files.height", "video_files.width", qb.addVideoFilesTable),
floatIntCriterionHandler(sceneFilter.Framerate, "ROUND(video_files.frame_rate)", qb.addVideoFilesTable),
intCriterionHandler(sceneFilter.Bitrate, "video_files.bit_rate", qb.addVideoFilesTable),
qb.codecCriterionHandler(sceneFilter.VideoCodec, "video_files.video_codec", qb.addVideoFilesTable),
qb.codecCriterionHandler(sceneFilter.AudioCodec, "video_files.audio_codec", qb.addVideoFilesTable),
qb.hasMarkersCriterionHandler(sceneFilter.HasMarkers),
qb.isMissingCriterionHandler(sceneFilter.IsMissing),
qb.urlsCriterionHandler(sceneFilter.URL),
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if sceneFilter.StashID != nil {
sceneRepository.stashIDs.join(f, "scene_stash_ids", "scenes.id")
stringCriterionHandler(sceneFilter.StashID, "scene_stash_ids.stash_id")(ctx, f)
}
}),
&stashIDCriterionHandler{
c: sceneFilter.StashIDEndpoint,
stashIDRepository: &sceneRepository.stashIDs,
stashIDTableAs: "scene_stash_ids",
parentIDCol: "scenes.id",
},
boolCriterionHandler(sceneFilter.Interactive, "video_files.interactive", qb.addVideoFilesTable),
intCriterionHandler(sceneFilter.InteractiveSpeed, "video_files.interactive_speed", qb.addVideoFilesTable),
qb.captionCriterionHandler(sceneFilter.Captions),
floatIntCriterionHandler(sceneFilter.ResumeTime, "scenes.resume_time", nil),
floatIntCriterionHandler(sceneFilter.PlayDuration, "scenes.play_duration", nil),
qb.playCountCriterionHandler(sceneFilter.PlayCount),
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if sceneFilter.LastPlayedAt != nil {
f.addLeftJoin(
fmt.Sprintf("(SELECT %s, MAX(%s) as last_played_at FROM %s GROUP BY %s)", sceneIDColumn, sceneViewDateColumn, scenesViewDatesTable, sceneIDColumn),
"scene_last_view",
fmt.Sprintf("scene_last_view.%s = scenes.id", sceneIDColumn),
)
h := timestampCriterionHandler{sceneFilter.LastPlayedAt, "IFNULL(last_played_at, datetime(0))", nil}
h.handle(ctx, f)
}
}),
qb.tagsCriterionHandler(sceneFilter.Tags),
qb.tagCountCriterionHandler(sceneFilter.TagCount),
qb.performersCriterionHandler(sceneFilter.Performers),
qb.performerCountCriterionHandler(sceneFilter.PerformerCount),
studioCriterionHandler(sceneTable, sceneFilter.Studios),
qb.moviesCriterionHandler(sceneFilter.Movies),
qb.galleriesCriterionHandler(sceneFilter.Galleries),
qb.performerTagsCriterionHandler(sceneFilter.PerformerTags),
qb.performerFavoriteCriterionHandler(sceneFilter.PerformerFavorite),
qb.performerAgeCriterionHandler(sceneFilter.PerformerAge),
qb.phashDuplicatedCriterionHandler(sceneFilter.Duplicated, qb.addSceneFilesTable),
&dateCriterionHandler{sceneFilter.Date, "scenes.date", nil},
&timestampCriterionHandler{sceneFilter.CreatedAt, "scenes.created_at", nil},
&timestampCriterionHandler{sceneFilter.UpdatedAt, "scenes.updated_at", nil},
&relatedFilterHandler{
relatedIDCol: "scenes_galleries.gallery_id",
relatedRepo: galleryRepository.repository,
relatedHandler: &galleryFilterHandler{sceneFilter.GalleriesFilter},
joinFn: func(f *filterBuilder) {
sceneRepository.galleries.innerJoin(f, "", "scenes.id")
},
},
&relatedFilterHandler{
relatedIDCol: "performers_join.performer_id",
relatedRepo: performerRepository.repository,
relatedHandler: &performerFilterHandler{sceneFilter.PerformersFilter},
joinFn: func(f *filterBuilder) {
sceneRepository.performers.innerJoin(f, "performers_join", "scenes.id")
},
},
&relatedFilterHandler{
relatedIDCol: "scenes.studio_id",
relatedRepo: studioRepository.repository,
relatedHandler: &studioFilterHandler{sceneFilter.StudiosFilter},
},
&relatedFilterHandler{
relatedIDCol: "scene_tag.tag_id",
relatedRepo: tagRepository.repository,
relatedHandler: &tagFilterHandler{sceneFilter.TagsFilter},
joinFn: func(f *filterBuilder) {
sceneRepository.tags.innerJoin(f, "scene_tag", "scenes.id")
},
},
&relatedFilterHandler{
relatedIDCol: "movies_scenes.movie_id",
relatedRepo: movieRepository.repository,
relatedHandler: &movieFilterHandler{sceneFilter.MoviesFilter},
joinFn: func(f *filterBuilder) {
sceneRepository.movies.innerJoin(f, "", "scenes.id")
},
},
&relatedFilterHandler{
relatedIDCol: "scene_markers.id",
relatedRepo: sceneMarkerRepository.repository,
relatedHandler: &sceneMarkerFilterHandler{sceneFilter.MarkersFilter},
joinFn: func(f *filterBuilder) {
f.addInnerJoin("scene_markers", "", "scenes.id")
},
},
}
}
func (qb *sceneFilterHandler) addSceneFilesTable(f *filterBuilder) {
f.addLeftJoin(scenesFilesTable, "", "scenes_files.scene_id = scenes.id")
}
func (qb *sceneFilterHandler) addFilesTable(f *filterBuilder) {
qb.addSceneFilesTable(f)
f.addLeftJoin(fileTable, "", "scenes_files.file_id = files.id")
}
func (qb *sceneFilterHandler) addFoldersTable(f *filterBuilder) {
qb.addFilesTable(f)
f.addLeftJoin(folderTable, "", "files.parent_folder_id = folders.id")
}
func (qb *sceneFilterHandler) addVideoFilesTable(f *filterBuilder) {
qb.addSceneFilesTable(f)
f.addLeftJoin(videoFileTable, "", "video_files.file_id = scenes_files.file_id")
}
func (qb *sceneFilterHandler) playCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: scenesViewDatesTable,
primaryFK: sceneIDColumn,
}
return h.handler(count)
}
func (qb *sceneFilterHandler) oCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: scenesODatesTable,
primaryFK: sceneIDColumn,
}
return h.handler(count)
}
func (qb *sceneFilterHandler) fileCountCriterionHandler(fileCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: scenesFilesTable,
primaryFK: sceneIDColumn,
}
return h.handler(fileCount)
}
func (qb *sceneFilterHandler) phashDuplicatedCriterionHandler(duplicatedFilter *models.PHashDuplicationCriterionInput, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
// TODO: Wishlist item: Implement Distance matching
if duplicatedFilter != nil {
if addJoinFn != nil {
addJoinFn(f)
}
var v string
if *duplicatedFilter.Duplicated {
v = ">"
} else {
v = "="
}
f.addInnerJoin("(SELECT file_id FROM files_fingerprints INNER JOIN (SELECT fingerprint FROM files_fingerprints WHERE type = 'phash' GROUP BY fingerprint HAVING COUNT (fingerprint) "+v+" 1) dupes on files_fingerprints.fingerprint = dupes.fingerprint)", "scph", "scenes_files.file_id = scph.file_id")
}
}
}
func (qb *sceneFilterHandler) codecCriterionHandler(codec *models.StringCriterionInput, codecColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if codec != nil {
if addJoinFn != nil {
addJoinFn(f)
}
stringCriterionHandler(codec, codecColumn)(ctx, f)
}
}
}
func (qb *sceneFilterHandler) hasMarkersCriterionHandler(hasMarkers *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if hasMarkers != nil {
f.addLeftJoin("scene_markers", "", "scene_markers.scene_id = scenes.id")
if *hasMarkers == "true" {
f.addHaving("count(scene_markers.scene_id) > 0")
} else {
f.addWhere("scene_markers.id IS NULL")
}
}
}
}
func (qb *sceneFilterHandler) isMissingCriterionHandler(isMissing *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if isMissing != nil && *isMissing != "" {
switch *isMissing {
case "url":
scenesURLsTableMgr.join(f, "", "scenes.id")
f.addWhere("scene_urls.url IS NULL")
case "galleries":
sceneRepository.galleries.join(f, "galleries_join", "scenes.id")
f.addWhere("galleries_join.scene_id IS NULL")
case "studio":
f.addWhere("scenes.studio_id IS NULL")
case "movie":
sceneRepository.movies.join(f, "movies_join", "scenes.id")
f.addWhere("movies_join.scene_id IS NULL")
case "performers":
sceneRepository.performers.join(f, "performers_join", "scenes.id")
f.addWhere("performers_join.scene_id IS NULL")
case "date":
f.addWhere(`scenes.date IS NULL OR scenes.date IS ""`)
case "tags":
sceneRepository.tags.join(f, "tags_join", "scenes.id")
f.addWhere("tags_join.scene_id IS NULL")
case "stash_id":
sceneRepository.stashIDs.join(f, "scene_stash_ids", "scenes.id")
f.addWhere("scene_stash_ids.scene_id IS NULL")
case "phash":
qb.addSceneFilesTable(f)
f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
f.addWhere("fingerprints_phash.fingerprint IS NULL")
case "cover":
f.addWhere("scenes.cover_blob IS NULL")
default:
f.addWhere("(scenes." + *isMissing + " IS NULL OR TRIM(scenes." + *isMissing + ") = '')")
}
}
}
}
func (qb *sceneFilterHandler) urlsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{
joinTable: scenesURLsTable,
stringColumn: sceneURLColumn,
addJoinTable: func(f *filterBuilder) {
scenesURLsTableMgr.join(f, "", "scenes.id")
},
}
return h.handler(url)
}
func (qb *sceneFilterHandler) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
return multiCriterionHandlerBuilder{
primaryTable: sceneTable,
foreignTable: foreignTable,
joinTable: joinTable,
primaryFK: sceneIDColumn,
foreignFK: foreignFK,
addJoinsFunc: addJoinsFunc,
}
}
func (qb *sceneFilterHandler) captionCriterionHandler(captions *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{
joinTable: videoCaptionsTable,
stringColumn: captionCodeColumn,
addJoinTable: func(f *filterBuilder) {
qb.addSceneFilesTable(f)
f.addLeftJoin(videoCaptionsTable, "", "video_captions.file_id = scenes_files.file_id")
},
}
return h.handler(captions)
}
func (qb *sceneFilterHandler) tagsCriterionHandler(tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
h := joinedHierarchicalMultiCriterionHandlerBuilder{
primaryTable: sceneTable,
foreignTable: tagTable,
foreignFK: "tag_id",
relationsTable: "tags_relations",
joinAs: "scene_tag",
joinTable: scenesTagsTable,
primaryFK: sceneIDColumn,
}
return h.handler(tags)
}
func (qb *sceneFilterHandler) tagCountCriterionHandler(tagCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: scenesTagsTable,
primaryFK: sceneIDColumn,
}
return h.handler(tagCount)
}
func (qb *sceneFilterHandler) performersCriterionHandler(performers *models.MultiCriterionInput) criterionHandlerFunc {
h := joinedMultiCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: performersScenesTable,
joinAs: "performers_join",
primaryFK: sceneIDColumn,
foreignFK: performerIDColumn,
addJoinTable: func(f *filterBuilder) {
sceneRepository.performers.join(f, "performers_join", "scenes.id")
},
}
return h.handler(performers)
}
func (qb *sceneFilterHandler) performerCountCriterionHandler(performerCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: performersScenesTable,
primaryFK: sceneIDColumn,
}
return h.handler(performerCount)
}
func (qb *sceneFilterHandler) performerFavoriteCriterionHandler(performerfavorite *bool) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if performerfavorite != nil {
f.addLeftJoin("performers_scenes", "", "scenes.id = performers_scenes.scene_id")
if *performerfavorite {
// contains at least one favorite
f.addLeftJoin("performers", "", "performers.id = performers_scenes.performer_id")
f.addWhere("performers.favorite = 1")
} else {
// contains zero favorites
f.addLeftJoin(`(SELECT performers_scenes.scene_id as id FROM performers_scenes
JOIN performers ON performers.id = performers_scenes.performer_id
GROUP BY performers_scenes.scene_id HAVING SUM(performers.favorite) = 0)`, "nofaves", "scenes.id = nofaves.id")
f.addWhere("performers_scenes.scene_id IS NULL OR nofaves.id IS NOT NULL")
}
}
}
}
func (qb *sceneFilterHandler) performerAgeCriterionHandler(performerAge *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if performerAge != nil {
f.addInnerJoin("performers_scenes", "", "scenes.id = performers_scenes.scene_id")
f.addInnerJoin("performers", "", "performers_scenes.performer_id = performers.id")
f.addWhere("scenes.date != '' AND performers.birthdate != ''")
f.addWhere("scenes.date IS NOT NULL AND performers.birthdate IS NOT NULL")
ageCalc := "cast(strftime('%Y.%m%d', scenes.date) - strftime('%Y.%m%d', performers.birthdate) as int)"
whereClause, args := getIntWhereClause(ageCalc, performerAge.Modifier, performerAge.Value, performerAge.Value2)
f.addWhere(whereClause, args...)
}
}
}
func (qb *sceneFilterHandler) moviesCriterionHandler(movies *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) {
sceneRepository.movies.join(f, "", "scenes.id")
f.addLeftJoin("movies", "", "movies_scenes.movie_id = movies.id")
}
h := qb.getMultiCriterionHandlerBuilder(movieTable, moviesScenesTable, "movie_id", addJoinsFunc)
return h.handler(movies)
}
func (qb *sceneFilterHandler) galleriesCriterionHandler(galleries *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) {
sceneRepository.galleries.join(f, "", "scenes.id")
f.addLeftJoin("galleries", "", "scenes_galleries.gallery_id = galleries.id")
}
h := qb.getMultiCriterionHandlerBuilder(galleryTable, scenesGalleriesTable, "gallery_id", addJoinsFunc)
return h.handler(galleries)
}
func (qb *sceneFilterHandler) performerTagsCriterionHandler(tags *models.HierarchicalMultiCriterionInput) criterionHandler {
return &joinedPerformerTagsHandler{
criterion: tags,
primaryTable: sceneTable,
joinTable: performersScenesTable,
joinPrimaryKey: sceneIDColumn,
}
}
func (qb *sceneFilterHandler) phashDistanceCriterionHandler(phashDistance *models.PhashDistanceCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if phashDistance != nil {
qb.addSceneFilesTable(f)
f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
value, _ := utils.StringToPhash(phashDistance.Value)
distance := 0
if phashDistance.Distance != nil {
distance = *phashDistance.Distance
}
if distance == 0 {
// use the default handler
intCriterionHandler(&models.IntCriterionInput{
Value: int(value),
Modifier: phashDistance.Modifier,
}, "fingerprints_phash.fingerprint", nil)(ctx, f)
}
switch {
case phashDistance.Modifier == models.CriterionModifierEquals && distance > 0:
// needed to avoid a type mismatch
f.addWhere("typeof(fingerprints_phash.fingerprint) = 'integer'")
f.addWhere("phash_distance(fingerprints_phash.fingerprint, ?) < ?", value, distance)
case phashDistance.Modifier == models.CriterionModifierNotEquals && distance > 0:
// needed to avoid a type mismatch
f.addWhere("typeof(fingerprints_phash.fingerprint) = 'integer'")
f.addWhere("phash_distance(fingerprints_phash.fingerprint, ?) > ?", value, distance)
default:
intCriterionHandler(&models.IntCriterionInput{
Value: int(value),
Modifier: phashDistance.Modifier,
}, "fingerprints_phash.fingerprint", nil)(ctx, f)
}
}
}
}

View File

@@ -75,24 +75,41 @@ func (r *sceneMarkerRowRecord) fromPartial(o models.SceneMarkerPartial) {
r.setTimestamp("updated_at", o.UpdatedAt)
}
type SceneMarkerStore struct {
type sceneMarkerRepositoryType struct {
repository
tableMgr *table
scenes repository
tags joinRepository
}
func NewSceneMarkerStore() *SceneMarkerStore {
return &SceneMarkerStore{
var (
sceneMarkerRepository = sceneMarkerRepositoryType{
repository: repository{
tableName: sceneMarkerTable,
idColumn: idColumn,
},
tableMgr: sceneMarkerTableMgr,
scenes: repository{
tableName: sceneTable,
idColumn: idColumn,
},
tags: joinRepository{
repository: repository{
tableName: "scene_markers_tags",
idColumn: "scene_marker_id",
},
fkColumn: tagIDColumn,
},
}
)
type SceneMarkerStore struct{}
func NewSceneMarkerStore() *SceneMarkerStore {
return &SceneMarkerStore{}
}
func (qb *SceneMarkerStore) table() exp.IdentifierExpression {
return qb.tableMgr.table
return sceneMarkerTableMgr.table
}
func (qb *SceneMarkerStore) selectDataset() *goqu.SelectDataset {
@@ -103,7 +120,7 @@ func (qb *SceneMarkerStore) Create(ctx context.Context, newObject *models.SceneM
var r sceneMarkerRow
r.fromSceneMarker(*newObject)
id, err := qb.tableMgr.insertID(ctx, r)
id, err := sceneMarkerTableMgr.insertID(ctx, r)
if err != nil {
return err
}
@@ -128,7 +145,7 @@ func (qb *SceneMarkerStore) UpdatePartial(ctx context.Context, id int, partial m
r.fromPartial(partial)
if len(r.Record) > 0 {
if err := qb.tableMgr.updateByID(ctx, id, r.Record); err != nil {
if err := sceneMarkerTableMgr.updateByID(ctx, id, r.Record); err != nil {
return nil, err
}
}
@@ -140,7 +157,7 @@ func (qb *SceneMarkerStore) Update(ctx context.Context, updatedObject *models.Sc
var r sceneMarkerRow
r.fromSceneMarker(*updatedObject)
if err := qb.tableMgr.updateByID(ctx, updatedObject.ID, r); err != nil {
if err := sceneMarkerTableMgr.updateByID(ctx, updatedObject.ID, r); err != nil {
return err
}
@@ -148,7 +165,7 @@ func (qb *SceneMarkerStore) Update(ctx context.Context, updatedObject *models.Sc
}
func (qb *SceneMarkerStore) Destroy(ctx context.Context, id int) error {
return qb.destroyExisting(ctx, []int{id})
return sceneMarkerRepository.destroyExisting(ctx, []int{id})
}
// returns nil, nil if not found
@@ -186,7 +203,7 @@ func (qb *SceneMarkerStore) FindMany(ctx context.Context, ids []int) ([]*models.
// returns nil, sql.ErrNoRows if not found
func (qb *SceneMarkerStore) find(ctx context.Context, id int) (*models.SceneMarker, error) {
q := qb.selectDataset().Where(qb.tableMgr.byID(id))
q := qb.selectDataset().Where(sceneMarkerTableMgr.byID(id))
ret, err := qb.get(ctx, q)
if err != nil {
@@ -243,7 +260,7 @@ func (qb *SceneMarkerStore) FindBySceneID(ctx context.Context, sceneID int) ([]*
func (qb *SceneMarkerStore) CountByTagID(ctx context.Context, tagID int) (int, error) {
args := []interface{}{tagID, tagID}
return qb.runCountQuery(ctx, qb.buildCountQuery(countSceneMarkersForTagQuery), args)
return sceneMarkerRepository.runCountQuery(ctx, sceneMarkerRepository.buildCountQuery(countSceneMarkersForTagQuery), args)
}
func (qb *SceneMarkerStore) GetMarkerStrings(ctx context.Context, q *string, sort *string) ([]*models.MarkerStringsResultType, error) {
@@ -272,21 +289,6 @@ func (qb *SceneMarkerStore) Wall(ctx context.Context, q *string) ([]*models.Scen
return qb.getMany(ctx, qq)
}
func (qb *SceneMarkerStore) makeFilter(ctx context.Context, sceneMarkerFilter *models.SceneMarkerFilterType) *filterBuilder {
query := &filterBuilder{}
query.handleCriterion(ctx, sceneMarkerTagIDCriterionHandler(qb, sceneMarkerFilter.TagID))
query.handleCriterion(ctx, sceneMarkerTagsCriterionHandler(qb, sceneMarkerFilter.Tags))
query.handleCriterion(ctx, sceneMarkerSceneTagsCriterionHandler(qb, sceneMarkerFilter.SceneTags))
query.handleCriterion(ctx, sceneMarkerPerformersCriterionHandler(qb, sceneMarkerFilter.Performers))
query.handleCriterion(ctx, timestampCriterionHandler(sceneMarkerFilter.CreatedAt, "scene_markers.created_at"))
query.handleCriterion(ctx, timestampCriterionHandler(sceneMarkerFilter.UpdatedAt, "scene_markers.updated_at"))
query.handleCriterion(ctx, dateCriterionHandler(sceneMarkerFilter.SceneDate, "scenes.date"))
query.handleCriterion(ctx, timestampCriterionHandler(sceneMarkerFilter.SceneCreatedAt, "scenes.created_at"))
query.handleCriterion(ctx, timestampCriterionHandler(sceneMarkerFilter.SceneUpdatedAt, "scenes.updated_at"))
return query
}
func (qb *SceneMarkerStore) makeQuery(ctx context.Context, sceneMarkerFilter *models.SceneMarkerFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
if sceneMarkerFilter == nil {
sceneMarkerFilter = &models.SceneMarkerFilterType{}
@@ -295,7 +297,7 @@ func (qb *SceneMarkerStore) makeQuery(ctx context.Context, sceneMarkerFilter *mo
findFilter = &models.FindFilterType{}
}
query := qb.newQuery()
query := sceneMarkerRepository.newQuery()
distinctIDs(&query, sceneMarkerTable)
if q := findFilter.Q; q != nil && *q != "" {
@@ -304,7 +306,9 @@ func (qb *SceneMarkerStore) makeQuery(ctx context.Context, sceneMarkerFilter *mo
query.parseQueryString(searchColumns, *q)
}
filter := qb.makeFilter(ctx, sceneMarkerFilter)
filter := filterBuilderFromHandler(ctx, &sceneMarkerFilterHandler{
sceneMarkerFilter: sceneMarkerFilter,
})
if err := query.addFilter(filter); err != nil {
return nil, err
@@ -346,135 +350,6 @@ func (qb *SceneMarkerStore) QueryCount(ctx context.Context, sceneMarkerFilter *m
return query.executeCount(ctx)
}
func sceneMarkerTagIDCriterionHandler(qb *SceneMarkerStore, tagID *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if tagID != nil {
f.addLeftJoin("scene_markers_tags", "", "scene_markers_tags.scene_marker_id = scene_markers.id")
f.addWhere("(scene_markers.primary_tag_id = ? OR scene_markers_tags.tag_id = ?)", *tagID, *tagID)
}
}
}
func sceneMarkerTagsCriterionHandler(qb *SceneMarkerStore, criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if criterion != nil {
tags := criterion.CombineExcludes()
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
var notClause string
if tags.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addLeftJoin("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 tags.Modifier == models.CriterionModifierEquals && tags.Depth != nil && *tags.Depth != 0 {
f.setError(fmt.Errorf("depth is not supported for equals modifier for marker tag filtering"))
return
}
if len(tags.Value) == 0 && len(tags.Excludes) == 0 {
return
}
if len(tags.Value) > 0 {
valuesClause, err := getHierarchicalValues(ctx, qb.tx, tags.Value, tagTable, "tags_relations", "parent_id", "child_id", tags.Depth)
if err != nil {
f.setError(err)
return
}
f.addWith(`marker_tags AS (
SELECT mt.scene_marker_id, t.column1 AS root_tag_id FROM scene_markers_tags mt
INNER JOIN (` + valuesClause + `) t ON t.column2 = mt.tag_id
UNION
SELECT m.id, t.column1 FROM scene_markers m
INNER JOIN (` + valuesClause + `) t ON t.column2 = m.primary_tag_id
)`)
f.addLeftJoin("marker_tags", "", "marker_tags.scene_marker_id = scene_markers.id")
switch tags.Modifier {
case models.CriterionModifierEquals:
// includes only the provided ids
f.addWhere("marker_tags.root_tag_id IS NOT NULL")
tagsLen := len(tags.Value)
f.addHaving(fmt.Sprintf("count(distinct marker_tags.root_tag_id) IS %d", tagsLen))
// decrement by one to account for primary tag id
f.addWhere("(SELECT COUNT(*) FROM scene_markers_tags s WHERE s.scene_marker_id = scene_markers.id) = ?", tagsLen-1)
case models.CriterionModifierNotEquals:
f.setError(fmt.Errorf("not equals modifier is not supported for scene marker tags"))
default:
addHierarchicalConditionClauses(f, tags, "marker_tags", "root_tag_id")
}
}
if len(criterion.Excludes) > 0 {
valuesClause, err := getHierarchicalValues(ctx, dbWrapper{}, tags.Excludes, tagTable, "tags_relations", "parent_id", "child_id", tags.Depth)
if err != nil {
f.setError(err)
return
}
clause := "scene_markers.id NOT IN (SELECT scene_markers_tags.scene_marker_id FROM scene_markers_tags WHERE scene_markers_tags.tag_id IN (SELECT column2 FROM (%s)))"
f.addWhere(fmt.Sprintf(clause, valuesClause))
f.addWhere(fmt.Sprintf("scene_markers.primary_tag_id NOT IN (SELECT column2 FROM (%s))", valuesClause))
}
}
}
}
func sceneMarkerSceneTagsCriterionHandler(qb *SceneMarkerStore, tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if tags != nil {
f.addLeftJoin("scenes_tags", "", "scene_markers.scene_id = scenes_tags.scene_id")
h := joinedHierarchicalMultiCriterionHandlerBuilder{
tx: qb.tx,
primaryTable: "scene_markers",
primaryKey: sceneIDColumn,
foreignTable: tagTable,
foreignFK: tagIDColumn,
relationsTable: "tags_relations",
joinTable: "scenes_tags",
joinAs: "marker_scenes_tags",
primaryFK: sceneIDColumn,
}
h.handler(tags).handle(ctx, f)
}
}
}
func sceneMarkerPerformersCriterionHandler(qb *SceneMarkerStore, performers *models.MultiCriterionInput) criterionHandlerFunc {
h := joinedMultiCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: performersScenesTable,
joinAs: "performers_join",
primaryFK: sceneIDColumn,
foreignFK: performerIDColumn,
addJoinTable: func(f *filterBuilder) {
f.addLeftJoin(performersScenesTable, "performers_join", "performers_join.scene_id = scene_markers.scene_id")
},
}
handler := h.handler(performers)
return func(ctx context.Context, f *filterBuilder) {
// Make sure scenes is included, otherwise excludes filter fails
f.addLeftJoin(sceneTable, "", "scenes.id = scene_markers.scene_id")
handler(ctx, f)
}
}
var sceneMarkerSortOptions = sortOptions{
"created_at",
"id",
@@ -514,7 +389,7 @@ func (qb *SceneMarkerStore) setSceneMarkerSort(query *queryBuilder, findFilter *
func (qb *SceneMarkerStore) querySceneMarkers(ctx context.Context, query string, args []interface{}) ([]*models.SceneMarker, error) {
const single = false
var ret []*models.SceneMarker
if err := qb.queryFunc(ctx, query, args, single, func(r *sqlx.Rows) error {
if err := sceneMarkerRepository.queryFunc(ctx, query, args, single, func(r *sqlx.Rows) error {
var f sceneMarkerRow
if err := r.StructScan(&f); err != nil {
return err
@@ -532,7 +407,7 @@ func (qb *SceneMarkerStore) querySceneMarkers(ctx context.Context, query string,
}
func (qb *SceneMarkerStore) queryMarkerStringsResultType(ctx context.Context, query string, args []interface{}) ([]*models.MarkerStringsResultType, error) {
rows, err := qb.tx.Queryx(ctx, query, args...)
rows, err := dbWrapper.Queryx(ctx, query, args...)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
@@ -554,24 +429,13 @@ func (qb *SceneMarkerStore) queryMarkerStringsResultType(ctx context.Context, qu
return markerStrings, nil
}
func (qb *SceneMarkerStore) tagsRepository() *joinRepository {
return &joinRepository{
repository: repository{
tx: qb.tx,
tableName: "scene_markers_tags",
idColumn: "scene_marker_id",
},
fkColumn: tagIDColumn,
}
}
func (qb *SceneMarkerStore) GetTagIDs(ctx context.Context, id int) ([]int, error) {
return qb.tagsRepository().getIDs(ctx, id)
return sceneMarkerRepository.tags.getIDs(ctx, id)
}
func (qb *SceneMarkerStore) UpdateTags(ctx context.Context, id int, tagIDs []int) error {
// Delete the existing joins and then create new ones
return qb.tagsRepository().replace(ctx, id, tagIDs)
return sceneMarkerRepository.tags.replace(ctx, id, tagIDs)
}
func (qb *SceneMarkerStore) Count(ctx context.Context) (int, error) {

View File

@@ -0,0 +1,189 @@
package sqlite
import (
"context"
"fmt"
"github.com/stashapp/stash/pkg/models"
)
type sceneMarkerFilterHandler struct {
sceneMarkerFilter *models.SceneMarkerFilterType
}
func (qb *sceneMarkerFilterHandler) validate() error {
return nil
}
func (qb *sceneMarkerFilterHandler) handle(ctx context.Context, f *filterBuilder) {
sceneMarkerFilter := qb.sceneMarkerFilter
if sceneMarkerFilter == nil {
return
}
if err := qb.validate(); err != nil {
f.setError(err)
return
}
f.handleCriterion(ctx, qb.criterionHandler())
}
func (qb *sceneMarkerFilterHandler) joinScenes(f *filterBuilder) {
sceneMarkerRepository.scenes.innerJoin(f, "", "scene_markers.scene_id")
}
func (qb *sceneMarkerFilterHandler) criterionHandler() criterionHandler {
sceneMarkerFilter := qb.sceneMarkerFilter
return compoundHandler{
qb.tagIDCriterionHandler(sceneMarkerFilter.TagID),
qb.tagsCriterionHandler(sceneMarkerFilter.Tags),
qb.sceneTagsCriterionHandler(sceneMarkerFilter.SceneTags),
qb.performersCriterionHandler(sceneMarkerFilter.Performers),
&timestampCriterionHandler{sceneMarkerFilter.CreatedAt, "scene_markers.created_at", nil},
&timestampCriterionHandler{sceneMarkerFilter.UpdatedAt, "scene_markers.updated_at", nil},
&dateCriterionHandler{sceneMarkerFilter.SceneDate, "scenes.date", qb.joinScenes},
&timestampCriterionHandler{sceneMarkerFilter.SceneCreatedAt, "scenes.created_at", qb.joinScenes},
&timestampCriterionHandler{sceneMarkerFilter.SceneUpdatedAt, "scenes.updated_at", qb.joinScenes},
&relatedFilterHandler{
relatedIDCol: "scenes.id",
relatedRepo: sceneRepository.repository,
relatedHandler: &sceneFilterHandler{sceneMarkerFilter.SceneFilter},
joinFn: func(f *filterBuilder) {
qb.joinScenes(f)
},
},
}
}
func (qb *sceneMarkerFilterHandler) tagIDCriterionHandler(tagID *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if tagID != nil {
f.addLeftJoin("scene_markers_tags", "", "scene_markers_tags.scene_marker_id = scene_markers.id")
f.addWhere("(scene_markers.primary_tag_id = ? OR scene_markers_tags.tag_id = ?)", *tagID, *tagID)
}
}
}
func (qb *sceneMarkerFilterHandler) tagsCriterionHandler(criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if criterion != nil {
tags := criterion.CombineExcludes()
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
var notClause string
if tags.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addLeftJoin("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 tags.Modifier == models.CriterionModifierEquals && tags.Depth != nil && *tags.Depth != 0 {
f.setError(fmt.Errorf("depth is not supported for equals modifier for marker tag filtering"))
return
}
if len(tags.Value) == 0 && len(tags.Excludes) == 0 {
return
}
if len(tags.Value) > 0 {
valuesClause, err := getHierarchicalValues(ctx, tags.Value, tagTable, "tags_relations", "parent_id", "child_id", tags.Depth)
if err != nil {
f.setError(err)
return
}
f.addWith(`marker_tags AS (
SELECT mt.scene_marker_id, t.column1 AS root_tag_id FROM scene_markers_tags mt
INNER JOIN (` + valuesClause + `) t ON t.column2 = mt.tag_id
UNION
SELECT m.id, t.column1 FROM scene_markers m
INNER JOIN (` + valuesClause + `) t ON t.column2 = m.primary_tag_id
)`)
f.addLeftJoin("marker_tags", "", "marker_tags.scene_marker_id = scene_markers.id")
switch tags.Modifier {
case models.CriterionModifierEquals:
// includes only the provided ids
f.addWhere("marker_tags.root_tag_id IS NOT NULL")
tagsLen := len(tags.Value)
f.addHaving(fmt.Sprintf("count(distinct marker_tags.root_tag_id) IS %d", tagsLen))
// decrement by one to account for primary tag id
f.addWhere("(SELECT COUNT(*) FROM scene_markers_tags s WHERE s.scene_marker_id = scene_markers.id) = ?", tagsLen-1)
case models.CriterionModifierNotEquals:
f.setError(fmt.Errorf("not equals modifier is not supported for scene marker tags"))
default:
addHierarchicalConditionClauses(f, tags, "marker_tags", "root_tag_id")
}
}
if len(criterion.Excludes) > 0 {
valuesClause, err := getHierarchicalValues(ctx, tags.Excludes, tagTable, "tags_relations", "parent_id", "child_id", tags.Depth)
if err != nil {
f.setError(err)
return
}
clause := "scene_markers.id NOT IN (SELECT scene_markers_tags.scene_marker_id FROM scene_markers_tags WHERE scene_markers_tags.tag_id IN (SELECT column2 FROM (%s)))"
f.addWhere(fmt.Sprintf(clause, valuesClause))
f.addWhere(fmt.Sprintf("scene_markers.primary_tag_id NOT IN (SELECT column2 FROM (%s))", valuesClause))
}
}
}
}
func (qb *sceneMarkerFilterHandler) sceneTagsCriterionHandler(tags *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if tags != nil {
f.addLeftJoin("scenes_tags", "", "scene_markers.scene_id = scenes_tags.scene_id")
h := joinedHierarchicalMultiCriterionHandlerBuilder{
primaryTable: "scene_markers",
primaryKey: sceneIDColumn,
foreignTable: tagTable,
foreignFK: tagIDColumn,
relationsTable: "tags_relations",
joinTable: "scenes_tags",
joinAs: "marker_scenes_tags",
primaryFK: sceneIDColumn,
}
h.handler(tags).handle(ctx, f)
}
}
}
func (qb *sceneMarkerFilterHandler) performersCriterionHandler(performers *models.MultiCriterionInput) criterionHandlerFunc {
h := joinedMultiCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: performersScenesTable,
joinAs: "performers_join",
primaryFK: sceneIDColumn,
foreignFK: performerIDColumn,
addJoinTable: func(f *filterBuilder) {
f.addLeftJoin(performersScenesTable, "performers_join", "performers_join.scene_id = scene_markers.scene_id")
},
}
handler := h.handler(performers)
return func(ctx context.Context, f *filterBuilder) {
if performers == nil {
return
}
// Make sure scenes is included, otherwise excludes filter fails
qb.joinScenes(f)
handler(ctx, f)
}
}

View File

@@ -2411,10 +2411,12 @@ func TestSceneQueryPathOr(t *testing.T) {
Value: scene1Path,
Modifier: models.CriterionModifierEquals,
},
Or: &models.SceneFilterType{
Path: &models.StringCriterionInput{
Value: scene2Path,
Modifier: models.CriterionModifierEquals,
OperatorFilter: models.OperatorFilter[models.SceneFilterType]{
Or: &models.SceneFilterType{
Path: &models.StringCriterionInput{
Value: scene2Path,
Modifier: models.CriterionModifierEquals,
},
},
},
}
@@ -2444,10 +2446,12 @@ func TestSceneQueryPathAndRating(t *testing.T) {
Value: scenePath,
Modifier: models.CriterionModifierEquals,
},
And: &models.SceneFilterType{
Rating100: &models.IntCriterionInput{
Value: sceneRating,
Modifier: models.CriterionModifierEquals,
OperatorFilter: models.OperatorFilter[models.SceneFilterType]{
And: &models.SceneFilterType{
Rating100: &models.IntCriterionInput{
Value: sceneRating,
Modifier: models.CriterionModifierEquals,
},
},
},
}
@@ -2484,8 +2488,10 @@ func TestSceneQueryPathNotRating(t *testing.T) {
sceneFilter := models.SceneFilterType{
Path: &pathCriterion,
Not: &models.SceneFilterType{
Rating100: &ratingCriterion,
OperatorFilter: models.OperatorFilter[models.SceneFilterType]{
Not: &models.SceneFilterType{
Rating100: &ratingCriterion,
},
},
}
@@ -2516,8 +2522,10 @@ func TestSceneIllegalQuery(t *testing.T) {
}
sceneFilter := &models.SceneFilterType{
And: &subFilter,
Or: &subFilter,
OperatorFilter: models.OperatorFilter[models.SceneFilterType]{
And: &subFilter,
Or: &subFilter,
},
}
withTxn(func(ctx context.Context) error {

View File

@@ -21,6 +21,11 @@ func distinctIDs(qb *queryBuilder, tableName string) {
qb.from = tableName
}
func selectIDs(qb *queryBuilder, tableName string) {
qb.addColumn(getColumn(tableName, "id"))
qb.from = tableName
}
func getColumn(tableName string, columnName string) string {
return tableName + "." + columnName
}

View File

@@ -90,8 +90,44 @@ func (r *studioRowRecord) fromPartial(o models.StudioPartial) {
r.setBool("ignore_auto_tag", o.IgnoreAutoTag)
}
type StudioStore struct {
type studioRepositoryType struct {
repository
stashIDs stashIDRepository
scenes repository
images repository
galleries repository
}
var (
studioRepository = studioRepositoryType{
repository: repository{
tableName: studioTable,
idColumn: idColumn,
},
stashIDs: stashIDRepository{
repository{
tableName: "studio_stash_ids",
idColumn: studioIDColumn,
},
},
scenes: repository{
tableName: sceneTable,
idColumn: studioIDColumn,
},
images: repository{
tableName: imageTable,
idColumn: studioIDColumn,
},
galleries: repository{
tableName: galleryTable,
idColumn: studioIDColumn,
},
}
)
type StudioStore struct {
blobJoinQueryBuilder
tableMgr *table
@@ -99,10 +135,6 @@ type StudioStore struct {
func NewStudioStore(blobStore *BlobStore) *StudioStore {
return &StudioStore{
repository: repository{
tableName: studioTable,
idColumn: idColumn,
},
blobJoinQueryBuilder: blobJoinQueryBuilder{
blobStore: blobStore,
joinTable: studioTable,
@@ -147,7 +179,7 @@ func (qb *StudioStore) Create(ctx context.Context, newObject *models.Studio) err
}
}
updated, _ := qb.find(ctx, id)
updated, err := qb.find(ctx, id)
if err != nil {
return fmt.Errorf("finding after create: %w", err)
}
@@ -220,7 +252,7 @@ func (qb *StudioStore) Destroy(ctx context.Context, id int) error {
return err
}
return qb.destroyExisting(ctx, []int{id})
return studioRepository.destroyExisting(ctx, []int{id})
}
// returns nil, nil if not found
@@ -452,83 +484,6 @@ func (qb *StudioStore) QueryForAutoTag(ctx context.Context, words []string) ([]*
return ret, nil
}
func (qb *StudioStore) validateFilter(filter *models.StudioFilterType) error {
const and = "AND"
const or = "OR"
const not = "NOT"
if filter.And != nil {
if filter.Or != nil {
return illegalFilterCombination(and, or)
}
if filter.Not != nil {
return illegalFilterCombination(and, not)
}
return qb.validateFilter(filter.And)
}
if filter.Or != nil {
if filter.Not != nil {
return illegalFilterCombination(or, not)
}
return qb.validateFilter(filter.Or)
}
if filter.Not != nil {
return qb.validateFilter(filter.Not)
}
return nil
}
func (qb *StudioStore) makeFilter(ctx context.Context, studioFilter *models.StudioFilterType) *filterBuilder {
query := &filterBuilder{}
if studioFilter.And != nil {
query.and(qb.makeFilter(ctx, studioFilter.And))
}
if studioFilter.Or != nil {
query.or(qb.makeFilter(ctx, studioFilter.Or))
}
if studioFilter.Not != nil {
query.not(qb.makeFilter(ctx, studioFilter.Not))
}
query.handleCriterion(ctx, stringCriterionHandler(studioFilter.Name, studioTable+".name"))
query.handleCriterion(ctx, stringCriterionHandler(studioFilter.Details, studioTable+".details"))
query.handleCriterion(ctx, stringCriterionHandler(studioFilter.URL, studioTable+".url"))
query.handleCriterion(ctx, intCriterionHandler(studioFilter.Rating100, studioTable+".rating", nil))
query.handleCriterion(ctx, boolCriterionHandler(studioFilter.Favorite, studioTable+".favorite", nil))
query.handleCriterion(ctx, boolCriterionHandler(studioFilter.IgnoreAutoTag, studioTable+".ignore_auto_tag", nil))
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if studioFilter.StashID != nil {
qb.stashIDRepository().join(f, "studio_stash_ids", "studios.id")
stringCriterionHandler(studioFilter.StashID, "studio_stash_ids.stash_id")(ctx, f)
}
}))
query.handleCriterion(ctx, &stashIDCriterionHandler{
c: studioFilter.StashIDEndpoint,
stashIDRepository: qb.stashIDRepository(),
stashIDTableAs: "studio_stash_ids",
parentIDCol: "studios.id",
})
query.handleCriterion(ctx, studioIsMissingCriterionHandler(qb, studioFilter.IsMissing))
query.handleCriterion(ctx, studioSceneCountCriterionHandler(qb, studioFilter.SceneCount))
query.handleCriterion(ctx, studioImageCountCriterionHandler(qb, studioFilter.ImageCount))
query.handleCriterion(ctx, studioGalleryCountCriterionHandler(qb, studioFilter.GalleryCount))
query.handleCriterion(ctx, studioParentCriterionHandler(qb, studioFilter.Parents))
query.handleCriterion(ctx, studioAliasCriterionHandler(qb, studioFilter.Aliases))
query.handleCriterion(ctx, studioChildCountCriterionHandler(qb, studioFilter.ChildCount))
query.handleCriterion(ctx, timestampCriterionHandler(studioFilter.CreatedAt, studioTable+".created_at"))
query.handleCriterion(ctx, timestampCriterionHandler(studioFilter.UpdatedAt, studioTable+".updated_at"))
return query
}
func (qb *StudioStore) makeQuery(ctx context.Context, studioFilter *models.StudioFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
if studioFilter == nil {
studioFilter = &models.StudioFilterType{}
@@ -537,7 +492,7 @@ func (qb *StudioStore) makeQuery(ctx context.Context, studioFilter *models.Studi
findFilter = &models.FindFilterType{}
}
query := qb.newQuery()
query := studioRepository.newQuery()
distinctIDs(&query, studioTable)
if q := findFilter.Q; q != nil && *q != "" {
@@ -546,10 +501,9 @@ func (qb *StudioStore) makeQuery(ctx context.Context, studioFilter *models.Studi
query.parseQueryString(searchColumns, *q)
}
if err := qb.validateFilter(studioFilter); err != nil {
return nil, err
}
filter := qb.makeFilter(ctx, studioFilter)
filter := filterBuilderFromHandler(ctx, &studioFilterHandler{
studioFilter: studioFilter,
})
if err := query.addFilter(filter); err != nil {
return nil, err
@@ -584,93 +538,6 @@ func (qb *StudioStore) Query(ctx context.Context, studioFilter *models.StudioFil
return studios, countResult, nil
}
func studioIsMissingCriterionHandler(qb *StudioStore, isMissing *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if isMissing != nil && *isMissing != "" {
switch *isMissing {
case "image":
f.addWhere("studios.image_blob IS NULL")
case "stash_id":
qb.stashIDRepository().join(f, "studio_stash_ids", "studios.id")
f.addWhere("studio_stash_ids.studio_id IS NULL")
default:
f.addWhere("(studios." + *isMissing + " IS NULL OR TRIM(studios." + *isMissing + ") = '')")
}
}
}
}
func studioSceneCountCriterionHandler(qb *StudioStore, sceneCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if sceneCount != nil {
f.addLeftJoin("scenes", "", "scenes.studio_id = studios.id")
clause, args := getIntCriterionWhereClause("count(distinct scenes.id)", *sceneCount)
f.addHaving(clause, args...)
}
}
}
func studioImageCountCriterionHandler(qb *StudioStore, imageCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if imageCount != nil {
f.addLeftJoin("images", "", "images.studio_id = studios.id")
clause, args := getIntCriterionWhereClause("count(distinct images.id)", *imageCount)
f.addHaving(clause, args...)
}
}
}
func studioGalleryCountCriterionHandler(qb *StudioStore, galleryCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if galleryCount != nil {
f.addLeftJoin("galleries", "", "galleries.studio_id = studios.id")
clause, args := getIntCriterionWhereClause("count(distinct galleries.id)", *galleryCount)
f.addHaving(clause, args...)
}
}
}
func studioParentCriterionHandler(qb *StudioStore, parents *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) {
f.addLeftJoin("studios", "parent_studio", "parent_studio.id = studios.parent_id")
}
h := multiCriterionHandlerBuilder{
primaryTable: studioTable,
foreignTable: "parent_studio",
joinTable: "",
primaryFK: studioIDColumn,
foreignFK: "parent_id",
addJoinsFunc: addJoinsFunc,
}
return h.handler(parents)
}
func studioAliasCriterionHandler(qb *StudioStore, alias *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{
joinTable: studioAliasesTable,
stringColumn: studioAliasColumn,
addJoinTable: func(f *filterBuilder) {
studiosAliasesTableMgr.join(f, "", "studios.id")
},
}
return h.handler(alias)
}
func studioChildCountCriterionHandler(qb *StudioStore, childCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if childCount != nil {
f.addLeftJoin("studios", "children_count", "children_count.parent_id = studios.id")
clause, args := getIntCriterionWhereClause("count(distinct children_count.id)", *childCount)
f.addHaving(clause, args...)
}
}
}
var studioSortOptions = sortOptions{
"child_count",
"created_at",
@@ -735,16 +602,6 @@ func (qb *StudioStore) destroyImage(ctx context.Context, studioID int) error {
return qb.blobJoinQueryBuilder.DestroyImage(ctx, studioID, studioImageBlobColumn)
}
func (qb *StudioStore) stashIDRepository() *stashIDRepository {
return &stashIDRepository{
repository{
tx: qb.tx,
tableName: "studio_stash_ids",
idColumn: studioIDColumn,
},
}
}
func (qb *StudioStore) GetStashIDs(ctx context.Context, studioID int) ([]models.StashID, error) {
return studiosStashIDsTableMgr.get(ctx, studioID)
}

200
pkg/sqlite/studio_filter.go Normal file
View File

@@ -0,0 +1,200 @@
package sqlite
import (
"context"
"github.com/stashapp/stash/pkg/models"
)
type studioFilterHandler struct {
studioFilter *models.StudioFilterType
}
func (qb *studioFilterHandler) validate() error {
studioFilter := qb.studioFilter
if studioFilter == nil {
return nil
}
if err := validateFilterCombination(studioFilter.OperatorFilter); err != nil {
return err
}
if subFilter := studioFilter.SubFilter(); subFilter != nil {
sqb := &studioFilterHandler{studioFilter: subFilter}
if err := sqb.validate(); err != nil {
return err
}
}
return nil
}
func (qb *studioFilterHandler) handle(ctx context.Context, f *filterBuilder) {
studioFilter := qb.studioFilter
if studioFilter == nil {
return
}
if err := qb.validate(); err != nil {
f.setError(err)
return
}
sf := studioFilter.SubFilter()
if sf != nil {
sub := &studioFilterHandler{sf}
handleSubFilter(ctx, sub, f, studioFilter.OperatorFilter)
}
f.handleCriterion(ctx, qb.criterionHandler())
}
func (qb *studioFilterHandler) criterionHandler() criterionHandler {
studioFilter := qb.studioFilter
return compoundHandler{
stringCriterionHandler(studioFilter.Name, studioTable+".name"),
stringCriterionHandler(studioFilter.Details, studioTable+".details"),
stringCriterionHandler(studioFilter.URL, studioTable+".url"),
intCriterionHandler(studioFilter.Rating100, studioTable+".rating", nil),
boolCriterionHandler(studioFilter.Favorite, studioTable+".favorite", nil),
boolCriterionHandler(studioFilter.IgnoreAutoTag, studioTable+".ignore_auto_tag", nil),
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if studioFilter.StashID != nil {
studioRepository.stashIDs.join(f, "studio_stash_ids", "studios.id")
stringCriterionHandler(studioFilter.StashID, "studio_stash_ids.stash_id")(ctx, f)
}
}),
&stashIDCriterionHandler{
c: studioFilter.StashIDEndpoint,
stashIDRepository: &studioRepository.stashIDs,
stashIDTableAs: "studio_stash_ids",
parentIDCol: "studios.id",
},
qb.isMissingCriterionHandler(studioFilter.IsMissing),
qb.sceneCountCriterionHandler(studioFilter.SceneCount),
qb.imageCountCriterionHandler(studioFilter.ImageCount),
qb.galleryCountCriterionHandler(studioFilter.GalleryCount),
qb.parentCriterionHandler(studioFilter.Parents),
qb.aliasCriterionHandler(studioFilter.Aliases),
qb.childCountCriterionHandler(studioFilter.ChildCount),
&timestampCriterionHandler{studioFilter.CreatedAt, studioTable + ".created_at", nil},
&timestampCriterionHandler{studioFilter.UpdatedAt, studioTable + ".updated_at", nil},
&relatedFilterHandler{
relatedIDCol: "scenes.id",
relatedRepo: sceneRepository.repository,
relatedHandler: &sceneFilterHandler{studioFilter.ScenesFilter},
joinFn: func(f *filterBuilder) {
sceneRepository.innerJoin(f, "", "studios.id")
},
},
&relatedFilterHandler{
relatedIDCol: "images.id",
relatedRepo: imageRepository.repository,
relatedHandler: &imageFilterHandler{studioFilter.ImagesFilter},
joinFn: func(f *filterBuilder) {
studioRepository.images.innerJoin(f, "", "studios.id")
},
},
&relatedFilterHandler{
relatedIDCol: "galleries.id",
relatedRepo: galleryRepository.repository,
relatedHandler: &galleryFilterHandler{studioFilter.GalleriesFilter},
joinFn: func(f *filterBuilder) {
studioRepository.galleries.innerJoin(f, "", "studios.id")
},
},
}
}
func (qb *studioFilterHandler) isMissingCriterionHandler(isMissing *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if isMissing != nil && *isMissing != "" {
switch *isMissing {
case "image":
f.addWhere("studios.image_blob IS NULL")
case "stash_id":
studioRepository.stashIDs.join(f, "studio_stash_ids", "studios.id")
f.addWhere("studio_stash_ids.studio_id IS NULL")
default:
f.addWhere("(studios." + *isMissing + " IS NULL OR TRIM(studios." + *isMissing + ") = '')")
}
}
}
}
func (qb *studioFilterHandler) sceneCountCriterionHandler(sceneCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if sceneCount != nil {
f.addLeftJoin("scenes", "", "scenes.studio_id = studios.id")
clause, args := getIntCriterionWhereClause("count(distinct scenes.id)", *sceneCount)
f.addHaving(clause, args...)
}
}
}
func (qb *studioFilterHandler) imageCountCriterionHandler(imageCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if imageCount != nil {
f.addLeftJoin("images", "", "images.studio_id = studios.id")
clause, args := getIntCriterionWhereClause("count(distinct images.id)", *imageCount)
f.addHaving(clause, args...)
}
}
}
func (qb *studioFilterHandler) galleryCountCriterionHandler(galleryCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if galleryCount != nil {
f.addLeftJoin("galleries", "", "galleries.studio_id = studios.id")
clause, args := getIntCriterionWhereClause("count(distinct galleries.id)", *galleryCount)
f.addHaving(clause, args...)
}
}
}
func (qb *studioFilterHandler) parentCriterionHandler(parents *models.MultiCriterionInput) criterionHandlerFunc {
addJoinsFunc := func(f *filterBuilder) {
f.addLeftJoin("studios", "parent_studio", "parent_studio.id = studios.parent_id")
}
h := multiCriterionHandlerBuilder{
primaryTable: studioTable,
foreignTable: "parent_studio",
joinTable: "",
primaryFK: studioIDColumn,
foreignFK: "parent_id",
addJoinsFunc: addJoinsFunc,
}
return h.handler(parents)
}
func (qb *studioFilterHandler) aliasCriterionHandler(alias *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{
joinTable: studioAliasesTable,
stringColumn: studioAliasColumn,
addJoinTable: func(f *filterBuilder) {
studiosAliasesTableMgr.join(f, "", "studios.id")
},
}
return h.handler(alias)
}
func (qb *studioFilterHandler) childCountCriterionHandler(childCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if childCount != nil {
f.addLeftJoin("studios", "children_count", "children_count.parent_id = studios.id")
clause, args := getIntCriterionWhereClause("count(distinct children_count.id)", *childCount)
f.addHaving(clause, args...)
}
}
}

View File

@@ -59,10 +59,12 @@ func TestStudioQueryNameOr(t *testing.T) {
Value: studio1Name,
Modifier: models.CriterionModifierEquals,
},
Or: &models.StudioFilterType{
Name: &models.StringCriterionInput{
Value: studio2Name,
Modifier: models.CriterionModifierEquals,
OperatorFilter: models.OperatorFilter[models.StudioFilterType]{
Or: &models.StudioFilterType{
Name: &models.StringCriterionInput{
Value: studio2Name,
Modifier: models.CriterionModifierEquals,
},
},
},
}
@@ -90,10 +92,12 @@ func TestStudioQueryNameAndUrl(t *testing.T) {
Value: studioName,
Modifier: models.CriterionModifierEquals,
},
And: &models.StudioFilterType{
URL: &models.StringCriterionInput{
Value: studioUrl,
Modifier: models.CriterionModifierEquals,
OperatorFilter: models.OperatorFilter[models.StudioFilterType]{
And: &models.StudioFilterType{
URL: &models.StringCriterionInput{
Value: studioUrl,
Modifier: models.CriterionModifierEquals,
},
},
},
}
@@ -128,8 +132,10 @@ func TestStudioQueryNameNotUrl(t *testing.T) {
studioFilter := models.StudioFilterType{
Name: &nameCriterion,
Not: &models.StudioFilterType{
URL: &urlCriterion,
OperatorFilter: models.OperatorFilter[models.StudioFilterType]{
Not: &models.StudioFilterType{
URL: &urlCriterion,
},
},
}
@@ -160,8 +166,10 @@ func TestStudioIllegalQuery(t *testing.T) {
}
studioFilter := &models.StudioFilterType{
And: &subFilter,
Or: &subFilter,
OperatorFilter: models.OperatorFilter[models.StudioFilterType]{
And: &subFilter,
Or: &subFilter,
},
}
withTxn(func(ctx context.Context) error {

View File

@@ -193,8 +193,7 @@ func (t *joinTable) insertJoins(ctx context.Context, id int, foreignIDs []int) e
// ignore duplicates
q := fmt.Sprintf("INSERT INTO %s (%s, %s) VALUES (?, ?) ON CONFLICT (%[2]s, %s) DO NOTHING", t.table.table.GetTable(), t.idColumn.GetCol(), t.fkColumn.GetCol())
tx := dbWrapper{}
stmt, err := tx.Prepare(ctx, q)
stmt, err := dbWrapper.Prepare(ctx, q)
if err != nil {
return err
}
@@ -204,7 +203,7 @@ func (t *joinTable) insertJoins(ctx context.Context, id int, foreignIDs []int) e
foreignIDs = sliceutil.AppendUniques(nil, foreignIDs)
for _, fk := range foreignIDs {
if _, err := tx.ExecStmt(ctx, stmt, id, fk); err != nil {
if _, err := dbWrapper.ExecStmt(ctx, stmt, id, fk); err != nil {
return err
}
}
@@ -1077,8 +1076,7 @@ func queryFunc(ctx context.Context, query *goqu.SelectDataset, single bool, f fu
return err
}
wrapper := dbWrapper{}
rows, err := wrapper.QueryxContext(ctx, q, args...)
rows, err := dbWrapper.QueryxContext(ctx, q, args...)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("querying `%s` [%v]: %w", q, args, err)
@@ -1107,8 +1105,7 @@ func querySimple(ctx context.Context, query *goqu.SelectDataset, out interface{}
return err
}
wrapper := dbWrapper{}
rows, err := wrapper.QueryxContext(ctx, q, args...)
rows, err := dbWrapper.QueryxContext(ctx, q, args...)
if err != nil {
return fmt.Errorf("querying `%s` [%v]: %w", q, args, err)
}

View File

@@ -90,8 +90,57 @@ func (r *tagRowRecord) fromPartial(o models.TagPartial) {
r.setTimestamp("updated_at", o.UpdatedAt)
}
type TagStore struct {
type tagRepositoryType struct {
repository
aliases stringRepository
scenes joinRepository
images joinRepository
galleries joinRepository
}
var (
tagRepository = tagRepositoryType{
repository: repository{
tableName: tagTable,
idColumn: idColumn,
},
aliases: stringRepository{
repository: repository{
tableName: tagAliasesTable,
idColumn: tagIDColumn,
},
stringColumn: tagAliasColumn,
},
scenes: joinRepository{
repository: repository{
tableName: scenesTagsTable,
idColumn: tagIDColumn,
},
fkColumn: sceneIDColumn,
foreignTable: sceneTable,
},
images: joinRepository{
repository: repository{
tableName: imagesTagsTable,
idColumn: tagIDColumn,
},
fkColumn: imageIDColumn,
foreignTable: imageTable,
},
galleries: joinRepository{
repository: repository{
tableName: galleriesTagsTable,
idColumn: tagIDColumn,
},
fkColumn: galleryIDColumn,
foreignTable: galleryTable,
},
}
)
type TagStore struct {
blobJoinQueryBuilder
tableMgr *table
@@ -99,10 +148,6 @@ type TagStore struct {
func NewTagStore(blobStore *BlobStore) *TagStore {
return &TagStore{
repository: repository{
tableName: tagTable,
idColumn: idColumn,
},
blobJoinQueryBuilder: blobJoinQueryBuilder{
blobStore: blobStore,
joinTable: tagTable,
@@ -176,7 +221,7 @@ func (qb *TagStore) Destroy(ctx context.Context, id int) error {
// cannot unset primary_tag_id in scene_markers because it is not nullable
countQuery := "SELECT COUNT(*) as count FROM scene_markers where primary_tag_id = ?"
args := []interface{}{id}
primaryMarkers, err := qb.runCountQuery(ctx, countQuery, args)
primaryMarkers, err := tagRepository.runCountQuery(ctx, countQuery, args)
if err != nil {
return err
}
@@ -185,7 +230,7 @@ func (qb *TagStore) Destroy(ctx context.Context, id int) error {
return errors.New("cannot delete tag used as a primary tag in scene markers")
}
return qb.destroyExisting(ctx, []int{id})
return tagRepository.destroyExisting(ctx, []int{id})
}
// returns nil, nil if not found
@@ -455,73 +500,6 @@ func (qb *TagStore) QueryForAutoTag(ctx context.Context, words []string) ([]*mod
return qb.queryTags(ctx, query+" WHERE "+where, args)
}
func (qb *TagStore) validateFilter(tagFilter *models.TagFilterType) error {
const and = "AND"
const or = "OR"
const not = "NOT"
if tagFilter.And != nil {
if tagFilter.Or != nil {
return illegalFilterCombination(and, or)
}
if tagFilter.Not != nil {
return illegalFilterCombination(and, not)
}
return qb.validateFilter(tagFilter.And)
}
if tagFilter.Or != nil {
if tagFilter.Not != nil {
return illegalFilterCombination(or, not)
}
return qb.validateFilter(tagFilter.Or)
}
if tagFilter.Not != nil {
return qb.validateFilter(tagFilter.Not)
}
return nil
}
func (qb *TagStore) makeFilter(ctx context.Context, tagFilter *models.TagFilterType) *filterBuilder {
query := &filterBuilder{}
if tagFilter.And != nil {
query.and(qb.makeFilter(ctx, tagFilter.And))
}
if tagFilter.Or != nil {
query.or(qb.makeFilter(ctx, tagFilter.Or))
}
if tagFilter.Not != nil {
query.not(qb.makeFilter(ctx, tagFilter.Not))
}
query.handleCriterion(ctx, stringCriterionHandler(tagFilter.Name, tagTable+".name"))
query.handleCriterion(ctx, tagAliasCriterionHandler(qb, tagFilter.Aliases))
query.handleCriterion(ctx, boolCriterionHandler(tagFilter.Favorite, tagTable+".favorite", nil))
query.handleCriterion(ctx, stringCriterionHandler(tagFilter.Description, tagTable+".description"))
query.handleCriterion(ctx, boolCriterionHandler(tagFilter.IgnoreAutoTag, tagTable+".ignore_auto_tag", nil))
query.handleCriterion(ctx, tagIsMissingCriterionHandler(qb, tagFilter.IsMissing))
query.handleCriterion(ctx, tagSceneCountCriterionHandler(qb, tagFilter.SceneCount))
query.handleCriterion(ctx, tagImageCountCriterionHandler(qb, tagFilter.ImageCount))
query.handleCriterion(ctx, tagGalleryCountCriterionHandler(qb, tagFilter.GalleryCount))
query.handleCriterion(ctx, tagPerformerCountCriterionHandler(qb, tagFilter.PerformerCount))
query.handleCriterion(ctx, tagMarkerCountCriterionHandler(qb, tagFilter.MarkerCount))
query.handleCriterion(ctx, tagParentsCriterionHandler(qb, tagFilter.Parents))
query.handleCriterion(ctx, tagChildrenCriterionHandler(qb, tagFilter.Children))
query.handleCriterion(ctx, tagParentCountCriterionHandler(qb, tagFilter.ParentCount))
query.handleCriterion(ctx, tagChildCountCriterionHandler(qb, tagFilter.ChildCount))
query.handleCriterion(ctx, timestampCriterionHandler(tagFilter.CreatedAt, "tags.created_at"))
query.handleCriterion(ctx, timestampCriterionHandler(tagFilter.UpdatedAt, "tags.updated_at"))
return query
}
func (qb *TagStore) Query(ctx context.Context, tagFilter *models.TagFilterType, findFilter *models.FindFilterType) ([]*models.Tag, int, error) {
if tagFilter == nil {
tagFilter = &models.TagFilterType{}
@@ -530,7 +508,7 @@ func (qb *TagStore) Query(ctx context.Context, tagFilter *models.TagFilterType,
findFilter = &models.FindFilterType{}
}
query := qb.newQuery()
query := tagRepository.newQuery()
distinctIDs(&query, tagTable)
if q := findFilter.Q; q != nil && *q != "" {
@@ -539,10 +517,9 @@ func (qb *TagStore) Query(ctx context.Context, tagFilter *models.TagFilterType,
query.parseQueryString(searchColumns, *q)
}
if err := qb.validateFilter(tagFilter); err != nil {
return nil, 0, err
}
filter := qb.makeFilter(ctx, tagFilter)
filter := filterBuilderFromHandler(ctx, &tagFilterHandler{
tagFilter: tagFilter,
})
if err := query.addFilter(filter); err != nil {
return nil, 0, err
@@ -567,297 +544,6 @@ func (qb *TagStore) Query(ctx context.Context, tagFilter *models.TagFilterType,
return tags, countResult, nil
}
func tagAliasCriterionHandler(qb *TagStore, alias *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{
joinTable: tagAliasesTable,
stringColumn: tagAliasColumn,
addJoinTable: func(f *filterBuilder) {
qb.aliasRepository().join(f, "", "tags.id")
},
}
return h.handler(alias)
}
func tagIsMissingCriterionHandler(qb *TagStore, isMissing *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if isMissing != nil && *isMissing != "" {
switch *isMissing {
case "image":
f.addWhere("tags.image_blob IS NULL")
default:
f.addWhere("(tags." + *isMissing + " IS NULL OR TRIM(tags." + *isMissing + ") = '')")
}
}
}
}
func tagSceneCountCriterionHandler(qb *TagStore, sceneCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if sceneCount != nil {
f.addLeftJoin("scenes_tags", "", "scenes_tags.tag_id = tags.id")
clause, args := getIntCriterionWhereClause("count(distinct scenes_tags.scene_id)", *sceneCount)
f.addHaving(clause, args...)
}
}
}
func tagImageCountCriterionHandler(qb *TagStore, imageCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if imageCount != nil {
f.addLeftJoin("images_tags", "", "images_tags.tag_id = tags.id")
clause, args := getIntCriterionWhereClause("count(distinct images_tags.image_id)", *imageCount)
f.addHaving(clause, args...)
}
}
}
func tagGalleryCountCriterionHandler(qb *TagStore, galleryCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if galleryCount != nil {
f.addLeftJoin("galleries_tags", "", "galleries_tags.tag_id = tags.id")
clause, args := getIntCriterionWhereClause("count(distinct galleries_tags.gallery_id)", *galleryCount)
f.addHaving(clause, args...)
}
}
}
func tagPerformerCountCriterionHandler(qb *TagStore, performerCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if performerCount != nil {
f.addLeftJoin("performers_tags", "", "performers_tags.tag_id = tags.id")
clause, args := getIntCriterionWhereClause("count(distinct performers_tags.performer_id)", *performerCount)
f.addHaving(clause, args...)
}
}
}
func tagMarkerCountCriterionHandler(qb *TagStore, markerCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if markerCount != nil {
f.addLeftJoin("scene_markers_tags", "", "scene_markers_tags.tag_id = tags.id")
f.addLeftJoin("scene_markers", "", "scene_markers_tags.scene_marker_id = scene_markers.id OR scene_markers.primary_tag_id = tags.id")
clause, args := getIntCriterionWhereClause("count(distinct scene_markers.id)", *markerCount)
f.addHaving(clause, args...)
}
}
}
func tagParentsCriterionHandler(qb *TagStore, criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if criterion != nil {
tags := criterion.CombineExcludes()
// validate the modifier
switch tags.Modifier {
case models.CriterionModifierIncludesAll, models.CriterionModifierIncludes, models.CriterionModifierExcludes, models.CriterionModifierIsNull, models.CriterionModifierNotNull:
// valid
default:
f.setError(fmt.Errorf("invalid modifier %s for tag parent/children", criterion.Modifier))
}
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
var notClause string
if tags.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addLeftJoin("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 && len(tags.Excludes) == 0 {
return
}
if len(tags.Value) > 0 {
var args []interface{}
for _, val := range tags.Value {
args = append(args, val)
}
depthVal := 0
if tags.Depth != nil {
depthVal = *tags.Depth
}
var depthCondition string
if depthVal != -1 {
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
}
query := `parents AS (
SELECT parent_id AS root_id, child_id AS item_id, 0 AS depth FROM tags_relations WHERE parent_id IN` + getInBinding(len(tags.Value)) + `
UNION
SELECT root_id, child_id, depth + 1 FROM tags_relations INNER JOIN parents ON item_id = parent_id ` + depthCondition + `
)`
f.addRecursiveWith(query, args...)
f.addLeftJoin("parents", "", "parents.item_id = tags.id")
addHierarchicalConditionClauses(f, tags, "parents", "root_id")
}
if len(tags.Excludes) > 0 {
var args []interface{}
for _, val := range tags.Excludes {
args = append(args, val)
}
depthVal := 0
if tags.Depth != nil {
depthVal = *tags.Depth
}
var depthCondition string
if depthVal != -1 {
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
}
query := `parents2 AS (
SELECT parent_id AS root_id, child_id AS item_id, 0 AS depth FROM tags_relations WHERE parent_id IN` + getInBinding(len(tags.Excludes)) + `
UNION
SELECT root_id, child_id, depth + 1 FROM tags_relations INNER JOIN parents2 ON item_id = parent_id ` + depthCondition + `
)`
f.addRecursiveWith(query, args...)
f.addLeftJoin("parents2", "", "parents2.item_id = tags.id")
addHierarchicalConditionClauses(f, models.HierarchicalMultiCriterionInput{
Value: tags.Excludes,
Depth: tags.Depth,
Modifier: models.CriterionModifierExcludes,
}, "parents2", "root_id")
}
}
}
}
func tagChildrenCriterionHandler(qb *TagStore, criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if criterion != nil {
tags := criterion.CombineExcludes()
// validate the modifier
switch tags.Modifier {
case models.CriterionModifierIncludesAll, models.CriterionModifierIncludes, models.CriterionModifierExcludes, models.CriterionModifierIsNull, models.CriterionModifierNotNull:
// valid
default:
f.setError(fmt.Errorf("invalid modifier %s for tag parent/children", criterion.Modifier))
}
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
var notClause string
if tags.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addLeftJoin("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 && len(tags.Excludes) == 0 {
return
}
if len(tags.Value) > 0 {
var args []interface{}
for _, val := range tags.Value {
args = append(args, val)
}
depthVal := 0
if tags.Depth != nil {
depthVal = *tags.Depth
}
var depthCondition string
if depthVal != -1 {
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
}
query := `children AS (
SELECT child_id AS root_id, parent_id AS item_id, 0 AS depth FROM tags_relations WHERE child_id IN` + getInBinding(len(tags.Value)) + `
UNION
SELECT root_id, parent_id, depth + 1 FROM tags_relations INNER JOIN children ON item_id = child_id ` + depthCondition + `
)`
f.addRecursiveWith(query, args...)
f.addLeftJoin("children", "", "children.item_id = tags.id")
addHierarchicalConditionClauses(f, tags, "children", "root_id")
}
if len(tags.Excludes) > 0 {
var args []interface{}
for _, val := range tags.Excludes {
args = append(args, val)
}
depthVal := 0
if tags.Depth != nil {
depthVal = *tags.Depth
}
var depthCondition string
if depthVal != -1 {
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
}
query := `children2 AS (
SELECT child_id AS root_id, parent_id AS item_id, 0 AS depth FROM tags_relations WHERE child_id IN` + getInBinding(len(tags.Excludes)) + `
UNION
SELECT root_id, parent_id, depth + 1 FROM tags_relations INNER JOIN children2 ON item_id = child_id ` + depthCondition + `
)`
f.addRecursiveWith(query, args...)
f.addLeftJoin("children2", "", "children2.item_id = tags.id")
addHierarchicalConditionClauses(f, models.HierarchicalMultiCriterionInput{
Value: tags.Excludes,
Depth: tags.Depth,
Modifier: models.CriterionModifierExcludes,
}, "children2", "root_id")
}
}
}
}
func tagParentCountCriterionHandler(qb *TagStore, parentCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if parentCount != nil {
f.addLeftJoin("tags_relations", "parents_count", "parents_count.child_id = tags.id")
clause, args := getIntCriterionWhereClause("count(distinct parents_count.parent_id)", *parentCount)
f.addHaving(clause, args...)
}
}
}
func tagChildCountCriterionHandler(qb *TagStore, childCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if childCount != nil {
f.addLeftJoin("tags_relations", "children_count", "children_count.parent_id = tags.id")
clause, args := getIntCriterionWhereClause("count(distinct children_count.child_id)", *childCount)
f.addHaving(clause, args...)
}
}
}
var tagSortOptions = sortOptions{
"created_at",
"galleries_count",
@@ -915,7 +601,7 @@ func (qb *TagStore) getTagSort(query *queryBuilder, findFilter *models.FindFilte
func (qb *TagStore) queryTags(ctx context.Context, query string, args []interface{}) ([]*models.Tag, error) {
const single = false
var ret []*models.Tag
if err := qb.queryFunc(ctx, query, args, single, func(r *sqlx.Rows) error {
if err := tagRepository.queryFunc(ctx, query, args, single, func(r *sqlx.Rows) error {
var f tagRow
if err := r.StructScan(&f); err != nil {
return err
@@ -935,7 +621,7 @@ func (qb *TagStore) queryTags(ctx context.Context, query string, args []interfac
func (qb *TagStore) queryTagPaths(ctx context.Context, query string, args []interface{}) ([]*models.TagPath, error) {
const single = false
var ret []*models.TagPath
if err := qb.queryFunc(ctx, query, args, single, func(r *sqlx.Rows) error {
if err := tagRepository.queryFunc(ctx, query, args, single, func(r *sqlx.Rows) error {
var f tagPathRow
if err := r.StructScan(&f); err != nil {
return err
@@ -968,23 +654,12 @@ func (qb *TagStore) destroyImage(ctx context.Context, tagID int) error {
return qb.blobJoinQueryBuilder.DestroyImage(ctx, tagID, tagImageBlobColumn)
}
func (qb *TagStore) aliasRepository() *stringRepository {
return &stringRepository{
repository: repository{
tx: qb.tx,
tableName: tagAliasesTable,
idColumn: tagIDColumn,
},
stringColumn: tagAliasColumn,
}
}
func (qb *TagStore) GetAliases(ctx context.Context, tagID int) ([]string, error) {
return qb.aliasRepository().get(ctx, tagID)
return tagRepository.aliases.get(ctx, tagID)
}
func (qb *TagStore) UpdateAliases(ctx context.Context, tagID int, aliases []string) error {
return qb.aliasRepository().replace(ctx, tagID, aliases)
return tagRepository.aliases.replace(ctx, tagID, aliases)
}
func (qb *TagStore) Merge(ctx context.Context, source []int, destination int) error {
@@ -1015,7 +690,7 @@ func (qb *TagStore) Merge(ctx context.Context, source []int, destination int) er
args = append(args, destination)
for table, idColumn := range tagTables {
_, err := qb.tx.Exec(ctx, `UPDATE OR IGNORE `+table+`
_, err := dbWrapper.Exec(ctx, `UPDATE OR IGNORE `+table+`
SET tag_id = ?
WHERE tag_id IN `+inBinding+`
AND NOT EXISTS(SELECT 1 FROM `+table+` o WHERE o.`+idColumn+` = `+table+`.`+idColumn+` AND o.tag_id = ?)`,
@@ -1026,22 +701,22 @@ AND NOT EXISTS(SELECT 1 FROM `+table+` o WHERE o.`+idColumn+` = `+table+`.`+idCo
}
// delete source tag ids from the table where they couldn't be set
if _, err := qb.tx.Exec(ctx, `DELETE FROM `+table+` WHERE tag_id IN `+inBinding, srcArgs...); err != nil {
if _, err := dbWrapper.Exec(ctx, `DELETE FROM `+table+` WHERE tag_id IN `+inBinding, srcArgs...); err != nil {
return err
}
}
_, err := qb.tx.Exec(ctx, "UPDATE "+sceneMarkerTable+" SET primary_tag_id = ? WHERE primary_tag_id IN "+inBinding, args...)
_, err := dbWrapper.Exec(ctx, "UPDATE "+sceneMarkerTable+" SET primary_tag_id = ? WHERE primary_tag_id IN "+inBinding, args...)
if err != nil {
return err
}
_, err = qb.tx.Exec(ctx, "INSERT INTO "+tagAliasesTable+" (tag_id, alias) SELECT ?, name FROM "+tagTable+" WHERE id IN "+inBinding, args...)
_, err = dbWrapper.Exec(ctx, "INSERT INTO "+tagAliasesTable+" (tag_id, alias) SELECT ?, name FROM "+tagTable+" WHERE id IN "+inBinding, args...)
if err != nil {
return err
}
_, err = qb.tx.Exec(ctx, "UPDATE "+tagAliasesTable+" SET tag_id = ? WHERE tag_id IN "+inBinding, args...)
_, err = dbWrapper.Exec(ctx, "UPDATE "+tagAliasesTable+" SET tag_id = ? WHERE tag_id IN "+inBinding, args...)
if err != nil {
return err
}
@@ -1057,8 +732,7 @@ AND NOT EXISTS(SELECT 1 FROM `+table+` o WHERE o.`+idColumn+` = `+table+`.`+idCo
}
func (qb *TagStore) UpdateParentTags(ctx context.Context, tagID int, parentIDs []int) error {
tx := qb.tx
if _, err := tx.Exec(ctx, "DELETE FROM tags_relations WHERE child_id = ?", tagID); err != nil {
if _, err := dbWrapper.Exec(ctx, "DELETE FROM tags_relations WHERE child_id = ?", tagID); err != nil {
return err
}
@@ -1071,7 +745,7 @@ func (qb *TagStore) UpdateParentTags(ctx context.Context, tagID int, parentIDs [
}
query := "INSERT INTO tags_relations (parent_id, child_id) VALUES " + strings.Join(values, ", ")
if _, err := tx.Exec(ctx, query, args...); err != nil {
if _, err := dbWrapper.Exec(ctx, query, args...); err != nil {
return err
}
}
@@ -1080,8 +754,7 @@ func (qb *TagStore) UpdateParentTags(ctx context.Context, tagID int, parentIDs [
}
func (qb *TagStore) UpdateChildTags(ctx context.Context, tagID int, childIDs []int) error {
tx := qb.tx
if _, err := tx.Exec(ctx, "DELETE FROM tags_relations WHERE parent_id = ?", tagID); err != nil {
if _, err := dbWrapper.Exec(ctx, "DELETE FROM tags_relations WHERE parent_id = ?", tagID); err != nil {
return err
}
@@ -1094,7 +767,7 @@ func (qb *TagStore) UpdateChildTags(ctx context.Context, tagID int, childIDs []i
}
query := "INSERT INTO tags_relations (parent_id, child_id) VALUES " + strings.Join(values, ", ")
if _, err := tx.Exec(ctx, query, args...); err != nil {
if _, err := dbWrapper.Exec(ctx, query, args...); err != nil {
return err
}
}

395
pkg/sqlite/tag_filter.go Normal file
View File

@@ -0,0 +1,395 @@
package sqlite
import (
"context"
"fmt"
"github.com/stashapp/stash/pkg/models"
)
type tagFilterHandler struct {
tagFilter *models.TagFilterType
}
func (qb *tagFilterHandler) validate() error {
tagFilter := qb.tagFilter
if tagFilter == nil {
return nil
}
if err := validateFilterCombination(tagFilter.OperatorFilter); err != nil {
return err
}
if subFilter := tagFilter.SubFilter(); subFilter != nil {
sqb := &tagFilterHandler{tagFilter: subFilter}
if err := sqb.validate(); err != nil {
return err
}
}
return nil
}
func (qb *tagFilterHandler) handle(ctx context.Context, f *filterBuilder) {
tagFilter := qb.tagFilter
if tagFilter == nil {
return
}
if err := qb.validate(); err != nil {
f.setError(err)
return
}
sf := tagFilter.SubFilter()
if sf != nil {
sub := &tagFilterHandler{sf}
handleSubFilter(ctx, sub, f, tagFilter.OperatorFilter)
}
f.handleCriterion(ctx, qb.criterionHandler())
}
func (qb *tagFilterHandler) criterionHandler() criterionHandler {
tagFilter := qb.tagFilter
return compoundHandler{
stringCriterionHandler(tagFilter.Name, tagTable+".name"),
qb.aliasCriterionHandler(tagFilter.Aliases),
boolCriterionHandler(tagFilter.Favorite, tagTable+".favorite", nil),
stringCriterionHandler(tagFilter.Description, tagTable+".description"),
boolCriterionHandler(tagFilter.IgnoreAutoTag, tagTable+".ignore_auto_tag", nil),
qb.isMissingCriterionHandler(tagFilter.IsMissing),
qb.sceneCountCriterionHandler(tagFilter.SceneCount),
qb.imageCountCriterionHandler(tagFilter.ImageCount),
qb.galleryCountCriterionHandler(tagFilter.GalleryCount),
qb.performerCountCriterionHandler(tagFilter.PerformerCount),
qb.markerCountCriterionHandler(tagFilter.MarkerCount),
qb.parentsCriterionHandler(tagFilter.Parents),
qb.childrenCriterionHandler(tagFilter.Children),
qb.parentCountCriterionHandler(tagFilter.ParentCount),
qb.childCountCriterionHandler(tagFilter.ChildCount),
&timestampCriterionHandler{tagFilter.CreatedAt, "tags.created_at", nil},
&timestampCriterionHandler{tagFilter.UpdatedAt, "tags.updated_at", nil},
&relatedFilterHandler{
relatedIDCol: "scenes_tags.scene_id",
relatedRepo: sceneRepository.repository,
relatedHandler: &sceneFilterHandler{tagFilter.ScenesFilter},
joinFn: func(f *filterBuilder) {
tagRepository.scenes.innerJoin(f, "", "tags.id")
},
},
&relatedFilterHandler{
relatedIDCol: "images_tags.image_id",
relatedRepo: imageRepository.repository,
relatedHandler: &imageFilterHandler{tagFilter.ImagesFilter},
joinFn: func(f *filterBuilder) {
tagRepository.images.innerJoin(f, "", "tags.id")
},
},
&relatedFilterHandler{
relatedIDCol: "galleries_tags.gallery_id",
relatedRepo: galleryRepository.repository,
relatedHandler: &galleryFilterHandler{tagFilter.GalleriesFilter},
joinFn: func(f *filterBuilder) {
tagRepository.galleries.innerJoin(f, "", "tags.id")
},
},
}
}
func (qb *tagFilterHandler) aliasCriterionHandler(alias *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{
joinTable: tagAliasesTable,
stringColumn: tagAliasColumn,
addJoinTable: func(f *filterBuilder) {
tagRepository.aliases.join(f, "", "tags.id")
},
}
return h.handler(alias)
}
func (qb *tagFilterHandler) isMissingCriterionHandler(isMissing *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if isMissing != nil && *isMissing != "" {
switch *isMissing {
case "image":
f.addWhere("tags.image_blob IS NULL")
default:
f.addWhere("(tags." + *isMissing + " IS NULL OR TRIM(tags." + *isMissing + ") = '')")
}
}
}
}
func (qb *tagFilterHandler) sceneCountCriterionHandler(sceneCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if sceneCount != nil {
f.addLeftJoin("scenes_tags", "", "scenes_tags.tag_id = tags.id")
clause, args := getIntCriterionWhereClause("count(distinct scenes_tags.scene_id)", *sceneCount)
f.addHaving(clause, args...)
}
}
}
func (qb *tagFilterHandler) imageCountCriterionHandler(imageCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if imageCount != nil {
f.addLeftJoin("images_tags", "", "images_tags.tag_id = tags.id")
clause, args := getIntCriterionWhereClause("count(distinct images_tags.image_id)", *imageCount)
f.addHaving(clause, args...)
}
}
}
func (qb *tagFilterHandler) galleryCountCriterionHandler(galleryCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if galleryCount != nil {
f.addLeftJoin("galleries_tags", "", "galleries_tags.tag_id = tags.id")
clause, args := getIntCriterionWhereClause("count(distinct galleries_tags.gallery_id)", *galleryCount)
f.addHaving(clause, args...)
}
}
}
func (qb *tagFilterHandler) performerCountCriterionHandler(performerCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if performerCount != nil {
f.addLeftJoin("performers_tags", "", "performers_tags.tag_id = tags.id")
clause, args := getIntCriterionWhereClause("count(distinct performers_tags.performer_id)", *performerCount)
f.addHaving(clause, args...)
}
}
}
func (qb *tagFilterHandler) markerCountCriterionHandler(markerCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if markerCount != nil {
f.addLeftJoin("scene_markers_tags", "", "scene_markers_tags.tag_id = tags.id")
f.addLeftJoin("scene_markers", "", "scene_markers_tags.scene_marker_id = scene_markers.id OR scene_markers.primary_tag_id = tags.id")
clause, args := getIntCriterionWhereClause("count(distinct scene_markers.id)", *markerCount)
f.addHaving(clause, args...)
}
}
}
func (qb *tagFilterHandler) parentsCriterionHandler(criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if criterion != nil {
tags := criterion.CombineExcludes()
// validate the modifier
switch tags.Modifier {
case models.CriterionModifierIncludesAll, models.CriterionModifierIncludes, models.CriterionModifierExcludes, models.CriterionModifierIsNull, models.CriterionModifierNotNull:
// valid
default:
f.setError(fmt.Errorf("invalid modifier %s for tag parent/children", criterion.Modifier))
}
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
var notClause string
if tags.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addLeftJoin("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 && len(tags.Excludes) == 0 {
return
}
if len(tags.Value) > 0 {
var args []interface{}
for _, val := range tags.Value {
args = append(args, val)
}
depthVal := 0
if tags.Depth != nil {
depthVal = *tags.Depth
}
var depthCondition string
if depthVal != -1 {
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
}
query := `parents AS (
SELECT parent_id AS root_id, child_id AS item_id, 0 AS depth FROM tags_relations WHERE parent_id IN` + getInBinding(len(tags.Value)) + `
UNION
SELECT root_id, child_id, depth + 1 FROM tags_relations INNER JOIN parents ON item_id = parent_id ` + depthCondition + `
)`
f.addRecursiveWith(query, args...)
f.addLeftJoin("parents", "", "parents.item_id = tags.id")
addHierarchicalConditionClauses(f, tags, "parents", "root_id")
}
if len(tags.Excludes) > 0 {
var args []interface{}
for _, val := range tags.Excludes {
args = append(args, val)
}
depthVal := 0
if tags.Depth != nil {
depthVal = *tags.Depth
}
var depthCondition string
if depthVal != -1 {
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
}
query := `parents2 AS (
SELECT parent_id AS root_id, child_id AS item_id, 0 AS depth FROM tags_relations WHERE parent_id IN` + getInBinding(len(tags.Excludes)) + `
UNION
SELECT root_id, child_id, depth + 1 FROM tags_relations INNER JOIN parents2 ON item_id = parent_id ` + depthCondition + `
)`
f.addRecursiveWith(query, args...)
f.addLeftJoin("parents2", "", "parents2.item_id = tags.id")
addHierarchicalConditionClauses(f, models.HierarchicalMultiCriterionInput{
Value: tags.Excludes,
Depth: tags.Depth,
Modifier: models.CriterionModifierExcludes,
}, "parents2", "root_id")
}
}
}
}
func (qb *tagFilterHandler) childrenCriterionHandler(criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if criterion != nil {
tags := criterion.CombineExcludes()
// validate the modifier
switch tags.Modifier {
case models.CriterionModifierIncludesAll, models.CriterionModifierIncludes, models.CriterionModifierExcludes, models.CriterionModifierIsNull, models.CriterionModifierNotNull:
// valid
default:
f.setError(fmt.Errorf("invalid modifier %s for tag parent/children", criterion.Modifier))
}
if tags.Modifier == models.CriterionModifierIsNull || tags.Modifier == models.CriterionModifierNotNull {
var notClause string
if tags.Modifier == models.CriterionModifierNotNull {
notClause = "NOT"
}
f.addLeftJoin("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 && len(tags.Excludes) == 0 {
return
}
if len(tags.Value) > 0 {
var args []interface{}
for _, val := range tags.Value {
args = append(args, val)
}
depthVal := 0
if tags.Depth != nil {
depthVal = *tags.Depth
}
var depthCondition string
if depthVal != -1 {
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
}
query := `children AS (
SELECT child_id AS root_id, parent_id AS item_id, 0 AS depth FROM tags_relations WHERE child_id IN` + getInBinding(len(tags.Value)) + `
UNION
SELECT root_id, parent_id, depth + 1 FROM tags_relations INNER JOIN children ON item_id = child_id ` + depthCondition + `
)`
f.addRecursiveWith(query, args...)
f.addLeftJoin("children", "", "children.item_id = tags.id")
addHierarchicalConditionClauses(f, tags, "children", "root_id")
}
if len(tags.Excludes) > 0 {
var args []interface{}
for _, val := range tags.Excludes {
args = append(args, val)
}
depthVal := 0
if tags.Depth != nil {
depthVal = *tags.Depth
}
var depthCondition string
if depthVal != -1 {
depthCondition = fmt.Sprintf("WHERE depth < %d", depthVal)
}
query := `children2 AS (
SELECT child_id AS root_id, parent_id AS item_id, 0 AS depth FROM tags_relations WHERE child_id IN` + getInBinding(len(tags.Excludes)) + `
UNION
SELECT root_id, parent_id, depth + 1 FROM tags_relations INNER JOIN children2 ON item_id = child_id ` + depthCondition + `
)`
f.addRecursiveWith(query, args...)
f.addLeftJoin("children2", "", "children2.item_id = tags.id")
addHierarchicalConditionClauses(f, models.HierarchicalMultiCriterionInput{
Value: tags.Excludes,
Depth: tags.Depth,
Modifier: models.CriterionModifierExcludes,
}, "children2", "root_id")
}
}
}
}
func (qb *tagFilterHandler) parentCountCriterionHandler(parentCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if parentCount != nil {
f.addLeftJoin("tags_relations", "parents_count", "parents_count.child_id = tags.id")
clause, args := getIntCriterionWhereClause("count(distinct parents_count.parent_id)", *parentCount)
f.addHaving(clause, args...)
}
}
}
func (qb *tagFilterHandler) childCountCriterionHandler(childCount *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if childCount != nil {
f.addLeftJoin("tags_relations", "children_count", "children_count.parent_id = tags.id")
clause, args := getIntCriterionWhereClause("count(distinct children_count.child_id)", *childCount)
f.addHaving(clause, args...)
}
}
}

View File

@@ -35,7 +35,9 @@ func logSQL(start time.Time, query string, args ...interface{}) {
}
}
type dbWrapper struct{}
type dbWrapperType struct{}
var dbWrapper = dbWrapperType{}
func sqlError(err error, sql string, args ...interface{}) error {
if err == nil {
@@ -45,7 +47,7 @@ func sqlError(err error, sql string, args ...interface{}) error {
return fmt.Errorf("error executing `%s` [%v]: %w", sql, args, err)
}
func (*dbWrapper) Get(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
func (*dbWrapperType) Get(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
tx, err := getDBReader(ctx)
if err != nil {
return sqlError(err, query, args...)
@@ -58,7 +60,7 @@ func (*dbWrapper) Get(ctx context.Context, dest interface{}, query string, args
return sqlError(err, query, args...)
}
func (*dbWrapper) Select(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
func (*dbWrapperType) Select(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
tx, err := getDBReader(ctx)
if err != nil {
return sqlError(err, query, args...)
@@ -71,7 +73,7 @@ func (*dbWrapper) Select(ctx context.Context, dest interface{}, query string, ar
return sqlError(err, query, args...)
}
func (*dbWrapper) Queryx(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) {
func (*dbWrapperType) Queryx(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) {
tx, err := getDBReader(ctx)
if err != nil {
return nil, sqlError(err, query, args...)
@@ -84,7 +86,7 @@ func (*dbWrapper) Queryx(ctx context.Context, query string, args ...interface{})
return ret, sqlError(err, query, args...)
}
func (*dbWrapper) QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) {
func (*dbWrapperType) QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) {
tx, err := getDBReader(ctx)
if err != nil {
return nil, sqlError(err, query, args...)
@@ -97,7 +99,7 @@ func (*dbWrapper) QueryxContext(ctx context.Context, query string, args ...inter
return ret, sqlError(err, query, args...)
}
func (*dbWrapper) NamedExec(ctx context.Context, query string, arg interface{}) (sql.Result, error) {
func (*dbWrapperType) NamedExec(ctx context.Context, query string, arg interface{}) (sql.Result, error) {
tx, err := getTx(ctx)
if err != nil {
return nil, sqlError(err, query, arg)
@@ -110,7 +112,7 @@ func (*dbWrapper) NamedExec(ctx context.Context, query string, arg interface{})
return ret, sqlError(err, query, arg)
}
func (*dbWrapper) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
func (*dbWrapperType) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
tx, err := getTx(ctx)
if err != nil {
return nil, sqlError(err, query, args...)
@@ -124,7 +126,7 @@ func (*dbWrapper) Exec(ctx context.Context, query string, args ...interface{}) (
}
// Prepare creates a prepared statement.
func (*dbWrapper) Prepare(ctx context.Context, query string, args ...interface{}) (*stmt, error) {
func (*dbWrapperType) Prepare(ctx context.Context, query string, args ...interface{}) (*stmt, error) {
tx, err := getTx(ctx)
if err != nil {
return nil, sqlError(err, query, args...)
@@ -142,7 +144,7 @@ func (*dbWrapper) Prepare(ctx context.Context, query string, args ...interface{}
}, nil
}
func (*dbWrapper) ExecStmt(ctx context.Context, stmt *stmt, args ...interface{}) (sql.Result, error) {
func (*dbWrapperType) ExecStmt(ctx context.Context, stmt *stmt, args ...interface{}) (sql.Result, error) {
_, err := getTx(ctx)
if err != nil {
return nil, sqlError(err, stmt.query, args...)