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)
This commit is contained in:
Silvan
2025-06-06 10:48:29 +02:00
committed by Livio Spring
parent 8b04ddf0e2
commit 8ac4b61ee6
26 changed files with 225 additions and 689 deletions

View File

@@ -84,7 +84,7 @@ func (q *Queries) OIDCSettingsByAggID(ctx context.Context, aggregateID string) (
OIDCSettingsColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
}).ToSql()
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 {

View File

@@ -103,7 +103,7 @@ func (q *Queries) GetOrgMetadataByKey(ctx context.Context, shouldTriggerBulk boo
}
stmt, args, err := query.Where(eq).ToSql()
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 {
@@ -133,7 +133,7 @@ func (q *Queries) SearchOrgMetadata(ctx context.Context, shouldTriggerBulk bool,
query, scan := prepareOrgMetadataListQuery()
stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
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 {

View File

@@ -115,7 +115,7 @@ func (q *Queries) ProjectByID(ctx context.Context, shouldTriggerBulk bool, id st
}
query, args, err := stmt.Where(eq).ToSql()
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 {

View File

@@ -122,7 +122,7 @@ func (q *Queries) ProjectGrantByID(ctx context.Context, shouldTriggerBulk bool,
}
query, args, err := stmt.Where(eq).ToSql()
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 {
@@ -144,7 +144,7 @@ func (q *Queries) ProjectGrantByIDAndGrantedOrg(ctx context.Context, id, granted
}
query, args, err := stmt.Where(eq).ToSql()
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 {

View File

@@ -2,9 +2,7 @@ package projection
import (
"context"
"strings"
sq "github.com/Masterminds/squirrel"
_ "embed"
"github.com/zitadel/zitadel/internal/eventstore"
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
@@ -58,105 +56,8 @@ const (
LoginNamePoliciesInstanceIDCol = "instance_id"
)
var (
policyUsers = sq.Select(
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)"
}
//go:embed login_name_query.sql
var loginNameViewStmt string
type loginNameProjection struct{}
@@ -170,7 +71,7 @@ func (*loginNameProjection) Name() string {
func (*loginNameProjection) Init() *old_handler.Check {
return handler.NewViewCheck(
viewStmt,
loginNameViewStmt,
handler.NewSuffixedTable(
[]*handler.InitColumn{
handler.NewColumn(LoginNameUserIDCol, handler.ColumnTypeText),
@@ -229,7 +130,9 @@ func (*loginNameProjection) Init() *old_handler.Check {
},
handler.NewPrimaryKey(LoginNamePoliciesInstanceIDCol, LoginNamePoliciesResourceOwnerCol),
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),
UserHumanSuffix,
handler.WithForeignKey(handler.NewForeignKeyOfPublicKeys()),
handler.WithIndex(handler.NewIndex("email", []string{HumanUserInstanceIDCol, "LOWER(" + HumanEmailCol + ")"})),
),
handler.NewSuffixedTable([]*handler.InitColumn{
handler.NewColumn(MachineUserIDCol, handler.ColumnTypeText),

View File

@@ -78,7 +78,7 @@ func (q *Queries) GetInstanceRestrictions(ctx context.Context) (restrictions Res
RestrictionsColumnResourceOwner.identifier(): instanceID,
}).ToSql()
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 {
restrictions, err = scan(row)

View File

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

View File

@@ -1204,7 +1204,7 @@ func TestTextQuery_comp(t *testing.T) {
Compare: TextEqualsIgnoreCase,
},
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,
},
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,
},
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,
},
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,
},
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,
},
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,
},
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{
Column: testCol,
Text: "Hurst",
Compare: TextContainsIgnoreCase,
},
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,
},
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{
Column: testCol,
Text: "Hurst",

View File

@@ -132,7 +132,7 @@ func (q *Queries) SecretGeneratorByType(ctx context.Context, generatorType domai
SecretGeneratorColumnInstanceID.identifier(): instanceID,
}).ToSql()
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 {

View File

@@ -67,7 +67,7 @@ func (q *Queries) SecurityPolicy(ctx context.Context) (policy *SecurityPolicy, e
SecurityPolicyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
}).ToSql()
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 {

View File

@@ -182,21 +182,15 @@ var (
userLoginNamesTable = loginNameTable.setAlias("login_names")
userLoginNamesUserIDCol = LoginNameUserIDCol.setTable(userLoginNamesTable)
userLoginNamesNameCol = LoginNameNameCol.setTable(userLoginNamesTable)
userLoginNamesInstanceIDCol = LoginNameInstanceIDCol.setTable(userLoginNamesTable)
userLoginNamesListCol = Column{
name: "loginnames",
name: "login_names",
table: userLoginNamesTable,
}
userLoginNamesLowerListCol = Column{
name: "loginnames_lower",
userPreferredLoginNameCol = Column{
name: "preferred_login_name",
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 (
@@ -441,7 +435,7 @@ func (q *Queries) GetHumanProfile(ctx context.Context, userID string, queries ..
}
stmt, args, err := query.Where(eq).ToSql()
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 {
@@ -465,7 +459,7 @@ func (q *Queries) GetHumanEmail(ctx context.Context, userID string, queries ...S
}
stmt, args, err := query.Where(eq).ToSql()
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 {
@@ -489,7 +483,7 @@ func (q *Queries) GetHumanPhone(ctx context.Context, userID string, queries ...S
}
stmt, args, err := query.Where(eq).ToSql()
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 {
@@ -575,7 +569,7 @@ func (q *Queries) GetNotifyUser(ctx context.Context, shouldTriggered bool, queri
}
stmt, args, err := query.Where(eq).ToSql()
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 {
@@ -593,7 +587,7 @@ func (q *Queries) CountUsers(ctx context.Context, queries *UserSearchQueries) (c
eq := sq.Eq{UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()}
stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
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 {
@@ -634,7 +628,7 @@ func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, f
stmt, args, err := query.ToSql()
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 {
@@ -681,7 +675,7 @@ func (q *Queries) IsUserUnique(ctx context.Context, username, email, resourceOwn
eq := sq.Eq{UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()}
stmt, args, err := query.Where(eq).ToSql()
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 {
@@ -780,12 +774,8 @@ func NewUserPreferredLoginNameSearchQuery(value string, comparison TextCompariso
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) {
// linking queries for the subselect
// linking queries for the sub select
instanceQuery, err := NewColumnComparisonQuery(LoginNameInstanceIDCol, UserInstanceIDCol, ColumnEquals)
if err != nil {
return nil, err
@@ -816,30 +806,16 @@ func triggerUserProjections(ctx context.Context) {
triggerBatch(ctx, projection.UserProjection, projection.LoginNameProjection)
}
func prepareLoginNamesQuery() (string, []interface{}, error) {
return sq.Select(
userLoginNamesUserIDCol.identifier(),
"ARRAY_AGG("+userLoginNamesNameCol.identifier()+")::TEXT[] AS "+userLoginNamesListCol.name,
"ARRAY_AGG(LOWER("+userLoginNamesNameCol.identifier()+"))::TEXT[] AS "+userLoginNamesLowerListCol.name,
userLoginNamesInstanceIDCol.identifier(),
).From(userLoginNamesTable.identifier()).
GroupBy(
userLoginNamesUserIDCol.identifier(),
userLoginNamesInstanceIDCol.identifier(),
).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()
}
var joinLoginNames = `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 = ` + UserIDCol.identifier() +
` AND ln.instance_id = ` + UserInstanceIDCol.identifier() +
`) AS login_names ON TRUE`
func scanUser(row *sql.Row) (*User, error) {
u := new(User)
@@ -939,64 +915,6 @@ func scanUser(row *sql.Row) (*User, error) {
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)) {
return sq.Select(
UserIDCol.identifier(),
@@ -1158,14 +1076,6 @@ func preparePhoneQuery() (sq.SelectBuilder, func(*sql.Row) (*Phone, 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(
UserIDCol.identifier(),
UserCreationDateCol.identifier(),
@@ -1196,14 +1106,7 @@ func prepareNotifyUserQuery() (sq.SelectBuilder, func(*sql.Row) (*NotifyUser, er
From(userTable.identifier()).
LeftJoin(join(HumanUserIDCol, UserIDCol)).
LeftJoin(join(NotifyUserIDCol, 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...).
JoinClause(joinLoginNames).
PlaceholderFormat(sq.Dollar),
scanNotifyUser
}
@@ -1347,14 +1250,6 @@ func prepareUserUniqueQuery() (sq.SelectBuilder, func(*sql.Row) (bool, 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(
UserIDCol.identifier(),
UserCreationDateCol.identifier(),
@@ -1389,14 +1284,7 @@ func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) {
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...).
JoinClause(joinLoginNames).
PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) (*Users, error) {
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
u.id
, u.creation_date
@@ -45,8 +7,8 @@ SELECT
, 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
, login_names.login_names AS login_names
, login_names.preferred_login_name AS preferred_login_name
, h.user_id
, h.first_name
, h.last_name
@@ -79,6 +41,16 @@ LEFT JOIN
ON
u.id = m.user_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
u.id = $1
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()
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 {
@@ -125,7 +125,7 @@ func (q *Queries) SearchUserMetadataForUsers(ctx context.Context, shouldTriggerB
}
stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
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 {
@@ -157,7 +157,7 @@ func (q *Queries) SearchUserMetadata(ctx context.Context, shouldTriggerBulk bool
}
stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
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 {

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
u.id
, u.creation_date
@@ -45,8 +7,8 @@ SELECT
, 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
, login_names.login_names AS login_names
, login_names.preferred_login_name AS preferred_login_name
, h.user_id
, h.first_name
, h.last_name
@@ -73,6 +35,16 @@ LEFT JOIN
ON
u.id = n.user_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
u.id = $1
AND u.instance_id = $2

View File

@@ -110,7 +110,7 @@ func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk
}
stmt, args, err := query.Where(eq).ToSql()
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 {

View File

@@ -222,87 +222,6 @@ func TestUser_userCheckPermission(t *testing.T) {
}
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,` +
` projections.users14.creation_date,` +
` projections.users14.change_date,` +
@@ -397,8 +316,8 @@ var (
` projections.users14.state,` +
` projections.users14.type,` +
` projections.users14.username,` +
` login_names.loginnames,` +
` preferred_login_name.login_name,` +
` login_names.login_names,` +
` login_names.preferred_login_name,` +
` projections.users14_humans.user_id,` +
` projections.users14_humans.first_name,` +
` projections.users14_humans.last_name,` +
@@ -417,12 +336,7 @@ var (
` 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_notifications ON projections.users14.id = projections.users14_notifications.user_id AND projections.users14.instance_id = projections.users14_notifications.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`
` 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`
notifyUserCols = []string{
"id",
"creation_date",
@@ -432,8 +346,8 @@ var (
"state",
"type",
"username",
"loginnames",
"login_name",
"login_names",
"preferred_login_name",
// human
"user_id",
"first_name",
@@ -460,8 +374,8 @@ var (
` projections.users14.state,` +
` projections.users14.type,` +
` projections.users14.username,` +
` login_names.loginnames,` +
` preferred_login_name.login_name,` +
` login_names.login_names,` +
` login_names.preferred_login_name,` +
` projections.users14_humans.user_id,` +
` projections.users14_humans.first_name,` +
` projections.users14_humans.last_name,` +
@@ -485,12 +399,7 @@ var (
` 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`
` 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`
usersCols = []string{
"id",
"creation_date",
@@ -500,8 +409,8 @@ var (
"state",
"type",
"username",
"loginnames",
"login_name",
"login_names",
"preferred_login_name",
// human
"user_id",
"first_name",
@@ -540,240 +449,6 @@ func Test_UserPrepares(t *testing.T) {
want want
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",
prepare: prepareProfileQuery,