1use std::collections::HashMap;
19use std::sync::{Arc, OnceLock, Weak};
20
21use async_trait::async_trait;
22use datafusion::catalog::Session;
23use datafusion::catalog::{TableProvider, UrlTableFactory};
24use datafusion::common::parsers::CompressionTypeVariant;
25use datafusion::common::GetExt;
26use datafusion::datasource::file_format::json::JsonFormatFactory;
27use datafusion::datasource::file_format::{FileFormat as DataFusionFileFormat, FileFormatFactory};
28use datafusion::datasource::listing::{ListingOptions, ListingTable, ListingTableConfig};
29use datafusion::error::{DataFusionError, Result};
30use datafusion::execution::context::SessionState;
31use futures::StreamExt;
32use parking_lot::RwLock;
33
34use crate::error::user_message;
35use crate::paths::{looks_like_location, table_url};
36use crate::stores::ensure_object_store;
37
38#[derive(Debug, Default)]
40pub(crate) struct FileTableFactory {
41 session: OnceLock<Weak<RwLock<SessionState>>>,
43}
44
45impl FileTableFactory {
46 pub(crate) fn attach(&self, session: Weak<RwLock<SessionState>>) {
47 let _ = self.session.set(session);
48 }
49}
50
51#[async_trait]
52impl UrlTableFactory for FileTableFactory {
53 async fn try_new(&self, name: &str) -> Result<Option<Arc<dyn TableProvider>>> {
54 if !looks_like_location(name) {
55 return Ok(None);
56 }
57 let state = self
58 .session
59 .get()
60 .and_then(Weak::upgrade)
61 .map(|state| state.read().clone())
62 .ok_or_else(|| DataFusionError::Internal("the session has been dropped".into()))?;
63 open_file_table(&state, name).await.map(Some)
64 }
65}
66
67pub async fn open_file_table(
74 state: &SessionState,
75 location: &str,
76) -> Result<Arc<dyn TableProvider>> {
77 open_files(state, &[location.to_string()], &FileOptions::default()).await
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum FileFormat {
83 Parquet,
84 Csv,
85 Json,
88 Arrow,
89}
90
91impl FileFormat {
92 pub fn from_extension(ext: &str) -> Option<Self> {
95 match ext.to_ascii_lowercase().as_str() {
96 "parquet" => Some(Self::Parquet),
97 "csv" => Some(Self::Csv),
98 "json" | "jsonl" | "ndjson" => Some(Self::Json),
99 "arrow" => Some(Self::Arrow),
100 _ => None,
101 }
102 }
103
104 fn factory(self) -> &'static str {
106 match self {
107 Self::Parquet => "parquet",
108 Self::Csv => "csv",
109 Self::Json => "json",
110 Self::Arrow => "arrow",
111 }
112 }
113
114 fn compressible(self) -> bool {
116 matches!(self, Self::Csv | Self::Json)
117 }
118}
119
120#[derive(Debug, Clone, Default, PartialEq, Eq)]
124pub struct FileOptions {
125 pub format: Option<FileFormat>,
127 pub compression: Option<CompressionTypeVariant>,
129 pub has_header: Option<bool>,
131 pub delimiter: Option<u8>,
133 pub quote: Option<u8>,
135 pub newline_delimited: Option<bool>,
137}
138
139pub async fn open_files(
144 state: &SessionState,
145 locations: &[String],
146 options: &FileOptions,
147) -> Result<Arc<dyn TableProvider>> {
148 let shown = locations.join("', '");
149 let mut urls = Vec::with_capacity(locations.len());
150 let mut first_file = None;
151 let mut extensions = Vec::with_capacity(locations.len());
153 for location in locations {
154 let url = table_url(location)?;
155 ensure_object_store(state.runtime_env(), url.get_url()).await?;
156 let store = state.runtime_env().object_store(&url)?;
157 let file = url
158 .list_all_files(state, store.as_ref(), "")
159 .await
160 .map_err(|e| explain(location, &e))?
161 .next()
162 .await
163 .transpose()
164 .map_err(|e| explain(location, &e))?
165 .ok_or_else(|| DataFusionError::Plan(format!("no file found at '{location}'")))?;
166 let (ext, suffix) = extensions_of(file.location.as_ref())?;
167 extensions.push(match (&ext, &suffix) {
171 (Some(ext), Some(suffix)) => format!(".{ext}.{suffix}"),
172 (Some(ext), None) => format!(".{ext}"),
173 (None, _) => String::new(),
174 });
175 first_file.get_or_insert((ext, suffix));
176 urls.push(url);
177 }
178 let (ext, suffix) =
179 first_file.ok_or_else(|| DataFusionError::Plan("no file to read".into()))?;
180 let named = ext.as_deref().and_then(FileFormat::from_extension);
181 let format = options.format.or(named).ok_or_else(|| {
182 DataFusionError::Plan(format!(
183 "cannot tell the format of '{shown}' from its extension; Ursa reads .parquet, \
184 .csv, .json, .jsonl, .ndjson and .arrow files, and compressed CSV and JSON (.gz, \
185 .bz2, .xz, .zst). read_parquet(), read_csv() and read_json() read a file \
186 whatever its extension"
187 ))
188 })?;
189 let mut format_options = HashMap::new();
190 let mut set = |key: &str, value: String| {
191 format_options.insert(format!("format.{key}"), value);
192 };
193 if format.compressible() {
194 let compression = match (options.compression, &suffix) {
195 (Some(c), _) => Some(c),
196 (None, Some(s)) => Some(s.parse::<CompressionTypeVariant>()?),
197 (None, None) => None,
198 };
199 if let Some(c) = compression {
200 set("compression", compression_name(c));
201 }
202 }
203 if format == FileFormat::Csv {
204 if let Some(header) = options.has_header {
205 set("has_header", header.to_string());
206 }
207 if let Some(delimiter) = options.delimiter {
208 set("delimiter", char::from(delimiter).to_string());
209 }
210 if let Some(quote) = options.quote {
211 set("quote", char::from(quote).to_string());
212 }
213 }
214 if format == FileFormat::Json {
215 if let Some(newline_delimited) = options.newline_delimited {
216 set("newline_delimited", newline_delimited.to_string());
217 }
218 }
219 let file_format = state
220 .get_file_format_factory(format.factory())
221 .ok_or_else(|| {
222 DataFusionError::Internal(format!("no {} file format factory", format.factory()))
223 })?
224 .create(state, &format_options)?;
225 let extension = if extensions.windows(2).all(|w| w[0] == w[1]) {
228 extensions.swap_remove(0)
229 } else {
230 String::new()
231 };
232 let config = ListingTableConfig::new_with_multi_paths(urls)
233 .with_listing_options(ListingOptions::new(file_format).with_file_extension(extension))
234 .infer_partitions_from_path(state)
235 .await
236 .map_err(|e| explain(&shown, &e))?
237 .infer_schema(state)
238 .await
239 .map_err(|e| explain(&shown, &e))?;
240 Ok(Arc::new(ListingTable::try_new(config)?))
241}
242
243#[derive(Debug)]
247pub(crate) struct JsonLines(pub(crate) &'static str);
248
249impl FileFormatFactory for JsonLines {
250 fn create(
251 &self,
252 state: &dyn Session,
253 format_options: &HashMap<String, String>,
254 ) -> Result<Arc<dyn DataFusionFileFormat>> {
255 JsonFormatFactory::new().create(state, format_options)
256 }
257
258 fn default(&self) -> Arc<dyn DataFusionFileFormat> {
259 JsonFormatFactory::new().default()
260 }
261}
262
263impl GetExt for JsonLines {
264 fn get_ext(&self) -> String {
265 self.0.to_string()
266 }
267}
268
269fn extensions_of(path: &str) -> Result<(Option<String>, Option<String>)> {
272 let (ext, suffix) = ListingTableConfig::infer_file_extension_and_compression_type(path)?;
273 let ext = (!ext.contains('/') && ext != path).then_some(ext);
275 Ok((ext, suffix))
276}
277
278fn compression_name(c: CompressionTypeVariant) -> String {
280 match c {
281 CompressionTypeVariant::UNCOMPRESSED => "uncompressed".to_string(),
282 other => other.to_string().to_lowercase(),
283 }
284}
285
286fn explain(location: &str, err: &DataFusionError) -> DataFusionError {
288 DataFusionError::Plan(format!("cannot read '{location}': {}", user_message(err)))
289}