mod session;
mod uri;
use std::sync::Arc;
use async_trait::async_trait;
use futures::StreamExt;
use futures::TryStreamExt;
use futures::stream;
pub use session::MultiFileSession;
use session::MultiFileSessionExt;
use tracing::debug;
pub use uri::parse_uri_or_path;
use vortex_error::VortexError;
use vortex_error::VortexResult;
use vortex_error::vortex_bail;
use vortex_io::VortexReadAt;
use vortex_io::filesystem::FileListing;
use vortex_io::filesystem::FileSystemRef;
use vortex_layout::LayoutReaderRef;
use vortex_layout::scan::multi::LayoutReaderFactory;
use vortex_layout::scan::multi::MultiLayoutDataSource;
use vortex_scan::DataSource;
use vortex_session::VortexSession;
use crate::OpenOptionsSessionExt;
use crate::VortexFile;
use crate::VortexOpenOptions;
pub struct MultiFileDataSource {
session: VortexSession,
glob_sources: Vec<(String, Option<FileSystemRef>)>,
open_options_fn: Arc<dyn Fn(VortexOpenOptions) -> VortexOpenOptions + Send + Sync>,
}
const GLOB_RESOLUTION_CONCURRENCY: usize = 16;
impl MultiFileDataSource {
pub fn new(session: VortexSession) -> Self {
Self {
session,
glob_sources: Vec::new(),
open_options_fn: Arc::new(|opts| opts),
}
}
pub fn with_glob(mut self, glob: impl Into<String>, fs: Option<FileSystemRef>) -> Self {
let glob = glob.into();
let glob = if fs.is_none() && std::path::Path::new(&glob).is_relative() {
std::env::current_dir()
.map(|cwd| cwd.join(&glob).to_string_lossy().into_owned())
.unwrap_or(glob)
.trim_start_matches('/')
.to_string()
} else {
glob.trim_start_matches('/').to_string()
};
self.glob_sources.push((glob, fs));
self
}
pub fn with_open_options(
mut self,
f: impl Fn(VortexOpenOptions) -> VortexOpenOptions + Send + Sync + 'static,
) -> Self {
self.open_options_fn = Arc::new(f);
self
}
pub async fn build(self) -> VortexResult<MultiLayoutDataSource> {
if self.glob_sources.is_empty() {
vortex_bail!("MultiFileDataSource requires at least one glob pattern");
}
let local_fs: Option<FileSystemRef> = self
.glob_sources
.iter()
.any(|(_, fs)| fs.is_none())
.then(|| create_local_filesystem(&self.session))
.transpose()?;
let globs: Vec<String> = self.glob_sources.iter().map(|(g, _)| g.clone()).collect();
let resolved: Vec<Vec<(FileListing, FileSystemRef)>> =
stream::iter(self.glob_sources.into_iter().map(|(glob, maybe_fs)| {
let fs = maybe_fs
.or_else(|| local_fs.as_ref().map(Arc::clone))
.unwrap_or_else(|| {
unreachable!("local_fs is set when any glob lacks a filesystem")
});
async move {
let files: Vec<FileListing> = fs.glob(&glob)?.try_collect().await?;
Ok::<_, VortexError>(
files
.into_iter()
.map(|file| (file, Arc::clone(&fs)))
.collect(),
)
}
}))
.buffered(GLOB_RESOLUTION_CONCURRENCY)
.try_collect()
.await?;
let all_files: Vec<(FileListing, FileSystemRef)> = resolved.into_iter().flatten().collect();
if all_files.is_empty() {
vortex_bail!("No files matched the glob pattern(s): {:?}", globs);
}
let file_count = all_files.len();
debug!(file_count, glob = ?globs, "discovered files");
let (first_file_listing, first_fs) = &all_files[0];
let open_fn = self.open_options_fn.as_ref();
let first_file = open_file(first_fs, first_file_listing, &self.session, open_fn).await?;
let first_reader = first_file.layout_reader()?;
let byte_sizes: Vec<Option<u64>> = all_files.iter().map(|(file, _)| file.size).collect();
let factories: Vec<Arc<dyn LayoutReaderFactory>> = all_files[1..]
.iter()
.map(|(file, fs)| {
Arc::new(VortexFileReaderFactory {
fs: Arc::clone(fs),
file: file.clone(),
session: self.session.clone(),
open_options_fn: Arc::clone(&self.open_options_fn),
}) as Arc<dyn LayoutReaderFactory>
})
.collect();
let inner = MultiLayoutDataSource::new_with_first(
first_reader,
factories,
byte_sizes,
&self.session,
);
debug!(file_count, dtype = %inner.dtype(), "built MultiFileDataSource");
Ok(inner)
}
}
#[cfg(feature = "object_store")]
fn create_local_filesystem(session: &VortexSession) -> VortexResult<FileSystemRef> {
use vortex_io::object_store::ObjectStoreFileSystem;
use vortex_io::session::RuntimeSessionExt;
let store = Arc::new(object_store::local::LocalFileSystem::default());
let fs: FileSystemRef = Arc::new(ObjectStoreFileSystem::new(store, session.handle()));
Ok(fs)
}
#[cfg(not(feature = "object_store"))]
fn create_local_filesystem(_session: &VortexSession) -> VortexResult<FileSystemRef> {
vortex_bail!(
"The 'object_store' feature is required for automatic local filesystem creation. \
Either enable the feature or provide a filesystem via .with_filesystem()."
);
}
async fn open_file(
fs: &FileSystemRef,
file: &FileListing,
session: &VortexSession,
open_options_fn: &(dyn Fn(VortexOpenOptions) -> VortexOpenOptions + Send + Sync),
) -> VortexResult<VortexFile> {
tracing::trace!(path = %file.path, "opening vortex file");
let source = fs.open_read(&file.path).await?;
let key = source.uri().is_none().then_some(file.path.as_str());
open_cached(session, key, source, file.size, open_options_fn).await
}
pub async fn open_cached(
session: &VortexSession,
mut key: Option<&str>,
source: Arc<dyn VortexReadAt>,
file_size: Option<u64>,
open_options_fn: &(dyn Fn(VortexOpenOptions) -> VortexOpenOptions + Send + Sync),
) -> VortexResult<VortexFile> {
let uri = source.uri().cloned();
if key.is_none() {
key = uri.as_deref();
}
let Some(key) = key else {
vortex_bail!("Missing cache key");
};
let mut options = open_options_fn(session.open_options());
if let Some(size) = file_size {
options = options.with_file_size(size);
}
{
if let Some(footer) = session.multi_file().get_footer(key) {
options = options.with_footer(footer);
}
}
let file = options.open(source).await?;
session.multi_file().put_footer(key, file.footer().clone());
Ok(file)
}
struct VortexFileReaderFactory {
fs: FileSystemRef,
file: FileListing,
session: VortexSession,
open_options_fn: Arc<dyn Fn(VortexOpenOptions) -> VortexOpenOptions + Send + Sync>,
}
#[async_trait]
impl LayoutReaderFactory for VortexFileReaderFactory {
async fn open(&self) -> VortexResult<Option<LayoutReaderRef>> {
let file = open_file(
&self.fs,
&self.file,
&self.session,
self.open_options_fn.as_ref(),
)
.await?;
Ok(Some(file.layout_reader()?))
}
}
#[cfg(test)]
mod tests {
use std::fmt;
use vortex_array::IntoArray;
use vortex_array::array_session;
use vortex_buffer::ByteBuffer;
use vortex_buffer::ByteBufferMut;
use vortex_buffer::buffer;
use vortex_io::VortexReadAt;
use vortex_io::filesystem::FileSystem;
use vortex_io::session::RuntimeSession;
use vortex_layout::session::LayoutSession;
use super::*;
use crate::WriteOptionsSessionExt;
struct MetadataFileSystem {
bytes: ByteBuffer,
}
impl fmt::Debug for MetadataFileSystem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MetadataFileSystem").finish()
}
}
#[async_trait]
impl FileSystem for MetadataFileSystem {
fn list(&self, _prefix: &str) -> stream::BoxStream<'_, VortexResult<FileListing>> {
stream::empty().boxed()
}
async fn head(&self, path: &str) -> VortexResult<Option<FileListing>> {
Ok((path == "metadata.vortex").then_some(FileListing {
path: path.to_string(),
size: Some(self.bytes.len() as u64),
}))
}
async fn open_read(&self, _path: &str) -> VortexResult<Arc<dyn VortexReadAt>> {
Ok(Arc::new(self.bytes.clone()))
}
async fn delete(&self, _path: &str) -> VortexResult<()> {
Ok(())
}
}
async fn assert_cached_footer_metadata_order(default_first: bool) -> VortexResult<()> {
let session = array_session()
.with::<LayoutSession>()
.with::<RuntimeSession>()
.with::<MultiFileSession>();
crate::register_default_encodings(&session);
crate::enable_all_registered_array_encodings(&session);
let expected = ByteBuffer::copy_from(b"cached metadata");
let mut output = ByteBufferMut::empty();
session
.write_options()
.with_metadata_segment("key", expected.clone())
.write(&mut output, buffer![1u32].into_array().to_array_stream())
.await?;
let fs: FileSystemRef = Arc::new(MetadataFileSystem {
bytes: ByteBuffer::from(output),
});
let listing = FileListing {
path: "metadata.vortex".to_string(),
size: None,
};
let default_options = |options: VortexOpenOptions| options;
let metadata_options = |options: VortexOpenOptions| options.include_metadata();
if default_first {
let default = open_file(&fs, &listing, &session, &default_options).await?;
assert!(default.metadata_segment("key").is_none());
let included = open_file(&fs, &listing, &session, &metadata_options).await?;
assert_eq!(
included.metadata_segment("key").map(ByteBuffer::as_slice),
Some(expected.as_slice())
);
} else {
let included = open_file(&fs, &listing, &session, &metadata_options).await?;
assert_eq!(
included.metadata_segment("key").map(ByteBuffer::as_slice),
Some(expected.as_slice())
);
let default = open_file(&fs, &listing, &session, &default_options).await?;
assert!(default.metadata_segment("key").is_none());
}
Ok(())
}
#[tokio::test]
async fn test_cached_footer_default_then_metadata_is_order_independent() -> VortexResult<()> {
assert_cached_footer_metadata_order(true).await
}
#[tokio::test]
async fn test_cached_footer_metadata_then_default_is_order_independent() -> VortexResult<()> {
assert_cached_footer_metadata_order(false).await
}
struct TwoFileSystem {
a: ByteBuffer,
b: ByteBuffer,
}
impl fmt::Debug for TwoFileSystem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TwoFileSystem").finish()
}
}
#[async_trait]
impl FileSystem for TwoFileSystem {
fn list(&self, _prefix: &str) -> stream::BoxStream<'_, VortexResult<FileListing>> {
stream::empty().boxed()
}
async fn head(&self, path: &str) -> VortexResult<Option<FileListing>> {
let size = match path {
"a.vortex" => self.a.len(),
"b.vortex" => self.b.len(),
_ => return Ok(None),
};
Ok(Some(FileListing {
path: path.to_string(),
size: Some(size as u64),
}))
}
async fn open_read(&self, path: &str) -> VortexResult<Arc<dyn VortexReadAt>> {
let bytes = match path {
"b.vortex" => self.b.clone(),
_ => self.a.clone(),
};
Ok(Arc::new(bytes))
}
async fn delete(&self, _path: &str) -> VortexResult<()> {
Ok(())
}
}
#[tokio::test]
async fn test_multi_file_metadata_isolated_per_uri() -> VortexResult<()> {
let session = array_session()
.with::<LayoutSession>()
.with::<RuntimeSession>()
.with::<MultiFileSession>();
crate::register_default_encodings(&session);
crate::enable_all_registered_array_encodings(&session);
let mut out_a = ByteBufferMut::empty();
session
.write_options()
.with_metadata_segment("k", ByteBuffer::copy_from(b"file-a"))
.write(&mut out_a, buffer![1u32].into_array().to_array_stream())
.await?;
let mut out_b = ByteBufferMut::empty();
session
.write_options()
.with_metadata_segment("k", ByteBuffer::copy_from(b"file-b"))
.write(&mut out_b, buffer![2u32].into_array().to_array_stream())
.await?;
let fs: FileSystemRef = Arc::new(TwoFileSystem {
a: ByteBuffer::from(out_a),
b: ByteBuffer::from(out_b),
});
let include = |options: VortexOpenOptions| options.include_metadata();
let a_listing = FileListing {
path: "a.vortex".to_string(),
size: None,
};
let b_listing = FileListing {
path: "b.vortex".to_string(),
size: None,
};
let fa = open_file(&fs, &a_listing, &session, &include).await?;
let fb = open_file(&fs, &b_listing, &session, &include).await?;
assert_eq!(
fa.metadata_segment("k").map(ByteBuffer::as_slice),
Some(b"file-a".as_slice())
);
assert_eq!(
fb.metadata_segment("k").map(ByteBuffer::as_slice),
Some(b"file-b".as_slice())
);
Ok(())
}
}