Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions aw-datastore/src/datastore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1037,4 +1037,115 @@ 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.
pub fn migrate_hostname(
&mut self,
conn: &Connection,
new_hostname: &str,
) -> Result<usize, DatastoreError> {
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");
}
Comment thread
TimeToBuildBob marked this conversation as resolved.

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 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,
) -> Result<usize, DatastoreError> {
info!("Migrating 'aw-watcher-android-test' bucket names to 'aw-watcher-android'");

let updated = match conn.execute(
"UPDATE OR IGNORE buckets SET name = 'aw-watcher-android' || SUBSTR(name, LENGTH('aw-watcher-android-test') + 1) \
WHERE name LIKE 'aw-watcher-android-test%'",
[],
) {
Comment thread
TimeToBuildBob marked this conversation as resolved.
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)
}
}
61 changes: 61 additions & 0 deletions aw-datastore/src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ pub enum Command {
SetKeyValue(String, String),
DeleteKeyValue(String),
RefreshPrivacyFilter(),
RenameBucket(String, String),
MigrateHostname(String),
MigrateTestBucketNames(),
Close(),
}

Expand Down Expand Up @@ -404,6 +407,33 @@ 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) => {
if count > 0 {
self.commit = true;
}
Ok(Response::Count(count as i64))
}
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())
Expand Down Expand Up @@ -620,6 +650,37 @@ 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());
_unwrap_empty_response(self.request(cmd)?)
}

/// 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<usize, DatastoreError> {
let cmd = Command::MigrateHostname(new_hostname.to_string());
match self.request(cmd)? {
Response::Count(n) => Ok(n as usize),
_ => Err(DatastoreError::InternalError(
"Unexpected response to MigrateHostname command".to_string(),
)),
}
}

/// 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<usize, DatastoreError> {
let cmd = Command::MigrateTestBucketNames();
match self.request(cmd)? {
Response::Count(n) => Ok(n as usize),
_ => Err(DatastoreError::InternalError(
"Unexpected response to MigrateTestBucketNames command".to_string(),
)),
}
}

// Should block until worker has stopped
pub fn close(&self) {
info!("Sending close request to database");
Expand Down
49 changes: 49 additions & 0 deletions aw-server/src/android/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,55 @@ 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_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_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,
Expand Down
Loading