Add a cancel button for Postgis and Spatialite

This commit is contained in:
Blottiere Paul 2018-01-22 10:26:31 +00:00
parent 0081f781d3
commit bf7df6d2e3
12 changed files with 347 additions and 34 deletions

View File

@ -42,6 +42,9 @@ class DBConnector(object):
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)

View File

@ -22,11 +22,18 @@ email : brush.tyler@gmail.com
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 @@ class TableDataModel(BaseTableModel):
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):

View File

@ -256,6 +256,11 @@ class Database(DbItemObject):
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:

View File

@ -186,6 +186,10 @@ class PostGisDBConnector(DBConnector):
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)

View File

@ -20,8 +20,9 @@ email : brush.tyler@gmail.com
***************************************************************************/
"""
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 @@ class PGTableDataModel(TableDataModel):
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

View File

@ -134,6 +134,11 @@ class PGDatabase(Database):
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)

View File

@ -59,6 +59,12 @@ class SpatiaLiteDBConnector(DBConnector):
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):

View File

@ -20,7 +20,10 @@ email : brush.tyler@gmail.com
***************************************************************************/
"""
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 @@ class SLTableDataModel(TableDataModel):
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

View File

@ -130,6 +130,11 @@ class SLDatabase(Database):
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)

View File

@ -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()

View File

@ -30,13 +30,14 @@ from qgis.PyQt.QtWidgets import QDialog, QWidget, QAction, QApplication, QInputD
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 @@ class DlgSqlWindow(QWidget, Ui_Dialog):
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 @@ class DlgSqlWindow(QWidget, Ui_Dialog):
self.editSql.setFocus()
self.filter = ""
def executeSqlCanceled(self):
self.modelAsync.cancel()
def executeSqlCompleted(self):
self.dlg_cancel_task.hide()
if self.modelAsync.task.status() == QgsTask.Complete:
model = self.modelAsync.model
cols = []
quotedCols = []
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
with OverrideCursor(Qt.WaitCursor):
# delete the old model
old_model = self.viewResult.model()
self.viewResult.setModel(None)
if old_model:
old_model.deleteLater()
# delete the old model
old_model = self.viewResult.model()
self.viewResult.setModel(None)
if old_model:
old_model.deleteLater()
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.setColumnCombos(cols, quotedCols)
self.update()
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

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DlgCancelTaskQuery</class>
<widget class="QDialog" name="DlgCancelTaskQuery">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>178</width>
<height>101</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<widget class="QWidget" name="verticalLayoutWidget">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>168</width>
<height>80</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="mLabel">
<property name="text">
<string>Executing SQL...</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="mMovie">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="mCancelButton">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>