Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 273 additions & 0 deletions src/ui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub enum Focus {
Help,
TableInspector,
ExportPicker,
StatsPanel,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -76,6 +77,12 @@ impl ExportFormat {
}
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TransactionState {
None,
Active,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SidebarTab {
Databases,
Expand Down Expand Up @@ -154,8 +161,17 @@ pub struct App {
// Export
pub export_selected: usize,

// Query statistics
pub query_stats: QueryStats,
pub stats_scroll: usize,

// Async connection task
pub pending_connection: Option<(ConnectionConfig, JoinHandle<Result<Client>>)>,

// Transaction management
pub transaction_state: TransactionState,
pub transaction_start: Option<Instant>,
pub transaction_query_count: usize,
}

#[derive(Debug, Clone, Copy)]
Expand Down Expand Up @@ -274,6 +290,67 @@ pub struct AutocompleteState {
pub prefix: String,
}

#[derive(Debug, Clone, Default)]
pub struct QueryStats {
pub total_queries: usize,
pub successful_queries: usize,
pub failed_queries: usize,
pub total_time_ms: f64,
pub min_time_ms: f64,
pub max_time_ms: f64,
pub session_queries: usize,
pub session_total_time_ms: f64,
}

impl QueryStats {
pub fn avg_time_ms(&self) -> f64 {
if self.total_queries == 0 {
0.0
} else {
self.total_time_ms / self.total_queries as f64
}
}

pub fn success_rate(&self) -> f64 {
if self.total_queries == 0 {
0.0
} else {
(self.successful_queries as f64 / self.total_queries as f64) * 100.0
}
}

pub fn session_avg_time_ms(&self) -> f64 {
if self.session_queries == 0 {
0.0
} else {
self.session_total_time_ms / self.session_queries as f64
}
}

pub fn record_query(&mut self, time_ms: f64, success: bool) {
self.total_queries += 1;
self.session_queries += 1;
self.total_time_ms += time_ms;
self.session_total_time_ms += time_ms;
if success {
self.successful_queries += 1;
} else {
self.failed_queries += 1;
}
if self.total_queries == 1 {
self.min_time_ms = time_ms;
self.max_time_ms = time_ms;
} else {
if time_ms < self.min_time_ms {
self.min_time_ms = time_ms;
}
if time_ms > self.max_time_ms {
self.max_time_ms = time_ms;
}
}
}
}

pub const SQL_FUNCTIONS: &[&str] = &[
"COUNT",
"SUM",
Expand Down Expand Up @@ -418,7 +495,15 @@ impl App {

table_inspector: None,
export_selected: 0,

query_stats: QueryStats::default(),
stats_scroll: 0,

pending_connection: None,

transaction_state: TransactionState::None,
transaction_start: None,
transaction_query_count: 0,
}
}

Expand Down Expand Up @@ -504,6 +589,7 @@ impl App {
Focus::Help => self.handle_help_input(key).await,
Focus::TableInspector => self.handle_table_inspector_input(key).await,
Focus::ExportPicker => self.handle_export_input(key).await,
Focus::StatsPanel => self.handle_stats_input(key).await,
}
}

Expand Down Expand Up @@ -885,6 +971,16 @@ impl App {
self.execute_query().await?;
self.focus = Focus::Results;
}
// Transaction management
KeyCode::Char('t') if ctrl => {
self.begin_transaction().await?;
}
KeyCode::Char('k') if ctrl => {
self.commit_transaction().await?;
}
KeyCode::Char('r') if ctrl && shift => {
self.rollback_transaction().await?;
}
KeyCode::Enter => {
self.editor.insert_newline();
self.autocomplete.active = false;
Expand Down Expand Up @@ -914,6 +1010,10 @@ impl App {
self.editor.clear();
self.autocomplete.active = false;
}
KeyCode::Char('S') if ctrl => {
self.focus = Focus::StatsPanel;
self.stats_scroll = 0;
}
// Pane resizing: Ctrl+Shift+Up/Down
KeyCode::Up if ctrl && shift => {
// Make editor smaller / results bigger
Expand Down Expand Up @@ -1220,6 +1320,28 @@ impl App {
Ok(())
}

async fn handle_stats_input(&mut self, key: KeyEvent) -> Result<()> {
match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
self.focus = Focus::Editor;
}
KeyCode::Up => {
self.stats_scroll = self.stats_scroll.saturating_sub(1);
}
KeyCode::Down => {
self.stats_scroll += 1;
}
KeyCode::PageUp => {
self.stats_scroll = self.stats_scroll.saturating_sub(10);
}
KeyCode::PageDown => {
self.stats_scroll += 10;
}
_ => {}
}
Ok(())
}

async fn open_table_inspector(&mut self) {
if self.sidebar_tab != SidebarTab::Tables || self.connection.client.is_none() {
return;
Expand Down Expand Up @@ -1631,6 +1753,24 @@ impl App {
return Ok(());
}

// Detect transaction commands typed manually
let query_upper = query.trim().to_uppercase();
if query_upper == "BEGIN" || query_upper == "START TRANSACTION" {
if self.transaction_state != TransactionState::Active {
self.transaction_state = TransactionState::Active;
self.transaction_start = Some(Instant::now());
self.transaction_query_count = 0;
}
} else if query_upper == "COMMIT" || query_upper == "END" {
self.transaction_state = TransactionState::None;
self.transaction_start = None;
self.transaction_query_count = 0;
} else if query_upper == "ROLLBACK" || query_upper == "ABORT" {
self.transaction_state = TransactionState::None;
self.transaction_start = None;
self.transaction_query_count = 0;
}

if self.connection.client.is_some() {
self.start_loading("Executing query...".to_string());

Expand All @@ -1649,6 +1789,10 @@ impl App {
self.query_history.add(entry);
let _ = self.query_history.save();

// Record query statistics
let time_ms = result.execution_time.as_secs_f64() * 1000.0;
self.query_stats.record_query(time_ms, result.error.is_none());

// Update status
if let Some(err) = &result.error {
self.set_status(format!("Error: {}", err), StatusType::Error);
Expand Down Expand Up @@ -1687,6 +1831,10 @@ impl App {
};

self.results.push(result);
// Track transaction query count
if self.transaction_state == TransactionState::Active {
self.transaction_query_count += 1;
}
self.explain_plans.push(plan);
self.current_result = self.results.len() - 1;
self.result_selected_row = 0;
Expand All @@ -1704,6 +1852,92 @@ impl App {
Ok(())
}

async fn begin_transaction(&mut self) -> Result<()> {
if self.connection.client.is_none() {
self.set_status("Not connected to database".to_string(), StatusType::Error);
return Ok(());
}
if self.transaction_state == TransactionState::Active {
self.set_status("Transaction already active".to_string(), StatusType::Warning);
return Ok(());
}
let client = self.connection.client.as_ref().unwrap();
match execute_query(client, "BEGIN").await {
Ok(_) => {
self.transaction_state = TransactionState::Active;
self.transaction_start = Some(Instant::now());
self.transaction_query_count = 0;
self.set_status("Transaction started".to_string(), StatusType::Info);
}
Err(e) => {
self.set_status(format!("BEGIN failed: {}", e), StatusType::Error);
}
}
Ok extraordinary(())
}

async fn commit_transaction(&mut self) -> Result<()> {
if self.connection.client.is_none() {
self.set_status("Not connected to database".to_string(), StatusType::Error);
return Ok(());
}
if self.transaction_state != TransactionState::Active {
self.set_status("No active transaction to commit".to_string(), StatusType::Warning);
return Ok(());
}
let client = self.connection.client.as_ref().unwrap();
match execute_query(client, "COMMIT").await {
Ok(_) => {
let duration = self.transaction_start
.map(|t| t.elapsed().as_secs())
.unwrap_or(0);
self.transaction_state = TransactionState::None;
self.transaction_start = None;
let msg = format!(
"Transaction committed ({} queries, {}s)",
self.transaction_query_count, duration
);
self.transaction_query_count = 0;
self.set_status(msg, StatusType::Success);
}
Err(e) => {
self.set_status(format!("COMMIT failed: {}", e), StatusType::Error);
}
}
Ok(())
}

async fn rollback_transaction(&mut self) -> Result<()> {
if self.connection.client.is_none() {
self.set_status("Not connected to database".to_string(), StatusType::Error);
return Ok(());
}
if self.transaction_state != TransactionState::Active {
self.set_status("No active transaction to rollback".to_string(), StatusType::Warning);
return Ok(());
}
let client = self.connection.client.as_ref().unwrap();
match execute_query(client, "ROLLBACK").await {
Ok(_) => {
let duration = self.transaction_start
.map(|t| t.elapsed().as_secs())
.unwrap_or(0);
self.transaction_state = TransactionState::None;
self.transaction_start = None;
let msg = format!(
"Transaction rolled back ({} queries, {}s)",
self.transaction_query_count, duration
);
self.transaction_query_count = 0;
self.set_status(msg, StatusType::Warning);
}
Err(e) => {
self.set_status(format!("ROLLBACK failed: {}", e), StatusType::Error);
}
}
Ok(())
}

fn update_autocomplete(&mut self) {
let line = self.editor.current_line().to_string();
let cursor_x = self.editor.cursor_x;
Expand Down Expand Up @@ -1891,3 +2125,42 @@ fn dialog_field_len(config: &ConnectionConfig, field_index: usize) -> usize {
_ => 0,
}
}


#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_query_stats_default() {
let stats = QueryStats::default();
assert_eq!(stats.total_queries, 0);
assert_eq!(stats.avg_time_ms(), 0.0);
assert_eq!(stats.success_rate(), 0.0);
}

#[test]
fn test_query_stats_record() {
let mut stats = QueryStats::default();
stats.record_query(100.0, true);
stats.record_query(200.0, true);
stats.record_query(50.0, false);
assert_eq!(stats.total_queries, 3);
assert_eq!(stats.successful_queries, 2);
assert_eq!(stats.failed_queries, 1);
assert!((stats.avg_time_ms() - 116.666).abs() < 1.0);
assert!((stats.success_rate() - 66.666).abs() < 1.0);
assert_eq!(stats.min_time_ms, 50.0);
assert_eq!(stats.max_time_ms, 200.0);
}

#[test]
fn test_query_stats_session() {
let mut stats = QueryStats::default();
stats.record_query(100.0, true);
stats.record_query(200.0, true);
assert_eq!(stats.session_queries, 2);
assert_eq!(stats.session_total_time_ms, 300.0);
assert_eq!(stats.session_avg_time_ms(), 150.0);
}
}
Loading
Loading