Skip to content

Commit

Permalink
edit while loading, show duration (#41)
Browse files Browse the repository at this point in the history
* roadmap

* TODOs

* remove unused deps

* TODOs

* allow other key eevnts when query loading

* show duration of query

* adjust scrolling

* adjust explain analyze tx

* roadmap

* update gif
  • Loading branch information
achristmascarl committed Aug 22, 2024
1 parent ad25a2d commit 73da773
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 88 deletions.
5 changes: 2 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ derive_deref = "1.1.1"
directories = "5.0.1"
futures = "0.3.28"
human-panic = "1.2.0"
json5 = "0.4.1"
lazy_static = "1.4.0"
libc = "0.2.148"
log = "0.4.20"
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,25 +178,26 @@ keybindings may not behave exactly like vim. the full list of active vim keybind
- [x] session history
- [x] changelog, release script
- [x] handle explain / analyze output
- [x] show query duration
- [ ] unit / e2e tests
- [ ] loading animation
- [ ] homebrew / [cargo-dist](https://github.com/axodotdev/cargo-dist)
- [ ] homebrew / [cargo-dist](https://github.com/axodotdev/cargo-dist) / install script for bins
</details>

<details>
<summary><b>backburner</b></summary>

- [ ] handle popular postgres extensions (postgis, pgvector, etc.)
- [ ] support mysql, sqlite, other sqlx adapters
- [ ] non-vim editor keybindings
- [ ] change cursor insert-mode style (not sure it's possible with tui-textarea)
- [ ] editor auto-complete
- [ ] live graphs / metrics (a la pgadmin)
- [ ] more packaging
- [ ] customization (keybindings, colors)
- [ ] change cursor insert-mode style (not sure it's possible with tui-textarea)
- [ ] better vim multi-line selection emulation
- [ ] handle more mouse events
- [ ] handle populoar postgres extensions (postgis, pgvector, etc.)
- [ ] support mysql, sqlite, other sqlx adaptors
- [ ] vhs in cd
- [ ] loading animation
</details>

## known issues and limitations
Expand Down
Binary file modified demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
129 changes: 73 additions & 56 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,13 @@ use crate::{
};

pub enum DbTask<'a> {
Query(tokio::task::JoinHandle<QueryResultsWithMetadata>),
TxStart(tokio::task::JoinHandle<(QueryResultsWithMetadata, Transaction<'a, Postgres>)>),
Query(tokio::task::JoinHandle<QueryResultsWithMetadata>, chrono::DateTime<chrono::Utc>),
TxStart(
tokio::task::JoinHandle<(QueryResultsWithMetadata, Transaction<'a, Postgres>)>,
chrono::DateTime<chrono::Utc>,
),
TxPending(Transaction<'a, Postgres>, QueryResultsWithMetadata),
TxCommit(tokio::task::JoinHandle<QueryResultsWithMetadata>),
TxCommit(tokio::task::JoinHandle<QueryResultsWithMetadata>, chrono::DateTime<chrono::Utc>),
}

pub struct HistoryEntry {
Expand Down Expand Up @@ -152,14 +155,14 @@ impl<'a> App<'a> {

loop {
match &mut self.state.query_task {
Some(DbTask::Query(task)) => {
Some(DbTask::Query(task, _)) => {
if task.is_finished() {
let results = task.await?;
self.state.query_task = None;
self.components.data.set_data_state(Some(results.results), Some(results.statement_type));
}
},
Some(DbTask::TxStart(task)) => {
Some(DbTask::TxStart(task, _)) => {
if task.is_finished() {
let (results, tx) = task.await?;
match results.results {
Expand All @@ -174,7 +177,7 @@ impl<'a> App<'a> {
}
}
},
Some(DbTask::TxCommit(task)) => {},
Some(DbTask::TxCommit(task, _)) => {},
_ => {},
}
if let Some(e) = tui.next().await {
Expand All @@ -196,27 +199,35 @@ impl<'a> App<'a> {
KeyCode::Char('Y') | KeyCode::Char('N') | KeyCode::Esc => {
let task = self.state.query_task.take();
if let Some(DbTask::TxPending(tx, results)) = task {
let mut rolled_back = false;
let result = match key.code {
KeyCode::Char('Y') => tx.commit().await,
KeyCode::Char('N') | KeyCode::Esc => tx.rollback().await,
KeyCode::Char('N') | KeyCode::Esc => {
rolled_back = true;
tx.rollback().await
},
_ => panic!("inconsistent key codes"),
};
self.components.data.set_data_state(
match result {
Ok(_) => {
match results.statement_type {
Statement::Explain { .. } if results.results.is_ok() => {
Statement::Explain { .. } if results.results.is_ok() && !rolled_back => {
Some(Ok(results.results.unwrap()))
},
_ => Some(Ok(Rows { headers: vec![], rows: vec![], rows_affected: None })),
}
},
Err(e) => Some(Err(Either::Left(e))),
},
Some(match key.code {
KeyCode::Char('Y') => Statement::Commit { chain: false },
KeyCode::Char('N') | KeyCode::Esc => Statement::Rollback { chain: false, savepoint: None },
_ => panic!("inconsistent key codes"),
Some(match rolled_back {
false => {
match results.statement_type {
Statement::Explain { .. } => results.statement_type,
_ => Statement::Commit { chain: false },
}
},
true => Statement::Rollback { chain: false, savepoint: None },
}),
);
}
Expand Down Expand Up @@ -356,49 +367,55 @@ impl<'a> App<'a> {
Ok(true) => {
self.components.data.set_loading();
let tx = pool.begin().await?;
self.state.query_task = Some(DbTask::TxStart(tokio::spawn(async move {
let (results, tx) = database::query_with_tx(tx, query_string.clone()).await;
match results {
Ok(Either::Left(rows_affected)) => {
log::info!("{:?} rows affected", rows_affected);
let statement_type = database::get_statement_type(query_string.clone().as_str()).unwrap();
(
QueryResultsWithMetadata {
results: Ok(Rows { headers: vec![], rows: vec![], rows_affected: Some(rows_affected) }),
statement_type,
},
tx,
)
},
Ok(Either::Right(rows)) => {
log::info!("{:?} rows affected", rows.rows_affected);
let statement_type = database::get_statement_type(query_string.clone().as_str()).unwrap();
(QueryResultsWithMetadata { results: Ok(rows), statement_type }, tx)
},
Err(e) => {
log::error!("{e:?}");
let statement_type = database::get_statement_type(&query_string).unwrap();
(QueryResultsWithMetadata { results: Err(e), statement_type }, tx)
},
}
})));
self.state.query_task = Some(DbTask::TxStart(
tokio::spawn(async move {
let (results, tx) = database::query_with_tx(tx, query_string.clone()).await;
match results {
Ok(Either::Left(rows_affected)) => {
log::info!("{:?} rows affected", rows_affected);
let statement_type = database::get_statement_type(query_string.clone().as_str()).unwrap();
(
QueryResultsWithMetadata {
results: Ok(Rows { headers: vec![], rows: vec![], rows_affected: Some(rows_affected) }),
statement_type,
},
tx,
)
},
Ok(Either::Right(rows)) => {
log::info!("{:?} rows affected", rows.rows_affected);
let statement_type = database::get_statement_type(query_string.clone().as_str()).unwrap();
(QueryResultsWithMetadata { results: Ok(rows), statement_type }, tx)
},
Err(e) => {
log::error!("{e:?}");
let statement_type = database::get_statement_type(&query_string).unwrap();
(QueryResultsWithMetadata { results: Err(e), statement_type }, tx)
},
}
}),
chrono::Utc::now(),
));
},
Ok(false) => {
self.components.data.set_loading();
self.state.query_task = Some(DbTask::Query(tokio::spawn(async move {
let results = database::query(query_string.clone(), &pool).await;
match &results {
Ok(rows) => {
log::info!("{:?} rows, {:?} affected", rows.rows.len(), rows.rows_affected);
},
Err(e) => {
log::error!("{e:?}");
},
};
let statement_type = database::get_statement_type(&query_string).unwrap();
self.state.query_task = Some(DbTask::Query(
tokio::spawn(async move {
let results = database::query(query_string.clone(), &pool).await;
match &results {
Ok(rows) => {
log::info!("{:?} rows, {:?} affected", rows.rows.len(), rows.rows_affected);
},
Err(e) => {
log::error!("{e:?}");
},
};
let statement_type = database::get_statement_type(&query_string).unwrap();

QueryResultsWithMetadata { results, statement_type }
})));
QueryResultsWithMetadata { results, statement_type }
}),
chrono::Utc::now(),
));
},
Err(e) => self.components.data.set_data_state(Some(Err(e)), None),
}
Expand All @@ -410,12 +427,12 @@ impl<'a> App<'a> {
},
Action::AbortQuery => {
match &self.state.query_task {
Some(DbTask::Query(task)) => {
Some(DbTask::Query(task, _)) => {
task.abort();
self.state.query_task = None;
self.components.data.set_cancelled();
},
Some(DbTask::TxStart(task)) => {
Some(DbTask::TxStart(task, _)) => {
task.abort();
self.state.query_task = None;
self.components.data.set_cancelled();
Expand Down Expand Up @@ -451,13 +468,13 @@ impl<'a> App<'a> {
if self.should_quit {
if let Some(query_task) = self.state.query_task.take() {
match query_task {
DbTask::Query(task) => {
DbTask::Query(task, _) => {
task.abort();
},
DbTask::TxStart(task) => {
DbTask::TxStart(task, _) => {
task.abort();
},
DbTask::TxCommit(task) => {
DbTask::TxCommit(task, _) => {
task.abort();
},
_ => {},
Expand Down
21 changes: 12 additions & 9 deletions src/components/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,11 @@ impl<'a> Data<'a> {
},
ScrollDirection::Left => {
self.explain_scroll =
Some(ExplainOffsets { y_offset: offsets.y_offset, x_offset: offsets.x_offset.saturating_sub(1) });
Some(ExplainOffsets { y_offset: offsets.y_offset, x_offset: offsets.x_offset.saturating_sub(2) });
},
ScrollDirection::Right => {
self.explain_scroll =
Some(ExplainOffsets { y_offset: offsets.y_offset, x_offset: offsets.x_offset.saturating_add(1) });
Some(ExplainOffsets { y_offset: offsets.y_offset, x_offset: offsets.x_offset.saturating_add(2) });
},
};
}
Expand Down Expand Up @@ -367,19 +367,22 @@ impl<'a> Component for Data<'a> {
fn draw(&mut self, f: &mut Frame<'_>, area: Rect, app_state: &AppState) -> Result<()> {
let focused = app_state.focus == Focus::Data;

self.explain_max_x_offset = self.explain_width.saturating_sub(area.width);
self.explain_max_y_offset = self.explain_height.saturating_sub(area.height);
let mut block = Block::default().borders(Borders::ALL).border_style(if focused {
Style::new().green()
} else {
Style::new().dim()
});

let inner_area = block.inner(area);

self.explain_max_x_offset = self.explain_width.saturating_sub(inner_area.width);
self.explain_max_y_offset = self.explain_height.saturating_sub(inner_area.height);
if let Some(ExplainOffsets { y_offset, x_offset }) = self.explain_scroll {
self.explain_scroll = Some(ExplainOffsets {
y_offset: y_offset.min(self.explain_max_y_offset),
x_offset: x_offset.min(self.explain_max_x_offset),
});
}
let mut block = Block::default().borders(Borders::ALL).border_style(if focused {
Style::new().green()
} else {
Style::new().dim()
});

if let DataState::HasResults(Rows { rows, .. }) = &self.data_state {
let (x, y) = self.scrollable.get_cell_offsets();
Expand Down
Loading

0 comments on commit 73da773

Please sign in to comment.