Files
zitadel/internal/query/user_login_name_exists_test.go
T
684de2e691 perf(query): use UNION of index seeks for ListUsers login equality (#12701)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
# Which Problems Are Solved

Login V2 `ListUsers` lookups of the form username OR email (and email OR
phone) hash-join the full `users14` / `users14_humans` tables, then
filter. That cost grows with instance size (~40ms at 10k users, ~650ms
at 200k).

Top-level login-name equals already used an indexed join, but the same
login-name filter inside an `OrQuery` fell back to the slow view-based
exists query.

# How the Problems Are Solved

**TL;DR:** equality lookups on username, email, phone, or login name
become `INNER JOIN (UNION of indexed ID seeks) AS matches`. Mixed /
contains / name searches keep the old WHERE shape.

### Reviewer map

| Read first | Why |
| --- | --- |
| `internal/query/user_login_equality.go` | Typed filters + extractor +
UNION join |
| `internal/query/user.go` (`prepareUsersQuery`, constructors) | Wiring;
username/email/phone equals return `lowerEqualsFilter` |
| `cmd/setup/76.go` + `cmd/setup/76/*.sql` | Concurrent indexes for
existing deployments; drop invalid leftovers |
| `internal/query/projection/user.go` | Same indexes in `Init()` for new
databases |
| `internal/query/user_login_name_exists_test.go` | SQL-shape coverage |
| `internal/api/grpc/user/v2/integration_test/query_test.go` | Nested
username AND/OR user results |

### Query rewrite

- Extract one equals leaf, a flat `OrQuery` of those leaves, or the same
nested under `AndQuery`. Remaining AND conjuncts stay as WHERE. Equality
nested under `OrQuery` / `NotQuery` is left as WHERE.
- Login-name equals is a UNION arm that reuses `login_names3_users` SQL,
so it works inside OR as well as at the top level.
- EQUALS seeks use `LOWER(col)` for the index, then recheck `col =
value` so case-sensitive searches do not match extra rows.
EQUALS_IGNORE_CASE is unchanged.
- Empty OR arms are not skipped (that would change OR meaning); mixed
empty-OR falls back to WHERE. Login UI already omits empty arms.

### Indexes

- Setup 76: `CREATE INDEX CONCURRENTLY IF NOT EXISTS` on
`users14_username_lower_idx` `(instance_id, LOWER(username))` and
`users14_humans_phone_lower_idx` `(instance_id, LOWER(phone))`. Not
wrapped in a transaction.
- Before create, drop any leftover `pg_index.indisvalid = false` indexes
of those names (failed/cancelled `CREATE INDEX CONCURRENTLY`). `IF NOT
EXISTS` would otherwise skip rebuild.
- `Init()` creates the same indexes without `CONCURRENTLY` (empty
tables). Existing `users14_username` and email lower indexes are
unchanged.

# Additional Changes

- Unit tests assert UNION SQL for username OR email, email OR phone,
login name OR email, username OR username, nested `AndQuery(loginName,
org)`, and nested `AndQuery(Or(...), org)`; that contains / display-name
/ mixed OR / nested `OrQuery(AndQuery(...), ...)` do not UNION; that
email EQUALS rechecks the raw column; and that empty-OR is not
extracted.
- Setup 76 sqlmock coverage: missing `users14` skips work; valid/absent
indexes only create; invalid leftover username index is dropped then
both creates run.
- `TestServer_ListUsers_UsernameAndOr` checks live results: org AND
(alice OR bob) returns both users, sibling username AND is empty, nested
`AndQuery(Or(...), org)` returns both.

# Additional Context

- Base branch: `main`
- Extends the login-name equals rewrite from #12460 to
username/email/phone OR trees used by Login V2 search.

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-1f043b1c-6168-45ca-9e9e-c18451994207?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-1f043b1c-6168-45ca-9e9e-c18451994207&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Silvan <[email protected]>
2026-09-08 13:11:16 +00:00

478 lines
16 KiB
Go

package query
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/api/authz"
)
func TestNewUserLoginNameExistsQuery_EqualsIgnoreCaseIsMarker(t *testing.T) {
qry, err := NewUserLoginNameExistsQuery("[email protected]", TextEqualsIgnoreCase)
require.NoError(t, err)
ln, ok := qry.(*loginNameEqualsFilter)
require.True(t, ok)
assert.Equal(t, "user.name", ln.username)
assert.Equal(t, "org.localhost", ln.domain)
assert.Equal(t, "[email protected]", ln.loginName)
assert.True(t, ln.ignoreCase)
}
func TestNewUserLoginNameExistsQuery_EqualsIsMarker(t *testing.T) {
qry, err := NewUserLoginNameExistsQuery("[email protected]", TextEquals)
require.NoError(t, err)
ln, ok := qry.(*loginNameEqualsFilter)
require.True(t, ok)
assert.Equal(t, "User.Name", ln.username)
assert.Equal(t, "Org.Localhost", ln.domain)
assert.Equal(t, "[email protected]", ln.loginName)
assert.False(t, ln.ignoreCase)
}
func TestNewUserLoginNameExistsQuery_ContainsFallsBackToView(t *testing.T) {
qry, err := NewUserLoginNameExistsQuery("user", TextContains)
require.NoError(t, err)
_, ok := qry.(*loginNameEqualsFilter)
assert.False(t, ok)
sql, _, err := qry.comp().ToSql()
require.NoError(t, err)
assert.Contains(t, sql, "projections.login_names3")
assert.Contains(t, strings.ToLower(sql), "login_name")
}
func TestExtractLoginEqualitySeeks_LoginNameAndOrg(t *testing.T) {
loginNameQuery, err := NewUserLoginNameExistsQuery("[email protected]", TextEqualsIgnoreCase)
require.NoError(t, err)
orgQuery, err := NewUserResourceOwnerSearchQuery("org1", TextEquals)
require.NoError(t, err)
seeks, remaining, ok := extractLoginEqualitySeeks("inst-1", []SearchQuery{loginNameQuery, orgQuery})
require.True(t, ok)
require.Len(t, seeks, 1)
assert.Contains(t, seeks[0].sql, "login_name_matches")
require.Len(t, remaining, 1)
assert.Equal(t, orgQuery, remaining[0])
}
func TestNewUserEmailSearchQuery_EqualsIsLowerEqualsFilter(t *testing.T) {
qry, err := NewUserEmailSearchQuery("[email protected]", TextEquals)
require.NoError(t, err)
_, ok := qry.(*lowerEqualsFilter)
require.True(t, ok)
contains, err := NewUserEmailSearchQuery("Ada", TextContains)
require.NoError(t, err)
_, ok = contains.(*lowerEqualsFilter)
assert.False(t, ok)
}
func TestExtractLoginEqualitySeeks_OrLoginNameAndEmail(t *testing.T) {
loginNameQuery, err := NewUserLoginNameExistsQuery("[email protected]", TextEqualsIgnoreCase)
require.NoError(t, err)
emailQuery, err := NewUserEmailSearchQuery("[email protected]", TextEqualsIgnoreCase)
require.NoError(t, err)
orQuery, err := NewOrQuery(loginNameQuery, emailQuery)
require.NoError(t, err)
seeks, remaining, ok := extractLoginEqualitySeeks("inst-1", []SearchQuery{orQuery})
require.True(t, ok)
require.Len(t, seeks, 2)
assert.Contains(t, seeks[0].sql, "login_name_matches")
assert.Contains(t, seeks[1].sql, "LOWER("+HumanEmailCol.identifier()+")")
assert.Empty(t, remaining)
}
func TestExtractLoginEqualitySeeks_EmptyOrArmDoesNotExtract(t *testing.T) {
emailQuery, err := NewUserEmailSearchQuery("", TextEqualsIgnoreCase)
require.NoError(t, err)
phoneQuery, err := NewUserPhoneSearchQuery("+41000000000", TextEquals)
require.NoError(t, err)
orQuery, err := NewOrQuery(emailQuery, phoneQuery)
require.NoError(t, err)
seeks, remaining, ok := extractLoginEqualitySeeks("inst-1", []SearchQuery{orQuery})
assert.False(t, ok)
assert.Nil(t, seeks)
require.Len(t, remaining, 1)
assert.Equal(t, orQuery, remaining[0])
}
func TestExtractLoginEqualitySeeks_NestedAndExtractsLoginName(t *testing.T) {
loginNameQuery, err := NewUserLoginNameExistsQuery("[email protected]", TextEqualsIgnoreCase)
require.NoError(t, err)
orgQuery, err := NewUserResourceOwnerSearchQuery("org1", TextEquals)
require.NoError(t, err)
andQuery, err := NewAndQuery(loginNameQuery, orgQuery)
require.NoError(t, err)
seeks, remaining, ok := extractLoginEqualitySeeks("inst-1", []SearchQuery{andQuery})
require.True(t, ok)
require.Len(t, seeks, 1)
assert.Contains(t, seeks[0].sql, "login_name_matches")
require.Len(t, remaining, 1)
assert.Equal(t, orgQuery, remaining[0])
}
func TestPrepareUsersQuery_LoginNameEqualsUsesIndexedJoin(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
loginNameQuery, err := NewUserLoginNameExistsQuery("[email protected]", TextEqualsIgnoreCase)
require.NoError(t, err)
q := &UserSearchQueries{
Queries: []SearchQuery{loginNameQuery},
}
builder, _ := q.prepareUsersQuery(ctx, false)
sql, args, err := builder.ToSql()
require.NoError(t, err)
assert.Contains(t, sql, "INNER JOIN")
assert.Contains(t, sql, "login_name_matches")
assert.Contains(t, sql, "login_names3_users")
assert.Contains(t, sql, "user_name_lower")
assert.NotContains(t, sql, "login_name_lower")
assert.NotContains(t, sql, "user_metadata5")
assert.NotContains(t, sql, "SELECT DISTINCT")
assert.Contains(t, args, "inst-1")
assert.Contains(t, args, "user165000")
assert.Contains(t, args, "org.localhost")
}
func TestPrepareUsersQuery_LoginNameEqualsCaseSensitive(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
loginNameQuery, err := NewUserLoginNameExistsQuery("[email protected]", TextEquals)
require.NoError(t, err)
q := &UserSearchQueries{
Queries: []SearchQuery{loginNameQuery},
}
builder, _ := q.prepareUsersQuery(ctx, false)
sql, args, err := builder.ToSql()
require.NoError(t, err)
assert.Contains(t, sql, "login_name_matches")
assert.Contains(t, sql, "u.user_name IN")
assert.NotContains(t, sql, "user_name_lower")
assert.Contains(t, args, "User165000")
assert.Contains(t, args, "Org.Localhost")
}
func TestPrepareUsersQuery_LoginNameEqualsWithOrgFilter(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
loginNameQuery, err := NewUserLoginNameExistsQuery("[email protected]", TextEqualsIgnoreCase)
require.NoError(t, err)
orgQuery, err := NewUserResourceOwnerSearchQuery("org1", TextEquals)
require.NoError(t, err)
q := &UserSearchQueries{
Queries: []SearchQuery{loginNameQuery, orgQuery},
}
builder, _ := q.prepareUsersQuery(ctx, false)
sql, args, err := builder.ToSql()
require.NoError(t, err)
assert.Contains(t, sql, "login_name_matches")
assert.Contains(t, sql, "resource_owner")
assert.Contains(t, args, "org1")
assert.Contains(t, args, "user")
}
func TestPrepareUsersQuery_LoginNameOrEmailUsesUnion(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
loginNameQuery, err := NewUserLoginNameExistsQuery("[email protected]", TextEqualsIgnoreCase)
require.NoError(t, err)
emailQuery, err := NewUserEmailSearchQuery("[email protected]", TextEqualsIgnoreCase)
require.NoError(t, err)
orQuery, err := NewOrQuery(loginNameQuery, emailQuery)
require.NoError(t, err)
q := &UserSearchQueries{
Queries: []SearchQuery{orQuery},
}
builder, _ := q.prepareUsersQuery(ctx, false)
sql, args, err := builder.ToSql()
require.NoError(t, err)
assert.Contains(t, sql, "UNION")
assert.Contains(t, sql, "AS matches")
assert.Contains(t, sql, "login_name_matches")
assert.Contains(t, sql, "login_names3_users")
assert.Contains(t, sql, "LOWER("+HumanEmailCol.identifier()+")")
assert.NotContains(t, sql, "login_name_lower")
assert.Contains(t, args, "[email protected]")
}
func TestPrepareUsersQuery_UsernameOrEmailUsesUnion(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
usernameQuery, err := NewUserUsernameSearchQuery("user165000", TextEqualsIgnoreCase)
require.NoError(t, err)
emailQuery, err := NewUserEmailSearchQuery("[email protected]", TextEqualsIgnoreCase)
require.NoError(t, err)
orQuery, err := NewOrQuery(usernameQuery, emailQuery)
require.NoError(t, err)
q := &UserSearchQueries{
Queries: []SearchQuery{orQuery},
}
builder, _ := q.prepareUsersQuery(ctx, false)
sql, args, err := builder.ToSql()
require.NoError(t, err)
assert.Contains(t, sql, "UNION")
assert.Contains(t, sql, "AS matches")
assert.Contains(t, sql, "LOWER("+UserUsernameCol.identifier()+")")
assert.Contains(t, sql, "LOWER("+HumanEmailCol.identifier()+")")
assert.NotContains(t, sql, "login_name_matches")
assert.Contains(t, args, "user165000")
assert.Contains(t, args, "[email protected]")
}
func TestPrepareUsersQuery_EmailOrPhoneUsesUnion(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
emailQuery, err := NewUserEmailSearchQuery("[email protected]", TextEqualsIgnoreCase)
require.NoError(t, err)
phoneQuery, err := NewUserPhoneSearchQuery("+41000000000", TextEquals)
require.NoError(t, err)
orQuery, err := NewOrQuery(emailQuery, phoneQuery)
require.NoError(t, err)
q := &UserSearchQueries{
Queries: []SearchQuery{orQuery},
}
builder, _ := q.prepareUsersQuery(ctx, false)
sql, args, err := builder.ToSql()
require.NoError(t, err)
assert.Contains(t, sql, "UNION")
assert.Contains(t, sql, "LOWER("+HumanEmailCol.identifier()+")")
assert.Contains(t, sql, "LOWER("+HumanPhoneCol.identifier()+")")
assert.Contains(t, args, "[email protected]")
assert.Contains(t, args, "+41000000000")
}
func TestPrepareUsersQuery_EmailEqualsRechecksExactColumn(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
emailQuery, err := NewUserEmailSearchQuery("[email protected]", TextEquals)
require.NoError(t, err)
q := &UserSearchQueries{
Queries: []SearchQuery{emailQuery},
}
builder, _ := q.prepareUsersQuery(ctx, false)
sql, args, err := builder.ToSql()
require.NoError(t, err)
assert.Contains(t, sql, "AS matches")
assert.Contains(t, sql, "LOWER("+HumanEmailCol.identifier()+")")
assert.Contains(t, sql, "LOWER($")
assert.Contains(t, sql, HumanEmailCol.identifier()+" =")
assert.Contains(t, args, "[email protected]")
assert.NotContains(t, args, "[email protected]")
}
func TestPrepareUsersQuery_EmailIgnoreCaseDoesNotRecheckExactColumn(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
emailQuery, err := NewUserEmailSearchQuery("[email protected]", TextEqualsIgnoreCase)
require.NoError(t, err)
q := &UserSearchQueries{
Queries: []SearchQuery{emailQuery},
}
builder, _ := q.prepareUsersQuery(ctx, false)
sql, args, err := builder.ToSql()
require.NoError(t, err)
assert.Contains(t, sql, "AS matches")
assert.Contains(t, sql, "LOWER("+HumanEmailCol.identifier()+")")
assert.NotContains(t, sql, "LOWER($")
assert.NotContains(t, sql, HumanEmailCol.identifier()+" =")
assert.Contains(t, args, "[email protected]")
assert.NotContains(t, args, "[email protected]")
}
func TestPrepareUsersQuery_PhoneIgnoreCaseUsesUnion(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
phoneQuery, err := NewUserPhoneSearchQuery("+41000000000", TextEqualsIgnoreCase)
require.NoError(t, err)
q := &UserSearchQueries{
Queries: []SearchQuery{phoneQuery},
}
builder, _ := q.prepareUsersQuery(ctx, false)
sql, args, err := builder.ToSql()
require.NoError(t, err)
assert.Contains(t, sql, "AS matches")
assert.Contains(t, sql, "LOWER("+HumanPhoneCol.identifier()+")")
assert.Contains(t, args, "+41000000000")
}
func TestPrepareUsersQuery_ContainsDoesNotUseUnion(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
usernameQuery, err := NewUserUsernameSearchQuery("user", TextContainsIgnoreCase)
require.NoError(t, err)
q := &UserSearchQueries{
Queries: []SearchQuery{usernameQuery},
}
builder, _ := q.prepareUsersQuery(ctx, false)
sql, _, err := builder.ToSql()
require.NoError(t, err)
assert.NotContains(t, sql, "AS matches")
assert.NotContains(t, sql, " UNION ")
}
func TestPrepareUsersQuery_DisplayNameDoesNotUseUnion(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
displayNameQuery, err := NewUserDisplayNameSearchQuery("Ada Lovelace", TextEqualsIgnoreCase)
require.NoError(t, err)
q := &UserSearchQueries{
Queries: []SearchQuery{displayNameQuery},
}
builder, _ := q.prepareUsersQuery(ctx, false)
sql, _, err := builder.ToSql()
require.NoError(t, err)
assert.NotContains(t, sql, "AS matches")
assert.NotContains(t, sql, " UNION ")
}
func TestPrepareUsersQuery_UsernameOrContainsDoesNotUseUnion(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
usernameQuery, err := NewUserUsernameSearchQuery("user165000", TextEqualsIgnoreCase)
require.NoError(t, err)
displayNameQuery, err := NewUserDisplayNameSearchQuery("user", TextContainsIgnoreCase)
require.NoError(t, err)
orQuery, err := NewOrQuery(usernameQuery, displayNameQuery)
require.NoError(t, err)
q := &UserSearchQueries{
Queries: []SearchQuery{orQuery},
}
builder, _ := q.prepareUsersQuery(ctx, false)
sql, _, err := builder.ToSql()
require.NoError(t, err)
assert.NotContains(t, sql, "AS matches")
assert.NotContains(t, sql, " UNION ")
}
func TestPrepareUsersQuery_UsernameOrUsernameUsesUnion(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
alice, err := NewUserUsernameSearchQuery("alice", TextEqualsIgnoreCase)
require.NoError(t, err)
bob, err := NewUserUsernameSearchQuery("bob", TextEqualsIgnoreCase)
require.NoError(t, err)
orQuery, err := NewOrQuery(alice, bob)
require.NoError(t, err)
q := &UserSearchQueries{
Queries: []SearchQuery{orQuery},
}
builder, _ := q.prepareUsersQuery(ctx, false)
sql, args, err := builder.ToSql()
require.NoError(t, err)
assert.Contains(t, sql, "UNION")
assert.Contains(t, sql, "AS matches")
assert.Equal(t, 2, strings.Count(sql, "LOWER("+UserUsernameCol.identifier()+")"))
assert.Contains(t, args, "alice")
assert.Contains(t, args, "bob")
}
func TestPrepareUsersQuery_SiblingUsernameAndKeepsRemainingWhere(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
alice, err := NewUserUsernameSearchQuery("alice", TextEqualsIgnoreCase)
require.NoError(t, err)
bob, err := NewUserUsernameSearchQuery("bob", TextEqualsIgnoreCase)
require.NoError(t, err)
q := &UserSearchQueries{
Queries: []SearchQuery{alice, bob},
}
builder, _ := q.prepareUsersQuery(ctx, false)
sql, args, err := builder.ToSql()
require.NoError(t, err)
assert.Contains(t, sql, "AS matches")
assert.NotContains(t, sql, " UNION ")
assert.Contains(t, args, "alice")
assert.Contains(t, args, "bob")
}
func TestPrepareUsersQuery_NestedAndOfOrUsesUnion(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
alice, err := NewUserUsernameSearchQuery("alice", TextEqualsIgnoreCase)
require.NoError(t, err)
bob, err := NewUserUsernameSearchQuery("bob", TextEqualsIgnoreCase)
require.NoError(t, err)
orQuery, err := NewOrQuery(alice, bob)
require.NoError(t, err)
orgQuery, err := NewUserResourceOwnerSearchQuery("org1", TextEquals)
require.NoError(t, err)
andQuery, err := NewAndQuery(orQuery, orgQuery)
require.NoError(t, err)
q := &UserSearchQueries{
Queries: []SearchQuery{andQuery},
}
builder, _ := q.prepareUsersQuery(ctx, false)
sql, args, err := builder.ToSql()
require.NoError(t, err)
assert.Contains(t, sql, "AS matches")
assert.Contains(t, sql, " UNION ")
assert.Contains(t, sql, "resource_owner")
assert.Contains(t, args, "org1")
assert.Contains(t, args, "alice")
assert.Contains(t, args, "bob")
}
func TestPrepareUsersQuery_NestedOrOfAndDoesNotUseUnion(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
alice, err := NewUserUsernameSearchQuery("alice", TextEqualsIgnoreCase)
require.NoError(t, err)
orgQuery, err := NewUserResourceOwnerSearchQuery("org1", TextEquals)
require.NoError(t, err)
andQuery, err := NewAndQuery(alice, orgQuery)
require.NoError(t, err)
bob, err := NewUserUsernameSearchQuery("bob", TextEqualsIgnoreCase)
require.NoError(t, err)
orQuery, err := NewOrQuery(andQuery, bob)
require.NoError(t, err)
q := &UserSearchQueries{
Queries: []SearchQuery{orQuery},
}
builder, _ := q.prepareUsersQuery(ctx, false)
sql, _, err := builder.ToSql()
require.NoError(t, err)
assert.NotContains(t, sql, "AS matches")
assert.NotContains(t, sql, " UNION ")
}
func TestPrepareUsersQuery_MetadataFilterKeepsDistinctJoin(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
metadataQuery, err := NewUserMetadataKeySearchQuery("key", TextContains)
require.NoError(t, err)
q := &UserSearchQueries{
Queries: []SearchQuery{metadataQuery},
}
builder, _ := q.prepareUsersQuery(ctx, false)
sql, _, err := builder.ToSql()
require.NoError(t, err)
assert.Contains(t, sql, "SELECT DISTINCT")
assert.Contains(t, sql, "user_metadata5")
}