Skip to content

Commit

Permalink
groupby
Browse files Browse the repository at this point in the history
  • Loading branch information
NodudeWasTaken committed Oct 14, 2024
1 parent 878c8e8 commit 088dd14
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 57 deletions.
5 changes: 4 additions & 1 deletion pkg/sqlite/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -941,8 +941,11 @@ func (qb *FileStore) setQuerySort(query *queryBuilder, findFilter *models.FindFi
case "path":
// special handling for path
query.sortAndPagination += fmt.Sprintf(" ORDER BY folders.path %s, files.basename %[1]s", direction)
query.addGroupBy([]string{"folders.path", "files.basename"}, true)
default:
query.sortAndPagination += getSort(sort, direction, "files")
add, agg := getSort(sort, direction, "files")
query.sortAndPagination += add
query.addGroupBy(agg, true)
}

return nil
Expand Down
11 changes: 9 additions & 2 deletions pkg/sqlite/gallery.go
Original file line number Diff line number Diff line change
Expand Up @@ -838,20 +838,27 @@ func (qb *GalleryStore) setGallerySort(query *queryBuilder, findFilter *models.F
addFileTable()
addFolderTable()
query.sortAndPagination += fmt.Sprintf(" ORDER BY COALESCE(folders.path, '') || COALESCE(file_folder.path, '') || COALESCE(files.basename, '') COLLATE NATURAL_CI %s", direction)
query.addGroupBy([]string{"folders.path", "file_folder.path", "files.basename"}, true)
case "file_mod_time":
sort = "mod_time"
addFileTable()
query.sortAndPagination += getSort(sort, direction, fileTable)
add, agg := getSort(sort, direction, fileTable)
query.sortAndPagination += add
query.addGroupBy(agg, true)
case "title":
addFileTable()
addFolderTable()
query.sortAndPagination += " ORDER BY COALESCE(galleries.title, files.basename, basename(COALESCE(folders.path, ''))) COLLATE NATURAL_CI " + direction + ", file_folder.path COLLATE NATURAL_CI " + direction
query.addGroupBy([]string{"galleries.title", "files.basename", "folders.path", "file_folder.path"}, true)
default:
query.sortAndPagination += getSort(sort, direction, "galleries")
add, agg := getSort(sort, direction, "galleries")
query.sortAndPagination += add
query.addGroupBy(agg, true)
}

// Whatever the sorting, always use title/id as a final sort
query.sortAndPagination += ", COALESCE(galleries.title, cast(galleries.id as text)) COLLATE NATURAL_CI ASC"
query.addGroupBy([]string{"galleries.title", "galleries.id"}, true)

return nil
}
Expand Down
13 changes: 10 additions & 3 deletions pkg/sqlite/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,23 +513,30 @@ func (qb *GroupStore) setGroupSort(query *queryBuilder, findFilter *models.FindF
case "sub_group_order":
// sub_group_order is a special sort that sorts by the order_index of the subgroups
if query.hasJoin("groups_parents") {
query.sortAndPagination += getSort("order_index", direction, "groups_parents")
add, agg := getSort("order_index", direction, "groups_parents")
query.sortAndPagination += add
query.addGroupBy(agg, true)
} else {
// this will give unexpected results if the query is not filtered by a parent group and
// the group has multiple parents and order indexes
query.join(groupRelationsTable, "", "groups.id = groups_relations.sub_id")
query.sortAndPagination += getSort("order_index", direction, groupRelationsTable)
add, agg := getSort("order_index", direction, groupRelationsTable)
query.sortAndPagination += add
query.addGroupBy(agg, true)
}
case "tag_count":
query.sortAndPagination += getCountSort(groupTable, groupsTagsTable, groupIDColumn, direction)
case "scenes_count": // generic getSort won't work for this
query.sortAndPagination += getCountSort(groupTable, groupsScenesTable, groupIDColumn, direction)
default:
query.sortAndPagination += getSort(sort, direction, "groups")
add, agg := getSort(sort, direction, "groups")
query.sortAndPagination += add
query.addGroupBy(agg, true)
}

// Whatever the sorting, always use name/id as a final sort
query.sortAndPagination += ", COALESCE(groups.name, cast(groups.id as text)) COLLATE NATURAL_CI ASC"
query.addGroupBy([]string{"groups.name", "groups.id"}, true)
return nil
}

Expand Down
11 changes: 9 additions & 2 deletions pkg/sqlite/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,7 @@ func (qb *ImageStore) setImageSortAndPagination(q *queryBuilder, findFilter *mod
addFilesJoin()
addFolderJoin()
sortClause = " ORDER BY COALESCE(folders.path, '') || COALESCE(files.basename, '') COLLATE NATURAL_CI " + direction
q.addGroupBy([]string{"folders.path", "files.basename"}, true)
case "file_count":
sortClause = getCountSort(imageTable, imagesFilesTable, imageIDColumn, direction)
case "tag_count":
Expand All @@ -977,17 +978,23 @@ func (qb *ImageStore) setImageSortAndPagination(q *queryBuilder, findFilter *mod
sortClause = getCountSort(imageTable, performersImagesTable, imageIDColumn, direction)
case "mod_time", "filesize":
addFilesJoin()
sortClause = getSort(sort, direction, "files")
add, agg := getSort(sort, direction, "files")
sortClause = add
q.addGroupBy(agg, true)
case "title":
addFilesJoin()
addFolderJoin()
sortClause = " ORDER BY COALESCE(images.title, files.basename) COLLATE NATURAL_CI " + direction + ", folders.path COLLATE NATURAL_CI " + direction
q.addGroupBy([]string{"images.title", "files.basename", "folders.path"}, true)
default:
sortClause = getSort(sort, direction, "images")
add, agg := getSort(sort, direction, "images")
sortClause = add
q.addGroupBy(agg, true)
}

// Whatever the sorting, always use title/id as a final sort
sortClause += ", COALESCE(images.title, cast(images.id as text)) COLLATE NATURAL_CI ASC"
q.addGroupBy([]string{"images.title", "images.id"}, true)
}

q.sortAndPagination = sortClause + getPagination(findFilter)
Expand Down
16 changes: 11 additions & 5 deletions pkg/sqlite/performer.go
Original file line number Diff line number Diff line change
Expand Up @@ -613,11 +613,13 @@ func (qb *PerformerStore) makeQuery(ctx context.Context, performerFilter *models
}

var err error
query.sortAndPagination, err = qb.getPerformerSort(findFilter)
var agg []string
query.sortAndPagination, agg, err = qb.getPerformerSort(findFilter)
if err != nil {
return nil, err
}
query.sortAndPagination += getPagination(findFilter)
query.addGroupBy(agg, true)

return &query, nil
}
Expand Down Expand Up @@ -731,7 +733,7 @@ var performerSortOptions = sortOptions{
"weight",
}

func (qb *PerformerStore) getPerformerSort(findFilter *models.FindFilterType) (string, error) {
func (qb *PerformerStore) getPerformerSort(findFilter *models.FindFilterType) (string, []string, error) {
var sort string
var direction string
if findFilter == nil {
Expand All @@ -744,9 +746,10 @@ func (qb *PerformerStore) getPerformerSort(findFilter *models.FindFilterType) (s

// CVE-2024-32231 - ensure sort is in the list of allowed sorts
if err := performerSortOptions.validateSort(sort); err != nil {
return "", err
return "", nil, err
}

var agg []string
sortQuery := ""
switch sort {
case "tag_count":
Expand All @@ -766,12 +769,15 @@ func (qb *PerformerStore) getPerformerSort(findFilter *models.FindFilterType) (s
case "last_o_at":
sortQuery += qb.sortByLastOAt(direction)
default:
sortQuery += getSort(sort, direction, "performers")
var add string
add, agg = getSort(sort, direction, "performers")
sortQuery += add
}

// Whatever the sorting, always use name/id as a final sort
sortQuery += ", COALESCE(performers.name, cast(performers.id as text)) COLLATE NATURAL_CI ASC"
return sortQuery, nil
agg = append(agg, "performers.name", "performers.id")
return sortQuery, agg, nil
}

func (qb *PerformerStore) GetTagIDs(ctx context.Context, id int) ([]int, error) {
Expand Down
13 changes: 8 additions & 5 deletions pkg/sqlite/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"

"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil"
)

type queryBuilder struct {
Expand Down Expand Up @@ -35,13 +36,15 @@ func (qb queryBuilder) body() string {
*/
func (qb *queryBuilder) addColumn(column string, nonaggregates []string) {
qb.columns = append(qb.columns, column)
if len(nonaggregates) > 0 && dbWrapper.dbType == PostgresBackend {
qb.addGroupBy(nonaggregates)
}
qb.addGroupBy(nonaggregates, dbWrapper.dbType == PostgresBackend)
}

func (qb *queryBuilder) addGroupBy(aggregate []string) {
qb.groupByClauses = append(qb.groupByClauses, aggregate...)
func (qb *queryBuilder) addGroupBy(aggregate []string, pgsqlfix bool) {
if !pgsqlfix || len(aggregate) == 0 {
return
}

qb.groupByClauses = sliceutil.AppendUniques(qb.groupByClauses, aggregate)
}

func (qb queryBuilder) toSQL(includeSortPagination bool) string {
Expand Down
40 changes: 31 additions & 9 deletions pkg/sqlite/scene.go
Original file line number Diff line number Diff line change
Expand Up @@ -1130,10 +1130,14 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF
switch sort {
case "movie_scene_number":
query.join(groupsScenesTable, "", "scenes.id = groups_scenes.scene_id")
query.sortAndPagination += getSort("scene_index", direction, groupsScenesTable)
add, agg := getSort("scene_index", direction, groupsScenesTable)
query.sortAndPagination += add
query.addGroupBy(agg, true)
case "group_scene_number":
query.join(groupsScenesTable, "scene_group", "scenes.id = scene_group.scene_id")
query.sortAndPagination += getSort("scene_index", direction, "scene_group")
add, agg := getSort("scene_index", direction, "scene_group")
query.sortAndPagination += add
query.addGroupBy(agg, true)
case "tag_count":
query.sortAndPagination += getCountSort(sceneTable, scenesTagsTable, sceneIDColumn, direction)
case "performer_count":
Expand All @@ -1145,6 +1149,7 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF
addFileTable()
addFolderTable()
query.sortAndPagination += fmt.Sprintf(" ORDER BY COALESCE(folders.path, '') || COALESCE(files.basename, '') COLLATE NATURAL_CI %s", direction)
query.addGroupBy([]string{"folders.path", "files.basename"}, true)
case "perceptual_similarity":
// special handling for phash
addFileTable()
Expand All @@ -1157,31 +1162,45 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF
)

query.sortAndPagination += " ORDER BY fingerprints_phash.fingerprint " + direction + ", files.size DESC"
query.addGroupBy([]string{"fingerprints_phash.fingerprint", "files.size"}, true)
case "bitrate":
sort = "bit_rate"
addVideoFileTable()
query.sortAndPagination += getSort(sort, direction, videoFileTable)
add, agg := getSort(sort, direction, videoFileTable)
query.sortAndPagination += add
query.addGroupBy(agg, true)
case "file_mod_time":
sort = "mod_time"
addFileTable()
query.sortAndPagination += getSort(sort, direction, fileTable)
add, agg := getSort(sort, direction, fileTable)
query.sortAndPagination += add
query.addGroupBy(agg, true)
case "framerate":
sort = "frame_rate"
addVideoFileTable()
query.sortAndPagination += getSort(sort, direction, videoFileTable)
add, agg := getSort(sort, direction, videoFileTable)
query.sortAndPagination += add
query.addGroupBy(agg, true)
case "filesize":
addFileTable()
query.sortAndPagination += getSort(sort, direction, fileTable)
add, agg := getSort(sort, direction, fileTable)
query.sortAndPagination += add
query.addGroupBy(agg, true)
case "duration":
addVideoFileTable()
query.sortAndPagination += getSort(sort, direction, videoFileTable)
add, agg := getSort(sort, direction, videoFileTable)
query.sortAndPagination += add
query.addGroupBy(agg, true)
case "interactive", "interactive_speed":
addVideoFileTable()
query.sortAndPagination += getSort(sort, direction, videoFileTable)
add, agg := getSort(sort, direction, videoFileTable)
query.sortAndPagination += add
query.addGroupBy(agg, true)
case "title":
addFileTable()
addFolderTable()
query.sortAndPagination += " ORDER BY COALESCE(scenes.title, files.basename) COLLATE NATURAL_CI " + direction + ", folders.path COLLATE NATURAL_CI " + direction
query.addGroupBy([]string{"scenes.title", "files.basename", "folders.path"}, true)
case "play_count":
query.sortAndPagination += getCountSort(sceneTable, scenesViewDatesTable, sceneIDColumn, direction)
case "last_played_at":
Expand All @@ -1191,11 +1210,14 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF
case "o_counter":
query.sortAndPagination += getCountSort(sceneTable, scenesODatesTable, sceneIDColumn, direction)
default:
query.sortAndPagination += getSort(sort, direction, "scenes")
add, agg := getSort(sort, direction, "scenes")
query.sortAndPagination += add
query.addGroupBy(agg, true)
}

// Whatever the sorting, always use title/id as a final sort
query.sortAndPagination += ", COALESCE(scenes.title, cast(scenes.id as text)) COLLATE NATURAL_CI ASC"
query.addGroupBy([]string{"scenes.title", "scenes.id"}, true)

return nil
}
Expand Down
8 changes: 6 additions & 2 deletions pkg/sqlite/scene_marker.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,12 +375,16 @@ func (qb *SceneMarkerStore) setSceneMarkerSort(query *queryBuilder, findFilter *
case "scenes_updated_at":
sort = "updated_at"
query.join(sceneTable, "", "scenes.id = scene_markers.scene_id")
query.sortAndPagination += getSort(sort, direction, sceneTable)
add, agg := getSort(sort, direction, sceneTable)
query.sortAndPagination += add
query.addGroupBy(agg, true)
case "title":
query.join(tagTable, "", "scene_markers.primary_tag_id = tags.id")
query.sortAndPagination += " ORDER BY COALESCE(NULLIF(scene_markers.title,''), tags.name) COLLATE NATURAL_CI " + direction
default:
query.sortAndPagination += getSort(sort, direction, sceneMarkerTable)
add, agg := getSort(sort, direction, sceneMarkerTable)
query.sortAndPagination += add
query.addGroupBy(agg, true)
}

query.sortAndPagination += ", scene_markers.scene_id ASC, scene_markers.seconds ASC"
Expand Down
21 changes: 13 additions & 8 deletions pkg/sqlite/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,20 @@ func getSortDirection(direction string) string {
return direction
}
}
func getSort(sort string, direction string, tableName string) string {
func getSort(sort string, direction string, tableName string) (string, []string) {
direction = getSortDirection(direction)
nonaggregates := []string{}

switch {
case strings.HasSuffix(sort, "_count"):
var relationTableName = strings.TrimSuffix(sort, "_count") // TODO: pluralize?
colName := getColumn(relationTableName, "id")
return " ORDER BY COUNT(distinct " + colName + ") " + direction
nonaggregates = append(nonaggregates, colName)
return " ORDER BY COUNT(distinct " + colName + ") " + direction, nonaggregates
case strings.Compare(sort, "filesize") == 0:
colName := getColumn(tableName, "size")
return " ORDER BY " + colName + " " + direction
nonaggregates = append(nonaggregates, colName)
return " ORDER BY " + colName + " " + direction, nonaggregates
case strings.HasPrefix(sort, randomSeedPrefix):
// seed as a parameter from the UI
seedStr := sort[len(randomSeedPrefix):]
Expand All @@ -108,22 +111,24 @@ func getSort(sort string, direction string, tableName string) string {
// fallback to a random seed
seed = rand.Uint64()
}
return getRandomSort(tableName, direction, seed)
return getRandomSort(tableName, direction, seed), nonaggregates
case strings.Compare(sort, "random") == 0:
return getRandomSort(tableName, direction, rand.Uint64())
return getRandomSort(tableName, direction, rand.Uint64()), nonaggregates
default:
colName := getColumn(tableName, sort)
if strings.Contains(sort, ".") {
colName = sort
}
nonaggregates = append(nonaggregates, colName)

if strings.Compare(sort, "name") == 0 {
return " ORDER BY " + colName + " COLLATE NATURAL_CI " + direction
return " ORDER BY " + colName + " COLLATE NATURAL_CI " + direction, nonaggregates
}
if strings.Compare(sort, "title") == 0 {
return " ORDER BY " + colName + " COLLATE NATURAL_CI " + direction
return " ORDER BY " + colName + " COLLATE NATURAL_CI " + direction, nonaggregates
}

return " ORDER BY " + colName + " " + direction
return " ORDER BY " + colName + " " + direction, nonaggregates
}
}

Expand Down
Loading

0 comments on commit 088dd14

Please sign in to comment.