From bf7df6d2e38babc9699f794832a8366a2d147edb Mon Sep 17 00:00:00 2001 From: Blottiere Paul Date: Mon, 22 Jan 2018 10:26:31 +0000 Subject: [PATCH] Add a cancel button for Postgis and Spatialite --- .../db_manager/db_plugins/connector.py | 3 + .../db_manager/db_plugins/data_model.py | 40 ++++++++- .../plugins/db_manager/db_plugins/plugin.py | 5 ++ .../db_plugins/postgis/connector.py | 4 + .../db_plugins/postgis/data_model.py | 41 ++++++++- .../db_manager/db_plugins/postgis/plugin.py | 5 ++ .../db_plugins/spatialite/connector.py | 6 ++ .../db_plugins/spatialite/data_model.py | 47 +++++++++- .../db_plugins/spatialite/plugin.py | 5 ++ .../db_manager/dlg_cancel_task_query.py | 66 ++++++++++++++ python/plugins/db_manager/dlg_sql_window.py | 71 +++++++++------ .../db_manager/ui/DlgCancelTaskQuery.ui | 86 +++++++++++++++++++ 12 files changed, 346 insertions(+), 33 deletions(-) create mode 100644 python/plugins/db_manager/dlg_cancel_task_query.py create mode 100644 python/plugins/db_manager/ui/DlgCancelTaskQuery.ui diff --git a/python/plugins/db_manager/db_plugins/connector.py b/python/plugins/db_manager/db_plugins/connector.py index 3eff8e1229a1..c59bcaa04f31 100644 --- a/python/plugins/db_manager/db_plugins/connector.py +++ b/python/plugins/db_manager/db_plugins/connector.py @@ -42,6 +42,9 @@ def __del__(self): def uri(self): return QgsDataSourceUri(self._uri.uri(False)) + def cancel(self): + pass + def publicUri(self): publicUri = QgsDataSourceUri.removePassword(self._uri.uri(False)) return QgsDataSourceUri(publicUri) diff --git a/python/plugins/db_manager/db_plugins/data_model.py b/python/plugins/db_manager/db_plugins/data_model.py index fa08661f5024..9fbb480683a6 100644 --- a/python/plugins/db_manager/db_plugins/data_model.py +++ b/python/plugins/db_manager/db_plugins/data_model.py @@ -22,11 +22,18 @@ from builtins import str from builtins import range -from qgis.PyQt.QtCore import Qt, QTime, QRegExp, QAbstractTableModel -from qgis.PyQt.QtGui import QFont, QStandardItemModel, QStandardItem +from qgis.PyQt.QtCore import (Qt, + QTime, + QRegExp, + QAbstractTableModel, + pyqtSignal, + QObject) +from qgis.PyQt.QtGui import (QFont, + QStandardItemModel, + QStandardItem) from qgis.PyQt.QtWidgets import QApplication -from .plugin import DbError +from .plugin import DbError, BaseError class BaseTableModel(QAbstractTableModel): @@ -139,6 +146,33 @@ def rowCount(self, index=None): return self.table.rowCount if self.table.rowCount is not None and self.columnCount(index) > 0 else 0 +class SqlResultModelAsync(QObject): + + done = pyqtSignal() + + def __init__(self, db, sql, parent=None): + QObject.__init__(self) + self.db = db + self.sql = sql + self.parent = parent + self.error = BaseError('') + self.status = None + self.model = None + self.task = None + + def cancel(self): + if self.task: + self.task.cancelQuery() + + def modelDone(self): + if self.task: + self.status = self.task.status + self.model = self.task.model + self.error = self.task.error + + self.done.emit() + + class SqlResultModel(BaseTableModel): def __init__(self, db, sql, parent=None): diff --git a/python/plugins/db_manager/db_plugins/plugin.py b/python/plugins/db_manager/db_plugins/plugin.py index 406db7ebed8c..10342772555f 100644 --- a/python/plugins/db_manager/db_plugins/plugin.py +++ b/python/plugins/db_manager/db_plugins/plugin.py @@ -256,6 +256,11 @@ def sqlResultModel(self, sql, parent): return SqlResultModel(self, sql, parent) + def sqlResultModelAsync(self, sql, parent): + from .data_model import SqlResultModelAsync + + return SqlResultModelAsync(self, sql, parent) + def columnUniqueValuesModel(self, col, table, limit=10): l = "" if limit is not None: diff --git a/python/plugins/db_manager/db_plugins/postgis/connector.py b/python/plugins/db_manager/db_plugins/postgis/connector.py index a0d5bf97037a..db8e4abf19a0 100644 --- a/python/plugins/db_manager/db_plugins/postgis/connector.py +++ b/python/plugins/db_manager/db_plugins/postgis/connector.py @@ -186,6 +186,10 @@ def _checkRasterColumnsTable(self): self.has_raster_columns_access = self.getTablePrivileges('raster_columns')[0] return self.has_raster_columns + def cancel(self): + if self.connection: + self.connection.cancel() + def getInfo(self): c = self._execute(None, u"SELECT version()") res = self._fetchone(c) diff --git a/python/plugins/db_manager/db_plugins/postgis/data_model.py b/python/plugins/db_manager/db_plugins/postgis/data_model.py index 73a07638c0db..f8865f2cb7e8 100644 --- a/python/plugins/db_manager/db_plugins/postgis/data_model.py +++ b/python/plugins/db_manager/db_plugins/postgis/data_model.py @@ -20,8 +20,9 @@ ***************************************************************************/ """ - -from ..data_model import TableDataModel, SqlResultModel +from qgis.core import QgsTask +from ..plugin import BaseError +from ..data_model import TableDataModel, SqlResultModel, SqlResultModelAsync class PGTableDataModel(TableDataModel): @@ -79,5 +80,41 @@ def fetchMoreData(self, row_start): self.fetchedFrom = row_start +class PGSqlResultModelTask(QgsTask): + + def __init__(self, db, sql, parent): + QgsTask.__init__(self) + self.db = db + self.sql = sql + self.parent = parent + self.error = BaseError('') + self.model = None + + def run(self): + + try: + self.model = PGSqlResultModel(self.db, self.sql, self.parent) + except BaseError as e: + self.error = e + QgsMessageLog.logMessage(e.msg) + return False + + return True + + def cancelQuery(self): + self.db.connector.cancel() + self.cancel() + + +class PGSqlResultModelAsync(SqlResultModelAsync): + + def __init__(self, db, sql, parent): + SqlResultModelAsync.__init__(self, db, sql, parent) + + self.task = PGSqlResultModelTask(db, sql, parent) + self.task.taskCompleted.connect(self.modelDone) + self.task.taskTerminated.connect(self.modelDone) + + class PGSqlResultModel(SqlResultModel): pass diff --git a/python/plugins/db_manager/db_plugins/postgis/plugin.py b/python/plugins/db_manager/db_plugins/postgis/plugin.py index 56a8505fcf2c..ccfcf9f8ba57 100644 --- a/python/plugins/db_manager/db_plugins/postgis/plugin.py +++ b/python/plugins/db_manager/db_plugins/postgis/plugin.py @@ -134,6 +134,11 @@ def sqlResultModel(self, sql, parent): return PGSqlResultModel(self, sql, parent) + def sqlResultModelAsync(self, sql, parent): + from .data_model import PGSqlResultModelAsync + + return PGSqlResultModelAsync(self, sql, parent) + def registerDatabaseActions(self, mainWindow): Database.registerDatabaseActions(self, mainWindow) diff --git a/python/plugins/db_manager/db_plugins/spatialite/connector.py b/python/plugins/db_manager/db_plugins/spatialite/connector.py index 4de742086916..aaff36babcad 100644 --- a/python/plugins/db_manager/db_plugins/spatialite/connector.py +++ b/python/plugins/db_manager/db_plugins/spatialite/connector.py @@ -59,6 +59,12 @@ def __init__(self, uri): def _connectionInfo(self): return str(self.dbname) + def cancel(self): + # https://www.sqlite.org/c3ref/interrupt.html + # This function causes any pending database operation to abort and return at its earliest opportunity. + if self.connection: + self.connection.interrupt() + @classmethod def isValidDatabase(self, path): if not QFile.exists(path): diff --git a/python/plugins/db_manager/db_plugins/spatialite/data_model.py b/python/plugins/db_manager/db_plugins/spatialite/data_model.py index 5af518af6940..fc01607980d7 100644 --- a/python/plugins/db_manager/db_plugins/spatialite/data_model.py +++ b/python/plugins/db_manager/db_plugins/spatialite/data_model.py @@ -20,7 +20,10 @@ ***************************************************************************/ """ -from ..data_model import TableDataModel, SqlResultModel +from qgis.core import QgsTask +from ..plugin import BaseError +from ..data_model import TableDataModel, SqlResultModel, SqlResultModelAsync +from .plugin import SLDatabase class SLTableDataModel(TableDataModel): @@ -60,5 +63,47 @@ def rowCount(self, index=None): return self.fetchedCount +class SLSqlResultModelTask(QgsTask): + + def __init__(self, db, sql, parent): + QgsTask.__init__(self) + self.db = db + self.sql = sql + self.parent = parent + self.error = BaseError('') + self.model = None + self.clone = None + + def run(self): + try: + self.clone = SLDatabase(None, self.db.connector.uri()) + + # import time + # self.clone.connector.connection.create_function("sleep", 1, time.sleep) + + self.model = SLSqlResultModel(self.clone, self.sql, None) + except BaseError as e: + self.error = e + QgsMessageLog.logMessage(e.msg) + return False + + return True + + def cancelQuery(self): + if self.clone: + self.clone.connector.cancel() + self.cancel() + + +class SLSqlResultModelAsync(SqlResultModelAsync): + + def __init__(self, db, sql, parent): + SqlResultModelAsync.__init__(self, db, sql, parent) + + self.task = SLSqlResultModelTask(db, sql, parent) + self.task.taskCompleted.connect(self.modelDone) + self.task.taskTerminated.connect(self.modelDone) + + class SLSqlResultModel(SqlResultModel): pass diff --git a/python/plugins/db_manager/db_plugins/spatialite/plugin.py b/python/plugins/db_manager/db_plugins/spatialite/plugin.py index 1033dce4615a..70899ba28f83 100644 --- a/python/plugins/db_manager/db_plugins/spatialite/plugin.py +++ b/python/plugins/db_manager/db_plugins/spatialite/plugin.py @@ -130,6 +130,11 @@ def sqlResultModel(self, sql, parent): return SLSqlResultModel(self, sql, parent) + def sqlResultModelAsync(self, sql, parent): + from .data_model import SLSqlResultModelAsync + + return SLSqlResultModelAsync(self, sql, parent) + def registerDatabaseActions(self, mainWindow): action = QAction(self.tr("Run &Vacuum"), self) mainWindow.registerAction(action, self.tr("&Database"), self.runVacuumActionSlot) diff --git a/python/plugins/db_manager/dlg_cancel_task_query.py b/python/plugins/db_manager/dlg_cancel_task_query.py new file mode 100644 index 000000000000..7387450ba718 --- /dev/null +++ b/python/plugins/db_manager/dlg_cancel_task_query.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +""" +/*************************************************************************** +Name : DB Manager +Description : Database manager plugin for QGIS +Date : January 15, 2018 +copyright : (C) 2018 by Paul Blottiere +email : paul.blottiere@oslandia.com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" + +from qgis.PyQt.QtWidgets import QDialog, QLabel, QHBoxLayout +from qgis.PyQt.QtGui import QMovie +from qgis.PyQt.QtCore import QSize, Qt, pyqtSignal + +from qgis.core import QgsApplication + +from .ui.ui_DlgCancelTaskQuery import Ui_DlgCancelTaskQuery as Ui_Dialog + + +class DlgCancelTaskQuery(QDialog, Ui_Dialog): + + canceled = pyqtSignal() + + def __init__(self, parent=None): + QDialog.__init__(self, parent) + self.setupUi(self) + + gif = QgsApplication.iconPath("/mIconLoading.gif") + self.mGif = QMovie(gif) + self.mGif.setScaledSize(QSize(16, 16)) + + self.mMovie.setMovie(self.mGif) + self.setWindowModality(Qt.ApplicationModal) + + self.mCancelButton.clicked.connect(self.cancel) + + self.cancelStatus = False + + def cancel(self): + self.mLabel.setText("Stopping SQL...") + self.cancelStatus = True + self.mCancelButton.setEnabled(False) + self.canceled.emit() + + def show(self): + self.cancelStatus = False + self.mGif.start() + self.mCancelButton.setEnabled(True) + self.mLabel.setText("Executing SQL...") + super(QDialog, self).show() + + def hide(self): + self.cancelStatus = False + self.mGif.stop() + super(QDialog, self).hide() diff --git a/python/plugins/db_manager/dlg_sql_window.py b/python/plugins/db_manager/dlg_sql_window.py index e8f8415abd92..6bd076cc7bbc 100644 --- a/python/plugins/db_manager/dlg_sql_window.py +++ b/python/plugins/db_manager/dlg_sql_window.py @@ -30,13 +30,14 @@ from qgis.PyQt.QtGui import QKeySequence, QCursor, QClipboard, QIcon, QStandardItemModel, QStandardItem from qgis.PyQt.Qsci import QsciAPIs -from qgis.core import QgsProject +from qgis.core import QgsProject, QgsApplication, QgsTask from qgis.utils import OverrideCursor from .db_plugins.plugin import BaseError from .db_plugins.postgis.plugin import PGDatabase from .dlg_db_error import DlgDbError from .dlg_query_builder import QueryBuilderDlg +from .dlg_cancel_task_query import DlgCancelTaskQuery try: from qgis.gui import QgsCodeEditorSQL # NOQA @@ -59,6 +60,9 @@ def __init__(self, iface, db, parent=None): self.iface = iface self.db = db self.filter = "" + self.modelAsync = None + self.dlg_cancel_task = DlgCancelTaskQuery(self) + self.dlg_cancel_task.canceled.connect(self.executeSqlCanceled) self.allowMultiColumnPk = isinstance(db, PGDatabase) # at the moment only PostgreSQL allows a primary key to span multiple columns, SpatiaLite doesn't self.aliasSubQuery = isinstance(db, PGDatabase) # only PostgreSQL requires subqueries to be aliases self.setupUi(self) @@ -177,40 +181,53 @@ def clearSql(self): self.editSql.setFocus() self.filter = "" - def executeSql(self): + def executeSqlCanceled(self): + self.modelAsync.cancel() - sql = self._getSqlQuery() - if sql == "": - return - - with OverrideCursor(Qt.WaitCursor): - # delete the old model - old_model = self.viewResult.model() - self.viewResult.setModel(None) - if old_model: - old_model.deleteLater() + def executeSqlCompleted(self): + self.dlg_cancel_task.hide() + if self.modelAsync.task.status() == QgsTask.Complete: + model = self.modelAsync.model cols = [] quotedCols = [] - try: - # set the new model - model = self.db.sqlResultModel(sql, self) - self.viewResult.setModel(model) - self.lblResult.setText(self.tr("{0} rows, {1:.1f} seconds").format(model.affectedRows(), model.secs())) - cols = self.viewResult.model().columnNames() - for col in cols: - quotedCols.append(self.db.connector.quoteId(col)) - - except BaseError as e: - DlgDbError.showError(e, self) - self.uniqueModel.clear() - self.geomCombo.clear() - return + self.viewResult.setModel(model) + self.lblResult.setText(self.tr("{0} rows, {1:.1f} seconds").format(model.affectedRows(), model.secs())) + cols = self.viewResult.model().columnNames() + for col in cols: + quotedCols.append(self.db.connector.quoteId(col)) self.setColumnCombos(cols, quotedCols) - self.update() + elif not self.dlg_cancel_task.cancelStatus: + DlgDbError.showError(self.modelAsync.error, self) + self.uniqueModel.clear() + self.geomCombo.clear() + pass + + def executeSql(self): + + sql = self._getSqlQuery() + if sql == "": + return + + # delete the old model + old_model = self.viewResult.model() + self.viewResult.setModel(None) + if old_model: + old_model.deleteLater() + + try: + self.modelAsync = self.db.sqlResultModelAsync(sql, self) + self.modelAsync.done.connect(self.executeSqlCompleted) + self.dlg_cancel_task.show() + QgsApplication.taskManager().addTask(self.modelAsync.task) + except Exception as e: + DlgDbError.showError(e, self) + self.uniqueModel.clear() + self.geomCombo.clear() + return def _getSqlLayer(self, _filter): hasUniqueField = self.uniqueColumnCheck.checkState() == Qt.Checked diff --git a/python/plugins/db_manager/ui/DlgCancelTaskQuery.ui b/python/plugins/db_manager/ui/DlgCancelTaskQuery.ui new file mode 100644 index 000000000000..5620c2fa21b4 --- /dev/null +++ b/python/plugins/db_manager/ui/DlgCancelTaskQuery.ui @@ -0,0 +1,86 @@ + + + DlgCancelTaskQuery + + + + 0 + 0 + 178 + 101 + + + + Dialog + + + + + 10 + 10 + 168 + 80 + + + + + + + + + Executing SQL... + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Cancel + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + +