Skip to main content

datui_lib/view/
view_apply.rs

1//! Saved views: the list, the save form, matching a view to a dataset, and replaying
2//! its steps with a rollback when its rows fail.
3
4use crate::app::jobs::{Answer, Job};
5use crate::app::modals::filter_modal::FilterStatement;
6use crate::table::DataTableState;
7use crate::view::SavedView;
8use crate::widgets::view_modal::{FormFocus, ViewModalMode, ViewRow};
9use crate::{
10    App, QueryRun, Replayed, RunOrigin, active_query_settings, view, view_settings_of, widgets,
11};
12use color_eyre::Result;
13use polars::prelude::DataFrame;
14
15/// Saved views, and the one applied to the dataset on screen.
16pub struct SavedViews {
17    pub(crate) manager: crate::view::Views,
18    pub(crate) active_id: Option<String>, // ID of currently applied view
19}
20
21/// Why a view is applied, which decides what is recorded and said for it.
22#[derive(Debug, Clone)]
23pub(crate) enum Applying {
24    /// Asked for: recorded as a use.
25    Asked,
26    /// Its criteria fit the dataset: recorded, and a flash names it and why once its
27    /// rows are in.
28    Matched(view::MatchReason),
29    /// The place a reopen keeps, recorded nowhere; the drill-down is taken again once
30    /// its rows are in.
31    Restored(Option<Box<crate::table::DrillPlace>>),
32}
33
34/// Why a view, or a reopened dataset's place, could not be applied, as the error
35/// modal or the Reopen question says it.
36pub(crate) fn view_failure(applying: &Applying, message: &str) -> String {
37    if matches!(applying, Applying::Restored(_)) {
38        format!("Reopened, but the query, filters and sort could not be applied again: {message}")
39    } else {
40        format!("Error applying view: {message}")
41    }
42}
43
44impl SavedViews {
45    /// A new dataset is on screen with no view applied.
46    pub(crate) fn reset_for_dataset(&mut self) {
47        self.active_id = None;
48    }
49}
50
51impl App {
52    /// Open the views list for the dataset on screen, scored against it.
53    pub(crate) fn open_view_list(&mut self) {
54        if self.view_dataset().is_none() {
55            return;
56        }
57        self.view_modal.table_state.select(Some(0));
58        self.refresh_view_list();
59        self.view_modal.mode = ViewModalMode::List;
60        self.open_overlay(crate::Overlay::View);
61    }
62
63    /// Rebuild the list from the store, scored against the open dataset; the selection
64    /// stays near where it was.
65    pub(crate) fn refresh_view_list(&mut self) {
66        let (Some(state), Some(dataset)) = (&self.data_table_state, self.view_dataset()) else {
67            return;
68        };
69        let rows: Vec<ViewRow> = self
70            .views
71            .manager
72            .find_relevant_views(dataset, state.source_schema())
73            .into_iter()
74            .map(|(view, score)| {
75                let reason = view::match_reason(&view, dataset, state.source_schema());
76                ViewRow {
77                    view,
78                    score,
79                    reason,
80                }
81            })
82            .collect();
83        self.view_modal.broken_views = self.views.manager.broken_views.clone();
84        let selected = self.view_modal.table_state.selected().unwrap_or(0);
85        self.view_modal.table_state.select(if rows.is_empty() {
86            None
87        } else {
88            Some(selected.min(rows.len() - 1))
89        });
90        self.view_modal.rows = rows;
91    }
92
93    /// Open the save-view form prefilled from the open dataset: a recognizable name,
94    /// this file's paths and patterns as criteria, and schema match on (the criterion
95    /// that carries the view to similar tables).
96    pub(crate) fn open_save_view_form(&mut self) {
97        self.view_modal
98            .enter_create_mode(self.display.history_limit, &self.theme);
99
100        let query = self.data_table_state.as_ref().and_then(|state| {
101            let (query, sql_query, fuzzy_query) = active_query_settings(
102                state.get_active_query(),
103                state.get_active_sql_query(),
104                state.get_active_fuzzy_query(),
105            );
106            sql_query.or(fuzzy_query).or(query)
107        });
108        self.view_modal.name_input.suggest(
109            self.views
110                .manager
111                .suggest_name(self.path.as_deref(), query.as_deref()),
112        );
113
114        // Data piped in has no file to pin; its columns are what match it.
115        if let Some(path) = self.path.as_ref().filter(|_| !self.reads_stdin()) {
116            // Pin this file: its absolute path or URL, its path relative to a working directory
117            // above it, and glob suggestions.
118            let absolute_path = view::exact_location(path);
119            self.view_modal
120                .exact_path_input
121                .suggest(absolute_path.to_string_lossy());
122            if let Some(relative) = view::relative_location(path) {
123                self.view_modal.relative_path_input.suggest(relative);
124            }
125
126            // A path pattern from the absolute path: a bare relative name's parent is "", and
127            // ""/*.parquet would match every parquet file anywhere. Use the path's own
128            // separator, or a Windows path never fits.
129            if let Some(parent) = absolute_path.parent()
130                && let Some(parent_str) = parent.to_str()
131                && !parent_str.is_empty()
132                && let Some(ext) = absolute_path.extension()
133            {
134                let separator = if crate::cloud::source::is_remote_url(path) {
135                    '/'
136                } else {
137                    std::path::MAIN_SEPARATOR
138                };
139                self.view_modal.path_pattern_input.suggest(format!(
140                    "{}{separator}*.{}",
141                    parent_str.trim_end_matches(separator),
142                    ext.to_string_lossy()
143                ));
144            }
145
146            // A filename pattern with digit runs wildcarded: sales_2024.csv matches
147            // sales_2025.csv.
148            if let Some(filename) = path.file_name()
149                && let Some(filename_str) = filename.to_str()
150            {
151                use regex::Regex;
152                let pattern = match Regex::new(r"\d+") {
153                    Ok(re) => re.replace_all(filename_str, "*").to_string(),
154                    Err(_) => filename_str.to_string(),
155                };
156                self.view_modal.filename_pattern_input.suggest(pattern);
157            }
158        }
159
160        self.view_modal.table = self.view_table().map(str::to_string);
161
162        // Schema match starts on: applying to similar tables is what views are for, and
163        // columns are the only criterion that says similar.
164        if let Some(ref state) = self.data_table_state
165            && !state.source_schema().is_empty()
166        {
167            self.view_modal.schema_match_enabled = true;
168        }
169    }
170
171    /// Validate and save the form (a new view or the edited one). A failed save keeps
172    /// the form open.
173    pub(crate) fn save_view_form(&mut self) {
174        self.view_modal.name_error = None;
175        let name = self.view_modal.name_input.value().trim().to_string();
176        if name.is_empty() {
177            self.view_modal.name_error = Some("name is required".to_string());
178            self.view_modal.form_focus = FormFocus::Name;
179            return;
180        }
181        let renaming_to_taken = match &self.view_modal.editing_view_id {
182            None => self.views.manager.view_exists(&name),
183            Some(id) => self
184                .views
185                .manager
186                .get_view_by_name(&name)
187                .is_some_and(|other| other.id != *id),
188        };
189        if renaming_to_taken {
190            self.view_modal.name_error = Some("name already exists".to_string());
191            self.view_modal.form_focus = FormFocus::Name;
192            return;
193        }
194
195        let non_empty = |input: &widgets::text_input::TextInput| {
196            let value = input.value().trim();
197            (!value.is_empty()).then(|| value.to_string())
198        };
199        let match_criteria = view::MatchCriteria {
200            exact_path: non_empty(&self.view_modal.exact_path_input).map(std::path::PathBuf::from),
201            relative_path: non_empty(&self.view_modal.relative_path_input),
202            path_pattern: non_empty(&self.view_modal.path_pattern_input),
203            filename_pattern: non_empty(&self.view_modal.filename_pattern_input),
204            // The columns the view's settings run on, not the query's output: the next file is
205            // matched as loaded.
206            schema_columns: if self.view_modal.schema_match_enabled {
207                self.data_table_state.as_ref().map(|state| {
208                    state
209                        .source_schema()
210                        .iter_names()
211                        .map(|s| s.to_string())
212                        .collect()
213                })
214            } else {
215                None
216            },
217            schema_types: None,
218            table: self.view_modal.table.clone(),
219        };
220        let description = {
221            let value = self.view_modal.description_input.value();
222            (!value.is_empty()).then(|| value.to_string())
223        };
224
225        let saved = if let Some(editing_id) = self.view_modal.editing_view_id.clone() {
226            let Some(mut view) = self.views.manager.get_view_by_id(&editing_id).cloned() else {
227                return;
228            };
229            view.name = name;
230            view.description = description;
231            let stored_schema = view.match_criteria.schema_columns.take();
232            view.match_criteria = match_criteria;
233            let editing_the_active_view =
234                self.views.active_id.as_deref() == Some(editing_id.as_str());
235            // Editing an unapplied view must not swap its matched columns for the open table's;
236            // the toggle still drops the criterion, and the active view follows its table.
237            if !editing_the_active_view
238                && self.view_modal.schema_match_enabled
239                && stored_schema.is_some()
240            {
241                view.match_criteria.schema_columns = stored_schema;
242            }
243            // The settings follow the table only for the view dressing it; editing an unapplied
244            // view changes only its name, description and matching.
245            if editing_the_active_view && let Some(state) = &self.data_table_state {
246                view.settings = view_settings_of(state);
247                view.settings.chart = self.saved_chart();
248            }
249            match self.views.manager.update_view(&view) {
250                Ok(()) => true,
251                Err(e) => {
252                    // Deleted elsewhere, it has left the list; otherwise the form stays to retry.
253                    if self.views.manager.get_view_by_id(&editing_id).is_none() {
254                        self.refresh_view_list();
255                        self.view_modal.exit_form();
256                    }
257                    self.error_modal.show(format!("Error saving view: {e}"));
258                    return;
259                }
260            }
261        } else {
262            self.create_view_from_current_state(name, description, match_criteria)
263                .is_ok()
264        };
265        if saved {
266            self.refresh_view_list();
267            self.view_modal.exit_form();
268        }
269    }
270
271    /// The selected view's score breakdown, for the list's `i` popup.
272    pub(crate) fn view_score_details(&self) -> Option<(String, String)> {
273        let state = self.data_table_state.as_ref()?;
274        let path = self.view_dataset()?;
275        let idx = self.view_modal.table_state.selected()?;
276        let row = self.view_modal.rows.get(idx)?;
277        let view = &row.view;
278
279        let exact_path_match = view::exact_path_matches(&view.match_criteria, path);
280        let relative_path_match = view::relative_path_matches(&view.match_criteria, path);
281        let file_cols: std::collections::HashSet<&str> = state
282            .source_schema()
283            .iter_names()
284            .map(|s| s.as_str())
285            .collect();
286        let exact_schema_match =
287            view.match_criteria
288                .schema_columns
289                .as_ref()
290                .is_some_and(|required| {
291                    let required: std::collections::HashSet<&str> =
292                        required.iter().map(|s| s.as_str()).collect();
293                    required.is_subset(&file_cols) && file_cols.len() == required.len()
294                });
295
296        let mut details = format!("Total score: {:.1}\n\n", row.score);
297        if exact_path_match && exact_schema_match {
298            details.push_str("Exact path + exact schema: 2000.0\n");
299        } else if exact_path_match {
300            details.push_str("Exact path: 1000.0\n");
301        } else if relative_path_match && exact_schema_match {
302            details.push_str("Relative path + exact schema: 1950.0\n");
303        } else if relative_path_match {
304            details.push_str("Relative path: 950.0\n");
305        } else if exact_schema_match {
306            details.push_str("Exact schema: 900.0\n");
307        } else {
308            if view::path_pattern_matches(&view.match_criteria, path) {
309                details.push_str("Path pattern match: 50.0+\n");
310            }
311            if view::filename_pattern_matches(&view.match_criteria, path) {
312                details.push_str("Filename pattern match: 30.0+\n");
313            }
314            if let Some(required_cols) = &view.match_criteria.schema_columns {
315                let matching_count = required_cols
316                    .iter()
317                    .filter(|col| file_cols.contains(col.as_str()))
318                    .count();
319                if matching_count > 0 {
320                    details.push_str(&format!(
321                        "Partial schema match: {:.1} ({} columns)\n",
322                        matching_count as f64 * 2.0,
323                        matching_count
324                    ));
325                }
326            }
327        }
328        if view.usage_count > 0 {
329            details.push_str(&format!(
330                "Usage count: {:.1}\n",
331                (view.usage_count.min(10) as f64) * 1.0
332            ));
333        }
334        if let Some(last_used) = view.last_used
335            && let Ok(duration) = std::time::SystemTime::now().duration_since(last_used)
336        {
337            let days_since = duration.as_secs() / 86400;
338            if days_since <= 7 {
339                details.push_str("Recent usage: 5.0\n");
340            } else if days_since <= 30 {
341                details.push_str("Recent usage: 2.0\n");
342            }
343        }
344        Some((format!("Score: {}", view.name), details))
345    }
346
347    /// Start applying `view`. Planning its steps reads nothing; a step that cannot plan
348    /// fails here, changing nothing. The pivot and first rows are read in the
349    /// background and the view installed when in; a failure there restores the
350    /// previous view.
351    pub(crate) fn apply_view(&mut self, view: &SavedView) -> Result<()> {
352        self.apply_view_with(view, Applying::Asked)
353    }
354
355    /// [`Self::apply_view`] for a view applied because its criteria fit (`why`): a flash
356    /// names it and the reason once its rows are in.
357    pub(crate) fn apply_matched_view(
358        &mut self,
359        view: &SavedView,
360        why: view::MatchReason,
361    ) -> Result<()> {
362        self.apply_view_with(view, Applying::Matched(why))
363    }
364
365    /// Put back `place`, the steps a dataset showed before it was reopened, and then its
366    /// drill-down: as a view applies, with `active` (the saved view it showed, if any)
367    /// marked applied again.
368    pub(crate) fn restore_place(
369        &mut self,
370        place: view::ViewSettings,
371        active: Option<String>,
372        drill: Option<Box<crate::table::DrillPlace>>,
373    ) -> Result<()> {
374        let view = SavedView {
375            id: active.unwrap_or_default(),
376            name: String::new(),
377            description: None,
378            created: std::time::SystemTime::now(),
379            last_used: None,
380            usage_count: 0,
381            last_matched_file: None,
382            match_criteria: view::MatchCriteria::default(),
383            settings: place,
384        };
385        self.apply_view_with(&view, Applying::Restored(drill))
386    }
387
388    fn apply_view_with(&mut self, view: &SavedView, applying: Applying) -> Result<()> {
389        self.jobs.supersede(|job| matches!(job, Job::ViewPivot(_)));
390        if let Some(saved) = &view.settings.sample {
391            return self.apply_sampled_view(view, saved, &applying);
392        }
393        let Some(state) = self.data_table_state.as_mut() else {
394            return Ok(());
395        };
396        match state.try_transition(|s| Self::replay_view(s, &view.settings, None))? {
397            (Replayed::Planned, rollback) => {
398                self.view_planned(view, rollback, applying);
399                Ok(())
400            }
401            (Replayed::Pivot(job), rollback) => {
402                // The table stays as it is while the pivot is read.
403                state.roll_back(rollback);
404                // Past any load-ahead for the view on screen, whose rows must not land in its
405                // replacement.
406                self.jobs.try_advance();
407                let pivot_view = Job::ViewPivot(Box::new((view.clone(), applying)));
408                self.spawn_job(pivot_view, Some(Self::APPLYING_VIEW), move |_| {
409                    let pivoted = job
410                        .run()
411                        .map_err(|e| crate::error_display::user_message_from_report(&e, None))?;
412                    Ok(Answer::ViewPivoted(pivoted))
413                });
414                Ok(())
415            }
416        }
417    }
418
419    /// The view's steps are planned over `rollback`, the replaced view: mark it applied
420    /// and read its first rows. A failure before they are in restores `rollback` and
421    /// its applied mark.
422    pub(crate) fn view_planned(
423        &mut self,
424        view: &SavedView,
425        rollback: crate::table::ViewRollback,
426        applying: Applying,
427    ) {
428        let previous = self.mark_view_applied(view, &applying);
429        let Some(state) = self.data_table_state.as_ref() else {
430            return;
431        };
432        self.prompt.query_running = Some(QueryRun {
433            origin: RunOrigin::View {
434                previous,
435                name: view.name.clone(),
436                applying,
437            },
438            frame: state.len_generation(),
439            rollback,
440            counts: self.counting.markers(),
441            rows: None,
442        });
443        if !self.spawn_async_collect(Self::APPLYING_VIEW) {
444            // Nothing to read: the view has no rows. Applied on open, it was the open's last
445            // step.
446            match self.prompt.query_running.as_ref().map(|run| &run.origin) {
447                Some(RunOrigin::View {
448                    applying: Applying::Matched(why),
449                    ..
450                }) => self.flash_view_applied(&view.name, *why),
451                // No rows: the group or value drilled into is not among them.
452                Some(RunOrigin::View {
453                    applying: Applying::Restored(Some(drill)),
454                    ..
455                }) => {
456                    let gone = crate::table::DrillGone(drill.describe());
457                    self.flash_note(gone.to_string());
458                }
459                _ => {}
460            }
461            self.prompt.query_running = None;
462            self.busy = false;
463            self.status_message = None;
464            self.first_rows_settled();
465        }
466    }
467
468    /// Record `view`'s use, unless it is a place put back, and mark it applied (none for
469    /// a place that showed no saved view); its chart comes back. Returns the view
470    /// marked applied before.
471    pub(crate) fn mark_view_applied(
472        &mut self,
473        view: &SavedView,
474        applying: &Applying,
475    ) -> Option<String> {
476        if !matches!(applying, Applying::Restored(_))
477            && let Some(path) = &self.path
478        {
479            use crate::logging::LogFailure;
480            self.views
481                .manager
482                .record_use(&view.id, path)
483                .or_log("record a view's use");
484        }
485        self.restore_view_chart(view.settings.chart.as_ref());
486        std::mem::replace(
487            &mut self.views.active_id,
488            (!view.id.is_empty()).then(|| view.id.clone()),
489        )
490    }
491
492    /// Whether a view is being applied at the table (its pivot or first rows are read).
493    pub(crate) fn view_applying(&self) -> bool {
494        if !self.is_busy() || !self.in_normal_table_view() {
495            return false;
496        }
497        let pivot = self
498            .jobs
499            .current(|job| matches!(job, Job::ViewPivot(_)))
500            .is_some();
501        let rows = self.prompt.query_running.as_ref().is_some_and(|run| {
502            matches!(run.origin, RunOrigin::View { .. })
503                && self
504                    .data_table_state
505                    .as_ref()
506                    .is_some_and(|state| state.len_generation() == run.frame)
507        });
508        pivot || rows
509    }
510
511    /// Stop applying a view and keep the previous one; the bump drops the worker's
512    /// answer.
513    pub(crate) fn cancel_view(&mut self) {
514        self.jobs.advance();
515        self.screen_generation = self.screen_generation.wrapping_add(1);
516        if let Some(run) = self.take_query_run() {
517            self.roll_back_query_run(run);
518        }
519        // A collect for the view, queued behind a worker, would read it after all.
520        self.forget_the_rows_read();
521        self.read_after_view_rollback();
522        self.flash_note("View cancelled".to_string());
523    }
524
525    /// A view's pivot could not be read or planned: the view before it stays.
526    pub(crate) fn view_pivot_failed(&mut self, applying: &Applying, message: &str) {
527        self.view_failed(applying, message);
528        self.read_after_view_rollback();
529    }
530
531    /// Say why a view, or a reopened dataset's place, could not be applied.
532    pub(crate) fn view_failed(&mut self, applying: &Applying, message: &str) {
533        self.say_read_failed(&view_failure(applying, message));
534    }
535
536    /// The view before a failed or cancelled one is back: read its rows if none are on
537    /// hand, else stop being busy.
538    pub(crate) fn read_after_view_rollback(&mut self) {
539        self.busy = false;
540        self.status_message = None;
541        if !self.spawn_async_collect(Self::LOADING_BUFFER) {
542            self.first_rows_settled();
543        }
544    }
545
546    /// Run a view's steps on `state` in build order. With a pivot or melt: query,
547    /// filters and sort under it, the reshape, then query, filters and sort on its
548    /// result. Without: query, filters, sort. Column order last. Stops at the first
549    /// failing step, and at a pivot unless `pivoted` holds it.
550    pub(crate) fn replay_view(
551        state: &mut DataTableState,
552        settings: &view::ViewSettings,
553        pivoted: Option<DataFrame>,
554    ) -> Result<Replayed> {
555        if settings.pivot.is_some() || settings.melt.is_some() {
556            if let Some(source) = &settings.reshape_source {
557                Self::replay_query(
558                    state,
559                    source.sql_query.as_deref(),
560                    source.query.as_deref(),
561                    source.fuzzy_query.as_deref(),
562                )?;
563                Self::replay_filters_and_sort(
564                    state,
565                    &source.filters,
566                    &source.sort_columns,
567                    source.sort_directions(),
568                )?;
569            }
570            let reshaped = match (&settings.pivot, &settings.melt, pivoted) {
571                (Some(spec), _, Some(pivoted)) => state.install_pivot(spec, pivoted),
572                (Some(spec), _, None) => {
573                    Self::check_plan(state)?;
574                    return Ok(Replayed::Pivot(Box::new(state.plan_pivot(spec))));
575                }
576                (None, Some(spec), _) => state.melt(spec),
577                (None, None, _) => Ok(()),
578            };
579            reshaped.map_err(|e| {
580                color_eyre::eyre::eyre!(
581                    "{}",
582                    crate::error_display::user_message_from_report(&e, None)
583                )
584            })?;
585        }
586        Self::replay_query(
587            state,
588            settings.sql_query.as_deref(),
589            settings.query.as_deref(),
590            settings.fuzzy_query.as_deref(),
591        )?;
592        // Before the filters, which may compare in the types it gives.
593        if !settings.columns.is_empty() {
594            state.set_column_changes(&settings.columns);
595        }
596        Self::replay_filters_and_sort(
597            state,
598            &settings.filters,
599            &settings.sort_columns,
600            settings.sort_directions(),
601        )?;
602        if !settings.column_order.is_empty() {
603            state.set_column_order(settings.column_order.clone());
604            state.set_locked_columns(settings.locked_columns_count);
605        }
606        Self::check_plan(state)?;
607        Ok(Replayed::Planned)
608    }
609
610    /// Whether the frame the steps so far built can be read, by its plan alone.
611    fn check_plan(state: &DataTableState) -> Result<()> {
612        state.check_plan().map_err(|e| {
613            color_eyre::eyre::eyre!("{}", crate::error_display::user_message_from_polars(&e))
614        })
615    }
616
617    /// A view's query: SQL or q (at most one is stored), then a Text query.
618    fn replay_query(
619        state: &mut DataTableState,
620        sql: Option<&str>,
621        dsl: Option<&str>,
622        fuzzy: Option<&str>,
623    ) -> Result<()> {
624        let stated = |q: Option<&str>| q.filter(|q| !q.trim().is_empty()).map(str::to_string);
625        if let Some(sql) = stated(sql) {
626            state.sql_query(sql);
627        } else if let Some(query) = stated(dsl) {
628            state.query(query);
629        }
630        if state.error().is_none()
631            && let Some(fuzzy) = stated(fuzzy)
632        {
633            state.fuzzy_search(fuzzy);
634        }
635        match state.error().cloned() {
636            Some(error) => Err(color_eyre::eyre::eyre!(
637                "{}",
638                crate::error_display::user_message_from_polars(&error)
639            )),
640            None => Ok(()),
641        }
642    }
643
644    /// A view's sidebar filters, then its sort.
645    fn replay_filters_and_sort(
646        state: &mut DataTableState,
647        filters: &[FilterStatement],
648        sort_columns: &[String],
649        descending: Vec<bool>,
650    ) -> Result<()> {
651        if !filters.is_empty() {
652            state.filter(filters.to_vec());
653            if let Some(error) = state.error().cloned() {
654                return Err(color_eyre::eyre::eyre!("{}", error));
655            }
656        }
657        if !sort_columns.is_empty() {
658            state.sort_by(sort_columns.to_vec(), descending);
659            if let Some(error) = state.error().cloned() {
660                return Err(color_eyre::eyre::eyre!("{}", error));
661            }
662        }
663        Ok(())
664    }
665
666    pub fn create_view_from_current_state(
667        &mut self,
668        name: String,
669        description: Option<String>,
670        match_criteria: view::MatchCriteria,
671    ) -> Result<view::SavedView> {
672        let settings = match &self.data_table_state {
673            Some(state) => view::ViewSettings {
674                chart: self.saved_chart(),
675                ..view_settings_of(state)
676            },
677            None => view::ViewSettings {
678                chart: None,
679                sample: None,
680                query: None,
681                sql_query: None,
682                fuzzy_query: None,
683                filters: Vec::new(),
684                sort_columns: Vec::new(),
685                sort_descending: Vec::new(),
686                sort_ascending: true,
687                column_order: Vec::new(),
688                locked_columns_count: 0,
689                pivot: None,
690                melt: None,
691                reshape_source: None,
692                columns: Vec::new(),
693            },
694        };
695
696        self.views
697            .manager
698            .create_view(name, description, match_criteria, settings)
699    }
700}