dialect/sql: add HasPrefixFold and HasSuffixFold predicates (#4233)

* FieldHasPrefixFold and HasPrefixFold predicates

* FieldHasSuffixFold and HasSuffixFold predicates

* Review feedback - per dialect SQL generation.
This commit is contained in:
Mohsin Hijazee
2024-10-09 15:32:48 +02:00
committed by GitHub
parent 9627017062
commit d1dab301c6
4 changed files with 180 additions and 22 deletions

View File

@@ -1716,6 +1716,32 @@ func (p *Predicate) escapedLike(col, left, right, word string) *Predicate {
})
}
// ContainsFold is a helper predicate that applies the LIKE predicate with case-folding.
func (p *Predicate) escapedLikeFold(col, left, substr, right string) *Predicate {
return p.Append(func(b *Builder) {
w, escaped := escape(substr)
switch b.dialect {
case dialect.MySQL:
// We assume the CHARACTER SET is configured to utf8mb4,
// because this how it is defined in dialect/sql/schema.
b.Ident(col).WriteString(" COLLATE utf8mb4_general_ci LIKE ")
b.Arg(left + strings.ToLower(w) + right)
case dialect.Postgres:
b.Ident(col).WriteString(" ILIKE ")
b.Arg(left + strings.ToLower(w) + right)
default: // SQLite.
var f Func
f.SetDialect(b.dialect)
f.Lower(col)
b.WriteString(f.String()).WriteString(" LIKE ")
b.Arg(left + strings.ToLower(w) + right)
if escaped {
p.WriteString(" ESCAPE ").Arg("\\")
}
}
})
}
// HasPrefix is a helper predicate that checks prefix using the LIKE predicate.
func HasPrefix(col, prefix string) *Predicate {
return P().HasPrefix(col, prefix)
@@ -1726,6 +1752,16 @@ func (p *Predicate) HasPrefix(col, prefix string) *Predicate {
return p.escapedLike(col, "", "%", prefix)
}
// HasPrefixFold is a helper predicate that checks prefix using the ILIKE predicate.
func HasPrefixFold(col, prefix string) *Predicate {
return P().HasPrefixFold(col, prefix)
}
// HasPrefixFold is a helper predicate that checks prefix using the ILIKE predicate.
func (p *Predicate) HasPrefixFold(col, prefix string) *Predicate {
return p.escapedLikeFold(col, "", prefix, "%")
}
// ColumnsHasPrefix appends a new predicate that checks if the given column begins with the other column (prefix).
func ColumnsHasPrefix(col, prefixC string) *Predicate {
return P().ColumnsHasPrefix(col, prefixC)
@@ -1760,6 +1796,14 @@ func (p *Predicate) HasSuffix(col, suffix string) *Predicate {
return p.escapedLike(col, "%", "", suffix)
}
// HasSuffixFold is a helper predicate that checks suffix using the ILIKE predicate.
func HasSuffixFold(col, suffix string) *Predicate { return P().HasSuffixFold(col, suffix) }
// HasSuffixFold is a helper predicate that checks suffix using the ILIKE predicate.
func (p *Predicate) HasSuffixFold(col, suffix string) *Predicate {
return p.escapedLikeFold(col, "%", suffix, "")
}
// EqualFold is a helper predicate that applies the "=" predicate with case-folding.
func EqualFold(col, sub string) *Predicate { return P().EqualFold(col, sub) }
@@ -1800,28 +1844,7 @@ func ContainsFold(col, sub string) *Predicate { return P().ContainsFold(col, sub
// ContainsFold is a helper predicate that applies the LIKE predicate with case-folding.
func (p *Predicate) ContainsFold(col, substr string) *Predicate {
return p.Append(func(b *Builder) {
w, escaped := escape(substr)
switch b.dialect {
case dialect.MySQL:
// We assume the CHARACTER SET is configured to utf8mb4,
// because this how it is defined in dialect/sql/schema.
b.Ident(col).WriteString(" COLLATE utf8mb4_general_ci LIKE ")
b.Arg("%" + strings.ToLower(w) + "%")
case dialect.Postgres:
b.Ident(col).WriteString(" ILIKE ")
b.Arg("%" + strings.ToLower(w) + "%")
default: // SQLite.
var f Func
f.SetDialect(b.dialect)
f.Lower(col)
b.WriteString(f.String()).WriteString(" LIKE ")
b.Arg("%" + strings.ToLower(w) + "%")
if escaped {
p.WriteString(" ESCAPE ").Arg("\\")
}
}
})
return p.escapedLikeFold(col, "%", substr, "%")
}
// CompositeGT returns a composite ">" predicate

View File

@@ -521,6 +521,91 @@ func TestBuilder(t *testing.T) {
wantQuery: `UPDATE "users" SET "age" = COALESCE("users"."age", 0) + $1 WHERE "nickname" LIKE $2`,
wantArgs: []any{1, "a8m%"},
},
{
input: Update("users").
Set("name", "foo").
Where(And(HasPrefixFold("nickname", "a8m"), Contains("lastname", "mash"))),
wantQuery: "UPDATE `users` SET `name` = ? WHERE LOWER(`nickname`) LIKE ? AND `lastname` LIKE ?",
wantArgs: []any{"foo", "a8m%", "%mash%"},
},
{
input: Dialect(dialect.Postgres).
Update("users").
Set("name", "foo").
Where(And(HasPrefixFold("nickname", "a8m"), Contains("lastname", "mash"))),
wantQuery: `UPDATE "users" SET "name" = $1 WHERE "nickname" ILIKE $2 AND "lastname" LIKE $3`,
wantArgs: []any{"foo", "a8m%", "%mash%"},
},
{
input: Update("users").
Add("age", 1).
Where(HasPrefixFold("nickname", "a8m")),
wantQuery: "UPDATE `users` SET `age` = COALESCE(`users`.`age`, 0) + ? WHERE LOWER(`nickname`) LIKE ?",
wantArgs: []any{1, "a8m%"},
},
{
input: Update("users").
Set("age", 1).
Add("age", 2).
Where(HasPrefixFold("nickname", "a8m")),
wantQuery: "UPDATE `users` SET `age` = ?, `age` = COALESCE(`users`.`age`, 0) + ? WHERE LOWER(`nickname`) LIKE ?",
wantArgs: []any{1, 2, "a8m%"},
},
{
input: Update("users").
Add("age", 2).
Set("age", 1).
Where(HasPrefixFold("nickname", "a8m")),
wantQuery: "UPDATE `users` SET `age` = ? WHERE LOWER(`nickname`) LIKE ?",
wantArgs: []any{1, "a8m%"},
},
{
input: Dialect(dialect.Postgres).
Update("users").
Add("age", 1).
Where(HasPrefixFold("nickname", "a8m")),
wantQuery: `UPDATE "users" SET "age" = COALESCE("users"."age", 0) + $1 WHERE "nickname" ILIKE $2`,
wantArgs: []any{1, "a8m%"},
},
{
input: Dialect(dialect.Postgres).
Update("users").
Set("name", "foo").
Where(And(HasSuffixFold("nickname", "a8m"), Contains("lastname", "mash"))),
wantQuery: `UPDATE "users" SET "name" = $1 WHERE "nickname" ILIKE $2 AND "lastname" LIKE $3`,
wantArgs: []any{"foo", "%a8m", "%mash%"},
},
{
input: Update("users").
Add("age", 1).
Where(HasSuffixFold("nickname", "a8m")),
wantQuery: "UPDATE `users` SET `age` = COALESCE(`users`.`age`, 0) + ? WHERE LOWER(`nickname`) LIKE ?",
wantArgs: []any{1, "%a8m"},
},
{
input: Update("users").
Set("age", 1).
Add("age", 2).
Where(HasSuffixFold("nickname", "a8m")),
wantQuery: "UPDATE `users` SET `age` = ?, `age` = COALESCE(`users`.`age`, 0) + ? WHERE LOWER(`nickname`) LIKE ?",
wantArgs: []any{1, 2, "%a8m"},
},
{
input: Update("users").
Add("age", 2).
Set("age", 1).
Where(HasSuffixFold("nickname", "a8m")),
wantQuery: "UPDATE `users` SET `age` = ? WHERE LOWER(`nickname`) LIKE ?",
wantArgs: []any{1, "%a8m"},
},
{
input: Dialect(dialect.Postgres).
Update("users").
Add("age", 1).
Where(HasSuffixFold("nickname", "a8m")),
wantQuery: `UPDATE "users" SET "age" = COALESCE("users"."age", 0) + $1 WHERE "nickname" ILIKE $2`,
wantArgs: []any{1, "%a8m"},
},
{
input: Update("users").
Add("age", 1).

View File

@@ -156,6 +156,13 @@ func FieldHasPrefix(name string, prefix string) func(*Selector) {
}
}
// FieldHasPrefixFold returns a raw predicate to check if the field has the given prefix with case-folding
func FieldHasPrefixFold(name string, prefix string) func(*Selector) {
return func(s *Selector) {
s.Where(HasPrefixFold(s.C(name), prefix))
}
}
// FieldHasSuffix returns a raw predicate to check if the field has the given suffix.
func FieldHasSuffix(name string, suffix string) func(*Selector) {
return func(s *Selector) {
@@ -163,6 +170,13 @@ func FieldHasSuffix(name string, suffix string) func(*Selector) {
}
}
// FieldHasSuffixFold returns a raw predicate to check if the field has the given suffix with case-folding
func FieldHasSuffixFold(name string, suffix string) func(*Selector) {
return func(s *Selector) {
s.Where(HasSuffixFold(s.C(name), suffix))
}
}
// FieldContains returns a raw predicate to check if the field contains the given substring.
func FieldContains(name string, substr string) func(*Selector) {
return func(s *Selector) {

View File

@@ -336,6 +336,24 @@ func TestFieldHasPrefix(t *testing.T) {
})
}
func TestFieldHasPrefixFold(t *testing.T) {
p := FieldHasPrefixFold("name", "a8m")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` COLLATE utf8mb4_general_ci LIKE ?", query)
require.Equal(t, []any{"a8m%"}, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."name" ILIKE $1`, query)
require.Equal(t, []any{"a8m%"}, args)
})
}
func TestFieldHasSuffix(t *testing.T) {
p := FieldHasSuffix("name", "a8m")
t.Run("MySQL", func(t *testing.T) {
@@ -354,6 +372,24 @@ func TestFieldHasSuffix(t *testing.T) {
})
}
func TestFieldHasSuffixFold(t *testing.T) {
p := FieldHasSuffixFold("name", "a8m")
t.Run("MySQL", func(t *testing.T) {
s := Dialect(dialect.MySQL).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, "SELECT * FROM `users` WHERE `users`.`name` COLLATE utf8mb4_general_ci LIKE ?", query)
require.Equal(t, []any{"%a8m"}, args)
})
t.Run("PostgreSQL", func(t *testing.T) {
s := Dialect(dialect.Postgres).Select("*").From(Table("users"))
p(s)
query, args := s.Query()
require.Equal(t, `SELECT * FROM "users" WHERE "users"."name" ILIKE $1`, query)
require.Equal(t, []any{"%a8m"}, args)
})
}
func TestFieldContains(t *testing.T) {
p := FieldContains("name", "a8m")
t.Run("MySQL", func(t *testing.T) {