stash/pkg/sqlite/query.go
WithoutPants 0fd7a2ac20
SQL performance improvements (#6378)
* Change queryStruct to use tx.Get instead of queryFunc

Using queryFunc meant that the performance logging was inaccurate due to the query actually being executed during the call to Scan.

* Only add join args if join was added

* Omit joins that are only used for sorting when skipping sorting

Should provide some marginal improvement on systems with a lot of items.

* Make all calls to the database pass context.

This means that long queries can be cancelled by navigating to another page. Previously the query would continue to run, impacting on future queries.
2025-12-08 08:08:31 +11:00

229 lines
4.9 KiB
Go

package sqlite
import (
"context"
"fmt"
"strings"
"github.com/stashapp/stash/pkg/models"
)
type queryBuilder struct {
repository *repository
columns []string
from string
joins joins
whereClauses []string
havingClauses []string
args []interface{}
withClauses []string
recursiveWith bool
sortAndPagination string
}
func (qb queryBuilder) body(includeSortPagination bool) string {
return fmt.Sprintf("SELECT %s FROM %s%s", strings.Join(qb.columns, ", "), qb.from, qb.joins.toSQL(includeSortPagination))
}
func (qb *queryBuilder) addColumn(column string) {
qb.columns = append(qb.columns, column)
}
func (qb queryBuilder) toSQL(includeSortPagination bool) string {
body := qb.body(includeSortPagination)
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(ctx context.Context) ([]int, error) {
const includeSortPagination = true
sql := qb.toSQL(includeSortPagination)
return qb.repository.runIdsQuery(ctx, sql, qb.args)
}
func (qb queryBuilder) executeFind(ctx context.Context) ([]int, int, error) {
const includeSortPagination = true
body := qb.body(includeSortPagination)
return qb.repository.executeFindQuery(ctx, body, qb.args, qb.sortAndPagination, qb.whereClauses, qb.havingClauses, qb.withClauses, qb.recursiveWith)
}
func (qb queryBuilder) executeCount(ctx context.Context) (int, error) {
const includeSortPagination = false
body := qb.body(includeSortPagination)
withClause := ""
if len(qb.withClauses) > 0 {
var recursive string
if qb.recursiveWith {
recursive = " RECURSIVE "
}
withClause = "WITH " + recursive + strings.Join(qb.withClauses, ", ") + " "
}
body = qb.repository.buildQueryBody(body, qb.whereClauses, qb.havingClauses)
countQuery := withClause + qb.repository.buildCountQuery(body)
return qb.repository.runCountQuery(ctx, countQuery, qb.args)
}
func (qb *queryBuilder) addWhere(clauses ...string) {
for _, clause := range clauses {
if len(clause) > 0 {
qb.whereClauses = append(qb.whereClauses, clause)
}
}
}
func (qb *queryBuilder) addHaving(clauses ...string) {
for _, clause := range clauses {
if len(clause) > 0 {
qb.havingClauses = append(qb.havingClauses, clause)
}
}
}
func (qb *queryBuilder) addWith(recursive bool, clauses ...string) {
for _, clause := range clauses {
if len(clause) > 0 {
qb.withClauses = append(qb.withClauses, clause)
}
}
qb.recursiveWith = qb.recursiveWith || recursive
}
func (qb *queryBuilder) addArg(args ...interface{}) {
qb.args = append(qb.args, args...)
}
func (qb *queryBuilder) hasJoin(alias string) bool {
for _, j := range qb.joins {
if j.alias() == alias {
return true
}
}
return false
}
func (qb *queryBuilder) join(table, as, onClause string) {
newJoin := join{
table: table,
as: as,
onClause: onClause,
joinType: "LEFT",
}
qb.joins.add(newJoin)
}
func (qb *queryBuilder) joinSort(table, as, onClause string) {
newJoin := join{
sort: true,
table: table,
as: as,
onClause: onClause,
joinType: "LEFT",
}
qb.joins.add(newJoin)
}
func (qb *queryBuilder) addJoins(joins ...join) {
for _, j := range joins {
if qb.joins.addUnique(j) {
qb.args = append(qb.args, j.args...)
}
}
}
func (qb *queryBuilder) addFilter(f *filterBuilder) error {
err := f.getError()
if err != nil {
return err
}
clause, args := f.generateWithClauses()
if len(clause) > 0 {
qb.addWith(f.recursiveWith, clause)
}
if len(args) > 0 {
// WITH clause always comes first and thus precedes alk args
qb.args = append(args, qb.args...)
}
// add joins here to insert args
qb.addJoins(f.getAllJoins()...)
clause, args = f.generateWhereClauses()
if len(clause) > 0 {
qb.addWhere(clause)
}
if len(args) > 0 {
qb.addArg(args...)
}
clause, args = f.generateHavingClauses()
if len(clause) > 0 {
qb.addHaving(clause)
}
if len(args) > 0 {
qb.addArg(args...)
}
return nil
}
func (qb *queryBuilder) parseQueryString(columns []string, q string) {
specs := models.ParseSearchString(q)
for _, t := range specs.MustHave {
var clauses []string
for _, column := range columns {
clauses = append(clauses, column+" LIKE ?")
qb.addArg(like(t))
}
qb.addWhere("(" + strings.Join(clauses, " OR ") + ")")
}
for _, t := range specs.MustNot {
for _, column := range columns {
qb.addWhere(coalesce(column) + " NOT LIKE ?")
qb.addArg(like(t))
}
}
for _, set := range specs.AnySets {
var clauses []string
for _, column := range columns {
for _, v := range set {
clauses = append(clauses, column+" LIKE ?")
qb.addArg(like(v))
}
}
qb.addWhere("(" + strings.Join(clauses, " OR ") + ")")
}
}