Reorganize the structure of the collab crate's `db` module (#2866)

Max Brunsfeld created

This PR just moves some code around, with the goal of making it easier
to find things in the `collab::db` module. That has become a large
module. Previously, most of the logic lived in one giant `impl Database`
item in `db.rs`.

I broke up this `impl` into several different `impl` blocks, grouped by
topic, each in a different file in a folder called `queries`.

I also pulled out the macro-generated id types into their own file,
moved the `TestDb` struct into its own file, and moved the `sea_orm`
entity declarations into a folder called `tables`.

New folder structure:

```
db
├── db_tests.rs
├── ids.rs
├── queries
│   ├── access_tokens.rs
│   ├── channels.rs
│   ├── contacts.rs
│   ├── projects.rs
│   ├── rooms.rs
│   ├── servers.rs
│   ├── signups.rs
│   └── users.rs
├── queries.rs
├── tables
│   ├── access_token.rs
│   ├── channel.rs
│   ├── channel_member.rs
│   ├── channel_path.rs
│   ├── contact.rs
│   ├── follower.rs
│   ├── language_server.rs
│   ├── project.rs
│   ├── project_collaborator.rs
│   ├── room.rs
│   ├── room_participant.rs
│   ├── server.rs
│   ├── signup.rs
│   ├── user.rs
│   ├── worktree.rs
│   ├── worktree_diagnostic_summary.rs
│   ├── worktree_entry.rs
│   ├── worktree_repository.rs
│   ├── worktree_repository_statuses.rs
│   └── worktree_settings_file.rs
├── tables.rs
└── test_db.rs
```

Release Notes:

- N/A

Change summary

crates/collab/src/db.rs                                     | 3914 ------
crates/collab/src/db/db_tests.rs                            |    5 
crates/collab/src/db/ids.rs                                 |  125 
crates/collab/src/db/queries.rs                             |   10 
crates/collab/src/db/queries/access_tokens.rs               |   53 
crates/collab/src/db/queries/channels.rs                    |  697 +
crates/collab/src/db/queries/contacts.rs                    |  298 
crates/collab/src/db/queries/projects.rs                    |  926 +
crates/collab/src/db/queries/rooms.rs                       | 1073 +
crates/collab/src/db/queries/servers.rs                     |   81 
crates/collab/src/db/queries/signups.rs                     |  349 
crates/collab/src/db/queries/users.rs                       |  243 
crates/collab/src/db/signup.rs                              |   57 
crates/collab/src/db/tables.rs                              |   20 
crates/collab/src/db/tables/access_token.rs                 |    2 
crates/collab/src/db/tables/channel.rs                      |    8 
crates/collab/src/db/tables/channel_member.rs               |    4 
crates/collab/src/db/tables/channel_path.rs                 |    2 
crates/collab/src/db/tables/contact.rs                      |   28 
crates/collab/src/db/tables/follower.rs                     |    5 
crates/collab/src/db/tables/language_server.rs              |    2 
crates/collab/src/db/tables/project.rs                      |    2 
crates/collab/src/db/tables/project_collaborator.rs         |    2 
crates/collab/src/db/tables/room.rs                         |    2 
crates/collab/src/db/tables/room_participant.rs             |    2 
crates/collab/src/db/tables/server.rs                       |    2 
crates/collab/src/db/tables/signup.rs                       |   28 
crates/collab/src/db/tables/user.rs                         |    2 
crates/collab/src/db/tables/worktree.rs                     |    2 
crates/collab/src/db/tables/worktree_diagnostic_summary.rs  |    2 
crates/collab/src/db/tables/worktree_entry.rs               |    2 
crates/collab/src/db/tables/worktree_repository.rs          |    2 
crates/collab/src/db/tables/worktree_repository_statuses.rs |    2 
crates/collab/src/db/tables/worktree_settings_file.rs       |    2 
crates/collab/src/db/test_db.rs                             |  120 
crates/collab/src/tests.rs                                  |    2 
36 files changed, 4,135 insertions(+), 3,941 deletions(-)

Detailed changes

crates/collab/src/db.rs 🔗

@@ -1,3850 +1,114 @@
-mod access_token;
-mod channel;
-mod channel_member;
-mod channel_path;
-mod contact;
-mod follower;
-mod language_server;
-mod project;
-mod project_collaborator;
-mod room;
-mod room_participant;
-mod server;
-mod signup;
 #[cfg(test)]
-mod tests;
-mod user;
-mod worktree;
-mod worktree_diagnostic_summary;
-mod worktree_entry;
-mod worktree_repository;
-mod worktree_repository_statuses;
-mod worktree_settings_file;
-
-use crate::executor::Executor;
-use crate::{Error, Result};
-use anyhow::anyhow;
-use collections::{BTreeMap, HashMap, HashSet};
-pub use contact::Contact;
-use dashmap::DashMap;
-use futures::StreamExt;
-use hyper::StatusCode;
-use rand::prelude::StdRng;
-use rand::{Rng, SeedableRng};
-use rpc::{proto, ConnectionId};
-use sea_orm::Condition;
-pub use sea_orm::ConnectOptions;
-use sea_orm::{
-    entity::prelude::*, ActiveValue, ConnectionTrait, DatabaseConnection, DatabaseTransaction,
-    DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect,
-    Statement, TransactionTrait,
-};
-use sea_query::{Alias, Expr, OnConflict, Query};
-use serde::{Deserialize, Serialize};
-pub use signup::{Invite, NewSignup, WaitlistSummary};
-use sqlx::migrate::{Migrate, Migration, MigrationSource};
-use sqlx::Connection;
-use std::fmt::Write as _;
-use std::ops::{Deref, DerefMut};
-use std::path::Path;
-use std::time::Duration;
-use std::{future::Future, marker::PhantomData, rc::Rc, sync::Arc};
-use tokio::sync::{Mutex, OwnedMutexGuard};
-pub use user::Model as User;
-
-pub struct Database {
-    options: ConnectOptions,
-    pool: DatabaseConnection,
-    rooms: DashMap<RoomId, Arc<Mutex<()>>>,
-    rng: Mutex<StdRng>,
-    executor: Executor,
-    #[cfg(test)]
-    runtime: Option<tokio::runtime::Runtime>,
-}
-
-impl Database {
-    pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> {
-        Ok(Self {
-            options: options.clone(),
-            pool: sea_orm::Database::connect(options).await?,
-            rooms: DashMap::with_capacity(16384),
-            rng: Mutex::new(StdRng::seed_from_u64(0)),
-            executor,
-            #[cfg(test)]
-            runtime: None,
-        })
-    }
-
-    #[cfg(test)]
-    pub fn reset(&self) {
-        self.rooms.clear();
-    }
-
-    pub async fn migrate(
-        &self,
-        migrations_path: &Path,
-        ignore_checksum_mismatch: bool,
-    ) -> anyhow::Result<Vec<(Migration, Duration)>> {
-        let migrations = MigrationSource::resolve(migrations_path)
-            .await
-            .map_err(|err| anyhow!("failed to load migrations: {err:?}"))?;
-
-        let mut connection = sqlx::AnyConnection::connect(self.options.get_url()).await?;
-
-        connection.ensure_migrations_table().await?;
-        let applied_migrations: HashMap<_, _> = connection
-            .list_applied_migrations()
-            .await?
-            .into_iter()
-            .map(|m| (m.version, m))
-            .collect();
-
-        let mut new_migrations = Vec::new();
-        for migration in migrations {
-            match applied_migrations.get(&migration.version) {
-                Some(applied_migration) => {
-                    if migration.checksum != applied_migration.checksum && !ignore_checksum_mismatch
-                    {
-                        Err(anyhow!(
-                            "checksum mismatch for applied migration {}",
-                            migration.description
-                        ))?;
-                    }
-                }
-                None => {
-                    let elapsed = connection.apply(&migration).await?;
-                    new_migrations.push((migration, elapsed));
-                }
-            }
-        }
-
-        Ok(new_migrations)
-    }
-
-    pub async fn create_server(&self, environment: &str) -> Result<ServerId> {
-        self.transaction(|tx| async move {
-            let server = server::ActiveModel {
-                environment: ActiveValue::set(environment.into()),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-            Ok(server.id)
-        })
-        .await
-    }
-
-    pub async fn stale_room_ids(
-        &self,
-        environment: &str,
-        new_server_id: ServerId,
-    ) -> Result<Vec<RoomId>> {
-        self.transaction(|tx| async move {
-            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-            enum QueryAs {
-                RoomId,
-            }
-
-            let stale_server_epochs = self
-                .stale_server_ids(environment, new_server_id, &tx)
-                .await?;
-            Ok(room_participant::Entity::find()
-                .select_only()
-                .column(room_participant::Column::RoomId)
-                .distinct()
-                .filter(
-                    room_participant::Column::AnsweringConnectionServerId
-                        .is_in(stale_server_epochs),
-                )
-                .into_values::<_, QueryAs>()
-                .all(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    pub async fn refresh_room(
-        &self,
-        room_id: RoomId,
-        new_server_id: ServerId,
-    ) -> Result<RoomGuard<RefreshedRoom>> {
-        self.room_transaction(room_id, |tx| async move {
-            let stale_participant_filter = Condition::all()
-                .add(room_participant::Column::RoomId.eq(room_id))
-                .add(room_participant::Column::AnsweringConnectionId.is_not_null())
-                .add(room_participant::Column::AnsweringConnectionServerId.ne(new_server_id));
-
-            let stale_participant_user_ids = room_participant::Entity::find()
-                .filter(stale_participant_filter.clone())
-                .all(&*tx)
-                .await?
-                .into_iter()
-                .map(|participant| participant.user_id)
-                .collect::<Vec<_>>();
-
-            // Delete participants who failed to reconnect and cancel their calls.
-            let mut canceled_calls_to_user_ids = Vec::new();
-            room_participant::Entity::delete_many()
-                .filter(stale_participant_filter)
-                .exec(&*tx)
-                .await?;
-            let called_participants = room_participant::Entity::find()
-                .filter(
-                    Condition::all()
-                        .add(
-                            room_participant::Column::CallingUserId
-                                .is_in(stale_participant_user_ids.iter().copied()),
-                        )
-                        .add(room_participant::Column::AnsweringConnectionId.is_null()),
-                )
-                .all(&*tx)
-                .await?;
-            room_participant::Entity::delete_many()
-                .filter(
-                    room_participant::Column::Id
-                        .is_in(called_participants.iter().map(|participant| participant.id)),
-                )
-                .exec(&*tx)
-                .await?;
-            canceled_calls_to_user_ids.extend(
-                called_participants
-                    .into_iter()
-                    .map(|participant| participant.user_id),
-            );
-
-            let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
-            let channel_members;
-            if let Some(channel_id) = channel_id {
-                channel_members = self.get_channel_members_internal(channel_id, &tx).await?;
-            } else {
-                channel_members = Vec::new();
-
-                // Delete the room if it becomes empty.
-                if room.participants.is_empty() {
-                    project::Entity::delete_many()
-                        .filter(project::Column::RoomId.eq(room_id))
-                        .exec(&*tx)
-                        .await?;
-                    room::Entity::delete_by_id(room_id).exec(&*tx).await?;
-                }
-            };
-
-            Ok(RefreshedRoom {
-                room,
-                channel_id,
-                channel_members,
-                stale_participant_user_ids,
-                canceled_calls_to_user_ids,
-            })
-        })
-        .await
-    }
-
-    pub async fn delete_stale_servers(
-        &self,
-        environment: &str,
-        new_server_id: ServerId,
-    ) -> Result<()> {
-        self.transaction(|tx| async move {
-            server::Entity::delete_many()
-                .filter(
-                    Condition::all()
-                        .add(server::Column::Environment.eq(environment))
-                        .add(server::Column::Id.ne(new_server_id)),
-                )
-                .exec(&*tx)
-                .await?;
-            Ok(())
-        })
-        .await
-    }
-
-    async fn stale_server_ids(
-        &self,
-        environment: &str,
-        new_server_id: ServerId,
-        tx: &DatabaseTransaction,
-    ) -> Result<Vec<ServerId>> {
-        let stale_servers = server::Entity::find()
-            .filter(
-                Condition::all()
-                    .add(server::Column::Environment.eq(environment))
-                    .add(server::Column::Id.ne(new_server_id)),
-            )
-            .all(&*tx)
-            .await?;
-        Ok(stale_servers.into_iter().map(|server| server.id).collect())
-    }
-
-    // users
-
-    pub async fn create_user(
-        &self,
-        email_address: &str,
-        admin: bool,
-        params: NewUserParams,
-    ) -> Result<NewUserResult> {
-        self.transaction(|tx| async {
-            let tx = tx;
-            let user = user::Entity::insert(user::ActiveModel {
-                email_address: ActiveValue::set(Some(email_address.into())),
-                github_login: ActiveValue::set(params.github_login.clone()),
-                github_user_id: ActiveValue::set(Some(params.github_user_id)),
-                admin: ActiveValue::set(admin),
-                metrics_id: ActiveValue::set(Uuid::new_v4()),
-                ..Default::default()
-            })
-            .on_conflict(
-                OnConflict::column(user::Column::GithubLogin)
-                    .update_column(user::Column::GithubLogin)
-                    .to_owned(),
-            )
-            .exec_with_returning(&*tx)
-            .await?;
-
-            Ok(NewUserResult {
-                user_id: user.id,
-                metrics_id: user.metrics_id.to_string(),
-                signup_device_id: None,
-                inviting_user_id: None,
-            })
-        })
-        .await
-    }
-
-    pub async fn get_user_by_id(&self, id: UserId) -> Result<Option<user::Model>> {
-        self.transaction(|tx| async move { Ok(user::Entity::find_by_id(id).one(&*tx).await?) })
-            .await
-    }
-
-    pub async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<user::Model>> {
-        self.transaction(|tx| async {
-            let tx = tx;
-            Ok(user::Entity::find()
-                .filter(user::Column::Id.is_in(ids.iter().copied()))
-                .all(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    pub async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
-        self.transaction(|tx| async move {
-            Ok(user::Entity::find()
-                .filter(user::Column::GithubLogin.eq(github_login))
-                .one(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    pub async fn get_or_create_user_by_github_account(
-        &self,
-        github_login: &str,
-        github_user_id: Option<i32>,
-        github_email: Option<&str>,
-    ) -> Result<Option<User>> {
-        self.transaction(|tx| async move {
-            let tx = &*tx;
-            if let Some(github_user_id) = github_user_id {
-                if let Some(user_by_github_user_id) = user::Entity::find()
-                    .filter(user::Column::GithubUserId.eq(github_user_id))
-                    .one(tx)
-                    .await?
-                {
-                    let mut user_by_github_user_id = user_by_github_user_id.into_active_model();
-                    user_by_github_user_id.github_login = ActiveValue::set(github_login.into());
-                    Ok(Some(user_by_github_user_id.update(tx).await?))
-                } else if let Some(user_by_github_login) = user::Entity::find()
-                    .filter(user::Column::GithubLogin.eq(github_login))
-                    .one(tx)
-                    .await?
-                {
-                    let mut user_by_github_login = user_by_github_login.into_active_model();
-                    user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id));
-                    Ok(Some(user_by_github_login.update(tx).await?))
-                } else {
-                    let user = user::Entity::insert(user::ActiveModel {
-                        email_address: ActiveValue::set(github_email.map(|email| email.into())),
-                        github_login: ActiveValue::set(github_login.into()),
-                        github_user_id: ActiveValue::set(Some(github_user_id)),
-                        admin: ActiveValue::set(false),
-                        invite_count: ActiveValue::set(0),
-                        invite_code: ActiveValue::set(None),
-                        metrics_id: ActiveValue::set(Uuid::new_v4()),
-                        ..Default::default()
-                    })
-                    .exec_with_returning(&*tx)
-                    .await?;
-                    Ok(Some(user))
-                }
-            } else {
-                Ok(user::Entity::find()
-                    .filter(user::Column::GithubLogin.eq(github_login))
-                    .one(tx)
-                    .await?)
-            }
-        })
-        .await
-    }
-
-    pub async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>> {
-        self.transaction(|tx| async move {
-            Ok(user::Entity::find()
-                .order_by_asc(user::Column::GithubLogin)
-                .limit(limit as u64)
-                .offset(page as u64 * limit as u64)
-                .all(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    pub async fn get_users_with_no_invites(
-        &self,
-        invited_by_another_user: bool,
-    ) -> Result<Vec<User>> {
-        self.transaction(|tx| async move {
-            Ok(user::Entity::find()
-                .filter(
-                    user::Column::InviteCount
-                        .eq(0)
-                        .and(if invited_by_another_user {
-                            user::Column::InviterId.is_not_null()
-                        } else {
-                            user::Column::InviterId.is_null()
-                        }),
-                )
-                .all(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    pub async fn get_user_metrics_id(&self, id: UserId) -> Result<String> {
-        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-        enum QueryAs {
-            MetricsId,
-        }
-
-        self.transaction(|tx| async move {
-            let metrics_id: Uuid = user::Entity::find_by_id(id)
-                .select_only()
-                .column(user::Column::MetricsId)
-                .into_values::<_, QueryAs>()
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("could not find user"))?;
-            Ok(metrics_id.to_string())
-        })
-        .await
-    }
-
-    pub async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()> {
-        self.transaction(|tx| async move {
-            user::Entity::update_many()
-                .filter(user::Column::Id.eq(id))
-                .set(user::ActiveModel {
-                    admin: ActiveValue::set(is_admin),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-            Ok(())
-        })
-        .await
-    }
-
-    pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
-        self.transaction(|tx| async move {
-            user::Entity::update_many()
-                .filter(user::Column::Id.eq(id))
-                .set(user::ActiveModel {
-                    connected_once: ActiveValue::set(connected_once),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-            Ok(())
-        })
-        .await
-    }
-
-    pub async fn destroy_user(&self, id: UserId) -> Result<()> {
-        self.transaction(|tx| async move {
-            access_token::Entity::delete_many()
-                .filter(access_token::Column::UserId.eq(id))
-                .exec(&*tx)
-                .await?;
-            user::Entity::delete_by_id(id).exec(&*tx).await?;
-            Ok(())
-        })
-        .await
-    }
-
-    // contacts
-
-    pub async fn get_contacts(&self, user_id: UserId) -> Result<Vec<Contact>> {
-        #[derive(Debug, FromQueryResult)]
-        struct ContactWithUserBusyStatuses {
-            user_id_a: UserId,
-            user_id_b: UserId,
-            a_to_b: bool,
-            accepted: bool,
-            should_notify: bool,
-            user_a_busy: bool,
-            user_b_busy: bool,
-        }
-
-        self.transaction(|tx| async move {
-            let user_a_participant = Alias::new("user_a_participant");
-            let user_b_participant = Alias::new("user_b_participant");
-            let mut db_contacts = contact::Entity::find()
-                .column_as(
-                    Expr::tbl(user_a_participant.clone(), room_participant::Column::Id)
-                        .is_not_null(),
-                    "user_a_busy",
-                )
-                .column_as(
-                    Expr::tbl(user_b_participant.clone(), room_participant::Column::Id)
-                        .is_not_null(),
-                    "user_b_busy",
-                )
-                .filter(
-                    contact::Column::UserIdA
-                        .eq(user_id)
-                        .or(contact::Column::UserIdB.eq(user_id)),
-                )
-                .join_as(
-                    JoinType::LeftJoin,
-                    contact::Relation::UserARoomParticipant.def(),
-                    user_a_participant,
-                )
-                .join_as(
-                    JoinType::LeftJoin,
-                    contact::Relation::UserBRoomParticipant.def(),
-                    user_b_participant,
-                )
-                .into_model::<ContactWithUserBusyStatuses>()
-                .stream(&*tx)
-                .await?;
-
-            let mut contacts = Vec::new();
-            while let Some(db_contact) = db_contacts.next().await {
-                let db_contact = db_contact?;
-                if db_contact.user_id_a == user_id {
-                    if db_contact.accepted {
-                        contacts.push(Contact::Accepted {
-                            user_id: db_contact.user_id_b,
-                            should_notify: db_contact.should_notify && db_contact.a_to_b,
-                            busy: db_contact.user_b_busy,
-                        });
-                    } else if db_contact.a_to_b {
-                        contacts.push(Contact::Outgoing {
-                            user_id: db_contact.user_id_b,
-                        })
-                    } else {
-                        contacts.push(Contact::Incoming {
-                            user_id: db_contact.user_id_b,
-                            should_notify: db_contact.should_notify,
-                        });
-                    }
-                } else if db_contact.accepted {
-                    contacts.push(Contact::Accepted {
-                        user_id: db_contact.user_id_a,
-                        should_notify: db_contact.should_notify && !db_contact.a_to_b,
-                        busy: db_contact.user_a_busy,
-                    });
-                } else if db_contact.a_to_b {
-                    contacts.push(Contact::Incoming {
-                        user_id: db_contact.user_id_a,
-                        should_notify: db_contact.should_notify,
-                    });
-                } else {
-                    contacts.push(Contact::Outgoing {
-                        user_id: db_contact.user_id_a,
-                    });
-                }
-            }
-
-            contacts.sort_unstable_by_key(|contact| contact.user_id());
-
-            Ok(contacts)
-        })
-        .await
-    }
-
-    pub async fn is_user_busy(&self, user_id: UserId) -> Result<bool> {
-        self.transaction(|tx| async move {
-            let participant = room_participant::Entity::find()
-                .filter(room_participant::Column::UserId.eq(user_id))
-                .one(&*tx)
-                .await?;
-            Ok(participant.is_some())
-        })
-        .await
-    }
-
-    pub async fn has_contact(&self, user_id_1: UserId, user_id_2: UserId) -> Result<bool> {
-        self.transaction(|tx| async move {
-            let (id_a, id_b) = if user_id_1 < user_id_2 {
-                (user_id_1, user_id_2)
-            } else {
-                (user_id_2, user_id_1)
-            };
-
-            Ok(contact::Entity::find()
-                .filter(
-                    contact::Column::UserIdA
-                        .eq(id_a)
-                        .and(contact::Column::UserIdB.eq(id_b))
-                        .and(contact::Column::Accepted.eq(true)),
-                )
-                .one(&*tx)
-                .await?
-                .is_some())
-        })
-        .await
-    }
-
-    pub async fn send_contact_request(&self, sender_id: UserId, receiver_id: UserId) -> Result<()> {
-        self.transaction(|tx| async move {
-            let (id_a, id_b, a_to_b) = if sender_id < receiver_id {
-                (sender_id, receiver_id, true)
-            } else {
-                (receiver_id, sender_id, false)
-            };
-
-            let rows_affected = contact::Entity::insert(contact::ActiveModel {
-                user_id_a: ActiveValue::set(id_a),
-                user_id_b: ActiveValue::set(id_b),
-                a_to_b: ActiveValue::set(a_to_b),
-                accepted: ActiveValue::set(false),
-                should_notify: ActiveValue::set(true),
-                ..Default::default()
-            })
-            .on_conflict(
-                OnConflict::columns([contact::Column::UserIdA, contact::Column::UserIdB])
-                    .values([
-                        (contact::Column::Accepted, true.into()),
-                        (contact::Column::ShouldNotify, false.into()),
-                    ])
-                    .action_and_where(
-                        contact::Column::Accepted.eq(false).and(
-                            contact::Column::AToB
-                                .eq(a_to_b)
-                                .and(contact::Column::UserIdA.eq(id_b))
-                                .or(contact::Column::AToB
-                                    .ne(a_to_b)
-                                    .and(contact::Column::UserIdA.eq(id_a))),
-                        ),
-                    )
-                    .to_owned(),
-            )
-            .exec_without_returning(&*tx)
-            .await?;
-
-            if rows_affected == 1 {
-                Ok(())
-            } else {
-                Err(anyhow!("contact already requested"))?
-            }
-        })
-        .await
-    }
-
-    /// Returns a bool indicating whether the removed contact had originally accepted or not
-    ///
-    /// Deletes the contact identified by the requester and responder ids, and then returns
-    /// whether the deleted contact had originally accepted or was a pending contact request.
-    ///
-    /// # Arguments
-    ///
-    /// * `requester_id` - The user that initiates this request
-    /// * `responder_id` - The user that will be removed
-    pub async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<bool> {
-        self.transaction(|tx| async move {
-            let (id_a, id_b) = if responder_id < requester_id {
-                (responder_id, requester_id)
-            } else {
-                (requester_id, responder_id)
-            };
-
-            let contact = contact::Entity::find()
-                .filter(
-                    contact::Column::UserIdA
-                        .eq(id_a)
-                        .and(contact::Column::UserIdB.eq(id_b)),
-                )
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such contact"))?;
-
-            contact::Entity::delete_by_id(contact.id).exec(&*tx).await?;
-            Ok(contact.accepted)
-        })
-        .await
-    }
-
-    pub async fn dismiss_contact_notification(
-        &self,
-        user_id: UserId,
-        contact_user_id: UserId,
-    ) -> Result<()> {
-        self.transaction(|tx| async move {
-            let (id_a, id_b, a_to_b) = if user_id < contact_user_id {
-                (user_id, contact_user_id, true)
-            } else {
-                (contact_user_id, user_id, false)
-            };
-
-            let result = contact::Entity::update_many()
-                .set(contact::ActiveModel {
-                    should_notify: ActiveValue::set(false),
-                    ..Default::default()
-                })
-                .filter(
-                    contact::Column::UserIdA
-                        .eq(id_a)
-                        .and(contact::Column::UserIdB.eq(id_b))
-                        .and(
-                            contact::Column::AToB
-                                .eq(a_to_b)
-                                .and(contact::Column::Accepted.eq(true))
-                                .or(contact::Column::AToB
-                                    .ne(a_to_b)
-                                    .and(contact::Column::Accepted.eq(false))),
-                        ),
-                )
-                .exec(&*tx)
-                .await?;
-            if result.rows_affected == 0 {
-                Err(anyhow!("no such contact request"))?
-            } else {
-                Ok(())
-            }
-        })
-        .await
-    }
-
-    pub async fn respond_to_contact_request(
-        &self,
-        responder_id: UserId,
-        requester_id: UserId,
-        accept: bool,
-    ) -> Result<()> {
-        self.transaction(|tx| async move {
-            let (id_a, id_b, a_to_b) = if responder_id < requester_id {
-                (responder_id, requester_id, false)
-            } else {
-                (requester_id, responder_id, true)
-            };
-            let rows_affected = if accept {
-                let result = contact::Entity::update_many()
-                    .set(contact::ActiveModel {
-                        accepted: ActiveValue::set(true),
-                        should_notify: ActiveValue::set(true),
-                        ..Default::default()
-                    })
-                    .filter(
-                        contact::Column::UserIdA
-                            .eq(id_a)
-                            .and(contact::Column::UserIdB.eq(id_b))
-                            .and(contact::Column::AToB.eq(a_to_b)),
-                    )
-                    .exec(&*tx)
-                    .await?;
-                result.rows_affected
-            } else {
-                let result = contact::Entity::delete_many()
-                    .filter(
-                        contact::Column::UserIdA
-                            .eq(id_a)
-                            .and(contact::Column::UserIdB.eq(id_b))
-                            .and(contact::Column::AToB.eq(a_to_b))
-                            .and(contact::Column::Accepted.eq(false)),
-                    )
-                    .exec(&*tx)
-                    .await?;
-
-                result.rows_affected
-            };
-
-            if rows_affected == 1 {
-                Ok(())
-            } else {
-                Err(anyhow!("no such contact request"))?
-            }
-        })
-        .await
-    }
-
-    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
-    }
-
-    pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result<Vec<User>> {
-        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.into(),
-                    vec![like_string.into(), name_query.into(), limit.into()],
-                ))
-                .all(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    // signups
-
-    pub async fn create_signup(&self, signup: &NewSignup) -> Result<()> {
-        self.transaction(|tx| async move {
-            signup::Entity::insert(signup::ActiveModel {
-                email_address: ActiveValue::set(signup.email_address.clone()),
-                email_confirmation_code: ActiveValue::set(random_email_confirmation_code()),
-                email_confirmation_sent: ActiveValue::set(false),
-                platform_mac: ActiveValue::set(signup.platform_mac),
-                platform_windows: ActiveValue::set(signup.platform_windows),
-                platform_linux: ActiveValue::set(signup.platform_linux),
-                platform_unknown: ActiveValue::set(false),
-                editor_features: ActiveValue::set(Some(signup.editor_features.clone())),
-                programming_languages: ActiveValue::set(Some(signup.programming_languages.clone())),
-                device_id: ActiveValue::set(signup.device_id.clone()),
-                added_to_mailing_list: ActiveValue::set(signup.added_to_mailing_list),
-                ..Default::default()
-            })
-            .on_conflict(
-                OnConflict::column(signup::Column::EmailAddress)
-                    .update_columns([
-                        signup::Column::PlatformMac,
-                        signup::Column::PlatformWindows,
-                        signup::Column::PlatformLinux,
-                        signup::Column::EditorFeatures,
-                        signup::Column::ProgrammingLanguages,
-                        signup::Column::DeviceId,
-                        signup::Column::AddedToMailingList,
-                    ])
-                    .to_owned(),
-            )
-            .exec(&*tx)
-            .await?;
-            Ok(())
-        })
-        .await
-    }
-
-    pub async fn get_signup(&self, email_address: &str) -> Result<signup::Model> {
-        self.transaction(|tx| async move {
-            let signup = signup::Entity::find()
-                .filter(signup::Column::EmailAddress.eq(email_address))
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| {
-                    anyhow!("signup with email address {} doesn't exist", email_address)
-                })?;
-
-            Ok(signup)
-        })
-        .await
-    }
-
-    pub async fn get_waitlist_summary(&self) -> Result<WaitlistSummary> {
-        self.transaction(|tx| async move {
-            let query = "
-                SELECT
-                    COUNT(*) as count,
-                    COALESCE(SUM(CASE WHEN platform_linux THEN 1 ELSE 0 END), 0) as linux_count,
-                    COALESCE(SUM(CASE WHEN platform_mac THEN 1 ELSE 0 END), 0) as mac_count,
-                    COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count,
-                    COALESCE(SUM(CASE WHEN platform_unknown THEN 1 ELSE 0 END), 0) as unknown_count
-                FROM (
-                    SELECT *
-                    FROM signups
-                    WHERE
-                        NOT email_confirmation_sent
-                ) AS unsent
-            ";
-            Ok(
-                WaitlistSummary::find_by_statement(Statement::from_sql_and_values(
-                    self.pool.get_database_backend(),
-                    query.into(),
-                    vec![],
-                ))
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("invalid result"))?,
-            )
-        })
-        .await
-    }
-
-    pub async fn record_sent_invites(&self, invites: &[Invite]) -> Result<()> {
-        let emails = invites
-            .iter()
-            .map(|s| s.email_address.as_str())
-            .collect::<Vec<_>>();
-        self.transaction(|tx| async {
-            let tx = tx;
-            signup::Entity::update_many()
-                .filter(signup::Column::EmailAddress.is_in(emails.iter().copied()))
-                .set(signup::ActiveModel {
-                    email_confirmation_sent: ActiveValue::set(true),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-            Ok(())
-        })
-        .await
-    }
-
-    pub async fn get_unsent_invites(&self, count: usize) -> Result<Vec<Invite>> {
-        self.transaction(|tx| async move {
-            Ok(signup::Entity::find()
-                .select_only()
-                .column(signup::Column::EmailAddress)
-                .column(signup::Column::EmailConfirmationCode)
-                .filter(
-                    signup::Column::EmailConfirmationSent.eq(false).and(
-                        signup::Column::PlatformMac
-                            .eq(true)
-                            .or(signup::Column::PlatformUnknown.eq(true)),
-                    ),
-                )
-                .order_by_asc(signup::Column::CreatedAt)
-                .limit(count as u64)
-                .into_model()
-                .all(&*tx)
-                .await?)
-        })
-        .await
-    }
-
-    // invite codes
-
-    pub async fn create_invite_from_code(
-        &self,
-        code: &str,
-        email_address: &str,
-        device_id: Option<&str>,
-        added_to_mailing_list: bool,
-    ) -> Result<Invite> {
-        self.transaction(|tx| async move {
-            let existing_user = user::Entity::find()
-                .filter(user::Column::EmailAddress.eq(email_address))
-                .one(&*tx)
-                .await?;
-
-            if existing_user.is_some() {
-                Err(anyhow!("email address is already in use"))?;
-            }
-
-            let inviting_user_with_invites = match user::Entity::find()
-                .filter(
-                    user::Column::InviteCode
-                        .eq(code)
-                        .and(user::Column::InviteCount.gt(0)),
-                )
-                .one(&*tx)
-                .await?
-            {
-                Some(inviting_user) => inviting_user,
-                None => {
-                    return Err(Error::Http(
-                        StatusCode::UNAUTHORIZED,
-                        "unable to find an invite code with invites remaining".to_string(),
-                    ))?
-                }
-            };
-            user::Entity::update_many()
-                .filter(
-                    user::Column::Id
-                        .eq(inviting_user_with_invites.id)
-                        .and(user::Column::InviteCount.gt(0)),
-                )
-                .col_expr(
-                    user::Column::InviteCount,
-                    Expr::col(user::Column::InviteCount).sub(1),
-                )
-                .exec(&*tx)
-                .await?;
-
-            let signup = signup::Entity::insert(signup::ActiveModel {
-                email_address: ActiveValue::set(email_address.into()),
-                email_confirmation_code: ActiveValue::set(random_email_confirmation_code()),
-                email_confirmation_sent: ActiveValue::set(false),
-                inviting_user_id: ActiveValue::set(Some(inviting_user_with_invites.id)),
-                platform_linux: ActiveValue::set(false),
-                platform_mac: ActiveValue::set(false),
-                platform_windows: ActiveValue::set(false),
-                platform_unknown: ActiveValue::set(true),
-                device_id: ActiveValue::set(device_id.map(|device_id| device_id.into())),
-                added_to_mailing_list: ActiveValue::set(added_to_mailing_list),
-                ..Default::default()
-            })
-            .on_conflict(
-                OnConflict::column(signup::Column::EmailAddress)
-                    .update_column(signup::Column::InvitingUserId)
-                    .to_owned(),
-            )
-            .exec_with_returning(&*tx)
-            .await?;
-
-            Ok(Invite {
-                email_address: signup.email_address,
-                email_confirmation_code: signup.email_confirmation_code,
-            })
-        })
-        .await
-    }
-
-    pub async fn create_user_from_invite(
-        &self,
-        invite: &Invite,
-        user: NewUserParams,
-    ) -> Result<Option<NewUserResult>> {
-        self.transaction(|tx| async {
-            let tx = tx;
-            let signup = signup::Entity::find()
-                .filter(
-                    signup::Column::EmailAddress
-                        .eq(invite.email_address.as_str())
-                        .and(
-                            signup::Column::EmailConfirmationCode
-                                .eq(invite.email_confirmation_code.as_str()),
-                        ),
-                )
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "no such invite".to_string()))?;
-
-            if signup.user_id.is_some() {
-                return Ok(None);
-            }
-
-            let user = user::Entity::insert(user::ActiveModel {
-                email_address: ActiveValue::set(Some(invite.email_address.clone())),
-                github_login: ActiveValue::set(user.github_login.clone()),
-                github_user_id: ActiveValue::set(Some(user.github_user_id)),
-                admin: ActiveValue::set(false),
-                invite_count: ActiveValue::set(user.invite_count),
-                invite_code: ActiveValue::set(Some(random_invite_code())),
-                metrics_id: ActiveValue::set(Uuid::new_v4()),
-                ..Default::default()
-            })
-            .on_conflict(
-                OnConflict::column(user::Column::GithubLogin)
-                    .update_columns([
-                        user::Column::EmailAddress,
-                        user::Column::GithubUserId,
-                        user::Column::Admin,
-                    ])
-                    .to_owned(),
-            )
-            .exec_with_returning(&*tx)
-            .await?;
-
-            let mut signup = signup.into_active_model();
-            signup.user_id = ActiveValue::set(Some(user.id));
-            let signup = signup.update(&*tx).await?;
-
-            if let Some(inviting_user_id) = signup.inviting_user_id {
-                let (user_id_a, user_id_b, a_to_b) = if inviting_user_id < user.id {
-                    (inviting_user_id, user.id, true)
-                } else {
-                    (user.id, inviting_user_id, false)
-                };
-
-                contact::Entity::insert(contact::ActiveModel {
-                    user_id_a: ActiveValue::set(user_id_a),
-                    user_id_b: ActiveValue::set(user_id_b),
-                    a_to_b: ActiveValue::set(a_to_b),
-                    should_notify: ActiveValue::set(true),
-                    accepted: ActiveValue::set(true),
-                    ..Default::default()
-                })
-                .on_conflict(OnConflict::new().do_nothing().to_owned())
-                .exec_without_returning(&*tx)
-                .await?;
-            }
-
-            Ok(Some(NewUserResult {
-                user_id: user.id,
-                metrics_id: user.metrics_id.to_string(),
-                inviting_user_id: signup.inviting_user_id,
-                signup_device_id: signup.device_id,
-            }))
-        })
-        .await
-    }
-
-    pub async fn set_invite_count_for_user(&self, id: UserId, count: i32) -> Result<()> {
-        self.transaction(|tx| async move {
-            if count > 0 {
-                user::Entity::update_many()
-                    .filter(
-                        user::Column::Id
-                            .eq(id)
-                            .and(user::Column::InviteCode.is_null()),
-                    )
-                    .set(user::ActiveModel {
-                        invite_code: ActiveValue::set(Some(random_invite_code())),
-                        ..Default::default()
-                    })
-                    .exec(&*tx)
-                    .await?;
-            }
-
-            user::Entity::update_many()
-                .filter(user::Column::Id.eq(id))
-                .set(user::ActiveModel {
-                    invite_count: ActiveValue::set(count),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-            Ok(())
-        })
-        .await
-    }
-
-    pub async fn get_invite_code_for_user(&self, id: UserId) -> Result<Option<(String, i32)>> {
-        self.transaction(|tx| async move {
-            match user::Entity::find_by_id(id).one(&*tx).await? {
-                Some(user) if user.invite_code.is_some() => {
-                    Ok(Some((user.invite_code.unwrap(), user.invite_count)))
-                }
-                _ => Ok(None),
-            }
-        })
-        .await
-    }
-
-    pub async fn get_user_for_invite_code(&self, code: &str) -> Result<User> {
-        self.transaction(|tx| async move {
-            user::Entity::find()
-                .filter(user::Column::InviteCode.eq(code))
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| {
-                    Error::Http(
-                        StatusCode::NOT_FOUND,
-                        "that invite code does not exist".to_string(),
-                    )
-                })
-        })
-        .await
-    }
-
-    // rooms
-
-    pub async fn incoming_call_for_user(
-        &self,
-        user_id: UserId,
-    ) -> Result<Option<proto::IncomingCall>> {
-        self.transaction(|tx| async move {
-            let pending_participant = room_participant::Entity::find()
-                .filter(
-                    room_participant::Column::UserId
-                        .eq(user_id)
-                        .and(room_participant::Column::AnsweringConnectionId.is_null()),
-                )
-                .one(&*tx)
-                .await?;
-
-            if let Some(pending_participant) = pending_participant {
-                let room = self.get_room(pending_participant.room_id, &tx).await?;
-                Ok(Self::build_incoming_call(&room, user_id))
-            } else {
-                Ok(None)
-            }
-        })
-        .await
-    }
-
-    pub async fn create_room(
-        &self,
-        user_id: UserId,
-        connection: ConnectionId,
-        live_kit_room: &str,
-    ) -> Result<proto::Room> {
-        self.transaction(|tx| async move {
-            let room = room::ActiveModel {
-                live_kit_room: ActiveValue::set(live_kit_room.into()),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-            room_participant::ActiveModel {
-                room_id: ActiveValue::set(room.id),
-                user_id: ActiveValue::set(user_id),
-                answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
-                answering_connection_server_id: ActiveValue::set(Some(ServerId(
-                    connection.owner_id as i32,
-                ))),
-                answering_connection_lost: ActiveValue::set(false),
-                calling_user_id: ActiveValue::set(user_id),
-                calling_connection_id: ActiveValue::set(connection.id as i32),
-                calling_connection_server_id: ActiveValue::set(Some(ServerId(
-                    connection.owner_id as i32,
-                ))),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-
-            let room = self.get_room(room.id, &tx).await?;
-            Ok(room)
-        })
-        .await
-    }
-
-    pub async fn call(
-        &self,
-        room_id: RoomId,
-        calling_user_id: UserId,
-        calling_connection: ConnectionId,
-        called_user_id: UserId,
-        initial_project_id: Option<ProjectId>,
-    ) -> Result<RoomGuard<(proto::Room, proto::IncomingCall)>> {
-        self.room_transaction(room_id, |tx| async move {
-            room_participant::ActiveModel {
-                room_id: ActiveValue::set(room_id),
-                user_id: ActiveValue::set(called_user_id),
-                answering_connection_lost: ActiveValue::set(false),
-                calling_user_id: ActiveValue::set(calling_user_id),
-                calling_connection_id: ActiveValue::set(calling_connection.id as i32),
-                calling_connection_server_id: ActiveValue::set(Some(ServerId(
-                    calling_connection.owner_id as i32,
-                ))),
-                initial_project_id: ActiveValue::set(initial_project_id),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-
-            let room = self.get_room(room_id, &tx).await?;
-            let incoming_call = Self::build_incoming_call(&room, called_user_id)
-                .ok_or_else(|| anyhow!("failed to build incoming call"))?;
-            Ok((room, incoming_call))
-        })
-        .await
-    }
-
-    pub async fn call_failed(
-        &self,
-        room_id: RoomId,
-        called_user_id: UserId,
-    ) -> Result<RoomGuard<proto::Room>> {
-        self.room_transaction(room_id, |tx| async move {
-            room_participant::Entity::delete_many()
-                .filter(
-                    room_participant::Column::RoomId
-                        .eq(room_id)
-                        .and(room_participant::Column::UserId.eq(called_user_id)),
-                )
-                .exec(&*tx)
-                .await?;
-            let room = self.get_room(room_id, &tx).await?;
-            Ok(room)
-        })
-        .await
-    }
-
-    pub async fn decline_call(
-        &self,
-        expected_room_id: Option<RoomId>,
-        user_id: UserId,
-    ) -> Result<Option<RoomGuard<proto::Room>>> {
-        self.optional_room_transaction(|tx| async move {
-            let mut filter = Condition::all()
-                .add(room_participant::Column::UserId.eq(user_id))
-                .add(room_participant::Column::AnsweringConnectionId.is_null());
-            if let Some(room_id) = expected_room_id {
-                filter = filter.add(room_participant::Column::RoomId.eq(room_id));
-            }
-            let participant = room_participant::Entity::find()
-                .filter(filter)
-                .one(&*tx)
-                .await?;
-
-            let participant = if let Some(participant) = participant {
-                participant
-            } else if expected_room_id.is_some() {
-                return Err(anyhow!("could not find call to decline"))?;
-            } else {
-                return Ok(None);
-            };
-
-            let room_id = participant.room_id;
-            room_participant::Entity::delete(participant.into_active_model())
-                .exec(&*tx)
-                .await?;
-
-            let room = self.get_room(room_id, &tx).await?;
-            Ok(Some((room_id, room)))
-        })
-        .await
-    }
-
-    pub async fn cancel_call(
-        &self,
-        room_id: RoomId,
-        calling_connection: ConnectionId,
-        called_user_id: UserId,
-    ) -> Result<RoomGuard<proto::Room>> {
-        self.room_transaction(room_id, |tx| async move {
-            let participant = room_participant::Entity::find()
-                .filter(
-                    Condition::all()
-                        .add(room_participant::Column::UserId.eq(called_user_id))
-                        .add(room_participant::Column::RoomId.eq(room_id))
-                        .add(
-                            room_participant::Column::CallingConnectionId
-                                .eq(calling_connection.id as i32),
-                        )
-                        .add(
-                            room_participant::Column::CallingConnectionServerId
-                                .eq(calling_connection.owner_id as i32),
-                        )
-                        .add(room_participant::Column::AnsweringConnectionId.is_null()),
-                )
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no call to cancel"))?;
-
-            room_participant::Entity::delete(participant.into_active_model())
-                .exec(&*tx)
-                .await?;
-
-            let room = self.get_room(room_id, &tx).await?;
-            Ok(room)
-        })
-        .await
-    }
-
-    pub async fn is_current_room_different_channel(
-        &self,
-        user_id: UserId,
-        channel_id: ChannelId,
-    ) -> Result<bool> {
-        self.transaction(|tx| async move {
-            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-            enum QueryAs {
-                ChannelId,
-            }
-
-            let channel_id_model: Option<ChannelId> = room_participant::Entity::find()
-                .select_only()
-                .column_as(room::Column::ChannelId, QueryAs::ChannelId)
-                .inner_join(room::Entity)
-                .filter(room_participant::Column::UserId.eq(user_id))
-                .into_values::<_, QueryAs>()
-                .one(&*tx)
-                .await?;
-
-            let result = channel_id_model
-                .map(|channel_id_model| channel_id_model != channel_id)
-                .unwrap_or(false);
-
-            Ok(result)
-        })
-        .await
-    }
-
-    pub async fn join_room(
-        &self,
-        room_id: RoomId,
-        user_id: UserId,
-        connection: ConnectionId,
-    ) -> Result<RoomGuard<JoinRoom>> {
-        self.room_transaction(room_id, |tx| async move {
-            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-            enum QueryChannelId {
-                ChannelId,
-            }
-            let channel_id: Option<ChannelId> = room::Entity::find()
-                .select_only()
-                .column(room::Column::ChannelId)
-                .filter(room::Column::Id.eq(room_id))
-                .into_values::<_, QueryChannelId>()
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such room"))?;
-
-            if let Some(channel_id) = channel_id {
-                self.check_user_is_channel_member(channel_id, user_id, &*tx)
-                    .await?;
-
-                room_participant::Entity::insert_many([room_participant::ActiveModel {
-                    room_id: ActiveValue::set(room_id),
-                    user_id: ActiveValue::set(user_id),
-                    answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
-                    answering_connection_server_id: ActiveValue::set(Some(ServerId(
-                        connection.owner_id as i32,
-                    ))),
-                    answering_connection_lost: ActiveValue::set(false),
-                    calling_user_id: ActiveValue::set(user_id),
-                    calling_connection_id: ActiveValue::set(connection.id as i32),
-                    calling_connection_server_id: ActiveValue::set(Some(ServerId(
-                        connection.owner_id as i32,
-                    ))),
-                    ..Default::default()
-                }])
-                .on_conflict(
-                    OnConflict::columns([room_participant::Column::UserId])
-                        .update_columns([
-                            room_participant::Column::AnsweringConnectionId,
-                            room_participant::Column::AnsweringConnectionServerId,
-                            room_participant::Column::AnsweringConnectionLost,
-                        ])
-                        .to_owned(),
-                )
-                .exec(&*tx)
-                .await?;
-            } else {
-                let result = room_participant::Entity::update_many()
-                    .filter(
-                        Condition::all()
-                            .add(room_participant::Column::RoomId.eq(room_id))
-                            .add(room_participant::Column::UserId.eq(user_id))
-                            .add(room_participant::Column::AnsweringConnectionId.is_null()),
-                    )
-                    .set(room_participant::ActiveModel {
-                        answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
-                        answering_connection_server_id: ActiveValue::set(Some(ServerId(
-                            connection.owner_id as i32,
-                        ))),
-                        answering_connection_lost: ActiveValue::set(false),
-                        ..Default::default()
-                    })
-                    .exec(&*tx)
-                    .await?;
-                if result.rows_affected == 0 {
-                    Err(anyhow!("room does not exist or was already joined"))?;
-                }
-            }
-
-            let room = self.get_room(room_id, &tx).await?;
-            let channel_members = if let Some(channel_id) = channel_id {
-                self.get_channel_members_internal(channel_id, &tx).await?
-            } else {
-                Vec::new()
-            };
-            Ok(JoinRoom {
-                room,
-                channel_id,
-                channel_members,
-            })
-        })
-        .await
-    }
-
-    pub async fn rejoin_room(
-        &self,
-        rejoin_room: proto::RejoinRoom,
-        user_id: UserId,
-        connection: ConnectionId,
-    ) -> Result<RoomGuard<RejoinedRoom>> {
-        let room_id = RoomId::from_proto(rejoin_room.id);
-        self.room_transaction(room_id, |tx| async {
-            let tx = tx;
-            let participant_update = room_participant::Entity::update_many()
-                .filter(
-                    Condition::all()
-                        .add(room_participant::Column::RoomId.eq(room_id))
-                        .add(room_participant::Column::UserId.eq(user_id))
-                        .add(room_participant::Column::AnsweringConnectionId.is_not_null())
-                        .add(
-                            Condition::any()
-                                .add(room_participant::Column::AnsweringConnectionLost.eq(true))
-                                .add(
-                                    room_participant::Column::AnsweringConnectionServerId
-                                        .ne(connection.owner_id as i32),
-                                ),
-                        ),
-                )
-                .set(room_participant::ActiveModel {
-                    answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
-                    answering_connection_server_id: ActiveValue::set(Some(ServerId(
-                        connection.owner_id as i32,
-                    ))),
-                    answering_connection_lost: ActiveValue::set(false),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-            if participant_update.rows_affected == 0 {
-                return Err(anyhow!("room does not exist or was already joined"))?;
-            }
-
-            let mut reshared_projects = Vec::new();
-            for reshared_project in &rejoin_room.reshared_projects {
-                let project_id = ProjectId::from_proto(reshared_project.project_id);
-                let project = project::Entity::find_by_id(project_id)
-                    .one(&*tx)
-                    .await?
-                    .ok_or_else(|| anyhow!("project does not exist"))?;
-                if project.host_user_id != user_id {
-                    return Err(anyhow!("no such project"))?;
-                }
-
-                let mut collaborators = project
-                    .find_related(project_collaborator::Entity)
-                    .all(&*tx)
-                    .await?;
-                let host_ix = collaborators
-                    .iter()
-                    .position(|collaborator| {
-                        collaborator.user_id == user_id && collaborator.is_host
-                    })
-                    .ok_or_else(|| anyhow!("host not found among collaborators"))?;
-                let host = collaborators.swap_remove(host_ix);
-                let old_connection_id = host.connection();
-
-                project::Entity::update(project::ActiveModel {
-                    host_connection_id: ActiveValue::set(Some(connection.id as i32)),
-                    host_connection_server_id: ActiveValue::set(Some(ServerId(
-                        connection.owner_id as i32,
-                    ))),
-                    ..project.into_active_model()
-                })
-                .exec(&*tx)
-                .await?;
-                project_collaborator::Entity::update(project_collaborator::ActiveModel {
-                    connection_id: ActiveValue::set(connection.id as i32),
-                    connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
-                    ..host.into_active_model()
-                })
-                .exec(&*tx)
-                .await?;
-
-                self.update_project_worktrees(project_id, &reshared_project.worktrees, &tx)
-                    .await?;
-
-                reshared_projects.push(ResharedProject {
-                    id: project_id,
-                    old_connection_id,
-                    collaborators: collaborators
-                        .iter()
-                        .map(|collaborator| ProjectCollaborator {
-                            connection_id: collaborator.connection(),
-                            user_id: collaborator.user_id,
-                            replica_id: collaborator.replica_id,
-                            is_host: collaborator.is_host,
-                        })
-                        .collect(),
-                    worktrees: reshared_project.worktrees.clone(),
-                });
-            }
-
-            project::Entity::delete_many()
-                .filter(
-                    Condition::all()
-                        .add(project::Column::RoomId.eq(room_id))
-                        .add(project::Column::HostUserId.eq(user_id))
-                        .add(
-                            project::Column::Id
-                                .is_not_in(reshared_projects.iter().map(|project| project.id)),
-                        ),
-                )
-                .exec(&*tx)
-                .await?;
-
-            let mut rejoined_projects = Vec::new();
-            for rejoined_project in &rejoin_room.rejoined_projects {
-                let project_id = ProjectId::from_proto(rejoined_project.id);
-                let Some(project) = project::Entity::find_by_id(project_id)
-                    .one(&*tx)
-                    .await? else { continue };
-
-                let mut worktrees = Vec::new();
-                let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?;
-                for db_worktree in db_worktrees {
-                    let mut worktree = RejoinedWorktree {
-                        id: db_worktree.id as u64,
-                        abs_path: db_worktree.abs_path,
-                        root_name: db_worktree.root_name,
-                        visible: db_worktree.visible,
-                        updated_entries: Default::default(),
-                        removed_entries: Default::default(),
-                        updated_repositories: Default::default(),
-                        removed_repositories: Default::default(),
-                        diagnostic_summaries: Default::default(),
-                        settings_files: Default::default(),
-                        scan_id: db_worktree.scan_id as u64,
-                        completed_scan_id: db_worktree.completed_scan_id as u64,
-                    };
-
-                    let rejoined_worktree = rejoined_project
-                        .worktrees
-                        .iter()
-                        .find(|worktree| worktree.id == db_worktree.id as u64);
-
-                    // File entries
-                    {
-                        let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
-                            worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
-                        } else {
-                            worktree_entry::Column::IsDeleted.eq(false)
-                        };
-
-                        let mut db_entries = worktree_entry::Entity::find()
-                            .filter(
-                                Condition::all()
-                                    .add(worktree_entry::Column::ProjectId.eq(project.id))
-                                    .add(worktree_entry::Column::WorktreeId.eq(worktree.id))
-                                    .add(entry_filter),
-                            )
-                            .stream(&*tx)
-                            .await?;
-
-                        while let Some(db_entry) = db_entries.next().await {
-                            let db_entry = db_entry?;
-                            if db_entry.is_deleted {
-                                worktree.removed_entries.push(db_entry.id as u64);
-                            } else {
-                                worktree.updated_entries.push(proto::Entry {
-                                    id: db_entry.id as u64,
-                                    is_dir: db_entry.is_dir,
-                                    path: db_entry.path,
-                                    inode: db_entry.inode as u64,
-                                    mtime: Some(proto::Timestamp {
-                                        seconds: db_entry.mtime_seconds as u64,
-                                        nanos: db_entry.mtime_nanos as u32,
-                                    }),
-                                    is_symlink: db_entry.is_symlink,
-                                    is_ignored: db_entry.is_ignored,
-                                    is_external: db_entry.is_external,
-                                    git_status: db_entry.git_status.map(|status| status as i32),
-                                });
-                            }
-                        }
-                    }
-
-                    // Repository Entries
-                    {
-                        let repository_entry_filter =
-                            if let Some(rejoined_worktree) = rejoined_worktree {
-                                worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id)
-                            } else {
-                                worktree_repository::Column::IsDeleted.eq(false)
-                            };
-
-                        let mut db_repositories = worktree_repository::Entity::find()
-                            .filter(
-                                Condition::all()
-                                    .add(worktree_repository::Column::ProjectId.eq(project.id))
-                                    .add(worktree_repository::Column::WorktreeId.eq(worktree.id))
-                                    .add(repository_entry_filter),
-                            )
-                            .stream(&*tx)
-                            .await?;
-
-                        while let Some(db_repository) = db_repositories.next().await {
-                            let db_repository = db_repository?;
-                            if db_repository.is_deleted {
-                                worktree
-                                    .removed_repositories
-                                    .push(db_repository.work_directory_id as u64);
-                            } else {
-                                worktree.updated_repositories.push(proto::RepositoryEntry {
-                                    work_directory_id: db_repository.work_directory_id as u64,
-                                    branch: db_repository.branch,
-                                });
-                            }
-                        }
-                    }
-
-                    worktrees.push(worktree);
-                }
-
-                let language_servers = project
-                    .find_related(language_server::Entity)
-                    .all(&*tx)
-                    .await?
-                    .into_iter()
-                    .map(|language_server| proto::LanguageServer {
-                        id: language_server.id as u64,
-                        name: language_server.name,
-                    })
-                    .collect::<Vec<_>>();
-
-                {
-                    let mut db_settings_files = worktree_settings_file::Entity::find()
-                        .filter(worktree_settings_file::Column::ProjectId.eq(project_id))
-                        .stream(&*tx)
-                        .await?;
-                    while let Some(db_settings_file) = db_settings_files.next().await {
-                        let db_settings_file = db_settings_file?;
-                        if let Some(worktree) = worktrees
-                            .iter_mut()
-                            .find(|w| w.id == db_settings_file.worktree_id as u64)
-                        {
-                            worktree.settings_files.push(WorktreeSettingsFile {
-                                path: db_settings_file.path,
-                                content: db_settings_file.content,
-                            });
-                        }
-                    }
-                }
-
-                let mut collaborators = project
-                    .find_related(project_collaborator::Entity)
-                    .all(&*tx)
-                    .await?;
-                let self_collaborator = if let Some(self_collaborator_ix) = collaborators
-                    .iter()
-                    .position(|collaborator| collaborator.user_id == user_id)
-                {
-                    collaborators.swap_remove(self_collaborator_ix)
-                } else {
-                    continue;
-                };
-                let old_connection_id = self_collaborator.connection();
-                project_collaborator::Entity::update(project_collaborator::ActiveModel {
-                    connection_id: ActiveValue::set(connection.id as i32),
-                    connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
-                    ..self_collaborator.into_active_model()
-                })
-                .exec(&*tx)
-                .await?;
-
-                let collaborators = collaborators
-                    .into_iter()
-                    .map(|collaborator| ProjectCollaborator {
-                        connection_id: collaborator.connection(),
-                        user_id: collaborator.user_id,
-                        replica_id: collaborator.replica_id,
-                        is_host: collaborator.is_host,
-                    })
-                    .collect::<Vec<_>>();
-
-                rejoined_projects.push(RejoinedProject {
-                    id: project_id,
-                    old_connection_id,
-                    collaborators,
-                    worktrees,
-                    language_servers,
-                });
-            }
-
-            let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
-            let channel_members = if let Some(channel_id) = channel_id {
-                self.get_channel_members_internal(channel_id, &tx).await?
-            } else {
-                Vec::new()
-            };
-
-            Ok(RejoinedRoom {
-                room,
-                channel_id,
-                channel_members,
-                rejoined_projects,
-                reshared_projects,
-            })
-        })
-        .await
-    }
-
-    pub async fn leave_room(
-        &self,
-        connection: ConnectionId,
-    ) -> Result<Option<RoomGuard<LeftRoom>>> {
-        self.optional_room_transaction(|tx| async move {
-            let leaving_participant = room_participant::Entity::find()
-                .filter(
-                    Condition::all()
-                        .add(
-                            room_participant::Column::AnsweringConnectionId
-                                .eq(connection.id as i32),
-                        )
-                        .add(
-                            room_participant::Column::AnsweringConnectionServerId
-                                .eq(connection.owner_id as i32),
-                        ),
-                )
-                .one(&*tx)
-                .await?;
-
-            if let Some(leaving_participant) = leaving_participant {
-                // Leave room.
-                let room_id = leaving_participant.room_id;
-                room_participant::Entity::delete_by_id(leaving_participant.id)
-                    .exec(&*tx)
-                    .await?;
-
-                // Cancel pending calls initiated by the leaving user.
-                let called_participants = room_participant::Entity::find()
-                    .filter(
-                        Condition::all()
-                            .add(
-                                room_participant::Column::CallingUserId
-                                    .eq(leaving_participant.user_id),
-                            )
-                            .add(room_participant::Column::AnsweringConnectionId.is_null()),
-                    )
-                    .all(&*tx)
-                    .await?;
-                room_participant::Entity::delete_many()
-                    .filter(
-                        room_participant::Column::Id
-                            .is_in(called_participants.iter().map(|participant| participant.id)),
-                    )
-                    .exec(&*tx)
-                    .await?;
-                let canceled_calls_to_user_ids = called_participants
-                    .into_iter()
-                    .map(|participant| participant.user_id)
-                    .collect();
-
-                // Detect left projects.
-                #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-                enum QueryProjectIds {
-                    ProjectId,
-                }
-                let project_ids: Vec<ProjectId> = project_collaborator::Entity::find()
-                    .select_only()
-                    .column_as(
-                        project_collaborator::Column::ProjectId,
-                        QueryProjectIds::ProjectId,
-                    )
-                    .filter(
-                        Condition::all()
-                            .add(
-                                project_collaborator::Column::ConnectionId.eq(connection.id as i32),
-                            )
-                            .add(
-                                project_collaborator::Column::ConnectionServerId
-                                    .eq(connection.owner_id as i32),
-                            ),
-                    )
-                    .into_values::<_, QueryProjectIds>()
-                    .all(&*tx)
-                    .await?;
-                let mut left_projects = HashMap::default();
-                let mut collaborators = project_collaborator::Entity::find()
-                    .filter(project_collaborator::Column::ProjectId.is_in(project_ids))
-                    .stream(&*tx)
-                    .await?;
-                while let Some(collaborator) = collaborators.next().await {
-                    let collaborator = collaborator?;
-                    let left_project =
-                        left_projects
-                            .entry(collaborator.project_id)
-                            .or_insert(LeftProject {
-                                id: collaborator.project_id,
-                                host_user_id: Default::default(),
-                                connection_ids: Default::default(),
-                                host_connection_id: Default::default(),
-                            });
-
-                    let collaborator_connection_id = collaborator.connection();
-                    if collaborator_connection_id != connection {
-                        left_project.connection_ids.push(collaborator_connection_id);
-                    }
-
-                    if collaborator.is_host {
-                        left_project.host_user_id = collaborator.user_id;
-                        left_project.host_connection_id = collaborator_connection_id;
-                    }
-                }
-                drop(collaborators);
-
-                // Leave projects.
-                project_collaborator::Entity::delete_many()
-                    .filter(
-                        Condition::all()
-                            .add(
-                                project_collaborator::Column::ConnectionId.eq(connection.id as i32),
-                            )
-                            .add(
-                                project_collaborator::Column::ConnectionServerId
-                                    .eq(connection.owner_id as i32),
-                            ),
-                    )
-                    .exec(&*tx)
-                    .await?;
-
-                // Unshare projects.
-                project::Entity::delete_many()
-                    .filter(
-                        Condition::all()
-                            .add(project::Column::RoomId.eq(room_id))
-                            .add(project::Column::HostConnectionId.eq(connection.id as i32))
-                            .add(
-                                project::Column::HostConnectionServerId
-                                    .eq(connection.owner_id as i32),
-                            ),
-                    )
-                    .exec(&*tx)
-                    .await?;
-
-                let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
-                let deleted = if room.participants.is_empty() {
-                    let result = room::Entity::delete_by_id(room_id)
-                        .filter(room::Column::ChannelId.is_null())
-                        .exec(&*tx)
-                        .await?;
-                    result.rows_affected > 0
-                } else {
-                    false
-                };
-
-                let channel_members = if let Some(channel_id) = channel_id {
-                    self.get_channel_members_internal(channel_id, &tx).await?
-                } else {
-                    Vec::new()
-                };
-                let left_room = LeftRoom {
-                    room,
-                    channel_id,
-                    channel_members,
-                    left_projects,
-                    canceled_calls_to_user_ids,
-                    deleted,
-                };
-
-                if left_room.room.participants.is_empty() {
-                    self.rooms.remove(&room_id);
-                }
-
-                Ok(Some((room_id, left_room)))
-            } else {
-                Ok(None)
-            }
-        })
-        .await
-    }
-
-    pub async fn follow(
-        &self,
-        project_id: ProjectId,
-        leader_connection: ConnectionId,
-        follower_connection: ConnectionId,
-    ) -> Result<RoomGuard<proto::Room>> {
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            follower::ActiveModel {
-                room_id: ActiveValue::set(room_id),
-                project_id: ActiveValue::set(project_id),
-                leader_connection_server_id: ActiveValue::set(ServerId(
-                    leader_connection.owner_id as i32,
-                )),
-                leader_connection_id: ActiveValue::set(leader_connection.id as i32),
-                follower_connection_server_id: ActiveValue::set(ServerId(
-                    follower_connection.owner_id as i32,
-                )),
-                follower_connection_id: ActiveValue::set(follower_connection.id as i32),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-
-            let room = self.get_room(room_id, &*tx).await?;
-            Ok(room)
-        })
-        .await
-    }
-
-    pub async fn unfollow(
-        &self,
-        project_id: ProjectId,
-        leader_connection: ConnectionId,
-        follower_connection: ConnectionId,
-    ) -> Result<RoomGuard<proto::Room>> {
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            follower::Entity::delete_many()
-                .filter(
-                    Condition::all()
-                        .add(follower::Column::ProjectId.eq(project_id))
-                        .add(
-                            follower::Column::LeaderConnectionServerId
-                                .eq(leader_connection.owner_id),
-                        )
-                        .add(follower::Column::LeaderConnectionId.eq(leader_connection.id))
-                        .add(
-                            follower::Column::FollowerConnectionServerId
-                                .eq(follower_connection.owner_id),
-                        )
-                        .add(follower::Column::FollowerConnectionId.eq(follower_connection.id)),
-                )
-                .exec(&*tx)
-                .await?;
-
-            let room = self.get_room(room_id, &*tx).await?;
-            Ok(room)
-        })
-        .await
-    }
-
-    pub async fn update_room_participant_location(
-        &self,
-        room_id: RoomId,
-        connection: ConnectionId,
-        location: proto::ParticipantLocation,
-    ) -> Result<RoomGuard<proto::Room>> {
-        self.room_transaction(room_id, |tx| async {
-            let tx = tx;
-            let location_kind;
-            let location_project_id;
-            match location
-                .variant
-                .as_ref()
-                .ok_or_else(|| anyhow!("invalid location"))?
-            {
-                proto::participant_location::Variant::SharedProject(project) => {
-                    location_kind = 0;
-                    location_project_id = Some(ProjectId::from_proto(project.id));
-                }
-                proto::participant_location::Variant::UnsharedProject(_) => {
-                    location_kind = 1;
-                    location_project_id = None;
-                }
-                proto::participant_location::Variant::External(_) => {
-                    location_kind = 2;
-                    location_project_id = None;
-                }
-            }
-
-            let result = room_participant::Entity::update_many()
-                .filter(
-                    Condition::all()
-                        .add(room_participant::Column::RoomId.eq(room_id))
-                        .add(
-                            room_participant::Column::AnsweringConnectionId
-                                .eq(connection.id as i32),
-                        )
-                        .add(
-                            room_participant::Column::AnsweringConnectionServerId
-                                .eq(connection.owner_id as i32),
-                        ),
-                )
-                .set(room_participant::ActiveModel {
-                    location_kind: ActiveValue::set(Some(location_kind)),
-                    location_project_id: ActiveValue::set(location_project_id),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-
-            if result.rows_affected == 1 {
-                let room = self.get_room(room_id, &tx).await?;
-                Ok(room)
-            } else {
-                Err(anyhow!("could not update room participant location"))?
-            }
-        })
-        .await
-    }
-
-    pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> {
-        self.transaction(|tx| async move {
-            let participant = room_participant::Entity::find()
-                .filter(
-                    Condition::all()
-                        .add(
-                            room_participant::Column::AnsweringConnectionId
-                                .eq(connection.id as i32),
-                        )
-                        .add(
-                            room_participant::Column::AnsweringConnectionServerId
-                                .eq(connection.owner_id as i32),
-                        ),
-                )
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("not a participant in any room"))?;
-
-            room_participant::Entity::update(room_participant::ActiveModel {
-                answering_connection_lost: ActiveValue::set(true),
-                ..participant.into_active_model()
-            })
-            .exec(&*tx)
-            .await?;
-
-            Ok(())
-        })
-        .await
-    }
-
-    fn build_incoming_call(
-        room: &proto::Room,
-        called_user_id: UserId,
-    ) -> Option<proto::IncomingCall> {
-        let pending_participant = room
-            .pending_participants
-            .iter()
-            .find(|participant| participant.user_id == called_user_id.to_proto())?;
-
-        Some(proto::IncomingCall {
-            room_id: room.id,
-            calling_user_id: pending_participant.calling_user_id,
-            participant_user_ids: room
-                .participants
-                .iter()
-                .map(|participant| participant.user_id)
-                .collect(),
-            initial_project: room.participants.iter().find_map(|participant| {
-                let initial_project_id = pending_participant.initial_project_id?;
-                participant
-                    .projects
-                    .iter()
-                    .find(|project| project.id == initial_project_id)
-                    .cloned()
-            }),
-        })
-    }
-    async fn get_room(&self, room_id: RoomId, tx: &DatabaseTransaction) -> Result<proto::Room> {
-        let (_, room) = self.get_channel_room(room_id, tx).await?;
-        Ok(room)
-    }
-
-    async fn get_channel_room(
-        &self,
-        room_id: RoomId,
-        tx: &DatabaseTransaction,
-    ) -> Result<(Option<ChannelId>, proto::Room)> {
-        let db_room = room::Entity::find_by_id(room_id)
-            .one(tx)
-            .await?
-            .ok_or_else(|| anyhow!("could not find room"))?;
-
-        let mut db_participants = db_room
-            .find_related(room_participant::Entity)
-            .stream(tx)
-            .await?;
-        let mut participants = HashMap::default();
-        let mut pending_participants = Vec::new();
-        while let Some(db_participant) = db_participants.next().await {
-            let db_participant = db_participant?;
-            if let Some((answering_connection_id, answering_connection_server_id)) = db_participant
-                .answering_connection_id
-                .zip(db_participant.answering_connection_server_id)
-            {
-                let location = match (
-                    db_participant.location_kind,
-                    db_participant.location_project_id,
-                ) {
-                    (Some(0), Some(project_id)) => {
-                        Some(proto::participant_location::Variant::SharedProject(
-                            proto::participant_location::SharedProject {
-                                id: project_id.to_proto(),
-                            },
-                        ))
-                    }
-                    (Some(1), _) => Some(proto::participant_location::Variant::UnsharedProject(
-                        Default::default(),
-                    )),
-                    _ => Some(proto::participant_location::Variant::External(
-                        Default::default(),
-                    )),
-                };
-
-                let answering_connection = ConnectionId {
-                    owner_id: answering_connection_server_id.0 as u32,
-                    id: answering_connection_id as u32,
-                };
-                participants.insert(
-                    answering_connection,
-                    proto::Participant {
-                        user_id: db_participant.user_id.to_proto(),
-                        peer_id: Some(answering_connection.into()),
-                        projects: Default::default(),
-                        location: Some(proto::ParticipantLocation { variant: location }),
-                    },
-                );
-            } else {
-                pending_participants.push(proto::PendingParticipant {
-                    user_id: db_participant.user_id.to_proto(),
-                    calling_user_id: db_participant.calling_user_id.to_proto(),
-                    initial_project_id: db_participant.initial_project_id.map(|id| id.to_proto()),
-                });
-            }
-        }
-        drop(db_participants);
-
-        let mut db_projects = db_room
-            .find_related(project::Entity)
-            .find_with_related(worktree::Entity)
-            .stream(tx)
-            .await?;
-
-        while let Some(row) = db_projects.next().await {
-            let (db_project, db_worktree) = row?;
-            let host_connection = db_project.host_connection()?;
-            if let Some(participant) = participants.get_mut(&host_connection) {
-                let project = if let Some(project) = participant
-                    .projects
-                    .iter_mut()
-                    .find(|project| project.id == db_project.id.to_proto())
-                {
-                    project
-                } else {
-                    participant.projects.push(proto::ParticipantProject {
-                        id: db_project.id.to_proto(),
-                        worktree_root_names: Default::default(),
-                    });
-                    participant.projects.last_mut().unwrap()
-                };
-
-                if let Some(db_worktree) = db_worktree {
-                    if db_worktree.visible {
-                        project.worktree_root_names.push(db_worktree.root_name);
-                    }
-                }
-            }
-        }
-        drop(db_projects);
-
-        let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?;
-        let mut followers = Vec::new();
-        while let Some(db_follower) = db_followers.next().await {
-            let db_follower = db_follower?;
-            followers.push(proto::Follower {
-                leader_id: Some(db_follower.leader_connection().into()),
-                follower_id: Some(db_follower.follower_connection().into()),
-                project_id: db_follower.project_id.to_proto(),
-            });
-        }
-
-        Ok((
-            db_room.channel_id,
-            proto::Room {
-                id: db_room.id.to_proto(),
-                live_kit_room: db_room.live_kit_room,
-                participants: participants.into_values().collect(),
-                pending_participants,
-                followers,
-            },
-        ))
-    }
-
-    // projects
-
-    pub async fn project_count_excluding_admins(&self) -> Result<usize> {
-        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-        enum QueryAs {
-            Count,
-        }
-
-        self.transaction(|tx| async move {
-            Ok(project::Entity::find()
-                .select_only()
-                .column_as(project::Column::Id.count(), QueryAs::Count)
-                .inner_join(user::Entity)
-                .filter(user::Column::Admin.eq(false))
-                .into_values::<_, QueryAs>()
-                .one(&*tx)
-                .await?
-                .unwrap_or(0i64) as usize)
-        })
-        .await
-    }
-
-    pub async fn share_project(
-        &self,
-        room_id: RoomId,
-        connection: ConnectionId,
-        worktrees: &[proto::WorktreeMetadata],
-    ) -> Result<RoomGuard<(ProjectId, proto::Room)>> {
-        self.room_transaction(room_id, |tx| async move {
-            let participant = room_participant::Entity::find()
-                .filter(
-                    Condition::all()
-                        .add(
-                            room_participant::Column::AnsweringConnectionId
-                                .eq(connection.id as i32),
-                        )
-                        .add(
-                            room_participant::Column::AnsweringConnectionServerId
-                                .eq(connection.owner_id as i32),
-                        ),
-                )
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("could not find participant"))?;
-            if participant.room_id != room_id {
-                return Err(anyhow!("shared project on unexpected room"))?;
-            }
-
-            let project = project::ActiveModel {
-                room_id: ActiveValue::set(participant.room_id),
-                host_user_id: ActiveValue::set(participant.user_id),
-                host_connection_id: ActiveValue::set(Some(connection.id as i32)),
-                host_connection_server_id: ActiveValue::set(Some(ServerId(
-                    connection.owner_id as i32,
-                ))),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-
-            if !worktrees.is_empty() {
-                worktree::Entity::insert_many(worktrees.iter().map(|worktree| {
-                    worktree::ActiveModel {
-                        id: ActiveValue::set(worktree.id as i64),
-                        project_id: ActiveValue::set(project.id),
-                        abs_path: ActiveValue::set(worktree.abs_path.clone()),
-                        root_name: ActiveValue::set(worktree.root_name.clone()),
-                        visible: ActiveValue::set(worktree.visible),
-                        scan_id: ActiveValue::set(0),
-                        completed_scan_id: ActiveValue::set(0),
-                    }
-                }))
-                .exec(&*tx)
-                .await?;
-            }
-
-            project_collaborator::ActiveModel {
-                project_id: ActiveValue::set(project.id),
-                connection_id: ActiveValue::set(connection.id as i32),
-                connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
-                user_id: ActiveValue::set(participant.user_id),
-                replica_id: ActiveValue::set(ReplicaId(0)),
-                is_host: ActiveValue::set(true),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-
-            let room = self.get_room(room_id, &tx).await?;
-            Ok((project.id, room))
-        })
-        .await
-    }
-
-    pub async fn unshare_project(
-        &self,
-        project_id: ProjectId,
-        connection: ConnectionId,
-    ) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
-
-            let project = project::Entity::find_by_id(project_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("project not found"))?;
-            if project.host_connection()? == connection {
-                project::Entity::delete(project.into_active_model())
-                    .exec(&*tx)
-                    .await?;
-                let room = self.get_room(room_id, &tx).await?;
-                Ok((room, guest_connection_ids))
-            } else {
-                Err(anyhow!("cannot unshare a project hosted by another user"))?
-            }
-        })
-        .await
-    }
-
-    pub async fn update_project(
-        &self,
-        project_id: ProjectId,
-        connection: ConnectionId,
-        worktrees: &[proto::WorktreeMetadata],
-    ) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            let project = project::Entity::find_by_id(project_id)
-                .filter(
-                    Condition::all()
-                        .add(project::Column::HostConnectionId.eq(connection.id as i32))
-                        .add(
-                            project::Column::HostConnectionServerId.eq(connection.owner_id as i32),
-                        ),
-                )
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such project"))?;
-
-            self.update_project_worktrees(project.id, worktrees, &tx)
-                .await?;
-
-            let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
-            let room = self.get_room(project.room_id, &tx).await?;
-            Ok((room, guest_connection_ids))
-        })
-        .await
-    }
-
-    async fn update_project_worktrees(
-        &self,
-        project_id: ProjectId,
-        worktrees: &[proto::WorktreeMetadata],
-        tx: &DatabaseTransaction,
-    ) -> Result<()> {
-        if !worktrees.is_empty() {
-            worktree::Entity::insert_many(worktrees.iter().map(|worktree| worktree::ActiveModel {
-                id: ActiveValue::set(worktree.id as i64),
-                project_id: ActiveValue::set(project_id),
-                abs_path: ActiveValue::set(worktree.abs_path.clone()),
-                root_name: ActiveValue::set(worktree.root_name.clone()),
-                visible: ActiveValue::set(worktree.visible),
-                scan_id: ActiveValue::set(0),
-                completed_scan_id: ActiveValue::set(0),
-            }))
-            .on_conflict(
-                OnConflict::columns([worktree::Column::ProjectId, worktree::Column::Id])
-                    .update_column(worktree::Column::RootName)
-                    .to_owned(),
-            )
-            .exec(&*tx)
-            .await?;
-        }
-
-        worktree::Entity::delete_many()
-            .filter(worktree::Column::ProjectId.eq(project_id).and(
-                worktree::Column::Id.is_not_in(worktrees.iter().map(|worktree| worktree.id as i64)),
-            ))
-            .exec(&*tx)
-            .await?;
-
-        Ok(())
-    }
-
-    pub async fn update_worktree(
-        &self,
-        update: &proto::UpdateWorktree,
-        connection: ConnectionId,
-    ) -> Result<RoomGuard<Vec<ConnectionId>>> {
-        let project_id = ProjectId::from_proto(update.project_id);
-        let worktree_id = update.worktree_id as i64;
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            // Ensure the update comes from the host.
-            let _project = project::Entity::find_by_id(project_id)
-                .filter(
-                    Condition::all()
-                        .add(project::Column::HostConnectionId.eq(connection.id as i32))
-                        .add(
-                            project::Column::HostConnectionServerId.eq(connection.owner_id as i32),
-                        ),
-                )
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such project"))?;
-
-            // Update metadata.
-            worktree::Entity::update(worktree::ActiveModel {
-                id: ActiveValue::set(worktree_id),
-                project_id: ActiveValue::set(project_id),
-                root_name: ActiveValue::set(update.root_name.clone()),
-                scan_id: ActiveValue::set(update.scan_id as i64),
-                completed_scan_id: if update.is_last_update {
-                    ActiveValue::set(update.scan_id as i64)
-                } else {
-                    ActiveValue::default()
-                },
-                abs_path: ActiveValue::set(update.abs_path.clone()),
-                ..Default::default()
-            })
-            .exec(&*tx)
-            .await?;
-
-            if !update.updated_entries.is_empty() {
-                worktree_entry::Entity::insert_many(update.updated_entries.iter().map(|entry| {
-                    let mtime = entry.mtime.clone().unwrap_or_default();
-                    worktree_entry::ActiveModel {
-                        project_id: ActiveValue::set(project_id),
-                        worktree_id: ActiveValue::set(worktree_id),
-                        id: ActiveValue::set(entry.id as i64),
-                        is_dir: ActiveValue::set(entry.is_dir),
-                        path: ActiveValue::set(entry.path.clone()),
-                        inode: ActiveValue::set(entry.inode as i64),
-                        mtime_seconds: ActiveValue::set(mtime.seconds as i64),
-                        mtime_nanos: ActiveValue::set(mtime.nanos as i32),
-                        is_symlink: ActiveValue::set(entry.is_symlink),
-                        is_ignored: ActiveValue::set(entry.is_ignored),
-                        is_external: ActiveValue::set(entry.is_external),
-                        git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)),
-                        is_deleted: ActiveValue::set(false),
-                        scan_id: ActiveValue::set(update.scan_id as i64),
-                    }
-                }))
-                .on_conflict(
-                    OnConflict::columns([
-                        worktree_entry::Column::ProjectId,
-                        worktree_entry::Column::WorktreeId,
-                        worktree_entry::Column::Id,
-                    ])
-                    .update_columns([
-                        worktree_entry::Column::IsDir,
-                        worktree_entry::Column::Path,
-                        worktree_entry::Column::Inode,
-                        worktree_entry::Column::MtimeSeconds,
-                        worktree_entry::Column::MtimeNanos,
-                        worktree_entry::Column::IsSymlink,
-                        worktree_entry::Column::IsIgnored,
-                        worktree_entry::Column::GitStatus,
-                        worktree_entry::Column::ScanId,
-                    ])
-                    .to_owned(),
-                )
-                .exec(&*tx)
-                .await?;
-            }
-
-            if !update.removed_entries.is_empty() {
-                worktree_entry::Entity::update_many()
-                    .filter(
-                        worktree_entry::Column::ProjectId
-                            .eq(project_id)
-                            .and(worktree_entry::Column::WorktreeId.eq(worktree_id))
-                            .and(
-                                worktree_entry::Column::Id
-                                    .is_in(update.removed_entries.iter().map(|id| *id as i64)),
-                            ),
-                    )
-                    .set(worktree_entry::ActiveModel {
-                        is_deleted: ActiveValue::Set(true),
-                        scan_id: ActiveValue::Set(update.scan_id as i64),
-                        ..Default::default()
-                    })
-                    .exec(&*tx)
-                    .await?;
-            }
-
-            if !update.updated_repositories.is_empty() {
-                worktree_repository::Entity::insert_many(update.updated_repositories.iter().map(
-                    |repository| worktree_repository::ActiveModel {
-                        project_id: ActiveValue::set(project_id),
-                        worktree_id: ActiveValue::set(worktree_id),
-                        work_directory_id: ActiveValue::set(repository.work_directory_id as i64),
-                        scan_id: ActiveValue::set(update.scan_id as i64),
-                        branch: ActiveValue::set(repository.branch.clone()),
-                        is_deleted: ActiveValue::set(false),
-                    },
-                ))
-                .on_conflict(
-                    OnConflict::columns([
-                        worktree_repository::Column::ProjectId,
-                        worktree_repository::Column::WorktreeId,
-                        worktree_repository::Column::WorkDirectoryId,
-                    ])
-                    .update_columns([
-                        worktree_repository::Column::ScanId,
-                        worktree_repository::Column::Branch,
-                    ])
-                    .to_owned(),
-                )
-                .exec(&*tx)
-                .await?;
-            }
-
-            if !update.removed_repositories.is_empty() {
-                worktree_repository::Entity::update_many()
-                    .filter(
-                        worktree_repository::Column::ProjectId
-                            .eq(project_id)
-                            .and(worktree_repository::Column::WorktreeId.eq(worktree_id))
-                            .and(
-                                worktree_repository::Column::WorkDirectoryId
-                                    .is_in(update.removed_repositories.iter().map(|id| *id as i64)),
-                            ),
-                    )
-                    .set(worktree_repository::ActiveModel {
-                        is_deleted: ActiveValue::Set(true),
-                        scan_id: ActiveValue::Set(update.scan_id as i64),
-                        ..Default::default()
-                    })
-                    .exec(&*tx)
-                    .await?;
-            }
-
-            let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
-            Ok(connection_ids)
-        })
-        .await
-    }
-
-    pub async fn update_diagnostic_summary(
-        &self,
-        update: &proto::UpdateDiagnosticSummary,
-        connection: ConnectionId,
-    ) -> Result<RoomGuard<Vec<ConnectionId>>> {
-        let project_id = ProjectId::from_proto(update.project_id);
-        let worktree_id = update.worktree_id as i64;
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            let summary = update
-                .summary
-                .as_ref()
-                .ok_or_else(|| anyhow!("invalid summary"))?;
-
-            // Ensure the update comes from the host.
-            let project = project::Entity::find_by_id(project_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such project"))?;
-            if project.host_connection()? != connection {
-                return Err(anyhow!("can't update a project hosted by someone else"))?;
-            }
-
-            // Update summary.
-            worktree_diagnostic_summary::Entity::insert(worktree_diagnostic_summary::ActiveModel {
-                project_id: ActiveValue::set(project_id),
-                worktree_id: ActiveValue::set(worktree_id),
-                path: ActiveValue::set(summary.path.clone()),
-                language_server_id: ActiveValue::set(summary.language_server_id as i64),
-                error_count: ActiveValue::set(summary.error_count as i32),
-                warning_count: ActiveValue::set(summary.warning_count as i32),
-                ..Default::default()
-            })
-            .on_conflict(
-                OnConflict::columns([
-                    worktree_diagnostic_summary::Column::ProjectId,
-                    worktree_diagnostic_summary::Column::WorktreeId,
-                    worktree_diagnostic_summary::Column::Path,
-                ])
-                .update_columns([
-                    worktree_diagnostic_summary::Column::LanguageServerId,
-                    worktree_diagnostic_summary::Column::ErrorCount,
-                    worktree_diagnostic_summary::Column::WarningCount,
-                ])
-                .to_owned(),
-            )
-            .exec(&*tx)
-            .await?;
-
-            let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
-            Ok(connection_ids)
-        })
-        .await
-    }
-
-    pub async fn start_language_server(
-        &self,
-        update: &proto::StartLanguageServer,
-        connection: ConnectionId,
-    ) -> Result<RoomGuard<Vec<ConnectionId>>> {
-        let project_id = ProjectId::from_proto(update.project_id);
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            let server = update
-                .server
-                .as_ref()
-                .ok_or_else(|| anyhow!("invalid language server"))?;
-
-            // Ensure the update comes from the host.
-            let project = project::Entity::find_by_id(project_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such project"))?;
-            if project.host_connection()? != connection {
-                return Err(anyhow!("can't update a project hosted by someone else"))?;
-            }
-
-            // Add the newly-started language server.
-            language_server::Entity::insert(language_server::ActiveModel {
-                project_id: ActiveValue::set(project_id),
-                id: ActiveValue::set(server.id as i64),
-                name: ActiveValue::set(server.name.clone()),
-                ..Default::default()
-            })
-            .on_conflict(
-                OnConflict::columns([
-                    language_server::Column::ProjectId,
-                    language_server::Column::Id,
-                ])
-                .update_column(language_server::Column::Name)
-                .to_owned(),
-            )
-            .exec(&*tx)
-            .await?;
-
-            let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
-            Ok(connection_ids)
-        })
-        .await
-    }
-
-    pub async fn update_worktree_settings(
-        &self,
-        update: &proto::UpdateWorktreeSettings,
-        connection: ConnectionId,
-    ) -> Result<RoomGuard<Vec<ConnectionId>>> {
-        let project_id = ProjectId::from_proto(update.project_id);
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            // Ensure the update comes from the host.
-            let project = project::Entity::find_by_id(project_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such project"))?;
-            if project.host_connection()? != connection {
-                return Err(anyhow!("can't update a project hosted by someone else"))?;
-            }
-
-            if let Some(content) = &update.content {
-                worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel {
-                    project_id: ActiveValue::Set(project_id),
-                    worktree_id: ActiveValue::Set(update.worktree_id as i64),
-                    path: ActiveValue::Set(update.path.clone()),
-                    content: ActiveValue::Set(content.clone()),
-                })
-                .on_conflict(
-                    OnConflict::columns([
-                        worktree_settings_file::Column::ProjectId,
-                        worktree_settings_file::Column::WorktreeId,
-                        worktree_settings_file::Column::Path,
-                    ])
-                    .update_column(worktree_settings_file::Column::Content)
-                    .to_owned(),
-                )
-                .exec(&*tx)
-                .await?;
-            } else {
-                worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel {
-                    project_id: ActiveValue::Set(project_id),
-                    worktree_id: ActiveValue::Set(update.worktree_id as i64),
-                    path: ActiveValue::Set(update.path.clone()),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-            }
-
-            let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
-            Ok(connection_ids)
-        })
-        .await
-    }
-
-    pub async fn join_project(
-        &self,
-        project_id: ProjectId,
-        connection: ConnectionId,
-    ) -> Result<RoomGuard<(Project, ReplicaId)>> {
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            let participant = room_participant::Entity::find()
-                .filter(
-                    Condition::all()
-                        .add(
-                            room_participant::Column::AnsweringConnectionId
-                                .eq(connection.id as i32),
-                        )
-                        .add(
-                            room_participant::Column::AnsweringConnectionServerId
-                                .eq(connection.owner_id as i32),
-                        ),
-                )
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("must join a room first"))?;
-
-            let project = project::Entity::find_by_id(project_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such project"))?;
-            if project.room_id != participant.room_id {
-                return Err(anyhow!("no such project"))?;
-            }
-
-            let mut collaborators = project
-                .find_related(project_collaborator::Entity)
-                .all(&*tx)
-                .await?;
-            let replica_ids = collaborators
-                .iter()
-                .map(|c| c.replica_id)
-                .collect::<HashSet<_>>();
-            let mut replica_id = ReplicaId(1);
-            while replica_ids.contains(&replica_id) {
-                replica_id.0 += 1;
-            }
-            let new_collaborator = project_collaborator::ActiveModel {
-                project_id: ActiveValue::set(project_id),
-                connection_id: ActiveValue::set(connection.id as i32),
-                connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
-                user_id: ActiveValue::set(participant.user_id),
-                replica_id: ActiveValue::set(replica_id),
-                is_host: ActiveValue::set(false),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-            collaborators.push(new_collaborator);
-
-            let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?;
-            let mut worktrees = db_worktrees
-                .into_iter()
-                .map(|db_worktree| {
-                    (
-                        db_worktree.id as u64,
-                        Worktree {
-                            id: db_worktree.id as u64,
-                            abs_path: db_worktree.abs_path,
-                            root_name: db_worktree.root_name,
-                            visible: db_worktree.visible,
-                            entries: Default::default(),
-                            repository_entries: Default::default(),
-                            diagnostic_summaries: Default::default(),
-                            settings_files: Default::default(),
-                            scan_id: db_worktree.scan_id as u64,
-                            completed_scan_id: db_worktree.completed_scan_id as u64,
-                        },
-                    )
-                })
-                .collect::<BTreeMap<_, _>>();
-
-            // Populate worktree entries.
-            {
-                let mut db_entries = worktree_entry::Entity::find()
-                    .filter(
-                        Condition::all()
-                            .add(worktree_entry::Column::ProjectId.eq(project_id))
-                            .add(worktree_entry::Column::IsDeleted.eq(false)),
-                    )
-                    .stream(&*tx)
-                    .await?;
-                while let Some(db_entry) = db_entries.next().await {
-                    let db_entry = db_entry?;
-                    if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) {
-                        worktree.entries.push(proto::Entry {
-                            id: db_entry.id as u64,
-                            is_dir: db_entry.is_dir,
-                            path: db_entry.path,
-                            inode: db_entry.inode as u64,
-                            mtime: Some(proto::Timestamp {
-                                seconds: db_entry.mtime_seconds as u64,
-                                nanos: db_entry.mtime_nanos as u32,
-                            }),
-                            is_symlink: db_entry.is_symlink,
-                            is_ignored: db_entry.is_ignored,
-                            is_external: db_entry.is_external,
-                            git_status: db_entry.git_status.map(|status| status as i32),
-                        });
-                    }
-                }
-            }
-
-            // Populate repository entries.
-            {
-                let mut db_repository_entries = worktree_repository::Entity::find()
-                    .filter(
-                        Condition::all()
-                            .add(worktree_repository::Column::ProjectId.eq(project_id))
-                            .add(worktree_repository::Column::IsDeleted.eq(false)),
-                    )
-                    .stream(&*tx)
-                    .await?;
-                while let Some(db_repository_entry) = db_repository_entries.next().await {
-                    let db_repository_entry = db_repository_entry?;
-                    if let Some(worktree) =
-                        worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
-                    {
-                        worktree.repository_entries.insert(
-                            db_repository_entry.work_directory_id as u64,
-                            proto::RepositoryEntry {
-                                work_directory_id: db_repository_entry.work_directory_id as u64,
-                                branch: db_repository_entry.branch,
-                            },
-                        );
-                    }
-                }
-            }
-
-            // Populate worktree diagnostic summaries.
-            {
-                let mut db_summaries = worktree_diagnostic_summary::Entity::find()
-                    .filter(worktree_diagnostic_summary::Column::ProjectId.eq(project_id))
-                    .stream(&*tx)
-                    .await?;
-                while let Some(db_summary) = db_summaries.next().await {
-                    let db_summary = db_summary?;
-                    if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) {
-                        worktree
-                            .diagnostic_summaries
-                            .push(proto::DiagnosticSummary {
-                                path: db_summary.path,
-                                language_server_id: db_summary.language_server_id as u64,
-                                error_count: db_summary.error_count as u32,
-                                warning_count: db_summary.warning_count as u32,
-                            });
-                    }
-                }
-            }
-
-            // Populate worktree settings files
-            {
-                let mut db_settings_files = worktree_settings_file::Entity::find()
-                    .filter(worktree_settings_file::Column::ProjectId.eq(project_id))
-                    .stream(&*tx)
-                    .await?;
-                while let Some(db_settings_file) = db_settings_files.next().await {
-                    let db_settings_file = db_settings_file?;
-                    if let Some(worktree) =
-                        worktrees.get_mut(&(db_settings_file.worktree_id as u64))
-                    {
-                        worktree.settings_files.push(WorktreeSettingsFile {
-                            path: db_settings_file.path,
-                            content: db_settings_file.content,
-                        });
-                    }
-                }
-            }
-
-            // Populate language servers.
-            let language_servers = project
-                .find_related(language_server::Entity)
-                .all(&*tx)
-                .await?;
-
-            let project = Project {
-                collaborators: collaborators
-                    .into_iter()
-                    .map(|collaborator| ProjectCollaborator {
-                        connection_id: collaborator.connection(),
-                        user_id: collaborator.user_id,
-                        replica_id: collaborator.replica_id,
-                        is_host: collaborator.is_host,
-                    })
-                    .collect(),
-                worktrees,
-                language_servers: language_servers
-                    .into_iter()
-                    .map(|language_server| proto::LanguageServer {
-                        id: language_server.id as u64,
-                        name: language_server.name,
-                    })
-                    .collect(),
-            };
-            Ok((project, replica_id as ReplicaId))
-        })
-        .await
-    }
-
-    pub async fn leave_project(
-        &self,
-        project_id: ProjectId,
-        connection: ConnectionId,
-    ) -> Result<RoomGuard<(proto::Room, LeftProject)>> {
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            let result = project_collaborator::Entity::delete_many()
-                .filter(
-                    Condition::all()
-                        .add(project_collaborator::Column::ProjectId.eq(project_id))
-                        .add(project_collaborator::Column::ConnectionId.eq(connection.id as i32))
-                        .add(
-                            project_collaborator::Column::ConnectionServerId
-                                .eq(connection.owner_id as i32),
-                        ),
-                )
-                .exec(&*tx)
-                .await?;
-            if result.rows_affected == 0 {
-                Err(anyhow!("not a collaborator on this project"))?;
-            }
-
-            let project = project::Entity::find_by_id(project_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such project"))?;
-            let collaborators = project
-                .find_related(project_collaborator::Entity)
-                .all(&*tx)
-                .await?;
-            let connection_ids = collaborators
-                .into_iter()
-                .map(|collaborator| collaborator.connection())
-                .collect();
-
-            follower::Entity::delete_many()
-                .filter(
-                    Condition::any()
-                        .add(
-                            Condition::all()
-                                .add(follower::Column::ProjectId.eq(project_id))
-                                .add(
-                                    follower::Column::LeaderConnectionServerId
-                                        .eq(connection.owner_id),
-                                )
-                                .add(follower::Column::LeaderConnectionId.eq(connection.id)),
-                        )
-                        .add(
-                            Condition::all()
-                                .add(follower::Column::ProjectId.eq(project_id))
-                                .add(
-                                    follower::Column::FollowerConnectionServerId
-                                        .eq(connection.owner_id),
-                                )
-                                .add(follower::Column::FollowerConnectionId.eq(connection.id)),
-                        ),
-                )
-                .exec(&*tx)
-                .await?;
-
-            let room = self.get_room(project.room_id, &tx).await?;
-            let left_project = LeftProject {
-                id: project_id,
-                host_user_id: project.host_user_id,
-                host_connection_id: project.host_connection()?,
-                connection_ids,
-            };
-            Ok((room, left_project))
-        })
-        .await
-    }
-
-    pub async fn project_collaborators(
-        &self,
-        project_id: ProjectId,
-        connection_id: ConnectionId,
-    ) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            let collaborators = project_collaborator::Entity::find()
-                .filter(project_collaborator::Column::ProjectId.eq(project_id))
-                .all(&*tx)
-                .await?
-                .into_iter()
-                .map(|collaborator| ProjectCollaborator {
-                    connection_id: collaborator.connection(),
-                    user_id: collaborator.user_id,
-                    replica_id: collaborator.replica_id,
-                    is_host: collaborator.is_host,
-                })
-                .collect::<Vec<_>>();
-
-            if collaborators
-                .iter()
-                .any(|collaborator| collaborator.connection_id == connection_id)
-            {
-                Ok(collaborators)
-            } else {
-                Err(anyhow!("no such project"))?
-            }
-        })
-        .await
-    }
-
-    pub async fn project_connection_ids(
-        &self,
-        project_id: ProjectId,
-        connection_id: ConnectionId,
-    ) -> Result<RoomGuard<HashSet<ConnectionId>>> {
-        let room_id = self.room_id_for_project(project_id).await?;
-        self.room_transaction(room_id, |tx| async move {
-            let mut collaborators = project_collaborator::Entity::find()
-                .filter(project_collaborator::Column::ProjectId.eq(project_id))
-                .stream(&*tx)
-                .await?;
-
-            let mut connection_ids = HashSet::default();
-            while let Some(collaborator) = collaborators.next().await {
-                let collaborator = collaborator?;
-                connection_ids.insert(collaborator.connection());
-            }
-
-            if connection_ids.contains(&connection_id) {
-                Ok(connection_ids)
-            } else {
-                Err(anyhow!("no such project"))?
-            }
-        })
-        .await
-    }
-
-    async fn project_guest_connection_ids(
-        &self,
-        project_id: ProjectId,
-        tx: &DatabaseTransaction,
-    ) -> Result<Vec<ConnectionId>> {
-        let mut collaborators = project_collaborator::Entity::find()
-            .filter(
-                project_collaborator::Column::ProjectId
-                    .eq(project_id)
-                    .and(project_collaborator::Column::IsHost.eq(false)),
-            )
-            .stream(tx)
-            .await?;
-
-        let mut guest_connection_ids = Vec::new();
-        while let Some(collaborator) = collaborators.next().await {
-            let collaborator = collaborator?;
-            guest_connection_ids.push(collaborator.connection());
-        }
-        Ok(guest_connection_ids)
-    }
-
-    async fn room_id_for_project(&self, project_id: ProjectId) -> Result<RoomId> {
-        self.transaction(|tx| async move {
-            let project = project::Entity::find_by_id(project_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("project {} not found", project_id))?;
-            Ok(project.room_id)
-        })
-        .await
-    }
-
-    // access tokens
-
-    pub async fn create_access_token(
-        &self,
-        user_id: UserId,
-        access_token_hash: &str,
-        max_access_token_count: usize,
-    ) -> Result<AccessTokenId> {
-        self.transaction(|tx| async {
-            let tx = tx;
-
-            let token = access_token::ActiveModel {
-                user_id: ActiveValue::set(user_id),
-                hash: ActiveValue::set(access_token_hash.into()),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-
-            access_token::Entity::delete_many()
-                .filter(
-                    access_token::Column::Id.in_subquery(
-                        Query::select()
-                            .column(access_token::Column::Id)
-                            .from(access_token::Entity)
-                            .and_where(access_token::Column::UserId.eq(user_id))
-                            .order_by(access_token::Column::Id, sea_orm::Order::Desc)
-                            .limit(10000)
-                            .offset(max_access_token_count as u64)
-                            .to_owned(),
-                    ),
-                )
-                .exec(&*tx)
-                .await?;
-            Ok(token.id)
-        })
-        .await
-    }
-
-    pub async fn get_access_token(
-        &self,
-        access_token_id: AccessTokenId,
-    ) -> Result<access_token::Model> {
-        self.transaction(|tx| async move {
-            Ok(access_token::Entity::find_by_id(access_token_id)
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("no such access token"))?)
-        })
-        .await
-    }
-
-    // channels
-
-    pub async fn create_root_channel(
-        &self,
-        name: &str,
-        live_kit_room: &str,
-        creator_id: UserId,
-    ) -> Result<ChannelId> {
-        self.create_channel(name, None, live_kit_room, creator_id)
-            .await
-    }
-
-    pub async fn create_channel(
-        &self,
-        name: &str,
-        parent: Option<ChannelId>,
-        live_kit_room: &str,
-        creator_id: UserId,
-    ) -> Result<ChannelId> {
-        let name = Self::sanitize_channel_name(name)?;
-        self.transaction(move |tx| async move {
-            if let Some(parent) = parent {
-                self.check_user_is_channel_admin(parent, creator_id, &*tx)
-                    .await?;
-            }
-
-            let channel = channel::ActiveModel {
-                name: ActiveValue::Set(name.to_string()),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-
-            let channel_paths_stmt;
-            if let Some(parent) = parent {
-                let sql = r#"
-                    INSERT INTO channel_paths
-                    (id_path, channel_id)
-                    SELECT
-                        id_path || $1 || '/', $2
-                    FROM
-                        channel_paths
-                    WHERE
-                        channel_id = $3
-                "#;
-                channel_paths_stmt = Statement::from_sql_and_values(
-                    self.pool.get_database_backend(),
-                    sql,
-                    [
-                        channel.id.to_proto().into(),
-                        channel.id.to_proto().into(),
-                        parent.to_proto().into(),
-                    ],
-                );
-                tx.execute(channel_paths_stmt).await?;
-            } else {
-                channel_path::Entity::insert(channel_path::ActiveModel {
-                    channel_id: ActiveValue::Set(channel.id),
-                    id_path: ActiveValue::Set(format!("/{}/", channel.id)),
-                })
-                .exec(&*tx)
-                .await?;
-            }
-
-            channel_member::ActiveModel {
-                channel_id: ActiveValue::Set(channel.id),
-                user_id: ActiveValue::Set(creator_id),
-                accepted: ActiveValue::Set(true),
-                admin: ActiveValue::Set(true),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-
-            room::ActiveModel {
-                channel_id: ActiveValue::Set(Some(channel.id)),
-                live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-
-            Ok(channel.id)
-        })
-        .await
-    }
-
-    pub async fn remove_channel(
-        &self,
-        channel_id: ChannelId,
-        user_id: UserId,
-    ) -> Result<(Vec<ChannelId>, Vec<UserId>)> {
-        self.transaction(move |tx| async move {
-            self.check_user_is_channel_admin(channel_id, user_id, &*tx)
-                .await?;
-
-            // Don't remove descendant channels that have additional parents.
-            let mut channels_to_remove = self.get_channel_descendants([channel_id], &*tx).await?;
-            {
-                let mut channels_to_keep = channel_path::Entity::find()
-                    .filter(
-                        channel_path::Column::ChannelId
-                            .is_in(
-                                channels_to_remove
-                                    .keys()
-                                    .copied()
-                                    .filter(|&id| id != channel_id),
-                            )
-                            .and(
-                                channel_path::Column::IdPath
-                                    .not_like(&format!("%/{}/%", channel_id)),
-                            ),
-                    )
-                    .stream(&*tx)
-                    .await?;
-                while let Some(row) = channels_to_keep.next().await {
-                    let row = row?;
-                    channels_to_remove.remove(&row.channel_id);
-                }
-            }
-
-            let channel_ancestors = self.get_channel_ancestors(channel_id, &*tx).await?;
-            let members_to_notify: Vec<UserId> = channel_member::Entity::find()
-                .filter(channel_member::Column::ChannelId.is_in(channel_ancestors))
-                .select_only()
-                .column(channel_member::Column::UserId)
-                .distinct()
-                .into_values::<_, QueryUserIds>()
-                .all(&*tx)
-                .await?;
-
-            channel::Entity::delete_many()
-                .filter(channel::Column::Id.is_in(channels_to_remove.keys().copied()))
-                .exec(&*tx)
-                .await?;
-
-            Ok((channels_to_remove.into_keys().collect(), members_to_notify))
-        })
-        .await
-    }
-
-    pub async fn invite_channel_member(
-        &self,
-        channel_id: ChannelId,
-        invitee_id: UserId,
-        inviter_id: UserId,
-        is_admin: bool,
-    ) -> Result<()> {
-        self.transaction(move |tx| async move {
-            self.check_user_is_channel_admin(channel_id, inviter_id, &*tx)
-                .await?;
-
-            channel_member::ActiveModel {
-                channel_id: ActiveValue::Set(channel_id),
-                user_id: ActiveValue::Set(invitee_id),
-                accepted: ActiveValue::Set(false),
-                admin: ActiveValue::Set(is_admin),
-                ..Default::default()
-            }
-            .insert(&*tx)
-            .await?;
-
-            Ok(())
-        })
-        .await
-    }
-
-    fn sanitize_channel_name(name: &str) -> Result<&str> {
-        let new_name = name.trim().trim_start_matches('#');
-        if new_name == "" {
-            Err(anyhow!("channel name can't be blank"))?;
-        }
-        Ok(new_name)
-    }
-
-    pub async fn rename_channel(
-        &self,
-        channel_id: ChannelId,
-        user_id: UserId,
-        new_name: &str,
-    ) -> Result<String> {
-        self.transaction(move |tx| async move {
-            let new_name = Self::sanitize_channel_name(new_name)?.to_string();
-
-            self.check_user_is_channel_admin(channel_id, user_id, &*tx)
-                .await?;
-
-            channel::ActiveModel {
-                id: ActiveValue::Unchanged(channel_id),
-                name: ActiveValue::Set(new_name.clone()),
-                ..Default::default()
-            }
-            .update(&*tx)
-            .await?;
-
-            Ok(new_name)
-        })
-        .await
-    }
-
-    pub async fn respond_to_channel_invite(
-        &self,
-        channel_id: ChannelId,
-        user_id: UserId,
-        accept: bool,
-    ) -> Result<()> {
-        self.transaction(move |tx| async move {
-            let rows_affected = if accept {
-                channel_member::Entity::update_many()
-                    .set(channel_member::ActiveModel {
-                        accepted: ActiveValue::Set(accept),
-                        ..Default::default()
-                    })
-                    .filter(
-                        channel_member::Column::ChannelId
-                            .eq(channel_id)
-                            .and(channel_member::Column::UserId.eq(user_id))
-                            .and(channel_member::Column::Accepted.eq(false)),
-                    )
-                    .exec(&*tx)
-                    .await?
-                    .rows_affected
-            } else {
-                channel_member::ActiveModel {
-                    channel_id: ActiveValue::Unchanged(channel_id),
-                    user_id: ActiveValue::Unchanged(user_id),
-                    ..Default::default()
-                }
-                .delete(&*tx)
-                .await?
-                .rows_affected
-            };
-
-            if rows_affected == 0 {
-                Err(anyhow!("no such invitation"))?;
-            }
-
-            Ok(())
-        })
-        .await
-    }
-
-    pub async fn remove_channel_member(
-        &self,
-        channel_id: ChannelId,
-        member_id: UserId,
-        remover_id: UserId,
-    ) -> Result<()> {
-        self.transaction(|tx| async move {
-            self.check_user_is_channel_admin(channel_id, remover_id, &*tx)
-                .await?;
-
-            let result = channel_member::Entity::delete_many()
-                .filter(
-                    channel_member::Column::ChannelId
-                        .eq(channel_id)
-                        .and(channel_member::Column::UserId.eq(member_id)),
-                )
-                .exec(&*tx)
-                .await?;
-
-            if result.rows_affected == 0 {
-                Err(anyhow!("no such member"))?;
-            }
-
-            Ok(())
-        })
-        .await
-    }
-
-    pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result<Vec<Channel>> {
-        self.transaction(|tx| async move {
-            let channel_invites = channel_member::Entity::find()
-                .filter(
-                    channel_member::Column::UserId
-                        .eq(user_id)
-                        .and(channel_member::Column::Accepted.eq(false)),
-                )
-                .all(&*tx)
-                .await?;
-
-            let channels = channel::Entity::find()
-                .filter(
-                    channel::Column::Id.is_in(
-                        channel_invites
-                            .into_iter()
-                            .map(|channel_member| channel_member.channel_id),
-                    ),
-                )
-                .all(&*tx)
-                .await?;
-
-            let channels = channels
-                .into_iter()
-                .map(|channel| Channel {
-                    id: channel.id,
-                    name: channel.name,
-                    parent_id: None,
-                })
-                .collect();
-
-            Ok(channels)
-        })
-        .await
-    }
-
-    pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
-        self.transaction(|tx| async move {
-            let tx = tx;
-
-            let channel_memberships = channel_member::Entity::find()
-                .filter(
-                    channel_member::Column::UserId
-                        .eq(user_id)
-                        .and(channel_member::Column::Accepted.eq(true)),
-                )
-                .all(&*tx)
-                .await?;
-
-            let parents_by_child_id = self
-                .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx)
-                .await?;
-
-            let channels_with_admin_privileges = channel_memberships
-                .iter()
-                .filter_map(|membership| membership.admin.then_some(membership.channel_id))
-                .collect();
-
-            let mut channels = Vec::with_capacity(parents_by_child_id.len());
-            {
-                let mut rows = channel::Entity::find()
-                    .filter(channel::Column::Id.is_in(parents_by_child_id.keys().copied()))
-                    .stream(&*tx)
-                    .await?;
-                while let Some(row) = rows.next().await {
-                    let row = row?;
-                    channels.push(Channel {
-                        id: row.id,
-                        name: row.name,
-                        parent_id: parents_by_child_id.get(&row.id).copied().flatten(),
-                    });
-                }
-            }
-
-            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-            enum QueryUserIdsAndChannelIds {
-                ChannelId,
-                UserId,
-            }
-
-            let mut channel_participants: HashMap<ChannelId, Vec<UserId>> = HashMap::default();
-            {
-                let mut rows = room_participant::Entity::find()
-                    .inner_join(room::Entity)
-                    .filter(room::Column::ChannelId.is_in(channels.iter().map(|c| c.id)))
-                    .select_only()
-                    .column(room::Column::ChannelId)
-                    .column(room_participant::Column::UserId)
-                    .into_values::<_, QueryUserIdsAndChannelIds>()
-                    .stream(&*tx)
-                    .await?;
-                while let Some(row) = rows.next().await {
-                    let row: (ChannelId, UserId) = row?;
-                    channel_participants.entry(row.0).or_default().push(row.1)
-                }
-            }
-
-            Ok(ChannelsForUser {
-                channels,
-                channel_participants,
-                channels_with_admin_privileges,
-            })
-        })
-        .await
-    }
+mod db_tests;
+#[cfg(test)]
+pub mod test_db;
 
-    pub async fn get_channel_members(&self, id: ChannelId) -> Result<Vec<UserId>> {
-        self.transaction(|tx| async move { self.get_channel_members_internal(id, &*tx).await })
-            .await
-    }
+mod ids;
+mod queries;
+mod tables;
 
-    pub async fn set_channel_member_admin(
-        &self,
-        channel_id: ChannelId,
-        from: UserId,
-        for_user: UserId,
-        admin: bool,
-    ) -> Result<()> {
-        self.transaction(|tx| async move {
-            self.check_user_is_channel_admin(channel_id, from, &*tx)
-                .await?;
+use crate::{executor::Executor, Error, Result};
+use anyhow::anyhow;
+use collections::{BTreeMap, HashMap, HashSet};
+use dashmap::DashMap;
+use futures::StreamExt;
+use rand::{prelude::StdRng, Rng, SeedableRng};
+use rpc::{proto, ConnectionId};
+use sea_orm::{
+    entity::prelude::*, ActiveValue, Condition, ConnectionTrait, DatabaseConnection,
+    DatabaseTransaction, DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType,
+    QueryOrder, QuerySelect, Statement, TransactionTrait,
+};
+use sea_query::{Alias, Expr, OnConflict, Query};
+use serde::{Deserialize, Serialize};
+use sqlx::{
+    migrate::{Migrate, Migration, MigrationSource},
+    Connection,
+};
+use std::{
+    fmt::Write as _,
+    future::Future,
+    marker::PhantomData,
+    ops::{Deref, DerefMut},
+    path::Path,
+    rc::Rc,
+    sync::Arc,
+    time::Duration,
+};
+use tables::*;
+use tokio::sync::{Mutex, OwnedMutexGuard};
 
-            let result = channel_member::Entity::update_many()
-                .filter(
-                    channel_member::Column::ChannelId
-                        .eq(channel_id)
-                        .and(channel_member::Column::UserId.eq(for_user)),
-                )
-                .set(channel_member::ActiveModel {
-                    admin: ActiveValue::set(admin),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
+pub use ids::*;
+pub use sea_orm::ConnectOptions;
+pub use tables::user::Model as User;
 
-            if result.rows_affected == 0 {
-                Err(anyhow!("no such member"))?;
-            }
+pub struct Database {
+    options: ConnectOptions,
+    pool: DatabaseConnection,
+    rooms: DashMap<RoomId, Arc<Mutex<()>>>,
+    rng: Mutex<StdRng>,
+    executor: Executor,
+    #[cfg(test)]
+    runtime: Option<tokio::runtime::Runtime>,
+}
 
-            Ok(())
+impl Database {
+    pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> {
+        Ok(Self {
+            options: options.clone(),
+            pool: sea_orm::Database::connect(options).await?,
+            rooms: DashMap::with_capacity(16384),
+            rng: Mutex::new(StdRng::seed_from_u64(0)),
+            executor,
+            #[cfg(test)]
+            runtime: None,
         })
-        .await
     }
 
-    pub async fn get_channel_member_details(
-        &self,
-        channel_id: ChannelId,
-        user_id: UserId,
-    ) -> Result<Vec<proto::ChannelMember>> {
-        self.transaction(|tx| async move {
-            self.check_user_is_channel_admin(channel_id, user_id, &*tx)
-                .await?;
-
-            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-            enum QueryMemberDetails {
-                UserId,
-                Admin,
-                IsDirectMember,
-                Accepted,
-            }
-
-            let tx = tx;
-            let ancestor_ids = self.get_channel_ancestors(channel_id, &*tx).await?;
-            let mut stream = channel_member::Entity::find()
-                .distinct()
-                .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied()))
-                .select_only()
-                .column(channel_member::Column::UserId)
-                .column(channel_member::Column::Admin)
-                .column_as(
-                    channel_member::Column::ChannelId.eq(channel_id),
-                    QueryMemberDetails::IsDirectMember,
-                )
-                .column(channel_member::Column::Accepted)
-                .order_by_asc(channel_member::Column::UserId)
-                .into_values::<_, QueryMemberDetails>()
-                .stream(&*tx)
-                .await?;
-
-            let mut rows = Vec::<proto::ChannelMember>::new();
-            while let Some(row) = stream.next().await {
-                let (user_id, is_admin, is_direct_member, is_invite_accepted): (
-                    UserId,
-                    bool,
-                    bool,
-                    bool,
-                ) = row?;
-                let kind = match (is_direct_member, is_invite_accepted) {
-                    (true, true) => proto::channel_member::Kind::Member,
-                    (true, false) => proto::channel_member::Kind::Invitee,
-                    (false, true) => proto::channel_member::Kind::AncestorMember,
-                    (false, false) => continue,
-                };
-                let user_id = user_id.to_proto();
-                let kind = kind.into();
-                if let Some(last_row) = rows.last_mut() {
-                    if last_row.user_id == user_id {
-                        if is_direct_member {
-                            last_row.kind = kind;
-                            last_row.admin = is_admin;
-                        }
-                        continue;
-                    }
-                }
-                rows.push(proto::ChannelMember {
-                    user_id,
-                    kind,
-                    admin: is_admin,
-                });
-            }
-
-            Ok(rows)
-        })
-        .await
+    #[cfg(test)]
+    pub fn reset(&self) {
+        self.rooms.clear();
     }
 
-    pub async fn get_channel_members_internal(
+    pub async fn migrate(
         &self,
-        id: ChannelId,
-        tx: &DatabaseTransaction,
-    ) -> Result<Vec<UserId>> {
-        let ancestor_ids = self.get_channel_ancestors(id, tx).await?;
-        let user_ids = channel_member::Entity::find()
-            .distinct()
-            .filter(
-                channel_member::Column::ChannelId
-                    .is_in(ancestor_ids.iter().copied())
-                    .and(channel_member::Column::Accepted.eq(true)),
-            )
-            .select_only()
-            .column(channel_member::Column::UserId)
-            .into_values::<_, QueryUserIds>()
-            .all(&*tx)
-            .await?;
-        Ok(user_ids)
-    }
+        migrations_path: &Path,
+        ignore_checksum_mismatch: bool,
+    ) -> anyhow::Result<Vec<(Migration, Duration)>> {
+        let migrations = MigrationSource::resolve(migrations_path)
+            .await
+            .map_err(|err| anyhow!("failed to load migrations: {err:?}"))?;
 
-    async fn check_user_is_channel_member(
-        &self,
-        channel_id: ChannelId,
-        user_id: UserId,
-        tx: &DatabaseTransaction,
-    ) -> Result<()> {
-        let channel_ids = self.get_channel_ancestors(channel_id, tx).await?;
-        channel_member::Entity::find()
-            .filter(
-                channel_member::Column::ChannelId
-                    .is_in(channel_ids)
-                    .and(channel_member::Column::UserId.eq(user_id)),
-            )
-            .one(&*tx)
-            .await?
-            .ok_or_else(|| anyhow!("user is not a channel member or channel does not exist"))?;
-        Ok(())
-    }
+        let mut connection = sqlx::AnyConnection::connect(self.options.get_url()).await?;
 
-    async fn check_user_is_channel_admin(
-        &self,
-        channel_id: ChannelId,
-        user_id: UserId,
-        tx: &DatabaseTransaction,
-    ) -> Result<()> {
-        let channel_ids = self.get_channel_ancestors(channel_id, tx).await?;
-        channel_member::Entity::find()
-            .filter(
-                channel_member::Column::ChannelId
-                    .is_in(channel_ids)
-                    .and(channel_member::Column::UserId.eq(user_id))
-                    .and(channel_member::Column::Admin.eq(true)),
-            )
-            .one(&*tx)
+        connection.ensure_migrations_table().await?;
+        let applied_migrations: HashMap<_, _> = connection
+            .list_applied_migrations()
             .await?
-            .ok_or_else(|| anyhow!("user is not a channel admin or channel does not exist"))?;
-        Ok(())
-    }
+            .into_iter()
+            .map(|m| (m.version, m))
+            .collect();
 
-    async fn get_channel_ancestors(
-        &self,
-        channel_id: ChannelId,
-        tx: &DatabaseTransaction,
-    ) -> Result<Vec<ChannelId>> {
-        let paths = channel_path::Entity::find()
-            .filter(channel_path::Column::ChannelId.eq(channel_id))
-            .all(tx)
-            .await?;
-        let mut channel_ids = Vec::new();
-        for path in paths {
-            for id in path.id_path.trim_matches('/').split('/') {
-                if let Ok(id) = id.parse() {
-                    let id = ChannelId::from_proto(id);
-                    if let Err(ix) = channel_ids.binary_search(&id) {
-                        channel_ids.insert(ix, id);
+        let mut new_migrations = Vec::new();
+        for migration in migrations {
+            match applied_migrations.get(&migration.version) {
+                Some(applied_migration) => {
+                    if migration.checksum != applied_migration.checksum && !ignore_checksum_mismatch
+                    {
+                        Err(anyhow!(
+                            "checksum mismatch for applied migration {}",
+                            migration.description
+                        ))?;
                     }
                 }
-            }
-        }
-        Ok(channel_ids)
-    }
-
-    async fn get_channel_descendants(
-        &self,
-        channel_ids: impl IntoIterator<Item = ChannelId>,
-        tx: &DatabaseTransaction,
-    ) -> Result<HashMap<ChannelId, Option<ChannelId>>> {
-        let mut values = String::new();
-        for id in channel_ids {
-            if !values.is_empty() {
-                values.push_str(", ");
-            }
-            write!(&mut values, "({})", id).unwrap();
-        }
-
-        if values.is_empty() {
-            return Ok(HashMap::default());
-        }
-
-        let sql = format!(
-            r#"
-            SELECT
-                descendant_paths.*
-            FROM
-                channel_paths parent_paths, channel_paths descendant_paths
-            WHERE
-                parent_paths.channel_id IN ({values}) AND
-                descendant_paths.id_path LIKE (parent_paths.id_path || '%')
-        "#
-        );
-
-        let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
-
-        let mut parents_by_child_id = HashMap::default();
-        let mut paths = channel_path::Entity::find()
-            .from_raw_sql(stmt)
-            .stream(tx)
-            .await?;
-
-        while let Some(path) = paths.next().await {
-            let path = path?;
-            let ids = path.id_path.trim_matches('/').split('/');
-            let mut parent_id = None;
-            for id in ids {
-                if let Ok(id) = id.parse() {
-                    let id = ChannelId::from_proto(id);
-                    if id == path.channel_id {
-                        break;
-                    }
-                    parent_id = Some(id);
+                None => {
+                    let elapsed = connection.apply(&migration).await?;
+                    new_migrations.push((migration, elapsed));
                 }
             }
-            parents_by_child_id.insert(path.channel_id, parent_id);
         }
 
-        Ok(parents_by_child_id)
-    }
-
-    /// Returns the channel with the given ID and:
-    /// - true if the user is a member
-    /// - false if the user hasn't accepted the invitation yet
-    pub async fn get_channel(
-        &self,
-        channel_id: ChannelId,
-        user_id: UserId,
-    ) -> Result<Option<(Channel, bool)>> {
-        self.transaction(|tx| async move {
-            let tx = tx;
-
-            let channel = channel::Entity::find_by_id(channel_id).one(&*tx).await?;
-
-            if let Some(channel) = channel {
-                if self
-                    .check_user_is_channel_member(channel_id, user_id, &*tx)
-                    .await
-                    .is_err()
-                {
-                    return Ok(None);
-                }
-
-                let channel_membership = channel_member::Entity::find()
-                    .filter(
-                        channel_member::Column::ChannelId
-                            .eq(channel_id)
-                            .and(channel_member::Column::UserId.eq(user_id)),
-                    )
-                    .one(&*tx)
-                    .await?;
-
-                let is_accepted = channel_membership
-                    .map(|membership| membership.accepted)
-                    .unwrap_or(false);
-
-                Ok(Some((
-                    Channel {
-                        id: channel.id,
-                        name: channel.name,
-                        parent_id: None,
-                    },
-                    is_accepted,
-                )))
-            } else {
-                Ok(None)
-            }
-        })
-        .await
-    }
-
-    pub async fn room_id_for_channel(&self, channel_id: ChannelId) -> Result<RoomId> {
-        self.transaction(|tx| async move {
-            let tx = tx;
-            let room = channel::Model {
-                id: channel_id,
-                ..Default::default()
-            }
-            .find_related(room::Entity)
-            .one(&*tx)
-            .await?
-            .ok_or_else(|| anyhow!("invalid channel"))?;
-            Ok(room.id)
-        })
-        .await
+        Ok(new_migrations)
     }
 
     async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>

crates/collab/src/db/tests.rs → crates/collab/src/db/db_tests.rs 🔗

@@ -1,9 +1,8 @@
 use super::*;
 use gpui::executor::{Background, Deterministic};
-use std::sync::Arc;
-
-#[cfg(test)]
 use pretty_assertions::{assert_eq, assert_ne};
+use std::sync::Arc;
+use test_db::TestDb;
 
 macro_rules! test_both_dbs {
     ($postgres_test_name:ident, $sqlite_test_name:ident, $db:ident, $body:block) => {

crates/collab/src/db/ids.rs 🔗

@@ -0,0 +1,125 @@
+use crate::Result;
+use sea_orm::DbErr;
+use sea_query::{Value, ValueTypeErr};
+use serde::{Deserialize, Serialize};
+
+macro_rules! id_type {
+    ($name:ident) => {
+        #[derive(
+            Clone,
+            Copy,
+            Debug,
+            Default,
+            PartialEq,
+            Eq,
+            PartialOrd,
+            Ord,
+            Hash,
+            Serialize,
+            Deserialize,
+        )]
+        #[serde(transparent)]
+        pub struct $name(pub i32);
+
+        impl $name {
+            #[allow(unused)]
+            pub const MAX: Self = Self(i32::MAX);
+
+            #[allow(unused)]
+            pub fn from_proto(value: u64) -> Self {
+                Self(value as i32)
+            }
+
+            #[allow(unused)]
+            pub fn to_proto(self) -> u64 {
+                self.0 as u64
+            }
+        }
+
+        impl std::fmt::Display for $name {
+            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+                self.0.fmt(f)
+            }
+        }
+
+        impl From<$name> for sea_query::Value {
+            fn from(value: $name) -> Self {
+                sea_query::Value::Int(Some(value.0))
+            }
+        }
+
+        impl sea_orm::TryGetable for $name {
+            fn try_get(
+                res: &sea_orm::QueryResult,
+                pre: &str,
+                col: &str,
+            ) -> Result<Self, sea_orm::TryGetError> {
+                Ok(Self(i32::try_get(res, pre, col)?))
+            }
+        }
+
+        impl sea_query::ValueType for $name {
+            fn try_from(v: Value) -> Result<Self, sea_query::ValueTypeErr> {
+                Ok(Self(value_to_integer(v)?))
+            }
+
+            fn type_name() -> String {
+                stringify!($name).into()
+            }
+
+            fn array_type() -> sea_query::ArrayType {
+                sea_query::ArrayType::Int
+            }
+
+            fn column_type() -> sea_query::ColumnType {
+                sea_query::ColumnType::Integer(None)
+            }
+        }
+
+        impl sea_orm::TryFromU64 for $name {
+            fn try_from_u64(n: u64) -> Result<Self, DbErr> {
+                Ok(Self(n.try_into().map_err(|_| {
+                    DbErr::ConvertFromU64(concat!(
+                        "error converting ",
+                        stringify!($name),
+                        " to u64"
+                    ))
+                })?))
+            }
+        }
+
+        impl sea_query::Nullable for $name {
+            fn null() -> Value {
+                Value::Int(None)
+            }
+        }
+    };
+}
+
+fn value_to_integer(v: Value) -> Result<i32, ValueTypeErr> {
+    match v {
+        Value::TinyInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
+        Value::SmallInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
+        Value::Int(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
+        Value::BigInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
+        Value::TinyUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
+        Value::SmallUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
+        Value::Unsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
+        Value::BigUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
+        _ => Err(ValueTypeErr),
+    }
+}
+
+id_type!(AccessTokenId);
+id_type!(ChannelId);
+id_type!(ChannelMemberId);
+id_type!(ContactId);
+id_type!(FollowerId);
+id_type!(RoomId);
+id_type!(RoomParticipantId);
+id_type!(ProjectId);
+id_type!(ProjectCollaboratorId);
+id_type!(ReplicaId);
+id_type!(ServerId);
+id_type!(SignupId);
+id_type!(UserId);

crates/collab/src/db/queries.rs 🔗

@@ -0,0 +1,10 @@
+use super::*;
+
+pub mod access_tokens;
+pub mod channels;
+pub mod contacts;
+pub mod projects;
+pub mod rooms;
+pub mod servers;
+pub mod signups;
+pub mod users;

crates/collab/src/db/queries/access_tokens.rs 🔗

@@ -0,0 +1,53 @@
+use super::*;
+
+impl Database {
+    pub async fn create_access_token(
+        &self,
+        user_id: UserId,
+        access_token_hash: &str,
+        max_access_token_count: usize,
+    ) -> Result<AccessTokenId> {
+        self.transaction(|tx| async {
+            let tx = tx;
+
+            let token = access_token::ActiveModel {
+                user_id: ActiveValue::set(user_id),
+                hash: ActiveValue::set(access_token_hash.into()),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            access_token::Entity::delete_many()
+                .filter(
+                    access_token::Column::Id.in_subquery(
+                        Query::select()
+                            .column(access_token::Column::Id)
+                            .from(access_token::Entity)
+                            .and_where(access_token::Column::UserId.eq(user_id))
+                            .order_by(access_token::Column::Id, sea_orm::Order::Desc)
+                            .limit(10000)
+                            .offset(max_access_token_count as u64)
+                            .to_owned(),
+                    ),
+                )
+                .exec(&*tx)
+                .await?;
+            Ok(token.id)
+        })
+        .await
+    }
+
+    pub async fn get_access_token(
+        &self,
+        access_token_id: AccessTokenId,
+    ) -> Result<access_token::Model> {
+        self.transaction(|tx| async move {
+            Ok(access_token::Entity::find_by_id(access_token_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such access token"))?)
+        })
+        .await
+    }
+}

crates/collab/src/db/queries/channels.rs 🔗

@@ -0,0 +1,697 @@
+use super::*;
+
+impl Database {
+    pub async fn create_root_channel(
+        &self,
+        name: &str,
+        live_kit_room: &str,
+        creator_id: UserId,
+    ) -> Result<ChannelId> {
+        self.create_channel(name, None, live_kit_room, creator_id)
+            .await
+    }
+
+    pub async fn create_channel(
+        &self,
+        name: &str,
+        parent: Option<ChannelId>,
+        live_kit_room: &str,
+        creator_id: UserId,
+    ) -> Result<ChannelId> {
+        let name = Self::sanitize_channel_name(name)?;
+        self.transaction(move |tx| async move {
+            if let Some(parent) = parent {
+                self.check_user_is_channel_admin(parent, creator_id, &*tx)
+                    .await?;
+            }
+
+            let channel = channel::ActiveModel {
+                name: ActiveValue::Set(name.to_string()),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            let channel_paths_stmt;
+            if let Some(parent) = parent {
+                let sql = r#"
+                    INSERT INTO channel_paths
+                    (id_path, channel_id)
+                    SELECT
+                        id_path || $1 || '/', $2
+                    FROM
+                        channel_paths
+                    WHERE
+                        channel_id = $3
+                "#;
+                channel_paths_stmt = Statement::from_sql_and_values(
+                    self.pool.get_database_backend(),
+                    sql,
+                    [
+                        channel.id.to_proto().into(),
+                        channel.id.to_proto().into(),
+                        parent.to_proto().into(),
+                    ],
+                );
+                tx.execute(channel_paths_stmt).await?;
+            } else {
+                channel_path::Entity::insert(channel_path::ActiveModel {
+                    channel_id: ActiveValue::Set(channel.id),
+                    id_path: ActiveValue::Set(format!("/{}/", channel.id)),
+                })
+                .exec(&*tx)
+                .await?;
+            }
+
+            channel_member::ActiveModel {
+                channel_id: ActiveValue::Set(channel.id),
+                user_id: ActiveValue::Set(creator_id),
+                accepted: ActiveValue::Set(true),
+                admin: ActiveValue::Set(true),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            room::ActiveModel {
+                channel_id: ActiveValue::Set(Some(channel.id)),
+                live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            Ok(channel.id)
+        })
+        .await
+    }
+
+    pub async fn remove_channel(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+    ) -> Result<(Vec<ChannelId>, Vec<UserId>)> {
+        self.transaction(move |tx| async move {
+            self.check_user_is_channel_admin(channel_id, user_id, &*tx)
+                .await?;
+
+            // Don't remove descendant channels that have additional parents.
+            let mut channels_to_remove = self.get_channel_descendants([channel_id], &*tx).await?;
+            {
+                let mut channels_to_keep = channel_path::Entity::find()
+                    .filter(
+                        channel_path::Column::ChannelId
+                            .is_in(
+                                channels_to_remove
+                                    .keys()
+                                    .copied()
+                                    .filter(|&id| id != channel_id),
+                            )
+                            .and(
+                                channel_path::Column::IdPath
+                                    .not_like(&format!("%/{}/%", channel_id)),
+                            ),
+                    )
+                    .stream(&*tx)
+                    .await?;
+                while let Some(row) = channels_to_keep.next().await {
+                    let row = row?;
+                    channels_to_remove.remove(&row.channel_id);
+                }
+            }
+
+            let channel_ancestors = self.get_channel_ancestors(channel_id, &*tx).await?;
+            let members_to_notify: Vec<UserId> = channel_member::Entity::find()
+                .filter(channel_member::Column::ChannelId.is_in(channel_ancestors))
+                .select_only()
+                .column(channel_member::Column::UserId)
+                .distinct()
+                .into_values::<_, QueryUserIds>()
+                .all(&*tx)
+                .await?;
+
+            channel::Entity::delete_many()
+                .filter(channel::Column::Id.is_in(channels_to_remove.keys().copied()))
+                .exec(&*tx)
+                .await?;
+
+            Ok((channels_to_remove.into_keys().collect(), members_to_notify))
+        })
+        .await
+    }
+
+    pub async fn invite_channel_member(
+        &self,
+        channel_id: ChannelId,
+        invitee_id: UserId,
+        inviter_id: UserId,
+        is_admin: bool,
+    ) -> Result<()> {
+        self.transaction(move |tx| async move {
+            self.check_user_is_channel_admin(channel_id, inviter_id, &*tx)
+                .await?;
+
+            channel_member::ActiveModel {
+                channel_id: ActiveValue::Set(channel_id),
+                user_id: ActiveValue::Set(invitee_id),
+                accepted: ActiveValue::Set(false),
+                admin: ActiveValue::Set(is_admin),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            Ok(())
+        })
+        .await
+    }
+
+    fn sanitize_channel_name(name: &str) -> Result<&str> {
+        let new_name = name.trim().trim_start_matches('#');
+        if new_name == "" {
+            Err(anyhow!("channel name can't be blank"))?;
+        }
+        Ok(new_name)
+    }
+
+    pub async fn rename_channel(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+        new_name: &str,
+    ) -> Result<String> {
+        self.transaction(move |tx| async move {
+            let new_name = Self::sanitize_channel_name(new_name)?.to_string();
+
+            self.check_user_is_channel_admin(channel_id, user_id, &*tx)
+                .await?;
+
+            channel::ActiveModel {
+                id: ActiveValue::Unchanged(channel_id),
+                name: ActiveValue::Set(new_name.clone()),
+                ..Default::default()
+            }
+            .update(&*tx)
+            .await?;
+
+            Ok(new_name)
+        })
+        .await
+    }
+
+    pub async fn respond_to_channel_invite(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+        accept: bool,
+    ) -> Result<()> {
+        self.transaction(move |tx| async move {
+            let rows_affected = if accept {
+                channel_member::Entity::update_many()
+                    .set(channel_member::ActiveModel {
+                        accepted: ActiveValue::Set(accept),
+                        ..Default::default()
+                    })
+                    .filter(
+                        channel_member::Column::ChannelId
+                            .eq(channel_id)
+                            .and(channel_member::Column::UserId.eq(user_id))
+                            .and(channel_member::Column::Accepted.eq(false)),
+                    )
+                    .exec(&*tx)
+                    .await?
+                    .rows_affected
+            } else {
+                channel_member::ActiveModel {
+                    channel_id: ActiveValue::Unchanged(channel_id),
+                    user_id: ActiveValue::Unchanged(user_id),
+                    ..Default::default()
+                }
+                .delete(&*tx)
+                .await?
+                .rows_affected
+            };
+
+            if rows_affected == 0 {
+                Err(anyhow!("no such invitation"))?;
+            }
+
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn remove_channel_member(
+        &self,
+        channel_id: ChannelId,
+        member_id: UserId,
+        remover_id: UserId,
+    ) -> Result<()> {
+        self.transaction(|tx| async move {
+            self.check_user_is_channel_admin(channel_id, remover_id, &*tx)
+                .await?;
+
+            let result = channel_member::Entity::delete_many()
+                .filter(
+                    channel_member::Column::ChannelId
+                        .eq(channel_id)
+                        .and(channel_member::Column::UserId.eq(member_id)),
+                )
+                .exec(&*tx)
+                .await?;
+
+            if result.rows_affected == 0 {
+                Err(anyhow!("no such member"))?;
+            }
+
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result<Vec<Channel>> {
+        self.transaction(|tx| async move {
+            let channel_invites = channel_member::Entity::find()
+                .filter(
+                    channel_member::Column::UserId
+                        .eq(user_id)
+                        .and(channel_member::Column::Accepted.eq(false)),
+                )
+                .all(&*tx)
+                .await?;
+
+            let channels = channel::Entity::find()
+                .filter(
+                    channel::Column::Id.is_in(
+                        channel_invites
+                            .into_iter()
+                            .map(|channel_member| channel_member.channel_id),
+                    ),
+                )
+                .all(&*tx)
+                .await?;
+
+            let channels = channels
+                .into_iter()
+                .map(|channel| Channel {
+                    id: channel.id,
+                    name: channel.name,
+                    parent_id: None,
+                })
+                .collect();
+
+            Ok(channels)
+        })
+        .await
+    }
+
+    pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
+        self.transaction(|tx| async move {
+            let tx = tx;
+
+            let channel_memberships = channel_member::Entity::find()
+                .filter(
+                    channel_member::Column::UserId
+                        .eq(user_id)
+                        .and(channel_member::Column::Accepted.eq(true)),
+                )
+                .all(&*tx)
+                .await?;
+
+            let parents_by_child_id = self
+                .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx)
+                .await?;
+
+            let channels_with_admin_privileges = channel_memberships
+                .iter()
+                .filter_map(|membership| membership.admin.then_some(membership.channel_id))
+                .collect();
+
+            let mut channels = Vec::with_capacity(parents_by_child_id.len());
+            {
+                let mut rows = channel::Entity::find()
+                    .filter(channel::Column::Id.is_in(parents_by_child_id.keys().copied()))
+                    .stream(&*tx)
+                    .await?;
+                while let Some(row) = rows.next().await {
+                    let row = row?;
+                    channels.push(Channel {
+                        id: row.id,
+                        name: row.name,
+                        parent_id: parents_by_child_id.get(&row.id).copied().flatten(),
+                    });
+                }
+            }
+
+            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+            enum QueryUserIdsAndChannelIds {
+                ChannelId,
+                UserId,
+            }
+
+            let mut channel_participants: HashMap<ChannelId, Vec<UserId>> = HashMap::default();
+            {
+                let mut rows = room_participant::Entity::find()
+                    .inner_join(room::Entity)
+                    .filter(room::Column::ChannelId.is_in(channels.iter().map(|c| c.id)))
+                    .select_only()
+                    .column(room::Column::ChannelId)
+                    .column(room_participant::Column::UserId)
+                    .into_values::<_, QueryUserIdsAndChannelIds>()
+                    .stream(&*tx)
+                    .await?;
+                while let Some(row) = rows.next().await {
+                    let row: (ChannelId, UserId) = row?;
+                    channel_participants.entry(row.0).or_default().push(row.1)
+                }
+            }
+
+            Ok(ChannelsForUser {
+                channels,
+                channel_participants,
+                channels_with_admin_privileges,
+            })
+        })
+        .await
+    }
+
+    pub async fn get_channel_members(&self, id: ChannelId) -> Result<Vec<UserId>> {
+        self.transaction(|tx| async move { self.get_channel_members_internal(id, &*tx).await })
+            .await
+    }
+
+    pub async fn set_channel_member_admin(
+        &self,
+        channel_id: ChannelId,
+        from: UserId,
+        for_user: UserId,
+        admin: bool,
+    ) -> Result<()> {
+        self.transaction(|tx| async move {
+            self.check_user_is_channel_admin(channel_id, from, &*tx)
+                .await?;
+
+            let result = channel_member::Entity::update_many()
+                .filter(
+                    channel_member::Column::ChannelId
+                        .eq(channel_id)
+                        .and(channel_member::Column::UserId.eq(for_user)),
+                )
+                .set(channel_member::ActiveModel {
+                    admin: ActiveValue::set(admin),
+                    ..Default::default()
+                })
+                .exec(&*tx)
+                .await?;
+
+            if result.rows_affected == 0 {
+                Err(anyhow!("no such member"))?;
+            }
+
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn get_channel_member_details(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+    ) -> Result<Vec<proto::ChannelMember>> {
+        self.transaction(|tx| async move {
+            self.check_user_is_channel_admin(channel_id, user_id, &*tx)
+                .await?;
+
+            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+            enum QueryMemberDetails {
+                UserId,
+                Admin,
+                IsDirectMember,
+                Accepted,
+            }
+
+            let tx = tx;
+            let ancestor_ids = self.get_channel_ancestors(channel_id, &*tx).await?;
+            let mut stream = channel_member::Entity::find()
+                .distinct()
+                .filter(channel_member::Column::ChannelId.is_in(ancestor_ids.iter().copied()))
+                .select_only()
+                .column(channel_member::Column::UserId)
+                .column(channel_member::Column::Admin)
+                .column_as(
+                    channel_member::Column::ChannelId.eq(channel_id),
+                    QueryMemberDetails::IsDirectMember,
+                )
+                .column(channel_member::Column::Accepted)
+                .order_by_asc(channel_member::Column::UserId)
+                .into_values::<_, QueryMemberDetails>()
+                .stream(&*tx)
+                .await?;
+
+            let mut rows = Vec::<proto::ChannelMember>::new();
+            while let Some(row) = stream.next().await {
+                let (user_id, is_admin, is_direct_member, is_invite_accepted): (
+                    UserId,
+                    bool,
+                    bool,
+                    bool,
+                ) = row?;
+                let kind = match (is_direct_member, is_invite_accepted) {
+                    (true, true) => proto::channel_member::Kind::Member,
+                    (true, false) => proto::channel_member::Kind::Invitee,
+                    (false, true) => proto::channel_member::Kind::AncestorMember,
+                    (false, false) => continue,
+                };
+                let user_id = user_id.to_proto();
+                let kind = kind.into();
+                if let Some(last_row) = rows.last_mut() {
+                    if last_row.user_id == user_id {
+                        if is_direct_member {
+                            last_row.kind = kind;
+                            last_row.admin = is_admin;
+                        }
+                        continue;
+                    }
+                }
+                rows.push(proto::ChannelMember {
+                    user_id,
+                    kind,
+                    admin: is_admin,
+                });
+            }
+
+            Ok(rows)
+        })
+        .await
+    }
+
+    pub async fn get_channel_members_internal(
+        &self,
+        id: ChannelId,
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<UserId>> {
+        let ancestor_ids = self.get_channel_ancestors(id, tx).await?;
+        let user_ids = channel_member::Entity::find()
+            .distinct()
+            .filter(
+                channel_member::Column::ChannelId
+                    .is_in(ancestor_ids.iter().copied())
+                    .and(channel_member::Column::Accepted.eq(true)),
+            )
+            .select_only()
+            .column(channel_member::Column::UserId)
+            .into_values::<_, QueryUserIds>()
+            .all(&*tx)
+            .await?;
+        Ok(user_ids)
+    }
+
+    pub async fn check_user_is_channel_member(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+        tx: &DatabaseTransaction,
+    ) -> Result<()> {
+        let channel_ids = self.get_channel_ancestors(channel_id, tx).await?;
+        channel_member::Entity::find()
+            .filter(
+                channel_member::Column::ChannelId
+                    .is_in(channel_ids)
+                    .and(channel_member::Column::UserId.eq(user_id)),
+            )
+            .one(&*tx)
+            .await?
+            .ok_or_else(|| anyhow!("user is not a channel member or channel does not exist"))?;
+        Ok(())
+    }
+
+    pub async fn check_user_is_channel_admin(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+        tx: &DatabaseTransaction,
+    ) -> Result<()> {
+        let channel_ids = self.get_channel_ancestors(channel_id, tx).await?;
+        channel_member::Entity::find()
+            .filter(
+                channel_member::Column::ChannelId
+                    .is_in(channel_ids)
+                    .and(channel_member::Column::UserId.eq(user_id))
+                    .and(channel_member::Column::Admin.eq(true)),
+            )
+            .one(&*tx)
+            .await?
+            .ok_or_else(|| anyhow!("user is not a channel admin or channel does not exist"))?;
+        Ok(())
+    }
+
+    pub async fn get_channel_ancestors(
+        &self,
+        channel_id: ChannelId,
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<ChannelId>> {
+        let paths = channel_path::Entity::find()
+            .filter(channel_path::Column::ChannelId.eq(channel_id))
+            .all(tx)
+            .await?;
+        let mut channel_ids = Vec::new();
+        for path in paths {
+            for id in path.id_path.trim_matches('/').split('/') {
+                if let Ok(id) = id.parse() {
+                    let id = ChannelId::from_proto(id);
+                    if let Err(ix) = channel_ids.binary_search(&id) {
+                        channel_ids.insert(ix, id);
+                    }
+                }
+            }
+        }
+        Ok(channel_ids)
+    }
+
+    async fn get_channel_descendants(
+        &self,
+        channel_ids: impl IntoIterator<Item = ChannelId>,
+        tx: &DatabaseTransaction,
+    ) -> Result<HashMap<ChannelId, Option<ChannelId>>> {
+        let mut values = String::new();
+        for id in channel_ids {
+            if !values.is_empty() {
+                values.push_str(", ");
+            }
+            write!(&mut values, "({})", id).unwrap();
+        }
+
+        if values.is_empty() {
+            return Ok(HashMap::default());
+        }
+
+        let sql = format!(
+            r#"
+            SELECT
+                descendant_paths.*
+            FROM
+                channel_paths parent_paths, channel_paths descendant_paths
+            WHERE
+                parent_paths.channel_id IN ({values}) AND
+                descendant_paths.id_path LIKE (parent_paths.id_path || '%')
+        "#
+        );
+
+        let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
+
+        let mut parents_by_child_id = HashMap::default();
+        let mut paths = channel_path::Entity::find()
+            .from_raw_sql(stmt)
+            .stream(tx)
+            .await?;
+
+        while let Some(path) = paths.next().await {
+            let path = path?;
+            let ids = path.id_path.trim_matches('/').split('/');
+            let mut parent_id = None;
+            for id in ids {
+                if let Ok(id) = id.parse() {
+                    let id = ChannelId::from_proto(id);
+                    if id == path.channel_id {
+                        break;
+                    }
+                    parent_id = Some(id);
+                }
+            }
+            parents_by_child_id.insert(path.channel_id, parent_id);
+        }
+
+        Ok(parents_by_child_id)
+    }
+
+    /// Returns the channel with the given ID and:
+    /// - true if the user is a member
+    /// - false if the user hasn't accepted the invitation yet
+    pub async fn get_channel(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+    ) -> Result<Option<(Channel, bool)>> {
+        self.transaction(|tx| async move {
+            let tx = tx;
+
+            let channel = channel::Entity::find_by_id(channel_id).one(&*tx).await?;
+
+            if let Some(channel) = channel {
+                if self
+                    .check_user_is_channel_member(channel_id, user_id, &*tx)
+                    .await
+                    .is_err()
+                {
+                    return Ok(None);
+                }
+
+                let channel_membership = channel_member::Entity::find()
+                    .filter(
+                        channel_member::Column::ChannelId
+                            .eq(channel_id)
+                            .and(channel_member::Column::UserId.eq(user_id)),
+                    )
+                    .one(&*tx)
+                    .await?;
+
+                let is_accepted = channel_membership
+                    .map(|membership| membership.accepted)
+                    .unwrap_or(false);
+
+                Ok(Some((
+                    Channel {
+                        id: channel.id,
+                        name: channel.name,
+                        parent_id: None,
+                    },
+                    is_accepted,
+                )))
+            } else {
+                Ok(None)
+            }
+        })
+        .await
+    }
+
+    pub async fn room_id_for_channel(&self, channel_id: ChannelId) -> Result<RoomId> {
+        self.transaction(|tx| async move {
+            let tx = tx;
+            let room = channel::Model {
+                id: channel_id,
+                ..Default::default()
+            }
+            .find_related(room::Entity)
+            .one(&*tx)
+            .await?
+            .ok_or_else(|| anyhow!("invalid channel"))?;
+            Ok(room.id)
+        })
+        .await
+    }
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+enum QueryUserIds {
+    UserId,
+}

crates/collab/src/db/queries/contacts.rs 🔗

@@ -0,0 +1,298 @@
+use super::*;
+
+impl Database {
+    pub async fn get_contacts(&self, user_id: UserId) -> Result<Vec<Contact>> {
+        #[derive(Debug, FromQueryResult)]
+        struct ContactWithUserBusyStatuses {
+            user_id_a: UserId,
+            user_id_b: UserId,
+            a_to_b: bool,
+            accepted: bool,
+            should_notify: bool,
+            user_a_busy: bool,
+            user_b_busy: bool,
+        }
+
+        self.transaction(|tx| async move {
+            let user_a_participant = Alias::new("user_a_participant");
+            let user_b_participant = Alias::new("user_b_participant");
+            let mut db_contacts = contact::Entity::find()
+                .column_as(
+                    Expr::tbl(user_a_participant.clone(), room_participant::Column::Id)
+                        .is_not_null(),
+                    "user_a_busy",
+                )
+                .column_as(
+                    Expr::tbl(user_b_participant.clone(), room_participant::Column::Id)
+                        .is_not_null(),
+                    "user_b_busy",
+                )
+                .filter(
+                    contact::Column::UserIdA
+                        .eq(user_id)
+                        .or(contact::Column::UserIdB.eq(user_id)),
+                )
+                .join_as(
+                    JoinType::LeftJoin,
+                    contact::Relation::UserARoomParticipant.def(),
+                    user_a_participant,
+                )
+                .join_as(
+                    JoinType::LeftJoin,
+                    contact::Relation::UserBRoomParticipant.def(),
+                    user_b_participant,
+                )
+                .into_model::<ContactWithUserBusyStatuses>()
+                .stream(&*tx)
+                .await?;
+
+            let mut contacts = Vec::new();
+            while let Some(db_contact) = db_contacts.next().await {
+                let db_contact = db_contact?;
+                if db_contact.user_id_a == user_id {
+                    if db_contact.accepted {
+                        contacts.push(Contact::Accepted {
+                            user_id: db_contact.user_id_b,
+                            should_notify: db_contact.should_notify && db_contact.a_to_b,
+                            busy: db_contact.user_b_busy,
+                        });
+                    } else if db_contact.a_to_b {
+                        contacts.push(Contact::Outgoing {
+                            user_id: db_contact.user_id_b,
+                        })
+                    } else {
+                        contacts.push(Contact::Incoming {
+                            user_id: db_contact.user_id_b,
+                            should_notify: db_contact.should_notify,
+                        });
+                    }
+                } else if db_contact.accepted {
+                    contacts.push(Contact::Accepted {
+                        user_id: db_contact.user_id_a,
+                        should_notify: db_contact.should_notify && !db_contact.a_to_b,
+                        busy: db_contact.user_a_busy,
+                    });
+                } else if db_contact.a_to_b {
+                    contacts.push(Contact::Incoming {
+                        user_id: db_contact.user_id_a,
+                        should_notify: db_contact.should_notify,
+                    });
+                } else {
+                    contacts.push(Contact::Outgoing {
+                        user_id: db_contact.user_id_a,
+                    });
+                }
+            }
+
+            contacts.sort_unstable_by_key(|contact| contact.user_id());
+
+            Ok(contacts)
+        })
+        .await
+    }
+
+    pub async fn is_user_busy(&self, user_id: UserId) -> Result<bool> {
+        self.transaction(|tx| async move {
+            let participant = room_participant::Entity::find()
+                .filter(room_participant::Column::UserId.eq(user_id))
+                .one(&*tx)
+                .await?;
+            Ok(participant.is_some())
+        })
+        .await
+    }
+
+    pub async fn has_contact(&self, user_id_1: UserId, user_id_2: UserId) -> Result<bool> {
+        self.transaction(|tx| async move {
+            let (id_a, id_b) = if user_id_1 < user_id_2 {
+                (user_id_1, user_id_2)
+            } else {
+                (user_id_2, user_id_1)
+            };
+
+            Ok(contact::Entity::find()
+                .filter(
+                    contact::Column::UserIdA
+                        .eq(id_a)
+                        .and(contact::Column::UserIdB.eq(id_b))
+                        .and(contact::Column::Accepted.eq(true)),
+                )
+                .one(&*tx)
+                .await?
+                .is_some())
+        })
+        .await
+    }
+
+    pub async fn send_contact_request(&self, sender_id: UserId, receiver_id: UserId) -> Result<()> {
+        self.transaction(|tx| async move {
+            let (id_a, id_b, a_to_b) = if sender_id < receiver_id {
+                (sender_id, receiver_id, true)
+            } else {
+                (receiver_id, sender_id, false)
+            };
+
+            let rows_affected = contact::Entity::insert(contact::ActiveModel {
+                user_id_a: ActiveValue::set(id_a),
+                user_id_b: ActiveValue::set(id_b),
+                a_to_b: ActiveValue::set(a_to_b),
+                accepted: ActiveValue::set(false),
+                should_notify: ActiveValue::set(true),
+                ..Default::default()
+            })
+            .on_conflict(
+                OnConflict::columns([contact::Column::UserIdA, contact::Column::UserIdB])
+                    .values([
+                        (contact::Column::Accepted, true.into()),
+                        (contact::Column::ShouldNotify, false.into()),
+                    ])
+                    .action_and_where(
+                        contact::Column::Accepted.eq(false).and(
+                            contact::Column::AToB
+                                .eq(a_to_b)
+                                .and(contact::Column::UserIdA.eq(id_b))
+                                .or(contact::Column::AToB
+                                    .ne(a_to_b)
+                                    .and(contact::Column::UserIdA.eq(id_a))),
+                        ),
+                    )
+                    .to_owned(),
+            )
+            .exec_without_returning(&*tx)
+            .await?;
+
+            if rows_affected == 1 {
+                Ok(())
+            } else {
+                Err(anyhow!("contact already requested"))?
+            }
+        })
+        .await
+    }
+
+    /// Returns a bool indicating whether the removed contact had originally accepted or not
+    ///
+    /// Deletes the contact identified by the requester and responder ids, and then returns
+    /// whether the deleted contact had originally accepted or was a pending contact request.
+    ///
+    /// # Arguments
+    ///
+    /// * `requester_id` - The user that initiates this request
+    /// * `responder_id` - The user that will be removed
+    pub async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<bool> {
+        self.transaction(|tx| async move {
+            let (id_a, id_b) = if responder_id < requester_id {
+                (responder_id, requester_id)
+            } else {
+                (requester_id, responder_id)
+            };
+
+            let contact = contact::Entity::find()
+                .filter(
+                    contact::Column::UserIdA
+                        .eq(id_a)
+                        .and(contact::Column::UserIdB.eq(id_b)),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such contact"))?;
+
+            contact::Entity::delete_by_id(contact.id).exec(&*tx).await?;
+            Ok(contact.accepted)
+        })
+        .await
+    }
+
+    pub async fn dismiss_contact_notification(
+        &self,
+        user_id: UserId,
+        contact_user_id: UserId,
+    ) -> Result<()> {
+        self.transaction(|tx| async move {
+            let (id_a, id_b, a_to_b) = if user_id < contact_user_id {
+                (user_id, contact_user_id, true)
+            } else {
+                (contact_user_id, user_id, false)
+            };
+
+            let result = contact::Entity::update_many()
+                .set(contact::ActiveModel {
+                    should_notify: ActiveValue::set(false),
+                    ..Default::default()
+                })
+                .filter(
+                    contact::Column::UserIdA
+                        .eq(id_a)
+                        .and(contact::Column::UserIdB.eq(id_b))
+                        .and(
+                            contact::Column::AToB
+                                .eq(a_to_b)
+                                .and(contact::Column::Accepted.eq(true))
+                                .or(contact::Column::AToB
+                                    .ne(a_to_b)
+                                    .and(contact::Column::Accepted.eq(false))),
+                        ),
+                )
+                .exec(&*tx)
+                .await?;
+            if result.rows_affected == 0 {
+                Err(anyhow!("no such contact request"))?
+            } else {
+                Ok(())
+            }
+        })
+        .await
+    }
+
+    pub async fn respond_to_contact_request(
+        &self,
+        responder_id: UserId,
+        requester_id: UserId,
+        accept: bool,
+    ) -> Result<()> {
+        self.transaction(|tx| async move {
+            let (id_a, id_b, a_to_b) = if responder_id < requester_id {
+                (responder_id, requester_id, false)
+            } else {
+                (requester_id, responder_id, true)
+            };
+            let rows_affected = if accept {
+                let result = contact::Entity::update_many()
+                    .set(contact::ActiveModel {
+                        accepted: ActiveValue::set(true),
+                        should_notify: ActiveValue::set(true),
+                        ..Default::default()
+                    })
+                    .filter(
+                        contact::Column::UserIdA
+                            .eq(id_a)
+                            .and(contact::Column::UserIdB.eq(id_b))
+                            .and(contact::Column::AToB.eq(a_to_b)),
+                    )
+                    .exec(&*tx)
+                    .await?;
+                result.rows_affected
+            } else {
+                let result = contact::Entity::delete_many()
+                    .filter(
+                        contact::Column::UserIdA
+                            .eq(id_a)
+                            .and(contact::Column::UserIdB.eq(id_b))
+                            .and(contact::Column::AToB.eq(a_to_b))
+                            .and(contact::Column::Accepted.eq(false)),
+                    )
+                    .exec(&*tx)
+                    .await?;
+
+                result.rows_affected
+            };
+
+            if rows_affected == 1 {
+                Ok(())
+            } else {
+                Err(anyhow!("no such contact request"))?
+            }
+        })
+        .await
+    }
+}

crates/collab/src/db/queries/projects.rs 🔗

@@ -0,0 +1,926 @@
+use super::*;
+
+impl Database {
+    pub async fn project_count_excluding_admins(&self) -> Result<usize> {
+        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+        enum QueryAs {
+            Count,
+        }
+
+        self.transaction(|tx| async move {
+            Ok(project::Entity::find()
+                .select_only()
+                .column_as(project::Column::Id.count(), QueryAs::Count)
+                .inner_join(user::Entity)
+                .filter(user::Column::Admin.eq(false))
+                .into_values::<_, QueryAs>()
+                .one(&*tx)
+                .await?
+                .unwrap_or(0i64) as usize)
+        })
+        .await
+    }
+
+    pub async fn share_project(
+        &self,
+        room_id: RoomId,
+        connection: ConnectionId,
+        worktrees: &[proto::WorktreeMetadata],
+    ) -> Result<RoomGuard<(ProjectId, proto::Room)>> {
+        self.room_transaction(room_id, |tx| async move {
+            let participant = room_participant::Entity::find()
+                .filter(
+                    Condition::all()
+                        .add(
+                            room_participant::Column::AnsweringConnectionId
+                                .eq(connection.id as i32),
+                        )
+                        .add(
+                            room_participant::Column::AnsweringConnectionServerId
+                                .eq(connection.owner_id as i32),
+                        ),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("could not find participant"))?;
+            if participant.room_id != room_id {
+                return Err(anyhow!("shared project on unexpected room"))?;
+            }
+
+            let project = project::ActiveModel {
+                room_id: ActiveValue::set(participant.room_id),
+                host_user_id: ActiveValue::set(participant.user_id),
+                host_connection_id: ActiveValue::set(Some(connection.id as i32)),
+                host_connection_server_id: ActiveValue::set(Some(ServerId(
+                    connection.owner_id as i32,
+                ))),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            if !worktrees.is_empty() {
+                worktree::Entity::insert_many(worktrees.iter().map(|worktree| {
+                    worktree::ActiveModel {
+                        id: ActiveValue::set(worktree.id as i64),
+                        project_id: ActiveValue::set(project.id),
+                        abs_path: ActiveValue::set(worktree.abs_path.clone()),
+                        root_name: ActiveValue::set(worktree.root_name.clone()),
+                        visible: ActiveValue::set(worktree.visible),
+                        scan_id: ActiveValue::set(0),
+                        completed_scan_id: ActiveValue::set(0),
+                    }
+                }))
+                .exec(&*tx)
+                .await?;
+            }
+
+            project_collaborator::ActiveModel {
+                project_id: ActiveValue::set(project.id),
+                connection_id: ActiveValue::set(connection.id as i32),
+                connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
+                user_id: ActiveValue::set(participant.user_id),
+                replica_id: ActiveValue::set(ReplicaId(0)),
+                is_host: ActiveValue::set(true),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            let room = self.get_room(room_id, &tx).await?;
+            Ok((project.id, room))
+        })
+        .await
+    }
+
+    pub async fn unshare_project(
+        &self,
+        project_id: ProjectId,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
+
+            let project = project::Entity::find_by_id(project_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("project not found"))?;
+            if project.host_connection()? == connection {
+                project::Entity::delete(project.into_active_model())
+                    .exec(&*tx)
+                    .await?;
+                let room = self.get_room(room_id, &tx).await?;
+                Ok((room, guest_connection_ids))
+            } else {
+                Err(anyhow!("cannot unshare a project hosted by another user"))?
+            }
+        })
+        .await
+    }
+
+    pub async fn update_project(
+        &self,
+        project_id: ProjectId,
+        connection: ConnectionId,
+        worktrees: &[proto::WorktreeMetadata],
+    ) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            let project = project::Entity::find_by_id(project_id)
+                .filter(
+                    Condition::all()
+                        .add(project::Column::HostConnectionId.eq(connection.id as i32))
+                        .add(
+                            project::Column::HostConnectionServerId.eq(connection.owner_id as i32),
+                        ),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such project"))?;
+
+            self.update_project_worktrees(project.id, worktrees, &tx)
+                .await?;
+
+            let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
+            let room = self.get_room(project.room_id, &tx).await?;
+            Ok((room, guest_connection_ids))
+        })
+        .await
+    }
+
+    pub(in crate::db) async fn update_project_worktrees(
+        &self,
+        project_id: ProjectId,
+        worktrees: &[proto::WorktreeMetadata],
+        tx: &DatabaseTransaction,
+    ) -> Result<()> {
+        if !worktrees.is_empty() {
+            worktree::Entity::insert_many(worktrees.iter().map(|worktree| worktree::ActiveModel {
+                id: ActiveValue::set(worktree.id as i64),
+                project_id: ActiveValue::set(project_id),
+                abs_path: ActiveValue::set(worktree.abs_path.clone()),
+                root_name: ActiveValue::set(worktree.root_name.clone()),
+                visible: ActiveValue::set(worktree.visible),
+                scan_id: ActiveValue::set(0),
+                completed_scan_id: ActiveValue::set(0),
+            }))
+            .on_conflict(
+                OnConflict::columns([worktree::Column::ProjectId, worktree::Column::Id])
+                    .update_column(worktree::Column::RootName)
+                    .to_owned(),
+            )
+            .exec(&*tx)
+            .await?;
+        }
+
+        worktree::Entity::delete_many()
+            .filter(worktree::Column::ProjectId.eq(project_id).and(
+                worktree::Column::Id.is_not_in(worktrees.iter().map(|worktree| worktree.id as i64)),
+            ))
+            .exec(&*tx)
+            .await?;
+
+        Ok(())
+    }
+
+    pub async fn update_worktree(
+        &self,
+        update: &proto::UpdateWorktree,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<Vec<ConnectionId>>> {
+        let project_id = ProjectId::from_proto(update.project_id);
+        let worktree_id = update.worktree_id as i64;
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            // Ensure the update comes from the host.
+            let _project = project::Entity::find_by_id(project_id)
+                .filter(
+                    Condition::all()
+                        .add(project::Column::HostConnectionId.eq(connection.id as i32))
+                        .add(
+                            project::Column::HostConnectionServerId.eq(connection.owner_id as i32),
+                        ),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such project"))?;
+
+            // Update metadata.
+            worktree::Entity::update(worktree::ActiveModel {
+                id: ActiveValue::set(worktree_id),
+                project_id: ActiveValue::set(project_id),
+                root_name: ActiveValue::set(update.root_name.clone()),
+                scan_id: ActiveValue::set(update.scan_id as i64),
+                completed_scan_id: if update.is_last_update {
+                    ActiveValue::set(update.scan_id as i64)
+                } else {
+                    ActiveValue::default()
+                },
+                abs_path: ActiveValue::set(update.abs_path.clone()),
+                ..Default::default()
+            })
+            .exec(&*tx)
+            .await?;
+
+            if !update.updated_entries.is_empty() {
+                worktree_entry::Entity::insert_many(update.updated_entries.iter().map(|entry| {
+                    let mtime = entry.mtime.clone().unwrap_or_default();
+                    worktree_entry::ActiveModel {
+                        project_id: ActiveValue::set(project_id),
+                        worktree_id: ActiveValue::set(worktree_id),
+                        id: ActiveValue::set(entry.id as i64),
+                        is_dir: ActiveValue::set(entry.is_dir),
+                        path: ActiveValue::set(entry.path.clone()),
+                        inode: ActiveValue::set(entry.inode as i64),
+                        mtime_seconds: ActiveValue::set(mtime.seconds as i64),
+                        mtime_nanos: ActiveValue::set(mtime.nanos as i32),
+                        is_symlink: ActiveValue::set(entry.is_symlink),
+                        is_ignored: ActiveValue::set(entry.is_ignored),
+                        is_external: ActiveValue::set(entry.is_external),
+                        git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)),
+                        is_deleted: ActiveValue::set(false),
+                        scan_id: ActiveValue::set(update.scan_id as i64),
+                    }
+                }))
+                .on_conflict(
+                    OnConflict::columns([
+                        worktree_entry::Column::ProjectId,
+                        worktree_entry::Column::WorktreeId,
+                        worktree_entry::Column::Id,
+                    ])
+                    .update_columns([
+                        worktree_entry::Column::IsDir,
+                        worktree_entry::Column::Path,
+                        worktree_entry::Column::Inode,
+                        worktree_entry::Column::MtimeSeconds,
+                        worktree_entry::Column::MtimeNanos,
+                        worktree_entry::Column::IsSymlink,
+                        worktree_entry::Column::IsIgnored,
+                        worktree_entry::Column::GitStatus,
+                        worktree_entry::Column::ScanId,
+                    ])
+                    .to_owned(),
+                )
+                .exec(&*tx)
+                .await?;
+            }
+
+            if !update.removed_entries.is_empty() {
+                worktree_entry::Entity::update_many()
+                    .filter(
+                        worktree_entry::Column::ProjectId
+                            .eq(project_id)
+                            .and(worktree_entry::Column::WorktreeId.eq(worktree_id))
+                            .and(
+                                worktree_entry::Column::Id
+                                    .is_in(update.removed_entries.iter().map(|id| *id as i64)),
+                            ),
+                    )
+                    .set(worktree_entry::ActiveModel {
+                        is_deleted: ActiveValue::Set(true),
+                        scan_id: ActiveValue::Set(update.scan_id as i64),
+                        ..Default::default()
+                    })
+                    .exec(&*tx)
+                    .await?;
+            }
+
+            if !update.updated_repositories.is_empty() {
+                worktree_repository::Entity::insert_many(update.updated_repositories.iter().map(
+                    |repository| worktree_repository::ActiveModel {
+                        project_id: ActiveValue::set(project_id),
+                        worktree_id: ActiveValue::set(worktree_id),
+                        work_directory_id: ActiveValue::set(repository.work_directory_id as i64),
+                        scan_id: ActiveValue::set(update.scan_id as i64),
+                        branch: ActiveValue::set(repository.branch.clone()),
+                        is_deleted: ActiveValue::set(false),
+                    },
+                ))
+                .on_conflict(
+                    OnConflict::columns([
+                        worktree_repository::Column::ProjectId,
+                        worktree_repository::Column::WorktreeId,
+                        worktree_repository::Column::WorkDirectoryId,
+                    ])
+                    .update_columns([
+                        worktree_repository::Column::ScanId,
+                        worktree_repository::Column::Branch,
+                    ])
+                    .to_owned(),
+                )
+                .exec(&*tx)
+                .await?;
+            }
+
+            if !update.removed_repositories.is_empty() {
+                worktree_repository::Entity::update_many()
+                    .filter(
+                        worktree_repository::Column::ProjectId
+                            .eq(project_id)
+                            .and(worktree_repository::Column::WorktreeId.eq(worktree_id))
+                            .and(
+                                worktree_repository::Column::WorkDirectoryId
+                                    .is_in(update.removed_repositories.iter().map(|id| *id as i64)),
+                            ),
+                    )
+                    .set(worktree_repository::ActiveModel {
+                        is_deleted: ActiveValue::Set(true),
+                        scan_id: ActiveValue::Set(update.scan_id as i64),
+                        ..Default::default()
+                    })
+                    .exec(&*tx)
+                    .await?;
+            }
+
+            let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
+            Ok(connection_ids)
+        })
+        .await
+    }
+
+    pub async fn update_diagnostic_summary(
+        &self,
+        update: &proto::UpdateDiagnosticSummary,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<Vec<ConnectionId>>> {
+        let project_id = ProjectId::from_proto(update.project_id);
+        let worktree_id = update.worktree_id as i64;
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            let summary = update
+                .summary
+                .as_ref()
+                .ok_or_else(|| anyhow!("invalid summary"))?;
+
+            // Ensure the update comes from the host.
+            let project = project::Entity::find_by_id(project_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such project"))?;
+            if project.host_connection()? != connection {
+                return Err(anyhow!("can't update a project hosted by someone else"))?;
+            }
+
+            // Update summary.
+            worktree_diagnostic_summary::Entity::insert(worktree_diagnostic_summary::ActiveModel {
+                project_id: ActiveValue::set(project_id),
+                worktree_id: ActiveValue::set(worktree_id),
+                path: ActiveValue::set(summary.path.clone()),
+                language_server_id: ActiveValue::set(summary.language_server_id as i64),
+                error_count: ActiveValue::set(summary.error_count as i32),
+                warning_count: ActiveValue::set(summary.warning_count as i32),
+                ..Default::default()
+            })
+            .on_conflict(
+                OnConflict::columns([
+                    worktree_diagnostic_summary::Column::ProjectId,
+                    worktree_diagnostic_summary::Column::WorktreeId,
+                    worktree_diagnostic_summary::Column::Path,
+                ])
+                .update_columns([
+                    worktree_diagnostic_summary::Column::LanguageServerId,
+                    worktree_diagnostic_summary::Column::ErrorCount,
+                    worktree_diagnostic_summary::Column::WarningCount,
+                ])
+                .to_owned(),
+            )
+            .exec(&*tx)
+            .await?;
+
+            let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
+            Ok(connection_ids)
+        })
+        .await
+    }
+
+    pub async fn start_language_server(
+        &self,
+        update: &proto::StartLanguageServer,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<Vec<ConnectionId>>> {
+        let project_id = ProjectId::from_proto(update.project_id);
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            let server = update
+                .server
+                .as_ref()
+                .ok_or_else(|| anyhow!("invalid language server"))?;
+
+            // Ensure the update comes from the host.
+            let project = project::Entity::find_by_id(project_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such project"))?;
+            if project.host_connection()? != connection {
+                return Err(anyhow!("can't update a project hosted by someone else"))?;
+            }
+
+            // Add the newly-started language server.
+            language_server::Entity::insert(language_server::ActiveModel {
+                project_id: ActiveValue::set(project_id),
+                id: ActiveValue::set(server.id as i64),
+                name: ActiveValue::set(server.name.clone()),
+                ..Default::default()
+            })
+            .on_conflict(
+                OnConflict::columns([
+                    language_server::Column::ProjectId,
+                    language_server::Column::Id,
+                ])
+                .update_column(language_server::Column::Name)
+                .to_owned(),
+            )
+            .exec(&*tx)
+            .await?;
+
+            let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
+            Ok(connection_ids)
+        })
+        .await
+    }
+
+    pub async fn update_worktree_settings(
+        &self,
+        update: &proto::UpdateWorktreeSettings,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<Vec<ConnectionId>>> {
+        let project_id = ProjectId::from_proto(update.project_id);
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            // Ensure the update comes from the host.
+            let project = project::Entity::find_by_id(project_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such project"))?;
+            if project.host_connection()? != connection {
+                return Err(anyhow!("can't update a project hosted by someone else"))?;
+            }
+
+            if let Some(content) = &update.content {
+                worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel {
+                    project_id: ActiveValue::Set(project_id),
+                    worktree_id: ActiveValue::Set(update.worktree_id as i64),
+                    path: ActiveValue::Set(update.path.clone()),
+                    content: ActiveValue::Set(content.clone()),
+                })
+                .on_conflict(
+                    OnConflict::columns([
+                        worktree_settings_file::Column::ProjectId,
+                        worktree_settings_file::Column::WorktreeId,
+                        worktree_settings_file::Column::Path,
+                    ])
+                    .update_column(worktree_settings_file::Column::Content)
+                    .to_owned(),
+                )
+                .exec(&*tx)
+                .await?;
+            } else {
+                worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel {
+                    project_id: ActiveValue::Set(project_id),
+                    worktree_id: ActiveValue::Set(update.worktree_id as i64),
+                    path: ActiveValue::Set(update.path.clone()),
+                    ..Default::default()
+                })
+                .exec(&*tx)
+                .await?;
+            }
+
+            let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
+            Ok(connection_ids)
+        })
+        .await
+    }
+
+    pub async fn join_project(
+        &self,
+        project_id: ProjectId,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<(Project, ReplicaId)>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            let participant = room_participant::Entity::find()
+                .filter(
+                    Condition::all()
+                        .add(
+                            room_participant::Column::AnsweringConnectionId
+                                .eq(connection.id as i32),
+                        )
+                        .add(
+                            room_participant::Column::AnsweringConnectionServerId
+                                .eq(connection.owner_id as i32),
+                        ),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("must join a room first"))?;
+
+            let project = project::Entity::find_by_id(project_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such project"))?;
+            if project.room_id != participant.room_id {
+                return Err(anyhow!("no such project"))?;
+            }
+
+            let mut collaborators = project
+                .find_related(project_collaborator::Entity)
+                .all(&*tx)
+                .await?;
+            let replica_ids = collaborators
+                .iter()
+                .map(|c| c.replica_id)
+                .collect::<HashSet<_>>();
+            let mut replica_id = ReplicaId(1);
+            while replica_ids.contains(&replica_id) {
+                replica_id.0 += 1;
+            }
+            let new_collaborator = project_collaborator::ActiveModel {
+                project_id: ActiveValue::set(project_id),
+                connection_id: ActiveValue::set(connection.id as i32),
+                connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
+                user_id: ActiveValue::set(participant.user_id),
+                replica_id: ActiveValue::set(replica_id),
+                is_host: ActiveValue::set(false),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+            collaborators.push(new_collaborator);
+
+            let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?;
+            let mut worktrees = db_worktrees
+                .into_iter()
+                .map(|db_worktree| {
+                    (
+                        db_worktree.id as u64,
+                        Worktree {
+                            id: db_worktree.id as u64,
+                            abs_path: db_worktree.abs_path,
+                            root_name: db_worktree.root_name,
+                            visible: db_worktree.visible,
+                            entries: Default::default(),
+                            repository_entries: Default::default(),
+                            diagnostic_summaries: Default::default(),
+                            settings_files: Default::default(),
+                            scan_id: db_worktree.scan_id as u64,
+                            completed_scan_id: db_worktree.completed_scan_id as u64,
+                        },
+                    )
+                })
+                .collect::<BTreeMap<_, _>>();
+
+            // Populate worktree entries.
+            {
+                let mut db_entries = worktree_entry::Entity::find()
+                    .filter(
+                        Condition::all()
+                            .add(worktree_entry::Column::ProjectId.eq(project_id))
+                            .add(worktree_entry::Column::IsDeleted.eq(false)),
+                    )
+                    .stream(&*tx)
+                    .await?;
+                while let Some(db_entry) = db_entries.next().await {
+                    let db_entry = db_entry?;
+                    if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) {
+                        worktree.entries.push(proto::Entry {
+                            id: db_entry.id as u64,
+                            is_dir: db_entry.is_dir,
+                            path: db_entry.path,
+                            inode: db_entry.inode as u64,
+                            mtime: Some(proto::Timestamp {
+                                seconds: db_entry.mtime_seconds as u64,
+                                nanos: db_entry.mtime_nanos as u32,
+                            }),
+                            is_symlink: db_entry.is_symlink,
+                            is_ignored: db_entry.is_ignored,
+                            is_external: db_entry.is_external,
+                            git_status: db_entry.git_status.map(|status| status as i32),
+                        });
+                    }
+                }
+            }
+
+            // Populate repository entries.
+            {
+                let mut db_repository_entries = worktree_repository::Entity::find()
+                    .filter(
+                        Condition::all()
+                            .add(worktree_repository::Column::ProjectId.eq(project_id))
+                            .add(worktree_repository::Column::IsDeleted.eq(false)),
+                    )
+                    .stream(&*tx)
+                    .await?;
+                while let Some(db_repository_entry) = db_repository_entries.next().await {
+                    let db_repository_entry = db_repository_entry?;
+                    if let Some(worktree) =
+                        worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
+                    {
+                        worktree.repository_entries.insert(
+                            db_repository_entry.work_directory_id as u64,
+                            proto::RepositoryEntry {
+                                work_directory_id: db_repository_entry.work_directory_id as u64,
+                                branch: db_repository_entry.branch,
+                            },
+                        );
+                    }
+                }
+            }
+
+            // Populate worktree diagnostic summaries.
+            {
+                let mut db_summaries = worktree_diagnostic_summary::Entity::find()
+                    .filter(worktree_diagnostic_summary::Column::ProjectId.eq(project_id))
+                    .stream(&*tx)
+                    .await?;
+                while let Some(db_summary) = db_summaries.next().await {
+                    let db_summary = db_summary?;
+                    if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) {
+                        worktree
+                            .diagnostic_summaries
+                            .push(proto::DiagnosticSummary {
+                                path: db_summary.path,
+                                language_server_id: db_summary.language_server_id as u64,
+                                error_count: db_summary.error_count as u32,
+                                warning_count: db_summary.warning_count as u32,
+                            });
+                    }
+                }
+            }
+
+            // Populate worktree settings files
+            {
+                let mut db_settings_files = worktree_settings_file::Entity::find()
+                    .filter(worktree_settings_file::Column::ProjectId.eq(project_id))
+                    .stream(&*tx)
+                    .await?;
+                while let Some(db_settings_file) = db_settings_files.next().await {
+                    let db_settings_file = db_settings_file?;
+                    if let Some(worktree) =
+                        worktrees.get_mut(&(db_settings_file.worktree_id as u64))
+                    {
+                        worktree.settings_files.push(WorktreeSettingsFile {
+                            path: db_settings_file.path,
+                            content: db_settings_file.content,
+                        });
+                    }
+                }
+            }
+
+            // Populate language servers.
+            let language_servers = project
+                .find_related(language_server::Entity)
+                .all(&*tx)
+                .await?;
+
+            let project = Project {
+                collaborators: collaborators
+                    .into_iter()
+                    .map(|collaborator| ProjectCollaborator {
+                        connection_id: collaborator.connection(),
+                        user_id: collaborator.user_id,
+                        replica_id: collaborator.replica_id,
+                        is_host: collaborator.is_host,
+                    })
+                    .collect(),
+                worktrees,
+                language_servers: language_servers
+                    .into_iter()
+                    .map(|language_server| proto::LanguageServer {
+                        id: language_server.id as u64,
+                        name: language_server.name,
+                    })
+                    .collect(),
+            };
+            Ok((project, replica_id as ReplicaId))
+        })
+        .await
+    }
+
+    pub async fn leave_project(
+        &self,
+        project_id: ProjectId,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<(proto::Room, LeftProject)>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            let result = project_collaborator::Entity::delete_many()
+                .filter(
+                    Condition::all()
+                        .add(project_collaborator::Column::ProjectId.eq(project_id))
+                        .add(project_collaborator::Column::ConnectionId.eq(connection.id as i32))
+                        .add(
+                            project_collaborator::Column::ConnectionServerId
+                                .eq(connection.owner_id as i32),
+                        ),
+                )
+                .exec(&*tx)
+                .await?;
+            if result.rows_affected == 0 {
+                Err(anyhow!("not a collaborator on this project"))?;
+            }
+
+            let project = project::Entity::find_by_id(project_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such project"))?;
+            let collaborators = project
+                .find_related(project_collaborator::Entity)
+                .all(&*tx)
+                .await?;
+            let connection_ids = collaborators
+                .into_iter()
+                .map(|collaborator| collaborator.connection())
+                .collect();
+
+            follower::Entity::delete_many()
+                .filter(
+                    Condition::any()
+                        .add(
+                            Condition::all()
+                                .add(follower::Column::ProjectId.eq(project_id))
+                                .add(
+                                    follower::Column::LeaderConnectionServerId
+                                        .eq(connection.owner_id),
+                                )
+                                .add(follower::Column::LeaderConnectionId.eq(connection.id)),
+                        )
+                        .add(
+                            Condition::all()
+                                .add(follower::Column::ProjectId.eq(project_id))
+                                .add(
+                                    follower::Column::FollowerConnectionServerId
+                                        .eq(connection.owner_id),
+                                )
+                                .add(follower::Column::FollowerConnectionId.eq(connection.id)),
+                        ),
+                )
+                .exec(&*tx)
+                .await?;
+
+            let room = self.get_room(project.room_id, &tx).await?;
+            let left_project = LeftProject {
+                id: project_id,
+                host_user_id: project.host_user_id,
+                host_connection_id: project.host_connection()?,
+                connection_ids,
+            };
+            Ok((room, left_project))
+        })
+        .await
+    }
+
+    pub async fn project_collaborators(
+        &self,
+        project_id: ProjectId,
+        connection_id: ConnectionId,
+    ) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            let collaborators = project_collaborator::Entity::find()
+                .filter(project_collaborator::Column::ProjectId.eq(project_id))
+                .all(&*tx)
+                .await?
+                .into_iter()
+                .map(|collaborator| ProjectCollaborator {
+                    connection_id: collaborator.connection(),
+                    user_id: collaborator.user_id,
+                    replica_id: collaborator.replica_id,
+                    is_host: collaborator.is_host,
+                })
+                .collect::<Vec<_>>();
+
+            if collaborators
+                .iter()
+                .any(|collaborator| collaborator.connection_id == connection_id)
+            {
+                Ok(collaborators)
+            } else {
+                Err(anyhow!("no such project"))?
+            }
+        })
+        .await
+    }
+
+    pub async fn project_connection_ids(
+        &self,
+        project_id: ProjectId,
+        connection_id: ConnectionId,
+    ) -> Result<RoomGuard<HashSet<ConnectionId>>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            let mut collaborators = project_collaborator::Entity::find()
+                .filter(project_collaborator::Column::ProjectId.eq(project_id))
+                .stream(&*tx)
+                .await?;
+
+            let mut connection_ids = HashSet::default();
+            while let Some(collaborator) = collaborators.next().await {
+                let collaborator = collaborator?;
+                connection_ids.insert(collaborator.connection());
+            }
+
+            if connection_ids.contains(&connection_id) {
+                Ok(connection_ids)
+            } else {
+                Err(anyhow!("no such project"))?
+            }
+        })
+        .await
+    }
+
+    async fn project_guest_connection_ids(
+        &self,
+        project_id: ProjectId,
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<ConnectionId>> {
+        let mut collaborators = project_collaborator::Entity::find()
+            .filter(
+                project_collaborator::Column::ProjectId
+                    .eq(project_id)
+                    .and(project_collaborator::Column::IsHost.eq(false)),
+            )
+            .stream(tx)
+            .await?;
+
+        let mut guest_connection_ids = Vec::new();
+        while let Some(collaborator) = collaborators.next().await {
+            let collaborator = collaborator?;
+            guest_connection_ids.push(collaborator.connection());
+        }
+        Ok(guest_connection_ids)
+    }
+
+    pub async fn room_id_for_project(&self, project_id: ProjectId) -> Result<RoomId> {
+        self.transaction(|tx| async move {
+            let project = project::Entity::find_by_id(project_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("project {} not found", project_id))?;
+            Ok(project.room_id)
+        })
+        .await
+    }
+
+    pub async fn follow(
+        &self,
+        project_id: ProjectId,
+        leader_connection: ConnectionId,
+        follower_connection: ConnectionId,
+    ) -> Result<RoomGuard<proto::Room>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            follower::ActiveModel {
+                room_id: ActiveValue::set(room_id),
+                project_id: ActiveValue::set(project_id),
+                leader_connection_server_id: ActiveValue::set(ServerId(
+                    leader_connection.owner_id as i32,
+                )),
+                leader_connection_id: ActiveValue::set(leader_connection.id as i32),
+                follower_connection_server_id: ActiveValue::set(ServerId(
+                    follower_connection.owner_id as i32,
+                )),
+                follower_connection_id: ActiveValue::set(follower_connection.id as i32),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            let room = self.get_room(room_id, &*tx).await?;
+            Ok(room)
+        })
+        .await
+    }
+
+    pub async fn unfollow(
+        &self,
+        project_id: ProjectId,
+        leader_connection: ConnectionId,
+        follower_connection: ConnectionId,
+    ) -> Result<RoomGuard<proto::Room>> {
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            follower::Entity::delete_many()
+                .filter(
+                    Condition::all()
+                        .add(follower::Column::ProjectId.eq(project_id))
+                        .add(
+                            follower::Column::LeaderConnectionServerId
+                                .eq(leader_connection.owner_id),
+                        )
+                        .add(follower::Column::LeaderConnectionId.eq(leader_connection.id))
+                        .add(
+                            follower::Column::FollowerConnectionServerId
+                                .eq(follower_connection.owner_id),
+                        )
+                        .add(follower::Column::FollowerConnectionId.eq(follower_connection.id)),
+                )
+                .exec(&*tx)
+                .await?;
+
+            let room = self.get_room(room_id, &*tx).await?;
+            Ok(room)
+        })
+        .await
+    }
+}

crates/collab/src/db/queries/rooms.rs 🔗

@@ -0,0 +1,1073 @@
+use super::*;
+
+impl Database {
+    pub async fn refresh_room(
+        &self,
+        room_id: RoomId,
+        new_server_id: ServerId,
+    ) -> Result<RoomGuard<RefreshedRoom>> {
+        self.room_transaction(room_id, |tx| async move {
+            let stale_participant_filter = Condition::all()
+                .add(room_participant::Column::RoomId.eq(room_id))
+                .add(room_participant::Column::AnsweringConnectionId.is_not_null())
+                .add(room_participant::Column::AnsweringConnectionServerId.ne(new_server_id));
+
+            let stale_participant_user_ids = room_participant::Entity::find()
+                .filter(stale_participant_filter.clone())
+                .all(&*tx)
+                .await?
+                .into_iter()
+                .map(|participant| participant.user_id)
+                .collect::<Vec<_>>();
+
+            // Delete participants who failed to reconnect and cancel their calls.
+            let mut canceled_calls_to_user_ids = Vec::new();
+            room_participant::Entity::delete_many()
+                .filter(stale_participant_filter)
+                .exec(&*tx)
+                .await?;
+            let called_participants = room_participant::Entity::find()
+                .filter(
+                    Condition::all()
+                        .add(
+                            room_participant::Column::CallingUserId
+                                .is_in(stale_participant_user_ids.iter().copied()),
+                        )
+                        .add(room_participant::Column::AnsweringConnectionId.is_null()),
+                )
+                .all(&*tx)
+                .await?;
+            room_participant::Entity::delete_many()
+                .filter(
+                    room_participant::Column::Id
+                        .is_in(called_participants.iter().map(|participant| participant.id)),
+                )
+                .exec(&*tx)
+                .await?;
+            canceled_calls_to_user_ids.extend(
+                called_participants
+                    .into_iter()
+                    .map(|participant| participant.user_id),
+            );
+
+            let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
+            let channel_members;
+            if let Some(channel_id) = channel_id {
+                channel_members = self.get_channel_members_internal(channel_id, &tx).await?;
+            } else {
+                channel_members = Vec::new();
+
+                // Delete the room if it becomes empty.
+                if room.participants.is_empty() {
+                    project::Entity::delete_many()
+                        .filter(project::Column::RoomId.eq(room_id))
+                        .exec(&*tx)
+                        .await?;
+                    room::Entity::delete_by_id(room_id).exec(&*tx).await?;
+                }
+            };
+
+            Ok(RefreshedRoom {
+                room,
+                channel_id,
+                channel_members,
+                stale_participant_user_ids,
+                canceled_calls_to_user_ids,
+            })
+        })
+        .await
+    }
+
+    pub async fn incoming_call_for_user(
+        &self,
+        user_id: UserId,
+    ) -> Result<Option<proto::IncomingCall>> {
+        self.transaction(|tx| async move {
+            let pending_participant = room_participant::Entity::find()
+                .filter(
+                    room_participant::Column::UserId
+                        .eq(user_id)
+                        .and(room_participant::Column::AnsweringConnectionId.is_null()),
+                )
+                .one(&*tx)
+                .await?;
+
+            if let Some(pending_participant) = pending_participant {
+                let room = self.get_room(pending_participant.room_id, &tx).await?;
+                Ok(Self::build_incoming_call(&room, user_id))
+            } else {
+                Ok(None)
+            }
+        })
+        .await
+    }
+
+    pub async fn create_room(
+        &self,
+        user_id: UserId,
+        connection: ConnectionId,
+        live_kit_room: &str,
+    ) -> Result<proto::Room> {
+        self.transaction(|tx| async move {
+            let room = room::ActiveModel {
+                live_kit_room: ActiveValue::set(live_kit_room.into()),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+            room_participant::ActiveModel {
+                room_id: ActiveValue::set(room.id),
+                user_id: ActiveValue::set(user_id),
+                answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
+                answering_connection_server_id: ActiveValue::set(Some(ServerId(
+                    connection.owner_id as i32,
+                ))),
+                answering_connection_lost: ActiveValue::set(false),
+                calling_user_id: ActiveValue::set(user_id),
+                calling_connection_id: ActiveValue::set(connection.id as i32),
+                calling_connection_server_id: ActiveValue::set(Some(ServerId(
+                    connection.owner_id as i32,
+                ))),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            let room = self.get_room(room.id, &tx).await?;
+            Ok(room)
+        })
+        .await
+    }
+
+    pub async fn call(
+        &self,
+        room_id: RoomId,
+        calling_user_id: UserId,
+        calling_connection: ConnectionId,
+        called_user_id: UserId,
+        initial_project_id: Option<ProjectId>,
+    ) -> Result<RoomGuard<(proto::Room, proto::IncomingCall)>> {
+        self.room_transaction(room_id, |tx| async move {
+            room_participant::ActiveModel {
+                room_id: ActiveValue::set(room_id),
+                user_id: ActiveValue::set(called_user_id),
+                answering_connection_lost: ActiveValue::set(false),
+                calling_user_id: ActiveValue::set(calling_user_id),
+                calling_connection_id: ActiveValue::set(calling_connection.id as i32),
+                calling_connection_server_id: ActiveValue::set(Some(ServerId(
+                    calling_connection.owner_id as i32,
+                ))),
+                initial_project_id: ActiveValue::set(initial_project_id),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+
+            let room = self.get_room(room_id, &tx).await?;
+            let incoming_call = Self::build_incoming_call(&room, called_user_id)
+                .ok_or_else(|| anyhow!("failed to build incoming call"))?;
+            Ok((room, incoming_call))
+        })
+        .await
+    }
+
+    pub async fn call_failed(
+        &self,
+        room_id: RoomId,
+        called_user_id: UserId,
+    ) -> Result<RoomGuard<proto::Room>> {
+        self.room_transaction(room_id, |tx| async move {
+            room_participant::Entity::delete_many()
+                .filter(
+                    room_participant::Column::RoomId
+                        .eq(room_id)
+                        .and(room_participant::Column::UserId.eq(called_user_id)),
+                )
+                .exec(&*tx)
+                .await?;
+            let room = self.get_room(room_id, &tx).await?;
+            Ok(room)
+        })
+        .await
+    }
+
+    pub async fn decline_call(
+        &self,
+        expected_room_id: Option<RoomId>,
+        user_id: UserId,
+    ) -> Result<Option<RoomGuard<proto::Room>>> {
+        self.optional_room_transaction(|tx| async move {
+            let mut filter = Condition::all()
+                .add(room_participant::Column::UserId.eq(user_id))
+                .add(room_participant::Column::AnsweringConnectionId.is_null());
+            if let Some(room_id) = expected_room_id {
+                filter = filter.add(room_participant::Column::RoomId.eq(room_id));
+            }
+            let participant = room_participant::Entity::find()
+                .filter(filter)
+                .one(&*tx)
+                .await?;
+
+            let participant = if let Some(participant) = participant {
+                participant
+            } else if expected_room_id.is_some() {
+                return Err(anyhow!("could not find call to decline"))?;
+            } else {
+                return Ok(None);
+            };
+
+            let room_id = participant.room_id;
+            room_participant::Entity::delete(participant.into_active_model())
+                .exec(&*tx)
+                .await?;
+
+            let room = self.get_room(room_id, &tx).await?;
+            Ok(Some((room_id, room)))
+        })
+        .await
+    }
+
+    pub async fn cancel_call(
+        &self,
+        room_id: RoomId,
+        calling_connection: ConnectionId,
+        called_user_id: UserId,
+    ) -> Result<RoomGuard<proto::Room>> {
+        self.room_transaction(room_id, |tx| async move {
+            let participant = room_participant::Entity::find()
+                .filter(
+                    Condition::all()
+                        .add(room_participant::Column::UserId.eq(called_user_id))
+                        .add(room_participant::Column::RoomId.eq(room_id))
+                        .add(
+                            room_participant::Column::CallingConnectionId
+                                .eq(calling_connection.id as i32),
+                        )
+                        .add(
+                            room_participant::Column::CallingConnectionServerId
+                                .eq(calling_connection.owner_id as i32),
+                        )
+                        .add(room_participant::Column::AnsweringConnectionId.is_null()),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no call to cancel"))?;
+
+            room_participant::Entity::delete(participant.into_active_model())
+                .exec(&*tx)
+                .await?;
+
+            let room = self.get_room(room_id, &tx).await?;
+            Ok(room)
+        })
+        .await
+    }
+
+    pub async fn join_room(
+        &self,
+        room_id: RoomId,
+        user_id: UserId,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<JoinRoom>> {
+        self.room_transaction(room_id, |tx| async move {
+            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+            enum QueryChannelId {
+                ChannelId,
+            }
+            let channel_id: Option<ChannelId> = room::Entity::find()
+                .select_only()
+                .column(room::Column::ChannelId)
+                .filter(room::Column::Id.eq(room_id))
+                .into_values::<_, QueryChannelId>()
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such room"))?;
+
+            if let Some(channel_id) = channel_id {
+                self.check_user_is_channel_member(channel_id, user_id, &*tx)
+                    .await?;
+
+                room_participant::Entity::insert_many([room_participant::ActiveModel {
+                    room_id: ActiveValue::set(room_id),
+                    user_id: ActiveValue::set(user_id),
+                    answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
+                    answering_connection_server_id: ActiveValue::set(Some(ServerId(
+                        connection.owner_id as i32,
+                    ))),
+                    answering_connection_lost: ActiveValue::set(false),
+                    calling_user_id: ActiveValue::set(user_id),
+                    calling_connection_id: ActiveValue::set(connection.id as i32),
+                    calling_connection_server_id: ActiveValue::set(Some(ServerId(
+                        connection.owner_id as i32,
+                    ))),
+                    ..Default::default()
+                }])
+                .on_conflict(
+                    OnConflict::columns([room_participant::Column::UserId])
+                        .update_columns([
+                            room_participant::Column::AnsweringConnectionId,
+                            room_participant::Column::AnsweringConnectionServerId,
+                            room_participant::Column::AnsweringConnectionLost,
+                        ])
+                        .to_owned(),
+                )
+                .exec(&*tx)
+                .await?;
+            } else {
+                let result = room_participant::Entity::update_many()
+                    .filter(
+                        Condition::all()
+                            .add(room_participant::Column::RoomId.eq(room_id))
+                            .add(room_participant::Column::UserId.eq(user_id))
+                            .add(room_participant::Column::AnsweringConnectionId.is_null()),
+                    )
+                    .set(room_participant::ActiveModel {
+                        answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
+                        answering_connection_server_id: ActiveValue::set(Some(ServerId(
+                            connection.owner_id as i32,
+                        ))),
+                        answering_connection_lost: ActiveValue::set(false),
+                        ..Default::default()
+                    })
+                    .exec(&*tx)
+                    .await?;
+                if result.rows_affected == 0 {
+                    Err(anyhow!("room does not exist or was already joined"))?;
+                }
+            }
+
+            let room = self.get_room(room_id, &tx).await?;
+            let channel_members = if let Some(channel_id) = channel_id {
+                self.get_channel_members_internal(channel_id, &tx).await?
+            } else {
+                Vec::new()
+            };
+            Ok(JoinRoom {
+                room,
+                channel_id,
+                channel_members,
+            })
+        })
+        .await
+    }
+
+    pub async fn rejoin_room(
+        &self,
+        rejoin_room: proto::RejoinRoom,
+        user_id: UserId,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<RejoinedRoom>> {
+        let room_id = RoomId::from_proto(rejoin_room.id);
+        self.room_transaction(room_id, |tx| async {
+            let tx = tx;
+            let participant_update = room_participant::Entity::update_many()
+                .filter(
+                    Condition::all()
+                        .add(room_participant::Column::RoomId.eq(room_id))
+                        .add(room_participant::Column::UserId.eq(user_id))
+                        .add(room_participant::Column::AnsweringConnectionId.is_not_null())
+                        .add(
+                            Condition::any()
+                                .add(room_participant::Column::AnsweringConnectionLost.eq(true))
+                                .add(
+                                    room_participant::Column::AnsweringConnectionServerId
+                                        .ne(connection.owner_id as i32),
+                                ),
+                        ),
+                )
+                .set(room_participant::ActiveModel {
+                    answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
+                    answering_connection_server_id: ActiveValue::set(Some(ServerId(
+                        connection.owner_id as i32,
+                    ))),
+                    answering_connection_lost: ActiveValue::set(false),
+                    ..Default::default()
+                })
+                .exec(&*tx)
+                .await?;
+            if participant_update.rows_affected == 0 {
+                return Err(anyhow!("room does not exist or was already joined"))?;
+            }
+
+            let mut reshared_projects = Vec::new();
+            for reshared_project in &rejoin_room.reshared_projects {
+                let project_id = ProjectId::from_proto(reshared_project.project_id);
+                let project = project::Entity::find_by_id(project_id)
+                    .one(&*tx)
+                    .await?
+                    .ok_or_else(|| anyhow!("project does not exist"))?;
+                if project.host_user_id != user_id {
+                    return Err(anyhow!("no such project"))?;
+                }
+
+                let mut collaborators = project
+                    .find_related(project_collaborator::Entity)
+                    .all(&*tx)
+                    .await?;
+                let host_ix = collaborators
+                    .iter()
+                    .position(|collaborator| {
+                        collaborator.user_id == user_id && collaborator.is_host
+                    })
+                    .ok_or_else(|| anyhow!("host not found among collaborators"))?;
+                let host = collaborators.swap_remove(host_ix);
+                let old_connection_id = host.connection();
+
+                project::Entity::update(project::ActiveModel {
+                    host_connection_id: ActiveValue::set(Some(connection.id as i32)),
+                    host_connection_server_id: ActiveValue::set(Some(ServerId(
+                        connection.owner_id as i32,
+                    ))),
+                    ..project.into_active_model()
+                })
+                .exec(&*tx)
+                .await?;
+                project_collaborator::Entity::update(project_collaborator::ActiveModel {
+                    connection_id: ActiveValue::set(connection.id as i32),
+                    connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
+                    ..host.into_active_model()
+                })
+                .exec(&*tx)
+                .await?;
+
+                self.update_project_worktrees(project_id, &reshared_project.worktrees, &tx)
+                    .await?;
+
+                reshared_projects.push(ResharedProject {
+                    id: project_id,
+                    old_connection_id,
+                    collaborators: collaborators
+                        .iter()
+                        .map(|collaborator| ProjectCollaborator {
+                            connection_id: collaborator.connection(),
+                            user_id: collaborator.user_id,
+                            replica_id: collaborator.replica_id,
+                            is_host: collaborator.is_host,
+                        })
+                        .collect(),
+                    worktrees: reshared_project.worktrees.clone(),
+                });
+            }
+
+            project::Entity::delete_many()
+                .filter(
+                    Condition::all()
+                        .add(project::Column::RoomId.eq(room_id))
+                        .add(project::Column::HostUserId.eq(user_id))
+                        .add(
+                            project::Column::Id
+                                .is_not_in(reshared_projects.iter().map(|project| project.id)),
+                        ),
+                )
+                .exec(&*tx)
+                .await?;
+
+            let mut rejoined_projects = Vec::new();
+            for rejoined_project in &rejoin_room.rejoined_projects {
+                let project_id = ProjectId::from_proto(rejoined_project.id);
+                let Some(project) = project::Entity::find_by_id(project_id)
+                    .one(&*tx)
+                    .await? else { continue };
+
+                let mut worktrees = Vec::new();
+                let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?;
+                for db_worktree in db_worktrees {
+                    let mut worktree = RejoinedWorktree {
+                        id: db_worktree.id as u64,
+                        abs_path: db_worktree.abs_path,
+                        root_name: db_worktree.root_name,
+                        visible: db_worktree.visible,
+                        updated_entries: Default::default(),
+                        removed_entries: Default::default(),
+                        updated_repositories: Default::default(),
+                        removed_repositories: Default::default(),
+                        diagnostic_summaries: Default::default(),
+                        settings_files: Default::default(),
+                        scan_id: db_worktree.scan_id as u64,
+                        completed_scan_id: db_worktree.completed_scan_id as u64,
+                    };
+
+                    let rejoined_worktree = rejoined_project
+                        .worktrees
+                        .iter()
+                        .find(|worktree| worktree.id == db_worktree.id as u64);
+
+                    // File entries
+                    {
+                        let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
+                            worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
+                        } else {
+                            worktree_entry::Column::IsDeleted.eq(false)
+                        };
+
+                        let mut db_entries = worktree_entry::Entity::find()
+                            .filter(
+                                Condition::all()
+                                    .add(worktree_entry::Column::ProjectId.eq(project.id))
+                                    .add(worktree_entry::Column::WorktreeId.eq(worktree.id))
+                                    .add(entry_filter),
+                            )
+                            .stream(&*tx)
+                            .await?;
+
+                        while let Some(db_entry) = db_entries.next().await {
+                            let db_entry = db_entry?;
+                            if db_entry.is_deleted {
+                                worktree.removed_entries.push(db_entry.id as u64);
+                            } else {
+                                worktree.updated_entries.push(proto::Entry {
+                                    id: db_entry.id as u64,
+                                    is_dir: db_entry.is_dir,
+                                    path: db_entry.path,
+                                    inode: db_entry.inode as u64,
+                                    mtime: Some(proto::Timestamp {
+                                        seconds: db_entry.mtime_seconds as u64,
+                                        nanos: db_entry.mtime_nanos as u32,
+                                    }),
+                                    is_symlink: db_entry.is_symlink,
+                                    is_ignored: db_entry.is_ignored,
+                                    is_external: db_entry.is_external,
+                                    git_status: db_entry.git_status.map(|status| status as i32),
+                                });
+                            }
+                        }
+                    }
+
+                    // Repository Entries
+                    {
+                        let repository_entry_filter =
+                            if let Some(rejoined_worktree) = rejoined_worktree {
+                                worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id)
+                            } else {
+                                worktree_repository::Column::IsDeleted.eq(false)
+                            };
+
+                        let mut db_repositories = worktree_repository::Entity::find()
+                            .filter(
+                                Condition::all()
+                                    .add(worktree_repository::Column::ProjectId.eq(project.id))
+                                    .add(worktree_repository::Column::WorktreeId.eq(worktree.id))
+                                    .add(repository_entry_filter),
+                            )
+                            .stream(&*tx)
+                            .await?;
+
+                        while let Some(db_repository) = db_repositories.next().await {
+                            let db_repository = db_repository?;
+                            if db_repository.is_deleted {
+                                worktree
+                                    .removed_repositories
+                                    .push(db_repository.work_directory_id as u64);
+                            } else {
+                                worktree.updated_repositories.push(proto::RepositoryEntry {
+                                    work_directory_id: db_repository.work_directory_id as u64,
+                                    branch: db_repository.branch,
+                                });
+                            }
+                        }
+                    }
+
+                    worktrees.push(worktree);
+                }
+
+                let language_servers = project
+                    .find_related(language_server::Entity)
+                    .all(&*tx)
+                    .await?
+                    .into_iter()
+                    .map(|language_server| proto::LanguageServer {
+                        id: language_server.id as u64,
+                        name: language_server.name,
+                    })
+                    .collect::<Vec<_>>();
+
+                {
+                    let mut db_settings_files = worktree_settings_file::Entity::find()
+                        .filter(worktree_settings_file::Column::ProjectId.eq(project_id))
+                        .stream(&*tx)
+                        .await?;
+                    while let Some(db_settings_file) = db_settings_files.next().await {
+                        let db_settings_file = db_settings_file?;
+                        if let Some(worktree) = worktrees
+                            .iter_mut()
+                            .find(|w| w.id == db_settings_file.worktree_id as u64)
+                        {
+                            worktree.settings_files.push(WorktreeSettingsFile {
+                                path: db_settings_file.path,
+                                content: db_settings_file.content,
+                            });
+                        }
+                    }
+                }
+
+                let mut collaborators = project
+                    .find_related(project_collaborator::Entity)
+                    .all(&*tx)
+                    .await?;
+                let self_collaborator = if let Some(self_collaborator_ix) = collaborators
+                    .iter()
+                    .position(|collaborator| collaborator.user_id == user_id)
+                {
+                    collaborators.swap_remove(self_collaborator_ix)
+                } else {
+                    continue;
+                };
+                let old_connection_id = self_collaborator.connection();
+                project_collaborator::Entity::update(project_collaborator::ActiveModel {
+                    connection_id: ActiveValue::set(connection.id as i32),
+                    connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
+                    ..self_collaborator.into_active_model()
+                })
+                .exec(&*tx)
+                .await?;
+
+                let collaborators = collaborators
+                    .into_iter()
+                    .map(|collaborator| ProjectCollaborator {
+                        connection_id: collaborator.connection(),
+                        user_id: collaborator.user_id,
+                        replica_id: collaborator.replica_id,
+                        is_host: collaborator.is_host,
+                    })
+                    .collect::<Vec<_>>();
+
+                rejoined_projects.push(RejoinedProject {
+                    id: project_id,
+                    old_connection_id,
+                    collaborators,
+                    worktrees,
+                    language_servers,
+                });
+            }
+
+            let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
+            let channel_members = if let Some(channel_id) = channel_id {
+                self.get_channel_members_internal(channel_id, &tx).await?
+            } else {
+                Vec::new()
+            };
+
+            Ok(RejoinedRoom {
+                room,
+                channel_id,
+                channel_members,
+                rejoined_projects,
+                reshared_projects,
+            })
+        })
+        .await
+    }
+
+    pub async fn leave_room(
+        &self,
+        connection: ConnectionId,
+    ) -> Result<Option<RoomGuard<LeftRoom>>> {
+        self.optional_room_transaction(|tx| async move {
+            let leaving_participant = room_participant::Entity::find()
+                .filter(
+                    Condition::all()
+                        .add(
+                            room_participant::Column::AnsweringConnectionId
+                                .eq(connection.id as i32),
+                        )
+                        .add(
+                            room_participant::Column::AnsweringConnectionServerId
+                                .eq(connection.owner_id as i32),
+                        ),
+                )
+                .one(&*tx)
+                .await?;
+
+            if let Some(leaving_participant) = leaving_participant {
+                // Leave room.
+                let room_id = leaving_participant.room_id;
+                room_participant::Entity::delete_by_id(leaving_participant.id)
+                    .exec(&*tx)
+                    .await?;
+
+                // Cancel pending calls initiated by the leaving user.
+                let called_participants = room_participant::Entity::find()
+                    .filter(
+                        Condition::all()
+                            .add(
+                                room_participant::Column::CallingUserId
+                                    .eq(leaving_participant.user_id),
+                            )
+                            .add(room_participant::Column::AnsweringConnectionId.is_null()),
+                    )
+                    .all(&*tx)
+                    .await?;
+                room_participant::Entity::delete_many()
+                    .filter(
+                        room_participant::Column::Id
+                            .is_in(called_participants.iter().map(|participant| participant.id)),
+                    )
+                    .exec(&*tx)
+                    .await?;
+                let canceled_calls_to_user_ids = called_participants
+                    .into_iter()
+                    .map(|participant| participant.user_id)
+                    .collect();
+
+                // Detect left projects.
+                #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+                enum QueryProjectIds {
+                    ProjectId,
+                }
+                let project_ids: Vec<ProjectId> = project_collaborator::Entity::find()
+                    .select_only()
+                    .column_as(
+                        project_collaborator::Column::ProjectId,
+                        QueryProjectIds::ProjectId,
+                    )
+                    .filter(
+                        Condition::all()
+                            .add(
+                                project_collaborator::Column::ConnectionId.eq(connection.id as i32),
+                            )
+                            .add(
+                                project_collaborator::Column::ConnectionServerId
+                                    .eq(connection.owner_id as i32),
+                            ),
+                    )
+                    .into_values::<_, QueryProjectIds>()
+                    .all(&*tx)
+                    .await?;
+                let mut left_projects = HashMap::default();
+                let mut collaborators = project_collaborator::Entity::find()
+                    .filter(project_collaborator::Column::ProjectId.is_in(project_ids))
+                    .stream(&*tx)
+                    .await?;
+                while let Some(collaborator) = collaborators.next().await {
+                    let collaborator = collaborator?;
+                    let left_project =
+                        left_projects
+                            .entry(collaborator.project_id)
+                            .or_insert(LeftProject {
+                                id: collaborator.project_id,
+                                host_user_id: Default::default(),
+                                connection_ids: Default::default(),
+                                host_connection_id: Default::default(),
+                            });
+
+                    let collaborator_connection_id = collaborator.connection();
+                    if collaborator_connection_id != connection {
+                        left_project.connection_ids.push(collaborator_connection_id);
+                    }
+
+                    if collaborator.is_host {
+                        left_project.host_user_id = collaborator.user_id;
+                        left_project.host_connection_id = collaborator_connection_id;
+                    }
+                }
+                drop(collaborators);
+
+                // Leave projects.
+                project_collaborator::Entity::delete_many()
+                    .filter(
+                        Condition::all()
+                            .add(
+                                project_collaborator::Column::ConnectionId.eq(connection.id as i32),
+                            )
+                            .add(
+                                project_collaborator::Column::ConnectionServerId
+                                    .eq(connection.owner_id as i32),
+                            ),
+                    )
+                    .exec(&*tx)
+                    .await?;
+
+                // Unshare projects.
+                project::Entity::delete_many()
+                    .filter(
+                        Condition::all()
+                            .add(project::Column::RoomId.eq(room_id))
+                            .add(project::Column::HostConnectionId.eq(connection.id as i32))
+                            .add(
+                                project::Column::HostConnectionServerId
+                                    .eq(connection.owner_id as i32),
+                            ),
+                    )
+                    .exec(&*tx)
+                    .await?;
+
+                let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
+                let deleted = if room.participants.is_empty() {
+                    let result = room::Entity::delete_by_id(room_id)
+                        .filter(room::Column::ChannelId.is_null())
+                        .exec(&*tx)
+                        .await?;
+                    result.rows_affected > 0
+                } else {
+                    false
+                };
+
+                let channel_members = if let Some(channel_id) = channel_id {
+                    self.get_channel_members_internal(channel_id, &tx).await?
+                } else {
+                    Vec::new()
+                };
+                let left_room = LeftRoom {
+                    room,
+                    channel_id,
+                    channel_members,
+                    left_projects,
+                    canceled_calls_to_user_ids,
+                    deleted,
+                };
+
+                if left_room.room.participants.is_empty() {
+                    self.rooms.remove(&room_id);
+                }
+
+                Ok(Some((room_id, left_room)))
+            } else {
+                Ok(None)
+            }
+        })
+        .await
+    }
+
+    pub async fn update_room_participant_location(
+        &self,
+        room_id: RoomId,
+        connection: ConnectionId,
+        location: proto::ParticipantLocation,
+    ) -> Result<RoomGuard<proto::Room>> {
+        self.room_transaction(room_id, |tx| async {
+            let tx = tx;
+            let location_kind;
+            let location_project_id;
+            match location
+                .variant
+                .as_ref()
+                .ok_or_else(|| anyhow!("invalid location"))?
+            {
+                proto::participant_location::Variant::SharedProject(project) => {
+                    location_kind = 0;
+                    location_project_id = Some(ProjectId::from_proto(project.id));
+                }
+                proto::participant_location::Variant::UnsharedProject(_) => {
+                    location_kind = 1;
+                    location_project_id = None;
+                }
+                proto::participant_location::Variant::External(_) => {
+                    location_kind = 2;
+                    location_project_id = None;
+                }
+            }
+
+            let result = room_participant::Entity::update_many()
+                .filter(
+                    Condition::all()
+                        .add(room_participant::Column::RoomId.eq(room_id))
+                        .add(
+                            room_participant::Column::AnsweringConnectionId
+                                .eq(connection.id as i32),
+                        )
+                        .add(
+                            room_participant::Column::AnsweringConnectionServerId
+                                .eq(connection.owner_id as i32),
+                        ),
+                )
+                .set(room_participant::ActiveModel {
+                    location_kind: ActiveValue::set(Some(location_kind)),
+                    location_project_id: ActiveValue::set(location_project_id),
+                    ..Default::default()
+                })
+                .exec(&*tx)
+                .await?;
+
+            if result.rows_affected == 1 {
+                let room = self.get_room(room_id, &tx).await?;
+                Ok(room)
+            } else {
+                Err(anyhow!("could not update room participant location"))?
+            }
+        })
+        .await
+    }
+
+    pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> {
+        self.transaction(|tx| async move {
+            let participant = room_participant::Entity::find()
+                .filter(
+                    Condition::all()
+                        .add(
+                            room_participant::Column::AnsweringConnectionId
+                                .eq(connection.id as i32),
+                        )
+                        .add(
+                            room_participant::Column::AnsweringConnectionServerId
+                                .eq(connection.owner_id as i32),
+                        ),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("not a participant in any room"))?;
+
+            room_participant::Entity::update(room_participant::ActiveModel {
+                answering_connection_lost: ActiveValue::set(true),
+                ..participant.into_active_model()
+            })
+            .exec(&*tx)
+            .await?;
+
+            Ok(())
+        })
+        .await
+    }
+
+    fn build_incoming_call(
+        room: &proto::Room,
+        called_user_id: UserId,
+    ) -> Option<proto::IncomingCall> {
+        let pending_participant = room
+            .pending_participants
+            .iter()
+            .find(|participant| participant.user_id == called_user_id.to_proto())?;
+
+        Some(proto::IncomingCall {
+            room_id: room.id,
+            calling_user_id: pending_participant.calling_user_id,
+            participant_user_ids: room
+                .participants
+                .iter()
+                .map(|participant| participant.user_id)
+                .collect(),
+            initial_project: room.participants.iter().find_map(|participant| {
+                let initial_project_id = pending_participant.initial_project_id?;
+                participant
+                    .projects
+                    .iter()
+                    .find(|project| project.id == initial_project_id)
+                    .cloned()
+            }),
+        })
+    }
+
+    pub async fn get_room(&self, room_id: RoomId, tx: &DatabaseTransaction) -> Result<proto::Room> {
+        let (_, room) = self.get_channel_room(room_id, tx).await?;
+        Ok(room)
+    }
+
+    async fn get_channel_room(
+        &self,
+        room_id: RoomId,
+        tx: &DatabaseTransaction,
+    ) -> Result<(Option<ChannelId>, proto::Room)> {
+        let db_room = room::Entity::find_by_id(room_id)
+            .one(tx)
+            .await?
+            .ok_or_else(|| anyhow!("could not find room"))?;
+
+        let mut db_participants = db_room
+            .find_related(room_participant::Entity)
+            .stream(tx)
+            .await?;
+        let mut participants = HashMap::default();
+        let mut pending_participants = Vec::new();
+        while let Some(db_participant) = db_participants.next().await {
+            let db_participant = db_participant?;
+            if let Some((answering_connection_id, answering_connection_server_id)) = db_participant
+                .answering_connection_id
+                .zip(db_participant.answering_connection_server_id)
+            {
+                let location = match (
+                    db_participant.location_kind,
+                    db_participant.location_project_id,
+                ) {
+                    (Some(0), Some(project_id)) => {
+                        Some(proto::participant_location::Variant::SharedProject(
+                            proto::participant_location::SharedProject {
+                                id: project_id.to_proto(),
+                            },
+                        ))
+                    }
+                    (Some(1), _) => Some(proto::participant_location::Variant::UnsharedProject(
+                        Default::default(),
+                    )),
+                    _ => Some(proto::participant_location::Variant::External(
+                        Default::default(),
+                    )),
+                };
+
+                let answering_connection = ConnectionId {
+                    owner_id: answering_connection_server_id.0 as u32,
+                    id: answering_connection_id as u32,
+                };
+                participants.insert(
+                    answering_connection,
+                    proto::Participant {
+                        user_id: db_participant.user_id.to_proto(),
+                        peer_id: Some(answering_connection.into()),
+                        projects: Default::default(),
+                        location: Some(proto::ParticipantLocation { variant: location }),
+                    },
+                );
+            } else {
+                pending_participants.push(proto::PendingParticipant {
+                    user_id: db_participant.user_id.to_proto(),
+                    calling_user_id: db_participant.calling_user_id.to_proto(),
+                    initial_project_id: db_participant.initial_project_id.map(|id| id.to_proto()),
+                });
+            }
+        }
+        drop(db_participants);
+
+        let mut db_projects = db_room
+            .find_related(project::Entity)
+            .find_with_related(worktree::Entity)
+            .stream(tx)
+            .await?;
+
+        while let Some(row) = db_projects.next().await {
+            let (db_project, db_worktree) = row?;
+            let host_connection = db_project.host_connection()?;
+            if let Some(participant) = participants.get_mut(&host_connection) {
+                let project = if let Some(project) = participant
+                    .projects
+                    .iter_mut()
+                    .find(|project| project.id == db_project.id.to_proto())
+                {
+                    project
+                } else {
+                    participant.projects.push(proto::ParticipantProject {
+                        id: db_project.id.to_proto(),
+                        worktree_root_names: Default::default(),
+                    });
+                    participant.projects.last_mut().unwrap()
+                };
+
+                if let Some(db_worktree) = db_worktree {
+                    if db_worktree.visible {
+                        project.worktree_root_names.push(db_worktree.root_name);
+                    }
+                }
+            }
+        }
+        drop(db_projects);
+
+        let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?;
+        let mut followers = Vec::new();
+        while let Some(db_follower) = db_followers.next().await {
+            let db_follower = db_follower?;
+            followers.push(proto::Follower {
+                leader_id: Some(db_follower.leader_connection().into()),
+                follower_id: Some(db_follower.follower_connection().into()),
+                project_id: db_follower.project_id.to_proto(),
+            });
+        }
+
+        Ok((
+            db_room.channel_id,
+            proto::Room {
+                id: db_room.id.to_proto(),
+                live_kit_room: db_room.live_kit_room,
+                participants: participants.into_values().collect(),
+                pending_participants,
+                followers,
+            },
+        ))
+    }
+}

crates/collab/src/db/queries/servers.rs 🔗

@@ -0,0 +1,81 @@
+use super::*;
+
+impl Database {
+    pub async fn create_server(&self, environment: &str) -> Result<ServerId> {
+        self.transaction(|tx| async move {
+            let server = server::ActiveModel {
+                environment: ActiveValue::set(environment.into()),
+                ..Default::default()
+            }
+            .insert(&*tx)
+            .await?;
+            Ok(server.id)
+        })
+        .await
+    }
+
+    pub async fn stale_room_ids(
+        &self,
+        environment: &str,
+        new_server_id: ServerId,
+    ) -> Result<Vec<RoomId>> {
+        self.transaction(|tx| async move {
+            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+            enum QueryAs {
+                RoomId,
+            }
+
+            let stale_server_epochs = self
+                .stale_server_ids(environment, new_server_id, &tx)
+                .await?;
+            Ok(room_participant::Entity::find()
+                .select_only()
+                .column(room_participant::Column::RoomId)
+                .distinct()
+                .filter(
+                    room_participant::Column::AnsweringConnectionServerId
+                        .is_in(stale_server_epochs),
+                )
+                .into_values::<_, QueryAs>()
+                .all(&*tx)
+                .await?)
+        })
+        .await
+    }
+
+    pub async fn delete_stale_servers(
+        &self,
+        environment: &str,
+        new_server_id: ServerId,
+    ) -> Result<()> {
+        self.transaction(|tx| async move {
+            server::Entity::delete_many()
+                .filter(
+                    Condition::all()
+                        .add(server::Column::Environment.eq(environment))
+                        .add(server::Column::Id.ne(new_server_id)),
+                )
+                .exec(&*tx)
+                .await?;
+            Ok(())
+        })
+        .await
+    }
+
+    async fn stale_server_ids(
+        &self,
+        environment: &str,
+        new_server_id: ServerId,
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<ServerId>> {
+        let stale_servers = server::Entity::find()
+            .filter(
+                Condition::all()
+                    .add(server::Column::Environment.eq(environment))
+                    .add(server::Column::Id.ne(new_server_id)),
+            )
+            .all(&*tx)
+            .await?;
+        Ok(stale_servers.into_iter().map(|server| server.id).collect())
+    }
+}

crates/collab/src/db/queries/signups.rs 🔗

@@ -0,0 +1,349 @@
+use super::*;
+use hyper::StatusCode;
+
+impl Database {
+    pub async fn create_invite_from_code(
+        &self,
+        code: &str,
+        email_address: &str,
+        device_id: Option<&str>,
+        added_to_mailing_list: bool,
+    ) -> Result<Invite> {
+        self.transaction(|tx| async move {
+            let existing_user = user::Entity::find()
+                .filter(user::Column::EmailAddress.eq(email_address))
+                .one(&*tx)
+                .await?;
+
+            if existing_user.is_some() {
+                Err(anyhow!("email address is already in use"))?;
+            }
+
+            let inviting_user_with_invites = match user::Entity::find()
+                .filter(
+                    user::Column::InviteCode
+                        .eq(code)
+                        .and(user::Column::InviteCount.gt(0)),
+                )
+                .one(&*tx)
+                .await?
+            {
+                Some(inviting_user) => inviting_user,
+                None => {
+                    return Err(Error::Http(
+                        StatusCode::UNAUTHORIZED,
+                        "unable to find an invite code with invites remaining".to_string(),
+                    ))?
+                }
+            };
+            user::Entity::update_many()
+                .filter(
+                    user::Column::Id
+                        .eq(inviting_user_with_invites.id)
+                        .and(user::Column::InviteCount.gt(0)),
+                )
+                .col_expr(
+                    user::Column::InviteCount,
+                    Expr::col(user::Column::InviteCount).sub(1),
+                )
+                .exec(&*tx)
+                .await?;
+
+            let signup = signup::Entity::insert(signup::ActiveModel {
+                email_address: ActiveValue::set(email_address.into()),
+                email_confirmation_code: ActiveValue::set(random_email_confirmation_code()),
+                email_confirmation_sent: ActiveValue::set(false),
+                inviting_user_id: ActiveValue::set(Some(inviting_user_with_invites.id)),
+                platform_linux: ActiveValue::set(false),
+                platform_mac: ActiveValue::set(false),
+                platform_windows: ActiveValue::set(false),
+                platform_unknown: ActiveValue::set(true),
+                device_id: ActiveValue::set(device_id.map(|device_id| device_id.into())),
+                added_to_mailing_list: ActiveValue::set(added_to_mailing_list),
+                ..Default::default()
+            })
+            .on_conflict(
+                OnConflict::column(signup::Column::EmailAddress)
+                    .update_column(signup::Column::InvitingUserId)
+                    .to_owned(),
+            )
+            .exec_with_returning(&*tx)
+            .await?;
+
+            Ok(Invite {
+                email_address: signup.email_address,
+                email_confirmation_code: signup.email_confirmation_code,
+            })
+        })
+        .await
+    }
+
+    pub async fn create_user_from_invite(
+        &self,
+        invite: &Invite,
+        user: NewUserParams,
+    ) -> Result<Option<NewUserResult>> {
+        self.transaction(|tx| async {
+            let tx = tx;
+            let signup = signup::Entity::find()
+                .filter(
+                    signup::Column::EmailAddress
+                        .eq(invite.email_address.as_str())
+                        .and(
+                            signup::Column::EmailConfirmationCode
+                                .eq(invite.email_confirmation_code.as_str()),
+                        ),
+                )
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "no such invite".to_string()))?;
+
+            if signup.user_id.is_some() {
+                return Ok(None);
+            }
+
+            let user = user::Entity::insert(user::ActiveModel {
+                email_address: ActiveValue::set(Some(invite.email_address.clone())),
+                github_login: ActiveValue::set(user.github_login.clone()),
+                github_user_id: ActiveValue::set(Some(user.github_user_id)),
+                admin: ActiveValue::set(false),
+                invite_count: ActiveValue::set(user.invite_count),
+                invite_code: ActiveValue::set(Some(random_invite_code())),
+                metrics_id: ActiveValue::set(Uuid::new_v4()),
+                ..Default::default()
+            })
+            .on_conflict(
+                OnConflict::column(user::Column::GithubLogin)
+                    .update_columns([
+                        user::Column::EmailAddress,
+                        user::Column::GithubUserId,
+                        user::Column::Admin,
+                    ])
+                    .to_owned(),
+            )
+            .exec_with_returning(&*tx)
+            .await?;
+
+            let mut signup = signup.into_active_model();
+            signup.user_id = ActiveValue::set(Some(user.id));
+            let signup = signup.update(&*tx).await?;
+
+            if let Some(inviting_user_id) = signup.inviting_user_id {
+                let (user_id_a, user_id_b, a_to_b) = if inviting_user_id < user.id {
+                    (inviting_user_id, user.id, true)
+                } else {
+                    (user.id, inviting_user_id, false)
+                };
+
+                contact::Entity::insert(contact::ActiveModel {
+                    user_id_a: ActiveValue::set(user_id_a),
+                    user_id_b: ActiveValue::set(user_id_b),
+                    a_to_b: ActiveValue::set(a_to_b),
+                    should_notify: ActiveValue::set(true),
+                    accepted: ActiveValue::set(true),
+                    ..Default::default()
+                })
+                .on_conflict(OnConflict::new().do_nothing().to_owned())
+                .exec_without_returning(&*tx)
+                .await?;
+            }
+
+            Ok(Some(NewUserResult {
+                user_id: user.id,
+                metrics_id: user.metrics_id.to_string(),
+                inviting_user_id: signup.inviting_user_id,
+                signup_device_id: signup.device_id,
+            }))
+        })
+        .await
+    }
+
+    pub async fn set_invite_count_for_user(&self, id: UserId, count: i32) -> Result<()> {
+        self.transaction(|tx| async move {
+            if count > 0 {
+                user::Entity::update_many()
+                    .filter(
+                        user::Column::Id
+                            .eq(id)
+                            .and(user::Column::InviteCode.is_null()),
+                    )
+                    .set(user::ActiveModel {
+                        invite_code: ActiveValue::set(Some(random_invite_code())),
+                        ..Default::default()
+                    })
+                    .exec(&*tx)
+                    .await?;
+            }
+
+            user::Entity::update_many()
+                .filter(user::Column::Id.eq(id))
+                .set(user::ActiveModel {
+                    invite_count: ActiveValue::set(count),
+                    ..Default::default()
+                })
+                .exec(&*tx)
+                .await?;
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn get_invite_code_for_user(&self, id: UserId) -> Result<Option<(String, i32)>> {
+        self.transaction(|tx| async move {
+            match user::Entity::find_by_id(id).one(&*tx).await? {
+                Some(user) if user.invite_code.is_some() => {
+                    Ok(Some((user.invite_code.unwrap(), user.invite_count)))
+                }
+                _ => Ok(None),
+            }
+        })
+        .await
+    }
+
+    pub async fn get_user_for_invite_code(&self, code: &str) -> Result<User> {
+        self.transaction(|tx| async move {
+            user::Entity::find()
+                .filter(user::Column::InviteCode.eq(code))
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| {
+                    Error::Http(
+                        StatusCode::NOT_FOUND,
+                        "that invite code does not exist".to_string(),
+                    )
+                })
+        })
+        .await
+    }
+
+    pub async fn create_signup(&self, signup: &NewSignup) -> Result<()> {
+        self.transaction(|tx| async move {
+            signup::Entity::insert(signup::ActiveModel {
+                email_address: ActiveValue::set(signup.email_address.clone()),
+                email_confirmation_code: ActiveValue::set(random_email_confirmation_code()),
+                email_confirmation_sent: ActiveValue::set(false),
+                platform_mac: ActiveValue::set(signup.platform_mac),
+                platform_windows: ActiveValue::set(signup.platform_windows),
+                platform_linux: ActiveValue::set(signup.platform_linux),
+                platform_unknown: ActiveValue::set(false),
+                editor_features: ActiveValue::set(Some(signup.editor_features.clone())),
+                programming_languages: ActiveValue::set(Some(signup.programming_languages.clone())),
+                device_id: ActiveValue::set(signup.device_id.clone()),
+                added_to_mailing_list: ActiveValue::set(signup.added_to_mailing_list),
+                ..Default::default()
+            })
+            .on_conflict(
+                OnConflict::column(signup::Column::EmailAddress)
+                    .update_columns([
+                        signup::Column::PlatformMac,
+                        signup::Column::PlatformWindows,
+                        signup::Column::PlatformLinux,
+                        signup::Column::EditorFeatures,
+                        signup::Column::ProgrammingLanguages,
+                        signup::Column::DeviceId,
+                        signup::Column::AddedToMailingList,
+                    ])
+                    .to_owned(),
+            )
+            .exec(&*tx)
+            .await?;
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn get_signup(&self, email_address: &str) -> Result<signup::Model> {
+        self.transaction(|tx| async move {
+            let signup = signup::Entity::find()
+                .filter(signup::Column::EmailAddress.eq(email_address))
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| {
+                    anyhow!("signup with email address {} doesn't exist", email_address)
+                })?;
+
+            Ok(signup)
+        })
+        .await
+    }
+
+    pub async fn get_waitlist_summary(&self) -> Result<WaitlistSummary> {
+        self.transaction(|tx| async move {
+            let query = "
+                SELECT
+                    COUNT(*) as count,
+                    COALESCE(SUM(CASE WHEN platform_linux THEN 1 ELSE 0 END), 0) as linux_count,
+                    COALESCE(SUM(CASE WHEN platform_mac THEN 1 ELSE 0 END), 0) as mac_count,
+                    COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count,
+                    COALESCE(SUM(CASE WHEN platform_unknown THEN 1 ELSE 0 END), 0) as unknown_count
+                FROM (
+                    SELECT *
+                    FROM signups
+                    WHERE
+                        NOT email_confirmation_sent
+                ) AS unsent
+            ";
+            Ok(
+                WaitlistSummary::find_by_statement(Statement::from_sql_and_values(
+                    self.pool.get_database_backend(),
+                    query.into(),
+                    vec![],
+                ))
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("invalid result"))?,
+            )
+        })
+        .await
+    }
+
+    pub async fn record_sent_invites(&self, invites: &[Invite]) -> Result<()> {
+        let emails = invites
+            .iter()
+            .map(|s| s.email_address.as_str())
+            .collect::<Vec<_>>();
+        self.transaction(|tx| async {
+            let tx = tx;
+            signup::Entity::update_many()
+                .filter(signup::Column::EmailAddress.is_in(emails.iter().copied()))
+                .set(signup::ActiveModel {
+                    email_confirmation_sent: ActiveValue::set(true),
+                    ..Default::default()
+                })
+                .exec(&*tx)
+                .await?;
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn get_unsent_invites(&self, count: usize) -> Result<Vec<Invite>> {
+        self.transaction(|tx| async move {
+            Ok(signup::Entity::find()
+                .select_only()
+                .column(signup::Column::EmailAddress)
+                .column(signup::Column::EmailConfirmationCode)
+                .filter(
+                    signup::Column::EmailConfirmationSent.eq(false).and(
+                        signup::Column::PlatformMac
+                            .eq(true)
+                            .or(signup::Column::PlatformUnknown.eq(true)),
+                    ),
+                )
+                .order_by_asc(signup::Column::CreatedAt)
+                .limit(count as u64)
+                .into_model()
+                .all(&*tx)
+                .await?)
+        })
+        .await
+    }
+}
+
+fn random_invite_code() -> String {
+    nanoid::nanoid!(16)
+}
+
+fn random_email_confirmation_code() -> String {
+    nanoid::nanoid!(64)
+}

crates/collab/src/db/queries/users.rs 🔗

@@ -0,0 +1,243 @@
+use super::*;
+
+impl Database {
+    pub async fn create_user(
+        &self,
+        email_address: &str,
+        admin: bool,
+        params: NewUserParams,
+    ) -> Result<NewUserResult> {
+        self.transaction(|tx| async {
+            let tx = tx;
+            let user = user::Entity::insert(user::ActiveModel {
+                email_address: ActiveValue::set(Some(email_address.into())),
+                github_login: ActiveValue::set(params.github_login.clone()),
+                github_user_id: ActiveValue::set(Some(params.github_user_id)),
+                admin: ActiveValue::set(admin),
+                metrics_id: ActiveValue::set(Uuid::new_v4()),
+                ..Default::default()
+            })
+            .on_conflict(
+                OnConflict::column(user::Column::GithubLogin)
+                    .update_column(user::Column::GithubLogin)
+                    .to_owned(),
+            )
+            .exec_with_returning(&*tx)
+            .await?;
+
+            Ok(NewUserResult {
+                user_id: user.id,
+                metrics_id: user.metrics_id.to_string(),
+                signup_device_id: None,
+                inviting_user_id: None,
+            })
+        })
+        .await
+    }
+
+    pub async fn get_user_by_id(&self, id: UserId) -> Result<Option<user::Model>> {
+        self.transaction(|tx| async move { Ok(user::Entity::find_by_id(id).one(&*tx).await?) })
+            .await
+    }
+
+    pub async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<user::Model>> {
+        self.transaction(|tx| async {
+            let tx = tx;
+            Ok(user::Entity::find()
+                .filter(user::Column::Id.is_in(ids.iter().copied()))
+                .all(&*tx)
+                .await?)
+        })
+        .await
+    }
+
+    pub async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
+        self.transaction(|tx| async move {
+            Ok(user::Entity::find()
+                .filter(user::Column::GithubLogin.eq(github_login))
+                .one(&*tx)
+                .await?)
+        })
+        .await
+    }
+
+    pub async fn get_or_create_user_by_github_account(
+        &self,
+        github_login: &str,
+        github_user_id: Option<i32>,
+        github_email: Option<&str>,
+    ) -> Result<Option<User>> {
+        self.transaction(|tx| async move {
+            let tx = &*tx;
+            if let Some(github_user_id) = github_user_id {
+                if let Some(user_by_github_user_id) = user::Entity::find()
+                    .filter(user::Column::GithubUserId.eq(github_user_id))
+                    .one(tx)
+                    .await?
+                {
+                    let mut user_by_github_user_id = user_by_github_user_id.into_active_model();
+                    user_by_github_user_id.github_login = ActiveValue::set(github_login.into());
+                    Ok(Some(user_by_github_user_id.update(tx).await?))
+                } else if let Some(user_by_github_login) = user::Entity::find()
+                    .filter(user::Column::GithubLogin.eq(github_login))
+                    .one(tx)
+                    .await?
+                {
+                    let mut user_by_github_login = user_by_github_login.into_active_model();
+                    user_by_github_login.github_user_id = ActiveValue::set(Some(github_user_id));
+                    Ok(Some(user_by_github_login.update(tx).await?))
+                } else {
+                    let user = user::Entity::insert(user::ActiveModel {
+                        email_address: ActiveValue::set(github_email.map(|email| email.into())),
+                        github_login: ActiveValue::set(github_login.into()),
+                        github_user_id: ActiveValue::set(Some(github_user_id)),
+                        admin: ActiveValue::set(false),
+                        invite_count: ActiveValue::set(0),
+                        invite_code: ActiveValue::set(None),
+                        metrics_id: ActiveValue::set(Uuid::new_v4()),
+                        ..Default::default()
+                    })
+                    .exec_with_returning(&*tx)
+                    .await?;
+                    Ok(Some(user))
+                }
+            } else {
+                Ok(user::Entity::find()
+                    .filter(user::Column::GithubLogin.eq(github_login))
+                    .one(tx)
+                    .await?)
+            }
+        })
+        .await
+    }
+
+    pub async fn get_all_users(&self, page: u32, limit: u32) -> Result<Vec<User>> {
+        self.transaction(|tx| async move {
+            Ok(user::Entity::find()
+                .order_by_asc(user::Column::GithubLogin)
+                .limit(limit as u64)
+                .offset(page as u64 * limit as u64)
+                .all(&*tx)
+                .await?)
+        })
+        .await
+    }
+
+    pub async fn get_users_with_no_invites(
+        &self,
+        invited_by_another_user: bool,
+    ) -> Result<Vec<User>> {
+        self.transaction(|tx| async move {
+            Ok(user::Entity::find()
+                .filter(
+                    user::Column::InviteCount
+                        .eq(0)
+                        .and(if invited_by_another_user {
+                            user::Column::InviterId.is_not_null()
+                        } else {
+                            user::Column::InviterId.is_null()
+                        }),
+                )
+                .all(&*tx)
+                .await?)
+        })
+        .await
+    }
+
+    pub async fn get_user_metrics_id(&self, id: UserId) -> Result<String> {
+        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+        enum QueryAs {
+            MetricsId,
+        }
+
+        self.transaction(|tx| async move {
+            let metrics_id: Uuid = user::Entity::find_by_id(id)
+                .select_only()
+                .column(user::Column::MetricsId)
+                .into_values::<_, QueryAs>()
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("could not find user"))?;
+            Ok(metrics_id.to_string())
+        })
+        .await
+    }
+
+    pub async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()> {
+        self.transaction(|tx| async move {
+            user::Entity::update_many()
+                .filter(user::Column::Id.eq(id))
+                .set(user::ActiveModel {
+                    admin: ActiveValue::set(is_admin),
+                    ..Default::default()
+                })
+                .exec(&*tx)
+                .await?;
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
+        self.transaction(|tx| async move {
+            user::Entity::update_many()
+                .filter(user::Column::Id.eq(id))
+                .set(user::ActiveModel {
+                    connected_once: ActiveValue::set(connected_once),
+                    ..Default::default()
+                })
+                .exec(&*tx)
+                .await?;
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn destroy_user(&self, id: UserId) -> Result<()> {
+        self.transaction(|tx| async move {
+            access_token::Entity::delete_many()
+                .filter(access_token::Column::UserId.eq(id))
+                .exec(&*tx)
+                .await?;
+            user::Entity::delete_by_id(id).exec(&*tx).await?;
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result<Vec<User>> {
+        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.into(),
+                    vec![like_string.into(), name_query.into(), limit.into()],
+                ))
+                .all(&*tx)
+                .await?)
+        })
+        .await
+    }
+
+    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
+    }
+}

crates/collab/src/db/signup.rs 🔗

@@ -1,57 +0,0 @@
-use super::{SignupId, UserId};
-use sea_orm::{entity::prelude::*, FromQueryResult};
-use serde::{Deserialize, Serialize};
-
-#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
-#[sea_orm(table_name = "signups")]
-pub struct Model {
-    #[sea_orm(primary_key)]
-    pub id: SignupId,
-    pub email_address: String,
-    pub email_confirmation_code: String,
-    pub email_confirmation_sent: bool,
-    pub created_at: DateTime,
-    pub device_id: Option<String>,
-    pub user_id: Option<UserId>,
-    pub inviting_user_id: Option<UserId>,
-    pub platform_mac: bool,
-    pub platform_linux: bool,
-    pub platform_windows: bool,
-    pub platform_unknown: bool,
-    pub editor_features: Option<Vec<String>>,
-    pub programming_languages: Option<Vec<String>>,
-    pub added_to_mailing_list: bool,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {}
-
-impl ActiveModelBehavior for ActiveModel {}
-
-#[derive(Clone, Debug, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)]
-pub struct Invite {
-    pub email_address: String,
-    pub email_confirmation_code: String,
-}
-
-#[derive(Clone, Debug, Deserialize)]
-pub struct NewSignup {
-    pub email_address: String,
-    pub platform_mac: bool,
-    pub platform_windows: bool,
-    pub platform_linux: bool,
-    pub editor_features: Vec<String>,
-    pub programming_languages: Vec<String>,
-    pub device_id: Option<String>,
-    pub added_to_mailing_list: bool,
-    pub created_at: Option<DateTime>,
-}
-
-#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromQueryResult)]
-pub struct WaitlistSummary {
-    pub count: i64,
-    pub linux_count: i64,
-    pub mac_count: i64,
-    pub windows_count: i64,
-    pub unknown_count: i64,
-}

crates/collab/src/db/tables.rs 🔗

@@ -0,0 +1,20 @@
+pub mod access_token;
+pub mod channel;
+pub mod channel_member;
+pub mod channel_path;
+pub mod contact;
+pub mod follower;
+pub mod language_server;
+pub mod project;
+pub mod project_collaborator;
+pub mod room;
+pub mod room_participant;
+pub mod server;
+pub mod signup;
+pub mod user;
+pub mod worktree;
+pub mod worktree_diagnostic_summary;
+pub mod worktree_entry;
+pub mod worktree_repository;
+pub mod worktree_repository_statuses;
+pub mod worktree_settings_file;

crates/collab/src/db/access_token.rs → crates/collab/src/db/tables/access_token.rs 🔗

@@ -1,4 +1,4 @@
-use super::{AccessTokenId, UserId};
+use crate::db::{AccessTokenId, UserId};
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]

crates/collab/src/db/channel.rs → crates/collab/src/db/tables/channel.rs 🔗

@@ -1,4 +1,4 @@
-use super::ChannelId;
+use crate::db::ChannelId;
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
@@ -30,9 +30,3 @@ impl Related<super::room::Entity> for Entity {
         Relation::Room.def()
     }
 }
-
-// impl Related<super::follower::Entity> for Entity {
-//     fn to() -> RelationDef {
-//         Relation::Follower.def()
-//     }
-// }

crates/collab/src/db/channel_member.rs → crates/collab/src/db/tables/channel_member.rs 🔗

@@ -1,6 +1,4 @@
-use crate::db::channel_member;
-
-use super::{ChannelId, ChannelMemberId, UserId};
+use crate::db::{channel_member, ChannelId, ChannelMemberId, UserId};
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]

crates/collab/src/db/channel_path.rs → crates/collab/src/db/tables/channel_path.rs 🔗

@@ -1,4 +1,4 @@
-use super::ChannelId;
+use crate::db::ChannelId;
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]

crates/collab/src/db/contact.rs → crates/collab/src/db/tables/contact.rs 🔗

@@ -1,4 +1,4 @@
-use super::{ContactId, UserId};
+use crate::db::{ContactId, UserId};
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
@@ -30,29 +30,3 @@ pub enum Relation {
 }
 
 impl ActiveModelBehavior for ActiveModel {}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum Contact {
-    Accepted {
-        user_id: UserId,
-        should_notify: bool,
-        busy: bool,
-    },
-    Outgoing {
-        user_id: UserId,
-    },
-    Incoming {
-        user_id: UserId,
-        should_notify: bool,
-    },
-}
-
-impl Contact {
-    pub fn user_id(&self) -> UserId {
-        match self {
-            Contact::Accepted { user_id, .. } => *user_id,
-            Contact::Outgoing { user_id } => *user_id,
-            Contact::Incoming { user_id, .. } => *user_id,
-        }
-    }
-}

crates/collab/src/db/follower.rs → crates/collab/src/db/tables/follower.rs 🔗

@@ -1,9 +1,8 @@
-use super::{FollowerId, ProjectId, RoomId, ServerId};
+use crate::db::{FollowerId, ProjectId, RoomId, ServerId};
 use rpc::ConnectionId;
 use sea_orm::entity::prelude::*;
-use serde::Serialize;
 
-#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
+#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
 #[sea_orm(table_name = "followers")]
 pub struct Model {
     #[sea_orm(primary_key)]

crates/collab/src/db/language_server.rs → crates/collab/src/db/tables/language_server.rs 🔗

@@ -1,4 +1,4 @@
-use super::ProjectId;
+use crate::db::ProjectId;
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]

crates/collab/src/db/project.rs → crates/collab/src/db/tables/project.rs 🔗

@@ -1,4 +1,4 @@
-use super::{ProjectId, Result, RoomId, ServerId, UserId};
+use crate::db::{ProjectId, Result, RoomId, ServerId, UserId};
 use anyhow::anyhow;
 use rpc::ConnectionId;
 use sea_orm::entity::prelude::*;

crates/collab/src/db/project_collaborator.rs → crates/collab/src/db/tables/project_collaborator.rs 🔗

@@ -1,4 +1,4 @@
-use super::{ProjectCollaboratorId, ProjectId, ReplicaId, ServerId, UserId};
+use crate::db::{ProjectCollaboratorId, ProjectId, ReplicaId, ServerId, UserId};
 use rpc::ConnectionId;
 use sea_orm::entity::prelude::*;
 

crates/collab/src/db/room.rs → crates/collab/src/db/tables/room.rs 🔗

@@ -1,4 +1,4 @@
-use super::{ChannelId, RoomId};
+use crate::db::{ChannelId, RoomId};
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Default, Debug, PartialEq, Eq, DeriveEntityModel)]

crates/collab/src/db/room_participant.rs → crates/collab/src/db/tables/room_participant.rs 🔗

@@ -1,4 +1,4 @@
-use super::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId};
+use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId};
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]

crates/collab/src/db/server.rs → crates/collab/src/db/tables/server.rs 🔗

@@ -1,4 +1,4 @@
-use super::ServerId;
+use crate::db::ServerId;
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]

crates/collab/src/db/tables/signup.rs 🔗

@@ -0,0 +1,28 @@
+use crate::db::{SignupId, UserId};
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "signups")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: SignupId,
+    pub email_address: String,
+    pub email_confirmation_code: String,
+    pub email_confirmation_sent: bool,
+    pub created_at: DateTime,
+    pub device_id: Option<String>,
+    pub user_id: Option<UserId>,
+    pub inviting_user_id: Option<UserId>,
+    pub platform_mac: bool,
+    pub platform_linux: bool,
+    pub platform_windows: bool,
+    pub platform_unknown: bool,
+    pub editor_features: Option<Vec<String>>,
+    pub programming_languages: Option<Vec<String>>,
+    pub added_to_mailing_list: bool,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {}
+
+impl ActiveModelBehavior for ActiveModel {}

crates/collab/src/db/worktree.rs → crates/collab/src/db/tables/worktree.rs 🔗

@@ -1,4 +1,4 @@
-use super::ProjectId;
+use crate::db::ProjectId;
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]

crates/collab/src/db/worktree_entry.rs → crates/collab/src/db/tables/worktree_entry.rs 🔗

@@ -1,4 +1,4 @@
-use super::ProjectId;
+use crate::db::ProjectId;
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]

crates/collab/src/db/test_db.rs 🔗

@@ -0,0 +1,120 @@
+use super::*;
+use gpui::executor::Background;
+use parking_lot::Mutex;
+use sea_orm::ConnectionTrait;
+use sqlx::migrate::MigrateDatabase;
+use std::sync::Arc;
+
+pub struct TestDb {
+    pub db: Option<Arc<Database>>,
+    pub connection: Option<sqlx::AnyConnection>,
+}
+
+impl TestDb {
+    pub fn sqlite(background: Arc<Background>) -> Self {
+        let url = format!("sqlite::memory:");
+        let runtime = tokio::runtime::Builder::new_current_thread()
+            .enable_io()
+            .enable_time()
+            .build()
+            .unwrap();
+
+        let mut db = runtime.block_on(async {
+            let mut options = ConnectOptions::new(url);
+            options.max_connections(5);
+            let db = Database::new(options, Executor::Deterministic(background))
+                .await
+                .unwrap();
+            let sql = include_str!(concat!(
+                env!("CARGO_MANIFEST_DIR"),
+                "/migrations.sqlite/20221109000000_test_schema.sql"
+            ));
+            db.pool
+                .execute(sea_orm::Statement::from_string(
+                    db.pool.get_database_backend(),
+                    sql.into(),
+                ))
+                .await
+                .unwrap();
+            db
+        });
+
+        db.runtime = Some(runtime);
+
+        Self {
+            db: Some(Arc::new(db)),
+            connection: None,
+        }
+    }
+
+    pub fn postgres(background: Arc<Background>) -> Self {
+        static LOCK: Mutex<()> = Mutex::new(());
+
+        let _guard = LOCK.lock();
+        let mut rng = StdRng::from_entropy();
+        let url = format!(
+            "postgres://postgres@localhost/zed-test-{}",
+            rng.gen::<u128>()
+        );
+        let runtime = tokio::runtime::Builder::new_current_thread()
+            .enable_io()
+            .enable_time()
+            .build()
+            .unwrap();
+
+        let mut db = runtime.block_on(async {
+            sqlx::Postgres::create_database(&url)
+                .await
+                .expect("failed to create test db");
+            let mut options = ConnectOptions::new(url);
+            options
+                .max_connections(5)
+                .idle_timeout(Duration::from_secs(0));
+            let db = Database::new(options, Executor::Deterministic(background))
+                .await
+                .unwrap();
+            let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations");
+            db.migrate(Path::new(migrations_path), false).await.unwrap();
+            db
+        });
+
+        db.runtime = Some(runtime);
+
+        Self {
+            db: Some(Arc::new(db)),
+            connection: None,
+        }
+    }
+
+    pub fn db(&self) -> &Arc<Database> {
+        self.db.as_ref().unwrap()
+    }
+}
+
+impl Drop for TestDb {
+    fn drop(&mut self) {
+        let db = self.db.take().unwrap();
+        if let sea_orm::DatabaseBackend::Postgres = db.pool.get_database_backend() {
+            db.runtime.as_ref().unwrap().block_on(async {
+                use util::ResultExt;
+                let query = "
+                        SELECT pg_terminate_backend(pg_stat_activity.pid)
+                        FROM pg_stat_activity
+                        WHERE
+                            pg_stat_activity.datname = current_database() AND
+                            pid <> pg_backend_pid();
+                    ";
+                db.pool
+                    .execute(sea_orm::Statement::from_string(
+                        db.pool.get_database_backend(),
+                        query.into(),
+                    ))
+                    .await
+                    .log_err();
+                sqlx::Postgres::drop_database(db.options.get_url())
+                    .await
+                    .log_err();
+            })
+        }
+    }
+}

crates/collab/src/tests.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    db::{NewUserParams, TestDb, UserId},
+    db::{test_db::TestDb, NewUserParams, UserId},
     executor::Executor,
     rpc::{Server, CLEANUP_TIMEOUT},
     AppState,