[Files Refactor] Performance tuning (#2809)

* Use cache during migration
* Avoid use of query views
* Use FindMany to find related objects
* Log slow queries
* Add folders to generated files
* Use SlimScene for scene queries
* Include filename in migration error message
This commit is contained in:
WithoutPants 2022-08-08 14:24:08 +10:00
parent c825cf5d09
commit 569c3a872a
21 changed files with 417 additions and 157 deletions

View file

@ -4,7 +4,7 @@ query FindScenes($filter: FindFilterType, $scene_filter: SceneFilterType, $scene
filesize filesize
duration duration
scenes { scenes {
...SceneData ...SlimSceneData
} }
} }
} }

View file

@ -147,7 +147,7 @@ func (r *galleryResolver) Date(ctx context.Context, obj *models.Gallery) (*strin
func (r *galleryResolver) Scenes(ctx context.Context, obj *models.Gallery) (ret []*models.Scene, err error) { func (r *galleryResolver) Scenes(ctx context.Context, obj *models.Gallery) (ret []*models.Scene, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error { if err := r.withTxn(ctx, func(ctx context.Context) error {
var err error var err error
ret, err = r.repository.Scene.FindByGalleryID(ctx, obj.ID) ret, err = r.repository.Scene.FindMany(ctx, obj.SceneIDs)
return err return err
}); err != nil { }); err != nil {
return nil, err return nil, err
@ -175,7 +175,7 @@ func (r *galleryResolver) Studio(ctx context.Context, obj *models.Gallery) (ret
func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) (ret []*models.Tag, err error) { func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) (ret []*models.Tag, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error { if err := r.withTxn(ctx, func(ctx context.Context) error {
var err error var err error
ret, err = r.repository.Tag.FindByGalleryID(ctx, obj.ID) ret, err = r.repository.Tag.FindMany(ctx, obj.TagIDs)
return err return err
}); err != nil { }); err != nil {
return nil, err return nil, err
@ -187,7 +187,7 @@ func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) (ret []
func (r *galleryResolver) Performers(ctx context.Context, obj *models.Gallery) (ret []*models.Performer, err error) { func (r *galleryResolver) Performers(ctx context.Context, obj *models.Gallery) (ret []*models.Performer, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error { if err := r.withTxn(ctx, func(ctx context.Context) error {
var err error var err error
ret, err = r.repository.Performer.FindByGalleryID(ctx, obj.ID) ret, err = r.repository.Performer.FindMany(ctx, obj.PerformerIDs)
return err return err
}); err != nil { }); err != nil {
return nil, err return nil, err

View file

@ -164,7 +164,7 @@ func (r *sceneResolver) Captions(ctx context.Context, obj *models.Scene) (ret []
func (r *sceneResolver) Galleries(ctx context.Context, obj *models.Scene) (ret []*models.Gallery, err error) { func (r *sceneResolver) Galleries(ctx context.Context, obj *models.Scene) (ret []*models.Gallery, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error { if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Gallery.FindBySceneID(ctx, obj.ID) ret, err = r.repository.Gallery.FindMany(ctx, obj.GalleryIDs)
return err return err
}); err != nil { }); err != nil {
return nil, err return nil, err
@ -216,7 +216,7 @@ func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*S
func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*models.Tag, err error) { func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*models.Tag, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error { if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.FindBySceneID(ctx, obj.ID) ret, err = r.repository.Tag.FindMany(ctx, obj.TagIDs)
return err return err
}); err != nil { }); err != nil {
return nil, err return nil, err
@ -227,7 +227,7 @@ func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*mod
func (r *sceneResolver) Performers(ctx context.Context, obj *models.Scene) (ret []*models.Performer, err error) { func (r *sceneResolver) Performers(ctx context.Context, obj *models.Scene) (ret []*models.Performer, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error { if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Performer.FindBySceneID(ctx, obj.ID) ret, err = r.repository.Performer.FindMany(ctx, obj.PerformerIDs)
return err return err
}); err != nil { }); err != nil {
return nil, err return nil, err

View file

@ -517,7 +517,7 @@ func getPathSearchClause(pathColumn, basenameColumn, p string, addWildcards, not
return ret return ret
} }
func intCriterionHandler(c *models.IntCriterionInput, column string) criterionHandlerFunc { func intCriterionHandler(c *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) { return func(ctx context.Context, f *filterBuilder) {
if c != nil { if c != nil {
clause, args := getIntCriterionWhereClause(column, *c) clause, args := getIntCriterionWhereClause(column, *c)
@ -526,9 +526,12 @@ func intCriterionHandler(c *models.IntCriterionInput, column string) criterionHa
} }
} }
func boolCriterionHandler(c *bool, column string) criterionHandlerFunc { func boolCriterionHandler(c *bool, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) { return func(ctx context.Context, f *filterBuilder) {
if c != nil { if c != nil {
if addJoinFn != nil {
addJoinFn(f)
}
var v string var v string
if *c { if *c {
v = "1" v = "1"

View file

@ -587,7 +587,8 @@ func (qb *GalleryStore) makeFilter(ctx context.Context, galleryFilter *models.Ga
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if galleryFilter.Checksum != nil { if galleryFilter.Checksum != nil {
f.addLeftJoin(fingerprintTable, "fingerprints_md5", "galleries_query.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'") 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) stringCriterionHandler(galleryFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
@ -595,19 +596,21 @@ func (qb *GalleryStore) makeFilter(ctx context.Context, galleryFilter *models.Ga
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if galleryFilter.IsZip != nil { if galleryFilter.IsZip != nil {
qb.addGalleriesFilesTable(f)
if *galleryFilter.IsZip { if *galleryFilter.IsZip {
f.addWhere("galleries_query.file_id IS NOT NULL")
f.addWhere("galleries_files.file_id IS NOT NULL")
} else { } else {
f.addWhere("galleries_query.file_id IS NULL") f.addWhere("galleries_files.file_id IS NULL")
} }
} }
})) }))
query.handleCriterion(ctx, pathCriterionHandler(galleryFilter.Path, "galleries_query.parent_folder_path", "galleries_query.basename", nil)) query.handleCriterion(ctx, pathCriterionHandler(galleryFilter.Path, "folders.path", "files.basename", qb.addFoldersTable))
query.handleCriterion(ctx, galleryFileCountCriterionHandler(qb, galleryFilter.FileCount)) query.handleCriterion(ctx, galleryFileCountCriterionHandler(qb, galleryFilter.FileCount))
query.handleCriterion(ctx, intCriterionHandler(galleryFilter.Rating, "galleries.rating")) query.handleCriterion(ctx, intCriterionHandler(galleryFilter.Rating, "galleries.rating", nil))
query.handleCriterion(ctx, stringCriterionHandler(galleryFilter.URL, "galleries.url")) query.handleCriterion(ctx, stringCriterionHandler(galleryFilter.URL, "galleries.url"))
query.handleCriterion(ctx, boolCriterionHandler(galleryFilter.Organized, "galleries.organized")) query.handleCriterion(ctx, boolCriterionHandler(galleryFilter.Organized, "galleries.organized", nil))
query.handleCriterion(ctx, galleryIsMissingCriterionHandler(qb, galleryFilter.IsMissing)) query.handleCriterion(ctx, galleryIsMissingCriterionHandler(qb, galleryFilter.IsMissing))
query.handleCriterion(ctx, galleryTagsCriterionHandler(qb, galleryFilter.Tags)) query.handleCriterion(ctx, galleryTagsCriterionHandler(qb, galleryFilter.Tags))
query.handleCriterion(ctx, galleryTagCountCriterionHandler(qb, galleryFilter.TagCount)) query.handleCriterion(ctx, galleryTagCountCriterionHandler(qb, galleryFilter.TagCount))
@ -623,6 +626,20 @@ func (qb *GalleryStore) makeFilter(ctx context.Context, galleryFilter *models.Ga
return query 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) { func (qb *GalleryStore) makeQuery(ctx context.Context, galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
if galleryFilter == nil { if galleryFilter == nil {
galleryFilter = &models.GalleryFilterType{} galleryFilter = &models.GalleryFilterType{}
@ -634,16 +651,33 @@ func (qb *GalleryStore) makeQuery(ctx context.Context, galleryFilter *models.Gal
query := qb.newQuery() query := qb.newQuery()
distinctIDs(&query, galleryTable) distinctIDs(&query, galleryTable)
// for convenience, join with the query view
query.addJoins(join{
table: galleriesQueryTable.GetTable(),
onClause: "galleries.id = galleries_query.id",
joinType: "INNER",
})
if q := findFilter.Q; q != nil && *q != "" { if q := findFilter.Q; q != nil && *q != "" {
query.addJoins(
join{
table: galleriesFilesTable,
onClause: "galleries_files.gallery_id = galleries.id",
},
join{
table: fileTable,
onClause: "galleries_files.file_id = files.id",
},
join{
table: folderTable,
onClause: "files.parent_folder_id = folders.id",
},
join{
table: fingerprintTable,
onClause: "files_fingerprints.file_id = galleries_files.file_id",
},
join{
table: folderTable,
as: "gallery_folder",
onClause: "galleries.folder_id = gallery_folder.id",
},
)
// add joins for files and checksum // add joins for files and checksum
searchColumns := []string{"galleries.title", "galleries_query.folder_path", "galleries_query.parent_folder_path", "galleries_query.basename", "galleries_query.fingerprint"} searchColumns := []string{"galleries.title", "gallery_folder.path", "folders.path", "files.basename", "files_fingerprints.fingerprint"}
query.parseQueryString(searchColumns, *q) query.parseQueryString(searchColumns, *q)
} }
@ -654,7 +688,8 @@ func (qb *GalleryStore) makeQuery(ctx context.Context, galleryFilter *models.Gal
query.addFilter(filter) query.addFilter(filter)
query.sortAndPagination = qb.getGallerySort(findFilter) + getPagination(findFilter) qb.setGallerySort(&query, findFilter)
query.sortAndPagination += getPagination(findFilter)
return &query, nil return &query, nil
} }
@ -902,33 +937,52 @@ func galleryAverageResolutionCriterionHandler(qb *GalleryStore, resolution *mode
} }
} }
func (qb *GalleryStore) getGallerySort(findFilter *models.FindFilterType) string { func (qb *GalleryStore) setGallerySort(query *queryBuilder, findFilter *models.FindFilterType) {
if findFilter == nil || findFilter.Sort == nil || *findFilter.Sort == "" { if findFilter == nil || findFilter.Sort == nil || *findFilter.Sort == "" {
return "" return
} }
sort := findFilter.GetSort("path") sort := findFilter.GetSort("path")
direction := findFilter.GetDirection() direction := findFilter.GetDirection()
// translate sort field addFileTable := func() {
if sort == "file_mod_time" { query.addJoins(
sort = "mod_time" join{
table: galleriesFilesTable,
onClause: "galleries_files.gallery_id = galleries.id",
},
join{
table: fileTable,
onClause: "galleries_files.file_id = files.id",
},
)
} }
switch sort { switch sort {
case "file_count": case "file_count":
return getCountSort(galleryTable, galleriesFilesTable, galleryIDColumn, direction) query.sortAndPagination += getCountSort(galleryTable, galleriesFilesTable, galleryIDColumn, direction)
case "images_count": case "images_count":
return getCountSort(galleryTable, galleriesImagesTable, galleryIDColumn, direction) query.sortAndPagination += getCountSort(galleryTable, galleriesImagesTable, galleryIDColumn, direction)
case "tag_count": case "tag_count":
return getCountSort(galleryTable, galleriesTagsTable, galleryIDColumn, direction) query.sortAndPagination += getCountSort(galleryTable, galleriesTagsTable, galleryIDColumn, direction)
case "performer_count": case "performer_count":
return getCountSort(galleryTable, performersGalleriesTable, galleryIDColumn, direction) query.sortAndPagination += getCountSort(galleryTable, performersGalleriesTable, galleryIDColumn, direction)
case "path": case "path":
// special handling for path // special handling for path
return fmt.Sprintf(" ORDER BY galleries_query.parent_folder_path %s, galleries_query.basename %[1]s", direction) addFileTable()
query.addJoins(
join{
table: folderTable,
onClause: "files.parent_folder_id = folders.id",
},
)
query.sortAndPagination += fmt.Sprintf(" ORDER BY folders.path %s, files.basename %[1]s", direction)
case "file_mod_time":
sort = "mod_time"
addFileTable()
query.sortAndPagination += getSort(sort, direction, fileTable)
default: default:
return getSort(sort, direction, "galleries_query") query.sortAndPagination += getSort(sort, direction, "galleries")
} }
} }

View file

@ -587,21 +587,21 @@ func (qb *ImageStore) makeFilter(ctx context.Context, imageFilter *models.ImageF
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if imageFilter.Checksum != nil { if imageFilter.Checksum != nil {
qb.addQueryTable(f) qb.addImagesFilesTable(f)
f.addInnerJoin(fingerprintTable, "fingerprints_md5", "galleries_query.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'") 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.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
})) }))
query.handleCriterion(ctx, stringCriterionHandler(imageFilter.Title, "images.title")) query.handleCriterion(ctx, stringCriterionHandler(imageFilter.Title, "images.title"))
query.handleCriterion(ctx, pathCriterionHandler(imageFilter.Path, "images_query.parent_folder_path", "images_query.basename", qb.addQueryTable)) query.handleCriterion(ctx, pathCriterionHandler(imageFilter.Path, "folders.path", "files.basename", qb.addFoldersTable))
query.handleCriterion(ctx, imageFileCountCriterionHandler(qb, imageFilter.FileCount)) query.handleCriterion(ctx, imageFileCountCriterionHandler(qb, imageFilter.FileCount))
query.handleCriterion(ctx, intCriterionHandler(imageFilter.Rating, "images.rating")) query.handleCriterion(ctx, intCriterionHandler(imageFilter.Rating, "images.rating", nil))
query.handleCriterion(ctx, intCriterionHandler(imageFilter.OCounter, "images.o_counter")) query.handleCriterion(ctx, intCriterionHandler(imageFilter.OCounter, "images.o_counter", nil))
query.handleCriterion(ctx, boolCriterionHandler(imageFilter.Organized, "images.organized")) query.handleCriterion(ctx, boolCriterionHandler(imageFilter.Organized, "images.organized", nil))
query.handleCriterion(ctx, resolutionCriterionHandler(imageFilter.Resolution, "images_query.image_height", "images_query.image_width", qb.addQueryTable)) query.handleCriterion(ctx, resolutionCriterionHandler(imageFilter.Resolution, "image_files.height", "image_files.width", qb.addImageFilesTable))
query.handleCriterion(ctx, imageIsMissingCriterionHandler(qb, imageFilter.IsMissing)) query.handleCriterion(ctx, imageIsMissingCriterionHandler(qb, imageFilter.IsMissing))
query.handleCriterion(ctx, imageTagsCriterionHandler(qb, imageFilter.Tags)) query.handleCriterion(ctx, imageTagsCriterionHandler(qb, imageFilter.Tags))
@ -616,8 +616,23 @@ func (qb *ImageStore) makeFilter(ctx context.Context, imageFilter *models.ImageF
return query return query
} }
func (qb *ImageStore) addQueryTable(f *filterBuilder) { func (qb *ImageStore) addImagesFilesTable(f *filterBuilder) {
f.addInnerJoin(imagesQueryTable.GetTable(), "", "images.id = images_query.id") 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) { func (qb *ImageStore) makeQuery(ctx context.Context, imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {

View file

@ -209,7 +209,7 @@ func (m *schema32Migrator) migrateFiles(ctx context.Context) error {
_, err = m.db.Exec("UPDATE `files` SET `parent_folder_id` = ?, `zip_file_id` = ?, `basename` = ? WHERE `id` = ?", parentID, zipFileID, basename, id) _, err = m.db.Exec("UPDATE `files` SET `parent_folder_id` = ?, `zip_file_id` = ?, `basename` = ? WHERE `id` = ?", parentID, zipFileID, basename, id)
if err != nil { if err != nil {
return err return fmt.Errorf("migrating file %s: %w", p, err)
} }
} }
@ -277,10 +277,23 @@ func (m *schema32Migrator) createFolderHierarchy(p string) (*int, sql.NullInt64,
return m.getOrCreateFolder(p, nil, sql.NullInt64{}) return m.getOrCreateFolder(p, nil, sql.NullInt64{})
} }
parentID, zipFileID, err := m.createFolderHierarchy(parent) var (
parentID *int
zipFileID sql.NullInt64
err error
)
// try to find parent folder in cache first
foundEntry, ok := m.folderCache[parent]
if ok {
parentID = &foundEntry.id
zipFileID = foundEntry.zipID
} else {
parentID, zipFileID, err = m.createFolderHierarchy(parent)
if err != nil { if err != nil {
return nil, sql.NullInt64{}, err return nil, sql.NullInt64{}, err
} }
}
return m.getOrCreateFolder(p, parentID, zipFileID) return m.getOrCreateFolder(p, parentID, zipFileID)
} }
@ -323,12 +336,12 @@ func (m *schema32Migrator) getOrCreateFolder(path string, parentID *int, zipFile
now := time.Now() now := time.Now()
result, err := m.db.Exec(insertSQL, path, parentFolderID, zipFileID, time.Time{}, now, now) result, err := m.db.Exec(insertSQL, path, parentFolderID, zipFileID, time.Time{}, now, now)
if err != nil { if err != nil {
return nil, sql.NullInt64{}, err return nil, sql.NullInt64{}, fmt.Errorf("creating folder %s: %w", path, err)
} }
id, err := result.LastInsertId() id, err := result.LastInsertId()
if err != nil { if err != nil {
return nil, sql.NullInt64{}, err return nil, sql.NullInt64{}, fmt.Errorf("creating folder %s: %w", path, err)
} }
idInt := int(id) idInt := int(id)

View file

@ -120,8 +120,8 @@ func (qb *movieQueryBuilder) makeFilter(ctx context.Context, movieFilter *models
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Name, "movies.name")) query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Name, "movies.name"))
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Director, "movies.director")) query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Director, "movies.director"))
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Synopsis, "movies.synopsis")) query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Synopsis, "movies.synopsis"))
query.handleCriterion(ctx, intCriterionHandler(movieFilter.Rating, "movies.rating")) query.handleCriterion(ctx, intCriterionHandler(movieFilter.Rating, "movies.rating", nil))
query.handleCriterion(ctx, durationCriterionHandler(movieFilter.Duration, "movies.duration")) query.handleCriterion(ctx, durationCriterionHandler(movieFilter.Duration, "movies.duration", nil))
query.handleCriterion(ctx, movieIsMissingCriterionHandler(qb, movieFilter.IsMissing)) query.handleCriterion(ctx, movieIsMissingCriterionHandler(qb, movieFilter.IsMissing))
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.URL, "movies.url")) query.handleCriterion(ctx, stringCriterionHandler(movieFilter.URL, "movies.url"))
query.handleCriterion(ctx, movieStudioCriterionHandler(qb, movieFilter.Studios)) query.handleCriterion(ctx, movieStudioCriterionHandler(qb, movieFilter.Studios))

View file

@ -245,8 +245,8 @@ func (qb *performerQueryBuilder) makeFilter(ctx context.Context, filter *models.
query.handleCriterion(ctx, stringCriterionHandler(filter.Name, tableName+".name")) query.handleCriterion(ctx, stringCriterionHandler(filter.Name, tableName+".name"))
query.handleCriterion(ctx, stringCriterionHandler(filter.Details, tableName+".details")) query.handleCriterion(ctx, stringCriterionHandler(filter.Details, tableName+".details"))
query.handleCriterion(ctx, boolCriterionHandler(filter.FilterFavorites, tableName+".favorite")) query.handleCriterion(ctx, boolCriterionHandler(filter.FilterFavorites, tableName+".favorite", nil))
query.handleCriterion(ctx, boolCriterionHandler(filter.IgnoreAutoTag, tableName+".ignore_auto_tag")) query.handleCriterion(ctx, boolCriterionHandler(filter.IgnoreAutoTag, tableName+".ignore_auto_tag", nil))
query.handleCriterion(ctx, yearFilterCriterionHandler(filter.BirthYear, tableName+".birthdate")) query.handleCriterion(ctx, yearFilterCriterionHandler(filter.BirthYear, tableName+".birthdate"))
query.handleCriterion(ctx, yearFilterCriterionHandler(filter.DeathYear, tableName+".death_date")) query.handleCriterion(ctx, yearFilterCriterionHandler(filter.DeathYear, tableName+".death_date"))
@ -269,10 +269,10 @@ func (qb *performerQueryBuilder) makeFilter(ctx context.Context, filter *models.
query.handleCriterion(ctx, stringCriterionHandler(filter.CareerLength, tableName+".career_length")) query.handleCriterion(ctx, stringCriterionHandler(filter.CareerLength, tableName+".career_length"))
query.handleCriterion(ctx, stringCriterionHandler(filter.Tattoos, tableName+".tattoos")) query.handleCriterion(ctx, stringCriterionHandler(filter.Tattoos, tableName+".tattoos"))
query.handleCriterion(ctx, stringCriterionHandler(filter.Piercings, tableName+".piercings")) query.handleCriterion(ctx, stringCriterionHandler(filter.Piercings, tableName+".piercings"))
query.handleCriterion(ctx, intCriterionHandler(filter.Rating, tableName+".rating")) query.handleCriterion(ctx, intCriterionHandler(filter.Rating, tableName+".rating", nil))
query.handleCriterion(ctx, stringCriterionHandler(filter.HairColor, tableName+".hair_color")) query.handleCriterion(ctx, stringCriterionHandler(filter.HairColor, tableName+".hair_color"))
query.handleCriterion(ctx, stringCriterionHandler(filter.URL, tableName+".url")) query.handleCriterion(ctx, stringCriterionHandler(filter.URL, tableName+".url"))
query.handleCriterion(ctx, intCriterionHandler(filter.Weight, tableName+".weight")) query.handleCriterion(ctx, intCriterionHandler(filter.Weight, tableName+".weight", nil))
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if filter.StashID != nil { if filter.StashID != nil {
qb.stashIDRepository().join(f, "performer_stash_ids", "performers.id") qb.stashIDRepository().join(f, "performer_stash_ids", "performers.id")

View file

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models"
) )
@ -58,7 +57,6 @@ func (qb queryBuilder) toSQL(includeSortPagination bool) string {
func (qb queryBuilder) findIDs(ctx context.Context) ([]int, error) { func (qb queryBuilder) findIDs(ctx context.Context) ([]int, error) {
const includeSortPagination = true const includeSortPagination = true
sql := qb.toSQL(includeSortPagination) sql := qb.toSQL(includeSortPagination)
logger.Tracef("SQL: %s, args: %v", sql, qb.args)
return qb.repository.runIdsQuery(ctx, sql, qb.args) return qb.repository.runIdsQuery(ctx, sql, qb.args)
} }

View file

@ -11,7 +11,6 @@ import (
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/file" "github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models"
) )
@ -154,8 +153,6 @@ 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 { func (r *repository) queryFunc(ctx context.Context, query string, args []interface{}, single bool, f func(rows *sqlx.Rows) error) error {
logger.Tracef("SQL: %s, args: %v", query, args)
rows, err := r.tx.Queryx(ctx, query, args...) rows, err := r.tx.Queryx(ctx, query, args...)
if err != nil && !errors.Is(err, sql.ErrNoRows) { if err != nil && !errors.Is(err, sql.ErrNoRows) {
@ -252,8 +249,6 @@ func (r *repository) executeFindQuery(ctx context.Context, body string, args []i
idsQuery := withClause + body + sortAndPagination idsQuery := withClause + body + sortAndPagination
// Perform query and fetch result // Perform query and fetch result
logger.Tracef("SQL: %s, args: %v", idsQuery, args)
var countResult int var countResult int
var countErr error var countErr error
var idsResult []int var idsResult []int

View file

@ -756,13 +756,14 @@ func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneF
query.not(qb.makeFilter(ctx, sceneFilter.Not)) query.not(qb.makeFilter(ctx, sceneFilter.Not))
} }
query.handleCriterion(ctx, pathCriterionHandler(sceneFilter.Path, "scenes_query.parent_folder_path", "scenes_query.basename", nil)) query.handleCriterion(ctx, pathCriterionHandler(sceneFilter.Path, "folders.path", "files.basename", qb.addFoldersTable))
query.handleCriterion(ctx, sceneFileCountCriterionHandler(qb, sceneFilter.FileCount)) query.handleCriterion(ctx, sceneFileCountCriterionHandler(qb, sceneFilter.FileCount))
query.handleCriterion(ctx, stringCriterionHandler(sceneFilter.Title, "scenes.title")) query.handleCriterion(ctx, stringCriterionHandler(sceneFilter.Title, "scenes.title"))
query.handleCriterion(ctx, stringCriterionHandler(sceneFilter.Details, "scenes.details")) query.handleCriterion(ctx, stringCriterionHandler(sceneFilter.Details, "scenes.details"))
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if sceneFilter.Oshash != nil { if sceneFilter.Oshash != nil {
f.addLeftJoin(fingerprintTable, "fingerprints_oshash", "scenes_query.file_id = fingerprints_oshash.file_id AND fingerprints_oshash.type = 'oshash'") 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) stringCriterionHandler(sceneFilter.Oshash, "fingerprints_oshash.fingerprint")(ctx, f)
@ -770,7 +771,8 @@ func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneF
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if sceneFilter.Checksum != nil { if sceneFilter.Checksum != nil {
f.addLeftJoin(fingerprintTable, "fingerprints_md5", "scenes_query.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'") 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) stringCriterionHandler(sceneFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
@ -778,22 +780,23 @@ func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneF
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if sceneFilter.Phash != nil { if sceneFilter.Phash != nil {
f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_query.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'") qb.addSceneFilesTable(f)
f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
value, _ := utils.StringToPhash(sceneFilter.Phash.Value) value, _ := utils.StringToPhash(sceneFilter.Phash.Value)
intCriterionHandler(&models.IntCriterionInput{ intCriterionHandler(&models.IntCriterionInput{
Value: int(value), Value: int(value),
Modifier: sceneFilter.Phash.Modifier, Modifier: sceneFilter.Phash.Modifier,
}, "fingerprints_phash.fingerprint")(ctx, f) }, "fingerprints_phash.fingerprint", nil)(ctx, f)
} }
})) }))
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.Rating, "scenes.rating")) query.handleCriterion(ctx, intCriterionHandler(sceneFilter.Rating, "scenes.rating", nil))
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.OCounter, "scenes.o_counter")) query.handleCriterion(ctx, intCriterionHandler(sceneFilter.OCounter, "scenes.o_counter", nil))
query.handleCriterion(ctx, boolCriterionHandler(sceneFilter.Organized, "scenes.organized")) query.handleCriterion(ctx, boolCriterionHandler(sceneFilter.Organized, "scenes.organized", nil))
query.handleCriterion(ctx, durationCriterionHandler(sceneFilter.Duration, "scenes_query.duration")) query.handleCriterion(ctx, durationCriterionHandler(sceneFilter.Duration, "video_files.duration", qb.addVideoFilesTable))
query.handleCriterion(ctx, resolutionCriterionHandler(sceneFilter.Resolution, "scenes_query.video_height", "scenes_query.video_width", nil)) query.handleCriterion(ctx, resolutionCriterionHandler(sceneFilter.Resolution, "video_files.height", "video_files.width", qb.addVideoFilesTable))
query.handleCriterion(ctx, hasMarkersCriterionHandler(sceneFilter.HasMarkers)) query.handleCriterion(ctx, hasMarkersCriterionHandler(sceneFilter.HasMarkers))
query.handleCriterion(ctx, sceneIsMissingCriterionHandler(qb, sceneFilter.IsMissing)) query.handleCriterion(ctx, sceneIsMissingCriterionHandler(qb, sceneFilter.IsMissing))
@ -806,8 +809,8 @@ func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneF
} }
})) }))
query.handleCriterion(ctx, boolCriterionHandler(sceneFilter.Interactive, "scenes.interactive")) query.handleCriterion(ctx, boolCriterionHandler(sceneFilter.Interactive, "video_files.interactive", qb.addVideoFilesTable))
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.InteractiveSpeed, "scenes.interactive_speed")) query.handleCriterion(ctx, intCriterionHandler(sceneFilter.InteractiveSpeed, "video_files.interactive_speed", qb.addVideoFilesTable))
query.handleCriterion(ctx, sceneCaptionCriterionHandler(qb, sceneFilter.Captions)) query.handleCriterion(ctx, sceneCaptionCriterionHandler(qb, sceneFilter.Captions))
@ -820,11 +823,30 @@ func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneF
query.handleCriterion(ctx, scenePerformerTagsCriterionHandler(qb, sceneFilter.PerformerTags)) query.handleCriterion(ctx, scenePerformerTagsCriterionHandler(qb, sceneFilter.PerformerTags))
query.handleCriterion(ctx, scenePerformerFavoriteCriterionHandler(sceneFilter.PerformerFavorite)) query.handleCriterion(ctx, scenePerformerFavoriteCriterionHandler(sceneFilter.PerformerFavorite))
query.handleCriterion(ctx, scenePerformerAgeCriterionHandler(sceneFilter.PerformerAge)) query.handleCriterion(ctx, scenePerformerAgeCriterionHandler(sceneFilter.PerformerAge))
query.handleCriterion(ctx, scenePhashDuplicatedCriterionHandler(sceneFilter.Duplicated)) query.handleCriterion(ctx, scenePhashDuplicatedCriterionHandler(sceneFilter.Duplicated, qb.addSceneFilesTable))
return query 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) Query(ctx context.Context, options models.SceneQueryOptions) (*models.SceneQueryResult, error) { func (qb *SceneStore) Query(ctx context.Context, options models.SceneQueryOptions) (*models.SceneQueryResult, error) {
sceneFilter := options.SceneFilter sceneFilter := options.SceneFilter
findFilter := options.FindFilter findFilter := options.FindFilter
@ -839,17 +861,31 @@ func (qb *SceneStore) Query(ctx context.Context, options models.SceneQueryOption
query := qb.newQuery() query := qb.newQuery()
distinctIDs(&query, sceneTable) distinctIDs(&query, sceneTable)
// for convenience, join with the query view
query.addJoins(join{
table: scenesQueryTable.GetTable(),
onClause: "scenes.id = scenes_query.id",
joinType: "INNER",
})
if q := findFilter.Q; q != nil && *q != "" { if q := findFilter.Q; q != nil && *q != "" {
query.join("scene_markers", "", "scene_markers.scene_id = scenes.id") query.addJoins(
join{
table: scenesFilesTable,
onClause: "scenes_files.scene_id = scenes.id",
},
join{
table: fileTable,
onClause: "scenes_files.file_id = files.id",
},
join{
table: folderTable,
onClause: "files.parent_folder_id = folders.id",
},
join{
table: fingerprintTable,
onClause: "files_fingerprints.file_id = scenes_files.file_id",
},
join{
table: sceneMarkerTable,
onClause: "scene_markers.scene_id = scenes.id",
},
)
searchColumns := []string{"scenes.title", "scenes.details", "scenes_query.parent_folder_path", "scenes_query.basename", "scenes_query.fingerprint", "scene_markers.title"} searchColumns := []string{"scenes.title", "scenes.details", "folders.path", "files.basename", "files_fingerprints.fingerprint", "scene_markers.title"}
query.parseQueryString(searchColumns, *q) query.parseQueryString(searchColumns, *q)
} }
@ -890,12 +926,32 @@ func (qb *SceneStore) queryGroupedFields(ctx context.Context, options models.Sce
} }
if options.TotalDuration { if options.TotalDuration {
query.addColumn("COALESCE(scenes_query.duration, 0) as duration") query.addJoins(
join{
table: scenesFilesTable,
onClause: "scenes_files.scene_id = scenes.id",
},
join{
table: videoFileTable,
onClause: "scenes_files.file_id = video_files.file_id",
},
)
query.addColumn("COALESCE(video_files.duration, 0) as duration")
aggregateQuery.addColumn("SUM(temp.duration) as duration") aggregateQuery.addColumn("SUM(temp.duration) as duration")
} }
if options.TotalSize { if options.TotalSize {
query.addColumn("COALESCE(scenes_query.size, 0) as size") query.addJoins(
join{
table: scenesFilesTable,
onClause: "scenes_files.scene_id = scenes.id",
},
join{
table: fileTable,
onClause: "scenes_files.file_id = files.id",
},
)
query.addColumn("COALESCE(files.size, 0) as size")
aggregateQuery.addColumn("SUM(temp.size) as size") aggregateQuery.addColumn("SUM(temp.size) as size")
} }
@ -928,24 +984,32 @@ func sceneFileCountCriterionHandler(qb *SceneStore, fileCount *models.IntCriteri
return h.handler(fileCount) return h.handler(fileCount)
} }
func scenePhashDuplicatedCriterionHandler(duplicatedFilter *models.PHashDuplicationCriterionInput) criterionHandlerFunc { func scenePhashDuplicatedCriterionHandler(duplicatedFilter *models.PHashDuplicationCriterionInput, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) { return func(ctx context.Context, f *filterBuilder) {
// TODO: Wishlist item: Implement Distance matching // TODO: Wishlist item: Implement Distance matching
if duplicatedFilter != nil { if duplicatedFilter != nil {
if addJoinFn != nil {
addJoinFn(f)
}
var v string var v string
if *duplicatedFilter.Duplicated { if *duplicatedFilter.Duplicated {
v = ">" v = ">"
} else { } else {
v = "=" 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_query.file_id = scph.file_id")
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 durationCriterionHandler(durationFilter *models.IntCriterionInput, column string) criterionHandlerFunc { func durationCriterionHandler(durationFilter *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) { return func(ctx context.Context, f *filterBuilder) {
if durationFilter != nil { if durationFilter != nil {
if addJoinFn != nil {
addJoinFn(f)
}
clause, args := getIntCriterionWhereClause("cast("+column+" as int)", *durationFilter) clause, args := getIntCriterionWhereClause("cast("+column+" as int)", *durationFilter)
f.addWhere(clause, args...) f.addWhere(clause, args...)
} }
@ -1015,7 +1079,8 @@ func sceneIsMissingCriterionHandler(qb *SceneStore, isMissing *string) criterion
qb.stashIDRepository().join(f, "scene_stash_ids", "scenes.id") qb.stashIDRepository().join(f, "scene_stash_ids", "scenes.id")
f.addWhere("scene_stash_ids.scene_id IS NULL") f.addWhere("scene_stash_ids.scene_id IS NULL")
case "phash": case "phash":
f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_query.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = '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") f.addWhere("fingerprints_phash.fingerprint IS NULL")
default: default:
f.addWhere("(scenes." + *isMissing + " IS NULL OR TRIM(scenes." + *isMissing + ") = '')") f.addWhere("(scenes." + *isMissing + " IS NULL OR TRIM(scenes." + *isMissing + ") = '')")
@ -1040,7 +1105,8 @@ func sceneCaptionCriterionHandler(qb *SceneStore, captions *models.StringCriteri
joinTable: videoCaptionsTable, joinTable: videoCaptionsTable,
stringColumn: captionCodeColumn, stringColumn: captionCodeColumn,
addJoinTable: func(f *filterBuilder) { addJoinTable: func(f *filterBuilder) {
f.addLeftJoin(videoCaptionsTable, "", "video_captions.file_id = scenes_query.file_id") qb.addSceneFilesTable(f)
f.addLeftJoin(videoCaptionsTable, "", "video_captions.file_id = scenes_files.file_id")
}, },
} }
@ -1201,14 +1267,27 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF
} }
sort := findFilter.GetSort("title") sort := findFilter.GetSort("title")
// translate sort field addFileTable := func() {
switch sort { query.addJoins(
case "bitrate": join{
sort = "bit_rate" table: scenesFilesTable,
case "file_mod_time": onClause: "scenes_files.scene_id = scenes.id",
sort = "mod_time" },
case "framerate": join{
sort = "frame_rate" table: fileTable,
onClause: "scenes_files.file_id = files.id",
},
)
}
addVideoFileTable := func() {
addFileTable()
query.addJoins(
join{
table: videoFileTable,
onClause: "video_files.file_id = scenes_files.file_id",
},
)
} }
direction := findFilter.GetDirection() direction := findFilter.GetDirection()
@ -1224,21 +1303,47 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF
query.sortAndPagination += getCountSort(sceneTable, scenesFilesTable, sceneIDColumn, direction) query.sortAndPagination += getCountSort(sceneTable, scenesFilesTable, sceneIDColumn, direction)
case "path": case "path":
// special handling for path // special handling for path
query.sortAndPagination += fmt.Sprintf(" ORDER BY scenes_query.parent_folder_path %s, scenes_query.basename %[1]s", direction) addFileTable()
query.addJoins(
join{
table: folderTable,
onClause: "files.parent_folder_id = folders.id",
},
)
query.sortAndPagination += fmt.Sprintf(" ORDER BY folders.path %s, files.basename %[1]s", direction)
case "perceptual_similarity": case "perceptual_similarity":
// special handling for phash // special handling for phash
query.addJoins(join{ addFileTable()
query.addJoins(
join{
table: fingerprintTable, table: fingerprintTable,
as: "fingerprints_phash", as: "fingerprints_phash",
onClause: "scenes_query.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'", onClause: "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'",
}) },
)
query.sortAndPagination += " ORDER BY fingerprints_phash.fingerprint " + direction + ", scenes_query.size DESC" query.sortAndPagination += " ORDER BY fingerprints_phash.fingerprint " + direction + ", files.size DESC"
case "bitrate":
sort = "bit_rate"
addVideoFileTable()
query.sortAndPagination += getSort(sort, direction, videoFileTable)
case "file_mod_time":
sort = "mod_time"
addFileTable()
query.sortAndPagination += getSort(sort, direction, fileTable)
case "framerate":
sort = "frame_rate"
addVideoFileTable()
query.sortAndPagination += getSort(sort, direction, videoFileTable)
case "size":
addFileTable()
query.sortAndPagination += getSort(sort, direction, fileTable)
case "duration":
addVideoFileTable()
query.sortAndPagination += getSort(sort, direction, videoFileTable)
default: default:
query.sortAndPagination += getSort(sort, direction, "scenes_query") query.sortAndPagination += getSort(sort, direction, "scenes")
} }
query.sortAndPagination += ", scenes_query.bit_rate DESC, scenes_query.frame_rate DESC, scenes.rating DESC, scenes_query.duration DESC"
} }
func (qb *SceneStore) imageRepository() *imageRepository { func (qb *SceneStore) imageRepository() *imageRepository {

View file

@ -1853,8 +1853,11 @@ func queryScene(ctx context.Context, t *testing.T, sqb models.SceneReader, scene
result, err := sqb.Query(ctx, models.SceneQueryOptions{ result, err := sqb.Query(ctx, models.SceneQueryOptions{
QueryOptions: models.QueryOptions{ QueryOptions: models.QueryOptions{
FindFilter: findFilter, FindFilter: findFilter,
Count: true,
}, },
SceneFilter: sceneFilter, SceneFilter: sceneFilter,
TotalDuration: true,
TotalSize: true,
}) })
if err != nil { if err != nil {
t.Errorf("Error querying scene: %v", err) t.Errorf("Error querying scene: %v", err)
@ -1875,7 +1878,9 @@ func sceneQueryQ(ctx context.Context, t *testing.T, sqb models.SceneReader, q st
} }
scenes := queryScene(ctx, t, sqb, nil, &filter) scenes := queryScene(ctx, t, sqb, nil, &filter)
assert.Len(t, scenes, 1) if !assert.Len(t, scenes, 1) {
return
}
scene := scenes[0] scene := scenes[0]
assert.Equal(t, sceneIDs[expectedSceneIdx], scene.ID) assert.Equal(t, sceneIDs[expectedSceneIdx], scene.ID)

View file

@ -207,8 +207,8 @@ func (qb *studioQueryBuilder) makeFilter(ctx context.Context, studioFilter *mode
query.handleCriterion(ctx, stringCriterionHandler(studioFilter.Name, studioTable+".name")) query.handleCriterion(ctx, stringCriterionHandler(studioFilter.Name, studioTable+".name"))
query.handleCriterion(ctx, stringCriterionHandler(studioFilter.Details, studioTable+".details")) query.handleCriterion(ctx, stringCriterionHandler(studioFilter.Details, studioTable+".details"))
query.handleCriterion(ctx, stringCriterionHandler(studioFilter.URL, studioTable+".url")) query.handleCriterion(ctx, stringCriterionHandler(studioFilter.URL, studioTable+".url"))
query.handleCriterion(ctx, intCriterionHandler(studioFilter.Rating, studioTable+".rating")) query.handleCriterion(ctx, intCriterionHandler(studioFilter.Rating, studioTable+".rating", nil))
query.handleCriterion(ctx, boolCriterionHandler(studioFilter.IgnoreAutoTag, studioTable+".ignore_auto_tag")) query.handleCriterion(ctx, boolCriterionHandler(studioFilter.IgnoreAutoTag, studioTable+".ignore_auto_tag", nil))
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if studioFilter.StashID != nil { if studioFilter.StashID != nil {

View file

@ -557,7 +557,6 @@ func queryFunc(ctx context.Context, query *goqu.SelectDataset, single bool, f fu
return err return err
} }
logger.Tracef("SQL: %s [%v]", q, args)
rows, err := tx.QueryxContext(ctx, q, args...) rows, err := tx.QueryxContext(ctx, q, args...)
if err != nil && !errors.Is(err, sql.ErrNoRows) { if err != nil && !errors.Is(err, sql.ErrNoRows) {
@ -592,7 +591,6 @@ func querySimple(ctx context.Context, query *goqu.SelectDataset, out interface{}
return err return err
} }
logger.Tracef("SQL: %s [%v]", q, args)
rows, err := tx.QueryxContext(ctx, q, args...) rows, err := tx.QueryxContext(ctx, q, args...)
if err != nil { if err != nil {
return fmt.Errorf("querying `%s` [%v]: %w", q, args, err) return fmt.Errorf("querying `%s` [%v]: %w", q, args, err)

View file

@ -297,7 +297,7 @@ func (qb *tagQueryBuilder) makeFilter(ctx context.Context, tagFilter *models.Tag
query.handleCriterion(ctx, stringCriterionHandler(tagFilter.Name, tagTable+".name")) query.handleCriterion(ctx, stringCriterionHandler(tagFilter.Name, tagTable+".name"))
query.handleCriterion(ctx, tagAliasCriterionHandler(qb, tagFilter.Aliases)) query.handleCriterion(ctx, tagAliasCriterionHandler(qb, tagFilter.Aliases))
query.handleCriterion(ctx, boolCriterionHandler(tagFilter.IgnoreAutoTag, tagTable+".ignore_auto_tag")) query.handleCriterion(ctx, boolCriterionHandler(tagFilter.IgnoreAutoTag, tagTable+".ignore_auto_tag", nil))
query.handleCriterion(ctx, tagIsMissingCriterionHandler(qb, tagFilter.IsMissing)) query.handleCriterion(ctx, tagIsMissingCriterionHandler(qb, tagFilter.IsMissing))
query.handleCriterion(ctx, tagSceneCountCriterionHandler(qb, tagFilter.SceneCount)) query.handleCriterion(ctx, tagSceneCountCriterionHandler(qb, tagFilter.SceneCount))

View file

@ -3,8 +3,14 @@ package sqlite
import ( import (
"context" "context"
"database/sql" "database/sql"
"time"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/logger"
)
const (
slowLogTime = time.Millisecond * 200
) )
type dbReader interface { type dbReader interface {
@ -14,6 +20,15 @@ type dbReader interface {
QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error)
} }
func logSQL(start time.Time, query string, args ...interface{}) {
since := time.Since(start)
if since >= slowLogTime {
logger.Debugf("SLOW SQL [%v]: %s, args: %v", since, query, args)
} else {
logger.Tracef("SQL [%v]: %s, args: %v", since, query, args)
}
}
type dbWrapper struct{} type dbWrapper struct{}
func (*dbWrapper) Get(ctx context.Context, dest interface{}, query string, args ...interface{}) error { func (*dbWrapper) Get(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
@ -22,7 +37,11 @@ func (*dbWrapper) Get(ctx context.Context, dest interface{}, query string, args
return err return err
} }
return tx.Get(dest, query, args...) start := time.Now()
err = tx.Get(dest, query, args...)
logSQL(start, query, args...)
return err
} }
func (*dbWrapper) Select(ctx context.Context, dest interface{}, query string, args ...interface{}) error { func (*dbWrapper) Select(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
@ -31,7 +50,11 @@ func (*dbWrapper) Select(ctx context.Context, dest interface{}, query string, ar
return err return err
} }
return tx.Select(dest, query, args...) start := time.Now()
err = tx.Select(dest, query, args...)
logSQL(start, query, args...)
return err
} }
func (*dbWrapper) Queryx(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) { func (*dbWrapper) Queryx(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) {
@ -40,7 +63,11 @@ func (*dbWrapper) Queryx(ctx context.Context, query string, args ...interface{})
return nil, err return nil, err
} }
return tx.Queryx(query, args...) start := time.Now()
ret, err := tx.Queryx(query, args...)
logSQL(start, query, args...)
return ret, err
} }
func (*dbWrapper) NamedExec(ctx context.Context, query string, arg interface{}) (sql.Result, error) { func (*dbWrapper) NamedExec(ctx context.Context, query string, arg interface{}) (sql.Result, error) {
@ -49,7 +76,11 @@ func (*dbWrapper) NamedExec(ctx context.Context, query string, arg interface{})
return nil, err return nil, err
} }
return tx.NamedExec(query, arg) start := time.Now()
ret, err := tx.NamedExec(query, arg)
logSQL(start, query, arg)
return ret, err
} }
func (*dbWrapper) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { func (*dbWrapper) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
@ -58,5 +89,9 @@ func (*dbWrapper) Exec(ctx context.Context, query string, args ...interface{}) (
return nil, err return nil, err
} }
return tx.Exec(query, args...) start := time.Now()
ret, err := tx.Exec(query, args...)
logSQL(start, query, args...)
return ret, err
} }

View file

@ -11,10 +11,12 @@ import (
"math" "math"
"math/rand" "math/rand"
"os" "os"
"path"
"strconv" "strconv"
"time" "time"
"github.com/stashapp/stash/pkg/file" "github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/hash/md5" "github.com/stashapp/stash/pkg/hash/md5"
"github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/intslice" "github.com/stashapp/stash/pkg/sliceutil/intslice"
@ -66,9 +68,6 @@ func main() {
log.Fatalf("couldn't initialize database: %v", err) log.Fatalf("couldn't initialize database: %v", err)
} }
logf("Populating database...") logf("Populating database...")
if err = makeFolder(); err != nil {
log.Fatalf("couldn't create folder: %v", err)
}
populateDB() populateDB()
} }
@ -117,18 +116,39 @@ func retry(attempts int, fn func() error) error {
return err return err
} }
func makeFolder() error { func getOrCreateFolder(ctx context.Context, p string) (*file.Folder, error) {
return withTxn(func(ctx context.Context) error { ret, err := repo.Folder.FindByPath(ctx, p)
f := file.Folder{ if err != nil {
Path: ".", return nil, err
}
if err := repo.Folder.Create(ctx, &f); err != nil {
return err
} }
folderID = f.ID if ret != nil {
return nil return ret, nil
}) }
var parentID *file.FolderID
if p != "." {
parent := path.Dir(p)
parentFolder, err := getOrCreateFolder(ctx, parent)
if err != nil {
return nil, err
}
parentID = &parentFolder.ID
}
f := file.Folder{
Path: p,
ParentFolderID: parentID,
}
if err := repo.Folder.Create(ctx, &f); err != nil {
return nil, err
}
ret = &f
return ret, nil
} }
func makeTags(n int) { func makeTags(n int) {
@ -230,11 +250,10 @@ func makePerformers(n int) {
} }
} }
func generateBaseFile(path string) *file.BaseFile { func generateBaseFile(parentFolderID file.FolderID, path string) *file.BaseFile {
return &file.BaseFile{ return &file.BaseFile{
Path: path,
Basename: path, Basename: path,
ParentFolderID: folderID, ParentFolderID: parentFolderID,
Fingerprints: []file.Fingerprint{ Fingerprints: []file.Fingerprint{
file.Fingerprint{ file.Fingerprint{
Type: "md5", Type: "md5",
@ -250,11 +269,11 @@ func generateBaseFile(path string) *file.BaseFile {
} }
} }
func generateVideoFile(path string) file.File { func generateVideoFile(parentFolderID file.FolderID, path string) file.File {
w, h := getResolution() w, h := getResolution()
return &file.VideoFile{ return &file.VideoFile{
BaseFile: generateBaseFile(path), BaseFile: generateBaseFile(parentFolderID, path),
Duration: rand.Float64() * 14400, Duration: rand.Float64() * 14400,
Height: h, Height: h,
Width: w, Width: w,
@ -262,7 +281,13 @@ func generateVideoFile(path string) file.File {
} }
func makeVideoFile(ctx context.Context, path string) (file.File, error) { func makeVideoFile(ctx context.Context, path string) (file.File, error) {
f := generateVideoFile(path) folderPath := fsutil.GetIntraDir(path, 2, 2)
parentFolder, err := getOrCreateFolder(ctx, folderPath)
if err != nil {
return nil, err
}
f := generateVideoFile(parentFolder.ID, path)
if err := repo.File.Create(ctx, f); err != nil { if err := repo.File.Create(ctx, f); err != nil {
return nil, err return nil, err
@ -341,18 +366,24 @@ func generateScene(i int) models.Scene {
} }
} }
func generateImageFile(path string) file.File { func generateImageFile(parentFolderID file.FolderID, path string) file.File {
w, h := getResolution() w, h := getResolution()
return &file.ImageFile{ return &file.ImageFile{
BaseFile: generateBaseFile(path), BaseFile: generateBaseFile(parentFolderID, path),
Height: h, Height: h,
Width: w, Width: w,
} }
} }
func makeImageFile(ctx context.Context, path string) (file.File, error) { func makeImageFile(ctx context.Context, path string) (file.File, error) {
f := generateImageFile(path) folderPath := fsutil.GetIntraDir(path, 2, 2)
parentFolder, err := getOrCreateFolder(ctx, folderPath)
if err != nil {
return nil, err
}
f := generateImageFile(parentFolder.ID, path)
if err := repo.File.Create(ctx, f); err != nil { if err := repo.File.Create(ctx, f); err != nil {
return nil, err return nil, err
@ -438,12 +469,18 @@ func makeGalleries(n int) {
} }
} }
func generateZipFile(path string) file.File { func generateZipFile(parentFolderID file.FolderID, path string) file.File {
return generateBaseFile(path) return generateBaseFile(parentFolderID, path)
} }
func makeZipFile(ctx context.Context, path string) (file.File, error) { func makeZipFile(ctx context.Context, path string) (file.File, error) {
f := generateZipFile(path) folderPath := fsutil.GetIntraDir(path, 2, 2)
parentFolder, err := getOrCreateFolder(ctx, folderPath)
if err != nil {
return nil, err
}
f := generateZipFile(parentFolder.ID, path)
if err := repo.File.Create(ctx, f); err != nil { if err := repo.File.Create(ctx, f); err != nil {
return nil, err return nil, err

View file

@ -1,7 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import cx from "classnames"; import cx from "classnames";
import * as GQL from "src/core/generated-graphql";
import { Button, Form, Spinner } from "react-bootstrap"; import { Button, Form, Spinner } from "react-bootstrap";
import Icon from "src/components/Shared/Icon"; import Icon from "src/components/Shared/Icon";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -13,9 +12,10 @@ import {
faStepForward, faStepForward,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { objectTitle } from "src/core/files"; import { objectTitle } from "src/core/files";
import { QueuedScene } from "src/models/sceneQueue";
export interface IPlaylistViewer { export interface IPlaylistViewer {
scenes?: GQL.SlimSceneDataFragment[]; scenes?: QueuedScene[];
currentID?: string; currentID?: string;
start?: number; start?: number;
continue?: boolean; continue?: boolean;
@ -54,7 +54,7 @@ export const QueueViewer: React.FC<IPlaylistViewer> = ({
setMoreLoading(false); setMoreLoading(false);
}, [scenes]); }, [scenes]);
function isCurrentScene(scene: GQL.SlimSceneDataFragment) { function isCurrentScene(scene: QueuedScene) {
return scene.id === currentID; return scene.id === currentID;
} }
@ -76,7 +76,7 @@ export const QueueViewer: React.FC<IPlaylistViewer> = ({
onMoreScenes(); onMoreScenes();
} }
function renderPlaylistEntry(scene: GQL.SlimSceneDataFragment) { function renderPlaylistEntry(scene: QueuedScene) {
return ( return (
<li <li
className={cx("my-2", { current: isCurrentScene(scene) })} className={cx("my-2", { current: isCurrentScene(scene) })}

View file

@ -19,7 +19,7 @@ import {
import Icon from "src/components/Shared/Icon"; import Icon from "src/components/Shared/Icon";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import SceneQueue from "src/models/sceneQueue"; import SceneQueue, { QueuedScene } from "src/models/sceneQueue";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import { OCounterButton } from "./OCounterButton"; import { OCounterButton } from "./OCounterButton";
@ -56,7 +56,7 @@ interface IProps {
scene: GQL.SceneDataFragment; scene: GQL.SceneDataFragment;
refetch: () => void; refetch: () => void;
setTimestamp: (num: number) => void; setTimestamp: (num: number) => void;
queueScenes: GQL.SceneDataFragment[]; queueScenes: QueuedScene[];
onQueueNext: () => void; onQueueNext: () => void;
onQueuePrevious: () => void; onQueuePrevious: () => void;
onQueueRandom: () => void; onQueueRandom: () => void;
@ -519,7 +519,7 @@ const SceneLoader: React.FC = () => {
() => SceneQueue.fromQueryParameters(location.search), () => SceneQueue.fromQueryParameters(location.search),
[location.search] [location.search]
); );
const [queueScenes, setQueueScenes] = useState<GQL.SceneDataFragment[]>([]); const [queueScenes, setQueueScenes] = useState<QueuedScene[]>([]);
const [queueTotal, setQueueTotal] = useState(0); const [queueTotal, setQueueTotal] = useState(0);
const [queueStart, setQueueStart] = useState(1); const [queueStart, setQueueStart] = useState(1);
@ -592,7 +592,7 @@ const SceneLoader: React.FC = () => {
const { scenes } = query.data.findScenes; const { scenes } = query.data.findScenes;
// prepend scenes to scene list // prepend scenes to scene list
const newScenes = scenes.concat(queueScenes); const newScenes = (scenes as QueuedScene[]).concat(queueScenes);
setQueueScenes(newScenes); setQueueScenes(newScenes);
setQueueStart(newStart); setQueueStart(newStart);
} }
@ -613,7 +613,7 @@ const SceneLoader: React.FC = () => {
const { scenes } = query.data.findScenes; const { scenes } = query.data.findScenes;
// append scenes to scene list // append scenes to scene list
const newScenes = scenes.concat(queueScenes); const newScenes = (scenes as QueuedScene[]).concat(queueScenes);
setQueueScenes(newScenes); setQueueScenes(newScenes);
// don't change queue start // don't change queue start
} }

View file

@ -1,9 +1,11 @@
import queryString from "query-string"; import queryString from "query-string";
import { RouteComponentProps } from "react-router-dom"; import { RouteComponentProps } from "react-router-dom";
import { FilterMode } from "src/core/generated-graphql"; import { FilterMode, Scene } from "src/core/generated-graphql";
import { ListFilterModel } from "./list-filter/filter"; import { ListFilterModel } from "./list-filter/filter";
import { SceneListFilterOptions } from "./list-filter/scenes"; import { SceneListFilterOptions } from "./list-filter/scenes";
export type QueuedScene = Pick<Scene, "id" | "title" | "paths">;
interface IQueryParameters { interface IQueryParameters {
qsort?: string; qsort?: string;
qsortd?: string; qsortd?: string;