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    /// The data as loaded is a local Parquet scan: a read decodes whole data pages, so
248    /// thousands of rows cost about what one page does. See `reach_ahead`.
249    decodes_pages: bool,
250    /// Where the rows of the data as loaded start, when it is one CSV scan: found on the
251    /// first window, shared by every copy of this state. See [`csv_marks::CsvMarks`].
252    csv_marks: CsvMarksOf,
253    /// A source that runs the sidebar's filters and sort itself (a SQLite table), while
254    /// the data as loaded is the root: see [`Self::pushed_view`].
255    pushdown: Option<Arc<dyn crate::formats::pushdown::Pushdown>>,
256    /// Stops what the source runs when this state goes.
257    source_hold: Option<crate::formats::sqlite::Hold>,
258    /// How the open reads the data. See [`crate::OpenOptions::read_mode`].
259    read_mode: Option<crate::ReadMode>,
260    /// The format the open read. See [`OpenFacts::read_as`].
261    read_as: Option<crate::FileFormat>,
262    /// The data was downloaded from a remote source before it was read.
263    fetched: bool,
264    /// What the file said besides its rows. See [`OpenFacts::detail`].
265    detail: Option<Arc<crate::formats::text_formats::Detail>>,
266    /// Each loaded column's unit, from the file. See [`OpenFacts::units`].
267    file_units: Arc<Vec<(String, String)>>,
268    /// Uncompressed bytes per row of each column from the footer, for `bytes_per_row`
269    /// before any collect.
270    column_bytes: Vec<(String, usize)>,
271    proximity_threshold: usize,
272    row_numbers: bool,
273    row_start_index: usize,
274    /// What the open did to the reader's rows, as Python calls (names trimmed, text typed).
275    read_python: Vec<String>,
276    /// The columns the read gave a type, and the frame before it did.
277    typing: Typing,
278    /// The notes on the values the types made null, once counted.
279    unfit_notes: Option<Vec<crate::notes::Note>>,
280    /// The notes on the values the view's types made null: for the version counted.
281    changes_unfit: Option<(u64, Vec<crate::notes::Note>)>,
282    /// When set, dataset was loaded with hive partitioning; partition column names for Info panel and predicate pushdown.
283    partition_columns: Option<Vec<String>>,
284    /// The temp file decompressed CSV was written to, kept alive for the lazy scan; shared
285    /// with views scanning it, removed with the last.
286    decompress_temp_file: Option<Arc<Decompressed>>,
287    /// The downloaded remote file this dataset was opened from, held while it is scanned.
288    download: Option<crate::cloud::download::TempDownload>,
289    /// The files a GPS log was read into, which the frame scans; held as `download` is.
290    converted: Vec<crate::cloud::download::TempDownload>,
291    /// The file's other tables, as `--table` names them; see [`OpenFacts::other_tables`].
292    other_tables: Vec<String>,
293    /// When true, use Polars streaming engine for LazyFrame collect when the streaming feature is enabled.
294    polars_streaming: bool,
295    /// When set, `collect()` / `apply_transformations()` skip the blocking collect; the
296    /// caller starts an async one.
297    defer_collect: bool,
298    /// Set by the renderer when `visible_rows` changes; the event loop then starts an async
299    /// collect.
300    pub needs_recollect: bool,
301    /// The watcher of the file this dataset follows (`--follow`), while it does.
302    follow: Option<crate::loading::follow::Follow>,
303    /// For a followed view that filters or sorts: known points (view rows, file row),
304    /// ascending, for the count generation they hold for. Counts read on from the last;
305    /// filtered windows from the one before.
306    follow_known: Option<(u64, Vec<(usize, usize)>)>,
307    /// The sample this view's rows are and the view it was drawn from: the step between
308    /// source and query.
309    sampled: Option<Box<Sampled>>,
310    /// What the view is: everything a checkpoint keeps and puts back.
311    pub(crate) view: View,
312}
313
314/// A view's sample, between source and query. The frames scan `Self::frame`, the
315/// chunks so far, growing as the draw continues.
316pub struct Sampled {
317    /// The view the sample was drawn from, restored when the sample is cleared.
318    source: Box<DataTableState>,
319    sample: crate::analysis::sampling::Sample,
320    rows: Arc<crate::analysis::table_sample::SampleRows>,
321    /// The frame the view's plans scan: the chunks taken so far, on their buffers.
322    frame: Arc<DataFrame>,
323    /// Drawn through the view's query or filters (which it then stands for), not the
324    /// source.
325    through: bool,
326    /// What the draw read, once it ended; `None` while it runs.
327    drawn: Option<crate::analysis::table_sample::Drawn>,
328    /// How a random sample of a stream is drawn, which a view keeps.
329    path: Option<crate::analysis::table_sample::DrawPath>,
330}
331
332impl Sampled {
333    pub fn sample(&self) -> &crate::analysis::sampling::Sample {
334        &self.sample
335    }
336
337    /// The view the sample was drawn from.
338    pub fn source(&self) -> &DataTableState {
339        &self.source
340    }
341
342    /// Whether the sample was drawn from the view's query or filters.
343    pub fn through(&self) -> bool {
344        self.through
345    }
346
347    /// Whether these are the rows `rows` holds: the sample a draw fills.
348    pub(crate) fn holds(&self, rows: &Arc<crate::analysis::table_sample::SampleRows>) -> bool {
349        Arc::ptr_eq(&self.rows, rows)
350    }
351
352    /// Whether rows are still arriving.
353    pub fn drawing(&self) -> bool {
354        self.drawn.is_none()
355    }
356
357    /// How a random sample of a stream is drawn: what draws the same rows again.
358    pub fn path(&self) -> Option<crate::analysis::table_sample::DrawPath> {
359        self.path
360    }
361
362    /// The frame the view's plans scan.
363    #[cfg(test)]
364    pub(crate) fn frame(&self) -> &DataFrame {
365        &self.frame
366    }
367
368    /// Rows the view has taken of the sample.
369    pub fn rows(&self) -> usize {
370        self.frame.height()
371    }
372
373    /// The footer segment: `sample 100,000 of 36.8M`, `sample 1,234+` while drawing,
374    /// `sample about 100,000 of 36.8M` when kept row by row by chance.
375    pub fn label(&self) -> String {
376        let rows = crate::numfmt::group_chrome(self.rows());
377        let Some(drawn) = &self.drawn else {
378            return format!("sample {rows}+");
379        };
380        let about = if drawn.about { "about " } else { "" };
381        let cut = if drawn.cut { ", stopped" } else { "" };
382        match drawn.total {
383            Some(total) if total > self.rows() => {
384                format!(
385                    "sample {about}{rows} of {}{cut}",
386                    crate::home::discover::format_rows(total)
387                )
388            }
389            _ => format!("sample {rows}{cut}"),
390        }
391    }
392}
393
394/// What an open learned besides frame and schema, given once via
395/// [`DataTableState::with_open`] so count, row groups, files and notes agree. Defaults
396/// mean not found.
397#[derive(Default)]
398pub struct OpenFacts {
399    /// A scan of an object store in place: a buffer is one window of whole row groups.
400    pub remote_source: bool,
401    /// Each file's row groups in scan order (one entry for a single object): the count.
402    /// With `remote_files`, one per listed file.
403    pub row_groups: Vec<Vec<usize>>,
404    /// The files of a remote dataset of many, and how to read some of them.
405    pub remote_files: Option<RemoteFiles>,
406    /// Each remote object the dataset reads, as the listing or footer found it.
407    pub remote_objects: Vec<RemoteObject>,
408    /// What the footers said about a many-file dataset's columns.
409    pub dataset: Option<DatasetAtOpen>,
410    /// The pass that reads the rest of the footers, for a dataset opened from a few.
411    pub footers_pending: Option<FootersJoin>,
412    /// Each column's uncompressed bytes per row, from the footers.
413    pub column_bytes: Vec<(String, usize)>,
414    /// The local Parquet hive directory whose footers sum to the count.
415    pub parquet_count_dir: Option<PathBuf>,
416    /// What finding and reading the dataset cost.
417    pub measurements: Arc<crate::loading::measurements::Meter>,
418    /// What the open itself has to say. See `DataTableState::open_notes`.
419    pub open_notes: Vec<crate::notes::Note>,
420    /// The lake format whose plain files this dataset is. See
421    /// [`DataTableState::not_the_table`].
422    pub not_the_table: Option<&'static str>,
423    /// What a read through a format spec found.
424    pub format_read: Option<Arc<crate::formats::Read>>,
425    /// What a read through a delimited spec found.
426    pub delimited: Option<Arc<crate::formats::delimited_spec::DelimitedRead>>,
427    /// The downloaded file the frame scans, held for as long as the state lives.
428    pub download: Option<crate::cloud::download::TempDownload>,
429    /// The files a GPS log was read into, which the frame scans.
430    pub converted: Vec<crate::cloud::download::TempDownload>,
431    /// The file's other tables as `--table` names them, with row counts where known, for
432    /// Info's Schema tab. Empty for a file of one.
433    pub other_tables: Vec<String>,
434    /// A source that runs the sidebar's filters and sort itself: a SQLite table.
435    pub pushdown: Option<Arc<dyn crate::formats::pushdown::Pushdown>>,
436    /// What stops that source's statements when the dataset goes.
437    pub hold: Option<crate::formats::sqlite::Hold>,
438    /// How the open reads the data. See [`crate::OpenOptions::read_mode`].
439    pub read_mode: Option<crate::ReadMode>,
440    /// The format the open read as, after sniffing and spec matching (which the name may
441    /// not say); Copy as Python and the export default follow it.
442    pub read_as: Option<crate::FileFormat>,
443    /// Downloaded from a remote source before reading (not a local stream conversion or
444    /// stdin spool, though held the same way).
445    pub fetched: bool,
446    /// What the file said besides its rows, for the Info panel.
447    pub detail: Option<Arc<crate::formats::text_formats::Detail>>,
448    /// Rows decoded straight from the file by a reader (NumPy array, audio frames), and
449    /// how many: deep pages and the count need no row index.
450    pub records: Option<(Arc<dyn crate::formats::pushdown::Windowed>, usize)>,
451    /// Each column's unit, where the file says one.
452    pub units: Vec<(String, String)>,
453    /// Lines still being indexed behind the first rows: the frames grow as they are.
454    pub indexing: Option<Arc<crate::formats::lines::Lines>>,
455    /// The lines of several files, which `#` numbers by their line in their own file.
456    pub numbering: Option<Arc<crate::formats::lines::Lines>>,
457    /// The columns the read gave a type, for the count of what did not fit.
458    pub typing: Typing,
459}
460
461/// The footers' account of a dataset of many files.
462pub struct DatasetAtOpen {
463    pub schema: crate::formats::schema_union::DatasetSchema,
464    /// Each file's row count in scan order; empty unless all are known (when the scan
465    /// numbers rows).
466    pub file_rows: Vec<usize>,
467    /// Every file's path or URL, in scan order.
468    pub files: Vec<String>,
469}
470
471/// Rows the display buffer may hold when `performance.max_buffered_rows` is unset; also
472/// a remote scan's window when the cap is off.
473pub const DEFAULT_MAX_BUFFERED_ROWS: usize = 100_000;
474
475/// The marks of the data as loaded, made when a window first asks for them.
476type CsvMarksOf = Arc<std::sync::OnceLock<Option<Arc<csv_marks::CsvMarks>>>>;
477
478/// Seeds `DataTableState::len_generation`, unique per state, so a count for one dataset
479/// never validates another.
480static NEXT_LEN_GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
481
482fn next_len_generation() -> u64 {
483    NEXT_LEN_GENERATION.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
484}
485
486/// The in-memory frame `lf` scans, when it is a scan of one.
487fn scanned_frame(lf: &LazyFrame) -> Option<Arc<DataFrame>> {
488    match &lf.logical_plan {
489        polars::lazy::dsl::DslPlan::DataFrameScan { df, .. } => Some(df.clone()),
490        _ => None,
491    }
492}
493
494/// Calls `f` on each plan `plan` reads from.
495pub(crate) fn for_each_input(
496    plan: &mut polars::lazy::dsl::DslPlan,
497    f: &mut dyn FnMut(&mut polars::lazy::dsl::DslPlan),
498) {
499    use polars::lazy::dsl::DslPlan;
500    match plan {
501        DslPlan::Sort { input, .. }
502        | DslPlan::Select { input, .. }
503        | DslPlan::GroupBy { input, .. }
504        | DslPlan::Filter { input, .. }
505        | DslPlan::Distinct { input, .. }
506        | DslPlan::Slice { input, .. }
507        | DslPlan::HStack { input, .. }
508        | DslPlan::MatchToSchema { input, .. }
509        | DslPlan::MapFunction { input, .. }
510        | DslPlan::Sink { input, .. }
511        | DslPlan::Cache { input, .. }
512        | DslPlan::Pivot { input, .. } => f(Arc::make_mut(input)),
513        DslPlan::Union { inputs, .. }
514        | DslPlan::HConcat { inputs, .. }
515        | DslPlan::SinkMultiple { inputs } => inputs.iter_mut().for_each(f),
516        DslPlan::PipeWithSchema { input, .. } => {
517            let mut inputs = input.to_vec();
518            inputs.iter_mut().for_each(&mut *f);
519            *input = inputs.into();
520        }
521        DslPlan::Join {
522            input_left,
523            input_right,
524            ..
525        } => {
526            f(Arc::make_mut(input_left));
527            f(Arc::make_mut(input_right));
528        }
529        DslPlan::Gather { input, idxs, .. } => {
530            f(Arc::make_mut(input));
531            f(Arc::make_mut(idxs));
532        }
533        DslPlan::ExtContext { input, contexts } => {
534            f(Arc::make_mut(input));
535            contexts.iter_mut().for_each(f);
536        }
537        _ => {}
538    }
539}
540
541impl DataTableState {
542    pub fn new(
543        lf: LazyFrame,
544        pages_lookahead: Option<usize>,
545        pages_lookback: Option<usize>,
546        max_buffered_rows: Option<usize>,
547        max_buffered_mb: Option<usize>,
548        polars_streaming: bool,
549    ) -> Result<Self> {
550        let options = OpenOptions {
551            pages_lookahead,
552            pages_lookback,
553            max_buffered_rows,
554            max_buffered_mb,
555            polars_streaming,
556            ..OpenOptions::default()
557        };
558        Self::from_lazyframe(lf, &options)
559    }
560
561    /// `schema` without the hidden row index, and whether it had one (the rows' source
562    /// position, which `#` shows).
563    fn without_source_rows(schema: Arc<Schema>) -> (Arc<Schema>, bool) {
564        if !schema.contains(crate::formats::schema_union::DRIFT_COLUMN) {
565            return (schema, false);
566        }
567        let mut schema = (*schema).clone();
568        schema.shift_remove(crate::formats::schema_union::DRIFT_COLUMN);
569        (Arc::new(schema), true)
570    }
571
572    /// Create state from an existing LazyFrame (e.g. from Python or in-memory). Uses OpenOptions for display/buffer settings.
573    pub fn from_lazyframe(lf: LazyFrame, options: &crate::OpenOptions) -> Result<Self> {
574        let schema = lf.clone().collect_schema()?;
575        Self::from_schema_and_lazyframe(schema, lf, options, None)
576    }
577
578    /// State from a pre-collected schema and LazyFrame (phased loading), without
579    /// `collect_schema()`; `df` is `None` so headers render while the first collect runs.
580    /// Hive partition columns come first.
581    pub fn from_schema_and_lazyframe(
582        schema: Arc<Schema>,
583        lf: LazyFrame,
584        options: &crate::OpenOptions,
585        partition_columns: Option<Vec<String>>,
586    ) -> Result<Self> {
587        let (schema, source_rows_at_open) = Self::without_source_rows(schema);
588        let column_order: Vec<String> = if let Some(ref part) = partition_columns {
589            let part_set: HashSet<&str> = part.iter().map(String::as_str).collect();
590            let rest: Vec<String> = schema
591                .iter_names()
592                .map(|s| s.to_string())
593                .filter(|c| !part_set.contains(c.as_str()))
594                .collect();
595            part.iter().cloned().chain(rest).collect()
596        } else {
597            schema.iter_names().map(|s| s.to_string()).collect()
598        };
599        Ok(Self {
600            original_lf: lf.clone(),
601            original_schema: schema.clone(),
602            table_state: TableState::default(),
603            visible_rows: 0,
604            termcol_index: 0,
605            visible_termcols: 0,
606            scroll_room: None,
607            column_moves: Vec::new(),
608            page_trail: Vec::new(),
609            on_screen: None,
610            drawn: None,
611            page_cells: Default::default(),
612            error: None,
613            suppress_error_display: false,
614            pristine_rows: None,
615            root_generation: next_len_generation(),
616            parquet_count_dir: None,
617            measurements: Arc::new(crate::loading::measurements::Meter::default()),
618            reveal_cursor: false,
619            widths: ColumnWidths::default(),
620            pages_lookahead: options.pages_lookahead.unwrap_or(3),
621            pages_lookback: options.pages_lookback.unwrap_or(3),
622            max_buffered_rows: options
623                .max_buffered_rows
624                .unwrap_or(DEFAULT_MAX_BUFFERED_ROWS),
625            max_buffered_mb: options.max_buffered_mb.unwrap_or(512),
626            remote_source: false,
627            row_group_offsets: None,
628            remote_files: None,
629            remote_objects: None,
630            dataset_schema: None,
631            drift_at_open: false,
632            groups_at_open: Arc::new(Vec::new()),
633            source_rows_at_open,
634            indexing: None,
635            numbering: None,
636            row_estimate: None,
637            indexing_notes: Vec::new(),
638            indexing_guessed: false,
639            drift_file_starts: Vec::new(),
640            drift_file_group: Vec::new(),
641            drift_files: Vec::new(),
642            footers_pending: None,
643            open_notes: Vec::new(),
644            not_the_table: None,
645            format_read: None,
646            delimited: None,
647            fixed_window: None,
648            csv_marks: CsvMarksOf::default(),
649            decodes_pages: buffer::decodes_pages(&lf),
650            pushdown: None,
651            source_hold: None,
652            read_mode: None,
653            read_as: None,
654            fetched: false,
655            detail: None,
656            file_units: Arc::new(Vec::new()),
657            notes_at_open: Vec::new(),
658            drift_dataset_rows: 0,
659            dataset_at_open: None,
660            read_as_text: Vec::new(),
661            column_bytes: Vec::new(),
662            // Set when the rows on screen are known.
663            proximity_threshold: 0,
664            row_numbers: options.row_numbers,
665            row_start_index: options.row_start_index,
666            read_python: Vec::new(),
667            typing: Typing::default(),
668            unfit_notes: None,
669            changes_unfit: None,
670            partition_columns,
671            decompress_temp_file: None,
672            download: None,
673            converted: Vec::new(),
674            other_tables: Vec::new(),
675            polars_streaming: options.polars_streaming,
676            defer_collect: false,
677            needs_recollect: false,
678            follow: None,
679            follow_known: None,
680            sampled: None,
681            view: View {
682                unsorted_lf: None,
683                base_lf: lf.clone(),
684                base_schema: schema.clone(),
685                lf,
686                df: None,
687                locked_df: None,
688                start_row: 0,
689                schema,
690                num_rows: 0,
691                num_rows_valid: false,
692                len_generation: next_len_generation(),
693                filters: Vec::new(),
694                sort_columns: Vec::new(),
695                sort_descending: Vec::new(),
696                sort_ascending: true,
697                cursor_column: None,
698                cursor_at: 0,
699                active_query: String::new(),
700                active_sql_query: String::new(),
701                query_order: Vec::new(),
702                active_fuzzy_query: String::new(),
703                column_order,
704                locked_columns_count: 0,
705                frozen_fit: (0, 0),
706                grouped: None,
707                group_source: None,
708                reshaped_lf: None,
709                drilled_down_group_index: None,
710                drilled_down_group_key: None,
711                drilled_down_group_key_columns: None,
712                drift_column_present: false,
713                drift_groups: Arc::new(Vec::new()),
714                view_numbered: false,
715                notes: Vec::new(),
716                notes_seen: false,
717                view_notes: Vec::new(),
718                observed_bytes_per_row: None,
719                buffered_start_row: 0,
720                buffered_end_row: 0,
721                buffered_df: None,
722                drawn_start: 0,
723                last_pivot_spec: None,
724                last_melt_spec: None,
725                reshape_source: None,
726                base_steps: Vec::new(),
727                column_changes: Vec::new(),
728                changes_version: 0,
729                changes_dropped: Vec::new(),
730                reshape_steps: None,
731                lineage: None,
732                reshape_lineage: None,
733            },
734        })
735    }
736
737    /// The state with everything the open found in `facts`: the only way findings reach a
738    /// state, while pristine, applied in dependency order (files before row groups,
739    /// which set the count). Later learning goes through [`Self::join_dataset_schema`] and
740    /// [`Self::count_landed`].
741    pub fn with_open(mut self, facts: OpenFacts) -> Self {
742        let OpenFacts {
743            remote_source,
744            row_groups,
745            remote_files,
746            remote_objects,
747            dataset,
748            footers_pending,
749            column_bytes,
750            parquet_count_dir,
751            measurements,
752            open_notes,
753            not_the_table,
754            format_read,
755            delimited,
756            download,
757            converted,
758            other_tables,
759            pushdown,
760            hold,
761            read_mode,
762            read_as,
763            fetched,
764            detail,
765            records,
766            units,
767            indexing,
768            numbering,
769            typing,
770        } = facts;
771        self.numbering = numbering;
772        self.typing = typing;
773        debug_assert!(
774            self.is_pristine(),
775            "an open's facts are for the data as loaded"
776        );
777        self.remote_source = remote_source;
778        self.remote_files = remote_files;
779        self.remote_objects = (!remote_objects.is_empty()).then(|| {
780            Arc::new(
781                remote_objects
782                    .into_iter()
783                    .map(|object| (object.url.clone(), object))
784                    .collect(),
785            )
786        });
787        if !row_groups.is_empty() {
788            if self.remote_files.is_some() {
789                self.record_file_row_groups(&row_groups);
790            } else {
791                let flat: Vec<usize> = row_groups.into_iter().flatten().collect();
792                self.record_row_groups(&flat);
793            }
794        }
795        if let Some(DatasetAtOpen {
796            schema,
797            file_rows,
798            files,
799        }) = dataset
800        {
801            self.record_dataset_schema(schema, &file_rows, &files);
802        }
803        self.footers_pending = footers_pending;
804        self.column_bytes = column_bytes;
805        self.parquet_count_dir = parquet_count_dir;
806        self.measurements = measurements;
807        self.open_notes = open_notes;
808        self.not_the_table = not_the_table;
809        self.fixed_window = format_read
810            .as_ref()
811            .map(|read| read.records.clone() as Arc<dyn crate::formats::pushdown::Windowed>);
812        if let Some(read) = &format_read {
813            // The reader counted records from the file size; a frame count would build the whole
814            // row index.
815            self.set_num_rows(read.records.rows());
816        }
817        self.pushdown = pushdown;
818        self.source_hold = hold;
819        self.format_read = format_read;
820        self.delimited = delimited;
821        self.download = download;
822        self.converted = converted;
823        self.other_tables = other_tables;
824        self.read_mode = read_mode;
825        self.read_as = read_as;
826        self.fetched = fetched;
827        self.detail = detail;
828        if let Some((window, rows)) = records {
829            // The reader knows its rows (a frame count would build the row index); lines still
830            // indexing know only some.
831            if indexing.is_none() {
832                self.set_num_rows(rows);
833            }
834            self.fixed_window = Some(window);
835        }
836        if let Some(lines) = &indexing {
837            // The lines' own notes, replaced once all lines are in and can describe the file.
838            self.indexing_guessed = self
839                .open_notes
840                .iter()
841                .any(|n| n.summary.starts_with(crate::formats::lines::GUESSED));
842            self.indexing_notes = crate::formats::lines::notes(lines, self.indexing_guessed);
843        }
844        self.indexing = indexing;
845        self.file_units = Arc::new(units);
846        self
847    }
848
849    /// Make `lf` the data as loaded with `schema` (root, base and shown frame) until the
850    /// caller relays filters and sort. Rows and counts from the old root drop, and older
851    /// checkpoints no longer apply.
852    fn replace_root(&mut self, lf: LazyFrame, schema: Arc<Schema>) {
853        // The records, or the table, no longer stand for the root.
854        self.fixed_window = None;
855        self.csv_marks = CsvMarksOf::default();
856        self.decodes_pages = buffer::decodes_pages(&lf);
857        self.pushdown = None;
858        self.root_generation = next_len_generation();
859        self.invalidate_num_rows();
860        self.original_schema = schema.clone();
861        self.view.base_schema = schema.clone();
862        self.view.schema = schema;
863        self.original_lf = lf.clone();
864        self.view.base_lf = lf.clone();
865        self.view.lf = lf;
866        self.view.unsorted_lf = None;
867        self.view.base_steps = Vec::new();
868        self.view.reshape_steps = None;
869        self.drop_buffer();
870    }
871
872    /// Make `lf` the shown frame and the base for filters and sort, `schema` its schema,
873    /// every column in view; row counts invalidated.
874    fn install_base(&mut self, lf: LazyFrame, schema: Arc<Schema>) {
875        self.invalidate_num_rows();
876        // A new frame is the user's own projection: its rows no longer stand for a file's, so
877        // no drift marks and file notes no longer apply.
878        self.view.drift_column_present = false;
879        self.view.view_numbered = false;
880        self.view.drift_groups = Arc::new(Vec::new());
881        self.view.notes = Vec::new();
882        self.view.view_notes = Vec::new();
883        // Measure the new shape afresh, not from the old width.
884        self.view.observed_bytes_per_row = None;
885        // A new frame is in no order a query named; `sql_query` names it after.
886        self.view.query_order = Vec::new();
887        // A column may keep its name and type and hold other values now.
888        self.widths.relearn();
889        self.view.base_lf = lf.clone();
890        self.view.base_schema = schema.clone();
891        self.view.lf = lf;
892        self.view.unsorted_lf = None;
893        // Callers say how the base was built; one that does not leaves a script saying so.
894        self.view.base_steps = vec![Step::Unreproducible(
895            "datui built the view from here in a way it cannot write as Python".to_string(),
896        )];
897        self.view.schema = schema;
898        self.view.column_order = self
899            .view
900            .schema
901            .iter_names()
902            .map(|s| s.to_string())
903            .collect();
904        // No column is a loaded one until the caller says which are.
905        self.view.lineage = Some(Arc::default());
906        self.settle_cursor();
907        // A query that groups records its source after installing its result.
908        self.view.group_source = None;
909        self.drop_buffer();
910    }
911
912    /// Forget rows read through the replaced frame so the next collect reads the new one.
913    fn drop_buffer(&mut self) {
914        self.view.buffered_start_row = 0;
915        self.view.buffered_end_row = 0;
916        self.view.buffered_df = None;
917    }
918
919    /// View state for a new root: no query text, no filters or sort, not drilled, the first
920    /// `locked_columns_count` frozen, buffer dropped, cursor at the top left.
921    fn reset_view_state(&mut self, locked_columns_count: usize) {
922        self.forget_column_changes();
923        self.view.active_query.clear();
924        self.view.active_sql_query.clear();
925        self.view.active_fuzzy_query.clear();
926        self.view.locked_columns_count = locked_columns_count;
927        self.view.filters.clear();
928        self.view.sort_columns.clear();
929        self.view.sort_descending.clear();
930        self.view.sort_ascending = true;
931        self.view.start_row = 0;
932        self.termcol_index = 0;
933        self.clear_column_moves();
934        self.place_cursor_at(0);
935        self.view.drilled_down_group_index = None;
936        self.view.drilled_down_group_key = None;
937        self.view.drilled_down_group_key_columns = None;
938        self.view.grouped = None;
939        self.drop_buffer();
940        self.table_state.select(Some(0));
941    }
942
943    /// Install a query's result as the root with `query` the active bar. Whether a pivot or
944    /// melt survives is the caller's call (SQL runs on it, others on the data as loaded).
945    /// The caller collects.
946    fn install_query_result(
947        &mut self,
948        lf: LazyFrame,
949        schema: Arc<Schema>,
950        query: ActiveQuery,
951        locked_columns_count: usize,
952        steps: Vec<Step>,
953    ) {
954        self.install_base(lf, schema);
955        self.view.base_steps = steps;
956        self.reset_view_state(locked_columns_count);
957        match query {
958            ActiveQuery::Dsl(q) => self.view.active_query = q,
959            #[cfg(feature = "sql")]
960            ActiveQuery::Sql(q) => self.view.active_sql_query = q,
961            ActiveQuery::Fuzzy(q) => self.view.active_fuzzy_query = q,
962        }
963    }
964
965    /// The view no longer shows the pivot or melt, so nothing may run against it.
966    fn forget_reshape(&mut self) {
967        self.view.reshaped_lf = None;
968        self.view.reshape_lineage = None;
969        self.view.reshape_steps = None;
970        self.view.last_pivot_spec = None;
971        self.view.last_melt_spec = None;
972        self.view.reshape_source = None;
973    }
974
975    /// Reset to `original_lf` with its loaded schema; the caller reads the rows.
976    fn reset_lf_to_original(&mut self) {
977        self.install_base(self.original_lf.clone(), self.query_source_schema());
978        self.view.base_steps = Vec::new();
979        self.view.reshape_steps = None;
980        self.view.lineage = None;
981        self.view.reshape_lineage = None;
982        // Back to the data as opened: rows stand for files again, and the notes apply.
983        self.view.drift_column_present = self.drift_at_open;
984        self.view.drift_groups = self.groups_at_open.clone();
985        self.view.notes = self.notes_at_open.clone();
986        self.view.reshaped_lf = None;
987        self.view.reshape_source = None;
988        self.reset_view_state(0);
989        self.restore_footer_count();
990    }
991
992    /// Back to the data as loaded, with nothing applied and no error showing.
993    fn return_to_root(&mut self) {
994        self.reset_lf_to_original();
995        self.error = None;
996        self.suppress_error_display = false;
997        self.view.last_pivot_spec = None;
998        self.view.last_melt_spec = None;
999    }
1000
1001    /// Back to the data as loaded with nothing applied, for replaying a view's steps.
1002    /// Reads nothing.
1003    pub(crate) fn reset_view_for_replay(&mut self) {
1004        self.return_to_root();
1005    }
1006
1007    /// Back to the table as opened: nothing applied, widths relearned from the first page.
1008    pub fn reset(&mut self) {
1009        self.widths = ColumnWidths::default();
1010        self.return_to_root();
1011        self.collect();
1012        if self.view.num_rows > 0 {
1013            self.view.start_row = 0;
1014        }
1015    }
1016
1017    /// The state of what a reader read, with the open's paging and row numbers.
1018    pub(crate) fn from_read(read: Read, options: &OpenOptions) -> Result<Self> {
1019        let mut state = Self::from_lazyframe(read.lf, options)?;
1020        state.read_python = read.python;
1021        state.typing = read.typing;
1022        state.decompress_temp_file = read.temp;
1023        Ok(state)
1024    }
1025
1026    pub fn set_row_numbers(&mut self, enabled: bool) {
1027        self.row_numbers = enabled;
1028    }
1029
1030    /// Toggle `#`. Returns whether the frame changed and rows must be reread (a sorted or
1031    /// filtered view of position-less data numbers its rows).
1032    pub fn toggle_row_numbers(&mut self) -> bool {
1033        self.row_numbers = !self.row_numbers;
1034        if self.row_numbers && self.wants_view_numbers() && !self.view.view_numbered {
1035            self.drop_buffer();
1036            self.apply_transformations();
1037            return true;
1038        }
1039        false
1040    }
1041
1042    /// Whether the view would number its own rows with `#` on: sorted or filtered over a
1043    /// scan whose rows lack a position.
1044    fn wants_view_numbers(&self) -> bool {
1045        // Not for a followed file (read from a mark, a row index would count from it), nor a
1046        // store or many files (the index would read every file), nor past its counting range.
1047        let too_many = self
1048            .pristine_rows
1049            .or(self.num_rows_if_valid())
1050            .is_some_and(|rows| rows > crate::formats::row_index::MAX_ROWS);
1051        self.scan_is_the_root()
1052            && self.follow.is_none()
1053            && !self.remote_source
1054            && self.remote_files.is_none()
1055            && self.parquet_count_dir.is_none()
1056            && !too_many
1057            && !self.view.drift_column_present
1058            && !self.source_rows_at_open
1059            && self.pushed_view().is_none()
1060            && (!self.view.filters.is_empty()
1061                || !self.view.sort_columns.is_empty()
1062                || !self.view.sort_ascending)
1063    }
1064
1065    /// Whether the row-number column is shown.
1066    pub fn row_numbers(&self) -> bool {
1067        self.row_numbers
1068    }
1069
1070    /// Row number display start (0 or 1); used by go-to-line to interpret user input.
1071    pub fn row_start_index(&self) -> usize {
1072        self.row_start_index
1073    }
1074}
1075
1076/// The frame counting `lf`'s rows. `len()` is `UInt32`, and summing it over a
1077/// many-file union widens to `UInt128`, which Polars 0.55 cannot reduce (error or
1078/// panic), so the count is cast to `UInt64`.
1079pub(crate) fn row_count_lf(lf: &LazyFrame) -> LazyFrame {
1080    lf.clone().select([len().cast(DataType::UInt64)])
1081}
1082
1083/// The stub shown for binary columns: their blobs are never read into the display
1084/// buffer (keeping scrolling fast); `lf` still has them for export and analysis. From
1085/// the glyph set, so ASCII terminals get `<binary>`.
1086pub(crate) fn binary_stub() -> &'static str {
1087    crate::glyphs::get().binary_stub
1088}
1089
1090/// The most sideways moves held for a draw, bounding the draw's work.
1091const MAX_WAITING_MOVES: usize = 32;
1092
1093/// A sideways move waiting on a draw: the view's own, or the column cursor's.
1094#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1095pub(crate) enum WaitingMove {
1096    View(ColumnMove),
1097    Cursor(CursorMove),
1098}
1099
1100pub(crate) fn visible_slice(df: &DataFrame, offset: usize, len: usize) -> Option<DataFrame> {
1101    let len = len.min(df.height().saturating_sub(offset));
1102    (offset < df.height() && len > 0).then(|| df.slice(offset as i64, len))
1103}
1104
1105/// Everything a checkpoint promises to put back, as a test can compare it: the rows
1106/// each frame of the pipeline reads, the unsorted one analyses read among them
1107/// (collected here, on the test's thread), the schema,
1108/// the query, filters, sort and layout, the reshape, the drill, what the notes say, the
1109/// selection, and the count and buffer the view holds.
1110#[cfg(test)]
1111#[derive(Debug, PartialEq)]
1112pub(crate) struct ViewSnapshot {
1113    rows: std::result::Result<DataFrame, String>,
1114    analysis_rows: std::result::Result<DataFrame, String>,
1115    base_rows: std::result::Result<DataFrame, String>,
1116    reshaped_rows: Option<std::result::Result<DataFrame, String>>,
1117    schema: Arc<Schema>,
1118    queries: [String; 3],
1119    filters: String,
1120    sort: (Vec<String>, Vec<bool>, bool),
1121    layout: (Vec<String>, usize),
1122    reshape: String,
1123    grouped: (bool, bool),
1124    drill: (Option<usize>, Option<Vec<String>>, Option<Vec<String>>),
1125    drift: (bool, Arc<Vec<crate::formats::schema_union::DriftGroup>>),
1126    notes: (Vec<crate::notes::Note>, bool, Vec<crate::notes::Note>),
1127    selection: (Option<usize>, usize, usize),
1128    count: (usize, bool, u64),
1129    buffer: (usize, usize, Option<DataFrame>),
1130    shown: Option<DataFrame>,
1131    error: Option<String>,
1132}
1133
1134#[cfg(test)]
1135impl ViewSnapshot {
1136    /// Whether the view's rows and count had been read.
1137    pub(crate) fn has_rows(&self) -> bool {
1138        self.buffer.2.is_some() && self.count.1
1139    }
1140}
1141
1142#[cfg(test)]
1143impl DataTableState {
1144    pub(crate) fn snapshot(&self) -> ViewSnapshot {
1145        let rows = |lf: &LazyFrame| lf.clone().collect().map_err(|e| e.to_string());
1146        ViewSnapshot {
1147            rows: rows(&self.view.lf),
1148            analysis_rows: rows(&self.analysis_lf()),
1149            base_rows: rows(&self.view.base_lf),
1150            reshaped_rows: self.view.reshaped_lf.as_ref().map(rows),
1151            schema: self.view.schema.clone(),
1152            queries: [
1153                self.view.active_query.clone(),
1154                self.view.active_sql_query.clone(),
1155                self.view.active_fuzzy_query.clone(),
1156            ],
1157            filters: format!("{:?}", self.view.filters),
1158            sort: (
1159                self.view.sort_columns.clone(),
1160                self.view.sort_descending.clone(),
1161                self.view.sort_ascending,
1162            ),
1163            layout: (
1164                self.view.column_order.clone(),
1165                self.view.locked_columns_count,
1166            ),
1167            reshape: format!(
1168                "{:?} {:?} {:?}",
1169                self.view.last_pivot_spec, self.view.last_melt_spec, self.view.reshape_source
1170            ),
1171            grouped: (
1172                self.view.grouped.is_some(),
1173                self.view.group_source.is_some(),
1174            ),
1175            drill: (
1176                self.view.drilled_down_group_index,
1177                self.view.drilled_down_group_key.clone(),
1178                self.view.drilled_down_group_key_columns.clone(),
1179            ),
1180            drift: (
1181                self.view.drift_column_present,
1182                self.view.drift_groups.clone(),
1183            ),
1184            notes: (
1185                self.view.notes.clone(),
1186                self.view.notes_seen,
1187                self.view.view_notes.clone(),
1188            ),
1189            selection: (
1190                self.table_state.selected(),
1191                self.view.start_row,
1192                self.termcol_index,
1193            ),
1194            count: (
1195                self.view.num_rows,
1196                self.view.num_rows_valid,
1197                self.view.len_generation,
1198            ),
1199            buffer: (
1200                self.view.buffered_start_row,
1201                self.view.buffered_end_row,
1202                self.view.buffered_df.clone(),
1203            ),
1204            shown: self.view.df.clone(),
1205            error: self.error.as_ref().map(|e| e.to_string()),
1206        }
1207    }
1208}
1209
1210mod buffer;
1211mod columns;
1212mod copy;
1213mod csv_marks;
1214mod drawn;
1215mod facts;
1216mod quality;
1217mod query;
1218mod view;
1219
1220pub use buffer::*;
1221pub use copy::*;
1222pub(crate) use drawn::DrawnTable;
1223pub use drawn::{CellHit, DrawnColumns};
1224pub use facts::*;
1225pub use query::*;
1226pub use view::*;
1227
1228#[cfg(test)]
1229mod checkpoint_tests;
1230#[cfg(test)]
1231mod fill_tests;
1232#[cfg(test)]
1233mod tests;