Skip to content
Open
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
130 changes: 130 additions & 0 deletions src/ui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,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 @@ -156,6 +162,11 @@ pub struct App {

// 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 @@ -419,6 +430,9 @@ impl App {
table_inspector: None,
export_selected: 0,
pending_connection: None,
transaction_state: TransactionState::None,
transaction_start: None,
transaction_query_count: 0,
}
}

Expand Down Expand Up @@ -885,6 +899,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 @@ -1631,6 +1655,22 @@ impl App {
return Ok(());
}

// Detect transaction commands typed manually
let query_upper = query.trim().to_uppercase();
if query_upper == "BEGIN" || query_upper == "START TRANSACTION" {
self.transaction_state = TransactionState::Active;
self.transaction_start = Some(Instant::now());
self.transaction_query_count = 0;
} else if query_upper == "COMMIT"
|| query_upper == "END"
|| 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 Down Expand Up @@ -1686,6 +1726,11 @@ impl App {
None
};

// Track transaction query count
if self.transaction_state == TransactionState::Active {
self.transaction_query_count += 1;
}

self.results.push(result);
self.explain_plans.push(plan);
self.current_result = self.results.len() - 1;
Expand All @@ -1704,6 +1749,91 @@ 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();
let result = execute_query(client, "BEGIN").await?;
if result.error.is_some() {
self.set_status("BEGIN failed".to_string(), StatusType::Error);
} else {
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);
}
Ok(())
}

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_string(), StatusType::Warning);
return Ok(());
}
let client = self.connection.client.as_ref().unwrap();
let result = execute_query(client, "COMMIT").await?;
if result.error.is_some() {
self.set_status("COMMIT failed".to_string(), StatusType::Error);
} else {
let duration = self
.transaction_start
.map(|t| t.elapsed().as_secs())
.unwrap_or(0);
let msg = format!(
"Transaction committed ({} queries, {}s)",
self.transaction_query_count, duration
);
self.transaction_state = TransactionState::None;
self.transaction_start = None;
self.transaction_query_count = 0;
self.set_status(msg, StatusType::Success);
}
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_string(), StatusType::Warning);
return Ok(());
}
let client = self.connection.client.as_ref().unwrap();
let result = execute_query(client, "ROLLBACK").await?;
if result.error.is_some() {
self.set_status("ROLLBACK failed".to_string(), StatusType::Error);
} else {
let duration = self
.transaction_start
.map(|t| t.elapsed().as_secs())
.unwrap_or(0);
let msg = format!(
"Transaction rolled back ({} queries, {}s)",
self.transaction_query_count, duration
);
self.transaction_state = TransactionState::None;
self.transaction_start = None;
self.transaction_query_count = 0;
self.set_status(msg, StatusType::Warning);
}
Ok(())
}

fn update_autocomplete(&mut self) {
let line = self.editor.current_line().to_string();
let cursor_x = self.editor.cursor_x;
Expand Down
33 changes: 29 additions & 4 deletions src/ui/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::explain::{
};
use crate::ui::{
is_sql_function, is_sql_keyword, is_sql_type, App, Focus, SidebarTab, StatusType, Theme,
EXPORT_FORMATS, SPINNER_FRAMES,
TransactionState, EXPORT_FORMATS, SPINNER_FRAMES,
};

pub fn draw(frame: &mut Frame, app: &App) {
Expand Down Expand Up @@ -83,12 +83,26 @@ pub fn draw(frame: &mut Frame, app: &App) {
fn draw_header(frame: &mut Frame, app: &App, area: Rect) {
let theme = &app.theme;

let txn_indicator = if app.transaction_state == TransactionState::Active {
let duration = app
.transaction_start
.map(|t| t.elapsed().as_secs())
.unwrap_or(0);
format!(
" | [TXN ACTIVE {}s, {} queries]",
duration, app.transaction_query_count
)
} else {
String::new()
};

let connection_info = if app.connection.is_connected() {
format!(
" {} | {} | {} ",
" {} | {} | {}{}",
app.connection.config.display_string(),
app.connection.current_database,
app.connection.current_schema
app.connection.current_schema,
txn_indicator
)
} else {
" Not Connected ".to_string()
Expand All @@ -100,7 +114,15 @@ fn draw_header(frame: &mut Frame, app: &App, area: Rect) {
" ".repeat(area.width.saturating_sub(connection_info.len() as u16 + 10) as usize)
);

let header = Paragraph::new(header_text).style(theme.header());
let header_style = if app.transaction_state == TransactionState::Active {
Style::default()
.fg(theme.bg_primary)
.bg(theme.warning)
.add_modifier(Modifier::BOLD)
} else {
theme.header()
};
let header = Paragraph::new(header_text).style(header_style);

frame.render_widget(header, area);
}
Expand Down Expand Up @@ -1501,6 +1523,9 @@ fn draw_help_overlay(frame: &mut Frame, app: &App) {
" Ctrl+Shift+Z/Y Redo",
" Ctrl+A Select all",
" Ctrl+Space Trigger autocomplete",
" Ctrl+T Begin transaction",
" Ctrl+K Commit transaction",
" Ctrl+Shift+R Rollback transaction",
" Tab Insert spaces",
"",
" SIDEBAR",
Expand Down