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
This commit is contained in:
Silvan
2025-06-06 10:48:29 +02:00
committed by GitHub
parent 647b3b57cf
commit 4df138286b
26 changed files with 225 additions and 689 deletions

49
cmd/setup/58.go Normal file
View File

@@ -0,0 +1,49 @@
package setup
import (
"context"
"database/sql"
"embed"
"fmt"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
)
var (
//go:embed 58/*.sql
replaceLoginNames3View embed.FS
)
type ReplaceLoginNames3View struct {
dbClient *database.DB
}
func (mig *ReplaceLoginNames3View) Execute(ctx context.Context, _ eventstore.Event) error {
var exists bool
err := mig.dbClient.QueryRowContext(ctx, func(r *sql.Row) error {
return r.Scan(&exists)
}, "SELECT exists(SELECT 1 from information_schema.views WHERE table_schema = 'projections' AND table_name = 'login_names3')")
if err != nil || !exists {
return err
}
statements, err := readStatements(replaceLoginNames3View, "58")
if err != nil {
return err
}
for _, stmt := range statements {
logging.WithFields("file", stmt.file, "migration", mig.String()).Info("execute statement")
if _, err := mig.dbClient.ExecContext(ctx, stmt.query); err != nil {
return fmt.Errorf("%s %s: %w", mig.String(), stmt.file, err)
}
}
return nil
}
func (mig *ReplaceLoginNames3View) String() string {
return "58_replace_login_names3_view"
}

View File

@@ -0,0 +1,36 @@
CREATE OR REPLACE VIEW projections.login_names3 AS
SELECT
u.id AS user_id
, CASE
WHEN p.must_be_domain THEN CONCAT(u.user_name, '@', d.name)
ELSE u.user_name
END AS login_name
, COALESCE(d.is_primary, TRUE) AS is_primary
, u.instance_id
FROM
projections.login_names3_users AS u
LEFT JOIN LATERAL (
SELECT
must_be_domain
, is_default
FROM
projections.login_names3_policies AS p
WHERE
(
p.instance_id = u.instance_id
AND NOT p.is_default
AND p.resource_owner = u.resource_owner
) OR (
p.instance_id = u.instance_id
AND p.is_default
)
ORDER BY
p.is_default -- custom first
LIMIT 1
) AS p ON TRUE
LEFT JOIN
projections.login_names3_domains d
ON
p.must_be_domain
AND u.resource_owner = d.resource_owner
AND u.instance_id = d.instance_id

View File

@@ -0,0 +1 @@
CREATE INDEX CONCURRENTLY IF NOT EXISTS login_names3_policies_is_default_owner_idx ON projections.login_names3_policies (instance_id, is_default, resource_owner) INCLUDE (must_be_domain)

View File

@@ -154,6 +154,7 @@ type Steps struct {
s55ExecutionHandlerStart *ExecutionHandlerStart s55ExecutionHandlerStart *ExecutionHandlerStart
s56IDPTemplate6SAMLFederatedLogout *IDPTemplate6SAMLFederatedLogout s56IDPTemplate6SAMLFederatedLogout *IDPTemplate6SAMLFederatedLogout
s57CreateResourceCounts *CreateResourceCounts s57CreateResourceCounts *CreateResourceCounts
s58ReplaceLoginNames3View *ReplaceLoginNames3View
} }
func MustNewSteps(v *viper.Viper) *Steps { func MustNewSteps(v *viper.Viper) *Steps {

View File

@@ -216,6 +216,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s55ExecutionHandlerStart = &ExecutionHandlerStart{dbClient: dbClient} steps.s55ExecutionHandlerStart = &ExecutionHandlerStart{dbClient: dbClient}
steps.s56IDPTemplate6SAMLFederatedLogout = &IDPTemplate6SAMLFederatedLogout{dbClient: dbClient} steps.s56IDPTemplate6SAMLFederatedLogout = &IDPTemplate6SAMLFederatedLogout{dbClient: dbClient}
steps.s57CreateResourceCounts = &CreateResourceCounts{dbClient: dbClient} steps.s57CreateResourceCounts = &CreateResourceCounts{dbClient: dbClient}
steps.s58ReplaceLoginNames3View = &ReplaceLoginNames3View{dbClient: dbClient}
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
logging.OnError(err).Fatal("unable to start projections") logging.OnError(err).Fatal("unable to start projections")
@@ -262,6 +263,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s55ExecutionHandlerStart, steps.s55ExecutionHandlerStart,
steps.s56IDPTemplate6SAMLFederatedLogout, steps.s56IDPTemplate6SAMLFederatedLogout,
steps.s57CreateResourceCounts, steps.s57CreateResourceCounts,
steps.s58ReplaceLoginNames3View,
} { } {
setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed") setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed")
if setupErr != nil { if setupErr != nil {

View File

@@ -84,7 +84,7 @@ func (q *Queries) OIDCSettingsByAggID(ctx context.Context, aggregateID string) (
OIDCSettingsColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), OIDCSettingsColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
}).ToSql() }).ToSql()
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-s9nle", "Errors.Query.SQLStatment") return nil, zerrors.ThrowInternal(err, "QUERY-s9nle", "Errors.Query.SQLStatement")
} }
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {

View File

@@ -103,7 +103,7 @@ func (q *Queries) GetOrgMetadataByKey(ctx context.Context, shouldTriggerBulk boo
} }
stmt, args, err := query.Where(eq).ToSql() stmt, args, err := query.Where(eq).ToSql()
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-aDaG2", "Errors.Query.SQLStatment") return nil, zerrors.ThrowInternal(err, "QUERY-aDaG2", "Errors.Query.SQLStatement")
} }
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
@@ -133,7 +133,7 @@ func (q *Queries) SearchOrgMetadata(ctx context.Context, shouldTriggerBulk bool,
query, scan := prepareOrgMetadataListQuery() query, scan := prepareOrgMetadataListQuery()
stmt, args, err := queries.toQuery(query).Where(eq).ToSql() stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Egbld", "Errors.Query.SQLStatment") return nil, zerrors.ThrowInternal(err, "QUERY-Egbld", "Errors.Query.SQLStatement")
} }
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {

View File

@@ -211,7 +211,7 @@ func (q *Queries) ProjectByID(ctx context.Context, shouldTriggerBulk bool, id st
} }
query, args, err := stmt.Where(eq).ToSql() query, args, err := stmt.Where(eq).ToSql()
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-2m00Q", "Errors.Query.SQLStatment") return nil, zerrors.ThrowInternal(err, "QUERY-2m00Q", "Errors.Query.SQLStatement")
} }
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {

View File

@@ -167,7 +167,7 @@ func (q *Queries) ProjectGrantByID(ctx context.Context, shouldTriggerBulk bool,
} }
query, args, err := stmt.Where(eq).ToSql() query, args, err := stmt.Where(eq).ToSql()
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Nf93d", "Errors.Query.SQLStatment") return nil, zerrors.ThrowInternal(err, "QUERY-Nf93d", "Errors.Query.SQLStatement")
} }
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
@@ -189,7 +189,7 @@ func (q *Queries) ProjectGrantByIDAndGrantedOrg(ctx context.Context, id, granted
} }
query, args, err := stmt.Where(eq).ToSql() query, args, err := stmt.Where(eq).ToSql()
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-MO9fs", "Errors.Query.SQLStatment") return nil, zerrors.ThrowInternal(err, "QUERY-MO9fs", "Errors.Query.SQLStatement")
} }
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {

View File

@@ -2,9 +2,7 @@ package projection
import ( import (
"context" "context"
"strings" _ "embed"
sq "github.com/Masterminds/squirrel"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
@@ -58,105 +56,8 @@ const (
LoginNamePoliciesInstanceIDCol = "instance_id" LoginNamePoliciesInstanceIDCol = "instance_id"
) )
var ( //go:embed login_name_query.sql
policyUsers = sq.Select( var loginNameViewStmt string
alias(
col(usersAlias, LoginNameUserIDCol),
LoginNameUserCol,
),
col(usersAlias, LoginNameUserUserNameCol),
col(usersAlias, LoginNameUserInstanceIDCol),
col(usersAlias, LoginNameUserResourceOwnerCol),
alias(
coalesce(col(policyCustomAlias, LoginNamePoliciesMustBeDomainCol), col(policyDefaultAlias, LoginNamePoliciesMustBeDomainCol)),
LoginNamePoliciesMustBeDomainCol,
),
).From(alias(LoginNameUserProjectionTable, usersAlias)).
LeftJoin(
leftJoin(LoginNamePolicyProjectionTable, policyCustomAlias,
eq(col(policyCustomAlias, LoginNamePoliciesResourceOwnerCol), col(usersAlias, LoginNameUserResourceOwnerCol)),
eq(col(policyCustomAlias, LoginNamePoliciesInstanceIDCol), col(usersAlias, LoginNameUserInstanceIDCol)),
),
).
LeftJoin(
leftJoin(LoginNamePolicyProjectionTable, policyDefaultAlias,
eq(col(policyDefaultAlias, LoginNamePoliciesIsDefaultCol), "true"),
eq(col(policyDefaultAlias, LoginNamePoliciesInstanceIDCol), col(usersAlias, LoginNameUserInstanceIDCol)),
),
)
loginNamesTable = sq.Select(
col(policyUsersAlias, LoginNameUserCol),
col(policyUsersAlias, LoginNameUserUserNameCol),
col(policyUsersAlias, LoginNameUserResourceOwnerCol),
alias(col(policyUsersAlias, LoginNameUserInstanceIDCol),
LoginNameInstanceIDCol),
col(policyUsersAlias, LoginNamePoliciesMustBeDomainCol),
alias(col(domainsAlias, LoginNameDomainNameCol),
domainAlias),
col(domainsAlias, LoginNameDomainIsPrimaryCol),
).FromSelect(policyUsers, policyUsersAlias).
LeftJoin(
leftJoin(LoginNameDomainProjectionTable, domainsAlias,
col(policyUsersAlias, LoginNamePoliciesMustBeDomainCol),
eq(col(policyUsersAlias, LoginNameUserResourceOwnerCol), col(domainsAlias, LoginNameDomainResourceOwnerCol)),
eq(col(policyUsersAlias, LoginNamePoliciesInstanceIDCol), col(domainsAlias, LoginNameDomainInstanceIDCol)),
),
)
viewStmt, _ = sq.Select(
LoginNameUserCol,
alias(
whenThenElse(
LoginNamePoliciesMustBeDomainCol,
concat(LoginNameUserUserNameCol, "'@'", domainAlias),
LoginNameUserUserNameCol),
LoginNameCol),
alias(coalesce(LoginNameDomainIsPrimaryCol, "true"),
LoginNameIsPrimaryCol),
LoginNameInstanceIDCol,
).FromSelect(loginNamesTable, LoginNameTableAlias).MustSql()
)
func col(table, name string) string {
return table + "." + name
}
func alias(col, alias string) string {
return col + " AS " + alias
}
func coalesce(values ...string) string {
str := "COALESCE("
for i, value := range values {
if i > 0 {
str += ", "
}
str += value
}
str += ")"
return str
}
func eq(first, second string) string {
return first + " = " + second
}
func leftJoin(table, alias, on string, and ...string) string {
st := table + " " + alias + " ON " + on
for _, a := range and {
st += " AND " + a
}
return st
}
func concat(strs ...string) string {
return "CONCAT(" + strings.Join(strs, ", ") + ")"
}
func whenThenElse(when, then, el string) string {
return "(CASE WHEN " + when + " THEN " + then + " ELSE " + el + " END)"
}
type loginNameProjection struct{} type loginNameProjection struct{}
@@ -170,7 +71,7 @@ func (*loginNameProjection) Name() string {
func (*loginNameProjection) Init() *old_handler.Check { func (*loginNameProjection) Init() *old_handler.Check {
return handler.NewViewCheck( return handler.NewViewCheck(
viewStmt, loginNameViewStmt,
handler.NewSuffixedTable( handler.NewSuffixedTable(
[]*handler.InitColumn{ []*handler.InitColumn{
handler.NewColumn(LoginNameUserIDCol, handler.ColumnTypeText), handler.NewColumn(LoginNameUserIDCol, handler.ColumnTypeText),
@@ -229,7 +130,9 @@ func (*loginNameProjection) Init() *old_handler.Check {
}, },
handler.NewPrimaryKey(LoginNamePoliciesInstanceIDCol, LoginNamePoliciesResourceOwnerCol), handler.NewPrimaryKey(LoginNamePoliciesInstanceIDCol, LoginNamePoliciesResourceOwnerCol),
loginNamePolicySuffix, loginNamePolicySuffix,
handler.WithIndex(handler.NewIndex("is_default", []string{LoginNamePoliciesResourceOwnerCol, LoginNamePoliciesIsDefaultCol})), // 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))),
), ),
) )
} }

View File

@@ -0,0 +1,35 @@
SELECT
u.id AS user_id
, CASE
WHEN p.must_be_domain THEN CONCAT(u.user_name, '@', d.name)
ELSE u.user_name
END AS login_name
, COALESCE(d.is_primary, TRUE) AS is_primary
, u.instance_id
FROM
projections.login_names3_users AS u
LEFT JOIN LATERAL (
SELECT
must_be_domain
, is_default
FROM
projections.login_names3_policies AS p
WHERE
(
p.instance_id = u.instance_id
AND NOT p.is_default
AND p.resource_owner = u.resource_owner
) OR (
p.instance_id = u.instance_id
AND p.is_default
)
ORDER BY
p.is_default -- custom first
LIMIT 1
) AS p ON TRUE
LEFT JOIN
projections.login_names3_domains d
ON
p.must_be_domain
AND u.resource_owner = d.resource_owner
AND u.instance_id = d.instance_id

View File

@@ -124,6 +124,7 @@ func (*userProjection) Init() *old_handler.Check {
handler.NewPrimaryKey(HumanUserInstanceIDCol, HumanUserIDCol), handler.NewPrimaryKey(HumanUserInstanceIDCol, HumanUserIDCol),
UserHumanSuffix, UserHumanSuffix,
handler.WithForeignKey(handler.NewForeignKeyOfPublicKeys()), handler.WithForeignKey(handler.NewForeignKeyOfPublicKeys()),
handler.WithIndex(handler.NewIndex("email", []string{HumanUserInstanceIDCol, "LOWER(" + HumanEmailCol + ")"})),
), ),
handler.NewSuffixedTable([]*handler.InitColumn{ handler.NewSuffixedTable([]*handler.InitColumn{
handler.NewColumn(MachineUserIDCol, handler.ColumnTypeText), handler.NewColumn(MachineUserIDCol, handler.ColumnTypeText),

View File

@@ -78,7 +78,7 @@ func (q *Queries) GetInstanceRestrictions(ctx context.Context) (restrictions Res
RestrictionsColumnResourceOwner.identifier(): instanceID, RestrictionsColumnResourceOwner.identifier(): instanceID,
}).ToSql() }).ToSql()
if err != nil { if err != nil {
return restrictions, zitade_errors.ThrowInternal(err, "QUERY-XnLMQ", "Errors.Query.SQLStatment") return restrictions, zitade_errors.ThrowInternal(err, "QUERY-XnLMQ", "Errors.Query.SQLStatement")
} }
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
restrictions, err = scan(row) restrictions, err = scan(row)

View File

@@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"reflect" "reflect"
"strings"
"time" "time"
sq "github.com/Masterminds/squirrel" sq "github.com/Masterminds/squirrel"
@@ -334,23 +335,23 @@ func (q *textQuery) comp() sq.Sqlizer {
case TextNotEquals: case TextNotEquals:
return sq.NotEq{q.Column.identifier(): q.Text} return sq.NotEq{q.Column.identifier(): q.Text}
case TextEqualsIgnoreCase: case TextEqualsIgnoreCase:
return sq.ILike{q.Column.identifier(): q.Text} return sq.Like{"LOWER(" + q.Column.identifier() + ")": strings.ToLower(q.Text)}
case TextNotEqualsIgnoreCase: case TextNotEqualsIgnoreCase:
return sq.NotILike{q.Column.identifier(): q.Text} return sq.NotLike{"LOWER(" + q.Column.identifier() + ")": strings.ToLower(q.Text)}
case TextStartsWith: case TextStartsWith:
return sq.Like{q.Column.identifier(): q.Text + "%"} return sq.Like{q.Column.identifier(): q.Text + "%"}
case TextStartsWithIgnoreCase: case TextStartsWithIgnoreCase:
return sq.ILike{q.Column.identifier(): q.Text + "%"} return sq.Like{"LOWER(" + q.Column.identifier() + ")": strings.ToLower(q.Text) + "%"}
case TextEndsWith: case TextEndsWith:
return sq.Like{q.Column.identifier(): "%" + q.Text} return sq.Like{q.Column.identifier(): "%" + q.Text}
case TextEndsWithIgnoreCase: case TextEndsWithIgnoreCase:
return sq.ILike{q.Column.identifier(): "%" + q.Text} return sq.Like{"LOWER(" + q.Column.identifier() + ")": "%" + strings.ToLower(q.Text)}
case TextContains: case TextContains:
return sq.Like{q.Column.identifier(): "%" + q.Text + "%"} return sq.Like{q.Column.identifier(): "%" + q.Text + "%"}
case TextContainsIgnoreCase: case TextContainsIgnoreCase:
return sq.ILike{q.Column.identifier(): "%" + q.Text + "%"} return sq.Like{"LOWER(" + q.Column.identifier() + ")": "%" + strings.ToLower(q.Text) + "%"}
case TextListContains: case TextListContains:
return &listContains{col: q.Column, args: []interface{}{q.Text}} return &listContains{col: q.Column, args: []any{q.Text}}
case textCompareMax: case textCompareMax:
return nil return nil
} }

View File

@@ -1204,7 +1204,7 @@ func TestTextQuery_comp(t *testing.T) {
Compare: TextEqualsIgnoreCase, Compare: TextEqualsIgnoreCase,
}, },
want: want{ want: want{
query: sq.ILike{"test_table.test_col": "Hurst"}, query: sq.Like{"LOWER(test_table.test_col)": "hurst"},
}, },
}, },
{ {
@@ -1226,7 +1226,7 @@ func TestTextQuery_comp(t *testing.T) {
Compare: TextNotEqualsIgnoreCase, Compare: TextNotEqualsIgnoreCase,
}, },
want: want{ want: want{
query: sq.NotILike{"test_table.test_col": "Hurst"}, query: sq.NotLike{"LOWER(test_table.test_col)": "hurst"},
}, },
}, },
{ {
@@ -1237,7 +1237,7 @@ func TestTextQuery_comp(t *testing.T) {
Compare: TextEqualsIgnoreCase, Compare: TextEqualsIgnoreCase,
}, },
want: want{ want: want{
query: sq.ILike{"test_table.test_col": "Hu\\%\\%rst"}, query: sq.Like{"LOWER(test_table.test_col)": "hu\\%\\%rst"},
}, },
}, },
{ {
@@ -1270,7 +1270,7 @@ func TestTextQuery_comp(t *testing.T) {
Compare: TextStartsWithIgnoreCase, Compare: TextStartsWithIgnoreCase,
}, },
want: want{ want: want{
query: sq.ILike{"test_table.test_col": "Hurst%"}, query: sq.Like{"LOWER(test_table.test_col)": "hurst%"},
}, },
}, },
{ {
@@ -1281,7 +1281,7 @@ func TestTextQuery_comp(t *testing.T) {
Compare: TextStartsWithIgnoreCase, Compare: TextStartsWithIgnoreCase,
}, },
want: want{ want: want{
query: sq.ILike{"test_table.test_col": "Hurst\\%%"}, query: sq.Like{"LOWER(test_table.test_col)": "hurst\\%%"},
}, },
}, },
{ {
@@ -1314,7 +1314,7 @@ func TestTextQuery_comp(t *testing.T) {
Compare: TextEndsWithIgnoreCase, Compare: TextEndsWithIgnoreCase,
}, },
want: want{ want: want{
query: sq.ILike{"test_table.test_col": "%Hurst"}, query: sq.Like{"LOWER(test_table.test_col)": "%hurst"},
}, },
}, },
{ {
@@ -1325,7 +1325,7 @@ func TestTextQuery_comp(t *testing.T) {
Compare: TextEndsWithIgnoreCase, Compare: TextEndsWithIgnoreCase,
}, },
want: want{ want: want{
query: sq.ILike{"test_table.test_col": "%\\%Hurst"}, query: sq.Like{"LOWER(test_table.test_col)": "%\\%hurst"},
}, },
}, },
{ {
@@ -1351,14 +1351,14 @@ func TestTextQuery_comp(t *testing.T) {
}, },
}, },
{ {
name: "containts ignore case", name: "contains ignore case",
fields: fields{ fields: fields{
Column: testCol, Column: testCol,
Text: "Hurst", Text: "Hurst",
Compare: TextContainsIgnoreCase, Compare: TextContainsIgnoreCase,
}, },
want: want{ want: want{
query: sq.ILike{"test_table.test_col": "%Hurst%"}, query: sq.Like{"LOWER(test_table.test_col)": "%hurst%"},
}, },
}, },
{ {
@@ -1369,11 +1369,11 @@ func TestTextQuery_comp(t *testing.T) {
Compare: TextContainsIgnoreCase, Compare: TextContainsIgnoreCase,
}, },
want: want{ want: want{
query: sq.ILike{"test_table.test_col": "%\\%Hurst\\%%"}, query: sq.Like{"LOWER(test_table.test_col)": "%\\%hurst\\%%"},
}, },
}, },
{ {
name: "list containts", name: "list contains",
fields: fields{ fields: fields{
Column: testCol, Column: testCol,
Text: "Hurst", Text: "Hurst",

View File

@@ -132,7 +132,7 @@ func (q *Queries) SecretGeneratorByType(ctx context.Context, generatorType domai
SecretGeneratorColumnInstanceID.identifier(): instanceID, SecretGeneratorColumnInstanceID.identifier(): instanceID,
}).ToSql() }).ToSql()
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-3k99f", "Errors.Query.SQLStatment") return nil, zerrors.ThrowInternal(err, "QUERY-3k99f", "Errors.Query.SQLStatement")
} }
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {

View File

@@ -67,7 +67,7 @@ func (q *Queries) SecurityPolicy(ctx context.Context) (policy *SecurityPolicy, e
SecurityPolicyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), SecurityPolicyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
}).ToSql() }).ToSql()
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Sf6d1", "Errors.Query.SQLStatment") return nil, zerrors.ThrowInternal(err, "QUERY-Sf6d1", "Errors.Query.SQLStatement")
} }
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {

View File

@@ -200,21 +200,15 @@ var (
userLoginNamesTable = loginNameTable.setAlias("login_names") userLoginNamesTable = loginNameTable.setAlias("login_names")
userLoginNamesUserIDCol = LoginNameUserIDCol.setTable(userLoginNamesTable) userLoginNamesUserIDCol = LoginNameUserIDCol.setTable(userLoginNamesTable)
userLoginNamesNameCol = LoginNameNameCol.setTable(userLoginNamesTable)
userLoginNamesInstanceIDCol = LoginNameInstanceIDCol.setTable(userLoginNamesTable) userLoginNamesInstanceIDCol = LoginNameInstanceIDCol.setTable(userLoginNamesTable)
userLoginNamesListCol = Column{ userLoginNamesListCol = Column{
name: "loginnames", name: "login_names",
table: userLoginNamesTable, table: userLoginNamesTable,
} }
userLoginNamesLowerListCol = Column{ userPreferredLoginNameCol = Column{
name: "loginnames_lower", name: "preferred_login_name",
table: userLoginNamesTable, table: userLoginNamesTable,
} }
userPreferredLoginNameTable = loginNameTable.setAlias("preferred_login_name")
userPreferredLoginNameUserIDCol = LoginNameUserIDCol.setTable(userPreferredLoginNameTable)
userPreferredLoginNameCol = LoginNameNameCol.setTable(userPreferredLoginNameTable)
userPreferredLoginNameIsPrimaryCol = LoginNameIsPrimaryCol.setTable(userPreferredLoginNameTable)
userPreferredLoginNameInstanceIDCol = LoginNameInstanceIDCol.setTable(userPreferredLoginNameTable)
) )
var ( var (
@@ -459,7 +453,7 @@ func (q *Queries) GetHumanProfile(ctx context.Context, userID string, queries ..
} }
stmt, args, err := query.Where(eq).ToSql() stmt, args, err := query.Where(eq).ToSql()
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatment") return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatement")
} }
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
@@ -483,7 +477,7 @@ func (q *Queries) GetHumanEmail(ctx context.Context, userID string, queries ...S
} }
stmt, args, err := query.Where(eq).ToSql() stmt, args, err := query.Where(eq).ToSql()
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-BHhj3", "Errors.Query.SQLStatment") return nil, zerrors.ThrowInternal(err, "QUERY-BHhj3", "Errors.Query.SQLStatement")
} }
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
@@ -507,7 +501,7 @@ func (q *Queries) GetHumanPhone(ctx context.Context, userID string, queries ...S
} }
stmt, args, err := query.Where(eq).ToSql() stmt, args, err := query.Where(eq).ToSql()
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatment") return nil, zerrors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatement")
} }
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
@@ -593,7 +587,7 @@ func (q *Queries) GetNotifyUser(ctx context.Context, shouldTriggered bool, queri
} }
stmt, args, err := query.Where(eq).ToSql() stmt, args, err := query.Where(eq).ToSql()
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Err3g", "Errors.Query.SQLStatment") return nil, zerrors.ThrowInternal(err, "QUERY-Err3g", "Errors.Query.SQLStatement")
} }
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
@@ -611,7 +605,7 @@ func (q *Queries) CountUsers(ctx context.Context, queries *UserSearchQueries) (c
eq := sq.Eq{UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()} eq := sq.Eq{UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()}
stmt, args, err := queries.toQuery(query).Where(eq).ToSql() stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
if err != nil { if err != nil {
return 0, zerrors.ThrowInternal(err, "QUERY-w3Dx", "Errors.Query.SQLStatment") return 0, zerrors.ThrowInternal(err, "QUERY-w3Dx", "Errors.Query.SQLStatement")
} }
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
@@ -646,7 +640,7 @@ func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, p
UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
}).ToSql() }).ToSql()
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatment") return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatement")
} }
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
@@ -693,7 +687,7 @@ func (q *Queries) IsUserUnique(ctx context.Context, username, email, resourceOwn
eq := sq.Eq{UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()} eq := sq.Eq{UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()}
stmt, args, err := query.Where(eq).ToSql() stmt, args, err := query.Where(eq).ToSql()
if err != nil { if err != nil {
return false, zerrors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatment") return false, zerrors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatement")
} }
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
@@ -792,10 +786,6 @@ func NewUserPreferredLoginNameSearchQuery(value string, comparison TextCompariso
return NewTextQuery(userPreferredLoginNameCol, value, comparison) return NewTextQuery(userPreferredLoginNameCol, value, comparison)
} }
func NewUserLoginNamesSearchQuery(value string) (SearchQuery, error) {
return NewTextQuery(userLoginNamesLowerListCol, strings.ToLower(value), TextListContains)
}
func NewUserLoginNameExistsQuery(value string, comparison TextComparison) (SearchQuery, error) { func NewUserLoginNameExistsQuery(value string, comparison TextComparison) (SearchQuery, error) {
// linking queries for the sub select // linking queries for the sub select
instanceQuery, err := NewColumnComparisonQuery(LoginNameInstanceIDCol, UserInstanceIDCol, ColumnEquals) instanceQuery, err := NewColumnComparisonQuery(LoginNameInstanceIDCol, UserInstanceIDCol, ColumnEquals)
@@ -828,30 +818,16 @@ func triggerUserProjections(ctx context.Context) {
triggerBatch(ctx, projection.UserProjection, projection.LoginNameProjection) triggerBatch(ctx, projection.UserProjection, projection.LoginNameProjection)
} }
func prepareLoginNamesQuery() (string, []interface{}, error) { var joinLoginNames = `LEFT JOIN LATERAL (` +
return sq.Select( `SELECT` +
userLoginNamesUserIDCol.identifier(), ` ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names,` +
"ARRAY_AGG("+userLoginNamesNameCol.identifier()+")::TEXT[] AS "+userLoginNamesListCol.name, ` MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name` +
"ARRAY_AGG(LOWER("+userLoginNamesNameCol.identifier()+"))::TEXT[] AS "+userLoginNamesLowerListCol.name, ` FROM` +
userLoginNamesInstanceIDCol.identifier(), ` projections.login_names3 AS ln` +
).From(userLoginNamesTable.identifier()). ` WHERE` +
GroupBy( ` ln.user_id = ` + UserIDCol.identifier() +
userLoginNamesUserIDCol.identifier(), ` AND ln.instance_id = ` + UserInstanceIDCol.identifier() +
userLoginNamesInstanceIDCol.identifier(), `) AS login_names ON TRUE`
).ToSql()
}
func preparePreferredLoginNamesQuery() (string, []interface{}, error) {
return sq.Select(
userPreferredLoginNameUserIDCol.identifier(),
userPreferredLoginNameCol.identifier(),
userPreferredLoginNameInstanceIDCol.identifier(),
).From(userPreferredLoginNameTable.identifier()).
Where(sq.Eq{
userPreferredLoginNameIsPrimaryCol.identifier(): true,
},
).ToSql()
}
func scanUser(row *sql.Row) (*User, error) { func scanUser(row *sql.Row) (*User, error) {
u := new(User) u := new(User)
@@ -951,64 +927,6 @@ func scanUser(row *sql.Row) (*User, error) {
return u, nil return u, nil
} }
func prepareUserQuery() (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(),
HumanPasswordChangeRequiredCol.identifier(),
HumanPasswordChangedCol.identifier(),
HumanMFAInitSkippedCol.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(),
preferredLoginNameArgs...).
PlaceholderFormat(sq.Dollar),
scanUser
}
func prepareProfileQuery() (sq.SelectBuilder, func(*sql.Row) (*Profile, error)) { func prepareProfileQuery() (sq.SelectBuilder, func(*sql.Row) (*Profile, error)) {
return sq.Select( return sq.Select(
UserIDCol.identifier(), UserIDCol.identifier(),
@@ -1170,14 +1088,6 @@ func preparePhoneQuery() (sq.SelectBuilder, func(*sql.Row) (*Phone, error)) {
} }
func prepareNotifyUserQuery() (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, error)) { func prepareNotifyUserQuery() (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, 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( return sq.Select(
UserIDCol.identifier(), UserIDCol.identifier(),
UserCreationDateCol.identifier(), UserCreationDateCol.identifier(),
@@ -1208,14 +1118,7 @@ func prepareNotifyUserQuery() (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, er
From(userTable.identifier()). From(userTable.identifier()).
LeftJoin(join(HumanUserIDCol, UserIDCol)). LeftJoin(join(HumanUserIDCol, UserIDCol)).
LeftJoin(join(NotifyUserIDCol, UserIDCol)). LeftJoin(join(NotifyUserIDCol, UserIDCol)).
LeftJoin("("+loginNamesQuery+") AS "+userLoginNamesTable.alias+" ON "+ JoinClause(joinLoginNames).
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(),
preferredLoginNameArgs...).
PlaceholderFormat(sq.Dollar), PlaceholderFormat(sq.Dollar),
scanNotifyUser scanNotifyUser
} }
@@ -1359,14 +1262,6 @@ func prepareUserUniqueQuery() (sq.SelectBuilder, func(*sql.Row) (bool, error)) {
} }
func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, 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( return sq.Select(
UserIDCol.identifier(), UserIDCol.identifier(),
UserCreationDateCol.identifier(), UserCreationDateCol.identifier(),
@@ -1401,14 +1296,7 @@ func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) {
From(userTable.identifier()). From(userTable.identifier()).
LeftJoin(join(HumanUserIDCol, UserIDCol)). LeftJoin(join(HumanUserIDCol, UserIDCol)).
LeftJoin(join(MachineUserIDCol, UserIDCol)). LeftJoin(join(MachineUserIDCol, UserIDCol)).
LeftJoin("("+loginNamesQuery+") AS "+userLoginNamesTable.alias+" ON "+ JoinClause(joinLoginNames).
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(),
preferredLoginNameArgs...).
PlaceholderFormat(sq.Dollar), PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) (*Users, error) { func(rows *sql.Rows) (*Users, error) {
users := make([]*User, 0) users := make([]*User, 0)

View File

@@ -1,41 +1,3 @@
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 = $3)
OR (p.instance_id = $3 AND p.resource_owner = u.resource_owner)
)
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
WHERE
u.id = $1
AND (u.resource_owner = $2 OR $2 = '')
AND u.instance_id = $3
)
SELECT SELECT
u.id u.id
, u.creation_date , u.creation_date
@@ -45,8 +7,8 @@ SELECT
, u.state , u.state
, u.type , u.type
, u.username , 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 , login_names.login_names AS login_names
, (SELECT ln.login_name login_names_lower FROM login_names ln WHERE ln.is_primary IS TRUE) preferred_login_name , login_names.preferred_login_name AS preferred_login_name
, h.user_id , h.user_id
, h.first_name , h.first_name
, h.last_name , h.last_name
@@ -79,6 +41,16 @@ LEFT JOIN
ON ON
u.id = m.user_id u.id = m.user_id
AND u.instance_id = m.instance_id AND u.instance_id = m.instance_id
LEFT JOIN LATERAL (
SELECT
ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names,
MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name
FROM
projections.login_names3 AS ln
WHERE
ln.user_id = u.id
AND ln.instance_id = u.instance_id
) AS login_names ON TRUE
WHERE WHERE
u.id = $1 u.id = $1
AND (u.resource_owner = $2 OR $2 = '') AND (u.resource_owner = $2 OR $2 = '')

View File

@@ -97,7 +97,7 @@ func (q *Queries) GetUserMetadataByKey(ctx context.Context, shouldTriggerBulk bo
} }
stmt, args, err := query.Where(eq).ToSql() stmt, args, err := query.Where(eq).ToSql()
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-aDGG2", "Errors.Query.SQLStatment") return nil, zerrors.ThrowInternal(err, "QUERY-aDGG2", "Errors.Query.SQLStatement")
} }
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
@@ -125,7 +125,7 @@ func (q *Queries) SearchUserMetadataForUsers(ctx context.Context, shouldTriggerB
} }
stmt, args, err := queries.toQuery(query).Where(eq).ToSql() stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Egbgd", "Errors.Query.SQLStatment") return nil, zerrors.ThrowInternal(err, "QUERY-Egbgd", "Errors.Query.SQLStatement")
} }
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
@@ -157,7 +157,7 @@ func (q *Queries) SearchUserMetadata(ctx context.Context, shouldTriggerBulk bool
} }
stmt, args, err := queries.toQuery(query).Where(eq).ToSql() stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Egbgd", "Errors.Query.SQLStatment") return nil, zerrors.ThrowInternal(err, "QUERY-Egbgd", "Errors.Query.SQLStatement")
} }
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {

View File

@@ -1,41 +1,3 @@
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)
)
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
WHERE
u.instance_id = $2
AND u.id = $1
)
SELECT SELECT
u.id u.id
, u.creation_date , u.creation_date
@@ -45,8 +7,8 @@ SELECT
, u.state , u.state
, u.type , u.type
, u.username , 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 , login_names.login_names AS login_names
, (SELECT ln.login_name login_names_lower FROM login_names ln WHERE ln.is_primary IS TRUE) preferred_login_name , login_names.preferred_login_name AS preferred_login_name
, h.user_id , h.user_id
, h.first_name , h.first_name
, h.last_name , h.last_name
@@ -73,6 +35,16 @@ LEFT JOIN
ON ON
u.id = n.user_id u.id = n.user_id
AND u.instance_id = n.instance_id AND u.instance_id = n.instance_id
LEFT JOIN LATERAL (
SELECT
ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names,
MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name
FROM
projections.login_names3 AS ln
WHERE
ln.user_id = u.id
AND ln.instance_id = u.instance_id
) AS login_names ON TRUE
WHERE WHERE
u.id = $1 u.id = $1
AND u.instance_id = $2 AND u.instance_id = $2

View File

@@ -118,7 +118,7 @@ func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk
} }
stmt, args, err := query.Where(eq).ToSql() stmt, args, err := query.Where(eq).ToSql()
if err != nil { if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Dgfb4", "Errors.Query.SQLStatment") return nil, zerrors.ThrowInternal(err, "QUERY-Dgfb4", "Errors.Query.SQLStatement")
} }
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {

View File

@@ -222,87 +222,6 @@ func TestUser_userCheckPermission(t *testing.T) {
} }
var ( var (
loginNamesQuery = `SELECT login_names.user_id, ARRAY_AGG(login_names.login_name)::TEXT[] AS loginnames, ARRAY_AGG(LOWER(login_names.login_name))::TEXT[] AS loginnames_lower, login_names.instance_id` +
` FROM projections.login_names3 AS login_names` +
` GROUP BY login_names.user_id, login_names.instance_id`
preferredLoginNameQuery = `SELECT preferred_login_name.user_id, preferred_login_name.login_name, preferred_login_name.instance_id` +
` FROM projections.login_names3 AS preferred_login_name` +
` WHERE preferred_login_name.is_primary = $1`
userQuery = `SELECT projections.users14.id,` +
` projections.users14.creation_date,` +
` projections.users14.change_date,` +
` projections.users14.resource_owner,` +
` projections.users14.sequence,` +
` projections.users14.state,` +
` projections.users14.type,` +
` projections.users14.username,` +
` login_names.loginnames,` +
` preferred_login_name.login_name,` +
` projections.users14_humans.user_id,` +
` projections.users14_humans.first_name,` +
` projections.users14_humans.last_name,` +
` projections.users14_humans.nick_name,` +
` projections.users14_humans.display_name,` +
` projections.users14_humans.preferred_language,` +
` projections.users14_humans.gender,` +
` projections.users14_humans.avatar_key,` +
` projections.users14_humans.email,` +
` projections.users14_humans.is_email_verified,` +
` projections.users14_humans.phone,` +
` projections.users14_humans.is_phone_verified,` +
` projections.users14_humans.password_change_required,` +
` projections.users14_humans.password_changed,` +
` projections.users14_humans.mfa_init_skipped,` +
` projections.users14_machines.user_id,` +
` projections.users14_machines.name,` +
` projections.users14_machines.description,` +
` projections.users14_machines.secret,` +
` projections.users14_machines.access_token_type,` +
` COUNT(*) OVER ()` +
` FROM projections.users14` +
` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` +
` LEFT JOIN projections.users14_machines ON projections.users14.id = projections.users14_machines.user_id AND projections.users14.instance_id = projections.users14_machines.instance_id` +
` LEFT JOIN` +
` (` + loginNamesQuery + `) AS login_names` +
` ON login_names.user_id = projections.users14.id AND login_names.instance_id = projections.users14.instance_id` +
` LEFT JOIN` +
` (` + preferredLoginNameQuery + `) AS preferred_login_name` +
` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id`
userCols = []string{
"id",
"creation_date",
"change_date",
"resource_owner",
"sequence",
"state",
"type",
"username",
"loginnames",
"login_name",
// human
"user_id",
"first_name",
"last_name",
"nick_name",
"display_name",
"preferred_language",
"gender",
"avatar_key",
"email",
"is_email_verified",
"phone",
"is_phone_verified",
"password_change_required",
"password_changed",
"mfa_init_skipped",
// machine
"user_id",
"name",
"description",
"secret",
"access_token_type",
"count",
}
profileQuery = `SELECT projections.users14.id,` + profileQuery = `SELECT projections.users14.id,` +
` projections.users14.creation_date,` + ` projections.users14.creation_date,` +
` projections.users14.change_date,` + ` projections.users14.change_date,` +
@@ -397,8 +316,8 @@ var (
` projections.users14.state,` + ` projections.users14.state,` +
` projections.users14.type,` + ` projections.users14.type,` +
` projections.users14.username,` + ` projections.users14.username,` +
` login_names.loginnames,` + ` login_names.login_names,` +
` preferred_login_name.login_name,` + ` login_names.preferred_login_name,` +
` projections.users14_humans.user_id,` + ` projections.users14_humans.user_id,` +
` projections.users14_humans.first_name,` + ` projections.users14_humans.first_name,` +
` projections.users14_humans.last_name,` + ` projections.users14_humans.last_name,` +
@@ -417,12 +336,7 @@ var (
` FROM projections.users14` + ` FROM projections.users14` +
` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` +
` LEFT JOIN projections.users14_notifications ON projections.users14.id = projections.users14_notifications.user_id AND projections.users14.instance_id = projections.users14_notifications.instance_id` + ` LEFT JOIN projections.users14_notifications ON projections.users14.id = projections.users14_notifications.user_id AND projections.users14.instance_id = projections.users14_notifications.instance_id` +
` LEFT JOIN` + ` LEFT JOIN LATERAL (SELECT ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names, MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name FROM projections.login_names3 AS ln WHERE ln.user_id = projections.users14.id AND ln.instance_id = projections.users14.instance_id) AS login_names ON TRUE`
` (` + loginNamesQuery + `) AS login_names` +
` ON login_names.user_id = projections.users14.id AND login_names.instance_id = projections.users14.instance_id` +
` LEFT JOIN` +
` (` + preferredLoginNameQuery + `) AS preferred_login_name` +
` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id`
notifyUserCols = []string{ notifyUserCols = []string{
"id", "id",
"creation_date", "creation_date",
@@ -432,8 +346,8 @@ var (
"state", "state",
"type", "type",
"username", "username",
"loginnames", "login_names",
"login_name", "preferred_login_name",
// human // human
"user_id", "user_id",
"first_name", "first_name",
@@ -460,8 +374,8 @@ var (
` projections.users14.state,` + ` projections.users14.state,` +
` projections.users14.type,` + ` projections.users14.type,` +
` projections.users14.username,` + ` projections.users14.username,` +
` login_names.loginnames,` + ` login_names.login_names,` +
` preferred_login_name.login_name,` + ` login_names.preferred_login_name,` +
` projections.users14_humans.user_id,` + ` projections.users14_humans.user_id,` +
` projections.users14_humans.first_name,` + ` projections.users14_humans.first_name,` +
` projections.users14_humans.last_name,` + ` projections.users14_humans.last_name,` +
@@ -485,12 +399,7 @@ var (
` FROM projections.users14` + ` FROM projections.users14` +
` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` + ` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` +
` LEFT JOIN projections.users14_machines ON projections.users14.id = projections.users14_machines.user_id AND projections.users14.instance_id = projections.users14_machines.instance_id` + ` LEFT JOIN projections.users14_machines ON projections.users14.id = projections.users14_machines.user_id AND projections.users14.instance_id = projections.users14_machines.instance_id` +
` LEFT JOIN` + ` LEFT JOIN LATERAL (SELECT ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names, MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name FROM projections.login_names3 AS ln WHERE ln.user_id = projections.users14.id AND ln.instance_id = projections.users14.instance_id) AS login_names ON TRUE`
` (` + loginNamesQuery + `) AS login_names` +
` ON login_names.user_id = projections.users14.id AND login_names.instance_id = projections.users14.instance_id` +
` LEFT JOIN` +
` (` + preferredLoginNameQuery + `) AS preferred_login_name` +
` ON preferred_login_name.user_id = projections.users14.id AND preferred_login_name.instance_id = projections.users14.instance_id`
usersCols = []string{ usersCols = []string{
"id", "id",
"creation_date", "creation_date",
@@ -500,8 +409,8 @@ var (
"state", "state",
"type", "type",
"username", "username",
"loginnames", "login_names",
"login_name", "preferred_login_name",
// human // human
"user_id", "user_id",
"first_name", "first_name",
@@ -540,240 +449,6 @@ func Test_UserPrepares(t *testing.T) {
want want want want
object interface{} object interface{}
}{ }{
{
name: "prepareUserQuery no result",
prepare: prepareUserQuery,
want: want{
sqlExpectations: mockQueryScanErr(
regexp.QuoteMeta(userQuery),
nil,
nil,
),
err: func(err error) (error, bool) {
if !zerrors.IsNotFound(err) {
return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false
}
return nil, true
},
},
object: (*User)(nil),
},
{
name: "prepareUserQuery human found",
prepare: prepareUserQuery,
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(userQuery),
userCols,
[]driver.Value{
"id",
testNow,
testNow,
"resource_owner",
uint64(20211108),
domain.UserStateActive,
domain.UserTypeHuman,
"username",
database.TextArray[string]{"login_name1", "login_name2"},
"login_name1",
// human
"id",
"first_name",
"last_name",
"nick_name",
"display_name",
"de",
domain.GenderUnspecified,
"avatar_key",
"email",
true,
"phone",
true,
true,
testNow,
testNow,
// machine
nil,
nil,
nil,
nil,
nil,
1,
},
),
},
object: &User{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
ResourceOwner: "resource_owner",
Sequence: 20211108,
State: domain.UserStateActive,
Type: domain.UserTypeHuman,
Username: "username",
LoginNames: database.TextArray[string]{"login_name1", "login_name2"},
PreferredLoginName: "login_name1",
Human: &Human{
FirstName: "first_name",
LastName: "last_name",
NickName: "nick_name",
DisplayName: "display_name",
AvatarKey: "avatar_key",
PreferredLanguage: language.German,
Gender: domain.GenderUnspecified,
Email: "email",
IsEmailVerified: true,
Phone: "phone",
IsPhoneVerified: true,
PasswordChangeRequired: true,
PasswordChanged: testNow,
MFAInitSkipped: testNow,
},
},
},
{
name: "prepareUserQuery machine found",
prepare: prepareUserQuery,
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(userQuery),
userCols,
[]driver.Value{
"id",
testNow,
testNow,
"resource_owner",
uint64(20211108),
domain.UserStateActive,
domain.UserTypeMachine,
"username",
database.TextArray[string]{"login_name1", "login_name2"},
"login_name1",
// human
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
// machine
"id",
"name",
"description",
nil,
domain.OIDCTokenTypeBearer,
1,
},
),
},
object: &User{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
ResourceOwner: "resource_owner",
Sequence: 20211108,
State: domain.UserStateActive,
Type: domain.UserTypeMachine,
Username: "username",
LoginNames: database.TextArray[string]{"login_name1", "login_name2"},
PreferredLoginName: "login_name1",
Machine: &Machine{
Name: "name",
Description: "description",
EncodedSecret: "",
AccessTokenType: domain.OIDCTokenTypeBearer,
},
},
},
{
name: "prepareUserQuery machine with secret found",
prepare: prepareUserQuery,
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(userQuery),
userCols,
[]driver.Value{
"id",
testNow,
testNow,
"resource_owner",
uint64(20211108),
domain.UserStateActive,
domain.UserTypeMachine,
"username",
database.TextArray[string]{"login_name1", "login_name2"},
"login_name1",
// human
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
// machine
"id",
"name",
"description",
"secret",
domain.OIDCTokenTypeBearer,
1,
},
),
},
object: &User{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
ResourceOwner: "resource_owner",
Sequence: 20211108,
State: domain.UserStateActive,
Type: domain.UserTypeMachine,
Username: "username",
LoginNames: database.TextArray[string]{"login_name1", "login_name2"},
PreferredLoginName: "login_name1",
Machine: &Machine{
Name: "name",
Description: "description",
EncodedSecret: "secret",
AccessTokenType: domain.OIDCTokenTypeBearer,
},
},
},
{
name: "prepareUserQuery sql err",
prepare: prepareUserQuery,
want: want{
sqlExpectations: mockQueryErr(
regexp.QuoteMeta(userQuery),
sql.ErrConnDone,
),
err: func(err error) (error, bool) {
if !errors.Is(err, sql.ErrConnDone) {
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
}
return nil, true
},
},
object: (*User)(nil),
},
{ {
name: "prepareProfileQuery no result", name: "prepareProfileQuery no result",
prepare: prepareProfileQuery, prepare: prepareProfileQuery,

View File

@@ -13,7 +13,7 @@ export function createOrg(accessToken: string): Promise<Org> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let response = http.asyncRequest( let response = http.asyncRequest(
'POST', 'POST',
url('/v2beta/organizations'), url('/v2/organizations'),
JSON.stringify({ JSON.stringify({
name: `load-test-${new Date(Date.now()).toISOString()}`, name: `load-test-${new Date(Date.now()).toISOString()}`,
}), }),

View File

@@ -15,7 +15,7 @@ export async function setup() {
} }
export default async function (data: any) { export default async function (data: any) {
const human = await createHuman(`vu-${__VU}`, data.org, data.tokens.accessToken); const human = await createHuman(`vu-${__VU}-${new Date(Date.now()).getTime()}`, data.org, data.tokens.accessToken);
const updateRes = await updateHuman( const updateRes = await updateHuman(
{ {
profile: { profile: {

View File

@@ -30,7 +30,7 @@ export function createHuman(username: string, org: Org, accessToken: string): Pr
familyName: 'Zitizen', familyName: 'Zitizen',
}, },
email: { email: {
email: `zitizen-@caos.ch`, email: `${username}@zitadel.com`,
isVerified: true, isVerified: true,
}, },
password: { password: {
@@ -50,11 +50,11 @@ export function createHuman(username: string, org: Org, accessToken: string): Pr
response response
.then((res) => { .then((res) => {
check(res, { check(res, {
'create user is status ok': (r) => r.status === 201, 'create user is status ok': (r) => r.status === 200,
}) || reject(`unable to create user(username: ${username}) status: ${res.status} body: ${res.body}`); }) || reject(`unable to create user(username: ${username}) status: ${res.status} body: ${res.body}`);
createHumanTrend.add(res.timings.duration); createHumanTrend.add(res.timings.duration);
const user = http.get(url(`/v2beta/users/${res.json('userId')!}`), { const user = http.get(url(`/v2/users/${res.json('userId')!}`), {
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',