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 { .. } => {
69                self.error_modal
70                    .show(format!("Error applying view: {message}"));
71                self.read_after_view_rollback();
72                return;
73            }
74            RunOrigin::Query(mode) if self.query_prompt_mode() == Some(mode) => mode,
75            RunOrigin::Query(_) => {
76                self.error_modal.show(message.to_string());
77                return;
78            }
79        };
80        let sql = mode == QueryMode::Sql;
81        self.prompt.query_run_error = Some(match conversion {
82            Some(failure) if sql => failure.sql_message(rows),
83            _ => message.to_string(),
84        });
85        self.prompt.inline_failures = self.prompt.inline_failures.wrapping_add(1);
86    }
87
88    /// Put back the view a running query or view replaced, with its row count, and
89    /// return where the query came from.
90    pub(crate) fn roll_back_query_run(&mut self, run: QueryRun) -> RunOrigin {
91        if let Some(state) = self.data_table_state.as_mut() {
92            state.roll_back(run.rollback);
93        }
94        self.counting.restore(run.counts);
95        if let RunOrigin::View { previous, .. } = &run.origin {
96            self.views.active_id = previous.clone();
97        }
98        run.origin
99    }
100
101    /// The command line's language while it is open.
102    pub fn query_prompt_mode(&self) -> Option<QueryMode> {
103        (self.input_mode == InputMode::Editing && self.prompt.input_type == Some(InputType::Query))
104            .then_some(self.prompt.query_mode)
105    }
106
107    /// `:` at the table: the command line, holding the query in effect, selected,
108    /// so typing states a new one and the arrows edit it.
109    pub(crate) fn open_command_line(&mut self) {
110        let Some(state) = self.data_table_state.as_mut() else {
111            return;
112        };
113        self.input_mode = InputMode::Editing;
114        self.prompt.input_type = Some(InputType::Query);
115        self.prompt.query_run_error = None;
116        self.prompt.sql_completion = None;
117        self.prompt.query_input.set_value(state.get_active_query());
118        self.prompt
119            .sql_input
120            .set_value(state.get_active_sql_query());
121        self.prompt.query_input.select_all();
122        self.prompt.sql_input.select_all();
123        state.suppress_error_display = true;
124        self.prompt.sql_columns = state.sql_table_columns();
125        self.prompt.query_mode = self.opening_query_mode();
126        self.prompt.query_text_restored = !self.query_input_shown().is_empty();
127        self.sync_query_focus();
128    }
129
130    /// The language `:` opens in: the query in effect's own, so editing never
131    /// reinterprets it; else the one Ctrl+T last chose; else the configured default.
132    fn opening_query_mode(&self) -> QueryMode {
133        let active = self.data_table_state.as_ref().and_then(|state| {
134            if !state.get_active_sql_query().trim().is_empty() {
135                Some(QueryMode::Sql)
136            } else if !state.get_active_query().trim().is_empty() {
137                Some(QueryMode::Q)
138            } else {
139                None
140            }
141        });
142        active
143            .or(self.prompt.query_mode_chosen)
144            .unwrap_or(self.app_config.query.default_mode)
145            .resolve()
146    }
147
148    /// The command line's input for its current language.
149    pub(crate) fn query_input_mut(&mut self) -> &mut TextInput {
150        match self.prompt.query_mode {
151            QueryMode::Sql => &mut self.prompt.sql_input,
152            QueryMode::Q => &mut self.prompt.query_input,
153        }
154    }
155
156    /// The command line's input for its current language.
157    pub(crate) fn query_input_shown(&self) -> &TextInput {
158        match self.prompt.query_mode {
159            QueryMode::Sql => &self.prompt.sql_input,
160            QueryMode::Q => &self.prompt.query_input,
161        }
162    }
163
164    /// Switch the prompt's mode. Each mode keeps its own text; an error from the
165    /// last run belongs to the mode that ran it.
166    pub(crate) fn set_query_mode(&mut self, mode: QueryMode) {
167        self.prompt.query_mode = mode.resolve();
168        if let Some(state) = &mut self.data_table_state {
169            state.dismiss_error();
170        }
171        self.prompt.query_run_error = None;
172        self.sync_query_focus();
173    }
174
175    /// Tab in the command line: complete the column name (or, in SQL, the table
176    /// name) being typed, and on further presses step through the other names that
177    /// match.
178    pub(crate) fn complete_column_name(&mut self) {
179        let sql = self.prompt.query_mode == QueryMode::Sql;
180        let columns = std::mem::take(&mut self.prompt.sql_columns);
181        let mut cycle = self.prompt.sql_completion.take();
182        let input = self.query_input_mut();
183        let line = input
184            .line_at(input.cursor_line())
185            .unwrap_or_default()
186            .to_string();
187        let value = input.value().to_string();
188        let complete = if sql {
189            sql_assist::tab
190        } else {
191            sql_assist::q_tab
192        };
193        if let Some(step) = complete(
194            &columns,
195            &line,
196            input.cursor_col(),
197            &value,
198            input.cursor(),
199            &mut cycle,
200        ) {
201            input.replace_before_cursor(step.span, &step.insert);
202            sql_assist::landed(&mut cycle, input.value(), input.cursor());
203        }
204        self.prompt.sql_completion = cycle;
205        self.prompt.sql_columns = columns;
206    }
207
208    /// The columns of `df` the word at the command line's cursor could name, for
209    /// the list under the input: every column while nothing is being typed.
210    pub(crate) fn sql_column_matches(&self) -> Vec<&(String, DataType)> {
211        let input = self.query_input_shown();
212        let line = input.line_at(input.cursor_line()).unwrap_or_default();
213        let word = match self.prompt.query_mode {
214            QueryMode::Sql => sql_assist::word_before(line, input.cursor_col()),
215            QueryMode::Q => sql_assist::q_word_before(line, input.cursor_col()),
216        }
217        .map(|w| w.text)
218        .unwrap_or_default();
219        sql_assist::matching(&self.prompt.sql_columns, &word)
220    }
221
222    /// The command line's text, while it is open.
223    pub fn query_prompt_text(&self) -> Option<&str> {
224        self.query_prompt_mode()?;
225        Some(self.query_input_shown().value())
226    }
227
228    /// Why the last run failed, for the line under the input: a statement that
229    /// failed while running, else one that could not be planned.
230    pub fn query_prompt_error(&self) -> Option<String> {
231        if let Some(error) = &self.prompt.query_run_error {
232            return Some(error.clone());
233        }
234        let state = self.data_table_state.as_ref()?;
235        let error = state.error()?;
236        Some(if self.prompt.query_mode == QueryMode::Sql {
237            crate::error_display::sql_error_message(error, state.sql_table_rows())
238        } else {
239            crate::error_display::user_message_from_polars(error)
240        })
241    }
242
243    /// Bumped each time a running statement's failure is put in the prompt.
244    pub fn inline_failures(&self) -> u64 {
245        self.prompt.inline_failures
246    }
247
248    /// Plan a query in `mode` and read its first rows in the background. A query that
249    /// cannot be planned leaves its error on the state, where the prompt shows it. One
250    /// that plans stays pending — the prompt open, when it came from there — until its
251    /// rows are in; if they fail, the view it replaced comes back.
252    pub(crate) fn run_query(&mut self, mode: QueryMode, text: &str, status: &str) {
253        self.prompt.query_run_error = None;
254        let Some(state) = self.data_table_state.as_mut() else {
255            return;
256        };
257        let rollback = state.rollback_point();
258        let rows = state.sql_table_rows();
259        // A query that cannot be planned changes nothing and leaves its error showing.
260        state.deferred(|s| match mode {
261            QueryMode::Sql => s.sql_query(text.to_string()),
262            QueryMode::Q => s.query(text.to_string()),
263        });
264        if state.error().is_some() {
265            return;
266        }
267        self.prompt.query_running = Some(QueryRun {
268            origin: RunOrigin::Query(mode),
269            frame: state.len_generation(),
270            rollback,
271            counts: self.counting.markers(),
272            rows,
273        });
274        if !self.spawn_async_collect(status) {
275            // Nothing to read: the rows on hand already show it.
276            self.prompt.query_running = None;
277            if self.query_prompt_mode() == Some(mode) {
278                self.leave_query_prompt_after_run();
279            }
280        }
281    }
282
283    /// The query still running over the frame on screen, taken. One whose frame has
284    /// since been replaced is dropped: its rollback would undo what replaced it.
285    pub(crate) fn take_query_run(&mut self) -> Option<QueryRun> {
286        let run = self.prompt.query_running.take()?;
287        let frame = self.data_table_state.as_ref()?.len_generation();
288        (run.frame == frame).then_some(run)
289    }
290
291    /// A query ran and its rows are in: the prompt closes on them.
292    pub(crate) fn leave_query_prompt_after_run(&mut self) {
293        self.prompt.sql_completion = None;
294        self.show_table();
295        self.prompt.sql_input.set_focused(false);
296        self.prompt.query_input.set_focused(false);
297        if let Some(state) = &mut self.data_table_state {
298            state.suppress_error_display = false;
299        }
300    }
301
302    /// Only the current language's input carries the cursor.
303    pub(crate) fn sync_query_focus(&mut self) {
304        let mode = self.prompt.query_mode;
305        self.prompt.sql_input.set_focused(mode == QueryMode::Sql);
306        self.prompt.query_input.set_focused(mode == QueryMode::Q);
307    }
308
309    /// Esc from anywhere in the prompt: nothing runs and nothing typed survives.
310    pub(crate) fn close_query_prompt(&mut self) {
311        self.prompt.query_run_error = None;
312        self.prompt.sql_completion = None;
313        self.prompt.query_input.clear();
314        self.prompt.sql_input.clear();
315        self.prompt.query_input.set_focused(false);
316        self.prompt.sql_input.set_focused(false);
317        self.show_table();
318        if let Some(state) = &mut self.data_table_state {
319            state.dismiss_error();
320            state.suppress_error_display = false;
321        }
322    }
323}