Skip to main content

datui_lib/table/
view.rs

1//! The view's settings, its checkpoint (`rollback_point`, `roll_back`,
2//! `try_transition`) and a followed file's view.
3
4use super::*;
5
6/// The view as it stood before a query or view replaced it: a checkpoint. A query can
7/// plan and still fail on its rows (a value that will not cast); then the table returns
8/// to this, rows and all. Frames and buffers are shared, not copied. Taken by
9/// [`DataTableState::rollback_point`] or [`DataTableState::try_transition`], restored by
10/// [`DataTableState::roll_back`].
11pub struct ViewRollback {
12    /// The data as loaded when this was taken; see [`DataTableState::roll_back`].
13    root_generation: u64,
14    /// A count of this frame that came back after it was replaced, to return with it.
15    counted: Option<CountedRows>,
16    table_state: TableState,
17    termcol_index: usize,
18    view: View,
19}
20
21impl ViewRollback {
22    /// A background count of frame `len_generation` came back while this checkpoint
23    /// was waiting. Kept when the frame is the one this restores, so the rows and the
24    /// count return together; returns whether it was.
25    pub fn count_landed(
26        &mut self,
27        len_generation: u64,
28        rows: usize,
29        file_row_groups: Option<&[Vec<usize>]>,
30    ) -> bool {
31        let ours = len_generation == self.view.len_generation;
32        if ours {
33            self.counted = Some(CountedRows {
34                rows,
35                file_row_groups: file_row_groups.map(<[_]>::to_vec),
36            });
37        }
38        ours
39    }
40}
41
42/// A row count read in the background: the total, and for a remote dataset of many
43/// files, the rows in each row group of each file.
44pub(super) struct CountedRows {
45    rows: usize,
46    file_row_groups: Option<Vec<Vec<usize>>>,
47}
48
49impl DataTableState {
50    // Getter methods for view creation
51    /// Filters for a view: while drilled into a group these are the grouped view's,
52    /// which is what a view reproduces (it cannot express a drill-down).
53    pub fn get_filters(&self) -> &[FilterStatement] {
54        match &self.view.grouped {
55            Some(view) => &view.filters,
56            None => &self.view.filters,
57        }
58    }
59
60    pub fn get_sort_columns(&self) -> &[String] {
61        match &self.view.grouped {
62            Some(view) => &view.sort_columns,
63            None => &self.view.sort_columns,
64        }
65    }
66
67    pub fn get_sort_ascending(&self) -> bool {
68        match &self.view.grouped {
69            Some(view) => view.sort_ascending,
70            None => self.view.sort_ascending,
71        }
72    }
73
74    pub fn get_sort_descending(&self) -> &[bool] {
75        match &self.view.grouped {
76            Some(view) => &view.sort_descending,
77            None => &self.view.sort_descending,
78        }
79    }
80
81    /// Filters applied to the frame on screen (inside the group while drilled). This is
82    /// what the Sort & Filter sidebar shows and edits.
83    pub fn view_filters(&self) -> &[FilterStatement] {
84        &self.view.filters
85    }
86
87    pub fn view_sort_columns(&self) -> &[String] {
88        &self.view.sort_columns
89    }
90
91    pub fn view_sort_ascending(&self) -> bool {
92        self.view.sort_ascending
93    }
94
95    pub fn view_sort_descending(&self) -> &[bool] {
96        &self.view.sort_descending
97    }
98
99    /// The header's sort marks: the sidebar's sort, or else the ORDER BY of the SQL
100    /// in effect, while its own rows are on screen (not a group drilled into).
101    pub(crate) fn header_sort(&self) -> (Vec<String>, Vec<bool>) {
102        if self.view.sort_columns.is_empty() && self.view.grouped.is_none() {
103            self.view.query_order.iter().cloned().unzip()
104        } else {
105            (
106                self.view.sort_columns.clone(),
107                self.view.sort_descending.clone(),
108            )
109        }
110    }
111
112    /// The pivot/melt result in effect, for a snapshot that may need to put it back.
113    #[cfg(test)]
114    pub(crate) fn reshaped_lf_clone(&self) -> Option<LazyFrame> {
115        self.view.reshaped_lf.clone()
116    }
117
118    pub fn get_column_order(&self) -> &[String] {
119        &self.view.column_order
120    }
121
122    /// Whether the table shows its defaults (no query, filters, sort or reshape, file
123    /// order, nothing locked): a view saved from here carries nothing and, matching by
124    /// schema, would shadow real views.
125    pub fn is_at_defaults(&self) -> bool {
126        self.sampled.is_none()
127            && self.view.column_changes.is_empty()
128            && self.view.active_query.is_empty()
129            && self.view.active_sql_query.is_empty()
130            && self.view.active_fuzzy_query.is_empty()
131            && self.view.filters.is_empty()
132            && self.view.sort_columns.is_empty()
133            && self.view.last_pivot_spec.is_none()
134            && self.view.last_melt_spec.is_none()
135            && self.locked_columns_count() == 0
136            && self.view.column_order.iter().map(String::as_str).eq(self
137                .view
138                .schema
139                .iter_names()
140                .map(|s| s.as_str()))
141    }
142
143    pub fn get_active_query(&self) -> &str {
144        &self.view.active_query
145    }
146
147    pub fn get_active_sql_query(&self) -> &str {
148        &self.view.active_sql_query
149    }
150
151    /// Whether the rows on screen can be read at all: a sort, a filter or a column
152    /// named in the layout that the frame does not have fails here. Resolves the plan
153    /// and reads no rows.
154    pub fn check_plan(&self) -> PolarsResult<()> {
155        self.view
156            .lf
157            .clone()
158            .select(self.binary_stub_exprs())
159            .collect_schema()
160            .map(|_| ())
161    }
162
163    /// The view as it is now, to go back to if a query fails while running.
164    pub fn rollback_point(&self) -> ViewRollback {
165        ViewRollback {
166            root_generation: self.root_generation,
167            counted: None,
168            table_state: self.table_state,
169            termcol_index: self.termcol_index,
170            view: self.view.clone(),
171        }
172    }
173
174    /// Restore the view `rollback_point` saved, with no error showing, with its row count,
175    /// buffer, and any count that landed meanwhile ([`ViewRollback::count_landed`]). Reads
176    /// nothing. A checkpoint over since-replaced data (a footer join, a column read as text)
177    /// would mix roots, so the view returns to the data as loaded instead.
178    pub fn roll_back(&mut self, saved: ViewRollback) {
179        if saved.root_generation != self.root_generation {
180            self.return_to_root();
181            return;
182        }
183        self.widths.keep_learned();
184        self.view = saved.view;
185        self.table_state = saved.table_state;
186        self.termcol_index = saved.termcol_index;
187        self.clear_column_moves();
188        self.reveal_cursor = true;
189        self.error = None;
190        // After the frame, so the count is taken as this frame's.
191        if let Some(counted) = saved.counted {
192            self.take_count(counted.rows, counted.file_row_groups.as_deref());
193        }
194    }
195
196    /// Run `steps` as one view transition, planned but never read, with no error showing.
197    /// A failing step restores the prior view and returns its error; on success the prior
198    /// view comes back with the result, for [`Self::roll_back`] if the new rows fail.
199    pub fn try_transition<T, E>(
200        &mut self,
201        steps: impl FnOnce(&mut Self) -> std::result::Result<T, E>,
202    ) -> std::result::Result<(T, ViewRollback), E> {
203        let saved = self.rollback_point();
204        self.error = None;
205        match self.deferred(steps) {
206            Ok(value) => Ok((value, saved)),
207            Err(e) => {
208                self.roll_back(saved);
209                Err(e)
210            }
211        }
212    }
213
214    /// Run `steps` with every collect they would make left to the caller, who reads
215    /// the rows off the UI thread (`prepare_async_collect`).
216    pub fn deferred<R>(&mut self, steps: impl FnOnce(&mut Self) -> R) -> R {
217        let deferred = std::mem::replace(&mut self.defer_collect, true);
218        let result = steps(self);
219        self.defer_collect = deferred;
220        result
221    }
222
223    /// A background count of frame `len_generation` came back. Taken when that frame is
224    /// the one on screen; returns whether it was.
225    pub fn count_landed(
226        &mut self,
227        len_generation: u64,
228        rows: usize,
229        file_row_groups: Option<&[Vec<usize>]>,
230    ) -> bool {
231        let current = len_generation == self.view.len_generation;
232        if current {
233            self.take_count(rows, file_row_groups);
234        }
235        current
236    }
237
238    /// What a staged open leaves: a row total from however far the buffer reached,
239    /// with no count taken.
240    #[cfg(test)]
241    pub(crate) fn set_provisional_rows(&mut self, n: usize) {
242        self.view.num_rows = n;
243    }
244
245    /// The count of the frame on screen: from the files' row groups when there are
246    /// some, else the total.
247    fn take_count(&mut self, rows: usize, file_row_groups: Option<&[Vec<usize>]>) {
248        match file_row_groups {
249            Some(groups) => self.record_file_row_groups(groups),
250            None => self.set_num_rows(rows),
251        }
252    }
253
254    /// The follow of the file this dataset reads, while it is followed.
255    pub fn follow(&self) -> Option<&crate::loading::follow::Follow> {
256        self.follow.as_ref()
257    }
258
259    pub fn follow_mut(&mut self) -> Option<&mut crate::loading::follow::Follow> {
260        self.follow.as_mut()
261    }
262
263    /// Join `fields` that a followed NDJSON pipe brought after the open: the scan reads
264    /// them, appended to the column order, as footer joins do. `Err` while the view is a
265    /// query, reshape or group (the caller holds them); `Ok(false)` when nothing joins.
266    pub(crate) fn join_followed_fields(
267        &mut self,
268        fields: &[Field],
269    ) -> std::result::Result<bool, ()> {
270        if !self.scan_is_the_root() {
271            return Err(());
272        }
273        let (Some(follow), Some(format)) = (self.follow.as_ref(), self.read_as) else {
274            return Ok(false);
275        };
276        let (path, rows) = (follow.path().to_path_buf(), follow.shown());
277        let Some(mut lf) =
278            crate::loading::follow::widen(&self.original_lf, &path, format, fields, rows)
279        else {
280            return Ok(false);
281        };
282        let Ok(schema) = lf.collect_schema() else {
283            return Ok(false);
284        };
285        let known: std::collections::HashSet<&str> =
286            self.view.column_order.iter().map(String::as_str).collect();
287        let joining: Vec<String> = schema
288            .iter_names()
289            .map(|name| name.to_string())
290            .filter(|name| !known.contains(name.as_str()))
291            .collect();
292        drop(known);
293        self.view.column_order.extend(joining);
294        self.replace_root(lf, schema);
295        if self.is_pristine() {
296            // The same rows the watcher counted, with more columns.
297            self.set_num_rows(rows);
298        }
299        // Rebuilt but not read, as a footer join is: the caller reads the rows on
300        // screen off the event loop.
301        self.deferred(Self::apply_transformations);
302        Ok(true)
303    }
304
305    /// Follow the file this dataset reads with `follow`, whose watcher is running.
306    pub fn start_following(&mut self, follow: crate::loading::follow::Follow) {
307        self.follow = Some(follow);
308    }
309
310    /// Stop following. The rows read so far stay.
311    pub fn stop_following(&mut self) {
312        if let Some(mut follow) = self.follow.take() {
313            follow.end();
314        }
315    }
316
317    /// Put the view on the last page, leaving the cursor where it is until the rows of
318    /// that page are read: the next read is of that page alone.
319    pub(crate) fn aim_at_end(&mut self) {
320        if self.view.num_rows_valid && self.visible_rows > 0 {
321            self.view.start_row = self.view.num_rows.saturating_sub(self.visible_rows);
322        }
323    }
324
325    /// Whether the cursor is on the last row of a view whose length is known.
326    pub fn on_last_row(&self) -> bool {
327        self.view.num_rows_valid
328            && (self.view.num_rows == 0
329                || self.view.start_row + self.table_state.selected().unwrap_or(0) + 1
330                    >= self.view.num_rows)
331    }
332
333    /// Every frame the view holds that carries the scan of the data as loaded.
334    pub(super) fn each_frame(&mut self, mut f: impl FnMut(&mut LazyFrame)) {
335        f(&mut self.original_lf);
336        f(&mut self.view.base_lf);
337        f(&mut self.view.lf);
338        if let Some(lf) = self.view.unsorted_lf.as_mut() {
339            f(lf);
340        }
341        if let Some(lf) = self.view.reshaped_lf.as_mut() {
342            f(lf);
343        }
344        if let Some(source) = self.view.group_source.as_mut() {
345            f(&mut source.rows);
346        }
347        if let Some(grouped) = self.view.grouped.as_mut() {
348            f(&mut grouped.lf);
349            f(&mut grouped.base_lf);
350            if let Some(source) = grouped.group_source.as_mut() {
351                f(&mut source.rows);
352            }
353        }
354    }
355
356    /// The followed file now holds `rows` complete rows: every frame reads that many.
357    /// `restarted` when reread from the start. Returns whether the rows on hand still stand
358    /// (a filter-only view keeps them; new rows come after).
359    pub(crate) fn follow_to(&mut self, rows: usize, restarted: bool) -> bool {
360        let Some(path) = self.follow.as_ref().map(|f| f.path().to_path_buf()) else {
361            return true;
362        };
363        let rows_stand = !restarted
364            && self.view.sort_columns.is_empty()
365            && self.view.sort_ascending
366            && self.scan_is_the_root();
367        let known = self.known_before_follow(&path, restarted);
368        self.each_frame(|lf| crate::loading::follow::bound(lf, &path, rows));
369        self.invalidate_num_rows();
370        self.follow_known = known.map(|known| (self.view.len_generation, known));
371        if self.is_pristine() {
372            // The watcher counted them as the scan reads them: nothing to count again.
373            self.set_num_rows(rows);
374        } else if self.scan_is_the_root() {
375            self.pristine_rows = Some(rows);
376        }
377        if restarted {
378            self.view.start_row = 0;
379            self.table_state.select(Some(0));
380        }
381        if !rows_stand {
382            self.drop_buffer();
383        }
384        rows_stand
385    }
386
387    /// Where the view's rows are known before frames read more of the followed `path`:
388    /// known points for the current count, plus the count when exact. Only for row-wise
389    /// views (filters and a sort over file rows), so new rows are counted alone.
390    fn known_before_follow(&mut self, path: &Path, restarted: bool) -> Option<Vec<(usize, usize)>> {
391        if restarted || self.is_pristine() || !self.scan_is_the_root() {
392            return None;
393        }
394        let mut known = self
395            .follow_known
396            .take()
397            .filter(|(generation, _)| *generation == self.view.len_generation)
398            .map(|(_, known)| known);
399        if self.view.num_rows_valid
400            && let Some(row) = crate::loading::follow::bound_of(&self.view.lf, path)
401        {
402            let known = known.get_or_insert_with(Vec::new);
403            // One point per stretch of marks is enough to read on from.
404            if let [.., before, last] = known.as_slice()
405                && last.1 - before.1 < crate::loading::follow::MARK_ROWS as usize
406            {
407                known.pop();
408            }
409            if known.last().is_none_or(|&(_, at)| at < row) {
410                known.push((self.view.num_rows, row));
411            }
412        }
413        known
414    }
415
416    /// The followed file was deleted: every frame reads it through `file`, a handle
417    /// held on it, which still reads what it held.
418    pub(crate) fn read_followed_through(&mut self, file: &std::fs::File) {
419        let Some(path) = self.follow.as_ref().map(|f| f.path().to_path_buf()) else {
420            return;
421        };
422        self.each_frame(|lf| crate::loading::follow::read_through(lf, &path, file));
423    }
424
425    /// The frame on screen: the root, then the query or reshape, the filters and the
426    /// sort. Column order is applied when rows are read.
427    pub fn lf(&self) -> &LazyFrame {
428        &self.view.lf
429    }
430
431    /// The schema of the frame on screen.
432    pub fn schema(&self) -> &Arc<Schema> {
433        &self.view.schema
434    }
435
436    /// The rows the frame holds: exact when [`Self::is_num_rows_valid`], else as far as
437    /// the reads so far have reached.
438    pub fn num_rows(&self) -> usize {
439        self.view.num_rows
440    }
441
442    /// Why the last query, step or read failed, while it is still showing.
443    pub fn error(&self) -> Option<&PolarsError> {
444        self.error.as_ref()
445    }
446
447    /// Stop showing the last failure. The view is as it was; only the message goes.
448    pub fn dismiss_error(&mut self) {
449        self.error = None;
450    }
451
452    /// The first row of the page on screen.
453    pub fn start_row(&self) -> usize {
454        self.view.start_row
455    }
456
457    /// The hive partition columns the dataset was loaded with.
458    pub fn partition_columns(&self) -> Option<&[String]> {
459        self.partition_columns.as_deref()
460    }
461
462    /// Whether reads use Polars' streaming engine.
463    pub fn polars_streaming(&self) -> bool {
464        self.polars_streaming
465    }
466}