mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Add sqlite filter builder. Add AND, OR, NOT filters to scene filter (#1115)
* Add resolution enum extension * Add filter builder * Use filterBuilder for scene query * Optimise joins * Add binary operators to scene query * Use Query for auto-tag
This commit is contained in:
@@ -75,6 +75,10 @@ input SceneMarkerFilterType {
|
||||
}
|
||||
|
||||
input SceneFilterType {
|
||||
AND: SceneFilterType
|
||||
OR: SceneFilterType
|
||||
NOT: SceneFilterType
|
||||
|
||||
"""Filter by path"""
|
||||
path: StringCriterionInput
|
||||
"""Filter by rating"""
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -38,13 +39,56 @@ func (t *AutoTagTask) getQueryRegex(name string) string {
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t *AutoTagTask) getQueryFilter(regex string) *models.SceneFilterType {
|
||||
organized := false
|
||||
ret := &models.SceneFilterType{
|
||||
Path: &models.StringCriterionInput{
|
||||
Modifier: models.CriterionModifierMatchesRegex,
|
||||
Value: "(?i)" + regex,
|
||||
},
|
||||
Organized: &organized,
|
||||
}
|
||||
|
||||
sep := string(filepath.Separator)
|
||||
|
||||
var or *models.SceneFilterType
|
||||
for _, p := range t.paths {
|
||||
newOr := &models.SceneFilterType{}
|
||||
if or == nil {
|
||||
ret.And = newOr
|
||||
} else {
|
||||
or.Or = newOr
|
||||
}
|
||||
|
||||
or = newOr
|
||||
|
||||
if !strings.HasSuffix(p, sep) {
|
||||
p = p + sep
|
||||
}
|
||||
|
||||
or.Path = &models.StringCriterionInput{
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
Value: p + "%",
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t *AutoTagTask) getFindFilter() *models.FindFilterType {
|
||||
perPage := 0
|
||||
return &models.FindFilterType{
|
||||
PerPage: &perPage,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *AutoTagPerformerTask) autoTagPerformer() {
|
||||
regex := t.getQueryRegex(t.performer.Name.String)
|
||||
|
||||
if err := t.txnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
||||
qb := r.Scene()
|
||||
|
||||
scenes, err := qb.QueryForAutoTag(regex, t.paths)
|
||||
scenes, _, err := qb.Query(t.getQueryFilter(regex), t.getFindFilter())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error querying scenes with regex '%s': %s", regex, err.Error())
|
||||
@@ -84,7 +128,7 @@ func (t *AutoTagStudioTask) autoTagStudio() {
|
||||
|
||||
if err := t.txnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
||||
qb := r.Scene()
|
||||
scenes, err := qb.QueryForAutoTag(regex, t.paths)
|
||||
scenes, _, err := qb.Query(t.getQueryFilter(regex), t.getFindFilter())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error querying scenes with regex '%s': %s", regex, err.Error())
|
||||
@@ -133,7 +177,7 @@ func (t *AutoTagTagTask) autoTagTag() {
|
||||
|
||||
if err := t.txnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
||||
qb := r.Scene()
|
||||
scenes, err := qb.QueryForAutoTag(regex, t.paths)
|
||||
scenes, _, err := qb.Query(t.getQueryFilter(regex), t.getFindFilter())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error querying scenes with regex '%s': %s", regex, err.Error())
|
||||
|
||||
65
pkg/models/extension_resolution.go
Normal file
65
pkg/models/extension_resolution.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package models
|
||||
|
||||
var resolutionMax = []int{
|
||||
240,
|
||||
360,
|
||||
480,
|
||||
540,
|
||||
720,
|
||||
1080,
|
||||
1440,
|
||||
1920,
|
||||
2160,
|
||||
2880,
|
||||
3384,
|
||||
4320,
|
||||
0,
|
||||
}
|
||||
|
||||
// GetMaxResolution returns the maximum width or height that media must be
|
||||
// to qualify as this resolution. A return value of 0 means that there is no
|
||||
// maximum.
|
||||
func (r *ResolutionEnum) GetMaxResolution() int {
|
||||
if !r.IsValid() {
|
||||
return 0
|
||||
}
|
||||
|
||||
// sanity check - length of arrays must be the same
|
||||
if len(resolutionMax) != len(AllResolutionEnum) {
|
||||
panic("resolutionMax array length != AllResolutionEnum array length")
|
||||
}
|
||||
|
||||
for i, rr := range AllResolutionEnum {
|
||||
if rr == *r {
|
||||
return resolutionMax[i]
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetMinResolution returns the minimum width or height that media must be
|
||||
// to qualify as this resolution.
|
||||
func (r *ResolutionEnum) GetMinResolution() int {
|
||||
if !r.IsValid() {
|
||||
return 0
|
||||
}
|
||||
|
||||
// sanity check - length of arrays must be the same
|
||||
if len(resolutionMax) != len(AllResolutionEnum) {
|
||||
panic("resolutionMax array length != AllResolutionEnum array length")
|
||||
}
|
||||
|
||||
// use the previous resolution max as this resolution min
|
||||
for i, rr := range AllResolutionEnum {
|
||||
if rr == *r {
|
||||
if i > 0 {
|
||||
return resolutionMax[i-1]
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -21,7 +21,6 @@ type SceneReader interface {
|
||||
CountMissingOSHash() (int, error)
|
||||
Wall(q *string) ([]*Scene, error)
|
||||
All() ([]*Scene, error)
|
||||
QueryForAutoTag(regex string, pathPrefixes []string) ([]*Scene, error)
|
||||
Query(sceneFilter *SceneFilterType, findFilter *FindFilterType) ([]*Scene, int, error)
|
||||
GetCover(sceneID int) ([]byte, error)
|
||||
GetMovies(sceneID int) ([]MoviesScenes, error)
|
||||
|
||||
400
pkg/sqlite/filter.go
Normal file
400
pkg/sqlite/filter.go
Normal file
@@ -0,0 +1,400 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type sqlClause struct {
|
||||
sql string
|
||||
args []interface{}
|
||||
}
|
||||
|
||||
func makeClause(sql string, args ...interface{}) sqlClause {
|
||||
return sqlClause{
|
||||
sql: sql,
|
||||
args: args,
|
||||
}
|
||||
}
|
||||
|
||||
type criterionHandler interface {
|
||||
handle(f *filterBuilder)
|
||||
}
|
||||
|
||||
type criterionHandlerFunc func(f *filterBuilder)
|
||||
|
||||
type join struct {
|
||||
table string
|
||||
as string
|
||||
onClause string
|
||||
}
|
||||
|
||||
// equals returns true if the other join alias/table is equal to this one
|
||||
func (j join) equals(o join) bool {
|
||||
return j.alias() == o.alias()
|
||||
}
|
||||
|
||||
// alias returns the as string, or the table if as is empty
|
||||
func (j join) alias() string {
|
||||
if j.as == "" {
|
||||
return j.table
|
||||
}
|
||||
|
||||
return j.as
|
||||
}
|
||||
|
||||
func (j join) toSQL() string {
|
||||
asStr := ""
|
||||
if j.as != "" && j.as != j.table {
|
||||
asStr = " AS " + j.as
|
||||
}
|
||||
|
||||
return fmt.Sprintf("LEFT JOIN %s%s ON %s", j.table, asStr, j.onClause)
|
||||
}
|
||||
|
||||
type joins []join
|
||||
|
||||
func (j *joins) add(newJoins ...join) {
|
||||
// only add if not already joined
|
||||
for _, newJoin := range newJoins {
|
||||
for _, jj := range *j {
|
||||
if jj.equals(newJoin) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
*j = append(*j, newJoin)
|
||||
}
|
||||
}
|
||||
|
||||
func (j *joins) toSQL() string {
|
||||
var ret []string
|
||||
for _, jj := range *j {
|
||||
ret = append(ret, jj.toSQL())
|
||||
}
|
||||
|
||||
return strings.Join(ret, " ")
|
||||
}
|
||||
|
||||
type filterBuilder struct {
|
||||
subFilter *filterBuilder
|
||||
subFilterOp string
|
||||
|
||||
joins joins
|
||||
whereClauses []sqlClause
|
||||
havingClauses []sqlClause
|
||||
|
||||
err error
|
||||
}
|
||||
|
||||
var errSubFilterAlreadySet error = errors.New(`sub-filter already set`)
|
||||
|
||||
// sub-filter operator values
|
||||
var (
|
||||
andOp = "AND"
|
||||
orOp = "OR"
|
||||
notOp = "AND NOT"
|
||||
)
|
||||
|
||||
// and sets the sub-filter that will be ANDed with this one.
|
||||
// Sets the error state if sub-filter is already set.
|
||||
func (f *filterBuilder) and(a *filterBuilder) {
|
||||
if f.subFilter != nil {
|
||||
f.setError(errSubFilterAlreadySet)
|
||||
return
|
||||
}
|
||||
|
||||
f.subFilter = a
|
||||
f.subFilterOp = andOp
|
||||
}
|
||||
|
||||
// or sets the sub-filter that will be ORed with this one.
|
||||
// Sets the error state if a sub-filter is already set.
|
||||
func (f *filterBuilder) or(o *filterBuilder) {
|
||||
if f.subFilter != nil {
|
||||
f.setError(errSubFilterAlreadySet)
|
||||
return
|
||||
}
|
||||
|
||||
f.subFilter = o
|
||||
f.subFilterOp = orOp
|
||||
}
|
||||
|
||||
// not sets the sub-filter that will be AND NOTed with this one.
|
||||
// Sets the error state if a sub-filter is already set.
|
||||
func (f *filterBuilder) not(n *filterBuilder) {
|
||||
if f.subFilter != nil {
|
||||
f.setError(errSubFilterAlreadySet)
|
||||
return
|
||||
}
|
||||
|
||||
f.subFilter = n
|
||||
f.subFilterOp = notOp
|
||||
}
|
||||
|
||||
// addJoin adds a join to the filter. The join is expressed in SQL as:
|
||||
// LEFT JOIN <table> [AS <as>] ON <onClause>
|
||||
// The AS is omitted if as is empty.
|
||||
// This method does not add a join if it its alias/table name is already
|
||||
// present in another existing join.
|
||||
func (f *filterBuilder) addJoin(table, as, onClause string) {
|
||||
newJoin := join{
|
||||
table: table,
|
||||
as: as,
|
||||
onClause: onClause,
|
||||
}
|
||||
|
||||
f.joins.add(newJoin)
|
||||
}
|
||||
|
||||
// addWhere adds a where clause and arguments to the filter. Where clauses
|
||||
// are ANDed together. Does not add anything if the provided string is empty.
|
||||
func (f *filterBuilder) addWhere(sql string, args ...interface{}) {
|
||||
if sql == "" {
|
||||
return
|
||||
}
|
||||
f.whereClauses = append(f.whereClauses, makeClause(sql, args...))
|
||||
}
|
||||
|
||||
// addHaving adds a where clause and arguments to the filter. Having clauses
|
||||
// are ANDed together. Does not add anything if the provided string is empty.
|
||||
func (f *filterBuilder) addHaving(sql string, args ...interface{}) {
|
||||
if sql == "" {
|
||||
return
|
||||
}
|
||||
f.havingClauses = append(f.havingClauses, makeClause(sql, args...))
|
||||
}
|
||||
|
||||
func (f *filterBuilder) getSubFilterClause(clause, subFilterClause string) string {
|
||||
ret := clause
|
||||
|
||||
if subFilterClause != "" {
|
||||
var op string
|
||||
if len(ret) > 0 {
|
||||
op = " " + f.subFilterOp + " "
|
||||
} else {
|
||||
if f.subFilterOp == notOp {
|
||||
op = "NOT "
|
||||
}
|
||||
}
|
||||
|
||||
ret += op + subFilterClause
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// generateWhereClauses generates the SQL where clause for this filter.
|
||||
// All where clauses within the filter are ANDed together. This is combined
|
||||
// with the sub-filter, which will use the applicable operator (AND/OR/AND NOT).
|
||||
func (f *filterBuilder) generateWhereClauses() (clause string, args []interface{}) {
|
||||
clause, args = f.andClauses(f.whereClauses)
|
||||
|
||||
if f.subFilter != nil {
|
||||
c, a := f.subFilter.generateWhereClauses()
|
||||
if c != "" {
|
||||
clause = f.getSubFilterClause(clause, c)
|
||||
if len(a) > 0 {
|
||||
args = append(args, a...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// generateHavingClauses generates the SQL having clause for this filter.
|
||||
// All having clauses within the filter are ANDed together. This is combined
|
||||
// with the sub-filter, which will use the applicable operator (AND/OR/AND NOT).
|
||||
func (f *filterBuilder) generateHavingClauses() (string, []interface{}) {
|
||||
clause, args := f.andClauses(f.havingClauses)
|
||||
|
||||
if f.subFilter != nil {
|
||||
c, a := f.subFilter.generateHavingClauses()
|
||||
if c != "" {
|
||||
clause += " " + f.subFilterOp + " " + c
|
||||
if len(a) > 0 {
|
||||
args = append(args, a...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return clause, args
|
||||
}
|
||||
|
||||
// getAllJoins returns all of the joins in this filter and any sub-filter(s).
|
||||
// Redundant joins will not be duplicated in the return value.
|
||||
func (f *filterBuilder) getAllJoins() joins {
|
||||
var ret joins
|
||||
ret.add(f.joins...)
|
||||
if f.subFilter != nil {
|
||||
subJoins := f.subFilter.getAllJoins()
|
||||
if len(subJoins) > 0 {
|
||||
ret.add(subJoins...)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// getError returns the error state on this filter, or on any sub-filter(s) if
|
||||
// the error state is nil.
|
||||
func (f *filterBuilder) getError() error {
|
||||
if f.err != nil {
|
||||
return f.err
|
||||
}
|
||||
|
||||
if f.subFilter != nil {
|
||||
return f.subFilter.getError()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleCriterion calls the handle function on the provided criterionHandler,
|
||||
// providing itself.
|
||||
func (f *filterBuilder) handleCriterion(handler criterionHandler) {
|
||||
f.handleCriterionFunc(func(h *filterBuilder) {
|
||||
handler.handle(h)
|
||||
})
|
||||
}
|
||||
|
||||
// handleCriterionFunc calls the provided criterion handler function providing
|
||||
// itself.
|
||||
func (f *filterBuilder) handleCriterionFunc(handler criterionHandlerFunc) {
|
||||
handler(f)
|
||||
}
|
||||
|
||||
func (f *filterBuilder) setError(e error) {
|
||||
if f.err == nil {
|
||||
f.err = e
|
||||
}
|
||||
}
|
||||
|
||||
func (f *filterBuilder) andClauses(input []sqlClause) (string, []interface{}) {
|
||||
var clauses []string
|
||||
var args []interface{}
|
||||
for _, w := range input {
|
||||
clauses = append(clauses, w.sql)
|
||||
args = append(args, w.args...)
|
||||
}
|
||||
|
||||
if len(clauses) > 0 {
|
||||
c := "(" + strings.Join(clauses, " AND ") + ")"
|
||||
return c, args
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func stringCriterionHandler(c *models.StringCriterionInput, column string) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if c != nil {
|
||||
if modifier := c.Modifier; c.Modifier.IsValid() {
|
||||
switch modifier {
|
||||
case models.CriterionModifierIncludes:
|
||||
clause, thisArgs := getSearchBinding([]string{column}, c.Value, false)
|
||||
f.addWhere(clause, thisArgs...)
|
||||
case models.CriterionModifierExcludes:
|
||||
clause, thisArgs := getSearchBinding([]string{column}, c.Value, true)
|
||||
f.addWhere(clause, thisArgs...)
|
||||
case models.CriterionModifierEquals:
|
||||
f.addWhere(column+" LIKE ?", c.Value)
|
||||
case models.CriterionModifierNotEquals:
|
||||
f.addWhere(column+" NOT LIKE ?", c.Value)
|
||||
case models.CriterionModifierMatchesRegex:
|
||||
if _, err := regexp.Compile(c.Value); err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
f.addWhere(column+" regexp ?", c.Value)
|
||||
case models.CriterionModifierNotMatchesRegex:
|
||||
if _, err := regexp.Compile(c.Value); err != nil {
|
||||
f.setError(err)
|
||||
return
|
||||
}
|
||||
f.addWhere(column+" NOT regexp ?", c.Value)
|
||||
default:
|
||||
clause, count := getSimpleCriterionClause(modifier, "?")
|
||||
|
||||
if count == 1 {
|
||||
f.addWhere(column+" "+clause, c.Value)
|
||||
} else {
|
||||
f.addWhere(column + " " + clause)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func intCriterionHandler(c *models.IntCriterionInput, column string) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if c != nil {
|
||||
clause, count := getIntCriterionWhereClause(column, *c)
|
||||
|
||||
if count == 1 {
|
||||
f.addWhere(clause, c.Value)
|
||||
} else {
|
||||
f.addWhere(clause)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func boolCriterionHandler(c *bool, column string) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if c != nil {
|
||||
var v string
|
||||
if *c {
|
||||
v = "1"
|
||||
} else {
|
||||
v = "0"
|
||||
}
|
||||
|
||||
f.addWhere(column + " = " + v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stringLiteralCriterionHandler(v *string, column string) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if v != nil {
|
||||
f.addWhere(column+" = ?", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type multiCriterionHandlerBuilder struct {
|
||||
primaryTable string
|
||||
foreignTable string
|
||||
joinTable string
|
||||
primaryFK string
|
||||
foreignFK string
|
||||
|
||||
// function that will be called to perform any necessary joins
|
||||
addJoinsFunc func(f *filterBuilder)
|
||||
}
|
||||
|
||||
func (m *multiCriterionHandlerBuilder) handler(criterion *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if criterion != nil && len(criterion.Value) > 0 {
|
||||
var args []interface{}
|
||||
for _, tagID := range criterion.Value {
|
||||
args = append(args, tagID)
|
||||
}
|
||||
|
||||
if m.addJoinsFunc != nil {
|
||||
m.addJoinsFunc(f)
|
||||
}
|
||||
|
||||
whereClause, havingClause := getMultiCriterionClause(m.primaryTable, m.foreignTable, m.joinTable, m.primaryFK, m.foreignFK, criterion)
|
||||
f.addWhere(whereClause, args...)
|
||||
f.addHaving(havingClause)
|
||||
}
|
||||
}
|
||||
}
|
||||
603
pkg/sqlite/filter_internal_test.go
Normal file
603
pkg/sqlite/filter_internal_test.go
Normal file
@@ -0,0 +1,603 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilterBuilderAnd(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
f := &filterBuilder{}
|
||||
other := &filterBuilder{}
|
||||
newBuilder := &filterBuilder{}
|
||||
|
||||
// and should set the subFilter
|
||||
f.and(other)
|
||||
assert.Equal(other, f.subFilter)
|
||||
assert.Nil(f.getError())
|
||||
|
||||
// and should set error if and is set
|
||||
f.and(newBuilder)
|
||||
assert.Equal(other, f.subFilter)
|
||||
assert.Equal(errSubFilterAlreadySet, f.getError())
|
||||
|
||||
// and should set error if or is set
|
||||
// and should not set subFilter if or is set
|
||||
f = &filterBuilder{}
|
||||
f.or(other)
|
||||
f.and(newBuilder)
|
||||
assert.Equal(other, f.subFilter)
|
||||
assert.Equal(errSubFilterAlreadySet, f.getError())
|
||||
|
||||
// and should set error if not is set
|
||||
// and should not set subFilter if not is set
|
||||
f = &filterBuilder{}
|
||||
f.not(other)
|
||||
f.and(newBuilder)
|
||||
assert.Equal(other, f.subFilter)
|
||||
assert.Equal(errSubFilterAlreadySet, f.getError())
|
||||
}
|
||||
|
||||
func TestFilterBuilderOr(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
f := &filterBuilder{}
|
||||
other := &filterBuilder{}
|
||||
newBuilder := &filterBuilder{}
|
||||
|
||||
// or should set the orFilter
|
||||
f.or(other)
|
||||
assert.Equal(other, f.subFilter)
|
||||
assert.Nil(f.getError())
|
||||
|
||||
// or should set error if or is set
|
||||
f.or(newBuilder)
|
||||
assert.Equal(newBuilder, f.subFilter)
|
||||
assert.Equal(errSubFilterAlreadySet, f.getError())
|
||||
|
||||
// or should set error if and is set
|
||||
// or should not set subFilter if and is set
|
||||
f = &filterBuilder{}
|
||||
f.and(other)
|
||||
f.or(newBuilder)
|
||||
assert.Equal(other, f.subFilter)
|
||||
assert.Equal(errSubFilterAlreadySet, f.getError())
|
||||
|
||||
// or should set error if not is set
|
||||
// or should not set subFilter if not is set
|
||||
f = &filterBuilder{}
|
||||
f.not(other)
|
||||
f.or(newBuilder)
|
||||
assert.Equal(other, f.subFilter)
|
||||
assert.Equal(errSubFilterAlreadySet, f.getError())
|
||||
}
|
||||
|
||||
func TestFilterBuilderNot(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
f := &filterBuilder{}
|
||||
other := &filterBuilder{}
|
||||
newBuilder := &filterBuilder{}
|
||||
|
||||
// not should set the subFilter
|
||||
f.not(other)
|
||||
// ensure and filter is set
|
||||
assert.Equal(other, f.subFilter)
|
||||
assert.Nil(f.getError())
|
||||
|
||||
// not should set error if not is set
|
||||
f.not(newBuilder)
|
||||
assert.Equal(newBuilder, f.subFilter)
|
||||
assert.Equal(errSubFilterAlreadySet, f.getError())
|
||||
|
||||
// not should set error if and is set
|
||||
// not should not set subFilter if and is set
|
||||
f = &filterBuilder{}
|
||||
f.and(other)
|
||||
f.not(newBuilder)
|
||||
assert.Equal(other, f.subFilter)
|
||||
assert.Equal(errSubFilterAlreadySet, f.getError())
|
||||
|
||||
// not should set error if or is set
|
||||
// not should not set subFilter if or is set
|
||||
f = &filterBuilder{}
|
||||
f.or(other)
|
||||
f.not(newBuilder)
|
||||
assert.Equal(other, f.subFilter)
|
||||
assert.Equal(errSubFilterAlreadySet, f.getError())
|
||||
}
|
||||
|
||||
func TestAddJoin(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
f := &filterBuilder{}
|
||||
|
||||
const (
|
||||
table1Name = "table1Name"
|
||||
table2Name = "table2Name"
|
||||
|
||||
as1Name = "as1"
|
||||
as2Name = "as2"
|
||||
|
||||
onClause = "onClause1"
|
||||
)
|
||||
|
||||
f.addJoin(table1Name, as1Name, onClause)
|
||||
|
||||
// ensure join is added
|
||||
assert.Len(f.joins, 1)
|
||||
assert.Equal(fmt.Sprintf("LEFT JOIN %s AS %s ON %s", table1Name, as1Name, onClause), f.joins[0].toSQL())
|
||||
|
||||
// ensure join with same as is not added
|
||||
f.addJoin(table2Name, as1Name, onClause)
|
||||
assert.Len(f.joins, 1)
|
||||
|
||||
// ensure same table with different alias can be added
|
||||
f.addJoin(table1Name, as2Name, onClause)
|
||||
assert.Len(f.joins, 2)
|
||||
assert.Equal(fmt.Sprintf("LEFT JOIN %s AS %s ON %s", table1Name, as2Name, onClause), f.joins[1].toSQL())
|
||||
|
||||
// ensure table without alias can be added if tableName != existing alias/tableName
|
||||
f.addJoin(table1Name, "", onClause)
|
||||
assert.Len(f.joins, 3)
|
||||
assert.Equal(fmt.Sprintf("LEFT JOIN %s ON %s", table1Name, onClause), f.joins[2].toSQL())
|
||||
|
||||
// ensure table with alias == table name of a join without alias is not added
|
||||
f.addJoin(table2Name, table1Name, onClause)
|
||||
assert.Len(f.joins, 3)
|
||||
|
||||
// ensure table without alias cannot be added if tableName == existing alias
|
||||
f.addJoin(as2Name, "", onClause)
|
||||
assert.Len(f.joins, 3)
|
||||
|
||||
// ensure AS is not used if same as table name
|
||||
f.addJoin(table2Name, table2Name, onClause)
|
||||
assert.Len(f.joins, 4)
|
||||
assert.Equal(fmt.Sprintf("LEFT JOIN %s ON %s", table2Name, onClause), f.joins[3].toSQL())
|
||||
}
|
||||
|
||||
func TestAddWhere(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
f := &filterBuilder{}
|
||||
|
||||
// ensure empty sql adds nothing
|
||||
f.addWhere("")
|
||||
assert.Len(f.whereClauses, 0)
|
||||
|
||||
const whereClause = "a = b"
|
||||
var args = []interface{}{"1", "2"}
|
||||
|
||||
// ensure addWhere sets where clause and args
|
||||
f.addWhere(whereClause, args...)
|
||||
assert.Len(f.whereClauses, 1)
|
||||
assert.Equal(whereClause, f.whereClauses[0].sql)
|
||||
assert.Equal(args, f.whereClauses[0].args)
|
||||
|
||||
// ensure addWhere without args sets where clause
|
||||
f.addWhere(whereClause)
|
||||
assert.Len(f.whereClauses, 2)
|
||||
assert.Equal(whereClause, f.whereClauses[1].sql)
|
||||
assert.Len(f.whereClauses[1].args, 0)
|
||||
}
|
||||
|
||||
func TestAddHaving(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
f := &filterBuilder{}
|
||||
|
||||
// ensure empty sql adds nothing
|
||||
f.addHaving("")
|
||||
assert.Len(f.havingClauses, 0)
|
||||
|
||||
const havingClause = "a = b"
|
||||
var args = []interface{}{"1", "2"}
|
||||
|
||||
// ensure addWhere sets where clause and args
|
||||
f.addHaving(havingClause, args...)
|
||||
assert.Len(f.havingClauses, 1)
|
||||
assert.Equal(havingClause, f.havingClauses[0].sql)
|
||||
assert.Equal(args, f.havingClauses[0].args)
|
||||
|
||||
// ensure addWhere without args sets where clause
|
||||
f.addHaving(havingClause)
|
||||
assert.Len(f.havingClauses, 2)
|
||||
assert.Equal(havingClause, f.havingClauses[1].sql)
|
||||
assert.Len(f.havingClauses[1].args, 0)
|
||||
}
|
||||
|
||||
func TestGenerateWhereClauses(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
f := &filterBuilder{}
|
||||
|
||||
const clause1 = "a = 1"
|
||||
const clause2 = "b = 2"
|
||||
const clause3 = "c = 3"
|
||||
|
||||
const arg1 = "1"
|
||||
const arg2 = "2"
|
||||
const arg3 = "3"
|
||||
|
||||
// ensure single where clause is generated correctly
|
||||
f.addWhere(clause1)
|
||||
r, rArgs := f.generateWhereClauses()
|
||||
assert.Equal("("+clause1+")", r)
|
||||
assert.Len(rArgs, 0)
|
||||
|
||||
// ensure multiple where clauses are surrounded with parenthesis and
|
||||
// ANDed together
|
||||
f.addWhere(clause2, arg1, arg2)
|
||||
r, rArgs = f.generateWhereClauses()
|
||||
assert.Equal("("+clause1+" AND "+clause2+")", r)
|
||||
assert.Len(rArgs, 2)
|
||||
|
||||
// ensure empty subfilter is not added to generated where clause
|
||||
sf := &filterBuilder{}
|
||||
f.and(sf)
|
||||
|
||||
r, rArgs = f.generateWhereClauses()
|
||||
assert.Equal("("+clause1+" AND "+clause2+")", r)
|
||||
assert.Len(rArgs, 2)
|
||||
|
||||
// ensure sub-filter is generated correctly
|
||||
sf.addWhere(clause3, arg3)
|
||||
r, rArgs = f.generateWhereClauses()
|
||||
assert.Equal("("+clause1+" AND "+clause2+") AND ("+clause3+")", r)
|
||||
assert.Len(rArgs, 3)
|
||||
|
||||
// ensure OR sub-filter is generated correctly
|
||||
f = &filterBuilder{}
|
||||
f.addWhere(clause1)
|
||||
f.addWhere(clause2, arg1, arg2)
|
||||
f.or(sf)
|
||||
|
||||
r, rArgs = f.generateWhereClauses()
|
||||
assert.Equal("("+clause1+" AND "+clause2+") OR ("+clause3+")", r)
|
||||
assert.Len(rArgs, 3)
|
||||
|
||||
// ensure NOT sub-filter is generated correctly
|
||||
f = &filterBuilder{}
|
||||
f.addWhere(clause1)
|
||||
f.addWhere(clause2, arg1, arg2)
|
||||
f.not(sf)
|
||||
|
||||
r, rArgs = f.generateWhereClauses()
|
||||
assert.Equal("("+clause1+" AND "+clause2+") AND NOT ("+clause3+")", r)
|
||||
assert.Len(rArgs, 3)
|
||||
|
||||
// ensure empty filter with ANDed sub-filter does not include AND
|
||||
f = &filterBuilder{}
|
||||
f.and(sf)
|
||||
|
||||
r, rArgs = f.generateWhereClauses()
|
||||
assert.Equal("("+clause3+")", r)
|
||||
assert.Len(rArgs, 1)
|
||||
|
||||
// ensure empty filter with ORed sub-filter does not include OR
|
||||
f = &filterBuilder{}
|
||||
f.or(sf)
|
||||
|
||||
r, rArgs = f.generateWhereClauses()
|
||||
assert.Equal("("+clause3+")", r)
|
||||
assert.Len(rArgs, 1)
|
||||
|
||||
// ensure empty filter with NOTed sub-filter does not include AND
|
||||
f = &filterBuilder{}
|
||||
f.not(sf)
|
||||
|
||||
r, rArgs = f.generateWhereClauses()
|
||||
assert.Equal("NOT ("+clause3+")", r)
|
||||
assert.Len(rArgs, 1)
|
||||
}
|
||||
|
||||
func TestGenerateHavingClauses(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
f := &filterBuilder{}
|
||||
|
||||
const clause1 = "a = 1"
|
||||
const clause2 = "b = 2"
|
||||
const clause3 = "c = 3"
|
||||
|
||||
const arg1 = "1"
|
||||
const arg2 = "2"
|
||||
const arg3 = "3"
|
||||
|
||||
// ensure single Having clause is generated correctly
|
||||
f.addHaving(clause1)
|
||||
r, rArgs := f.generateHavingClauses()
|
||||
assert.Equal("("+clause1+")", r)
|
||||
assert.Len(rArgs, 0)
|
||||
|
||||
// ensure multiple Having clauses are surrounded with parenthesis and
|
||||
// ANDed together
|
||||
f.addHaving(clause2, arg1, arg2)
|
||||
r, rArgs = f.generateHavingClauses()
|
||||
assert.Equal("("+clause1+" AND "+clause2+")", r)
|
||||
assert.Len(rArgs, 2)
|
||||
|
||||
// ensure empty subfilter is not added to generated Having clause
|
||||
sf := &filterBuilder{}
|
||||
f.and(sf)
|
||||
|
||||
r, rArgs = f.generateHavingClauses()
|
||||
assert.Equal("("+clause1+" AND "+clause2+")", r)
|
||||
assert.Len(rArgs, 2)
|
||||
|
||||
// ensure sub-filter is generated correctly
|
||||
sf.addHaving(clause3, arg3)
|
||||
r, rArgs = f.generateHavingClauses()
|
||||
assert.Equal("("+clause1+" AND "+clause2+") AND ("+clause3+")", r)
|
||||
assert.Len(rArgs, 3)
|
||||
|
||||
// ensure OR sub-filter is generated correctly
|
||||
f = &filterBuilder{}
|
||||
f.addHaving(clause1)
|
||||
f.addHaving(clause2, arg1, arg2)
|
||||
f.or(sf)
|
||||
|
||||
r, rArgs = f.generateHavingClauses()
|
||||
assert.Equal("("+clause1+" AND "+clause2+") OR ("+clause3+")", r)
|
||||
assert.Len(rArgs, 3)
|
||||
|
||||
// ensure NOT sub-filter is generated correctly
|
||||
f = &filterBuilder{}
|
||||
f.addHaving(clause1)
|
||||
f.addHaving(clause2, arg1, arg2)
|
||||
f.not(sf)
|
||||
|
||||
r, rArgs = f.generateHavingClauses()
|
||||
assert.Equal("("+clause1+" AND "+clause2+") AND NOT ("+clause3+")", r)
|
||||
assert.Len(rArgs, 3)
|
||||
}
|
||||
|
||||
func TestGetAllJoins(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
f := &filterBuilder{}
|
||||
|
||||
const (
|
||||
table1Name = "table1Name"
|
||||
table2Name = "table2Name"
|
||||
|
||||
as1Name = "as1"
|
||||
as2Name = "as2"
|
||||
|
||||
onClause = "onClause1"
|
||||
)
|
||||
|
||||
f.addJoin(table1Name, as1Name, onClause)
|
||||
|
||||
// ensure join is returned
|
||||
joins := f.getAllJoins()
|
||||
assert.Len(joins, 1)
|
||||
assert.Equal(fmt.Sprintf("LEFT JOIN %s AS %s ON %s", table1Name, as1Name, onClause), joins[0].toSQL())
|
||||
|
||||
// ensure joins in sub-filter are returned
|
||||
subFilter := &filterBuilder{}
|
||||
f.and(subFilter)
|
||||
subFilter.addJoin(table2Name, as2Name, onClause)
|
||||
|
||||
joins = f.getAllJoins()
|
||||
assert.Len(joins, 2)
|
||||
assert.Equal(fmt.Sprintf("LEFT JOIN %s AS %s ON %s", table2Name, as2Name, onClause), joins[1].toSQL())
|
||||
|
||||
// ensure redundant joins are not returned
|
||||
subFilter.addJoin(as1Name, "", onClause)
|
||||
joins = f.getAllJoins()
|
||||
assert.Len(joins, 2)
|
||||
}
|
||||
|
||||
func TestGetError(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
f := &filterBuilder{}
|
||||
subFilter := &filterBuilder{}
|
||||
|
||||
f.and(subFilter)
|
||||
|
||||
expectedErr := errors.New("test error")
|
||||
expectedErr2 := errors.New("test error2")
|
||||
f.err = expectedErr
|
||||
subFilter.err = expectedErr2
|
||||
|
||||
// ensure getError returns the top-level error state
|
||||
assert.Equal(expectedErr, f.getError())
|
||||
|
||||
// ensure getError returns sub-filter error state if top-level error
|
||||
// is nil
|
||||
f.err = nil
|
||||
assert.Equal(expectedErr2, f.getError())
|
||||
|
||||
// ensure getError returns nil if all error states are nil
|
||||
subFilter.err = nil
|
||||
assert.Nil(f.getError())
|
||||
}
|
||||
|
||||
func TestStringCriterionHandlerIncludes(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
const column = "column"
|
||||
const value1 = "two words"
|
||||
const quotedValue = `"two words"`
|
||||
|
||||
f := &filterBuilder{}
|
||||
f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{
|
||||
Modifier: models.CriterionModifierIncludes,
|
||||
Value: value1,
|
||||
}, column))
|
||||
|
||||
assert.Len(f.whereClauses, 1)
|
||||
assert.Equal(fmt.Sprintf("(%[1]s LIKE ? OR %[1]s LIKE ?)", column), f.whereClauses[0].sql)
|
||||
assert.Len(f.whereClauses[0].args, 2)
|
||||
assert.Equal("%two%", f.whereClauses[0].args[0])
|
||||
assert.Equal("%words%", f.whereClauses[0].args[1])
|
||||
|
||||
f = &filterBuilder{}
|
||||
f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{
|
||||
Modifier: models.CriterionModifierIncludes,
|
||||
Value: quotedValue,
|
||||
}, column))
|
||||
|
||||
assert.Len(f.whereClauses, 1)
|
||||
assert.Equal(fmt.Sprintf("(%[1]s LIKE ?)", column), f.whereClauses[0].sql)
|
||||
assert.Len(f.whereClauses[0].args, 1)
|
||||
assert.Equal("%two words%", f.whereClauses[0].args[0])
|
||||
}
|
||||
|
||||
func TestStringCriterionHandlerExcludes(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
const column = "column"
|
||||
const value1 = "two words"
|
||||
const quotedValue = `"two words"`
|
||||
|
||||
f := &filterBuilder{}
|
||||
f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{
|
||||
Modifier: models.CriterionModifierExcludes,
|
||||
Value: value1,
|
||||
}, column))
|
||||
|
||||
assert.Len(f.whereClauses, 1)
|
||||
assert.Equal(fmt.Sprintf("(%[1]s NOT LIKE ? AND %[1]s NOT LIKE ?)", column), f.whereClauses[0].sql)
|
||||
assert.Len(f.whereClauses[0].args, 2)
|
||||
assert.Equal("%two%", f.whereClauses[0].args[0])
|
||||
assert.Equal("%words%", f.whereClauses[0].args[1])
|
||||
|
||||
f = &filterBuilder{}
|
||||
f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{
|
||||
Modifier: models.CriterionModifierExcludes,
|
||||
Value: quotedValue,
|
||||
}, column))
|
||||
|
||||
assert.Len(f.whereClauses, 1)
|
||||
assert.Equal(fmt.Sprintf("(%[1]s NOT LIKE ?)", column), f.whereClauses[0].sql)
|
||||
assert.Len(f.whereClauses[0].args, 1)
|
||||
assert.Equal("%two words%", f.whereClauses[0].args[0])
|
||||
}
|
||||
|
||||
func TestStringCriterionHandlerEquals(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
const column = "column"
|
||||
const value1 = "two words"
|
||||
|
||||
f := &filterBuilder{}
|
||||
f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
Value: value1,
|
||||
}, column))
|
||||
|
||||
assert.Len(f.whereClauses, 1)
|
||||
assert.Equal(fmt.Sprintf("%[1]s LIKE ?", column), f.whereClauses[0].sql)
|
||||
assert.Len(f.whereClauses[0].args, 1)
|
||||
assert.Equal(value1, f.whereClauses[0].args[0])
|
||||
}
|
||||
|
||||
func TestStringCriterionHandlerNotEquals(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
const column = "column"
|
||||
const value1 = "two words"
|
||||
|
||||
f := &filterBuilder{}
|
||||
f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{
|
||||
Modifier: models.CriterionModifierNotEquals,
|
||||
Value: value1,
|
||||
}, column))
|
||||
|
||||
assert.Len(f.whereClauses, 1)
|
||||
assert.Equal(fmt.Sprintf("%[1]s NOT LIKE ?", column), f.whereClauses[0].sql)
|
||||
assert.Len(f.whereClauses[0].args, 1)
|
||||
assert.Equal(value1, f.whereClauses[0].args[0])
|
||||
}
|
||||
|
||||
func TestStringCriterionHandlerMatchesRegex(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
const column = "column"
|
||||
const validValue = "two words"
|
||||
const invalidValue = "*two words"
|
||||
|
||||
f := &filterBuilder{}
|
||||
f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{
|
||||
Modifier: models.CriterionModifierMatchesRegex,
|
||||
Value: validValue,
|
||||
}, column))
|
||||
|
||||
assert.Len(f.whereClauses, 1)
|
||||
assert.Equal(fmt.Sprintf("%[1]s regexp ?", column), f.whereClauses[0].sql)
|
||||
assert.Len(f.whereClauses[0].args, 1)
|
||||
assert.Equal(validValue, f.whereClauses[0].args[0])
|
||||
|
||||
// ensure invalid regex sets error state
|
||||
f = &filterBuilder{}
|
||||
f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{
|
||||
Modifier: models.CriterionModifierMatchesRegex,
|
||||
Value: invalidValue,
|
||||
}, column))
|
||||
|
||||
assert.NotNil(f.getError())
|
||||
}
|
||||
|
||||
func TestStringCriterionHandlerNotMatchesRegex(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
const column = "column"
|
||||
const validValue = "two words"
|
||||
const invalidValue = "*two words"
|
||||
|
||||
f := &filterBuilder{}
|
||||
f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{
|
||||
Modifier: models.CriterionModifierNotMatchesRegex,
|
||||
Value: validValue,
|
||||
}, column))
|
||||
|
||||
assert.Len(f.whereClauses, 1)
|
||||
assert.Equal(fmt.Sprintf("%[1]s NOT regexp ?", column), f.whereClauses[0].sql)
|
||||
assert.Len(f.whereClauses[0].args, 1)
|
||||
assert.Equal(validValue, f.whereClauses[0].args[0])
|
||||
|
||||
// ensure invalid regex sets error state
|
||||
f = &filterBuilder{}
|
||||
f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{
|
||||
Modifier: models.CriterionModifierNotMatchesRegex,
|
||||
Value: invalidValue,
|
||||
}, column))
|
||||
|
||||
assert.NotNil(f.getError())
|
||||
}
|
||||
|
||||
func TestStringCriterionHandlerIsNull(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
const column = "column"
|
||||
|
||||
f := &filterBuilder{}
|
||||
f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{
|
||||
Modifier: models.CriterionModifierIsNull,
|
||||
}, column))
|
||||
|
||||
assert.Len(f.whereClauses, 1)
|
||||
assert.Equal(fmt.Sprintf("%[1]s IS NULL", column), f.whereClauses[0].sql)
|
||||
assert.Len(f.whereClauses[0].args, 0)
|
||||
}
|
||||
|
||||
func TestStringCriterionHandlerNotNull(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
const column = "column"
|
||||
|
||||
f := &filterBuilder{}
|
||||
f.handleCriterionFunc(stringCriterionHandler(&models.StringCriterionInput{
|
||||
Modifier: models.CriterionModifierNotNull,
|
||||
}, column))
|
||||
|
||||
assert.Len(f.whereClauses, 1)
|
||||
assert.Equal(fmt.Sprintf("%[1]s IS NOT NULL", column), f.whereClauses[0].sql)
|
||||
assert.Len(f.whereClauses[0].args, 0)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ type queryBuilder struct {
|
||||
|
||||
body string
|
||||
|
||||
joins joins
|
||||
whereClauses []string
|
||||
havingClauses []string
|
||||
args []interface{}
|
||||
@@ -25,7 +26,10 @@ func (qb queryBuilder) executeFind() ([]int, int, error) {
|
||||
return nil, 0, qb.err
|
||||
}
|
||||
|
||||
return qb.repository.executeFindQuery(qb.body, qb.args, qb.sortAndPagination, qb.whereClauses, qb.havingClauses)
|
||||
body := qb.body
|
||||
body += qb.joins.toSQL()
|
||||
|
||||
return qb.repository.executeFindQuery(body, qb.args, qb.sortAndPagination, qb.whereClauses, qb.havingClauses)
|
||||
}
|
||||
|
||||
func (qb *queryBuilder) addWhere(clauses ...string) {
|
||||
@@ -48,6 +52,48 @@ func (qb *queryBuilder) addArg(args ...interface{}) {
|
||||
qb.args = append(qb.args, args...)
|
||||
}
|
||||
|
||||
func (qb *queryBuilder) join(table, as, onClause string) {
|
||||
newJoin := join{
|
||||
table: table,
|
||||
as: as,
|
||||
onClause: onClause,
|
||||
}
|
||||
|
||||
qb.joins.add(newJoin)
|
||||
}
|
||||
|
||||
func (qb *queryBuilder) addJoins(joins ...join) {
|
||||
qb.joins.add(joins...)
|
||||
}
|
||||
|
||||
func (qb *queryBuilder) addFilter(f *filterBuilder) {
|
||||
err := f.getError()
|
||||
if err != nil {
|
||||
qb.err = err
|
||||
return
|
||||
}
|
||||
|
||||
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...)
|
||||
}
|
||||
|
||||
qb.addJoins(f.getAllJoins()...)
|
||||
}
|
||||
|
||||
func (qb *queryBuilder) handleIntCriterionInput(c *models.IntCriterionInput, column string) {
|
||||
if c != nil {
|
||||
clause, count := getIntCriterionWhereClause(column, *c)
|
||||
|
||||
@@ -273,6 +273,18 @@ func (r *repository) newQuery() queryBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *repository) join(j joiner, as string, parentIDCol string) {
|
||||
t := r.tableName
|
||||
if as != "" {
|
||||
t = as
|
||||
}
|
||||
j.addJoin(r.tableName, as, fmt.Sprintf("%s.%s = %s", t, r.idColumn, parentIDCol))
|
||||
}
|
||||
|
||||
type joiner interface {
|
||||
addJoin(table, as, onClause string)
|
||||
}
|
||||
|
||||
type joinRepository struct {
|
||||
repository
|
||||
fkColumn string
|
||||
|
||||
@@ -3,8 +3,7 @@ package sqlite
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"strconv"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -290,51 +289,70 @@ func (qb *sceneQueryBuilder) All() ([]*models.Scene, error) {
|
||||
return qb.queryScenes(selectAll(sceneTable)+qb.getSceneSort(nil), nil)
|
||||
}
|
||||
|
||||
// QueryForAutoTag queries for scenes whose paths match the provided regex and
|
||||
// are optionally within the provided path. Excludes organized scenes.
|
||||
// TODO - this should be replaced with Query once it can perform multiple
|
||||
// filters on the same field.
|
||||
func (qb *sceneQueryBuilder) QueryForAutoTag(regex string, pathPrefixes []string) ([]*models.Scene, error) {
|
||||
var args []interface{}
|
||||
body := selectDistinctIDs("scenes") + ` WHERE
|
||||
scenes.path regexp ? AND
|
||||
scenes.organized = 0`
|
||||
func illegalFilterCombination(type1, type2 string) error {
|
||||
return fmt.Errorf("cannot have %s and %s in the same filter", type1, type2)
|
||||
}
|
||||
|
||||
args = append(args, "(?i)"+regex)
|
||||
func (qb *sceneQueryBuilder) validateFilter(sceneFilter *models.SceneFilterType) error {
|
||||
const and = "AND"
|
||||
const or = "OR"
|
||||
const not = "NOT"
|
||||
|
||||
var pathClauses []string
|
||||
for _, p := range pathPrefixes {
|
||||
pathClauses = append(pathClauses, "scenes.path like ?")
|
||||
|
||||
sep := string(filepath.Separator)
|
||||
if !strings.HasSuffix(p, sep) {
|
||||
p = p + sep
|
||||
if sceneFilter.And != nil {
|
||||
if sceneFilter.Or != nil {
|
||||
return illegalFilterCombination(and, or)
|
||||
}
|
||||
args = append(args, p+"%")
|
||||
if sceneFilter.Not != nil {
|
||||
return illegalFilterCombination(and, not)
|
||||
}
|
||||
|
||||
if len(pathClauses) > 0 {
|
||||
body += " AND (" + strings.Join(pathClauses, " OR ") + ")"
|
||||
return qb.validateFilter(sceneFilter.And)
|
||||
}
|
||||
|
||||
idsResult, err := qb.runIdsQuery(body, args)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if sceneFilter.Or != nil {
|
||||
if sceneFilter.Not != nil {
|
||||
return illegalFilterCombination(or, not)
|
||||
}
|
||||
|
||||
var scenes []*models.Scene
|
||||
for _, id := range idsResult {
|
||||
scene, err := qb.Find(id)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return qb.validateFilter(sceneFilter.Or)
|
||||
}
|
||||
|
||||
scenes = append(scenes, scene)
|
||||
if sceneFilter.Not != nil {
|
||||
return qb.validateFilter(sceneFilter.Not)
|
||||
}
|
||||
|
||||
return scenes, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *sceneQueryBuilder) makeFilter(sceneFilter *models.SceneFilterType) *filterBuilder {
|
||||
query := &filterBuilder{}
|
||||
|
||||
if sceneFilter.And != nil {
|
||||
query.and(qb.makeFilter(sceneFilter.And))
|
||||
}
|
||||
if sceneFilter.Or != nil {
|
||||
query.or(qb.makeFilter(sceneFilter.Or))
|
||||
}
|
||||
if sceneFilter.Not != nil {
|
||||
query.not(qb.makeFilter(sceneFilter.Not))
|
||||
}
|
||||
|
||||
query.handleCriterionFunc(stringCriterionHandler(sceneFilter.Path, "scenes.path"))
|
||||
query.handleCriterionFunc(intCriterionHandler(sceneFilter.Rating, "scenes.rating"))
|
||||
query.handleCriterionFunc(intCriterionHandler(sceneFilter.OCounter, "scenes.o_counter"))
|
||||
query.handleCriterionFunc(boolCriterionHandler(sceneFilter.Organized, "scenes.organized"))
|
||||
query.handleCriterionFunc(durationCriterionHandler(sceneFilter.Duration, "scenes.duration"))
|
||||
query.handleCriterionFunc(resolutionCriterionHandler(sceneFilter.Resolution, "scenes.height", "scenes.width"))
|
||||
query.handleCriterionFunc(hasMarkersCriterionHandler(sceneFilter.HasMarkers))
|
||||
query.handleCriterionFunc(sceneIsMissingCriterionHandler(qb, sceneFilter.IsMissing))
|
||||
|
||||
query.handleCriterionFunc(sceneTagsCriterionHandler(qb, sceneFilter.Tags))
|
||||
query.handleCriterionFunc(scenePerformersCriterionHandler(qb, sceneFilter.Performers))
|
||||
query.handleCriterionFunc(sceneStudioCriterionHandler(qb, sceneFilter.Studios))
|
||||
query.handleCriterionFunc(sceneMoviesCriterionHandler(qb, sceneFilter.Movies))
|
||||
query.handleCriterionFunc(sceneStashIDsHandler(qb, sceneFilter.StashID))
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilter *models.FindFilterType) ([]*models.Scene, int, error) {
|
||||
@@ -348,152 +366,21 @@ func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilt
|
||||
query := qb.newQuery()
|
||||
|
||||
query.body = selectDistinctIDs(sceneTable)
|
||||
query.body += `
|
||||
left join scene_markers on scene_markers.scene_id = scenes.id
|
||||
left join performers_scenes as performers_join on performers_join.scene_id = scenes.id
|
||||
left join movies_scenes as movies_join on movies_join.scene_id = scenes.id
|
||||
left join studios as studio on studio.id = scenes.studio_id
|
||||
left join scenes_galleries as galleries_join on galleries_join.scene_id = scenes.id
|
||||
left join scenes_tags as tags_join on tags_join.scene_id = scenes.id
|
||||
left join scene_stash_ids on scene_stash_ids.scene_id = scenes.id
|
||||
`
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
query.join("scene_markers", "", "scene_markers.scene_id = scenes.id")
|
||||
searchColumns := []string{"scenes.title", "scenes.details", "scenes.path", "scenes.oshash", "scenes.checksum", "scene_markers.title"}
|
||||
clause, thisArgs := getSearchBinding(searchColumns, *q, false)
|
||||
query.addWhere(clause)
|
||||
query.addArg(thisArgs...)
|
||||
}
|
||||
|
||||
query.handleStringCriterionInput(sceneFilter.Path, "scenes.path")
|
||||
query.handleIntCriterionInput(sceneFilter.Rating, "scenes.rating")
|
||||
query.handleIntCriterionInput(sceneFilter.OCounter, "scenes.o_counter")
|
||||
if err := qb.validateFilter(sceneFilter); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
filter := qb.makeFilter(sceneFilter)
|
||||
|
||||
if Organized := sceneFilter.Organized; Organized != nil {
|
||||
var organized string
|
||||
if *Organized == true {
|
||||
organized = "1"
|
||||
} else {
|
||||
organized = "0"
|
||||
}
|
||||
query.addWhere("scenes.organized = " + organized)
|
||||
}
|
||||
|
||||
if durationFilter := sceneFilter.Duration; durationFilter != nil {
|
||||
clause, thisArgs := getDurationWhereClause(*durationFilter)
|
||||
query.addWhere(clause)
|
||||
query.addArg(thisArgs...)
|
||||
}
|
||||
|
||||
if resolutionFilter := sceneFilter.Resolution; resolutionFilter != nil {
|
||||
if resolution := resolutionFilter.String(); resolutionFilter.IsValid() {
|
||||
switch resolution {
|
||||
case "VERY_LOW":
|
||||
query.addWhere("MIN(scenes.height, scenes.width) < 240")
|
||||
case "LOW":
|
||||
query.addWhere("(MIN(scenes.height, scenes.width) >= 240 AND MIN(scenes.height, scenes.width) < 360)")
|
||||
case "R360P":
|
||||
query.addWhere("(MIN(scenes.height, scenes.width) >= 360 AND MIN(scenes.height, scenes.width) < 480)")
|
||||
case "STANDARD":
|
||||
query.addWhere("(MIN(scenes.height, scenes.width) >= 480 AND MIN(scenes.height, scenes.width) < 540)")
|
||||
case "WEB_HD":
|
||||
query.addWhere("(MIN(scenes.height, scenes.width) >= 540 AND MIN(scenes.height, scenes.width) < 720)")
|
||||
case "STANDARD_HD":
|
||||
query.addWhere("(MIN(scenes.height, scenes.width) >= 720 AND MIN(scenes.height, scenes.width) < 1080)")
|
||||
case "FULL_HD":
|
||||
query.addWhere("(MIN(scenes.height, scenes.width) >= 1080 AND MIN(scenes.height, scenes.width) < 1440)")
|
||||
case "QUAD_HD":
|
||||
query.addWhere("(MIN(scenes.height, scenes.width) >= 1440 AND MIN(scenes.height, scenes.width) < 1920)")
|
||||
case "VR_HD":
|
||||
query.addWhere("(MIN(scenes.height, scenes.width) >= 1920 AND MIN(scenes.height, scenes.width) < 2160)")
|
||||
case "FOUR_K":
|
||||
query.addWhere("(MIN(scenes.height, scenes.width) >= 2160 AND MIN(scenes.height, scenes.width) < 2880)")
|
||||
case "FIVE_K":
|
||||
query.addWhere("(MIN(scenes.height, scenes.width) >= 2880 AND MIN(scenes.height, scenes.width) < 3384)")
|
||||
case "SIX_K":
|
||||
query.addWhere("(MIN(scenes.height, scenes.width) >= 3384 AND MIN(scenes.height, scenes.width) < 4320)")
|
||||
case "EIGHT_K":
|
||||
query.addWhere("MIN(scenes.height, scenes.width) >= 4320")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasMarkersFilter := sceneFilter.HasMarkers; hasMarkersFilter != nil {
|
||||
if strings.Compare(*hasMarkersFilter, "true") == 0 {
|
||||
query.addHaving("count(scene_markers.scene_id) > 0")
|
||||
} else {
|
||||
query.addWhere("scene_markers.id IS NULL")
|
||||
}
|
||||
}
|
||||
|
||||
if isMissingFilter := sceneFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
|
||||
switch *isMissingFilter {
|
||||
case "galleries":
|
||||
query.addWhere("galleries_join.scene_id IS NULL")
|
||||
case "studio":
|
||||
query.addWhere("scenes.studio_id IS NULL")
|
||||
case "movie":
|
||||
query.addWhere("movies_join.scene_id IS NULL")
|
||||
case "performers":
|
||||
query.addWhere("performers_join.scene_id IS NULL")
|
||||
case "date":
|
||||
query.addWhere("scenes.date IS \"\" OR scenes.date IS \"0001-01-01\"")
|
||||
case "tags":
|
||||
query.addWhere("tags_join.scene_id IS NULL")
|
||||
case "stash_id":
|
||||
query.addWhere("scene_stash_ids.scene_id IS NULL")
|
||||
default:
|
||||
query.addWhere("(scenes." + *isMissingFilter + " IS NULL OR TRIM(scenes." + *isMissingFilter + ") = '')")
|
||||
}
|
||||
}
|
||||
|
||||
if tagsFilter := sceneFilter.Tags; tagsFilter != nil && len(tagsFilter.Value) > 0 {
|
||||
for _, tagID := range tagsFilter.Value {
|
||||
query.addArg(tagID)
|
||||
}
|
||||
|
||||
query.body += " LEFT JOIN tags on tags_join.tag_id = tags.id"
|
||||
whereClause, havingClause := getMultiCriterionClause("scenes", "tags", "scenes_tags", "scene_id", "tag_id", tagsFilter)
|
||||
query.addWhere(whereClause)
|
||||
query.addHaving(havingClause)
|
||||
}
|
||||
|
||||
if performersFilter := sceneFilter.Performers; performersFilter != nil && len(performersFilter.Value) > 0 {
|
||||
for _, performerID := range performersFilter.Value {
|
||||
query.addArg(performerID)
|
||||
}
|
||||
|
||||
query.body += " LEFT JOIN performers ON performers_join.performer_id = performers.id"
|
||||
whereClause, havingClause := getMultiCriterionClause("scenes", "performers", "performers_scenes", "scene_id", "performer_id", performersFilter)
|
||||
query.addWhere(whereClause)
|
||||
query.addHaving(havingClause)
|
||||
}
|
||||
|
||||
if studiosFilter := sceneFilter.Studios; studiosFilter != nil && len(studiosFilter.Value) > 0 {
|
||||
for _, studioID := range studiosFilter.Value {
|
||||
query.addArg(studioID)
|
||||
}
|
||||
|
||||
whereClause, havingClause := getMultiCriterionClause("scenes", "studio", "", "", "studio_id", studiosFilter)
|
||||
query.addWhere(whereClause)
|
||||
query.addHaving(havingClause)
|
||||
}
|
||||
|
||||
if moviesFilter := sceneFilter.Movies; moviesFilter != nil && len(moviesFilter.Value) > 0 {
|
||||
for _, movieID := range moviesFilter.Value {
|
||||
query.addArg(movieID)
|
||||
}
|
||||
|
||||
query.body += " LEFT JOIN movies ON movies_join.movie_id = movies.id"
|
||||
whereClause, havingClause := getMultiCriterionClause("scenes", "movies", "movies_scenes", "scene_id", "movie_id", moviesFilter)
|
||||
query.addWhere(whereClause)
|
||||
query.addHaving(havingClause)
|
||||
}
|
||||
|
||||
if stashIDFilter := sceneFilter.StashID; stashIDFilter != nil {
|
||||
query.addWhere("scene_stash_ids.stash_id = ?")
|
||||
query.addArg(stashIDFilter)
|
||||
}
|
||||
query.addFilter(filter)
|
||||
|
||||
query.sortAndPagination = qb.getSceneSort(findFilter) + getPagination(findFilter)
|
||||
|
||||
@@ -522,7 +409,16 @@ func appendClause(clauses []string, clause string) []string {
|
||||
return clauses
|
||||
}
|
||||
|
||||
func getDurationWhereClause(durationFilter models.IntCriterionInput) (string, []interface{}) {
|
||||
func durationCriterionHandler(durationFilter *models.IntCriterionInput, column string) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if durationFilter != nil {
|
||||
clause, thisArgs := getDurationWhereClause(*durationFilter, column)
|
||||
f.addWhere(clause, thisArgs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getDurationWhereClause(durationFilter models.IntCriterionInput, column string) (string, []interface{}) {
|
||||
// special case for duration. We accept duration as seconds as int but the
|
||||
// field is floating point. Change the equals filter to return a range
|
||||
// between x and x + 1
|
||||
@@ -532,16 +428,16 @@ func getDurationWhereClause(durationFilter models.IntCriterionInput) (string, []
|
||||
|
||||
value := durationFilter.Value
|
||||
if durationFilter.Modifier == models.CriterionModifierEquals {
|
||||
clause = "scenes.duration >= ? AND scenes.duration < ?"
|
||||
clause = fmt.Sprintf("%[1]s >= ? AND %[1]s < ?", column)
|
||||
args = append(args, value)
|
||||
args = append(args, value+1)
|
||||
} else if durationFilter.Modifier == models.CriterionModifierNotEquals {
|
||||
clause = "(scenes.duration < ? OR scenes.duration >= ?)"
|
||||
clause = fmt.Sprintf("(%[1]s < ? OR %[1]s >= ?)", column)
|
||||
args = append(args, value)
|
||||
args = append(args, value+1)
|
||||
} else {
|
||||
var count int
|
||||
clause, count = getIntCriterionWhereClause("scenes.duration", durationFilter)
|
||||
clause, count = getIntCriterionWhereClause(column, durationFilter)
|
||||
if count == 1 {
|
||||
args = append(args, value)
|
||||
}
|
||||
@@ -550,6 +446,125 @@ func getDurationWhereClause(durationFilter models.IntCriterionInput) (string, []
|
||||
return clause, args
|
||||
}
|
||||
|
||||
func resolutionCriterionHandler(resolution *models.ResolutionEnum, heightColumn string, widthColumn string) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if resolution != nil && resolution.IsValid() {
|
||||
min := resolution.GetMinResolution()
|
||||
max := resolution.GetMaxResolution()
|
||||
|
||||
widthHeight := fmt.Sprintf("MIN(%s, %s)", widthColumn, heightColumn)
|
||||
|
||||
if min > 0 {
|
||||
f.addWhere(widthHeight + " >= " + strconv.Itoa(min))
|
||||
}
|
||||
|
||||
if max > 0 {
|
||||
f.addWhere(widthHeight + " < " + strconv.Itoa(max))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hasMarkersCriterionHandler(hasMarkers *string) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if hasMarkers != nil {
|
||||
f.addJoin("scene_markers", "", "scene_markers.scene_id = scenes.id")
|
||||
if *hasMarkers == "true" {
|
||||
f.addHaving("count(scene_markers.scene_id) > 0")
|
||||
} else {
|
||||
f.addWhere("scene_markers.id IS NULL")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sceneIsMissingCriterionHandler(qb *sceneQueryBuilder, isMissing *string) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if isMissing != nil && *isMissing != "" {
|
||||
switch *isMissing {
|
||||
case "galleries":
|
||||
qb.galleriesRepository().join(f, "galleries_join", "scenes.id")
|
||||
f.addWhere("galleries_join.scene_id IS NULL")
|
||||
case "studio":
|
||||
f.addWhere("scenes.studio_id IS NULL")
|
||||
case "movie":
|
||||
qb.moviesRepository().join(f, "movies_join", "scenes.id")
|
||||
f.addWhere("movies_join.scene_id IS NULL")
|
||||
case "performers":
|
||||
qb.performersRepository().join(f, "performers_join", "scenes.id")
|
||||
f.addWhere("performers_join.scene_id IS NULL")
|
||||
case "date":
|
||||
f.addWhere("scenes.date IS \"\" OR scenes.date IS \"0001-01-01\"")
|
||||
case "tags":
|
||||
qb.tagsRepository().join(f, "tags_join", "scenes.id")
|
||||
f.addWhere("tags_join.scene_id IS NULL")
|
||||
case "stash_id":
|
||||
qb.stashIDRepository().join(f, "scene_stash_ids", "scenes.id")
|
||||
f.addWhere("scene_stash_ids.scene_id IS NULL")
|
||||
default:
|
||||
f.addWhere("(scenes." + *isMissing + " IS NULL OR TRIM(scenes." + *isMissing + ") = '')")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *sceneQueryBuilder) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
|
||||
return multiCriterionHandlerBuilder{
|
||||
primaryTable: sceneTable,
|
||||
foreignTable: foreignTable,
|
||||
joinTable: joinTable,
|
||||
primaryFK: sceneIDColumn,
|
||||
foreignFK: foreignFK,
|
||||
addJoinsFunc: addJoinsFunc,
|
||||
}
|
||||
}
|
||||
func sceneTagsCriterionHandler(qb *sceneQueryBuilder, tags *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
addJoinsFunc := func(f *filterBuilder) {
|
||||
qb.tagsRepository().join(f, "tags_join", "scenes.id")
|
||||
f.addJoin("tags", "", "tags_join.tag_id = tags.id")
|
||||
}
|
||||
h := qb.getMultiCriterionHandlerBuilder(tagTable, scenesTagsTable, tagIDColumn, addJoinsFunc)
|
||||
|
||||
return h.handler(tags)
|
||||
}
|
||||
|
||||
func scenePerformersCriterionHandler(qb *sceneQueryBuilder, performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
addJoinsFunc := func(f *filterBuilder) {
|
||||
qb.performersRepository().join(f, "performers_join", "scenes.id")
|
||||
f.addJoin("performers", "", "performers_join.performer_id = performers.id")
|
||||
}
|
||||
h := qb.getMultiCriterionHandlerBuilder(performerTable, performersScenesTable, performerIDColumn, addJoinsFunc)
|
||||
|
||||
return h.handler(performers)
|
||||
}
|
||||
|
||||
func sceneStudioCriterionHandler(qb *sceneQueryBuilder, studios *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
addJoinsFunc := func(f *filterBuilder) {
|
||||
f.addJoin("studios", "studio", "studio.id = scenes.studio_id")
|
||||
}
|
||||
h := qb.getMultiCriterionHandlerBuilder("studio", "", studioIDColumn, addJoinsFunc)
|
||||
|
||||
return h.handler(studios)
|
||||
}
|
||||
|
||||
func sceneMoviesCriterionHandler(qb *sceneQueryBuilder, movies *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
addJoinsFunc := func(f *filterBuilder) {
|
||||
qb.moviesRepository().join(f, "movies_join", "scenes.id")
|
||||
f.addJoin("movies", "", "movies_join.movie_id = movies.id")
|
||||
}
|
||||
h := qb.getMultiCriterionHandlerBuilder(movieTable, moviesScenesTable, "movie_id", addJoinsFunc)
|
||||
return h.handler(movies)
|
||||
}
|
||||
|
||||
func sceneStashIDsHandler(qb *sceneQueryBuilder, stashID *string) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if stashID != nil && *stashID != "" {
|
||||
qb.stashIDRepository().join(f, "scene_stash_ids", "scenes.id")
|
||||
stringLiteralCriterionHandler(stashID, "scene_stash_ids.stash_id")(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *sceneQueryBuilder) getSceneSort(findFilter *models.FindFilterType) string {
|
||||
if findFilter == nil {
|
||||
return " ORDER BY scenes.path, scenes.date ASC "
|
||||
|
||||
@@ -139,6 +139,7 @@ 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)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying scene: %s", err.Error())
|
||||
@@ -186,6 +187,143 @@ func TestSceneQueryPath(t *testing.T) {
|
||||
verifyScenesPath(t, pathCriterion)
|
||||
}
|
||||
|
||||
func TestSceneQueryPathOr(t *testing.T) {
|
||||
const scene1Idx = 1
|
||||
const scene2Idx = 2
|
||||
|
||||
scene1Path := getSceneStringValue(scene1Idx, "Path")
|
||||
scene2Path := getSceneStringValue(scene2Idx, "Path")
|
||||
|
||||
sceneFilter := models.SceneFilterType{
|
||||
Path: &models.StringCriterionInput{
|
||||
Value: scene1Path,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
Or: &models.SceneFilterType{
|
||||
Path: &models.StringCriterionInput{
|
||||
Value: scene2Path,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
withTxn(func(r models.Repository) error {
|
||||
sqb := r.Scene()
|
||||
|
||||
scenes := queryScene(t, sqb, &sceneFilter, nil)
|
||||
|
||||
assert.Len(t, scenes, 2)
|
||||
assert.Equal(t, scene1Path, scenes[0].Path)
|
||||
assert.Equal(t, scene2Path, scenes[1].Path)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestSceneQueryPathAndRating(t *testing.T) {
|
||||
const sceneIdx = 1
|
||||
scenePath := getSceneStringValue(sceneIdx, "Path")
|
||||
sceneRating := getRating(sceneIdx)
|
||||
|
||||
sceneFilter := models.SceneFilterType{
|
||||
Path: &models.StringCriterionInput{
|
||||
Value: scenePath,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
And: &models.SceneFilterType{
|
||||
Rating: &models.IntCriterionInput{
|
||||
Value: int(sceneRating.Int64),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
withTxn(func(r models.Repository) error {
|
||||
sqb := r.Scene()
|
||||
|
||||
scenes := queryScene(t, sqb, &sceneFilter, nil)
|
||||
|
||||
assert.Len(t, scenes, 1)
|
||||
assert.Equal(t, scenePath, scenes[0].Path)
|
||||
assert.Equal(t, sceneRating.Int64, scenes[0].Rating.Int64)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestSceneQueryPathNotRating(t *testing.T) {
|
||||
const sceneIdx = 1
|
||||
|
||||
sceneRating := getRating(sceneIdx)
|
||||
|
||||
pathCriterion := models.StringCriterionInput{
|
||||
Value: "scene_.*1_Path",
|
||||
Modifier: models.CriterionModifierMatchesRegex,
|
||||
}
|
||||
|
||||
ratingCriterion := models.IntCriterionInput{
|
||||
Value: int(sceneRating.Int64),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
}
|
||||
|
||||
sceneFilter := models.SceneFilterType{
|
||||
Path: &pathCriterion,
|
||||
Not: &models.SceneFilterType{
|
||||
Rating: &ratingCriterion,
|
||||
},
|
||||
}
|
||||
|
||||
withTxn(func(r models.Repository) error {
|
||||
sqb := r.Scene()
|
||||
|
||||
scenes := queryScene(t, sqb, &sceneFilter, nil)
|
||||
|
||||
for _, scene := range scenes {
|
||||
verifyString(t, scene.Path, pathCriterion)
|
||||
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
||||
verifyInt64(t, scene.Rating, ratingCriterion)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestSceneIllegalQuery(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
const sceneIdx = 1
|
||||
subFilter := models.SceneFilterType{
|
||||
Path: &models.StringCriterionInput{
|
||||
Value: getSceneStringValue(sceneIdx, "Path"),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
}
|
||||
|
||||
sceneFilter := &models.SceneFilterType{
|
||||
And: &subFilter,
|
||||
Or: &subFilter,
|
||||
}
|
||||
|
||||
withTxn(func(r models.Repository) error {
|
||||
sqb := r.Scene()
|
||||
|
||||
_, _, err := sqb.Query(sceneFilter, nil)
|
||||
assert.NotNil(err)
|
||||
|
||||
sceneFilter.Or = nil
|
||||
sceneFilter.Not = &subFilter
|
||||
_, _, err = sqb.Query(sceneFilter, nil)
|
||||
assert.NotNil(err)
|
||||
|
||||
sceneFilter.And = nil
|
||||
sceneFilter.Or = &subFilter
|
||||
_, _, err = sqb.Query(sceneFilter, nil)
|
||||
assert.NotNil(err)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func verifyScenesPath(t *testing.T, pathCriterion models.StringCriterionInput) {
|
||||
withTxn(func(r models.Repository) error {
|
||||
sqb := r.Scene()
|
||||
|
||||
@@ -103,7 +103,7 @@ func getSearchBinding(columns []string, q string, not bool) (string, []interface
|
||||
notStr := ""
|
||||
binaryType := " OR "
|
||||
if not {
|
||||
notStr = " NOT "
|
||||
notStr = " NOT"
|
||||
binaryType = " AND "
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user