dlg_sql_layer_window.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  1. """
  2. /***************************************************************************
  3. Name : DB Manager
  4. Description : Database manager plugin for QGIS
  5. Date : May 23, 2011
  6. copyright : (C) 2011 by Giuseppe Sucameli
  7. email : brush.tyler@gmail.com
  8. The content of this file is based on
  9. - PG_Manager by Martin Dobias (GPLv2 license)
  10. ***************************************************************************/
  11. /***************************************************************************
  12. * *
  13. * This program is free software; you can redistribute it and/or modify *
  14. * it under the terms of the GNU General Public License as published by *
  15. * the Free Software Foundation; either version 2 of the License, or *
  16. * (at your option) any later version. *
  17. * *
  18. ***************************************************************************/
  19. """
  20. from hashlib import md5
  21. from qgis.PyQt.QtCore import Qt, pyqtSignal
  22. from qgis.PyQt.QtWidgets import (QDialog,
  23. QWidget,
  24. QAction,
  25. QApplication,
  26. QStyledItemDelegate,
  27. QMessageBox
  28. )
  29. from qgis.PyQt.QtGui import (QKeySequence,
  30. QCursor,
  31. QClipboard,
  32. QIcon,
  33. QStandardItemModel,
  34. QStandardItem
  35. )
  36. from qgis.PyQt.Qsci import QsciAPIs
  37. from qgis.PyQt.QtXml import QDomDocument
  38. from qgis.core import (
  39. QgsProject,
  40. QgsDataSourceUri,
  41. QgsReadWriteContext,
  42. QgsMapLayerType
  43. )
  44. from qgis.utils import OverrideCursor
  45. from .db_plugins import createDbPlugin
  46. from .db_plugins.plugin import BaseError
  47. from .db_plugins.postgis.plugin import PGDatabase
  48. from .dlg_db_error import DlgDbError
  49. from .dlg_query_builder import QueryBuilderDlg
  50. try:
  51. from qgis.gui import QgsCodeEditorSQL # NOQA
  52. except:
  53. from .sqledit import SqlEdit
  54. from qgis import gui
  55. gui.QgsCodeEditorSQL = SqlEdit
  56. from .ui.ui_DlgSqlLayerWindow import Ui_DbManagerDlgSqlLayerWindow as Ui_Dialog
  57. import re
  58. class DlgSqlLayerWindow(QWidget, Ui_Dialog):
  59. nameChanged = pyqtSignal(str)
  60. hasChanged = False
  61. def __init__(self, iface, layer, parent=None):
  62. QWidget.__init__(self, parent)
  63. self.iface = iface
  64. self.layer = layer
  65. uri = QgsDataSourceUri(layer.source())
  66. dbplugin = None
  67. db = None
  68. if layer.dataProvider().name() == 'postgres':
  69. dbplugin = createDbPlugin('postgis', 'postgres')
  70. elif layer.dataProvider().name() == 'spatialite':
  71. dbplugin = createDbPlugin('spatialite', 'spatialite')
  72. elif layer.dataProvider().name() == 'oracle':
  73. dbplugin = createDbPlugin('oracle', 'oracle')
  74. elif layer.dataProvider().name() == 'virtual':
  75. dbplugin = createDbPlugin('vlayers', 'virtual')
  76. elif layer.dataProvider().name() == 'ogr':
  77. dbplugin = createDbPlugin('gpkg', 'gpkg')
  78. if dbplugin:
  79. dbplugin.connectToUri(uri)
  80. db = dbplugin.db
  81. self.dbplugin = dbplugin
  82. self.db = db
  83. self.filter = ""
  84. self.allowMultiColumnPk = isinstance(db, PGDatabase) # at the moment only PostgreSQL allows a primary key to span multiple columns, SpatiaLite doesn't
  85. self.aliasSubQuery = isinstance(db, PGDatabase) # only PostgreSQL requires subqueries to be aliases
  86. self.setupUi(self)
  87. self.setWindowTitle(
  88. "%s - %s [%s]" % (self.windowTitle(), db.connection().connectionName(), db.connection().typeNameString()))
  89. self.defaultLayerName = self.tr('QueryLayer')
  90. if self.allowMultiColumnPk:
  91. self.uniqueColumnCheck.setText(self.tr("Column(s) with unique values"))
  92. else:
  93. self.uniqueColumnCheck.setText(self.tr("Column with unique values"))
  94. self.editSql.setFocus()
  95. self.editSql.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
  96. self.editSql.setLineNumbersVisible(True)
  97. self.initCompleter()
  98. self.editSql.textChanged.connect(lambda: self.setHasChanged(True))
  99. # allow copying results
  100. copyAction = QAction("copy", self)
  101. self.viewResult.addAction(copyAction)
  102. copyAction.setShortcuts(QKeySequence.Copy)
  103. copyAction.triggered.connect(self.copySelectedResults)
  104. self.btnExecute.clicked.connect(self.executeSql)
  105. self.btnSetFilter.clicked.connect(self.setFilter)
  106. self.btnClear.clicked.connect(self.clearSql)
  107. self.presetStore.clicked.connect(self.storePreset)
  108. self.presetDelete.clicked.connect(self.deletePreset)
  109. self.presetCombo.activated[str].connect(self.loadPreset)
  110. self.presetCombo.activated[str].connect(self.presetName.setText)
  111. self.editSql.textChanged.connect(self.updatePresetButtonsState)
  112. self.presetName.textChanged.connect(self.updatePresetButtonsState)
  113. self.presetCombo.currentIndexChanged.connect(self.updatePresetButtonsState)
  114. self.updatePresetsCombobox()
  115. self.geomCombo.setEditable(True)
  116. self.geomCombo.lineEdit().setReadOnly(True)
  117. self.uniqueCombo.setEditable(True)
  118. self.uniqueCombo.lineEdit().setReadOnly(True)
  119. self.uniqueModel = QStandardItemModel(self.uniqueCombo)
  120. self.uniqueCombo.setModel(self.uniqueModel)
  121. if self.allowMultiColumnPk:
  122. self.uniqueCombo.setItemDelegate(QStyledItemDelegate())
  123. self.uniqueModel.itemChanged.connect(self.uniqueChanged) # react to the (un)checking of an item
  124. self.uniqueCombo.lineEdit().textChanged.connect(
  125. self.uniqueTextChanged) # there are other events that change the displayed text and some of them can not be caught directly
  126. self.layerTypeWidget.hide() # show if load as raster is supported
  127. # self.loadLayerBtn.clicked.connect(self.loadSqlLayer)
  128. self.updateLayerBtn.clicked.connect(self.updateSqlLayer)
  129. self.getColumnsBtn.clicked.connect(self.fillColumnCombos)
  130. self.queryBuilderFirst = True
  131. self.queryBuilderBtn.setIcon(QIcon(":/db_manager/icons/sql.gif"))
  132. self.queryBuilderBtn.clicked.connect(self.displayQueryBuilder)
  133. self.presetName.textChanged.connect(self.nameChanged)
  134. # Update from layer
  135. # First the SQL from QgsDataSourceUri table
  136. sql = uri.table().replace('\n', ' ').strip()
  137. if uri.keyColumn() == '_uid_':
  138. match = re.search(r'^\(SELECT .+ AS _uid_,\* FROM \((.*)\) AS _subq_.+_\s*\)$', sql, re.S | re.X | re.IGNORECASE)
  139. if match:
  140. sql = match.group(1)
  141. else:
  142. match = re.search(r'^\((SELECT .+ FROM .+)\)$', sql, re.S | re.X | re.IGNORECASE)
  143. if match:
  144. sql = match.group(1)
  145. # Need to check on table() since the parentheses were removed by the regexp
  146. if not uri.table().startswith('(') and not uri.table().endswith(')'):
  147. schema = uri.schema()
  148. if schema and schema.upper() != 'PUBLIC':
  149. sql = 'SELECT * FROM {}.{}'.format(self.db.connector.quoteId(schema), self.db.connector.quoteId(sql))
  150. else:
  151. sql = 'SELECT * FROM {}'.format(self.db.connector.quoteId(sql))
  152. self.editSql.setText(sql)
  153. self.executeSql()
  154. # Then the columns
  155. self.geomCombo.setCurrentIndex(self.geomCombo.findText(uri.geometryColumn(), Qt.MatchExactly))
  156. if uri.keyColumn() != '_uid_':
  157. self.uniqueColumnCheck.setCheckState(Qt.Checked)
  158. if self.allowMultiColumnPk:
  159. # Unchecked default values
  160. for item in self.uniqueModel.findItems("*", Qt.MatchWildcard):
  161. if item.checkState() == Qt.Checked:
  162. item.setCheckState(Qt.Unchecked)
  163. # Get key columns
  164. itemsData = uri.keyColumn().split(',')
  165. # Checked key columns
  166. for keyColumn in itemsData:
  167. for item in self.uniqueModel.findItems(keyColumn):
  168. item.setCheckState(Qt.Checked)
  169. else:
  170. keyColumn = uri.keyColumn()
  171. if self.uniqueModel.findItems(keyColumn):
  172. self.uniqueCombo.setCurrentIndex(self.uniqueCombo.findText(keyColumn, Qt.MatchExactly))
  173. # Finally layer name, filter and selectAtId
  174. self.layerNameEdit.setText(layer.name())
  175. self.filter = uri.sql()
  176. if uri.selectAtIdDisabled():
  177. self.avoidSelectById.setCheckState(Qt.Checked)
  178. def getQueryHash(self, name):
  179. return 'q%s' % md5(name.encode('utf8')).hexdigest()
  180. def updatePresetButtonsState(self, *args):
  181. """Slot called when the combo box or the sql or the query name have changed:
  182. sets store button state"""
  183. self.presetStore.setEnabled(bool(self._getSqlQuery() and self.presetName.text()))
  184. self.presetDelete.setEnabled(bool(self.presetCombo.currentIndex() != -1))
  185. def updatePresetsCombobox(self):
  186. self.presetCombo.clear()
  187. names = []
  188. entries = QgsProject.instance().subkeyList('DBManager', 'savedQueries')
  189. for entry in entries:
  190. name = QgsProject.instance().readEntry('DBManager', 'savedQueries/' + entry + '/name')[0]
  191. names.append(name)
  192. for name in sorted(names):
  193. self.presetCombo.addItem(name)
  194. self.presetCombo.setCurrentIndex(-1)
  195. def storePreset(self):
  196. query = self._getSqlQuery()
  197. if query == "":
  198. return
  199. name = self.presetName.text()
  200. QgsProject.instance().writeEntry('DBManager', 'savedQueries/' + self.getQueryHash(name) + '/name', name)
  201. QgsProject.instance().writeEntry('DBManager', 'savedQueries/' + self.getQueryHash(name) + '/query', query)
  202. index = self.presetCombo.findText(name)
  203. if index == -1:
  204. self.presetCombo.addItem(name)
  205. self.presetCombo.setCurrentIndex(self.presetCombo.count() - 1)
  206. else:
  207. self.presetCombo.setCurrentIndex(index)
  208. def deletePreset(self):
  209. name = self.presetCombo.currentText()
  210. QgsProject.instance().removeEntry('DBManager', 'savedQueries/q' + self.getQueryHash(name))
  211. self.presetCombo.removeItem(self.presetCombo.findText(name))
  212. self.presetCombo.setCurrentIndex(-1)
  213. def loadPreset(self, name):
  214. query = QgsProject.instance().readEntry('DBManager', 'savedQueries/' + self.getQueryHash(name) + '/query')[0]
  215. self.editSql.setText(query)
  216. def clearSql(self):
  217. self.editSql.clear()
  218. self.editSql.setFocus()
  219. self.filter = ""
  220. def executeSql(self):
  221. sql = self._getSqlQuery()
  222. if sql == "":
  223. return
  224. with OverrideCursor(Qt.WaitCursor):
  225. # delete the old model
  226. old_model = self.viewResult.model()
  227. self.viewResult.setModel(None)
  228. if old_model:
  229. old_model.deleteLater()
  230. quotedCols = []
  231. try:
  232. # set the new model
  233. model = self.db.sqlResultModel(sql, self)
  234. self.viewResult.setModel(model)
  235. self.lblResult.setText(self.tr("{0} rows, {1:.3f} seconds").format(model.affectedRows(), model.secs()))
  236. cols = self.viewResult.model().columnNames()
  237. for col in cols:
  238. quotedCols.append(self.db.connector.quoteId(col))
  239. except BaseError as e:
  240. DlgDbError.showError(e, self)
  241. self.uniqueModel.clear()
  242. self.geomCombo.clear()
  243. return
  244. self.setColumnCombos(cols, quotedCols)
  245. self.update()
  246. def _getSqlLayer(self, _filter):
  247. hasUniqueField = self.uniqueColumnCheck.checkState() == Qt.Checked
  248. if hasUniqueField and self.allowMultiColumnPk:
  249. checkedCols = [
  250. item.data()
  251. for item in self.uniqueModel.findItems("*", Qt.MatchWildcard)
  252. if item.checkState() == Qt.Checked
  253. ]
  254. uniqueFieldName = ",".join(checkedCols)
  255. elif (
  256. hasUniqueField
  257. and not self.allowMultiColumnPk
  258. and self.uniqueCombo.currentIndex() >= 0
  259. ):
  260. uniqueFieldName = self.uniqueModel.item(self.uniqueCombo.currentIndex()).data()
  261. else:
  262. uniqueFieldName = None
  263. hasGeomCol = self.hasGeometryCol.checkState() == Qt.Checked
  264. if hasGeomCol:
  265. geomFieldName = self.geomCombo.currentText()
  266. else:
  267. geomFieldName = None
  268. query = self._getSqlQuery()
  269. if query == "":
  270. return None
  271. # remove a trailing ';' from query if present
  272. if query.strip().endswith(';'):
  273. query = query.strip()[:-1]
  274. layerType = QgsMapLayerType.VectorLayer if self.vectorRadio.isChecked() else QgsMapLayerType.RasterLayer
  275. # get a new layer name
  276. names = []
  277. for layer in list(QgsProject.instance().mapLayers().values()):
  278. names.append(layer.name())
  279. layerName = self.layerNameEdit.text()
  280. if layerName == "":
  281. layerName = self.defaultLayerName
  282. newLayerName = layerName
  283. index = 1
  284. while newLayerName in names:
  285. index += 1
  286. newLayerName = "%s_%d" % (layerName, index)
  287. # create the layer
  288. layer = self.db.toSqlLayer(query, geomFieldName, uniqueFieldName, newLayerName, layerType,
  289. self.avoidSelectById.isChecked(), _filter)
  290. if layer.isValid():
  291. return layer
  292. else:
  293. return None
  294. def loadSqlLayer(self):
  295. with OverrideCursor(Qt.WaitCursor):
  296. layer = self._getSqlLayer(self.filter)
  297. if layer is None:
  298. return
  299. QgsProject.instance().addMapLayers([layer], True)
  300. def updateSqlLayer(self):
  301. with OverrideCursor(Qt.WaitCursor):
  302. layer = self._getSqlLayer(self.filter)
  303. if layer is None:
  304. return
  305. # self.layer.dataProvider().setDataSourceUri(layer.dataProvider().dataSourceUri())
  306. # self.layer.dataProvider().reloadData()
  307. XMLDocument = QDomDocument("style")
  308. XMLMapLayers = XMLDocument.createElement("maplayers")
  309. XMLMapLayer = XMLDocument.createElement("maplayer")
  310. self.layer.writeLayerXml(XMLMapLayer, XMLDocument, QgsReadWriteContext())
  311. XMLMapLayer.firstChildElement("datasource").firstChild().setNodeValue(layer.source())
  312. XMLMapLayers.appendChild(XMLMapLayer)
  313. XMLDocument.appendChild(XMLMapLayers)
  314. self.layer.readLayerXml(XMLMapLayer, QgsReadWriteContext())
  315. self.layer.reload()
  316. self.iface.actionDraw().trigger()
  317. self.iface.mapCanvas().refresh()
  318. def fillColumnCombos(self):
  319. query = self._getSqlQuery()
  320. if query == "":
  321. return
  322. with OverrideCursor(Qt.WaitCursor):
  323. # remove a trailing ';' from query if present
  324. if query.strip().endswith(';'):
  325. query = query.strip()[:-1]
  326. # get all the columns
  327. quotedCols = []
  328. connector = self.db.connector
  329. if self.aliasSubQuery:
  330. # get a new alias
  331. aliasIndex = 0
  332. while True:
  333. alias = "_subQuery__%d" % aliasIndex
  334. escaped = re.compile('\\b("?)' + re.escape(alias) + '\\1\\b')
  335. if not escaped.search(query):
  336. break
  337. aliasIndex += 1
  338. sql = "SELECT * FROM (%s\n) AS %s LIMIT 0" % (str(query), connector.quoteId(alias))
  339. else:
  340. sql = "SELECT * FROM (%s\n) WHERE 1=0" % str(query)
  341. c = None
  342. try:
  343. c = connector._execute(None, sql)
  344. cols = connector._get_cursor_columns(c)
  345. for col in cols:
  346. quotedCols.append(connector.quoteId(col))
  347. except BaseError as e:
  348. DlgDbError.showError(e, self)
  349. self.uniqueModel.clear()
  350. self.geomCombo.clear()
  351. return
  352. finally:
  353. if c:
  354. c.close()
  355. del c
  356. self.setColumnCombos(cols, quotedCols)
  357. def setColumnCombos(self, cols, quotedCols):
  358. # 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)
  359. try:
  360. defaultGeomCol = next(col for col in cols if col in ['geom', 'geometry', 'the_geom', 'way'])
  361. except:
  362. defaultGeomCol = None
  363. try:
  364. defaultUniqueCol = [col for col in cols if 'id' in col][0]
  365. except:
  366. defaultUniqueCol = None
  367. colNames = sorted(zip(cols, quotedCols))
  368. newItems = []
  369. uniqueIsFilled = False
  370. for (col, quotedCol) in colNames:
  371. item = QStandardItem(col)
  372. item.setData(quotedCol)
  373. item.setEnabled(True)
  374. item.setCheckable(self.allowMultiColumnPk)
  375. item.setSelectable(not self.allowMultiColumnPk)
  376. if self.allowMultiColumnPk:
  377. matchingItems = self.uniqueModel.findItems(col)
  378. if matchingItems:
  379. item.setCheckState(matchingItems[0].checkState())
  380. uniqueIsFilled = uniqueIsFilled or matchingItems[0].checkState() == Qt.Checked
  381. else:
  382. item.setCheckState(Qt.Unchecked)
  383. newItems.append(item)
  384. if self.allowMultiColumnPk:
  385. self.uniqueModel.clear()
  386. self.uniqueModel.appendColumn(newItems)
  387. self.uniqueChanged()
  388. else:
  389. previousUniqueColumn = self.uniqueCombo.currentText()
  390. self.uniqueModel.clear()
  391. self.uniqueModel.appendColumn(newItems)
  392. if self.uniqueModel.findItems(previousUniqueColumn):
  393. self.uniqueCombo.setEditText(previousUniqueColumn)
  394. uniqueIsFilled = True
  395. oldGeometryColumn = self.geomCombo.currentText()
  396. self.geomCombo.clear()
  397. self.geomCombo.addItems(cols)
  398. self.geomCombo.setCurrentIndex(self.geomCombo.findText(oldGeometryColumn, Qt.MatchExactly))
  399. # set sensible default columns if the columns are not already set
  400. try:
  401. if self.geomCombo.currentIndex() == -1:
  402. self.geomCombo.setCurrentIndex(cols.index(defaultGeomCol))
  403. except:
  404. pass
  405. items = self.uniqueModel.findItems(defaultUniqueCol)
  406. if items and not uniqueIsFilled:
  407. if self.allowMultiColumnPk:
  408. items[0].setCheckState(Qt.Checked)
  409. else:
  410. self.uniqueCombo.setEditText(defaultUniqueCol)
  411. def copySelectedResults(self):
  412. if len(self.viewResult.selectedIndexes()) <= 0:
  413. return
  414. model = self.viewResult.model()
  415. # convert to string using tab as separator
  416. text = model.headerToString("\t")
  417. for idx in self.viewResult.selectionModel().selectedRows():
  418. text += "\n" + model.rowToString(idx.row(), "\t")
  419. QApplication.clipboard().setText(text, QClipboard.Selection)
  420. QApplication.clipboard().setText(text, QClipboard.Clipboard)
  421. def initCompleter(self):
  422. dictionary = None
  423. if self.db:
  424. dictionary = self.db.connector.getSqlDictionary()
  425. if not dictionary:
  426. # use the generic sql dictionary
  427. from .sql_dictionary import getSqlDictionary
  428. dictionary = getSqlDictionary()
  429. wordlist = []
  430. for name, value in dictionary.items():
  431. wordlist += value # concat lists
  432. wordlist = list(set(wordlist)) # remove duplicates
  433. api = QsciAPIs(self.editSql.lexer())
  434. for word in wordlist:
  435. api.add(word)
  436. api.prepare()
  437. self.editSql.lexer().setAPIs(api)
  438. def displayQueryBuilder(self):
  439. dlg = QueryBuilderDlg(self.iface, self.db, self, reset=self.queryBuilderFirst)
  440. self.queryBuilderFirst = False
  441. r = dlg.exec_()
  442. if r == QDialog.Accepted:
  443. self.editSql.setText(dlg.query)
  444. def _getSqlQuery(self):
  445. sql = self.editSql.selectedText()
  446. if len(sql) == 0:
  447. sql = self.editSql.text()
  448. return sql
  449. def uniqueChanged(self):
  450. # when an item is (un)checked, simply trigger an update of the combobox text
  451. self.uniqueTextChanged(None)
  452. def uniqueTextChanged(self, text):
  453. # Whenever there is new text displayed in the combobox, check if it is the correct one and if not, display the correct one.
  454. checkedItems = [
  455. item.text()
  456. for item in self.uniqueModel.findItems("*", Qt.MatchWildcard)
  457. if item.checkState() == Qt.Checked
  458. ]
  459. label = ", ".join(checkedItems)
  460. if text != label:
  461. self.uniqueCombo.setEditText(label)
  462. def setFilter(self):
  463. from qgis.gui import QgsQueryBuilder
  464. layer = self._getSqlLayer("")
  465. if not layer:
  466. return
  467. dlg = QgsQueryBuilder(layer)
  468. dlg.setSql(self.filter)
  469. if dlg.exec_():
  470. self.filter = dlg.sql()
  471. layer.deleteLater()
  472. def setHasChanged(self, hasChanged):
  473. self.hasChanged = hasChanged
  474. def close(self):
  475. if self.hasChanged:
  476. ret = QMessageBox.question(
  477. self, self.tr('Unsaved Changes?'),
  478. self.tr('There are unsaved changes. Do you want to keep them?'),
  479. QMessageBox.Save | QMessageBox.Cancel | QMessageBox.Discard, QMessageBox.Cancel)
  480. if ret == QMessageBox.Save:
  481. self.saveAsFilePreset()
  482. return True
  483. elif ret == QMessageBox.Discard:
  484. return True
  485. else:
  486. return False
  487. else:
  488. return True