Skip to content

Commit e9ab3c1

Browse files
committed
feat(vx browse): add interactive SQL query tab
Adds a Query tab to the TUI browse mode with: - SQL input with cursor navigation - Paginated result display with LIMIT/OFFSET - Column sorting (click header to cycle ASC/DESC/none) - Uses shared DataFusion helper for query execution Signed-off-by: Baris Palaska <[email protected]>
1 parent dd2fbcd commit e9ab3c1

File tree

5 files changed

+804
-137
lines changed

5 files changed

+804
-137
lines changed

vortex-tui/src/browse/app.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use vortex::dtype::DType;
1212
use vortex::error::VortexExpect;
1313
use vortex::error::VortexResult;
1414
use vortex::error::VortexUnwrap;
15+
use vortex::error::vortex_err;
1516
use vortex::file::Footer;
1617
use vortex::file::OpenOptionsSessionExt;
1718
use vortex::file::SegmentSpec;
@@ -24,6 +25,7 @@ use vortex::layout::segments::SegmentId;
2425
use vortex::layout::segments::SegmentSource;
2526

2627
use crate::SESSION;
28+
use crate::browse::ui::QueryState;
2729
use crate::browse::ui::SegmentGridState;
2830

2931
#[derive(Default, Copy, Clone, Eq, PartialEq)]
@@ -34,8 +36,9 @@ pub enum Tab {
3436

3537
/// Show a segment map of the file
3638
Segments,
37-
// TODO(aduffy): SQL query page powered by DF
38-
// Query,
39+
40+
/// SQL query interface powered by DataFusion
41+
Query,
3942
}
4043

4144
/// A pointer into the `Layout` hierarchy that can be advanced.
@@ -199,6 +202,12 @@ pub struct AppState<'a> {
199202

200203
/// Scroll offset for the encoding tree display in FlatLayout view
201204
pub tree_scroll_offset: u16,
205+
206+
/// State for the Query tab
207+
pub query_state: QueryState,
208+
209+
/// File path for use in query execution
210+
pub file_path: String,
202211
}
203212

204213
impl AppState<'_> {
@@ -210,6 +219,11 @@ impl AppState<'_> {
210219

211220
/// Create an app backed from a file path.
212221
pub async fn create_file_app<'a>(path: impl AsRef<Path>) -> VortexResult<AppState<'a>> {
222+
let file_path = path
223+
.as_ref()
224+
.to_str()
225+
.ok_or_else(|| vortex_err!("Path is not valid UTF-8"))?
226+
.to_string();
213227
let vxf = SESSION.open_options().open(path.as_ref()).await?;
214228

215229
let cursor = LayoutCursor::new(vxf.footer().clone(), vxf.segment_source());
@@ -225,5 +239,7 @@ pub async fn create_file_app<'a>(path: impl AsRef<Path>) -> VortexResult<AppStat
225239
segment_grid_state: SegmentGridState::default(),
226240
frame_size: Size::new(0, 0),
227241
tree_scroll_offset: 0,
242+
query_state: QueryState::default(),
243+
file_path,
228244
})
229245
}

vortex-tui/src/browse/mod.rs

Lines changed: 129 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ use vortex::error::VortexExpect;
1919
use vortex::error::VortexResult;
2020
use vortex::layout::layouts::flat::FlatVTable;
2121

22+
use crate::browse::ui::QueryFocus;
23+
use crate::browse::ui::SortDirection;
24+
2225
mod app;
2326
mod ui;
2427

@@ -47,10 +50,61 @@ enum HandleResult {
4750
Exit,
4851
}
4952

53+
#[allow(clippy::cognitive_complexity)]
5054
fn handle_normal_mode(app: &mut AppState, event: Event) -> HandleResult {
5155
if let Event::Key(key) = event
5256
&& key.kind == KeyEventKind::Press
5357
{
58+
// Check if we're in Query tab with SQL input focus - handle text input first
59+
let in_sql_input =
60+
app.current_tab == Tab::Query && app.query_state.focus == QueryFocus::SqlInput;
61+
62+
// Handle SQL input mode - most keys should type into the input
63+
if in_sql_input {
64+
match (key.code, key.modifiers) {
65+
// These keys exit/switch even in SQL input mode
66+
(KeyCode::Tab, _) => {
67+
app.current_tab = Tab::Layout;
68+
}
69+
(KeyCode::Esc, _) => {
70+
app.query_state.toggle_focus();
71+
}
72+
(KeyCode::Enter, _) => {
73+
// Execute the SQL query with COUNT(*) for pagination
74+
app.query_state.sort_column = None;
75+
app.query_state.sort_direction = SortDirection::None;
76+
let file_path = app.file_path.clone();
77+
app.query_state.execute_initial_query(&file_path);
78+
// Switch focus to results table after executing
79+
app.query_state.focus = QueryFocus::ResultsTable;
80+
}
81+
// Navigation keys
82+
(KeyCode::Left, _) => app.query_state.move_cursor_left(),
83+
(KeyCode::Right, _) => app.query_state.move_cursor_right(),
84+
(KeyCode::Home, _) => app.query_state.move_cursor_start(),
85+
(KeyCode::End, _) => app.query_state.move_cursor_end(),
86+
// Control key shortcuts
87+
(KeyCode::Char('a'), KeyModifiers::CONTROL) => app.query_state.move_cursor_start(),
88+
(KeyCode::Char('e'), KeyModifiers::CONTROL) => app.query_state.move_cursor_end(),
89+
(KeyCode::Char('u'), KeyModifiers::CONTROL) => app.query_state.clear_input(),
90+
(KeyCode::Char('b'), KeyModifiers::CONTROL) => app.query_state.move_cursor_left(),
91+
(KeyCode::Char('f'), KeyModifiers::CONTROL) => app.query_state.move_cursor_right(),
92+
(KeyCode::Char('d'), KeyModifiers::CONTROL) => {
93+
app.query_state.delete_char_forward()
94+
}
95+
// Delete keys
96+
(KeyCode::Backspace, _) => app.query_state.delete_char(),
97+
(KeyCode::Delete, _) => app.query_state.delete_char_forward(),
98+
// All other characters get typed into the input
99+
(KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
100+
app.query_state.insert_char(c);
101+
}
102+
_ => {}
103+
}
104+
return HandleResult::Continue;
105+
}
106+
107+
// Normal mode handling for all other cases
54108
match (key.code, key.modifiers) {
55109
(KeyCode::Char('q'), _) => {
56110
// Close the process down.
@@ -60,9 +114,25 @@ fn handle_normal_mode(app: &mut AppState, event: Event) -> HandleResult {
60114
// toggle between tabs
61115
app.current_tab = match app.current_tab {
62116
Tab::Layout => Tab::Segments,
63-
Tab::Segments => Tab::Layout,
117+
Tab::Segments => Tab::Query,
118+
Tab::Query => Tab::Layout,
64119
};
65120
}
121+
122+
// Query tab: Ctrl+h for previous page
123+
(KeyCode::Char('h'), KeyModifiers::CONTROL) => {
124+
if app.current_tab == Tab::Query {
125+
app.query_state.prev_page(&app.file_path.clone());
126+
}
127+
}
128+
129+
// Query tab: Ctrl+l for next page
130+
(KeyCode::Char('l'), KeyModifiers::CONTROL) => {
131+
if app.current_tab == Tab::Query {
132+
app.query_state.next_page(&app.file_path.clone());
133+
}
134+
}
135+
66136
(KeyCode::Up | KeyCode::Char('k'), _) | (KeyCode::Char('p'), KeyModifiers::CONTROL) => {
67137
// We send the key-up to the list state if we're looking at
68138
// the Layouts tab.
@@ -75,6 +145,9 @@ fn handle_normal_mode(app: &mut AppState, event: Event) -> HandleResult {
75145
}
76146
}
77147
Tab::Segments => app.segment_grid_state.scroll_up(10),
148+
Tab::Query => {
149+
app.query_state.table_state.select_previous();
150+
}
78151
}
79152
}
80153
(KeyCode::Down | KeyCode::Char('j'), _)
@@ -87,6 +160,9 @@ fn handle_normal_mode(app: &mut AppState, event: Event) -> HandleResult {
87160
}
88161
}
89162
Tab::Segments => app.segment_grid_state.scroll_down(10),
163+
Tab::Query => {
164+
app.query_state.table_state.select_next();
165+
}
90166
},
91167
(KeyCode::PageUp, _) | (KeyCode::Char('v'), KeyModifiers::ALT) => {
92168
match app.current_tab {
@@ -98,6 +174,9 @@ fn handle_normal_mode(app: &mut AppState, event: Event) -> HandleResult {
98174
}
99175
}
100176
Tab::Segments => app.segment_grid_state.scroll_up(100),
177+
Tab::Query => {
178+
app.query_state.prev_page(&app.file_path.clone());
179+
}
101180
}
102181
}
103182
(KeyCode::PageDown, _) | (KeyCode::Char('v'), KeyModifiers::CONTROL) => {
@@ -110,25 +189,39 @@ fn handle_normal_mode(app: &mut AppState, event: Event) -> HandleResult {
110189
}
111190
}
112191
Tab::Segments => app.segment_grid_state.scroll_down(100),
192+
Tab::Query => {
193+
app.query_state.next_page(&app.file_path.clone());
194+
}
113195
}
114196
}
115197
(KeyCode::Home, _) | (KeyCode::Char('<'), KeyModifiers::ALT) => match app.current_tab {
116198
Tab::Layout => app.layouts_list_state.select_first(),
117199
Tab::Segments => app.segment_grid_state.scroll_left(200),
200+
Tab::Query => {
201+
app.query_state.table_state.select_first();
202+
}
118203
},
119204
(KeyCode::End, _) | (KeyCode::Char('>'), KeyModifiers::ALT) => match app.current_tab {
120205
Tab::Layout => app.layouts_list_state.select_last(),
121206
Tab::Segments => app.segment_grid_state.scroll_right(200),
207+
Tab::Query => {
208+
app.query_state.table_state.select_last();
209+
}
122210
},
123211
(KeyCode::Enter, _) => {
124-
if app.current_tab == Tab::Layout && app.cursor.layout().nchildren() > 0 {
125-
// Descend into the layout subtree for the selected child.
126-
let selected = app.layouts_list_state.selected().unwrap_or_default();
127-
app.cursor = app.cursor.child(selected);
212+
match app.current_tab {
213+
Tab::Layout => {
214+
if app.cursor.layout().nchildren() > 0 {
215+
// Descend into the layout subtree for the selected child.
216+
let selected = app.layouts_list_state.selected().unwrap_or_default();
217+
app.cursor = app.cursor.child(selected);
128218

129-
// Reset the list scroll state and tree scroll offset.
130-
app.layouts_list_state = ListState::default().with_selected(Some(0));
131-
app.tree_scroll_offset = 0;
219+
// Reset the list scroll state and tree scroll offset.
220+
app.layouts_list_state = ListState::default().with_selected(Some(0));
221+
app.tree_scroll_offset = 0;
222+
}
223+
}
224+
Tab::Query | Tab::Segments => {}
132225
}
133226
}
134227
(KeyCode::Left | KeyCode::Char('h'), _)
@@ -142,17 +235,44 @@ fn handle_normal_mode(app: &mut AppState, event: Event) -> HandleResult {
142235
app.tree_scroll_offset = 0;
143236
}
144237
Tab::Segments => app.segment_grid_state.scroll_left(20),
238+
Tab::Query => {
239+
app.query_state.horizontal_scroll =
240+
app.query_state.horizontal_scroll.saturating_sub(1);
241+
}
145242
}
146243
}
147244
(KeyCode::Right | KeyCode::Char('l'), _) | (KeyCode::Char('b'), KeyModifiers::ALT) => {
148245
match app.current_tab {
149246
Tab::Layout => {}
150247
Tab::Segments => app.segment_grid_state.scroll_right(20),
248+
Tab::Query => {
249+
let max_col = app.query_state.column_count().saturating_sub(1);
250+
if app.query_state.horizontal_scroll < max_col {
251+
app.query_state.horizontal_scroll += 1;
252+
}
253+
}
151254
}
152255
}
153256

154257
(KeyCode::Char('/'), _) | (KeyCode::Char('s'), KeyModifiers::CONTROL) => {
155-
app.key_mode = KeyMode::Search;
258+
if app.current_tab != Tab::Query {
259+
app.key_mode = KeyMode::Search;
260+
}
261+
}
262+
263+
(KeyCode::Char('s'), KeyModifiers::NONE) => {
264+
if app.current_tab == Tab::Query {
265+
// Sort by selected column - modifies the SQL query
266+
let col = app.query_state.selected_column();
267+
app.query_state.apply_sort(col, &app.file_path);
268+
}
269+
}
270+
271+
(KeyCode::Esc, _) => {
272+
if app.current_tab == Tab::Query {
273+
// Toggle focus in Query tab
274+
app.query_state.toggle_focus();
275+
}
156276
}
157277

158278
// Most events not handled

vortex-tui/src/browse/ui/mod.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

44
mod layouts;
5+
mod query;
56
mod segments;
67

78
use layouts::render_layouts;
9+
pub use query::QueryFocus;
10+
pub use query::QueryState;
11+
pub use query::SortDirection;
12+
use query::render_query;
813
use ratatui::prelude::*;
914
use ratatui::widgets::Block;
1015
use ratatui::widgets::BorderType;
@@ -57,17 +62,13 @@ pub fn render_app(app: &mut AppState<'_>, frame: &mut Frame<'_>) {
5762
let selected_tab = match app.current_tab {
5863
Tab::Layout => 0,
5964
Tab::Segments => 1,
65+
Tab::Query => 2,
6066
};
6167

62-
let tabs = Tabs::new([
63-
"File Layout",
64-
"Segments",
65-
// TODO(aduffy): add SQL query interface
66-
// "Query",
67-
])
68-
.style(Style::default().bold().white())
69-
.highlight_style(Style::default().bold().black().on_white())
70-
.select(Some(selected_tab));
68+
let tabs = Tabs::new(["File Layout", "Segments", "Query"])
69+
.style(Style::default().bold().white())
70+
.highlight_style(Style::default().bold().black().on_white())
71+
.select(Some(selected_tab));
7172

7273
frame.render_widget(tabs, tab_view);
7374

@@ -77,5 +78,6 @@ pub fn render_app(app: &mut AppState<'_>, frame: &mut Frame<'_>) {
7778
render_layouts(app, app_view, frame.buffer_mut());
7879
}
7980
Tab::Segments => segments_ui(app, app_view, frame.buffer_mut()),
81+
Tab::Query => render_query(app, app_view, frame.buffer_mut()),
8082
}
8183
}

0 commit comments

Comments
 (0)