dialect/sql/sqlgraph: add function to order by edge terms (#3426)

This commit is contained in:
Ariel Mashraki
2023-04-01 20:55:00 +03:00
committed by GitHub
parent 6f847a3492
commit 60bb939fc2
4 changed files with 399 additions and 41 deletions

View File

@@ -2214,6 +2214,20 @@ func (s *Selector) AppendSelect(columns ...string) *Selector {
return s
}
// AppendSelectAs appends additional column to the SELECT statement with the given alias.
func (s *Selector) AppendSelectAs(column, as string) *Selector {
s.selection = append(s.selection, ExprFunc(func(b *Builder) {
if b.isIdent(column) {
b.WriteString(column)
} else {
b.WriteString(s.C(column))
}
b.WriteString(" AS ")
b.Ident(as)
}))
return s
}
// SelectExpr changes the columns selection of the SELECT statement
// with custom list of expressions.
func (s *Selector) SelectExpr(exprs ...Querier) *Selector {
@@ -2826,6 +2840,14 @@ func (s *Selector) OrderExpr(exprs ...Querier) *Selector {
return s
}
// OrderExprFunc appends the `ORDER BY` expression that evaluates
// the given function.
func (s *Selector) OrderExprFunc(f func(*Builder)) *Selector {
return s.OrderExpr(
Dialect(s.Dialect()).Expr(f),
)
}
// ClearOrder clears the ORDER BY clause to be empty.
func (s *Selector) ClearOrder() *Selector {
s.order = nil
@@ -3382,6 +3404,11 @@ func (b *Builder) WriteString(s string) *Builder {
return b
}
// S is a short version of WriteString.
func (b *Builder) S(s string) *Builder {
return b.WriteString(s)
}
// Len returns the number of accumulated bytes.
func (b *Builder) Len() int {
if b.sb == nil {

View File

@@ -82,8 +82,6 @@ type Step struct {
Columns []string
// Inverse indicates if the edge is an inverse edge.
Inverse bool
// Name allows giving this edge a name for making queries more readable.
Name string
}
// To is the dest of the path (the neighbors).
To struct {
@@ -292,12 +290,14 @@ func HasNeighborsWith(q *sql.Selector, s *Step, pred func(*sql.Selector)) {
type (
// OrderByOptions holds the information needed to order a query by an edge.
OrderByOptions struct {
// Step to get the edge to order by.
// Step to get the edge to order by its count.
Step *Step
// Desc indicates if the ordering should be descending.
// When false, nulls are ordered first. When true, nulls
// are ordered last.
// Desc indicates if the ordering should be in descending order. When false, NULL values
// are ordered first (default in MySQL and SQLite). When true, NULLs are ordered last.
Desc bool
// Terms used for non-aggregation ordering.
// See, OrderByNeighborTerms for more info.
Terms []OrderByTerm
}
// OrderByInfo holds the information done by the OrderBy functions.
OrderByInfo struct {
@@ -307,21 +307,90 @@ type (
OrderByTerm struct {
Column string // Column name. If empty, an expression is used.
Expr sql.Querier // Expression. If nil, the column is used.
As string // Optional alias.
Type field.Type // Term type.
Desc bool // Descending order.
}
// OrderByOption allows configuring OrderByOptions using functional options.
OrderByOption func(*OrderByOptions)
)
// OrderDesc sets the order to be descending order.
// This option is valid only for a single count order.
func OrderDesc() OrderByOption {
return func(opts *OrderByOptions) {
opts.Desc = true
}
}
// OrderByExpr appends an expression to the order by clause.
func OrderByExpr(x sql.Querier, as string) OrderByOption {
return func(opts *OrderByOptions) {
opts.Terms = append(opts.Terms, OrderByTerm{
Expr: x,
As: as,
})
}
}
// OrderByExprDesc appends an expression to the order by clause in descending order.
func OrderByExprDesc(x sql.Querier, as string) OrderByOption {
return func(opts *OrderByOptions) {
opts.Terms = append(opts.Terms, OrderByTerm{
Expr: x,
As: as,
Desc: true,
})
}
}
// OrderByColumn appends a column to the order by clause.
func OrderByColumn(c string) OrderByOption {
return func(opts *OrderByOptions) {
opts.Terms = append(opts.Terms, OrderByTerm{
Column: c,
})
}
}
// OrderByColumnDesc appends a column to the order by clause in descending order.
func OrderByColumnDesc(c string) OrderByOption {
return func(opts *OrderByOptions) {
opts.Terms = append(opts.Terms, OrderByTerm{
Column: c,
Desc: true,
})
}
}
// NewOrderBy gets list of options and returns a configured order-by.
//
// NewOrderBy(
// sqlgraph.NewStep(
// sqlgraph.From(user.Table, user.FieldID),
// sqlgraph.To(group.Table, group.FieldID),
// sqlgraph.Edge(sqlgraph.M2M, false, user.GroupsTable, user.GroupsPrimaryKey...),
// ),
// OrderByExpr(
// sql.Expr("SUM(age)"),
// "sum_age",
// ),
// )
func NewOrderBy(s *Step, opts ...OrderByOption) *OrderByOptions {
r := &OrderByOptions{Step: s}
for _, opt := range opts {
opt(r)
}
return r
}
// countAlias returns the alias to use for the count column.
func countAlias(q *sql.Selector, s *Step) string {
eName := s.Edge.Name
if eName == "" {
eName = s.To.Table
}
selected := make(map[string]struct{})
for _, c := range q.SelectedColumns() {
selected[c] = struct{}{}
}
column := fmt.Sprintf("count_%s", eName)
column := fmt.Sprintf("count_%s", s.To.Table)
// If the column was already selected,
// try to find a free alias.
if _, ok := selected[column]; ok {
@@ -335,12 +404,12 @@ func countAlias(q *sql.Selector, s *Step) string {
return column
}
// OrderByCountNeighbors appends ordering based on the number of neighbors.
// OrderByNeighborsCount appends ordering based on the number of neighbors.
// For example, order users by their number of posts.
// HasNeighbors applies on the given Selector a neighbors check.
func OrderByCountNeighbors(q *sql.Selector, opts *OrderByOptions) *OrderByInfo {
func OrderByNeighborsCount(q *sql.Selector, opts *OrderByOptions) *OrderByInfo {
var (
countC string
join *sql.Selector
build = sql.Dialect(q.Dialect())
)
switch s, r := opts.Step, opts.Step.Edge.Rel; {
@@ -368,47 +437,120 @@ func OrderByCountNeighbors(q *sql.Selector, opts *OrderByOptions) *OrderByInfo {
pk1 = s.Edge.Columns[1]
}
joinT := build.Table(s.Edge.Table).Schema(s.Edge.Schema)
to := build.Select(
join = build.Select(
joinT.C(pk1),
build.String(func(b *sql.Builder) {
b.WriteString("COUNT(*) AS ").Ident(countC)
}),
).From(joinT).GroupBy(joinT.C(pk1))
q.LeftJoin(to).
q.LeftJoin(join).
On(
q.C(s.From.Column),
to.C(pk1),
join.C(pk1),
)
case r == O2M || (r == O2O && !s.Edge.Inverse):
countC = countAlias(q, s)
edgeT := build.Table(s.Edge.Table).Schema(s.Edge.Schema)
to := build.Select(
join = build.Select(
edgeT.C(s.Edge.Columns[0]),
build.String(func(b *sql.Builder) {
b.WriteString("COUNT(*) AS ").Ident(countC)
}),
).From(edgeT).GroupBy(edgeT.C(s.Edge.Columns[0]))
q.LeftJoin(to).
q.LeftJoin(join).
On(
q.C(s.From.Column),
to.C(s.Edge.Columns[0]),
join.C(s.Edge.Columns[0]),
)
}
q.OrderExpr(
build.Expr(func(b *sql.Builder) {
b.WriteString("COALESCE(").Ident(countC).WriteString(", 0)")
if opts.Desc {
terms := []OrderByTerm{
{Column: countC, Type: field.TypeInt, Desc: opts.Desc},
}
orderTerms(q, join, terms)
return &OrderByInfo{Terms: terms}
}
func orderTerms(q, join *sql.Selector, ts []OrderByTerm) {
for _, t := range ts {
t := t
q.OrderExprFunc(func(b *sql.Builder) {
switch {
case t.As != "":
b.WriteString(join.C(t.As))
case t.Column != "":
b.WriteString(join.C(t.Column))
case t.Expr != nil:
b.Join(t.Expr)
}
// Unlike MySQL and SQLite, NULL values sort as if larger than any other value.
// Therefore, we need to explicitly order NULLs first on ASC and last on DESC.
switch pg := b.Dialect() == dialect.Postgres; {
case pg && t.Desc:
b.WriteString(" DESC NULLS LAST")
case pg:
b.WriteString(" NULLS FIRST")
case t.Desc:
b.WriteString(" DESC")
}
}),
)
return &OrderByInfo{
Terms: []OrderByTerm{
{Column: countC, Type: field.TypeInt},
},
})
}
}
func selectTerms(q *sql.Selector, ts []OrderByTerm) {
for _, t := range ts {
switch {
case t.Column != "" && t.As != "":
q.AppendSelect(q.C(t.Column), t.As)
case t.Column != "":
q.AppendSelect(q.C(t.Column))
case t.Expr != nil:
q.AppendSelectExprAs(t.Expr, t.As)
}
}
}
// OrderByNeighborTerms appends ordering based on the number of neighbors.
// For example, order users by their number of posts.
func OrderByNeighborTerms(q *sql.Selector, opts *OrderByOptions) {
var (
join *sql.Selector
build = sql.Dialect(q.Dialect())
)
switch s, r := opts.Step, opts.Step.Edge.Rel; {
case r == M2O || (r == O2O && s.Edge.Inverse):
toT := build.Table(s.To.Table).Schema(s.To.Schema)
join = build.Select(toT.C(s.To.Column)).
From(toT)
selectTerms(join, opts.Terms)
q.LeftJoin(join).
On(q.C(s.Edge.Columns[0]), join.C(s.To.Column))
case r == M2M:
pk1, pk2 := s.Edge.Columns[1], s.Edge.Columns[0]
if s.Edge.Inverse {
pk1, pk2 = pk2, pk1
}
toT := build.Table(s.To.Table).Schema(s.To.Schema)
joinT := build.Table(s.Edge.Table).Schema(s.Edge.Schema)
join = build.Select(pk2).
From(toT).
Join(joinT).
On(toT.C(s.To.Column), joinT.C(pk1)).
GroupBy(pk2)
selectTerms(join, opts.Terms)
q.LeftJoin(join).
On(q.C(s.From.Column), join.C(pk2))
case r == O2M || (r == O2O && !s.Edge.Inverse):
toT := build.Table(s.Edge.Table).Schema(s.Edge.Schema)
join = build.Select(toT.C(s.Edge.Columns[0])).
From(toT).
GroupBy(toT.C(s.Edge.Columns[0]))
selectTerms(join, opts.Terms)
q.LeftJoin(join).
On(q.C(s.From.Column), join.C(s.Edge.Columns[0]))
}
orderTerms(q, join, opts.Terms)
}
type (
// FieldSpec holds the information for updating a field
// column in the database.

View File

@@ -911,14 +911,14 @@ func TestHasNeighborsWithContext(t *testing.T) {
}
}
func TestOrderByCountNeighbors(t *testing.T) {
func TestOrderByNeighborsCount(t *testing.T) {
build := sql.Dialect(dialect.Postgres)
t1 := build.Table("users")
s := build.Select(t1.C("name")).
From(t1)
t.Run("O2M", func(t *testing.T) {
s := s.Clone()
OrderByCountNeighbors(s, &OrderByOptions{
OrderByNeighborsCount(s, &OrderByOptions{
Step: NewStep(
From("users", "id"),
To("pets", "owner_id"),
@@ -928,11 +928,11 @@ func TestOrderByCountNeighbors(t *testing.T) {
})
query, args := s.Query()
require.Empty(t, args)
require.Equal(t, `SELECT "users"."name" FROM "users" LEFT JOIN (SELECT "pets"."owner_id", COUNT(*) AS "count_pets" FROM "pets" GROUP BY "pets"."owner_id") AS "t1" ON "users"."id" = "t1"."owner_id" ORDER BY COALESCE("count_pets", 0) DESC`, query)
require.Equal(t, `SELECT "users"."name" FROM "users" LEFT JOIN (SELECT "pets"."owner_id", COUNT(*) AS "count_pets" FROM "pets" GROUP BY "pets"."owner_id") AS "t1" ON "users"."id" = "t1"."owner_id" ORDER BY "t1"."count_pets" DESC NULLS LAST`, query)
})
t.Run("M2M", func(t *testing.T) {
s := s.Clone()
OrderByCountNeighbors(s, &OrderByOptions{
OrderByNeighborsCount(s, &OrderByOptions{
Step: NewStep(
From("users", "id"),
To("groups", "id"),
@@ -941,12 +941,12 @@ func TestOrderByCountNeighbors(t *testing.T) {
})
query, args := s.Query()
require.Empty(t, args)
require.Equal(t, `SELECT "users"."name" FROM "users" LEFT JOIN (SELECT "user_groups"."user_id", COUNT(*) AS "count_groups" FROM "user_groups" GROUP BY "user_groups"."user_id") AS "t1" ON "users"."id" = "t1"."user_id" ORDER BY COALESCE("count_groups", 0)`, query)
require.Equal(t, `SELECT "users"."name" FROM "users" LEFT JOIN (SELECT "user_groups"."user_id", COUNT(*) AS "count_groups" FROM "user_groups" GROUP BY "user_groups"."user_id") AS "t1" ON "users"."id" = "t1"."user_id" ORDER BY "t1"."count_groups" NULLS FIRST`, query)
})
// Zero or one.
t.Run("M2O", func(t *testing.T) {
s1, s2 := s.Clone(), s.Clone()
OrderByCountNeighbors(s1, &OrderByOptions{
OrderByNeighborsCount(s1, &OrderByOptions{
Step: NewStep(
From("pets", "owner_id"),
To("users", "id"),
@@ -957,7 +957,7 @@ func TestOrderByCountNeighbors(t *testing.T) {
require.Empty(t, args)
require.Equal(t, `SELECT "users"."name" FROM "users" ORDER BY "owner_id" IS NULL`, query)
OrderByCountNeighbors(s2, &OrderByOptions{
OrderByNeighborsCount(s2, &OrderByOptions{
Step: NewStep(
From("pets", "owner_id"),
To("users", "id"),
@@ -971,6 +971,65 @@ func TestOrderByCountNeighbors(t *testing.T) {
})
}
func TestOrderByNeighborTerms(t *testing.T) {
build := sql.Dialect(dialect.Postgres)
t1 := build.Table("users")
s := build.Select(t1.C("name")).
From(t1)
t.Run("M2O", func(t *testing.T) {
s := s.Clone()
OrderByNeighborTerms(s, NewOrderBy(
NewStep(
From("users", "id"),
To("workplace", "id"),
Edge(M2O, true, "users", "workplace_id"),
),
OrderByColumn("name"),
))
query, args := s.Query()
require.Empty(t, args)
require.Equal(t, `SELECT "users"."name" FROM "users" LEFT JOIN (SELECT "workplace"."id", "workplace"."name" FROM "workplace") AS "t1" ON "users"."workplace_id" = "t1"."id" ORDER BY "t1"."name" NULLS FIRST`, query)
})
t.Run("O2M", func(t *testing.T) {
s := s.Clone()
OrderByNeighborTerms(s, NewOrderBy(
NewStep(
From("users", "id"),
To("repos", "id"),
Edge(O2M, false, "repo", "owner_id"),
),
OrderByExpr(
sql.ExprFunc(func(b *sql.Builder) {
b.S("SUM(").Ident("num_stars").S(")")
}),
"total_stars",
),
))
query, args := s.Query()
require.Empty(t, args)
require.Equal(t, `SELECT "users"."name" FROM "users" LEFT JOIN (SELECT "repo"."owner_id", (SUM("num_stars")) AS "total_stars" FROM "repo" GROUP BY "repo"."owner_id") AS "t1" ON "users"."id" = "t1"."owner_id" ORDER BY "t1"."total_stars" NULLS FIRST`, query)
})
t.Run("M2M", func(t *testing.T) {
s := s.Clone()
OrderByNeighborTerms(s, NewOrderBy(
NewStep(
From("users", "id"),
To("group", "id"),
Edge(M2M, false, "user_groups", "user_id", "group_id"),
),
OrderByExpr(
sql.ExprFunc(func(b *sql.Builder) {
b.S("SUM(").Ident("num_users").S(")")
}),
"total_users",
),
))
query, args := s.Query()
require.Empty(t, args)
require.Equal(t, `SELECT "users"."name" FROM "users" LEFT JOIN (SELECT "user_id", (SUM("num_users")) AS "total_users" FROM "group" JOIN "user_groups" AS "t1" ON "group"."id" = "t1"."group_id" GROUP BY "user_id") AS "t1" ON "users"."id" = "t1"."user_id" ORDER BY "t1"."total_users" NULLS FIRST`, query)
})
}
func TestCreateNode(t *testing.T) {
tests := []struct {
name string

View File

@@ -167,6 +167,7 @@ var (
NillableRequired,
ExtValueScan,
OrderByEdgeCount,
OrderByEdgeTerms,
}
)
@@ -2392,7 +2393,7 @@ func OrderByEdgeCount(t *testing.T, client *ent.Client) {
} {
ids := client.User.Query().
Order(func(s *sql.Selector) {
sqlgraph.OrderByCountNeighbors(s, &sqlgraph.OrderByOptions{
sqlgraph.OrderByNeighborsCount(s, &sqlgraph.OrderByOptions{
Desc: tt.desc,
Step: sqlgraph.NewStep(
sqlgraph.From(user.Table, user.FieldID),
@@ -2415,7 +2416,7 @@ func OrderByEdgeCount(t *testing.T, client *ent.Client) {
ids := client.Pet.Query().
Order(
func(s *sql.Selector) {
sqlgraph.OrderByCountNeighbors(s, &sqlgraph.OrderByOptions{
sqlgraph.OrderByNeighborsCount(s, &sqlgraph.OrderByOptions{
Desc: tt.desc,
Step: sqlgraph.NewStep(
sqlgraph.From(pet.Table, pet.OwnerColumn),
@@ -2447,7 +2448,7 @@ func OrderByEdgeCount(t *testing.T, client *ent.Client) {
} {
ids := client.Group.Query().
Order(func(s *sql.Selector) {
sqlgraph.OrderByCountNeighbors(s, &sqlgraph.OrderByOptions{
sqlgraph.OrderByNeighborsCount(s, &sqlgraph.OrderByOptions{
Desc: tt.desc,
Step: sqlgraph.NewStep(
sqlgraph.From(group.Table, group.FieldID),
@@ -2469,7 +2470,7 @@ func OrderByEdgeCount(t *testing.T, client *ent.Client) {
} {
ids := client.User.Query().
Order(func(s *sql.Selector) {
sqlgraph.OrderByCountNeighbors(s, &sqlgraph.OrderByOptions{
sqlgraph.OrderByNeighborsCount(s, &sqlgraph.OrderByOptions{
Desc: tt.desc,
Step: sqlgraph.NewStep(
sqlgraph.From(user.Table, user.FieldID),
@@ -2483,6 +2484,135 @@ func OrderByEdgeCount(t *testing.T, client *ent.Client) {
}
}
// Testing the "low-level" behavior of the sqlgraph package.
// This functionality may be extended to the generated fluent API.
func OrderByEdgeTerms(t *testing.T, client *ent.Client) {
ctx := context.Background()
users := client.User.CreateBulk(
client.User.Create().SetName("a").SetAge(1),
client.User.Create().SetName("b").SetAge(2),
client.User.Create().SetName("c").SetAge(3),
client.User.Create().SetName("d").SetAge(4),
).SaveX(ctx)
pets := client.Pet.CreateBulk(
client.Pet.Create().SetName("aa").SetAge(2).SetOwner(users[1]),
client.Pet.Create().SetName("ab").SetAge(2).SetOwner(users[1]),
client.Pet.Create().SetName("ac").SetAge(1).SetOwner(users[0]),
client.Pet.Create().SetName("ba").SetAge(1).SetOwner(users[0]),
client.Pet.Create().SetName("bb").SetAge(1).SetOwner(users[0]),
client.Pet.Create().SetName("ca").SetAge(3).SetOwner(users[2]),
client.Pet.Create().SetName("d"),
client.Pet.Create().SetName("e"),
).SaveX(ctx)
// M2O edge (inverse).
// Order pets by their owner's name.
for _, tt := range []struct {
opt sqlgraph.OrderByOption
ids []int
}{
{
opt: sqlgraph.OrderByColumn(user.FieldName),
ids: []int{pets[6].ID, pets[7].ID, pets[2].ID, pets[3].ID, pets[4].ID, pets[0].ID, pets[1].ID, pets[5].ID},
},
{
opt: sqlgraph.OrderByColumnDesc(user.FieldName),
ids: []int{pets[5].ID, pets[0].ID, pets[1].ID, pets[2].ID, pets[3].ID, pets[4].ID, pets[6].ID, pets[7].ID},
},
} {
ids := client.Pet.Query().
Order(func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, sqlgraph.NewOrderBy(
sqlgraph.NewStep(
sqlgraph.From(pet.Table, pet.FieldID),
sqlgraph.To(user.Table, user.FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, pet.Table, pet.OwnerColumn),
),
tt.opt,
))
}).
Order(ent.Asc(pet.FieldID)).
IDsX(ctx)
require.Equal(t, tt.ids, ids)
}
// O2M edge (aggregation).
for _, tt := range []struct {
opt sqlgraph.OrderByOption
ids []int
}{
{
opt: sqlgraph.OrderByExpr(sql.Expr("SUM(age)"), "sum_age"),
ids: []int{users[3].ID, users[0].ID, users[2].ID, users[1].ID},
},
{
opt: sqlgraph.OrderByExprDesc(sql.Expr("SUM(age)"), "sum_age"),
ids: []int{users[1].ID, users[0].ID, users[2].ID, users[3].ID},
},
} {
ids := client.User.Query().
Order(func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, sqlgraph.NewOrderBy(
sqlgraph.NewStep(
sqlgraph.From(user.Table, user.FieldID),
sqlgraph.To(pet.Table, pet.FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, pet.Table, pet.OwnerColumn),
),
tt.opt,
))
}).
Order(ent.Asc(user.FieldID)).
IDsX(ctx)
require.Equal(t, tt.ids, ids)
}
inf, exp := client.GroupInfo.Create().SetDesc("desc").SaveX(ctx), time.Now()
client.Group.CreateBulk(
client.Group.Create().SetName("Group: 4 users").SetExpire(exp).SetMaxUsers(40).SetInfo(inf).AddUsers(users...),
client.Group.Create().SetName("Group: 3 users").SetExpire(exp).SetMaxUsers(20).SetInfo(inf).AddUsers(users[:3]...),
client.Group.Create().SetName("Group: 2 users").SetExpire(exp).SetMaxUsers(20).SetInfo(inf).AddUsers(users[:2]...),
client.Group.Create().SetName("Group: 1 users").SetExpire(exp).SetMaxUsers(100).SetInfo(inf).AddUsers(users[:1]...),
client.Group.Create().SetName("Group: 0 users").SetExpire(exp).SetInfo(inf),
).ExecX(ctx)
// M2M edge.
// O2M edge (aggregation).
for _, tt := range []struct {
opt sqlgraph.OrderByOption
ids []int
}{
{
opt: sqlgraph.OrderByExpr(
sql.ExprFunc(func(b *sql.Builder) {
b.S("SUM(").Ident(group.FieldMaxUsers).S(")")
}),
"sum_max_users",
),
ids: []int{users[3].ID, users[2].ID, users[1].ID, users[0].ID},
},
{
opt: sqlgraph.OrderByExprDesc(
sql.ExprFunc(func(b *sql.Builder) {
b.S("SUM(").Ident(group.FieldMaxUsers).S(")")
}),
"sum_max_users",
),
ids: []int{users[0].ID, users[1].ID, users[2].ID, users[3].ID},
},
} {
ids := client.User.Query().
Order(func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, sqlgraph.NewOrderBy(
sqlgraph.NewStep(
sqlgraph.From(user.Table, user.FieldID),
sqlgraph.To(group.Table, group.FieldID),
sqlgraph.Edge(sqlgraph.M2M, false, user.GroupsTable, user.GroupsPrimaryKey...),
),
tt.opt,
))
}).
IDsX(ctx)
require.Equal(t, tt.ids, ids)
}
}
func skip(t *testing.T, names ...string) {
for _, n := range names {
if strings.Contains(t.Name(), n) {