This commit is contained in:
Keith Martin 2026-01-22 04:51:53 +01:00 committed by GitHub
commit 88f614e894
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 326 additions and 243 deletions

View file

@ -183,23 +183,26 @@ func UserAlbums(frm form.SearchAlbums, sess *entity.Session) (results AlbumResul
if txt.NotEmpty(frm.Year) {
// Filter by the pictures included if it is a manually managed album, as these do not have an explicit
// year assigned to them, unlike calendar albums and moments for example.
w, v := AnyInt("albums.album_year", frm.Year, txt.Or, entity.UnknownYear, txt.YearMax)
if frm.Type == entity.AlbumManual {
s = s.Where("? OR albums.album_uid IN (SELECT DISTINCT pay.album_uid FROM photos_albums pay "+
"JOIN photos py ON pay.photo_uid = py.photo_uid WHERE py.photo_year IN (?) AND pay.hidden = 0 AND pay.missing = 0)",
gorm.Expr(AnyInt("albums.album_year", frm.Year, txt.Or, entity.UnknownYear, txt.YearMax)), strings.Split(frm.Year, txt.Or))
gorm.Expr(w, v...), strings.Split(frm.Year, txt.Or))
} else {
s = s.Where(AnyInt("albums.album_year", frm.Year, txt.Or, entity.UnknownYear, txt.YearMax))
s = s.Where(w, v...)
}
}
// Filter by month?
if txt.NotEmpty(frm.Month) {
s = s.Where(AnyInt("albums.album_month", frm.Month, txt.Or, entity.UnknownMonth, txt.MonthMax))
w, v := AnyInt("albums.album_month", frm.Month, txt.Or, entity.UnknownMonth, txt.MonthMax)
s = s.Where(w, v...)
}
// Filter by day?
if txt.NotEmpty(frm.Day) {
s = s.Where(AnyInt("albums.album_day", frm.Day, txt.Or, entity.UnknownDay, txt.DayMax))
w, v := AnyInt("albums.album_day", frm.Day, txt.Or, entity.UnknownDay, txt.DayMax)
s = s.Where(w, v...)
}
// Limit result count.

View file

@ -17,12 +17,19 @@ func Like(s string) string {
return strings.Trim(clean.SqlString(s), " |&*%")
}
// SQLParam cleans and preps a string for use as a parameter in a query. Pre and Post are used to add a wild card character for like's.
func SQLParam(s, pre, post string) string {
return pre + strings.Trim(clean.SQLClean(s), " |&*%") + post
}
// LikeAny builds OR-chained LIKE predicates for a text column. The input string
// may contain AND / OR separators; keywords trigger stemming and plural
// normalization while exact mode disables wildcard suffixes.
func LikeAny(col, s string, keywords, exact bool) (wheres []string) {
// Expectation is that each set of results will be fed into gorm.Expr
// eg. gorm.Expr(wheres[0], valuesSlice[0]...)
func LikeAny(col, s string, keywords, exact bool) (wheres []string, valuesSlice [][]interface{}) {
if s == "" {
return wheres
return wheres, valuesSlice
}
s = txt.StripOr(clean.SearchQuery(s))
@ -39,6 +46,7 @@ func LikeAny(col, s string, keywords, exact bool) (wheres []string) {
for _, k := range txt.UnTrimmedSplitWithEscape(s, txt.AndRune, txt.EscapeRune) {
var orWheres []string
var orValues []interface{}
var words []string
if keywords {
@ -53,9 +61,11 @@ func LikeAny(col, s string, keywords, exact bool) (wheres []string) {
for _, w := range words {
if wildcardThreshold > 0 && len(w) >= wildcardThreshold {
orWheres = append(orWheres, fmt.Sprintf("%s LIKE '%s%%'", col, Like(w)))
orWheres = append(orWheres, fmt.Sprintf("%s LIKE ?", col))
orValues = append(orValues, SQLParam(w, "", "%"))
} else {
orWheres = append(orWheres, fmt.Sprintf("%s LIKE '%s'", col, Like(w)))
orWheres = append(orWheres, fmt.Sprintf("%s LIKE ?", col))
orValues = append(orValues, SQLParam(w, "", ""))
}
if !keywords || !txt.ContainsASCIILetters(w) {
@ -65,35 +75,45 @@ func LikeAny(col, s string, keywords, exact bool) (wheres []string) {
singular := inflection.Singular(w)
if singular != w {
orWheres = append(orWheres, fmt.Sprintf("%s LIKE '%s'", col, Like(singular)))
orWheres = append(orWheres, fmt.Sprintf("%s LIKE ?", col))
orValues = append(orValues, SQLParam(singular, "", ""))
}
}
if len(orWheres) > 0 {
wheres = append(wheres, strings.Join(orWheres, " OR "))
valuesSlice = append(valuesSlice, orValues)
}
}
return wheres
return wheres, valuesSlice
}
// LikeAnyKeyword is a keyword-optimized wrapper around LikeAny.
func LikeAnyKeyword(col, s string) (wheres []string) {
// It returns a slice of where statements, and slices of slice the parameter values.
// Expectation is that each set of results will be fed into gorm.Expr
// eg. gorm.Expr(wheres[0], valuesSlice[0]...)
func LikeAnyKeyword(col, s string) (wheres []string, valuesSlice [][]interface{}) {
return LikeAny(col, s, true, false)
}
// LikeAnyWord matches whole words and keeps wildcard thresholds tuned for
// free-form text search instead of keyword lists.
func LikeAnyWord(col, s string) (wheres []string) {
// It returns a slice of where statements, and slices of slice the parameter values.
// Expectation is that each set of results will be fed into gorm.Expr
// eg. gorm.Expr(wheres[0], valuesSlice[0]...)
func LikeAnyWord(col, s string) (wheres []string, valuesSlice [][]interface{}) {
return LikeAny(col, s, false, false)
}
// LikeAll produces AND-chained LIKE predicates for every significant token in
// the search string. When exact is false, longer words receive a suffix
// wildcard to support prefix matches.
func LikeAll(col, s string, keywords, exact bool) (wheres []string) {
// Expectation is that each set of results will be fed into gorm.Expr
// eg. gorm.Expr(wheres[0], valuesSlice[0]...)
func LikeAll(col, s string, keywords, exact bool) (wheres []string, valuesSlice [][]interface{}) {
if s == "" {
return wheres
return wheres, valuesSlice
}
var words []string
@ -108,42 +128,54 @@ func LikeAll(col, s string, keywords, exact bool) (wheres []string) {
}
if len(words) == 0 {
return wheres
return wheres, valuesSlice
} else if exact {
wildcardThreshold = -1
}
for _, w := range words {
var value []interface{}
if wildcardThreshold > 0 && len(w) >= wildcardThreshold {
wheres = append(wheres, fmt.Sprintf("%s LIKE '%s%%'", col, Like(w)))
wheres = append(wheres, fmt.Sprintf("%s LIKE ?", col))
value = append(value, SQLParam(w, "", "%"))
} else {
wheres = append(wheres, fmt.Sprintf("%s LIKE '%s'", col, Like(w)))
wheres = append(wheres, fmt.Sprintf("%s LIKE ?", col))
value = append(value, SQLParam(w, "", ""))
}
valuesSlice = append(valuesSlice, value)
}
return wheres
return wheres, valuesSlice
}
// LikeAllKeywords is LikeAll specialized for keyword search.
func LikeAllKeywords(col, s string) (wheres []string) {
// Expectation is that each set of results will be fed into gorm.Expr
// eg. gorm.Expr(wheres[0], valuesSlice[0]...)
func LikeAllKeywords(col, s string) (wheres []string, valuesSlice [][]interface{}) {
return LikeAll(col, s, true, false)
}
// LikeAllWords is LikeAll specialized for general word search.
func LikeAllWords(col, s string) (wheres []string) {
// Expectation is that each set of results will be fed into gorm.Expr
// eg. gorm.Expr(wheres[0], valuesSlice[0]...)
func LikeAllWords(col, s string) (wheres []string, valuesSlice [][]interface{}) {
return LikeAll(col, s, false, false)
}
// LikeAllNames splits a name query into AND-separated groups and generates
// prefix or substring matches against each provided column, keeping multi-word
// tokens intact so "John Doe" still matches full-name columns.
func LikeAllNames(cols Cols, s string) (wheres []string) {
// It returns a slice of where statements, and slices of slice the parameter values.
// Expectation is that each set of results will be fed into gorm.Expr
// eg. gorm.Expr(wheres[0], valuesSlice[0]...)
func LikeAllNames(cols Cols, s string) (wheres []string, valuesSlice [][]interface{}) {
if len(cols) == 0 || len(s) < 1 {
return wheres
return wheres, valuesSlice
}
for _, k := range txt.UnTrimmedSplitWithEscape(s, txt.AndRune, txt.EscapeRune) {
var orWheres []string
var orValues []interface{}
for _, w := range txt.UnTrimmedSplitWithEscape(k, txt.OrRune, txt.EscapeRune) {
w = strings.TrimSpace(w)
@ -154,27 +186,33 @@ func LikeAllNames(cols Cols, s string) (wheres []string) {
for _, c := range cols {
if strings.Contains(w, txt.Space) {
orWheres = append(orWheres, fmt.Sprintf("%s LIKE '%s%%'", c, Like(w)))
orWheres = append(orWheres, fmt.Sprintf("%s LIKE ?", c))
orValues = append(orValues, SQLParam(w, "", "%"))
} else {
orWheres = append(orWheres, fmt.Sprintf("%s LIKE '%%%s%%'", c, Like(w)))
orWheres = append(orWheres, fmt.Sprintf("%s LIKE ?", c))
orValues = append(orValues, SQLParam(w, "%", "%"))
}
}
}
if len(orWheres) > 0 {
wheres = append(wheres, strings.Join(orWheres, " OR "))
valuesSlice = append(valuesSlice, orValues)
}
}
return wheres
return wheres, valuesSlice
}
// AnySlug converts human-friendly search terms into slugs and matches them
// against the provided slug column, including the singularized variant for
// plural words (e.g. "Cats" -> "cat").
func AnySlug(col, search, sep string) (where string) {
// It returns a where statement, and a slice of the parameter values.
// Expectation is that each set of results will be fed into gorm.Expr
// eg. gorm.Expr(where, values...)
func AnySlug(col, search, sep string) (where string, values []interface{}) {
if search == "" {
return ""
return "", values
}
if sep == "" {
@ -201,21 +239,25 @@ func AnySlug(col, search, sep string) (where string) {
}
if len(words) == 0 {
return ""
return "", values
}
for _, w := range words {
wheres = append(wheres, fmt.Sprintf("%s = '%s'", col, Like(w)))
wheres = append(wheres, fmt.Sprintf("%s = ?", col))
values = append(values, SQLParam(w, "", ""))
}
return strings.Join(wheres, " OR ")
return strings.Join(wheres, " OR "), values
}
// AnyInt filters user-specified integers through an allowed range and returns
// an OR-chained equality predicate for the values that remain.
func AnyInt(col, numbers, sep string, min, max int) (where string) {
// It returns a where statement, and a slice of the parameter values.
// Expectation is that each set of results will be fed into gorm.Expr
// eg. gorm.Expr(where, values...)
func AnyInt(col, numbers, sep string, low, high int) (where string, values []interface{}) {
if numbers == "" {
return ""
return "", values
}
if sep == "" {
@ -228,7 +270,7 @@ func AnyInt(col, numbers, sep string, min, max int) (where string) {
for _, n := range strings.Split(numbers, sep) {
i := txt.Int(n)
if i == 0 || i < min || i > max {
if i == 0 || i < low || i > high {
continue
}
@ -236,19 +278,23 @@ func AnyInt(col, numbers, sep string, min, max int) (where string) {
}
if len(matches) == 0 {
return ""
return "", values
}
for _, n := range matches {
wheres = append(wheres, fmt.Sprintf("%s = %d", col, n))
wheres = append(wheres, fmt.Sprintf("%s = ?", col))
values = append(values, n)
}
return strings.Join(wheres, " OR ")
return strings.Join(wheres, " OR "), values
}
// OrLike prepares a parameterised OR/LIKE clause for a single column. Star (* )
// wildcards are mapped to SQL percent wildcards before returning the query and
// bind values.
// It returns a where statement, and a slice of the parameter values.
// Expectation is that each set of results will be fed into gorm.Expr
// eg. gorm.Expr(where, values...)
func OrLike(col, s string) (where string, values []interface{}) {
if txt.Empty(col) || txt.Empty(s) {
return "", []interface{}{}
@ -279,6 +325,9 @@ func OrLike(col, s string) (where string, values []interface{}) {
// OrLikeCols behaves like OrLike but fans out the same search terms across
// multiple columns, preserving the order of values so callers can feed them to
// database/sql.
// It returns a where statement, and a slice of the parameter values.
// Expectation is that each set of results will be fed into gorm.Expr
// eg. gorm.Expr(where, values...)
func OrLikeCols(cols []string, s string) (where string, values []interface{}) {
if len(cols) == 0 || txt.Empty(s) {
return "", []interface{}{}

View file

@ -31,289 +31,248 @@ func TestLike(t *testing.T) {
func TestLikeAny(t *testing.T) {
t.Run("AndOrSearch", func(t *testing.T) {
if w := LikeAny("k.keyword", "table spoon & usa | img json", true, false); len(w) != 2 {
t.Fatal("two where conditions expected")
} else {
assert.Equal(t, "k.keyword LIKE 'spoon%' OR k.keyword LIKE 'table%'", w[0])
assert.Equal(t, "k.keyword LIKE 'json%' OR k.keyword LIKE 'usa'", w[1])
}
w, v := LikeAny("k.keyword", "table spoon & usa | img json", true, false)
assert.Equal(t, []string{"k.keyword LIKE ? OR k.keyword LIKE ?", "k.keyword LIKE ? OR k.keyword LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"spoon%", "table%"}, {"json%", "usa"}}, v)
})
t.Run("ExactAndOrSearch", func(t *testing.T) {
if w := LikeAny("k.keyword", "table spoon & usa | img json", true, true); len(w) != 2 {
t.Fatal("two where conditions expected")
} else {
assert.Equal(t, "k.keyword LIKE 'spoon' OR k.keyword LIKE 'table'", w[0])
assert.Equal(t, "k.keyword LIKE 'json' OR k.keyword LIKE 'usa'", w[1])
}
w, v := LikeAny("k.keyword", "table spoon & usa | img json", true, true)
assert.Equal(t, []string{"k.keyword LIKE ? OR k.keyword LIKE ?", "k.keyword LIKE ? OR k.keyword LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"spoon", "table"}, {"json", "usa"}}, v)
})
t.Run("AndOrSearchEn", func(t *testing.T) {
if w := LikeAny("k.keyword", "table spoon and usa or img json", true, false); len(w) != 2 {
t.Fatal("two where conditions expected")
} else {
assert.Equal(t, "k.keyword LIKE 'spoon%' OR k.keyword LIKE 'table%'", w[0])
assert.Equal(t, "k.keyword LIKE 'json%' OR k.keyword LIKE 'usa'", w[1])
}
w, v := LikeAny("k.keyword", "table spoon and usa or img json", true, false)
assert.Equal(t, []string{"k.keyword LIKE ? OR k.keyword LIKE ?", "k.keyword LIKE ? OR k.keyword LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"spoon%", "table%"}, {"json%", "usa"}}, v)
})
t.Run("TableSpoonUsaImgJson", func(t *testing.T) {
if w := LikeAny("k.keyword", "table spoon usa img json", true, false); len(w) != 1 {
t.Fatal("one where condition expected")
} else {
assert.Equal(t, "k.keyword LIKE 'json%' OR k.keyword LIKE 'spoon%' OR k.keyword LIKE 'table%' OR k.keyword LIKE 'usa'", w[0])
}
w, v := LikeAny("k.keyword", "table spoon usa img json", true, false)
assert.Equal(t, []string{"k.keyword LIKE ? OR k.keyword LIKE ? OR k.keyword LIKE ? OR k.keyword LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"json%", "spoon%", "table%", "usa"}}, v)
})
t.Run("CatDog", func(t *testing.T) {
if w := LikeAny("k.keyword", "cat dog", true, false); len(w) != 1 {
t.Fatal("one where condition expected")
} else {
assert.Equal(t, "k.keyword LIKE 'cat' OR k.keyword LIKE 'dog'", w[0])
}
w, v := LikeAny("k.keyword", "cat dog", true, false)
assert.Equal(t, []string{"k.keyword LIKE ? OR k.keyword LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"cat", "dog"}}, v)
})
t.Run("CatsDogs", func(t *testing.T) {
if w := LikeAny("k.keyword", "cats dogs", true, false); len(w) != 1 {
t.Fatal("one where condition expected")
} else {
assert.Equal(t, "k.keyword LIKE 'cats%' OR k.keyword LIKE 'cat' OR k.keyword LIKE 'dogs%' OR k.keyword LIKE 'dog'", w[0])
}
w, v := LikeAny("k.keyword", "cats dogs", true, false)
assert.Equal(t, []string{"k.keyword LIKE ? OR k.keyword LIKE ? OR k.keyword LIKE ? OR k.keyword LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"cats%", "cat", "dogs%", "dog"}}, v)
})
t.Run("Spoon", func(t *testing.T) {
if w := LikeAny("k.keyword", "spoon", true, false); len(w) != 1 {
t.Fatal("one where condition expected")
} else {
assert.Equal(t, "k.keyword LIKE 'spoon%'", w[0])
}
w, v := LikeAny("k.keyword", "spoon", true, false)
assert.Equal(t, []string{"k.keyword LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"spoon%"}}, v)
})
t.Run("Img", func(t *testing.T) {
if w := LikeAny("k.keyword", "img", true, false); len(w) > 0 {
t.Fatal("no where condition expected")
}
w, v := LikeAny("k.keyword", "img", true, false)
assert.Empty(t, w, "where")
assert.Empty(t, v, "value")
})
t.Run("Empty", func(t *testing.T) {
if w := LikeAny("k.keyword", "", true, false); len(w) > 0 {
t.Fatal("no where condition expected")
}
w, v := LikeAny("k.keyword", "", true, false)
assert.Empty(t, w, "where")
assert.Empty(t, v, "value")
})
}
func TestLikeAnyKeyword(t *testing.T) {
t.Run("AndOrSearch", func(t *testing.T) {
if w := LikeAnyKeyword("k.keyword", "table spoon & usa | img json"); len(w) != 2 {
t.Fatal("two where conditions expected")
} else {
assert.Equal(t, "k.keyword LIKE 'spoon%' OR k.keyword LIKE 'table%'", w[0])
assert.Equal(t, "k.keyword LIKE 'json%' OR k.keyword LIKE 'usa'", w[1])
}
w, v := LikeAnyKeyword("k.keyword", "table spoon & usa | img json")
assert.Equal(t, []string{"k.keyword LIKE ? OR k.keyword LIKE ?", "k.keyword LIKE ? OR k.keyword LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"spoon%", "table%"}, {"json%", "usa"}}, v)
})
t.Run("AndOrSearchEn", func(t *testing.T) {
if w := LikeAnyKeyword("k.keyword", "table spoon and usa or img json"); len(w) != 2 {
t.Fatal("two where conditions expected")
} else {
assert.Equal(t, "k.keyword LIKE 'spoon%' OR k.keyword LIKE 'table%'", w[0])
assert.Equal(t, "k.keyword LIKE 'json%' OR k.keyword LIKE 'usa'", w[1])
}
w, v := LikeAnyKeyword("k.keyword", "table spoon and usa or img json")
assert.Equal(t, []string{"k.keyword LIKE ? OR k.keyword LIKE ?", "k.keyword LIKE ? OR k.keyword LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"spoon%", "table%"}, {"json%", "usa"}}, v)
})
}
func TestLikeAnyWord(t *testing.T) {
t.Run("SearchAndOr", func(t *testing.T) {
if w := LikeAnyWord("k.keyword", "table spoon & usa | img json"); len(w) != 2 {
t.Fatal("two where conditions expected")
} else {
assert.Equal(t, "k.keyword LIKE 'spoon%' OR k.keyword LIKE 'table%'", w[0])
assert.Equal(t, "k.keyword LIKE 'img%' OR k.keyword LIKE 'json%' OR k.keyword LIKE 'usa%'", w[1])
}
w, v := LikeAnyWord("k.keyword", "table spoon & usa | img json")
assert.Equal(t, []string{"k.keyword LIKE ? OR k.keyword LIKE ?", "k.keyword LIKE ? OR k.keyword LIKE ? OR k.keyword LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"spoon%", "table%"}, {"img%", "json%", "usa%"}}, v)
})
t.Run("SearchAndOrEnglish", func(t *testing.T) {
if w := LikeAnyWord("k.keyword", "table spoon and usa or img json"); len(w) != 2 {
t.Fatal("two where conditions expected")
} else {
assert.Equal(t, "k.keyword LIKE 'spoon%' OR k.keyword LIKE 'table%'", w[0])
assert.Equal(t, "k.keyword LIKE 'img%' OR k.keyword LIKE 'json%' OR k.keyword LIKE 'usa%'", w[1])
}
w, v := LikeAnyWord("k.keyword", "table spoon and usa or img json")
assert.Equal(t, []string{"k.keyword LIKE ? OR k.keyword LIKE ?", "k.keyword LIKE ? OR k.keyword LIKE ? OR k.keyword LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"spoon%", "table%"}, {"img%", "json%", "usa%"}}, v)
})
t.Run("EscapeSql", func(t *testing.T) {
if w := LikeAnyWord("k.keyword", "table% | 'spoon' & \"us'a"); len(w) != 2 {
t.Fatalf("two where conditions expected: %#v", w)
} else {
assert.Equal(t, "k.keyword LIKE 'spoon%' OR k.keyword LIKE 'table%'", w[0])
assert.Equal(t, "k.keyword LIKE '\"\"us''a%'", w[1])
}
w, v := LikeAnyWord("k.keyword", "table% | 'spoon' & \"us'a")
assert.Equal(t, []string{"k.keyword LIKE ? OR k.keyword LIKE ?", "k.keyword LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"spoon%", "table%"}, {`"us'a%`}}, v)
})
}
func TestLikeAll(t *testing.T) {
t.Run("Keywords", func(t *testing.T) {
if w := LikeAll("k.keyword", "Jo Mander 李", true, false); len(w) == 2 {
assert.Equal(t, "k.keyword LIKE 'mander%'", w[0])
assert.Equal(t, "k.keyword LIKE '李'", w[1])
} else {
t.Logf("wheres: %#v", w)
t.Fatal("two where conditions expected")
}
w, v := LikeAll("k.keyword", "Jo Mander 李", true, false)
assert.Equal(t, []string{"k.keyword LIKE ?", "k.keyword LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"mander%"}, {"李"}}, v)
})
t.Run("Exact", func(t *testing.T) {
if w := LikeAll("k.keyword", "Jo Mander 李", true, true); len(w) == 2 {
assert.Equal(t, "k.keyword LIKE 'mander'", w[0])
assert.Equal(t, "k.keyword LIKE '李'", w[1])
} else {
t.Logf("wheres: %#v", w)
t.Fatal("two where conditions expected")
}
w, v := LikeAll("k.keyword", "Jo Mander 李", true, true)
assert.Equal(t, []string{"k.keyword LIKE ?", "k.keyword LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"mander"}, {"李"}}, v)
})
t.Run("StringEmpty", func(t *testing.T) {
w := LikeAll("k.keyword", "", true, true)
w, v := LikeAll("k.keyword", "", true, true)
assert.Empty(t, w)
assert.Empty(t, v)
})
t.Run("ZeroWords", func(t *testing.T) {
w := LikeAll("k.keyword", "ab", true, true)
w, v := LikeAll("k.keyword", "ab", true, true)
assert.Empty(t, w)
assert.Empty(t, v)
})
}
func TestLikeAllKeywords(t *testing.T) {
t.Run("Keywords", func(t *testing.T) {
if w := LikeAllKeywords("k.keyword", "Jo Mander 李"); len(w) == 2 {
assert.Equal(t, "k.keyword LIKE 'mander%'", w[0])
assert.Equal(t, "k.keyword LIKE '李'", w[1])
} else {
t.Fatalf("unexpected result: %#v", w)
}
w, v := LikeAllKeywords("k.keyword", "Jo Mander 李")
assert.Equal(t, []string{"k.keyword LIKE ?", "k.keyword LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"mander%"}, {"李"}}, v)
})
}
func TestLikeAllWords(t *testing.T) {
t.Run("Keywords", func(t *testing.T) {
if w := LikeAllWords("k.name", "Jo Mander 王"); len(w) == 3 {
assert.Equal(t, "k.name LIKE 'jo%'", w[0])
assert.Equal(t, "k.name LIKE 'mander%'", w[1])
assert.Equal(t, "k.name LIKE '王%'", w[2])
} else {
t.Fatalf("unexpected result: %#v", w)
}
w, v := LikeAllWords("k.name", "Jo Mander 王")
assert.Equal(t, []string{"k.name LIKE ?", "k.name LIKE ?", "k.name LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"jo%"}, {"mander%"}, {"王%"}}, v)
})
}
func TestLikeAllNames(t *testing.T) {
t.Run("MultipleNames", func(t *testing.T) {
if w := LikeAllNames(Cols{"k.name"}, "j Mander 王"); len(w) == 1 {
assert.Equal(t, "k.name LIKE 'j Mander 王%'", w[0])
} else {
t.Fatalf("unexpected result: %#v", w)
}
w, v := LikeAllNames(Cols{"k.name"}, "j Mander 王")
assert.Equal(t, []string{"k.name LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"j Mander 王%"}}, v)
})
t.Run("MultipleColumns", func(t *testing.T) {
if w := LikeAllNames(Cols{"a.col1", "b.col2"}, "Mo Mander"); len(w) == 1 {
assert.Equal(t, "a.col1 LIKE 'Mo Mander%' OR b.col2 LIKE 'Mo Mander%'", w[0])
} else {
t.Fatalf("unexpected result: %#v", w)
}
w, v := LikeAllNames(Cols{"a.col1", "b.col2"}, "Mo Mander")
assert.Equal(t, []string{"a.col1 LIKE ? OR b.col2 LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"Mo Mander%", "Mo Mander%"}}, v)
})
t.Run("EmptyName", func(t *testing.T) {
w := LikeAllNames(Cols{"k.name"}, "")
w, v := LikeAllNames(Cols{"k.name"}, "")
assert.Empty(t, w)
assert.Empty(t, v)
})
t.Run("SingleCharacter", func(t *testing.T) {
if w := LikeAllNames(Cols{"k.name"}, "a"); len(w) == 1 {
assert.Equal(t, "k.name LIKE '%a%'", w[0])
} else {
t.Fatalf("unexpected result: %#v", w)
}
w, v := LikeAllNames(Cols{"k.name"}, "a")
assert.Equal(t, []string{"k.name LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"%a%"}}, v)
})
t.Run("FullNames", func(t *testing.T) {
if w := LikeAllNames(Cols{"j.name", "j.alias"}, "Bill & Melinda Gates"); len(w) == 2 {
assert.Equal(t, "j.name LIKE '%Bill%' OR j.alias LIKE '%Bill%'", w[0])
assert.Equal(t, "j.name LIKE 'Melinda Gates%' OR j.alias LIKE 'Melinda Gates%'", w[1])
} else {
t.Fatalf("unexpected result: %#v", w)
}
w, v := LikeAllNames(Cols{"j.name", "j.alias"}, "Bill & Melinda Gates")
assert.Equal(t, []string{"j.name LIKE ? OR j.alias LIKE ?", "j.name LIKE ? OR j.alias LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"%Bill%", "%Bill%"}, {"Melinda Gates%", "Melinda Gates%"}}, v)
})
t.Run("Plus", func(t *testing.T) {
if w := LikeAllNames(Cols{"name"}, clean.SearchQuery("Paul + Paula")); len(w) == 2 {
assert.Equal(t, "name LIKE '%Paul%'", w[0])
assert.Equal(t, "name LIKE '%Paula%'", w[1])
} else {
t.Fatalf("unexpected result: %#v", w)
}
w, v := LikeAllNames(Cols{"name"}, clean.SearchQuery("Paul + Paula"))
assert.Equal(t, []string{"name LIKE ?", "name LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"%Paul%"}, {"%Paula%"}}, v)
})
t.Run("And", func(t *testing.T) {
if w := LikeAllNames(Cols{"name"}, clean.SearchQuery("P and Paula")); len(w) == 2 {
assert.Equal(t, "name LIKE '%P%'", w[0])
assert.Equal(t, "name LIKE '%Paula%'", w[1])
} else {
t.Fatalf("unexpected result: %#v", w)
}
w, v := LikeAllNames(Cols{"name"}, clean.SearchQuery("P and Paula"))
assert.Equal(t, []string{"name LIKE ?", "name LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"%P%"}, {"%Paula%"}}, v)
})
t.Run("Or", func(t *testing.T) {
if w := LikeAllNames(Cols{"name"}, clean.SearchQuery("Paul or Paula")); len(w) == 1 {
assert.Equal(t, "name LIKE '%Paul%' OR name LIKE '%Paula%'", w[0])
} else {
t.Fatalf("unexpected result: %#v", w)
}
w, v := LikeAllNames(Cols{"name"}, clean.SearchQuery("Paul or Paula"))
assert.Equal(t, []string{"name LIKE ? OR name LIKE ?"}, w)
assert.Equal(t, [][]interface{}{{"%Paul%", "%Paula%"}}, v)
})
}
func TestAnySlug(t *testing.T) {
t.Run("Multiple", func(t *testing.T) {
where := AnySlug("custom_slug", "table spoon usa img json", " ")
assert.Equal(t, "custom_slug = 'table' OR custom_slug = 'spoon' OR custom_slug = 'usa' OR custom_slug = 'img' OR custom_slug = 'json'", where)
w, v := AnySlug("custom_slug", "table spoon usa img json", " ")
assert.Equal(t, "custom_slug = ? OR custom_slug = ? OR custom_slug = ? OR custom_slug = ? OR custom_slug = ?", w)
assert.Equal(t, []interface{}{"table", "spoon", "usa", "img", "json"}, v)
})
t.Run("CatDog", func(t *testing.T) {
where := AnySlug("custom_slug", "cat dog", " ")
assert.Equal(t, "custom_slug = 'cat' OR custom_slug = 'dog'", where)
w, v := AnySlug("custom_slug", "cat dog", " ")
assert.Equal(t, "custom_slug = ? OR custom_slug = ?", w)
assert.Equal(t, []interface{}{"cat", "dog"}, v)
})
t.Run("CatsDogs", func(t *testing.T) {
where := AnySlug("custom_slug", "cats dogs", " ")
assert.Equal(t, "custom_slug = 'cats' OR custom_slug = 'cat' OR custom_slug = 'dogs' OR custom_slug = 'dog'", where)
w, v := AnySlug("custom_slug", "cats dogs", " ")
assert.Equal(t, "custom_slug = ? OR custom_slug = ? OR custom_slug = ? OR custom_slug = ?", w)
assert.Equal(t, []interface{}{"cats", "cat", "dogs", "dog"}, v)
})
t.Run("Spoon", func(t *testing.T) {
where := AnySlug("custom_slug", "spoon", " ")
assert.Equal(t, "custom_slug = 'spoon'", where)
w, v := AnySlug("custom_slug", "spoon", " ")
assert.Equal(t, "custom_slug = ?", w)
assert.Equal(t, []interface{}{"spoon"}, v)
})
t.Run("Img", func(t *testing.T) {
where := AnySlug("custom_slug", "img", " ")
assert.Equal(t, "custom_slug = 'img'", where)
w, v := AnySlug("custom_slug", "img", " ")
assert.Equal(t, "custom_slug = ?", w)
assert.Equal(t, []interface{}{"img"}, v)
})
t.Run("Space", func(t *testing.T) {
where := AnySlug("custom_slug", " ", "")
assert.Equal(t, "custom_slug = '' OR custom_slug = ''", where)
w, v := AnySlug("custom_slug", " ", "")
assert.Equal(t, "custom_slug = ? OR custom_slug = ?", w)
assert.Equal(t, []interface{}{"", ""}, v)
})
t.Run("Empty", func(t *testing.T) {
where := AnySlug("custom_slug", "", " ")
where, values := AnySlug("custom_slug", "", " ")
assert.Equal(t, "", where)
assert.Empty(t, values)
})
t.Run("CommaSeparated", func(t *testing.T) {
where := AnySlug("custom_slug", "botanical-garden|landscape|bay", txt.Or)
assert.Equal(t, "custom_slug = 'botanical-garden' OR custom_slug = 'landscape' OR custom_slug = 'bay'", where)
w, v := AnySlug("custom_slug", "botanical-garden,landscape,bay", ",")
assert.Equal(t, "custom_slug = ? OR custom_slug = ? OR custom_slug = ?", w)
assert.Equal(t, []interface{}{"botanical-garden", "landscape", "bay"}, v)
})
t.Run("PipeSeparated", func(t *testing.T) {
w, v := AnySlug("custom_slug", "botanical-garden|landscape|bay", txt.Or)
assert.Equal(t, "custom_slug = ? OR custom_slug = ? OR custom_slug = ?", w)
assert.Equal(t, []interface{}{"botanical-garden", "landscape", "bay"}, v)
})
t.Run("Emoji", func(t *testing.T) {
where := AnySlug("custom_slug", "💐", "|")
assert.Equal(t, "custom_slug = '_5cpzfea'", where)
w, v := AnySlug("custom_slug", "💐", "|")
assert.Equal(t, "custom_slug = ?", w)
assert.Equal(t, []interface{}{"_5cpzfea"}, v)
})
t.Run("EmojiSlug", func(t *testing.T) {
where := AnySlug("custom_slug", "_5cpzfea", "|")
assert.Equal(t, "custom_slug = '_5cpzfea'", where)
w, v := AnySlug("custom_slug", "_5cpzfea", "|")
assert.Equal(t, "custom_slug = ?", w)
assert.Equal(t, []interface{}{"_5cpzfea"}, v)
})
}
func TestAnyInt(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
where := AnyInt("photos.photo_month", "", txt.Or, entity.UnknownMonth, txt.MonthMax)
where, values := AnyInt("photos.photo_month", "", txt.Or, entity.UnknownMonth, txt.MonthMax)
assert.Equal(t, "", where)
assert.Empty(t, values)
})
t.Run("Range", func(t *testing.T) {
where := AnyInt("photos.photo_month", "-3|0|10|9|11|12|13", txt.Or, entity.UnknownMonth, txt.MonthMax)
assert.Equal(t, "photos.photo_month = 10 OR photos.photo_month = 9 OR photos.photo_month = 11 OR photos.photo_month = 12", where)
w, v := AnyInt("photos.photo_month", "-3|0|10|9|11|12|13", txt.Or, entity.UnknownMonth, txt.MonthMax)
assert.Equal(t, "photos.photo_month = ? OR photos.photo_month = ? OR photos.photo_month = ? OR photos.photo_month = ?", w)
assert.Equal(t, []interface{}{10, 9, 11, 12}, v)
})
t.Run("Chars", func(t *testing.T) {
where := AnyInt("photos.photo_month", "a|b|c", txt.Or, entity.UnknownMonth, txt.MonthMax)
where, values := AnyInt("photos.photo_month", "a|b|c", txt.Or, entity.UnknownMonth, txt.MonthMax)
assert.Equal(t, "", where)
assert.Empty(t, values)
})
t.Run("CommaSeparated", func(t *testing.T) {
where := AnyInt("photos.photo_month", "-3,10,9,11,12,13", ",", entity.UnknownMonth, txt.MonthMax)
assert.Equal(t, "photos.photo_month = 10 OR photos.photo_month = 9 OR photos.photo_month = 11 OR photos.photo_month = 12", where)
w, v := AnyInt("photos.photo_month", "-3,10,9,11,12,13", ",", entity.UnknownMonth, txt.MonthMax)
assert.Equal(t, "photos.photo_month = ? OR photos.photo_month = ? OR photos.photo_month = ? OR photos.photo_month = ?", w)
assert.Equal(t, []interface{}{10, 9, 11, 12}, v)
})
t.Run("Invalid", func(t *testing.T) {
where := AnyInt("photos.photo_month", " , | ", ",", entity.UnknownMonth, txt.MonthMax)
where, values := AnyInt("photos.photo_month", " , | ", ",", entity.UnknownMonth, txt.MonthMax)
assert.Equal(t, "", where)
assert.Empty(t, values)
})
}

View file

@ -290,7 +290,9 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
var labels []entity.Label
var labelIds []uint
if labelErr := Db().Where(AnySlug("label_slug", frm.Label, txt.Or)).Or(AnySlug("custom_slug", frm.Label, txt.Or)).Find(&labels).Error; len(labels) == 0 || labelErr != nil {
wl, vl := AnySlug("label_slug", frm.Label, txt.Or)
wc, vc := AnySlug("custom_slug", frm.Label, txt.Or)
if labelErr := Db().Where(wl, vl...).Or(wc, vc...).Find(&labels).Error; len(labels) == 0 || labelErr != nil {
log.Debugf("search: label %s not found", txt.LogParamLower(frm.Label))
return PhotoResults{}, 0, nil
} else {
@ -400,11 +402,13 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
var labels []entity.Label
var labelIds []uint
if labelsErr := Db().Where(AnySlug("custom_slug", frm.Query, " ")).Find(&labels).Error; len(labels) == 0 || labelsErr != nil {
w, v := AnySlug("custom_slug", frm.Query, " ")
if labelsErr := Db().Where(w, v...).Find(&labels).Error; len(labels) == 0 || labelsErr != nil {
log.Tracef("search: label %s not found, using fuzzy search", txt.LogParamLower(frm.Query))
for _, where := range LikeAnyKeyword("k.keyword", frm.Query) {
s = s.Where("files.photo_id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?))", gorm.Expr(where))
wheres, values := LikeAnyKeyword("k.keyword", frm.Query)
for i, where := range wheres {
s = s.Where("files.photo_id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?))", gorm.Expr(where, values[i]...))
}
} else {
for _, l := range labels {
@ -419,10 +423,10 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
}
}
if wheres := LikeAnyKeyword("k.keyword", frm.Query); len(wheres) > 0 {
for _, where := range wheres {
if wheres, values := LikeAnyKeyword("k.keyword", frm.Query); len(wheres) > 0 {
for i, where := range wheres {
s = s.Where("files.photo_id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?)) OR "+
"files.photo_id IN (SELECT pl.photo_id FROM photos_labels pl WHERE pl.uncertainty < 100 AND pl.label_id IN (?))", gorm.Expr(where), labelIds)
"files.photo_id IN (SELECT pl.photo_id FROM photos_labels pl WHERE pl.uncertainty < 100 AND pl.label_id IN (?))", gorm.Expr(where, values[i]...), labelIds)
}
} else {
s = s.Where("files.photo_id IN (SELECT pl.photo_id FROM photos_labels pl WHERE pl.uncertainty < 100 AND pl.label_id IN (?))", labelIds)
@ -432,8 +436,9 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
// Search for one or more keywords.
if txt.NotEmpty(frm.Keywords) {
for _, where := range LikeAnyWord("k.keyword", frm.Keywords) {
s = s.Where("files.photo_id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?))", gorm.Expr(where))
wheres, values := LikeAnyWord("k.keyword", frm.Keywords)
for i, where := range wheres {
s = s.Where("files.photo_id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?))", gorm.Expr(where, values[i]...))
}
}
@ -480,14 +485,16 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
s = s.Where(fmt.Sprintf("files.photo_id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE subj_uid IN (?))",
entity.Marker{}.TableName()), subjects)
} else {
w, v := AnySlug("s.subj_slug", subj, txt.Or)
s = s.Where(fmt.Sprintf("files.photo_id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 JOIN %s s ON s.subj_uid = m.subj_uid WHERE (?))",
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(AnySlug("s.subj_slug", subj, txt.Or)))
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(w, v...))
}
}
} else if txt.NotEmpty(frm.Subjects) {
for _, where := range LikeAllNames(Cols{"subj_name", "subj_alias"}, frm.Subjects) {
wheres, values := LikeAllNames(Cols{"subj_name", "subj_alias"}, frm.Subjects)
for i, where := range wheres {
s = s.Where(fmt.Sprintf("files.photo_id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 JOIN %s s ON s.subj_uid = m.subj_uid WHERE (?))",
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(where))
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(where, values[i]...))
}
}
@ -548,17 +555,20 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
// Filter by year.
if frm.Year != "" {
s = s.Where(AnyInt("photos.photo_year", frm.Year, txt.Or, entity.UnknownYear, txt.YearMax))
w, v := AnyInt("photos.photo_year", frm.Year, txt.Or, entity.UnknownYear, txt.YearMax)
s = s.Where(w, v...)
}
// Filter by month.
if frm.Month != "" {
s = s.Where(AnyInt("photos.photo_month", frm.Month, txt.Or, entity.UnknownMonth, txt.MonthMax))
w, v := AnyInt("photos.photo_month", frm.Month, txt.Or, entity.UnknownMonth, txt.MonthMax)
s = s.Where(w, v...)
}
// Filter by day.
if frm.Day != "" {
s = s.Where(AnyInt("photos.photo_day", frm.Day, txt.Or, entity.UnknownDay, txt.DayMax))
w, v := AnyInt("photos.photo_day", frm.Day, txt.Or, entity.UnknownDay, txt.DayMax)
s = s.Where(w, v...)
}
// Filter by Resolution in Megapixels (MP).
@ -826,8 +836,9 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
v := strings.Trim(frm.Album, "*%") + "%"
s = s.Where("photos.photo_uid IN (SELECT pa.photo_uid FROM photos_albums pa JOIN albums a ON a.album_uid = pa.album_uid AND pa.hidden = 0 WHERE (a.album_title LIKE ? OR a.album_slug LIKE ?))", v, v)
} else if txt.NotEmpty(frm.Albums) {
for _, where := range LikeAnyWord("a.album_title", frm.Albums) {
s = s.Where("photos.photo_uid IN (SELECT pa.photo_uid FROM photos_albums pa JOIN albums a ON a.album_uid = pa.album_uid AND pa.hidden = 0 WHERE (?))", gorm.Expr(where))
wheres, values := LikeAnyWord("a.album_title", frm.Albums)
for i, where := range wheres {
s = s.Where("photos.photo_uid IN (SELECT pa.photo_uid FROM photos_albums pa JOIN albums a ON a.album_uid = pa.album_uid AND pa.hidden = 0 WHERE (?))", gorm.Expr(where, values[i]...))
}
}
}

View file

@ -212,7 +212,9 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
var labels []entity.Label
var labelIds []uint
if labelErr := Db().Where(AnySlug("label_slug", frm.Label, txt.Or)).Or(AnySlug("custom_slug", frm.Label, txt.Or)).Find(&labels).Error; len(labels) == 0 || labelErr != nil {
wl, vl := AnySlug("label_slug", frm.Label, txt.Or)
wc, vc := AnySlug("custom_slug", frm.Label, txt.Or)
if labelErr := Db().Where(wl, vl...).Or(wc, vc...).Find(&labels).Error; len(labels) == 0 || labelErr != nil {
log.Debugf("search: label %s not found", txt.LogParamLower(frm.Label))
return GeoResults{}, nil
} else {
@ -312,11 +314,13 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
var labels []entity.Label
var labelIds []uint
if labelsErr := Db().Where(AnySlug("custom_slug", frm.Query, " ")).Find(&labels).Error; len(labels) == 0 || labelsErr != nil {
w, v := AnySlug("custom_slug", frm.Query, " ")
if labelsErr := Db().Where(w, v...).Find(&labels).Error; len(labels) == 0 || labelsErr != nil {
log.Tracef("search: label %s not found, using fuzzy search", txt.LogParamLower(frm.Query))
for _, where := range LikeAnyKeyword("k.keyword", frm.Query) {
s = s.Where("photos.id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?))", gorm.Expr(where))
wheres, values := LikeAnyKeyword("k.keyword", frm.Query)
for i, where := range wheres {
s = s.Where("photos.id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?))", gorm.Expr(where, values[i]...))
}
} else {
for _, l := range labels {
@ -330,10 +334,10 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
}
}
if wheres := LikeAnyKeyword("k.keyword", frm.Query); len(wheres) > 0 {
for _, where := range wheres {
if wheres, values := LikeAnyKeyword("k.keyword", frm.Query); len(wheres) > 0 {
for i, where := range wheres {
s = s.Where("photos.id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?)) OR "+
"photos.id IN (SELECT pl.photo_id FROM photos_labels pl WHERE pl.uncertainty < 100 AND pl.label_id IN (?))", gorm.Expr(where), labelIds)
"photos.id IN (SELECT pl.photo_id FROM photos_labels pl WHERE pl.uncertainty < 100 AND pl.label_id IN (?))", gorm.Expr(where, values[i]...), labelIds)
}
} else {
s = s.Where("photos.id IN (SELECT pl.photo_id FROM photos_labels pl WHERE pl.uncertainty < 100 AND pl.label_id IN (?))", labelIds)
@ -343,8 +347,9 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
// Search for one or more keywords.
if frm.Keywords != "" {
for _, where := range LikeAnyWord("k.keyword", frm.Keywords) {
s = s.Where("photos.id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?))", gorm.Expr(where))
wheres, values := LikeAnyWord("k.keyword", frm.Keywords)
for i, where := range wheres {
s = s.Where("photos.id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?))", gorm.Expr(where, values[i]...))
}
}
@ -391,14 +396,16 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE subj_uid IN (?))",
entity.Marker{}.TableName()), subjects)
} else {
w, v := AnySlug("s.subj_slug", subj, txt.Or)
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 JOIN %s s ON s.subj_uid = m.subj_uid WHERE (?))",
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(AnySlug("s.subj_slug", subj, txt.Or)))
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(w, v...))
}
}
} else if frm.Subjects != "" {
for _, where := range LikeAllNames(Cols{"subj_name", "subj_alias"}, frm.Subjects) {
wheres, values := LikeAllNames(Cols{"subj_name", "subj_alias"}, frm.Subjects)
for i, where := range wheres {
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 JOIN %s s ON s.subj_uid = m.subj_uid WHERE (?))",
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(where))
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(where, values[i]...))
}
}
@ -410,8 +417,9 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
v := strings.Trim(frm.Album, "*%") + "%"
s = s.Where("photos.photo_uid IN (SELECT pa.photo_uid FROM photos_albums pa JOIN albums a ON a.album_uid = pa.album_uid AND pa.hidden = 0 WHERE (a.album_title LIKE ? OR a.album_slug LIKE ?))", v, v)
} else if txt.NotEmpty(frm.Albums) {
for _, where := range LikeAnyWord("a.album_title", frm.Albums) {
s = s.Where("photos.photo_uid IN (SELECT pa.photo_uid FROM photos_albums pa JOIN albums a ON a.album_uid = pa.album_uid AND pa.hidden = 0 WHERE (?))", gorm.Expr(where))
wheres, values := LikeAnyWord("a.album_title", frm.Albums)
for i, where := range wheres {
s = s.Where("photos.photo_uid IN (SELECT pa.photo_uid FROM photos_albums pa JOIN albums a ON a.album_uid = pa.album_uid AND pa.hidden = 0 WHERE (?))", gorm.Expr(where, values[i]...))
}
}
}
@ -443,17 +451,20 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
// Filter by year.
if frm.Year != "" {
s = s.Where(AnyInt("photos.photo_year", frm.Year, txt.Or, entity.UnknownYear, txt.YearMax))
w, v := AnyInt("photos.photo_year", frm.Year, txt.Or, entity.UnknownYear, txt.YearMax)
s = s.Where(w, v...)
}
// Filter by month.
if frm.Month != "" {
s = s.Where(AnyInt("photos.photo_month", frm.Month, txt.Or, entity.UnknownMonth, txt.MonthMax))
w, v := AnyInt("photos.photo_month", frm.Month, txt.Or, entity.UnknownMonth, txt.MonthMax)
s = s.Where(w, v...)
}
// Filter by day.
if frm.Day != "" {
s = s.Where(AnyInt("photos.photo_day", frm.Day, txt.Or, entity.UnknownDay, txt.DayMax))
w, v := AnyInt("photos.photo_day", frm.Day, txt.Or, entity.UnknownDay, txt.DayMax)
s = s.Where(w, v...)
}
// Filter by Resolution in Megapixels (MP).

View file

@ -56,8 +56,9 @@ func Subjects(frm form.SearchSubjects) (results SubjectResults, err error) {
}
if frm.Query != "" {
for _, where := range LikeAllNames(Cols{"subj_name", "subj_alias"}, frm.Query) {
s = s.Where("?", gorm.Expr(where))
wheres, values := LikeAllNames(Cols{"subj_name", "subj_alias"}, frm.Query)
for i, where := range wheres {
s = s.Where("?", gorm.Expr(where, values[i]...))
}
}
@ -121,7 +122,7 @@ func SubjectUIDs(s string) (result []string, names []string, remaining string) {
var matches []Matches
wheres := LikeAllNames(Cols{"subj_name", "subj_alias"}, s)
wheres, values := LikeAllNames(Cols{"subj_name", "subj_alias"}, s)
if len(wheres) == 0 {
return result, names, s
@ -129,11 +130,11 @@ func SubjectUIDs(s string) (result []string, names []string, remaining string) {
remaining = s
for _, where := range wheres {
for i, where := range wheres {
var subj []string
stmt := Db().Model(entity.Subject{})
stmt = stmt.Where("?", gorm.Expr(where))
stmt = stmt.Where("?", gorm.Expr(where, values[i]...))
if err := stmt.Scan(&matches).Error; err != nil {
log.Errorf("search: %s while finding subjects", err)

View file

@ -51,3 +51,34 @@ func SqlString(s string) string {
return string(b[:j])
}
// SQLClean removes bytes from a string that are determined as requiring omitting
func SQLClean(s string) string {
var i int
for i = range len(s) {
if _, found := SqlSpecial(s[i]); found {
break
}
}
// Return if no omittable characters were found.
if i >= len(s) {
return s
}
b := make([]byte, 2*len(s)-i)
copy(b, s[:i])
j := i
for ; i < len(s); i++ {
if _, omit := SqlSpecial(s[i]); !omit {
// Keep all bytes not omitted
b[j] = s[i]
j++
}
}
return string(b[:j])
}

View file

@ -82,3 +82,21 @@ func TestSqlString(t *testing.T) {
assert.Equal(t, "123ABCabc", SqlString("123ABCabc"))
})
}
func TestSQLClean(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
assert.Equal(t, "", SQLClean(""))
})
t.Run("Special", func(t *testing.T) {
s := "' \" \t \n %_''" // Single Quote, Space, Double Qoute, Space, Tab, Space, New Line, Space, Percent, Underline, Single Quote, Single Quote
exp := "' \" %_''" // Single Quote, Space, Double Qoute, Space, Space, Space, Percent, Underline, Single Quote, Single Quote
result := SQLClean(s)
assert.Equal(t, exp, result)
})
t.Run("Alnum", func(t *testing.T) {
assert.Equal(t, "123ABCabc", SQLClean("123ABCabc"))
})
t.Run("Unicode", func(t *testing.T) {
assert.Equal(t, "Clean《MeUp✀☒ちュس", SQLClean("Clean《MeUp✀☒ちュس"))
})
}