Files
zitadel/internal/query/user_login_name_exists_test.go
T
318acd4bc3 perf(query): speed up ListUsers login name equality filters (#12460)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary

`ListUsers` with `LoginNameQuery` + equals/equals-ignore-case was very
slow on large orgs (e.g. ~330k users) because the generated SQL filtered
the `projections.login_names3` **view** on the computed
`login_name_lower` expression and correlated that subquery on
`users14.id`. Postgres nested-looped every user and never used
`login_names3_users_search (instance_id, user_name_lower)`.

This change rewrites the **query planner** for that hot path: when an
equals/equals-ignore-case login-name filter is present (and not under
`OR`/`NOT`), the user list query **INNER JOINs** an indexed matches
subquery instead of filtering via the view expression. The matches SQL
mirrors `user_by_login_name.sql` (`user_name_lower` / domain paths +
`preferred` / `is_primary`).

Non-equals methods and OR combinations keep the previous view-based
filter so semantics stay unchanged.

Also adds a k6 use case that mirrors login v2 discovery
(`loginNameQuery` EQUALS_IGNORE_CASE + `organizationIdQuery`, `limit:
2`):

```bash
cd benchmark
make users_by_login_name USER_AMOUNT=100000 VUS=10 DURATION=60s
```

## Approach

1. `NewLoginNameSearchQuery` for equals / equals-ignore-case returns a
marker `loginNameEqualsFilter` (other methods unchanged).
2. `prepareUsersQuery` extracts that marker when safe, then:
- builds the usual `sq.SelectBuilder` **without** the login-name view
predicate
- adds `JoinClause` to `user_login_name_matches(.sql)` /
`_case_sensitive.sql` as `login_name_matches`
- keeps metadata JOIN/`DISTINCT` only when metadata filters are present
(same as before)
3. Embedded SQL files under `internal/query/` for the matches subquery.

Local smoke against ~330k synthetic users: baseline ~1392ms → rewritten
path ~0.4ms for a single equals-ignore-case lookup.

## Test plan

- [x] `go test ./internal/query/ -run
'TestLoginName|TestUsers|TestUserByLoginName'` (after generate-stubs)
- [x] Existing `user_test` expected SQL updated (no always-on metadata
join; login-name equals uses JOIN)
- [ ] Run k6 before/after on a large `USER_AMOUNT` (e.g. 50k–100k+) and
compare `list_users_duration` p50/p95/p99
- [x] Manual login v2 username discovery against a large org
- [x] Confirm OR / NOT / CONTAINS login-name queries still return
expected results

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-252883bd-48d9-492e-b619-5ccfa93cf9c3"><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-252883bd-48d9-492e-b619-5ccfa93cf9c3"><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 <cursoragent@cursor.com>
Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
2026-07-28 11:56:07 +00:00

176 lines
5.8 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("User.Name@Org.Localhost", 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, "user.name@org.localhost", ln.loginName)
assert.True(t, ln.ignoreCase)
}
func TestNewUserLoginNameExistsQuery_EqualsIsMarker(t *testing.T) {
qry, err := NewUserLoginNameExistsQuery("User.Name@Org.Localhost", 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, "User.Name@Org.Localhost", 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 TestExtractLoginNameEqualsFilter_TopLevel(t *testing.T) {
loginNameQuery, err := NewUserLoginNameExistsQuery("user@org.localhost", TextEqualsIgnoreCase)
require.NoError(t, err)
orgQuery, err := NewUserResourceOwnerSearchQuery("org1", TextEquals)
require.NoError(t, err)
filter, remaining, ok := extractLoginNameEqualsFilter([]SearchQuery{loginNameQuery, orgQuery})
require.True(t, ok)
require.NotNil(t, filter)
assert.Equal(t, "user", filter.username)
require.Len(t, remaining, 1)
assert.Equal(t, orgQuery, remaining[0])
}
func TestExtractLoginNameEqualsFilter_SkipsOrQuery(t *testing.T) {
loginNameQuery, err := NewUserLoginNameExistsQuery("user@org.localhost", TextEqualsIgnoreCase)
require.NoError(t, err)
emailQuery, err := NewUserEmailSearchQuery("user@example.com", TextEqualsIgnoreCase)
require.NoError(t, err)
orQuery, err := NewOrQuery(loginNameQuery, emailQuery)
require.NoError(t, err)
_, remaining, ok := extractLoginNameEqualsFilter([]SearchQuery{orQuery})
assert.False(t, ok)
require.Len(t, remaining, 1)
assert.Equal(t, orQuery, remaining[0])
}
func TestPrepareUsersQuery_LoginNameEqualsUsesIndexedJoin(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
loginNameQuery, err := NewUserLoginNameExistsQuery("user165000@org.localhost", 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("User165000@Org.Localhost", 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("user@org.localhost", 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_LoginNameOrEmailDoesNotRewrite(t *testing.T) {
ctx := authz.WithInstanceID(t.Context(), "inst-1")
loginNameQuery, err := NewUserLoginNameExistsQuery("user@org.localhost", TextEqualsIgnoreCase)
require.NoError(t, err)
emailQuery, err := NewUserEmailSearchQuery("user@example.com", 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, _, err := builder.ToSql()
require.NoError(t, err)
assert.NotContains(t, sql, "login_name_matches")
assert.Contains(t, sql, "projections.login_names3")
}
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")
}