Skip to content
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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,5 @@ body:
id: logs
attributes:
label: Error message or log
description: Paste it as text. For an update or AI assistant problem, add the end of `update.log` or `mcp.log` from the [data folder](https://github.com/Bare7a/BarSQL/blob/master/guide/app.md#-portable-by-design). Leave out passwords, connection strings and private data.
description: Paste it as text. If BarSQL closed by itself or said it hit a bug, add `crash.log` from the [data folder](https://github.com/Bare7a/BarSQL/blob/master/guide/app.md#-portable-by-design). For an update or AI assistant problem, add the end of `update.log` or `mcp.log`. Leave out passwords, connection strings and private data.
render: text
16 changes: 8 additions & 8 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ resolver = "3"
members = ["crates/*", "xtask"]

[workspace.package]
version = "1.2.2"
version = "1.2.3"
edition = "2024"
license = "GPL-3.0-only"
publish = false
Expand Down
6 changes: 1 addition & 5 deletions crates/barsql-app/src/backup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::io::Write;
use std::path::{Path, PathBuf};

use barsql_core::QueryError;
use barsql_db::{Cancel, Cell, ScriptEvent};
use barsql_db::{Cancel, Cell, ScriptEvent, join_error};
use barsql_sql::ddl::terminate_statement;
use barsql_sql::table_ref;

Expand Down Expand Up @@ -59,10 +59,6 @@ fn io_error(error: std::io::Error) -> QueryError {
QueryError::message(error.to_string())
}

fn join_error(error: tokio::task::JoinError) -> QueryError {
QueryError::message(error.to_string())
}

struct Inserts<'a> {
head: &'a str,
rows: usize,
Expand Down
1 change: 1 addition & 0 deletions crates/barsql-app/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub struct ImportProgress {
pub struct ImportDone {
pub result: Option<ImportResult>,
pub error: String,
pub bug: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down
24 changes: 20 additions & 4 deletions crates/barsql-app/src/import.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::panic::AssertUnwindSafe;
use std::path::Path;
use std::time::{Duration, Instant};

Expand All @@ -12,6 +13,7 @@ use barsql_sql::dml::{
build_batch_insert_with, build_import_create_table, build_truncate, is_bit_column,
};
use barsql_sql::{Dialect, READ_ONLY_ERROR, split_statement_texts};
use futures_util::FutureExt as _;
use serde::{Deserialize, Serialize};

use crate::BarApp;
Expand Down Expand Up @@ -193,17 +195,18 @@ impl BarApp {
self.inner.runtime.spawn(async move {
let emitter = Emitter { events: tx.clone(), total_rows: 0, last: None };
let started = Instant::now();
let outcome = work(app.clone(), emitter, cancel.clone()).await;
let work = AssertUnwindSafe(work(app.clone(), emitter, cancel.clone()));
let outcome = work.catch_unwind().await.unwrap_or_else(|panic| Err(QueryError::from_panic(panic)));
let done = match outcome {
Ok(mut result) => {
result.duration_ms = started.elapsed().as_millis() as i64;
ImportDone { result: Some(result), error: String::new() }
ImportDone { result: Some(result), ..Default::default() }
}
Err(err) if cancel.is_cancelled() || err.cancelled => ImportDone {
result: Some(ImportResult { cancelled: true, ..Default::default() }),
error: String::new(),
..Default::default()
},
Err(err) => ImportDone { result: None, error: err.message },
Err(err) => ImportDone { result: None, error: err.message, bug: err.bug },
};
app.inner.jobs.end(&key, job);
let _ = tx.send(ImportEvent::Done(done)).await;
Expand Down Expand Up @@ -733,6 +736,19 @@ fn mysql_datetime(v: &str) -> String {
mod tests {
use super::*;

async fn boom<T>() -> T {
panic!("boom")
}

#[tokio::test]
async fn an_import_that_panics_ends_with_a_bug() {
let (app, id, _dir) = crate::connections::tests::setup().await;
let handle = app.run_import("import", &id, |_, _, _| boom());
let event = handle.events.recv().await;
assert!(matches!(&event, Ok(ImportEvent::Done(done)) if done.bug && done.result.is_none()), "{event:?}");
assert!(!app.cancel_query("import"), "the import's job ended");
}

#[test]
fn sql_scripts_must_be_utf8_and_lose_their_bom() {
let path = Path::new("dump.sql");
Expand Down
5 changes: 4 additions & 1 deletion crates/barsql-app/src/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::fs::{self, OpenOptions};
use std::io::{self, BufRead, Write};
use std::panic::AssertUnwindSafe;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, mpsc};
use std::time::{Duration, Instant};

use barsql_core::{ConnectionConfig, DriverType, ObjectKind, ObjectRef, QueryError};
use barsql_db::{ColumnMeta, ResultChunk};
use futures_util::FutureExt as _;
use serde_json::{Value, json};
use tokio::sync::{oneshot, watch};

Expand Down Expand Up @@ -162,7 +164,8 @@ impl Server {
let result = if *cancelled.borrow() {
Err(CANCELLED.into())
} else {
self.run(tool, args, &tab, cancelled.clone()).await
let run = AssertUnwindSafe(self.run(tool, args, &tab, cancelled.clone()));
run.catch_unwind().await.unwrap_or_else(|panic| Err(failed(QueryError::from_panic(panic))))
};
// Gives the connection back at once: SQLite has only one.
self.bar.cleanup_tab(&tab).await;
Expand Down
83 changes: 67 additions & 16 deletions crates/barsql-app/src/runs.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::panic::AssertUnwindSafe;
use std::time::Instant;

use barsql_core::clock::now_rfc3339;
Expand All @@ -8,6 +9,7 @@ use barsql_sql::{
ExplainStrategy, QueryPlan, ServerVersion, assert_read_only, build_explain, controls_transactions, first_keyword,
is_read_only, parse_plan, single_statement, split_statement_texts, strip_leading_comments,
};
use futures_util::FutureExt as _;
use serde::Serialize;

use crate::events::{RunEvent, RunHandle, RunResult};
Expand Down Expand Up @@ -95,19 +97,40 @@ impl BarApp {
self.inner.jobs.end(tab_id, stream_id);
return Err(error);
}
let (tx, rx) = async_channel::bounded(RUN_CHANNEL);
let app = self.clone();
let (tab, connection) = (tab_id.to_string(), connection_id.to_string());
self.inner.runtime.spawn(async move {
let done = match app.run_batch(&tab, slot, &connection, &statements, &tx, &cancel).await {
Ok(self.spawn_run(tab_id, stream_id, Some(slot.clone()), move |app, tx| async move {
match app.run_batch(&tab, slot, &connection, &statements, &tx, &cancel).await {
Ok(result_count) => RunEvent::Done { result_count, error: None },
Err(error) => RunEvent::Done { result_count: 0, error: Some(error) },
}
}))
}

// Ends with Done even when `work` panics. A bug can leave the tab's session mid-run, so the session goes too.
fn spawn_run<F, Fut>(&self, tab_id: &str, stream_id: u64, slot: Option<Slot>, work: F) -> RunHandle
where
F: FnOnce(BarApp, async_channel::Sender<RunEvent>) -> Fut + Send + 'static,
Fut: Future<Output = RunEvent> + Send,
{
let (tx, rx) = async_channel::bounded(RUN_CHANNEL);
let app = self.clone();
let tab = tab_id.to_string();
self.inner.runtime.spawn(async move {
let work = AssertUnwindSafe(work(app.clone(), tx.clone()));
let done = match work.catch_unwind().await {
Ok(done) => done,
Err(panic) => RunEvent::Done { result_count: 0, error: Some(QueryError::from_panic(panic)) },
};
if let (RunEvent::Done { error: Some(error), .. }, Some(slot)) = (&done, slot)
&& error.bug
{
app.end_bugged_session(&tab, slot).await;
}
// Ended first, so a cancel after Done finds nothing running.
app.inner.jobs.end(&tab, stream_id);
let _ = tx.send(done).await;
});
Ok(RunHandle { stream_id, events: rx })
RunHandle { stream_id, events: rx }
}

async fn run_batch(
Expand Down Expand Up @@ -200,12 +223,10 @@ impl BarApp {
req.limit = 100;
}
let (stream_id, cancel) = self.inner.jobs.start(tab_id, connection_id);
let (tx, rx) = async_channel::bounded(RUN_CHANNEL);
let app = self.clone();
let (tab, connection) = (tab_id.to_string(), connection_id.to_string());
self.inner.runtime.spawn(async move {
let connection = connection_id.to_string();
Ok(self.spawn_run(tab_id, stream_id, None, move |app, tx| async move {
// So Stop works while the connection is still opening.
let done = match app.engine_unless_cancelled(&connection, &cancel).await {
match app.engine_unless_cancelled(&connection, &cancel).await {
Err(error) => RunEvent::Done { result_count: 0, error: Some(error) },
Ok(engine) => {
if let Err(error) = app.stream_table_page(&engine, &req, &tx, &cancel).await {
Expand All @@ -214,11 +235,8 @@ impl BarApp {
}
RunEvent::Done { result_count: 1, error: None }
}
};
app.inner.jobs.end(&tab, stream_id);
let _ = tx.send(done).await;
});
Ok(RunHandle { stream_id, events: rx })
}
}))
}

async fn stream_table_page(
Expand Down Expand Up @@ -591,9 +609,42 @@ async fn plan_of(

#[cfg(test)]
mod tests {
use barsql_core::{DriverType, QueryError};
use std::time::Duration;

use barsql_core::{DriverType, QueryError, Value};

use super::left_applied;
use crate::connections::tests::setup;
use crate::events::{AppEvent, RunEvent, RunHandle};

async fn boom<T>() -> T {
panic!("boom")
}

async fn drain(run: RunHandle) -> Vec<RunEvent> {
let mut events = Vec::new();
while let Ok(event) = run.events.recv().await {
events.push(event);
}
events
}

#[tokio::test]
async fn a_run_that_panics_ends_with_a_bug_and_rolls_back_its_tab() {
let (app, id, _dir) = setup().await;
let notices = app.events();
app.begin_transaction(&id, "tab").await.unwrap();
drain(app.execute_query_stream(&id, "tab", "CREATE TABLE t (a INTEGER)").await.unwrap()).await;
let (stream_id, _) = app.inner.jobs.start("tab", &id);
let events = drain(app.spawn_run("tab", stream_id, Some(app.inner.tabs.slot("tab")), |_, _| boom())).await;
assert!(matches!(events.as_slice(), [RunEvent::Done { error: Some(error), .. }] if error.bug), "{events:?}");
assert!(!app.cancel_query("tab"), "the run's job ended");
assert!(!app.transaction_status("tab"));
assert_eq!(notices.try_recv(), Ok(AppEvent::TransactionsEnded { tab_ids: vec!["tab".into()] }));
let count = app.execute_query(&id, "SELECT count(*) FROM sqlite_master WHERE name = 't'");
let made = tokio::time::timeout(Duration::from_secs(5), count).await.expect("the tab kept the connection");
assert_eq!(made.unwrap().rows, [[Value::Int(0)]], "the tab's transaction was rolled back");
}

fn left(driver: DriverType, statements: &[&str]) -> String {
let statements: Vec<String> = statements.iter().map(|stmt| stmt.to_string()).collect();
Expand Down
16 changes: 10 additions & 6 deletions crates/barsql-app/src/tabs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

use barsql_core::QueryError;
use barsql_db::{Cancel, Engine, Session};
use barsql_db::{Cancel, Engine, Session, join_error};
use futures_util::future::join_all;
use tokio::sync::OwnedMutexGuard;

use crate::events::AppEvent;
use crate::{BarApp, lock};

// Each open editor tab gets its own session so SET, temp tables and transactions stay with that tab.
Expand Down Expand Up @@ -149,11 +150,7 @@ impl BarApp {
let app = self.clone();
let id = connection_id.to_string();
let connect = self.inner.runtime.spawn(async move { app.engine(&id).await });
unless_cancelled(
async { connect.await.unwrap_or_else(|err| Err(QueryError::message(err.to_string()))) },
cancel,
)
.await
unless_cancelled(async { connect.await.unwrap_or_else(|err| Err(join_error(err))) }, cancel).await
}

// Called when the tab's work is done with the slot.
Expand Down Expand Up @@ -214,6 +211,13 @@ impl BarApp {
self.inner.tabs.mark(tab_id, false);
rolled_back
}

pub(crate) async fn end_bugged_session(&self, tab_id: &str, slot: Slot) {
let mut slot = slot.lock().await;
if self.end_session(tab_id, &mut slot).await {
self.emit(AppEvent::TransactionsEnded { tab_ids: vec![tab_id.to_string()] });
}
}
}

pub(crate) async fn unless_cancelled<T>(
Expand Down
Loading
Loading