Start on a new `db2` module that uses SeaORM

Antonio Scandurra created

Change summary

Cargo.lock                                                     | 280 +++
crates/collab/Cargo.toml                                       |   2 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql |   2 
crates/collab/src/db2.rs                                       | 316 ++++
crates/collab/src/db2/project.rs                               |  37 
crates/collab/src/db2/project_collaborator.rs                  |  18 
crates/collab/src/db2/room.rs                                  |  31 
crates/collab/src/db2/room_participant.rs                      |  34 
crates/collab/src/db2/worktree.rs                              |  33 
crates/collab/src/lib.rs                                       |  12 
crates/collab/src/main.rs                                      |   1 
11 files changed, 765 insertions(+), 1 deletion(-)

Detailed changes

Cargo.lock 🔗

@@ -2,6 +2,12 @@
 # It is not intended for manual editing.
 version = 3
 
+[[package]]
+name = "Inflector"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
+
 [[package]]
 name = "activity_indicator"
 version = "0.1.0"
@@ -107,6 +113,12 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "aliasable"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
+
 [[package]]
 name = "ambient-authority"
 version = "0.0.1"
@@ -547,6 +559,19 @@ dependencies = [
  "rustc-demangle",
 ]
 
+[[package]]
+name = "bae"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33b8de67cc41132507eeece2584804efcb15f85ba516e34c944b7667f480397a"
+dependencies = [
+ "heck 0.3.3",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "base64"
 version = "0.13.0"
@@ -635,6 +660,51 @@ dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "borsh"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa"
+dependencies = [
+ "borsh-derive",
+ "hashbrown 0.11.2",
+]
+
+[[package]]
+name = "borsh-derive"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775"
+dependencies = [
+ "borsh-derive-internal",
+ "borsh-schema-derive-internal",
+ "proc-macro-crate",
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "borsh-derive-internal"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "borsh-schema-derive-internal"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "breadcrumbs"
 version = "0.1.0"
@@ -678,6 +748,27 @@ version = "3.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
 
+[[package]]
+name = "bytecheck"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d11cac2c12b5adc6570dad2ee1b87eff4955dac476fe12d81e5fdd352e52406f"
+dependencies = [
+ "bytecheck_derive",
+ "ptr_meta",
+]
+
+[[package]]
+name = "bytecheck_derive"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13e576ebe98e605500b3c8041bb888e966653577172df6dd97398714eb30b9bf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "bytemuck"
 version = "1.12.1"
@@ -841,6 +932,7 @@ dependencies = [
  "js-sys",
  "num-integer",
  "num-traits",
+ "serde",
  "time 0.1.44",
  "wasm-bindgen",
  "winapi 0.3.9",
@@ -1065,6 +1157,7 @@ dependencies = [
  "reqwest",
  "rpc",
  "scrypt",
+ "sea-orm",
  "sea-query",
  "sea-query-binder",
  "serde",
@@ -3843,6 +3936,29 @@ version = "6.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff"
 
+[[package]]
+name = "ouroboros"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbb50b356159620db6ac971c6d5c9ab788c9cc38a6f49619fca2a27acb062ca"
+dependencies = [
+ "aliasable",
+ "ouroboros_macro",
+]
+
+[[package]]
+name = "ouroboros_macro"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a0d9d1a6191c4f391f87219d1ea42b23f09ee84d64763cd05ee6ea88d9f384d"
+dependencies = [
+ "Inflector",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "outline"
 version = "0.1.0"
@@ -4201,6 +4317,15 @@ version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
 
+[[package]]
+name = "proc-macro-crate"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785"
+dependencies = [
+ "toml",
+]
+
 [[package]]
 name = "proc-macro-error"
 version = "1.0.4"
@@ -4446,6 +4571,26 @@ dependencies = [
  "cc",
 ]
 
+[[package]]
+name = "ptr_meta"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
+dependencies = [
+ "ptr_meta_derive",
+]
+
+[[package]]
+name = "ptr_meta_derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "pulldown-cmark"
 version = "0.9.2"
@@ -4683,6 +4828,15 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "rend"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79af64b4b6362ffba04eef3a4e10829718a4896dac19daa741851c86781edf95"
+dependencies = [
+ "bytecheck",
+]
+
 [[package]]
 name = "reqwest"
 version = "0.11.12"
@@ -4760,6 +4914,31 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "rkyv"
+version = "0.7.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cec2b3485b07d96ddfd3134767b8a447b45ea4eb91448d0a35180ec0ffd5ed15"
+dependencies = [
+ "bytecheck",
+ "hashbrown 0.12.3",
+ "ptr_meta",
+ "rend",
+ "rkyv_derive",
+ "seahash",
+]
+
+[[package]]
+name = "rkyv_derive"
+version = "0.7.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6eaedadc88b53e36dd32d940ed21ae4d850d5916f2581526921f553a72ac34c4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "rmp"
 version = "0.8.11"
@@ -4911,6 +5090,24 @@ dependencies = [
  "walkdir",
 ]
 
+[[package]]
+name = "rust_decimal"
+version = "1.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33c321ee4e17d2b7abe12b5d20c1231db708dd36185c8a21e9de5fed6da4dbe9"
+dependencies = [
+ "arrayvec 0.7.2",
+ "borsh",
+ "bytecheck",
+ "byteorder",
+ "bytes 1.2.1",
+ "num-traits",
+ "rand 0.8.5",
+ "rkyv",
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "rustc-demangle"
 version = "0.1.21"
@@ -4982,6 +5179,12 @@ dependencies = [
  "base64",
 ]
 
+[[package]]
+name = "rustversion"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8"
+
 [[package]]
 name = "rustybuzz"
 version = "0.3.0"
@@ -5123,13 +5326,59 @@ dependencies = [
  "untrusted",
 ]
 
+[[package]]
+name = "sea-orm"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3120bc435b8640963ffda698f877610e07e077157e216eb99408d819c344034d"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "chrono",
+ "futures 0.3.24",
+ "futures-util",
+ "log",
+ "ouroboros",
+ "rust_decimal",
+ "sea-orm-macros",
+ "sea-query",
+ "sea-query-binder",
+ "sea-strum",
+ "serde",
+ "serde_json",
+ "sqlx",
+ "thiserror",
+ "time 0.3.15",
+ "tracing",
+ "url",
+ "uuid 1.2.1",
+]
+
+[[package]]
+name = "sea-orm-macros"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c54bacfeb842813c16821e21f9456c358861a448294075184ea1d6307e386d08"
+dependencies = [
+ "bae",
+ "heck 0.3.3",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "sea-query"
 version = "0.27.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a4f0fc4d8e44e1d51c739a68d336252a18bc59553778075d5e32649be6ec92ed"
 dependencies = [
+ "chrono",
+ "rust_decimal",
  "sea-query-derive",
+ "serde_json",
+ "time 0.3.15",
+ "uuid 1.2.1",
 ]
 
 [[package]]
@@ -5138,8 +5387,13 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c2585b89c985cfacfe0ec9fc9e7bb055b776c1a2581c4e3c6185af2b8bf8865"
 dependencies = [
+ "chrono",
+ "rust_decimal",
  "sea-query",
+ "serde_json",
  "sqlx",
+ "time 0.3.15",
+ "uuid 1.2.1",
 ]
 
 [[package]]
@@ -5155,6 +5409,28 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "sea-strum"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "391d06a6007842cfe79ac6f7f53911b76dfd69fc9a6769f1cf6569d12ce20e1b"
+dependencies = [
+ "sea-strum_macros",
+]
+
+[[package]]
+name = "sea-strum_macros"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69b4397b825df6ccf1e98bcdabef3bbcfc47ff5853983467850eeab878384f21"
+dependencies = [
+ "heck 0.3.3",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn",
+]
+
 [[package]]
 name = "seahash"
 version = "4.1.0"
@@ -5670,6 +5946,7 @@ dependencies = [
  "bitflags",
  "byteorder",
  "bytes 1.2.1",
+ "chrono",
  "crc",
  "crossbeam-queue",
  "dirs 4.0.0",
@@ -5693,10 +5970,12 @@ dependencies = [
  "log",
  "md-5",
  "memchr",
+ "num-bigint",
  "once_cell",
  "paste",
  "percent-encoding",
  "rand 0.8.5",
+ "rust_decimal",
  "rustls 0.20.7",
  "rustls-pemfile",
  "serde",
@@ -6847,6 +7126,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83"
 dependencies = [
  "getrandom 0.2.7",
+ "serde",
 ]
 
 [[package]]

crates/collab/Cargo.toml 🔗

@@ -36,6 +36,7 @@ prometheus = "0.13"
 rand = "0.8"
 reqwest = { version = "0.11", features = ["json"], optional = true }
 scrypt = "0.7"
+sea-orm = { version = "0.10", features = ["sqlx-postgres", "runtime-tokio-rustls"] }
 sea-query = { version = "0.27", features = ["derive"] }
 sea-query-binder = { version = "0.2", features = ["sqlx-postgres"] }
 serde = { version = "1.0", features = ["derive", "rc"] }
@@ -74,6 +75,7 @@ env_logger = "0.9"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 util = { path = "../util" }
 lazy_static = "1.4"
+sea-orm = { version = "0.10", features = ["sqlx-sqlite"] }
 sea-query-binder = { version = "0.2", features = ["sqlx-sqlite"] }
 serde_json = { version = "1.0", features = ["preserve_order"] }
 sqlx = { version = "0.6", features = ["sqlite"] }

crates/collab/migrations.sqlite/20221109000000_test_schema.sql 🔗

@@ -41,7 +41,7 @@ CREATE TABLE "rooms" (
 
 CREATE TABLE "projects" (
     "id" INTEGER PRIMARY KEY,
-    "room_id" INTEGER REFERENCES rooms (id),
+    "room_id" INTEGER REFERENCES rooms (id) NOT NULL,
     "host_user_id" INTEGER REFERENCES users (id) NOT NULL,
     "host_connection_id" INTEGER NOT NULL
 );

crates/collab/src/db2.rs 🔗

@@ -0,0 +1,316 @@
+mod project;
+mod project_collaborator;
+mod room;
+mod room_participant;
+mod worktree;
+
+use crate::{Error, Result};
+use anyhow::anyhow;
+use collections::HashMap;
+use dashmap::DashMap;
+use futures::StreamExt;
+use rpc::{proto, ConnectionId};
+use sea_orm::ActiveValue;
+use sea_orm::{
+    entity::prelude::*, ConnectOptions, DatabaseConnection, DatabaseTransaction, DbErr,
+    TransactionTrait,
+};
+use serde::{Deserialize, Serialize};
+use std::ops::{Deref, DerefMut};
+use std::{future::Future, marker::PhantomData, rc::Rc, sync::Arc};
+use tokio::sync::{Mutex, OwnedMutexGuard};
+
+pub struct Database {
+    pool: DatabaseConnection,
+    rooms: DashMap<RoomId, Arc<Mutex<()>>>,
+    #[cfg(test)]
+    background: Option<std::sync::Arc<gpui::executor::Background>>,
+    #[cfg(test)]
+    runtime: Option<tokio::runtime::Runtime>,
+}
+
+impl Database {
+    pub async fn new(url: &str, max_connections: u32) -> Result<Self> {
+        let mut options = ConnectOptions::new(url.into());
+        options.max_connections(max_connections);
+        Ok(Self {
+            pool: sea_orm::Database::connect(options).await?,
+            rooms: DashMap::with_capacity(16384),
+            #[cfg(test)]
+            background: None,
+            #[cfg(test)]
+            runtime: None,
+        })
+    }
+
+    pub async fn share_project(
+        &self,
+        room_id: RoomId,
+        connection_id: ConnectionId,
+        worktrees: &[proto::WorktreeMetadata],
+    ) -> Result<RoomGuard<(ProjectId, proto::Room)>> {
+        self.transact(|tx| async move {
+            let participant = room_participant::Entity::find()
+                .filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.0))
+                .one(&tx)
+                .await?
+                .ok_or_else(|| anyhow!("could not find participant"))?;
+            if participant.room_id != room_id.0 {
+                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(connection_id.0 as i32),
+                ..Default::default()
+            }
+            .insert(&tx)
+            .await?;
+
+            worktree::Entity::insert_many(worktrees.iter().map(|worktree| worktree::ActiveModel {
+                id: ActiveValue::set(worktree.id as i32),
+                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),
+                is_complete: ActiveValue::set(false),
+            }))
+            .exec(&tx)
+            .await?;
+
+            project_collaborator::ActiveModel {
+                project_id: ActiveValue::set(project.id),
+                connection_id: ActiveValue::set(connection_id.0 as i32),
+                user_id: ActiveValue::set(participant.user_id),
+                replica_id: ActiveValue::set(0),
+                is_host: ActiveValue::set(true),
+                ..Default::default()
+            }
+            .insert(&tx)
+            .await?;
+
+            let room = self.get_room(room_id, &tx).await?;
+            self.commit_room_transaction(room_id, tx, (ProjectId(project.id), room))
+                .await
+        })
+        .await
+    }
+
+    async fn get_room(&self, room_id: RoomId, tx: &DatabaseTransaction) -> Result<proto::Room> {
+        let db_room = room::Entity::find_by_id(room_id.0)
+            .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) = db_participant.answering_connection_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 as u64,
+                            },
+                        ))
+                    }
+                    (Some(1), _) => Some(proto::participant_location::Variant::UnsharedProject(
+                        Default::default(),
+                    )),
+                    _ => Some(proto::participant_location::Variant::External(
+                        Default::default(),
+                    )),
+                };
+                participants.insert(
+                    answering_connection_id,
+                    proto::Participant {
+                        user_id: db_participant.user_id as u64,
+                        peer_id: answering_connection_id as u32,
+                        projects: Default::default(),
+                        location: Some(proto::ParticipantLocation { variant: location }),
+                    },
+                );
+            } else {
+                pending_participants.push(proto::PendingParticipant {
+                    user_id: db_participant.user_id as u64,
+                    calling_user_id: db_participant.calling_user_id as u64,
+                    initial_project_id: db_participant.initial_project_id.map(|id| id as u64),
+                });
+            }
+        }
+
+        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?;
+            if let Some(participant) = participants.get_mut(&db_project.host_connection_id) {
+                let project = if let Some(project) = participant
+                    .projects
+                    .iter_mut()
+                    .find(|project| project.id as i32 == db_project.id)
+                {
+                    project
+                } else {
+                    participant.projects.push(proto::ParticipantProject {
+                        id: db_project.id as u64,
+                        worktree_root_names: Default::default(),
+                    });
+                    participant.projects.last_mut().unwrap()
+                };
+
+                if let Some(db_worktree) = db_worktree {
+                    project.worktree_root_names.push(db_worktree.root_name);
+                }
+            }
+        }
+
+        Ok(proto::Room {
+            id: db_room.id as u64,
+            live_kit_room: db_room.live_kit_room,
+            participants: participants.into_values().collect(),
+            pending_participants,
+        })
+    }
+
+    async fn commit_room_transaction<T>(
+        &self,
+        room_id: RoomId,
+        tx: DatabaseTransaction,
+        data: T,
+    ) -> Result<RoomGuard<T>> {
+        let lock = self.rooms.entry(room_id).or_default().clone();
+        let _guard = lock.lock_owned().await;
+        tx.commit().await?;
+        Ok(RoomGuard {
+            data,
+            _guard,
+            _not_send: PhantomData,
+        })
+    }
+
+    async fn transact<F, Fut, T>(&self, f: F) -> Result<T>
+    where
+        F: Send + Fn(DatabaseTransaction) -> Fut,
+        Fut: Send + Future<Output = Result<T>>,
+    {
+        let body = async {
+            loop {
+                let tx = self.pool.begin().await?;
+                match f(tx).await {
+                    Ok(result) => return Ok(result),
+                    Err(error) => match error {
+                        Error::Database2(
+                            DbErr::Exec(sea_orm::RuntimeErr::SqlxError(error))
+                            | DbErr::Query(sea_orm::RuntimeErr::SqlxError(error)),
+                        ) if error
+                            .as_database_error()
+                            .and_then(|error| error.code())
+                            .as_deref()
+                            == Some("40001") =>
+                        {
+                            // Retry (don't break the loop)
+                        }
+                        error @ _ => return Err(error),
+                    },
+                }
+            }
+        };
+
+        #[cfg(test)]
+        {
+            if let Some(background) = self.background.as_ref() {
+                background.simulate_random_delay().await;
+            }
+
+            self.runtime.as_ref().unwrap().block_on(body)
+        }
+
+        #[cfg(not(test))]
+        {
+            body.await
+        }
+    }
+}
+
+pub struct RoomGuard<T> {
+    data: T,
+    _guard: OwnedMutexGuard<()>,
+    _not_send: PhantomData<Rc<()>>,
+}
+
+impl<T> Deref for RoomGuard<T> {
+    type Target = T;
+
+    fn deref(&self) -> &T {
+        &self.data
+    }
+}
+
+impl<T> DerefMut for RoomGuard<T> {
+    fn deref_mut(&mut self) -> &mut T {
+        &mut self.data
+    }
+}
+
+macro_rules! id_type {
+    ($name:ident) => {
+        #[derive(
+            Clone,
+            Copy,
+            Debug,
+            Default,
+            PartialEq,
+            Eq,
+            PartialOrd,
+            Ord,
+            Hash,
+            sqlx::Type,
+            Serialize,
+            Deserialize,
+        )]
+        #[sqlx(transparent)]
+        #[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)
+            }
+        }
+    };
+}
+
+id_type!(UserId);
+id_type!(RoomId);
+id_type!(RoomParticipantId);
+id_type!(ProjectId);
+id_type!(WorktreeId);

crates/collab/src/db2/project.rs 🔗

@@ -0,0 +1,37 @@
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "projects")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: i32,
+    pub room_id: i32,
+    pub host_user_id: i32,
+    pub host_connection_id: i32,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(
+        belongs_to = "super::room::Entity",
+        from = "Column::RoomId",
+        to = "super::room::Column::Id"
+    )]
+    Room,
+    #[sea_orm(has_many = "super::worktree::Entity")]
+    Worktree,
+}
+
+impl Related<super::room::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Room.def()
+    }
+}
+
+impl Related<super::worktree::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Worktree.def()
+    }
+}
+
+impl ActiveModelBehavior for ActiveModel {}

crates/collab/src/db2/project_collaborator.rs 🔗

@@ -0,0 +1,18 @@
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "project_collaborators")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: i32,
+    pub project_id: i32,
+    pub connection_id: i32,
+    pub user_id: i32,
+    pub replica_id: i32,
+    pub is_host: bool,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {}
+
+impl ActiveModelBehavior for ActiveModel {}

crates/collab/src/db2/room.rs 🔗

@@ -0,0 +1,31 @@
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "room_participants")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: i32,
+    pub live_kit_room: String,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(has_many = "super::room_participant::Entity")]
+    RoomParticipant,
+    #[sea_orm(has_many = "super::project::Entity")]
+    Project,
+}
+
+impl Related<super::room_participant::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::RoomParticipant.def()
+    }
+}
+
+impl Related<super::project::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Project.def()
+    }
+}
+
+impl ActiveModelBehavior for ActiveModel {}

crates/collab/src/db2/room_participant.rs 🔗

@@ -0,0 +1,34 @@
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "room_participants")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: i32,
+    pub room_id: i32,
+    pub user_id: i32,
+    pub answering_connection_id: Option<i32>,
+    pub location_kind: Option<i32>,
+    pub location_project_id: Option<i32>,
+    pub initial_project_id: Option<i32>,
+    pub calling_user_id: i32,
+    pub calling_connection_id: i32,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(
+        belongs_to = "super::room::Entity",
+        from = "Column::RoomId",
+        to = "super::room::Column::Id"
+    )]
+    Room,
+}
+
+impl Related<super::room::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Room.def()
+    }
+}
+
+impl ActiveModelBehavior for ActiveModel {}

crates/collab/src/db2/worktree.rs 🔗

@@ -0,0 +1,33 @@
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "worktrees")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: i32,
+    #[sea_orm(primary_key)]
+    pub project_id: i32,
+    pub abs_path: String,
+    pub root_name: String,
+    pub visible: bool,
+    pub scan_id: i64,
+    pub is_complete: bool,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(
+        belongs_to = "super::project::Entity",
+        from = "Column::ProjectId",
+        to = "super::project::Column::Id"
+    )]
+    Project,
+}
+
+impl Related<super::project::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Project.def()
+    }
+}
+
+impl ActiveModelBehavior for ActiveModel {}

crates/collab/src/lib.rs 🔗

@@ -5,6 +5,7 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
 pub enum Error {
     Http(StatusCode, String),
     Database(sqlx::Error),
+    Database2(sea_orm::error::DbErr),
     Internal(anyhow::Error),
 }
 
@@ -20,6 +21,12 @@ impl From<sqlx::Error> for Error {
     }
 }
 
+impl From<sea_orm::error::DbErr> for Error {
+    fn from(error: sea_orm::error::DbErr) -> Self {
+        Self::Database2(error)
+    }
+}
+
 impl From<axum::Error> for Error {
     fn from(error: axum::Error) -> Self {
         Self::Internal(error.into())
@@ -45,6 +52,9 @@ impl IntoResponse for Error {
             Error::Database(error) => {
                 (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
             }
+            Error::Database2(error) => {
+                (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
+            }
             Error::Internal(error) => {
                 (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
             }
@@ -57,6 +67,7 @@ impl std::fmt::Debug for Error {
         match self {
             Error::Http(code, message) => (code, message).fmt(f),
             Error::Database(error) => error.fmt(f),
+            Error::Database2(error) => error.fmt(f),
             Error::Internal(error) => error.fmt(f),
         }
     }
@@ -67,6 +78,7 @@ impl std::fmt::Display for Error {
         match self {
             Error::Http(code, message) => write!(f, "{code}: {message}"),
             Error::Database(error) => error.fmt(f),
+            Error::Database2(error) => error.fmt(f),
             Error::Internal(error) => error.fmt(f),
         }
     }