This is an automated email from the ASF dual-hosted git repository.

alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs-object-store.git


The following commit(s) were added to refs/heads/main by this push:
     new c2048e0  feat: add deregister to ObjectStoreRegistry (#784)
c2048e0 is described below

commit c2048e03380ca4656203ab0f0421873382ecf161
Author: Krisztián Szűcs <[email protected]>
AuthorDate: Wed Jul 8 20:23:53 2026 +0200

    feat: add deregister to ObjectStoreRegistry (#784)
    
    Add `ObjectStoreRegistry::deregister`, the inverse of `register`: it removes
    the store registered at the exact scheme/authority/path of a URL and returns
    it, pruning emptied tree branches. A default implementation returns `None`, 
so
    the addition is non-breaking for existing implementors.
---
 src/registry.rs | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 111 insertions(+)

diff --git a/src/registry.rs b/src/registry.rs
index 4794842..29282c1 100644
--- a/src/registry.rs
+++ b/src/registry.rs
@@ -82,6 +82,36 @@ pub trait ObjectStoreRegistry: Send + Sync + std::fmt::Debug 
+ 'static {
     /// assert!(Arc::ptr_eq(&ret, &bucket3));
     /// ```
     fn resolve(&self, url: &Url) -> crate::Result<(Arc<dyn ObjectStore>, 
Path)>;
+
+    /// Deregister the store registered for exactly `url`, returning it if 
present.
+    ///
+    /// Unlike [`resolve`](Self::resolve), which matches the longest registered
+    /// path that is a prefix of the object URL, this removes the store
+    /// registered at the *exact* scheme, authority and path of `url` (the
+    /// inverse of [`register`](Self::register)). A store registered at a
+    /// different path under the same authority is left in place.
+    ///
+    /// The default implementation does nothing and returns `None`.
+    ///
+    /// ```
+    /// # use std::sync::Arc;
+    /// # use url::Url;
+    /// # use object_store::memory::InMemory;
+    /// # use object_store::ObjectStore;
+    /// # use object_store::registry::{DefaultObjectStoreRegistry, 
ObjectStoreRegistry};
+    /// #
+    /// let registry = DefaultObjectStoreRegistry::new();
+    /// let bucket = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
+    /// let url = Url::parse("s3://bucket/path").unwrap();
+    /// registry.register(url.clone(), bucket.clone());
+    ///
+    /// let removed = registry.deregister(&url).unwrap();
+    /// assert!(Arc::ptr_eq(&removed, &bucket));
+    /// ```
+    fn deregister(&self, url: &Url) -> Option<Arc<dyn ObjectStore>> {
+        let _ = url;
+        None
+    }
 }
 
 /// Error type for [`DefaultObjectStoreRegistry`]
@@ -164,6 +194,22 @@ impl PathEntry {
         }
         ret
     }
+
+    /// Remove the store registered at the exact `segments` path under this
+    /// entry, returning it. Child entries left empty are pruned.
+    fn remove(&mut self, segments: &[&str]) -> Option<Arc<dyn ObjectStore>> {
+        match segments.split_first() {
+            None => self.store.take(),
+            Some((head, rest)) => {
+                let child = self.children.get_mut(*head)?;
+                let removed = child.remove(rest);
+                if child.store.is_none() && child.children.is_empty() {
+                    self.children.remove(*head);
+                }
+                removed
+            }
+        }
+    }
 }
 
 impl DefaultObjectStoreRegistry {
@@ -215,6 +261,19 @@ impl ObjectStoreRegistry for DefaultObjectStoreRegistry {
 
         Err(Error::NotFound.into())
     }
+
+    fn deregister(&self, url: &Url) -> Option<Arc<dyn ObjectStore>> {
+        let mut map = self.map.write();
+        let key = url_key(url);
+        let segments: Vec<&str> = path_segments(url.path()).collect();
+        let entry = map.get_mut(key)?;
+        let removed = entry.remove(&segments);
+        // Drop the authority entry entirely once it holds no stores.
+        if entry.store.is_none() && entry.children.is_empty() {
+            map.remove(key);
+        }
+        removed
+    }
 }
 
 /// Extracts the scheme and authority of a URL (components before the Path)
@@ -343,4 +402,56 @@ mod tests {
         assert_eq!(path.as_ref(), "6/7");
         assert!(Arc::ptr_eq(&resolved, &custom_scheme));
     }
+
+    #[test]
+    fn test_deregister() {
+        let registry = DefaultObjectStoreRegistry::new();
+        let a = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
+        let b = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
+
+        // Use a scheme `parse_url_opts` cannot lazily create, so a failed
+        // `resolve` reflects deregistration rather than the environment.
+        let a_url = Url::parse("mock://bucket/a").unwrap();
+        let b_url = Url::parse("mock://bucket/b").unwrap();
+        registry.register(a_url.clone(), Arc::clone(&a));
+        registry.register(b_url.clone(), Arc::clone(&b));
+
+        // Removes exactly the store at `a`, leaving its sibling `b` in place.
+        let removed = registry.deregister(&a_url).unwrap();
+        assert!(Arc::ptr_eq(&removed, &a));
+        assert!(registry.resolve(&a_url).is_err());
+        let (resolved, _) = registry.resolve(&b_url).unwrap();
+        assert!(Arc::ptr_eq(&resolved, &b));
+
+        // Deregistering again returns None.
+        assert!(registry.deregister(&a_url).is_none());
+        // A never-registered URL returns None.
+        assert!(
+            registry
+                .deregister(&Url::parse("mock://other/x").unwrap())
+                .is_none()
+        );
+    }
+
+    #[test]
+    fn test_deregister_keeps_longer_prefix() {
+        let registry = DefaultObjectStoreRegistry::new();
+        let outer = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
+        let inner = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
+        let outer_url = Url::parse("mock://bucket/a").unwrap();
+        let inner_url = Url::parse("mock://bucket/a/b").unwrap();
+        registry.register(outer_url.clone(), Arc::clone(&outer));
+        registry.register(inner_url.clone(), Arc::clone(&inner));
+
+        // Removing the outer mount leaves the nested one resolvable.
+        assert!(Arc::ptr_eq(
+            &registry.deregister(&outer_url).unwrap(),
+            &outer
+        ));
+        let (resolved, path) = registry
+            .resolve(&Url::parse("mock://bucket/a/b/x").unwrap())
+            .unwrap();
+        assert!(Arc::ptr_eq(&resolved, &inner));
+        assert_eq!(path.as_ref(), "x");
+    }
 }

Reply via email to