1use crate::app::feedback::Confirm;
5use crate::app::jobs::{Answer, Job};
6use crate::app::modals::pivot_melt_modal::PivotMeltModal;
7use crate::app::modals::sort_filter_modal::SortFilterModal;
8use crate::cli::{CompressionFormat, FileFormat};
9#[cfg(feature = "cloud")]
10use crate::cloud::cloud_hive;
11use crate::loading::open_options::{OpenOptions, ReadReport, UnaskedDownload};
12use crate::loading::scan::Scan;
13use crate::table::{DataTableState, OpenFacts};
14#[cfg(feature = "cloud")]
15use crate::wait_on_runtime;
16use crate::{
17 App, AppEvent, UNSUPPORTED, cli, cloud::source, formats::dataset_files, home, home::catalog,
18 home::discover, loading,
19};
20use color_eyre::Result;
21#[cfg(feature = "cloud")]
22use polars::io::cloud::{AmazonS3ConfigKey, CloudOptions};
23use polars::prelude::{LazyFrame, Schema, col};
24#[cfg(feature = "cloud")]
25use polars::prelude::{PlRefPath, ScanArgsParquet};
26use std::path::{Path, PathBuf};
27use std::sync::{Arc, Mutex};
28
29#[derive(Default)]
31pub struct OpenedSource {
32 pub(crate) original_file_format: Option<crate::export::export_modal::ExportFormat>,
33 pub(crate) original_file_delimiter: Option<u8>,
34 pub(crate) opened: Option<(Vec<PathBuf>, OpenOptions)>,
36 pub(crate) opened_from_home: bool,
38 pub(crate) startup_view: Option<String>,
40 pub(crate) shape_remembered: Option<u64>,
43}
44
45impl OpenedSource {
46 pub(crate) fn reset_for_dataset(
51 &mut self,
52 opened: Option<(Vec<PathBuf>, OpenOptions)>,
53 from_home: bool,
54 format: Option<crate::export::export_modal::ExportFormat>,
55 delimiter: Option<u8>,
56 ) {
57 self.opened = opened;
58 self.opened_from_home |= from_home;
59 self.original_file_format = format;
60 self.original_file_delimiter = delimiter;
61 }
62}
63
64#[cfg(feature = "cloud")]
66enum Unnamed {
67 Missing,
69 Parquet,
70 Other {
72 size: u64,
73 },
74}
75
76#[cfg(feature = "cloud")]
79struct CloudTarget<'a> {
80 full: &'a str,
82 key: String,
84 pattern: Option<&'a globset::GlobMatcher>,
87}
88
89pub(crate) fn hoist_partition_columns(
93 lf: LazyFrame,
94 schema: &Schema,
95 partition_columns: &[String],
96 drifts: bool,
97) -> LazyFrame {
98 if partition_columns.is_empty() {
99 return lf;
100 }
101 let mut exprs: Vec<_> = partition_columns
102 .iter()
103 .map(|s| col(s.as_str()))
104 .chain(
105 schema
106 .iter_names()
107 .map(|s| s.to_string())
108 .filter(|c| !partition_columns.contains(c))
109 .map(|s| col(s.as_str())),
110 )
111 .collect();
112 if drifts {
113 exprs.push(col(crate::formats::schema_union::DRIFT_COLUMN));
114 }
115 lf.select(exprs)
116}
117
118impl App {
119 pub(crate) fn awaiting_dataset(&self) -> bool {
122 self.loading.awaiting_dataset()
123 }
124
125 pub(crate) fn load_shown(&self) -> Option<(&str, u16, Option<&Path>, u64)> {
127 self.loading.current().map(|load| {
128 let (phase, percent) = load.phase().label();
129 (phase, percent, load.path(), load.size())
130 })
131 }
132
133 pub(crate) fn loading_phase<'a>(&self, phase: &'a str) -> std::borrow::Cow<'a, str> {
136 match self.counting.footers_this_frame {
137 Some((read, total)) => std::borrow::Cow::Owned(format!(
138 "Reading footers: {} of {}",
139 crate::numfmt::group_chrome(read),
140 crate::numfmt::group_chrome(total)
141 )),
142 None => match self.counting.listed_this_frame {
144 Some(listed) => std::borrow::Cow::Owned(format!(
145 "Listing files: {}",
146 crate::numfmt::group_chrome(listed)
147 )),
148 None => std::borrow::Cow::Borrowed(phase),
149 },
150 }
151 }
152
153 pub fn set_loading_phase(&mut self, phase: impl Into<String>, progress_percent: u16) {
157 self.announce_open(false, phase.into(), progress_percent);
158 }
159
160 pub(crate) fn announce_open(&mut self, from_home: bool, phase: String, percent: u16) {
162 self.put_down_load_in_flight();
163 self.loading.announce(from_home, phase, percent);
164 }
165
166 pub(crate) fn name_what_is_loading(&mut self, path: PathBuf) {
168 self.loading.name(path);
169 }
170
171 pub(crate) fn put_down_load_in_flight(&mut self) {
174 if let Some(retired) = self.loading.make_way() {
175 self.put_down_load(retired);
176 }
177 }
178
179 pub(crate) fn begin_new_dataset(&mut self) {
182 self.put_down_load_in_flight();
183 self.home_app.previews.drop_prepared();
185 self.reset_chart_state();
186 self.jobs.advance();
187 self.counting.stop_footer_pass();
190 }
191
192 pub(crate) fn put_down_load(&mut self, retired: loading::Retired) {
195 let id = retired.id;
196 let lines = self.jobs.quiet(|job| job.load() == Some(id));
197 self.jobs.supersede(|job| job.load() == Some(id));
198 if self
199 .status_message
200 .as_ref()
201 .is_some_and(|status| lines.contains(status))
202 {
203 self.status_message = None;
204 }
205 if retired.asking {
206 self.confirmation_modal.hide();
207 }
208 }
209
210 pub(crate) fn run_load_step(&mut self, step: loading::Step) -> Option<AppEvent> {
212 use loading::Step;
213 let load = self.loading.id();
214 match step {
215 Step::Nothing => None,
216 Step::Crash(message) => Some(AppEvent::Crash(message)),
217 Step::Failed(failed) => {
218 self.load_failed(failed);
219 None
220 }
221 Step::Tables(tables) => {
222 self.land_on_tables(tables);
223 None
224 }
225 Step::Hex(hex) => {
226 self.land_on_hex(hex);
227 None
228 }
229 Step::Install(loaded) => {
230 if self.install_dataset(*loaded) {
232 return None;
233 }
234 #[cfg(test)]
235 {
236 self.counting.first_rows_asked += 1;
237 }
238 if !self.spawn_async_collect(Self::LOADING_BUFFER) {
239 if self.status_message.as_deref() == Some(Self::LOADING_BUFFER) {
241 self.status_message = None;
242 }
243 self.first_rows_settled();
244 }
245 None
246 }
247 #[cfg(any(feature = "http", feature = "cloud"))]
248 Step::Ask(pending) => {
249 self.confirmation_modal.show(
252 Self::download_confirmation_message(&pending, self.loading.download_note()),
253 Confirm::Download,
254 );
255 None
256 }
257 Step::AskRead(read) => {
258 self.loading.hold_while_asking(self.jobs.hold());
260 self.confirmation_modal.show(
261 Self::in_memory_confirmation_message(&read),
262 Confirm::Download,
263 );
264 None
265 }
266 step => {
267 let load = load.expect("a step that runs work belongs to the open in flight");
268 self.spawn_load_phase(load, step);
269 None
270 }
271 }
272 }
273
274 pub(crate) fn remember_a_downloads_shape(&mut self) {
278 if self.source.shape_remembered == Some(self.dataset_generation) {
279 return;
280 }
281 let Some(url) = self.path.clone().filter(|p| source::is_remote_url(p)) else {
282 return;
283 };
284 let Some(state) = self.data_table_state.as_ref().filter(|s| s.fetched()) else {
285 return;
286 };
287 let Some(rows) = state.num_rows_if_valid().filter(|_| !state.changes_rows()) else {
288 return;
289 };
290 self.source.shape_remembered = Some(self.dataset_generation);
291 let columns: Vec<String> = state
292 .source_schema()
293 .iter_names()
294 .map(|name| name.to_string())
295 .collect();
296 let facts = crate::cache::DatasetFacts {
297 mtime: std::time::SystemTime::now()
298 .duration_since(std::time::UNIX_EPOCH)
299 .map(|d| d.as_secs())
300 .unwrap_or_default(),
301 size: 0,
302 rows: Some(rows),
303 cols: Some(columns.len()),
304 cols_sampled: false,
305 columns,
306 kind: Some(discover::EntryKind::File),
307 classified_by: discover::CLASSIFIER_VERSION,
308 cost: Default::default(),
309 holds: Default::default(),
310 };
311 let cache = self.cache.clone();
313 self.cache_writes
314 .spawn(move || cache.record_dataset_facts(&[(url, facts)]));
315 }
316
317 pub(crate) fn install_dataset(&mut self, loaded: loading::Loaded) -> bool {
321 let loading::Loaded {
322 state,
323 path,
324 options,
325 debug_label,
326 paths,
327 recent,
328 from_home,
329 footers,
330 } = loaded;
331 let options = &options;
332 self.dataset_generation = self.dataset_generation.wrapping_add(1);
335 self.counting.reset_for_dataset(footers);
337 self.quality.reset_for_dataset();
338 self.analysis_modal.quality.reset_for_dataset();
339 self.prompt.reset_for_dataset();
340 self.sample.reset_for_dataset();
341 self.views.reset_for_dataset();
342 self.info
343 .reset_for_dataset(&self.app_config, path.as_deref());
344 self.sort_filter_modal = SortFilterModal::new();
345 self.pivot_melt_modal = PivotMeltModal::new();
346 self.jobs.supersede(|job| matches!(job, Job::ViewPivot(_)));
349 self.put_down_sample_draw();
350 self.reset_chart_state();
351 self.debug.schema_load = debug_label;
352 let opened = paths.map(|paths| {
355 let options = OpenOptions {
356 format_read: None,
357 sqlite: None,
358 tail: None,
360 prepared: None,
361 place: None,
362 ..options.clone()
363 };
364 (paths, options)
365 });
366 let (format, delimiter) = match path.as_deref() {
367 Some(p) => (
368 Self::export_format_for(p, state.read_as().or(options.format)),
369 Some(options.separator_or(b',')),
372 ),
373 None => (None, None),
374 };
375 self.source
376 .reset_for_dataset(opened, from_home, format, delimiter);
377 if let Some(path) = recent {
379 let cache = self.cache.clone();
382 self.cache_writes.spawn(move || {
383 cache.push_recent(&path);
384 });
385 }
386 self.forget_the_rows_read();
387 if let Some(closed) = self.data_table_state.replace(state) {
389 crate::app::background::release(closed);
390 }
391 if options.follow
393 && let Some(state) = self.data_table_state.as_mut()
394 {
395 match options.tail.as_deref() {
396 Some(tail) => {
397 let follow = crate::loading::follow::Follow::start(
398 tail.clone(),
399 self.app_config.read.follow_interval.duration(),
400 self.events.clone(),
401 options.spool.clone(),
402 );
403 state.start_following(if options.pipe {
404 follow.as_pipe()
405 } else {
406 follow
407 });
408 state.follow_to(tail.rows(), false);
410 }
411 None => self.flash_note(
413 "Only text and Arrow streams are followed: this shows what had arrived, and recording goes on"
414 .to_string(),
415 ),
416 }
417 }
418 self.retire_a_count_the_rows_answered();
420 self.path = path.clone();
421 self.open_info_documentation();
423 if self.overlay.shows(&crate::Overlay::Info) {
425 self.read_file_facts();
426 self.count_unfit();
427 }
428 self.start_pending_footers();
430 self.index_lines();
431 if options.row_numbers_auto
433 && let Some(state) = self.data_table_state.as_mut()
434 && state.numbered_by_default()
435 {
436 state.set_row_numbers(true);
437 }
438 self.status_message = Some(Self::LOADING_BUFFER.to_string());
439
440 if let Some(place) = options.place.as_deref() {
442 let restored = self.restore_place(
443 place.settings.clone(),
444 place.active.clone(),
445 place.drill.clone(),
446 );
447 return match restored {
448 Ok(()) => true,
449 Err(e) => {
450 let applying = crate::view::view_apply::Applying::Restored(None);
451 self.view_failed(&applying, &e.to_string());
452 false
453 }
454 };
455 }
456 let (view, reason) = match self.source.startup_view.take() {
460 Some(name) => match self.views.manager.get_view_by_name(&name).cloned() {
461 Some(view) => (Some(view), None),
462 None => {
463 self.error_modal.show(format!("No view named \"{name}\""));
464 (None, None)
465 }
466 },
467 None if self.app_config.views.auto_apply => self
468 .view_dataset()
469 .zip(self.data_table_state.as_ref())
470 .and_then(|(dataset, state)| {
471 self.views
472 .manager
473 .get_most_relevant(dataset, state.source_schema())
474 })
475 .map_or((None, None), |(view, reason)| (Some(view), Some(reason))),
476 None => (None, None),
477 };
478 let Some(view) = view else {
479 return false;
480 };
481 let applied = match reason {
482 Some(why) => self.apply_matched_view(&view, why),
484 None => self.apply_view(&view),
485 };
486 match applied {
487 Ok(()) => true,
489 Err(e) => {
490 self.error_modal
491 .show(format!("Error applying view \"{}\": {e}", view.name));
492 false
493 }
494 }
495 }
496
497 pub(crate) fn reopen_in_place(
504 &mut self,
505 asked: Option<Box<loading::open_options::KeptPlace>>,
506 ) -> Option<AppEvent> {
507 let (paths, options) = self.source.opened.clone()?;
508 if self.query_prompt_mode().is_some() {
509 self.close_query_prompt();
510 }
511 self.close_overlays();
513 let place = asked.map(|asked| *asked).or_else(|| self.place_on_screen());
514 let options = OpenOptions {
515 view: None,
516 prepared: None,
517 place: place.map(Arc::new),
518 ..options
519 };
520 self.set_loading_phase("Scanning input", 10);
521 self.name_what_is_loading(paths[0].clone());
522 Some(AppEvent::Open(paths, options))
523 }
524
525 pub(crate) fn place_on_screen(&self) -> Option<loading::open_options::KeptPlace> {
528 let state = self.data_table_state.as_ref()?;
529 let mut settings = crate::view_settings_of(state);
530 settings.chart = self.saved_chart();
531 if let Some((order, locked)) = state.grouped_column_order() {
533 settings.column_order = order.to_vec();
534 settings.locked_columns_count = locked;
535 }
536 Some(loading::open_options::KeptPlace {
537 settings,
538 active: self.views.active_id.clone(),
539 drill: state.drill_place().map(Box::new),
540 })
541 }
542
543 pub fn abandon_load(&mut self) {
549 let retired = self.loading.retire();
550 if let Some(retired) = retired {
551 self.put_down_load(retired);
552 }
553 self.reset_chart_state();
556 if self.jobs.supersede(|job| matches!(job, Job::Classify(_))) {
558 self.home.status = None;
559 }
560 self.jobs.take_owed(Self::owed_rows);
563 if retired.is_some() {
566 self.busy = false;
567 let quieted = self.jobs.quiet(Self::reading_rows);
568 if self
569 .status_message
570 .as_ref()
571 .is_some_and(|status| quieted.contains(status))
572 {
573 self.status_message = None;
574 }
575 }
576 self.screen_generation = self.screen_generation.wrapping_add(1);
579 }
580
581 pub(crate) fn open_what_it_is(
584 &mut self,
585 path: PathBuf,
586 kind: discover::EntryKind,
587 jump: bool,
588 ) -> Option<AppEvent> {
589 let go_inside = |app: &mut Self, path: PathBuf| {
590 if jump {
591 app.home_jump_into(path);
592 } else {
593 app.home_browse_into(path);
594 }
595 };
596 if kind == discover::EntryKind::Directory {
597 go_inside(self, path);
598 return None;
599 }
600 if kind == discover::EntryKind::Other {
603 if matches!(source::input_source(&path), source::InputSource::Local(_)) {
604 self.open_hex(path, crate::app::hex_view::Origin::Home, true, None);
605 }
606 return None;
607 }
608 if let Some(format) = kind.lake_name() {
611 self.home.lake_here = Some((path.clone(), format));
612 go_inside(self, path);
613 return None;
614 }
615 let directory = matches!(
616 kind,
617 discover::EntryKind::Hive | discover::EntryKind::MultiFile
618 );
619 if directory && jump {
621 go_inside(self, path);
622 return None;
623 }
624 if directory && home::is_object_store_url(&path) {
627 return Some(self.home_open_path(home::directory_dataset_url(&path), false));
628 }
629 let a_spec_may_read = !self.formats.by_glob(&path, false).is_empty()
633 || self.formats.specs.iter().any(|f| !f.spec.magic.is_empty());
634 if kind == discover::EntryKind::File
637 && discover::unreadable_by_name(&path)
638 && !a_spec_may_read
639 && crate::formats::members::split(&path).is_none()
640 && crate::formats::members::holder(&path).is_none()
641 && crate::formats::members::split_variant(&path, &self.formats).is_none()
642 && crate::formats::hf_splits::split_place(&path).is_none()
643 {
644 self.home.status = Some(discover::NO_READER.to_string());
645 return None;
646 }
647 let prepared = (!directory)
650 .then(|| self.home_app.previews.take_prepared(&path))
651 .flatten();
652 let unasked = self
655 .home
656 .catalogs
657 .iter()
658 .filter(|c| c.origin == catalog::Origin::Bundled)
659 .flat_map(|c| c.datasets.iter())
660 .find(|d| d.location == path)
661 .filter(|_| {
662 !jump && matches!(source::input_source(&path), source::InputSource::Http(_))
663 })
664 .map(|dataset| UnaskedDownload {
665 limit: UnaskedDownload::LIMIT,
666 listed: dataset.size,
667 });
668 match self.home_open_path(path, directory) {
669 AppEvent::Open(paths, mut options) => {
670 options.prepared = prepared.map(|p| Arc::new(Mutex::new(Some(p))));
671 options.download_unasked = unasked;
672 Some(AppEvent::Open(paths, options))
673 }
674 event => Some(event),
675 }
676 }
677
678 pub fn route_named_paths(paths: Vec<PathBuf>, options: OpenOptions) -> AppEvent {
684 Self::route_named_paths_with(paths, options, &crate::formats::Registry::default())
685 }
686
687 pub fn route_named_paths_with(
690 paths: Vec<PathBuf>,
691 options: OpenOptions,
692 formats: &crate::formats::Registry,
693 ) -> AppEvent {
694 if let Some(event) = Self::route_named_without_looking(&paths, &options) {
695 return event;
696 }
697 let single = (paths.len() == 1 && !options.hive).then(|| paths[0].clone());
700 let Some(dir) = single.filter(|p| p.is_dir()) else {
701 return AppEvent::Open(paths, options);
702 };
703 if options.spec_file.is_some()
705 || options.spec_name.is_some()
706 || !formats.by_glob(&dir, true).is_empty()
707 {
708 return AppEvent::Open(paths, options);
709 }
710 AppEvent::LookThenOpenDirectory(dir, options)
713 }
714
715 pub(crate) fn route_named_without_looking(
719 paths: &[PathBuf],
720 options: &OpenOptions,
721 ) -> Option<AppEvent> {
722 #[cfg(feature = "cloud")]
723 if let [dir] = paths
724 && !options.hive
725 && home::is_object_store_url(dir)
726 && options.format.is_none()
727 && !dir.to_string_lossy().contains('*')
728 && !home::names_a_file(dir)
729 {
730 return Some(AppEvent::LookThenOpenDirectory(
731 dir.clone(),
732 options.clone(),
733 ));
734 }
735 let _ = (paths, options);
736 None
737 }
738
739 pub fn missing_named_path(
742 paths: &[PathBuf],
743 formats: &crate::formats::Registry,
744 ) -> Option<PathBuf> {
745 paths
746 .iter()
747 .find(|path| {
748 !source::is_remote_url(path)
749 && !crate::loading::stdin::is_stdin(path)
750 && !source::expands_as_glob(path)
751 && !path.exists()
752 && crate::formats::members::split(path).is_none()
753 && crate::formats::members::split_variant(path, formats).is_none()
754 })
755 .cloned()
756 }
757
758 pub(crate) fn open_the_directory_looked_at(
761 &mut self,
762 dir: PathBuf,
763 kind: discover::EntryKind,
764 holds: Option<&discover::Holds>,
765 mut options: OpenOptions,
766 ) -> Option<AppEvent> {
767 #[cfg(feature = "cloud")]
768 if home::is_object_store_url(&dir) {
769 return self.open_the_cloud_directory_looked_at(dir, kind, holds, options);
770 }
771 let _ = holds;
772 if let Some(format) = kind.lake_name() {
776 self.rest_at_start();
777 self.enter_home();
778 self.home.lake_here = Some((dir.clone(), format));
779 self.home_jump_into(dir);
780 return None;
781 }
782 if matches!(
785 kind,
786 discover::EntryKind::Hive | discover::EntryKind::MultiFile
787 ) {
788 options.hive = true;
789 self.set_loading_phase("Scanning input", 10);
790 self.name_what_is_loading(dir.clone());
791 return Some(AppEvent::Open(vec![dir], options));
792 }
793 self.rest_at_start();
796 self.enter_home();
797 self.home_jump_into(dir);
798 None
799 }
800
801 #[cfg(feature = "cloud")]
804 fn open_the_cloud_directory_looked_at(
805 &mut self,
806 dir: PathBuf,
807 kind: discover::EntryKind,
808 holds: Option<&discover::Holds>,
809 options: OpenOptions,
810 ) -> Option<AppEvent> {
811 let open = |app: &mut Self, path: PathBuf, options: OpenOptions| {
812 app.set_loading_phase("Scanning input", 10);
813 app.name_what_is_loading(path.clone());
814 Some(AppEvent::Open(vec![path], options))
815 };
816 let Some(holds) = holds else {
818 return open(self, dir, options);
819 };
820 if let Some(format) = kind.lake_name() {
821 self.rest_at_start();
822 self.enter_home();
823 self.home.lake_here = Some((dir.clone(), format));
824 self.home_jump_into(dir);
825 return None;
826 }
827 let directory = home::directory_dataset_url(&dir);
828 if matches!(
829 kind,
830 discover::EntryKind::Hive | discover::EntryKind::MultiFile
831 ) {
832 let options = OpenOptions {
833 hive: true,
834 ..options
835 };
836 return open(self, directory, options);
837 }
838 if let Some((format, left_out)) = Self::cloud_prefix_format(holds) {
839 let options = OpenOptions {
840 hive: true,
841 format: Some(format),
842 left_out,
843 ..options
844 };
845 return open(self, directory, options);
846 }
847 self.rest_at_start();
849 self.enter_home();
850 self.home_jump_into(dir);
851 self.home.status = Self::why_a_cloud_prefix_cannot_be_read(holds);
852 None
853 }
854
855 pub(crate) fn open_defaults(&self) -> OpenOptions {
858 match crate::cli::parse_args(["datui"]) {
859 Ok(args) => OpenOptions::from_args_and_config(&args, &self.app_config),
860 Err(_) => OpenOptions::default(),
861 }
862 }
863
864 fn with_delimited_spec(
868 file: &Path,
869 mut options: OpenOptions,
870 formats: &crate::formats::Registry,
871 ) -> Result<OpenOptions> {
872 if options.delimited.is_some() {
873 return Ok(options);
874 }
875 let asked = crate::formats::Asked {
876 spec_file: options.spec_file.clone(),
877 spec: options.spec_fetched.clone(),
878 spec_name: options.spec_name.clone(),
879 compression: options.compression,
880 ..Default::default()
881 };
882 let crate::formats::Route::Delimited(choice) =
883 crate::formats::route(file, &asked, formats).map_err(|e| color_eyre::eyre::eyre!(e))?
884 else {
885 return Ok(options);
886 };
887 let Some(delimited) = choice.spec.delimited.clone() else {
888 return Ok(options);
889 };
890 delimited.apply(&mut options);
891 let chosen = crate::formats::delimited_spec::DelimitedRead::chosen(
892 choice.spec,
893 choice.by,
894 choice.also,
895 );
896 let read =
897 crate::formats::delimited_spec::read_facts(&chosen, &[file.to_path_buf()], &options)?;
898 options.delimited = Some(Arc::new(read));
899 Ok(options)
900 }
901
902 fn decompressed_delimited_state(
906 path: &Path,
907 options: &OpenOptions,
908 writer: &crate::loading::unfinished::Writer,
909 ) -> Result<DataTableState> {
910 let separator = options
911 .format
912 .and_then(FileFormat::separator)
913 .unwrap_or(b',');
914 DataTableState::from_read(
915 crate::formats::readers::csv::read_delimited(path, separator, options, writer)?,
916 options,
917 )
918 }
919
920 #[cfg(feature = "cloud")]
922 fn build_s3_cloud_options(settings: &crate::cloud::cloud_sources::S3Settings) -> CloudOptions {
923 let settings = settings.clone();
924 let virtual_hosted = (settings.endpoint.is_some() || settings.virtual_hosted.is_some())
925 .then(|| settings.virtual_hosted_style().to_string());
926 let configs: Vec<(AmazonS3ConfigKey, String)> = [
927 (AmazonS3ConfigKey::Endpoint, settings.endpoint),
928 (AmazonS3ConfigKey::AccessKeyId, settings.access_key_id),
929 (
930 AmazonS3ConfigKey::SecretAccessKey,
931 settings.secret_access_key,
932 ),
933 (AmazonS3ConfigKey::Token, settings.session_token),
934 (AmazonS3ConfigKey::Region, settings.region),
935 (AmazonS3ConfigKey::VirtualHostedStyleRequest, virtual_hosted),
936 (
937 AmazonS3ConfigKey::SkipSignature,
938 settings.skip_signature.then(|| "true".to_string()),
939 ),
940 ]
941 .into_iter()
942 .filter_map(|(key, value)| value.map(|v| (key, v)))
943 .chain([(
944 AmazonS3ConfigKey::Client(crate::cloud::user_agent::CLIENT_KEY),
945 crate::cloud::user_agent::get(),
946 )])
947 .collect();
948 CloudOptions::default().with_aws(configs)
949 }
950
951 #[cfg(feature = "cloud")]
954 pub(crate) fn cloud_bucket_and_key(url: &str) -> Result<(String, String)> {
955 if let Some((_, container, key)) = source::azure_parts(url) {
956 return Ok((container, key.trim_matches('/').to_string()));
957 }
958 crate::cloud::cloud_browse::split_bucket_url(url)
959 .map(|(_, bucket, key)| (bucket, key))
960 .ok_or_else(|| {
961 color_eyre::eyre::eyre!("URL must be s3://bucket/key or gs://bucket/key")
962 })
963 }
964
965 #[cfg(feature = "cloud")]
969 fn polars_object_store(
970 url: &str,
971 options: &CloudOptions,
972 runtime: &tokio::runtime::Handle,
973 ) -> Result<Arc<dyn object_store::ObjectStore>> {
974 let url = url.to_string();
975 let options = options.clone();
976 wait_on_runtime(runtime, async move {
977 let (_, store) = polars::io::cloud::build_object_store(
978 PlRefPath::new(url.as_str()),
979 Some(&options),
980 false,
981 )
982 .await?;
983 polars::prelude::PolarsResult::Ok(store.to_dyn_object_store().await.into_owned())
984 })
985 .ok_or_else(|| color_eyre::eyre::eyre!("cancelled"))?
986 .map_err(|e| color_eyre::eyre::eyre!("Object store config failed: {}", e))
987 }
988
989 #[cfg(feature = "http")]
993 fn http_agent(total: std::time::Duration) -> ureq::Agent {
994 crate::cloud::user_agent::ureq_config()
995 .timeout_global(Some(total))
996 .build()
997 .into()
998 }
999
1000 #[cfg(feature = "http")]
1003 pub(crate) fn fetch_remote_size_http(
1004 url: &str,
1005 ) -> std::result::Result<Option<u64>, crate::error_display::HttpGone> {
1006 let agent = Self::http_agent(std::time::Duration::from_secs(15));
1007 match agent.head(url).header("Accept-Encoding", "identity").call() {
1010 Ok(r) => Ok(r
1011 .headers()
1012 .get("Content-Length")
1013 .and_then(|v| v.to_str().ok())
1014 .and_then(|s| s.parse::<u64>().ok())),
1015 Err(e) => crate::error_display::http_gone(url, &e).map_or(Ok(None), Err),
1016 }
1017 }
1018
1019 #[cfg(feature = "cloud")]
1021 fn fetch_remote_size_cloud(
1022 url: &str,
1023 cloud: &crate::config::CloudConfig,
1024 runtime: &tokio::runtime::Handle,
1025 ) -> Result<Option<u64>> {
1026 use object_store::ObjectStoreExt;
1027
1028 let (_bucket, key) = Self::cloud_bucket_and_key(url)?;
1029 if key.is_empty() {
1030 return Ok(None);
1031 }
1032 let (_, _, store) = Self::cloud_store_for(Path::new(url), cloud, runtime)?;
1033 let path = crate::cloud::cloud_browse::object_path(&key);
1034 let head = wait_on_runtime(runtime, async move { store.head(&path).await });
1035 Ok(head.and_then(|r| r.ok()).map(|meta| meta.size))
1036 }
1037
1038 #[cfg(feature = "cloud")]
1041 fn sniff_cloud_object(
1042 url: &str,
1043 cloud: &crate::config::CloudConfig,
1044 runtime: &tokio::runtime::Handle,
1045 ) -> Result<Unnamed> {
1046 use object_store::ObjectStoreExt;
1047
1048 let (_bucket, key) = Self::cloud_bucket_and_key(url)?;
1049 let (_, _, store) = Self::cloud_store_for(Path::new(url), cloud, runtime)?;
1050 let path = crate::cloud::cloud_browse::object_path(&key);
1051 let sniffed = wait_on_runtime(runtime, async move {
1052 let size = match store.head(&path).await {
1053 Ok(meta) => meta.size,
1054 Err(object_store::Error::NotFound { .. }) => return Ok(Unnamed::Missing),
1055 Err(e) => return Err(e),
1056 };
1057 let end = size.min(crate::formats::readers::HEAD as u64);
1058 let head = if end == 0 {
1059 Default::default()
1060 } else {
1061 store.get_range(&path, 0..end).await?
1062 };
1063 let parquet = crate::formats::readers::sniff(
1064 &head,
1065 None,
1066 crate::formats::readers::Asked::Open { extension: false },
1067 |f| f == FileFormat::Parquet,
1068 )
1069 .is_some();
1070 Ok(if parquet {
1071 Unnamed::Parquet
1072 } else {
1073 Unnamed::Other { size }
1074 })
1075 });
1076 sniffed
1077 .ok_or_else(|| color_eyre::eyre::eyre!("cancelled"))?
1078 .map_err(|e| color_eyre::eyre::eyre!(crate::error_display::store_message(&e)))
1079 }
1080
1081 #[cfg(feature = "http")]
1086 fn download_http_to_temp(
1087 url: &str,
1088 temp_dir: Option<&Path>,
1089 extension: Option<&str>,
1090 limit: Option<u64>,
1091 writer: &crate::loading::unfinished::Writer,
1092 ) -> Result<crate::cloud::download::TempDownload> {
1093 use crate::cloud::download::StreamError;
1094
1095 let url = url.to_string();
1096 let open = move || {
1097 let agent = Self::http_agent(std::time::Duration::from_secs(300));
1098 let response = agent
1100 .get(&url)
1101 .call()
1102 .map_err(|e| crate::error_display::http_message(&url, &e))?;
1103 Ok((response.into_body().into_reader(), None))
1105 };
1106 crate::cloud::download::read_to_temp(temp_dir, extension, open, writer, limit).map_err(
1107 |error| match error {
1108 StreamError::Open(message) => color_eyre::eyre::eyre!(message),
1109 StreamError::Read(e) => {
1110 color_eyre::eyre::eyre!("Download failed partway. Check your connection: {e}")
1111 }
1112 StreamError::Short { expected, got } => color_eyre::eyre::eyre!(
1113 "Download failed partway: it ended after {got} of {expected} bytes."
1114 ),
1115 StreamError::Write(report) => report,
1116 StreamError::Cut => color_eyre::eyre::eyre!("Download was cancelled."),
1117 },
1118 )
1119 }
1120
1121 #[cfg(feature = "cloud")]
1125 fn download_cloud_to_temp(
1126 url: &str,
1127 cloud: &crate::config::CloudConfig,
1128 options: &OpenOptions,
1129 runtime: &tokio::runtime::Handle,
1130 writer: &crate::loading::unfinished::Writer,
1131 ) -> Result<crate::cloud::download::TempDownload> {
1132 use crate::cloud::download::StreamError;
1133 use object_store::ObjectStoreExt;
1134
1135 let (label, example) = match source::input_source(Path::new(url)) {
1136 source::InputSource::Gcs(_) => ("GCS", "gs://bucket/path/file.csv"),
1137 source::InputSource::Azure(_) => (
1138 "Azure",
1139 "abfss://container@account.dfs.core.windows.net/path/file.csv",
1140 ),
1141 _ => ("S3", "s3://bucket/path/file.csv"),
1142 };
1143 let ext = source::download_suffix(url);
1144 let (_bucket, key) = Self::cloud_bucket_and_key(url)?;
1145 if key.is_empty() {
1146 return Err(crate::error_display::FileError::new(
1147 Path::new(url),
1148 format!("a {label} URL names an object here, such as {example}"),
1149 )
1150 .into());
1151 }
1152 let (_, _, store) = Self::cloud_store_for(Path::new(url), cloud, runtime)?;
1153
1154 let path = crate::cloud::cloud_browse::object_path(&key);
1155 let open = async move {
1156 let got = store
1157 .get(&path)
1158 .await
1159 .map_err(|e| crate::error_display::store_message(&e))?;
1160 let len = got.range.end - got.range.start;
1161 Ok((got.into_stream(), Some(len)))
1162 };
1163 let failed = |what: String| -> color_eyre::Report {
1164 crate::error_display::FileError::new(Path::new(url), what).into()
1165 };
1166 crate::cloud::download::stream_to_temp(
1167 runtime,
1168 options.temp_dir.as_deref(),
1169 ext.as_deref(),
1170 open,
1171 writer,
1172 )
1173 .map_err(|error| match error {
1174 StreamError::Open(e) => failed(e),
1175 StreamError::Read(e) => failed(format!("the download stopped: {e}")),
1176 StreamError::Short { expected, got } => {
1177 failed(format!("it ended after {got} of {expected} bytes"))
1178 }
1179 StreamError::Write(report) => report,
1180 StreamError::Cut => failed("the download was cancelled".to_string()),
1181 })
1182 }
1183
1184 pub(crate) fn scan_for_open(
1188 cloud: &crate::config::CloudConfig,
1189 formats: &crate::formats::Registry,
1190 paths: &[PathBuf],
1191 options: OpenOptions,
1192 path: Option<PathBuf>,
1193 ) -> std::result::Result<loading::LoadAnswer, String> {
1194 use loading::LoadAnswer;
1195 let bytes_of = |files: &[PathBuf]| -> u64 {
1196 files
1197 .iter()
1198 .filter_map(|f| std::fs::metadata(f).ok())
1199 .map(|m| m.len())
1200 .sum()
1201 };
1202 let mut report = ReadReport {
1207 left_out: options.left_out.clone(),
1208 files_disagree: options.files_disagree,
1209 format: None,
1210 format_read: None,
1211 read_python: Vec::new(),
1212 sqlite: None,
1213 opened: None,
1214 splits: options.splits.clone(),
1215 delimited: None,
1216 table: None,
1217 guessed: false,
1218 read_notes: Vec::new(),
1219 typing: Default::default(),
1220 };
1221 let options = OpenOptions {
1224 ignore_errors: options.ignore_errors || options.follow,
1225 ..options
1226 };
1227 let named = |e: color_eyre::Report| {
1228 crate::error_display::user_message_from_report(&e, path.as_deref())
1229 };
1230 let followed_stream = options.follow
1233 && crate::loading::follow::followed_stream(
1234 &paths[0],
1235 Some(crate::loading::follow::format_of(&paths[0], options.format)),
1236 &options,
1237 );
1238 let scan = if followed_stream {
1239 crate::loading::follow::stream::scan(&paths[0])
1240 .map(Scan::from)
1241 .map_err(|e| color_eyre::eyre::eyre!(e))
1242 } else {
1243 Self::build_lazyframe_from_paths_with(cloud, paths, &options, &mut report, formats)
1244 }
1245 .map_err(named)?;
1247 let format = scan.format(report.format.or(options.format));
1248 let recording = options
1251 .spool
1252 .as_ref()
1253 .is_some_and(|handle| handle.spool().tee().is_some());
1254 let (scan, tail) = match scan {
1255 Scan::Frame(lf) if options.follow => {
1256 let format = crate::loading::follow::format_of(&paths[0], format);
1257 let refused = (!followed_stream)
1258 .then(|| crate::loading::follow::refusal(Some(format), &options))
1259 .flatten();
1260 match refused {
1261 Some(_) if recording => (Scan::Frame(lf), None),
1262 Some(refusal) => return Err(refusal),
1263 None => {
1264 let (lf, tail) = crate::loading::follow::bound_to_complete(
1265 *lf, &paths[0], format, &options,
1266 )
1267 .map_err(named)?;
1268 (Scan::Frame(Box::new(lf)), Some(Arc::new(tail)))
1269 }
1270 }
1271 }
1272 _ if options.follow && !recording => {
1273 return Err(crate::loading::follow::refusal(format, &options)
1274 .unwrap_or_else(|| "This file cannot be followed as it grows.".to_string()));
1275 }
1276 scan => (scan, None),
1277 };
1278 let read_mode = scan.read_mode(format, report.format_read.is_some(), &options);
1279 let mut options = OpenOptions {
1280 left_out: report.left_out,
1281 files_disagree: report.files_disagree,
1282 format,
1283 format_read: report.format_read,
1284 sqlite: report.sqlite,
1285 opened: report.opened,
1286 splits: report.splits,
1287 read_python: report.read_python,
1288 read_mode,
1289 tail,
1290 table: report.table.or_else(|| options.table.clone()),
1291 format_guessed: options.format_guessed || report.guessed,
1292 read_notes: report.read_notes,
1293 typing: report.typing,
1294 ..options
1295 };
1296 if let Some(read) = report.delimited {
1299 read.delimited().apply(&mut options);
1300 options.delimited = Some(read);
1301 }
1302 Ok(match scan {
1303 Scan::Frame(lf) => LoadAnswer::Scanned { lf, path, options },
1304 Scan::Decompress { file, .. } => LoadAnswer::Compressed {
1305 file,
1306 path,
1307 options,
1308 },
1309 Scan::Streams(files) => LoadAnswer::Convert {
1310 what: loading::Conversion::Streams,
1311 bytes: bytes_of(&files),
1312 files,
1313 path,
1314 options,
1315 },
1316 Scan::DecompressSpec { file, choice } => LoadAnswer::CompressedRecords {
1317 file,
1318 path,
1319 choice,
1320 options,
1321 },
1322 Scan::ReadInto { files, format } => LoadAnswer::Convert {
1323 what: loading::Conversion::Text(format),
1324 bytes: bytes_of(&files),
1325 files,
1326 path,
1327 options,
1328 },
1329 Scan::Tables { file, tables, .. } => LoadAnswer::Tables { file, tables, path },
1330 Scan::Unpack {
1331 file,
1332 member,
1333 format,
1334 } => LoadAnswer::Convert {
1335 what: loading::Conversion::Text(format),
1336 bytes: bytes_of(std::slice::from_ref(&file)),
1337 files: vec![file],
1338 path,
1339 options: OpenOptions {
1340 table: Some(member),
1341 ..options
1342 },
1343 },
1344 Scan::Hex { file, asked } => LoadAnswer::Hex {
1345 file,
1346 asked,
1347 record_size: options.record_size,
1348 },
1349 })
1350 }
1351
1352 pub(crate) fn read_schema_for_open(
1355 lf: LazyFrame,
1356 path: Option<PathBuf>,
1357 options: OpenOptions,
1358 cloud: &crate::config::CloudConfig,
1359 runtime: &tokio::runtime::Handle,
1360 report: &crate::loading::measurements::OpenReport,
1361 made: loading::Made,
1362 ) -> std::result::Result<loading::LoadAnswer, String> {
1363 use loading::LoadAnswer;
1364 let (state, facts, debug_label) =
1365 Self::build_schema_state(lf, path.as_deref(), &options, cloud, runtime, report)
1366 .map_err(|e| crate::error_display::user_message_from_report(&e, path.as_deref()))?;
1367 let loading::Made {
1369 download,
1370 converted,
1371 notes,
1372 other_tables,
1373 detail,
1374 } = made;
1375 let mut open_notes = facts.open_notes;
1376 open_notes.extend(notes);
1377 let mut other_tables_found = facts.other_tables;
1378 other_tables_found.extend(other_tables);
1379 let state = state.with_open(OpenFacts {
1380 fetched: Self::was_fetched(download.as_ref(), path.as_deref()),
1381 download,
1382 converted,
1383 other_tables: other_tables_found,
1384 open_notes,
1385 detail: detail.or(facts.detail),
1386 ..facts
1387 });
1388 Ok(LoadAnswer::SchemaRead {
1389 state: Box::new(state),
1390 path,
1391 options,
1392 debug_label: Some(debug_label),
1393 })
1394 }
1395
1396 fn spawn_load_phase(&mut self, load: loading::LoadId, step: loading::Step) {
1400 use loading::{LoadAnswer, Step};
1401 let job = Job::Load(load);
1402 let (cloud, runtime) = (self.app_config.cloud.clone(), self.runtime.clone());
1404 match step {
1405 #[cfg(any(feature = "http", feature = "cloud"))]
1406 Step::ReadHeaders {
1407 url,
1408 format,
1409 options,
1410 writer,
1411 } => {
1412 self.spawn_job(job, Some("Reading headers..."), move |_| {
1413 let read =
1414 crate::cloud::remote_model::read(&url, format, &cloud, &runtime, &|| {
1415 writer.stopped()
1416 });
1417 let crate::cloud::remote_model::Read { lf, summary, notes } = match read {
1418 Ok(read) => read,
1419 Err(crate::formats::model_files::RangeError::NoRanges) => {
1420 return Ok(Answer::Load(Box::new(LoadAnswer::NoRanges { options })));
1421 }
1422 Err(crate::formats::model_files::RangeError::Failed(message)) => {
1424 return Err(crate::logging::redact(&message, &[]));
1425 }
1426 };
1427 let opened = Arc::new(crate::formats::model_files::opened(&summary));
1428 let options = OpenOptions {
1429 format: Some(format),
1430 opened: Some(opened.clone()),
1431 ..options
1432 };
1433 let state = Self::schema_state_from_full_scan(
1435 lf,
1436 None,
1437 &OpenOptions {
1438 hive: false,
1439 ..options.clone()
1440 },
1441 )
1442 .map_err(|e| crate::error_display::user_message_from_report(&e, Some(&url)))?
1443 .with_open(OpenFacts {
1444 detail: opened.detail.clone(),
1445 open_notes: notes,
1446 read_as: Some(format),
1447 ..Default::default()
1448 });
1449 Ok(Answer::Load(Box::new(LoadAnswer::SchemaRead {
1450 state: Box::new(state),
1451 path: Some(url),
1452 options,
1453 debug_label: Some("model headers (ranged)".to_string()),
1454 })))
1455 });
1456 }
1457 #[cfg(any(feature = "http", feature = "cloud"))]
1458 Step::Probe(pending) => {
1459 self.spawn_job(job, Some("Checking size..."), move |_| {
1460 #[cfg(feature = "cloud")]
1462 if let loading::PendingDownload::Arrow { url, .. } = &pending {
1463 let (_, _, options) = pending.parts();
1464 let (objects, options) =
1465 crate::cloud::cloud_arrow::list(url, options, &cloud, &runtime)
1466 .map_err(|e| {
1467 crate::error_display::user_message_from_report(&e, None)
1468 })?;
1469 let size = crate::cloud::cloud_arrow::stream_bytes(&objects);
1470 return Ok(Answer::Load(Box::new(LoadAnswer::Sized(
1471 loading::PendingDownload::Arrow {
1472 url: url.clone(),
1473 objects,
1474 size: Some(size),
1475 options,
1476 },
1477 ))));
1478 }
1479 let size = match &pending {
1480 #[cfg(feature = "http")]
1481 loading::PendingDownload::Http { url, .. } => {
1482 Self::fetch_remote_size_http(url).map_err(|gone| gone.message)?
1485 }
1486 #[cfg(feature = "cloud")]
1487 loading::PendingDownload::S3 { url, options, .. }
1488 | loading::PendingDownload::Gcs { url, options, .. }
1489 | loading::PendingDownload::Azure { url, options, .. }
1490 if loading::unnamed_object(url, options) =>
1491 {
1492 match Self::sniff_cloud_object(url, &cloud, &runtime) {
1495 Ok(Unnamed::Missing) => {
1497 return Ok(Answer::Load(Box::new(LoadAnswer::InPlace {
1498 path: PathBuf::from(format!("{url}/")),
1499 options: options.clone(),
1500 })));
1501 }
1502 Ok(Unnamed::Parquet) => {
1503 return Ok(Answer::Load(Box::new(LoadAnswer::InPlace {
1504 path: PathBuf::from(url),
1505 options: OpenOptions {
1506 format: Some(FileFormat::Parquet),
1507 ..options.clone()
1508 },
1509 })));
1510 }
1511 Ok(Unnamed::Other { size }) => Some(size),
1512 Err(_) => None,
1514 }
1515 }
1516 #[cfg(feature = "cloud")]
1517 loading::PendingDownload::S3 { url, .. }
1518 | loading::PendingDownload::Gcs { url, .. }
1519 | loading::PendingDownload::Azure { url, .. } => {
1520 Self::fetch_remote_size_cloud(url, &cloud, &runtime).unwrap_or(None)
1521 }
1522 #[cfg(feature = "cloud")]
1523 loading::PendingDownload::Arrow { size, .. } => *size,
1524 };
1525 Ok(Answer::Load(Box::new(LoadAnswer::Sized(
1526 pending.with_size(size),
1527 ))))
1528 });
1529 }
1530 #[cfg(any(feature = "http", feature = "cloud"))]
1531 Step::Download { pending, writer } => {
1532 let status = match &pending {
1535 #[cfg(feature = "http")]
1536 loading::PendingDownload::Http { .. } => "Downloading...",
1537 #[cfg(feature = "cloud")]
1538 loading::PendingDownload::S3 { .. } => "Downloading from S3...",
1539 #[cfg(feature = "cloud")]
1540 loading::PendingDownload::Gcs { .. } => "Downloading from GCS...",
1541 #[cfg(feature = "cloud")]
1542 loading::PendingDownload::Azure { .. } => "Downloading from Azure...",
1543 #[cfg(feature = "cloud")]
1544 loading::PendingDownload::Arrow { url, .. } => {
1545 match source::input_source(Path::new(url)) {
1546 source::InputSource::Gcs(_) => "Downloading from GCS...",
1547 source::InputSource::Azure(_) => "Downloading from Azure...",
1548 _ => "Downloading from S3...",
1549 }
1550 }
1551 };
1552 let sized = pending
1554 .parts()
1555 .1
1556 .filter(|_| status == "Downloading...")
1557 .map(|size| format!("Downloading {}...", crate::numfmt::bytes(size)));
1558 let status = sized.as_deref().unwrap_or(status);
1559 self.spawn_job(job, Some(status), move |_| {
1560 let (url, _, options) = pending.parts();
1561 let fetched = match &pending {
1562 #[cfg(feature = "http")]
1563 loading::PendingDownload::Http { .. } => {
1564 let ext = source::download_suffix(url);
1565 let limit = options
1568 .download_unasked
1569 .filter(|_| pending.parts().1.is_none())
1570 .map(|unasked| unasked.limit);
1571 Self::download_http_to_temp(
1572 url,
1573 options.temp_dir.as_deref(),
1574 ext.as_deref(),
1575 limit,
1576 &writer,
1577 )
1578 .map(|file| (file, options.clone()))
1579 }
1580 #[cfg(feature = "cloud")]
1581 loading::PendingDownload::S3 { .. }
1582 | loading::PendingDownload::Gcs { .. }
1583 | loading::PendingDownload::Azure { .. } => {
1584 Self::download_cloud_to_temp(url, &cloud, options, &runtime, &writer)
1585 .map(|file| (file, options.clone()))
1586 }
1587 #[cfg(feature = "cloud")]
1589 loading::PendingDownload::Arrow { objects, .. } => {
1590 crate::cloud::cloud_arrow::download(
1591 objects, options, &cloud, &runtime, &writer,
1592 )
1593 .map(|(file, parts)| {
1594 let options = OpenOptions {
1595 format: Some(FileFormat::Arrow),
1596 hive: false,
1597 arrow_parts: Some(Arc::new(parts)),
1598 ..options.clone()
1599 };
1600 (file, options)
1601 })
1602 }
1603 };
1604 let (download, options) = match fetched {
1605 Err(e)
1606 if e.downcast_ref::<crate::cloud::download::PastLimit>()
1607 .is_some() =>
1608 {
1609 return Ok(Answer::Load(Box::new(LoadAnswer::PastLimit(pending))));
1610 }
1611 fetched => fetched.map_err(|e| {
1612 crate::error_display::user_message_from_report(&e, None)
1613 })?,
1614 };
1615 Ok(Answer::Load(Box::new(LoadAnswer::Downloaded {
1616 download,
1617 options,
1618 })))
1619 });
1620 }
1621 Step::Spool {
1622 options,
1623 writer,
1624 read,
1625 } => {
1626 let piped = self.pipes.stdin_reader.take();
1629 let stdout = self.pipes.stdout_pass.take();
1630 self.spawn_job(job, Some("Reading stdin..."), move |_| {
1631 let open =
1632 move || -> crate::cloud::download::Opened<Box<dyn std::io::Read + Send>> {
1633 Ok((piped.unwrap_or_else(|| Box::new(std::io::stdin())), None))
1634 };
1635 let (download, options) = if options.follow
1638 || options.tee.is_some()
1639 || crate::loading::stdin::may_read_as_it_arrives(&options)
1640 {
1641 match crate::loading::follow::spool(open, options, &writer, &read, stdout)?
1642 {
1643 (crate::loading::follow::Spooled::Temp(download), options) => {
1644 (download, options)
1645 }
1646 (crate::loading::follow::Spooled::Kept(file), options) => {
1647 return Ok(Answer::Load(Box::new(LoadAnswer::Recorded {
1648 file,
1649 options,
1650 })));
1651 }
1652 }
1653 } else {
1654 crate::loading::stdin::spool(open, options, &writer, &read)?
1655 };
1656 Ok(Answer::Load(Box::new(LoadAnswer::Spooled {
1657 download,
1658 options,
1659 })))
1660 });
1661 }
1662 Step::FetchSpec {
1663 url,
1664 options,
1665 writer,
1666 } => {
1667 self.spawn_job(job, Some("Reading spec..."), move |_| {
1668 #[cfg(any(feature = "http", feature = "cloud"))]
1669 let fetched = crate::cloud::remote_model::fetch_small(
1670 &url,
1671 crate::formats::MAX_SPEC_BYTES,
1672 &cloud,
1673 &runtime,
1674 &|| writer.stopped(),
1675 );
1676 #[cfg(not(any(feature = "http", feature = "cloud")))]
1677 let fetched: std::result::Result<Option<Vec<u8>>, String> = {
1678 let _ = &writer;
1679 Err(crate::error_display::file_message(
1680 &url,
1681 "this build reads no URLs",
1682 ))
1683 };
1684 let bytes = fetched
1686 .map_err(|message| crate::logging::redact(&message, &[]))?
1687 .ok_or_else(|| {
1688 crate::logging::redact(
1689 &crate::error_display::file_message(
1690 &url,
1691 &format!(
1692 "a format spec is at most {}",
1693 crate::formats::MAX_SPEC_SAID
1694 ),
1695 ),
1696 &[],
1697 )
1698 })?;
1699 let spec = crate::formats::Spec::from_bytes(&bytes, &url)
1700 .map_err(|e| crate::logging::redact(&e.to_string(), &[]))?;
1701 Ok(Answer::Load(Box::new(LoadAnswer::SpecFetched {
1702 spec: Arc::new(spec),
1703 options,
1704 })))
1705 });
1706 }
1707 Step::DecompressRecords {
1708 file,
1709 path,
1710 choice,
1711 options,
1712 writer,
1713 } => {
1714 self.spawn_job(job, Some("Decompressing..."), move |_| {
1715 let failed = |e: color_eyre::Report| {
1716 crate::error_display::user_message_from_report(&e, Some(path.as_path()))
1717 };
1718 let compression = options
1719 .compression
1720 .or_else(|| CompressionFormat::from_extension(&file))
1721 .ok_or_else(|| format!("{} is not compressed", path.display()))?;
1722 let temp_dir = options.temp_dir.clone().unwrap_or_else(std::env::temp_dir);
1723 let copy = crate::formats::readers::csv::decompress_to_copy(
1724 &file,
1725 compression,
1726 &temp_dir,
1727 &writer,
1728 )
1729 .map_err(failed)?;
1730 Ok(Answer::Load(Box::new(LoadAnswer::DecompressedRecords {
1731 copy,
1732 path,
1733 choice,
1734 options,
1735 })))
1736 });
1737 }
1738 Step::ReadRecords {
1739 copy,
1740 path,
1741 choice,
1742 options,
1743 } => {
1744 self.spawn_job(job, Some("Reading records..."), move |_| {
1745 let named = path
1746 .file_name()
1747 .map(|n| n.to_string_lossy().into_owned())
1748 .unwrap_or_default();
1749 let read = crate::formats::read(©, &named, choice)?;
1750 let lf = Arc::clone(&read.records).into_lazy().map_err(|e| {
1751 crate::error_display::user_message_from_report(
1752 &color_eyre::eyre::eyre!(e),
1753 Some(path.as_path()),
1754 )
1755 })?;
1756 Ok(Answer::Load(Box::new(LoadAnswer::Scanned {
1757 lf: Box::new(lf),
1758 path: Some(path),
1759 options: OpenOptions {
1760 format_read: Some(Arc::new(read)),
1761 ..options
1762 },
1763 })))
1764 });
1765 }
1766 Step::Decompress {
1767 file,
1768 path,
1769 options,
1770 writer,
1771 download,
1772 } => {
1773 let options = OpenOptions {
1775 format: options.format.or(Some(FileFormat::TEXT)),
1776 ..options
1777 };
1778 let formats = self.formats.clone();
1779 self.spawn_job(job, Some("Decompressing..."), move |_| {
1780 let failed = |e: color_eyre::Report| {
1781 crate::error_display::user_message_from_report(&e, Some(path.as_path()))
1782 };
1783 let options =
1784 Self::with_delimited_spec(&file, options, &formats).map_err(failed)?;
1785 let lines = options.delimited.is_none()
1786 && options.format.is_some_and(FileFormat::is_lines);
1787 let (state, opened) = if lines {
1788 let (read, opened) = crate::formats::readers::csv::from_lines_decompressed(
1789 &file, &options, &writer,
1790 )
1791 .map_err(failed)?;
1792 let state = DataTableState::from_read(read, &options).map_err(failed)?;
1793 (state, Some(opened))
1794 } else {
1795 let state = Self::decompressed_delimited_state(&file, &options, &writer)
1796 .map_err(failed)?;
1797 (state, None)
1798 };
1799 let mut open_notes = options
1800 .delimited
1801 .as_ref()
1802 .map(|read| read.notes())
1803 .unwrap_or_default();
1804 open_notes.extend(opened.iter().flat_map(|o| o.notes.iter().cloned()));
1805 let state = state.with_open(OpenFacts {
1806 fetched: Self::was_fetched(download.as_ref(), Some(&path)),
1807 download,
1808 open_notes,
1809 records: opened.and_then(|o| o.window),
1810 delimited: options.delimited.clone(),
1811 read_as: options.format,
1812 read_mode: options.format.and_then(|f| {
1814 f.read_mode(crate::Stored::Compressed {
1815 in_memory: options.decompress_in_memory,
1816 })
1817 }),
1818 ..Default::default()
1819 });
1820 Ok(Answer::Load(Box::new(LoadAnswer::SchemaRead {
1821 state: Box::new(state),
1822 path: Some(path),
1823 options,
1824 debug_label: Some("decompressed delimited".to_string()),
1825 })))
1826 });
1827 }
1828 Step::Convert {
1829 what,
1830 files,
1831 path,
1832 options,
1833 writer,
1834 read,
1835 } => {
1836 let formats = self.formats.clone();
1839 self.spawn_job(job, Some(what.status()), move |_| {
1840 let named = |e: color_eyre::Report| {
1841 crate::error_display::user_message_from_report(&e, path.as_deref())
1842 };
1843 let converted = match what {
1844 loading::Conversion::Streams => {
1845 let converted = crate::formats::ipc_stream::convert(
1846 &files,
1847 options.temp_dir.as_deref(),
1848 &writer,
1849 &read,
1850 )
1851 .map_err(named)?;
1852 loading::Converted::Streams {
1853 file: converted.file,
1854 parts: converted.parts,
1855 }
1856 }
1857 loading::Conversion::Text(format) => {
1858 let display = path.clone().unwrap_or_else(|| files[0].clone());
1859 let (converted, detail) = crate::formats::readers::convert(
1860 &crate::formats::readers::ConvertIn {
1861 files: &files,
1862 display: &display,
1863 format,
1864 options: &options,
1865 formats: &formats,
1866 writer: &writer,
1867 read: &read,
1868 },
1869 )
1870 .map_err(named)?;
1871 loading::Converted::Frame {
1872 files: converted.files,
1873 lf: Box::new(converted.lf),
1874 notes: converted.notes,
1875 other_tables: converted.other_tables,
1876 detail,
1877 }
1878 }
1879 };
1880 Ok(Answer::Load(Box::new(LoadAnswer::Converted {
1881 converted,
1882 path,
1883 options,
1884 })))
1885 });
1886 }
1887 Step::Scan {
1888 paths,
1889 options,
1890 display,
1891 status,
1892 } => {
1893 let formats = self.formats.clone();
1894 let path = display.or_else(|| paths.first().cloned());
1896 self.home_app.reads.scans += 1;
1897 self.spawn_job(job, Some(status), move |_| {
1898 Self::scan_for_open(&cloud, &formats, &paths, options, path)
1899 .map(|answer| Answer::Load(Box::new(answer)))
1900 });
1901 }
1902 Step::ReadSchema {
1903 lf,
1904 path,
1905 options,
1906 progress,
1907 made,
1908 } => {
1909 self.debug.schema_load = None;
1910 let report = crate::loading::measurements::OpenReport {
1911 progress,
1912 meter: Arc::new(crate::loading::measurements::Meter::default()),
1913 remembered: Some(self.cache.clone()),
1914 writes: self.cache_writes.clone(),
1915 };
1916 self.spawn_job(job, Some("Reading schema..."), move |_| {
1917 Self::read_schema_for_open(*lf, path, options, &cloud, &runtime, &report, made)
1918 .map(|answer| Answer::Load(Box::new(answer)))
1919 });
1920 }
1921 Step::Nothing
1922 | Step::Crash(_)
1923 | Step::Install(_)
1924 | Step::Failed(_)
1925 | Step::Tables(_)
1926 | Step::Hex(_) => {
1927 unreachable!("not a phase with a worker")
1928 }
1929 #[cfg(any(feature = "http", feature = "cloud"))]
1930 Step::Ask(_) => unreachable!("not a phase with a worker"),
1931 Step::AskRead(_) => unreachable!("not a phase with a worker"),
1932 }
1933 }
1934
1935 fn in_memory_confirmation_message(read: &loading::InMemory) -> String {
1938 let what = match read.files {
1939 1 => format!(
1940 "{}: {} reads",
1941 read.name
1942 .file_name()
1943 .map(|n| n.to_string_lossy().into_owned())
1944 .unwrap_or_else(|| read.name.display().to_string()),
1945 read.format.title()
1946 ),
1947 n => format!("{n} {} files read", read.format.title()),
1948 };
1949 format!(
1950 "{what} {} into memory before the table appears.\n\nRead it?",
1951 crate::numfmt::bytes(read.bytes)
1952 )
1953 }
1954
1955 #[cfg(any(feature = "http", feature = "cloud"))]
1957 fn download_confirmation_message(
1958 pending: &loading::PendingDownload,
1959 note: Option<&str>,
1960 ) -> String {
1961 let (url, size, options) = pending.parts();
1962 let size_str = size
1963 .map(crate::numfmt::bytes)
1964 .unwrap_or_else(|| "unknown".to_string());
1965 let dest_dir = options
1966 .temp_dir
1967 .as_deref()
1968 .map(|p| p.display().to_string())
1969 .unwrap_or_else(|| std::env::temp_dir().display().to_string());
1970 let note = note.map(|note| format!("{note}\n\n")).unwrap_or_default();
1971 let files = match pending.arrow_files() {
1973 Some((1, 0)) => "Arrow stream: converted as it downloads\n".to_string(),
1974 Some((streams, 0)) => {
1975 format!("Files: {streams} Arrow streams, converted as they download\n")
1976 }
1977 Some((streams, in_place)) => {
1978 let streams = match streams {
1979 1 => "1 Arrow stream, converted as it downloads".to_string(),
1980 n => format!("{n} Arrow streams, converted as they download"),
1981 };
1982 let in_place = match in_place {
1983 1 => "1 IPC file read in place".to_string(),
1984 n => format!("{n} IPC files read in place"),
1985 };
1986 format!("Files: {streams}; {in_place}\n")
1987 }
1988 None => String::new(),
1989 };
1990 format!(
1991 "{note}URL: {url}\n{files}File size: {size_str}\nDestination: {dest_dir} (temporary file)\n\nContinue with download?"
1992 )
1993 }
1994}
1995
1996impl App {
1998 fn hoist_partition_columns(
1999 lf: LazyFrame,
2000 schema: &Schema,
2001 partition_columns: &[String],
2002 drifts: bool,
2003 ) -> LazyFrame {
2004 hoist_partition_columns(lf, schema, partition_columns, drifts)
2005 }
2006
2007 pub(crate) fn schema_state_from_local_hive(
2013 path: Option<&Path>,
2014 options: &OpenOptions,
2015 report: &crate::loading::measurements::OpenReport,
2016 ) -> Option<(DataTableState, OpenFacts)> {
2017 if !options.single_spine_schema {
2018 return None;
2019 }
2020 let p = path.filter(|p| p.is_dir() && options.hive)?;
2021 dataset_files::open(Arc::new(dataset_files::LocalFiles::new(p)), options, report)
2022 }
2023
2024 #[cfg(feature = "cloud")]
2026 fn schema_state_from_cloud_hive(
2027 path: Option<&Path>,
2028 options: &OpenOptions,
2029 cloud: &crate::config::CloudConfig,
2030 runtime: &tokio::runtime::Handle,
2031 report: &crate::loading::measurements::OpenReport,
2032 ) -> Option<(DataTableState, OpenFacts)> {
2033 if !options.single_spine_schema || options.format == Some(FileFormat::Arrow) {
2035 return None;
2036 }
2037 let p = path.filter(|p| {
2040 let s = p.as_os_str().to_string_lossy();
2041 home::is_object_store_url(p) && (options.hive || source::is_prefix_or_glob(&s))
2042 })?;
2043
2044 let (full, cloud_opts, store) = Self::cloud_store_for(p, cloud, runtime).ok()?;
2045 let (_bucket, key) = Self::cloud_bucket_and_key(&full).ok()?;
2046 Self::schema_state_from_cloud_hive_with(
2047 full, key, store, cloud_opts, options, runtime, report,
2048 )
2049 }
2050
2051 #[cfg(feature = "cloud")]
2054 pub(crate) fn schema_state_from_cloud_hive_with(
2055 full: String,
2056 key: String,
2057 store: Arc<dyn object_store::ObjectStore>,
2058 cloud_opts: CloudOptions,
2059 options: &OpenOptions,
2060 runtime: &tokio::runtime::Handle,
2061 report: &crate::loading::measurements::OpenReport,
2062 ) -> Option<(DataTableState, OpenFacts)> {
2063 let pattern = full.contains('*').then(|| {
2068 globset::GlobBuilder::new(&key)
2069 .literal_separator(true)
2070 .build()
2071 .map(|g| g.compile_matcher())
2072 });
2073 let pattern = match pattern {
2074 Some(Err(_)) => return None,
2076 Some(Ok(matcher)) => Some(matcher),
2077 None => None,
2078 };
2079 let listed = cloud_hive::prefix_of_glob(&key).to_string();
2080 Self::schema_state_from_cloud_files(
2081 CloudTarget {
2082 full: &full,
2083 key: listed,
2084 pattern: pattern.as_ref(),
2085 },
2086 store,
2087 cloud_opts,
2088 options,
2089 runtime,
2090 report,
2091 )
2092 }
2093
2094 #[cfg(feature = "cloud")]
2099 fn schema_state_from_cloud_files(
2100 target: CloudTarget<'_>,
2101 store: Arc<dyn object_store::ObjectStore>,
2102 cloud_opts: CloudOptions,
2103 options: &OpenOptions,
2104 runtime: &tokio::runtime::Handle,
2105 report: &crate::loading::measurements::OpenReport,
2106 ) -> Option<(DataTableState, OpenFacts)> {
2107 let CloudTarget { full, key, pattern } = target;
2108 dataset_files::open(
2109 Arc::new(dataset_files::StoreFiles::new(
2110 full,
2111 key,
2112 pattern.cloned(),
2113 store,
2114 cloud_opts,
2115 runtime,
2116 )),
2117 options,
2118 report,
2119 )
2120 }
2121
2122 #[cfg(feature = "cloud")]
2127 pub(crate) fn record_cloud_object_facts(
2128 cache: Option<&crate::cache::CacheManager>,
2129 full: &str,
2130 footer: &cloud_hive::FileFooter,
2131 ) {
2132 let Some(cache) = cache else {
2133 return;
2134 };
2135 let columns: Vec<String> = footer
2136 .schema
2137 .iter_names()
2138 .map(|name| name.to_string())
2139 .collect();
2140 cache.record_dataset_facts(&[(
2141 PathBuf::from(full),
2142 crate::cache::DatasetFacts {
2143 mtime: std::time::SystemTime::now()
2144 .duration_since(std::time::UNIX_EPOCH)
2145 .map(|d| d.as_secs())
2146 .unwrap_or_default(),
2147 size: 0,
2148 rows: Some(footer.rows()),
2149 cols: Some(columns.len()),
2150 cols_sampled: false,
2151 columns,
2152 kind: Some(discover::EntryKind::File),
2153 classified_by: discover::CLASSIFIER_VERSION,
2154 cost: discover::Cost {
2155 row_groups: Some(footer.row_group_rows.len()),
2156 ..Default::default()
2157 },
2158 holds: Default::default(),
2159 },
2160 )]);
2161 }
2162
2163 fn schema_state_from_full_scan(
2166 mut lf: LazyFrame,
2167 path: Option<&Path>,
2168 options: &OpenOptions,
2169 ) -> Result<DataTableState> {
2170 let schema = lf
2171 .collect_schema()
2172 .map_err(color_eyre::eyre::Report::from)?;
2173 let partition_columns =
2174 match path.filter(|p| options.hive && (p.is_dir() || source::expands_as_glob(p))) {
2175 Some(p) => crate::formats::readers::hive::discover_hive_partition_columns(p)
2176 .into_iter()
2177 .filter(|c| schema.contains(c.as_str()))
2178 .collect::<Vec<_>>(),
2179 None => Vec::new(),
2180 };
2181 let lf = Self::hoist_partition_columns(lf, &schema, &partition_columns, false);
2182 let part_cols = (!partition_columns.is_empty()).then_some(partition_columns);
2183 DataTableState::from_schema_and_lazyframe(schema, lf, options, part_cols)
2184 }
2185
2186 fn build_schema_state(
2189 lf: LazyFrame,
2190 path: Option<&Path>,
2191 options: &OpenOptions,
2192 cloud: &crate::config::CloudConfig,
2193 runtime: &tokio::runtime::Handle,
2194 report: &crate::loading::measurements::OpenReport,
2195 ) -> Result<(DataTableState, OpenFacts, String)> {
2196 let (state, mut facts, label) =
2199 Self::schema_state_by_route(lf, path, options, cloud, runtime, report)?;
2200 let names_look_like_data = options.format.and_then(FileFormat::separator).is_some()
2205 && options.has_header != Some(false)
2206 && !crate::formats::schema_union::names_are_names(
2207 &state
2208 .schema()
2209 .iter_names()
2210 .map(|n| n.to_string())
2211 .collect::<Vec<_>>(),
2212 );
2213 facts.open_notes = crate::notes::from_the_open(
2214 &options.left_out,
2215 options.read_as_plain_files_of,
2216 options.files_disagree,
2217 names_look_like_data,
2218 );
2219 facts.not_the_table = options.read_as_plain_files_of;
2222 if let Some(splits) = &options.splits {
2223 facts.other_tables = splits.others.clone();
2224 facts
2225 .open_notes
2226 .extend(crate::notes::map_caches(splits.caches));
2227 }
2228 if let Some(read) = &options.format_read {
2229 facts.open_notes.extend(read.notes());
2230 facts.format_read = Some(read.clone());
2231 }
2232 if let Some(opened) = &options.opened {
2233 facts.records = opened.window.clone();
2234 facts.detail = opened.detail.clone();
2235 facts.other_tables = opened.other_tables.clone();
2236 facts.open_notes.extend(opened.notes.iter().cloned());
2237 facts.units = opened.units.clone();
2238 facts.indexing = opened.indexing.clone();
2239 facts.numbering = opened.numbering.clone();
2240 }
2241 if let Some(sqlite) = &options.sqlite {
2242 facts.pushdown = Some(sqlite.pushdown.clone());
2243 facts.hold = sqlite.hold.lock().ok().and_then(|mut hold| hold.take());
2244 facts.other_tables = sqlite.other_tables.clone();
2245 }
2246 if let Some(read) = &options.delimited {
2247 facts.open_notes.extend(read.notes());
2248 facts.delimited = Some(read.clone());
2249 }
2250 facts.open_notes.extend(options.read_notes.iter().cloned());
2251 facts.typing = options.typing.clone();
2252 facts.read_mode = options.read_mode;
2253 facts.read_as = options.format;
2254 facts.remote_source = match &options.arrow_parts {
2258 Some(parts) => parts.iter().any(|part| {
2259 matches!(part, crate::formats::ipc_stream::Part::InPlace(p) if source::is_remote_url(p))
2260 }),
2261 None => path.is_some_and(source::scans_in_place),
2262 };
2263 if options.hive
2267 && facts.remote_files.is_none()
2268 && options.format.is_none_or(|f| f == FileFormat::Parquet)
2269 && let Some(dir) = path.filter(|p| !source::is_remote_url(p) && p.is_dir())
2270 {
2271 facts.parquet_count_dir = Some(dir.to_path_buf());
2272 }
2273 Ok((state, facts, label))
2274 }
2275
2276 #[cfg(feature = "cloud")]
2281 pub(crate) fn scan_cloud_prefix(
2282 url: &str,
2283 cloud_opts: CloudOptions,
2284 format: FileFormat,
2285 glob: bool,
2286 options: &OpenOptions,
2287 ) -> Option<Result<LazyFrame>> {
2288 if !format.reads_bucket_prefix() {
2291 return None;
2292 }
2293 let pl_path = if url.ends_with('/') && !url.contains('*') {
2296 PlRefPath::new(format!("{url}**/*.*").as_str())
2297 } else {
2298 PlRefPath::new(url)
2299 };
2300 let scan = crate::formats::readers::of(format).bucket_scan?;
2301 Some(scan(crate::formats::readers::BucketIn {
2302 url,
2303 path: pl_path,
2304 cloud: cloud_opts,
2305 glob,
2306 options,
2307 format,
2308 }))
2309 }
2310
2311 #[cfg(feature = "cloud")]
2314 fn cloud_glob_format(url: &str, options: &OpenOptions) -> Option<FileFormat> {
2315 options
2316 .format
2317 .or_else(|| {
2318 url.contains('*')
2319 .then(|| FileFormat::from_path(Path::new(url)))
2320 .flatten()
2321 })
2322 .filter(|f| *f != FileFormat::Parquet)
2323 }
2324
2325 #[cfg(feature = "cloud")]
2328 fn resolve_cloud_url(
2329 path: &Path,
2330 cloud: &crate::config::CloudConfig,
2331 ) -> Result<(String, CloudOptions)> {
2332 let text = path.to_string_lossy();
2333 let resolved = crate::cloud::cloud_sources::resolve_for_open(&text, cloud)
2334 .map_err(|e| color_eyre::eyre::eyre!(e))?;
2335 use object_store::azure::AzureConfigKey;
2336 use polars::io::cloud::GoogleConfigKey;
2337 let gcs_agent = (
2338 GoogleConfigKey::Client(crate::cloud::user_agent::CLIENT_KEY),
2339 crate::cloud::user_agent::get(),
2340 );
2341 let options = match resolved.kind {
2342 crate::cloud::source::ProviderKind::S3 => Self::build_s3_cloud_options(&resolved.s3),
2343 crate::cloud::source::ProviderKind::Gcs
2344 if resolved.signing == crate::cloud::cloud_sources::Signing::Unsigned =>
2345 {
2346 CloudOptions::default()
2347 .with_gcp([(GoogleConfigKey::SkipSignature, "true".into()), gcs_agent])
2348 }
2349 crate::cloud::source::ProviderKind::Gcs => match &resolved.gcloud {
2350 Some((configuration, _)) => CloudOptions::default()
2352 .with_gcp([gcs_agent])
2353 .with_credential_provider(Some(crate::cloud::gcloud::polars_provider(
2354 configuration,
2355 ))),
2356 None => match &resolved.google_credentials {
2357 Some(file) => CloudOptions::default().with_gcp([
2358 (
2359 GoogleConfigKey::ApplicationCredentials,
2360 file.to_string_lossy().into_owned(),
2361 ),
2362 gcs_agent,
2363 ]),
2364 None => CloudOptions::default().with_gcp([gcs_agent]),
2365 },
2366 },
2367 crate::cloud::source::ProviderKind::Azure => {
2368 let (account, _, _) = source::azure_parts(&resolved.url)
2369 .ok_or_else(|| color_eyre::eyre::eyre!("not an Azure URL"))?;
2370 let mut azure = crate::cloud::azure::polars_options(&account, &resolved.azure);
2371 azure.push((
2372 AzureConfigKey::Client(crate::cloud::user_agent::CLIENT_KEY),
2373 crate::cloud::user_agent::get(),
2374 ));
2375 CloudOptions::default().with_azure(azure)
2376 }
2377 };
2378 Ok((resolved.url, options))
2379 }
2380
2381 #[cfg(feature = "cloud")]
2384 pub(crate) fn cloud_store_for(
2385 path: &Path,
2386 cloud: &crate::config::CloudConfig,
2387 runtime: &tokio::runtime::Handle,
2388 ) -> Result<(String, CloudOptions, Arc<dyn object_store::ObjectStore>)> {
2389 let (full, cloud_opts) = Self::resolve_cloud_url(path, cloud)?;
2390 let store = Self::polars_object_store(&full, &cloud_opts, runtime)?;
2391 Ok((full, cloud_opts, store))
2392 }
2393
2394 #[cfg(feature = "cloud")]
2399 fn schema_state_from_cloud_object(
2400 path: &Path,
2401 options: &OpenOptions,
2402 cloud: &crate::config::CloudConfig,
2403 runtime: &tokio::runtime::Handle,
2404 report: &crate::loading::measurements::OpenReport,
2405 ) -> Result<(DataTableState, OpenFacts)> {
2406 let (full, cloud_opts, store) = Self::cloud_store_for(path, cloud, runtime)?;
2407 let (_bucket, key) = Self::cloud_bucket_and_key(&full)?;
2408 if key.is_empty() {
2409 return Err(color_eyre::eyre::eyre!("a bucket, not an object"));
2410 }
2411 let meter = report.meter.clone();
2412 let (footer, etag) = wait_on_runtime(runtime, async move {
2413 cloud_hive::footer_of_cloud_parquet(store, &key, &meter).await
2414 })
2415 .ok_or_else(|| color_eyre::eyre::eyre!("cancelled"))??;
2416 let args = ScanArgsParquet {
2417 schema: Some(footer.schema.clone()),
2418 cloud_options: Some(cloud_opts),
2419 hive_options: polars::io::HiveOptions::default(),
2420 glob: false,
2421 ..Default::default()
2422 };
2423 let lf = LazyFrame::scan_parquet(PlRefPath::new(full.as_str()), args)?;
2424 let state =
2425 DataTableState::from_schema_and_lazyframe(footer.schema.clone(), lf, options, None)?;
2426 Self::record_cloud_object_facts(report.remembered.as_ref(), &full, &footer);
2428 let column_bytes =
2429 crate::formats::schema_union::column_bytes_per_row(&[Some(footer.clone())]);
2430 let facts = OpenFacts {
2431 remote_objects: vec![crate::cloud::local_copy::RemoteObject {
2432 url: full,
2433 size: footer.file_bytes as u64,
2434 etag,
2435 }],
2436 row_groups: vec![footer.row_group_rows],
2437 column_bytes,
2438 ..Default::default()
2439 };
2440 Ok((state, facts))
2441 }
2442
2443 fn schema_state_by_route(
2446 lf: LazyFrame,
2447 path: Option<&Path>,
2448 options: &OpenOptions,
2449 cloud: &crate::config::CloudConfig,
2450 runtime: &tokio::runtime::Handle,
2451 report: &crate::loading::measurements::OpenReport,
2452 ) -> Result<(DataTableState, OpenFacts, String)> {
2453 #[cfg(not(feature = "cloud"))]
2454 let _ = (cloud, runtime);
2455
2456 let attempt = |report: &crate::loading::measurements::OpenReport| {
2462 crate::loading::measurements::OpenReport {
2463 progress: report.progress.clone(),
2464 meter: Arc::new(crate::loading::measurements::Meter::default()),
2465 remembered: report.remembered.clone(),
2466 writes: report.writes.clone(),
2467 }
2468 };
2469
2470 let local = attempt(report);
2471 if let Some((state, facts)) = Self::schema_state_from_local_hive(path, options, &local) {
2472 let facts = OpenFacts {
2473 measurements: local.meter,
2474 ..facts
2475 };
2476 return Ok((state, facts, "one-file (local)".to_string()));
2477 }
2478 if report.progress.is_cancelled() {
2480 return Err(color_eyre::eyre::eyre!("cancelled"));
2481 }
2482 #[cfg(feature = "cloud")]
2483 let cloud_hive_attempt = attempt(report);
2484 #[cfg(feature = "cloud")]
2485 if let Some((state, facts)) =
2486 Self::schema_state_from_cloud_hive(path, options, cloud, runtime, &cloud_hive_attempt)
2487 {
2488 let facts = OpenFacts {
2489 measurements: cloud_hive_attempt.meter,
2490 ..facts
2491 };
2492 return Ok((state, facts, "one-file (cloud)".to_string()));
2493 }
2494 #[cfg(feature = "cloud")]
2495 if let Some(p) = path.filter(|p| {
2496 source::scans_in_place(p)
2497 && !options.hive
2498 && !source::is_prefix_or_glob(&p.to_string_lossy())
2499 }) {
2500 let object = attempt(report);
2501 match Self::schema_state_from_cloud_object(p, options, cloud, runtime, &object) {
2502 Ok((state, facts)) => {
2503 let facts = OpenFacts {
2504 measurements: object.meter,
2505 ..facts
2506 };
2507 return Ok((state, facts, "footer (cloud)".to_string()));
2508 }
2509 Err(e) => {
2511 return Self::schema_state_from_full_scan(lf, path, options).map(|state| {
2514 (
2515 state,
2516 OpenFacts::default(),
2517 format!("full scan (cloud footer: {e})"),
2518 )
2519 });
2520 }
2521 }
2522 }
2523 Self::schema_state_from_full_scan(lf, path, options)
2524 .map(|state| (state, OpenFacts::default(), "full scan".to_string()))
2525 }
2526
2527 fn hugging_face_split(
2531 dir: &Path,
2532 format: FileFormat,
2533 files: Vec<PathBuf>,
2534 options: &OpenOptions,
2535 report: &mut ReadReport,
2536 ) -> Result<Vec<PathBuf>> {
2537 let metadata = || {
2538 ["dataset_info.json", "state.json"]
2539 .iter()
2540 .any(|name| dir.join(name).is_file())
2541 };
2542 if format != FileFormat::Arrow || !metadata() {
2543 return Ok(files);
2544 }
2545 let names: Vec<&str> = files
2546 .iter()
2547 .map(|f| f.file_name().and_then(|n| n.to_str()).unwrap_or_default())
2548 .collect();
2549 let (chosen, splits) = crate::formats::hf_splits::choose(&names, options.table.as_deref())
2550 .map_err(|e| color_eyre::eyre::eyre!("{}: {e}", dir.display()))?;
2551 report.splits = Some(Arc::new(splits));
2552 Ok(chosen.into_iter().map(|i| files[i].clone()).collect())
2553 }
2554
2555 fn one_table(path: Option<&Path>, format: Option<FileFormat>) -> color_eyre::Report {
2557 match path {
2558 Some(path) => crate::error_display::FileError::new(path, cli::one_table(format)).into(),
2559 None => color_eyre::eyre::eyre!(cli::one_table(format)),
2560 }
2561 }
2562
2563 #[cfg(feature = "cloud")]
2565 pub(crate) fn cloud_scan_failed(
2566 url: &str,
2567 e: &polars::prelude::PolarsError,
2568 ) -> color_eyre::Report {
2569 let said = crate::error_display::user_message_from_polars(e);
2570 let (first, rest) = said.split_once('\n').unwrap_or((&said, ""));
2571 let first = first.trim_end().trim_end_matches('.');
2572 fn from_the_store(e: &polars::prelude::PolarsError) -> bool {
2575 use polars::prelude::PolarsError;
2576 match e {
2577 PolarsError::IO { .. } => true,
2578 PolarsError::Context { error, .. } => from_the_store(error),
2579 _ => false,
2580 }
2581 }
2582 let check = if from_the_store(e) {
2583 " Check the credentials and the URL."
2584 } else {
2585 ""
2586 };
2587 let what = format!("could not read it: {first}.{check}");
2588 let what = match rest {
2589 "" => what,
2590 rest => format!("{what}\n{rest}"),
2591 };
2592 crate::error_display::FileError::new(Path::new(url), what).into()
2593 }
2594
2595 fn scan_arrow_parts(
2599 cloud: &crate::config::CloudConfig,
2600 converted: Option<&PathBuf>,
2601 parts: &[crate::formats::ipc_stream::Part],
2602 ) -> Result<LazyFrame> {
2603 use crate::formats::ipc_stream::Part;
2604 #[cfg(not(feature = "cloud"))]
2605 let _ = cloud;
2606 let scan = |path: &Path| -> Result<LazyFrame> {
2607 #[cfg(feature = "cloud")]
2608 if source::is_remote_url(path) {
2609 let (url, cloud_options) = Self::resolve_cloud_url(path, cloud)?;
2610 let args = polars::prelude::UnifiedScanArgs {
2611 cloud_options: Some(cloud_options),
2612 ..Default::default()
2613 };
2614 return Ok(LazyFrame::scan_ipc(
2615 PlRefPath::new(url.as_str()),
2616 Default::default(),
2617 args,
2618 )?);
2619 }
2620 let args = polars::prelude::UnifiedScanArgs {
2623 glob: source::expands_as_glob(path),
2624 ..Default::default()
2625 };
2626 Ok(LazyFrame::scan_ipc(
2627 polars::prelude::PlRefPath::try_from_path(path)?,
2628 Default::default(),
2629 args,
2630 )?)
2631 };
2632 let streams = |offset: u64, rows: u64| -> Result<LazyFrame> {
2633 let file = converted
2634 .ok_or_else(|| color_eyre::eyre::eyre!("No converted Arrow file to read."))?;
2635 let lf = scan(file)?;
2636 let whole = offset == 0
2638 && parts
2639 .iter()
2640 .all(|part| matches!(part, Part::Converted { .. }));
2641 Ok(if whole {
2642 lf
2643 } else {
2644 lf.slice(offset as i64, rows as polars::prelude::IdxSize)
2645 })
2646 };
2647 let mut frames = Vec::new();
2648 let mut run: Option<(u64, u64)> = None;
2649 for part in parts {
2650 match part {
2651 Part::Converted { offset, rows, .. } => {
2652 run = Some(match run {
2653 Some((start, n)) if start + n == *offset => (start, n + rows),
2654 Some((start, n)) => {
2655 frames.push(streams(start, n)?);
2656 (*offset, *rows)
2657 }
2658 None => (*offset, *rows),
2659 });
2660 }
2661 Part::InPlace(path) => {
2662 if let Some((start, n)) = run.take() {
2663 frames.push(streams(start, n)?);
2664 }
2665 frames.push(scan(path)?);
2666 }
2667 }
2668 }
2669 if let Some((start, n)) = run {
2670 frames.push(streams(start, n)?);
2671 }
2672 match frames.len() {
2673 0 => Err(color_eyre::eyre::eyre!("No Arrow files to read.")),
2674 1 => Ok(frames.remove(0)),
2675 _ => Ok(polars::prelude::concat(
2676 frames.as_slice(),
2677 crate::formats::readers::polars::union_of_files(),
2678 )?),
2679 }
2680 }
2681
2682 fn dataset_dict_split(
2686 dir: &Path,
2687 splits: &[String],
2688 options: &OpenOptions,
2689 report: &mut ReadReport,
2690 formats: &crate::formats::Registry,
2691 ) -> Result<Scan> {
2692 let listed: Vec<&str> = splits.iter().map(String::as_str).collect();
2693 let mut picked = crate::formats::hf_splits::pick(&listed, options.table.as_deref())
2694 .map_err(|e| color_eyre::eyre::eyre!("{}: {e}", dir.display()))?;
2695 let split = dir.join(picked.split.as_deref().unwrap_or_default());
2696 let inner = OpenOptions {
2697 table: None,
2698 splits: None,
2699 ..options.clone()
2700 };
2701 let scan = Self::build_local_lazyframe(&[split], &inner, report, formats)?;
2702 picked.caches = report.splits.as_ref().map_or(0, |inner| inner.caches);
2704 report.splits = Some(Arc::new(picked));
2705 Ok(scan)
2706 }
2707
2708 fn files_disagree(
2712 files: &[PathBuf],
2713 options: &OpenOptions,
2714 found: FileFormat,
2715 ) -> crate::formats::schema_union::Disagreement {
2716 let format = options.format.unwrap_or(found);
2719 if format == FileFormat::Parquet {
2720 return Default::default();
2721 }
2722 if options.null_values.is_some() {
2725 return Default::default();
2726 }
2727 crate::formats::schema_union::sample_files(files, format, &Self::read_as(options))
2728 .disagreement()
2729 }
2730
2731 pub(crate) fn read_as(options: &OpenOptions) -> crate::formats::schema_union::ReadAs {
2736 crate::formats::schema_union::ReadAs {
2737 delimiter: options.delimiter,
2738 has_header: options.has_header,
2739 skip_rows: options.skip_rows,
2740 skip_lines: options.skip_lines,
2741 infer_schema_length: options.infer_schema_length,
2742 ignore_errors: options.ignore_errors,
2743 try_parse_dates: options.csv_try_parse_dates(),
2744 comment_char: options.comment_char.clone(),
2745 header_rows: options.header_rows.clone(),
2746 header_join: options.header_join.clone(),
2747 }
2748 }
2749
2750 pub(crate) fn refuse_spec_in_place(path: &Path, options: &OpenOptions) -> Result<()> {
2754 if source::is_remote_url(path)
2755 && (options.spec_file.is_some() || options.spec_name.is_some())
2756 {
2757 return Err(crate::error_display::FileError::new(
2758 path,
2759 "a format spec reads one remote object at a time, not a prefix or a glob; name the object",
2760 )
2761 .into());
2762 }
2763 Ok(())
2764 }
2765
2766 pub(crate) fn build_lazyframe_from_paths_with(
2771 cloud: &crate::config::CloudConfig,
2772 paths: &[PathBuf],
2773 options: &OpenOptions,
2774 report: &mut ReadReport,
2775 formats: &crate::formats::Registry,
2776 ) -> Result<Scan> {
2777 if let Some(parts) = &options.arrow_parts {
2780 if options.table.is_some() && options.splits.is_none() {
2781 let named = parts.first().map(|part| match part {
2783 crate::formats::ipc_stream::Part::InPlace(path) => path.as_path(),
2784 crate::formats::ipc_stream::Part::Converted { source, .. } => source.as_path(),
2785 });
2786 return Err(Self::one_table(named, Some(FileFormat::Arrow)));
2787 }
2788 return Self::scan_arrow_parts(cloud, paths.first(), parts).map(Scan::from);
2789 }
2790 #[cfg(not(feature = "cloud"))]
2792 let _ = cloud;
2793 let path = &paths[0];
2794 Self::refuse_spec_in_place(path, options)?;
2795 match source::input_source(path) {
2796 source::InputSource::Http(_url) => {
2797 #[cfg(feature = "http")]
2798 {
2799 return Err(color_eyre::eyre::eyre!(
2800 "HTTP/HTTPS load is handled in the event loop; this path should not be reached."
2801 ));
2802 }
2803 #[cfg(not(feature = "http"))]
2804 {
2805 return Err(color_eyre::eyre::eyre!(
2806 "HTTP/HTTPS URLs are not supported in this build. Rebuild with default features."
2807 ));
2808 }
2809 }
2810 source::InputSource::S3(url) => {
2811 #[cfg(feature = "cloud")]
2812 {
2813 let (full, cloud_opts) =
2814 Self::resolve_cloud_url(Path::new(&format!("s3://{url}")), cloud)?;
2815 let is_glob = source::is_prefix_or_glob(&full);
2816 if let Some(format) = Self::cloud_glob_format(&full, options)
2819 && let Some(lf) = Self::scan_cloud_prefix(
2820 &full,
2821 cloud_opts.clone(),
2822 format,
2823 is_glob,
2824 options,
2825 )
2826 {
2827 return lf.map(Scan::from);
2828 }
2829 let pl_path = PlRefPath::new(full.as_str());
2830 let hive_options = if is_glob {
2831 polars::io::HiveOptions::new_enabled()
2832 } else {
2833 polars::io::HiveOptions::default()
2834 };
2835 let args = ScanArgsParquet {
2836 cloud_options: Some(cloud_opts),
2837 hive_options,
2838 glob: is_glob,
2839 ..Default::default()
2840 };
2841 let lf = LazyFrame::scan_parquet(pl_path, args)
2842 .map_err(|e| Self::cloud_scan_failed(&full, &e))?;
2843 return Ok(lf.into());
2846 }
2847 #[cfg(not(feature = "cloud"))]
2848 {
2849 let _ = url;
2850 return Err(color_eyre::eyre::eyre!(
2851 "S3 is not supported in this build. Rebuild with default features and set AWS credentials (e.g. AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION)."
2852 ));
2853 }
2854 }
2855 source::InputSource::Gcs(url) => {
2856 #[cfg(feature = "cloud")]
2857 {
2858 let (full, cloud_opts) =
2859 Self::resolve_cloud_url(Path::new(&format!("gs://{url}")), cloud)?;
2860 let is_glob = source::is_prefix_or_glob(&full);
2861 if let Some(format) = Self::cloud_glob_format(&full, options)
2864 && let Some(lf) = Self::scan_cloud_prefix(
2865 &full,
2866 cloud_opts.clone(),
2867 format,
2868 is_glob,
2869 options,
2870 )
2871 {
2872 return lf.map(Scan::from);
2873 }
2874 let pl_path = PlRefPath::new(full.as_str());
2875 let hive_options = if is_glob {
2876 polars::io::HiveOptions::new_enabled()
2877 } else {
2878 polars::io::HiveOptions::default()
2879 };
2880 let args = ScanArgsParquet {
2881 cloud_options: Some(cloud_opts),
2882 hive_options,
2883 glob: is_glob,
2884 ..Default::default()
2885 };
2886 let lf = LazyFrame::scan_parquet(pl_path, args)
2887 .map_err(|e| Self::cloud_scan_failed(&full, &e))?;
2888 return Ok(lf.into());
2889 }
2890 #[cfg(not(feature = "cloud"))]
2891 {
2892 let _ = url;
2893 return Err(color_eyre::eyre::eyre!(
2894 "GCS (gs://) is not supported in this build. Rebuild with default features."
2895 ));
2896 }
2897 }
2898 source::InputSource::Azure(url) => {
2899 #[cfg(feature = "cloud")]
2900 {
2901 let (full, cloud_opts) = Self::resolve_cloud_url(Path::new(&url), cloud)?;
2902 let is_glob = source::is_prefix_or_glob(&full);
2903 if let Some(format) = Self::cloud_glob_format(&full, options)
2906 && let Some(lf) = Self::scan_cloud_prefix(
2907 &full,
2908 cloud_opts.clone(),
2909 format,
2910 is_glob,
2911 options,
2912 )
2913 {
2914 return lf.map(Scan::from);
2915 }
2916 let args = ScanArgsParquet {
2917 cloud_options: Some(cloud_opts),
2918 hive_options: if is_glob {
2919 polars::io::HiveOptions::new_enabled()
2920 } else {
2921 polars::io::HiveOptions::default()
2922 },
2923 glob: is_glob,
2924 ..Default::default()
2925 };
2926 let lf = LazyFrame::scan_parquet(PlRefPath::new(full.as_str()), args)
2927 .map_err(|e| Self::cloud_scan_failed(&full, &e))?;
2928 return Ok(lf.into());
2929 }
2930 #[cfg(not(feature = "cloud"))]
2931 {
2932 let _ = url;
2933 return Err(color_eyre::eyre::eyre!(
2934 "Azure is not supported in this build. Rebuild with default features."
2935 ));
2936 }
2937 }
2938 source::InputSource::Local(_) => {}
2939 }
2940 Self::build_local_lazyframe(paths, options, report, formats)
2941 }
2942
2943 fn read_directory_files(
2946 files: &[PathBuf],
2947 options: &OpenOptions,
2948 found: FileFormat,
2949 report: &mut ReadReport,
2950 formats: &crate::formats::Registry,
2951 ) -> Result<Scan> {
2952 if options.delimited.is_none()
2953 && options.format.is_none()
2954 && found.separator().is_some()
2955 && let Some(first) = files
2956 .iter()
2957 .find(|f| !crate::formats::nul_tail::holds_nothing(f))
2958 && let Some(choice) = Self::delimited_spec_of(first, options, formats)?
2959 {
2960 let nested = OpenOptions {
2961 hive: false,
2962 format: Some(found),
2963 splits: report.splits.clone(),
2964 ..options.clone()
2965 };
2966 return Self::read_with_delimited_spec(files, &nested, report, formats, choice);
2967 }
2968 report.files_disagree = Self::files_disagree(files, options, found);
2970 let nested = OpenOptions {
2971 hive: false,
2972 format: Some(options.format.unwrap_or(found)),
2973 splits: report.splits.clone(),
2974 ..options.clone()
2975 };
2976 Self::build_local_lazyframe(files, &nested, report, formats)
2977 }
2978
2979 fn delimited_spec_of(
2981 file: &Path,
2982 options: &OpenOptions,
2983 formats: &crate::formats::Registry,
2984 ) -> Result<Option<crate::formats::Choice>> {
2985 let asked = crate::formats::Asked {
2986 compression: options.compression,
2987 text_only: true,
2988 ..Default::default()
2989 };
2990 match crate::formats::route(file, &asked, formats)
2991 .map_err(|e| color_eyre::eyre::eyre!(e))?
2992 {
2993 crate::formats::Route::Delimited(choice) => Ok(Some(choice)),
2994 _ => Ok(None),
2995 }
2996 }
2997
2998 fn read_with_delimited_spec(
3000 paths: &[PathBuf],
3001 options: &OpenOptions,
3002 report: &mut ReadReport,
3003 formats: &crate::formats::Registry,
3004 choice: crate::formats::Choice,
3005 ) -> Result<Scan> {
3006 let mut nested = options.clone();
3007 let Some(delimited) = choice.spec.delimited.clone() else {
3008 return Err(color_eyre::eyre::eyre!(
3009 "{} is not a delimited spec",
3010 choice.spec.name
3011 ));
3012 };
3013 delimited.apply(&mut nested);
3014 nested.delimited = Some(Arc::new(
3015 crate::formats::delimited_spec::DelimitedRead::chosen(
3016 choice.spec,
3017 choice.by,
3018 choice.also,
3019 ),
3020 ));
3021 Self::build_local_lazyframe(paths, &nested, report, formats)
3022 }
3023
3024 pub(crate) fn build_local_lazyframe(
3027 paths: &[PathBuf],
3028 options: &OpenOptions,
3029 report: &mut ReadReport,
3030 formats: &crate::formats::Registry,
3031 ) -> Result<Scan> {
3032 let path = &paths[0];
3033
3034 if options.hex
3036 && let [one] = paths
3037 && one.is_file()
3038 {
3039 return Ok(Scan::Hex {
3040 file: one.clone(),
3041 asked: true,
3042 });
3043 }
3044
3045 if let [pattern] = paths
3048 && !options.hive
3049 && options.delimited.is_none()
3050 && options.format.is_none()
3051 && source::expands_as_glob(pattern)
3052 {
3053 let files = crate::loading::local_glob::expand(pattern);
3054 if let Some(first) = files
3055 .iter()
3056 .find(|f| !crate::formats::nul_tail::holds_nothing(f))
3057 && let Some(choice) = Self::delimited_spec_of(first, options, formats)?
3058 {
3059 let format = FileFormat::from_path(first).filter(|f| f.separator().is_some());
3060 let nested = OpenOptions {
3061 format: format.or(Some(FileFormat::Csv)),
3062 ..options.clone()
3063 };
3064 return Self::read_with_delimited_spec(&files, &nested, report, formats, choice);
3065 }
3066 }
3067
3068 if !options.hive && options.delimited.is_none() {
3072 let asked = crate::formats::Asked {
3073 spec_file: options.spec_file.clone(),
3074 spec_name: options.spec_name.clone(),
3075 variant: options.table.clone(),
3076 spec: options.spec_fetched.clone(),
3077 builtin: options.format.is_some(),
3078 compression: options.compression,
3079 text_only: paths.len() > 1,
3080 };
3081 match crate::formats::route(path, &asked, formats)
3082 .map_err(|e| color_eyre::eyre::eyre!(e))?
3083 {
3084 crate::formats::Route::Elsewhere => {}
3085 crate::formats::Route::Delimited(choice) => {
3086 return Self::read_with_delimited_spec(paths, options, report, formats, choice);
3087 }
3088 crate::formats::Route::Read(read) => {
3089 let lf = Arc::clone(&read.records).into_lazy()?;
3090 report.format_read = Some(Arc::new(*read));
3091 return Ok(lf.into());
3092 }
3093 crate::formats::Route::Decompress(choice) => {
3094 return Ok(Scan::DecompressSpec {
3095 file: path.clone(),
3096 choice,
3097 });
3098 }
3099 }
3100 } else if options.hive && (options.spec_file.is_some() || options.spec_name.is_some()) {
3101 return Err(color_eyre::eyre::eyre!(
3102 "a format spec reads one file, or one directory of column files"
3103 ));
3104 }
3105
3106 if let Some(read) = &options.delimited
3108 && report.delimited.is_none()
3109 && path.is_file()
3110 {
3111 report.delimited = Some(Arc::new(crate::formats::delimited_spec::read_facts(
3112 read, paths, options,
3113 )?));
3114 }
3115
3116 if paths.len() == 1 && (options.hive || path.is_dir()) {
3119 let is_single_file = path.is_file();
3121 if !is_single_file {
3122 if path.is_dir()
3124 && let Some(splits) = crate::formats::hf_splits::dataset_dict(path)
3125 {
3126 return Self::dataset_dict_split(path, &splits, options, report, formats);
3127 }
3128 if path.is_dir() {
3129 match crate::home::discover::directory_format(path) {
3130 crate::home::discover::DirectoryFormat::One(FileFormat::Parquet, _) => {}
3132 crate::home::discover::DirectoryFormat::Deeper => {
3136 if let crate::home::discover::DirectoryFormat::One(found, files) =
3137 crate::home::discover::hive_leaf_format(path)
3138 && found != FileFormat::Parquet
3139 {
3140 let named = files
3142 .first()
3143 .and_then(|f| crate::home::discover::data_extension(f))
3144 .unwrap_or_else(|| format!("{found:?}").to_lowercase());
3145 return Err(color_eyre::eyre::eyre!(
3146 "{} is partitioned into key=value directories of .{} \
3147 files. datui reads hive partitioning for Parquet \
3148 only — open one partition instead.",
3149 path.display(),
3150 named
3151 ));
3152 }
3153 }
3154 crate::home::discover::DirectoryFormat::One(found, files) => {
3155 let format = options.format.unwrap_or(found);
3158 let files =
3159 Self::hugging_face_split(path, format, files, options, report)?;
3160 return Self::read_directory_files(
3161 &files, options, found, report, formats,
3162 );
3163 }
3164 crate::home::discover::DirectoryFormat::Mixed {
3165 format: found,
3166 files,
3167 passed_over,
3168 } => {
3169 let format = options.format.unwrap_or(found);
3172 let files =
3173 Self::hugging_face_split(path, format, files, options, report)?;
3174 let lf = Self::read_directory_files(
3175 &files, options, found, report, formats,
3176 )?;
3177 if !matches!(found, FileFormat::Safetensors | FileFormat::Gguf) {
3180 report.left_out = passed_over;
3181 }
3182 return Ok(lf);
3183 }
3184 }
3185 }
3186 let use_parquet_hive =
3187 path.is_dir() || path.as_os_str().to_string_lossy().contains(".parquet");
3188 if use_parquet_hive {
3189 return crate::formats::readers::hive::scan_parquet_hive(path).map(Scan::from);
3191 }
3192 return Err(color_eyre::eyre::eyre!(
3193 "With --hive use a directory or a glob pattern for Parquet (e.g. path/to/dir or path/**/*.parquet)"
3194 ));
3195 }
3196 }
3197
3198 let compressed = options
3202 .compression
3203 .or_else(|| CompressionFormat::from_extension(path))
3204 .is_some();
3205 let named = FileFormat::from_path(path).or_else(|| {
3208 compressed
3209 .then(|| FileFormat::from_path(Path::new(path.file_stem()?)))
3210 .flatten()
3211 .filter(|f| f.decompressed_once())
3212 });
3213 let mut effective_format = options
3214 .format
3215 .or_else(|| {
3217 named.filter(|f| !f.is_lines()).map(|f| {
3218 (!compressed)
3219 .then(|| crate::formats::readers::refined(path, f))
3220 .flatten()
3221 .unwrap_or(f)
3222 })
3223 })
3224 .or_else(|| {
3225 (path.extension().is_none()
3226 && crate::home::discover::is_parquet_key(&path.to_string_lossy()))
3227 .then_some(FileFormat::Parquet)
3228 })
3229 .or_else(|| crate::formats::readers::sniff_open(path, options.compression))
3231 .or(named);
3232 if effective_format.is_none()
3235 && let [file] = paths
3236 && file.is_file()
3237 {
3238 effective_format = crate::formats::lines::guess_file(file, options.compression)
3239 .map(|f| crate::formats::lines::as_asked(f, options));
3240 report.guessed = effective_format.is_some();
3241 }
3242 report.format = effective_format;
3243
3244 if options.table.is_some()
3247 && !effective_format.is_some_and(FileFormat::takes_table)
3248 && options.splits.is_none()
3249 {
3250 return Err(Self::one_table(Some(path), effective_format));
3251 }
3252
3253 if let [file] = paths
3257 && compressed
3258 && let Some(format) = effective_format.filter(|f| f.decompressed_once())
3259 {
3260 return Ok(Scan::Decompress {
3261 file: file.clone(),
3262 format,
3263 });
3264 }
3265
3266 let Some(format) = effective_format else {
3267 if !path.exists() {
3268 return Err(std::io::Error::new(
3269 std::io::ErrorKind::NotFound,
3270 format!("File not found: {}", path.display()),
3271 )
3272 .into());
3273 }
3274 if paths.len() == 1 && path.is_file() {
3276 return Ok(Scan::Hex {
3277 file: path.clone(),
3278 asked: false,
3279 });
3280 }
3281 return Err(color_eyre::eyre::eyre!(match paths.len() {
3282 1 => UNSUPPORTED.to_string(),
3283 _ => crate::formats::readers::many_files_refused(),
3284 }));
3285 };
3286 if paths.len() > 1 && !format.reads_many_files() {
3289 if !path.exists() {
3290 return Err(std::io::Error::new(
3291 std::io::ErrorKind::NotFound,
3292 format!("File not found: {}", path.display()),
3293 )
3294 .into());
3295 }
3296 return Err(color_eyre::eyre::eyre!(
3297 crate::formats::readers::many_files_refused()
3298 ));
3299 }
3300 let guessed;
3301 let options = if report.guessed {
3302 guessed = OpenOptions {
3303 format_guessed: true,
3304 ..options.clone()
3305 };
3306 &guessed
3307 } else {
3308 options
3309 };
3310 crate::formats::readers::scan(crate::formats::readers::ScanIn {
3311 format,
3312 paths,
3313 options,
3314 report,
3315 formats,
3316 })
3317 }
3318}