Skip to content

Commit be332fc

Browse files
authored
Read hf:// URLs from DuckDB by mounting stores where the registry says (#9265)
Duckdb filesystem resolution uses default registry behaviour
1 parent 1d3e6b4 commit be332fc

3 files changed

Lines changed: 31 additions & 32 deletions

File tree

vortex-cloud/src/hf/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
//! `resolve` prefix, carrying a bearer token when one is available, is the whole implementation.
99
//! Reads therefore keep every [`object_store::ClientOptions`] setting a caller passes —
1010
//! connect/request timeouts, retries, proxy configuration, `allow_http` — unlike the OpenDAL-backed
11-
//! schemes in [`crate::opendal`], whose bridge owns its own HTTP client.
11+
//! schemes in the crate's `opendal` module, whose bridge owns its own HTTP client.
1212
//!
1313
//! # URL grammar
1414
//!

vortex-duckdb/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ tracing-subscriber = { workspace = true }
4040
url = { workspace = true }
4141
vortex = { workspace = true, features = [
4242
"files",
43+
"hf",
4344
"tokio",
4445
"object_store",
4546
"object_store_registry",

vortex-duckdb/src/multi_file.rs

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ use vortex::io::filesystem::FileSystemRef;
1818
use vortex::io::object_store::ObjectStoreFileSystem;
1919
use vortex::io::runtime::BlockingRuntime;
2020
use vortex::layout::scan::multi::MultiLayoutDataSource;
21-
use vortex_utils::aliases::hash_map::HashMap;
2221

2322
use crate::RUNTIME;
2423
use crate::SESSION;
@@ -28,23 +27,34 @@ use crate::duckdb::ExtractedValue;
2827
/// Process-wide registry, so repeated scans against the same bucket share one client.
2928
static 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

Comments
 (0)