Skip to content

Commit

Permalink
#5135 Counting rows in enormous tables (which can take very long time…
Browse files Browse the repository at this point in the history
…) no longer blocks the application.
  • Loading branch information
pawelsalawa committed Nov 29, 2024
1 parent b19c2e7 commit 844901d
Show file tree
Hide file tree
Showing 7 changed files with 1,678 additions and 338 deletions.
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- BUGFIX: #5134 Improved performance significantly when working with big values (~1MB per cell) in multiple columns/rows of a table data grid view.
- BUGFIX: #5122 Fixed Linux packages to run properly under Wayland.
- BUGFIX: #5136 Fixed application hanging at quitting when the database is in WAL journaling mode and there is another client still accessing the database.
- BUGFIX: #5135 Counting rows in enormous tables (which can take very long time) no longer blocks the application.

### 3.4.6
- BUGFIX: #5114 Fixed black highlighting for current query on systems, where SQLiteStudio was never configured to use theme different than default.
Expand Down
3 changes: 2 additions & 1 deletion SQLiteStudio3/coreSQLiteStudio/db/abstractdb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,10 @@ bool AbstractDb::openQuiet()

bool AbstractDb::closeQuiet()
{
QWriteLocker locker(&dbOperLock);
QWriteLocker connectionLocker(&connectionStateLock);
interruptExecution();

QWriteLocker locker(&dbOperLock);
bool res = closeInternal();
clearAttaches();
registeredFunctions.clear();
Expand Down
5 changes: 4 additions & 1 deletion SQLiteStudio3/coreSQLiteStudio/db/abstractdb3.h
Original file line number Diff line number Diff line change
Expand Up @@ -1235,7 +1235,7 @@ int AbstractDb3<T>::Query::fetchNext()
int res;
int secondsSpent = 0;
bool zeroTimeout = flags.testFlag(Db::Flag::ZERO_TIMEOUT);
while ((res = T::step(stmt)) == T::BUSY && !zeroTimeout && secondsSpent < db->getTimeout())
while ((res = T::step(stmt)) == T::BUSY && !zeroTimeout && secondsSpent < db->getTimeout() && !T::is_interrupted(db->dbHandle))
{
QThread::sleep(1);
if (db->getTimeout() >= 0)
Expand All @@ -1250,6 +1250,9 @@ int AbstractDb3<T>::Query::fetchNext()
case T::DONE:
// Empty pointer as no more results are available.
break;
case T::INTERRUPT:
setError(res, QString::fromUtf8(T::errmsg(db->dbHandle)));
return T::INTERRUPT;
default:
setError(res, QString::fromUtf8(T::errmsg(db->dbHandle)));
return T::ERROR;
Expand Down
75 changes: 37 additions & 38 deletions SQLiteStudio3/coreSQLiteStudio/db/queryexecutor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "sqlerrorcodes.h"
#include "services/dbmanager.h"
#include "db/sqlerrorcodes.h"
#include "db/dbsqlite3.h"
#include "services/notifymanager.h"
#include "queryexecutorsteps/queryexecutoraddrowids.h"
#include "queryexecutorsteps/queryexecutorcolumns.h"
Expand All @@ -19,13 +20,13 @@
#include "queryexecutorsteps/queryexecutordetectschemaalter.h"
#include "queryexecutorsteps/queryexecutorvaluesmode.h"
#include "queryexecutorsteps/queryexecutorcolumntype.h"
#include "db/queryexecutorsteps/queryexecutorsmarthints.h"
#include "common/unused.h"
#include "chainexecutor.h"
#include "log.h"
#include "schemaresolver.h"
#include "parser/lexer.h"
#include "common/table.h"
#include "db/queryexecutorsteps/queryexecutorsmarthints.h"
#include <QMutexLocker>
#include <QDateTime>
#include <QThreadPool>
Expand Down Expand Up @@ -56,8 +57,14 @@ QueryExecutor::QueryExecutor(Db* db, const QString& query, QObject *parent) :

QueryExecutor::~QueryExecutor()
{
delete context;
context = nullptr;
safe_delete(context);
if (countingDb)
{
if (countingDb->isOpen())
countingDb->closeQuiet();

delete countingDb;
}
}

void QueryExecutor::setupExecutionChain()
Expand Down Expand Up @@ -249,6 +256,9 @@ void QueryExecutor::exec(Db::QueryResultsHandler resultsHandler)
executionInProgress = true;
executionMutex.unlock();

if (countingDb && countingDb->isOpen())
countingDb->interrupt();

this->resultsHandler = resultsHandler;

if (asyncMode)
Expand Down Expand Up @@ -331,37 +341,32 @@ bool QueryExecutor::countResults()
if (context->countingQuery.isEmpty()) // simple method doesn't provide that
return false;

if (!countingDb)
return false; // no db defined, so no countingDb defined

if (!countingDb->isOpen() && !countingDb->openQuiet())
{
notifyError(tr("An error occured while executing the count(*) query, thus data paging will be disabled. Error details from the database: %1")
.arg("Failed to establish dedicated connection for results counting."));
return false;
}

if (asyncMode)
{
// Start asynchronous results counting query
resultsCountingAsyncId = db->asyncExec(context->countingQuery, context->queryParameters, Db::Flag::NO_LOCK);
countingDb->asyncExec(context->countingQuery, context->queryParameters, [=](SqlQueryPtr results)
{
handleRowCountingResults(results);
}, Db::Flag::PRELOAD);
}
else
{
SqlQueryPtr results = db->exec(context->countingQuery, context->queryParameters, Db::Flag::NO_LOCK);
context->totalRowsReturned = results->getSingleCell().toLongLong();
context->totalPages = (int)qCeil(((double)(context->totalRowsReturned)) / ((double)getResultsPerPage()));

emit resultsCountingFinished(context->rowsAffected, context->totalRowsReturned, context->totalPages);

if (results->isError())
{
notifyError(tr("An error occured while executing the count(*) query, thus data paging will be disabled. Error details from the database: %1")
.arg(results->getErrorText()));
return false;
}
SqlQueryPtr results = countingDb->exec(context->countingQuery, context->queryParameters, Db::Flag::PRELOAD);
handleRowCountingResults(results);
}
return true;
}

void QueryExecutor::dbAsyncExecFinished(quint32 asyncId, SqlQueryPtr results)
{
if (handleRowCountingResults(asyncId, results))
return;

// If this was raised by any other asyncExec, handle it here.
}

qint64 QueryExecutor::getLastExecutionTime() const
{
return context->executionTime;
Expand Down Expand Up @@ -575,25 +580,17 @@ void QueryExecutor::cleanup()
}
}

bool QueryExecutor::handleRowCountingResults(quint32 asyncId, SqlQueryPtr results)
bool QueryExecutor::handleRowCountingResults(SqlQueryPtr results)
{
if (resultsCountingAsyncId == 0)
return false;

if (resultsCountingAsyncId != asyncId)
return false;

if (isExecutionInProgress()) // shouldn't be true, but just in case
return false;

resultsCountingAsyncId = 0;

context->totalRowsReturned = results->getSingleCell().toLongLong();
context->totalPages = (int)qCeil(((double)(context->totalRowsReturned)) / ((double)getResultsPerPage()));

emit resultsCountingFinished(context->rowsAffected, context->totalRowsReturned, context->totalPages);

if (results->isError())
if (results->isError() && results->getErrorCode() != Sqlite3::INTERRUPT)
{
notifyError(tr("An error occured while executing the count(*) query, thus data paging will be disabled. Error details from the database: %1")
.arg(results->getErrorText()));
Expand Down Expand Up @@ -874,13 +871,15 @@ Db* QueryExecutor::getDb() const

void QueryExecutor::setDb(Db* value)
{
if (db)
disconnect(db, SIGNAL(asyncExecFinished(quint32,SqlQueryPtr)), this, SLOT(dbAsyncExecFinished(quint32,SqlQueryPtr)));

db = value;

if (countingDb)
{
countingDb->closeQuiet();
safe_delete(countingDb);
}
if (db)
connect(db, SIGNAL(asyncExecFinished(quint32,SqlQueryPtr)), this, SLOT(dbAsyncExecFinished(quint32,SqlQueryPtr)));
countingDb = db->clone();
}

bool QueryExecutor::getSkipRowCounting() const
Expand Down
21 changes: 4 additions & 17 deletions SQLiteStudio3/coreSQLiteStudio/db/queryexecutor.h
Original file line number Diff line number Diff line change
Expand Up @@ -1228,17 +1228,14 @@ class API_EXPORT QueryExecutor : public QObject, public QRunnable

/**
* @brief Extracts counting query results.
* @param asyncId Asynchronous ID of the counting query execution.
* @param results Results from the counting query execution.
* @return true if passed asyncId is the one for currently running counting query, or false otherwise.
*
* It's called from database asynchronous execution thread. The database might have executed
* some other acynchronous queries too, so this method checks if the asyncId is the expected one.
* @return true if counting was successful, i.e. there is no ongoing query execution, or false otherwise.
*
* It may be called from database asynchronous execution thread.
* Basicly this method is called a result of countResults() call. Extracts counted number of rows
* and stores it in query executor's context.
*/
bool handleRowCountingResults(quint32 asyncId, SqlQueryPtr results);
bool handleRowCountingResults(SqlQueryPtr results);

QStringList applyFiltersAndLimitAndOrderForSimpleMethod(const QStringList &queries);

Expand Down Expand Up @@ -1498,6 +1495,7 @@ class API_EXPORT QueryExecutor : public QObject, public QRunnable

bool forceSimpleMode = false;
ChainExecutor* simpleExecutor = nullptr;
Db* countingDb = nullptr;

signals:
/**
Expand Down Expand Up @@ -1585,17 +1583,6 @@ class API_EXPORT QueryExecutor : public QObject, public QRunnable
* In case of success emits executionFinished(), in case of error emits executionFailed().
*/
void simpleExecutionFinished(SqlQueryPtr results);

/**
* @brief Handles asynchronous database execution results.
* @param asyncId Asynchronous ID of the execution.
* @param results Results from the execution.
*
* QueryExecutor checks whether the \p asyncId belongs to the counting query execution,
* or the simple execution.
* Dispatches query results to a proper handler method.
*/
void dbAsyncExecFinished(quint32 asyncId, SqlQueryPtr results);
};

int qHash(QueryExecutor::EditionForbiddenReason reason);
Expand Down
Loading

0 comments on commit 844901d

Please sign in to comment.