@@ -18,7 +18,6 @@ use vortex::io::filesystem::FileSystemRef;
1818use vortex:: io:: object_store:: ObjectStoreFileSystem ;
1919use vortex:: io:: runtime:: BlockingRuntime ;
2020use vortex:: layout:: scan:: multi:: MultiLayoutDataSource ;
21- use vortex_utils:: aliases:: hash_map:: HashMap ;
2221
2322use crate :: RUNTIME ;
2423use crate :: SESSION ;
@@ -28,23 +27,34 @@ use crate::duckdb::ExtractedValue;
2827/// Process-wide registry, so repeated scans against the same bucket share one client.
2928static REGISTRY : LazyLock < Registry > = LazyLock :: new ( Registry :: new) ;
3029
31- fn resolve_filesystem ( base_url : & Url ) -> VortexResult < FileSystemRef > {
30+ fn resolve_filesystem ( glob_url : & Url ) -> VortexResult < ( FileSystemRef , String ) > {
3231 // Compat makes us use tokio which is very bad for local reads on
3332 // high-core machines because reads go into blocking pool
34- if base_url. scheme ( ) == "file" {
35- return Ok ( Arc :: new ( ObjectStoreFileSystem :: local ( RUNTIME . handle ( ) ) ) ) ;
33+ if glob_url. scheme ( ) == "file" {
34+ return Ok ( (
35+ Arc :: new ( ObjectStoreFileSystem :: local ( RUNTIME . handle ( ) ) ) ,
36+ glob_url. path ( ) . to_string ( ) ,
37+ ) ) ;
3638 }
3739
38- // `base_url` has its path cleared by the caller, so the resolved path is empty and only the
39- // store matters here. Going through the shared registry means DuckDB resolves the same set of
40- // schemes as the Python and Java bindings, including the OpenDAL-backed ones when the
41- // `opendal` feature is on.
42- let ( object_store, _) = REGISTRY . resolve ( base_url) ?;
40+ // The full URL goes through the shared registry, which reports the glob as a path *within*
41+ // the store it returns. For most schemes the store is mounted at the URL authority, so the
42+ // path is the whole URL path — but not for all of them: an `hf://` store is rooted at a
43+ // repository and revision, which occupy path segments. Only the registry knows how deep the
44+ // store is mounted, so globbing anything other than the path it reports would address the
45+ // wrong keys. Going through the registry also means DuckDB resolves the same set of schemes
46+ // as the Python and Java bindings, including the OpenDAL-backed ones when the `opendal`
47+ // feature is on. The registry caches one client per store prefix, so repeated scans against
48+ // the same bucket or repository share a client even though the filesystem wrapper is rebuilt.
49+ let ( object_store, path) = REGISTRY . resolve ( glob_url) ?;
4350
44- Ok ( Arc :: new ( ObjectStoreFileSystem :: new (
45- Arc :: new ( Compat :: new ( object_store) ) ,
46- RUNTIME . handle ( ) ,
47- ) ) )
51+ Ok ( (
52+ Arc :: new ( ObjectStoreFileSystem :: new (
53+ Arc :: new ( Compat :: new ( object_store) ) ,
54+ RUNTIME . handle ( ) ,
55+ ) ) ,
56+ path. to_string ( ) ,
57+ ) )
4858}
4959
5060/// Shared bind logic for both single-glob and multi-glob variants.
@@ -77,28 +87,16 @@ pub fn bind_multi_file_scan(input: &BindInputRef) -> VortexResult<MultiLayoutDat
7787 glob_urls. push ( parse_uri_or_path ( glob_str) ?) ;
7888 }
7989
80- // Cache filesystems by base URL to avoid resolving the same filesystem multiple times.
81- let mut fs_cache: HashMap < Url , FileSystemRef > = HashMap :: new ( ) ;
82- for glob_url in & glob_urls {
83- let mut base_url = glob_url. clone ( ) ;
84- base_url. set_path ( "" ) ;
85- if !fs_cache. contains_key ( & base_url) {
86- let fs = resolve_filesystem ( & base_url) ?;
87- fs_cache. insert ( base_url, fs) ;
88- }
89- }
90+ let resolved = glob_urls
91+ . iter ( )
92+ . map ( resolve_filesystem)
93+ . collect :: < VortexResult < Vec < _ > > > ( ) ?;
9094
9195 RUNTIME . block_on ( async {
9296 let mut builder = MultiFileDataSource :: new ( SESSION . clone ( ) ) ;
9397
94- for glob_url in & glob_urls {
95- let mut base_url = glob_url. clone ( ) ;
96- base_url. set_path ( "" ) ;
97- let fs = fs_cache
98- . get ( & base_url)
99- . map ( Arc :: clone)
100- . unwrap_or_else ( || unreachable ! ( "fs should be cached for all base URLs" ) ) ;
101- builder = builder. with_glob ( glob_url. path ( ) , Some ( fs) ) ;
98+ for ( fs, glob) in resolved {
99+ builder = builder. with_glob ( & glob, Some ( fs) ) ;
102100 }
103101
104102 builder. build ( ) . await
0 commit comments