feat: user search (#6302)

* feat: user search

* refactor: use sqlx macro

* refactor: better name for escaped query

* style: cleanup unused import

* chore: update sqlx query cache

* fix: tests

---------

Co-authored-by: sychic <47618543+Sychic@users.noreply.github.com>
This commit is contained in:
Calum H.
2026-06-03 19:38:24 +00:00
committed by GitHub
co-authored by sychic
parent d907083d83
commit 58ad58f958
6 changed files with 237 additions and 2 deletions
@@ -57,6 +57,13 @@ pub struct DBUser {
pub is_subscribed_to_newsletter: bool,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct DBSearchUser {
pub id: DBUserId,
pub username: String,
pub avatar_url: Option<String>,
}
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct Pride26CampaignDonation {
pub last_donated_at: DateTime<Utc>,
@@ -264,6 +271,44 @@ impl DBUser {
Ok(val)
}
pub async fn search<'a, E>(
query: &str,
exec: E,
) -> Result<Vec<DBSearchUser>, sqlx::Error>
where
E: crate::database::Executor<'a, Database = sqlx::Postgres>,
{
if query.is_empty() {
return Ok(Vec::new());
}
let lowercase_query = query.to_lowercase();
let escaped_query = format!("{}%", escape_like(&lowercase_query));
let users = sqlx::query!(
"
SELECT id, username, avatar_url
FROM users
WHERE LOWER(username) LIKE $1 ESCAPE '\'
ORDER BY LOWER(username) = $2 DESC, LOWER(username), username
LIMIT 25
",
escaped_query,
lowercase_query
)
.fetch_all(exec)
.await?
.into_iter()
.map(|row| DBSearchUser {
id: DBUserId(row.id),
username: row.username,
avatar_url: row.avatar_url,
})
.collect();
Ok(users)
}
pub async fn get_by_email<'a, E>(
email: &str,
exec: E,
@@ -1044,3 +1089,14 @@ impl DBUser {
}
}
}
fn escape_like(query: &str) -> String {
let mut escaped = String::with_capacity(query.len());
for ch in query.chars() {
if matches!(ch, '\\' | '%' | '_') {
escaped.push('\\');
}
escaped.push(ch);
}
escaped
}