Move SQL execution in Execute SQL tab into a separate thread

This commit is contained in:
Martin Kleusberg
2018-10-14 22:00:29 +02:00
parent 7e0ff3061b
commit 1f9101ae2a
12 changed files with 312 additions and 71 deletions

View File

@@ -30,6 +30,7 @@
#include "CondFormat.h"
#include "RunSql.h"
#include <chrono>
#include <QFile>
#include <QApplication>
#include <QTextStream>
@@ -40,7 +41,6 @@
#include <QDragEnterEvent>
#include <QScrollBar>
#include <QSortFilterProxyModel>
#include <QElapsedTimer>
#include <QMimeData>
#include <QColorDialog>
#include <QDesktopServices>
@@ -113,7 +113,8 @@ MainWindow::MainWindow(QWidget* parent)
plotDock(new PlotDock(this)),
remoteDock(new RemoteDock(this)),
findReplaceDialog(new FindReplaceDialog(this)),
gotoValidator(new QIntValidator(0, 0, this))
gotoValidator(new QIntValidator(0, 0, this)),
execute_sql_worker(nullptr)
{
ui->setupUi(this);
init();
@@ -144,8 +145,8 @@ void MainWindow::init()
#endif
// Connect SQL logging and database state setting to main window
connect(&db, SIGNAL(dbChanged(bool)), this, SLOT(dbState(bool)));
connect(&db, SIGNAL(sqlExecuted(QString, int)), this, SLOT(logSql(QString,int)));
connect(&db, &DBBrowserDB::dbChanged, this, &MainWindow::dbState, Qt::QueuedConnection);
connect(&db, &DBBrowserDB::sqlExecuted, this, &MainWindow::logSql, Qt::QueuedConnection);
connect(&db, &DBBrowserDB::requestCollation, this, &MainWindow::requestCollation);
// Set the validator for the goto line edit
@@ -162,11 +163,11 @@ void MainWindow::init()
// Set up DB structure tab
dbStructureModel = new DbStructureModel(db, this);
connect(&db, &DBBrowserDB::structureUpdated, [this]() {
connect(&db, &DBBrowserDB::structureUpdated, this, [this]() {
QString old_table = ui->comboBrowseTable->currentText();
dbStructureModel->reloadData();
populateStructure(old_table);
});
}, Qt::QueuedConnection);
ui->dbTreeWidget->setModel(dbStructureModel);
ui->dbTreeWidget->setColumnWidth(DbStructureModel::ColumnName, 300);
ui->dbTreeWidget->setColumnHidden(DbStructureModel::ColumnObjectType, true);
@@ -389,6 +390,11 @@ void MainWindow::init()
plotDock->updatePlot(m_browseTableModel, &settings, true, false);
});
connect(ui->actionSqlStop, &QAction::triggered, [this]() {
if(execute_sql_worker && execute_sql_worker->isRunning())
execute_sql_worker->stop();
});
// Lambda function for keyboard shortcuts for selecting next/previous table in Browse Data tab
connect(ui->dataTable, &ExtendedTableWidget::switchTable, [this](bool next) {
int index = ui->comboBrowseTable->currentIndex();
@@ -797,6 +803,19 @@ void MainWindow::applyBrowseTableSettings(BrowseDataTableSettings storedData, bo
bool MainWindow::fileClose()
{
// Stop any running SQL statements before closing the database
if(execute_sql_worker && execute_sql_worker->isRunning())
{
if(QMessageBox::warning(this, qApp->applicationName(),
tr("You are still executing SQL statements. When closing the database now the execution will be stopped. maybe "
"leaving the database in an incosistent state. Are you sure you want to close the database?"),
QMessageBox::Yes, QMessageBox::Cancel | QMessageBox::Default | QMessageBox::Escape) == QMessageBox::Cancel)
return false;
execute_sql_worker->stop();
execute_sql_worker->wait();
}
// Close the database but stop the closing process here if the user pressed the cancel button in there
if(!db.close())
return false;
@@ -1237,9 +1256,25 @@ void MainWindow::executeQuery()
if(!db.isOpen())
return;
// Check if other task is still running and stop it if necessary
if(execute_sql_worker && execute_sql_worker->isRunning())
{
// Ask the user and do nothing if he/she doesn't want to interrupt the running query
if(QMessageBox::warning(this, qApp->applicationName(),
tr("You are already executing SQL statements. Do you want to stop them in order to execute the current "
"statements instead? Note that this might leave the database in an inconsistent state."),
QMessageBox::Yes, QMessageBox::Cancel | QMessageBox::Default | QMessageBox::Escape) == QMessageBox::Cancel)
return;
// Stop the running query
execute_sql_worker->stop();
execute_sql_worker->wait();
}
// Get current SQL tab and editor
SqlExecutionArea* sqlWidget = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->currentWidget());
SqlTextEdit* editor = sqlWidget->getEditor();
auto* current_tab = ui->tabSqlAreas->currentWidget();
const QString tabName = ui->tabSqlAreas->tabText(ui->tabSqlAreas->currentIndex()).remove('&');
// Remove any error indicators
@@ -1339,23 +1374,30 @@ void MainWindow::executeQuery()
sqlWidget->finishExecution(log_message, ok);
};
// Run the query
RunSql r(db);
connect(&r, &RunSql::statementErrored, [query_logger, this, sqlWidget](const QString& status_message, int from_position, int to_position) {
// Prepare the SQL worker to run the query. We set the context of each signal-slot connection to the current SQL execution area.
// This means that if the tab is closed all these signals are automatically disconnected so the lambdas won't be called for a not
// existing execution area.
execute_sql_worker.reset(new RunSql(db, sqlWidget->getSql(), execute_from_position, execute_to_position, true));
connect(execute_sql_worker.get(), &RunSql::statementErrored, sqlWidget, [query_logger, this, sqlWidget](const QString& status_message, int from_position, int to_position) {
sqlWidget->getModel()->reset();
ui->actionSqlResultsSave->setEnabled(false);
ui->actionSqlResultsSaveAsView->setEnabled(false);
attachPlot(sqlWidget->getTableResult(), sqlWidget->getModel());
query_logger(false, status_message, from_position, to_position);
});
connect(&r, &RunSql::statementExecuted, [query_logger, this, sqlWidget](const QString& status_message, int from_position, int to_position) {
}, Qt::QueuedConnection);
connect(execute_sql_worker.get(), &RunSql::statementExecuted, sqlWidget, [query_logger, this, sqlWidget](const QString& status_message, int from_position, int to_position) {
sqlWidget->getModel()->reset();
ui->actionSqlResultsSave->setEnabled(false);
ui->actionSqlResultsSaveAsView->setEnabled(false);
attachPlot(sqlWidget->getTableResult(), sqlWidget->getModel());
query_logger(true, status_message, from_position, to_position);
});
connect(&r, &RunSql::statementReturnsRows, [query_logger, this, sqlWidget](const QString& query, int from_position, int to_position, qint64 time_in_ms) {
QElapsedTimer timer;
timer.start();
execute_sql_worker->startNextStatement();
}, Qt::QueuedConnection);
connect(execute_sql_worker.get(), &RunSql::statementReturnsRows, sqlWidget, [query_logger, this, sqlWidget](const QString& query, int from_position, int to_position, qint64 time_in_ms_so_far) {
auto time_start = std::chrono::high_resolution_clock::now();
ui->actionSqlResultsSave->setEnabled(true);
ui->actionSqlResultsSaveAsView->setEnabled(!db.readOnly());
@@ -1363,20 +1405,62 @@ void MainWindow::executeQuery()
auto * model = sqlWidget->getModel();
model->setQuery(query);
// Wait until the initial loading of data (= first chunk and row count) has been performed. I have the
// feeling that a lot of stuff would need rewriting if we wanted to become more asynchronous here:
// essentially the entire loop over the commands would need to be signal-driven.
model->waitUntilIdle();
qApp->processEvents(); // to make row count available
// Wait until the initial loading of data (= first chunk and row count) has been performed
auto conn = std::make_shared<QMetaObject::Connection>();
*conn = connect(model, &SqliteTableModel::finishedFetch, [=]() {
// Disconnect this connection right now. This avoids calling this slot multiple times
disconnect(*conn);
attachPlot(sqlWidget->getTableResult(), sqlWidget->getModel());
connect(sqlWidget->getTableResult(), &ExtendedTableWidget::activated, this, &MainWindow::dataTableSelectionChanged);
connect(sqlWidget->getTableResult(), &QTableView::doubleClicked, this, &MainWindow::doubleClickTable);
attachPlot(sqlWidget->getTableResult(), sqlWidget->getModel());
connect(sqlWidget->getTableResult(), &ExtendedTableWidget::activated, this, &MainWindow::dataTableSelectionChanged);
connect(sqlWidget->getTableResult(), &QTableView::doubleClicked, this, &MainWindow::doubleClickTable);
query_logger(true, tr("%1 rows returned in %2ms").arg(model->rowCount()).arg(time_in_ms+timer.elapsed()), from_position, to_position);
auto time_end = std::chrono::high_resolution_clock::now();
auto time_in_ms = std::chrono::duration_cast<std::chrono::milliseconds>(time_end-time_start);
query_logger(true, tr("%1 rows returned in %2ms").arg(model->rowCount()).arg(time_in_ms.count()+time_in_ms_so_far), from_position, to_position);
execute_sql_worker->startNextStatement();
});
}, Qt::QueuedConnection);
connect(execute_sql_worker.get(), &RunSql::confirmSaveBeforePragmaOrVacuum, sqlWidget, [this]() {
if(QMessageBox::question(nullptr, QApplication::applicationName(),
tr("Setting PRAGMA values or vacuuming will commit your current transaction.\nAre you sure?"),
QMessageBox::Yes | QMessageBox::Default,
QMessageBox::No | QMessageBox::Escape) == QMessageBox::No)
execute_sql_worker->stop();
}, Qt::BlockingQueuedConnection);
connect(execute_sql_worker.get(), &RunSql::finished, sqlWidget, [this, current_tab, sqlWidget]() {
// We work with a pointer to the current tab here instead of its index because the user might reorder the tabs in the meantime
ui->tabSqlAreas->setTabIcon(ui->tabSqlAreas->indexOf(current_tab), QIcon());
// We don't need to check for the current SQL tab here because two concurrently running queries are not allowed
ui->actionSqlExecuteLine->setEnabled(true);
ui->actionExecuteSql->setEnabled(true);
ui->actionSqlStop->setEnabled(false);
sqlWidget->getEditor()->setReadOnly(false);
// Show Done message
if(sqlWidget->inErrorState())
sqlWidget->getStatusEdit()->setPlainText(tr("Execution finished with errors.") + "\n\n" + sqlWidget->getStatusEdit()->toPlainText());
else
sqlWidget->getStatusEdit()->setPlainText(tr("Execution finished without errors.") + "\n\n" + sqlWidget->getStatusEdit()->toPlainText());
});
r.runStatements(sqlWidget->getSql(), execute_from_position, execute_to_position);
// Add an hourglass icon to the current tab to indicate that there's a running execution in there.
// NOTE It's a bit hack-ish but we don't use this icon just as a signal to the user but also check for it in various places to check whether a
// specific SQL tab is currently running a query or not.
ui->tabSqlAreas->setTabIcon(ui->tabSqlAreas->currentIndex(), QIcon(":icons/hourglass"));
// Deactivate the buttons to start a query and activate the button to stop the query
ui->actionSqlExecuteLine->setEnabled(false);
ui->actionExecuteSql->setEnabled(false);
ui->actionSqlStop->setEnabled(true);
// Make the SQL editor widget read-only. We do this because the error indicators would be misplaced if the user changed the SQL text during execution
sqlWidget->getEditor()->setReadOnly(true);
// Start the execution
execute_sql_worker->start();
}
void MainWindow::mainTabSelected(int tabindex)
@@ -1976,6 +2060,20 @@ void MainWindow::closeSqlTab(int index, bool force)
if(ui->tabSqlAreas->count() == 1 && !force)
return;
// Check if we're still executing statements from this tab and stop them before proceeding
if(!ui->tabSqlAreas->tabIcon(index).isNull())
{
if(QMessageBox::warning(this, qApp->applicationName(), tr("The statements in this tab are still executing. Closing the tab will stop the "
"execution. This might leave the database in an inconsistent state. Are you sure "
"you want to close the tab?"),
QMessageBox::Yes,
QMessageBox::Cancel | QMessageBox::Default | QMessageBox::Escape) == QMessageBox::Cancel)
return;
execute_sql_worker->stop();
execute_sql_worker->wait();
}
// Remove the tab and delete the widget
QWidget* w = ui->tabSqlAreas->widget(index);
ui->tabSqlAreas->removeTab(index);
@@ -2000,11 +2098,27 @@ unsigned int MainWindow::openSqlTab(bool resetCounter)
return index;
}
void MainWindow::changeSqlTab(int /*index*/)
void MainWindow::changeSqlTab(int index)
{
// Instead of figuring out if there are some execution results in the new tab and which statement was used to generate them,
// we just disable the export buttons in the toolbar.
ui->actionSqlResultsSave->setEnabled(false);
// Check if the new tab is currently running a query or not
if(ui->tabSqlAreas->tabIcon(index).isNull())
{
// Not running a query
ui->actionSqlExecuteLine->setEnabled(true);
ui->actionExecuteSql->setEnabled(true);
ui->actionSqlStop->setEnabled(false);
} else {
// Running a query
ui->actionSqlExecuteLine->setEnabled(false);
ui->actionExecuteSql->setEnabled(false);
ui->actionSqlStop->setEnabled(true);
}
}
void MainWindow::openSqlFile()

View File

@@ -6,7 +6,9 @@
#include "Palette.h"
#include "CondFormat.h"
#include "sql/Query.h"
#include "RunSql.h"
#include <memory>
#include <QMainWindow>
#include <QMap>
@@ -167,6 +169,8 @@ private:
Palette m_condFormatPalette;
std::unique_ptr<RunSql> execute_sql_worker;
void init();
void clearCompleterModelsFields();

View File

@@ -941,6 +941,7 @@ You can drag SQL statements from an object row and drop them into other applicat
<addaction name="separator"/>
<addaction name="actionExecuteSql"/>
<addaction name="actionSqlExecuteLine"/>
<addaction name="actionSqlStop"/>
<addaction name="separator"/>
<addaction name="actionSqlResultsSave"/>
<addaction name="separator"/>
@@ -2384,6 +2385,18 @@ You can drag SQL statements from the Schema column and drop them into the SQL ed
<string>Ctrl+Shift+/</string>
</property>
</action>
<action name="actionSqlStop">
<property name="icon">
<iconset resource="icons/icons.qrc">
<normaloff>:/icons/cancel</normaloff>:/icons/cancel</iconset>
</property>
<property name="text">
<string>Stop SQL execution</string>
</property>
<property name="toolTip">
<string>Stop the currently running SQL script</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View File

@@ -3,23 +3,23 @@
#include "sqlitedb.h"
#include "sqlitetablemodel.h"
#include <chrono>
#include <QApplication>
#include <QElapsedTimer>
#include <QMessageBox>
RunSql::RunSql(DBBrowserDB& _db) :
db(_db)
RunSql::RunSql(DBBrowserDB& _db, QString query, int execute_from_position, int _execute_to_position, bool _interrupt_after_statements) :
db(_db),
may_continue_with_execution(true),
interrupt_after_statements(_interrupt_after_statements),
execute_current_position(execute_from_position),
execute_to_position(_execute_to_position),
structure_updated(false),
savepoint_created(false),
was_dirty(db.getDirty()),
modified(false)
{
}
void RunSql::runStatements(QString query, int execute_from_position, int _execute_to_position)
{
// Save data for this task
execute_current_position = execute_from_position;
execute_to_position = _execute_to_position;
structure_updated = false;
savepoint_created = false;
was_dirty = db.getDirty();
// Get lock to set up everything
std::unique_lock<std::mutex> lk(m);
// Cancel if there is nothing to execute
if(query.trimmed().isEmpty() || query.trimmed() == ";" || execute_from_position == execute_to_position ||
@@ -37,23 +37,46 @@ void RunSql::runStatements(QString query, int execute_from_position, int _execut
// until the end of the SQL code. By doing so we go further than the determined end position because in Line
// mode the last statement might go beyond that point.
queries_left_to_execute = query.toUtf8().mid(execute_from_position);
}
// Set cursor
QApplication::setOverrideCursor(Qt::BusyCursor);
void RunSql::run()
{
// Execute statement by statement
for(;;)
{
if(!executeNextStatement())
break;
}
// Execute each statement until there is nothing left to do
while(executeNextStatement())
;
// Execution finished
// If the DB structure was changed by some command in this SQL script, update our schema representations
if(structure_updated)
db.updateSchema();
}
QApplication::restoreOverrideCursor();
void RunSql::startNextStatement()
{
std::unique_lock<std::mutex> lk(m);
may_continue_with_execution = true;
cv.notify_one();
}
void RunSql::stop()
{
std::unique_lock<std::mutex> lk(m);
stopExecution();
if(pDb)
sqlite3_interrupt(pDb.get());
may_continue_with_execution = true;
cv.notify_all();
}
bool RunSql::executeNextStatement()
{
std::unique_lock<std::mutex> lk(m);
// Is there anything left to do?
if(queries_left_to_execute.isEmpty())
return false;
@@ -84,11 +107,11 @@ bool RunSql::executeNextStatement()
// user about that
if(db.getDirty())
{
if(QMessageBox::question(nullptr,
QApplication::applicationName(),
tr("Setting PRAGMA values or vacuuming will commit your current transaction.\nAre you sure?"),
QMessageBox::Yes | QMessageBox::Default,
QMessageBox::No | QMessageBox::Escape) == QMessageBox::Yes)
lk.unlock();
// Ask user, then check if we should abort execution or continue with it. We depend on a BlockingQueueConnection here which makes sure to
// block this worker thread until the slot function in the main thread is completed and could tell us about its decision.
emit confirmSaveBeforePragmaOrVacuum();
if(!queries_left_to_execute.isEmpty())
{
// Commit all changes
db.releaseAllSavepoints();
@@ -97,6 +120,7 @@ bool RunSql::executeNextStatement()
emit statementErrored(tr("Execution aborted by user"), execute_current_position, execute_current_position + (query_type == PragmaStatement ? 5 : 6));
return false;
}
lk.lock();
}
} else {
// We're not trying to set a pragma or to vacuum the database. In this case make sure a savepoint has been created in order to avoid committing
@@ -113,24 +137,25 @@ bool RunSql::executeNextStatement()
// Start execution timer. We do that after opening any message boxes and after creating savepoints because both are not part of the actual
// query execution.
QElapsedTimer timer;
timer.start();
auto time_start = std::chrono::high_resolution_clock::now();
// Execute next statement
const char* tail = queries_left_to_execute.data();
int tail_length = queries_left_to_execute.length();
lk.unlock();
const char* qbegin = tail;
auto pDb = db.get(tr("executing query"));
acquireDbAccess();
sqlite3_stmt* vm;
int sql3status = sqlite3_prepare_v2(pDb.get(), tail, tail_length, &vm, &tail);
QString queryPart = QString::fromUtf8(qbegin, tail - qbegin);
int tail_length_before = tail_length;
tail_length -= (tail - qbegin);
int end_of_current_statement_position = execute_current_position + tail_length_before - tail_length;
bool modified = false;
// Save remaining statements
lk.lock();
queries_left_to_execute = QByteArray(tail);
lk.unlock();
if (sql3status == SQLITE_OK)
{
@@ -152,9 +177,20 @@ bool RunSql::executeNextStatement()
{
// If we get here, the SQL statement returns some sort of data. So hand it over to the model for display. Don't set the modified flag
// because statements that display data don't change data as well.
pDb = nullptr;
emit statementReturnsRows(queryPart, execute_current_position, end_of_current_statement_position, timer.elapsed());
releaseDbAccess();
lk.lock();
may_continue_with_execution = false;
auto time_end = std::chrono::high_resolution_clock::now();
auto time_in_ms = std::chrono::duration_cast<std::chrono::milliseconds>(time_end - time_start);
emit statementReturnsRows(queryPart, execute_current_position, end_of_current_statement_position, time_in_ms.count());
// Make sure the next statement isn't executed until we're told to do so
if(interrupt_after_statements)
cv.wait(lk, [this](){ return may_continue_with_execution; });
lk.unlock();
break;
}
case SQLITE_DONE:
@@ -167,29 +203,47 @@ bool RunSql::executeNextStatement()
if(query_part_type == InsertStatement || query_part_type == UpdateStatement || query_part_type == DeleteStatement)
stmtHasChangedDatabase = tr(", %1 rows affected").arg(sqlite3_changes(pDb.get()));
releaseDbAccess();
lk.lock();
// Attach/Detach statements don't modify the original database
if(query_part_type != StatementType::AttachStatement && query_part_type != StatementType::DetachStatement)
modified = true;
emit statementExecuted(tr("query executed successfully. Took %1ms%2").arg(timer.elapsed()).arg(stmtHasChangedDatabase),
may_continue_with_execution = false;
auto time_end = std::chrono::high_resolution_clock::now();
auto time_in_ms = std::chrono::duration_cast<std::chrono::milliseconds>(time_end - time_start);
emit statementExecuted(tr("query executed successfully. Took %1ms%2").arg(time_in_ms.count()).arg(stmtHasChangedDatabase),
execute_current_position, end_of_current_statement_position);
// Make sure the next statement isn't executed until we're told to do so
if(interrupt_after_statements)
cv.wait(lk, [this](){ return may_continue_with_execution; });
lk.unlock();
break;
}
case SQLITE_MISUSE:
break;
default:
emit statementErrored(QString::fromUtf8(sqlite3_errmsg(pDb.get())), execute_current_position, end_of_current_statement_position);
QString error = QString::fromUtf8(sqlite3_errmsg(pDb.get()));
releaseDbAccess();
emit statementErrored(error, execute_current_position, end_of_current_statement_position);
stopExecution();
return false;
}
} else {
emit statementErrored(QString::fromUtf8(sqlite3_errmsg(pDb.get())), execute_current_position, end_of_current_statement_position);
QString error = QString::fromUtf8(sqlite3_errmsg(pDb.get()));
releaseDbAccess();
emit statementErrored(error, execute_current_position, end_of_current_statement_position);
stopExecution();
return false;
}
// Release the database
pDb = nullptr;
lk.lock();
releaseDbAccess();
// Revert to save point now if it wasn't needed. We need to do this here because there are some rare cases where the next statement might
// be affected by what is only a temporary and unnecessary savepoint. For example in this case:
@@ -211,9 +265,6 @@ bool RunSql::executeNextStatement()
return false;
}
// Process events to keep the UI responsive
qApp->processEvents();
return true;
}
@@ -241,3 +292,13 @@ RunSql::StatementType RunSql::getQueryType(const QString& query)
return OtherStatement;
}
void RunSql::acquireDbAccess()
{
pDb = db.get(tr("executing query"), true);
}
void RunSql::releaseDbAccess()
{
pDb = nullptr;
}

View File

@@ -1,16 +1,22 @@
#ifndef RUNSQL_H
#define RUNSQL_H
#include <QObject>
#include <memory>
#include <mutex>
#include <condition_variable>
#include <QThread>
class DBBrowserDB;
struct sqlite3;
class RunSql : public QObject
class RunSql : public QThread
{
Q_OBJECT
void run() override;
public:
RunSql(DBBrowserDB& _db);
RunSql(DBBrowserDB& db, QString query, int execute_from_position, int execute_to_position, bool interrupt_after_statements = false);
~RunSql() override = default;
enum StatementType
@@ -32,15 +38,29 @@ public:
static StatementType getQueryType(const QString& query);
void runStatements(QString _query, int execute_from_position, int _execute_to_position);
void startNextStatement();
void stop();
signals:
void statementErrored(QString message, int from_position, int to_position);
void statementExecuted(QString message, int from_position, int to_position);
void statementReturnsRows(QString query, int from_position, int to_position, qint64 time_in_ms);
/**
* This signal must be connected with a Qt::BlockingQueuedConnection in order to work as expected!
*/
void confirmSaveBeforePragmaOrVacuum();
private:
DBBrowserDB& db;
std::shared_ptr<sqlite3> pDb;
mutable std::mutex m;
mutable std::condition_variable cv;
bool may_continue_with_execution;
bool interrupt_after_statements;
QByteArray queries_left_to_execute;
int execute_current_position;
@@ -48,9 +68,13 @@ private:
bool structure_updated;
bool savepoint_created;
bool was_dirty;
bool modified;
void stopExecution();
bool executeNextStatement();
void acquireDbAccess();
void releaseDbAccess();
};
#endif

View File

@@ -14,7 +14,8 @@ SqlExecutionArea::SqlExecutionArea(DBBrowserDB& _db, QWidget* parent) :
QWidget(parent),
db(_db),
ui(new Ui::SqlExecutionArea),
m_columnsResized(false)
m_columnsResized(false),
error_state(false)
{
// Create UI
ui->setupUi(this);
@@ -57,6 +58,7 @@ QString SqlExecutionArea::getSelectedSql() const
void SqlExecutionArea::finishExecution(const QString& result, const bool ok)
{
error_state = !ok;
m_columnsResized = false;
ui->editErrors->setPlainText(result);
// Set reddish background when not ok
@@ -97,6 +99,11 @@ ExtendedTableWidget *SqlExecutionArea::getTableResult()
return ui->tableResult;
}
QTextEdit* SqlExecutionArea::getStatusEdit()
{
return ui->editErrors;
}
void SqlExecutionArea::saveAsCsv()
{
ExportDataDialog dialog(db, ExportDataDialog::ExportFormatCsv, this, model->query());

View File

@@ -8,6 +8,8 @@ class SqliteTableModel;
class DBBrowserDB;
class ExtendedTableWidget;
class QTextEdit;
namespace Ui {
class SqlExecutionArea;
}
@@ -29,6 +31,9 @@ public:
SqliteTableModel* getModel() { return model; }
SqlTextEdit* getEditor();
ExtendedTableWidget *getTableResult();
QTextEdit* getStatusEdit();
bool inErrorState() const { return error_state; }
public slots:
void finishExecution(const QString& result, const bool ok);
@@ -54,6 +59,7 @@ private:
Ui::SqlExecutionArea* ui;
bool m_columnsResized; // This is set to true if the columns of the table view were already adjusted to fit their contents
bool showErrorIndicators;
bool error_state;
};
#endif

BIN
src/icons/hourglass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

View File

@@ -15,6 +15,7 @@
#include <QDir>
#include <QDateTime>
#include <QDebug>
#include <QThread>
#include <functional>
#include <atomic>
#include <algorithm>
@@ -599,12 +600,12 @@ bool DBBrowserDB::close()
return true;
}
DBBrowserDB::db_pointer_type DBBrowserDB::get(QString user)
DBBrowserDB::db_pointer_type DBBrowserDB::get(QString user, bool force_wait)
{
if(!_db)
return nullptr;
waitForDbRelease();
waitForDbRelease(force_wait ? Wait : Ask);
db_user = user;
db_used = true;
@@ -618,6 +619,11 @@ void DBBrowserDB::waitForDbRelease(ChoiceOnUse choice)
if(!_db)
return;
// We can't show a message box from another thread than the main thread. So instead of crashing we
// just decide that we don't interrupt any running query in this case.
if(choice == Ask && QThread::currentThread() != QApplication::instance()->thread())
choice = Wait;
std::unique_lock<std::mutex> lk(m);
while(db_used) {
// notify user, give him the opportunity to cancel that

View File

@@ -82,10 +82,13 @@ public:
\param user a string that identifies the new user, and which
can be displayed in the dialog box.
\param force_wait if set to true we won't ask the user to cancel
the running query but just wait until it is done.
\returns a unique_ptr containing the SQLite database handle, or
nullptr in case no database is open.
**/
db_pointer_type get (QString user);
db_pointer_type get (QString user, bool force_wait = false);
bool setSavepoint(const QString& pointname = "RESTOREPOINT");
bool releaseSavepoint(const QString& pointname = "RESTOREPOINT");

View File

@@ -91,6 +91,8 @@ void SqliteTableModel::handleRowCountComplete (int life_id, int num_rows)
m_rowCountAvailable = RowCount::Complete;
handleFinishedFetch(life_id, num_rows, num_rows);
emit finishedRowCount();
}
void SqliteTableModel::reset()

View File

@@ -118,6 +118,7 @@ public slots:
signals:
void finishedFetch(int fetched_row_begin, int fetched_row_end);
void finishedRowCount();
protected:
Qt::DropActions supportedDropActions() const override;