mirror of
https://codeberg.org/readeck/readeck.git
synced 2026-06-18 11:04:36 +00:00
202 lines
4.3 KiB
Go
202 lines
4.3 KiB
Go
// SPDX-FileCopyrightText: © 2024 Olivier Meunier <olivier@neokraft.net>
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
package searchstring
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/doug-martin/goqu/v9"
|
|
"github.com/doug-martin/goqu/v9/exp"
|
|
)
|
|
|
|
const sqliteCatchAll = "catchall:oooooo"
|
|
|
|
var valueCleanup = func() *strings.Replacer {
|
|
excluded := [][2]int{
|
|
{0x00, 0x1f},
|
|
{0x21, 0x2f},
|
|
{0x3a, 0x40},
|
|
{0x5b, 0x60},
|
|
{0x7b, 0xbf},
|
|
|
|
{0xFDD0, 0xFDDF},
|
|
{0xFFFE, 0xFFFF},
|
|
|
|
{0x1FFFE, 0x1FFFF},
|
|
{0x2FFFE, 0x2FFFF},
|
|
{0x3FFFE, 0x3FFFF},
|
|
{0x4FFFE, 0x4FFFF},
|
|
{0x5FFFE, 0x5FFFF},
|
|
{0x6FFFE, 0x6FFFF},
|
|
{0x7FFFE, 0x7FFFF},
|
|
{0x8FFFE, 0x8FFFF},
|
|
{0x9FFFE, 0x9FFFF},
|
|
{0xAFFFE, 0xAFFFF},
|
|
{0xBFFFE, 0xBFFFF},
|
|
{0xCFFFE, 0xCFFFF},
|
|
{0xDFFFE, 0xDFFFF},
|
|
{0xEFFFE, 0xEFFFF},
|
|
{0xFFFFE, 0xFFFFF},
|
|
{0x10FFFE, 0x10FFFF},
|
|
}
|
|
repl := []string{}
|
|
for _, t := range excluded {
|
|
for i := t[0]; i <= t[1]; i++ {
|
|
repl = append(repl, string(rune(i)), " ")
|
|
}
|
|
}
|
|
return strings.NewReplacer(repl...)
|
|
}()
|
|
|
|
// BuilderConfig contains the configuration for the SQL builder.
|
|
type BuilderConfig struct {
|
|
relation [2]exp.IdentifierExpression
|
|
fieldList [][2]string
|
|
allowedFields map[string]string
|
|
}
|
|
|
|
// NewBuilderConfig returns a new BuilderConfig.
|
|
func NewBuilderConfig(left, right exp.IdentifierExpression, fields [][2]string) *BuilderConfig {
|
|
res := &BuilderConfig{
|
|
relation: [2]exp.IdentifierExpression{left, right},
|
|
fieldList: fields,
|
|
allowedFields: map[string]string{},
|
|
}
|
|
|
|
for _, x := range res.fieldList {
|
|
res.allowedFields[x[0]] = x[1]
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
// BuildSQL returns a new dataset with the search query.
|
|
func BuildSQL(ds *goqu.SelectDataset, q SearchQuery, conf *BuilderConfig) *goqu.SelectDataset {
|
|
switch ds.Dialect().Dialect() {
|
|
case "postgres":
|
|
return builderPostgres(ds, q, conf)
|
|
case "sqlite3":
|
|
return buildSqlite(ds, q, conf)
|
|
}
|
|
|
|
panic("dialect not implemented")
|
|
}
|
|
|
|
func buildSqlite(ds *goqu.SelectDataset, q SearchQuery, cfg *BuilderConfig) *goqu.SelectDataset {
|
|
// We need the first catchall query in order to use
|
|
// any operator (AND and NOT)later.
|
|
match := []string{sqliteCatchAll}
|
|
groups := regroupFields(q.Terms, cfg)
|
|
|
|
for _, x := range cfg.fieldList {
|
|
terms, ok := groups[x[0]]
|
|
|
|
if !ok {
|
|
continue
|
|
}
|
|
f := x[1]
|
|
for _, t := range terms {
|
|
op := "AND"
|
|
wildcard := ""
|
|
if t.Exclude {
|
|
op = "NOT"
|
|
}
|
|
if t.Wildcard {
|
|
wildcard = "*"
|
|
}
|
|
match = append(match, fmt.Sprintf(`%s %s:"%s"%s`, op, f, t.Value, wildcard))
|
|
}
|
|
}
|
|
|
|
return ds.Join(
|
|
goqu.T(cfg.relation[1].GetTable()),
|
|
goqu.On(cfg.relation[1].Eq(cfg.relation[0])),
|
|
).
|
|
Where(goqu.L(
|
|
"? match ?",
|
|
goqu.T(cfg.relation[1].GetTable()),
|
|
goqu.V(strings.Join(match, " ")),
|
|
)).
|
|
Order(goqu.L("rank").Asc())
|
|
}
|
|
|
|
func builderPostgres(ds *goqu.SelectDataset, q SearchQuery, cfg *BuilderConfig) *goqu.SelectDataset {
|
|
where := goqu.And()
|
|
order := []exp.OrderedExpression{}
|
|
groups := regroupFields(q.Terms, cfg)
|
|
|
|
for _, x := range cfg.fieldList {
|
|
terms, ok := groups[x[0]]
|
|
if !ok {
|
|
continue
|
|
}
|
|
f := x[1]
|
|
values := []string{}
|
|
|
|
for _, t := range terms {
|
|
words := strings.Fields(t.Value)
|
|
neg := ""
|
|
if t.Exclude {
|
|
neg = "!"
|
|
}
|
|
|
|
var value string
|
|
if t.Exact {
|
|
value = fmt.Sprintf("'%s'", strings.Join(words, " "))
|
|
if t.Wildcard {
|
|
value += ":*"
|
|
}
|
|
} else {
|
|
if t.Wildcard {
|
|
for i := range words {
|
|
words[i] += ":*"
|
|
}
|
|
}
|
|
value = strings.Join(words, " & ")
|
|
}
|
|
|
|
values = append(values, fmt.Sprintf("%s(%s)", neg, value))
|
|
}
|
|
|
|
value := goqu.V(strings.Join(values, " & "))
|
|
where = where.Append(goqu.L(
|
|
"? @@ to_tsquery('ts', ?)",
|
|
goqu.L(f), value,
|
|
))
|
|
order = append(order, goqu.L(`ts_rank_cd(?, to_tsquery('ts', ?))`, goqu.L(f), value).Desc())
|
|
}
|
|
|
|
return ds.Join(
|
|
goqu.T(cfg.relation[1].GetTable()),
|
|
goqu.On(cfg.relation[1].Eq(cfg.relation[0])),
|
|
).
|
|
Where(where).
|
|
Order(order...)
|
|
}
|
|
|
|
func regroupFields(terms []SearchTerm, cfg *BuilderConfig) map[string][]SearchTerm {
|
|
groups := map[string][]SearchTerm{}
|
|
|
|
for _, t := range terms {
|
|
if _, ok := cfg.allowedFields[t.Field]; !ok {
|
|
t.Value = t.Field + ":" + t.Value
|
|
t.Field = ""
|
|
t.Exact = true
|
|
}
|
|
|
|
values := strings.Fields(valueCleanup.Replace(t.Value))
|
|
if len(values) == 0 {
|
|
continue
|
|
}
|
|
|
|
t.Value = strings.Join(values, " ")
|
|
|
|
groups[t.Field] = append(groups[t.Field], t)
|
|
}
|
|
|
|
return groups
|
|
}
|