mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
Show duration and filesize in results (#1776)
* Add new query interface * Refactor query builder * Change Query interface * Return duration and filesize in scene query * Adjust UI for scene metadata * Introduce new image query interface * Change image Query interface * Add megapixels and size to image query * Update image UI Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
1b411e3f43
commit
4dd56c3d82
@@ -84,12 +84,16 @@ func (j *joins) add(newJoins ...join) {
|
||||
}
|
||||
|
||||
func (j *joins) toSQL() string {
|
||||
if len(*j) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var ret []string
|
||||
for _, jj := range *j {
|
||||
ret = append(ret, jj.toSQL())
|
||||
}
|
||||
|
||||
return strings.Join(ret, " ")
|
||||
return " " + strings.Join(ret, " ")
|
||||
}
|
||||
|
||||
type filterBuilder struct {
|
||||
|
||||
@@ -233,8 +233,7 @@ func (qb *galleryQueryBuilder) makeQuery(galleryFilter *models.GalleryFilterType
|
||||
}
|
||||
|
||||
query := qb.newQuery()
|
||||
|
||||
query.body = selectDistinctIDs(galleryTable)
|
||||
distinctIDs(&query, galleryTable)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
searchColumns := []string{"galleries.title", "galleries.path", "galleries.checksum"}
|
||||
|
||||
@@ -962,18 +962,24 @@ func verifyGalleriesImageCount(t *testing.T, imageCountCriterion models.IntCrite
|
||||
for _, gallery := range galleries {
|
||||
pp := 0
|
||||
|
||||
_, count, err := r.Image().Query(&models.ImageFilterType{
|
||||
Galleries: &models.MultiCriterionInput{
|
||||
Value: []string{strconv.Itoa(gallery.ID)},
|
||||
Modifier: models.CriterionModifierIncludes,
|
||||
result, err := r.Image().Query(models.ImageQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: &models.FindFilterType{
|
||||
PerPage: &pp,
|
||||
},
|
||||
Count: true,
|
||||
},
|
||||
ImageFilter: &models.ImageFilterType{
|
||||
Galleries: &models.MultiCriterionInput{
|
||||
Value: []string{strconv.Itoa(gallery.ID)},
|
||||
Modifier: models.CriterionModifierIncludes,
|
||||
},
|
||||
},
|
||||
}, &models.FindFilterType{
|
||||
PerPage: &pp,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
verifyInt(t, count, imageCountCriterion)
|
||||
verifyInt(t, result.Count, imageCountCriterion)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -261,8 +261,7 @@ func (qb *imageQueryBuilder) makeQuery(imageFilter *models.ImageFilterType, find
|
||||
}
|
||||
|
||||
query := qb.newQuery()
|
||||
|
||||
query.body = selectDistinctIDs(imageTable)
|
||||
distinctIDs(&query, imageTable)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
searchColumns := []string{"images.title", "images.path", "images.checksum"}
|
||||
@@ -283,28 +282,65 @@ func (qb *imageQueryBuilder) makeQuery(imageFilter *models.ImageFilterType, find
|
||||
return &query, nil
|
||||
}
|
||||
|
||||
func (qb *imageQueryBuilder) Query(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) ([]*models.Image, int, error) {
|
||||
query, err := qb.makeQuery(imageFilter, findFilter)
|
||||
func (qb *imageQueryBuilder) Query(options models.ImageQueryOptions) (*models.ImageQueryResult, error) {
|
||||
query, err := qb.makeQuery(options.ImageFilter, options.FindFilter)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idsResult, countResult, err := query.executeFind()
|
||||
result, err := qb.queryGroupedFields(options, *query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, fmt.Errorf("error querying aggregate fields: %w", err)
|
||||
}
|
||||
|
||||
var images []*models.Image
|
||||
for _, id := range idsResult {
|
||||
image, err := qb.Find(id)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
images = append(images, image)
|
||||
idsResult, err := query.findIDs()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error finding IDs: %w", err)
|
||||
}
|
||||
|
||||
return images, countResult, nil
|
||||
result.IDs = idsResult
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (qb *imageQueryBuilder) queryGroupedFields(options models.ImageQueryOptions, query queryBuilder) (*models.ImageQueryResult, error) {
|
||||
if !options.Count && !options.Megapixels && !options.TotalSize {
|
||||
// nothing to do - return empty result
|
||||
return models.NewImageQueryResult(qb), nil
|
||||
}
|
||||
|
||||
aggregateQuery := qb.newQuery()
|
||||
|
||||
if options.Count {
|
||||
aggregateQuery.addColumn("COUNT(temp.id) as total")
|
||||
}
|
||||
|
||||
if options.Megapixels {
|
||||
query.addColumn("COALESCE(images.width, 0) * COALESCE(images.height, 0) / 1000000 as megapixels")
|
||||
aggregateQuery.addColumn("COALESCE(SUM(temp.megapixels), 0) as megapixels")
|
||||
}
|
||||
|
||||
if options.TotalSize {
|
||||
query.addColumn("COALESCE(images.size, 0) as size")
|
||||
aggregateQuery.addColumn("COALESCE(SUM(temp.size), 0) as size")
|
||||
}
|
||||
|
||||
const includeSortPagination = false
|
||||
aggregateQuery.from = fmt.Sprintf("(%s) as temp", query.toSQL(includeSortPagination))
|
||||
|
||||
out := struct {
|
||||
Total int
|
||||
Megapixels float64
|
||||
Size int
|
||||
}{}
|
||||
if err := qb.repository.queryStruct(aggregateQuery.toSQL(includeSortPagination), query.args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := models.NewImageQueryResult(qb)
|
||||
ret.Count = out.Total
|
||||
ret.Megapixels = out.Megapixels
|
||||
ret.TotalSize = out.Size
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (qb *imageQueryBuilder) QueryCount(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) (int, error) {
|
||||
|
||||
@@ -83,14 +83,31 @@ func TestImageQueryQ(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func queryImagesWithCount(sqb models.ImageReader, imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) ([]*models.Image, int, error) {
|
||||
result, err := sqb.Query(models.ImageQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: findFilter,
|
||||
Count: true,
|
||||
},
|
||||
ImageFilter: imageFilter,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
images, err := result.Resolve()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return images, result.Count, nil
|
||||
}
|
||||
|
||||
func imageQueryQ(t *testing.T, sqb models.ImageReader, q string, expectedImageIdx int) {
|
||||
filter := models.FindFilterType{
|
||||
Q: &q,
|
||||
}
|
||||
images, _, err := sqb.Query(nil, &filter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
images := queryImages(t, sqb, nil, &filter)
|
||||
|
||||
assert.Len(t, images, 1)
|
||||
image := images[0]
|
||||
@@ -104,10 +121,7 @@ func imageQueryQ(t *testing.T, sqb models.ImageReader, q string, expectedImageId
|
||||
|
||||
// no Q should return all results
|
||||
filter.Q = nil
|
||||
images, _, err = sqb.Query(nil, &filter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
images = queryImages(t, sqb, nil, &filter)
|
||||
|
||||
assert.Len(t, images, totalImages)
|
||||
}
|
||||
@@ -141,10 +155,7 @@ func verifyImagePath(t *testing.T, pathCriterion models.StringCriterionInput, ex
|
||||
Path: &pathCriterion,
|
||||
}
|
||||
|
||||
images, _, err := sqb.Query(&imageFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
images := queryImages(t, sqb, &imageFilter, nil)
|
||||
|
||||
assert.Equal(t, expected, len(images), "number of returned images")
|
||||
|
||||
@@ -276,17 +287,17 @@ func TestImageIllegalQuery(t *testing.T) {
|
||||
withTxn(func(r models.Repository) error {
|
||||
sqb := r.Image()
|
||||
|
||||
_, _, err := sqb.Query(imageFilter, nil)
|
||||
_, _, err := queryImagesWithCount(sqb, imageFilter, nil)
|
||||
assert.NotNil(err)
|
||||
|
||||
imageFilter.Or = nil
|
||||
imageFilter.Not = &subFilter
|
||||
_, _, err = sqb.Query(imageFilter, nil)
|
||||
_, _, err = queryImagesWithCount(sqb, imageFilter, nil)
|
||||
assert.NotNil(err)
|
||||
|
||||
imageFilter.And = nil
|
||||
imageFilter.Or = &subFilter
|
||||
_, _, err = sqb.Query(imageFilter, nil)
|
||||
_, _, err = queryImagesWithCount(sqb, imageFilter, nil)
|
||||
assert.NotNil(err)
|
||||
|
||||
return nil
|
||||
@@ -325,7 +336,7 @@ func verifyImagesRating(t *testing.T, ratingCriterion models.IntCriterionInput)
|
||||
Rating: &ratingCriterion,
|
||||
}
|
||||
|
||||
images, _, err := sqb.Query(&imageFilter, nil)
|
||||
images, _, err := queryImagesWithCount(sqb, &imageFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -364,7 +375,7 @@ func verifyImagesOCounter(t *testing.T, oCounterCriterion models.IntCriterionInp
|
||||
OCounter: &oCounterCriterion,
|
||||
}
|
||||
|
||||
images, _, err := sqb.Query(&imageFilter, nil)
|
||||
images, _, err := queryImagesWithCount(sqb, &imageFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -396,7 +407,7 @@ func verifyImagesResolution(t *testing.T, resolution models.ResolutionEnum) {
|
||||
},
|
||||
}
|
||||
|
||||
images, _, err := sqb.Query(&imageFilter, nil)
|
||||
images, _, err := queryImagesWithCount(sqb, &imageFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -440,7 +451,7 @@ func TestImageQueryIsMissingGalleries(t *testing.T) {
|
||||
Q: &q,
|
||||
}
|
||||
|
||||
images, _, err := sqb.Query(&imageFilter, &findFilter)
|
||||
images, _, err := queryImagesWithCount(sqb, &imageFilter, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -448,7 +459,7 @@ func TestImageQueryIsMissingGalleries(t *testing.T) {
|
||||
assert.Len(t, images, 0)
|
||||
|
||||
findFilter.Q = nil
|
||||
images, _, err = sqb.Query(&imageFilter, &findFilter)
|
||||
images, _, err = queryImagesWithCount(sqb, &imageFilter, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -475,7 +486,7 @@ func TestImageQueryIsMissingStudio(t *testing.T) {
|
||||
Q: &q,
|
||||
}
|
||||
|
||||
images, _, err := sqb.Query(&imageFilter, &findFilter)
|
||||
images, _, err := queryImagesWithCount(sqb, &imageFilter, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -483,7 +494,7 @@ func TestImageQueryIsMissingStudio(t *testing.T) {
|
||||
assert.Len(t, images, 0)
|
||||
|
||||
findFilter.Q = nil
|
||||
images, _, err = sqb.Query(&imageFilter, &findFilter)
|
||||
images, _, err = queryImagesWithCount(sqb, &imageFilter, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -510,7 +521,7 @@ func TestImageQueryIsMissingPerformers(t *testing.T) {
|
||||
Q: &q,
|
||||
}
|
||||
|
||||
images, _, err := sqb.Query(&imageFilter, &findFilter)
|
||||
images, _, err := queryImagesWithCount(sqb, &imageFilter, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -518,7 +529,7 @@ func TestImageQueryIsMissingPerformers(t *testing.T) {
|
||||
assert.Len(t, images, 0)
|
||||
|
||||
findFilter.Q = nil
|
||||
images, _, err = sqb.Query(&imageFilter, &findFilter)
|
||||
images, _, err = queryImagesWithCount(sqb, &imageFilter, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -547,7 +558,7 @@ func TestImageQueryIsMissingTags(t *testing.T) {
|
||||
Q: &q,
|
||||
}
|
||||
|
||||
images, _, err := sqb.Query(&imageFilter, &findFilter)
|
||||
images, _, err := queryImagesWithCount(sqb, &imageFilter, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -555,7 +566,7 @@ func TestImageQueryIsMissingTags(t *testing.T) {
|
||||
assert.Len(t, images, 0)
|
||||
|
||||
findFilter.Q = nil
|
||||
images, _, err = sqb.Query(&imageFilter, &findFilter)
|
||||
images, _, err = queryImagesWithCount(sqb, &imageFilter, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -574,7 +585,7 @@ func TestImageQueryIsMissingRating(t *testing.T) {
|
||||
IsMissing: &isMissing,
|
||||
}
|
||||
|
||||
images, _, err := sqb.Query(&imageFilter, nil)
|
||||
images, _, err := queryImagesWithCount(sqb, &imageFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -604,7 +615,7 @@ func TestImageQueryGallery(t *testing.T) {
|
||||
Galleries: &galleryCriterion,
|
||||
}
|
||||
|
||||
images, _, err := sqb.Query(&imageFilter, nil)
|
||||
images, _, err := queryImagesWithCount(sqb, &imageFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -624,7 +635,7 @@ func TestImageQueryGallery(t *testing.T) {
|
||||
Modifier: models.CriterionModifierIncludesAll,
|
||||
}
|
||||
|
||||
images, _, err = sqb.Query(&imageFilter, nil)
|
||||
images, _, err = queryImagesWithCount(sqb, &imageFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -644,7 +655,7 @@ func TestImageQueryGallery(t *testing.T) {
|
||||
Q: &q,
|
||||
}
|
||||
|
||||
images, _, err = sqb.Query(&imageFilter, &findFilter)
|
||||
images, _, err = queryImagesWithCount(sqb, &imageFilter, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -669,7 +680,7 @@ func TestImageQueryPerformers(t *testing.T) {
|
||||
Performers: &performerCriterion,
|
||||
}
|
||||
|
||||
images, _, err := sqb.Query(&imageFilter, nil)
|
||||
images, _, err := queryImagesWithCount(sqb, &imageFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -689,7 +700,7 @@ func TestImageQueryPerformers(t *testing.T) {
|
||||
Modifier: models.CriterionModifierIncludesAll,
|
||||
}
|
||||
|
||||
images, _, err = sqb.Query(&imageFilter, nil)
|
||||
images, _, err = queryImagesWithCount(sqb, &imageFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -709,7 +720,7 @@ func TestImageQueryPerformers(t *testing.T) {
|
||||
Q: &q,
|
||||
}
|
||||
|
||||
images, _, err = sqb.Query(&imageFilter, &findFilter)
|
||||
images, _, err = queryImagesWithCount(sqb, &imageFilter, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -734,7 +745,7 @@ func TestImageQueryTags(t *testing.T) {
|
||||
Tags: &tagCriterion,
|
||||
}
|
||||
|
||||
images, _, err := sqb.Query(&imageFilter, nil)
|
||||
images, _, err := queryImagesWithCount(sqb, &imageFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -754,7 +765,7 @@ func TestImageQueryTags(t *testing.T) {
|
||||
Modifier: models.CriterionModifierIncludesAll,
|
||||
}
|
||||
|
||||
images, _, err = sqb.Query(&imageFilter, nil)
|
||||
images, _, err = queryImagesWithCount(sqb, &imageFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -774,7 +785,7 @@ func TestImageQueryTags(t *testing.T) {
|
||||
Q: &q,
|
||||
}
|
||||
|
||||
images, _, err = sqb.Query(&imageFilter, &findFilter)
|
||||
images, _, err = queryImagesWithCount(sqb, &imageFilter, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -798,7 +809,7 @@ func TestImageQueryStudio(t *testing.T) {
|
||||
Studios: &studioCriterion,
|
||||
}
|
||||
|
||||
images, _, err := sqb.Query(&imageFilter, nil)
|
||||
images, _, err := queryImagesWithCount(sqb, &imageFilter, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -820,7 +831,7 @@ func TestImageQueryStudio(t *testing.T) {
|
||||
Q: &q,
|
||||
}
|
||||
|
||||
images, _, err = sqb.Query(&imageFilter, &findFilter)
|
||||
images, _, err = queryImagesWithCount(sqb, &imageFilter, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -892,7 +903,7 @@ func TestImageQueryStudioDepth(t *testing.T) {
|
||||
}
|
||||
|
||||
func queryImages(t *testing.T, sqb models.ImageReader, imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) []*models.Image {
|
||||
images, _, err := sqb.Query(imageFilter, findFilter)
|
||||
images, _, err := queryImagesWithCount(sqb, imageFilter, findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying images: %s", err.Error())
|
||||
}
|
||||
@@ -1047,7 +1058,7 @@ func TestImageQuerySorting(t *testing.T) {
|
||||
}
|
||||
|
||||
sqb := r.Image()
|
||||
images, _, err := sqb.Query(nil, &findFilter)
|
||||
images, _, err := queryImagesWithCount(sqb, nil, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -1062,7 +1073,7 @@ func TestImageQuerySorting(t *testing.T) {
|
||||
// sort in descending order
|
||||
direction = models.SortDirectionEnumDesc
|
||||
|
||||
images, _, err = sqb.Query(nil, &findFilter)
|
||||
images, _, err = queryImagesWithCount(sqb, nil, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -1084,7 +1095,7 @@ func TestImageQueryPagination(t *testing.T) {
|
||||
}
|
||||
|
||||
sqb := r.Image()
|
||||
images, _, err := sqb.Query(nil, &findFilter)
|
||||
images, _, err := queryImagesWithCount(sqb, nil, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -1095,7 +1106,7 @@ func TestImageQueryPagination(t *testing.T) {
|
||||
|
||||
page := 2
|
||||
findFilter.Page = &page
|
||||
images, _, err = sqb.Query(nil, &findFilter)
|
||||
images, _, err = queryImagesWithCount(sqb, nil, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
@@ -1107,7 +1118,7 @@ func TestImageQueryPagination(t *testing.T) {
|
||||
perPage = 2
|
||||
page = 1
|
||||
|
||||
images, _, err = sqb.Query(nil, &findFilter)
|
||||
images, _, err = queryImagesWithCount(sqb, nil, &findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying image: %s", err.Error())
|
||||
}
|
||||
|
||||
@@ -141,8 +141,7 @@ func (qb *movieQueryBuilder) Query(movieFilter *models.MovieFilterType, findFilt
|
||||
}
|
||||
|
||||
query := qb.newQuery()
|
||||
|
||||
query.body = selectDistinctIDs("movies")
|
||||
distinctIDs(&query, movieTable)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
searchColumns := []string{"movies.name"}
|
||||
|
||||
@@ -303,10 +303,8 @@ func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterTy
|
||||
findFilter = &models.FindFilterType{}
|
||||
}
|
||||
|
||||
tableName := "performers"
|
||||
query := qb.newQuery()
|
||||
|
||||
query.body = selectDistinctIDs(tableName)
|
||||
distinctIDs(&query, performerTable)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
searchColumns := []string{"performers.name", "performers.aliases"}
|
||||
|
||||
@@ -665,18 +665,24 @@ func verifyPerformersImageCount(t *testing.T, imageCountCriterion models.IntCrit
|
||||
for _, performer := range performers {
|
||||
pp := 0
|
||||
|
||||
_, count, err := r.Image().Query(&models.ImageFilterType{
|
||||
Performers: &models.MultiCriterionInput{
|
||||
Value: []string{strconv.Itoa(performer.ID)},
|
||||
Modifier: models.CriterionModifierIncludes,
|
||||
result, err := r.Image().Query(models.ImageQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: &models.FindFilterType{
|
||||
PerPage: &pp,
|
||||
},
|
||||
Count: true,
|
||||
},
|
||||
ImageFilter: &models.ImageFilterType{
|
||||
Performers: &models.MultiCriterionInput{
|
||||
Value: []string{strconv.Itoa(performer.ID)},
|
||||
Modifier: models.CriterionModifierIncludes,
|
||||
},
|
||||
},
|
||||
}, &models.FindFilterType{
|
||||
PerPage: &pp,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
verifyInt(t, count, imageCountCriterion)
|
||||
verifyInt(t, result.Count, imageCountCriterion)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type queryBuilder struct {
|
||||
repository *repository
|
||||
|
||||
body string
|
||||
columns []string
|
||||
from string
|
||||
|
||||
joins joins
|
||||
whereClauses []string
|
||||
@@ -21,13 +23,45 @@ type queryBuilder struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (qb queryBuilder) body() string {
|
||||
return fmt.Sprintf("SELECT %s FROM %s%s", strings.Join(qb.columns, ", "), qb.from, qb.joins.toSQL())
|
||||
}
|
||||
|
||||
func (qb *queryBuilder) addColumn(column string) {
|
||||
qb.columns = append(qb.columns, column)
|
||||
}
|
||||
|
||||
func (qb queryBuilder) toSQL(includeSortPagination bool) string {
|
||||
body := qb.body()
|
||||
|
||||
withClause := ""
|
||||
if len(qb.withClauses) > 0 {
|
||||
var recursive string
|
||||
if qb.recursiveWith {
|
||||
recursive = " RECURSIVE "
|
||||
}
|
||||
withClause = "WITH " + recursive + strings.Join(qb.withClauses, ", ") + " "
|
||||
}
|
||||
|
||||
body = withClause + qb.repository.buildQueryBody(body, qb.whereClauses, qb.havingClauses)
|
||||
if includeSortPagination {
|
||||
body += qb.sortAndPagination
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
func (qb queryBuilder) findIDs() ([]int, error) {
|
||||
const includeSortPagination = true
|
||||
return qb.repository.runIdsQuery(qb.toSQL(includeSortPagination), qb.args)
|
||||
}
|
||||
|
||||
func (qb queryBuilder) executeFind() ([]int, int, error) {
|
||||
if qb.err != nil {
|
||||
return nil, 0, qb.err
|
||||
}
|
||||
|
||||
body := qb.body
|
||||
body += qb.joins.toSQL()
|
||||
body := qb.body()
|
||||
|
||||
return qb.repository.executeFindQuery(body, qb.args, qb.sortAndPagination, qb.whereClauses, qb.havingClauses, qb.withClauses, qb.recursiveWith)
|
||||
}
|
||||
@@ -37,8 +71,7 @@ func (qb queryBuilder) executeCount() (int, error) {
|
||||
return 0, qb.err
|
||||
}
|
||||
|
||||
body := qb.body
|
||||
body += qb.joins.toSQL()
|
||||
body := qb.body()
|
||||
|
||||
withClause := ""
|
||||
if len(qb.withClauses) > 0 {
|
||||
|
||||
@@ -33,7 +33,7 @@ func (r *repository) get(id int, dest interface{}) error {
|
||||
|
||||
func (r *repository) getAll(id int, f func(rows *sqlx.Rows) error) error {
|
||||
stmt := fmt.Sprintf("SELECT * FROM %s WHERE %s = ?", r.tableName, r.idColumn)
|
||||
return r.queryFunc(stmt, []interface{}{id}, f)
|
||||
return r.queryFunc(stmt, []interface{}{id}, false, f)
|
||||
}
|
||||
|
||||
func (r *repository) insert(obj interface{}) (sql.Result, error) {
|
||||
@@ -170,7 +170,7 @@ func (r *repository) runSumQuery(query string, args []interface{}) (float64, err
|
||||
return result.Float64, nil
|
||||
}
|
||||
|
||||
func (r *repository) queryFunc(query string, args []interface{}, f func(rows *sqlx.Rows) error) error {
|
||||
func (r *repository) queryFunc(query string, args []interface{}, single bool, f func(rows *sqlx.Rows) error) error {
|
||||
rows, err := r.tx.Queryx(query, args...)
|
||||
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
@@ -182,6 +182,9 @@ func (r *repository) queryFunc(query string, args []interface{}, f func(rows *sq
|
||||
if err := f(rows); err != nil {
|
||||
return err
|
||||
}
|
||||
if single {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
@@ -192,26 +195,23 @@ func (r *repository) queryFunc(query string, args []interface{}, f func(rows *sq
|
||||
}
|
||||
|
||||
func (r *repository) query(query string, args []interface{}, out objectList) error {
|
||||
rows, err := r.tx.Queryx(query, args...)
|
||||
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
return r.queryFunc(query, args, false, func(rows *sqlx.Rows) error {
|
||||
object := out.New()
|
||||
if err := rows.StructScan(object); err != nil {
|
||||
return err
|
||||
}
|
||||
out.Append(object)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
func (r *repository) queryStruct(query string, args []interface{}, out interface{}) error {
|
||||
return r.queryFunc(query, args, true, func(rows *sqlx.Rows) error {
|
||||
if err := rows.StructScan(out); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *repository) querySimple(query string, args []interface{}, out interface{}) error {
|
||||
@@ -361,7 +361,7 @@ type stringRepository struct {
|
||||
func (r *stringRepository) get(id int) ([]string, error) {
|
||||
query := fmt.Sprintf("SELECT %s from %s WHERE %s = ?", r.stringColumn, r.tableName, r.idColumn)
|
||||
var ret []string
|
||||
err := r.queryFunc(query, []interface{}{id}, func(rows *sqlx.Rows) error {
|
||||
err := r.queryFunc(query, []interface{}{id}, false, func(rows *sqlx.Rows) error {
|
||||
var out string
|
||||
if err := rows.Scan(&out); err != nil {
|
||||
return err
|
||||
|
||||
@@ -395,7 +395,10 @@ func (qb *sceneQueryBuilder) makeFilter(sceneFilter *models.SceneFilterType) *fi
|
||||
return query
|
||||
}
|
||||
|
||||
func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilter *models.FindFilterType) ([]*models.Scene, int, error) {
|
||||
func (qb *sceneQueryBuilder) Query(options models.SceneQueryOptions) (*models.SceneQueryResult, error) {
|
||||
sceneFilter := options.SceneFilter
|
||||
findFilter := options.FindFilter
|
||||
|
||||
if sceneFilter == nil {
|
||||
sceneFilter = &models.SceneFilterType{}
|
||||
}
|
||||
@@ -404,8 +407,7 @@ func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilt
|
||||
}
|
||||
|
||||
query := qb.newQuery()
|
||||
|
||||
query.body = selectDistinctIDs(sceneTable)
|
||||
distinctIDs(&query, sceneTable)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
query.join("scene_markers", "", "scene_markers.scene_id = scenes.id")
|
||||
@@ -416,7 +418,7 @@ func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilt
|
||||
}
|
||||
|
||||
if err := qb.validateFilter(sceneFilter); err != nil {
|
||||
return nil, 0, err
|
||||
return nil, err
|
||||
}
|
||||
filter := qb.makeFilter(sceneFilter)
|
||||
|
||||
@@ -425,21 +427,59 @@ func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilt
|
||||
qb.setSceneSort(&query, findFilter)
|
||||
query.sortAndPagination += getPagination(findFilter)
|
||||
|
||||
idsResult, countResult, err := query.executeFind()
|
||||
result, err := qb.queryGroupedFields(options, query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, fmt.Errorf("error querying aggregate fields: %w", err)
|
||||
}
|
||||
|
||||
var scenes []*models.Scene
|
||||
for _, id := range idsResult {
|
||||
scene, err := qb.Find(id)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
scenes = append(scenes, scene)
|
||||
idsResult, err := query.findIDs()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error finding IDs: %w", err)
|
||||
}
|
||||
|
||||
return scenes, countResult, nil
|
||||
result.IDs = idsResult
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (qb *sceneQueryBuilder) queryGroupedFields(options models.SceneQueryOptions, query queryBuilder) (*models.SceneQueryResult, error) {
|
||||
if !options.Count && !options.TotalDuration && !options.TotalSize {
|
||||
// nothing to do - return empty result
|
||||
return models.NewSceneQueryResult(qb), nil
|
||||
}
|
||||
|
||||
aggregateQuery := qb.newQuery()
|
||||
|
||||
if options.Count {
|
||||
aggregateQuery.addColumn("COUNT(temp.id) as total")
|
||||
}
|
||||
|
||||
if options.TotalDuration {
|
||||
query.addColumn("COALESCE(scenes.duration, 0) as duration")
|
||||
aggregateQuery.addColumn("COALESCE(SUM(temp.duration), 0) as duration")
|
||||
}
|
||||
|
||||
if options.TotalSize {
|
||||
query.addColumn("COALESCE(scenes.size, 0) as size")
|
||||
aggregateQuery.addColumn("COALESCE(SUM(temp.size), 0) as size")
|
||||
}
|
||||
|
||||
const includeSortPagination = false
|
||||
aggregateQuery.from = fmt.Sprintf("(%s) as temp", query.toSQL(includeSortPagination))
|
||||
|
||||
out := struct {
|
||||
Total int
|
||||
Duration float64
|
||||
Size int
|
||||
}{}
|
||||
if err := qb.repository.queryStruct(aggregateQuery.toSQL(includeSortPagination), query.args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := models.NewSceneQueryResult(qb)
|
||||
ret.Count = out.Total
|
||||
ret.TotalDuration = out.Duration
|
||||
ret.TotalSize = out.Size
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func phashCriterionHandler(phashFilter *models.StringCriterionInput) criterionHandlerFunc {
|
||||
@@ -848,7 +888,7 @@ func (qb *sceneQueryBuilder) FindDuplicates(distance int) ([][]*models.Scene, er
|
||||
} else {
|
||||
var hashes []*utils.Phash
|
||||
|
||||
if err := qb.queryFunc(findAllPhashesQuery, nil, func(rows *sqlx.Rows) error {
|
||||
if err := qb.queryFunc(findAllPhashesQuery, nil, false, func(rows *sqlx.Rows) error {
|
||||
phash := utils.Phash{
|
||||
Bucket: -1,
|
||||
}
|
||||
|
||||
@@ -147,8 +147,7 @@ func (qb *sceneMarkerQueryBuilder) Query(sceneMarkerFilter *models.SceneMarkerFi
|
||||
}
|
||||
|
||||
query := qb.newQuery()
|
||||
|
||||
query.body = selectDistinctIDs("scene_markers")
|
||||
distinctIDs(&query, sceneMarkerTable)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
searchColumns := []string{"scene_markers.title", "scenes.title"}
|
||||
|
||||
@@ -141,9 +141,19 @@ func TestSceneQueryQ(t *testing.T) {
|
||||
|
||||
func queryScene(t *testing.T, sqb models.SceneReader, sceneFilter *models.SceneFilterType, findFilter *models.FindFilterType) []*models.Scene {
|
||||
t.Helper()
|
||||
scenes, _, err := sqb.Query(sceneFilter, findFilter)
|
||||
result, err := sqb.Query(models.SceneQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: findFilter,
|
||||
},
|
||||
SceneFilter: sceneFilter,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Error querying scene: %s", err.Error())
|
||||
t.Errorf("Error querying scene: %v", err)
|
||||
}
|
||||
|
||||
scenes, err := result.Resolve()
|
||||
if err != nil {
|
||||
t.Errorf("Error resolving scenes: %v", err)
|
||||
}
|
||||
|
||||
return scenes
|
||||
@@ -346,17 +356,21 @@ func TestSceneIllegalQuery(t *testing.T) {
|
||||
withTxn(func(r models.Repository) error {
|
||||
sqb := r.Scene()
|
||||
|
||||
_, _, err := sqb.Query(sceneFilter, nil)
|
||||
queryOptions := models.SceneQueryOptions{
|
||||
SceneFilter: sceneFilter,
|
||||
}
|
||||
|
||||
_, err := sqb.Query(queryOptions)
|
||||
assert.NotNil(err)
|
||||
|
||||
sceneFilter.Or = nil
|
||||
sceneFilter.Not = &subFilter
|
||||
_, _, err = sqb.Query(sceneFilter, nil)
|
||||
_, err = sqb.Query(queryOptions)
|
||||
assert.NotNil(err)
|
||||
|
||||
sceneFilter.And = nil
|
||||
sceneFilter.Or = &subFilter
|
||||
_, _, err = sqb.Query(sceneFilter, nil)
|
||||
_, err = sqb.Query(queryOptions)
|
||||
assert.NotNil(err)
|
||||
|
||||
return nil
|
||||
|
||||
@@ -20,9 +20,9 @@ func selectAll(tableName string) string {
|
||||
return "SELECT " + idColumn + " FROM " + tableName + " "
|
||||
}
|
||||
|
||||
func selectDistinctIDs(tableName string) string {
|
||||
idColumn := getColumn(tableName, "id")
|
||||
return "SELECT DISTINCT " + idColumn + " FROM " + tableName + " "
|
||||
func distinctIDs(qb *queryBuilder, tableName string) {
|
||||
qb.addColumn("DISTINCT " + getColumn(tableName, "id"))
|
||||
qb.from = tableName
|
||||
}
|
||||
|
||||
func getColumn(tableName string, columnName string) string {
|
||||
|
||||
@@ -228,8 +228,7 @@ func (qb *studioQueryBuilder) Query(studioFilter *models.StudioFilterType, findF
|
||||
}
|
||||
|
||||
query := qb.newQuery()
|
||||
|
||||
query.body = selectDistinctIDs("studios")
|
||||
distinctIDs(&query, studioTable)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
query.join(studioAliasesTable, "", "studio_aliases.studio_id = studios.id")
|
||||
|
||||
@@ -507,18 +507,24 @@ func verifyStudiosImageCount(t *testing.T, imageCountCriterion models.IntCriteri
|
||||
for _, studio := range studios {
|
||||
pp := 0
|
||||
|
||||
_, count, err := r.Image().Query(&models.ImageFilterType{
|
||||
Studios: &models.HierarchicalMultiCriterionInput{
|
||||
Value: []string{strconv.Itoa(studio.ID)},
|
||||
Modifier: models.CriterionModifierIncludes,
|
||||
result, err := r.Image().Query(models.ImageQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: &models.FindFilterType{
|
||||
PerPage: &pp,
|
||||
},
|
||||
Count: true,
|
||||
},
|
||||
ImageFilter: &models.ImageFilterType{
|
||||
Studios: &models.HierarchicalMultiCriterionInput{
|
||||
Value: []string{strconv.Itoa(studio.ID)},
|
||||
Modifier: models.CriterionModifierIncludes,
|
||||
},
|
||||
},
|
||||
}, &models.FindFilterType{
|
||||
PerPage: &pp,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
verifyInt(t, count, imageCountCriterion)
|
||||
verifyInt(t, result.Count, imageCountCriterion)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -324,8 +324,7 @@ func (qb *tagQueryBuilder) Query(tagFilter *models.TagFilterType, findFilter *mo
|
||||
}
|
||||
|
||||
query := qb.newQuery()
|
||||
|
||||
query.body = selectDistinctIDs(tagTable)
|
||||
distinctIDs(&query, tagTable)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
query.join(tagAliasesTable, "", "tag_aliases.tag_id = tags.id")
|
||||
|
||||
Reference in New Issue
Block a user