collab: Replace TransitionalUserService with CloudUserService (#56538)

This PR replaces the `TransitionalUserService` with the
`CloudUserService`, as all of the calls are now all going through Cloud.

This allows us to delete the `TransitionalUserService`, the
`DatabaseUserService`, as well as the backing database queries that are
no longer used.

Closes CLO-758.

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2026-05-12 10:32:14 -04:00 committed by GitHub
parent b13cb40b97
commit 6f1409b31c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 38 additions and 395 deletions

View file

@ -4,7 +4,7 @@ use rpc::{
ErrorCode, ErrorCodeExt,
proto::{ChannelBufferVersion, VectorClockEntry},
};
use sea_orm::{ActiveValue, DbBackend, TryGetableMany};
use sea_orm::{ActiveValue, TryGetableMany};
impl Database {
#[cfg(feature = "test-support")]
@ -704,57 +704,29 @@ impl Database {
.await
}
/// Returns the details for the specified channel member.
pub async fn get_channel_participant_details(
/// Returns the members for the given channel.
#[cfg(feature = "test-support")]
pub async fn get_channel_members(
&self,
channel: &Channel,
filter: &str,
limit: u64,
) -> Result<(Vec<channel_member::Model>, Vec<user::Model>)> {
let members = self
.transaction(move |tx| async move {
let mut query = channel_member::Entity::find()
.find_also_related(user::Entity)
.filter(channel_member::Column::ChannelId.eq(channel.root_id()));
) -> Result<Vec<channel_member::Model>> {
self.transaction(move |tx| async move {
let members = channel_member::Entity::find()
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
.order_by(
Expr::cust(
"not role = 'admin', not role = 'member', not role = 'guest', not accepted",
),
sea_orm::Order::Asc,
)
.limit(limit)
.all(&*tx)
.await?;
if cfg!(any(test, feature = "sqlite")) && self.pool.get_database_backend() == DbBackend::Sqlite {
query = query.filter(Expr::cust_with_values(
"UPPER(github_login) LIKE ?",
[Self::fuzzy_like_string(&filter.to_uppercase())],
))
} else {
query = query.filter(Expr::cust_with_values(
"github_login ILIKE $1",
[Self::fuzzy_like_string(filter)],
))
}
let members = query.order_by(
Expr::cust(
"not role = 'admin', not role = 'member', not role = 'guest', not accepted, github_login",
),
sea_orm::Order::Asc,
)
.limit(limit)
.all(&*tx)
.await?;
Ok(members)
})
.await?;
let mut users: Vec<user::Model> = Vec::with_capacity(members.len());
let members = members
.into_iter()
.map(|(member, user)| {
if let Some(user) = user {
users.push(user)
}
member
})
.collect();
Ok((members, users))
Ok(members)
})
.await
}
/// Returns whether the given user is an admin in the specified channel.

View file

@ -44,35 +44,6 @@ impl Database {
.await
}
/// Returns all users by ID. There are no access checks here, so this should only be used internally.
pub async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<user::Model>> {
if ids.len() >= 10000_usize {
return Err(anyhow!("too many users"))?;
}
self.transaction(|tx| async {
let tx = tx;
Ok(user::Entity::find()
.filter(user::Column::Id.is_in(ids.iter().copied()))
.all(&*tx)
.await?)
})
.await
}
/// Returns a user by GitHub login. There are no access checks here, so this should only be used internally.
pub async fn get_user_by_github_login(
&self,
github_login: &str,
) -> Result<Option<user::Model>> {
self.transaction(|tx| async move {
Ok(user::Entity::find()
.filter(user::Column::GithubLogin.eq(github_login))
.one(&*tx)
.await?)
})
.await
}
pub async fn update_or_create_user_by_github_account(
&self,
github_login: &str,
@ -208,47 +179,4 @@ impl Database {
})
.await
}
/// Find users where github_login ILIKE name_query.
pub async fn fuzzy_search_users(
&self,
name_query: &str,
limit: u32,
) -> Result<Vec<user::Model>> {
self.transaction(|tx| async {
let tx = tx;
let like_string = Self::fuzzy_like_string(name_query);
let query = "
SELECT users.*
FROM users
WHERE github_login ILIKE $1
ORDER BY github_login <-> $2
LIMIT $3
";
Ok(user::Entity::find()
.from_raw_sql(Statement::from_sql_and_values(
self.pool.get_database_backend(),
query,
vec![like_string.into(), name_query.into(), limit.into()],
))
.all(&*tx)
.await?)
})
.await
}
/// fuzzy_like_string creates a string for matching in-order using fuzzy_search_users.
/// e.g. "cir" would become "%c%i%r%"
pub fn fuzzy_like_string(string: &str) -> String {
let mut result = String::with_capacity(string.len() * 2 + 1);
for c in string.chars() {
if c.is_alphanumeric() {
result.push('%');
result.push(c);
}
}
result.push('%');
result
}
}

View file

@ -20,9 +20,7 @@ use serde::Deserialize;
use std::{path::PathBuf, sync::Arc};
use util::ResultExt;
use crate::services::{
CloudUserService, DatabaseUserService, TransitionalUserService, UserService,
};
use crate::services::{CloudUserService, UserService};
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const REVISION: Option<&'static str> = option_env!("GITHUB_SHA");
@ -265,19 +263,11 @@ impl AppState {
} else {
None
},
user_service: {
let database_user_service = DatabaseUserService::new(db);
let cloud_user_service = CloudUserService::new(
http_client,
config.zed_cloud_url().to_string(),
config.zed_cloud_internal_api_key.clone(),
);
Arc::new(TransitionalUserService::new(
cloud_user_service,
database_user_service,
))
},
user_service: Arc::new(CloudUserService::new(
http_client,
config.zed_cloud_url().to_string(),
config.zed_cloud_internal_api_key.clone(),
)),
config,
};
Ok(Arc::new(this))

View file

@ -1,5 +1,3 @@
use std::sync::Arc;
use anyhow::{Context as _, anyhow};
use async_trait::async_trait;
use cloud_api_types::internal_api::{
@ -13,7 +11,7 @@ use rpc::proto;
use serde::de::DeserializeOwned;
use crate::Result;
use crate::db::{Channel, Database, UserId};
use crate::db::{Channel, UserId};
use crate::entities::User;
#[cfg(feature = "test-support")]
@ -40,60 +38,11 @@ pub trait UserService: Send + Sync + 'static {
) -> Result<(Vec<proto::ChannelMember>, Vec<User>)>;
#[cfg(feature = "test-support")]
fn as_fake(&self) -> Arc<FakeUserService> {
fn as_fake(&self) -> std::sync::Arc<FakeUserService> {
panic!("called as_fake on a real `UserService`");
}
}
/// A [`UserService`] implementation for transitioning from reading from the database to reading from Cloud.
pub struct TransitionalUserService {
cloud_user_service: CloudUserService,
#[expect(dead_code)]
database_user_service: DatabaseUserService,
}
impl TransitionalUserService {
pub fn new(
cloud_user_service: CloudUserService,
database_user_service: DatabaseUserService,
) -> Self {
Self {
cloud_user_service,
database_user_service,
}
}
}
#[async_trait]
impl UserService for TransitionalUserService {
async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<User>> {
self.cloud_user_service.get_users_by_ids(ids).await
}
async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
self.cloud_user_service
.get_user_by_github_login(github_login)
.await
}
async fn fuzzy_search_users(&self, query: &str, limit: u32) -> Result<Vec<User>> {
self.cloud_user_service
.fuzzy_search_users(query, limit)
.await
}
async fn search_channel_members(
&self,
channel: &Channel,
query: &str,
limit: u32,
) -> Result<(Vec<proto::ChannelMember>, Vec<User>)> {
self.cloud_user_service
.search_channel_members(channel, query, limit)
.await
}
}
/// A [`UserService`] implementation backed by Cloud.
pub struct CloudUserService {
http_client: reqwest::Client,
@ -271,65 +220,15 @@ impl From<internal_api::User> for User {
}
}
/// A [`UserService`] implementation backed by the database.
pub struct DatabaseUserService {
database: Arc<Database>,
}
impl DatabaseUserService {
pub fn new(database: Arc<Database>) -> Self {
Self { database }
}
}
#[async_trait]
impl UserService for DatabaseUserService {
async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<User>> {
let users = self.database.get_users_by_ids(ids).await?;
Ok(users.into_iter().map(User::from).collect())
}
async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
let user = self.database.get_user_by_github_login(github_login).await?;
Ok(user.map(User::from))
}
async fn fuzzy_search_users(&self, query: &str, limit: u32) -> Result<Vec<User>> {
let users = self.database.fuzzy_search_users(query, limit).await?;
Ok(users.into_iter().map(User::from).collect())
}
async fn search_channel_members(
&self,
channel: &Channel,
query: &str,
limit: u32,
) -> Result<(Vec<proto::ChannelMember>, Vec<User>)> {
let (members, users) = self
.database
.get_channel_participant_details(channel, query, limit as u64)
.await?;
Ok((
members
.into_iter()
.map(proto::ChannelMember::from)
.collect(),
users.into_iter().map(User::from).collect(),
))
}
}
#[cfg(feature = "test-support")]
mod fake_user_service {
use std::sync::Weak;
use std::sync::{Arc, Weak};
use collections::HashMap;
use tokio::sync::Mutex;
use crate::db::Database;
use super::*;
#[derive(Debug)]

View file

@ -37,10 +37,7 @@ async fn test_channels(db: &Arc<Database>) {
.unwrap();
let replace_channel = db.get_channel(replace_id, a_id).await.unwrap();
let (members, _) = db
.get_channel_participant_details(&replace_channel, "", 10)
.await
.unwrap();
let members = db.get_channel_members(&replace_channel, 10).await.unwrap();
let ids = members.into_iter().map(|m| m.user_id).collect::<Vec<_>>();
assert_eq!(ids, &[a_id, b_id]);
@ -191,10 +188,7 @@ async fn test_channel_invites(db: &Arc<Database>) {
assert_eq!(user_3_invites, &[channel_1_1_id]);
let channel_1_1 = db.get_channel(channel_1_1_id, user_1).await.unwrap();
let (members, _) = db
.get_channel_participant_details(&channel_1_1, "", 100)
.await
.unwrap();
let members = db.get_channel_members(&channel_1_1, 100).await.unwrap();
let mut members = members
.into_iter()
.map(proto::ChannelMember::from)
@ -231,10 +225,7 @@ async fn test_channel_invites(db: &Arc<Database>) {
.unwrap();
let channel_1_3 = db.get_channel(channel_1_3_id, user_1).await.unwrap();
let (members, _) = db
.get_channel_participant_details(&channel_1_3, "", 100)
.await
.unwrap();
let members = db.get_channel_members(&channel_1_3, 100).await.unwrap();
let members = members
.into_iter()
.map(proto::ChannelMember::from)
@ -735,10 +726,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
.unwrap();
let public_channel = db.get_channel(public_channel_id, admin).await.unwrap();
let (members, _) = db
.get_channel_participant_details(&public_channel, "", 100)
.await
.unwrap();
let members = db.get_channel_members(&public_channel, 100).await.unwrap();
let mut members = members
.into_iter()
.map(proto::ChannelMember::from)
@ -814,10 +802,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
);
let public_channel = db.get_channel(public_channel_id, admin).await.unwrap();
let (members, _) = db
.get_channel_participant_details(&public_channel, "", 100)
.await
.unwrap();
let members = db.get_channel_members(&public_channel, 100).await.unwrap();
let mut members = members
.into_iter()
.map(proto::ChannelMember::from)
@ -854,10 +839,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
// currently people invited to parent channels are not shown here
let public_channel = db.get_channel(public_channel_id, admin).await.unwrap();
let (members, _) = db
.get_channel_participant_details(&public_channel, "", 100)
.await
.unwrap();
let members = db.get_channel_members(&public_channel, 100).await.unwrap();
let mut members = members
.into_iter()
.map(proto::ChannelMember::from)
@ -927,10 +909,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
.unwrap();
let public_channel = db.get_channel(public_channel_id, admin).await.unwrap();
let (members, _) = db
.get_channel_participant_details(&public_channel, "", 100)
.await
.unwrap();
let members = db.get_channel_members(&public_channel, 100).await.unwrap();
let mut members = members
.into_iter()
.map(proto::ChannelMember::from)

View file

@ -7,71 +7,6 @@ use pretty_assertions::assert_eq;
use rpc::ConnectionId;
use std::sync::Arc;
test_both_dbs!(
test_get_users,
test_get_users_by_ids_postgres,
test_get_users_by_ids_sqlite
);
async fn test_get_users(db: &Arc<Database>) {
let mut user_ids = Vec::new();
for i in 1..=4 {
let user = db
.create_user(
&format!("user{i}@example.com"),
None,
false,
NewUserParams {
github_login: format!("user{i}"),
github_user_id: i,
},
)
.await
.unwrap();
user_ids.push(user.user_id);
}
assert_eq!(
db.get_users_by_ids(user_ids.clone())
.await
.unwrap()
.into_iter()
.map(|user| (
user.id,
user.github_login,
user.github_user_id,
user.email_address
))
.collect::<Vec<_>>(),
vec![
(
user_ids[0],
"user1".to_string(),
1,
Some("user1@example.com".to_string()),
),
(
user_ids[1],
"user2".to_string(),
2,
Some("user2@example.com".to_string()),
),
(
user_ids[2],
"user3".to_string(),
3,
Some("user3@example.com".to_string()),
),
(
user_ids[3],
"user4".to_string(),
4,
Some("user4@example.com".to_string()),
)
]
);
}
test_both_dbs!(
test_add_contacts,
test_add_contacts_postgres,
@ -329,66 +264,6 @@ async fn test_project_count(db: &Arc<Database>) {
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
}
#[test]
fn test_fuzzy_like_string() {
assert_eq!(Database::fuzzy_like_string("abcd"), "%a%b%c%d%");
assert_eq!(Database::fuzzy_like_string("x y"), "%x%y%");
assert_eq!(Database::fuzzy_like_string(" z "), "%z%");
}
#[gpui::test]
async fn test_fuzzy_search_users(cx: &mut gpui::TestAppContext) {
// In CI, only run postgres tests on Linux (where we have the postgres service).
// Locally, always run them (assuming postgres is available).
if std::env::var("CI").is_ok() && !cfg!(target_os = "linux") {
return;
}
let test_db = TestDb::postgres(cx.executor());
let db = test_db.db();
for (i, github_login) in [
"California",
"colorado",
"oregon",
"washington",
"florida",
"delaware",
"rhode-island",
]
.into_iter()
.enumerate()
{
db.create_user(
&format!("{github_login}@example.com"),
None,
false,
NewUserParams {
github_login: github_login.into(),
github_user_id: i as i32,
},
)
.await
.unwrap();
}
assert_eq!(
fuzzy_search_user_names(db, "clr").await,
&["colorado", "California"]
);
assert_eq!(
fuzzy_search_user_names(db, "ro").await,
&["rhode-island", "colorado", "oregon"],
);
async fn fuzzy_search_user_names(db: &Database, query: &str) -> Vec<String> {
db.fuzzy_search_users(query, 10)
.await
.unwrap()
.into_iter()
.map(|user| user.github_login)
.collect::<Vec<_>>()
}
}
test_both_dbs!(
test_upsert_shared_thread,
test_upsert_shared_thread_postgres,