Skip to main content

datui_lib/table/
mod.rs

1use color_eyre::Result;
2use std::collections::HashSet;
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use polars::frame::PivotColumnNaming;
7use polars::prelude::*;
8use ratatui::widgets::TableState;
9
10use crate::OpenOptions;
11use crate::analysis::statistics::collect_lazy;
12use crate::app::modals::filter_modal::FilterStatement;
13use crate::app::modals::pivot_melt_modal::{MeltSpec, PivotAggregation, PivotSpec, ReshapeSource};
14use crate::cloud::local_copy::RemoteObject;
15use crate::export::python_script::{SidebarFilter, Step};
16use crate::formats::readers::csv::Decompressed;
17use crate::formats::readers::{Read, Typing};
18use crate::numfmt::{self};
19#[cfg(feature = "sql")]
20use crate::query::sql_plan::{
21    count_subquery_values_once, leftover_subquery_value_columns, ordered_by, stable_order,
22};
23use crate::query::{ParsedQuery, parse_query_over};
24use crate::widgets::column_paging::{ColumnMove, CursorMove, OnScreen, Room};
25use crate::widgets::column_widths::{ColumnWidths, WidthChoice};
26
27/// The view on screen: frames, what built them, rows held and layout. One value, so a
28/// checkpoint is a clone and restoring is one assignment.
29#[derive(Clone)]
30pub(crate) struct View {
31    lf: LazyFrame,
32    /// `lf` before its sort, when it has one. See [`DataTableState::analysis_lf`].
33    unsorted_lf: Option<LazyFrame>,
34    /// What filters and sort apply to: the active query's result (DSL, SQL or fuzzy), the
35    /// last pivot/melt, or `original_lf`. Pipeline: original → query/reshape (`base_lf`)
36    /// → filters → sort (`lf`) → column order (at collect), so filters never discard the
37    /// query.
38    base_lf: LazyFrame,
39    /// `base_lf`'s schema as built, checked against by column changes without resolving
40    /// the plan.
41    base_schema: Arc<Schema>,
42    pub(crate) df: Option<DataFrame>, // Scrollable columns dataframe
43    pub(crate) locked_df: Option<DataFrame>, // Locked columns dataframe
44    pub(crate) start_row: usize,
45    /// The column cursor's column, by name, so it follows hide, reorder and freeze.
46    /// `None` is the first column. See [`DataTableState::current_column`].
47    cursor_column: Option<String>,
48    /// The cursor's position in `column_order` when placed: if its column is hidden, the
49    /// cursor goes to the one now there.
50    cursor_at: usize,
51    pub(crate) schema: Arc<Schema>,
52    num_rows: usize,
53    /// When true, collect() skips the len() query.
54    pub(crate) num_rows_valid: bool,
55    /// Bumped whenever `lf` changes (`invalidate_num_rows`); a background `len()` whose
56    /// generation no longer matches is dropped. Separate from `task_generation` so a
57    /// scroll does not restart a count. Seeded from a process-wide counter, unique across
58    /// datasets, so a closed dataset's count never lands on the next.
59    len_generation: u64,
60    filters: Vec<FilterStatement>,
61    sort_columns: Vec<String>,
62    /// Per `sort_columns` entry, whether it is descending; always the same length.
63    sort_descending: Vec<bool>,
64    sort_ascending: bool,
65    /// Last executed DSL query. At most one `active_*` query is set; running one clears the
66    /// others.
67    active_query: String,
68    /// Last executed SQL (Sql tab).
69    active_sql_query: String,
70    /// The leading columns the SQL's ORDER BY names in its result, and their directions:
71    /// the header's sort marks while the sidebar sorts nothing. Empty for an expression.
72    query_order: Vec<(String, bool)>,
73    /// Last executed fuzzy search (Fuzzy tab).
74    active_fuzzy_query: String,
75    pub(crate) column_order: Vec<String>, // Order of columns for display
76    locked_columns_count: usize,          // Number of locked columns (from left)
77    /// The last layout's frozen columns: the count asked, and how many fit beside a
78    /// usable scrolling column (the rest scroll until there is room). A new count starts
79    /// over.
80    frozen_fit: (usize, usize),
81    /// The grouped view a drill-down left, restored exactly by `drill_up`.
82    grouped: Option<GroupedView>,
83    /// The rows behind a grouped query result, so Enter can drill from an aggregate.
84    group_source: Option<GroupSource>,
85    /// The last pivot/melt result while in effect; SQL runs against it (see `query_root`).
86    reshaped_lf: Option<LazyFrame>,
87    drilled_down_group_index: Option<usize>, // Index of the group we're viewing
88    drilled_down_group_key: Option<Vec<String>>, // Key values of the drilled down group
89    drilled_down_group_key_columns: Option<Vec<String>>, // Key column names of the drilled down group
90    /// Whether the frame still has the scan's hidden drift column: true when a dataset's
91    /// files differ, false once a query or reshape builds a new frame.
92    drift_column_present: bool,
93    /// What each drift group is missing, shared with the renderer (no per-frame
94    /// allocation), indexed by the drift column.
95    drift_groups: Arc<Vec<crate::formats::schema_union::DriftGroup>>,
96    /// The sorted or filtered view numbers its own rows (`#` on, data with no source
97    /// position): a row index over the base, under filters and sort. Only while `#` is on,
98    /// since it blocks filter pushdown.
99    view_numbered: bool,
100    /// What datui noticed about the dataset, from the footers it had to read anyway.
101    notes: Vec<crate::notes::Note>,
102    /// Whether Info has opened since the notes were gathered; per dataset.
103    notes_seen: bool,
104    /// Notes on what the filter and sort leave out, recomputed when either changes.
105    view_notes: Vec<crate::notes::Note>,
106    /// Bytes per row of the last buffer, preferred over the schema estimate.
107    observed_bytes_per_row: Option<usize>,
108    pub(crate) buffered_start_row: usize,
109    buffered_end_row: usize,
110    /// The full buffered frame (all columns in `column_order`) for the buffer range, so
111    /// column scrolling re-slices without collecting.
112    buffered_df: Option<DataFrame>,
113    /// The first row of the last page drawn whole. See [`DataTableState::start_to_draw`].
114    drawn_start: usize,
115    /// Last applied pivot spec, if current lf is result of a pivot. Used for views.
116    last_pivot_spec: Option<PivotSpec>,
117    /// Last applied melt spec, if current lf is result of a melt. Used for views.
118    last_melt_spec: Option<MeltSpec>,
119    /// The query, filters and sort the pivot or melt ran over, for a view to replay first.
120    /// `None` without one, or when it ran over the data as loaded.
121    reshape_source: Option<ReshapeSource>,
122    /// How `base_lf` was built from the data as loaded, for Copy as Python; empty for the
123    /// data as loaded.
124    base_steps: Vec<Step>,
125    /// The view's column types and derived columns in order: a step of `lf` before the
126    /// filters, as a spec's `[columns]` would say.
127    column_changes: Vec<crate::formats::column_types::ColumnChange>,
128    /// Bumped per change to `column_changes`, so a null count answers for its changes.
129    changes_version: u64,
130    /// Steps of a saved view whose columns this data does not have.
131    changes_dropped: Vec<crate::notes::Note>,
132    /// How `reshaped_lf` was built, while there is one: what SQL runs over.
133    reshape_steps: Option<Vec<Step>>,
134    /// Which loaded column each column of the base is (see [`Lineage`]).
135    lineage: Lineage,
136    /// The same for the pivot or melt in effect, which SQL runs against.
137    reshape_lineage: Lineage,
138}
139
140pub struct DataTableState {
141    original_lf: LazyFrame,
142    original_schema: Arc<Schema>,
143    pub table_state: TableState,
144    pub visible_rows: usize,
145    pub termcol_index: usize,
146    /// The cursor may be off screen (order, frozen count or room changed): the next draw
147    /// scrolls minimally to show it.
148    reveal_cursor: bool,
149    pub visible_termcols: usize,
150    /// The scrolling side as last drawn, for planning sideways pages; `None` before the
151    /// first draw.
152    scroll_room: Option<Room>,
153    /// Sideways moves waiting for the next draw to measure undrawn columns, in order. See
154    /// [`Self::scroll_columns`].
155    column_moves: Vec<WaitingMove>,
156    /// The pages `]` went, from and to, so `[` straight after goes back exactly.
157    page_trail: Vec<(usize, usize)>,
158    /// Which columns the last draw showed, while some are off screen.
159    pub(crate) on_screen: Option<OnScreen>,
160    /// Where the last frame drew the rows and columns, for a click.
161    pub(crate) drawn: Option<DrawnTable>,
162    /// The cells the last frame formatted, for the next one to draw again.
163    pub(crate) page_cells: crate::widgets::table::PageCells,
164    error: Option<PolarsError>,
165    pub suppress_error_display: bool, // When true, don't show errors in main view (e.g., when query input is active)
166    /// The dataset's row count from when the frame was last pristine, for the footer's
167    /// "417 of 1,000" without recounting; `None` until known.
168    pristine_rows: Option<usize>,
169    /// Renewed whenever `original_lf` is replaced; checkpoints record it, so one from
170    /// other data is never restored.
171    root_generation: u64,
172    /// The local Parquet hive directory loaded from, whose footer counts sum to the exact
173    /// row count while pristine: far cheaper than a `len()` scan.
174    parquet_count_dir: Option<PathBuf>,
175    /// What finding and reading this dataset cost. On the dataset, not the app: a failed
176    /// open leaves the last dataset up, and its figures stay with it.
177    measurements: Arc<crate::loading::measurements::Meter>,
178    /// Each column's drawn width by identity, so paging, reordering, hiding and sidebars
179    /// move nothing. Learned while formatting; not rolled back (it describes columns).
180    pub(crate) widths: ColumnWidths,
181    pages_lookahead: usize,
182    pages_lookback: usize,
183    max_buffered_rows: usize, // 0 = no limit
184    max_buffered_mb: usize,   // 0 = no limit
185    /// A scan of an object store, where a fill is a ranged read of whole row groups. See
186    /// `is_remote_source`.
187    remote_source: bool,
188    /// Where each row group of a remote Parquet object starts, total last, from the footer.
189    /// See `record_row_groups`.
190    row_group_offsets: Option<Vec<usize>>,
191    /// The files of a remote dataset, when it is many. See `RemoteFiles`.
192    remote_files: Option<RemoteFiles>,
193    /// Each remote object read, by URL, with size and tag: what a Data Quality local copy
194    /// would fetch.
195    remote_objects: Option<Arc<std::collections::HashMap<String, RemoteObject>>>,
196    /// What the footers said of a many-file dataset's columns (schema origin, columns not
197    /// in every file); `None` for one file.
198    dataset_schema: Option<crate::formats::schema_union::DatasetSchema>,
199    /// The two above as the dataset was opened, so a reset returns to them.
200    drift_at_open: bool,
201    groups_at_open: Arc<Vec<crate::formats::schema_union::DriftGroup>>,
202    /// The data as loaded carries each row's source position in the hidden row index
203    /// (lines), shown by `#` while the frame is the scan's.
204    source_rows_at_open: bool,
205    /// Lines still being indexed behind the first rows: the frames grow as they are.
206    indexing: Option<Arc<crate::formats::lines::Lines>>,
207    /// The lines of several files, which `#` numbers by their line in their own file.
208    numbering: Option<Arc<crate::formats::lines::Lines>>,
209    /// The dataset's row count from a sample of its footers, until it is counted.
210    row_estimate: Option<crate::formats::schema_union::RowEstimate>,
211    /// The notes the lines gave when they opened, replaced once they are all indexed.
212    indexing_notes: Vec<crate::notes::Note>,
213    /// Whether the open guessed the lines were text, which their notes say.
214    indexing_guessed: bool,
215    /// Each file's first row and drift group: together they map a row to what its file
216    /// lacked.
217    drift_file_starts: Vec<usize>,
218    drift_file_group: Vec<u32>,
219    /// Rows in the dataset per the footers, closing the last file's range.
220    drift_dataset_rows: usize,
221    /// The dataset as its footers found it, kept because reading a column as text needs
222    /// the per-file types the view no longer has.
223    dataset_at_open: Option<crate::formats::schema_union::DatasetSchema>,
224    /// Columns read as text from every file instead of the majority type; empty as opened.
225    read_as_text: Vec<PlSmallStr>,
226    /// Each file's path or URL in scan order, to trace rows and name them in exports.
227    drift_files: Vec<String>,
228    /// Set while the dataset shows from a footer or two and the rest are being read;
229    /// cleared when they join. See [`FootersJoin`].
230    footers_pending: Option<FootersJoin>,
231    /// The notes as the dataset was opened, so a reset and a drill up restore them.
232    notes_at_open: Vec<crate::notes::Note>,
233    /// Notes about the read itself (files passed over, a lake table's plain files). Kept
234    /// apart from [`Self::notes`], which footers overwrite when they land, and they
235    /// survive reshapes.
236    open_notes: Vec<crate::notes::Note>,
237    /// The lake format whose plain files this dataset is, if it is one. See
238    /// [`crate::OpenOptions::read_as_plain_files_of`].
239    not_the_table: Option<&'static str>,
240    /// What a read through a format spec found: the spec, why, and its notes.
241    format_read: Option<Arc<crate::formats::Read>>,
242    /// What a read through a delimited spec found: units and metadata.
243    delimited: Option<Arc<crate::formats::delimited_spec::DelimitedRead>>,
244    /// The fixed records the data as loaded is, while pristine: a window starts decoding
245    /// at the window, not row 0.
246    fixed_window: Option<Arc<dyn crate::formats::pushdown::Windowed>>,
247    /// A source that runs the sidebar's filters and sort itself (a SQLite table), while
248    /// the data as loaded is the root: see [`Self::pushed_view`].
249    pushdown: Option<Arc<dyn crate::formats::pushdown::Pushdown>>,
250    /// Stops what the source runs when this state goes.
251    source_hold: Option<crate::formats::sqlite::Hold>,
252    /// How the open reads the data. See [`crate::OpenOptions::read_mode`].
253    read_mode: Option<crate::ReadMode>,
254    /// The format the open read. See [`OpenFacts::read_as`].
255    read_as: Option<crate::FileFormat>,
256    /// The data was downloaded from a remote source before it was read.
257    fetched: bool,
258    /// What the file said besides its rows. See [`OpenFacts::detail`].
259    detail: Option<Arc<crate::formats::text_formats::Detail>>,
260    /// Each loaded column's unit, from the file. See [`OpenFacts::units`].
261    file_units: Arc<Vec<(String, String)>>,
262    /// Uncompressed bytes per row of each column from the footer, for `bytes_per_row`
263    /// before any collect.
264    column_bytes: Vec<(String, usize)>,
265    proximity_threshold: usize,
266    row_numbers: bool,
267    row_start_index: usize,
268    /// What the open did to the reader's rows, as Python calls (names trimmed, text typed).
269    read_python: Vec<String>,
270    /// The columns the read gave a type, and the frame before it did.
271    typing: Typing,
272    /// The notes on the values the types made null, once counted.
273    unfit_notes: Option<Vec<crate::notes::Note>>,
274    /// The notes on the values the view's types made null: for the version counted.
275    changes_unfit: Option<(u64, Vec<crate::notes::Note>)>,
276    /// When set, dataset was loaded with hive partitioning; partition column names for Info panel and predicate pushdown.
277    partition_columns: Option<Vec<String>>,
278    /// The temp file decompressed CSV was written to, kept alive for the lazy scan; shared
279    /// with views scanning it, removed with the last.
280    decompress_temp_file: Option<Arc<Decompressed>>,
281    /// The downloaded remote file this dataset was opened from, held while it is scanned.
282    download: Option<crate::cloud::download::TempDownload>,
283    /// The files a GPS log was read into, which the frame scans; held as `download` is.
284    converted: Vec<crate::cloud::download::TempDownload>,
285    /// The file's other tables, as `--table` names them; see [`OpenFacts::other_tables`].
286    other_tables: Vec<String>,
287    /// When true, use Polars streaming engine for LazyFrame collect when the streaming feature is enabled.
288    polars_streaming: bool,
289    /// When set, `collect()` / `apply_transformations()` skip the blocking collect; the
290    /// caller starts an async one.
291    defer_collect: bool,
292    /// Set by the renderer when `visible_rows` changes; the event loop then starts an async
293    /// collect.
294    pub needs_recollect: bool,
295    /// The watcher of the file this dataset follows (`--follow`), while it does.
296    follow: Option<crate::loading::follow::Follow>,
297    /// For a followed view that filters or sorts: known points (view rows, file row),
298    /// ascending, for the count generation they hold for. Counts read on from the last;
299    /// filtered windows from the one before.
300    follow_known: Option<(u64, Vec<(usize, usize)>)>,
301    /// The sample this view's rows are and the view it was drawn from: the step between
302    /// source and query.
303    sampled: Option<Box<Sampled>>,
304    /// What the view is: everything a checkpoint keeps and puts back.
305    pub(crate) view: View,
306}
307
308/// A view's sample, between source and query. The frames scan `Self::frame`, the
309/// chunks so far, growing as the draw continues.
310pub struct Sampled {
311    /// The view the sample was drawn from, restored when the sample is cleared.
312    source: Box<DataTableState>,
313    sample: crate::analysis::sampling::Sample,
314    rows: Arc<crate::analysis::table_sample::SampleRows>,
315    /// The frame the view's plans scan: the chunks taken so far, on their buffers.
316    frame: Arc<DataFrame>,
317    /// Drawn through the view's query or filters (which it then stands for), not the
318    /// source.
319    through: bool,
320    /// What the draw read, once it ended; `None` while it runs.
321    drawn: Option<crate::analysis::table_sample::Drawn>,
322    /// How a random sample of a stream is drawn, which a view keeps.
323    path: Option<crate::analysis::table_sample::DrawPath>,
324}
325
326impl Sampled {
327    pub fn sample(&self) -> &crate::analysis::sampling::Sample {
328        &self.sample
329    }
330
331    /// The view the sample was drawn from.
332    pub fn source(&self) -> &DataTableState {
333        &self.source
334    }
335
336    /// Whether the sample was drawn from the view's query or filters.
337    pub fn through(&self) -> bool {
338        self.through
339    }
340
341    /// Whether these are the rows `rows` holds: the sample a draw fills.
342    pub(crate) fn holds(&self, rows: &Arc<crate::analysis::table_sample::SampleRows>) -> bool {
343        Arc::ptr_eq(&self.rows, rows)
344    }
345
346    /// Whether rows are still arriving.
347    pub fn drawing(&self) -> bool {
348        self.drawn.is_none()
349    }
350
351    /// How a random sample of a stream is drawn: what draws the same rows again.
352    pub fn path(&self) -> Option<crate::analysis::table_sample::DrawPath> {
353        self.path
354    }
355
356    /// The frame the view's plans scan.
357    #[cfg(test)]
358    pub(crate) fn frame(&self) -> &DataFrame {
359        &self.frame
360    }
361
362    /// Rows the view has taken of the sample.
363    pub fn rows(&self) -> usize {
364        self.frame.height()
365    }
366
367    /// The footer segment: `sample 100,000 of 36.8M`, `sample 1,234+` while drawing,
368    /// `sample about 100,000 of 36.8M` when kept row by row by chance.
369    pub fn label(&self) -> String {
370        let rows = crate::numfmt::group_chrome(self.rows());
371        let Some(drawn) = &self.drawn else {
372            return format!("sample {rows}+");
373        };
374        let about = if drawn.about { "about " } else { "" };
375        let cut = if drawn.cut { ", stopped" } else { "" };
376        match drawn.total {
377            Some(total) if total > self.rows() => {
378                format!(
379                    "sample {about}{rows} of {}{cut}",
380                    crate::home::discover::format_rows(total)
381                )
382            }
383            _ => format!("sample {rows}{cut}"),
384        }
385    }
386}
387
388/// What an open learned besides frame and schema, given once via
389/// [`DataTableState::with_open`] so count, row groups, files and notes agree. Defaults
390/// mean not found.
391#[derive(Default)]
392pub struct OpenFacts {
393    /// A scan of an object store in place: a buffer is one window of whole row groups.
394    pub remote_source: bool,
395    /// Each file's row groups in scan order (one entry for a single object): the count.
396    /// With `remote_files`, one per listed file.
397    pub row_groups: Vec<Vec<usize>>,
398    /// The files of a remote dataset of many, and how to read some of them.
399    pub remote_files: Option<RemoteFiles>,
400    /// Each remote object the dataset reads, as the listing or footer found it.
401    pub remote_objects: Vec<RemoteObject>,
402    /// What the footers said about a many-file dataset's columns.
403    pub dataset: Option<DatasetAtOpen>,
404    /// The pass that reads the rest of the footers, for a dataset opened from a few.
405    pub footers_pending: Option<FootersJoin>,
406    /// Each column's uncompressed bytes per row, from the footers.
407    pub column_bytes: Vec<(String, usize)>,
408    /// The local Parquet hive directory whose footers sum to the count.
409    pub parquet_count_dir: Option<PathBuf>,
410    /// What finding and reading the dataset cost.
411    pub measurements: Arc<crate::loading::measurements::Meter>,
412    /// What the open itself has to say. See `DataTableState::open_notes`.
413    pub open_notes: Vec<crate::notes::Note>,
414    /// The lake format whose plain files this dataset is. See
415    /// [`DataTableState::not_the_table`].
416    pub not_the_table: Option<&'static str>,
417    /// What a read through a format spec found.
418    pub format_read: Option<Arc<crate::formats::Read>>,
419    /// What a read through a delimited spec found.
420    pub delimited: Option<Arc<crate::formats::delimited_spec::DelimitedRead>>,
421    /// The downloaded file the frame scans, held for as long as the state lives.
422    pub download: Option<crate::cloud::download::TempDownload>,
423    /// The files a GPS log was read into, which the frame scans.
424    pub converted: Vec<crate::cloud::download::TempDownload>,
425    /// The file's other tables as `--table` names them, with row counts where known, for
426    /// Info's Schema tab. Empty for a file of one.
427    pub other_tables: Vec<String>,
428    /// A source that runs the sidebar's filters and sort itself: a SQLite table.
429    pub pushdown: Option<Arc<dyn crate::formats::pushdown::Pushdown>>,
430    /// What stops that source's statements when the dataset goes.
431    pub hold: Option<crate::formats::sqlite::Hold>,
432    /// How the open reads the data. See [`crate::OpenOptions::read_mode`].
433    pub read_mode: Option<crate::ReadMode>,
434    /// The format the open read as, after sniffing and spec matching (which the name may
435    /// not say); Copy as Python and the export default follow it.
436    pub read_as: Option<crate::FileFormat>,
437    /// Downloaded from a remote source before reading (not a local stream conversion or
438    /// stdin spool, though held the same way).
439    pub fetched: bool,
440    /// What the file said besides its rows, for the Info panel.
441    pub detail: Option<Arc<crate::formats::text_formats::Detail>>,
442    /// Rows decoded straight from the file by a reader (NumPy array, audio frames), and
443    /// how many: deep pages and the count need no row index.
444    pub records: Option<(Arc<dyn crate::formats::pushdown::Windowed>, usize)>,
445    /// Each column's unit, where the file says one.
446    pub units: Vec<(String, String)>,
447    /// Lines still being indexed behind the first rows: the frames grow as they are.
448    pub indexing: Option<Arc<crate::formats::lines::Lines>>,
449    /// The lines of several files, which `#` numbers by their line in their own file.
450    pub numbering: Option<Arc<crate::formats::lines::Lines>>,
451    /// The columns the read gave a type, for the count of what did not fit.
452    pub typing: Typing,
453}
454
455/// The footers' account of a dataset of many files.
456pub struct DatasetAtOpen {
457    pub schema: crate::formats::schema_union::DatasetSchema,
458    /// Each file's row count in scan order; empty unless all are known (when the scan
459    /// numbers rows).
460    pub file_rows: Vec<usize>,
461    /// Every file's path or URL, in scan order.
462    pub files: Vec<String>,
463}
464
465/// Rows the display buffer may hold when `performance.max_buffered_rows` is unset; also
466/// a remote scan's window when the cap is off.
467pub const DEFAULT_MAX_BUFFERED_ROWS: usize = 100_000;
468
469/// Seeds `DataTableState::len_generation`, unique per state, so a count for one dataset
470/// never validates another.
471static NEXT_LEN_GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
472
473fn next_len_generation() -> u64 {
474    NEXT_LEN_GENERATION.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
475}
476
477/// The in-memory frame `lf` scans, when it is a scan of one.
478fn scanned_frame(lf: &LazyFrame) -> Option<Arc<DataFrame>> {
479    match &lf.logical_plan {
480        polars::lazy::dsl::DslPlan::DataFrameScan { df, .. } => Some(df.clone()),
481        _ => None,
482    }
483}
484
485/// Calls `f` on each plan `plan` reads from.
486pub(crate) fn for_each_input(
487    plan: &mut polars::lazy::dsl::DslPlan,
488    f: &mut dyn FnMut(&mut polars::lazy::dsl::DslPlan),
489) {
490    use polars::lazy::dsl::DslPlan;
491    match plan {
492        DslPlan::Sort { input, .. }
493        | DslPlan::Select { input, .. }
494        | DslPlan::GroupBy { input, .. }
495        | DslPlan::Filter { input, .. }
496        | DslPlan::Distinct { input, .. }
497        | DslPlan::Slice { input, .. }
498        | DslPlan::HStack { input, .. }
499        | DslPlan::MatchToSchema { input, .. }
500        | DslPlan::MapFunction { input, .. }
501        | DslPlan::Sink { input, .. }
502        | DslPlan::Cache { input, .. }
503        | DslPlan::Pivot { input, .. } => f(Arc::make_mut(input)),
504        DslPlan::Union { inputs, .. }
505        | DslPlan::HConcat { inputs, .. }
506        | DslPlan::SinkMultiple { inputs } => inputs.iter_mut().for_each(f),
507        DslPlan::PipeWithSchema { input, .. } => {
508            let mut inputs = input.to_vec();
509            inputs.iter_mut().for_each(&mut *f);
510            *input = inputs.into();
511        }
512        DslPlan::Join {
513            input_left,
514            input_right,
515            ..
516        } => {
517            f(Arc::make_mut(input_left));
518            f(Arc::make_mut(input_right));
519        }
520        DslPlan::Gather { input, idxs, .. } => {
521            f(Arc::make_mut(input));
522            f(Arc::make_mut(idxs));
523        }
524        DslPlan::ExtContext { input, contexts } => {
525            f(Arc::make_mut(input));
526            contexts.iter_mut().for_each(f);
527        }
528        _ => {}
529    }
530}
531
532impl DataTableState {
533    pub fn new(
534        lf: LazyFrame,
535        pages_lookahead: Option<usize>,
536        pages_lookback: Option<usize>,
537        max_buffered_rows: Option<usize>,
538        max_buffered_mb: Option<usize>,
539        polars_streaming: bool,
540    ) -> Result<Self> {
541        let options = OpenOptions {
542            pages_lookahead,
543            pages_lookback,
544            max_buffered_rows,
545            max_buffered_mb,
546            polars_streaming,
547            ..OpenOptions::default()
548        };
549        Self::from_lazyframe(lf, &options)
550    }
551
552    /// `schema` without the hidden row index, and whether it had one (the rows' source
553    /// position, which `#` shows).
554    fn without_source_rows(schema: Arc<Schema>) -> (Arc<Schema>, bool) {
555        if !schema.contains(crate::formats::schema_union::DRIFT_COLUMN) {
556            return (schema, false);
557        }
558        let mut schema = (*schema).clone();
559        schema.shift_remove(crate::formats::schema_union::DRIFT_COLUMN);
560        (Arc::new(schema), true)
561    }
562
563    /// Create state from an existing LazyFrame (e.g. from Python or in-memory). Uses OpenOptions for display/buffer settings.
564    pub fn from_lazyframe(lf: LazyFrame, options: &crate::OpenOptions) -> Result<Self> {
565        let schema = lf.clone().collect_schema()?;
566        Self::from_schema_and_lazyframe(schema, lf, options, None)
567    }
568
569    /// State from a pre-collected schema and LazyFrame (phased loading), without
570    /// `collect_schema()`; `df` is `None` so headers render while the first collect runs.
571    /// Hive partition columns come first.
572    pub fn from_schema_and_lazyframe(
573        schema: Arc<Schema>,
574        lf: LazyFrame,
575        options: &crate::OpenOptions,
576        partition_columns: Option<Vec<String>>,
577    ) -> Result<Self> {
578        let (schema, source_rows_at_open) = Self::without_source_rows(schema);
579        let column_order: Vec<String> = if let Some(ref part) = partition_columns {
580            let part_set: HashSet<&str> = part.iter().map(String::as_str).collect();
581            let rest: Vec<String> = schema
582                .iter_names()
583                .map(|s| s.to_string())
584                .filter(|c| !part_set.contains(c.as_str()))
585                .collect();
586            part.iter().cloned().chain(rest).collect()
587        } else {
588            schema.iter_names().map(|s| s.to_string()).collect()
589        };
590        Ok(Self {
591            original_lf: lf.clone(),
592            original_schema: schema.clone(),
593            table_state: TableState::default(),
594            visible_rows: 0,
595            termcol_index: 0,
596            visible_termcols: 0,
597            scroll_room: None,
598            column_moves: Vec::new(),
599            page_trail: Vec::new(),
600            on_screen: None,
601            drawn: None,
602            page_cells: Default::default(),
603            error: None,
604            suppress_error_display: false,
605            pristine_rows: None,
606            root_generation: next_len_generation(),
607            parquet_count_dir: None,
608            measurements: Arc::new(crate::loading::measurements::Meter::default()),
609            reveal_cursor: false,
610            widths: ColumnWidths::default(),
611            pages_lookahead: options.pages_lookahead.unwrap_or(3),
612            pages_lookback: options.pages_lookback.unwrap_or(3),
613            max_buffered_rows: options
614                .max_buffered_rows
615                .unwrap_or(DEFAULT_MAX_BUFFERED_ROWS),
616            max_buffered_mb: options.max_buffered_mb.unwrap_or(512),
617            remote_source: false,
618            row_group_offsets: None,
619            remote_files: None,
620            remote_objects: None,
621            dataset_schema: None,
622            drift_at_open: false,
623            groups_at_open: Arc::new(Vec::new()),
624            source_rows_at_open,
625            indexing: None,
626            numbering: None,
627            row_estimate: None,
628            indexing_notes: Vec::new(),
629            indexing_guessed: false,
630            drift_file_starts: Vec::new(),
631            drift_file_group: Vec::new(),
632            drift_files: Vec::new(),
633            footers_pending: None,
634            open_notes: Vec::new(),
635            not_the_table: None,
636            format_read: None,
637            delimited: None,
638            fixed_window: None,
639            pushdown: None,
640            source_hold: None,
641            read_mode: None,
642            read_as: None,
643            fetched: false,
644            detail: None,
645            file_units: Arc::new(Vec::new()),
646            notes_at_open: Vec::new(),
647            drift_dataset_rows: 0,
648            dataset_at_open: None,
649            read_as_text: Vec::new(),
650            column_bytes: Vec::new(),
651            // Set when the rows on screen are known.
652            proximity_threshold: 0,
653            row_numbers: options.row_numbers,
654            row_start_index: options.row_start_index,
655            read_python: Vec::new(),
656            typing: Typing::default(),
657            unfit_notes: None,
658            changes_unfit: None,
659            partition_columns,
660            decompress_temp_file: None,
661            download: None,
662            converted: Vec::new(),
663            other_tables: Vec::new(),
664            polars_streaming: options.polars_streaming,
665            defer_collect: false,
666            needs_recollect: false,
667            follow: None,
668            follow_known: None,
669            sampled: None,
670            view: View {
671                unsorted_lf: None,
672                base_lf: lf.clone(),
673                base_schema: schema.clone(),
674                lf,
675                df: None,
676                locked_df: None,
677                start_row: 0,
678                schema,
679                num_rows: 0,
680                num_rows_valid: false,
681                len_generation: next_len_generation(),
682                filters: Vec::new(),
683                sort_columns: Vec::new(),
684                sort_descending: Vec::new(),
685                sort_ascending: true,
686                cursor_column: None,
687                cursor_at: 0,
688                active_query: String::new(),
689                active_sql_query: String::new(),
690                query_order: Vec::new(),
691                active_fuzzy_query: String::new(),
692                column_order,
693                locked_columns_count: 0,
694                frozen_fit: (0, 0),
695                grouped: None,
696                group_source: None,
697                reshaped_lf: None,
698                drilled_down_group_index: None,
699                drilled_down_group_key: None,
700                drilled_down_group_key_columns: None,
701                drift_column_present: false,
702                drift_groups: Arc::new(Vec::new()),
703                view_numbered: false,
704                notes: Vec::new(),
705                notes_seen: false,
706                view_notes: Vec::new(),
707                observed_bytes_per_row: None,
708                buffered_start_row: 0,
709                buffered_end_row: 0,
710                buffered_df: None,
711                drawn_start: 0,
712                last_pivot_spec: None,
713                last_melt_spec: None,
714                reshape_source: None,
715                base_steps: Vec::new(),
716                column_changes: Vec::new(),
717                changes_version: 0,
718                changes_dropped: Vec::new(),
719                reshape_steps: None,
720                lineage: None,
721                reshape_lineage: None,
722            },
723        })
724    }
725
726    /// The state with everything the open found in `facts`: the only way findings reach a
727    /// state, while pristine, applied in dependency order (files before row groups,
728    /// which set the count). Later learning goes through [`Self::join_dataset_schema`] and
729    /// [`Self::count_landed`].
730    pub fn with_open(mut self, facts: OpenFacts) -> Self {
731        let OpenFacts {
732            remote_source,
733            row_groups,
734            remote_files,
735            remote_objects,
736            dataset,
737            footers_pending,
738            column_bytes,
739            parquet_count_dir,
740            measurements,
741            open_notes,
742            not_the_table,
743            format_read,
744            delimited,
745            download,
746            converted,
747            other_tables,
748            pushdown,
749            hold,
750            read_mode,
751            read_as,
752            fetched,
753            detail,
754            records,
755            units,
756            indexing,
757            numbering,
758            typing,
759        } = facts;
760        self.numbering = numbering;
761        self.typing = typing;
762        debug_assert!(
763            self.is_pristine(),
764            "an open's facts are for the data as loaded"
765        );
766        self.remote_source = remote_source;
767        self.remote_files = remote_files;
768        self.remote_objects = (!remote_objects.is_empty()).then(|| {
769            Arc::new(
770                remote_objects
771                    .into_iter()
772                    .map(|object| (object.url.clone(), object))
773                    .collect(),
774            )
775        });
776        if !row_groups.is_empty() {
777            if self.remote_files.is_some() {
778                self.record_file_row_groups(&row_groups);
779            } else {
780                let flat: Vec<usize> = row_groups.into_iter().flatten().collect();
781                self.record_row_groups(&flat);
782            }
783        }
784        if let Some(DatasetAtOpen {
785            schema,
786            file_rows,
787            files,
788        }) = dataset
789        {
790            self.record_dataset_schema(schema, &file_rows, &files);
791        }
792        self.footers_pending = footers_pending;
793        self.column_bytes = column_bytes;
794        self.parquet_count_dir = parquet_count_dir;
795        self.measurements = measurements;
796        self.open_notes = open_notes;
797        self.not_the_table = not_the_table;
798        self.fixed_window = format_read
799            .as_ref()
800            .map(|read| read.records.clone() as Arc<dyn crate::formats::pushdown::Windowed>);
801        if let Some(read) = &format_read {
802            // The reader counted records from the file size; a frame count would build the whole
803            // row index.
804            self.set_num_rows(read.records.rows());
805        }
806        self.pushdown = pushdown;
807        self.source_hold = hold;
808        self.format_read = format_read;
809        self.delimited = delimited;
810        self.download = download;
811        self.converted = converted;
812        self.other_tables = other_tables;
813        self.read_mode = read_mode;
814        self.read_as = read_as;
815        self.fetched = fetched;
816        self.detail = detail;
817        if let Some((window, rows)) = records {
818            // The reader knows its rows (a frame count would build the row index); lines still
819            // indexing know only some.
820            if indexing.is_none() {
821                self.set_num_rows(rows);
822            }
823            self.fixed_window = Some(window);
824        }
825        if let Some(lines) = &indexing {
826            // The lines' own notes, replaced once all lines are in and can describe the file.
827            self.indexing_guessed = self
828                .open_notes
829                .iter()
830                .any(|n| n.summary.starts_with(crate::formats::lines::GUESSED));
831            self.indexing_notes = crate::formats::lines::notes(lines, self.indexing_guessed);
832        }
833        self.indexing = indexing;
834        self.file_units = Arc::new(units);
835        self
836    }
837
838    /// Make `lf` the data as loaded with `schema` (root, base and shown frame) until the
839    /// caller relays filters and sort. Rows and counts from the old root drop, and older
840    /// checkpoints no longer apply.
841    fn replace_root(&mut self, lf: LazyFrame, schema: Arc<Schema>) {
842        // The records, or the table, no longer stand for the root.
843        self.fixed_window = None;
844        self.pushdown = None;
845        self.root_generation = next_len_generation();
846        self.invalidate_num_rows();
847        self.original_schema = schema.clone();
848        self.view.base_schema = schema.clone();
849        self.view.schema = schema;
850        self.original_lf = lf.clone();
851        self.view.base_lf = lf.clone();
852        self.view.lf = lf;
853        self.view.unsorted_lf = None;
854        self.view.base_steps = Vec::new();
855        self.view.reshape_steps = None;
856        self.drop_buffer();
857    }
858
859    /// Make `lf` the shown frame and the base for filters and sort, `schema` its schema,
860    /// every column in view; row counts invalidated.
861    fn install_base(&mut self, lf: LazyFrame, schema: Arc<Schema>) {
862        self.invalidate_num_rows();
863        // A new frame is the user's own projection: its rows no longer stand for a file's, so
864        // no drift marks and file notes no longer apply.
865        self.view.drift_column_present = false;
866        self.view.view_numbered = false;
867        self.view.drift_groups = Arc::new(Vec::new());
868        self.view.notes = Vec::new();
869        self.view.view_notes = Vec::new();
870        // Measure the new shape afresh, not from the old width.
871        self.view.observed_bytes_per_row = None;
872        // A new frame is in no order a query named; `sql_query` names it after.
873        self.view.query_order = Vec::new();
874        // A column may keep its name and type and hold other values now.
875        self.widths.relearn();
876        self.view.base_lf = lf.clone();
877        self.view.base_schema = schema.clone();
878        self.view.lf = lf;
879        self.view.unsorted_lf = None;
880        // Callers say how the base was built; one that does not leaves a script saying so.
881        self.view.base_steps = vec![Step::Unreproducible(
882            "datui built the view from here in a way it cannot write as Python".to_string(),
883        )];
884        self.view.schema = schema;
885        self.view.column_order = self
886            .view
887            .schema
888            .iter_names()
889            .map(|s| s.to_string())
890            .collect();
891        // No column is a loaded one until the caller says which are.
892        self.view.lineage = Some(Arc::default());
893        self.settle_cursor();
894        // A query that groups records its source after installing its result.
895        self.view.group_source = None;
896        self.drop_buffer();
897    }
898
899    /// Forget rows read through the replaced frame so the next collect reads the new one.
900    fn drop_buffer(&mut self) {
901        self.view.buffered_start_row = 0;
902        self.view.buffered_end_row = 0;
903        self.view.buffered_df = None;
904    }
905
906    /// View state for a new root: no query text, no filters or sort, not drilled, the first
907    /// `locked_columns_count` frozen, buffer dropped, cursor at the top left.
908    fn reset_view_state(&mut self, locked_columns_count: usize) {
909        self.forget_column_changes();
910        self.view.active_query.clear();
911        self.view.active_sql_query.clear();
912        self.view.active_fuzzy_query.clear();
913        self.view.locked_columns_count = locked_columns_count;
914        self.view.filters.clear();
915        self.view.sort_columns.clear();
916        self.view.sort_descending.clear();
917        self.view.sort_ascending = true;
918        self.view.start_row = 0;
919        self.termcol_index = 0;
920        self.clear_column_moves();
921        self.place_cursor_at(0);
922        self.view.drilled_down_group_index = None;
923        self.view.drilled_down_group_key = None;
924        self.view.drilled_down_group_key_columns = None;
925        self.view.grouped = None;
926        self.drop_buffer();
927        self.table_state.select(Some(0));
928    }
929
930    /// Install a query's result as the root with `query` the active bar. Whether a pivot or
931    /// melt survives is the caller's call (SQL runs on it, others on the data as loaded).
932    /// The caller collects.
933    fn install_query_result(
934        &mut self,
935        lf: LazyFrame,
936        schema: Arc<Schema>,
937        query: ActiveQuery,
938        locked_columns_count: usize,
939        steps: Vec<Step>,
940    ) {
941        self.install_base(lf, schema);
942        self.view.base_steps = steps;
943        self.reset_view_state(locked_columns_count);
944        match query {
945            ActiveQuery::Dsl(q) => self.view.active_query = q,
946            #[cfg(feature = "sql")]
947            ActiveQuery::Sql(q) => self.view.active_sql_query = q,
948            ActiveQuery::Fuzzy(q) => self.view.active_fuzzy_query = q,
949        }
950    }
951
952    /// The view no longer shows the pivot or melt, so nothing may run against it.
953    fn forget_reshape(&mut self) {
954        self.view.reshaped_lf = None;
955        self.view.reshape_lineage = None;
956        self.view.reshape_steps = None;
957        self.view.last_pivot_spec = None;
958        self.view.last_melt_spec = None;
959        self.view.reshape_source = None;
960    }
961
962    /// Reset to `original_lf` with its loaded schema; the caller reads the rows.
963    fn reset_lf_to_original(&mut self) {
964        self.install_base(self.original_lf.clone(), self.query_source_schema());
965        self.view.base_steps = Vec::new();
966        self.view.reshape_steps = None;
967        self.view.lineage = None;
968        self.view.reshape_lineage = None;
969        // Back to the data as opened: rows stand for files again, and the notes apply.
970        self.view.drift_column_present = self.drift_at_open;
971        self.view.drift_groups = self.groups_at_open.clone();
972        self.view.notes = self.notes_at_open.clone();
973        self.view.reshaped_lf = None;
974        self.view.reshape_source = None;
975        self.reset_view_state(0);
976        self.restore_footer_count();
977    }
978
979    /// Back to the data as loaded, with nothing applied and no error showing.
980    fn return_to_root(&mut self) {
981        self.reset_lf_to_original();
982        self.error = None;
983        self.suppress_error_display = false;
984        self.view.last_pivot_spec = None;
985        self.view.last_melt_spec = None;
986    }
987
988    /// Back to the data as loaded with nothing applied, for replaying a view's steps.
989    /// Reads nothing.
990    pub(crate) fn reset_view_for_replay(&mut self) {
991        self.return_to_root();
992    }
993
994    /// Back to the table as opened: nothing applied, widths relearned from the first page.
995    pub fn reset(&mut self) {
996        self.widths = ColumnWidths::default();
997        self.return_to_root();
998        self.collect();
999        if self.view.num_rows > 0 {
1000            self.view.start_row = 0;
1001        }
1002    }
1003
1004    /// The state of what a reader read, with the open's paging and row numbers.
1005    pub(crate) fn from_read(read: Read, options: &OpenOptions) -> Result<Self> {
1006        let mut state = Self::from_lazyframe(read.lf, options)?;
1007        state.read_python = read.python;
1008        state.typing = read.typing;
1009        state.decompress_temp_file = read.temp;
1010        Ok(state)
1011    }
1012
1013    pub fn set_row_numbers(&mut self, enabled: bool) {
1014        self.row_numbers = enabled;
1015    }
1016
1017    /// Toggle `#`. Returns whether the frame changed and rows must be reread (a sorted or
1018    /// filtered view of position-less data numbers its rows).
1019    pub fn toggle_row_numbers(&mut self) -> bool {
1020        self.row_numbers = !self.row_numbers;
1021        if self.row_numbers && self.wants_view_numbers() && !self.view.view_numbered {
1022            self.drop_buffer();
1023            self.apply_transformations();
1024            return true;
1025        }
1026        false
1027    }
1028
1029    /// Whether the view would number its own rows with `#` on: sorted or filtered over a
1030    /// scan whose rows lack a position.
1031    fn wants_view_numbers(&self) -> bool {
1032        // Not for a followed file (read from a mark, a row index would count from it), nor a
1033        // store or many files (the index would read every file), nor past its counting range.
1034        let too_many = self
1035            .pristine_rows
1036            .or(self.num_rows_if_valid())
1037            .is_some_and(|rows| rows > crate::formats::row_index::MAX_ROWS);
1038        self.scan_is_the_root()
1039            && self.follow.is_none()
1040            && !self.remote_source
1041            && self.remote_files.is_none()
1042            && self.parquet_count_dir.is_none()
1043            && !too_many
1044            && !self.view.drift_column_present
1045            && !self.source_rows_at_open
1046            && self.pushed_view().is_none()
1047            && (!self.view.filters.is_empty()
1048                || !self.view.sort_columns.is_empty()
1049                || !self.view.sort_ascending)
1050    }
1051
1052    /// Whether the row-number column is shown.
1053    pub fn row_numbers(&self) -> bool {
1054        self.row_numbers
1055    }
1056
1057    /// Row number display start (0 or 1); used by go-to-line to interpret user input.
1058    pub fn row_start_index(&self) -> usize {
1059        self.row_start_index
1060    }
1061}
1062
1063/// The frame counting `lf`'s rows. `len()` is `UInt32`, and summing it over a
1064/// many-file union widens to `UInt128`, which Polars 0.55 cannot reduce (error or
1065/// panic), so the count is cast to `UInt64`.
1066pub(crate) fn row_count_lf(lf: &LazyFrame) -> LazyFrame {
1067    lf.clone().select([len().cast(DataType::UInt64)])
1068}
1069
1070/// The stub shown for binary columns: their blobs are never read into the display
1071/// buffer (keeping scrolling fast); `lf` still has them for export and analysis. From
1072/// the glyph set, so ASCII terminals get `<binary>`.
1073pub(crate) fn binary_stub() -> &'static str {
1074    crate::glyphs::get().binary_stub
1075}
1076
1077/// The most sideways moves held for a draw, bounding the draw's work.
1078const MAX_WAITING_MOVES: usize = 32;
1079
1080/// A sideways move waiting on a draw: the view's own, or the column cursor's.
1081#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1082pub(crate) enum WaitingMove {
1083    View(ColumnMove),
1084    Cursor(CursorMove),
1085}
1086
1087pub(crate) fn visible_slice(df: &DataFrame, offset: usize, len: usize) -> Option<DataFrame> {
1088    let len = len.min(df.height().saturating_sub(offset));
1089    (offset < df.height() && len > 0).then(|| df.slice(offset as i64, len))
1090}
1091
1092/// Everything a checkpoint promises to put back, as a test can compare it: the rows
1093/// each frame of the pipeline reads, the unsorted one analyses read among them
1094/// (collected here, on the test's thread), the schema,
1095/// the query, filters, sort and layout, the reshape, the drill, what the notes say, the
1096/// selection, and the count and buffer the view holds.
1097#[cfg(test)]
1098#[derive(Debug, PartialEq)]
1099pub(crate) struct ViewSnapshot {
1100    rows: std::result::Result<DataFrame, String>,
1101    analysis_rows: std::result::Result<DataFrame, String>,
1102    base_rows: std::result::Result<DataFrame, String>,
1103    reshaped_rows: Option<std::result::Result<DataFrame, String>>,
1104    schema: Arc<Schema>,
1105    queries: [String; 3],
1106    filters: String,
1107    sort: (Vec<String>, Vec<bool>, bool),
1108    layout: (Vec<String>, usize),
1109    reshape: String,
1110    grouped: (bool, bool),
1111    drill: (Option<usize>, Option<Vec<String>>, Option<Vec<String>>),
1112    drift: (bool, Arc<Vec<crate::formats::schema_union::DriftGroup>>),
1113    notes: (Vec<crate::notes::Note>, bool, Vec<crate::notes::Note>),
1114    selection: (Option<usize>, usize, usize),
1115    count: (usize, bool, u64),
1116    buffer: (usize, usize, Option<DataFrame>),
1117    shown: Option<DataFrame>,
1118    error: Option<String>,
1119}
1120
1121#[cfg(test)]
1122impl ViewSnapshot {
1123    /// Whether the view's rows and count had been read.
1124    pub(crate) fn has_rows(&self) -> bool {
1125        self.buffer.2.is_some() && self.count.1
1126    }
1127}
1128
1129#[cfg(test)]
1130impl DataTableState {
1131    pub(crate) fn snapshot(&self) -> ViewSnapshot {
1132        let rows = |lf: &LazyFrame| lf.clone().collect().map_err(|e| e.to_string());
1133        ViewSnapshot {
1134            rows: rows(&self.view.lf),
1135            analysis_rows: rows(&self.analysis_lf()),
1136            base_rows: rows(&self.view.base_lf),
1137            reshaped_rows: self.view.reshaped_lf.as_ref().map(rows),
1138            schema: self.view.schema.clone(),
1139            queries: [
1140                self.view.active_query.clone(),
1141                self.view.active_sql_query.clone(),
1142                self.view.active_fuzzy_query.clone(),
1143            ],
1144            filters: format!("{:?}", self.view.filters),
1145            sort: (
1146                self.view.sort_columns.clone(),
1147                self.view.sort_descending.clone(),
1148                self.view.sort_ascending,
1149            ),
1150            layout: (
1151                self.view.column_order.clone(),
1152                self.view.locked_columns_count,
1153            ),
1154            reshape: format!(
1155                "{:?} {:?} {:?}",
1156                self.view.last_pivot_spec, self.view.last_melt_spec, self.view.reshape_source
1157            ),
1158            grouped: (
1159                self.view.grouped.is_some(),
1160                self.view.group_source.is_some(),
1161            ),
1162            drill: (
1163                self.view.drilled_down_group_index,
1164                self.view.drilled_down_group_key.clone(),
1165                self.view.drilled_down_group_key_columns.clone(),
1166            ),
1167            drift: (
1168                self.view.drift_column_present,
1169                self.view.drift_groups.clone(),
1170            ),
1171            notes: (
1172                self.view.notes.clone(),
1173                self.view.notes_seen,
1174                self.view.view_notes.clone(),
1175            ),
1176            selection: (
1177                self.table_state.selected(),
1178                self.view.start_row,
1179                self.termcol_index,
1180            ),
1181            count: (
1182                self.view.num_rows,
1183                self.view.num_rows_valid,
1184                self.view.len_generation,
1185            ),
1186            buffer: (
1187                self.view.buffered_start_row,
1188                self.view.buffered_end_row,
1189                self.view.buffered_df.clone(),
1190            ),
1191            shown: self.view.df.clone(),
1192            error: self.error.as_ref().map(|e| e.to_string()),
1193        }
1194    }
1195}
1196
1197mod buffer;
1198mod columns;
1199mod copy;
1200mod drawn;
1201mod facts;
1202mod quality;
1203mod query;
1204mod view;
1205
1206pub use buffer::*;
1207pub use copy::*;
1208pub(crate) use drawn::DrawnTable;
1209pub use drawn::{CellHit, DrawnColumns};
1210pub use facts::*;
1211pub use query::*;
1212pub use view::*;
1213
1214#[cfg(test)]
1215mod checkpoint_tests;
1216#[cfg(test)]
1217mod tests;