From 321b7a240bf1dcac2bbe0eac62a718df48320bc8 Mon Sep 17 00:00:00 2001 From: Brayo Date: Thu, 26 Mar 2026 13:40:42 +0300 Subject: [PATCH 1/6] feat(android): migrate unknown hostname --- aw-datastore/src/datastore.rs | 36 +++++++++++++++++++++++++++++++++++ aw-datastore/src/worker.rs | 28 +++++++++++++++++++++++++++ aw-server/src/android/mod.rs | 18 ++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/aw-datastore/src/datastore.rs b/aw-datastore/src/datastore.rs index 20cd3634..4e16e114 100644 --- a/aw-datastore/src/datastore.rs +++ b/aw-datastore/src/datastore.rs @@ -1037,4 +1037,40 @@ impl DatastoreInstance { }, } } + + /// Migrates all buckets whose hostname is "unknown" or "Unknown" to `new_hostname`. + /// Events are left untouched; only the bucket metadata is updated. + /// Returns the number of buckets that were updated. + pub fn migrate_hostname( + &mut self, + conn: &Connection, + new_hostname: &str, + ) -> Result { + info!( + "Migrating hostname from 'unknown'/'Unknown' to '{}'", + new_hostname + ); + + let updated = match conn.execute( + "UPDATE buckets SET hostname = ?1 WHERE hostname = 'unknown' OR hostname = 'Unknown'", + [new_hostname], + ) { + Ok(n) => n, + Err(err) => { + return Err(DatastoreError::InternalError(format!( + "Failed to migrate hostname: {err}" + ))) + } + }; + + if updated > 0 { + info!("Migrated hostname for {} bucket(s)", updated); + // Refresh the in-memory cache so callers see the new hostnames immediately. + self.get_stored_buckets(conn)?; + } else { + info!("No buckets with hostname 'unknown'/'Unknown' found; nothing to migrate"); + } + + Ok(updated) + } } diff --git a/aw-datastore/src/worker.rs b/aw-datastore/src/worker.rs index e9481da1..24c84161 100644 --- a/aw-datastore/src/worker.rs +++ b/aw-datastore/src/worker.rs @@ -78,6 +78,7 @@ pub enum Command { SetKeyValue(String, String), DeleteKeyValue(String), RefreshPrivacyFilter(), + MigrateHostname(String), Close(), } @@ -404,6 +405,17 @@ impl DatastoreWorker { } Ok(Response::Empty()) } + Command::MigrateHostname(new_hostname) => { + match ds.migrate_hostname(tx, &new_hostname) { + Ok(count) => { + if count > 0 { + self.commit = true; + } + Ok(Response::Count(count as i64)) + } + Err(e) => Err(e), + } + } Command::Close() => { self.quit = true; Ok(Response::Empty()) @@ -620,6 +632,22 @@ impl Datastore { _unwrap_empty_response(self.request(Command::RefreshPrivacyFilter())?) } + /// Migrates all buckets whose hostname is "unknown" or "Unknown" to `new_hostname`. + /// Returns the number of buckets updated. + pub fn migrate_hostname(&self, new_hostname: &str) -> Result { + let cmd = Command::MigrateHostname(new_hostname.to_string()); + let receiver = self.requester.request(cmd).unwrap(); + match receiver.collect().unwrap() { + Ok(r) => match r { + Response::Count(n) => Ok(n as usize), + _ => Err(DatastoreError::InternalError( + "Unexpected response to MigrateHostname command".to_string(), + )), + }, + Err(e) => Err(e), + } + } + // Should block until worker has stopped pub fn close(&self) { info!("Sending close request to database"); diff --git a/aw-server/src/android/mod.rs b/aw-server/src/android/mod.rs index fae6140c..9fef7dd0 100644 --- a/aw-server/src/android/mod.rs +++ b/aw-server/src/android/mod.rs @@ -248,6 +248,24 @@ pub mod android { } } + #[no_mangle] + pub unsafe extern "C" fn Java_net_activitywatch_android_RustInterface_migrateHostname( + env: JNIEnv, + _: JClass, + hostname: JString, + ) -> jstring { + let hostname = jstring_to_string(&env, hostname); + if hostname.is_empty() { + return create_error_object(&env, "hostname must not be empty".to_string()); + } + match openDatastore().migrate_hostname(&hostname) { + Ok(count) => { + string_to_jstring(&env, format!("Migrated hostname for {} bucket(s)", count)) + } + Err(e) => create_error_object(&env, format!("Failed to migrate hostname: {:?}", e)), + } + } + #[no_mangle] pub unsafe extern "C" fn Java_net_activitywatch_android_RustInterface_query( env: JNIEnv, From 4ed8ddd7c5c238011dc74025b7e92eb8ef02057e Mon Sep 17 00:00:00 2001 From: Brayo Date: Sun, 29 Mar 2026 17:57:45 +0300 Subject: [PATCH 2/6] feat(android): migrate legacy bucket name --- aw-datastore/src/datastore.rs | 38 +++++++++++++++++++++++++++++++++++ aw-datastore/src/worker.rs | 15 ++++++++++++++ aw-server/src/android/mod.rs | 14 +++++++++++++ 3 files changed, 67 insertions(+) diff --git a/aw-datastore/src/datastore.rs b/aw-datastore/src/datastore.rs index 4e16e114..1263cbd0 100644 --- a/aw-datastore/src/datastore.rs +++ b/aw-datastore/src/datastore.rs @@ -1038,6 +1038,44 @@ impl DatastoreInstance { } } + /// Renames a bucket from `old_id` to `new_id`. + /// Events are left untouched because they reference the integer row ID, not the name. + /// Returns `NoSuchBucket` if `old_id` does not exist, or `BucketAlreadyExists` if + /// `new_id` is already taken. + pub fn rename_bucket( + &mut self, + conn: &Connection, + old_id: &str, + new_id: &str, + ) -> Result<(), DatastoreError> { + if !self.buckets_cache.contains_key(old_id) { + return Err(DatastoreError::NoSuchBucket(old_id.to_string())); + } + if self.buckets_cache.contains_key(new_id) { + return Err(DatastoreError::BucketAlreadyExists(new_id.to_string())); + } + + match conn.execute( + "UPDATE buckets SET name = ?1 WHERE name = ?2", + [new_id, old_id], + ) { + Ok(0) => Err(DatastoreError::NoSuchBucket(old_id.to_string())), + Ok(_) => { + info!("Renamed bucket '{}' to '{}'", old_id, new_id); + // Update the in-memory cache: remove the old entry and re-insert under the new id. + if let Some(mut bucket) = self.buckets_cache.remove(old_id) { + bucket.id = new_id.to_string(); + self.buckets_cache.insert(new_id.to_string(), bucket); + } + Ok(()) + } + Err(err) => Err(DatastoreError::InternalError(format!( + "Failed to rename bucket '{}' to '{}': {err}", + old_id, new_id + ))), + } + } + /// Migrates all buckets whose hostname is "unknown" or "Unknown" to `new_hostname`. /// Events are left untouched; only the bucket metadata is updated. /// Returns the number of buckets that were updated. diff --git a/aw-datastore/src/worker.rs b/aw-datastore/src/worker.rs index 24c84161..cf81f6e1 100644 --- a/aw-datastore/src/worker.rs +++ b/aw-datastore/src/worker.rs @@ -78,6 +78,7 @@ pub enum Command { SetKeyValue(String, String), DeleteKeyValue(String), RefreshPrivacyFilter(), + RenameBucket(String, String), MigrateHostname(String), Close(), } @@ -405,6 +406,13 @@ impl DatastoreWorker { } Ok(Response::Empty()) } + Command::RenameBucket(old_id, new_id) => match ds.rename_bucket(tx, &old_id, &new_id) { + Ok(()) => { + self.commit = true; + Ok(Response::Empty()) + } + Err(e) => Err(e), + }, Command::MigrateHostname(new_hostname) => { match ds.migrate_hostname(tx, &new_hostname) { Ok(count) => { @@ -632,6 +640,13 @@ impl Datastore { _unwrap_empty_response(self.request(Command::RefreshPrivacyFilter())?) } + /// Renames a bucket from `old_id` to `new_id`. + pub fn rename_bucket(&self, old_id: &str, new_id: &str) -> Result<(), DatastoreError> { + let cmd = Command::RenameBucket(old_id.to_string(), new_id.to_string()); + let receiver = self.requester.request(cmd).unwrap(); + _unwrap_response(receiver) + } + /// Migrates all buckets whose hostname is "unknown" or "Unknown" to `new_hostname`. /// Returns the number of buckets updated. pub fn migrate_hostname(&self, new_hostname: &str) -> Result { diff --git a/aw-server/src/android/mod.rs b/aw-server/src/android/mod.rs index 9fef7dd0..7214fccb 100644 --- a/aw-server/src/android/mod.rs +++ b/aw-server/src/android/mod.rs @@ -266,6 +266,20 @@ pub mod android { } } + #[no_mangle] + pub unsafe extern "C" fn Java_net_activitywatch_android_RustInterface_migrateAndroidBucketName( + env: JNIEnv, + _: JClass, + ) -> jstring { + match openDatastore().rename_bucket("aw-android-test", "aw-android") { + Ok(()) => string_to_jstring( + &env, + "Renamed bucket 'aw-android-test' to 'aw-android'".to_string(), + ), + Err(e) => create_error_object(&env, format!("Failed to rename bucket: {:?}", e)), + } + } + #[no_mangle] pub unsafe extern "C" fn Java_net_activitywatch_android_RustInterface_query( env: JNIEnv, From db738e71b5162cb9a0091aadccbae80fe1a34637 Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 2 Jul 2026 21:43:10 +0000 Subject: [PATCH 3/6] feat(android): add migration for aw-watcher-android-test bucket names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds migrate_test_bucket_names() to DatastoreInstance and the Datastore worker, which renames any bucket whose name starts with 'aw-watcher-android-test' to 'aw-watcher-android' (e.g. aw-watcher-android-test_hostname → aw-watcher-android_hostname). This covers the old debug-build bucket naming convention and complements the existing migrateAndroidBucketName() exact rename that handles the 'aw-android-test' → 'aw-android' legacy case. Exposes a JNI entry point: Java_net_activitywatch_android_RustInterface_migrateWatcherAndroidBucketNames Requested in ActivityWatch/aw-android#150. --- aw-datastore/src/datastore.rs | 35 +++++++++++++++++++++++++++++++++++ aw-datastore/src/worker.rs | 29 +++++++++++++++++++++++++++++ aw-server/src/android/mod.rs | 17 +++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/aw-datastore/src/datastore.rs b/aw-datastore/src/datastore.rs index 1263cbd0..75eff5f3 100644 --- a/aw-datastore/src/datastore.rs +++ b/aw-datastore/src/datastore.rs @@ -1111,4 +1111,39 @@ impl DatastoreInstance { Ok(updated) } + + /// Migrates all buckets whose name starts with `aw-watcher-android-test` to use + /// `aw-watcher-android` instead. This covers the old debug-build bucket naming + /// convention (e.g. `aw-watcher-android-test_hostname` → `aw-watcher-android_hostname`). + /// Events are left untouched; only the bucket metadata is updated. + /// Returns the number of buckets that were updated. + pub fn migrate_test_bucket_names( + &mut self, + conn: &Connection, + ) -> Result { + info!("Migrating 'aw-watcher-android-test' bucket names to 'aw-watcher-android'"); + + let updated = match conn.execute( + "UPDATE buckets SET name = REPLACE(name, 'aw-watcher-android-test', 'aw-watcher-android') \ + WHERE name LIKE 'aw-watcher-android-test%'", + [], + ) { + Ok(n) => n, + Err(err) => { + return Err(DatastoreError::InternalError(format!( + "Failed to migrate test bucket names: {err}" + ))) + } + }; + + if updated > 0 { + info!("Migrated {} 'aw-watcher-android-test' bucket(s)", updated); + // Refresh the in-memory cache so callers see the new names immediately. + self.get_stored_buckets(conn)?; + } else { + info!("No 'aw-watcher-android-test' buckets found; nothing to migrate"); + } + + Ok(updated) + } } diff --git a/aw-datastore/src/worker.rs b/aw-datastore/src/worker.rs index cf81f6e1..2c41916b 100644 --- a/aw-datastore/src/worker.rs +++ b/aw-datastore/src/worker.rs @@ -80,6 +80,7 @@ pub enum Command { RefreshPrivacyFilter(), RenameBucket(String, String), MigrateHostname(String), + MigrateTestBucketNames(), Close(), } @@ -424,6 +425,17 @@ impl DatastoreWorker { Err(e) => Err(e), } } + Command::MigrateTestBucketNames() => { + match ds.migrate_test_bucket_names(tx) { + Ok(count) => { + if count > 0 { + self.commit = true; + } + Ok(Response::Count(count as i64)) + } + Err(e) => Err(e), + } + } Command::Close() => { self.quit = true; Ok(Response::Empty()) @@ -663,6 +675,23 @@ impl Datastore { } } + /// Migrates all buckets whose name starts with `aw-watcher-android-test` to use + /// `aw-watcher-android` instead (e.g. debug-build buckets from older app versions). + /// Returns the number of buckets updated. + pub fn migrate_test_bucket_names(&self) -> Result { + let cmd = Command::MigrateTestBucketNames(); + let receiver = self.requester.request(cmd).unwrap(); + match receiver.collect().unwrap() { + Ok(r) => match r { + Response::Count(n) => Ok(n as usize), + _ => Err(DatastoreError::InternalError( + "Unexpected response to MigrateTestBucketNames command".to_string(), + )), + }, + Err(e) => Err(e), + } + } + // Should block until worker has stopped pub fn close(&self) { info!("Sending close request to database"); diff --git a/aw-server/src/android/mod.rs b/aw-server/src/android/mod.rs index 7214fccb..1c0b1efa 100644 --- a/aw-server/src/android/mod.rs +++ b/aw-server/src/android/mod.rs @@ -280,6 +280,23 @@ pub mod android { } } + #[no_mangle] + pub unsafe extern "C" fn Java_net_activitywatch_android_RustInterface_migrateWatcherAndroidBucketNames( + env: JNIEnv, + _: JClass, + ) -> jstring { + match openDatastore().migrate_test_bucket_names() { + Ok(count) => string_to_jstring( + &env, + format!("Migrated {} 'aw-watcher-android-test' bucket(s)", count), + ), + Err(e) => create_error_object( + &env, + format!("Failed to migrate watcher bucket names: {:?}", e), + ), + } + } + #[no_mangle] pub unsafe extern "C" fn Java_net_activitywatch_android_RustInterface_query( env: JNIEnv, From cdafb86fa6d19e4e082f0fd0699883c82637c557 Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 2 Jul 2026 22:07:26 +0000 Subject: [PATCH 4/6] =?UTF-8?q?fix(datastore):=20address=20Greptile=20revi?= =?UTF-8?q?ew=20=E2=80=94=20OR=20IGNORE=20collision=20handling=20and=20pre?= =?UTF-8?q?fix-only=20rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - P1: Change UPDATE to UPDATE OR IGNORE so a UNIQUE constraint collision on any single target name skips conflicting rows instead of aborting the entire batch migration - P2: Replace REPLACE() with prefix-based SUBSTR() so only the leading 'aw-watcher-android-test' prefix is swapped, not all substring occurrences --- aw-datastore/src/datastore.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aw-datastore/src/datastore.rs b/aw-datastore/src/datastore.rs index 75eff5f3..66e4df4a 100644 --- a/aw-datastore/src/datastore.rs +++ b/aw-datastore/src/datastore.rs @@ -1116,7 +1116,9 @@ impl DatastoreInstance { /// `aw-watcher-android` instead. This covers the old debug-build bucket naming /// convention (e.g. `aw-watcher-android-test_hostname` → `aw-watcher-android_hostname`). /// Events are left untouched; only the bucket metadata is updated. - /// Returns the number of buckets that were updated. + /// Returns the number of buckets that were migrated. + /// Note: if a UNIQUE constraint violation occurs on any single row, `UPDATE OR IGNORE` + /// will skip conflicting rows instead of aborting the entire batch. pub fn migrate_test_bucket_names( &mut self, conn: &Connection, @@ -1124,7 +1126,7 @@ impl DatastoreInstance { info!("Migrating 'aw-watcher-android-test' bucket names to 'aw-watcher-android'"); let updated = match conn.execute( - "UPDATE buckets SET name = REPLACE(name, 'aw-watcher-android-test', 'aw-watcher-android') \ + "UPDATE OR IGNORE buckets SET name = 'aw-watcher-android' || SUBSTR(name, LENGTH('aw-watcher-android-test') + 1) \ WHERE name LIKE 'aw-watcher-android-test%'", [], ) { From 9157941ccb09d313b2f18ad7d136fabb58f1a6a9 Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 2 Jul 2026 22:20:35 +0000 Subject: [PATCH 5/6] fix(datastore): adapt rename_bucket to current worker API --- aw-datastore/src/worker.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/aw-datastore/src/worker.rs b/aw-datastore/src/worker.rs index 2c41916b..f59c24f6 100644 --- a/aw-datastore/src/worker.rs +++ b/aw-datastore/src/worker.rs @@ -425,17 +425,15 @@ impl DatastoreWorker { Err(e) => Err(e), } } - Command::MigrateTestBucketNames() => { - match ds.migrate_test_bucket_names(tx) { - Ok(count) => { - if count > 0 { - self.commit = true; - } - Ok(Response::Count(count as i64)) + Command::MigrateTestBucketNames() => match ds.migrate_test_bucket_names(tx) { + Ok(count) => { + if count > 0 { + self.commit = true; } - Err(e) => Err(e), + Ok(Response::Count(count as i64)) } - } + Err(e) => Err(e), + }, Command::Close() => { self.quit = true; Ok(Response::Empty()) @@ -655,8 +653,7 @@ impl Datastore { /// Renames a bucket from `old_id` to `new_id`. pub fn rename_bucket(&self, old_id: &str, new_id: &str) -> Result<(), DatastoreError> { let cmd = Command::RenameBucket(old_id.to_string(), new_id.to_string()); - let receiver = self.requester.request(cmd).unwrap(); - _unwrap_response(receiver) + _unwrap_empty_response(self.request(cmd)?) } /// Migrates all buckets whose hostname is "unknown" or "Unknown" to `new_hostname`. From eaeb67205ee73303b532b86c2fdc7bc9c1a5b98d Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 2 Jul 2026 22:51:23 +0000 Subject: [PATCH 6/6] fix(datastore): route migration requests through worker helper --- aw-datastore/src/worker.rs | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/aw-datastore/src/worker.rs b/aw-datastore/src/worker.rs index f59c24f6..add5a756 100644 --- a/aw-datastore/src/worker.rs +++ b/aw-datastore/src/worker.rs @@ -660,15 +660,11 @@ impl Datastore { /// Returns the number of buckets updated. pub fn migrate_hostname(&self, new_hostname: &str) -> Result { let cmd = Command::MigrateHostname(new_hostname.to_string()); - let receiver = self.requester.request(cmd).unwrap(); - match receiver.collect().unwrap() { - Ok(r) => match r { - Response::Count(n) => Ok(n as usize), - _ => Err(DatastoreError::InternalError( - "Unexpected response to MigrateHostname command".to_string(), - )), - }, - Err(e) => Err(e), + match self.request(cmd)? { + Response::Count(n) => Ok(n as usize), + _ => Err(DatastoreError::InternalError( + "Unexpected response to MigrateHostname command".to_string(), + )), } } @@ -677,15 +673,11 @@ impl Datastore { /// Returns the number of buckets updated. pub fn migrate_test_bucket_names(&self) -> Result { let cmd = Command::MigrateTestBucketNames(); - let receiver = self.requester.request(cmd).unwrap(); - match receiver.collect().unwrap() { - Ok(r) => match r { - Response::Count(n) => Ok(n as usize), - _ => Err(DatastoreError::InternalError( - "Unexpected response to MigrateTestBucketNames command".to_string(), - )), - }, - Err(e) => Err(e), + match self.request(cmd)? { + Response::Count(n) => Ok(n as usize), + _ => Err(DatastoreError::InternalError( + "Unexpected response to MigrateTestBucketNames command".to_string(), + )), } }