Skip to main content

datui_lib/query/
query_prompt.rs

1//! The command line: its modes (rows, SQL, q, text), completion, running a query off
2//! the UI thread, and rolling a failed one back.
3
4use crate::config::QueryMode;
5use crate::widgets::text_input::TextInput;
6use crate::{App, InputMode, InputType, QueryRun, RunOrigin, query::sql_assist};
7use polars::datatypes::DataType;
8
9/// The command line: its inputs per mode, completion, and the query it is running.
10pub struct QueryPrompt {
11    // One input per command line language, each with its own history. The history
12    // ids ("query", "sql") name files already on disk; they stay as they are so no
13    // history is lost or read as another language's.
14    pub(crate) query_input: TextInput, // q, history id "query"
15    pub(crate) sql_input: TextInput,   // SQL, history id "sql"
16    /// The find prompt (`/`) and the find `n` and `N` repeat; history id "find".
17    pub find: crate::find::Find,
18    /// The column cursor moved last: the footer offers the column's keys.
19    pub(crate) column_hints: bool,
20    pub(crate) input_type: Option<InputType>,
21    pub(crate) query_mode: QueryMode,
22    /// The language Ctrl+T last chose, which the command line opens on until a query
23    /// in effect says otherwise.
24    pub(crate) query_mode_chosen: Option<QueryMode>,
25    /// The command line holds the query in effect, selected and untouched: Ctrl+T
26    /// carries it selected, so typing still replaces it.
27    pub(crate) query_text_restored: bool,
28    /// The columns of `df`, for the command line's list and completion. Taken from
29    /// the schema when it opens.
30    pub(crate) sql_columns: Vec<(String, DataType)>,
31    /// A Tab completion in progress in the command line.
32    pub(crate) sql_completion: Option<sql_assist::Cycle>,
33    /// A query whose first collect is running, and the view to go back to if it
34    /// fails. From the prompt, the prompt stays open until it is done.
35    pub(crate) query_running: Option<QueryRun>,
36    /// Why the last statement failed once it ran, shown under it in the prompt.
37    pub(crate) query_run_error: Option<String>,
38    /// Bumped when a running statement's failure lands in the prompt. Keys typed while
39    /// it ran were not answers to it; see `EventPump`.
40    pub(crate) inline_failures: u64,
41}
42
43impl QueryPrompt {
44    /// A new dataset is on screen: a query running was over the one it replaced.
45    pub(crate) fn reset_for_dataset(&mut self) {
46        self.query_running = None;
47    }
48}
49
50impl App {
51    /// A query or view whose first rows could not be read is not applied: put back
52    /// what it replaced and say why where its origin says to.
53    pub(crate) fn fail_query_run(
54        &mut self,
55        run: QueryRun,
56        message: &str,
57        conversion: Option<&crate::error_display::ConversionFailure>,
58    ) {
59        let rows = run.rows;
60        let origin = self.roll_back_query_run(run);
61        self.first_rows_settled();
62        self.status_message = None;
63        self.busy = false;
64        // Run from the prompt, the reason goes under the query, which stays open to
65        // be fixed. Sent any other way — a view applied — there is nothing to edit,
66        // and the error modal says why.
67        let mode = match origin {
68            RunOrigin::View { applying, .. } => {
69                self.view_failed(&applying, message);
70                self.read_after_view_rollback();
71                return;
72            }
73            RunOrigin::Query(mode) if self.query_prompt_mode() == Some(mode) => mode,
74            RunOrigin::Query(_) => {
75                self.say_read_failed(message);
76                return;
77            }
78        };
79        // A file gone since the open is no fault of the query: the reopen is asked over
80        // the prompt, which keeps the reason.
81        let sql = mode == QueryMode::Sql;
82        self.prompt.query_run_error = Some(match conversion {
83            Some(failure) if sql => failure.sql_message(rows),
84            _ => message.to_string(),
85        });
86        self.prompt.inline_failures = self.prompt.inline_failures.wrapping_add(1);
87    }
88
89    /// Put back the view a running query or view replaced, with its row count, and
90    /// return where the query came from.
91    pub(crate) fn roll_back_query_run(&mut self, run: QueryRun) -> RunOrigin {
92        if let Some(state) = self.data_table_state.as_mut() {
93            state.roll_back(run.rollback);
94        }
95        self.counting.restore(run.counts);
96        if let RunOrigin::View { previous, .. } = &run.origin {
97            self.views.active_id = previous.clone();
98        }
99        run.origin
100    }
101
102    /// The command line's language while it is open.
103    pub fn query_prompt_mode(&self) -> Option<QueryMode> {
104        (self.input_mode == InputMode::Editing && self.prompt.input_type == Some(InputType::Query))
105            .then_some(self.prompt.query_mode)
106    }
107
108    /// `:` at the table: the command line, holding the query in effect, selected,
109    /// so typing states a new one and the arrows edit it.
110    pub(crate) fn open_command_line(&mut self) {
111        let Some(state) = self.data_table_state.as_mut() else {
112            return;
113        };
114        self.input_mode = InputMode::Editing;
115        self.prompt.input_type = Some(InputType::Query);
116        self.prompt.query_run_error = None;
117        self.prompt.sql_completion = None;
118        self.prompt.query_input.set_value(state.get_active_query());
119        self.prompt
120            .sql_input
121            .set_value(state.get_active_sql_query());
122        self.prompt.query_input.select_all();
123        self.prompt.sql_input.select_all();
124        state.suppress_error_display = true;
125        self.prompt.sql_columns = state.sql_table_columns();
126        self.prompt.query_mode = self.opening_query_mode();
127        self.prompt.query_text_restored = !self.query_input_shown().is_empty();
128        self.sync_query_focus();
129    }
130
131    /// The language `:` opens in: the query in effect's own, so editing never
132    /// reinterprets it; else the one Ctrl+T last chose; else the configured default.
133    fn opening_query_mode(&self) -> QueryMode {
134        let active = self.data_table_state.as_ref().and_then(|state| {
135            if !state.get_active_sql_query().trim().is_empty() {
136                Some(QueryMode::Sql)
137            } else if !state.get_active_query().trim().is_empty() {
138                Some(QueryMode::Q)
139            } else {
140                None
141            }
142        });
143        active
144            .or(self.prompt.query_mode_chosen)
145            .unwrap_or(self.app_config.query.default_mode)
146            .resolve()
147    }
148
149    /// The command line's input for its current language.
150    pub(crate) fn query_input_mut(&mut self) -> &mut TextInput {
151        match self.prompt.query_mode {
152            QueryMode::Sql => &mut self.prompt.sql_input,
153            QueryMode::Q => &mut self.prompt.query_input,
154        }
155    }
156
157    /// The command line's input for its current language.
158    pub(crate) fn query_input_shown(&self) -> &TextInput {
159        match self.prompt.query_mode {
160            QueryMode::Sql => &self.prompt.sql_input,
161            QueryMode::Q => &self.prompt.query_input,
162        }
163    }
164
165    /// Switch the prompt's mode. Each mode keeps its own text; an error from the
166    /// last run belongs to the mode that ran it.
167    pub(crate) fn set_query_mode(&mut self, mode: QueryMode) {
168        self.prompt.query_mode = mode.resolve();
169        if let Some(state) = &mut self.data_table_state {
170            state.dismiss_error();
171        }
172        self.prompt.query_run_error = None;
173        self.sync_query_focus();
174    }
175
176    /// Tab in the command line: complete the column name (or, in SQL, the table
177    /// name) being typed, and on further presses step through the other names that
178    /// match.
179    pub(crate) fn complete_column_name(&mut self) {
180        let sql = self.prompt.query_mode == QueryMode::Sql;
181        let columns = std::mem::take(&mut self.prompt.sql_columns);
182        let mut cycle = self.prompt.sql_completion.take();
183        let input = self.query_input_mut();
184        let line = input
185            .line_at(input.cursor_line())
186            .unwrap_or_default()
187            .to_string();
188        let value = input.value().to_string();
189        let complete = if sql {
190            sql_assist::tab
191        } else {
192            sql_assist::q_tab
193        };
194        if let Some(step) = complete(
195            &columns,
196            &line,
197            input.cursor_col(),
198            &value,
199            input.cursor(),
200            &mut cycle,
201        ) {
202            input.replace_before_cursor(step.span, &step.insert);
203            sql_assist::landed(&mut cycle, input.value(), input.cursor());
204        }
205        self.prompt.sql_completion = cycle;
206        self.prompt.sql_columns = columns;
207    }
208
209    /// The columns of `df` the word at the command line's cursor could name, for
210    /// the list under the input: every column while nothing is being typed.
211    pub(crate) fn sql_column_matches(&self) -> Vec<&(String, DataType)> {
212        let input = self.query_input_shown();
213        let line = input.line_at(input.cursor_line()).unwrap_or_default();
214        let word = match self.prompt.query_mode {
215            QueryMode::Sql => sql_assist::word_before(line, input.cursor_col()),
216            QueryMode::Q => sql_assist::q_word_before(line, input.cursor_col()),
217        }
218        .map(|w| w.text)
219        .unwrap_or_default();
220        sql_assist::matching(&self.prompt.sql_columns, &word)
221    }
222
223    /// The command line's text, while it is open.
224    pub fn query_prompt_text(&self) -> Option<&str> {
225        self.query_prompt_mode()?;
226        Some(self.query_input_shown().value())
227    }
228
229    /// Why the last run failed, for the line under the input: a statement that
230    /// failed while running, else one that could not be planned.
231    pub fn query_prompt_error(&self) -> Option<String> {
232        if let Some(error) = &self.prompt.query_run_error {
233            return Some(error.clone());
234        }
235        let state = self.data_table_state.as_ref()?;
236        let error = state.error()?;
237        Some(if self.prompt.query_mode == QueryMode::Sql {
238            crate::error_display::sql_error_message(error, state.sql_table_rows())
239        } else {
240            crate::error_display::user_message_from_polars(error)
241        })
242    }
243
244    /// Bumped each time a running statement's failure is put in the prompt.
245    pub fn inline_failures(&self) -> u64 {
246        self.prompt.inline_failures
247    }
248
249    /// Plan a query in `mode` and read its first rows in the background. A query that
250    /// cannot be planned leaves its error on the state, where the prompt shows it. One
251    /// that plans stays pending — the prompt open, when it came from there — until its
252    /// rows are in; if they fail, the view it replaced comes back.
253    pub(crate) fn run_query(&mut self, mode: QueryMode, text: &str, status: &str) {
254        self.prompt.query_run_error = None;
255        let Some(state) = self.data_table_state.as_mut() else {
256            return;
257        };
258        let rollback = state.rollback_point();
259        let rows = state.sql_table_rows();
260        // A query that cannot be planned changes nothing and leaves its error showing.
261        state.deferred(|s| match mode {
262            QueryMode::Sql => s.sql_query(text.to_string()),
263            QueryMode::Q => s.query(text.to_string()),
264        });
265        if state.error().is_some() {
266            return;
267        }
268        self.prompt.query_running = Some(QueryRun {
269            origin: RunOrigin::Query(mode),
270            frame: state.len_generation(),
271            rollback,
272            counts: self.counting.markers(),
273            rows,
274        });
275        if !self.spawn_async_collect(status) {
276            // Nothing to read: the rows on hand already show it.
277            self.prompt.query_running = None;
278            if self.query_prompt_mode() == Some(mode) {
279                self.leave_query_prompt_after_run();
280            }
281        }
282    }
283
284    /// The query still running over the frame on screen.
285    pub(crate) fn query_run(&self) -> Option<&QueryRun> {
286        let run = self.prompt.query_running.as_ref()?;
287        let frame = self.data_table_state.as_ref()?.len_generation();
288        (run.frame == frame).then_some(run)
289    }
290
291    /// The query still running over the frame on screen, taken. One whose frame has
292    /// since been replaced is dropped: its rollback would undo what replaced it.
293    pub(crate) fn take_query_run(&mut self) -> Option<QueryRun> {
294        let run = self.prompt.query_running.take()?;
295        let frame = self.data_table_state.as_ref()?.len_generation();
296        (run.frame == frame).then_some(run)
297    }
298
299    /// A query ran and its rows are in: the prompt closes on them.
300    pub(crate) fn leave_query_prompt_after_run(&mut self) {
301        self.prompt.sql_completion = None;
302        self.show_table();
303        self.prompt.sql_input.set_focused(false);
304        self.prompt.query_input.set_focused(false);
305        if let Some(state) = &mut self.data_table_state {
306            state.suppress_error_display = false;
307        }
308    }
309
310    /// Only the current language's input carries the cursor.
311    pub(crate) fn sync_query_focus(&mut self) {
312        let mode = self.prompt.query_mode;
313        self.prompt.sql_input.set_focused(mode == QueryMode::Sql);
314        self.prompt.query_input.set_focused(mode == QueryMode::Q);
315    }
316
317    /// Esc from anywhere in the prompt: nothing runs and nothing typed survives.
318    pub(crate) fn close_query_prompt(&mut self) {
319        self.prompt.query_run_error = None;
320        self.prompt.sql_completion = None;
321        self.prompt.query_input.clear();
322        self.prompt.sql_input.clear();
323        self.prompt.query_input.set_focused(false);
324        self.prompt.sql_input.set_focused(false);
325        self.show_table();
326        if let Some(state) = &mut self.data_table_state {
327            state.dismiss_error();
328            state.suppress_error_display = false;
329        }
330    }
331}