123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579 |
- """
- /***************************************************************************
- Name : DB Manager
- Description : Database manager plugin for QGIS
- Date : May 23, 2011
- copyright : (C) 2011 by Giuseppe Sucameli
- email : brush.tyler@gmail.com
- The content of this file is based on
- - PG_Manager by Martin Dobias (GPLv2 license)
- ***************************************************************************/
- /***************************************************************************
- * *
- * 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 hashlib import md5
- from qgis.PyQt.QtCore import Qt, pyqtSignal
- from qgis.PyQt.QtWidgets import (QDialog,
- QWidget,
- QAction,
- QApplication,
- QStyledItemDelegate,
- QMessageBox
- )
- from qgis.PyQt.QtGui import (QKeySequence,
- QCursor,
- QClipboard,
- QIcon,
- QStandardItemModel,
- QStandardItem
- )
- from qgis.PyQt.Qsci import QsciAPIs
- from qgis.PyQt.QtXml import QDomDocument
- from qgis.core import (
- QgsProject,
- QgsDataSourceUri,
- QgsReadWriteContext,
- QgsMapLayerType
- )
- from qgis.utils import OverrideCursor
- from .db_plugins import createDbPlugin
- 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
- try:
- from qgis.gui import QgsCodeEditorSQL # NOQA
- except:
- from .sqledit import SqlEdit
- from qgis import gui
- gui.QgsCodeEditorSQL = SqlEdit
- from .ui.ui_DlgSqlLayerWindow import Ui_DbManagerDlgSqlLayerWindow as Ui_Dialog
- import re
- class DlgSqlLayerWindow(QWidget, Ui_Dialog):
- nameChanged = pyqtSignal(str)
- hasChanged = False
- def __init__(self, iface, layer, parent=None):
- QWidget.__init__(self, parent)
- self.iface = iface
- self.layer = layer
- uri = QgsDataSourceUri(layer.source())
- dbplugin = None
- db = None
- if layer.dataProvider().name() == 'postgres':
- dbplugin = createDbPlugin('postgis', 'postgres')
- elif layer.dataProvider().name() == 'spatialite':
- dbplugin = createDbPlugin('spatialite', 'spatialite')
- elif layer.dataProvider().name() == 'oracle':
- dbplugin = createDbPlugin('oracle', 'oracle')
- elif layer.dataProvider().name() == 'virtual':
- dbplugin = createDbPlugin('vlayers', 'virtual')
- elif layer.dataProvider().name() == 'ogr':
- dbplugin = createDbPlugin('gpkg', 'gpkg')
- if dbplugin:
- dbplugin.connectToUri(uri)
- db = dbplugin.db
- self.dbplugin = dbplugin
- self.db = db
- self.filter = ""
- 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)
- self.setWindowTitle(
- "%s - %s [%s]" % (self.windowTitle(), db.connection().connectionName(), db.connection().typeNameString()))
- self.defaultLayerName = self.tr('QueryLayer')
- if self.allowMultiColumnPk:
- self.uniqueColumnCheck.setText(self.tr("Column(s) with unique values"))
- else:
- self.uniqueColumnCheck.setText(self.tr("Column with unique values"))
- self.editSql.setFocus()
- self.editSql.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
- self.editSql.setLineNumbersVisible(True)
- self.initCompleter()
- self.editSql.textChanged.connect(lambda: self.setHasChanged(True))
- # allow copying results
- copyAction = QAction("copy", self)
- self.viewResult.addAction(copyAction)
- copyAction.setShortcuts(QKeySequence.Copy)
- copyAction.triggered.connect(self.copySelectedResults)
- self.btnExecute.clicked.connect(self.executeSql)
- self.btnSetFilter.clicked.connect(self.setFilter)
- self.btnClear.clicked.connect(self.clearSql)
- self.presetStore.clicked.connect(self.storePreset)
- self.presetDelete.clicked.connect(self.deletePreset)
- self.presetCombo.activated[str].connect(self.loadPreset)
- self.presetCombo.activated[str].connect(self.presetName.setText)
- self.editSql.textChanged.connect(self.updatePresetButtonsState)
- self.presetName.textChanged.connect(self.updatePresetButtonsState)
- self.presetCombo.currentIndexChanged.connect(self.updatePresetButtonsState)
- self.updatePresetsCombobox()
- self.geomCombo.setEditable(True)
- self.geomCombo.lineEdit().setReadOnly(True)
- self.uniqueCombo.setEditable(True)
- self.uniqueCombo.lineEdit().setReadOnly(True)
- self.uniqueModel = QStandardItemModel(self.uniqueCombo)
- self.uniqueCombo.setModel(self.uniqueModel)
- if self.allowMultiColumnPk:
- self.uniqueCombo.setItemDelegate(QStyledItemDelegate())
- self.uniqueModel.itemChanged.connect(self.uniqueChanged) # react to the (un)checking of an item
- self.uniqueCombo.lineEdit().textChanged.connect(
- self.uniqueTextChanged) # there are other events that change the displayed text and some of them can not be caught directly
- self.layerTypeWidget.hide() # show if load as raster is supported
- # self.loadLayerBtn.clicked.connect(self.loadSqlLayer)
- self.updateLayerBtn.clicked.connect(self.updateSqlLayer)
- self.getColumnsBtn.clicked.connect(self.fillColumnCombos)
- self.queryBuilderFirst = True
- self.queryBuilderBtn.setIcon(QIcon(":/db_manager/icons/sql.gif"))
- self.queryBuilderBtn.clicked.connect(self.displayQueryBuilder)
- self.presetName.textChanged.connect(self.nameChanged)
- # Update from layer
- # First the SQL from QgsDataSourceUri table
- sql = uri.table().replace('\n', ' ').strip()
- if uri.keyColumn() == '_uid_':
- match = re.search(r'^\(SELECT .+ AS _uid_,\* FROM \((.*)\) AS _subq_.+_\s*\)$', sql, re.S | re.X | re.IGNORECASE)
- if match:
- sql = match.group(1)
- else:
- match = re.search(r'^\((SELECT .+ FROM .+)\)$', sql, re.S | re.X | re.IGNORECASE)
- if match:
- sql = match.group(1)
- # Need to check on table() since the parentheses were removed by the regexp
- if not uri.table().startswith('(') and not uri.table().endswith(')'):
- schema = uri.schema()
- if schema and schema.upper() != 'PUBLIC':
- sql = 'SELECT * FROM {}.{}'.format(self.db.connector.quoteId(schema), self.db.connector.quoteId(sql))
- else:
- sql = 'SELECT * FROM {}'.format(self.db.connector.quoteId(sql))
- self.editSql.setText(sql)
- self.executeSql()
- # Then the columns
- self.geomCombo.setCurrentIndex(self.geomCombo.findText(uri.geometryColumn(), Qt.MatchExactly))
- if uri.keyColumn() != '_uid_':
- self.uniqueColumnCheck.setCheckState(Qt.Checked)
- if self.allowMultiColumnPk:
- # Unchecked default values
- for item in self.uniqueModel.findItems("*", Qt.MatchWildcard):
- if item.checkState() == Qt.Checked:
- item.setCheckState(Qt.Unchecked)
- # Get key columns
- itemsData = uri.keyColumn().split(',')
- # Checked key columns
- for keyColumn in itemsData:
- for item in self.uniqueModel.findItems(keyColumn):
- item.setCheckState(Qt.Checked)
- else:
- keyColumn = uri.keyColumn()
- if self.uniqueModel.findItems(keyColumn):
- self.uniqueCombo.setCurrentIndex(self.uniqueCombo.findText(keyColumn, Qt.MatchExactly))
- # Finally layer name, filter and selectAtId
- self.layerNameEdit.setText(layer.name())
- self.filter = uri.sql()
- if uri.selectAtIdDisabled():
- self.avoidSelectById.setCheckState(Qt.Checked)
- def getQueryHash(self, name):
- return 'q%s' % md5(name.encode('utf8')).hexdigest()
- def updatePresetButtonsState(self, *args):
- """Slot called when the combo box or the sql or the query name have changed:
- sets store button state"""
- self.presetStore.setEnabled(bool(self._getSqlQuery() and self.presetName.text()))
- self.presetDelete.setEnabled(bool(self.presetCombo.currentIndex() != -1))
- def updatePresetsCombobox(self):
- self.presetCombo.clear()
- names = []
- entries = QgsProject.instance().subkeyList('DBManager', 'savedQueries')
- for entry in entries:
- name = QgsProject.instance().readEntry('DBManager', 'savedQueries/' + entry + '/name')[0]
- names.append(name)
- for name in sorted(names):
- self.presetCombo.addItem(name)
- self.presetCombo.setCurrentIndex(-1)
- def storePreset(self):
- query = self._getSqlQuery()
- if query == "":
- return
- name = self.presetName.text()
- QgsProject.instance().writeEntry('DBManager', 'savedQueries/' + self.getQueryHash(name) + '/name', name)
- QgsProject.instance().writeEntry('DBManager', 'savedQueries/' + self.getQueryHash(name) + '/query', query)
- index = self.presetCombo.findText(name)
- if index == -1:
- self.presetCombo.addItem(name)
- self.presetCombo.setCurrentIndex(self.presetCombo.count() - 1)
- else:
- self.presetCombo.setCurrentIndex(index)
- def deletePreset(self):
- name = self.presetCombo.currentText()
- QgsProject.instance().removeEntry('DBManager', 'savedQueries/q' + self.getQueryHash(name))
- self.presetCombo.removeItem(self.presetCombo.findText(name))
- self.presetCombo.setCurrentIndex(-1)
- def loadPreset(self, name):
- query = QgsProject.instance().readEntry('DBManager', 'savedQueries/' + self.getQueryHash(name) + '/query')[0]
- self.editSql.setText(query)
- def clearSql(self):
- self.editSql.clear()
- self.editSql.setFocus()
- self.filter = ""
- 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()
- quotedCols = []
- try:
- # set the new model
- model = self.db.sqlResultModel(sql, self)
- self.viewResult.setModel(model)
- self.lblResult.setText(self.tr("{0} rows, {1:.3f} 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()
- def _getSqlLayer(self, _filter):
- hasUniqueField = self.uniqueColumnCheck.checkState() == Qt.Checked
- if hasUniqueField and self.allowMultiColumnPk:
- checkedCols = [
- item.data()
- for item in self.uniqueModel.findItems("*", Qt.MatchWildcard)
- if item.checkState() == Qt.Checked
- ]
- uniqueFieldName = ",".join(checkedCols)
- elif (
- hasUniqueField
- and not self.allowMultiColumnPk
- and self.uniqueCombo.currentIndex() >= 0
- ):
- uniqueFieldName = self.uniqueModel.item(self.uniqueCombo.currentIndex()).data()
- else:
- uniqueFieldName = None
- hasGeomCol = self.hasGeometryCol.checkState() == Qt.Checked
- if hasGeomCol:
- geomFieldName = self.geomCombo.currentText()
- else:
- geomFieldName = None
- query = self._getSqlQuery()
- if query == "":
- return None
- # remove a trailing ';' from query if present
- if query.strip().endswith(';'):
- query = query.strip()[:-1]
- layerType = QgsMapLayerType.VectorLayer if self.vectorRadio.isChecked() else QgsMapLayerType.RasterLayer
- # get a new layer name
- names = []
- for layer in list(QgsProject.instance().mapLayers().values()):
- names.append(layer.name())
- layerName = self.layerNameEdit.text()
- if layerName == "":
- layerName = self.defaultLayerName
- newLayerName = layerName
- index = 1
- while newLayerName in names:
- index += 1
- newLayerName = "%s_%d" % (layerName, index)
- # create the layer
- layer = self.db.toSqlLayer(query, geomFieldName, uniqueFieldName, newLayerName, layerType,
- self.avoidSelectById.isChecked(), _filter)
- if layer.isValid():
- return layer
- else:
- return None
- def loadSqlLayer(self):
- with OverrideCursor(Qt.WaitCursor):
- layer = self._getSqlLayer(self.filter)
- if layer is None:
- return
- QgsProject.instance().addMapLayers([layer], True)
- def updateSqlLayer(self):
- with OverrideCursor(Qt.WaitCursor):
- layer = self._getSqlLayer(self.filter)
- if layer is None:
- return
- # self.layer.dataProvider().setDataSourceUri(layer.dataProvider().dataSourceUri())
- # self.layer.dataProvider().reloadData()
- XMLDocument = QDomDocument("style")
- XMLMapLayers = XMLDocument.createElement("maplayers")
- XMLMapLayer = XMLDocument.createElement("maplayer")
- self.layer.writeLayerXml(XMLMapLayer, XMLDocument, QgsReadWriteContext())
- XMLMapLayer.firstChildElement("datasource").firstChild().setNodeValue(layer.source())
- XMLMapLayers.appendChild(XMLMapLayer)
- XMLDocument.appendChild(XMLMapLayers)
- self.layer.readLayerXml(XMLMapLayer, QgsReadWriteContext())
- self.layer.reload()
- self.iface.actionDraw().trigger()
- self.iface.mapCanvas().refresh()
- def fillColumnCombos(self):
- query = self._getSqlQuery()
- if query == "":
- return
- with OverrideCursor(Qt.WaitCursor):
- # remove a trailing ';' from query if present
- if query.strip().endswith(';'):
- query = query.strip()[:-1]
- # get all the columns
- quotedCols = []
- connector = self.db.connector
- if self.aliasSubQuery:
- # get a new alias
- aliasIndex = 0
- while True:
- alias = "_subQuery__%d" % aliasIndex
- escaped = re.compile('\\b("?)' + re.escape(alias) + '\\1\\b')
- if not escaped.search(query):
- break
- aliasIndex += 1
- sql = "SELECT * FROM (%s\n) AS %s LIMIT 0" % (str(query), connector.quoteId(alias))
- else:
- sql = "SELECT * FROM (%s\n) WHERE 1=0" % str(query)
- c = None
- try:
- c = connector._execute(None, sql)
- cols = connector._get_cursor_columns(c)
- for col in cols:
- quotedCols.append(connector.quoteId(col))
- except BaseError as e:
- DlgDbError.showError(e, self)
- self.uniqueModel.clear()
- self.geomCombo.clear()
- return
- finally:
- if c:
- c.close()
- del c
- self.setColumnCombos(cols, quotedCols)
- def setColumnCombos(self, cols, quotedCols):
- # get sensible default columns. do this before sorting in case there's hints in the column order (e.g., id is more likely to be first)
- try:
- defaultGeomCol = next(col for col in cols if col in ['geom', 'geometry', 'the_geom', 'way'])
- except:
- defaultGeomCol = None
- try:
- defaultUniqueCol = [col for col in cols if 'id' in col][0]
- except:
- defaultUniqueCol = None
- colNames = sorted(zip(cols, quotedCols))
- newItems = []
- uniqueIsFilled = False
- for (col, quotedCol) in colNames:
- item = QStandardItem(col)
- item.setData(quotedCol)
- item.setEnabled(True)
- item.setCheckable(self.allowMultiColumnPk)
- item.setSelectable(not self.allowMultiColumnPk)
- if self.allowMultiColumnPk:
- matchingItems = self.uniqueModel.findItems(col)
- if matchingItems:
- item.setCheckState(matchingItems[0].checkState())
- uniqueIsFilled = uniqueIsFilled or matchingItems[0].checkState() == Qt.Checked
- else:
- item.setCheckState(Qt.Unchecked)
- newItems.append(item)
- if self.allowMultiColumnPk:
- self.uniqueModel.clear()
- self.uniqueModel.appendColumn(newItems)
- self.uniqueChanged()
- else:
- previousUniqueColumn = self.uniqueCombo.currentText()
- self.uniqueModel.clear()
- self.uniqueModel.appendColumn(newItems)
- if self.uniqueModel.findItems(previousUniqueColumn):
- self.uniqueCombo.setEditText(previousUniqueColumn)
- uniqueIsFilled = True
- oldGeometryColumn = self.geomCombo.currentText()
- self.geomCombo.clear()
- self.geomCombo.addItems(cols)
- self.geomCombo.setCurrentIndex(self.geomCombo.findText(oldGeometryColumn, Qt.MatchExactly))
- # set sensible default columns if the columns are not already set
- try:
- if self.geomCombo.currentIndex() == -1:
- self.geomCombo.setCurrentIndex(cols.index(defaultGeomCol))
- except:
- pass
- items = self.uniqueModel.findItems(defaultUniqueCol)
- if items and not uniqueIsFilled:
- if self.allowMultiColumnPk:
- items[0].setCheckState(Qt.Checked)
- else:
- self.uniqueCombo.setEditText(defaultUniqueCol)
- def copySelectedResults(self):
- if len(self.viewResult.selectedIndexes()) <= 0:
- return
- model = self.viewResult.model()
- # convert to string using tab as separator
- text = model.headerToString("\t")
- for idx in self.viewResult.selectionModel().selectedRows():
- text += "\n" + model.rowToString(idx.row(), "\t")
- QApplication.clipboard().setText(text, QClipboard.Selection)
- QApplication.clipboard().setText(text, QClipboard.Clipboard)
- def initCompleter(self):
- dictionary = None
- if self.db:
- dictionary = self.db.connector.getSqlDictionary()
- if not dictionary:
- # use the generic sql dictionary
- from .sql_dictionary import getSqlDictionary
- dictionary = getSqlDictionary()
- wordlist = []
- for name, value in dictionary.items():
- wordlist += value # concat lists
- wordlist = list(set(wordlist)) # remove duplicates
- api = QsciAPIs(self.editSql.lexer())
- for word in wordlist:
- api.add(word)
- api.prepare()
- self.editSql.lexer().setAPIs(api)
- def displayQueryBuilder(self):
- dlg = QueryBuilderDlg(self.iface, self.db, self, reset=self.queryBuilderFirst)
- self.queryBuilderFirst = False
- r = dlg.exec_()
- if r == QDialog.Accepted:
- self.editSql.setText(dlg.query)
- def _getSqlQuery(self):
- sql = self.editSql.selectedText()
- if len(sql) == 0:
- sql = self.editSql.text()
- return sql
- def uniqueChanged(self):
- # when an item is (un)checked, simply trigger an update of the combobox text
- self.uniqueTextChanged(None)
- def uniqueTextChanged(self, text):
- # Whenever there is new text displayed in the combobox, check if it is the correct one and if not, display the correct one.
- checkedItems = [
- item.text()
- for item in self.uniqueModel.findItems("*", Qt.MatchWildcard)
- if item.checkState() == Qt.Checked
- ]
- label = ", ".join(checkedItems)
- if text != label:
- self.uniqueCombo.setEditText(label)
- def setFilter(self):
- from qgis.gui import QgsQueryBuilder
- layer = self._getSqlLayer("")
- if not layer:
- return
- dlg = QgsQueryBuilder(layer)
- dlg.setSql(self.filter)
- if dlg.exec_():
- self.filter = dlg.sql()
- layer.deleteLater()
- def setHasChanged(self, hasChanged):
- self.hasChanged = hasChanged
- def close(self):
- if self.hasChanged:
- ret = QMessageBox.question(
- self, self.tr('Unsaved Changes?'),
- self.tr('There are unsaved changes. Do you want to keep them?'),
- QMessageBox.Save | QMessageBox.Cancel | QMessageBox.Discard, QMessageBox.Cancel)
- if ret == QMessageBox.Save:
- self.saveAsFilePreset()
- return True
- elif ret == QMessageBox.Discard:
- return True
- else:
- return False
- else:
- return True
|