From 4a45bebc9fe344862d6776d5d59047a5f2c540db Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 16 Mar 2025 21:32:55 +0100 Subject: [PATCH 1/2] feat: added sync group table and models --- migrations/2025-02-23-095541_initial/up.sql | 7 +++++ src/db/mod.rs | 2 ++ src/db/models/device.rs | 1 + src/db/models/mod.rs | 1 + src/db/models/sync_group.rs | 33 +++++++++++++++++++++ src/db/schema.rs | 8 +++++ 6 files changed, 52 insertions(+) create mode 100644 src/db/models/sync_group.rs diff --git a/migrations/2025-02-23-095541_initial/up.sql b/migrations/2025-02-23-095541_initial/up.sql index 76449a4..e5e7922 100644 --- a/migrations/2025-02-23-095541_initial/up.sql +++ b/migrations/2025-02-23-095541_initial/up.sql @@ -24,6 +24,9 @@ create table devices ( user_id bigint not null references users (id) on delete cascade, + sync_group_id bigint + references sync_group (id) + on delete set null, caption text not null, type text not null, @@ -31,6 +34,10 @@ create table devices ( unique (user_id, device_id) ); +create table sync_groups ( + id integer primary key not null +); + create table device_subscriptions ( id integer primary key not null, diff --git a/src/db/mod.rs b/src/db/mod.rs index 54c5f74..ef3eeb8 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -5,10 +5,12 @@ mod schema; use diesel::connection::InstrumentationEvent; use diesel::r2d2::CustomizeConnection; use diesel::Connection; + pub use models::device::{Device, DeviceType, NewDevice}; pub use models::device_subscription::{DeviceSubscription, NewDeviceSubscription}; pub use models::episode_action::{ActionType, EpisodeAction, NewEpisodeAction}; pub use models::session::Session; +pub use models::sync_group::SyncGroup; pub use models::user::{NewUser, User}; pub use repository::SqliteRepository; diff --git a/src/db/models/device.rs b/src/db/models/device.rs index f94bf43..2be8f75 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -21,6 +21,7 @@ pub struct Device { pub user_id: i64, pub caption: String, pub type_: DeviceType, + pub sync_group_id: Option, } #[derive(Deserialize, Insertable)] diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 440060b..8e6ebf9 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -2,4 +2,5 @@ pub mod device; pub mod device_subscription; pub mod episode_action; pub mod session; +pub mod sync_group; pub mod user; diff --git a/src/db/models/sync_group.rs b/src/db/models/sync_group.rs new file mode 100644 index 0000000..edeca8c --- /dev/null +++ b/src/db/models/sync_group.rs @@ -0,0 +1,33 @@ +use diesel::{ + dsl::{exists, not}, + prelude::*, +}; + +use crate::db::schema::*; + +#[derive(Queryable, Selectable)] +#[diesel(table_name = sync_groups)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct SyncGroup { + pub id: i64, +} + +impl SyncGroup { + pub fn new(conn: &mut SqliteConnection) -> QueryResult { + diesel::insert_into(sync_groups::table) + .default_values() + .returning(SyncGroup::as_returning()) + .get_result(conn) + } + + pub fn remove_unused(conn: &mut SqliteConnection) -> QueryResult { + diesel::delete( + sync_groups::table.filter(not(exists( + devices::table + .select(1.into_sql::()) + .filter(devices::sync_group_id.eq(sync_groups::id.nullable())), + ))), + ) + .execute(conn) + } +} diff --git a/src/db/schema.rs b/src/db/schema.rs index 2f597f1..7eb0429 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -15,6 +15,7 @@ diesel::table! { id -> BigInt, device_id -> Text, user_id -> BigInt, + sync_group_id -> Nullable, caption -> Text, #[sql_name = "type"] type_ -> Text, @@ -45,6 +46,12 @@ diesel::table! { } } +diesel::table! { + sync_groups (id) { + id -> BigInt, + } +} + diesel::table! { users (id) { id -> BigInt, @@ -64,5 +71,6 @@ diesel::allow_tables_to_appear_in_same_query!( devices, episode_actions, sessions, + sync_groups, users, ); From 158910a61fdb2712879eef2eaa43f15b8b4bb901 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 16 Mar 2025 21:42:11 +0100 Subject: [PATCH 2/2] feat: implement sync group merge, unsync and devices by sync group --- src/db/repository/device.rs | 106 ++++++++++++++++++++++++++++++++++-- src/gpodder/mod.rs | 4 +- src/gpodder/repository.rs | 2 +- 3 files changed, 104 insertions(+), 8 deletions(-) diff --git a/src/db/repository/device.rs b/src/db/repository/device.rs index 2d02c1c..38d5d4c 100644 --- a/src/db/repository/device.rs +++ b/src/db/repository/device.rs @@ -3,7 +3,7 @@ use diesel::prelude::*; use super::SqliteRepository; use crate::{ - db::{self, schema::*}, + db::{self, schema::*, SyncGroup}, gpodder, }; @@ -102,7 +102,58 @@ impl gpodder::DeviceRepository for SqliteRepository { user: &gpodder::User, device_ids: Vec<&str>, ) -> Result { - todo!() + let conn = &mut self.pool.get()?; + + Ok(conn.transaction(|conn| { + let devices: Vec<(i64, Option)> = devices::table + .select((devices::id, devices::sync_group_id)) + .filter( + devices::user_id + .eq(user.id) + .and(devices::device_id.eq_any(device_ids)), + ) + .get_results(conn)?; + + let mut sync_group_ids: Vec = devices + .iter() + .filter_map(|(_, group_id)| *group_id) + .collect(); + + // Remove any duplicates, giving us each sync group ID once + sync_group_ids.sort(); + sync_group_ids.dedup(); + + // If any of the devices are already in a sync group, we reuse the first one we find. + // Otherwise, we generate a new one. + let sync_group_id = if let Some(id) = sync_group_ids.pop() { + id + } else { + db::SyncGroup::new(conn)?.id + }; + + // Move all devices in the other sync groups into the new sync group + diesel::update( + devices::table.filter(devices::sync_group_id.eq_any(sync_group_ids.iter())), + ) + .set(devices::sync_group_id.eq(sync_group_id)) + .execute(conn)?; + + // Add the non-synchronized devices into the new sync group + let unsynced_device_ids = + devices + .iter() + .filter_map(|(id, group_id)| if group_id.is_none() { Some(id) } else { None }); + + diesel::update(devices::table.filter(devices::id.eq_any(unsynced_device_ids))) + .set(devices::sync_group_id.eq(sync_group_id)) + .execute(conn)?; + + // Remove the other now unused sync groups + diesel::delete(sync_groups::table.filter(sync_groups::id.eq_any(sync_group_ids))) + .execute(conn)?; + + Ok::<_, diesel::result::Error>(sync_group_id) + })?) } fn remove_from_sync_group( @@ -110,7 +161,23 @@ impl gpodder::DeviceRepository for SqliteRepository { user: &gpodder::User, device_ids: Vec<&str>, ) -> Result<(), gpodder::AuthErr> { - todo!() + let conn = &mut self.pool.get()?; + + diesel::update( + devices::table.filter( + devices::user_id + .eq(user.id) + .and(devices::device_id.eq_any(device_ids)), + ), + ) + .set(devices::sync_group_id.eq(None::)) + .execute(conn)?; + + // This is in a different transaction on purpose, as the success of this removal shouldn't + // fail the entire query + SyncGroup::remove_unused(conn)?; + + Ok(()) } fn synchronize_sync_group( @@ -124,7 +191,36 @@ impl gpodder::DeviceRepository for SqliteRepository { fn devices_by_sync_group( &self, user: &gpodder::User, - ) -> Result<(Vec, Vec>), gpodder::AuthErr> { - todo!() + ) -> Result<(Vec, Vec>), gpodder::AuthErr> { + let mut not_synchronized = Vec::new(); + let mut synchronized = Vec::new(); + + let conn = &mut self.pool.get()?; + let mut devices = devices::table + .select((devices::device_id, devices::sync_group_id)) + .filter(devices::user_id.eq(user.id)) + .order(devices::sync_group_id) + .load_iter::<(String, Option), _>(conn)?; + + let mut cur_group = &mut not_synchronized; + let mut cur_group_id: Option = None; + + while let Some((device_id, group_id)) = devices.next().transpose()? { + if group_id != cur_group_id { + if group_id.is_none() { + cur_group = &mut not_synchronized; + } else { + synchronized.push(Vec::new()); + let index = synchronized.len() - 1; + cur_group = &mut synchronized[index]; + } + + cur_group_id = group_id; + } + + cur_group.push(device_id); + } + + Ok((not_synchronized, synchronized)) } } diff --git a/src/gpodder/mod.rs b/src/gpodder/mod.rs index 7d26b84..c9abd8e 100644 --- a/src/gpodder/mod.rs +++ b/src/gpodder/mod.rs @@ -92,7 +92,7 @@ pub trait DeviceRepository { /// Remove the devices from their respective sync groups fn remove_from_sync_group(&self, user: &User, device_ids: Vec<&str>) -> Result<(), AuthErr>; - /// Return all devices for the user, grouped per sync group + /// Return all device IDs for the user, grouped per sync group /// /// # Returns /// @@ -100,7 +100,7 @@ pub trait DeviceRepository { fn devices_by_sync_group( &self, user: &User, - ) -> Result<(Vec, Vec>), AuthErr>; + ) -> Result<(Vec, Vec>), AuthErr>; } pub trait SubscriptionRepository { diff --git a/src/gpodder/repository.rs b/src/gpodder/repository.rs index 4cc17be..62f860e 100644 --- a/src/gpodder/repository.rs +++ b/src/gpodder/repository.rs @@ -106,7 +106,7 @@ impl GpodderRepository { pub fn devices_by_sync_group( &self, user: &models::User, - ) -> Result<(Vec, Vec>), AuthErr> { + ) -> Result<(Vec, Vec>), AuthErr> { self.store.devices_by_sync_group(user) }