@@ -5,18 +5,23 @@ use std::path::Path;
55use std:: path:: absolute;
66use std:: sync:: Arc ;
77
8+ use itertools:: Itertools ;
89use url:: Url ;
910use vortex:: error:: VortexResult ;
11+ use vortex:: error:: vortex_bail;
1012use vortex:: error:: vortex_err;
1113use vortex:: file:: multi:: MultiFileDataSource ;
14+ use vortex:: io:: filesystem:: FileSystemRef ;
1215use vortex:: io:: runtime:: BlockingRuntime ;
1316use vortex:: scan:: DataSourceRef ;
17+ use vortex_utils:: aliases:: hash_map:: HashMap ;
1418
1519use crate :: RUNTIME ;
1620use crate :: SESSION ;
1721use crate :: datasource:: DataSourceTableFunction ;
1822use crate :: duckdb:: BindInputRef ;
1923use crate :: duckdb:: ClientContextRef ;
24+ use crate :: duckdb:: ExtractedValue ;
2025use crate :: duckdb:: LogicalType ;
2126use crate :: filesystem:: resolve_filesystem;
2227
@@ -60,34 +65,95 @@ fn normalize_path(path: std::path::PathBuf) -> std::path::PathBuf {
6065#[ derive( Debug ) ]
6166pub struct VortexMultiFileScan ;
6267
68+ /// Variant of [`VortexMultiFileScan`] that accepts a list of globs.
69+ ///
70+ /// This is registered as a separate overload to handle `read_vortex(['a.vortex', 'b.vortex'])`.
71+ #[ derive( Debug ) ]
72+ pub struct VortexMultiFileScanList ;
73+
6374impl DataSourceTableFunction for VortexMultiFileScan {
6475 fn parameters ( ) -> Vec < LogicalType > {
6576 vec ! [ LogicalType :: varchar( ) ]
6677 }
6778
6879 fn bind ( ctx : & ClientContextRef , input : & BindInputRef ) -> VortexResult < DataSourceRef > {
69- let glob_url_parameter = input
70- . get_parameter ( 0 )
71- . ok_or_else ( || vortex_err ! ( "Missing file glob parameter" ) ) ?;
80+ bind_multi_file_scan ( ctx, input)
81+ }
82+ }
83+
84+ impl DataSourceTableFunction for VortexMultiFileScanList {
85+ fn parameters ( ) -> Vec < LogicalType > {
86+ vec ! [
87+ LogicalType :: list_type( LogicalType :: varchar( ) )
88+ . unwrap_or_else( |_| unreachable!( "creating list<varchar> type should not fail" ) ) ,
89+ ]
90+ }
91+
92+ fn bind ( ctx : & ClientContextRef , input : & BindInputRef ) -> VortexResult < DataSourceRef > {
93+ bind_multi_file_scan ( ctx, input)
94+ }
95+ }
7296
73- // Parse the URL and separate the base URL (keep scheme, host, etc.) from the path.
74- let glob_url_string = glob_url_parameter. as_string ( ) ;
75- let glob_url_str = glob_url_string. as_str ( ) ;
76- let glob_url = parse_glob_url ( glob_url_str) ?;
97+ /// Shared bind logic for both single-glob and multi-glob variants.
98+ fn bind_multi_file_scan (
99+ ctx : & ClientContextRef ,
100+ input : & BindInputRef ,
101+ ) -> VortexResult < DataSourceRef > {
102+ let glob_url_parameter = input
103+ . get_parameter ( 0 )
104+ . ok_or_else ( || vortex_err ! ( "Missing file glob parameter" ) ) ?;
77105
106+ // The input to the table function can either be a single glob, or a List of glob patterns.
107+ let glob_strings: Vec < String > = match glob_url_parameter. extract ( ) {
108+ ExtractedValue :: Varchar ( glob) => {
109+ vec ! [ glob. to_string( ) ]
110+ }
111+ ExtractedValue :: List ( globs) => globs
112+ . into_iter ( )
113+ . map ( |glob| {
114+ let ExtractedValue :: Varchar ( string) = glob. extract ( ) else {
115+ vortex_bail ! ( "list element must be Varchar type" )
116+ } ;
117+
118+ Ok ( string. to_string ( ) )
119+ } )
120+ . try_collect ( ) ?,
121+ _ => vortex_bail ! ( "Invalid argument to read_vortex table function" ) ,
122+ } ;
123+
124+ // Parse each glob URL and resolve its filesystem.
125+ let mut glob_urls: Vec < Url > = Vec :: with_capacity ( glob_strings. len ( ) ) ;
126+ for glob_str in & glob_strings {
127+ glob_urls. push ( parse_glob_url ( glob_str) ?) ;
128+ }
129+
130+ // Cache filesystems by base URL to avoid resolving the same filesystem multiple times.
131+ let mut fs_cache: HashMap < Url , FileSystemRef > = HashMap :: new ( ) ;
132+ for glob_url in & glob_urls {
78133 let mut base_url = glob_url. clone ( ) ;
79134 base_url. set_path ( "" ) ;
135+ if !fs_cache. contains_key ( & base_url) {
136+ let fs = resolve_filesystem ( & base_url, ctx) ?;
137+ fs_cache. insert ( base_url, fs) ;
138+ }
139+ }
80140
81- let fs = resolve_filesystem ( & base_url, ctx) ?;
141+ RUNTIME . block_on ( async {
142+ let mut builder = MultiFileDataSource :: new ( SESSION . clone ( ) ) ;
82143
83- RUNTIME . block_on ( async {
84- let builder = MultiFileDataSource :: new ( SESSION . clone ( ) )
85- . with_filesystem ( fs)
86- . with_glob ( glob_url. path ( ) ) ;
87- let ds = builder. build ( ) . await ?;
88- VortexResult :: Ok ( Arc :: new ( ds) as DataSourceRef )
89- } )
90- }
144+ for glob_url in & glob_urls {
145+ let mut base_url = glob_url. clone ( ) ;
146+ base_url. set_path ( "" ) ;
147+ let fs = fs_cache
148+ . get ( & base_url)
149+ . map ( Arc :: clone)
150+ . unwrap_or_else ( || unreachable ! ( "fs should be cached for all base URLs" ) ) ;
151+ builder = builder. with_glob ( glob_url. path ( ) , Some ( fs) ) ;
152+ }
153+
154+ let ds = builder. build ( ) . await ?;
155+ Ok ( Arc :: new ( ds) as DataSourceRef )
156+ } )
91157}
92158
93159#[ cfg( test) ]
0 commit comments