Skip to content

Commit 96c18ce

Browse files
created new maybe more performant version of the tags package (wich removes unneeded braces)
1 parent a1f4988 commit 96c18ce

6 files changed

Lines changed: 570 additions & 0 deletions

File tree

tags2/bun.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright (c) 2026 Keymaster Team
2+
// Keymaster - SSH key management system
3+
// This source code is licensed under the MIT license found in the LICENSE file.
4+
package tags2
5+
6+
import (
7+
"strings"
8+
9+
"github.com/toeirei/keymaster/util/slicest"
10+
"github.com/uptrace/bun"
11+
)
12+
13+
const (
14+
bunAnd bunMode = true
15+
bunOr bunMode = false
16+
17+
// tuned for postgres
18+
bunEscape string = "!"
19+
bunEscapedChars string = `%_[]^-{}`
20+
bunWildcard string = "_"
21+
bunWildcards string = "%"
22+
bunTagDelimiter string = "|"
23+
)
24+
25+
type bunMode bool
26+
27+
func ToBunString(tags Tags) string {
28+
return bunTagDelimiter + strings.Join(tags.Slice(), bunTagDelimiter) + bunTagDelimiter
29+
}
30+
31+
func FromBunString(str string) Tags {
32+
if str == "" {
33+
return Tags{}
34+
}
35+
str, _ = strings.CutPrefix(str, bunTagDelimiter)
36+
str, _ = strings.CutSuffix(str, bunTagDelimiter)
37+
strs := strings.Split(str, bunTagDelimiter)
38+
return slicest.Map(strs, func(str string) Tag { return Tag(str) })
39+
}
40+
41+
func ApplyToBunQuery(expr Expr, qb bun.QueryBuilder, column string) bun.QueryBuilder {
42+
return expr.applyToBunQuery(qb, column, bunAnd, false)
43+
}
44+
45+
func (e ValueExpr) applyToBunQuery(qb bun.QueryBuilder, column string, mode bunMode, negate bool) bun.QueryBuilder {
46+
sqlExpr := e.Value
47+
48+
// escape special chars
49+
sqlExpr = slicest.ReduceD([]rune(bunEscapedChars), sqlExpr, func(char rune, sqlexpr string) string {
50+
return strings.ReplaceAll(sqlexpr, string(char), bunEscape+string(char))
51+
})
52+
53+
// enable wildcards
54+
sqlExpr = strings.ReplaceAll(sqlExpr, exprWildcards, bunWildcards)
55+
sqlExpr = strings.ReplaceAll(sqlExpr, exprWildcard, bunWildcard)
56+
57+
// add delimiters and wildcards to not match across multiple tags
58+
sqlExpr = bunWildcards + bunTagDelimiter + sqlExpr + bunTagDelimiter + bunWildcards
59+
60+
// setup negation string
61+
var queryNot string
62+
if negate {
63+
queryNot = " NOT"
64+
}
65+
66+
// build query string
67+
query := column + queryNot + " LIKE ? ESCAPE '" + bunEscape + "'"
68+
69+
// apply query
70+
if mode == bunAnd {
71+
return qb.Where(query, sqlExpr)
72+
} else {
73+
return qb.WhereOr(query, sqlExpr)
74+
}
75+
}
76+
77+
func (e AndExpr) applyToBunQuery(qb bun.QueryBuilder, column string, _ bunMode, negate bool) bun.QueryBuilder {
78+
// applys all sub expressions
79+
return slicest.ReduceD(e.Exprs, qb, func(expr Expr, qb bun.QueryBuilder) bun.QueryBuilder {
80+
// flips mode AND to OR, if negated
81+
switch expr.(type) {
82+
case OrExpr:
83+
return applyBracesToBunQuery(expr, qb, column, bunMode(!negate), negate)
84+
default:
85+
return expr.applyToBunQuery(qb, column, bunMode(!negate), negate)
86+
}
87+
})
88+
}
89+
90+
func (e OrExpr) applyToBunQuery(qb bun.QueryBuilder, column string, _ bunMode, negate bool) bun.QueryBuilder {
91+
// applys all sub expressions
92+
return slicest.ReduceD(e.Exprs, qb, func(expr Expr, qb bun.QueryBuilder) bun.QueryBuilder {
93+
// flips mode OR to AND, if negated
94+
switch expr.(type) {
95+
case AndExpr, OrExpr:
96+
return applyBracesToBunQuery(expr, qb, column, bunMode(negate), negate)
97+
default:
98+
return expr.applyToBunQuery(qb, column, bunMode(negate), negate)
99+
}
100+
})
101+
}
102+
103+
func (e NotExpr) applyToBunQuery(qb bun.QueryBuilder, column string, mode bunMode, negate bool) bun.QueryBuilder {
104+
// flips negate for subexpression
105+
switch e.Expr.(type) {
106+
case AndExpr, OrExpr:
107+
return applyBracesToBunQuery(e.Expr, qb, column, mode, !negate)
108+
default:
109+
return e.Expr.applyToBunQuery(qb, column, mode, !negate)
110+
}
111+
}
112+
113+
func applyBracesToBunQuery(e Expr, qb bun.QueryBuilder, column string, mode bunMode, negate bool) bun.QueryBuilder {
114+
var seperator string
115+
switch mode {
116+
case bunAnd:
117+
seperator = " AND "
118+
case bunOr:
119+
seperator = " OR "
120+
}
121+
122+
return qb.WhereGroup(seperator, func(qb bun.QueryBuilder) bun.QueryBuilder {
123+
// braces them self can't be negated.
124+
// flipping mode and passing negated flag, has the effect of negating the whole braces content.
125+
return e.applyToBunQuery(qb, column, !bunMode(negate), negate)
126+
})
127+
}

tags2/bun_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) 2026 Keymaster Team
2+
// Keymaster - SSH key management system
3+
// This source code is licensed under the MIT license found in the LICENSE file.
4+
package tags2_test
5+
6+
import (
7+
"fmt"
8+
"testing"
9+
10+
tags "github.com/toeirei/keymaster/tags2"
11+
"github.com/uptrace/bun"
12+
"github.com/uptrace/bun/dialect/sqlitedialect"
13+
"github.com/uptrace/bun/schema"
14+
)
15+
16+
type bunTest struct {
17+
matcher string
18+
sqlQuery string
19+
}
20+
21+
var bunTests = []bunTest{
22+
// --- Basic Exact Matches ---
23+
{"prod", "SELECT * WHERE (tags LIKE '%|prod|%' ESCAPE '!')"},
24+
25+
// --- Wildcards (* = single char, ** = any sequence) ---
26+
{"v*", "SELECT * WHERE (tags LIKE '%|v_|%' ESCAPE '!')"},
27+
{"api-**", "SELECT * WHERE (tags LIKE '%|api!-%|%' ESCAPE '!')"},
28+
29+
// --- NOT (!) ---
30+
{"!deprecated", "SELECT * WHERE (tags NOT LIKE '%|deprecated|%' ESCAPE '!')"},
31+
32+
// --- AND (&) ---
33+
{"golang&backend", "SELECT * WHERE (tags LIKE '%|golang|%' ESCAPE '!') AND (tags LIKE '%|backend|%' ESCAPE '!')"},
34+
35+
// --- OR (|) ---
36+
{"ios|android", "SELECT * WHERE (tags LIKE '%|ios|%' ESCAPE '!') OR (tags LIKE '%|android|%' ESCAPE '!')"},
37+
38+
// --- Complex Nesting ---
39+
{"(aws|gcp)&!legacy", "SELECT * WHERE ((tags LIKE '%|aws|%' ESCAPE '!') OR (tags LIKE '%|gcp|%' ESCAPE '!')) AND (tags NOT LIKE '%|legacy|%' ESCAPE '!')"},
40+
{"auth&(**-admin|super-**)", "SELECT * WHERE (tags LIKE '%|auth|%' ESCAPE '!') AND ((tags LIKE '%|%!-admin|%' ESCAPE '!') OR (tags LIKE '%|super!-%|%' ESCAPE '!'))"},
41+
{"!(test|stage)&prod", "SELECT * WHERE ((tags NOT LIKE '%|test|%' ESCAPE '!') AND (tags NOT LIKE '%|stage|%' ESCAPE '!')) AND (tags LIKE '%|prod|%' ESCAPE '!')"},
42+
43+
// --- Edge Cases ---
44+
{"**", "SELECT * WHERE (tags LIKE '%|%|%' ESCAPE '!')"},
45+
{"*", "SELECT * WHERE (tags LIKE '%|_|%' ESCAPE '!')"},
46+
}
47+
48+
func TestApplyToBunQuery(t *testing.T) {
49+
for _, bt := range bunTests {
50+
t.Run(fmt.Sprintf("Matcher(%q)", bt.matcher), func(t *testing.T) {
51+
expr, err := tags.ParseMatcher(bt.matcher)
52+
if err != nil {
53+
t.Fatalf("failed to parse matcher %q: %v", bt.matcher, err)
54+
}
55+
56+
// apply expr to new QueryBuilder
57+
qb := tags.ApplyToBunQuery(expr, bun.NewSelectQuery(nil).QueryBuilder(), "tags")
58+
59+
// render QueryBuilder to fresh []byte
60+
queryBytes, err := qb.AppendQuery(schema.NewQueryGen(sqlitedialect.New()), []byte{})
61+
if err != nil {
62+
t.Fatalf("failed to render QueryBuilder: %v", err)
63+
}
64+
got := string(queryBytes)
65+
66+
if got != bt.sqlQuery {
67+
t.Errorf("got = %q; expected = %q", got, bt.sqlQuery)
68+
}
69+
70+
t.Log(got)
71+
})
72+
}
73+
}

tags2/expr.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright (c) 2026 Keymaster Team
2+
// Keymaster - SSH key management system
3+
// This source code is licensed under the MIT license found in the LICENSE file.
4+
package tags2
5+
6+
import (
7+
"fmt"
8+
"regexp"
9+
"strings"
10+
11+
"github.com/toeirei/keymaster/util/slicest"
12+
"github.com/uptrace/bun"
13+
)
14+
15+
const (
16+
exprAnd rune = '&'
17+
exprOr rune = '|'
18+
exprNot string = "!"
19+
exprBracesOpen string = "("
20+
exprBracesClose string = ")"
21+
exprWildcard string = "*"
22+
exprWildcards string = "**"
23+
)
24+
25+
type Expr interface {
26+
fmt.Stringer
27+
Eval(tags Tags) bool
28+
applyToBunQuery(qb bun.QueryBuilder, column string, mode bunMode, negate bool) bun.QueryBuilder
29+
}
30+
31+
type ValueExpr struct{ Value string }
32+
type AndExpr struct{ Exprs []Expr }
33+
type OrExpr struct{ Exprs []Expr }
34+
type NotExpr struct{ Expr Expr }
35+
36+
// type BracesExpr struct{ Expr Expr }
37+
38+
// [ValueExpr] implements [Expr]
39+
// [AndExpr] implements [Expr]
40+
// [OrExpr] implements [Expr]
41+
// [NotExpr] implements [Expr]
42+
// // [BracesExpr] implements [Expr]
43+
44+
var _ Expr = ValueExpr{}
45+
var _ Expr = AndExpr{}
46+
var _ Expr = OrExpr{}
47+
var _ Expr = NotExpr{}
48+
49+
// var _ Expr = BracesExpr{}
50+
51+
// --- [fmt.Stringer] implementations ---
52+
53+
func (e ValueExpr) String() string {
54+
return e.Value
55+
}
56+
func (e AndExpr) String() string {
57+
return strings.Join(slicest.Map(e.Exprs, func(e Expr) string {
58+
switch e.(type) {
59+
case OrExpr:
60+
return exprBracesOpen + e.String() + exprBracesClose
61+
default:
62+
return e.String()
63+
}
64+
}), " "+string(exprAnd)+" ")
65+
}
66+
func (e OrExpr) String() string {
67+
return strings.Join(slicest.Map(e.Exprs, func(e Expr) string {
68+
switch e.(type) {
69+
case AndExpr, OrExpr:
70+
return exprBracesOpen + e.String() + exprBracesClose
71+
default:
72+
return e.String()
73+
}
74+
}), " "+string(exprOr)+" ")
75+
}
76+
func (e NotExpr) String() string {
77+
switch e.Expr.(type) {
78+
case AndExpr, OrExpr:
79+
return exprNot + exprBracesOpen + e.Expr.String() + exprBracesClose
80+
default:
81+
return exprNot + e.Expr.String()
82+
}
83+
}
84+
85+
// func (e BracesExpr) String() string {
86+
// return exprBracesOpen + e.Expr.String() + exprBracesClose
87+
// }
88+
89+
// --- [Expr.Eval] implementations ---
90+
91+
func (e ValueExpr) Eval(tags Tags) bool {
92+
expr := regexp.QuoteMeta(e.Value)
93+
expr = strings.ReplaceAll(expr, regexp.QuoteMeta(exprWildcards), ".*")
94+
expr = strings.ReplaceAll(expr, regexp.QuoteMeta(exprWildcard), ".")
95+
regexpr := regexp.MustCompile("^" + expr + "$")
96+
return slicest.Contains(tags, func(tag Tag) bool {
97+
return regexpr.MatchString(string(tag))
98+
})
99+
}
100+
func (e AndExpr) Eval(tags Tags) bool {
101+
return !slicest.Contains(e.Exprs, func(expr Expr) bool { return !expr.Eval(tags) })
102+
}
103+
func (e OrExpr) Eval(tags Tags) bool {
104+
return slicest.Contains(e.Exprs, func(expr Expr) bool { return expr.Eval(tags) })
105+
}
106+
func (e NotExpr) Eval(tags Tags) bool {
107+
return !e.Expr.Eval(tags)
108+
}
109+
110+
// func (e BracesExpr) Eval(tags Tags) bool {
111+
// return e.Expr.Eval(tags)
112+
// }

0 commit comments

Comments
 (0)