Files
zitadel/internal/query/projection/login_name.go
Silvan 8ac4b61ee6 perf(query): reduce user query duration (#10037)
# Which Problems Are Solved

The resource usage to query user(s) on the database was high and
therefore could have performance impact.

# How the Problems Are Solved

Database queries involving the users and loginnames table were improved
and an index was added for user by email query.

# Additional Changes

- spellchecks
- updated apis on load tests

# additional info

needs cherry pick to v3

(cherry picked from commit 4df138286b)
2025-06-12 07:06:45 +02:00

516 lines
18 KiB
Go

package projection
import (
"context"
_ "embed"
"github.com/zitadel/zitadel/internal/eventstore"
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/policy"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
// if the table name of the users or domains table is changed please update setup step 18
LoginNameTableAlias = "login_names3"
LoginNameProjectionTable = "projections." + LoginNameTableAlias
LoginNameUserProjectionTable = LoginNameProjectionTable + "_" + loginNameUserSuffix
LoginNamePolicyProjectionTable = LoginNameProjectionTable + "_" + loginNamePolicySuffix
LoginNameDomainProjectionTable = LoginNameProjectionTable + "_" + loginNameDomainSuffix
LoginNameCol = "login_name"
LoginNameUserCol = "user_id"
LoginNameIsPrimaryCol = "is_primary"
LoginNameInstanceIDCol = "instance_id"
usersAlias = "users"
policyCustomAlias = "policy_custom"
policyDefaultAlias = "policy_default"
policyUsersAlias = "policy_users"
domainsAlias = "domains"
domainAlias = "domain"
loginNameUserSuffix = "users"
LoginNameUserIDCol = "id"
LoginNameUserUserNameCol = "user_name"
// internal fields for faster search
loginNameUserUserNameLowerCol = "user_name_lower"
LoginNameUserResourceOwnerCol = "resource_owner"
LoginNameUserInstanceIDCol = "instance_id"
loginNameDomainSuffix = "domains"
LoginNameDomainNameCol = "name"
loginNameDomainNameLowerCol = "name_lower"
LoginNameDomainIsPrimaryCol = "is_primary"
LoginNameDomainResourceOwnerCol = "resource_owner"
LoginNameDomainInstanceIDCol = "instance_id"
loginNamePolicySuffix = "policies"
LoginNamePoliciesMustBeDomainCol = "must_be_domain"
LoginNamePoliciesIsDefaultCol = "is_default"
LoginNamePoliciesResourceOwnerCol = "resource_owner"
LoginNamePoliciesInstanceIDCol = "instance_id"
)
//go:embed login_name_query.sql
var loginNameViewStmt string
type loginNameProjection struct{}
func newLoginNameProjection(ctx context.Context, config handler.Config) *handler.Handler {
return handler.NewHandler(ctx, &config, new(loginNameProjection))
}
func (*loginNameProjection) Name() string {
return LoginNameProjectionTable
}
func (*loginNameProjection) Init() *old_handler.Check {
return handler.NewViewCheck(
loginNameViewStmt,
handler.NewSuffixedTable(
[]*handler.InitColumn{
handler.NewColumn(LoginNameUserIDCol, handler.ColumnTypeText),
handler.NewColumn(LoginNameUserUserNameCol, handler.ColumnTypeText),
// TODO: implement computed columns
// handler.NewComputedColumn(loginNameUserUserNameLowerCol, handler.ColumnTypeText),
handler.NewColumn(LoginNameUserResourceOwnerCol, handler.ColumnTypeText),
handler.NewColumn(LoginNameUserInstanceIDCol, handler.ColumnTypeText),
},
handler.NewPrimaryKey(LoginNameUserInstanceIDCol, LoginNameUserIDCol),
loginNameUserSuffix,
handler.WithIndex(handler.NewIndex("instance_user_name", []string{LoginNameUserInstanceIDCol, LoginNameUserUserNameCol},
handler.WithInclude(LoginNameUserResourceOwnerCol),
)),
handler.WithIndex(
handler.NewIndex("lnu_instance_ro_id", []string{LoginNameUserInstanceIDCol, LoginNameUserResourceOwnerCol, LoginNameUserIDCol},
handler.WithInclude(
LoginNameUserUserNameCol,
),
),
),
// TODO: uncomment the following line when login_names4 will be created
// handler.WithIndex(
// handler.NewIndex("search", []string{LoginNameUserInstanceIDCol, loginNameUserUserNameLowerCol},
// handler.WithInclude(LoginNameUserResourceOwnerCol),
// ),
// ),
),
handler.NewSuffixedTable(
[]*handler.InitColumn{
handler.NewColumn(LoginNameDomainNameCol, handler.ColumnTypeText),
// TODO: implement computed columns
// handler.NewComputedColumn(loginNameDomainNameLowerCol, handler.ColumnTypeText),
handler.NewColumn(LoginNameDomainIsPrimaryCol, handler.ColumnTypeBool, handler.Default(false)),
handler.NewColumn(LoginNameDomainResourceOwnerCol, handler.ColumnTypeText),
handler.NewColumn(LoginNameDomainInstanceIDCol, handler.ColumnTypeText),
},
handler.NewPrimaryKey(LoginNameDomainInstanceIDCol, LoginNameDomainResourceOwnerCol, LoginNameDomainNameCol),
loginNameDomainSuffix,
// TODO: uncomment the following line when login_names4 will be created
// handler.WithIndex(
// handler.NewIndex("search", []string{LoginNameDomainInstanceIDCol, LoginNameDomainResourceOwnerCol, loginNameDomainNameLowerCol}),
// ),
// handler.WithIndex(
// handler.NewIndex("search_result", []string{LoginNameDomainInstanceIDCol, LoginNameDomainResourceOwnerCol},
// handler.WithInclude(LoginNameDomainIsPrimaryCol),
// ),
// ),
),
handler.NewSuffixedTable(
[]*handler.InitColumn{
handler.NewColumn(LoginNamePoliciesMustBeDomainCol, handler.ColumnTypeBool),
handler.NewColumn(LoginNamePoliciesIsDefaultCol, handler.ColumnTypeBool),
handler.NewColumn(LoginNamePoliciesResourceOwnerCol, handler.ColumnTypeText),
handler.NewColumn(LoginNamePoliciesInstanceIDCol, handler.ColumnTypeText),
},
handler.NewPrimaryKey(LoginNamePoliciesInstanceIDCol, LoginNamePoliciesResourceOwnerCol),
loginNamePolicySuffix,
// this index is not used anymore, but kept for understanding why the default exists on existing systems, TODO: remove in login_names4
// handler.WithIndex(handler.NewIndex("is_default", []string{LoginNamePoliciesResourceOwnerCol, LoginNamePoliciesIsDefaultCol})),
handler.WithIndex(handler.NewIndex("is_default_owner", []string{LoginNamePoliciesInstanceIDCol, LoginNamePoliciesIsDefaultCol, LoginNamePoliciesResourceOwnerCol}, handler.WithInclude(LoginNamePoliciesMustBeDomainCol))),
),
)
}
func (p *loginNameProjection) Reducers() []handler.AggregateReducer {
return []handler.AggregateReducer{
{
Aggregate: user.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: user.UserV1AddedType,
Reduce: p.reduceUserCreated,
},
{
Event: user.HumanAddedType,
Reduce: p.reduceUserCreated,
},
{
Event: user.HumanRegisteredType,
Reduce: p.reduceUserCreated,
},
{
Event: user.UserV1RegisteredType,
Reduce: p.reduceUserCreated,
},
{
Event: user.MachineAddedEventType,
Reduce: p.reduceUserCreated,
},
{
Event: user.UserRemovedType,
Reduce: p.reduceUserRemoved,
},
{
Event: user.UserUserNameChangedType,
Reduce: p.reduceUserNameChanged,
},
{
// changes the username of the user
// this event occures in orgs
// where policy.must_be_domain=false
Event: user.UserDomainClaimedType,
Reduce: p.reduceUserDomainClaimed,
},
},
},
{
Aggregate: org.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: org.DomainPolicyAddedEventType,
Reduce: p.reduceOrgIAMPolicyAdded,
},
{
Event: org.DomainPolicyChangedEventType,
Reduce: p.reduceDomainPolicyChanged,
},
{
Event: org.DomainPolicyRemovedEventType,
Reduce: p.reduceDomainPolicyRemoved,
},
{
Event: org.OrgDomainPrimarySetEventType,
Reduce: p.reducePrimaryDomainSet,
},
{
Event: org.OrgDomainRemovedEventType,
Reduce: p.reduceDomainRemoved,
},
{
Event: org.OrgDomainVerifiedEventType,
Reduce: p.reduceDomainVerified,
},
{
Event: org.OrgRemovedEventType,
Reduce: p.reduceOwnerRemoved,
},
},
},
{
Aggregate: instance.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: instance.DomainPolicyAddedEventType,
Reduce: p.reduceOrgIAMPolicyAdded,
},
{
Event: instance.DomainPolicyChangedEventType,
Reduce: p.reduceDomainPolicyChanged,
},
{
Event: instance.InstanceRemovedEventType,
Reduce: p.reduceInstanceRemoved,
},
},
},
}
}
func (p *loginNameProjection) reduceUserCreated(event eventstore.Event) (*handler.Statement, error) {
var userName string
switch e := event.(type) {
case *user.HumanAddedEvent:
userName = e.UserName
case *user.HumanRegisteredEvent:
userName = e.UserName
case *user.MachineAddedEvent:
userName = e.UserName
default:
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-ayo69", "reduce.wrong.event.type %v", []eventstore.EventType{user.UserV1AddedType, user.HumanAddedType, user.UserV1RegisteredType, user.HumanRegisteredType, user.MachineAddedEventType})
}
return handler.NewCreateStatement(
event,
[]handler.Column{
handler.NewCol(LoginNameUserIDCol, event.Aggregate().ID),
handler.NewCol(LoginNameUserUserNameCol, userName),
handler.NewCol(LoginNameUserResourceOwnerCol, event.Aggregate().ResourceOwner),
handler.NewCol(LoginNameUserInstanceIDCol, event.Aggregate().InstanceID),
},
handler.WithTableSuffix(loginNameUserSuffix),
), nil
}
func (p *loginNameProjection) reduceUserRemoved(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.UserRemovedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-QIe3C", "reduce.wrong.event.type %s", user.UserRemovedType)
}
return handler.NewDeleteStatement(
event,
[]handler.Condition{
handler.NewCond(LoginNameUserIDCol, e.Aggregate().ID),
handler.NewCond(LoginNameUserInstanceIDCol, e.Aggregate().InstanceID),
},
handler.WithTableSuffix(loginNameUserSuffix),
), nil
}
func (p *loginNameProjection) reduceUserNameChanged(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.UsernameChangedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-QlwjC", "reduce.wrong.event.type %s", user.UserUserNameChangedType)
}
return handler.NewUpdateStatement(
event,
[]handler.Column{
handler.NewCol(LoginNameUserUserNameCol, e.UserName),
},
[]handler.Condition{
handler.NewCond(LoginNameUserIDCol, e.Aggregate().ID),
handler.NewCond(LoginNameUserInstanceIDCol, e.Aggregate().InstanceID),
},
handler.WithTableSuffix(loginNameUserSuffix),
), nil
}
func (p *loginNameProjection) reduceUserDomainClaimed(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.DomainClaimedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-AQMBY", "reduce.wrong.event.type %s", user.UserDomainClaimedType)
}
return handler.NewUpdateStatement(
event,
[]handler.Column{
handler.NewCol(LoginNameUserUserNameCol, e.UserName),
},
[]handler.Condition{
handler.NewCond(LoginNameUserIDCol, e.Aggregate().ID),
handler.NewCond(LoginNameUserInstanceIDCol, e.Aggregate().InstanceID),
},
handler.WithTableSuffix(loginNameUserSuffix),
), nil
}
func (p *loginNameProjection) reduceOrgIAMPolicyAdded(event eventstore.Event) (*handler.Statement, error) {
var (
policyEvent *policy.DomainPolicyAddedEvent
isDefault bool
)
switch e := event.(type) {
case *org.DomainPolicyAddedEvent:
policyEvent = &e.DomainPolicyAddedEvent
isDefault = false
case *instance.DomainPolicyAddedEvent:
policyEvent = &e.DomainPolicyAddedEvent
isDefault = true
default:
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-yCV6S", "reduce.wrong.event.type %v", []eventstore.EventType{org.DomainPolicyAddedEventType, instance.DomainPolicyAddedEventType})
}
return handler.NewCreateStatement(
event,
[]handler.Column{
handler.NewCol(LoginNamePoliciesMustBeDomainCol, policyEvent.UserLoginMustBeDomain),
handler.NewCol(LoginNamePoliciesIsDefaultCol, isDefault),
handler.NewCol(LoginNamePoliciesResourceOwnerCol, policyEvent.Aggregate().ResourceOwner),
handler.NewCol(LoginNamePoliciesInstanceIDCol, policyEvent.Aggregate().InstanceID),
},
handler.WithTableSuffix(loginNamePolicySuffix),
), nil
}
func (p *loginNameProjection) reduceDomainPolicyChanged(event eventstore.Event) (*handler.Statement, error) {
var policyEvent *policy.DomainPolicyChangedEvent
switch e := event.(type) {
case *org.DomainPolicyChangedEvent:
policyEvent = &e.DomainPolicyChangedEvent
case *instance.DomainPolicyChangedEvent:
policyEvent = &e.DomainPolicyChangedEvent
default:
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-ArFDd", "reduce.wrong.event.type %v", []eventstore.EventType{org.DomainPolicyChangedEventType, instance.DomainPolicyChangedEventType})
}
if policyEvent.UserLoginMustBeDomain == nil {
return handler.NewNoOpStatement(event), nil
}
return handler.NewUpdateStatement(
event,
[]handler.Column{
handler.NewCol(LoginNamePoliciesMustBeDomainCol, *policyEvent.UserLoginMustBeDomain),
},
[]handler.Condition{
handler.NewCond(LoginNamePoliciesResourceOwnerCol, policyEvent.Aggregate().ResourceOwner),
handler.NewCond(LoginNamePoliciesInstanceIDCol, policyEvent.Aggregate().InstanceID),
},
handler.WithTableSuffix(loginNamePolicySuffix),
), nil
}
func (p *loginNameProjection) reduceDomainPolicyRemoved(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*org.DomainPolicyRemovedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-ysEeB", "reduce.wrong.event.type %s", org.DomainPolicyRemovedEventType)
}
return handler.NewDeleteStatement(
event,
[]handler.Condition{
handler.NewCond(LoginNamePoliciesResourceOwnerCol, e.Aggregate().ResourceOwner),
handler.NewCond(LoginNamePoliciesInstanceIDCol, e.Aggregate().InstanceID),
},
handler.WithTableSuffix(loginNamePolicySuffix),
), nil
}
func (p *loginNameProjection) reduceDomainVerified(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*org.DomainVerifiedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-weGAh", "reduce.wrong.event.type %s", org.OrgDomainVerifiedEventType)
}
return handler.NewCreateStatement(
event,
[]handler.Column{
handler.NewCol(LoginNameDomainNameCol, e.Domain),
handler.NewCol(LoginNameDomainResourceOwnerCol, e.Aggregate().ResourceOwner),
handler.NewCol(LoginNameDomainInstanceIDCol, e.Aggregate().InstanceID),
},
handler.WithTableSuffix(loginNameDomainSuffix),
), nil
}
func (p *loginNameProjection) reducePrimaryDomainSet(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*org.DomainPrimarySetEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-eOXPN", "reduce.wrong.event.type %s", org.OrgDomainPrimarySetEventType)
}
return handler.NewMultiStatement(
e,
handler.AddUpdateStatement(
[]handler.Column{
handler.NewCol(LoginNameDomainIsPrimaryCol, false),
},
[]handler.Condition{
handler.NewCond(LoginNameDomainResourceOwnerCol, e.Aggregate().ResourceOwner),
handler.NewCond(LoginNameDomainIsPrimaryCol, true),
handler.NewCond(LoginNameDomainInstanceIDCol, e.Aggregate().InstanceID),
},
handler.WithTableSuffix(loginNameDomainSuffix),
),
handler.AddUpdateStatement(
[]handler.Column{
handler.NewCol(LoginNameDomainIsPrimaryCol, true),
},
[]handler.Condition{
handler.NewCond(LoginNameDomainNameCol, e.Domain),
handler.NewCond(LoginNameDomainResourceOwnerCol, e.Aggregate().ResourceOwner),
handler.NewCond(LoginNameDomainInstanceIDCol, e.Aggregate().InstanceID),
},
handler.WithTableSuffix(loginNameDomainSuffix),
),
), nil
}
func (p *loginNameProjection) reduceDomainRemoved(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*org.DomainRemovedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-4RHYq", "reduce.wrong.event.type %s", org.OrgDomainRemovedEventType)
}
return handler.NewDeleteStatement(
event,
[]handler.Condition{
handler.NewCond(LoginNameDomainNameCol, e.Domain),
handler.NewCond(LoginNameDomainResourceOwnerCol, e.Aggregate().ResourceOwner),
handler.NewCond(LoginNameDomainInstanceIDCol, e.Aggregate().InstanceID),
},
handler.WithTableSuffix(loginNameDomainSuffix),
), nil
}
func (p *loginNameProjection) reduceInstanceRemoved(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*instance.InstanceRemovedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-ASeg3", "reduce.wrong.event.type %s", instance.InstanceRemovedEventType)
}
return handler.NewMultiStatement(
event,
handler.AddDeleteStatement(
[]handler.Condition{
handler.NewCond(LoginNameDomainInstanceIDCol, e.Aggregate().ID),
},
handler.WithTableSuffix(loginNameDomainSuffix),
),
handler.AddDeleteStatement(
[]handler.Condition{
handler.NewCond(LoginNamePoliciesInstanceIDCol, e.Aggregate().ID),
},
handler.WithTableSuffix(loginNamePolicySuffix),
),
handler.AddDeleteStatement(
[]handler.Condition{
handler.NewCond(LoginNameUserInstanceIDCol, e.Aggregate().ID),
},
handler.WithTableSuffix(loginNameUserSuffix),
),
), nil
}
func (p *loginNameProjection) reduceOwnerRemoved(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*org.OrgRemovedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-px02mo", "reduce.wrong.event.type %s", org.OrgRemovedEventType)
}
return handler.NewMultiStatement(
event,
handler.AddDeleteStatement(
[]handler.Condition{
handler.NewCond(LoginNameDomainInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCond(LoginNameDomainResourceOwnerCol, e.Aggregate().ID),
},
handler.WithTableSuffix(loginNameDomainSuffix),
),
handler.AddDeleteStatement(
[]handler.Condition{
handler.NewCond(LoginNamePoliciesInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCond(LoginNamePoliciesResourceOwnerCol, e.Aggregate().ID),
},
handler.WithTableSuffix(loginNamePolicySuffix),
),
handler.AddDeleteStatement(
[]handler.Condition{
handler.NewCond(LoginNameUserInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCond(LoginNameUserResourceOwnerCol, e.Aggregate().ID),
},
handler.WithTableSuffix(loginNameUserSuffix),
),
), nil
}