use std::sync::{Arc, OnceLock, Weak};
use async_trait::async_trait;
use datafusion::catalog::{TableProvider, UrlTableFactory};
use datafusion::datasource::listing::{ListingTable, ListingTableConfig, ListingTableConfigExt};
use datafusion::error::{DataFusionError, Result};
use datafusion::execution::context::SessionState;
use parking_lot::RwLock;
use crate::error::{matched, user_message};
use crate::paths::{looks_like_location, table_url};
use crate::stores::ensure_object_store;
#[derive(Debug, Default)]
pub(crate) struct FileTableFactory {
session: OnceLock<Weak<RwLock<SessionState>>>,
}
impl FileTableFactory {
pub(crate) fn attach(&self, session: Weak<RwLock<SessionState>>) {
let _ = self.session.set(session);
}
}
#[async_trait]
impl UrlTableFactory for FileTableFactory {
async fn try_new(&self, name: &str) -> Result<Option<Arc<dyn TableProvider>>> {
if !looks_like_location(name) {
return Ok(None);
}
let state = self
.session
.get()
.and_then(Weak::upgrade)
.map(|state| state.read().clone())
.ok_or_else(|| DataFusionError::Internal("the session has been dropped".into()))?;
open_file_table(&state, name).await.map(Some)
}
}
pub async fn open_file_table(
state: &SessionState,
location: &str,
) -> Result<Arc<dyn TableProvider>> {
let url = table_url(location)?;
ensure_object_store(state.runtime_env(), url.get_url()).await?;
let config = ListingTableConfig::new(url)
.infer_options(state)
.await
.map_err(|e| explain(location, &e))?
.infer_partitions_from_path(state)
.await
.map_err(|e| explain(location, &e))?
.infer_schema(state)
.await
.map_err(|e| explain(location, &e))?;
Ok(Arc::new(ListingTable::try_new(config)?))
}
fn explain(location: &str, err: &DataFusionError) -> DataFusionError {
let message = user_message(err);
if message.contains(matched::NO_FILES_FOR_TABLE) {
DataFusionError::Plan(format!("no file found at '{location}'"))
} else if message.contains(matched::NO_FILE_FORMAT_FOR_EXTENSION) {
DataFusionError::Plan(format!(
"cannot tell the format of '{location}' from its extension; Ursa reads .parquet, \
.csv, .json and .arrow files, and compressed CSV and JSON (.gz, .bz2, .xz, .zst)"
))
} else {
DataFusionError::Plan(format!("cannot read '{location}': {message}"))
}
}