Reorganize source files of collab::db

Max Brunsfeld created

* Move all sea_orm tables into a 'tables' module
* Move TestDb into its own file
* Move id types into their own module

Change summary

crates/collab/src/db.rs                                     | 377 +-----
crates/collab/src/db/db_tests.rs                            |   5 
crates/collab/src/db/ids.rs                                 | 125 ++
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 
27 files changed, 397 insertions(+), 412 deletions(-)

Detailed changes

crates/collab/src/db.rs 🔗

@@ -1,56 +1,47 @@
-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;
+mod db_tests;
+mod ids;
+mod tables;
+#[cfg(test)]
+pub mod test_db;
 
 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,
+    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};
-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 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};
-pub use user::Model as User;
+
+pub use ids::*;
+pub use sea_orm::ConnectOptions;
+pub use tables::user::Model as User;
 
 pub struct Database {
     options: ConnectOptions,
@@ -4083,6 +4074,60 @@ impl<T> RoomGuard<T> {
     }
 }
 
+#[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,
+        }
+    }
+}
+
+#[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,
+}
+
 #[derive(Debug, Serialize, Deserialize)]
 pub struct NewUserParams {
     pub github_login: String,
@@ -4120,139 +4165,6 @@ fn random_email_confirmation_code() -> String {
     nanoid::nanoid!(64)
 }
 
-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> {
-                match v {
-                    Value::TinyInt(Some(int)) => {
-                        Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?))
-                    }
-                    Value::SmallInt(Some(int)) => {
-                        Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?))
-                    }
-                    Value::Int(Some(int)) => {
-                        Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?))
-                    }
-                    Value::BigInt(Some(int)) => {
-                        Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?))
-                    }
-                    Value::TinyUnsigned(Some(int)) => {
-                        Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?))
-                    }
-                    Value::SmallUnsigned(Some(int)) => {
-                        Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?))
-                    }
-                    Value::Unsigned(Some(int)) => {
-                        Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?))
-                    }
-                    Value::BigUnsigned(Some(int)) => {
-                        Ok(Self(int.try_into().map_err(|_| sea_query::ValueTypeErr)?))
-                    }
-                    _ => Err(sea_query::ValueTypeErr),
-                }
-            }
-
-            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)
-            }
-        }
-    };
-}
-
-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);
-
 #[derive(Clone)]
 pub struct JoinRoom {
     pub room: proto::Room,
@@ -4370,130 +4282,3 @@ pub struct WorktreeSettingsFile {
 enum QueryUserIds {
     UserId,
 }
-
-#[cfg(test)]
-pub use test::*;
-
-#[cfg(test)]
-mod test {
-    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/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/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,