fix(query): user performance (#6537)

* start user by id

* ignore debug bin

* use new user by id

* new sql

* fix(sql): replace STRING with text for psql compatabilit

* some changes

* fix: correct user queries

* fix tests

* unify sql statements

* use specific get user methods

* search login name case insensitive

* refactor: optimise user statements

* add index

* fix queries

* fix: correct domain segregation

* return all login names

* fix queries

* improve readability

* query should be correct now

* cleanup statements

* fix username / loginname handling

* fix: psql doesn't support create view if not exists

* fix: create pre-release

* ignore release comments

* add lower fields

* fix: always to lower

* update to latest projection

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Silvan
2023-12-08 13:14:22 +01:00
committed by GitHub
parent 94e0caa601
commit ddbea119f1
21 changed files with 754 additions and 272 deletions

26
cmd/setup/18.go Normal file
View File

@@ -0,0 +1,26 @@
package setup
import (
"context"
_ "embed"
"github.com/zitadel/zitadel/internal/database"
)
var (
//go:embed 18.sql
addLowerFieldsToLoginNames string
)
type AddLowerFieldsToLoginNames struct {
dbClient *database.DB
}
func (mig *AddLowerFieldsToLoginNames) Execute(ctx context.Context) error {
_, err := mig.dbClient.ExecContext(ctx, addLowerFieldsToLoginNames)
return err
}
func (mig *AddLowerFieldsToLoginNames) String() string {
return "18_add_lower_fields_to_login_names"
}

6
cmd/setup/18.sql Normal file
View File

@@ -0,0 +1,6 @@
ALTER TABLE IF EXISTS projections.login_names3_users ADD COLUMN IF NOT EXISTS user_name_lower TEXT GENERATED ALWAYS AS (lower(user_name)) STORED;
CREATE INDEX IF NOT EXISTS login_names3_users_search ON projections.login_names3_users (instance_id, user_name_lower) INCLUDE (resource_owner);
ALTER TABLE IF EXISTS projections.login_names3_domains ADD COLUMN IF NOT EXISTS name_lower TEXT GENERATED ALWAYS AS (lower(name)) STORED;
CREATE INDEX IF NOT EXISTS login_names3_domain_search ON projections.login_names3_domains (instance_id, resource_owner, name_lower);
CREATE INDEX IF NOT EXISTS login_names3_domain_search_result ON projections.login_names3_domains (instance_id, resource_owner) INCLUDE (is_primary);

View File

@@ -75,6 +75,7 @@ type Steps struct {
s15CurrentStates *CurrentProjectionState
s16UniqueConstraintsLower *UniqueConstraintToLower
s17AddOffsetToUniqueConstraints *AddOffsetToCurrentStates
s18AddLowerFieldsToLoginNames *AddLowerFieldsToLoginNames
}
type encryptionKeyConfig struct {

View File

@@ -105,6 +105,7 @@ func Setup(config *Config, steps *Steps, masterKey string) {
steps.s15CurrentStates = &CurrentProjectionState{dbClient: zitadelDBClient}
steps.s16UniqueConstraintsLower = &UniqueConstraintToLower{dbClient: zitadelDBClient}
steps.s17AddOffsetToUniqueConstraints = &AddOffsetToCurrentStates{dbClient: zitadelDBClient}
steps.s18AddLowerFieldsToLoginNames = &AddLowerFieldsToLoginNames{dbClient: zitadelDBClient}
err = projection.Create(ctx, zitadelDBClient, eventstoreClient, config.Projections, nil, nil, nil)
logging.OnError(err).Fatal("unable to start projections")
@@ -154,6 +155,10 @@ func Setup(config *Config, steps *Steps, masterKey string) {
err = migration.Migrate(ctx, eventstoreClient, repeatableStep)
logging.OnError(err).Fatalf("unable to migrate repeatable step: %s", repeatableStep.String())
}
// This step is executed after the repeatable steps because it adds fields to the login_names3 projection
err = migration.Migrate(ctx, eventstoreClient, steps.s18AddLowerFieldsToLoginNames)
logging.WithFields("name", steps.s18AddLowerFieldsToLoginNames.String()).OnError(err).Fatal("migration failed")
}
func readStmt(fs embed.FS, folder, typ, filename string) (string, error) {

View File

@@ -20,6 +20,7 @@ import (
"github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/repository/user"
@@ -27,13 +28,12 @@ import (
)
func (s *Server) getUserByID(ctx context.Context, id string) (*query.User, error) {
owner, err := query.NewUserResourceOwnerSearchQuery(authz.GetCtxData(ctx).OrgID, query.TextEquals)
user, err := s.query.GetUserByID(ctx, true, id)
if err != nil {
return nil, err
}
user, err := s.query.GetUserByID(ctx, true, id, owner)
if err != nil {
return nil, err
if user.ResourceOwner != authz.GetCtxData(ctx).OrgID {
return nil, errors.ThrowNotFound(nil, "MANAG-fpo4B", "Errors.User.NotFound")
}
return user, nil
}
@@ -49,11 +49,7 @@ func (s *Server) GetUserByID(ctx context.Context, req *mgmt_pb.GetUserByIDReques
}
func (s *Server) GetUserByLoginNameGlobal(ctx context.Context, req *mgmt_pb.GetUserByLoginNameGlobalRequest) (*mgmt_pb.GetUserByLoginNameGlobalResponse, error) {
loginName, err := query.NewUserPreferredLoginNameSearchQuery(req.LoginName, query.TextEquals)
if err != nil {
return nil, err
}
user, err := s.query.GetUser(ctx, true, loginName)
user, err := s.query.GetUserByLoginName(ctx, true, req.LoginName)
if err != nil {
return nil, err
}

View File

@@ -474,11 +474,7 @@ func userByID(userID string) userSearch {
}
func userByLoginName(loginName string) (userSearch, error) {
loginNameQuery, err := query.NewUserLoginNamesSearchQuery(loginName)
if err != nil {
return nil, err
}
return userSearchByLoginName{loginNameQuery}, nil
return userSearchByLoginName{loginName}, nil
}
type userSearchByID struct {
@@ -490,9 +486,9 @@ func (u userSearchByID) search(ctx context.Context, q *query.Queries) (*query.Us
}
type userSearchByLoginName struct {
loginNameQuery query.SearchQuery
loginName string
}
func (u userSearchByLoginName) search(ctx context.Context, q *query.Queries) (*query.User, error) {
return q.GetUser(ctx, true, u.loginNameQuery)
return q.GetUserByLoginName(ctx, true, u.loginName)
}

View File

@@ -586,12 +586,6 @@ func Test_sessionQueryToQuery(t *testing.T) {
}
}
func mustUserLoginNamesSearchQuery(t testing.TB, value string) query.SearchQuery {
loginNameQuery, err := query.NewUserLoginNamesSearchQuery("bar")
require.NoError(t, err)
return loginNameQuery
}
func Test_userCheck(t *testing.T) {
type args struct {
user *session.CheckUser
@@ -623,7 +617,7 @@ func Test_userCheck(t *testing.T) {
LoginName: "bar",
},
}},
want: userSearchByLoginName{mustUserLoginNamesSearchQuery(t, "bar")},
want: userSearchByLoginName{"bar"},
},
{
name: "unimplemented error",

View File

@@ -197,11 +197,7 @@ func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection
}
func (o *OPStorage) ClientCredentialsTokenRequest(ctx context.Context, clientID string, scope []string) (op.TokenRequest, error) {
loginname, err := query.NewUserLoginNamesSearchQuery(clientID)
if err != nil {
return nil, err
}
user, err := o.query.GetUser(ctx, false, loginname)
user, err := o.query.GetUserByLoginName(ctx, false, clientID)
if err != nil {
return nil, err
}

View File

@@ -34,11 +34,7 @@ func (c *clientCredentialsRequest) GetScopes() []string {
}
func (s *Server) clientCredentialsAuth(ctx context.Context, clientID, clientSecret string) (op.Client, error) {
searchQuery, err := query.NewUserLoginNamesSearchQuery(clientID)
if err != nil {
return nil, err
}
user, err := s.query.GetUser(ctx, false, searchQuery)
user, err := s.query.GetUserByLoginName(ctx, false, clientID)
if errors.IsNotFound(err) {
return nil, oidc.ErrInvalidClient().WithParent(err).WithDescription("client not found")
}

View File

@@ -159,11 +159,7 @@ func (p *Storage) SetUserinfoWithLoginName(ctx context.Context, userinfo models.
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
loginNameSQ, err := query.NewUserLoginNamesSearchQuery(loginName)
if err != nil {
return err
}
user, err := p.query.GetUser(ctx, true, loginNameSQ)
user, err := p.query.GetUserByLoginName(ctx, true, loginName)
if err != nil {
return err
}

View File

@@ -7,7 +7,6 @@ import (
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query"
)
const (
@@ -91,7 +90,7 @@ func (l *Login) resendPasswordSet(w http.ResponseWriter, r *http.Request, authRe
if authReq != nil {
userOrg = authReq.UserOrgID
}
loginName, err := query.NewUserLoginNamesSearchQuery(authReq.LoginName)
user, err := l.query.GetUserByLoginName(setContext(r.Context(), userOrg), false, authReq.LoginName)
if err != nil {
l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
return
@@ -101,11 +100,6 @@ func (l *Login) resendPasswordSet(w http.ResponseWriter, r *http.Request, authRe
l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
return
}
user, err := l.query.GetUser(setContext(r.Context(), userOrg), false, loginName)
if err != nil {
l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
return
}
_, err = l.command.RequestSetPassword(setContext(r.Context(), userOrg), user.ID, user.ResourceOwner, domain.NotificationTypeEmail, passwordCodeGenerator)
l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
}

View File

@@ -5,7 +5,6 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query"
)
const (
@@ -18,12 +17,7 @@ func (l *Login) handlePasswordReset(w http.ResponseWriter, r *http.Request) {
l.renderError(w, r, authReq, err)
return
}
loginName, err := query.NewUserLoginNamesSearchQuery(authReq.LoginName)
if err != nil {
l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
return
}
user, err := l.query.GetUser(setContext(r.Context(), authReq.UserOrgID), true, loginName)
user, err := l.query.GetUserByLoginName(setContext(r.Context(), authReq.UserOrgID), true, authReq.LoginName)
if err != nil {
if authReq.LoginPolicy.IgnoreUnknownUsernames && errors.IsNotFound(err) {
err = nil

View File

@@ -4,6 +4,7 @@ import (
"context"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/query"
@@ -21,25 +22,31 @@ func (v *View) UserByID(userID, instanceID string) (*model.UserView, error) {
}
func (v *View) UserByLoginName(ctx context.Context, loginName, instanceID string) (*model.UserView, error) {
loginNameQuery, err := query.NewUserLoginNamesSearchQuery(loginName)
queriedUser, err := v.query.GetNotifyUserByLoginName(ctx, true, loginName)
if err != nil {
return nil, err
}
return v.userByID(ctx, instanceID, loginNameQuery)
//nolint: contextcheck // no lint was added because refactor would change too much code
return view.UserByID(v.Db, userTable, queriedUser.ID, instanceID)
}
func (v *View) UserByLoginNameAndResourceOwner(ctx context.Context, loginName, resourceOwner, instanceID string) (*model.UserView, error) {
loginNameQuery, err := query.NewUserLoginNamesSearchQuery(loginName)
if err != nil {
return nil, err
}
resourceOwnerQuery, err := query.NewUserResourceOwnerSearchQuery(resourceOwner, query.TextEquals)
queriedUser, err := v.query.GetNotifyUserByLoginName(ctx, true, loginName)
if err != nil {
return nil, err
}
return v.userByID(ctx, instanceID, loginNameQuery, resourceOwnerQuery)
//nolint: contextcheck // no lint was added because refactor would change too much code
user, err := view.UserByID(v.Db, userTable, queriedUser.ID, instanceID)
if err != nil {
return nil, err
}
if user.ResourceOwner != resourceOwner {
return nil, errors.ThrowNotFound(nil, "VIEW-qScmi", "Errors.User.NotFound")
}
return user, nil
}
func (v *View) UserByEmail(ctx context.Context, email, instanceID string) (*model.UserView, error) {

View File

@@ -101,23 +101,18 @@ func (mr *MockQueriesMockRecorder) GetInstanceRestrictions(arg0 any) *gomock.Cal
}
// GetNotifyUserByID mocks base method.
func (m *MockQueries) GetNotifyUserByID(arg0 context.Context, arg1 bool, arg2 string, arg3 ...query.SearchQuery) (*query.NotifyUser, error) {
func (m *MockQueries) GetNotifyUserByID(arg0 context.Context, arg1 bool, arg2 string) (*query.NotifyUser, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1, arg2}
for _, a := range arg3 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "GetNotifyUserByID", varargs...)
ret := m.ctrl.Call(m, "GetNotifyUserByID", arg0, arg1, arg2)
ret0, _ := ret[0].(*query.NotifyUser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetNotifyUserByID indicates an expected call of GetNotifyUserByID.
func (mr *MockQueriesMockRecorder) GetNotifyUserByID(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
func (mr *MockQueriesMockRecorder) GetNotifyUserByID(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1, arg2}, arg3...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotifyUserByID", reflect.TypeOf((*MockQueries)(nil).GetNotifyUserByID), varargs...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotifyUserByID", reflect.TypeOf((*MockQueries)(nil).GetNotifyUserByID), arg0, arg1, arg2)
}
// MailTemplateByOrg mocks base method.

View File

@@ -13,7 +13,7 @@ import (
type Queries interface {
ActiveLabelPolicyByOrg(ctx context.Context, orgID string, withOwnerRemoved bool) (*query.LabelPolicy, error)
MailTemplateByOrg(ctx context.Context, orgID string, withOwnerRemoved bool) (*query.MailTemplate, error)
GetNotifyUserByID(ctx context.Context, shouldTriggered bool, userID string, queries ...query.SearchQuery) (*query.NotifyUser, error)
GetNotifyUserByID(ctx context.Context, shouldTriggered bool, userID string) (*query.NotifyUser, error)
CustomTextListByTemplate(ctx context.Context, aggregateID, template string, withOwnerRemoved bool) (*query.CustomTexts, error)
SearchInstanceDomains(ctx context.Context, queries *query.InstanceDomainSearchQueries) (*query.InstanceDomains, error)
SessionByID(ctx context.Context, shouldTriggerBulk bool, id, sessionToken string) (*query.Session, error)

View File

@@ -17,6 +17,7 @@ import (
)
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
@@ -38,11 +39,14 @@ const (
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"
@@ -171,12 +175,16 @@ func (*loginNameProjection) Init() *old_handler.Check {
[]*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("resource_owner", []string{LoginNameUserResourceOwnerCol})),
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(
@@ -184,16 +192,33 @@ func (*loginNameProjection) Init() *old_handler.Check {
),
),
),
// 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{

View File

@@ -3,6 +3,7 @@ package query
import (
"context"
"database/sql"
_ "embed"
errs "errors"
"strings"
"time"
@@ -314,7 +315,10 @@ var (
}
)
func (q *Queries) GetUserByID(ctx context.Context, shouldTriggerBulk bool, userID string, queries ...SearchQuery) (user *User, err error) {
//go:embed user_by_id.sql
var userByIDQuery string
func (q *Queries) GetUserByID(ctx context.Context, shouldTriggerBulk bool, userID string) (user *User, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -322,26 +326,55 @@ func (q *Queries) GetUserByID(ctx context.Context, shouldTriggerBulk bool, userI
triggerUserProjections(ctx)
}
query, scan := prepareUserQuery(ctx, q.client)
for _, q := range queries {
query = q.toQuery(query)
}
eq := sq.Eq{
UserIDCol.identifier(): userID,
UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
}
stmt, args, err := query.Where(eq).ToSql()
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-FBg21", "Errors.Query.SQLStatment")
}
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
user, err = scan(row)
err = q.client.QueryRowContext(ctx,
func(row *sql.Row) error {
user, err = scanUser(row)
return err
}, stmt, args...)
},
userByIDQuery,
userID,
authz.GetInstance(ctx).InstanceID(),
)
return user, err
}
//go:embed user_by_login_name.sql
var userByLoginNameQuery string
func (q *Queries) GetUserByLoginName(ctx context.Context, shouldTriggered bool, loginName string) (user *User, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if shouldTriggered {
triggerUserProjections(ctx)
}
loginName = strings.ToLower(loginName)
username := loginName
domainIndex := strings.LastIndex(loginName, "@")
var domainSuffix string
// split between the last @ (so ignore it if the login name ends with it)
if domainIndex > 0 && domainIndex != len(loginName)-1 {
domainSuffix = loginName[domainIndex+1:]
username = loginName[:domainIndex]
}
err = q.client.QueryRowContext(ctx,
func(row *sql.Row) error {
user, err = scanUser(row)
return err
},
userByLoginNameQuery,
username,
domainSuffix,
loginName,
authz.GetInstance(ctx).InstanceID(),
)
return user, err
}
// Deprecated: use either GetUserByID or GetUserByLoginName
func (q *Queries) GetUser(ctx context.Context, shouldTriggerBulk bool, queries ...SearchQuery) (user *User, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -441,7 +474,10 @@ func (q *Queries) GetHumanPhone(ctx context.Context, userID string, queries ...S
return phone, err
}
func (q *Queries) GetNotifyUserByID(ctx context.Context, shouldTriggered bool, userID string, queries ...SearchQuery) (user *NotifyUser, err error) {
//go:embed user_notify_by_id.sql
var notifyUserByIDQuery string
func (q *Queries) GetNotifyUserByID(ctx context.Context, shouldTriggered bool, userID string) (user *NotifyUser, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -449,23 +485,51 @@ func (q *Queries) GetNotifyUserByID(ctx context.Context, shouldTriggered bool, u
triggerUserProjections(ctx)
}
query, scan := prepareNotifyUserQuery(ctx, q.client)
for _, q := range queries {
query = q.toQuery(query)
}
eq := sq.Eq{
UserIDCol.identifier(): userID,
UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
}
stmt, args, err := query.Where(eq).ToSql()
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-Err3g", "Errors.Query.SQLStatment")
err = q.client.QueryRowContext(ctx,
func(row *sql.Row) error {
user, err = scanNotifyUser(row)
return err
},
notifyUserByIDQuery,
userID,
authz.GetInstance(ctx).InstanceID(),
)
return user, err
}
//go:embed user_notify_by_login_name.sql
var notifyUserByLoginNameQuery string
func (q *Queries) GetNotifyUserByLoginName(ctx context.Context, shouldTriggered bool, loginName string) (user *NotifyUser, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if shouldTriggered {
triggerUserProjections(ctx)
}
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
user, err = scan(row)
loginName = strings.ToLower(loginName)
username := loginName
domainIndex := strings.LastIndex(loginName, "@")
var domainSuffix string
// split between the last @ (so ignore it if the login name ends with it)
if domainIndex > 0 && domainIndex != len(loginName)-1 {
domainSuffix = loginName[domainIndex+1:]
username = loginName[:domainIndex]
}
err = q.client.QueryRowContext(ctx,
func(row *sql.Row) error {
user, err = scanNotifyUser(row)
return err
}, stmt, args...)
},
notifyUserByLoginNameQuery,
username,
domainSuffix,
loginName,
authz.GetInstance(ctx).InstanceID(),
)
return user, err
}
@@ -706,58 +770,7 @@ func preparePreferredLoginNamesQuery() (string, []interface{}, error) {
).ToSql()
}
func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*User, error)) {
loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery()
if err != nil {
return sq.SelectBuilder{}, nil
}
preferredLoginNameQuery, preferredLoginNameArgs, err := preparePreferredLoginNamesQuery()
if err != nil {
return sq.SelectBuilder{}, nil
}
return sq.Select(
UserIDCol.identifier(),
UserCreationDateCol.identifier(),
UserChangeDateCol.identifier(),
UserResourceOwnerCol.identifier(),
UserSequenceCol.identifier(),
UserStateCol.identifier(),
UserTypeCol.identifier(),
UserUsernameCol.identifier(),
userLoginNamesListCol.identifier(),
userPreferredLoginNameCol.identifier(),
HumanUserIDCol.identifier(),
HumanFirstNameCol.identifier(),
HumanLastNameCol.identifier(),
HumanNickNameCol.identifier(),
HumanDisplayNameCol.identifier(),
HumanPreferredLanguageCol.identifier(),
HumanGenderCol.identifier(),
HumanAvatarURLCol.identifier(),
HumanEmailCol.identifier(),
HumanIsEmailVerifiedCol.identifier(),
HumanPhoneCol.identifier(),
HumanIsPhoneVerifiedCol.identifier(),
MachineUserIDCol.identifier(),
MachineNameCol.identifier(),
MachineDescriptionCol.identifier(),
MachineSecretCol.identifier(),
MachineAccessTokenTypeCol.identifier(),
countColumn.identifier(),
).
From(userTable.identifier()).
LeftJoin(join(HumanUserIDCol, UserIDCol)).
LeftJoin(join(MachineUserIDCol, UserIDCol)).
LeftJoin("("+loginNamesQuery+") AS "+userLoginNamesTable.alias+" ON "+
userLoginNamesUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+
userLoginNamesInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(),
loginNamesArgs...).
LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+
userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+
userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier()+db.Timetravel(call.Took(ctx)),
preferredLoginNameArgs...).
PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*User, error) {
func scanUser(row *sql.Row) (*User, error) {
u := new(User)
var count int
preferredLoginName := sql.NullString{}
@@ -844,7 +857,60 @@ func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder
}
}
return u, nil
}
func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*User, error)) {
loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery()
if err != nil {
return sq.SelectBuilder{}, nil
}
preferredLoginNameQuery, preferredLoginNameArgs, err := preparePreferredLoginNamesQuery()
if err != nil {
return sq.SelectBuilder{}, nil
}
return sq.Select(
UserIDCol.identifier(),
UserCreationDateCol.identifier(),
UserChangeDateCol.identifier(),
UserResourceOwnerCol.identifier(),
UserSequenceCol.identifier(),
UserStateCol.identifier(),
UserTypeCol.identifier(),
UserUsernameCol.identifier(),
userLoginNamesListCol.identifier(),
userPreferredLoginNameCol.identifier(),
HumanUserIDCol.identifier(),
HumanFirstNameCol.identifier(),
HumanLastNameCol.identifier(),
HumanNickNameCol.identifier(),
HumanDisplayNameCol.identifier(),
HumanPreferredLanguageCol.identifier(),
HumanGenderCol.identifier(),
HumanAvatarURLCol.identifier(),
HumanEmailCol.identifier(),
HumanIsEmailVerifiedCol.identifier(),
HumanPhoneCol.identifier(),
HumanIsPhoneVerifiedCol.identifier(),
MachineUserIDCol.identifier(),
MachineNameCol.identifier(),
MachineDescriptionCol.identifier(),
MachineSecretCol.identifier(),
MachineAccessTokenTypeCol.identifier(),
countColumn.identifier(),
).
From(userTable.identifier()).
LeftJoin(join(HumanUserIDCol, UserIDCol)).
LeftJoin(join(MachineUserIDCol, UserIDCol)).
LeftJoin("("+loginNamesQuery+") AS "+userLoginNamesTable.alias+" ON "+
userLoginNamesUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+
userLoginNamesInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier(),
loginNamesArgs...).
LeftJoin("("+preferredLoginNameQuery+") AS "+userPreferredLoginNameTable.alias+" ON "+
userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier()+" AND "+
userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier()+db.Timetravel(call.Took(ctx)),
preferredLoginNameArgs...).
PlaceholderFormat(sq.Dollar),
scanUser
}
func prepareProfileQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Profile, error)) {
@@ -1055,7 +1121,10 @@ func prepareNotifyUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectB
userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier()+db.Timetravel(call.Took(ctx)),
preferredLoginNameArgs...).
PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*NotifyUser, error) {
scanNotifyUser
}
func scanNotifyUser(row *sql.Row) (*NotifyUser, error) {
u := new(NotifyUser)
var count int
loginNames := database.TextArray[string]{}
@@ -1136,7 +1205,6 @@ func prepareNotifyUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectB
u.PasswordSet = notifyPasswordSet.Bool
return u, nil
}
}
func prepareUserUniqueQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (bool, error)) {

View File

@@ -0,0 +1,81 @@
WITH login_names AS (SELECT
u.id user_id
, u.instance_id
, u.resource_owner
, u.user_name
, d.name domain_name
, d.is_primary
, p.must_be_domain
, CASE WHEN p.must_be_domain
THEN concat(u.user_name, '@', d.name)
ELSE u.user_name
END login_name
FROM
projections.login_names3_users u
JOIN lateral (
SELECT
p.must_be_domain
FROM
projections.login_names3_policies p
WHERE
u.instance_id = p.instance_id
AND (
(p.is_default IS TRUE AND p.instance_id = $2)
OR (p.instance_id = $2 AND p.resource_owner = u.resource_owner)
)
AND
u.id = $1
ORDER BY is_default
LIMIT 1
) p ON TRUE
JOIN
projections.login_names3_domains d
ON
u.instance_id = d.instance_id
AND u.resource_owner = d.resource_owner
)
SELECT
u.id
, u.creation_date
, u.change_date
, u.resource_owner
, u.sequence
, u.state
, u.type
, u.username
, (SELECT array_agg(ln.login_name)::TEXT[] login_names FROM login_names ln GROUP BY ln.user_id, ln.instance_id) login_names
, (SELECT ln.login_name login_names_lower FROM login_names ln WHERE ln.is_primary IS TRUE) preferred_login_name
, h.user_id
, h.first_name
, h.last_name
, h.nick_name
, h.display_name
, h.preferred_language
, h.gender
, h.avatar_key
, h.email
, h.is_email_verified
, h.phone
, h.is_phone_verified
, m.user_id
, m.name
, m.description
, m.secret
, m.access_token_type
, count(*) OVER ()
FROM projections.users10 u
LEFT JOIN
projections.users10_humans h
ON
u.id = h.user_id
AND u.instance_id = h.instance_id
LEFT JOIN
projections.users10_machines m
ON
u.id = m.user_id
AND u.instance_id = m.instance_id
WHERE
u.id = $1
AND u.instance_id = $2
LIMIT 1
;

View File

@@ -0,0 +1,115 @@
WITH found_users AS (
SELECT DISTINCT
u.id
, u.instance_id
, u.resource_owner
, u.user_name
FROM
projections.login_names3_users u
JOIN lateral (
SELECT
p.must_be_domain
FROM
projections.login_names3_policies p
WHERE
u.instance_id = p.instance_id
AND (
(p.is_default IS TRUE AND p.instance_id = $4)
OR (p.instance_id = $4 AND p.resource_owner = u.resource_owner)
)
AND (
(p.must_be_domain IS TRUE AND user_name_lower = $1)
OR (p.must_be_domain IS FALSE AND user_name_lower = $3)
)
ORDER BY is_default
LIMIT 1
) p ON TRUE
JOIN
projections.login_names3_domains d
ON
u.instance_id = d.instance_id
AND u.resource_owner = d.resource_owner
AND CASE WHEN p.must_be_domain THEN d.name_lower = $2 ELSE TRUE END
),
login_names AS (SELECT
fu.id user_id
, fu.instance_id
, fu.resource_owner
, fu.user_name
, d.name domain_name
, d.is_primary
, p.must_be_domain
, CASE WHEN p.must_be_domain
THEN concat(fu.user_name, '@', d.name)
ELSE fu.user_name
END login_name
FROM
found_users fu
JOIN lateral (
SELECT
p.must_be_domain
FROM
projections.login_names3_policies p
WHERE
fu.instance_id = p.instance_id
AND (
(p.is_default IS TRUE AND p.instance_id = $4)
OR (p.instance_id = $4 AND p.resource_owner = fu.resource_owner)
)
ORDER BY is_default
LIMIT 1
) p ON TRUE
JOIN
projections.login_names3_domains d
ON
fu.instance_id = d.instance_id
AND fu.resource_owner = d.resource_owner
)
SELECT
u.id
, u.creation_date
, u.change_date
, u.resource_owner
, u.sequence
, u.state
, u.type
, u.username
, (SELECT array_agg(ln.login_name)::TEXT[] login_names FROM login_names ln WHERE fu.id = ln.user_id GROUP BY ln.user_id, ln.instance_id) login_names
, (SELECT ln.login_name login_names_lower FROM login_names ln WHERE fu.id = ln.user_id AND ln.is_primary IS TRUE) preferred_login_name
, h.user_id
, h.first_name
, h.last_name
, h.nick_name
, h.display_name
, h.preferred_language
, h.gender
, h.avatar_key
, h.email
, h.is_email_verified
, h.phone
, h.is_phone_verified
, m.user_id
, m.name
, m.description
, m.secret
, m.access_token_type
, count(*) OVER ()
FROM found_users fu
JOIN
projections.users10 u
ON
fu.id = u.id
AND fu.instance_id = u.instance_id
LEFT JOIN
projections.users10_humans h
ON
fu.id = h.user_id
AND fu.instance_id = h.instance_id
LEFT JOIN
projections.users10_machines m
ON
fu.id = m.user_id
AND fu.instance_id = m.instance_id
WHERE
u.instance_id = $4
;

View File

@@ -0,0 +1,79 @@
WITH login_names AS (
SELECT
u.id user_id
, u.instance_id
, u.resource_owner
, u.user_name
, d.name domain_name
, d.is_primary
, p.must_be_domain
, CASE WHEN p.must_be_domain
THEN concat(u.user_name, '@', d.name)
ELSE u.user_name
END login_name
FROM
projections.login_names3_users u
JOIN lateral (
SELECT
p.must_be_domain
FROM
projections.login_names3_policies p
WHERE
u.instance_id = p.instance_id
AND (
(p.is_default IS TRUE AND p.instance_id = $2)
OR (p.instance_id = $2 AND p.resource_owner = u.resource_owner)
)
AND
u.id = $1
ORDER BY is_default
LIMIT 1
) p ON TRUE
JOIN
projections.login_names3_domains d
ON
u.instance_id = d.instance_id
AND u.resource_owner = d.resource_owner
)
SELECT
u.id
, u.creation_date
, u.change_date
, u.resource_owner
, u.sequence
, u.state
, u.type
, u.username
, (SELECT array_agg(ln.login_name)::TEXT[] login_names FROM login_names ln GROUP BY ln.user_id, ln.instance_id) login_names
, (SELECT ln.login_name login_names_lower FROM login_names ln WHERE ln.is_primary IS TRUE) preferred_login_name
, h.user_id
, h.first_name
, h.last_name
, h.nick_name
, h.display_name
, h.preferred_language
, h.gender
, h.avatar_key
, n.user_id
, n.last_email
, n.verified_email
, n.last_phone
, n.verified_phone
, n.password_set
, count(*) OVER ()
FROM projections.users10 u
LEFT JOIN
projections.users10_humans h
ON
u.id = h.user_id
AND u.instance_id = h.instance_id
LEFT JOIN
projections.users10_notifications n
ON
u.id = n.user_id
AND u.instance_id = n.instance_id
WHERE
u.id = $1
AND u.instance_id = $2
LIMIT 1
;

View File

@@ -0,0 +1,112 @@
WITH found_users AS (
SELECT DISTINCT
u.id
, u.instance_id
, u.resource_owner
, u.user_name
FROM
projections.login_names3_users u
JOIN lateral (
SELECT
p.must_be_domain
FROM
projections.login_names3_policies p
WHERE
u.instance_id = p.instance_id
AND (
(p.is_default IS TRUE AND p.instance_id = $4)
OR (p.instance_id = $4 AND p.resource_owner = u.resource_owner)
)
AND (
(p.must_be_domain IS TRUE AND u.user_name_lower = $1)
OR (p.must_be_domain IS FALSE AND u.user_name_lower = $3)
)
ORDER BY is_default
LIMIT 1
) p ON TRUE
JOIN
projections.login_names3_domains d
ON
u.instance_id = d.instance_id
AND u.resource_owner = d.resource_owner
AND CASE WHEN p.must_be_domain THEN d.name_lower = $2 ELSE TRUE END
),
login_names AS (SELECT
fu.id user_id
, fu.instance_id
, fu.resource_owner
, fu.user_name
, d.name domain_name
, d.is_primary
, p.must_be_domain
, CASE WHEN p.must_be_domain
THEN concat(fu.user_name, '@', d.name)
ELSE fu.user_name
END login_name
FROM
found_users fu
JOIN lateral (
SELECT
p.must_be_domain
FROM
projections.login_names3_policies p
WHERE
fu.instance_id = p.instance_id
AND (
(p.is_default IS TRUE AND p.instance_id = $4)
OR (p.instance_id = $4 AND p.resource_owner = fu.resource_owner)
)
ORDER BY is_default
LIMIT 1
) p ON TRUE
JOIN
projections.login_names3_domains d
ON
fu.instance_id = d.instance_id
AND fu.resource_owner = d.resource_owner
)
SELECT
u.id
, u.creation_date
, u.change_date
, u.resource_owner
, u.sequence
, u.state
, u.type
, u.username
, (SELECT array_agg(ln.login_name)::TEXT[] login_names FROM login_names ln WHERE fu.id = ln.user_id GROUP BY ln.user_id, ln.instance_id) login_names
, (SELECT ln.login_name login_names_lower FROM login_names ln WHERE fu.id = ln.user_id AND ln.is_primary IS TRUE) preferred_login_name
, h.user_id
, h.first_name
, h.last_name
, h.nick_name
, h.display_name
, h.preferred_language
, h.gender
, h.avatar_key
, n.user_id
, n.last_email
, n.verified_email
, n.last_phone
, n.verified_phone
, n.password_set
, count(*) OVER ()
FROM found_users fu
JOIN
projections.users10 u
ON
fu.id = u.id
AND fu.instance_id = u.instance_id
LEFT JOIN
projections.users10_humans h
ON
fu.id = h.user_id
AND fu.instance_id = h.instance_id
LEFT JOIN
projections.users10_notifications n
ON
fu.id = n.user_id
AND fu.instance_id = n.instance_id
WHERE
u.instance_id = $4
;