Skip to content

bypass sqlparser #193

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 24, 2025
Merged
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
1 change: 1 addition & 0 deletions .config/rainfrog_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mouse_mode = true
[keybindings.Editor]
"<Alt-q>" = "AbortQuery"
"<F5>" = "SubmitEditorQuery"
"<F7>" = "SubmitEditorQueryBypassParser"
"<Alt-1>" = "FocusMenu"
"<Alt-2>" = "FocusEditor"
"<Alt-3>" = "FocusData"
Expand Down
3 changes: 2 additions & 1 deletion src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ pub enum Action {
Error(String),
Help,
SubmitEditorQuery,
Query(Vec<String>, bool), // (query_lines, execution_confirmed)
SubmitEditorQueryBypassParser,
Query(Vec<String>, bool, bool), // (query_lines, execution_confirmed, bypass_parser)
MenuPreview(MenuPreview, String, String), // (preview, schema, table)
QueryToEditor(Vec<String>),
ClearHistory,
Expand Down
37 changes: 25 additions & 12 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#[cfg(not(feature = "termux"))]
use arboard::Clipboard;
use color_eyre::eyre::Result;
use color_eyre::eyre::{Result, eyre};
use crossterm::event::{KeyEvent, MouseEvent, MouseEventKind};
use ratatui::{
Frame,
Expand Down Expand Up @@ -29,8 +29,8 @@ use crate::{
database::{self, Database, DbTaskResult, ExecutionType, Rows},
focus::Focus,
popups::{
PopUp, PopUpPayload, confirm_export::ConfirmExport, confirm_query::ConfirmQuery, confirm_tx::ConfirmTx,
exporting::Exporting, name_favorite::NameFavorite,
PopUp, PopUpPayload, confirm_bypass::ConfirmBypass, confirm_export::ConfirmExport, confirm_query::ConfirmQuery,
confirm_tx::ConfirmTx, exporting::Exporting, name_favorite::NameFavorite,
},
tui,
ui::center,
Expand Down Expand Up @@ -199,13 +199,13 @@ impl App {
}
match database.get_query_results().await? {
DbTaskResult::Finished(results) => {
self.components.data.set_data_state(Some(results.results), Some(results.statement_type));
self.components.data.set_data_state(Some(results.results), results.statement_type);
self.state.last_query_end = Some(chrono::Utc::now());
self.state.query_task_running = false;
},
DbTaskResult::ConfirmTx(rows_affected, statement) => {
self.state.last_query_end = Some(chrono::Utc::now());
self.set_popup(Box::new(ConfirmTx::new(rows_affected, statement.clone())));
self.set_popup(Box::new(ConfirmTx::new(rows_affected, statement)));
self.state.query_task_running = true;
},
DbTaskResult::Pending => {
Expand Down Expand Up @@ -239,7 +239,11 @@ impl App {
self.set_focus(Focus::Editor);
},
Some(PopUpPayload::ConfirmQuery(query)) => {
action_tx.send(Action::Query(vec![query], true))?;
action_tx.send(Action::Query(vec![query], true, false))?;
self.set_focus(Focus::Editor);
},
Some(PopUpPayload::ConfirmBypass(query)) => {
action_tx.send(Action::Query(vec![query], true, true))?;
self.set_focus(Focus::Editor);
},
Some(PopUpPayload::ConfirmExport(confirmed)) => {
Expand All @@ -261,7 +265,7 @@ impl App {
let response = database.commit_tx().await?;
self.state.last_query_end = Some(chrono::Utc::now());
if let Some(results) = response {
self.components.data.set_data_state(Some(results.results), Some(results.statement_type));
self.components.data.set_data_state(Some(results.results), results.statement_type);
self.set_focus(Focus::Editor);
}
},
Expand Down Expand Up @@ -370,30 +374,39 @@ impl App {
let rows = database.load_menu().await;
self.components.menu.set_table_list(Some(rows));
},
Action::Query(query_lines, confirmed) => 'query_action: {
Action::Query(query_lines, confirmed, bypass) => 'query_action: {
let query_string = query_lines.clone().join(" \n");
if query_string.is_empty() {
break 'query_action;
}
self.add_to_history(query_lines.clone());
let execution_info = database::get_execution_type(query_string.clone(), *confirmed, driver);
if *bypass && !confirmed {
log::warn!("Bypassing parser");
self.set_popup(Box::new(ConfirmBypass::new(query_string.clone())));
break 'query_action;
}
let execution_info = match *bypass && *confirmed {
true => Ok((ExecutionType::Normal, None)),
false => database::get_execution_type(query_string.clone(), *confirmed, driver),
};
match execution_info {
Ok((ExecutionType::Transaction, _)) => {
self.components.data.set_loading();
database.start_tx(query_string).await?;
self.state.last_query_start = Some(chrono::Utc::now());
self.state.last_query_end = None;
},
Ok((ExecutionType::Confirm, statement_type)) => {
Ok((ExecutionType::Confirm, Some(statement_type))) => {
self.set_popup(Box::new(ConfirmQuery::new(query_string.clone(), statement_type)));
},
Ok((ExecutionType::Normal, _)) => {
self.components.data.set_loading();
database.start_query(query_string).await?;
database.start_query(query_string, *bypass).await?;
self.state.last_query_start = Some(chrono::Utc::now());
self.state.last_query_end = None;
},
Err(e) => self.components.data.set_data_state(Some(Err(e)), None),
_ => self.components.data.set_data_state(Some(Err(eyre!("Missing statement type but not bypass"))), None),
}
},
Action::AbortQuery => match database.abort_query().await {
Expand All @@ -417,7 +430,7 @@ impl App {
action_tx.send(Action::QueryToEditor(vec![preview_query.clone()]))?;
action_tx.send(Action::FocusEditor)?;
action_tx.send(Action::FocusMenu)?;
action_tx.send(Action::Query(vec![preview_query.clone()], false))?;
action_tx.send(Action::Query(vec![preview_query.clone()], false, false))?;
},

Action::RequestSaveFavorite(query_lines) => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ impl Component for Data<'_> {
}

fn update(&mut self, action: Action, app_state: &AppState) -> Result<Option<Action>> {
if let Action::Query(query, confirmed) = action {
if let Action::Query(query, confirmed, bypass) = action {
self.scrollable.reset_scroll();
} else if let Action::ExportData(format) = action {
let DataState::HasResults(rows) = &self.data_state else {
Expand Down Expand Up @@ -443,7 +443,7 @@ impl Component for Data<'_> {
},
DataState::StatementCompleted(statement) => {
f.render_widget(
Paragraph::new(format!("{} statement completed", statement_type_string(statement)))
Paragraph::new(format!("{} statement completed", statement_type_string(Some(statement.clone()))))
.wrap(Wrap { trim: false })
.block(block),
area,
Expand Down
9 changes: 7 additions & 2 deletions src/components/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ impl Editor<'_> {
Input { key: Key::Enter, alt: true, .. } | Input { key: Key::Enter, ctrl: true, .. } => {
if !app_state.query_task_running {
if let Some(sender) = &self.command_tx {
sender.send(Action::Query(self.textarea.lines().to_vec(), false))?;
sender.send(Action::Query(self.textarea.lines().to_vec(), false, false))?;
self.vim_state = Vim::new(Mode::Normal);
self.vim_state.register_action_handler(self.command_tx.clone())?;
self.cursor_style = Mode::Normal.cursor_style();
Expand Down Expand Up @@ -166,9 +166,14 @@ impl Component for Editor<'_> {

fn update(&mut self, action: Action, app_state: &AppState) -> Result<Option<Action>> {
match action {
Action::SubmitEditorQueryBypassParser => {
if let Some(sender) = &self.command_tx {
sender.send(Action::Query(self.textarea.lines().to_vec(), false, true))?;
}
},
Action::SubmitEditorQuery => {
if let Some(sender) = &self.command_tx {
sender.send(Action::Query(self.textarea.lines().to_vec(), false))?;
sender.send(Action::Query(self.textarea.lines().to_vec(), false, false))?;
}
},
Action::QueryToEditor(lines) => {
Expand Down
29 changes: 18 additions & 11 deletions src/database/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub struct Rows {
#[derive(Debug)]
pub struct QueryResultsWithMetadata {
pub results: Result<Rows>,
pub statement_type: Statement,
pub statement_type: Option<Statement>,
}

#[derive(Debug, Clone, PartialEq)]
Expand All @@ -68,7 +68,7 @@ pub type QueryTask = JoinHandle<QueryResultsWithMetadata>;

pub enum DbTaskResult {
Finished(QueryResultsWithMetadata),
ConfirmTx(Option<u64>, Statement),
ConfirmTx(Option<u64>, Option<Statement>),
Pending,
NoTask,
}
Expand All @@ -83,7 +83,7 @@ pub trait Database {

/// Spawns a tokio task that runs the query. The task should
/// expect to be polled via the `get_query_results()` method.
async fn start_query(&mut self, query: String) -> Result<()>;
async fn start_query(&mut self, query: String, bypass_parser: bool) -> Result<()>;

/// Aborts the tokio task running the active query or transaction.
/// Some drivers also kill the process that was running the query,
Expand Down Expand Up @@ -144,11 +144,15 @@ fn get_first_query(query: String, driver: Driver) -> Result<(String, Statement),
}
}

pub fn get_execution_type(query: String, confirmed: bool, driver: Driver) -> Result<(ExecutionType, Statement)> {
pub fn get_execution_type(
query: String,
confirmed: bool,
driver: Driver,
) -> Result<(ExecutionType, Option<Statement>)> {
let first_query = get_first_query(query, driver);

match first_query {
Ok((_, statement)) => Ok((get_default_execution_type(statement.clone(), confirmed), statement.clone())),
Ok((_, statement)) => Ok((get_default_execution_type(statement.clone(), confirmed), Some(statement.clone()))),
Err(e) => Err(eyre::Report::new(e)),
}
}
Expand Down Expand Up @@ -189,12 +193,15 @@ fn get_default_execution_type(statement: Statement, confirmed: bool) -> Executio
}
}

pub fn statement_type_string(statement: &Statement) -> String {
format!("{statement:?}").split('(').collect::<Vec<&str>>()[0].split('{').collect::<Vec<&str>>()[0]
.split('[')
.collect::<Vec<&str>>()[0]
.trim()
.to_string()
pub fn statement_type_string(statement: Option<Statement>) -> String {
match statement {
Some(stmt) => format!("{stmt:?}").split('(').collect::<Vec<&str>>()[0].split('{').collect::<Vec<&str>>()[0]
.split('[')
.collect::<Vec<&str>>()[0]
.trim()
.to_string(),
None => "UNKNOWN".to_string(),
}
}

pub fn vec_to_string<T: std::string::ToString>(vec: Vec<T>) -> String {
Expand Down
16 changes: 11 additions & 5 deletions src/database/mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,14 @@ impl Database for MySqlDriver<'_> {

// since it's possible for raw_sql to execute multiple queries in a single string,
// we only execute the first one and then drop the rest.
async fn start_query(&mut self, query: String) -> Result<()> {
let (first_query, statement_type) = super::get_first_query(query, Driver::MySql)?;
async fn start_query(&mut self, query: String, bypass_parser: bool) -> Result<()> {
let (first_query, statement_type) = match bypass_parser {
true => (query, None),
false => {
let (first, stmt) = super::get_first_query(query, Driver::MySql)?;
(first, Some(stmt))
},
};
let pool = self.pool.clone().unwrap();
self.querying_conn = Some(Arc::new(Mutex::new(pool.acquire().await?)));
let conn = self.querying_conn.clone().unwrap();
Expand Down Expand Up @@ -154,18 +160,18 @@ impl Database for MySqlDriver<'_> {
(
QueryResultsWithMetadata {
results: Ok(Rows { headers: vec![], rows: vec![], rows_affected: Some(rows_affected) }),
statement_type,
statement_type: Some(statement_type),
},
tx,
)
},
Ok(Either::Right(rows)) => {
log::info!("{:?} rows affected", rows.rows_affected);
(QueryResultsWithMetadata { results: Ok(rows), statement_type }, tx)
(QueryResultsWithMetadata { results: Ok(rows), statement_type: Some(statement_type) }, tx)
},
Err(e) => {
log::error!("{e:?}");
(QueryResultsWithMetadata { results: Err(e), statement_type }, tx)
(QueryResultsWithMetadata { results: Err(e), statement_type: Some(statement_type) }, tx)
},
}
})));
Expand Down
16 changes: 11 additions & 5 deletions src/database/postgresql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,14 @@ impl Database for PostgresDriver<'_> {

// since it's possible for raw_sql to execute multiple queries in a single string,
// we only execute the first one and then drop the rest.
async fn start_query(&mut self, query: String) -> Result<()> {
let (first_query, statement_type) = super::get_first_query(query, Driver::Postgres)?;
async fn start_query(&mut self, query: String, bypass_parser: bool) -> Result<()> {
let (first_query, statement_type) = match bypass_parser {
true => (query, None),
false => {
let (first, stmt) = super::get_first_query(query, Driver::Postgres)?;
(first, Some(stmt))
},
};
let pool = self.pool.clone().unwrap();
self.querying_conn = Some(Arc::new(Mutex::new(pool.acquire().await?)));
let conn = self.querying_conn.clone().unwrap();
Expand Down Expand Up @@ -170,18 +176,18 @@ impl Database for PostgresDriver<'_> {
(
QueryResultsWithMetadata {
results: Ok(Rows { headers: vec![], rows: vec![], rows_affected: Some(rows_affected) }),
statement_type,
statement_type: Some(statement_type),
},
tx,
)
},
Ok(Either::Right(rows)) => {
log::info!("{:?} rows affected", rows.rows_affected);
(QueryResultsWithMetadata { results: Ok(rows), statement_type }, tx)
(QueryResultsWithMetadata { results: Ok(rows), statement_type: Some(statement_type) }, tx)
},
Err(e) => {
log::error!("{e:?}");
(QueryResultsWithMetadata { results: Err(e), statement_type }, tx)
(QueryResultsWithMetadata { results: Err(e), statement_type: Some(statement_type) }, tx)
},
}
})));
Expand Down
16 changes: 11 additions & 5 deletions src/database/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,14 @@ impl Database for SqliteDriver<'_> {

// since it's possible for raw_sql to execute multiple queries in a single string,
// we only execute the first one and then drop the rest.
async fn start_query(&mut self, query: String) -> Result<()> {
let (first_query, statement_type) = super::get_first_query(query, Driver::Sqlite)?;
async fn start_query(&mut self, query: String, bypass_parser: bool) -> Result<()> {
let (first_query, statement_type) = match bypass_parser {
true => (query, None),
false => {
let (first, stmt) = super::get_first_query(query, Driver::Sqlite)?;
(first, Some(stmt))
},
};
let pool = self.pool.clone().unwrap();
self.task = Some(SqliteTask::Query(tokio::spawn(async move {
let results = query_with_pool(pool, first_query.clone()).await;
Expand Down Expand Up @@ -125,18 +131,18 @@ impl Database for SqliteDriver<'_> {
(
QueryResultsWithMetadata {
results: Ok(Rows { headers: vec![], rows: vec![], rows_affected: Some(rows_affected) }),
statement_type,
statement_type: Some(statement_type),
},
tx,
)
},
Ok(Either::Right(rows)) => {
log::info!("{:?} rows affected", rows.rows_affected);
(QueryResultsWithMetadata { results: Ok(rows), statement_type }, tx)
(QueryResultsWithMetadata { results: Ok(rows), statement_type: Some(statement_type) }, tx)
},
Err(e) => {
log::error!("{e:?}");
(QueryResultsWithMetadata { results: Err(e), statement_type }, tx)
(QueryResultsWithMetadata { results: Err(e), statement_type: Some(statement_type) }, tx)
},
}
})));
Expand Down
36 changes: 36 additions & 0 deletions src/popups/confirm_bypass.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use crossterm::event::KeyCode;

use super::{PopUp, PopUpPayload};

#[derive(Debug)]
pub struct ConfirmBypass {
pending_query: String,
}

impl ConfirmBypass {
pub fn new(pending_query: String) -> Self {
Self { pending_query }
}
}

impl PopUp for ConfirmBypass {
fn handle_key_events(
&mut self,
key: crossterm::event::KeyEvent,
app_state: &mut crate::app::AppState,
) -> color_eyre::eyre::Result<Option<PopUpPayload>> {
match key.code {
KeyCode::Char('Y') => Ok(Some(PopUpPayload::ConfirmBypass(self.pending_query.to_owned()))),
KeyCode::Char('N') | KeyCode::Esc => Ok(Some(PopUpPayload::SetDataTable(None, None))),
_ => Ok(None),
}
}

fn get_cta_text(&self, app_state: &crate::app::AppState) -> String {
"Are you sure you want to bypass the query parser? The query will not be wrapped in a transaction, so it cannot be undone.".to_string()
}

fn get_actions_text(&self, app_state: &crate::app::AppState) -> String {
"[Y]es to confirm | [N]o to cancel".to_string()
}
}
Loading
Loading