Skip to main content

datui_lib/loading/
mod.rs

1//! Opening a dataset, from the request to its first rows, and its one owner.
2//!
3//! [`Loader`] holds the open in flight ([`Load`]): its origin, paths, phase, loading
4//! screen text, and what it holds (stop flag, footer counter, download, converted IPC
5//! file, the generation hold while a download is asked about). The app reports events
6//! (a request, a worker's answer or failure, the user's download answer) and carries
7//! out the [`Step`] it returns. Nothing else keeps the open's state.
8//!
9//! - **Identity.** Each load has a [`LoadId`], carried by its phases' jobs. An answer
10//!   is taken only for the load in flight, in the phase that asked; others are dropped
11//!   with their payload.
12//! - **Retirement.** Abandoning, replacing or failing a load raises its stop flag (a
13//!   download or conversion stops at its next chunk and removes its file; a footer
14//!   pass stops reading), releases its files and its hold.
15//! - **Handover.** The dataset is built holding the load's download or converted file
16//!   and everything found ([`crate::table::OpenFacts`]), and takes the footer counter on
17//!   install; what remains is the first-rows read ([`Phase::FirstRows`]), whose
18//!   abandonment stops neither.
19//!
20//! Home's looks, analyses and charts are not loads.
21
22pub(crate) mod counting;
23pub(crate) mod first_rows_trace;
24pub mod follow;
25pub(crate) mod local_glob;
26pub mod measurements;
27pub(crate) mod open_options;
28pub(crate) mod open_scan;
29pub(crate) mod scan;
30pub mod stdin;
31pub mod tee;
32pub(crate) mod unfinished;
33
34use std::path::{Path, PathBuf};
35use std::sync::Arc;
36use std::sync::atomic::{AtomicU64, Ordering};
37
38use polars::prelude::LazyFrame;
39
40use crate::cloud::download::TempDownload;
41use crate::formats::schema_union::FooterProgress;
42use crate::loading::unfinished::{Unfinished, Writer};
43use crate::table::DataTableState;
44use crate::{CompressionFormat, FileFormat, OpenOptions, cloud::source};
45
46use crate::app::jobs::Hold;
47#[cfg(any(feature = "http", feature = "cloud"))]
48use crate::app::jobs::Jobs;
49
50/// Names one open, from the moment it is asked for until it is done.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub(crate) struct LoadId(u64);
53
54impl LoadId {
55    /// A load's name for a job a test starts by hand.
56    #[cfg(test)]
57    pub(crate) fn for_tests(n: u64) -> Self {
58        Self(n)
59    }
60}
61
62/// A remote file to download once the user agrees; its size is what the probe found.
63#[cfg(any(feature = "http", feature = "cloud"))]
64#[derive(Clone)]
65pub(crate) enum PendingDownload {
66    #[cfg(feature = "http")]
67    Http {
68        url: String,
69        size: Option<u64>,
70        options: OpenOptions,
71    },
72    #[cfg(feature = "cloud")]
73    S3 {
74        url: String,
75        size: Option<u64>,
76        options: OpenOptions,
77    },
78    #[cfg(feature = "cloud")]
79    Gcs {
80        url: String,
81        size: Option<u64>,
82        options: OpenOptions,
83    },
84    #[cfg(feature = "cloud")]
85    Azure {
86        url: String,
87        size: Option<u64>,
88        options: OpenOptions,
89    },
90    /// Arrow in S3, GCS or Azure, one object or a prefix: `objects` are what the probe
91    /// chose, `size` its streams' bytes (downloaded and converted); IPC files are scanned
92    /// in place (`cloud_arrow`).
93    #[cfg(feature = "cloud")]
94    Arrow {
95        url: String,
96        objects: Vec<crate::cloud::cloud_arrow::Object>,
97        size: Option<u64>,
98        options: OpenOptions,
99    },
100}
101
102#[cfg(any(feature = "http", feature = "cloud"))]
103impl PendingDownload {
104    /// The url, the size the probe found, and the open options — the same three
105    /// fields whichever store this came from.
106    pub(crate) fn parts(&self) -> (&str, Option<u64>, &OpenOptions) {
107        match self {
108            #[cfg(feature = "http")]
109            PendingDownload::Http { url, size, options } => (url, *size, options),
110            #[cfg(feature = "cloud")]
111            PendingDownload::S3 { url, size, options } => (url, *size, options),
112            #[cfg(feature = "cloud")]
113            PendingDownload::Gcs { url, size, options } => (url, *size, options),
114            #[cfg(feature = "cloud")]
115            PendingDownload::Azure { url, size, options } => (url, *size, options),
116            #[cfg(feature = "cloud")]
117            PendingDownload::Arrow {
118                url, size, options, ..
119            } => (url, *size, options),
120        }
121    }
122
123    /// How many Arrow streams the download is, and how many IPC files are read in
124    /// place beside them, for the question about it.
125    pub(crate) fn arrow_files(&self) -> Option<(usize, usize)> {
126        match self {
127            #[cfg(feature = "cloud")]
128            PendingDownload::Arrow { objects, .. } => {
129                let streams = objects.iter().filter(|o| o.stream).count();
130                Some((streams, objects.len() - streams))
131            }
132            _ => None,
133        }
134    }
135
136    /// The download once the user has agreed to it: it never asks again, so it has
137    /// no limit to stop at.
138    pub(crate) fn asked(mut self) -> Self {
139        match &mut self {
140            #[cfg(feature = "http")]
141            PendingDownload::Http { options, .. } => options.download_unasked = None,
142            #[cfg(feature = "cloud")]
143            PendingDownload::S3 { options, .. }
144            | PendingDownload::Gcs { options, .. }
145            | PendingDownload::Azure { options, .. }
146            | PendingDownload::Arrow { options, .. } => options.download_unasked = None,
147        }
148        self
149    }
150
151    /// Replace the placeholder size with what the probe actually found.
152    pub(crate) fn with_size(mut self, found: Option<u64>) -> Self {
153        match &mut self {
154            #[cfg(feature = "http")]
155            PendingDownload::Http { size, .. } => *size = found,
156            #[cfg(feature = "cloud")]
157            PendingDownload::S3 { size, .. } => *size = found,
158            #[cfg(feature = "cloud")]
159            PendingDownload::Gcs { size, .. } => *size = found,
160            #[cfg(feature = "cloud")]
161            PendingDownload::Azure { size, .. } => *size = found,
162            #[cfg(feature = "cloud")]
163            PendingDownload::Arrow { size, .. } => *size = found,
164        }
165        self
166    }
167}
168
169/// Paths asked to be opened, and what the screen and the recents need of them. Built
170/// once, when the open is asked for, and not changed after.
171#[derive(Clone)]
172pub(crate) struct OpenRequest {
173    pub(crate) paths: Vec<PathBuf>,
174    pub(crate) options: OpenOptions,
175    /// The first path's size on disk, for the loading screen; 0 for a URL.
176    pub(crate) size: u64,
177    /// The path recorded as a recent if the dataset installs: the first, as named,
178    /// unless it is a local path that is not there.
179    pub(crate) recent: Option<PathBuf>,
180    /// What the loading screen names in place of the first path: a table inside a
181    /// database.
182    pub(crate) shown: Option<PathBuf>,
183    /// Ask before reading more than this many bytes whole into memory
184    /// (`[read] memory_warning`); `None` never asks.
185    pub(crate) warn_in_memory_above: Option<u64>,
186}
187
188impl OpenRequest {
189    /// The request for `paths`, asking the filesystem for the first one's size and
190    /// existence. Every open records a recent (command-line ones especially; URLs
191    /// verbatim), under the name given, not a download's temp copy. A table inside a file
192    /// (`app.db/users`) opens as the file with `--table` and is recorded as the table.
193    pub(crate) fn named(
194        mut paths: Vec<PathBuf>,
195        mut options: OpenOptions,
196        formats: &crate::formats::Registry,
197    ) -> Self {
198        let first = paths[0].clone();
199        let piped = stdin::is_stdin(&first);
200        let local = !piped && matches!(source::input_source(&first), source::InputSource::Local(_));
201        let mut table = None;
202        if local
203            && paths.len() == 1
204            && let Some((db, name)) = crate::formats::members::split(&first)
205        {
206            table = Some(first.clone());
207            options.table = Some(name);
208            paths = vec![db];
209        } else if local
210            && first.is_file()
211            && let Some(name) = options.table.as_deref()
212            && crate::formats::members::holder(&first).is_some()
213        {
214            table = Some(crate::formats::members::place(&first, name));
215        } else if local
216            && paths.len() == 1
217            && options.table.is_none()
218            && let Some((dir, split)) = crate::formats::hf_splits::split_place(&first)
219        {
220            // A split of a Hugging Face cache, as the home screen lists it.
221            table = Some(first.clone());
222            options.table = Some(split);
223            paths = vec![dir];
224        } else if local
225            && first.is_dir()
226            && let Some(split) = options.table.as_deref()
227            && crate::formats::hf_splits::cache_splits(&first)
228                .iter()
229                .any(|s| s == split)
230        {
231            table = Some(first.join(split));
232        } else if local
233            && paths.len() == 1
234            && options.table.is_none()
235            && let Some((file, variant)) = crate::formats::members::split_variant(&first, formats)
236        {
237            // A variant of a file a spec reads as several, as the home screen lists it.
238            table = Some(first.clone());
239            options.table = Some(variant);
240            paths = vec![file];
241        } else if local
242            && let Some(variant) = options.table.as_deref()
243            && first.is_file()
244            && formats.variants_of(&first).is_some()
245        {
246            table = Some(crate::formats::members::place(&first, variant));
247        }
248        let first = &paths[0];
249        let size = if local {
250            std::fs::metadata(first).map(|m| m.len()).unwrap_or(0)
251        } else {
252            0
253        };
254        // Standard input cannot be opened again from a list.
255        let recent = table
256            .clone()
257            .or_else(|| (!piped && (!local || first.exists())).then(|| first.clone()));
258        Self {
259            paths,
260            options,
261            size,
262            recent,
263            shown: table,
264            warn_in_memory_above: None,
265        }
266    }
267}
268
269/// Where an open stands. Each phase but the first rows is waiting on one worker, or on
270/// the user.
271pub(crate) enum Phase {
272    /// Asked for, before its first step: the frame between a key and the open it asks
273    /// for, or a startup open before its paths are looked at. Says what its caller says.
274    Starting {
275        label: String,
276        percent: u16,
277    },
278    /// Whether the paths named on the command line are there, and which is a directory.
279    LookingAtPaths,
280    /// The spec a `--format` URL names, fetched before anything is read with it.
281    ReadingSpec,
282    /// What a directory named on the command line holds, before it is opened.
283    LookingAtDirectory,
284    /// A remote model's headers, read by range rather than downloaded.
285    #[cfg(any(feature = "http", feature = "cloud"))]
286    ReadingHeaders,
287    /// The size of a remote file, to put the download to the user; `note` says why it
288    /// is downloaded when it would not have been.
289    #[cfg(any(feature = "http", feature = "cloud"))]
290    CheckingSize {
291        note: Option<&'static str>,
292    },
293    /// Waiting on the user to agree to read files whole into memory, past the size
294    /// that asks first. Holds the generation meanwhile, once the app gives it a hold.
295    ConfirmingRead {
296        scan: Box<Scan>,
297        _hold: Option<Hold>,
298    },
299    /// Waiting on the user to agree to the download. Holds the generation meanwhile:
300    /// nothing is running, and the open is very much unfinished.
301    #[cfg(any(feature = "http", feature = "cloud"))]
302    Confirming {
303        pending: Box<PendingDownload>,
304        note: Option<&'static str>,
305        _hold: Hold,
306    },
307    #[cfg(any(feature = "http", feature = "cloud"))]
308    Downloading,
309    /// Standard input being read to a file; `read` counts its bytes.
310    Spooling {
311        read: Arc<AtomicU64>,
312    },
313    Decompressing,
314    /// A compressed file a format spec reads, being decompressed to a copy.
315    DecompressingRecords,
316    /// The decompressed copy's records, read through the spec.
317    ReadingRecords,
318    /// Files being converted to ones the dataset scans, `read` of their `total` bytes:
319    /// Arrow IPC streams to one IPC file, or GPS logs to their table.
320    Converting {
321        what: Conversion,
322        read: Arc<AtomicU64>,
323        total: u64,
324    },
325    /// A CSV read with its string columns parsed.
326    ScanningStrings,
327    /// A CSV whose footer rows are dropped (`--footer-rows`): the scan counts every
328    /// row of the file first, which is the wait.
329    CountingFooter,
330    /// The scan; `downloaded` when it reads a download rather than what was named.
331    Scanning {
332        downloaded: bool,
333    },
334    /// The scan of text read as lines, which indexes them.
335    ReadingLines,
336    /// The schema, and whatever the dataset needs before its first rows.
337    ReadingSchema,
338    /// Installed: its first rows are being read. The dataset is the one on screen, and
339    /// what the load held is its now.
340    FirstRows,
341}
342
343impl Phase {
344    /// What the loading screen and the footer call this phase, and the flat
345    /// percentage the bar shows beside it (0 for none).
346    pub(crate) fn label(&self) -> (&str, u16) {
347        match self {
348            Phase::Starting { label, percent } => (label, *percent),
349            Phase::LookingAtPaths => ("Scanning input", 10),
350            Phase::ReadingSpec => ("Reading spec", 5),
351            Phase::LookingAtDirectory => (crate::App::LOOKING_AT_A_DIRECTORY, 5),
352            #[cfg(any(feature = "http", feature = "cloud"))]
353            Phase::ReadingHeaders => ("Reading headers", 20),
354            #[cfg(any(feature = "http", feature = "cloud"))]
355            Phase::CheckingSize { .. } | Phase::Confirming { .. } => ("Checking size", 0),
356            #[cfg(any(feature = "http", feature = "cloud"))]
357            Phase::Downloading => ("Downloading", 20),
358            Phase::Spooling { .. } => ("Reading stdin", 5),
359            Phase::ConfirmingRead { .. } => ("Scanning input", 0),
360            Phase::Decompressing | Phase::DecompressingRecords => ("Decompressing", 30),
361            Phase::ReadingRecords => ("Reading records", 35),
362            Phase::Converting { what, read, total } => {
363                let done = read.load(Ordering::Relaxed).min(*total);
364                // Up to the scan or the schema read that follows.
365                let share = (done * 20).checked_div(*total).unwrap_or(0);
366                (what.label(), 10 + share as u16)
367            }
368            Phase::ScanningStrings => ("Scanning string columns", 55),
369            Phase::CountingFooter => (COUNTING_FOOTER, 10),
370            Phase::Scanning { downloaded: false } => ("Scanning input", 10),
371            Phase::Scanning { downloaded: true } => ("Scanning", 30),
372            Phase::ReadingLines => (READING_LINES, 10),
373            Phase::ReadingSchema => ("Reading schema", 40),
374            Phase::FirstRows => ("Loading buffer", 70),
375        }
376    }
377
378    /// Whether the open waits on the user's answer to a question.
379    fn asks(&self) -> bool {
380        match self {
381            Phase::ConfirmingRead { .. } => true,
382            #[cfg(any(feature = "http", feature = "cloud"))]
383            Phase::Confirming { .. } => true,
384            _ => false,
385        }
386    }
387
388    /// Whether this phase's worker answers with the dataset itself.
389    fn builds_the_dataset(&self) -> bool {
390        match self {
391            Phase::ReadingSchema | Phase::Decompressing => true,
392            #[cfg(any(feature = "http", feature = "cloud"))]
393            Phase::ReadingHeaders => true,
394            _ => false,
395        }
396    }
397
398    /// Whether the open has not started any work of its own yet, so an open asked for
399    /// now carries it on rather than replacing it: the `Open` a key or a look returns.
400    fn starting(&self) -> bool {
401        matches!(
402            self,
403            Phase::Starting { .. } | Phase::LookingAtPaths | Phase::LookingAtDirectory
404        )
405    }
406}
407
408/// A download a load fetched, and the URL it was fetched from: standard input's `-`
409/// for what was piped in.
410#[derive(Clone)]
411struct Fetched {
412    url: PathBuf,
413    file: TempDownload,
414    /// What the listing of a store's Arrow chose, read again with the file: each
415    /// input's place, the split and the `--table` that chose it.
416    arrow: Option<KeptArrow>,
417}
418
419#[derive(Clone)]
420struct KeptArrow {
421    parts: Arc<Vec<crate::formats::ipc_stream::Part>>,
422    splits: Option<Arc<crate::formats::hf_splits::Splits>>,
423    table: Option<String>,
424}
425
426impl Fetched {
427    /// Whether an open with `options` reads what this download holds: not when it
428    /// asks for another split.
429    fn serves(&self, options: &OpenOptions) -> bool {
430        self.arrow
431            .as_ref()
432            .is_none_or(|arrow| arrow.table == options.table)
433    }
434}
435
436/// The open in flight.
437pub(crate) struct Load {
438    id: LoadId,
439    /// Chosen on the home screen, which is where its failure is reported: the dataset
440    /// left over from before is not what the user was looking at when they chose.
441    from_home: bool,
442    phase: Phase,
443    /// What the screen names: the path as asked for (a URL, not the temporary file it
444    /// landed in), and its size on disk.
445    path: Option<PathBuf>,
446    size: u64,
447    /// The paths asked for, which `H` opens again; `None` for a frame handed over.
448    paths: Option<Vec<PathBuf>>,
449    recent: Option<PathBuf>,
450    /// This load's footer counter, and through it its stop flag.
451    progress: Arc<FooterProgress>,
452    /// The stop flag again, for the workers that write files, and where they claim them.
453    writer: Writer,
454    download: Option<Fetched>,
455    /// The IPC files its Arrow streams or GPS logs were converted to, which the
456    /// dataset scans.
457    converted: Vec<TempDownload>,
458    /// See [`OpenRequest::warn_in_memory_above`].
459    warn_in_memory_above: Option<u64>,
460}
461
462/// A scan held while the user is asked about it.
463pub(crate) struct Scan {
464    paths: Vec<PathBuf>,
465    options: OpenOptions,
466    display: Option<PathBuf>,
467}
468
469/// What a read whole into memory would take, put to the user before it starts.
470#[derive(Debug, Clone, PartialEq, Eq)]
471pub(crate) struct InMemory {
472    /// The files' bytes on disk.
473    pub(crate) bytes: u64,
474    pub(crate) format: FileFormat,
475    pub(crate) files: usize,
476    /// The first file, as the loading screen names it.
477    pub(crate) name: PathBuf,
478}
479
480impl Load {
481    pub(crate) fn phase(&self) -> &Phase {
482        &self.phase
483    }
484
485    /// The files the frame about to have its schema read is made from: the download,
486    /// and what a conversion wrote.
487    fn made(&self) -> Made {
488        Made {
489            download: self.download.as_ref().map(|fetched| fetched.file.clone()),
490            converted: self.converted.clone(),
491            ..Made::default()
492        }
493    }
494
495    /// The path the screen names, if it names one.
496    pub(crate) fn path(&self) -> Option<&Path> {
497        self.path.as_deref()
498    }
499
500    /// The size the screen shows beside the path: while standard input is read, what
501    /// has come in so far.
502    pub(crate) fn size(&self) -> u64 {
503        match &self.phase {
504            Phase::Spooling { read } => read.load(Ordering::Relaxed),
505            _ => self.size,
506        }
507    }
508}
509
510/// What the app does next for the open.
511pub(crate) enum Step {
512    /// Nothing: the answer was for an open nobody is waiting on, or the phase waits.
513    Nothing,
514    /// The open cannot be read at all, and the session ends saying why.
515    Crash(String),
516    /// Read the headers of the remote model at `url` as `format`, by range; `writer`
517    /// carries the load's stop flag.
518    #[cfg(any(feature = "http", feature = "cloud"))]
519    ReadHeaders {
520        url: PathBuf,
521        format: FileFormat,
522        options: OpenOptions,
523        writer: Writer,
524    },
525    /// Find the remote file's size.
526    #[cfg(any(feature = "http", feature = "cloud"))]
527    Probe(PendingDownload),
528    /// Ask the user whether to download it.
529    #[cfg(any(feature = "http", feature = "cloud"))]
530    Ask(PendingDownload),
531    /// Ask the user whether to read files this large whole into memory.
532    AskRead(InMemory),
533    /// Download it, writing through `writer`: the load's stop flag, and its claim on
534    /// the file for quitting to find.
535    #[cfg(any(feature = "http", feature = "cloud"))]
536    Download {
537        pending: PendingDownload,
538        writer: Writer,
539    },
540    /// Fetch the spec at `url`, asking `writer`'s stop flag before the request.
541    FetchSpec {
542        url: PathBuf,
543        options: OpenOptions,
544        writer: Writer,
545    },
546    /// Read standard input to a file through `writer`, counting its bytes in `read`.
547    Spool {
548        options: OpenOptions,
549        writer: Writer,
550        read: Arc<AtomicU64>,
551    },
552    /// Decompress the CSV in `file`, writing through `writer`; `path` names it on screen
553    /// and in errors.
554    Decompress {
555        file: PathBuf,
556        path: PathBuf,
557        options: OpenOptions,
558        writer: Writer,
559        /// The download `file` is, given to the dataset built from it.
560        download: Option<TempDownload>,
561    },
562    /// Decompress `file`, which `choice`'s spec reads, to a copy written through
563    /// `writer`; `path` names it on screen and in errors.
564    DecompressRecords {
565        file: PathBuf,
566        path: PathBuf,
567        choice: crate::formats::Choice,
568        options: OpenOptions,
569        writer: Writer,
570    },
571    /// Read the records of `copy`, the decompressed file `path` names, with
572    /// `choice`'s spec.
573    ReadRecords {
574        copy: PathBuf,
575        path: PathBuf,
576        choice: crate::formats::Choice,
577        options: OpenOptions,
578    },
579    /// Convert `files` as `what` says into temp IPC files via `writer`, counting bytes read
580    /// in `read`; `path` names them on screen and in errors.
581    Convert {
582        what: Conversion,
583        files: Vec<PathBuf>,
584        path: Option<PathBuf>,
585        options: OpenOptions,
586        writer: Writer,
587        read: Arc<AtomicU64>,
588    },
589    /// Scan `paths`, saying `status` on the footer; `display` names the dataset when
590    /// what is scanned is a download.
591    Scan {
592        paths: Vec<PathBuf>,
593        options: OpenOptions,
594        display: Option<PathBuf>,
595        status: &'static str,
596    },
597    /// Read the scan's schema, reporting footers to `progress`.
598    ReadSchema {
599        lf: Box<LazyFrame>,
600        path: Option<PathBuf>,
601        options: OpenOptions,
602        progress: Arc<FooterProgress>,
603        /// What the open made the frame from, given to the dataset built from it.
604        made: Made,
605    },
606    /// Install the dataset, then read its first rows.
607    Install(Box<Loaded>),
608    /// The open failed. Its load is retired.
609    Failed(Failed),
610    /// The open found a SQLite database of several tables and was asked for none: the
611    /// home screen lists them. Its load is retired.
612    Tables(Tables),
613    /// The open found a local file no reader and no spec takes, or was asked for its
614    /// bytes: the hex view shows it. Its load is retired.
615    Hex(Hex),
616}
617
618/// A local file to show in the hex view.
619#[derive(Debug)]
620pub(crate) struct Hex {
621    pub(crate) file: PathBuf,
622    /// A download `file` is: the URL it came from and the hold on it.
623    pub(crate) fetched: Option<(PathBuf, TempDownload)>,
624    pub(crate) from_home: bool,
625    /// Asked for (`--hex`) rather than fallen back to.
626    pub(crate) asked: bool,
627    /// `--hex-width`: the bytes a row holds.
628    pub(crate) record_size: Option<usize>,
629}
630
631/// A file of several tables, to be listed on the home screen.
632#[derive(Debug)]
633pub(crate) struct Tables {
634    /// The database file, as the user named it.
635    pub(crate) database: PathBuf,
636    pub(crate) from_home: bool,
637}
638
639/// What a conversion turns into files the dataset scans.
640#[derive(Debug, Clone, Copy, PartialEq, Eq)]
641pub(crate) enum Conversion {
642    /// Arrow IPC streams, into one IPC file; the IPC files among them stay put.
643    Streams,
644    /// GPS logs of this format, each into IPC files of its own, read as one table; or
645    /// a VCD dump, FIX log or SDF file into its own.
646    Text(FileFormat),
647}
648
649impl Conversion {
650    /// What the loading screen calls it.
651    pub(crate) fn label(self) -> &'static str {
652        match self {
653            Conversion::Streams => "Converting Arrow stream",
654            Conversion::Text(format) => format.conversion().label,
655        }
656    }
657
658    /// What the footer says while it runs.
659    pub(crate) fn status(self) -> &'static str {
660        match self {
661            Conversion::Streams => "Converting Arrow stream...",
662            Conversion::Text(format) => format.conversion().status,
663        }
664    }
665}
666
667/// What a conversion wrote. Dropped unused, it removes its files.
668pub(crate) enum Converted {
669    /// The streams in one IPC file, and where each input's rows are: scanned next.
670    Streams {
671        file: TempDownload,
672        parts: Vec<crate::formats::ipc_stream::Part>,
673    },
674    /// The logs' IPC files and the frame over them, which only needs its schema read,
675    /// and what reading them noticed.
676    Frame {
677        files: Vec<TempDownload>,
678        lf: Box<LazyFrame>,
679        notes: Vec<crate::notes::Note>,
680        other_tables: Vec<String>,
681        /// What the file says besides its rows, for the Info panel.
682        detail: Option<Arc<crate::formats::text_formats::Detail>>,
683    },
684}
685
686/// The files an open made or fetched for the frame it scans, and what making them
687/// found: handed to the dataset as it is built, which holds the files from then on.
688#[derive(Default)]
689pub(crate) struct Made {
690    /// The download the frame reads.
691    pub(crate) download: Option<TempDownload>,
692    /// The files a conversion wrote.
693    pub(crate) converted: Vec<TempDownload>,
694    pub(crate) notes: Vec<crate::notes::Note>,
695    pub(crate) other_tables: Vec<String>,
696    pub(crate) detail: Option<Arc<crate::formats::text_formats::Detail>>,
697}
698
699/// A failed open: why, and whether it was chosen on the home screen.
700#[derive(Debug)]
701pub(crate) struct Failed {
702    pub(crate) message: String,
703    pub(crate) from_home: bool,
704}
705
706/// A dataset read and ready to install, with everything the load hands over to it.
707pub(crate) struct Loaded {
708    pub(crate) state: DataTableState,
709    /// What names the dataset: the path or URL asked for.
710    pub(crate) path: Option<PathBuf>,
711    pub(crate) options: OpenOptions,
712    pub(crate) debug_label: Option<String>,
713    /// The paths asked for, which `H` opens again; `None` for a frame handed over.
714    pub(crate) paths: Option<Vec<PathBuf>>,
715    /// Recorded as a recent once installed.
716    pub(crate) recent: Option<PathBuf>,
717    pub(crate) from_home: bool,
718    /// The footer counter the dataset's own pass reports to from now on.
719    pub(crate) footers: Arc<FooterProgress>,
720}
721
722/// What a phase's worker found.
723pub(crate) enum LoadAnswer {
724    /// The scan's frame; `path` names the dataset.
725    Scanned {
726        lf: Box<LazyFrame>,
727        path: Option<PathBuf>,
728        options: OpenOptions,
729    },
730    /// The scan found one compressed delimited file, `file`, to decompress first.
731    Compressed {
732        file: PathBuf,
733        path: Option<PathBuf>,
734        options: OpenOptions,
735    },
736    /// The spec a `--format` URL names, and the open's options to carry it in.
737    SpecFetched {
738        spec: Arc<crate::formats::Spec>,
739        options: OpenOptions,
740    },
741    /// The scan found a compressed file, `file`, that `choice`'s spec reads once it is
742    /// decompressed.
743    CompressedRecords {
744        file: PathBuf,
745        path: Option<PathBuf>,
746        choice: crate::formats::Choice,
747        options: OpenOptions,
748    },
749    /// The decompressed copy of a file a spec reads. Dropped unused, it removes itself.
750    DecompressedRecords {
751        copy: TempDownload,
752        path: PathBuf,
753        choice: crate::formats::Choice,
754        options: OpenOptions,
755    },
756    /// The scan found `files`, `bytes` in all as stored, which have to be converted as
757    /// `what` says before they can be read: Arrow IPC streams, or GPS logs.
758    Convert {
759        what: Conversion,
760        files: Vec<PathBuf>,
761        bytes: u64,
762        path: Option<PathBuf>,
763        options: OpenOptions,
764    },
765    /// What the conversion wrote. Dropped unused, it removes its files.
766    Converted {
767        converted: Converted,
768        path: Option<PathBuf>,
769        options: OpenOptions,
770    },
771    /// The scan found a file of several tables (a SQLite database, a NumPy archive),
772    /// `file`, its tables named `tables`, and no `--table` to say which.
773    Tables {
774        file: PathBuf,
775        tables: Vec<String>,
776        path: Option<PathBuf>,
777    },
778    /// The scan found a local file no reader and no spec takes, or `--hex` asked for
779    /// its bytes.
780    Hex {
781        file: PathBuf,
782        asked: bool,
783        record_size: Option<usize>,
784    },
785    /// The dataset, its schema read.
786    SchemaRead {
787        state: Box<DataTableState>,
788        path: Option<PathBuf>,
789        options: OpenOptions,
790        debug_label: Option<String>,
791    },
792    /// The remote model's server sends whole files, not ranges: it is downloaded.
793    #[cfg(any(feature = "http", feature = "cloud"))]
794    NoRanges { options: OpenOptions },
795    /// The remote file's size.
796    #[cfg(any(feature = "http", feature = "cloud"))]
797    Sized(PendingDownload),
798    /// An object in a store named with no format is Parquet by its first bytes, or is
799    /// no object but a prefix (`path`, with its slash): read where it is, not
800    /// downloaded.
801    #[cfg(feature = "cloud")]
802    InPlace { path: PathBuf, options: OpenOptions },
803    /// A download started without asking passed its limit and was stopped, its
804    /// partial file removed: the user is asked before it is fetched whole.
805    #[cfg(any(feature = "http", feature = "cloud"))]
806    PastLimit(PendingDownload),
807    /// The remote file, downloaded. Dropped unused, it removes the file.
808    #[cfg(any(feature = "http", feature = "cloud"))]
809    Downloaded {
810        download: TempDownload,
811        options: OpenOptions,
812    },
813    /// Standard input, read to a file, and `options` with the format it holds.
814    Spooled {
815        download: TempDownload,
816        options: OpenOptions,
817    },
818    /// Standard input, recorded to the file `--tee` named, which is read from here on
819    /// as any file is.
820    Recorded { file: PathBuf, options: OpenOptions },
821}
822
823/// A load put down before it finished: which, and what of it the app has to put down.
824#[derive(Debug, Clone, Copy)]
825pub(crate) struct Retired {
826    pub(crate) id: LoadId,
827    /// It was asking the user about a download: the question goes with it.
828    pub(crate) asking: bool,
829}
830
831/// The owner of the open in flight, and of the last download, kept to be read again.
832#[derive(Default)]
833pub(crate) struct Loader {
834    next_id: u64,
835    load: Option<Load>,
836    /// The last remote file an installed open downloaded, kept so reopening the URL (`H`)
837    /// reads it instead of downloading again; released when other data opens, removed
838    /// once the scanning dataset also lets go. Stdin, read once, is kept the same way.
839    kept: Option<Fetched>,
840    /// The files every load's workers have written and not yet let go, for quitting to
841    /// remove. See [`crate::loading::unfinished`].
842    unfinished: Unfinished,
843}
844
845impl Loader {
846    /// The files this loader's opens are writing, swept when the app exits.
847    pub(crate) fn unfinished(&self) -> &Unfinished {
848        &self.unfinished
849    }
850
851    /// The open in flight, if there is one.
852    pub(crate) fn current(&self) -> Option<&Load> {
853        self.load.as_ref()
854    }
855
856    pub(crate) fn id(&self) -> Option<LoadId> {
857        self.load.as_ref().map(|load| load.id)
858    }
859
860    fn current_in(&self, id: LoadId, phase: impl Fn(&Phase) -> bool) -> bool {
861        self.load
862            .as_ref()
863            .is_some_and(|load| load.id == id && phase(&load.phase))
864    }
865
866    /// Whether `id` is still looking at the paths named on the command line.
867    pub(crate) fn looking_at_paths(&self, id: LoadId) -> bool {
868        self.current_in(id, |phase| matches!(phase, Phase::LookingAtPaths))
869    }
870
871    /// Whether `id` is still looking at a directory named on the command line.
872    pub(crate) fn looking_at_directory(&self, id: LoadId) -> bool {
873        self.current_in(id, |phase| matches!(phase, Phase::LookingAtDirectory))
874    }
875
876    /// Whether an open is on its way and its dataset is not installed yet. Whatever
877    /// table is up meanwhile belongs to the dataset being replaced.
878    pub(crate) fn awaiting_dataset(&self) -> bool {
879        self.load
880            .as_ref()
881            .is_some_and(|load| !matches!(load.phase, Phase::FirstRows))
882    }
883
884    /// Whether the user waits on the open (keys held): not while it asks about a download
885    /// (the question takes the keys), nor once the dataset is up.
886    pub(crate) fn waits(&self) -> bool {
887        self.awaiting_dataset() && !self.asking()
888    }
889
890    /// Whether the open is waiting on the user to agree to a download.
891    pub(crate) fn asking(&self) -> bool {
892        self.load.as_ref().is_some_and(|load| load.phase.asks())
893    }
894
895    /// Give the question being asked the hold that keeps the generation while it waits.
896    pub(crate) fn hold_while_asking(&mut self, hold: Hold) {
897        if let Some(Load {
898            phase: Phase::ConfirmingRead { _hold, .. },
899            ..
900        }) = self.load.as_mut()
901        {
902            *_hold = Some(hold);
903        }
904    }
905
906    /// Why the download being asked about is a download, when it would not have been.
907    #[cfg(any(feature = "http", feature = "cloud"))]
908    pub(crate) fn download_note(&self) -> Option<&'static str> {
909        match self.load.as_ref().map(|load| &load.phase) {
910            Some(Phase::Confirming { note, .. }) => *note,
911            _ => None,
912        }
913    }
914
915    /// The footer counter of the open that has not installed yet: what the loading
916    /// screen counts.
917    pub(crate) fn progress(&self) -> Option<&Arc<FooterProgress>> {
918        self.load
919            .as_ref()
920            .filter(|load| !matches!(load.phase, Phase::FirstRows))
921            .map(|load| &load.progress)
922    }
923
924    /// Put down the open in flight, if there is one, and what it holds. Its stop flag is
925    /// raised unless its dataset is installed, when the counter is the dataset's.
926    pub(crate) fn retire(&mut self) -> Option<Retired> {
927        let load = self.load.take()?;
928        if !matches!(load.phase, Phase::FirstRows) {
929            load.progress.cancel();
930        }
931        Some(Retired {
932            id: load.id,
933            asking: load.phase.asks(),
934        })
935    }
936
937    /// Make way for an open being asked for: the load in flight is retired, unless it
938    /// has not started work of its own yet, in which case the open carries it on.
939    pub(crate) fn make_way(&mut self) -> Option<Retired> {
940        if self.load.as_ref().is_some_and(|load| load.phase.starting()) {
941            return None;
942        }
943        self.retire()
944    }
945
946    /// The load an open being asked for belongs to: the starting one, or a new one.
947    fn start(&mut self, from_home: bool) -> &mut Load {
948        // A caller that did not make way would leave a load doing work behind with its
949        // stop flag down; retired here so its download and its footer pass stop.
950        if self
951            .load
952            .as_ref()
953            .is_some_and(|load| !load.phase.starting())
954        {
955            debug_assert!(false, "an open started without making way");
956            self.retire();
957        }
958        if self.load.is_none() {
959            self.next_id = self.next_id.wrapping_add(1);
960            let progress = Arc::<FooterProgress>::default();
961            self.load = Some(Load {
962                id: LoadId(self.next_id),
963                from_home,
964                phase: Phase::Starting {
965                    label: "Loading".to_string(),
966                    percent: 0,
967                },
968                path: None,
969                size: 0,
970                paths: None,
971                recent: None,
972                writer: self.unfinished.writer(progress.cancel_flag()),
973                progress,
974                download: None,
975                converted: Vec::new(),
976                warn_in_memory_above: None,
977            });
978        }
979        let load = self.load.as_mut().expect("started just above");
980        load.from_home |= from_home;
981        load
982    }
983
984    /// An open is on its way, asked for from the home screen when `from_home`: the
985    /// screen is handed over to it now, saying `label`.
986    pub(crate) fn announce(&mut self, from_home: bool, label: String, percent: u16) -> LoadId {
987        let load = self.start(from_home);
988        load.phase = Phase::Starting { label, percent };
989        load.id
990    }
991
992    /// An open whose dataset is up and whose first rows are being read, with nothing
993    /// running: for tests of what ends that wait.
994    #[cfg(test)]
995    pub(crate) fn first_rows_for_tests(&mut self) {
996        self.retire();
997        self.start(false).phase = Phase::FirstRows;
998    }
999
1000    /// Give the starting load's path a size, as an open asking the filesystem does.
1001    #[cfg(test)]
1002    pub(crate) fn size_for_tests(&mut self, size: u64) {
1003        if let Some(load) = self.load.as_mut() {
1004            load.size = size;
1005        }
1006    }
1007
1008    /// Put `path` on the loading screen, so a wait says what it is waiting for.
1009    pub(crate) fn name(&mut self, path: PathBuf) {
1010        if let Some(load) = self
1011            .load
1012            .as_mut()
1013            .filter(|load| !matches!(load.phase, Phase::FirstRows))
1014        {
1015            load.path = Some(stdin::named(&path));
1016        }
1017    }
1018
1019    /// The paths named on the command line are looked at before they are opened.
1020    pub(crate) fn look_at_paths(&mut self) -> LoadId {
1021        let load = self.start(false);
1022        load.phase = Phase::LookingAtPaths;
1023        load.id
1024    }
1025
1026    /// A directory named on the command line is looked at before it is opened.
1027    pub(crate) fn look_at_directory(&mut self, dir: PathBuf) -> LoadId {
1028        let load = self.start(false);
1029        load.phase = Phase::LookingAtDirectory;
1030        load.path = Some(dir);
1031        load.id
1032    }
1033
1034    /// Open `request`: carry on the starting load, or begin one. The last download is
1035    /// let go unless this opens it again.
1036    pub(crate) fn open(&mut self, request: OpenRequest) -> Step {
1037        if !(request.paths.len() == 1
1038            && self
1039                .kept
1040                .as_ref()
1041                .is_some_and(|kept| kept.url == request.paths[0]))
1042        {
1043            self.kept = None;
1044        }
1045        let OpenRequest {
1046            paths,
1047            mut options,
1048            size,
1049            recent,
1050            shown,
1051            warn_in_memory_above,
1052        } = request;
1053        // What an earlier load found of its Arrow is not this one's to read.
1054        options.arrow_parts = None;
1055        let prepared = options
1056            .prepared
1057            .take()
1058            .and_then(|handoff| handoff.lock().ok()?.take());
1059        let load = self.start(false);
1060        load.path = Some(shown.unwrap_or_else(|| stdin::named(&paths[0])));
1061        load.size = size;
1062        load.recent = recent;
1063        load.paths = Some(paths.clone());
1064        load.warn_in_memory_above = warn_in_memory_above;
1065        match prepared {
1066            Some(prepared) => self.install_prepared(*prepared),
1067            None => self.first_step(paths, options),
1068        }
1069    }
1070
1071    /// Install the dataset home's preview built: scan, schema and first page are read, so
1072    /// the open goes straight to its (on-hand) first rows.
1073    fn install_prepared(&mut self, prepared: crate::home::home_preview::Prepared) -> Step {
1074        let crate::home::home_preview::Prepared {
1075            state,
1076            options,
1077            debug_label,
1078            progress,
1079        } = prepared;
1080        let writer = self.unfinished.writer(progress.cancel_flag());
1081        let load = self.load.as_mut().expect("started by the open");
1082        load.phase = Phase::FirstRows;
1083        // The dataset was built reporting to the preview's counter, which is the one
1084        // its own footer pass goes on with.
1085        load.progress = progress;
1086        load.writer = writer;
1087        Step::Install(Box::new(Loaded {
1088            state: *state,
1089            path: load.path.clone(),
1090            options,
1091            debug_label,
1092            paths: load.paths.clone(),
1093            recent: load.recent.clone(),
1094            from_home: load.from_home,
1095            footers: load.progress.clone(),
1096        }))
1097    }
1098
1099    /// Open a frame handed over (the Python binding's): only its schema is read.
1100    pub(crate) fn open_frame(&mut self, lf: LazyFrame, options: OpenOptions) -> Step {
1101        let load = self.start(false);
1102        load.path = None;
1103        load.size = 0;
1104        load.paths = None;
1105        load.recent = None;
1106        load.phase = Phase::ReadingSchema;
1107        Step::ReadSchema {
1108            lf: Box::new(lf),
1109            path: None,
1110            options,
1111            progress: load.progress.clone(),
1112            made: Made::default(),
1113        }
1114    }
1115
1116    /// What the open of `paths` does first: read standard input, decompress, download,
1117    /// or scan.
1118    fn first_step(&mut self, paths: Vec<PathBuf>, options: OpenOptions) -> Step {
1119        let first = paths[0].clone();
1120        let src = source::input_source(&first);
1121        if paths.len() > 1 {
1122            let only_one = match &src {
1123                source::InputSource::S3(_) => {
1124                    Some("Only one S3 URL at a time. Open a single s3:// path.")
1125                }
1126                source::InputSource::Gcs(_) => {
1127                    Some("Only one GCS URL at a time. Open a single gs:// path.")
1128                }
1129                source::InputSource::Azure(_) => {
1130                    Some("Only one Azure URL at a time. Open a single abfss:// path.")
1131                }
1132                source::InputSource::Http(_) => {
1133                    Some("Only one HTTP/HTTPS URL at a time. Open a single URL.")
1134                }
1135                source::InputSource::Local(_) => None,
1136            };
1137            if let Some(message) = only_one {
1138                self.load = None;
1139                return Step::Crash(message.to_string());
1140            }
1141        }
1142        if let Some(message) = stdin::refuse(&paths, true) {
1143            self.load = None;
1144            return Step::Crash(message.to_string());
1145        }
1146        if options.follow
1147            && let Some(message) = crate::loading::follow::refuse_paths(&paths, &options)
1148        {
1149            self.load = None;
1150            return Step::Crash(message);
1151        }
1152        // A remote spec is fetched once, before any phase reads with it; the open
1153        // then starts again from here with it in hand.
1154        if let Some(url) = options
1155            .spec_file
1156            .clone()
1157            .filter(|file| options.spec_fetched.is_none() && source::is_remote_url(file))
1158        {
1159            let load = self.load.as_mut().expect("an open has a load");
1160            load.phase = Phase::ReadingSpec;
1161            return Step::FetchSpec {
1162                url,
1163                options,
1164                writer: load.writer.clone(),
1165            };
1166        }
1167        if stdin::is_stdin(&first) {
1168            // Read once: opened again (`H`), the copy on hand is read.
1169            if let Some(kept) = self.kept.clone().filter(|kept| {
1170                kept.url == first && kept.file.path().exists() && kept.serves(&options)
1171            }) {
1172                return self.read_download(kept, options);
1173            }
1174            let load = self.load.as_mut().expect("an open has a load");
1175            let read = Arc::<AtomicU64>::default();
1176            load.phase = Phase::Spooling { read: read.clone() };
1177            return Step::Spool {
1178                options,
1179                writer: load.writer.clone(),
1180                read,
1181            };
1182        }
1183        let load = self.load.as_mut().expect("an open has a load");
1184        let compression = options
1185            .compression
1186            .or_else(|| CompressionFormat::from_extension(&first));
1187        let delimited = delimited_format(&first, &options);
1188        if matches!(src, source::InputSource::Local(_))
1189            && paths.len() == 1
1190            && compression.is_some()
1191            && let Some(format) = delimited
1192        {
1193            load.phase = Phase::Decompressing;
1194            return Step::Decompress {
1195                file: first.clone(),
1196                path: first,
1197                options: OpenOptions {
1198                    format: Some(format),
1199                    ..options
1200                },
1201                writer: load.writer.clone(),
1202                download: None,
1203            };
1204        }
1205        // Opened again, and downloaded already: read the copy on hand.
1206        #[cfg(any(feature = "http", feature = "cloud"))]
1207        if let Some(kept) = self
1208            .kept
1209            .clone()
1210            .filter(|kept| paths.len() == 1 && kept.file.path().exists() && kept.serves(&options))
1211        {
1212            return self.read_download(kept, options);
1213        }
1214        // A remote model's headers are all it needs: read by range, not downloaded.
1215        #[cfg(any(feature = "http", feature = "cloud"))]
1216        if paths.len() == 1
1217            && let Some(format) = crate::cloud::remote_model::model_format(&first, options.format)
1218        {
1219            let load = self.load.as_mut().expect("an open has a load");
1220            load.phase = Phase::ReadingHeaders;
1221            return Step::ReadHeaders {
1222                url: first,
1223                format,
1224                options,
1225                writer: load.writer.clone(),
1226            };
1227        }
1228        #[cfg(any(feature = "http", feature = "cloud"))]
1229        if let Some(pending) = remote_download(&src, &options) {
1230            let load = self.load.as_mut().expect("an open has a load");
1231            load.phase = Phase::CheckingSize { note: None };
1232            return Step::Probe(pending);
1233        }
1234        let load = self.load.as_mut().expect("an open has a load");
1235        if counts_footer(&paths, &options) {
1236            load.phase = Phase::CountingFooter;
1237            let display = load.path.clone().filter(|shown| *shown != paths[0]);
1238            return Step::Scan {
1239                paths,
1240                options,
1241                display,
1242                status: COUNTING_FOOTER_STATUS,
1243            };
1244        }
1245        if paths.len() == 1 && delimited.is_some() && options.parse_strings.is_some() {
1246            load.phase = Phase::ScanningStrings;
1247            return Step::Scan {
1248                paths,
1249                options,
1250                display: None,
1251                status: "Scanning string columns...",
1252            };
1253        }
1254        // A table inside a database goes by its path there, on screen and once open.
1255        let display = load.path.clone().filter(|shown| *shown != paths[0]);
1256        // A large file read whole is put to the user before the read starts.
1257        if let Some(limit) = load.warn_in_memory_above
1258            && let Some(read) = in_memory(&paths, &options)
1259            && read.bytes > limit
1260        {
1261            load.phase = Phase::ConfirmingRead {
1262                scan: Box::new(Scan {
1263                    paths,
1264                    options,
1265                    display,
1266                }),
1267                _hold: None,
1268            };
1269            return Step::AskRead(read);
1270        }
1271        if reads_lines(&paths, &options) {
1272            load.phase = Phase::ReadingLines;
1273            return Step::Scan {
1274                paths,
1275                options,
1276                display,
1277                status: READING_LINES_STATUS,
1278            };
1279        }
1280        load.phase = Phase::Scanning { downloaded: false };
1281        Step::Scan {
1282            paths,
1283            options,
1284            display,
1285            status: "Scanning input...",
1286        }
1287    }
1288
1289    /// Read a download: decompress compressed CSV, TSV or PSV first, else scan it, named by
1290    /// the URL (or `stdin`), not the temp file.
1291    fn read_download(&mut self, fetched: Fetched, mut options: OpenOptions) -> Step {
1292        if let Some(arrow) = &fetched.arrow {
1293            options.format = Some(FileFormat::Arrow);
1294            options.hive = false;
1295            options.arrow_parts = Some(arrow.parts.clone());
1296            options.splits = arrow.splits.clone();
1297        }
1298        let load = self.load.as_mut().expect("a download read has a load");
1299        let file = fetched.file.path().to_path_buf();
1300        let url = stdin::named(&fetched.url);
1301        let download = fetched.file.clone();
1302        load.download = Some(fetched);
1303        // Compressed delimited text must be decompressed before scanning, as from disk.
1304        let compressed = options
1305            .compression
1306            .or_else(|| CompressionFormat::from_extension(&file))
1307            .is_some();
1308        if compressed && let Some(format) = delimited_format(&file, &options) {
1309            load.phase = Phase::Decompressing;
1310            return Step::Decompress {
1311                file,
1312                path: url,
1313                options: OpenOptions {
1314                    format: Some(format),
1315                    ..options
1316                },
1317                writer: load.writer.clone(),
1318                download: Some(download),
1319            };
1320        }
1321        let paths = vec![file];
1322        let status = if counts_footer(&paths, &options) {
1323            load.phase = Phase::CountingFooter;
1324            COUNTING_FOOTER_STATUS
1325        } else if reads_lines(&paths, &options) {
1326            load.phase = Phase::ReadingLines;
1327            READING_LINES_STATUS
1328        } else {
1329            load.phase = Phase::Scanning { downloaded: true };
1330            "Scanning..."
1331        };
1332        Step::Scan {
1333            paths,
1334            options,
1335            display: Some(url),
1336            status,
1337        }
1338    }
1339
1340    /// A worker of `id` answered. Taken only while `id` is in flight and in the phase
1341    /// that asked; otherwise the answer, and whatever it carries, is dropped.
1342    pub(crate) fn answered(
1343        &mut self,
1344        id: LoadId,
1345        answer: LoadAnswer,
1346        #[cfg(any(feature = "http", feature = "cloud"))] jobs: &Jobs,
1347    ) -> Step {
1348        let Some(load) = self.load.as_mut().filter(|load| load.id == id) else {
1349            return Step::Nothing;
1350        };
1351        match (answer, &load.phase) {
1352            (
1353                LoadAnswer::Scanned { lf, path, options },
1354                Phase::Scanning { .. }
1355                | Phase::ScanningStrings
1356                | Phase::CountingFooter
1357                | Phase::ReadingRecords
1358                | Phase::ReadingLines,
1359            ) => {
1360                load.phase = Phase::ReadingSchema;
1361                Step::ReadSchema {
1362                    lf,
1363                    path,
1364                    options,
1365                    progress: load.progress.clone(),
1366                    made: load.made(),
1367                }
1368            }
1369            (
1370                LoadAnswer::Convert {
1371                    what,
1372                    files,
1373                    bytes,
1374                    path,
1375                    options,
1376                },
1377                Phase::Scanning { .. }
1378                | Phase::ScanningStrings
1379                | Phase::CountingFooter
1380                | Phase::ReadingLines,
1381            ) if load.converted.is_empty() => {
1382                let read = Arc::<AtomicU64>::default();
1383                load.phase = Phase::Converting {
1384                    what,
1385                    read: read.clone(),
1386                    total: bytes,
1387                };
1388                Step::Convert {
1389                    what,
1390                    files,
1391                    path,
1392                    options,
1393                    writer: load.writer.clone(),
1394                    read,
1395                }
1396            }
1397            (
1398                LoadAnswer::Converted {
1399                    converted: Converted::Streams { file, parts },
1400                    path,
1401                    options,
1402                },
1403                Phase::Converting { .. },
1404            ) => {
1405                let paths = vec![file.path().to_path_buf()];
1406                // A converted download or pipe spool is kept as its copy, to reread without
1407                // converting; the stream is released.
1408                if let Some(fetched) = load.download.as_mut() {
1409                    fetched.file = file.clone();
1410                }
1411                load.converted = vec![file];
1412                load.phase = Phase::Scanning { downloaded: true };
1413                Step::Scan {
1414                    paths,
1415                    options: OpenOptions {
1416                        format: Some(FileFormat::Arrow),
1417                        hive: false,
1418                        arrow_parts: Some(Arc::new(parts)),
1419                        ..options
1420                    },
1421                    display: path,
1422                    status: "Scanning...",
1423                }
1424            }
1425            (
1426                LoadAnswer::Converted {
1427                    converted:
1428                        Converted::Frame {
1429                            files,
1430                            lf,
1431                            notes,
1432                            other_tables,
1433                            detail,
1434                        },
1435                    path,
1436                    options,
1437                },
1438                Phase::Converting { .. },
1439            ) => {
1440                load.converted = files;
1441                load.phase = Phase::ReadingSchema;
1442                Step::ReadSchema {
1443                    lf,
1444                    path,
1445                    options,
1446                    progress: load.progress.clone(),
1447                    made: Made {
1448                        notes,
1449                        other_tables,
1450                        detail,
1451                        ..load.made()
1452                    },
1453                }
1454            }
1455            (
1456                LoadAnswer::Compressed {
1457                    file,
1458                    path,
1459                    options,
1460                },
1461                Phase::Scanning { .. }
1462                | Phase::ScanningStrings
1463                | Phase::CountingFooter
1464                | Phase::ReadingLines,
1465            ) => {
1466                load.phase = Phase::Decompressing;
1467                Step::Decompress {
1468                    path: path.unwrap_or_else(|| file.clone()),
1469                    file,
1470                    options,
1471                    writer: load.writer.clone(),
1472                    download: load.download.as_ref().map(|fetched| fetched.file.clone()),
1473                }
1474            }
1475            (LoadAnswer::SpecFetched { spec, options }, Phase::ReadingSpec) => {
1476                let paths = load.paths.clone().unwrap_or_default();
1477                if paths.is_empty() {
1478                    return Step::Nothing;
1479                }
1480                self.first_step(
1481                    paths,
1482                    OpenOptions {
1483                        spec_fetched: Some(spec),
1484                        ..options
1485                    },
1486                )
1487            }
1488            (
1489                LoadAnswer::CompressedRecords {
1490                    file,
1491                    path,
1492                    choice,
1493                    options,
1494                },
1495                Phase::Scanning { .. } | Phase::ScanningStrings | Phase::ReadingLines,
1496            ) => {
1497                load.phase = Phase::DecompressingRecords;
1498                Step::DecompressRecords {
1499                    path: path.unwrap_or_else(|| file.clone()),
1500                    file,
1501                    choice,
1502                    options,
1503                    writer: load.writer.clone(),
1504                }
1505            }
1506            (
1507                LoadAnswer::DecompressedRecords {
1508                    copy,
1509                    path,
1510                    choice,
1511                    options,
1512                },
1513                Phase::DecompressingRecords,
1514            ) => {
1515                // The load holds the copy until the dataset built from it does.
1516                let file = copy.path().to_path_buf();
1517                load.converted = vec![copy];
1518                load.phase = Phase::ReadingRecords;
1519                Step::ReadRecords {
1520                    copy: file,
1521                    path,
1522                    choice,
1523                    options,
1524                }
1525            }
1526            (
1527                LoadAnswer::Hex {
1528                    file,
1529                    asked,
1530                    record_size,
1531                },
1532                Phase::Scanning { .. }
1533                | Phase::ScanningStrings
1534                | Phase::CountingFooter
1535                | Phase::ReadingLines,
1536            ) => {
1537                let from_home = load.from_home;
1538                // A download is shown as a local file is, the view holding it; kept too,
1539                // so reading it with a spec reads the copy on hand.
1540                let fetched = load.download.take();
1541                self.retire();
1542                if let Some(fetched) = &fetched {
1543                    self.kept = Some(fetched.clone());
1544                }
1545                Step::Hex(Hex {
1546                    file,
1547                    fetched: fetched.map(|fetched| (fetched.url, fetched.file)),
1548                    from_home,
1549                    asked,
1550                    record_size,
1551                })
1552            }
1553            (
1554                LoadAnswer::Tables { file, tables, path },
1555                Phase::Scanning { .. }
1556                | Phase::ScanningStrings
1557                | Phase::CountingFooter
1558                | Phase::ReadingLines,
1559            ) => {
1560                let from_home = load.from_home;
1561                let database = path.unwrap_or(file);
1562                // A download or standard input has no place on the home screen to list
1563                // the tables at: the table is named on the command line instead.
1564                let fetched = load.download.is_some();
1565                self.retire();
1566                if fetched {
1567                    let shown = tables.iter().take(20).cloned().collect::<Vec<_>>();
1568                    let more = tables.len().saturating_sub(shown.len());
1569                    let more = match more {
1570                        0 => String::new(),
1571                        n => format!(" and {n} more"),
1572                    };
1573                    return Step::Failed(Failed {
1574                        message: format!(
1575                            "{} holds {} tables: {}{more}. Open one with --table NAME.",
1576                            database.display(),
1577                            tables.len(),
1578                            shown.join(", ")
1579                        ),
1580                        from_home,
1581                    });
1582                }
1583                Step::Tables(Tables {
1584                    database,
1585                    from_home,
1586                })
1587            }
1588            (
1589                LoadAnswer::SchemaRead {
1590                    state,
1591                    path,
1592                    options,
1593                    debug_label,
1594                },
1595                phase,
1596            ) if phase.builds_the_dataset() => {
1597                load.phase = Phase::FirstRows;
1598                // The dataset was built holding its download (`Step::ReadSchema`); the
1599                // loader keeps it too, to be read again.
1600                if let Some(fetched) = load.download.take() {
1601                    self.kept = Some(fetched);
1602                }
1603                let state = *state;
1604                let load = self.load.as_ref().expect("installing its load");
1605                Step::Install(Box::new(Loaded {
1606                    state,
1607                    path,
1608                    options,
1609                    debug_label,
1610                    paths: load.paths.clone(),
1611                    recent: load.recent.clone(),
1612                    from_home: load.from_home,
1613                    footers: load.progress.clone(),
1614                }))
1615            }
1616            // Arrow in a store with no stream in it: nothing to download or ask about.
1617            #[cfg(feature = "cloud")]
1618            (
1619                LoadAnswer::Sized(PendingDownload::Arrow {
1620                    url,
1621                    objects,
1622                    options,
1623                    ..
1624                }),
1625                Phase::CheckingSize { .. },
1626            ) if crate::cloud::cloud_arrow::in_place(&objects).is_some() => {
1627                load.phase = Phase::Scanning { downloaded: false };
1628                let parts = crate::cloud::cloud_arrow::in_place(&objects).unwrap_or_default();
1629                Step::Scan {
1630                    paths: vec![PathBuf::from(url)],
1631                    options: OpenOptions {
1632                        format: Some(FileFormat::Arrow),
1633                        hive: false,
1634                        arrow_parts: Some(Arc::new(parts)),
1635                        ..options
1636                    },
1637                    display: None,
1638                    status: "Scanning...",
1639                }
1640            }
1641            // An unnamed object whose first bytes say Parquet, or a prefix: scanned where
1642            // it is.
1643            #[cfg(feature = "cloud")]
1644            (LoadAnswer::InPlace { path, options }, Phase::CheckingSize { .. }) => {
1645                load.phase = Phase::Scanning { downloaded: false };
1646                let display = load.path.clone().filter(|shown| *shown != path);
1647                Step::Scan {
1648                    paths: vec![path],
1649                    options,
1650                    display,
1651                    status: "Scanning...",
1652                }
1653            }
1654            #[cfg(any(feature = "http", feature = "cloud"))]
1655            (LoadAnswer::NoRanges { options }, Phase::ReadingHeaders) => {
1656                let first = load.paths.as_ref().and_then(|paths| paths.first().cloned());
1657                match first
1658                    .and_then(|first| remote_download(&source::input_source(&first), &options))
1659                {
1660                    Some(pending) => {
1661                        load.phase = Phase::CheckingSize {
1662                            note: Some(NO_RANGES),
1663                        };
1664                        Step::Probe(pending)
1665                    }
1666                    None => self.failed(id, NO_RANGES),
1667                }
1668            }
1669            // A small file of the built-in catalog: its row said what it is and what it
1670            // weighs, so it is fetched without a question.
1671            #[cfg(any(feature = "http", feature = "cloud"))]
1672            (LoadAnswer::Sized(pending), Phase::CheckingSize { note: None })
1673                if pending
1674                    .parts()
1675                    .2
1676                    .download_unasked
1677                    .is_some_and(|unasked| unasked.covers(pending.parts().1)) =>
1678            {
1679                load.phase = Phase::Downloading;
1680                Step::Download {
1681                    pending,
1682                    writer: load.writer.clone(),
1683                }
1684            }
1685            #[cfg(any(feature = "http", feature = "cloud"))]
1686            (LoadAnswer::Sized(pending), Phase::CheckingSize { note }) => {
1687                load.phase = Phase::Confirming {
1688                    pending: Box::new(pending.clone()),
1689                    note: *note,
1690                    _hold: jobs.hold(),
1691                };
1692                Step::Ask(pending)
1693            }
1694            // More arrived than the catalog said: asked once, as any large download is.
1695            // Its size is unknown now, whatever the server said.
1696            #[cfg(any(feature = "http", feature = "cloud"))]
1697            (LoadAnswer::PastLimit(pending), Phase::Downloading) => {
1698                let pending = pending.with_size(None);
1699                load.phase = Phase::Confirming {
1700                    pending: Box::new(pending.clone()),
1701                    note: Some(PAST_LIMIT),
1702                    _hold: jobs.hold(),
1703                };
1704                Step::Ask(pending)
1705            }
1706            #[cfg(any(feature = "http", feature = "cloud"))]
1707            (LoadAnswer::Downloaded { download, options }, Phase::Downloading) => {
1708                let fetched = Fetched {
1709                    // The URL the open was asked for, which is what opening it again names.
1710                    url: load.path.clone().unwrap_or_default(),
1711                    file: download,
1712                    arrow: options.arrow_parts.clone().map(|parts| KeptArrow {
1713                        parts,
1714                        splits: options.splits.clone(),
1715                        table: options.table.clone(),
1716                    }),
1717                };
1718                self.read_download(fetched, options)
1719            }
1720            (LoadAnswer::Recorded { file, options }, Phase::Spooling { read }) => {
1721                load.size = read.load(Ordering::Relaxed);
1722                // The user's file, not a copy: `H` reads it again, and it is a recent.
1723                self.kept = None;
1724                load.path = Some(file.clone());
1725                load.paths = Some(vec![file.clone()]);
1726                load.recent = Some(file.clone());
1727                let paths = vec![file];
1728                let status = if counts_footer(&paths, &options) {
1729                    load.phase = Phase::CountingFooter;
1730                    COUNTING_FOOTER_STATUS
1731                } else {
1732                    load.phase = Phase::Scanning { downloaded: false };
1733                    "Scanning input..."
1734                };
1735                Step::Scan {
1736                    paths,
1737                    options,
1738                    display: None,
1739                    status,
1740                }
1741            }
1742            (LoadAnswer::Spooled { download, options }, Phase::Spooling { read }) => {
1743                load.size = read.load(Ordering::Relaxed);
1744                let fetched = Fetched {
1745                    url: PathBuf::from(stdin::PATH),
1746                    file: download,
1747                    arrow: None,
1748                };
1749                self.read_download(fetched, options)
1750            }
1751            _ => Step::Nothing,
1752        }
1753    }
1754
1755    /// A worker of `id` failed. The open ends there, with its reason, unless it is no
1756    /// longer the one in flight or its dataset is already up.
1757    pub(crate) fn failed(&mut self, id: LoadId, message: &str) -> Step {
1758        let Some(load) = self.load.as_ref().filter(|load| load.id == id) else {
1759            return Step::Nothing;
1760        };
1761        if matches!(load.phase, Phase::FirstRows) {
1762            return Step::Nothing;
1763        }
1764        let from_home = load.from_home;
1765        // A download is read from a temp file the user never typed: the reason names
1766        // the URL they did, or `stdin`.
1767        let mut message = match &load.download {
1768            Some(fetched) => crate::error_display::named_by_source(
1769                message,
1770                fetched.file.path(),
1771                &stdin::named(&fetched.url),
1772            ),
1773            None => message.to_string(),
1774        };
1775        // So are the IPC files a conversion wrote.
1776        if let Some(path) = &load.path {
1777            for converted in &load.converted {
1778                message = crate::error_display::named_by_source(&message, converted.path(), path);
1779            }
1780        }
1781        self.retire();
1782        Step::Failed(Failed { message, from_home })
1783    }
1784
1785    /// The user agreed to what the open asked: let go of the hold, and fetch the
1786    /// download or start the read.
1787    pub(crate) fn confirmed(&mut self) -> Step {
1788        let Some(load) = self.load.as_mut().filter(|load| load.phase.asks()) else {
1789            return Step::Nothing;
1790        };
1791        // The hold goes as the phase changes: the job the caller starts next holds the
1792        // generation before anything else can look at it.
1793        match std::mem::replace(&mut load.phase, Phase::Scanning { downloaded: false }) {
1794            Phase::ConfirmingRead { scan, .. } => {
1795                let Scan {
1796                    paths,
1797                    options,
1798                    display,
1799                } = *scan;
1800                Step::Scan {
1801                    paths,
1802                    options,
1803                    display,
1804                    status: "Scanning input...",
1805                }
1806            }
1807            #[cfg(any(feature = "http", feature = "cloud"))]
1808            Phase::Confirming { pending, .. } => {
1809                load.phase = Phase::Downloading;
1810                Step::Download {
1811                    pending: pending.asked(),
1812                    writer: load.writer.clone(),
1813                }
1814            }
1815            _ => unreachable!("a phase that asks"),
1816        }
1817    }
1818
1819    /// The first rows of the installed dataset are on screen, or will not be read: the
1820    /// open is done.
1821    pub(crate) fn first_rows_settled(&mut self) {
1822        if self
1823            .load
1824            .as_ref()
1825            .is_some_and(|load| matches!(load.phase, Phase::FirstRows))
1826        {
1827            self.load = None;
1828        }
1829    }
1830}
1831
1832impl Drop for Loader {
1833    /// A download still running stops at its next chunk and removes its partial file.
1834    fn drop(&mut self) {
1835        self.retire();
1836    }
1837}
1838
1839/// What the loading screen and the footer say while text is read as lines.
1840const READING_LINES: &str = "Reading as lines";
1841const READING_LINES_STATUS: &str = "Reading as lines...";
1842
1843/// Whether `paths` are read as lines per `--format` or their names, for the loading line
1844/// (bytes may still say otherwise, e.g. a candump `.log`).
1845fn reads_lines(paths: &[PathBuf], options: &OpenOptions) -> bool {
1846    options
1847        .format
1848        .or_else(|| match paths {
1849            [one] => FileFormat::from_path(one),
1850            _ => None,
1851        })
1852        .is_some_and(FileFormat::is_lines)
1853}
1854
1855/// What reading `paths` would read whole into memory, by names and options only (not
1856/// bytes, on the event thread): local files of an in-memory format
1857/// ([`FileFormat::read_mode`]). `None` when none is.
1858pub(crate) fn in_memory(paths: &[PathBuf], options: &OpenOptions) -> Option<InMemory> {
1859    let mut found: Option<InMemory> = None;
1860    for path in paths {
1861        if !matches!(source::input_source(path), source::InputSource::Local(_)) {
1862            continue;
1863        }
1864        let compression = options
1865            .compression
1866            .or_else(|| CompressionFormat::from_extension(path));
1867        // By name, or by the first bytes the open will judge it by: journal JSON
1868        // piped to a file with no name, or in a `.json` one.
1869        let format = options.format.or_else(|| match compression {
1870            Some(_) => path
1871                .file_stem()
1872                .and_then(|stem| FileFormat::from_path(Path::new(stem))),
1873            None => match FileFormat::from_path(path) {
1874                Some(named) => Some(crate::formats::readers::refined(path, named).unwrap_or(named)),
1875                None => crate::formats::readers::sniff_open(path, None),
1876            },
1877        });
1878        let Some(format) = format else {
1879            continue;
1880        };
1881        let stored = match compression {
1882            Some(_) => crate::Stored::Compressed {
1883                in_memory: options.decompress_in_memory,
1884            },
1885            None => crate::Stored::Plain,
1886        };
1887        // A model file's table comes from its header, which is what a URL of one is
1888        // read in place for: small however large the file.
1889        if format.read_mode(stored) != Some(crate::ReadMode::InMemory)
1890            || format.http_file() == crate::RemoteRead::InPlace
1891        {
1892            continue;
1893        }
1894        let Some(bytes) = std::fs::metadata(path)
1895            .ok()
1896            .filter(|m| m.is_file())
1897            .map(|m| m.len())
1898        else {
1899            continue;
1900        };
1901        match &mut found {
1902            Some(read) => {
1903                read.bytes += bytes;
1904                read.files += 1;
1905            }
1906            None => {
1907                found = Some(InMemory {
1908                    bytes,
1909                    format,
1910                    files: 1,
1911                    name: path.clone(),
1912                })
1913            }
1914        }
1915    }
1916    found
1917}
1918
1919/// What the loading screen says while a CSV is counted to drop its footer rows.
1920const COUNTING_FOOTER: &str = "Counting rows to skip the footer";
1921/// The footer's line for the same wait.
1922const COUNTING_FOOTER_STATUS: &str = "Counting rows to skip the footer...";
1923
1924/// Whether the scan of `paths` counts every row first: delimited text whose footer
1925/// rows are dropped (`--footer-rows`).
1926fn counts_footer(paths: &[PathBuf], options: &OpenOptions) -> bool {
1927    options.skip_tail_rows.is_some_and(|n| n > 0)
1928        && paths.iter().all(|p| delimited_format(p, options).is_some())
1929}
1930
1931/// The delimited format (CSV, TSV, PSV) or text `path` is read as, if any: `--format`,
1932/// else the extension past any compression suffix (`x.tsv.gz` is TSV).
1933pub(crate) fn delimited_format(path: &Path, options: &OpenOptions) -> Option<FileFormat> {
1934    let format = options.format.or_else(|| {
1935        FileFormat::from_path(path).or_else(|| {
1936            CompressionFormat::from_extension(path)?;
1937            FileFormat::from_path(Path::new(path.file_stem()?))
1938        })
1939    })?;
1940    format.decompressed_once().then_some(format)
1941}
1942
1943/// Why a catalog file that started without a question asks partway.
1944#[cfg(any(feature = "http", feature = "cloud"))]
1945pub(crate) const PAST_LIMIT: &str =
1946    "This catalog file passed 50 MiB, more than it may download without asking.";
1947
1948/// Why a remote model is downloaded rather than read by its headers.
1949#[cfg(any(feature = "http", feature = "cloud"))]
1950pub(crate) const NO_RANGES: &str = "The server does not send byte ranges, so the model's header cannot be read without downloading the whole file.";
1951
1952/// The download a remote source needs first, if any: always for HTTP, and for a store
1953/// object that cannot be scanned in place, or one a format spec reads.
1954#[cfg(any(feature = "http", feature = "cloud"))]
1955fn remote_download(src: &source::InputSource, options: &OpenOptions) -> Option<PendingDownload> {
1956    #[cfg(feature = "cloud")]
1957    let spec = options.spec_file.is_some() || options.spec_name.is_some();
1958    #[cfg(feature = "cloud")]
1959    let should_download = |url: &str| {
1960        should_download(url)
1961            || (spec && !source::is_prefix_or_glob(url))
1962            || unnamed_object(url, options)
1963    };
1964    #[cfg(feature = "cloud")]
1965    if !spec && let Some(arrow) = cloud_arrow(src, options) {
1966        return Some(arrow);
1967    }
1968    let options = options.clone();
1969    match src {
1970        #[cfg(feature = "http")]
1971        source::InputSource::Http(url) => Some(PendingDownload::Http {
1972            url: url.clone(),
1973            size: None,
1974            options,
1975        }),
1976        #[cfg(feature = "cloud")]
1977        source::InputSource::S3(url) => {
1978            let full = format!("s3://{url}");
1979            should_download(&full).then_some(PendingDownload::S3 {
1980                url: full,
1981                size: None,
1982                options,
1983            })
1984        }
1985        #[cfg(feature = "cloud")]
1986        source::InputSource::Gcs(url) => {
1987            let full = format!("gs://{url}");
1988            should_download(&full).then_some(PendingDownload::Gcs {
1989                url: full,
1990                size: None,
1991                options,
1992            })
1993        }
1994        #[cfg(feature = "cloud")]
1995        source::InputSource::Azure(url) => should_download(url).then(|| PendingDownload::Azure {
1996            url: url.clone(),
1997            size: None,
1998            options,
1999        }),
2000        _ => None,
2001    }
2002}
2003
2004/// Arrow in a store (object or listed prefix, not a glob): the probe lists it, its
2005/// streams are downloaded, its IPC files scanned in place.
2006#[cfg(feature = "cloud")]
2007fn cloud_arrow(src: &source::InputSource, options: &OpenOptions) -> Option<PendingDownload> {
2008    let url = match src {
2009        source::InputSource::S3(url) => format!("s3://{url}"),
2010        source::InputSource::Gcs(url) => format!("gs://{url}"),
2011        source::InputSource::Azure(url) => url.clone(),
2012        _ => return None,
2013    };
2014    if url.contains('*') {
2015        return None;
2016    }
2017    let arrow = if url.ends_with('/') {
2018        options.format == Some(FileFormat::Arrow)
2019    } else {
2020        // Compressed, it is a download like any other compressed object.
2021        options.compression.is_none()
2022            && options
2023                .format
2024                .or_else(|| FileFormat::from_path(Path::new(&url)))
2025                == Some(FileFormat::Arrow)
2026    };
2027    arrow.then(|| PendingDownload::Arrow {
2028        url,
2029        objects: Vec::new(),
2030        size: None,
2031        options: options.clone(),
2032    })
2033}
2034
2035/// An object in a store whose name says no format: the probe reads its first bytes
2036/// ([`LoadAnswer::InPlace`] for Parquet or a prefix named without its slash), and anything
2037/// else is downloaded and told apart as a local file is. A part file in a `.parquet`
2038/// directory is Parquet by its name, as it is on disk.
2039#[cfg(feature = "cloud")]
2040pub(crate) fn unnamed_object(url: &str, options: &OpenOptions) -> bool {
2041    options.format.is_none()
2042        && options.spec_file.is_none()
2043        && options.spec_name.is_none()
2044        && !source::is_prefix_or_glob(url)
2045        && source::url_path_extension(url).1.is_none()
2046        && !crate::home::discover::is_parquet_key(url)
2047}
2048
2049#[cfg(feature = "cloud")]
2050fn should_download(url: &str) -> bool {
2051    let (_, ext) = source::url_path_extension(url);
2052    source::cloud_path_should_download(ext.as_deref(), source::is_prefix_or_glob(url))
2053}
2054
2055#[cfg(test)]
2056mod tests;