fix(usernames): sync index active collectible aliases
This commit is contained in:
parent
2f995f607a
commit
5156b17c1b
29 changed files with 657 additions and 112 deletions
|
|
@ -395,6 +395,10 @@ WHERE c.id = ANY($2::bigint[]) AND NOT c.deleted`, viewerUserID, ids)
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
publicUsernameIDs, err := activeCollectibleUsernamePeerIDs(ctx, s.db, peerUsernameTypeChannel, remaining)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, channel := range channels {
|
||||
if member, ok := linkedGuests[channel.ID]; ok {
|
||||
views[channel.ID] = domain.ChannelView{
|
||||
|
|
@ -427,7 +431,8 @@ WHERE c.id = ANY($2::bigint[]) AND NOT c.deleted`, viewerUserID, ids)
|
|||
continue
|
||||
}
|
||||
}
|
||||
if !publicPreviewableChannel(channel) {
|
||||
_, hasActiveUsername := publicUsernameIDs[channel.ID]
|
||||
if !publicPreviewableChannel(channel, hasActiveUsername) {
|
||||
continue
|
||||
}
|
||||
existing, found := previewMembers[channel.ID]
|
||||
|
|
@ -514,10 +519,10 @@ func finishChannelScan(ch *domain.Channel, rights, reactionPolicy string, wallpa
|
|||
}
|
||||
}
|
||||
|
||||
func publicPreviewableChannel(channel domain.Channel) bool {
|
||||
func publicPreviewableChannel(channel domain.Channel, hasActiveUsername bool) bool {
|
||||
return !channel.Deleted &&
|
||||
(channel.Broadcast || channel.Megagroup) &&
|
||||
strings.TrimSpace(channel.Username) != ""
|
||||
(strings.TrimSpace(channel.Username) != "" || hasActiveUsername)
|
||||
}
|
||||
|
||||
func refreshChannelCountsTx(ctx context.Context, tx pgx.Tx, channel domain.Channel) (domain.Channel, error) {
|
||||
|
|
|
|||
|
|
@ -150,11 +150,28 @@ func (s *ChannelStore) SearchPublicChannels(ctx context.Context, viewerUserID in
|
|||
queryPrefix := escapeLike(queryLower) + "%"
|
||||
queryLike := "%" + escapeLike(queryLower) + "%"
|
||||
rows, err := s.db.Query(ctx, `
|
||||
WITH username_matches AS (
|
||||
SELECT
|
||||
peer_id,
|
||||
MIN(CASE
|
||||
WHEN username_lower = $2 THEN 0
|
||||
ELSE 1
|
||||
END) AS rank
|
||||
FROM peer_usernames
|
||||
WHERE peer_type = 'channel'
|
||||
AND active
|
||||
AND collectible_id IS NOT NULL
|
||||
AND (
|
||||
username_lower = $2
|
||||
OR username_lower LIKE $3 ESCAPE '\'
|
||||
)
|
||||
GROUP BY peer_id
|
||||
)
|
||||
SELECT `+channelColumns+`
|
||||
FROM channels c
|
||||
LEFT JOIN username_matches um ON um.peer_id = c.id
|
||||
WHERE NOT c.deleted
|
||||
AND (c.broadcast OR c.megagroup)
|
||||
AND COALESCE(c.username, '') <> ''
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM channel_members m
|
||||
|
|
@ -163,15 +180,16 @@ WHERE NOT c.deleted
|
|||
AND m.status = 'active'
|
||||
)
|
||||
AND (
|
||||
lower(c.username) = $2
|
||||
um.peer_id IS NOT NULL
|
||||
OR lower(c.username) = $2
|
||||
OR lower(c.username) LIKE $3 ESCAPE '\'
|
||||
OR lower(c.title) LIKE $3 ESCAPE '\'
|
||||
OR lower(c.username) LIKE $4 ESCAPE '\'
|
||||
OR lower(c.title) LIKE $4 ESCAPE '\'
|
||||
)
|
||||
ORDER BY CASE
|
||||
WHEN lower(c.username) = $2 THEN 0
|
||||
WHEN lower(c.username) LIKE $3 ESCAPE '\' THEN 1
|
||||
WHEN um.rank = 0 OR lower(c.username) = $2 THEN 0
|
||||
WHEN um.rank = 1 OR lower(c.username) LIKE $3 ESCAPE '\' THEN 1
|
||||
WHEN lower(c.username) LIKE $4 ESCAPE '\' THEN 2
|
||||
WHEN lower(c.title) LIKE $3 ESCAPE '\' THEN 3
|
||||
ELSE 4
|
||||
|
|
@ -421,7 +439,12 @@ func (s *ChannelStore) getChannelForViewer(ctx context.Context, db sqlcgen.DBTX,
|
|||
return ch, syntheticMonoforumUserMember(ch, viewerUserID), true, nil
|
||||
}
|
||||
}
|
||||
if !publicPreviewableChannel(ch) {
|
||||
publicUsernameIDs, err := activeCollectibleUsernamePeerIDs(ctx, db, peerUsernameTypeChannel, []int64{ch.ID})
|
||||
if err != nil {
|
||||
return domain.Channel{}, domain.ChannelMember{}, false, err
|
||||
}
|
||||
_, hasActiveUsername := publicUsernameIDs[ch.ID]
|
||||
if !publicPreviewableChannel(ch, hasActiveUsername) {
|
||||
return domain.Channel{}, domain.ChannelMember{}, false, domain.ErrChannelPrivate
|
||||
}
|
||||
member, err = s.getPublicPreviewMember(ctx, db, viewerUserID, ch)
|
||||
|
|
|
|||
|
|
@ -510,6 +510,9 @@ func (s *ChannelStore) ResolvePublicChannelUsername(ctx context.Context, viewerU
|
|||
if !found || owner.peerType != peerUsernameTypeChannel {
|
||||
return domain.Channel{}, false, nil
|
||||
}
|
||||
if !owner.active {
|
||||
return domain.Channel{}, false, nil
|
||||
}
|
||||
ch, err := getChannelByID(ctx, s.db, owner.peerID)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrChannelInvalid) {
|
||||
|
|
@ -517,7 +520,7 @@ func (s *ChannelStore) ResolvePublicChannelUsername(ctx context.Context, viewerU
|
|||
}
|
||||
return domain.Channel{}, false, fmt.Errorf("resolve public channel username channel: %w", err)
|
||||
}
|
||||
if !publicPreviewableChannel(ch) {
|
||||
if !publicPreviewableChannel(ch, true) {
|
||||
return domain.Channel{}, false, nil
|
||||
}
|
||||
// A collectible row is authoritative for its own name: the channel's scalar
|
||||
|
|
@ -527,9 +530,6 @@ func (s *ChannelStore) ResolvePublicChannelUsername(ctx context.Context, viewerU
|
|||
if !owner.collectible && !strings.EqualFold(ch.Username, usernameLower) {
|
||||
return domain.Channel{}, false, nil
|
||||
}
|
||||
if owner.collectible && !owner.active {
|
||||
return domain.Channel{}, false, nil
|
||||
}
|
||||
return ch, true, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -90,6 +91,164 @@ func registryRows(t *testing.T, pool *pgxpool.Pool, peer domain.Peer) []domain.U
|
|||
return list
|
||||
}
|
||||
|
||||
func TestCollectibleUsernameResolveAndSearchUseActiveRegistry(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
seed := time.Now().UnixNano() % 1_000_000
|
||||
viewer := collectibleTestUser(t, pool, 3_100_000_000+seed, "")
|
||||
userPeer := collectibleTestUser(t, pool, 3_200_000_000+seed, "")
|
||||
userEditable := fmt.Sprintf("uedit%d", seed)
|
||||
userCollectible := fmt.Sprintf("unft%d", seed)
|
||||
setEditableUsername(t, pool, userPeer, userEditable)
|
||||
cleanupCollectible(t, pool, lowerASCII(userCollectible))
|
||||
|
||||
registry := NewCollectibleUsernameStore(pool)
|
||||
if _, created, err := registry.MintCollectibleUsername(ctx, mintRequest(userCollectible, userPeer, "")); err != nil || !created {
|
||||
t.Fatalf("mint user collectible: created=%v err=%v", created, err)
|
||||
}
|
||||
users := NewUserStore(pool)
|
||||
resolvedUser, found, err := users.ByUsername(ctx, userCollectible)
|
||||
if err != nil || !found || resolvedUser.ID != userPeer.ID {
|
||||
t.Fatalf("resolve user collectible = %+v found=%v err=%v", resolvedUser, found, err)
|
||||
}
|
||||
userSearch, err := users.Search(ctx, viewer.ID, userCollectible, "", 10)
|
||||
if err != nil || len(userSearch.Results) != 1 || userSearch.Results[0].ID != userPeer.ID {
|
||||
t.Fatalf("search user collectible = %+v err=%v", userSearch, err)
|
||||
}
|
||||
if changed, err := registry.SetUsernameActive(ctx, userPeer, userCollectible, false); err != nil || !changed {
|
||||
t.Fatalf("deactivate user collectible: changed=%v err=%v", changed, err)
|
||||
}
|
||||
if _, found, err := users.ByUsername(ctx, userCollectible); err != nil || found {
|
||||
t.Fatalf("resolve inactive user collectible found=%v err=%v", found, err)
|
||||
}
|
||||
if hidden, err := users.Search(ctx, viewer.ID, userCollectible, "", 10); err != nil || len(hidden.Results)+len(hidden.MyResults) != 0 {
|
||||
t.Fatalf("search inactive user collectible = %+v err=%v", hidden, err)
|
||||
}
|
||||
|
||||
channels := NewChannelStore(pool)
|
||||
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
||||
CreatorUserID: userPeer.ID,
|
||||
Title: "Unrelated collectible channel",
|
||||
Broadcast: true,
|
||||
Date: int(time.Now().Unix()),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create channel: %v", err)
|
||||
}
|
||||
channelPeer := domain.Peer{Type: domain.PeerTypeChannel, ID: created.Channel.ID}
|
||||
t.Cleanup(func() {
|
||||
_, _ = pool.Exec(context.Background(), `DELETE FROM channels WHERE id = $1`, channelPeer.ID)
|
||||
})
|
||||
channelEditable := fmt.Sprintf("cedit%d", seed)
|
||||
if _, err := channels.UpdateUsername(ctx, domain.UpdateChannelUsernameRequest{
|
||||
UserID: userPeer.ID,
|
||||
ChannelID: channelPeer.ID,
|
||||
Username: channelEditable,
|
||||
}); err != nil {
|
||||
t.Fatalf("set channel editable username: %v", err)
|
||||
}
|
||||
channelCollectible := fmt.Sprintf("cnft%d", seed)
|
||||
cleanupCollectible(t, pool, lowerASCII(channelCollectible))
|
||||
if _, created, err := registry.MintCollectibleUsername(ctx, mintRequest(channelCollectible, channelPeer, "")); err != nil || !created {
|
||||
t.Fatalf("mint channel collectible: created=%v err=%v", created, err)
|
||||
}
|
||||
resolvedChannel, found, err := channels.ResolvePublicChannelUsername(ctx, viewer.ID, channelCollectible)
|
||||
if err != nil || !found || resolvedChannel.ID != channelPeer.ID {
|
||||
t.Fatalf("resolve channel collectible = %+v found=%v err=%v", resolvedChannel, found, err)
|
||||
}
|
||||
channelSearch, err := channels.SearchPublicChannels(ctx, viewer.ID, channelCollectible, 10)
|
||||
if err != nil || len(channelSearch.Results) != 1 || channelSearch.Results[0].ID != channelPeer.ID {
|
||||
t.Fatalf("search channel collectible = %+v err=%v", channelSearch, err)
|
||||
}
|
||||
if _, err := channels.UpdateUsername(ctx, domain.UpdateChannelUsernameRequest{
|
||||
UserID: userPeer.ID,
|
||||
ChannelID: channelPeer.ID,
|
||||
Username: "",
|
||||
}); err != nil {
|
||||
t.Fatalf("clear channel editable username: %v", err)
|
||||
}
|
||||
if nftOnly, found, err := channels.ResolvePublicChannelUsername(ctx, viewer.ID, channelCollectible); err != nil || !found || nftOnly.ID != channelPeer.ID {
|
||||
t.Fatalf("resolve NFT-only channel = %+v found=%v err=%v", nftOnly, found, err)
|
||||
}
|
||||
if view, err := channels.GetChannel(ctx, viewer.ID, channelPeer.ID); err != nil || view.Channel.ID != channelPeer.ID {
|
||||
t.Fatalf("preview NFT-only channel = %+v err=%v", view, err)
|
||||
}
|
||||
if _, err := channels.UpdateUsername(ctx, domain.UpdateChannelUsernameRequest{
|
||||
UserID: userPeer.ID,
|
||||
ChannelID: channelPeer.ID,
|
||||
Username: channelEditable,
|
||||
}); err != nil {
|
||||
t.Fatalf("restore channel editable username: %v", err)
|
||||
}
|
||||
if changed, err := registry.SetUsernameActive(ctx, channelPeer, channelCollectible, false); err != nil || !changed {
|
||||
t.Fatalf("deactivate channel collectible: changed=%v err=%v", changed, err)
|
||||
}
|
||||
if _, found, err := channels.ResolvePublicChannelUsername(ctx, viewer.ID, channelCollectible); err != nil || found {
|
||||
t.Fatalf("resolve inactive channel collectible found=%v err=%v", found, err)
|
||||
}
|
||||
if hidden, err := channels.SearchPublicChannels(ctx, viewer.ID, channelCollectible, 10); err != nil || len(hidden.Results) != 0 {
|
||||
t.Fatalf("search inactive channel collectible = %+v err=%v", hidden, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectibleUsernameSearchPrefixUsesActiveRegistryIndex(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("begin: %v", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
if _, err := tx.Exec(ctx, `
|
||||
WITH names AS (
|
||||
SELECT
|
||||
CASE WHEN n = 1 THEN 'nftplanfixture' ELSE 'otherplanfixture' || n::text END AS username,
|
||||
9100000000 + n AS owner_peer_id
|
||||
FROM generate_series(1, 5000) AS n
|
||||
),
|
||||
assets AS (
|
||||
INSERT INTO collectible_usernames (
|
||||
username, username_lower, status, owner_peer_type, owner_peer_id,
|
||||
purchase_date, currency, amount,
|
||||
original_owner_peer_type, original_owner_peer_id,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
username, username, 'owned', 'user', owner_peer_id,
|
||||
now(), 'XTR', 1,
|
||||
'user', owner_peer_id,
|
||||
now(), now()
|
||||
FROM names
|
||||
RETURNING id, username, username_lower, owner_peer_id
|
||||
)
|
||||
INSERT INTO peer_usernames (
|
||||
username_lower, username, peer_type, peer_id,
|
||||
active, editable, sort_order, collectible_id
|
||||
)
|
||||
SELECT
|
||||
username_lower, username, 'user', owner_peer_id,
|
||||
true, false, 0, id
|
||||
FROM assets`); err != nil {
|
||||
t.Fatalf("seed username plan fixture: %v", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, "ANALYZE peer_usernames"); err != nil {
|
||||
t.Fatalf("analyze username plan fixture: %v", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, "SET LOCAL enable_seqscan = off"); err != nil {
|
||||
t.Fatalf("disable seqscan: %v", err)
|
||||
}
|
||||
plan := explainText(t, ctx, tx, `
|
||||
SELECT peer_id
|
||||
FROM peer_usernames
|
||||
WHERE peer_type = 'user'
|
||||
AND active
|
||||
AND collectible_id IS NOT NULL
|
||||
AND username_lower LIKE $1 || '%' ESCAPE '\'`, "nft")
|
||||
if !strings.Contains(plan, "peer_usernames_active_search_idx") {
|
||||
t.Fatalf("active username prefix plan = %s, want peer_usernames_active_search_idx", plan)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectibleUsernameMintIntoVault covers a vault mint: the asset exists, the
|
||||
// name is not projected into any peer's registry, and the provenance log records
|
||||
// the mint.
|
||||
|
|
|
|||
|
|
@ -69,6 +69,39 @@ func peerUsernameAvailable(ctx context.Context, db sqlcgen.DBTX, usernameLower,
|
|||
return owner.matches(peerType, peerID), nil
|
||||
}
|
||||
|
||||
// activeCollectibleUsernamePeerIDs returns the requested peers that own at
|
||||
// least one active collectible username. Editable registry rows are excluded:
|
||||
// their scalar users.username/channels.username value is the cross-check that
|
||||
// prevents a stale registry row from making a private peer public.
|
||||
func activeCollectibleUsernamePeerIDs(ctx context.Context, db sqlcgen.DBTX, peerType string, peerIDs []int64) (map[int64]struct{}, error) {
|
||||
out := make(map[int64]struct{})
|
||||
if len(peerIDs) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
rows, err := db.Query(ctx, `
|
||||
SELECT DISTINCT peer_id
|
||||
FROM peer_usernames
|
||||
WHERE peer_type = $1
|
||||
AND active
|
||||
AND collectible_id IS NOT NULL
|
||||
AND peer_id = ANY($2::bigint[])`, peerType, peerIDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list peers with active usernames: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var peerID int64
|
||||
if err := rows.Scan(&peerID); err != nil {
|
||||
return nil, fmt.Errorf("scan peer with active username: %w", err)
|
||||
}
|
||||
out[peerID] = struct{}{}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("list peers with active usernames: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// replacePeerUsernameTx rewrites the peer's editable username slot. username is
|
||||
// the display form (original case) and usernameLower its registry key; an empty
|
||||
// pair clears the slot.
|
||||
|
|
|
|||
|
|
@ -20,7 +20,18 @@ ORDER BY id;
|
|||
SELECT * FROM users WHERE lower(username) = lower($1) AND username <> '' AND deleted_at IS NULL;
|
||||
|
||||
-- name: SearchUsers :many
|
||||
WITH matched AS (
|
||||
WITH username_matches AS (
|
||||
SELECT
|
||||
peer_id,
|
||||
bool_or(username_lower = sqlc.arg(query_lower)::text) AS exact
|
||||
FROM peer_usernames
|
||||
WHERE peer_type = 'user'
|
||||
AND active
|
||||
AND collectible_id IS NOT NULL
|
||||
AND username_lower LIKE sqlc.arg(query_like)::text || '%' ESCAPE '\'
|
||||
GROUP BY peer_id
|
||||
),
|
||||
matched AS (
|
||||
SELECT
|
||||
u.id,
|
||||
u.access_hash,
|
||||
|
|
@ -51,7 +62,7 @@ WITH matched AS (
|
|||
COALESCE(c.mutual, false)::boolean AS mutual,
|
||||
CASE
|
||||
WHEN sqlc.arg(phone_query)::text <> '' AND u.phone = sqlc.arg(phone_query)::text THEN 0
|
||||
WHEN lower(u.username) = sqlc.arg(query_lower)::text THEN 1
|
||||
WHEN COALESCE(um.exact, false) OR lower(u.username) = sqlc.arg(query_lower)::text THEN 1
|
||||
WHEN lower(COALESCE(NULLIF(c.contact_first_name, ''), u.first_name)) = sqlc.arg(query_lower)::text THEN 2
|
||||
WHEN lower(u.first_name) = sqlc.arg(query_lower)::text THEN 3
|
||||
WHEN c.contact_user_id IS NOT NULL THEN 4
|
||||
|
|
@ -59,11 +70,13 @@ WITH matched AS (
|
|||
END AS rank
|
||||
FROM users u
|
||||
LEFT JOIN contacts c ON c.user_id = sqlc.arg(current_user_id)::bigint AND c.contact_user_id = u.id
|
||||
LEFT JOIN username_matches um ON um.peer_id = u.id
|
||||
WHERE u.id <> sqlc.arg(current_user_id)::bigint
|
||||
AND u.deleted_at IS NULL
|
||||
AND sqlc.arg(query_lower)::text <> ''
|
||||
AND (
|
||||
(sqlc.arg(phone_query)::text <> '' AND u.phone LIKE sqlc.arg(phone_query)::text || '%')
|
||||
OR um.peer_id IS NOT NULL
|
||||
OR lower(u.username) LIKE sqlc.arg(query_like)::text || '%' ESCAPE '\'
|
||||
OR lower(u.first_name) LIKE '%' || sqlc.arg(query_like)::text || '%' ESCAPE '\'
|
||||
OR lower(u.last_name) LIKE '%' || sqlc.arg(query_like)::text || '%' ESCAPE '\'
|
||||
|
|
|
|||
|
|
@ -364,7 +364,18 @@ func (q *Queries) GetUsersByPhones(ctx context.Context, phones []string) ([]User
|
|||
}
|
||||
|
||||
const searchUsers = `-- name: SearchUsers :many
|
||||
WITH matched AS (
|
||||
WITH username_matches AS (
|
||||
SELECT
|
||||
peer_id,
|
||||
bool_or(username_lower = $2::text) AS exact
|
||||
FROM peer_usernames
|
||||
WHERE peer_type = 'user'
|
||||
AND active
|
||||
AND collectible_id IS NOT NULL
|
||||
AND username_lower LIKE $3::text || '%' ESCAPE '\'
|
||||
GROUP BY peer_id
|
||||
),
|
||||
matched AS (
|
||||
SELECT
|
||||
u.id,
|
||||
u.access_hash,
|
||||
|
|
@ -394,27 +405,29 @@ WITH matched AS (
|
|||
(c.contact_user_id IS NOT NULL)::boolean AS contact,
|
||||
COALESCE(c.mutual, false)::boolean AS mutual,
|
||||
CASE
|
||||
WHEN $2::text <> '' AND u.phone = $2::text THEN 0
|
||||
WHEN lower(u.username) = $3::text THEN 1
|
||||
WHEN lower(COALESCE(NULLIF(c.contact_first_name, ''), u.first_name)) = $3::text THEN 2
|
||||
WHEN lower(u.first_name) = $3::text THEN 3
|
||||
WHEN $4::text <> '' AND u.phone = $4::text THEN 0
|
||||
WHEN COALESCE(um.exact, false) OR lower(u.username) = $2::text THEN 1
|
||||
WHEN lower(COALESCE(NULLIF(c.contact_first_name, ''), u.first_name)) = $2::text THEN 2
|
||||
WHEN lower(u.first_name) = $2::text THEN 3
|
||||
WHEN c.contact_user_id IS NOT NULL THEN 4
|
||||
ELSE 5
|
||||
END AS rank
|
||||
FROM users u
|
||||
LEFT JOIN contacts c ON c.user_id = $4::bigint AND c.contact_user_id = u.id
|
||||
WHERE u.id <> $4::bigint
|
||||
LEFT JOIN contacts c ON c.user_id = $5::bigint AND c.contact_user_id = u.id
|
||||
LEFT JOIN username_matches um ON um.peer_id = u.id
|
||||
WHERE u.id <> $5::bigint
|
||||
AND u.deleted_at IS NULL
|
||||
AND $3::text <> ''
|
||||
AND $2::text <> ''
|
||||
AND (
|
||||
($2::text <> '' AND u.phone LIKE $2::text || '%')
|
||||
OR lower(u.username) LIKE $5::text || '%' ESCAPE '\'
|
||||
OR lower(u.first_name) LIKE '%' || $5::text || '%' ESCAPE '\'
|
||||
OR lower(u.last_name) LIKE '%' || $5::text || '%' ESCAPE '\'
|
||||
OR lower(trim(u.first_name || ' ' || u.last_name)) LIKE '%' || $5::text || '%' ESCAPE '\'
|
||||
OR lower(c.contact_first_name) LIKE '%' || $5::text || '%' ESCAPE '\'
|
||||
OR lower(c.contact_last_name) LIKE '%' || $5::text || '%' ESCAPE '\'
|
||||
OR lower(trim(c.contact_first_name || ' ' || c.contact_last_name)) LIKE '%' || $5::text || '%' ESCAPE '\'
|
||||
($4::text <> '' AND u.phone LIKE $4::text || '%')
|
||||
OR um.peer_id IS NOT NULL
|
||||
OR lower(u.username) LIKE $3::text || '%' ESCAPE '\'
|
||||
OR lower(u.first_name) LIKE '%' || $3::text || '%' ESCAPE '\'
|
||||
OR lower(u.last_name) LIKE '%' || $3::text || '%' ESCAPE '\'
|
||||
OR lower(trim(u.first_name || ' ' || u.last_name)) LIKE '%' || $3::text || '%' ESCAPE '\'
|
||||
OR lower(c.contact_first_name) LIKE '%' || $3::text || '%' ESCAPE '\'
|
||||
OR lower(c.contact_last_name) LIKE '%' || $3::text || '%' ESCAPE '\'
|
||||
OR lower(trim(c.contact_first_name || ' ' || c.contact_last_name)) LIKE '%' || $3::text || '%' ESCAPE '\'
|
||||
)
|
||||
)
|
||||
SELECT
|
||||
|
|
@ -452,10 +465,10 @@ LIMIT $1
|
|||
|
||||
type SearchUsersParams struct {
|
||||
LimitCount int32
|
||||
PhoneQuery string
|
||||
QueryLower string
|
||||
CurrentUserID int64
|
||||
QueryLike string
|
||||
PhoneQuery string
|
||||
CurrentUserID int64
|
||||
}
|
||||
|
||||
type SearchUsersRow struct {
|
||||
|
|
@ -491,10 +504,10 @@ type SearchUsersRow struct {
|
|||
func (q *Queries) SearchUsers(ctx context.Context, arg SearchUsersParams) ([]SearchUsersRow, error) {
|
||||
rows, err := q.db.Query(ctx, searchUsers,
|
||||
arg.LimitCount,
|
||||
arg.PhoneQuery,
|
||||
arg.QueryLower,
|
||||
arg.CurrentUserID,
|
||||
arg.QueryLike,
|
||||
arg.PhoneQuery,
|
||||
arg.CurrentUserID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue