@@ -356,6 +356,33 @@ impl DuckDBTableProviderFactory {
356356
357357 Ok ( pool)
358358 }
359+
360+ /// Drop the cached pool entry for `key` if any, returning the previously
361+ /// cached pool. Subsequent calls to `get_or_init_*` for the same key will
362+ /// build a fresh pool.
363+ ///
364+ /// This is intended for callers that replace the underlying database file
365+ /// out-of-band (for example, after restoring it from a snapshot in object
366+ /// storage). Existing connections held by other clones of the previously
367+ /// returned pool keep operating against the file descriptor they opened;
368+ /// once they are dropped the OS releases the prior inode. New providers
369+ /// built after invalidation will open the file fresh and observe the
370+ /// replacement contents.
371+ pub async fn invalidate_instance ( & self , key : & DbInstanceKey ) -> Option < DuckDbConnectionPool > {
372+ self . instances . lock ( ) . await . remove ( key)
373+ }
374+
375+ /// Drop the cached pool entry for the file at `path` if any.
376+ ///
377+ /// Convenience wrapper over [`Self::invalidate_instance`] for the common
378+ /// file-mode case.
379+ pub async fn invalidate_file_instance (
380+ & self ,
381+ path : impl Into < Arc < str > > ,
382+ ) -> Option < DuckDbConnectionPool > {
383+ self . invalidate_instance ( & DbInstanceKey :: file ( path. into ( ) ) )
384+ . await
385+ }
359386}
360387
361388type DynDuckDbConnectionPool = dyn DbConnectionPool < r2d2:: PooledConnection < DuckdbConnectionManager > , DuckDBParameter >
@@ -779,6 +806,45 @@ pub(crate) mod tests {
779806 use std:: collections:: HashMap ;
780807 use std:: sync:: Arc ;
781808
809+ #[ tokio:: test]
810+ async fn invalidate_instance_drops_cached_pool ( ) {
811+ let factory = DuckDBTableProviderFactory :: new ( duckdb:: AccessMode :: ReadWrite ) ;
812+ let options = HashMap :: new ( ) ;
813+
814+ // First call populates the cache.
815+ let _pool1 = factory
816+ . get_or_init_memory_instance ( & options)
817+ . await
818+ . expect ( "first init" ) ;
819+ assert_eq ! ( factory. instances. lock( ) . await . len( ) , 1 ) ;
820+
821+ // Second call (without invalidation) returns the cached pool clone
822+ // without growing the registry.
823+ let _pool2 = factory
824+ . get_or_init_memory_instance ( & options)
825+ . await
826+ . expect ( "cached init" ) ;
827+ assert_eq ! ( factory. instances. lock( ) . await . len( ) , 1 ) ;
828+
829+ // Invalidate; entry is evicted and returned.
830+ let evicted = factory. invalidate_instance ( & DbInstanceKey :: memory ( ) ) . await ;
831+ assert ! ( evicted. is_some( ) , "invalidate returns evicted pool" ) ;
832+ assert_eq ! ( factory. instances. lock( ) . await . len( ) , 0 ) ;
833+
834+ // Re-invalidating a missing key is a no-op.
835+ assert ! ( factory
836+ . invalidate_instance( & DbInstanceKey :: file( "never-cached" . into( ) ) )
837+ . await
838+ . is_none( ) ) ;
839+
840+ // Next get_or_init repopulates the cache.
841+ let _pool3 = factory
842+ . get_or_init_memory_instance ( & options)
843+ . await
844+ . expect ( "reinit after invalidate" ) ;
845+ assert_eq ! ( factory. instances. lock( ) . await . len( ) , 1 ) ;
846+ }
847+
782848 #[ tokio:: test]
783849 async fn test_create_with_memory_limit ( ) {
784850 let table_name = TableReference :: bare ( "test_table" ) ;
0 commit comments